import type { AgencyNode, Expression, TypeAliasEntry, IfElse, VariableType } from "../types.js"; import type { StringLiteralType, NumberLiteralType, BooleanLiteralType } from "../types/typeHints.js"; import { Scope } from "./scope.js"; import type { Reference } from "./pathSegments.js"; /** * What a candidate narrows to. Tagged so a new narrowing form slots in as one * more `narrowByRefine` case. `discriminant` filters a union by * `v.prop == literal` — and drives Result narrowing too (`isSuccess`/`isFailure`/ * `if (r.success)` all narrow on the `success` field, with Result viewed as a * union via `resultToObjectUnion`). `presence` filters the `null` member for * optional / truthiness narrowing (`if (x != null)` / `if (x)`). */ export type Refine = { kind: "discriminant"; prop: string; literal: StringLiteralType | NumberLiteralType | BooleanLiteralType; keep: boolean; } | { kind: "presence"; present: boolean; } | { kind: "typeTest"; testedType: VariableType; }; export type NarrowCandidate = { ref: Reference; refine: Refine; }; export type ConditionFacts = { then: NarrowCandidate[]; else: NarrowCandidate[]; }; /** * The single Refine dispatcher: given a refine + the variable's current * (pre-resolved) type, the narrowed type, or null for "no narrowing". Used by * BOTH the flow path (`applyRefine`, flow.ts) and the legacy child-scope path * (`applyNarrowing`). The switch is exhaustive, so a new `Refine` variant is a * compile error here. (The "how" — child scope, reassignment gate, * declareLocal — lives in `applyNarrowing`.) */ export declare function narrowByRefine(refine: Refine, current: VariableType, aliases: Record): VariableType | null; /** * Inspect a (post-lowering) boolean condition and report the narrowing * candidates it implies for the then- and else-branches. * * Increment 1 recognizes a single `isSuccess(x)` / `isFailure(x)` guard where * `x` is a bare variable. Both names are RESERVED_FUNCTION_NAMES * (lib/typeChecker/resolveCall.ts) and cannot be user-redefined, so matching on * the function name is unambiguous — no resolveCall lookup is required. */ export declare function analyzeCondition(condition: Expression): ConditionFacts; /** * Filter a union's members by `prop == literal` (keep) or `prop != literal` * (!keep). Sound/conservative: drops only provably-excluded members; never * narrows to `never`; non-union → null (no narrowing). */ export declare function narrowUnionByDiscriminant(type: VariableType, prop: string, literal: StringLiteralType | NumberLiteralType | BooleanLiteralType, keep: boolean, aliases: Record): VariableType | null; /** * Filter the `null` member of a union for presence narrowing. * - `present: true` (e.g. `if (x != null)`): drop the `null` member. * - `present: false` (e.g. `if (x == null)`): keep only the `null` member. * Returns `null` (no narrowing) for a non-union type, a union with no `null` * member, or any result that would be empty — so it never narrows to `never`. */ export declare function narrowUnionByPresence(type: VariableType, present: boolean, aliases: Record): VariableType | null; /** * Conservative "this body always transfers control out of the enclosing * function" check. Increment 2 counts ONLY `return`: `raise` (interrupt) can * resume and continue, and `propagate` semantics are likewise non-trivial, so * treating either as an exit could be unsound. False negatives are fine — they * only cost a missed narrowing, never a wrong one. */ export declare function alwaysExits(body: AgencyNode[]): boolean; /** * Facts that hold for the statements *after* an `if`, given which branch (if * any) always exits. If the then-branch exits and the else doesn't (or is * absent), reaching the after-code means the condition was false → else-facts. * Symmetrically for an exiting else-branch. If both or neither exit, nothing * is known (both-exit ⇒ after-code is dead; neither ⇒ both paths merge). */ export declare function postGuardFacts(node: IfElse, facts: ConditionFacts): NarrowCandidate[]; /** * Apply narrowing candidates to a fresh child scope for one branch body. * * Soundness gate: if the branch reassigns the variable (`r = ...`) ANYWHERE * in its whole body, skip narrowing it — its type may change mid-branch. * * Two intentional sources of imprecision (both conservative — false negatives, * never false positives, so soundness is preserved): * 1. The scan is whole-body. A reassignment buried in a nested `if`'s OTHER * branch still blocks narrowing in the access site. A future increment * could thread a flow analysis here; not worth it yet. * 2. `walkNodes` doesn't know about scoping, so a `for (r in xs)` iterator * or a nested function that shadows `r` would also trip the gate. * * Refinements are written with declareLocal so they vanish when the child * scope is dropped at branch exit and never leak to the function scope. A * narrowed binding is the concrete member type (e.g. a Result's success-member * object type), so nothing branch-specific persists once the child is dropped. */ export declare function applyNarrowing(childScope: Scope, candidates: NarrowCandidate[], branchBody: AgencyNode[], typeAliases: Record): void; /** * The declarative one-call helper that every walkScopeBody narrowing site uses. * * Encapsulates the three-step "create a throwaway child scope, install * refinements into it, then walk the branch body in that scope" recipe so * callers say *what* they want ("walk this body under these narrowings") * rather than open-coding the *how*. Adding a new narrowing site (e.g., * a future `for`-loop body if the spec ever calls for it) becomes a single * call rather than three lines copy-pasted. * * The generic `ctx` + injected `walk` keeps this module free of any * dependency on `scopes.ts` (which is where `walkScopeBody` and * `TypeCheckerContext` live), avoiding a circular import. */ export declare function walkWithNarrowing(parent: Scope, body: AgencyNode[], candidates: NarrowCandidate[], typeAliases: Record, ctx: C, walk: (body: AgencyNode[], scope: Scope, ctx: C) => void): void;