diff --git a/crates/oxc_data_structures/Cargo.toml b/crates/oxc_data_structures/Cargo.toml index a4b7d3fe128e7..eaa09e5436ecc 100644 --- a/crates/oxc_data_structures/Cargo.toml +++ b/crates/oxc_data_structures/Cargo.toml @@ -26,9 +26,10 @@ ropey = { workspace = true, optional = true } [features] default = [] -all = ["assert_unchecked", "code_buffer", "inline_string", "rope", "stack"] +all = ["assert_unchecked", "code_buffer", "inline_string", "nonmax", "rope", "stack"] assert_unchecked = [] code_buffer = ["assert_unchecked"] inline_string = [] +nonmax = ["assert_unchecked"] rope = ["dep:ropey"] stack = ["assert_unchecked"] diff --git a/crates/oxc_data_structures/src/lib.rs b/crates/oxc_data_structures/src/lib.rs index 76309e61864db..198a1522a87f2 100644 --- a/crates/oxc_data_structures/src/lib.rs +++ b/crates/oxc_data_structures/src/lib.rs @@ -11,6 +11,9 @@ pub mod code_buffer; #[cfg(feature = "inline_string")] pub mod inline_string; +#[cfg(feature = "nonmax")] +pub mod nonmax; + #[cfg(feature = "rope")] pub mod rope; diff --git a/crates/oxc_data_structures/src/nonmax/mod.rs b/crates/oxc_data_structures/src/nonmax/mod.rs new file mode 100644 index 0000000000000..c9ffbb75fe859 --- /dev/null +++ b/crates/oxc_data_structures/src/nonmax/mod.rs @@ -0,0 +1,37 @@ +//! [`NonZeroU32`] equivalent, except with the illegal value at the top of the range (`u32::MAX`). +//! +//! [`NonMaxU32`] can represent any number from 0 to `u32::MAX - 1` inclusive. +//! +//! `NonMaxU32` has a niche, so `Option` is 4 bytes. +//! +//! On *nix, Mac, and WASI, this type is completely zero cost. +//! +//! On Windows, we wrap a `NonZeroU32` and XOR it with `u32::MAX` during conversion to/from `u32`, +//! same as `nonmax` crate does. This does have a (small) cost. +//! +//! # Hashing +//! Note that the Unix and Windows versions will produce different hashes from each other. +//! +//! [`NonZeroU32`]: std::num::NonZeroU32 + +// Version for *nix, Mac and WASI. +// `os::fd` is only available on these platforms. +// https://github.com/rust-lang/rust/blob/75530e9f72a1990ed2305e16fd51d02f47048f12/library/std/src/os/mod.rs#L185-L186 +#[cfg(any(unix, target_os = "hermit", target_os = "wasi"))] +mod unix; +#[cfg(any(unix, target_os = "hermit", target_os = "wasi"))] +pub use unix::NonMaxU32; + +// Version for other platforms (primarily Windows) +#[cfg(not(any(unix, target_os = "hermit", target_os = "wasi")))] +mod windows; +#[cfg(not(any(unix, target_os = "hermit", target_os = "wasi")))] +pub use windows::NonMaxU32; + +// Implementations which are shared between both versions +mod shared; +pub use shared::TryFromU32Error; + +// Tests +#[cfg(test)] +mod test; diff --git a/crates/oxc_data_structures/src/nonmax/shared.rs b/crates/oxc_data_structures/src/nonmax/shared.rs new file mode 100644 index 0000000000000..55513ab2baf4a --- /dev/null +++ b/crates/oxc_data_structures/src/nonmax/shared.rs @@ -0,0 +1,78 @@ +// All methods are either zero cost, very cheap, or delegate, so marked `#[inline(always)]` +#![expect(clippy::inline_always)] + +use std::{ + convert::TryFrom, + error::Error, + fmt::{self, Display}, +}; + +use super::NonMaxU32; + +impl NonMaxU32 { + /// [`NonMaxU32`] with the value zero (0). + pub const ZERO: Self = { + // SAFETY: 0 is a valid value + unsafe { Self::new_unchecked(0) } + }; + + /// [`NonMaxU32`] with the maximum value zero (`u32::MAX - 1`). + pub const MAX: Self = { + // SAFETY: `u32::MAX - 1` is a valid value + unsafe { Self::new_unchecked(u32::MAX - 1) } + }; +} + +impl Default for NonMaxU32 { + #[inline(always)] + fn default() -> Self { + Self::ZERO + } +} + +impl From for u32 { + #[inline(always)] + fn from(value: NonMaxU32) -> u32 { + value.get() + } +} + +impl TryFrom for NonMaxU32 { + type Error = TryFromU32Error; + + #[inline(always)] + fn try_from(n: u32) -> Result { + // Note: Conversion from `Option` to `Result` here is zero-cost + Self::new(n).ok_or(TryFromU32Error(())) + } +} + +macro_rules! impl_fmt { + ($Trait:ident) => { + impl fmt::$Trait for NonMaxU32 { + #[inline(always)] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::$Trait::fmt(&self.get(), f) + } + } + }; +} + +impl_fmt!(Debug); +impl_fmt!(Display); +impl_fmt!(Binary); +impl_fmt!(Octal); +impl_fmt!(LowerHex); +impl_fmt!(UpperHex); + +/// Error type for failed conversion from [`u32`] to [`NonMaxU32`]. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct TryFromU32Error(()); + +impl Error for TryFromU32Error {} + +impl Display for TryFromU32Error { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + "Out of range conversion to `NonMaxU32` attempted".fmt(fmt) + } +} diff --git a/crates/oxc_data_structures/src/nonmax/test.rs b/crates/oxc_data_structures/src/nonmax/test.rs new file mode 100644 index 0000000000000..556d005d20f76 --- /dev/null +++ b/crates/oxc_data_structures/src/nonmax/test.rs @@ -0,0 +1,91 @@ +use super::*; + +#[test] +fn construct() { + let zero = NonMaxU32::new(0).unwrap(); + assert_eq!(zero.get(), 0); + + let some = NonMaxU32::new(19).unwrap(); + assert_eq!(some.get(), 19); + + let max_minus_one = NonMaxU32::new(u32::MAX - 1).unwrap(); + assert_eq!(max_minus_one.get(), u32::MAX - 1); + + let max = NonMaxU32::new(u32::MAX); + assert_eq!(max, None); +} + +#[test] +fn construct_unchecked() { + // SAFETY: 0 is a valid value + let zero = unsafe { NonMaxU32::new_unchecked(0) }; + assert_eq!(zero.get(), 0); + + // SAFETY: 19 is a valid value + let some = unsafe { NonMaxU32::new_unchecked(19) }; + assert_eq!(some.get(), 19); + + // SAFETY: `u32::MAX - 1` is a valid value + let max_minus_one = unsafe { NonMaxU32::new_unchecked(u32::MAX - 1) }; + assert_eq!(max_minus_one.get(), u32::MAX - 1); +} + +#[test] +fn convert() { + let zero = NonMaxU32::try_from(0u32).unwrap(); + let zero = u32::from(zero); + assert_eq!(zero, 0); + + NonMaxU32::try_from(u32::MAX).unwrap_err(); +} + +#[test] +fn eq() { + let zero = NonMaxU32::new(0).unwrap(); + let one = NonMaxU32::new(1).unwrap(); + let two = NonMaxU32::new(2).unwrap(); + assert_eq!(zero, zero); + assert_eq!(one, one); + assert_eq!(two, two); + + assert_ne!(zero, one); + assert_ne!(zero, two); + assert_ne!(one, two); + assert_ne!(one, zero); + assert_ne!(two, zero); + assert_ne!(two, one); +} + +#[test] +fn cmp() { + let zero = NonMaxU32::new(0).unwrap(); + let one = NonMaxU32::new(1).unwrap(); + let two = NonMaxU32::new(2).unwrap(); + assert!(zero < one); + assert!(one < two); + assert!(two > one); + assert!(one > zero); +} + +#[test] +fn constants() { + let zero = NonMaxU32::ZERO; + let max = NonMaxU32::MAX; + assert_eq!(zero.get(), 0); + assert_eq!(max.get(), u32::MAX - 1); +} + +#[test] +fn fmt() { + let zero = NonMaxU32::new(0).unwrap(); + let some = NonMaxU32::new(19).unwrap(); + let max_minus_one = NonMaxU32::new(u32::MAX - 1).unwrap(); + for value in [zero, some, max_minus_one].iter().copied() { + assert_eq!(format!("{}", value.get()), format!("{}", value)); // Display + assert_eq!(format!("{:?}", value.get()), format!("{:?}", value)); // Debug + assert_eq!(format!("{:b}", value.get()), format!("{:b}", value)); // Binary + assert_eq!(format!("{:o}", value.get()), format!("{:o}", value)); // Octal + assert_eq!(format!("{:x}", value.get()), format!("{:x}", value)); // LowerHex + assert_eq!(format!("{:X}", value.get()), format!("{:X}", value)); // UpperHex + } +} diff --git a/crates/oxc_data_structures/src/nonmax/unix.rs b/crates/oxc_data_structures/src/nonmax/unix.rs new file mode 100644 index 0000000000000..04bbdebb40dd5 --- /dev/null +++ b/crates/oxc_data_structures/src/nonmax/unix.rs @@ -0,0 +1,130 @@ +// Conversion methods are zero cost, and other methods just delegate, so marked `#[inline(always)]` +#![expect(clippy::inline_always)] + +use crate::assert_unchecked; + +// # Implementation details +// +// `std::os::fd::BorrowedFd` is a wrapper around `std::os::fd::RawFd`, and is `#[repr(transparent)]`. +// `RawFd` is a type alias for `i32`. `i32` has same layout as `u32`. +// Therefore `BorrowedFd` has the same layout as `u32`. +// `NonZeroU32` is `#[repr(transparent)]`, and `ManuallyDrop` is also `#[repr(transparent)]`. +// Therefore `NonMaxU32` has same layout as `u32`. +// +// The feature that `BorrowedFd` brings is that it has a +// `#[rustc_layout_scalar_valid_range_end(0xFF_FF_FF_FE)]` attribute, +// meaning `u32::MAX` is an invalid bit pattern for the type. This invalid bit pattern forms a niche. +// +// +// `BorrowedFd` and `OwnedFd` are the only public stable types in the standard library which exhibit +// this property. `OwnedFd` is not `Copy`, so we use `BorrowedFd`. +// We never use the `BorrowedFd` as an actual file descriptor, only utilize it for its layout. +// +// `BorrowedFd` is not `Drop`, but we wrap it in `ManuallyDrop` anyway, just to make sure. +// +// Because of the niche, `Option` is 4 bytes. +// +// Unlike the `NonMaxU32` type from `nonmax` crate, this type has zero cost converting to and from `u32`. +// https://godbolt.org/z/cGaqhcco4 +// +// `BorrowedFd` is only available on Unix-like platforms. +// We substitute a less efficient version on Windows, which does have a (small) conversion cost. + +use std::{ + cmp::Ordering, + hash::{Hash, Hasher}, + mem::{ManuallyDrop, transmute}, + os::fd::BorrowedFd, +}; + +/// [`NonZeroU32`] equivalent, except with the illegal value at the top of the range (`u32::MAX`). +/// +/// [`NonMaxU32`] can represent any number from 0 to `u32::MAX - 1` inclusive. +/// +/// `NonMaxU32` has a niche, so `Option` is 4 bytes. +/// +/// [`NonZeroU32`]: std::num::NonZeroU32 +#[derive(Clone, Copy)] +#[repr(transparent)] +pub struct NonMaxU32(ManuallyDrop>); + +impl NonMaxU32 { + /// Create [`NonMaxU32`] from [`u32`]. + /// + /// Returns `None` if `n` is `u32::MAX`. + #[inline(always)] + pub const fn new(n: u32) -> Option { + if n < u32::MAX { + // SAFETY: We just checked `n < u32::MAX` + Some(unsafe { Self::new_unchecked(n) }) + } else { + None + } + } + + /// Create [`NonMaxU32`] from [`u32`], without checks. + /// + /// # SAFETY + /// `n` must not be `u32::MAX`. + #[inline(always)] + pub const unsafe fn new_unchecked(n: u32) -> Self { + // SAFETY: Caller guarantees `n` is not `u32::MAX`. + // See implementation details comment above. + unsafe { transmute::(n) } + } + + /// Convert [`NonMaxU32`] to [`u32`]. + #[inline(always)] + pub const fn get(self) -> u32 { + // SAFETY: See implementation details comment above + let n = unsafe { transmute::(self) }; + + // Make sure compiler understands return value cannot be `u32::MAX`. + // This may aid it to make optimizations in some cases. + // SAFETY: `NonMaxU32` cannot represent `u32::MAX`. + unsafe { assert_unchecked!(n < u32::MAX) }; + + n + } +} + +impl Eq for NonMaxU32 {} + +impl PartialEq for NonMaxU32 { + #[inline(always)] + fn eq(&self, other: &Self) -> bool { + self.get() == other.get() + } +} + +impl Ord for NonMaxU32 { + #[inline(always)] + fn cmp(&self, other: &Self) -> Ordering { + self.get().cmp(&other.get()) + } +} + +impl PartialOrd for NonMaxU32 { + #[inline(always)] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Hash for NonMaxU32 { + #[inline(always)] + fn hash(&self, state: &mut H) { + self.get().hash(state); + } +} + +const _: () = { + assert!(size_of::() == 4); + assert!(align_of::() == 4); + assert!(size_of::() == 4); + assert!(align_of::() == 4); + assert!(size_of::() == 4); + assert!(align_of::() == 4); + assert!(size_of::>() == 4); + assert!(size_of::>>() == 8); +}; diff --git a/crates/oxc_data_structures/src/nonmax/windows.rs b/crates/oxc_data_structures/src/nonmax/windows.rs new file mode 100644 index 0000000000000..b8b72797e9fbc --- /dev/null +++ b/crates/oxc_data_structures/src/nonmax/windows.rs @@ -0,0 +1,66 @@ +// All methods are very cheap, so marked `#[inline(always)]` +#![expect(clippy::inline_always)] + +use std::{cmp::Ordering, num::NonZeroU32}; + +/// [`NonZeroU32`] equivalent, except with the illegal value at the top of the range (`u32::MAX`). +/// +/// [`NonMaxU32`] can represent any number from 0 to `u32::MAX - 1` inclusive. +/// +/// `NonMaxU32` has a niche, so `Option` is 4 bytes. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +#[repr(transparent)] +pub struct NonMaxU32(NonZeroU32); + +impl NonMaxU32 { + /// Create [`NonMaxU32`] from [`u32`]. + /// + /// Returns `None` if `n` is `u32::MAX`. + #[inline(always)] + pub const fn new(n: u32) -> Option { + match NonZeroU32::new(n ^ u32::MAX) { + Some(non_zero) => Some(Self(non_zero)), + None => None, + } + } + + /// Create [`NonMaxU32`] from [`u32`], without checks. + /// + /// # SAFETY + /// `n` must not be `u32::MAX`. + #[inline(always)] + pub const unsafe fn new_unchecked(n: u32) -> Self { + // SAFETY: Caller guarantees `n` is not `u32::MAX`. + let non_zero = unsafe { NonZeroU32::new_unchecked(n ^ u32::MAX) }; + Self(non_zero) + } + + /// Convert [`NonMaxU32`] to [`u32`]. + #[inline(always)] + pub const fn get(self) -> u32 { + self.0.get() ^ u32::MAX + } +} + +impl Ord for NonMaxU32 { + #[inline(always)] + fn cmp(&self, other: &Self) -> Ordering { + self.0.cmp(&other.0).reverse() + } +} + +impl PartialOrd for NonMaxU32 { + #[inline(always)] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +const _: () = { + assert!(size_of::() == 4); + assert!(align_of::() == 4); + assert!(size_of::() == 4); + assert!(align_of::() == 4); + assert!(size_of::>() == 4); + assert!(size_of::>>() == 8); +};