[JSC] Additional module loaders per global object, sharing linked module code between them
Lets a global object have more than one module loader. Creating another JSModuleLoader for a global object gives a loader with its own registry; importing through it runs the ordinary fetch / parse / link / evaluate pipeline and yields fresh module records — own environments, namespaces and evaluation state — while the global object's own loader (and any other loader) is untouched. Nothing in module linking or evaluation changes.
Changes
-
A module record (
AbstractModuleRecordand its subclasses, viaModuleAnalyzerand theSyntheticModuleRecord/WebAssemblyModuleRecordfactories), aModuleRegistryEntryand aModuleLoadingContextare constructed with theJSModuleLoaderthey belong to.JSModuleLoader::makeModuleis a member so the factories receive it; WebAssembly instances created through the JS API belong to the global object's loader. -
The load-pipeline continuations in
JSMicrotask.cppuse the loader carried by their context / registry entry instead ofglobalObject->moduleLoader(), so a load registers, provides, builds records and reports errors in the registry it was started on. -
import()passes its loader from the bytecode: every module environment binds a private@moduleLoaderto its record's loader (declared alongside@starNamespacein JS, synthetic and Wasm module environments; bound inAbstractModuleRecord::setModuleEnvironment), a loader's module scope may declare one, and the global object declares it as a static global bound to its own loader.import(x, opts)is emitted as@importModule(x, opts, deferred, @moduleLoader); module code, functions nested in it and directevalresolve the module's binding, everything else (scripts, indirect eval,Function-constructor code — which the spec's GetActiveScriptOrModule would attribute to a calling module) resolves the global's as aGlobalVar. -
The
jscshell's import hook honours itsJSModuleLoader*argument;$vm.createModuleLoader(bindings?, sharing?)/$vm.moduleLoaderImport()for tests. -
A record's
evaluate/import.metahost hooks and its prelinked-record lookups go through the record's own loader.
Sharing linked code between loaders (second commit)
A JSModuleRecord for a URL and source that another record in the same global object already linked reuses that record's ModuleProgramExecutable — and so the module's CodeBlocks, JIT code and its function declarations' FunctionExecutables — instead of linking its own from the cached unlinked code; environments, namespaces and evaluation state stay per record. For that, linked code no longer embeds the environment an imported binding lives in: every JSModuleEnvironment carries one import slot per import entry, and op_resolve_scope for a ModuleVar loads the exporting environment from the importing environment's slot (LLInt, baseline, LOL, DFG). A record's slots are filled from its own import resolution when its evaluation starts (every module it imports from has an environment by then); module code that runs earlier — a hoisted function called across an import cycle — finds an empty slot and the existing op_resolve_scope slow path fills it. Slots are ordered by the sorted local names of the single-binding imports, so their indices are a function of the source. DFG constant-folds a filled slot through the importing environment's symbol-table singleton exactly as it folds a ClosureVar scope, so single-instance code is unchanged after tier-up, and the existing symbol-table / singleton-function watchpoints de-specialise optimised code once a second environment exists. Linked code still embeds each imported binding's ScopeOffset in the exporting environment, which the exporting module's source text determines; so a record adopts a shared executable only when its imports resolve to modules with the same source text and binding names as the record the executable was created for (recorded on the executable at creation), and otherwise links its own, which later records are compared against. The global object keeps the current executable per (module key, module scope) in a WeakGCMap; an executable whose code was deleted (ScriptExecutable::clearCode) is not adopted. Bytecode cache format revision bumped.
Module scope. A loader is created with the scope its records' module environments are created in (JSModuleLoader::moduleScope): the global lexical environment for the global object's own loader (what initializeEnvironment used before), or, for an embedder-created loader, lexical environments ending in it, so identifiers the loader's modules leave free resolve there before the global scope. Because linked code embeds those variables' offsets and resolves them as closure variables where records of the same source under another loader resolve globals, records share an executable only when their loaders' module scopes have the same symbol tables, and an executable whose module scope is not the global lexical environment decodes or generates its own unlinked code instead of using the VM-wide one for that source (whose cached baseline code assumes one resolution per instruction).
With a single loader nothing changes after tier-up: there is one environment per module, the symbol-table singleton holds, and imported bindings fold to constants as before (LLInt and baseline pay one dependent load and an empty check per ModuleVar resolve). Once a second record runs the same executable, the shared optimized code is what JSC produces for any function closed over more than one activation: the singleton and function-declaration watchpoints fire once, imported and module-level bindings become loads instead of constants, and identity-keyed inline caches (e.g. on namespace objects) see one structure/cell per instance. That is the cost of running one copy of the code for all instances; it is paid once, when the second instance appears, not per instance.
Tests: JSTests/modules/module-loaders*.js cover per-loader state and import() affinity, cycles, module scopes (resolution, assignment, isolation, which loaders share code), one CodeBlock across loaders with no recompiles per instance, a dependency changing between loaders (fresh code, then shared again), and code deletion.