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}