/** * Shared types for the algebraic effects system. * * This module exists to break the circular dependency between `effects.ts` * (standalone functions) and `evaluator/trampoline.ts` (trampoline loop). * Both modules import types from here without creating a cycle. */ import type { Any } from '../interface'; import type { DvalaError } from '../errors'; import type { ContinuationStack } from './frames'; import type { AstNode, SourceMap } from '../parser/types'; import type { ContextStack } from './ContextStack'; export declare const SUSPENDED_MESSAGE = "Program suspended"; /** * A captured continuation point. Created by `suspend()` or `checkpoint()`. * The `continuation` field is opaque — hosts should not inspect or modify it. */ export interface Snapshot { /** Unique ID for this snapshot, generated at creation time. */ readonly id: string; /** Opaque serialized continuation. Do not inspect or modify. */ readonly continuation: unknown; /** Wall-clock timestamp (Date.now()) when snapshot was taken. */ readonly timestamp: number; /** Stable sequence number (0-based, never reused within an execution lineage). */ readonly index: number; /** UUID identifying the run() or resume() call that created this snapshot. */ readonly executionId: string; /** Human-readable label from the checkpoint perform call. */ readonly message: string; /** * True when this snapshot represents the terminal state of a run (completed or failed). * False/absent for mid-execution checkpoints that can be resumed. */ readonly terminal?: boolean; /** Optional domain metadata from the perform call or suspend call. */ readonly meta?: unknown; /** * The name of the effect that was being handled when the program suspended. * Undefined when suspension occurred outside of an effect handler (e.g. in parallel/race branches). */ readonly effectName?: string; /** * The payload passed to the suspended effect's perform call. * Undefined when suspension occurred outside of an effect handler. */ readonly effectArg?: unknown; } /** * Generate a UUID for identifying a run() or resume() call. * Uses crypto.randomUUID() when available, falls back to a simple generator. */ export declare function generateUUID(): string; /** * Create a Snapshot with a freshly generated unique `id`. * effectArg is converted to plain JS so it serializes cleanly via JSON * and compares correctly in tests without PV/PM internals leaking out. */ export declare function createSnapshot(fields: Omit): Snapshot; /** * Lazy accessor for the current execution context at a node evaluation point. * Only allocated when the caller actually invokes `getContinuation()` — the * coverage path never calls it, so there is no allocation on the hot path. */ export interface Continuation { /** Current lexical environment — read variable bindings from here. */ env: ContextStack; /** Continuation stack — use to reconstruct the call stack for display. */ k: ContinuationStack; /** Resume execution from this point (for debugger "continue" / "step"). */ resume: () => void; /** All post-effect snapshots taken so far — enables time travel. */ getSnapshots: () => Snapshot[]; } /** * Mutable snapshot state that lives for the duration of a single * `runEffectLoop` invocation. Threaded through tick → dispatchPerform → * dispatchHostHandler so that host handlers can access and create snapshots. */ export interface SnapshotState { /** Accumulated snapshots, oldest first. */ readonly snapshots: Snapshot[]; /** High-water mark counter for snapshot indices (never reused, even across rollbacks). */ nextSnapshotIndex: number; /** UUID identifying this run()/resume() call. */ readonly executionId: string; /** Maximum number of snapshots to retain. Oldest are evicted when exceeded. */ readonly maxSnapshots?: number; /** When true, automatically capture a checkpoint at program start and after every non-checkpoint effect. */ readonly autoCheckpoint?: boolean; /** When true, always create a terminal snapshot on completion/error/halt even if autoCheckpoint is false. */ readonly terminalSnapshot?: boolean; /** * Optional hook called on every AST node evaluation. Used for coverage tracking and debugging. * `getContinuation` is lazy — only call it when you need env/k/resume (e.g. on a breakpoint hit). * For coverage, record `node[2]` (the node ID) and return without calling `getContinuation()`. */ onNodeEval?: (node: AstNode, getContinuation: () => Continuation) => void | Promise; /** * Program-level cleanup callbacks — registered by host effect handlers * via `ctx.onScopeExit` when there is no enclosing Dvala handler frame * to attach to. Fires at program completion (completed / halted / error) * in LIFO. Each entry records its source effect name for error messages. * * Treated exactly like frame-level cleanups for snapshot/suspend * restrictions — the runtime refuses capture while any program-level * cleanup is live, because a serialized continuation that outlives the * process cannot be followed by the cleanup. * * Lazily initialized — undefined until `onScopeExit` without an * enclosing frame needs it. */ programCleanups?: { callback: () => void | Promise; effectName: string; }[]; } /** * Context passed to a host effect handler. * * The handler must call exactly one of `resume`, `suspend`, `fail`, or `next`, * exactly once. Calling more than one, or calling any more than once, is a * programming error. */ export interface EffectContext { /** Full dotted name of the performed effect (useful for wildcard handlers). */ effectName: string; /** The single payload from the Dvala `perform(eff, payload)` call. */ arg: unknown; /** * Aborted when: `race()` branch loses, runtime is disposed, or host cancels. * Combine with timeout: `AbortSignal.any([signal, AbortSignal.timeout(ms)])` */ signal: AbortSignal; /** * Resume the program with the given value (or a Promise that resolves to one). * The value becomes the result of the `perform(...)` expression in Dvala. */ resume: (value: unknown) => void; /** * Propagate as a Dvala-level error. If `msg` is provided it overrides the * default error message. The error flows through `dvala.error` handlers. */ fail: (msg?: string) => void; /** * Suspend the program. The entire execution state is captured and returned * in `RunResult` as `{ type: 'suspended', snapshot }`. * `meta` is passed through to `Snapshot.meta` for domain context * (e.g., assignee, deadline, priority). */ suspend: (meta?: unknown) => void; /** * Pass to the next registered handler whose pattern matches this effect. * If no further handler matches, the effect is unhandled. */ next: () => void; /** All snapshots taken so far, oldest first. Read-only view. */ snapshots: readonly Snapshot[]; /** * Explicitly capture a snapshot at the current continuation point. * Returns the new Snapshot. This is the host-side equivalent of * `perform(@dvala.checkpoint)`. */ checkpoint: (message: string, meta?: unknown) => Snapshot; /** * Abandon current execution and resume from a previous snapshot. * All snapshots after the target are discarded. */ resumeFrom: (snapshot: Snapshot, value: unknown) => void; /** * Halt the program immediately. Returns a `{ type: 'halted', value }` result. * Unlike `fail()`, this does not trigger error handlers — it's a clean termination. * If `value` is omitted, defaults to `null`. */ halt: (value?: unknown) => void; /** * Register a cleanup callback tied to the nearest enclosing Dvala handler * frame at the moment of this effect call. The callback fires (in LIFO * order with other registrations on the same frame) when that frame * terminally exits — normal completion, abort via a non-resuming clause, * or snapshot discard. * * Use this for host-side resource cleanup (closing files, releasing * connections). While any registered callback is live, snapshot capture * and continuation multi-shot are refused with a runtime error — the * runtime cannot guarantee cleanup if the computation is allowed to * serialize or re-enter a torn-down frame. * * Callbacks may be async; they are awaited sequentially during scope * exit. They may not perform Dvala effects (they run after the Dvala * execution window for that scope). Errors thrown by one callback do * not block subsequent callbacks; they aggregate and surface once all * cleanups have run. * * If no enclosing Dvala handler frame exists (the effect was performed * at top level with no wrapping handler), the callback is registered * on a program-level list that fires at program completion or halt. * * See design doc: `design/archive/2026-04-19_host-scoped-resources.md`. */ onScopeExit: (callback: () => void | Promise) => void; } /** A function that handles an effect by calling `resume`, `suspend`, `fail`, `halt`, or `next`. */ export type EffectHandler = (ctx: EffectContext) => void | Promise; /** A single handler registration: a pattern (e.g. `'llm.complete'`, `'dvala.*'`, `'*'`) paired with its handler. */ export interface HandlerRegistration { pattern: string; handler: EffectHandler; } /** An ordered list of effect handler registrations. Earlier entries are checked first. */ export type Handlers = HandlerRegistration[]; /** * Create a handler registration for `@dvala.host` from a plain record. * * The returned handler resumes with the record value when the name is found, * or calls `fail()` when it's not. Works with both `run()` (sync) and * `runAsync()` (async). * * @example * ```typescript * import { hostHandler } from '@mojir/dvala' * dvala.runAsync(source, { * effectHandlers: [hostHandler({ configExists: true, dirName: '/app' })] * }) * ``` */ export declare function hostHandler(values: Record): HandlerRegistration; /** * Test whether a handler pattern key matches a given effect name. * * Rules: * - No wildcard → exact match only * - `.*` suffix → matches the named effect itself AND all descendants * (dot boundary enforced: `dvala.*` matches `dvala.error` but NOT `dvalaXXX`) * - `*` alone → matches everything */ export declare function qualifiedNameMatchesPattern(name: string, pattern: string): boolean; /** * Find all matching async handlers for an effect name, in registration order. * Returns an array of `[pattern, handler]` pairs. */ export declare function findMatchingHandlers(effectName: string, handlers: Handlers | undefined): [string, EffectHandler][]; /** * The result of `run()` — always resolves, never rejects. * Errors are captured in the `error` variant. * * When time travel is enabled, `completed` and `error` results include a * terminal snapshot containing the checkpoint history for debugging/replay. */ export type RunResult = { type: 'completed'; value: unknown; scope?: Record; snapshot?: Snapshot; sourceMap?: SourceMap; } | { type: 'suspended'; snapshot: Snapshot; sourceMap?: SourceMap; /** @internal Raw continuation and snapshots from branch suspension — used by executeParallelBranches for composition */ _rawSuspension?: { k: ContinuationStack; snapshots: Snapshot[]; nextSnapshotIndex: number; meta?: unknown; effectName?: string; effectArg?: Any; }; } | { type: 'error'; error: DvalaError; snapshot?: Snapshot; sourceMap?: SourceMap; } | { type: 'halted'; value: unknown; snapshot?: Snapshot; sourceMap?: SourceMap; }; /** * Thrown (as a promise rejection) by `suspend()` inside a host handler. * Caught by the effect trampoline loop — NOT by Dvala-level try/catch. */ export declare class SuspensionSignal { /** The captured continuation stack at the point of suspension. */ readonly k: ContinuationStack; /** Accumulated snapshots at the point of suspension. */ readonly snapshots: Snapshot[]; /** High-water mark for snapshot indices at the point of suspension. */ readonly nextSnapshotIndex: number; /** Optional domain metadata passed through to RunResult. */ readonly meta?: unknown | undefined; /** The effect name being handled when suspend() was called. */ readonly effectName?: string | undefined; /** The effect payload being handled when suspend() was called. */ readonly effectArg?: Any | undefined; readonly _brand: "SuspensionSignal"; constructor( /** The captured continuation stack at the point of suspension. */ k: ContinuationStack, /** Accumulated snapshots at the point of suspension. */ snapshots: Snapshot[], /** High-water mark for snapshot indices at the point of suspension. */ nextSnapshotIndex: number, /** Optional domain metadata passed through to RunResult. */ meta?: unknown | undefined, /** The effect name being handled when suspend() was called. */ effectName?: string | undefined, /** The effect payload being handled when suspend() was called. */ effectArg?: Any | undefined); } export declare function isSuspensionSignal(value: unknown): value is SuspensionSignal; /** * Thrown (as a promise rejection) by `resumeFrom()` inside a host handler. * Caught by the effect trampoline loop — NOT by Dvala-level try/catch. * Carries the serialized continuation from the target snapshot, the value * to resume with, and the snapshot index for trimming. */ export declare class ResumeFromSignal { /** The serialized continuation from the target snapshot. */ readonly continuation: unknown; /** The value to feed into the restored continuation. */ readonly value: Any; /** Snapshots with index > trimToIndex will be discarded. */ readonly trimToIndex: number; /** Parallel/race branch boundary path at the resumeFrom() call site. */ readonly boundaryPath: string[]; readonly _brand: "ResumeFromSignal"; constructor( /** The serialized continuation from the target snapshot. */ continuation: unknown, /** The value to feed into the restored continuation. */ value: Any, /** Snapshots with index > trimToIndex will be discarded. */ trimToIndex: number, /** Parallel/race branch boundary path at the resumeFrom() call site. */ boundaryPath?: string[]); } export declare function isResumeFromSignal(value: unknown): value is ResumeFromSignal; /** * Thrown (as a promise rejection) by `halt()` inside a host handler. * Caught by the effect trampoline loop — NOT by Dvala-level try/catch. * Terminates execution immediately and returns a halted result. */ export declare class HaltSignal { /** The value to return as the halted result. */ readonly value: Any; /** Accumulated snapshots at the point of halt. */ readonly snapshots: Snapshot[]; /** High-water mark for snapshot indices at the point of halt. */ readonly nextSnapshotIndex: number; readonly _brand: "HaltSignal"; constructor( /** The value to return as the halted result. */ value: Any, /** Accumulated snapshots at the point of halt. */ snapshots: Snapshot[], /** High-water mark for snapshot indices at the point of halt. */ nextSnapshotIndex: number); } export declare function isHaltSignal(value: unknown): value is HaltSignal;