Are engines allowed to implement their own reentrancy guards for native functions?

I was playing around with recursive structures, and infinite recursion.

const x = { toString(){ return "x,"+x; } };
x.toString();
const y = {}; y["y"] = y;
JSON.stringify(y);

As expected, these throw exceptions - a RangeError: Maximum call stack size exceeded stack overflow and a TypeError: Converting circular structure to JSON respectively. Nothing wrong with that.

I was very surprised to see V8's Array.prototype.join to deviate from this:

const z = ["z",]; z[1] = z;
z.join();

I would have expected a stack overflow, but instead of an infinite recursive the reentrant call of .join() on the same array simply returned the empty string. This happens even when not directly calling itself but with indirection via user code:

const o = [{
  toString(){
    const str = o.join();
    console.log(str);
    return 'dubious';
  },
}];
console.log(o.join());

Is this a bug in V8, violating the spec, or does ECMAScript permit implementations to do this?

The case of reëntrant Array.prototype.join is a known discrepancy between spec and web reality. See:

1 Like