Open kriskowal opened 2 years ago
I'm still very confused why we'd want to expose, pre-evaluation, whether a module references import.meta
or not. Why wouldn't we want to deterministically assume every module needs import.meta
?
Because for most modules it is trivially statically apparent that they do not, and there is benefit to being able to avoid creating it for the vast majority of modules that do not need it.
Can't it be represented by, instead of an object, a thunk? ie, a function that's called the first time it's needed, creates and returns the object, and cached forever after that?
Can't it be represented by, instead of an object, a thunk? ie, a function that's called the first time it's needed, creates and returns the object, and cached forever after that?
How does this differ from Lazy (option 3)?
ah, i suppose that's the same thing then, hmm :-/
the information i'd prefer to avoid exposing is "whether a module uses import.meta
" - otherwise that becomes a part of the module's API, and starting to use it, or no longer using it, becomes a breaking change - where currently, it's not.
Lazy has the problem that user code then interleaves at the point of evaluating the import.meta
expression, which is at least surprising, whereas eager only interleaves when the module is created, which is a user-code-interleaving point anyway.
As for API fragility, eager makes the dependency only on that static text of the module, whereas lazy makes it depend on what happens dynamically at runtime, which can be data dependent.
Right - but the static text of the module is, just like the source of a function, not supposed to be reliably observable from without, for the same reasons.
I’m sympathetic to both interleaving (Lazy) and breaking change hazards (Prior, Eager). Can I ask @erights and @ljharb to contrive concrete examples?
The interleaving hazard takes the form below, where the source is able to tease importMetaHook
into calling on its stack with some bad effect. That might involve throwing from the hook or catching around import.meta
in the source, or stack inspection. We may have to choose whether to protect the invariant that import.meta
cannot currently throw in any real environment.
const s = new ModuleSource(/* ... */);
const m = new Module(s, { importMetaHook() { /* ... */ } });
The breaking change hazard will always consist of a pair of versions of the same module source, one with and the other without import.meta
. I presume a game rule for this kind of hazard is that working code must turn into broken code in the presence of a defect-free virtualized module system. For a “Prior” style module system to be correct, it must provide an importMeta
in new Module(source, { importMeta })
if source.needsImportMeta
is true, for whatever contractual obligations exist between the source and the host for import.meta
(which vary between hosts).
I imagine there’s another class of hazard, where something about this design increases the probability of a defect in a virtualized module system. That’s worth talking about too.
the information i'd prefer to avoid exposing is "whether a module uses import.meta" - otherwise that becomes a part of the module's API, and starting to use it, or no longer using it, becomes a breaking change - where currently, it's not.
Right - but the static text of the module is, just like the source of a function, not supposed to be reliably observable from without, for the same reasons.
How is import.meta
really different in this regard than from say an import ... from
statement? Previously one could not directly observe imports from a module, however using the same module via Module
will ultimately expose those previously hidden things.
Like a module could change it's imports and a loader would need to be prepared to handle that, so why would import.meta
be more breaking in comparison?
That’s a really good point, and a wider potential concern for this proposal.
From a high level I'd be fine with either a lazy api or passing the object into the constructor. A hook that is just called immediately in the constructor seems a bit pointless/confusing in comparison. I'd also be against trying to be super clever about whether a module contains import.meta
, 3rd parties can do their own analysis if they really care about that. I will note that node's vm api uses the lazy hook api already, and while that api is currently marked experimental (no stability guarantee) it would be nice to avoid changes where possible.
A hook, even if called immediately, allows to properly replicate the behavior of every host. Just passing an object to the constructor doesn't allow, for example, to freeze import.meta
or to define its [[Prototype]]
.
At the September 21, 2022 SES Strategy call we discussed three options for timing the construction of
import.meta
for a virtual module.needsImportMeta
property that indicates whether the source contains AST nodes for eitherimport.meta
oreval
, and the module handler passed toModule(source, handler)
must have a pre-builtimportMeta
property that will be used by that module if it ever evaluates animport.meta
expression.Module(source, handler)
constructor that it must callhandler.importMetaHook()
to construct animport.meta
and capture it on the Module instance’s [[ImportMeta]] internal slot before the constructor returns.importMetaHook()
that will be called upon the first evaluation of animport.meta
expression, capturing [[ImportMeta]] on the Module instance internal slot for every subsequent evaluation ofimport.meta
.Commentary:
Lazy may or may not pose either a reentrance hazard. The virtual module is vulnerable to faulty arrangement by whomever constructs the
Module
regardless, but the question is whether there is undue risk if we allow the host to execute arbitrary code in the context of the virtual module.In particular, of the participants of the SES call, @nicolo-ribaudo and @erights agree that we take as a guiding principle in this design that hosts and virtual hosts should be given equivalent power, so if a host may execute on the stack of
import.meta
and also may have side-effects as a consequence of evaluatingimport.meta
, it stands to reason that a virtual host should be allowed the same. However, if 262 constrains hosts to have no observable side-effects uponimport.meta
, virtual hosts mustn’t be able to execute arbitrary code at that time.Whether the import meta object is constructed at all is not equivalent between these two proposals. There are cases where eager is necessarily over-eager, since it must speculate on whether
eval
will result in evaluation ofimport.meta
. We doubt that the difference in behavior will be germane to the performance motivations for lazy construction on Node.js. There will be very few cases where a module uses eitherimport.meta
oreval
, much fewer for both.The choice between eager and prior construction is a matter of developer ergonomics.