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) <noreply@anthropic.com>
This commit is contained in:
Gergo Magyar 2026-06-14 13:40:44 +00:00
parent 82159c93d4
commit 0e137cce27
5 changed files with 1931 additions and 0 deletions

View file

@ -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<number>();
private readonly useSeen = new Set<number>();
private readonly mayDefSeen = new Set<number>();
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<string, number>();
private readonly synthetic = new Map<string, number>();
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;
}
}

View file

@ -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_<node_type>` 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<SyntaxNode> {
return { buildFunctionCfg, isFunction };
}
export { SWIFT_FUNCTION_TYPES };

View file

@ -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,

View file

@ -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)
}
}
}
}

View file

@ -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<string> => new Set(cfg.edges.map((e) => e.kind));
function reaches(cfg: FunctionCfg, from: number, to: number): boolean {
const adj = new Map<number, number[]>();
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
});
});