From 22fd5d727349dcd4184bf7073dbbd1ec803fc1cf Mon Sep 17 00:00:00 2001 From: Mazdak Farrokhzad Date: Thu, 16 Nov 2023 13:51:01 +0100 Subject: [PATCH] Add the `slim_slice` data-structures (#528) * add the slim_slice data-structures * slim_slice: addres some review comments * slim_slice: add basic SlimSmallSliceBox type + comment on SlimNonEmptyBox * slim_slice: prefix conversions with 'from_' * slim_slice: use transmute_copy * add empty LICENSE file * pacify clippy --- Cargo.lock | 5 + crates/data-structures/Cargo.toml | 6 + crates/data-structures/LICENSE | 1 + crates/data-structures/src/lib.rs | 1 + crates/data-structures/src/nstr.rs | 10 + crates/data-structures/src/slim_slice.rs | 1937 ++++++++++++++++++++++ 6 files changed, 1960 insertions(+) create mode 100644 crates/data-structures/src/slim_slice.rs diff --git a/Cargo.lock b/Cargo.lock index 341d0353af..d7b3ef0eae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4430,6 +4430,11 @@ dependencies = [ [[package]] name = "spacetimedb-data-structures" version = "0.7.3" +dependencies = [ + "nonempty", + "serde", + "thiserror", +] [[package]] name = "spacetimedb-lib" diff --git a/crates/data-structures/Cargo.toml b/crates/data-structures/Cargo.toml index 8f5270f9d2..5f0948628c 100644 --- a/crates/data-structures/Cargo.toml +++ b/crates/data-structures/Cargo.toml @@ -5,4 +5,10 @@ edition = "2021" license-file = "LICENSE" description = "Assorted data structures used in spacetimedb" +[features] +serde = ["dep:serde"] + [dependencies] +nonempty.workspace = true +serde = { workspace = true, optional = true } +thiserror.workspace = true diff --git a/crates/data-structures/LICENSE b/crates/data-structures/LICENSE index e69de29bb2..8d1c8b69c3 100644 --- a/crates/data-structures/LICENSE +++ b/crates/data-structures/LICENSE @@ -0,0 +1 @@ + diff --git a/crates/data-structures/src/lib.rs b/crates/data-structures/src/lib.rs index 00264f9c53..bbe41b35bc 100644 --- a/crates/data-structures/src/lib.rs +++ b/crates/data-structures/src/lib.rs @@ -2,3 +2,4 @@ //! These structures are general and could be used outside spacetimedb. pub mod nstr; +pub mod slim_slice; diff --git a/crates/data-structures/src/nstr.rs b/crates/data-structures/src/nstr.rs index b13b96094e..124b6f0f50 100644 --- a/crates/data-structures/src/nstr.rs +++ b/crates/data-structures/src/nstr.rs @@ -5,6 +5,7 @@ //! An example would be `nstr!("spacetime"): NStr<9>`. use core::{fmt, ops::Deref, str}; +use std::ops::DerefMut; /// A UTF-8 string of known length `N`. /// @@ -68,6 +69,15 @@ impl Deref for NStr { } } +impl DerefMut for NStr { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + // SAFETY: An `NStr` can only be made through `__nstr(..)` + // and which receives an `&str` which is valid UTF-8. + unsafe { str::from_utf8_unchecked_mut(&mut self.0) } + } +} + impl fmt::Debug for NStr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.deref()) diff --git a/crates/data-structures/src/slim_slice.rs b/crates/data-structures/src/slim_slice.rs new file mode 100644 index 0000000000..4e62f2cb5c --- /dev/null +++ b/crates/data-structures/src/slim_slice.rs @@ -0,0 +1,1937 @@ +//! Defines slimmer versions of slices, both shared, mutable, and owned. +//! +//! They are slimmer in the sense that whereas e.g., +//! `size_of::>() == 16`, on a 64-bit machine, +//! a `SlimSliceBox` only takes up 12 bytes. +//! These 4 bytes in difference can help +//! when these types are stored in enum variants +//! due to alignment and the space needed for storing tags. +//! +//! The difference size (4 bytes), is due to storing the length as a `u32` +//! rather than storing the length as a `usize` (`u64` on 64-bit machine). +//! This implies that the length can be at most `u32::MAX`, +//! so no more elements than that can be stored or pointed to with these types. +//! +//! Because hitting `u32::MAX` is substantially more likely than `u64::MAX`, +//! the risk of overflow is greater. +//! To mitigate this issue, rather than default to panicing, +//! this module tries, for the most part, +//! to force its user to handle any overflow +//! when converting to the slimmer types. +//! +//! The slimmer types include: +//! +//! - [`SlimSliceBox`], a slimmer version of `Box<[T]>` +//! - [`SlimSmallSliceBox`], a slimmer version of `SmallVec<[T; N]>` +//! but without the growing functionality. +//! - [`SlimNonEmptyBox`], a slimmer version of `NonEmpty` +//! but without the growing functionality. +//! - [`SlimStrBox`], a slimmer version of `Box` +//! - [`SlimSlice<'a, T>`], a slimmer verion of `&'a [T]` +//! - [`SlimSliceMut<'a, T>`], a slimmer version of `&'a mut [T]` +//! - [`SlimStr<'a>`], a slimmer version of `&'a str` +//! - [`SlimStrMut<'a>`], a slimmer version of `&'a mut str` +//! +//! The following convenience conversion functions are provided: +//! +//! - [`slice`] converts `&[T] -> SlimSlice`, panicing on overflow +//! - [`slice_mut`] converts `&mut [T] -> SlimSliceMut`, panicing on overflow +//! - [`str`] converts `&str -> SlimStr`, panicing on overflow +//! - [`str_mut`] converts `&mut str -> SlimStrMut`, panicing on overflow +//! - [`string`] converts `&str -> SlimStrBox`, panicing on overflow +//! +//! These conversions should be reserved for cases where it is known +//! that the length `<= u32::MAX` and should be used sparingly. +//! +//! Some auxiliary and utility functionality is provided: +//! +//! - [`SlimSliceBoxCollected`] exists to indirectly provide `FromIterator` +//! for [`SlimSliceBox`] +//! +//! - [`LenTooLong`], the error type when a conversion to a slimmer type +//! would result in a length overflow. +//! Optionally, the to-convert object is provided back to the user +//! for handling +//! +//! - [`try_into`] tries to convert the input to a slim type +//! and forgets the input if an error occurred +//! +//! - [`SafelyExchangeable`] is implemented to assert that `Self` +//! is safely transmutable, including when stored in a collection, to type `T` + +use core::{ + borrow::Borrow, + cmp::Ordering, + fmt::{Debug, Display}, + hash::{Hash, Hasher}, + marker::PhantomData, + mem, + mem::ManuallyDrop, + ops::{Deref, DerefMut}, + ptr::{slice_from_raw_parts_mut, NonNull}, + slice, + str::{from_utf8_unchecked, from_utf8_unchecked_mut}, +}; +use nonempty::NonEmpty; +use thiserror::Error; + +// ============================================================================= +// Errors +// ============================================================================= + +/// An error signifying that a container's size was over `u32::MAX`. +/// +/// Optionally, the to-convert object is provided back to the user for handling. +/// This is what the generic parameter `T` is for. +#[derive(Error, Debug)] +#[error("The length `{len}` was too long")] +pub struct LenTooLong { + /// The size of the container that was too large. + pub len: usize, + /// The container that was too large for into-slim conversion. + pub too_long: T, +} + +impl LenTooLong { + /// Forgets the container part of the error. + pub fn forget(self) -> LenTooLong { + self.map(drop) + } + + /// Maps the container part of the error. + pub fn map(self, with: impl FnOnce(T) -> U) -> LenTooLong { + LenTooLong { + len: self.len, + too_long: with(self.too_long), + } + } +} + +/// Try to convert `x` into `B` and forget the container part of the error if any. +#[inline] +pub fn try_into>>(x: A) -> Result { + x.try_into().map_err(|e: LenTooLong| e.forget()) +} + +// ============================================================================= +// Utils +// ============================================================================= + +/// Ensures that `$thing.len() <= u32::MAX`. +macro_rules! ensure_len_fits { + ($thing:expr) => { + let Ok(_) = u32::try_from($thing.len()) else { + return Err(LenTooLong { + len: $thing.len(), + too_long: $thing, + }); + }; + }; +} + +/// Convert to `A` but panic if `x.len() > u32::MAX`. +#[inline] +fn expect_fit>>(x: B) -> A { + x.try_into().map_err(|e| e.len).expect("length didn't fit in `u32`") +} + +#[inline] +fn into_box>>(x: U) -> Box<[T]> { + x.into() +} + +// Asserts, in the type system, that `N <= u32::MAX`. +struct AssertU32; +impl AssertU32 { + const OK: () = assert!(N <= u32::MAX as usize); +} + +// ============================================================================= +// Raw slice utility type +// ============================================================================= + +/// Implementors decree that `Self` can be *safely* transmuted to `T`, +/// including covariantly under a pointer. +/// +/// # Safety +/// +/// It is not sufficient that ´Self` and `T` have the same representation. +/// That is, validity requirements are not enough. +/// The safety requirements must also be the same. +pub unsafe trait SafelyExchangeable {} + +#[inline] +fn transmute_safely_exchangeable>(x: T) -> U { + let x = ManuallyDrop::new(x); + // SAFETY: Caller has proven that `x` is valid for `U`. + // so `&x` is also valid for `size_of::`. + // The type `T` and `U` are known to have the same size. + // + // Also, taking a copy is not a problem, even when `!(T: Copy)`, + // as due to `ManuallyDrop` above, this is really a move of `x: T` to `U`. + unsafe { mem::transmute_copy(&x) } +} + +/// Implementation detail of the other types. +/// Provides some convenience but users of the type are responsible +/// for safety, invariants and variance. +#[repr(packed)] +struct SlimRawSlice { + /// A valid pointer to the slice data. + ptr: NonNull, + /// The length of the slice. + len: u32, +} + +impl SlimRawSlice { + /// Returns a dangling slim raw slice. + #[inline] + fn dangling() -> Self { + let ptr = NonNull::dangling(); + Self { len: 0, ptr } + } + + /// Casts this raw slice `SlimRawSlice` to `SlimRawSlice`. + /// + /// That is, a cast from elements of `T` to elements of `U`. + /// The caller has ensured by `U: SafelyExchangeable` + /// that `T` is safely exchangeable for `U`. + #[inline] + fn cast>(self) -> SlimRawSlice { + SlimRawSlice { + ptr: self.ptr.cast(), + len: self.len, + } + } + + /// Split `self` into a raw pointer and the slice length. + #[inline] + fn split(self) -> (*mut T, usize) { + (self.ptr.as_ptr(), self.len as usize) + } + + /// Dereferences this raw slice into a shared slice. + /// + /// SAFETY: `self.ptr` and `self.len` + /// must satisfy [`std::slice::from_raw_parts`]'s requirements. + /// That is, + /// * `self.ptr` must be valid for reads + /// for `self.len * size_of::` many bytes and must be aligned. + /// + /// * `self.ptr` must point to `self.len` + /// consecutive properly initialized values of type `T`. + /// + /// * The memory referenced by the returned slice + /// must not be mutated for the duration of lifetime `'a`, + /// except inside an `UnsafeCell`. + /// + /// * The total size `self.len * mem::size_of::()` + /// of the slice must be no larger than `isize::MAX`, + /// and adding that size to `data` + /// must not "wrap around" the address space. + #[allow(clippy::needless_lifetimes)] + #[inline] + unsafe fn deref<'a>(&'a self) -> &'a [T] { + let (ptr, len) = self.split(); + // SAFETY: caller is responsible for these. + unsafe { slice::from_raw_parts(ptr, len) } + } + + /// Dereferences this raw slice into a mutable slice. + /// + /// SAFETY: `self.ptr` and `self.len` + /// must satisfy [`std::slice::from_raw_parts_mut`]'s requirements. + /// That is, + /// * `self.ptr` must be [valid] for both reads and writes + /// for `self.len * mem::size_of::()` many bytes, + /// and it must be properly aligned. + /// + /// * `self.ptr` must point to `self.len` + /// consecutive properly initialized values of type `T`. + /// + /// * The memory referenced by the returned slice + /// must not be accessed through any other pointer + /// (not derived from the return value) for the duration of lifetime `'a`. + /// Both read and write accesses are forbidden. + /// + /// * The total size `self.len * mem::size_of::()` + /// of the slice must be no larger than `isize::MAX`, + /// and adding that size to `data` must not "wrap around" the address space. + #[allow(clippy::needless_lifetimes)] + #[inline] + unsafe fn deref_mut<'a>(&'a mut self) -> &'a mut [T] { + let (ptr, len) = self.split(); + // SAFETY: caller is responsible for these. + unsafe { slice::from_raw_parts_mut(ptr, len) } + } + + /// Creates the raw slice from a pointer to the data and a length. + /// + /// It is assumed that `len <= u32::MAX`. + /// The caller must ensure that `ptr != NULL`. + #[inline] + const unsafe fn from_len_ptr(len: usize, ptr: *mut T) -> Self { + // SAFETY: caller ensured that `!ptr.is_null()`. + let ptr = NonNull::new_unchecked(ptr); + let len = len as u32; + Self { ptr, len } + } +} + +impl Copy for SlimRawSlice {} +impl Clone for SlimRawSlice { + #[inline] + fn clone(&self) -> Self { + *self + } +} + +// ============================================================================= +// Owned boxed slice +// ============================================================================= + +pub use slim_slice_box::*; +mod slim_slice_box { + // ^-- In the interest of soundness, + // this module exists to limit access to the private fields + // of `SlimSliceBox` to a few key functions. + use super::*; + + /// Provides a slimmer version of `Box<[T]>` + /// using `u32` for its length instead of `usize`. + #[repr(transparent)] + pub struct SlimSliceBox { + /// The representation of this boxed slice. + /// + /// To convert to a `SlimSliceBox` we must first have a `Box<[T]>`. + raw: SlimRawSlice, + /// Marker to ensure covariance and dropck ownership. + owned: PhantomData, + } + + impl Drop for SlimSliceBox { + #[inline] + fn drop(&mut self) { + // Get us an owned `SlimSliceBox` + // by replacing `self` with garbage that won't be dropped + // as the drop glue for the constituent fields does nothing. + let raw = SlimRawSlice::dangling(); + let owned = PhantomData; + let this = mem::replace(self, Self { raw, owned }); + + // Convert into `Box<[T]>` and let it deal with dropping. + drop(into_box(this)); + } + } + + impl SlimSliceBox { + /// Converts `boxed` to `Self` without checking `boxed.len() <= u32::MAX`. + /// + /// # Safety + /// + /// The caller must ensure that `boxed.len() <= u32::MAX`. + #[allow(clippy::boxed_local)] + #[inline] + // Clippy doesn't seem to consider unsafe code here. + pub unsafe fn from_boxed_unchecked(boxed: Box<[T]>) -> Self { + let len = boxed.len(); + let ptr = Box::into_raw(boxed) as *mut T; + // SAFETY: `Box`'s ptr was a `NonNull` already. + // and our caller has promised that `boxed.len() <= u32::MAX`. + let raw = SlimRawSlice::from_len_ptr(len, ptr); + let owned = PhantomData; + Self { raw, owned } + } + + /// Returns a limited shared slice to this boxed slice. + #[allow(clippy::needless_lifetimes)] + #[inline] + pub fn shared_ref<'a>(&'a self) -> &'a SlimSlice<'a, T> { + // SAFETY: The reference lives as long as `self`. + // By virtue of `repr(transparent)` we're also allowed these reference casts. + unsafe { mem::transmute(self) } + } + + /// Returns a limited mutable slice to this boxed slice. + #[allow(clippy::needless_lifetimes)] + #[inline] + pub fn exclusive_ref<'a>(&'a mut self) -> &'a mut SlimSliceMut<'a, T> { + // SAFETY: The reference lives as long as `self` + // and we have exclusive access to the heap data thanks to `&'a mut self`. + // By virtue of `repr(transparent)` we're also allowed these reference casts. + unsafe { mem::transmute(self) } + } + + /// Map every element `x: T` to `U` by transmuting. + /// + /// This will not reallocate. + #[inline] + pub fn map_safely_exchangeable>(self) -> SlimSliceBox { + // SAFETY: By `U: SafelyExchangeable`, + // the caller has proven that we can exchange `T -> U` + // even under an owned covariant pointer. + SlimSliceBox { + raw: self.raw.cast(), + owned: PhantomData, + } + } + } + + impl From> for Box<[T]> { + #[inline] + fn from(slice: SlimSliceBox) -> Self { + let slice = ManuallyDrop::new(slice); + let (ptr, len) = slice.raw.split(); + // SAFETY: All paths to creating a `SlimSliceBox` + // go through `SlimSliceBox::from_boxed_unchecked` + // which requires a valid `Box<[T]>`. + // The function also uses `Box::into_raw` + // and the original length is kept. + // + // It therefore follows that if we reuse the same + // pointer and length as given to us by a valid `Box<[T]>`, + // we can use `Box::from_raw` to reconstruct the `Box<[T]>`. + // + // We also no longer claim ownership of the data pointed to by `ptr` + // by virtue of `ManuallyDrop` preventing `Drop for SlimSliceBox`. + unsafe { Box::from_raw(slice_from_raw_parts_mut(ptr, len)) } + } + } +} + +/// `SlimSliceBox` is `Send` if `T` is `Send` because the data is owned. +unsafe impl Send for SlimSliceBox {} + +/// `SlimSliceBox` is `Sync` if `T` is `Sync` because the data is owned. +unsafe impl Sync for SlimSliceBox {} + +impl Deref for SlimSliceBox { + type Target = [T]; + + #[inline] + fn deref(&self) -> &Self::Target { + self.shared_ref().deref() + } +} + +impl DerefMut for SlimSliceBox { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + self.exclusive_ref().deref_mut() + } +} + +impl SlimSliceBox { + /// Converts `boxed: Box<[T]>` into `SlimSliceBox`. + /// + /// Panics when `boxed.len() > u32::MAX`. + #[inline] + pub fn from_boxed(boxed: Box<[T]>) -> Self { + expect_fit(boxed) + } + + /// Converts `vec: Vec` into `SlimSliceBox`. + /// + /// Panics when `vec.len() > u32::MAX`. + #[inline] + pub fn from_vec(vec: Vec) -> Self { + Self::from_boxed(vec.into()) + } + + /// Maps all elements with `by` from `T` to `U`. + /// + /// Will allocate once for the new boxed slice. + #[inline] + pub fn map(self, by: impl FnMut(T) -> U) -> SlimSliceBox { + let mapped = self.into_iter().map(by).collect::>(); + // SAFETY: Doing `.map(..)` can never change the length. + unsafe { SlimSliceBox::from_boxed_unchecked(mapped) } + } + + /// Maps all elements with `by` from `&T` to `U`. + /// + /// Will allocate once for the new boxed slice. + #[inline] + pub fn map_borrowed(&self, by: impl FnMut(&T) -> U) -> SlimSliceBox { + let mapped = self.iter().map(by).collect::>(); + // SAFETY: Doing `.map(..)` can never change the length. + unsafe { SlimSliceBox::from_boxed_unchecked(mapped) } + } +} + +impl TryFrom> for SlimSliceBox { + type Error = LenTooLong>; + + #[inline] + fn try_from(boxed: Box<[T]>) -> Result { + ensure_len_fits!(boxed); + // SAFETY: Checked above that `len <= u32::MAX`. + Ok(unsafe { Self::from_boxed_unchecked(boxed) }) + } +} + +impl TryFrom> for SlimSliceBox { + type Error = LenTooLong>; + + #[inline] + fn try_from(vec: Vec) -> Result { + ensure_len_fits!(vec); + // SAFETY: Checked above that `len <= u32::MAX`. + Ok(unsafe { Self::from_boxed_unchecked(vec.into_boxed_slice()) }) + } +} + +impl From<[T; N]> for SlimSliceBox { + #[inline] + fn from(arr: [T; N]) -> Self { + #[allow(clippy::let_unit_value)] + let () = AssertU32::::OK; + + // SAFETY: We verified statically by `AssertU32` above that `N` fits in u32. + unsafe { Self::from_boxed_unchecked(into_box(arr)) } + } +} + +impl From> for Vec { + #[inline] + fn from(slice: SlimSliceBox) -> Self { + into_box(slice).into() + } +} + +impl Debug for SlimSliceBox { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Debug::fmt(self.deref(), f) + } +} + +impl Clone for SlimSliceBox { + #[inline] + fn clone(&self) -> Self { + // Allocate exactly the right amount + // so we later don't reallocate due to excess capacity. + let mut vec = Vec::with_capacity(self.len()); + vec.extend_from_slice(self); + // SAFETY: We know `self.len() <= u32::MAX`. + unsafe { Self::from_boxed_unchecked(into_box(vec)) } + } +} + +impl PartialEq for SlimSliceBox +where + [T]: PartialEq, +{ + #[inline] + fn eq(&self, other: &R) -> bool { + **self == **other + } +} + +impl Eq for SlimSliceBox {} + +impl PartialOrd for SlimSliceBox +where + [T]: PartialOrd, +{ + #[inline] + fn partial_cmp(&self, other: &R) -> Option { + (**self).partial_cmp(&**other) + } +} + +impl Ord for SlimSliceBox { + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + (**self).cmp(&**other) + } +} + +impl Hash for SlimSliceBox { + #[inline] + fn hash(&self, state: &mut H) { + Hash::hash(&**self, state) + } +} + +impl IntoIterator for SlimSliceBox { + type Item = T; + type IntoIter = as IntoIterator>::IntoIter; + #[inline] + fn into_iter(self) -> Self::IntoIter { + Vec::from(self).into_iter() + } +} + +/// A wrapper to achieve `FromIterator for Result, Vec>`. +/// +/// We cannot do this directly due to orphan rules. +pub struct SlimSliceBoxCollected { + /// The result of `from_iter`. + pub inner: Result, LenTooLong>>, +} + +impl SlimSliceBoxCollected { + #[inline] + pub fn unwrap(self) -> SlimSliceBox { + self.inner.expect("number of elements overflowed `u32::MAX`") + } +} + +impl FromIterator for SlimSliceBoxCollected { + #[inline] + fn from_iter>(iter: T) -> Self { + let inner = iter.into_iter().collect::>().try_into(); + SlimSliceBoxCollected { inner } + } +} + +// ============================================================================= +// Owned boxed slice with SSO +// ============================================================================= + +pub struct SlimSmallSliceBox(SlimSmallSliceBoxData); + +/// The representation of [`SlimSmallSliceBox`]. +/// +/// The parameter `N` is the number of elements that can be inline. +enum SlimSmallSliceBoxData { + /// The data is inline, not using any indirections. + Inline([T; N]), + /// The data is boxed up. + Heap(SlimSliceBox), +} + +impl From<[T; N]> for SlimSmallSliceBox { + fn from(value: [T; N]) -> Self { + #[allow(clippy::let_unit_value)] + let () = AssertU32::::OK; + + Self(SlimSmallSliceBoxData::Inline(value)) + } +} + +impl From> for SlimSmallSliceBox { + fn from(value: SlimSliceBox) -> Self { + Self(SlimSmallSliceBoxData::Heap(value)) + } +} + +impl From> for SlimSliceBox { + fn from(SlimSmallSliceBox(value): SlimSmallSliceBox) -> Self { + match value { + SlimSmallSliceBoxData::Inline(i) => i.into(), + SlimSmallSliceBoxData::Heap(h) => h, + } + } +} + +impl Deref for SlimSmallSliceBox { + type Target = [T]; + fn deref(&self) -> &Self::Target { + match &self.0 { + SlimSmallSliceBoxData::Inline(i) => i, + SlimSmallSliceBoxData::Heap(h) => h, + } + } +} + +impl DerefMut for SlimSmallSliceBox { + fn deref_mut(&mut self) -> &mut Self::Target { + match &mut self.0 { + SlimSmallSliceBoxData::Inline(i) => i, + SlimSmallSliceBoxData::Heap(h) => h, + } + } +} + +// ============================================================================= +// Non-empty owned boxed slice +// ============================================================================= + +// TODO(Centril): This type was introduced because `NonEmpty` +// was used to store `cols: NonEmpty`. +// However, it is not efficient to store `cols` this way because +// we need to convert <=> `ArrayValue` and <=> view `cols` as `&[ColId]`. +// As `SlimNonEmptyBox` and `NonEmpty` store `head` in a separate allocation, +// this makes a direct conversion to `&[ColId]` impossible and <=> `ArrayValue` slow. +// Instead of dealing with non-emptiness at the representation layer, +// we can use `SlimSmallSliceBox`, and if needs be, +// add non-emptiness purely as a logical layer. + +/// A non-empty slim owned slice. +#[derive(Clone, Eq, PartialEq, Hash, Debug)] +pub struct SlimNonEmptyBox { + /// The head of the list, ensuring there's at least one element. + pub head: T, + /// The tail of the list. Invariant `tail.len() < u32::MAX`. + tail: SlimSliceBox, +} + +impl SlimNonEmptyBox { + /// Returns the non-empty vector `[head]`. + #[inline] + pub fn new(head: T) -> Self { + Self { head, tail: [].into() } + } + + /// Returns the length of the vector. + #[allow(clippy::len_without_is_empty)] + #[inline] + pub fn len(&self) -> usize { + 1 + self.tail.len() + } + + /// Map every element `x: T` to `U` by transmuting. + /// + /// This will not reallocate. + #[inline] + pub fn map_safely_exchangeable>(self) -> SlimNonEmptyBox { + SlimNonEmptyBox { + head: transmute_safely_exchangeable(self.head), + tail: self.tail.map_safely_exchangeable(), + } + } + + /// Returns an iterator over borrowed elements of the vector. + #[inline] + pub fn iter(&self) -> Iter<'_, T> { + let elems = Some((&self.head, &*self.tail)); + Iter { elems } + } +} + +impl From for SlimNonEmptyBox { + #[inline] + fn from(value: T) -> Self { + Self::new(value) + } +} + +pub struct Iter<'a, T> { + elems: Option<(&'a T, &'a [T])>, +} + +impl<'a, T> Iterator for Iter<'a, T> { + type Item = &'a T; + + #[inline] + fn next(&mut self) -> Option { + let (next, tail) = self.elems.take().unzip(); + self.elems = tail.unwrap_or_default().split_first(); + next + } +} + +impl ExactSizeIterator for Iter<'_, T> { + #[inline] + fn len(&self) -> usize { + self.elems.map_or(0, |(_, tail)| 1 + tail.len()) + } +} + +impl core::iter::FusedIterator for Iter<'_, T> {} + +impl TryFrom> for SlimNonEmptyBox { + type Error = LenTooLong>; + + #[inline] + fn try_from(value: NonEmpty) -> Result { + ensure_len_fits!(value); + + // SAFETY: Ensured ^-- that `([head] ++ tail).len() <= u32::MAX`. + // This implies `[head].len() + tail.len() <= u32::MAX` + // so `1 + tail.len() <= u32::MAX` + // so `tail.len() <= u32::MAX` also holds. + let tail = unsafe { SlimSliceBox::from_boxed_unchecked(value.tail.into()) }; + + Ok(Self { head: value.head, tail }) + } +} + +impl From> for SlimSliceBox { + #[inline] + fn from(value: SlimNonEmptyBox) -> Self { + // Construct `[head] ++ tail`. + let mut vec = Vec::with_capacity(1 + value.tail.len()); + vec.push(value.head); + vec.append(&mut value.tail.into()); + + // SAFETY: By (invariant) `tail.len() < u32::MAX` + // we also know that `1 + tail.len() <= u32::MAX` + // thus `vec.len() <= u32::MAX` which is our safety requirement. + unsafe { Self::from_boxed_unchecked(vec.into()) } + } +} + +impl TryFrom<&SlimSliceBox> for SlimNonEmptyBox { + type Error = (); + + #[inline] + fn try_from(value: &SlimSliceBox) -> Result { + let [head, tail @ ..] = &**value else { + return Err(()); + }; + let head = head.clone(); + // SAFETY: We know that `tail.len() < u32::MAX`. + let tail = unsafe { SlimSliceBox::from_boxed_unchecked(tail.to_owned().into()) }; + Ok(Self { head, tail }) + } +} + +// ============================================================================= +// Owned boxed string slice +// ============================================================================= + +/// Provides a slimmer version of `Box` +/// using `u32` for its length instead of `usize`. +#[repr(transparent)] +pub struct SlimStrBox { + /// The underlying byte slice. + raw: SlimSliceBox, +} + +#[cfg(feature = "serde")] +impl serde::Serialize for SlimStrBox { + fn serialize(&self, s: S) -> Result { + s.serialize_str(self.deref()) + } +} + +impl SlimStrBox { + /// Converts `boxed` to `Self` without checking `boxed.len() <= u32::MAX`. + /// + /// # Safety + /// + /// The caller must ensure that `boxed.len() <= u32::MAX`. + #[inline] + pub unsafe fn from_boxed_unchecked(boxed: Box) -> Self { + // SAFETY: Caller has promised that `boxed.len() <= u32::MAX`. + let raw = unsafe { SlimSliceBox::from_boxed_unchecked(into_box(boxed)) }; + Self { raw } + } + + /// Converts `boxed: Box` into `SlimStrBox`. + /// + /// Panics when `boxed.len() > u32::MAX`. + #[inline] + pub fn from_boxed(boxed: Box) -> Self { + expect_fit(boxed) + } + + /// Converts `str: String` into `SlimStrBox`. + /// + /// Panics when `str.len() > u32::MAX`. + #[inline] + pub fn from_string(str: String) -> Self { + Self::from_boxed(str.into()) + } + + /// Returns a limited shared string slice to this boxed string slice. + #[allow(clippy::needless_lifetimes)] + #[inline] + pub fn shared_ref<'a>(&'a self) -> &'a SlimStr<'a> { + // SAFETY: The reference lives as long as `self`, + // we have shared access already, + // and by construction we know it's UTF-8. + unsafe { mem::transmute(self.raw.shared_ref()) } + } + + /// Returns a limited mutable string slice to this boxed string slice. + #[allow(clippy::needless_lifetimes)] + #[inline] + pub fn exclusive_ref<'a>(&'a mut self) -> &'a mut SlimStrMut<'a> { + // SAFETY: The reference lives as long as `self`, + // we had `&mut self`, + // and by construction we know it's UTF-8. + unsafe { mem::transmute(self.raw.exclusive_ref()) } + } +} + +impl Deref for SlimStrBox { + type Target = str; + + #[inline] + fn deref(&self) -> &Self::Target { + self.shared_ref().deref() + } +} + +impl DerefMut for SlimStrBox { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + self.exclusive_ref().deref_mut() + } +} + +impl From> for SlimStrBox { + #[inline] + fn from(arr: NStr) -> Self { + (&arr).into() + } +} + +impl From<&NStr> for SlimStrBox { + #[inline] + fn from(arr: &NStr) -> Self { + >::from(arr).into() + } +} + +impl TryFrom> for SlimStrBox { + type Error = LenTooLong>; + + #[inline] + fn try_from(boxed: Box) -> Result { + ensure_len_fits!(boxed); + // SAFETY: Checked above that `len <= u32::MAX`. + Ok(unsafe { Self::from_boxed_unchecked(boxed) }) + } +} + +impl TryFrom for SlimStrBox { + type Error = LenTooLong; + + #[inline] + fn try_from(str: String) -> Result { + ensure_len_fits!(str); + // SAFETY: Checked above that `len <= u32::MAX`. + Ok(unsafe { Self::from_boxed_unchecked(str.into_boxed_str()) }) + } +} + +impl<'a> TryFrom<&'a str> for SlimStrBox { + type Error = LenTooLong<&'a str>; + + #[inline] + fn try_from(str: &'a str) -> Result { + str.try_into().map(|s: SlimStr<'_>| s.into()) + } +} + +impl From for Box { + #[inline] + fn from(str: SlimStrBox) -> Self { + let raw_box = into_box(str.raw); + // SAFETY: By construction, `SlimStrBox` is valid UTF-8. + unsafe { Box::from_raw(Box::into_raw(raw_box) as *mut str) } + } +} + +impl From for String { + #[inline] + fn from(str: SlimStrBox) -> Self { + >::from(str).into() + } +} + +impl Debug for SlimStrBox { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Debug::fmt(self.deref(), f) + } +} + +impl Display for SlimStrBox { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Display::fmt(self.deref(), f) + } +} + +impl Clone for SlimStrBox { + #[inline] + fn clone(&self) -> Self { + Self { raw: self.raw.clone() } + } +} + +impl PartialEq for SlimStrBox +where + str: PartialEq, +{ + #[inline] + fn eq(&self, other: &R) -> bool { + self.deref() == other.deref() + } +} + +impl Eq for SlimStrBox {} + +impl PartialOrd for SlimStrBox +where + str: PartialOrd, +{ + #[inline] + fn partial_cmp(&self, other: &R) -> Option { + self.deref().partial_cmp(other.deref()) + } +} + +impl Ord for SlimStrBox { + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + self.deref().cmp(other.deref()) + } +} + +impl Hash for SlimStrBox { + #[inline] + fn hash(&self, state: &mut H) { + Hash::hash(self.deref(), state) + } +} + +impl Borrow for SlimStrBox { + #[inline] + fn borrow(&self) -> &str { + self + } +} + +// ============================================================================= +// Shared slice reference +// ============================================================================= + +#[allow(clippy::module_inception)] +mod slim_slice { + use super::*; + + /// A shared reference to `[T]` limited to `u32::MAX` in length. + #[repr(transparent)] + #[derive(Clone, Copy)] + pub struct SlimSlice<'a, T> { + /// The representation of this shared slice. + raw: SlimRawSlice, + /// Marker to ensure covariance for `'a`. + covariant: PhantomData<&'a [T]>, + } + + impl<'a, T> SlimSlice<'a, T> { + /// Converts a `&[T]` to the limited version without length checking. + /// + /// SAFETY: `slice.len() <= u32::MAX` must hold. + pub(super) const unsafe fn from_slice_unchecked(slice: &'a [T]) -> Self { + let len = slice.len(); + let ptr = slice.as_ptr().cast_mut(); + // SAFETY: `&mut [T]` implies that the pointer is non-null. + let raw = SlimRawSlice::from_len_ptr(len, ptr); + // SAFETY: Our length invariant is satisfied by the caller. + let covariant = PhantomData; + Self { raw, covariant } + } + } + + impl Deref for SlimSlice<'_, T> { + type Target = [T]; + + fn deref(&self) -> &Self::Target { + // SAFETY: `ptr` and `len` are either + // a) derived from a live `Box<[T]>` valid for `'self` + // b) derived from a live `&'self [T]` + // so we satisfy all safety requirements for `from_raw_parts`. + unsafe { self.raw.deref() } + } + } +} +pub use slim_slice::*; + +use crate::nstr::NStr; + +// SAFETY: Same rules as for `&[T]`. +unsafe impl Send for SlimSlice<'_, T> {} + +// SAFETY: Same rules as for `&[T]`. +unsafe impl Sync for SlimSlice<'_, T> {} + +impl SlimSlice<'_, T> { + /// Falibly maps all elements with `by` to `Result`. + /// + /// Returns `Err(_)` if any call to `by` did. + #[inline] + pub fn try_map(&self, by: impl FnMut(&T) -> Result) -> Result, E> { + let mapped = self.iter().map(by).collect::>()?; + // SAFETY: Doing `.map(..)` can never change the length. + Ok(unsafe { SlimSliceBox::from_boxed_unchecked(mapped) }) + } +} + +impl Debug for SlimSlice<'_, T> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Debug::fmt(self.deref(), f) + } +} + +impl Hash for SlimSlice<'_, T> { + #[inline] + fn hash(&self, state: &mut H) { + Hash::hash(self.deref(), state) + } +} + +impl Eq for SlimSlice<'_, T> {} +impl PartialEq for SlimSlice<'_, T> { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.deref() == other.deref() + } +} +impl PartialEq<[T]> for SlimSlice<'_, T> { + #[inline] + fn eq(&self, other: &[T]) -> bool { + self.deref() == other + } +} + +impl Ord for SlimSlice<'_, T> { + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + self.deref().cmp(other) + } +} +impl PartialOrd for SlimSlice<'_, T> { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + self.deref().partial_cmp(other.deref()) + } +} +impl PartialOrd<[T]> for SlimSlice<'_, T> { + #[inline] + fn partial_cmp(&self, other: &[T]) -> Option { + self.deref().partial_cmp(other) + } +} + +impl From<&SlimSlice<'_, T>> for SlimSliceBox { + #[inline] + fn from(slice: &SlimSlice<'_, T>) -> Self { + let boxed = into_box(slice.deref()); + // SAFETY: `slice` is limited to `len: u32` by construction. + unsafe { Self::from_boxed_unchecked(boxed) } + } +} +impl From<&SlimSlice<'_, T>> for Box<[T]> { + #[inline] + fn from(slice: &SlimSlice<'_, T>) -> Self { + slice.deref().into() + } +} +impl From<&SlimSlice<'_, T>> for Vec { + #[inline] + fn from(slice: &SlimSlice<'_, T>) -> Self { + slice.deref().into() + } +} + +impl From> for SlimSliceBox { + #[inline] + fn from(slice: SlimSlice<'_, T>) -> Self { + (&slice).into() + } +} +impl From> for Box<[T]> { + #[inline] + fn from(slice: SlimSlice<'_, T>) -> Self { + slice.deref().into() + } +} +impl From> for Vec { + #[inline] + fn from(slice: SlimSlice<'_, T>) -> Self { + slice.deref().into() + } +} + +impl<'a, T> TryFrom<&'a [T]> for SlimSlice<'a, T> { + type Error = LenTooLong<&'a [T]>; + + #[inline] + fn try_from(slice: &'a [T]) -> Result { + ensure_len_fits!(slice); + // SAFETY: ^-- satisfies `len <= u32::MAX`. + Ok(unsafe { Self::from_slice_unchecked(slice) }) + } +} + +/// Converts `&[T]` into the slim limited version. +/// +/// Panics when `slice.len() > u32::MAX`. +#[inline] +pub fn from_slice(s: &[T]) -> SlimSlice<'_, T> { + expect_fit(s) +} + +// ============================================================================= +// Mutable slice reference +// ============================================================================= + +/// A mutable reference to `[T]` limited to `u32::MAX` in length. +#[repr(transparent)] +pub struct SlimSliceMut<'a, T> { + /// The representation of this mutable slice. + raw: SlimRawSlice, + /// Marker to ensure invariance for `'a`. + invariant: PhantomData<&'a mut [T]>, +} + +// SAFETY: Same rules as for `&mut [T]`. +unsafe impl Send for SlimSliceMut<'_, T> {} + +// SAFETY: Same rules as for `&mut [T]`. +unsafe impl Sync for SlimSliceMut<'_, T> {} + +impl<'a, T> SlimSliceMut<'a, T> { + /// Convert this mutable reference to a shared one. + #[inline] + pub fn shared(&'a self) -> &'a SlimSlice<'a, T> { + // SAFETY: By virtue of `&'a mut X -> &'a X` being sound, this is also. + // The types and references to them have the same layout as well. + unsafe { mem::transmute(self) } + } + + /// Converts a `&mut [T]` to the limited version without length checking. + /// + /// SAFETY: `slice.len() <= u32::MAX` must hold. + #[inline] + unsafe fn from_slice_unchecked(slice: &'a mut [T]) -> Self { + // SAFETY: `&mut [T]` implies that the pointer is non-null. + let raw = SlimRawSlice::from_len_ptr(slice.len(), slice.as_mut_ptr()); + // SAFETY: Our invariants are satisfied by the caller + // and that `&mut [T]` implies exclusive access to the data. + let invariant = PhantomData; + Self { raw, invariant } + } +} + +impl Deref for SlimSliceMut<'_, T> { + type Target = [T]; + + #[inline] + fn deref(&self) -> &Self::Target { + self.shared().deref() + } +} + +impl DerefMut for SlimSliceMut<'_, T> { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + // SAFETY: `ptr` and `len` are either + // a) derived from a live `Box<[T]>` valid for `'self` + // b) derived from a live `&'self [T]` + // and additionally, we have the only pointer to the data. + // so we satisfy all safety requirements for `from_raw_parts_mut`. + unsafe { self.raw.deref_mut() } + } +} + +impl Debug for SlimSliceMut<'_, T> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Debug::fmt(self.deref(), f) + } +} + +impl Hash for SlimSliceMut<'_, T> { + #[inline] + fn hash(&self, state: &mut H) { + Hash::hash(self.deref(), state) + } +} + +impl Eq for SlimSliceMut<'_, T> {} +impl PartialEq for SlimSliceMut<'_, T> { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.deref() == other.deref() + } +} +impl PartialEq<[T]> for SlimSliceMut<'_, T> { + #[inline] + fn eq(&self, other: &[T]) -> bool { + self.deref() == other + } +} + +impl Ord for SlimSliceMut<'_, T> { + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + self.deref().cmp(other) + } +} +impl PartialOrd for SlimSliceMut<'_, T> { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + self.deref().partial_cmp(other.deref()) + } +} +impl PartialOrd<[T]> for SlimSliceMut<'_, T> { + #[inline] + fn partial_cmp(&self, other: &[T]) -> Option { + self.deref().partial_cmp(other) + } +} + +impl From<&SlimSliceMut<'_, T>> for SlimSliceBox { + #[inline] + fn from(slice: &SlimSliceMut<'_, T>) -> Self { + // SAFETY: `slice` is limited to `len: u32` by construction. + unsafe { Self::from_boxed_unchecked(into_box(slice.deref())) } + } +} +impl From<&SlimSliceMut<'_, T>> for Box<[T]> { + #[inline] + fn from(slice: &SlimSliceMut<'_, T>) -> Self { + slice.deref().into() + } +} +impl From<&SlimSliceMut<'_, T>> for Vec { + #[inline] + fn from(slice: &SlimSliceMut<'_, T>) -> Self { + slice.deref().into() + } +} + +impl From> for SlimSliceBox { + #[inline] + fn from(slice: SlimSliceMut<'_, T>) -> Self { + (&slice).into() + } +} +impl From> for Box<[T]> { + #[inline] + fn from(slice: SlimSliceMut<'_, T>) -> Self { + slice.deref().into() + } +} +impl From> for Vec { + #[inline] + fn from(slice: SlimSliceMut<'_, T>) -> Self { + slice.deref().into() + } +} + +impl<'a, T> TryFrom<&'a mut [T]> for SlimSliceMut<'a, T> { + type Error = LenTooLong<&'a mut [T]>; + + #[inline] + fn try_from(slice: &'a mut [T]) -> Result { + ensure_len_fits!(slice); + // SAFETY: ^-- satisfies `len <= u32::MAX`. + Ok(unsafe { Self::from_slice_unchecked(slice) }) + } +} + +/// Converts `&mut [T]` into the slim limited version. +/// +/// Panics when `slice.len() > u32::MAX`. +#[inline] +pub fn from_slice_mut(s: &mut [T]) -> SlimSliceMut<'_, T> { + expect_fit(s) +} + +// ============================================================================= +// Shared string slice reference +// ============================================================================= + +/// A shared reference to `str` limited to `u32::MAX` in length. +#[repr(transparent)] +#[derive(Clone, Copy)] +pub struct SlimStr<'a> { + /// The raw byte slice. + raw: SlimSlice<'a, u8>, +} + +impl<'a> SlimStr<'a> { + /// Converts `s` to `Self` without checking `s.len() <= u32::MAX`. + /// + /// # Safety + /// + /// The caller must ensure that `s.len() <= u32::MAX`. + #[inline] + const unsafe fn from_str_unchecked(s: &'a str) -> Self { + // SAFETY: Caller has promised that `s.len() <= u32::MAX`. + let raw = unsafe { SlimSlice::from_slice_unchecked(s.as_bytes()) }; + // SAFETY: `s: &str` is always UTF-8. + Self { raw } + } +} + +impl Deref for SlimStr<'_> { + type Target = str; + + #[inline] + fn deref(&self) -> &Self::Target { + // SAFETY: Data is derived from `str` originally so it's valid UTF-8. + unsafe { from_utf8_unchecked(self.raw.deref()) } + } +} + +impl Debug for SlimStr<'_> { + #[inline] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Debug::fmt(self.deref(), f) + } +} + +impl Display for SlimStr<'_> { + #[inline] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Display::fmt(self.deref(), f) + } +} + +impl Hash for SlimStr<'_> { + #[inline] + fn hash(&self, state: &mut H) { + Hash::hash(self.deref(), state) + } +} + +impl Eq for SlimStr<'_> {} +impl PartialEq for SlimStr<'_> { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.deref() == other.deref() + } +} +impl PartialEq for SlimStr<'_> { + #[inline] + fn eq(&self, other: &str) -> bool { + self.deref() == other + } +} + +impl Ord for SlimStr<'_> { + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + self.deref().cmp(other) + } +} +impl PartialOrd for SlimStr<'_> { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + self.deref().partial_cmp(other.deref()) + } +} +impl PartialOrd for SlimStr<'_> { + #[inline] + fn partial_cmp(&self, other: &str) -> Option { + self.deref().partial_cmp(other) + } +} + +impl From<&SlimStr<'_>> for SlimStrBox { + #[inline] + fn from(slice: &SlimStr<'_>) -> Self { + (*slice).into() + } +} +impl From<&SlimStr<'_>> for Box { + #[inline] + fn from(slice: &SlimStr<'_>) -> Self { + slice.deref().into() + } +} +impl From<&SlimStr<'_>> for String { + #[inline] + fn from(slice: &SlimStr<'_>) -> Self { + slice.deref().into() + } +} + +impl From> for SlimStrBox { + #[inline] + fn from(slice: SlimStr<'_>) -> Self { + // SAFETY: `slice` is limited to `len: u32` by construction + UTF-8. + Self { raw: slice.raw.into() } + } +} +impl From> for Box { + #[inline] + fn from(slice: SlimStr<'_>) -> Self { + slice.deref().into() + } +} +impl From> for String { + #[inline] + fn from(slice: SlimStr<'_>) -> Self { + slice.deref().into() + } +} + +impl<'a, const N: usize> From<&'a NStr> for SlimStr<'a> { + #[inline] + fn from(arr: &'a NStr) -> Self { + #[allow(clippy::let_unit_value)] + let () = AssertU32::::OK; + + // SAFETY: We verified statically by `AssertU32` above that `N` fits in u32. + unsafe { Self::from_str_unchecked(arr) } + } +} +impl<'a> TryFrom<&'a str> for SlimStr<'a> { + type Error = LenTooLong<&'a str>; + + #[inline] + fn try_from(s: &'a str) -> Result { + ensure_len_fits!(s); + // SAFETY: ^-- satisfies `len <= u32::MAX`. + Ok(unsafe { Self::from_str_unchecked(s) }) + } +} + +/// Converts `&str` into the slim limited version. +/// +/// Panics when `str.len() > u32::MAX`. +#[inline] +pub const fn from_str(s: &str) -> SlimStr<'_> { + if s.len() > u32::MAX as usize { + panic!("length didn't fit in `u32`"); + } + + // SAFETY: ^-- satisfies `len <= u32::MAX`. + unsafe { SlimStr::from_str_unchecked(s) } +} + +/// Converts `&str` into the owned slim limited version. +/// +/// Panics when `str.len() > u32::MAX`. +#[inline] +pub fn from_string(s: &str) -> SlimStrBox { + from_str(s).into() +} + +// ============================================================================= +// Mutable string slice reference +// ============================================================================= + +/// A mutable reference to `str` limited to `u32::MAX` in length. +#[repr(transparent)] +pub struct SlimStrMut<'a> { + /// The raw byte slice. + raw: SlimSliceMut<'a, u8>, +} + +impl<'a> SlimStrMut<'a> { + /// Converts `s` to `Self` without checking `s.len() <= u32::MAX`. + /// + /// # Safety + /// + /// The caller must ensure that `s.len() <= u32::MAX`. + #[inline] + unsafe fn from_str_unchecked(s: &'a mut str) -> Self { + // SAFETY: Caller has promised that `s.len() <= u32::MAX`. + let raw = unsafe { SlimSliceMut::from_slice_unchecked(s.as_bytes_mut()) }; + // SAFETY: `s: &mut str` is always UTF-8. + Self { raw } + } +} + +impl Deref for SlimStrMut<'_> { + type Target = str; + + #[inline] + fn deref(&self) -> &Self::Target { + // SAFETY: Data is derived from `str` originally so it's valid UTF-8. + unsafe { from_utf8_unchecked(self.raw.deref()) } + } +} + +impl DerefMut for SlimStrMut<'_> { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + // SAFETY: Data is derived from `str` originally so it's valid UTF-8. + unsafe { from_utf8_unchecked_mut(self.raw.deref_mut()) } + } +} + +impl Debug for SlimStrMut<'_> { + #[inline] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Debug::fmt(self.deref(), f) + } +} + +impl Display for SlimStrMut<'_> { + #[inline] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Display::fmt(self.deref(), f) + } +} + +impl Hash for SlimStrMut<'_> { + #[inline] + fn hash(&self, state: &mut H) { + Hash::hash(self.deref(), state) + } +} + +impl Eq for SlimStrMut<'_> {} +impl PartialEq for SlimStrMut<'_> { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.deref() == other.deref() + } +} +impl PartialEq for SlimStrMut<'_> { + #[inline] + fn eq(&self, other: &str) -> bool { + self.deref() == other + } +} + +impl Ord for SlimStrMut<'_> { + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + self.deref().cmp(other) + } +} +impl PartialOrd for SlimStrMut<'_> { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + self.deref().partial_cmp(other.deref()) + } +} +impl PartialOrd for SlimStrMut<'_> { + #[inline] + fn partial_cmp(&self, other: &str) -> Option { + self.deref().partial_cmp(other) + } +} + +impl From<&SlimStrMut<'_>> for SlimStrBox { + #[inline] + fn from(slice: &SlimStrMut<'_>) -> Self { + // SAFETY: `slice` is limited to `len: u32` by construction + UTF-8. + Self { + raw: (&slice.raw).into(), + } + } +} +impl From<&SlimStrMut<'_>> for Box { + #[inline] + fn from(slice: &SlimStrMut<'_>) -> Self { + slice.deref().into() + } +} +impl From<&SlimStrMut<'_>> for String { + #[inline] + fn from(slice: &SlimStrMut<'_>) -> Self { + slice.deref().into() + } +} + +impl From> for SlimStrBox { + #[inline] + fn from(slice: SlimStrMut<'_>) -> Self { + (&slice).into() + } +} +impl From> for Box { + #[inline] + fn from(slice: SlimStrMut<'_>) -> Self { + slice.deref().into() + } +} +impl From> for String { + #[inline] + fn from(slice: SlimStrMut<'_>) -> Self { + slice.deref().into() + } +} + +impl<'a, const N: usize> From<&'a mut NStr> for SlimStrMut<'a> { + #[inline] + fn from(arr: &'a mut NStr) -> Self { + #[allow(clippy::let_unit_value)] + let () = AssertU32::::OK; + + // SAFETY: We verified statically by `AssertU32` above that `N` fits in u32. + unsafe { Self::from_str_unchecked(arr) } + } +} +impl<'a> TryFrom<&'a mut str> for SlimStrMut<'a> { + type Error = LenTooLong<&'a mut str>; + + #[inline] + fn try_from(slice: &'a mut str) -> Result { + ensure_len_fits!(slice); + // SAFETY: ^-- satisfies `len <= u32::MAX`. + Ok(unsafe { Self::from_str_unchecked(slice) }) + } +} + +/// Converts `&mut str` into the slim limited version. +/// +/// Panics when `str.len() > u32::MAX`. +#[inline] +pub fn from_str_mut(s: &mut str) -> SlimStrMut<'_> { + expect_fit(s) +} + +#[cfg(test)] +mod tests { + use std::collections::hash_map::DefaultHasher; + + use crate::nstr; + + use super::*; + + fn hash_of(x: T) -> u64 { + let mut hasher = DefaultHasher::new(); + x.hash(&mut hasher); + hasher.finish() + } + + fn hash_properties(a: &T, b: &T, a_deref: &T::Target, b_deref: &T::Target) + where + T: Hash + Debug + Deref, + ::Target: Hash, + { + assert_eq!(hash_of(a), hash_of(a_deref)); + assert_eq!(hash_of(b), hash_of(b_deref)); + assert_ne!(hash_of(a), hash_of(b)); + } + + fn ord_properties(a: &T, b: &T, a_deref: &T::Target, b_deref: &T::Target) + where + T: Ord + Debug + Deref + PartialOrd<::Target>, + { + assert_eq!(a.partial_cmp(b), Some(Ordering::Less)); + assert_eq!(b.partial_cmp(a), Some(Ordering::Greater)); + assert_eq!(a.partial_cmp(a), Some(Ordering::Equal)); + assert_eq!(b.partial_cmp(b), Some(Ordering::Equal)); + assert_eq!(a.partial_cmp(b_deref), Some(Ordering::Less)); + assert_eq!(b.partial_cmp(a_deref), Some(Ordering::Greater)); + assert_eq!(a.partial_cmp(a_deref), Some(Ordering::Equal)); + assert_eq!(b.partial_cmp(b_deref), Some(Ordering::Equal)); + + assert_eq!(a.cmp(b), Ordering::Less); + assert_eq!(b.cmp(a), Ordering::Greater); + assert_eq!(a.cmp(a), Ordering::Equal); + assert_eq!(b.cmp(b), Ordering::Equal); + } + + #[allow(clippy::eq_op)] + fn eq_properties(a: &T, b: &T, a_deref: &T::Target, b_deref: &T::Target) + where + T: Eq + Debug + Deref + PartialEq<::Target>, + { + assert!(a != b); + assert!(b != a); + assert_eq!(a, a); + assert!(a != b_deref); + assert!(a == a_deref); + assert!(b != a_deref); + assert!(b == b_deref); + } + + fn debug_properties(a: &T, b: &T, a_cmp: &U, b_cmp: &U) { + assert_eq!(format!("{:?}", a), format!("{:?}", a_cmp)); + assert_eq!(format!("{:?}", b), format!("{:?}", b_cmp)); + } + + fn display_properties(a: &T, b: &T, a_cmp: &U, b_cmp: &U) { + assert_eq!(a.to_string(), a_cmp.to_string()); + assert_eq!(b.to_string(), b_cmp.to_string()); + } + + fn general_properties(a: &T, b: &T, a_deref: &U, b_deref: &U) + where + T: Deref + Debug + Eq + PartialEq + PartialOrd + Ord + Hash, + U: ?Sized + Debug + Eq + Ord + Hash, + { + eq_properties(a, b, a_deref, b_deref); + ord_properties(a, b, a_deref, b_deref); + hash_properties(a, b, a_deref, b_deref); + debug_properties(a, b, a_deref, b_deref); + } + + const TEST_STR: &str = "foo"; + const TEST_STR2: &str = "fop"; + const TEST_SLICE: &[u8] = TEST_STR.as_bytes(); + const TEST_SLICE2: &[u8] = TEST_STR2.as_bytes(); + + fn test_strings() -> [String; 2] { + [TEST_STR.to_string(), TEST_STR2.to_string()] + } + + fn test_slices() -> [Vec; 2] { + [TEST_SLICE.to_owned(), TEST_SLICE2.to_owned()] + } + + fn various_boxed_slices() -> [[SlimSliceBox; 2]; 5] { + [ + test_slices().map(SlimSliceBox::from_vec), + test_slices().map(Box::from).map(SlimSliceBox::from_boxed), + test_slices().map(|s| SlimSliceBox::try_from(s).unwrap()), + test_slices().map(|s| SlimSliceBox::try_from(s.into_boxed_slice()).unwrap()), + test_slices().map(|s| SlimSliceBox::from(<[u8; 3]>::try_from(s).unwrap())), + ] + } + + fn various_boxed_strs() -> [[SlimStrBox; 2]; 7] { + [ + [nstr!("foo"), nstr!("fop")], + test_strings().map(|s| from_string(&s)), + test_strings().map(SlimStrBox::from_string), + test_strings().map(Box::from).map(SlimStrBox::from_boxed), + test_strings().map(|s| SlimStrBox::try_from(s).unwrap()), + test_strings().map(|s| SlimStrBox::try_from(s.into_boxed_str()).unwrap()), + test_strings().map(|s| SlimStrBox::try_from(s.deref()).unwrap()), + ] + } + + fn assert_str_mut_properties(s1: &mut SlimStrMut<'_>, s2: &mut SlimStrMut<'_>) { + let a: &SlimStrMut<'_> = s1; + let b: &SlimStrMut<'_> = s2; + + assert_eq!(a.deref(), TEST_STR); + assert_eq!(SlimStrBox::from(a).clone().deref(), TEST_STR); + assert_eq!(b.deref(), TEST_STR2); + + assert_eq!(String::from(a), TEST_STR); + assert_eq!(>::from(a).deref(), TEST_STR); + assert_eq!(SlimStrBox::from(a).deref(), TEST_STR); + assert_eq!(>::from(SlimStrBox::from(a)).deref(), TEST_STR); + + general_properties(a, b, TEST_STR, TEST_STR2); + display_properties(a, b, TEST_STR, TEST_STR2); + + s1.deref_mut().make_ascii_uppercase(); + assert_eq!(&**s1, TEST_STR.to_uppercase()); + } + + #[test] + fn str_mut_call() { + let [mut s1, mut s2] = test_strings(); + let s1 = &mut from_str_mut(s1.as_mut_str()); + let s2 = &mut from_str_mut(s2.as_mut_str()); + assert_str_mut_properties(s1, s2); + } + + #[test] + fn str_mut_try_into() { + let [mut s1, mut s2] = test_strings(); + let s1: &mut SlimStrMut = &mut s1.as_mut().try_into().unwrap(); + let s2: &mut SlimStrMut = &mut s2.as_mut().try_into().unwrap(); + assert_str_mut_properties(s1, s2); + } + + #[test] + fn str_mut_exclusive_ref_various() { + for [mut a, mut b] in various_boxed_strs() { + assert_str_mut_properties(a.exclusive_ref(), b.exclusive_ref()) + } + } + + fn assert_str_properties(a: &SlimStr<'_>, b: &SlimStr<'_>) { + assert_eq!(a.deref(), TEST_STR); + assert_eq!(SlimStrBox::from(a).clone().deref(), TEST_STR); + assert_eq!(b.deref(), TEST_STR2); + + assert_eq!(String::from(a), TEST_STR); + assert_eq!(>::from(a).deref(), TEST_STR); + assert_eq!(SlimStrBox::from(a).deref(), TEST_STR); + assert_eq!(String::from(SlimStrBox::from(a)).deref(), TEST_STR); + assert_eq!(>::from(SlimStrBox::from(a)).deref(), TEST_STR); + + general_properties(a, b, TEST_STR, TEST_STR2); + display_properties(a, b, TEST_STR, TEST_STR2); + } + + #[test] + fn str_call() { + let [s1, s2] = test_strings(); + assert_str_properties(&from_str(&s1), &from_str(&s2)); + } + + #[test] + fn str_try_into() { + let [s1, s2] = test_strings(); + let s1: &SlimStr = &mut s1.deref().try_into().unwrap(); + let s2: &SlimStr = &mut s2.deref().try_into().unwrap(); + assert_str_properties(s1, s2); + } + + #[test] + fn str_shared_ref_various() { + for [a, b] in various_boxed_strs() { + assert_str_properties(a.shared_ref(), b.shared_ref()) + } + } + + fn assert_slice_mut_properties(s1: &mut SlimSliceMut<'_, u8>, s2: &mut SlimSliceMut<'_, u8>) { + let a: &SlimSliceMut<'_, u8> = s1; + let b: &SlimSliceMut<'_, u8> = s2; + + assert_eq!(a.deref(), TEST_SLICE); + assert_eq!(SlimSliceBox::from(a).clone().deref(), TEST_SLICE); + assert_eq!(b.deref(), TEST_SLICE2); + + assert_eq!(>::from(a), TEST_SLICE); + assert_eq!(>::from(a).deref(), TEST_SLICE); + assert_eq!(>::from(a).deref(), TEST_SLICE); + assert_eq!(>::from(>::from(a)).deref(), TEST_SLICE); + + general_properties(a, b, TEST_SLICE, TEST_SLICE2); + + s1.deref_mut().make_ascii_uppercase(); + let mut upper = TEST_SLICE.to_owned(); + upper.iter_mut().for_each(|x| x.make_ascii_uppercase()); + assert_eq!(&**s1, upper); + } + + #[test] + fn slice_mut_call() { + let [mut s1, mut s2] = test_slices(); + let s1 = &mut from_slice_mut(s1.as_mut()); + let s2 = &mut from_slice_mut(s2.as_mut()); + assert_slice_mut_properties(s1, s2); + } + + #[test] + fn slice_mut_try_into() { + let [mut s1, mut s2] = test_slices(); + let s1: &mut SlimSliceMut = &mut s1.deref_mut().try_into().unwrap(); + let s2: &mut SlimSliceMut = &mut s2.deref_mut().try_into().unwrap(); + assert_slice_mut_properties(s1, s2); + } + + #[test] + fn slice_mut_exclusive_ref_various() { + for [mut a, mut b] in various_boxed_slices() { + assert_slice_mut_properties(a.exclusive_ref(), b.exclusive_ref()); + } + } + + fn assert_slice_properties(a: &SlimSlice<'_, u8>, b: &SlimSlice<'_, u8>) { + assert_eq!(a.deref(), TEST_SLICE); + assert_eq!(SlimSliceBox::from(a).clone().deref(), TEST_SLICE); + assert_eq!(b.deref(), TEST_SLICE2); + + assert_eq!(>::from(a), TEST_SLICE); + assert_eq!(>::from(a).deref(), TEST_SLICE); + assert_eq!(>::from(a).deref(), TEST_SLICE); + assert_eq!(>::from(>::from(a)).deref(), TEST_SLICE); + + general_properties(a, b, TEST_SLICE, TEST_SLICE2); + } + + #[test] + fn slice_call() { + let [s1, s2] = test_slices(); + assert_slice_properties(&from_slice(&s1), &from_slice(&s2)); + } + + #[test] + fn slice_try_into() { + let [s1, s2] = test_slices(); + let s1: &SlimSlice = &s1.deref().try_into().unwrap(); + let s2: &SlimSlice = &s2.deref().try_into().unwrap(); + assert_slice_properties(s1, s2); + } + + #[test] + fn slice_shared_ref_various() { + for [a, b] in various_boxed_slices() { + assert_slice_properties(a.shared_ref(), b.shared_ref()) + } + } +}