Skip to content

Commit 253bd71

Browse files
committed
feat(resolver): track array spread and Array.from/concat/flat callbacks (JS)
Phase 8.3e — array-element points-to analysis for JS/TS. Closes #1321 ## What's resolved `f(...arr)`, `for (x of arr)`, `Array.from(arr, cb)`, and `new Set(arr)` patterns now produce call edges where function references flow through array operations: const arr = [a, b]; f(...arr); // f→a, f→b via spread for (x of arr) x() // outer→a, outer→b via iteration Recall on Jelly micro-test fixtures: spread 0→100%, more1 0→100%. ## Implementation - **types.ts** — 4 new interfaces: `ArrayElemBinding`, `SpreadArgBinding`, `ForOfBinding`, `ArrayCallbackBinding` - **extractors/javascript.ts** — `extractArrayElemBindingsWalk` + `extractSpreadForOfWalk` hooked into both query and walk paths - **points-to.ts** — array-element seeding, wildcard constraints, per-index spread constraints, for-of and callback constraints - **build-edges.ts** — passes new bindings to pts map builder; `buildParamFlowPtsPostPass` extended to handle all pts binding types - **wasm-worker-{protocol,entry,pool}.ts** — serializes/deserializes new bindings across the WASM Worker thread boundary - **tests/** — pts unit tests + jelly-micro fixtures for spread/more1
1 parent 832f7fc commit 253bd71

12 files changed

Lines changed: 547 additions & 9 deletions

File tree

src/domain/graph/builder/stages/build-edges.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -830,7 +830,15 @@ function buildPointsToMapForFile(
830830
symbols: ExtractorOutput,
831831
importedNames: Map<string, string>,
832832
): PointsToMap | null {
833-
if (!symbols.fnRefBindings?.length && !symbols.paramBindings?.length) return null;
833+
if (
834+
!symbols.fnRefBindings?.length &&
835+
!symbols.paramBindings?.length &&
836+
!symbols.arrayElemBindings?.length &&
837+
!symbols.spreadArgBindings?.length &&
838+
!symbols.forOfBindings?.length &&
839+
!symbols.arrayCallbackBindings?.length
840+
)
841+
return null;
834842
const defNames = new Set(
835843
symbols.definitions
836844
.filter((d) => d.kind === 'function' || d.kind === 'method')
@@ -843,6 +851,10 @@ function buildPointsToMapForFile(
843851
importedNames,
844852
symbols.paramBindings,
845853
definitionParams,
854+
symbols.arrayElemBindings,
855+
symbols.spreadArgBindings,
856+
symbols.forOfBindings,
857+
symbols.arrayCallbackBindings,
846858
);
847859
}
848860

src/domain/graph/resolver/points-to.ts

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
* that build-edges.ts already builds per file is the cross-module link — if
2020
* a variable aliases an imported name, resolveCallTargets follows it).
2121
*/
22-
import type { FnRefBinding, ParamBinding } from '../../../types.js';
22+
import type { ArrayCallbackBinding, ArrayElemBinding, FnRefBinding, ForOfBinding, ParamBinding, SpreadArgBinding } from '../../../types.js';
2323

2424
export type PointsToMap = Map<string, Set<string>>;
2525

@@ -41,18 +41,26 @@ const MAX_SOLVER_ITERATIONS = 50;
4141
* look up — either a locally-defined function name (found via byNameAndFile) or
4242
* an imported name (found via importedNames → byNameAndFile in the source file).
4343
*
44-
* @param fnRefBindings - identifier/member-expr bindings from the extractor
45-
* @param definitionNames - locally-defined callable names in this file
46-
* @param importedNames - names imported into this file (name → resolved file)
47-
* @param paramBindings - call-site arg→param bindings (Phase 8.3c)
48-
* @param definitionParams - per-function ordered parameter names (Phase 8.3c)
44+
* @param fnRefBindings - identifier/member-expr bindings from the extractor
45+
* @param definitionNames - locally-defined callable names in this file
46+
* @param importedNames - names imported into this file (name → resolved file)
47+
* @param paramBindings - call-site arg→param bindings (Phase 8.3c)
48+
* @param definitionParams - per-function ordered parameter names (Phase 8.3c)
49+
* @param arrayElemBindings - array literal element bindings (Phase 8.3e)
50+
* @param spreadArgBindings - spread-argument bindings (Phase 8.3e)
51+
* @param forOfBindings - for-of iteration variable bindings (Phase 8.3e)
52+
* @param arrayCallbackBindings - Array.from/callback bindings (Phase 8.3e)
4953
*/
5054
export function buildPointsToMap(
5155
fnRefBindings: readonly FnRefBinding[],
5256
definitionNames: ReadonlySet<string>,
5357
importedNames: ReadonlyMap<string, string>,
5458
paramBindings?: readonly ParamBinding[],
5559
definitionParams?: ReadonlyMap<string, readonly string[]>,
60+
arrayElemBindings?: readonly ArrayElemBinding[],
61+
spreadArgBindings?: readonly SpreadArgBinding[],
62+
forOfBindings?: readonly ForOfBinding[],
63+
arrayCallbackBindings?: readonly ArrayCallbackBinding[],
5664
): PointsToMap {
5765
const pts: PointsToMap = new Map();
5866

@@ -100,6 +108,68 @@ export function buildPointsToMap(
100108
}
101109
}
102110

111+
// Phase 8.3e: array-element bindings — seed concrete elements and wildcard.
112+
// `arr[0]` etc. are seeded from literal arrays; `arr[*]` collects all elements.
113+
if (arrayElemBindings && arrayElemBindings.length > 0) {
114+
for (const { arrayName, index, elemName } of arrayElemBindings) {
115+
const elemKey = `${arrayName}[${index}]`;
116+
const wildcardKey = `${arrayName}[*]`;
117+
// Seed the per-index entry if the elemName is a concrete function.
118+
if (!pts.has(elemKey)) pts.set(elemKey, new Set());
119+
pts.get(elemKey)!.add(elemName);
120+
// Wildcard: array[*] collects all element targets for imprecise spread/for-of.
121+
constraints.push({ lhs: wildcardKey, rhsKey: elemKey });
122+
}
123+
}
124+
125+
// Phase 8.3e: spread-argument constraints.
126+
// f(...arr) → pts[f::param_i] ⊇ pts[arr[i]] for each known element.
127+
if (spreadArgBindings && spreadArgBindings.length > 0 && definitionParams) {
128+
// Build a per-array index count from arrayElemBindings for precise per-index constraints.
129+
const arrayMaxIndex = new Map<string, number>();
130+
for (const { arrayName, index } of (arrayElemBindings ?? [])) {
131+
const cur = arrayMaxIndex.get(arrayName) ?? -1;
132+
if (index > cur) arrayMaxIndex.set(arrayName, index);
133+
}
134+
135+
for (const { callee, arrayName, startIndex } of spreadArgBindings) {
136+
const params = definitionParams.get(callee);
137+
if (!params) continue;
138+
const maxIdx = arrayMaxIndex.get(arrayName) ?? -1;
139+
if (maxIdx >= 0) {
140+
// Precise: per-element constraints.
141+
for (let i = 0; i <= maxIdx; i++) {
142+
const paramIdx = startIndex + i;
143+
if (paramIdx >= params.length) break;
144+
constraints.push({ lhs: `${callee}::${params[paramIdx]}`, rhsKey: `${arrayName}[${i}]` });
145+
}
146+
} else {
147+
// Unknown array size: all params at/after startIndex get the wildcard.
148+
for (let j = startIndex; j < params.length; j++) {
149+
constraints.push({ lhs: `${callee}::${params[j]}`, rhsKey: `${arrayName}[*]` });
150+
}
151+
}
152+
}
153+
}
154+
155+
// Phase 8.3e: for-of iteration constraints.
156+
// `for (const x of arr)` inside `outer` → pts[outer::x] ⊇ pts[arr[*]]
157+
if (forOfBindings) {
158+
for (const { varName, sourceName, enclosingFunc } of forOfBindings) {
159+
constraints.push({ lhs: `${enclosingFunc}::${varName}`, rhsKey: `${sourceName}[*]` });
160+
}
161+
}
162+
163+
// Phase 8.3e: Array.from / callback constraints.
164+
// Array.from(source, cb) → pts[cb::param0] ⊇ pts[source[*]]
165+
if (arrayCallbackBindings && definitionParams) {
166+
for (const { sourceName, calleeName } of arrayCallbackBindings) {
167+
const params = definitionParams.get(calleeName);
168+
if (!params || params.length === 0) continue;
169+
constraints.push({ lhs: `${calleeName}::${params[0]}`, rhsKey: `${sourceName}[*]` });
170+
}
171+
}
172+
103173
if (constraints.length === 0) return pts;
104174

105175
// Fixed-point iteration: propagate pts sets until no new information flows.

src/domain/wasm-worker-entry.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -803,6 +803,13 @@ function serializeExtractorOutput(
803803
dataflow: symbols.dataflow,
804804
astNodes,
805805
...(symbols.fnRefBindings?.length ? { fnRefBindings: symbols.fnRefBindings } : {}),
806+
...(symbols.paramBindings?.length ? { paramBindings: symbols.paramBindings } : {}),
807+
...(symbols.arrayElemBindings?.length ? { arrayElemBindings: symbols.arrayElemBindings } : {}),
808+
...(symbols.spreadArgBindings?.length ? { spreadArgBindings: symbols.spreadArgBindings } : {}),
809+
...(symbols.forOfBindings?.length ? { forOfBindings: symbols.forOfBindings } : {}),
810+
...(symbols.arrayCallbackBindings?.length
811+
? { arrayCallbackBindings: symbols.arrayCallbackBindings }
812+
: {}),
806813
...(symbols.newExpressions?.length ? { newExpressions: symbols.newExpressions } : {}),
807814
};
808815
}

src/domain/wasm-worker-pool.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,11 @@ function deserializeResult(ser: SerializedExtractorOutput | null): ExtractorOutp
107107
// visitor output is cast the same way.
108108
if (ser.astNodes !== undefined) out.astNodes = ser.astNodes as unknown as ASTNodeRow[];
109109
if (ser.fnRefBindings?.length) out.fnRefBindings = ser.fnRefBindings;
110+
if (ser.paramBindings?.length) out.paramBindings = ser.paramBindings;
111+
if (ser.arrayElemBindings?.length) out.arrayElemBindings = ser.arrayElemBindings;
112+
if (ser.spreadArgBindings?.length) out.spreadArgBindings = ser.spreadArgBindings;
113+
if (ser.forOfBindings?.length) out.forOfBindings = ser.forOfBindings;
114+
if (ser.arrayCallbackBindings?.length) out.arrayCallbackBindings = ser.arrayCallbackBindings;
110115
if (ser.newExpressions?.length) out.newExpressions = ser.newExpressions;
111116
return out;
112117
}

src/domain/wasm-worker-protocol.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ export interface SerializedExtractorOutput {
6363
receiver?: string;
6464
}>;
6565
fnRefBindings?: import('../types.js').FnRefBinding[];
66+
paramBindings?: import('../types.js').ParamBinding[];
67+
arrayElemBindings?: import('../types.js').ArrayElemBinding[];
68+
spreadArgBindings?: import('../types.js').SpreadArgBinding[];
69+
forOfBindings?: import('../types.js').ForOfBinding[];
70+
arrayCallbackBindings?: import('../types.js').ArrayCallbackBinding[];
6671
newExpressions?: readonly string[];
6772
}
6873

0 commit comments

Comments
 (0)