From d4145b72a3bc6207228dd51fd2b3c6ea078e2370 Mon Sep 17 00:00:00 2001 From: Vivek Revankar Date: Sun, 27 Jul 2025 21:33:07 -0700 Subject: [PATCH 01/12] optimize AsciiNumber --- src/lib.rs | 44 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index cba1706..8ed81d1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -75,7 +75,7 @@ #![no_std] use core::fmt::{Debug, Display, Formatter}; -use core::mem::size_of; +use core::mem::{size_of, MaybeUninit}; use core::ops::Deref; use core::str; @@ -131,21 +131,23 @@ const MAX_SUPPORTED_BASE: u128 = LOOKUP.len() as u128; /// The result of a number conversion to ascii containing a string with at most length `N` bytes/characters pub struct AsciiNumber { - string: [u8; N], + string: [MaybeUninit; N], start: usize, } impl AsciiNumber { /// Get the ascii representation of the number as a byte slice pub const fn as_slice(&self) -> &[u8] { - self.string.split_at(self.start).1 + // SAFETY: number was initialized from self.start + unsafe { assume_slice_init(self.string.split_at(self.start).1) } } /// Get the ascii representation of the number as a string slice pub const fn as_str(&self) -> &str { + // SAFETY: data is always ascii unsafe { core::str::from_utf8_unchecked(Self::as_slice(self)) } } /// Consume this AsciiNumber to return the underlying buffer & string start position - pub const fn into_inner(self) -> ([u8;N], usize) { + pub const fn into_inner(self) -> ([MaybeUninit;N], usize) { (self.string, self.start) } } @@ -169,6 +171,16 @@ impl Debug for AsciiNumber { } } +#[inline] +const unsafe fn assume_mut_slice_init(thing: &mut [MaybeUninit]) -> &mut [T] { + core::mem::transmute::<&mut [MaybeUninit], &mut [T]>(thing) +} + +#[inline] +const unsafe fn assume_slice_init(thing: &[MaybeUninit]) -> &[T] { + core::mem::transmute::<&[MaybeUninit], &[T]>(thing) +} + macro_rules! copy_2_dec_lut_bytes { ($to:ident,$to_index:expr,$lut_index:expr) => { $to[$to_index as usize] = DEC_LOOKUP[$lut_index as usize]; @@ -257,7 +269,7 @@ macro_rules! impl_unsigned_numtoa_for { impl NumToA for $type_name { fn numtoa(self, base: $type_name, string: &mut [u8]) -> &[u8] { - $core_function_name(self, base, string) + $core_function_name(self, base, string) } fn numtoa_str(self, base: $type_name, buf: &mut [u8]) -> &str { $str_function_name(self, base, buf) @@ -490,15 +502,25 @@ macro_rules! impl_numtoa_streamlined_for_type { $needed_buffer_size:expr) => { pub const fn $base_n_function_name(num: $type_name) -> AsciiNumber<$needed_buffer_size> { - let mut string = [0_u8; $needed_buffer_size]; - let start = $needed_buffer_size - $core_function_name(num, $base, &mut string).len(); + let mut string: [MaybeUninit; $needed_buffer_size] = [MaybeUninit::uninit(); $needed_buffer_size]; + let start = $needed_buffer_size - $core_function_name( + num, + $base, + // SAFETY: numtoa never reads from the string + unsafe { assume_mut_slice_init(&mut string) }, + ).len(); return AsciiNumber { string, start } } pub const fn $padded_function_name(num: $type_name, padding: u8) -> AsciiNumber { const { assert!(LENGTH >= $needed_buffer_size) } - let mut string = [padding; LENGTH]; - let _ = $core_function_name(num, $base, &mut string); + let mut string = [MaybeUninit::new(padding); LENGTH]; + let _ = $core_function_name( + num, + $base, + // SAFETY: numtoa never reads from the string + unsafe { assume_mut_slice_init(&mut string) }, + ); return AsciiNumber { string, start: 0 } } }; @@ -520,7 +542,9 @@ macro_rules! impl_numtoa_streamlined_for { $i128_needed_size:expr ) => { pub mod $base_module_name { - + + use core::mem::MaybeUninit; + use assume_mut_slice_init; use AsciiNumber; use numtoa_u8; use numtoa_u16; From c20759887e80ce24d72de41c16a713f4153da86a Mon Sep 17 00:00:00 2001 From: Vivek Revankar Date: Sun, 27 Jul 2025 22:18:27 -0700 Subject: [PATCH 02/12] optimize everything --- src/lib.rs | 167 +++++++++++++++++++++++++++++------------------------ 1 file changed, 91 insertions(+), 76 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 8ed81d1..5ef45bb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -176,6 +176,12 @@ const unsafe fn assume_mut_slice_init(thing: &mut [MaybeUninit]) -> &mut [ core::mem::transmute::<&mut [MaybeUninit], &mut [T]>(thing) } +#[inline] +const fn assume_mut_slice_uninit(thing: &mut [T]) -> &mut [MaybeUninit] { + // SAFETY: tbh i think this is always safe + unsafe { core::mem::transmute::<&mut [T], &mut [MaybeUninit]>(thing) } +} + #[inline] const unsafe fn assume_slice_init(thing: &[MaybeUninit]) -> &[T] { core::mem::transmute::<&[MaybeUninit], &[T]>(thing) @@ -183,8 +189,8 @@ const unsafe fn assume_slice_init(thing: &[MaybeUninit]) -> &[T] { macro_rules! copy_2_dec_lut_bytes { ($to:ident,$to_index:expr,$lut_index:expr) => { - $to[$to_index as usize] = DEC_LOOKUP[$lut_index as usize]; - $to[$to_index as usize+1] = DEC_LOOKUP[$lut_index as usize+1]; + $to[$to_index as usize] = MaybeUninit::new(DEC_LOOKUP[$lut_index as usize]); + $to[$to_index as usize+1] = MaybeUninit::new(DEC_LOOKUP[$lut_index as usize+1]); }; } @@ -207,14 +213,14 @@ macro_rules! base_10 { } else if $number > 99 { let section = ($number as u16 / 10) * 2; copy_2_dec_lut_bytes!($string, $index-2, section); - $string[$index] = LOOKUP[($number % 10) as usize]; + $string[$index] = MaybeUninit::new(LOOKUP[($number % 10) as usize]); $index = $index.wrapping_sub(3); } else if $number > 9 { $number *= 2; copy_2_dec_lut_bytes!($string, $index-1, $number); $index = $index.wrapping_sub(2); } else { - $string[$index] = LOOKUP[$number as usize]; + $string[$index] = MaybeUninit::new(LOOKUP[$number as usize]); $index = $index.wrapping_sub(1); } } @@ -223,11 +229,12 @@ macro_rules! base_10 { macro_rules! impl_unsigned_numtoa_for { ( $type_name:ty, + $uninit_function_name:ident, $core_function_name:ident, $str_function_name:ident ) => { - pub const fn $core_function_name(mut num: $type_name, base: $type_name, string: &mut [u8]) -> &[u8] { + pub const fn $uninit_function_name(mut num: $type_name, base: $type_name, string: &mut [MaybeUninit]) -> &[u8] { // Check if the buffer is large enough and panic on debug builds if it isn't if cfg!(debug_assertions) { debug_assert!(base > 1 && base as u128 <= MAX_SUPPORTED_BASE, "unsupported base"); @@ -244,8 +251,8 @@ macro_rules! impl_unsigned_numtoa_for { let mut index = string.len() - 1; if num == 0 { - string[index] = b'0'; - return string.split_at(index).1; + string[index] = MaybeUninit::new(b'0'); + return unsafe { assume_slice_init(string.split_at(index).1) } } if base == 10 { @@ -254,13 +261,17 @@ macro_rules! impl_unsigned_numtoa_for { } else { while num != 0 { let rem = num % base; - string[index] = LOOKUP[rem as usize]; + string[index] = MaybeUninit::new(LOOKUP[rem as usize]); index = index.wrapping_sub(1); num /= base; } } - string.split_at(index.wrapping_add(1)).1 + unsafe { assume_slice_init(string.split_at(index.wrapping_add(1)).1) } + } + + pub const fn $core_function_name(num: $type_name, base: $type_name, string: &mut [u8]) -> &[u8] { + $uninit_function_name(num, base, assume_mut_slice_uninit(string)) } pub const fn $str_function_name(num: $type_name, base: $type_name, string: &mut [u8]) -> &str { @@ -282,11 +293,12 @@ macro_rules! impl_unsigned_numtoa_for { macro_rules! impl_signed_numtoa_for { ( $type_name:ty, + $uninit_function_name:ident, $core_function_name:ident, $str_function_name:ident ) => { - pub const fn $core_function_name(mut num: $type_name, base: $type_name, string: &mut [u8]) -> &[u8] { + pub const fn $uninit_function_name(mut num: $type_name, base: $type_name, string: &mut [MaybeUninit]) -> &[u8] { if cfg!(debug_assertions) { debug_assert!(base > 1 && base as u128 <= MAX_SUPPORTED_BASE, "unsupported base"); if base == 10 { @@ -309,14 +321,14 @@ macro_rules! impl_signed_numtoa_for { Some(value) => value, None => { let value = <$type_name>::max_value(); - string[index] = LOOKUP[((value % base + 1) % base) as usize]; + string[index] = MaybeUninit::new(LOOKUP[((value % base + 1) % base) as usize]); index -= 1; value / base + ((value % base == base - 1) as $type_name) } }; } else if num == 0 { - string[index] = b'0'; - return string.split_at(index).1; + string[index] = MaybeUninit::new(b'0'); + return unsafe { assume_slice_init(string.split_at(index).1) }; } if base == 10 { @@ -325,18 +337,22 @@ macro_rules! impl_signed_numtoa_for { } else { while num != 0 { let rem = num % base; - string[index] = LOOKUP[rem as usize]; + string[index] = MaybeUninit::new(LOOKUP[rem as usize]); index = index.wrapping_sub(1); num /= base; } } if is_negative { - string[index] = b'-'; + string[index] = MaybeUninit::new(b'-'); index = index.wrapping_sub(1); } - string.split_at(index.wrapping_add(1)).1 + unsafe { assume_slice_init(string.split_at(index.wrapping_add(1)).1) } + } + + pub const fn $core_function_name(num: $type_name, base: $type_name, string: &mut [u8]) -> &[u8] { + $uninit_function_name(num, base, assume_mut_slice_uninit(string)) } pub const fn $str_function_name(num: $type_name, base: $type_name, string: &mut [u8]) -> &str { @@ -355,18 +371,18 @@ macro_rules! impl_signed_numtoa_for { } } -impl_signed_numtoa_for!(i16,numtoa_i16,numtoa_i16_str); -impl_signed_numtoa_for!(i32,numtoa_i32,numtoa_i32_str); -impl_signed_numtoa_for!(i64,numtoa_i64,numtoa_i64_str); -impl_signed_numtoa_for!(i128,numtoa_i128,numtoa_i128_str); -impl_signed_numtoa_for!(isize,numtoa_isize,numtoa_isize_str); -impl_unsigned_numtoa_for!(u16,numtoa_u16,numtoa_u16_str); -impl_unsigned_numtoa_for!(u32,numtoa_u32,numtoa_u32_str); -impl_unsigned_numtoa_for!(u64,numtoa_u64,numtoa_u64_str); -impl_unsigned_numtoa_for!(u128,numtoa_u128,numtoa_u128_str); -impl_unsigned_numtoa_for!(usize,numtoa_usize,numtoa_usize_str); +impl_signed_numtoa_for!(i16,numtoa_uninit_i16,numtoa_i16,numtoa_i16_str); +impl_signed_numtoa_for!(i32,numtoa_uninit_i32,numtoa_i32,numtoa_i32_str); +impl_signed_numtoa_for!(i64,numtoa_uninit_i64,numtoa_i64,numtoa_i64_str); +impl_signed_numtoa_for!(i128,numtoa_uninit_i128,numtoa_i128,numtoa_i128_str); +impl_signed_numtoa_for!(isize,numtoa_uninit_isize,numtoa_isize,numtoa_isize_str); +impl_unsigned_numtoa_for!(u16,numtoa_uninit_u16,numtoa_u16,numtoa_u16_str); +impl_unsigned_numtoa_for!(u32,numtoa_uninit_u32,numtoa_u32,numtoa_u32_str); +impl_unsigned_numtoa_for!(u64,numtoa_uninit_u64,numtoa_u64,numtoa_u64_str); +impl_unsigned_numtoa_for!(u128,numtoa_uninit_u128,numtoa_u128,numtoa_u128_str); +impl_unsigned_numtoa_for!(usize,numtoa_uninit_usize,numtoa_usize,numtoa_usize_str); -pub const fn numtoa_i8(mut num: i8, base: i8, string: &mut [u8]) -> &[u8] { +pub const fn numtoa_uninit_i8(mut num: i8, base: i8, string: &mut [MaybeUninit]) -> &[u8] { if cfg!(debug_assertions) { debug_assert!(base > 1 && base as u128 <= MAX_SUPPORTED_BASE, "unsupported base"); if base == 10 { @@ -383,45 +399,49 @@ pub const fn numtoa_i8(mut num: i8, base: i8, string: &mut [u8]) -> &[u8] { Some(value) => value, None => { let value = ::max_value(); - string[index] = LOOKUP[((value % base + 1) % base) as usize]; + string[index] = MaybeUninit::new(LOOKUP[((value % base + 1) % base) as usize]); index -= 1; value / base + ((value % base == base - 1) as i8) } }; } else if num == 0 { - string[index] = b'0'; - return string.split_at(index).1; + string[index] = MaybeUninit::new(b'0'); + return unsafe { assume_slice_init(string.split_at(index).1) }; } if base == 10 { if num > 99 { let section = (num / 10) * 2; copy_2_dec_lut_bytes!(string, index-2, section); - string[index] = LOOKUP[(num % 10) as usize]; + string[index] = MaybeUninit::new(LOOKUP[(num % 10) as usize]); index = index.wrapping_sub(3); } else if num > 9 { let idx = num as usize * 2; copy_2_dec_lut_bytes!(string, index-1, idx); index = index.wrapping_sub(2); } else { - string[index] = LOOKUP[num as usize]; + string[index] = MaybeUninit::new(LOOKUP[num as usize]); index = index.wrapping_sub(1); } } else { while num != 0 { let rem = num % base; - string[index] = LOOKUP[rem as usize]; + string[index] = MaybeUninit::new(LOOKUP[rem as usize]); index = index.wrapping_sub(1); num /= base; } } if is_negative { - string[index] = b'-'; + string[index] = MaybeUninit::new(b'-'); index = index.wrapping_sub(1); } - string.split_at(index.wrapping_add(1)).1 + unsafe { assume_slice_init(string.split_at(index.wrapping_add(1)).1) } +} + +pub const fn numtoa_i8(num: i8, base: i8, string: &mut [u8]) -> &[u8] { + numtoa_uninit_i8(num, base, assume_mut_slice_uninit(string)) } pub const fn numtoa_i8_str(num: i8, base: i8, string: &mut [u8]) -> &str { @@ -438,7 +458,7 @@ impl NumToA for i8 { } } -pub const fn numtoa_u8(mut num: u8, base: u8, string: &mut [u8]) -> &[u8] { +pub const fn numtoa_uninit_u8(mut num: u8, base: u8, string: &mut [MaybeUninit]) -> &[u8] { if cfg!(debug_assertions) { debug_assert!(base > 1 && base as u128 <= MAX_SUPPORTED_BASE, "unsupported base"); if base == 10 { @@ -448,34 +468,38 @@ pub const fn numtoa_u8(mut num: u8, base: u8, string: &mut [u8]) -> &[u8] { let mut index = string.len() - 1; if num == 0 { - string[index] = b'0'; - return string.split_at(index).1; + string[index] = MaybeUninit::new(b'0'); + return unsafe { assume_slice_init(string.split_at(index).1) }; } if base == 10 { if num > 99 { let section = (num / 10) * 2; copy_2_dec_lut_bytes!(string, index-2, section); - string[index] = LOOKUP[(num % 10) as usize]; + string[index] = MaybeUninit::new(LOOKUP[(num % 10) as usize]); index = index.wrapping_sub(3); } else if num > 9 { num *= 2; copy_2_dec_lut_bytes!(string, index-1, num); index = index.wrapping_sub(2); } else { - string[index] = LOOKUP[num as usize]; + string[index] = MaybeUninit::new(LOOKUP[num as usize]); index = index.wrapping_sub(1); } } else { while num != 0 { let rem = num % base; - string[index] = LOOKUP[rem as usize]; + string[index] = MaybeUninit::new(LOOKUP[rem as usize]); index = index.wrapping_sub(1); num /= base; } } - string.split_at(index.wrapping_add(1)).1 + unsafe { assume_slice_init(string.split_at(index.wrapping_add(1)).1) } +} + +pub const fn numtoa_u8(num: u8, base: u8, string: &mut [u8]) -> &[u8] { + numtoa_uninit_u8(num, base, assume_mut_slice_uninit(string)) } pub const fn numtoa_u8_str(num: u8, base: u8, string: &mut [u8]) -> &str { @@ -503,26 +527,18 @@ macro_rules! impl_numtoa_streamlined_for_type { pub const fn $base_n_function_name(num: $type_name) -> AsciiNumber<$needed_buffer_size> { let mut string: [MaybeUninit; $needed_buffer_size] = [MaybeUninit::uninit(); $needed_buffer_size]; - let start = $needed_buffer_size - $core_function_name( - num, - $base, - // SAFETY: numtoa never reads from the string - unsafe { assume_mut_slice_init(&mut string) }, - ).len(); + let start = $needed_buffer_size - $core_function_name(num, $base, &mut string).len(); return AsciiNumber { string, start } } pub const fn $padded_function_name(num: $type_name, padding: u8) -> AsciiNumber { const { assert!(LENGTH >= $needed_buffer_size) } let mut string = [MaybeUninit::new(padding); LENGTH]; - let _ = $core_function_name( - num, - $base, - // SAFETY: numtoa never reads from the string - unsafe { assume_mut_slice_init(&mut string) }, - ); + let _ = $needed_buffer_size - $core_function_name(num, $base, &mut string).len(); + // n.b. this is safe because any repeating u8 is guaranteed to be ascii return AsciiNumber { string, start: 0 } } + }; } @@ -544,29 +560,28 @@ macro_rules! impl_numtoa_streamlined_for { pub mod $base_module_name { use core::mem::MaybeUninit; - use assume_mut_slice_init; use AsciiNumber; - use numtoa_u8; - use numtoa_u16; - use numtoa_u32; - use numtoa_u64; - use numtoa_u128; - use numtoa_i8; - use numtoa_i16; - use numtoa_i32; - use numtoa_i64; - use numtoa_i128; - - impl_numtoa_streamlined_for_type!(u8,$base_value,numtoa_u8,u8,u8_padded,$u8_needed_size); - impl_numtoa_streamlined_for_type!(u16,$base_value,numtoa_u16,u16,u16_padded,$u16_needed_size); - impl_numtoa_streamlined_for_type!(u32,$base_value,numtoa_u32,u32,u32_padded,$u32_needed_size); - impl_numtoa_streamlined_for_type!(u64,$base_value,numtoa_u64,u64,u64_padded,$u64_needed_size); - impl_numtoa_streamlined_for_type!(u128,$base_value,numtoa_u128,u128,u128_padded,$u128_needed_size); - impl_numtoa_streamlined_for_type!(i8,$base_value,numtoa_i8,i8,i8_padded,$i8_needed_size); - impl_numtoa_streamlined_for_type!(i16,$base_value,numtoa_i16,i16,i16_padded,$i16_needed_size); - impl_numtoa_streamlined_for_type!(i32,$base_value,numtoa_i32,i32,i32_padded,$i32_needed_size); - impl_numtoa_streamlined_for_type!(i64,$base_value,numtoa_i64,i64,i64_padded,$i64_needed_size); - impl_numtoa_streamlined_for_type!(i128,$base_value,numtoa_i128,i128,i128_padded,$i128_needed_size); + use numtoa_uninit_u8; + use numtoa_uninit_u16; + use numtoa_uninit_u32; + use numtoa_uninit_u64; + use numtoa_uninit_u128; + use numtoa_uninit_i8; + use numtoa_uninit_i16; + use numtoa_uninit_i32; + use numtoa_uninit_i64; + use numtoa_uninit_i128; + + impl_numtoa_streamlined_for_type!(u8,$base_value,numtoa_uninit_u8,u8,u8_padded,$u8_needed_size); + impl_numtoa_streamlined_for_type!(u16,$base_value,numtoa_uninit_u16,u16,u16_padded,$u16_needed_size); + impl_numtoa_streamlined_for_type!(u32,$base_value,numtoa_uninit_u32,u32,u32_padded,$u32_needed_size); + impl_numtoa_streamlined_for_type!(u64,$base_value,numtoa_uninit_u64,u64,u64_padded,$u64_needed_size); + impl_numtoa_streamlined_for_type!(u128,$base_value,numtoa_uninit_u128,u128,u128_padded,$u128_needed_size); + impl_numtoa_streamlined_for_type!(i8,$base_value,numtoa_uninit_i8,i8,i8_padded,$i8_needed_size); + impl_numtoa_streamlined_for_type!(i16,$base_value,numtoa_uninit_i16,i16,i16_padded,$i16_needed_size); + impl_numtoa_streamlined_for_type!(i32,$base_value,numtoa_uninit_i32,i32,i32_padded,$i32_needed_size); + impl_numtoa_streamlined_for_type!(i64,$base_value,numtoa_uninit_i64,i64,i64_padded,$i64_needed_size); + impl_numtoa_streamlined_for_type!(i128,$base_value,numtoa_uninit_i128,i128,i128_padded,$i128_needed_size); } }; } From 08e36384cecaf4cf608fd5f98fc9e88f11d15421 Mon Sep 17 00:00:00 2001 From: Vivek Revankar Date: Sun, 27 Jul 2025 22:23:54 -0700 Subject: [PATCH 03/12] remove unused function --- src/lib.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5ef45bb..1c26ae0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -171,11 +171,6 @@ impl Debug for AsciiNumber { } } -#[inline] -const unsafe fn assume_mut_slice_init(thing: &mut [MaybeUninit]) -> &mut [T] { - core::mem::transmute::<&mut [MaybeUninit], &mut [T]>(thing) -} - #[inline] const fn assume_mut_slice_uninit(thing: &mut [T]) -> &mut [MaybeUninit] { // SAFETY: tbh i think this is always safe From c71845ef0bb0ebffc26945a39aad6d856b94a369 Mon Sep 17 00:00:00 2001 From: Vivek Revankar Date: Sun, 27 Jul 2025 22:37:15 -0700 Subject: [PATCH 04/12] add debug assertion --- src/lib.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 1c26ae0..d7b1c91 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -130,20 +130,29 @@ const DEC_LOOKUP: &[u8; 200] = b"0001020304050607080910111213141516171819\ const MAX_SUPPORTED_BASE: u128 = LOOKUP.len() as u128; /// The result of a number conversion to ascii containing a string with at most length `N` bytes/characters +// invariant: string[start..] always contains valid ascii pub struct AsciiNumber { string: [MaybeUninit; N], start: usize, } impl AsciiNumber { + + const fn new(string: [MaybeUninit; N], start: usize) -> Self { + if cfg!(debug_assertions) { + debug_assert!(core::str::from_utf8(unsafe { assume_slice_init(&string) }).is_ok(), "ascii number created with non-ascii data"); + } + Self { string, start } + } + /// Get the ascii representation of the number as a byte slice pub const fn as_slice(&self) -> &[u8] { - // SAFETY: number was initialized from self.start + // SAFETY: invariant unsafe { assume_slice_init(self.string.split_at(self.start).1) } } /// Get the ascii representation of the number as a string slice pub const fn as_str(&self) -> &str { - // SAFETY: data is always ascii + // SAFETY: invariant unsafe { core::str::from_utf8_unchecked(Self::as_slice(self)) } } /// Consume this AsciiNumber to return the underlying buffer & string start position @@ -523,7 +532,7 @@ macro_rules! impl_numtoa_streamlined_for_type { pub const fn $base_n_function_name(num: $type_name) -> AsciiNumber<$needed_buffer_size> { let mut string: [MaybeUninit; $needed_buffer_size] = [MaybeUninit::uninit(); $needed_buffer_size]; let start = $needed_buffer_size - $core_function_name(num, $base, &mut string).len(); - return AsciiNumber { string, start } + return AsciiNumber::new(string, start) } pub const fn $padded_function_name(num: $type_name, padding: u8) -> AsciiNumber { @@ -531,7 +540,7 @@ macro_rules! impl_numtoa_streamlined_for_type { let mut string = [MaybeUninit::new(padding); LENGTH]; let _ = $needed_buffer_size - $core_function_name(num, $base, &mut string).len(); // n.b. this is safe because any repeating u8 is guaranteed to be ascii - return AsciiNumber { string, start: 0 } + return AsciiNumber::new(string, 0) } }; From c53d2df88920ac93fde374c51bea8718968a4946 Mon Sep 17 00:00:00 2001 From: Vivek Revankar Date: Sun, 27 Jul 2025 22:40:44 -0700 Subject: [PATCH 05/12] fix debug assertion --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index d7b1c91..039b9b7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -140,7 +140,7 @@ impl AsciiNumber { const fn new(string: [MaybeUninit; N], start: usize) -> Self { if cfg!(debug_assertions) { - debug_assert!(core::str::from_utf8(unsafe { assume_slice_init(&string) }).is_ok(), "ascii number created with non-ascii data"); + debug_assert!(core::str::from_utf8(unsafe { assume_slice_init(string.split_at(start).1) }).is_ok(), "ascii number created with non-ascii data"); } Self { string, start } } From 58d07d9900f4f77580977064ebabe54ea31665f0 Mon Sep 17 00:00:00 2001 From: Vivek Revankar Date: Sun, 27 Jul 2025 22:45:29 -0700 Subject: [PATCH 06/12] remove generics for increased safety --- src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 039b9b7..ebc6b44 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -181,14 +181,14 @@ impl Debug for AsciiNumber { } #[inline] -const fn assume_mut_slice_uninit(thing: &mut [T]) -> &mut [MaybeUninit] { +const fn assume_mut_slice_uninit(thing: &mut [u8]) -> &mut [MaybeUninit] { // SAFETY: tbh i think this is always safe - unsafe { core::mem::transmute::<&mut [T], &mut [MaybeUninit]>(thing) } + unsafe { core::mem::transmute::<&mut [u8], &mut [MaybeUninit]>(thing) } } #[inline] -const unsafe fn assume_slice_init(thing: &[MaybeUninit]) -> &[T] { - core::mem::transmute::<&[MaybeUninit], &[T]>(thing) +const unsafe fn assume_slice_init(thing: &[MaybeUninit]) -> &[u8] { + core::mem::transmute::<&[MaybeUninit], &[u8]>(thing) } macro_rules! copy_2_dec_lut_bytes { From daf6322a6f12ec96440a1dce3610017e356bb2dd Mon Sep 17 00:00:00 2001 From: Vivek Revankar Date: Sun, 27 Jul 2025 22:50:20 -0700 Subject: [PATCH 07/12] add uninit str functions --- src/lib.rs | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index ebc6b44..305e4b5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -233,12 +233,13 @@ macro_rules! base_10 { macro_rules! impl_unsigned_numtoa_for { ( $type_name:ty, - $uninit_function_name:ident, + $uninit_core_function_name:ident, + $uninit_str_function_name:ident, $core_function_name:ident, $str_function_name:ident ) => { - pub const fn $uninit_function_name(mut num: $type_name, base: $type_name, string: &mut [MaybeUninit]) -> &[u8] { + pub const fn $uninit_core_function_name(mut num: $type_name, base: $type_name, string: &mut [MaybeUninit]) -> &[u8] { // Check if the buffer is large enough and panic on debug builds if it isn't if cfg!(debug_assertions) { debug_assert!(base > 1 && base as u128 <= MAX_SUPPORTED_BASE, "unsupported base"); @@ -274,8 +275,12 @@ macro_rules! impl_unsigned_numtoa_for { unsafe { assume_slice_init(string.split_at(index.wrapping_add(1)).1) } } + pub const fn $uninit_str_function_name(num: $type_name, base: $type_name, string: &mut [MaybeUninit]) -> &str { + unsafe { core::str::from_utf8_unchecked($uninit_core_function_name(num, base, string)) } + } + pub const fn $core_function_name(num: $type_name, base: $type_name, string: &mut [u8]) -> &[u8] { - $uninit_function_name(num, base, assume_mut_slice_uninit(string)) + $uninit_core_function_name(num, base, assume_mut_slice_uninit(string)) } pub const fn $str_function_name(num: $type_name, base: $type_name, string: &mut [u8]) -> &str { @@ -298,6 +303,7 @@ macro_rules! impl_signed_numtoa_for { ( $type_name:ty, $uninit_function_name:ident, + $uninit_str_function_name:ident, $core_function_name:ident, $str_function_name:ident ) => { @@ -355,6 +361,10 @@ macro_rules! impl_signed_numtoa_for { unsafe { assume_slice_init(string.split_at(index.wrapping_add(1)).1) } } + pub const fn $uninit_str_function_name(num: $type_name, base: $type_name, string: &mut [MaybeUninit]) -> &str { + unsafe { core::str::from_utf8_unchecked($uninit_function_name(num, base, string)) } + } + pub const fn $core_function_name(num: $type_name, base: $type_name, string: &mut [u8]) -> &[u8] { $uninit_function_name(num, base, assume_mut_slice_uninit(string)) } @@ -375,16 +385,16 @@ macro_rules! impl_signed_numtoa_for { } } -impl_signed_numtoa_for!(i16,numtoa_uninit_i16,numtoa_i16,numtoa_i16_str); -impl_signed_numtoa_for!(i32,numtoa_uninit_i32,numtoa_i32,numtoa_i32_str); -impl_signed_numtoa_for!(i64,numtoa_uninit_i64,numtoa_i64,numtoa_i64_str); -impl_signed_numtoa_for!(i128,numtoa_uninit_i128,numtoa_i128,numtoa_i128_str); -impl_signed_numtoa_for!(isize,numtoa_uninit_isize,numtoa_isize,numtoa_isize_str); -impl_unsigned_numtoa_for!(u16,numtoa_uninit_u16,numtoa_u16,numtoa_u16_str); -impl_unsigned_numtoa_for!(u32,numtoa_uninit_u32,numtoa_u32,numtoa_u32_str); -impl_unsigned_numtoa_for!(u64,numtoa_uninit_u64,numtoa_u64,numtoa_u64_str); -impl_unsigned_numtoa_for!(u128,numtoa_uninit_u128,numtoa_u128,numtoa_u128_str); -impl_unsigned_numtoa_for!(usize,numtoa_uninit_usize,numtoa_usize,numtoa_usize_str); +impl_signed_numtoa_for!(i16,numtoa_uninit_i16,numtoa_uninit_i16_str,numtoa_i16,numtoa_i16_str); +impl_signed_numtoa_for!(i32,numtoa_uninit_i32,numtoa_uninit_i32_str,numtoa_i32,numtoa_i32_str); +impl_signed_numtoa_for!(i64,numtoa_uninit_i64,numtoa_uninit_i64_str,numtoa_i64,numtoa_i64_str); +impl_signed_numtoa_for!(i128,numtoa_uninit_i128,numtoa_uninit_i128_str,numtoa_i128,numtoa_i128_str); +impl_signed_numtoa_for!(isize,numtoa_uninit_isize,numtoa_uninit_isize_str,numtoa_isize,numtoa_isize_str); +impl_unsigned_numtoa_for!(u16,numtoa_uninit_u16,numtoa_uninit_u16_str,numtoa_u16,numtoa_u16_str); +impl_unsigned_numtoa_for!(u32,numtoa_uninit_u32,numtoa_uninit_u32_str,numtoa_u32,numtoa_u32_str); +impl_unsigned_numtoa_for!(u64,numtoa_uninit_u64,numtoa_uninit_u64_str,numtoa_u64,numtoa_u64_str); +impl_unsigned_numtoa_for!(u128,numtoa_uninit_u128,numtoa_uninit_u128_str,numtoa_u128,numtoa_u128_str); +impl_unsigned_numtoa_for!(usize,numtoa_uninit_usize,numtoa_uninit_usize_str,numtoa_usize,numtoa_usize_str); pub const fn numtoa_uninit_i8(mut num: i8, base: i8, string: &mut [MaybeUninit]) -> &[u8] { if cfg!(debug_assertions) { @@ -667,6 +677,11 @@ fn base_too_high() { numtoa_i32(36, 37, &mut [0u8; 100]); } +#[test] +fn str_convenience_uninit() { + assert_eq!("256123", numtoa_i32_str(256123_i32, 10, &mut [0u8; 20])); +} + #[test] fn str_convenience_core() { assert_eq!("256123", numtoa_i32_str(256123_i32, 10, &mut [0u8; 20])); From d0cf86462c72c9d6fd5e75f6aa5a2769030fde21 Mon Sep 17 00:00:00 2001 From: Vivek Revankar Date: Sun, 27 Jul 2025 22:53:22 -0700 Subject: [PATCH 08/12] don't forget u8 & i8 --- src/lib.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 305e4b5..b591ba8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -454,6 +454,10 @@ pub const fn numtoa_uninit_i8(mut num: i8, base: i8, string: &mut [MaybeUninit]) -> &str { + unsafe { str::from_utf8_unchecked(numtoa_uninit_i8(num, base, string)) } +} + pub const fn numtoa_i8(num: i8, base: i8, string: &mut [u8]) -> &[u8] { numtoa_uninit_i8(num, base, assume_mut_slice_uninit(string)) } @@ -512,6 +516,10 @@ pub const fn numtoa_uninit_u8(mut num: u8, base: u8, string: &mut [MaybeUninit]) -> &str { + unsafe { str::from_utf8_unchecked(numtoa_uninit_u8(num, base, string)) } +} + pub const fn numtoa_u8(num: u8, base: u8, string: &mut [u8]) -> &[u8] { numtoa_uninit_u8(num, base, assume_mut_slice_uninit(string)) } From 50f0520ee2389458d17bd3c33e08d6e1502dd005 Mon Sep 17 00:00:00 2001 From: Vivek Revankar Date: Sun, 27 Jul 2025 22:55:30 -0700 Subject: [PATCH 09/12] fix new test --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index b591ba8..522dfa6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -687,7 +687,7 @@ fn base_too_high() { #[test] fn str_convenience_uninit() { - assert_eq!("256123", numtoa_i32_str(256123_i32, 10, &mut [0u8; 20])); + assert_eq!("256123", numtoa_uninit_i32_str(256123_i32, 10, &mut [MaybeUninit::uninit(); 20])); } #[test] From b87d90bc74ba1104073f4731f7276c4d69405ff4 Mon Sep 17 00:00:00 2001 From: Vivek Revankar Date: Sun, 27 Jul 2025 23:20:17 -0700 Subject: [PATCH 10/12] remove inline --- src/lib.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 522dfa6..bc88785 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -180,13 +180,11 @@ impl Debug for AsciiNumber { } } -#[inline] const fn assume_mut_slice_uninit(thing: &mut [u8]) -> &mut [MaybeUninit] { // SAFETY: tbh i think this is always safe unsafe { core::mem::transmute::<&mut [u8], &mut [MaybeUninit]>(thing) } } -#[inline] const unsafe fn assume_slice_init(thing: &[MaybeUninit]) -> &[u8] { core::mem::transmute::<&[MaybeUninit], &[u8]>(thing) } From 103076975dbeac3aa42306016fda29038f3511cf Mon Sep 17 00:00:00 2001 From: Vivek Revankar Date: Sun, 27 Jul 2025 23:53:39 -0700 Subject: [PATCH 11/12] add numtoa_uninit --- src/lib.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index bc88785..085c0c5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -111,6 +111,8 @@ pub trait NumToA { /// ``` fn numtoa(self, base: Self, string: &mut [u8]) -> &[u8]; + fn numtoa_uninit(self, base: Self, string: &mut [MaybeUninit]) -> &[u8]; + /// Convenience method for quickly getting a string from the input's array buffer. fn numtoa_str(self, base: Self, buf: &mut [u8]) -> &str; } @@ -286,6 +288,9 @@ macro_rules! impl_unsigned_numtoa_for { } impl NumToA for $type_name { + fn numtoa_uninit(self, base: $type_name, string: &mut [MaybeUninit]) -> &[u8] { + $uninit_core_function_name(self, base, string) + } fn numtoa(self, base: $type_name, string: &mut [u8]) -> &[u8] { $core_function_name(self, base, string) } @@ -372,10 +377,12 @@ macro_rules! impl_signed_numtoa_for { } impl NumToA for $type_name { + fn numtoa_uninit(self, base: $type_name, string: &mut [MaybeUninit]) -> &[u8] { + $uninit_function_name(self, base, string) + } fn numtoa(self, base: $type_name, string: &mut [u8]) -> &[u8] { $core_function_name(self, base, string) } - fn numtoa_str(self, base: $type_name, buf: &mut [u8]) -> &str { $str_function_name(self, base, buf) } @@ -465,10 +472,12 @@ pub const fn numtoa_i8_str(num: i8, base: i8, string: &mut [u8]) -> &str { } impl NumToA for i8 { + fn numtoa_uninit(self, base: i8, string: &mut [MaybeUninit]) -> &[u8] { + numtoa_uninit_i8(self, base, string) + } fn numtoa(self, base: i8, string: &mut [u8]) -> &[u8] { numtoa_i8(self, base, string) } - fn numtoa_str(self, base: Self, buf: &mut [u8]) -> &str { numtoa_i8_str(self, base, buf) } @@ -527,6 +536,11 @@ pub const fn numtoa_u8_str(num: u8, base: u8, string: &mut [u8]) -> &str { } impl NumToA for u8 { + + fn numtoa_uninit(self, base: u8, string: &mut [MaybeUninit]) -> &[u8] { + numtoa_uninit_u8(self, base, string) + } + fn numtoa(self, base: u8, string: &mut [u8]) -> &[u8] { numtoa_u8(self, base, string) } From 63fb0ab02e785dadc70a7333abef49405eecfdff Mon Sep 17 00:00:00 2001 From: Vivek Revankar Date: Mon, 28 Jul 2025 00:00:25 -0700 Subject: [PATCH 12/12] add new tests --- src/lib.rs | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 085c0c5..b1ae4b0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -113,6 +113,8 @@ pub trait NumToA { fn numtoa_uninit(self, base: Self, string: &mut [MaybeUninit]) -> &[u8]; + fn numtoa_uninit_str(self, base: Self, string: &mut [MaybeUninit]) -> &str; + /// Convenience method for quickly getting a string from the input's array buffer. fn numtoa_str(self, base: Self, buf: &mut [u8]) -> &str; } @@ -291,6 +293,9 @@ macro_rules! impl_unsigned_numtoa_for { fn numtoa_uninit(self, base: $type_name, string: &mut [MaybeUninit]) -> &[u8] { $uninit_core_function_name(self, base, string) } + fn numtoa_uninit_str(self, base: $type_name, string: &mut [MaybeUninit]) -> &str { + $uninit_str_function_name(self, base, string) + } fn numtoa(self, base: $type_name, string: &mut [u8]) -> &[u8] { $core_function_name(self, base, string) } @@ -380,6 +385,9 @@ macro_rules! impl_signed_numtoa_for { fn numtoa_uninit(self, base: $type_name, string: &mut [MaybeUninit]) -> &[u8] { $uninit_function_name(self, base, string) } + fn numtoa_uninit_str(self, base: $type_name, string: &mut [MaybeUninit]) -> &str { + $uninit_str_function_name(self, base, string) + } fn numtoa(self, base: $type_name, string: &mut [u8]) -> &[u8] { $core_function_name(self, base, string) } @@ -475,6 +483,9 @@ impl NumToA for i8 { fn numtoa_uninit(self, base: i8, string: &mut [MaybeUninit]) -> &[u8] { numtoa_uninit_i8(self, base, string) } + fn numtoa_uninit_str(self, base: i8, string: &mut [MaybeUninit]) -> &str { + numtoa_uninit_i8_str(self, base, string) + } fn numtoa(self, base: i8, string: &mut [u8]) -> &[u8] { numtoa_i8(self, base, string) } @@ -536,15 +547,15 @@ pub const fn numtoa_u8_str(num: u8, base: u8, string: &mut [u8]) -> &str { } impl NumToA for u8 { - fn numtoa_uninit(self, base: u8, string: &mut [MaybeUninit]) -> &[u8] { numtoa_uninit_u8(self, base, string) } - + fn numtoa_uninit_str(self, base: u8, string: &mut [MaybeUninit]) -> &str { + numtoa_uninit_u8_str(self, base, string) + } fn numtoa(self, base: u8, string: &mut [u8]) -> &[u8] { numtoa_u8(self, base, string) } - fn numtoa_str(self, base: Self, buf: &mut [u8]) -> &str { numtoa_u8_str(self, base, buf) } @@ -712,6 +723,11 @@ fn str_convenience_core_u8() { assert_eq!("42", numtoa_u8_str(42u8, 10, &mut [b'X'; 20])); } +#[test] +fn str_convenience_core_u8_uninit() { + assert_eq!("42", numtoa_uninit_u8_str(42u8, 10, &mut [MaybeUninit::uninit(); 20])); +} + #[test] fn str_convenience_core_i8() { assert_eq!("42", numtoa_i8_str(42i8, 10, &mut [b'X'; 20])); @@ -722,6 +738,11 @@ fn str_convenience_trait() { assert_eq!("256123", 256123.numtoa_str(10, &mut [0u8; 20])); } +#[test] +fn str_convenience_trait_uninit() { + assert_eq!("256123", 256123.numtoa_uninit_str(10, &mut [MaybeUninit::uninit(); 20])); +} + #[test] fn str_convenience_base2() { assert_eq!("111110100001111011", base2::i32(256123).as_str());