Background
In ed78a78f ("Detect prototype cycles in Object.setPrototypeOf, isolate prototypes per test262 file") we landed two fixes. The second one — scripts/test262_harness/prototypeIsolation.js — is a workaround, not a real solution.
Goccia worker threads run multiple test262 files sequentially in the same engine. Built-in prototypes (Array.prototype, Object.prototype, …) are shared across those files, so a fixture that mutates Array.prototype.foo leaks the mutation into every subsequent file the same worker picks up, producing failures unrelated to the second file.
The current mitigation is a JS-level harness shim that:
- snapshots own descriptors of 13 standard prototypes in
beforeEach, and
- restores them in
afterEach.
This is enough to knock out the bulk of the spurious failures, but it has known gaps:
- Hard-coded set of 13 prototypes. Anything the test mutates outside that list (e.g. a less-common built-in, future additions) still leaks.
- Own descriptors only. Prototype-chain mutations (
Object.setPrototypeOf(Array.prototype, …)), symbol-keyed properties not enumerated, internal slots, and frozen/sealed state aren't fully captured.
- Other globals leak.
globalThis properties added by a test, mutations to Math, JSON, Reflect, Symbol.<wellknown> registries, the Symbol-for global registry, etc. are not reset.
- Runs inside the same realm. Anything that closes over a built-in (e.g. captured
Array reference) still observes the mutated version mid-test.
- Cost lives in JS. Every test pays the snapshot/restore cost in interpreted JS rather than the engine resetting cheaply.
What we should do instead
Refresh global state at the engine boundary, per file, so each test262 file starts in a pristine realm. Concretely:
- A runner-level mechanism (in
scripts/run_test262_suite.py and/or the engine itself) that gives each file a fresh set of built-ins — equivalent to a fresh realm.
- Drop
prototypeIsolation.js once the engine-level reset is in place.
- This should also cover non-prototype globals (
globalThis additions, Symbol.for registry, etc.), not just the 13 prototypes the harness currently snapshots.
Two plausible implementation directions:
- Per-file fresh engine. Tear down and rebuild the engine (or the global object + built-in tables) between files in a worker. Simpler conceptually; cost is the bootstrap time per file × thousands of files.
- Realm reset primitive. Keep the engine, expose a "reset realm" entry point that re-runs the built-in registration step against a clean global. Cheaper per-file; requires the bootstrap routines (in
Goccia.Engine.pas + Goccia.Runtime.Bootstrap.pas) to be safely re-runnable.
Direction 2 is more aligned with how V8/JSC handle this (Realms / contexts), and it also gives us a reusable primitive for the test262 `$262.createRealm` hook later.
Acceptance criteria
- A test262 file that mutates any built-in (own properties, prototype chain, frozen state, symbol keys, global properties) does not affect any subsequent file in the same worker.
prototypeIsolation.js is removed from the harness build.
- Full test262 run on
main shows no regression in pass count vs. the snapshot-harness baseline.
Related
- Commit
ed78a78f — current workaround
scripts/test262_harness/prototypeIsolation.js — file to be removed once engine-level reset is in place
scripts/run_test262_suite.py — where the per-file dispatch lives today
Background
In
ed78a78f("Detect prototype cycles in Object.setPrototypeOf, isolate prototypes per test262 file") we landed two fixes. The second one —scripts/test262_harness/prototypeIsolation.js— is a workaround, not a real solution.Goccia worker threads run multiple test262 files sequentially in the same engine. Built-in prototypes (
Array.prototype,Object.prototype, …) are shared across those files, so a fixture that mutatesArray.prototype.fooleaks the mutation into every subsequent file the same worker picks up, producing failures unrelated to the second file.The current mitigation is a JS-level harness shim that:
beforeEach, andafterEach.This is enough to knock out the bulk of the spurious failures, but it has known gaps:
Object.setPrototypeOf(Array.prototype, …)), symbol-keyed properties not enumerated, internal slots, and frozen/sealed state aren't fully captured.globalThisproperties added by a test, mutations toMath,JSON,Reflect,Symbol.<wellknown>registries, the Symbol-for global registry, etc. are not reset.Arrayreference) still observes the mutated version mid-test.What we should do instead
Refresh global state at the engine boundary, per file, so each test262 file starts in a pristine realm. Concretely:
scripts/run_test262_suite.pyand/or the engine itself) that gives each file a fresh set of built-ins — equivalent to a fresh realm.prototypeIsolation.jsonce the engine-level reset is in place.globalThisadditions,Symbol.forregistry, etc.), not just the 13 prototypes the harness currently snapshots.Two plausible implementation directions:
Goccia.Engine.pas+Goccia.Runtime.Bootstrap.pas) to be safely re-runnable.Direction 2 is more aligned with how V8/JSC handle this (Realms / contexts), and it also gives us a reusable primitive for the test262 `$262.createRealm` hook later.
Acceptance criteria
prototypeIsolation.jsis removed from the harness build.mainshows no regression in pass count vs. the snapshot-harness baseline.Related
ed78a78f— current workaroundscripts/test262_harness/prototypeIsolation.js— file to be removed once engine-level reset is in placescripts/run_test262_suite.py— where the per-file dispatch lives today