Skip to content

innerModuleEvaluation: only skip async-dep wait when cycle root has executed#202

Merged
Jarred-Sumner merged 1 commit into
mainfrom
claude/narrow-evaluating-async-skip
Apr 27, 2026
Merged

innerModuleEvaluation: only skip async-dep wait when cycle root has executed#202
Jarred-Sumner merged 1 commit into
mainfrom
claude/narrow-evaluating-async-skip

Conversation

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

The depWasAlreadyEvaluatingAsync skip (added in 18d48e3 for the dynamic-import-re-enters-its-own-TLA-awaiter deadlock) also fires for sibling static imports in the same Evaluate() pass: an earlier sibling pops an SCC to EvaluatingAsync, then a later sibling that imports an SCC member sees status == EvaluatingAsync, skips the wait, and runs with the SCC's bindings still in TDZ.

Minimal repro (works in Node and pre-rewrite Bun, TDZ's after #199):

entry → A, B
A ↔ C (cycle)
C → tla.mjs (HasTLA, even if the await is runtime-dead)
B → C (reads C's binding at top-level)

When entry processes A, the {A,C} SCC pops to EvaluatingAsync. When entry then processes B→C, the patch sees C already EvaluatingAsync and makes B skip the wait — B executes synchronously while C's bindings are TDZ.

Fix: narrow the skip to also require cyclic->pendingAsyncDependencies() == 0 — i.e. the cycle root's ExecuteModule/ExecuteAsyncModule has been called and its bindings before the first await are initialised. For an SCC still queued behind an async dep the count is > 0, so the spec-mandated wait is preserved. For the Nitro deadlock case (entry or non-entry), the re-entered module's body is mid-await with count == 0, so the skip still fires.

Bun-side test coverage (3 cases) is in the paired bun PR.

…xecuted

The depWasAlreadyEvaluatingAsync skip (added for the dynamic-import-re-
enters-its-own-TLA-awaiter deadlock) also fired for sibling static
imports in the same Evaluate() pass: an earlier sibling pops an SCC to
EvaluatingAsync, then a later sibling that imports an SCC member sees
status==EvaluatingAsync, skips the wait, and runs with the SCC's
bindings still in TDZ.

Narrow the skip to require the cycle root's pendingAsyncDependencies
== 0, i.e. its body has been entered (the Nitro case, where bindings
before the await are initialised). For an SCC still queued behind an
async dep the count is > 0, so the spec-mandated wait is preserved.
@coderabbitai

coderabbitai Bot commented Apr 27, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4ba6f1c4-5df9-4513-9704-f4c0167e8c8b

📥 Commits

Reviewing files that changed from the base of the PR and between bdf6aab and 413d263.

📒 Files selected for processing (1)
  • Source/JavaScriptCore/runtime/AbstractModuleRecord.cpp

Walkthrough

Updated TLA cycle handling in innerModuleEvaluation to refine async wait skip logic. The decision now checks both whether a dynamic import dependency is in EvaluatingAsync and whether the SCC cycle root's pendingAsyncDependencies equals zero, preventing premature evaluation when async status originates from sibling/static imports within the same evaluation pass.

Changes

Cohort / File(s) Summary
TLA Cycle Handling
Source/JavaScriptCore/runtime/AbstractModuleRecord.cpp
Refined innerModuleEvaluation logic to add pendingAsyncDependencies == 0 check on SCC cycle root alongside existing EvaluatingAsync check, preventing early evaluation when async status comes from siblings/static imports. Updated accompanying comment and adjusted condition logic.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title precisely and concisely describes the narrowed condition for skipping the async-dep wait (only when cycle root has executed), which is the core change in the PR.
Description check ✅ Passed The PR description comprehensively explains the bug, reproduction case, fix mechanism, and includes references to test coverage, though it lacks a Bugzilla link and structured sections as per the WebKit template.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Jarred-Sumner added a commit to oven-sh/bun that referenced this pull request Apr 27, 2026
Adds two cases to dynamic-import-tla-cycle.test.ts:

- static sibling import waits for an async-pending SCC: entry imports A
  then B; A↔C cycle; C imports a HasTLA module; B reads a binding from
  C at top-level. Works in Node and 1.3.13, TDZ's on main since #29393
  because the depWasAlreadyEvaluatingAsync skip in innerModuleEvaluation
  fires for sibling static imports in the same Evaluate() pass.

- non-entry TLA self-import: same Nitro deadlock pattern as the existing
  test, but the awaiting module is a static dep of the entry rather than
  the entry itself. Locks in why the discriminator must be
  pendingAsyncDependencies==0 (cycle root's body has been entered), not
  topLevelCapability (only set on the Evaluate() entry).

The static-sibling test fails until WEBKIT_VERSION includes
oven-sh/WebKit#202.
Jarred-Sumner added a commit to oven-sh/bun that referenced this pull request Apr 27, 2026
`fetch-cli.ts` is imported by `source.ts`/`zig.ts` as a library (for
`fetchCliPath`) and also runs as a CLI. The guarded `await main()` marks
the module `HasTLA`, which forces every importer — and the
`{config,webkit,flags,source}` cycle — onto the spec's async-evaluation
path for code that's dead on import. Replace with `main().catch(...)` so
the module stays sync when imported.

This is the immediate trigger for the `ReferenceError: Cannot access
'webkit' before initialization` crash a freshly-built bun hits running
`scripts/build.ts`, which several open `farm/*` branches (#29725,
#29731, #29733, #29749, #29756, #28512) each work around by relocating
`WEBKIT_VERSION`. Supersedes the `scripts/build/` portions of those.

The underlying loader regression (the `depWasAlreadyEvaluatingAsync`
skip in `innerModuleEvaluation` over-firing for sibling static imports)
is fixed in oven-sh/WebKit#202; tests + `WEBKIT_VERSION` bump are in
#29770. This change is independently correct and lands first so the farm
stops thrashing.

Verified: `build/debug/bun-debug scripts/build.ts --help` (was crashing,
now works), `fetch-cli.ts` CLI usage and BuildError/non-BuildError exit
codes unchanged.
@github-actions

Copy link
Copy Markdown

Preview Builds

Commit Release Date
413d2639 autobuild-preview-pr-202-413d2639 2026-04-27 06:35:01 UTC

@Jarred-Sumner Jarred-Sumner merged commit 137c596 into main Apr 27, 2026
43 checks passed
Jarred-Sumner added a commit to oven-sh/bun that referenced this pull request Apr 27, 2026
Narrows the depWasAlreadyEvaluatingAsync skip in innerModuleEvaluation
to require the cycle root's pendingAsyncDependencies == 0, so sibling
static imports in the same Evaluate() pass wait for an async-pending
SCC instead of running with TDZ bindings.
xhjkl pushed a commit to xhjkl/bun that referenced this pull request May 14, 2026
`fetch-cli.ts` is imported by `source.ts`/`zig.ts` as a library (for
`fetchCliPath`) and also runs as a CLI. The guarded `await main()` marks
the module `HasTLA`, which forces every importer — and the
`{config,webkit,flags,source}` cycle — onto the spec's async-evaluation
path for code that's dead on import. Replace with `main().catch(...)` so
the module stays sync when imported.

This is the immediate trigger for the `ReferenceError: Cannot access
'webkit' before initialization` crash a freshly-built bun hits running
`scripts/build.ts`, which several open `farm/*` branches (oven-sh#29725,
oven-sh#29731, oven-sh#29733, oven-sh#29749, oven-sh#29756, oven-sh#28512) each work around by relocating
`WEBKIT_VERSION`. Supersedes the `scripts/build/` portions of those.

The underlying loader regression (the `depWasAlreadyEvaluatingAsync`
skip in `innerModuleEvaluation` over-firing for sibling static imports)
is fixed in oven-sh/WebKit#202; tests + `WEBKIT_VERSION` bump are in
oven-sh#29770. This change is independently correct and lands first so the farm
stops thrashing.

Verified: `build/debug/bun-debug scripts/build.ts --help` (was crashing,
now works), `fetch-cli.ts` CLI usage and BuildError/non-BuildError exit
codes unchanged.
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.

1 participant