Skip to main content

fission_ir/
node_id.rs

1//! Content-addressed node identity.
2//!
3//! Every node in the IR graph is identified by a [`NodeId`] -- a 128-bit value
4//! derived from a BLAKE3 hash. Two construction strategies are available:
5//!
6//! * **Explicit** -- hash a user-provided string key. Stable across rebuilds as
7//!   long as the key string does not change.
8//! * **Derived** -- hash a parent ID plus a child-index path. Gives every node in
9//!   a subtree a deterministic identity based on its structural position.
10
11use serde::{Deserialize, Serialize};
12use std::fmt;
13
14/// A content-addressed 128-bit node identity.
15///
16/// `NodeId` is the primary key used throughout the Fission pipeline to refer to a
17/// specific node. Because it is derived from BLAKE3 hashes, two nodes with the same
18/// derivation inputs always produce the same `NodeId`, which makes tree diffing cheap.
19///
20/// # Construction
21///
22/// ```rust
23/// use fission_ir::NodeId;
24///
25/// // From a stable string key (good for well-known, named nodes):
26/// let id = NodeId::explicit("sidebar");
27///
28/// // From a parent ID and a position path (good for list items):
29/// let parent = NodeId::explicit("list");
30/// let item_3 = NodeId::derived(parent.as_u128(), &[3]);
31/// ```
32///
33/// # Equality and hashing
34///
35/// `NodeId` implements `Eq`, `Hash`, and `Ord`, so it can be used as a key in
36/// `HashMap`, `BTreeMap`, and `HashSet`.
37#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
38pub struct NodeId(u128);
39
40impl NodeId {
41    /// Creates a `NodeId` from a raw 128-bit value.
42    ///
43    /// This is intended for internal use or deserialization. In most cases you
44    /// should use [`NodeId::explicit`] or [`NodeId::derived`] instead.
45    pub const fn from_u128(val: u128) -> Self {
46        Self(val)
47    }
48
49    /// Returns the underlying 128-bit value.
50    ///
51    /// Useful when you need to feed a node's identity into another hash (e.g.,
52    /// when deriving child IDs with [`NodeId::derived`]).
53    pub fn as_u128(&self) -> u128 {
54        self.0
55    }
56
57    /// Creates a `NodeId` from a user-provided string key.
58    ///
59    /// The key is hashed with BLAKE3 (prefixed with `"explicit:"`), producing a
60    /// deterministic ID that is stable across rebuilds as long as the key string
61    /// does not change. Use this for well-known, named nodes like `"root"` or
62    /// `"toolbar"`.
63    ///
64    /// # Example
65    ///
66    /// ```rust
67    /// use fission_ir::NodeId;
68    /// let a = NodeId::explicit("header");
69    /// let b = NodeId::explicit("header");
70    /// assert_eq!(a, b); // same key -> same ID
71    /// ```
72    pub fn explicit(key: &str) -> Self {
73        let mut hasher = blake3::Hasher::new();
74        hasher.update(b"explicit:");
75        hasher.update(key.as_bytes());
76        let hash = hasher.finalize();
77        Self(u128::from_le_bytes(
78            hash.as_bytes()[0..16].try_into().unwrap(),
79        ))
80    }
81
82    /// Creates a `NodeId` derived from a parent ID and a child-index path.
83    ///
84    /// This implements *structural identity*: a node's ID is determined by its
85    /// position in the tree rather than by a user-provided name. Useful for
86    /// dynamically generated children like list items.
87    ///
88    /// # Arguments
89    ///
90    /// * `parent` -- The parent node's raw `u128` value (see [`NodeId::as_u128`]).
91    /// * `path` -- One or more child indices describing the path from the parent.
92    ///
93    /// # Example
94    ///
95    /// ```rust
96    /// use fission_ir::NodeId;
97    /// let parent = NodeId::explicit("list");
98    /// let item_0 = NodeId::derived(parent.as_u128(), &[0]);
99    /// let item_1 = NodeId::derived(parent.as_u128(), &[1]);
100    /// assert_ne!(item_0, item_1);
101    /// ```
102    pub fn derived(parent: u128, path: &[u32]) -> Self {
103        let mut hasher = blake3::Hasher::new();
104        hasher.update(b"derived:");
105        hasher.update(&parent.to_le_bytes());
106        for index in path {
107            hasher.update(&index.to_le_bytes());
108        }
109        let hash = hasher.finalize();
110        Self(u128::from_le_bytes(
111            hash.as_bytes()[0..16].try_into().unwrap(),
112        ))
113    }
114}
115
116impl fmt::Debug for NodeId {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        write!(f, "NodeId({:032x})", self.0)
119    }
120}
121
122impl fmt::Display for NodeId {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        write!(f, "{:032x}", self.0)
125    }
126}