endo
endo copied to clipboard
Propagating globals through child Compartments
Description of the problem
Overriding intrinsics using a Compartment's endowments is currently hazardous because it requires also overriding the Compartment
constructor inside the new compartment instance, so that code evaluated inside the compartment cannot use a nested compartment to retrieve the original intrinsics. Doing this is complicated by the fact that Compartment
constructors are technically unique per compartment.
The above is the approach taken by compartment-mapper in agoric-sdk.
The problem of overriding intrinsics is actually generalizable to propagating globals from one compartment to its child by default, while allowing one compartment to override the defaults for its own descendants. The SES shim has a mechanism planned for trusted shims, but it's not part of the standard Compartment API. Shimming capability is an integral part of the language, and the standardized Compartment API should not create unnecessary roadblocks.
Proposed solution
The solution can be inspired from the getIntrinsic
proposal. In that proposal a global getIntrinsic
would exist, allowing code to get the value of any intrinsic by its name. If a shim wants to be complete, it would update the global that it's shimming or fixing, and wrap the getIntrinsic
global to return the updated intrinsics it installed. Code that grabbed the getIntrinsic
before this wrapping wouldn't be affected by later changes, only further code execution.
For compartments, what matters is not the full set of intrinsics, but the shared values of the global object to propagate. And this would be exposed as a global itself, for example directly as a property: declare var propagatingGlobals: Frozen<{[primordialName: string]: any}>
. Exact name to be bikeshed (I'd love to name this sharedPrimordials
but @erights will disagree).
The Compartment
constructor would not query the current value of this global when creating a new instance, but would remember the value when the current compartment was created. However to override or extend the intrinsics of a new compartment (and its children), the constructor would perform a lookup on the endowments. E.g. with propagatingGlobals
:
const c = new Compartment({propagatingGlobals: {...propagatingGlobals, WeakMap: MyWeakMap, WeakSet: MyWeakSet}});
assert(c.evaluate(`(OtherWeakMap) => OtherWeakMap === WeakMap`)(MyWeakMap));
c.evaluate(`globalThis.propagatingGlobals = {...propagatingGlobals, WeakMap: 'foo', WeakSet: 'foo'}`);
const grandC = c.evaluate(`new Compartment()`);
assert(grandC.evaluate(`(OtherWeakMap) => OtherWeakMap === WeakMap`)(MyWeakMap));
Alternatives
Instead of its own global, this could be modeled as an extension of getIntrinsics
, either as a %PropagatingGlobals%
intrinsic which would have a value shaped like {[primordialName:string]: string}
aka a map of global name to intrinsic name, or a set of %PropagatingGlobals.PrimordialName%
if the "get-intrinsics" proposal has a mechanism for scoped enumeration.
The precedent of module map should also be considered. Module maps (currently not implemented but proposed in the Compartment API, see Moddable's implementation) are pure additions/overrides because they're relative to their parent. Thanks to that they don't need to be exposed. A case could be made that the shared global don't need reified either, and that propagating overrides/additions should be a dedicated Compartment
option. A compartment creator can figure out what the shared intrinsics are anyway by diffing the initial globals from the endowments. That however leaves the question of removal of propagated globals (should an undefined
value remove the global altogether or set its value to undefined
).
Non-reified propagation would also be valuable for the globalLexicals
option of Compartment
. However while the semantics of nested globalLexicals
are fairly clear (addition/override), that feature is usually used with global transforms, where the stacking semantics of independent transforms are not clear (what happens if a global transform attempts to modify the same part of the source).
I think we could move this in the icebox for now until we find a need for it?