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; };