import { AgencyProgram } from "../types.js"; import { AgencyConfig } from "../config.js"; import type { TsNode } from "../ir/tsIR.js"; import type { CompilationUnit } from "../compilationUnit.js"; export declare class TypeScriptBuilder { private generatedStatements; private generatedTypeAliases; /** * TypeAlias AST nodes whose declaration has been hoisted to the * containing function/node's outer scope (so it's visible to every * runner.step closure). When processNode sees one of these inside a * body, it returns ts.empty() to avoid a redeclaration. */ private hoistedTypeAliasNodes; private importStatements; private toolRegistrations; private functionsUsed; private scopes; private steps; private names; private tracking; private pipes; private assigns; private finalize; private agencyConfig; private adjacentNodes; private currentAdjacentNodes; private isInsideGraphNode; private loopVars; private insideHandlerBody; private handlerFuncResultCounter; private insideGlobalInit; private plainForLoopCounter; private insidePlainMatchExpr; private _asyncBranchCheckNeeded; private _sourceMapBuilder; private compilationUnit; private moduleId; private outputFile; /** * Optional per-module init plan + cross-module alias resolver produced * by `compileClosure`. When present, drives: * - the order of local static/global assignments (topsort-ordered * rather than source-ordered), and * - the per-phase `await __awaitStaticInit(...)` / * `__awaitGlobalsInit(...)` prelude emitted in this module's * init functions, and * - the PR-1 trap message: `__readStatic` is called with the * SOURCE moduleId of an imported binding, not the placeholder * "" used before. * * When absent (callers that haven't migrated to compileClosure, or * single-file unit tests), codegen falls back to source order and * omits the cross-module awaits — the lazy per-function * `isInitialized` guards plus the runtime trap remain the safety net. */ private initPlan?; /** * @param config - Agency compiler configuration (model defaults, logging, etc.) * @param info - Pre-collected program metadata (function definitions, graph nodes, imports, type hints) * @param moduleId - Unique identifier for this module (e.g., "foo.agency"), used to * namespace global variables in the GlobalStore so that different modules' globals * don't collide. Must be consistent between the defining module and any importers. * @param outputFile - Absolute path where the generated code will be written. * Used to compute relative import paths for stdlib. If not provided, falls * back to resolving moduleId against cwd. * @param initPlan - Optional per-module init plan + resolver from * `compileClosure`. See the field docstring above. */ constructor(config: AgencyConfig | undefined, info: CompilationUnit, moduleId: string, outputFile?: string, initPlan?: TypeScriptBuilder["initPlan"]); private configDefaults; /** Convert a TsNode to string (for use in template-based methods) */ private str; /** Returns the template opts for checkpoint creation (moduleId, scopeName, stepPath as JSON-quoted strings). */ private checkpointOpts; private forkBranchSetup; /** Generate a TsNode for `hasInterrupts(x)` */ private interruptCheck; /** Generate a raw string for `hasInterrupts(x)` */ private interruptCheckRaw; private agencyFileToDefaultImportName; private needsParensLeft; private needsParensRight; build(program: AgencyProgram): TsNode; /** Process a function call argument, unwrapping NamedArgument and SplatExpression. */ private processCallArg; /** Process resolved arguments into TsNodes, tracking function usage. */ private processResolvedArgs; /** * Adjust call-site arguments to match the function's parameter list: * 1. Pad omitted optional args (those with defaults) with null * 2. Wrap extra args into an array for variadic params */ private processNode; private processKeyword; /** * Walk a function/node body and collect every typeAlias declaration * that belongs to this body's scope. Used to hoist body-local type * aliases up to the enclosing function/node's outer scope so the * generated zod schemas are visible to every runner.step closure. * * Delegates body recursion to `walkNodes` so any new body-bearing * AST node (thread, parallelBlock, seqBlock, …) is automatically * handled. Aliases nested inside a function/graphNode/class method * are skipped — those defs hoist their own aliases when their bodies * are built. */ private collectBodyTypeAliases; /** * Hoist body-local type aliases. Returns the generated TS declarations * (to be inserted at the top of the enclosing function/node body) and * marks each AST node so processNode skips its in-body emission. * * Coalesces duplicates by alias name: if the same name appears in * multiple branches/blocks, only the first declaration is emitted to * avoid redeclaration errors at the function scope. (The Agency * typechecker is responsible for diagnosing genuine name collisions.) */ private hoistBodyTypeAliases; private processTypeAlias; /** * Build a zod validation schema string for a VariableType taken from user * source. Deep-resolves user-defined generic aliases first so that uses * like `Container` become a concrete object/array/etc. before the * (alias-unaware) zod mapper runs. */ private zodSchemaFor; /** See build() — plain top-level aliases in source order. */ private aliasEmissionOrder; /** * Aliases whose schema const is NOT yet initialized when module-load * code originating at source offset `start` runs: every plain top-level * alias declared at-or-after that offset. A PURE derivation from the * program captured in build() — no mutable emitted-set to keep in sync. * Generic/value-param aliases never emit consts (inlined / hoisted * factory functions) and function-body aliases initialize at call time, * so neither is listed. `start` undefined treats ALL listed aliases as * pending — z.lazy is always safe; over-inclusion only costs * indirection. */ private pendingAliasesFor; /** * Build the validation expression for a `!` site. If the resolved type * carries no `@validate(...)` tag anywhere, return the existing * `__validateType(value, schema)` call (zero behavior change). Otherwise * return `await __validateChainRecursive(value, , __ctx)`, * which runs Zod parse + the validator chain at each level. */ private validateExpr; private processComment; private processAgencyObject; private processAgencyArray; private generateLiteral; private generateStringLiteralNode; private processValueAccess; /** Emit an assignment/update *target*. A `valueAccess` target must stay a * raw lvalue (never `__nn`-wrapped); anything else goes through the normal * value path. Used by `++`/`--` and compound-assignment (`+=` etc.). */ private processAssignTarget; private awaitChainCall; private processBinOpExpression; private processRegexMatchExpression; private processCatchExpression; private processPipeExpression; private processTryExpression; private processNewExpression; private processSchemaExpression; /** * A type pattern's runtime test (`x is T`, arm `p: T`). Tier 1 (coarse * primitives and `any[]`) compiles to `__coarseTypeTest(v, kind)`; every * other type reuses the bang's validation path via `validateExpr`, wrapped * in `isSuccess(...)` — shape AND `@validate` validators decide the match. */ private processTypeTestExpression; /** * After the ALS migration AND the trailing-`state`-arg drop, every * Agency call site reads `ctx` / `stack` / `threads` / `callsite` * from the active `agencyStore` frame. `__call` / `__callMethod` no * longer accept a state-extras object, and the codegen never emits * one. This helper is kept as a stable seam in case future codegen * needs to attach per-call metadata, but for now it always returns * `undefined` — callers should simply omit the third positional. * * Async-fork sites that need to override the active branch stack * wrap their `__call(...)` invocation in an `agencyStore.run` * frame in codegen (see `emitRuntimeDispatchCall`) — not via this * helper. */ private buildStateConfig; private processIfElseWithSteps; private processForLoopWithSteps; private processWhileLoopWithSteps; private processMatchBlockWithSteps; /** Lowered `return` inside a match arm used as an expression. Same * halt+return shape as `processReturnStatement`, but the value is stored as * the match result via `runner.exitMatch` and control unwinds to the owning * ifElse rather than the enclosing function. Never produced by the parser — * only by pattern lowering (Task 6). */ /** A graph-node call is a control-flow transition, not a value, so it cannot * be the result of a match/if arm. `processMatchYield` catches the common * case where the arm value is yielded directly; but a single-expression arm * whose value may interrupt is hoisted to a temp binding first (#430), which * hides the call from that check — so `_processAssignmentInner` re-runs this * guard on the marked binding. Shared here to keep one error message. */ private assertMatchArmValueNotGraphNode; private processMatchYield; /** * Plain-mode (handler body) routing for the two lowered shapes of a branching * construct: a match expression (literal arms → `matchBlock`, pattern arms → * `ifElse`, both carrying `matchExprId`) becomes a self-contained IIFE; a plain * statement match / if compiles as ordinary JS. Single source of truth so the * `matchBlock` and `ifElse` dispatch cases can't drift apart — a mismatch would * silently emit a statement match whose consumer reads an unassigned * `__matchval_N` as `undefined`. */ private processPlainBranching; /** * Compile an expression-position `match` inside a handler body (plain mode). * * Stepped code unwinds a match arm's value via `runner.exitMatch` + the owning * `runner.ifElse` clearing `_matchExit`. A handler body compiles to plain JS * with no owning `runner.ifElse`, so that protocol would leak the flag and * silently skip the rest of the enclosing node. Instead we emit the arm * dispatch as an async IIFE whose arms `return` their value, and store the * result in the `__matchval_` frame local the consumer reads: * * __stack.locals.__matchval_ = await (async () => { })(); * * `node` is either the pass-through `matchBlock` (literal arms) or the lowered * `ifElse` chain (pattern arms) — both carry `matchExprId`. `processBlockPlain` * builds the plain if/else for either; `insidePlainMatchExpr` makes the arms' * `matchYield` nodes emit real returns out of the IIFE. */ private processMatchExpressionPlain; private processImportStatement; private processImportNodeStatement; /** * Process a block argument into a wrapped AgencyFunction TsNode. * Shared by generateFunctionCallExpression and buildCallDescriptor. */ private processBlockArgument; /** * Compile a BlockArgument that appears as a standalone expression * (e.g. as a named arg value in .partial(), or assigned to a variable). * Uses type annotations on the block params if present, falls back to 'any'. */ private processBlockAsExpression; /** * Classify a parameter's contribution to a tool's JSON schema. * * Single source of truth for "what does this param contribute?" — consumed * by `buildToolDefinition` (and only by it). Three outcomes: * * - `drop` : function-typed, function-union, or variadic-of-function. * Omitted from the schema entirely. The required-vs-optional * distinction is enforced separately by the tool-binding * validator (lib/typeChecker/toolBlockBinding.ts). * - `scalar` : a regular field — type derived from the declared hint * (`string` is the historical default for untyped params). * Optional params get `.nullable().describe("Default: ...")` * appended so the LLM understands the value can be omitted. * - `array` : a variadic param whose element type is non-function. The * emitted schema is `z.array()` so the LLM supplies * the whole spread as a single array via the named-array * calling convention (`foo(rest: [1,2,3])`). */ private paramSchemaContribution; /** * Build a tool definition TsNode for an Agency function. * Returns ts.id("null") if the function has no parameters (no schema needed for tools). * * Every "should this param appear in the schema?" decision is funneled * through `paramSchemaContribution` — the single classifier that knows * which params are dropped (function-typed, function-union, variadic of * function), which become scalar, and which become array (variadic). * No ad-hoc filter or inline predicate is allowed in this function; the * spec's discipline rule (§4.6 rule #3) is enforced by code review. */ private buildToolDefinition; /** * Returns true if any function or graph node in the compilation unit * has a doc string with at least one interpolation segment. */ private hasDocStringInterpolation; /** * Generate __toolRegistry as an empty object. AgencyFunction.create() calls * register local functions into it. Imported and builtin tools are registered * here directly. The reviver is bound at the end. */ private generateToolRegistry; /** * For Result-returning functions: emit a pinned checkpoint at function entry * and a preamble that applies arg overrides on restore (for result.retry()). */ private buildResultCheckpointSetup; /** * Build the body statements for an Agency function or class method. * Includes setup, runner, result checkpoint, try/catch/finally, hooks. * Shared between processFunctionDefinition and class method compilation. */ private buildFunctionBody; private processFunctionDefinition; private processStatement; private interruptTemplateArgs; private buildInterruptReturnStructured; private extractInterruptFields; private isInterruptExpression; private processInterruptStatement; private processFunctionCallAsStatement; private processFunctionCall; private generateFunctionCallExpression; private emitRuntimeDispatchCall; /** * Emit a plain direct function call: `f(arg1, arg2, blockArg?)`. * Context-injected builtins reuse this with `prependArgs = * [__ctx]`; the registry lookup at the call site is what marks the * intent, no separate method needed. */ private emitDirectFunctionCall; /** * Build a CallType descriptor TsNode for an Agency function call. * Determines whether to emit positional or named call type based on arguments. */ private buildCallDescriptor; private processForkCall; private generateNodeCallExpression; private processGraphNode; /** If the enclosing function/node has returnTypeValidated, wrap value in __validateType */ private maybeWrapReturnValidation; private processGotoStatement; private processReturnStatement; /** In a finalize-bearing scope, a direct-call return must stop at the * finalize instead of passing an aborted result through (pass-through * would silently skip the finalize). AG6036 guarantees a direct call * is the only call-bearing return shape that reaches codegen here; * `return llm(...)` hoists through its own path and cannot produce an * AbortedResult. */ private isFinalizeInterceptedReturn; private processAssignment; /** True when an assignment RHS can evaluate to a bubbled Interrupt[] * without any halt check of its own: a tryExpression anywhere in a binOp * tree. `__tryCall` and `__catchResult` both forward a non-Result value * unchanged, and `||`/`??` pass a (truthy, non-null) array through — so * `try f()`, `try f() catch v`, and `try f() || x` all deliver the raw * batch to the assignment. Plain functionCall RHS is handled by its own * richer branch (async fork setup etc.), not this predicate. */ private rhsMayBubbleInterrupts; /** The propagation guards emitted after a sync assignment whose RHS is * a call (or a `try` expression that may deliver a callee's raw * result). Two markers can come back instead of a normal value, and * both must stop this scope: * * - an Interrupt[] — the callee paused; halt so the batch propagates * (awaitAll first), or throw inside a handler body, where interrupts * are not allowed. * - an AbortedResult — the callee was aborted; this scope stops too and * returns ITS OWN saved draft via carryThrough (the callee's partial * is dropped: salvage is opt-in per level). Nodes and handler bodies * rebuild the exception instead, so everything above compiled code * (the graph engine, the CLI entry) sees aborts exactly as before. * * Returns null only at global scope for the interrupt half; the aborted * check still applies there (an abort during module init should crash * init, not become data in a global). */ private assignmentInterruptGuard; /** The aborted-result half of the post-call guard. Function and block * scopes halt with their own AbortedResult (carryThrough applies the * salvage rule); handler bodies and every other scope rebuild the * exception — handlers run outside the runner-step machinery (there is * no runner to halt), and nothing above compiled code consumes aborted * values. * * In a finalize-bearing scope, the stop runs the finalize — and when * the guarded value is a real local (`bindOnAborted`), the callee's * partial is first bound into it via partialValueOrNull(), so the * finalize reads it like any other local. Bare-call temps have nothing * to bind. */ private assignmentAbortedGuard; private _processAssignmentInner; /** * Process an llm() function call. Generates a direct runPrompt() call * (no inner function wrapper). Handles async/sync, interrupt checking, * response format from type hints, and tools from config object. */ private processLlmCall; private processDebuggerStatement; private processMessageThread; private processBlockPlain; /** * Emit a plain-JS for-loop (used inside handler bodies) that iterates arrays * by (element, index) and records/plain objects by (key, value) — the same * array-vs-record normalization `Runner.loop` applies in stepped mode. The * old emission looped `i < iterable.length` over `iterable[i]`, which over a * record iterated zero times (`.length` is undefined) and made `for (k in * record)` a `for...of` over a non-iterable object (a TypeError). All three * for-loop implementations (this, `Runner.loop`, the type checker) now agree. * * const __src = ; * const __isArr = Array.isArray(__src); * const __keys = __isArr ? __src * : (__src != null && typeof __src === "object" ? Object.keys(__src) : []); * for (let __i = 0; __i < __keys.length; __i++) { * const = __keys[__i]; // element | key * const = __isArr ? __i : __src[__keys[__i]]; // index | value * ...body * } */ private processPlainForLoop; /** * Compile the synthetic `__objectRest(source, ["a", "b", ...])` call emitted * by the pattern lowering pass for `let { a, b, ...rest } = obj` into a * native-JS IIFE. No runtime helper required. * * __objectRest(source, ["a", "b"]) * → (({ a: __k0, b: __k1, ...__r }) => __r)() * * For `let { ...rest } = obj` (no excluded keys), emits * (({ ...__r }) => __r)() */ private buildObjectRestIIFE; private processNodeInGlobalInit; private buildHandlerArrow; private processHandleBlockWithSteps; private processWithModifier; /** In debugger mode, insert debuggerStatement nodes before each * step-triggering statement so that debugStep() is called at every * substep boundary, not just top-level steps. */ private insertDebugSteps; private processBodyAsParts; private generateBuiltins; /** Build the baked `smoltalkDefaults` object: nested per-provider `apiKey` * and `baseUrl` maps (each key/URL falling back to its conventional env * var), plus model/logLevel/statelog and an optional default provider. * `ollama` is intentionally omitted from `apiKey` — it uses OLLAMA_HOST. */ private buildSmoltalkDefaults; private generateImports; private preprocess; private postprocess; }