/** * Per-variable initialization dependency graphs. * * Walks every top-level declaration + bare statement in a set of * pre-collected agency programs (the entry's full import closure) and * produces two independent dep graphs: * * - `staticGraph` — one node per top-level `static const`, edges from * each node to every other init-var node its initializer references. * Sorted by `topSortInitGraph` to drive Phase A (once per process). * - `globalGraph` — one node per non-static `const` / `let` / * unscoped assignment, plus one node per bare top-level statement * (function call etc., keyed by a synthetic `__bareStmt_…` name). * Drives Phase B (every run). * * Edges are derived only from *direct* free-variable references in the * initializer expression. Function references (`def foo`, `node foo`) * are NOT edges — the dep graph orders *values*, not callable code. * The runtime read-before-init trap (PR 1) catches the residual case * where an initializer indirectly reads an unset static through a * function call. * * Cross-module references are resolved through the shared * {@link ImportAliasResolver}, which walks `export { x } from "y"` * chains to the ultimate source. The same resolver is reused by the * codegen wrap site (Task 4) to thread the source `moduleId` into the * PR-1 trap message — there is exactly one re-export resolver in the * codebase. * * **Closed interface:** consumers see only `nodes + edges` per graph; * file-import depth and source line are baked into each node's * `sequenceHint` at build time so the topsort has one ordering rule. * * **Closure walking is NOT done here.** The caller (Task 3's * `compileClosure`) is responsible for parsing every reachable module * once and passing the full `programs` map in. Keeps this module pure * and testable. * * Phase coupling rules enforced by this builder: * - A static initializer that references a global → throws * `StaticReferencesGlobalError` (the global doesn't exist yet at * Phase A). * - A global initializer that references a static → allowed, * no edge (statics are already initialized at Phase B time). */ import type { AgencyProgram, AgencyNode, Expression } from "../types.js"; import type { SourceLocation } from "../types/base.js"; import type { FunctionDefinition } from "../types/function.js"; import type { SymbolTable } from "../symbolTable.js"; export type InitVarKind = "static" | "global"; /** * One node in an init dep graph — corresponds to a single top-level * declaration (or, for the global graph only, a single bare top-level * statement). * * `moduleId` is the absolute path of the source file the var was * declared in (NOT the file that imports it). For re-exports, the * canonical node lives in the originating source module — re-exporters * do not get their own node. * * `sequenceHint` packs `(fileImportDepth, sourceLine)` into one number * so the topsort can break ties deterministically with a single key. * Lower wins (initializes earlier). */ export type InitVarNode = { moduleId: string; varName: string; kind: InitVarKind; /** For bare statements (global graph only) this is the wrapping * statement node; for assignments it's the right-hand side. */ initExpr: Expression | AgencyNode; loc?: SourceLocation; exported: boolean; sequenceHint: number; /** Set when the source wrapped the decl/statement in `with approve`. * `handle { ... }` blocks are NOT legal at module top level, so this * single optional flag covers the full top-level handler surface. */ withApprove?: boolean; }; /** Composite key for indexing init-var nodes: `${moduleId}::${varName}`. */ export type InitVarKey = string; export declare function makeKey(moduleId: string, varName: string): InitVarKey; /** * Edges go from a node to every other node its initializer references. * `topSortInitGraph` uses these to compute initialization order; cycles * produce a `CycleError`. */ export type InitDepGraph = { nodes: Record; edges: Record; }; export type BuildInitDepGraphsResult = { staticGraph: InitDepGraph; globalGraph: InitDepGraph; /** Reused by codegen (Task 4) for PR-1 thread-through. */ resolver: ImportAliasResolver; /** Look-up `(name | namespace.member) → top-level FunctionDefinition`, * scoped to the importing module. PR-2.5 uses it to drive depth-1 * expansion of init-expression deps through direct function calls. */ functionDefs: FunctionDefLookup; }; /** * Resolves a name used inside one module to the top-level Agency * function definition that backs it. Returns the function's home * `moduleId` alongside the AST so callers can resolve free identifiers * *inside* the function body against the function's own import surface * — not the caller's. * * `find` handles bare-name calls (`getBar()`). `findNamespaceMember` * handles namespace-prefixed calls (`bar.getBar()`). * * Returns `null` for names that don't resolve to an Agency function we * have AST for — stdlib calls, function values stored in variables, * unknown names, etc. The depth-1 expansion treats those as "no * expansion", and the runtime read-before-init trap (PR 1) remains the * safety net for anything the static analysis can't see. * * Built once per `compileClosure` call; cached per importing module so * a lookup is O(1) after first use per module. */ export type FunctionDefLookup = { find(name: string, inModuleId: string): { moduleId: string; def: FunctionDefinition; } | null; findNamespaceMember(prefix: string, member: string, inModuleId: string): { moduleId: string; def: FunctionDefinition; } | null; }; /** * Build a `FunctionDefLookup` over the entry's full import closure. * * Two layers of cache: * - `localDefsByModule[moduleId]` — name → FunctionDefinition for all * top-level `def` statements declared in that module. Populated * lazily; same shape used as the destination of both bare-name and * namespace-member lookups. * - `cache[inModuleId]` — name → resolved `(moduleId, def)` for the * importing module's view (local defs + named-import aliases of * functions declared elsewhere). Populated lazily on first lookup. */ export declare function makeFunctionDefLookup(programs: Record, resolver: ImportAliasResolver, symbolTable: SymbolTable | undefined): FunctionDefLookup; /** * Compile-time error: a `static` initializer references a `global`. The * global doesn't exist yet at Phase A time, so this is unsatisfiable. * * Surface format prefers human-readable names: `static const x` is shown * as `static const 'x'`, while a `static ` whose synthetic varName * starts with `__bareStmt_` is shown as `static `. Line * numbers fall back to `?` only when neither the static wrapper nor the * dep node has a usable `loc`. */ export declare class StaticReferencesGlobalError extends Error { readonly staticNode: InitVarNode; readonly globalNode: InitVarNode; constructor(staticNode: InitVarNode, globalNode: InitVarNode); } /** * Resolves a locally-bound name (used inside an initializer in some * module) to the `(moduleId, name)` pair that defines the value, walking * `export { x } from "y"` chains to the ultimate source. * * `resolveNamespace` covers the `import * as bar from "./bar.agency"` * shape: given the local prefix `bar`, returns the source module so * `bar.barStatic` can be resolved as `(bar.agency, barStatic)`. Returns * null when no namespace import bound that prefix in this module. * * Built once per `compileClosure` call; cached internally so a name * lookup is O(1) after first use per module. */ export type ImportAliasResolver = { resolve(localName: string, inModuleId: string): { sourceModuleId: string; sourceName: string; } | null; resolveNamespace(prefix: string, inModuleId: string): { sourceModuleId: string; } | null; }; export declare function makeImportAliasResolver(programs: Record, symbolTable: SymbolTable | undefined): ImportAliasResolver; /** * Build the two phase-separated dep graphs from the entry's full * pre-parsed import closure. * * `programs` maps absolute module paths to their parsed AST and MUST * already contain every module reachable from `entryModuleId`. We do * not re-walk imports — the entry point (`compileClosure`) owns that. * * `symbolTable`, when provided, enables re-export chain resolution. * Without it, re-exporters appear as unresolved local names and produce * no edge — fine for the simplest unit tests but pulls the * `importedAlias` codegen path into the same starvation case the * PR-1 trap covers, so production callers should always pass one. */ export declare function buildInitDepGraphs(programs: Record, symbolTable: SymbolTable | undefined, entryModuleId: string): BuildInitDepGraphsResult; /** * Walk an init expression for **direct call sites** — both bare-name * calls (`getBar()`) and namespace-method calls (`bar.getBar()`) — and * return the FunctionDefinitions they resolve to, paired with the * function's home module. Used by the depth-1 expansion to discover * which functions to look inside. * * Skips identifiers inside nested `function` / `graphNode` bodies via * the same ancestor check `collectFreeIdentifiers` uses, so when we * are already mid-walk of one function's body we don't accidentally * descend into another function defined alongside it. * * `collectFreeIdentifiers` cannot do this job because `walkNodes`'s * `functionCall` branch deliberately does not yield the function name * as a `variableName` (the call's identifier isn't a value * reference). Method-call chain elements (`obj.method()`) likewise * don't surface as `member` free refs — they're `methodCall` chain * entries, not `property` chain entries. */ export declare function collectDirectCalls(expr: Expression | AgencyNode, inModuleId: string, functionDefs: FunctionDefLookup): { moduleId: string; def: FunctionDefinition; }[]; /** * Collect every free identifier in a function body, treating each top- * level statement of the body independently so the function's *own* * `function` node isn't on the ancestor stack (which would cause * `collectFreeIdentifiers` to skip everything by design). The result * still skips any further nested `function` / `graphNode` bodies — the * depth-1 boundary holds. * * **Parameter-shadow filtering.** Drops refs whose name (or `prefix`, * for member refs) matches one of the function's parameter names. * Otherwise a parameter that happens to share a name with a top-level * decl (e.g. `def readG(g: string) { return g }` when a top-level * `g` exists) would resolve through the import alias resolver to the * top-level binding and produce a spurious init edge or false * `StaticReferencesGlobalError`. */ export declare function collectFunctionBodyFreeRefs(def: FunctionDefinition): FreeRef[]; /** * A single free reference inside an initializer expression: * - `name`: a bare identifier like `barStatic` (resolved via the * `resolve` alias map). * - `member`: a two-part reference of the form `prefix.member` * where `prefix` is bound by a local declaration. The dep graph * treats it as an edge target only when `prefix` was bound by a * namespace import (`import * as bar from "./bar.agency"`), in * which case it resolves to `(bar.agency, member)` — the same * edge a named import would have produced. Local-variable * member accesses (`person.name`) produce no edge. */ export type FreeRef = { kind: "name"; name: string; } | { kind: "member"; prefix: string; member: string; }; /** * Collect every free reference in `expr`, declaratively, via the * shared `walkNodes` walker. Skips identifiers that appear inside * nested name-binding constructs (`function`, `graphNode`) by checking * the ancestor stack — those bodies don't execute during the outer * initializer evaluation. * * Surfaces both bare identifiers (`barStatic`) and `prefix.member` * patterns (`bar.barStatic`) so the dep graph can resolve namespace * imports as cross-module edges. When a variableName is the base of * a `prefix.member`-shape valueAccess we skip its standalone yield; * the member form supersedes it. * * Exported so callers outside this module (e.g. `compileClosure`) can * reuse the same free-reference discipline when computing additional * cross-phase await dependencies that aren't representable as edges in * either single-phase graph. */ export declare function collectFreeIdentifiers(expr: Expression | AgencyNode): FreeRef[];