Skip to main content

saf_core/
ids.rs

1//! Newtype wrappers for SAF entity IDs.
2//!
3//! All IDs are `u128` values derived from BLAKE3 hashes, ensuring deterministic
4//! identification across runs (FR-AIR-002). Newtypes provide type safety and
5//! prevent accidental mixing of different ID kinds.
6
7use std::fmt;
8
9use serde::{Deserialize, Deserializer, Serialize, Serializer};
10
11use crate::id::{id_to_hex, make_id};
12
13/// Common interface for all SAF entity ID types.
14///
15/// Provides a uniform way to access the raw `u128` value and hex formatting
16/// across all ID newtypes (`FunctionId`, `BlockId`, `ValueId`, etc.).
17pub trait EntityId:
18    Copy + Eq + Ord + std::hash::Hash + std::fmt::Display + std::fmt::Debug
19{
20    /// Get the raw `u128` value.
21    fn raw(self) -> u128;
22
23    /// Format as hex string with `0x` prefix.
24    fn to_hex(self) -> String {
25        crate::id::id_to_hex(self.raw())
26    }
27}
28
29/// Helper macro to define ID newtypes with common implementations.
30macro_rules! define_id_type {
31    ($(#[$meta:meta])* $name:ident, $domain:literal) => {
32        $(#[$meta])*
33        #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
34        pub struct $name(pub u128);
35
36        impl $name {
37            /// Create a new ID from raw `u128` value.
38            #[must_use]
39            pub const fn new(id: u128) -> Self {
40                Self(id)
41            }
42
43            /// Derive a deterministic ID from the given data bytes.
44            #[must_use]
45            pub fn derive(data: &[u8]) -> Self {
46                Self(make_id($domain, data))
47            }
48
49            /// Get the raw `u128` value.
50            #[must_use]
51            pub const fn raw(self) -> u128 {
52                self.0
53            }
54
55            /// Format as hex string with `0x` prefix.
56            #[must_use]
57            pub fn to_hex(self) -> String {
58                id_to_hex(self.0)
59            }
60        }
61
62        impl fmt::Debug for $name {
63            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64                write!(f, "{}({})", stringify!($name), id_to_hex(self.0))
65            }
66        }
67
68        impl fmt::Display for $name {
69            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70                write!(f, "{}", id_to_hex(self.0))
71            }
72        }
73
74        impl From<u128> for $name {
75            fn from(id: u128) -> Self {
76                Self(id)
77            }
78        }
79
80        impl From<$name> for u128 {
81            fn from(id: $name) -> Self {
82                id.0
83            }
84        }
85
86        impl Serialize for $name {
87            fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
88                serializer.serialize_str(&id_to_hex(self.0))
89            }
90        }
91
92        impl<'de> Deserialize<'de> for $name {
93            fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
94                let s = String::deserialize(deserializer)?;
95                parse_hex_id(&s)
96                    .map(Self)
97                    .map_err(serde::de::Error::custom)
98            }
99        }
100
101        impl $crate::ids::EntityId for $name {
102            fn raw(self) -> u128 {
103                self.0
104            }
105        }
106    };
107}
108
109/// Parse a hex string (with or without `0x` prefix) into a `u128`.
110fn parse_hex_id(s: &str) -> Result<u128, String> {
111    let hex_str = s.strip_prefix("0x").unwrap_or(s);
112    u128::from_str_radix(hex_str, 16).map_err(|e| format!("invalid hex ID '{s}': {e}"))
113}
114
115define_id_type!(
116    /// Unique identifier for an AIR module.
117    ModuleId,
118    "module"
119);
120
121define_id_type!(
122    /// Unique identifier for a function within an AIR module.
123    FunctionId,
124    "function"
125);
126
127define_id_type!(
128    /// Unique identifier for a basic block within a function.
129    BlockId,
130    "block"
131);
132
133define_id_type!(
134    /// Unique identifier for an instruction.
135    InstId,
136    "inst"
137);
138
139define_id_type!(
140    /// Unique identifier for a value (instruction result, parameter, constant).
141    ValueId,
142    "value"
143);
144
145define_id_type!(
146    /// Unique identifier for a memory object (allocation site).
147    ObjId,
148    "obj"
149);
150
151define_id_type!(
152    /// Unique identifier for a source location.
153    LocId,
154    "loc"
155);
156
157define_id_type!(
158    /// Unique identifier for a type in the type table.
159    TypeId,
160    "type"
161);
162
163define_id_type!(
164    /// Unique identifier for a source file referenced by spans.
165    FileId,
166    "file"
167);
168
169define_id_type!(
170    /// Unique identifier for a whole program (set of linked modules).
171    ProgramId,
172    "program"
173);
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn id_derive_is_deterministic() {
181        let a = FunctionId::derive(b"main");
182        let b = FunctionId::derive(b"main");
183        assert_eq!(a, b);
184    }
185
186    #[test]
187    fn different_domains_produce_different_ids() {
188        // Same data, different ID types (which use different domains)
189        let fn_id = FunctionId::derive(b"main");
190        let block_id = BlockId::derive(b"main");
191        assert_ne!(fn_id.raw(), block_id.raw());
192    }
193
194    #[test]
195    fn id_display_format() {
196        let id = FunctionId::new(0x1234_5678_9abc_def0_1234_5678_9abc_def0);
197        let hex = id.to_string();
198        assert!(hex.starts_with("0x"));
199        assert_eq!(hex.len(), 34); // 0x + 32 hex chars
200    }
201
202    #[test]
203    fn id_serialization_roundtrip() {
204        let original = FunctionId::derive(b"test_function");
205        let json = serde_json::to_string(&original).expect("serialize");
206        let parsed: FunctionId = serde_json::from_str(&json).expect("deserialize");
207        assert_eq!(original, parsed);
208    }
209
210    #[test]
211    fn id_parse_with_and_without_prefix() {
212        let id = FunctionId::new(0x123);
213
214        // With prefix
215        let json_with_prefix = "\"0x00000000000000000000000000000123\"";
216        let parsed: FunctionId = serde_json::from_str(json_with_prefix).expect("parse with prefix");
217        assert_eq!(parsed, id);
218
219        // Without prefix
220        let json_without_prefix = "\"00000000000000000000000000000123\"";
221        let parsed: FunctionId =
222            serde_json::from_str(json_without_prefix).expect("parse without prefix");
223        assert_eq!(parsed, id);
224    }
225
226    #[test]
227    fn id_ordering() {
228        let a = FunctionId::new(1);
229        let b = FunctionId::new(2);
230        let c = FunctionId::new(1);
231        assert!(a < b);
232        assert_eq!(a, c);
233    }
234
235    #[test]
236    fn type_id_derive_is_deterministic() {
237        let a = TypeId::derive(b"integer:32");
238        let b = TypeId::derive(b"integer:32");
239        assert_eq!(a, b);
240    }
241
242    #[test]
243    fn type_id_different_from_other_domains() {
244        let type_id = TypeId::derive(b"test");
245        let value_id = ValueId::derive(b"test");
246        assert_ne!(type_id.raw(), value_id.raw());
247    }
248
249    #[test]
250    fn entity_id_trait_works() {
251        let func_id = FunctionId::derive(b"test");
252        // Test via EntityId trait
253        let raw: u128 = EntityId::raw(func_id);
254        assert_eq!(raw, func_id.0);
255        let hex: String = EntityId::to_hex(func_id);
256        assert!(hex.starts_with("0x"));
257        assert_eq!(hex.len(), 34);
258    }
259
260    #[test]
261    fn entity_id_trait_is_generic() {
262        // Verify that EntityId works as a generic bound
263        fn check_id<T: EntityId>(id: T) -> u128 {
264            id.raw()
265        }
266        let fid = FunctionId::new(42);
267        let bid = BlockId::new(42);
268        // Same raw value, different types
269        assert_eq!(check_id(fid), check_id(bid));
270    }
271}