import type { Roll } from './dice-expression'; import type { Program, Expression, IfExpr, MatchExpr, Value, Assignment, ParameterDeclaration, ComprehensionExpr, RepeatExpr, FoldExpr } from './program'; import type { RollResult, StructuredDiceRollResult } from './roll-result'; /** * Thrown by the evaluator when a single execution path encounters an * undefined outcome — currently produced only by division by zero * (`d6 / (d6 - 1)` when the d6 rolls a 1, `1 / 0` always, etc.). * * Monte Carlo loops catch this, skip the sample, and accumulate the * dropped probability into `partial-number` / `undefined` FieldStats * variants rather than propagating the throw out of `analyze`. The * exact-tier symbolic-distribution machinery handles undefined via * the UNDEFINED_SENTINEL string in `sym-dist`, never reaching this * throw in the first place when the analysis stays on the exact path. */ export declare class UndefinedOutcomeError extends Error { constructor(message: string); } /** * Thrown when a structural access has *no value at that position* — an * absent record field (`rec.field` where `field ∉ rec`) or an out-of-range * index (`arr[i]` outside `0 … len-1`). Distinct from a type error (field * access on a non-record) or an undefined outcome (division by zero). * * The `default` operator (`primary default fallback`) catches exactly this * (plus {@link UndefinedOutcomeError}) to substitute the fallback; every * other error propagates, so strict access (issue #13) stays the default. * Subclasses `Error` and preserves the legacy `no field` / `out of bounds` * messages so existing `toThrow(/no field/i)` expectations still hold. */ export declare class MissingValueError extends Error { constructor(message: string); } export type DiceTerm = Extract; export type StructuredDiceTerm = Extract; export type DiceTermResult = { kind: 'arithmetic'; node: DiceTerm; result: RollResult; } | { kind: 'structured'; node: StructuredDiceTerm; result: StructuredDiceRollResult; }; /** * Event for the `onAssignment` hook. Fires once per execution of an * `Assignment` statement, after the RHS resolves and the binding is * installed in the environment. */ export interface AssignmentEvent { type: 'assignment'; /** The assignment statement node from the parsed program. Stable * across runs; consumers can key a `Map` on identity to dedup * per-iteration firings inside a `repeat`. */ node: Assignment; /** Resolved value of the RHS. For nested assignments inside a * `repeat` body, this is the value at the current iteration. */ value: Value; } /** * Event for the `onParameter` hook. Fires once per execution of a * `parameter-declaration` statement, after the value resolves (either * from the provided override or from evaluating the default). */ export interface ParameterEvent { type: 'parameter'; /** The parameter-declaration node from the parsed program. */ node: ParameterDeclaration; /** Resolved value: the override if supplied, otherwise the evaluated default. */ value: Value; /** True when the value came from `options.parameters`; false when it * was computed from `spec.default`. */ fromOverride: boolean; } /** * Scope-boundary event for the `onScope` callback. Lets consumers walk * the AST in lockstep with execution — when the AST descends into a * comprehension / repeat / fold the next boundary event tells the * consumer which iteration we're in; when the AST is done with that * iteration's body, the matching `iteration-exit` fires. * * Events fire in **source order** for every iteration form, including * `sort` comprehensions — the evaluator computes each body value during * the source-order pass and only rearranges the precomputed array at * the end. A single `sort-reorder` event fires after the last * `iteration-exit` of a sort comprehension, carrying the permutation * so consumers can render the rows in sorted order without * re-implementing the sort. */ export type ScopeEvent = { kind: 'iteration-enter'; node: ComprehensionExpr | RepeatExpr | FoldExpr; /** Source-order position of this iteration (0..total-1). */ index: number; total: number; /** Bound element for comprehensions + fold. Absent for `repeat`. */ element?: Value; /** Current accumulator for fold. */ accumulator?: Value; } | { kind: 'iteration-exit'; node: ComprehensionExpr | RepeatExpr | FoldExpr; index: number; } | { kind: 'filter-skip'; node: ComprehensionExpr; index: number; element: Value; } | { kind: 'sort-reorder'; node: ComprehensionExpr; /** Permutation such that sorted-position `i` is source position * `sourceToSorted[i]`. */ sourceToSorted: number[]; } | BranchEnterEvent | BranchExitEvent; /** * Discriminator for `branch-enter` events. Distinguishes the three places * the evaluator can take a conditional branch: an `if`-expression's `then` * arm, an `if`-expression's `else` arm, or a `match`-expression arm at a * specific source index. Carrying the arm index lets consumers correlate * the event back to the `MatchArm` node without re-running the match * decision against the AST. */ export type BranchSelection = { kind: 'if-then'; } | { kind: 'if-else'; } | { kind: 'match-arm'; /** Zero-based position of the matched arm within `MatchExpr.arms`. */ armIndex: number; /** True iff the matched arm's pattern is the `_` wildcard. */ isWildcard: boolean; }; /** * Fires once per `if` / `match` evaluation, after the condition or * scrutinee resolves and immediately before the selected arm's body * evaluates. The opposite arm is never entered, so consumers see no * `branch-enter` for it. The matching `branch-exit` fires after the * arm's body returns. * * `scrutinee` is present only for value-mode `match` (i.e. `match VAL { … }`); * absent for `if` and for guard-mode `match { … }`. Use `'scrutinee' in event` * as the value-mode discriminator — the field is omitted, not set to * `undefined`, so the absence/presence pattern survives JSON round-trips. * * Branch events nest with iteration events on a single shared depth * stack. Consumers compute "which open branch contains this dice term" * by walking the event stream and pushing on enter / popping on exit. */ export interface BranchEnterEvent { kind: 'branch-enter'; node: IfExpr | MatchExpr; branch: BranchSelection; scrutinee?: Value; } /** * Fires after the selected arm's body returns, before any sibling * expression evaluates. Always paired with the preceding `branch-enter` * unless the body throws — error propagation leaves the event stream * mid-branch, matching the convention already used by iteration events. */ export interface BranchExitEvent { kind: 'branch-exit'; node: IfExpr | MatchExpr; } /** * Half-away-from-zero rounding. JavaScript's `Math.round(-1.5)` returns * `-1` (half toward +∞), which silently breaks symmetry across zero; * we explicitly flip sign for negatives so `-1.5 round` → `-2` while * `1.5 round` → `2`. */ export declare function roundHalfAwayFromZero(v: number): number; /** * Banker's rounding (round-half-to-even). On exact halves the result is * the nearest even integer; otherwise behaves like ordinary half-away- * from-zero rounding. The exact-half check uses * `Math.abs(v - Math.trunc(v)) === 0.5`, which is exact for half- * integers representable in IEEE-754 (i.e. `|v| < 2^53`). At the * representation edge the implementation defaults to whatever the * underlying `Math.round` family produces — best-effort, IEEE-754-bound. */ export declare function roundHalfEven(v: number): number; /** * Apply a `RoundMode` literal to a finite number. Shared by the runtime * evaluator and `ProgramStats` symbolic distribution analysis so both * code paths agree on the rounding semantics (negative-half asymmetry, * banker's-rounding tie-breaking, etc.). */ export declare function applyRoundMode(v: number, mode: 'round' | 'round-up' | 'round-down' | 'truncate' | 'round-half-even'): number; export interface EvaluatorOptions { maxRepeatIterations?: number; /** * Hard ceiling on the *total* number of loop iterations (repeat, * comprehension, and fold passes combined) a single {@link Evaluator.run} * may perform. `maxRepeatIterations` bounds each individual `repeat`, but * nested loops multiply: `repeat 10000 { repeat 10000 { 1 } }` stays within * the per-loop cap yet allocates 10^8 elements. This evaluation-wide budget * is the backstop against that CPU/memory denial-of-service from small * input. Defaults to 10,000,000. */ maxTotalIterations?: number; /** * Called once per dice term the Evaluator executes. A "dice term" is any * lifted dice expression node — `die`, `n-dice`, `custom-die`, the * reduce wrapper `dice-reduce`, or `structured-dice-roll`. The callback * fires in evaluation order; terms inside non-taken branches do not fire; * terms inside `repeat` fire once per iteration. * * The result payload is tagged `arithmetic` for ordinary dice rolls * (carries a `RollResult` tree) and `structured` for structured-face * dice (carries a `StructuredDiceRollResult`). */ onDiceTerm?: (event: DiceTermResult) => void; /** * Called when iteration scope boundaries are crossed during execution. * See `ScopeEvent` for the variants; fires for comprehensions * (including filter / sort variants), `repeat`, and `fold`. Both * `onDiceTerm` and `onScope` fire synchronously from the same * evaluator, so push order matches evaluation order — consumers that * need a merged stream can subscribe to both and append to one buffer. */ onScope?: (event: ScopeEvent) => void; /** * Called after each `$name = expr` statement evaluates. Fires once * per execution — including N times inside `repeat N { … }`, where * the same `Assignment` node fires per iteration with the value * resolved at that iteration. Non-taken branches (failing match * arms, dead `if` sides) do not fire. * * Dice events in the RHS fire on `onDiceTerm` before this event * fires for the enclosing assignment. * * Consumers that want one value per source location should key a * `Map` on `event.node` (or `event.node.loc.offset`) and use * last-write-wins. Keying by `event.node.name` is a footgun — * different scopes can declare the same name with distinct AST * nodes. * * Does NOT fire for parameter declarations (`$x is { … }`) — those * have their own `onParameter` hook. Comprehension / fold binders * (`$x` in `for $x in …`) are not surfaced today. */ onAssignment?: (event: AssignmentEvent) => void; /** * Called after each `$name is { … }` parameter-declaration statement * resolves. Fires once per run per declaration, in source order. The * payload carries the override flag so consumers can distinguish * caller-provided values from defaults. */ onParameter?: (event: ParameterEvent) => void; } export interface RunOptions { parameters?: Record; } /** * Result of `Evaluator.runWithCapture`. Carries the program's final value * alongside a map from every visited `Expression` node to its most-recently * resolved value. * * Nodes that the run did not evaluate (the losing side of an `if`, * unmatched `match` arms, sub-expressions in skipped iterations) are * absent from the map — `perNodeValues.has(node)` is a valid * "was this evaluated this run?" probe. * * Inside `repeat` / comprehension / fold bodies, the same node is * re-visited per iteration; last-iteration-wins applies. */ export interface RunWithCaptureResult { value: Value; perNodeValues: Map; } export declare class Evaluator { private readonly rollFn; private readonly maxRepeat; private readonly maxTotalIterations; /** Running count of loop iterations performed by the in-progress `run`. * Reset to 0 at the start of every `run` so the budget is per-evaluation. */ private totalIterations; private readonly onDiceTerm; private readonly onScope; private readonly onAssignment; private readonly onParameter; /** Active capture map for the current `runWithCapture` invocation. * Non-null only while a captured run is in progress; ordinary `run` * leaves it null so the hot path stays allocation-free. */ private captureMap; constructor(rollFn?: Roll, options?: EvaluatorOptions); private emitScope; /** * Run the program and additionally return a `perNodeValues` map keyed by * `Expression` node identity, holding each visited node's most-recently * resolved value. Unvisited nodes are absent (not mapped to `undefined`). * * Calls are not re-entrant: if `runWithCapture` is invoked while a * previous capture is still in progress on the same Evaluator instance, * the inner call throws. This is a safeguard against accidental nested * use — the per-Evaluator capture state isn't designed for nesting. */ runWithCapture(prog: Program, options?: RunOptions): RunWithCaptureResult; /** * Charge `n` units against the evaluation-wide iteration budget and throw * once it is exhausted. Called by every loop construct (repeat / * comprehension / fold) so that nested loops, whose products exceed any * single per-loop cap, cannot run the process out of CPU or memory. */ private tick; run(prog: Program, options?: RunOptions): Value; private execStatement; private evalExpr; private evalExprCore; private aggregateSum; private evalStructuredDiceRoll; private armFires; private toNumber; private isTruthy; }