use crate::token::ATOM_DYNAMIC_BIT;
use crate::{AtomSet, DynAtomSet};
use fnv::FnvHashMap;
use std::fmt::{Debug, Formatter, Result};
use std::marker::PhantomData;
use std::sync::RwLock;
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct Atom<T: AtomSet> {
bits: u32,
_phantom: PhantomData<T>,
}
impl<T: AtomSet> Atom<T> {
#[inline]
const fn new(bits: u32) -> Self {
Self { bits, _phantom: PhantomData }
}
#[inline]
pub const fn as_bits(self) -> u32 {
self.bits
}
#[inline]
pub const fn is_dynamic(self) -> bool {
self.bits & (1 << ATOM_DYNAMIC_BIT) != 0
}
#[inline]
pub const fn is_static(self) -> bool {
!self.is_dynamic()
}
}
pub struct DynAtomRegistry<T: AtomSet> {
static_atoms: T,
str_to_bits_map: RwLock<FnvHashMap<&'static str, u32>>,
bits_to_str_vec: RwLock<Vec<&'static str>>,
}
impl<T: AtomSet> DynAtomRegistry<T> {
#[doc(hidden)]
pub fn new() -> Self {
Self {
static_atoms: T::default(),
str_to_bits_map: RwLock::new(FnvHashMap::default()),
bits_to_str_vec: RwLock::new(Vec::new()),
}
}
pub fn atom_from_str(&self, s: &str) -> Atom<T> {
Atom::new(self.str_to_bits(s))
}
pub fn atom_from_bits(&self, bits: u32) -> Atom<T> {
Atom::new(bits)
}
pub fn atom_to_str(&self, atom: Atom<T>) -> Option<&'static str> {
let bits = atom.as_bits();
if DynAtomRegistry::<T>::is_dynamic(bits) { self.lookup(bits) } else { Some(T::from_bits(bits).to_str()) }
}
fn atomize(&self, s: &str) -> u32 {
if let Some(&bits) = self.str_to_bits_map.read().unwrap().get(s) {
return bits;
}
let mut str_to_bits = self.str_to_bits_map.write().unwrap();
let mut bits_to_str = self.bits_to_str_vec.write().unwrap();
if let Some(&bits) = str_to_bits.get(s) {
return bits;
}
let id = bits_to_str.len() as u32;
let bits = id | (1 << ATOM_DYNAMIC_BIT);
let static_str: &'static str = Box::leak(s.into());
bits_to_str.push(static_str);
str_to_bits.insert(static_str, bits);
bits
}
fn lookup(&self, bits: u32) -> Option<&'static str> {
debug_assert!(Self::is_dynamic(bits), "lookup() called on static atom bits");
let index = (bits & ((1 << ATOM_DYNAMIC_BIT) - 1)) as usize; self.bits_to_str_vec.read().unwrap().get(index).copied()
}
#[inline]
pub const fn is_dynamic(bits: u32) -> bool {
bits & (1 << ATOM_DYNAMIC_BIT) != 0
}
}
impl<T: AtomSet> Default for DynAtomRegistry<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: AtomSet + Debug> Debug for DynAtomRegistry<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
f.debug_struct("DynAtomRegistry").field("static_atoms", &self.static_atoms).finish_non_exhaustive()
}
}
impl<T: AtomSet> DynAtomSet for DynAtomRegistry<T> {
fn str_to_bits(&self, keyword: &str) -> u32 {
let static_atom = T::from_str(keyword);
let default_bits = T::default().as_bits();
if static_atom.as_bits() != default_bits {
let bits = static_atom.as_bits();
debug_assert!(bits & (1 << ATOM_DYNAMIC_BIT) == 0, "Static atoms must have dynamic bit = 0");
return bits;
}
let bits = self.atomize(keyword);
debug_assert!(bits & (1 << ATOM_DYNAMIC_BIT) != 0, "Dynamic atoms must have dynamic bit = 1");
bits
}
fn bits_to_str(&self, bits: u32) -> &'static str {
if Self::is_dynamic(bits) {
return self.lookup(bits).unwrap_or_else(|| T::from_bits(0).to_str());
}
T::from_bits(bits).to_str()
}
fn bits(&self) -> u32 {
self.static_atoms.as_bits()
}
}
pub trait RegisteredAtomSet: AtomSet + 'static {
fn get_dyn_set() -> &'static DynAtomRegistry<Self>;
}
#[macro_export]
macro_rules! register_atom_set {
($atom_type:ty) => {
static __ATOM_SET: ::std::sync::LazyLock<$crate::DynAtomRegistry<$atom_type>> =
::std::sync::LazyLock::new(|| $crate::DynAtomRegistry::new());
impl $crate::RegisteredAtomSet for $atom_type {
#[inline]
fn get_dyn_set() -> &'static $crate::DynAtomRegistry<Self> {
&__ATOM_SET
}
}
};
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Default, Copy, Clone, PartialEq)]
enum TestAtomSet {
#[default]
None,
Px,
Rem,
}
impl AtomSet for TestAtomSet {
fn from_str(keyword: &str) -> Self {
match keyword {
"px" => Self::Px,
"rem" => Self::Rem,
_ => Self::None,
}
}
fn to_str(self) -> &'static str {
match self {
Self::None => "",
Self::Px => "px",
Self::Rem => "rem",
}
}
fn len(&self) -> u32 {
self.to_str().len() as u32
}
fn from_bits(bits: u32) -> Self {
match bits {
0 => Self::None,
1 => Self::Px,
2 => Self::Rem,
_ => Self::None,
}
}
fn as_bits(&self) -> u32 {
*self as u32
}
}
#[test]
fn test_static_atoms_have_dynamic_bit_zero() {
let combined = DynAtomRegistry::<TestAtomSet>::new();
let px_bits = combined.str_to_bits("px");
assert_eq!(px_bits & (1 << ATOM_DYNAMIC_BIT), 0, "Static atoms should have dynamic bit = 0");
let rem_bits = combined.str_to_bits("rem");
assert_eq!(rem_bits & (1 << ATOM_DYNAMIC_BIT), 0, "Static atoms should have dynamic bit = 0");
}
#[test]
fn test_dynamic_atoms_have_dynamic_bit_one() {
let combined = DynAtomRegistry::<TestAtomSet>::new();
let custom_bits = combined.str_to_bits("custom");
assert_eq!(
custom_bits & (1 << ATOM_DYNAMIC_BIT),
1 << ATOM_DYNAMIC_BIT,
"Dynamic atoms should have dynamic bit = 1"
);
}
#[test]
fn test_dynamic_atom_interning() {
let combined = DynAtomRegistry::<TestAtomSet>::new();
let bits1 = combined.str_to_bits("custom");
let bits2 = combined.str_to_bits("custom");
assert_eq!(bits1, bits2, "Same string should atomize to same bits");
}
#[test]
fn test_lookup() {
let combined = DynAtomRegistry::<TestAtomSet>::new();
let custom_bits = combined.str_to_bits("my-custom-value");
assert_eq!(combined.lookup(custom_bits), Some("my-custom-value"));
}
#[test]
fn test_different_dynamic_atoms_get_different_bits() {
let combined = DynAtomRegistry::<TestAtomSet>::new();
let bits1 = combined.str_to_bits("custom1");
let bits2 = combined.str_to_bits("custom2");
assert_ne!(bits1, bits2, "Different strings should get different bits");
assert_eq!(bits1 & (1 << ATOM_DYNAMIC_BIT), 1 << ATOM_DYNAMIC_BIT);
assert_eq!(bits2 & (1 << ATOM_DYNAMIC_BIT), 1 << ATOM_DYNAMIC_BIT);
}
#[test]
fn test_typed_atoms() {
let combined = DynAtomRegistry::<TestAtomSet>::new();
let px_atom: Atom<TestAtomSet> = combined.atom_from_str("px");
assert!(px_atom.is_static());
assert!(!px_atom.is_dynamic());
assert_eq!(combined.atom_to_str(px_atom), Some("px"));
let custom_atom: Atom<TestAtomSet> = combined.atom_from_str("custom");
assert!(custom_atom.is_dynamic());
assert!(!custom_atom.is_static());
assert_eq!(combined.atom_to_str(custom_atom), Some("custom"));
assert_ne!(px_atom, custom_atom);
let custom_atom2 = combined.atom_from_str("custom");
assert_eq!(custom_atom, custom_atom2);
}
#[test]
fn test_atom_from_bits() {
let combined = DynAtomRegistry::<TestAtomSet>::new();
let px_atom = combined.atom_from_str("px");
let px_bits = px_atom.as_bits();
let px_atom2 = combined.atom_from_bits(px_bits);
assert_eq!(px_atom, px_atom2);
assert_eq!(combined.atom_to_str(px_atom2), Some("px"));
}
}