From 0e137cce272a8195c036df103573f82900590e86 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Sun, 14 Jun 2026 13:40:44 +0000 Subject: [PATCH] feat(cfg): Swift CFG visitor + def/use harvest (#2195 U12) Add createSwiftCfgVisitor + swift-harvest (vendored tree-sitter-swift via requireVendoredGrammar): if/else + optional binding (if let), guard...else (diverging early exit), for-in/while/repeat-while (bottom-test), switch (no implicit fallthrough; explicit fallthrough keyword; where guards), do/catch + try/try?/try!, defer (LIFO finalizer at scope exit), labeled break/continue, control_transfer_statement (one node for break/continue/ return/throw). Wire into swiftProvider. Every literal validated against the vendored grammar via the probe (no block node; if-let folds into condition+bound_identifier; defer parses as a call_expression with trailing closure). while true keeps EXIT reverse-reachable (production CDG probe: 3 edges). 24 real-parser tests; comprehensive sweep green (543). Gaps: computed properties, defer block-scope approx, fatalError traps. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../ingestion/cfg/visitors/swift-harvest.ts | 548 +++++++++++ .../src/core/ingestion/cfg/visitors/swift.ts | 919 ++++++++++++++++++ .../src/core/ingestion/languages/swift.ts | 2 + .../cfg/fixtures/swift-hazards.swift | 136 +++ gitnexus/test/unit/cfg/swift-visitor.test.ts | 326 +++++++ 5 files changed, 1931 insertions(+) create mode 100644 gitnexus/src/core/ingestion/cfg/visitors/swift-harvest.ts create mode 100644 gitnexus/src/core/ingestion/cfg/visitors/swift.ts create mode 100644 gitnexus/test/integration/cfg/fixtures/swift-hazards.swift create mode 100644 gitnexus/test/unit/cfg/swift-visitor.test.ts diff --git a/gitnexus/src/core/ingestion/cfg/visitors/swift-harvest.ts b/gitnexus/src/core/ingestion/cfg/visitors/swift-harvest.ts new file mode 100644 index 000000000..6e5fa9649 --- /dev/null +++ b/gitnexus/src/core/ingestion/cfg/visitors/swift-harvest.ts @@ -0,0 +1,548 @@ +/** + * Swift def/use harvester (#2195) — the Swift analogue of + * {@link import('./typescript-harvest.js').TsHarvester} and the C-family / Go / + * Rust / Python harvesters. Like the Python / Rust harvesters it harvests NO + * call-site `sites[]` (the call-site taint substrate is a later step): it emits + * only the per-function binding table ({@link BindingEntry}[]) plus + * {@link StatementFacts} (defs / uses / mayDefs) via a local + * {@link FactAccumulator} with no site machinery, so the produced facts never + * carry a `sites` key. + * + * Runs in the parse worker next to the Swift CFG visitor. Output is the binding + * table the {@link import('../cfg-builder.js').CfgBuilder} stamps onto the CFG, + * plus the per-block def/use facts the reaching-defs / CDG solvers consume. + * + * Every node type and field literal below was grammar-validated against the + * VENDORED tree-sitter-swift via the introspection probe before use (mandatory + * pre-step). Swift shapes pre-empted (verified by a real parse): + * - functions: `function_declaration` / `init_declaration` / `deinit_declaration` + * (field `body`=`function_body`, which wraps a `statements` node) and + * `lambda_literal` (a closure — its `statements` follow an optional + * `lambda_function_type` + `in`, NO `function_body` wrapper). + * - parameters: `parameter` (fields `external_name`?/`name`=`simple_identifier`, + * plus a type child). A closure's parameters live in `lambda_function_type` → + * `lambda_function_type_parameters` (bare `simple_identifier`s). + * - `property_declaration` — Swift's `let`/`var` binding: a `value_binding_pattern` + * (`mutability` = `let`/`var`), then repeated `name`=`pattern` + `value`= pairs + * (`let p = 1, q = 2`). A `pattern` binds via `bound_identifier`=`simple_identifier` + * or nests `pattern`s for tuple destructuring (`let (a, b) = pair`). + * - optional binding (`if let` / `while let` / `guard let`): a `value_binding_pattern` + * in the construct's `condition` fields, then a `bound_identifier` field and the + * bound value as further `condition` fields. + * - `for_statement` fields `item`=`pattern` / `collection` / optional `where_clause`. + * - `catch_block` field `error`=`pattern` (the bound error). + * - reads: `simple_identifier`, `navigation_expression` (`a.b` — fields + * `target`/`suffix`), `call_expression` (`f()` — `call_suffix`), + * `assignment` (fields `target`/`operator`/`result`). + * + * TWO-PHASE, ORDER-INDEPENDENT (load-bearing — mirrors the Rust / Go / C + * harvesters): the CFG walk is NOT source-order (`repeat … while` builds the + * condition after the body), so resolving names against a scope stack populated + * *during* the walk would mis-resolve. Phase 1 pre-scans the whole function + * subtree once, declaring every bound name into ONE function table; phase 2 + * resolves defs/uses against that finished table from any walk order. Swift DOES + * have block scope + shadowing, but a single function table is the documented v1 + * simplification used by the Python / Rust harvesters — distinct shadowing + * redeclarations of the same name collapse onto one binding (an over-approximation + * that can falsely kill across a shadow, the sound direction for taint). + * + * v1 def-semantics scope: + * - `property_declaration` (`let`/`var PAT = …`) — each `simple_identifier` + * leaf of every `name` pattern is a def; the values are walked for uses. + * - `assignment` plain `=` — a plain-identifier target is a def; a + * `navigation_expression` / subscript target (`self.x = …`, `a[i] = …`) is + * NOT a scalar def (its root is a use). A compound `+=`/`-=`/… target + * def-AND-uses the lvalue. + * - `for x in xs` — the loop pattern's leaves are defs, the collection a use. + * - optional binding (`if let` / `while let` / `guard let`) binds its pattern. + * - `catch_block`'s `error` pattern binds. + * - parameters (incl. closure params) are `param`-kind defs. + * EXCLUDED, deliberately (TypeScript-CFA precedent): member / subscript writes + * (`obj.f = …`, `a[i] = …`) are NOT scalar defs — their root identifiers are + * uses only. Nested-function bodies (`lambda_literal`, a nested + * `function_declaration`) are opaque in BOTH directions (captured reads/writes + * invisible). + * + * MAY-DEFS: a def inside a conditionally-evaluated subexpression — the right + * operand of `&&` / `||` short-circuit, and a switch-case `where` guard / case + * test — is a may-def (gen WITHOUT kill), so the not-taken path's prior def is + * not falsely killed. A `while let` re-test binding is also a may-def (the bind + * does not happen on the exit iteration). + * + * Identifiers with no in-function declaration (module/global functions, types, + * enum cases) resolve to a SYNTHETIC module-level binding (`name@module`), + * applied identically by def and use harvesting. + * + * NOTE: nothing serialized here may carry a field named `nodeId` — the durable + * parsedfile-store reviver dedups objects keyed on that field name. + */ +import type { SyntaxNode } from '../../utils/ast-helpers.js'; +import type { BindingEntry, StatementFacts } from '../types.js'; + +/** Node types that own a nested CFG — their subtrees are opaque to harvesting. */ +const NESTED_FUNCTION_TYPES = new Set([ + 'function_declaration', + 'init_declaration', + 'deinit_declaration', + 'lambda_literal', +]); + +/** + * Minimal ordered, deduplicating def/use collector for one statement record. + * Deliberately NOT the shared {@link import('./call-site-harvest.js') + * CallSiteFactAccumulator} — this unit harvests NO call sites (taint substrate + * is a later step), so a local accumulator with only the def/use/may-def + * machinery keeps `swift-harvest.ts` free of site logic and guarantees the + * emitted facts carry no `sites` key (mirrors the Python / Rust harvesters). + */ +class FactAccumulator { + private readonly defs: number[] = []; + private readonly uses: number[] = []; + private readonly mayDefs: number[] = []; + private readonly defSeen = new Set(); + private readonly useSeen = new Set(); + private readonly mayDefSeen = new Set(); + + constructor(private readonly line: number) {} + + addDef(idx: number): void { + if (this.defSeen.has(idx)) return; + this.defSeen.add(idx); + this.defs.push(idx); + } + + /** A def that may not execute (conditional context) — gen without kill. */ + addMayDef(idx: number): void { + if (this.mayDefSeen.has(idx)) return; + this.mayDefSeen.add(idx); + this.mayDefs.push(idx); + } + + addUse(idx: number): void { + if (this.useSeen.has(idx)) return; + this.useSeen.add(idx); + this.uses.push(idx); + } + + defCount(): number { + return this.defs.length + this.mayDefs.length; + } + + useCount(): number { + return this.uses.length; + } + + finish(): StatementFacts { + return { + line: this.line, + defs: this.defs, + uses: this.uses, + // Stay absent when empty — keeps the serialized side-channel payload lean. + ...(this.mayDefs.length > 0 ? { mayDefs: this.mayDefs } : {}), + }; + } +} + +export class SwiftHarvester { + private readonly bindings: BindingEntry[] = []; + /** Single function-scope name → binding index (v1: no block scope). */ + private readonly table = new Map(); + private readonly synthetic = new Map(); + private readonly fnId: number; + /** >0 while walking a conditionally-evaluated subexpression — defs become may-defs. */ + private conditionalDepth = 0; + + constructor(private readonly fnNode: SyntaxNode) { + this.fnId = fnNode.id; + this.declareParams(fnNode); + const body = this.bodyOf(fnNode); + if (body) this.prescan(body); + } + + /** The completed binding table — pass to `CfgBuilder.finish`. */ + bindingTable(): readonly BindingEntry[] { + return this.bindings; + } + + /** + * The function/closure body `statements` node. A `function_declaration` / + * `init_declaration` / `deinit_declaration` wraps it in a `function_body`; a + * `lambda_literal` carries the `statements` directly. + */ + private bodyOf(fnNode: SyntaxNode): SyntaxNode | undefined { + const fb = fnNode.childForFieldName('body') ?? fnNode.namedChildren.find((c) => c.type === 'function_body'); + if (fb && fb.type === 'function_body') { + return fb.namedChildren.find((c) => c.type === 'statements') ?? fb; + } + // lambda_literal — its `statements` is a direct named child. + return fnNode.namedChildren.find((c) => c.type === 'statements'); + } + + // ── phase 1: declaration pre-scan ──────────────────────────────────────── + + private declare(nameNode: SyntaxNode, kind: BindingEntry['kind']): void { + const name = nameNode.text; + if (!name || name === '_' || this.table.has(name)) return; + this.table.set(name, this.bindings.length); + this.bindings.push({ + name, + declLine: nameNode.startPosition.row + 1, + declColumn: nameNode.startPosition.column, + kind, + }); + } + + /** Declare every parameter binder of a fn / init / closure. */ + private declareParams(fnNode: SyntaxNode): void { + for (const p of fnNode.namedChildren) { + if (p.type === 'parameter') { + const name = p.childForFieldName('name'); + if (name && name.type === 'simple_identifier') this.declare(name, 'param'); + } + } + // Closure params live in lambda_function_type → lambda_function_type_parameters. + const lambdaType = fnNode.namedChildren.find((c) => c.type === 'lambda_function_type'); + if (lambdaType) this.declareClosureParams(lambdaType); + } + + private declareClosureParams(lambdaType: SyntaxNode): void { + for (const params of lambdaType.namedChildren) { + if (params.type !== 'lambda_function_type_parameters') continue; + for (const id of params.namedChildren) { + if (id.type === 'simple_identifier') this.declare(id, 'param'); + else if (id.type === 'lambda_parameter') { + const name = id.childForFieldName('name') ?? id.namedChildren.find((c) => c.type === 'simple_identifier'); + if (name) this.declare(name, 'param'); + } + } + } + } + + /** + * Pre-scan the function body once, declaring every bound name. Recurses into + * compound expressions but NOT into nested `function_declaration` / + * `lambda_literal` bodies (opaque). + */ + private prescan(node: SyntaxNode): void { + const t = node.type; + if (NESTED_FUNCTION_TYPES.has(t) && node.id !== this.fnId) return; + + switch (t) { + case 'property_declaration': + // `let`/`var PAT = …` — declare every `name` pattern's leaves. + for (let i = 0; i < node.childCount; i++) { + if (node.fieldNameForChild(i) === 'name') { + const pat = node.child(i); + if (pat) this.declarePattern(pat); + } + } + break; + case 'for_statement': { + const pat = node.childForFieldName('item'); + if (pat) this.declarePattern(pat); + break; + } + case 'catch_block': { + const err = node.childForFieldName('error'); + if (err) this.declarePattern(err); + break; + } + default: + // Optional binding (`if let` / `while let` / `guard let`): a + // `value_binding_pattern` condition followed by a `bound_identifier`. + if (t === 'if_statement' || t === 'while_statement' || t === 'guard_statement') { + this.declareOptionalBindings(node); + } + break; + } + + for (let i = 0; i < node.namedChildCount; i++) { + const c = node.namedChild(i); + if (c) this.prescan(c); + } + } + + /** Declare the `bound_identifier` of each optional binding in a condition. */ + private declareOptionalBindings(node: SyntaxNode): void { + for (let i = 0; i < node.childCount; i++) { + if (node.fieldNameForChild(i) === 'bound_identifier') { + const id = node.child(i); + if (id) this.declare(id, 'let'); + } + } + } + + /** + * Declare every `simple_identifier` leaf of a binding pattern. Handles the + * common Swift pattern shapes: a `bound_identifier` simple pattern and tuple + * destructuring (`(a, b)`), which nests `pattern` children. `_` (the wildcard) + * binds nothing. + */ + private declarePattern(pat: SyntaxNode): void { + const bound = pat.childForFieldName?.('bound_identifier'); + if (bound && bound.type === 'simple_identifier') { + this.declare(bound, 'let'); + return; + } + if (pat.type === 'simple_identifier') { + this.declare(pat, 'let'); + return; + } + // Tuple / nested pattern — recurse into child patterns / identifiers. + for (let i = 0; i < pat.namedChildCount; i++) { + const c = pat.namedChild(i); + if (!c) continue; + if (c.type === 'pattern') this.declarePattern(c); + else if (c.type === 'simple_identifier') this.declare(c, 'let'); + else if (c.type === 'value_binding_pattern') continue; + else this.declarePattern(c); + } + } + + // ── phase 2: per-statement fact extraction ─────────────────────────────── + + /** Def/use facts for one statement (or construct-header expression) node. */ + facts(node: SyntaxNode): StatementFacts { + const acc = new FactAccumulator(node.startPosition.row + 1); + this.walkValue(node, acc); + return acc.finish(); + } + + /** Facts for an expression whose WHOLE evaluation is conditional (guards/tests). */ + factsConditional(node: SyntaxNode): StatementFacts { + const acc = new FactAccumulator(node.startPosition.row + 1); + this.conditional(() => this.walkValue(node, acc)); + return acc.finish(); + } + + /** + * Facts for a `for item in COLLECTION` head: the loop pattern's leaves are + * defs, the iterated collection a use. The `where` guard (if any) is harvested + * conditionally. + */ + forHeadFacts(stmt: SyntaxNode): StatementFacts { + const acc = new FactAccumulator(stmt.startPosition.row + 1); + const collection = stmt.childForFieldName('collection'); + const item = stmt.childForFieldName('item'); + if (collection) this.walkValue(collection, acc); + if (item) this.defPattern(item, acc); + const where = stmt.namedChildren.find((c) => c.type === 'where_clause'); + if (where) this.conditional(() => this.walkValue(where, acc)); + return acc.finish(); + } + + /** + * Facts for an `if`/`while`/`guard` condition: optional bindings bind their + * pattern (a def — a may-def when `conditional`), and the condition expression + * children are uses. The construct's `condition` / `bound_identifier` fields are + * interleaved, so we walk all children and classify them. + */ + conditionFacts(stmt: SyntaxNode, conditional: boolean): StatementFacts { + const acc = new FactAccumulator(stmt.startPosition.row + 1); + const run = (): void => { + for (let i = 0; i < stmt.childCount; i++) { + const field = stmt.fieldNameForChild(i); + const child = stmt.child(i); + if (!child) continue; + if (field === 'bound_identifier') this.def(child, acc); + else if (field === 'condition') { + // `value_binding_pattern` (`let`) and the `=` operator carry no uses. + if (child.type === 'value_binding_pattern') continue; + if (!child.isNamed) continue; + this.walkValue(child, acc); + } + } + }; + if (conditional) this.conditional(run); + else run(); + return acc.finish(); + } + + /** ENTRY-block facts for the parameters (defs only). */ + paramFacts(): StatementFacts | undefined { + const acc = new FactAccumulator(this.fnNode.startPosition.row + 1); + for (const p of this.fnNode.namedChildren) { + if (p.type === 'parameter') { + const name = p.childForFieldName('name'); + if (name && name.type === 'simple_identifier') this.def(name, acc); + } + } + const lambdaType = this.fnNode.namedChildren.find((c) => c.type === 'lambda_function_type'); + if (lambdaType) { + for (const params of lambdaType.namedChildren) { + if (params.type !== 'lambda_function_type_parameters') continue; + for (const id of params.namedChildren) { + if (id.type === 'simple_identifier') this.def(id, acc); + else if (id.type === 'lambda_parameter') { + const name = id.childForFieldName('name') ?? id.namedChildren.find((c) => c.type === 'simple_identifier'); + if (name) this.def(name, acc); + } + } + } + } + return acc.defCount() ? acc.finish() : undefined; + } + + /** Def fact for a `catch let e` error pattern — prepend to the handler entry block. */ + catchErrorFacts(catchBlock: SyntaxNode): StatementFacts | undefined { + const err = catchBlock.childForFieldName('error'); + if (!err) return undefined; + const acc = new FactAccumulator(catchBlock.startPosition.row + 1); + this.defPattern(err, acc); + return acc.defCount() ? acc.finish() : undefined; + } + + private resolve(nameNode: SyntaxNode): number { + const name = nameNode.text; + const idx = this.table.get(name); + if (idx !== undefined) return idx; + let syn = this.synthetic.get(name); + if (syn === undefined) { + syn = this.bindings.length; + this.synthetic.set(name, syn); + this.bindings.push({ name, declLine: 0, declColumn: 0, kind: 'module', synthetic: true }); + } + return syn; + } + + private def(nameNode: SyntaxNode, acc: FactAccumulator): void { + if (nameNode.text === '_') return; // blank target defines nothing + if (this.conditionalDepth > 0) acc.addMayDef(this.resolve(nameNode)); + else acc.addDef(this.resolve(nameNode)); + } + + private use(nameNode: SyntaxNode, acc: FactAccumulator): void { + if (nameNode.text === '_') return; + acc.addUse(this.resolve(nameNode)); + } + + /** Run `fn` with defs demoted to may-defs (conditionally-evaluated context). */ + private conditional(fn: () => void): void { + this.conditionalDepth++; + try { + fn(); + } finally { + this.conditionalDepth--; + } + } + + /** + * Def each `simple_identifier` leaf of a binding pattern (the def-position + * analogue of {@link declarePattern}). Tuple destructuring recurses; `_` binds + * nothing. + */ + private defPattern(pat: SyntaxNode, acc: FactAccumulator): void { + const bound = pat.childForFieldName?.('bound_identifier'); + if (bound && bound.type === 'simple_identifier') { + this.def(bound, acc); + return; + } + if (pat.type === 'simple_identifier') { + this.def(pat, acc); + return; + } + for (let i = 0; i < pat.namedChildCount; i++) { + const c = pat.namedChild(i); + if (!c) continue; + if (c.type === 'pattern') this.defPattern(c, acc); + else if (c.type === 'simple_identifier') this.def(c, acc); + else if (c.type === 'value_binding_pattern') continue; + else this.defPattern(c, acc); + } + } + + /** Value-position walk: collect uses; route def positions to the pattern handler. */ + private walkValue(node: SyntaxNode, acc: FactAccumulator): void { + const t = node.type; + if (NESTED_FUNCTION_TYPES.has(t) && node.id !== this.fnId) return; // opaque + + switch (t) { + case 'simple_identifier': + this.use(node, acc); + return; + case 'property_declaration': { + // Walk each `value` for uses, then def each `name` pattern's leaves. + const names: SyntaxNode[] = []; + for (let i = 0; i < node.childCount; i++) { + const field = node.fieldNameForChild(i); + const child = node.child(i); + if (!child) continue; + if (field === 'value') this.walkValue(child, acc); + else if (field === 'name') names.push(child); + else if (field === 'computed_value') this.walkValue(child, acc); + } + for (const pat of names) this.defPattern(pat, acc); + return; + } + case 'assignment': { + const target = node.childForFieldName('target'); + const result = node.childForFieldName('result'); + const op = node.childForFieldName('operator')?.text ?? '='; + if (result) this.walkValue(result, acc); + if (target) { + const lv = this.unwrapAssignable(target); + if (lv.type === 'simple_identifier') { + this.def(lv, acc); + if (op !== '=') this.use(lv, acc); // compound assign reads too + } else { + // `self.x = …`, `a[i] = …` — root is a use only (not a scalar def). + this.walkValue(lv, acc); + } + } + return; + } + case 'navigation_expression': { + // `a.b` — value read of the chain root only; the suffix name is not a + // scalar binding. + const target = node.childForFieldName('target'); + if (target) this.walkValue(target, acc); + return; + } + case 'try_expression': { + // `try expr` / `try? expr` / `try! expr` — the wrapped expression's uses. + const expr = node.childForFieldName('expr'); + if (expr) this.walkValue(expr, acc); + else for (let i = 0; i < node.namedChildCount; i++) { + const c = node.namedChild(i); + if (c && c.type !== 'try_operator') this.walkValue(c, acc); + } + return; + } + case 'conjunction_expression': + case 'disjunction_expression': { + // `a && b` / `a || b` — the right operand is conditionally evaluated. + const lhs = node.childForFieldName('lhs'); + const rhs = node.childForFieldName('rhs'); + if (lhs) this.walkValue(lhs, acc); + else if (node.namedChildCount > 0) this.walkValue(node.namedChild(0)!, acc); + if (rhs) this.conditional(() => this.walkValue(rhs, acc)); + else if (node.namedChildCount > 1) { + this.conditional(() => this.walkValue(node.namedChild(node.namedChildCount - 1)!, acc)); + } + return; + } + case 'value_binding_pattern': + case 'type_identifier': + case 'user_type': + // Binding keyword / type position — no scalar value uses. + return; + default: + for (let i = 0; i < node.namedChildCount; i++) { + const c = node.namedChild(i); + if (c) this.walkValue(c, acc); + } + } + } + + /** Strip a `directly_assignable_expression` wrapper around an lvalue. */ + private unwrapAssignable(node: SyntaxNode): SyntaxNode { + let n = node; + let hops = 4; + while (n.type === 'directly_assignable_expression' && hops-- > 0) { + const inner = n.namedChild(0); + if (!inner) break; + n = inner; + } + return n; + } +} diff --git a/gitnexus/src/core/ingestion/cfg/visitors/swift.ts b/gitnexus/src/core/ingestion/cfg/visitors/swift.ts new file mode 100644 index 000000000..24cba3262 --- /dev/null +++ b/gitnexus/src/core/ingestion/cfg/visitors/swift.ts @@ -0,0 +1,919 @@ +/** + * Swift CfgVisitor (#2195) — the VENDORED-GRAMMAR, control-keyword-overloaded + * CFG target. Swift's tree-sitter grammar is unusual: a single + * `control_transfer_statement` node represents `break` / `continue` / `return` / + * `throw` (distinguished by its leading keyword child), there is no separate + * `block` node (statement lists are bare `statements` nodes), optional binding + * (`if let` / `guard let` / `while let`) is folded into the construct's + * `condition` fields with no dedicated `if_let` node, and `defer` is parsed as a + * `call_expression` to a `defer` identifier carrying a trailing-closure + * `lambda_literal` (NOT a `defer_statement`). Every node type and field literal + * below was grammar-validated against the vendored tree-sitter-swift via the + * introspection probe before use (mandatory pre-step — the grammar-literal CI + * gate maps `swift.ts → Swift`). + * + * The visitor drives the language-agnostic {@link CfgBuilder} to produce a + * serializable {@link FunctionCfg} plus a def/use harvest ({@link SwiftHarvester}) + * for the reaching-defs / CDG solvers, structured like the sibling visitors — a + * `visit_` dispatch over the control-flow taxonomy driving a + * per-function {@link ControlFlowContext} for labeled break/continue and the + * `defer` completion chain (Swift's analogue of finally / Go-defer route-through). + * + * Swift shapes pre-empted (verified by a real parse): + * - functions: `function_declaration` / `init_declaration` / `deinit_declaration` + * (field `body`=`function_body`, which wraps a `statements`) and `lambda_literal` + * (a closure — `statements` follows an optional `lambda_function_type` + `in`). + * - `if_statement` field `condition` (a plain expr, or a `value_binding_pattern` + * +`bound_identifier`+value for `if let`); the THEN body is the first + * `statements`; an optional `else` keyword is followed by EITHER a nested + * `if_statement` (`else if`) OR the else-body `statements`. + * - `guard_statement` — like `if`, but its `else` `statements` MUST diverge + * (return/throw/break/continue); the guard body continues straight-line after. + * - `for_statement` fields `item`=`pattern` / `collection` / optional `where_clause`; + * body is the trailing `statements`. + * - `while_statement` field `condition` (may be a `value_binding_pattern` for + * `while let`); body `statements`. + * - `repeat_while_statement` — BOTTOM-TEST: body `statements` then the `while` + * keyword + `condition`. + * - `switch_statement` field `expr`; children `switch_entry` (each with a + * `switch_pattern` or `default_keyword`, an optional `where_keyword`+guard, a + * `statements` body, and an optional trailing `fallthrough` keyword child). + * Cases do NOT fall through implicitly; an explicit `fallthrough` spills to the + * next case. + * - `do_statement` — `statements` body + one or more `catch_block` (field + * `error`=`pattern`); `try_expression` (`try`/`try?`/`try!`). + * - `control_transfer_statement` — break / continue / return / throw, the first + * keyword child decides; `break outer` / `continue outer` carry the label as a + * `result` `simple_identifier`; `return x` / `throw e` carry the value. + * - `statement_label` (`outer:`) — a SIBLING preceding the labeled loop/switch in + * the same `statements`, NOT a wrapper. + * + * Edge-kind contract (matches the existing visitors — RD/CDG consume these): + * - if / else (incl. `if let`) → `cond-true` / `cond-false` + * - guard → `cond-true` (body continuation) / `cond-false` (the diverging else) + * - `for` / `while` / `while let` → `cond-true` / `loop-back` / `cond-false` + * - `repeat … while` → bottom-test: body runs first, condition `loop-back` (true) + * / `cond-false` (exit) + * - `switch` dispatch → `switch-case` (NO implicit fallthrough); an explicit + * `fallthrough` → a `fallthrough` edge to the next case + * - `do`/`catch` → `throw` (each protected block edges to the first handler) + * - a `defer` (and the normal completion / each `return`) threads through the + * active defer chain as `return` (first leg) + `finally-return` (each defer's + * completion leg), LIFO + * - return / throw / break / continue → the matching terminator kind; a labeled + * `break outer` / `continue outer` targets the labeled loop frame + * - straight-line → `seq` + * + * Swift-specific modeling decisions (documented approximations): + * - `defer { … }` runs at SCOPE EXIT in LIFO order. Modeled exactly as the Go + * visitor models Go's `defer`: each registers a finalizer frame that stays + * active for the rest of the function tail, so every later `return` AND the + * normal fall-off thread through ALL active defers innermost-first. APPROXIMATION: + * a `defer` is registered at the point it executes, so a defer inside a + * not-yet-run branch is conservatively treated as active for the whole remaining + * function tail. Swift `defer` is scope-bound (block-level), not function-bound; + * modeling it as function-tail-bound is a sound over-approximation for v1. + * - `while true {}` / `repeat {} while true` may never terminate; like the + * C-family / Go / Rust visitors, this visitor ALWAYS emits the structural + * `header → loopExit` `cond-false` escape edge so EXIT stays reverse-reachable + * and the post-dominator / CDG pass is not silently skipped for the function. + * This is the single highest-risk correctness property. + * - `try` / `try?` / `try!` and a `throw` inside a `do` route to the enclosing + * `catch` conservatively (every protected block → the first handler, matching + * the C++/TS over-approximation). A `throw` with no enclosing `do/catch` routes + * to EXIT (the function propagates the error to its caller). + * - a closure (`lambda_literal`) is collected as its OWN function by `isFunction`, + * so its body gets a standalone CFG; in the ENCLOSING function it is an opaque + * straight-line value (its body is not followed inline) — except a `defer`'s + * trailing closure, which is unwrapped to model scope-exit flow. + * + * Known limitations: + * - computed properties (`var y: Int { get { … } set { … } }`) have their bodies + * inside `computed_getter` / `computed_setter` rather than a function node; v1 + * does NOT build a CFG for them (documented gap, not faked). + * - `async` / `await`: suspension points are normal straight-line flow (no + * scheduler edges). `Task { … }` closures get their own CFG like any closure. + * - block-scope shadowing in the harvest is flattened to one function table (see + * swift-harvest.ts) — a documented v1 over-approximation. + * - the panic-like `fatalError()` / forced-unwrap traps abort abnormally but + * tree-sitter sees a normal call — that abnormal path is not modeled. + * + * Returns `undefined` (never throws) for an AST shape it cannot model, so a + * malformed function never drops the whole file's CFG group (R4). + */ +import type { SyntaxNode } from '../../utils/ast-helpers.js'; +import { CfgBuilder } from '../cfg-builder.js'; +import { ControlFlowContext, wireJumpThroughFinalizers } from '../control-flow-context.js'; +import { drainFinalizerPending } from '../control-flow-context.js'; +import type { FinalizerFrame } from '../control-flow-context.js'; +import type { TraversalResult } from '../traversal-result.js'; +import type { CfgVisitor, FunctionCfg } from '../types.js'; +import { SwiftHarvester } from './swift-harvest.js'; + +/** Swift node types that own a CFG-bearing function body. */ +const SWIFT_FUNCTION_TYPES = new Set([ + 'function_declaration', + 'init_declaration', + 'deinit_declaration', + 'lambda_literal', +]); + +/** Statement node types that break a basic block (everything else coalesces). */ +const CONTROL_FLOW_TYPES = new Set([ + 'if_statement', + 'guard_statement', + 'for_statement', + 'while_statement', + 'repeat_while_statement', + 'switch_statement', + 'do_statement', + 'control_transfer_statement', + 'statements', +]); + +const startLineOf = (n: SyntaxNode): number => n.startPosition.row + 1; +const endLineOf = (n: SyntaxNode): number => n.endPosition.row + 1; + +/** A statement sequence that produced no blocks (empty body) is "transparent". */ +type SeqResult = TraversalResult | null; + +/** + * Per-function Swift walk state. One instance per function so the + * {@link ControlFlowContext}, the `defer` finalizer chain, and the label tables + * are scoped to that function and never leak across functions. + */ +class SwiftCfgWalk { + private readonly cfc = new ControlFlowContext(); + /** Stack of `do`/`catch` handler entry blocks a `throw`/`try` jumps to. */ + private readonly handlers: number[] = []; + /** Label(s) pending attachment to the NEXT pushed loop/switch frame. */ + private pendingLabels: string[] = []; + /** + * Active `defer` finalizer frames in source (push) order. Innermost-LIFO is the + * REVERSE of this list. Frames stay active for the whole function tail and are + * drained once at the top-level walk's end (mirrors the Go visitor). + */ + private readonly deferFrames: FinalizerFrame[] = []; + + constructor( + private readonly builder: CfgBuilder, + private readonly harvest: SwiftHarvester, + ) {} + + /** Statements of a `statements` node, ignoring comments. */ + private statementsOf(block: SyntaxNode): SyntaxNode[] { + return block.namedChildren.filter((c) => c.type !== 'comment' && c.type !== 'multiline_comment'); + } + + /** The body `statements` of a loop/branch node (the LAST `statements` child). */ + private bodyStatements(node: SyntaxNode): SyntaxNode | undefined { + const all = node.namedChildren.filter((c) => c.type === 'statements'); + return all.length ? all[all.length - 1] : undefined; + } + + /** Visit a body that is a `statements` node (or a single statement). */ + private visitBody(node: SyntaxNode | undefined | null): SeqResult { + if (!node) return null; + if (node.type === 'statements') return this.visitSeq(this.statementsOf(node)); + return this.visitStmt(node); + } + + /** Wire a sequence of statements, coalescing straight-line runs into blocks. */ + visitSeq(stmts: SyntaxNode[]): SeqResult { + let entry: number | undefined; + let dangling: number[] = []; + let openSimple: number | undefined; + + for (const stmt of stmts) { + if (this.isControlFlow(stmt)) { + openSimple = undefined; // close any open straight-line block + const res = this.visitStmt(stmt); + if (res === null) continue; // transparent (empty nested block / label) + if (entry === undefined) entry = res.entry; + else this.builder.connect(dangling, res.entry, 'seq'); + dangling = [...res.exits]; + } else { + if (openSimple === undefined) { + const idx = this.builder.newBlock( + startLineOf(stmt), + endLineOf(stmt), + stmt.text, + 'normal', + this.harvest.facts(stmt), + ); + if (entry === undefined) entry = idx; + else this.builder.connect(dangling, idx, 'seq'); + openSimple = idx; + dangling = [idx]; + } else { + this.builder.extendBlock(openSimple, endLineOf(stmt), stmt.text, this.harvest.facts(stmt)); + } + // A straight-line statement may contain a `try` — it can error-return. + this.wireTryExits(stmt, openSimple); + } + } + + if (entry === undefined) return null; + return { entry, exits: dangling }; + } + + /** Whether a statement node breaks the current straight-line block. */ + private isControlFlow(stmt: SyntaxNode): boolean { + if (stmt.type === 'statement_label') return true; // queue label, emit no block + if (this.isDeferCall(stmt)) return true; + return CONTROL_FLOW_TYPES.has(stmt.type); + } + + /** Dispatch one statement to its handler. Non-null except for empty / label-only. */ + visitStmt(stmt: SyntaxNode): SeqResult { + if (stmt.type === 'statement_label') { + // A label preceding its loop/switch — queue it; the construct picks it up. + const name = this.labelName(stmt); + if (name !== undefined) this.pendingLabels = [...this.pendingLabels, name]; + return null; // emits no block of its own + } + if (this.isDeferCall(stmt)) return this.visitDefer(stmt); + switch (stmt.type) { + case 'if_statement': + return this.visitIf(stmt); + case 'guard_statement': + return this.visitGuard(stmt); + case 'for_statement': + return this.visitFor(stmt); + case 'while_statement': + return this.visitWhile(stmt); + case 'repeat_while_statement': + return this.visitRepeatWhile(stmt); + case 'switch_statement': + return this.visitSwitch(stmt); + case 'do_statement': + return this.visitDo(stmt); + case 'control_transfer_statement': + return this.visitControlTransfer(stmt); + case 'statements': + return this.visitSeq(this.statementsOf(stmt)); + default: + return this.visitSimple(stmt); + } + } + + private visitSimple(stmt: SyntaxNode): TraversalResult { + const idx = this.builder.newBlock( + startLineOf(stmt), + endLineOf(stmt), + stmt.text, + 'normal', + this.harvest.facts(stmt), + ); + this.wireTryExits(stmt, idx); + return { entry: idx, exits: [idx] }; + } + + // ── control transfer (break / continue / return / throw) ───────────────── + + /** The leading keyword of a `control_transfer_statement` decides its kind. */ + private transferKeyword(stmt: SyntaxNode): string { + const first = stmt.child(0); + return first?.text ?? ''; + } + + private visitControlTransfer(stmt: SyntaxNode): TraversalResult { + const kw = this.transferKeyword(stmt); + switch (kw) { + case 'return': + return this.visitReturn(stmt); + case 'throw': + return this.visitThrow(stmt); + case 'break': + return this.visitBreak(stmt); + case 'continue': + return this.visitContinue(stmt); + default: + // `fallthrough` standalone (rare outside a switch) / unknown — straight + // through (the switch handler treats the in-case `fallthrough` keyword). + return this.visitSimple(stmt); + } + } + + /** `return [expr]` — threads through every active `defer` (LIFO) before EXIT. */ + private visitReturn(stmt: SyntaxNode): TraversalResult { + const idx = this.builder.newBlock( + startLineOf(stmt), + endLineOf(stmt), + stmt.text, + 'normal', + this.harvest.facts(stmt), + ); + this.wireTryExits(stmt, idx); + wireJumpThroughFinalizers( + this.builder, + idx, + this.cfc.finalizersForReturn(), + this.builder.exitIndex, + 'return', + ); + return { entry: idx, exits: [] }; + } + + /** `throw e` — routes to the nearest enclosing `catch` handler, else EXIT. */ + private visitThrow(stmt: SyntaxNode): TraversalResult { + const idx = this.builder.newBlock( + startLineOf(stmt), + endLineOf(stmt), + stmt.text, + 'normal', + this.harvest.facts(stmt), + ); + this.builder.edge(idx, this.currentHandler(), 'throw'); + return { entry: idx, exits: [] }; + } + + private visitBreak(stmt: SyntaxNode): TraversalResult { + const idx = this.builder.newBlock(startLineOf(stmt), endLineOf(stmt), stmt.text); + const label = this.jumpLabel(stmt); + const res = this.cfc.resolveBreak(label); + const { target, finalizers } = res ?? { + target: this.builder.exitIndex, + finalizers: this.cfc.finalizersForReturn(), + }; + wireJumpThroughFinalizers(this.builder, idx, finalizers, target, 'break'); + return { entry: idx, exits: [] }; + } + + private visitContinue(stmt: SyntaxNode): TraversalResult { + const idx = this.builder.newBlock(startLineOf(stmt), endLineOf(stmt), stmt.text); + const label = this.jumpLabel(stmt); + const res = this.cfc.resolveContinue(label); + const { target, finalizers } = res ?? { + target: this.builder.exitIndex, + finalizers: this.cfc.finalizersForReturn(), + }; + wireJumpThroughFinalizers(this.builder, idx, finalizers, target, 'continue'); + return { entry: idx, exits: [] }; + } + + /** The `result` label of a `break outer` / `continue outer`, if any. */ + private jumpLabel(stmt: SyntaxNode): string | undefined { + const result = stmt.childForFieldName('result'); + return result?.type === 'simple_identifier' ? result.text : undefined; + } + + /** The bare name of a `statement_label` (`outer:` ⇒ `outer`). */ + private labelName(label: SyntaxNode): string | undefined { + const id = label.namedChildren.find((c) => c.type === 'simple_identifier'); + if (id?.text) return id.text; + const stripped = label.text.replace(/:\s*$/, '').trim(); + return stripped || undefined; + } + + /** Take and clear the labels queued by a preceding `statement_label`. */ + private takeLabels(): string[] { + const labels = this.pendingLabels; + this.pendingLabels = []; + return labels; + } + + // ── branches ────────────────────────────────────────────────────────────── + + /** + * `if COND { … } [else { … } | else if …]`. COND may be a plain expression or + * an optional binding (`if let y = opt`). The else is the node AFTER the `else` + * keyword — a nested `if_statement` (`else if`) or the else-body `statements`. + */ + private visitIf(stmt: SyntaxNode): TraversalResult { + const header = this.builder.newBlock( + startLineOf(stmt), + this.conditionEndLine(stmt), + this.conditionText(stmt), + 'normal', + this.harvest.conditionFacts(stmt, false), + ); + + const exits: number[] = []; + const thenRes = this.visitBody(this.thenStatements(stmt)); + if (thenRes) { + this.builder.edge(header, thenRes.entry, 'cond-true'); + exits.push(...thenRes.exits); + } else { + exits.push(header); // empty then — true path falls through + } + + const elseNode = this.elseNodeOf(stmt); + if (elseNode) { + const elseRes = this.visitBody(elseNode); + if (elseRes) { + this.builder.edge(header, elseRes.entry, 'cond-false'); + exits.push(...elseRes.exits); + } else { + exits.push(header); + } + } else { + exits.push(header); // no else — false path falls through to the join + } + + return { entry: header, exits: [...new Set(exits)] }; + } + + /** + * `guard COND else { … }` — the inverse of `if`. The `else` body runs when COND + * is false and (per Swift) MUST diverge (return/throw/break/continue); it is + * branched with a `cond-false` edge. The guard body CONTINUES straight-line on + * the true path (`cond-true`). + */ + private visitGuard(stmt: SyntaxNode): TraversalResult { + const header = this.builder.newBlock( + startLineOf(stmt), + this.conditionEndLine(stmt), + this.conditionText(stmt), + 'normal', + this.harvest.conditionFacts(stmt, false), + ); + + // The else body is the guard's `statements` (after the `else` keyword). + const elseNode = this.elseNodeOf(stmt) ?? this.bodyStatements(stmt); + if (elseNode) { + const elseRes = this.visitBody(elseNode); + if (elseRes) { + this.builder.edge(header, elseRes.entry, 'cond-false'); + // The else MUST diverge; any normal exit it leaves rejoins EXIT-bound + // flow conservatively (Swift forbids fall-through, but stay robust). + this.builder.connect(elseRes.exits, this.builder.exitIndex, 'seq'); + } else { + this.builder.edge(header, this.builder.exitIndex, 'cond-false'); + } + } else { + this.builder.edge(header, this.builder.exitIndex, 'cond-false'); + } + + // The true path continues straight-line after the guard. + return { entry: header, exits: [header] }; + } + + /** The THEN body `statements` of an `if`/`guard` (the first `statements` child). */ + private thenStatements(stmt: SyntaxNode): SyntaxNode | undefined { + return stmt.namedChildren.find((c) => c.type === 'statements'); + } + + /** The node after the `else` keyword: a nested `if_statement` or the else `statements`. */ + private elseNodeOf(stmt: SyntaxNode): SyntaxNode | undefined { + let sawElse = false; + for (let i = 0; i < stmt.childCount; i++) { + const c = stmt.child(i); + if (!c) continue; + if (sawElse && c.isNamed) return c; + if (c.type === 'else') sawElse = true; + } + return undefined; + } + + /** End line of a construct's condition (the last `condition`-field child). */ + private conditionEndLine(stmt: SyntaxNode): number { + let line = startLineOf(stmt); + for (let i = 0; i < stmt.childCount; i++) { + const field = stmt.fieldNameForChild(i); + const c = stmt.child(i); + if (c && (field === 'condition' || field === 'bound_identifier')) line = endLineOf(c); + } + return line; + } + + /** Display text for a construct's condition (joined `condition`-field children). */ + private conditionText(stmt: SyntaxNode): string { + const parts: string[] = []; + const kw = stmt.child(0); + if (kw && !kw.isNamed) parts.push(kw.text); + for (let i = 0; i < stmt.childCount; i++) { + const field = stmt.fieldNameForChild(i); + const c = stmt.child(i); + if (c && (field === 'condition' || field === 'bound_identifier')) parts.push(c.text); + } + return parts.join(' ') || stmt.text; + } + + // ── loops ─────────────────────────────────────────────────────────────── + + /** `for item in COLLECTION [where …] { … }`. */ + private visitFor(stmt: SyntaxNode): TraversalResult { + const labels = this.takeLabels(); + const collection = stmt.childForFieldName('collection'); + const header = this.builder.newBlock( + startLineOf(stmt), + collection ? endLineOf(collection) : startLineOf(stmt), + this.forHeaderText(stmt), + 'normal', + this.harvest.forHeadFacts(stmt), + ); + const loopExit = this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), ''); + + this.cfc.pushLoop(header, loopExit, labels); + const body = this.visitBody(this.bodyStatements(stmt)); + this.cfc.pop(); + + if (body) { + this.builder.edge(header, body.entry, 'cond-true'); + this.builder.connect(body.exits, header, 'loop-back'); + } else { + this.builder.edge(header, header, 'loop-back'); // empty body re-iterates + } + this.builder.edge(header, loopExit, 'cond-false'); + return { entry: header, exits: [loopExit] }; + } + + private forHeaderText(stmt: SyntaxNode): string { + const item = stmt.childForFieldName('item')?.text ?? ''; + const collection = stmt.childForFieldName('collection')?.text ?? ''; + return item || collection ? `for ${item} in ${collection}` : 'for'; + } + + /** `while COND { … }` (and `while let PAT = e { … }`). */ + private visitWhile(stmt: SyntaxNode): TraversalResult { + const labels = this.takeLabels(); + const header = this.builder.newBlock( + startLineOf(stmt), + this.conditionEndLine(stmt), + this.conditionText(stmt), + 'normal', + this.harvest.conditionFacts(stmt, true), + ); + const loopExit = this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), ''); + + this.cfc.pushLoop(header, loopExit, labels); + const body = this.visitBody(this.bodyStatements(stmt)); + this.cfc.pop(); + + if (body) { + this.builder.edge(header, body.entry, 'cond-true'); + this.builder.connect(body.exits, header, 'loop-back'); + } else { + this.builder.edge(header, header, 'loop-back'); // empty body re-tests + } + // Structural exit edge — even `while true {}` keeps EXIT reverse-reachable. + this.builder.edge(header, loopExit, 'cond-false'); + return { entry: header, exits: [loopExit] }; + } + + /** + * `repeat { … } while COND` — BOTTOM-TEST: the body runs at least once, THEN + * the condition decides whether to loop back. The body entry is the loop entry; + * the condition block is the loop-back / exit decision. + */ + private visitRepeatWhile(stmt: SyntaxNode): TraversalResult { + const labels = this.takeLabels(); + const cond = stmt.childForFieldName('condition'); + const condBlock = this.builder.newBlock( + cond ? startLineOf(cond) : endLineOf(stmt), + cond ? endLineOf(cond) : endLineOf(stmt), + cond ? `while ${cond.text}` : 'while', + 'normal', + cond ? this.harvest.facts(cond) : undefined, + ); + const loopExit = this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), ''); + + // `continue` re-tests the condition; `break` leaves the loop. + this.cfc.pushLoop(condBlock, loopExit, labels); + const body = this.visitBody(this.bodyStatements(stmt)); + this.cfc.pop(); + + const backTarget = body ? body.entry : condBlock; + if (body) this.builder.connect(body.exits, condBlock, 'seq'); + this.builder.edge(condBlock, backTarget, 'loop-back'); // cond true → run body again + // Structural exit edge — even `repeat {} while true` keeps EXIT reachable. + this.builder.edge(condBlock, loopExit, 'cond-false'); + return { entry: backTarget, exits: [loopExit] }; + } + + // ── switch ─────────────────────────────────────────────────────────────── + + /** + * `switch EXPR { case … : … fallthrough? … default: … }`. Cases do NOT fall + * through implicitly (the opposite of C); an explicit `fallthrough` keyword at + * the end of a `switch_entry` spills into the NEXT entry's body. A `where` guard + * on a case is harvested conditionally onto the dispatch block. + */ + private visitSwitch(stmt: SyntaxNode): TraversalResult { + const labels = this.takeLabels(); + const value = stmt.childForFieldName('expr'); + const dispatch = this.builder.newBlock( + startLineOf(stmt), + value ? endLineOf(value) : startLineOf(stmt), + value ? `switch ${value.text}` : 'switch', + 'normal', + value ? this.harvest.facts(value) : undefined, + ); + const switchExit = this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), ''); + + this.cfc.pushSwitch(switchExit, labels); + const entries = stmt.namedChildren.filter((c) => c.type === 'switch_entry'); + + // A case `where` guard evaluates conditionally before the body — harvest its + // uses onto the dispatch block (a later case tests only when earlier ones + // didn't match; any def there is a may-def). + for (const entry of entries) { + const guard = this.entryGuard(entry); + if (guard) this.builder.attachFacts(dispatch, this.harvest.factsConditional(guard)); + } + + const entryResults = entries.map((e) => this.visitBody(this.entryBody(e))); + const hasDefault = entries.some((e) => this.entryIsDefault(e)); + + const entryOf: number[] = new Array(entries.length); + let after = switchExit; + for (let i = entries.length - 1; i >= 0; i--) { + entryOf[i] = entryResults[i]?.entry ?? after; + after = entryOf[i]; + } + + for (let i = 0; i < entries.length; i++) { + this.builder.edge(dispatch, entryOf[i], 'switch-case'); + } + if (!hasDefault) this.builder.edge(dispatch, switchExit, 'switch-case'); // no-match path + + // A case body rejoins AFTER the switch (no implicit fallthrough) UNLESS the + // entry ends in an explicit `fallthrough`, which spills into the next entry. + for (let i = 0; i < entries.length; i++) { + const res = entryResults[i]; + if (!res) continue; + const fallsThrough = this.entryFallsThrough(entries[i]); + const fallTarget = i + 1 < entries.length ? entryOf[i + 1] : switchExit; + if (fallsThrough) this.builder.connect(res.exits, fallTarget, 'fallthrough'); + else this.builder.connect(res.exits, switchExit, 'seq'); + } + + this.cfc.pop(); + return { entry: dispatch, exits: [switchExit] }; + } + + /** The `where` guard expression of a `switch_entry`, if any. */ + private entryGuard(entry: SyntaxNode): SyntaxNode | undefined { + let sawWhere = false; + for (let i = 0; i < entry.childCount; i++) { + const c = entry.child(i); + if (!c) continue; + if (sawWhere && c.isNamed) return c; + if (c.type === 'where_keyword') sawWhere = true; + } + return undefined; + } + + /** The body `statements` of a `switch_entry`. */ + private entryBody(entry: SyntaxNode): SyntaxNode | undefined { + return entry.namedChildren.find((c) => c.type === 'statements'); + } + + private entryIsDefault(entry: SyntaxNode): boolean { + return entry.namedChildren.some((c) => c.type === 'default_keyword') || + entry.children.some((c) => c.type === 'default_keyword'); + } + + /** A `switch_entry` ends in an explicit `fallthrough` keyword child. */ + private entryFallsThrough(entry: SyntaxNode): boolean { + for (let i = 0; i < entry.childCount; i++) { + if (entry.child(i)?.type === 'fallthrough') return true; + } + return false; + } + + // ── do / catch (error handling) ────────────────────────────────────────── + + /** + * `do { … } catch [pat] { … }`. Conservative exceptional flow (mirrors the C++ + * `try`/`catch` over-approximation): every block created while walking the + * protected `do` body edges to the FIRST catch handler — a `try`/`throw` may + * fire mid-block. Each `catch_block`'s normal completion joins the post-`do` + * continuation. + */ + private visitDo(stmt: SyntaxNode): SeqResult { + const bodyNode = stmt.namedChildren.find((c) => c.type === 'statements'); + const catchBlocks = stmt.namedChildren.filter((c) => c.type === 'catch_block'); + + // Build each catch handler. The handler entry is the error-binding block (a + // facts-only block) in front of the body, so the binding happens once on entry. + const handlerEntries: number[] = []; + const handlerExits: number[] = []; + for (const clause of catchBlocks) { + const clauseBody = clause.namedChildren.find((c) => c.type === 'statements'); + let res: SeqResult = clauseBody ? this.visitSeq(this.statementsOf(clauseBody)) : null; + if (res === null) { + // Empty catch body still CATCHES — synthesize one block so the error + // lands somewhere and post-`do` code stays reachable. + const idx = this.builder.newBlock(startLineOf(clause), endLineOf(clause), ''); + res = { entry: idx, exits: [idx] }; + } + const errFacts = this.harvest.catchErrorFacts(clause); + if (errFacts) { + const errBlock = this.builder.newBlock(startLineOf(clause), startLineOf(clause), '', 'normal', errFacts); + this.builder.edge(errBlock, res.entry, 'seq'); + res = { entry: errBlock, exits: res.exits }; + } + handlerEntries.push(res.entry); + handlerExits.push(...res.exits); + } + + // The protected body's handler is the FIRST catch (if any), else the outer. + const doHandler = handlerEntries[0] ?? this.currentHandler(); + const protectedStart = this.builder.blockCount; + this.handlers.push(doHandler); + const bodyRes = bodyNode ? this.visitSeq(this.statementsOf(bodyNode)) : null; + this.handlers.pop(); + + // Conservative exceptional edges: every protected-region block → the handler. + if (catchBlocks.length > 0) { + for (let b = protectedStart; b < this.builder.blockCount; b++) { + this.builder.edge(b, doHandler, 'throw'); + } + } + + const exits: number[] = []; + if (bodyRes) exits.push(...bodyRes.exits); + exits.push(...handlerExits); + // No catch at all — a `do {}` without `catch` is just a scope: the body + // flows straight through to its own normal exits (no extra edge needed). + + const entry = bodyRes?.entry ?? handlerEntries[0]; + if (entry === undefined) return null; + return { entry, exits: [...new Set(exits)] }; + } + + /** Nearest enclosing `catch` handler, or the function EXIT. */ + private currentHandler(): number { + return this.handlers.length ? this.handlers[this.handlers.length - 1] : this.builder.exitIndex; + } + + /** + * Emit a `throw` edge to the nearest handler (or EXIT) for every `try`/`try?`/ + * `try!` operator inside a straight-line statement's subtree (excluding nested + * function bodies). A `try` can propagate an error mid-statement; routing it + * keeps the error path represented while the success path falls through. + * Deduped by the builder, so repeated `try` in a statement emit one edge. + */ + private wireTryExits(stmt: SyntaxNode, fromBlock: number): void { + if (this.containsTry(stmt)) this.builder.edge(fromBlock, this.currentHandler(), 'throw'); + } + + private containsTry(node: SyntaxNode): boolean { + if (node.type === 'try_expression') { + // `try?` / `try!` handle the error in-place (optional / trap), so only a + // bare `try` propagates — but conservatively any `try` may error in a + // throwing context; route all to the handler (deduped, low cost). + return true; + } + if (SWIFT_FUNCTION_TYPES.has(node.type)) return false; // opaque nested fn + for (let i = 0; i < node.namedChildCount; i++) { + const c = node.namedChild(i); + if (c && this.containsTry(c)) return true; + } + return false; + } + + // ── defer ────────────────────────────────────────────────────────────── + + /** + * `defer { … }` is parsed as a `call_expression` whose callee is a bare + * `defer` identifier carrying a trailing-closure `lambda_literal`. Detect that + * exact shape so the deferred body threads the function-exit completion chain. + */ + private isDeferCall(stmt: SyntaxNode): boolean { + if (stmt.type !== 'call_expression') return false; + const callee = stmt.namedChild(0); + if (!callee || callee.type !== 'simple_identifier' || callee.text !== 'defer') return false; + return this.deferClosure(stmt) !== undefined; + } + + /** The trailing-closure `lambda_literal` body of a `defer { … }` call. */ + private deferClosure(stmt: SyntaxNode): SyntaxNode | undefined { + for (let i = 0; i < stmt.namedChildCount; i++) { + const c = stmt.namedChild(i); + if (c?.type === 'call_suffix') { + const lambda = c.namedChildren.find((x) => x.type === 'lambda_literal'); + if (lambda) return lambda; + } + if (c?.type === 'lambda_literal') return c; + } + return undefined; + } + + /** + * `defer { … }` — register the deferred body as a finalizer frame that stays + * active for the rest of the function tail (mirrors the Go visitor). Every later + * `return` AND the normal fall-off thread through it; LIFO across multiple + * defers falls out of `finalizersForReturn()` yielding innermost-first. The + * deferred block carries the closure body's def/use facts. The `defer` itself is + * a no-op at its source position — control falls straight through. + */ + private visitDefer(stmt: SyntaxNode): TraversalResult { + const lambda = this.deferClosure(stmt); + const deferBlock = this.builder.newBlock( + startLineOf(stmt), + endLineOf(stmt), + stmt.text, + 'normal', + lambda ? this.harvest.facts(lambda) : undefined, + ); + const frame = this.cfc.pushFinalizer(deferBlock); + this.deferFrames.push(frame); + // The `defer` statement is a no-op inline — return a SEPARATE marker block as + // the source position so the deferred block stays out of straight-line flow. + const marker = this.builder.newBlock(startLineOf(stmt), startLineOf(stmt), ''); + return { entry: marker, exits: [marker] }; + } + + /** + * Drain the active `defer` chain at function end (mirrors the Go visitor). The + * chain runs innermost-first (LIFO). Returns the block set control reaches AFTER + * the whole defer chain runs (to be wired to EXIT by the caller), or + * `normalExits` unchanged when there are no defers. + */ + finishDefers(normalExits: readonly number[]): readonly number[] { + if (this.deferFrames.length === 0) return normalExits; + const lifo = [...this.deferFrames].reverse(); + for (let i = 0; i < lifo.length; i++) this.cfc.pop(); + for (const frame of lifo) drainFinalizerPending(this.builder, frame, [frame.entry]); + + if (normalExits.length > 0) { + this.builder.connect(normalExits, lifo[0].entry, 'return'); + for (let i = 0; i + 1 < lifo.length; i++) { + this.builder.edge(lifo[i].entry, lifo[i + 1].entry, 'finally-return'); + } + } + return [lifo[lifo.length - 1].entry]; + } +} + +/** Build the CFG for one Swift function node, or `undefined` if not modelable. */ +function buildFunctionCfg(fnNode: SyntaxNode, filePath: string): FunctionCfg | undefined { + try { + if (!SWIFT_FUNCTION_TYPES.has(fnNode.type)) return undefined; + const startLine = startLineOf(fnNode); + const endLine = endLineOf(fnNode); + const startColumn = fnNode.startPosition.column; + + // A function/init/deinit must own a `function_body`; a lambda owns its + // `statements` directly. Absence ⇒ a protocol requirement / signature-only + // declaration with no body to model (return undefined). A PRESENT-but-empty + // body is a valid empty function (ENTRY → EXIT), distinct from "no body". + const hasBodyContainer = + fnNode.childForFieldName('body') !== null || + fnNode.namedChildren.some((c) => c.type === 'function_body') || + (fnNode.type === 'lambda_literal' && + fnNode.namedChildren.some((c) => c.type === 'statements')) || + fnNode.type === 'lambda_literal'; + if (!hasBodyContainer) return undefined; + + const body = bodyStatementsOf(fnNode); // may be undefined for an empty body + + const builder = new CfgBuilder(filePath, startLine, endLine, startColumn); + const harvest = new SwiftHarvester(fnNode); + + const paramFacts = harvest.paramFacts(); + if (paramFacts) builder.attachFacts(builder.entryIndex, paramFacts); + + const walk = new SwiftCfgWalk(builder, harvest); + const res = body + ? walk.visitSeq( + body.namedChildren.filter((c) => c.type !== 'comment' && c.type !== 'multiline_comment'), + ) + : null; + + builder.edge(builder.entryIndex, res ? res.entry : builder.exitIndex, 'seq'); + // Normal fall-off threads through the active `defer` chain (LIFO) → EXIT. + const normalExits = res ? res.exits : [builder.entryIndex]; + const afterDefers = walk.finishDefers(normalExits); + builder.connect(afterDefers, builder.exitIndex, 'seq'); + return builder.finish(harvest.bindingTable()); + } catch (err) { + // Never throw out of buildFunctionCfg — a malformed AST shape must skip only + // this one function's CFG, never drop the whole file's language group (R4). + // eslint-disable-next-line no-console + console.warn(`[cfg] Swift buildFunctionCfg skipped a function in ${filePath}: ${String(err)}`); + return undefined; + } +} + +/** + * The function's body `statements` node. A `function_declaration` / `init_` / + * `deinit_declaration` wraps it in a `function_body`; a `lambda_literal` carries + * the `statements` directly (after an optional `lambda_function_type` + `in`). + */ +function bodyStatementsOf(fnNode: SyntaxNode): SyntaxNode | undefined { + const fb = + fnNode.childForFieldName('body') ?? fnNode.namedChildren.find((c) => c.type === 'function_body'); + if (fb && fb.type === 'function_body') { + return fb.namedChildren.find((c) => c.type === 'statements'); + } + if (fnNode.type === 'lambda_literal') { + return fnNode.namedChildren.find((c) => c.type === 'statements'); + } + return undefined; +} + +/** Whether a node is a Swift function/closure this visitor builds a CFG for. */ +function isFunction(node: SyntaxNode): boolean { + return SWIFT_FUNCTION_TYPES.has(node.type); +} + +/** The Swift CFG visitor. */ +export function createSwiftCfgVisitor(): CfgVisitor { + return { buildFunctionCfg, isFunction }; +} + +export { SWIFT_FUNCTION_TYPES }; diff --git a/gitnexus/src/core/ingestion/languages/swift.ts b/gitnexus/src/core/ingestion/languages/swift.ts index 9c6d9974e..02a7b89e8 100644 --- a/gitnexus/src/core/ingestion/languages/swift.ts +++ b/gitnexus/src/core/ingestion/languages/swift.ts @@ -25,6 +25,7 @@ import { createVariableExtractor } from '../variable-extractors/generic.js'; import { swiftVariableConfig } from '../variable-extractors/configs/swift.js'; import { createCallExtractor } from '../call-extractors/generic.js'; import { swiftCallConfig } from '../call-extractors/configs/swift.js'; +import { createSwiftCfgVisitor } from '../cfg/visitors/swift.js'; import { emitSwiftScopeCaptures, interpretSwiftImport, @@ -247,6 +248,7 @@ export const swiftProvider = defineLanguage({ // ── Scope-based resolution hooks (RFC #909 Ring 3, issue #937). See // languages/swift/ for the implementations. ────────────────────── emitScopeCaptures: emitSwiftScopeCaptures, + cfgVisitor: createSwiftCfgVisitor(), interpretImport: interpretSwiftImport, interpretTypeBinding: interpretSwiftTypeBinding, bindingScopeFor: swiftBindingScopeFor, diff --git a/gitnexus/test/integration/cfg/fixtures/swift-hazards.swift b/gitnexus/test/integration/cfg/fixtures/swift-hazards.swift new file mode 100644 index 000000000..037fb8275 --- /dev/null +++ b/gitnexus/test/integration/cfg/fixtures/swift-hazards.swift @@ -0,0 +1,136 @@ +// Swift CFG hazard fixture (#2195) — one of every control-flow shape the Swift +// CfgVisitor models, so the worker-roundtrip / integration passes exercise the +// real grammar end-to-end. Mirrors the sibling fixtures (go-hazards, rust-hazards). + +import Foundation + +// if / else + optional binding (`if let`) +func branching(_ x: Int, opt: Int?) -> Int { + if x > 0 { + positive() + } else if x < 0 { + negative() + } else { + zero() + } + if let y = opt { + use(y) + } + return x +} + +// guard let early-exit — the else MUST diverge, the body continues +func guarded(opt: Int?) -> Int { + guard let value = opt else { + return -1 + } + guard ready() else { + return -2 + } + return value +} + +// for-in / while / repeat-while (bottom-test) + `where` +func loops() { + for item in collection where item > 0 { + consume(item) + } + while running() { + tick() + } + repeat { + retry() + } while shouldRetry() +} + +// switch — no implicit fallthrough + explicit `fallthrough` + `where` guard +func dispatch(_ x: Int) { + switch x { + case 1: + one() + fallthrough + case 2 where x > 0: + two() + case let n where n > 10: + big(n) + default: + other() + } +} + +// do / catch (Swift error handling) with `try` / `try?` / `try!` +func errorHandling() { + do { + try risky() + let cached = try? maybe() + try! forced() + deeper(cached) + } catch let error { + handle(error) + } catch { + fallbackHandler() + } + afterDo() +} + +// defer — runs at scope exit, LIFO +func deferred() -> Int { + defer { cleanupA() } + defer { cleanupB() } + let result = compute() + return result +} + +// labeled break / continue +func labeled() { + outer: for i in rows { + for j in cols { + if skip(i, j) { + continue outer + } + if stop(i, j) { + break outer + } + process(i, j) + } + } + done() +} + +// `while true {}` — non-terminating; EXIT must stay reverse-reachable +func eventLoop(_ active: Bool) { + while true { + if active { + poll() + } + } +} + +// tuple destructuring + straight-line def/use +func destructure(pair: (Int, Int)) { + let (a, b) = pair + let sum = a + b + use(sum) +} + +// init / deinit are CFG-bearing +class Resource { + var handle: Int + + init(handle: Int) { + self.handle = handle + } + + deinit { + release(handle) + } + + // a closure is its own CFG + func register() { + events.forEach { event in + if event.isValid { + accept(event) + } + } + } +} diff --git a/gitnexus/test/unit/cfg/swift-visitor.test.ts b/gitnexus/test/unit/cfg/swift-visitor.test.ts new file mode 100644 index 000000000..6249c2cf0 --- /dev/null +++ b/gitnexus/test/unit/cfg/swift-visitor.test.ts @@ -0,0 +1,326 @@ +import { describe, it, expect } from 'vitest'; +import { requireVendoredGrammar } from '../../../src/core/tree-sitter/vendored-grammars.js'; +import { createSwiftCfgVisitor } from '../../../src/core/ingestion/cfg/visitors/swift.js'; +import type { FunctionCfg } from '../../../src/core/ingestion/cfg/types.js'; +import { makeCfgHarness, type CfgHarness } from '../../helpers/cfg-harness.js'; + +// The Swift CfgVisitor, one hazard per test (real-parser regression, NOT +// snapshot-pinning). Swift's grammar is VENDORED (not an npm package): the +// grammar loads from vendor/ via `requireVendoredGrammar('tree-sitter-swift')`, +// exactly like the C/C++ test loads the vendored tree-sitter-c. Each fixture's +// distinctive statement text (step(), done(), handle(e), …) lets us locate the +// block for a region by text and assert the control-flow topology around it. + +const swiftGrammar = requireVendoredGrammar('tree-sitter-swift') as Parameters< + typeof makeCfgHarness +>[0]; + +const swift: CfgHarness = makeCfgHarness(swiftGrammar, createSwiftCfgVisitor(), 'fixture.swift'); + +const block = (cfg: FunctionCfg, substr: string): number => { + const b = cfg.blocks.find((bl) => bl.text.includes(substr)); + if (!b) throw new Error(`no block containing ${JSON.stringify(substr)}`); + return b.index; +}; + +const edgeKinds = (cfg: FunctionCfg): Set => new Set(cfg.edges.map((e) => e.kind)); + +function reaches(cfg: FunctionCfg, from: number, to: number): boolean { + const adj = new Map(); + for (const e of cfg.edges) (adj.get(e.from) ?? adj.set(e.from, []).get(e.from)!).push(e.to); + const seen = new Set([from]); + const stack = [from]; + while (stack.length) { + const n = stack.pop() as number; + if (n === to) return true; + for (const nx of adj.get(n) ?? []) if (!seen.has(nx)) (seen.add(nx), stack.push(nx)); + } + return seen.has(to); +} +const reachable = (cfg: FunctionCfg, idx: number): boolean => reaches(cfg, cfg.entryIndex, idx); + +/** Is EXIT reverse-reachable from every reachable block? (CDG soundness gate.) */ +function exitReachableFromAll(cfg: FunctionCfg): boolean { + for (const b of cfg.blocks) { + if (b.index === cfg.exitIndex) continue; + if (!reachable(cfg, b.index)) continue; // unreachable blocks exempt + if (!reaches(cfg, b.index, cfg.exitIndex)) return false; + } + return true; +} + +/** Resolve a binding by name → its index in the function's binding table. */ +function bindingIdx(cfg: FunctionCfg, name: string): number { + const i = (cfg.bindings ?? []).findIndex((b) => b.name === name); + if (i < 0) throw new Error(`no binding ${name}`); + return i; +} + +describe('Swift CfgVisitor — structure', () => { + it('straight-line body: ENTRY → block → EXIT (seq)', () => { + const cfg = swift.cfgOf(`func f() { a(); b(); c() }`); + expect(cfg.blocks.filter((b) => b.kind === 'normal')).toHaveLength(1); + const body = block(cfg, 'a()'); + expect(cfg.edges).toContainEqual({ from: cfg.entryIndex, to: body, kind: 'seq' }); + expect(reaches(cfg, body, cfg.exitIndex)).toBe(true); + }); + + it('empty body: ENTRY → EXIT', () => { + const cfg = swift.cfgOf(`func f() {}`); + expect(cfg.blocks).toHaveLength(2); + expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true); + }); + + it('an unmodeled shape produces a graceful partial CFG (never throws)', () => { + // A protocol method requirement (`func f()`) has no body — buildFunctionCfg + // must return undefined rather than throw; a real function still builds. + const root = swift.parse(`protocol P { func f() }`); + const fns = swift.collectFunctions(root); + for (const fn of fns) { + expect(() => createSwiftCfgVisitor().buildFunctionCfg(fn, 'p.swift')).not.toThrow(); + } + // A normal function still builds a well-formed CFG. + const cfg = swift.cfgOf(`func g() { x() }`); + expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true); + }); + + it('init and deinit are CFG-bearing functions', () => { + const cfgs = swift.cfgsOf(`class C { init(x: Int) { self.x = x } ; deinit { cleanup() } }`); + expect(cfgs).toHaveLength(2); + for (const cfg of cfgs) expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true); + }); +}); + +describe('Swift CfgVisitor — branching', () => { + it('if/else: cond-true to then, cond-false to else, both reach the join', () => { + const cfg = swift.cfgOf(`func f(x: Int) { if x > 0 { a() } else { b() } ; c() }`); + const kinds = edgeKinds(cfg); + expect(kinds.has('cond-true')).toBe(true); + expect(kinds.has('cond-false')).toBe(true); + const join = block(cfg, 'c()'); + expect(reaches(cfg, block(cfg, 'a()'), join)).toBe(true); + expect(reaches(cfg, block(cfg, 'b()'), join)).toBe(true); + }); + + it('else-if chain branches each condition', () => { + const cfg = swift.cfgOf(`func f(x: Int) { if x == 1 { a() } else if x == 2 { b() } else { c() } ; d() }`); + const join = block(cfg, 'd()'); + expect(reaches(cfg, block(cfg, 'a()'), join)).toBe(true); + expect(reaches(cfg, block(cfg, 'b()'), join)).toBe(true); + expect(reaches(cfg, block(cfg, 'c()'), join)).toBe(true); + }); + + it('if let binds the optional and reaches both arms', () => { + const cfg = swift.cfgOf(`func f(opt: Int?) { if let y = opt { use(y) } ; after() }`); + // `y` is a binding defined by the optional binding. + const y = bindingIdx(cfg, 'y'); + const defined = cfg.blocks.some((bl) => bl.statements?.some((s) => s.defs.includes(y))); + expect(defined).toBe(true); + expect(edgeKinds(cfg).has('cond-true')).toBe(true); + expect(reachable(cfg, block(cfg, 'use(y)'))).toBe(true); + expect(reachable(cfg, block(cfg, 'after()'))).toBe(true); + }); +}); + +describe('Swift CfgVisitor — guard', () => { + it('guard let ... else { return }: the else DIVERGES, the body CONTINUES', () => { + const cfg = swift.cfgOf(`func f(opt: Int?) -> Int { guard let y = opt else { return 0 } ; use(y) ; return y }`); + const header = block(cfg, 'guard'); + const elseBlk = block(cfg, 'return 0'); + // The else is the cond-false (diverging) arm and returns. + expect(cfg.edges).toContainEqual({ from: header, to: elseBlk, kind: 'cond-false' }); + expect(reaches(cfg, elseBlk, cfg.exitIndex)).toBe(true); + // The guarded body continues straight-line on the success path. + expect(reaches(cfg, header, block(cfg, 'use(y)'))).toBe(true); + // `y` is bound by the guard and used after it. + const y = bindingIdx(cfg, 'y'); + expect(cfg.blocks.some((bl) => bl.statements?.some((s) => s.defs.includes(y)))).toBe(true); + expect(cfg.blocks.some((bl) => bl.statements?.some((s) => s.uses.includes(y)))).toBe(true); + }); +}); + +describe('Swift CfgVisitor — loops', () => { + it('for-in: header + body + loop-back + exit', () => { + const cfg = swift.cfgOf(`func f() { for item in items { step() } ; done() }`); + const header = block(cfg, 'for item in items'); + const body = block(cfg, 'step()'); + expect(cfg.edges).toContainEqual({ from: body, to: header, kind: 'loop-back' }); + expect(edgeKinds(cfg).has('cond-true')).toBe(true); + expect(reaches(cfg, header, block(cfg, 'done()'))).toBe(true); + // The loop pattern binds `item`. + const item = bindingIdx(cfg, 'item'); + expect(cfg.blocks.some((bl) => bl.statements?.some((s) => s.defs.includes(item)))).toBe(true); + }); + + it('while: header tests first, body loops back', () => { + const cfg = swift.cfgOf(`func f() { while cond() { step() } ; done() }`); + const header = block(cfg, 'while cond()'); + const body = block(cfg, 'step()'); + expect(cfg.edges).toContainEqual({ from: body, to: header, kind: 'loop-back' }); + expect(edgeKinds(cfg).has('cond-true')).toBe(true); + expect(reaches(cfg, header, block(cfg, 'done()'))).toBe(true); + }); + + it('repeat-while runs the body BEFORE testing, then loops back from the bottom', () => { + const cfg = swift.cfgOf(`func f() { repeat { step() } while cond() ; done() }`); + const body = block(cfg, 'step()'); + const cond = block(cfg, 'while cond()'); + expect(reaches(cfg, cfg.entryIndex, body)).toBe(true); // body runs first + expect(reaches(cfg, body, cond)).toBe(true); // condition tests at the bottom + expect(cfg.edges).toContainEqual({ from: cond, to: body, kind: 'loop-back' }); + expect(reaches(cfg, cond, block(cfg, 'done()'))).toBe(true); + }); + + it('while true {} keeps EXIT reverse-reachable (structural exit-escape edge)', () => { + const cfg = swift.cfgOf(`func f() { while true { work() } }`); + expect(edgeKinds(cfg).has('cond-false')).toBe(true); + expect(exitReachableFromAll(cfg)).toBe(true); + expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true); + }); + + it('repeat {} while true keeps EXIT reverse-reachable', () => { + const cfg = swift.cfgOf(`func f() { repeat { work() } while true }`); + expect(edgeKinds(cfg).has('cond-false')).toBe(true); + expect(exitReachableFromAll(cfg)).toBe(true); + expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true); + }); +}); + +describe('Swift CfgVisitor — switch (no implicit fallthrough)', () => { + it('a case without fallthrough rejoins after the switch (no fall into next case)', () => { + const cfg = swift.cfgOf(`func f(x: Int) { + switch x { + case 1: one() + case 2: two() + default: other() + } + after() + }`); + expect(edgeKinds(cfg).has('switch-case')).toBe(true); + // case 1 does NOT fall into case 2 (Swift has no implicit fallthrough). + expect(reaches(cfg, block(cfg, 'one()'), block(cfg, 'two()'))).toBe(false); + // every case reaches the post-switch continuation. + expect(reaches(cfg, block(cfg, 'one()'), block(cfg, 'after()'))).toBe(true); + expect(reaches(cfg, block(cfg, 'two()'), block(cfg, 'after()'))).toBe(true); + }); + + it('an explicit `fallthrough` spills into the next case', () => { + const cfg = swift.cfgOf(`func f(x: Int) { + switch x { + case 1: + one() + fallthrough + case 2: + two() + default: + other() + } + after() + }`); + expect(edgeKinds(cfg).has('fallthrough')).toBe(true); + // case 1 (explicit fallthrough) FALLS THROUGH into case 2. + expect(reaches(cfg, block(cfg, 'one()'), block(cfg, 'two()'))).toBe(true); + }); + + it('a `where` guard on a case is harvested onto the dispatch block', () => { + const cfg = swift.cfgOf(`func f(x: Int) { + switch x { + case let n where n > 0: pos() + default: other() + } + }`); + // `x` (the subject) is used at the dispatch; the where guard uses are harvested. + expect(edgeKinds(cfg).has('switch-case')).toBe(true); + expect(reachable(cfg, block(cfg, 'pos()'))).toBe(true); + expect(reachable(cfg, block(cfg, 'other()'))).toBe(true); + }); +}); + +describe('Swift CfgVisitor — do/catch (error handling)', () => { + it('do/catch: a throw edge runs from each protected block to the handler', () => { + const cfg = swift.cfgOf(`func f() { + do { try risky() ; deeper() } catch let e { handle(e) } + after() + }`); + expect(edgeKinds(cfg).has('throw')).toBe(true); + const handler = block(cfg, 'handle(e)'); + expect(reaches(cfg, block(cfg, 'try risky()'), handler)).toBe(true); + // after() is still reachable (handler completion rejoins). + expect(reachable(cfg, block(cfg, 'after()'))).toBe(true); + // the catch binds the error `e`. + const e = bindingIdx(cfg, 'e'); + expect(cfg.blocks.some((bl) => bl.statements?.some((s) => s.defs.includes(e)))).toBe(true); + }); + + it('a throw with NO enclosing do/catch routes to EXIT and ends its block', () => { + const cfg = swift.cfgOf(`func f(x: Bool) throws { if x { throw E.bad } ; done() }`); + const thr = block(cfg, 'throw E.bad'); + expect(cfg.edges).toContainEqual({ from: thr, to: cfg.exitIndex, kind: 'throw' }); + // throw terminates its block — control does not fall into done() from it. + expect(reaches(cfg, thr, block(cfg, 'done()'))).toBe(false); + expect(reachable(cfg, block(cfg, 'done()'))).toBe(true); // via the if false branch + }); +}); + +describe('Swift CfgVisitor — defer (LIFO scope-exit)', () => { + it('a defer runs at scope exit: the return threads through the deferred block', () => { + const cfg = swift.cfgOf(`func f() { defer { cleanup() } ; work() ; return }`); + const kinds = edgeKinds(cfg); + // The deferred completion threads as return + finally-return. + expect(kinds.has('return')).toBe(true); + const deferBlk = block(cfg, 'defer { cleanup() }'); + // The deferred block reaches EXIT (it runs on scope exit). + expect(reaches(cfg, deferBlk, cfg.exitIndex)).toBe(true); + // The explicit return reaches the deferred block. + const ret = block(cfg, 'return'); + expect(reaches(cfg, ret, deferBlk)).toBe(true); + }); +}); + +describe('Swift CfgVisitor — labeled break/continue', () => { + it('labeled break targets the OUTER loop, not the inner one', () => { + const cfg = swift.cfgOf(`func f() { + outer: for i in xs { + for j in ys { break outer } + } + done() + }`); + const brk = block(cfg, 'break outer'); + expect(edgeKinds(cfg).has('break')).toBe(true); + // the labeled break escapes BOTH loops and reaches done(). + expect(reaches(cfg, brk, block(cfg, 'done()'))).toBe(true); + }); +}); + +describe('Swift CfgVisitor — def/use harvest', () => { + it('let x = compute(); use(x) produces a def of x and a use in the consumer', () => { + const cfg = swift.cfgOf(`func f() { let x = compute() ; use(x) }`); + const x = bindingIdx(cfg, 'x'); + expect(cfg.blocks.some((bl) => bl.statements?.some((s) => s.defs.includes(x)))).toBe(true); + expect(cfg.blocks.some((bl) => bl.statements?.some((s) => s.uses.includes(x)))).toBe(true); + }); + + it('tuple destructuring `let (a, b) = pair` defines both names', () => { + const cfg = swift.cfgOf(`func f(pair: (Int, Int)) { let (a, b) = pair ; use(a) ; use(b) }`); + for (const name of ['a', 'b']) { + const idx = bindingIdx(cfg, name); + expect(cfg.blocks.some((bl) => bl.statements?.some((s) => s.defs.includes(idx)))).toBe(true); + } + }); + + it('closures are collected as their own CFG (opaque in the enclosing function)', () => { + const cfgs = swift.cfgsOf(`func f() { items.forEach { item in if item > 0 { use(item) } } }`); + // f and the closure are both CFG-bearing. + expect(cfgs.length).toBeGreaterThanOrEqual(2); + for (const cfg of cfgs) expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true); + }); +}); + +describe('Swift CfgVisitor — functionStartColumn', () => { + it('two same-line functions get distinct functionStartColumn', () => { + const cfgs = swift.cfgsOf(`func a() { x() }; func b() { y() }`); + expect(cfgs).toHaveLength(2); + expect(cfgs[0].functionStartLine).toBe(cfgs[1].functionStartLine); // same line + expect(cfgs[0].functionStartColumn).not.toBe(cfgs[1].functionStartColumn); // distinct column + }); +});