rssn_advanced/dag/metadata.rs
1//! Per-node metadata for DAG entries.
2//!
3//! Each DAG node carries metadata that describes its algebraic properties:
4//! structural hash (via `rapidhash`), numeric coefficient, arity, and
5//! commutativity flags.
6
7use bincode_next::{Decode, Encode};
8
9/// Hash value computed by `rapidhash` for structural deduplication.
10///
11/// Two nodes with the same `NodeHash` are structurally identical
12/// (modulo hash collisions, which are verified by full structural comparison).
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Encode, Decode)]
14pub struct NodeHash(pub(crate) u64);
15
16impl NodeHash {
17 /// The hash value for an empty / uninitialized node.
18 pub const ZERO: Self = Self(0);
19}
20
21/// Flags that describe algebraic properties of a node.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Encode, Decode)]
23pub struct NodeFlags {
24 bits: u8,
25}
26
27impl NodeFlags {
28 /// No flags set.
29 pub const EMPTY: Self = Self { bits: 0 };
30
31 /// This node's operator is commutative (e.g. `+`, `*`).
32 const COMMUTATIVE: u8 = 0b0000_0001;
33 /// This node's operator is associative.
34 const ASSOCIATIVE: u8 = 0b0000_0010;
35 /// This node has been simplified / is in canonical form.
36 const CANONICAL: u8 = 0b0000_0100;
37
38 /// Creates flags with commutativity set.
39 #[must_use]
40 pub const fn commutative() -> Self {
41 Self {
42 bits: Self::COMMUTATIVE,
43 }
44 }
45
46 /// Creates flags with both commutativity and associativity set.
47 #[must_use]
48 pub const fn commutative_associative() -> Self {
49 Self {
50 bits: Self::COMMUTATIVE | Self::ASSOCIATIVE,
51 }
52 }
53
54 /// Returns `true` if the commutative flag is set.
55 #[must_use]
56 pub const fn is_commutative(self) -> bool {
57 self.bits & Self::COMMUTATIVE != 0
58 }
59
60 /// Returns `true` if the associative flag is set.
61 #[must_use]
62 pub const fn is_associative(self) -> bool {
63 self.bits & Self::ASSOCIATIVE != 0
64 }
65
66 /// Returns `true` if the canonical flag is set.
67 #[must_use]
68 pub const fn is_canonical(self) -> bool {
69 self.bits & Self::CANONICAL != 0
70 }
71
72 /// Sets the canonical flag.
73 #[must_use]
74 pub const fn with_canonical(self) -> Self {
75 Self {
76 bits: self.bits | Self::CANONICAL,
77 }
78 }
79
80 /// Returns the flags with the canonical bit cleared.
81 ///
82 /// Used by the deduplication map to compare structural identity without
83 /// regard to whether a node has been marked canonical at runtime.
84 #[must_use]
85 pub const fn without_canonical(self) -> Self {
86 Self {
87 bits: self.bits & !Self::CANONICAL,
88 }
89 }
90
91 /// Returns the raw bit pattern — used when packing to the wire
92 /// representation in [`crate::dag::packed`].
93 #[must_use]
94 pub const fn bits(self) -> u8 {
95 self.bits
96 }
97
98 /// Reconstructs flags from a raw bit pattern. Unknown bits are
99 /// preserved; the future-proofing matters for forward compatibility
100 /// when newer encodings carry flags this version doesn't know yet.
101 #[must_use]
102 pub const fn from_bits(bits: u8) -> Self {
103 Self { bits }
104 }
105}
106
107/// Metadata attached to every DAG node.
108///
109/// This is stored inline in the `DagNode` and contains all the information
110/// needed for hash-consing, algebraic classification, and simplification.
111///
112/// Note: `arity` was removed — it always equalled `node.children.len()`.
113/// Callers that need the arity call `node.children.len()` directly.
114#[derive(Debug, Clone, PartialEq, Encode, Decode)]
115pub struct NodeMetadata {
116 /// Structural hash for deduplication lookups.
117 pub hash: NodeHash,
118 /// Numeric coefficient (e.g. the `3` in `3*x`).
119 /// Defaults to `1.0` for non-coefficient nodes.
120 pub coefficient: f64,
121 /// Algebraic property flags.
122 pub flags: NodeFlags,
123}
124
125impl NodeMetadata {
126 /// Creates metadata for a leaf node with coefficient 1.
127 #[must_use]
128 pub const fn leaf(hash: NodeHash) -> Self {
129 Self {
130 hash,
131 coefficient: 1.0,
132 flags: NodeFlags::EMPTY,
133 }
134 }
135
136 /// Creates metadata for an operator node with the given flags.
137 #[must_use]
138 pub const fn operator(hash: NodeHash, flags: NodeFlags) -> Self {
139 Self {
140 hash,
141 coefficient: 1.0,
142 flags,
143 }
144 }
145
146 /// Returns a copy with the coefficient set to the given value.
147 #[must_use]
148 pub const fn with_coefficient(mut self, coeff: f64) -> Self {
149 self.coefficient = coeff;
150 self
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn flags_round_trip() {
160 let f = NodeFlags::commutative_associative();
161 assert!(f.is_commutative());
162 assert!(f.is_associative());
163 assert!(!f.is_canonical());
164
165 let f2 = f.with_canonical();
166 assert!(f2.is_canonical());
167 assert!(f2.is_commutative());
168 }
169
170 #[test]
171 fn leaf_metadata() {
172 let m = NodeMetadata::leaf(NodeHash(42));
173 assert_eq!(m.coefficient, 1.0);
174 assert_eq!(m.hash, NodeHash(42));
175 }
176
177 #[test]
178 fn coefficient_override() {
179 let m = NodeMetadata::leaf(NodeHash(1)).with_coefficient(3.5);
180 assert_eq!(m.coefficient, 3.5);
181 }
182}