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 worker`` is equivalent to ``agent worker()``.

AG-20:    Agents execute as interleaved processes with coroutine-like
          semantics: an agent runs until it reaches a suspension point
          (``timeout`` or a blocking ``claim``), 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 ``timeout`` nor ``claim`` never
          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 ``fn``
          it 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 ``init`` block, from events, and
          from other agents.  Spawning an agent from a ``fn`` or ``pi`` body
          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 ``timeout`` and ``claim``.  The same mutation
          rules as events apply: only ``state`` variables 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 ``scan``
          follows (CLAIM-70).

AG-80:    ``timeout(dt)`` suspends the current agent for ``dt`` time units,
          rescheduling it to resume at ``current_time + dt``.  ``dt`` must be
          non-negative; a negative value is a compile-time error when
          constant, a run-time error otherwise.  ``timeout`` is valid only
          inside an ``agent`` body or ``init`` block; using it in an
          ``event``, ``fn``, or ``pi`` is a compile-time error.


Resource Claims
---------------

CLAIM-10: ``claim(res)`` acquires one unit of resource ``res``.  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 of ``res`` held 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 ``res`` is available when ``claim`` is 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: ``claim`` and ``release`` are valid only inside an ``agent`` body
          or ``init`` block; using either in an ``event``, ``fn``, or ``pi``
          is 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 ``claim`` is a suspension point and therefore cuts an agent into
          segments; a ``release`` is not and does not (see AG-20).  A
          ``release`` does make units available, so the segment containing it
          has a resource ``scan`` appended — 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 = e`` declares with an explicit type; ``name :type``
          forward-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^64

          The 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 tick`` is equivalent to ``event tick()``.

EV-20:    Only ``event``-declared units may be scheduled with ``sched()``.
          Passing a ``fn`` to ``sched()`` 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 — including ``run()``,
          ``sched()`` registrations, and ``:print`` calls — are valid only
          after ``exec``.  A file without ``exec`` is a pure model.  ``exec``
          is 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.
          ``scenario`` selects parameter values; ``label`` identifies output.
          ``duration`` is the maximum simulation time per run (or ``:huge``
          for a run with no time horizon; see EXEC-15); ``nruns`` is the
          number of independent runs.  For pi-only programs, ``duration``
          equals the number of periods (each period advances time by 1).
          For programs with events or agents, the run ends when simulation
          time would exceed ``duration`` or the event queue is empty,
          whichever comes first.

EXEC-15:  ``duration`` may be ``:huge`` (positive infinity, INTR-75).  Such a
          run has **no time horizon**: it ends when a ``stop_r`` fires
          (SCHED-20), ``halt`` executes (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 ``:huge`` and the model contains ``pi``
          declarations but no ``stop_r`` or ``halt``, it is a compile-time
          error.  When the duration is not compile-time constant,
          termination is the programmer's responsibility.

EXEC-20:  ``scenario`` and ``label`` are optional.  If ``scenario`` is
          omitted, module-level default values are used.  A ``label`` may
          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 identical ``run()`` statements after one ``:randseed``
          produce 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, after ``init`` (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.
          ``return`` is valid only in a ``fn`` body and requires an
          expression.

FN-30:    A function may not mutate state variables.  A function may not
          call ``sched()``, ``stop_r()``, ``stop_s()``, or ``halt()``.

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 full ``int`` expression
          (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; bare ``voter_id`` is 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``, and ``agent``.  Group declarations are not permitted
          for ``event`` or ``fn``.

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
          ``true`` or ``_`` (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
          ``end`` with no effect.

GUARD-40: Guard-expression arms may mix ``int`` and ``dbl``; 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 item ``name`` from
          ``k`` evaluations ago, where ``k`` is a positive integer literal.
          ``name[-1]`` is the value from the immediately preceding
          evaluation.  For a pi with period ``p > 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] = expression`` at the start of a pi body.  Offsets are
          ``0`` or 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 to ``name[-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 end`` executes the statements in ``S`` if and only
          if ``cond`` evaluates to ``true``.  If ``cond`` is false, execution
          continues after ``end`` with no effect.

IF-20:    ``if cond then S1 else S2 end`` executes ``S1`` if ``cond`` is
          ``true``, otherwise executes ``S2``.

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 end``

          There is no ``elif``; use ``match`` for multi-branch conditionals.


Init Block
----------

INIT-10:  The ``init`` block is a singleton declaration; at most one ``init``
          block may appear per program.

INIT-20:  The ``init`` block 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 ``init`` block has the same capabilities as an agent body: it
          may spawn agents, call functions, use ``timeout`` and ``claim``,
          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 ``init`` executes (see EXEC-30).


Intrinsics
----------

INTR-10:  Intrinsic functions use the ``:name`` prefix 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 ``init`` bodies 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 in
          ``obs_r`` and ``obs_s`` declarations, where they aggregate over the
          observations collected at finer granularities.  After a ``run()``
          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: ``:quantile`` uses linear
          interpolation (R type 7); ``:var`` is the n−1 sample variance;
          ``:mean`` always returns ``dbl``; ``:sum`` returns the element
          type; ``:probability(x)`` takes a ``bool`` observation 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 the ``i``-th value of observation variable ``x``; indices
          are 1-based, matching ``:p`` and ``: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 outside ``1..: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 ``int`` or ``dbl`` and return
          ``dbl``.

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 accept ``int`` or ``dbl``.  If
          the arguments mix types the result is ``dbl``; otherwise the result
          takes the type of the arguments.

INTR-60:  ``:print`` writes its arguments to stdout.  ``:printf`` writes
          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:  ``:print`` of 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.  ``:print`` of a
          *declared* symbol (pi, state, global) always prints that symbol's
          current or final scalar value, even when the symbol is observed;
          use ``:dump`` to export its series.  This format is normative and
          identical across implementations (DET-10).  ``:printf`` has no
          format specifier for series; passing an observation series to
          ``:printf`` is a compile-time error.

INTR-66:  ``:dump(x)`` writes every value of observation series ``x`` to
          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:  ``:boxplot`` returns a string that is an ASCII box-plot summary of
          observation data.

INTR-75:  ``:huge`` returns positive infinity (IEEE 754 ``+∞``) as a ``dbl``.
          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 ``:huge`` is 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 of ``start`` time.  Each
          pair defines a segment: rate ``rate`` applies from ``start`` until
          the next segment's ``start`` (or forever, for the last segment).
          At least one pair is required.  An odd number of arguments is a
          compile-time error.

INTR-82:  ``:nhpp`` reads the current event time (``:t``) internally.  It
          draws one unit of exponential arrival potential and walks the
          schedule segments forward from ``:t`` until that potential is
          consumed, returning the elapsed time to the arrival point.
          Segments that ended before ``:t`` are skipped.  Segments within
          which ``:t`` falls are entered at ``:t``, not at their ``start``.

INTR-83:  A segment with ``rate = 0`` contributes 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,
          ``:nhpp`` returns 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:  ``:nhpp`` is meaningful only when event time is active (i.e.,
          called from within an ``event``, or from a ``fn`` called by an
          ``event``).  Calling it during pi evaluation — where ``:t``
          reflects 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) name`` fires every ``p`` ticks; ``pi(p, d)`` first fires at
          ``t = d``.  Defaults are ``p = 1`` and ``d = p``.  In this version
          ``p`` and ``d`` must be compile-time integer constants, with
          ``p >= 1`` and ``d >= 0``.

          .. admonition:: 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 that ``dbl``
             periods 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_p`` recording, 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
-----------------

.. admonition:: 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:   ``export`` makes a module-level symbol visible to other modules.
          Exportable symbols include variables, state variables, periodic
          items, and functions.

MOD-20:   ``import module_name`` makes all exported symbols of
          ``module_name`` accessible 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``, and ``init`` declarations 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
          ``agent`` declarations, one or more observations, and one or more
          ``run()``.  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 ``agent`` and ``resource``
          declarations for process-oriented modeling.  Agents execute as
          coroutines and interact through shared resources.  An ``init``
          block 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), so ``obs_p`` records 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 of ``obs_p`` values is
          therefore not known in advance.

OBS-20:   ``obs_r`` records a value at the end of each run.  It produces R
          values.

OBS-30:   ``obs_s`` records 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_r`` and ``obs_s`` declarations
          aggregate over the observation data collected at finer
          granularities (``obs_r`` aggregates over period observations;
          ``obs_s`` aggregates over run observations).

OBS-60:   Aggregation for ``obs_r`` occurs at the end of the run, before
          ``obs_p`` data are discarded.


Resources
---------

RC-10:    A resource (``resource``) is a named synchronization primitive with
          an integer capacity.  ``resource teller := 1`` is 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 same ``rsc`` value 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 resource ``r`` to ``n``.
          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 an ``int``; relative schedules are written
          ``:capacity(t, :capacity(t) + 1)``.  ``r`` is a resource name, a
          group member reference, or an ``rsc`` parameter; ``n`` must be
          ``int``.

          ``:capacity`` is valid in ``pi``, ``event``, ``agent``, and
          ``init`` bodies — unlike ``claim`` it never suspends, so it is not
          restricted to agents.  It is not valid in a ``fn`` body (no side
          effects) or at exec level (resources exist only for the duration of
          a ``run()``).

          A capacity raise increases availability, so any segment that may
          execute a ``:capacity`` set has a resource ``scan`` appended.
          Because ``n`` is 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 the
          ``held < capacity`` test until enough releases land.  Waiters are
          untouched by a reduction: they stay queued in FIFO order and are
          granted by the ``scan`` that 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/capacity`` averaged.  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 ``scenario`` block 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 per
          ``run`` statement, 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)`` places ``handler`` on the event queue
          at time ``current_time + delta_t``.  ``delta_t`` must 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
          ``condition`` is 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_r`` is valid only inside ``pi``,
          ``event``, ``agent``, and ``init`` bodies; ``stop_r`` at module
          level is a compile-time error.  (A ``stop_r`` in 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_r`` executes inside a pi
          body, the run ends at the end of the current period: remaining pis
          evaluate and ``obs_p`` records before the run ends.  Inside an
          event, agent, or ``init`` block, 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_r`` is a completed run: ``message`` is
          recorded, end-of-run processing occurs (``obs_r`` values 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_r`` aggregation (OBS-60), so the
          condition may reference observation data collected so far.  When
          ``condition`` is true, the simulation ends and ``message`` is
          recorded; no further runs execute.  ``stop_s`` is valid only at
          model level; using it inside a ``pi``, ``event``, ``agent``,
          ``fn``, or ``init`` body is a compile-time error.

SCHED-40: ``halt(message)`` immediately terminates the current run and the
          simulation.  ``message`` is 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 the ``init`` block 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: ``for`` loop variables are local to the loop body and follow the
          same no-shadowing rule.


State Variables
---------------

STATE-10: A ``state`` variable is declared with the ``state`` keyword 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 ``init`` block.  Assignment to a state
          variable outside an ``event``, ``agent``, or ``init`` block 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 ``event`` or ``agent``
          declarations).  They do not exist in pure pi models (models with
          neither ``event`` nor ``agent`` declarations) — 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 ``event`` or
          ``agent`` in 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 ``p`` corresponds to simulation time ``t = p`` (periods are
          1-indexed; period boundaries fall on integer times).  Time ``t = 0``
          precedes period 1; the ``init`` block executes at ``t = 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_r`` or ``halt`` ends 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 = 0`` go 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:  ``rsc`` is an opaque reference type for resources (see RC-40).  It
          is valid only as the declared type of a ``fn`` or ``agent``
          parameter.  Values of type ``rsc`` are created only by
          ``resource`` declarations; ``rsc`` may 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:  ``int`` is implicitly promoted to ``dbl`` in any binary arithmetic
          operation where the other operand is ``dbl``.  The result type of
          such an operation is ``dbl``.

TYPE-40:  ``dbl`` is never implicitly narrowed to ``int``.  Assigning a
          ``dbl`` expression to an ``int`` variable is a compile-time error.

TYPE-50:  ``/`` (division) ALWAYS produces ``dbl``, regardless of operand
          types.  If either operand is ``int``, it is promoted to ``dbl``
          before the division.

TYPE-60:  ``//`` (integer division) produces ``int`` when both operands are
          ``int``.  When either operand is ``dbl``, both are promoted to
          ``dbl`` and the result is ``floor(a / b)`` as ``dbl``, where
          ``floor()`` is IEEE 754 floor (round toward −∞).

TYPE-65:  ``//`` and ``%`` are floor division and a floor-consistent
          remainder: ``a % b == a - (a // b) * b``.  ``int // 0`` and
          ``int % 0`` are runtime errors; the ``dbl`` variants follow
          IEEE 754.

TYPE-70:  Arithmetic operators ``+``, ``-``, ``*``, ``^`` on two ``int``
          operands produce ``int``.  If either operand is ``dbl``, the
          ``int`` operand is promoted and the result is ``dbl``.

TYPE-75:  A negative ``int`` exponent to ``^`` is a runtime error; use
          ``dbl`` operands.

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 (``==``, ``!=``, ``<``, ``<=``, ``>``,
          ``>=``) produce ``bool``.  Operands follow the same promotion rules
          as arithmetic: mixed ``int``/``dbl`` promotes the ``int``.

TYPE-100: Logical operators ``and``, ``or``, ``not`` operate on ``bool`` and
          produce ``bool``.

TYPE-105: ``and`` and ``or`` short-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).
