feat(cfg): Rust CFG visitor + def/use harvest (#2195 U11)

Add createRustCfgVisitor + rust-harvest for the expression-oriented Rust:
if/else + if-let, loop (infinite -- structural escape edge), while/
while-let/for, match (no fallthrough) + guards, labeled break/continue
('outer), break-with-value, ? operator (try_expression) as an
early-return throw edge to EXIT, let-else (diverging else). visitLet
handles control-flow in value position (let x = loop/if/match). Wire into
rustProvider.

Every literal validated against tree-sitter-rust via the probe (label is
a named child not a field; line_comment; _ pattern). loop {} keeps EXIT
reverse-reachable (production CDG probe: 3 edges). 33 real-parser tests;
comprehensive sweep green (519). Gaps: panic, async/.await, macro bodies.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Gergo Magyar 2026-06-14 13:18:44 +00:00
parent ad0a50271e
commit 82159c93d4
5 changed files with 1925 additions and 0 deletions

View file

@ -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<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 RustHarvester {
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 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);
}
}
}
}

View file

@ -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_<node_type>` 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 `<expr>;` 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<RustHarvester['facts']> {
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<SyntaxNode> {
return { buildFunctionCfg, isFunction };
}
export { RUST_FUNCTION_TYPES };

View file

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

View file

@ -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>) -> 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>) -> 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>) -> i32 {
let mut total = 0;
for item in xs {
total += item;
}
total
}
// ── labeled break / continue from nested loops ──────────────────────────────
fn labeled_jumps(grid: Vec<Vec<i32>>) -> 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<i32>) -> Option<i32> {
// `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>) -> 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>) -> 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
}
}

View file

@ -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<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);
/** 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<i32>) { 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<i32>) { 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<i32>) { 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<i32> { 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<i32>) -> Option<i32> { 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<i32>) { 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);
});
});