css_lexer/
dyn_atom_registry.rs1use crate::token::ATOM_DYNAMIC_BIT;
2use atom_set::{AtomSet, DynAtomSet};
3use fnv::FnvHashMap;
4use std::fmt::{Debug, Formatter, Result};
5use std::marker::PhantomData;
6use std::sync::RwLock;
7
8#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
10pub struct Atom<T: AtomSet> {
11 bits: u32,
12 _phantom: PhantomData<T>,
13}
14
15impl<T: AtomSet> Atom<T> {
16 #[inline]
18 const fn new(bits: u32) -> Self {
19 Self { bits, _phantom: PhantomData }
20 }
21
22 #[inline]
24 pub const fn as_bits(self) -> u32 {
25 self.bits
26 }
27
28 #[inline]
30 pub const fn is_dynamic(self) -> bool {
31 self.bits & (1 << ATOM_DYNAMIC_BIT) != 0
32 }
33
34 #[inline]
36 pub const fn is_static(self) -> bool {
37 !self.is_dynamic()
38 }
39}
40
41pub struct DynAtomRegistry<T: AtomSet> {
72 static_atoms: T,
73 str_to_bits_map: RwLock<FnvHashMap<&'static str, u32>>,
74 bits_to_str_vec: RwLock<Vec<&'static str>>,
75}
76
77impl<T: AtomSet> DynAtomRegistry<T> {
78 #[doc(hidden)]
79 pub fn new() -> Self {
80 Self {
81 static_atoms: T::default(),
82 str_to_bits_map: RwLock::new(FnvHashMap::default()),
83 bits_to_str_vec: RwLock::new(Vec::new()),
84 }
85 }
86
87 pub fn atom_from_str(&self, s: &str) -> Atom<T> {
89 Atom::new(self.str_to_bits(s))
90 }
91
92 pub fn atom_from_bits(&self, bits: u32) -> Atom<T> {
94 Atom::new(bits)
95 }
96
97 pub fn atom_to_str(&self, atom: Atom<T>) -> Option<&'static str> {
99 let bits = atom.as_bits();
100 if DynAtomRegistry::<T>::is_dynamic(bits) { self.lookup(bits) } else { Some(T::from_bits(bits).to_str()) }
101 }
102
103 fn atomize(&self, s: &str) -> u32 {
105 if let Some(&bits) = self.str_to_bits_map.read().unwrap().get(s) {
106 return bits;
107 }
108
109 let mut str_to_bits = self.str_to_bits_map.write().unwrap();
110 let mut bits_to_str = self.bits_to_str_vec.write().unwrap();
111
112 if let Some(&bits) = str_to_bits.get(s) {
113 return bits;
114 }
115
116 let id = bits_to_str.len() as u32;
117 let bits = id | (1 << ATOM_DYNAMIC_BIT); let static_str: &'static str = Box::leak(s.into());
123 bits_to_str.push(static_str);
124 str_to_bits.insert(static_str, bits);
125
126 bits
127 }
128
129 fn lookup(&self, bits: u32) -> Option<&'static str> {
131 debug_assert!(Self::is_dynamic(bits), "lookup() called on static atom bits");
132 let index = (bits & ((1 << ATOM_DYNAMIC_BIT) - 1)) as usize; self.bits_to_str_vec.read().unwrap().get(index).copied()
134 }
135
136 #[inline]
138 pub const fn is_dynamic(bits: u32) -> bool {
139 bits & (1 << ATOM_DYNAMIC_BIT) != 0
140 }
141}
142
143impl<T: AtomSet> Default for DynAtomRegistry<T> {
144 fn default() -> Self {
145 Self::new()
146 }
147}
148
149impl<T: AtomSet + Debug> Debug for DynAtomRegistry<T> {
150 fn fmt(&self, f: &mut Formatter<'_>) -> Result {
151 f.debug_struct("DynAtomRegistry").field("static_atoms", &self.static_atoms).finish_non_exhaustive()
152 }
153}
154
155impl<T: AtomSet> DynAtomSet for DynAtomRegistry<T> {
156 fn str_to_bits(&self, keyword: &str) -> u32 {
157 let static_atom = T::from_str(keyword);
158 let default_bits = T::default().as_bits();
159
160 if static_atom.as_bits() != default_bits {
161 let bits = static_atom.as_bits();
162 debug_assert!(bits & (1 << ATOM_DYNAMIC_BIT) == 0, "Static atoms must have dynamic bit = 0");
163 return bits;
164 }
165
166 let bits = self.atomize(keyword);
167 debug_assert!(bits & (1 << ATOM_DYNAMIC_BIT) != 0, "Dynamic atoms must have dynamic bit = 1");
168 bits
169 }
170
171 fn bits_to_str(&self, bits: u32) -> &'static str {
172 if Self::is_dynamic(bits) {
173 return self.lookup(bits).unwrap_or_else(|| T::from_bits(0).to_str());
174 }
175 T::from_bits(bits).to_str()
176 }
177
178 fn bits(&self) -> u32 {
179 self.static_atoms.as_bits()
180 }
181}
182
183pub trait RegisteredAtomSet: AtomSet + 'static {
185 fn get_dyn_set() -> &'static DynAtomRegistry<Self>;
187}
188
189#[macro_export]
214macro_rules! register_atom_set {
215 ($atom_type:ty) => {
216 static __ATOM_SET: ::std::sync::LazyLock<$crate::DynAtomRegistry<$atom_type>> =
217 ::std::sync::LazyLock::new(|| $crate::DynAtomRegistry::new());
218
219 impl $crate::RegisteredAtomSet for $atom_type {
220 #[inline]
221 fn get_dyn_set() -> &'static $crate::DynAtomRegistry<Self> {
222 &__ATOM_SET
223 }
224 }
225 };
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[derive(Debug, Default, Copy, Clone, PartialEq)]
233 enum TestAtomSet {
234 #[default]
235 None,
236 Px,
237 Rem,
238 }
239
240 impl AtomSet for TestAtomSet {
241 fn from_str(keyword: &str) -> Self {
242 match keyword {
243 "px" => Self::Px,
244 "rem" => Self::Rem,
245 _ => Self::None,
246 }
247 }
248
249 fn to_str(self) -> &'static str {
250 match self {
251 Self::None => "",
252 Self::Px => "px",
253 Self::Rem => "rem",
254 }
255 }
256
257 fn len(&self) -> u32 {
258 self.to_str().len() as u32
259 }
260
261 fn from_bits(bits: u32) -> Self {
262 match bits {
263 0 => Self::None,
264 1 => Self::Px,
265 2 => Self::Rem,
266 _ => Self::None,
267 }
268 }
269
270 fn as_bits(&self) -> u32 {
271 *self as u32
272 }
273 }
274
275 #[test]
276 fn test_static_atoms_have_dynamic_bit_zero() {
277 let combined = DynAtomRegistry::<TestAtomSet>::new();
278
279 let px_bits = combined.str_to_bits("px");
280 assert_eq!(px_bits & (1 << ATOM_DYNAMIC_BIT), 0, "Static atoms should have dynamic bit = 0");
281
282 let rem_bits = combined.str_to_bits("rem");
283 assert_eq!(rem_bits & (1 << ATOM_DYNAMIC_BIT), 0, "Static atoms should have dynamic bit = 0");
284 }
285
286 #[test]
287 fn test_dynamic_atoms_have_dynamic_bit_one() {
288 let combined = DynAtomRegistry::<TestAtomSet>::new();
289
290 let custom_bits = combined.str_to_bits("custom");
291 assert_eq!(
292 custom_bits & (1 << ATOM_DYNAMIC_BIT),
293 1 << ATOM_DYNAMIC_BIT,
294 "Dynamic atoms should have dynamic bit = 1"
295 );
296 }
297
298 #[test]
299 fn test_dynamic_atom_interning() {
300 let combined = DynAtomRegistry::<TestAtomSet>::new();
301
302 let bits1 = combined.str_to_bits("custom");
303 let bits2 = combined.str_to_bits("custom");
304
305 assert_eq!(bits1, bits2, "Same string should atomize to same bits");
306 }
307
308 #[test]
309 fn test_lookup() {
310 let combined = DynAtomRegistry::<TestAtomSet>::new();
311
312 let custom_bits = combined.str_to_bits("my-custom-value");
313
314 assert_eq!(combined.lookup(custom_bits), Some("my-custom-value"));
315 }
316
317 #[test]
318 fn test_different_dynamic_atoms_get_different_bits() {
319 let combined = DynAtomRegistry::<TestAtomSet>::new();
320
321 let bits1 = combined.str_to_bits("custom1");
322 let bits2 = combined.str_to_bits("custom2");
323
324 assert_ne!(bits1, bits2, "Different strings should get different bits");
325 assert_eq!(bits1 & (1 << ATOM_DYNAMIC_BIT), 1 << ATOM_DYNAMIC_BIT);
326 assert_eq!(bits2 & (1 << ATOM_DYNAMIC_BIT), 1 << ATOM_DYNAMIC_BIT);
327 }
328
329 #[test]
330 fn test_typed_atoms() {
331 let combined = DynAtomRegistry::<TestAtomSet>::new();
332
333 let px_atom: Atom<TestAtomSet> = combined.atom_from_str("px");
335 assert!(px_atom.is_static());
336 assert!(!px_atom.is_dynamic());
337 assert_eq!(combined.atom_to_str(px_atom), Some("px"));
338
339 let custom_atom: Atom<TestAtomSet> = combined.atom_from_str("custom");
341 assert!(custom_atom.is_dynamic());
342 assert!(!custom_atom.is_static());
343 assert_eq!(combined.atom_to_str(custom_atom), Some("custom"));
344
345 assert_ne!(px_atom, custom_atom);
347
348 let custom_atom2 = combined.atom_from_str("custom");
350 assert_eq!(custom_atom, custom_atom2);
351 }
352
353 #[test]
354 fn test_atom_from_bits() {
355 let combined = DynAtomRegistry::<TestAtomSet>::new();
356
357 let px_atom = combined.atom_from_str("px");
358 let px_bits = px_atom.as_bits();
359
360 let px_atom2 = combined.atom_from_bits(px_bits);
361 assert_eq!(px_atom, px_atom2);
362 assert_eq!(combined.atom_to_str(px_atom2), Some("px"));
363 }
364}