Skip to main content

maskforge_core/
primitives.rs

1//! The newtype spine: zero-cost, transparent ids that never appear as a bare integer in new code.
2
3use std::num::TryFromIntError;
4
5use bincode::{Decode, Encode};
6
7/// Builds a `#[repr(transparent)]` newtype over `$inner` with a checked `TryFrom<usize>`.
8macro_rules! id_newtype {
9    ($(#[$meta:meta])* $name:ident($inner:ty)) => {
10        $(#[$meta])*
11        #[repr(transparent)]
12        #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default, Encode, Decode)]
13        pub struct $name(pub $inner);
14
15        impl $name {
16            /// Returns the wrapped integer.
17            #[inline]
18            #[must_use]
19            pub const fn get(self) -> $inner {
20                self.0
21            }
22        }
23
24        impl From<$name> for $inner {
25            #[inline]
26            fn from(id: $name) -> $inner {
27                id.0
28            }
29        }
30
31        impl TryFrom<usize> for $name {
32            type Error = TryFromIntError;
33            /// Fails (never truncates) when `value` exceeds the inner integer's range.
34            #[inline]
35            fn try_from(value: usize) -> Result<Self, Self::Error> {
36                <$inner>::try_from(value).map(Self)
37            }
38        }
39    };
40}
41
42id_newtype!(
43    /// Index of a state in the reference engine (dense `0..n`; `StateId(0)` is canonical DEAD).
44    StateId(u32)
45);
46
47impl StateId {
48    /// Reinterprets caller-owned raw ids as `StateId` values without allocation or copying.
49    /// Avoids rebuilding a `Vec<StateId>` for batch mask queries.
50    #[inline]
51    #[must_use]
52    pub fn cast_slice(raw: &[u32]) -> &[StateId] {
53        // SAFETY: `StateId` is `repr(transparent)` over `u32` with identical layout.
54        // Every `u32` bit pattern is a valid `StateId`.
55        unsafe { std::slice::from_raw_parts(raw.as_ptr().cast::<StateId>(), raw.len()) }
56    }
57}
58id_newtype!(
59    /// Identifier of a vocabulary token.
60    TokenId(u32)
61);
62id_newtype!(
63    /// Identifier of a minterm (a byte-equivalence class in a collapsed alphabet).
64    MintermId(u32)
65);
66id_newtype!(
67    /// Index of a byte class into `ClassTable`.
68    ByteClassId(u16)
69);
70id_newtype!(
71    /// Index of a node in the arena IR. `NodeId(0)` is a valid node; range is checked on decode.
72    NodeId(u32)
73);
74id_newtype!(
75    /// Index of a node in a vocabulary trie. `TrieNodeId(0)` is the root.
76    TrieNodeId(u32)
77);
78
79#[cfg(test)]
80mod tests {
81    use std::mem::{align_of, size_of};
82
83    use super::*;
84
85    #[test]
86    fn newtypes_are_zero_cost() {
87        assert_eq!(size_of::<StateId>(), size_of::<u32>());
88        assert_eq!(align_of::<StateId>(), align_of::<u32>());
89        assert_eq!(size_of::<TokenId>(), size_of::<u32>());
90        assert_eq!(size_of::<MintermId>(), size_of::<u32>());
91        assert_eq!(size_of::<NodeId>(), size_of::<u32>());
92        assert_eq!(align_of::<NodeId>(), align_of::<u32>());
93        assert_eq!(size_of::<ByteClassId>(), size_of::<u16>());
94        assert_eq!(align_of::<ByteClassId>(), align_of::<u16>());
95    }
96
97    #[test]
98    fn cast_slice_matches_element_by_element_construction_with_no_reallocation() {
99        let raw = [0u32, 7, 42, u32::MAX];
100        let cast = StateId::cast_slice(&raw);
101        let built: Vec<StateId> = raw.iter().copied().map(StateId).collect();
102        assert_eq!(cast, &built[..]);
103        assert_eq!(
104            cast.as_ptr().cast::<u32>(),
105            raw.as_ptr(),
106            "cast_slice must reinterpret raw's own allocation, not build a new one"
107        );
108    }
109
110    #[test]
111    fn cast_slice_of_empty_is_empty() {
112        let raw: [u32; 0] = [];
113        assert!(StateId::cast_slice(&raw).is_empty());
114    }
115
116    #[test]
117    fn checked_conversion_fails_out_of_range() {
118        assert_eq!(TokenId::try_from(7usize).map(TokenId::get), Ok(7));
119        if let Some(too_big) = usize::try_from(u32::MAX)
120            .ok()
121            .and_then(|m| m.checked_add(1))
122        {
123            assert!(TokenId::try_from(too_big).is_err());
124        }
125        assert!(ByteClassId::try_from(usize::from(u16::MAX) + 1).is_err());
126        if let Some(too_big) = usize::try_from(u32::MAX)
127            .ok()
128            .and_then(|m| m.checked_add(1))
129        {
130            assert!(NodeId::try_from(too_big).is_err());
131        }
132    }
133}