ReviewOS

zig-utils/zig-js

Errors: use JavaScriptCore's "is not an object" wording for member writes and deletes

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

Parent: #459 Related: #889, #892

Problem

RequireObjectCoercible failures on a read already match JavaScriptCore's leading clause. The write and delete paths do not — they use two different messages that JSC never emits:

sourcezig-jsJavaScriptCore
var v = null; v.x = 1Cannot access property of null or undefinednull is not an object (evaluating 'v.x = 1')
var x = {}; x.y.z = 2Cannot access property of null or undefinedundefined is not an object (evaluating 'x.y.z = 2')
var z = {}; delete z.a.bCannot convert undefined or null to objectundefined is not an object (evaluating 'delete z.a.b')

For comparison, these reads are already exact:

var p = null; p.x        -> null is not an object          (leading clause matches)
var o = {}; o.m.o        -> undefined is not an object     (leading clause matches)
var t = {}; t.z[u]       -> undefined is not an object     (leading clause matches)

So the same operation reports one of three different strings depending on whether the member expression is being read, assigned, or deleted. Only the read wording is right.

This is a leading clause mismatch, which is the half that matters for substring matching in ported test suites — distinct from the missing (evaluating '...') suffix that #892 covers for all three.

Required implementation

  • Route the assignment and delete RequireObjectCoercible failures through the same notAnObjectMessage(base) the read path uses, so all three say null is not an object / undefined is not an object.
  • Check both tiers: the tree-walker's member-assignment and delete_expr paths in src/interpreter.zig, and the corresponding store/delete opcodes in src/vm.zig. Message-text divergence between tiers is invisible to test262, which asserts none of it.
  • Verify against the oracle rather than by eye: ~/Code/Home/lang/zig-out/bin/home-tool run is a real JSC backend. Drive the probe from inside a function — indirect eval in a script shares global scope with the harness, so single-letter case variables silently clobber the driver's loop counter and truncate the run.
  • Note JSC names the whole assignment/delete expression in the suffix ('v.x = 1', 'delete z.a.b'), not just the member — relevant when #892 adds the suffix.
9 days ago

Sharpening this with a measurement made while landing #891/#892: the two tiers already disagree here, and the bytecode VM is the one that matches JavaScriptCore.

Running the same case on the tree-walker and then inside a generator body (which only the bytecode tier can execute):

sourcetree-walkerbytecode VMJavaScriptCore
var v = null; v.x = 1Cannot access property of null or undefinednull is not an objectnull is not an object (evaluating 'v.x = 1')
var x = {}; x.y.z = 2Cannot access property of null or undefinedundefined is not an objectundefined is not an object (evaluating 'x.y.z = 2')

So the VM's store opcodes already produce JSC's leading clause and the tree-walker's member assignment path does not. This is pre-existing — it reproduces on fb7c1aee, before any of the #891/#892 work — and it is invisible to test262, which asserts no message text.

That makes the fix smaller and better-defined than this issue originally described: bring the tree-walker's member-assignment path onto notAnObjectMessage(base), which the VM and the read path already use. The delete path (Cannot convert undefined or null to object) still needs checking on both tiers separately.

Method note for whoever picks this up: drive the differential from inside a function. Indirect eval in a script shares global scope with the harness, so single-letter case variables silently clobber the driver's loop counter and truncate the run — that cost me a bogus "0/36 match" reading before I caught it.

Sign in to comment on this issue.