Constraints#

What each execution unit may read, may write, and may not do. This document is normative; it restates the rules of eb-semantics.rst in the axis a modeler actually asks about (“can I do this here?”) and adds no new ones. Every clause cites the rule it comes from.

The five units are pi, event, agent, fn, and init, plus the exec section, which is not an execution unit but does admit statements.

Periodic items (pi)#

May read — earlier pis’ current-period values, in declaration order (HIST-60); any pi’s history via [-k] (HIST-10); module-level variables; context values :p/:i/:t/:r (CTX-10..30); fn calls (FN-40).

May write or do — its own value, and only through the mandatory terminating expression (PI-20); ephemeral locals (PI-30); stop_r (SCHED-20); :capacity (RC-60); trailing keyword statements after the value is committed (PI-25).

May not — be assigned to (PI-40); mutate state variables (STATE-20); spawn agents (AG-25); timeout or claim (AG-80, CLAIM-40).

Events (event)#

May read — state variables; module-level variables; the arguments bound at schedule time (SCHED-12); context values; fn calls.

May write or do — mutate state variables (STATE-20); sched() further events (SCHED-10); spawn agents (AG-25); stop_r (SCHED-20); :capacity (RC-60); ephemeral locals (EV-40). Executes atomically when dequeued (EV-10).

May not — suspend: no timeout, no claim (AG-80, CLAIM-40); write module-level variables; read a pi’s value (EV-35); be invoked directly by name (EV-25).

Agents (agent)#

May read — everything an event may read, plus the arguments bound at spawn time and rsc parameters (RC-40).

May write or do — everything an event may do, plus timeout (AG-80), claim and release (CLAIM-10, CLAIM-15). Ephemeral locals (AG-40).

May not — write module-level variables (AG-50); read a pi’s value (AG-55); return a value — an agent invocation is a statement, not an expression (AG-60).

Suspension is not restricted to the top level

An earlier specification asserted that suspension could occur only at the top level of an agent body, and used that to argue implementations need no coroutines. That restriction is not part of this language. timeout and claim are valid wherever a statement is valid inside an agent, including within if, match, and for. The segment model accommodates this directly: an agent is a cyclic graph of segments cut at suspension points, not a straight-line sequence, so a suspension inside a loop is an ordinary back edge.

Functions (fn)#

May read — its parameters and module-level variables. Nothing else.

May write or do — compute and return a value of its declared type (FN-25). Draw from the random stream (INTR-31). Nothing observable outside its own frame; locals are ephemeral (FN-50).

May not — mutate state variables (FN-30) or read them (FN-32); reference a pi by name (FN-35); call sched(), stop_r(), stop_s(), or halt() (FN-30); timeout or claim (AG-80, CLAIM-40); spawn agents (AG-25); change resource capacity (RC-60).

Init block (init)#

Has the same powers as an agent body (INIT-30): spawn agents, call functions, timeout, claim, release, mutate state, set capacity. It is a singleton (INIT-10), runs once per run at t = 0 (INIT-20), and runs after state and resources are reset (INIT-40).

Exec section#

Statements — including run(), sched() registrations, and :print — are valid only after exec (EXEC-05). Assignment at exec level reaches exec locals and module globals only (EXEC-45). :randseed is valid only here, at most once (EXEC-35). :capacity is not valid here: resources exist only for the duration of a run() (RC-60).

After a run() completes, exec statements may read a pi’s or state variable’s final value directly, in addition to the observation-based intrinsics (EXEC-45, INTR-40).

Cross-cutting rules#

These are the rules that close the system — the ones that make the table above exhaustive rather than indicative.

STATE-20

State variables are mutable only inside event, agent, or init.

STATE-30

State variables are the Markov state; pure pi models have none.

SCEN-10

Scenarios contain only assignments to module-level variables, applied once per run before period 1 (SCEN-05) — the only sanctioned way module-level values change between runs.

SCOPE-40

No shadowing, anywhere.

GUARD-50

Guard expressions are side-effect free by construction.

GRP-60

Group declarations apply to pi, state, resource, and agent only — not event (which has no persistent state to multiply) and not fn.

SCHED-20/30

stop_r only inside pi/event/agent/init; stop_s only at model level.

MOD-40

Imported non-state symbols are read-only; imported state variables are mutable only from an event/agent in the importer (MOD-50); agent, resource, and init are module-private (MOD-80). Reserved — modules are not implemented.

Why the constraints exist#

A bug in a Monte Carlo model does not crash. It produces a plausible wrong distribution — the worst failure mode, because randomized output cannot be verified by inspection. Nearly every constraint above converts some source of silent bias into a compile-time error, and eb-errors.rst shows each one surfacing as a specific, positioned message.

All causality is findable. State changes happen only in event/agent/init bodies (STATE-20) — a small, syntactically identifiable set of sites. Function calls, pi references, and guard expressions can never move the simulation behind your back (FN-30, PI-40, GUARD-50).

Pis are spreadsheet rows with the race conditions outlawed. Once per period, in declaration order, immutable once produced (PI-10, PI-50). Same-period references must point backward in declaration order (HIST-60), so same-period cycles cannot be written; feedback must go through explicit history, making every recurrence visible in the source.

Function dependencies are visible at the call site. A function that could reach into simulation state would have dependencies invisible where it is called. FN-32 and FN-35 make the answer to “what state does this touch?” always “none”.

An agent body is the whole story of its process. Suspension is syntactically visible — timeout, claim — and no fn can hide a wait, because functions cannot suspend at all (FN-30, CLAIM-40). Note that explicit release (CLAIM-15) reintroduces one bug class the older block form excluded: a held unit can now be forgotten. CLAIM-70 bounds the damage by reclaiming held units when an agent terminates, so a leak costs capacity for the agent’s remaining lifetime rather than for the rest of the run.

Reproducibility is a theorem, not a hope. DET-10 — same code, inputs, scenario, and seed give identical observable results — is only promisable because of the constraint system: single-threaded atomic handlers (EV-10, DET-20), FIFO ordering in both the event queue (TIME-30) and resource wait queues (CLAIM-30), exact IEEE 754 time comparison (TIME-50), and no mutation channel outside the sequenced units. EXEC-30’s total per-run reset makes runs independent, which is what licenses parallel run execution (DET-30) with no change in results.