use super::arena::DagArena;
use super::metadata::NodeHash;
use super::node::{ChildList, DagNode, DagNodeId};
use super::symbol::SymbolKind;
const INIT_CAP: usize = 64;
#[derive(Clone, Default)]
#[allow(dead_code)]
enum Slot {
#[default]
Empty,
Tombstone,
Occupied {
raw_hash: u64,
id: DagNodeId,
},
}
#[derive(Clone)]
pub struct DedupMap {
slots: Vec<Slot>,
len: usize,
cap: usize,
}
impl Default for DedupMap {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for DedupMap {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DedupMap")
.field("len", &self.len)
.field("cap", &self.cap)
.finish_non_exhaustive() }
}
impl DedupMap {
#[must_use]
pub fn new() -> Self {
let cap = INIT_CAP;
Self {
slots: vec![Slot::Empty; cap],
len: 0,
cap,
}
}
#[must_use]
pub fn hash_variable(kind: &SymbolKind) -> NodeHash {
use std::hash::{Hash, Hasher};
let mut h = rapidhash::fast::RapidHasher::default();
kind.hash(&mut h);
NodeHash(h.finish())
}
#[must_use]
pub fn hash_constant(kind: &SymbolKind) -> NodeHash {
let val = if let SymbolKind::Constant(v) = kind {
*v
} else {
0.0
};
use std::hash::Hasher;
let mut h = rapidhash::fast::RapidHasher::default();
h.write_u64(val.to_bits());
NodeHash(h.finish())
}
#[must_use]
pub fn hash_operator(kind: &SymbolKind, children: &ChildList) -> NodeHash {
Self::hash_operator_full(kind, children, 1.0, super::metadata::NodeFlags::EMPTY)
}
#[must_use]
pub fn hash_operator_full(
kind: &SymbolKind,
children: &ChildList,
coefficient: f64,
flags: super::metadata::NodeFlags,
) -> NodeHash {
use std::hash::{Hash, Hasher};
let mut h = rapidhash::fast::RapidHasher::default();
kind.hash(&mut h);
for &c in children.as_slice() {
c.0.hash(&mut h);
}
h.write_u64(coefficient.to_bits());
h.write_u8(flags.bits());
NodeHash(h.finish())
}
#[inline]
const fn start_idx(&self, raw_hash: u64) -> usize {
(raw_hash as usize) & (self.cap - 1)
}
pub fn get_or_insert(
&mut self,
arena: &mut DagArena,
kind: SymbolKind,
hash: NodeHash,
children: ChildList,
coefficient: f64,
flags: super::metadata::NodeFlags,
) -> DagNodeId {
if self.len + 1 > self.cap * 7 / 10 {
self.grow();
}
let raw = hash.0;
let mut idx = self.start_idx(raw);
loop {
match &self.slots[idx] {
Slot::Empty | Slot::Tombstone => {
let meta = super::metadata::NodeMetadata {
hash,
coefficient,
flags,
};
let node = DagNode {
kind,
meta,
children,
};
let new_id = arena.alloc(node);
self.slots[idx] = Slot::Occupied {
raw_hash: raw,
id: new_id,
};
self.len += 1;
return new_id;
}
Slot::Occupied { raw_hash, id } => {
if *raw_hash == raw {
let found_id = *id;
if let Some(existing) = arena.get(found_id)
&& existing.kind == kind
&& existing.children == children
&& existing.meta.coefficient.to_bits() == coefficient.to_bits()
&& existing.meta.flags.without_canonical() == flags.without_canonical()
{
return found_id;
}
}
idx = (idx + 1) & (self.cap - 1);
}
}
}
}
fn grow(&mut self) {
let new_cap = self.cap * 2;
let mut new_slots: Vec<Slot> = (0..new_cap).map(|_| Slot::Empty).collect();
for slot in self.slots.drain(..) {
if let Slot::Occupied { raw_hash, id } = slot {
let mut idx = (raw_hash as usize) & (new_cap - 1);
loop {
match &new_slots[idx] {
Slot::Empty => {
new_slots[idx] = Slot::Occupied { raw_hash, id };
break;
}
_ => {
idx = (idx + 1) & (new_cap - 1);
}
}
}
}
}
self.slots = new_slots;
self.cap = new_cap;
}
pub fn clear(&mut self) {
for slot in &mut self.slots {
*slot = Slot::Empty;
}
self.len = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dag::metadata::NodeFlags;
use crate::dag::symbol::SymbolId;
#[test]
fn test_dedup_constant() {
let mut arena = DagArena::new();
let mut dedup = DedupMap::new();
let val = 3.14;
let kind = SymbolKind::Constant(val);
let hash = DedupMap::hash_constant(&kind);
let id1 = dedup.get_or_insert(
&mut arena,
kind,
hash,
ChildList::Empty,
1.0,
NodeFlags::EMPTY,
);
let id2 = dedup.get_or_insert(
&mut arena,
kind,
hash,
ChildList::Empty,
1.0,
NodeFlags::EMPTY,
);
assert_eq!(
id1, id2,
"Identical constants must resolve to the same node ID"
);
assert_eq!(
arena.len(),
1,
"Only one node should be allocated in the arena"
);
}
#[test]
fn test_dedup_variable() {
let mut arena = DagArena::new();
let mut dedup = DedupMap::new();
let kind = SymbolKind::Variable(SymbolId(0));
let hash = DedupMap::hash_variable(&kind);
let id1 = dedup.get_or_insert(
&mut arena,
kind,
hash,
ChildList::Empty,
1.0,
NodeFlags::EMPTY,
);
let id2 = dedup.get_or_insert(
&mut arena,
kind,
hash,
ChildList::Empty,
1.0,
NodeFlags::EMPTY,
);
assert_eq!(
id1, id2,
"Identical variables must resolve to the same node ID"
);
assert_eq!(arena.len(), 1);
}
#[test]
fn flat_table_grows_and_deduplicates() {
let mut arena = DagArena::new();
let mut dedup = DedupMap::new();
let mut ids = Vec::new();
for i in 0..50u32 {
let kind = SymbolKind::Variable(SymbolId(i));
let hash = DedupMap::hash_variable(&kind);
let id = dedup.get_or_insert(
&mut arena,
kind,
hash,
ChildList::Empty,
1.0,
NodeFlags::EMPTY,
);
ids.push(id);
}
assert_eq!(
arena.len(),
50,
"50 distinct variables should produce 50 nodes"
);
for i in 0..50u32 {
let kind = SymbolKind::Variable(SymbolId(i));
let hash = DedupMap::hash_variable(&kind);
let id = dedup.get_or_insert(
&mut arena,
kind,
hash,
ChildList::Empty,
1.0,
NodeFlags::EMPTY,
);
assert_eq!(
id, ids[i as usize],
"dedup failed after grow for variable {i}"
);
}
assert_eq!(
arena.len(),
50,
"No new nodes should be allocated on re-lookup"
);
}
#[test]
fn clear_resets_flat_table() {
let mut arena = DagArena::new();
let mut dedup = DedupMap::new();
let kind = SymbolKind::Constant(1.0);
let hash = DedupMap::hash_constant(&kind);
let _ = dedup.get_or_insert(
&mut arena,
kind,
hash,
ChildList::Empty,
1.0,
NodeFlags::EMPTY,
);
assert_eq!(dedup.len, 1);
dedup.clear();
assert_eq!(dedup.len, 0);
let id2 = dedup.get_or_insert(
&mut arena,
kind,
hash,
ChildList::Empty,
1.0,
NodeFlags::EMPTY,
);
assert_eq!(arena.len(), 2);
assert_eq!(dedup.len, 1);
assert_eq!(id2, DagNodeId::new(1));
}
}