tc39 / proposal-compartments

Compartmentalization of host behavior hooks for JS
MIT License
122 stars 10 forks source link

Layer 3: Evaluators, how to inherit intrinsic? #73

Open Jack-Works opened 2 years ago

Jack-Works commented 2 years ago
const e = new Evaluator({ globalThis: {} })
e.eval("Math")
// object? ReferenceError?
e.eval("Module")
// equal to e.Module? ReferenceError?
e.eval("Evaluator")
// ?
Jamesernator commented 2 years ago

I feel like it would simpler to go with one of the previous ideas where there was an actual Global constructor that made new global objects. This means the host could just perform the population of the global object for the user i.e.:

// Creates a new global object with the appropriate
// intrinsics already attached
const g = new Global();
g.Array === Array; // true
const e = new Evaluator({ global: g });

e.eval(`Math`); // object Math { ... }

Customizing these globals wouldn't be particularly hard, just an Object.assign or similar:

const e = new Evaluator({
    global: Object.assign(new Global(), {
        myApi: () => { ... },
    }),
});

If there's desire for exotic behaviour (i.e. #38) one could just allow new Global() to take proxy traps:

const fakeDocument = new FakeDocument();
const g = new Global({
    // proxy hooks...
    get(global, prop, receiver) {

    },
});

I feel like the questions about e.eval("Evaluator") is a much wider problem than just inheriting, like the point of having say evaluator.Function !== Function is so that new Function("return someGlobal") has the right evaluator, but this becomes weird given that inherited intrinsics will have the original Function i.e.:

const g = new Global();
const e = new Evaluator({
    // the following problem is independent on this API shape
    global: g,
});
// Set the Function global
g.Function = e.Function;

// Multiple Function constructors floating around in the same evaluator
e.eval(`
    Function === Array.constructor; // false
`);
// And so code run inside the evaluator can still access parent globals fairly unrestrictedly
e.eval(`
    const OuterFunction = Array.constructor;
    const OuterModule = new OuterFunction("return Module");
    const module = new OuterModule(new ModuleSourceText(`
         // do thing in the parent evaluator's global scope
    `)):
`);

I don't see any obvious way to repair this if we allow multiple Function in particular to exist within the same evaluator.

Nevermind this, I thought .constructor was an own property on builtin functions, but it's actually just inherited from Function.prototype.constructor.

Jamesernator commented 2 years ago

Nevermind this, I thought .constructor was an own property on builtin functions, but it's actually just inherited from Function.prototype.constructor.

Actually no this is still a problem, Array.constructor can't be e.Function, as the outer and inner Array.constructor need to agree:

const g = CREATE_GLOBAL_SOMEHOW();
const e = new Evaluator({ global: g });
g.Function = e.Function;

// If this agrees:
Array === e.eval(`Array`); // true
// then so must
Array.constructor === e.eval(`Array.constructor`); // true
// hence Array.constructor is the outer Function
Function === e.eval(`Array.constructor`); // true

The only way for this not to agree would be if Function.prototype.constructor actually checked the caller's evaluator, although other than direct eval this is unlike simply calling a function (.prototype.[[Get]](...)) would usually be capable of doing.

kriskowal commented 2 years ago

My intent is that whomever creates the intrinsics is in a position to arrange the new intrinsics however they see fit, and that the common case would be:

const localThis = Object.create(globalThis);
const evaluators = new Evaluators(localThis);
Object.assign(localThis, evaluators);

Such that direct eval would work as-expected in evaluated code. This would be sufficient for the DSL usecase.

This is not very different from a Global constructor, except that it allows for the possibility of:

const evaluators = new Evaluators(globalThis, { importHook, importMeta });

Where the globalThis is object identical to the surrounding environment but import behavior is virtualized. I imagine this to be a common need as well. This would have the surprising but probably okay side-effect of disabling direct eval. That could of course be recovered if it’s actually needed, by binding eval lexically, as in:

new evaluators.Function('eval', 'text', 'eval(text)')(evaluators.eval, text);

I’m not strongly partial to either Global, Evaluators, or just lockdown Compartment. Any of these are sufficient for isolating dependencies or other guest programs.