Skip to main content

radixdb_core/
smart_string.rs

1// Copyright 2026 RadixDB Contributors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! SmartString - A 16-byte string type with inline small string optimization
16//!
17//! This is a compact string representation that enables 16-byte Value enums
18//! through niche optimization. The design:
19//!
20//! - **Inline**: strings ≤15 bytes stored inline (no heap allocation)
21//! - **Heap**: strings >15 bytes stored as `Arc<str>` for O(1) clone
22//!
23//! ## Memory Layout (16 bytes, 8-byte aligned)
24//!
25//! ```text
26//! Inline (≤15 bytes):
27//!   [tag: 1 byte] [data: 15 bytes]
28//!   tag = 0-15 (encodes length)
29//!
30//! Heap (>15 bytes) on 64-bit:
31//!   [tag: 1 byte] [length: 7 bytes] [Arc<str> data pointer: 8 bytes]
32//!   tag = 16 (heap marker), pointer at data[7..15]
33//!
34//! Heap (>15 bytes) on 32-bit:
35//!   [tag: 1 byte] [length: 11 bytes] [Arc<str> data pointer: 4 bytes]
36//!   tag = 16 (heap marker), pointer at data[11..15]
37//! ```
38//!
39//! ## Niche Optimization
40//!
41//! StringTag only uses values 0-16, leaving values 17-255 as "niches".
42//! The Rust compiler uses these niches to store the Value enum's discriminant,
43//! enabling a 16-byte Value enum without explicit discriminant storage.
44
45use std::borrow::Borrow;
46use std::cmp::Ordering;
47use std::fmt;
48use std::hash::{Hash, Hasher};
49use std::mem;
50use std::ops::Deref;
51use std::ptr;
52use std::sync::Arc;
53
54/// Maximum inline string length (15 bytes)
55pub const MAX_INLINE_LEN: usize = 15;
56
57/// Platform-specific pointer layout for heap strings.
58/// On 64-bit: 8-byte pointer stored at data[7..15]
59/// On 32-bit: 4-byte pointer stored at data[11..15]
60#[cfg(target_pointer_width = "64")]
61mod ptr_layout {
62    /// Offset into data array where pointer starts
63    pub const OFFSET: usize = 7;
64    /// Pointer size in bytes
65    pub const SIZE: usize = 8;
66    /// Type alias for pointer bytes array
67    pub type Bytes = [u8; 8];
68}
69
70#[cfg(target_pointer_width = "32")]
71mod ptr_layout {
72    /// Offset into data array where pointer starts
73    pub const OFFSET: usize = 11;
74    /// Pointer size in bytes
75    pub const SIZE: usize = 4;
76    /// Type alias for pointer bytes array
77    pub type Bytes = [u8; 4];
78}
79
80/// String tag that encodes storage mode and inline length.
81///
82/// Values 0-16 are valid, 17-255 are niches for Value enum discriminant.
83#[repr(u8)]
84#[derive(Clone, Copy, Debug, PartialEq, Eq)]
85pub enum StringTag {
86    // Inline lengths 0-15
87    Inline0 = 0,
88    Inline1 = 1,
89    Inline2 = 2,
90    Inline3 = 3,
91    Inline4 = 4,
92    Inline5 = 5,
93    Inline6 = 6,
94    Inline7 = 7,
95    Inline8 = 8,
96    Inline9 = 9,
97    Inline10 = 10,
98    Inline11 = 11,
99    Inline12 = 12,
100    Inline13 = 13,
101    Inline14 = 14,
102    Inline15 = 15,
103    // Heap storage
104    Heap = 16,
105    // Values 17-255: NICHES for Value enum discriminant (239 niches)
106}
107
108impl StringTag {
109    /// Returns true if the string is stored inline
110    #[inline]
111    pub const fn is_inline(self) -> bool {
112        (self as u8) <= 15
113    }
114
115    /// Returns true if the string is stored on the heap
116    #[inline]
117    pub const fn is_heap(self) -> bool {
118        (self as u8) == 16
119    }
120
121    /// Get the inline length if this is an inline tag
122    #[inline]
123    pub const fn inline_len(self) -> Option<usize> {
124        let v = self as u8;
125        if v <= 15 {
126            Some(v as usize)
127        } else {
128            None
129        }
130    }
131
132    /// Create an inline tag for the given length (0-15)
133    #[inline]
134    const fn inline(len: usize) -> Self {
135        debug_assert!(len <= 15);
136        // SAFETY: len is 0-15, which maps to valid Inline variants
137        unsafe { std::mem::transmute(len as u8) }
138    }
139}
140
141/// A 16-byte string with inline SSO (Small String Optimization).
142///
143/// This compact representation enables 16-byte Value enums through niche optimization.
144/// The StringTag provides 239 niche values (17-255) that Rust uses to store Value's
145/// discriminant without additional memory.
146#[repr(C, align(8))]
147pub struct SmartString {
148    /// Tag byte: encodes storage mode (inline length or heap)
149    tag: StringTag,
150    /// Data bytes: inline string data OR (length + `Arc<str>` data pointer)
151    /// - 64-bit: 7-byte length + 8-byte pointer at data[7..15]
152    /// - 32-bit: 11-byte length area + 4-byte pointer at data[11..15]
153    data: [u8; 15],
154}
155
156impl SmartString {
157    /// Create a new SmartString from a string slice.
158    #[inline]
159    pub fn new(s: &str) -> Self {
160        let len = s.len();
161        if len <= MAX_INLINE_LEN {
162            let mut data = [0u8; 15];
163            data[..len].copy_from_slice(s.as_bytes());
164            SmartString {
165                tag: StringTag::inline(len),
166                data,
167            }
168        } else {
169            Self::new_heap(s.to_string())
170        }
171    }
172
173    /// Create a SmartString from an owned String (heap case)
174    #[inline]
175    fn new_heap(s: String) -> Self {
176        Self::new_heap_arc(Arc::<str>::from(s))
177    }
178
179    #[inline]
180    fn new_heap_arc(arc: Arc<str>) -> Self {
181        let len = arc.len();
182        let mut data = [0u8; 15];
183        Self::encode_heap_len(&mut data, len);
184        let ptr = Arc::into_raw(arc) as *const u8 as usize;
185        let end = ptr_layout::OFFSET + ptr_layout::SIZE;
186        data[ptr_layout::OFFSET..end].copy_from_slice(&ptr.to_ne_bytes());
187        SmartString {
188            tag: StringTag::Heap,
189            data,
190        }
191    }
192
193    #[inline]
194    fn encode_heap_len(data: &mut [u8; 15], len: usize) {
195        let bytes = len.to_le_bytes();
196        let stored = ptr_layout::OFFSET.min(bytes.len());
197        assert!(
198            bytes[stored..].iter().all(|byte| *byte == 0),
199            "SmartString heap length exceeds compact representation"
200        );
201        data[..stored].copy_from_slice(&bytes[..stored]);
202    }
203
204    #[inline(always)]
205    fn heap_len(&self) -> usize {
206        let mut bytes = [0u8; mem::size_of::<usize>()];
207        let stored = ptr_layout::OFFSET.min(bytes.len());
208        bytes[..stored].copy_from_slice(&self.data[..stored]);
209        usize::from_le_bytes(bytes)
210    }
211
212    /// Create from a string slice - for API compatibility
213    #[inline]
214    pub fn const_new(s: &str) -> Self {
215        Self::new(s)
216    }
217
218    /// Create from owned String
219    #[inline]
220    pub fn from_string(s: String) -> Self {
221        let len = s.len();
222        if len <= MAX_INLINE_LEN {
223            let mut data = [0u8; 15];
224            data[..len].copy_from_slice(s.as_bytes());
225            SmartString {
226                tag: StringTag::inline(len),
227                data,
228            }
229        } else {
230            Self::new_heap(s)
231        }
232    }
233
234    /// Create from owned String - uses Arc for heap (for values that will be cloned)
235    #[inline]
236    pub fn from_string_shared(s: String) -> Self {
237        // This is the same as from_string since we always use Arc for heap
238        Self::from_string(s)
239    }
240
241    /// Returns a string slice of the contents.
242    #[inline(always)]
243    pub fn as_str(&self) -> &str {
244        if let Some(len) = self.tag.inline_len() {
245            // Inline case: data contains the string bytes
246            // SAFETY: SmartString only stores valid UTF-8
247            unsafe { std::str::from_utf8_unchecked(&self.data[..len]) }
248        } else {
249            // SAFETY: Heap values store the data pointer and length from a
250            // live Arc<str>; get_arc_ptr reconstructs the original fat pointer.
251            unsafe { &*self.get_arc_ptr() }
252        }
253    }
254
255    /// Returns the length of the string in bytes.
256    #[inline(always)]
257    pub fn len(&self) -> usize {
258        if let Some(len) = self.tag.inline_len() {
259            len
260        } else {
261            self.as_str().len()
262        }
263    }
264
265    #[inline]
266    pub fn is_empty(&self) -> bool {
267        self.len() == 0
268    }
269
270    #[inline]
271    pub fn is_inline(&self) -> bool {
272        self.tag.is_inline()
273    }
274
275    #[inline]
276    pub fn is_heap(&self) -> bool {
277        self.tag.is_heap()
278    }
279
280    /// Reconstruct the raw Arc<str> pointer for heap strings.
281    #[inline]
282    fn get_arc_ptr(&self) -> *const str {
283        debug_assert!(self.tag.is_heap());
284        let end = ptr_layout::OFFSET + ptr_layout::SIZE;
285        let ptr_bytes: ptr_layout::Bytes = self.data[ptr_layout::OFFSET..end].try_into().unwrap();
286        let data_ptr = usize::from_ne_bytes(ptr_bytes) as *const u8;
287        ptr::slice_from_raw_parts(data_ptr, self.heap_len()) as *const str
288    }
289
290    /// Appends a character to the string.
291    ///
292    /// # Performance Warning
293    ///
294    /// This operation may require reallocation. For string building,
295    /// use `String` first then convert with `SmartString::from_string()`.
296    #[inline]
297    pub fn push(&mut self, ch: char) {
298        let ch_len = ch.len_utf8();
299
300        if let Some(current_len) = self.tag.inline_len() {
301            if current_len + ch_len <= MAX_INLINE_LEN {
302                // Still fits inline
303                ch.encode_utf8(&mut self.data[current_len..]);
304                self.tag = StringTag::inline(current_len + ch_len);
305                return;
306            }
307        }
308
309        // Need to go to heap or already on heap
310        let mut s = self.as_str().to_string();
311        s.push(ch);
312        *self = Self::from_string(s);
313    }
314
315    /// Appends a string slice to the string.
316    #[inline]
317    pub fn push_str(&mut self, string: &str) {
318        if string.is_empty() {
319            return;
320        }
321
322        let add_len = string.len();
323
324        if let Some(current_len) = self.tag.inline_len() {
325            if current_len + add_len <= MAX_INLINE_LEN {
326                // Still fits inline
327                self.data[current_len..current_len + add_len].copy_from_slice(string.as_bytes());
328                self.tag = StringTag::inline(current_len + add_len);
329                return;
330            }
331        }
332
333        // Need to go to heap or already on heap
334        let mut s = self.as_str().to_string();
335        s.push_str(string);
336        *self = Self::from_string(s);
337    }
338
339    #[inline]
340    pub fn to_lowercase(&self) -> SmartString {
341        SmartString::from_string(self.as_str().to_lowercase())
342    }
343
344    #[inline]
345    pub fn to_uppercase(&self) -> SmartString {
346        SmartString::from_string(self.as_str().to_uppercase())
347    }
348
349    #[inline]
350    pub fn is_ascii(&self) -> bool {
351        self.as_str().is_ascii()
352    }
353
354    #[inline]
355    pub fn make_ascii_uppercase(&mut self) {
356        if let Some(len) = self.tag.inline_len() {
357            self.data[..len].make_ascii_uppercase();
358        } else {
359            let mut s = self.as_str().to_string();
360            s.make_ascii_uppercase();
361            *self = Self::from_string(s);
362        }
363    }
364
365    #[inline]
366    pub fn make_ascii_lowercase(&mut self) {
367        if let Some(len) = self.tag.inline_len() {
368            self.data[..len].make_ascii_lowercase();
369        } else {
370            let mut s = self.as_str().to_string();
371            s.make_ascii_lowercase();
372            *self = Self::from_string(s);
373        }
374    }
375
376    #[inline]
377    pub fn into_string(self) -> String {
378        if self.tag.is_inline() {
379            self.as_str().to_owned()
380        } else {
381            // Take ownership of the Arc without incrementing refcount
382            let ptr = self.get_arc_ptr();
383            std::mem::forget(self); // Prevent Drop from decrementing
384                                    // SAFETY: ptr is the original Arc<str> raw pointer, and we've
385                                    // prevented self's Drop from running, so ownership transfers here.
386            let arc = unsafe { Arc::from_raw(ptr) };
387            arc.as_ref().to_owned()
388        }
389    }
390
391    #[inline]
392    pub fn concat(a: &str, b: &str) -> SmartString {
393        let total_len = a.len() + b.len();
394        if total_len <= MAX_INLINE_LEN {
395            let mut data = [0u8; 15];
396            data[..a.len()].copy_from_slice(a.as_bytes());
397            data[a.len()..total_len].copy_from_slice(b.as_bytes());
398            SmartString {
399                tag: StringTag::inline(total_len),
400                data,
401            }
402        } else {
403            let mut s = String::with_capacity(total_len);
404            s.push_str(a);
405            s.push_str(b);
406            SmartString::from_string(s)
407        }
408    }
409
410    /// Build an inline SmartString by writing bytes directly.
411    ///
412    /// # Safety
413    ///
414    /// The caller must ensure that the bytes written by `builder` form valid UTF-8.
415    #[inline]
416    pub unsafe fn build_inline<F>(total_len: usize, builder: F) -> Option<SmartString>
417    where
418        F: FnOnce(&mut [u8]),
419    {
420        if total_len <= MAX_INLINE_LEN {
421            let mut data = [0u8; 15];
422            builder(&mut data[..total_len]);
423            Some(SmartString {
424                tag: StringTag::inline(total_len),
425                data,
426            })
427        } else {
428            None
429        }
430    }
431}
432
433impl Clone for SmartString {
434    #[inline]
435    fn clone(&self) -> Self {
436        if self.tag.is_heap() {
437            // Heap: increment Arc refcount to balance the new owner's Drop
438            // SAFETY: get_arc_ptr() reconstructs the original Arc<str> pointer
439            unsafe {
440                Arc::increment_strong_count(self.get_arc_ptr());
441            }
442        }
443        // Data bytes already contain the correct content (inline bytes or pointer)
444        SmartString {
445            tag: self.tag,
446            data: self.data,
447        }
448    }
449}
450
451impl Drop for SmartString {
452    #[inline]
453    fn drop(&mut self) {
454        if self.tag.is_heap() {
455            // Heap: decrement Arc refcount
456            let ptr = self.get_arc_ptr();
457            // SAFETY: We only store valid Arc<str> pointers
458            unsafe {
459                Arc::from_raw(ptr);
460            }
461        }
462    }
463}
464
465impl Default for SmartString {
466    #[inline]
467    fn default() -> Self {
468        SmartString {
469            tag: StringTag::Inline0,
470            data: [0u8; 15],
471        }
472    }
473}
474
475impl Deref for SmartString {
476    type Target = str;
477    #[inline]
478    fn deref(&self) -> &str {
479        self.as_str()
480    }
481}
482
483impl AsRef<str> for SmartString {
484    #[inline]
485    fn as_ref(&self) -> &str {
486        self.as_str()
487    }
488}
489
490impl Borrow<str> for SmartString {
491    #[inline]
492    fn borrow(&self) -> &str {
493        self.as_str()
494    }
495}
496
497impl From<&str> for SmartString {
498    #[inline]
499    fn from(s: &str) -> Self {
500        SmartString::new(s)
501    }
502}
503
504impl From<String> for SmartString {
505    #[inline]
506    fn from(s: String) -> Self {
507        SmartString::from_string(s)
508    }
509}
510
511impl From<Arc<str>> for SmartString {
512    #[inline]
513    fn from(arc: Arc<str>) -> Self {
514        if arc.len() <= MAX_INLINE_LEN {
515            SmartString::new(&arc)
516        } else {
517            SmartString::new_heap_arc(arc)
518        }
519    }
520}
521
522impl fmt::Debug for SmartString {
523    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
524        fmt::Debug::fmt(self.as_str(), f)
525    }
526}
527
528impl fmt::Display for SmartString {
529    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
530        fmt::Display::fmt(self.as_str(), f)
531    }
532}
533
534impl PartialEq for SmartString {
535    #[inline]
536    fn eq(&self, other: &Self) -> bool {
537        self.as_str() == other.as_str()
538    }
539}
540
541impl Eq for SmartString {}
542
543impl PartialOrd for SmartString {
544    #[inline]
545    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
546        Some(self.cmp(other))
547    }
548}
549
550impl Ord for SmartString {
551    #[inline]
552    fn cmp(&self, other: &Self) -> Ordering {
553        self.as_str().cmp(other.as_str())
554    }
555}
556
557impl Hash for SmartString {
558    #[inline]
559    fn hash<H: Hasher>(&self, state: &mut H) {
560        // IMPORTANT: Must delegate to str::hash to maintain Hash-Borrow contract.
561        // SmartString implements Borrow<str>, so its hash must match str's hash
562        // for the same content.
563        self.as_str().hash(state);
564    }
565}
566
567impl PartialEq<str> for SmartString {
568    #[inline]
569    fn eq(&self, other: &str) -> bool {
570        self.as_str() == other
571    }
572}
573
574impl PartialEq<&str> for SmartString {
575    #[inline]
576    fn eq(&self, other: &&str) -> bool {
577        self.as_str() == *other
578    }
579}
580
581impl PartialEq<String> for SmartString {
582    #[inline]
583    fn eq(&self, other: &String) -> bool {
584        self.as_str() == other
585    }
586}
587
588impl PartialEq<SmartString> for str {
589    #[inline]
590    fn eq(&self, other: &SmartString) -> bool {
591        self == other.as_str()
592    }
593}
594
595impl PartialEq<SmartString> for &str {
596    #[inline]
597    fn eq(&self, other: &SmartString) -> bool {
598        *self == other.as_str()
599    }
600}
601
602impl From<SmartString> for String {
603    #[inline]
604    fn from(s: SmartString) -> Self {
605        s.into_string()
606    }
607}
608
609impl PartialEq<SmartString> for String {
610    #[inline]
611    fn eq(&self, other: &SmartString) -> bool {
612        self.as_str() == other.as_str()
613    }
614}
615
616impl std::iter::FromIterator<char> for SmartString {
617    fn from_iter<I: IntoIterator<Item = char>>(iter: I) -> Self {
618        let s: String = iter.into_iter().collect();
619        SmartString::from_string(s)
620    }
621}
622
623impl fmt::Write for SmartString {
624    #[inline]
625    fn write_str(&mut self, s: &str) -> fmt::Result {
626        self.push_str(s);
627        Ok(())
628    }
629
630    #[inline]
631    fn write_char(&mut self, c: char) -> fmt::Result {
632        self.push(c);
633        Ok(())
634    }
635}
636
637#[cfg(test)]
638mod tests {
639    use super::*;
640    use std::collections::HashMap;
641    use std::mem::size_of;
642
643    #[test]
644    fn test_size() {
645        assert_eq!(size_of::<SmartString>(), 16);
646        assert_eq!(size_of::<Option<SmartString>>(), 16);
647    }
648
649    #[test]
650    fn test_inline() {
651        let s = SmartString::new("hello");
652        assert!(s.is_inline());
653        assert!(!s.is_heap());
654        assert_eq!(s.as_str(), "hello");
655        assert_eq!(s.len(), 5);
656    }
657
658    #[test]
659    fn test_inline_max() {
660        // 15 bytes is the max inline length
661        let s = SmartString::new("123456789012345");
662        assert!(s.is_inline());
663        assert_eq!(s.len(), 15);
664        assert_eq!(s.as_str(), "123456789012345");
665    }
666
667    #[test]
668    fn test_heap() {
669        // 16 bytes exceeds inline capacity
670        let s = SmartString::new("1234567890123456");
671        assert!(s.is_heap());
672        assert!(!s.is_inline());
673        assert_eq!(s.len(), 16);
674        assert_eq!(s.as_str(), "1234567890123456");
675    }
676
677    #[test]
678    fn test_clone_inline() {
679        let s1 = SmartString::new("hello");
680        let s2 = s1.clone();
681        assert_eq!(s1.as_str(), s2.as_str());
682        assert!(s1.is_inline());
683        assert!(s2.is_inline());
684    }
685
686    #[test]
687    fn test_clone_heap() {
688        let s1 = SmartString::new("this is a longer string that goes on the heap");
689        let s2 = s1.clone();
690        assert_eq!(s1.as_str(), s2.as_str());
691        assert!(s1.is_heap());
692        assert!(s2.is_heap());
693    }
694
695    #[test]
696    fn test_push() {
697        let mut s = SmartString::new("hello");
698        s.push(' ');
699        s.push('w');
700        assert_eq!(s.as_str(), "hello w");
701        assert!(s.is_inline());
702    }
703
704    #[test]
705    fn test_push_str() {
706        let mut s = SmartString::new("hello");
707        s.push_str(" world");
708        assert_eq!(s.as_str(), "hello world");
709        assert!(s.is_inline());
710    }
711
712    #[test]
713    fn test_push_str_to_heap() {
714        let mut s = SmartString::new("hello");
715        s.push_str(" world, this is a long string");
716        assert_eq!(s.as_str(), "hello world, this is a long string");
717        assert!(s.is_heap());
718    }
719
720    #[test]
721    fn test_from_string() {
722        let s = SmartString::from_string("hello".to_string());
723        assert!(s.is_inline());
724        assert_eq!(s.as_str(), "hello");
725
726        let s = SmartString::from_string("this is a longer string".to_string());
727        assert!(s.is_heap());
728        assert_eq!(s.as_str(), "this is a longer string");
729    }
730
731    #[test]
732    fn test_into_string() {
733        let s = SmartString::new("hello");
734        let string: String = s.into_string();
735        assert_eq!(string, "hello");
736
737        let s = SmartString::new("this is a longer string");
738        let string: String = s.into_string();
739        assert_eq!(string, "this is a longer string");
740    }
741
742    #[test]
743    fn test_eq() {
744        let s1 = SmartString::new("hello");
745        let s2 = SmartString::new("hello");
746        let s3 = SmartString::new("world");
747        assert_eq!(s1, s2);
748        assert_ne!(s1, s3);
749    }
750
751    #[test]
752    fn test_ord() {
753        let s1 = SmartString::new("apple");
754        let s2 = SmartString::new("banana");
755        assert!(s1 < s2);
756    }
757
758    #[test]
759    fn test_hash() {
760        let mut map = HashMap::new();
761        map.insert(SmartString::new("key"), "value");
762        assert_eq!(map.get(&SmartString::new("key")), Some(&"value"));
763    }
764
765    #[test]
766    fn test_borrow_lookup() {
767        use std::collections::HashMap;
768        let mut map: HashMap<SmartString, i32> = HashMap::new();
769        map.insert(SmartString::new("hello"), 42);
770
771        // Can look up with &str due to Borrow<str> implementation
772        assert_eq!(map.get("hello"), Some(&42));
773    }
774
775    #[test]
776    fn test_concat() {
777        let s = SmartString::concat("hello", " world");
778        assert_eq!(s.as_str(), "hello world");
779        assert!(s.is_inline());
780
781        let s = SmartString::concat("hello", " world, this is a very long string");
782        assert_eq!(s.as_str(), "hello world, this is a very long string");
783        assert!(s.is_heap());
784    }
785
786    #[test]
787    fn test_empty() {
788        let s = SmartString::new("");
789        assert!(s.is_empty());
790        assert!(s.is_inline());
791        assert_eq!(s.len(), 0);
792    }
793
794    #[test]
795    fn test_unicode() {
796        let s = SmartString::new("こんにちは"); // 15 bytes in UTF-8
797        assert!(s.is_inline());
798        assert_eq!(s.as_str(), "こんにちは");
799
800        let s = SmartString::new("こんにちは!"); // 16 bytes
801        assert!(s.is_heap());
802        assert_eq!(s.as_str(), "こんにちは!");
803    }
804
805    #[test]
806    fn test_case_conversion() {
807        let s = SmartString::new("Hello World");
808        assert_eq!(s.to_lowercase().as_str(), "hello world");
809        assert_eq!(s.to_uppercase().as_str(), "HELLO WORLD");
810    }
811
812    #[test]
813    fn test_ascii_case() {
814        let mut s = SmartString::new("Hello");
815        s.make_ascii_uppercase();
816        assert_eq!(s.as_str(), "HELLO");
817
818        s.make_ascii_lowercase();
819        assert_eq!(s.as_str(), "hello");
820    }
821
822    #[test]
823    fn test_default() {
824        let s = SmartString::default();
825        assert!(s.is_empty());
826        assert!(s.is_inline());
827    }
828
829    #[test]
830    fn test_from_arc_str() {
831        let arc: Arc<str> = Arc::from("hello");
832        let s = SmartString::from(arc);
833        assert_eq!(s.as_str(), "hello");
834    }
835
836    #[test]
837    fn test_build_inline() {
838        // SAFETY: Callback writes exactly 5 valid UTF-8 bytes ("hello")
839        let s = unsafe {
840            SmartString::build_inline(5, |buf| {
841                buf.copy_from_slice(b"hello");
842            })
843        };
844        assert!(s.is_some());
845        assert_eq!(s.unwrap().as_str(), "hello");
846
847        // SAFETY: Length 20 exceeds inline capacity, callback won't be invoked
848        let s = unsafe {
849            SmartString::build_inline(20, |_| {
850                // Too long, won't be called
851            })
852        };
853        assert!(s.is_none());
854    }
855}