diff --git a/gitnexus/src/core/ingestion/cfg/visitors/rust-harvest.ts b/gitnexus/src/core/ingestion/cfg/visitors/rust-harvest.ts new file mode 100644 index 000000000..86db8ba32 --- /dev/null +++ b/gitnexus/src/core/ingestion/cfg/visitors/rust-harvest.ts @@ -0,0 +1,632 @@ +/** + * Rust def/use harvester (#2195 U7) — the Rust analogue of + * {@link import('./typescript-harvest.js').TsHarvester} and the C-family / + * Go / Python harvesters. Like the Python harvester 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 Rust 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 + * tree-sitter-rust via the introspection probe before use (mandatory pre-step). + * Rust shapes pre-empted (verified by a real parse): + * - functions: `function_item` (fields `name`/`parameters`/`return_type`/`body`; + * methods are `function_item` inside an `impl_item`'s `declaration_list`) and + * `closure_expression` (field `parameters`=`closure_parameters`, `body` is a + * `block` OR a bare expression). + * - parameters: `parameter` (field `pattern`, optional `mutable_specifier`), + * `self_parameter`. A `closure_parameters` lists bare `identifier`s and/or + * `parameter` nodes. + * - declarations: `let_declaration` (field `pattern`, optional `value`, optional + * `alternative` block for `let … else`; optional `mutable_specifier`). The + * `mut` keyword is irrelevant to def-ness. + * - patterns (each bound `identifier` leaf is a def): `identifier`, + * `tuple_pattern`, `slice_pattern`, `struct_pattern` (`field_pattern`s whose + * `name` is a `shorthand_field_identifier`, or `name: pat`), `tuple_struct_pattern` + * (field `type` is the variant path — NOT a binding; the inner patterns bind), + * `ref_pattern` / `mut_pattern` (the inner identifier binds), `captured_pattern` + * (`v @ subpat` — `v` binds, and the subpattern's leaves bind), `or_pattern`, + * `range_pattern` (binds nothing). The wildcard `_` binds nothing. + * - assignments: `assignment_expression` (fields `left`/`right`), + * `compound_assignment_expr` (fields `left`/`operator`/`right` — read+write). + * - loop / match binders: `for_expression` `pattern`; `match_arm` `pattern` + * (a `match_pattern` whose leaves bind, plus an optional `if` guard with field + * `condition`); `let_condition` `pattern` (`if let` / `while let`). + * - reads: `field_expression` (fields `value`/`field`), `call_expression` + * (fields `function`/`arguments`), `binary_expression` (fields + * `left`/`operator`/`right`), `try_expression` (`expr?`). + * + * TWO-PHASE, ORDER-INDEPENDENT (load-bearing — mirrors the TS / Go / C + * harvesters): the CFG walk is NOT source-order, 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. Rust DOES have block scope + shadowing, but a single function table is + * the documented v1 simplification used by the Python harvester — 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: never a missed flow). + * + * v1 def-semantics scope: + * - `let PAT = …` (and `let PAT = … else { … }`) — each identifier leaf of PAT + * is a def; the value (and the `else` block) are walked for uses. + * - `assignment_expression` plain `=` — a plain-identifier lvalue is a def; a + * `field_expression` / index lvalue is NOT a scalar def (its root is a use). + * - `compound_assignment_expr` (`x += 1`) — def AND use the lvalue. + * - `for PAT in ITER` — the loop pattern's leaves are defs, ITER a use. + * - `match` arm patterns bind their leaves; `if let` / `while let` patterns bind. + * - parameters (incl. `mut`, typed, closure params) are `param`-kind defs. + * EXCLUDED, deliberately (TypeScript-CFA precedent): field / index writes + * (`obj.f = …`, `arr[i] = …`) are NOT scalar defs — their root identifiers are + * uses only. Nested-function bodies (`closure_expression`, an inner + * `function_item`) 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 match-arm guard / `if let` pattern + * test — is a may-def (gen WITHOUT kill), so the not-taken path's prior def is + * not falsely killed. (Rust assignment is an expression but yields `()`, so an + * in-`&&` assignment is rare; the machinery is kept for guard / case-test parity.) + * + * Identifiers with no in-function declaration (module items, imported names, + * constants, enum variants) 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_item', 'closure_expression']); + +/** Pattern containers whose identifier leaves are binding targets. */ +const PATTERN_CONTAINER_TYPES = new Set([ + 'tuple_pattern', + 'slice_pattern', + 'or_pattern', + 'reference_pattern', +]); + +/** + * 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 `rust-harvest.ts` free of site logic and guarantees the + * emitted facts carry no `sites` key (mirrors the Python harvester). + */ +class FactAccumulator { + private readonly defs: number[] = []; + private readonly uses: number[] = []; + private readonly mayDefs: number[] = []; + private readonly defSeen = new Set(); + private readonly useSeen = new Set(); + private readonly mayDefSeen = new Set(); + + constructor(private readonly line: number) {} + + addDef(idx: number): void { + if (this.defSeen.has(idx)) return; + this.defSeen.add(idx); + this.defs.push(idx); + } + + /** A def that may not execute (conditional context) — gen without kill. */ + addMayDef(idx: number): void { + if (this.mayDefSeen.has(idx)) return; + this.mayDefSeen.add(idx); + this.mayDefs.push(idx); + } + + addUse(idx: number): void { + if (this.useSeen.has(idx)) return; + this.useSeen.add(idx); + this.uses.push(idx); + } + + defCount(): number { + return this.defs.length + this.mayDefs.length; + } + + useCount(): number { + return this.uses.length; + } + + finish(): StatementFacts { + return { + line: this.line, + defs: this.defs, + uses: this.uses, + // Stay absent when empty — keeps the serialized side-channel payload lean. + ...(this.mayDefs.length > 0 ? { mayDefs: this.mayDefs } : {}), + }; + } +} + +export class RustHarvester { + private readonly bindings: BindingEntry[] = []; + /** Single function-scope name → binding index (v1: no block scope). */ + private readonly table = new Map(); + private readonly synthetic = new Map(); + private readonly fnId: number; + /** >0 while walking a conditionally-evaluated subexpression — defs become may-defs. */ + private conditionalDepth = 0; + + constructor(private readonly fnNode: SyntaxNode) { + this.fnId = fnNode.id; + this.declareParams(fnNode); + const body = this.bodyOf(fnNode); + if (body) this.prescan(body); + } + + /** The completed binding table — pass to `CfgBuilder.finish`. */ + bindingTable(): readonly BindingEntry[] { + return this.bindings; + } + + /** The function/closure body node (a `block` for a fn, block-or-expr for a closure). */ + private bodyOf(fnNode: SyntaxNode): SyntaxNode | undefined { + return fnNode.childForFieldName('body') ?? undefined; + } + + // ── 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 / closure (incl. `mut`, typed). */ + private declareParams(fnNode: SyntaxNode): void { + const params = + fnNode.childForFieldName('parameters') ?? + fnNode.namedChildren.find( + (c) => c.type === 'parameters' || c.type === 'closure_parameters', + ); + if (!params) return; + for (let i = 0; i < params.namedChildCount; i++) { + const p = params.namedChild(i); + if (!p) continue; + if (p.type === 'parameter') { + const pat = p.childForFieldName('pattern'); + if (pat) this.declarePattern(pat, 'param'); + } else if (p.type === 'self_parameter') { + // `&self` / `self` — bind `self` so reads of it resolve to a real + // binding rather than a synthetic module name. + const id = p.namedChildren.find((c) => c.type === 'self'); + if (id) this.declare(id, 'param'); + } else if (p.type === 'identifier') { + // Bare closure param `|x|`. + this.declare(p, 'param'); + } else { + // Typed closure param without the `parameter` wrapper, etc. + this.declarePattern(p, 'param'); + } + } + } + + /** + * Pre-scan the function body once, declaring every bound name. Recurses into + * compound expressions but NOT into nested `function_item` / `closure_expression` + * 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 'let_declaration': { + const pat = node.childForFieldName('pattern'); + if (pat) this.declarePattern(pat, 'let'); + break; + } + case 'for_expression': { + const pat = node.childForFieldName('pattern'); + if (pat) this.declarePattern(pat, 'let'); + break; + } + case 'let_condition': { + // `if let PAT = …` / `while let PAT = …`. + const pat = node.childForFieldName('pattern'); + if (pat) this.declarePattern(pat, 'let'); + break; + } + case 'match_arm': { + const pat = node.childForFieldName('pattern'); + if (pat) this.declarePattern(pat, 'let'); + break; + } + default: + break; + } + + for (let i = 0; i < node.namedChildCount; i++) { + const c = node.namedChild(i); + if (c) this.prescan(c); + } + } + + /** + * Declare every identifier leaf of a binding pattern. Handles the full Rust + * pattern taxonomy: tuple / slice / struct / tuple-struct / ref / mut / + * captured (`@`) / or patterns. A `tuple_struct_pattern`'s `type` field is the + * variant PATH (`Some`, `Ok`) — not a binding; only its inner patterns bind. + * `_`, literals and range patterns bind nothing. + */ + private declarePattern(pat: SyntaxNode, kind: BindingEntry['kind']): void { + const t = pat.type; + if (t === 'identifier') { + this.declare(pat, kind); + return; + } + if (t === '_') return; // standalone wildcard pattern is the `_` node + if (t === 'match_pattern') { + // The arm pattern wrapper — declare its (non-guard) sub-patterns. The + // guard `if cond` is a value test, not a binder. + const guard = pat.childForFieldName('condition'); + for (let i = 0; i < pat.namedChildCount; i++) { + const c = pat.namedChild(i); + if (c && c.id !== guard?.id) this.declarePattern(c, kind); + } + return; + } + if (PATTERN_CONTAINER_TYPES.has(t)) { + for (let i = 0; i < pat.namedChildCount; i++) { + const c = pat.namedChild(i); + if (c) this.declarePattern(c, kind); + } + return; + } + if (t === 'tuple_struct_pattern') { + // `Some(n)` / `Ok(v)` — the `type` field is the variant path (not a binder); + // every other named child is an inner binding pattern. + const typeNode = pat.childForFieldName('type'); + for (let i = 0; i < pat.namedChildCount; i++) { + const c = pat.namedChild(i); + if (c && c.id !== typeNode?.id) this.declarePattern(c, kind); + } + return; + } + if (t === 'struct_pattern') { + // `Point { x, y }` — each `field_pattern` binds; shorthand `x` binds `x`, + // `x: pat` binds `pat`'s leaves. The `type` field is the struct path. + for (let i = 0; i < pat.namedChildCount; i++) { + const c = pat.namedChild(i); + if (!c) continue; + if (c.type === 'field_pattern') { + this.declareFieldPattern(c, kind); + } else if (c.type === 'shorthand_field_identifier') { + this.declare(c, kind); + } + } + return; + } + if (t === 'ref_pattern' || t === 'mut_pattern' || t === 'reference_pattern') { + // `ref r` / `mut m` / `&p` — unwrap to the inner pattern. + for (let i = 0; i < pat.namedChildCount; i++) { + const c = pat.namedChild(i); + if (c && c.type !== 'mutable_specifier') this.declarePattern(c, kind); + } + return; + } + if (t === 'captured_pattern') { + // `v @ subpat` — `v` binds AND the subpattern's leaves bind. + for (let i = 0; i < pat.namedChildCount; i++) { + const c = pat.namedChild(i); + if (c) this.declarePattern(c, kind); + } + return; + } + // range_pattern / literal patterns / scoped paths bind nothing. + } + + /** `field_pattern` — shorthand `x` binds `x`; `x: pat` binds `pat`'s leaves. */ + private declareFieldPattern(field: SyntaxNode, kind: BindingEntry['kind']): void { + const name = field.childForFieldName('name'); + const pattern = field.childForFieldName('pattern'); + if (pattern) { + this.declarePattern(pattern, kind); + return; + } + if (name && name.type === 'shorthand_field_identifier') { + this.declare(name, kind); + return; + } + // Fallback: declare any identifier / shorthand leaf. + for (let i = 0; i < field.namedChildCount; i++) { + const c = field.namedChild(i); + if (c?.type === 'shorthand_field_identifier' || c?.type === 'identifier') this.declare(c, kind); + } + } + + // ── 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 PAT in ITER` head: the loop pattern's leaves are defs, the + * iterated expression a use. + */ + forHeadFacts(stmt: SyntaxNode): StatementFacts { + const acc = new FactAccumulator(stmt.startPosition.row + 1); + const value = stmt.childForFieldName('value'); + const pat = stmt.childForFieldName('pattern'); + if (value) this.walkValue(value, acc); + if (pat) this.defPattern(pat, acc); + return acc.finish(); + } + + /** + * Facts for ONLY a `let_declaration`'s PATTERN bindings (no value walk) — used + * when the value is a control-flow expression already harvested by the visitor, + * so the binding defs land on a separate continuation block without + * double-counting the value's uses. + */ + letPatternFacts(stmt: SyntaxNode): StatementFacts { + const acc = new FactAccumulator(stmt.startPosition.row + 1); + const pat = stmt.childForFieldName('pattern'); + if (pat) this.defPattern(pat, acc); + return acc.finish(); + } + + /** + * Facts for a `let PAT = VALUE` condition (`if let` / `while let`): the value + * is a use, the pattern's leaves are defs. When `conditional` is true the defs + * become may-defs (a `while let` re-test may not bind on the exit iteration). + */ + letConditionFacts(cond: SyntaxNode, conditional: boolean): StatementFacts { + const acc = new FactAccumulator(cond.startPosition.row + 1); + const run = (): void => { + const value = cond.childForFieldName('value'); + const pat = cond.childForFieldName('pattern'); + if (value) this.walkValue(value, acc); + if (pat) this.defPattern(pat, acc); + }; + if (conditional) this.conditional(run); + else run(); + return acc.finish(); + } + + /** ENTRY-block facts for the parameters (defs only — incl. default-position uses). */ + paramFacts(): StatementFacts | undefined { + const params = + this.fnNode.childForFieldName('parameters') ?? + this.fnNode.namedChildren.find( + (c) => c.type === 'parameters' || c.type === 'closure_parameters', + ); + if (!params) return undefined; + const acc = new FactAccumulator(this.fnNode.startPosition.row + 1); + for (let i = 0; i < params.namedChildCount; i++) { + const p = params.namedChild(i); + if (!p) continue; + if (p.type === 'parameter') { + const pat = p.childForFieldName('pattern'); + if (pat) this.defPattern(pat, acc); + } else if (p.type === 'self_parameter') { + const id = p.namedChildren.find((c) => c.type === 'self'); + if (id) this.def(id, acc); + } else if (p.type === 'identifier') { + this.def(p, acc); + } else { + this.defPattern(p, 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 identifier leaf of a binding pattern (the def-position analogue of + * {@link declarePattern}). A `tuple_struct_pattern`'s `type` field path is a + * variant name, not a def; its inner patterns bind. A struct field shorthand + * binds; `_` binds nothing. + */ + private defPattern(pat: SyntaxNode, acc: FactAccumulator): void { + const t = pat.type; + if (t === 'identifier') { + this.def(pat, acc); + return; + } + if (t === '_') return; // standalone wildcard pattern is the `_` node + if (t === 'match_pattern') { + const guard = pat.childForFieldName('condition'); + for (let i = 0; i < pat.namedChildCount; i++) { + const c = pat.namedChild(i); + if (c && c.id !== guard?.id) this.defPattern(c, acc); + } + return; + } + if (PATTERN_CONTAINER_TYPES.has(t)) { + for (let i = 0; i < pat.namedChildCount; i++) { + const c = pat.namedChild(i); + if (c) this.defPattern(c, acc); + } + return; + } + if (t === 'tuple_struct_pattern') { + const typeNode = pat.childForFieldName('type'); + for (let i = 0; i < pat.namedChildCount; i++) { + const c = pat.namedChild(i); + if (c && c.id !== typeNode?.id) this.defPattern(c, acc); + } + return; + } + if (t === 'struct_pattern') { + for (let i = 0; i < pat.namedChildCount; i++) { + const c = pat.namedChild(i); + if (!c) continue; + if (c.type === 'field_pattern') this.defFieldPattern(c, acc); + else if (c.type === 'shorthand_field_identifier') this.def(c, acc); + } + return; + } + if (t === 'ref_pattern' || t === 'mut_pattern' || t === 'reference_pattern') { + for (let i = 0; i < pat.namedChildCount; i++) { + const c = pat.namedChild(i); + if (c && c.type !== 'mutable_specifier') this.defPattern(c, acc); + } + return; + } + if (t === 'captured_pattern') { + for (let i = 0; i < pat.namedChildCount; i++) { + const c = pat.namedChild(i); + if (c) this.defPattern(c, acc); + } + return; + } + // range / literal / scoped path — binds nothing. + } + + private defFieldPattern(field: SyntaxNode, acc: FactAccumulator): void { + const name = field.childForFieldName('name'); + const pattern = field.childForFieldName('pattern'); + if (pattern) { + this.defPattern(pattern, acc); + return; + } + if (name && name.type === 'shorthand_field_identifier') { + this.def(name, acc); + return; + } + for (let i = 0; i < field.namedChildCount; i++) { + const c = field.namedChild(i); + if (c?.type === 'shorthand_field_identifier' || c?.type === 'identifier') this.def(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 'identifier': + this.use(node, acc); + return; + case 'let_declaration': { + const value = node.childForFieldName('value'); + const pat = node.childForFieldName('pattern'); + const alt = node.childForFieldName('alternative'); // `let … else { … }` + if (value) this.walkValue(value, acc); + if (alt) this.walkValue(alt, acc); + if (pat) this.defPattern(pat, acc); + return; + } + case 'let_condition': { + const value = node.childForFieldName('value'); + const pat = node.childForFieldName('pattern'); + if (value) this.walkValue(value, acc); + if (pat) this.defPattern(pat, acc); + return; + } + case 'assignment_expression': { + const left = node.childForFieldName('left'); + const right = node.childForFieldName('right'); + if (right) this.walkValue(right, acc); + if (left) { + if (left.type === 'identifier') { + this.def(left, acc); + } else { + // field / index lvalue (`obj.f = …`, `a[i] = …`) — root is a use only. + this.walkValue(left, acc); + } + } + return; + } + case 'compound_assignment_expr': { + const left = node.childForFieldName('left'); + const right = node.childForFieldName('right'); + if (right) this.walkValue(right, acc); + if (left) { + if (left.type === 'identifier') { + this.use(left, acc); + this.def(left, acc); + } else { + this.walkValue(left, acc); + } + } + return; + } + case 'binary_expression': { + const left = node.childForFieldName('left'); + const right = node.childForFieldName('right'); + const op = node.childForFieldName('operator')?.text ?? ''; + if (left) this.walkValue(left, acc); + if (right) { + if (op === '&&' || op === '||') this.conditional(() => this.walkValue(right, acc)); + else this.walkValue(right, acc); + } + return; + } + case 'field_expression': { + // `a.b` — value read of the chain root only; the field name is not a + // scalar binding. + const value = node.childForFieldName('value'); + if (value) this.walkValue(value, acc); + return; + } + default: + for (let i = 0; i < node.namedChildCount; i++) { + const c = node.namedChild(i); + if (c) this.walkValue(c, acc); + } + } + } +} diff --git a/gitnexus/src/core/ingestion/cfg/visitors/rust.ts b/gitnexus/src/core/ingestion/cfg/visitors/rust.ts new file mode 100644 index 000000000..1482982bf --- /dev/null +++ b/gitnexus/src/core/ingestion/cfg/visitors/rust.ts @@ -0,0 +1,724 @@ +/** + * Rust CfgVisitor (#2195 U7) — the EXPRESSION-ORIENTED CFG target. Unlike the + * C-family / Go / Python visitors (statement languages), Rust control flow is + * built from EXPRESSIONS: `if` / `loop` / `while` / `for` / `match` / `block` + * are all expressions that produce a value. The visitor still drives the + * language-agnostic {@link CfgBuilder} to produce a serializable + * {@link FunctionCfg} plus a def/use harvest ({@link RustHarvester}) for the + * reaching-defs / CDG solvers, structured like the sibling visitors — a + * `visit_` dispatch over the control-flow taxonomy driving a + * per-function {@link ControlFlowContext} for labeled break/continue. + * + * Every node type and field literal below was grammar-validated against + * tree-sitter-rust via the introspection probe before use (mandatory pre-step). + * Rust shapes pre-empted (verified by a real parse): + * - functions: `function_item` (fields `name`/`parameters`/`return_type`/`body`; + * a method is a `function_item` inside an `impl_item`'s `declaration_list`) + * and `closure_expression` (field `parameters`=`closure_parameters`; `body` is + * a `block` OR a bare expression — `|x| x + 1`). + * - `if_expression` fields `condition` / `consequence` (a `block`) / + * `alternative` (an `else_clause` wrapping a `block` or a nested + * `if_expression` of an `else if`). The condition can be a `let_condition` + * (`if let PAT = e`) or a `let_chain` (`if let PAT = e && cond`). + * - `loop_expression` field `body` — the INFINITE loop (NO `condition` field); + * an optional `label` NAMED CHILD (label is NOT a field). The key + * non-terminating case. + * - `while_expression` fields `condition` (may be a `let_condition` for + * `while let`) / `body`; optional `label` named child. + * - `for_expression` fields `pattern` / `value` / `body`; optional `label` + * named child. + * - `match_expression` fields `value` / `body` (a `match_block` of `match_arm`s). + * A `match_arm` has field `pattern` (a `match_pattern`, which may carry an `if` + * guard with field `condition`) and field `value` (the arm body — an + * expression or a `block`). Arms do NOT fall through. + * - `break_expression` — optional `label` named child AND an optional value + * expression (`break 'a 42`). `continue_expression` — optional `label`. + * - `return_expression` — optional value child. `try_expression` (`expr?`) — + * the early-return operator. + * - a `label` node's bare name is its `identifier` child's text (`'outer`'s name + * is `outer`), matching `break 'outer` / `continue 'outer`. + * + * Edge-kind contract (matches the existing visitors — RD/CDG consume these): + * - if / else (incl. `if let`) → `cond-true` / `cond-false` + * - `loop {}` (no condition) → `loop-back` (body re-enters) PLUS a structural + * `cond-false` escape edge (header → loopExit) so EXIT stays reverse-reachable + * - `while` / `while let` / `for` → `cond-true` / `loop-back` / `cond-false` + * - `match` dispatch → `switch-case` (NO fallthrough — like Go / Python) + * - `break` / `continue` / `return` → the matching terminator kind; a labeled + * `break 'outer` / `continue 'outer` targets the labeled loop frame; a + * `break value` is still a `break` edge + * - the `?` operator (`try_expression`) → `throw` — an early error-return edge to + * EXIT from the `?` site (a conservative throw-like edge; the Err/None path) + * - straight-line → `seq` + * + * Rust-specific modeling decisions (documented approximations): + * - `loop {}` is the canonical Rust infinite loop with NO condition. We ALWAYS + * emit a structural `header → loopExit` escape edge (exactly as the C-family / + * Go visitors do for `while(true)` / `for {}`), 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. A `loop {}` that + * DOES `break` also reaches EXIT via the break; the structural escape edge is + * emitted either way. + * - the `?` operator desugars to a `match` that returns the Err/None early. We + * model it conservatively as a `throw` edge to the function EXIT from the block + * that contains the `?` — so the post-`?` continuation stays reachable (the Ok + * path) AND the early-exit path is represented. Multiple `?` in one block emit + * one early-exit edge per `?`-bearing block (deduped by the builder). + * - a closure (`closure_expression`) 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), exactly + * as the Go visitor treats a spawned closure and the TS visitor an arrow body. + * - the trailing tail expression of a `block` (no `;`) is the block's value; for + * control-flow purposes it just falls off normally to the block's successor. + * + * Known limitations: + * - panic: a `panic!()` (or an out-of-bounds index, an `unwrap` on `None`) aborts + * the function abnormally, but tree-sitter sees only a normal macro/method call. + * The panic-unwind path is NOT modeled (documented gap, not faked) — Rust has + * no try/catch, so there is no handler structure to route to. + * - async (`async fn`, `.await`): the suspension points are modeled as normal + * straight-line control flow (no scheduler edges). + * - block scope + shadowing in the def/use harvest is flattened to one function + * table (see rust-harvest.ts) — a documented v1 over-approximation. + * - macro bodies (`println!`, `vec!`, custom macros) are opaque token trees — any + * control flow expanded by a macro is invisible. + * + * 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 type { TraversalResult } from '../traversal-result.js'; +import type { CfgVisitor, FunctionCfg } from '../types.js'; +import { RustHarvester } from './rust-harvest.js'; + +/** Rust node types that own a CFG-bearing function body. */ +const RUST_FUNCTION_TYPES = new Set(['function_item', 'closure_expression']); + +/** + * Expression / statement node types that break a basic block (everything else + * coalesces). `expression_statement` is the `;` wrapper; the control-flow + * EXPRESSIONS inside it are unwrapped by {@link visitStmt}. + */ +const CONTROL_FLOW_TYPES = new Set([ + 'expression_statement', + 'if_expression', + 'loop_expression', + 'while_expression', + 'for_expression', + 'match_expression', + 'return_expression', + 'break_expression', + 'continue_expression', + 'block', +]); + +/** Expression node types that the statement walker treats as control-flow. */ +const CONTROL_FLOW_EXPR_TYPES = new Set([ + 'if_expression', + 'loop_expression', + 'while_expression', + 'for_expression', + 'match_expression', + 'return_expression', + 'break_expression', + 'continue_expression', + 'block', +]); + +/** Node types whose subtrees are opaque (a nested function owns its own CFG). */ +const NESTED_FUNCTION_TYPES = new Set(['function_item', 'closure_expression']); + +/** Rust comment node types — line (`//`) and block (slash-star) comments. */ +const COMMENT_TYPES = new Set(['line_comment', 'block_comment']); +const isNotComment = (n: SyntaxNode): boolean => !COMMENT_TYPES.has(n.type); + +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 Rust walk state. One instance per function so the + * {@link ControlFlowContext} and label tables are scoped to that function and + * never leak across functions. + */ +class RustCfgWalk { + private readonly cfc = new ControlFlowContext(); + + constructor( + private readonly builder: CfgBuilder, + private readonly harvest: RustHarvester, + ) {} + + /** Statements of a `block`, ignoring comments. */ + private statementsOf(block: SyntaxNode): SyntaxNode[] { + return block.namedChildren.filter(isNotComment); + } + + /** The `body` of a node (a `block`, or a bare expression for a closure / arm). */ + private bodyOf(node: SyntaxNode): SyntaxNode | undefined { + return node.childForFieldName('body') ?? undefined; + } + + /** Visit a body that may be a `block` or a single expression. */ + private visitBody(node: SyntaxNode | undefined | null): SeqResult { + if (!node) return null; + if (node.type === 'block') 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) + if (entry === undefined) entry = res.entry; + else this.builder.connect(dangling, res.entry, 'seq'); + dangling = [...res.exits]; + } else { + const idx = + openSimple === undefined + ? this.openBlock(stmt) + : this.extendOpen(openSimple, stmt); + if (openSimple === undefined) { + if (entry === undefined) entry = idx; + else this.builder.connect(dangling, idx, 'seq'); + dangling = [idx]; + } + openSimple = idx; + // A straight-line statement that contains a `?` early-returns to EXIT. + this.wireTryExits(stmt, idx); + } + } + + if (entry === undefined) return null; + return { entry, exits: dangling }; + } + + private openBlock(stmt: SyntaxNode): number { + return this.builder.newBlock( + startLineOf(stmt), + endLineOf(stmt), + stmt.text, + 'normal', + this.harvest.facts(stmt), + ); + } + + private extendOpen(open: number, stmt: SyntaxNode): number { + this.builder.extendBlock(open, endLineOf(stmt), stmt.text, this.harvest.facts(stmt)); + return open; + } + + /** Whether a statement node breaks the current straight-line block. */ + private isControlFlow(stmt: SyntaxNode): boolean { + if (stmt.type === 'expression_statement') { + const inner = this.exprStmtInner(stmt); + return inner ? CONTROL_FLOW_EXPR_TYPES.has(inner.type) : false; + } + if (stmt.type === 'let_declaration') { + // `let v = loop {…}` / `let w = if c {…} else {…}` / `let PAT = e else {…}` + // — the value is a control-flow EXPRESSION, or the let-else alternative + // block is divergent; either way the let must be modeled structurally. + const value = stmt.childForFieldName('value'); + const alt = stmt.childForFieldName('alternative'); + return (value !== null && CONTROL_FLOW_EXPR_TYPES.has(value.type)) || alt !== null; + } + return CONTROL_FLOW_TYPES.has(stmt.type); + } + + /** The inner expression of an `expression_statement` (`if x {…};`). */ + private exprStmtInner(stmt: SyntaxNode): SyntaxNode | undefined { + return stmt.namedChildren.find(isNotComment); + } + + /** Dispatch one statement to its handler. Non-null except for empty blocks. */ + visitStmt(stmt: SyntaxNode): SeqResult { + // Unwrap an `expression_statement` to its control-flow expression. + if (stmt.type === 'expression_statement') { + const inner = this.exprStmtInner(stmt); + if (inner && CONTROL_FLOW_EXPR_TYPES.has(inner.type)) return this.visitStmt(inner); + return this.visitSimple(stmt); + } + if (stmt.type === 'let_declaration') return this.visitLet(stmt); + switch (stmt.type) { + case 'if_expression': + return this.visitIf(stmt); + case 'loop_expression': + return this.visitLoop(stmt); + case 'while_expression': + return this.visitWhile(stmt); + case 'for_expression': + return this.visitFor(stmt); + case 'match_expression': + return this.visitMatch(stmt); + case 'return_expression': + return this.visitReturn(stmt); + case 'break_expression': + return this.visitBreak(stmt); + case 'continue_expression': + return this.visitContinue(stmt); + case 'block': + return this.visitSeq(this.statementsOf(stmt)); + default: + return this.visitSimple(stmt); + } + } + + private visitSimple(stmt: SyntaxNode): TraversalResult { + const idx = this.openBlock(stmt); + this.wireTryExits(stmt, idx); + return { entry: idx, exits: [idx] }; + } + + /** + * `let PAT = VALUE [else { ALT }]` whose VALUE is a control-flow expression + * (`let v = loop {…}`, `let w = if c {…} else {…}`, `let m = match …`) or which + * has a divergent let-else ALT block. A plain `let x = e;` never reaches here + * (it coalesces into a straight-line block in {@link visitSeq}). + * + * The value's control-flow construct is visited as a sub-CFG; the let-pattern's + * bindings are defs that happen on the value's NORMAL completion — attached to a + * facts-only continuation block the value's exits feed. For a `let … else`, the + * ALT block runs on the refutable-failure path and (per Rust's rules) MUST + * diverge (return/break/continue/panic); it is visited as control flow, so its + * own jumps wire directly to their targets and it contributes no normal exit. + */ + private visitLet(stmt: SyntaxNode): TraversalResult { + const value = stmt.childForFieldName('value'); + const alt = stmt.childForFieldName('alternative'); + const cfValue = value !== null && CONTROL_FLOW_EXPR_TYPES.has(value.type); + + let entry: number; + let normalExits: number[]; + if (cfValue && value) { + // `let v = loop {…}` — the value is a control-flow construct: visit it, then + // attach ONLY the pattern's binding defs to a facts-only continuation (the + // value's uses are already harvested onto its own blocks). + const valueRes = this.visitStmt(value); + const cont = this.builder.newBlock( + startLineOf(stmt), + startLineOf(stmt), + '', + 'normal', + this.harvest.letPatternFacts(stmt), + ); + this.builder.connect(valueRes?.exits ?? [], cont, 'seq'); + entry = valueRes ? valueRes.entry : cont; + normalExits = [cont]; + } else { + // A simple-value `let PAT = e [else {…}]` — ONE block with the whole let's + // facts (value uses + pattern defs). It reaches here only via the let-else + // alternative (a plain `let x = e;` coalesces in visitSeq). + const idx = this.openBlock(stmt); + this.wireTryExits(stmt, idx); + entry = idx; + normalExits = [idx]; + } + + // `let … else { … }` — the else block runs on the binding-FAILURE path and + // (per Rust) MUST diverge (return/break/continue/panic); visit it as control + // flow so its jumps wire themselves to their targets. It is NOT on the normal + // continuation — branched from the binding site with a `cond-false` (refute) + // edge. + if (alt) { + const altRes = this.visitBody(alt); + if (altRes) this.builder.connect(normalExits, altRes.entry, 'cond-false'); + } + + return { entry, exits: normalExits }; + } + + /** + * Emit a `throw` (early-return) edge to EXIT for every `?` operator inside a + * straight-line statement's subtree (excluding nested function bodies). The + * `?` desugars to "return Err(...) early"; modeling it as a throw-like edge to + * EXIT keeps the early-exit path represented while the Ok path falls through + * normally. Deduped by the builder, so repeated `?` in a block emit one edge. + */ + private wireTryExits(stmt: SyntaxNode, fromBlock: number): void { + if (this.containsTry(stmt)) this.builder.edge(fromBlock, this.builder.exitIndex, 'throw'); + } + + private containsTry(node: SyntaxNode): boolean { + if (node.type === 'try_expression') return true; + if (NESTED_FUNCTION_TYPES.has(node.type)) return false; // opaque + for (let i = 0; i < node.namedChildCount; i++) { + const c = node.namedChild(i); + if (c && this.containsTry(c)) return true; + } + return false; + } + + /** `return [expr]` — direct edge to the function EXIT. */ + private visitReturn(stmt: SyntaxNode): TraversalResult { + const idx = this.builder.newBlock( + startLineOf(stmt), + endLineOf(stmt), + stmt.text, + 'normal', + this.harvest.facts(stmt), + ); + // A `return f()?;` early-returns on the `?` path too. + this.wireTryExits(stmt, idx); + this.builder.edge(idx, this.builder.exitIndex, 'return'); + return { entry: idx, exits: [] }; + } + + /** + * `break ['label] [value]` — targets the labeled loop frame if labeled, else the + * nearest enclosing loop. `break value` is still a `break` edge (the value is a + * normal use harvested onto the block). + */ + private visitBreak(stmt: SyntaxNode): TraversalResult { + const idx = this.builder.newBlock( + startLineOf(stmt), + endLineOf(stmt), + stmt.text, + 'normal', + this.harvest.facts(stmt), + ); + this.wireTryExits(stmt, idx); + 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: [] }; + } + + /** `continue ['label]` — re-tests the labeled (or nearest) loop header. */ + 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 bare name (`outer`) of a `break`/`continue`'s `'label`, if any. */ + private jumpLabel(stmt: SyntaxNode): string | undefined { + const label = stmt.namedChildren.find((c) => c.type === 'label'); + return this.labelName(label); + } + + /** The bare identifier name of a `label` node (`'outer` ⇒ `outer`). */ + private labelName(label: SyntaxNode | undefined): string | undefined { + if (!label) return undefined; + const id = label.namedChildren.find((c) => c.type === 'identifier'); + return id?.text ?? label.text.replace(/^'/, ''); + } + + /** + * The optional `'label` of a loop expression (a NAMED CHILD, not a field — Rust + * attaches the label directly to the loop, unlike Go's `labeled_statement`). + */ + private loopLabels(stmt: SyntaxNode): string[] { + const label = stmt.namedChildren.find((c) => c.type === 'label'); + const name = this.labelName(label); + return name !== undefined ? [name] : []; + } + + /** + * `if COND { … } [else { … } | else if …]`. The condition can be a plain + * expression, a `let_condition` (`if let PAT = e`), or a `let_chain`. The header + * carries the condition's def/use facts (an `if let` pattern is a def, its value + * a use). The else `alternative` is an `else_clause` wrapping a `block` or a + * nested `if_expression` (the `else if` chain). + */ + private visitIf(stmt: SyntaxNode): TraversalResult { + const cond = stmt.childForFieldName('condition') ?? stmt; + const header = this.builder.newBlock( + startLineOf(stmt), + endLineOf(cond), + cond.text, + 'normal', + this.condFacts(cond, false), + ); + this.wireTryExits(cond, header); + + const exits: number[] = []; + const thenRes = this.visitBody(stmt.childForFieldName('consequence')); + 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.elseBodyOf(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)] }; + } + + /** The else body of an `if_expression` (unwraps the `else_clause` wrapper). */ + private elseBodyOf(stmt: SyntaxNode): SyntaxNode | undefined { + const alt = stmt.childForFieldName('alternative'); + if (!alt) return undefined; + if (alt.type === 'else_clause') { + // The clause wraps a `block` or a nested `if_expression` (`else if`). + return alt.namedChildren.find(isNotComment); + } + return alt; + } + + /** + * `loop { … }` — Rust's INFINITE loop (NO condition). Body exits re-enter the + * header (`loop-back`); a `break` reaches `loopExit`. We ALWAYS emit a + * structural `header → loopExit` `cond-false` escape edge so EXIT stays + * reverse-reachable (a `loop {}` with no break never reaches EXIT otherwise, and + * the CDG pass would be silently skipped for the whole function). + */ + private visitLoop(stmt: SyntaxNode): TraversalResult { + const labels = this.loopLabels(stmt); + const header = this.builder.newBlock(startLineOf(stmt), startLineOf(stmt), 'loop'); + const loopExit = this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), ''); + + this.cfc.pushLoop(header, loopExit, labels); + const body = this.visitBody(this.bodyOf(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 `loop {}` re-enters + } + // Structural escape edge — keeps EXIT reverse-reachable even for `loop {}` + // with no `break` (the canonical Rust non-terminating case). + this.builder.edge(header, loopExit, 'cond-false'); + return { entry: header, exits: [loopExit] }; + } + + /** + * `while COND { … }` (and `while let PAT = e { … }`). Standard loop: header + * tests, true → body → loop-back, false → loop exit. The `while let` pattern is + * a may-def on the header (the binding does not happen on the exit iteration). + */ + private visitWhile(stmt: SyntaxNode): TraversalResult { + const labels = this.loopLabels(stmt); + const cond = stmt.childForFieldName('condition') ?? stmt; + const header = this.builder.newBlock( + startLineOf(stmt), + endLineOf(cond), + cond.text, + 'normal', + this.condFacts(cond, true), + ); + const loopExit = this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), ''); + + this.cfc.pushLoop(header, loopExit, labels); + const body = this.visitBody(this.bodyOf(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] }; + } + + /** + * `for PAT in ITER { … }`. The header binds the loop pattern (a def) and uses + * the iterated expression. Standard loop topology. + */ + private visitFor(stmt: SyntaxNode): TraversalResult { + const labels = this.loopLabels(stmt); + const value = stmt.childForFieldName('value'); + const headEnd = value ? endLineOf(value) : startLineOf(stmt); + const header = this.builder.newBlock( + startLineOf(stmt), + headEnd, + 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.bodyOf(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 pat = stmt.childForFieldName('pattern')?.text ?? ''; + const value = stmt.childForFieldName('value')?.text ?? ''; + return pat || value ? `for ${pat} in ${value}` : 'for'; + } + + /** + * `match VALUE { PAT [if guard] => ARM, … }`. Arms do NOT fall through (like + * Go / Python). Each arm body is dispatched from the subject block with a + * `switch-case` edge; arm bodies rejoin AFTER the match. A `match` with no + * irrefutable `_` arm also reaches the join directly (no-match path), keeping + * EXIT reverse-reachable. + */ + private visitMatch(stmt: SyntaxNode): TraversalResult { + const value = stmt.childForFieldName('value'); + const dispatch = this.builder.newBlock( + startLineOf(stmt), + value ? endLineOf(value) : startLineOf(stmt), + value ? `match ${value.text}` : 'match', + 'normal', + value ? this.harvest.facts(value) : undefined, + ); + if (value) this.wireTryExits(value, dispatch); + const matchExit = this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), ''); + + const body = + stmt.childForFieldName('body') ?? stmt.namedChildren.find((c) => c.type === 'match_block'); + const arms = body ? body.namedChildren.filter((c) => c.type === 'match_arm') : []; + + // A guarded arm (`PAT if g`) evaluates `g` conditionally — harvest its uses + // onto the dispatch block (a later arm tests only when earlier ones didn't + // match; any def there is a may-def). + for (const arm of arms) { + const guard = this.armGuard(arm); + if (guard) this.builder.attachFacts(dispatch, this.harvest.factsConditional(guard)); + } + + this.cfc.pushSwitch(matchExit, []); + let hasIrrefutable = false; + for (const arm of arms) { + // The arm-pattern bindings are defs that happen on dispatch into the arm — + // attach them to the arm body's entry. The arm body may be an expr or block. + const armBody = this.visitBody(arm.childForFieldName('value')); + const entry = armBody?.entry ?? matchExit; + this.builder.edge(dispatch, entry, 'switch-case'); + if (armBody) this.builder.connect(armBody.exits, matchExit, 'seq'); + if (this.isIrrefutableArm(arm)) hasIrrefutable = true; + } + this.cfc.pop(); + + // No catch-all arm → a no-match path reaches the exit directly. (A real Rust + // match is exhaustive, but a non-`_`-tailed match keeps EXIT reverse-reachable + // even when every arm body jumps.) + if (!hasIrrefutable) this.builder.edge(dispatch, matchExit, 'switch-case'); + + return { entry: dispatch, exits: [matchExit] }; + } + + /** The guard condition of a `match_arm` (`PAT if g`), if any. */ + private armGuard(arm: SyntaxNode): SyntaxNode | undefined { + const pat = arm.childForFieldName('pattern'); + return pat?.childForFieldName('condition') ?? undefined; + } + + /** A `_ =>` arm with no guard is the unconditional catch-all. */ + private isIrrefutableArm(arm: SyntaxNode): boolean { + if (this.armGuard(arm)) return false; + const pat = arm.childForFieldName('pattern'); + return pat?.text.trim() === '_'; + } + + /** + * Def/use facts for an `if`/`while` condition. A `let_condition` binds a pattern + * (a def — a may-def for `while let`, which re-tests) and uses its value; a + * `let_chain` threads through each `let_condition`. A plain expression is walked + * for uses. + */ + private condFacts(cond: SyntaxNode, loopCond: boolean): ReturnType { + if (cond.type === 'let_condition') { + return this.harvest.letConditionFacts(cond, loopCond); + } + // A `let_chain` (`let PAT = e && cond`) — harvest the whole chain. The let + // bindings inside it are defs (may-defs for a while-let chain). + return this.harvest.facts(cond); + } +} + +/** Build the CFG for one Rust function / closure node, or `undefined`. */ +function buildFunctionCfg(fnNode: SyntaxNode, filePath: string): FunctionCfg | undefined { + try { + if (!RUST_FUNCTION_TYPES.has(fnNode.type)) return undefined; + const startLine = startLineOf(fnNode); + const endLine = endLineOf(fnNode); + const startColumn = fnNode.startPosition.column; + + const body = fnNode.childForFieldName('body'); + if (!body) return undefined; // trait method signature / no body + + const builder = new CfgBuilder(filePath, startLine, endLine, startColumn); + const harvest = new RustHarvester(fnNode); + + const paramFacts = harvest.paramFacts(); + if (paramFacts) builder.attachFacts(builder.entryIndex, paramFacts); + + const walk = new RustCfgWalk(builder, harvest); + + if (body.type !== 'block') { + // A closure with an expression body (`|x| x + 1`): one block whose value is + // the returned expression. A `?` inside it early-returns to EXIT. + const res = walk.visitStmt(body); + builder.edge(builder.entryIndex, res ? res.entry : builder.exitIndex, 'seq'); + builder.connect(res ? res.exits : [builder.entryIndex], builder.exitIndex, 'seq'); + return builder.finish(harvest.bindingTable()); + } + + const res = walk.visitSeq(body.namedChildren.filter(isNotComment)); + if (!res) { + builder.edge(builder.entryIndex, builder.exitIndex, 'seq'); // empty body + return builder.finish(harvest.bindingTable()); + } + builder.edge(builder.entryIndex, res.entry, 'seq'); + builder.connect(res.exits, builder.exitIndex, 'seq'); // normal fall-off → EXIT + 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] Rust buildFunctionCfg skipped a function in ${filePath}: ${String(err)}`); + return undefined; + } +} + +/** Whether a node is a Rust function/closure this visitor builds a CFG for. */ +function isFunction(node: SyntaxNode): boolean { + return RUST_FUNCTION_TYPES.has(node.type); +} + +/** The Rust CFG visitor. */ +export function createRustCfgVisitor(): CfgVisitor { + return { buildFunctionCfg, isFunction }; +} + +export { RUST_FUNCTION_TYPES }; diff --git a/gitnexus/src/core/ingestion/languages/rust.ts b/gitnexus/src/core/ingestion/languages/rust.ts index 9f1bbc7ae..a0102c334 100644 --- a/gitnexus/src/core/ingestion/languages/rust.ts +++ b/gitnexus/src/core/ingestion/languages/rust.ts @@ -28,6 +28,7 @@ import { createVariableExtractor } from '../variable-extractors/generic.js'; import { rustVariableConfig } from '../variable-extractors/configs/rust.js'; import { createCallExtractor } from '../call-extractors/generic.js'; import { rustCallConfig } from '../call-extractors/configs/rust.js'; +import { createRustCfgVisitor } from '../cfg/visitors/rust.js'; import { emitRustScopeCaptures, rustArityCompatibility, @@ -178,6 +179,7 @@ export const rustProvider = defineLanguage({ builtInNames: BUILT_INS, // ── RFC #909 Ring 3: scope-based resolution hooks ────────── emitScopeCaptures: emitRustScopeCaptures, + cfgVisitor: createRustCfgVisitor(), interpretImport: interpretRustImport, interpretTypeBinding: interpretRustTypeBinding, bindingScopeFor: rustBindingScopeFor, diff --git a/gitnexus/test/integration/cfg/fixtures/rust-hazards.rs b/gitnexus/test/integration/cfg/fixtures/rust-hazards.rs new file mode 100644 index 000000000..4870bbdbb --- /dev/null +++ b/gitnexus/test/integration/cfg/fixtures/rust-hazards.rs @@ -0,0 +1,172 @@ +// Rust CFG hazard fixture (#2195 U7). Exercises every control-flow construct the +// Rust visitor models, including the EXIT-reachability hazard (`loop {}` — the +// canonical INFINITE loop with NO condition) the CDG soundness gate depends on, +// plus the Rust-specific shapes: everything is an EXPRESSION (if / loop / while / +// for / match / block), `if let` / `while let` / let-chains, labeled break / +// continue (with a value), the `?` early-return operator, let-else, and the +// destructuring def shapes (tuple / struct / slice / tuple-struct patterns). + +// ── if / else if / else (incl. if let) ────────────────────────────────────── + +fn if_elif_else(x: i32) -> i32 { + if x > 0 { + positive() + } else if x < 0 { + negative() + } else { + zero() + } +} + +fn if_let_chain(opt: Option) -> i32 { + // `if let PAT = e && cond` — a let-chain condition; the binding is a def. + if let Some(n) = opt && n > 0 { + both(n) + } else { + none() + } +} + +// ── loop {} — the INFINITE loop (NO condition), the EXIT-reachability hazard ── + +fn loop_forever(x: bool) { + // `loop {}` never terminates on its own; the visitor must emit a structural + // escape edge so EXIT stays reverse-reachable (else CDG is silently skipped). + loop { + if x { + tick(); + } + } +} + +fn loop_with_break(items: Vec) -> i32 { + let mut i = 0; + // `let v = loop { break value; }` — a loop in value position whose `break` + // carries a value. + let found = loop { + if i >= items.len() { + break -1; + } + if is_match(i) { + break i as i32; + } + i += 1; + }; + found +} + +// ── while / while let / for ───────────────────────────────────────────────── + +fn while_loop(mut x: i32) -> i32 { + while x > 0 { + x -= 1; + } + x +} + +fn while_let(mut it: Iter) -> i32 { + let mut sum = 0; + // `while let Some(n) = it.next()` — the binding is a MAY-def (no bind on the + // exit iteration). + while let Some(n) = it.next() { + sum += n; + } + sum +} + +fn for_loop(xs: Vec) -> i32 { + let mut total = 0; + for item in xs { + total += item; + } + total +} + +// ── labeled break / continue from nested loops ────────────────────────────── + +fn labeled_jumps(grid: Vec>) -> i32 { + 'outer: for row in grid { + for cell in row { + if cell < 0 { + break 'outer; // exits BOTH loops + } + if cell == 0 { + continue 'outer; // re-tests the OUTER loop + } + use_cell(cell); + } + } + done() +} + +// ── match (no fallthrough) + guards + binding patterns ────────────────────── + +fn match_arms(x: i32) -> i32 { + match x { + 0 => zero(), + 1 | 2 => small(), // or-pattern + n if n > 100 => big(n), // guarded arm + v @ 3..=9 => mid(v), // captured (`@`) + range pattern + _ => { // block-bodied catch-all arm + let r = other(); + r + } + } +} + +// ── return + the ? early-return operator ──────────────────────────────────── + +fn try_operator(opt: Option) -> Option { + // `opt?` early-returns None on the None path (a throw-like edge to EXIT); + // the Ok path falls through. + let n = opt?; + if n > 0 { + return Some(n * 2); + } + Some(n) +} + +// ── let destructuring + let-else ──────────────────────────────────────────── + +fn destructuring() -> i32 { + let (a, b) = pair(); // tuple pattern — both a and b are defs + let Point { x, y } = point(); // struct pattern — x and y are defs + let [p, q] = couple(); // slice pattern — p and q are defs + a + b + x + y + p + q +} + +fn let_else(opt: Option) -> i32 { + // `let Some(n) = e else { … }` — the else block diverges (return) on the + // binding-failure path; `n` is a def on the success path. + let Some(n) = opt else { + return -1; + }; + n * 2 +} + +// ── closures (own CFG; opaque to the enclosing function) ──────────────────── + +fn with_closure(xs: Vec) -> i32 { + let doubler = |v: i32| -> i32 { v * 2 }; // closure body is its own CFG + let mut total = 0; + for x in xs { + total += doubler(x); + } + total +} + +// ── method + impl block ───────────────────────────────────────────────────── + +struct Counter { + value: i32, +} + +impl Counter { + fn bump(&mut self, by: i32) -> i32 { + self.value += by; + if self.value > 100 { + return self.value; + } + self.value + } +} diff --git a/gitnexus/test/unit/cfg/rust-visitor.test.ts b/gitnexus/test/unit/cfg/rust-visitor.test.ts new file mode 100644 index 000000000..fd0f008ff --- /dev/null +++ b/gitnexus/test/unit/cfg/rust-visitor.test.ts @@ -0,0 +1,395 @@ +import { describe, it, expect } from 'vitest'; +import { createRequire } from 'node:module'; +import { createRustCfgVisitor } from '../../../src/core/ingestion/cfg/visitors/rust.js'; +import type { FunctionCfg } from '../../../src/core/ingestion/cfg/types.js'; +import { makeCfgHarness, type CfgHarness } from '../../helpers/cfg-harness.js'; +import { isExitReachableFromAllBlocks } from '../../../src/core/ingestion/cfg/post-dominators.js'; +import { computeControlDependence } from '../../../src/core/ingestion/cfg/control-dependence.js'; + +// U7 — the Rust CfgVisitor, one hazard per test (real-parser regression, NOT +// snapshot-pinning). Each fixture's distinctive statement text (step(), done(), +// one(), …) lets us locate the block for a region by text and assert the +// control-flow topology around it. Rust is the EXPRESSION-oriented target — +// `loop {}` (the canonical infinite loop, NO condition) is the load-bearing +// EXIT-reachability case, and the `?` operator is an early-return edge. + +const rustGrammar = createRequire(import.meta.url)('tree-sitter-rust') as Parameters< + typeof makeCfgHarness +>[0]; + +const rust: CfgHarness = makeCfgHarness(rustGrammar, createRustCfgVisitor(), 'fixture.rs'); + +const block = (cfg: FunctionCfg, substr: string): number => { + const b = cfg.blocks.find((bl) => bl.text.includes(substr)); + if (!b) throw new Error(`no block containing ${JSON.stringify(substr)}`); + return b.index; +}; + +const edgeKinds = (cfg: FunctionCfg): Set => new Set(cfg.edges.map((e) => e.kind)); + +function reaches(cfg: FunctionCfg, from: number, to: number): boolean { + const adj = new Map(); + for (const e of cfg.edges) (adj.get(e.from) ?? adj.set(e.from, []).get(e.from)!).push(e.to); + const seen = new Set([from]); + const stack = [from]; + while (stack.length) { + const n = stack.pop() as number; + if (n === to) return true; + for (const nx of adj.get(n) ?? []) if (!seen.has(nx)) (seen.add(nx), stack.push(nx)); + } + return seen.has(to); +} +const reachable = (cfg: FunctionCfg, idx: number): boolean => reaches(cfg, cfg.entryIndex, idx); + +/** 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; +} + +const hasDef = (cfg: FunctionCfg, idx: number): boolean => + cfg.blocks.some((bl) => bl.statements?.some((s) => s.defs.includes(idx))); +const hasUse = (cfg: FunctionCfg, idx: number): boolean => + cfg.blocks.some((bl) => bl.statements?.some((s) => s.uses.includes(idx))); +const hasMayDef = (cfg: FunctionCfg, idx: number): boolean => + cfg.blocks.some((bl) => bl.statements?.some((s) => (s.mayDefs ?? []).includes(idx))); + +describe('Rust CfgVisitor — structure', () => { + it('straight-line body: ENTRY → block → EXIT (seq)', () => { + const cfg = rust.cfgOf(`fn 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 = rust.cfgOf(`fn f() {}`); + expect(cfg.blocks).toHaveLength(2); + expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true); + }); + + it('fn, method (impl), and closure are all CFG-bearing', () => { + const cfgs = rust.cfgsOf( + `fn f() { x(); }\nstruct S;\nimpl S { fn m(&self) { y(); } }\nfn g() { let c = || { z(); }; use_it(c); }`, + ); + expect(cfgs.length).toBeGreaterThanOrEqual(4); // f, m, g, the closure + for (const cfg of cfgs) expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true); + }); + + it('trait method signature / no body → undefined, never throws', () => { + const root = rust.parse(`trait T { fn m(&self); }`); + const fns = rust.collectFunctions(root); + for (const fn of fns) { + expect(() => createRustCfgVisitor().buildFunctionCfg(fn, 'f.rs')).not.toThrow(); + } + }); + + it('two same-line fns get distinct functionStartColumn', () => { + const cfgs = rust.cfgsOf(`fn a() { x(); } fn b() { y(); }`); + expect(cfgs).toHaveLength(2); + expect(cfgs[0].functionStartLine).toBe(cfgs[1].functionStartLine); + expect(cfgs[0].functionStartColumn).not.toBe(cfgs[1].functionStartColumn); + }); +}); + +describe('Rust CfgVisitor — if / else / if let', () => { + it('if/else if/else: every arm reaches EXIT, both senses present', () => { + const cfg = rust.cfgOf( + `fn f(x: i32) { if x > 0 { a(); } else if x < 0 { b(); } else { c(); } }`, + ); + const kinds = edgeKinds(cfg); + expect(kinds.has('cond-true')).toBe(true); + expect(kinds.has('cond-false')).toBe(true); + expect(reaches(cfg, block(cfg, 'a()'), cfg.exitIndex)).toBe(true); + expect(reaches(cfg, block(cfg, 'b()'), cfg.exitIndex)).toBe(true); + expect(reaches(cfg, block(cfg, 'c()'), cfg.exitIndex)).toBe(true); + }); + + it('both arms reach a common join after the if', () => { + const cfg = rust.cfgOf(`fn f(x: i32) { if x > 0 { a(); } else { b(); } c(); }`); + const join = block(cfg, 'c()'); + expect(reaches(cfg, block(cfg, 'a()'), join)).toBe(true); + expect(reaches(cfg, block(cfg, 'b()'), join)).toBe(true); + }); + + it('if let Some(n) = opt { … } defines n (cond-true), else reachable', () => { + const cfg = rust.cfgOf( + `fn f(opt: Option) { if let Some(n) = opt { use_n(n); } else { none(); } }`, + ); + expect(edgeKinds(cfg).has('cond-true')).toBe(true); + expect(edgeKinds(cfg).has('cond-false')).toBe(true); + expect(hasDef(cfg, bindingIdx(cfg, 'n'))).toBe(true); + expect(hasUse(cfg, bindingIdx(cfg, 'opt'))).toBe(true); + expect(reaches(cfg, block(cfg, 'none()'), cfg.exitIndex)).toBe(true); + }); + + it('if let with a let-chain (let PAT = e && cond) still binds + branches', () => { + const cfg = rust.cfgOf( + `fn f(opt: Option) { if let Some(n) = opt && n > 0 { both(); } }`, + ); + expect(edgeKinds(cfg).has('cond-true')).toBe(true); + expect(hasDef(cfg, bindingIdx(cfg, 'n'))).toBe(true); + expect(reaches(cfg, block(cfg, 'both()'), cfg.exitIndex)).toBe(true); + }); +}); + +describe('Rust CfgVisitor — loop {} (infinite, the EXIT-reachability hazard)', () => { + it('loop {} with no break keeps EXIT reverse-reachable AND emits CDG > 0', () => { + // The plan's load-bearing CDG probe: `loop {}` is the canonical Rust infinite + // loop with NO condition. A structural escape edge must keep EXIT + // reverse-reachable or the CDG pass is silently skipped for the function. + const cfg = rust.cfgOf(`fn f(x: bool) { loop { if x { g(); } } }`); + expect(edgeKinds(cfg).has('loop-back')).toBe(true); + expect(edgeKinds(cfg).has('cond-false')).toBe(true); // the structural escape edge + expect(isExitReachableFromAllBlocks(cfg)).toBe(true); + // The inner `if` is a real control point — CDG is only computed when EXIT + // stays reverse-reachable, so a non-empty CDG proves the escape edge works. + expect(computeControlDependence(cfg).edges.length).toBeGreaterThan(0); + }); + + it('loop { … break; } reaches EXIT via the break', () => { + const cfg = rust.cfgOf(`fn f() { loop { work(); break; } done(); }`); + expect(edgeKinds(cfg).has('break')).toBe(true); + const brk = block(cfg, 'break'); + expect(reaches(cfg, brk, block(cfg, 'done()'))).toBe(true); + expect(reaches(cfg, brk, cfg.exitIndex)).toBe(true); + expect(isExitReachableFromAllBlocks(cfg)).toBe(true); + }); + + it('let v = loop { break 42; }; (break with a value) is a break edge', () => { + const cfg = rust.cfgOf(`fn f() { let v = loop { break 42; }; use_v(v); }`); + expect(edgeKinds(cfg).has('break')).toBe(true); + // `break 42` still terminates the loop and reaches the post-loop continuation. + expect(reaches(cfg, block(cfg, 'break 42'), block(cfg, 'use_v(v)'))).toBe(true); + }); +}); + +describe('Rust CfgVisitor — while / while let / for', () => { + it('while cond {}: header + back-edge + exit', () => { + const cfg = rust.cfgOf(`fn f(x: bool) { while x { step(); } done(); }`); + const header = block(cfg, 'x'); + 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(edgeKinds(cfg).has('cond-false')).toBe(true); + expect(reaches(cfg, header, block(cfg, 'done()'))).toBe(true); + }); + + it('while let Some(n) = next() { … }: header binds n (may-def), uses next', () => { + const cfg = rust.cfgOf(`fn f() { while let Some(n) = next() { use_n(n); } done(); }`); + expect(edgeKinds(cfg).has('cond-true')).toBe(true); + expect(edgeKinds(cfg).has('loop-back')).toBe(true); + // `while let` re-tests, so the binding is a MAY-def (it doesn't bind on the + // exit iteration), not falsely killing a prior def. + expect(hasMayDef(cfg, bindingIdx(cfg, 'n'))).toBe(true); + expect(reaches(cfg, block(cfg, 'use_n(n)'), block(cfg, 'done()'))).toBe(true); + }); + + it('for item in xs {}: loop var is a def, source a use, back-edge present', () => { + const cfg = rust.cfgOf(`fn f(xs: Vec) { for item in xs { use_item(item); } done(); }`); + const body = block(cfg, 'use_item(item)'); + expect(edgeKinds(cfg).has('cond-true')).toBe(true); + expect(edgeKinds(cfg).has('loop-back')).toBe(true); + expect(hasDef(cfg, bindingIdx(cfg, 'item'))).toBe(true); + expect(hasUse(cfg, bindingIdx(cfg, 'xs'))).toBe(true); + const header = cfg.edges.find((e) => e.kind === 'loop-back' && e.from === body)?.to; + expect(header).toBeDefined(); + expect(reaches(cfg, header!, block(cfg, 'done()'))).toBe(true); + }); + + it('while true {} still emits the structural escape edge (EXIT reachable)', () => { + const cfg = rust.cfgOf(`fn f() { while true { if cond() { g(); } } }`); + expect(edgeKinds(cfg).has('cond-false')).toBe(true); + expect(isExitReachableFromAllBlocks(cfg)).toBe(true); + expect(computeControlDependence(cfg).edges.length).toBeGreaterThan(0); + }); +}); + +describe('Rust CfgVisitor — match (no fallthrough) + guards', () => { + it('match arms do NOT fall through; each body rejoins after the match', () => { + const cfg = rust.cfgOf( + `fn f(x: i32) { + match x { + 1 => one(), + 2 => two(), + _ => other(), + } + after(); + }`, + ); + expect(edgeKinds(cfg).has('switch-case')).toBe(true); + expect(reaches(cfg, block(cfg, 'one()'), block(cfg, 'after()'))).toBe(true); + expect(reaches(cfg, block(cfg, 'two()'), block(cfg, 'after()'))).toBe(true); + // arm 1 does NOT fall into arm 2 (no fallthrough). + expect(reaches(cfg, block(cfg, 'one()'), block(cfg, 'two()'))).toBe(false); + }); + + it('a guarded arm (n if n > 0) harvests the guard onto the dispatch block', () => { + const cfg = rust.cfgOf( + `fn f(x: i32) { + match x { + n if n > 0 => pos(n), + _ => other(), + } + after(); + }`, + ); + expect(edgeKinds(cfg).has('switch-case')).toBe(true); + // the guard test `n > 0` is a use on the dispatch header (n binds in the arm). + expect(hasUse(cfg, bindingIdx(cfg, 'n'))).toBe(true); + expect(reaches(cfg, block(cfg, 'pos(n)'), block(cfg, 'after()'))).toBe(true); + }); + + it('a match with NO `_` arm keeps a no-match path to the exit', () => { + const cfg = rust.cfgOf( + `fn f(x: i32) { match x { 1 => a(), 2 => b(), } after(); }`, + ); + expect(reaches(cfg, cfg.entryIndex, block(cfg, 'after()'))).toBe(true); + expect(isExitReachableFromAllBlocks(cfg)).toBe(true); + }); +}); + +describe('Rust CfgVisitor — labeled break / continue', () => { + it("break 'outer from a nested loop targets the OUTER loop", () => { + const cfg = rust.cfgOf( + `fn f() { + 'outer: for i in 0..10 { + for j in 0..10 { + if cond() { break 'outer; } + } + } + done(); + }`, + ); + expect(edgeKinds(cfg).has('break')).toBe(true); + const brk = block(cfg, "break 'outer"); + // the labeled break reaches the post-loop `done()`, skipping BOTH loops. + expect(reaches(cfg, brk, block(cfg, 'done()'))).toBe(true); + }); + + it("continue 'outer re-tests the OUTER loop", () => { + const cfg = rust.cfgOf( + `fn f() { + 'outer: for i in 0..10 { + for j in 0..10 { + if cond() { continue 'outer; } + } + } + }`, + ); + expect(edgeKinds(cfg).has('continue')).toBe(true); + const outerHeader = block(cfg, 'for i in 0..10'); + expect(reaches(cfg, block(cfg, "continue 'outer"), outerHeader)).toBe(true); + }); + + it("a plain (unlabeled) break exits the INNER loop only", () => { + const cfg = rust.cfgOf( + `fn f() { + for i in 0..10 { + inner(); + for j in 0..10 { if cond() { break; } } + tail(); + } + done(); + }`, + ); + const brk = block(cfg, 'break'); + // the unlabeled break lands AFTER the inner loop, so `tail()` is reachable. + expect(reaches(cfg, brk, block(cfg, 'tail()'))).toBe(true); + }); +}); + +describe('Rust CfgVisitor — return and the ? operator', () => { + it('return Some(n) flows directly to EXIT (return edge)', () => { + const cfg = rust.cfgOf( + `fn f(n: i32) -> Option { if n > 0 { return Some(n); } Some(0) }`, + ); + expect(edgeKinds(cfg).has('return')).toBe(true); + expect(reaches(cfg, block(cfg, 'return Some(n)'), cfg.exitIndex)).toBe(true); + }); + + it('the ? operator emits an early-return (throw) edge to EXIT', () => { + const cfg = rust.cfgOf( + `fn f(opt: Option) -> Option { let n = opt?; use_n(n); Some(n) }`, + ); + expect(edgeKinds(cfg).has('throw')).toBe(true); + // the `?`-bearing block has a direct early-exit edge to EXIT (Err/None path)... + const tryBlock = block(cfg, 'opt?'); + expect(cfg.edges).toContainEqual({ from: tryBlock, to: cfg.exitIndex, kind: 'throw' }); + // ...while the Ok path falls through to `use_n(n)`. + expect(reaches(cfg, tryBlock, block(cfg, 'use_n(n)'))).toBe(true); + }); +}); + +describe('Rust CfgVisitor — def/use harvest (patterns)', () => { + it('let x = …; use(x) → def then use', () => { + const cfg = rust.cfgOf(`fn f(a: i32, b: i32) { let x = a + b; use_x(x); }`); + const x = bindingIdx(cfg, 'x'); + expect(hasDef(cfg, x)).toBe(true); + expect(hasUse(cfg, x)).toBe(true); + }); + + it('let (a, b) = pair() defines BOTH a and b (tuple destructuring)', () => { + const cfg = rust.cfgOf(`fn f() { let (a, b) = pair(); use_ab(a, b); }`); + expect(hasDef(cfg, bindingIdx(cfg, 'a'))).toBe(true); + expect(hasDef(cfg, bindingIdx(cfg, 'b'))).toBe(true); + expect(hasUse(cfg, bindingIdx(cfg, 'a'))).toBe(true); + expect(hasUse(cfg, bindingIdx(cfg, 'b'))).toBe(true); + }); + + it('let Point { x, y } = pt() defines both struct fields', () => { + const cfg = rust.cfgOf(`fn f() { let Point { x, y } = pt(); use_xy(x, y); }`); + expect(hasDef(cfg, bindingIdx(cfg, 'x'))).toBe(true); + expect(hasDef(cfg, bindingIdx(cfg, 'y'))).toBe(true); + }); + + it('let Some(n) = opt() in a let_else defines n (the tuple-struct inner binds, not the path)', () => { + const cfg = rust.cfgOf(`fn f(opt: Option) { let Some(n) = opt() else { return; }; use_n(n); }`); + expect(hasDef(cfg, bindingIdx(cfg, 'n'))).toBe(true); + expect(hasUse(cfg, bindingIdx(cfg, 'n'))).toBe(true); + // the let_else `else { return; }` jumps to EXIT. + expect(edgeKinds(cfg).has('return')).toBe(true); + }); + + it('compound assignment (x += 1) reads AND writes the lvalue', () => { + const cfg = rust.cfgOf(`fn f() { let mut x = 1; x += 3; use_x(x); }`); + const x = bindingIdx(cfg, 'x'); + expect(hasDef(cfg, x)).toBe(true); + expect(hasUse(cfg, x)).toBe(true); + }); + + it('plain assignment (x = p) defines x; a field write (obj.f = …) is NOT a scalar def', () => { + const cfg = rust.cfgOf(`fn f(p: i32, obj: T) { let mut x = 0; x = p; obj.field = 1; use_x(x); }`); + expect(hasDef(cfg, bindingIdx(cfg, 'x'))).toBe(true); + // `obj` is used (its field is written), never a scalar def for `field`. + expect(hasUse(cfg, bindingIdx(cfg, 'obj'))).toBe(true); + }); + + it('the wildcard `_` binds nothing', () => { + const cfg = rust.cfgOf(`fn f() { let _ = compute(); done(); }`); + expect((cfg.bindings ?? []).some((b) => b.name === '_')).toBe(false); + }); + + it('a closure body is opaque to the enclosing function harvest (own CFG)', () => { + const cfgs = rust.cfgsOf(`fn f() { let cb = || { let inner = 1; use_inner(inner); }; call(cb); }`); + // The OUTER fn has a `cb` binding but NOT `inner` (the closure body is opaque). + const outer = cfgs.find((c) => (c.bindings ?? []).some((b) => b.name === 'cb')); + expect(outer).toBeDefined(); + expect((outer!.bindings ?? []).some((b) => b.name === 'inner')).toBe(false); + // The closure has its OWN CFG where `inner` is a real def. + const inner = cfgs.find((c) => (c.bindings ?? []).some((b) => b.name === 'inner')); + expect(inner).toBeDefined(); + expect(hasDef(inner!, bindingIdx(inner!, 'inner'))).toBe(true); + }); +}); + +describe('Rust CfgVisitor — graceful degradation', () => { + it('an unmodeled / malformed shape never throws (returns a partial CFG)', () => { + // Macros are opaque token trees; any control flow they expand is invisible, + // but the visitor must still produce a valid partial CFG, never throw. + const cfg = rust.cfgOf(`fn f() { println!("{}", compute()); vec![1, 2, 3]; done(); }`); + expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true); + }); +});