Skip to content

Clear [[AsyncEvaluation]] when a module finishes evaluating - #1646

Open
andreasrosdal wants to merge 2 commits into
quickjs-ng:masterfrom
nordstjernen-web:fix-stale-async-evaluation-flag
Open

Clear [[AsyncEvaluation]] when a module finishes evaluating#1646
andreasrosdal wants to merge 2 commits into
quickjs-ng:masterfrom
nordstjernen-web:fix-stale-async-evaluation-flag

Conversation

@andreasrosdal

Copy link
Copy Markdown
Contributor

AsyncModuleExecutionFulfilled clears module->async_evaluation, but the two other paths that move a module to JS_MODULE_STATUS_EVALUATED do not: js_set_module_evaluated(), which finishes every module drained from the available-ancestors list, and js_async_module_execution_rejected(). Both leave the flag set on a module that is finished, which is exactly what [[AsyncEvaluationOrder]] = done exists to prevent.

A later evaluation then treats the finished module as still pending. js_inner_module_evaluation() checks m1->async_evaluation to decide whether to register a dependency, so it bumps pending_async_dependencies and appends itself to the finished module's async_parent_modules. Nothing will ever drain that list again, so the new module never executes and its top-level promise never settles:

// a.mjs
await 0;
export const x = 1;
// b.mjs
import { x } from './a.mjs';
export const y = x + 1;
// c.mjs
import { y } from './b.mjs';
export const z = y + 1;
// main.mjs
const b = await import('./b.mjs');
console.log('b.y =', b.y);           // 2
const c = await import('./c.mjs');   // hangs, never resolves
console.log('c.z =', c.z);

b is evaluated through the available-ancestors path, so its flag stays set after a's top-level await resolves; importing c afterwards deadlocks. V8 prints both lines.

Clear the flag in both places. Fixes test262/test/language/module-code/top-level-await/module-graphs-does-not-hang.js, which is listed in test262_errors.txt; the entry is dropped.

🤖 Generated with Claude Code

https://claude.ai/code/session_014mv33YvfHz7t9mmkituBnn


Generated by Claude Code

claude added 2 commits August 6, 2026 17:30
AsyncModuleExecutionFulfilled clears module->async_evaluation, but the two
other paths that move a module to JS_MODULE_STATUS_EVALUATED do not:
js_set_module_evaluated(), which finishes every module drained from the
available-ancestors list, and js_async_module_execution_rejected(). Both
leave the flag set on a module that is finished, which is what
[[AsyncEvaluationOrder]] = done exists to prevent.

A later evaluation then treats the finished module as still pending.
js_inner_module_evaluation() checks m1->async_evaluation to decide whether
to register a dependency, so it bumps pending_async_dependencies and
appends itself to the finished module's async_parent_modules. Nothing will
ever drain that list again, so the new module never executes and its
top-level promise never settles:

    // a.mjs
    await 0;
    export const x = 1;
    // b.mjs
    import { x } from './a.mjs';
    export const y = x + 1;
    // c.mjs
    import { y } from './b.mjs';
    export const z = y + 1;
    // main.mjs
    const b = await import('./b.mjs');   // b.y = 2
    console.log('b.y =', b.y);
    const c = await import('./c.mjs');   // hangs, never resolves
    console.log('c.z =', c.z);

b is evaluated through the available-ancestors path, so its flag stays set
after a's top-level await resolves; importing c afterwards deadlocks. V8
prints both lines.

Clear the flag in both places. Fixes test262
language/module-code/top-level-await/module-graphs-does-not-hang.js, which
is listed in test262_errors.txt; drop the entry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014mv33YvfHz7t9mmkituBnn
The stale flag is only observable from a *later* module: importing a
finished module that still looks asynchronous registers the importer as an
async parent of a module that will never notify its parents again, so the
importer's evaluation promise never settles.

Settling is decided against a bounded chain of microtask turns instead of a
timer, so the test fails deterministically on the wedged case rather than
letting the process exit quietly with the promise still pending. Both the
fulfilled and the rejected path are covered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6eRbuuuCujKgQkrHgvMrc
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants