mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-11 03:38:07 +00:00
feat(cfg): C and C++ CFG visitor + def/use harvest (#2195 U2)
Add createCCfgVisitor/createCppCfgVisitor over a shared CCfgWalk core. Grammar introspection confirmed tree-sitter-c and tree-sitter-cpp share every control-flow node type/field, so CppCfgWalk extends CCfgWalk with only the C++-only nodes (try/catch/throw/for_range_loop/lambda) via a visitExtra hook -- no language conditionals (AGENTS no-language-naming). Wire both into c-cpp.ts providers. Harvest (c-cpp-harvest.ts): two-phase binding table + per-statement defs/uses/mayDefs (no sites[] yet -- U6). Edge kinds match the TS contract; functionStartColumn populated; non-terminating loops (for(;;), while(1)) emit the structural exit-escape edge so EXIT stays reverse-reachable and CDG is not silently skipped -- verified against the production post-dominator + control-dependence solvers (for(;;) -> 3 CDG edges). buildFunctionCfg returns undefined rather than throwing. 23 real-parser regression tests; grammar-literal gate green (literals validated against both grammars). Documented gaps: C++ RAII destructors, setjmp/longjmp, computed goto (route to EXIT + warn). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bc7cc3ffc8
commit
018635074a
6 changed files with 1745 additions and 0 deletions
516
gitnexus/src/core/ingestion/cfg/visitors/c-cpp-harvest.ts
Normal file
516
gitnexus/src/core/ingestion/cfg/visitors/c-cpp-harvest.ts
Normal file
|
|
@ -0,0 +1,516 @@
|
|||
/**
|
||||
* C / C++ def/use harvester (#2195 U2, plan KTD2) — the C-family analogue of
|
||||
* {@link import('./typescript-harvest.js').TsHarvester}.
|
||||
*
|
||||
* Runs in the parse worker next to the C/C++ CFG visitor, extracting
|
||||
* per-statement variable definition/use facts that ride the side channel for
|
||||
* the reaching-defs / CDG solvers. Output is the per-function binding table
|
||||
* ({@link BindingEntry}[]) plus {@link StatementFacts} the visitor attaches to
|
||||
* blocks as it walks. One class serves both languages: the control-flow node
|
||||
* set is identical (grammar-introspection probe confirmed — see U2 report),
|
||||
* and the harvest's def/use node taxonomy (`declaration`/`init_declarator`/
|
||||
* `assignment_expression`/`update_expression`/`parameter_declaration`) is shared
|
||||
* too; C++-only `lambda_expression` is handled exactly like a nested function
|
||||
* (opaque), so no language naming or branching is needed.
|
||||
*
|
||||
* TWO-PHASE, ORDER-INDEPENDENT (load-bearing — mirrors the TS harvester): the
|
||||
* CFG walk is NOT source-order (`visitFor` builds the init block after the body,
|
||||
* `visitDoWhile` the condition before the body), so resolving names against a
|
||||
* scope stack populated *during* the walk would mis-resolve. Phase 1 pre-scans
|
||||
* the whole function subtree once into a completed lexical scope tree; phase 2
|
||||
* resolves defs/uses against that finished tree from any walk order.
|
||||
*
|
||||
* v1 def-semantics scope:
|
||||
* - `declaration` → `init_declarator` (an initialized local is a def; a bare
|
||||
* `int x;` with no initializer writes nothing at runtime — not a def, like
|
||||
* the TS bare-`var` rule).
|
||||
* - `assignment_expression` (plain + compound `+=` etc.), `update_expression`
|
||||
* (`x++`/`--x`) — define and (for compound/update) also use the lvalue.
|
||||
* - parameters (`parameter_declaration` declarator chain).
|
||||
* EXCLUDED, deliberately (TypeScript-CFA precedent): member / pointer / array
|
||||
* writes (`obj.f = …`, `*p = …`, `a[i] = …`) are NOT scalar defs — their
|
||||
* identifiers are uses only. Both directions of nested-function (C++ lambda)
|
||||
* capture are invisible (the lambda body is an opaque block in the enclosing
|
||||
* CFG, exactly as TS treats arrow/function bodies).
|
||||
*
|
||||
* MAY-DEFS: a def inside a conditionally-evaluated subexpression — the right
|
||||
* operand of `&&`/`||` (`if (a && (x = f()))`), a ternary arm, or a switch
|
||||
* case-test — is a may-def (gen without kill), so the not-taken path's prior
|
||||
* def is not falsely killed.
|
||||
*
|
||||
* Identifiers with no in-function declaration (globals, macros, params of an
|
||||
* enclosing scope) resolve to a SYNTHETIC module-level binding (`name@module`),
|
||||
* applied identically by def and use harvesting.
|
||||
*
|
||||
* RAII NOTE: C++ destructors that run at scope exit are NOT represented in the
|
||||
* tree-sitter AST (they are implicit), so this harvest cannot and does not model
|
||||
* destructor side effects — documented gap, see the visitor doc-comment.
|
||||
*
|
||||
* 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(['lambda_expression', 'function_definition']);
|
||||
|
||||
/**
|
||||
* Nodes that open a lexical scope for block-local declarations. A `compound_
|
||||
* statement` is one scope; the for-loops open a scope for their loop variable.
|
||||
*/
|
||||
const SCOPE_TYPES = new Set([
|
||||
'compound_statement',
|
||||
'for_statement',
|
||||
'for_range_loop',
|
||||
'catch_clause',
|
||||
]);
|
||||
|
||||
/** Type-position subtrees — identifiers inside them are not value uses. */
|
||||
const TYPE_CONTEXT_TYPES = new Set([
|
||||
'type_descriptor',
|
||||
'template_argument_list',
|
||||
'template_type',
|
||||
'sized_type_specifier',
|
||||
'primitive_type',
|
||||
]);
|
||||
|
||||
interface Scope {
|
||||
readonly parent: Scope | null;
|
||||
/** name → binding index */
|
||||
readonly table: Map<string, number>;
|
||||
}
|
||||
|
||||
export class CCppHarvester {
|
||||
private readonly bindings: BindingEntry[] = [];
|
||||
private readonly scopeByNode = new Map<number, Scope>();
|
||||
private readonly root: Scope = { parent: null, table: new Map() };
|
||||
private readonly synthetic = new Map<string, number>();
|
||||
private readonly fnId: number;
|
||||
/** Innermost enclosing scope per visited node id (prescan-filled) — O(scope-chain) phase-2 resolution. */
|
||||
private readonly nearestScopeCache = new Map<number, Scope>();
|
||||
/** >0 while walking a conditionally-evaluated subexpression — defs become may-defs. */
|
||||
private conditionalDepth = 0;
|
||||
|
||||
constructor(private readonly fnNode: SyntaxNode) {
|
||||
this.fnId = fnNode.id;
|
||||
this.scopeByNode.set(fnNode.id, this.root);
|
||||
this.declareParams(fnNode);
|
||||
const body = this.bodyOf(fnNode);
|
||||
if (body) this.prescan(body, this.openScope(body));
|
||||
}
|
||||
|
||||
/** The completed binding table — pass to `CfgBuilder.finish`. */
|
||||
table(): readonly BindingEntry[] {
|
||||
return this.bindings;
|
||||
}
|
||||
|
||||
/** The function/lambda body block (`compound_statement`). */
|
||||
private bodyOf(fnNode: SyntaxNode): SyntaxNode | undefined {
|
||||
const body = fnNode.childForFieldName('body');
|
||||
if (body) return body;
|
||||
return fnNode.namedChildren.find((c) => c.type === 'compound_statement');
|
||||
}
|
||||
|
||||
// ── phase 1: declaration pre-scan ────────────────────────────────────────
|
||||
|
||||
private openScope(node: SyntaxNode): Scope {
|
||||
const existing = this.scopeByNode.get(node.id);
|
||||
if (existing) return existing;
|
||||
const scope: Scope = { parent: this.nearestScopeOf(node), table: new Map() };
|
||||
this.scopeByNode.set(node.id, scope);
|
||||
return scope;
|
||||
}
|
||||
|
||||
private nearestScopeOf(node: SyntaxNode): Scope {
|
||||
for (let p = node.parent; p; p = p.parent) {
|
||||
const s = this.scopeByNode.get(p.id);
|
||||
if (s) return s;
|
||||
if (p.id === this.fnId) break;
|
||||
}
|
||||
return this.root;
|
||||
}
|
||||
|
||||
private declare(nameNode: SyntaxNode, kind: BindingEntry['kind'], scope: Scope): void {
|
||||
const name = nameNode.text;
|
||||
if (!name || scope.table.has(name)) return;
|
||||
scope.table.set(name, this.bindings.length);
|
||||
this.bindings.push({
|
||||
name,
|
||||
declLine: nameNode.startPosition.row + 1,
|
||||
declColumn: nameNode.startPosition.column,
|
||||
kind,
|
||||
});
|
||||
}
|
||||
|
||||
/** The bare identifier a declarator chain ultimately names (or undefined). */
|
||||
private declaratorName(node: SyntaxNode | null): SyntaxNode | undefined {
|
||||
let cur: SyntaxNode | null = node;
|
||||
let hops = 12;
|
||||
while (cur && hops-- > 0) {
|
||||
if (cur.type === 'identifier' || cur.type === 'field_identifier') return cur;
|
||||
// Unwrap pointer/reference/array/init/parenthesized declarator layers.
|
||||
const next =
|
||||
cur.childForFieldName('declarator') ??
|
||||
cur.namedChildren.find(
|
||||
(c) =>
|
||||
c.type === 'identifier' ||
|
||||
c.type === 'field_identifier' ||
|
||||
c.type === 'pointer_declarator' ||
|
||||
c.type === 'reference_declarator' ||
|
||||
c.type === 'array_declarator' ||
|
||||
c.type === 'parenthesized_declarator' ||
|
||||
c.type === 'init_declarator',
|
||||
);
|
||||
if (!next || next.id === cur.id) break;
|
||||
cur = next;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private declareParams(fnNode: SyntaxNode): void {
|
||||
// function_definition: declarator → function_declarator → parameter_list.
|
||||
// A C++ lambda routes through abstract_function_declarator.
|
||||
const declarator = fnNode.childForFieldName('declarator');
|
||||
const fnDeclarator = this.findFunctionDeclarator(declarator);
|
||||
const params = fnDeclarator?.childForFieldName('parameters');
|
||||
if (!params) return;
|
||||
for (let i = 0; i < params.namedChildCount; i++) {
|
||||
const p = params.namedChild(i);
|
||||
if (p?.type !== 'parameter_declaration') continue;
|
||||
const name = this.declaratorName(p.childForFieldName('declarator') ?? null);
|
||||
if (name) this.declare(name, 'param', this.root);
|
||||
}
|
||||
}
|
||||
|
||||
private findFunctionDeclarator(node: SyntaxNode | null): SyntaxNode | undefined {
|
||||
let cur: SyntaxNode | null = node;
|
||||
let hops = 10;
|
||||
while (cur && hops-- > 0) {
|
||||
if (cur.type === 'function_declarator' || cur.type === 'abstract_function_declarator') {
|
||||
return cur;
|
||||
}
|
||||
cur = cur.childForFieldName('declarator') ?? null;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private prescan(node: SyntaxNode, scope: Scope): void {
|
||||
this.nearestScopeCache.set(node.id, scope);
|
||||
const t = node.type;
|
||||
if (NESTED_FUNCTION_TYPES.has(t) && node.id !== this.fnId) {
|
||||
// A nested function/lambda body is opaque — do not descend.
|
||||
return;
|
||||
}
|
||||
|
||||
let childScope = scope;
|
||||
if (SCOPE_TYPES.has(t)) childScope = this.openScope(node);
|
||||
|
||||
switch (t) {
|
||||
case 'declaration':
|
||||
this.declareDeclarators(node, childScope);
|
||||
break;
|
||||
case 'for_range_loop': {
|
||||
// `for (int x : xs)` — the declarator binds in the loop scope.
|
||||
const decl = node.childForFieldName('declarator');
|
||||
const name = this.declaratorName(decl ?? null);
|
||||
if (name) this.declare(name, 'var', childScope);
|
||||
break;
|
||||
}
|
||||
case 'catch_clause': {
|
||||
const params = node.childForFieldName('parameters');
|
||||
if (params) {
|
||||
for (let i = 0; i < params.namedChildCount; i++) {
|
||||
const p = params.namedChild(i);
|
||||
if (p?.type !== 'parameter_declaration') continue;
|
||||
const name = this.declaratorName(p.childForFieldName('declarator') ?? null);
|
||||
if (name) this.declare(name, 'catch', childScope);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const c = node.namedChild(i);
|
||||
if (c) this.prescan(c, childScope);
|
||||
}
|
||||
}
|
||||
|
||||
private declareDeclarators(declNode: SyntaxNode, scope: Scope): void {
|
||||
for (let i = 0; i < declNode.namedChildCount; i++) {
|
||||
const d = declNode.namedChild(i);
|
||||
if (!d) continue;
|
||||
if (d.type === 'init_declarator') {
|
||||
const name = this.declaratorName(d.childForFieldName('declarator') ?? null);
|
||||
if (name) this.declare(name, 'var', scope);
|
||||
} else if (
|
||||
d.type === 'identifier' ||
|
||||
d.type === 'pointer_declarator' ||
|
||||
d.type === 'array_declarator' ||
|
||||
d.type === 'reference_declarator'
|
||||
) {
|
||||
// Uninitialized local (`int x;`) — declare the BINDING (so a later
|
||||
// assignment resolves to a real, non-synthetic binding) but the
|
||||
// declaration itself produces no def (handled in phase 2).
|
||||
const name = this.declaratorName(d);
|
||||
if (name) this.declare(name, 'var', scope);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 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 (case 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 (decl : right)` range head: decl binds, right is used. */
|
||||
forRangeHeadFacts(stmt: SyntaxNode): StatementFacts {
|
||||
const acc = new FactAccumulator(stmt.startPosition.row + 1);
|
||||
const decl = stmt.childForFieldName('declarator');
|
||||
const right = stmt.childForFieldName('right');
|
||||
const name = this.declaratorName(decl ?? null);
|
||||
if (name) this.def(name, acc);
|
||||
if (right) this.walkValue(right, acc);
|
||||
return acc.finish();
|
||||
}
|
||||
|
||||
/** ENTRY-block facts for the function's parameters (defs only). */
|
||||
paramFacts(): StatementFacts | undefined {
|
||||
const declarator = this.fnNode.childForFieldName('declarator');
|
||||
const fnDeclarator = this.findFunctionDeclarator(declarator);
|
||||
const params = fnDeclarator?.childForFieldName('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?.type !== 'parameter_declaration') continue;
|
||||
const name = this.declaratorName(p.childForFieldName('declarator') ?? null);
|
||||
if (name) this.def(name, acc);
|
||||
}
|
||||
return acc.defCount() ? acc.finish() : undefined;
|
||||
}
|
||||
|
||||
/** Def fact for a `catch (T& e)` parameter — prepend to the handler entry block. */
|
||||
catchParamFacts(catchClause: SyntaxNode): StatementFacts | undefined {
|
||||
const params = catchClause.childForFieldName('parameters');
|
||||
if (!params) return undefined;
|
||||
const acc = new FactAccumulator(catchClause.startPosition.row + 1);
|
||||
for (let i = 0; i < params.namedChildCount; i++) {
|
||||
const p = params.namedChild(i);
|
||||
if (p?.type !== 'parameter_declaration') continue;
|
||||
const name = this.declaratorName(p.childForFieldName('declarator') ?? null);
|
||||
if (name) this.def(name, acc);
|
||||
}
|
||||
return acc.defCount() ? acc.finish() : undefined;
|
||||
}
|
||||
|
||||
private resolve(nameNode: SyntaxNode): number {
|
||||
const name = nameNode.text;
|
||||
const cached = this.nearestScopeCache.get(nameNode.id);
|
||||
let startScope: Scope | null = cached ?? null;
|
||||
if (!startScope) {
|
||||
for (let p: SyntaxNode | null = nameNode; p; p = p.parent) {
|
||||
const scope = this.scopeByNode.get(p.id) ?? this.nearestScopeCache.get(p.id);
|
||||
if (scope) {
|
||||
startScope = scope;
|
||||
break;
|
||||
}
|
||||
if (p.id === this.fnId) {
|
||||
startScope = this.root;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let s: Scope | null = startScope; s; s = s.parent) {
|
||||
const idx = s.table.get(name);
|
||||
if (idx !== undefined) return idx;
|
||||
}
|
||||
let idx = this.synthetic.get(name);
|
||||
if (idx === undefined) {
|
||||
idx = this.bindings.length;
|
||||
this.synthetic.set(name, idx);
|
||||
this.bindings.push({ name, declLine: 0, declColumn: 0, kind: 'module', synthetic: true });
|
||||
}
|
||||
return idx;
|
||||
}
|
||||
|
||||
private def(nameNode: SyntaxNode, acc: FactAccumulator): void {
|
||||
if (this.conditionalDepth > 0) acc.addMayDef(this.resolve(nameNode));
|
||||
else acc.addDef(this.resolve(nameNode));
|
||||
}
|
||||
|
||||
private use(nameNode: SyntaxNode, acc: FactAccumulator): void {
|
||||
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--;
|
||||
}
|
||||
}
|
||||
|
||||
/** Strip parenthesized wrappers around an lvalue (`(x) = 1`). */
|
||||
private unwrapLvalue(node: SyntaxNode): SyntaxNode {
|
||||
let n = node;
|
||||
let hops = 8;
|
||||
while (n.type === 'parenthesized_expression' && hops-- > 0) {
|
||||
const inner = n.namedChild(0);
|
||||
if (!inner) break;
|
||||
n = inner;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/** Value-position walk: collect uses; route def positions to the lvalue handler. */
|
||||
private walkValue(node: SyntaxNode, acc: FactAccumulator): void {
|
||||
const t = node.type;
|
||||
if (TYPE_CONTEXT_TYPES.has(t)) return;
|
||||
if (NESTED_FUNCTION_TYPES.has(t) && node.id !== this.fnId) {
|
||||
// Opaque nested function / lambda — captured reads/writes are invisible.
|
||||
return;
|
||||
}
|
||||
|
||||
switch (t) {
|
||||
case 'identifier':
|
||||
case 'field_identifier':
|
||||
this.use(node, acc);
|
||||
return;
|
||||
case 'declaration':
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const d = node.namedChild(i);
|
||||
if (d?.type !== 'init_declarator') continue;
|
||||
const declarator = d.childForFieldName('declarator');
|
||||
const value = d.childForFieldName('value');
|
||||
const name = declarator ? this.declaratorName(declarator) : undefined;
|
||||
// Only an INITIALIZED declarator writes (`int x = e;`). A bare
|
||||
// `int x;` is not a def (it writes nothing at runtime), matching the
|
||||
// TS bare-`var` rule. Pointer/array/member declarators are not scalar
|
||||
// defs either — their inner identifiers stay uses.
|
||||
if (name && value && declarator?.type === 'identifier') this.def(name, acc);
|
||||
else if (declarator && declarator.type !== 'identifier') this.walkValue(declarator, acc);
|
||||
if (value) this.walkValue(value, acc);
|
||||
}
|
||||
return;
|
||||
case 'assignment_expression': {
|
||||
const left = node.childForFieldName('left');
|
||||
const right = node.childForFieldName('right');
|
||||
const op = node.childForFieldName('operator')?.text ?? '=';
|
||||
if (left) {
|
||||
const lv = this.unwrapLvalue(left);
|
||||
if (lv.type === 'identifier') {
|
||||
this.def(lv, acc);
|
||||
if (op !== '=') this.use(lv, acc); // compound assign reads too
|
||||
} else {
|
||||
this.walkValue(lv, acc); // member/pointer/subscript target — uses only
|
||||
}
|
||||
}
|
||||
if (right) this.walkValue(right, acc);
|
||||
return;
|
||||
}
|
||||
case 'update_expression': {
|
||||
const rawArg = node.childForFieldName('argument');
|
||||
const arg = rawArg ? this.unwrapLvalue(rawArg) : null;
|
||||
if (arg?.type === 'identifier') {
|
||||
this.def(arg, acc);
|
||||
this.use(arg, acc);
|
||||
} else if (arg) {
|
||||
this.walkValue(arg, 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 'conditional_expression': {
|
||||
const cond = node.childForFieldName('condition');
|
||||
const cons = node.childForFieldName('consequence');
|
||||
const alt = node.childForFieldName('alternative');
|
||||
if (cond) this.walkValue(cond, acc);
|
||||
if (cons) this.conditional(() => this.walkValue(cons, acc));
|
||||
if (alt) this.conditional(() => this.walkValue(alt, acc));
|
||||
return;
|
||||
}
|
||||
case 'field_expression': {
|
||||
// `a.b` / `a->b` — value read of the chain root only; the field name is
|
||||
// not a scalar binding. Mirrors the TS member-read use semantics.
|
||||
const arg = node.childForFieldName('argument');
|
||||
if (arg) this.walkValue(arg, acc);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const c = node.namedChild(i);
|
||||
if (c && !TYPE_CONTEXT_TYPES.has(c.type)) this.walkValue(c, acc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Ordered, deduplicating def/use collector for one statement record. */
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
finish(): StatementFacts {
|
||||
return {
|
||||
line: this.line,
|
||||
defs: this.defs,
|
||||
uses: this.uses,
|
||||
...(this.mayDefs.length > 0 ? { mayDefs: this.mayDefs } : {}),
|
||||
};
|
||||
}
|
||||
}
|
||||
773
gitnexus/src/core/ingestion/cfg/visitors/c-cpp.ts
Normal file
773
gitnexus/src/core/ingestion/cfg/visitors/c-cpp.ts
Normal file
|
|
@ -0,0 +1,773 @@
|
|||
/**
|
||||
* C / C++ CfgVisitor (#2195 U2, plan KTD1).
|
||||
*
|
||||
* Walks a C or C++ function's tree-sitter AST and drives the language-agnostic
|
||||
* {@link CfgBuilder} to produce a serializable {@link FunctionCfg}, plus a
|
||||
* def/use harvest ({@link CCppHarvester}) for the reaching-defs / CDG solvers.
|
||||
*
|
||||
* SHARED CORE, TWO FACTORIES. A grammar-introspection probe (mandatory pre-step,
|
||||
* KTD1) confirmed every control-flow node type and field this visitor uses is
|
||||
* IDENTICAL between tree-sitter-c and tree-sitter-cpp — `if_statement`,
|
||||
* `for_statement`, `while_statement`, `do_statement`, `switch_statement` /
|
||||
* `case_statement`, `return`/`break`/`continue`/`goto_statement` /
|
||||
* `labeled_statement`, `compound_statement`, and the `condition`/`consequence`/
|
||||
* `alternative`/`initializer`/`update`/`body`/`label`/`value` fields. So the C
|
||||
* walk ({@link CCfgWalk}) is grammar-shared, and C++ EXTENDS it ({@link
|
||||
* CppCfgWalk}) with exception flow (`try_statement` / `catch_clause` /
|
||||
* `throw_statement`) and `for_range_loop` — node types that simply never occur
|
||||
* in a C parse, so there is no `if (lang === …)` branching (AGENTS.md
|
||||
* no-language-naming rule). The two factories differ only in which walk class
|
||||
* and function-node set they install.
|
||||
*
|
||||
* Edge-kind contract (matches the TS visitor — RD/CDG consume these):
|
||||
* - if/else → `cond-true` / `cond-false`
|
||||
* - loops (for / while / do-while / for-range) → `cond-true` / `loop-back` /
|
||||
* `cond-false`
|
||||
* - switch → `switch-case` / `fallthrough` (C-style: a case body that does not
|
||||
* `break` falls into the next case)
|
||||
* - try/catch (C++) → `throw` (every protected-region block → the handler)
|
||||
* - return / throw / break / continue → the matching terminator kind
|
||||
* - straight-line → `seq`
|
||||
*
|
||||
* Classic hazards, handled explicitly:
|
||||
* - loops allocate a dedicated loop-exit block so `break` has a target before
|
||||
* the loop's successor is known; `continue` targets the header/increment.
|
||||
* - `for (;;) {}` / `while (1) {}` still emit the structural `header → loopExit`
|
||||
* `cond-false` escape edge so EXIT stays reverse-reachable from every block —
|
||||
* the post-dominator / CDG pass is unsound otherwise (it silently emits zero
|
||||
* CDG for the function).
|
||||
* - C `goto`/`labeled_statement`: labels resolve within the function (forward
|
||||
* AND backward); an unresolved `goto` (label not in this function) routes to
|
||||
* EXIT and logs via the builder warn path, preserving single-exit.
|
||||
* - C++ `try`/`catch`: conservative exceptional flow — EVERY block in the
|
||||
* protected region edges to the handler (an exception may fire mid-block),
|
||||
* matching the TS `visitTry` over-approximation. A `throw` with no enclosing
|
||||
* try routes to EXIT.
|
||||
*
|
||||
* Known limitations:
|
||||
* - C++ RAII: a destructor runs implicitly at scope exit, but tree-sitter does
|
||||
* NOT represent that call in the AST. This visitor therefore does NOT model
|
||||
* destructor-at-scope-exit control/data flow — it is a documented gap, not
|
||||
* faked. (C++ has no `finally`; `try`/`catch` is the only modeled exception
|
||||
* construct, so the TS finalizer-frame machinery is unused here.)
|
||||
* - A `goto` whose label is undefined in the function keeps the conservative
|
||||
* route-to-EXIT fallback (single-exit preserved; the continuation path is
|
||||
* approximate). `setjmp`/`longjmp` non-local control is not modeled.
|
||||
* - Computed `goto` (`goto *ptr;`, a GNU extension) has no static label target
|
||||
* and routes to EXIT like an unresolved label.
|
||||
* - Def/use harvest scope: see `c-cpp-harvest.ts` — member/pointer/array writes
|
||||
* are not scalar defs; C++ lambda bodies are opaque in both directions.
|
||||
*
|
||||
* 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 type { TraversalResult } from '../traversal-result.js';
|
||||
import type { CfgVisitor, FunctionCfg } from '../types.js';
|
||||
import { CCppHarvester } from './c-cpp-harvest.js';
|
||||
|
||||
/** C function node — only `function_definition` owns a CFG-bearing body. */
|
||||
const C_FUNCTION_TYPES = new Set(['function_definition']);
|
||||
/** C++ adds the lambda as a CFG-bearing function. */
|
||||
const CPP_FUNCTION_TYPES = new Set(['function_definition', 'lambda_expression']);
|
||||
|
||||
/** Statement node types that break a basic block (everything else coalesces). */
|
||||
const C_CONTROL_FLOW_TYPES = new Set([
|
||||
'if_statement',
|
||||
'while_statement',
|
||||
'do_statement',
|
||||
'for_statement',
|
||||
'switch_statement',
|
||||
'return_statement',
|
||||
'break_statement',
|
||||
'continue_statement',
|
||||
'goto_statement',
|
||||
'labeled_statement',
|
||||
'compound_statement',
|
||||
]);
|
||||
|
||||
/** C++ control-flow node types (C set ∪ exceptions ∪ range-for). */
|
||||
const CPP_CONTROL_FLOW_TYPES = new Set([
|
||||
...C_CONTROL_FLOW_TYPES,
|
||||
'for_range_loop',
|
||||
'try_statement',
|
||||
'throw_statement',
|
||||
]);
|
||||
|
||||
const LOOP_OR_SWITCH_TYPES = new Set([
|
||||
'while_statement',
|
||||
'do_statement',
|
||||
'for_statement',
|
||||
'for_range_loop',
|
||||
'switch_statement',
|
||||
]);
|
||||
|
||||
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;
|
||||
|
||||
/** A `break`/`continue` jump-target frame (loop or switch). */
|
||||
interface JumpFrame {
|
||||
readonly kind: 'loop' | 'switch';
|
||||
/** Where a `break` jumps to (loop exit / switch exit). */
|
||||
readonly breakTo: number;
|
||||
/** Where a `continue` jumps to (loop header / increment). -1 for a switch. */
|
||||
readonly continueTo: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-function C walk state. One instance per function so the jump-target stack,
|
||||
* exception-handler stack, and label tables are scoped to that function.
|
||||
*
|
||||
* Designed to be EXTENDED by the C++ walk: the dispatch table is open via the
|
||||
* protected {@link visitStmt} override hook, so C++ adds its node types without
|
||||
* any language conditional in the C core.
|
||||
*/
|
||||
class CCfgWalk {
|
||||
protected readonly jumps: JumpFrame[] = [];
|
||||
/** Stack of exception-handler entry blocks (catch) a `throw` jumps to. */
|
||||
protected readonly handlers: number[] = [];
|
||||
/** label name → its `labeled_statement` body's entry block (resolved on demand). */
|
||||
protected readonly labelBlocks = new Map<string, number>();
|
||||
/** Pending gotos to a label not yet seen: label → list of source blocks. */
|
||||
protected readonly pendingGotos = new Map<string, number[]>();
|
||||
/** Set of node types that break a block, used by {@link visitSeq}. */
|
||||
protected readonly controlFlowTypes: ReadonlySet<string>;
|
||||
|
||||
constructor(
|
||||
protected readonly builder: CfgBuilder,
|
||||
protected readonly harvest: CCppHarvester,
|
||||
controlFlowTypes: ReadonlySet<string>,
|
||||
) {
|
||||
this.controlFlowTypes = controlFlowTypes;
|
||||
}
|
||||
|
||||
/** Statements of a block node, ignoring comments. */
|
||||
protected statementsOf(block: SyntaxNode): SyntaxNode[] {
|
||||
return block.namedChildren.filter((c) => c.type !== 'comment');
|
||||
}
|
||||
|
||||
/** The `body` block of a node (field, or the first compound_statement child). */
|
||||
protected bodyBlockOf(node: SyntaxNode): SyntaxNode | undefined {
|
||||
return (
|
||||
node.childForFieldName('body') ??
|
||||
node.namedChildren.find((c) => c.type === 'compound_statement')
|
||||
);
|
||||
}
|
||||
|
||||
/** Visit a body that may be a `compound_statement` or a single statement. */
|
||||
protected visitBody(node: SyntaxNode | undefined | null): SeqResult {
|
||||
if (!node) return null;
|
||||
if (node.type === 'compound_statement') 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.controlFlowTypes.has(stmt.type)) {
|
||||
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 {
|
||||
if (openSimple === undefined) {
|
||||
const idx = this.builder.newBlock(
|
||||
startLineOf(stmt),
|
||||
endLineOf(stmt),
|
||||
stmt.text,
|
||||
'normal',
|
||||
this.harvest.facts(stmt),
|
||||
);
|
||||
if (entry === undefined) entry = idx;
|
||||
else this.builder.connect(dangling, idx, 'seq');
|
||||
openSimple = idx;
|
||||
dangling = [idx];
|
||||
} else {
|
||||
this.builder.extendBlock(openSimple, endLineOf(stmt), stmt.text, this.harvest.facts(stmt));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (entry === undefined) return null;
|
||||
return { entry, exits: dangling };
|
||||
}
|
||||
|
||||
/** Dispatch one statement to its handler. Non-null except for empty blocks. */
|
||||
visitStmt(stmt: SyntaxNode): SeqResult {
|
||||
switch (stmt.type) {
|
||||
case 'if_statement':
|
||||
return this.visitIf(stmt);
|
||||
case 'while_statement':
|
||||
return this.visitWhile(stmt);
|
||||
case 'do_statement':
|
||||
return this.visitDoWhile(stmt);
|
||||
case 'for_statement':
|
||||
return this.visitFor(stmt);
|
||||
case 'switch_statement':
|
||||
return this.visitSwitch(stmt);
|
||||
case 'return_statement':
|
||||
return this.visitReturn(stmt);
|
||||
case 'break_statement':
|
||||
return this.visitBreak(stmt);
|
||||
case 'continue_statement':
|
||||
return this.visitContinue(stmt);
|
||||
case 'goto_statement':
|
||||
return this.visitGoto(stmt);
|
||||
case 'labeled_statement':
|
||||
return this.visitLabeled(stmt);
|
||||
case 'compound_statement':
|
||||
return this.visitSeq(this.statementsOf(stmt));
|
||||
default:
|
||||
return this.visitExtra(stmt) ?? this.visitSimple(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension hook for node types the C core does not handle (C++ try/catch/
|
||||
* throw/for-range). Returns `undefined` to fall through to {@link visitSimple}.
|
||||
* The C core has none, so this is a no-op here.
|
||||
*/
|
||||
protected visitExtra(_stmt: SyntaxNode): SeqResult | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected visitSimple(stmt: SyntaxNode): TraversalResult {
|
||||
const idx = this.builder.newBlock(
|
||||
startLineOf(stmt),
|
||||
endLineOf(stmt),
|
||||
stmt.text,
|
||||
'normal',
|
||||
this.harvest.facts(stmt),
|
||||
);
|
||||
return { entry: idx, exits: [idx] };
|
||||
}
|
||||
|
||||
protected visitReturn(stmt: SyntaxNode): TraversalResult {
|
||||
const idx = this.builder.newBlock(
|
||||
startLineOf(stmt),
|
||||
endLineOf(stmt),
|
||||
stmt.text,
|
||||
'normal',
|
||||
this.harvest.facts(stmt),
|
||||
);
|
||||
this.builder.edge(idx, this.builder.exitIndex, 'return');
|
||||
return { entry: idx, exits: [] };
|
||||
}
|
||||
|
||||
protected visitBreak(stmt: SyntaxNode): TraversalResult {
|
||||
const idx = this.builder.newBlock(startLineOf(stmt), endLineOf(stmt), stmt.text);
|
||||
const target = this.nearestBreakTarget();
|
||||
this.builder.edge(idx, target ?? this.builder.exitIndex, 'break');
|
||||
return { entry: idx, exits: [] };
|
||||
}
|
||||
|
||||
protected visitContinue(stmt: SyntaxNode): TraversalResult {
|
||||
const idx = this.builder.newBlock(startLineOf(stmt), endLineOf(stmt), stmt.text);
|
||||
const target = this.nearestContinueTarget();
|
||||
this.builder.edge(idx, target ?? this.builder.exitIndex, 'continue');
|
||||
return { entry: idx, exits: [] };
|
||||
}
|
||||
|
||||
/** Nearest enclosing loop/switch `break` target, or undefined (→ EXIT). */
|
||||
private nearestBreakTarget(): number | undefined {
|
||||
return this.jumps.length ? this.jumps[this.jumps.length - 1].breakTo : undefined;
|
||||
}
|
||||
|
||||
/** Nearest enclosing loop `continue` target (switches don't catch continue). */
|
||||
private nearestContinueTarget(): number | undefined {
|
||||
for (let i = this.jumps.length - 1; i >= 0; i--) {
|
||||
if (this.jumps[i].kind === 'loop') return this.jumps[i].continueTo;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** `goto label;` — route to the label block if known, else defer / EXIT. */
|
||||
protected visitGoto(stmt: SyntaxNode): TraversalResult {
|
||||
const idx = this.builder.newBlock(startLineOf(stmt), endLineOf(stmt), stmt.text);
|
||||
const label = this.labelOf(stmt);
|
||||
if (label === undefined) {
|
||||
// Computed goto (`goto *p;`) or malformed — route to EXIT (single-exit).
|
||||
this.builder.edge(idx, this.builder.exitIndex, 'seq');
|
||||
return { entry: idx, exits: [] };
|
||||
}
|
||||
const target = this.labelBlocks.get(label);
|
||||
if (target !== undefined) {
|
||||
this.builder.edge(idx, target, 'seq'); // backward goto: label already built
|
||||
} else {
|
||||
// Forward goto — wire once the label is created (or to EXIT at finish()).
|
||||
const list = this.pendingGotos.get(label);
|
||||
if (list) list.push(idx);
|
||||
else this.pendingGotos.set(label, [idx]);
|
||||
}
|
||||
return { entry: idx, exits: [] };
|
||||
}
|
||||
|
||||
protected visitLabeled(stmt: SyntaxNode): SeqResult {
|
||||
const label = this.labelOf(stmt);
|
||||
// The labeled statement's body is the trailing named child (no `body` field
|
||||
// on labeled_statement in C/C++).
|
||||
const body =
|
||||
stmt.namedChildren.find((c) => c.type !== 'statement_identifier' && c.type !== 'comment') ??
|
||||
null;
|
||||
const res = this.visitBody(body);
|
||||
if (label !== undefined) {
|
||||
const entry = res?.entry ?? this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), '');
|
||||
this.labelBlocks.set(label, entry);
|
||||
// Resolve any forward gotos that were waiting on this label.
|
||||
const pending = this.pendingGotos.get(label);
|
||||
if (pending) {
|
||||
for (const from of pending) this.builder.edge(from, entry, 'seq');
|
||||
this.pendingGotos.delete(label);
|
||||
}
|
||||
if (!res) return { entry, exits: [entry] };
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
protected visitIf(stmt: SyntaxNode): TraversalResult {
|
||||
const cond = stmt.childForFieldName('condition') ?? stmt;
|
||||
const condBlock = this.builder.newBlock(
|
||||
startLineOf(stmt),
|
||||
endLineOf(cond),
|
||||
cond.text,
|
||||
'normal',
|
||||
this.harvest.facts(cond),
|
||||
);
|
||||
const exits: number[] = [];
|
||||
|
||||
const thenRes = this.visitBody(stmt.childForFieldName('consequence'));
|
||||
if (thenRes) {
|
||||
this.builder.edge(condBlock, thenRes.entry, 'cond-true');
|
||||
exits.push(...thenRes.exits);
|
||||
} else {
|
||||
exits.push(condBlock); // empty then — true path falls through
|
||||
}
|
||||
|
||||
const elseNode = this.elseBodyOf(stmt);
|
||||
if (elseNode) {
|
||||
const elseRes = this.visitBody(elseNode);
|
||||
if (elseRes) {
|
||||
this.builder.edge(condBlock, elseRes.entry, 'cond-false');
|
||||
exits.push(...elseRes.exits);
|
||||
} else {
|
||||
exits.push(condBlock);
|
||||
}
|
||||
} else {
|
||||
exits.push(condBlock); // no else — false path falls through to the join
|
||||
}
|
||||
|
||||
return { entry: condBlock, exits: [...new Set(exits)] };
|
||||
}
|
||||
|
||||
/** The else body node (unwraps an `else_clause` wrapper if present). */
|
||||
private elseBodyOf(ifStmt: SyntaxNode): SyntaxNode | undefined {
|
||||
const alt = ifStmt.childForFieldName('alternative');
|
||||
if (!alt) return undefined;
|
||||
if (alt.type === 'else_clause') {
|
||||
return alt.childForFieldName('body') ?? alt.namedChildren[0];
|
||||
}
|
||||
return alt; // an `else if` is the nested if_statement directly
|
||||
}
|
||||
|
||||
protected visitWhile(stmt: SyntaxNode): TraversalResult {
|
||||
const cond = stmt.childForFieldName('condition') ?? stmt;
|
||||
const header = this.builder.newBlock(
|
||||
startLineOf(stmt),
|
||||
endLineOf(cond),
|
||||
cond.text,
|
||||
'normal',
|
||||
this.harvest.facts(cond),
|
||||
);
|
||||
const loopExit = this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), '');
|
||||
|
||||
this.jumps.push({ kind: 'loop', breakTo: loopExit, continueTo: header });
|
||||
const body = this.visitBody(this.bodyBlockOf(stmt));
|
||||
this.jumps.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
|
||||
}
|
||||
// Always emit the structural exit edge — even `while (1)` keeps EXIT
|
||||
// reverse-reachable for the post-dominator / CDG pass.
|
||||
this.builder.edge(header, loopExit, 'cond-false');
|
||||
return { entry: header, exits: [loopExit] };
|
||||
}
|
||||
|
||||
protected visitDoWhile(stmt: SyntaxNode): TraversalResult {
|
||||
const cond = stmt.childForFieldName('condition') ?? stmt;
|
||||
const condBlock = this.builder.newBlock(
|
||||
startLineOf(cond),
|
||||
endLineOf(cond),
|
||||
cond.text,
|
||||
'normal',
|
||||
this.harvest.facts(cond),
|
||||
);
|
||||
const loopExit = this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), '');
|
||||
|
||||
this.jumps.push({ kind: 'loop', breakTo: loopExit, continueTo: condBlock });
|
||||
const body = this.visitBody(this.bodyBlockOf(stmt));
|
||||
this.jumps.pop();
|
||||
|
||||
const backTarget = body ? body.entry : condBlock;
|
||||
if (body) this.builder.connect(body.exits, condBlock, 'seq');
|
||||
this.builder.edge(condBlock, backTarget, 'loop-back'); // cond true → run body again
|
||||
this.builder.edge(condBlock, loopExit, 'cond-false');
|
||||
return { entry: backTarget, exits: [loopExit] };
|
||||
}
|
||||
|
||||
protected visitFor(stmt: SyntaxNode): TraversalResult {
|
||||
const init = stmt.childForFieldName('initializer');
|
||||
const cond = stmt.childForFieldName('condition');
|
||||
const incr = stmt.childForFieldName('update');
|
||||
|
||||
const header = this.builder.newBlock(
|
||||
startLineOf(stmt),
|
||||
cond ? endLineOf(cond) : startLineOf(stmt),
|
||||
cond ? cond.text : 'for(;;)',
|
||||
'normal',
|
||||
cond ? this.harvest.facts(cond) : undefined,
|
||||
);
|
||||
const loopExit = this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), '');
|
||||
|
||||
let incrBlock = header;
|
||||
if (incr) {
|
||||
incrBlock = this.builder.newBlock(
|
||||
startLineOf(incr),
|
||||
endLineOf(incr),
|
||||
incr.text,
|
||||
'normal',
|
||||
this.harvest.facts(incr),
|
||||
);
|
||||
this.builder.edge(incrBlock, header, 'loop-back');
|
||||
}
|
||||
|
||||
this.jumps.push({ kind: 'loop', breakTo: loopExit, continueTo: incrBlock });
|
||||
const body = this.visitBody(this.bodyBlockOf(stmt));
|
||||
this.jumps.pop();
|
||||
|
||||
if (body) {
|
||||
this.builder.edge(header, body.entry, 'cond-true');
|
||||
this.builder.connect(body.exits, incrBlock, incr ? 'seq' : 'loop-back');
|
||||
} else {
|
||||
this.builder.edge(header, incrBlock, 'cond-true');
|
||||
if (!incr) this.builder.edge(header, header, 'loop-back');
|
||||
}
|
||||
// Structural exit edge — `for (;;) {}` (no condition) still keeps EXIT
|
||||
// reverse-reachable so CDG is not silently skipped for the function.
|
||||
this.builder.edge(header, loopExit, 'cond-false');
|
||||
|
||||
let entry = header;
|
||||
if (init) {
|
||||
const initBlock = this.builder.newBlock(
|
||||
startLineOf(init),
|
||||
endLineOf(init),
|
||||
init.text,
|
||||
'normal',
|
||||
this.harvest.facts(init),
|
||||
);
|
||||
this.builder.edge(initBlock, header, 'seq');
|
||||
entry = initBlock;
|
||||
}
|
||||
return { entry, exits: [loopExit] };
|
||||
}
|
||||
|
||||
protected visitSwitch(stmt: SyntaxNode): TraversalResult {
|
||||
const value = stmt.childForFieldName('condition') ?? stmt;
|
||||
const dispatch = this.builder.newBlock(
|
||||
startLineOf(stmt),
|
||||
endLineOf(value),
|
||||
value.text,
|
||||
'normal',
|
||||
this.harvest.facts(value),
|
||||
);
|
||||
const switchExit = this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), '');
|
||||
|
||||
this.jumps.push({ kind: 'switch', breakTo: switchExit, continueTo: -1 });
|
||||
const body = stmt.childForFieldName('body');
|
||||
// C/C++ uses ONE `case_statement` node for both `case X:` and `default:`;
|
||||
// a default has no `value` field.
|
||||
const cases = body ? body.namedChildren.filter((c) => c.type === 'case_statement') : [];
|
||||
|
||||
// `case X:` test expressions live in no block — harvest their uses onto the
|
||||
// dispatch block as may-defs/uses (sound over-approx of in-order evaluation).
|
||||
for (const c of cases) {
|
||||
const caseValue = c.childForFieldName('value');
|
||||
if (caseValue) this.builder.attachFacts(dispatch, this.harvest.factsConditional(caseValue));
|
||||
}
|
||||
|
||||
const caseResults = cases.map((c) => this.visitSeq(this.caseStatements(c)));
|
||||
const hasDefault = cases.some((c) => !c.childForFieldName('value'));
|
||||
|
||||
const entryOf: number[] = new Array(cases.length);
|
||||
let after = switchExit;
|
||||
for (let i = cases.length - 1; i >= 0; i--) {
|
||||
entryOf[i] = caseResults[i]?.entry ?? after;
|
||||
after = entryOf[i];
|
||||
}
|
||||
|
||||
for (let i = 0; i < cases.length; i++) {
|
||||
this.builder.edge(dispatch, entryOf[i], 'switch-case');
|
||||
}
|
||||
if (!hasDefault) this.builder.edge(dispatch, switchExit, 'switch-case'); // no-match path
|
||||
|
||||
for (let i = 0; i < cases.length; i++) {
|
||||
const res = caseResults[i];
|
||||
if (!res) continue;
|
||||
const fallTarget = i + 1 < cases.length ? entryOf[i + 1] : switchExit;
|
||||
this.builder.connect(res.exits, fallTarget, 'fallthrough');
|
||||
}
|
||||
|
||||
this.jumps.pop();
|
||||
return { entry: dispatch, exits: [switchExit] };
|
||||
}
|
||||
|
||||
/** A case's body statements (everything but the `value` test and comments). */
|
||||
private caseStatements(caseNode: SyntaxNode): SyntaxNode[] {
|
||||
const value = caseNode.childForFieldName('value');
|
||||
return caseNode.namedChildren.filter((c) => c.id !== value?.id && c.type !== 'comment');
|
||||
}
|
||||
|
||||
/** Nearest enclosing exception handler, or the function EXIT. */
|
||||
protected currentHandler(): number {
|
||||
return this.handlers.length ? this.handlers[this.handlers.length - 1] : this.builder.exitIndex;
|
||||
}
|
||||
|
||||
private labelOf(stmt: SyntaxNode): string | undefined {
|
||||
const id =
|
||||
stmt.childForFieldName('label') ??
|
||||
stmt.namedChildren.find((c) => c.type === 'statement_identifier');
|
||||
return id?.text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drain any forward gotos whose label never appeared in the function (a label
|
||||
* defined in a header macro, or malformed source) — route them to EXIT so the
|
||||
* graph stays single-exit. Logs via console.warn (the builder's warn path)
|
||||
* so a dropped jump is never silent (R4). Called once after the body walk.
|
||||
*/
|
||||
finishGotos(): void {
|
||||
for (const [label, froms] of this.pendingGotos) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`[cfg] unresolved goto label "${label}" routed to EXIT (${froms.length} site(s))`);
|
||||
for (const from of froms) this.builder.edge(from, this.builder.exitIndex, 'seq');
|
||||
}
|
||||
this.pendingGotos.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* C++ walk — extends the C core with exception flow and the range-for loop.
|
||||
* These node types never appear in a C parse, so no language conditional is
|
||||
* needed; the C core dispatches them through {@link visitExtra}.
|
||||
*/
|
||||
class CppCfgWalk extends CCfgWalk {
|
||||
protected override visitExtra(stmt: SyntaxNode): SeqResult | undefined {
|
||||
switch (stmt.type) {
|
||||
case 'for_range_loop':
|
||||
return this.visitForRange(stmt);
|
||||
case 'try_statement':
|
||||
return this.visitTry(stmt);
|
||||
case 'throw_statement':
|
||||
return this.visitThrow(stmt);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private visitThrow(stmt: SyntaxNode): TraversalResult {
|
||||
const idx = this.builder.newBlock(
|
||||
startLineOf(stmt),
|
||||
endLineOf(stmt),
|
||||
stmt.text,
|
||||
'normal',
|
||||
this.harvest.facts(stmt),
|
||||
);
|
||||
this.builder.edge(idx, this.currentHandler(), 'throw');
|
||||
return { entry: idx, exits: [] };
|
||||
}
|
||||
|
||||
private visitForRange(stmt: SyntaxNode): TraversalResult {
|
||||
// Header text is synthesized; facts come from the declarator (def) + the
|
||||
// iterated expression (use) directly.
|
||||
const header = this.builder.newBlock(
|
||||
startLineOf(stmt),
|
||||
startLineOf(stmt),
|
||||
this.forRangeHeaderText(stmt),
|
||||
'normal',
|
||||
this.harvest.forRangeHeadFacts(stmt),
|
||||
);
|
||||
const loopExit = this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), '');
|
||||
|
||||
this.jumps.push({ kind: 'loop', breakTo: loopExit, continueTo: header });
|
||||
const body = this.visitBody(this.bodyBlockOf(stmt));
|
||||
this.jumps.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');
|
||||
}
|
||||
this.builder.edge(header, loopExit, 'cond-false');
|
||||
return { entry: header, exits: [loopExit] };
|
||||
}
|
||||
|
||||
private forRangeHeaderText(stmt: SyntaxNode): string {
|
||||
const decl = stmt.childForFieldName('declarator')?.text ?? '';
|
||||
const right = stmt.childForFieldName('right')?.text ?? '';
|
||||
return decl || right ? `for(${decl} : ${right})` : 'for(… : …)';
|
||||
}
|
||||
|
||||
/**
|
||||
* try / catch (C++ has no `finally`). Conservative exceptional flow: every
|
||||
* block created while walking the protected body edges to the (first) catch
|
||||
* handler — an exception may fire mid-block, and a branched body must still
|
||||
* reach the handler from any interior block (matching the TS visitTry
|
||||
* over-approximation). Multiple `catch` clauses chain: the body's throw routes
|
||||
* to the first handler; each handler's normal completion joins the post-try
|
||||
* continuation.
|
||||
*/
|
||||
private visitTry(stmt: SyntaxNode): SeqResult {
|
||||
const bodyNode = stmt.childForFieldName('body');
|
||||
const catchClauses: SyntaxNode[] = [];
|
||||
for (let i = 0; i < stmt.namedChildCount; i++) {
|
||||
const c = stmt.namedChild(i);
|
||||
if (c?.type === 'catch_clause') catchClauses.push(c);
|
||||
}
|
||||
|
||||
// Build each catch handler. The handler entry is the catch-param binding
|
||||
// block (a facts-only block) in front of the body, so the exception's
|
||||
// binding happens exactly once on handler entry.
|
||||
const handlerEntries: number[] = [];
|
||||
const handlerExits: number[] = [];
|
||||
for (const clause of catchClauses) {
|
||||
const clauseBody = this.bodyBlockOf(clause);
|
||||
let res: SeqResult = clauseBody ? this.visitSeq(this.statementsOf(clauseBody)) : null;
|
||||
if (res === null) {
|
||||
// Empty catch body still CATCHES — synthesize one block so the
|
||||
// exception lands somewhere and the post-try code stays reachable.
|
||||
const idx = this.builder.newBlock(startLineOf(clause), endLineOf(clause), '');
|
||||
res = { entry: idx, exits: [idx] };
|
||||
}
|
||||
const paramFacts = this.harvest.catchParamFacts(clause);
|
||||
if (paramFacts) {
|
||||
const paramBlock = this.builder.newBlock(
|
||||
startLineOf(clause),
|
||||
startLineOf(clause),
|
||||
'',
|
||||
'normal',
|
||||
paramFacts,
|
||||
);
|
||||
this.builder.edge(paramBlock, res.entry, 'seq');
|
||||
res = { entry: paramBlock, exits: res.exits };
|
||||
}
|
||||
handlerEntries.push(res.entry);
|
||||
handlerExits.push(...res.exits);
|
||||
}
|
||||
|
||||
// The protected body's handler is the FIRST catch (if any), else the outer.
|
||||
const tryHandler = handlerEntries[0] ?? this.currentHandler();
|
||||
const protectedStart = this.builder.blockCount;
|
||||
this.handlers.push(tryHandler);
|
||||
const bodyRes = bodyNode ? this.visitSeq(this.statementsOf(bodyNode)) : null;
|
||||
this.handlers.pop();
|
||||
|
||||
// Conservative exceptional edges: every protected-region block → the handler.
|
||||
if (catchClauses.length > 0) {
|
||||
for (let b = protectedStart; b < this.builder.blockCount; b++) {
|
||||
this.builder.edge(b, tryHandler, 'throw');
|
||||
}
|
||||
}
|
||||
|
||||
const exits: number[] = [];
|
||||
if (bodyRes) exits.push(...bodyRes.exits);
|
||||
exits.push(...handlerExits);
|
||||
// No catch clause at all — an exception re-propagates to the outer handler.
|
||||
if (catchClauses.length === 0 && bodyRes) {
|
||||
// (A bare `try {}` with no catch is ill-formed C++, but stay robust.)
|
||||
this.builder.connect(bodyRes.exits, this.currentHandler(), 'throw');
|
||||
}
|
||||
|
||||
const entry = bodyRes?.entry ?? handlerEntries[0];
|
||||
if (entry === undefined) return null;
|
||||
return { entry, exits: [...new Set(exits)] };
|
||||
}
|
||||
}
|
||||
|
||||
/** Build the CFG for one C/C++ function node, or `undefined` if not modelable. */
|
||||
function buildFunctionCfg(
|
||||
fnNode: SyntaxNode,
|
||||
filePath: string,
|
||||
functionTypes: ReadonlySet<string>,
|
||||
controlFlowTypes: ReadonlySet<string>,
|
||||
WalkClass: typeof CCfgWalk,
|
||||
): FunctionCfg | undefined {
|
||||
try {
|
||||
if (!functionTypes.has(fnNode.type)) return undefined;
|
||||
const startLine = startLineOf(fnNode);
|
||||
const endLine = endLineOf(fnNode);
|
||||
const startColumn = fnNode.startPosition.column;
|
||||
|
||||
// The body is a compound_statement (field `body`, or first such child).
|
||||
const body =
|
||||
fnNode.childForFieldName('body') ??
|
||||
fnNode.namedChildren.find((c) => c.type === 'compound_statement');
|
||||
if (!body || body.type !== 'compound_statement') return undefined; // declaration / no body
|
||||
|
||||
const builder = new CfgBuilder(filePath, startLine, endLine, startColumn);
|
||||
const harvest = new CCppHarvester(fnNode);
|
||||
|
||||
const paramFacts = harvest.paramFacts();
|
||||
if (paramFacts) builder.attachFacts(builder.entryIndex, paramFacts);
|
||||
|
||||
const walk = new WalkClass(builder, harvest, controlFlowTypes);
|
||||
const res = walk.visitSeq(body.namedChildren.filter((c) => c.type !== 'comment'));
|
||||
walk.finishGotos();
|
||||
if (!res) {
|
||||
builder.edge(builder.entryIndex, builder.exitIndex, 'seq'); // empty body
|
||||
return builder.finish(harvest.table());
|
||||
}
|
||||
builder.edge(builder.entryIndex, res.entry, 'seq');
|
||||
builder.connect(res.exits, builder.exitIndex, 'seq'); // normal fall-off → EXIT
|
||||
return builder.finish(harvest.table());
|
||||
} 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] C/C++ buildFunctionCfg skipped a function in ${filePath}: ${String(err)}`);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** The C CFG visitor. */
|
||||
export function createCCfgVisitor(): CfgVisitor<SyntaxNode> {
|
||||
return {
|
||||
isFunction: (node) => C_FUNCTION_TYPES.has(node.type),
|
||||
buildFunctionCfg: (fnNode, filePath) =>
|
||||
buildFunctionCfg(fnNode, filePath, C_FUNCTION_TYPES, C_CONTROL_FLOW_TYPES, CCfgWalk),
|
||||
};
|
||||
}
|
||||
|
||||
/** The C++ CFG visitor (C core + exceptions + range-for + lambdas). */
|
||||
export function createCppCfgVisitor(): CfgVisitor<SyntaxNode> {
|
||||
return {
|
||||
isFunction: (node) => CPP_FUNCTION_TYPES.has(node.type),
|
||||
buildFunctionCfg: (fnNode, filePath) =>
|
||||
buildFunctionCfg(fnNode, filePath, CPP_FUNCTION_TYPES, CPP_CONTROL_FLOW_TYPES, CppCfgWalk),
|
||||
};
|
||||
}
|
||||
|
||||
export { C_FUNCTION_TYPES, CPP_FUNCTION_TYPES };
|
||||
|
|
@ -70,6 +70,7 @@ import {
|
|||
type CppConstraintPayload,
|
||||
} from './cpp/constraint-extractor.js';
|
||||
import { assertCloneable } from '../workers/clone-safety.js';
|
||||
import { createCCfgVisitor, createCppCfgVisitor } from '../cfg/visitors/c-cpp.js';
|
||||
|
||||
const C_BUILT_INS: ReadonlySet<string> = new Set([
|
||||
'printf',
|
||||
|
|
@ -401,6 +402,7 @@ export const cProvider = defineLanguage({
|
|||
|
||||
// ── RFC #909 Ring 3: scope-based resolution hooks (RFC §5) ──────────
|
||||
emitScopeCaptures: emitCScopeCaptures,
|
||||
cfgVisitor: createCCfgVisitor(),
|
||||
// Worker-side: snapshot the module-level `static`-linkage marks
|
||||
// `emitCScopeCaptures` just populated for this file (`markStaticName` →
|
||||
// `staticNames`) into plain data on `ParsedFile.captureSideChannel`, so the
|
||||
|
|
@ -484,6 +486,7 @@ export const cppProvider = defineLanguage({
|
|||
|
||||
// ── RFC #909 Ring 3: scope-based resolution hooks (RFC §5) ──────────
|
||||
emitScopeCaptures: emitCppScopeCaptures,
|
||||
cfgVisitor: createCppCfgVisitor(),
|
||||
// Worker-side: snapshot the module-level capture marks `emitCppScopeCaptures`
|
||||
// just populated for this file into plain data on `ParsedFile.captureSideChannel`,
|
||||
// so the main thread can restore them via `applyCaptureSideChannel` WITHOUT a
|
||||
|
|
|
|||
87
gitnexus/test/integration/cfg/fixtures/c-hazards.c
Normal file
87
gitnexus/test/integration/cfg/fixtures/c-hazards.c
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
/* C CFG hazard fixture (#2195 U2). Exercises every modeled C control-flow
|
||||
* construct so the pipeline emits BasicBlock + CFG + REACHING_DEF + CDG. */
|
||||
|
||||
int straight_line(int a, int b) {
|
||||
int x = a + b;
|
||||
int y = x * 2;
|
||||
return y;
|
||||
}
|
||||
|
||||
int if_else(int x) {
|
||||
int r;
|
||||
if (x > 0) {
|
||||
r = 1;
|
||||
} else {
|
||||
r = -1;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
int while_loop(int n) {
|
||||
int i = 0;
|
||||
int sum = 0;
|
||||
while (i < n) {
|
||||
sum = sum + i;
|
||||
i++;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
int do_while_loop(int n) {
|
||||
int i = 0;
|
||||
int sum = 0;
|
||||
do {
|
||||
sum = sum + i;
|
||||
i++;
|
||||
} while (i < n);
|
||||
return sum;
|
||||
}
|
||||
|
||||
int for_loop(int n) {
|
||||
int sum = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
sum = sum + i;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
const char *switch_fallthrough(int code) {
|
||||
const char *msg;
|
||||
switch (code) {
|
||||
case 1:
|
||||
msg = "one";
|
||||
case 2:
|
||||
msg = "two-or-more";
|
||||
break;
|
||||
default:
|
||||
msg = "other";
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
int goto_jump(int n) {
|
||||
int i = 0;
|
||||
int sum = 0;
|
||||
loop:
|
||||
if (i >= n) goto end;
|
||||
sum = sum + i;
|
||||
i++;
|
||||
goto loop;
|
||||
end:
|
||||
return sum;
|
||||
}
|
||||
|
||||
/* Non-terminating loop: EXIT must stay reverse-reachable for the CDG pass. */
|
||||
void server_forever(void) {
|
||||
for (;;) {
|
||||
handle_request();
|
||||
}
|
||||
}
|
||||
|
||||
int may_def(int a) {
|
||||
int x = 0;
|
||||
if (a && (x = compute())) {
|
||||
use(x);
|
||||
}
|
||||
return x;
|
||||
}
|
||||
61
gitnexus/test/integration/cfg/fixtures/cpp-hazards.cpp
Normal file
61
gitnexus/test/integration/cfg/fixtures/cpp-hazards.cpp
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
// C++ CFG hazard fixture (#2195 U2). Exercises the C core plus the C++-only
|
||||
// constructs: try/catch, throw, range-for, and lambdas.
|
||||
|
||||
#include <vector>
|
||||
#include <stdexcept>
|
||||
|
||||
int if_else(int x) {
|
||||
int r;
|
||||
if (x > 0) {
|
||||
r = 1;
|
||||
} else {
|
||||
r = -1;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
int try_catch(int x) {
|
||||
int result = 0;
|
||||
try {
|
||||
if (x < 0) {
|
||||
throw std::runtime_error("negative");
|
||||
}
|
||||
result = compute(x);
|
||||
} catch (const std::exception &e) {
|
||||
result = -1;
|
||||
handle(e);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void throw_no_try(int x) {
|
||||
if (x < 0) {
|
||||
throw std::runtime_error("bad");
|
||||
}
|
||||
proceed(x);
|
||||
}
|
||||
|
||||
int range_for(const std::vector<int> &xs) {
|
||||
int sum = 0;
|
||||
for (int x : xs) {
|
||||
sum = sum + x;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
int with_lambda(int n) {
|
||||
auto doubler = [](int v) {
|
||||
if (v > 0) {
|
||||
return v * 2;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
return doubler(n);
|
||||
}
|
||||
|
||||
// Non-terminating server loop — EXIT must stay reverse-reachable for the CDG pass.
|
||||
void run_forever() {
|
||||
while (true) {
|
||||
poll();
|
||||
}
|
||||
}
|
||||
305
gitnexus/test/unit/cfg/c-cpp-visitor.test.ts
Normal file
305
gitnexus/test/unit/cfg/c-cpp-visitor.test.ts
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { createRequire } from 'node:module';
|
||||
import { requireVendoredGrammar } from '../../../src/core/tree-sitter/vendored-grammars.js';
|
||||
import {
|
||||
createCCfgVisitor,
|
||||
createCppCfgVisitor,
|
||||
} from '../../../src/core/ingestion/cfg/visitors/c-cpp.js';
|
||||
import type { FunctionCfg } from '../../../src/core/ingestion/cfg/types.js';
|
||||
import { makeCfgHarness, type CfgHarness } from '../../helpers/cfg-harness.js';
|
||||
|
||||
// U2 — the C/C++ CfgVisitor, one hazard per test (KTD5: real-parser regression,
|
||||
// NOT snapshot-pinning). Each fixture's distinctive statement text (step(),
|
||||
// done(), handle(e), …) lets us locate the block for a region by text and assert
|
||||
// the control-flow topology around it.
|
||||
|
||||
const cGrammar = requireVendoredGrammar('tree-sitter-c') as Parameters<typeof makeCfgHarness>[0];
|
||||
const cppGrammar = createRequire(import.meta.url)('tree-sitter-cpp') as Parameters<
|
||||
typeof makeCfgHarness
|
||||
>[0];
|
||||
|
||||
const c: CfgHarness = makeCfgHarness(cGrammar, createCCfgVisitor(), 'fixture.c');
|
||||
const cpp: CfgHarness = makeCfgHarness(cppGrammar, createCppCfgVisitor(), 'fixture.cpp');
|
||||
|
||||
const block = (cfg: FunctionCfg, substr: string): number => {
|
||||
const b = cfg.blocks.find((bl) => bl.text.includes(substr));
|
||||
if (!b) throw new Error(`no block containing ${JSON.stringify(substr)}`);
|
||||
return b.index;
|
||||
};
|
||||
|
||||
const edgeKinds = (cfg: FunctionCfg): Set<string> => new Set(cfg.edges.map((e) => e.kind));
|
||||
|
||||
function reaches(cfg: FunctionCfg, from: number, to: number): boolean {
|
||||
const adj = new Map<number, number[]>();
|
||||
for (const e of cfg.edges) (adj.get(e.from) ?? adj.set(e.from, []).get(e.from)!).push(e.to);
|
||||
const seen = new Set([from]);
|
||||
const stack = [from];
|
||||
while (stack.length) {
|
||||
const n = stack.pop() as number;
|
||||
if (n === to) return true;
|
||||
for (const nx of adj.get(n) ?? []) if (!seen.has(nx)) (seen.add(nx), stack.push(nx));
|
||||
}
|
||||
return seen.has(to);
|
||||
}
|
||||
const reachable = (cfg: FunctionCfg, idx: number): boolean => reaches(cfg, cfg.entryIndex, idx);
|
||||
|
||||
/** Is EXIT reverse-reachable from every reachable block? (CDG soundness gate.) */
|
||||
function exitReachableFromAll(cfg: FunctionCfg): boolean {
|
||||
for (const b of cfg.blocks) {
|
||||
if (b.index === cfg.exitIndex) continue;
|
||||
if (!reachable(cfg, b.index)) continue; // unreachable blocks exempt
|
||||
if (!reaches(cfg, b.index, cfg.exitIndex)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Resolve a binding by name → its index in the function's binding table. */
|
||||
function bindingIdx(cfg: FunctionCfg, name: string): number {
|
||||
const i = (cfg.bindings ?? []).findIndex((b) => b.name === name);
|
||||
if (i < 0) throw new Error(`no binding ${name}`);
|
||||
return i;
|
||||
}
|
||||
|
||||
describe('C CfgVisitor — structure', () => {
|
||||
it('straight-line body: ENTRY → block → EXIT (seq)', () => {
|
||||
const cfg = c.cfgOf(`void 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 = c.cfgOf(`void f() {}`);
|
||||
expect(cfg.blocks).toHaveLength(2);
|
||||
expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true);
|
||||
});
|
||||
|
||||
it('malformed/unmodeled body returns undefined without throwing', () => {
|
||||
// A forward-declaration prototype (`int f(int);`) has no compound_statement
|
||||
// body — buildFunctionCfg must return undefined rather than throw.
|
||||
const root = c.parse(`int f(int);`);
|
||||
const fns = c.collectFunctions(root);
|
||||
// No function_definition (only a declaration) → nothing to build.
|
||||
expect(fns).toHaveLength(0);
|
||||
// And a body-less function node yields undefined, not a throw.
|
||||
const decl = c.parse(`void g() { x(); }`);
|
||||
const fn = c.collectFunctions(decl)[0];
|
||||
expect(() => createCCfgVisitor().buildFunctionCfg(fn, 'f.c')).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('C CfgVisitor — branching', () => {
|
||||
it('if/else: cond-true to then, cond-false to else, both reach the join', () => {
|
||||
const cfg = c.cfgOf(`void f(int x) { if (x) { a(); } else { b(); } c(); }`);
|
||||
const kinds = edgeKinds(cfg);
|
||||
expect(kinds.has('cond-true')).toBe(true);
|
||||
expect(kinds.has('cond-false')).toBe(true);
|
||||
const join = block(cfg, 'c();');
|
||||
expect(reaches(cfg, block(cfg, 'a();'), join)).toBe(true);
|
||||
expect(reaches(cfg, block(cfg, 'b();'), join)).toBe(true);
|
||||
});
|
||||
|
||||
it('plain if (no else): condition reaches both the body and the join', () => {
|
||||
const cfg = c.cfgOf(`void f(int x) { if (x) { a(); } b(); }`);
|
||||
const cond = block(cfg, 'x');
|
||||
expect(reaches(cfg, cond, block(cfg, 'a();'))).toBe(true);
|
||||
expect(reaches(cfg, cond, block(cfg, 'b();'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('C CfgVisitor — loops', () => {
|
||||
it('while loop: header + back-edge + exit', () => {
|
||||
const cfg = c.cfgOf(`void f(int x) { while (x > 0) { step(); } done(); }`);
|
||||
const header = block(cfg, 'x > 0');
|
||||
const body = block(cfg, 'step();');
|
||||
expect(cfg.edges).toContainEqual({ from: body, to: header, kind: 'loop-back' });
|
||||
expect(edgeKinds(cfg).has('cond-true')).toBe(true);
|
||||
expect(reaches(cfg, header, block(cfg, 'done();'))).toBe(true);
|
||||
});
|
||||
|
||||
it('do-while runs the body BEFORE testing, then loops back from the bottom', () => {
|
||||
const cfg = c.cfgOf(`void f(int x) { do { step(); } while (x > 0); done(); }`);
|
||||
const body = block(cfg, 'step();');
|
||||
const cond = block(cfg, 'x > 0');
|
||||
expect(reaches(cfg, cfg.entryIndex, body)).toBe(true); // body runs first
|
||||
// The back-edge / condition tests at the BOTTOM (cond reachable from body).
|
||||
expect(reaches(cfg, body, cond)).toBe(true);
|
||||
expect(cfg.edges).toContainEqual({ from: cond, to: body, kind: 'loop-back' });
|
||||
expect(reaches(cfg, cond, block(cfg, 'done();'))).toBe(true);
|
||||
});
|
||||
|
||||
it('C-style for: init once, condition header, back-edge through increment', () => {
|
||||
const cfg = c.cfgOf(`void f() { for (int i = 0; i < n; i++) { step(); } done(); }`);
|
||||
const init = block(cfg, 'int i = 0');
|
||||
const header = block(cfg, 'i < n');
|
||||
const incr = block(cfg, 'i++');
|
||||
const body = block(cfg, 'step();');
|
||||
expect(cfg.edges).toContainEqual({ from: cfg.entryIndex, to: init, kind: 'seq' });
|
||||
expect(reaches(cfg, body, incr)).toBe(true);
|
||||
expect(cfg.edges).toContainEqual({ from: incr, to: header, kind: 'loop-back' });
|
||||
expect(reaches(cfg, header, block(cfg, 'done();'))).toBe(true);
|
||||
});
|
||||
|
||||
it('for(;;) {} keeps EXIT reverse-reachable (structural exit-escape edge)', () => {
|
||||
const cfg = c.cfgOf(`void f() { for (;;) { work(); } }`);
|
||||
// The header has a cond-false escape to the loop-exit even with no condition.
|
||||
expect(edgeKinds(cfg).has('cond-false')).toBe(true);
|
||||
expect(exitReachableFromAll(cfg)).toBe(true);
|
||||
expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('C CfgVisitor — switch (C-style fallthrough)', () => {
|
||||
it('a case without break falls into the next case (switch-case + fallthrough)', () => {
|
||||
const cfg = c.cfgOf(`void f(int x) {
|
||||
switch (x) {
|
||||
case 1: one();
|
||||
case 2: two(); break;
|
||||
default: other();
|
||||
}
|
||||
after();
|
||||
}`);
|
||||
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
|
||||
expect(edgeKinds(cfg).has('fallthrough')).toBe(true);
|
||||
// case 1 (no break) FALLS THROUGH into case 2.
|
||||
expect(reaches(cfg, block(cfg, 'one();'), block(cfg, 'two();'))).toBe(true);
|
||||
// both cases reach the post-switch continuation.
|
||||
expect(reaches(cfg, block(cfg, 'two();'), block(cfg, 'after();'))).toBe(true);
|
||||
});
|
||||
|
||||
it('break-terminated case does not fall into the next case', () => {
|
||||
const cfg = c.cfgOf(`void f(int x) {
|
||||
switch (x) { case 1: one(); break; case 2: two(); break; }
|
||||
after();
|
||||
}`);
|
||||
expect(reaches(cfg, block(cfg, 'one();'), block(cfg, 'two();'))).toBe(false);
|
||||
expect(reaches(cfg, block(cfg, 'one();'), block(cfg, 'after();'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('C CfgVisitor — goto / labels', () => {
|
||||
it('backward goto wires to an already-seen label block', () => {
|
||||
const cfg = c.cfgOf(`void f() { int i = 0; loop: work(); i++; if (i < 10) goto loop; done(); }`);
|
||||
const gotoB = block(cfg, 'goto loop;');
|
||||
const label = block(cfg, 'work();');
|
||||
expect(reaches(cfg, gotoB, label)).toBe(true);
|
||||
expect(cfg.edges.some((e) => e.from === gotoB && e.to === label)).toBe(true);
|
||||
});
|
||||
|
||||
it('forward goto wires to a label that appears later', () => {
|
||||
const cfg = c.cfgOf(`void f(int x) { if (x) goto end; work(); end: done(); }`);
|
||||
const gotoB = block(cfg, 'goto end;');
|
||||
const label = block(cfg, 'done();');
|
||||
expect(reaches(cfg, gotoB, label)).toBe(true);
|
||||
// the goto skips work() on its path.
|
||||
expect(reachable(cfg, block(cfg, 'work();'))).toBe(true);
|
||||
});
|
||||
|
||||
it('goto to an UNDEFINED label routes to EXIT (single-exit preserved) and warns', () => {
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
try {
|
||||
const cfg = c.cfgOf(`void f() { work(); goto missing; }`);
|
||||
const gotoB = block(cfg, 'goto missing;');
|
||||
expect(reaches(cfg, gotoB, cfg.exitIndex)).toBe(true);
|
||||
expect(warn).toHaveBeenCalled();
|
||||
} finally {
|
||||
warn.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('C CfgVisitor — def/use harvest', () => {
|
||||
it('int x = a + b; use(x); produces a def of x and a use in the consumer', () => {
|
||||
const cfg = c.cfgOf(`void f(int a, int b) { int x = a + b; use(x); }`);
|
||||
const x = bindingIdx(cfg, 'x');
|
||||
// x is defined somewhere…
|
||||
const defined = cfg.blocks.some((bl) => bl.statements?.some((s) => s.defs.includes(x)));
|
||||
expect(defined).toBe(true);
|
||||
// …and used somewhere.
|
||||
const used = cfg.blocks.some((bl) => bl.statements?.some((s) => s.uses.includes(x)));
|
||||
expect(used).toBe(true);
|
||||
});
|
||||
|
||||
it('if (a && (x = f())) records x as a MAY-def, not a must-kill', () => {
|
||||
const cfg = c.cfgOf(`void f(int a) { int x = 0; if (a && (x = g())) h(x); }`);
|
||||
const x = bindingIdx(cfg, 'x');
|
||||
const hasMayDef = cfg.blocks.some((bl) =>
|
||||
bl.statements?.some((s) => (s.mayDefs ?? []).includes(x)),
|
||||
);
|
||||
expect(hasMayDef).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('C CfgVisitor — functionStartColumn', () => {
|
||||
it('two same-line functions get distinct functionStartColumn', () => {
|
||||
const cfgs = c.cfgsOf(`int a(){return 1;} int b(){return 2;}`);
|
||||
expect(cfgs).toHaveLength(2);
|
||||
expect(cfgs[0].functionStartLine).toBe(cfgs[1].functionStartLine); // same line
|
||||
expect(cfgs[0].functionStartColumn).not.toBe(cfgs[1].functionStartColumn); // distinct column
|
||||
});
|
||||
});
|
||||
|
||||
describe('C++ CfgVisitor — exceptions', () => {
|
||||
it('try/catch: a throw edge runs from each protected block to the handler', () => {
|
||||
const cfg = cpp.cfgOf(`void f() {
|
||||
try { risky(); deeper(); } catch (std::exception& e) { handle(e); }
|
||||
after();
|
||||
}`);
|
||||
expect(edgeKinds(cfg).has('throw')).toBe(true);
|
||||
const handler = block(cfg, 'handle(e);');
|
||||
// every protected-region block reaches the handler.
|
||||
expect(reaches(cfg, block(cfg, 'risky();'), handler)).toBe(true);
|
||||
// and after() is still reachable (handler completion rejoins).
|
||||
expect(reachable(cfg, block(cfg, 'after();'))).toBe(true);
|
||||
});
|
||||
|
||||
it('throw inside a branched try body reaches the handler from the interior block', () => {
|
||||
const cfg = cpp.cfgOf(`void f(int x) {
|
||||
try { guard(); if (x) { deep(); } } catch (int e) { onErr(); }
|
||||
}`);
|
||||
const handler = block(cfg, 'onErr();');
|
||||
expect(reaches(cfg, block(cfg, 'deep();'), handler)).toBe(true);
|
||||
});
|
||||
|
||||
it('throw with NO enclosing try routes to EXIT and ends its block', () => {
|
||||
const cfg = cpp.cfgOf(`void f(int x) { if (x) { throw 1; } done(); }`);
|
||||
const thr = block(cfg, 'throw 1;');
|
||||
expect(cfg.edges).toContainEqual({ from: thr, to: cfg.exitIndex, kind: 'throw' });
|
||||
// throw terminates its block — control does not fall into done() from it.
|
||||
expect(reaches(cfg, thr, block(cfg, 'done();'))).toBe(false);
|
||||
expect(reachable(cfg, block(cfg, 'done();'))).toBe(true); // via the if false branch
|
||||
});
|
||||
});
|
||||
|
||||
describe('C++ CfgVisitor — range-for', () => {
|
||||
it('for_range_loop: header + body + loop-back + exit', () => {
|
||||
const cfg = cpp.cfgOf(`void f(std::vector<int>& xs) { for (int x : xs) { use(x); } done(); }`);
|
||||
const body = block(cfg, 'use(x);');
|
||||
expect(edgeKinds(cfg).has('cond-true')).toBe(true);
|
||||
expect(edgeKinds(cfg).has('loop-back')).toBe(true);
|
||||
// the loop body loops back to the header and the loop has an exit to done().
|
||||
const header = cfg.edges.find((e) => e.kind === 'loop-back' && e.from === body)?.to;
|
||||
expect(header).toBeDefined();
|
||||
expect(reachable(cfg, block(cfg, 'done();'))).toBe(true);
|
||||
expect(exitReachableFromAll(cfg)).toBe(true);
|
||||
});
|
||||
|
||||
it('range-for declarator defines the loop variable; iterated expr is a use', () => {
|
||||
const cfg = cpp.cfgOf(`void f(std::vector<int>& xs) { for (int x : xs) { use(x); } }`);
|
||||
const x = bindingIdx(cfg, 'x');
|
||||
const defined = cfg.blocks.some((bl) => bl.statements?.some((s) => s.defs.includes(x)));
|
||||
expect(defined).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('C++ CfgVisitor — lambdas are CFG-bearing functions', () => {
|
||||
it('a lambda body yields its own well-formed CFG', () => {
|
||||
const cfgs = cpp.cfgsOf(`void f() { auto g = [](int x) { if (x) { a(); } return x; }; }`);
|
||||
// f and the lambda are both CFG-bearing.
|
||||
expect(cfgs.length).toBeGreaterThanOrEqual(2);
|
||||
for (const cfg of cfgs) {
|
||||
expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue