tc39 / proposal-module-declarations

JavaScript Module Declarations
https://tc39.es/proposal-module-declarations
MIT License
372 stars 7 forks source link

Linkage resolution of static module declarations #13

Open mhofman opened 3 years ago

mhofman commented 3 years ago

Considering the following example:

module uppercaseBlock {
  export function uppercase(string) {
    return string.toUpperCase();
  }
}

module workerBlock {
  import {uppercase} from uppercaseBlock;

  onmessage = function({data}) {
    postMessage(uppercase(data));
  }
}

let worker = new Worker(workerBlock, {type: "module"});
worker.onmessage = ({data}) => console.log(data);
worker.postMessage("daniel");

What would be sent to the worker so that it can resolve uppercaseBlock if static module fragment declarations are not linked.

mhofman commented 3 years ago

Reading https://github.com/tc39/proposal-module-fragments/issues/5#issuecomment-872182208, is the idea that the module declaration isn't linked until it is referenced? So in the example above, when it is referenced for the Worker constructor call, linkage information for the identifiers that are variables (uppercaseBlock in the example) would be built, and I suppose sent along to the worker?

However would a module instance be created at that time? E.g. in the example, if workerBlock also contained an import {foo} from 'foo.js';, would foo.js be fetched and parsed in the current context or only in the Worker context?

Somewhat related, when exactly are exported module blocks linked then evaluated? What happens in the following case:

// utils.js
export module countBlock {
  let i = 0;

  export function count() {
    i++;
    return i;
  }
}

// a.js
import {countBlock} from 'utils.js';
import {count} from countBlock;

export function countFromA() {
  return count();
}

// b.js
import {countBlock} from 'utils.js';
import {count} from countBlock;

export function countFromB() {
  return count();
}

// main.js
import {countFromA} from 'a.js';
import {countFromB} from 'b.js';

console.log(countFromA()); // 1
console.log(countFromB()); // I assume we want 2 here
kriskowal commented 3 years ago

I am reading this question as “How would this proposal interact with a module loader API, if we had a module loader API.” Specifically, I’m maintaining a prototype Compartments shim https://github.com/endojs/endo/blob/master/packages/ses/README.md#compartment, https://github.com/tc39/proposal-compartments#compartments.

There are two possible design directions.

  1. Static module records correspond to whole files and may subsume fragments
  2. Static module records correspond to fragments and need to capture additional metadata that translate block names to full module specifiers that in turn can be used to fetch those fragments.

I’m sure 1 is not desirable, since module and Module should be 1:1. I’m not sure 2 is feasible. It would certainly require an envelope around the text of the module, which would at least be undesirable, constraining fragments to have a different MIME type than ordinary modules and a lot of additional machinery to host them.

In either case, this proposal forces us to give fragments URL’s, and probably using the URL fragment to address the sub-resource fragment. This would in turn suggest that all module specifier namespaces need to be subsets of URL’s, although they already in practice do not work exactly like URL’s. There’s some tension regarding resolving the distinction between specifiers that cross scope or package boundaries and those that do not.

My inclination with the Compartments proposal was to design the loader API such that it was general enough to facilitate URL-like and CommonJS-like module specifier namespaces without imposing a hard constraint on the range of expressible module specifiers. Perhaps that is still possible, and I’d invite folks to propose solutions.

For virtualization, this would imply more API on StaticModuleRecord (perhaps simply Module since it is one-to-one with a module block) such that each SMR is a node in an SMR tree, recursively. For example, module.get('countBlock').

kriskowal commented 3 years ago

(My tentative conclusion is that module fragments could be nice for passing entry-points to workers, but should not be used for bundle formats and should not support sub-resource fragment inter-linkage. The weaker conclusion is that being able to pass entry fragments is not sufficiently useful to warrant the additional complexity to the language, particularly a module loader API.)

guybedford commented 3 years ago

My understanding from the incubator discussion was that the goal was to extend the static linking phase, in addition to normal linking, to also construct the module fragment linkage table. So even before instantiation of the outer code of the example, the full linkage of the fragments would be fully constructed.

kriskowal commented 3 years ago

@guybedford Okay, that would seem to suggest going down the first path. A module does not correspond to a StaticModuleRecord, but something else that indicates the source and some sort of path to the nested module block within that file. It would have to be a path, like #child#child#child in the case of treble nested named module blocks. This also implies that all module blocks would need unique, statically analyzable names, so they would not participate in the lexical namespace. That would suggest a parallel module lexical namespace that might be reflected in the ordinary lexical namespace in their declaration scope.