nominals/
nominalstring.rs

1#![allow(unsafe_code)]
2
3#[cfg(feature = "alloc")]
4use alloc::string::String;
5use core::fmt::{Debug, Display};
6use core::mem::MaybeUninit;
7use core::ops::{Deref, DerefMut};
8
9/// A string that can contain most formatted nominals without a heap allocation.
10///
11/// This type can store up to 47 bytes on the stack before requiring a heap
12/// allocation. The total size of this structure is 64 bytes on a 64-bit
13/// architecture.
14#[derive(Debug)]
15#[cfg_attr(feature = "alloc", derive(Clone))]
16pub struct NominalString(MaybeInline);
17
18impl NominalString {
19    /// The maximum byte capacity this type can contain before allocating on the
20    /// heap.
21    ///
22    /// The capacity is 62 bytes.
23    ///
24    /// ```rust
25    /// use nominals::NominalString;
26    ///
27    /// assert_eq!(NominalString::INLINE_CAPACITY, 62);
28    ///
29    /// let max_inline = "a".repeat(NominalString::INLINE_CAPACITY);
30    /// let mut s = NominalString::from(&max_inline);
31    /// assert!(s.is_inline());
32    ///
33    /// s.try_push('a').unwrap();
34    /// assert!(!s.is_inline());
35    /// ```
36    pub const INLINE_CAPACITY: usize = MaybeInline::INLINE_CAPACITY as usize;
37
38    /// Returns an empty string.
39    #[must_use]
40    pub const fn new() -> Self {
41        Self(MaybeInline::new())
42    }
43
44    /// Returns an empty string, optimized for
45    /// [`try_push_front()`](Self::try_push_front) calls.
46    ///
47    /// The returned string has identical "observable" behavior to a string
48    /// returned from [`NominalString::new()`]. Calling the same series of push
49    /// operations on either kind of string will result in identical strings.
50    /// These constructors only affect the performance.
51    ///
52    /// While this string is on the stack, it fills its data starting at the end
53    /// of the inline buffer. This allows `try_push_front()` to operate in
54    /// `O(1)` time until it overflows on the stack.
55    #[must_use]
56    pub const fn new_reverse() -> Self {
57        Self(MaybeInline::reverse())
58    }
59
60    /// Pushes `ch` to the end of the string.
61    ///
62    /// # Errors
63    ///
64    /// Returns [`OutOfMemoryError`] if no additional space is available and the
65    /// `alloc` feature is disabled.
66    pub fn try_push(&mut self, ch: char) -> Result<(), OutOfMemoryError> {
67        self.0.push(ch)
68    }
69
70    /// Pushes `str` to the end of the string.
71    ///
72    /// # Errors
73    ///
74    /// Returns [`OutOfMemoryError`] if no additional space is available and the
75    /// `alloc` feature is disabled.
76    pub fn try_push_str(&mut self, str: &str) -> Result<(), OutOfMemoryError> {
77        self.0.push_str(str)
78    }
79
80    /// Pushes `ch` to the start of the string.
81    ///
82    /// # Errors
83    ///
84    /// Returns [`OutOfMemoryError`] if no additional space is available and the
85    /// `alloc` feature is disabled.
86    pub fn try_push_front(&mut self, ch: char) -> Result<(), OutOfMemoryError> {
87        self.0.push_front(ch)
88    }
89
90    /// Returns true if this string is currently stored on the stack.
91    #[must_use]
92    pub fn is_inline(&self) -> bool {
93        matches!(self.0, MaybeInline::Inline(_))
94    }
95
96    /// Returns the heap-allocated [`String`] inside `self`, if `self` is
97    /// heap allocated.
98    ///
99    /// # Errors
100    ///
101    /// If `self` is inline, `Err(self)` will be returned.
102    #[cfg(feature = "alloc")]
103    pub fn try_into_string(self) -> Result<String, Self> {
104        match self.0 {
105            MaybeInline::Inline(inline) => Err(Self(MaybeInline::Inline(inline))),
106            MaybeInline::Heap(string) => Ok(string),
107        }
108    }
109}
110
111#[cfg(feature = "alloc")]
112impl From<NominalString> for String {
113    fn from(s: NominalString) -> Self {
114        match s.try_into_string() {
115            Ok(string) => string,
116            Err(s) => String::from(&*s),
117        }
118    }
119}
120
121impl Display for NominalString {
122    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
123        Display::fmt(&**self, f)
124    }
125}
126
127impl Default for NominalString {
128    fn default() -> Self {
129        Self::new()
130    }
131}
132
133#[cfg(feature = "alloc")]
134impl From<String> for NominalString {
135    fn from(value: String) -> Self {
136        NominalString(MaybeInline::Heap(value))
137    }
138}
139
140#[cfg(feature = "alloc")]
141impl From<&'_ String> for NominalString {
142    fn from(value: &'_ String) -> Self {
143        Self::from(value.as_str())
144    }
145}
146
147impl From<&'_ str> for NominalString {
148    fn from(value: &'_ str) -> Self {
149        match u8::try_from(value.len()) {
150            Ok(value_len) if value_len <= MaybeInline::INLINE_CAPACITY => {
151                let mut bytes = [MaybeUninit::uninit(); MaybeInline::INLINE_CAPACITY as usize];
152
153                for (dest, src) in bytes[0..value.len()].iter_mut().zip(value.as_bytes()) {
154                    dest.write(*src);
155                }
156                NominalString(MaybeInline::Inline(InlineString {
157                    length: value_len,
158                    bytes,
159                }))
160            }
161            _ => {
162                #[cfg(feature = "alloc")]
163                {
164                    Self::from(String::from(value))
165                }
166
167                #[cfg(not(feature = "alloc"))]
168                {
169                    panic!("string too long to store on stack");
170                }
171            }
172        }
173    }
174}
175
176impl From<char> for NominalString {
177    fn from(ch: char) -> Self {
178        let mut s = Self::new();
179        s.try_push(ch).expect("at least one char fits inline");
180        s
181    }
182}
183
184impl Deref for NominalString {
185    type Target = str;
186
187    fn deref(&self) -> &Self::Target {
188        self.0.as_str()
189    }
190}
191
192impl DerefMut for NominalString {
193    fn deref_mut(&mut self) -> &mut Self::Target {
194        self.0.as_str_mut()
195    }
196}
197
198impl Eq for NominalString {}
199
200impl PartialEq<str> for NominalString {
201    fn eq(&self, other: &str) -> bool {
202        &**self == other
203    }
204}
205
206impl PartialEq for NominalString {
207    fn eq(&self, other: &Self) -> bool {
208        self == &**other
209    }
210}
211
212impl PartialEq<&'_ str> for NominalString {
213    fn eq(&self, other: &&'_ str) -> bool {
214        self == *other
215    }
216}
217#[cfg(feature = "alloc")]
218impl PartialEq<String> for NominalString {
219    fn eq(&self, other: &String) -> bool {
220        self == other.as_str()
221    }
222}
223
224impl PartialOrd<str> for NominalString {
225    fn partial_cmp(&self, other: &str) -> Option<core::cmp::Ordering> {
226        Some((**self).cmp(other))
227    }
228}
229
230#[cfg(feature = "alloc")]
231impl PartialOrd<String> for NominalString {
232    fn partial_cmp(&self, other: &String) -> Option<core::cmp::Ordering> {
233        self.partial_cmp(other.as_str())
234    }
235}
236
237impl Ord for NominalString {
238    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
239        (**self).cmp(&**other)
240    }
241}
242
243impl PartialOrd for NominalString {
244    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
245        Some(self.cmp(other))
246    }
247}
248
249#[derive(Clone)]
250enum MaybeInline {
251    Inline(InlineString),
252    #[cfg(feature = "alloc")]
253    Heap(String),
254}
255
256impl Debug for MaybeInline {
257    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
258        match self {
259            Self::Inline(_) => f.debug_tuple("Inline").field(&self.as_str()).finish(),
260            #[cfg(feature = "alloc")]
261            Self::Heap(_) => f.debug_tuple("Heap").field(&self.as_str()).finish(),
262        }
263    }
264}
265
266#[derive(Clone, Copy)]
267struct InlineString {
268    length: u8,
269    bytes: [MaybeUninit<u8>; MaybeInline::INLINE_CAPACITY as usize],
270}
271
272impl InlineString {
273    const fn len(&self) -> u8 {
274        self.length & 0x7F
275    }
276
277    const fn len_usize(&self) -> usize {
278        self.len() as usize
279    }
280
281    const fn is_reverse(&self) -> bool {
282        self.length & 0x80 != 0
283    }
284
285    fn data_offset(&self) -> usize {
286        if self.is_reverse() {
287            usize::from(MaybeInline::INLINE_CAPACITY) - self.len_usize()
288        } else {
289            0
290        }
291    }
292
293    fn as_bytes(&self) -> &[u8] {
294        // SAFETY: This function only returns access to the bytes that have
295        // been initialized, indicated by `self.length`.
296        unsafe {
297            core::slice::from_raw_parts(
298                self.bytes.as_ptr().cast::<u8>().add(self.data_offset()),
299                self.len_usize(),
300            )
301        }
302    }
303
304    fn as_bytes_mut(&mut self) -> &mut [u8] {
305        // SAFETY: This function only returns access to the bytes that have been
306        // initialized, indicated by `self.length`. Because this borrow uses
307        // `self`'s lifetime, it ensures only one exclusive reference can be
308        // created.
309        unsafe {
310            core::slice::from_raw_parts_mut(
311                self.bytes.as_mut_ptr().cast::<u8>().add(self.data_offset()),
312                self.len_usize(),
313            )
314        }
315    }
316
317    fn as_str(&self) -> &str {
318        // SAFETY: This type only performs unicode-safe operations, and ensures
319        // that the bytes through `length` are valid UTF-8. `as_bytes` only
320        // returns the written-to portions of the string.
321        unsafe { core::str::from_utf8_unchecked(self.as_bytes()) }
322    }
323
324    fn as_mut_str(&mut self) -> &mut str {
325        // SAFETY: This type only performs unicode-safe operations, and ensures
326        // that the bytes through `length` are valid UTF-8. `as_bytes_mut` only
327        // returns the written-to portions of the string.
328        unsafe { core::str::from_utf8_unchecked_mut(self.as_bytes_mut()) }
329    }
330
331    fn set_length(&mut self, new_length: u8) {
332        self.length = self.length & 0x80 | new_length;
333    }
334
335    fn push(&mut self, ch: char, char_len: u8, inline_len: u8, new_length: u8) {
336        let mut utf8_bytes = [0; 4];
337        ch.encode_utf8(&mut utf8_bytes);
338        if self.is_reverse() {
339            // Copy to make room at the end
340            let current_offset = self.data_offset();
341            let size_of_bytes = usize::from(MaybeInline::INLINE_CAPACITY);
342            self.bytes
343                .copy_within(current_offset.., size_of_bytes - usize::from(new_length));
344
345            for (dest, src) in self.bytes[size_of_bytes - usize::from(char_len)..]
346                .iter_mut()
347                .zip(&utf8_bytes[0..usize::from(char_len)])
348            {
349                dest.write(*src);
350            }
351        } else {
352            for (dest, src) in self.bytes[usize::from(inline_len)..usize::from(new_length)]
353                .iter_mut()
354                .zip(&utf8_bytes[0..usize::from(char_len)])
355            {
356                dest.write(*src);
357            }
358        }
359        self.set_length(new_length);
360    }
361
362    fn push_str(&mut self, str: &str, new_length: u8) {
363        if self.is_reverse() {
364            // Copy to make room at the end
365            let current_offset = self.data_offset();
366            let size_of_bytes = usize::from(MaybeInline::INLINE_CAPACITY);
367            self.bytes
368                .copy_within(current_offset.., size_of_bytes - usize::from(new_length));
369
370            for (dest, src) in self.bytes[size_of_bytes - str.len()..]
371                .iter_mut()
372                .zip(str.as_bytes())
373            {
374                dest.write(*src);
375            }
376        } else {
377            // SAFETY: This snippet copies initialized data into
378            // uninitialized locations, causing them to become
379            // initialized. No read access is performed on
380            // uninitialized data.
381            unsafe {
382                self.bytes
383                    .as_mut_ptr()
384                    .cast::<u8>()
385                    .add(self.len_usize())
386                    .copy_from(str.as_bytes().as_ptr(), str.len());
387            };
388        }
389
390        self.set_length(new_length);
391    }
392
393    fn push_front(&mut self, ch: char, char_len: u8, inline_len: u8, new_length: u8) {
394        if self.is_reverse() {
395            let mut utf8_bytes = [0; 4];
396            ch.encode_utf8(&mut utf8_bytes);
397
398            let start = MaybeInline::INLINE_CAPACITY - new_length;
399            let end = MaybeInline::INLINE_CAPACITY - inline_len;
400            for (dest, src) in self.bytes[usize::from(start)..usize::from(end)]
401                .iter_mut()
402                .zip(&utf8_bytes[0..usize::from(char_len)])
403            {
404                dest.write(*src);
405            }
406            self.set_length(new_length);
407        } else {
408            self.bytes
409                .copy_within(0..usize::from(inline_len), usize::from(char_len));
410            self.set_length(new_length);
411
412            ch.encode_utf8(self.as_bytes_mut());
413        }
414    }
415}
416
417impl MaybeInline {
418    const INLINE_CAPACITY: u8 = 62;
419
420    const fn new() -> MaybeInline {
421        MaybeInline::Inline(InlineString {
422            length: 0,
423            bytes: [MaybeUninit::uninit(); Self::INLINE_CAPACITY as usize],
424        })
425    }
426
427    const fn reverse() -> MaybeInline {
428        MaybeInline::Inline(InlineString {
429            length: 0x80,
430            bytes: [MaybeUninit::uninit(); Self::INLINE_CAPACITY as usize],
431        })
432    }
433
434    #[allow(clippy::unnecessary_wraps)]
435    fn push(&mut self, ch: char) -> Result<(), OutOfMemoryError> {
436        match self {
437            MaybeInline::Inline(inline) => {
438                let char_len = u8::try_from(ch.len_utf8()).expect("4 < u8::MAX");
439                let inline_len = inline.len();
440                let new_length = inline_len + char_len;
441                if new_length <= Self::INLINE_CAPACITY {
442                    inline.push(ch, char_len, inline_len, new_length);
443                } else {
444                    #[cfg(feature = "alloc")]
445                    {
446                        let mut string = String::with_capacity(usize::from(new_length));
447                        string.push_str(inline.as_str());
448                        string.push(ch);
449                        *self = MaybeInline::Heap(string);
450                    }
451                    #[cfg(not(feature = "alloc"))]
452                    {
453                        return Err(OutOfMemoryError);
454                    }
455                }
456            }
457            #[cfg(feature = "alloc")]
458            MaybeInline::Heap(s) => s.push(ch),
459        }
460        Ok(())
461    }
462
463    #[allow(clippy::unnecessary_wraps)]
464    fn push_str(&mut self, str: &str) -> Result<(), OutOfMemoryError> {
465        match self {
466            MaybeInline::Inline(inline) => {
467                if let Ok(insert_len) = u8::try_from(str.len()) {
468                    let new_length = inline.len().checked_add(insert_len);
469                    match new_length {
470                        Some(new_length) if new_length <= Self::INLINE_CAPACITY => {
471                            inline.push_str(str, new_length);
472                            return Ok(());
473                        }
474                        _ => {}
475                    }
476                }
477
478                #[cfg(feature = "alloc")]
479                {
480                    let new_length = inline.len_usize() + str.len();
481                    let mut string = String::with_capacity(new_length);
482                    string.push_str(inline.as_str());
483                    string.push_str(str);
484                    *self = MaybeInline::Heap(string);
485                    Ok(())
486                }
487                #[cfg(not(feature = "alloc"))]
488                {
489                    Err(OutOfMemoryError)
490                }
491            }
492            #[cfg(feature = "alloc")]
493            MaybeInline::Heap(s) => {
494                s.push_str(str);
495                Ok(())
496            }
497        }
498    }
499
500    #[allow(clippy::unnecessary_wraps)]
501    fn push_front(&mut self, ch: char) -> Result<(), OutOfMemoryError> {
502        match self {
503            MaybeInline::Inline(inline) => {
504                let char_len = u8::try_from(ch.len_utf8()).expect("4 < u8::MAX");
505                let inline_len = inline.len();
506                let new_length = inline_len + char_len;
507                if new_length <= Self::INLINE_CAPACITY {
508                    inline.push_front(ch, char_len, inline_len, new_length);
509                } else {
510                    #[cfg(feature = "alloc")]
511                    {
512                        let mut string = String::with_capacity(usize::from(new_length));
513                        string.push(ch);
514                        string.push_str(inline.as_str());
515                        *self = MaybeInline::Heap(string);
516                    }
517                    #[cfg(not(feature = "alloc"))]
518                    {
519                        return Err(OutOfMemoryError);
520                    }
521                }
522            }
523            #[cfg(feature = "alloc")]
524            MaybeInline::Heap(s) => s.insert(0, ch),
525        }
526        Ok(())
527    }
528
529    fn as_str(&self) -> &str {
530        match self {
531            MaybeInline::Inline(s) => s.as_str(),
532            #[cfg(feature = "alloc")]
533            MaybeInline::Heap(s) => s,
534        }
535    }
536
537    fn as_str_mut(&mut self) -> &mut str {
538        match self {
539            MaybeInline::Inline(s) => s.as_mut_str(),
540            #[cfg(feature = "alloc")]
541            MaybeInline::Heap(s) => s.as_mut_str(),
542        }
543    }
544}
545
546/// Was unable to allocate additional memory.
547#[derive(Debug, Clone, Copy, Eq, PartialEq)]
548pub struct OutOfMemoryError;
549
550#[test]
551fn preconditions() {
552    // The push[_front]() functions rely on the fact that at the time of writing
553    // its code, INLINE_CAPACITY was a fixed value. This guaranteees that adding
554    // the length of a utf-8 encoded char will never overflow. If we change INLINE_CAPACITY to be something that could be 251 or larger, we shou
555    assert!(MaybeInline::INLINE_CAPACITY.checked_add(4).is_some());
556}
557
558#[test]
559fn forward_ops() {
560    let mut s = NominalString::new();
561    s.try_push('a').unwrap();
562    assert_eq!(s, "a");
563    s.try_push_str("bc").unwrap();
564    assert_eq!(s, "abc");
565    s.try_push_front('_').unwrap();
566    assert_eq!(s, "_abc");
567
568    let init = "a".repeat(usize::from(MaybeInline::INLINE_CAPACITY));
569    let check_after = init.clone() + "b";
570    let check_before = String::from("b") + &init;
571
572    let borderline = NominalString::from(&init);
573    let mut s = borderline.clone();
574    assert!(s.is_inline());
575    s.try_push('b').unwrap();
576    assert!(!s.is_inline());
577    assert_eq!(s, check_after);
578
579    let borderline = NominalString::from(&init);
580    let mut s = borderline.clone();
581    assert!(s.is_inline());
582    s.try_push_str("b").unwrap();
583    assert!(!s.is_inline());
584    assert_eq!(s, check_after);
585
586    let borderline = NominalString::from(&init);
587    let mut s = borderline.clone();
588    assert!(s.is_inline());
589    s.try_push_front('b').unwrap();
590    assert!(!s.is_inline());
591    assert_eq!(s, check_before);
592}
593
594#[test]
595fn reverse_ops() {
596    let mut s = NominalString::new_reverse();
597    s.try_push('a').unwrap();
598    assert_eq!(s, "a");
599    s.try_push_str("bc").unwrap();
600    assert_eq!(s, "abc");
601    s.try_push_front('_').unwrap();
602    assert_eq!(s, "_abc");
603
604    let init = "a".repeat(usize::from(MaybeInline::INLINE_CAPACITY));
605    let check_after = init.clone() + "b";
606    let check_before = String::from("b") + &init;
607
608    let borderline = NominalString::from(&init);
609    let mut s = borderline.clone();
610    assert!(s.is_inline());
611    s.try_push('b').unwrap();
612    assert!(!s.is_inline());
613    assert_eq!(s, check_after);
614
615    let borderline = NominalString::from(&init);
616    let mut s = borderline.clone();
617    assert!(s.is_inline());
618    s.try_push_str("b").unwrap();
619    assert!(!s.is_inline());
620    assert_eq!(s, check_after);
621
622    let borderline = NominalString::from(&init);
623    let mut s = borderline.clone();
624    assert!(s.is_inline());
625    s.try_push_front('b').unwrap();
626    assert!(!s.is_inline());
627    assert_eq!(s, check_before);
628}