Skip to main content

mago_atom/
lib.rs

1//! A high-performance, globally-interned string library for the Mago ecosystem.
2//!
3//! This crate provides `Atom`, a canonical string type that guarantees any given
4//! string is stored in memory only once. It acts as a wrapper for the `ustr` crate and adds
5//! highly-optimized constructors for common string manipulations like lowercasing,
6//! concatenation, and number formatting.
7//!
8//! The key feature is the ability to perform these operations without heap allocations
9//! for common cases by using stack-allocated buffers, making this crate ideal for
10//! performance-critical code.
11//!
12//! # Usage
13//!
14//! ```
15//! use mago_atom::*;
16//!
17//! // Create an Atom. This is a cheap lookup in a global cache.
18//! let s1 = atom("Hello");
19//!
20//! // Use an optimized, zero-heap-allocation constructor.
21//! let s2 = ascii_lowercase_atom("Hello");
22//!
23//! assert_eq!(s2.as_str(), "hello");
24//!
25//! // Use the specialized, high-performance map.
26//! let mut map = AtomMap::default();
27//! map.insert(s1, 123);
28//! ```
29
30#[cfg(target_arch = "aarch64")]
31use std::arch::aarch64::vandq_u8;
32#[cfg(target_arch = "aarch64")]
33use std::arch::aarch64::vceqq_u8;
34#[cfg(target_arch = "aarch64")]
35use std::arch::aarch64::vcgeq_u8;
36#[cfg(target_arch = "aarch64")]
37use std::arch::aarch64::vcleq_u8;
38#[cfg(target_arch = "aarch64")]
39use std::arch::aarch64::vdupq_n_u8;
40#[cfg(target_arch = "aarch64")]
41use std::arch::aarch64::vld1q_u8;
42#[cfg(target_arch = "aarch64")]
43use std::arch::aarch64::vminvq_u8;
44#[cfg(target_arch = "aarch64")]
45use std::arch::aarch64::vorrq_u8;
46#[cfg(target_arch = "x86_64")]
47use std::arch::x86_64::__m256i;
48#[cfg(target_arch = "x86_64")]
49use std::arch::x86_64::_mm256_add_epi8;
50#[cfg(target_arch = "x86_64")]
51use std::arch::x86_64::_mm256_and_si256;
52#[cfg(target_arch = "x86_64")]
53use std::arch::x86_64::_mm256_cmpeq_epi8;
54#[cfg(target_arch = "x86_64")]
55use std::arch::x86_64::_mm256_cmpgt_epi8;
56#[cfg(target_arch = "x86_64")]
57use std::arch::x86_64::_mm256_loadu_si256;
58#[cfg(target_arch = "x86_64")]
59use std::arch::x86_64::_mm256_movemask_epi8;
60#[cfg(target_arch = "x86_64")]
61use std::arch::x86_64::_mm256_or_si256;
62#[cfg(target_arch = "x86_64")]
63use std::arch::x86_64::_mm256_set1_epi8;
64#[cfg(target_arch = "x86_64")]
65use std::arch::x86_64::_mm256_sub_epi8;
66use std::collections::HashMap;
67use std::collections::HashSet;
68use std::hash::BuildHasherDefault;
69
70use ustr::IdentityHasher;
71
72/// A canonical, globally-interned string. Two `Atom`s with the same content always share storage.
73pub type Atom = ustr::Ustr;
74
75/// Interns a string and returns its canonical [`Atom`].
76#[inline]
77#[must_use]
78pub fn atom(s: &str) -> Atom {
79    ustr::ustr(s)
80}
81
82/// A high-performance `HashMap` using `Atom` as the key.
83///
84/// This map is significantly faster than a standard `HashMap` because it uses the
85/// `Atom`'s pre-computed hash instead of hashing the string content on every lookup.
86pub type AtomMap<V> = HashMap<Atom, V, BuildHasherDefault<IdentityHasher>>;
87
88/// A high-performance `HashSet` using `Atom` as the key.
89///
90/// This set is significantly faster than a standard `HashSet` because it uses the
91/// `Atom`'s pre-computed hash.
92pub type AtomSet = HashSet<Atom, BuildHasherDefault<IdentityHasher>>;
93
94/// The maximum size in bytes for a string to be processed on the stack.
95const STACK_BUF_SIZE: usize = 256;
96
97thread_local! {
98    static EMPTY_ATOM: Atom = atom("");
99}
100
101/// Returns the canonical `Atom` for an empty string.
102///
103/// This is a very cheap operation.
104#[inline]
105#[must_use]
106pub fn empty_atom() -> Atom {
107    EMPTY_ATOM.with(|&atom| atom)
108}
109
110/// A macro to concatenate between 2 and 12 string slices into a single `Atom`.
111///
112/// This macro dispatches to a specialized, zero-heap-allocation function based on the
113/// number of arguments provided, making it highly performant for a known number of inputs.
114/// It uses a stack-allocated buffer to avoid hitting the heap.
115///
116/// # Panics
117///
118/// Panics at compile time if called with 0, 1, or more than 12 arguments.
119#[macro_export]
120macro_rules! concat_atom {
121    ($s1:expr, $s2:expr $(,)?) => {
122        $crate::concat_atom2(&$s1, &$s2)
123    };
124    ($s1:expr, $s2:expr, $s3:expr $(,)?) => {
125        $crate::concat_atom3(&$s1, &$s2, &$s3)
126    };
127    ($s1:expr, $s2:expr, $s3:expr, $s4:expr $(,)?) => {
128        $crate::concat_atom4(&$s1, &$s2, &$s3, &$s4)
129    };
130    ($s1:expr, $s2:expr, $s3:expr, $s4:expr, $s5:expr $(,)?) => {
131        $crate::concat_atom5(&$s1, &$s2, &$s3, &$s4, &$s5)
132    };
133    ($s1:expr, $s2:expr, $s3:expr, $s4:expr, $s5:expr, $s6:expr $(,)?) => {
134        $crate::concat_atom6(&$s1, &$s2, &$s3, &$s4, &$s5, &$s6)
135    };
136    ($s1:expr, $s2:expr, $s3:expr, $s4:expr, $s5:expr, $s6:expr, $s7:expr $(,)?) => {
137        $crate::concat_atom7(&$s1, &$s2, &$s3, &$s4, &$s5, &$s6, &$s7)
138    };
139    ($s1:expr, $s2:expr, $s3:expr, $s4:expr, $s5:expr, $s6:expr, $s7:expr, $s8:expr $(,)?) => {
140        $crate::concat_atom8(&$s1, &$s2, &$s3, &$s4, &$s5, &$s6, &$s7, &$s8)
141    };
142    ($s1:expr, $s2:expr, $s3:expr, $s4:expr, $s5:expr, $s6:expr, $s7:expr, $s8:expr, $s9:expr $(,)?) => {
143        $crate::concat_atom9(&$s1, &$s2, &$s3, &$s4, &$s5, &$s6, &$s7, &$s8, &$s9)
144    };
145    ($s1:expr, $s2:expr, $s3:expr, $s4:expr, $s5:expr, $s6:expr, $s7:expr, $s8:expr, $s9:expr, $s10:expr $(,)?) => {
146        $crate::concat_atom10(&$s1, &$s2, &$s3, &$s4, &$s5, &$s6, &$s7, &$s8, &$s9, &$s10)
147    };
148    ($s1:expr, $s2:expr, $s3:expr, $s4:expr, $s5:expr, $s6:expr, $s7:expr, $s8:expr, $s9:expr, $s10:expr, $s11:expr $(,)?) => {
149        $crate::concat_atom11(&$s1, &$s2, &$s3, &$s4, &$s5, &$s6, &$s7, &$s8, &$s9, &$s10, &$s11)
150    };
151    ($s1:expr, $s2:expr, $s3:expr, $s4:expr, $s5:expr, $s6:expr, $s7:expr, $s8:expr, $s9:expr, $s10:expr, $s11:expr, $s12:expr $(,)?) => {
152        $crate::concat_atom12(&$s1, &$s2, &$s3, &$s4, &$s5, &$s6, &$s7, &$s8, &$s9, &$s10, &$s11, &$s12)
153    };
154    ($($arg:expr),+ $(,)?) => {
155        compile_error!("concat_atom! macro supports between 2 and 12 arguments only")
156    };
157}
158
159/// Creates an `Atom` from a constant name, lowercasing only the namespace part.
160///
161/// This function is optimized to avoid heap allocations for constant names up to
162/// `STACK_BUF_SIZE` bytes by building the new string on the stack. For names
163/// longer than the buffer, it falls back to a heap allocation.
164#[inline]
165#[must_use]
166pub fn ascii_lowercase_constant_name_atom(name: &str) -> Atom {
167    if let Some(last_slash_idx) = name.rfind('\\') {
168        let (namespace, const_name) = name.split_at(last_slash_idx);
169        let const_name = &const_name[1..];
170
171        if name.len() > STACK_BUF_SIZE {
172            let mut lowercased_namespace = namespace.to_ascii_lowercase();
173            lowercased_namespace.push('\\');
174            lowercased_namespace.push_str(const_name);
175            return atom(&lowercased_namespace);
176        }
177
178        let mut stack_buf = [0u8; STACK_BUF_SIZE];
179        let mut index = 0;
180
181        for byte in namespace.bytes() {
182            stack_buf[index] = byte.to_ascii_lowercase();
183            index += 1;
184        }
185
186        stack_buf[index] = b'\\';
187        index += 1;
188
189        let const_bytes = const_name.as_bytes();
190        stack_buf[index..index + const_bytes.len()].copy_from_slice(const_bytes);
191        index += const_bytes.len();
192
193        atom(
194            // SAFETY: We only write valid UTF-8 bytes into the stack buffer.
195            unsafe { std::str::from_utf8_unchecked(&stack_buf[..index]) },
196        )
197    } else {
198        atom(name)
199    }
200}
201
202/// Creates an `Atom` from a lowercased version of a string slice.
203///
204/// This function is highly optimized. It performs a fast scan, and if the string
205/// is already lowercase, it returns an `Atom` without any new allocations.
206/// Otherwise, it builds the lowercase version on the stack for strings up to
207/// `STACK_BUF_SIZE` bytes.
208#[inline]
209#[must_use]
210pub fn ascii_lowercase_atom(s: &str) -> Atom {
211    let bytes = s.as_bytes();
212
213    // Fast path: check if all ASCII is already lowercase
214    if !bytes.iter().any(u8::is_ascii_uppercase) {
215        return atom(s);
216    }
217
218    // Fast path for short strings, use a stack buffer
219    if s.len() <= STACK_BUF_SIZE {
220        let mut stack_buf = [0u8; STACK_BUF_SIZE];
221        for (i, &b) in bytes.iter().enumerate() {
222            stack_buf[i] = b.to_ascii_lowercase();
223        }
224        return atom(
225            // SAFETY: ASCII lowercase of ASCII bytes is valid UTF-8
226            unsafe { std::str::from_utf8_unchecked(&stack_buf[..s.len()]) },
227        );
228    }
229
230    atom(&s.to_ascii_lowercase())
231}
232
233/// Checks if `haystack` starts with `prefix`, ignoring ASCII case.
234///
235/// This function uses SIMD instructions (AVX2 on `x86_64`, NEON on aarch64)
236/// when available and beneficial for the input size.
237///
238/// # Examples
239///
240/// ```
241/// use mago_atom::starts_with_ignore_case;
242///
243/// assert!(starts_with_ignore_case("HelloWorld", "hello"));
244/// assert!(starts_with_ignore_case("FOOBAR", "FooBar"));
245/// assert!(starts_with_ignore_case("test", "TEST"));
246/// assert!(starts_with_ignore_case("abcdefghijklmnop", "ABCDEFGHIJKLMNOP"));
247/// assert!(starts_with_ignore_case("abcdefghijklmnopqrstuvwxyzabcdef", "ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEF"));
248/// assert!(!starts_with_ignore_case("hello", "world"));
249/// assert!(!starts_with_ignore_case("hi", "hello"));
250/// ```
251#[inline]
252#[must_use]
253pub fn starts_with_ignore_case(haystack: &str, prefix: &str) -> bool {
254    #[cfg(target_arch = "x86_64")]
255    #[target_feature(enable = "avx2")]
256    unsafe fn starts_with_avx2(haystack: &str, prefix: &str, len: usize) -> bool {
257        #[allow(clippy::multiple_unsafe_ops_per_block)]
258        // SAFETY: caller has verified AVX2 is available and `haystack.len() >= len`; loads of 32 bytes from
259        // `haystack[i..i+32]` and `prefix[i..i+32]` stay within bounds because the loop guard is `i + 32 <= len`.
260        unsafe {
261            let haystack_bytes = haystack.as_bytes();
262            let prefix_bytes = prefix.as_bytes();
263
264            let upper_a = _mm256_set1_epi8(b'A' as i8);
265            let upper_z = _mm256_set1_epi8(b'Z' as i8);
266            let case_bit = _mm256_set1_epi8(0x20);
267
268            let mut i = 0;
269            while i + 32 <= len {
270                // SAFETY: `_mm256_loadu_si256` performs an unaligned load, so the pointer
271                // need not satisfy `__m256i`'s 32-byte alignment requirement.
272                #[allow(clippy::cast_ptr_alignment)]
273                let h = _mm256_loadu_si256(haystack_bytes.as_ptr().add(i).cast::<__m256i>());
274                #[allow(clippy::cast_ptr_alignment)]
275                let p = _mm256_loadu_si256(prefix_bytes.as_ptr().add(i).cast::<__m256i>());
276
277                // Convert haystack chunk to lowercase
278                let h_is_upper = _mm256_and_si256(
279                    _mm256_cmpgt_epi8(h, _mm256_sub_epi8(upper_a, _mm256_set1_epi8(1))),
280                    _mm256_cmpgt_epi8(_mm256_add_epi8(upper_z, _mm256_set1_epi8(1)), h),
281                );
282                let h_lower = _mm256_or_si256(h, _mm256_and_si256(h_is_upper, case_bit));
283
284                // Convert prefix chunk to lowercase
285                let p_is_upper = _mm256_and_si256(
286                    _mm256_cmpgt_epi8(p, _mm256_sub_epi8(upper_a, _mm256_set1_epi8(1))),
287                    _mm256_cmpgt_epi8(_mm256_add_epi8(upper_z, _mm256_set1_epi8(1)), p),
288                );
289                let p_lower = _mm256_or_si256(p, _mm256_and_si256(p_is_upper, case_bit));
290
291                let eq = _mm256_cmpeq_epi8(h_lower, p_lower);
292                let mask = _mm256_movemask_epi8(eq);
293                if mask != -1i32 {
294                    return false;
295                }
296
297                i += 32;
298            }
299
300            // Handle remaining bytes
301            haystack_bytes[i..len].eq_ignore_ascii_case(&prefix_bytes[i..len])
302        }
303    }
304
305    #[cfg(target_arch = "aarch64")]
306    #[target_feature(enable = "neon")]
307    unsafe fn starts_with_neon(haystack: &str, prefix: &str, len: usize) -> bool {
308        #[allow(clippy::multiple_unsafe_ops_per_block)]
309        // SAFETY: NEON is always available on aarch64 and the caller has verified `haystack.len() >= len`; loads of
310        // 16 bytes from `haystack[i..i+16]` and `prefix[i..i+16]` stay within bounds because the loop guard is
311        // `i + 16 <= len`.
312        unsafe {
313            let haystack_bytes = haystack.as_bytes();
314            let prefix_bytes = prefix.as_bytes();
315
316            let upper_a = vdupq_n_u8(b'A');
317            let upper_z = vdupq_n_u8(b'Z');
318            let case_bit = vdupq_n_u8(0x20);
319
320            let mut i = 0;
321            while i + 16 <= len {
322                let h = vld1q_u8(haystack_bytes.as_ptr().add(i));
323                let p = vld1q_u8(prefix_bytes.as_ptr().add(i));
324
325                // Convert haystack chunk to lowercase
326                let h_ge_a = vcgeq_u8(h, upper_a);
327                let h_le_z = vcleq_u8(h, upper_z);
328                let h_is_upper = vandq_u8(h_ge_a, h_le_z);
329                let h_lower = vorrq_u8(h, vandq_u8(h_is_upper, case_bit));
330
331                // Convert prefix chunk to lowercase
332                let p_ge_a = vcgeq_u8(p, upper_a);
333                let p_le_z = vcleq_u8(p, upper_z);
334                let p_is_upper = vandq_u8(p_ge_a, p_le_z);
335                let p_lower = vorrq_u8(p, vandq_u8(p_is_upper, case_bit));
336
337                let eq = vceqq_u8(h_lower, p_lower);
338                let min = vminvq_u8(eq);
339                if min != 0xFF {
340                    return false;
341                }
342
343                i += 16;
344            }
345
346            // Handle remaining bytes
347            haystack_bytes[i..len].eq_ignore_ascii_case(&prefix_bytes[i..len])
348        }
349    }
350
351    let len = prefix.len();
352    if haystack.len() < len {
353        return false;
354    }
355
356    #[cfg(target_arch = "x86_64")]
357    {
358        if len >= 32 && std::is_x86_feature_detected!("avx2") {
359            // SAFETY: we've checked that AVX2 is available and haystack.len() >= len
360            return unsafe { starts_with_avx2(haystack, prefix, len) };
361        }
362    }
363
364    #[cfg(target_arch = "aarch64")]
365    {
366        if len >= 16 {
367            // SAFETY: NEON is always available on aarch64 and haystack.len() >= len
368            return unsafe { starts_with_neon(haystack, prefix, len) };
369        }
370    }
371
372    haystack.as_bytes()[..len].eq_ignore_ascii_case(prefix.as_bytes())
373}
374
375/// A helper macro to generate the specialized `*_atom` functions for integer types.
376macro_rules! integer_to_atom_fns {
377    ( $( $func_name:ident($num_type:ty) ),+ $(,)? ) => {
378        $(
379            #[doc = "Creates an `Atom` from a `"]
380            #[doc = stringify!($num_type)]
381            #[doc = "` value with zero heap allocations."]
382            #[inline]
383            #[must_use]
384            pub fn $func_name(n: $num_type) -> Atom {
385                let mut buffer = itoa::Buffer::new();
386                let s = buffer.format(n);
387
388                atom(s)
389            }
390        )+
391    };
392}
393
394/// A helper macro to generate the specialized `*_atom` functions for float types.
395macro_rules! float_to_atom_fns {
396    ( $( $func_name:ident($num_type:ty) ),+ $(,)? ) => {
397        $(
398            #[doc = "Creates an `Atom` from a `"]
399            #[doc = stringify!($num_type)]
400            #[doc = "` value with zero heap allocations."]
401            #[inline]
402            #[must_use]
403            pub fn $func_name(n: $num_type) -> Atom {
404                let mut buffer = ryu::Buffer::new();
405                let s = buffer.format(n);
406
407                atom(s)
408            }
409        )+
410    };
411}
412
413/// A helper macro to generate the specialized `concat_atomN` functions.
414macro_rules! concat_fns {
415    ( $( $func_name:ident($n:literal, $($s:ident),+) ),+ $(,)?) => {
416        $(
417            #[doc = "Creates an `Atom` as a result of concatenating "]
418            #[doc = stringify!($n)]
419            #[doc = " string slices."]
420            #[inline]
421            #[must_use]
422            #[allow(unused_assignments)]
423            #[allow(clippy::too_many_arguments)]
424            pub fn $func_name($($s: &str),+) -> Atom {
425                let total_len = 0 $(+ $s.len())+;
426
427                if total_len <= STACK_BUF_SIZE {
428                    let mut buffer = [0u8; STACK_BUF_SIZE];
429                    let mut index = 0;
430                    $(
431                        buffer[index..index + $s.len()].copy_from_slice($s.as_bytes());
432                        index += $s.len();
433                    )+
434
435                    return atom(
436                        // SAFETY: every byte written to `buffer` came from `&str::as_bytes()`, so the
437                        // sub-slice `&buffer[..total_len]` is a concatenation of valid UTF-8 sequences.
438                        unsafe { std::str::from_utf8_unchecked(&buffer[..total_len]) },
439                    );
440                }
441
442                // Fallback to heap for very long strings.
443                let mut result = String::with_capacity(total_len);
444                $( result.push_str($s); )+
445                atom(&result)
446            }
447        )+
448    };
449}
450
451// Generate functions for integer types
452integer_to_atom_fns!(
453    i8_atom(i8),
454    i16_atom(i16),
455    i32_atom(i32),
456    i64_atom(i64),
457    i128_atom(i128),
458    isize_atom(isize),
459    u8_atom(u8),
460    u16_atom(u16),
461    u32_atom(u32),
462    u64_atom(u64),
463    u128_atom(u128),
464    usize_atom(usize),
465);
466
467float_to_atom_fns!(f32_atom(f32), f64_atom(f64),);
468
469concat_fns!(
470    concat_atom2(2, s1, s2),
471    concat_atom3(3, s1, s2, s3),
472    concat_atom4(4, s1, s2, s3, s4),
473    concat_atom5(5, s1, s2, s3, s4, s5),
474    concat_atom6(6, s1, s2, s3, s4, s5, s6),
475    concat_atom7(7, s1, s2, s3, s4, s5, s6, s7),
476    concat_atom8(8, s1, s2, s3, s4, s5, s6, s7, s8),
477    concat_atom9(9, s1, s2, s3, s4, s5, s6, s7, s8, s9),
478    concat_atom10(10, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10),
479    concat_atom11(11, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11),
480    concat_atom12(12, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11, s12),
481);