refactor(cfg): consolidate scope-tree substrate into ScopeTreeHarvester (#2195 U6)

The Go/Java/C#/C-C++ def/use harvesters each carried a byte-identical copy
of the lexical scope-tree machinery (Scope record, two-phase resolution
cache, openScope/nearestScopeOf/resolve/def/use/conditional/bindingTable,
~270 lines total). Extract it into an abstract ScopeTreeHarvester base; the
four harvesters now extend it and supply only their genuine per-language
variation (the prescan switch, plus Go's _-blank-identifier overrides of
declare/def/use). Net -422 lines. Mechanical and byte-equivalent: cfg unit
suite 613 passed, bench --check fingerprints unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Gergo Magyar 2026-06-14 16:32:24 +00:00
parent 7192036183
commit 7ebd579524
5 changed files with 197 additions and 448 deletions

View file

@ -52,13 +52,7 @@
import type { SyntaxNode } from '../../utils/ast-helpers.js';
import type { BindingEntry, StatementFacts } from '../types.js';
import { CallSiteFactAccumulator } from './call-site-harvest.js';
/**
* The per-statement def/use + call-site collector. Aliased to the shared
* {@link CallSiteFactAccumulator} so the harvester references one name for both
* the value (constructor) and the type.
*/
type FactAccumulator = CallSiteFactAccumulator;
import { ScopeTreeHarvester, type Scope, type FactAccumulator } from './scope-tree-harvest.js';
/** Node types that own a nested CFG — their subtrees are opaque to harvesting. */
const NESTED_FUNCTION_TYPES = new Set(['lambda_expression', 'function_definition']);
@ -83,43 +77,14 @@ const TYPE_CONTEXT_TYPES = new Set([
'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;
/**
* Call/new node id → bindings whose declarator/assignment VALUE is exactly
* that call (#2195 U6). Registered by the declaration/assignment handlers
* BEFORE the value walk, consumed by {@link visitCall} when it reaches the
* node (mirrors the TS harvester's `resultDefTargets`).
*/
private readonly resultDefTargets = new Map<number, number[]>();
constructor(private readonly fnNode: SyntaxNode) {
this.fnId = fnNode.id;
this.scopeByNode.set(fnNode.id, this.root);
export class CCppHarvester extends ScopeTreeHarvester {
constructor(fnNode: SyntaxNode) {
super(fnNode);
this.declareParams(fnNode);
const body = this.bodyOf(fnNode);
if (body) this.prescan(body, this.openScope(body));
}
/** The completed binding table — pass to `CfgBuilder.finish`. */
bindingTable(): readonly BindingEntry[] {
return this.bindings;
}
/** The function/lambda body block (`compound_statement`). */
private bodyOf(fnNode: SyntaxNode): SyntaxNode | undefined {
const body = fnNode.childForFieldName('body');
@ -129,35 +94,6 @@ export class CCppHarvester {
// ── 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;
@ -210,7 +146,7 @@ export class CCppHarvester {
return undefined;
}
private prescan(node: SyntaxNode, scope: Scope): void {
protected 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) {
@ -333,55 +269,6 @@ export class CCppHarvester {
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;

View file

@ -47,12 +47,7 @@
import type { SyntaxNode } from '../../utils/ast-helpers.js';
import type { BindingEntry, StatementFacts } from '../types.js';
import { CallSiteFactAccumulator } from './call-site-harvest.js';
/**
* The per-statement def/use + call-site collector, aliased to the shared
* {@link CallSiteFactAccumulator} (one name for the value and the type).
*/
type FactAccumulator = CallSiteFactAccumulator;
import { ScopeTreeHarvester, type Scope, type FactAccumulator } from './scope-tree-harvest.js';
/** Node types that own a nested CFG — their subtrees are opaque to harvesting. */
const NESTED_FUNCTION_TYPES = new Set([
@ -80,42 +75,14 @@ const SCOPE_TYPES = new Set([
'switch_expression_arm',
]);
interface Scope {
readonly parent: Scope | null;
/** name → binding index */
readonly table: Map<string, number>;
}
export class CsharpHarvester {
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;
/**
* Call/new node id → bindings whose declarator/assignment VALUE is exactly
* that call (#2195 U6). Registered before the value walk, consumed by
* {@link visitCall} (mirrors the TS harvester's `resultDefTargets`).
*/
private readonly resultDefTargets = new Map<number, number[]>();
constructor(private readonly fnNode: SyntaxNode) {
this.fnId = fnNode.id;
this.scopeByNode.set(fnNode.id, this.root);
export class CsharpHarvester extends ScopeTreeHarvester {
constructor(fnNode: SyntaxNode) {
super(fnNode);
this.declareParams(fnNode);
const body = this.bodyOf(fnNode);
if (body) this.prescan(body, this.openScope(body));
}
/** The completed binding table — pass to `CfgBuilder.finish`. */
bindingTable(): readonly BindingEntry[] {
return this.bindings;
}
/** The function/lambda body node (a `block` or an expression for `=> expr`). */
private bodyOf(fnNode: SyntaxNode): SyntaxNode | undefined {
const body = fnNode.childForFieldName('body');
@ -126,35 +93,6 @@ export class CsharpHarvester {
// ── 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,
});
}
private declareParams(fnNode: SyntaxNode): void {
const params =
fnNode.childForFieldName('parameters') ??
@ -175,7 +113,7 @@ export class CsharpHarvester {
}
}
private prescan(node: SyntaxNode, scope: Scope): void {
protected 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) {
@ -308,55 +246,6 @@ export class CsharpHarvester {
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;

View file

@ -68,12 +68,7 @@
import type { SyntaxNode } from '../../utils/ast-helpers.js';
import type { BindingEntry, StatementFacts } from '../types.js';
import { CallSiteFactAccumulator } from './call-site-harvest.js';
/**
* The per-statement def/use + call-site collector, aliased to the shared
* {@link CallSiteFactAccumulator} (one name for the value and the type).
*/
type FactAccumulator = CallSiteFactAccumulator;
import { ScopeTreeHarvester, type Scope, type FactAccumulator } from './scope-tree-harvest.js';
/** Node types that own a nested CFG — their subtrees are opaque to harvesting. */
const NESTED_FUNCTION_TYPES = new Set([
@ -100,43 +95,15 @@ const SCOPE_TYPES = new Set([
'communication_case',
]);
interface Scope {
readonly parent: Scope | null;
/** name → binding index */
readonly table: Map<string, number>;
}
export class GoHarvester {
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;
/**
* Call node id → bindings whose declaration/assignment VALUE is exactly that
* call (#2195 U6). Registered before the value walk, consumed by
* {@link visitCall} (mirrors the TS harvester's `resultDefTargets`).
*/
private readonly resultDefTargets = new Map<number, number[]>();
constructor(private readonly fnNode: SyntaxNode) {
this.fnId = fnNode.id;
this.scopeByNode.set(fnNode.id, this.root);
export class GoHarvester extends ScopeTreeHarvester {
constructor(fnNode: SyntaxNode) {
super(fnNode);
this.declareReceiver(fnNode);
this.declareParams(fnNode);
const body = this.bodyOf(fnNode);
if (body && body.type === 'block') this.prescan(body, this.openScope(body));
}
/** The completed binding table — pass to `CfgBuilder.finish`. */
bindingTable(): readonly BindingEntry[] {
return this.bindings;
}
/** The function/method/literal body node (always a `block` in Go). */
private bodyOf(fnNode: SyntaxNode): SyntaxNode | undefined {
return fnNode.childForFieldName('body') ?? undefined;
@ -144,24 +111,8 @@ export class GoHarvester {
// ── 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 {
/** Go override: `_` is the blank identifier and binds nothing. */
protected override declare(nameNode: SyntaxNode, kind: BindingEntry['kind'], scope: Scope): void {
const name = nameNode.text;
if (!name || name === '_' || scope.table.has(name)) return; // `_` is the blank identifier
scope.table.set(name, this.bindings.length);
@ -201,7 +152,7 @@ export class GoHarvester {
}
}
private prescan(node: SyntaxNode, scope: Scope): void {
protected 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) {
@ -393,57 +344,19 @@ export class GoHarvester {
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 {
/** Go override: the blank identifier (`_`) defines nothing. */
protected override def(nameNode: SyntaxNode, acc: FactAccumulator): void {
if (nameNode.text === '_') return; // blank identifier defines nothing
if (this.conditionalDepth > 0) acc.addMayDef(this.resolve(nameNode));
else acc.addDef(this.resolve(nameNode));
}
private use(nameNode: SyntaxNode, acc: FactAccumulator): void {
/** Go override: the blank identifier (`_`) is read of nothing. */
protected override 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--;
}
}
/** Strip parenthesized wrappers around an lvalue (`(x) = 1`). */
private unwrapLvalue(node: SyntaxNode): SyntaxNode {
let n = node;

View file

@ -44,12 +44,7 @@
import type { SyntaxNode } from '../../utils/ast-helpers.js';
import type { BindingEntry, StatementFacts } from '../types.js';
import { CallSiteFactAccumulator } from './call-site-harvest.js';
/**
* The per-statement def/use + call-site collector, aliased to the shared
* {@link CallSiteFactAccumulator} (one name for the value and the type).
*/
type FactAccumulator = CallSiteFactAccumulator;
import { ScopeTreeHarvester, type Scope, type FactAccumulator } from './scope-tree-harvest.js';
/** Node types that own a nested CFG — their subtrees are opaque to harvesting. */
const NESTED_FUNCTION_TYPES = new Set([
@ -80,42 +75,14 @@ const SCOPE_TYPES = new Set([
/** Comment node types tree-sitter-java surfaces (NOT `comment`). */
const COMMENT_TYPES = new Set(['line_comment', 'block_comment']);
interface Scope {
readonly parent: Scope | null;
/** name → binding index */
readonly table: Map<string, number>;
}
export class JavaHarvester {
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;
/**
* Call/new node id → bindings whose declarator/assignment VALUE is exactly
* that call (#2195 U6). Registered before the value walk, consumed by
* {@link visitCall} (mirrors the TS harvester's `resultDefTargets`).
*/
private readonly resultDefTargets = new Map<number, number[]>();
constructor(private readonly fnNode: SyntaxNode) {
this.fnId = fnNode.id;
this.scopeByNode.set(fnNode.id, this.root);
export class JavaHarvester extends ScopeTreeHarvester {
constructor(fnNode: SyntaxNode) {
super(fnNode);
this.declareParams(fnNode);
const body = this.bodyOf(fnNode);
if (body && body.type === 'block') this.prescan(body, this.openScope(body));
}
/** The completed binding table — pass to `CfgBuilder.finish`. */
bindingTable(): readonly BindingEntry[] {
return this.bindings;
}
/** The function/lambda body node (a `block`, or an expression for `x -> expr`). */
private bodyOf(fnNode: SyntaxNode): SyntaxNode | undefined {
const body = fnNode.childForFieldName('body');
@ -125,35 +92,6 @@ export class JavaHarvester {
// ── 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,
});
}
private declareParams(fnNode: SyntaxNode): void {
const params = fnNode.childForFieldName('parameters');
if (!params) {
@ -190,7 +128,7 @@ export class JavaHarvester {
return declarator?.childForFieldName('name');
}
private prescan(node: SyntaxNode, scope: Scope): void {
protected 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) {
@ -315,55 +253,6 @@ export class JavaHarvester {
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;

View file

@ -0,0 +1,171 @@
/**
* Shared scope-tree substrate for the C-family / Go / Java / C# def/use
* harvesters (#2197 U6, plan KTD4 — a byte-equivalent consolidation).
*
* The Go ({@link import('./go-harvest.js').GoHarvester}), Java ({@link
* import('./java-harvest.js').JavaHarvester}), C# ({@link
* import('./csharp-harvest.js').CsharpHarvester}) and C/C++ ({@link
* import('./c-cpp-harvest.js').CCppHarvester}) harvesters each carried a
* BYTE-IDENTICAL copy of the lexical scope tree machinery: the {@link Scope}
* record, the binding/scope/synthetic state, the two-phase resolution cache, and
* the `openScope` / `nearestScopeOf` / `resolve` / `def` / `use` / `conditional`
* / `bindingTable` methods. This base holds that one copy; the four harvesters
* extend it and supply ONLY their genuine per-language variation — the
* `prescan` switch (abstract) and, for Go, the blank-identifier (`_`) overrides
* of `declare` / `def` / `use`.
*
* TWO-PHASE, ORDER-INDEPENDENT (load-bearing): 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 (`prescan`, per-language) pre-scans the
* whole function subtree once into a completed lexical scope tree; phase 2
* (`resolve`) resolves defs/uses against that finished tree from any walk order.
*
* Identifiers with no in-function declaration (globals, fields, imports, …)
* resolve to a SYNTHETIC module-level binding (`name@module`), created on first
* reference and applied identically by def and use harvesting.
*
* NOTE: nothing serialized via the harvested bindings/facts 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 } from '../types.js';
import { CallSiteFactAccumulator } from './call-site-harvest.js';
/**
* The per-statement def/use + call-site collector, aliased to the shared
* {@link CallSiteFactAccumulator} (one name for the value and the type).
*/
export type FactAccumulator = CallSiteFactAccumulator;
export interface Scope {
readonly parent: Scope | null;
/** name → binding index */
readonly table: Map<string, number>;
}
/**
* Abstract base owning the lexical scope tree + the two-phase resolution
* substrate. Subclasses provide the per-language constructor wiring (param /
* receiver declaration + the body `prescan` kick-off) and the abstract
* `prescan`; Go additionally overrides `declare` / `def` / `use` for its `_`
* blank-identifier semantics.
*/
export abstract class ScopeTreeHarvester {
protected readonly bindings: BindingEntry[] = [];
protected readonly scopeByNode = new Map<number, Scope>();
protected readonly root: Scope = { parent: null, table: new Map() };
protected readonly synthetic = new Map<string, number>();
protected readonly fnId: number;
/** Innermost enclosing scope per visited node id (prescan-filled) — O(scope-chain) phase-2 resolution. */
protected readonly nearestScopeCache = new Map<number, Scope>();
/** >0 while walking a conditionally-evaluated subexpression — defs become may-defs. */
protected conditionalDepth = 0;
/**
* Call/new node id → bindings whose declaration/assignment VALUE is exactly
* that call (#2195 U6). Registered before the value walk, consumed by the
* language harvester's `visitCall` (mirrors the TS harvester's
* `resultDefTargets`).
*/
protected readonly resultDefTargets = new Map<number, number[]>();
constructor(protected readonly fnNode: SyntaxNode) {
this.fnId = fnNode.id;
this.scopeByNode.set(fnNode.id, this.root);
}
/** The completed binding table — pass to `CfgBuilder.finish`. */
bindingTable(): readonly BindingEntry[] {
return this.bindings;
}
// ── phase 1: declaration pre-scan ────────────────────────────────────────
protected 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;
}
protected 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;
}
protected 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,
});
}
/**
* Phase-1 declaration pre-scan — the only genuine per-language variation (each
* grammar has a distinct declaration-node taxonomy). Walks the function
* subtree once, filling `nearestScopeCache` and the scope tables.
*/
protected abstract prescan(node: SyntaxNode, scope: Scope): void;
// ── phase 2: per-statement fact extraction ───────────────────────────────
protected 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;
}
protected def(nameNode: SyntaxNode, acc: FactAccumulator): void {
if (this.conditionalDepth > 0) acc.addMayDef(this.resolve(nameNode));
else acc.addDef(this.resolve(nameNode));
}
protected use(nameNode: SyntaxNode, acc: FactAccumulator): void {
acc.addUse(this.resolve(nameNode));
}
/** Run `fn` with defs demoted to may-defs (conditionally-evaluated context). */
protected conditional(fn: () => void): void {
this.conditionalDepth++;
try {
fn();
} finally {
this.conditionalDepth--;
}
}
}