Module loading: remove unsafe from ModuleLoader, AsyncModule and RuntimeTranspilerStore
What
Same programme as #40055 … #40284, applied to the JS-thread ↔ transpile-worker module-loading path in src/jsc/.
ModuleLoader.rs 9 → 1 and AsyncModule.rs 25 → 1 (residuals are all-safe fn extern blocks), RuntimeTranspilerStore.rs 50 → 3, job.rs 50 → 17. Collateral: jsc_hooks.rs −10, VirtualMachine.rs −8, JsAffine impls across glob/dns/Image/CompressionStreamCoder/read_file become plain impls.
- ModuleLoader:
__bun_transpile_source_codeis a safefn(&mut VirtualMachine, TranspileArgs<'_, '_>);TranspileArgs/TranspileExtracarry&mut Log/&mut BufferPrinter/Option<&PromiseSlot>instead of raw pointers;Bun__fetchBuiltinModule,ModuleLoader__isBuiltin,Bun__getDefaultLoader,Bun__runVirtualModuleareHOST_EXPORTs. - AsyncModule:
WakeContextno longer holds*mut Queue— the package manager wakes the JS thread withVmHandle::post_ping(kind, tag);Queue::on_dependency_error(&mut self)is reached through the current thread's VM;resume_loading_moduleuses aSourceCodePrinterRAII loan (nodetach_lifetime). - RuntimeTranspilerStore: the hive-slot
TranspilerJob+ intrusive queue + raw*mut VMbecomebun_jsc::Job<TranspileJob>; VM inputs are snapshotted on the JS thread (TranspileInput); shared state isBackRef<SavedSourceMap>/BackRef<AtomicU32>; the worker printer is a safe thread-local loan; path text is interned rather than boxed with a fake'static; theRuntimeTranspilerStoretask tag andrelease_queued_jobs_for_teardownare gone. - job.rs:
JsAffineis a safe marker (nothing unsafe relies on it); erased dispatch goes through a safeerasefn + oneunsafe fn erased_from_rawused bydispatch.rs;complete/release_unrun/into_halvestakeBox<Self>;Completion::finishuses aManuallyDrop'd obligation ZST instead ofptr::read. - Primitives:
bun_event_loop::{Task::ping, BoxedTask + boxed_task!},VmHandle::post_ping,SavedSourceMapoverGuardedwith&selfmethods (it was already used from three threads through aliased&mut),SourceMapHandlerGetter::new(&SavedSourceMap, inline)+write_inline_trailer,bun_ast::ErasedEntry+take_entry,bun_bundler::transpiler::{JobTranspiler, JobTranspilerCall}(per-call arena/log re-aim guard, also used byJSTranspiler),jsc_hooks::intern_path(single-hash).
Residuals and their blockers — RuntimeTranspilerStore.rs (3): unsafe impl Send for TranspileWork, JobTranspiler::new, and one &mut ImportWatcher for add_file; root causes are that Transpiler has no per-call-session split (parse/print need &mut) and Watcher::add_file takes &mut self though it is mutex-serialised — both are bundler/watcher refactors. job.rs (17) is the primitive (JsPtr, intrusive JobList, pool container_of entry, erased_from_raw, Send for Completion). hot_reloader.rs is untouched here (covered by a sibling PR).
Perf (release, taskset -c 56-63, interleaved ×20, perf stat -e instructions:u, median; A-vs-A noise ±0.07 %): 300-TS-file cold import +0.08 %, import() ×50 +0.15 %, setImmediate 1e6 +0.03 %, Bun.sleep 1e6 −0.02 %, fs.promises.readFile ×100k −0.45 %. Malloc counts (uprobes): import300 35,990 → 35,772; readFile 620,668 → 620,633.
Testing
Debug+ASAN, --timeout 60000: test/js/bun/resolve 361 pass (3 baseline-identical reds: root-user ×2, one load timeout), test/js/node/module 107/107, test/cli/run (autoinstall 9/12 — same 3 network reds on baseline; transpiler-cache 13/13; preload 2/2), test/cli/hot 17/17, fs.test.ts 520/520, fs.promises 30/30, sleep, timers (setImmediate 3/3; RSS-leak/LSAN cases identical on baseline debug), plugin 48/48, bundler/transpiler 190/190, cyclic-imports/require-extensions regressions; source lints 163/163 (vm-thread-door inventory regenerated). Hand-driven: 300-file cold/warm, syntax error at file #150 (correct path/line, exit 1), import() ×50, TLA rejected import, 20 Workers × 50 imports terminated mid-import × 5 (no ASAN, RSS flat), bun --hot 30 rapid edits (31 reloads), auto-install -i/--install=fallback/force. clippy clean on bun_ast/bun_event_loop/bun_bundler/bun_jsc/bun_runtime.