meta-ast 0.3.0

Polyglot static-analysis engine: extract symbols and cross-language dependency graphs from 8 supported source languages, with optional MetaCall deployment manifest generation.
Documentation
//! Newtyped ID types with thread-safe generation.
//!
//! `FileId`, `SymbolId`, and `SnapshotId` are newtyped `u32` values
//! generated by `IdGenerator<T>` which wraps an `AtomicU32` for
//! lock-free allocation across rayon worker threads.

use std::sync::atomic::{AtomicU32, Ordering};

use serde::{Deserialize, Serialize};

macro_rules! define_id_type {
    ($name:ident) => {
        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
        #[serde(transparent)]
        pub struct $name(pub u32);

        impl $name {
            pub fn to_raw(self) -> u32 {
                self.0
            }
        }
    };
}

define_id_type!(FileId);
define_id_type!(SymbolId);
define_id_type!(SnapshotId);

#[derive(Debug)]
pub struct IdGenerator<T> {
    counter: AtomicU32,
    _marker: std::marker::PhantomData<T>,
}

impl<T> IdGenerator<T> {
    pub fn new() -> Self {
        Self {
            counter: AtomicU32::new(0),
            _marker: std::marker::PhantomData,
        }
    }

    pub fn next(&self) -> T
    where
        T: From<u32>,
    {
        let val = self.counter.fetch_add(1, Ordering::SeqCst);
        T::from(val)
    }
}

impl<T> Default for IdGenerator<T> {
    fn default() -> Self {
        Self::new()
    }
}

impl From<u32> for FileId {
    fn from(val: u32) -> Self {
        Self(val)
    }
}

impl From<u32> for SymbolId {
    fn from(val: u32) -> Self {
        Self(val)
    }
}

impl From<u32> for SnapshotId {
    fn from(val: u32) -> Self {
        Self(val)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::HashSet;
    use std::sync::Arc;

    #[test]
    fn file_id_sequential() {
        let idgen = IdGenerator::<FileId>::new();
        assert_eq!(idgen.next(), FileId(0));
        assert_eq!(idgen.next(), FileId(1));
        assert_eq!(idgen.next(), FileId(2));
    }

    #[test]
    fn symbol_id_sequential() {
        let idgen = IdGenerator::<SymbolId>::new();
        assert_eq!(idgen.next(), SymbolId(0));
        assert_eq!(idgen.next(), SymbolId(1));
        assert_eq!(idgen.next(), SymbolId(2));
    }

    #[test]
    fn snapshot_id_sequential() {
        let idgen = IdGenerator::<SnapshotId>::new();
        assert_eq!(idgen.next(), SnapshotId(0));
        assert_eq!(idgen.next(), SnapshotId(1));
        assert_eq!(idgen.next(), SnapshotId(2));
    }

    #[test]
    fn id_generator_thread_safe() {
        let idgen = Arc::new(IdGenerator::<SymbolId>::new());
        let mut handles = Vec::new();

        for _ in 0..4 {
            let idgen = Arc::clone(&idgen);
            handles.push(std::thread::spawn(move || {
                let mut ids = Vec::with_capacity(250);
                for _ in 0..250 {
                    ids.push(idgen.next());
                }
                ids
            }));
        }

        let all_ids: Vec<SymbolId> = handles
            .into_iter()
            .flat_map(|h| h.join().unwrap_or_default())
            .collect();

        let unique: HashSet<SymbolId> = all_ids.iter().copied().collect();
        assert_eq!(unique.len(), 1000, "expected 1000 unique IDs");
        assert!(
            unique.iter().all(|id| id.to_raw() < 1000),
            "all IDs must be in range 0..1000"
        );
    }

    #[test]
    fn file_id_serde_roundtrip() {
        let original = FileId(42);
        let json = serde_json::to_string(&original).unwrap();
        let roundtrip: FileId = serde_json::from_str(&json).unwrap();
        assert_eq!(original, roundtrip);
    }

    #[test]
    fn symbol_id_serde_roundtrip() {
        let original = SymbolId(99);
        let json = serde_json::to_string(&original).unwrap();
        let roundtrip: SymbolId = serde_json::from_str(&json).unwrap();
        assert_eq!(original, roundtrip);
    }

    #[test]
    fn file_id_to_raw() {
        assert_eq!(FileId(7).to_raw(), 7);
    }

    #[test]
    fn id_generator_default() {
        let idgen = IdGenerator::<FileId>::default();
        assert_eq!(idgen.next(), FileId(0));
    }
}