Skip to main content

turbo_rcstr/
lib.rs

1// Allow the `rcstr!` proc macro's emitted `::turbo_rcstr::...` paths to
2// resolve when used inside this crate's own source (e.g. tests, doctests).
3extern crate self as turbo_rcstr;
4
5use std::{
6    borrow::{Borrow, Cow},
7    collections::HashMap,
8    ffi::OsStr,
9    fmt::{Debug, Display},
10    hash::{Hash, Hasher},
11    mem::{ManuallyDrop, forget},
12    num::NonZeroU8,
13    ops::Deref,
14    path::{Path, PathBuf},
15    sync::LazyLock,
16};
17
18use bincode::{
19    Decode, Encode,
20    de::{Decoder, read::Reader},
21    enc::Encoder,
22    error::{DecodeError, EncodeError},
23    impl_borrow_decode,
24};
25use bytes_str::BytesStr;
26use debug_unreachable::debug_unreachable;
27use rustc_hash::FxBuildHasher;
28#[cfg(not(target_family = "wasm"))]
29use scattered_collect::slice::ScatteredSlice;
30use serde::{Deserialize, Deserializer, Serialize, Serializer};
31use shrink_to_fit::ShrinkToFit;
32use smallvec::SmallVec;
33use triomphe::Arc;
34use turbo_tasks_hash::{DeterministicHash, DeterministicHasher};
35
36use crate::{
37    dynamic::{
38        DynamicPrehashedString, deref_dynamic, deref_static, hash_bytes, new_atom,
39        new_atom_from_prehashed, new_static_atom,
40    },
41    tagged_value::{MAX_INLINE_LEN, TaggedValue},
42};
43
44mod dynamic;
45mod tagged_value;
46
47/// An immutable reference counted [`String`], similar to [`Arc<String>`][std::sync::Arc].
48///
49/// This is the preferred immutable string type for [`turbo_tasks::function`][func] arguments and
50/// inside of [`turbo_tasks::value`][value].
51///
52/// As turbo-tasks must store copies of function arguments to enable caching, non-reference counted
53/// [`String`]s would incur frequent cloning. Reference counting typically decreases memory
54/// consumption and CPU time in these cases.
55///
56/// [func]: https://turbopack-rust-docs.vercel.sh/rustdoc/turbo_tasks/attr.function.html
57/// [value]: https://turbopack-rust-docs.vercel.sh/rustdoc/turbo_tasks/attr.value.html
58///
59/// ## Conversion
60///
61/// Converting a `String` or `&str` to an `RcStr` can be performed using `.into()`,
62/// `RcStr::from(...)`, or the `rcstr!` macro.
63///
64/// ```
65/// # use turbo_rcstr::{RcStr, rcstr};
66/// #
67/// let s = "foo";
68/// let rc_s1: RcStr = s.into();
69/// let rc_s2 = RcStr::from(s);
70/// let rc_s3 = rcstr!("foo");
71/// assert_eq!(rc_s1, rc_s2);
72/// ```
73///
74/// Generally speaking you should
75///  * use `rcstr!` when converting a `const`-compatible `str`
76///  * use `RcStr::from` for readability
77///  * use `.into()` when context makes it clear.
78///
79/// Converting from an [`RcStr`] to a `&str` should be done with [`RcStr::as_str`]. Converting to a
80/// `String` should be done with [`RcStr::into_owned`].
81///
82/// ## Future Optimizations
83///
84/// This type is intentionally opaque to allow for optimizations to the underlying representation.
85/// Future implementations may use inline representations or interning.
86//
87// If you want to change the underlying string type to `Arc<str>`, please ensure that you profile
88// performance. The current implementation offers very cheap `String -> RcStr -> String`, meaning we
89// only pay for the allocation for `Arc` when we pass `format!("").into()` to a function.
90pub struct RcStr {
91    unsafe_data: TaggedValue,
92}
93
94const _: () = {
95    // Enforce that RcStr triggers the non-zero size optimization.
96    assert!(std::mem::size_of::<RcStr>() == std::mem::size_of::<Option<RcStr>>());
97};
98
99unsafe impl Send for RcStr {}
100unsafe impl Sync for RcStr {}
101
102// Marks a payload that is stored in an Arc
103const DYNAMIC_TAG: u8 = 0b_10;
104// Marks a payload that has been leaked since it has a static lifetime
105const STATIC_TAG: u8 = 0b_00;
106// The payload is stored inline
107const INLINE_TAG: u8 = 0b_01; // len in upper nybble
108const INLINE_TAG_INIT: NonZeroU8 = NonZeroU8::new(INLINE_TAG).unwrap();
109const TAG_MASK: u8 = 0b_11;
110// For inline tags the length is stored in the upper 4 bits of the tag byte
111const LEN_OFFSET: usize = 4;
112const LEN_MASK: u8 = 0xf0;
113
114impl RcStr {
115    #[inline(always)]
116    fn tag(&self) -> u8 {
117        self.unsafe_data.tag_byte() & TAG_MASK
118    }
119
120    #[inline(never)]
121    pub fn as_str(&self) -> &str {
122        match self.tag() {
123            STATIC_TAG => unsafe { deref_static(self.unsafe_data).value },
124            DYNAMIC_TAG => unsafe { &deref_dynamic(self.unsafe_data).value },
125            INLINE_TAG => self.inline_as_str(),
126            _ => unsafe { debug_unreachable!() },
127        }
128    }
129
130    fn inline_as_str(&self) -> &str {
131        debug_assert!(self.tag() == INLINE_TAG);
132        let len = (self.unsafe_data.tag_byte() & LEN_MASK) >> LEN_OFFSET;
133        let src = self.unsafe_data.data();
134        unsafe { std::str::from_utf8_unchecked(&src[..(len as usize)]) }
135    }
136
137    /// Returns an owned mutable [`String`].
138    ///
139    /// This implementation is more efficient than [`ToString::to_string`]:
140    ///
141    /// - If the reference count is 1, the `Arc` can be unwrapped, giving ownership of the
142    ///   underlying string without cloning in `O(1)` time.
143    /// - This avoids some of the potential overhead of the `Display` trait.
144    pub fn into_owned(self) -> String {
145        match self.tag() {
146            DYNAMIC_TAG => {
147                // convert `self` into `arc`
148                let arc = unsafe { dynamic::restore_arc(ManuallyDrop::new(self).unsafe_data) };
149                match Arc::try_unwrap(arc) {
150                    // `String::from(Box<str>)` reuses the boxed allocation, so this is O(1).
151                    Ok(v) => String::from(v.value),
152                    Err(arc) => arc.value.to_string(),
153                }
154            }
155            INLINE_TAG => self.inline_as_str().to_string(),
156            STATIC_TAG => unsafe { deref_static(self.unsafe_data).value.to_string() },
157            _ => unsafe { debug_unreachable!() },
158        }
159    }
160
161    pub fn map(self, f: impl FnOnce(String) -> String) -> Self {
162        RcStr::from(Cow::Owned(f(self.into_owned())))
163    }
164
165    /// Create an RcStr from a deserialized string, checking the static constant
166    /// table first. If the string matches an `rcstr!` constant, returns a
167    /// zero-cost static copy instead of allocating a new Arc.
168    ///
169    /// Accepts `&str` so that borrow-decode paths can avoid heap allocation
170    /// entirely for inline strings (≤7 bytes) and static table hits.
171    fn from_deserialized(s: &str) -> Self {
172        if !is_atom_inlineable(s) {
173            let hash = hash_bytes(s.as_bytes());
174            // Check the static table
175            if let Some(entries) = STATIC_TABLE.get(&hash)
176                && let Some(static_phs) = entries.iter().find(|phs| phs.value == s)
177            {
178                new_static_atom(static_phs)
179            } else {
180                new_atom_from_prehashed(DynamicPrehashedString {
181                    hash,
182                    value: s.into(),
183                })
184            }
185        } else {
186            inline_atom(s).unwrap()
187        }
188    }
189}
190
191impl DeterministicHash for RcStr {
192    fn deterministic_hash<H: DeterministicHasher>(&self, state: &mut H) {
193        state.write_usize(self.len());
194        state.write_bytes(self.as_bytes());
195    }
196}
197
198impl Deref for RcStr {
199    type Target = str;
200
201    fn deref(&self) -> &Self::Target {
202        self.as_str()
203    }
204}
205
206impl Borrow<str> for RcStr {
207    fn borrow(&self) -> &str {
208        self.as_str()
209    }
210}
211
212impl AsRef<str> for RcStr {
213    fn as_ref(&self) -> &str {
214        self.as_str()
215    }
216}
217
218impl From<BytesStr> for RcStr {
219    fn from(s: BytesStr) -> Self {
220        let bytes: Vec<u8> = s.into_bytes().into();
221        RcStr::from(unsafe {
222            // Safety: BytesStr are valid utf-8
223            String::from_utf8_unchecked(bytes)
224        })
225    }
226}
227
228impl From<Arc<String>> for RcStr {
229    fn from(s: Arc<String>) -> Self {
230        match Arc::try_unwrap(s) {
231            Ok(v) => new_atom(Cow::Owned(v)),
232            Err(arc) => new_atom(Cow::Borrowed(&**arc)),
233        }
234    }
235}
236
237impl From<String> for RcStr {
238    fn from(s: String) -> Self {
239        new_atom(Cow::Owned(s))
240    }
241}
242
243impl From<&'_ str> for RcStr {
244    fn from(s: &str) -> Self {
245        new_atom(Cow::Borrowed(s))
246    }
247}
248
249impl From<Cow<'_, str>> for RcStr {
250    fn from(s: Cow<str>) -> Self {
251        new_atom(s)
252    }
253}
254
255/// Mimic `&str`
256impl AsRef<Path> for RcStr {
257    fn as_ref(&self) -> &Path {
258        self.as_str().as_ref()
259    }
260}
261
262/// Mimic `&str`
263impl AsRef<OsStr> for RcStr {
264    fn as_ref(&self) -> &OsStr {
265        self.as_str().as_ref()
266    }
267}
268
269/// Mimic `&str`
270impl AsRef<[u8]> for RcStr {
271    fn as_ref(&self) -> &[u8] {
272        self.as_str().as_ref()
273    }
274}
275
276impl From<RcStr> for BytesStr {
277    fn from(value: RcStr) -> Self {
278        Self::from_str_slice(value.as_str())
279    }
280}
281
282impl PartialEq<str> for RcStr {
283    fn eq(&self, other: &str) -> bool {
284        self.as_str() == other
285    }
286}
287
288impl PartialEq<&'_ str> for RcStr {
289    fn eq(&self, other: &&str) -> bool {
290        self.as_str() == *other
291    }
292}
293
294impl PartialEq<String> for RcStr {
295    fn eq(&self, other: &String) -> bool {
296        self.as_str() == other.as_str()
297    }
298}
299
300impl Debug for RcStr {
301    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
302        Debug::fmt(&self.as_str(), f)
303    }
304}
305
306impl Display for RcStr {
307    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
308        Display::fmt(&self.as_str(), f)
309    }
310}
311
312impl From<RcStr> for String {
313    fn from(s: RcStr) -> Self {
314        s.into_owned()
315    }
316}
317
318impl From<RcStr> for PathBuf {
319    fn from(s: RcStr) -> Self {
320        String::from(s).into()
321    }
322}
323
324impl Clone for RcStr {
325    #[inline(always)]
326    fn clone(&self) -> Self {
327        // We only need to increment the ref count for DYNAMIC_TAG values.
328        // For STATIC_TAG and INLINE_TAG we can just copy the value.
329        if self.tag() == DYNAMIC_TAG {
330            unsafe {
331                let arc = dynamic::restore_arc(self.unsafe_data);
332                forget(arc.clone());
333                forget(arc);
334            }
335        }
336
337        RcStr {
338            unsafe_data: self.unsafe_data,
339        }
340    }
341}
342
343impl Default for RcStr {
344    fn default() -> Self {
345        rcstr!("")
346    }
347}
348
349impl PartialEq for RcStr {
350    fn eq(&self, other: &Self) -> bool {
351        // For inline RcStrs this is sufficient and for out of line values it handles a simple
352        // identity cases
353        if self.unsafe_data == other.unsafe_data {
354            return true;
355        }
356        // If either side is inline, they can't be equal: an inline string is always shorter than
357        // any heap-allocated one (construction splits on length), and two inline strings would
358        // have been caught by the `unsafe_data == unsafe_data` check above.
359        if self.tag() == INLINE_TAG || other.tag() == INLINE_TAG {
360            return false;
361        }
362
363        // slow path compare precomputed hashes and string refs
364        let (l_hash, l_str) = unsafe { heap_hash_and_str(self) };
365        let (r_hash, r_str) = unsafe { heap_hash_and_str(other) };
366        l_hash == r_hash && l_str == r_str
367    }
368}
369
370/// Caller must ensure `s.tag()` is `STATIC_TAG` or `DYNAMIC_TAG`.
371#[inline]
372unsafe fn heap_hash_and_str(s: &RcStr) -> (u64, &str) {
373    match s.tag() {
374        STATIC_TAG => {
375            let p = unsafe { deref_static(s.unsafe_data) };
376            (p.hash, p.value)
377        }
378        DYNAMIC_TAG => {
379            let p = unsafe { deref_dynamic(s.unsafe_data) };
380            (p.hash, &p.value)
381        }
382        _ => unsafe { debug_unreachable!() },
383    }
384}
385
386impl Eq for RcStr {}
387
388impl PartialOrd for RcStr {
389    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
390        Some(self.cmp(other))
391    }
392}
393
394impl Ord for RcStr {
395    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
396        self.as_str().cmp(other.as_str())
397    }
398}
399
400impl Hash for RcStr {
401    fn hash<H: Hasher>(&self, state: &mut H) {
402        match self.tag() {
403            STATIC_TAG => {
404                state.write_u64(unsafe { deref_static(self.unsafe_data).hash });
405                state.write_u8(0xff); // matches the implementation of the `str` Hash impl
406            }
407            DYNAMIC_TAG => {
408                state.write_u64(unsafe { deref_dynamic(self.unsafe_data).hash });
409                state.write_u8(0xff); // matches the implementation of the `str` Hash impl
410            }
411            INLINE_TAG => {
412                self.inline_as_str().hash(state);
413            }
414            _ => unsafe { debug_unreachable!() },
415        }
416    }
417}
418
419impl Serialize for RcStr {
420    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
421        serializer.serialize_str(self.as_str())
422    }
423}
424
425impl<'de> Deserialize<'de> for RcStr {
426    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
427        struct RcStrVisitor;
428
429        impl serde::de::Visitor<'_> for RcStrVisitor {
430            type Value = RcStr;
431
432            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
433                f.write_str("a string")
434            }
435
436            fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<RcStr, E> {
437                Ok(RcStr::from_deserialized(v))
438            }
439
440            fn visit_string<E: serde::de::Error>(self, v: String) -> Result<RcStr, E> {
441                Ok(RcStr::from_deserialized(&v))
442            }
443        }
444
445        deserializer.deserialize_str(RcStrVisitor)
446    }
447}
448
449impl Encode for RcStr {
450    fn encode<E: Encoder>(&self, encoder: &mut E) -> Result<(), EncodeError> {
451        self.as_str().encode(encoder)
452    }
453}
454
455impl<Context> Decode<Context> for RcStr {
456    fn decode<D: Decoder<Context = Context>>(decoder: &mut D) -> Result<Self, DecodeError> {
457        // Decode the length prefix
458        let len = u64::decode(decoder)?;
459        let len: usize = len
460            .try_into()
461            .map_err(|_| DecodeError::OutsideUsizeRange(len))?;
462
463        if unty::type_equal::<D::R, turbo_bincode::TurboBincodeReader>() {
464            // We know the reader is a TurboBincodeReader backed by &[u8], so peek_read
465            // returning None means data corruption (not enough bytes), not "unsupported".
466            let bytes = decoder
467                .reader()
468                .peek_read(len)
469                .ok_or(DecodeError::UnexpectedEnd { additional: len })?;
470            let s = core::str::from_utf8(bytes).map_err(|inner| DecodeError::Utf8 { inner })?;
471            let rcstr = RcStr::from_deserialized(s);
472            decoder.reader().consume(len);
473            Ok(rcstr)
474        } else {
475            unreachable!(
476                "RcStr::decode expected TurboBincodeReader, but was called with a {} reader",
477                std::any::type_name::<D::R>(),
478            )
479        }
480    }
481}
482
483impl_borrow_decode!(RcStr);
484
485impl Drop for RcStr {
486    fn drop(&mut self) {
487        match self.tag() {
488            DYNAMIC_TAG => unsafe { drop(dynamic::restore_arc(self.unsafe_data)) },
489            INLINE_TAG | STATIC_TAG => {
490                // no-ops
491            }
492            _ => unsafe { debug_unreachable!() },
493        }
494    }
495}
496
497// Exports for our macro
498#[doc(hidden)]
499pub const fn inline_atom(s: &str) -> Option<RcStr> {
500    dynamic::inline_atom(s)
501}
502
503// Exports for our macro
504#[doc(hidden)]
505pub const fn is_atom_inlineable(s: &str) -> bool {
506    s.len() <= MAX_INLINE_LEN
507}
508
509#[doc(hidden)]
510#[inline(always)]
511pub const fn from_static(s: &'static StaticPrehashedString) -> RcStr {
512    dynamic::new_static_atom(s)
513}
514#[doc(hidden)]
515pub use dynamic::StaticPrehashedString;
516
517#[doc(hidden)]
518pub const fn make_const_prehashed_string(text: &'static str) -> StaticPrehashedString {
519    StaticPrehashedString {
520        value: text,
521        hash: hash_bytes(text.as_bytes()),
522    }
523}
524
525// Re-export scattered-collect so the `rcstr!` macro can reference it via
526// `$crate::scattered_collect`.
527#[cfg(not(target_family = "wasm"))]
528#[doc(hidden)]
529pub use scattered_collect;
530
531/// Wrapper for collecting `rcstr!` static constants at link time.
532#[doc(hidden)]
533pub struct StaticRcStr(pub &'static StaticPrehashedString);
534
535// Link-time collection of every `rcstr!` static.
536//
537// Disabled under wasm because scattered-collect relies on a environment provided function
538// described in <https://docs.rs/link-section/latest/link_section/#wasm> and installing it is tricky
539// using wasm-bindgen. Also this is only here to support deserialization of rcstrs which shouldn't
540// happen under wasm anyway.
541#[cfg(not(target_family = "wasm"))]
542#[doc(hidden)]
543#[scattered_collect::gather]
544pub static STATIC_RCSTRS: ScatteredSlice<StaticRcStr>;
545// stubbed out for wasm
546#[cfg(target_family = "wasm")]
547const STATIC_RCSTRS: [StaticRcStr; 0] = [];
548
549/// Submits a `StaticRcStr` into [`STATIC_RCSTRS`] at link time.
550#[doc(hidden)]
551#[macro_export]
552macro_rules! __rcstr_static_submit {
553    ($value:expr) => {
554        #[cfg(not(target_family = "wasm"))]
555        $crate::scattered_collect::declarative::scatter! {
556            #[scatter($crate::STATIC_RCSTRS)]
557            const _: $crate::StaticRcStr = $value;
558        }
559    };
560}
561
562/// Read-only lookup table mapping precomputed hash -> static StaticPrehashedString.
563/// Built once on first access from all `rcstr!` constants gathered at link time into
564/// [`STATIC_RCSTRS`].
565///
566/// Multiple `rcstr!` calls with the same string content will each scatter an entry, but we
567/// deduplicate by content here so only one entry per unique string is stored.
568static STATIC_TABLE: LazyLock<
569    HashMap<u64, SmallVec<[&'static StaticPrehashedString; 1]>, FxBuildHasher>,
570> = LazyLock::new(|| {
571    let mut map: HashMap<u64, SmallVec<[&'static StaticPrehashedString; 1]>, FxBuildHasher> =
572        HashMap::with_hasher(FxBuildHasher);
573    for &StaticRcStr(phs) in STATIC_RCSTRS.iter() {
574        if phs.value.len() <= MAX_INLINE_LEN {
575            // This is rare, but possible if our macro cannot determine the length of the string at
576            // macro time we may end up with a wasted StaticPrehashedString scattered into the
577            // collection.
578
579            // Just skip it
580            continue;
581        }
582        let entries = map.entry(phs.hash).or_default();
583        // Deduplicate: skip if an entry with the same string content exists
584        // Mostly linkers will merge static strings but this isn't guaranteed so we cannot just rely
585        // on pointer equality.
586        if !entries.iter().any(|e| e.value == phs.value) {
587            entries.push(phs);
588        }
589    }
590    map.shrink_to_fit(); // this map will never change again
591    map
592});
593
594/// Create an rcstr from a string literal.
595/// Allocates the RcStr inline when possible, otherwise uses a static `PrehashedString`.  In
596/// either case this is a compile time constant
597pub use turbo_rcstr_macros::rcstr;
598
599/// noop
600impl ShrinkToFit for RcStr {
601    #[inline(always)]
602    fn shrink_to_fit(&mut self) {}
603}
604
605#[cfg(all(feature = "napi", target_family = "wasm"))]
606compile_error!("The napi feature cannot be enabled for wasm targets");
607
608#[cfg(all(feature = "napi", not(target_family = "wasm")))]
609mod napi_impl {
610    use napi::{
611        bindgen_prelude::{FromNapiValue, ToNapiValue, TypeName, ValidateNapiValue},
612        sys::{napi_env, napi_value},
613    };
614
615    use super::*;
616
617    impl TypeName for RcStr {
618        fn type_name() -> &'static str {
619            String::type_name()
620        }
621
622        fn value_type() -> napi::ValueType {
623            String::value_type()
624        }
625    }
626
627    impl ToNapiValue for RcStr {
628        unsafe fn to_napi_value(env: napi_env, val: Self) -> napi::Result<napi_value> {
629            unsafe { ToNapiValue::to_napi_value(env, val.as_str()) }
630        }
631    }
632
633    impl FromNapiValue for RcStr {
634        unsafe fn from_napi_value(env: napi_env, napi_val: napi_value) -> napi::Result<Self> {
635            Ok(RcStr::from(unsafe {
636                String::from_napi_value(env, napi_val)
637            }?))
638        }
639    }
640
641    impl ValidateNapiValue for RcStr {
642        unsafe fn validate(env: napi_env, napi_val: napi_value) -> napi::Result<napi_value> {
643            unsafe { String::validate(env, napi_val) }
644        }
645    }
646}
647
648/// Runtime string interning table.
649///
650/// Deduplicates strings by storing them in an `FxHashSet<RcStr>`. Strings
651/// shorter than the inline threshold are already zero-allocation, so only
652/// longer strings benefit from interning.
653pub struct RcStrInterning {
654    set: rustc_hash::FxHashSet<RcStr>,
655}
656
657impl Default for RcStrInterning {
658    fn default() -> Self {
659        Self::new()
660    }
661}
662
663impl RcStrInterning {
664    /// Create a new empty interning table.
665    pub fn new() -> Self {
666        Self {
667            set: rustc_hash::FxHashSet::default(),
668        }
669    }
670
671    /// Intern a string slice. Returns a cheap-to-clone [`RcStr`].
672    ///
673    /// Strings below the inline threshold are returned directly (they are
674    /// already zero-allocation inline atoms). Longer strings are looked up
675    /// in the interning table and deduplicated.
676    pub fn intern(&mut self, s: &str) -> RcStr {
677        if is_atom_inlineable(s) {
678            // Inline atom — no allocation needed, don't bother with the set.
679            return RcStr::from(s);
680        }
681        if let Some(existing) = self.set.get(s) {
682            return existing.clone();
683        }
684        let rc = RcStr::from(s);
685        self.set.insert(rc.clone());
686        rc
687    }
688
689    /// Intern an owned `String`. When the string is not yet interned, avoids
690    /// an extra copy compared to [`intern`](Self::intern).
691    fn intern_owned(&mut self, s: String) -> RcStr {
692        if is_atom_inlineable(&s) {
693            return RcStr::from(s);
694        }
695        if let Some(existing) = self.set.get(s.as_str()) {
696            return existing.clone();
697        }
698        let rc = RcStr::from(s);
699        self.set.insert(rc.clone());
700        rc
701    }
702
703    /// Intern a `Cow<str>`. When the cow is `Owned`, avoids an extra copy
704    /// if the string is not yet interned.
705    pub fn intern_cow(&mut self, s: std::borrow::Cow<'_, str>) -> RcStr {
706        match s {
707            std::borrow::Cow::Borrowed(s) => self.intern(s),
708            std::borrow::Cow::Owned(s) => self.intern_owned(s),
709        }
710    }
711
712    /// Intern the [`Display`](std::fmt::Display) output of a value.
713    pub fn intern_display(&mut self, v: &impl std::fmt::Display) -> RcStr {
714        self.intern_owned(v.to_string())
715    }
716}
717
718#[cfg(test)]
719mod tests {
720    use std::mem::ManuallyDrop;
721
722    use super::*;
723
724    #[test]
725    fn test_refcount() {
726        fn refcount(str: &RcStr) -> usize {
727            assert!(str.tag() == DYNAMIC_TAG);
728            let arc = ManuallyDrop::new(unsafe { dynamic::restore_arc(str.unsafe_data) });
729            triomphe::Arc::count(&arc)
730        }
731
732        let str = RcStr::from("this is a long string that won't be inlined");
733
734        assert_eq!(refcount(&str), 1);
735        assert_eq!(refcount(&str), 1); // refcount should not modify the refcount itself
736
737        let cloned_str = str.clone();
738        assert_eq!(refcount(&str), 2);
739
740        drop(cloned_str);
741        assert_eq!(refcount(&str), 1);
742
743        let _ = str.clone().into_owned();
744        assert_eq!(refcount(&str), 1);
745    }
746
747    #[test]
748    fn test_rcstr() {
749        // Test enough to exceed the small string optimization
750        assert_eq!(rcstr!(""), RcStr::default());
751        assert_eq!(rcstr!(""), RcStr::from(""));
752        assert_eq!(rcstr!("a"), RcStr::from("a"));
753        assert_eq!(rcstr!("ab"), RcStr::from("ab"));
754        assert_eq!(rcstr!("abc"), RcStr::from("abc"));
755        assert_eq!(rcstr!("abcd"), RcStr::from("abcd"));
756        assert_eq!(rcstr!("abcde"), RcStr::from("abcde"));
757        assert_eq!(rcstr!("abcdef"), RcStr::from("abcdef"));
758        assert_eq!(rcstr!("abcdefg"), RcStr::from("abcdefg"));
759        assert_eq!(rcstr!("abcdefgh"), RcStr::from("abcdefgh"));
760        assert_eq!(rcstr!("abcdefghi"), RcStr::from("abcdefghi"));
761    }
762
763    #[test]
764    fn test_static_atom() {
765        const LONG: &str = "a very long string that lives forever";
766        let leaked = rcstr!(LONG);
767        let not_leaked = RcStr::from(LONG);
768        assert_ne!(leaked.tag(), not_leaked.tag());
769        assert_eq!(leaked, not_leaked);
770    }
771
772    #[test]
773    fn test_inline_atom() {
774        // This is a silly test, just asserts that we can evaluate this in a constant context.
775        const STR: RcStr = {
776            let inline = inline_atom("hello");
777            if inline.is_some() {
778                inline.unwrap()
779            } else {
780                unreachable!();
781            }
782        };
783        assert_eq!(STR, RcStr::from("hello"));
784    }
785
786    #[test]
787    fn test_hash_matches_str() {
788        use std::hash::{Hash, Hasher};
789
790        use rustc_hash::FxHasher;
791
792        fn fxhash<T: Hash>(value: T) -> u64 {
793            let mut hasher = FxHasher::default();
794            value.hash(&mut hasher);
795            hasher.finish()
796        }
797
798        // Test various string lengths covering inline and prehashed storage
799        let test_strings = [
800            "",
801            "a",
802            "ab",
803            "abc",
804            "abcdef",  // max inline (6 chars)
805            "abcdefg", // just beyond inline (7 chars)
806            "abcdefgh",
807            "a very long string that exceeds sixteen bytes",
808        ];
809
810        // Test RcStr vs &str
811        for s in test_strings {
812            let rcstr = RcStr::from(s);
813            assert_eq!(
814                fxhash(&rcstr),
815                fxhash(s),
816                "Hash mismatch for string of length {}: {:?}",
817                s.len(),
818                s
819            );
820        }
821
822        // Test (RcStr, RcStr) vs (&str, &str)
823        for s1 in test_strings {
824            for s2 in test_strings {
825                let rcstr1 = RcStr::from(s1);
826                let rcstr2 = RcStr::from(s2);
827                assert_eq!(
828                    fxhash((&rcstr1, &rcstr2)),
829                    fxhash((s1, s2)),
830                    "Tuple hash mismatch for ({:?}, {:?})",
831                    s1,
832                    s2
833                );
834            }
835        }
836    }
837
838    #[test]
839    fn test_bincode_roundtrip() {
840        use turbo_bincode::{turbo_bincode_decode, turbo_bincode_encode};
841
842        // Test inline string
843        let short = RcStr::from("hi");
844        let encoded = turbo_bincode_encode(&short).unwrap();
845        let decoded: RcStr = turbo_bincode_decode(&encoded).unwrap();
846        assert_eq!(decoded, short);
847        assert_eq!(decoded.tag(), INLINE_TAG);
848
849        // Test dynamic string (no static match)
850        let long = RcStr::from("bincode_roundtrip: no matching rcstr constant");
851        let encoded = turbo_bincode_encode(&long).unwrap();
852        let decoded: RcStr = turbo_bincode_decode(&encoded).unwrap();
853        assert_eq!(decoded, long);
854        assert_eq!(decoded.tag(), DYNAMIC_TAG);
855
856        // Test static dedup via decode
857        const STATIC_STR: &str = "bincode_roundtrip: a static constant for testing";
858        let _register = rcstr!(STATIC_STR);
859        let original = RcStr::from(STATIC_STR); // DYNAMIC since from() doesn't check
860        let encoded = turbo_bincode_encode(&original).unwrap();
861        let decoded: RcStr = turbo_bincode_decode(&encoded).unwrap();
862        assert_eq!(decoded.as_str(), STATIC_STR);
863        // Decoded via peek_read path should find the static constant
864        assert_eq!(decoded.tag(), STATIC_TAG);
865    }
866
867    #[test]
868    fn test_interning() {
869        let mut interner = RcStrInterning::new();
870
871        // Short strings are always inline (no interning needed)
872        let a = interner.intern("hi");
873        let b = interner.intern("hi");
874        assert_eq!(a, b);
875
876        // Long strings should be deduplicated to the same allocation.
877        let long = "this is a long string that exceeds inline threshold";
878        let c = interner.intern(long);
879        let d = interner.intern(long);
880        assert_eq!(c, d);
881        assert!(std::ptr::eq(c.as_str().as_ptr(), d.as_str().as_ptr()));
882
883        // intern_cow with borrowed — same allocation as c
884        let e = interner.intern_cow(std::borrow::Cow::Borrowed(long));
885        assert_eq!(e, c);
886        assert!(std::ptr::eq(e.as_str().as_ptr(), c.as_str().as_ptr()));
887
888        // intern_cow with owned — same allocation as c (no new alloc)
889        let f = interner.intern_cow(std::borrow::Cow::Owned(long.to_string()));
890        assert_eq!(f, c);
891        assert!(std::ptr::eq(f.as_str().as_ptr(), c.as_str().as_ptr()));
892
893        // intern_display — a fresh long string, verify it is interned too
894        let long2 = "another long string that exceeds the inline threshold here";
895        let g = interner.intern_display(&long2);
896        let h = interner.intern_display(&long2);
897        assert_eq!(g, h);
898        assert!(std::ptr::eq(g.as_str().as_ptr(), h.as_str().as_ptr()));
899    }
900}