Skip to main content

brink_format/
id.rs

1use core::fmt;
2
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4
5/// Tag discriminant stored in the high byte of a [`DefinitionId`].
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7#[repr(u8)]
8pub enum DefinitionTag {
9    Address = 0x01,
10    GlobalVar = 0x02,
11    ListDef = 0x03,
12    ListItem = 0x04,
13    ExternalFn = 0x05,
14    /// Params and temps — scoped to a container, not serialized in bytecode.
15    LocalVar = 0x07,
16}
17
18impl DefinitionTag {
19    /// Try to convert a raw `u8` into a known tag.
20    pub fn from_u8(byte: u8) -> Option<Self> {
21        match byte {
22            0x01 => Some(Self::Address),
23            0x02 => Some(Self::GlobalVar),
24            0x03 => Some(Self::ListDef),
25            0x04 => Some(Self::ListItem),
26            0x05 => Some(Self::ExternalFn),
27            0x07 => Some(Self::LocalVar),
28            _ => None,
29        }
30    }
31}
32
33/// Mask for the 56-bit hash portion of a definition id.
34const HASH_MASK: u64 = (1 << 56) - 1;
35
36/// A tagged 64-bit identifier for any definition in a compiled story.
37///
38/// Layout: `[tag: 8 bits][hash: 56 bits]`
39#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
40pub struct DefinitionId(u64);
41
42impl DefinitionId {
43    /// Create a new id from a tag and a 56-bit hash.
44    ///
45    /// The hash is masked to 56 bits — upper bits are silently discarded.
46    pub fn new(tag: DefinitionTag, hash: u64) -> Self {
47        let raw = (u64::from(tag as u8) << 56) | (hash & HASH_MASK);
48        Self(raw)
49    }
50
51    /// Extract the tag byte.
52    pub fn tag(self) -> DefinitionTag {
53        // SAFETY-equivalent: we only construct from known tags, so the unwrap
54        // below is always valid. We use `unwrap_or` to satisfy the lint.
55        let byte = (self.0 >> 56) as u8;
56        // This should never fail for a validly-constructed id.
57        DefinitionTag::from_u8(byte).unwrap_or(DefinitionTag::Address)
58    }
59
60    /// Extract the 56-bit hash.
61    pub fn hash(self) -> u64 {
62        self.0 & HASH_MASK
63    }
64
65    /// Return the raw `u64` representation.
66    pub fn to_raw(self) -> u64 {
67        self.0
68    }
69
70    /// Reconstruct from a raw `u64`, returning `None` if the tag byte is
71    /// invalid.
72    pub fn from_raw(raw: u64) -> Option<Self> {
73        let byte = (raw >> 56) as u8;
74        DefinitionTag::from_u8(byte)?;
75        Some(Self(raw))
76    }
77}
78
79impl Serialize for DefinitionId {
80    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
81        // Serialize as "$tt_hhhhhhhhhhhhhh" — tag byte + 56-bit hash.
82        serializer.serialize_str(&format!("{self}"))
83    }
84}
85
86impl<'de> Deserialize<'de> for DefinitionId {
87    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
88        let s = <&str>::deserialize(deserializer)?;
89        // Parse "$tt_hhhhhhhhhhhhhh"
90        if !s.starts_with('$') || s.len() != 18 || s.as_bytes()[3] != b'_' {
91            return Err(serde::de::Error::custom(format!(
92                "invalid DefinitionId: {s:?}"
93            )));
94        }
95        let tag_byte = u8::from_str_radix(&s[1..3], 16).map_err(serde::de::Error::custom)?;
96        let tag = DefinitionTag::from_u8(tag_byte).ok_or_else(|| {
97            serde::de::Error::custom(format!("invalid tag byte: {tag_byte:#04x}"))
98        })?;
99        let hash = u64::from_str_radix(&s[4..], 16).map_err(serde::de::Error::custom)?;
100        Ok(Self::new(tag, hash))
101    }
102}
103
104impl fmt::Display for DefinitionId {
105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106        write!(f, "${:02x}_{:014x}", self.tag() as u8, self.hash())
107    }
108}
109
110impl fmt::Debug for DefinitionId {
111    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112        write!(f, "{:?}({:#014x})", self.tag(), self.hash())
113    }
114}
115
116/// An index into the story name table.
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
118pub struct NameId(pub u16);
119
120/// A reference to a specific line within a container.
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
122pub struct LineId {
123    pub container: DefinitionId,
124    pub index: u16,
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn roundtrip_raw() {
133        let id = DefinitionId::new(DefinitionTag::Address, 0xDEAD_BEEF);
134        let raw = id.to_raw();
135        let recovered = DefinitionId::from_raw(raw).unwrap();
136        assert_eq!(id, recovered);
137    }
138
139    #[test]
140    fn tag_extraction() {
141        for tag in [
142            DefinitionTag::Address,
143            DefinitionTag::GlobalVar,
144            DefinitionTag::ListDef,
145            DefinitionTag::ListItem,
146            DefinitionTag::ExternalFn,
147        ] {
148            let id = DefinitionId::new(tag, 42);
149            assert_eq!(id.tag(), tag);
150        }
151    }
152
153    #[test]
154    fn hash_masking() {
155        // High bits beyond 56 should be discarded.
156        let id = DefinitionId::new(DefinitionTag::ListDef, u64::MAX);
157        assert_eq!(id.hash(), HASH_MASK);
158        assert_eq!(id.tag(), DefinitionTag::ListDef);
159    }
160
161    #[test]
162    fn invalid_tag_rejection() {
163        // Forge a raw value with tag byte 0x00.
164        let raw = 0x00_DEAD_BEEF_CAFE_u64;
165        assert!(DefinitionId::from_raw(raw).is_none());
166
167        // Tag byte 0xFF is also invalid.
168        let raw = 0xFF_0000_0000_0000_u64;
169        assert!(DefinitionId::from_raw(raw).is_none());
170    }
171
172    #[test]
173    fn debug_format() {
174        let id = DefinitionId::new(DefinitionTag::ExternalFn, 0xCAFE);
175        let s = format!("{id:?}");
176        assert!(s.contains("ExternalFn"));
177        assert!(s.contains("0x"));
178    }
179
180    #[test]
181    fn line_id_equality() {
182        let c = DefinitionId::new(DefinitionTag::Address, 1);
183        let a = LineId {
184            container: c,
185            index: 0,
186        };
187        let b = LineId {
188            container: c,
189            index: 0,
190        };
191        assert_eq!(a, b);
192    }
193}