proposal-compartments icon indicating copy to clipboard operation
proposal-compartments copied to clipboard

`Function("import(specifier)")`

Open nicolo-ribaudo opened this issue 2 years ago • 3 comments

The function constructor captures the active script or module when creating a function object, at step 15 of https://tc39.es/ecma262/#sec-ordinaryfunctioncreate.

This means that, given this a.js module:

Function("import('./b.js')")();

it will call the host hook with a.js as the referrer and ./b.js as the specifier.

I would thus expect it to call the importHook when running in a new Module already in layer 0. If instead it goes through the evaluators from layer 3, they should have a way to get the correct referrer module.

nicolo-ribaudo avatar Jun 27 '23 14:06 nicolo-ribaudo

Note that the current layer 0 spec already calls the importHook in this case, but as far as I remembered we so far assumed that it wouldn't do it.

nicolo-ribaudo avatar Jun 27 '23 14:06 nicolo-ribaudo

Honestly this seems like a benefit to evaluators as it means there doesn't need to be multiple copies of Function floating about the same realm.

i.e. In the current proposal the pattern would be to do this:

const evaluator = new Evaluator({
    globalThis: {
        ...globalThis,
        SOME_GLOBAL: "SOME_GLOBAL",
    },
});
evaluator.globalThis.Function = evaluator.Function;

evaluator.eval(`
   const fn = Function("return SOME_GLOBAL")
   console.log(fn()); // SOME_GLOBAL
`);

however in doing this we wind up with multiple copies of Function floating around in the same realm:

const evaluator = new Evaluator({ globalThis: { ...globalThis } });
evaluator.globalThis.Function = evaluator.Function;

evaluator.eval(`
   // Multiple Function constructors are floating around
   console.log(parseInt.constructor === Function); // false
`);

However with the existing behaviour we need not bother create a new Function at all, it'll just work when run inside the associated evaluator:

const evaluator = new Evaluator({
    globalThis: {
        ...globalThis.
        someGlobal: "SOME_GLOBAL",
    },
});

// With the current spec, this should just work
evaluator.eval(`
   const fn = Function("return SOME_GLOBAL")
   console.log(fn()); // SOME_GLOBAL
`);

// No Function discontinuity
evaluator.eval(`
   console.log(parseInt.constructor === Function); // true
`);

The only thing lost is the ability to do new evaluator.Function(...) from the outside, but this could be fixed easily by providing a evaluator.createFunction or similar that changes what evaluator context the created function belongs to.

Jamesernator avatar Jun 30 '23 06:06 Jamesernator

Frankly I think new Module(...) and eval should do the same so that we don't need to install these objects on the evaluator's globalThis at all. No power would actually be lost in such an approach, if we want to expose the outer eval/Function/Module we can just wrap it (just like we could do with import(...) today):

const evaluator = new Evaluator({ ...globalThis, SOME_GLOBAL: "SOME_GLOBAL" });

evaluator.eval(`
   // Would just work as eval would capture current script/module and get it's associated
   // evaluator
   console.log(eval.call(null, "SOME_GLOBAL"));
`);

// But we can still customize eval by wrapping with a closure that ensures the nearest script/module record is our current level
const evaluator2 = new Evaluator({
   globalThis: {
        ...globalThis, 
        eval: (arg) => eval.call(null, arg),
        SOME_GLOBAL: "SOME_GLOBAL",
   },
});

globalThis.SOME_GLOBAL = "NOT_IN_EVALUATOR";

evaluator2.eval(`
    console.log(eval.call(null, "SOME_GLOBAL")); // NOT_IN_EVALUATOR
`);

Jamesernator avatar Jun 30 '23 06:06 Jamesernator