diff --git a/quickjs.c b/quickjs.c index bbac00c33..fcc71629a 100644 --- a/quickjs.c +++ b/quickjs.c @@ -32042,6 +32042,7 @@ static JSValue js_dynamic_import(JSContext *ctx, JSValueConst specifier, static void js_set_module_evaluated(JSContext *ctx, JSModuleDef *m) { m->status = JS_MODULE_STATUS_EVALUATED; + m->async_evaluation = false; if (!JS_IsUndefined(m->promise)) { JSValue ret_val; assert(m->cycle_root == m); @@ -32137,6 +32138,7 @@ static JSValue js_async_module_execution_rejected(JSContext *ctx, JSValueConst t module->eval_has_exception = true; module->eval_exception = js_dup(error); module->status = JS_MODULE_STATUS_EVALUATED; + module->async_evaluation = false; for(i = 0; i < module->async_parent_modules_count; i++) { JSModuleDef *m = module->async_parent_modules[i]; diff --git a/test262_errors.txt b/test262_errors.txt index 082fb016a..25d92040e 100644 --- a/test262_errors.txt +++ b/test262_errors.txt @@ -35,7 +35,6 @@ test262/test/language/module-code/ambiguous-export-bindings/import-and-export-pr test262/test/language/module-code/ambiguous-export-bindings/namespace-unambiguous-if-export-star-as-from-and-import-star-as-and-export.js:74: SyntaxError: export 'foo' in module 'test262/test/language/module-code/ambiguous-export-bindings/nam' is ambiguous test262/test/language/module-code/ambiguous-export-bindings/namespace-unambiguous-if-export-star-as-from.js:75: SyntaxError: export 'foo' in module 'test262/test/language/module-code/ambiguous-export-bindings/nam' is ambiguous test262/test/language/module-code/ambiguous-export-bindings/namespace-unambiguous-if-import-star-as-and-export.js:74: SyntaxError: export 'foo' in module 'test262/test/language/module-code/ambiguous-export-bindings/nam' is ambiguous -test262/test/language/module-code/top-level-await/module-graphs-does-not-hang.js:10: TypeError: $DONE() not called test262/test/language/module-code/top-level-await/rejection-order.js:20: TypeError: $DONE() not called test262/test/language/statements/await-using/initializer-Symbol.asyncDispose-disposed-at-end-of-imported-module.js:11: SyntaxError: exported variable 'resource' does not exist test262/test/language/statements/await-using/initializer-Symbol.dispose-disposed-at-end-of-imported-module.js:11: SyntaxError: exported variable 'resource' does not exist diff --git a/tests.conf b/tests.conf index 192656e0b..81f42ae23 100644 --- a/tests.conf +++ b/tests.conf @@ -16,3 +16,8 @@ tests/fixture_throwing_module.js tests/fixture_reexport_source.js tests/fixture_reexport_missing.js tests/fixture_reexport_direct.js +tests/fixture_async_leaf.js +tests/fixture_async_sync_parent.js +tests/fixture_async_grandparent.js +tests/fixture_async_rejects.js +tests/fixture_async_rejects_parent.js diff --git a/tests/fixture_async_grandparent.js b/tests/fixture_async_grandparent.js new file mode 100644 index 000000000..9cab842b7 --- /dev/null +++ b/tests/fixture_async_grandparent.js @@ -0,0 +1,3 @@ +/* imports the already evaluated sync parent: nothing here is async */ +import { parent } from "./fixture_async_sync_parent.js"; +export const grandparent = parent + 1; diff --git a/tests/fixture_async_leaf.js b/tests/fixture_async_leaf.js new file mode 100644 index 000000000..ed393c733 --- /dev/null +++ b/tests/fixture_async_leaf.js @@ -0,0 +1,3 @@ +/* a module with a top-level await: it evaluates asynchronously */ +await Promise.resolve(); +export const leaf = 1; diff --git a/tests/fixture_async_rejects.js b/tests/fixture_async_rejects.js new file mode 100644 index 000000000..fcc67f825 --- /dev/null +++ b/tests/fixture_async_rejects.js @@ -0,0 +1,3 @@ +/* an async module that rejects after its top-level await */ +await Promise.resolve(); +throw new Error("leaf boom"); diff --git a/tests/fixture_async_rejects_parent.js b/tests/fixture_async_rejects_parent.js new file mode 100644 index 000000000..76c0c28fe --- /dev/null +++ b/tests/fixture_async_rejects_parent.js @@ -0,0 +1,3 @@ +/* a sync module whose only asynchrony comes from the rejecting leaf */ +import "./fixture_async_rejects.js"; +export const unreachable = 1; diff --git a/tests/fixture_async_sync_parent.js b/tests/fixture_async_sync_parent.js new file mode 100644 index 000000000..87c7efdcb --- /dev/null +++ b/tests/fixture_async_sync_parent.js @@ -0,0 +1,5 @@ +/* no top-level await of its own, but an async dependency: the module is + marked [[AsyncEvaluation]] while it waits for the leaf and must be + unmarked once it has run */ +import { leaf } from "./fixture_async_leaf.js"; +export const parent = leaf + 1; diff --git a/tests/module-async-evaluation-flag.js b/tests/module-async-evaluation-flag.js new file mode 100644 index 000000000..8eecb96a9 --- /dev/null +++ b/tests/module-async-evaluation-flag.js @@ -0,0 +1,70 @@ +import { assert } from "./assert.js"; + +/* [[AsyncEvaluation]] must be cleared once a module has finished evaluating. + A module that is only asynchronous because a dependency has a top-level + await keeps the flag set otherwise, and the *next* module that imports it + then treats a finished module as a pending async dependency: it registers + itself in [[AsyncParentModules]] of a module that will never notify its + parents again, so its evaluation promise never settles. */ + +/* Settling is decided against a bounded chain of microtask turns rather than + a timer: a working import settles in a handful of turns, a wedged one + never settles at all. */ +function turns(n) { + let p = Promise.resolve(); + for (let i = 0; i < n; i++) + p = p.then(() => {}); + return p.then(() => "pending"); +} + +async function settles(promise) { + return Promise.race([promise.then(v => ({ ok: v }), e => ({ err: e })), + turns(200)]); +} + +/* the leaf has the top-level await, the parent only inherits its asynchrony */ +{ + const parent = await settles(import("./fixture_async_sync_parent.js")); + assert(parent.ok !== undefined, true, "parent import settled"); + assert(parent.ok.parent, 2); +} + +/* importing the finished module again must not wedge */ +{ + const again = await settles(import("./fixture_async_sync_parent.js")); + assert(again.ok !== undefined, true, "re-import settled"); + assert(again.ok.parent, 2); +} + +/* and neither must a fresh module that depends on it: this is the one that + used to hang, because the finished parent still looked async */ +{ + const grand = await settles(import("./fixture_async_grandparent.js")); + assert(grand.ok !== undefined, true, "grandparent import settled"); + assert(grand.ok.grandparent, 3); +} + +/* the leaf itself is fine too, before and after */ +{ + const leaf = await settles(import("./fixture_async_leaf.js")); + assert(leaf.ok !== undefined, true, "leaf import settled"); + assert(leaf.ok.leaf, 1); +} + +/* a rejecting async module must clear the flag as well, so that a later + importer of a *different* finished module is unaffected and the rejected + one keeps reporting its error rather than hanging */ +{ + const bad = await settles(import("./fixture_async_rejects.js")); + assert(bad.err instanceof Error, true, "rejecting module settled"); + assert(bad.err.message, "leaf boom"); + + const badAgain = await settles(import("./fixture_async_rejects.js")); + assert(badAgain.err instanceof Error, true, "re-import settled"); + assert(badAgain.err.message, "leaf boom"); + + /* a module importing the failed one reports the same failure, promptly */ + const importer = await settles(import("./fixture_async_rejects_parent.js")); + assert(importer.err instanceof Error, true, "importer settled"); + assert(importer.err.message, "leaf boom"); +}