Design Rationale
================

Two program modes: imperative and simulation
--------------------------------------------

:Decision: EB supports two valid program shapes — imperative (no ``pi``,
           no ``obs_*``, no ``run()``) and simulation (requires all three).

:Alternatives: Require ``run()`` in every EB program, even for simple
               computations.

:Rationale: Many useful tasks — parameter calculations, function libraries,
            unit checks, exploratory computation — do not need a simulation
            loop. Requiring ``run()`` everywhere would make EB needlessly
            hostile to these uses. The two modes are structurally disjoint:
            the presence of ``pi``, ``event``, or ``agent`` signals simulation
            intent, and the compiler enforces that the rest of the simulation
            machinery (``obs_*``, ``run()``) is present.

Intrinsics hide simulation algorithms so modelers focus on the model
--------------------------------------------------------------------

:Decision: EB provides high-level simulation intrinsics (e.g., ``:nhpp``) that
           encapsulate well-known stochastic algorithms.

:Alternatives: Require users to implement these algorithms themselves in ``fn``
               blocks using lower-level primitives.

:Rationale: EB strives to reduce the need for users to write algorithms.
            Rather, they focus on the model.  For example, instead of having
            to write your own approach to a Non-Homogeneous Poisson Process,
            you can use ``:nhpp(schedule)``.  The algorithm is correct,
            tested, and invisible — the modeler declares *what* the arrival
            process is, not *how* to sample from it.

Determinism: FIFO ordering, IEEE 754 exact comparison, no epsilon
-----------------------------------------------------------------

:Decision: Events sharing the same scheduled time are dequeued in FIFO
           (insertion) order. Time comparisons are exact under IEEE 754
           with no tolerance or epsilon.

:Alternatives: Priority queues with tie-breaking by event type; approximate
               time comparison with a small epsilon.

:Rationale: FIFO ordering makes simulation traces reproducible and
            predictable — the order in which events fire at the same
            time is determined entirely by the order they were scheduled,
            not by implementation details of a priority queue. Exact
            IEEE 754 comparison follows from the same principle: given
            identical inputs and seed, results must be bit-for-bit
            identical across runs and implementations.

Per-run PRNG reseed via a keyed-hash derivation (DET-30, EXEC-30)
-----------------------------------------------------------------

:Decision: The PRNG is re-seeded at the start of every run with
           ``runseed(r) = (simseed + r * 2654435761) mod 2^64``, hashed
           through splitmix64 to produce the initial xoshiro256** state.
           The exact formula and generator are pinned by DET-30.

:Alternatives: One continuous stream across all runs (no per-run reseed);
               the xoshiro256** jump function, advancing the master state
               by 2^128 draws per run.

:Rationale: The requirement came out of the GPU experiments (aa30-gpu0,
            aa31-gpu1): to execute runs in parallel — one GPU thread per
            run — each run's starting PRNG state must be computable
            independently, without knowing how many draws any other run
            consumed.  A single continuous stream makes run r's results
            depend on the draw counts of runs 1..r-1, which forbids
            parallel execution and means an early-terminated run
            (``stop_r``) perturbs every run after it.

            The keyed-hash derivation is chosen over the jump function,
            which the GPU proofs-of-concept originally used, for two
            reasons.  First, it is random-access: any thread computes its
            own starting state directly from (simseed, r), whereas
            reaching run r by jumping requires r sequential jump
            applications on the host.  Second, it generalizes: keying can
            extend to finer stream ownership — per (simseed, sampler, r) —
            which the jump's fixed 2^128 stride cannot express.  The
            trade-off is that non-overlap of the per-run streams is
            probabilistic (splitmix64 scrambling) rather than guaranteed
            by construction; with 2^64 seed space and typical run counts,
            overlap probability is negligible.

            Pinning the exact constant, hash, and generator makes the
            per-run streams part of the language contract: identical
            seeds produce bit-identical results on every backend, so a
            model debugged on the VM replays exactly on native or GPU
            (DET-10).

Static scalar types, no aggregates
----------------------------------

:Decision: EB is statically typed with four scalar primitives (``int``,
           ``dbl``, ``str``, ``bool``) plus the opaque resource type
           ``rsc``.  Every variable's type is fixed at declaration
           (TYPE-20) and inferred by default.  There are no aggregate
           types — no records, structs, or arrays.

:Alternatives: Dynamic typing (a single number type with runtime tags);
               a richer static type system (records, unions, generics).

:Rationale: Types in EB pay in exactly two currencies — representation
            and numeric correctness — and in neither does EB need more
            than scalars.

            **Representation.**  Because the compiler knows the machine
            representation of every value, ``int + int`` compiles to an
            integer add and ``dbl * dbl`` to a floating-point multiply,
            with no runtime tag checks and no boxing.  Fixed-size values
            are what make the memory model work: one fixed-size state
            record per group entity (GRP-10), agent locals at fixed byte
            offsets (see ``docs/source/rewrite.rst``), and
            history and observation buffers as flat native arrays.
            Dynamic tagged values would roughly double memory and add a
            branch per access — and GPU compilation (``--target gpu``)
            would be infeasible rather than merely slower.

            **Numeric correctness.**  A numeric type 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.  TYPE-40 (no silent
            narrowing), TYPE-50 (``/`` always produces ``dbl``), and
            TYPE-60 (integer division is the explicit ``//``) turn this
            class of silent corruption into compile-time errors.  Static
            types also decide at compile time whether each operation is
            int or IEEE 754 double, forcing every backend — VM or native
            — to compute identical results; this underwrites DET-10.

            The benefits richer type systems provide are architectural —
            refactoring safety, API contracts across large codebases —
            and do not apply to EB.  EB models scale by data, not by
            code: a three-million-entity model is a handful of group and
            agent declarations backed by millions of state records
            (GRP-10), not millions of lines with deep abstraction
            layers.  With no aggregate shapes to describe and no large
            API surface to protect, the type system is minimal and stays
            so.  Most of the value comes from the type *rules*, not
            annotations: inference (``:=``) does the work, and explicit
            types are needed only at abstraction boundaries (function
            parameters, FN-20; forward declarations).

Agents as processes alongside event handlers
--------------------------------------------

:Decision: EB adds a process-based simulation style via ``agent`` (agents),
           ``resource`` (resources), ``claim``, ``timeout``, and ``init``,
           coexisting with the existing event-handler (``event``/``sched``) style.

:Alternatives: Extend only the event-handler model with indexed state arrays
               and ID-threading through handler arguments; or replace event
               handlers with agents entirely.

:Rationale: Process-based models let modelers write sequential logic —
            "arrive, wait for server, be served, leave" — rather than
            inverting control into separate handlers for each state transition.
            This is more readable for entity-flow models (queuing networks,
            agent-based models) where the narrative of a single entity's
            lifecycle is the natural unit of thought.  The event-handler style
            remains appropriate for models naturally expressed as state
            transitions (reliability, Markov chains).  Both styles compile to
            the same underlying event queue and can coexist in one program.

            What makes an agent genuinely an agent is **suspension** —
            ``timeout`` or ``claim``.  An ``agent`` body that contains neither
            never suspends: it completes in zero simulation time when
            dequeued.  Even then it is not a function call — its execution
            is deferred through the event queue, and it may mutate state
            variables, which a ``fn`` may not (AG-22).  The ``agent`` keyword
            signals process-oriented intent; the suspension points are what
            make that intent real.

Suspension never below a call frame: agents without coroutines
--------------------------------------------------------------

:Decision: ``timeout`` and ``claim`` are legal only inside an ``agent``
           body or ``init`` block (AG-80, CLAIM-40) — never inside a ``fn``
           or ``pi``, and therefore never anywhere down a call chain.  They
           *are* legal within ``if``, ``match``, and ``for`` inside an agent
           body.

:Alternatives: Allow suspension anywhere in an agent's dynamic extent, as
               general coroutines do, so that helper functions could
               encapsulate waiting patterns.

:Rationale: The property that buys the implementation everything is that an
            agent never suspends *with call frames beneath it*.  Its saved
            execution context is then nothing more than which instruction
            runs next plus its local variables — no captured call stack, no
            coroutine machinery, no stack switching.  This is what lets EB
            agents compile to flat native code and scale to large
            populations at the cost of one small heap record each.

            Note that nesting inside ``if``, ``match``, or ``for`` does not
            threaten this.  Those are control flow within a single body, not
            call frames, so a suspension inside a loop is an ordinary back
            edge in the agent's segment graph — an agent is a cyclic graph
            of segments cut at suspension points, not a straight-line
            sequence.  An earlier version of this document stated the
            decision as "top level only" and rested the no-coroutines
            argument on it; that was stronger than necessary, and stronger
            than the compiler enforces.

            The restriction that does the work preserves the purity rules: a
            ``fn`` that could suspend would no longer be a pure computation
            (FN-30), and every call site would need to be suspension-aware.
            The cost is that helpers cannot hide waiting: any ``timeout`` or
            ``claim`` must be written in the agent body where it occurs.
            That is arguably a readability win — an agent's complete waiting
            behavior is visible in one place, the body itself.

``claim`` and ``release`` are separate statements
-------------------------------------------------

:Decision: Resource acquisition is ``claim(res)``, and the unit is held
           until an explicit ``release(res)`` or until the agent terminates
           (CLAIM-10, CLAIM-15, CLAIM-70).

:Alternatives: A structured block, ``claim(res) block end``, releasing
               automatically at ``end`` — the original decision, superseded
               12 AUG 2026.

:Rationale: The block form was chosen to eliminate the forget-to-release
            bug class: with no ``break`` or early return, ``end`` is the
            only exit path, so auto-release is unconditional and resource
            lifetime is lexically obvious.  That reasoning is sound and the
            guarantee was real.

            It was given up because it cannot express **crossing
            lifetimes**.  Hand-over-hand acquisition — hold A, acquire B,
            release A — is routine in queueing and manufacturing models,
            and lexical nesting can only express lifetimes that nest.  A
            scope stack has no way to represent two overlapping holds where
            neither contains the other.  Modelers hitting this had no
            recourse within the language.

            The bug class does come back.  Two things bound it: releasing a
            unit the agent does not hold is a runtime error rather than
            silent corruption (CLAIM-15), and an agent that terminates
            holding units returns them (CLAIM-70), so a leak costs capacity
            for the agent's remaining lifetime rather than for the rest of
            the run.  Neither is as strong as the lexical guarantee, and
            that is the price of expressiveness here.

``resource`` as declaration keyword; ``rsc`` as the type name
-------------------------------------------------------------

:Decision: Resources are declared with the keyword ``resource``
           (``resource name := N``) but typed as ``rsc`` in parameter
           lists and type annotations.

:Alternatives: Use one word for both declaration and type, either
               ``resource`` or ``rsc``.

:Rationale: Using one word for both would cause syntax highlighters to
            render it as a type everywhere it appears, including in
            ``resource teller := 1`` where it reads as a
            declaration keyword.  The abbreviation ``rsc`` (parallel to
            ``str``, ``int``, ``dbl``, ``bool``) is consistent with EB's
            naming convention for types and is unambiguously a type wherever
            it appears.  The declaration keyword ``resource`` is then
            highlighted as a keyword, and the type ``rsc`` as a type.

``timeout``, ``claim``, and ``init`` have no colon prefix
---------------------------------------------------------

:Decision: Agent control constructs use plain identifiers — ``timeout``,
           ``claim``, ``init`` — not the ``:name`` colon-prefix form.

:Alternatives: Use ``:timeout``, ``:claim``, ``:init`` to signal that they
               are runtime-provided.

:Rationale: The colon prefix in EB marks value-producing runtime intrinsics
            (``:rande``, ``:print``, ``:nhpp``).  Simulation control
            statements — ``sched``, ``halt``, ``stop_r``, ``stop_s`` — are
            also runtime-provided but carry no colon, because they affect
            execution flow rather than producing values.  ``timeout`` and
            ``claim`` are simulation control, so they follow the same
            no-colon pattern.  ``init`` is a declaration block, like
            ``scenario`` and ``event``, neither of which uses a colon.

``init`` block for lazy agent spawning
---------------------------------------

:Decision: The ``init`` block executes as a coroutine at the start of each
           run.  Calling ``timeout(dt)`` inside ``init`` genuinely suspends
           it, allowing other agents to run before spawning continues.

:Alternatives: Treat ``init`` as a pre-simulation setup phase that runs to
               completion before t=0, pre-loading all initial events into the
               queue.

:Rationale: A pre-simulation ``init`` would force all N initial agents to be
            enqueued before any of them fire — acceptable for small N, but a
            memory and initialisation cost proportional to N for large
            populations.  Treating ``init`` as a coroutine means a loop like
            ``for i = 1, N do spawn(i); timeout(1) end`` spawns one agent,
            suspends, lets that agent begin running, then resumes to spawn the
            next.  At any moment only a small number of events are live in the
            queue, regardless of N.

``pi`` has no ``end``; the asymmetry is intentional
----------------------------------------------------

:Decision: Periodic item bodies (``pi``) have no closing ``end`` keyword.  All
           other execution units (``event``, ``fn``, ``agent``) close with ``end``.

:Alternatives: Require ``end`` for all execution units including ``pi``; make
               ``end`` optional in ``pi``.

:Rationale: A periodic item body is expression-valued: the final expression IS the
            value and therefore the natural terminator.  There is no open-ended
            statement sequence to close explicitly.  Requiring ``end`` would
            add noise to the common case (``pi x := 42 end``) with no benefit.
            Making ``end`` optional is worse than either: optional syntax
            creates reader uncertainty ("does this ``pi`` have an ``end``?"),
            writer uncertainty, and parser complexity for no gain.  The
            asymmetry is intentional — it signals the semantic difference:
            ``pi`` produces a value; ``event``, ``fn``, and ``agent`` execute
            statements.

``duration`` as maximum simulation time
----------------------------------------

:Decision: The ``{duration, nruns}`` argument to ``run()`` is defined as the
           maximum simulation time per run, not a period count.

:Alternatives: Keep ``duration`` as a strict period count meaningful only for
               periodic item programs; require separate syntax for time-bounded
               event and agent programs.

:Rationale: Periodic item programs advance time by exactly 1 per period, so "number
            of periods" and "maximum simulation time" are equivalent for them —
            no existing programs are affected.  The generalised definition
            makes a single ``run()`` syntax work uniformly across all program
            modes (PI, EV, AG, and hybrids), and matches the intuition that
            ``run(){100, 10}`` means "simulate 100 time units, 10 times."

Functions receive periodic item values as parameters, not by name (FN-35)
-------------------------------------------------------------------------

:Decision: A function body may not reference a periodic item by name.  A
           periodic item's value may only enter a function through an explicitly
           typed parameter, passed by the caller.

:Alternatives: Allow function bodies to reference any in-scope periodic item
               by name, as they can reference ordinary variables.

:Rationale: A function that references periodic items by name has hidden
            dependencies on specific module-level declarations.  Those
            dependencies are invisible at the call site — the reader must
            trace into the function body to know what simulation state it
            touches, and the compiler must do the same to verify
            declaration-order constraints (HIST-60).  Requiring values to
            be passed as arguments makes every periodic item dependency explicit
            at the call site, keeps functions module-agnostic and
            reusable across programs, and preserves the clean separation
            between periodic items (model declarations) and functions (pure
            computations on values).

Group declarations: shared body, N independent state records
------------------------------------------------------------

:Decision: A group declaration (``pi name_{idx|lo..hi}``) compiles the body
           once as shared parameterized bytecode and allocates one independent
           state record per entity.  The compiler does not emit N copies of
           the body.

:Alternatives: Compile-time textual expansion to N separate declarations
               (N body copies); a runtime array or list type; a preprocessor
               macro.

:Rationale: Many simulation models require a large number of structurally
            identical entities — voters in a voter model, cells in a spatial
            grid, servers in a pool.  Writing each declaration explicitly does
            not scale and invites copy-paste errors.

            Naïve compile-time expansion (N body copies) has the same problem
            in a different form: if the body is long, memory usage grows as
            N × body-size.  The shared-body design keeps memory proportional
            to N × (state per entity) — just the history slots and current
            value — which makes N in the millions or billions practical where
            expansion would be prohibitive.

            A runtime array or list type would impose uniform element types
            and access syntax that conflicts with EB's design: each entity in
            a group is a first-class declaration with its own history, its own
            observations, and its own guard expression.  Hiding entities
            inside an array loses those capabilities.

            The ``{idx|lo..hi}`` notation echoes the ``{duration,nruns}``
            braces of ``run()`` — braces consistently signal arguments that
            are resolved before simulation begins, whether that is a run
            configuration or a group expansion range.  The ``{...}`` wrapper
            is also required at every reference site inside the body, making
            substitution points visually distinct from ordinary identifiers and
            eliminating the ambiguity that would arise if bare ``voter_id``
            could mean either a literal name or "the voter at index id."

``sched`` arguments instead of event group declarations
-------------------------------------------------------

:Decision: Event handlers receive identity through arguments passed at
           ``sched()`` time — ``sched(dt, handler(id))`` — rather than
           through group declarations of the form ``event name_{idx|lo..hi}``.
           Group declarations are therefore restricted to ``pi``, ``state``,
           ``resource``, and ``agent``; they do not apply to ``event``.

:Alternatives: Allow ``event`` in group declarations, producing N separately
               named handlers (``arrival_1``, ``arrival_2``, …) with shared
               source code; or provide both mechanisms side by side.

:Rationale: Groups exist to give N entities **independent persistent state**
            — separate history arrays, separate resource pools, separate
            per-period values.  That is the capability that would be
            prohibitively verbose to hand-write.

            Event handlers have no persistent state (EV-40: all locals are
            ephemeral).  A group of event declarations would therefore yield N
            identically-behaved callables with no independent storage to
            justify the expansion.  The only thing such a group would add is
            routing identity — the ability to schedule ``arrival_3`` versus
            ``arrival_7``.  Passing an argument achieves the same routing
            without any of the group machinery:

            .. code-block:: eb

               event arrival(entity_id: int)
                 ...
               end

               sched(dt, arrival(3))
               sched(dt, arrival(7))

            The argument is evaluated at ``sched()`` call time and stored in
            the event record.  When the event fires, ``entity_id`` is bound
            in the handler's local scope, indistinguishable from a normal
            parameter.  This is the same mechanism agents already use at
            spawn time (``agent_name(args)``), so it requires no new
            concepts from the modeler.

            Keeping groups out of ``event`` also keeps the group grammar
            unambiguous: group identity (``{idx|lo..hi}``) is always a
            compile-time quantity derived from a range, while event argument
            values are runtime values.  Mixing the two would require
            distinguishing compile-time-fixed group members from
            dynamically-supplied arguments, complicating both the language
            and the compiler with no clear benefit.

FIFO wait queue for resource claims
------------------------------------

:Decision: Agents waiting to ``claim`` a resource are queued in FIFO order
           and resumed in that order as units become available.

:Alternatives: Random selection among waiting agents; priority-based
               selection.

:Rationale: FIFO is consistent with EB's existing event-ordering principle
            (TIME-30), which dequeues simultaneous events in insertion order.
            Applying the same discipline to resource wait queues keeps the
            model predictable and reproducible.  The agent that has waited
            longest is always served next, which also prevents starvation
            under sustained load.
