Skip to content

bundler: run a Bun.build on the bundle thread through the task pointer, not a build-long &mut self - #37740

Open
robobun wants to merge 5 commits into
mainfrom
farm/df6b3781/bundle-completion-task-handoff
Open

robobun wants to merge 5 commits into
mainfrom
farm/df6b3781/bundle-completion-task-handoff

Conversation

@robobun

@robobun robobun commented Aug 12, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • One JSBundleCompletionTask allocation is shared by the JS thread and the bundle thread while a Bun.build() or HTML route build runs. On main the bundle thread holds it as &mut self for the whole build, so anything the JS thread does to it meanwhile is a write into memory a protected &mut covers.
  • Three such writes exist: promise / started_at_ns / html_build_task are set after the task is already enqueued (every build); cancelled is stored on VM teardown or route deinit mid-build; and the bundler writes the task's log through a pointer taken out of one &mut self while a later &mut self is live.
  • A reduction of the three shapes fails under Miri with Tree Borrows and Stacked Borrows (write access through <..> is forbidden, the accessed tag is foreign to the protected tag) and passes with pointer receivers.
  • No crash is known and it is ASAN-clean today, since the bundle thread never reads the fields the JS side writes. The contract is wrong, and noalias / dereferenceable on the receiver let the compiler assume it. Same family as bundler: hand a finished Bun.build back through its pointer, not a &mut receiver #37723, which converts the hand-back; this covers the rest of the build.

Fix

  • Every CompletionStruct method takes the task as this: *mut Self and reaches only the fields it uses; the bundle thread no longer reborrows the dequeued task. Trait methods that existed only for the impl to call on itself go away, as does the transpiler field, which nothing read. Order of operations is unchanged.
  • Scheduling takes a Deliver (a promise for Bun.build, a route for Bun.serve), fills the per-owner fields at construction, and enqueues as its last statement. Those fields are private now, so the old writes do not compile. Only observable difference: started_at_ns is taken a few microseconds earlier.
  • Property to check: after the enqueue the JS thread reaches the task only through cancelled and the queued-release handshake, and nothing on the bundle thread holds a reference to the whole task. Interior mutability would not do it, since a &mut self retag claims UnsafeCell bytes too; the racing fields are already atomics.
  • Verification: a new source-lint test pins the shape (pointer receivers, no reborrow, nothing after the enqueue, private fields); it fails on main and passes here. The Miri reduction is in the description, not the tree. Existing bundler, HTML-serve and worker-teardown tests pass on a debug (ASAN) build.

Background

  • The bundle thread: Bun.build() and Bun.serve HTML routes do not bundle on the JS thread. They box a JSBundleCompletionTask, push it onto a queue, and one bundle thread pops it, runs the build, and posts it back to the owning event loop. Until that post, both threads can reach the same allocation.
  • CompletionStruct is the trait the generic bundle thread (in bun_bundler) uses to drive a task whose concrete type lives in bun_runtime; most of the diff is changing its receivers and the impl behind them.
  • Protected references: under Rust's aliasing models (Stacked Borrows and Tree Borrows, which bun run rust:miri checks) a &mut self argument is protected for the whole call. Any access through another pointer during that call is UB, even an atomic store, even to bytes the callee never touches. A raw *mut makes no such claim, and &raw mut (*this).field claims only that field.
  • Miri is the interpreter that reports these violations. ASAN cannot; it only sees real bad reads and writes, and there are none here.
  • Deliver is the new enum naming who gets the result (promise or route). It is consumed at construction, so the per-owner fields are set before the task is ever on the queue.

[review] gate passed · iteration 1 · 5 files touched

fails on main (without fix)
ASAN without fix: 4 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/internal/source-lints/bundle-completion-task-handoff.test.ts
bun test v1.4.0 (fb842a03b)

test/internal/source-lints/bundle-completion-task-handoff.test.ts:
(pass) the scans recognize the shapes they claim to [38.58ms]
125 |       "create_and_configure_transpiler",
126 |       "init_and_run",
127 |       "complete_on_bundle_thread",
128 |     ]),
129 |   );
130 |   expect(receivers.filter(r => !THIS_PTR.test(r.first)).map(r => `${r.name}(${r.first})`)).toEqual([]);
                                                                                                 ^
error: expect(received).toEqual(expected)

- []
+ [
+   "configure_bundler(&mut self)",
+   "try_start(&mut self)",
+   "complete_on_bundle_thread(&mut self)",
+   "set_result(&mut self)",
+   "set_log(&mut self)",
+   "set_transpiler(&mut self)",
+   "plugins(&self)",
+   "file_map(&mut self)",
+   "as_js_bundle_completion_task(&mut self)",
+   "create_and_configure_transpiler(&mut self)",
+   "init_and_run(&mut self)",
+ ]

- Expected  - 1
+ Received  + 13

      at <ano
... (truncated)

release without fix: 4 FAILED
bun test v1.4.0-canary.1 (da3851e57)

test/internal/source-lints/bundle-completion-task-handoff.test.ts:
(pass) the scans recognize the shapes they claim to [0.52ms]
125 |       "create_and_configure_transpiler",
126 |       "init_and_run",
127 |       "complete_on_bundle_thread",
128 |     ]),
129 |   );
130 |   expect(receivers.filter(r => !THIS_PTR.test(r.first)).map(r => `${r.name}(${r.first})`)).toEqual([]);
                                                                                                 ^
error: expect(received).toEqual(expected)

- []
+ [
+   "configure_bundler(&mut self)",
+   "try_start(&mut self)",
+   "complete_on_bundle_thread(&mut self)",
+   "set_result(&mut self)",
+   "set_log(&mut self)",
+   "set_transpiler(&mut self)",
+   "plugins(&self)",
+   "file_map(&mut self)",
+   "as_js_bundle_completion_task(&mut self)",
+   "create_and_configure_transpiler(&mut self)",
+   "init_and_run(&mut self)",
+ ]

- Expected  - 1
+ Received  + 13

      at <anonymous> (/workspace/bun/test/internal/source-lints/bundle-completion-task-handoff.test.ts:130:92)
(fail) CompletionStruct takes the task by pointer in every method [0.75ms]
136 |   // dequeue
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/internal/source-lints/bundle-completion-task-handoff.test.ts
bun test v1.4.0 (fb842a03b)

test/internal/source-lints/bundle-completion-task-handoff.test.ts:
(pass) the scans recognize the shapes they claim to [37.39ms]
(pass) CompletionStruct takes the task by pointer in every method [22.55ms]
(pass) BundleThread.rs never reborrows the dequeued task [13.08ms]
(pass) create_and_schedule_completion_task does not touch the task after enqueuing it [13.36ms]
(pass) the per-owner fields are private, so they can only be set through Deliver [20.29ms]

 5 pass
 0 fail
 17 expect() calls
Ran 5 tests across 1 file. [2.66s]
__F:0:S:0

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 1085ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/151] gen NodeModuleModule.lut.h
Generating /workspace/bun/build/release/codegen/NodeModuleModule.lut.h from /workspace/bun/src/jsc/modules/NodeModuleModule.cpp
[2/151] gen BunProcess.lut.h
Generating /workspace/bun/build/release/codegen/BunProcess.lut.h from /workspace/bun/src/jsc/bindings/BunProcess.cpp
[3/151] gen generated_host_exports.rs
generated_host_exports.rs: 93 exports (host=3, lazy=10, generic=80, rust=0); 239 extern-C blocks audited
[4/151] gen ZigGeneratedClasses.{cpp,h,rs}
Found 2 classes from /workspace/bun/src/jsc/resolve_message.classes.ts
  - ResolveMessage (15 fields)
  - BuildMessage (10 fields)
Found 1 classes from /workspace/bun/src/runtime/api/Archive.classes.ts
  - Archive (4 fields, 1 class fields)
Found 2 classes from /workspace/bun/src/runtime/api/BunObject.classes.ts
  - ResourceUsage (8 fields)
  - Subprocess (20 fields)
Found 1 classes from /workspace/bun/src/runtime/api/cron.classes.ts
  - CronJob (5 fields)
Found 3 classes from /workspace/bun/src/runtime/api/filesystem_r
... (truncated)
diff hotspot
src/bundler/BundleThread.rs                        | 165 +++--
 src/runtime/api/JSBundler.rs                       |  22 +-
 src/runtime/api/js_bundle_completion_task.rs       | 675 +++++++++++----------
 src/runtime/server/HTMLBundle.rs                   |  22 +-
 .../bundle-completion-task-handoff.test.ts         | 170 ++++++
 5 files changed, 613 insertions(+), 441 deletions(-)

gate history · 2 passed · 0 rejected · iteration 1

evidence per changed file
file                                                      reads  edits  tests
src/bundler/BundleThread.rs                                   5     11      0
src/runtime/api/JSBundler.rs                                  3      1      0
src/runtime/api/js_bundle_completion_task.rs                 15     42      0
src/runtime/server/HTMLBundle.rs                              5      6      0
…nal/source-lints/bundle-completion-task-handoff.test.ts      2     11      0
Original description

Problem

One JSBundleCompletionTask allocation is shared between the JS thread and the bundle thread for as long as a Bun.build() (or an HTML route's build) runs. On main the bundle thread holds it as &mut for the whole build: BundleThread::thread_main (src/bundler/BundleThread.rs) reborrows the dequeued task, and every CompletionStruct method takes &mut self, create_and_configure_transpiler for the option setup and init_and_run from before BundleV2::init until after run_from_js_in_new_thread returns. A reference argument is protected for the duration of its call, so while those calls are running, three things that happen on every build or on every teardown are accesses into protected memory through a pointer that is not derived from the reference:

  1. JS-side writes after the enqueue. create_and_schedule_completion_task (src/runtime/api/js_bundle_completion_task.rs) enqueued the task and only then did poll_ref.ref_(..), and its callers kept writing: JSBundler::build set promise (the comment there, "sole owner on the JS thread until enqueued task runs", was not true, the enqueue had already happened inside the callee) and HTMLBundle::Route::schedule_bundle set started_at_ns and html_build_task. By then the bundle thread may already be inside create_and_configure_transpiler(&mut self). This is the normal path of every build.
  2. Cancellation. stop_for_vm_teardown reads plugins and stores cancelled / loads bundle_loop, and HTMLBundle::State::deinit stores cancelled, while init_and_run(&mut self) is live on the bundle thread. They are atomics, but under both aliasing models an atomic store through a foreign pointer into memory a protected &mut covers is still a foreign write.
  3. The log. create_and_configure_transpiler handed Transpiler::init a &raw mut self.log taken out of its own &mut self, and the bundler writes that log for the whole build, i.e. while the later, sibling &mut self of init_and_run is protected. This one is single-threaded.

A reduction of exactly these three shapes fails under Miri with Tree Borrows (the model bun run rust:miri uses) and with Stacked Borrows, in each case pointing at the &mut self receiver, and passes once the methods take the pointer (and, for 1, the field is written before the hand-off):

before 1:  write access through <2086> at alloc990[0x0] is forbidden        (*task).promise = 1           on the JS thread
before 2:  write access through <14944> at alloc990[0x20] is forbidden      (*task).cancelled.store(..)   on the JS thread
before 3:  write access through <5544> at alloc990[0x10] is forbidden       *log = 1                      on the bundle thread
    = help: the accessed tag is foreign to the protected tag (i.e., it is not a child)
    = help: this foreign write access would cause the protected tag (currently Reserved) to become Disabled
    = help: protected tags must never be Disabled
help: the protected tag was created here, in the initial state Reserved
    |     fn configure_before(&mut self, ..)          (1)
    |     fn init_and_run_before(&mut self, ..)       (2, 3)

Stacked Borrows reports not granting access to tag .. because that would remove [Unique for ..] which is strongly protected for 1 and 2 and that tag does not exist in the borrow stack for 3. No crash is known from any of this; ASAN-clean today, because nothing on the bundle thread actually reads the fields the JS side writes. It is the contract that is wrong, and it is what noalias/dereferenceable on the receiver let the compiler assume. Same family as #37723, which converts the hand-back (complete_on_bundle_thread) and named the rest of the build's lifetime as a separate change; this is that change. The two overlap on complete_on_bundle_thread and on the pointer plumbing in thread_main / generate_in_new_thread, where both end up with the same shape, so whichever lands second has a small rebase.

Reduction run under Miri
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{Receiver, Sender, channel};
use std::thread;

struct SendPtr(*mut Task);
unsafe impl Send for SendPtr {}

struct Task { cancelled: AtomicBool, promise: u64, config: u64, log: u64, result: u64 }

/// Pins the interleaving the real code allows: the JS side acts while the
/// bundle thread is inside the call.
struct Rendezvous { inside: Sender<()>, js_done: Receiver<()> }
impl Rendezvous {
    fn js_turn(&self) { self.inside.send(()).unwrap(); self.js_done.recv().unwrap(); }
}

impl Task {
    // main
    fn configure_before(&mut self, rv: &Rendezvous) -> *mut u64 {
        let _ = self.config;
        rv.js_turn();
        &raw mut self.log
    }
    fn init_and_run_before(&mut self, log: *mut u64, rv: &Rendezvous, write_log: bool) {
        self.result = 1;
        if write_log { unsafe { *log = 1 } }          // the bundler logging during the build
        rv.js_turn();
        let _ = self.cancelled.load(Ordering::Acquire);
    }
    // this PR
    unsafe fn configure_after(this: *mut Task, rv: &Rendezvous) -> *mut u64 {
        let _ = unsafe { (*this).config };
        rv.js_turn();
        unsafe { &raw mut (*this).log }
    }
    unsafe fn init_and_run_after(this: *mut Task, log: *mut u64, rv: &Rendezvous, write_log: bool) {
        unsafe { (*this).result = 1 };
        if write_log { unsafe { *log = 1 } }
        rv.js_turn();
        let _ = unsafe { (*this).cancelled.load(Ordering::Acquire) };
    }
}

fn main() {
    let mut args = std::env::args().skip(1);
    let after = args.next().as_deref() == Some("after");
    let scenario: u32 = args.next().unwrap().parse().unwrap();
    let task = Box::into_raw(Box::new(Task {
        cancelled: AtomicBool::new(false), promise: 0, config: 7, log: 0, result: 0,
    }));
    let (inside_tx, inside_rx) = channel::<()>();
    let (js_done_tx, js_done_rx) = channel::<()>();
    let rv = Rendezvous { inside: inside_tx, js_done: js_done_rx };

    if after && scenario == 1 {
        // The fix for 1 is ordering: the owner's fields go in before the enqueue.
        unsafe { (*task).promise = 1 };
    }
    let ptr = SendPtr(task);
    let bundle_thread = thread::spawn(move || {
        let ptr = ptr;
        let task = ptr.0;
        let write_log = scenario == 3;
        if after {
            let log = unsafe { Task::configure_after(task, &rv) };
            unsafe { Task::init_and_run_after(task, log, &rv, write_log) };
        } else {
            let log = unsafe { (*task).configure_before(&rv) };
            unsafe { (*task).init_and_run_before(log, &rv, write_log) };
        }
    });

    // JS thread. First rendezvous: the bundle thread is inside `configure`.
    inside_rx.recv().unwrap();
    if !after && scenario == 1 {
        // JSBundler::build / HTMLBundle::schedule_bundle on main: writing the
        // task after create_and_schedule_completion_task enqueued it.
        unsafe { (*task).promise = 1 };
    }
    js_done_tx.send(()).unwrap();
    // Second rendezvous: inside `init_and_run`.
    inside_rx.recv().unwrap();
    if scenario == 2 {
        // stop_for_vm_teardown / HTMLBundle::State::deinit, identical in both
        // shapes; what differs is what the bundle thread holds meanwhile.
        unsafe { (*task).cancelled.store(true, Ordering::Release) };
    }
    js_done_tx.send(()).unwrap();
    bundle_thread.join().unwrap();
    drop(unsafe { Box::from_raw(task) });
}

MIRIFLAGS=-Zmiri-tree-borrows cargo miri run -- before {1,2,3} and the Stacked Borrows default all exit 1 with the errors quoted above; -- after {1,2,3} exit 0 under both models.

Fix

  • CompletionStruct (src/bundler/BundleThread.rs) takes the task as this: *mut Self in every method (try_start, free_released_unstarted, complete_on_bundle_thread, set_result, set_log, create_and_configure_transpiler, init_and_run); the trait doc carries the argument above and the one # Safety contract. thread_main no longer reborrows the dequeued task and generate_in_new_thread takes the pointer. The impl projects the fields it uses: create_and_configure_transpiler borrows config, takes &raw mut (*this).log and reads env; init_and_run stores bundle_loop through the atomic, reads plugins, borrows config for the file map and entry points, and builds the dispatch handle from this; the vtable thunks (task_of) project result / cancelled / loop_handle instead of forming a &JSBundleCompletionTask to the whole task. Same order of operations as before on both the success and the error path.
  • Methods the trait only had because the impl called them on itself are gone from it: configure_bundler is a free function over &mut Config (its body is unchanged apart from indentation and one SAFETY comment; git diff -w shows it), plugins / file_map / as_js_bundle_completion_task are inlined into init_and_run, and set_transpiler is deleted together with the transpiler field it wrote, which nothing read (the C++ plugin's config pointer that used to be the other way to reach the task is passed back as _unused).
  • create_and_schedule_completion_task takes a Deliver (Promise(JSPromiseStrong) from Bun.build, HtmlRoute(NonNull<Route>) from Bun.serve), fills promise / html_build_task / started_at_ns in at construction, takes the keep-alive ref before the enqueue, and the enqueue is its last statement. JSBundler::build creates the promise and reads its value before scheduling; HTMLBundle::schedule_bundle refs the route before handing its pointer over and only stores the returned pointer (for the cancel in State::deinit). promise, html_build_task and started_at_ns are private now (started_at_ns() for the dev-mode timing line), so the old writes do not compile. started_at_ns is taken a few microseconds earlier than before, that is the only observable difference.

Why pointer receivers rather than interior mutability: the fields the JS thread touches during the build are already atomics, and that does not help, because a &mut self retag claims the UnsafeCell bytes too (only shared references leave them out). The interior-mutability version of this fix would therefore have to give the bundle thread a &self to the whole task and put everything the bundle thread writes (log, result, the config.compile clear, the handle wiring) behind cells, touching every JS-side use of those fields as well. Taking the task by pointer and projecting fields is the smaller change and is the shape the rest of this struct's lifecycle already uses (free_released_unstarted, stop_for_vm_teardown, on_complete_anytask, and #37723's hand-back), as do the other conversions in this family.

Tests

test/internal/source-lints/bundle-completion-task-handoff.test.ts pins the two signatures the compiler cannot: every method of CompletionStruct takes this: *mut Self (and the methods that bracket the build are still there, so the check cannot pass vacuously), BundleThread.rs contains no reborrow of the dequeued task, nothing in create_and_schedule_completion_task goes through the task after the enqueue call, and the three per-owner fields are private. It checks its scans against positive and negative examples. Against main it reports the eleven self receivers, BundleThread.rs:235: &mut *completion, the (*completion) after the enqueue, and pub(crate) on all three fields; it passes with this branch.

Verification

Debug (ASAN) build: test/bundler/bun-build-api.test.ts and bundler_plugin.test.ts (105 pass, including the build-thousands-of-times test), bundler_plugin_chain, bundler_html_server, plugin-error-nested-throw, plugin-sync-exception-fallback, test/js/bun/http/bun-serve-html.test.ts, bun-serve-html-405, bun-serve-html-manifest (the HTMLBundle::Route path in development and production mode), the two worker tests in test/js/node/worker_threads/worker_threads.test.ts that terminate workers with builds both queued and mid-plugin (both stop_for_vm_teardown branches, racing try_start / free_released_unstarted), and the counted family of test/js/web/workers/worker-terminate-funnels.test.ts. bun test test/internal/source-lints/ (19 files) passes; cargo clippy -p bun_runtime -p bun_bundler and cargo fmt --check are clean. bun-serve-html-entry.test.ts cannot connect to localhost in this container with the release binary either (already noted on #37723).

…r, not &mut self

The bundle thread used to hold the dequeued JSBundleCompletionTask as &mut
for the whole build (thread_main reborrowed it, and every CompletionStruct
method took &mut self, init_and_run's call spanning the entire bundle),
while the JS thread still wrote to the task after enqueuing it (poll_ref,
promise, html_build_task, started_at_ns) and can cancel it at any point
(stop_for_vm_teardown, HTMLBundle State::deinit), and while the bundler
wrote the task's log through the pointer create_and_configure_transpiler
took out of an earlier &mut self. Each of those is a foreign access into
memory a protected reference covers.

CompletionStruct now takes the task as this: *mut Self in every method and
the impl projects the fields it uses; thread_main and
generate_in_new_thread carry the raw pointer. The trait loses the methods
only the impl itself called (configure_bundler becomes a free function over
the config, plugins/file_map/as_js_bundle_completion_task are inlined) and
set_transpiler together with the write-only transpiler field it set. The
dispatch vtable thunks project fields too instead of forming a & to the
whole task.

create_and_schedule_completion_task takes a Deliver (the Bun.build promise
or the HTML route) and fills the task in before the enqueue, which is now
its last statement; the keep-alive ref moves before it as well. promise,
html_build_task and started_at_ns become private so JSBundler::build and
HTMLBundle::schedule_bundle cannot write them afterwards.

A source lint pins both shapes: no self receivers on CompletionStruct, no
reborrow of the dequeued task in BundleThread.rs, nothing touching the task
after the enqueue, and the per-owner fields private.
@robobun

robobun commented Aug 12, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 1:28 PM PT - Aug 12th, 2026

❌ @robobun, your commit fb842a0 has 4 failures in Build #93546 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 37740

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

bun-37740 --bun

@robobun

robobun commented Aug 12, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review (head fb842a0, an empty commit on top of 1fadd08); needs a maintainer.

Reproduced structurally: the Miri reduction in the description fails on the three shapes main has (JS-side write after the enqueue, cancel store during init_and_run(&mut self), log written through a pointer from an earlier &mut self) and passes with the pointer receivers, and test/internal/source-lints/bundle-completion-task-handoff.test.ts fails against main (eleven self receivers, the &mut *completion reborrow and generate_in_new_thread(completion: &mut C), the (*completion) after the enqueue, the pub(crate) fields) and passes on this branch. No runtime behaviour changes; the suites listed under Verification pass on the debug/ASAN build. CI: build 92863 on 882708f passed on every lane. 1fadd08 and fb842a0 (empty) change nothing the compiler sees relative to it: 1fadd08's build 93274 passed 192 jobs and went red only because the darwin 26 test lane expired waiting for an agent, and fb842a0's build 93546 failed in the build steps on every platform with Failed to download after 5 attempts ... cause: fetch failed for the c-ares / mimalloc / lol-html tarballs and the prebuilt WebKit from github.com at about 20:15 UTC, i.e. a download outage, not this tree (the same window also gave linux-aarch64 verify-baseline a 503 fetching qemu, lost the windows 2019 test agent, and failed test/js/bun/test/parallel/test-docker-build-debian.ts inside its apt step on ubuntu x64, which is reported to main-break triage separately; none of those touch this change). I am not going to push further retriggers; a rebuild of 93546 from the Buildkite UI (or merging on the strength of 92863) is the remaining step.

Review: configure_bundler doc, the stale set_transpiler clause and the lint's binding pin (e092531); the optimize_imports concern checked under Miri and withdrawn (thread has the output); comments shortened (882708f); a further pass over the comments the receiver change had left stale or overclaiming (1fadd08), after which a second self-review pass on 1fadd08 raised nothing. All review threads are resolved.

Overlaps with #37723 on complete_on_bundle_thread and the pointer plumbing in BundleThread.rs, where both arrive at the same shape; whichever lands second gets a small rebase.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6c45d5df-3b22-46c8-ab2f-e51e6fd20a16

📥 Commits

Reviewing files that changed from the base of the PR and between c5a6101 and e092531.

📒 Files selected for processing (3)
  • src/bundler/BundleThread.rs
  • src/runtime/api/js_bundle_completion_task.rs
  • test/internal/source-lints/bundle-completion-task-handoff.test.ts

Walkthrough

Changes

The bundle completion flow now supports promise and HTML-route delivery targets. Task lifecycle callbacks use raw pointers across queue processing, bundle execution, result handoff, and cleanup. JavaScript and HTML callers use the updated scheduler API. Source-lint tests enforce the handoff rules.

Bundle completion handoff

Layer / File(s) Summary
Delivery-aware task scheduling
src/runtime/api/js_bundle_completion_task.rs, src/runtime/api/JSBundler.rs, src/runtime/server/HTMLBundle.rs
Deliver selects promise or HTML-route completion. Task creation returns NonNull, registers active handles before enqueueing, and exposes build timing through started_at_ns().
Raw-pointer lifecycle and queue handoff
src/bundler/BundleThread.rs, src/runtime/api/js_bundle_completion_task.rs, test/internal/source-lints/bundle-completion-task-handoff.test.ts
Completion callbacks and queue processing use raw task pointers. Unstarted tasks are explicitly freed. Source-lint tests check receiver signatures, reborrows, post-enqueue access, and private task fields.
Transpiler configuration and bundle execution
src/bundler/BundleThread.rs, src/runtime/api/js_bundle_completion_task.rs
Transpiler setup, bundler configuration, plugin and file-map projection, log transfer, result storage, and completion handoff use the raw-pointer API.

Possibly related PRs

  • oven-sh/bun#37703: Refactors asynchronous task lifecycle operations to use raw-pointer APIs with source-lint coverage.
  • oven-sh/bun#37716: Applies a similar raw-pointer lifecycle callback pattern to JSSink finalization.
  • oven-sh/bun#37741: Refactors asynchronous Rust task lifecycles and adds source-lint coverage for pointer handoff safety.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The description references related issue #37723 and explains the overlap and landing impact.
Out of Scope Changes check ✅ Passed The changes remain focused on bundle-task pointer handoff, lifecycle safety, related callers, and targeted source-lint coverage.
Title check ✅ Passed The title clearly and concisely identifies the main change: using a task pointer instead of a build-long mutable reference.
Description check ✅ Passed The description explains the problem, fix, testing, verification results, and relevant limitations, satisfying the repository template in substance.

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

@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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/bundler/BundleThread.rs (1)

273-320: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

The ? on Line 283 skips ast_memory_store.pop() and the teardown at Lines 342-347.

generate_in_new_thread calls ast_memory_store.push() on Line 279 and pairs it with pop() on Line 322. C::create_and_configure_transpiler(completion, bump)? on Line 283 returns early on Err and bypasses both pop() and the drop_in_place block on Lines 342-347.

Consequences on that path:

  • The AST-allocator thread-local stays pushed on the bundle thread. That thread is process-lifetime and serves every later build.
  • ASTMemoryAllocator is not dropped, so the embedded mi_heap leaks. This is the exact leak the comment on Lines 324-334 describes.
  • heap still drops, so the pushed thread-local now refers to reclaimed arena memory.

The task itself is still handed back, because thread_main runs set_result(Err) and complete_on_bundle_thread on Lines 242-243. So the build fails cleanly from the caller's view, but the bundle thread is left in a corrupted state.

Move the fallible section into an inner function or a scope guard so pop() and the teardown run on every path.

The coding guidelines require: "Pair every resource acquisition with release at the acquisition site, including all early-return, error, success, and lifecycle paths." and "Every error, abort, and timeout path must complete the operation: ... mirror success-path cleanup."

🛠️ Sketch: run the fallible section inside a closure so teardown always runs
         let bump = &heap;
         let ast_memory_store: &mut bun_ast::ASTMemoryAllocator =
             bump.alloc(bun_ast::ASTMemoryAllocator::new(bump));
         ast_memory_store.reset();
         ast_memory_store.push();
 
-        // Allocate + configure folded — see `create_and_configure_transpiler` doc.
-        // SAFETY: fn contract.
-        let transpiler = unsafe { C::create_and_configure_transpiler(completion, bump)? };
+        // Allocate + configure folded — see `create_and_configure_transpiler` doc.
+        // SAFETY: fn contract.
+        let transpiler = match unsafe { C::create_and_configure_transpiler(completion, bump) } {
+            Ok(t) => t,
+            Err(err) => {
+                // Mirror the success-path teardown before returning.
+                ast_memory_store.pop();
+                // SAFETY: unique `bump.alloc` slot; nothing else references it.
+                unsafe {
+                    core::ptr::drop_in_place(
+                        std::ptr::from_mut::<bun_ast::ASTMemoryAllocator>(ast_memory_store),
+                    );
+                }
+                return Err(err);
+            }
+        };
🤖 Prompt for 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.

In `@src/bundler/BundleThread.rs` around lines 273 - 320, Update
generate_in_new_thread so every error path after ast_memory_store.push()
performs the matching pop() and ASTMemoryAllocator teardown before returning.
Encapsulate the fallible transpiler creation and run flow in an inner scope or
scope guard, ensuring cleanup executes for both create_and_configure_transpiler
errors and successful runs while preserving existing log handling and completion
behavior.

Source: Coding guidelines

🤖 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 `@src/runtime/api/js_bundle_completion_task.rs`:
- Around line 1109-1301: Update the doc comment for configure_bundler to
explicitly state that its standalone-HTML branch clears config.compile; preserve
the existing implementation and make clear that the mutable config parameter is
required because this mutation is observed later.
- Around line 988-998: Update create_and_configure_transpiler to project
config.optimize_imports directly from the raw this pointer before
configure_bundler retains the config reference, then store that value in
transpiler.options.optimize_imports. Avoid deriving optimize_imports from the
existing &mut (*this).config borrow; leave the synchronous file_map and
entry-point borrows unchanged.

In `@test/internal/source-lints/bundle-completion-task-handoff.test.ts`:
- Around line 133-135: Strengthen the test around the `reborrows` assertion by
first asserting that the dequeue loop in `BundleThread.rs` still binds the task
as `completion`. Use the existing stripped-source inspection or matching helpers
to make this a positive assertion, while preserving the empty-result assertion
for `reborrows` so renaming the binding fails loudly.

---

Outside diff comments:
In `@src/bundler/BundleThread.rs`:
- Around line 273-320: Update generate_in_new_thread so every error path after
ast_memory_store.push() performs the matching pop() and ASTMemoryAllocator
teardown before returning. Encapsulate the fallible transpiler creation and run
flow in an inner scope or scope guard, ensuring cleanup executes for both
create_and_configure_transpiler errors and successful runs while preserving
existing log handling and completion behavior.
🪄 Autofix

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: c38366ed-b96b-4426-a5c7-5c21a667de5b

📥 Commits

Reviewing files that changed from the base of the PR and between f59f705 and c5a6101.

📒 Files selected for processing (5)
  • src/bundler/BundleThread.rs
  • src/runtime/api/JSBundler.rs
  • src/runtime/api/js_bundle_completion_task.rs
  • src/runtime/server/HTMLBundle.rs
  • test/internal/source-lints/bundle-completion-task-handoff.test.ts

Comment thread src/runtime/api/js_bundle_completion_task.rs
Comment thread src/runtime/api/js_bundle_completion_task.rs
Comment thread test/internal/source-lints/bundle-completion-task-handoff.test.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.

Beyond the inline nit: I checked the reordering in HTMLBundle::on_plugins_resolved — moving RefCount::<Route>::ref_ before the enqueue is the correct order (the matching ScopedRef::adopt in Route::on_complete now cannot precede it), and state.set(Building) landing after the enqueue is safe because on_complete is posted back to the same JS thread's event loop and cannot interleave. Grep also confirms no remaining callers of the deleted trait methods (set_transpiler / as_js_bundle_completion_task / file_map / plugins / from_completion_handle) beyond the stale comment already flagged.

Extended reasoning...

This PR converts every CompletionStruct method from &mut self to this: *mut Self, reorders create_and_schedule_completion_task so all JS-side writes precede the enqueue, and threads a new Deliver enum through both call sites. It is a large unsafe-Rust change to cross-thread memory-safety-critical code (the Bun.build / HTML-route bundle handoff), so it is not a candidate for auto-approval. Beyond the one nit already reported inline, I verified two things a reviewer would otherwise re-check: (1) in HTMLBundle::on_plugins_resolved, the route's self-ref now precedes the enqueue and state.set(Building) follows it — the former is required because Route::on_complete adopts that +1, and the latter is sound because on_complete_anytask is dispatched via loop_handle.post_task to the same JS thread, so it cannot run before on_plugins_resolved returns; (2) a repo-wide grep for the removed trait surface (set_transpiler, as_js_bundle_completion_task, file_map, plugins, from_completion_handle) and for create_and_schedule_completion_task callers turns up only the two updated call sites and the stale SAFETY comment at BundleThread.rs:337 that the inline nit already covers.

Comment thread src/bundler/BundleThread.rs
…_run reborrows it shared; pin the task binding the lint scans for
Comment thread src/bundler/BundleThread.rs Outdated
Comment thread src/bundler/BundleThread.rs Outdated
Comment thread src/bundler/BundleThread.rs Outdated
Comment thread src/bundler/BundleThread.rs
Comment thread src/bundler/BundleThread.rs
Comment thread src/bundler/BundleThread.rs
Comment thread src/bundler/BundleThread.rs
Comment thread src/bundler/BundleThread.rs
Comment thread src/bundler/BundleThread.rs
Comment thread src/runtime/api/js_bundle_completion_task.rs Outdated
Comment thread src/runtime/api/js_bundle_completion_task.rs Outdated
Comment thread src/runtime/api/js_bundle_completion_task.rs Outdated
Comment thread src/runtime/api/js_bundle_completion_task.rs Outdated
Comment thread src/runtime/api/js_bundle_completion_task.rs Outdated
Comment thread src/runtime/api/js_bundle_completion_task.rs
Comment thread src/runtime/api/js_bundle_completion_task.rs Outdated
Comment thread src/runtime/api/js_bundle_completion_task.rs Outdated
Comment thread src/runtime/api/js_bundle_completion_task.rs Outdated
Comment thread src/runtime/api/js_bundle_completion_task.rs
Comment thread src/runtime/api/js_bundle_completion_task.rs
Comment thread src/runtime/api/js_bundle_completion_task.rs
Comment thread src/runtime/api/js_bundle_completion_task.rs
Comment thread src/bundler/BundleThread.rs
Comment thread src/bundler/BundleThread.rs
Comment thread src/bundler/BundleThread.rs
Comment thread src/bundler/BundleThread.rs
Comment thread src/runtime/api/js_bundle_completion_task.rs Outdated
Comment thread src/runtime/api/js_bundle_completion_task.rs
Comment thread src/runtime/api/js_bundle_completion_task.rs Outdated
Comment thread src/runtime/api/js_bundle_completion_task.rs
Comment thread src/runtime/api/js_bundle_completion_task.rs Outdated
Comment thread src/runtime/api/js_bundle_completion_task.rs
@robobun

robobun commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed two follow-ups for this review round. e092531: configure_bundler's doc says why it takes config by &mut, the SAFETY comment above the transpiler drop_in_place no longer names the deleted set_transpiler, and the lint now pins the completion binding and the generate_in_new_thread(completion: *mut C) parameter it scans for (so it also fails on main for that reason). 882708f shortens the comments this PR adds. The optimize_imports thread has the Miri output for why the retained reference is valid with the shared reborrow in init_and_run (the &mut self receiver on main was the invalidating operation); only the two SAFETY comments changed for it. Every thread is replied to and resolved; the pre-existing comments that show up as added are the body of configure_bundler moving out of the impl (git diff -w).

@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 re-reviewed after e092531 and 882708f — the stale set_transpiler SAFETY comment I flagged is fixed, the configure_bundler doc and the lint's binding-name pin from CodeRabbit are in, and the comment-cop hits were shortened. No bugs found this pass. Given this reworks the cross-thread ownership contract for every Bun.build() (trait-wide &mut self → *mut Self, enqueue reordering, field-visibility changes), a human look at the overall shape is still worthwhile.

What was reviewed:

  • create_and_schedule_completion_task: verified nothing touches the task after enqueue, poll_ref.ref_ / embedded_work_scheduled / ActiveHandle::register all moved before it, and the returned pointer is only used for cancel/state-store at the two call sites.
  • init_and_run: checked the new &(*this).config is shared (not &mut) so it coexists with the transpiler's erased &config.optimize_imports from configure_bundler; configure_bundler body is byte-identical modulo indent and one SAFETY comment.
  • HTMLBundle::schedule_bundle: the route's self-ref now precedes the Deliver::HtmlRoute hand-off, so the on_complete deref contract holds even if the build completes before state.set.
  • generate_in_new_thread error path: set_result/complete_on_bundle_thread ordering matches the old code; set_log still runs on both arms before the hand-back.
Extended reasoning...

Overview

This PR fixes aliasing-model UB in the Bun.build() / HTML-route bundler path by converting every CompletionStruct trait method from &mut self to unsafe fn(this: *mut Self), and by moving all JS-side writes to the JSBundleCompletionTask struct to before it is enqueued to the bundle thread. The change spans src/bundler/BundleThread.rs (trait definition, thread_main, generate_in_new_thread), src/runtime/api/js_bundle_completion_task.rs (trait impl, create_and_schedule_completion_task, new Deliver enum, configure_bundler extracted to a free fn, transpiler field deleted, vtable thunks now project fields), src/runtime/api/JSBundler.rs and src/runtime/server/HTMLBundle.rs (call sites pass Deliver and no longer write to the task after scheduling), plus a new source-lint test that pins the signatures.

Security risks

None identified. This is an internal aliasing-contract change with no user-facing surface, no new input parsing, and no auth/crypto/permission code. The one behavioral difference the PR names (started_at_ns sampled a few µs earlier) affects only a dev-mode timing print.

Level of scrutiny

High. This is native cross-thread ownership code on the hot path of every Bun.build() and every HTML-route request in Bun.serve. REVIEW.md calls memory safety the most-blocked category. The change replaces compiler-enforced borrows with a documented # Safety contract on the trait, deletes a field (transpiler) and four trait methods, and reorders the enqueue relative to poll_ref, embedded_work_scheduled, set_config, and ActiveHandle::register. Each of those reorderings looks correct on inspection (all now happen while the JS thread is still the sole owner), but the blast radius of a mistake here is a use-after-free or hang on every build. The PR is well-argued (Miri reduction for all three shapes, exhaustive verification list), and the earlier review round's nits are all addressed, but the trait-wide design choice — raw-pointer receivers rather than, say, UnsafeCell-wrapping the concurrently-touched fields — is the kind of architectural call a maintainer should sign off on.

Other factors

  • My previous inline nit (stale set_transpiler clause in the drop_in_place SAFETY comment) is resolved; the comment now correctly names the BundleV2 borrow ending inside init_and_run.
  • Both CodeRabbit suggestions (documenting why configure_bundler takes &mut config; pinning the completion binding name so the lint can't go vacuous) are applied in e092531.
  • The comment-cop bot flagged many long comments; 882708f shortened them and the current file reads within the repo's usual density.
  • Overlaps with #37723 on complete_on_bundle_thread and the thread_main/generate_in_new_thread plumbing (noted in the description); whichever lands second needs a small rebase — a human should confirm the intended landing order.
  • The new source-lint test guards the shape but is not a runtime test of the fix; the actual verification is the existing bundler/HTML/worker suites the description lists, which exercise both stop_for_vm_teardown branches and the plugin path.

The BundleThread doc still listed configure_bundler as a trait member, the
log-copy comment still spoke of init_and_run owning `this` (which now names
the task), generate_in_new_thread claimed to be called from JS, and the two
docs describing the hand-off said nothing on the JS thread writes the task
after the enqueue, which leaves out the cancel and release-while-queued
paths the trait exists for. The optimize_imports SAFETY comment now states
the invariant that actually holds (the reference is dead once the bundle is
over, before the task is handed back) instead of the task outliving the
transpiler, which is not true on the success path.
Comment thread src/bundler/BundleThread.rs
Comment thread src/bundler/BundleThread.rs
Comment thread src/runtime/api/js_bundle_completion_task.rs
Comment thread src/runtime/api/js_bundle_completion_task.rs
Comment thread src/runtime/api/js_bundle_completion_task.rs

@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 reviewed this PR again after e092531/882708f/1fadd08f and didn't find any bugs; the stale set_transpiler SAFETY clause I flagged earlier is now reworded. Because this reworks the cross-thread JSBundleCompletionTask hand-off (every CompletionStruct receiver, the enqueue ordering, and the vtable thunks) under Stacked/Tree Borrows reasoning, a human look is still worthwhile.

What was reviewed:

  • create_and_schedule_completion_task ordering — poll_ref.ref_, embedded_work_scheduled, set_config, and ActiveHandle::register all land before the enqueue; the enqueue is the last touch.
  • HTMLBundle::on_plugins_resolved — the route's self-ref is now taken before Deliver::HtmlRoute(NonNull::from(self)) is handed over; Route's mutable state is all Cell/JsCell, so shared-provenance is fine for Route::on_complete.
  • Deleted transpiler field / set_transpiler — no remaining readers in src/; _unused in JSBundlerPlugin__onLoadAsync/onResolveAsync was already unused.
  • configure_bundler body vs. the old method — checked with git diff -w; only the optimize_imports SAFETY comment and the banner/footer comment wording differ.
Extended reasoning...

Overview

This PR converts the CompletionStruct trait (the bundle thread's view of a queued Bun.build()) from &mut self/&self receivers to unsafe fn(this: *mut Self) across all seven methods, removes the &mut *completion reborrow in BundleThread::thread_main, and reorders create_and_schedule_completion_task so every JS-side field write (promise, html_build_task, started_at_ns, keep-alive ref, embedded_work_scheduled, plugin set_config, active-handle registration) happens before the enqueue. A new Deliver enum carries the per-owner delivery target at construction; promise/html_build_task/started_at_ns become module-private so the old post-enqueue writes no longer compile. configure_bundler moves from a trait method to a free fn(&mut Config, &mut Transpiler) (body unchanged per git diff -w), the never-read transpiler: *mut BundleV2 field and set_transpiler are deleted, and the CompletionDispatch vtable thunks project single fields via task_of() returning *mut instead of forming &JSBundleCompletionTask to the whole allocation. A source-lint test pins the receiver shape, the dequeue binding, the post-enqueue tail, and the field visibilities.

Security risks

None identified. This is an internal aliasing-model correctness refactor with no user-facing surface, no new input parsing, and no change to trust boundaries. The order of operations on both success and error paths in generate_in_new_thread/thread_main is preserved.

Level of scrutiny

High. This is ~600 net lines across the bundle-thread hand-off, the JSBundleCompletionTask struct, both call sites (Bun.build and HTMLBundle::Route), and the dispatch vtable — all unsafe Rust reasoning about cross-thread pointer provenance under Stacked/Tree Borrows. REVIEW.md flags native memory safety as the most-blocked category, and this change specifically trades compiler-checked &mut exclusivity for hand-proven per-field projection. The PR description's Miri reduction is thorough and the shape matches the already-landed #37723 family, but the reasoning about which fields the JS thread may touch concurrently (atomics only: cancelled, bundle_loop, stage) versus which the bundle thread owns until hand-back (config, log, result) is subtle enough that a maintainer familiar with this struct's lifecycle should confirm it.

Other factors

My prior inline nit (stale set_transpiler clause in the drop_in_place SAFETY comment) was addressed in e092531. CodeRabbit's optimize_imports retained-reference concern was checked under Miri and withdrawn — the shared reborrow in init_and_run is valid under both models; the &mut self on main was the invalidating operation. CI passed on 882708f. Five comment-cop notices from the 14:22 run remain open on 2-4-line doc/SAFETY comments; the author addressed the substance in the previous round (they state contracts, not workarounds), but they are technically outstanding automated feedback. No human reviewer has weighed in yet.

@robobun

robobun commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator Author

Small overlap with #37709: that PR deletes CompletionDispatch::result_is_err (and the rejected parameter of JSBundlerPlugin__drainDeferred) because the only caller, the deferred batch, can only run while result is still Pending, so it was always false. This PR rewrites that same vtable closure in COMPLETION_VTABLE and removes the as_js_bundle_completion_task trait method whose doc line #37709 edits, so whichever lands second gets a trivial conflict there; if #37709 goes first, the result_is_err rewrite here can simply be dropped.

@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 reviewed this PR and didn't find any bugs; my earlier note about the stale set_transpiler reference in the drop_in_place SAFETY comment was addressed in e092531. Because this reworks the cross-thread ownership contract of JSBundleCompletionTask (every CompletionStruct receiver, the enqueue ordering, and the vtable thunks) across ~600 lines of unsafe Rust, a human look is still worthwhile.

What was reviewed:

  • create_and_schedule_completion_task ordering — poll_ref.ref_, embedded_work_scheduled, set_config, and ActiveHandle::register all now precede the enqueue; the enqueue is the last touch and the tail only returns the pointer.
  • HTMLBundle::schedule_bundle — the route's self-ref is taken before Deliver::HtmlRoute hands the pointer over; State::Building still stores the completion pointer for State::deinit's cancel.
  • init_and_run's &(*this).config borrow vs the retained &config.optimize_imports in the transpiler — shared reborrow only, matching the Miri result in the resolved thread.
  • The deleted transpiler field / set_transpiler — grepped for remaining readers; none, and the _unused params on JSBundlerPlugin__onLoadAsync/onResolveAsync were already unused.
Extended reasoning...

Overview

This PR converts the bundle-thread side of JSBundleCompletionTask's lifecycle from &mut self receivers to raw *mut Self field projections, and reorders the JS-thread hand-off so every field is written before the task is enqueued. Files touched: src/bundler/BundleThread.rs (the CompletionStruct trait and thread_main/generate_in_new_thread), src/runtime/api/js_bundle_completion_task.rs (the impl, create_and_schedule_completion_task, the Deliver enum, the COMPLETION_VTABLE thunks, and configure_bundler moved to a free function), the two callers in JSBundler.rs and HTMLBundle.rs, and a new source-lint test that pins the receiver shapes and the enqueue-is-last invariant.

Security risks

None identified. This is an aliasing-model correctness fix (Miri-diagnosed protected-tag violations under both Stacked and Tree Borrows); no user-facing input handling, no auth/crypto, no new I/O.

Level of scrutiny

High. This is exactly the class REVIEW.md calls out as "the most-blocked category": one allocation shared between the JS thread and the bundle thread for the whole build, with concurrent cancel/release paths (stop_for_vm_teardown, HTMLBundle::State::deinit) racing the bundle thread's try_start/init_and_run. Every reordered statement in create_and_schedule_completion_task and every field projection in the trait impl carries a thread-affinity or lifetime obligation. The PR description is unusually thorough (a runnable Miri reduction for all three failure shapes, and a rationale for pointer receivers over interior mutability), and the extensive comment-cop / CodeRabbit threads on the PR were all resolved with either trimming or a Miri counter-check — but the design choice (pointer receivers on a trait, field-projection discipline enforced only by a source-lint) is one a maintainer should sign off on.

Other factors

  • My earlier inline finding (stale set_transpiler mention in a SAFETY comment) was fixed in e092531; the comment now names the actual invariant (BundleV2 borrowing the transpiler went away inside init_and_run).
  • CodeRabbit's concern about the retained &config.optimize_imports was withdrawn after robobun's Miri reduction showed the later &(*this).config is a shared reborrow that both models accept; the SAFETY comments at both ends now record that constraint.
  • The configure_bundler body is unchanged modulo indentation and one SAFETY comment (verified against the diff; it moved from a trait method to a free fn(&mut Config, &mut Transpiler)).
  • The author noted a small overlap with #37709 (result_is_err deletion) and #37723 (complete_on_bundle_thread), so whichever lands second gets a trivial rebase.
  • The new source-lint test self-checks its regexes against positive/negative fixtures, so it is not vacuous, but it is a text scan rather than a type-level guarantee — that trade-off is called out in the test's header comment.

This branch has not been deployed

No deployments
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