Skip to content

module-loader: resume suspended TLA body when a cycle sibling throws - #33961

Open
robobun wants to merge 1 commit into
mainfrom
farm/687d2e90/tla-cycle-sibling-throw-resume
Open

module-loader: resume suspended TLA body when a cycle sibling throws#33961
robobun wants to merge 1 commit into
mainfrom
farm/687d2e90/tla-cycle-sibling-throw-resume

Conversation

@robobun

@robobun robobun commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

Repro

// a.mjs:  import "./b.mjs"; import "./c.mjs";
// b.mjs:  import "./a.mjs"; FLAG = "started"; await 0; FLAG = "done";   // cycle
// c.mjs:  throw new Error("boom");
try { await import("./a.mjs") } catch {}
console.log(FLAG);   // bun: "started"   node/spec: "done"

A top-level-await module that is suspended at its await inside an import cycle is never resumed if a sibling module throws during the same Evaluate() pass. Everything after the await (finally blocks, export initialisers, resource registration) is silently skipped while the process continues; its try { await } catch {} catches nothing. Node and the spec run the continuation.

Cause

While b is suspended at await 0 it is still on the InnerModuleEvaluation stack with status Evaluating (its SCC has not been popped yet because it cycles through a). When c throws, step 9 of CyclicModuleRecord::evaluate() stamps the error onto every stack member, including b, as evaluationError and transitions them to Evaluated.

When b's resume microtask (AsyncModuleExecutionResume) later fires, it re-enters JSModuleRecord::evaluate(), which sees evaluationError() set and re-throws it instead of resuming the body. Per spec the body runs as an independent async context (AsyncBlockStart); AsyncModuleExecutionFulfilled / AsyncModuleExecutionRejected already handle the "status is already Evaluated" case afterwards (step 1 of each).

Fix

oven-sh/WebKit#281 gates the evaluationError() early-return on the generator State still being Init (body not yet started). A suspended body (State > 0) proceeds to executeModuleProgram and runs to completion.

-    if (JSValue error = evaluationError()) {
-        scope.throwException(globalObject, error);
-        return { };
+    JSValue state = internalField(Field::State).get();
+    if (state.isNumber() && state.asInt32AsAnyInt() == std::to_underlying(State::Init)) {
+        if (JSValue error = evaluationError()) {
+            scope.throwException(globalObject, error);
+            return { };
+        }
     }

Verification

  • USE_SYSTEM_BUN=1 bun test test/js/bun/resolve/dynamic-import-tla-cycle.test.ts → 3 new tests fail (flag: "started", missing b:after-await/b:end, finally not run)
  • bun run build:local test test/js/bun/resolve/dynamic-import-tla-cycle.test.ts with JSModuleRecord::evaluate: resume suspended TLA body when a cycle sibling throws WebKit#281 applied → 9/9 pass, output matches Node exactly
  • BUN_JSC_validateExceptionChecks=1 passes
  • test/js/bun/resolve/ (282 tests), test/regression/issue/{24387,30887,32178}.test.ts, require-esm-transitive-tla.test.ts all pass

WEBKIT_VERSION is pinned to autobuild-preview-pr-281-3a40c5b6 (oven-sh/WebKit#281 rebased onto 4895f45d so it sits on top of #34009's allocator changes). Once that PR merges I will switch this to the merged commit hash.

Note: the fix lives in the prebuilt WebKit, so the fail-before gate (which stashes src/ and packages/) cannot demonstrate it; fail-before is shown above via USE_SYSTEM_BUN=1.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Updates the default WebKit preview version and adds three regression tests for top-level-await import cycles involving sibling evaluation errors.

WebKit version update

Layer / File(s) Summary
Update default WebKit version
scripts/build/deps/webkit.ts
Changes WEBKIT_VERSION to the new autobuild preview tag.

Top-level-await cycle regressions

Layer / File(s) Summary
Cover suspended cycle resumption
test/js/bun/resolve/dynamic-import-tla-cycle.test.ts
Adds tests for await resumption, finally execution, multiple await points, caught errors, output ordering, and clean process results.

Possibly related PRs

  • oven-sh/bun#33956: Updates the same WebKit version constant with a preview/build tag.
  • oven-sh/bun#34138: Updates the same WebKit version constant to another preview tag.

Suggested reviewers: jarred-sumner, sosukesuzuki

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title is concise and accurately summarizes the main change: resuming suspended TLA bodies when a cycle sibling throws.
Description check ✅ Passed The description covers the change and includes concrete verification steps, even though it uses different headings than the template.

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

@robobun

robobun commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 5:52 AM PT - Jul 14th, 2026

@robobun, your commit 4a61e27 has 1 failures in Build #72926 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 33961

That installs a local version of the PR into your bun-33961 executable, so you can run:

bun-33961 --bun

@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. Broken imports #30651 - Reports "Cannot access 'isCI' before initialization" where a variable defined after an await in a module is never initialized, matching the exact pattern of a suspended TLA body not resuming in a cycle

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #30651

🤖 Generated with Claude Code

@robobun

robobun commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator Author

Re #30651: that repro does have the cycle (config.tsbindings/utils/binariesGithubRelease.ts) and a post-await export (isCI), but I don't see a sibling module that throws synchronously during the same Evaluate() pass, which is what this PR addresses. #30651 looks closer to the #30259 / #30634 family (sibling runs before the shared TLA dep settles), previously addressed via #32437. Leaving it off the Fixes list until verified against the actual node-llama-cpp repro.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't find issues in the tests, but deferring to a human: the actual fix lives in oven-sh/WebKit#281 (JSC's JSModuleRecord::evaluate() — core module-evaluation semantics), and WEBKIT_VERSION is currently pinned to that PR's preview-build tag pending its merge.

What was reviewed:

  • The three new tests follow the existing patterns in this file (tempDir, concurrent pipe drain, exit-code asserted last) and assert exact log ordering / flag values.
  • Checked that prebuiltUrl() / prebuiltDestDir() already handle autobuild--prefixed version strings, so the preview tag resolves correctly.
  • Confirmed the tests exercise distinct variants (plain resume, try/finally, multiple awaits) rather than duplicating one repro.
Extended reasoning...

Overview

This PR pairs with oven-sh/WebKit#281. In this repo it changes two files: scripts/build/deps/webkit.ts bumps WEBKIT_VERSION from a commit sha to the preview-build tag autobuild-preview-pr-281-0e491dad, and test/js/bun/resolve/dynamic-import-tla-cycle.test.ts gains three regression tests for the TLA-cycle-sibling-throw scenario. The behavioral fix itself — gating the evaluationError() early-return in JSModuleRecord::evaluate() on the generator still being in State::Init — lives entirely in the WebKit fork.

Security risks

None identified. The bun-repo diff is a version-string change plus test fixtures that spawn bunExe() in a temp dir. No auth, crypto, network, or untrusted-input handling is touched here.

Level of scrutiny

High, and outside what I can sign off on. The substantive change modifies JavaScriptCore's cyclic-module evaluation / async-module resume path, which is spec-defined (CyclicModuleRecord::Evaluate, AsyncModuleExecutionFulfilled/Rejected, AsyncBlockStart) and historically subtle — several existing tests in this same file guard adjacent edge cases. Whether skipping the evaluationError() re-throw for a suspended body is correct in all callers of JSModuleRecord::evaluate() (not just the resume microtask) needs a human who can read the WebKit-side diff in context.

Other factors

  • The PR description explicitly states WEBKIT_VERSION is a temporary preview pin and will be switched to the merged commit hash once oven-sh/WebKit#281 lands — so this PR is not yet in its final form by the author's own account.
  • Per CLAUDE.md's "Dependencies & vendoring" guidance, WebKit bumps warrant explicit review.
  • The tests themselves look solid: they use tempDir/bunEnv/bunExe, drain stdout/stderr/exited concurrently, assert combined {out, stderr} objects before the exit code, and the author verified fail-before via USE_SYSTEM_BUN=1. I did note the two expect(stderr).toBe("") lines in the third test are stricter than the "never assert stderr is exactly empty" guidance, but the first two tests wrap stderr into the combined object which is the recommended shape — minor and consistent with the rest of this file.

A top-level-await module that is suspended at its await inside an import
cycle was never resumed if a sibling module threw during the same
Evaluate() pass: everything after the await (finally blocks, export
initialisers) was silently skipped while the process continued. Node and
the spec run the continuation.

The resume microtask re-enters JSModuleRecord::evaluate(), which took the
evaluationError() early-return once the cycle root had stamped the
sibling's error onto the suspended module. The WebKit change gates that
early-return on the generator State still being Init; a suspended body
proceeds to executeModuleProgram and runs to completion, and
asyncExecution{Fulfilled,Rejected} already no-op when status is Evaluated.

WebKit change: oven-sh/WebKit#281 (autobuild-preview-pr-281-0e491dad)
@robobun
robobun force-pushed the farm/687d2e90/tla-cycle-sibling-throw-resume branch from f72406c to 4a61e27 Compare July 14, 2026 12:40

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/build/deps/webkit.ts`:
- Line 10: Update WEBKIT_VERSION to a permanent merged-main commit SHA or stable
release tag instead of the temporary autobuild-preview-pr-281-3a40c5b6 value,
preserving it as the default prebuilt WebKit source.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0a24c74c-cb06-4301-9438-2d6d6b727913

📥 Commits

Reviewing files that changed from the base of the PR and between cc0c1e8 and 4a61e27.

📒 Files selected for processing (2)
  • scripts/build/deps/webkit.ts
  • test/js/bun/resolve/dynamic-import-tla-cycle.test.ts

Comment thread scripts/build/deps/webkit.ts

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found in the tests or the version-bump plumbing, but the actual fix here is a change to JSC's module evaluation semantics shipped via a WebKit bump — and WEBKIT_VERSION is currently pinned to an unmerged preview-build tag that the description says will be swapped for the merged hash. That's a two-repo coordination on engine-critical behavior, so leaving this for a human to sign off.

What was reviewed:

  • The three new tests follow the existing patterns in this file (tempDir, concurrent pipe drain, exit-code-last) and assert exact log ordering.
  • prebuiltUrl() / prebuiltDestDir() already handle autobuild--prefixed versions, so the preview tag resolves correctly.
  • Checked that the new tests don't rely on stderr being exactly empty in a way that would break under debug/ASAN — they assert it as part of a combined object, matching the file's existing convention.
Extended reasoning...

Overview

This PR adds three regression tests to test/js/bun/resolve/dynamic-import-tla-cycle.test.ts covering a TLA module suspended inside an import cycle whose sibling throws, and bumps WEBKIT_VERSION in scripts/build/deps/webkit.ts to a preview-build tag from oven-sh/WebKit#281. The behavioral fix lives entirely in the WebKit prebuilt — this repo's diff is version pin + test coverage only.

Security risks

None. No user-input parsing, auth, crypto, or filesystem-path handling is touched. The tests spawn hermetic subprocesses in temp dirs.

Level of scrutiny

High. Although the in-repo diff is small and mechanical, the change it pulls in modifies JSModuleRecord::evaluate() — the resume path for top-level-await module bodies — which is spec-sensitive and affects every ESM program with TLA + cycles + errors. The PR description's analysis is careful and the fix is narrowly gated on generator State::Init, but this is exactly the kind of engine-semantics change a maintainer should look at directly (and likely wants to review alongside the WebKit-side PR).

Other factors

  • The PR description explicitly states WEBKIT_VERSION is temporarily pinned to a preview tag and will be updated once oven-sh/WebKit#281 merges — so the PR is not yet in its final mergeable state by the author's own account.
  • WebKit version bumps are a vendored-dependency change; per the repo's review guidance, dependency bumps warrant human review.
  • The tests themselves look correct and hermetic; the ruled-out finding (preview tag being unmerged) is real as a process concern but not a code bug — the build machinery already handles autobuild- tags.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant