diff --git a/test/language/export/export-defer/load-and-evaluation/circular-references/consumer-cycle/barrel_FIXTURE.js b/test/language/export/export-defer/load-and-evaluation/circular-references/consumer-cycle/barrel_FIXTURE.js new file mode 100644 index 00000000000..67dc1622632 --- /dev/null +++ b/test/language/export/export-defer/load-and-evaluation/circular-references/consumer-cycle/barrel_FIXTURE.js @@ -0,0 +1,4 @@ +// Copyright (C) 2026 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +export defer { x } from "./entry_FIXTURE.js"; diff --git a/test/language/export/export-defer/load-and-evaluation/circular-references/consumer-cycle/consumer-cycle.js b/test/language/export/export-defer/load-and-evaluation/circular-references/consumer-cycle/consumer-cycle.js new file mode 100644 index 00000000000..2a663d82f43 --- /dev/null +++ b/test/language/export/export-defer/load-and-evaluation/circular-references/consumer-cycle/consumer-cycle.js @@ -0,0 +1,28 @@ +// Copyright (C) 2026 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-resolveexport +description: A deferred re-export forming a cycle with its consumer throws SyntaxError at link time +info: | + ResolveExport ( exportName [ , resolveSet [ , deferNamespaceExportSet ] ] ) + 1. Assert: module.[[Status]] is not NEW. + 1. If resolveSet is not present, set resolveSet to a new empty List. + 1. If deferNamespaceExportSet is not present, set deferNamespaceExportSet to a new empty List. + 1. For each Record { [[Module]], [[ExportName]] } r of resolveSet, do + 1. If module and r.[[Module]] are the same Module Record and exportName is r.[[ExportName]], then + 1. Assert: This is a circular import request. + 1. Return null. + 1. ... + + Like we have for ordinary re-exports, circular deferred re-exports also throws SyntaxError. +flags: [module, async] +features: [export-defer, dynamic-import] +includes: [asyncHelpers.js] +---*/ + +asyncTest(async () => { + let err; + await import("./entry_FIXTURE.js").catch((e) => { err = e; }); + assert.sameValue(err instanceof SyntaxError, true, "consumer-cycle throws SyntaxError"); +}); diff --git a/test/language/export/export-defer/load-and-evaluation/circular-references/consumer-cycle/entry_FIXTURE.js b/test/language/export/export-defer/load-and-evaluation/circular-references/consumer-cycle/entry_FIXTURE.js new file mode 100644 index 00000000000..9c788500316 --- /dev/null +++ b/test/language/export/export-defer/load-and-evaluation/circular-references/consumer-cycle/entry_FIXTURE.js @@ -0,0 +1,4 @@ +// Copyright (C) 2026 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +import { x } from "./barrel_FIXTURE.js"; diff --git a/test/language/export/export-defer/load-and-evaluation/circular-references/cycle-imported/a_FIXTURE.js b/test/language/export/export-defer/load-and-evaluation/circular-references/cycle-imported/a_FIXTURE.js new file mode 100644 index 00000000000..89c0e82e489 --- /dev/null +++ b/test/language/export/export-defer/load-and-evaluation/circular-references/cycle-imported/a_FIXTURE.js @@ -0,0 +1,5 @@ +// Copyright (C) 2026 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +globalThis.evaluations.push("a"); +export defer { x } from "./b_FIXTURE.js"; diff --git a/test/language/export/export-defer/load-and-evaluation/circular-references/cycle-imported/b_FIXTURE.js b/test/language/export/export-defer/load-and-evaluation/circular-references/cycle-imported/b_FIXTURE.js new file mode 100644 index 00000000000..a3c84c46c2b --- /dev/null +++ b/test/language/export/export-defer/load-and-evaluation/circular-references/cycle-imported/b_FIXTURE.js @@ -0,0 +1,5 @@ +// Copyright (C) 2026 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +globalThis.evaluations.push("b"); +export defer { x } from "./a_FIXTURE.js"; diff --git a/test/language/export/export-defer/load-and-evaluation/circular-references/cycle-imported/consumer_FIXTURE.js b/test/language/export/export-defer/load-and-evaluation/circular-references/cycle-imported/consumer_FIXTURE.js new file mode 100644 index 00000000000..01d2069c22e --- /dev/null +++ b/test/language/export/export-defer/load-and-evaluation/circular-references/cycle-imported/consumer_FIXTURE.js @@ -0,0 +1,4 @@ +// Copyright (C) 2026 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +import { x } from "./a_FIXTURE.js"; diff --git a/test/language/export/export-defer/load-and-evaluation/circular-references/cycle-imported/cycle-imported-throws.js b/test/language/export/export-defer/load-and-evaluation/circular-references/cycle-imported/cycle-imported-throws.js new file mode 100644 index 00000000000..4916625b876 --- /dev/null +++ b/test/language/export/export-defer/load-and-evaluation/circular-references/cycle-imported/cycle-imported-throws.js @@ -0,0 +1,26 @@ +// Copyright (C) 2026 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-resolveexport +description: Cyclic deferred re-exports of the same binding throw SyntaxError when the binding is imported +info: | + ResolveExport ( exportName [ , resolveSet [ , deferNamespaceExportSet ] ] ) + 1. Assert: module.[[Status]] is not NEW. + 1. If resolveSet is not present, set resolveSet to a new empty List. + 1. If deferNamespaceExportSet is not present, set deferNamespaceExportSet to a new empty List. + 1. For each Record { [[Module]], [[ExportName]] } r of resolveSet, do + 1. If module and r.[[Module]] are the same Module Record and exportName is r.[[ExportName]], then + 1. Assert: This is a circular import request. + 1. Return null. + 1. ... +flags: [module, async] +features: [export-defer, dynamic-import] +includes: [asyncHelpers.js] +---*/ + +asyncTest(async () => { + let err; + await import("./consumer_FIXTURE.js").catch((e) => { err = e; }); + assert.sameValue(err instanceof SyntaxError, true, "cyclic deferred re-exports throw SyntaxError when imported"); +}); diff --git a/test/language/export/export-defer/load-and-evaluation/circular-references/cycle-imported/cycle-not-imported-no-throw.js b/test/language/export/export-defer/load-and-evaluation/circular-references/cycle-imported/cycle-not-imported-no-throw.js new file mode 100644 index 00000000000..cc8deb07235 --- /dev/null +++ b/test/language/export/export-defer/load-and-evaluation/circular-references/cycle-imported/cycle-not-imported-no-throw.js @@ -0,0 +1,39 @@ +// Copyright (C) 2026 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-resolveexport +description: Cyclic deferred re-exports do not throw SyntaxError if the cyclic binding is never imported +info: | + InnerModuleLoading ( state, module, importedNames ) + 1. Assert: state.[[IsLoading]] is true. + 1. If module is a Cyclic Module Record, then + 1. ... + 1. Let optionalIndirectRequests be GetNewOptionalIndirectExportsModuleRequests(module, importedNames, state.[[Visited]]). + 1. Set requestsToLoad to the list-concatenation of requestsToLoad and optionalIndirectRequests. + 1. ... + + ResolveExport ( exportName [ , resolveSet [ , deferNamespaceExportSet ] ] ) + 1. Assert: module.[[Status]] is not NEW. + 1. If resolveSet is not present, set resolveSet to a new empty List. + 1. If deferNamespaceExportSet is not present, set deferNamespaceExportSet to a new empty List. + 1. For each Record { [[Module]], [[ExportName]] } r of resolveSet, do + 1. If module and r.[[Module]] are the same Module Record and exportName is r.[[ExportName]], then + 1. Assert: This is a circular import request. + 1. Return null. + 1. ... + + Not imported names aren't loaded, so their cycle nevergets visited and resolved. +flags: [module] +features: [export-defer] +includes: [compareArray.js] +---*/ + +import "../setup_FIXTURE.js"; +import "./a_FIXTURE.js"; + +assert.compareArray( + globalThis.evaluations, + ["a"], + "a evaluated; b's cycle never forced because no consumer requested x" +); diff --git a/test/language/export/export-defer/load-and-evaluation/circular-references/setup_FIXTURE.js b/test/language/export/export-defer/load-and-evaluation/circular-references/setup_FIXTURE.js new file mode 100644 index 00000000000..c72aabeac21 --- /dev/null +++ b/test/language/export/export-defer/load-and-evaluation/circular-references/setup_FIXTURE.js @@ -0,0 +1,4 @@ +// Copyright (C) 2026 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +globalThis.evaluations = []; diff --git a/test/language/export/export-defer/load-and-evaluation/runtime-errors/barrel-defer-throws_FIXTURE.js b/test/language/export/export-defer/load-and-evaluation/runtime-errors/barrel-defer-throws_FIXTURE.js new file mode 100644 index 00000000000..1c9012a62e0 --- /dev/null +++ b/test/language/export/export-defer/load-and-evaluation/runtime-errors/barrel-defer-throws_FIXTURE.js @@ -0,0 +1,5 @@ +// Copyright (C) 2026 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +globalThis.evaluations.push("barrel-defer-throws"); +export defer { x } from "./throws_FIXTURE.js"; diff --git a/test/language/export/export-defer/load-and-evaluation/runtime-errors/namespace-access-throws-repeated.js b/test/language/export/export-defer/load-and-evaluation/runtime-errors/namespace-access-throws-repeated.js new file mode 100644 index 00000000000..15fcbbbe881 --- /dev/null +++ b/test/language/export/export-defer/load-and-evaluation/runtime-errors/namespace-access-throws-repeated.js @@ -0,0 +1,47 @@ +// Copyright (C) 2026 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-module-namespace-exotic-objects-get-p-receiver-EnsureDeferredNamespaceEvaluation +description: Namespace [[Get]] of a deferred re-export with an erroring source throws on first access and on every subsequent access with the same error value +info: | + [[Get]] ( P, Receiver ) + 1. ... + 1. If m is a Cyclic Module Record and m.GetOptionalIndirectExportsModuleRequests(« P ») is not empty, then + 1. Perform ? EvaluateModuleSync(m, « P »). + 1. ... + + EvaluateModuleSync ( module [ , importedNames ] ) + 1. ... + 1. Let promise be module.Evaluate(importedNames). + 1. Assert: promise.[[PromiseState]] is either FULFILLED or REJECTED. + 1. If promise.[[PromiseState]] is REJECTED, then + 1. If promise.[[PromiseIsHandled]] is false, perform HostPromiseRejectionTracker(promise, "handle"). + 1. Set promise.[[PromiseIsHandled]] to true. + 1. Return ThrowCompletion(promise.[[PromiseResult]]). + 1. ... + + Evaluate ( [ importedNames ] ) + 1. ... + 1. If module.[[Status]] is either EVALUATING-ASYNC or EVALUATED, then + 1. Assert: module.[[CycleRoot]].[[TopLevelCapability]] is not EMPTY. + 1. Let topLevelPromise be module.[[CycleRoot]].[[TopLevelCapability]].[[Promise]]. + 1. ... + 1. If topLevelPromise.[[PromiseState]] is rejected, return topLevelPromise. + +flags: [module, async] +features: [export-defer, dynamic-import] +includes: [asyncHelpers.js] +---*/ + +import "./setup_FIXTURE.js"; + +asyncTest(async () => { + let err1; + try { await import("./barrel-defer-throws_FIXTURE.js"); } catch (e) { err1 = e; } + assert.sameValue(err1.someError, "the error from throws_FIXTURE"); + + let err2; + try { await import("./barrel-defer-throws_FIXTURE.js"); } catch (e) { err2 = e; } + assert.sameValue(err2, err1, "subsequent accesses throw the same error value"); +}); diff --git a/test/language/export/export-defer/load-and-evaluation/runtime-errors/never-imported.js b/test/language/export/export-defer/load-and-evaluation/runtime-errors/never-imported.js new file mode 100644 index 00000000000..3b31419dcac --- /dev/null +++ b/test/language/export/export-defer/load-and-evaluation/runtime-errors/never-imported.js @@ -0,0 +1,38 @@ +// Copyright (C) 2026 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-module-namespace-exotic-objects-get-p-receiver-EnsureDeferredNamespaceEvaluation +description: A deferred re-export source that throws on evaluation does not throw if its binding is never accessed +info: | + InnerModuleLoading ( state, module, importedNames ) + 1. Assert: state.[[IsLoading]] is true. + 1. If module is a Cyclic Module Record, then + 1. ... + 1. Let optionalIndirectRequests be GetNewOptionalIndirectExportsModuleRequests(module, importedNames, state.[[Visited]]). + 1. Set requestsToLoad to the list-concatenation of requestsToLoad and optionalIndirectRequests. + 1. ... + + ResolveExport ( exportName [ , resolveSet [ , deferNamespaceExportSet ] ] ) + 1. Assert: module.[[Status]] is not NEW. + 1. If resolveSet is not present, set resolveSet to a new empty List. + 1. If deferNamespaceExportSet is not present, set deferNamespaceExportSet to a new empty List. + 1. For each Record { [[Module]], [[ExportName]] } r of resolveSet, do + 1. If module and r.[[Module]] are the same Module Record and exportName is r.[[ExportName]], then + 1. Assert: This is a circular import request. + 1. Return null. + 1. ... + +flags: [module] +features: [export-defer] +includes: [compareArray.js] +---*/ + +import "./setup_FIXTURE.js"; +import "./barrel-defer-throws_FIXTURE.js"; + +assert.compareArray( + globalThis.evaluations, + ["barrel-defer-throws"], + "barrel evaluated; throws_FIXTURE was loaded but never evaluated" +); diff --git a/test/language/export/export-defer/load-and-evaluation/runtime-errors/reexport1_FIXTURE.js b/test/language/export/export-defer/load-and-evaluation/runtime-errors/reexport1_FIXTURE.js new file mode 100644 index 00000000000..a04da216738 --- /dev/null +++ b/test/language/export/export-defer/load-and-evaluation/runtime-errors/reexport1_FIXTURE.js @@ -0,0 +1,4 @@ +// Copyright (C) 2026 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +export defer { x } from "./throws_FIXTURE.js"; diff --git a/test/language/export/export-defer/load-and-evaluation/runtime-errors/reexport2_FIXTURE.js b/test/language/export/export-defer/load-and-evaluation/runtime-errors/reexport2_FIXTURE.js new file mode 100644 index 00000000000..a04da216738 --- /dev/null +++ b/test/language/export/export-defer/load-and-evaluation/runtime-errors/reexport2_FIXTURE.js @@ -0,0 +1,4 @@ +// Copyright (C) 2026 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +export defer { x } from "./throws_FIXTURE.js"; diff --git a/test/language/export/export-defer/load-and-evaluation/runtime-errors/setup_FIXTURE.js b/test/language/export/export-defer/load-and-evaluation/runtime-errors/setup_FIXTURE.js new file mode 100644 index 00000000000..c72aabeac21 --- /dev/null +++ b/test/language/export/export-defer/load-and-evaluation/runtime-errors/setup_FIXTURE.js @@ -0,0 +1,4 @@ +// Copyright (C) 2026 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +globalThis.evaluations = []; diff --git a/test/language/export/export-defer/load-and-evaluation/runtime-errors/throws_FIXTURE.js b/test/language/export/export-defer/load-and-evaluation/runtime-errors/throws_FIXTURE.js new file mode 100644 index 00000000000..7cb039d7bcb --- /dev/null +++ b/test/language/export/export-defer/load-and-evaluation/runtime-errors/throws_FIXTURE.js @@ -0,0 +1,6 @@ +// Copyright (C) 2026 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +globalThis.evaluations.push("throws"); +throw { someError: "the error from throws_FIXTURE" }; +export const x = "never-reached"; diff --git a/test/language/export/export-defer/load-and-evaluation/runtime-errors/two-namespaces-same-error.js b/test/language/export/export-defer/load-and-evaluation/runtime-errors/two-namespaces-same-error.js new file mode 100644 index 00000000000..a578a108ca7 --- /dev/null +++ b/test/language/export/export-defer/load-and-evaluation/runtime-errors/two-namespaces-same-error.js @@ -0,0 +1,47 @@ +// Copyright (C) 2026 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-EvaluateModuleSync +description: Two distinct deferred re-exports of x from the same throwing source surface the same error value through both namespaces +info: | + [[Get]] ( P, Receiver ) + 1. ... + 1. If m is a Cyclic Module Record and m.GetOptionalIndirectExportsModuleRequests(« P ») is not empty, then + 1. Perform ? EvaluateModuleSync(m, « P »). + 1. ... + + EvaluateModuleSync ( module [ , importedNames ] ) + 1. ... + 1. Let promise be module.Evaluate(importedNames). + 1. Assert: promise.[[PromiseState]] is either FULFILLED or REJECTED. + 1. If promise.[[PromiseState]] is REJECTED, then + 1. If promise.[[PromiseIsHandled]] is false, perform HostPromiseRejectionTracker(promise, "handle"). + 1. Set promise.[[PromiseIsHandled]] to true. + 1. Return ThrowCompletion(promise.[[PromiseResult]]). + 1. ... + + Evaluate ( [ importedNames ] ) + 1. ... + 1. If module.[[Status]] is either EVALUATING-ASYNC or EVALUATED, then + 1. Assert: module.[[CycleRoot]].[[TopLevelCapability]] is not EMPTY. + 1. Let topLevelPromise be module.[[CycleRoot]].[[TopLevelCapability]].[[Promise]]. + 1. ... + 1. If topLevelPromise.[[PromiseState]] is rejected, return topLevelPromise. + +flags: [module, async] +features: [export-defer, dynamic-import] +includes: [asyncHelpers.js] +---*/ + +import "./setup_FIXTURE.js"; + +asyncTest(async () => { + let err1; + try { await import("./reexport1_FIXTURE.js"); } catch (e) { err1 = e; } + let err2; + try { await import("./reexport2_FIXTURE.js"); } catch (e) { err2 = e; } + + assert.sameValue(err1.someError, "the error from throws_FIXTURE"); + assert.sameValue(err1, err2, "both namespaces yield the same error value (identity)"); +}); diff --git a/test/language/export/export-defer/load-and-evaluation/runtime-errors/used-throws.js b/test/language/export/export-defer/load-and-evaluation/runtime-errors/used-throws.js new file mode 100644 index 00000000000..0fb32110507 --- /dev/null +++ b/test/language/export/export-defer/load-and-evaluation/runtime-errors/used-throws.js @@ -0,0 +1,43 @@ +// Copyright (C) 2026 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-EvaluateModuleSync +description: When a deferred re-export binding is statically imported and read, the source's runtime error propagates to the consumer +info: | + EvaluateModuleSync ( module [ , importedNames ] ) + 1. ... + 1. Let promise be module.Evaluate(importedNames). + 1. Assert: promise.[[PromiseState]] is either FULFILLED or REJECTED. + 1. If promise.[[PromiseState]] is REJECTED, then + 1. If promise.[[PromiseIsHandled]] is false, perform HostPromiseRejectionTracker(promise, "handle"). + 1. Set promise.[[PromiseIsHandled]] to true. + 1. Return ThrowCompletion(promise.[[PromiseResult]]). + 1. ... + + Evaluate ( [ importedNames ] ) + 1. ... + 1. For each ModuleRequest Record request of optionalIndirectRequests, do + 1. Let requiredModule be GetImportedModule(module, request). + 1. Assert: requiredModule.[[Status]] is one of LINKED, EVALUATING-ASYNC, or EVALUATED. + 1. Let innerPromise be requiredModule.Evaluate(request.[[ImportedNames]]). + 1. If innerPromise.[[PromiseState]] is REJECTED, return innerPromise. + 1. Append innerPromise to promises. + 1. ... + +flags: [module, async] +features: [export-defer, dynamic-import] +includes: [asyncHelpers.js] +---*/ + +import "./setup_FIXTURE.js"; + +asyncTest(async () => { + let err; + await import("./uses-x_FIXTURE.js").catch((e) => { err = e; }); + assert.sameValue( + err.someError, + "the error from throws_FIXTURE", + "evaluation error from deferred source propagates" + ); +}); diff --git a/test/language/export/export-defer/load-and-evaluation/runtime-errors/uses-x_FIXTURE.js b/test/language/export/export-defer/load-and-evaluation/runtime-errors/uses-x_FIXTURE.js new file mode 100644 index 00000000000..2217783addb --- /dev/null +++ b/test/language/export/export-defer/load-and-evaluation/runtime-errors/uses-x_FIXTURE.js @@ -0,0 +1,4 @@ +// Copyright (C) 2026 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +import { x } from "./barrel-defer-throws_FIXTURE.js";