Browse documentation

Opt-in ShadowRealm

Date: 2026-06-24 Area: engine

Added the TC39 ShadowRealm proposal (stage 2.7) behind opt-in --unsafe-shadowrealm / "unsafe-shadowrealm". ShadowRealm is a global ECMAScript built-in constructor, so the engine owns it (Goccia.Builtins.GlobalShadowRealm.EnableShadowRealm) rather than a runtime extension — that is the only home that works for both the runtime-bearing Script Loader and the runtime-less Bare Script Loader used by test262. It stays off by default because ShadowRealm.prototype.evaluate performs dynamic source evaluation and ShadowRealm.prototype.importValue imports modules into the child realm, capabilities GocciaScript keeps out of normal runtimes (see VISION.md), following the same explicit-capability pattern as --unsafe-function-constructor. The flag is its own capability gate, not folded into --unsafe-function-constructor.

Each new ShadowRealm() owns a fresh child TGocciaEngine (its own realm, intrinsics, and global object), reusing the per-engine realm isolation of ADR 0032. Child realms are owned by the creating engine and freed at its teardown, mirroring the $262.createRealm ownership model: GC sweep runs object destructors with no active realm context, so a child engine cannot be safely freed from a collected instance's destructor. evaluate uses eval-style declaration instantiation (var/function bind in the realm's global environment and persist across calls; let/const bind in a fresh per-call lexical scope), and the callable boundary is implemented as wrapped function exotic objects that marshal arguments and results across realms and copy name/length. To support callable wrapped targets, InvokeCallable now routes callable non-function/non-class values (e.g. callable proxies) through the proxy apply hook.

ShadowRealm.prototype.importValue imports a module into the child realm — reusing the creating realm's module content provider so host file access resolves the same way, and the requested specifier resolves relative to the creating realm's entry path — then reads the requested export and resolves with GetWrappedValue of that binding. A failed import (resolution, parse, link, or evaluation throw) or a missing export rejects with a caller-realm TypeError. Because GocciaScript module loading is synchronous, the dynamic import runs inline and the caller-realm promise is settled before importValue returns; the .then reactions the caller attaches still run as microtasks.

Scope boundaries originally recorded here — child realms not exposing eval, and a per-realm Symbol.for registry — were later revisited for full built-ins/ShadowRealm test262 conformance; see ADR 0075.