mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-11 03:38:07 +00:00
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:
parent
82159c93d4
commit
0e137cce27
5 changed files with 1931 additions and 0 deletions
548
gitnexus/src/core/ingestion/cfg/visitors/swift-harvest.ts
Normal file
548
gitnexus/src/core/ingestion/cfg/visitors/swift-harvest.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
919
gitnexus/src/core/ingestion/cfg/visitors/swift.ts
Normal file
919
gitnexus/src/core/ingestion/cfg/visitors/swift.ts
Normal 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 };
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
136
gitnexus/test/integration/cfg/fixtures/swift-hazards.swift
Normal file
136
gitnexus/test/integration/cfg/fixtures/swift-hazards.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
326
gitnexus/test/unit/cfg/swift-visitor.test.ts
Normal file
326
gitnexus/test/unit/cfg/swift-visitor.test.ts
Normal 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
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue