Grammar

Grammar#

The complete grammar, in the notation described at the top of the file. This is the source of truth for syntax, including the keyword and intrinsic lists — ./mk tokens checks the three syntax highlighters against it.

// ============================================================
// EB Language Grammar — authoritative for the eb-lang compiler
// ============================================================
//
// This is the merged grammar agreed 10 AUG 2026: it keeps `:=`
// declarations and the explicit `state` keyword from the original
// specification, and adopts `pi` (with period/offset), the long
// keywords `event`/`agent`/`resource`, and the two-section program
// structure from the later eb.ebnf draft in eichenbank-claude.
//
// Whitespace and newlines are not significant.
//
// A program has two sections separated by the contextual keyword
// `exec` (a bare word at top level):
//
//   - the MODEL section (before `exec`): declarations, in any
//     order; lexical order carries no meaning, and module binding
//     evaluation order is determined by dependency analysis.
//   - the EXEC section (after): scenario definitions and imperative
//     execution statements, in program order.
//
// A file with no `exec` section is a pure model. A purely
// imperative program is model bindings followed by `exec` and its
// statements (MODEL-20).

// ------------------------------------------------------------
// Program Structure
// ------------------------------------------------------------

program        -> modelDecl* ( "exec" execItem* )? EOF ;

modelDecl      -> binding                 // module constant
               |  stateDecl
               |  piDecl
               |  eventDecl
               |  agentDecl
               |  fnDecl
               |  resourceDecl
               |  groupDecl
               |  obsDecl
               |  stopStmt                // stop_s registration (SCHED-30)
               |  initDecl ;

execItem       -> scenarioDecl
               |  statement ;             // bindings are statements

// ------------------------------------------------------------
// Bindings
// ------------------------------------------------------------

// `:=` declares with an inferred type (DECL-10); `name :type = e`
// declares with an explicit type; `name :type` is a forward
// declaration completed by a later `=` assignment (TYPE-20); bare
// `=` assigns to an existing variable. Assignment context rules
// (which variables may be assigned where) are semantic.

binding        -> IDENTIFIER ":=" expression
               |  IDENTIFIER ":" type ( "=" expression )?
               |  IDENTIFIER "=" expression ;

stateDecl      -> "state" binding ;

// ------------------------------------------------------------
// Declarations
// ------------------------------------------------------------

// Period and offset attach to the keyword: pi(4) yearly, or
// pi(4, 1) with an explicit first-firing offset. Omitted period
// means 1 (evaluation every tick); omitted offset defaults to the
// period. In this version period and offset must be compile-time
// non-negative integer constants (period >= 1).
//
// OPEN ISSUE: non-integer (dbl) periods — e.g. pi(0.5) — are
// intended but unresolved; see "pi periods and offsets" in
// eb-semantics.rst (PI-15) for what a resolution must specify.
piDecl         -> "pi" period? declName ( ":" type )? ( ":=" | "=" ) piBody ;

period         -> "(" expression ( "," expression )? ")" ;

// A pi body is zero or more history initializers and body items,
// EXACTLY ONE result expression, then optional trailing items.
// The result expression is greedy (maximal munch). Trailing items
// (stop_r, halt, if, match, for, calls — not bindings) run after
// the result is committed; a bare self-reference there reads the
// value just computed rather than the prior period's.
piBody         -> historyInit* piItem* expression piTrailing* ;

piItem         -> binding
               |  matchStmt
               |  ifStmt
               |  forStmt
               |  haltStmt
               |  stopStmt
               |  callStmt ;

piTrailing     -> matchStmt | ifStmt | forStmt | haltStmt
               |  stopStmt | callStmt ;

historyInit    -> "[" ( "0" | "-" NUMINT ) "]" "=" expression ;

// Reserved for a future version: sampler declarations owning a
// named PRNG stream keyed (simseed, sampler, run) per DET-30.
// smDecl      -> "sm" IDENTIFIER ( ":" type )? "=" expression ;

// The parameter list may be omitted for a parameterless event or
// agent.
eventDecl      -> "event" declName ( "(" parameters? ")" )? block "end" ;

agentDecl      -> "agent" declName ( "(" parameters? ")" )? block "end" ;

// Return type is mandatory: fns exist to provide calculation.
// They allow no side effects — no state mutation, no scheduler
// interaction — but may draw from the random stream (sema).
fnDecl         -> "fn" IDENTIFIER "(" parameters? ")" ":" type block "end" ;

// Capacity is a non-negative integer unit count (TYPE-15, RC-30);
// this is the declaration and the initial capacity. A capacity of 0
// declares a resource that starts offline. Later changes use the
// :capacity intrinsic (RC-60) — a resource name is a handle, not an
// assignable variable.
resourceDecl   -> "resource" declName ( ":" "int" "=" | ":=" ) expression ;

initDecl       -> "init" block "end" ;

// Group declarations expand a name with index specifications
// (GRP rules); they apply to pi, state, resource, and agent
// declarations only (GRP-60).
declName       -> IDENTIFIER indexSpec* ;

indexSpec      -> "{" IDENTIFIER "|" range "}" IDENTIFIER? ;

range          -> expression ".." expression ;

groupDecl      -> piDecl | stateDecl | resourceDecl | agentDecl ;
                                          // with a declName containing
                                          // at least one indexSpec

parameters     -> parameter ( "," parameter )* ;

parameter      -> IDENTIFIER ":" paramType ;

// "rsc" is valid only as a parameter type (TYPE-15, RC-40), and
// only for fn and agent parameters.
paramType      -> type | "rsc" ;

type           -> "int" | "dbl" | "str" | "bool" ;

scenarioDecl   -> "scenario" IDENTIFIER binding* "end" ;

obsDecl        -> ( "obs_p" | "obs_i" | "obs_r" | "obs_s" )
                  ( binding | IDENTIFIER ( "," IDENTIFIER )* ) ;

// ------------------------------------------------------------
// Statements
// ------------------------------------------------------------

statement      -> binding
               |  runStmt
               |  schedStmt
               |  stopStmt
               |  haltStmt
               |  matchStmt
               |  ifStmt
               |  forStmt
               |  returnStmt
               |  callStmt
               |  groupAssign
               |  timeoutStmt
               |  claimStmt
               |  releaseStmt ;

// label may only be supplied together with a scenario (EXEC-20)
runStmt        -> "run" "(" ( IDENTIFIER ( "," expression )? )? ")"
                  "{" expression "," expression "}" ;

schedStmt      -> "sched" "(" expression "," IDENTIFIER ( "(" arguments? ")" )? ")" ;

// stop_r may only appear inside pi, event, agent, and init bodies
// (SCHED-20); stop_s only at model level (SCHED-30).
stopStmt       -> ( "stop_r" | "stop_s" ) "(" expression "," expression ")" ;

haltStmt       -> "halt" "(" expression ")" ;

matchStmt      -> "match" matchArm+ "end" ;

matchArm       -> "|" matchCond "->" statement+ ;

matchCond      -> expression | "_" ;

ifStmt         -> "if" expression "then" statement+
                  ( "else" statement+ )? "end" ;

forStmt        -> "for" IDENTIFIER "=" expression "," expression
                  ( "," expression )? "do" statement+ "end" ;

// the expression is mandatory: fns return a value
returnStmt     -> "return" expression ;

// Covers the two-argument set form of :capacity (RC-60); the
// one-argument read form is an expression (primary + call).
callStmt       -> IDENTIFIER "(" arguments? ")"
               |  groupRef "(" arguments? ")"
               |  INTRINSIC "(" arguments? ")" ;

groupAssign    -> groupRef "=" expression ;

timeoutStmt    -> "timeout" "(" expression ")" ;

// claim and release are independent statements, not a block: crossing
// resource lifetimes (hold A, acquire B, release A) are common in real
// models and a scope stack cannot express them (CLAIM-10, CLAIM-15).
claimStmt      -> "claim" "(" expression ")" ;

releaseStmt    -> "release" "(" expression ")" ;

block          -> statement* ;

arguments      -> expression ( "," expression )* ;

// ------------------------------------------------------------
// Expressions
// ------------------------------------------------------------

expression     -> guardExpr
               |  or ;

guardExpr      -> guardArm+ ;             // last arm must be `_` or `true`
                                          // (GUARD-30)

guardArm       -> "|" guardCond "->" expression ;

guardCond      -> expression | "_" ;

or             -> and ( "or" and )* ;

and            -> equality ( "and" equality )* ;

equality       -> comparison ( ( "==" | "!=" ) comparison )* ;

comparison     -> term ( ( "<" | "<=" | ">" | ">=" ) term )* ;

term           -> factor ( ( "+" | "-" ) factor )* ;

factor         -> power ( ( "*" | "/" | "//" | "%" ) power )* ;

power          -> unary ( "^" power )? ;

unary          -> ( "-" | "not" ) unary
               |  postfix ;

postfix        -> histAccess | call ;

histAccess     -> ( IDENTIFIER | groupRef ) "[" "-" NUMINT "]" ;

call           -> primary ( "(" arguments? ")" )? ;

// Reference to a group member (GRP-40, GRP-50). Each braced index
// is an int expression (e.g. source_{i%N+1}); when it cannot be
// resolved at compile time the compiler generates a runtime
// dispatch with a bounds check.
groupRef       -> IDENTIFIER ( "{" expression "}" IDENTIFIER? )+ ;

primary        -> NUMINT
               |  NUMDBL
               |  STRING
               |  "true" | "false"
               |  IDENTIFIER
               |  groupRef
               |  INTRINSIC
               |  "(" expression ")" ;

// ------------------------------------------------------------
// Lexical Grammar
// ------------------------------------------------------------

NUMINT         -> DIGIT ( "_"* DIGIT )* ;

NUMDBL         -> DIGIT ( "_"* DIGIT )* "." DIGIT ( "_"* DIGIT )* ;

STRING         -> '"' ( ESCAPE | <any char except '"' and '\'> )* '"' ;

ESCAPE         -> '\' ( 'n' | 't' | '\' | '"' | '0' ) ;

IDENTIFIER     -> ALPHA ( ALPHA | DIGIT )* ;

INTRINSIC      -> ":" IDENTIFIER ;

ALPHA          -> "a" ... "z" | "A" ... "Z" | "_" ;

DIGIT          -> "0" ... "9" ;

// Comments run from `#` to end of line.

// ------------------------------------------------------------
// Keywords
// ------------------------------------------------------------

// agent and as bool claim dbl do else end event export false fn
// for halt if import init int match not obs_i obs_p obs_r obs_s
// or pi release resource return rsc run scenario sched sm state
// stop_r stop_s str then timeout true
//
// `exec` is contextual: it is the section marker only as a bare
// word at top level (not followed by :=, :, =, `(`, or `{`), and
// remains usable as an identifier elsewhere.
//
// `sm`, `import`, `export`, and `as` are reserved for future
// versions and rejected by this compiler.

// ------------------------------------------------------------
// Intrinsics
// ------------------------------------------------------------

// :abs :boxplot :capacity :count :dump :exp :first :huge :last
// :log :max :mean :median :min :nhpp :nth :print :printf
// :probability :quantile :randb :rande :randi :randn :randp :randu
// :randseed :sqrt :sum :var

// ------------------------------------------------------------
// Context Values
// ------------------------------------------------------------

// :p   current period (int, 1-indexed)
// :i   synonym for :p
// :t   current event time (dbl)
// :r   current run number (int, 1-indexed)