use std::collections::HashMap;
pub type NodeIndex = u32;
pub type LabelId = u32;
pub const EMPTY_LABEL: LabelId = 0;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IdKind {
Node,
More,
Aggregate,
Loading,
OtherSynthetic,
}
impl IdKind {
pub fn is_synthetic(self) -> bool { !matches!(self, IdKind::Node) }
fn classify(id: &str) -> Self {
if id == "__more" { IdKind::More }
else if id == "__loading" { IdKind::Loading }
else if id.starts_with("__agg:") { IdKind::Aggregate }
else if id.starts_with("__") { IdKind::OtherSynthetic }
else { IdKind::Node }
}
}
#[derive(Debug, Default)]
pub struct Interner {
to_index: HashMap<String, NodeIndex>,
to_id: Vec<String>,
kinds: Vec<IdKind>,
}
impl Interner {
pub fn new() -> Self { Self::default() }
pub fn intern(&mut self, id: &str) -> NodeIndex {
if let Some(&ix) = self.to_index.get(id) { return ix; }
assert!(self.to_id.len() < u32::MAX as usize, "interner full");
let ix = self.to_id.len() as NodeIndex;
self.to_index.insert(id.to_string(), ix);
self.to_id.push(id.to_string());
self.kinds.push(IdKind::classify(id));
ix
}
pub fn get(&self, id: &str) -> Option<NodeIndex> { self.to_index.get(id).copied() }
pub fn resolve(&self, ix: NodeIndex) -> &str { &self.to_id[ix as usize] }
pub fn kind(&self, ix: NodeIndex) -> IdKind { self.kinds[ix as usize] }
pub fn len(&self) -> usize { self.to_id.len() }
pub fn is_empty(&self) -> bool { self.to_id.is_empty() }
}
#[derive(Debug)]
pub struct LabelInterner {
to_index: HashMap<String, LabelId>,
to_text: Vec<String>,
}
impl Default for LabelInterner { fn default() -> Self { Self::new() } }
impl LabelInterner {
pub fn new() -> Self {
let mut s = Self { to_index: HashMap::new(), to_text: Vec::new() };
let zero = s.raw_intern("");
debug_assert_eq!(zero, EMPTY_LABEL);
s
}
fn raw_intern(&mut self, text: &str) -> LabelId {
if let Some(&ix) = self.to_index.get(text) { return ix; }
assert!(self.to_text.len() < u32::MAX as usize, "interner full");
let ix = self.to_text.len() as LabelId;
self.to_index.insert(text.to_string(), ix);
self.to_text.push(text.to_string());
ix
}
pub fn intern(&mut self, text: &str) -> LabelId { self.raw_intern(text) }
pub fn resolve(&self, ix: LabelId) -> &str { &self.to_text[ix as usize] }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn intern_is_idempotent_and_stable() {
let mut i = Interner::new();
let a = i.intern("alpha");
let b = i.intern("beta");
assert_ne!(a, b);
assert_eq!(i.intern("alpha"), a, "re-intern returns the same index");
assert_eq!(i.resolve(a), "alpha");
assert_eq!(i.resolve(b), "beta");
assert_eq!(i.len(), 2);
}
#[test]
fn indices_are_append_only_never_reused() {
let mut i = Interner::new();
let a = i.intern("a");
let b = i.intern("b");
let c = i.intern("c");
assert_eq!((a, b, c), (0, 1, 2));
assert_eq!(i.intern("d"), 3);
}
#[test]
fn kind_is_classified_once_at_intern_time() {
let mut i = Interner::new();
let n = i.intern("node-1");
let m = i.intern("__more");
let g = i.intern("__agg:Sorcery");
let l = i.intern("__loading");
assert_eq!(i.kind(n), IdKind::Node);
assert_eq!(i.kind(m), IdKind::More);
assert_eq!(i.kind(g), IdKind::Aggregate);
assert_eq!(i.kind(l), IdKind::Loading);
assert!(!i.kind(n).is_synthetic());
assert!(i.kind(m).is_synthetic());
assert!(i.kind(g).is_synthetic());
assert!(i.kind(l).is_synthetic());
}
#[test]
fn near_miss_synthetic_ids_fall_back_to_other_synthetic() {
let mut i = Interner::new();
let no_colon_agg = i.intern("__agg"); let near_more = i.intern("__morex");
let bare = i.intern("__x");
assert_eq!(i.kind(no_colon_agg), IdKind::OtherSynthetic);
assert_eq!(i.kind(near_more), IdKind::OtherSynthetic);
assert_eq!(i.kind(bare), IdKind::OtherSynthetic);
assert!(i.kind(no_colon_agg).is_synthetic());
assert!(i.kind(near_more).is_synthetic());
assert!(i.kind(bare).is_synthetic());
}
#[test]
fn lookup_without_interning() {
let mut i = Interner::new();
let a = i.intern("a");
assert_eq!(i.get("a"), Some(a));
assert_eq!(i.get("nope"), None);
}
#[test]
fn label_interner_reserves_zero_for_empty() {
let mut l = LabelInterner::new();
assert_eq!(l.intern(""), EMPTY_LABEL);
let x = l.intern("Sorcery");
assert_ne!(x, EMPTY_LABEL);
assert_eq!(l.intern("Sorcery"), x);
assert_eq!(l.resolve(x), "Sorcery");
assert_eq!(l.resolve(EMPTY_LABEL), "");
}
}