endo icon indicating copy to clipboard operation
endo copied to clipboard

Propagating globals through child Compartments

Open mhofman opened this issue 3 years ago • 1 comments

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).

mhofman avatar Sep 29 '21 05:09 mhofman

I think we could move this in the icebox for now until we find a need for it?

mhofman avatar Feb 07 '22 23:02 mhofman