Semantic Rules#
This document is normative. Where it and spec/eb.ebnf disagree with
any older specification, this document wins. The executable suite in
tests/suite pins the behavior described here.
Summary of Rules
Agents │ AG-i │ Coroutines, spawn, local scope, timeout │
Resource Claims │ CLAIM-i │ Acquire, block, explicit release, wait queue │
Context Values │ CTX-i │ :p, :t, :r, read-only │
Declaration/ │ DECL-i │ := vs =, redeclaration │
Assignment │ │ │
Determinism │ DET-i │ Overall simulation determinism │
Events │ EV-i │ Atomic execution, scheduling, local scope │
Execution Model │ EXEC-i │ Program sections, run(), independence, reset │
Functions │ FN-i │ Zero-time, no side effects, local scope │
Group Decls │ GRP-i │ Expansion, index scope, compile-time vs runtime │
Guards │ GUARD-i │ Expressions, statements, default arm, │
│ │ type consistency │
History Access │ HIST-i │ [-k] notation, zero defaults, explicit init │
If Statements │ IF-i │ if/else, bool condition, sugar for match │
Init Block │ INIT-i │ Per-run entry point, singleton │
Intrinsics │ INTR-i │ Naming, RNG, stats, math, I/O, boxplot, huge, │
│ │ NHPP │
Periodic Items │ PI-i │ Per-period eval, declaration order, immutability│
Import/Export │ MOD-i │ RESERVED — not implemented in this version │
Model │ MODEL-i │ Model overview │
Observations │ OBS-i │ obs_p/i/r/s, aggregation │
Resources │ RC-i │ Capacity, availability, rsc type, reset │
Scenarios │ SCEN-i │ Assignment-only, inheritance, application │
Scheduling/ │ SCHED-i │ sched, stop_r, stop_s, halt │
Control │ │ │
Scoping │ SCOPE-i │ Modules, execution units, no shadowing │
State Variables │ STATE-i │ Persistence, event-only mutation │
Timing │ TIME-i │ Periods vs events, hybrid ordering, FIFO, │
│ │ IEEE 754 │
Types │ TYPE-i │ Primitives, promotion, division, operators, rsc │
Zero Values │ ZERO-i │ Default initialization │
Rule prefixes are historical in two cases: PI-i rules were LI-i
when periodic items were called lineitems (li), and EV-i rules
were EH-i when events were called event handlers (eh). The
prefixes were renamed with the keywords; AG-i and RC-i already
matched agent and resource.
Agents#
- AG-10: An agent (
agent) defines a named process template. Invoking an agent by name with arguments spawns a new independent execution instance scheduled to begin at the current simulation time.
- AG-15: The parameter list may be omitted for a parameterless agent:
agent workeris equivalent toagent worker().- AG-20: Agents execute as interleaved processes with coroutine-like
semantics: an agent runs until it reaches a suspension point (
timeoutor a blockingclaim), at which point it suspends and other scheduled events may execute. The suspension mechanism is an implementation choice, not part of the language.- AG-22: An agent body that contains neither
timeoutnorclaimnever suspends: once dequeued, it runs to completion in zero simulation time. It is not equivalent to a function call: its execution is deferred until the spawning context completes its current atomic step or reaches a suspension point (see AG-30), and unlike a
fnit may mutate state variables. Spawning such an agent is a legitimate way to defer a zero-time action into the current time’s FIFO queue.- AG-25: Agents may be spawned from the
initblock, from events, and from other agents. Spawning an agent from a
fnorpibody is a compile-time error (consistent with the no-side-effect rule for functions, FN-30, and the purity of periodic items, PI-40).- AG-30: Spawning an agent schedules it at the current simulation time and
returns immediately; the spawning context does not wait for the spawned agent to complete. Multiple instances of the same agent may be active simultaneously.
- AG-40: Variables declared inside an agent are local and ephemeral. They
are discarded when the agent terminates.
- AG-50: Agents may mutate state variables, call functions (
fn), spawn other agents, and use
timeoutandclaim. The same mutation rules as events apply: onlystatevariables may be mutated, not module-level variables.- AG-55: An agent body may not read the value of a periodic item. Bridge
through a state variable or an observation instead.
- AG-60: Agents do not return values. An agent invocation is a statement,
not an expression.
- AG-70: An agent that reaches the end of its body terminates silently. No
event is fired on termination. Units of any resource still held by the agent are reclaimed at termination, and a resource
scanfollows (CLAIM-70).- AG-80:
timeout(dt)suspends the current agent fordttime units, rescheduling it to resume at
current_time + dt.dtmust be non-negative; a negative value is a compile-time error when constant, a run-time error otherwise.timeoutis valid only inside anagentbody orinitblock; using it in anevent,fn, orpiis a compile-time error.
Resource Claims#
- CLAIM-10:
claim(res)acquires one unit of resourceres. It is a statement, not a block: the unit is held until an explicit
release(res), or until the holding agent terminates (AG-70).- CLAIM-15:
release(res)returns one unit ofresheld by the current agent. Releasing a resource the agent does not hold is a runtime error — one lookup that catches double release, release without claim, and release of another agent’s unit alike.
- CLAIM-17: The rationale for separating the two is that resource lifetimes
cross. Hand-over-hand acquisition — hold A, acquire B, release A — is common in real models, and a scope stack cannot express it. The cost is that holding is no longer bounded by syntax; see CLAIM-70.
- CLAIM-20: If a unit of
resis available whenclaimis reached, the agent acquires it immediately and continues without suspending.
- CLAIM-30: If no unit is available, the agent suspends and is placed in a
wait queue for
res. Waiting agents are resumed in FIFO order as units become available. A reduction in capacity does not disturb the queue (RC-70).- CLAIM-40:
claimandreleaseare valid only inside anagentbody or
initblock; using either in anevent,fn, orpiis a compile-time error.- CLAIM-50: An agent may hold units of several resources at once, acquired and
released in any order. Deadlock from circular waiting is possible and is the programmer’s responsibility to avoid.
- CLAIM-60: A
claimis a suspension point and therefore cuts an agent into segments; a
releaseis not and does not (see AG-20). Areleasedoes make units available, so the segment containing it has a resourcescanappended — the same treatment a capacity raise receives (RC-60).- CLAIM-70: An agent that terminates while still holding units returns them at
termination (AG-70). Without that rule an agent leaking a unit would remove capacity permanently and block every waiter in silence.
Context Values#
CTX-10: :p is the current period (int, 1-indexed).
CTX-15: :i is a synonym for :p.
CTX-20: :t is the current event time (dbl).
CTX-30: :r is the current run number (int, 1-indexed).
- CTX-40: Context values are read-only. Assignment to a context value is a
compile-time error.
Declaration and Assignment#
- DECL-10:
:=declares a new variable.=assigns to an existing variable. These are distinct operations.
- DECL-20: A variable must be declared exactly once. Redeclaration is a
compile-time error.
DECL-30: Assignment to an undeclared variable is a compile-time error.
- DECL-40:
:=must not be used after a variable’s initial declaration. name :type = edeclares with an explicit type;name :typeforward-declares, completed by a later=.
Determinism#
- DET-10: Given identical module code, identical inputs, identical scenario,
and identical random seed, simulation execution is deterministic. All observable results are reproducible.
- DET-20: Within a run, event handling and periodic evaluation are
single-threaded and strictly sequential. No intra-run parallelism is permitted.
DET-30: Runs may be executed in parallel without affecting determinism.
Each simulation instance has a simulation seed (simseed). The value of simseed is either explicitly set by the user via
:randseed()or generated by the runtime at simulation start.For run index r (1-indexed, in program order of the
run()statement), the run-specific seed is:runseed(r) = (simseed + r * 2654435761) mod 2^64The PRNG is xoshiro256**. Its state at the start of run r is obtained by seeding splitmix64 with runseed(r) and taking four successive splitmix64 outputs as the initial xoshiro256** state.
This derivation is a pure, random-access function of (simseed, r): a run’s starting state does not depend on execution order, scheduling, or how many random numbers other runs consumed. Every conforming implementation — VM, native, or GPU — must use exactly this generator and derivation, so that identical seeds yield bit-identical streams on every backend (DET-10).
Events#
- EV-10: An event (
event) is an entry point into the simulation kernel. It executes atomically when dequeued from the event queue.
- EV-15: The parameter list may be omitted for a parameterless event:
event tickis equivalent toevent tick().- EV-20: Only
event-declared units may be scheduled withsched(). Passing a
fntosched()is a compile-time error. Arguments may be passed at schedule time:sched(dt, handler(a, b)). They are evaluated immediately and stored in the event record. When the handler fires, they are bound to its declared parameters in order, as if called with those values.- EV-25: Direct invocation of an event by name is a compile-time error. An
event enters the simulation only through
sched().- EV-30: An event may mutate state variables, call helper functions
(
fn), declare and modify local variables, execute statements, schedule further events, spawn agents (see AG-25), and change resource capacity (RC-60).- EV-35: An event body may not read the value of a periodic item. Bridge
through a state variable or an observation instead.
- EV-40: Variables declared inside an event are local and ephemeral. They
are discarded when the handler returns.
Execution Model#
- EXEC-05: A program has two sections: model declarations, then the
contextual keyword
exec, then execution items (scenarios and statements) in program order. Statements — includingrun(),sched()registrations, and:printcalls — are valid only afterexec. A file withoutexecis a pure model.execis contextual: it marks the section only as a bare word at top level, and remains usable as an identifier elsewhere.- EXEC-08: Model-section declarations may appear in any order. Module
binding initializers are evaluated in dependency order; a cycle among them is a compile-time error.
- EXEC-10:
run(scenario, label) {duration, nruns}initiates a simulation. scenarioselects parameter values;labelidentifies output.durationis the maximum simulation time per run (or:hugefor a run with no time horizon; see EXEC-15);nrunsis the number of independent runs. For pi-only programs,durationequals the number of periods (each period advances time by 1). For programs with events or agents, the run ends when simulation time would exceeddurationor the event queue is empty, whichever comes first.- EXEC-15:
durationmay be:huge(positive infinity, INTR-75). Such a run has no time horizon: it ends when a
stop_rfires (SCHED-20),haltexecutes (SCHED-40), or the event queue is empty, whichever comes first.- EXEC-16: A no-horizon pi model cannot end via queue exhaustion (see
TIME-45), so it must be able to stop: if the duration expression is the compile-time constant
:hugeand the model containspideclarations but nostop_rorhalt, it is a compile-time error. When the duration is not compile-time constant, termination is the programmer’s responsibility.- EXEC-20:
scenarioandlabelare optional. Ifscenariois omitted, module-level default values are used. A
labelmay only be supplied together with a scenario.- EXEC-30: Each run is independent. All pi values, state variables, local
variables, and resource availability are reset to their initial values at the start of each run. Any agent instances active at run end are discarded. The PRNG is re-seeded at the start of each run with a run-specific seed derived deterministically from the simulation seed and the run index (see DET-30).
- EXEC-32: Module-level values that are set for a scenario are kept for the
duration of the run.
- EXEC-35:
:randseed(seed)is valid only as an exec statement, at most once per program; a second occurrence is a compile-time error. It sets the simulation seed used by subsequent
run()statements, so two identicalrun()statements after one:randseedproduce identical results (DET-30, common random numbers).- EXEC-36: An exec-level
sched(dt, handler(args))registers an initial event: it is applied at the start of every run of every subsequent
run()statement, in program order, afterinit(INIT-20).- EXEC-40: Within a run, periodic items evaluate once per period in
declaration order (see PI-10). Observations are recorded at the boundaries defined by their type (see OBS rules).
- EXEC-45: After a
run()completes, exec statements may read a pi’s or state variable’s final value directly (
:print(x)), in addition to the observation-based intrinsics of INTR-40. Assignment at exec level reaches exec locals and module globals only.
Functions#
- FN-10: A function (
fn) is a synchronous, zero-time computation. It does not advance simulation time.
- FN-20: A function may have zero or more parameters; any parameters it has
must be explicitly typed.
- FN-25: The return type is mandatory: functions exist to provide
calculation, and must return a value of the declared type.
returnis valid only in afnbody and requires an expression.- FN-30: A function may not mutate state variables. A function may not
call
sched(),stop_r(),stop_s(), orhalt().- FN-32: A function body may not read state variables. Functions see their
parameters and module values only; pass state as an argument.
- FN-35: A function body may not reference a periodic item by name. To use
a pi’s value inside a function, the caller must pass it as an argument and the function must declare a corresponding typed parameter.
- FN-40: Functions may be called from periodic items, events, agents, and
other functions.
FN-50: Variables declared inside a function are local and ephemeral.
Group Declarations#
- GRP-10: A group declaration defines N entities with independent state and a
single shared body. The body is compiled once as parameterized bytecode in which the index variable is a bound parameter. The compiler allocates one state record per entity (history slots and current value); it does not emit N copies of the body. At evaluation time the shared body executes once per entity, with the index bound to that entity’s value. Memory cost is proportional to N × (state per entity), not N × (body size), making large N (millions or more) practical.
- GRP-20: The range bounds must be compile-time constant integer
expressions. The lower bound must be less than or equal to the upper bound. A range with equal bounds (
1..1) expands to a single declaration.- GRP-30: The index variable is in scope throughout the declaration body,
including any history-init clauses. It is not visible outside the declaration.
- GRP-40: Within the body, any reference to the declared group name that
includes an index in braces (e.g.,
voter_{id}) is a group member reference. The braced index is a fullintexpression (e.g.,source_{i%N+1}[-1]); when it can be resolved at compile time the compiler substitutes the concrete expanded name. The{...}wrapper is required; barevoter_idis a separate identifier and is not a group member reference.- GRP-50: When the index cannot be resolved at compile time the compiler
generates a runtime dispatch over the expanded names with a bounds check. An index outside the declared range at runtime halts the run with an out-of-range error.
- GRP-60: Group declarations may be applied to
pi,state, resource, andagent. Group declarations are not permitted foreventorfn.- GRP-70: Expanded names must not overlap with each other or with any
ordinary declaration in the same scope. A group declaration whose range would produce a name already declared is a compile-time error.
- GRP-80: A multi-dimensional group declaration (two or more index specs,
e.g.,
node_{r|1..R}_{c|1..C}) expands to the Cartesian product of all index ranges. All index variables are simultaneously in scope within the body. Expansion is lexicographic: innermost (rightmost) index increments first.
Guards#
- GUARD-10: A guard expression evaluates to a value. Guard arms are evaluated
top-to-bottom; the first arm whose condition is true determines the value.
- GUARD-20: A guard statement (
match ... end) executes the statements of the first arm whose condition is true.
- GUARD-30: A guard expression must include a terminating arm with condition
trueor_(wildcard). Omitting the default arm is a compile-time error. This is required because a guard expression must produce a value for every possible input.- GUARD-35: A guard statement (
match ... end) does not require a default arm. If no arm’s condition is true, execution continues after
endwith no effect.- GUARD-40: Guard-expression arms may mix
intanddbl; all arms then widen to
dbl. Any other type mix across arms is a compile-time error.- GUARD-50: Since there is no notion of an expression that does assignment,
guard expressions are side-effect free.
History Access#
- HIST-10:
name[-k]refers to the value of periodic itemnamefrom kevaluations ago, wherekis a positive integer literal.name[-1]is the value from the immediately preceding evaluation. For a pi with periodp > 1,[-k]counts evaluations, not ticks (PI-15).- HIST-20: History access is available for periodic items only. Applying
[-k]to a non-pi variable is a compile-time error.- HIST-30: When a historical reference reaches before the first evaluation
(e.g.,
x[-1]at period 1), the value is the zero value for the pi’s type (see ZERO-10), unless an explicit history initialization has been provided.- HIST-40: Explicit history initialization uses the form
[offset] = expressionat the start of a pi body. Offsets are0or negative integer literals.- HIST-50: Within a pi body, a bare reference to the pi’s own name (without
[-k]) implicitly refers to its value from the immediately preceding evaluation (equivalent toname[-1]). To access two evaluations ago,name[-2]is required explicitly. A bare reference to a different pi always refers to that pi’s value for the current period (subject to declaration order; see HIST-60). Within a pi body, any k=0 reference into the pi’s own group reads the prior evaluation — deterministic regardless of member order. In a trailing item (PI-25) a bare self-reference reads the value just committed.- HIST-60: A pi may reference its own prior values or the prior values of
other pis, subject to declaration order: a pi may only reference pis declared before it for the current period’s value, but may reference any pi’s prior period values.
- HIST-70: Historical references refer to prior-period values and therefore
do not impose declaration-order constraints.
If Statements#
- IF-10:
if cond then S endexecutes the statements inSif and only if
condevaluates totrue. Ifcondis false, execution continues afterendwith no effect.- IF-20:
if cond then S1 else S2 endexecutesS1ifcondis true, otherwise executesS2.- IF-30: The condition expression must be of type
bool. A non-bool condition is a compile-time error.
IF-40: if is syntactic sugar for match:
if cond then S end≡match | cond -> S end
if cond then S1 else S2 end≡match | cond -> S1 | _ -> S2 endThere is no
elif; usematchfor multi-branch conditionals.
Init Block#
- INIT-10: The
initblock is a singleton declaration; at most oneinit block may appear per program.
- INIT-20: The
initblock executes once per run, at time t=0, before any other events. It is the primary entry point for spawning the initial population of agents.
- INIT-30: The
initblock has the same capabilities as an agent body: it may spawn agents, call functions, use
timeoutandclaim, declare local variables, mutate state variables, and change resource capacity (RC-60).- INIT-40: State variables and resources are reset to their declared default
values before
initexecutes (see EXEC-30).
Intrinsics#
- INTR-10: Intrinsic functions use the
:nameprefix and are provided by the runtime. They are not user-definable. The complete set is listed in
eb.ebnf.- INTR-20: A user may declare a variable or function with the same name as an
intrinsic (without the colon prefix) without conflict.
- INTR-30: Random number intrinsics (
:randu,:randn,:rande, :randp,:randb,:randi) draw from their respective distributions.:randseed(seed)sets the PRNG seed for reproducibility (EXEC-35).- INTR-31: RNG intrinsics are valid in
pi,event,agent,fn, and
initbodies only. MODEL-20’s imperative intrinsic subset has no RNG.- INTR-32: The RNG algorithms are normative, so that every backend produces
identical streams (DET-10):
u01 (next_u64() >> 11) * 2^-53 :randi(lo, hi) inclusive; lo + next_u64() mod (hi-lo+1) :randn Box–Muller, cosine branch, exactly two uniform draws :rande(m) -m * ln(u) :randp Knuth's method
- INTR-40: Statistical intrinsics (
:mean,:median,:quantile, :probability,:min,:max,:count,:sum,:var) operate on observation data. They are meaningful inobs_randobs_sdeclarations, where they aggregate over the observations collected at finer granularities. After arun()completes, they may also be applied to observation variables in ordinary statements, where they aggregate over the observation data of the most recently completed simulation.- INTR-41: Statistical details are normative:
:quantileuses linear interpolation (R type 7);
:varis the n−1 sample variance;:meanalways returnsdbl;:sumreturns the element type;:probability(x)takes aboolobservation series and returns the fraction of true values.- INTR-42: Series extraction intrinsics
:first,:last, and:nth return a single element of an observation series.
:nth(i, x)returns thei-th value of observation variablex; indices are 1-based, matching:pand:r(CTX-10, CTX-30).:first(x)is equivalent to:nth(1, x);:last(x)is equivalent to:nth(:count(x), x). The result type is the element type of the series. An index outside1..:count(x)is a runtime error. Context rules follow INTR-40.- INTR-50: Math intrinsics (
:abs,:log,:exp,:sqrt) operate on numeric values. They accept
intordbland returndbl.- INTR-55: Some intrinsics can be either statistical or vararg (
:max(...), :min(...)). In the vararg form they operate on a variable number of numeric arguments. They acceptintordbl. If the arguments mix types the result isdbl; otherwise the result takes the type of the arguments.- INTR-60:
:printwrites its arguments to stdout.:printfwrites formatted output to stdout using a subset of C-style format strings.
INTR-62: :print formats values according to their type:
int : no decimal point (42, not 42.0)
dbl : always includes decimal (5.0, not 5)
str : without quotes
bool : true or false
- INTR-64:
:printof a computed observation variable (one declared with obs_x name := expr) prints its series. Let N be the number of values. When N <= 100 all values are printed, comma-space separated, enclosed in square brackets, each element formatted per INTR-62:[1, 2, 3, 4, 5]
When N > 100, the first four and last four values are printed with an ellipsis between them, followed by the count:
[1, 2, 3, 4, ..., 98, 99, 100, 101] (101 values)
A single-value series prints as the bare value.
:printof a declared symbol (pi, state, global) always prints that symbol’s current or final scalar value, even when the symbol is observed; use:dumpto export its series. This format is normative and identical across implementations (DET-10).:printfhas no format specifier for series; passing an observation series to:printfis a compile-time error.- INTR-66:
:dump(x)writes every value of observation seriesxto stdout with no truncation, one value per line, each formatted per INTR-62. It is the intended way to export a full series — even millions of values — for external analysis. Context rules follow INTR-40.
- INTR-70:
:boxplotreturns a string that is an ASCII box-plot summary of observation data.
- INTR-75:
:hugereturns positive infinity (IEEE 754+∞) as adbl. It is the canonical sentinel for “no further arrivals” when used with
:nhpp, and for any other computation that needs to represent an unbounded value. EB literal syntax does not support infinity directly, so:hugeis the standard way to name it.- INTR-80:
:nhpp(start0, rate0, start1, rate1, ...)returns the next interarrival time (
dbl) for a Non-Homogeneous Poisson Process given a piecewise-constant rate schedule.- INTR-81: The schedule is expressed as a flat list of
(start, rate) pairs, all
dbl, in ascending order ofstarttime. Each pair defines a segment: raterateapplies fromstartuntil the next segment’sstart(or forever, for the last segment). At least one pair is required. An odd number of arguments is a compile-time error.- INTR-82:
:nhppreads the current event time (:t) internally. It draws one unit of exponential arrival potential and walks the schedule segments forward from
:tuntil that potential is consumed, returning the elapsed time to the arrival point. Segments that ended before:tare skipped. Segments within which:tfalls are entered at:t, not at theirstart.- INTR-83: A segment with
rate = 0contributes no arrival potential. Potential is not consumed while traversing it. A zero-rate segment therefore acts as a gap during which no arrivals can occur.
- INTR-84: If the schedule is exhausted before arrival potential is consumed,
:nhppreturns positive infinity — the value of:huge(see INTR-75). The caller is responsible for treating this as “no further arrivals” — typically by not scheduling the next event.- INTR-85:
:nhppis meaningful only when event time is active (i.e., called from within an
event, or from afncalled by anevent). Calling it during pi evaluation — where:treflects period boundaries, not event time — produces results that are technically defined but unlikely to be meaningful.
Periodic Items#
- PI-10: A periodic item (
pi) is evaluated once per period, in declaration order.
- PI-15:
pi(p) namefires everypticks;pi(p, d)first fires at t = d. Defaults arep = 1andd = p. In this versionpanddmust be compile-time integer constants, withp >= 1andd >= 0.OPEN ISSUE — non-integer periods
Sub-tick periods (
pi(0.5) value) are rejected in this version. With integer periods every pi rides one integer-tick period driver, which pins down three things thatdblperiods leave open: same-instant ordering across period classes (PI-10/HIST-60 rely on declaration order, but whether two firing times coincide at all becomes a floating-point question); the relationship to the period counter, since:p,obs_precording, and duration counting are defined at integer times (TIME-15, OBS-15); and drift-free firing times under TIME-50, which requires times to come from multiplication (d + k*p), never accumulation. The kernel is ready — each (period, offset) class can be its own self-rescheduling activation — what is missing is the semantic specification.- PI-20: A pi body consists of optional history initializations, optional
local declarations and assignments, and a mandatory terminating expression. The value of that expression becomes the pi’s value for the current evaluation.
- PI-25: Keyword statements and calls may follow the terminating expression
(
stop_r,halt,if,match,for, call statements — not bindings). These trailing items belong to the pi and run after the value is committed, which gives the idiomatic per-period stop check:pi x :int = x + 1 stop_r(x >= 3, "done")
- PI-30: Local variables declared inside a pi body are ephemeral — they
exist only for the current evaluation.
- PI-40: A pi may not be assigned to. Its value is derived, not stored. It
is a compile-time error to use
=on a pi name outside its own body.- PI-50: In a pi model, state is implicit: it is the sequence of values
produced by all pis over all periods. Once produced, a pi’s value for a period is immutable.
Import and Export#
RESERVED
import, export, and as are reserved words rejected by this
version of the compiler. The MOD-i rules below record the intended
semantics and are not normative until modules ship.
- MOD-10:
exportmakes a module-level symbol visible to other modules. Exportable symbols include variables, state variables, periodic items, and functions.
- MOD-20:
import module_namemakes all exported symbols of module_nameaccessible via qualified names (module_name.symbol).
MOD-30: import module_name as alias provides an alternative qualifier.
- MOD-40: Imported non-state symbols are read-only. Assignment to an
imported non-state variable is a compile-time error.
- MOD-50: Imported state variables may be mutated, but only from within an
event or agent in the importing module (see STATE-40).
- MOD-60: Mutation of imported state variables occurs in the same atomic
step as local state mutation, that is, immediately in the event.
- MOD-70: Importing a pi provides access to its current and historical
values. The importing module may not redefine the pi’s computation.
- MOD-80:
agent,resource, andinitdeclarations are module-private and may not be exported. Cross-module interaction with a module’s agent processes happens through its exported state variables.
Model#
MODEL-10: EB programs are either imperative programs or simulations.
- MODEL-20: EB imperative programs have varDecl, fnDecl, statements, and a
subset of intrinsics (output, math, and those statistical intrinsics that have vararg forms). Execution consists of executing the statements from top to bottom, once. A purely imperative program is bindings,
exec, and statements.- MODEL-30: EB simulation programs have one or more
pi,event, and/or agentdeclarations, one or more observations, and one or morerun(). Execution consists of an ordered sequence of runs. Within each run, execution proceeds by alternating between period evaluation, event handling, and agent execution according to the Timing rules.- MODEL-35: Agent-based simulation programs use
agentandresource declarations for process-oriented modeling. Agents execute as coroutines and interact through shared resources. An
initblock fires at the start of each run to spawn the initial population of agents.
Observations#
- OBS-10:
obs_p(synonym:obs_i) records the value of a variable at the end of each period. Over a run, this produces P (number of periods) values. At the start of a new run all previous values are discarded and that memory reused to record P values for this run. At the end of the simulation only the values from the last run are available. At no point are P × R values (periods × runs) produced.
- OBS-15: Periods exist in every simulation, including models with no
periodic items: the period counter advances at each integer time step up to
duration(see TIME-15), soobs_precords at each period boundary regardless of model style. In a no-horizon run (EXEC-15), the period counter advances at each integer time step reached before the run ends; the number ofobs_pvalues is therefore not known in advance.- OBS-20:
obs_rrecords a value at the end of each run. It produces R values.
- OBS-30:
obs_srecords a value at the end of the simulation. It produces one value.
- OBS-40: An observation declaration may either name existing variables or
declare a new computed variable:
obs_p x, y # observe existing variables obs_r avg := :mean(x) # declare and observe a computed value
- OBS-50: Statistical intrinsics in
obs_randobs_sdeclarations aggregate over the observation data collected at finer granularities (
obs_raggregates over period observations;obs_saggregates over run observations).- OBS-60: Aggregation for
obs_roccurs at the end of the run, before obs_pdata are discarded.
Resources#
- RC-10: A resource (
resource) is a named synchronization primitive with an integer capacity.
resource teller := 1is the declaration and the initial capacity; the capacity may change during a run through:capacity(RC-60).- RC-20: A resource tracks the number of currently held units. Available
units at any point equal
capacity − held. Initially all units are available.- RC-30: A capacity — declared or set — must be a non-negative integer.
A negative capacity is a compile-time error when constant, a run-time error otherwise. Zero is legal: the resource is offline, it drains, and it admits nobody until a later raise.
- RC-40: A resource variable has type
rsc. It may be passed as an argument to agents and functions that declare a parameter of type
rsc. Resources are passed by reference: all callers sharing the samerscvalue contend for the same pool of units.- RC-50: Resources are reset to fully available at the start of each run,
and the declaration’s capacity expression is re-evaluated then.
- RC-60:
:capacity(r, n)sets the capacity of resourcerton. The value is absolute, not a delta: after
:capacity(teller, 2)the resource has 2 units regardless of its previous capacity.:capacity(r)is the matching read form, returning the current capacity as anint; relative schedules are written:capacity(t, :capacity(t) + 1).ris a resource name, a group member reference, or anrscparameter;nmust beint.:capacityis valid inpi,event,agent, andinitbodies — unlikeclaimit never suspends, so it is not restricted to agents. It is not valid in afnbody (no side effects) or at exec level (resources exist only for the duration of arun()).A capacity raise increases availability, so any segment that may execute a
:capacityset has a resourcescanappended. Becausenis a general expression the compiler cannot in general tell a raise from a reduction, so the rule is syntactic and conservative: presence of a set form in the segment, not its direction. A pure reduction therefore costs one no-op scan.- RC-70: Drain, never preempt. Units already held are never revoked.
Lowering capacity below the number held is legal and leaves the resource over-subscribed (
held > capacity); that is a normal transient state, not an error. New claims keep failing theheld < capacitytest until enough releases land. Waiters are untouched by a reduction: they stay queued in FIFO order and are granted by thescanthat follows a later raise.Note
When resource observables are added, utilization under a time-varying capacity is a time-weighted ratio against a varying denominator — not
held/capacityaveraged. Do not define a resource observable that assumes a constant denominator.
Scenarios#
SCEN-05: Scenario application happens once per run, before period 1.
- SCEN-10: A
scenarioblock contains only assignments (=) to variables already declared at module level. Declarations inside a scenario are not permitted.
- SCEN-20: All scenarios implicitly inherit from the module-level default
values. A scenario overrides only the variables it explicitly assigns.
- SCEN-30: When
run()is invoked with a scenario, the scenario’s assignments are applied before execution begins. Module-level variables not overridden retain their default values.
- SCEN-40: Module defaults are restored and the scenario applied before the
run()duration and nruns expressions are evaluated — once perrunstatement, and again before each run (EXEC-30, EXEC-32). Accordingly, if a scenario assigns a value to a variable referenced in{duration, nruns}, the assigned value determines the number of periods and runs.- SCEN-50: A scenario may not assign a variable used in a group range: the
range was fixed at compile time (GRP-20).
Scheduling and Control#
- SCHED-10:
sched(delta_t, handler)placeshandleron the event queue at time
current_time + delta_t.delta_tmust be non-negative.- SCHED-12:
sched(delta_t, handler(args...))evaluates each argument expression at schedule time and stores the values in the event record. When the event fires the values are bound to the handler’s declared parameters in positional order. The number of supplied arguments must equal the number of declared parameters; a mismatch is a compile-time error.
- SCHED-15: If
delta_t < 0, it is a compile-time error when constant; otherwise a run-time error.
- SCHED-20:
stop_r(condition, message)ends the current run when conditionis true. It is imperative: the condition is evaluated when the statement executes; nothing is registered for later re-evaluation. When the condition is false, execution continues with no effect.stop_ris valid only insidepi,event,agent, andinitbodies;stop_rat module level is a compile-time error. (Astop_rin a pi trailing item re-executes each period as part of normal evaluation — the idiomatic per-period stop check, PI-25.)- SCHED-22: Timing of the stop. When a true
stop_rexecutes inside a pi body, the run ends at the end of the current period: remaining pis evaluate and
obs_precords before the run ends. Inside an event, agent, orinitblock, the run ends when the enclosing handler or agent segment completes its atomic step; events remaining in the event queue (including same-time FIFO entries) are discarded.- SCHED-24: A run ended by
stop_ris a completed run:messageis recorded, end-of-run processing occurs (
obs_rvalues are recorded per OBS-60), and if additional runs remain, execution continues with the next run.- SCHED-30:
stop_s(condition, message)registers a check evaluated at the end of each run, after
obs_raggregation (OBS-60), so the condition may reference observation data collected so far. Whenconditionis true, the simulation ends andmessageis recorded; no further runs execute.stop_sis valid only at model level; using it inside api,event,agent,fn, orinitbody is a compile-time error.- SCHED-40:
halt(message)immediately terminates the current run and the simulation.
messageis written to stderr.
Scoping#
- SCOPE-10: Each source file defines a module. Declarations at module
level are visible throughout the module and private unless explicitly exported.
- SCOPE-20:
pi,event,agent,fn, and theinitblock each introduce an execution unit with its own lexical scope.
- SCOPE-30: Variables declared inside an execution unit are local to that unit.
They exist only for the duration of the unit’s execution.
- SCOPE-40: Shadowing is prohibited. It is a compile-time error to declare a
variable whose name matches any name in an enclosing scope, including module-level variables, state variables, functions, periodic items, and imported symbols.
- SCOPE-50:
forloop variables are local to the loop body and follow the same no-shadowing rule.
State Variables#
- STATE-10: A
statevariable is declared with thestatekeyword and is persistent across event firings within a run. The effect discipline stays declared, not inferred.
- STATE-20: State variables may only be mutated (assigned with
=) inside events, agents, or the
initblock. Assignment to a state variable outside anevent,agent, orinitblock is a compile-time error.- STATE-30: State variables form the Markov state of an event-driven or
agent-based model (that is, a model with
eventoragentdeclarations). They do not exist in pure pi models (models with neithereventnoragentdeclarations) — see PI-50.- STATE-40: State variables are the only variables whose values may be mutated
by an importing module, and only from within an
eventoragentin that module. (Reserved with the MOD-i rules.)
Timing#
- TIME-10: EB supports two notions of time: periods (integer, discrete,
advancing by 1) and event time (double, continuous, advancing according to
sched()). Both are unit-less.- TIME-15: Period
pcorresponds to simulation timet = p(periods are 1-indexed; period boundaries fall on integer times). Time
t = 0precedes period 1; theinitblock executes att = 0(INIT-20), before period 1 evaluation and before any events.- TIME-20: In hybrid models, period-based execution precedes event handling
when a period boundary and an event share the same numeric time.
- TIME-30: Events are ordered by
(scheduled_time, insertion_order). When multiple events share a scheduled time, they are dequeued in FIFO order.
- TIME-40: All pi periods are considered scheduled at the start of a run. As
a consequence of FIFO ordering, pis for a given period execute before any events scheduled at the same time.
- TIME-45: Period boundaries are not events: pending period boundaries do not
count as queue contents for the queue-empty test (EXEC-10, EXEC-15). In a no-horizon run, pi periods are scheduled indefinitely — pis continue to evaluate each period until a
stop_rorhaltends the run. In a model with no pis, period boundaries alone do not keep the run alive.- TIME-50: There is no epsilon or tolerance for time comparisons. Equality
between period values and event times is exact under IEEE 754.
- TIME-60: Events scheduled with
delta_t = 0go into the FIFO for the current period. Since scheduling is FIFO, such events are handled after any other pis or events already scheduled.
- TIME-70: The scheduler pulls the next item from the FIFO only after the
completion of a pi evaluation or an event.
Types#
TYPE-10: EB has four primitive types: int, dbl, str, bool.
- TYPE-15:
rscis an opaque reference type for resources (see RC-40). It is valid only as the declared type of a
fnoragentparameter. Values of typerscare created only byresourcedeclarations;rscmay not appear in variable declarations, forward declarations, return types, or pi type annotations.- TYPE-20: Every variable has a type fixed at declaration time. Types are
either inferred from the initializer (with
:=), stated explicitly (with:type =), or given by an explicit forward declaration (:type).- TYPE-30:
intis implicitly promoted todblin any binary arithmetic operation where the other operand is
dbl. The result type of such an operation isdbl.- TYPE-40:
dblis never implicitly narrowed toint. Assigning a dblexpression to anintvariable is a compile-time error.- TYPE-50:
/(division) ALWAYS producesdbl, regardless of operand types. If either operand is
int, it is promoted todblbefore the division.- TYPE-60:
//(integer division) producesintwhen both operands are int. When either operand isdbl, both are promoted todbland the result isfloor(a / b)asdbl, wherefloor()is IEEE 754 floor (round toward −∞).- TYPE-65:
//and%are floor division and a floor-consistent remainder:
a % b == a - (a // b) * b.int // 0andint % 0are runtime errors; thedblvariants follow IEEE 754.- TYPE-70: Arithmetic operators
+,-,*,^on twoint operands produce
int. If either operand isdbl, theintoperand is promoted and the result isdbl.- TYPE-75: A negative
intexponent to^is a runtime error; use dbloperands.
TYPE-80: Unary - preserves the type of its operand.
TYPE-85: int arithmetic wraps on overflow (two’s complement, 64-bit).
- TYPE-90: Comparison operators (
==,!=,<,<=,>, >=) producebool. Operands follow the same promotion rules as arithmetic: mixedint/dblpromotes theint.- TYPE-100: Logical operators
and,or,notoperate onbooland produce
bool.- TYPE-105:
andandorshort-circuit. The right operand is not evaluated — and draws no random numbers — when the left operand decides the result.
Zero Values#
ZERO-10: Every type has a zero value used for implicit initialization:
int : 0
dbl : 0.0
str : ""
bool : false
- ZERO-20: An uninitialized pi history slot contains the zero value for the
pi’s type (see HIST-30).