diff --git a/Cargo.lock b/Cargo.lock index 3ae3d5fb238..0fa93bb8aef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1600,7 +1600,6 @@ dependencies = [ "bun_collections", "bun_core", "bun_http_types", - "bun_js_parser", "bun_options_types", "bun_paths", "bun_ptr", @@ -2136,6 +2135,7 @@ dependencies = [ "bun_alloc", "bun_collections", "bun_core", + "bun_opaque", "bun_paths", "bun_wyhash", "const_format", diff --git a/src/bun_core/output.rs b/src/bun_core/output.rs index 703dc80dcb2..999d2d54e87 100644 --- a/src/bun_core/output.rs +++ b/src/bun_core/output.rs @@ -1746,16 +1746,6 @@ pub fn clear_to_end() { // - reset // - reset -/// Lowercase lookup wrapper. The table -/// itself lives in `bun_output_tags` (shared with the `pretty_fmt!` proc-macro -/// so there is exactly one copy). -pub mod color_map { - #[inline] - pub fn get(name: &[u8]) -> Option<&'static str> { - bun_output_tags::color_for_bytes(name) - } -} - pub use ansi::{BOLD, DIM, RESET}; pub use bun_output_tags::{ansi, ansi_b}; @@ -2057,82 +2047,9 @@ pub fn pretty_fmt_args( } /// Runtime `` → ANSI rewriter, used for testing the proc-macro and for -/// the rare dynamic case. -/// -/// Colour table lives in `bun_output_tags`; the state machine is kept duplicated -/// vs `bun_core_macros::rewrite` because the two intentionally diverge in the -/// `{` arm (proc-macro rewrites specs `{s}`→`{}`; this side copies braces -/// verbatim) and on unknown tags (proc-macro errors; this side emits `""`). -pub fn pretty_fmt_runtime(fmt: &[u8], is_enabled: bool) -> Vec { - let mut out = Vec::with_capacity(fmt.len() * 4); - let mut i = 0usize; - while i < fmt.len() { - match fmt[i] { - b'\\' => { - i += 1; - if i < fmt.len() { - match fmt[i] { - b'<' | b'>' => { - out.push(fmt[i]); - i += 1; - } - _ => { - out.push(b'\\'); - out.push(fmt[i]); - i += 1; - } - } - } - } - b'>' => { - i += 1; - } - b'{' => { - while i < fmt.len() && fmt[i] != b'}' { - out.push(fmt[i]); - i += 1; - } - } - b'<' => { - i += 1; - let mut is_reset = i < fmt.len() && fmt[i] == b'/'; - if is_reset { - i += 1; - } - let start = i; - while i < fmt.len() && fmt[i] != b'>' { - i += 1; - } - let color_name = &fmt[start..i]; - let color_str: &str = 'picker: { - if let Some(lit) = color_map::get(color_name) { - break 'picker lit; - } else if color_name == b"r" { - is_reset = true; - break 'picker ""; - } else { - // Unknown tag: the `pretty_fmt!` proc-macro rejects - // this at its call sites; this runtime path drops the - // tag. - break 'picker ""; - } - }; - if is_enabled { - out.extend_from_slice(if is_reset { - RESET.as_bytes() - } else { - color_str.as_bytes() - }); - } - } - _ => { - out.push(fmt[i]); - i += 1; - } - } - } - out -} +/// the rare dynamic case. The implementation lives in `bun_output_tags` so the +/// `bun_clap_macros` proc-macro crate shares the same state machine. +pub use bun_output_tags::pretty_fmt_runtime; #[doc(hidden)] #[inline] diff --git a/src/bun_core/string/mod.rs b/src/bun_core/string/mod.rs index 01c10021cde..a27181b07e3 100644 --- a/src/bun_core/string/mod.rs +++ b/src/bun_core/string/mod.rs @@ -2438,8 +2438,12 @@ pub mod printer { /// `MutableString`, and any other `crate::io::Write` sink. pub use crate::io::Write as PrinterWriter; + // PERF: `ascii_only` is a *runtime* arg so the large callers + // (`write_pre_quoted_string_inner`, `bun_js_printer::estimate_length_for_utf8`) + // collapse to a single monomorphization instead of one per + // (ascii_only × quote_char × …) combo — see `write_pre_quoted_string_inner`. #[inline] - pub(crate) fn can_print_without_escape(c: i32, ascii_only: bool) -> bool { + pub fn can_print_without_escape(c: i32, ascii_only: bool) -> bool { if c <= LAST_ASCII as i32 { c >= FIRST_ASCII as i32 && c != b'\\' as i32 @@ -2456,9 +2460,20 @@ pub mod printer { } } - /// Same algorithm as `bun_js_printer::write_pre_quoted_string`. - /// PERF: (quote_char, ascii_only, json, encoding) are runtime params — - /// profile if it shows up on a hot path. + /// `strings::Encoding` stand-in that derives `ConstParamTy` so it can be + /// used as a const-generic parameter (`const ENCODING: Encoding`). The + /// variant set is identical; convert at the boundary if a + /// `strings::Encoding` is ever needed. + #[derive(Clone, Copy, Debug, PartialEq, Eq, core::marker::ConstParamTy)] + pub enum Encoding { + Ascii, + Utf8, + Latin1, + Utf16, + } + + /// Runtime-encoding adapter: selects the matching monomorphized + /// [`write_pre_quoted_string_inner`] instance. pub fn write_pre_quoted_string( text_in: &[u8], writer: &mut W, @@ -2467,68 +2482,119 @@ pub mod printer { json: bool, encoding: StrEncoding, ) -> Result<(), crate::Error> { - debug_assert!(!json || quote_char == b'"'); - // utf16 view over the same bytes (only used when encoding == Utf16). - // Callers pass 2-byte-aligned even-length input for Utf16; `cast_slice` - // panics (rather than UB) if that contract is violated. - let text16: &[u16] = if encoding == StrEncoding::Utf16 { - crate::cast_slice::(text_in) - } else { - &[] - }; - let n: usize = if encoding == StrEncoding::Utf16 { - text16.len() - } else { - text_in.len() - }; + match encoding { + StrEncoding::Ascii => write_pre_quoted_string_inner::( + text_in, writer, quote_char, ascii_only, json, + ), + StrEncoding::Utf8 => write_pre_quoted_string_inner::( + text_in, writer, quote_char, ascii_only, json, + ), + StrEncoding::Latin1 => write_pre_quoted_string_inner::( + text_in, writer, quote_char, ascii_only, json, + ), + StrEncoding::Utf16 => write_pre_quoted_string_inner::( + text_in, writer, quote_char, ascii_only, json, + ), + } + } + + /// `quote_char` / `ascii_only` / `json` are runtime args: the branches on + /// them are cheap and well-predicted, and collapsing the monomorphizations + /// keeps the hot transpile pages dense. `ENCODING` stays `const` — it + /// changes the code-unit indexing structure of the loop, so a per-encoding + /// copy is genuinely different code. + #[inline(never)] + pub fn write_pre_quoted_string_inner( + text_in: &[u8], + writer: &mut W, + quote_char: u8, + ascii_only: bool, + json: bool, + ) -> Result<(), crate::Error> + where + W: PrinterWriter + ?Sized, + { + debug_assert!( + !(json && quote_char != b'"'), + "for json, quote_char must be '\"'" + ); + + let text = text_in; let mut i: usize = 0; + let n: usize = match ENCODING { + Encoding::Utf16 => text.len() / 2, + _ => text.len(), + }; + + macro_rules! code_unit_at { + ($idx:expr) => { + match ENCODING { + Encoding::Utf16 => { + let lo = text[$idx * 2]; + let hi = text[$idx * 2 + 1]; + u16::from_le_bytes([lo, hi]) as i32 + } + _ => text[$idx] as i32, + } + }; + } while i < n { - let width: u8 = match encoding { - StrEncoding::Latin1 | StrEncoding::Ascii | StrEncoding::Utf16 => 1, - StrEncoding::Utf8 => strings::wtf8_byte_sequence_length_with_invalid(text_in[i]), + let width: u8 = match ENCODING { + Encoding::Latin1 | Encoding::Ascii => 1, + Encoding::Utf8 => strings::wtf8_byte_sequence_length_with_invalid(text[i]), + Encoding::Utf16 => 1, }; let clamped_width = (width as usize).min(n.saturating_sub(i)); - let c: i32 = match encoding { - StrEncoding::Utf8 => { - let mut buf = [0u8; 4]; - buf[..clamped_width].copy_from_slice(&text_in[i..i + clamped_width]); - strings::decode_wtf8_rune_t::(buf, width, 0) + let c: i32 = match ENCODING { + Encoding::Utf8 => { + let bytes: [u8; 4] = match clamped_width { + 1 => [text[i], 0, 0, 0], + 2 => [text[i], text[i + 1], 0, 0], + 3 => [text[i], text[i + 1], text[i + 2], 0], + 4 => [text[i], text[i + 1], text[i + 2], text[i + 3]], + _ => unreachable!(), + }; + strings::decode_wtf8_rune_t::(bytes, width, 0) + } + Encoding::Ascii => { + debug_assert!(text[i] <= 0x7F); + text[i] as i32 } - StrEncoding::Ascii => { - debug_assert!(text_in[i] <= 0x7F); - text_in[i] as i32 + Encoding::Latin1 => text[i] as i32, + Encoding::Utf16 => { + // TODO: if this is a part of a surrogate pair, we could parse the whole codepoint in order + // to emit it as a single \u{result} rather than two paired \uLOW\uHIGH. + // eg: "\u{10334}" will convert to "𐌴" without this. + code_unit_at!(i) } - StrEncoding::Latin1 => text_in[i] as i32, - StrEncoding::Utf16 => text16[i] as i32, }; if can_print_without_escape(c, ascii_only) { - match encoding { - StrEncoding::Ascii | StrEncoding::Utf8 => { - let remain = &text_in[i + clamped_width..]; + match ENCODING { + Encoding::Ascii | Encoding::Utf8 => { + let remain = &text[i + clamped_width..]; if let Some(j) = strings::index_of_needs_escape_for_java_script_string( remain, quote_char, ) { - writer.write_all(&text_in[i..i + clamped_width])?; - i += clamped_width; - writer.write_all(&remain[..j as usize])?; - i += j as usize; + let j = j as usize; + writer.write_all(&text[i..i + clamped_width + j])?; + i += clamped_width + j; } else { - writer.write_all(&text_in[i..])?; + writer.write_all(&text[i..])?; break; } } - StrEncoding::Latin1 | StrEncoding::Utf16 => { - let mut cp = [0u8; 4]; - let cp_len = strings::encode_wtf8_rune(&mut cp, c as u32); - writer.write_all(&cp[..cp_len])?; + Encoding::Latin1 | Encoding::Utf16 => { + let mut codepoint_bytes = [0u8; 4]; + let codepoint_len = + strings::encode_wtf8_rune(&mut codepoint_bytes, c as u32); + writer.write_all(&codepoint_bytes[..codepoint_len])?; i += clamped_width; } } continue; } - match c { 0x07 => { writer.write_all(b"\\x07")?; @@ -2572,11 +2638,16 @@ pub mod printer { } 0x24 => { if quote_char == b'`' { - let next_is_brace = match encoding { - StrEncoding::Utf16 => i + 1 < n && text16[i + 1] == b'{' as u16, - _ => i + 1 < n && text_in[i + 1] == b'{', + let next = if i + clamped_width < n { + Some(code_unit_at!(i + clamped_width)) + } else { + None }; - writer.write_all(if next_is_brace { b"\\$" } else { b"$" })?; + if next == Some(b'{' as i32) { + writer.write_all(b"\\$")?; + } else { + writer.write_all(b"$")?; + } } else { writer.write_all(b"$")?; } @@ -2588,6 +2659,7 @@ pub mod printer { } _ => { i += width as usize; + if c <= 0xFF && !json { let h = hex2_upper(c as u8); writer.write_all(&[b'\\', b'x', h[0], h[1]])?; @@ -2609,9 +2681,22 @@ pub mod printer { bytes: &mut MutableString, ascii_only: bool, ) -> Result<(), crate::Error> { - // PERF: consider pre-growing via an estimated UTF-8 length — profile if it shows up on a hot path. + // `ascii_only` is threaded at runtime so + // the heavy escaper isn't monomorphized per ascii_only/quote-char combo. + // + // Heuristic reservation (~12.5% slack) instead of a full + // escaped-length pre-scan, which would do a SIMD scan + per-escape rune + // decode over `text` just to size the buffer — the same work + // `write_pre_quoted_string_inner` repeats immediately below. + // Tab-indented JS (e.g. three.js) has ~9.4% of bytes needing 2-byte + // escapes (tabs + newlines + quotes/backslashes), so 6.25% slack would + // under-shoot and force a 2x doubling memcpy of the whole source. The + // writer still grows on demand if this under-shoots. + bytes.grow_if_needed(text.len() + (text.len() >> 3) + 8)?; bytes.append_char(b'"')?; - write_pre_quoted_string(text, bytes, b'"', ascii_only, true, StrEncoding::Utf8)?; + write_pre_quoted_string_inner::<_, { Encoding::Utf8 }>( + text, bytes, b'"', ascii_only, true, + )?; bytes.append_char(b'"').expect("unreachable"); Ok(()) } diff --git a/src/bun_output_tags/lib.rs b/src/bun_output_tags/lib.rs index 239866f1505..5d11753d813 100644 --- a/src/bun_output_tags/lib.rs +++ b/src/bun_output_tags/lib.rs @@ -1,10 +1,13 @@ -//! Single source of truth for Bun's `` → ANSI colour table **and** the +//! Single source of truth for Bun's `` → ANSI colour table, the //! named ANSI escape constants used directly by REPL / diff-printer / multi-run -//! output. Zero-dep `#![no_std]` leaf so both the proc-macro crate and runtime -//! crates can import it without a cycle. +//! output, **and** the runtime `` rewriter ([`pretty_fmt_runtime`]). +//! Zero-dep `#![no_std]` (+`alloc`) leaf so both the proc-macro crates and +//! runtime crates can import it without a cycle. #![no_std] +extern crate alloc; + /// Named ANSI SGR escape sequences. One canonical literal per colour/attribute; /// every other crate aliases this module rather than re-declaring the bytes. /// @@ -100,3 +103,84 @@ pub fn color_for_bytes(name: &[u8]) -> Option<&'static str> { } None } + +/// Runtime `` → ANSI rewriter: expands tags to escape sequences when +/// `is_enabled`, or strips them when not. `\<`/`\>` escapes pass the bracket +/// through, `{…}` spec bodies are copied verbatim, `` and `` emit +/// [`RESET`], and unknown tags are dropped silently. +/// +/// Shared by `bun_core::output` (CLI logging) and `bun_clap_macros` (which runs +/// it at macro-expansion time with `is_enabled = false` to bake tag-stripped +/// help strings into rodata). The `pretty_fmt!` proc-macro keeps its own +/// deliberately divergent variant (`{s}`→`{}` spec rewriting, compile errors on +/// unknown tags). +#[inline] +pub fn pretty_fmt_runtime(fmt: &[u8], is_enabled: bool) -> alloc::vec::Vec { + // `* 2` covers the worst-case expansion: the shortest tags (``, ``, + // 3 bytes) become 5-byte escapes, a ratio under 2. + let mut out = alloc::vec::Vec::with_capacity(fmt.len() * 2); + let mut i = 0usize; + while i < fmt.len() { + match fmt[i] { + b'\\' => { + i += 1; + if i < fmt.len() { + match fmt[i] { + b'<' | b'>' => { + out.push(fmt[i]); + i += 1; + } + _ => { + out.push(b'\\'); + out.push(fmt[i]); + i += 1; + } + } + } + } + b'>' => { + i += 1; + } + b'{' => { + while i < fmt.len() && fmt[i] != b'}' { + out.push(fmt[i]); + i += 1; + } + } + b'<' => { + i += 1; + let mut is_reset = i < fmt.len() && fmt[i] == b'/'; + if is_reset { + i += 1; + } + let start = i; + while i < fmt.len() && fmt[i] != b'>' { + i += 1; + } + let name = &fmt[start..i]; + let seq: &str = if let Some(c) = color_for_bytes(name) { + c + } else if name == b"r" { + is_reset = true; + "" + } else { + // Unknown tag: dropped silently (the `pretty_fmt!` + // proc-macro rejects these at compile time instead). + "" + }; + if is_enabled { + out.extend_from_slice(if is_reset { + RESET.as_bytes() + } else { + seq.as_bytes() + }); + } + } + _ => { + out.push(fmt[i]); + i += 1; + } + } + } + out +} diff --git a/src/clap_macros/lib.rs b/src/clap_macros/lib.rs index 0969a83031f..2ccad13dc5e 100644 --- a/src/clap_macros/lib.rs +++ b/src/clap_macros/lib.rs @@ -265,91 +265,17 @@ fn byte_str_b(s: &[u8]) -> LitByteStr { LitByteStr::new(s, Span::call_site()) } -/// 1:1 port of `bun_core::output::pretty_fmt_runtime` — rewrites Bun's `` -/// colour markup to ANSI escape sequences when `is_enabled`, or strips it when -/// not. Run here at macro-expansion time with `is_enabled = false` so each param -/// description's tag-stripped form (`Help::msg_plain`) is a `const` byte literal -/// in rodata. The ANSI form is *not* baked in — it is rare (only `bun --help` on -/// a colour TTY) and would otherwise roughly triple the help-string rodata, so -/// `bun_clap::pretty_help_desc` derives it from `Help::msg` on demand instead. -fn pretty_rewrite(fmt: &[u8], is_enabled: bool) -> Vec { - use bun_output_tags::{RESET, color_for_bytes}; - let mut out: Vec = Vec::with_capacity(fmt.len() * 2); - let mut i = 0usize; - while i < fmt.len() { - match fmt[i] { - b'\\' => { - i += 1; - if i < fmt.len() { - match fmt[i] { - b'<' | b'>' => { - out.push(fmt[i]); - i += 1; - } - _ => { - out.push(b'\\'); - out.push(fmt[i]); - i += 1; - } - } - } - } - b'>' => { - i += 1; - } - b'{' => { - while i < fmt.len() && fmt[i] != b'}' { - out.push(fmt[i]); - i += 1; - } - } - b'<' => { - i += 1; - let mut is_reset = i < fmt.len() && fmt[i] == b'/'; - if is_reset { - i += 1; - } - let start = i; - while i < fmt.len() && fmt[i] != b'>' { - i += 1; - } - let name = &fmt[start..i]; - let seq: &str = if let Some(c) = color_for_bytes(name) { - c - } else if name == b"r" { - is_reset = true; - "" - } else { - // Unknown tag: `pretty_fmt_runtime` (the path this replaces) - // drops it silently. Match - // the lenient runtime behaviour — a compile error would be - // stricter than what shipped, and param specs don't carry - // unknown tags anyway. - "" - }; - if is_enabled { - out.extend_from_slice(if is_reset { - RESET.as_bytes() - } else { - seq.as_bytes() - }); - } - } - _ => { - out.push(fmt[i]); - i += 1; - } - } - } - out -} - fn emit_param(krate: &Path, p: &Param) -> TokenStream2 { let msg = byte_str(&p.id.msg); // Precompute only the tag-stripped form (the non-TTY help path needs it ready - // without a TTY check); the ANSI form is derived lazily from `msg` by - // `bun_clap::pretty_help_desc`, so it stays out of rodata. - let msg_plain = byte_str_b(&pretty_rewrite(p.id.msg.as_bytes(), false)); + // without a TTY check) so it is a `const` byte literal in rodata; the ANSI + // form is derived lazily from `msg` by `bun_clap::pretty_help_desc` — it is + // rare (only `bun --help` on a colour TTY) and baking it in would roughly + // triple the help-string rodata. + let msg_plain = byte_str_b(&bun_output_tags::pretty_fmt_runtime( + p.id.msg.as_bytes(), + false, + )); let value = byte_str(&p.id.value); let short = match p.names.short { diff --git a/src/collections/StaticHashMap.rs b/src/collections/StaticHashMap.rs index 4769089f1c2..b4b0a5fbf23 100644 --- a/src/collections/StaticHashMap.rs +++ b/src/collections/StaticHashMap.rs @@ -281,10 +281,6 @@ impl< Ok(()) } - pub fn put_context(&mut self, key: K, value: V, _ctx: Ctx) -> Result<(), AllocError> { - self.put(key, value) - } - pub fn get_or_put(&mut self, key: K) -> Result, AllocError> { self.ensure_unused_capacity(1)?; Ok(self.get_or_put_assume_capacity(key)) diff --git a/src/collections/array_hash_map.rs b/src/collections/array_hash_map.rs index f7a8c3b2948..2cffcc8141b 100644 --- a/src/collections/array_hash_map.rs +++ b/src/collections/array_hash_map.rs @@ -567,18 +567,6 @@ impl ArrayHashMap { self.drop_index(); } - /// Same as `ensure_total_capacity` but takes an explicit `ctx` for the - /// stored key type. Capacity reservation is purely a Vec operation here, - /// so the context is accepted and ignored. - #[inline] - pub fn ensure_total_capacity_context( - &mut self, - n: usize, - _ctx: Ctx, - ) -> Result<(), AllocError> { - self.ensure_total_capacity(n) - } - /// Insert/replace using an externally-supplied /// hash/eql context instead of the stored `C`. Used when `C = AutoContext` /// can't satisfy `K: Hash` (e.g. `bun_semver::String`, whose hash needs the @@ -933,17 +921,6 @@ impl ArrayHashMap { } } - /// Mutable access to the entry at `index` (key + value). Returns `None` if - /// `index >= len`. Mirrors `indexmap::IndexMap::get_index_mut`. - pub fn get_index_mut(&mut self, index: usize) -> Option<(&mut K, &mut V)> { - if index >= self.keys.len() { - return None; - } - // `keys` and `values` are distinct struct fields; borrowck permits one - // `&mut` into each simultaneously. Bound proven above. - Some((&mut self.keys[index], &mut self.values[index])) - } - /// Remove the entry at `index` by swapping in the last /// entry. O(1); does not preserve insertion order. Returns the removed pair. pub fn swap_remove_at(&mut self, index: usize) -> (K, V) { @@ -1925,15 +1902,6 @@ impl StringHashMap Ok(()) } - /// Insert a pre-boxed key without re-allocating it. Uses `try_reserve` so - /// OOM surfaces as `Err` instead of aborting; callers can roll back side - /// effects on failure. - pub fn put_owned(&mut self, key: Box<[u8], A>, value: V) -> Result<(), AllocError> { - self.inner.try_reserve(1).map_err(|_| AllocError)?; - self.inner.insert(StringHashMapKey::owned(key), value); - Ok(()) - } - /// PERF: std::HashMap cannot skip the grow check, so this is /// just `put` without the `Result`. #[inline] diff --git a/src/collections/array_list.rs b/src/collections/array_list.rs index 510ddce2f96..e3a7e9badfe 100644 --- a/src/collections/array_list.rs +++ b/src/collections/array_list.rs @@ -79,24 +79,11 @@ impl ArrayListAlignedIn { } } - // Sentinel-terminated owned slices are not a Rust type, so this takes a `Box<[T]>` with - // the sentinel already stripped. - pub fn from_owned_slice_sentinel(/* sentinel: T, */ slice: Slice) -> Self { - Self { - unmanaged: Vec::from(slice), - } - } - /// This method empties `self`. pub fn move_to_unmanaged(&mut self) -> Unmanaged { mem::take(&mut self.unmanaged) } - /// Unlike `move_to_unmanaged`, this method *consumes* `self`. - pub fn into_unmanaged_with_allocator(self) -> (Unmanaged, ()) { - (self.unmanaged, ()) - } - /// The contents of `unmanaged` must have been allocated by the global allocator. /// This function takes ownership of `unmanaged`. pub fn from_unmanaged(unmanaged: Unmanaged) -> Self { @@ -134,31 +121,6 @@ impl ArrayListAlignedIn { self.unmanaged.insert(i, item); } - /// Note that this creates *shallow* copies of `value`. - pub fn add_many_at( - &mut self, - index: usize, - value: T, - count: usize, - ) -> Result<&mut [T], AllocError> - where - T: Clone, - { - self.unmanaged - .splice(index..index, core::iter::repeat_n(value, count)); - Ok(&mut self.unmanaged[index..index + count]) - } - - /// Note that this creates *shallow* copies of `value`. - pub fn add_many_at_assume_capacity(&mut self, index: usize, value: T, count: usize) -> &mut [T] - where - T: Clone, - { - self.unmanaged - .splice(index..index, core::iter::repeat_n(value, count)); - &mut self.unmanaged[index..index + count] - } - /// Note that this `Clone`s each element of `new_items`. pub fn insert_slice(&mut self, index: usize, new_items: &[T]) -> Result<(), AllocError> where @@ -186,15 +148,6 @@ impl ArrayListAlignedIn { Ok(()) } - /// This method `Drop`s the removed items. - /// Note that this `Clone`s each element of `new_items` (see `insert_slice`). - pub fn replace_range_assume_capacity(&mut self, start: usize, len: usize, new_items: &[T]) - where - T: Clone, - { - let _ = self.replace_range(start, len, new_items); - } - pub fn append(&mut self, item: T) -> Result<(), AllocError> { self.unmanaged.push(item); Ok(()) @@ -229,25 +182,6 @@ impl ArrayListAlignedIn { self.unmanaged.extend_from_slice(new_items); } - /// Note that this `Clone`s each element of `new_items` (see `insert_slice`). - pub fn append_unaligned_slice(&mut self, new_items: &[T]) -> Result<(), AllocError> - where - T: Clone, - { - // Rust `&[T]` is always naturally aligned, so this is identical to `append_slice`; a - // caller that truly has unaligned bytes needs `ptr::read_unaligned` at the call site. - self.unmanaged.extend_from_slice(new_items); - Ok(()) - } - - /// Note that this `Clone`s each element of `new_items` (see `insert_slice`). - pub fn append_unaligned_slice_assume_capacity(&mut self, new_items: &[T]) - where - T: Clone, - { - self.unmanaged.extend_from_slice(new_items); - } - /// Note that this creates *shallow* copies of `value`. #[inline] pub fn append_n_times(&mut self, value: T, n: usize) -> Result<(), AllocError> @@ -340,19 +274,6 @@ impl ArrayListAlignedIn { &items[items.len() - 1] } - pub fn get_last_mut(&mut self) -> &mut T { - let len = self.unmanaged.len(); - &mut self.unmanaged[len - 1] - } - - pub fn get_last_or_null(&self) -> Option<&T> { - if self.is_empty() { - None - } else { - Some(self.get_last()) - } - } - pub fn is_empty(&self) -> bool { self.items().is_empty() } diff --git a/src/collections/bit_set.rs b/src/collections/bit_set.rs index c1b843e5139..1674c321f8f 100644 --- a/src/collections/bit_set.rs +++ b/src/collections/bit_set.rs @@ -317,23 +317,6 @@ impl IntegerBitSet { other.subset_of(self) } - /// Returns the complement bit sets. Bits in the result - /// are set if the corresponding bits were not set. - pub fn complement(self) -> Self { - let mut result = self; - result.toggle_all(); - result - } - - /// Returns the union of two bit sets. Bits in the - /// result are set if the corresponding bits were set - /// in either input. - pub fn union_with(self, other: Self) -> Self { - let mut result = self; - result.set_union(other); - result - } - /// Returns the intersection of two bit sets. Bits in /// the result are set if the corresponding bits were /// set in both inputs. @@ -343,24 +326,6 @@ impl IntegerBitSet { result } - /// Returns the xor of two bit sets. Bits in the - /// result are set if the corresponding bits were - /// not the same in both inputs. - pub fn xor_with(self, other: Self) -> Self { - let mut result = self; - result.toggle_set(other); - result - } - - /// Returns the difference of two bit sets. Bits in - /// the result are set if set in the first but not - /// set in the second set. - pub fn difference_with(self, other: Self) -> Self { - let mut result = self; - result.set_intersection(other.complement()); - result - } - /// Iterates through the items in the set, according to the options. /// The default options (.{}) will iterate indices of set bits in /// ascending order. Modifications to the underlying bit set may @@ -669,23 +634,6 @@ impl ArrayBitSet { other.subset_of(self) } - /// Returns the complement bit sets. Bits in the result - /// are set if the corresponding bits were not set. - pub fn complement(&self) -> Self { - let mut result = *self; - result.toggle_all(); - result - } - - /// Returns the union of two bit sets. Bits in the - /// result are set if the corresponding bits were set - /// in either input. - pub fn union_with(&self, other: &Self) -> Self { - let mut result = *self; - result.set_union(other); - result - } - /// Returns the intersection of two bit sets. Bits in /// the result are set if the corresponding bits were /// set in both inputs. @@ -705,24 +653,6 @@ impl ArrayBitSet { false } - /// Returns the xor of two bit sets. Bits in the - /// result are set if the corresponding bits were - /// not the same in both inputs. - pub fn xor_with(&self, other: &Self) -> Self { - let mut result = *self; - result.toggle_set(other); - result - } - - /// Returns the difference of two bit sets. Bits in - /// the result are set if set in the first but not - /// set in the second set. - pub fn difference_with(&self, other: &Self) -> Self { - let mut result = *self; - result.set_intersection(&other.complement()); - result - } - /// Iterates through the items in the set, according to the options. /// The default options (.{}) will iterate indices of set bits in /// ascending order. Modifications to the underlying bit set may @@ -823,15 +753,6 @@ impl DynamicBitSetUnmanaged { unsafe { slice::from_raw_parts_mut(self.masks, n) } } - /// Raw pointer to the mask words. Use this (not `masks_slice{,_mut}`) when - /// `self` and another `DynamicBitSetUnmanaged` may point at the same - /// storage and both are accessed in the same operation — forming - /// overlapping `&mut [usize]` / `&[usize]` would be UB. - #[inline(always)] - pub fn masks_ptr(&self) -> *mut usize { - self.masks - } - /// `self.masks[i] = f(self.masks[i], other.masks[i])` for every mask word. /// Centralises the binary set-op loop (`set_union` / `set_intersection` / /// `set_exclude` / `toggle_set` / `copy_into`) behind a single audited @@ -1149,14 +1070,6 @@ impl DynamicBitSetUnmanaged { self.zip_masks_raw(other, |a, b| a & b); } - pub fn set_exclude_two(&mut self, other: &Self, third: &Self) { - debug_assert!(other.bit_length == self.bit_length); - // Two passes is equivalent to the original fused loop: each word is - // independent, so `(a & !b) & !c` per index is associative across passes. - self.zip_masks_raw(other, |a, b| a & !b); - self.zip_masks_raw(third, |a, c| a & !c); - } - pub fn set_exclude(&mut self, other: &Self) { debug_assert!(other.bit_length == self.bit_length); self.zip_masks_raw(other, |a, b| a & !b); diff --git a/src/collections/lib.rs b/src/collections/lib.rs index 57547112bce..76e48185743 100644 --- a/src/collections/lib.rs +++ b/src/collections/lib.rs @@ -420,13 +420,6 @@ impl SmallList { self.0.insert_many(index as usize, items.iter().cloned()) } #[inline] - pub fn insert_slice_assume_capacity(&mut self, index: u32, items: &[T]) - where - T: Clone, - { - self.0.insert_many(index as usize, items.iter().cloned()) - } - #[inline] pub fn pop(&mut self) -> Option { self.0.pop() } diff --git a/src/collections/multi_array_list.rs b/src/collections/multi_array_list.rs index 883f3d72888..85f9d835002 100644 --- a/src/collections/multi_array_list.rs +++ b/src/collections/multi_array_list.rs @@ -536,7 +536,7 @@ impl<'a, F> ColMut<'a, F> { } } -/// Index-based comparison context for `sort` / `sort_span` / `sort_unstable`. +/// Index-based comparison context for `sort` / `sort_unstable`. pub trait SortContext { fn less_than(&self, a_index: usize, b_index: usize) -> bool; } @@ -1208,21 +1208,11 @@ impl MultiArrayList { self.sort_internal::(0, self.len, ctx); } - /// Stable sort of `[a, b)` by index-based context. - pub fn sort_span(&mut self, a: usize, b: usize, ctx: &C) { - self.sort_internal::(a, b, ctx); - } - /// Unstable sort by index-based context. pub fn sort_unstable(&mut self, ctx: &C) { self.sort_internal::(0, self.len, ctx); } - /// Unstable sort of `[a, b)` by index-based context. - pub fn sort_span_unstable(&mut self, a: usize, b: usize, ctx: &C) { - self.sort_internal::(a, b, ctx); - } - pub fn capacity_in_bytes(capacity: usize) -> usize { Reflected::::ELEM_BYTES * capacity } diff --git a/src/collections/pool.rs b/src/collections/pool.rs index 7fc46c48bd0..c7f2a12e1f7 100644 --- a/src/collections/pool.rs +++ b/src/collections/pool.rs @@ -66,35 +66,6 @@ impl Node { self.next = std::ptr::from_mut::>(new_node); } - /// Remove a node from the list. - /// - /// Arguments: - /// node: Pointer to the node to be removed. - /// Returns: - /// node removed - pub fn remove_next(&mut self) -> Option<*mut Node> { - let next_node = if self.next.is_null() { - return None; - } else { - self.next - }; - self.next = Node::next_of(next_node); - Some(next_node) - } - - /// Iterate over the singly-linked list from this node, until the final node is found. - /// This operation is O(N). - pub fn find_last(&mut self) -> *mut Node { - let mut it: *mut Node = std::ptr::from_mut::>(self); - loop { - let next = Node::next_of(it); - if next.is_null() { - return it; - } - it = next; - } - } - /// Iterate over each next node, returning the count of all nodes except the starting one. /// This operation is O(N). pub fn count_children(&self) -> usize { diff --git a/src/ini/lib.rs b/src/ini/lib.rs index 5627368ebc2..122334a7647 100644 --- a/src/ini/lib.rs +++ b/src/ini/lib.rs @@ -1497,7 +1497,7 @@ mod draft { if let Some(public_hoist_pattern_expr) = out.get(b"public-hoist-pattern") { install.public_hoist_pattern = - match pnpm_matcher_from_expr(&public_hoist_pattern_expr, log, source, bump) { + match PnpmMatcher::from_expr(&public_hoist_pattern_expr, log, source) { Ok(v) => Some(v), Err(FromExprError::OutOfMemory) => return Err(AllocError), Err(_) => { @@ -1509,16 +1509,15 @@ mod draft { } if let Some(hoist_pattern_expr) = out.get(b"hoist-pattern") { - install.hoist_pattern = - match pnpm_matcher_from_expr(&hoist_pattern_expr, log, source, bump) { - Ok(v) => Some(v), - Err(FromExprError::OutOfMemory) => return Err(AllocError), - Err(_) => { - // error.InvalidRegExp, error.UnexpectedExpr - log.reset(); - None - } - }; + install.hoist_pattern = match PnpmMatcher::from_expr(&hoist_pattern_expr, log, source) { + Ok(v) => Some(v), + Err(FromExprError::OutOfMemory) => return Err(AllocError), + Err(_) => { + // error.InvalidRegExp, error.UnexpectedExpr + log.reset(); + None + } + }; } let mut registry_map = install.scoped.take().unwrap_or_default(); @@ -1799,125 +1798,7 @@ mod draft { Ok(()) } - use bun_install_types::NodeLinker::{ - Behavior as PnpmBehavior, CreateMatcherError, FromExprError, Matcher as PnpmMatcherEntry, - PnpmMatcher, create_matcher, - }; - - /// `PnpmMatcher.fromExpr` operating on - /// `bun_ast::Expr` instead of the lower-tier `bun_ast::Expr`. - /// - /// `bun_install_types` (T2) cannot depend on `bun_js_parser` (T4), - /// and the two `ExprData` enums are distinct (closed Rust enums; only the leaf - /// `E::*` payloads are shared). `bun_ini` depends on both, so the T4-typed - /// overload lives here. The matcher construction is delegated to the shared - /// `create_matcher` helper in `bun_install_types::NodeLinker`. - fn pnpm_matcher_from_expr( - expr: &Expr, - log: &mut Log, - source: &Source, - bump: &Arena, - ) -> Result { - let mut buf: Vec = Vec::new(); - - // bun.jsc.initialize(false) is performed lazily inside the regex vtable - // compile hook (tier-6 owns it). - - let mut matchers: Vec = Vec::new(); - let mut has_include = false; - let mut has_exclude = false; - - match &expr.data { - ExprData::EString(s) => { - // SAFETY: arena-backed `EString::slice` mutates only its own - // resolved-data cache; the StoreRef pointee outlives this call. - let s_mut: &mut E::EString = unsafe { &mut *s.as_ptr() }; - let pattern = s_mut.slice(bump); - let matcher = match create_matcher(pattern, &mut buf) { - Ok(m) => m, - Err(CreateMatcherError::OutOfMemory) => return Err(FromExprError::OutOfMemory), - Err(CreateMatcherError::InvalidRegExp) => { - log.add_error_fmt_opts( - format_args!("Invalid regex: {}", bstr::BStr::new(pattern)), - bun_ast::AddErrorOptions { - loc: expr.loc, - redact_sensitive_information: true, - source: Some(source), - ..Default::default() - }, - ); - return Err(FromExprError::InvalidRegExp); - } - }; - has_include = has_include || !matcher.is_exclude; - has_exclude = has_exclude || matcher.is_exclude; - matchers.push(matcher); - } - ExprData::EArray(patterns) => { - for pattern_expr in patterns.items.slice() { - if let Some(pattern) = pattern_expr.as_string_cloned(bump)? { - let matcher = match create_matcher(pattern, &mut buf) { - Ok(m) => m, - Err(CreateMatcherError::OutOfMemory) => { - return Err(FromExprError::OutOfMemory); - } - Err(CreateMatcherError::InvalidRegExp) => { - log.add_error_fmt_opts( - format_args!("Invalid regex: {}", bstr::BStr::new(pattern)), - bun_ast::AddErrorOptions { - loc: pattern_expr.loc, - redact_sensitive_information: true, - source: Some(source), - ..Default::default() - }, - ); - return Err(FromExprError::InvalidRegExp); - } - }; - has_include = has_include || !matcher.is_exclude; - has_exclude = has_exclude || matcher.is_exclude; - matchers.push(matcher); - } else { - log.add_error_opts( - b"Expected a string or an array of strings", - bun_ast::AddErrorOptions { - loc: pattern_expr.loc, - redact_sensitive_information: true, - source: Some(source), - ..Default::default() - }, - ); - return Err(FromExprError::UnexpectedExpr); - } - } - } - _ => { - log.add_error_opts( - b"Expected a string or an array of strings", - bun_ast::AddErrorOptions { - loc: expr.loc, - redact_sensitive_information: true, - source: Some(source), - ..Default::default() - }, - ); - return Err(FromExprError::UnexpectedExpr); - } - } - - let behavior = if !has_include { - PnpmBehavior::AllMatchersExclude - } else if !has_exclude { - PnpmBehavior::AllMatchersInclude - } else { - PnpmBehavior::HasExcludeAndIncludeMatchers - }; - - Ok(PnpmMatcher { - matchers: matchers.into_boxed_slice(), - behavior, - }) - } + use bun_install_types::NodeLinker::{FromExprError, PnpmMatcher}; fn handle_auth( v: &mut NpmRegistry, diff --git a/src/io/PipeWriter.rs b/src/io/PipeWriter.rs index 2e53891c22f..53a3cca1480 100644 --- a/src/io/PipeWriter.rs +++ b/src/io/PipeWriter.rs @@ -2363,44 +2363,56 @@ impl WindowsStreamingWriter { } } + /// Blocking write for the `Source::SyncFile` path. Encodes via + /// `write_or_fallback`, then loops `sys::write` until drained. + #[inline] + fn write_sync_file( + &mut self, + buffer_u8: Option<&[u8]>, + buffer_u16: Option<&[u16]>, + kind: WriteKind, + ) -> WriteResult { + let fd = Fd::from_uv(match &self.source { + Some(Source::SyncFile(f)) => f.file, + _ => unreachable!(), + }); + let result = (|| { + let remain = match self.outgoing.write_or_fallback(buffer_u8, buffer_u16, kind) { + Ok(r) => r, + Err(_) => return WriteResult::Err(sys::Error::oom()), + }; + let initial_len = remain.len(); + let mut remain = remain; + + while remain.len() > 0 { + match sys::write(fd, remain) { + sys::Result::Err(err) => return WriteResult::Err(err), + sys::Result::Ok(wrote) => { + remain = &remain[wrote..]; + if wrote == 0 { + break; + } + } + } + } + + let wrote = initial_len - remain.len(); + if wrote == 0 { + return WriteResult::Done(wrote); + } + WriteResult::Wrote(wrote) + })(); + self.outgoing.reset(); + result + } + fn write_internal_u8(&mut self, buffer: &[u8], kind: WriteKind) -> WriteResult { if self.is_done { return WriteResult::Done(0); } if matches!(self.source, Some(Source::SyncFile(_))) { - let result = (|| { - let remain = match self.outgoing.write_or_fallback(Some(buffer), None, kind) { - Ok(r) => r, - Err(_) => return WriteResult::Err(sys::Error::oom()), - }; - let initial_len = remain.len(); - let mut remain = remain; - let fd = Fd::from_uv(match &self.source { - Some(Source::SyncFile(f)) => f.file, - _ => unreachable!(), - }); - - while remain.len() > 0 { - match sys::write(fd, remain) { - sys::Result::Err(err) => return WriteResult::Err(err), - sys::Result::Ok(wrote) => { - remain = &remain[wrote..]; - if wrote == 0 { - break; - } - } - } - } - - let wrote = initial_len - remain.len(); - if wrote == 0 { - return WriteResult::Done(wrote); - } - WriteResult::Wrote(wrote) - })(); - self.outgoing.reset(); - return result; + return self.write_sync_file(Some(buffer), None, kind); } let had_buffered_data = self.outgoing.is_not_empty(); @@ -2425,42 +2437,7 @@ impl WindowsStreamingWriter { } if matches!(self.source, Some(Source::SyncFile(_))) { - let result = (|| { - let remain = - match self - .outgoing - .write_or_fallback(None, Some(buffer), WriteKind::Utf16) - { - Ok(r) => r, - Err(_) => return WriteResult::Err(sys::Error::oom()), - }; - let initial_len = remain.len(); - let mut remain = remain; - let fd = Fd::from_uv(match &self.source { - Some(Source::SyncFile(f)) => f.file, - _ => unreachable!(), - }); - - while remain.len() > 0 { - match sys::write(fd, remain) { - sys::Result::Err(err) => return WriteResult::Err(err), - sys::Result::Ok(wrote) => { - remain = &remain[wrote..]; - if wrote == 0 { - break; - } - } - } - } - - let wrote = initial_len - remain.len(); - if wrote == 0 { - return WriteResult::Done(wrote); - } - WriteResult::Wrote(wrote) - })(); - self.outgoing.reset(); - return result; + return self.write_sync_file(None, Some(buffer), WriteKind::Utf16); } let had_buffered_data = self.outgoing.is_not_empty(); diff --git a/src/io/posix_event_loop.rs b/src/io/posix_event_loop.rs index 942da6b2d8d..46fbeac0d28 100644 --- a/src/io/posix_event_loop.rs +++ b/src/io/posix_event_loop.rs @@ -2,7 +2,6 @@ use core::ffi::c_int; use core::ffi::c_void; use core::fmt; -#[cfg(unix)] use core::ptr; #[cfg(not(windows))] @@ -287,6 +286,107 @@ pub enum AllocatorType { Mini, } +/// Pure flag-accessor methods whose bodies are identical for the POSIX and +/// Windows `FilePoll` types (distinct structs — see `windows_event_loop`). +/// Expanded inside each platform's `impl FilePoll`; `Flags` and `Fd` resolve +/// at the expansion site. Platform-divergent methods (the keep-alive / +/// activate family) stay in their own modules — their flag semantics differ. +macro_rules! impl_file_poll_flag_methods { + () => { + #[inline] + pub fn is_active(&self) -> bool { + self.flags.contains(Flags::HasIncrementedPollCount) + } + + #[inline] + pub fn is_watching(&self) -> bool { + !self.flags.contains(Flags::NeedsRearm) + && (self.flags.contains(Flags::PollReadable) + || self.flags.contains(Flags::PollWritable) + || self.flags.contains(Flags::PollProcess)) + } + + pub fn is_registered(&self) -> bool { + self.flags.contains(Flags::PollWritable) + || self.flags.contains(Flags::PollReadable) + || self.flags.contains(Flags::PollProcess) + || self.flags.contains(Flags::PollMachport) + } + + pub fn clear_event(&mut self, flag: Flags) { + self.flags.remove(flag); + } + + pub fn is_readable(&mut self) -> bool { + let readable = self.flags.contains(Flags::Readable); + self.flags.remove(Flags::Readable); + readable + } + + pub fn is_hup(&mut self) -> bool { + let readable = self.flags.contains(Flags::Hup); + self.flags.remove(Flags::Hup); + readable + } + + pub fn is_eof(&mut self) -> bool { + let readable = self.flags.contains(Flags::Eof); + self.flags.remove(Flags::Eof); + readable + } + + pub fn is_writable(&mut self) -> bool { + let readable = self.flags.contains(Flags::Writable); + self.flags.remove(Flags::Writable); + readable + } + + #[inline] + pub fn can_unref(&self) -> bool { + self.flags.contains(Flags::HasIncrementedPollCount) + } + + #[inline] + pub fn file_descriptor(&self) -> Fd { + self.fd + } + }; +} +// Only `windows_event_loop` needs the path-based re-export; this module +// invokes the macros textually. +#[cfg(windows)] +pub(crate) use impl_file_poll_flag_methods; + +/// `PollSlot` impl shared verbatim by both platform `FilePoll` types; both +/// have the `next_to_free` / `flags` fields the trait contract requires. +macro_rules! impl_poll_slot { + ($t:ty) => { + impl $crate::posix_event_loop::PollSlot for $t { + #[inline] + unsafe fn next_to_free(p: *mut Self) -> *mut Self { + // SAFETY: caller upholds the trait-level contract (`p` is a live hive + // slot; raw-pointer field op only). + unsafe { (*p).next_to_free } + } + #[inline] + unsafe fn set_next_to_free(p: *mut Self, next: *mut Self) { + // SAFETY: caller upholds the trait-level contract. + unsafe { (*p).next_to_free = next } + } + #[inline] + unsafe fn ignore_updates(p: *mut Self) { + // SAFETY: caller upholds the trait-level contract. + unsafe { + (*p).flags + .insert($crate::posix_event_loop::Flags::IgnoreUpdates) + }; + } + } + }; +} +#[cfg(windows)] +pub(crate) use impl_poll_slot; + // `FilePoll`/`Store` here are POSIX-specific (kqueue/epoll registration, // generation_number, allocator_type). On Windows the variants live in // `windows_event_loop`; the shared `EventLoopCtxVTable` above names @@ -370,33 +470,7 @@ impl FilePoll { self.on_update(0); } - pub fn clear_event(&mut self, flag: Flags) { - self.flags.remove(flag); - } - - pub fn is_readable(&mut self) -> bool { - let readable = self.flags.contains(Flags::Readable); - self.flags.remove(Flags::Readable); - readable - } - - pub fn is_hup(&mut self) -> bool { - let readable = self.flags.contains(Flags::Hup); - self.flags.remove(Flags::Hup); - readable - } - - pub fn is_eof(&mut self) -> bool { - let readable = self.flags.contains(Flags::Eof); - self.flags.remove(Flags::Eof); - readable - } - - pub fn is_writable(&mut self) -> bool { - let readable = self.flags.contains(Flags::Writable); - self.flags.remove(Flags::Writable); - readable - } + impl_file_poll_flag_methods!(); // Note: not `impl Drop` — FilePoll is pool-allocated (HiveArray) and explicitly // put back via `Store::put`; Drop would be wrong here. @@ -437,13 +511,6 @@ impl FilePoll { self.deinit_possibly_defer(vm, false); } - pub fn is_registered(&self) -> bool { - self.flags.contains(Flags::PollWritable) - || self.flags.contains(Flags::PollReadable) - || self.flags.contains(Flags::PollProcess) - || self.flags.contains(Flags::PollMachport) - } - pub fn on_update(&mut self, size_or_offset: i64) { if self.flags.contains(Flags::OneShot) && !self.flags.contains(Flags::NeedsRearm) { self.flags.insert(Flags::NeedsRearm); @@ -459,19 +526,6 @@ impl FilePoll { unsafe { __bun_run_file_poll(self, size_or_offset) }; } - #[inline] - pub fn is_active(&self) -> bool { - self.flags.contains(Flags::HasIncrementedPollCount) - } - - #[inline] - pub fn is_watching(&self) -> bool { - !self.flags.contains(Flags::NeedsRearm) - && (self.flags.contains(Flags::PollReadable) - || self.flags.contains(Flags::PollWritable) - || self.flags.contains(Flags::PollProcess)) - } - /// This decrements the active counter if it was previously incremented /// "active" controls whether or not the event loop should potentially idle pub fn disable_keeping_process_alive(&mut self, event_loop_ctx: EventLoopCtx) { @@ -586,11 +640,6 @@ impl FilePoll { !self.flags.contains(Flags::HasIncrementedPollCount) } - #[inline] - pub fn can_unref(&self) -> bool { - self.flags.contains(Flags::HasIncrementedPollCount) - } - /// Prevent a poll from keeping the process alive. pub fn unref(&mut self, event_loop_ctx: EventLoopCtx) { syslog!("unref"); @@ -614,11 +663,6 @@ impl FilePoll { self.deactivate(event_loop_ctx.loop_mut()); } - #[inline] - pub fn file_descriptor(&self) -> Fd { - self.fd - } - pub fn register(&mut self, loop_: &mut Loop, flag: Flags, one_shot: bool) -> sys::Result<()> { self.register_with_fd( loop_, @@ -1349,25 +1393,49 @@ impl fmt::Display for FlagsFormatter { // `bun_alloc::heap_breakdown` is a no-op outside macOS Instruments // heap-breakdown builds, so the 128-slot hive is unconditional here (same // choice as `RuntimeTranspilerStore`'s TranspilerJob hive). -#[cfg(not(windows))] const HIVE_SIZE: usize = 128; + +/// Raw-pointer field accessors implemented by the platform `FilePoll` types so +/// the deferred-free [`PollStore`] can link slots intrusively. +/// +/// Every method shares one contract: `p` is a live, fully-initialized hive +/// slot, and the body performs raw-pointer field ops only — materializing a +/// `&mut Self` would alias the `&mut PollStore` borrow that covers the inline +/// hive buffer (Stacked Borrows UB). +pub trait PollSlot: Sized { + /// # Safety + /// See the trait-level contract. + unsafe fn next_to_free(p: *mut Self) -> *mut Self; + /// # Safety + /// See the trait-level contract. + unsafe fn set_next_to_free(p: *mut Self, next: *mut Self); + /// Insert `Flags::IgnoreUpdates`. + /// # Safety + /// See the trait-level contract. + unsafe fn ignore_updates(p: *mut Self); +} + #[cfg(not(windows))] -type FilePollHive = bun_collections::hive_array::Fallback; +pub type Store = PollStore; + +#[cfg(not(windows))] +impl_poll_slot!(FilePoll); /// We defer freeing FilePoll until the end of the next event loop iteration /// This ensures that we don't free a FilePoll before the next callback is called -#[cfg(not(windows))] -pub struct Store { - hive: FilePollHive, - pending_free_head: *mut FilePoll, - pending_free_tail: *mut FilePoll, +pub struct PollStore { + hive: bun_collections::hive_array::Fallback, + pending_free_head: *mut P, + pending_free_tail: *mut P, } -#[cfg(not(windows))] -impl Store { - pub fn init() -> Store { - Store { - hive: FilePollHive::init(), +impl PollStore

{ + pub fn init() -> Self { + // `hive.put` recycles slots without honoring drop glue on the deferred + // path's assumptions; the store requires plain-old-data slots. + const { assert!(!core::mem::needs_drop::

()) }; + PollStore { + hive: bun_collections::hive_array::Fallback::init(), pending_free_head: ptr::null_mut(), pending_free_tail: ptr::null_mut(), } @@ -1375,7 +1443,7 @@ impl Store { /// Claim a hive slot and move `value` into it. Infallible (heap fallback). #[inline] - pub fn get_init(&mut self, value: FilePoll) -> ptr::NonNull { + pub fn get_init(&mut self, value: P) -> ptr::NonNull

{ self.hive.get_init(value) } @@ -1384,13 +1452,14 @@ impl Store { while !next.is_null() { let current = next; // SAFETY: intrusive list; nodes were allocated by this hive. Walk via - // raw-pointer reads/writes only — materializing a `&mut FilePoll` + // raw-pointer reads/writes only — materializing a `&mut P` // here would alias the `&mut self.hive` borrow taken by `put()` // below (the slot may live inside the inline hive array). unsafe { - next = (*current).next_to_free; - (*current).next_to_free = ptr::null_mut(); - // FilePoll has no drop glue; `put` is a no-op drop + recycle. + next = P::next_to_free(current); + P::set_next_to_free(current, ptr::null_mut()); + // `P` has no drop glue (asserted in `init`); `put` is a no-op + // drop + recycle. self.hive.put(current); } } @@ -1399,29 +1468,29 @@ impl Store { } /// `poll` is a live, fully-initialized slot in `self.hive`. It may point - /// *inside* `self.hive`'s inline `[FilePoll; 128]` buffer, so accepting it - /// as `&mut FilePoll` while `&mut self` is live would retag overlapping + /// *inside* `self.hive`'s inline `[P; 128]` buffer, so accepting it + /// as `&mut P` while `&mut self` is live would retag overlapping /// storage under Stacked Borrows (UB). Take it as a raw pointer and /// touch fields only through raw pointer ops — same /// rationale as `process_deferred_frees` above. - pub fn put(&mut self, poll: ptr::NonNull, vm: EventLoopCtx, ever_registered: bool) { + pub fn put(&mut self, poll: ptr::NonNull

, vm: EventLoopCtx, ever_registered: bool) { let poll = poll.as_ptr(); if !ever_registered { - // SAFETY: `poll` is a fully-initialized hive slot; FilePoll has no + // SAFETY: `poll` is a fully-initialized hive slot; `P` has no // drop glue, so `put` is a no-op drop + recycle. unsafe { self.hive.put(poll) }; return; } // SAFETY: `poll` is a live hive slot (see fn-level comment); raw read of a POD field. - debug_assert!(unsafe { (*poll).next_to_free }.is_null()); + debug_assert!(unsafe { P::next_to_free(poll) }.is_null()); if !self.pending_free_tail.is_null() { debug_assert!(!self.pending_free_head.is_null()); // SAFETY: tail is non-null and points into the hive. unsafe { - debug_assert!((*self.pending_free_tail).next_to_free.is_null()); - (*self.pending_free_tail).next_to_free = poll; + debug_assert!(P::next_to_free(self.pending_free_tail).is_null()); + P::set_next_to_free(self.pending_free_tail, poll); } } @@ -1431,7 +1500,7 @@ impl Store { } // SAFETY: see fn-level comment — raw-pointer field access only. - unsafe { (*poll).flags.insert(Flags::IgnoreUpdates) }; + unsafe { P::ignore_updates(poll) }; self.pending_free_tail = poll; let callback: OpaqueCallback = Self::process_deferred_frees_thunk; @@ -1441,16 +1510,16 @@ impl Store { ); vm.set_after_event_loop_callback( Some(callback), - core::ptr::NonNull::new(std::ptr::from_mut::(self).cast::()), + core::ptr::NonNull::new(std::ptr::from_mut::(self).cast::()), ); } // Safe fn item: module-private thunk, only coerced to the C-ABI // `OpaqueCallback` fn-pointer type — never callable by name outside - // `Store`. Body wraps its raw-ptr op explicitly. + // the store. Body wraps its raw-ptr op explicitly. extern "C" fn process_deferred_frees_thunk(ctx: *mut c_void) { - // SAFETY: ctx was set to `self as *mut Store` in `put` above. - let this = unsafe { bun_ptr::callback_ctx::(ctx) }; + // SAFETY: ctx was set to `self as *mut Self` in `put` above. + let this = unsafe { bun_ptr::callback_ctx::(ctx) }; this.process_deferred_frees(); } } diff --git a/src/io/windows_event_loop.rs b/src/io/windows_event_loop.rs index 572945b71d8..528d53f4dd9 100644 --- a/src/io/windows_event_loop.rs +++ b/src/io/windows_event_loop.rs @@ -1,4 +1,3 @@ -use core::ffi::c_void; use core::ptr; use bun_sys::Fd; @@ -40,31 +39,13 @@ pub struct FilePoll { } impl FilePoll { - #[inline] - pub fn is_active(&self) -> bool { - self.flags.contains(Flags::HasIncrementedPollCount) - } - - #[inline] - pub fn is_watching(&self) -> bool { - !self.flags.contains(Flags::NeedsRearm) - && (self.flags.contains(Flags::PollReadable) - || self.flags.contains(Flags::PollWritable) - || self.flags.contains(Flags::PollProcess)) - } + posix::impl_file_poll_flag_methods!(); #[inline] pub fn is_keeping_process_alive(&self) -> bool { !self.flags.contains(Flags::Closed) && self.is_active() } - pub fn is_registered(&self) -> bool { - self.flags.contains(Flags::PollWritable) - || self.flags.contains(Flags::PollReadable) - || self.flags.contains(Flags::PollProcess) - || self.flags.contains(Flags::PollMachport) - } - /// Make calling ref() on this poll into a no-op. pub fn disable_keeping_process_alive(&mut self, vm: EventLoopCtx) { if self.flags.contains(Flags::Closed) { @@ -102,11 +83,6 @@ impl FilePoll { self.deinit_with_vm(js_vm_ctx()); } - #[inline] - pub fn file_descriptor(&self) -> Fd { - self.fd - } - pub fn deinit_force_unregister(&mut self) { self.deinit() } @@ -150,34 +126,6 @@ impl FilePoll { vm.file_polls_mut().put(this, vm, was_ever_registered); } - pub fn is_readable(&mut self) -> bool { - let readable = self.flags.contains(Flags::Readable); - self.flags.remove(Flags::Readable); - readable - } - - pub fn is_hup(&mut self) -> bool { - let readable = self.flags.contains(Flags::Hup); - self.flags.remove(Flags::Hup); - readable - } - - pub fn is_eof(&mut self) -> bool { - let readable = self.flags.contains(Flags::Eof); - self.flags.remove(Flags::Eof); - readable - } - - pub fn clear_event(&mut self, flag: Flags) { - self.flags.remove(flag); - } - - pub fn is_writable(&mut self) -> bool { - let readable = self.flags.contains(Flags::Writable); - self.flags.remove(Flags::Writable); - readable - } - pub fn deinit_with_vm(&mut self, vm: EventLoopCtx) { // `loop_mut()` — crate-private nonnull-asref accessor (single deref in // `EventLoopCtx`); the uws loop is a disjoint allocation from `self`. @@ -233,11 +181,6 @@ impl FilePoll { !self.flags.contains(Flags::HasIncrementedPollCount) } - #[inline] - pub fn can_unref(&self) -> bool { - self.flags.contains(Flags::HasIncrementedPollCount) - } - pub fn on_ended(&mut self, event_loop_ctx: EventLoopCtx) { self.flags.remove(Flags::KeepsEventLoopAlive); self.flags.insert(Flags::Closed); @@ -267,104 +210,9 @@ impl FilePoll { } } -type FilePollHiveArray = bun_collections::hive_array::Fallback; - -pub struct Store { - hive: FilePollHiveArray, - pending_free_head: *mut FilePoll, - pending_free_tail: *mut FilePoll, -} - -impl Store { - pub fn init() -> Store { - Store { - hive: FilePollHiveArray::init(), - pending_free_head: ptr::null_mut(), - pending_free_tail: ptr::null_mut(), - } - } - - #[inline] - pub fn get_init(&mut self, value: FilePoll) -> ptr::NonNull { - self.hive.get_init(value) - } - - pub fn process_deferred_frees(&mut self) { - let mut next = self.pending_free_head; - while !next.is_null() { - let current = next; - // SAFETY: intrusive deferred-free list; nodes are valid HiveArray slots - // until put(). Walk via raw-pointer reads/writes only — materializing a - // `&mut FilePoll` here would alias the `&mut self.hive` borrow taken by - // `put()` below (the slot may live inside the inline hive buffer). - unsafe { - next = (*current).next_to_free; - (*current).next_to_free = ptr::null_mut(); - // FilePoll has no drop glue; `put` is a no-op drop + recycle. - self.hive.put(current); - } - } - self.pending_free_head = ptr::null_mut(); - self.pending_free_tail = ptr::null_mut(); - } - - /// `poll` is a live, fully-initialized slot in `self.hive`. Touched only - /// through raw pointer ops to avoid forming a `&mut FilePoll` that would - /// alias `&mut self` (the hive buffer is inline storage). - pub fn put(&mut self, poll: ptr::NonNull, vm: EventLoopCtx, ever_registered: bool) { - let poll = poll.as_ptr(); - if !ever_registered { - // SAFETY: `poll` is a fully-initialized hive slot; FilePoll has no - // drop glue, so `put` is a no-op drop + recycle. - unsafe { self.hive.put(poll) }; - return; - } - - // SAFETY: `poll` is a valid HiveArray slot pointer. It may live inside - // `self.hive.buffer`, so we access it via raw pointer only (no `&mut FilePoll` - // materialized) to avoid aliasing `&mut self`. - debug_assert!(unsafe { (*poll).next_to_free }.is_null()); - - let tail = self.pending_free_tail; - if !tail.is_null() { - debug_assert!(!self.pending_free_head.is_null()); - // SAFETY: `tail` is a valid slot in the intrusive deferred-free list; - // raw-ptr access avoids a second `&mut FilePoll` overlapping `poll`/`self`. - debug_assert!(unsafe { (*tail).next_to_free }.is_null()); - unsafe { (*tail).next_to_free = poll }; - } - - if self.pending_free_head.is_null() { - self.pending_free_head = poll; - debug_assert!(self.pending_free_tail.is_null()); - } - - // SAFETY: see above — short-lived field borrow through raw `poll`, no overlap held. - unsafe { (*poll).flags.insert(Flags::IgnoreUpdates) }; - self.pending_free_tail = poll; +pub type Store = posix::PollStore; - let callback: OpaqueCallback = Self::process_deferred_frees_thunk; - debug_assert!( - vm.after_event_loop_callback().is_none() - || vm.after_event_loop_callback().map(|f| f as usize) == Some(callback as usize) - ); - vm.set_after_event_loop_callback( - Some(callback), - core::ptr::NonNull::new(core::ptr::from_mut::(self).cast::()), - ); - } - - // Safe fn item: module-private thunk, only coerced to the C-ABI - // `OpaqueCallback` fn-pointer type — never callable by name outside - // `Store`. Body wraps its raw-ptr op explicitly. - extern "C" fn process_deferred_frees_thunk(ctx: *mut c_void) { - // SAFETY: `ctx` was set to `self as *mut Store` in `put` above. The thunk fires - // from the event loop's after-tick hook with no other `&mut Store` borrow live, - // so this is the unique accessor (safe-single-owner). - let this = unsafe { bun_ptr::callback_ctx::(ctx) }; - this.process_deferred_frees(); - } -} +posix::impl_poll_slot!(FilePoll); pub struct Waker { // `BackRef`: `WindowsLoop::get()` hands out the shared diff --git a/src/paths/lib.rs b/src/paths/lib.rs index b8598cfd244..8552b263233 100644 --- a/src/paths/lib.rs +++ b/src/paths/lib.rs @@ -684,12 +684,6 @@ pub mod fs { self.base } - /// The dir component, or `"."` when it is empty. - #[inline] - pub fn dir_or_dot(&self) -> &'a [u8] { - if self.dir.is_empty() { b"." } else { self.dir } - } - /// Formats [`Self::non_unique_name_string_base`] as a valid JS /// identifier. #[inline] @@ -924,23 +918,6 @@ pub mod fs { } } - /// Same const-concat caveat as `init_with_namespace_virtual`: - /// callers pass the precomputed `concatcp!` result as `pretty`. - #[inline] - pub const fn init_for_kit_built_in( - namespace: &'static [u8], - pretty: &'static [u8], - text: &'static [u8], - ) -> Path<'static> { - Path { - pretty, - is_symlink: true, - text, - namespace, - is_disabled: false, - } - } - /// Debug-only check that `pretty` /// contains no backslashes (Windows). No-op on POSIX. #[inline] diff --git a/src/paths/resolve_path.rs b/src/paths/resolve_path.rs index 8f9ec1cb2d0..0c66d1e155b 100644 --- a/src/paths/resolve_path.rs +++ b/src/paths/resolve_path.rs @@ -48,9 +48,6 @@ pub fn z<'a>(input: &[u8], output: &'a mut PathBuffer) -> &'a ZStr { } type IsSeparatorFunc = fn(char: u8) -> bool; -// Rust cannot express "fn(T) -> bool" as a value, so the generic-`T` -// callers dispatch via Platform methods instead of fn pointers. -type LastSeparatorFunction = fn(slice: &[u8]) -> Option; #[inline(always)] fn is_dotdot_with_type(slice: &[T]) -> bool { @@ -1246,14 +1243,6 @@ impl Platform { } } - pub const fn get_last_separator_func(self) -> LastSeparatorFunction { - match self { - Platform::Loose => last_index_of_separator_loose, - Platform::Nt | Platform::Windows => last_index_of_separator_windows, - Platform::Posix => last_index_of_separator_posix, - } - } - #[inline(always)] pub fn is_separator(self, char: u8) -> bool { self.is_separator_t::(char) diff --git a/src/router/Cargo.toml b/src/router/Cargo.toml index f4a43995a09..8c80a339c35 100644 --- a/src/router/Cargo.toml +++ b/src/router/Cargo.toml @@ -32,7 +32,3 @@ bun_resolver.workspace = true bun_sys.workspace = true bun_url.workspace = true bun_wyhash.workspace = true - -[dev-dependencies] -# Test harness (Test::make / Test::make_routes) needs the AST stores. -bun_js_parser.workspace = true diff --git a/src/router/lib.rs b/src/router/lib.rs index 743513c9ff9..ba237ede7de 100644 --- a/src/router/lib.rs +++ b/src/router/lib.rs @@ -1940,278 +1940,13 @@ pub mod pattern { pub use pattern::Pattern; // ────────────────────────────────────────────────────────────────────────── -// Tests + test helpers +// Tests // ────────────────────────────────────────────────────────────────────────── #[cfg(test)] mod tests { use super::*; - struct MockRequestContextType { - controlled: bool, - url: URLPath, - match_file_path_buf: [u8; 1024], - - handle_request_called: bool, - redirect_called: bool, - matched_route: Option>, - has_called_done: bool, - } - - impl Default for MockRequestContextType { - fn default() -> Self { - Self { - controlled: false, - url: URLPath::default(), - match_file_path_buf: [0; 1024], - handle_request_called: false, - redirect_called: false, - matched_route: None, - has_called_done: false, - } - } - } - - impl MockRequestContextType { - fn handle_request(&mut self) -> Result<(), bun_core::Error> { - self.handle_request_called = true; - Ok(()) - } - - fn handle_redirect(&mut self, _: &[u8]) -> Result<(), bun_core::Error> { - self.redirect_called = true; - Ok(()) - } - } - - struct JavaScriptHandler; - impl JavaScriptHandler { - fn enqueue( - _: &mut MockRequestContextType, - _: &mut MockServer, - _: &mut route_param::List<'_>, - ) -> Result<(), bun_core::Error> { - Ok(()) - } - } - - pub struct MockServer { - watchloop_handle: Option, - watcher: MockWatcher, - } - - impl Default for MockServer { - fn default() -> Self { - Self { - watchloop_handle: None, - watcher: MockWatcher::default(), - } - } - } - - #[derive(Default)] - pub struct MockWatcher { - watchloop_handle: Option, - } - impl MockWatcher { - pub fn start(&mut self) -> Result<(), bun_core::Error> { - Ok(()) - } - } - - fn make_test(cwd_path: &[u8], data: &[(&str, &str)]) -> Result<(), bun_core::Error> { - Output::init_test(); - debug_assert!(cwd_path.len() > 1 && cwd_path != b"/" && !cwd_path.ends_with(b"bun")); - let bun_tests_dir = bun_sys::Dir::cwd() - .make_open_path(b"bun-test-scratch", bun_sys::OpenDirOptions::default())?; - let _ = bun_tests_dir.delete_tree(cwd_path); - - let cwd = bun_tests_dir.make_open_path(cwd_path, bun_sys::OpenDirOptions::default())?; - bun_sys::fchdir(cwd.fd())?; - - for (name, value) in data { - let name_b = name.as_bytes(); - // NOTE: paths without a '/' have no parent dir to create; - // rposition on '/' finds the parent (test fixture paths are - // always forward-slash). - if let Some(slash) = name_b.iter().rposition(|&c| c == b'/') { - if slash > 0 { - cwd.make_path(&name_b[..slash])?; - } - } - let file = bun_sys::File::create(cwd.fd(), name_b, true)?; - file.write_all(value.as_bytes())?; - let _ = file.close(); - } - Ok(()) - } - - /// Newtype so the orphan rule lets us `impl ResolverLike` for a - /// foreign-crate type. - struct TestResolver<'a>(bun_resolver::Resolver<'a>); - - impl<'a> ResolverLike for TestResolver<'a> { - fn fs(&self) -> &'static FileSystem { - // SAFETY: process-static singleton (see `FileSystem::instance`). - unsafe { &*self.0.fs() } - } - fn fs_impl(&self) -> *mut Fs::Implementation { - // SAFETY: `&fs.fs` — the `Implementation` field of the singleton. - unsafe { core::ptr::from_mut(&mut (*self.0.fs()).fs) } - } - fn read_dir_info_ignore_error(&mut self, path: &[u8]) -> Option { - self.0.read_dir_info_ignore_error(path) - } - } - - pub struct Test; - - impl Test { - pub fn make_routes( - test_name: &'static str, - data: &[(&str, &str)], - ) -> Result { - Output::init_test(); - make_test(test_name.as_bytes(), data)?; - bun_ast::initialize_store(); - // const fs = try FileSystem.init(null); - let _ = bun_resolver::fs::FileSystem::init(None)?; - let top_level_dir = bun_resolver::fs::FileSystem::get().top_level_dir; - - // var pages_parts = [_]string{ top_level_dir, "pages" }; - // const pages_dir = try Fs.FileSystem.instance.absAlloc(default_allocator, &pages_parts); - let pages_parts: [&[u8]; 2] = [top_level_dir, b"pages"]; - let pages_dir = bun_resolver::fs::FileSystem::instance() - .abs_alloc(&pages_parts) - .map_err(|_| bun_core::err!("OutOfMemory"))?; - - // const router = try Router.init(&FileSystem.instance, default_allocator, RouteConfig{...}); - // SAFETY: process-static singleton just initialized above. - let fs_opaque: &'static FileSystem = unsafe { &*fs }; - let router = Router::init( - fs_opaque, - RouteConfig { - dir: pages_dir.to_vec().into_boxed_slice(), - routes_enabled: true, - extensions: vec![b"js".as_slice().into()].into_boxed_slice(), - ..RouteConfig::default() - }, - )?; - - let mut log = bun_ast::Log::init(); - // NOTE: `errdefer logger.print(Output.errorWriter())` — Rust has - // no errdefer; the test harness panics on error anyway, but the guard - // still flushes diagnostics on early-return for parity. - let _err_dump = scopeguard::guard(core::ptr::from_mut(&mut log), |log| { - // SAFETY: pointer to a stack local that outlives this guard. - let _ = unsafe { &*log }.print(bun_core::output::error_writer()); - }); - - // const opts = Options.BundleOptions{ .target = .browser, ... }; - // NOTE: the resolver-side `BundleOptions` subset omits - // `loaders`/`define`/`log`/`routes`/`entry_points`/`out_extensions`/ - // `transform_options` — none are read by `Resolver::init1` or the - // dir-info walk, so `Default` + `target` is the faithful projection. - let opts = bun_resolver::options::BundleOptions { - target: bun_ast::Target::Browser, - external: bun_resolver::options::ExternalModules::default(), - ..Default::default() - }; - - // var resolver = Resolver.init1(default_allocator, &logger, &FileSystem.instance, opts); - let mut resolver = TestResolver(bun_resolver::Resolver::init1( - core::ptr::NonNull::from(&mut log), - fs, - opts, - )); - - // const root_dir = (try resolver.readDirInfo(pages_dir)).?; - let root_dir = resolver - .0 - .read_dir_info(pages_dir)? - .ok_or_else(|| bun_core::err!("FileNotFound"))?; - - // return RouteLoader.loadAll(..., opts.routes, &logger, Resolver, &resolver, root_dir); - // SAFETY: `_err_dump` only re-derives `&*log` on drop (after this borrow ends). - let routes = RouteLoader::load_all( - router.config.clone(), - unsafe { &mut *core::ptr::from_mut(&mut log) }, - &mut resolver, - &root_dir, - top_level_dir, - ); - scopeguard::ScopeGuard::into_inner(_err_dump); - Ok(routes) - } - - pub fn make( - test_name: &'static str, - data: &[(&str, &str)], - ) -> Result, bun_core::Error> { - make_test(test_name.as_bytes(), data)?; - bun_ast::initialize_store(); - // const fs = try FileSystem.initWithForce(null, true); - let _ = bun_resolver::fs::FileSystem::init_with_force::(None)?; - let top_level_dir = bun_resolver::fs::FileSystem::get().top_level_dir; - - let pages_parts: [&[u8]; 2] = [top_level_dir, b"pages"]; - let pages_dir = bun_resolver::fs::FileSystem::instance() - .abs_alloc(&pages_parts) - .map_err(|_| bun_core::err!("OutOfMemory"))?; - - // var router = try Router.init(&FileSystem.instance, default_allocator, RouteConfig{...}); - // SAFETY: process-static singleton just initialized above. - let fs_opaque: &'static FileSystem = unsafe { &*fs }; - let mut router = Router::init( - fs_opaque, - RouteConfig { - dir: pages_dir.to_vec().into_boxed_slice(), - routes_enabled: true, - extensions: vec![b"js".as_slice().into()].into_boxed_slice(), - ..RouteConfig::default() - }, - )?; - - let mut log = bun_ast::Log::init(); - let _err_dump = scopeguard::guard(core::ptr::from_mut(&mut log), |log| { - // SAFETY: pointer to a stack local that outlives this guard. - let _ = unsafe { &*log }.print(bun_core::output::error_writer()); - }); - - let opts = bun_resolver::options::BundleOptions { - target: bun_ast::Target::Browser, - external: bun_resolver::options::ExternalModules::default(), - ..Default::default() - }; - - let mut resolver = TestResolver(bun_resolver::Resolver::init1( - core::ptr::NonNull::from(&mut log), - fs, - opts, - )); - - // const root_dir = (try resolver.readDirInfo(pages_dir)).?; - let root_dir = resolver - .0 - .read_dir_info(pages_dir)? - .ok_or_else(|| bun_core::err!("FileNotFound"))?; - - // try router.loadRoutes(&logger, root_dir, Resolver, &resolver, top_level_dir); - // SAFETY: `_err_dump` only re-derives `&*log` on drop (after this borrow ends). - router.load_routes( - unsafe { &mut *core::ptr::from_mut(&mut log) }, - &root_dir, - &mut resolver, - top_level_dir, - )?; - let entry_points = router.get_entry_points(); - - assert_eq!(data.len(), entry_points.len()); - scopeguard::ScopeGuard::into_inner(_err_dump); - Ok(router) - } - } - #[test] fn pattern_match() { type Entry = Param<'static>; diff --git a/src/semver/Version.rs b/src/semver/Version.rs index 8431e1c86e1..7ad5681473a 100644 --- a/src/semver/Version.rs +++ b/src/semver/Version.rs @@ -103,10 +103,6 @@ impl VersionType { impl VersionType { /// Assumes that there is only one buffer for all the strings - pub fn sort_gt(ctx: &[u8], lhs: Self, rhs: Self) -> bool { - Self::order_fn(ctx, lhs, rhs) == Ordering::Greater - } - pub fn order_fn(ctx: &[u8], lhs: Self, rhs: Self) -> Ordering { lhs.order(rhs, ctx, ctx) } diff --git a/src/semver/lib.rs b/src/semver/lib.rs index 3e0fd629010..a2701ae3c63 100644 --- a/src/semver/lib.rs +++ b/src/semver/lib.rs @@ -321,11 +321,35 @@ pub mod semver_string { const MAX_INLINE_LEN_M1: usize = String::MAX_INLINE_LEN - 1; match buf.len() { 0..=MAX_INLINE_LEN_M1 => true, + // A string of exactly MAX_INLINE_LEN bytes is only inlinable when its + // final byte's top bit is clear — that bit distinguishes inline data + // from a packed pointer. Non-ASCII strings of exactly 8 bytes hit this. Self::MAX_INLINE_LEN => buf[Self::MAX_INLINE_LEN - 1] & 0x80 == 0, _ => false, } } + /// Caller must ensure `in_.len() <= MAX_INLINE_LEN`. + /// Unrolled per length so this compiles to direct loads instead of a + /// memcpy libcall — short strings dominate lockfile/install parsing. + #[inline] + fn inline_bytes(in_: &[u8]) -> [u8; Self::MAX_INLINE_LEN] { + match in_.len() { + 0 => [0, 0, 0, 0, 0, 0, 0, 0], + 1 => [in_[0], 0, 0, 0, 0, 0, 0, 0], + 2 => [in_[0], in_[1], 0, 0, 0, 0, 0, 0], + 3 => [in_[0], in_[1], in_[2], 0, 0, 0, 0, 0], + 4 => [in_[0], in_[1], in_[2], in_[3], 0, 0, 0, 0], + 5 => [in_[0], in_[1], in_[2], in_[3], in_[4], 0, 0, 0], + 6 => [in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], 0, 0], + 7 => [in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], in_[6], 0], + Self::MAX_INLINE_LEN => [ + in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], in_[6], in_[7], + ], + _ => unreachable!(), + } + } + #[inline] pub fn is_inline(self) -> bool { self.bytes[Self::MAX_INLINE_LEN - 1] & 0x80 == 0 @@ -347,133 +371,34 @@ pub mod semver_string { // bun_install (or bun_install_types) as inherent helpers there. pub fn init(buf: &[u8], in_: &[u8]) -> String { - match in_.len() { - 0 => String::default(), - 1 => String { - bytes: [in_[0], 0, 0, 0, 0, 0, 0, 0], - }, - 2 => String { - bytes: [in_[0], in_[1], 0, 0, 0, 0, 0, 0], - }, - 3 => String { - bytes: [in_[0], in_[1], in_[2], 0, 0, 0, 0, 0], - }, - 4 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], 0, 0, 0, 0], - }, - 5 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], in_[4], 0, 0, 0], - }, - 6 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], 0, 0], - }, - 7 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], in_[6], 0], - }, - Self::MAX_INLINE_LEN => { - // If they use the final bit, then it's a big string. - // This should only happen for non-ascii strings that are exactly 8 bytes. - // so that's an edge-case - if in_[Self::MAX_INLINE_LEN - 1] >= 128 { - let ptr_bits: u64 = Pointer::init(buf, in_).to_bits(); - let packed: u64 = (ptr_bits & MAX_ADDRESSABLE_SPACE_MASK) | (1u64 << 63); - String { - bytes: packed.to_ne_bytes(), - } - } else { - String { - bytes: [ - in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], in_[6], in_[7], - ], - } - } + if Self::can_inline(in_) { + String { + bytes: Self::inline_bytes(in_), } - _ => { - let ptr_bits: u64 = Pointer::init(buf, in_).to_bits(); - let packed: u64 = (ptr_bits & MAX_ADDRESSABLE_SPACE_MASK) | (1u64 << 63); - String { - bytes: packed.to_ne_bytes(), - } + } else { + let ptr_bits: u64 = Pointer::init(buf, in_).to_bits(); + let packed: u64 = (ptr_bits & MAX_ADDRESSABLE_SPACE_MASK) | (1u64 << 63); + String { + bytes: packed.to_ne_bytes(), } } } pub fn init_inline(in_: &[u8]) -> String { debug_assert!(Self::can_inline(in_)); - match in_.len() { - 0 => String::default(), - 1 => String { - bytes: [in_[0], 0, 0, 0, 0, 0, 0, 0], - }, - 2 => String { - bytes: [in_[0], in_[1], 0, 0, 0, 0, 0, 0], - }, - 3 => String { - bytes: [in_[0], in_[1], in_[2], 0, 0, 0, 0, 0], - }, - 4 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], 0, 0, 0, 0], - }, - 5 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], in_[4], 0, 0, 0], - }, - 6 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], 0, 0], - }, - 7 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], in_[6], 0], - }, - 8 => String { - bytes: [ - in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], in_[6], in_[7], - ], - }, - _ => unreachable!(), + String { + bytes: Self::inline_bytes(in_), } } pub fn init_append_if_needed(buf: &mut Vec, in_: &[u8]) -> Result { - Ok(match in_.len() { - 0 => String::default(), - 1 => String { - bytes: [in_[0], 0, 0, 0, 0, 0, 0, 0], - }, - 2 => String { - bytes: [in_[0], in_[1], 0, 0, 0, 0, 0, 0], - }, - 3 => String { - bytes: [in_[0], in_[1], in_[2], 0, 0, 0, 0, 0], - }, - 4 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], 0, 0, 0, 0], - }, - 5 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], in_[4], 0, 0, 0], - }, - 6 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], 0, 0], - }, - 7 => String { - bytes: [in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], in_[6], 0], - }, - - Self::MAX_INLINE_LEN => { - // If they use the final bit, then it's a big string. - // This should only happen for non-ascii strings that are exactly 8 bytes. - // so that's an edge-case - if in_[Self::MAX_INLINE_LEN - 1] >= 128 { - Self::init_append(buf, in_)? - } else { - String { - bytes: [ - in_[0], in_[1], in_[2], in_[3], in_[4], in_[5], in_[6], in_[7], - ], - } - } - } - - _ => Self::init_append(buf, in_)?, - }) + if Self::can_inline(in_) { + Ok(String { + bytes: Self::inline_bytes(in_), + }) + } else { + Self::init_append(buf, in_) + } } pub fn init_append(buf: &mut Vec, in_: &[u8]) -> Result { diff --git a/src/sourcemap/Chunk.rs b/src/sourcemap/Chunk.rs index b7e87959f7a..2787df0ead9 100644 --- a/src/sourcemap/Chunk.rs +++ b/src/sourcemap/Chunk.rs @@ -55,20 +55,6 @@ impl Chunk { unsafe { core::ptr::read(self) } } - pub fn print_source_map_contents( - &self, - source: &Source, - mutable: &mut MutableString, - include_sources_contents: bool, - ) -> Result<(), bun_core::Error> { - print_source_map_contents_json::( - source, - mutable, - include_sources_contents, - self.buffer.list.as_slice(), - ) - } - /// `chunk.buffer` holds an InternalSourceMap blob (the runtime path). Re-encode /// to a standard VLQ "mappings" string before emitting JSON. pub fn print_source_map_contents_from_internal( diff --git a/src/sourcemap/lib.rs b/src/sourcemap/lib.rs index 2d58dd7c90b..35fcc207af0 100644 --- a/src/sourcemap/lib.rs +++ b/src/sourcemap/lib.rs @@ -1352,22 +1352,3 @@ fn find_source_mapping_url_u16(source: &[u16]) -> Option( - origin: &bun_url::URL<'_>, - source: &bun_ast::Source, - asset_prefix_path: &[u8], - writer: &mut W, -) -> bun_io::Result<()> { - writer.write_all(b"\n//# sourceMappingURL=")?; - writer.write_all(bun_core::strings::without_trailing_slash(origin.href))?; - if !asset_prefix_path.is_empty() { - writer.write_all(asset_prefix_path)?; - } - if !source.path.pretty.is_empty() && source.path.pretty[0] != b'/' { - writer.write_all(b"/")?; - } - writer.write_all(source.path.pretty)?; - writer.write_all(b".map")?; - Ok(()) -} diff --git a/src/threading/WaitGroup.rs b/src/threading/WaitGroup.rs index 584fea64aaa..256107994e4 100644 --- a/src/threading/WaitGroup.rs +++ b/src/threading/WaitGroup.rs @@ -31,10 +31,6 @@ impl WaitGroup { } } - pub fn add_unsynchronized(&mut self, n: usize) { - *self.raw_count.get_mut() += n; - } - pub fn add(&self, n: usize) { // Not Acquire because we don't need to synchronize with other tasks (each runs independently). // Not Release because there are no side effects that other threads depend on when they see diff --git a/src/threading/channel.rs b/src/threading/channel.rs index 075fa622404..6e20f6692c9 100644 --- a/src/threading/channel.rs +++ b/src/threading/channel.rs @@ -60,14 +60,6 @@ impl<'a, T: Copy> Channel> { } } -impl Channel> { - #[inline] - pub fn init_dynamic() -> Self { - // No allocator param; this non-AST crate uses the global mimalloc. - Self::with_buffer(LinearFifo::>::init()) - } -} - // `T: Copy` because `LinearFifo::write`/`read` are slice-copy based. All // in-tree channel payloads are POD; revisit if a non-`Copy` T appears. impl> Channel { @@ -91,11 +83,6 @@ impl> Channel { self.getters.broadcast(); } - pub fn try_write_item(&self, item: T) -> Result { - let wrote = self.write(core::slice::from_ref(&item))?; - Ok(wrote == 1) - } - pub fn write_item(&self, item: T) -> Result<(), ChannelError> { self.write_all(core::slice::from_ref(&item)) } diff --git a/src/threading/guarded.rs b/src/threading/guarded.rs index 180b4936ada..62173867a3b 100644 --- a/src/threading/guarded.rs +++ b/src/threading/guarded.rs @@ -78,15 +78,6 @@ impl GuardedBy { None } } - - /// Borrow the underlying raw [`Mutex`]. Needed by callers that split - /// `lock()`/`unlock()` across function boundaries (e.g. `Progress.rs` - /// porting `lock_api::RawMutex`) or pair this `Guarded` with a bare - /// [`Condition::wait`](crate::Condition::wait). - #[inline] - pub fn raw_mutex(&self) -> &Mutex { - &self.mutex - } } impl GuardedBy { diff --git a/src/threading/work_pool.rs b/src/threading/work_pool.rs index 8980a815497..60f4a0c98fe 100644 --- a/src/threading/work_pool.rs +++ b/src/threading/work_pool.rs @@ -146,10 +146,6 @@ impl WorkPool { POOL.get_or_init(create) } - pub fn schedule_batch(batch: Batch) { - Self::get().schedule(batch); - } - pub fn schedule(task: *mut Task) { Self::get().schedule(Batch::from(task)); } diff --git a/src/url/Cargo.toml b/src/url/Cargo.toml index 856e9e2013c..0b8fbedfa1b 100644 --- a/src/url/Cargo.toml +++ b/src/url/Cargo.toml @@ -20,6 +20,7 @@ libc.workspace = true bitflags.workspace = true bun_alloc.workspace = true bun_core.workspace = true +bun_opaque.workspace = true bun_collections.workspace = true # TODO(b1): bun_io gated — crate does not compile yet; local Write stub in lib.rs # bun_io.workspace = true diff --git a/src/url/lib.rs b/src/url/lib.rs index 12c404b1cb3..ac22636ad27 100644 --- a/src/url/lib.rs +++ b/src/url/lib.rs @@ -42,11 +42,12 @@ pub mod whatwg { use super::BunString as String; use super::strings; - /// Opaque handle to a heap-allocated WTF::URL (C++). Always behind `*mut URL`. - /// Construct via `from_string`/`from_utf8`; free via `deinit`. - #[repr(C)] - pub struct URL { - _opaque: [u8; 0], + bun_opaque::opaque_ffi! { + /// Opaque handle to a heap-allocated WTF::URL (C++). Always behind `*mut URL`. + /// Construct via `from_string`/`from_utf8`; free via `deinit`. + /// `!Send`/`!Sync` per the macro: WTF::URL holds non-atomically-refcounted + /// WTF::Strings, so the handle must stay on the thread that created it. + pub struct URL; } // Getters take `*const URL` — the C++ side (BunString.cpp) never mutates the @@ -122,12 +123,22 @@ pub mod whatwg { } impl URL { - pub fn from_string(str: &String) -> Option> { - let mut input = *str; + // `from_string`/`from_utf8` return an owned C++ heap pointer that the + // caller must free exactly once via `deinit`/`destroy`. + pub fn from_string(str: String) -> Option> { + let mut input = str; URL__fromString(&mut input) } pub fn from_utf8(input: &[u8]) -> Option> { - Self::from_string(&String::borrow_utf8(input)) + Self::from_string(String::borrow_utf8(input)) + } + /// By-value form of the free [`file_url_from_string`] helper. + pub fn file_url_from_string(str: String) -> String { + file_url_from_string(&str) + } + /// By-value form of the free [`path_from_file_url`] helper. + pub fn path_from_file_url(str: String) -> String { + path_from_file_url(&str) } /// Includes the leading '#'. pub fn hash(&self) -> String { @@ -185,6 +196,15 @@ pub mod whatwg { pub fn deinit(&mut self) { URL__deinit(self) } + /// Raw-pointer form of [`URL::deinit`]. + /// + /// # Safety + /// `this` must be a live heap pointer from `from_string`/`from_utf8` + /// (or the C++ side), freed exactly once. + pub unsafe fn destroy(this: *mut Self) { + // SAFETY: caller guarantees `this` is valid and uniquely owned. + unsafe { URL__deinit(&mut *this) } + } } } // Re-export the free helpers at crate root so lower-tier callers can write