Skip to content

Conversation

nicolo-ribaudo
Copy link
Member

This documents explains how I think module blocks should interact with the evaluation cache of modules. Differences between the proposal spec and this document should be considered spec bugs.

A few comments:

  • (1) is already true since ES2015
  • (2) is not yet specified
  • (3), (4), (5) and (6) are already specified
  • (7) needs to be done in the HTML spec

// file.js
export const check = {};
```
3. Importing the same module block twice _from the same Realm_, even if from different modules, results in a single evaluation:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
3. Importing the same module block twice _from the same Realm_, even if from different modules, results in a single evaluation:
3. Importing the same module block twice _from the same Realm_, even if from different parent modules, results in a single evaluation:

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we can have syntactically nested modules we should come up with two different words for "parent" that represent a parent in the imports linking graph and a parent in the AST.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m confused; there shouldn’t be any such concept as “parent”, since a module can be imported many times from many different places, including itself. A module just has 0-N importers, which may be static or dynamic. (ref: node’s CJS module.parent which is a massive misfeature for this reason)

Copy link
Member Author

@nicolo-ribaudo nicolo-ribaudo Jul 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup you are right, the word I needed is "importer".

@guybedford I'm marking this conversation as "resolved", since "importing from different importer modules" does not add anything to "importing from different modules".

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good - importer or importing works for me. Perhaps we can update the former referrer terminology here as well.


These invariants cannot be enforced by ecma262, since it doesn't define how cloning works and how string specifiers are resolved. They will be respected by the HTML integration, and the champion group suggests that hosts that have a similar modules model could follow these patterns.

7. When serializing&deserializing a module block, the "referrer" used as the base to resolve string specifiers should be kept the same:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
7. When serializing&deserializing a module block, the "referrer" used as the base to resolve string specifiers should be kept the same:
7. When serializing and deserializing a module block, the "referrer" used as the base to resolve string specifiers should be kept the same:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can specify this in ECMA-262 and without needing to specify serialization or string specifier resolution.

Basically, if we can relate the concept of a "compiled module record" as existing across realms, then a module block can be associated against its parent source text compiled module record both of which can be fully defined ECMA-262 constructs. The resolution idempotence is then that:

If compiled module record b is a module block, which is defined within a source text module compiled module record s, then for any realm r and specifier specifier, HostResolveImportedModule(specifier, b, r) === HostResolveImportedModule(specifier, s, r) where HostResolveImportedModule is updated to itself return a compiled module record which is then singly instanced within the realm. This is a fully-well defined invariant definition in ECMA-262. Separately, specs like compartments can then tackle the problem of multiple instancing of a compiled module within the same realm and how to relax the constraint again.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current thinking, between @caridy and myself, is that we will bring back you ModuleInstance sketch, such that each module instance has its own memoization for its importHook. We would introduce a _Virtual Module Record_ that manages the lifecycle and contains a reference to a Static Module Record which, as you describe, would be immutable and safely shared between realms in the same process. Source Text Static Module Record would initially be the only concrete implementation.

That is to say, we would not alter per-realm behavior at all and that we would enforce invariance on a per-instance basis, recovering sufficient idempotence in aggregate by induction over the import hook.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes sense that for arbitrary resolution hooks an instance model is the idempotence model.

But it's important to note that these are two separate resolution models - one is the host default resolution model, and one is a custom resolution model.

One nice thing about maintaining the concept of a singular compartment, is that the compartment itself forms the unit of idempotence, whereas with a full instance model that boundary is no longer clear.

Therefore different constraints might apply at the uninstrumented host level, the compartment level, or the arbitrary instance level (from most constrainted idempotence to the least).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regardless how the above works out, I think it makes sense for the host contraints for the default resolution model, and an instrumented resolution model to be clearly different resolution constraint scenarios.

Copy link
Member Author

@nicolo-ribaudo nicolo-ribaudo Jul 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically, if we can relate the concept of a "compiled module record" as existing across realms, then a module block can be associated against its parent source text compiled module record both of which can be fully defined ECMA-262 constructs. The resolution idempotence is then that:

How much "global" can something be? Can something also exist across agents? For example, web workers are different agents from the main one.
Also, I'm not convinced that it will work. When you pass a module block to a worker there is some serialization going on, and this serialization could "detach" a module block from its parents if the slot where it's stored is not properly carried to the other side.

Regardless how the above works out, I think it makes sense for the host contraints for the default resolution model, and an instrumented resolution model to be clearly different resolution constraint scenarios.

I disagree. If we impose a constraint on the host it's because code needs to rely on those guarantees and thus they should also be enforced in compartments (or whatever loader we will have). If a guarantee is deemed not necessary, then we can relax the host constraint.

EDIT: For this example, yes I agree. I will clarify that this point is "the default behavior in HTML & servers should be like this"

Copy link
Member Author

@nicolo-ribaudo nicolo-ribaudo Jul 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I think that the proposed constraint is ok as-is and doesn't need to be relaxed for compartments/loaders. structuredClone will keep the same referrer, but every compartment can map a (referrer, specifier) to something different. This is not visible in this proposal, because currently there is 1 compartment per Realm: the cache map will need to be moved from the Realm to the Compartment.

With the alternative "compartments" design mentioned by @kriskowal, you can explicitly (not implicitly via structuredClone) create a different ModuleInstance from the same "module source" but with a different "referrer URL".

@nicolo-ribaudo nicolo-ribaudo force-pushed the module-caching-document branch from d52f8e3 to bfd8003 Compare July 4, 2022 12:29
@nicolo-ribaudo nicolo-ribaudo force-pushed the module-caching-document branch from ba150b0 to ff13c9f Compare July 4, 2022 16:23
It follows that when importing two copies of the same module block from the same realm, transitive dependencies should only be evaluated once:
```js
globalThis.count = 0;
const mod = module { export { check } from "./foo.js"; };
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const mod = module { export { check } from "./foo.js"; };
const mod = module { export { check } from "./file.js"; };

// main.js
const mod1 = module { export { check } from "./file.js"; };
const mod2 = module { export { check } from "./file.js"; };
const { check: c2 } = await import(mod1);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const { check: c2 } = await import(mod1);
const { check: c1 } = await import(mod1);

```js
const mod = module { globalThis.modEvaluated = true; };
const realm = createLegacyRealm(); // [1]
await realm.eval(`s => import(s)`)(mod);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this one is very interesting. since mod instanceof Module, what happen if I do import(mod) before or after realm.eval()? To be more clear, this is the question:

  • import(mod) !== realm.eval('s => import(s)')(mod)

Copy link

@caridy caridy Nov 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be clear, the inconsistency here with my mental model might be coming from the fact that you might do new Module(moduleSource) twice, and it produces two different modules, while here, it is really about the module instance. And to be a lot more specific, does it affect the way you have to cache things inside a module hook? if instead of a module block, you use a programmatic module instance using new ModuleSource, and you provide a hook for resolution, which resolves to other modules instances, what happen when you import the module instance from another legacy realm? And what happen when a module instance exists but wasn't created from source?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wrote this document a while ago and I haven't checked if it's still correct. The proposal initially cached modules based on the realm of the import call instead of the one of the module expression.

This example is probably just outdated, I have to go through the document and review it. When in doubt, please check the proposal spec text!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants