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