/** * async-plan.ts — the SHARED, LANGUAGE-AGNOSTIC async/sync propagation pass (bc#97 increment 2). * * # Why this is one pass, not per-emitter logic * * async/sync is a language-runtime concern that MUST NOT enter the portable IR (a rust-async IR ≠ a go IR * — the "one IR → N emitters" invariant would break, exactly the go/rust drift class this epic hit). So * async-ness is a codegen INPUT (the consumer declares which TERMINAL handlers do async I/O), and the * derived per-node/per-runner `isAsync` is computed HERE, once, from pure graph structure — NOT inside * each emitter. Each language emitter then only CONSUMES the annotation: * - rust / (future python native) — have async/await → emit `async fn` + `.await` where `isAsync`. * - go — no async/await concept → IGNORE the annotation entirely (no-op, blocking calls as today). * * # The model (issue #97 design, agreed) * * 1. PER-TERMINAL-HANDLER I/O model. The consumer declares, per terminal node (a leaf op handler — * GetItem / Query / BatchGet …), whether that handler does async I/O. The declaration is keyed to the * handler's CATALOG COMPONENT name (a consumer implements one handler per leaf op and declares e.g. * "my `Query` handler is async"); a body node is an async terminal iff its `component` is declared * async. UNSPECIFIED → sync (the neutral floor; runtime-free; biased toward no consumer). No third * "pass-through" state. * 2. NOT in the IR (see above) — this pass takes the IR + the declaration as separate inputs. * 3. NO await nodes in the DAG. The DAG carries resolved values; await is a handler-call-boundary concern * emitted by the language emitter, never an IR/DAG node. This pass annotates NODES (call boundaries), * it does not rewrite the graph. * 4. BOTTOM-UP propagation. A runner (and any intermediate node / parent sub-runner) is async IFF it * transitively calls an async terminal handler OR an async sub-runner. Pure / no-I/O nodes stay sync. * Mixed sync+async terminals in one async runner is fine (sync handlers are called directly). * 5. await placement is DERIVED (by the emitter) from the per-node annotation this pass produces — the * emitter awaits an async terminal's future at the call site where its result is consumed. * * # Bottom-up over sub-runners * * A covered bc read compiles to ONE flat runner that dispatches only to terminal handlers (`node_*`) — * there is no runner→runner call on the covered path today, so `runnerIsAsync` collapses to "any terminal * node is async". But the pass is written to be correct if a node ever references another component (a * sub-runner): such a node is async iff the referenced component's runner is async (transitive). We fold * that in via a fixpoint over the component graph so the SSoT stays valid as coverage grows. */ import type { ComponentGraphIR } from "../behavior.js"; import type { IoModel } from "./core.js"; export type { IoModel }; /** * AsyncPlan — the per-component derived async annotation the emitters consume. `nodeIsAsync(nodeId)` is * true iff that body node's handler call must be awaited (an async TERMINAL, or a node that calls an async * sub-runner). `runnerIsAsync` is true iff the runner awaits anything (⇒ the emitter makes it `async fn`). * A `cond` node (no handler) is never async. */ export interface AsyncPlan { /** true iff node `nodeId`'s handler-call boundary must be awaited (per-node granularity). */ nodeIsAsync(nodeId: string): boolean; /** true iff the runner awaits at least one call site ⇒ it must be emitted as `async fn`. */ readonly runnerIsAsync: boolean; /** true iff EVERY handler-dispatching node in the runner is async (⇒ the uniform increment-1 shape; * lets the emitter assert the increment-1 non-regression: uniform async ≡ every node awaited). */ readonly allTerminalsAsync: boolean; /** DERIVED (issue #97 item 5) join!-eligible groups: node-id groups (each ≥2 async, mutually * independent per the concurrency stage = no data-dependency edge) that MAY be issued as concurrent * futures and join!ed. `join!` EMIT is optional; this derivation must be correct. A dependent chain * yields NO group (its awaits are strictly sequential). */ readonly concurrentAsyncGroups: string[][]; } /** * buildAsyncPlans — the SSoT propagation pass. Given the whole IR + the I/O-model declaration, compute a * per-COMPONENT AsyncPlan bottom-up. Language-agnostic: the result is a pure function of the component * graph structure + the declaration, so rust / (future) python native / a hypothetical ts-native all read * the SAME annotation and can never drift; go ignores it. * * Propagation (fixpoint over sub-runner references, monotone ⇒ terminates): * - a TERMINAL node (componentRef / map) is async iff its catalog component is declared async, OR the * component it references is itself an async runner (a sub-runner call — transitive); * - a runner is async iff ANY of its nodes is async; * - iterate to fixpoint (a runner becoming async can make a caller node async). * On the covered path there are no sub-runner references, so this converges in one pass to "runner async * iff any terminal declared async" — but the fixpoint keeps the SSoT correct as coverage grows. */ export declare function buildAsyncPlans(ir: ComponentGraphIR, io: IoModel): Map; //# sourceMappingURL=async-plan.d.ts.map