Related: #938 (its review found this while checking print of a self-referential array)
Confirmed by execution
A cyclic array is converted to a string with a nested reference skipped, in JavaScriptCore and V8 alike. zig-js throws RangeError instead.
| expression | JavaScriptCore (home-tool) | node v24.4.1 | zig-js (b895723) |
|---|---|---|---|
String(a) where a = [1]; a.push(a) | "1," | "1," | RangeError: Maximum call stack size exceeded. |
b.join("-") where b = [1, [2]]; b[1].push(b) | "1-2," | "1-2," | RangeError |
c.toLocaleString() where c = [o]; c.push(c) and o.toString returns "O" | "O," | "O," | RangeError |
ECMA-262's Array.prototype.join has no cycle detection, so zig-js follows the letter of the spec: the recursion runs until the stack guard throws. But every major engine keeps a set of arrays currently being joined and treats a re-entered array as the empty string. Code written for browsers or Bun relies on that, including logging of self-referential structures, and test262 does not cover it. This is a JavaScriptCore compatibility gap, not a crash.
Direction
Keep a per-realm (or per-interpreter) stack of receivers whose join is in progress, as JavaScriptCore does. On re-entry, return the empty string, and pop on every exit path, including a throw from a user toString. Array.prototype.toString and toLocaleString both go through join. print in the test runner does too, since #938 made it use ECMAScript ToString.