ReviewOS

zig-utils/zig-js

JIT: name the callee in "is not a function" from optimized native dispatch

#890
Closed glennmichael123 opened this 9 days ago ·
9 days ago

Parent: #889 Related: #459, #484

Problem

#889 makes a failed call name its callee with JavaScriptCore's exact text on both the tree-walker and the bytecode VM. The optimizing tier's native runtime-operation dispatch (nativeOperationDispatch in src/vm.zig) is the one remaining [[Call]] boundary that cannot, so a call that fails inside optimized native code still reports the value-only wording:

TypeError: value is not a function

instead of

TypeError: o.m is not a function. (In 'o.m()', 'o.m' is undefined)

The tiers therefore disagree on message text for exactly the calls that got hot enough to be optimized — the class of divergence a green corpus hides, because test262 does not assert message text at all.

Why it was left out

jit.NativeFrame is an extern struct shared with generated code and carries no owning bc.Chunk, so the dispatch entry point cannot reach chunk.callSiteAt the way the interpreter loop does. Every call arm in nativeOperationDispatch currently passes .none.

Required implementation

jit.NativeOperationMetadata already carries per-operation side tables that the artifact builder fills in from the chunk — operation_names (consumed by metadata.nameFor, which supplies call_method's property name) and call_links. A call-site span table is the same pattern:

  • add a call_sites: []bc.CallSiteSpan parallel array to NativeOperationMetadata, populated where operation_names already is, from the originating chunk's call_sites keyed by NativeOperationDescriptor.origin (which is the bytecode instruction index);
  • expose metadata.callSiteFor(operation_id) and pass the resulting Interpreter.CallSite from every call arm in nativeOperationDispatch instead of .none;
  • gate it: the existing #889 regression test in src/context.zig asserts the identical text on the tree-walker and the VM, and the optimized tier must be driven to the same text by a warmed loop whose callee becomes non-callable.

No hot-path cost is involved: the span is read only after a callee has already turned out not to be callable.

9 days ago

Concrete repro and a scope correction, measured while landing #891/#892.

The divergence is real but narrower than expected

Running each form cold, then again after 200k iterations so the optimizing tier compiles it:

formcoldhot
new C()1 is not a constructor (evaluating 'new C()')same
new C(...a)1 is not a constructor (evaluating 'new C(...a)')1 is not a constructor
t`x` 1 is not a function (near '...t`x`...')same
f()f is not a function. (In 'f()', 'f' is 1)same

So new_call, the tagged-template opcodes, and plain call all keep their span through tier-up — only new_spread loses it.

function mkS(C, a) { return new C(...a); }
for (var j = 0; j < 200000; j++) { try { mkS(Object, []); } catch (e) {} }
try { mkS(1, []); } catch (e) { e.message }   // loses "(evaluating 'new C(...a)')"

Why

nativeOperationDispatch routes bc.Op.new_spread to constructSpreadValue (src/vm.zig), which hardcodes .none. That helper is reachable only from the JIT native dispatch — the bytecode interpreter's own .new_spread arm passes vmCallSite(chunk, ip) — so this is squarely the NativeFrame-has-no-chunk blocker this issue already describes, not a separate gap.

Scope note

This issue was filed for is not a function. It should also cover is not a constructor: after #892's construction half, new C(...a) is a form where the tiers now disagree where they previously agreed (at fb7c1aee every tier produced the short text). The fix is the same one already sketched here — a call_sites parallel array on NativeOperationMetadata keyed by NativeOperationDescriptor.origin, mirroring operation_names — and it covers all three message forms at once, since they share bc.CallSiteSpan and its kind discriminator.

Gate

The existing expectJscMessages helper in src/context.zig only drives the tree-walker and a generator body (the bytecode tier). A JIT-tier gate needs a warmed loop whose callee turns non-callable/non-constructable after tier-up, like the repro above.

Sign in to comment on this issue.