fix(wasm): emit dyn=0 for f.call/bind alias calls (closes #1687)#1693
Conversation
When `f.call({})` or `f.apply({})` is called and `f` is a plain
identifier, the callee is statically known — no dynamic dispatch is
required. The previous code emitted `{ dynamic: true, dynamicKind:
'reflection' }` which caused the `dynZeroEdgeRows` upgrade path in
`emitDirectCallEdgesForCall` to wrongly promote a prior dyn=0 edge
(from a direct `f()` call) to dyn=1, diverging from native Rust which
simply drops the second call once the edge is in `seenEdges`.
Fix: omit the `dynamic` flag for `.call/.apply/.bind` on plain
identifiers in `extractMemberExprCallInfo`. Member-expression objects
(`obj.method.call({})`) still receive `dynamicKind: 'reflection'`
since the inner callee requires a second resolution hop.
Update tests in dynamic-call-ffi.test.ts and javascript.test.ts to
reflect the new static-call behavior for plain-identifier receivers.
Impact: 1 functions changed, 8 affected
Greptile SummaryThis PR fixes a WASM/native parity divergence where
Confidence Score: 4/5Safe to merge — the core logic change is a one-liner with a clear causal chain, and the new static-call behavior is confirmed by updated tests and matches the native Rust engine output. The production code change is minimal and well-reasoned. The one gap is that the test titled for tests/parsers/javascript.test.ts — the new test at line 645 doesn't cover the plain-identifier Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["extractMemberExprCallInfo\ncallee is member_expression"] --> B{propText is\n.call/.apply/.bind?}
B -- No --> C[Other method call\nreturn static call with receiver]
B -- Yes --> D{obj.type?}
D -- identifier\ne.g. f.call --> E["Return static call\n{ name: obj.text, line }\ndyn=0"]
D -- member_expression\ne.g. obj.method.call --> F["Return dynamic/reflection call\n{ name: innerProp.text, dynamic: true,\ndynamicKind: 'reflection' }\ndyn=1"]
D -- other\ne.g. function expr --> G[Fall through to\ndefault handling]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A["extractMemberExprCallInfo\ncallee is member_expression"] --> B{propText is\n.call/.apply/.bind?}
B -- No --> C[Other method call\nreturn static call with receiver]
B -- Yes --> D{obj.type?}
D -- identifier\ne.g. f.call --> E["Return static call\n{ name: obj.text, line }\ndyn=0"]
D -- member_expression\ne.g. obj.method.call --> F["Return dynamic/reflection call\n{ name: innerProp.text, dynamic: true,\ndynamicKind: 'reflection' }\ndyn=1"]
D -- other\ne.g. function expr --> G[Fall through to\ndefault handling]
Reviews (1): Last reviewed commit: "fix(wasm): emit dyn=0 for f.call/bind al..." | Re-trigger Greptile |
| it('emits static call for .call/.apply/.bind on plain identifier (#1687)', () => { | ||
| // `f.call({})` where f is a plain identifier — target is statically known; | ||
| // must emit dyn=0 (no dynamic flag) to match native Rust engine parity. | ||
| const symbols = parseJS(`const f = function () {}.bind({}); f(); f.call({});`); | ||
| const fCallCalls = symbols.calls.filter((c) => c.name === 'f'); | ||
| expect(fCallCalls.length).toBeGreaterThanOrEqual(1); | ||
| for (const c of fCallCalls) { | ||
| expect(c.dynamic).toBeFalsy(); // all f() / f.call({}) calls must be dyn=0 | ||
| } | ||
| }); |
There was a problem hiding this comment.
Test title over-promises on
.bind coverage
The test is named "emits static call for .call/.apply/.bind on plain identifier" but the only .bind call in the fixture is function () {}.bind({}), where the receiver is a function_expression — not a plain identifier. That expression falls through the new identifier-check branch and is emitted as { name: 'bind', … }, so it is never included in fCallCalls. The actual plain-identifier .bind code path (e.g. f.bind(ctx) → { name: 'f' }) isn't exercised by any new test.
Codegraph Impact Analysis1 functions changed → 8 callers affected across 1 files
|
Summary
extractMemberExprCallInfo, change.call/.apply/.bindon plain identifiers from emitting{ dynamic: true, dynamicKind: 'reflection' }to emitting a static call (no dynamic flag)bind.jsfixture: WASM was emittingdyn=1while native emitteddyn=0forf.call({})patternsdynamic-call-ffi.test.tsandjavascript.test.tsto assert the new static-call behaviorRoot Cause
When
f.call({})precedes or follows a directf()call for the same target, the WASM edge builder'sdynZeroEdgeRowsupgrade path inemitDirectCallEdgesForCallwas converting adyn=0edge (emitted byf()) todyn=1because thef.call({})call haddynamicKind: 'reflection'set, triggering the upgrade conditionisDynamic === 1 && hasDynamicKind.The native Rust engine silently drops the second call (the
f.call({})one) once the edge is already inseenEdges, preservingdyn=0. The fix aligns WASM behavior: when the.call/.apply/.bindreceiver is a plain identifier, the target is statically known so we emit a static call —isDynamic = 0— which never triggers the upgrade.Member-expression objects (
obj.method.call({})) still receivedynamicKind: 'reflection'since the inner callee requires a second resolution hop.Closes #1687