Skip to content

bun test --isolate: a macro call is undefined from the second test file on - #46405

Merged
dylan-conway merged 1 commit into
mainfrom
claude/macro-under-isolate
Oct 22, 2026
Merged

dylan-conway merged 1 commit into
mainfrom
claude/macro-under-isolate

Conversation

@dylan-conway

@dylan-conway dylan-conway commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

What does this PR do?

Under bun test --isolate (and so --parallel), a macro call in a test file evaluates to undefined from the second file of the process on.

// m.ts
export function mark(s: string) { return "M:" + s; }

// t1.test.ts (t2 and t3 are the same with their own name)
import { test, expect } from "bun:test";
import { mark } from "./m.ts" with { type: "macro" };
const v = mark("t1");
test("t1", () => { expect(v).toBe("M:t1"); });

bun test --isolate: 3 pass on 1.4.2, 1 pass and 2 fail on 1.4.3 and main.

Cause

Two pieces of macro state belong to the VM, not to the global, and swap_global_for_test_isolation left both alone:

  • vm.macros maps a macro's id to the function that Bun.registerMacro was given, protect()ed.
  • vm.transpiler.macro_context remembers which ids have been loaded, and skips Macro::init for those.

So the second file finds the macro already loaded and calls the function the first file registered, which is a function of the first file's realm. That realm was retired at the swap. Since #44327 JSValue::call does not enter a retired realm and returns undefined, and the macro runner turns that undefined into the call's value.

Before #44327 the call went through, which hid two other things: every file ran the first file's copy of the macro module (module-level state carried over from file to file), and the protect() kept the global of any file that loaded a macro alive for the rest of the run.

Fix

src/jsc/VirtualMachine.rs, in swap_global_for_test_isolation: unprotect and clear vm.macros, and free vm.transpiler.macro_context. parse_maybe makes a new context when there is none, so the next file loads the macro in its own realm. It is done after the realm is retired, when nothing of the outgoing file can load a macro again.

The context is freed with deinit() and not only set to None: it has no Drop, so that would leak the box and its arena once per file. No AST is waiting to be printed at the swap, which is what the arena has to outlive.

The other source changes are comments that said vm.transpiler's context is leaked for the life of the process.

Not changed

A module that is transpiled on a pool thread (a static or dynamic import of a non-entry file) runs its macros in that thread's own macro VM. That VM has no realm to retire, so those calls were never undefined. It also still keeps macro module state from file to file, as before.

How did you verify your code works?

New tests in test/cli/test/isolation.test.ts:

  • a test file's macros are loaded again for each file, for --isolate and for a --parallel worker that takes all three files. Each file calls the macro itself and through a module it require()s, and the macro counts its calls, so the test also sees which copy of the module ran.
  • a macro left loaded, beside the other "collects globals pinned by leaked handles" tests. Each of the 8 files loads a different macro.
build macros loaded again (both rows) a macro left loaded
main (491aaad15), debug fail: undefined in files 2 and 3 fail: 8 live globals
this branch, debug pass pass
only vm.macros cleared fail pass
only macro_context freed pass fail: 8 live globals
1.4.2 (USE_SYSTEM_BUN=1) fail: b:4, b:5, required-b:6 (the first file's copy goes on counting) fail: 8 live globals

bun bd test test/cli/test/isolation.test.ts: 50 pass, 0 fail.

By hand with the debug build: the repro above gives 3 pass with --isolate, with --parallel=2 and with neither. Macros reached through a static import, import(), require(), a --preload and an async macro give the right value in all three files. A macro module that throws while loading in the first file loads in the third.


Generated by Claude Code

@coderabbitai

coderabbitai Bot commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 15 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: oven-sh/bun/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 08d7bc73-3168-4eac-96d4-5fb3a9e7796d
📥 Commits

Reviewing files that changed from the base of the PR and between cb1c0a2 and dec84cc.

📒 Files selected for processing: 5
  • src/js_parser/lib.rs
  • src/js_parser_jsc/Macro.rs
  • src/jsc/RuntimeTranspilerStore.rs
  • src/jsc/VirtualMachine.rs
  • test/cli/test/isolation.test.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@robobun

robobun commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator
Updated 7:05 PM PT - Oct 21st, 2026

@claude, your commit dec84cc is building: #130431

@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.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline findings, I also checked that the manual unprotect() loop over vm.macros is balanced: Bun.registerMacro (src/runtime/api/BunObject.rs) is the only insert path and protects every stored callback, so no entry is unprotected without a matching protect. Calling deinit() on the JS thread does not drop the source-code printer, since drop_source_code_printer_if_macro_owned returns early when the runtime VM owns it. BUN_TEST_PARALLEL_SCALE_MS is read by the parallel runner, so the --parallel test row does route all three files through one worker.

Extended reasoning...

The change adds ~9 lines to swap_global_for_test_isolation in src/jsc/VirtualMachine.rs (unprotect and clear vm.macros, take and deinit vm.transpiler.macro_context), rewrites three comments that described the context as process-lifetime, and adds two tests plus a fixture generalization in test/cli/test/isolation.test.ts. It touches no security-sensitive surface. Two inline findings are already posted (a missed comment site in src/bundler/transpiler.rs and the pool-thread macro VM still carrying module state, which the PR description acknowledges), so a human look is already signalled; this note only records what else was examined and ruled out.

Comment thread src/jsc/VirtualMachine.rs
Comment thread src/jsc/VirtualMachine.rs

Copy link
Copy Markdown
Member Author

buildkite/bun is red on dec84ccc (#130431), and no test failed.

  • The only failed jobs are the two :darwin: any x64 - test-bun shards. Both are expired: runnable at 22:56 UTC, expired at 00:05 UTC, never started, no exit status. No macOS x64 agent picked them up.
  • The other 179 jobs passed, including macOS arm64, Windows x64 and arm64, and Linux x64 ASAN. There is no error annotation on the build.
  • test/cli/test/isolation.test.ts ran on macOS arm64 on this commit: 50 pass, 0 fail.

There is nothing in this PR to fix for it. BuildKite has already queued a retry of both shards, and they are waiting for an agent. I cannot re-run BuildKite jobs myself.


Generated by Claude Code

@dylan-conway
dylan-conway merged commit 0de6da2 into main Oct 22, 2026
10 of 11 checks passed
@dylan-conway
dylan-conway deleted the claude/macro-under-isolate branch October 22, 2026 00:13
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.

3 participants