1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
//! High-level DAG construction API.
//!
//! `DagBuilder` provides a unified, thread-safe or single-threaded context
//! that holds the `SymbolRegistry`, the `DagArena`, and the `DedupMap`
//! to construct structurally deduplicated symbolic expressions.
use super::arena::DagArena;
use super::dedup::DedupMap;
use super::metadata::NodeFlags;
use super::node::{ChildList, DagNodeId};
use super::symbol::{FnId, OpKind, SymbolKind, SymbolRegistry};
/// The primary context for building symbolic expression DAGs.
///
/// It coordinates the symbol registry, arena storage, and deduplication map
/// to construct perfectly-shared Directed Acyclic Graphs.
#[derive(Debug, Clone, Default)]
pub struct DagBuilder {
/// Opaque registry mapping variable names to `SymbolId`.
registry: SymbolRegistry,
/// Registry mapping function names to `FnId`. Stored separately so
/// function IDs and variable IDs occupy independent namespaces.
fn_registry: SymbolRegistry,
/// Vector-backed contiguous storage for nodes.
arena: DagArena,
/// Fast structural deduplication lookup.
dedup: DedupMap,
}
impl DagBuilder {
/// Creates a new, empty `DagBuilder` context.
#[must_use]
pub fn new() -> Self {
Self {
registry: SymbolRegistry::new(),
fn_registry: SymbolRegistry::new(),
arena: DagArena::new(),
dedup: DedupMap::new(),
}
}
/// Accesses the underlying arena.
#[must_use]
pub const fn arena(&self) -> &DagArena {
&self.arena
}
/// Accesses the underlying arena mutably.
pub const fn arena_mut(&mut self) -> &mut DagArena {
&mut self.arena
}
/// Accesses the underlying symbol registry.
#[must_use]
pub const fn registry(&self) -> &SymbolRegistry {
&self.registry
}
/// Accesses the underlying deduplication map.
#[must_use]
pub const fn dedup(&self) -> &DedupMap {
&self.dedup
}
/// Interns or retrieves a variable name, producing a unique leaf node.
pub fn variable(&mut self, name: &str) -> DagNodeId {
let sym_id = self.registry.intern(name);
self.variable_with_sym_id(sym_id)
}
/// Like [`Self::variable`] but accepts a raw byte slice — used by
/// the FFI surface to skip the `to_string_lossy` allocation.
///
/// Returns `None` if `name_bytes` is not valid UTF-8.
pub fn variable_bytes(&mut self, name_bytes: &[u8]) -> Option<DagNodeId> {
let sym_id = self.registry.intern_bytes(name_bytes)?;
Some(self.variable_with_sym_id(sym_id))
}
fn variable_with_sym_id(&mut self, sym_id: crate::dag::symbol::SymbolId) -> DagNodeId {
let kind = SymbolKind::Variable(sym_id);
let hash = DedupMap::hash_variable(&kind);
self.dedup.get_or_insert(
&mut self.arena,
kind,
hash,
ChildList::Empty,
1.0,
NodeFlags::EMPTY,
)
}
/// Constructs a unique numeric constant node.
pub fn constant(&mut self, val: f64) -> DagNodeId {
let kind = SymbolKind::Constant(val);
let hash = DedupMap::hash_constant(&kind);
self.dedup.get_or_insert(
&mut self.arena,
kind,
hash,
ChildList::Empty,
val, // coefficient matches the constant value for leaf constants
NodeFlags::EMPTY,
)
}
/// Constructs an addition node: `left + right`.
pub fn add(&mut self, left: DagNodeId, right: DagNodeId) -> DagNodeId {
let kind = SymbolKind::Operator(OpKind::Add);
let children = ChildList::from_slice(&[left, right]);
let hash = DedupMap::hash_operator(&kind, &children);
let flags = NodeFlags::commutative_associative();
self.dedup
.get_or_insert(&mut self.arena, kind, hash, children, 1.0, flags)
}
/// Constructs a subtraction node: `left - right`.
pub fn sub(&mut self, left: DagNodeId, right: DagNodeId) -> DagNodeId {
let kind = SymbolKind::Operator(OpKind::Sub);
let children = ChildList::from_slice(&[left, right]);
let hash = DedupMap::hash_operator(&kind, &children);
self.dedup
.get_or_insert(&mut self.arena, kind, hash, children, 1.0, NodeFlags::EMPTY)
}
/// Constructs a multiplication node: `left * right`.
pub fn mul(&mut self, left: DagNodeId, right: DagNodeId) -> DagNodeId {
let kind = SymbolKind::Operator(OpKind::Mul);
let children = ChildList::from_slice(&[left, right]);
let hash = DedupMap::hash_operator(&kind, &children);
let flags = NodeFlags::commutative_associative();
self.dedup
.get_or_insert(&mut self.arena, kind, hash, children, 1.0, flags)
}
/// Constructs a division node: `left / right`.
pub fn div(&mut self, left: DagNodeId, right: DagNodeId) -> DagNodeId {
let kind = SymbolKind::Operator(OpKind::Div);
let children = ChildList::from_slice(&[left, right]);
let hash = DedupMap::hash_operator(&kind, &children);
self.dedup
.get_or_insert(&mut self.arena, kind, hash, children, 1.0, NodeFlags::EMPTY)
}
/// Constructs an exponentiation node: `left ^ right`.
pub fn pow(&mut self, left: DagNodeId, right: DagNodeId) -> DagNodeId {
let kind = SymbolKind::Operator(OpKind::Pow);
let children = ChildList::from_slice(&[left, right]);
let hash = DedupMap::hash_operator(&kind, &children);
self.dedup
.get_or_insert(&mut self.arena, kind, hash, children, 1.0, NodeFlags::EMPTY)
}
/// Constructs a floating-point remainder node: `left % right`.
///
/// Semantics follow IEEE-754 `remainder` (same as Cranelift `frem` and Rust `f64::rem`):
/// result has the same sign as the dividend, and `x % 0 → NaN`.
pub fn modulo(&mut self, left: DagNodeId, right: DagNodeId) -> DagNodeId {
let kind = SymbolKind::Operator(OpKind::Mod);
let children = ChildList::from_slice(&[left, right]);
let hash = DedupMap::hash_operator(&kind, &children);
self.dedup
.get_or_insert(&mut self.arena, kind, hash, children, 1.0, NodeFlags::EMPTY)
}
/// Constructs a unary negation node: `-operand`.
pub fn neg(&mut self, operand: DagNodeId) -> DagNodeId {
let kind = SymbolKind::Operator(OpKind::Neg);
let children = ChildList::from_slice(&[operand]);
let hash = DedupMap::hash_operator(&kind, &children);
self.dedup
.get_or_insert(&mut self.arena, kind, hash, children, 1.0, NodeFlags::EMPTY)
}
/// Interns a function name and returns its [`FnId`].
///
/// The function namespace is independent of the variable namespace, so a
/// function named `"x"` never collides with a variable named `"x"`.
pub fn intern_function(&mut self, name: &str) -> FnId {
let sym_id = self.fn_registry.intern(name);
FnId(sym_id.0)
}
/// Returns the name of the function with the given [`FnId`], if interned.
#[must_use]
pub fn function_name(&self, fn_id: FnId) -> Option<&str> {
use super::symbol::SymbolId;
self.fn_registry.name(SymbolId(fn_id.0))
}
/// Constructs a function-call node: `name(args...)`.
///
/// Equivalent to calling [`Self::operator`] with `SymbolKind::Function`,
/// but looks up the function's name in the registry for you.
pub fn function_call(&mut self, fn_id: FnId, args: &[DagNodeId]) -> DagNodeId {
self.operator(SymbolKind::Function(fn_id), args, NodeFlags::EMPTY)
}
/// Constructs an arbitrary custom operator or function node.
pub fn operator(
&mut self,
kind: SymbolKind,
children: &[DagNodeId],
flags: NodeFlags,
) -> DagNodeId {
let children_list = ChildList::from_slice(children);
let hash = DedupMap::hash_operator(&kind, &children_list);
self.dedup
.get_or_insert(&mut self.arena, kind, hash, children_list, 1.0, flags)
}
/// Resets the builder to a completely fresh state.
pub fn clear(&mut self) {
self.arena.clear();
self.dedup.clear();
self.registry = SymbolRegistry::new();
self.fn_registry = SymbolRegistry::new();
}
/// Returns the number of nodes currently in the arena.
#[must_use]
pub const fn node_count(&self) -> usize {
self.arena.len()
}
/// Returns `true` if the arena contains no nodes.
#[must_use]
pub const fn is_empty(&self) -> bool {
self.arena.is_empty()
}
/// Returns a snapshot of the arena in the 32-byte packed format.
///
/// This is the primary bridge to the packed wire representation:
/// the returned [`super::packed::PackedArenaImage`] can be encoded
/// for disk storage, shipped across an FFI boundary as a contiguous
/// byte buffer, or iterated cache-efficiently for batch operations.
///
/// The snapshot is a *copy* — subsequent mutations to the builder do
/// not affect it. For a live zero-copy view, encode then decode via
/// `BorrowedArenaView`.
#[must_use]
pub fn packed_snapshot(&self) -> super::packed::PackedArenaImage {
super::packed::PackedArenaImage::from_arena(&self.arena)
}
/// Iterates all nodes in ID order, calling `f(id, &node)` for each.
///
/// Uses the arena's contiguous storage for cache-friendly sequential
/// access. Equivalent to iterating `0..node_count()` and calling
/// `arena().get(DagNodeId(i))`, but avoids repeated bounds checks.
pub fn for_each_node<F>(&self, mut f: F)
where
F: FnMut(DagNodeId, &super::node::DagNode),
{
for i in 0..self.arena.len() {
let id = DagNodeId(i as u32);
if let Some(node) = self.arena.get(id) {
f(id, node);
}
}
}
/// Parses a textual expression and inserts it into this builder's DAG.
///
/// This is a convenience wrapper around [`crate::parser::parse_expression`].
/// It avoids the need to import the parser separately for simple use cases.
///
/// # Errors
///
/// Returns a [`crate::parser::error::ParseError`] if the expression is
/// syntactically invalid or if the paren depth is exceeded.
pub fn parse(
&mut self,
expr: &str,
) -> Result<super::node::DagNodeId, crate::parser::error::ParseError> {
crate::parser::expr::parse_expression(expr, self)
}
/// Constructs the sum of all nodes in `terms`.
///
/// Returns the single node if `terms` has exactly one element, or
/// constructs a left-associative addition tree otherwise.
///
/// Returns `None` if `terms` is empty.
#[must_use]
pub fn add_many(&mut self, terms: &[super::node::DagNodeId]) -> Option<super::node::DagNodeId> {
let mut iter = terms.iter().copied();
let first = iter.next()?;
Some(iter.fold(first, |acc, t| self.add(acc, t)))
}
/// Constructs the product of all nodes in `factors`.
///
/// Returns the single node if `factors` has exactly one element, or
/// constructs a left-associative multiplication tree otherwise.
///
/// Returns `None` if `factors` is empty.
#[must_use]
pub fn mul_many(
&mut self,
factors: &[super::node::DagNodeId],
) -> Option<super::node::DagNodeId> {
let mut iter = factors.iter().copied();
let first = iter.next()?;
Some(iter.fold(first, |acc, f| self.mul(acc, f)))
}
/// Constructs `base^2` (one multiplication, no `powf` call).
///
/// Equivalent to `self.mul(base, base)` but communicates intent more
/// clearly at the call site.
pub fn square(&mut self, base: super::node::DagNodeId) -> super::node::DagNodeId {
self.mul(base, base)
}
/// Constructs `-(lhs - rhs)` = `rhs - lhs` without an extra negation node.
pub fn sub_rev(
&mut self,
lhs: super::node::DagNodeId,
rhs: super::node::DagNodeId,
) -> super::node::DagNodeId {
self.sub(rhs, lhs)
}
/// Returns the `SymbolId` of a variable, if it has already been interned.
///
/// Unlike [`Self::variable`] this does NOT create a new node or intern the name.
#[must_use]
pub fn lookup_variable(&self, name: &str) -> Option<super::symbol::SymbolId> {
self.registry.lookup(name)
}
/// Returns the `FnId` of a function, if it has already been interned.
#[must_use]
pub fn lookup_function(&self, name: &str) -> Option<super::symbol::FnId> {
self.fn_registry.lookup(name).map(|sid| FnId(sid.0))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_simple_expressions() {
let mut builder = DagBuilder::new();
// Build: x + y
let x = builder.variable("x");
let y = builder.variable("y");
let expr1 = builder.add(x, y);
// Build: x + y again
let expr2 = builder.add(x, y);
assert_eq!(
expr1, expr2,
"Structural deduplication failed for operators"
);
// Build: x * 2.0
let c = builder.constant(2.0);
let expr3 = builder.mul(x, c);
let node = builder.arena().get(expr3).unwrap();
assert_eq!(node.children.len(), 2);
}
}