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)