import type { AgencyNode, TypeAliasEntry, VariableType } from "../types.js"; import type { Refine, NarrowCandidate } from "./narrowing.js"; import { Scope, type ScopeType } from "./scope.js"; import type { Reference } from "./pathSegments.js"; export { type PathSegment, type Reference, segKey, referenceKey, isPrefixOf, toSegment, chainToSegments, stablePrefix, } from "./pathSegments.js"; /** The DECLARED (un-narrowed) type of a path, from the base var's scope type. */ export declare function declaredPathType(scope: Scope, ref: Reference, aliases: Record): ScopeType; /** * A program point. Type checking builds and discards this graph per check; it * NEVER appears in AST output, so it cannot affect lowering, codegen, or * interrupt handling. (See the flow-typed checker spec, "Interrupt safety".) */ export type FlowNode = { kind: "start"; scope: Scope; } | { kind: "assign"; prev: FlowNode; ref: Reference; type: ScopeType; } | { kind: "narrow"; prev: FlowNode; ref: Reference; refine: Refine; } | { kind: "join"; prev: FlowNode[]; } | { kind: "loop"; prev: FlowNode; widened: Record; } | { kind: "exit"; }; /** * The typeAt memo plus the scope-tree generation it was filled under. A BOX * shared by reference across every env that wraps the same check run — * including the spread copies the synthesizer makes ({ ...ctx.flowEnv, * typeAliases }) — so an invalidation made through any env is visible to all. * Mutate the box fields in place; the `readonly` on FlowEnvironment.memo * makes replacing the box itself a compile error. */ export type FlowMemo = { gen: number; map: WeakMap>; }; /** A fresh memo box. gen -1 guarantees the first typeAt query stamps it. */ export declare function freshMemo(): FlowMemo; export type FlowEnvironment = { scope: Scope; flowOf: WeakMap; typeAliases: Record; /** * Per-flow-node, per-reference-key memo for typeAt. WeakMap because keys are * FlowNode identities; without it, nested joins/loops re-walk super-linearly. * * Invalidation is AUTOMATIC for Scope mutations: `start` nodes read * `scope.lookup(...)` live, so any scope mutation stales the cache — typeAt * compares `memo.gen` against the scope tree generation * (Scope.currentGeneration) on entry and rebuilds the map on mismatch. * Passes that retype scope entries (e.g. computeMatchExprTypes) need no * manual reset: their declare() calls bump the generation. A FlowNode * patched IN PLACE (assignFlow.type = ...) is invisible to the counter and * still needs a paired declare() bump — see matchConsumerAssignFlows. */ readonly memo: FlowMemo; /** * The end-of-body flow node per scopeKey, populated by `buildFlowGraphs`. * `exit` means every path through that scope diverges (returns). Consumed by * definite-return checking. Optional: bare envs built in tests don't set it. */ scopeTerminals?: Record; /** * The `assign` flow node created for each expression-match consumer * assignment (`const x = match(...)`, tagged `matchExprSource`). Populated by * the flow builder's assignment rule; consumed by `computeMatchExprTypes`, * which runs AFTER `buildFlowGraphs` (so yield synthesis sees narrowing) and * therefore must patch the eagerly-snapshotted `type` on this node with the * computed union — otherwise downstream reads of `x` resolve through typeAt * to the stale "any" recorded at build time. The paired consumer re-declare * bumps the generation, so the memo needs no manual reset. Optional: bare * envs in tests. */ matchConsumerAssignFlows?: WeakMap; }; /** * Union construction for flow joins. `any` dominates (a branch we can't type * makes the join untyped); `never` is the identity element (drops out); members * are deduped structurally; a single survivor unwraps; an empty union is * `never`. Literal members are preserved (not widened to primitives) so a * discriminant union survives a join with full precision. * * Dedup keys via `typeKey` (typeKey.ts), which resolves top-level aliases, * ignores property order and non-semantic metadata, and keys recursive * references nominally — the gaps raw `JSON.stringify` keying had. */ export declare function uniteTypes(types: ScopeType[], aliases: Record): ScopeType; /** * Refine a (non-any) type by a single refine. Returns the narrowed type, or * null for "no narrowing". Delegates to `narrowByRefine` (the shared dispatcher) * so discriminant and presence narrowing stay in lockstep with the legacy path; * both are sound/conservative (Result is viewed as a union via resultUnion.ts). */ export declare function applyRefine(base: VariableType, refine: Refine, aliases: Record): VariableType | null; /** * The type of `ref` at flow node `at`. The single oracle every pass consults. * Memoized per (flow node, reference key). */ export declare function typeAt(ref: Reference, at: FlowNode, env: FlowEnvironment): ScopeType; /** * Wrap `flow` in a `narrow` node for each candidate (innermost = first). With no * candidates the flow is returned unchanged. Used by the builder (PR 1b) to turn * a branch's ConditionFacts into flow nodes. */ export declare function wrapFacts(flow: FlowNode, candidates: NarrowCandidate[]): FlowNode; /** * Does a `narrow` node for `ref` (exact key) apply on the flow path before any * rebinding of it? Walk back until the first node that (re)establishes the ref's * base — `start`/`loop`/`exit`, or an `assign` to `ref` or a prefix of it — * returning true iff a `narrow` for `ref` is seen first. `synthValueAccess` uses * this to route a member-path read through `typeAt` ONLY when narrowing genuinely * applies, so an un-narrowed path still hits the structural walk's diagnostics * (e.g. strict member access on un-guarded `b.r.value`). */ export declare function flowHasNarrowFor(ref: Reference, flow: FlowNode): boolean; /** * Merge control-flow branch ends. `exit` predecessors (e.g. a branch that * `return`ed) are dropped: reaching the code after a merge means a live branch * got here. No live branches → `exit` (the after-code is unreachable). One → * itself. Two or more → a `join`. */ export declare function mergeFlows(flows: FlowNode[]): FlowNode; /** * Widen at a loop back-edge. For each name, compute its pre-loop and body-end * type; if they differ (the body reassigned it) widen to their union, else pass * the pre-loop type through. Sound: never under-widens a variable the body * changed. Can over-widen when a narrowing actually holds across iterations — * acceptable, documented (no fixpoint). * * The unchanged-check uses `typeKey` equality (same as `uniteTypes`), so * alias-vs-body and property-order differences no longer pessimistically * widen. The `"any"` sentinel short-circuits before typeKey, which only * accepts real VariableTypes. */ export declare function widenAtLoopBackEdge(loopEntry: FlowNode, bodyEnd: FlowNode, names: string[], env: FlowEnvironment): FlowNode;