Skip to content

[JSC] innerModuleEvaluation: don't skip async-dep wait for siblings in same Evaluate()#215

Merged
sosukesuzuki merged 1 commit into
mainfrom
claude/fix-30259-tla-sibling-watermark
May 6, 2026
Merged

[JSC] innerModuleEvaluation: don't skip async-dep wait for siblings in same Evaluate()#215
sosukesuzuki merged 1 commit into
mainfrom
claude/fix-30259-tla-sibling-watermark

Conversation

@sosukesuzuki

Copy link
Copy Markdown

Fixes oven-sh/bun#30259.

What

The Bun-specific skip at 11.c.v (avoid self-deadlock when require(esm) / dynamic import re-enters a TLA module from inside its own suspension) previously fired whenever the dep was already EvaluatingAsync with pendingAsyncDependencies == 0.

That also matches a sibling static import within the same Evaluate() pass when the TLA dep has no async deps of its own: the dep is suspended at its first await and bindings declared after it are still TDZ, so the importer ran too early.

// root.ts
import { foo } from "./await.ts";   // await.ts → EvaluatingAsync, suspended at `await 0`
import "./child.ts";                // child.ts visits await.ts, skips wait, runs with foo in TDZ

// await.ts
await 0;
export const foo = 123;

// child.ts
import { foo } from "./await.ts";
console.log(foo);                   // ReferenceError: Cannot access 'foo' before initialization

How

Thread an asyncOrderWatermark (the VM's moduleAsyncEvaluationCount snapshot at the start of this Evaluate()) through innerModuleEvaluation and only skip when the cycle root's asyncEvaluationOrder() predates it — i.e. the dep transitioned to EvaluatingAsync in a prior Evaluate() call (the re-entrant deadlock case). Siblings that transition during the current DFS get order >= watermark and keep the spec-mandated wait.

The existing pendingAsyncDependencies == 0 guard is kept: a prior-Evaluate() dep can still be queued behind its own async deps with its body never entered.

Tests

Regression tests added on the Bun side in test/js/bun/resolve/dynamic-import-tla-cycle.test.ts (direct sibling + indirectly-shared sibling). Existing re-entrancy / deadlock-avoidance tests in the same file continue to pass.

…n same Evaluate()

The Bun-specific skip at 11.c.v (avoid self-deadlock when require(esm) /
dynamic import re-enters a TLA module from inside its own suspension)
previously fired whenever the dep was already EvaluatingAsync with
pendingAsyncDependencies == 0. That also matches a sibling static import
within the *same* Evaluate() pass when the TLA dep has no async deps of
its own: the dep is suspended at its first await and bindings declared
after it are still TDZ, so the importer ran too early
(oven-sh/bun#30259).

Thread an asyncOrderWatermark (the VM's moduleAsyncEvaluationCount at
the start of this Evaluate()) through innerModuleEvaluation and only
skip when the cycle root's asyncEvaluationOrder predates it, i.e. the
dep transitioned to EvaluatingAsync in a *prior* Evaluate(). Siblings
that transition during the current DFS keep the spec wait.
@coderabbitai

coderabbitai Bot commented May 5, 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: 5f7b0d5f-7233-4966-bc07-ad0c7f3eeb69

📥 Commits

Reviewing files that changed from the base of the PR and between d5ed1db and df660a8.

📒 Files selected for processing (4)
  • Source/JavaScriptCore/runtime/AbstractModuleRecord.cpp
  • Source/JavaScriptCore/runtime/AbstractModuleRecord.h
  • Source/JavaScriptCore/runtime/CyclicModuleRecord.cpp
  • Source/JavaScriptCore/runtime/VM.h

Walkthrough

This PR adds async module evaluation watermarking to JavaScriptCore's module evaluation logic when USE(BUN_JSC_ADDITIONS) is enabled. An asyncOrderWatermark parameter is threaded through module dependency resolution to refine when the spec-mandated EvaluatingAsync wait may be skipped, preventing spurious "before initialization" errors on re-imports of modules with top-level await.

Changes

Async Module Evaluation Watermark

Layer / File(s) Summary
Data Shape & Accessors
Source/JavaScriptCore/runtime/VM.h
Added int64_t VM::moduleAsyncEvaluationCount() const getter to expose the async evaluation watermark counter under USE(BUN_JSC_ADDITIONS).
Method Signature
Source/JavaScriptCore/runtime/AbstractModuleRecord.h
AbstractModuleRecord::innerModuleEvaluation signature conditionally includes int64_t asyncOrderWatermark parameter when USE(BUN_JSC_ADDITIONS) is enabled; otherwise retains the original 3-parameter form.
Core Implementation
Source/JavaScriptCore/runtime/AbstractModuleRecord.cpp
Watermark parameter is threaded through recursive innerModuleEvaluation calls during dependency DFS; async cycle skip condition now checks asyncEvaluationOrder().order() >= asyncOrderWatermark to prevent skipping when cycle root entered EvaluatingAsync in the current evaluation context.
Entry Point Wiring
Source/JavaScriptCore/runtime/CyclicModuleRecord.cpp
CyclicModuleRecord::evaluate() passes vm.moduleAsyncEvaluationCount() as the initial watermark argument when invoking innerModuleEvaluation.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and accurately summarizes the main technical change: preventing the skip of async-dependency waits for sibling imports in the same Evaluate() call.
Description check ✅ Passed The description provides a detailed explanation of the bug, reproduction case, and the fix implemented, aligning well with the template's commit-message format, though it references an external issue rather than including a Bugzilla link.
Linked Issues check ✅ Passed The code changes fully address the regression: threadinging asyncOrderWatermark through innerModuleEvaluation to distinguish prior Evaluate() calls from same-DFS siblings [#30259].
Out of Scope Changes check ✅ Passed All changes are tightly scoped to fixing the TLA sibling evaluation issue: header/implementation updates for asyncOrderWatermark parameter, VM accessor, and call-site updates.

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

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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

@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown

Preview Builds

Commit Release Date
df660a83 autobuild-preview-pr-215-df660a83 2026-05-05 00:24:29 UTC

@sosukesuzuki sosukesuzuki merged commit 88b2f7a into main May 6, 2026
43 checks passed
sosukesuzuki added a commit to oven-sh/bun that referenced this pull request May 7, 2026
sosukesuzuki added a commit to oven-sh/bun that referenced this pull request May 7, 2026
dylan-conway pushed a commit to oven-sh/bun that referenced this pull request May 7, 2026
…0262)

Fixes #30259. Bumps WebKit to `88b2f7a2159c913f7dd0d73c0e88d66138cd67ba`
(oven-sh/WebKit#215, merged).

## What

A static import that re-imports a TLA dep already visited by a sibling
earlier in the **same** `Evaluate()` pass was skipping the spec-mandated
wait and running with the dep's post-`await` bindings still in TDZ:

```ts
// root.ts
import { foo } from "./await.ts"; // await.ts → EvaluatingAsync, suspended at `await 0`
import "./child.ts";              // child.ts visits await.ts, skips wait, runs too early

// await.ts
await 0;
export const foo = 123;

// child.ts
import { foo } from "./await.ts";
console.log(foo); // ReferenceError: Cannot access 'foo' before initialization
```

## Why

The Bun-specific skip at `innerModuleEvaluation` 11.c.v (which avoids
self-deadlock when `require(esm)` / dynamic import re-enters a TLA
module from inside its own suspension) used `pendingAsyncDependencies ==
0` as the discriminator for "body already entered". A TLA dep with no
async deps of its own also has count 0 after suspending at its first
`await` within the same DFS, so the discriminator matched the sibling
case.

## How (oven-sh/WebKit#215)

Snapshot `vm.moduleAsyncEvaluationCount()` at the start of each
`Evaluate()` and thread it through `innerModuleEvaluation` as
`asyncOrderWatermark`. Only skip the wait when the cycle root's
`asyncEvaluationOrder()` **predates** the watermark (i.e. it became
`EvaluatingAsync` in a *prior* `Evaluate()` — the re-entrant deadlock
case). Siblings that transition during the current DFS get `order >=
watermark` and keep the spec wait.

## Tests

Two regression tests in
`test/js/bun/resolve/dynamic-import-tla-cycle.test.ts`:
- direct sibling re-import (the issue repro)
- indirectly-shared TLA dep through different parents (guards against
discriminating by "asyncParentModule on stack")

Existing re-entrancy / deadlock-avoidance tests in the same file
continue to pass.
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.

regression: when importing a module with top-level await multiple times, all but the first import throw ReferenceError

1 participant