Skip to main content

brink_format/
id.rs

1use core::fmt;
2
3use alloc::format;
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5
6/// Tag discriminant stored in the high byte of a [`DefinitionId`].
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8#[repr(u8)]
9pub enum DefinitionTag {
10    Address = 0x01,
11    GlobalVar = 0x02,
12    ListDef = 0x03,
13    ListItem = 0x04,
14    ExternalFn = 0x05,
15    /// A `STRUCT` shape declaration (TM-4b, `docs/typed-mode-spec.md` §6).
16    /// Compiler-side bookkeeping only — the analyzer's `SymbolIndex` needs a
17    /// stable `DefinitionId` for a struct name like every other declared
18    /// symbol (duplicate detection, goto-def, resolution), but this tag is
19    /// never serialized to `.inkb`: the runtime-facing shape identity is the
20    /// separate `ShapeId`/`StructShapes` space `brink-format::value` already
21    /// reserves, which TM-4c's codegen populates once struct constructs
22    /// lower to bytecode. Until then a `StructDef`-tagged id never reaches
23    /// the linker.
24    StructDef = 0x06,
25    /// Params and temps — scoped to a container, not serialized in bytecode.
26    LocalVar = 0x07,
27}
28
29impl DefinitionTag {
30    /// Try to convert a raw `u8` into a known tag.
31    pub fn from_u8(byte: u8) -> Option<Self> {
32        match byte {
33            0x01 => Some(Self::Address),
34            0x02 => Some(Self::GlobalVar),
35            0x03 => Some(Self::ListDef),
36            0x04 => Some(Self::ListItem),
37            0x05 => Some(Self::ExternalFn),
38            0x06 => Some(Self::StructDef),
39            0x07 => Some(Self::LocalVar),
40            _ => None,
41        }
42    }
43}
44
45/// Mask for the 56-bit hash portion of a definition id.
46const HASH_MASK: u64 = (1 << 56) - 1;
47
48/// A tagged 64-bit identifier for any definition in a compiled story.
49///
50/// Layout: `[tag: 8 bits][hash: 56 bits]`
51#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
52pub struct DefinitionId(u64);
53
54impl DefinitionId {
55    /// The well-known cell id of the **`std::rand` RNG state cell** (NS-A6,
56    /// `docs/stdlib-spec.md` §7, ruled 2026-07-18): RNG state is a named
57    /// runtime state cell, and every draw is an ordinary *write* to it in a
58    /// definition's effect row — no new row dimension. This constant is that
59    /// cell's name in the `DefinitionId` space shared by the effect-row
60    /// machinery (`brink-analyzer`'s harvest, `@[effects(…)]` assertions,
61    /// the wake-condition purity gate) and the runtime's ground-truth
62    /// recorder (`brink-runtime::effect_trace`).
63    ///
64    /// The cell is compiler-owned — no source declaration mints it — so its
65    /// hash is a fixed, documented constant rather than a content hash. A
66    /// collision with a real content-hashed `GlobalVar` would require a
67    /// user `VAR`/`CONST` to hash to exactly this 56-bit value (probability
68    /// 2⁻⁵⁶ per global; `hash_qualified_name` output is uniform), which is
69    /// the same residual risk every pair of user globals already carries.
70    ///
71    /// The cell's *runtime* representation is not a `Value` slot: it is the
72    /// `(rng_seed, previous_random)` pair `ContextAccess` has always carried
73    /// (and saves have always round-tripped). This id names that state for
74    /// the effect system; it never appears in a global table.
75    pub const RNG_CELL: DefinitionId =
76        DefinitionId(((DefinitionTag::GlobalVar as u64) << 56) | 0x00_5EED_0000_D1CE);
77
78    /// Create a new id from a tag and a 56-bit hash.
79    ///
80    /// The hash is masked to 56 bits — upper bits are silently discarded.
81    pub fn new(tag: DefinitionTag, hash: u64) -> Self {
82        let raw = (u64::from(tag as u8) << 56) | (hash & HASH_MASK);
83        Self(raw)
84    }
85
86    /// Extract the tag byte.
87    pub fn tag(self) -> DefinitionTag {
88        // SAFETY-equivalent: we only construct from known tags, so the unwrap
89        // below is always valid. We use `unwrap_or` to satisfy the lint.
90        let byte = (self.0 >> 56) as u8;
91        // This should never fail for a validly-constructed id.
92        DefinitionTag::from_u8(byte).unwrap_or(DefinitionTag::Address)
93    }
94
95    /// Extract the 56-bit hash.
96    pub fn hash(self) -> u64 {
97        self.0 & HASH_MASK
98    }
99
100    /// Return the raw `u64` representation.
101    pub fn to_raw(self) -> u64 {
102        self.0
103    }
104
105    /// Reconstruct from a raw `u64`, returning `None` if the tag byte is
106    /// invalid.
107    pub fn from_raw(raw: u64) -> Option<Self> {
108        let byte = (raw >> 56) as u8;
109        DefinitionTag::from_u8(byte)?;
110        Some(Self(raw))
111    }
112}
113
114impl Serialize for DefinitionId {
115    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
116        // Serialize as "$tt_hhhhhhhhhhhhhh" — tag byte + 56-bit hash.
117        serializer.serialize_str(&format!("{self}"))
118    }
119}
120
121impl<'de> Deserialize<'de> for DefinitionId {
122    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
123        let s = <&str>::deserialize(deserializer)?;
124        // Parse "$tt_hhhhhhhhhhhhhh"
125        if !s.starts_with('$') || s.len() != 18 || s.as_bytes()[3] != b'_' {
126            return Err(serde::de::Error::custom(format!(
127                "invalid DefinitionId: {s:?}"
128            )));
129        }
130        let tag_byte = u8::from_str_radix(&s[1..3], 16).map_err(serde::de::Error::custom)?;
131        let tag = DefinitionTag::from_u8(tag_byte).ok_or_else(|| {
132            serde::de::Error::custom(format!("invalid tag byte: {tag_byte:#04x}"))
133        })?;
134        let hash = u64::from_str_radix(&s[4..], 16).map_err(serde::de::Error::custom)?;
135        Ok(Self::new(tag, hash))
136    }
137}
138
139impl fmt::Display for DefinitionId {
140    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141        write!(f, "${:02x}_{:014x}", self.tag() as u8, self.hash())
142    }
143}
144
145impl fmt::Debug for DefinitionId {
146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147        write!(f, "{:?}({:#014x})", self.tag(), self.hash())
148    }
149}
150
151/// An index into the story name table.
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
153pub struct NameId(pub u16);
154
155/// A reference to a specific line within a container.
156#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
157pub struct LineId {
158    pub container: DefinitionId,
159    pub index: u16,
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn roundtrip_raw() {
168        let id = DefinitionId::new(DefinitionTag::Address, 0xDEAD_BEEF);
169        let raw = id.to_raw();
170        let recovered = DefinitionId::from_raw(raw).unwrap();
171        assert_eq!(id, recovered);
172    }
173
174    #[test]
175    fn tag_extraction() {
176        for tag in [
177            DefinitionTag::Address,
178            DefinitionTag::GlobalVar,
179            DefinitionTag::ListDef,
180            DefinitionTag::ListItem,
181            DefinitionTag::ExternalFn,
182            DefinitionTag::StructDef,
183            DefinitionTag::LocalVar,
184        ] {
185            let id = DefinitionId::new(tag, 42);
186            assert_eq!(id.tag(), tag);
187        }
188    }
189
190    #[test]
191    fn struct_def_tag_roundtrips_through_from_u8() {
192        assert_eq!(DefinitionTag::from_u8(0x06), Some(DefinitionTag::StructDef));
193    }
194
195    #[test]
196    fn hash_masking() {
197        // High bits beyond 56 should be discarded.
198        let id = DefinitionId::new(DefinitionTag::ListDef, u64::MAX);
199        assert_eq!(id.hash(), HASH_MASK);
200        assert_eq!(id.tag(), DefinitionTag::ListDef);
201    }
202
203    #[test]
204    fn invalid_tag_rejection() {
205        // Forge a raw value with tag byte 0x00.
206        let raw = 0x00_DEAD_BEEF_CAFE_u64;
207        assert!(DefinitionId::from_raw(raw).is_none());
208
209        // Tag byte 0xFF is also invalid.
210        let raw = 0xFF_0000_0000_0000_u64;
211        assert!(DefinitionId::from_raw(raw).is_none());
212    }
213
214    #[test]
215    fn debug_format() {
216        let id = DefinitionId::new(DefinitionTag::ExternalFn, 0xCAFE);
217        let s = format!("{id:?}");
218        assert!(s.contains("ExternalFn"));
219        assert!(s.contains("0x"));
220    }
221
222    #[test]
223    fn rng_cell_is_a_well_formed_global_var_id() {
224        // NS-A6: the well-known `std::rand` cell id must be a valid,
225        // round-trippable `GlobalVar`-tagged id — it flows through the same
226        // effect-row wire section (`EffectRows`) as content-hashed ids.
227        let id = DefinitionId::RNG_CELL;
228        assert_eq!(id.tag(), DefinitionTag::GlobalVar);
229        assert_eq!(id.hash(), 0x00_5EED_0000_D1CE);
230        assert_eq!(DefinitionId::from_raw(id.to_raw()), Some(id));
231        assert_eq!(
232            id,
233            DefinitionId::new(DefinitionTag::GlobalVar, 0x00_5EED_0000_D1CE)
234        );
235    }
236
237    #[test]
238    fn line_id_equality() {
239        let c = DefinitionId::new(DefinitionTag::Address, 1);
240        let a = LineId {
241            container: c,
242            index: 0,
243        };
244        let b = LineId {
245            container: c,
246            index: 0,
247        };
248        assert_eq!(a, b);
249    }
250}