From ee6c3be34ce96ffa3a500f0390deb6148bd4f46f Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 18 May 2026 04:20:25 +0000 Subject: [PATCH 01/32] perf(threading): cache thread_id::current() in a #[thread_local] slot The bundler's Worker::get(ctx) calls bun_threading::current_thread_id() once per scheduled task to look up the thread's Worker in the pool's assignment map. That routes to bun_core::thread_id::current(), which made a fresh gettid()/pthread_threadid_np()/GetCurrentThreadId() syscall on every call. A 19 K-module bundle (rolldown apps/10000) schedules ~5.7 tasks per module (parse, line-offset table, quoted source contents, compile-result generation, link step 5), so it paid ~109 K gettid syscalls vs. the Zig version's ~129 - about 36% of the build's total syscall time. Zig's std.Thread.getCurrentId() doesn't have this cost: LinuxThreadImpl reads a threadlocal var tls_thread_id set once at thread start (vendor/zig/lib/std/Thread.zig:841,885). Cache the result in a bare #[thread_local] Cell slot so subsequent calls are a single TLS load with no LocalKey initialization branch or destructor registration. Lazy rather than set-at-spawn so threads not started through Bun's pool (FFI callbacks, the main thread) still get a valid ID; 0 is the unset sentinel since kernel TIDs and Win32/Darwin thread IDs are nonzero. --- src/bun_core/lib.rs | 1 + src/bun_core/thread_id.rs | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/bun_core/lib.rs b/src/bun_core/lib.rs index 497f7156610..ac446bd1f51 100644 --- a/src/bun_core/lib.rs +++ b/src/bun_core/lib.rs @@ -1,6 +1,7 @@ #![feature(allocator_api)] #![feature(adt_const_params)] #![feature(macro_metavar_expr)] // `$$` in define_scoped_log! (nightly-2026-05-06) +#![feature(thread_local)] // bare `__thread` slot for `thread_id::current()` cache #![allow( unused, non_snake_case, diff --git a/src/bun_core/thread_id.rs b/src/bun_core/thread_id.rs index 7111169f7c8..5ad58663eba 100644 --- a/src/bun_core/thread_id.rs +++ b/src/bun_core/thread_id.rs @@ -107,13 +107,46 @@ pub type AtomicThreadId = core::sync::atomic::AtomicUsize; // Zig: `pub const invalid = std.math.maxInt(std.Thread.Id);` pub const INVALID: ThreadId = ThreadId::MAX; +/// Per-thread cache of [`current()`]. Zig's `LinuxThreadImpl.getCurrentId()` +/// reads a `threadlocal var tls_thread_id` set once at thread start +/// (vendor/zig/lib/std/Thread.zig:841,885); the Rust port called the syscall +/// (`gettid`/`pthread_threadid_np`/`GetCurrentThreadId`) on every call. The +/// bundler's `Worker::get(ctx)` calls `current()` once per scheduled task — +/// parse, line-offset table, quoted source contents, compile-result +/// generation, link step 5 — so a 19 K-module build paid ~109 K `gettid` +/// syscalls (~36 % of total syscall time on the rolldown `apps/10000` bench). +/// +/// `0` is the unset sentinel: kernel TIDs / `pthread_threadid_np` IDs / +/// Win32 thread IDs are all nonzero. A bare `#[thread_local]` slot (not the +/// `thread_local!` macro) so this is a single TLS load with no `LocalKey` +/// initialization-state branch or destructor registration — same as Zig's +/// `threadlocal var`. +#[thread_local] +static TLS_THREAD_ID: core::cell::Cell = core::cell::Cell::new(0); + /// Returns the platform's notion of the calling thread's ID. /// /// Port of Zig `std.Thread.getCurrentId()` (`PosixThreadImpl` / `WindowsThreadImpl` / /// `LinuxThreadImpl`). Attempts to use OS-specific primitives so the value matches what /// debuggers/tracers report; falls back to `pthread_self()` as a `usize` on unknown targets. +/// +/// Cached per-thread after the first call (see [`TLS_THREAD_ID`]); subsequent +/// calls are a single TLS read with no syscall, matching Zig's +/// `tls_thread_id` slot. Lazy rather than set-at-spawn so threads not started +/// by Bun's pool (FFI callbacks, the main thread) still get a valid ID. #[inline] pub fn current() -> ThreadId { + let cached = TLS_THREAD_ID.get(); + if cached != 0 { + return cached; + } + let id = current_uncached(); + TLS_THREAD_ID.set(id); + id +} + +#[cold] +fn current_uncached() -> ThreadId { #[cfg(any(target_os = "linux", target_os = "android"))] { // Zig: `LinuxThreadImpl.getCurrentId()` → `linux.gettid()`. From d4252e99634c922a924df886eb6dc93a75708326 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 18 May 2026 05:11:22 +0000 Subject: [PATCH 02/32] ast: arena-back Ast.symbols/parts/import_records via ArenaVec --- src/ast/ast_result.rs | 67 ++++++++++++++++------------------------ src/ast/import_record.rs | 2 +- src/ast/nodes.rs | 2 +- src/ast/symbol.rs | 32 +++++++++---------- src/bun_alloc/lib.rs | 10 ++++++ 5 files changed, 54 insertions(+), 59 deletions(-) diff --git a/src/ast/ast_result.rs b/src/ast/ast_result.rs index 4bb493c9963..f53eedc712d 100644 --- a/src/ast/ast_result.rs +++ b/src/ast/ast_result.rs @@ -6,9 +6,8 @@ use bun_alloc::AstAlloc; use bun_collections::array_hash_map::{AutoContext, StringContext}; -use bun_collections::{ArrayHashMap, StringArrayHashMap, StringHashMap, VecExt}; +use bun_collections::{ArrayHashMap, StringArrayHashMap, StringHashMap}; -use crate::import_record::ImportRecord; use crate::runtime; use crate::{ CharFreq, ExportsKind, Expr, InlinedEnumValue, LocRef, NamedExport, NamedImport, Part, Range, @@ -17,12 +16,11 @@ use crate::{ use crate::part::List as PartList; use crate::symbol::List as SymbolList; -// `ImportRecord.List` is `Vec` (`bun_ast::import_record::List`). -type ImportRecordList = Vec; +type ImportRecordList<'a> = crate::import_record::List<'a>; pub type TopLevelSymbolToParts = ArrayHashMap>; -pub struct Ast { +pub struct Ast<'a> { pub approximate_newline_count: usize, pub has_lazy_export: bool, pub runtime_imports: runtime::Imports, @@ -51,16 +49,16 @@ pub struct Ast { /// These are stored at the AST level instead of on individual AST nodes so /// they can be manipulated efficiently without a full AST traversal - pub import_records: ImportRecordList, + pub import_records: ImportRecordList<'a>, // `hashbang`/`directive` are `[]const u8` slices into source text (not // freed in Zig `deinit`). `StoreStr` records them under the same // lifetime-erased contract as `StoreRef`. pub hashbang: StoreStr, pub directive: Option, - pub parts: PartList, + pub parts: PartList<'a>, // This list may be mutated later, so we should store the capacity - pub symbols: SymbolList, + pub symbols: SymbolList<'a>, pub module_scope: Scope, pub char_freq: Option, pub exports_ref: Ref, @@ -102,8 +100,12 @@ pub struct Ast { // PORT NOTE: Zig field defaults reference named constants (`Ref.None`, `logger.Range.None`, // `ExportsKind.none`, `Target.browser`) whose equivalence to the Rust types' `Default::default()` // is unverified across crates, so spell them out here instead of `#[derive(Default)]`. -impl Default for Ast { - fn default() -> Self { +// +// `parts`/`symbols`/`import_records` are now `ArenaVec`s and need an allocator, +// so `Default` no longer applies; use `Ast::empty_in(arena)` (or `Ast::empty()` +// with the process-static arena for placeholder values). +impl<'a> Ast<'a> { + pub fn empty_in(arena: &'a bun_alloc::MimallocArena) -> Self { Self { approximate_newline_count: 0, has_lazy_export: false, @@ -121,11 +123,11 @@ impl Default for Ast { import_keyword: Range::NONE, export_keyword: Range::NONE, top_level_await_keyword: Range::NONE, - import_records: Default::default(), + import_records: ImportRecordList::new_in(arena), hashbang: StoreStr::EMPTY, directive: None, - parts: Default::default(), - symbols: Default::default(), + parts: PartList::new_in(arena), + symbols: SymbolList::new_in(arena), module_scope: Scope::default(), char_freq: None, exports_ref: Ref::NONE, @@ -175,39 +177,24 @@ pub type NamedExports = StringArrayHashMap pub type ConstValuesMap = ArrayHashMap; pub type TsEnumsMap = ArrayHashMap, AutoContext, AstAlloc>; -impl Ast { - pub fn from_parts(parts: Box<[Part]>) -> Ast { - Ast { - parts: PartList::from_owned_slice(parts), - runtime_imports: Default::default(), - ..Default::default() - } - } - - // Zig `initTest` borrowed `parts` via `Part.List.fromBorrowedSliceDangerous` - // and relied on explicit `deinit` never being called. `Vec::drop` now - // unconditionally guards on `Origin::Borrowed` (not debug-only), so unwrapping - // the `ManuallyDrop` is safe — the caller's slice is never freed by `Ast`'s Drop. - pub fn init_test(parts: &[Part]) -> Ast { +impl Ast<'static> { + pub fn from_parts(parts: Box<[Part]>) -> Ast<'static> { + let arena = bun_alloc::global_arena(); + let mut p = PartList::with_capacity_in(parts.len(), arena); + p.extend(parts.into_vec()); Ast { - // SAFETY: test-only helper; the borrowed list is tagged - // `Origin::Borrowed`, so `Vec::drop` skips the free, and no - // grow/free path is reached on `Ast.parts` before the borrow ends. - parts: std::mem::ManuallyDrop::into_inner(unsafe { - PartList::from_borrowed_slice_dangerous(parts) - }), - runtime_imports: Default::default(), - ..Default::default() + parts: p, + ..Ast::empty() } } // Zig: `pub const empty = Ast{ .parts = Part.List{}, .runtime_imports = .{} };` - // All fields use their defaults, so `Ast::default()` is the Rust equivalent. - // TODO(port): if a true `const` is required at use sites, revisit once field types are `const`-constructible. - pub fn empty() -> Ast { - Ast::default() + pub fn empty() -> Ast<'static> { + Ast::empty_in(bun_alloc::global_arena()) } +} +impl<'a> Ast<'a> { // Zig: `std.json.stringify(self.parts, opts, stream)` where // `opts = .{ .whitespace = .{ .separator = true } }`. In the Rust port the // `crate::JsonWriter` trait stands in for the configured @@ -219,7 +206,7 @@ impl Ast { pub fn to_json(&self, stream: &mut W) -> Result<(), bun_core::Error> { // PORT NOTE: `whitespace.separator = true` is the caller's // responsibility when constructing the `JsonWriter` impl. - stream.write(self.parts.slice()) + stream.write(self.parts.as_slice()) } // Zig `deinit` only freed `parts`, `symbols`, `import_records` via `bun.default_allocator`, diff --git a/src/ast/import_record.rs b/src/ast/import_record.rs index 65640e163bd..cc2bc80661a 100644 --- a/src/ast/import_record.rs +++ b/src/ast/import_record.rs @@ -108,7 +108,7 @@ bitflags::bitflags! { } } -pub type List = Vec; +pub type List<'a> = bun_alloc::ArenaVec<'a, ImportRecord>; #[repr(u8)] #[derive(Copy, Clone, Eq, PartialEq, Debug, Default)] diff --git a/src/ast/nodes.rs b/src/ast/nodes.rs index 0884f5d8896..a4d86e11742 100644 --- a/src/ast/nodes.rs +++ b/src/ast/nodes.rs @@ -1137,7 +1137,7 @@ pub struct Part { } pub type PartImportRecordIndices = Vec; -pub type PartList = Vec; +pub type PartList<'a> = bun_alloc::ArenaVec<'a, Part>; #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub enum PartTag { diff --git a/src/ast/symbol.rs b/src/ast/symbol.rs index 785943436cb..43872f9b652 100644 --- a/src/ast/symbol.rs +++ b/src/ast/symbol.rs @@ -1,8 +1,6 @@ use core::sync::atomic::{AtomicU32, Ordering}; use std::cell::Cell; -use bun_collections::VecExt; - use crate::ImportItemStatus; use crate::base::Ref; use crate::g as G; @@ -388,8 +386,8 @@ pub struct Use { pub count_estimate: u32, } -pub type List = Vec; -pub type NestedList = Vec; +pub type List<'a> = bun_alloc::ArenaVec<'a, Symbol>; +pub type NestedList = Vec>; impl Symbol { pub fn merge_contents_with(&mut self, old: &mut Symbol) { @@ -418,9 +416,9 @@ pub struct Map { impl Map { // Debug-only dump of the symbol table. pub fn dump(&self) { - for (i, symbols) in self.symbols_for_source.slice().iter().enumerate() { + for (i, symbols) in self.symbols_for_source.iter().enumerate() { bun_core::prettyln!("\n\n-- Source ID: {} ({} symbols) --\n", i, symbols.len(),); - for (inner_index, symbol) in symbols.slice().iter().enumerate() { + for (inner_index, symbol) in symbols.iter().enumerate() { let display_ref = if symbol.has_link() { symbol.link.get() } else { @@ -550,7 +548,7 @@ impl Map { // SAFETY: src in-bounds (parser-produced ref); raw-ptr field read — no `&` to the // element is created. idx in-bounds of the inner list. unsafe { - let inner: *mut List = self.symbols_for_source.as_ptr().cast_mut().add(src); + let inner: *mut List<'static> = self.symbols_for_source.as_ptr().cast_mut().add(src); debug_assert!(idx < (*inner).len()); Some((*inner).as_mut_ptr().add(idx)) } @@ -585,11 +583,11 @@ impl Map { pub fn init(source_count: usize) -> Map { // Zig: `arena.alloc([]Symbol, sourceCount)` (default_allocator) then NestedList.init. - // Per PORTING.md §Allocators (non-arena path), use Vec → Vec. - let mut v: Vec = Vec::with_capacity(source_count); - v.resize_with(source_count, List::default); + let arena = bun_alloc::global_arena(); + let mut v: NestedList = Vec::with_capacity(source_count); + v.resize_with(source_count, || List::new_in(arena)); Map { - symbols_for_source: NestedList::move_from_list(v), + symbols_for_source: v, } } @@ -600,8 +598,8 @@ impl Map { // caller is the printer one-shot, cold). // OWNERSHIP: returned `Map` is *owned*; the `Vec` allocated here leaks if a // consumer parks it in `ManuallyDrop` (e.g. renamer.rs `MinifyRenamer.symbols`). - pub fn init_with_one_list(list: List) -> Map { - Self::init_list(NestedList::move_from_list(vec![list])) + pub fn init_with_one_list(list: List<'static>) -> Map { + Self::init_list(vec![list]) } pub fn init_list(list: NestedList) -> Map { @@ -647,8 +645,8 @@ impl Map { // `link` is `Cell`, so we can iterate the table by shared ref and // mutate `link` in place; `follow()` only takes `&self` and only touches // `link`, so the nested shared borrows coexist. - for symbols in self.symbols_for_source.slice().iter() { - for symbol in symbols.slice().iter() { + for symbols in self.symbols_for_source.iter() { + for symbol in symbols.iter() { if !symbol.has_link() { continue; } @@ -690,10 +688,10 @@ impl Map { // such refs satisfy the in-bounds contract (see `get_const`): // `(source_index, inner_index)` with tag ∈ {Symbol, AllocatedName}, // never `SourceContentsSlice` and never the null source sentinel. - let outer = self.symbols_for_source.slice(); + let outer = self.symbols_for_source.as_slice(); let lookup = |r: Ref| -> &Symbol { debug_assert!(!r.is_source_contents_slice()); - &outer[r.source_index() as usize].slice()[r.inner_index() as usize] + &outer[r.source_index() as usize][r.inner_index() as usize] }; let mut root = link; diff --git a/src/bun_alloc/lib.rs b/src/bun_alloc/lib.rs index 905acdbd6e2..9d48d890245 100644 --- a/src/bun_alloc/lib.rs +++ b/src/bun_alloc/lib.rs @@ -270,6 +270,16 @@ pub type Bump = bumpalo::Bump; pub type ArenaVec<'a, T> = Vec; pub use mimalloc_arena::{ArenaString, ArenaVecExt, live_arena_heaps, vec_from_iter_in}; +/// Process-wide [`MimallocArena`] over `mi_heap_main()` — thread-safe to +/// allocate from on any thread, never resets. Use as the allocator for +/// placeholder/sentinel [`ArenaVec`]s (`Vec::new_in(global_arena())` is +/// alloc-free) and for `ArenaVec`s that outlive any per-task arena (linker +/// graph clones). Allocations behave identically to the global allocator. +pub fn global_arena() -> &'static MimallocArena { + static ARENA: std::sync::OnceLock = std::sync::OnceLock::new(); + ARENA.get_or_init(MimallocArena::borrowing_default) +} + /// `bumpalo::format!` parity — `arena_format!(in arena, "...", ..)` → /// [`ArenaString`]. #[macro_export] From 766b5522b4103b67e91ae9ec2f8bbdbb1c164df9 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 18 May 2026 05:19:29 +0000 Subject: [PATCH 03/32] js_parser: zero-copy move of symbols/parts/import_records in to_ast --- src/js_parser/lib.rs | 4 +- src/js_parser/p.rs | 39 ++++++---------- src/js_parser/parse/parse_entry.rs | 37 ++++++--------- src/js_parser/parser.rs | 73 ++++++++++++++++++------------ 4 files changed, 71 insertions(+), 82 deletions(-) diff --git a/src/js_parser/lib.rs b/src/js_parser/lib.rs index ec1c599f733..9554c19dfec 100644 --- a/src/js_parser/lib.rs +++ b/src/js_parser/lib.rs @@ -226,10 +226,10 @@ use bun_ast::{Ast, Ref}; // `Ast` variant collapses `Result` to 16 B so only a thin pointer is moved up // the stack — one mimalloc-arena alloc per parsed module is far cheaper than // 4+ kilobyte memmoves. The other variants are already tiny. -pub enum Result { +pub enum Result<'a> { AlreadyBundled(AlreadyBundled), Cached, - Ast(Box), + Ast(Box>), } #[derive(Copy, Clone, PartialEq, Eq, Debug)] diff --git a/src/js_parser/p.rs b/src/js_parser/p.rs index 6100e6e1f7b..06b07fc16e2 100644 --- a/src/js_parser/p.rs +++ b/src/js_parser/p.rs @@ -156,10 +156,12 @@ impl<'a> ImportRecordList<'a> { /// Drop then ran element destructors on records the returned `Ast` still /// pointed at. This adapter restores Zig's move-and-zero semantics for both /// the bump-backed and externally-borrowed variants. - pub fn move_to_baby_list(&mut self, arena: &'a Bump) -> Vec { + pub fn move_to_baby_list(&mut self, arena: &'a Bump) -> BumpVec<'a, ImportRecord> { match core::mem::replace(self, Self::Owned(BumpVec::new_in(arena))) { - Self::Owned(v) => Vec::from_bump_vec(v), - Self::Borrowed(v) => core::mem::take(v), + Self::Owned(v) => v, + // SCAN_ONLY path never reaches `to_ast`, so `Borrowed` never hits + // this arm at runtime; the copy keeps the type checker happy. + Self::Borrowed(v) => bun_alloc::vec_from_iter_in(v.drain(..), arena), } } } @@ -8356,7 +8358,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O exports_kind: js_ast::ExportsKind, wrap_mode: WrapMode, hashbang: &'a [u8], - ) -> Result, bun_core::Error> { + ) -> Result>, bun_core::Error> { use crate::lower::lower_esm_exports_hmr::ConvertESMExportsForHmr; use crate::scan::scan_imports::ImportScanner; @@ -8861,28 +8863,13 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O let require_ref = self.runtime_imports.__require.unwrap_or(self.require_ref); let runtime_imports = core::mem::take(&mut self.runtime_imports); - // PORT NOTE: BumpVec<'a, T> can't be moved into a global-arena Vec; - // wrap the bump-backed storage as a Borrowed Vec (Drop is no-op). - // Spec P.zig:6695-6696 uses `moveFromList`, which transfers storage and - // *zeroes the source*. Mirror that move-and-zero with - // `mem::replace(.., new_in)` + `into_bump_slice_mut()` so the leftover - // BumpVec is empty when `P`/the caller's `parts` drops — `Part` carries - // owning fields (`symbol_uses`, `declared_symbols`, - // `import_record_indices`) and aliasing the live BumpVec slice (the old - // `as_mut_slice()` shape) double-dropped them once the parser fell out - // of scope. Same fix `ImportRecordList::move_to_baby_list` applies. - let symbols = js_ast::symbol::List::from_bump_vec(core::mem::replace( - &mut self.symbols, - BumpVec::new_in(arena), - )); - let parts_list = - Vec::::from_bump_vec(core::mem::replace(parts, BumpVec::new_in(arena))); - // Spec P.zig:6697: `ImportRecord.List.moveFromList(&p.import_records)`. - // Round-G fix: use the dedicated adapter so the parser-side list is - // left empty (Zig move-and-zero) and the BumpVec is leaked into the - // arena rather than dropped — downstream (printer, linker) resolves - // every `S.Import`/`E.RequireString`/`E.Import` by index against this. - let import_records: Vec = self.import_records.move_to_baby_list(arena); + // Spec P.zig:6695-6697 (`moveFromList`): re-tag the arena-backed buffer + // into the `Ast` and leave the parser-side slot empty — a pointer move, + // no realloc/memcpy. `Ast.{symbols,parts,import_records}` are now + // `ArenaVec<'a, T>` so the move is type-checked. + let symbols = core::mem::replace(&mut self.symbols, BumpVec::new_in(arena)); + let parts_list = core::mem::replace(parts, BumpVec::new_in(arena)); + let import_records = self.import_records.move_to_baby_list(arena); // PERF: box at the construction site so the ~1 KB `Ast` is written // straight into the heap allocation and only the thin `Box` pointer is diff --git a/src/js_parser/parse/parse_entry.rs b/src/js_parser/parse/parse_entry.rs index 96ca6004d7c..7f4e625952c 100644 --- a/src/js_parser/parse/parse_entry.rs +++ b/src/js_parser/parse/parse_entry.rs @@ -357,7 +357,7 @@ impl<'a> Parser<'a> { // surface lands. impl<'a> Parser<'a> { #[cfg_attr(not(target_arch = "wasm32"), allow(unused_mut))] - pub fn parse(mut self) -> Result { + pub fn parse(mut self) -> Result, Error> { // TODO(port): narrow error set #[cfg(target_arch = "wasm32")] { @@ -543,8 +543,8 @@ impl<'a> Parser<'a> { &mut self, expr: Expr, runtime_api_call: &'static [u8], - symbols: js_ast::symbol::List, - ) -> Result { + symbols: js_ast::symbol::List<'a>, + ) -> Result, Error> { // TODO(port): narrow error set // Zig moves lexer/options by value into `P` (Parser.zig) and only // `defer p.lexer.deinit()` cleans up — Zig has no implicit destructor @@ -581,13 +581,9 @@ impl<'a> Parser<'a> { // If we added to `p.symbols` it's going to fuck up all the indices // in the `symbols` array. debug_assert!(p.symbols.len() == 0); - let mut symbols_ = symbols; - // PORT NOTE: Zig `moveToListManaged(arena)` rebinds the same - // backing storage to an `ArrayList(arena)`. The Rust Vec - // adapter returns a `std::Vec`; `p.symbols` is a bump-backed Vec, so - // copy elements into the arena. TODO(perf): consider a zero-copy adapter. - p.symbols = - bun_alloc::vec_from_iter_in(symbols_.move_to_list_managed().into_iter(), p.arena); + // Zig: `moveToListManaged(arena)` — the buffer is already arena-backed, + // so this is now a plain move (matches Zig's pointer re-tag). + p.symbols = symbols; p.prepare_for_visit_pass()?; @@ -734,7 +730,7 @@ impl<'a> Parser<'a> { Ok(()) } - fn _parse(self) -> Result { + fn _parse(self) -> Result, Error> { // TODO(port): narrow error set // TODO(port): bun_crash_handler::current_action — `Action` stores // `&'static [u8]` but `self.source.path.text` is `'a`; widen @@ -1424,16 +1420,13 @@ impl<'a> Parser<'a> { if let Some(id) = redirect_import_record_index { part.symbol_uses = Default::default(); return Ok(crate::Result::Ast(Box::new(js_ast::Ast { - // Borrow the arena/Vec-backed records as a Vec view - // (matches `P::to_ast`); `p` is dropped immediately - // after this return so no double-ownership. - import_records: unsafe { - Vec::from_bump_slice(p.import_records.items_mut()) - }, + import_records: p + .import_records + .move_to_baby_list(p.arena), redirect_import_record_index: Some(id), named_imports: core::mem::take(&mut *p.named_imports), named_exports: core::mem::take(&mut p.named_exports), - ..Default::default() + ..js_ast::Ast::empty_in(p.arena) }))); } } @@ -1610,15 +1603,11 @@ impl<'a> Parser<'a> { if let Some(star) = export_star_redirect { return Ok(crate::Result::Ast(Box::new(js_ast::Ast { - // TODO(port): Zig set `.arena = p.arena`; arena ownership tracked elsewhere in Rust - // See note on the matching arm above re double-ownership. - import_records: unsafe { - Vec::from_bump_slice(p.import_records.items_mut()) - }, + import_records: p.import_records.move_to_baby_list(p.arena), redirect_import_record_index: Some(star.import_record_index), named_imports: core::mem::take(&mut *p.named_imports), named_exports: core::mem::take(&mut p.named_exports), - ..Default::default() + ..js_ast::Ast::empty_in(p.arena) }))); } } diff --git a/src/js_parser/parser.rs b/src/js_parser/parser.rs index 9a0a814a35a..49e58099687 100644 --- a/src/js_parser/parser.rs +++ b/src/js_parser/parser.rs @@ -2218,13 +2218,13 @@ impl Default for DeferredArrowArgErrors { pub fn new_lazy_export_ast<'bump>( bump: &'bump bun_alloc::Arena, - define: &mut Define, - opts: ParserOptions, + define: &'bump mut Define, + opts: ParserOptions<'bump>, log_to_copy_into: &mut bun_ast::Log, expr: Expr, - source: &bun_ast::Source, + source: &'bump bun_ast::Source, runtime_api_call: &'static [u8], // PERF(port): was comptime monomorphization -) -> Result, bun_core::Error> { +) -> Result>, bun_core::Error> { new_lazy_export_ast_impl( bump, define, @@ -2233,25 +2233,31 @@ pub fn new_lazy_export_ast<'bump>( expr, source, runtime_api_call, - js_ast::symbol::List::default(), + js_ast::symbol::List::new_in(bump), ) } pub fn new_lazy_export_ast_impl<'bump>( bump: &'bump bun_alloc::Arena, - define: &mut Define, - opts: ParserOptions, + define: &'bump mut Define, + opts: ParserOptions<'bump>, log_to_copy_into: &mut bun_ast::Log, expr: Expr, - source: &bun_ast::Source, + source: &'bump bun_ast::Source, runtime_api_call: &'static [u8], // PERF(port): was comptime monomorphization - symbols: js_ast::symbol::List, -) -> Result, bun_core::Error> { - let mut temp_log = bun_ast::Log::init(); + symbols: js_ast::symbol::List<'bump>, +) -> Result>, bun_core::Error> { + // `Ast<'bump>` borrows the arena for `'bump`, so the parser's `'a` must be + // `'bump`. The lexer's log is fed through `Lexer<'a>::init_without_reading` + // (`log: &'a mut Log`), which forces the log to also live for `'bump` — a + // stack local cannot satisfy that. Allocate the scratch log on `bump`; the + // arena is leaked/reset by the caller, so no extra allocation persists past + // the parse session. + let temp_log: &'bump mut bun_ast::Log = bump.alloc(bun_ast::Log::init()); // Zig held two aliasing `*Log` (parser.log + lexer.log). Both sides store // `NonNull` in Rust; copy the lexer's pointer so they share one // provenance chain. See `Parser::init` for the same pattern. - let lexer = js_lexer::Lexer::init_without_reading(&mut temp_log, source, bump); + let lexer = js_lexer::Lexer::init_without_reading(temp_log, source, bump); let log_ptr = lexer.log; let mut parser = Parser { options: opts, @@ -2261,28 +2267,35 @@ pub fn new_lazy_export_ast_impl<'bump>( source, log: log_ptr, }; - let result = match parser.to_lazy_export_ast(expr, runtime_api_call, symbols) { - Ok(r) => r, - Err(err) => { - let range = parser.lexer.range(); - drop(parser); - if temp_log.errors == 0 { - log_to_copy_into.add_range_error(Some(source), range, err.name().as_bytes()); - } - let _ = temp_log.append_to_maybe_recycled(log_to_copy_into, source); - return Ok(None); - } - }; - drop(parser); - - let _ = temp_log.append_to_maybe_recycled(log_to_copy_into, source); + let result = parser.to_lazy_export_ast(expr, runtime_api_call, symbols); + let range = parser.lexer.range(); + // `parser.log_mut()` re-derives the same `*mut Log` the lexer was handed, + // letting us flush diagnostics into `log_to_copy_into` while the parser + // (and thus `'bump`) is still alive — the safe replacement for the old + // post-`drop(parser)` reads of the stack-local `temp_log`. match result { - crate::Result::Ast(mut ast) => { + Ok(crate::Result::Ast(mut ast)) => { + let _ = parser + .log_mut() + .append_to_maybe_recycled(log_to_copy_into, source); + drop(parser); ast.has_lazy_export = true; Ok(Some(*ast)) } - // `to_lazy_export_ast` always returns `Result::Ast` (no parse pass runs). - _ => unreachable!("to_lazy_export_ast returns Result::Ast"), + Ok(_) => { + drop(parser); + // `to_lazy_export_ast` always returns `Result::Ast` (no parse pass runs). + unreachable!("to_lazy_export_ast returns Result::Ast") + } + Err(err) => { + let log = parser.log_mut(); + if log.errors == 0 { + log_to_copy_into.add_range_error(Some(source), range, err.name().as_bytes()); + } + let _ = log.append_to_maybe_recycled(log_to_copy_into, source); + drop(parser); + Ok(None) + } } } From a07bd69de53b1854c54670d823f870d0e0ecf5f6 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 18 May 2026 05:21:13 +0000 Subject: [PATCH 04/32] css/js_printer/resolver: adapt to arena-backed Ast lists --- src/css/css_parser.rs | 2 +- src/css/selectors/selector.rs | 2 +- src/css/values/ident.rs | 4 ++-- src/js_printer/lib.rs | 13 +++++++------ src/resolver/lib.rs | 2 +- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/css/css_parser.rs b/src/css/css_parser.rs index 043bade4693..c7d5e2c9bb9 100644 --- a/src/css/css_parser.rs +++ b/src/css/css_parser.rs @@ -21,7 +21,7 @@ use bun_core::strings; /// `bun.ast.Index` — bundler source-file index. Hoisted into /// `bun_options_types` to keep css below the parser tier. use bun_ast::Index as SrcIndex; -use bun_ast::symbol::List as SymbolList; +type SymbolList = Vec; use bun_ast::{ImportKind, ImportRecord}; pub use crate::compat::{self, Feature}; diff --git a/src/css/selectors/selector.rs b/src/css/selectors/selector.rs index ee59eb71c99..27a839d06f8 100644 --- a/src/css/selectors/selector.rs +++ b/src/css/selectors/selector.rs @@ -3,7 +3,7 @@ use crate::css_parser::compat::Feature; use crate::css_parser::targets::Targets; use crate::css_parser::{CSSString, PrintErr, Printer, StyleContext, VendorPrefix}; use crate::{CSSStringFns, IdentFns}; -use bun_ast::symbol::List as SymbolList; +type SymbolList = Vec; use bun_alloc::Arena as Bump; use bun_collections::ArrayHashMap; diff --git a/src/css/values/ident.rs b/src/css/values/ident.rs index 9c54f7396e9..fba98e6344b 100644 --- a/src/css/values/ident.rs +++ b/src/css/values/ident.rs @@ -415,13 +415,13 @@ impl IdentOrRef { .map(|p| unsafe { crate::arena_str(&**p) }) } - pub fn as_original_string(self, symbols: &bun_ast::symbol::List) -> &[u8] { + pub fn as_original_string(self, symbols: &[bun_ast::Symbol]) -> &[u8] { if self.is_ident() { // SAFETY: arena slice reconstructed from packed ptr/len return unsafe { crate::arena_str(self.as_ident().unwrap().v) }; } let r = self.as_ref().unwrap(); - symbols.at(r.inner_index() as usize).original_name.slice() + symbols[r.inner_index() as usize].original_name.slice() } pub fn hash(&self, hasher: &mut Wyhash) { diff --git a/src/js_printer/lib.rs b/src/js_printer/lib.rs index 704221887d1..24140ce4e0f 100644 --- a/src/js_printer/lib.rs +++ b/src/js_printer/lib.rs @@ -13,6 +13,7 @@ #![allow(unused, nonstandard_style, clippy::all)] #![warn(unused_must_use)] #![feature(adt_const_params)] +#![feature(allocator_api)] use bun_collections::VecExt; @@ -7975,7 +7976,7 @@ pub fn print_ast<'a, W: WriterTrait, const ASCII_ONLY: bool, const GENERATE_SOUR )?; } - for part in parts.slice() { + for part in parts.iter() { minify_renamer.accumulate_symbol_use_counts( &mut top_level_symbols, &part.symbol_uses, @@ -8025,7 +8026,7 @@ pub fn print_ast<'a, W: WriterTrait, const ASCII_ONLY: bool, const GENERATE_SOUR let mut printer = PrinterType::::init( writer, bump, - tree.import_records.slice(), + tree.import_records.as_slice(), opts, renamer, source_map_builder, @@ -8087,7 +8088,7 @@ pub fn print_ast<'a, W: WriterTrait, const ASCII_ONLY: bool, const GENERATE_SOUR } } - for part in tree.parts.slice() { + for part in tree.parts.iter() { for stmt in slice_of(part.stmts).iter() { printer.print_stmt(*stmt, TopLevel::init(IsTopLevel::Yes))?; printer.writer.get_error()?; @@ -8171,7 +8172,7 @@ pub fn print_json( // constructs the same empty inputs without round-tripping through `Ast`. let bump = bun_alloc::Arena::new(); let mut no_op = - rename::NoOpRenamer::init(js_ast::symbol::Map::init_list(vec![Vec::new()]), source); + rename::NoOpRenamer::init(js_ast::symbol::Map::init_list(vec![Vec::new_in(bun_alloc::global_arena())]), source); let full_opts = Options { indent: opts.indent, @@ -8402,7 +8403,7 @@ pub fn print_common_js< let mut printer = PrinterType::::init( writer, bump, - tree.import_records.slice(), + tree.import_records.as_slice(), opts, renamer.to_renamer(), source_map_builder, @@ -8432,7 +8433,7 @@ pub fn print_common_js< // PERF(port): was stack-fallback allocator printer.binary_expression_stack = Vec::new(); - for part in tree.parts.slice() { + for part in tree.parts.iter() { for stmt in slice_of(part.stmts).iter() { printer.print_stmt(*stmt, TopLevel::init(IsTopLevel::Yes))?; printer.writer.get_error()?; diff --git a/src/resolver/lib.rs b/src/resolver/lib.rs index 8ddfd19171a..157b9ee0a83 100644 --- a/src/resolver/lib.rs +++ b/src/resolver/lib.rs @@ -2594,7 +2594,7 @@ pub mod cache { #[derive(Default)] pub struct JavaScript {} - pub type JavaScriptResult = bun_js_parser::Result; + pub type JavaScriptResult<'a> = bun_js_parser::Result<'a>; impl JavaScript { #[inline] From 1dac3fb859a9f5cdd41f5b582e992089aa74df5a Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 18 May 2026 05:28:13 +0000 Subject: [PATCH 05/32] ast/js_parser: thread arena through Ast::empty_in/from_parts; decouple lexer log lifetime --- src/ast/ast_result.rs | 14 ++--- src/bundler/LinkerContext.rs | 16 +++--- src/bundler/LinkerGraph.rs | 8 +-- src/bundler/ParseTask.rs | 2 +- src/bundler/barrel_imports.rs | 28 +++++----- src/bundler/bundle_v2.rs | 32 +++++------ src/bundler/bundled_ast.rs | 25 ++++----- src/bundler/linker_context/MetafileBuilder.rs | 2 +- .../linker_context/StaticRouteVisitor.rs | 2 +- .../linker_context/convertStmtsForChunk.rs | 2 +- .../convertStmtsForChunkForDevServer.rs | 4 +- src/bundler/linker_context/doStep5.rs | 4 +- .../findImportedCSSFilesInJSOrder.rs | 2 +- .../generateCodeForFileInChunkJS.rs | 4 +- .../generateCodeForLazyExport.rs | 6 +-- .../generateCompileResultForHtmlChunk.rs | 2 +- .../linker_context/postProcessJSChunk.rs | 6 +-- .../linker_context/renameSymbolsInChunk.rs | 6 +-- .../linker_context/scanImportsAndExports.rs | 6 +-- src/js_parser/lexer.rs | 10 ++-- src/js_parser/parse/parse_entry.rs | 2 +- src/js_parser/parser.rs | 53 +++++++------------ 22 files changed, 110 insertions(+), 126 deletions(-) diff --git a/src/ast/ast_result.rs b/src/ast/ast_result.rs index f53eedc712d..8b3b1e305a1 100644 --- a/src/ast/ast_result.rs +++ b/src/ast/ast_result.rs @@ -177,24 +177,16 @@ pub type NamedExports = StringArrayHashMap pub type ConstValuesMap = ArrayHashMap; pub type TsEnumsMap = ArrayHashMap, AutoContext, AstAlloc>; -impl Ast<'static> { - pub fn from_parts(parts: Box<[Part]>) -> Ast<'static> { - let arena = bun_alloc::global_arena(); +impl<'a> Ast<'a> { + pub fn from_parts(parts: Box<[Part]>, arena: &'a bun_alloc::MimallocArena) -> Ast<'a> { let mut p = PartList::with_capacity_in(parts.len(), arena); p.extend(parts.into_vec()); Ast { parts: p, - ..Ast::empty() + ..Ast::empty_in(arena) } } - // Zig: `pub const empty = Ast{ .parts = Part.List{}, .runtime_imports = .{} };` - pub fn empty() -> Ast<'static> { - Ast::empty_in(bun_alloc::global_arena()) - } -} - -impl<'a> Ast<'a> { // Zig: `std.json.stringify(self.parts, opts, stream)` where // `opts = .{ .whitespace = .{ .separator = true } }`. In the Rust port the // `crate::JsonWriter` trait stands in for the configured diff --git a/src/bundler/LinkerContext.rs b/src/bundler/LinkerContext.rs index 633877897ae..8ab9686bf08 100644 --- a/src/bundler/LinkerContext.rs +++ b/src/bundler/LinkerContext.rs @@ -489,7 +489,7 @@ impl<'a> LinkerContext<'a> { if stmts.len() == 1 { if let Some(s_import) = stmts[0].data.s_import() { let record = self.graph.ast.items_import_records()[source_index as usize] - .at(s_import.import_record_index as usize); + [s_import.import_record_index as usize]; if record.source_index.is_valid() && self.graph.meta.items_flags()[record.source_index.get() as usize].wrap == WrapKind::None @@ -746,7 +746,7 @@ impl<'a> LinkerContext<'a> { // on shape mismatch). let original_ref = unsafe { (*self.graph.ast.items_parts()[html_import as usize] - .at(1) + [1] .stmts)[0] .data .s_lazy_export() @@ -2042,7 +2042,7 @@ impl<'a> LinkerContext<'a> { alloc: &Bump, ast: &JSAst, ) -> Result { - let record = ast.import_records.at(import_record_index as usize); + let record = ast.import_records[import_record_index as usize]; // Barrel optimization: deferred import records should be dropped if record.flags.contains(bun_ast::ImportRecordFlags::IS_UNUSED) { return Ok(true); @@ -2309,7 +2309,7 @@ impl<'a> LinkerContext<'a> { &printer_ast, source, print_options, - ast.import_records.slice(), + ast.import_records.as_slice(), parts_to_print, r, ) @@ -2321,7 +2321,7 @@ impl<'a> LinkerContext<'a> { &printer_ast, source, print_options, - ast.import_records.slice(), + ast.import_records.as_slice(), parts_to_print, r, ) @@ -3150,7 +3150,7 @@ impl<'a> LinkerContext<'a> { // no-op kept for parity with the original. for &part_id in common_js_parts { let runtime_parts = - self.graph.ast.items_parts()[Index::RUNTIME.get() as usize].slice(); + self.graph.ast.items_parts()[Index::RUNTIME.get() as usize].as_slice(); let part: &Part = &runtime_parts[part_id as usize]; let symbol_refs = part.symbol_uses.keys(); for r#ref in symbol_refs { @@ -3240,7 +3240,7 @@ impl<'a> LinkerContext<'a> { let mut async_import_count: usize = 0; { let import_records = - self.graph.ast.items_import_records()[source_index as usize].slice(); + self.graph.ast.items_import_records()[source_index as usize].as_slice(); let meta_flags = self.graph.meta.items_flags(); for record in import_records { @@ -3367,7 +3367,7 @@ impl<'a> LinkerContext<'a> { let ast_flags = self.graph.ast.items_flags(); // Is this an external file? - let record: &ImportRecord = import_records.at(named_import.import_record_index as usize); + let record: &ImportRecord = import_records[named_import.import_record_index as usize]; if !record.source_index.is_valid() { return ImportTrackerIterator { value: Default::default(), diff --git a/src/bundler/LinkerGraph.rs b/src/bundler/LinkerGraph.rs index 710a1951db8..5a4ed9830c6 100644 --- a/src/bundler/LinkerGraph.rs +++ b/src/bundler/LinkerGraph.rs @@ -232,7 +232,7 @@ pub fn add_part_to_file( // call (O(1); the cache was a Zig micro-opt that does not survive // Stacked Borrows). let declared_symbols: &mut DeclaredSymbolList = - &mut parts[id as usize].mut_(part_id as usize).declared_symbols; + &mut parts[id as usize][part_id as usize].declared_symbols; struct Ctx<'a> { overlay: &'a mut [TopLevelSymbolToParts], @@ -297,7 +297,7 @@ pub fn generate_symbol_import_and_use( // Mark this symbol as used by this part { - let part: &mut Part = &mut parts[source_index as usize].slice_mut()[part_index as usize]; + let part: &mut Part = &mut parts[source_index as usize].as_mut_slice()[part_index as usize]; let uses_entry = part.symbol_uses.get_or_put(ref_)?; if !uses_entry.found_existing { *uses_entry.value_ptr = symbol::Use { @@ -342,7 +342,7 @@ pub fn generate_symbol_import_and_use( ref_, ); let dependencies = - &mut parts[source_index as usize].slice_mut()[part_index as usize].dependencies; + &mut parts[source_index as usize].as_mut_slice()[part_index as usize].dependencies; // SAFETY: every element of `new_dependencies` is overwritten in the // zip-loop immediately below before any read/drop. let new_dependencies = unsafe { dependencies.writable_slice(part_ids.len()) }; @@ -630,7 +630,7 @@ impl LinkerGraph { self.ast.items_import_records_mut(); for source_id in self.reachable_files.slice() { for import_record in import_records_list[source_id.get() as usize] - .slice_mut() + .as_mut_slice() .iter_mut() { if import_record.source_index.is_valid() diff --git a/src/bundler/ParseTask.rs b/src/bundler/ParseTask.rs index d5256d2560a..21fc8fa5a01 100644 --- a/src/bundler/ParseTask.rs +++ b/src/bundler/ParseTask.rs @@ -1170,7 +1170,7 @@ pub mod parse_worker { // gave up on figuring out how to fix it so that // this feature could ship. ast.has_lazy_export = false; - ast.parts.slice_mut()[1] = Part { + ast.parts.as_mut_slice()[1] = Part { stmts: ast::StoreSlice::EMPTY, is_live: true, import_record_indices: { diff --git a/src/bundler/barrel_imports.rs b/src/bundler/barrel_imports.rs index db8fda6a8db..3c2dd424026 100644 --- a/src/bundler/barrel_imports.rs +++ b/src/bundler/barrel_imports.rs @@ -191,7 +191,7 @@ fn apply_barrel_optimization_impl( for rec_idx in needed_records.keys() { if (*rec_idx as usize) < ast.import_records.len() { - needed_paths.put(ast.import_records.slice()[*rec_idx as usize].path.text, ())?; + needed_paths.put(ast.import_records.as_slice()[*rec_idx as usize].path.text, ())?; } } @@ -200,7 +200,7 @@ fn apply_barrel_optimization_impl( if let Some(imp) = ast.named_imports.get(&entry.ref_) { if (imp.import_record_index as usize) < ast.import_records.len() { if needed_paths.contains( - ast.import_records.slice()[imp.import_record_index as usize] + ast.import_records.as_slice()[imp.import_record_index as usize] .path .text, ) { @@ -222,7 +222,7 @@ fn apply_barrel_optimization_impl( let iri = imp.import_record_index; if !needed_records.contains(&iri) { if (iri as usize) < ast.import_records.len() { - ast.import_records.slice_mut()[iri as usize] + ast.import_records.as_mut_slice()[iri as usize] .flags .insert(import_record::Flags::IS_UNUSED); has_deferrals = true; @@ -270,7 +270,7 @@ fn un_defer_record(import_records: &mut import_record::List, record_idx: u32) -> if record_idx as usize >= import_records.len() { return false; } - let rec = &mut import_records.slice_mut()[record_idx as usize]; + let rec = &mut import_records.as_mut_slice()[record_idx as usize]; if rec.flags.contains(import_record::Flags::IS_INTERNAL) || !rec.flags.contains(import_record::Flags::IS_UNUSED) { @@ -410,7 +410,7 @@ pub fn schedule_barrel_deferred_imports( // text, using non-unused records in this file. See #28886. let mut dedup_fallback: StringArrayHashMap<&'static [u8]> = StringArrayHashMap::default(); if dev_handle.is_some() { - for ir_probe in file_import_records.slice() { + for ir_probe in file_import_records.as_slice() { if ir_probe.flags.contains(import_record::Flags::IS_UNUSED) || ir_probe.flags.contains(import_record::Flags::IS_INTERNAL) { @@ -432,7 +432,7 @@ pub fn schedule_barrel_deferred_imports( continue; } named_ir_indices.put(ni.import_record_index, ())?; - let ir = &file_import_records.slice()[ni.import_record_index as usize]; + let ir = &file_import_records.as_slice()[ni.import_record_index as usize]; // In dev server mode, source_index may not be patched — resolve via // path map as a read-only fallback. Do NOT write back to the import // record — the dev server intentionally leaves source_indices unset @@ -493,7 +493,7 @@ pub fn schedule_barrel_deferred_imports( // - `import("x")`: returns the full module namespace at runtime — consumer // can destructure or access any export. Must mark as .all. We cannot // safely assume which exports will be used. - for (idx, ir) in file_import_records.slice().iter().enumerate() { + for (idx, ir) in file_import_records.as_slice().iter().enumerate() { let target = if ir.source_index.is_valid() { ir.source_index.get() } else if let Some(map) = path_to_source_index_map { @@ -538,7 +538,7 @@ pub fn schedule_barrel_deferred_imports( if ni.import_record_index as usize >= file_import_records.len() { continue; } - let ir = &file_import_records.slice()[ni.import_record_index as usize]; + let ir = &file_import_records.as_slice()[ni.import_record_index as usize]; let resolved_path_text = if ir.flags.contains(import_record::Flags::IS_UNUSED) { dedup_fallback .get(ir.path.text) @@ -575,7 +575,7 @@ pub fn schedule_barrel_deferred_imports( // Add bare require/dynamic-import targets to BFS as star imports — both // always need the full namespace. - for (idx, ir) in file_import_records.slice().iter().enumerate() { + for (idx, ir) in file_import_records.as_slice().iter().enumerate() { let target = if ir.source_index.is_valid() { ir.source_index.get() } else if let Some(map) = path_to_source_index_map { @@ -702,7 +702,7 @@ pub fn schedule_barrel_deferred_imports( // PORT NOTE: reshaped for borrowck — read flags by index, then mutate let len = barrel_ir.len(); for idx in 0..len { - let flags = barrel_ir.slice()[idx].flags; + let flags = barrel_ir.as_slice()[idx].flags; if flags.contains(import_record::Flags::IS_UNUSED) && !flags.contains(import_record::Flags::IS_INTERNAL) { @@ -735,14 +735,14 @@ pub fn schedule_barrel_deferred_imports( if un_defer_record(barrel_ir, star_idx) { barrels_to_resolve.put(barrel_idx, ())?; } - let mut star_rec_si = barrel_ir.slice()[star_idx as usize].source_index; + let mut star_rec_si = barrel_ir.as_slice()[star_idx as usize].source_index; if !star_rec_si.is_valid() { // Deferred record was never resolved — resolve inline now. newly_scheduled += resolve_barrel_records(this, barrel_idx, &mut barrels_to_resolve); // Re-derive after resolution may have mutated slices. star_rec_si = this.graph.ast.items_import_records_mut()[barrel_idx as usize] - .slice()[star_idx as usize] + .as_slice()[star_idx as usize] .source_index; } if star_rec_si.is_valid() { @@ -770,12 +770,12 @@ pub fn schedule_barrel_deferred_imports( }; if (resolution.import_record_index as usize) < barrel_ir.len() { let mut rec_si = - barrel_ir.slice()[resolution.import_record_index as usize].source_index; + barrel_ir.as_slice()[resolution.import_record_index as usize].source_index; if !rec_si.is_valid() { // Deferred record was never resolved — resolve inline now. newly_scheduled += resolve_barrel_records(this, barrel_idx, &mut barrels_to_resolve); - rec_si = this.graph.ast.items_import_records_mut()[barrel_idx as usize].slice() + rec_si = this.graph.ast.items_import_records_mut()[barrel_idx as usize].as_slice() [resolution.import_record_index as usize] .source_index; } diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index fb308978570..806d8763d2c 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -2173,7 +2173,7 @@ pub mod bv2_impl { for (ast_import_record_list, target) in ast_import_records.iter_mut().zip(targets.iter()) { - let import_records = ast_import_record_list.slice_mut(); + let import_records = ast_import_record_list.as_mut_slice(); let path_to_source_index_map = &self.graph.build_graphs[*target]; for import_record in import_records.iter_mut() { let source_index = import_record.source_index.get(); @@ -2239,7 +2239,7 @@ pub mod bv2_impl { let record: &mut ImportRecord = &mut self.graph.ast.items_import_records_mut() [import_record.importer_source_index as usize] - .slice_mut() + .as_mut_slice() [import_record.import_record_index as usize]; if let Some(out_loader) = record.loader { break 'brk out_loader; @@ -2269,14 +2269,14 @@ pub mod bv2_impl { let record: &mut ImportRecord = &mut self.graph.ast.items_import_records_mut() [import_record.importer_source_index as usize] - .slice_mut() + .as_mut_slice() [import_record.import_record_index as usize]; record.source_index = Index::init(idx); } else { let record: &mut ImportRecord = &mut self.graph.ast.items_import_records_mut() [import_record.importer_source_index as usize] - .slice_mut() + .as_mut_slice() [import_record.import_record_index as usize]; // SAFETY: see `value_ptr` note above. record.source_index = Index::init(unsafe { *value_ptr }); @@ -2350,7 +2350,7 @@ pub mod bv2_impl { let record: &mut ImportRecord = &mut self.graph.ast.items_import_records_mut() [import_record.importer_source_index as usize] - .slice_mut() + .as_mut_slice() [import_record.import_record_index as usize]; handles_import_errors = record .flags @@ -2435,7 +2435,7 @@ pub mod bv2_impl { None => { let record: &mut ImportRecord = &mut self.graph.ast.items_import_records_mut() [import_record.importer_source_index as usize] - .slice_mut()[import_record.import_record_index as usize]; + .as_mut_slice()[import_record.import_record_index as usize]; // Disable failing packages from being printed. // This may cause broken code to write. // However, doing this means we tell them all the resolve errors @@ -2485,7 +2485,7 @@ pub mod bv2_impl { let loader: Loader = 'brk: { let record: &ImportRecord = &self.graph.ast.items_import_records() [import_record.importer_source_index as usize] - .slice()[import_record.import_record_index as usize]; + .as_slice()[import_record.import_record_index as usize]; if let Some(out_loader) = record.loader { break 'brk out_loader; } @@ -2560,7 +2560,7 @@ pub mod bv2_impl { if let Some(source_index) = out_source_index { let record: &mut ImportRecord = &mut self.graph.ast.items_import_records_mut() [import_record.importer_source_index as usize] - .slice_mut()[import_record.import_record_index as usize]; + .as_mut_slice()[import_record.import_record_index as usize]; record.source_index = source_index; } } @@ -3821,7 +3821,7 @@ pub mod bv2_impl { let import_records = self.graph.ast.items_import_records(); for source_index in reachable_files { - let records: &[ImportRecord] = import_records[source_index.get() as usize].slice(); + let records: &[ImportRecord] = import_records[source_index.get() as usize].as_slice(); for record in records { if !record.source_index.is_valid() && record.tag == bun_ast::ImportRecordTag::None @@ -4867,7 +4867,7 @@ pub mod bv2_impl { }); } else { let import_record: &mut ImportRecord = &mut source_import_records - .slice_mut() + .as_mut_slice() [resolve.import_record.import_record_index as usize]; import_record.source_index = source_index; } @@ -5377,7 +5377,7 @@ pub mod bv2_impl { let mut log = bun_ast::Log::init(); if LinkerContext::scan_css_imports( u32::try_from(index).expect("int cast"), - import_records.slice(), + import_records.as_slice(), // PORT NOTE: `scan_css_imports` takes the column as a raw // `*const` slice (the scanImportsAndExports caller holds raw // SoA pointers); it only reads via `is_none()`. Zig spec @@ -5419,13 +5419,13 @@ pub mod bv2_impl { js_files.push(Index::init(u32::try_from(index).expect("int cast"))); // PERF(port): was assume_capacity // Mark every part live. - for p in part_list.slice_mut() { + for p in part_list.as_mut_slice() { p.is_live = true; } } // Discover all CSS roots. - for record in import_records.slice_mut() { + for record in import_records.as_mut_slice() { if !record.source_index.is_valid() { continue; } @@ -6866,7 +6866,7 @@ pub mod bv2_impl { if save_import_record_source_index || input_file_loaders[to_assign.to_source_index.get() as usize].is_css() { - import_records.slice_mut()[to_assign.import_record_index as usize] + import_records.as_mut_slice()[to_assign.import_record_index as usize] .source_index = to_assign.to_source_index; } } @@ -6876,7 +6876,7 @@ pub mod bv2_impl { // Inlined `self.path_to_source_index_map(ctx.target)` (== `&mut self.graph.build_graphs[target]`) // so borrowck sees it as disjoint from `self.graph.input_files` above. let path_to_source_index_map = &mut self.graph.build_graphs[ctx.target]; - for (i, record) in import_records.slice_mut().iter_mut().enumerate() { + for (i, record) in import_records.as_mut_slice().iter_mut().enumerate() { if let Some(source_index) = path_to_source_index_map.get_path(&record.path) { if save_import_record_source_index || input_file_loaders[source_index as usize].is_css() @@ -7210,7 +7210,7 @@ pub mod bv2_impl { let result_ast_target = result.ast.target; for star_record_idx in result.ast.export_star_import_records.iter() { if (*star_record_idx as usize) < import_records.len() as usize { - let star_ir = &import_records.slice()[*star_record_idx as usize]; + let star_ir = &import_records.as_slice()[*star_record_idx as usize]; let resolved_index = if star_ir.source_index.is_valid() { star_ir.source_index.get() } else if let Some(idx) = diff --git a/src/bundler/bundled_ast.rs b/src/bundler/bundled_ast.rs index bef908c8d19..77097fafab4 100644 --- a/src/bundler/bundled_ast.rs +++ b/src/bundler/bundled_ast.rs @@ -60,17 +60,17 @@ pub struct BundledAst<'arena> { /// These are stored at the AST level instead of on individual AST nodes so /// they can be manipulated efficiently without a full AST traversal - pub import_records: import_record::List, + pub import_records: import_record::List<'arena>, // PORT NOTE: Ast.hashbang is `StoreStr`; mirror it here so init/to_ast can // round-trip. pub hashbang: StoreStr, - pub parts: part::List, + pub parts: part::List<'arena>, // Zig: `?*bun.css.BundlerStyleSheet`. See `CssAstRef` doc for the arena // drop-order invariant that backs the safe `Deref`. pub css: CssCol, pub url_for_css: &'arena [u8], - pub symbols: symbol::List, + pub symbols: symbol::List<'arena>, pub module_scope: Scope, // TODO(port): Zig used `= undefined`; only valid when flags.HAS_CHAR_FREQ is set. pub char_freq: CharFreq, @@ -112,12 +112,12 @@ bun_collections::multi_array_columns! { approximate_newline_count: u32, nested_scope_slot_counts: SlotCounts, exports_kind: ExportsKind, - import_records: import_record::List, + import_records: import_record::List<'arena>, hashbang: StoreStr, - parts: part::List, + parts: part::List<'arena>, css: CssCol, url_for_css: &'arena [u8], - symbols: symbol::List, + symbols: symbol::List<'arena>, module_scope: Scope, char_freq: CharFreq, exports_ref: Ref, @@ -161,15 +161,16 @@ bitflags::bitflags! { impl<'arena> BundledAst<'arena> { // TODO(port): Zig `pub const empty = BundledAst.init(Ast.empty);` — cannot be a `const` in Rust // because `init` is not const-evaluable. Consider a `static` via `OnceLock` or make - // `init`/`Ast::empty` const fn if feasible. - pub fn empty() -> Self { - Self::init(Ast::empty()) + // `init`/`Ast::empty_in` const fn if feasible. + pub fn empty_in(arena: &'arena bun_alloc::Arena) -> Self { + Self::init(Ast::empty_in(arena)) } // PORT NOTE: Zig's `*const BundledAst` bitwise-copies every field; the Rust // collection types aren't Copy, so consume `self` to move them (toAST is a // one-shot conversion back to the fat Ast). - pub fn to_ast(self) -> Ast { + pub fn to_ast(self) -> Ast<'arena> { + let arena: &'arena bun_alloc::Arena = *self.parts.allocator(); Ast { approximate_newline_count: self.approximate_newline_count as usize, nested_scope_slot_counts: self.nested_scope_slot_counts, @@ -241,11 +242,11 @@ impl<'arena> BundledAst<'arena> { None }, has_import_meta: self.flags.contains(Flags::HAS_IMPORT_META), - ..Ast::default() + ..Ast::empty_in(arena) } } - pub fn init(ast: Ast) -> Self { + pub fn init(ast: Ast<'arena>) -> Self { let mut flags = Flags::empty(); flags.set(Flags::USES_EXPORTS_REF, ast.uses_exports_ref); flags.set(Flags::USES_MODULE_REF, ast.uses_module_ref); diff --git a/src/bundler/linker_context/MetafileBuilder.rs b/src/bundler/linker_context/MetafileBuilder.rs index de76a7e1531..f9aa775dee7 100644 --- a/src/bundler/linker_context/MetafileBuilder.rs +++ b/src/bundler/linker_context/MetafileBuilder.rs @@ -282,7 +282,7 @@ pub fn generate(c: &mut LinkerContext, chunks: &mut [Chunk]) -> Result if (source_index as usize) < import_records_list.len() { let import_records = &import_records_list[source_index as usize]; let mut first_import = true; - for record in import_records.slice() { + for record in import_records.as_slice() { if record.kind == ImportKind::Internal { continue; } diff --git a/src/bundler/linker_context/StaticRouteVisitor.rs b/src/bundler/linker_context/StaticRouteVisitor.rs index 641d5b89da3..4ac81719540 100644 --- a/src/bundler/linker_context/StaticRouteVisitor.rs +++ b/src/bundler/linker_context/StaticRouteVisitor.rs @@ -81,7 +81,7 @@ impl<'a> StaticRouteVisitor<'a> { let import_records = &all_import_records[source_index.get() as usize]; let result = 'result: { - for import_record in import_records.slice() { + for import_record in import_records.as_slice() { if !import_record.source_index.is_valid() { continue; } diff --git a/src/bundler/linker_context/convertStmtsForChunk.rs b/src/bundler/linker_context/convertStmtsForChunk.rs index 8accc34e96b..d3704693783 100644 --- a/src/bundler/linker_context/convertStmtsForChunk.rs +++ b/src/bundler/linker_context/convertStmtsForChunk.rs @@ -145,7 +145,7 @@ pub fn convert_stmts_for_chunk( } // "export * from 'path'" - let record = ast.import_records.at(s.import_record_index as usize); + let record = ast.import_records[s.import_record_index as usize]; // Barrel optimization: deferred export * records should be dropped if record.flags.contains(ImportRecordFlags::IS_UNUSED) { diff --git a/src/bundler/linker_context/convertStmtsForChunkForDevServer.rs b/src/bundler/linker_context/convertStmtsForChunkForDevServer.rs index cb42cea860d..a571f1696d4 100644 --- a/src/bundler/linker_context/convertStmtsForChunkForDevServer.rs +++ b/src/bundler/linker_context/convertStmtsForChunkForDevServer.rs @@ -58,7 +58,7 @@ pub fn convert_stmts_for_chunk_for_dev_server<'bump>( let input_files = &c.parse_graph().input_files; let loaders = input_files.items_loader(); let sources = input_files.items_source(); - for record in ast.import_records.slice_mut() { + for record in ast.import_records.as_mut_slice() { if record.path.is_disabled { continue; } @@ -78,7 +78,7 @@ pub fn convert_stmts_for_chunk_for_dev_server<'bump>( for stmt in part_stmts { match &stmt.data { StmtData::SImport(st) => { - let record = ast.import_records.mut_(st.import_record_index as usize); + let record = ast.import_records[st.import_record_index as usize]; if record.path.is_disabled { continue; } diff --git a/src/bundler/linker_context/doStep5.rs b/src/bundler/linker_context/doStep5.rs index a410f3ee5ac..a2658550199 100644 --- a/src/bundler/linker_context/doStep5.rs +++ b/src/bundler/linker_context/doStep5.rs @@ -200,7 +200,7 @@ impl LinkerContext<'_> { // PORT NOTE: reshaped for borrowck — multiple `&mut` into graph SoA; // raw per-row pointers via `split_raw()` so concurrent tasks never // hold overlapping `&mut [T]`. - let parts_slice: *mut [Part] = row_mut!(ast.parts, bun_ast::PartList, id).slice_mut(); + let parts_slice: *mut [Part] = row_mut!(ast.parts, bun_ast::PartList, id).as_mut_slice(); let named_imports: *mut crate::bundled_ast::NamedImports = (ast.named_imports as *mut crate::bundled_ast::NamedImports).wrapping_add(id as usize); // SAFETY: `named_imports` is a stable column pointer (see above). We @@ -681,7 +681,7 @@ impl LinkerContext<'_> { // Initialize the part that was allocated for us earlier. The information // here will be used after this during tree shaking. - ast_parts.slice_mut()[bun_ast::NAMESPACE_EXPORT_PART_INDEX as usize] = Part { + ast_parts.as_mut_slice()[bun_ast::NAMESPACE_EXPORT_PART_INDEX as usize] = Part { stmts: if self.options.output_format != Format::InternalBakeDev { let init = &mut stmts_slab[all_export_stmts_base..stmts_head]; debug_assert_eq!(init.len(), all_export_stmts_len); diff --git a/src/bundler/linker_context/findImportedCSSFilesInJSOrder.rs b/src/bundler/linker_context/findImportedCSSFilesInJSOrder.rs index e38758dd469..5c837c3c773 100644 --- a/src/bundler/linker_context/findImportedCSSFilesInJSOrder.rs +++ b/src/bundler/linker_context/findImportedCSSFilesInJSOrder.rs @@ -60,7 +60,7 @@ pub fn find_imported_css_files_in_js_order( let p = &parts[source_index.get() as usize]; // Iterate over each part in the file in order - for part in p.slice() { + for part in p.as_slice() { // Traverse any files imported by this part. Note that CommonJS calls // to "require()" count as imports too, sort of as if the part has an // ESM "import" statement in it. This may seem weird because ESM imports diff --git a/src/bundler/linker_context/generateCodeForFileInChunkJS.rs b/src/bundler/linker_context/generateCodeForFileInChunkJS.rs index c6390392a40..e329c0fbab7 100644 --- a/src/bundler/linker_context/generateCodeForFileInChunkJS.rs +++ b/src/bundler/linker_context/generateCodeForFileInChunkJS.rs @@ -51,7 +51,7 @@ pub fn generate_code_for_file_in_chunk_js<'r, 'src>( let parts: *mut [Part] = unsafe { let list = &mut c.graph.ast.items_parts_mut()[source_index]; core::ptr::addr_of_mut!( - list.slice_mut() + list.as_mut_slice() [part_range.part_index_begin as usize..part_range.part_index_end as usize] ) }; @@ -442,7 +442,7 @@ pub fn generate_code_for_file_in_chunk_js<'r, 'src>( .graph .top_level_symbol_to_parts(source_index as u32, export_ref)[0] as usize; - let export_part = &ast.parts.slice()[part_idx]; + let export_part = &ast.parts.as_slice()[part_idx]; if export_part.is_live { // PTR_AUDIT(#1): `*prop` is a bitwise copy of // `e_object.properties[i]` (see `copy_nonoverlapping` diff --git a/src/bundler/linker_context/generateCodeForLazyExport.rs b/src/bundler/linker_context/generateCodeForLazyExport.rs index c301509870d..70a6da0529a 100644 --- a/src/bundler/linker_context/generateCodeForLazyExport.rs +++ b/src/bundler/linker_context/generateCodeForLazyExport.rs @@ -42,7 +42,7 @@ pub fn generate_code_for_lazy_export( let exports_kind = this.graph.ast.items_exports_kind()[source_index as usize]; // PORT NOTE: reshaped for borrowck — take `parts` as a raw pointer *before* the // long-lived immutable `items_css()` borrow below; re-borrowed again later as needed. - let parts: *mut [Part] = this.graph.ast.items_parts_mut()[source_index as usize].slice_mut(); + let parts: *mut [Part] = this.graph.ast.items_parts_mut()[source_index as usize].as_mut_slice(); // SAFETY: parse_graph backref; raw deref because `all_sources` is held // across `&mut *this.log` below (split borrow). let all_sources = unsafe { &(*this.parse_graph).input_files }.items_source(); @@ -504,7 +504,7 @@ pub fn generate_code_for_lazy_export( key.loc, ))); // PORT NOTE: `parts.ptr[generated[1]]` — re-borrow `parts` here for borrowck. - let parts = this.graph.ast.items_parts_mut()[source_index as usize].slice_mut(); + let parts = this.graph.ast.items_parts_mut()[source_index as usize].as_mut_slice(); parts[generated.1 as usize].stmts = bun_ast::StoreSlice::new_mut(new_stmts); } } @@ -539,7 +539,7 @@ pub fn generate_code_for_lazy_export( }, stmt.loc, ))); - let parts = this.graph.ast.items_parts_mut()[source_index as usize].slice_mut(); + let parts = this.graph.ast.items_parts_mut()[source_index as usize].as_mut_slice(); parts[generated.1 as usize].stmts = bun_ast::StoreSlice::new_mut(new_stmts); } } diff --git a/src/bundler/linker_context/generateCompileResultForHtmlChunk.rs b/src/bundler/linker_context/generateCompileResultForHtmlChunk.rs index e2ad1574d57..a96f7db6bac 100644 --- a/src/bundler/linker_context/generateCompileResultForHtmlChunk.rs +++ b/src/bundler/linker_context/generateCompileResultForHtmlChunk.rs @@ -424,7 +424,7 @@ fn generate_compile_result_for_html_chunk_impl<'a>( let compile_to_standalone_html = c.options.compile_to_standalone_html; let has_dev_server = c.dev_server.is_some(); let contents: &[u8] = &sources[source_index as usize].contents; - let records = import_records[source_index as usize].slice(); + let records = import_records[source_index as usize].as_slice(); let mut html_loader = HTMLLoader { linker: c, diff --git a/src/bundler/linker_context/postProcessJSChunk.rs b/src/bundler/linker_context/postProcessJSChunk.rs index 1804b22780a..7c9ae378630 100644 --- a/src/bundler/linker_context/postProcessJSChunk.rs +++ b/src/bundler/linker_context/postProcessJSChunk.rs @@ -301,9 +301,9 @@ pub fn post_process_js_chunk( if all_flags[part_range.source_index.get() as usize].wrap == crate::WrapKind::Cjs { continue; } - let source_parts = all_parts[part_range.source_index.get() as usize].slice(); + let source_parts = all_parts[part_range.source_index.get() as usize].as_slice(); let source_import_records = - all_import_records[part_range.source_index.get() as usize].slice(); + all_import_records[part_range.source_index.get() as usize].as_slice(); let mut part_i = part_range.part_index_begin; while part_i < part_range.part_index_end { // `Part.stmts: StoreSlice` — arena-backed, safe `Deref`. @@ -1369,7 +1369,7 @@ pub fn generate_entry_point_tail_js<'a>( // which outlives `'a` (the chunk-processing scope). Detach the borrow from // the local `ast_view` so it can satisfy `print`'s `&'a [ImportRecord]`. let import_records: &'a [ImportRecord] = - unsafe { bun_ptr::detach_lifetime(ast_view.import_records.slice()) }; + unsafe { bun_ptr::detach_lifetime(ast_view.import_records.as_slice()) }; CompileResult::Javascript { result: js_printer::print::( diff --git a/src/bundler/linker_context/renameSymbolsInChunk.rs b/src/bundler/linker_context/renameSymbolsInChunk.rs index 4462fe70630..153c8cdc38e 100644 --- a/src/bundler/linker_context/renameSymbolsInChunk.rs +++ b/src/bundler/linker_context/renameSymbolsInChunk.rs @@ -226,7 +226,7 @@ pub unsafe fn rename_symbols_in_chunk( )?; } - for part in parts.slice() { + for part in parts.as_slice() { if !part.is_live { continue; } @@ -285,7 +285,7 @@ pub unsafe fn rename_symbols_in_chunk( for &source_index in files_in_order { let wrap = all_flags[source_index as usize].wrap; // PORT NOTE: need `&mut [Part]` for `add_top_level_declared_symbols`. - let parts: &mut [Part] = all_parts[source_index as usize].slice_mut(); + let parts: &mut [Part] = all_parts[source_index as usize].as_mut_slice(); match wrap { // Modules wrapped in a CommonJS closure look like this: @@ -310,7 +310,7 @@ pub unsafe fn rename_symbols_in_chunk( // add those symbols to the top-level scope to avoid causing name // collisions. This code special-cases only those symbols. if c.options.output_format.keep_es6_import_export_syntax() { - let import_records = all_import_records[source_index as usize].slice(); + let import_records = all_import_records[source_index as usize].as_slice(); for part in parts.iter() { for stmt in part.stmts.slice() { match stmt.data { diff --git a/src/bundler/linker_context/scanImportsAndExports.rs b/src/bundler/linker_context/scanImportsAndExports.rs index 54d373d125a..fd737a83bc6 100644 --- a/src/bundler/linker_context/scanImportsAndExports.rs +++ b/src/bundler/linker_context/scanImportsAndExports.rs @@ -731,7 +731,7 @@ pub fn scan_imports_and_exports( // PERF(port): was zero-copy slice borrow; profile. let part: &mut Part = - &mut col!(parts_list)[id].slice_mut()[part_index as usize]; + &mut col!(parts_list)[id].as_mut_slice()[part_index as usize]; let re_exports: &[Dependency] = &re_exports_ptr; let total_len = parts_declaring_symbol.len() + re_exports.len() @@ -869,11 +869,11 @@ pub fn scan_imports_and_exports( // Imports of wrapped files must depend on the wrapper // PORT NOTE: iterate by index so each iteration re-borrows // `import_records` (the body calls `&mut this.graph` methods). - let import_record_indices_len = col_ref!(parts_list)[id].slice()[part_index] + let import_record_indices_len = col_ref!(parts_list)[id].as_slice()[part_index] .import_record_indices .len() as usize; for iri in 0..import_record_indices_len { - let import_record_index = col_ref!(parts_list)[id].slice()[part_index] + let import_record_index = col_ref!(parts_list)[id].as_slice()[part_index] .import_record_indices .slice()[iri]; let (kind, rec_source_index, rec_flags) = { diff --git a/src/js_parser/lexer.rs b/src/js_parser/lexer.rs index 139d78fc9ba..5ef78555ea9 100644 --- a/src/js_parser/lexer.rs +++ b/src/js_parser/lexer.rs @@ -2684,7 +2684,7 @@ lexer_impl_header! { } pub fn init_json( - log: &'a mut Log, + log: &mut Log, source: &'a Source, arena: &'a Arena, ) -> Result { @@ -2694,8 +2694,12 @@ lexer_impl_header! { Ok(lex) } + /// `log` is *not* tied to `'a`: the lexer stores it as `NonNull` (see + /// the `log` field doc) and the caller must keep the pointee alive for the + /// lexer's lifetime. The looser bound lets `'a` (which `Ast<'a>` borrows + /// through `arena`) outlive a stack-local scratch log. pub fn init_without_reading( - log: &'a mut Log, + log: &mut Log, source: &'a Source, arena: &'a Arena, ) -> Self { @@ -2748,7 +2752,7 @@ lexer_impl_header! { } pub fn init( - log: &'a mut Log, + log: &mut Log, source: &'a Source, arena: &'a Arena, ) -> Result { diff --git a/src/js_parser/parse/parse_entry.rs b/src/js_parser/parse/parse_entry.rs index 7f4e625952c..a822d26e58b 100644 --- a/src/js_parser/parse/parse_entry.rs +++ b/src/js_parser/parse/parse_entry.rs @@ -317,7 +317,7 @@ impl<'a> Options<'a> { impl<'a> Parser<'a> { pub fn init( options: Options<'a>, - log: &'a mut bun_ast::Log, + log: &mut bun_ast::Log, source: &'a bun_ast::Source, define: &'a Define, bump: &'a Arena, diff --git a/src/js_parser/parser.rs b/src/js_parser/parser.rs index 49e58099687..336423a2487 100644 --- a/src/js_parser/parser.rs +++ b/src/js_parser/parser.rs @@ -2247,17 +2247,11 @@ pub fn new_lazy_export_ast_impl<'bump>( runtime_api_call: &'static [u8], // PERF(port): was comptime monomorphization symbols: js_ast::symbol::List<'bump>, ) -> Result>, bun_core::Error> { - // `Ast<'bump>` borrows the arena for `'bump`, so the parser's `'a` must be - // `'bump`. The lexer's log is fed through `Lexer<'a>::init_without_reading` - // (`log: &'a mut Log`), which forces the log to also live for `'bump` — a - // stack local cannot satisfy that. Allocate the scratch log on `bump`; the - // arena is leaked/reset by the caller, so no extra allocation persists past - // the parse session. - let temp_log: &'bump mut bun_ast::Log = bump.alloc(bun_ast::Log::init()); + let mut temp_log = bun_ast::Log::init(); // Zig held two aliasing `*Log` (parser.log + lexer.log). Both sides store // `NonNull` in Rust; copy the lexer's pointer so they share one // provenance chain. See `Parser::init` for the same pattern. - let lexer = js_lexer::Lexer::init_without_reading(temp_log, source, bump); + let lexer = js_lexer::Lexer::init_without_reading(&mut temp_log, source, bump); let log_ptr = lexer.log; let mut parser = Parser { options: opts, @@ -2267,35 +2261,28 @@ pub fn new_lazy_export_ast_impl<'bump>( source, log: log_ptr, }; - let result = parser.to_lazy_export_ast(expr, runtime_api_call, symbols); - let range = parser.lexer.range(); - // `parser.log_mut()` re-derives the same `*mut Log` the lexer was handed, - // letting us flush diagnostics into `log_to_copy_into` while the parser - // (and thus `'bump`) is still alive — the safe replacement for the old - // post-`drop(parser)` reads of the stack-local `temp_log`. - match result { - Ok(crate::Result::Ast(mut ast)) => { - let _ = parser - .log_mut() - .append_to_maybe_recycled(log_to_copy_into, source); - drop(parser); - ast.has_lazy_export = true; - Ok(Some(*ast)) - } - Ok(_) => { - drop(parser); - // `to_lazy_export_ast` always returns `Result::Ast` (no parse pass runs). - unreachable!("to_lazy_export_ast returns Result::Ast") - } + let result = match parser.to_lazy_export_ast(expr, runtime_api_call, symbols) { + Ok(r) => r, Err(err) => { - let log = parser.log_mut(); - if log.errors == 0 { + let range = parser.lexer.range(); + drop(parser); + if temp_log.errors == 0 { log_to_copy_into.add_range_error(Some(source), range, err.name().as_bytes()); } - let _ = log.append_to_maybe_recycled(log_to_copy_into, source); - drop(parser); - Ok(None) + let _ = temp_log.append_to_maybe_recycled(log_to_copy_into, source); + return Ok(None); + } + }; + drop(parser); + + let _ = temp_log.append_to_maybe_recycled(log_to_copy_into, source); + match result { + crate::Result::Ast(mut ast) => { + ast.has_lazy_export = true; + Ok(Some(*ast)) } + // `to_lazy_export_ast` always returns `Result::Ast` (no parse pass runs). + _ => unreachable!("to_lazy_export_ast returns Result::Ast"), } } From e035622f52581eea0c0571578e1f5400ac8b2586 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 18 May 2026 05:29:48 +0000 Subject: [PATCH 06/32] ast: keep symbol::Map storage on the global allocator (NestedList = Vec>) --- src/ast/symbol.rs | 13 ++++++++----- src/bundler/bundle_v2.rs | 22 +++++++++++----------- src/js_printer/lib.rs | 2 +- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/ast/symbol.rs b/src/ast/symbol.rs index 43872f9b652..92adb2de807 100644 --- a/src/ast/symbol.rs +++ b/src/ast/symbol.rs @@ -387,7 +387,11 @@ pub struct Use { } pub type List<'a> = bun_alloc::ArenaVec<'a, Symbol>; -pub type NestedList = Vec>; +/// `Map.symbols_for_source` storage. Decoupled from [`List`] (which is +/// arena-backed): the linker clones every per-source symbol table here so it +/// can mutate them independently of the parsed `BundledAst.symbols`, and those +/// clones are owned for the link lifetime — global allocator, no arena tag. +pub type NestedList = Vec>; impl Symbol { pub fn merge_contents_with(&mut self, old: &mut Symbol) { @@ -548,7 +552,7 @@ impl Map { // SAFETY: src in-bounds (parser-produced ref); raw-ptr field read — no `&` to the // element is created. idx in-bounds of the inner list. unsafe { - let inner: *mut List<'static> = self.symbols_for_source.as_ptr().cast_mut().add(src); + let inner: *mut Vec = self.symbols_for_source.as_ptr().cast_mut().add(src); debug_assert!(idx < (*inner).len()); Some((*inner).as_mut_ptr().add(idx)) } @@ -583,9 +587,8 @@ impl Map { pub fn init(source_count: usize) -> Map { // Zig: `arena.alloc([]Symbol, sourceCount)` (default_allocator) then NestedList.init. - let arena = bun_alloc::global_arena(); let mut v: NestedList = Vec::with_capacity(source_count); - v.resize_with(source_count, || List::new_in(arena)); + v.resize_with(source_count, Vec::new); Map { symbols_for_source: v, } @@ -598,7 +601,7 @@ impl Map { // caller is the printer one-shot, cold). // OWNERSHIP: returned `Map` is *owned*; the `Vec` allocated here leaks if a // consumer parks it in `ManuallyDrop` (e.g. renamer.rs `MinifyRenamer.symbols`). - pub fn init_with_one_list(list: List<'static>) -> Map { + pub fn init_with_one_list(list: Vec) -> Map { Self::init_list(vec![list]) } diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index 806d8763d2c..a27eb00dab1 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -2604,7 +2604,7 @@ pub mod bv2_impl { self.path_to_source_index_map(target) .put(path_slice, source_index.get()) .expect("oom"); - let _ = self.graph.ast.append(JSAst::empty()); // OOM/capacity: Zig aborts; port keeps fire-and-forget + let _ = self.graph.ast.append(JSAst::empty_in(bun_alloc::global_arena())); // OOM/capacity: Zig aborts; port keeps fire-and-forget self.graph.input_files.append(crate::Graph::InputFile { source: bun_ast::Source { @@ -2720,7 +2720,7 @@ pub mod bv2_impl { self.path_to_source_index_map(target) .put(&path.text, source_index.get()) .expect("oom"); - let _ = self.graph.ast.append(JSAst::empty()); // OOM/capacity: Zig aborts; port keeps fire-and-forget + let _ = self.graph.ast.append(JSAst::empty_in(bun_alloc::global_arena())); // OOM/capacity: Zig aborts; port keeps fire-and-forget let side_effects = result.primary_side_effects_data; self.graph.input_files.append(crate::Graph::InputFile { @@ -3240,7 +3240,7 @@ pub mod bv2_impl { })?; // try this.graph.entry_points.append(arena, Index.runtime); - let _ = self.graph.ast.append(JSAst::empty()); // OOM/capacity: Zig aborts; port keeps fire-and-forget + let _ = self.graph.ast.append(JSAst::empty_in(bun_alloc::global_arena())); // OOM/capacity: Zig aborts; port keeps fire-and-forget self.path_to_source_index_map(self.transpiler.options.target) .put(&b"bun:wrap"[..], Index::RUNTIME.get()) .expect("oom"); @@ -3557,7 +3557,7 @@ pub mod bv2_impl { known_target: options::Target, ) -> Result { let source_index = Index::init(u32::try_from(self.graph.ast.len()).expect("int cast")); - let _ = self.graph.ast.append(JSAst::empty()); // OOM/capacity: Zig aborts; port keeps fire-and-forget + let _ = self.graph.ast.append(JSAst::empty_in(bun_alloc::global_arena())); // OOM/capacity: Zig aborts; port keeps fire-and-forget self.graph.input_files.append(crate::Graph::InputFile { source: core::mem::take(source), @@ -3611,7 +3611,7 @@ pub mod bv2_impl { known_target: options::Target, ) -> Result { let source_index = Index::init(u32::try_from(self.graph.ast.len()).expect("int cast")); - let _ = self.graph.ast.append(JSAst::empty()); // OOM/capacity: Zig aborts; port keeps fire-and-forget + let _ = self.graph.ast.append(JSAst::empty_in(bun_alloc::global_arena())); // OOM/capacity: Zig aborts; port keeps fire-and-forget self.graph.input_files.append(crate::Graph::InputFile { source: core::mem::take(source), @@ -3726,7 +3726,7 @@ pub mod bv2_impl { side_effects: bun_ast::SideEffects::HasSideEffects, ..Default::default() })?; - let _ = self.graph.ast.append(JSAst::empty()); // OOM/capacity: Zig aborts; port keeps fire-and-forget + let _ = self.graph.ast.append(JSAst::empty_in(bun_alloc::global_arena())); // OOM/capacity: Zig aborts; port keeps fire-and-forget // PORT NOTE: `bun.new(ServerComponentParseTask, …)` — heap-owned by the // worker pool; freed via `bun.destroy` in `on_complete` after the @@ -4753,7 +4753,7 @@ pub mod bv2_impl { Index::init(u32::try_from(this.graph.ast.len()).expect("int cast")); unsafe { *value_ptr = source_index.get() }; out_source_index = Some(source_index); - let _ = this.graph.ast.append(JSAst::empty()); // OOM/capacity: Zig aborts; port keeps fire-and-forget + let _ = this.graph.ast.append(JSAst::empty_in(bun_alloc::global_arena())); // OOM/capacity: Zig aborts; port keeps fire-and-forget let loader = path .loader(&this.transpiler.options.loaders) .unwrap_or(Loader::File); @@ -5887,8 +5887,8 @@ pub mod bv2_impl { == Index::BAKE_CLIENT_DATA.get() ); - let _ = self.graph.ast.append(JSAst::empty()); // PERF(port): was assume_capacity - let _ = self.graph.ast.append(JSAst::empty()); // PERF(port): was assume_capacity + let _ = self.graph.ast.append(JSAst::empty_in(bun_alloc::global_arena())); // PERF(port): was assume_capacity + let _ = self.graph.ast.append(JSAst::empty_in(bun_alloc::global_arena())); // PERF(port): was assume_capacity Ok(()) } @@ -6759,7 +6759,7 @@ pub mod bv2_impl { .input_files .append(new_input_file) .expect("unreachable"); - let _ = self.graph.ast.append(JSAst::empty()); // OOM/capacity: Zig aborts; port keeps fire-and-forget + let _ = self.graph.ast.append(JSAst::empty_in(bun_alloc::global_arena())); // OOM/capacity: Zig aborts; port keeps fire-and-forget if is_html_entrypoint { self.ensure_client_transpiler(); @@ -7257,7 +7257,7 @@ pub mod bv2_impl { this.graph.ast.set( result_source_index, - core::mem::replace(&mut result.ast, JSAst::empty()), + core::mem::replace(&mut result.ast, JSAst::empty_in(bun_alloc::global_arena())), ); // Barrel optimization: eagerly record import requests and diff --git a/src/js_printer/lib.rs b/src/js_printer/lib.rs index 24140ce4e0f..3057bd955d1 100644 --- a/src/js_printer/lib.rs +++ b/src/js_printer/lib.rs @@ -8172,7 +8172,7 @@ pub fn print_json( // constructs the same empty inputs without round-tripping through `Ast`. let bump = bun_alloc::Arena::new(); let mut no_op = - rename::NoOpRenamer::init(js_ast::symbol::Map::init_list(vec![Vec::new_in(bun_alloc::global_arena())]), source); + rename::NoOpRenamer::init(js_ast::symbol::Map::init_list(vec![Vec::new()]), source); let full_opts = Options { indent: opts.indent, From b14f5eeee5443700984d30ece9321d89bf19b742 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 18 May 2026 06:01:48 +0000 Subject: [PATCH 07/32] bundler: thread Graph<'a>/LinkerGraph<'a>/JSAst<'a>, borrow Graph.heap (WIP: ~60 errors) --- src/bun_alloc/lib.rs | 9 --- src/bundler/AstBuilder.rs | 8 +-- src/bundler/Chunk.rs | 6 +- src/bundler/Graph.rs | 14 ++--- src/bundler/HTMLImportManifest.rs | 4 +- src/bundler/LinkerContext.rs | 16 ++--- src/bundler/LinkerGraph.rs | 28 ++++----- src/bundler/ParseTask.rs | 16 ++--- src/bundler/bundle_v2.rs | 62 +++++++++---------- src/bundler/cache.rs | 4 +- src/bundler/linker.rs | 6 +- .../linker_context/StaticRouteVisitor.rs | 4 +- .../linker_context/convertStmtsForChunk.rs | 4 +- .../convertStmtsForChunkForDevServer.rs | 4 +- .../linker_context/scanImportsAndExports.rs | 42 ++++++------- src/bundler/transpiler.rs | 47 +++++++------- src/bundler/ungate_support.rs | 4 +- 17 files changed, 134 insertions(+), 144 deletions(-) diff --git a/src/bun_alloc/lib.rs b/src/bun_alloc/lib.rs index 9d48d890245..338bc606ee4 100644 --- a/src/bun_alloc/lib.rs +++ b/src/bun_alloc/lib.rs @@ -270,15 +270,6 @@ pub type Bump = bumpalo::Bump; pub type ArenaVec<'a, T> = Vec; pub use mimalloc_arena::{ArenaString, ArenaVecExt, live_arena_heaps, vec_from_iter_in}; -/// Process-wide [`MimallocArena`] over `mi_heap_main()` — thread-safe to -/// allocate from on any thread, never resets. Use as the allocator for -/// placeholder/sentinel [`ArenaVec`]s (`Vec::new_in(global_arena())` is -/// alloc-free) and for `ArenaVec`s that outlive any per-task arena (linker -/// graph clones). Allocations behave identically to the global allocator. -pub fn global_arena() -> &'static MimallocArena { - static ARENA: std::sync::OnceLock = std::sync::OnceLock::new(); - ARENA.get_or_init(MimallocArena::borrowing_default) -} /// `bumpalo::format!` parity — `arena_format!(in arena, "...", ..)` → /// [`ArenaString`]. diff --git a/src/bundler/AstBuilder.rs b/src/bundler/AstBuilder.rs index 4225d719980..01f5aefb90a 100644 --- a/src/bundler/AstBuilder.rs +++ b/src/bundler/AstBuilder.rs @@ -300,7 +300,7 @@ impl<'a, 'bump> AstBuilder<'a, 'bump> { debug_assert!(self.scopes.is_empty()); let module_scope = self.current_scope; - let mut parts = PartList::init_capacity(2); + let mut parts = Vec::with_capacity_in(2, self.arena); // PORT NOTE: Zig grew len then wrote `parts.mut(i).* = ...`, which is a // bitwise store on the SoA slot. In Rust `*parts.mut_(i) = ...` first // *drops* the (uninitialized) prior `Part` — and `Part` carries Drop @@ -591,13 +591,11 @@ impl<'a, 'bump> AstBuilder<'a, 'bump> { Ok(crate::BundledAst { parts, module_scope: module_scope_value, - symbols: symbol::List::move_from_list(core::mem::take(&mut self.symbols)), + symbols: bun_alloc::vec_from_iter_in(core::mem::take(&mut self.symbols).into_iter(), self.arena), exports_ref: Ref::NONE, wrapper_ref: Ref::NONE, module_ref: self.module_ref, - import_records: import_record::List::move_from_list(core::mem::take( - &mut self.import_records, - )), + import_records: bun_alloc::vec_from_iter_in(core::mem::take(&mut self.import_records).into_iter(), self.arena), export_star_import_records: Box::default(), approximate_newline_count: 1, exports_kind: ExportsKind::Esm, diff --git a/src/bundler/Chunk.rs b/src/bundler/Chunk.rs index f9bca21a948..fe81cca1bc7 100644 --- a/src/bundler/Chunk.rs +++ b/src/bundler/Chunk.rs @@ -543,7 +543,7 @@ impl IntermediateOutput { &mut self, allocator_to_use: Option<&DynAlloc>, parse_graph: &Graph, - linker_graph: &LinkerGraph, + linker_graph: &LinkerGraph<'_>, import_prefix: &[u8], // PORT NOTE: Zig passed `*Chunk` / `[]Chunk` (freely aliased — `chunk` // is `&chunks[i]`). The body only reads both, so take `&` to avoid @@ -594,7 +594,7 @@ impl IntermediateOutput { &mut self, allocator_to_use: Option<&DynAlloc>, parse_graph: &Graph, - linker_graph: &LinkerGraph, + linker_graph: &LinkerGraph<'_>, import_prefix: &[u8], // See `code()` PORT NOTE — `chunk` aliases `chunks[i]`; body is read-only. chunk: &Chunk, @@ -639,7 +639,7 @@ impl IntermediateOutput { &mut self, allocator_to_use: Option<&DynAlloc>, graph: &Graph, - linker_graph: &LinkerGraph, + linker_graph: &LinkerGraph<'_>, import_prefix: &[u8], // See `code()` PORT NOTE — `chunk` aliases `chunks[i]`; body is read-only. chunk: &Chunk, diff --git a/src/bundler/Graph.rs b/src/bundler/Graph.rs index 7ce9ab03730..2c6112bf0e5 100644 --- a/src/bundler/Graph.rs +++ b/src/bundler/Graph.rs @@ -17,7 +17,7 @@ use bun_ast::Ref; // `bun.ast.Index.Int` — the underlying integer repr of `Index`. pub(crate) use crate::IndexInt; -pub struct Graph { +pub struct Graph<'a> { // TODO(port): lifetime — no direct LIFETIMES.tsv row for Graph.pool, but row 170 // (ThreadPool.v2, BACKREF) evidence states "BundleV2.graph.pool owns ThreadPool". // bundle_v2.zig:992 allocates it from `this.arena()` (the `self.heap` arena) and @@ -26,7 +26,7 @@ pub struct Graph { // safe — the BACKREF invariant (pointee outlives holder) holds for the entire // bundle pass. pub pool: bun_ptr::BackRef, - pub heap: ThreadLocalArena, + pub heap: &'a ThreadLocalArena, /// Mapping user-specified entry points to their Source Index // PERF(port): Zig fed this ArrayList from `self.heap` (self-referential arena). @@ -39,7 +39,7 @@ pub struct Graph { /// When a parse is in progress / queued, it is `Ast.empty` // PORT NOTE: BundledAst<'arena> borrows from self.heap (sibling-field self-ref); // 'static here is a placeholder. TODO(refactor): thread the lifetime via raw ptr or Ouroboros. - pub ast: MultiArrayList>, + pub ast: MultiArrayList>, /// During the scan + parse phase, this value keeps a count of the remaining /// tasks. Once it hits zero, the scan phase ends and linking begins. Note @@ -140,13 +140,13 @@ bitflags::bitflags! { } } -impl Default for Graph { - fn default() -> Self { +impl<'a> Graph<'a> { + pub fn new(heap: &'a ThreadLocalArena) -> Self { Self { // Self-referential arena pointer; real value wired in // `BundleV2::init` before any use (Graph.zig has `= undefined`). pool: bun_ptr::BackRef::from(NonNull::::dangling()), - heap: ThreadLocalArena::new(), + heap, entry_points: Vec::new(), entry_point_original_names: IndexStringMap::default(), input_files: MultiArrayList::default(), @@ -166,7 +166,7 @@ impl Default for Graph { } } -impl Graph { +impl<'a> Graph<'a> { /// Shared borrow of the bundler `ThreadPool`. /// /// `pool` is arena-allocated in `BundleV2::init` (bundle_v2.zig:992) and diff --git a/src/bundler/HTMLImportManifest.rs b/src/bundler/HTMLImportManifest.rs index cab554f3fcc..cf112b823dd 100644 --- a/src/bundler/HTMLImportManifest.rs +++ b/src/bundler/HTMLImportManifest.rs @@ -134,7 +134,7 @@ fn write_entry_item( pub fn write_escaped_json( index: u32, graph: &Graph, - linker_graph: &LinkerGraph, + linker_graph: &LinkerGraph<'_>, chunks: &[Chunk], writer: &mut W, ) -> Result<(), bun_core::Error> { @@ -178,7 +178,7 @@ impl<'a> HTMLImportManifest<'a> { pub fn write( index: u32, graph: &Graph, - linker_graph: &LinkerGraph, + linker_graph: &LinkerGraph<'_>, chunks: &[Chunk], writer: &mut W, ) -> Result<(), bun_core::Error> { diff --git a/src/bundler/LinkerContext.rs b/src/bundler/LinkerContext.rs index 8ab9686bf08..dd54c30270e 100644 --- a/src/bundler/LinkerContext.rs +++ b/src/bundler/LinkerContext.rs @@ -176,8 +176,8 @@ pub use crate::DeferredBatchTask::DeferredBatchTask; pub use crate::ParseTask; pub struct LinkerContext<'a> { - pub parse_graph: *mut Graph, - pub graph: LinkerGraph, + pub parse_graph: *mut Graph<'a>, + pub graph: LinkerGraph<'a>, /// Backref into `Transpiler.log`, assigned in [`Self::load`]. Stored as a /// raw pointer (like `parse_graph` / `resolver`) so `Default` can be /// `null_mut()` instead of a dangling `&mut` (instant UB). Use @@ -488,7 +488,7 @@ impl<'a> LinkerContext<'a> { let stmts: &[Stmt] = part.stmts.slice(); if stmts.len() == 1 { if let Some(s_import) = stmts[0].data.s_import() { - let record = self.graph.ast.items_import_records()[source_index as usize] + let record = &self.graph.ast.items_import_records()[source_index as usize] [s_import.import_record_index as usize]; if record.source_index.is_valid() && self.graph.meta.items_flags()[record.source_index.get() as usize].wrap @@ -705,7 +705,7 @@ impl<'a> LinkerContext<'a> { // PORT NOTE: reshaped for borrowck — Zig held overlapping `&`/`&mut` // into `parse_graph.html_imports` and `parse_graph.input_files`; here // we go through raw pointers and reborrow per use. - let parse_graph: *mut Graph = self.parse_graph; + let parse_graph: *mut Graph<'a> = self.parse_graph; // SAFETY: see above; sole accessor of `html_imports` for this scope. let server_len = unsafe { (*parse_graph).html_imports.server_source_indices.len() }; if server_len > 0 { @@ -814,7 +814,7 @@ impl<'a> LinkerContext<'a> { // reallocate inside `validate_tla`; we cache raw column pointers // and reborrow per call to satisfy borrowck (`&mut self` is held // across the recursion). - let parse_graph: *mut Graph = self.parse_graph; + let parse_graph: *mut Graph<'a> = self.parse_graph; let import_records_list: *const [Vec] = self.graph.ast.items_import_records(); let flags: *mut [crate::ungate_support::js_meta::Flags] = @@ -2040,9 +2040,9 @@ impl<'a> LinkerContext<'a> { namespace_ref: Ref, import_record_index: u32, alloc: &Bump, - ast: &JSAst, + ast: &JSAst<'_>, ) -> Result { - let record = ast.import_records[import_record_index as usize]; + let record = &ast.import_records[import_record_index as usize]; // Barrel optimization: deferred import records should be dropped if record.flags.contains(bun_ast::ImportRecordFlags::IS_UNUSED) { return Ok(true); @@ -2176,7 +2176,7 @@ impl<'a> LinkerContext<'a> { alloc: &Bump, writer: &mut js_printer::BufferWriter, out_stmts: &mut [Stmt], - ast: &JSAst, + ast: &JSAst<'_>, flags: crate::ungate_support::js_meta::Flags, to_esm_ref: Ref, to_commonjs_ref: Ref, diff --git a/src/bundler/LinkerGraph.rs b/src/bundler/LinkerGraph.rs index 5a4ed9830c6..0a49db6ab40 100644 --- a/src/bundler/LinkerGraph.rs +++ b/src/bundler/LinkerGraph.rs @@ -29,7 +29,7 @@ bun_core::declare_scope!(LinkerGraph, visible); // were drafted against an older `.items().field_name` shape; rewritten to the // `items_()` spelling (matches `LinkerContext.rs`). -pub struct LinkerGraph { +pub struct LinkerGraph<'a> { pub files: FileList, pub files_live: BitSet, pub entry_points: entry_point::List, @@ -47,7 +47,7 @@ pub struct LinkerGraph { // This is an alias from Graph // it is not a clone! - pub ast: MultiArrayList, + pub ast: MultiArrayList>, pub meta: MultiArrayList, /// We should avoid traversing all files in the bundle, because the linker @@ -93,10 +93,10 @@ pub struct LinkerGraph { // `Send` is required because `LinkerGraph` is moved into `LinkerContext` // which is itself sent to the link task; the only `!Send` constituent is the // raw `*const Arena`, whose pointee is `Sync` and outlives the graph. -unsafe impl Send for LinkerGraph {} -unsafe impl Sync for LinkerGraph {} +unsafe impl Send for LinkerGraph<'_> {} +unsafe impl Sync for LinkerGraph<'_> {} -impl LinkerGraph { +impl<'a> LinkerGraph<'a> { /// `&Arena` accessor — `bump` is a raw backref into `BundleV2`. #[inline] pub fn arena(&self) -> &Arena { @@ -106,7 +106,7 @@ impl LinkerGraph { } } -impl LinkerGraph { +impl<'a> LinkerGraph<'a> { pub fn init(bump: &Arena, file_count: usize) -> Result { // TODO(port): narrow error set Ok(LinkerGraph { @@ -126,7 +126,7 @@ impl LinkerGraph { } } -impl Default for LinkerGraph { +impl Default for LinkerGraph<'_> { fn default() -> Self { LinkerGraph { files: FileList::default(), @@ -213,7 +213,7 @@ pub fn top_level_symbol_to_parts<'a>( } pub fn add_part_to_file( - parts: &mut [part::List], + parts: &mut [part::List<'_>], top_level_symbol_to_parts_overlay: &mut [TopLevelSymbolToParts], top_level_symbols_to_parts: &[bundled_ast::TopLevelSymbolToParts], id: u32, @@ -271,7 +271,7 @@ pub fn add_part_to_file( #[allow(clippy::too_many_arguments)] pub fn generate_symbol_import_and_use( - parts: &mut [part::List], + parts: &mut [part::List<'_>], ast_flags: &mut [bundled_ast::Flags], exports_ref: &[Ref], module_ref: &[Ref], @@ -359,7 +359,7 @@ pub fn generate_symbol_import_and_use( Ok(()) } -impl LinkerGraph { +impl<'a> LinkerGraph<'a> { pub fn runtime_function(&self, name: &[u8]) -> Ref { runtime_function(self.ast.items_named_exports(), name) } @@ -489,7 +489,7 @@ impl LinkerGraph { } } -impl LinkerGraph { +impl<'a> LinkerGraph<'a> { pub fn load( &mut self, entry_points: &[Index], @@ -626,7 +626,7 @@ impl LinkerGraph { } // For client components, the import record index currently points to the original source index, instead of the reference source index. - let import_records_list: &mut [import_record::List] = + let import_records_list: &mut [import_record::List<'_>] = self.ast.items_import_records_mut(); for source_id in self.reachable_files.slice() { for import_record in import_records_list[source_id.get() as usize] @@ -686,7 +686,7 @@ impl LinkerGraph { let mut symbols: symbol::NestedList = Vec::with_capacity(src_symbols.len()); for src in src_symbols { let n = src.len(); - let mut dest: symbol::List = Vec::with_capacity(n); + let mut dest: Vec = Vec::with_capacity(n); // SAFETY: `dest` has capacity `n`; `src` is `n` initialized // `Symbol`s; `Symbol` is bitwise-copyable (no `Drop`). unsafe { @@ -782,7 +782,7 @@ impl LinkerGraph { // TODO(port): narrow error set struct State<'a> { visited: AutoBitSet, - import_records: &'a [import_record::List], + import_records: &'a [import_record::List<'a>], flags: &'a mut [js_meta::Flags], } diff --git a/src/bundler/ParseTask.rs b/src/bundler/ParseTask.rs index 21fc8fa5a01..a9c53722506 100644 --- a/src/bundler/ParseTask.rs +++ b/src/bundler/ParseTask.rs @@ -164,7 +164,7 @@ impl WatcherData { } pub struct Success { - pub ast: JSAst, + pub ast: JSAst<'static>, pub source: Source, pub log: Log, pub use_directive: UseDirective, @@ -615,7 +615,7 @@ pub mod parse_worker { opts: ParserOptions, bump: &'static Bump, source: &Source, - ) -> core::result::Result { + ) -> core::result::Result, AnyError> { let root = Expr::init(E::Object::default(), Loc { start: 0 }); // SAFETY: `transpiler` is a live worker-owned `*mut Transpiler`; `options` // is disjoint from any other field the caller may hold a pointer to. @@ -635,7 +635,7 @@ pub mod parse_worker { opts: ParserOptions, bump: &'static Bump, source: &Source, - ) -> core::result::Result { + ) -> core::result::Result, AnyError> { let root = Expr::init(RootType::default(), Loc::EMPTY); // SAFETY: see `get_empty_css_ast` — disjoint field of a live `*mut Transpiler`. let define = unsafe { &mut (*transpiler).options.define }; @@ -663,7 +663,7 @@ pub mod parse_worker { // populated symbol table (.zig:613). // ─────────────────────────────────────────────────────────────────────────── - fn css_symbols_to_parser_symbols(src: Vec) -> bun_ast::symbol::List { + fn css_symbols_to_parser_symbols(src: Vec) -> bun_ast::symbol::List<'static> { use bun_ast::symbol::{Kind as PKind, Symbol as PSym}; let mut out = Vec::::init_capacity(src.len() as usize); for s in src.slice() { @@ -732,7 +732,7 @@ pub mod parse_worker { unique_key_prefix: u64, unique_key_for_additional_file: &mut FileLoaderHash, has_any_css_locals: &AtomicU32, - ) -> core::result::Result { + ) -> core::result::Result, AnyError> { use core::fmt::Write as _; // SAFETY: `transpiler` is a live worker-owned `*mut Transpiler`. @@ -811,7 +811,7 @@ pub mod parse_worker { // scopeguard would alias `log`/`temp_log` (both borrowed mutably // below); reshape as a closure so every `?` exits through one // post-amble that flushes `temp_log`. - let result = (|| -> core::result::Result { + let result = (|| -> core::result::Result, AnyError> { let root: Expr = bun_parsers::toml::TOML::parse(source, &mut temp_log, bump, false)?.into(); Ok(JSAst::init( @@ -833,7 +833,7 @@ pub mod parse_worker { Loader::Yaml => { let _trace = perf::trace("Bundler.ParseYAML"); let mut temp_log = Log::init(); - let result = (|| -> core::result::Result { + let result = (|| -> core::result::Result, AnyError> { let root: Expr = bun_parsers::yaml::YAML::parse(source, &mut temp_log, bump)?.into(); Ok(JSAst::init( @@ -855,7 +855,7 @@ pub mod parse_worker { Loader::Json5 => { let _trace = perf::trace("Bundler.ParseJSON5"); let mut temp_log = Log::init(); - let result = (|| -> core::result::Result { + let result = (|| -> core::result::Result, AnyError> { let root: Expr = bun_parsers::json5::JSON5Parser::parse(source, &mut temp_log, bump)?.into(); Ok(JSAst::init( diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index a27eb00dab1..4ecd4a8bbbe 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -100,7 +100,7 @@ pub struct BundleV2<'a> { pub ssr_transpiler: *mut Transpiler<'a>, /// When Bun Bake is used, the resolved framework is passed here. pub framework: Option, - pub graph: Graph, + pub graph: Graph<'a>, // `LinkerContext<'a>` borrows the same arena lifetime as `transpiler` // (Zig stored both as raw pointers into the bundler heap). pub linker: LinkerContext<'a>, @@ -1758,7 +1758,7 @@ pub mod bv2_impl { pub struct ReachableFileVisitor<'a> { pub reachable: Vec, pub visited: DynamicBitSet, - pub all_import_records: &'a mut [import_record::List], + pub all_import_records: &'a mut [import_record::List<'a>], pub all_loaders: &'a [Loader], pub all_urls_for_css: &'a [&'a [u8]], pub redirects: &'a [u32], @@ -1835,7 +1835,7 @@ pub mod bv2_impl { for ir_idx in 0..import_records_len { let import_record = &mut self.all_import_records [import_record_list_id.get() as usize] - .slice_mut()[ir_idx]; + .as_mut_slice()[ir_idx]; let mut other_source = import_record.source_index; if other_source.is_valid() { let mut redirect_count: usize = 0; @@ -1856,7 +1856,7 @@ pub mod bv2_impl { }; let import_record = &mut self.all_import_records [import_record_list_id.get() as usize] - .slice_mut()[ir_idx]; + .as_mut_slice()[ir_idx]; import_record.source_index = other_src_idx; import_record.path = other_path; other_source = other_src_idx; @@ -1876,7 +1876,7 @@ pub mod bv2_impl { let import_record = &self.all_import_records [import_record_list_id.get() as usize] - .slice()[ir_idx]; + .as_slice()[ir_idx]; // Mark if the file is imported by JS and its URL is inlined for CSS let is_inlined = import_record.source_index.is_valid() && !self.all_urls_for_css[import_record.source_index.get() as usize] @@ -1904,7 +1904,7 @@ pub mod bv2_impl { { let redirect_source_index = self.all_import_records [source_index.get() as usize] - .slice()[redirect_id as usize] + .as_slice()[redirect_id as usize] .source_index .get(); self.visit::( @@ -1989,7 +1989,7 @@ pub mod bv2_impl { // `split_mut()` on the local can coexist with the shared borrows // below. The slab does not resize for the duration of this function. let mut ast_slice = self.graph.ast.slice(); - let all_import_records: &mut [import_record::List] = + let all_import_records: &mut [import_record::List<'_>] = ast_slice.split_mut().import_records; let all_urls_for_css = self.graph.ast.items_url_for_css(); @@ -2162,7 +2162,7 @@ pub mod bv2_impl { // borrowing `self.graph.ast`; read the per-target map through the // disjoint `build_graphs` field instead of the `&mut self` accessor. let mut ast_slice = self.graph.ast.slice(); - let ast_import_records: &mut [import_record::List] = + let ast_import_records: &mut [import_record::List<'_>] = ast_slice.split_mut().import_records; let targets = self.graph.ast.items_target(); let max_valid_source_index = Index::init(self.graph.input_files.len()); @@ -2604,7 +2604,7 @@ pub mod bv2_impl { self.path_to_source_index_map(target) .put(path_slice, source_index.get()) .expect("oom"); - let _ = self.graph.ast.append(JSAst::empty_in(bun_alloc::global_arena())); // OOM/capacity: Zig aborts; port keeps fire-and-forget + let _ = self.graph.ast.append(JSAst::empty_in(self.graph.heap)); // OOM/capacity: Zig aborts; port keeps fire-and-forget self.graph.input_files.append(crate::Graph::InputFile { source: bun_ast::Source { @@ -2720,7 +2720,7 @@ pub mod bv2_impl { self.path_to_source_index_map(target) .put(&path.text, source_index.get()) .expect("oom"); - let _ = self.graph.ast.append(JSAst::empty_in(bun_alloc::global_arena())); // OOM/capacity: Zig aborts; port keeps fire-and-forget + let _ = self.graph.ast.append(JSAst::empty_in(self.graph.heap)); // OOM/capacity: Zig aborts; port keeps fire-and-forget let side_effects = result.primary_side_effects_data; self.graph.input_files.append(crate::Graph::InputFile { @@ -2787,7 +2787,7 @@ pub mod bv2_impl { // here into `ThreadPool::init`, which stores it as `*mut`. Creating a // `&mut` along the way would violate Stacked Borrows. thread_pool: Option>, - heap: ThreadLocalArena, + heap: &'a ThreadLocalArena, ) -> Result>, Error> { // TODO(port): arena-allocate self via bump.alloc — Box::new is wrong arena (Zig: arena.create(@This()) on arena) transpiler.env().load_tracy(); @@ -2810,7 +2810,7 @@ pub mod bv2_impl { heap, kit_referenced_server_data: false, kit_referenced_client_data: false, - ..Default::default() + ..Graph::new(heap) }, linker: LinkerContext { r#loop: event_loop, @@ -2864,7 +2864,7 @@ pub mod bv2_impl { // Rust `Transpiler<'a>`/`Resolver<'a>` store `&'a Arena` and `Log.msgs` // is a `Vec` (global alloc), so only `linker.graph.bump` needs the // backref into the now-stable `this.graph.heap` slot. - this.linker.graph.bump = bun_ptr::BackRef::new(&this.graph.heap); + this.linker.graph.bump = bun_ptr::BackRef::new(this.graph.heap); this.transpiler.log_mut().clone_line_text = true; // We don't expose an option to disable this. Bake forbids tree-shaking @@ -3240,7 +3240,7 @@ pub mod bv2_impl { })?; // try this.graph.entry_points.append(arena, Index.runtime); - let _ = self.graph.ast.append(JSAst::empty_in(bun_alloc::global_arena())); // OOM/capacity: Zig aborts; port keeps fire-and-forget + let _ = self.graph.ast.append(JSAst::empty_in(self.graph.heap)); // OOM/capacity: Zig aborts; port keeps fire-and-forget self.path_to_source_index_map(self.transpiler.options.target) .put(&b"bun:wrap"[..], Index::RUNTIME.get()) .expect("oom"); @@ -3557,7 +3557,7 @@ pub mod bv2_impl { known_target: options::Target, ) -> Result { let source_index = Index::init(u32::try_from(self.graph.ast.len()).expect("int cast")); - let _ = self.graph.ast.append(JSAst::empty_in(bun_alloc::global_arena())); // OOM/capacity: Zig aborts; port keeps fire-and-forget + let _ = self.graph.ast.append(JSAst::empty_in(self.graph.heap)); // OOM/capacity: Zig aborts; port keeps fire-and-forget self.graph.input_files.append(crate::Graph::InputFile { source: core::mem::take(source), @@ -3611,7 +3611,7 @@ pub mod bv2_impl { known_target: options::Target, ) -> Result { let source_index = Index::init(u32::try_from(self.graph.ast.len()).expect("int cast")); - let _ = self.graph.ast.append(JSAst::empty_in(bun_alloc::global_arena())); // OOM/capacity: Zig aborts; port keeps fire-and-forget + let _ = self.graph.ast.append(JSAst::empty_in(self.graph.heap)); // OOM/capacity: Zig aborts; port keeps fire-and-forget self.graph.input_files.append(crate::Graph::InputFile { source: core::mem::take(source), @@ -3726,7 +3726,7 @@ pub mod bv2_impl { side_effects: bun_ast::SideEffects::HasSideEffects, ..Default::default() })?; - let _ = self.graph.ast.append(JSAst::empty_in(bun_alloc::global_arena())); // OOM/capacity: Zig aborts; port keeps fire-and-forget + let _ = self.graph.ast.append(JSAst::empty_in(self.graph.heap)); // OOM/capacity: Zig aborts; port keeps fire-and-forget // PORT NOTE: `bun.new(ServerComponentParseTask, …)` — heap-owned by the // worker pool; freed via `bun.destroy` in `on_complete` after the @@ -3853,7 +3853,7 @@ pub mod bv2_impl { #[cold] pub fn generate_from_cli( transpiler: &'a mut Transpiler<'a>, - alloc: &bun_alloc::Arena, + alloc: &'a bun_alloc::Arena, event_loop: EventLoop, enable_reloading: bool, reachable_files_count: &mut usize, @@ -3868,7 +3868,7 @@ pub mod bv2_impl { event_loop, enable_reloading, None, - ThreadLocalArena::new(), + alloc, )?; this.unique_key = generate_unique_key(); @@ -4009,7 +4009,7 @@ pub mod bv2_impl { #[cold] pub fn scan_module_graph_from_cli( transpiler: &'a mut Transpiler<'a>, - alloc: &bun_alloc::Arena, + alloc: &'a bun_alloc::Arena, event_loop: EventLoop, entry_points: &[&[u8]], ) -> Result>, Error> { @@ -4020,7 +4020,7 @@ pub mod bv2_impl { event_loop, false, None, - ThreadLocalArena::new(), + alloc, )?; this.unique_key = generate_unique_key(); @@ -4049,7 +4049,7 @@ pub mod bv2_impl { entry_points: &bake_types::production::EntryPointMap, server_transpiler: &'a mut Transpiler<'a>, bake_options: BakeOptions<'a>, - alloc: &bun_alloc::Arena, + alloc: &'a bun_alloc::Arena, event_loop: EventLoop, ) -> Result, Error> { let mut this = BundleV2::init( @@ -4059,7 +4059,7 @@ pub mod bv2_impl { event_loop, false, None, - ThreadLocalArena::new(), + alloc, )?; this.unique_key = generate_unique_key(); @@ -4753,7 +4753,7 @@ pub mod bv2_impl { Index::init(u32::try_from(this.graph.ast.len()).expect("int cast")); unsafe { *value_ptr = source_index.get() }; out_source_index = Some(source_index); - let _ = this.graph.ast.append(JSAst::empty_in(bun_alloc::global_arena())); // OOM/capacity: Zig aborts; port keeps fire-and-forget + let _ = this.graph.ast.append(JSAst::empty_in(this.graph.heap)); // OOM/capacity: Zig aborts; port keeps fire-and-forget let loader = path .loader(&this.transpiler.options.loaders) .unwrap_or(Loader::File); @@ -5887,8 +5887,8 @@ pub mod bv2_impl { == Index::BAKE_CLIENT_DATA.get() ); - let _ = self.graph.ast.append(JSAst::empty_in(bun_alloc::global_arena())); // PERF(port): was assume_capacity - let _ = self.graph.ast.append(JSAst::empty_in(bun_alloc::global_arena())); // PERF(port): was assume_capacity + let _ = self.graph.ast.append(JSAst::empty_in(self.graph.heap)); // PERF(port): was assume_capacity + let _ = self.graph.ast.append(JSAst::empty_in(self.graph.heap)); // PERF(port): was assume_capacity Ok(()) } @@ -5965,7 +5965,7 @@ pub mod bv2_impl { } pub struct ResolveImportRecordCtx<'a> { - pub import_records: &'a mut import_record::List, + pub import_records: &'a mut import_record::List<'a>, pub source: &'a bun_ast::Source, pub loader: Loader, pub target: options::Target, @@ -5990,7 +5990,7 @@ pub mod bv2_impl { let loader = ctx.loader; let source_dir = source.path.source_dir(); let mut estimated_resolve_queue_count: usize = 0; - for import_record in ctx.import_records.slice_mut() { + for import_record in ctx.import_records.as_mut_slice() { if import_record .flags .contains(bun_ast::ImportRecordFlags::IS_INTERNAL) @@ -6026,7 +6026,7 @@ pub mod bv2_impl { let mut last_error: Option = None; - 'outer: for (i, import_record) in ctx.import_records.slice_mut().iter_mut().enumerate() + 'outer: for (i, import_record) in ctx.import_records.as_mut_slice().iter_mut().enumerate() { // Preserve original import specifier before resolution modifies path if import_record.original_path.is_empty() { @@ -6759,7 +6759,7 @@ pub mod bv2_impl { .input_files .append(new_input_file) .expect("unreachable"); - let _ = self.graph.ast.append(JSAst::empty_in(bun_alloc::global_arena())); // OOM/capacity: Zig aborts; port keeps fire-and-forget + let _ = self.graph.ast.append(JSAst::empty_in(self.graph.heap)); // OOM/capacity: Zig aborts; port keeps fire-and-forget if is_html_entrypoint { self.ensure_client_transpiler(); @@ -7257,7 +7257,7 @@ pub mod bv2_impl { this.graph.ast.set( result_source_index, - core::mem::replace(&mut result.ast, JSAst::empty_in(bun_alloc::global_arena())), + core::mem::replace(&mut result.ast, JSAst::empty_in(self.graph.heap)), ); // Barrel optimization: eagerly record import requests and diff --git a/src/bundler/cache.rs b/src/bundler/cache.rs index 72f90118c3c..7d9e3b077ee 100644 --- a/src/bundler/cache.rs +++ b/src/bundler/cache.rs @@ -481,7 +481,7 @@ impl Css { pub struct JavaScript {} -pub type JavaScriptResult = js_parser::Result; +pub type JavaScriptResult<'a> = js_parser::Result<'a>; impl JavaScript { pub fn init() -> JavaScript { @@ -499,7 +499,7 @@ impl JavaScript { defines: &'a Define, log: &mut bun_ast::Log, source: &'a bun_ast::Source, - ) -> Result, bun_core::Error> { + ) -> Result>, bun_core::Error> { let mut temp_log = bun_ast::Log::init(); temp_log.level = log.level; let mut parser = match js_parser::Parser::init(opts, &mut temp_log, source, defines, bump) { diff --git a/src/bundler/linker.rs b/src/bundler/linker.rs index dc3bb9005e6..ebdf5142c7e 100644 --- a/src/bundler/linker.rs +++ b/src/bundler/linker.rs @@ -412,7 +412,7 @@ impl Linker { // borrows (`&result.source` + `&mut result.ast.*`) where // needed, and hoist `is_pending_import` (which borrows the // whole `result`) before any `ast` mut borrow. - let len = result.ast.import_records.slice().len(); + let len = result.ast.import_records.as_slice().len(); for record_i in 0..len { let record_index = u32::try_from(record_i).expect("int cast"); @@ -422,7 +422,7 @@ impl Linker { // Field-split borrow: `source` ⟂ `ast`. let source = &result.source; let ast = &mut result.ast; - let import_record = &mut ast.import_records.slice_mut()[record_i]; + let import_record = &mut ast.import_records.as_mut_slice()[record_i]; if import_record.flags.contains(ImportRecordFlags::IS_UNUSED) || skip_deferred { continue; @@ -510,7 +510,7 @@ impl Linker { } if let Some(runner) = self.plugin_runner { - let import_record = &mut result.ast.import_records.slice_mut()[record_i]; + let import_record = &mut result.ast.import_records.as_mut_slice()[record_i]; if PluginRunner::could_be_plugin(import_record.path.text) { // SAFETY: `plugin_runner` is `Some` only when set // by the owning `Transpiler` to a live JSC-heap diff --git a/src/bundler/linker_context/StaticRouteVisitor.rs b/src/bundler/linker_context/StaticRouteVisitor.rs index 4ac81719540..e07dcc53c77 100644 --- a/src/bundler/linker_context/StaticRouteVisitor.rs +++ b/src/bundler/linker_context/StaticRouteVisitor.rs @@ -39,7 +39,7 @@ impl<'a> StaticRouteVisitor<'a> { // the `&mut self` call below. `parse_graph()` is the safe backref // accessor (one centralized `unsafe`, see `LinkerContext::parse_graph`). let parse_graph = self.c.parse_graph(); - let all_import_records: &[import_record::List] = parse_graph.ast.items_import_records(); + let all_import_records: &[import_record::List<'_>] = parse_graph.ast.items_import_records(); let referenced_source_indices: &[u32] = parse_graph .server_component_boundaries .list @@ -65,7 +65,7 @@ impl<'a> StaticRouteVisitor<'a> { /// static. fn has_transitive_use_client_impl( &mut self, - all_import_records: &[import_record::List], + all_import_records: &[import_record::List<'_>], referenced_source_indices: &[u32], use_directives: &[UseDirective], source_index: Index, diff --git a/src/bundler/linker_context/convertStmtsForChunk.rs b/src/bundler/linker_context/convertStmtsForChunk.rs index d3704693783..174bc8d0ab8 100644 --- a/src/bundler/linker_context/convertStmtsForChunk.rs +++ b/src/bundler/linker_context/convertStmtsForChunk.rs @@ -47,7 +47,7 @@ pub fn convert_stmts_for_chunk( chunk: &mut Chunk, bump: &Bump, wrap: WrapKind, - ast: &JSAst, + ast: &JSAst<'_>, ) -> Result<(), bun_core::Error> { let _ = bump; let should_extract_esm_stmts_for_wrap = wrap != WrapKind::None; @@ -145,7 +145,7 @@ pub fn convert_stmts_for_chunk( } // "export * from 'path'" - let record = ast.import_records[s.import_record_index as usize]; + let record = &ast.import_records[s.import_record_index as usize]; // Barrel optimization: deferred export * records should be dropped if record.flags.contains(ImportRecordFlags::IS_UNUSED) { diff --git a/src/bundler/linker_context/convertStmtsForChunkForDevServer.rs b/src/bundler/linker_context/convertStmtsForChunkForDevServer.rs index a571f1696d4..c88502e3d4c 100644 --- a/src/bundler/linker_context/convertStmtsForChunkForDevServer.rs +++ b/src/bundler/linker_context/convertStmtsForChunkForDevServer.rs @@ -47,7 +47,7 @@ pub fn convert_stmts_for_chunk_for_dev_server<'bump>( stmts: &mut StmtList, part_stmts: &[bun_ast::Stmt], bump: &'bump Bump, - ast: &mut JSAst, + ast: &mut JSAst<'_>, ) -> Result<(), AllocError> { // TODO(port): narrow error set let hmr_api_ref = ast.wrapper_ref; @@ -78,7 +78,7 @@ pub fn convert_stmts_for_chunk_for_dev_server<'bump>( for stmt in part_stmts { match &stmt.data { StmtData::SImport(st) => { - let record = ast.import_records[st.import_record_index as usize]; + let record = &mut ast.import_records[st.import_record_index as usize]; if record.path.is_disabled { continue; } diff --git a/src/bundler/linker_context/scanImportsAndExports.rs b/src/bundler/linker_context/scanImportsAndExports.rs index fd737a83bc6..42a55805e15 100644 --- a/src/bundler/linker_context/scanImportsAndExports.rs +++ b/src/bundler/linker_context/scanImportsAndExports.rs @@ -32,7 +32,7 @@ use bun_js_parser as js_ast; use crate::linker_context_mod::LinkerCtx; type AstFlags = bundled_ast::Flags; -type ImportRecordList = import_record::List; +type ImportRecordList<'a> = import_record::List<'a>; #[derive(thiserror::Error, Debug, strum::IntoStaticStr)] pub enum ScanImportsAndExportsError { @@ -105,7 +105,7 @@ pub fn scan_imports_and_exports( let input = this.parse_graph().input_files.split_raw(); use crate::bundled_ast::CssCol; - let import_records_list: *mut [ImportRecordList] = ast.import_records; + let import_records_list: *mut [ImportRecordList<'_>] = ast.import_records; let exports_kind: *mut [ExportsKind] = ast.exports_kind; let entry_point_kinds: *mut [EntryPoint::Kind] = files.entry_point_kind; let named_imports: *mut [NamedImports] = ast.named_imports; @@ -151,7 +151,7 @@ pub fn scan_imports_and_exports( // Inline URLs for non-CSS files into the CSS file let _ = LinkerContext::scan_css_imports( id as u32, - col_ref!(import_records_list)[id].slice(), + col_ref!(import_records_list)[id].as_slice(), css_asts, col_ref!(input_files), col_ref!(loaders), @@ -172,7 +172,7 @@ pub fn scan_imports_and_exports( continue; } - for record in col_ref!(import_records_list)[id].slice() { + for record in col_ref!(import_records_list)[id].as_slice() { if !record.source_index.is_valid() { continue; } @@ -347,7 +347,7 @@ pub fn scan_imports_and_exports( // `import_records` is a `&'a [_]` (Copy) field — copy it out so // the loop borrow does not overlap `&mut dependency_wrapper`. let import_records = dependency_wrapper.import_records; - for record in import_records[id].slice() { + for record in import_records[id].as_slice() { if record.source_index.is_valid() { let si = record.source_index.get(); if dependency_wrapper.exports_kind[si as usize] == ExportsKind::Cjs { @@ -877,7 +877,7 @@ pub fn scan_imports_and_exports( .import_record_indices .slice()[iri]; let (kind, rec_source_index, rec_flags) = { - let record = &col_ref!(import_records_list)[id].slice() + let record = &col_ref!(import_records_list)[id].as_slice() [import_record_index as usize]; (record.kind, record.source_index, record.flags) }; @@ -887,7 +887,7 @@ pub fn scan_imports_and_exports( // PORT NOTE: short-circuit — `is_external_dynamic_import` indexes by // `record.source_index`, so it must only run when that index is valid. let is_external_dyn = rec_source_index.is_valid() && { - let record = &col_ref!(import_records_list)[id].slice() + let record = &col_ref!(import_records_list)[id].as_slice() [import_record_index as usize]; this.is_external_dynamic_import(record, source_index) }; @@ -955,14 +955,14 @@ pub fn scan_imports_and_exports( == ExportsKind::Cjs { // Cross-chunk dynamic import to CJS - needs special handling in printer - col!(import_records_list)[id].slice_mut() + col!(import_records_list)[id].as_mut_slice() [import_record_index as usize] .flags .insert(ImportRecordFlags::WRAP_WITH_TO_ESM); to_esm_uses += 1; } else if kind != ImportKind::Dynamic { // Static imports to external CJS modules need __toESM wrapping - col!(import_records_list)[id].slice_mut() + col!(import_records_list)[id].as_mut_slice() [import_record_index as usize] .flags .insert(ImportRecordFlags::WRAP_WITH_TO_ESM); @@ -999,7 +999,7 @@ pub fn scan_imports_and_exports( && other_export_kind == ExportsKind::Cjs && output_format != Format::InternalBakeDev { - col!(import_records_list)[id].slice_mut()[import_record_index as usize] + col!(import_records_list)[id].as_mut_slice()[import_record_index as usize] .flags .insert(ImportRecordFlags::WRAP_WITH_TO_ESM); to_esm_uses += 1; @@ -1029,7 +1029,7 @@ pub fn scan_imports_and_exports( // and subtle set of transpiler interop issues. See for example // https://github.com/evanw/esbuild/issues/1591. if kind == ImportKind::Require { - col!(import_records_list)[id].slice_mut() + col!(import_records_list)[id].as_mut_slice() [import_record_index as usize] .flags .insert(ImportRecordFlags::WRAP_WITH_TO_COMMONJS); @@ -1060,7 +1060,7 @@ pub fn scan_imports_and_exports( for import_record_index in col_ref!(export_star_import_records)[id].iter() { let (rec_source_index,) = { - let record = &col_ref!(import_records_list)[id].slice() + let record = &col_ref!(import_records_list)[id].as_slice() [*import_record_index as usize]; (record.source_index,) }; @@ -1101,7 +1101,7 @@ pub fn scan_imports_and_exports( Index::source(source_index), )?; col!(ast_flags_list)[id].insert(AstFlags::USES_EXPORTS_REF); - col!(import_records_list)[id].slice_mut()[*import_record_index as usize] + col!(import_records_list)[id].as_mut_slice()[*import_record_index as usize] .flags .insert(ImportRecordFlags::CALLS_RUNTIME_RE_EXPORT_FN); re_export_uses += 1; @@ -1161,7 +1161,7 @@ fn should_call_runtime_require(format: options::Format) -> bool { struct DependencyWrapper<'a> { flags: &'a mut [js_meta::Flags], exports_kind: &'a mut [ExportsKind], - import_records: &'a [ImportRecordList], + import_records: &'a [ImportRecordList<'a>], export_star_map: HashMap, entry_point_kinds: &'a [EntryPoint::Kind], export_star_records: &'a [Box<[u32]>], @@ -1192,7 +1192,7 @@ impl DependencyWrapper<'_> { // having an export star from a file with dynamic exports. let kind = self.entry_point_kinds[source_index as usize]; let rec_source_index = - self.import_records[source_index as usize].slice()[*id as usize].source_index; + self.import_records[source_index as usize].as_slice()[*id as usize].source_index; if (rec_source_index.is_invalid() && (!kind.is_entry_point() || !self.output_format.keep_es6_import_export_syntax())) || (rec_source_index.is_valid() @@ -1232,7 +1232,7 @@ impl DependencyWrapper<'_> { // `import_records` is a `&'a [_]` (Copy) field — copy it out so the // recursive `&mut self` call does not overlap the iterator borrow. let records = self.import_records; - for record in records[source_index as usize].slice() { + for record in records[source_index as usize].as_slice() { if !record.source_index.is_valid() { continue; } @@ -1245,7 +1245,7 @@ impl DependencyWrapper<'_> { // ExportStarContext — port of the inner Zig struct. Holds raw column ptrs. // ────────────────────────────────────────────────────────────────────────── struct ExportStarContext { - import_records_list: *mut [ImportRecordList], + import_records_list: *mut [ImportRecordList<'_>], source_index_stack: Vec, exports_kind: *mut [ExportsKind], named_exports: *mut [NamedExports], @@ -1410,14 +1410,14 @@ mod __css_validation { this: &mut LinkerContext, id: usize, css_asts: *mut [CssCol], - import_records_list: *mut [ImportRecordList], + import_records_list: *mut [ImportRecordList<'_>], input_files: *mut [Source], ) { // `css_asts[id]` checked Some by caller. We only *read* the AST here; // `other_css_ast` below may alias the same allocation when a file // composes from itself, so bind as shared. let css_ast: &BundlerStyleSheet = col_ref!(css_asts)[id].as_deref().unwrap(); - let import_records: &[ImportRecord] = col_ref!(import_records_list)[id].slice(); + let import_records: &[ImportRecord] = col_ref!(import_records_list)[id].as_slice(); // Validate cross-file "composes: ... from" named imports for composes in css_ast.composes.values() { @@ -1494,7 +1494,7 @@ mod __css_validation { this: &mut LinkerContext, index: IndexInt, root_css_ast: &BundlerStyleSheet, - import_records_list: *mut [ImportRecordList], + import_records_list: *mut [ImportRecordList<'_>], all_css_asts: *mut [CssCol], ) { #[derive(Default)] @@ -1506,7 +1506,7 @@ mod __css_validation { struct Visitor<'a> { visited: ArrayHashMap, properties: StringArrayHashMap, - all_import_records: *mut [ImportRecordList], + all_import_records: *mut [ImportRecordList<'_>], all_css_asts: *mut [CssCol], all_symbols: &'a symbol::Map, all_sources: *mut [Source], diff --git a/src/bundler/transpiler.rs b/src/bundler/transpiler.rs index a64412a45a5..17569b0e1b8 100644 --- a/src/bundler/transpiler.rs +++ b/src/bundler/transpiler.rs @@ -841,7 +841,7 @@ pub enum AlreadyBundled { } impl Default for AlreadyBundled { - fn default() -> Self { + pub fn empty(arena: &'a bun_alloc::Arena) -> Self { AlreadyBundled::None } } @@ -873,10 +873,10 @@ impl AlreadyBundled { // PORT NOTE: lifetime-free — `runtime_transpiler_cache` is a raw pointer (Zig // `?*RuntimeTranspilerCache`) so `AsyncModule.parse_result` / `JSTranspiler` // can store this by value without threading a borrow lifetime. -pub struct ParseResult { +pub struct ParseResult<'a> { pub source: bun_ast::Source, pub loader: options::Loader, - pub ast: bun_ast::Ast, + pub ast: bun_ast::Ast<'a>, pub already_bundled: AlreadyBundled, pub input_fd: Option, pub empty: bool, @@ -906,12 +906,12 @@ pub struct ParseResult { pub source_contents_backing: resolver::cache::Contents, } -impl Default for ParseResult { +impl<'a> ParseResult<'a> { /// Spec transpiler.zig — `ParseResult` is value-copied (e.g. /// `AsyncModule.resumeLoadingModule` reads/writes `this.parse_result` by /// value). `Default` lets the Rust port `mem::take` it across that /// boundary; see `AsyncModule::resume_loading_module`. - fn default() -> Self { + pub fn empty(arena: &'a bun_alloc::Arena) -> Self { ParseResult { source: Default::default(), // PORT NOTE: `options::Loader` has no `Default`; Zig field had no @@ -919,7 +919,7 @@ impl Default for ParseResult { // (BundleEnums.rs:353), and `Default` here exists only for // `mem::take` in `AsyncModule::resume_loading_module`. loader: options::Loader::File, - ast: bun_ast::Ast::empty(), + ast: bun_ast::Ast::empty_in(arena), already_bundled: Default::default(), input_fd: None, empty: true, @@ -930,9 +930,10 @@ impl Default for ParseResult { } } -impl ParseResult { +impl<'a> ParseResult<'a> { #[inline] fn empty_with( + arena: &'a bun_alloc::Arena, source: bun_ast::Source, loader: options::Loader, input_fd: Option, @@ -941,7 +942,7 @@ impl ParseResult { ParseResult { source, loader, - ast: bun_ast::Ast::empty(), + ast: bun_ast::Ast::empty_in(arena), already_bundled: AlreadyBundled::None, input_fd, empty: true, @@ -1377,7 +1378,7 @@ impl<'a> Transpiler<'a> { &mut self, this_parse: ParseOptions<'_>, client_entry_point_: Option<&mut EntryPoints::ClientEntryPoint>, - ) -> Option { + ) -> Option> { self.parse_maybe_return_file_only::(this_parse, client_entry_point_) } @@ -1385,7 +1386,7 @@ impl<'a> Transpiler<'a> { &mut self, this_parse: ParseOptions<'_>, client_entry_point_: Option<&mut EntryPoints::ClientEntryPoint>, - ) -> Option { + ) -> Option> { self.parse_maybe_return_file_only_allow_shared_buffer::( this_parse, client_entry_point_, @@ -1403,7 +1404,7 @@ impl<'a> Transpiler<'a> { // callers pass a different type, introduce a `ClientEntryPointLike` // trait with `fn source() -> Option<&Source>`. client_entry_point_: Option<&mut EntryPoints::ClientEntryPoint>, - ) -> Option { + ) -> Option> { let arena = this_parse.arena; let dirname_fd = this_parse.dirname_fd; let file_descriptor = this_parse.file_descriptor; @@ -1758,7 +1759,7 @@ impl<'a> Transpiler<'a> { }, js_ast::Result::Cached => ParseResult { // TODO(port): Zig used `undefined` for ast here. - ast: bun_ast::Ast::empty(), + ast: bun_ast::Ast::empty_in(arena), runtime_transpiler_cache: rtc_ptr, source: source.clone(), loader, @@ -1770,7 +1771,7 @@ impl<'a> Transpiler<'a> { }, js_ast::Result::AlreadyBundled(already_bundled) => ParseResult { // TODO(port): Zig used `undefined` for ast here. - ast: bun_ast::Ast::empty(), + ast: bun_ast::Ast::empty_in(arena), already_bundled: match already_bundled { js_ast::AlreadyBundled::Bun => AlreadyBundled::SourceCode, js_ast::AlreadyBundled::BunCjs => AlreadyBundled::SourceCodeCjs, @@ -1906,7 +1907,7 @@ fn parse_data_loader( arena: &Arena, log: &mut bun_ast::Log, keep_json_and_toml_as_one_statement: bool, -) -> Option { +) -> Option> { // PERF(port): was `inline .toml, .yaml, .json, .jsonc, .json5 // => |kind|` — comptime monomorphization per loader; profile if it // shows up on a hot path. @@ -2129,7 +2130,7 @@ fn parse_data_loader( }]); } }; - let mut ast = bun_ast::Ast::from_parts(parts); + ast.symbols = bun_alloc::vec_from_iter_in(symbols.into_iter(), arena); ast.symbols = bun_ast::symbol::List::from_owned_slice(symbols.into_boxed_slice()); return Some(ParseResult { @@ -2153,7 +2154,7 @@ fn parse_text_loader( input_fd: Option, source_backing: resolver::cache::Contents, arena: &Arena, -) -> Option { +) -> Option> { let expr = bun_ast::Expr::init( bun_ast::E::EString::init(&source.contents), bun_ast::Loc::EMPTY, @@ -2176,7 +2177,7 @@ fn parse_text_loader( }]); return Some(ParseResult { - ast: bun_ast::Ast::from_parts(parts), + ast: bun_ast::Ast::from_parts(parts, arena), source: source.clone(), loader, input_fd, @@ -2197,7 +2198,7 @@ fn parse_md_loader( source_backing: resolver::cache::Contents, arena: &Arena, log: &mut bun_ast::Log, -) -> Option { +) -> Option> { let html: &'static [u8] = match bun_md::root::render_to_html(&source.contents) { // Spec transpiler.zig:1162 allocates the rendered HTML via // `arena` (the per-parse arena), so it is freed with the @@ -2234,7 +2235,7 @@ fn parse_md_loader( }]); return Some(ParseResult { - ast: bun_ast::Ast::from_parts(parts), + ast: bun_ast::Ast::from_parts(parts, arena), source: source.clone(), loader, input_fd, @@ -2256,7 +2257,7 @@ fn parse_wasm_loader( path: &bun_paths::fs::Path<'static>, target: options::Target, log: &mut bun_ast::Log, -) -> Option { +) -> Option> { if target.is_bun() { if !source.is_web_assembly() { let _ = log.add_error_fmt( @@ -2271,7 +2272,7 @@ fn parse_wasm_loader( } return Some(ParseResult { - ast: bun_ast::Ast::empty(), + ast: bun_ast::Ast::empty_in(arena), source: source.clone(), loader, input_fd, @@ -2375,7 +2376,7 @@ impl<'a> Transpiler<'a> { // take the column out (the printer never reads `tree.symbols`; it // walks `symbols` exclusively — `rg tree.symbols js_printer/lib.rs` is // empty). `init_with_one_list` boxes the single inner list. - // PERF(port): one extra alloc vs Zig's borrowed-slice — profile if hot. + let symbols = bun_ast::symbol::Map::init_with_one_list(core::mem::replace(&mut ast.symbols, Vec::new_in(arena)).into_iter().collect()); let symbols = bun_ast::symbol::Map::init_with_one_list(core::mem::take(&mut ast.symbols)); // `runtime_imports` is now forwarded — after Round-G `Ast.runtime_imports` @@ -3299,7 +3300,7 @@ pub struct BuildResolveResultPair { } impl Default for BuildResolveResultPair { - fn default() -> Self { + pub fn empty(arena: &'a bun_alloc::Arena) -> Self { Self { written: 0, input_fd: None, diff --git a/src/bundler/ungate_support.rs b/src/bundler/ungate_support.rs index e447dc22242..49f16004095 100644 --- a/src/bundler/ungate_support.rs +++ b/src/bundler/ungate_support.rs @@ -547,7 +547,7 @@ pub mod html_import_manifest { pub fn write_escaped_json( index: u32, graph: &Graph, - linker_graph: &LinkerGraph, + linker_graph: &LinkerGraph<'_>, chunks: &[Chunk], w: &mut &mut [u8], ) -> Result<(), core::fmt::Error> { @@ -605,7 +605,7 @@ pub use bun_js_printer::MangledProps; /// stored in a `MultiArrayList` SoA inside `LinkerGraph`/`Graph`, neither of /// which carries a lifetime parameter yet. Pin to `'static` until `'bump` /// is threaded through `Chunk`/`LinkerGraph`/`LinkerContext`. -pub type JSAst = crate::BundledAst<'static>; +pub type JSAst<'a> = crate::BundledAst<'a>; pub(crate) use bun_ast::{Part, Ref, Symbol}; /// `bundle_v2.zig:EntryPoint` — both a struct and (via the sibling module From 3095c9123090f8e5eea700b5dfae3ae482561feb Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 18 May 2026 06:16:55 +0000 Subject: [PATCH 08/32] bundler: finish threading arena lifetime through Graph/LinkerContext/ParseTask - ParseResult/ParseOptions carry the arena lifetime; cold loader fns take &'a Arena - ResolveImportRecordCtx/ImportInfo take &[ImportRecord] (allocator-agnostic) - arena-allocate parser Source so Ast<'bump> isn't pinned to the stack frame - ArenaVec call sites use std slice/index ops instead of BabyListExt - Worker::arena() returns &'static (centralises the per-task detach) --- src/bundler/AstBuilder.rs | 36 +++++----- src/bundler/HTMLImportManifest.rs | 4 +- src/bundler/LinkerContext.rs | 63 ++++++++--------- src/bundler/LinkerGraph.rs | 2 +- src/bundler/ParseTask.rs | 45 +++++++------ src/bundler/ServerComponentParseTask.rs | 2 +- src/bundler/ThreadPool.rs | 11 ++- src/bundler/barrel_imports.rs | 6 +- src/bundler/bundle_v2.rs | 41 +++++++----- .../computeCrossChunkDependencies.rs | 12 ++-- .../findAllImportedPartsInJSOrder.rs | 8 +-- .../findImportedCSSFilesInJSOrder.rs | 6 +- .../findImportedFilesInCSSOrder.rs | 10 +-- .../generateCodeForLazyExport.rs | 27 +++----- .../linker_context/scanImportsAndExports.rs | 16 ++--- src/bundler/transpiler.rs | 67 +++++++++++-------- src/css/dependencies.rs | 7 +- src/css/printer.rs | 12 ++-- src/css/properties/custom.rs | 3 +- src/css/values/url.rs | 7 +- 20 files changed, 205 insertions(+), 180 deletions(-) diff --git a/src/bundler/AstBuilder.rs b/src/bundler/AstBuilder.rs index 01f5aefb90a..5ae0344e154 100644 --- a/src/bundler/AstBuilder.rs +++ b/src/bundler/AstBuilder.rs @@ -295,12 +295,12 @@ impl<'a, 'bump> AstBuilder<'a, 'bump> { pub fn to_bundled_ast( &mut self, target: options::Target, - ) -> Result, OOM> { + ) -> Result, OOM> { // TODO: missing import scanner debug_assert!(self.scopes.is_empty()); let module_scope = self.current_scope; - let mut parts = Vec::with_capacity_in(2, self.arena); + let mut parts = Vec::with_capacity_in(2, self.bump); // PORT NOTE: Zig grew len then wrote `parts.mut(i).* = ...`, which is a // bitwise store on the SoA slot. In Rust `*parts.mut_(i) = ...` first // *drops* the (uninitialized) prior `Part` — and `Part` carries Drop @@ -308,8 +308,8 @@ impl<'a, 'bump> AstBuilder<'a, 'bump> { // heap (observed downstream as `printStmt` reading a junk `Stmt` // discriminant from an arena allocation that was clobbered). Append // into the reserved capacity instead so no drop runs. - parts.append_assume_capacity(Part::default()); - parts.append_assume_capacity(Part { + parts.push(Part::default()); + parts.push(Part { // overwritten below with the arena-backed copy (`stmts_in_bump`) stmts: bun_ast::StoreSlice::EMPTY, can_be_removed_if_unused: false, @@ -448,12 +448,11 @@ impl<'a, 'bump> AstBuilder<'a, 'bump> { // matches Zig's value copy (no Drop fields touched). let value = unsafe { core::ptr::read(&raw const st.value) }.to_expr(); let temp_id = self.generate_temp_ref(Some(b"default_export")); - parts.mut_(1).declared_symbols.append(DeclaredSymbol { + parts[1].declared_symbols.append(DeclaredSymbol { ref_: temp_id, is_top_level: true, })?; - parts - .mut_(1) + parts[1] .symbol_uses .put(temp_id, symbol::Use { count_estimate: 1 })?; VecExt::append(&mut self.current_scope_mut().generated, temp_id); @@ -510,25 +509,24 @@ impl<'a, 'bump> AstBuilder<'a, 'bump> { Loc::EMPTY, )); // mark a dependency on module_ref so it is renamed - parts - .mut_(1) + parts[1] .symbol_uses .put(self.module_ref, symbol::Use { count_estimate: 1 })?; - parts.mut_(1).declared_symbols.append(DeclaredSymbol { + parts[1].declared_symbols.append(DeclaredSymbol { ref_: self.module_ref, is_top_level: true, })?; } // Head-part bookkeeping (only `parts[0]`, which is the empty // namespace-export part): mark dead and depend on `parts[1]`. - parts.mut_(0).tag = bun_ast::PartTag::DeadDueToInlining; - parts.mut_(0).dependencies.push(bun_ast::Dependency { + parts[0].tag = bun_ast::PartTag::DeadDueToInlining; + parts[0].dependencies.push(bun_ast::Dependency { part_index: 1, source_index: bun_ast::Index(self.source_index), }); let stmts_in_bump: &mut [Stmt] = self.bump.alloc_slice_copy(hmr_stmts.as_slice()); - parts.mut_(1).stmts = bun_ast::StoreSlice::new_mut(stmts_in_bump); + parts[1].stmts = bun_ast::StoreSlice::new_mut(stmts_in_bump); } else { // Non-HMR path: mirror `ImportScanner.scan(AstBuilder, p, stmts, // false, false, {})` for the stmt shapes AstBuilder callers emit @@ -574,13 +572,13 @@ impl<'a, 'bump> AstBuilder<'a, 'bump> { } } let stmts_in_bump: &mut [Stmt] = self.bump.alloc_slice_copy(in_stmts.as_slice()); - parts.mut_(1).stmts = bun_ast::StoreSlice::new_mut(stmts_in_bump); + parts[1].stmts = bun_ast::StoreSlice::new_mut(stmts_in_bump); } - parts.mut_(1).declared_symbols = core::mem::take(&mut self.declared_symbols); - parts.mut_(1).scopes = + parts[1].declared_symbols = core::mem::take(&mut self.declared_symbols); + parts[1].scopes = bun_ast::StoreSlice::new_mut(self.bump.alloc_slice_copy(self.scopes.as_slice())); - parts.mut_(1).import_record_indices = bun_ast::PartImportRecordIndices::move_from_list( + parts[1].import_record_indices = bun_ast::PartImportRecordIndices::move_from_list( core::mem::take(&mut self.import_records_for_current_part), ); @@ -591,11 +589,11 @@ impl<'a, 'bump> AstBuilder<'a, 'bump> { Ok(crate::BundledAst { parts, module_scope: module_scope_value, - symbols: bun_alloc::vec_from_iter_in(core::mem::take(&mut self.symbols).into_iter(), self.arena), + symbols: bun_alloc::vec_from_iter_in(core::mem::take(&mut self.symbols).into_iter(), self.bump), exports_ref: Ref::NONE, wrapper_ref: Ref::NONE, module_ref: self.module_ref, - import_records: bun_alloc::vec_from_iter_in(core::mem::take(&mut self.import_records).into_iter(), self.arena), + import_records: bun_alloc::vec_from_iter_in(core::mem::take(&mut self.import_records).into_iter(), self.bump), export_star_import_records: Box::default(), approximate_newline_count: 1, exports_kind: ExportsKind::Esm, diff --git a/src/bundler/HTMLImportManifest.rs b/src/bundler/HTMLImportManifest.rs index cf112b823dd..5b1e8bd7acb 100644 --- a/src/bundler/HTMLImportManifest.rs +++ b/src/bundler/HTMLImportManifest.rs @@ -56,9 +56,9 @@ use crate::{BundleV2, Chunk, LinkerGraph}; #[derive(Clone, Copy)] pub struct HTMLImportManifest<'a> { pub index: u32, - pub graph: &'a Graph, + pub graph: &'a Graph<'a>, pub chunks: &'a [Chunk], - pub linker_graph: &'a LinkerGraph, + pub linker_graph: &'a LinkerGraph<'a>, } impl<'a> fmt::Display for HTMLImportManifest<'a> { diff --git a/src/bundler/LinkerContext.rs b/src/bundler/LinkerContext.rs index dd54c30270e..a5aa362598b 100644 --- a/src/bundler/LinkerContext.rs +++ b/src/bundler/LinkerContext.rs @@ -306,7 +306,7 @@ impl<'a> LinkerContext<'a> { /// split-borrow patterns that interleave `&mut Graph` with other `self` /// borrows. #[inline] - pub fn parse_graph_mut(&mut self) -> &mut Graph { + pub fn parse_graph_mut(&mut self) -> &mut Graph<'a> { debug_assert!( !self.parse_graph.is_null(), "LinkerContext.parse_graph accessed before load()" @@ -515,7 +515,7 @@ impl<'a> LinkerContext<'a> { /// (or otherwise not overlap the fields named above). pub unsafe fn load( &mut self, - bundle: *mut BundleV2, + bundle: *mut BundleV2<'a>, entry_points: &[Index], server_component_boundaries: &bun_ast::server_component_boundary::List, reachable: &[Index], @@ -787,7 +787,7 @@ impl<'a> LinkerContext<'a> { #[inline(never)] pub unsafe fn link( &mut self, - bundle: *mut BundleV2, + bundle: *mut BundleV2<'a>, entry_points: &[Index], server_component_boundaries: &bun_ast::server_component_boundary::List, reachable: &[Index], @@ -815,7 +815,7 @@ impl<'a> LinkerContext<'a> { // and reborrow per call to satisfy borrowck (`&mut self` is held // across the recursion). let parse_graph: *mut Graph<'a> = self.parse_graph; - let import_records_list: *const [Vec] = + let import_records_list: *const [bun_ast::import_record::List<'a>] = self.graph.ast.items_import_records(); let flags: *mut [crate::ungate_support::js_meta::Flags] = self.graph.meta.items_flags_mut(); @@ -858,7 +858,7 @@ impl<'a> LinkerContext<'a> { continue; } - let import_records = import_records_list[source_index as usize].slice(); + let import_records = import_records_list[source_index as usize].as_slice(); let _ = self.validate_tla( source_index, tla_keywords, @@ -922,8 +922,9 @@ impl<'a> LinkerContext<'a> { // Zig held them simultaneously. The SoA columns are physically disjoint // and the underlying slabs don't reallocate during tree-shaking, so we // cache raw column base pointers and reborrow at each recursive call. - let parts: *mut [Vec] = self.graph.ast.items_parts_mut(); - let import_records: *const [Vec] = self.graph.ast.items_import_records(); + let parts: *mut [bun_ast::PartList<'a>] = self.graph.ast.items_parts_mut(); + let import_records: *const [bun_ast::import_record::List<'a>] = + self.graph.ast.items_import_records(); let css_reprs: *const [crate::bundled_ast::CssCol] = self.graph.ast.items_css(); let side_effects: *const [SideEffects] = self.parse_graph().input_files.items_side_effects(); @@ -1886,7 +1887,7 @@ impl<'a> LinkerContext<'a> { input_files: &[Source], import_records: &[ImportRecord], meta_flags: &mut [crate::ungate_support::js_meta::Flags], - ast_import_records: &[Vec], + ast_import_records: &[bun_ast::import_record::List<'a>], ) -> Result { // PORT NOTE: reshaped for borrowck — Zig held &mut tla_checks[source_index] across recursive // calls that also mutate tla_checks. We re-index after each recursion. @@ -1905,7 +1906,7 @@ impl<'a> LinkerContext<'a> { tla_keywords, tla_checks, input_files, - ast_import_records[record.source_index.get() as usize].slice(), + ast_import_records[record.source_index.get() as usize].as_slice(), meta_flags, ast_import_records, )?; @@ -1990,7 +1991,7 @@ impl<'a> LinkerContext<'a> { text: text.into(), location: bun_ast::Location::init_or_null( Some(&input_files[parent_source_index as usize]), - ast_import_records[parent_source_index as usize].slice() + ast_import_records[parent_source_index as usize].as_slice() [tla_checks[parent_source_index as usize] .import_record_index as usize] @@ -2363,7 +2364,7 @@ impl<'a> LinkerContext<'a> { } let all_css_asts = self.graph.ast.items_css(); - let all_symbols: &[Vec] = self.graph.ast.items_symbols(); + let all_symbols: &[bun_ast::symbol::List<'a>] = self.graph.ast.items_symbols(); // SAFETY: parse_graph backref; raw deref because `all_sources` is held // across `&mut self.mangled_props` below (split borrow). let all_sources: &[Source] = unsafe { (*self.parse_graph).input_files.items_source() }; @@ -2378,7 +2379,7 @@ impl<'a> LinkerContext<'a> { continue; } let symbols = &all_symbols[source_index]; - for (inner_index, symbol_) in symbols.slice_const().iter().enumerate() { + for (inner_index, symbol_) in symbols.as_slice().iter().enumerate() { let mut symbol = symbol_; if symbol.kind == bun_ast::symbol::Kind::LocalCss { let r#ref = 'follow: { @@ -2391,8 +2392,8 @@ impl<'a> LinkerContext<'a> { ); while symbol.has_link() { r#ref = symbol.link.get(); - symbol = all_symbols[r#ref.source_index() as usize] - .at(r#ref.inner_index() as usize); + symbol = &all_symbols[r#ref.source_index() as usize] + [r#ref.inner_index() as usize]; } break 'follow r#ref; }; @@ -2618,8 +2619,8 @@ impl<'a> LinkerContext<'a> { // Spec (LinkerContext.zig:1579) passes `parts: []Vec(Part)` and only // reads it. `&mut` here forced an aliased reborrow against the // `parts_in_file` slice below — borrowck conflict in un-gated code. - parts: &[Vec], - import_records: &[Vec], + parts: &[bun_ast::PartList<'a>], + import_records: &[bun_ast::import_record::List<'a>], file_entry_bits: &mut [AutoBitSet], css_reprs: &[crate::bundled_ast::CssCol], ) { @@ -2662,7 +2663,7 @@ impl<'a> LinkerContext<'a> { } if css_reprs[source_index as usize].is_some() { - for record in import_records[source_index as usize].slice() { + for record in import_records[source_index as usize].as_slice() { if record.source_index.is_valid() && !self.is_external_dynamic_import(record, source_index) { @@ -2681,7 +2682,7 @@ impl<'a> LinkerContext<'a> { return; } - for record in import_records[source_index as usize].slice() { + for record in import_records[source_index as usize].as_slice() { if record.source_index.is_valid() && !self.is_external_dynamic_import(record, source_index) { @@ -2698,7 +2699,7 @@ impl<'a> LinkerContext<'a> { } } - let parts_in_file = parts[source_index as usize].slice(); + let parts_in_file = parts[source_index as usize].as_slice(); for part in parts_in_file { for dependency in part.dependencies.slice() { if dependency.source_index.get() != source_index { @@ -2721,8 +2722,8 @@ impl<'a> LinkerContext<'a> { &mut self, source_index: crate::IndexInt, side_effects: &[SideEffects], - parts: &mut [Vec], - import_records: &[Vec], + parts: &mut [bun_ast::PartList<'a>], + import_records: &[bun_ast::import_record::List<'a>], entry_point_kinds: &[EntryPoint::Kind], css_reprs: &[crate::bundled_ast::CssCol], ) { @@ -2767,7 +2768,7 @@ impl<'a> LinkerContext<'a> { } if css_reprs[source_index as usize].is_some() { - for record in import_records[source_index as usize].slice() { + for record in import_records[source_index as usize].as_slice() { let other_source_index = record.source_index.get(); if record.source_index.is_valid() { self.mark_file_live_for_tree_shaking( @@ -2787,7 +2788,7 @@ impl<'a> LinkerContext<'a> { // via .url kind import records. Follow all import records for HTML files // so these assets are marked live and included in the manifest. if self.parse_graph().input_files.items_loader()[source_index as usize] == Loader::Html { - for record in import_records[source_index as usize].slice() { + for record in import_records[source_index as usize].as_slice() { if record.source_index.is_valid() { self.mark_file_live_for_tree_shaking( record.source_index.get(), @@ -2802,10 +2803,10 @@ impl<'a> LinkerContext<'a> { return; } - let part_count = parts[source_index as usize].slice().len(); + let part_count = parts[source_index as usize].len(); for part_index in 0..part_count { // PORT NOTE: reshaped for borrowck — re-borrow part each iteration since recursion mutates `parts` - let part = &parts[source_index as usize].slice()[part_index]; + let part = &parts[source_index as usize].as_slice()[part_index]; let mut can_be_removed_if_unused = part.can_be_removed_if_unused; if can_be_removed_if_unused && part.tag == bun_ast::PartTag::CommonjsNamedExport { @@ -2818,7 +2819,7 @@ impl<'a> LinkerContext<'a> { // PORT NOTE: clone indices to avoid holding borrow across recursive call let import_indices: Vec = part.import_record_indices.slice().to_vec(); for import_index in import_indices { - let record = import_records[source_index as usize].at(import_index as usize); + let record = &import_records[source_index as usize][import_index as usize]; if record.kind != ImportKind::Stmt { continue; } @@ -2860,7 +2861,7 @@ impl<'a> LinkerContext<'a> { // everything if tree-shaking is disabled. Note that we still want to // perform tree-shaking on the runtime even if tree-shaking is disabled. let force_tree_shaking = - parts[source_index as usize].slice()[part_index].force_tree_shaking; + parts[source_index as usize].as_slice()[part_index].force_tree_shaking; if !can_be_removed_if_unused || (!force_tree_shaking && !self.options.tree_shaking @@ -2884,12 +2885,12 @@ impl<'a> LinkerContext<'a> { part_index: crate::IndexInt, source_index: crate::IndexInt, side_effects: &[SideEffects], - parts: &mut [Vec], - import_records: &[Vec], + parts: &mut [bun_ast::PartList<'a>], + import_records: &[bun_ast::import_record::List<'a>], entry_point_kinds: &[EntryPoint::Kind], css_reprs: &[crate::bundled_ast::CssCol], ) { - let part: &mut Part = &mut parts[source_index as usize].slice_mut()[part_index as usize]; + let part: &mut Part = &mut parts[source_index as usize].as_mut_slice()[part_index as usize]; // only once if part.is_live { @@ -3367,7 +3368,7 @@ impl<'a> LinkerContext<'a> { let ast_flags = self.graph.ast.items_flags(); // Is this an external file? - let record: &ImportRecord = import_records[named_import.import_record_index as usize]; + let record: &ImportRecord = &import_records[named_import.import_record_index as usize]; if !record.source_index.is_valid() { return ImportTrackerIterator { value: Default::default(), diff --git a/src/bundler/LinkerGraph.rs b/src/bundler/LinkerGraph.rs index 0a49db6ab40..64875749546 100644 --- a/src/bundler/LinkerGraph.rs +++ b/src/bundler/LinkerGraph.rs @@ -802,7 +802,7 @@ impl<'a> LinkerGraph<'a> { return; } - for import_record in self.import_records[index].slice_const().iter() { + for import_record in self.import_records[index].as_slice().iter() { match import_record.kind { ImportKind::Stmt => {} diff --git a/src/bundler/ParseTask.rs b/src/bundler/ParseTask.rs index a9c53722506..090f61baa6f 100644 --- a/src/bundler/ParseTask.rs +++ b/src/bundler/ParseTask.rs @@ -612,9 +612,9 @@ pub mod parse_worker { fn get_empty_css_ast( log: &mut Log, transpiler: *mut Transpiler, - opts: ParserOptions, + opts: ParserOptions<'static>, bump: &'static Bump, - source: &Source, + source: &'static Source, ) -> core::result::Result, AnyError> { let root = Expr::init(E::Object::default(), Loc { start: 0 }); // SAFETY: `transpiler` is a live worker-owned `*mut Transpiler`; `options` @@ -632,9 +632,9 @@ pub mod parse_worker { fn get_empty_ast( log: &mut Log, transpiler: *mut Transpiler, - opts: ParserOptions, + opts: ParserOptions<'static>, bump: &'static Bump, - source: &Source, + source: &'static Source, ) -> core::result::Result, AnyError> { let root = Expr::init(RootType::default(), Loc::EMPTY); // SAFETY: see `get_empty_css_ast` — disjoint field of a live `*mut Transpiler`. @@ -663,9 +663,12 @@ pub mod parse_worker { // populated symbol table (.zig:613). // ─────────────────────────────────────────────────────────────────────────── - fn css_symbols_to_parser_symbols(src: Vec) -> bun_ast::symbol::List<'static> { + fn css_symbols_to_parser_symbols( + src: Vec, + bump: &'static Bump, + ) -> bun_ast::symbol::List<'static> { use bun_ast::symbol::{Kind as PKind, Symbol as PSym}; - let mut out = Vec::::init_capacity(src.len() as usize); + let mut out = Vec::with_capacity_in(src.len(), bump); for s in src.slice() { // Post-dedup `bun_ast::Symbol` IS `bun_ast::symbol::Symbol`, so // `s.kind`/`s.import_item_status` are already the target nominal types @@ -675,7 +678,7 @@ pub mod parse_worker { // `bun_ast::Ref` is a re-export of `bun_ast::Ref` (ast/base.rs:172) // — same nominal type, no bridge needed. let link: bun_ast::Ref = s.link.get(); - out.append_assume_capacity(PSym { + out.push(PSym { original_name: bun_ast::StoreStr::new(s.original_name.slice()), // CSS-module locals are never ES6 namespace-aliased (the CSS parser // never assigns `namespace_alias`); drop rather than bridge the @@ -724,10 +727,10 @@ pub mod parse_worker { fn get_ast( log: &mut Log, transpiler: *mut Transpiler, - opts: ParserOptions, + opts: ParserOptions<'static>, bump: &'static Bump, resolver: *mut Resolver, - source: &Source, + source: &'static Source, loader: Loader, unique_key_prefix: u64, unique_key_for_additional_file: &mut FileLoaderHash, @@ -1158,7 +1161,8 @@ pub mod parse_worker { b"", )? .unwrap(); - ast.import_records = import_records; + ast.import_records = + bun_alloc::vec_from_iter_in(import_records.into_iter(), bump); // We're banning import default of html loader files for now. // @@ -1267,7 +1271,7 @@ pub mod parse_worker { // `Vec`. Both port the same Zig // `js_ast.Symbol`; convert field-by-field so CSS-module local refs // index a populated symbol table (.zig:613). - let symbols = css_symbols_to_parser_symbols(extra.symbols); + let symbols = css_symbols_to_parser_symbols(extra.symbols, bump); // PORT NOTE: Zig `defer temp_log.appendToMaybeRecycled(log, source)` // (.zig:564-566) flushes on EVERY exit including this `try`; mirror // by matching explicitly so accumulated CSS-module diagnostics are @@ -1286,7 +1290,8 @@ pub mod parse_worker { let mut ast = JSAst::init(lazy?.unwrap()); let css_ast_heap = crate::bundled_ast::CssAstRef::from_bump(bump.alloc(css_ast)); ast.css = Some(css_ast_heap); - ast.import_records = import_records; + ast.import_records = + bun_alloc::vec_from_iter_in(import_records.into_iter(), bump); return Ok(ast); } // TODO: @@ -2370,7 +2375,9 @@ pub mod parse_worker { // reassigned above); reborrow only the disjoint `options` field. let topts = unsafe { &(*transpiler).options }; - let source = Source { + // Allocated in the worker arena so `js_parser::new_lazy_export_ast`'s + // `&'bump Source` parameter is satisfied (`bump` is the same arena). + let source: &'static Source = bump.alloc(Source { // PORT NOTE: `Source.path` is `bun_paths::fs::Path<'static>`, distinct from // `bun_resolver::fs::Path` (TYPE_ONLY mirror). Construct // field-by-field across the type boundary. @@ -2392,7 +2399,7 @@ pub mod parse_worker { contents: std::borrow::Cow::Borrowed(ast::StoreStr::new(entry_contents).slice()), contents_is_recycled: false, ..Default::default() - }; + }); let target = (if task.source_index.get() == 1 { target_from_hashbang(entry_contents) @@ -2601,18 +2608,18 @@ pub mod parse_worker { opts, bump, resolver, - &source, + source, loader, task_ctx.unique_key, &mut unique_key_for_additional_file, &task_ctx.linker.has_any_css_locals, ) } else if loader.is_css() { - get_empty_css_ast(log, transpiler, opts, bump, &source) + get_empty_css_ast(log, transpiler, opts, bump, source) } else if module_type == options::ModuleType::Esm { - get_empty_ast::(log, transpiler, opts, bump, &source) + get_empty_ast::(log, transpiler, opts, bump, source) } else { - get_empty_ast::(log, transpiler, opts, bump, &source) + get_empty_ast::(log, transpiler, opts, bump, source) }; // PERF(port): Zig used `switch (bool) { inline else => |as_undefined| ... }` // to monomorphize. Expanded to if/else. @@ -2654,7 +2661,7 @@ pub mod parse_worker { Ok(Success { ast, - source, + source: source.clone(), log: core::mem::take(log), // PORT NOTE: Zig returned `log.*` by value; here we take ownership. use_directive, diff --git a/src/bundler/ServerComponentParseTask.rs b/src/bundler/ServerComponentParseTask.rs index 0f05788f680..3fa1832195a 100644 --- a/src/bundler/ServerComponentParseTask.rs +++ b/src/bundler/ServerComponentParseTask.rs @@ -149,7 +149,7 @@ fn on_complete_mini(result: *mut parse_task::Result, _ctx: *mut BundleV2<'static fn task_callback( task: &mut ServerComponentParseTask, log: &mut Log, - bump: &Arena, + bump: &'static Arena, ) -> Result { // `ctx` is a `ParentRef` BACKREF to the owning BundleV2; safe `Deref`. let ctx: &BundleV2 = task diff --git a/src/bundler/ThreadPool.rs b/src/bundler/ThreadPool.rs index 2d094e90673..f1265d79b59 100644 --- a/src/bundler/ThreadPool.rs +++ b/src/bundler/ThreadPool.rs @@ -515,9 +515,16 @@ impl Worker { /// can observe the `Worker`, and is never dangling after that point. The /// pointee is the worker's own `heap` field, which is pinned for the /// worker's lifetime. + /// The worker-owned bump arena. Returns `&'static` because the arena is + /// pinned for the worker's lifetime and `Worker::get` already hands out a + /// `&'static mut Worker`; centralising the erasure here avoids per-call-site + /// `detach_lifetime_ref` (the previous pattern at `ParseTask::run`). #[inline] - pub fn arena(&self) -> &ThreadLocalArena { - self.arena.get() + pub fn arena(&self) -> &'static ThreadLocalArena { + // SAFETY: `self.arena` is a `BackRef` to the worker-owned heap, set in + // `Worker::create` and never reassigned; the heap is pinned for the + // process lifetime (workers are never destroyed before exit). + unsafe { bun_ptr::detach_lifetime_ref(self.arena.get()) } } } diff --git a/src/bundler/barrel_imports.rs b/src/bundler/barrel_imports.rs index 3c2dd424026..50ae7111f2d 100644 --- a/src/bundler/barrel_imports.rs +++ b/src/bundler/barrel_imports.rs @@ -300,7 +300,11 @@ fn resolve_barrel_records( let loader = this.graph.input_files.items_loader()[idx]; // Move the column cells out so the `&mut self` method calls below don't // alias borrows into `graph.ast` / `graph.input_files`. - let mut barrel_ir = core::mem::take(&mut this.graph.ast.items_import_records_mut()[idx]); + let heap = this.graph.heap; + let mut barrel_ir = core::mem::replace( + &mut this.graph.ast.items_import_records_mut()[idx], + Vec::new_in(heap), + ); let source = core::mem::take(&mut this.graph.input_files.items_source_mut()[idx]); let source_path: &'static [u8] = source.path.text; diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index 4ecd4a8bbbe..25ded3959c1 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -1846,7 +1846,7 @@ pub mod bv2_impl { // (source_index, path) before re-borrowing `all_import_records` mutably. let (other_src_idx, other_path) = { let other_import_records = - self.all_import_records[other_source.get() as usize].slice(); + self.all_import_records[other_source.get() as usize].as_slice(); let other_import_record = &other_import_records[redirect_id as usize]; ( @@ -2940,8 +2940,8 @@ pub mod bv2_impl { Ok(this) } - pub fn arena(&self) -> &bun_alloc::Arena { - &self.graph.heap + pub fn arena(&self) -> &'a bun_alloc::Arena { + self.graph.heap } /// Allocate `value` into the bundler's arena (`self.graph.heap`) and return @@ -4963,13 +4963,13 @@ pub mod bv2_impl { ($ast:expr) => {{ let ast = $ast; for v in ast.items_parts_mut() { - drop(core::mem::take(v)); + drop(core::mem::replace(v, Vec::new_in(*v.allocator()))); } for v in ast.items_symbols_mut() { - drop(core::mem::take(v)); + drop(core::mem::replace(v, Vec::new_in(*v.allocator()))); } for v in ast.items_import_records_mut() { - drop(core::mem::take(v)); + drop(core::mem::replace(v, Vec::new_in(*v.allocator()))); } for v in ast.items_named_imports_mut() { drop(core::mem::take(v)); @@ -5944,8 +5944,9 @@ pub mod bv2_impl { // bounds crashes in BundleV2.onResolve / runResolver. The linker // never runs because `transpiler.log.errors > 0` aborts the // build before link time, so saving the AST is safe. + let result_heap = *result.ast.import_records.allocator(); this.graph.ast.items_import_records_mut()[source_index.0 as usize] = - core::mem::take(&mut result.ast.import_records); + core::mem::replace(&mut result.ast.import_records, Vec::new_in(result_heap)); // Move the CSS stylesheet onto the graph row so teardown can find // and drop it — the `Success` arm that would normally do this is skipped. @@ -5965,7 +5966,7 @@ pub mod bv2_impl { } pub struct ResolveImportRecordCtx<'a> { - pub import_records: &'a mut import_record::List<'a>, + pub import_records: &'a mut [ImportRecord], pub source: &'a bun_ast::Source, pub loader: Loader, pub target: options::Target, @@ -5990,7 +5991,7 @@ pub mod bv2_impl { let loader = ctx.loader; let source_dir = source.path.source_dir(); let mut estimated_resolve_queue_count: usize = 0; - for import_record in ctx.import_records.as_mut_slice() { + for import_record in ctx.import_records.iter_mut() { if import_record .flags .contains(bun_ast::ImportRecordFlags::IS_INTERNAL) @@ -6026,7 +6027,7 @@ pub mod bv2_impl { let mut last_error: Option = None; - 'outer: for (i, import_record) in ctx.import_records.as_mut_slice().iter_mut().enumerate() + 'outer: for (i, import_record) in ctx.import_records.iter_mut().enumerate() { // Preserve original import specifier before resolution modifies path if import_record.original_path.is_empty() { @@ -6907,12 +6908,13 @@ pub mod bv2_impl { // 3. Add it to the graph // PORT NOTE: Zig aliased `graph = &this.graph;` — re-borrow `self.graph` // at each use so the `self.*` method calls below don't conflict. - let empty_html_file_source = bun_ast::Source { + let heap = self.graph.heap; + let empty_html_file_source: &mut bun_ast::Source = self.arena_create(bun_ast::Source { path: path_as_static(path.clone()), index: bun_ast::Index(self.graph.input_files.len() as u32), contents: std::borrow::Cow::Borrowed(&b""[..]), ..Default::default() - }; + }); let mut js_parser_options = bun_js_parser::ParserOptions::init( self.transpiler_for_target(target) .options @@ -6952,7 +6954,7 @@ pub mod bv2_impl { let ast_for_html_entrypoint = JSAst::init( bun_js_parser::new_lazy_export_ast( - self.arena(), + heap, unsafe { &mut *define_ptr }, js_parser_options, unsafe { &mut *log_ptr }, @@ -6963,7 +6965,7 @@ pub mod bv2_impl { }, bun_ast::Loc::EMPTY, ), - &empty_html_file_source, + empty_html_file_source, // We replace this runtime API call's ref later via .link on the Symbol. b"__jsonParse", )? @@ -6971,7 +6973,7 @@ pub mod bv2_impl { ); let fake_input_file = crate::Graph::InputFile { - source: empty_html_file_source, + source: empty_html_file_source.clone(), side_effects: bun_ast::SideEffects::NoSideEffectsPureData, ..Default::default() }; @@ -7187,7 +7189,11 @@ pub mod bv2_impl { result_source_index as IndexInt, ); - let mut import_records = core::mem::take(&mut result.ast.import_records); + let result_heap = *result.ast.import_records.allocator(); + let mut import_records = core::mem::replace( + &mut result.ast.import_records, + Vec::new_in(result_heap), + ); let source_path_owned: Box<[u8]> = source_path_text.into(); this.patch_import_record_source_indices( &mut import_records, @@ -7255,9 +7261,10 @@ pub mod bv2_impl { None }; + let result_heap = *result.ast.parts.allocator(); this.graph.ast.set( result_source_index, - core::mem::replace(&mut result.ast, JSAst::empty_in(self.graph.heap)), + core::mem::replace(&mut result.ast, JSAst::empty_in(result_heap)), ); // Barrel optimization: eagerly record import requests and diff --git a/src/bundler/linker_context/computeCrossChunkDependencies.rs b/src/bundler/linker_context/computeCrossChunkDependencies.rs index 9a1242b055d..500181b58dd 100644 --- a/src/bundler/linker_context/computeCrossChunkDependencies.rs +++ b/src/bundler/linker_context/computeCrossChunkDependencies.rs @@ -87,15 +87,15 @@ pub fn compute_cross_chunk_dependencies( compute_cross_chunk_dependencies_with_chunk_metas(c, chunks, &mut chunk_metas) } -pub struct CrossChunkDependencies<'a> { +pub struct CrossChunkDependencies<'a, 'bump> { chunk_meta: &'a mut [ChunkMeta], // PORT NOTE: `BackRef` — the same `[Chunk]` slice is also iterated mutably by // the caller's sequential `walk` loop; `walk` only reads `chunks[other].unique_key` // (disjoint from the per-iteration `&mut Chunk`). The slice outlives the struct // (caller stack frame). chunks: bun_ptr::BackRef<[Chunk]>, - parts: &'a [Vec], - import_records: &'a mut [Vec], + parts: &'a [bun_ast::PartList<'bump>], + import_records: &'a mut [bun_ast::import_record::List<'bump>], flags: &'a [js_meta::Flags], entry_point_chunk_indices: &'a [IndexInt], imports_to_bind: &'a [RefImportData], @@ -119,7 +119,7 @@ pub struct CrossChunkDependencies<'a> { symbols: bun_ptr::BackRef, } -impl<'a> CrossChunkDependencies<'a> { +impl<'a, 'bump> CrossChunkDependencies<'a, 'bump> { // Called once per chunk from the sequential loop above. Writes: // `self.chunk_meta[chunk_index]` (per-chunk disjoint), // `self.import_records[source_index][rec].{path,source_index}` (per-chunk @@ -156,8 +156,8 @@ impl<'a> CrossChunkDependencies<'a> { } // Go over each part in this file that's marked for inclusion in this chunk - let parts = deps.parts[source_index as usize].slice(); - let import_records = deps.import_records[source_index as usize].slice_mut(); + let parts = deps.parts[source_index as usize].as_slice(); + let import_records = deps.import_records[source_index as usize].as_mut_slice(); let imports_to_bind = &deps.imports_to_bind[source_index as usize]; let wrap = deps.flags[source_index as usize].wrap; let wrapper_ref = deps.wrapper_refs[source_index as usize]; diff --git a/src/bundler/linker_context/findAllImportedPartsInJSOrder.rs b/src/bundler/linker_context/findAllImportedPartsInJSOrder.rs index 8c41c0b58e8..d2b1246419e 100644 --- a/src/bundler/linker_context/findAllImportedPartsInJSOrder.rs +++ b/src/bundler/linker_context/findAllImportedPartsInJSOrder.rs @@ -150,8 +150,8 @@ fn run_visits( pub struct FindImportedPartsVisitor<'a, 'ctx> { pub entry_bits: &'a AutoBitSet, pub flags: &'a [crate::js_meta::Flags], - pub parts: &'a [Vec], - pub import_records: &'a [Vec], + pub parts: &'a [bun_ast::PartList<'ctx>], + pub import_records: &'a [bun_ast::import_record::List<'ctx>], pub files: Vec, pub part_ranges: Vec, pub visited: HashMap, @@ -215,7 +215,7 @@ impl<'a, 'ctx> FindImportedPartsVisitor<'a, 'ctx> { // Wrapped files can't be split because they are all inside the wrapper let can_be_split = self.flags[source_index as usize].wrap == Wrap::None; - let parts = self.parts[source_index as usize].slice(); + let parts = self.parts[source_index as usize].as_slice(); if can_be_split && is_file_in_chunk && parts[bun_ast::NAMESPACE_EXPORT_PART_INDEX as usize].is_live @@ -227,7 +227,7 @@ impl<'a, 'ctx> FindImportedPartsVisitor<'a, 'ctx> { ); } - let records = self.import_records[source_index as usize].slice(); + let records = self.import_records[source_index as usize].as_slice(); for part_index_ in 0..parts.len() { let part = &parts[part_index_]; diff --git a/src/bundler/linker_context/findImportedCSSFilesInJSOrder.rs b/src/bundler/linker_context/findImportedCSSFilesInJSOrder.rs index 5c837c3c773..08fc90fc6c9 100644 --- a/src/bundler/linker_context/findImportedCSSFilesInJSOrder.rs +++ b/src/bundler/linker_context/findImportedCSSFilesInJSOrder.rs @@ -43,8 +43,8 @@ pub fn find_imported_css_files_in_js_order( #[allow(clippy::too_many_arguments)] fn visit( c: &LinkerContext, - import_records: &[Vec], - parts: &[PartList], + import_records: &[bun_ast::import_record::List<'_>], + parts: &[PartList<'_>], loaders: &[Loader], visits: &mut BitSet, o: &mut Vec, @@ -56,7 +56,7 @@ pub fn find_imported_css_files_in_js_order( } visits.set(source_index.get() as usize); - let records: &[ImportRecord] = import_records[source_index.get() as usize].slice(); + let records: &[ImportRecord] = import_records[source_index.get() as usize].as_slice(); let p = &parts[source_index.get() as usize]; // Iterate over each part in the file in order diff --git a/src/bundler/linker_context/findImportedFilesInCSSOrder.rs b/src/bundler/linker_context/findImportedFilesInCSSOrder.rs index cc3c654a019..5d0afe7d3d7 100644 --- a/src/bundler/linker_context/findImportedFilesInCSSOrder.rs +++ b/src/bundler/linker_context/findImportedFilesInCSSOrder.rs @@ -81,7 +81,7 @@ pub fn find_imported_files_in_css_order<'a>( arena: &'a Arena, // `BundledAst.css` SoA column. css_asts: &'a [crate::bundled_ast::CssCol], - all_import_records: &'a [Vec], + all_import_records: &'a [bun_ast::import_record::List<'a>], // PORT NOTE: Zig's `graph: *LinkerGraph` is never read in `visit()`; // dropped here to avoid an aliasing `&mut this.graph` borrow against @@ -89,7 +89,7 @@ pub fn find_imported_files_in_css_order<'a>( // `BackRef` (not `&'a Graph`) so the visitor's `'a` borrow stays // disjoint from `LinkerContext` (constructed from the raw `parse_graph` // backref, valid for the link step). - parse_graph: bun_ptr::BackRef, + parse_graph: bun_ptr::BackRef>, has_external_import: bool, visited: Vec, @@ -164,7 +164,7 @@ pub fn find_imported_files_in_css_order<'a>( if let BundlerCssRule::Import(import_rule) = rule { // `defer import_record_idx += 1;` — increment at end of this arm let record = - self.all_import_records[source_index.get() as usize].at(import_record_idx); + &self.all_import_records[source_index.get() as usize][import_record_idx]; // Follow internal dependencies if record.source_index.is_valid() { @@ -257,7 +257,7 @@ pub fn find_imported_files_in_css_order<'a>( // Iterate over the "composes" directives. Note that the order doesn't // matter for these because the output order is explicitly undfened // in the specification. - for record in self.all_import_records[source_index.get() as usize].slice_const() { + for record in self.all_import_records[source_index.get() as usize].as_slice() { if record.kind == ImportKind::Composes && record.source_index.is_valid() { self.visit( record.source_index, @@ -293,7 +293,7 @@ pub fn find_imported_files_in_css_order<'a>( // PORT NOTE: reshaped for borrowck — read MultiArrayList columns before constructing visitor. let css_asts_slice: &[crate::bundled_ast::CssCol] = this.graph.ast.items_css(); - let all_import_records_slice: &[Vec] = this.graph.ast.items_import_records(); + let all_import_records_slice = this.graph.ast.items_import_records(); let arena = this.graph.arena(); let mut visitor = Visitor { diff --git a/src/bundler/linker_context/generateCodeForLazyExport.rs b/src/bundler/linker_context/generateCodeForLazyExport.rs index 70a6da0529a..baa41fbb49e 100644 --- a/src/bundler/linker_context/generateCodeForLazyExport.rs +++ b/src/bundler/linker_context/generateCodeForLazyExport.rs @@ -19,7 +19,7 @@ use crate::bun_css::{BundlerStyleSheet, CssRef, CssRefTag}; use crate::{Index, IndexInt, LinkerContext}; use bun_collections::DynamicBitSetUnmanaged as BitSet; -type SymbolList = Vec; +type SymbolList<'a> = bun_ast::symbol::List<'a>; /// `ArrayHashAdapter` so `LocalScope` (`ArrayHashMap, LocalEntry>`) /// can be queried by borrowed `&[u8]` (CSS idents are arena `*const [u8]`). @@ -81,8 +81,8 @@ pub fn generate_code_for_lazy_export( } let mut exports = E::Object::default(); - let symbols: &SymbolList = &this.graph.ast.items_symbols()[source_index as usize]; - let all_import_records: &[Vec] = this.graph.ast.items_import_records(); + let symbols: &SymbolList<'_> = &this.graph.ast.items_symbols()[source_index as usize]; + let all_import_records = this.graph.ast.items_import_records(); let values = css_ast.local_scope.values(); if values.len() == 0 { @@ -106,11 +106,11 @@ pub fn generate_code_for_lazy_export( // Zig: `std.AutoArrayHashMap(Ref, void)` → `ArrayHashMap` per collections map. composes_visited: &'a mut ArrayHashMap, parts: &'a mut Vec, - all_import_records: &'a [Vec], + all_import_records: &'a [bun_ast::import_record::List<'a>], // `BundledAst.css` SoA column. all_css_asts: &'a [crate::bundled_ast::CssCol], all_sources: &'a [Source], - all_symbols: &'a [SymbolList], + all_symbols: &'a [SymbolList<'a>], source_index: IndexInt, log: &'a mut Log, loc: Loc, @@ -163,12 +163,10 @@ pub fn generate_code_for_lazy_export( compose_loc: Loc, ) { let _ = self.arena; - let syms: &SymbolList = &self.all_symbols[css_ref.source_index(idx) as usize]; + let syms: &SymbolList<'_> = + &self.all_symbols[css_ref.source_index(idx) as usize]; // `Symbol.original_name: StoreStr` — arena-owned for the link pass. - let name: &[u8] = syms - .at(css_ref.inner_index() as usize) - .original_name - .slice(); + let name: &[u8] = syms[css_ref.inner_index() as usize].original_name.slice(); let loc = ast .local_scope .get_adapted(name, SliceBoxAdapter) @@ -209,10 +207,10 @@ pub fn generate_code_for_lazy_export( match &compose.from { // it is imported Some(CssSpecifier::ImportRecordIndex(import_record_idx)) => { - let import_records: &Vec = + let import_records = &self.all_import_records[idx as usize]; let import_record = - import_records.at(*import_record_idx as usize); + &import_records[*import_record_idx as usize]; if import_record.source_index.is_valid() { let Some(other_file) = self.all_css_asts [import_record.source_index.get() as usize] @@ -378,10 +376,7 @@ pub fn generate_code_for_lazy_export( } // `Symbol.original_name: StoreStr` — arena-owned for the link pass. - let key: &[u8] = symbols - .at(ref_.inner_index() as usize) - .original_name - .slice(); + let key: &[u8] = symbols[ref_.inner_index() as usize].original_name.slice(); exports.put(arena, key, value)?; } diff --git a/src/bundler/linker_context/scanImportsAndExports.rs b/src/bundler/linker_context/scanImportsAndExports.rs index 42a55805e15..b32d778f83a 100644 --- a/src/bundler/linker_context/scanImportsAndExports.rs +++ b/src/bundler/linker_context/scanImportsAndExports.rs @@ -1244,8 +1244,8 @@ impl DependencyWrapper<'_> { // ────────────────────────────────────────────────────────────────────────── // ExportStarContext — port of the inner Zig struct. Holds raw column ptrs. // ────────────────────────────────────────────────────────────────────────── -struct ExportStarContext { - import_records_list: *mut [ImportRecordList<'_>], +struct ExportStarContext<'a> { + import_records_list: *mut [ImportRecordList<'a>], source_index_stack: Vec, exports_kind: *mut [ExportsKind], named_exports: *mut [NamedExports], @@ -1253,7 +1253,7 @@ struct ExportStarContext { export_star_records: *mut [Box<[u32]>], } -impl ExportStarContext { +impl<'a> ExportStarContext<'a> { /// Recursively merge re-exports from `source_index` into /// `resolved_exports[target_id]`. fn add_exports( @@ -1273,7 +1273,7 @@ impl ExportStarContext { for import_id in col_ref!(self.export_star_records)[source_index as usize].iter() { let other_source_index = col_ref!(self.import_records_list)[source_index as usize] - .slice()[*import_id as usize] + .as_slice()[*import_id as usize] .source_index .get(); @@ -1503,10 +1503,10 @@ mod __css_validation { range: bun_ast::Range, } - struct Visitor<'a> { + struct Visitor<'a, 'bump> { visited: ArrayHashMap, properties: StringArrayHashMap, - all_import_records: *mut [ImportRecordList<'_>], + all_import_records: *mut [ImportRecordList<'bump>], all_css_asts: *mut [CssCol], all_symbols: &'a symbol::Map, all_sources: *mut [Source], @@ -1515,7 +1515,7 @@ mod __css_validation { // PORT NOTE: `pub fn deinit` → Drop on `visited` / `properties` handles cleanup. - impl<'a> Visitor<'a> { + impl<'a, 'bump> Visitor<'a, 'bump> { fn add_property_or_warn( &mut self, local: bun_ast::Ref, @@ -1617,7 +1617,7 @@ mod __css_validation { if let Some(from) = compose.from.as_ref() { if let Specifier::ImportRecordIndex(import_record_idx) = from { let record = &col_ref!(self.all_import_records)[idx as usize] - .slice()[*import_record_idx as usize]; + .as_slice()[*import_record_idx as usize]; if record.source_index.is_invalid() { continue; } diff --git a/src/bundler/transpiler.rs b/src/bundler/transpiler.rs index 17569b0e1b8..d124cbc1d45 100644 --- a/src/bundler/transpiler.rs +++ b/src/bundler/transpiler.rs @@ -841,7 +841,7 @@ pub enum AlreadyBundled { } impl Default for AlreadyBundled { - pub fn empty(arena: &'a bun_alloc::Arena) -> Self { + fn default() -> Self { AlreadyBundled::None } } @@ -1376,7 +1376,7 @@ impl<'a> Transpiler<'a> { pub fn parse( &mut self, - this_parse: ParseOptions<'_>, + this_parse: ParseOptions<'a>, client_entry_point_: Option<&mut EntryPoints::ClientEntryPoint>, ) -> Option> { self.parse_maybe_return_file_only::(this_parse, client_entry_point_) @@ -1384,7 +1384,7 @@ impl<'a> Transpiler<'a> { pub fn parse_maybe_return_file_only( &mut self, - this_parse: ParseOptions<'_>, + this_parse: ParseOptions<'a>, client_entry_point_: Option<&mut EntryPoints::ClientEntryPoint>, ) -> Option> { self.parse_maybe_return_file_only_allow_shared_buffer::( @@ -1398,7 +1398,7 @@ impl<'a> Transpiler<'a> { const USE_SHARED_BUFFER: bool, >( &mut self, - mut this_parse: ParseOptions<'_>, + mut this_parse: ParseOptions<'a>, // TODO(port): Zig `anytype` + `@hasField(.., "source")` — only ever // called with `?*EntryPoints.ClientEntryPoint` in this file. If other // callers pass a different type, introduce a `ClientEntryPointLike` @@ -1428,7 +1428,7 @@ impl<'a> Transpiler<'a> { // PORT NOTE: Zig `&brk: { ... }` took the address of a temporary; Rust // owns the value and borrows it after the block. - let source_owned: bun_ast::Source = 'brk: { + let source: &'a bun_ast::Source = arena.alloc('brk: { if let Some(virtual_source) = this_parse.virtual_source { break 'brk virtual_source.clone(); } @@ -1552,11 +1552,11 @@ impl<'a> Transpiler<'a> { Ok(s) => break 'brk s, Err(_) => return None, } - }; - let source: &bun_ast::Source = &source_owned; + }); if RETURN_FILE_ONLY { return Some(ParseResult::empty_with( + arena, source.clone(), loader, input_fd, @@ -1569,6 +1569,7 @@ impl<'a> Transpiler<'a> { { if !loader.handles_empty_file() { return Some(ParseResult::empty_with( + arena, source.clone(), loader, input_fd, @@ -1585,6 +1586,7 @@ impl<'a> Transpiler<'a> { // wasm magic number if source.is_web_assembly() { return Some(ParseResult::empty_with( + arena, source.clone(), options::Loader::Wasm, input_fd, @@ -1720,18 +1722,23 @@ impl<'a> Transpiler<'a> { self.macro_context.as_mut().unwrap().javascript_object = this_parse.macro_js_ctx; } - opts.macro_context = self.macro_context.as_mut(); - // `crate::defines::Define` IS // `bun_js_parser::defines::Define`. Hand the parser the real // table so user `--define` values apply at parse time. - // SAFETY: `self.options.define` is `Box` owned by the - // long-lived `Transpiler`; the parser borrows it for `'a` - // (arena lifetime). Erase to `'a` to satisfy - // `JavaScript::parse`'s `&'a Define` param — the box is never - // dropped while a parse is in flight (Zig held `*const Define`). - let define: &'a js_ast::defines::Define = - unsafe { &*(&raw const *self.options.define) }; + // SAFETY: `self.options.define` / `self.macro_context` are + // owned by the long-lived `Transpiler`; the parser borrows + // them for `'a` (arena lifetime). Erase to `'a` so the + // returned `Ast<'a>` is not pinned to the `&mut self` borrow + // — neither field is dropped while a parse is in flight + // (Zig held `*const Define` / `*MacroContext`). + let define: &'a js_ast::defines::Define; + unsafe { + define = &*(&raw const *self.options.define); + opts.macro_context = self + .macro_context + .as_mut() + .map(|m| &mut *core::ptr::from_mut(m)); + } // PORT NOTE: spec calls `transpiler.resolver.caches.js.parse`. // The resolver-side `cache::JavaScript` is a fieldless @@ -1870,6 +1877,7 @@ impl<'a> Transpiler<'a> { loader, input_fd, source_backing, + arena, &path, self.options.target, log, @@ -1899,12 +1907,12 @@ impl<'a> Transpiler<'a> { #[cold] #[inline(never)] -fn parse_data_loader( +fn parse_data_loader<'a>( source: &bun_ast::Source, loader: options::Loader, input_fd: Option, source_backing: resolver::cache::Contents, - arena: &Arena, + arena: &'a Arena, log: &mut bun_ast::Log, keep_json_and_toml_as_one_statement: bool, ) -> Option> { @@ -2130,8 +2138,8 @@ fn parse_data_loader( }]); } }; + let mut ast = bun_ast::Ast::from_parts(parts, arena); ast.symbols = bun_alloc::vec_from_iter_in(symbols.into_iter(), arena); - ast.symbols = bun_ast::symbol::List::from_owned_slice(symbols.into_boxed_slice()); return Some(ParseResult { ast, @@ -2148,12 +2156,12 @@ fn parse_data_loader( #[cold] #[inline(never)] -fn parse_text_loader( +fn parse_text_loader<'a>( source: &bun_ast::Source, loader: options::Loader, input_fd: Option, source_backing: resolver::cache::Contents, - arena: &Arena, + arena: &'a Arena, ) -> Option> { let expr = bun_ast::Expr::init( bun_ast::E::EString::init(&source.contents), @@ -2191,12 +2199,12 @@ fn parse_text_loader( #[cold] #[inline(never)] -fn parse_md_loader( +fn parse_md_loader<'a>( source: &bun_ast::Source, loader: options::Loader, input_fd: Option, source_backing: resolver::cache::Contents, - arena: &Arena, + arena: &'a Arena, log: &mut bun_ast::Log, ) -> Option> { let html: &'static [u8] = match bun_md::root::render_to_html(&source.contents) { @@ -2249,11 +2257,12 @@ fn parse_md_loader( #[cold] #[inline(never)] -fn parse_wasm_loader( +fn parse_wasm_loader<'a>( source: &bun_ast::Source, loader: options::Loader, input_fd: Option, source_backing: resolver::cache::Contents, + arena: &'a Arena, path: &bun_paths::fs::Path<'static>, target: options::Target, log: &mut bun_ast::Log, @@ -2376,8 +2385,12 @@ impl<'a> Transpiler<'a> { // take the column out (the printer never reads `tree.symbols`; it // walks `symbols` exclusively — `rg tree.symbols js_printer/lib.rs` is // empty). `init_with_one_list` boxes the single inner list. - let symbols = bun_ast::symbol::Map::init_with_one_list(core::mem::replace(&mut ast.symbols, Vec::new_in(arena)).into_iter().collect()); - let symbols = bun_ast::symbol::Map::init_with_one_list(core::mem::take(&mut ast.symbols)); + let arena = *ast.symbols.allocator(); + let symbols = bun_ast::symbol::Map::init_with_one_list( + core::mem::replace(&mut ast.symbols, Vec::new_in(arena)) + .into_iter() + .collect(), + ); // `runtime_imports` is now forwarded — after Round-G `Ast.runtime_imports` // is the real `parser::Runtime::Imports`, the same type @@ -3300,7 +3313,7 @@ pub struct BuildResolveResultPair { } impl Default for BuildResolveResultPair { - pub fn empty(arena: &'a bun_alloc::Arena) -> Self { + fn default() -> Self { Self { written: 0, input_fd: None, diff --git a/src/css/dependencies.rs b/src/css/dependencies.rs index 46b14ed6291..ea6f47cfb93 100644 --- a/src/css/dependencies.rs +++ b/src/css/dependencies.rs @@ -155,14 +155,11 @@ impl UrlDependency { bump: &'bump bun_alloc::Arena, url: &crate::values::url::Url, filename: &[u8], - import_records: &Vec, + import_records: &[bun_ast::ImportRecord], ) -> UrlDependency { // TODO(port): `bun_paths::fs::Path::pretty` is currently `&'static str`; // should become `&[u8]` per PORTING.md §Strings. Until then, `.as_bytes()`. - let theurl: &[u8] = import_records - .at(url.import_record_idx as usize) - .path - .pretty; + let theurl: &[u8] = import_records[url.import_record_idx as usize].path.pretty; let placeholder = crate::css_modules::hash( bump, format_args!("{}_{}", bstr::BStr::new(filename), bstr::BStr::new(theurl)), diff --git a/src/css/printer.rs b/src/css/printer.rs index 5ceb923989d..fa286e5dcb3 100644 --- a/src/css/printer.rs +++ b/src/css/printer.rs @@ -94,7 +94,7 @@ pub use css::targets::Features; #[derive(Clone, Copy)] pub struct ImportInfo<'a> { - pub import_records: &'a Vec, + pub import_records: &'a [ImportRecord], /// bundle_v2.graph.ast.items(.url_for_css) pub ast_urls_for_css: &'a [&'a [u8]], /// bundle_v2.graph.input_files.items(.unique_key_for_additional_file) @@ -104,7 +104,7 @@ pub struct ImportInfo<'a> { impl<'a> ImportInfo<'a> { /// Only safe to use when outside the bundler. As in, the import records /// were not resolved to source indices. This will out-of-bounds otherwise. - pub fn init_outside_of_bundler(records: &'a Vec) -> ImportInfo<'a> { + pub fn init_outside_of_bundler(records: &'a [ImportRecord]) -> ImportInfo<'a> { ImportInfo { import_records: records, ast_urls_for_css: &[], @@ -337,7 +337,7 @@ impl<'a> Printer<'a> { } #[inline] - pub fn get_import_records(&mut self) -> PrintResult<&'a Vec> { + pub fn get_import_records(&mut self) -> PrintResult<&'a [ImportRecord]> { if let Some(info) = &self.import_info { return Ok(info.import_records); } @@ -346,7 +346,7 @@ impl<'a> Printer<'a> { pub fn print_import_record(&mut self, import_record_idx: u32) -> PrintResult<()> { if let Some(info) = &self.import_info { - let import_record = info.import_records.at(import_record_idx as usize); + let import_record = &info.import_records[import_record_idx as usize]; let [a, b] = bun_core::cheap_prefix_normalizer(self.public_path, &import_record.path.text); // PORT NOTE: reshaped for borrowck — copied (a, b) out before re-borrowing &mut self @@ -363,7 +363,7 @@ impl<'a> Printer<'a> { #[inline] pub fn import_record(&mut self, import_record_idx: u32) -> PrintResult<&ImportRecord> { if let Some(info) = &self.import_info { - return Ok(info.import_records.at(import_record_idx as usize)); + return Ok(&info.import_records[import_record_idx as usize]); } Err(self.add_no_import_record_error()) } @@ -373,7 +373,7 @@ impl<'a> Printer<'a> { let Some(import_info) = &self.import_info else { return Err(self.add_no_import_record_error()); }; - let record = import_info.import_records.at(import_record_idx as usize); + let record = &import_info.import_records[import_record_idx as usize]; if record.source_index.is_valid() { // It has an inlined url for CSS let urls_for_css = import_info.ast_urls_for_css[record.source_index.get() as usize]; diff --git a/src/css/properties/custom.rs b/src/css/properties/custom.rs index 19ef4ef4ead..21526eda403 100644 --- a/src/css/properties/custom.rs +++ b/src/css/properties/custom.rs @@ -347,8 +347,7 @@ impl TokenList { && !url.is_absolute(dest.get_import_records()?) { let pretty = std::ptr::from_ref::<[u8]>( - dest.get_import_records()? - .at(url.import_record_idx as usize) + dest.get_import_records()?[url.import_record_idx as usize] .path .pretty, ); diff --git a/src/css/values/url.rs b/src/css/values/url.rs index a201a57ef29..a89e1dbc7c8 100644 --- a/src/css/values/url.rs +++ b/src/css/values/url.rs @@ -27,11 +27,8 @@ impl Url { } /// Returns whether the URL is absolute, and not relative. - pub fn is_absolute(&self, import_records: &Vec) -> bool { - let url: &[u8] = import_records - .at(self.import_record_idx as usize) - .path - .pretty; + pub fn is_absolute(&self, import_records: &[ImportRecord]) -> bool { + let url: &[u8] = import_records[self.import_record_idx as usize].path.pretty; // Quick checks. If the url starts with '.', it is relative. if strings::starts_with_char(url, b'.') { From beccfc24210dd1b8ffcefc37d1a704ac14716c64 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 18 May 2026 06:22:34 +0000 Subject: [PATCH 09/32] runtime/jsc: thread arena lifetime through ParseOptions/ParseResult callers - ParseOptions splits arena lifetime from short-lived input borrows - DevServer CurrentBundle owns the boxed arena bv2.graph.heap borrows - JSTranspiler/jsc_hooks reuse the existing per-call arena erasure for ParseOptions.arena - AsyncModule/js_bundle_completion_task adapt to borrowed Graph.heap --- src/bundler/transpiler.rs | 14 +++--- src/jsc/AsyncModule.rs | 29 +++++------ src/jsc/RuntimeTranspilerStore.rs | 2 +- src/jsc/lib.rs | 1 + src/runtime/api/JSTranspiler.rs | 34 +++++++------ src/runtime/api/js_bundle_completion_task.rs | 10 ++-- src/runtime/bake/DevServer.rs | 48 +++++++------------ .../bake/dev_server/incremental_graph.rs | 8 ++-- .../cli/create/SourceFileProjectGenerator.rs | 2 +- src/runtime/cli/repl.rs | 8 +++- src/runtime/cli/test/ChangedFilesFilter.rs | 2 +- src/runtime/jsc_hooks.rs | 9 +++- src/runtime/test_runner/snapshot.rs | 2 +- 13 files changed, 80 insertions(+), 89 deletions(-) diff --git a/src/bundler/transpiler.rs b/src/bundler/transpiler.rs index d124cbc1d45..9f61ae85fce 100644 --- a/src/bundler/transpiler.rs +++ b/src/bundler/transpiler.rs @@ -963,14 +963,14 @@ impl<'a> ParseResult<'a> { } /// Port of `transpiler.zig:Transpiler.ParseOptions`. -pub struct ParseOptions<'a> { +pub struct ParseOptions<'a, 'b> { pub arena: &'a Arena, pub dirname_fd: FD, pub file_descriptor: Option, pub file_hash: Option, /// On exception, we might still want to watch the file. - pub file_fd_ptr: Option<&'a mut FD>, + pub file_fd_ptr: Option<&'b mut FD>, pub path: bun_paths::fs::Path<'static>, pub loader: options::Loader, @@ -979,7 +979,7 @@ pub struct ParseOptions<'a> { pub jsx: crate::options_impl::jsx::Pragma, pub macro_remappings: MacroRemap, pub macro_js_ctx: MacroJSCtx, - pub virtual_source: Option<&'a bun_ast::Source>, + pub virtual_source: Option<&'b bun_ast::Source>, /// Zig: `runtime.Runtime.Features.ReplaceableExport.Map`. pub replace_exports: bun_collections::StringArrayHashMap, pub inject_jest_globals: bool, @@ -997,7 +997,7 @@ pub struct ParseOptions<'a> { /// See: https://nodejs.org/api/packages.html#type pub module_type: options::ModuleType, - pub runtime_transpiler_cache: Option<&'a mut RuntimeTranspilerCache>, + pub runtime_transpiler_cache: Option<&'b mut RuntimeTranspilerCache>, pub keep_json_and_toml_as_one_statement: bool, pub allow_bytecode_cache: bool, @@ -1376,7 +1376,7 @@ impl<'a> Transpiler<'a> { pub fn parse( &mut self, - this_parse: ParseOptions<'a>, + this_parse: ParseOptions<'a, '_>, client_entry_point_: Option<&mut EntryPoints::ClientEntryPoint>, ) -> Option> { self.parse_maybe_return_file_only::(this_parse, client_entry_point_) @@ -1384,7 +1384,7 @@ impl<'a> Transpiler<'a> { pub fn parse_maybe_return_file_only( &mut self, - this_parse: ParseOptions<'a>, + this_parse: ParseOptions<'a, '_>, client_entry_point_: Option<&mut EntryPoints::ClientEntryPoint>, ) -> Option> { self.parse_maybe_return_file_only_allow_shared_buffer::( @@ -1398,7 +1398,7 @@ impl<'a> Transpiler<'a> { const USE_SHARED_BUFFER: bool, >( &mut self, - mut this_parse: ParseOptions<'a>, + mut this_parse: ParseOptions<'a, '_>, // TODO(port): Zig `anytype` + `@hasField(.., "source")` — only ever // called with `?*EntryPoints.ClientEntryPoint` in this file. If other // callers pass a different type, introduce a `ClientEntryPointLike` diff --git a/src/jsc/AsyncModule.rs b/src/jsc/AsyncModule.rs index 51441e75e1a..99eb6e5a97f 100644 --- a/src/jsc/AsyncModule.rs +++ b/src/jsc/AsyncModule.rs @@ -26,7 +26,7 @@ use crate::{ bun_core::declare_scope!(AsyncModule, hidden); pub struct InitOpts<'a> { - pub parse_result: ParseResult, + pub parse_result: ParseResult<'static>, pub referrer: &'a [u8], pub specifier: &'a [u8], pub path: Fs::Path<'a>, @@ -40,7 +40,7 @@ pub struct InitOpts<'a> { pub struct AsyncModule { // This is all the state used by the printer to print the module - pub parse_result: ParseResult, + pub parse_result: ParseResult<'static>, pub promise: StrongOptional, // Strong.Optional, default .empty /// Packed `referrer ++ specifier ++ path.text`. Owns the bytes that the /// Zig version aliased via `buf.allocatedSlice()`. Stored as offsets so @@ -938,11 +938,7 @@ impl AsyncModule { ); let location = bun_ast::range_data( Some(&self.parse_result.source), - self.parse_result - .ast - .import_records - .at(import_record_id as usize) - .range, + self.parse_result.ast.import_records[import_record_id as usize].range, b"", ) .location @@ -1162,11 +1158,7 @@ impl AsyncModule { let location = bun_ast::range_data( Some(&self.parse_result.source), - self.parse_result - .ast - .import_records - .at(import_record_id as usize) - .range, + self.parse_result.ast.import_records[import_record_id as usize].range, b"", ) .location @@ -1175,10 +1167,7 @@ impl AsyncModule { global_this, b"specifier", ZigString::from_bytes( - self.parse_result - .ast - .import_records - .at(import_record_id as usize) + self.parse_result.ast.import_records[import_record_id as usize] .path .text, ) @@ -1241,7 +1230,9 @@ impl AsyncModule { // back. Rust takes-by-value via `mem::take` then restores below to // satisfy borrowck around `linker.link(&mut parse_result)` while // `self` is also borrowed. - let mut parse_result = core::mem::take(&mut self.parse_result); + let arena = *self.parse_result.ast.parts.allocator(); + let mut parse_result = + core::mem::replace(&mut self.parse_result, ParseResult::empty(arena)); // SAFETY: `string_buf` is a `Box<[u8]>` whose backing allocation is // stable for the lifetime of `*self`; this fn never replaces it, so // slices into it remain valid across the `&mut self` reborrows below @@ -1300,7 +1291,9 @@ impl AsyncModule { let is_commonjs_module = self.parse_result.ast.has_commonjs_export_names || self.parse_result.ast.exports_kind == bun_ast::ExportsKind::Cjs; let input_fd = self.parse_result.input_fd; - let parse_result = core::mem::take(&mut self.parse_result); + let arena = *self.parse_result.ast.parts.allocator(); + let parse_result = + core::mem::replace(&mut self.parse_result, ParseResult::empty(arena)); // PORT NOTE: `VirtualMachine.source_code_printer` is a thread-local // `?*BufferPrinter` (see `SOURCE_CODE_PRINTER`); Zig dereferenced to diff --git a/src/jsc/RuntimeTranspilerStore.rs b/src/jsc/RuntimeTranspilerStore.rs index a860ae860d0..39a95de2e6f 100644 --- a/src/jsc/RuntimeTranspilerStore.rs +++ b/src/jsc/RuntimeTranspilerStore.rs @@ -1063,7 +1063,7 @@ impl TranspilerJob { return; } - for import_record in parse_result.ast.import_records.slice_mut() { + for import_record in parse_result.ast.import_records.as_mut_slice() { let import_record: &mut ImportRecord = import_record; if let Some(replacement) = HardcodedAlias::get( diff --git a/src/jsc/lib.rs b/src/jsc/lib.rs index 277c158d5c3..0ac87499e2b 100644 --- a/src/jsc/lib.rs +++ b/src/jsc/lib.rs @@ -3,6 +3,7 @@ //! //! Web and runtime-specific APIs should go in `bun.webcore` and `bun.api`. //! +#![feature(allocator_api)] //! LAYERING: `jsc.zig` carries deprecated aliases `WebCore = bun.webcore`, //! `API = bun.api`, `Node = bun.api.node`, `Subprocess = bun.api.Subprocess`. //! In the Rust crate graph those targets live in `bun_runtime`, which depends diff --git a/src/runtime/api/JSTranspiler.rs b/src/runtime/api/JSTranspiler.rs index 8795aa1f12b..36d25e447d2 100644 --- a/src/runtime/api/JSTranspiler.rs +++ b/src/runtime/api/JSTranspiler.rs @@ -766,7 +766,6 @@ impl<'a> TransformTask<'a> { pub fn run(&mut self) { let name = self.loader.stdin_name(); - let source = bun_ast::Source::init_path_string(name, self.input_code.slice()); // PERF(port): was MimallocArena bulk-free — profile if hot. let arena = Arena::new(); @@ -799,8 +798,10 @@ impl<'a> TransformTask<'a> { // SAFETY: `arena` outlives every use through `self.transpiler` in this fn body; // Transpiler<'static> forces the borrow to 'static, so launder through a raw ptr. - self.transpiler - .set_arena(unsafe { bun_ptr::detach_lifetime_ref(&arena) }); + let arena_ref: &'static Arena = unsafe { bun_ptr::detach_lifetime_ref(&arena) }; + let source: &bun_ast::Source = + arena_ref.alloc(bun_ast::Source::init_path_string(name, self.input_code.slice())); + self.transpiler.set_arena(arena_ref); self.transpiler.set_log(&raw mut self.log); // self.log.msgs.allocator = bun.default_allocator → no-op @@ -812,14 +813,14 @@ impl<'a> TransformTask<'a> { }; let parse_options = ParseOptions { - arena: &arena, + arena: arena_ref, macro_remappings: clone_macro_map(&self.macro_map), dirname_fd: bun_sys::Fd::INVALID, file_descriptor: None, loader: self.loader, jsx, path: source.path.clone(), - virtual_source: Some(&source), + virtual_source: Some(source), replace_exports: self.replace_exports.entries.clone().expect("OOM"), experimental_decorators: self.tsconfig.map_or(false, |ts| ts.experimental_decorators), emit_decorator_metadata: self.tsconfig.map_or(false, |ts| ts.emit_decorator_metadata), @@ -1258,11 +1259,11 @@ impl JSTranspiler { fn get_parse_result( &self, - arena: &Arena, + arena: &'static Arena, code: &[u8], loader: Option, macro_js_ctx: MacroJSCtx, - ) -> Option { + ) -> Option> { let config = self.config.get(); let name = config.default_loader.stdin_name(); @@ -1284,7 +1285,8 @@ impl JSTranspiler { code }; - let source = bun_ast::Source::init_path_string(name, processed_code); + let source: &bun_ast::Source = + arena.alloc(bun_ast::Source::init_path_string(name, processed_code)); let jsx = match config.tsconfig.as_deref() { Some(ts) => ts @@ -1294,14 +1296,14 @@ impl JSTranspiler { }; let parse_options = ParseOptions { - arena: arena, + arena, macro_remappings: clone_macro_map(&config.macro_map), dirname_fd: bun_sys::Fd::INVALID, file_descriptor: None, loader: loader.unwrap_or(config.default_loader), jsx, path: source.path.clone(), - virtual_source: Some(&source), + virtual_source: Some(source), replace_exports: config.runtime.replace_exports.entries.clone().expect("OOM"), macro_js_ctx, experimental_decorators: config @@ -1368,9 +1370,10 @@ impl JSTranspiler { // `_restore` (declared after `arena`/`log`, so dropped first) restores // `prev_arena` and `&self.config.log` before either local drops. // `with_mut` borrow is closure-scoped; no JS re-entry inside. + let arena_ref: &'static Arena = unsafe { bun_ptr::detach_lifetime_ref(&arena) }; let prev_arena = self.transpiler.with_mut(|t| { let prev = t.arena; - t.set_arena(unsafe { bun_ptr::detach_lifetime_ref(&arena) }); + t.set_arena(arena_ref); t.set_log(&raw mut log); prev }); @@ -1384,7 +1387,7 @@ impl JSTranspiler { let mut ast_memory_allocator = bun_ast::ASTMemoryAllocator::new(&arena); let _ast_scope = ast_memory_allocator.enter(); - let parse_result = self.get_parse_result(&arena, code, loader, MacroJSCtx::ZERO); + let parse_result = self.get_parse_result(arena_ref, code, loader, MacroJSCtx::ZERO); let log_ref = self.transpiler.get().log_mut(); let Some(mut parse_result) = parse_result else { if (log_ref.warnings + log_ref.errors) > 0 { @@ -1401,7 +1404,7 @@ impl JSTranspiler { let imports_label = ZigString::static_(b"imports"); let named_imports_value = named_imports_to_js( global, - parse_result.ast.import_records.slice(), + parse_result.ast.import_records.as_slice(), self.config.get().trim_unused_imports.unwrap_or(false), )?; @@ -1557,11 +1560,12 @@ impl JSTranspiler { // `_restore` (declared after `arena`/`log`, so dropped first) restores // `prev_arena`, `&self.config.log`, and `prev_macro_context` before either drops. // `with_mut` borrow is closure-scoped; no JS re-entry inside. + let arena_ref: &'static Arena = unsafe { bun_ptr::detach_lifetime_ref(&arena) }; let (prev_arena, prev_macro_context) = self.transpiler.with_mut(|t| { let prev_arena = t.arena; // `take()` both reads the prior value AND nulls it (spec: `macro_context = null`). let prev_mc = t.macro_context.take(); - t.set_arena(unsafe { bun_ptr::detach_lifetime_ref(&arena) }); + t.set_arena(arena_ref); t.set_log(&raw mut log); (prev_arena, prev_mc) }); @@ -1574,7 +1578,7 @@ impl JSTranspiler { // `MacroJSCtx` carries the encoded `JSValue` bits (`#[repr(transparent)] i64`). let macro_js_ctx: MacroJSCtx = MacroJSCtx(js_ctx_value.0 as i64); - let parse_result = self.get_parse_result(&arena, code, loader, macro_js_ctx); + let parse_result = self.get_parse_result(arena_ref, code, loader, macro_js_ctx); let log_ref = self.transpiler.get().log_mut(); let Some(parse_result) = parse_result else { if (log_ref.warnings + log_ref.errors) > 0 { diff --git a/src/runtime/api/js_bundle_completion_task.rs b/src/runtime/api/js_bundle_completion_task.rs index 62399af0610..2fc3a187c51 100644 --- a/src/runtime/api/js_bundle_completion_task.rs +++ b/src/runtime/api/js_bundle_completion_task.rs @@ -1102,12 +1102,6 @@ impl CompletionStruct for JSBundleCompletionTask { let event_loop: bun_bundler::linker_context_mod::EventLoop = Some(NonNull::from(&mut any_loop).cast::>()); - // Zig passed the same `heap` by value (mimalloc handle struct copy); - // bumpalo arenas can't be aliased that way, so `BundleV2` owns its - // own arena (its only consumer is `linker.graph.bump`, repointed in - // `BundleV2::init`). Transpiler/AST allocations stay in `bump`. - let heap = Arena::new(); - // `thread_pool` is the `WorkPool` singleton (`OnceLock`-backed, // process-lifetime, concurrently read by worker threads). Do NOT // materialize `&mut` from it — its provenance is `&'static`, so even a @@ -1115,7 +1109,9 @@ impl CompletionStruct for JSBundleCompletionTask { // (`NonNull`) end-to-end; `ThreadPool::init` stores it as `*mut`. let worker_pool = NonNull::new(thread_pool); - let mut bv2 = BundleV2::init(transpiler, None, bump, event_loop, false, worker_pool, heap)?; + // Zig passed the same `heap` by value (mimalloc handle struct copy); + // `Graph.heap` is now a borrow, so reuse the caller-owned `bump`. + let mut bv2 = BundleV2::init(transpiler, None, bump, event_loop, false, worker_pool, bump)?; bv2.plugins = self.plugins(); bv2.completion = Some(self.as_js_bundle_completion_task()); diff --git a/src/runtime/bake/DevServer.rs b/src/runtime/bake/DevServer.rs index 493e1b9b573..71b54082160 100644 --- a/src/runtime/bake/DevServer.rs +++ b/src/runtime/bake/DevServer.rs @@ -276,6 +276,9 @@ pub struct CurrentBundle { /// make `DevServer` self-referential; raw-ptr aliasing inside `BundleV2` /// already encodes that contract. pub bv2: Box>, + /// Owns the arena that `bv2.graph.heap` borrows (`'static` self-ref via the + /// boxed allocation's stable address; same erasure as `bv2` above). + pub heap: Box, /// Information BundleV2 needs to finalize the bundle pub start_data: bundler::bundle_v2::DevServerInput, /// Started when the bundle was queued @@ -2848,7 +2851,7 @@ impl DevServer { fn generate_javascript_code_for_html_file( &mut self, index: bun_ast::Index, - import_records: &[Vec], + import_records: &[bun_ast::import_record::List<'_>], input_file_sources: &[bun_ast::Source], loaders: &[Loader], ) -> Result, bun_core::Error> { @@ -2863,7 +2866,7 @@ impl DevServer { )?; w.extend_from_slice(b": [ ["); let mut any = false; - for import in import_records[index.get() as usize].slice() { + for import in import_records[index.get() as usize].as_slice() { if import.source_index.is_valid() { if !loaders[import.source_index.get() as usize].is_javascript_like() { continue; // ignore non-JavaScript imports @@ -3156,16 +3159,11 @@ impl DevServer { server.on_pending_request(); } - let heap = bun_alloc::MimallocArena::new(); - // TODO(port): heap is moved into BundleV2; errdefer heap.deinit() handled by Drop - // PORT NOTE: `MimallocArena = bumpalo::Bump` (no `.arena()` accessor); - // `Bump::alloc` is the inherent method, and `BundleV2::init`'s `alloc` - // param is `&bun_alloc::Arena` (== `&Bump`). + // Boxed so its address is stable: `bv2.graph.heap: &'static Arena` + // borrows it (self-ref via `CurrentBundle`, see PORT NOTE on + // `CurrentBundle.bv2`). + let heap: Box = Box::new(bun_alloc::MimallocArena::new()); // TODO(port): ASTMemoryAllocator scope — bake is an AST crate; arena threading required - // PORT NOTE: `heap.alloc` returns `&mut T` borrowing `heap`, but `heap` is - // later moved into `bv2.graph.heap`. Bumpalo chunk storage is heap-allocated - // and stable across the move of the `Bump` handle, so erase the borrow to a - // raw pointer (same rationale as `event_loop` below). let ast_memory_store: *mut bun_ast::ASTMemoryAllocator = heap.alloc(bun_ast::ASTMemoryAllocator::default()); // SAFETY: the `ASTMemoryAllocator` lives in a bumpalo chunk owned by @@ -3184,20 +3182,10 @@ impl DevServer { bun_event_loop::AnyEventLoop::js(self.vm().event_loop().cast()), ))); - // PORT NOTE: `BundleV2::init` consumes `heap` and also wants - // `alloc: &Arena` derived from it. Zig's `heap.arena()` is a - // `Copy` vtable handle that survives the move; in Rust the `Bump` is - // moved into `bv2.graph.heap`, so any pre-move borrow would dangle. - // `BundleV2::init` itself re-derives `linker.graph.bump = &this.graph - // .heap` internally and only uses `alloc` for short-lived setup — - // pass the heap's address via raw pointer (it lives at a stable - // `Box`-interior slot once `init` writes it). - // - // SAFETY: `heap_ptr` is read by `BundleV2::init` only after `heap` is - // moved into `this.graph.heap` (same allocation, stable address inside - // the freshly-`Box::new`'d `BundleV2`). The borrow is scoped to the - // call; we never reuse `heap_ptr` after `init` returns. - let heap_ptr: *const bun_alloc::Arena = &raw const heap; + // SAFETY: `heap` is `Box`-allocated above and moved into + // `CurrentBundle` (which also owns `bv2`); the boxed arena's address + // is stable for the lifetime of `bv2.graph.heap`'s borrow. + let heap_ptr: *const bun_alloc::Arena = &raw const *heap; // PORT NOTE: split `&mut self` into disjoint field reborrows so // `server_transpiler` (`&'a mut`) and `client/ssr_transpiler` // (NonNull) don't trip the single-`&mut self` rule. @@ -3224,7 +3212,8 @@ impl DevServer { Some(::core::ptr::NonNull::from( bun_threading::work_pool::WorkPool::get(), )), - heap, + // SAFETY: see `heap_ptr` note above. + unsafe { &*heap_ptr }, )?; bv2.bun_watcher = Some(::core::ptr::NonNull::from(&mut **self.bun_watcher)); bv2.asynchronous = true; @@ -3257,6 +3246,7 @@ impl DevServer { drop(entry_points); self.current_bundle = Some(CurrentBundle { bv2, + heap, timer, start_data, had_reload_event, @@ -3671,7 +3661,7 @@ pub struct HotUpdateContext<'a> { /// bundle_v2.Graph.input_files.items(.source) pub sources: &'a [bun_ast::Source], /// bundle_v2.Graph.ast.items(.import_records) - pub import_records: &'a [Vec], + pub import_records: &'a [bun_ast::import_record::List<'a>], /// bundle_v2.Graph.server_component_boundaries.slice() pub scbs: bun_ast::server_component_boundary::Slice<'a>, /// bundle_v2.Graph.input_files.items(.loader) @@ -3762,15 +3752,13 @@ pub fn finalize_bundle( // SAFETY: `dev`/`bv2` are `&mut` params; both outlive this fn-scoped guard. let dev = unsafe { &mut *dev_ptr_outer }; let bv2 = unsafe { &mut *bv2_ptr_outer }; - // TODO(port): heap moved out before deinit - let mut heap = ::core::mem::replace(&mut bv2.graph.heap, bun_alloc::Arena::new()); bv2.deinit_without_freeing_arena(); if let Some(cb) = &mut dev.current_bundle { cb.promise.deinit_idempotently(); } + // Drops `CurrentBundle.heap` (the arena `bv2.graph.heap` borrows). dev.current_bundle = None; dev.log.clear_and_free(); - drop(heap); let _ = dev.assets.reindex_if_needed(); // not fatal diff --git a/src/runtime/bake/dev_server/incremental_graph.rs b/src/runtime/bake/dev_server/incremental_graph.rs index 6b245bf269c..6634c404a91 100644 --- a/src/runtime/bake/dev_server/incremental_graph.rs +++ b/src/runtime/bake/dev_server/incremental_graph.rs @@ -947,13 +947,13 @@ impl IncrementalGraph { debug_assert!(index.is_valid()); debug_assert!(!ctx.loaders[index.get() as usize].is_css()); - let records_len = ctx.import_records[index.get() as usize].slice().len(); + let records_len = ctx.import_records[index.get() as usize].len(); for i in 0..records_len { // PORT NOTE: snapshot the three fields we need so the shared borrow // on `ctx.import_records` ends before `process_edge_attachment` // takes `&mut ctx`. let (flags, src, key) = { - let ir = &ctx.import_records[index.get() as usize].slice()[i]; + let ir = &ctx.import_records[index.get() as usize].as_slice()[i]; ( ir.flags, ir.source_index, @@ -992,10 +992,10 @@ impl IncrementalGraph { queue.push(bundler_index); while let Some(idx) = queue.pop() { - let records_len = ctx.import_records[idx.get() as usize].slice().len(); + let records_len = ctx.import_records[idx.get() as usize].len(); for i in 0..records_len { let (flags, src, key) = { - let ir = &ctx.import_records[idx.get() as usize].slice()[i]; + let ir = &ctx.import_records[idx.get() as usize].as_slice()[i]; ( ir.flags, ir.source_index, diff --git a/src/runtime/cli/create/SourceFileProjectGenerator.rs b/src/runtime/cli/create/SourceFileProjectGenerator.rs index 1b22b59ae7e..c64401a1ec9 100644 --- a/src/runtime/cli/create/SourceFileProjectGenerator.rs +++ b/src/runtime/cli/create/SourceFileProjectGenerator.rs @@ -622,7 +622,7 @@ fn get_shadcn_components( match loaders[file.get() as usize] { bun_ast::Loader::Tsx | bun_ast::Loader::Jsx => { let import_records = &all[file.get() as usize]; - for import_record in import_records.slice() { + for import_record in import_records.as_slice() { if import_record.path.text.starts_with(b"@/components/ui/") { icons.insert(&import_record.path.text[b"@/components/ui/".len()..])?; } diff --git a/src/runtime/cli/repl.rs b/src/runtime/cli/repl.rs index 4c6a360049d..83f0416ecf2 100644 --- a/src/runtime/cli/repl.rs +++ b/src/runtime/cli/repl.rs @@ -1849,8 +1849,12 @@ impl<'a> Repl<'a> { // a stack 1-slot slice). `Map::init_with_one_list` takes ownership of // `ast.symbols` instead — see Symbol.rs PORT NOTE on the dangling-slice // hazard. - let symbols_map = - bun_ast::symbol::Map::init_with_one_list(core::mem::take(&mut ast.symbols)); + let arena = *ast.symbols.allocator(); + let symbols_map = bun_ast::symbol::Map::init_with_one_list( + core::mem::replace(&mut ast.symbols, Vec::new_in(arena)) + .into_iter() + .collect(), + ); if bun_js_printer::print_ast::< _, diff --git a/src/runtime/cli/test/ChangedFilesFilter.rs b/src/runtime/cli/test/ChangedFilesFilter.rs index cae903ccf38..fe0a9923408 100644 --- a/src/runtime/cli/test/ChangedFilesFilter.rs +++ b/src/runtime/cli/test/ChangedFilesFilter.rs @@ -230,7 +230,7 @@ pub fn filter<'a>( for (idx, records) in import_records.iter().enumerate() { let importer = u32::try_from(idx).unwrap(); - for record in records.slice() { + for record in records.as_slice() { let dep = record.source_index; if !dep.is_valid() || dep.is_runtime() { continue; diff --git a/src/runtime/jsc_hooks.rs b/src/runtime/jsc_hooks.rs index 0742952621a..e1355ad0a2a 100644 --- a/src/runtime/jsc_hooks.rs +++ b/src/runtime/jsc_hooks.rs @@ -2076,6 +2076,9 @@ fn transpile_source_code_inner( let mut arena: Box = unsafe { (*jsc_vm).module_loader.transpile_source_code_arena.take() } .unwrap_or_else(|| Box::new(bun_alloc::Arena::new())); + // Stable heap address (Box interior); survives the move into + // `arena_guard` and into the VM slot on give-back. + let arena_ptr: *const bun_alloc::Arena = &raw const *arena; // Route `AstAlloc` to `arena`'s `mi_heap_t*` (see the // `reset_store` note above). `_ast_scope.enter()` already nulled // `AST_HEAP`; this rebinds it to the heap that the parser scratch @@ -2461,7 +2464,9 @@ fn transpile_source_code_inner( } }; let parse_options = ParseOptions { - arena: &arena_guard.1, + // SAFETY: `arena_ptr` points at the `Box` interior + // held by `arena_guard`; the guard outlives `parse_result`. + arena: unsafe { &*arena_ptr }, path: parse_path, loader, dirname_fd: bun_sys::Fd::INVALID, @@ -2655,7 +2660,7 @@ fn transpile_source_code_inner( // — `Expr` lives in `bun_js_parser` (no JSC dep), so // the JS materialization is the `bun_js_parser_jsc` // extension fn. - let part = parse_result.ast.parts.at(0); + let part = &parse_result.ast.parts[0]; // SAFETY: `Part.stmts` is an arena-owned slice; the // arena outlives this call (returned to the VM by the // scopeguard above only after we return). diff --git a/src/runtime/test_runner/snapshot.rs b/src/runtime/test_runner/snapshot.rs index 63c6e0dcad0..4799b914d1e 100644 --- a/src/runtime/test_runner/snapshot.rs +++ b/src/runtime/test_runner/snapshot.rs @@ -278,7 +278,7 @@ impl<'a> Snapshots<'a> { // TODO: when common js transform changes, keep this updated or add flag to support this version - for part in ast.parts.slice_mut() { + for part in ast.parts.as_mut_slice() { // `part.stmts` is an arena-owned `StoreSlice`; arena outlives this // loop and `ast` is owned here, so unique access is upheld. for stmt in part.stmts.slice_mut() { From 44fc2ae46cc067386fd5f4d73a54926d4d398ba5 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 18 May 2026 06:43:41 +0000 Subject: [PATCH 10/32] bundler: re-seat linker-side parts/import_records onto Graph.heap in LinkerGraph::load Per-file PartList/import_record::List buffers come from per-worker mi_heaps, which mi_heap_malloc cannot grow from the linker thread. Bitwise-move them into the linker-thread arena alongside the existing symbol-map copy so add_part_to_file etc. can append. The parse-side alias keeps the original handle (slab-freed without element drop, same as before). --- src/bundler/LinkerContext.rs | 2 ++ src/bundler/LinkerGraph.rs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/bundler/LinkerContext.rs b/src/bundler/LinkerContext.rs index a5aa362598b..81b369815f4 100644 --- a/src/bundler/LinkerContext.rs +++ b/src/bundler/LinkerContext.rs @@ -557,6 +557,8 @@ impl<'a> LinkerContext<'a> { let sources: &[Source] = unsafe { (*self.parse_graph).input_files.items_source() }; self.graph.load( + // SAFETY: parse_graph backref just assigned above. + unsafe { (*self.parse_graph).heap }, entry_points, sources, server_component_boundaries, diff --git a/src/bundler/LinkerGraph.rs b/src/bundler/LinkerGraph.rs index 64875749546..ef154cd8e25 100644 --- a/src/bundler/LinkerGraph.rs +++ b/src/bundler/LinkerGraph.rs @@ -492,6 +492,7 @@ impl<'a> LinkerGraph<'a> { impl<'a> LinkerGraph<'a> { pub fn load( &mut self, + heap: &'a Arena, entry_points: &[Index], sources: &[bun_ast::Source], server_component_boundaries: &server_component_boundary::List, @@ -698,6 +699,35 @@ impl<'a> LinkerGraph<'a> { self.symbols = symbol::Map::init_list(symbols); } + // Re-seat the growable arena-backed columns onto `self.bump` (the + // linker-thread heap). `clone_ast` left each `PartList`/import-record + // list bitwise-aliasing the parse-side buffer in a per-worker + // `mi_heap`, which `add_part_to_file` / `generate_new_symbol`-adjacent + // pushes cannot grow from this thread. Same bitwise-move pattern as + // the symbol-map copy above: the linker side becomes the post-mutation + // owner (`take_ast_cols!` drops it once), the parse side keeps the + // original handle and is slab-freed without element drop. + { + macro_rules! reseat_col { + ($col:ident) => { + for v in self.ast.$col() { + let n = v.len(); + let mut new = Vec::with_capacity_in(n, heap); + // SAFETY: `new` has capacity `n`; `v` has `n` initialized + // elements. Bitwise-moved; the parse-side alias is never + // element-dropped (see `take_ast_cols!` in bundle_v2.rs). + unsafe { + core::ptr::copy_nonoverlapping(v.as_ptr(), new.as_mut_ptr(), n); + new.set_len(n); + core::ptr::write(v, new); + } + } + }; + } + reseat_col!(items_parts_mut); + reseat_col!(items_import_records_mut); + } + // TODO: const_values // { // var const_values = this.const_values; From e07627495b1d2a3d4f400a0b07ca6d0f9394845e Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 18 May 2026 07:13:03 +0000 Subject: [PATCH 11/32] bundler: zero-copy allocator retag in take_ast_ownership instead of eager re-seat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace LinkerGraph::load's reseat_col! (Vec::with_capacity_in + memcpy for every file's parts/import_records) with bun_alloc::transfer_arena — swap the ArenaVec's &Arena handle from the per-worker mi_heap to the bundle-thread heap via ManuallyDrop + from_raw_parts_in. Only files the linker actually grows pay a (lazy) cross-heap mi_heap_realloc migration. <&MimallocArena as Allocator>::deallocate is heap-agnostic mi_free, and grow is mi_heap_realloc_aligned(dst, ptr, ..) — alloc on dst, mi_free old — so retagging preserves the single-thread-alloc contract while matching Zig's BabyList.transferOwnership (release no-op there because BabyList is allocator-erased; Vec stores the handle, hence the swap). Drop the post-step-5 take_ast_ownership call: do_step_5 only pushes to global-allocator Vecs (Dependency, local_parts_with_uses), never to the arena-backed PartList/import-record columns. rolldown apps/10000 (--production --sourcemap, 8 runs): wall 520ms -> 501ms RSS 947MB -> 896MB vs bun-1.3.14: 433ms / 647MB --- src/bun_alloc/lib.rs | 27 +++++++++ src/bundler/LinkerContext.rs | 2 - src/bundler/LinkerGraph.rs | 57 +++++++------------ src/bundler/bundle_v2.rs | 2 +- .../linker_context/scanImportsAndExports.rs | 8 ++- 5 files changed, 55 insertions(+), 41 deletions(-) diff --git a/src/bun_alloc/lib.rs b/src/bun_alloc/lib.rs index 338bc606ee4..26352e5d9bd 100644 --- a/src/bun_alloc/lib.rs +++ b/src/bun_alloc/lib.rs @@ -270,6 +270,33 @@ pub type Bump = bumpalo::Bump; pub type ArenaVec<'a, T> = Vec; pub use mimalloc_arena::{ArenaString, ArenaVecExt, live_arena_heaps, vec_from_iter_in}; +/// Re-tag an [`ArenaVec`]'s allocator handle to `dst` without copying data. +/// +/// Zig parity: `BabyList.transferOwnership` (collections/baby_list.zig). Zig's +/// `BabyList` is allocator-erased — the linker passes a different allocator at +/// each `append(allocator, ..)` call site; the Rust port stores `&'a Arena` in +/// the `Vec`, so the equivalent is swapping that field. +/// +/// Sound because `<&MimallocArena as Allocator>` is heap-agnostic on the +/// existing buffer: +/// - `deallocate` → `mi_free(ptr)`: looks up the owning heap from the pointer's +/// page metadata; works from any thread on any heap's allocation. +/// - `grow`/`shrink` → `mi_heap_realloc_aligned(dst, ptr, ..)`: returns `ptr` +/// in-place if it fits (read-only `mi_usable_size`), else allocs on `dst`, +/// `memcpy`s, then `mi_free(ptr)`. +/// +/// The original arena is never `mi_heap_malloc`-ed from again via this `Vec`, +/// so the [`MimallocArena`] single-thread-alloc contract is preserved. +#[inline] +pub fn transfer_arena<'a, T>(v: &mut ArenaVec<'a, T>, dst: &'a MimallocArena) { + let mut old = core::mem::ManuallyDrop::new(core::mem::replace(v, Vec::new_in(dst))); + let (ptr, len, cap) = (old.as_mut_ptr(), old.len(), old.capacity()); + // SAFETY: see fn doc — `<&MimallocArena as Allocator>::{deallocate,grow}` + // are heap-agnostic on `ptr`; `(ptr, len, cap)` are the just-decomposed + // valid `Vec` triplet from `old`, whose `Drop` is suppressed. + *v = unsafe { Vec::from_raw_parts_in(ptr, len, cap, dst) }; +} + /// `bumpalo::format!` parity — `arena_format!(in arena, "...", ..)` → /// [`ArenaString`]. diff --git a/src/bundler/LinkerContext.rs b/src/bundler/LinkerContext.rs index 81b369815f4..a5aa362598b 100644 --- a/src/bundler/LinkerContext.rs +++ b/src/bundler/LinkerContext.rs @@ -557,8 +557,6 @@ impl<'a> LinkerContext<'a> { let sources: &[Source] = unsafe { (*self.parse_graph).input_files.items_source() }; self.graph.load( - // SAFETY: parse_graph backref just assigned above. - unsafe { (*self.parse_graph).heap }, entry_points, sources, server_component_boundaries, diff --git a/src/bundler/LinkerGraph.rs b/src/bundler/LinkerGraph.rs index ef154cd8e25..6ec2231e5c7 100644 --- a/src/bundler/LinkerGraph.rs +++ b/src/bundler/LinkerGraph.rs @@ -492,7 +492,6 @@ impl<'a> LinkerGraph<'a> { impl<'a> LinkerGraph<'a> { pub fn load( &mut self, - heap: &'a Arena, entry_points: &[Index], sources: &[bun_ast::Source], server_component_boundaries: &server_component_boundary::List, @@ -699,35 +698,6 @@ impl<'a> LinkerGraph<'a> { self.symbols = symbol::Map::init_list(symbols); } - // Re-seat the growable arena-backed columns onto `self.bump` (the - // linker-thread heap). `clone_ast` left each `PartList`/import-record - // list bitwise-aliasing the parse-side buffer in a per-worker - // `mi_heap`, which `add_part_to_file` / `generate_new_symbol`-adjacent - // pushes cannot grow from this thread. Same bitwise-move pattern as - // the symbol-map copy above: the linker side becomes the post-mutation - // owner (`take_ast_cols!` drops it once), the parse side keeps the - // original handle and is slab-freed without element drop. - { - macro_rules! reseat_col { - ($col:ident) => { - for v in self.ast.$col() { - let n = v.len(); - let mut new = Vec::with_capacity_in(n, heap); - // SAFETY: `new` has capacity `n`; `v` has `n` initialized - // elements. Bitwise-moved; the parse-side alias is never - // element-dropped (see `take_ast_cols!` in bundle_v2.rs). - unsafe { - core::ptr::copy_nonoverlapping(v.as_ptr(), new.as_mut_ptr(), n); - new.set_len(n); - core::ptr::write(v, new); - } - } - }; - } - reseat_col!(items_parts_mut); - reseat_col!(items_import_records_mut); - } - // TODO: const_values // { // var const_values = this.const_values; @@ -802,11 +772,28 @@ impl<'a> LinkerGraph<'a> { Ok(()) } - /// No-op: with `Vec` replaced by `Vec` (cat-4, BABYLIST_REPLACEMENT.md), - /// the parser hands the linker globally-owned `Vec`s directly — there is - /// nothing to "transfer". Kept as an empty fn so the call site in - /// `LinkerGraph::load` stays diff-stable; delete once that caller drops it. - pub fn take_ast_ownership(&mut self) {} + /// Port of `LinkerGraph.zig:takeAstOwnership`. `clone_ast` left each + /// `PartList`/import-record list with its allocator handle pointing at + /// the per-worker `mi_heap` that built it; re-tag to `heap` (the + /// bundle-thread arena) so linker-side `add_part_to_file` pushes call + /// `mi_heap_realloc_aligned(heap, worker_ptr, ..)` from the thread that + /// owns `heap`. Zero-copy: only files the linker actually grows pay a + /// (lazy, mimalloc-internal) cross-heap migration on first realloc. + /// + /// Zig is a release no-op because `BabyList` passes the allocator at each + /// `append` call site; the Rust `Vec` stores it, so swap here. + /// Zig also transfers `part.dependencies` and `symbols`; the Rust port + /// keeps `DependencyList` on the global allocator and feeds new symbols + /// through `self.symbols: symbol::Map` (also global) — both thread-safe + /// to grow, so no transfer needed. + pub fn take_ast_ownership(&mut self, heap: &'a Arena) { + for v in self.ast.items_import_records_mut() { + bun_alloc::transfer_arena(v, heap); + } + for v in self.ast.items_parts_mut() { + bun_alloc::transfer_arena(v, heap); + } + } pub fn propagate_async_dependencies(&mut self) -> Result<(), bun_core::Error> { // TODO(port): narrow error set diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index 25ded3959c1..87ca150097f 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -3285,7 +3285,7 @@ pub mod bv2_impl { // Some parts of the AST are owned by worker allocators at this point. // Transfer ownership to the graph heap. - self.linker.graph.take_ast_ownership(); + self.linker.graph.take_ast_ownership(self.graph.heap); Ok(()) } diff --git a/src/bundler/linker_context/scanImportsAndExports.rs b/src/bundler/linker_context/scanImportsAndExports.rs index b32d778f83a..3fd155f79b1 100644 --- a/src/bundler/linker_context/scanImportsAndExports.rs +++ b/src/bundler/linker_context/scanImportsAndExports.rs @@ -526,9 +526,11 @@ pub fn scan_imports_and_exports( ); } - // Some parts of the AST may now be owned by worker allocators. Transfer ownership back - // to the graph arena. - this.graph.take_ast_ownership(); + // Zig calls `takeAstOwnership` here because `doStep5` appends to + // `part.dependencies`/`declared_symbols` with the worker allocator. + // In the Rust port those are global-allocator `Vec`s (thread-safe to + // grow) and `do_step_5` never pushes to the arena-backed `PartList`/ + // import-record columns, so no transfer is needed. } if FeatureFlags::HELP_CATCH_MEMORY_ISSUES { From f2a0af66ba302c0ee0fd3b807fc0d9a1b836715f Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 18 May 2026 07:45:57 +0000 Subject: [PATCH 12/32] bundler: TLS-cache Worker in get_worker; lock only on first touch per (thread, pool) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keyed on a monotonic per-pool generation (not pool address — Bun.build() reuse makes pointer identity ABA). Drops the workers_assignments lock from the ~100K-per-build hot path to ~nthreads acquisitions; perf attributed ~97% of the build's futex traffic to the per-call lock on the rolldown 19K-module benchmark. Also drops the dead HELP_CATCH_MEMORY_ISSUES blocks in Worker::get/unget and the stale bumpalo references in this file. --- src/bundler/ThreadPool.rs | 57 +++++++++++++++++++++++++++++---------- src/bundler/lib.rs | 2 +- 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/src/bundler/ThreadPool.rs b/src/bundler/ThreadPool.rs index f1265d79b59..61eca172522 100644 --- a/src/bundler/ThreadPool.rs +++ b/src/bundler/ThreadPool.rs @@ -11,7 +11,7 @@ use core::mem::{ManuallyDrop, MaybeUninit}; use core::ptr::{self, NonNull}; use core::sync::atomic::{AtomicUsize, Ordering}; -use bun_alloc::Arena as ThreadLocalArena; // Zig: bun.allocators.MimallocArena → bumpalo::Bump +use bun_alloc::Arena as ThreadLocalArena; // Zig: bun.allocators.MimallocArena use bun_collections::VecExt; use bun_collections::{ArrayHashMap, MapEntry}; use bun_core::{self, FeatureFlags, env_var, output as Output}; @@ -63,6 +63,10 @@ pub struct ThreadPool { // would alias `&mut ThreadPool` across threads (UB before the lock is even // reached). pub workers_assignments: bun_threading::Guarded>, + /// Monotonic per-pool stamp for the [`TLS_WORKER`] fast-path key. Pointer + /// identity is unsound across `Bun.build()` calls (mimalloc reuses the + /// freed slot), so each pool draws a fresh `u64` from [`POOL_GENERATION`]. + pub generation: u64, // BACKREF (LIFETIMES.tsv row 170: ThreadPool.v2). `BundleV2` is generic // over `'a`; erase to `'static` behind the raw pointer like ParseTask.ctx. pub v2: *const BundleV2<'static>, @@ -85,6 +89,7 @@ impl Default for ThreadPool { worker_pool: ptr::null_mut(), worker_pool_is_owned: false, workers_assignments: bun_threading::Guarded::new(ArrayHashMap::default()), + generation: POOL_GENERATION.fetch_add(1, Ordering::Relaxed), v2: ptr::null(), } } @@ -256,6 +261,7 @@ impl ThreadPool { v2: std::ptr::from_ref(v2).cast::>(), worker_pool_is_owned: false, workers_assignments: bun_threading::Guarded::new(ArrayHashMap::default()), + generation: POOL_GENERATION.fetch_add(1, Ordering::Relaxed), } } @@ -406,7 +412,30 @@ impl ThreadPool { // Takes `&self` (not `&mut`) because this is called concurrently from // worker-pool threads via `Worker::get`; mutation goes through the // `bun_threading::Guarded` on `workers_assignments`. + // + // Fast path is a per-thread `(pool, worker)` cache: the map lookup is a + // pure `current_thread_id() → *mut Worker` re-read after first touch, so + // every subsequent call from the same thread for the same pool is a TLS + // load + pointer compare. The lock is only taken on first touch per + // `(thread, pool)` pair. Zig (ThreadPool.zig:180) takes the lock on every + // call; on a 19 K-module build that's ~100 K contended acquisitions, which + // perf attributes ~97 % of the build's futex traffic to. + #[inline] pub fn get_worker(&self, id: ThreadId) -> &'static mut Worker { + let (generation, worker) = TLS_WORKER.get(); + if generation == self.generation { + // SAFETY: cached by `get_worker_slow` on this thread; the Worker is + // heap-pinned (boxed in the slow path) and live while + // `self.generation` is — `deinit_soon` runs before the pool is + // dropped, and a new pool at the same address has a fresh + // generation, so a stale TLS entry never matches here. + return unsafe { &mut *worker }; + } + self.get_worker_slow(id) + } + + #[cold] + fn get_worker_slow(&self, id: ThreadId) -> &'static mut Worker { let worker: *mut Worker; { let mut map = self.workers_assignments.lock(); @@ -414,6 +443,7 @@ impl ThreadPool { MapEntry::Occupied(o) => { let w = *o.into_mut(); drop(map); + TLS_WORKER.set((self.generation, w)); // SAFETY: map only stores live heap-allocated Workers (inserted below). return unsafe { &mut *w }; } @@ -454,11 +484,21 @@ impl ThreadPool { stmt_list: None, }); (*worker).init(&*self.v2); + TLS_WORKER.set((self.generation, worker)); &mut *worker } } } +/// Per-thread cache for [`ThreadPool::get_worker`]. Keyed on +/// [`ThreadPool::generation`] (not the pool pointer — `Bun.build()` reuse makes +/// pointer identity ABA). `0` never matches a live pool. +#[thread_local] +static TLS_WORKER: core::cell::Cell<(u64, *mut Worker)> = + core::cell::Cell::new((0, core::ptr::null_mut())); + +static POOL_GENERATION: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(1); + // ─────────────────────────────────────────────────────────────────────────── // Worker // ─────────────────────────────────────────────────────────────────────────── @@ -621,8 +661,8 @@ impl Worker { worker.stmt_list = None; } // SAFETY: `ast_memory_store` is always a valid `ManuallyDrop` — - // `get_worker` unconditionally writes `ASTMemoryAllocator::default()` - // (which owns a live `bumpalo::Bump`), and `create()` may overwrite it + // `get_worker` unconditionally writes `ASTMemoryAllocator::default()`, + // and `create()` may overwrite it // via `*ast_memory_store = ...`. Dropped exactly once here, *outside* // the `has_created` guard so the default-constructed arena is freed // even when `create()` never ran (Zig left it `undefined`; Rust does @@ -658,21 +698,10 @@ impl Worker { worker.ast_memory_store.push(); - if FeatureFlags::HELP_CATCH_MEMORY_ISSUES { - // PORT NOTE: `MimallocArena::help_catch_memory_issues` collected - // mimalloc's deferred frees + zero-filled freed pages. The Rust - // arena is `bumpalo::Bump`, which has no equivalent — calls - // dropped, gated on the real `MimallocArena` un-gate - // (`bun_alloc/MimallocArena.rs` is ``). - } - worker } pub fn unget(&mut self) { - if FeatureFlags::HELP_CATCH_MEMORY_ISSUES { - // See `get()` — `help_catch_memory_issues` no-op while heap = Bump. - } self.ast_memory_store.pop(); } diff --git a/src/bundler/lib.rs b/src/bundler/lib.rs index 8f2c90c3180..077c6d8d06e 100644 --- a/src/bundler/lib.rs +++ b/src/bundler/lib.rs @@ -1,5 +1,5 @@ #![feature(inherent_associated_types)] -#![feature(adt_const_params, allocator_api)] +#![feature(adt_const_params, allocator_api, thread_local)] #![allow(incomplete_features)] // inherent_associated_types — used only for ThreadPool::Worker path compat with Zig #![allow( unused, From 3a5d33e0487d69cc7951daf58bf0644713646a52 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 07:47:59 +0000 Subject: [PATCH 13/32] [autofix.ci] apply automated fixes --- src/bun_alloc/lib.rs | 1 - src/bundler/AstBuilder.rs | 10 ++++++++-- src/bundler/LinkerContext.rs | 4 +--- src/bundler/ParseTask.rs | 6 ++---- src/bundler/ThreadPool.rs | 1 - src/bundler/barrel_imports.rs | 5 ++++- src/bundler/bundle_v2.rs | 16 ++++------------ .../linker_context/generateCodeForLazyExport.rs | 6 +++--- .../linker_context/scanImportsAndExports.rs | 3 ++- src/js_parser/parse/parse_entry.rs | 4 +--- src/jsc/AsyncModule.rs | 3 +-- src/runtime/api/JSTranspiler.rs | 6 ++++-- 12 files changed, 30 insertions(+), 35 deletions(-) diff --git a/src/bun_alloc/lib.rs b/src/bun_alloc/lib.rs index 26352e5d9bd..9bb3b1649aa 100644 --- a/src/bun_alloc/lib.rs +++ b/src/bun_alloc/lib.rs @@ -297,7 +297,6 @@ pub fn transfer_arena<'a, T>(v: &mut ArenaVec<'a, T>, dst: &'a MimallocArena) { *v = unsafe { Vec::from_raw_parts_in(ptr, len, cap, dst) }; } - /// `bumpalo::format!` parity — `arena_format!(in arena, "...", ..)` → /// [`ArenaString`]. #[macro_export] diff --git a/src/bundler/AstBuilder.rs b/src/bundler/AstBuilder.rs index 5ae0344e154..32cbaa5ff33 100644 --- a/src/bundler/AstBuilder.rs +++ b/src/bundler/AstBuilder.rs @@ -589,11 +589,17 @@ impl<'a, 'bump> AstBuilder<'a, 'bump> { Ok(crate::BundledAst { parts, module_scope: module_scope_value, - symbols: bun_alloc::vec_from_iter_in(core::mem::take(&mut self.symbols).into_iter(), self.bump), + symbols: bun_alloc::vec_from_iter_in( + core::mem::take(&mut self.symbols).into_iter(), + self.bump, + ), exports_ref: Ref::NONE, wrapper_ref: Ref::NONE, module_ref: self.module_ref, - import_records: bun_alloc::vec_from_iter_in(core::mem::take(&mut self.import_records).into_iter(), self.bump), + import_records: bun_alloc::vec_from_iter_in( + core::mem::take(&mut self.import_records).into_iter(), + self.bump, + ), export_star_import_records: Box::default(), approximate_newline_count: 1, exports_kind: ExportsKind::Esm, diff --git a/src/bundler/LinkerContext.rs b/src/bundler/LinkerContext.rs index a5aa362598b..fc055d0a6b1 100644 --- a/src/bundler/LinkerContext.rs +++ b/src/bundler/LinkerContext.rs @@ -745,9 +745,7 @@ impl<'a> LinkerContext<'a> { // `.unwrap()` mirrors Zig's untagged-union field reads (panic // on shape mismatch). let original_ref = unsafe { - (*self.graph.ast.items_parts()[html_import as usize] - [1] - .stmts)[0] + (*self.graph.ast.items_parts()[html_import as usize][1].stmts)[0] .data .s_lazy_export() .unwrap() diff --git a/src/bundler/ParseTask.rs b/src/bundler/ParseTask.rs index 090f61baa6f..2873b3019f1 100644 --- a/src/bundler/ParseTask.rs +++ b/src/bundler/ParseTask.rs @@ -1161,8 +1161,7 @@ pub mod parse_worker { b"", )? .unwrap(); - ast.import_records = - bun_alloc::vec_from_iter_in(import_records.into_iter(), bump); + ast.import_records = bun_alloc::vec_from_iter_in(import_records.into_iter(), bump); // We're banning import default of html loader files for now. // @@ -1290,8 +1289,7 @@ pub mod parse_worker { let mut ast = JSAst::init(lazy?.unwrap()); let css_ast_heap = crate::bundled_ast::CssAstRef::from_bump(bump.alloc(css_ast)); ast.css = Some(css_ast_heap); - ast.import_records = - bun_alloc::vec_from_iter_in(import_records.into_iter(), bump); + ast.import_records = bun_alloc::vec_from_iter_in(import_records.into_iter(), bump); return Ok(ast); } // TODO: diff --git a/src/bundler/ThreadPool.rs b/src/bundler/ThreadPool.rs index 61eca172522..2a4ad241535 100644 --- a/src/bundler/ThreadPool.rs +++ b/src/bundler/ThreadPool.rs @@ -702,7 +702,6 @@ impl Worker { } pub fn unget(&mut self) { - self.ast_memory_store.pop(); } diff --git a/src/bundler/barrel_imports.rs b/src/bundler/barrel_imports.rs index 50ae7111f2d..80b6685423e 100644 --- a/src/bundler/barrel_imports.rs +++ b/src/bundler/barrel_imports.rs @@ -191,7 +191,10 @@ fn apply_barrel_optimization_impl( for rec_idx in needed_records.keys() { if (*rec_idx as usize) < ast.import_records.len() { - needed_paths.put(ast.import_records.as_slice()[*rec_idx as usize].path.text, ())?; + needed_paths.put( + ast.import_records.as_slice()[*rec_idx as usize].path.text, + (), + )?; } } diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index 87ca150097f..507d067e60b 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -3821,7 +3821,8 @@ pub mod bv2_impl { let import_records = self.graph.ast.items_import_records(); for source_index in reachable_files { - let records: &[ImportRecord] = import_records[source_index.get() as usize].as_slice(); + let records: &[ImportRecord] = + import_records[source_index.get() as usize].as_slice(); for record in records { if !record.source_index.is_valid() && record.tag == bun_ast::ImportRecordTag::None @@ -4013,15 +4014,7 @@ pub mod bv2_impl { event_loop: EventLoop, entry_points: &[&[u8]], ) -> Result>, Error> { - let mut this = BundleV2::init( - transpiler, - None, - alloc, - event_loop, - false, - None, - alloc, - )?; + let mut this = BundleV2::init(transpiler, None, alloc, event_loop, false, None, alloc)?; this.unique_key = generate_unique_key(); if this.transpiler.log().has_errors() { @@ -6027,8 +6020,7 @@ pub mod bv2_impl { let mut last_error: Option = None; - 'outer: for (i, import_record) in ctx.import_records.iter_mut().enumerate() - { + 'outer: for (i, import_record) in ctx.import_records.iter_mut().enumerate() { // Preserve original import specifier before resolution modifies path if import_record.original_path.is_empty() { import_record.original_path = import_record.path.text; diff --git a/src/bundler/linker_context/generateCodeForLazyExport.rs b/src/bundler/linker_context/generateCodeForLazyExport.rs index baa41fbb49e..c766ac129f2 100644 --- a/src/bundler/linker_context/generateCodeForLazyExport.rs +++ b/src/bundler/linker_context/generateCodeForLazyExport.rs @@ -207,8 +207,7 @@ pub fn generate_code_for_lazy_export( match &compose.from { // it is imported Some(CssSpecifier::ImportRecordIndex(import_record_idx)) => { - let import_records = - &self.all_import_records[idx as usize]; + let import_records = &self.all_import_records[idx as usize]; let import_record = &import_records[*import_record_idx as usize]; if import_record.source_index.is_valid() { @@ -499,7 +498,8 @@ pub fn generate_code_for_lazy_export( key.loc, ))); // PORT NOTE: `parts.ptr[generated[1]]` — re-borrow `parts` here for borrowck. - let parts = this.graph.ast.items_parts_mut()[source_index as usize].as_mut_slice(); + let parts = + this.graph.ast.items_parts_mut()[source_index as usize].as_mut_slice(); parts[generated.1 as usize].stmts = bun_ast::StoreSlice::new_mut(new_stmts); } } diff --git a/src/bundler/linker_context/scanImportsAndExports.rs b/src/bundler/linker_context/scanImportsAndExports.rs index 3fd155f79b1..126954624b3 100644 --- a/src/bundler/linker_context/scanImportsAndExports.rs +++ b/src/bundler/linker_context/scanImportsAndExports.rs @@ -1001,7 +1001,8 @@ pub fn scan_imports_and_exports( && other_export_kind == ExportsKind::Cjs && output_format != Format::InternalBakeDev { - col!(import_records_list)[id].as_mut_slice()[import_record_index as usize] + col!(import_records_list)[id].as_mut_slice() + [import_record_index as usize] .flags .insert(ImportRecordFlags::WRAP_WITH_TO_ESM); to_esm_uses += 1; diff --git a/src/js_parser/parse/parse_entry.rs b/src/js_parser/parse/parse_entry.rs index a822d26e58b..5173b5a50cb 100644 --- a/src/js_parser/parse/parse_entry.rs +++ b/src/js_parser/parse/parse_entry.rs @@ -1420,9 +1420,7 @@ impl<'a> Parser<'a> { if let Some(id) = redirect_import_record_index { part.symbol_uses = Default::default(); return Ok(crate::Result::Ast(Box::new(js_ast::Ast { - import_records: p - .import_records - .move_to_baby_list(p.arena), + import_records: p.import_records.move_to_baby_list(p.arena), redirect_import_record_index: Some(id), named_imports: core::mem::take(&mut *p.named_imports), named_exports: core::mem::take(&mut p.named_exports), diff --git a/src/jsc/AsyncModule.rs b/src/jsc/AsyncModule.rs index 99eb6e5a97f..f194ea785ea 100644 --- a/src/jsc/AsyncModule.rs +++ b/src/jsc/AsyncModule.rs @@ -1292,8 +1292,7 @@ impl AsyncModule { || self.parse_result.ast.exports_kind == bun_ast::ExportsKind::Cjs; let input_fd = self.parse_result.input_fd; let arena = *self.parse_result.ast.parts.allocator(); - let parse_result = - core::mem::replace(&mut self.parse_result, ParseResult::empty(arena)); + let parse_result = core::mem::replace(&mut self.parse_result, ParseResult::empty(arena)); // PORT NOTE: `VirtualMachine.source_code_printer` is a thread-local // `?*BufferPrinter` (see `SOURCE_CODE_PRINTER`); Zig dereferenced to diff --git a/src/runtime/api/JSTranspiler.rs b/src/runtime/api/JSTranspiler.rs index 36d25e447d2..b3f583311be 100644 --- a/src/runtime/api/JSTranspiler.rs +++ b/src/runtime/api/JSTranspiler.rs @@ -799,8 +799,10 @@ impl<'a> TransformTask<'a> { // SAFETY: `arena` outlives every use through `self.transpiler` in this fn body; // Transpiler<'static> forces the borrow to 'static, so launder through a raw ptr. let arena_ref: &'static Arena = unsafe { bun_ptr::detach_lifetime_ref(&arena) }; - let source: &bun_ast::Source = - arena_ref.alloc(bun_ast::Source::init_path_string(name, self.input_code.slice())); + let source: &bun_ast::Source = arena_ref.alloc(bun_ast::Source::init_path_string( + name, + self.input_code.slice(), + )); self.transpiler.set_arena(arena_ref); self.transpiler.set_log(&raw mut self.log); // self.log.msgs.allocator = bun.default_allocator → no-op From 4aa39de6807298336cdf04bce98d2d74e26d290e Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 18 May 2026 08:23:39 +0000 Subject: [PATCH 14/32] bundler: dense Vec> instead of ArrayHashMap source_index keys are dense 0..module_count and this map is probed once per import inside on_parse_task_complete (the main-thread parse-phase throughput limiter). Replaces hash+probe with direct index. --- src/bundler/barrel_imports.rs | 82 ++++++++++++++++++----------------- src/bundler/bundle_v2.rs | 9 +++- 2 files changed, 49 insertions(+), 42 deletions(-) diff --git a/src/bundler/barrel_imports.rs b/src/bundler/barrel_imports.rs index 80b6685423e..20f12b0c688 100644 --- a/src/bundler/barrel_imports.rs +++ b/src/bundler/barrel_imports.rs @@ -33,6 +33,26 @@ impl Default for RequestedExports { } } +impl RequestedExports { + /// `get_or_put`-shaped entry on the dense `Vec>` storage. + /// Returns `(found_existing, &mut value)`; inserts `Default` when absent. + #[inline] + pub fn entry(map: &mut Vec>, idx: u32) -> (bool, &mut Self) { + let i = idx as usize; + if i >= map.len() { + map.resize_with(i + 1, || None); + } + let slot = &mut map[i]; + let found = slot.is_some(); + (found, slot.get_or_insert_with(Self::default)) + } + + #[inline] + pub fn lookup(map: &[Option], idx: u32) -> Option<&Self> { + map.get(idx as usize).and_then(Option::as_ref) + } +} + // PORT NOTE: `original_alias` is stored as the raw arena `*const [u8]` (the // `NamedImport.alias` representation) instead of a tied `&'a [u8]` so the BFS // loop can hold a `BarrelExportResolution` across a `&mut graph.ast` reborrow @@ -124,7 +144,7 @@ fn apply_barrel_optimization_impl( // files parsed before this barrel. scheduleBarrelDeferredImports records // requests eagerly as each file is processed, so we don't need to scan // the graph. - if let Some(existing) = this.requested_exports.get(&source_index) { + if let Some(existing) = RequestedExports::lookup(&this.requested_exports, source_index) { match existing { RequestedExports::All => return Ok(()), // import * already seen — load everything RequestedExports::Partial(_) => {} @@ -140,7 +160,7 @@ fn apply_barrel_optimization_impl( needed_records.put(*record_idx, ())?; } - if let Some(existing) = this.requested_exports.get(&source_index) { + if let Some(existing) = RequestedExports::lookup(&this.requested_exports, source_index) { match existing { RequestedExports::All => unreachable!(), // handled above RequestedExports::Partial(partial) => { @@ -251,10 +271,7 @@ fn apply_barrel_optimization_impl( ); // Merge with existing entry (keep already-requested names) or create new - let gop = this.requested_exports.get_or_put(source_index)?; - if !gop.found_existing { - *gop.value_ptr = RequestedExports::Partial(StringArrayHashMap::default()); - } + let _ = RequestedExports::entry(&mut this.requested_exports, source_index); // Register with DevServer so isFileCached returns null for this barrel, // ensuring it gets re-parsed on every incremental build. This is needed @@ -466,22 +483,15 @@ pub fn schedule_barrel_deferred_imports( continue; }; - let gop = this.requested_exports.get_or_put(target)?; + let (_, value) = RequestedExports::entry(&mut this.requested_exports, target); if ni.alias_is_star { - *gop.value_ptr = RequestedExports::All; + *value = RequestedExports::All; } else if let Some(alias_ptr) = ni.alias { // SAFETY: arena-backed `*const [u8]` valid for the AST lifetime. let alias: &[u8] = alias_ptr.slice(); - if gop.found_existing { - match gop.value_ptr { - RequestedExports::All => {} - RequestedExports::Partial(p) => { - p.put(alias, ())?; - } - } - } else { - *gop.value_ptr = RequestedExports::Partial(StringArrayHashMap::default()); - if let RequestedExports::Partial(p) = gop.value_ptr { + match value { + RequestedExports::All => {} + RequestedExports::Partial(p) => { p.put(alias, ())?; } } @@ -489,8 +499,6 @@ pub fn schedule_barrel_deferred_imports( if let Some(dev) = dev_handle { persist_barrel_export(&dev, resolved_path_text, alias); } - } else if !gop.found_existing { - *gop.value_ptr = RequestedExports::Partial(StringArrayHashMap::default()); } } @@ -523,13 +531,10 @@ pub fn schedule_barrel_deferred_imports( { continue; } - if ir.kind == ImportKind::Require { - let gop = this.requested_exports.get_or_put(target)?; - *gop.value_ptr = RequestedExports::All; - } else if ir.kind == ImportKind::Dynamic { - // import() returns the full module namespace — must preserve all exports. - let gop = this.requested_exports.get_or_put(target)?; - *gop.value_ptr = RequestedExports::All; + if matches!(ir.kind, ImportKind::Require | ImportKind::Dynamic) { + // require() and import() expose the full module namespace — preserve all exports. + let (_, value) = RequestedExports::entry(&mut this.requested_exports, target); + *value = RequestedExports::All; } } @@ -621,7 +626,7 @@ pub fn schedule_barrel_deferred_imports( // but B hadn't been parsed when A's BFS ran, so B's export * records // were empty and the propagation stopped. let this_source_index = result_source_index; - if let Some(existing) = this.requested_exports.get(&this_source_index) { + if let Some(existing) = RequestedExports::lookup(&this.requested_exports, this_source_index) { match existing { RequestedExports::All => queue.push(BarrelWorkItem { barrel_source_index: this_source_index, @@ -670,28 +675,25 @@ pub fn schedule_barrel_deferred_imports( // For BFS-propagated items (not from initial queue), use // requested_exports for dedup and cycle detection. if qi >= initial_queue_len { - let gop = this.requested_exports.get_or_put(barrel_idx)?; + let (found, value) = RequestedExports::entry(&mut this.requested_exports, barrel_idx); if item_is_star { - *gop.value_ptr = RequestedExports::All; - } else if gop.found_existing { - match gop.value_ptr { + *value = RequestedExports::All; + } else { + match value { RequestedExports::All => { - qi += 1; - continue; + if found { + qi += 1; + continue; + } } RequestedExports::Partial(p) => { let alias_gop = p.get_or_put(item_alias)?; - if alias_gop.found_existing { + if found && alias_gop.found_existing { qi += 1; continue; } } } - } else { - *gop.value_ptr = RequestedExports::Partial(StringArrayHashMap::default()); - if let RequestedExports::Partial(p) = gop.value_ptr { - p.put(item_alias, ())?; - } } } diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index 507d067e60b..c702b14bfe3 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -148,7 +148,12 @@ pub struct BundleV2<'a> { /// track requested export names for deduplication and cycle detection. /// Persists across calls to `scheduleBarrelDeferredImports` so cross-file /// deduplication is free. - pub requested_exports: ArrayHashMap, + /// + /// Indexed by `source_index` (dense `0..module_count`); a `Vec>` + /// instead of the Zig `AutoArrayHashMap` because the key space is + /// dense and this is probed once per import in `on_parse_task_complete` + /// (the main-thread parse-phase throughput limiter). + pub requested_exports: Vec>, } bun_core::declare_scope!(Bundle, visible); @@ -2833,7 +2838,7 @@ pub mod bv2_impl { drain_defer_task: DeferredBatchTask::default(), asynchronous: false, has_any_top_level_await_modules: false, - requested_exports: ArrayHashMap::new(), + requested_exports: Vec::new(), }); if let Some(bo) = bake_options { this.client_transpiler = Some(bo.client_transpiler.into()); From f91c9360cf700fbdb3cda7903fdd8090a3ffe245 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 18 May 2026 08:29:12 +0000 Subject: [PATCH 15/32] paths: pool JoinScratch buffer instead of heap-allocating per join --- src/paths/resolve_path.rs | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/paths/resolve_path.rs b/src/paths/resolve_path.rs index 8c7cb257d48..0e32a4253c9 100644 --- a/src/paths/resolve_path.rs +++ b/src/paths/resolve_path.rs @@ -1710,24 +1710,35 @@ pub fn join_string_buf_t<'a, T: PathChar, P: PlatformT>( normalize_string_node_t::(&temp_buf[0..written], buf) } -/// Inline `MAX_PATH_BYTES * 2` stack buffer that heap-allocates when the -/// requested size exceeds it. Keeps `_join_abs_string_buf`'s scratch buffer safe -/// for arbitrarily long inputs while preserving zero-alloc behaviour for the -/// common case. -struct JoinScratch { - // PERF(port): was StackFallbackAllocator(MAX_PATH_BYTES * 2) — using Vec. - // TODO(perf): consider a smallvec / stack-alloc fast path. - buf: Vec, +/// Scratch buffer for `_join_abs_string_buf`'s unnormalized concatenation. +/// Zig used `std.heap.stackFallback(MAX_PATH_BYTES * 2)`; we draw from the +/// thread-local `path_buffer_pool` for the common case and only heap-allocate +/// when the concatenation would overflow a single `PathBuffer`. The pooled +/// buffer is not re-zeroed — callers write every byte they later read. +enum JoinScratch { + Pooled(crate::path_buffer_pool::Guard), + Heap(Vec), } impl JoinScratch { + #[inline] pub(crate) fn init(base: usize, parts: &[&[u8]]) -> Self { let mut total = base + 2; for p in parts { total += p.len() + 1; } - Self { - buf: vec![0u8; total], + if total <= MAX_PATH_BYTES { + JoinScratch::Pooled(crate::path_buffer_pool::get()) + } else { + JoinScratch::Heap(vec![0u8; total]) + } + } + + #[inline] + fn buf(&mut self) -> &mut [u8] { + match self { + JoinScratch::Pooled(g) => &mut g[..], + JoinScratch::Heap(v) => &mut v[..], } } } @@ -1895,7 +1906,7 @@ fn _join_abs_string_buf<'a, const IS_SENTINEL: bool, P: PlatformT>( } let mut scratch = JoinScratch::init(cwd.len(), parts); - let temp_buf = &mut scratch.buf; + let temp_buf = scratch.buf(); temp_buf[..cwd.len()].copy_from_slice(cwd); out = cwd.len(); @@ -2014,7 +2025,7 @@ fn _join_abs_string_buf_windows<'a, const IS_SENTINEL: bool>( } let mut scratch = JoinScratch::init(root.len() + set_cwd.len(), &parts[n_start..]); - let temp_buf = &mut scratch.buf; + let temp_buf = scratch.buf(); temp_buf[0..root.len()].copy_from_slice(root); temp_buf[root.len()..root.len() + set_cwd.len()].copy_from_slice(set_cwd); From 74017e2725166bee85197d550a0dc29f8311b387 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 18 May 2026 08:29:29 +0000 Subject: [PATCH 16/32] resolver: borrow ESM ConditionsMap instead of deep-cloning per resolve --- src/resolver/package_json.rs | 2 +- src/resolver/resolver.rs | 64 +++++++++++++++++++++--------------- 2 files changed, 39 insertions(+), 27 deletions(-) diff --git a/src/resolver/package_json.rs b/src/resolver/package_json.rs index b7f6066c3ec..3488d55fdfb 100644 --- a/src/resolver/package_json.rs +++ b/src/resolver/package_json.rs @@ -1990,7 +1990,7 @@ pub type ConditionsMap = StringArrayHashMap<()>; pub struct ESModule<'a> { pub debug_logs: Option<&'a mut resolver::DebugLogs>, - pub conditions: ConditionsMap, + pub conditions: &'a ConditionsMap, // allocator dropped — global mimalloc pub module_type: &'a mut ModuleType, } diff --git a/src/resolver/resolver.rs b/src/resolver/resolver.rs index ad5c134b015..6331c4e5705 100644 --- a/src/resolver/resolver.rs +++ b/src/resolver/resolver.rs @@ -2756,16 +2756,6 @@ impl<'a> Resolver<'a> { // `&mut self` call is aliased-&mut UB. Build a fresh short-lived // `ESModule` per `resolve` call so its borrow ends before // `self.handle_esm_resolution` re-borrows `self`. - let conditions = match kind { - ast::ImportKind::Require | ast::ImportKind::RequireResolve => { - self.opts.conditions.require.clone().expect("oom") - } - ast::ImportKind::At | ast::ImportKind::AtConditional => { - self.opts.conditions.style.clone().expect("oom") - } - _ => self.opts.conditions.import.clone().expect("oom"), - }; - // Resolve against the path "/", then join it with the absolute // directory path. This is done because ESM package resolution uses // URLs while our path resolution uses file system paths. We don't @@ -2773,9 +2763,18 @@ impl<'a> Resolver<'a> { // paths. We also want to avoid any "%" characters in the absolute // directory path accidentally being interpreted as URL escapes. { - // PERF(port): extra conditions clone vs Zig — profile if hot. let esm_resolution = ESModule { - conditions: conditions.clone().expect("oom"), + conditions: match kind { + ast::ImportKind::Require + | ast::ImportKind::RequireResolve => { + &self.opts.conditions.require + } + ast::ImportKind::At + | ast::ImportKind::AtConditional => { + &self.opts.conditions.style + } + _ => &self.opts.conditions.import, + }, debug_logs: self.debug_logs.as_mut(), module_type: &mut module_type, } @@ -2819,7 +2818,17 @@ impl<'a> Resolver<'a> { let extname = bun_paths::extension(esm.subpath); if extname == b".js" && esm.subpath.len() > 3 { let esm_resolution = ESModule { - conditions, + conditions: match kind { + ast::ImportKind::Require + | ast::ImportKind::RequireResolve => { + &self.opts.conditions.require + } + ast::ImportKind::At + | ast::ImportKind::AtConditional => { + &self.opts.conditions.style + } + _ => &self.opts.conditions.import, + }, debug_logs: self.debug_logs.as_mut(), module_type: &mut module_type, } @@ -3202,14 +3211,6 @@ impl<'a> Resolver<'a> { if let Some(exports_map) = package_json.exports.as_ref() { // The condition set is determined by the kind of import // PORT NOTE: reshaped for borrowck — see identical note above. - let conditions = match kind { - ast::ImportKind::Require - | ast::ImportKind::RequireResolve => { - self.opts.conditions.require.clone().expect("oom") - } - _ => self.opts.conditions.import.clone().expect("oom"), - }; - // Resolve against the path "/", then join it with the absolute // directory path. This is done because ESM package resolution uses // URLs while our path resolution uses file system paths. We don't @@ -3217,9 +3218,14 @@ impl<'a> Resolver<'a> { // paths. We also want to avoid any "%" characters in the absolute // directory path accidentally being interpreted as URL escapes. { - // PERF(port): extra conditions clone vs Zig — profile if hot. let esm_resolution = ESModule { - conditions: conditions.clone().expect("oom"), + conditions: match kind { + ast::ImportKind::Require + | ast::ImportKind::RequireResolve => { + &self.opts.conditions.require + } + _ => &self.opts.conditions.import, + }, debug_logs: self.debug_logs.as_mut(), module_type: &mut module_type, } @@ -3249,7 +3255,13 @@ impl<'a> Resolver<'a> { let extname = bun_paths::extension(esm.subpath); if extname == b".js" && esm.subpath.len() > 3 { let esm_resolution = ESModule { - conditions, + conditions: match kind { + ast::ImportKind::Require + | ast::ImportKind::RequireResolve => { + &self.opts.conditions.require + } + _ => &self.opts.conditions.import, + }, debug_logs: self.debug_logs.as_mut(), module_type: &mut module_type, } @@ -4828,9 +4840,9 @@ impl<'a> Resolver<'a> { let esm_resolution = ESModule { conditions: match kind { ast::ImportKind::Require | ast::ImportKind::RequireResolve => { - self.opts.conditions.require.clone().expect("oom") + &self.opts.conditions.require } - _ => self.opts.conditions.import.clone().expect("oom"), + _ => &self.opts.conditions.import, }, debug_logs: self.debug_logs.as_mut(), module_type: &mut module_type, From c05b341af98dc7b482e4b93533f15a08f38b99b2 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 18 May 2026 08:29:32 +0000 Subject: [PATCH 17/32] resolver: avoid eager Pragma clone for the discarded outer Result in resolve_without_symlinks --- src/resolver/resolver.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/resolver/resolver.rs b/src/resolver/resolver.rs index 6331c4e5705..9d875c90602 100644 --- a/src/resolver/resolver.rs +++ b/src/resolver/resolver.rs @@ -1703,7 +1703,7 @@ impl<'a> Resolver<'a> { } if let Some(tsconfig) = dir.enclosing_tsconfig_json { - result.jsx = tsconfig.merge_jsx(result.jsx.clone()); + result.jsx = tsconfig.merge_jsx(core::mem::take(&mut result.jsx)); result.flags.set_emit_decorator_metadata( result.flags.emit_decorator_metadata() || tsconfig.emit_decorator_metadata, ); @@ -1834,7 +1834,6 @@ impl<'a> Resolver<'a> { primary: Path::empty(), secondary: None, }, - jsx: self.opts.jsx.clone(), ..Default::default() }; @@ -1891,7 +1890,7 @@ impl<'a> Resolver<'a> { package_json: res.package_json, dirname_fd: res.dirname_fd, file_fd: res.file_fd, - jsx: tsconfig.merge_jsx(result.jsx), + jsx: tsconfig.merge_jsx(self.opts.jsx.clone()), ..Default::default() }); } @@ -1981,6 +1980,7 @@ impl<'a> Resolver<'a> { if check_package { if self.opts.polyfill_node_globals { + result.jsx = self.opts.jsx.clone(); let had_node_prefix = import_path.starts_with(b"node:"); let import_path_without_node_prefix = if had_node_prefix { &import_path[b"node:".len()..] @@ -2359,7 +2359,6 @@ impl<'a> Resolver<'a> { result.flags.set_is_from_node_modules( result.flags.is_from_node_modules() || res.is_node_module, ); - result.jsx = self.opts.jsx.clone(); result.module_type = res.module_type; result.flags.set_is_external(res.is_external); // Potentially rewrite the import path if it's external that From e856dd8987bb6082908fc4040d4850f990a44804 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 18 May 2026 08:29:36 +0000 Subject: [PATCH 18/32] bundler: spell out BundledAst::empty_in defaults instead of round-tripping through Ast::empty_in + init --- src/bundler/bundled_ast.rs | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/src/bundler/bundled_ast.rs b/src/bundler/bundled_ast.rs index 77097fafab4..adc2801fc79 100644 --- a/src/bundler/bundled_ast.rs +++ b/src/bundler/bundled_ast.rs @@ -159,11 +159,39 @@ bitflags::bitflags! { } impl<'arena> BundledAst<'arena> { - // TODO(port): Zig `pub const empty = BundledAst.init(Ast.empty);` — cannot be a `const` in Rust - // because `init` is not const-evaluable. Consider a `static` via `OnceLock` or make - // `init`/`Ast::empty_in` const fn if feasible. + // Zig: `pub const empty = BundledAst.init(Ast.empty)` (comptime). The three `ArenaVec` + // fields prevent `const fn` here, but spell out the defaults directly instead of + // round-tripping through `Ast::empty_in` + `init` — this runs once per discovered + // module on the main thread. pub fn empty_in(arena: &'arena bun_alloc::Arena) -> Self { - Self::init(Ast::empty_in(arena)) + Self { + approximate_newline_count: 0, + nested_scope_slot_counts: SlotCounts::default(), + exports_kind: ExportsKind::None, + import_records: import_record::List::new_in(arena), + hashbang: StoreStr::EMPTY, + parts: part::List::new_in(arena), + css: None, + url_for_css: b"", + symbols: symbol::List::new_in(arena), + module_scope: Scope::default(), + char_freq: CharFreq::default(), + exports_ref: Ref::NONE, + module_ref: Ref::NONE, + wrapper_ref: Ref::NONE, + require_ref: Ref::NONE, + top_level_await_keyword: bun_ast::Range::NONE, + tla_check: TlaCheck::default(), + named_imports: NamedImports::default(), + named_exports: NamedExports::default(), + export_star_import_records: Box::default(), + top_level_symbols_to_parts: TopLevelSymbolToParts::default(), + commonjs_named_exports: CommonJSNamedExports::default(), + redirect_import_record_index: u32::MAX, + target: bun_ast::Target::Browser, + ts_enums: bun_ast::ast_result::TsEnumsMap::default(), + flags: Flags::empty(), + } } // PORT NOTE: Zig's `*const BundledAst` bitwise-copies every field; the Rust From 696e13677d0789f331573df11802d14213b464bb Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 18 May 2026 08:29:36 +0000 Subject: [PATCH 19/32] bundler: drop redundant Box of already-'static source path in on_parse_task_complete --- src/bundler/bundle_v2.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index c702b14bfe3..888a1280d0c 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -7191,12 +7191,11 @@ pub mod bv2_impl { &mut result.ast.import_records, Vec::new_in(result_heap), ); - let source_path_owned: Box<[u8]> = source_path_text.into(); this.patch_import_record_source_indices( &mut import_records, PatchImportRecordsCtx { source_index: Index::init(result_source_index as IndexInt), - source_path: &source_path_owned, + source_path: source_path_text, loader: result.loader, target: result.ast.target, redirect_import_record_index: result.ast.redirect_import_record_index, @@ -7375,7 +7374,6 @@ pub mod bv2_impl { ) .expect("oom"); } - let _ = source_path_owned; } parse_task::ResultValue::Err(err) => { if cfg!(feature = "debug_logs") { From e2f6a248abb4819224816fb3ef77d73002ed2a0c Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 18 May 2026 08:29:36 +0000 Subject: [PATCH 20/32] bundler: format source identifier once in scan_imports_and_exports symbol-name pass --- .../linker_context/scanImportsAndExports.rs | 45 +++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/src/bundler/linker_context/scanImportsAndExports.rs b/src/bundler/linker_context/scanImportsAndExports.rs index 126954624b3..151ca954d68 100644 --- a/src/bundler/linker_context/scanImportsAndExports.rs +++ b/src/bundler/linker_context/scanImportsAndExports.rs @@ -545,6 +545,7 @@ pub fn scan_imports_and_exports( // const needs_export_symbol_from_runtime: []const bool = this.graph.meta.items().needs_export_symbol_from_runtime; let mut runtime_export_symbol_ref: Ref = Ref::NONE; + let mut ident_scratch: Vec = Vec::new(); for source_index_ in &reachable { let source_index = source_index_.get(); @@ -560,6 +561,21 @@ pub fn scan_imports_and_exports( let exports_ref = col_ref!(exports_refs)[id]; let module_ref = col_ref!(module_refs)[id]; + // Format the source identifier once into a reusable scratch so the + // per-file `init_/exports_/module_` writes below are plain memcpys + // instead of three trips through `core::fmt::write`. + let ident: &[u8] = if !source.identifier_name.is_empty() { + &source.identifier_name[..] + } else { + ident_scratch.clear(); + core::fmt::Write::write_fmt( + &mut bun_core::fmt::VecWriter(&mut ident_scratch), + format_args!("{}", source.fmt_identifier()), + ) + .expect("infallible: VecWriter never errors"); + &ident_scratch[..] + }; + let string_buffer_len: usize = 'brk: { let mut count: usize = 0; if is_entry_point && output_format == Format::Esm { @@ -571,11 +587,7 @@ pub fn scan_imports_and_exports( } } - let ident_fmt_len: usize = if source.identifier_name.len() > 0 { - source.identifier_name.len() - } else { - bun_core::fmt::count(format_args!("{}", source.fmt_identifier())) - }; + let ident_fmt_len = ident.len(); if wrap == WrapKind::Esm && col_ref!(wrapper_refs)[id].is_valid() { count += "init_".len() + ident_fmt_len; @@ -637,8 +649,11 @@ pub fn scan_imports_and_exports( if wrap == WrapKind::Esm { let r#ref = col_ref!(wrapper_refs)[id]; if r#ref.is_valid() { - let original_name = - builder.fmt(format_args!("init_{}", source.fmt_identifier())); + let start = builder.len; + builder.append(b"init_"); + builder.append(ident); + let end = builder.len; + let original_name = &builder.allocated_slice()[start..end]; unsafe { this.graph.symbol_mut(r#ref) }.original_name = bun_ast::StoreStr::new(original_name); } @@ -653,12 +668,16 @@ pub fn scan_imports_and_exports( && export_kind != ExportsKind::Cjs && output_format != Format::InternalBakeDev { - let exports_name = bun_ast::StoreStr::new( - builder.fmt(format_args!("exports_{}", source.fmt_identifier())), - ); - let module_name = bun_ast::StoreStr::new( - builder.fmt(format_args!("module_{}", source.fmt_identifier())), - ); + let start = builder.len; + builder.append(b"exports_"); + builder.append(ident); + let end = builder.len; + let exports_name = bun_ast::StoreStr::new(&builder.allocated_slice()[start..end]); + let start = builder.len; + builder.append(b"module_"); + builder.append(ident); + let end = builder.len; + let module_name = bun_ast::StoreStr::new(&builder.allocated_slice()[start..end]); // Note: it's possible for the symbols table to be resized // so we cannot call .get() above this scope. From 2f17a5c32ffdc747f6c2004f1bc8e054a393cfa5 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 18 May 2026 09:21:24 +0000 Subject: [PATCH 21/32] bundler: use AutoBitSet for named_ir_indices in schedule_barrel_deferred_imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Zig original used a 4096-byte stack-fallback ArrayHashMap; the Rust port heap-allocated an ArrayHashMap per parsed file. Swap to AutoBitSet sized to file_import_records.len() — it stays in its inline 2-word Static arm for the typical <128-record file and is O(1) word ops to set/probe instead of hash+probe. --- src/bundler/barrel_imports.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/bundler/barrel_imports.rs b/src/bundler/barrel_imports.rs index 20f12b0c688..cfa963f40ea 100644 --- a/src/bundler/barrel_imports.rs +++ b/src/bundler/barrel_imports.rs @@ -12,7 +12,7 @@ use crate::mal_prelude::*; use bun_alloc::AllocError; use bun_ast::{ImportKind, import_record}; use bun_collections::VecExt; -use bun_collections::{ArrayHashMap, StringArrayHashMap}; +use bun_collections::{ArrayHashMap, AutoBitSet, StringArrayHashMap}; use crate::Graph::{InputFileColumns as _, InputFileFlags}; use crate::Index; @@ -395,11 +395,6 @@ pub fn schedule_barrel_deferred_imports( // is later parsed, applyBarrelOptimization reads these pre-recorded requests // to decide which exports to keep. O(file's imports) per file. - // Build a set of import_record_indices that have named_imports entries, - // so we can detect bare imports (those with no specific export bindings). - // PERF(port): was stack-fallback (4096) — profile if hot. - let mut named_ir_indices: ArrayHashMap = ArrayHashMap::default(); - // In dev server mode, patchImportRecordSourceIndices skips saving source_indices // on import records (the dev server uses path-based identifiers instead). But // barrel optimization requires source_indices to seed requested_exports and to @@ -423,6 +418,10 @@ pub fn schedule_barrel_deferred_imports( // See PORT NOTE above — read-only deref valid through Phase 2. let file_import_records = file_import_records.get(); + // Build a set of import_record_indices that have named_imports entries, + // so we can detect bare imports (those with no specific export bindings). + let mut named_ir_indices = AutoBitSet::init_empty(file_import_records.len())?; + // In HMR, ConvertESMExportsForHmr deduplicates import records by path: // two `import { X } from 'mod'` statements become one, and the second // record is marked is_unused=true. resolveImportRecords then skips those @@ -455,7 +454,7 @@ pub fn schedule_barrel_deferred_imports( if ni.import_record_index as usize >= file_import_records.len() { continue; } - named_ir_indices.put(ni.import_record_index, ())?; + named_ir_indices.set(ni.import_record_index as usize); let ir = &file_import_records.as_slice()[ni.import_record_index as usize]; // In dev server mode, source_index may not be patched — resolve via // path map as a read-only fallback. Do NOT write back to the import @@ -522,7 +521,7 @@ pub fn schedule_barrel_deferred_imports( if ir.flags.contains(import_record::Flags::IS_INTERNAL) { continue; } - if named_ir_indices.contains(&u32::try_from(idx).unwrap()) { + if named_ir_indices.is_set(idx) { continue; } if ir @@ -601,7 +600,7 @@ pub fn schedule_barrel_deferred_imports( if ir.flags.contains(import_record::Flags::IS_INTERNAL) { continue; } - if named_ir_indices.contains(&u32::try_from(idx).unwrap()) { + if named_ir_indices.is_set(idx) { continue; } if ir From e9eec9fb21c345beb4e6c71115c5418d8a9f66bb Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 18 May 2026 09:21:28 +0000 Subject: [PATCH 22/32] resolver: borrow Entry in value_for_key/resolve_exports instead of deep-cloning Zig's Entry.data holds slices/pointers so its by-value return is a shallow few-word copy. The Rust port made EntryData own boxed slices/Vecs, so entry.value.clone() and exports.clone() deep-copied the entire conditions subtree on every resolve. Return Option<&Entry> from value_for_key and match exports by reference in resolve_exports; resolve_target already takes &Entry so callers just drop the local sentinel and pass the borrow through. --- src/resolver/package_json.rs | 36 ++++++++++++++---------------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/src/resolver/package_json.rs b/src/resolver/package_json.rs index 3488d55fdfb..2f10983ec07 100644 --- a/src/resolver/package_json.rs +++ b/src/resolver/package_json.rs @@ -1969,13 +1969,13 @@ impl Entry { matches!(&self.data, EntryData::Map(m) if !m.list.is_empty() && strings::starts_with_char(&m.list[0].key, b'.')) } - pub fn value_for_key(&self, key_: &[u8]) -> Option { + pub fn value_for_key(&self, key_: &[u8]) -> Option<&Entry> { match &self.data { EntryData::Map(m) => { // TODO(port): bun_collections::MultiArrayList column accessor; Vec placeholder. for entry in m.list.iter() { if strings::eql(&entry.key, key_) { - return Some(entry.value.clone()); + return Some(&entry.value); } } @@ -2487,27 +2487,19 @@ impl<'a> ESModule<'a> { } if subpath == b"." { - let mut main_export = Entry { - data: EntryData::Null, - first_token: bun_ast::Range::NONE, + let main_export: Option<&Entry> = match &exports.data { + EntryData::String(_) | EntryData::Array(_) => Some(exports), + EntryData::Map(_) if !exports.keys_start_with_dot() => Some(exports), + EntryData::Map(_) => exports.value_for_key(b"."), + _ => None, }; - let cond = match &exports.data { - EntryData::String(_) | EntryData::Array(_) => true, - EntryData::Map(_) => !exports.keys_start_with_dot(), - _ => false, - }; - if cond { - main_export = exports.clone(); - } else if matches!(exports.data, EntryData::Map(_)) { - if let Some(value) = exports.value_for_key(b".") { - main_export = value; - } - } - if !matches!(main_export.data, EntryData::Null) { - let result = self.resolve_target::(package_url, &main_export, b"", false); - if result.status != Status::Null && result.status != Status::Undefined { - return result; + if let Some(main_export) = main_export { + if !matches!(main_export.data, EntryData::Null) { + let result = self.resolve_target::(package_url, main_export, b"", false); + if result.status != Status::Null && result.status != Status::Undefined { + return result; + } } } } else if matches!(exports.data, EntryData::Map(_)) && exports.keys_start_with_dot() { @@ -2566,7 +2558,7 @@ impl<'a> ESModule<'a> { log.add_note_fmt(format_args!("Found \"{}\"", bstr::BStr::new(match_key))); } - return self.resolve_target::(package_url, &target, b"", is_imports); + return self.resolve_target::(package_url, target, b"", is_imports); } } From 624bb5681fa6af8636ffb2d5c9b322ba1500872e Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 18 May 2026 10:46:19 +0000 Subject: [PATCH 23/32] ast_alloc: keep bump cursor across same-heap set_thread_heap; invalidate on mi_heap_destroy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit set_thread_heap() previously bump_reset() unconditionally, so the bundler's per-task Worker::get → ASTMemoryAllocator::push() abandoned a 16 KB bump chunk on every task (~70K tasks × 16 KB ≈ 1.1 GB into never-reset worker arenas, mostly <500 B used per chunk). Now tracks BUMP_HEAP (the chunk's owner) and keeps the cursor when re-entering that same heap; MimallocArena reset/Drop calls bump_invalidate_heap() before mi_heap_destroy so a recycled mi_heap_t* slot can't ABA-match a stale cursor. rolldown apps/10000 (20K modules): peak RSS 895 → 607 MB, wall 466 → 448 ms. --- src/bun_alloc/MimallocArena.rs | 2 ++ src/bun_alloc/ast_alloc.rs | 36 ++++++++++++++++++++++++++++++---- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/bun_alloc/MimallocArena.rs b/src/bun_alloc/MimallocArena.rs index 58f837c15a9..877fc183360 100644 --- a/src/bun_alloc/MimallocArena.rs +++ b/src/bun_alloc/MimallocArena.rs @@ -243,6 +243,7 @@ impl MimallocArena { // destroyed (we own it). After this call all outstanding allocations // are freed; replacing `self.heap` with a fresh heap restores the // invariant. + crate::ast_alloc::bump_invalidate_heap(self.heap_ptr()); unsafe { mimalloc::mi_heap_destroy(self.heap_ptr()) }; let heap = unsafe { mimalloc::mi_heap_new() }; self.heap = NonNull::new(heap).unwrap_or_else(|| crate::out_of_memory()); @@ -585,6 +586,7 @@ impl Drop for MimallocArena { // every block still allocated in it without running per-block free. // SAFETY: `self.heap` is a live heap obtained from `mi_heap_new` and // is destroyed exactly once here. + crate::ast_alloc::bump_invalidate_heap(self.heap_ptr()); unsafe { mimalloc::mi_heap_destroy(self.heap_ptr()) }; } } diff --git a/src/bun_alloc/ast_alloc.rs b/src/bun_alloc/ast_alloc.rs index f97fe38f858..4f76b7cd08c 100644 --- a/src/bun_alloc/ast_alloc.rs +++ b/src/bun_alloc/ast_alloc.rs @@ -97,14 +97,32 @@ static BUMP_CUR: Cell<*mut u8> = Cell::new(core::ptr::null_mut()); /// One-past-the-end of the active bump chunk. #[thread_local] static BUMP_END: Cell<*mut u8> = Cell::new(core::ptr::null_mut()); +/// The `mi_heap_t*` that owns the active bump chunk (set in [`bump_refill`]). +/// Lets [`set_thread_heap`] keep the cursor when re-entering the same heap so +/// the bundler's per-task `push()`/`pop()` doesn't abandon a 16 KB chunk every +/// task — that was ~70 K tasks × 16 KB ≈ 1.1 GB of mostly-empty chunks held in +/// the never-reset worker `ast_memory_store` arenas. +#[thread_local] +static BUMP_HEAP: Cell<*mut mimalloc::Heap> = Cell::new(core::ptr::null_mut()); -/// Drop the bump cursor (the chunk itself is owned by `AST_HEAP` and reclaimed -/// by `mi_heap_destroy`). Called on every [`set_thread_heap`] so a cursor never -/// outlives the heap that backs its chunk. +/// Drop the bump cursor (the chunk itself is owned by `BUMP_HEAP` and reclaimed +/// by `mi_heap_destroy`). #[inline] fn bump_reset() { BUMP_CUR.set(core::ptr::null_mut()); BUMP_END.set(core::ptr::null_mut()); + BUMP_HEAP.set(core::ptr::null_mut()); +} + +/// Invalidate the bump cursor if it is backed by `heap`. Called from +/// `MimallocArena::reset`/`Drop` immediately before `mi_heap_destroy(heap)` so +/// a recycled `mi_heap_t*` slot can't ABA-match `BUMP_HEAP` and serve a stale +/// (freed) chunk under [`set_thread_heap`]. +#[inline] +pub(crate) fn bump_invalidate_heap(heap: *mut mimalloc::Heap) { + if BUMP_HEAP.get() == heap { + bump_reset(); + } } /// Carve `size` bytes at `align` (a power of two `<= MI_MAX_ALIGN_SIZE`) from @@ -149,6 +167,7 @@ fn bump_refill(heap: *mut mimalloc::Heap, size: usize) -> Option<*mut u8> { // both `add`s are in bounds. BUMP_CUR.set(unsafe { chunk.add(size) }); BUMP_END.set(unsafe { chunk.add(BUMP_CHUNK) }); + BUMP_HEAP.set(heap); Some(chunk) } @@ -172,7 +191,16 @@ fn bump_refill(heap: *mut mimalloc::Heap, size: usize) -> Option<*mut u8> { #[inline] pub fn set_thread_heap(heap: *mut mimalloc::Heap) { AST_HEAP.set(heap); - bump_reset(); + // Keep the cursor when re-entering the heap that owns it (the bundler's + // per-task `push()`/`pop()` always passes the same per-worker arena). Only + // discard when switching to a *different* non-null heap — the chunk then + // belongs to the wrong owner. Clearing to null is a no-op for the cursor + // (no allocs happen while `AST_HEAP` is null), so it survives the + // `pop()→push()` round-trip. Heap destruction must call [`bump_reset`] + // explicitly to defend against address reuse. + if !heap.is_null() && heap != BUMP_HEAP.get() { + bump_reset(); + } } /// Current thread's AST heap, or null if no `ASTMemoryAllocator` scope is From e21d0b85be9b102c1c06f1562662e4fabdf4397c Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 18 May 2026 11:27:10 +0000 Subject: [PATCH 24/32] bun_alloc: BabyVec<'a,T> with u32 len/cap; retarget ArenaVec alias Port of BabyList(T) (collections/baby_list.zig: ptr + u32 len + u32 cap = 16 B). The Rust port keeps the &'a MimallocArena handle inline for lifetime checking, so 24 B vs the previous Vec = 32 B. Growth/free route through <&MimallocArena as Allocator> as before; transfer_arena is now a single allocator-field swap. Covers the existing call surface (push/pop/insert/swap_remove/append/ prepend_from/truncate/extend/drain/leak/allocator/IntoIterator) plus io::Write for BabyVec<'_, u8>. ArenaVecExt is implemented for both BabyVec and the legacy Vec so ArenaString stays on usize-backed Vec. --- src/bun_alloc/MimallocArena.rs | 42 +- src/bun_alloc/baby_vec.rs | 518 ++++++++++++++++++ src/bun_alloc/lib.rs | 33 +- src/bun_core/util.rs | 14 +- src/bundler/AstBuilder.rs | 2 +- src/bundler/ParseTask.rs | 2 +- src/bundler/barrel_imports.rs | 2 +- src/bundler/bundle_v2.rs | 13 +- .../linker_context/prepareCssAstsForChunk.rs | 4 +- src/bundler/transpiler.rs | 2 +- src/js_parser/parse/parse_entry.rs | 4 +- src/runtime/cli/repl.rs | 2 +- src/threading/ThreadPool.rs | 33 ++ 13 files changed, 627 insertions(+), 44 deletions(-) create mode 100644 src/bun_alloc/baby_vec.rs diff --git a/src/bun_alloc/MimallocArena.rs b/src/bun_alloc/MimallocArena.rs index 877fc183360..10d20dfa2ee 100644 --- a/src/bun_alloc/MimallocArena.rs +++ b/src/bun_alloc/MimallocArena.rs @@ -800,21 +800,6 @@ pub fn std_vtables() -> [&'static crate::AllocatorVTable; 2] { } // ── ArenaVec helpers ───────────────────────────────────────────────────── -// `std::vec::Vec` lacks `from_iter_in` / `into_bump_slice*`; provide -// thin shims so call sites that used `bumpalo::collections::Vec` keep working. - -/// `bumpalo::collections::Vec::from_iter_in` parity for `Vec`. -#[inline] -pub fn vec_from_iter_in<'a, T, I>(iter: I, arena: &'a MimallocArena) -> Vec -where - I: IntoIterator, -{ - let iter = iter.into_iter(); - let (lo, _) = iter.size_hint(); - let mut v = Vec::with_capacity_in(lo, arena); - v.extend(iter); - v -} /// `bumpalo::collections::String` parity — a UTF-8 buffer backed by the arena. /// Thin newtype over `Vec` so `write!` works and @@ -906,12 +891,33 @@ pub trait ArenaVecExt<'a, T> { impl<'a, T> ArenaVecExt<'a, T> for Vec { #[inline] fn from_iter_in>(iter: I, arena: &'a MimallocArena) -> Self { - vec_from_iter_in(iter, arena) + let iter = iter.into_iter(); + let (lo, _) = iter.size_hint(); + let mut v = Vec::with_capacity_in(lo, arena); + v.extend(iter); + v + } + #[inline] + fn into_bump_slice(self) -> &'a [T] { + &*self.leak() + } + #[inline] + fn into_bump_slice_mut(self) -> &'a mut [T] { + self.leak() + } + #[inline] + fn bump(&self) -> &'a MimallocArena { + *self.allocator() + } +} + +impl<'a, T> ArenaVecExt<'a, T> for crate::BabyVec<'a, T> { + #[inline] + fn from_iter_in>(iter: I, arena: &'a MimallocArena) -> Self { + crate::vec_from_iter_in(iter, arena) } #[inline] fn into_bump_slice(self) -> &'a [T] { - // Storage is owned by the arena and lives for `'a`; `Vec::leak` forgoes - // the `Vec` drop so the arena reclaims it on `reset`/`Drop`. &*self.leak() } #[inline] diff --git a/src/bun_alloc/baby_vec.rs b/src/bun_alloc/baby_vec.rs new file mode 100644 index 00000000000..b63018aa7a2 --- /dev/null +++ b/src/bun_alloc/baby_vec.rs @@ -0,0 +1,518 @@ +//! `BabyVec<'a, T>` — arena-backed growable array with `u32` length/capacity. +//! +//! Port target: `BabyList(T)` (collections/baby_list.zig) = +//! `(ptr: [*]T, len: u32, cap: u32)` = 16 B. The Rust port stores the owning +//! `&'a MimallocArena` inline (lifetime-checked allocator vs Zig passing the +//! allocator at every `append(allocator, ..)` call site), so 24 B instead of +//! 16. Still 8 B smaller than `Vec` (32 B), which +//! matters for AST node lists embedded in `Part` / `BundledAst` columns. +//! +//! `len`/`cap` are stored as `u32` (`usize` on the public API for ergonomics). +//! No bundler list approaches 2³² elements; debug-asserted on every grow path. + +use core::alloc::{Allocator, Layout}; +use core::mem::{ManuallyDrop, size_of}; +use core::ops::{Deref, DerefMut, RangeBounds}; +use core::ptr::{self, NonNull}; +use core::{fmt, slice}; + +use crate::MimallocArena; + +/// Arena-backed `Vec` with `u32` length/capacity. See module doc. +pub struct BabyVec<'a, T> { + ptr: NonNull, + len: u32, + cap: u32, + alloc: &'a MimallocArena, +} + +const _: () = assert!(size_of::>() == 24); + +// SAFETY: same as `Vec` — `Send`/`Sync` follow `T` and the +// allocator handle (`&MimallocArena: Sync` is already declared upstream; the +// raw `NonNull` is the only auto-trait opt-out). +unsafe impl<'a, T: Send> Send for BabyVec<'a, T> {} +unsafe impl<'a, T: Sync> Sync for BabyVec<'a, T> {} + +impl<'a, T> BabyVec<'a, T> { + const T_IS_ZST: bool = size_of::() == 0; + + #[inline] + pub const fn new_in(alloc: &'a MimallocArena) -> Self { + BabyVec { + ptr: NonNull::dangling(), + len: 0, + cap: if Self::T_IS_ZST { u32::MAX } else { 0 }, + alloc, + } + } + + #[inline] + pub fn with_capacity_in(cap: usize, alloc: &'a MimallocArena) -> Self { + let mut v = Self::new_in(alloc); + if cap > 0 { + v.grow_to(cap); + } + v + } + + /// # Safety + /// `(ptr, len, cap)` must describe a valid allocation owned by `alloc` + /// (i.e. obtainable from a prior `BabyVec::into_raw_parts` or + /// `<&MimallocArena as Allocator>::allocate` with `Layout::array::(cap)`), + /// with `len <= cap` initialized elements. + #[inline] + pub unsafe fn from_raw_parts_in( + ptr: *mut T, + len: usize, + cap: usize, + alloc: &'a MimallocArena, + ) -> Self { + debug_assert!(len <= cap && cap <= u32::MAX as usize); + BabyVec { + // SAFETY: caller contract — `ptr` is a valid (or dangling-for-empty) + // allocation pointer; `Vec` uses the same dangling-NonNull encoding. + ptr: unsafe { NonNull::new_unchecked(ptr) }, + len: len as u32, + cap: if Self::T_IS_ZST { u32::MAX } else { cap as u32 }, + alloc, + } + } + + #[inline] + pub fn into_raw_parts(self) -> (*mut T, usize, usize, &'a MimallocArena) { + let me = ManuallyDrop::new(self); + (me.ptr.as_ptr(), me.len as usize, me.cap as usize, me.alloc) + } + + #[inline] + pub fn allocator(&self) -> &&'a MimallocArena { + &self.alloc + } + + /// Re-tag the stored allocator handle. See [`crate::transfer_arena`]. + #[inline] + pub(crate) fn set_allocator(&mut self, alloc: &'a MimallocArena) { + self.alloc = alloc; + } + + #[inline] + pub fn len(&self) -> usize { + self.len as usize + } + #[inline] + pub fn is_empty(&self) -> bool { + self.len == 0 + } + #[inline] + pub fn capacity(&self) -> usize { + self.cap as usize + } + + #[inline] + pub fn as_ptr(&self) -> *const T { + self.ptr.as_ptr() + } + #[inline] + pub fn as_mut_ptr(&mut self) -> *mut T { + self.ptr.as_ptr() + } + #[inline] + pub fn as_slice(&self) -> &[T] { + // SAFETY: `[ptr, ptr+len)` are `len` initialized `T` (struct invariant). + unsafe { slice::from_raw_parts(self.ptr.as_ptr(), self.len as usize) } + } + #[inline] + pub fn as_mut_slice(&mut self) -> &mut [T] { + // SAFETY: as above; `&mut self` proves exclusive access. + unsafe { slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len as usize) } + } + + /// # Safety + /// `new_len <= capacity()`, and `[old_len, new_len)` must be initialized + /// when growing. + #[inline] + pub unsafe fn set_len(&mut self, new_len: usize) { + debug_assert!(new_len <= self.cap as usize); + self.len = new_len as u32; + } + + #[inline] + pub fn reserve(&mut self, additional: usize) { + let need = self.len as usize + additional; + if need > self.cap as usize { + self.grow_to(need); + } + } + + #[inline] + pub fn reserve_exact(&mut self, additional: usize) { + let need = self.len as usize + additional; + if need > self.cap as usize { + self.grow_exact(need); + } + } + + #[inline] + pub fn push(&mut self, value: T) { + if self.len == self.cap { + self.grow_to(self.len as usize + 1); + } + // SAFETY: `len < cap` after grow; slot is in-bounds and uninit. + unsafe { self.ptr.as_ptr().add(self.len as usize).write(value) }; + self.len += 1; + } + + #[inline] + pub fn pop(&mut self) -> Option { + if self.len == 0 { + return None; + } + self.len -= 1; + // SAFETY: slot was initialized; ownership moves out, len already + // decremented so it won't be dropped again. + Some(unsafe { self.ptr.as_ptr().add(self.len as usize).read() }) + } + + pub fn insert(&mut self, index: usize, value: T) { + let len = self.len as usize; + assert!(index <= len, "BabyVec::insert index {index} > len {len}"); + if self.len == self.cap { + self.grow_to(len + 1); + } + // SAFETY: `index <= len < cap` after grow; shifting `len - index` + // initialized elements one slot right stays within `[0, cap)`. + unsafe { + let p = self.ptr.as_ptr().add(index); + ptr::copy(p, p.add(1), len - index); + p.write(value); + } + self.len += 1; + } + + pub fn swap_remove(&mut self, index: usize) -> T { + let len = self.len as usize; + assert!(index < len, "BabyVec::swap_remove index {index} >= len {len}"); + // SAFETY: `index < len`; reading the hole then overwriting with the + // last element (possibly itself) is the standard swap-remove. Len is + // decremented before the read of `last` so the moved-from tail slot + // is no longer considered initialized. + unsafe { + let p = self.ptr.as_ptr(); + let v = p.add(index).read(); + self.len -= 1; + ptr::copy(p.add(self.len as usize), p.add(index), 1); + v + } + } + + /// `Vec::append` parity — bitwise-move all elements from `other` to the + /// end of `self`, leaving `other` empty. + pub fn append(&mut self, other: &mut Self) { + let n = other.len as usize; + if n == 0 { + return; + } + self.reserve(n); + // SAFETY: `reserve` guarantees room for `n` more; `self`/`other` are + // distinct (`&mut` × 2). Elements are bitwise-moved; `other.len` is + // zeroed so it relinquishes ownership before `self` claims it. + unsafe { + ptr::copy_nonoverlapping( + other.ptr.as_ptr(), + self.ptr.as_ptr().add(self.len as usize), + n, + ); + other.len = 0; + self.len += n as u32; + } + } + + /// Bitwise-move all elements from `src` to the *front* of `self`, leaving + /// `src` empty. Mirrors `bun_collections::prepend_from` for `Vec`. + pub fn prepend_from(&mut self, src: &mut Self) { + let src_len = src.len as usize; + if src_len == 0 { + return; + } + let dst_len = self.len as usize; + self.reserve(src_len); + // SAFETY: capacity holds `dst_len + src_len`; the right-shift memmove + // and front copy together fully initialize `[0, dst_len+src_len)`. + // `src.len` is zeroed before `self.len` is grown so no element is ever + // owned by both. + unsafe { + let base = self.ptr.as_ptr(); + ptr::copy(base, base.add(src_len), dst_len); + ptr::copy_nonoverlapping(src.ptr.as_ptr(), base, src_len); + src.len = 0; + self.len += src_len as u32; + } + } + + pub fn remove(&mut self, index: usize) -> T { + let len = self.len as usize; + assert!(index < len, "BabyVec::remove index {index} >= len {len}"); + // SAFETY: `index < len`; read moves out the element, then shift the + // `len-1-index` initialized tail down by one. `len` decremented after. + unsafe { + let p = self.ptr.as_ptr().add(index); + let v = p.read(); + ptr::copy(p.add(1), p, len - index - 1); + self.len -= 1; + v + } + } + + #[inline] + pub fn truncate(&mut self, new_len: usize) { + if new_len >= self.len as usize { + return; + } + let drop_from = new_len; + let drop_count = self.len as usize - new_len; + self.len = new_len as u32; + // SAFETY: `[drop_from, drop_from+drop_count)` were initialized; len + // already shortened so a panic in a destructor doesn't double-drop. + unsafe { + ptr::drop_in_place(ptr::slice_from_raw_parts_mut( + self.ptr.as_ptr().add(drop_from), + drop_count, + )); + } + } + + #[inline] + pub fn clear(&mut self) { + self.truncate(0); + } + + /// `Vec::leak` parity — forget the `BabyVec`, return the buffer as an + /// arena-lifetime slice. Reclaimed when the arena resets/drops. + #[inline] + pub fn leak(self) -> &'a mut [T] { + let me = ManuallyDrop::new(self); + // SAFETY: `[ptr, ptr+len)` are `len` initialized `T` valid for `'a` + // (the buffer is owned by `me.alloc`, which outlives `'a`). + unsafe { slice::from_raw_parts_mut(me.ptr.as_ptr(), me.len as usize) } + } + + /// Drain all elements (only the full range is supported; debug-asserted). + /// Zig `BabyList` has no partial drain and no caller needs one. + pub fn drain>(&mut self, range: R) -> IntoIter<'a, T> { + use core::ops::Bound::*; + debug_assert!(matches!(range.start_bound(), Unbounded | Included(0))); + debug_assert!(match range.end_bound() { + Unbounded => true, + Excluded(n) => *n == self.len as usize, + Included(n) => *n + 1 == self.len as usize, + }); + let _ = range; + core::mem::replace(self, BabyVec::new_in(self.alloc)).into_iter() + } + + pub fn extend_from_slice(&mut self, other: &[T]) + where + T: Copy, + { + let n = other.len(); + self.reserve(n); + // SAFETY: `reserve` guarantees `cap >= len + n`; the source/destination + // ranges are disjoint (`other` borrows immutably, `self` exclusively). + unsafe { + ptr::copy_nonoverlapping( + other.as_ptr(), + self.ptr.as_ptr().add(self.len as usize), + n, + ); + self.len += n as u32; + } + } + + #[cold] + fn grow_to(&mut self, at_least: usize) { + debug_assert!(at_least <= u32::MAX as usize, "BabyVec capacity overflow"); + if Self::T_IS_ZST { + return; + } + // Same growth as `Vec`: max(2×cap, at_least, 4). + let new_cap = (self.cap as usize * 2).max(at_least).max(4); + self.grow_exact(new_cap); + } + + #[cold] + fn grow_exact(&mut self, new_cap: usize) { + if Self::T_IS_ZST { + return; + } + debug_assert!(new_cap <= u32::MAX as usize, "BabyVec capacity overflow"); + let new_cap = new_cap.min(u32::MAX as usize); + let new_layout = + Layout::array::(new_cap).unwrap_or_else(|_| crate::out_of_memory()); + let new_ptr = if self.cap == 0 { + (&self.alloc) + .allocate(new_layout) + .unwrap_or_else(|_| crate::out_of_memory()) + } else { + let old_layout = Layout::array::(self.cap as usize).unwrap(); + // SAFETY: `self.ptr` was returned by `(&self.alloc).allocate` (or + // `grow`) with `old_layout`; `new_layout.size() >= old_layout.size()`. + unsafe { + (&self.alloc) + .grow(self.ptr.cast::(), old_layout, new_layout) + .unwrap_or_else(|_| crate::out_of_memory()) + } + }; + self.ptr = new_ptr.cast::(); + self.cap = new_cap as u32; + } +} + +impl<'a, T> Drop for BabyVec<'a, T> { + #[inline] + fn drop(&mut self) { + // SAFETY: `[ptr, ptr+len)` are `len` initialized `T`. + unsafe { + ptr::drop_in_place(ptr::slice_from_raw_parts_mut( + self.ptr.as_ptr(), + self.len as usize, + )); + } + if !Self::T_IS_ZST && self.cap != 0 { + let layout = Layout::array::(self.cap as usize).unwrap(); + // SAFETY: `ptr` was allocated by `(&self.alloc)` with `layout`. + unsafe { (&self.alloc).deallocate(self.ptr.cast::(), layout) }; + } + } +} + +impl<'a, T> Deref for BabyVec<'a, T> { + type Target = [T]; + #[inline] + fn deref(&self) -> &[T] { + self.as_slice() + } +} +impl<'a, T> DerefMut for BabyVec<'a, T> { + #[inline] + fn deref_mut(&mut self) -> &mut [T] { + self.as_mut_slice() + } +} + +impl<'a, T> Extend for BabyVec<'a, T> { + fn extend>(&mut self, iter: I) { + let iter = iter.into_iter(); + let (lo, _) = iter.size_hint(); + self.reserve(lo); + for v in iter { + self.push(v); + } + } +} + +impl<'a, 'b, T: Copy> Extend<&'b T> for BabyVec<'a, T> { + #[inline] + fn extend>(&mut self, iter: I) { + for v in iter { + self.push(*v); + } + } +} + +impl<'a, 'b, T> IntoIterator for &'b BabyVec<'a, T> { + type Item = &'b T; + type IntoIter = slice::Iter<'b, T>; + #[inline] + fn into_iter(self) -> Self::IntoIter { + self.as_slice().iter() + } +} +impl<'a, 'b, T> IntoIterator for &'b mut BabyVec<'a, T> { + type Item = &'b mut T; + type IntoIter = slice::IterMut<'b, T>; + #[inline] + fn into_iter(self) -> Self::IntoIter { + self.as_mut_slice().iter_mut() + } +} +impl<'a, T> IntoIterator for BabyVec<'a, T> { + type Item = T; + type IntoIter = IntoIter<'a, T>; + #[inline] + fn into_iter(self) -> IntoIter<'a, T> { + let me = ManuallyDrop::new(self); + IntoIter { + ptr: me.ptr, + idx: 0, + len: me.len, + cap: me.cap, + alloc: me.alloc, + } + } +} + +/// Consuming iterator. Drops any unyielded tail and frees the buffer on drop. +pub struct IntoIter<'a, T> { + ptr: NonNull, + idx: u32, + len: u32, + cap: u32, + alloc: &'a MimallocArena, +} + +impl<'a, T> Iterator for IntoIter<'a, T> { + type Item = T; + #[inline] + fn next(&mut self) -> Option { + if self.idx == self.len { + return None; + } + let i = self.idx as usize; + self.idx += 1; + // SAFETY: `i < len` and slot has not been read yet (idx monotone). + Some(unsafe { self.ptr.as_ptr().add(i).read() }) + } + #[inline] + fn size_hint(&self) -> (usize, Option) { + let n = (self.len - self.idx) as usize; + (n, Some(n)) + } +} +impl<'a, T> ExactSizeIterator for IntoIter<'a, T> {} + +impl<'a, T> Drop for IntoIter<'a, T> { + fn drop(&mut self) { + // SAFETY: `[idx, len)` are the unyielded initialized elements. + unsafe { + ptr::drop_in_place(ptr::slice_from_raw_parts_mut( + self.ptr.as_ptr().add(self.idx as usize), + (self.len - self.idx) as usize, + )); + } + if size_of::() != 0 && self.cap != 0 { + let layout = Layout::array::(self.cap as usize).unwrap(); + // SAFETY: buffer was allocated by `(&self.alloc)` with `layout`. + unsafe { (&self.alloc).deallocate(self.ptr.cast::(), layout) }; + } + } +} + +impl<'a, T: fmt::Debug> fmt::Debug for BabyVec<'a, T> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.as_slice().fmt(f) + } +} + +impl<'a, T> core::borrow::Borrow<[T]> for BabyVec<'a, T> { + #[inline] + fn borrow(&self) -> &[T] { + self.as_slice() + } +} +impl<'a, T> AsRef<[T]> for BabyVec<'a, T> { + #[inline] + fn as_ref(&self) -> &[T] { + self.as_slice() + } +} diff --git a/src/bun_alloc/lib.rs b/src/bun_alloc/lib.rs index 9bb3b1649aa..403d1454853 100644 --- a/src/bun_alloc/lib.rs +++ b/src/bun_alloc/lib.rs @@ -265,10 +265,28 @@ pub use mimalloc_arena::MimallocArena; pub type Arena = MimallocArena; /// `bumpalo::Bump` — kept for genuinely bump-only scratch that's never resized. pub type Bump = bumpalo::Bump; -/// Arena-backed `Vec` — `Vec`. Real `deallocate`/`grow` -/// via `mi_free`/`mi_heap_realloc_aligned`; reclaimed on arena `reset`/`Drop`. -pub type ArenaVec<'a, T> = Vec; -pub use mimalloc_arena::{ArenaString, ArenaVecExt, live_arena_heaps, vec_from_iter_in}; +mod baby_vec; +pub use baby_vec::BabyVec; +/// Arena-backed `Vec` with `u32` length/capacity — port of Zig's +/// `BabyList(T)`. 24 B (vs 32 B for `Vec`); the +/// allocator handle is kept inline for lifetime checking. Growth/free route +/// through `<&MimallocArena as Allocator>` (= `mi_heap_realloc_aligned` / +/// `mi_free`); reclaimed on arena `reset`/`Drop`. +pub type ArenaVec<'a, T> = BabyVec<'a, T>; +pub use mimalloc_arena::{ArenaString, ArenaVecExt, live_arena_heaps}; + +/// `bumpalo::collections::Vec::from_iter_in` parity for [`ArenaVec`]. +#[inline] +pub fn vec_from_iter_in<'a, T, I>(iter: I, arena: &'a MimallocArena) -> ArenaVec<'a, T> +where + I: IntoIterator, +{ + let iter = iter.into_iter(); + let (lo, _) = iter.size_hint(); + let mut v = ArenaVec::with_capacity_in(lo, arena); + v.extend(iter); + v +} /// Re-tag an [`ArenaVec`]'s allocator handle to `dst` without copying data. /// @@ -289,12 +307,7 @@ pub use mimalloc_arena::{ArenaString, ArenaVecExt, live_arena_heaps, vec_from_it /// so the [`MimallocArena`] single-thread-alloc contract is preserved. #[inline] pub fn transfer_arena<'a, T>(v: &mut ArenaVec<'a, T>, dst: &'a MimallocArena) { - let mut old = core::mem::ManuallyDrop::new(core::mem::replace(v, Vec::new_in(dst))); - let (ptr, len, cap) = (old.as_mut_ptr(), old.len(), old.capacity()); - // SAFETY: see fn doc — `<&MimallocArena as Allocator>::{deallocate,grow}` - // are heap-agnostic on `ptr`; `(ptr, len, cap)` are the just-decomposed - // valid `Vec` triplet from `old`, whose `Drop` is suppressed. - *v = unsafe { Vec::from_raw_parts_in(ptr, len, cap, dst) }; + v.set_allocator(dst); } /// `bumpalo::format!` parity — `arena_format!(in arena, "...", ..)` → diff --git a/src/bun_core/util.rs b/src/bun_core/util.rs index 2a35c571e09..ad3d3b5eabb 100644 --- a/src/bun_core/util.rs +++ b/src/bun_core/util.rs @@ -2140,8 +2140,6 @@ pub mod io { } /// In-memory growable sink. Zig: `std.Io.Writer.Allocating`. - /// Generic over the allocator so `bun_alloc::ArenaVec<'_, u8>` - /// (= `Vec`) gets the same impl as `Vec`. impl Write for Vec { #[inline] fn write_all(&mut self, buf: &[u8]) -> Result<(), crate::Error> { @@ -2154,6 +2152,18 @@ pub mod io { } } + impl<'a> Write for bun_alloc::BabyVec<'a, u8> { + #[inline] + fn write_all(&mut self, buf: &[u8]) -> Result<(), crate::Error> { + self.extend_from_slice(buf); + Ok(()) + } + #[inline] + fn written_len(&self) -> usize { + self.len() + } + } + /// Bridge the type-erased vtable header into the generic `Write` trait so /// printers taking `W: io::Write` accept process stdout/stderr sinks. impl Write for Writer { diff --git a/src/bundler/AstBuilder.rs b/src/bundler/AstBuilder.rs index 32cbaa5ff33..9585aed836b 100644 --- a/src/bundler/AstBuilder.rs +++ b/src/bundler/AstBuilder.rs @@ -300,7 +300,7 @@ impl<'a, 'bump> AstBuilder<'a, 'bump> { debug_assert!(self.scopes.is_empty()); let module_scope = self.current_scope; - let mut parts = Vec::with_capacity_in(2, self.bump); + let mut parts = bun_ast::PartList::with_capacity_in(2, self.bump); // PORT NOTE: Zig grew len then wrote `parts.mut(i).* = ...`, which is a // bitwise store on the SoA slot. In Rust `*parts.mut_(i) = ...` first // *drops* the (uninitialized) prior `Part` — and `Part` carries Drop diff --git a/src/bundler/ParseTask.rs b/src/bundler/ParseTask.rs index 2873b3019f1..8d897d4311d 100644 --- a/src/bundler/ParseTask.rs +++ b/src/bundler/ParseTask.rs @@ -668,7 +668,7 @@ pub mod parse_worker { bump: &'static Bump, ) -> bun_ast::symbol::List<'static> { use bun_ast::symbol::{Kind as PKind, Symbol as PSym}; - let mut out = Vec::with_capacity_in(src.len(), bump); + let mut out = bun_ast::symbol::List::with_capacity_in(src.len(), bump); for s in src.slice() { // Post-dedup `bun_ast::Symbol` IS `bun_ast::symbol::Symbol`, so // `s.kind`/`s.import_item_status` are already the target nominal types diff --git a/src/bundler/barrel_imports.rs b/src/bundler/barrel_imports.rs index cfa963f40ea..5e78a2dd31c 100644 --- a/src/bundler/barrel_imports.rs +++ b/src/bundler/barrel_imports.rs @@ -323,7 +323,7 @@ fn resolve_barrel_records( let heap = this.graph.heap; let mut barrel_ir = core::mem::replace( &mut this.graph.ast.items_import_records_mut()[idx], - Vec::new_in(heap), + bun_alloc::ArenaVec::new_in(heap), ); let source = core::mem::take(&mut this.graph.input_files.items_source_mut()[idx]); let source_path: &'static [u8] = source.path.text; diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index 888a1280d0c..b47764c78e0 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -4961,13 +4961,13 @@ pub mod bv2_impl { ($ast:expr) => {{ let ast = $ast; for v in ast.items_parts_mut() { - drop(core::mem::replace(v, Vec::new_in(*v.allocator()))); + drop(core::mem::replace(v, bun_alloc::ArenaVec::new_in(*v.allocator()))); } for v in ast.items_symbols_mut() { - drop(core::mem::replace(v, Vec::new_in(*v.allocator()))); + drop(core::mem::replace(v, bun_alloc::ArenaVec::new_in(*v.allocator()))); } for v in ast.items_import_records_mut() { - drop(core::mem::replace(v, Vec::new_in(*v.allocator()))); + drop(core::mem::replace(v, bun_alloc::ArenaVec::new_in(*v.allocator()))); } for v in ast.items_named_imports_mut() { drop(core::mem::take(v)); @@ -5944,7 +5944,10 @@ pub mod bv2_impl { // build before link time, so saving the AST is safe. let result_heap = *result.ast.import_records.allocator(); this.graph.ast.items_import_records_mut()[source_index.0 as usize] = - core::mem::replace(&mut result.ast.import_records, Vec::new_in(result_heap)); + core::mem::replace( + &mut result.ast.import_records, + bun_alloc::ArenaVec::new_in(result_heap), + ); // Move the CSS stylesheet onto the graph row so teardown can find // and drop it — the `Success` arm that would normally do this is skipped. @@ -7189,7 +7192,7 @@ pub mod bv2_impl { let result_heap = *result.ast.import_records.allocator(); let mut import_records = core::mem::replace( &mut result.ast.import_records, - Vec::new_in(result_heap), + bun_alloc::ArenaVec::new_in(result_heap), ); this.patch_import_record_source_indices( &mut import_records, diff --git a/src/bundler/linker_context/prepareCssAstsForChunk.rs b/src/bundler/linker_context/prepareCssAstsForChunk.rs index 66d4258f83f..2816824a8dd 100644 --- a/src/bundler/linker_context/prepareCssAstsForChunk.rs +++ b/src/bundler/linker_context/prepareCssAstsForChunk.rs @@ -397,7 +397,7 @@ fn prepare_css_asts_for_chunk_impl(c: &mut LinkerContext, chunk: &mut Chunk, bum // so we don't mutate the shared backing array. // Preserve the "@layer" statements from the // prefix and append the remaining tail. - let mut new_rules: ArenaVec = Vec::with_capacity_in( + let mut new_rules: ArenaVec = ArenaVec::with_capacity_in( layer_count + (original_rules.len() - prefix_end), bump, ); @@ -456,7 +456,7 @@ fn arena_rule_list(rules: ArenaVec<'_, BundlerCssRule>) -> BundlerCssRuleList { /// Single-element shorthand for [`arena_rule_list`]. fn arena_rule_list_one(bump: &Bump, rule: BundlerCssRule) -> BundlerCssRuleList { - let mut v: ArenaVec = Vec::with_capacity_in(1, bump); + let mut v: ArenaVec = ArenaVec::with_capacity_in(1, bump); v.push(rule); arena_rule_list(v) } diff --git a/src/bundler/transpiler.rs b/src/bundler/transpiler.rs index 9f61ae85fce..8ccb71a0392 100644 --- a/src/bundler/transpiler.rs +++ b/src/bundler/transpiler.rs @@ -2387,7 +2387,7 @@ impl<'a> Transpiler<'a> { // empty). `init_with_one_list` boxes the single inner list. let arena = *ast.symbols.allocator(); let symbols = bun_ast::symbol::Map::init_with_one_list( - core::mem::replace(&mut ast.symbols, Vec::new_in(arena)) + core::mem::replace(&mut ast.symbols, bun_alloc::ArenaVec::new_in(arena)) .into_iter() .collect(), ); diff --git a/src/js_parser/parse/parse_entry.rs b/src/js_parser/parse/parse_entry.rs index 5173b5a50cb..fafc08aa6a0 100644 --- a/src/js_parser/parse/parse_entry.rs +++ b/src/js_parser/parse/parse_entry.rs @@ -2213,8 +2213,8 @@ impl<'a> Parser<'a> { // Single up-front reserve preserves the Zig fused-growth; the inner // reserve() calls in prepend_from / append become no-ops. parts.reserve(before.len() + after.len()); - bun_collections::prepend_from(&mut parts, &mut before); - parts.append(&mut after); // std Vec::append: bitwise-move tail, same allocator + parts.prepend_from(&mut before); + parts.append(&mut after); } // Pop the module scope to apply the "ContainsDirectEval" rules diff --git a/src/runtime/cli/repl.rs b/src/runtime/cli/repl.rs index 83f0416ecf2..295669d0c60 100644 --- a/src/runtime/cli/repl.rs +++ b/src/runtime/cli/repl.rs @@ -1851,7 +1851,7 @@ impl<'a> Repl<'a> { // hazard. let arena = *ast.symbols.allocator(); let symbols_map = bun_ast::symbol::Map::init_with_one_list( - core::mem::replace(&mut ast.symbols, Vec::new_in(arena)) + core::mem::replace(&mut ast.symbols, bun_alloc::ArenaVec::new_in(arena)) .into_iter() .collect(), ); diff --git a/src/threading/ThreadPool.rs b/src/threading/ThreadPool.rs index cf18ba0bb28..881361f55ae 100644 --- a/src/threading/ThreadPool.rs +++ b/src/threading/ThreadPool.rs @@ -209,6 +209,28 @@ pub struct ThreadPool { stats: PoolStats, } +static NTF_TOTAL: AtomicU32 = AtomicU32::new(0); +static NTF_FAST_RET: AtomicU32 = AtomicU32::new(0); +static NTF_WAKE: AtomicU32 = AtomicU32::new(0); +static NTF_SPAWN: AtomicU32 = AtomicU32::new(0); +static NTF_PENDING: AtomicU32 = AtomicU32::new(0); +static NTF_NOWAKE: AtomicU32 = AtomicU32::new(0); +static NTF_CANTWAKE: AtomicU32 = AtomicU32::new(0); +static NTF_MAX_SPAWNED: AtomicU32 = AtomicU32::new(0); +pub fn dump_notify_stats() { + eprintln!( + "[notify] total={} fast_ret={} wake_idle={} spawn={} pending={} nowake={} cantwake={} max_spawned_seen={}", + NTF_TOTAL.load(Ordering::Relaxed), + NTF_FAST_RET.load(Ordering::Relaxed), + NTF_WAKE.load(Ordering::Relaxed), + NTF_SPAWN.load(Ordering::Relaxed), + NTF_PENDING.load(Ordering::Relaxed), + NTF_NOWAKE.load(Ordering::Relaxed), + NTF_CANTWAKE.load(Ordering::Relaxed), + NTF_MAX_SPAWNED.load(Ordering::Relaxed), + ); +} + /// Configuration options for the thread pool. /// TODO: add CPU core affinity? pub struct Config { @@ -259,6 +281,8 @@ impl ThreadPool { if !stats_enabled() { return; } + dump_notify_stats(); + eprintln!("[dump_stats] self.max_threads={}", self.max_threads); let now = now_ns(); let idle = self.stats.idle_ns.swap(0, Ordering::Relaxed); let busy = self.stats.busy_ns.swap(0, Ordering::Relaxed); @@ -680,6 +704,7 @@ impl ThreadPool { #[inline(always)] fn notify(&self, is_waking: bool) { + NTF_TOTAL.fetch_add(1, Ordering::Relaxed); // Fast path to check the Sync state to avoid calling into notify_slow(). // If we're waking, then we need to update the state regardless if !is_waking { @@ -690,6 +715,7 @@ impl ThreadPool { // worker sees run_queue empty" → task stranded let sync = self.sync.fetch_or(Sync::zero(), Ordering::Release); if sync.notified() { + NTF_FAST_RET.fetch_add(1, Ordering::Relaxed); return; } } @@ -784,6 +810,7 @@ impl ThreadPool { fn notify_slow(&self, is_waking: bool) { self.is_running.store(true, Ordering::Relaxed); let mut sync = self.sync.load(Ordering::Relaxed); + NTF_MAX_SPAWNED.fetch_max(sync.spawned() as u32, Ordering::Relaxed); while sync.state() != SyncState::Shutdown { let can_wake = is_waking || (sync.state() == SyncState::Pending); if is_waking { @@ -794,17 +821,23 @@ impl ThreadPool { new_sync.set_notified(true); if can_wake && sync.idle() > 0 { // wake up an idle thread + NTF_WAKE.fetch_add(1, Ordering::Relaxed); new_sync.set_state(SyncState::Signaled); } else if can_wake && (sync.spawned() as u32) < self.max_threads { // spawn a new thread + NTF_SPAWN.fetch_add(1, Ordering::Relaxed); new_sync.set_state(SyncState::Signaled); new_sync.set_spawned(new_sync.spawned() + 1); } else if is_waking { + NTF_PENDING.fetch_add(1, Ordering::Relaxed); // no other thread to pass on "waking" status new_sync.set_state(SyncState::Pending); } else if sync.notified() { + NTF_NOWAKE.fetch_add(1, Ordering::Relaxed); // nothing to update return; + } else { + NTF_CANTWAKE.fetch_add(1, Ordering::Relaxed); } // Release barrier synchronizes with Acquire in wait() From 1d8e7855acc487defbabdfa44d3503ac21cb0abb Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 07:34:46 +0000 Subject: [PATCH 25/32] [autofix.ci] apply automated fixes --- src/bun_alloc/baby_vec.rs | 14 ++++++-------- src/bundler/bundle_v2.rs | 15 ++++++++++++--- .../linker_context/prepareCssAstsForChunk.rs | 9 +++++---- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/bun_alloc/baby_vec.rs b/src/bun_alloc/baby_vec.rs index b63018aa7a2..8d35af0dd7c 100644 --- a/src/bun_alloc/baby_vec.rs +++ b/src/bun_alloc/baby_vec.rs @@ -192,7 +192,10 @@ impl<'a, T> BabyVec<'a, T> { pub fn swap_remove(&mut self, index: usize) -> T { let len = self.len as usize; - assert!(index < len, "BabyVec::swap_remove index {index} >= len {len}"); + assert!( + index < len, + "BabyVec::swap_remove index {index} >= len {len}" + ); // SAFETY: `index < len`; reading the hole then overwriting with the // last element (possibly itself) is the standard swap-remove. Len is // decremented before the read of `last` so the moved-from tail slot @@ -320,11 +323,7 @@ impl<'a, T> BabyVec<'a, T> { // SAFETY: `reserve` guarantees `cap >= len + n`; the source/destination // ranges are disjoint (`other` borrows immutably, `self` exclusively). unsafe { - ptr::copy_nonoverlapping( - other.as_ptr(), - self.ptr.as_ptr().add(self.len as usize), - n, - ); + ptr::copy_nonoverlapping(other.as_ptr(), self.ptr.as_ptr().add(self.len as usize), n); self.len += n as u32; } } @@ -347,8 +346,7 @@ impl<'a, T> BabyVec<'a, T> { } debug_assert!(new_cap <= u32::MAX as usize, "BabyVec capacity overflow"); let new_cap = new_cap.min(u32::MAX as usize); - let new_layout = - Layout::array::(new_cap).unwrap_or_else(|_| crate::out_of_memory()); + let new_layout = Layout::array::(new_cap).unwrap_or_else(|_| crate::out_of_memory()); let new_ptr = if self.cap == 0 { (&self.alloc) .allocate(new_layout) diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index b47764c78e0..f435cd424d7 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -4961,13 +4961,22 @@ pub mod bv2_impl { ($ast:expr) => {{ let ast = $ast; for v in ast.items_parts_mut() { - drop(core::mem::replace(v, bun_alloc::ArenaVec::new_in(*v.allocator()))); + drop(core::mem::replace( + v, + bun_alloc::ArenaVec::new_in(*v.allocator()), + )); } for v in ast.items_symbols_mut() { - drop(core::mem::replace(v, bun_alloc::ArenaVec::new_in(*v.allocator()))); + drop(core::mem::replace( + v, + bun_alloc::ArenaVec::new_in(*v.allocator()), + )); } for v in ast.items_import_records_mut() { - drop(core::mem::replace(v, bun_alloc::ArenaVec::new_in(*v.allocator()))); + drop(core::mem::replace( + v, + bun_alloc::ArenaVec::new_in(*v.allocator()), + )); } for v in ast.items_named_imports_mut() { drop(core::mem::take(v)); diff --git a/src/bundler/linker_context/prepareCssAstsForChunk.rs b/src/bundler/linker_context/prepareCssAstsForChunk.rs index 2816824a8dd..fb5d74b63e5 100644 --- a/src/bundler/linker_context/prepareCssAstsForChunk.rs +++ b/src/bundler/linker_context/prepareCssAstsForChunk.rs @@ -397,10 +397,11 @@ fn prepare_css_asts_for_chunk_impl(c: &mut LinkerContext, chunk: &mut Chunk, bum // so we don't mutate the shared backing array. // Preserve the "@layer" statements from the // prefix and append the remaining tail. - let mut new_rules: ArenaVec = ArenaVec::with_capacity_in( - layer_count + (original_rules.len() - prefix_end), - bump, - ); + let mut new_rules: ArenaVec = + ArenaVec::with_capacity_in( + layer_count + (original_rules.len() - prefix_end), + bump, + ); for rule in &original_rules[0..prefix_end] { if matches!(rule, BundlerCssRule::LayerStatement(_)) { // SAFETY: Zig by-value copy of arena-backed rule. From 1d1f2ed5e590339ace9dec442f0720d3d5a5c34e Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Tue, 19 May 2026 08:14:14 +0000 Subject: [PATCH 26/32] threading: drop leftover NTF_* notify-stats instrumentation Ungated fetch_add(Relaxed) on shared counters in the notify() hot path, accidentally swept into e21d0b85 from an abandoned investigation. --- src/threading/ThreadPool.rs | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/src/threading/ThreadPool.rs b/src/threading/ThreadPool.rs index 881361f55ae..cf18ba0bb28 100644 --- a/src/threading/ThreadPool.rs +++ b/src/threading/ThreadPool.rs @@ -209,28 +209,6 @@ pub struct ThreadPool { stats: PoolStats, } -static NTF_TOTAL: AtomicU32 = AtomicU32::new(0); -static NTF_FAST_RET: AtomicU32 = AtomicU32::new(0); -static NTF_WAKE: AtomicU32 = AtomicU32::new(0); -static NTF_SPAWN: AtomicU32 = AtomicU32::new(0); -static NTF_PENDING: AtomicU32 = AtomicU32::new(0); -static NTF_NOWAKE: AtomicU32 = AtomicU32::new(0); -static NTF_CANTWAKE: AtomicU32 = AtomicU32::new(0); -static NTF_MAX_SPAWNED: AtomicU32 = AtomicU32::new(0); -pub fn dump_notify_stats() { - eprintln!( - "[notify] total={} fast_ret={} wake_idle={} spawn={} pending={} nowake={} cantwake={} max_spawned_seen={}", - NTF_TOTAL.load(Ordering::Relaxed), - NTF_FAST_RET.load(Ordering::Relaxed), - NTF_WAKE.load(Ordering::Relaxed), - NTF_SPAWN.load(Ordering::Relaxed), - NTF_PENDING.load(Ordering::Relaxed), - NTF_NOWAKE.load(Ordering::Relaxed), - NTF_CANTWAKE.load(Ordering::Relaxed), - NTF_MAX_SPAWNED.load(Ordering::Relaxed), - ); -} - /// Configuration options for the thread pool. /// TODO: add CPU core affinity? pub struct Config { @@ -281,8 +259,6 @@ impl ThreadPool { if !stats_enabled() { return; } - dump_notify_stats(); - eprintln!("[dump_stats] self.max_threads={}", self.max_threads); let now = now_ns(); let idle = self.stats.idle_ns.swap(0, Ordering::Relaxed); let busy = self.stats.busy_ns.swap(0, Ordering::Relaxed); @@ -704,7 +680,6 @@ impl ThreadPool { #[inline(always)] fn notify(&self, is_waking: bool) { - NTF_TOTAL.fetch_add(1, Ordering::Relaxed); // Fast path to check the Sync state to avoid calling into notify_slow(). // If we're waking, then we need to update the state regardless if !is_waking { @@ -715,7 +690,6 @@ impl ThreadPool { // worker sees run_queue empty" → task stranded let sync = self.sync.fetch_or(Sync::zero(), Ordering::Release); if sync.notified() { - NTF_FAST_RET.fetch_add(1, Ordering::Relaxed); return; } } @@ -810,7 +784,6 @@ impl ThreadPool { fn notify_slow(&self, is_waking: bool) { self.is_running.store(true, Ordering::Relaxed); let mut sync = self.sync.load(Ordering::Relaxed); - NTF_MAX_SPAWNED.fetch_max(sync.spawned() as u32, Ordering::Relaxed); while sync.state() != SyncState::Shutdown { let can_wake = is_waking || (sync.state() == SyncState::Pending); if is_waking { @@ -821,23 +794,17 @@ impl ThreadPool { new_sync.set_notified(true); if can_wake && sync.idle() > 0 { // wake up an idle thread - NTF_WAKE.fetch_add(1, Ordering::Relaxed); new_sync.set_state(SyncState::Signaled); } else if can_wake && (sync.spawned() as u32) < self.max_threads { // spawn a new thread - NTF_SPAWN.fetch_add(1, Ordering::Relaxed); new_sync.set_state(SyncState::Signaled); new_sync.set_spawned(new_sync.spawned() + 1); } else if is_waking { - NTF_PENDING.fetch_add(1, Ordering::Relaxed); // no other thread to pass on "waking" status new_sync.set_state(SyncState::Pending); } else if sync.notified() { - NTF_NOWAKE.fetch_add(1, Ordering::Relaxed); // nothing to update return; - } else { - NTF_CANTWAKE.fetch_add(1, Ordering::Relaxed); } // Release barrier synchronizes with Acquire in wait() From f368ec882ea52676b9a26448cfec6d49ee25252d Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Tue, 19 May 2026 08:18:23 +0000 Subject: [PATCH 27/32] bundler: drop stale 'static PORT NOTE on JSAst alias --- src/bundler/ungate_support.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/bundler/ungate_support.rs b/src/bundler/ungate_support.rs index 49f16004095..9a9555c6340 100644 --- a/src/bundler/ungate_support.rs +++ b/src/bundler/ungate_support.rs @@ -597,14 +597,6 @@ pub use bun_js_printer::MangledProps; /// `bun.logger` — alias used by the original drafts as `crate::bun_ast::Source`. /// `js_ast.BundledAst` (the bundler-facing AST view). -/// -/// PORT NOTE: lifetime-erased to `'static`. `BundledAst<'arena>` borrows the -/// per-file parse arena (`hashbang`/`url_for_css`/`export_star_import_records` -/// slices). The bundler owns those arenas for the entire link (see -/// `LinkerGraph.bump: *const Arena` "stays `'static`-ish" note); `JSAst` is -/// stored in a `MultiArrayList` SoA inside `LinkerGraph`/`Graph`, neither of -/// which carries a lifetime parameter yet. Pin to `'static` until `'bump` -/// is threaded through `Chunk`/`LinkerGraph`/`LinkerContext`. pub type JSAst<'a> = crate::BundledAst<'a>; pub(crate) use bun_ast::{Part, Ref, Symbol}; From 65e2ccd0b5899abff91113a22a589dc44f133e31 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Tue, 19 May 2026 08:18:27 +0000 Subject: [PATCH 28/32] ast: drop stale Ast::empty() reference from comment --- src/ast/ast_result.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ast/ast_result.rs b/src/ast/ast_result.rs index 8b3b1e305a1..bb9ed6e05d4 100644 --- a/src/ast/ast_result.rs +++ b/src/ast/ast_result.rs @@ -102,8 +102,7 @@ pub struct Ast<'a> { // is unverified across crates, so spell them out here instead of `#[derive(Default)]`. // // `parts`/`symbols`/`import_records` are now `ArenaVec`s and need an allocator, -// so `Default` no longer applies; use `Ast::empty_in(arena)` (or `Ast::empty()` -// with the process-static arena for placeholder values). +// so `Default` no longer applies; use `Ast::empty_in(arena)`. impl<'a> Ast<'a> { pub fn empty_in(arena: &'a bun_alloc::MimallocArena) -> Self { Self { From e7dc28fcb24824071a46e3100711d593b3f2130e Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Tue, 19 May 2026 08:18:34 +0000 Subject: [PATCH 29/32] jsc: move feature(allocator_api) out of crate doc comment --- src/jsc/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jsc/lib.rs b/src/jsc/lib.rs index 0ac87499e2b..c4286114d57 100644 --- a/src/jsc/lib.rs +++ b/src/jsc/lib.rs @@ -3,7 +3,6 @@ //! //! Web and runtime-specific APIs should go in `bun.webcore` and `bun.api`. //! -#![feature(allocator_api)] //! LAYERING: `jsc.zig` carries deprecated aliases `WebCore = bun.webcore`, //! `API = bun.api`, `Node = bun.api.node`, `Subprocess = bun.api.Subprocess`. //! In the Rust crate graph those targets live in `bun_runtime`, which depends @@ -34,6 +33,7 @@ // accessor inlining (every `VirtualMachine::get_or_null()` ≥3×/run_callback). // Precedent: 064951400fa4 did this for `bun_alloc`/`bun_ast`. #![feature(thread_local)] +#![feature(allocator_api)] #![allow(incomplete_features)] extern crate alloc; From f357814ca269bc126cfdae8ccd98c4183f0aa0b8 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Tue, 19 May 2026 08:18:40 +0000 Subject: [PATCH 30/32] bundler: fix incorrect SAFETY claim on Worker::arena lifetime --- src/bundler/ThreadPool.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/bundler/ThreadPool.rs b/src/bundler/ThreadPool.rs index 2a4ad241535..33bf7e09812 100644 --- a/src/bundler/ThreadPool.rs +++ b/src/bundler/ThreadPool.rs @@ -561,9 +561,11 @@ impl Worker { /// `detach_lifetime_ref` (the previous pattern at `ParseTask::run`). #[inline] pub fn arena(&self) -> &'static ThreadLocalArena { - // SAFETY: `self.arena` is a `BackRef` to the worker-owned heap, set in - // `Worker::create` and never reassigned; the heap is pinned for the - // process lifetime (workers are never destroyed before exit). + // SAFETY: `self.arena` is a `BackRef` to `self.heap`, set in + // `Worker::create` and never reassigned. `Worker::get` already returns + // `&'static mut Worker`; callers are task callbacks that complete + // before `deinit_soon` tears the worker down, so the arena outlives + // every reference handed out here. unsafe { bun_ptr::detach_lifetime_ref(self.arena.get()) } } } From efe38b73af99b337104a1dd20a9332463fa01363 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Tue, 19 May 2026 08:18:46 +0000 Subject: [PATCH 31/32] bun_alloc: hard-assert BabyVec u32 capacity instead of clamping --- src/bun_alloc/baby_vec.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/bun_alloc/baby_vec.rs b/src/bun_alloc/baby_vec.rs index 8d35af0dd7c..f056c7cbde7 100644 --- a/src/bun_alloc/baby_vec.rs +++ b/src/bun_alloc/baby_vec.rs @@ -330,12 +330,15 @@ impl<'a, T> BabyVec<'a, T> { #[cold] fn grow_to(&mut self, at_least: usize) { - debug_assert!(at_least <= u32::MAX as usize, "BabyVec capacity overflow"); + assert!(at_least <= u32::MAX as usize, "BabyVec capacity overflow"); if Self::T_IS_ZST { return; } - // Same growth as `Vec`: max(2×cap, at_least, 4). - let new_cap = (self.cap as usize * 2).max(at_least).max(4); + // Same growth as `Vec`: max(2×cap, at_least, 4), capped at u32::MAX. + let new_cap = (self.cap as usize * 2) + .max(at_least) + .max(4) + .min(u32::MAX as usize); self.grow_exact(new_cap); } @@ -344,8 +347,7 @@ impl<'a, T> BabyVec<'a, T> { if Self::T_IS_ZST { return; } - debug_assert!(new_cap <= u32::MAX as usize, "BabyVec capacity overflow"); - let new_cap = new_cap.min(u32::MAX as usize); + assert!(new_cap <= u32::MAX as usize, "BabyVec capacity overflow"); let new_layout = Layout::array::(new_cap).unwrap_or_else(|_| crate::out_of_memory()); let new_ptr = if self.cap == 0 { (&self.alloc) From 3f4f1c3cbf745df69849f973d1a36f707f489bac Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Tue, 19 May 2026 08:18:52 +0000 Subject: [PATCH 32/32] bun_alloc: release-assert full range in BabyVec::drain --- src/bun_alloc/baby_vec.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/bun_alloc/baby_vec.rs b/src/bun_alloc/baby_vec.rs index f056c7cbde7..686ee901e03 100644 --- a/src/bun_alloc/baby_vec.rs +++ b/src/bun_alloc/baby_vec.rs @@ -300,17 +300,21 @@ impl<'a, T> BabyVec<'a, T> { unsafe { slice::from_raw_parts_mut(me.ptr.as_ptr(), me.len as usize) } } - /// Drain all elements (only the full range is supported; debug-asserted). + /// Drain all elements. Only the full range is supported — the `RangeBounds` + /// parameter exists for drop-in `ArenaVec` alias parity with `Vec::drain(..)`. /// Zig `BabyList` has no partial drain and no caller needs one. pub fn drain>(&mut self, range: R) -> IntoIter<'a, T> { use core::ops::Bound::*; - debug_assert!(matches!(range.start_bound(), Unbounded | Included(0))); - debug_assert!(match range.end_bound() { - Unbounded => true, - Excluded(n) => *n == self.len as usize, - Included(n) => *n + 1 == self.len as usize, - }); - let _ = range; + // Const-folded for `..`; guards release builds against partial ranges. + assert!( + matches!(range.start_bound(), Unbounded | Included(0)) + && match range.end_bound() { + Unbounded => true, + Excluded(n) => *n == self.len as usize, + Included(n) => *n + 1 == self.len as usize, + }, + "BabyVec::drain only supports the full range", + ); core::mem::replace(self, BabyVec::new_in(self.alloc)).into_iter() }