Skip to main content

rssn_advanced/dag/
node.rs

1//! DAG node representation.
2//!
3//! A `DagNode` is a vertex in the global expression graph. It owns its
4//! metadata inline and stores children as compact arena indices.
5
6use core::fmt;
7
8use bincode_next::{Decode, Encode};
9
10use super::metadata::NodeMetadata;
11use super::symbol::SymbolKind;
12
13/// An index into the `DagArena`, identifying a specific `DagNode`.
14///
15/// This is a lightweight handle (4 bytes) that can be freely copied
16/// and compared. It is only valid within the arena that created it.
17#[derive(Clone, Copy, PartialEq, Eq, Hash, Encode, Decode)]
18pub struct DagNodeId(pub(crate) u32);
19
20impl DagNodeId {
21    /// Creates a new `DagNodeId` from a raw u32 index.
22    #[must_use]
23    pub const fn new(id: u32) -> Self {
24        Self(id)
25    }
26
27    /// Returns the raw u32 index of this node ID.
28    #[must_use]
29    pub const fn value(self) -> u32 {
30        self.0
31    }
32
33    /// A sentinel value representing "no node" / null.
34    pub const NONE: Self = Self(u32::MAX);
35
36    /// Returns the raw index value.
37    #[must_use]
38    pub const fn index(self) -> usize {
39        self.0 as usize
40    }
41
42    /// Returns `true` if this is the null sentinel.
43    #[must_use]
44    pub const fn is_none(self) -> bool {
45        self.0 == u32::MAX
46    }
47}
48
49impl fmt::Debug for DagNodeId {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        if self.is_none() {
52            f.write_str("DagNodeId(NONE)")
53        } else {
54            write!(f, "DagNodeId({})", self.0)
55        }
56    }
57}
58
59impl fmt::Display for DagNodeId {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        if self.is_none() {
62            f.write_str("∅")
63        } else {
64            write!(f, "#{}", self.0)
65        }
66    }
67}
68
69/// Maximum number of children stored inline (on the stack) before
70/// spilling to the heap. 4 covers the vast majority of nodes
71/// (binary ops = 2, ternary = 3, quaternary = 4).
72const INLINE_CHILDREN: usize = 4;
73
74/// A compact child list that stores up to `INLINE_CHILDREN` IDs inline.
75///
76/// For nodes with more children (e.g. variadic `+` or `*` chains),
77/// the list spills to a heap-allocated `Vec`.
78#[derive(Debug, Clone, PartialEq, Eq, Hash, Encode, Decode)]
79pub enum ChildList {
80    /// Zero children (leaf node).
81    Empty,
82    /// One child (unary operator).
83    One([DagNodeId; 1]),
84    /// Two children (binary operator — the most common case).
85    Two([DagNodeId; 2]),
86    /// Three children.
87    Three([DagNodeId; 3]),
88    /// Four children (inline limit).
89    Four([DagNodeId; 4]),
90    /// More than `INLINE_CHILDREN` children (heap-allocated).
91    Many(Vec<DagNodeId>),
92}
93
94impl ChildList {
95    /// Creates a child list from a slice of node IDs.
96    #[must_use]
97    pub fn from_slice(ids: &[DagNodeId]) -> Self {
98        match ids.len() {
99            0 => Self::Empty,
100            1 => Self::One([ids[0]]),
101            2 => Self::Two([ids[0], ids[1]]),
102            3 => Self::Three([ids[0], ids[1], ids[2]]),
103            4 => Self::Four([ids[0], ids[1], ids[2], ids[3]]),
104            _ => Self::Many(ids.to_vec()),
105        }
106    }
107
108    /// Returns the number of children.
109    #[must_use]
110    pub const fn len(&self) -> usize {
111        match self {
112            Self::Empty => 0,
113            Self::One(_) => 1,
114            Self::Two(_) => 2,
115            Self::Three(_) => 3,
116            Self::Four(_) => INLINE_CHILDREN,
117            Self::Many(v) => v.len(),
118        }
119    }
120
121    /// Returns `true` if there are no children.
122    #[must_use]
123    pub const fn is_empty(&self) -> bool {
124        matches!(self, Self::Empty)
125    }
126
127    /// Returns the children as a slice.
128    #[must_use]
129    pub fn as_slice(&self) -> &[DagNodeId] {
130        match self {
131            Self::Empty => &[],
132            Self::One(a) => a,
133            Self::Two(a) => a,
134            Self::Three(a) => a,
135            Self::Four(a) => a,
136            Self::Many(v) => v,
137        }
138    }
139
140    /// Returns an iterator over the children.
141    pub fn iter(&self) -> impl Iterator<Item = DagNodeId> + '_ {
142        self.as_slice().iter().copied()
143    }
144}
145
146/// A node in the global DAG.
147///
148/// Each `DagNode` represents a unique sub-expression. It contains:
149/// - What **kind** of symbol it is (variable, constant, operator, function).
150/// - **Metadata** (hash, coefficient, arity, flags).
151/// - **Children** (references to other DAG nodes via `DagNodeId`).
152///
153/// For constant nodes, the value is stored inline in `kind` as
154/// `SymbolKind::Constant(val)` — no separate `Option<f64>` field.
155#[derive(Debug, Clone, Encode, Decode)]
156pub struct DagNode {
157    /// The classification of this node.
158    pub kind: SymbolKind,
159    /// Algebraic metadata (hash, coefficient, flags).
160    pub meta: NodeMetadata,
161    /// References to child nodes in the arena.
162    pub children: ChildList,
163}
164
165impl DagNode {
166    /// Creates a variable leaf node.
167    #[must_use]
168    pub const fn variable(kind: SymbolKind, meta: NodeMetadata) -> Self {
169        Self {
170            kind,
171            meta,
172            children: ChildList::Empty,
173        }
174    }
175
176    /// Creates a numeric constant leaf node.
177    #[must_use]
178    pub const fn constant(val: f64, meta: NodeMetadata) -> Self {
179        Self {
180            kind: SymbolKind::Constant(val),
181            meta,
182            children: ChildList::Empty,
183        }
184    }
185
186    /// Creates an operator node with the given children.
187    #[must_use]
188    pub const fn operator(kind: SymbolKind, meta: NodeMetadata, children: ChildList) -> Self {
189        Self {
190            kind,
191            meta,
192            children,
193        }
194    }
195
196    /// Returns `true` if this is a leaf node (no children).
197    #[must_use]
198    pub const fn is_leaf(&self) -> bool {
199        self.children.is_empty()
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206    use crate::dag::metadata::{NodeFlags, NodeHash};
207    use crate::dag::symbol::{OpKind, SymbolId};
208
209    #[test]
210    fn child_list_inline() {
211        let ids = [DagNodeId(0), DagNodeId(1)];
212        let cl = ChildList::from_slice(&ids);
213        assert_eq!(cl.len(), 2);
214        assert_eq!(cl.as_slice(), &ids);
215    }
216
217    #[test]
218    fn child_list_many() {
219        let ids: Vec<DagNodeId> = (0..10).map(DagNodeId).collect();
220        let cl = ChildList::from_slice(&ids);
221        assert_eq!(cl.len(), 10);
222        assert_eq!(cl.as_slice(), &ids);
223    }
224
225    #[test]
226    fn variable_node() {
227        let meta = NodeMetadata::leaf(NodeHash(100));
228        let node = DagNode::variable(SymbolKind::Variable(SymbolId(0)), meta);
229        assert!(node.is_leaf());
230        assert!(!matches!(node.kind, SymbolKind::Constant(_)));
231    }
232
233    #[test]
234    fn constant_node() {
235        let meta = NodeMetadata::leaf(NodeHash(200));
236        let node = DagNode::constant(3.14, meta);
237        assert!(node.is_leaf());
238        assert!(matches!(node.kind, SymbolKind::Constant(v) if (v - 3.14).abs() < f64::EPSILON));
239    }
240
241    #[test]
242    fn operator_node() {
243        let meta = NodeMetadata::operator(NodeHash(300), NodeFlags::commutative_associative());
244        let children = ChildList::from_slice(&[DagNodeId(0), DagNodeId(1)]);
245        let node = DagNode::operator(SymbolKind::Operator(OpKind::Add), meta, children);
246        assert!(!node.is_leaf());
247        assert_eq!(node.children.len(), 2);
248        assert!(node.meta.flags.is_commutative());
249    }
250}