From ff6929f3eac0afa88640b82a2ba0a07223584a9e Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Tue, 15 Sep 2026 19:30:19 +0000 Subject: [PATCH] [JSC] Module linking: import slots without sorting; nothing resolved to compare records until there are two Since #522 a module environment has an import slot per import binding, and records of the same source in different loaders can share a ModuleProgramExecutable. Three parts of that ran for every record of every loader, whether or not any other record ever shared its code: - importSlotNames() collected, atomized and sorted the record's import local names (the slot count needs it, so every record paid at instantiation), and importSlotIndex() binary-searched them by string at each import access site when a CodeBlock links, after resolveImport() had already looked the import up. - getOrMakeExecutable() resolved every import of the record to build the bindings it compares executables by. - evaluate() resolved every import again to fill the slots before running. Slots are now numbered without comparing names: a prelinked record's slot is the import's position in the graph's imports of the module (already in a fixed order; a namespace import's position is a slot nothing uses), any other record's is its position among the import entries, which iterate in insertion order, with a name -> index map for lookups. ImportedBinding carries the slot, so records only share an executable when they number every binding alike. JSScope::abstractAccess resolves an import and gets its slot in one step (JSModuleRecord::resolveImportWithSlot); for a prelinked record that is a single lookup in the graph, through a by-entry tryResolveImportPrelinked next to the by-name one, as tryResolveExportPrelinked already has. getOrMakeExecutable() no longer resolves anything to make an executable. The executable remembers, weakly, the record it was linked for; when another record for the same key, source and module scope turns up, JSModuleRecord::resolvesImportsLike() computes that first record's bindings then, once, and keeps them on the executable, which is all later comparisons need (the first record may be collected while others go on sharing). If it is collected before there is a second record there is nothing to compare with: the second links its own code and later ones share with that. A prelinked record's bindings leave out every import its graph resolved to a binding of another of its modules when the record itself resolves the import to that binding of that module's record (tryResolveImportPrelinked's answer, which is also what its code links against): that is the same variable of the same source for every record of the graph module that resolves it so, and the executable notes which graph module it was linked for, so only such records are compared with it. What is left to compare is what the graph could not answer, and anything a record resolved some other way. evaluate() fills the slots up front only for an executable that is shared; code only one record runs fills them on first use, which every tier already handles. JSTests/modules/import-slots-*.js cover import bindings of every kind on the global object's own loader and across additional loaders; JSTests/wasm/modules/js-wasm-cycle-loaders.js an exporter that has no environment yet when its importer links. --- .../modules/import-slots-aliases-shadowing.js | 12 + JSTests/modules/import-slots-assign.js | 15 ++ JSTests/modules/import-slots-basic.js | 20 ++ JSTests/modules/import-slots-cycles.js | 20 ++ JSTests/modules/import-slots-default-forms.js | 12 + .../modules/import-slots-dynamic-import.js | 19 ++ JSTests/modules/import-slots-errors.js | 21 ++ JSTests/modules/import-slots-forms.js | 31 +++ .../modules/import-slots-late-first-use.js | 16 ++ JSTests/modules/import-slots-live-bindings.js | 17 ++ .../import-slots-loaders-concurrent.js | 30 +++ .../import-slots-loaders-cycles-tla.js | 33 +++ .../modules/import-slots-loaders-errors.js | 27 +++ ...-slots-loaders-first-instance-collected.js | 27 +++ JSTests/modules/import-slots-loaders-gc.js | 21 ++ JSTests/modules/import-slots-loaders-json.js | 23 ++ .../import-slots-loaders-late-first-use.js | 19 ++ JSTests/modules/import-slots-loaders-many.js | 19 ++ JSTests/modules/import-slots-loaders-mixes.js | 36 +++ JSTests/modules/import-slots-loaders-order.js | 44 ++++ ...ort-slots-loaders-share-before-evaluate.js | 30 +++ JSTests/modules/import-slots-many.js | 18 ++ JSTests/modules/import-slots-names.js | 14 ++ JSTests/modules/import-slots-namespace-mix.js | 18 ++ JSTests/modules/import-slots-tla.js | 9 + JSTests/modules/import-slots-unused.js | 14 ++ JSTests/modules/import-slots/aliases.js | 5 + JSTests/modules/import-slots/assign.js | 17 ++ JSTests/modules/import-slots/code.js | 4 + JSTests/modules/import-slots/cycle-a.js | 7 + JSTests/modules/import-slots/cycle-b.js | 10 + JSTests/modules/import-slots/data.json | 1 + .../import-slots/default-anonymous-class.js | 1 + .../default-anonymous-function.js | 1 + .../import-slots/default-expression.js | 1 + JSTests/modules/import-slots/default-forms.js | 7 + .../import-slots/default-named-function.js | 3 + JSTests/modules/import-slots/forms.js | 20 ++ JSTests/modules/import-slots/gate-dep.js | 8 + JSTests/modules/import-slots/gate-main.js | 12 + JSTests/modules/import-slots/globals-named.js | 2 + .../modules/import-slots/imports-conflict.js | 2 + .../modules/import-slots/imports-missing.js | 2 + JSTests/modules/import-slots/json-user.js | 5 + JSTests/modules/import-slots/late.js | 17 ++ JSTests/modules/import-slots/live-aliases.js | 2 + JSTests/modules/import-slots/loader-main.js | 14 ++ JSTests/modules/import-slots/many-0.js | 17 ++ JSTests/modules/import-slots/many-1.js | 17 ++ JSTests/modules/import-slots/many-2.js | 17 ++ JSTests/modules/import-slots/many-3.js | 17 ++ JSTests/modules/import-slots/many-4.js | 17 ++ JSTests/modules/import-slots/many-5.js | 17 ++ JSTests/modules/import-slots/many-6.js | 17 ++ JSTests/modules/import-slots/many-7.js | 17 ++ JSTests/modules/import-slots/many.js | 12 + JSTests/modules/import-slots/mix-first.js | 5 + JSTests/modules/import-slots/mix-last.js | 6 + JSTests/modules/import-slots/mix-middle.js | 7 + JSTests/modules/import-slots/names.js | 6 + JSTests/modules/import-slots/no-imports.js | 4 + .../modules/import-slots/only-namespaces.js | 4 + .../modules/import-slots/reexport-chain.js | 2 + .../modules/import-slots/reexport-named.js | 4 + JSTests/modules/import-slots/reexport-star.js | 3 + JSTests/modules/import-slots/second.js | 4 + JSTests/modules/import-slots/self.js | 5 + JSTests/modules/import-slots/shadow.js | 9 + JSTests/modules/import-slots/star-conflict.js | 2 + JSTests/modules/import-slots/third.js | 3 + JSTests/modules/import-slots/tla-dep.js | 4 + JSTests/modules/import-slots/tla-main.js | 7 + JSTests/modules/import-slots/unused.js | 7 + JSTests/modules/import-slots/values.js | 9 + JSTests/wasm/modules/js-wasm-cycle-loaders.js | 24 ++ .../runtime/AbstractModuleRecord.cpp | 11 +- .../runtime/AbstractModuleRecord.h | 1 + .../JavaScriptCore/runtime/JSModuleRecord.cpp | 227 ++++++++++++++---- .../JavaScriptCore/runtime/JSModuleRecord.h | 23 +- Source/JavaScriptCore/runtime/JSScope.cpp | 7 +- .../runtime/ModuleProgramExecutable.cpp | 29 ++- .../runtime/ModuleProgramExecutable.h | 35 ++- 82 files changed, 1232 insertions(+), 70 deletions(-) create mode 100644 JSTests/modules/import-slots-aliases-shadowing.js create mode 100644 JSTests/modules/import-slots-assign.js create mode 100644 JSTests/modules/import-slots-basic.js create mode 100644 JSTests/modules/import-slots-cycles.js create mode 100644 JSTests/modules/import-slots-default-forms.js create mode 100644 JSTests/modules/import-slots-dynamic-import.js create mode 100644 JSTests/modules/import-slots-errors.js create mode 100644 JSTests/modules/import-slots-forms.js create mode 100644 JSTests/modules/import-slots-late-first-use.js create mode 100644 JSTests/modules/import-slots-live-bindings.js create mode 100644 JSTests/modules/import-slots-loaders-concurrent.js create mode 100644 JSTests/modules/import-slots-loaders-cycles-tla.js create mode 100644 JSTests/modules/import-slots-loaders-errors.js create mode 100644 JSTests/modules/import-slots-loaders-first-instance-collected.js create mode 100644 JSTests/modules/import-slots-loaders-gc.js create mode 100644 JSTests/modules/import-slots-loaders-json.js create mode 100644 JSTests/modules/import-slots-loaders-late-first-use.js create mode 100644 JSTests/modules/import-slots-loaders-many.js create mode 100644 JSTests/modules/import-slots-loaders-mixes.js create mode 100644 JSTests/modules/import-slots-loaders-order.js create mode 100644 JSTests/modules/import-slots-loaders-share-before-evaluate.js create mode 100644 JSTests/modules/import-slots-many.js create mode 100644 JSTests/modules/import-slots-names.js create mode 100644 JSTests/modules/import-slots-namespace-mix.js create mode 100644 JSTests/modules/import-slots-tla.js create mode 100644 JSTests/modules/import-slots-unused.js create mode 100644 JSTests/modules/import-slots/aliases.js create mode 100644 JSTests/modules/import-slots/assign.js create mode 100644 JSTests/modules/import-slots/code.js create mode 100644 JSTests/modules/import-slots/cycle-a.js create mode 100644 JSTests/modules/import-slots/cycle-b.js create mode 100644 JSTests/modules/import-slots/data.json create mode 100644 JSTests/modules/import-slots/default-anonymous-class.js create mode 100644 JSTests/modules/import-slots/default-anonymous-function.js create mode 100644 JSTests/modules/import-slots/default-expression.js create mode 100644 JSTests/modules/import-slots/default-forms.js create mode 100644 JSTests/modules/import-slots/default-named-function.js create mode 100644 JSTests/modules/import-slots/forms.js create mode 100644 JSTests/modules/import-slots/gate-dep.js create mode 100644 JSTests/modules/import-slots/gate-main.js create mode 100644 JSTests/modules/import-slots/globals-named.js create mode 100644 JSTests/modules/import-slots/imports-conflict.js create mode 100644 JSTests/modules/import-slots/imports-missing.js create mode 100644 JSTests/modules/import-slots/json-user.js create mode 100644 JSTests/modules/import-slots/late.js create mode 100644 JSTests/modules/import-slots/live-aliases.js create mode 100644 JSTests/modules/import-slots/loader-main.js create mode 100644 JSTests/modules/import-slots/many-0.js create mode 100644 JSTests/modules/import-slots/many-1.js create mode 100644 JSTests/modules/import-slots/many-2.js create mode 100644 JSTests/modules/import-slots/many-3.js create mode 100644 JSTests/modules/import-slots/many-4.js create mode 100644 JSTests/modules/import-slots/many-5.js create mode 100644 JSTests/modules/import-slots/many-6.js create mode 100644 JSTests/modules/import-slots/many-7.js create mode 100644 JSTests/modules/import-slots/many.js create mode 100644 JSTests/modules/import-slots/mix-first.js create mode 100644 JSTests/modules/import-slots/mix-last.js create mode 100644 JSTests/modules/import-slots/mix-middle.js create mode 100644 JSTests/modules/import-slots/names.js create mode 100644 JSTests/modules/import-slots/no-imports.js create mode 100644 JSTests/modules/import-slots/only-namespaces.js create mode 100644 JSTests/modules/import-slots/reexport-chain.js create mode 100644 JSTests/modules/import-slots/reexport-named.js create mode 100644 JSTests/modules/import-slots/reexport-star.js create mode 100644 JSTests/modules/import-slots/second.js create mode 100644 JSTests/modules/import-slots/self.js create mode 100644 JSTests/modules/import-slots/shadow.js create mode 100644 JSTests/modules/import-slots/star-conflict.js create mode 100644 JSTests/modules/import-slots/third.js create mode 100644 JSTests/modules/import-slots/tla-dep.js create mode 100644 JSTests/modules/import-slots/tla-main.js create mode 100644 JSTests/modules/import-slots/unused.js create mode 100644 JSTests/modules/import-slots/values.js create mode 100644 JSTests/wasm/modules/js-wasm-cycle-loaders.js diff --git a/JSTests/modules/import-slots-aliases-shadowing.js b/JSTests/modules/import-slots-aliases-shadowing.js new file mode 100644 index 0000000000000..8dc28650cff5e --- /dev/null +++ b/JSTests/modules/import-slots-aliases-shadowing.js @@ -0,0 +1,12 @@ +import { shouldBe } from "./resources/assert.js"; +import * as aliases from "./import-slots/aliases.js"; +import * as shadow from "./import-slots/shadow.js"; + +for (let i = 0; i < testLoopCount; ++i) { + shouldBe(aliases.read(), `${i},${i},${i},${i},true`); + aliases.increment(); + shouldBe(shadow.read("parameter"), "parameter,second:second,third:first"); + shouldBe(shadow.inner(), "second:first,local"); + shouldBe(shadow.globals(), "string,not the global Map,NaN-like,function"); +} +shouldBe(typeof Map, "function"); diff --git a/JSTests/modules/import-slots-assign.js b/JSTests/modules/import-slots-assign.js new file mode 100644 index 0000000000000..a10e708d372f2 --- /dev/null +++ b/JSTests/modules/import-slots-assign.js @@ -0,0 +1,15 @@ +import { shouldBe, shouldThrow } from "./resources/assert.js"; +import * as assign from "./import-slots/assign.js"; +import { count } from "./import-slots/values.js"; + +// An import is not assignable from the importing module, whatever it is bound to. +const readonly = "TypeError: Attempted to assign to readonly property."; +for (let i = 0; i < testLoopCount; ++i) { + for (const name of ["assign", "increment", "compound", "destructuring", "objectDestructuring", "forIn", "forOf", "assignFunction", "assignDefault", "assignNamespace"]) + shouldThrow(assign[name], readonly); + shouldThrow(assign.assignNamespaceProperty, readonly); + assign.logical(); + shouldBe(assign.read(), 0); + shouldBe(count, 0); + shouldBe(assign.mutateObject(), true); +} diff --git a/JSTests/modules/import-slots-basic.js b/JSTests/modules/import-slots-basic.js new file mode 100644 index 0000000000000..13c08fe1cf99a --- /dev/null +++ b/JSTests/modules/import-slots-basic.js @@ -0,0 +1,20 @@ +import { shouldBe } from "./resources/assert.js"; +import defaultFunction, { count, increment, set, object, constant, variable, declared, Klass } from "./import-slots/values.js"; +import { first, second } from "./import-slots/second.js"; +import { first as thirdFirst, third } from "./import-slots/third.js"; +import thirdDefault from "./import-slots/third.js"; + +// Every kind of export, read at the top level and from functions. +shouldBe(JSON.stringify([count, constant, variable, declared(), Klass.tag, defaultFunction(), object.tag]), `[0,42,"var","declared","class","default","object"]`); +shouldBe([first, second, thirdFirst, third, thirdDefault.tag].join(), "second:first,second:second,third:first,third:third,third:default"); + +function read() { return count; } +for (let i = 0; i < testLoopCount; ++i) { + shouldBe(increment(), i + 1); + shouldBe(read(), i + 1); + shouldBe(count, i + 1); +} +set(-1); +shouldBe(read(), -1); +shouldBe(typeof increment, "function"); +shouldBe(typeof count, "number"); diff --git a/JSTests/modules/import-slots-cycles.js b/JSTests/modules/import-slots-cycles.js new file mode 100644 index 0000000000000..90a597c06ffbb --- /dev/null +++ b/JSTests/modules/import-slots-cycles.js @@ -0,0 +1,20 @@ +import { shouldBe } from "./resources/assert.js"; +import { sawFromB, bValueSeen, viaB, setA, aValue } from "./import-slots/cycle-a.js"; +import { earlyError, readA, b } from "./import-slots/cycle-b.js"; +import { read, bump, value } from "./import-slots/self.js"; + +// cycle-b is evaluated first: it can call cycle-a's hoisted function but not read its `let`. +shouldBe(sawFromB, "b:a()"); +shouldBe(bValueSeen, "bValue"); +shouldBe(viaB, "a"); +shouldBe(earlyError, "ReferenceError: Cannot access 'aValue' before initialization."); +for (let i = 0; i < testLoopCount; ++i) { + setA(i); + shouldBe(readA(), i); + shouldBe(aValue, i); + shouldBe(b(), "b:a()"); +} +// A module that imports itself. +shouldBe(JSON.stringify(read()), "[1,1,true]"); +shouldBe(bump(), 2); +shouldBe(JSON.stringify([read(), value]), "[[2,2,true],2]"); diff --git a/JSTests/modules/import-slots-default-forms.js b/JSTests/modules/import-slots-default-forms.js new file mode 100644 index 0000000000000..81f39ab7a33e4 --- /dev/null +++ b/JSTests/modules/import-slots-default-forms.js @@ -0,0 +1,12 @@ +import { shouldBe } from "./resources/assert.js"; +import { read, rename, named } from "./import-slots/default-forms.js"; +import theDefault from "./import-slots/default-named-function.js"; + +shouldBe(read(), "default,anonymous function,default,42,named,true,42"); +shouldBe(named(), "named"); +shouldBe(theDefault(), "named"); +rename(); +// `export default function named` exports the variable, so the reassignment is visible both ways. +shouldBe(read(), "default,anonymous function,default,42,replaced,true,42"); +shouldBe(named(), "replaced"); +shouldBe(theDefault(), "replaced"); diff --git a/JSTests/modules/import-slots-dynamic-import.js b/JSTests/modules/import-slots-dynamic-import.js new file mode 100644 index 0000000000000..dbacab245fef0 --- /dev/null +++ b/JSTests/modules/import-slots-dynamic-import.js @@ -0,0 +1,19 @@ +import { shouldBe } from "./resources/assert.js"; +import { count, increment, object } from "./import-slots/values.js"; +import * as staticNamespace from "./import-slots/values.js"; + +const namespace = await import("./import-slots/values.js"); +shouldBe(namespace === staticNamespace, true); +shouldBe(namespace.object === object, true); +for (let i = 1; i <= testLoopCount; ++i) { + namespace.increment(); + shouldBe(count, i); + shouldBe(namespace.count, i); + shouldBe(staticNamespace.count, i); +} +shouldBe(namespace.increment === increment, true); +// Imported for the first time dynamically, after this module started running. +const late = await import("./import-slots/late.js"); +shouldBe(late.sometimes(true), "42,0,third:third"); +const again = await import("./import-slots/late.js"); +shouldBe(again === late, true); diff --git a/JSTests/modules/import-slots-errors.js b/JSTests/modules/import-slots-errors.js new file mode 100644 index 0000000000000..35c72db2249a3 --- /dev/null +++ b/JSTests/modules/import-slots-errors.js @@ -0,0 +1,21 @@ +import { shouldBe } from "./resources/assert.js"; +import { count, increment } from "./import-slots/values.js"; + +async function rejection(specifier) { + try { + await import(specifier); + } catch (error) { + return error instanceof SyntaxError; + } + return "loaded"; +} +// Linking fails the same way every time, and leaves the modules it shares with others usable. +for (let i = 0; i < 3; ++i) { + shouldBe(await rejection("./import-slots/imports-missing.js"), true); + shouldBe(await rejection("./import-slots/imports-conflict.js"), true); +} +shouldBe(increment(), 1); +shouldBe(count, 1); +const star = await import("./import-slots/star-conflict.js"); +shouldBe(JSON.stringify(Object.keys(star).sort()), `["bump","live","second","third"]`); +shouldBe("first" in star, false); diff --git a/JSTests/modules/import-slots-forms.js b/JSTests/modules/import-slots-forms.js new file mode 100644 index 0000000000000..9d416d337685d --- /dev/null +++ b/JSTests/modules/import-slots-forms.js @@ -0,0 +1,31 @@ +import { shouldBe } from "./resources/assert.js"; +import * as forms from "./import-slots/forms.js"; + +async function drain(iterable) { const values = []; for await (const value of iterable) values.push(value); return values; } +for (let i = 0; i < testLoopCount; ++i) { + forms.set(i); + shouldBe(forms.arrow(), i); + shouldBe(forms.nested(), i); + shouldBe(forms.viaEval(), i); + shouldBe(forms.viaIndirectEval(), "undefined"); + shouldBe(forms.viaFunctionConstructor(), "undefined"); + shouldBe(forms.defaultParameter(), i); + shouldBe(forms.defaultParameter("given"), "given"); + shouldBe(forms.withTypeof(), "number,number,object,object"); + shouldBe(forms.WithStatic.read(), i); + shouldBe(new forms.WithStatic().accessor, i); + shouldBe(forms.objectLiteral.accessor, i); + shouldBe(forms.objectLiteral.method(), i); + shouldBe(forms.destructure(), `${i},${i}`); + shouldBe(forms.labelled(), i); + shouldBe(forms.templated(), `${i}:42`); + shouldBe(forms.optional(), "object,object"); + shouldBe(forms.inTryFinally(), i); + shouldBe(forms.arrow(), i + 1); +} +shouldBe(forms.WithStatic.captured, 0); +shouldBe(forms.objectLiteral.computed42, 0); +forms.set(10); +shouldBe(JSON.stringify([...forms.generator()]), "[10,11]"); +shouldBe(await forms.asynchronous(), 11); +shouldBe(JSON.stringify(await drain(forms.asyncGenerator())), "[11]"); diff --git a/JSTests/modules/import-slots-late-first-use.js b/JSTests/modules/import-slots-late-first-use.js new file mode 100644 index 0000000000000..27b4b96ad2cd8 --- /dev/null +++ b/JSTests/modules/import-slots-late-first-use.js @@ -0,0 +1,16 @@ +import { shouldBe } from "./resources/assert.js"; +import { sometimes, loop, increment, bump } from "./import-slots/late.js"; + +// Imports first read on a path that is only taken once the function is already hot, so the +// optimizing tiers meet a binding no lower tier has read. +for (let i = 0; i < testLoopCount; ++i) + shouldBe(sometimes(false), 0); +shouldBe(loop(testLoopCount, false), 0); +increment(); +bump(); +shouldBe(sometimes(true), "42,1,third:third"); +shouldBe(loop(testLoopCount, true), "42,1,third:third"); +for (let i = 0; i < testLoopCount; ++i) { + bump(); + shouldBe(sometimes(i & 1), (i & 1) ? `42,${i + 2},third:third` : 1); +} diff --git a/JSTests/modules/import-slots-live-bindings.js b/JSTests/modules/import-slots-live-bindings.js new file mode 100644 index 0000000000000..8c94429b32c04 --- /dev/null +++ b/JSTests/modules/import-slots-live-bindings.js @@ -0,0 +1,17 @@ +import { shouldBe } from "./resources/assert.js"; +import { count, increment, set } from "./import-slots/values.js"; +import { total, valuesNamespace, valuesDefault } from "./import-slots/reexport-named.js"; +import { count as throughStar, own } from "./import-slots/reexport-star.js"; +import { grandTotal, third } from "./import-slots/reexport-chain.js"; +import { one } from "./import-slots/live-aliases.js"; + +// One variable seen through a direct import, a renaming re-export, a star re-export, a chain of +// both, a namespace re-export and another module's alias of it. +function all() { return [count, total, throughStar, grandTotal, valuesNamespace.count, one()].join(); } +for (let i = 1; i <= testLoopCount; ++i) { + increment(); + shouldBe(all(), Array(6).fill(i).join()); +} +set("reset"); +shouldBe(all(), Array(6).fill("reset").join()); +shouldBe([own, third, valuesDefault()].join(), "own,third:third,default"); diff --git a/JSTests/modules/import-slots-loaders-concurrent.js b/JSTests/modules/import-slots-loaders-concurrent.js new file mode 100644 index 0000000000000..797ccd9b458f7 --- /dev/null +++ b/JSTests/modules/import-slots-loaders-concurrent.js @@ -0,0 +1,30 @@ +import { shouldBe } from "./resources/assert.js"; +import { sameCode } from "./import-slots/code.js"; + +// Several loaders, the global object's own among them, start loading the same modules before any +// of them has linked or evaluated one: whichever record links a module first, the others are +// compared with it while its body has yet to run (tla-main.js and tla-dep.js suspend in theirs). +const loaders = [$vm.createModuleLoader(), $vm.createModuleLoader(), $vm.createModuleLoader()]; +const started = []; +for (const specifier of ["./import-slots/tla-main.js", "./import-slots/loader-main.js"]) { + started.push(import(specifier)); + for (const loader of loaders) + started.push($vm.moduleLoaderImport(loader, specifier)); +} +const instances = await Promise.all(started); +const asyncs = instances.slice(0, 4); +const mains = instances.slice(4); + +for (const instance of asyncs) { + shouldBe(JSON.stringify([instance.seenAtStart, instance.seenAfterAwait]), `[["ready","after"],["ready","after",1]]`); + shouldBe(JSON.stringify(instance.read()), `["ready",1]`); +} +// (tla-main.js has incremented its loader's values.js once.) +for (let k = 0; k < mains.length; ++k) + shouldBe(mains[k].run(k + 1), `${k + 2},${k + 1}`); +for (let i = 0; i < testLoopCount; ++i) + shouldBe(mains.map(main => main.read()).join(";"), [1, 2, 3, 4].map(n => `${n + 1},${n + 1},${n},true,third:default`).join(";")); +for (let k = 1; k < 4; ++k) { + shouldBe(sameCode(mains[0].run, mains[k].run), true); + shouldBe(sameCode(asyncs[0].read, asyncs[k].read), true); +} diff --git a/JSTests/modules/import-slots-loaders-cycles-tla.js b/JSTests/modules/import-slots-loaders-cycles-tla.js new file mode 100644 index 0000000000000..9aa8b98f56965 --- /dev/null +++ b/JSTests/modules/import-slots-loaders-cycles-tla.js @@ -0,0 +1,33 @@ +import { shouldBe } from "./resources/assert.js"; +import { sameCode } from "./import-slots/code.js"; + +const loaders = [$vm.createModuleLoader(), $vm.createModuleLoader(), $vm.createModuleLoader()]; +const cycles = []; +for (const loader of loaders) + cycles.push([await $vm.moduleLoaderImport(loader, "./import-slots/cycle-a.js"), await $vm.moduleLoaderImport(loader, "./import-slots/cycle-b.js")]); +for (let k = 0; k < cycles.length; ++k) { + const [a, b] = cycles[k]; + shouldBe([a.sawFromB, a.bValueSeen, a.viaB, b.earlyError].join(), "b:a(),bValue,a,ReferenceError: Cannot access 'aValue' before initialization."); + a.setA(`set${k}`); +} +for (let i = 0; i < testLoopCount; ++i) { + for (let k = 0; k < cycles.length; ++k) + shouldBe([cycles[k][1].readA(), cycles[k][0].aValue].join(), `set${k},set${k}`); +} +shouldBe(sameCode(cycles[1][1].readA, cycles[0][1].readA), true); + +const selves = []; +for (const loader of loaders) + selves.push(await $vm.moduleLoaderImport(loader, "./import-slots/self.js")); +selves[1].bump(); +selves[2].bump(); +selves[2].bump(); +shouldBe(JSON.stringify(selves.map(self => self.read())), "[[1,1,true],[2,2,true],[3,3,true]]"); + +const asyncs = []; +for (const loader of loaders) + asyncs.push(await $vm.moduleLoaderImport(loader, "./import-slots/tla-main.js")); +for (const instance of asyncs) { + shouldBe(JSON.stringify([instance.seenAtStart, instance.seenAfterAwait]), `[["ready","after"],["ready","after",1]]`); + shouldBe(JSON.stringify(instance.read()), `["ready",1]`); +} diff --git a/JSTests/modules/import-slots-loaders-errors.js b/JSTests/modules/import-slots-loaders-errors.js new file mode 100644 index 0000000000000..6d0e39493ec1a --- /dev/null +++ b/JSTests/modules/import-slots-loaders-errors.js @@ -0,0 +1,27 @@ +import { shouldBe } from "./resources/assert.js"; + +// A module whose imports do not resolve fails to link in every loader, each time it is asked for, +// with the same error; the modules it would have shared with others stay usable, one instance +// per loader. +const loaders = [$vm.createModuleLoader(), $vm.createModuleLoader(), $vm.createModuleLoader()]; +async function rejection(loader, specifier) { + try { + await $vm.moduleLoaderImport(loader, specifier); + } catch (error) { + return [error instanceof SyntaxError, String(error)].join(); + } + return "loaded"; +} +for (const specifier of ["./import-slots/imports-missing.js", "./import-slots/imports-conflict.js"]) { + const expected = await rejection(loaders[0], specifier); + shouldBe(expected.startsWith("true,SyntaxError: "), true); + for (let round = 0; round < 2; ++round) { + for (const loader of loaders) + shouldBe(await rejection(loader, specifier), expected); + } +} +const values = []; +for (const loader of loaders) + values.push(await $vm.moduleLoaderImport(loader, "./import-slots/values.js")); +values[1].increment(); +shouldBe(values.map(instance => instance.count).join(), "0,1,0"); diff --git a/JSTests/modules/import-slots-loaders-first-instance-collected.js b/JSTests/modules/import-slots-loaders-first-instance-collected.js new file mode 100644 index 0000000000000..8b421cfc4ef04 --- /dev/null +++ b/JSTests/modules/import-slots-loaders-first-instance-collected.js @@ -0,0 +1,27 @@ +import { shouldBe } from "./resources/assert.js"; +import { sameCode } from "./import-slots/code.js"; + +// The only instance of a module is collected, and its code with it, before another loader asks for +// the module: there is nothing to take code from or to be compared with, so the next instance +// links its own, and the ones after that share with it. (The instances below behave the same +// whether or not the collector has got to the first one by then.) +const load = () => $vm.moduleLoaderImport($vm.createModuleLoader(), "./import-slots/loader-main.js"); +await (async () => { + const first = await load(); + shouldBe(first.run(testLoopCount), `${testLoopCount},${testLoopCount}`); +})(); +for (let i = 0; i < 4; ++i) { + await new Promise(resolve => setTimeout(resolve, 0)); + fullGC(); +} +const a = await load(); +shouldBe(a.run(1), "1,1"); +shouldBe(a.read(), "1,1,1,true,third:default"); +const b = await load(); +const c = await load(); +shouldBe(typeof $vm.codeBlockFor(b.run), "string"); +shouldBe(sameCode(a.run, b.run), true); +shouldBe(sameCode(a.read, c.read), true); +shouldBe(b.run(2), "2,2"); +for (let i = 0; i < testLoopCount; ++i) + shouldBe([a.read(), b.read(), c.read()].join(";"), "1,1,1,true,third:default;2,2,2,true,third:default;0,0,0,true,third:default"); diff --git a/JSTests/modules/import-slots-loaders-gc.js b/JSTests/modules/import-slots-loaders-gc.js new file mode 100644 index 0000000000000..71fc2441b9c6d --- /dev/null +++ b/JSTests/modules/import-slots-loaders-gc.js @@ -0,0 +1,21 @@ +import { shouldBe } from "./resources/assert.js"; + +// Instances come and go; the ones that stay keep reading their own bindings from code that +// instances since collected also ran. +const keep = []; +for (let round = 0; round < 12; ++round) { + const instance = await $vm.moduleLoaderImport($vm.createModuleLoader(), "./import-slots/loader-main.js"); + shouldBe(instance.run(round + 1), `${round + 1},${round + 1}`); + if (!(round % 4)) + keep.push([instance, round + 1]); + if (round % 3 === 2) + fullGC(); +} +fullGC(); +for (let i = 0; i < testLoopCount; ++i) { + for (const [instance, n] of keep) + shouldBe(instance.read(), `${n},${n},${n},true,third:default`); +} +const fresh = await $vm.moduleLoaderImport($vm.createModuleLoader(), "./import-slots/loader-main.js"); +shouldBe(fresh.read(), "0,0,0,true,third:default"); +shouldBe((await fresh.loadValues()) === fresh.values, true); diff --git a/JSTests/modules/import-slots-loaders-json.js b/JSTests/modules/import-slots-loaders-json.js new file mode 100644 index 0000000000000..18a716d170c29 --- /dev/null +++ b/JSTests/modules/import-slots-loaders-json.js @@ -0,0 +1,23 @@ +import { shouldBe } from "./resources/assert.js"; +import { sameCode } from "./import-slots/code.js"; + +// An import from a module that is not a source text module: what the importer's code embeds is the +// binding's offset in that module's environment, and records whose offsets agree share the code. +// Each loader has its own JSON module, so its own object. +const load = () => $vm.moduleLoaderImport($vm.createModuleLoader(), "./import-slots/json-user.js"); +const a = await load(); +const b = await load(); +const c = await load(); +b.increment(); +b.data.n = 5; +for (let i = 0; i < testLoopCount; ++i) + shouldBe([a.read(), b.read(), c.read()].join(";"), "json,1,0,true;json,5,1,true;json,1,0,true"); +shouldBe(sameCode(a.read, b.read), true); +shouldBe(sameCode(a.read, c.read), true); +shouldBe(a.data === b.data, false); +shouldBe(a.data === c.data, false); + +const own = await import("./import-slots/json-user.js"); +shouldBe(own.read(), "json,1,0,true"); +shouldBe(sameCode(own.read, a.read), true); +shouldBe(own.data === a.data, false); diff --git a/JSTests/modules/import-slots-loaders-late-first-use.js b/JSTests/modules/import-slots-loaders-late-first-use.js new file mode 100644 index 0000000000000..b299a553093f0 --- /dev/null +++ b/JSTests/modules/import-slots-loaders-late-first-use.js @@ -0,0 +1,19 @@ +import { shouldBe } from "./resources/assert.js"; +import { sameCode } from "./import-slots/code.js"; + +// One instance makes the code hot without ever reading some of its imports; another instance, +// running that same code, is the first to read them. +const load = () => $vm.moduleLoaderImport($vm.createModuleLoader(), "./import-slots/late.js"); +const a = await load(); +shouldBe(a.loop(testLoopCount, false), 0); +const b = await load(); +const c = await load(); +shouldBe(typeof $vm.codeBlockFor(b.sometimes), "string"); +shouldBe(sameCode(a.sometimes, b.sometimes), true); +b.bump(); +c.bump(); +c.bump(); +c.increment(); +shouldBe([b.sometimes(true), c.sometimes(true), a.sometimes(true)].join(";"), "42,1,third:third;42,2,third:third;42,0,third:third"); +shouldBe([b.loop(testLoopCount, true), c.loop(testLoopCount, true), a.loop(testLoopCount, true)].join(";"), "42,1,third:third;42,2,third:third;42,0,third:third"); +shouldBe([a.sometimes(false), b.sometimes(false), c.sometimes(false)].join(), "0,0,1"); diff --git a/JSTests/modules/import-slots-loaders-many.js b/JSTests/modules/import-slots-loaders-many.js new file mode 100644 index 0000000000000..c06d538a84026 --- /dev/null +++ b/JSTests/modules/import-slots-loaders-many.js @@ -0,0 +1,19 @@ +import { shouldBe } from "./resources/assert.js"; +import { sameCode } from "./import-slots/code.js"; + +const load = () => $vm.moduleLoaderImport($vm.createModuleLoader(), "./import-slots/many.js"); +const [a, b, c] = [await load(), await load(), await load()]; +let base = 0; +for (let m = 0; m < 8; ++m) { + for (let i = 0; i < 16; ++i) + base += m * 100 + i; +} +for (let i = 0; i < testLoopCount; ++i) + shouldBe(a.sum(), base); +shouldBe(typeof $vm.codeBlockFor(b.sum), "string"); +shouldBe(sameCode(a.sum, b.sum), true); +b.bumpEverything(); +c.bumpEverything(); +c.bumpEverything(); +for (let i = 0; i < testLoopCount; ++i) + shouldBe([a.sum(), b.sum(), c.sum(), a.last(), b.last(), c.last(), a.first(), b.first(), c.first()].join(), [base, base + 128, base + 256, 715, 716, 717, 0, 1, 2].join()); diff --git a/JSTests/modules/import-slots-loaders-mixes.js b/JSTests/modules/import-slots-loaders-mixes.js new file mode 100644 index 0000000000000..e8852cd536c5d --- /dev/null +++ b/JSTests/modules/import-slots-loaders-mixes.js @@ -0,0 +1,36 @@ +import { shouldBe } from "./resources/assert.js"; +import { sameCode } from "./import-slots/code.js"; + +// Records that share code number their import slots alike, whatever mix of namespace and +// named imports the module has, and keep their own bindings in every tier. +const loaders = []; +for (let i = 0; i < 4; ++i) + loaders.push($vm.createModuleLoader()); +const expectations = { + "mix-first.js": n => `${n},${n},second:first,true`, + "mix-middle.js": n => `${n},${n},second:first,second:first,true`, + "mix-last.js": n => `${n},${n},second:first,third:third,true,true`, + "only-namespaces.js": n => `${n},second:first`, + "no-imports.js": n => `${n}`, + "aliases.js": n => `${n},${n},${n},${n},true`, +}; +for (const [file, expected] of Object.entries(expectations)) { + const instances = []; + for (const loader of loaders) + instances.push(await $vm.moduleLoaderImport(loader, `./import-slots/${file}`)); + // values.js is one instance per loader, shared by the files above: read its count first. + const base = instances.map(instance => Number(instance.read().split(",")[0])); + for (let i = 0; i < testLoopCount; ++i) + instances[0].read(); + for (let k = 1; k < instances.length; ++k) { + shouldBe(sameCode(instances[k].read, instances[0].read), true); + } + for (let k = 0; k < instances.length; ++k) { + for (let i = 0; i <= k; ++i) + instances[k].increment(); + } + for (let round = 0; round < testLoopCount; ++round) { + for (let k = 0; k < instances.length; ++k) + shouldBe(instances[k].read(), expected(base[k] + k + 1)); + } +} diff --git a/JSTests/modules/import-slots-loaders-order.js b/JSTests/modules/import-slots-loaders-order.js new file mode 100644 index 0000000000000..5e55b0d642e76 --- /dev/null +++ b/JSTests/modules/import-slots-loaders-order.js @@ -0,0 +1,44 @@ +import { shouldBe } from "./resources/assert.js"; +import { sameCode } from "./import-slots/code.js"; + +const specifier = "./import-slots/loader-main.js"; +const load = (loader, what = specifier) => $vm.moduleLoaderImport(loader, what); + +// Loaded by the global object's own loader while it is the only loader there is: nothing about +// its imports was resolved for sharing then. +const own = await import(specifier); +shouldBe(own.run(testLoopCount), `${testLoopCount},${testLoopCount}`); + +// Loaders made afterwards have their own instances, and run that same code: the comparison +// happens when the second record for the module turns up, whichever loader it is in. +const a = await load($vm.createModuleLoader()); +const b = await load($vm.createModuleLoader()); +const c = await load($vm.createModuleLoader()); +shouldBe(typeof $vm.codeBlockFor(a.run), "string"); +shouldBe(a.run(1), "1,1"); +shouldBe(a.read(), "1,1,1,true,third:default"); +shouldBe(typeof $vm.codeBlockFor(c.read), "string"); +shouldBe(sameCode(a.run, own.run), true); +shouldBe(sameCode(a.run, b.run), true); +shouldBe(sameCode(a.run, c.run), true); +shouldBe(b.run(2), "2,2"); +shouldBe(c.run(3), "3,3"); +shouldBe([own.read(), a.read(), b.read(), c.read()].join(";"), [testLoopCount, 1, 2, 3].map(n => `${n},${n},${n},true,third:default`).join(";")); + +// The same for a module the global object's own loader first loads after other loaders exist, +// in either order. +const ownLate = await import("./import-slots/late.js"); +shouldBe(ownLate.loop(testLoopCount, false), testLoopCount); +const lateA = await load($vm.createModuleLoader(), "./import-slots/late.js"); +shouldBe(typeof $vm.codeBlockFor(lateA.sometimes), "string"); +shouldBe(sameCode(ownLate.sometimes, lateA.sometimes), true); +lateA.increment(); +shouldBe([ownLate.sometimes(false), lateA.sometimes(false), lateA.sometimes(true), ownLate.sometimes(true)].join(";"), `${testLoopCount};1;42,0,third:third;42,${testLoopCount},third:third`); + +const aliasesA = await load($vm.createModuleLoader(), "./import-slots/aliases.js"); +shouldBe(aliasesA.read(), "0,0,0,0,true"); +const ownAliases = await import("./import-slots/aliases.js"); +shouldBe(typeof $vm.codeBlockFor(ownAliases.read), "string"); +shouldBe(sameCode(ownAliases.read, aliasesA.read), true); +aliasesA.increment(); +shouldBe([ownAliases.read(), aliasesA.read()].join(";"), `${testLoopCount},${testLoopCount},${testLoopCount},${testLoopCount},true;1,1,1,1,true`); diff --git a/JSTests/modules/import-slots-loaders-share-before-evaluate.js b/JSTests/modules/import-slots-loaders-share-before-evaluate.js new file mode 100644 index 0000000000000..da09a112d89cd --- /dev/null +++ b/JSTests/modules/import-slots-loaders-share-before-evaluate.js @@ -0,0 +1,30 @@ +import { shouldBe } from "./resources/assert.js"; +import { sameCode } from "./import-slots/code.js"; + +// The first loader's gate-main.js links while it is the only record of the module, and then waits +// for its dependency, which is suspended at a top-level await. Meanwhile a second loader's record +// takes the first one's code, evaluates and makes that code hot. The first record evaluates only +// after that; its dependency's body, shared by then too, resumes in the middle. +let release; +const gates = [new Promise(resolve => { release = resolve; }), Promise.resolve()]; +globalThis.importSlotsGate = { started: 0, next: () => gates.shift() }; +const specifier = "./import-slots/gate-main.js"; + +const pendingFirst = $vm.moduleLoaderImport($vm.createModuleLoader(), specifier); +while (globalThis.importSlotsGate.started < 1) + await Promise.resolve(); + +const second = await $vm.moduleLoaderImport($vm.createModuleLoader(), specifier); +shouldBe(second.seenAtStart, "ready,0,1,1,0"); +second.increment(); +second.bump(); +shouldBe(second.loop(testLoopCount), "ready,2,1"); + +release(); +const first = await pendingFirst; +shouldBe(first.seenAtStart, "ready,0,1,1,0"); +shouldBe(sameCode(first.read, second.read), true); +shouldBe(first.loop(testLoopCount), "ready,1,0"); +shouldBe(second.loop(testLoopCount), "ready,2,1"); +first.bump(); +shouldBe([first.read(), second.read()].join(";"), "ready,1,1;ready,2,1"); diff --git a/JSTests/modules/import-slots-many.js b/JSTests/modules/import-slots-many.js new file mode 100644 index 0000000000000..d3679630f5f7d --- /dev/null +++ b/JSTests/modules/import-slots-many.js @@ -0,0 +1,18 @@ +import { shouldBe } from "./resources/assert.js"; +import { sum, bumpEverything, first, last } from "./import-slots/many.js"; + +// 128 imported bindings from 8 modules in one module. +let expected = 0; +for (let m = 0; m < 8; ++m) { + for (let i = 0; i < 16; ++i) + expected += m * 100 + i; +} +shouldBe(sum(), expected); +shouldBe(first(), 0); +shouldBe(last(), 715); +for (let i = 1; i <= testLoopCount; ++i) { + bumpEverything(); + shouldBe(sum(), expected + 128 * i); +} +shouldBe(first(), testLoopCount); +shouldBe(last(), 715 + testLoopCount); diff --git a/JSTests/modules/import-slots-names.js b/JSTests/modules/import-slots-names.js new file mode 100644 index 0000000000000..0f5a0d1471dd5 --- /dev/null +++ b/JSTests/modules/import-slots-names.js @@ -0,0 +1,14 @@ +import { shouldBe } from "./resources/assert.js"; +// Imported in an order that is neither the exporter's, nor code point order. +import { Z, aa, _, b, \u03c0, A, $, z0, a, \u{1D4D0}, B, \u00e4 } from "./import-slots/names.js"; +import { "string name" as spaced, "" as empty, "dashed-name" as dashed } from "./import-slots/names.js"; +import theDefault, { default as alsoDefault } from "./import-slots/names.js"; + +function all() { return [a, A, aa, b, B, Z, z0, _, $, \u00e4, \u03c0, \u{1D4D0}, spaced, dashed, empty, theDefault, alsoDefault].join(); } +const expected = "a,A,aa,b,B,Z,z0,_,$,a-umlaut,pi,astral,string name,dashed-name,empty,default,default"; +for (let i = 0; i < testLoopCount; ++i) + shouldBe(all(), expected); +// Each one alone, from its own function, so each access site links separately. +const readers = [() => a, () => A, () => aa, () => b, () => B, () => Z, () => z0, () => _, () => $, () => \u00e4, () => \u03c0, () => \u{1D4D0}, () => spaced, () => dashed, () => empty, () => theDefault, () => alsoDefault]; +shouldBe(readers.map(reader => reader()).join(), expected); +shouldBe(readers.reverse().map(reader => reader()).reverse().join(), expected); diff --git a/JSTests/modules/import-slots-namespace-mix.js b/JSTests/modules/import-slots-namespace-mix.js new file mode 100644 index 0000000000000..a9b61a44ad5a7 --- /dev/null +++ b/JSTests/modules/import-slots-namespace-mix.js @@ -0,0 +1,18 @@ +import { shouldBe } from "./resources/assert.js"; +import * as first from "./import-slots/mix-first.js"; +import * as middle from "./import-slots/mix-middle.js"; +import * as last from "./import-slots/mix-last.js"; +import * as only from "./import-slots/only-namespaces.js"; + +// Namespace imports before, between and after the named ones. +shouldBe(first.read(), "0,0,second:first,true"); +shouldBe(middle.read(), "0,0,second:first,second:first,true"); +shouldBe(last.read(), "0,0,second:first,third:third,true,true"); +shouldBe(only.read(), "0,second:first"); +for (let i = 1; i <= testLoopCount; ++i) { + [first, middle, last, only][i % 4].increment(); + shouldBe(first.read(), `${i},${i},second:first,true`); + shouldBe(middle.read(), `${i},${i},second:first,second:first,true`); + shouldBe(last.read(), `${i},${i},second:first,third:third,true,true`); + shouldBe(only.read(), `${i},second:first`); +} diff --git a/JSTests/modules/import-slots-tla.js b/JSTests/modules/import-slots-tla.js new file mode 100644 index 0000000000000..5d7167b7c2b3a --- /dev/null +++ b/JSTests/modules/import-slots-tla.js @@ -0,0 +1,9 @@ +import { shouldBe } from "./resources/assert.js"; +import { seenAtStart, seenAfterAwait, read } from "./import-slots/tla-main.js"; +import { count } from "./import-slots/values.js"; + +shouldBe(JSON.stringify(seenAtStart), `["ready","after"]`); +shouldBe(JSON.stringify(seenAfterAwait), `["ready","after",1]`); +for (let i = 0; i < testLoopCount; ++i) + shouldBe(JSON.stringify(read()), `["ready",1]`); +shouldBe(count, 1); diff --git a/JSTests/modules/import-slots-unused.js b/JSTests/modules/import-slots-unused.js new file mode 100644 index 0000000000000..d24fcd7a7f8a8 --- /dev/null +++ b/JSTests/modules/import-slots-unused.js @@ -0,0 +1,14 @@ +import { shouldBe } from "./resources/assert.js"; +import { evaluated, onlySecond, neverCalled } from "./import-slots/unused.js"; +import { count } from "./import-slots/values.js"; + +// A module whose imports are never read, or first read long after it was evaluated. +shouldBe(evaluated, true); +for (let i = 0; i < testLoopCount; ++i) + shouldBe(onlySecond(), "second:second"); +fullGC(); +const values = neverCalled(); +shouldBe(values.length, 10); +shouldBe(values[0], count); +shouldBe(values[8], "second:first"); +shouldBe(values[6].constant, 42); diff --git a/JSTests/modules/import-slots/aliases.js b/JSTests/modules/import-slots/aliases.js new file mode 100644 index 0000000000000..4095eac4b18a8 --- /dev/null +++ b/JSTests/modules/import-slots/aliases.js @@ -0,0 +1,5 @@ +import { count as one, count as two, increment, increment as again } from "./values.js"; +import { count as three } from "./values.js"; +import { total } from "./reexport-named.js"; +export function read() { return [one, two, three, total, increment === again].join(); } +export { increment }; diff --git a/JSTests/modules/import-slots/assign.js b/JSTests/modules/import-slots/assign.js new file mode 100644 index 0000000000000..45ce12b9af46e --- /dev/null +++ b/JSTests/modules/import-slots/assign.js @@ -0,0 +1,17 @@ +import { count, object, declared } from "./values.js"; +import * as values from "./values.js"; +import defaultFunction from "./values.js"; +export function assign() { count = 1; } +export function increment() { count++; } +export function compound() { count += 1; } +export function logical() { count ??= 1; } +export function destructuring() { [count] = [1]; } +export function objectDestructuring() { ({ count } = { count: 1 }); } +export function forIn() { for (count in { a: 1 }); } +export function forOf() { for (count of [1]); } +export function assignFunction() { declared = null; } +export function assignDefault() { defaultFunction = null; } +export function assignNamespace() { values = null; } +export function assignNamespaceProperty() { values.count = 1; } +export function mutateObject() { object.mutated = true; return object.mutated; } +export function read() { return count; } diff --git a/JSTests/modules/import-slots/code.js b/JSTests/modules/import-slots/code.js new file mode 100644 index 0000000000000..88fa8bfc1f98f --- /dev/null +++ b/JSTests/modules/import-slots/code.js @@ -0,0 +1,4 @@ +// The executable a function's code belongs to (the end of codeBlockFor's chain): the same for +// functions that share code, whichever tier either is in when asked. +export const executableOf = f => /->(0x[0-9a-f]+),/.exec($vm.codeBlockFor(f))[1]; +export const sameCode = (f, g) => executableOf(f) === executableOf(g); diff --git a/JSTests/modules/import-slots/cycle-a.js b/JSTests/modules/import-slots/cycle-a.js new file mode 100644 index 0000000000000..56e474e9ddeac --- /dev/null +++ b/JSTests/modules/import-slots/cycle-a.js @@ -0,0 +1,7 @@ +import { b, bValue, readA } from "./cycle-b.js"; +export let aValue = "a"; +export function a() { return "a()"; } +export const sawFromB = b(); +export const bValueSeen = bValue; +export const viaB = readA(); +export function setA(value) { aValue = value; } diff --git a/JSTests/modules/import-slots/cycle-b.js b/JSTests/modules/import-slots/cycle-b.js new file mode 100644 index 0000000000000..409e57235d8c2 --- /dev/null +++ b/JSTests/modules/import-slots/cycle-b.js @@ -0,0 +1,10 @@ +import { a, aValue } from "./cycle-a.js"; +export function b() { return "b:" + a(); } +export let earlyError = ""; +try { + aValue; +} catch (error) { + earlyError = String(error); +} +export const bValue = "bValue"; +export function readA() { return aValue; } diff --git a/JSTests/modules/import-slots/data.json b/JSTests/modules/import-slots/data.json new file mode 100644 index 0000000000000..8082ff73e1270 --- /dev/null +++ b/JSTests/modules/import-slots/data.json @@ -0,0 +1 @@ +{"tag":"json","n":1} diff --git a/JSTests/modules/import-slots/default-anonymous-class.js b/JSTests/modules/import-slots/default-anonymous-class.js new file mode 100644 index 0000000000000..0b1f950ed7532 --- /dev/null +++ b/JSTests/modules/import-slots/default-anonymous-class.js @@ -0,0 +1 @@ +export default class { } diff --git a/JSTests/modules/import-slots/default-anonymous-function.js b/JSTests/modules/import-slots/default-anonymous-function.js new file mode 100644 index 0000000000000..3e16d2eb2c627 --- /dev/null +++ b/JSTests/modules/import-slots/default-anonymous-function.js @@ -0,0 +1 @@ +export default function () { return "anonymous function"; } diff --git a/JSTests/modules/import-slots/default-expression.js b/JSTests/modules/import-slots/default-expression.js new file mode 100644 index 0000000000000..d9c59b2aebe33 --- /dev/null +++ b/JSTests/modules/import-slots/default-expression.js @@ -0,0 +1 @@ +export default 6 * 7; diff --git a/JSTests/modules/import-slots/default-forms.js b/JSTests/modules/import-slots/default-forms.js new file mode 100644 index 0000000000000..7f132b61a1529 --- /dev/null +++ b/JSTests/modules/import-slots/default-forms.js @@ -0,0 +1,7 @@ +import anonymousFunction from "./default-anonymous-function.js"; +import anonymousClass from "./default-anonymous-class.js"; +import expression from "./default-expression.js"; +import named, { named as alsoNamed, rename } from "./default-named-function.js"; +import { default as viaName } from "./default-expression.js"; +export function read() { return [anonymousFunction.name, anonymousFunction(), anonymousClass.name, expression, named(), named === alsoNamed, viaName].join(); } +export { rename, named }; diff --git a/JSTests/modules/import-slots/default-named-function.js b/JSTests/modules/import-slots/default-named-function.js new file mode 100644 index 0000000000000..e19d913b9bbcf --- /dev/null +++ b/JSTests/modules/import-slots/default-named-function.js @@ -0,0 +1,3 @@ +export default function named() { return "named"; } +export { named }; +export function rename() { named = function replaced() { return "replaced"; }; } diff --git a/JSTests/modules/import-slots/forms.js b/JSTests/modules/import-slots/forms.js new file mode 100644 index 0000000000000..08642cd0bb7d6 --- /dev/null +++ b/JSTests/modules/import-slots/forms.js @@ -0,0 +1,20 @@ +import { count, increment, set, constant, object } from "./values.js"; +import * as values from "./values.js"; +export const arrow = () => count; +export function* generator() { yield count; increment(); yield count; } +export async function asynchronous() { await null; return count; } +export async function* asyncGenerator() { yield count; } +export function nested() { return (() => (() => count)())(); } +export function viaEval() { return eval("count"); } +export function viaIndirectEval() { return (0, eval)("typeof count"); } +export function viaFunctionConstructor() { return new Function("return typeof count")(); } +export function defaultParameter(value = count) { return value; } +export function withTypeof() { return [typeof count, typeof constant, typeof object, typeof values].join(); } +export class WithStatic { static captured = count; static read() { return count; } get accessor() { return count; } } +export const objectLiteral = { get accessor() { return count; }, method() { return count; }, [`computed${constant}`]: count }; +export function destructure() { const { count: local } = values; return [local, count].join(); } +export function inTryFinally() { try { return count; } finally { increment(); } } +export function labelled() { outer: for (;;) { for (;;) { if (count >= 0) break outer; } } return count; } +export function templated() { return `${count}:${constant}`; } +export function optional() { return [object?.tag, values?.object?.tag].join(); } +export { increment, set }; diff --git a/JSTests/modules/import-slots/gate-dep.js b/JSTests/modules/import-slots/gate-dep.js new file mode 100644 index 0000000000000..abaabeb40ac2e --- /dev/null +++ b/JSTests/modules/import-slots/gate-dep.js @@ -0,0 +1,8 @@ +import { count, increment } from "./values.js"; +export let ready = "pending"; +export const countBefore = count; +globalThis.importSlotsGate.started++; +await globalThis.importSlotsGate.next(); +increment(); +export const countAfter = count; +ready = "ready"; diff --git a/JSTests/modules/import-slots/gate-main.js b/JSTests/modules/import-slots/gate-main.js new file mode 100644 index 0000000000000..927ff602ebc24 --- /dev/null +++ b/JSTests/modules/import-slots/gate-main.js @@ -0,0 +1,12 @@ +import { ready, countBefore, countAfter } from "./gate-dep.js"; +import { count, increment } from "./values.js"; +import { live, bump } from "./second.js"; +export const seenAtStart = [ready, countBefore, countAfter, count, live].join(); +export function read() { return [ready, count, live].join(); } +export function loop(n) { + let result; + for (let i = 0; i < n; ++i) + result = read(); + return result; +} +export { increment, bump }; diff --git a/JSTests/modules/import-slots/globals-named.js b/JSTests/modules/import-slots/globals-named.js new file mode 100644 index 0000000000000..c599647c5ace3 --- /dev/null +++ b/JSTests/modules/import-slots/globals-named.js @@ -0,0 +1,2 @@ +export const Map = "not the global Map"; +export const undefinedLike = "NaN-like"; diff --git a/JSTests/modules/import-slots/imports-conflict.js b/JSTests/modules/import-slots/imports-conflict.js new file mode 100644 index 0000000000000..08b0604e479a2 --- /dev/null +++ b/JSTests/modules/import-slots/imports-conflict.js @@ -0,0 +1,2 @@ +import { first } from "./star-conflict.js"; +export const value = first; diff --git a/JSTests/modules/import-slots/imports-missing.js b/JSTests/modules/import-slots/imports-missing.js new file mode 100644 index 0000000000000..e073c28a782d6 --- /dev/null +++ b/JSTests/modules/import-slots/imports-missing.js @@ -0,0 +1,2 @@ +import { doesNotExist } from "./values.js"; +export const value = doesNotExist; diff --git a/JSTests/modules/import-slots/json-user.js b/JSTests/modules/import-slots/json-user.js new file mode 100644 index 0000000000000..1f90c14b01a08 --- /dev/null +++ b/JSTests/modules/import-slots/json-user.js @@ -0,0 +1,5 @@ +import data from "./data.json" with { type: "json" }; +import { count, increment } from "./values.js"; +import * as ns from "./data.json" with { type: "json" }; +export function read() { return [data.tag, data.n, count, ns.default === data].join(); } +export { increment, data }; diff --git a/JSTests/modules/import-slots/late.js b/JSTests/modules/import-slots/late.js new file mode 100644 index 0000000000000..3f656324f0894 --- /dev/null +++ b/JSTests/modules/import-slots/late.js @@ -0,0 +1,17 @@ +import { count, increment, constant } from "./values.js"; +import { live, bump } from "./second.js"; +import * as namespace from "./third.js"; +// `constant`, `live` and `namespace` are only read on a path that is first taken after the +// function is hot. +export function sometimes(flag) { + if (flag) + return [constant, live, namespace.third].join(); + return count; +} +export function loop(n, flag) { + let result; + for (let i = 0; i < n; ++i) + result = sometimes(flag); + return result; +} +export { increment, bump }; diff --git a/JSTests/modules/import-slots/live-aliases.js b/JSTests/modules/import-slots/live-aliases.js new file mode 100644 index 0000000000000..3bd0460da10ec --- /dev/null +++ b/JSTests/modules/import-slots/live-aliases.js @@ -0,0 +1,2 @@ +import { count as first, count as second } from "./values.js"; +export function one() { if (first !== second) throw new Error("aliases differ"); return first; } diff --git a/JSTests/modules/import-slots/loader-main.js b/JSTests/modules/import-slots/loader-main.js new file mode 100644 index 0000000000000..02ccc1d11fd5e --- /dev/null +++ b/JSTests/modules/import-slots/loader-main.js @@ -0,0 +1,14 @@ +import { count, increment, set, object } from "./values.js"; +import * as values from "./values.js"; +import { live, bump } from "./second.js"; +import thirdDefault from "./third.js"; +export function run(n) { + for (let i = 0; i < n; ++i) { + increment(); + bump(); + } + return [count, live].join(); +} +export function read() { return [count, values.count, live, object === values.object, thirdDefault.tag].join(); } +export const loadValues = () => import("./values.js"); +export { values, set }; diff --git a/JSTests/modules/import-slots/many-0.js b/JSTests/modules/import-slots/many-0.js new file mode 100644 index 0000000000000..354de87f8a577 --- /dev/null +++ b/JSTests/modules/import-slots/many-0.js @@ -0,0 +1,17 @@ +export let v0_0 = 0; +export let v0_1 = 1; +export let v0_2 = 2; +export let v0_3 = 3; +export let v0_4 = 4; +export let v0_5 = 5; +export let v0_6 = 6; +export let v0_7 = 7; +export let v0_8 = 8; +export let v0_9 = 9; +export let v0_10 = 10; +export let v0_11 = 11; +export let v0_12 = 12; +export let v0_13 = 13; +export let v0_14 = 14; +export let v0_15 = 15; +export function bumpAll() { ++v0_0; ++v0_1; ++v0_2; ++v0_3; ++v0_4; ++v0_5; ++v0_6; ++v0_7; ++v0_8; ++v0_9; ++v0_10; ++v0_11; ++v0_12; ++v0_13; ++v0_14; ++v0_15; } diff --git a/JSTests/modules/import-slots/many-1.js b/JSTests/modules/import-slots/many-1.js new file mode 100644 index 0000000000000..f0a93179a4e1e --- /dev/null +++ b/JSTests/modules/import-slots/many-1.js @@ -0,0 +1,17 @@ +export let v1_0 = 100; +export let v1_1 = 101; +export let v1_2 = 102; +export let v1_3 = 103; +export let v1_4 = 104; +export let v1_5 = 105; +export let v1_6 = 106; +export let v1_7 = 107; +export let v1_8 = 108; +export let v1_9 = 109; +export let v1_10 = 110; +export let v1_11 = 111; +export let v1_12 = 112; +export let v1_13 = 113; +export let v1_14 = 114; +export let v1_15 = 115; +export function bumpAll() { ++v1_0; ++v1_1; ++v1_2; ++v1_3; ++v1_4; ++v1_5; ++v1_6; ++v1_7; ++v1_8; ++v1_9; ++v1_10; ++v1_11; ++v1_12; ++v1_13; ++v1_14; ++v1_15; } diff --git a/JSTests/modules/import-slots/many-2.js b/JSTests/modules/import-slots/many-2.js new file mode 100644 index 0000000000000..cf9eb17add5dd --- /dev/null +++ b/JSTests/modules/import-slots/many-2.js @@ -0,0 +1,17 @@ +export let v2_0 = 200; +export let v2_1 = 201; +export let v2_2 = 202; +export let v2_3 = 203; +export let v2_4 = 204; +export let v2_5 = 205; +export let v2_6 = 206; +export let v2_7 = 207; +export let v2_8 = 208; +export let v2_9 = 209; +export let v2_10 = 210; +export let v2_11 = 211; +export let v2_12 = 212; +export let v2_13 = 213; +export let v2_14 = 214; +export let v2_15 = 215; +export function bumpAll() { ++v2_0; ++v2_1; ++v2_2; ++v2_3; ++v2_4; ++v2_5; ++v2_6; ++v2_7; ++v2_8; ++v2_9; ++v2_10; ++v2_11; ++v2_12; ++v2_13; ++v2_14; ++v2_15; } diff --git a/JSTests/modules/import-slots/many-3.js b/JSTests/modules/import-slots/many-3.js new file mode 100644 index 0000000000000..199d77c3b2d32 --- /dev/null +++ b/JSTests/modules/import-slots/many-3.js @@ -0,0 +1,17 @@ +export let v3_0 = 300; +export let v3_1 = 301; +export let v3_2 = 302; +export let v3_3 = 303; +export let v3_4 = 304; +export let v3_5 = 305; +export let v3_6 = 306; +export let v3_7 = 307; +export let v3_8 = 308; +export let v3_9 = 309; +export let v3_10 = 310; +export let v3_11 = 311; +export let v3_12 = 312; +export let v3_13 = 313; +export let v3_14 = 314; +export let v3_15 = 315; +export function bumpAll() { ++v3_0; ++v3_1; ++v3_2; ++v3_3; ++v3_4; ++v3_5; ++v3_6; ++v3_7; ++v3_8; ++v3_9; ++v3_10; ++v3_11; ++v3_12; ++v3_13; ++v3_14; ++v3_15; } diff --git a/JSTests/modules/import-slots/many-4.js b/JSTests/modules/import-slots/many-4.js new file mode 100644 index 0000000000000..8d1e61f370855 --- /dev/null +++ b/JSTests/modules/import-slots/many-4.js @@ -0,0 +1,17 @@ +export let v4_0 = 400; +export let v4_1 = 401; +export let v4_2 = 402; +export let v4_3 = 403; +export let v4_4 = 404; +export let v4_5 = 405; +export let v4_6 = 406; +export let v4_7 = 407; +export let v4_8 = 408; +export let v4_9 = 409; +export let v4_10 = 410; +export let v4_11 = 411; +export let v4_12 = 412; +export let v4_13 = 413; +export let v4_14 = 414; +export let v4_15 = 415; +export function bumpAll() { ++v4_0; ++v4_1; ++v4_2; ++v4_3; ++v4_4; ++v4_5; ++v4_6; ++v4_7; ++v4_8; ++v4_9; ++v4_10; ++v4_11; ++v4_12; ++v4_13; ++v4_14; ++v4_15; } diff --git a/JSTests/modules/import-slots/many-5.js b/JSTests/modules/import-slots/many-5.js new file mode 100644 index 0000000000000..b66ce1d8cd147 --- /dev/null +++ b/JSTests/modules/import-slots/many-5.js @@ -0,0 +1,17 @@ +export let v5_0 = 500; +export let v5_1 = 501; +export let v5_2 = 502; +export let v5_3 = 503; +export let v5_4 = 504; +export let v5_5 = 505; +export let v5_6 = 506; +export let v5_7 = 507; +export let v5_8 = 508; +export let v5_9 = 509; +export let v5_10 = 510; +export let v5_11 = 511; +export let v5_12 = 512; +export let v5_13 = 513; +export let v5_14 = 514; +export let v5_15 = 515; +export function bumpAll() { ++v5_0; ++v5_1; ++v5_2; ++v5_3; ++v5_4; ++v5_5; ++v5_6; ++v5_7; ++v5_8; ++v5_9; ++v5_10; ++v5_11; ++v5_12; ++v5_13; ++v5_14; ++v5_15; } diff --git a/JSTests/modules/import-slots/many-6.js b/JSTests/modules/import-slots/many-6.js new file mode 100644 index 0000000000000..a1e20197d13b0 --- /dev/null +++ b/JSTests/modules/import-slots/many-6.js @@ -0,0 +1,17 @@ +export let v6_0 = 600; +export let v6_1 = 601; +export let v6_2 = 602; +export let v6_3 = 603; +export let v6_4 = 604; +export let v6_5 = 605; +export let v6_6 = 606; +export let v6_7 = 607; +export let v6_8 = 608; +export let v6_9 = 609; +export let v6_10 = 610; +export let v6_11 = 611; +export let v6_12 = 612; +export let v6_13 = 613; +export let v6_14 = 614; +export let v6_15 = 615; +export function bumpAll() { ++v6_0; ++v6_1; ++v6_2; ++v6_3; ++v6_4; ++v6_5; ++v6_6; ++v6_7; ++v6_8; ++v6_9; ++v6_10; ++v6_11; ++v6_12; ++v6_13; ++v6_14; ++v6_15; } diff --git a/JSTests/modules/import-slots/many-7.js b/JSTests/modules/import-slots/many-7.js new file mode 100644 index 0000000000000..5c2bcac4b1d71 --- /dev/null +++ b/JSTests/modules/import-slots/many-7.js @@ -0,0 +1,17 @@ +export let v7_0 = 700; +export let v7_1 = 701; +export let v7_2 = 702; +export let v7_3 = 703; +export let v7_4 = 704; +export let v7_5 = 705; +export let v7_6 = 706; +export let v7_7 = 707; +export let v7_8 = 708; +export let v7_9 = 709; +export let v7_10 = 710; +export let v7_11 = 711; +export let v7_12 = 712; +export let v7_13 = 713; +export let v7_14 = 714; +export let v7_15 = 715; +export function bumpAll() { ++v7_0; ++v7_1; ++v7_2; ++v7_3; ++v7_4; ++v7_5; ++v7_6; ++v7_7; ++v7_8; ++v7_9; ++v7_10; ++v7_11; ++v7_12; ++v7_13; ++v7_14; ++v7_15; } diff --git a/JSTests/modules/import-slots/many.js b/JSTests/modules/import-slots/many.js new file mode 100644 index 0000000000000..dfa67f046dfd3 --- /dev/null +++ b/JSTests/modules/import-slots/many.js @@ -0,0 +1,12 @@ +import { v0_0, v0_1, v0_2, v0_3, v0_4, v0_5, v0_6, v0_7, v0_8, v0_9, v0_10, v0_11, v0_12, v0_13, v0_14, v0_15, bumpAll as bump0 } from "./many-0.js"; +import { v1_0, v1_1, v1_2, v1_3, v1_4, v1_5, v1_6, v1_7, v1_8, v1_9, v1_10, v1_11, v1_12, v1_13, v1_14, v1_15, bumpAll as bump1 } from "./many-1.js"; +import { v2_0, v2_1, v2_2, v2_3, v2_4, v2_5, v2_6, v2_7, v2_8, v2_9, v2_10, v2_11, v2_12, v2_13, v2_14, v2_15, bumpAll as bump2 } from "./many-2.js"; +import { v3_0, v3_1, v3_2, v3_3, v3_4, v3_5, v3_6, v3_7, v3_8, v3_9, v3_10, v3_11, v3_12, v3_13, v3_14, v3_15, bumpAll as bump3 } from "./many-3.js"; +import { v4_0, v4_1, v4_2, v4_3, v4_4, v4_5, v4_6, v4_7, v4_8, v4_9, v4_10, v4_11, v4_12, v4_13, v4_14, v4_15, bumpAll as bump4 } from "./many-4.js"; +import { v5_0, v5_1, v5_2, v5_3, v5_4, v5_5, v5_6, v5_7, v5_8, v5_9, v5_10, v5_11, v5_12, v5_13, v5_14, v5_15, bumpAll as bump5 } from "./many-5.js"; +import { v6_0, v6_1, v6_2, v6_3, v6_4, v6_5, v6_6, v6_7, v6_8, v6_9, v6_10, v6_11, v6_12, v6_13, v6_14, v6_15, bumpAll as bump6 } from "./many-6.js"; +import { v7_0, v7_1, v7_2, v7_3, v7_4, v7_5, v7_6, v7_7, v7_8, v7_9, v7_10, v7_11, v7_12, v7_13, v7_14, v7_15, bumpAll as bump7 } from "./many-7.js"; +export function sum() { return [v0_0, v0_1, v0_2, v0_3, v0_4, v0_5, v0_6, v0_7, v0_8, v0_9, v0_10, v0_11, v0_12, v0_13, v0_14, v0_15, v1_0, v1_1, v1_2, v1_3, v1_4, v1_5, v1_6, v1_7, v1_8, v1_9, v1_10, v1_11, v1_12, v1_13, v1_14, v1_15, v2_0, v2_1, v2_2, v2_3, v2_4, v2_5, v2_6, v2_7, v2_8, v2_9, v2_10, v2_11, v2_12, v2_13, v2_14, v2_15, v3_0, v3_1, v3_2, v3_3, v3_4, v3_5, v3_6, v3_7, v3_8, v3_9, v3_10, v3_11, v3_12, v3_13, v3_14, v3_15, v4_0, v4_1, v4_2, v4_3, v4_4, v4_5, v4_6, v4_7, v4_8, v4_9, v4_10, v4_11, v4_12, v4_13, v4_14, v4_15, v5_0, v5_1, v5_2, v5_3, v5_4, v5_5, v5_6, v5_7, v5_8, v5_9, v5_10, v5_11, v5_12, v5_13, v5_14, v5_15, v6_0, v6_1, v6_2, v6_3, v6_4, v6_5, v6_6, v6_7, v6_8, v6_9, v6_10, v6_11, v6_12, v6_13, v6_14, v6_15, v7_0, v7_1, v7_2, v7_3, v7_4, v7_5, v7_6, v7_7, v7_8, v7_9, v7_10, v7_11, v7_12, v7_13, v7_14, v7_15].reduce((a, b) => a + b, 0); } +export function bumpEverything() { bump0(); bump1(); bump2(); bump3(); bump4(); bump5(); bump6(); bump7(); } +export function last() { return v7_15; } +export function first() { return v0_0; } diff --git a/JSTests/modules/import-slots/mix-first.js b/JSTests/modules/import-slots/mix-first.js new file mode 100644 index 0000000000000..02d38ccb99259 --- /dev/null +++ b/JSTests/modules/import-slots/mix-first.js @@ -0,0 +1,5 @@ +import * as values from "./values.js"; +import { count, increment } from "./values.js"; +import { first } from "./second.js"; +export function read() { return [count, values.count, first, values.increment === increment].join(); } +export { increment }; diff --git a/JSTests/modules/import-slots/mix-last.js b/JSTests/modules/import-slots/mix-last.js new file mode 100644 index 0000000000000..c491596806a33 --- /dev/null +++ b/JSTests/modules/import-slots/mix-last.js @@ -0,0 +1,6 @@ +import { count, increment } from "./values.js"; +import { first } from "./second.js"; +import thirdDefault, * as third from "./third.js"; +import * as values from "./values.js"; +export function read() { return [count, values.count, first, third.third, thirdDefault === third.default, values.increment === increment].join(); } +export { increment }; diff --git a/JSTests/modules/import-slots/mix-middle.js b/JSTests/modules/import-slots/mix-middle.js new file mode 100644 index 0000000000000..6b4e6da3e50e6 --- /dev/null +++ b/JSTests/modules/import-slots/mix-middle.js @@ -0,0 +1,7 @@ +import { count } from "./values.js"; +import * as values from "./values.js"; +import * as second from "./second.js"; +import { increment } from "./values.js"; +import { first } from "./second.js"; +export function read() { return [count, values.count, first, second.first, values.increment === increment].join(); } +export { increment }; diff --git a/JSTests/modules/import-slots/names.js b/JSTests/modules/import-slots/names.js new file mode 100644 index 0000000000000..87bb824ada4a1 --- /dev/null +++ b/JSTests/modules/import-slots/names.js @@ -0,0 +1,6 @@ +// Names chosen so that source order, code point order and hash order all differ. +export const b = "b", a = "a", B = "B", _ = "_", $ = "$", z0 = "z0", Z = "Z", aa = "aa", A = "A"; +export const \u00e4 = "a-umlaut", \u03c0 = "pi", \u{1D4D0} = "astral"; +const spaced = "string name", dashed = "dashed-name", empty = "empty"; +export { spaced as "string name", dashed as "dashed-name", empty as "" }; +export default "default"; diff --git a/JSTests/modules/import-slots/no-imports.js b/JSTests/modules/import-slots/no-imports.js new file mode 100644 index 0000000000000..377c2be1a4c95 --- /dev/null +++ b/JSTests/modules/import-slots/no-imports.js @@ -0,0 +1,4 @@ +export let hits = 0; +export function hit() { return ++hits; } +export function read() { return String(hits); } +export const increment = hit; diff --git a/JSTests/modules/import-slots/only-namespaces.js b/JSTests/modules/import-slots/only-namespaces.js new file mode 100644 index 0000000000000..8a833db493c12 --- /dev/null +++ b/JSTests/modules/import-slots/only-namespaces.js @@ -0,0 +1,4 @@ +import * as values from "./values.js"; +import * as second from "./second.js"; +export function read() { return [values.count, second.first].join(); } +export function increment() { return values.increment(); } diff --git a/JSTests/modules/import-slots/reexport-chain.js b/JSTests/modules/import-slots/reexport-chain.js new file mode 100644 index 0000000000000..9ff77251b3f65 --- /dev/null +++ b/JSTests/modules/import-slots/reexport-chain.js @@ -0,0 +1,2 @@ +export * from "./reexport-star.js"; +export { total as grandTotal } from "./reexport-named.js"; diff --git a/JSTests/modules/import-slots/reexport-named.js b/JSTests/modules/import-slots/reexport-named.js new file mode 100644 index 0000000000000..f423c69856355 --- /dev/null +++ b/JSTests/modules/import-slots/reexport-named.js @@ -0,0 +1,4 @@ +export { count as total, increment, set } from "./values.js"; +export { first as secondFirst } from "./second.js"; +export { default as valuesDefault } from "./values.js"; +export * as valuesNamespace from "./values.js"; diff --git a/JSTests/modules/import-slots/reexport-star.js b/JSTests/modules/import-slots/reexport-star.js new file mode 100644 index 0000000000000..166b25ae8d25a --- /dev/null +++ b/JSTests/modules/import-slots/reexport-star.js @@ -0,0 +1,3 @@ +export * from "./values.js"; +export * from "./third.js"; +export const own = "own"; diff --git a/JSTests/modules/import-slots/second.js b/JSTests/modules/import-slots/second.js new file mode 100644 index 0000000000000..3880977044df3 --- /dev/null +++ b/JSTests/modules/import-slots/second.js @@ -0,0 +1,4 @@ +export const first = "second:first"; +export const second = "second:second"; +export let live = 0; +export function bump() { return ++live; } diff --git a/JSTests/modules/import-slots/self.js b/JSTests/modules/import-slots/self.js new file mode 100644 index 0000000000000..79844d64144dc --- /dev/null +++ b/JSTests/modules/import-slots/self.js @@ -0,0 +1,5 @@ +import { value as imported, bump as importedBump } from "./self.js"; +import * as self from "./self.js"; +export let value = 1; +export function bump() { return ++value; } +export function read() { return [imported, self.value, importedBump === self.bump && importedBump === bump]; } diff --git a/JSTests/modules/import-slots/shadow.js b/JSTests/modules/import-slots/shadow.js new file mode 100644 index 0000000000000..a35c25624868c --- /dev/null +++ b/JSTests/modules/import-slots/shadow.js @@ -0,0 +1,9 @@ +import { first, second } from "./second.js"; +import { first as thirdFirst } from "./third.js"; +import { Map, undefinedLike as NaNLike } from "./globals-named.js"; +export function read(first) { return [first, second, thirdFirst].join(); } +export function inner() { + let second = "local"; + return (() => [first, second].join())(); +} +export function globals() { return [typeof Map, Map, NaNLike, typeof globalThis.Map].join(); } diff --git a/JSTests/modules/import-slots/star-conflict.js b/JSTests/modules/import-slots/star-conflict.js new file mode 100644 index 0000000000000..dd93e41d52e36 --- /dev/null +++ b/JSTests/modules/import-slots/star-conflict.js @@ -0,0 +1,2 @@ +export * from "./second.js"; +export * from "./third.js"; diff --git a/JSTests/modules/import-slots/third.js b/JSTests/modules/import-slots/third.js new file mode 100644 index 0000000000000..d8cdbb8cf5fcd --- /dev/null +++ b/JSTests/modules/import-slots/third.js @@ -0,0 +1,3 @@ +export const first = "third:first"; +export const third = "third:third"; +export default { tag: "third:default" }; diff --git a/JSTests/modules/import-slots/tla-dep.js b/JSTests/modules/import-slots/tla-dep.js new file mode 100644 index 0000000000000..07a13b76546c1 --- /dev/null +++ b/JSTests/modules/import-slots/tla-dep.js @@ -0,0 +1,4 @@ +export let ready = "pending"; +await Promise.resolve(); +ready = "ready"; +export const afterAwait = "after"; diff --git a/JSTests/modules/import-slots/tla-main.js b/JSTests/modules/import-slots/tla-main.js new file mode 100644 index 0000000000000..5d73895430997 --- /dev/null +++ b/JSTests/modules/import-slots/tla-main.js @@ -0,0 +1,7 @@ +import { ready, afterAwait } from "./tla-dep.js"; +import { count, increment } from "./values.js"; +export const seenAtStart = [ready, afterAwait]; +increment(); +await null; +export const seenAfterAwait = [ready, afterAwait, count]; +export function read() { return [ready, count]; } diff --git a/JSTests/modules/import-slots/unused.js b/JSTests/modules/import-slots/unused.js new file mode 100644 index 0000000000000..4c8a1fe03782c --- /dev/null +++ b/JSTests/modules/import-slots/unused.js @@ -0,0 +1,7 @@ +import { count, increment, object, constant, declared, Klass } from "./values.js"; +import * as namespace from "./values.js"; +import defaultFunction from "./values.js"; +import { first, second } from "./second.js"; +export const evaluated = true; +export function neverCalled() { return [count, increment, object, constant, declared, Klass, namespace, defaultFunction, first, second]; } +export function onlySecond() { return second; } diff --git a/JSTests/modules/import-slots/values.js b/JSTests/modules/import-slots/values.js new file mode 100644 index 0000000000000..7d73fc79749d2 --- /dev/null +++ b/JSTests/modules/import-slots/values.js @@ -0,0 +1,9 @@ +export let count = 0; +export function increment() { return ++count; } +export function set(value) { count = value; } +export const object = { tag: "object" }; +export const constant = 42; +export var variable = "var"; +export function declared() { return "declared"; } +export class Klass { static tag = "class"; } +export default function defaultFunction() { return "default"; } diff --git a/JSTests/wasm/modules/js-wasm-cycle-loaders.js b/JSTests/wasm/modules/js-wasm-cycle-loaders.js new file mode 100644 index 0000000000000..3e90b318ad075 --- /dev/null +++ b/JSTests/wasm/modules/js-wasm-cycle-loaders.js @@ -0,0 +1,24 @@ +import * as assert from '../assert.js'; + +// The JS<->Wasm cycle of js-wasm-cycle.js in several module loaders. entry-function.js imports f2 +// from function.wasm, a module whose environment exists only once it has linked. Loaded with +// entry-function.js as the root, the wasm module links first and the JS module's code is shared +// by records that import f2 at the same place in it. Loaded with function.wasm as the root, the +// JS module links while the wasm module has no environment yet, so its imports cannot be compared +// with another record's then: it links code of its own, which later records are compared with +// once that is possible. +const executableOf = f => /->(0x[0-9a-f]+),/.exec($vm.codeBlockFor(f))[1]; +const sameCode = (f, g) => executableOf(f) === executableOf(g); +const jsRoot = () => $vm.moduleLoaderImport($vm.createModuleLoader(), "./js-wasm-cycle/entry-function.js"); +async function wasmRoot() { + const loader = $vm.createModuleLoader(); + await $vm.moduleLoaderImport(loader, "./js-wasm-cycle/function.wasm"); + return $vm.moduleLoaderImport(loader, "./js-wasm-cycle/entry-function.js"); +} + +const instances = [await jsRoot(), await wasmRoot(), await jsRoot(), await jsRoot(), await wasmRoot()]; +const [m1, m2, m3, m4, m5] = instances; +for (let i = 0; i < testLoopCount; ++i) + assert.eq(instances.map(instance => [instance.f2(), instance.f(i)].join()).join(";"), Array(5).fill(`43,${i + 1}`).join(";")); +assert.eq([sameCode(m1.f, m2.f), sameCode(m1.f, m3.f), sameCode(m2.f, m3.f), sameCode(m3.f, m4.f), sameCode(m4.f, m5.f)].join(), "false,false,true,true,false"); +assert.eq([m1.f2 === m3.f2, m2.f2 === m3.f2, m3.f2 === m4.f2].join(), "false,false,false"); diff --git a/Source/JavaScriptCore/runtime/AbstractModuleRecord.cpp b/Source/JavaScriptCore/runtime/AbstractModuleRecord.cpp index 88942329c412d..f78c3f07c1416 100644 --- a/Source/JavaScriptCore/runtime/AbstractModuleRecord.cpp +++ b/Source/JavaScriptCore/runtime/AbstractModuleRecord.cpp @@ -452,11 +452,16 @@ auto AbstractModuleRecord::prelinkedResolution(JSGlobalObject* globalObject, Pre // names variables, not import indices); the binding is computed once per import and reused by every use site. auto AbstractModuleRecord::tryResolveImportPrelinked(JSGlobalObject* globalObject, const Identifier& localName) -> std::optional { - const auto& module = prelinkedModule(); - const PrelinkedModuleGraph::Import* import = m_prelinked->findImport(module, localName.impl()); + const PrelinkedModuleGraph::Import* import = m_prelinked->findImport(prelinkedModule(), localName.impl()); if (!import) return Resolution::notFound(); - auto imports = m_prelinked->imports(module); + return tryResolveImportPrelinked(globalObject, *import); +} + +auto AbstractModuleRecord::tryResolveImportPrelinked(JSGlobalObject* globalObject, const PrelinkedModuleGraph::Import& entry) -> std::optional +{ + const PrelinkedModuleGraph::Import* import = &entry; + auto imports = m_prelinked->imports(prelinkedModule()); if (m_prelinkedImportResolutions.size() != imports.size()) [[unlikely]] m_prelinkedImportResolutions = FixedVector(imports.size()); Resolution& memo = m_prelinkedImportResolutions[import - imports.data()]; diff --git a/Source/JavaScriptCore/runtime/AbstractModuleRecord.h b/Source/JavaScriptCore/runtime/AbstractModuleRecord.h index 862e6eef78d1f..84bc22718de3f 100644 --- a/Source/JavaScriptCore/runtime/AbstractModuleRecord.h +++ b/Source/JavaScriptCore/runtime/AbstractModuleRecord.h @@ -356,6 +356,7 @@ class AbstractModuleRecord : public JSInternalFieldObjectImpl<2> { protected: // nullopt: not answerable from the graph (take the by-name path). std::optional tryResolveImportPrelinked(JSGlobalObject*, const Identifier& localName); + std::optional tryResolveImportPrelinked(JSGlobalObject*, const PrelinkedModuleGraph::Import&); std::optional tryResolveExportPrelinked(JSGlobalObject*, const Identifier& exportName); std::optional tryResolveExportPrelinked(JSGlobalObject*, const PrelinkedModuleGraph::Export&); std::optional prelinkedResolution(JSGlobalObject*, PrelinkedModuleGraph::ResolutionKind, uint32_t resolvedModule, uint32_t resolvedLocalSid, uint32_t requestIndex, uint32_t importNameSid); diff --git a/Source/JavaScriptCore/runtime/JSModuleRecord.cpp b/Source/JavaScriptCore/runtime/JSModuleRecord.cpp index 3bcce2d0484c6..9e89b94543c6b 100644 --- a/Source/JavaScriptCore/runtime/JSModuleRecord.cpp +++ b/Source/JavaScriptCore/runtime/JSModuleRecord.cpp @@ -252,20 +252,25 @@ JSValue JSModuleRecord::evaluate(JSGlobalObject* globalObject, JSValue sentValue // Every module this one imports from has its environment now. Filling the import // slots here rather than on first use (JSModuleEnvironment::fillImportSlot) keeps - // optimized code shared with other records from meeting an empty slot per record. + // optimized code shared with other records from meeting an empty slot per record; + // code only this record runs fills them on first use. ModuleProgramExecutable* executable = m_moduleProgramExecutable.get(); - JSModuleEnvironment* environment = moduleEnvironment(); - for (unsigned i = 0, count = importSlotCount(); i < count; ++i) { - if (environment->importSlot(i)) - continue; - Resolution resolution = resolveImport(globalObject, importSlotNames()[i]); + if (executable->isShared()) { + JSModuleEnvironment* environment = moduleEnvironment(); + forEachImportSlot([&](unsigned index, const Identifier& localName) { + if (environment->importSlot(index)) + return IterationStatus::Continue; + Resolution resolution = resolveImport(globalObject, localName); + RETURN_IF_EXCEPTION(scope, IterationStatus::Done); + if (resolution.type == Resolution::Type::Resolved) + environment->importSlot(index).set(vm, environment, resolution.moduleRecord->moduleEnvironment()); + return IterationStatus::Continue; + }); if (scope.exception()) [[unlikely]] { didFinishWithExecutable(vm); return { }; } - if (resolution.type == Resolution::Type::Resolved) - environment->importSlot(i).set(vm, environment, resolution.moduleRecord->moduleEnvironment()); } JSValue resultOrAwaitedValue = vm.interpreter.executeModuleProgram(this, executable, globalObject, moduleEnvironment(), sentValue, resumeMode); @@ -332,34 +337,100 @@ void JSModuleRecord::execute(JSGlobalObject* globalObject, JSPromise* capability const Vector& JSModuleRecord::importSlotNames() { - if (!m_importSlotNames) { - Vector names; #if USE(BUN_JSC_ADDITIONS) - if (importEntriesArePrelinked()) { - for (const auto& import : prelinkedGraph()->imports(prelinkedModule())) { - if (!import.isNamespace()) - names.append(prelinkedGraph()->identifier(import.localSid)); - } - } else + ASSERT(!isPrelinked()); #endif - { - for (const auto& entry : importEntries().values()) { - if (entry.type != ImportEntryType::Namespace) - names.append(entry.localName); - } + if (!m_importSlotNames) { + Vector names; + for (const auto& entry : importEntries().values()) { + if (entry.type == ImportEntryType::Namespace) + continue; + m_importSlotIndices.add(entry.localName.impl(), names.size()); + names.append(entry.localName); } - std::sort(names.begin(), names.end(), [](const Identifier& a, const Identifier& b) { return codePointCompare(a.string(), b.string()) < 0; }); m_importSlotNames = WTF::move(names); } return *m_importSlotNames; } -unsigned JSModuleRecord::importSlotIndex(UniquedStringImpl* localName) +unsigned JSModuleRecord::importSlotCount() { +#if USE(BUN_JSC_ADDITIONS) + if (isPrelinked()) + return prelinkedModule().importCount; +#endif + return importSlotNames().size(); +} + +const Identifier& JSModuleRecord::importSlotLocalName(unsigned index) +{ +#if USE(BUN_JSC_ADDITIONS) + if (isPrelinked()) + return prelinkedGraph()->identifier(prelinkedGraph()->imports(prelinkedModule())[index].localSid); +#endif + return importSlotNames()[index]; +} + +template +void JSModuleRecord::forEachImportSlot(const Functor& functor) +{ +#if USE(BUN_JSC_ADDITIONS) + if (isPrelinked()) { + auto imports = prelinkedGraph()->imports(prelinkedModule()); + for (unsigned i = 0; i < imports.size(); ++i) { + if (!imports[i].isNamespace() && functor(i, importSlotLocalName(i)) == IterationStatus::Done) + return; + } + return; + } +#endif const Vector& names = importSlotNames(); - auto iterator = std::lower_bound(names.begin(), names.end(), localName, [](const Identifier& name, UniquedStringImpl* localName) { return codePointCompare(StringView(name.string()), StringView(*localName)) < 0; }); - RELEASE_ASSERT(iterator != names.end() && iterator->impl() == localName); - return iterator - names.begin(); + for (unsigned i = 0; i < names.size(); ++i) { + if (functor(i, names[i]) == IterationStatus::Done) + return; + } +} + +unsigned JSModuleRecord::importSlotIndex(UniquedStringImpl* localName) +{ +#if USE(BUN_JSC_ADDITIONS) + if (isPrelinked()) { + auto imports = prelinkedGraph()->imports(prelinkedModule()); + const auto* import = prelinkedGraph()->findImport(prelinkedModule(), localName); + RELEASE_ASSERT(import && !import->isNamespace()); + return import - imports.data(); + } +#endif + importSlotNames(); + auto iterator = m_importSlotIndices.find(localName); + RELEASE_ASSERT(iterator != m_importSlotIndices.end()); + return iterator->value; +} + +auto JSModuleRecord::resolveImportWithSlot(JSGlobalObject* globalObject, const Identifier& localName, unsigned& importSlot) -> Resolution +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + +#if USE(BUN_JSC_ADDITIONS) + // One lookup in the graph answers both; resolveImport() and importSlotIndex() would each make it. + if (isPrelinked() && !Options::validatePrelinkedModuleInfo()) [[likely]] { + const auto* import = prelinkedGraph()->findImport(prelinkedModule(), localName.impl()); + if (!import) + return Resolution::notFound(); + std::optional resolution = tryResolveImportPrelinked(globalObject, *import); + RETURN_IF_EXCEPTION(scope, Resolution::error()); + if (resolution) [[likely]] { + importSlot = import - prelinkedGraph()->imports(prelinkedModule()).data(); + return *resolution; + } + } +#endif + Resolution resolution = resolveImport(globalObject, localName); + RETURN_IF_EXCEPTION(scope, Resolution::error()); + if (resolution.type == Resolution::Type::Resolved) + importSlot = importSlotIndex(localName.impl()); + return resolution; } JSModuleEnvironment* JSModuleRecord::fillImportSlot(JSGlobalObject* globalObject, unsigned index) @@ -367,7 +438,7 @@ JSModuleEnvironment* JSModuleRecord::fillImportSlot(JSGlobalObject* globalObject VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); - Resolution resolution = resolveImport(globalObject, importSlotNames()[index]); + Resolution resolution = resolveImport(globalObject, importSlotLocalName(index)); RETURN_IF_EXCEPTION(scope, nullptr); RELEASE_ASSERT(resolution.type == Resolution::Type::Resolved); JSModuleEnvironment* environment = resolution.moduleRecord->moduleEnvironment(); @@ -381,27 +452,81 @@ std::optional JSModuleRecord::importe auto scope = DECLARE_THROW_SCOPE(vm); ModuleProgramExecutable::ImportedBindings bindings; - for (const Identifier& localName : importSlotNames()) { + bool comparable = true; + forEachImportSlot([&](unsigned index, const Identifier& localName) { +#if USE(BUN_JSC_ADDITIONS) + // An import the graph resolved to a binding of another of its modules, and that this record resolves to that + // binding of that module's record (what its code links against: tryResolveImportPrelinked keeps the answer), is + // left out. It is the same variable of the same source for every record of this module of the graph that resolves + // it so (createPrelinked() is given a graph module's own source), and only those are compared with each other + // (ModuleProgramExecutable::wasLinkedFor). A record that resolves it to anything else lists it, and so differs. + if (isPrelinked()) { + const auto& import = prelinkedGraph()->imports(prelinkedModule())[index]; + if (import.resolution() == PrelinkedModuleGraph::ResolutionKind::Binding) { + std::optional resolution = tryResolveImportPrelinked(globalObject, import); + RETURN_IF_EXCEPTION(scope, IterationStatus::Done); + if (resolution && resolution->type == Resolution::Type::Resolved) { + auto* exporter = dynamicDowncast(resolution->moduleRecord); + if (exporter && exporter->prelinkedGraph() == prelinkedGraph() && exporter->prelinkedIndex() == import.resolvedModule + && resolution->localName.impl() == prelinkedGraph()->identifier(import.resolvedLocalSid).impl()) + return IterationStatus::Continue; + } + } + } +#endif Resolution resolution = resolveImport(globalObject, localName); - RETURN_IF_EXCEPTION(scope, std::nullopt); + RETURN_IF_EXCEPTION(scope, IterationStatus::Done); if (resolution.type != Resolution::Type::Resolved) - continue; + return IterationStatus::Continue; if (auto* sourceTextModule = dynamicDowncast(resolution.moduleRecord)) { - bindings.append({ localName.impl(), sourceTextModule->sourceCode().provider(), resolution.localName.impl(), 0 }); - continue; + bindings.append({ localName.impl(), sourceTextModule->sourceCode().provider(), resolution.localName.impl(), 0, index }); + return IterationStatus::Continue; } JSModuleEnvironment* environment = resolution.moduleRecord->moduleEnvironmentMayBeNull(); - if (!environment) - return std::nullopt; + if (!environment) { + comparable = false; + return IterationStatus::Done; + } SymbolTable* symbolTable = environment->symbolTable(); ConcurrentJSLocker locker(symbolTable->m_lock); auto iterator = symbolTable->find(locker, resolution.localName.impl()); RELEASE_ASSERT(iterator != symbolTable->end(locker)); - bindings.append({ localName.impl(), nullptr, resolution.localName.impl(), iterator->value.scopeOffset().offset() }); - } + bindings.append({ localName.impl(), nullptr, resolution.localName.impl(), iterator->value.scopeOffset().offset(), index }); + return IterationStatus::Continue; + }); + RETURN_IF_EXCEPTION(scope, std::nullopt); + if (!comparable) + return std::nullopt; return bindings; } +bool JSModuleRecord::resolvesImportsLike(JSGlobalObject* globalObject, ModuleProgramExecutable* executable) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + +#if USE(BUN_JSC_ADDITIONS) + if (!executable->wasLinkedFor(prelinkedGraph(), isPrelinked() ? prelinkedIndex() : PrelinkedModuleGraph::noModule)) + return false; +#endif + if (!executable->linkerImportedBindings()) { + // Nothing resolved the linker's imports for this when it linked the executable: there was no record to compare it + // with. What they resolve to does not change, so now is as good. A linker that is gone, or whose imports cannot + // be compared, leaves nothing to compare with. + JSModuleRecord* linker = executable->linker(); + if (!linker) + return false; + std::optional bindings = linker->importedBindings(globalObject); + RETURN_IF_EXCEPTION(scope, false); + executable->setLinkerImportedBindings(WTF::move(bindings)); + if (!executable->linkerImportedBindings()) + return false; + } + std::optional mine = importedBindings(globalObject); + RETURN_IF_EXCEPTION(scope, false); + return mine && *mine == *executable->linkerImportedBindings(); +} + ModuleProgramExecutable* JSModuleRecord::getOrMakeExecutable(JSGlobalObject* globalObject) { ModuleProgramExecutable* executable = m_moduleProgramExecutable.get(); @@ -419,8 +544,8 @@ ModuleProgramExecutable* JSModuleRecord::getOrMakeExecutable(JSGlobalObject* glo // loaders' module scopes have the same symbol tables, share the executable: // CodeBlocks, JIT code and the function declarations' executables. A record for // which these differ links its own, which later records are then compared against. - std::optional bindings = importedBindings(globalObject); - RETURN_IF_EXCEPTION(scope, nullptr); + // Nothing about the imports is resolved for this until a second record for the key and module scope turns up + // (resolvesImportsLike): a record that is the only one to link a module pays for the comparison nothing. Vector moduleScopeSymbolTables; for (JSScope* moduleScope = moduleLoader()->moduleScope(); moduleScope != globalObject->globalLexicalEnvironment(); moduleScope = moduleScope->next()) moduleScopeSymbolTables.append(uncheckedDowncast(moduleScope)->symbolTable()); @@ -429,7 +554,7 @@ ModuleProgramExecutable* JSModuleRecord::getOrMakeExecutable(JSGlobalObject* glo // reused for another module fails the URL / source comparison and is replaced. auto& executables = globalObject->moduleProgramExecutables(); JSGlobalObject::ModuleProgramExecutableKey key { moduleKey().impl(), moduleScopeSymbolTables.isEmpty() ? nullptr : moduleScopeSymbolTables.first() }; - if (bindings) { + { ModuleProgramExecutable* shared = executables.get(key); // (An executable whose code was deleted, ScriptExecutable::clearCode, is left to the // records that have it. One that only let go of unlinked code it can decode again, @@ -437,24 +562,28 @@ ModuleProgramExecutable* JSModuleRecord::getOrMakeExecutable(JSGlobalObject* glo // executable's code is in the mode of its first code, see getUnlinkedCodeBlock, which // has to be the one this record would ask for.) if (shared && (shared->unlinkedCodeBlock() || shared->hasReleasedUnlinkedCode()) && shared->codeGenerationMode() == globalObject->defaultCodeGenerationMode() - && shared->importedBindings() == bindings && shared->hasModuleScopeSymbolTables(moduleScopeSymbolTables) + && shared->hasModuleScopeSymbolTables(moduleScopeSymbolTables) && shared->source().provider()->sourceURL() == sourceCode().provider()->sourceURL() && shared->source().provider()->hash() == sourceCode().provider()->hash() && shared->source().view() == sourceCode().view()) { - if (!shared->unlinkedCodeBlock()) { - shared->getUnlinkedCodeBlock(globalObject); - RETURN_IF_EXCEPTION(scope, nullptr); + bool alike = resolvesImportsLike(globalObject, shared); + RETURN_IF_EXCEPTION(scope, nullptr); + if (alike) { + if (!shared->unlinkedCodeBlock()) { + shared->getUnlinkedCodeBlock(globalObject); + RETURN_IF_EXCEPTION(scope, nullptr); + } + shared->willBeEvaluatedByAnotherRecord(); + shared->didShare(); + m_moduleProgramExecutable.set(vm, this, shared); + return shared; } - shared->willBeEvaluatedByAnotherRecord(); - m_moduleProgramExecutable.set(vm, this, shared); - return shared; } } - executable = ModuleProgramExecutable::tryCreate(globalObject, sourceCode(), WTF::move(bindings), moduleScopeSymbolTables); + executable = ModuleProgramExecutable::tryCreate(globalObject, sourceCode(), this, moduleScopeSymbolTables); RETURN_IF_EXCEPTION(scope, nullptr); executable->willBeEvaluatedByAnotherRecord(); m_moduleProgramExecutable.set(vm, this, executable); - if (executable->importedBindings()) - executables.set(key, Weak(executable)); + executables.set(key, Weak(executable)); return executable; } diff --git a/Source/JavaScriptCore/runtime/JSModuleRecord.h b/Source/JavaScriptCore/runtime/JSModuleRecord.h index bb14a07f10951..d7e20eef002f8 100644 --- a/Source/JavaScriptCore/runtime/JSModuleRecord.h +++ b/Source/JavaScriptCore/runtime/JSModuleRecord.h @@ -97,24 +97,35 @@ class JSModuleRecord final : public CyclicModuleRecord { JSValue readFunctionDeclarationSlot(VM&, JSModuleEnvironment*, ScopeOffset); unsigned numberOfUninstantiatedFunctionDeclarations() const { return m_uninstantiatedFunctionDeclarations ? m_uninstantiatedFunctionDeclarations->remaining : 0; } - // Local names of the import entries that bind a single export (namespace imports - // are variables of this module's environment), sorted: the environment has one - // import slot per name in this order (JSModuleEnvironment::importSlot), so the order - // is the same for every record of the same source. - const Vector& importSlotNames(); - unsigned importSlotCount() { return importSlotNames().size(); } + // The environment has an import slot (JSModuleEnvironment::importSlot) for each import entry + // that binds a single export (namespace imports are variables of this module's environment), + // numbered the same way for every record of the same source: a prelinked record's by the + // entry's position among the graph's imports of the module (a namespace import's position is + // a slot nothing uses), any other record's in the order of its import entries. + unsigned importSlotCount(); unsigned importSlotIndex(UniquedStringImpl* localName); + // resolveImport(localName) and, for a resolved binding, its import slot. + Resolution resolveImportWithSlot(JSGlobalObject*, const Identifier& localName, unsigned& importSlot); JSModuleEnvironment* fillImportSlot(JSGlobalObject*, unsigned index); std::optional importedBindings(JSGlobalObject*); + // Whether every import binding of this record is at the same place for `executable`'s code as for the record it + // was linked for (its linker): the same slot, and the same variable of a module with the same source (or, for an + // exporter that is not a source text module, at the same offset). + bool resolvesImportsLike(JSGlobalObject*, ModuleProgramExecutable*); private: JSModuleRecord(VM&, Structure*, JSModuleLoader*, const Identifier&, const SourceCode&, CodeFeatures); void finishCreation(JSGlobalObject*, VM&); + const Vector& importSlotNames(); // not prelinked: the local names that have a slot, in the import entries' order + const Identifier& importSlotLocalName(unsigned index); + template void forEachImportSlot(const Functor&); // (index, local name) -> IterationStatus + SourceCode m_sourceCode; WriteBarrier m_moduleProgramExecutable; std::optional> m_importSlotNames; + HashMap, unsigned, IdentifierRepHash> m_importSlotIndices; // filled with m_importSlotNames CodeFeatures m_features; struct UninstantiatedFunctionDeclarations { diff --git a/Source/JavaScriptCore/runtime/JSScope.cpp b/Source/JavaScriptCore/runtime/JSScope.cpp index 440e69cbc528e..c44c9928f64d2 100644 --- a/Source/JavaScriptCore/runtime/JSScope.cpp +++ b/Source/JavaScriptCore/runtime/JSScope.cpp @@ -92,7 +92,9 @@ static inline bool abstractAccess(JSGlobalObject* globalObject, JSScope* scope, JSModuleEnvironment* moduleEnvironment = uncheckedDowncast(scope); AbstractModuleRecord* moduleRecord = moduleEnvironment->moduleRecord(); auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); - AbstractModuleRecord::Resolution resolution = moduleRecord->resolveImport(globalObject, ident); + unsigned importSlot = 0; + auto* sourceTextModuleRecord = dynamicDowncast(moduleRecord); + AbstractModuleRecord::Resolution resolution = sourceTextModuleRecord ? sourceTextModuleRecord->resolveImportWithSlot(globalObject, ident, importSlot) : moduleRecord->resolveImport(globalObject, ident); catchScope.releaseAssertNoException(); if (resolution.type == AbstractModuleRecord::Resolution::Type::Resolved) { AbstractModuleRecord* importedRecord = resolution.moduleRecord; @@ -103,7 +105,8 @@ static inline bool abstractAccess(JSGlobalObject* globalObject, JSScope* scope, ASSERT(iter != symbolTable->end(locker)); SymbolTableEntry& entry = iter->value; ASSERT(!entry.isNull()); - unsigned moduleImportSlot = JSModuleEnvironment::importSlotScopeOffset(moduleEnvironment->symbolTable(), uncheckedDowncast(moduleRecord)->importSlotIndex(ident.impl())).offset(); + RELEASE_ASSERT(sourceTextModuleRecord); + unsigned moduleImportSlot = JSModuleEnvironment::importSlotScopeOffset(moduleEnvironment->symbolTable(), importSlot).offset(); op = ResolveOp(makeType(ModuleVar, needsVarInjectionChecks), depth, nullptr, importedEnvironment, entry.watchpointSet(), entry.scopeOffset().offset(), resolution.localName.impl(), moduleImportSlot); return true; } diff --git a/Source/JavaScriptCore/runtime/ModuleProgramExecutable.cpp b/Source/JavaScriptCore/runtime/ModuleProgramExecutable.cpp index a29ddd8ee9025..ed023067c2a4a 100644 --- a/Source/JavaScriptCore/runtime/ModuleProgramExecutable.cpp +++ b/Source/JavaScriptCore/runtime/ModuleProgramExecutable.cpp @@ -30,20 +30,28 @@ #include "Debugger.h" #include "Error.h" #include "FunctionExecutable.h" +#include "JSModuleRecord.h" #include "UnlinkedFunctionExecutable.h" #include "UnlinkedModuleProgramCodeBlock.h" +#include "WeakInlines.h" namespace JSC { const ClassInfo ModuleProgramExecutable::s_info = { "ModuleProgramExecutable"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(ModuleProgramExecutable) }; -ModuleProgramExecutable::ModuleProgramExecutable(JSGlobalObject* globalObject, const SourceCode& source, std::optional&& importedBindings, const Vector& moduleScopeSymbolTables) +ModuleProgramExecutable::ModuleProgramExecutable(JSGlobalObject* globalObject, const SourceCode& source, JSModuleRecord* linker, const Vector& moduleScopeSymbolTables) : Base(globalObject->vm().moduleProgramExecutableStructure.get(), globalObject->vm(), source, StrictModeLexicallyScopedFeature, DerivedContextType::None, false, false, EvalContextType::None, NoIntrinsic) - , m_importedBindings(WTF::move(importedBindings)) + , m_linker(linker) , m_moduleScopeSymbolTables(moduleScopeSymbolTables.size()) { for (unsigned i = 0; i < moduleScopeSymbolTables.size(); ++i) m_moduleScopeSymbolTables[i].setWithoutWriteBarrier(moduleScopeSymbolTables[i]); +#if USE(BUN_JSC_ADDITIONS) + if (linker && linker->isPrelinked()) { + m_linkerPrelinkedGraph = linker->prelinkedGraph(); + m_linkerPrelinkedIndex = linker->prelinkedIndex(); + } +#endif SourceProviderSourceType sourceType = source.provider()->sourceType(); ASSERT(sourceType == SourceProviderSourceType::Module #if USE(BUN_JSC_ADDITIONS) @@ -115,6 +123,17 @@ UnlinkedModuleProgramCodeBlock* ModuleProgramExecutable::getUnlinkedCodeBlock(JS RELEASE_AND_RETURN(throwScope, unlinkedModuleProgramCode); } +JSModuleRecord* ModuleProgramExecutable::linker() const +{ + return m_linker.get(); +} + +void ModuleProgramExecutable::setLinkerImportedBindings(std::optional&& bindings) +{ + m_linkerImportedBindings = WTF::move(bindings); + m_linker.clear(); +} + bool ModuleProgramExecutable::hasModuleScopeSymbolTables(const Vector& symbolTables) const { if (m_moduleScopeSymbolTables.size() != symbolTables.size()) @@ -128,7 +147,7 @@ bool ModuleProgramExecutable::hasModuleScopeSymbolTables(const Vector&& importedBindings, const Vector& moduleScopeSymbolTables) +ModuleProgramExecutable* ModuleProgramExecutable::tryCreate(JSGlobalObject* globalObject, const SourceCode& source, JSModuleRecord* linker, const Vector& moduleScopeSymbolTables) { VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); - ModuleProgramExecutable* executable = new (NotNull, allocateCell(vm)) ModuleProgramExecutable(globalObject, source, WTF::move(importedBindings), moduleScopeSymbolTables); + ModuleProgramExecutable* executable = new (NotNull, allocateCell(vm)) ModuleProgramExecutable(globalObject, source, linker, moduleScopeSymbolTables); executable->finishCreation(vm); if (!executable->getUnlinkedCodeBlock(globalObject)) [[unlikely]] // This generates and binds unlinked code block. return nullptr; diff --git a/Source/JavaScriptCore/runtime/ModuleProgramExecutable.h b/Source/JavaScriptCore/runtime/ModuleProgramExecutable.h index 0dcc8f9d0b1e5..4ec4b08282ce1 100644 --- a/Source/JavaScriptCore/runtime/ModuleProgramExecutable.h +++ b/Source/JavaScriptCore/runtime/ModuleProgramExecutable.h @@ -26,10 +26,15 @@ #pragma once #include "GlobalExecutable.h" +#if USE(BUN_JSC_ADDITIONS) +#include "PrelinkedModuleGraph.h" +#endif +#include "Weak.h" namespace JSC { class FunctionExecutable; +class JSModuleRecord; class SymbolTable; class UnlinkedFunctionExecutable; class UnlinkedModuleProgramCodeBlock; @@ -49,20 +54,22 @@ class ModuleProgramExecutable final : public GlobalExecutable { // What an imported binding read by this executable's code resolves to: the // exporting module's source (whose text fixes the binding's ScopeOffset) and the // binding's name there, or for exporters that are not source text modules the - // ScopeOffset itself. + // ScopeOffset itself; and the import slot the code reads the exporter from. struct ImportedBinding { RefPtr localName; RefPtr exporterSource; RefPtr exporterLocalName; unsigned offset { 0 }; + unsigned importSlot { 0 }; bool operator==(const ImportedBinding&) const; }; using ImportedBindings = Vector; + // linker: the record the executable is made for, if other records may come to share it (JSModuleRecord::getOrMakeExecutable). // moduleScopeSymbolTables: the symbol tables of the lexical environments between // the module environment and the global lexical environment (JSModuleLoader::moduleScope); // linked code embeds their variables' offsets too. - JS_EXPORT_PRIVATE static ModuleProgramExecutable* tryCreate(JSGlobalObject*, const SourceCode&, std::optional&& = std::nullopt, const Vector& moduleScopeSymbolTables = { }); + JS_EXPORT_PRIVATE static ModuleProgramExecutable* tryCreate(JSGlobalObject*, const SourceCode&, JSModuleRecord* linker = nullptr, const Vector& moduleScopeSymbolTables = { }); static void destroy(JSCell*); @@ -106,7 +113,19 @@ class ModuleProgramExecutable final : public GlobalExecutable { // linkedFunctionDeclaration() first, and linkFunctionDeclaration() with the module's own functionDecl(index) if that is null. FunctionExecutable* linkedFunctionDeclaration(unsigned index) const { return index < m_functionDeclarations.size() ? m_functionDeclarations[index].get() : nullptr; } FunctionExecutable* linkFunctionDeclaration(VM&, unsigned index, UnlinkedFunctionExecutable*); - const std::optional& importedBindings() const { return m_importedBindings; } + // What another record's import bindings are compared with before it shares this executable: the linker's + // (JSModuleRecord::importedBindings), which nothing resolves until there is such a record. The linker, to ask, until + // they have been asked for; with neither, there is nothing to compare with. + JSModuleRecord* linker() const; + const std::optional& linkerImportedBindings() const { return m_linkerImportedBindings; } + void setLinkerImportedBindings(std::optional&&); +#if USE(BUN_JSC_ADDITIONS) + // The linker was module `moduleIndex` of `graph` (JSModuleRecord::createPrelinked), or with null was not such a record. + bool wasLinkedFor(PrelinkedModuleGraph* graph, uint32_t moduleIndex) const { return m_linkerPrelinkedGraph.get() == graph && m_linkerPrelinkedIndex == moduleIndex; } +#endif + // A second record has taken this executable: its code now runs against more than one module environment. + bool isShared() const { return m_isShared; } + void didShare() { m_isShared = true; } bool hasModuleScopeSymbolTables(const Vector&) const; // Whether the module environment is created directly in the global lexical environment (JSModuleLoader::moduleScope). bool resolvesInGlobalScope() const { return m_moduleScopeSymbolTables.isEmpty(); } @@ -134,16 +153,22 @@ class ModuleProgramExecutable final : public GlobalExecutable { friend class ExecutableBase; friend class ScriptExecutable; - ModuleProgramExecutable(JSGlobalObject*, const SourceCode&, std::optional&&, const Vector& moduleScopeSymbolTables); + ModuleProgramExecutable(JSGlobalObject*, const SourceCode&, JSModuleRecord* linker, const Vector& moduleScopeSymbolTables); WriteBarrier m_moduleEnvironmentSymbolTable; FixedVector> m_functionDeclarations; - std::optional m_importedBindings; + Weak m_linker; +#if USE(BUN_JSC_ADDITIONS) + RefPtr m_linkerPrelinkedGraph; + uint32_t m_linkerPrelinkedIndex { PrelinkedModuleGraph::noModule }; +#endif + std::optional m_linkerImportedBindings; FixedVector> m_moduleScopeSymbolTables; FixedVector> m_functionExpressions; unsigned m_recordsYetToFinishEvaluation { 0 }; bool m_hasBeenEvaluated { false }; bool m_hasReleasedUnlinkedCode { false }; + bool m_isShared { false }; OptionSet m_codeGenerationMode; std::unique_ptr m_templateObjectMap; };