Skip to main content

css_lexer/
dyn_atom_registry.rs

1use 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/// A typed atom that belongs to a specific `DynAtomRegistry<T>`.
9#[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	/// Creates an atom from raw bits.
17	#[inline]
18	const fn new(bits: u32) -> Self {
19		Self { bits, _phantom: PhantomData }
20	}
21
22	/// Returns the raw bit representation.
23	#[inline]
24	pub const fn as_bits(self) -> u32 {
25		self.bits
26	}
27
28	/// Returns true if this is a dynamic (runtime-interned) atom.
29	#[inline]
30	pub const fn is_dynamic(self) -> bool {
31		self.bits & (1 << ATOM_DYNAMIC_BIT) != 0
32	}
33
34	/// Returns true if this is a static atom.
35	#[inline]
36	pub const fn is_static(self) -> bool {
37		!self.is_dynamic()
38	}
39}
40
41/// Combines static atoms with dynamic (runtime-interned) atoms.
42///
43/// Each `DynAtomRegistry<T>` should be registered as a singleton using the `register_atom_set!` macro. This ensures
44///
45/// # Example
46///
47/// ```rust
48/// use atom_set::AtomSet;
49/// use css_lexer::{Atom, DynAtomRegistry, RegisteredAtomSet, register_atom_set};
50/// use derive_atom_set::AtomSet as DeriveAtomSet;
51///
52/// #[derive(Debug, Default, Copy, Clone, PartialEq, DeriveAtomSet)]
53/// enum MyAtoms {
54///     #[default]
55///     _None,
56///     Px,
57/// }
58///
59/// // Register singleton
60/// register_atom_set!(MyAtoms);
61///
62/// let atoms = MyAtoms::get_dyn_set();
63///
64/// // Work with typed atoms
65/// let px: Atom<MyAtoms> = atoms.atom_from_str("px");
66/// let custom: Atom<MyAtoms> = atoms.atom_from_str("custom-value");
67///
68/// assert!(px.is_static());
69/// assert!(custom.is_dynamic());
70/// ```
71pub 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	/// Converts a string to a typed atom.
88	pub fn atom_from_str(&self, s: &str) -> Atom<T> {
89		Atom::new(self.str_to_bits(s))
90	}
91
92	/// Converts raw bits to a typed atom.
93	pub fn atom_from_bits(&self, bits: u32) -> Atom<T> {
94		Atom::new(bits)
95	}
96
97	/// Converts an atom back to its string representation. Returns `None` for invalid atoms.
98	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	/// Interns a string, returning its dynamic atom bits.
104	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); // Set dynamic bit
118
119		// Leak the string to get a 'static reference - this is an intern pool
120		// where strings live for the entire program lifetime anyway due to static
121		// lifetime of singleton instances.
122		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	/// Looks up a string by its dynamic atom bits.
130	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; // Mask off dynamic bit to get index
133		self.bits_to_str_vec.read().unwrap().get(index).copied()
134	}
135
136	/// Returns true if the given bits represent a dynamic atom.
137	#[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
183/// Trait for atom types that have a registered singleton `DynAtomRegistry`.
184pub trait RegisteredAtomSet: AtomSet + 'static {
185	/// Returns the singleton instance of the `DynAtomRegistry` for this atom type.
186	fn get_dyn_set() -> &'static DynAtomRegistry<Self>;
187}
188
189/// Registers a singleton `DynAtomRegistry<T>` for the given atom type.
190///
191/// This macro creates a static instance and implements the `RegisteredAtomSet` trait.
192///
193/// # Example
194///
195/// ```rust
196/// use atom_set::AtomSet;
197/// use css_lexer::{RegisteredAtomSet, register_atom_set};
198/// use derive_atom_set::AtomSet as DeriveAtomSet;
199///
200/// #[derive(Debug, Default, Copy, Clone, PartialEq, DeriveAtomSet)]
201/// enum MyAtoms {
202///     #[default]
203///     _None,
204///     Foo,
205/// }
206///
207/// register_atom_set!(MyAtoms);
208///
209/// // Now you can use the singleton
210/// let atoms = MyAtoms::get_dyn_set();
211/// let atom = atoms.atom_from_str("foo");
212/// ```
213#[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		// Static atom
334		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		// Dynamic atom
340		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		// Atoms should not be equal
346		assert_ne!(px_atom, custom_atom);
347
348		// Same string should give same atom
349		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}