Skip to main content

patch_prolog_shared/
interner.rs

1//! String interner mapping atom names to dense `AtomId`s.
2//!
3//! Ported from patch-prolog's `term.rs` interner (same API), with two
4//! deliberate changes:
5//! - no serde/fnv dependencies (this crate ships in every compiled
6//!   binary and must stay dependency-free)
7//! - pre-seeds the well-known atoms so fixed ids hold (see `atom.rs`)
8
9use crate::atom::{AtomId, WELL_KNOWN_ATOMS};
10use std::collections::HashMap;
11
12#[derive(Debug, Clone)]
13pub struct StringInterner {
14    to_id: HashMap<String, AtomId>,
15    to_str: Vec<String>,
16}
17
18impl StringInterner {
19    pub fn new() -> Self {
20        let mut interner = StringInterner {
21            to_id: HashMap::new(),
22            to_str: Vec::new(),
23        };
24        for name in WELL_KNOWN_ATOMS {
25            interner.intern(name);
26        }
27        interner
28    }
29
30    /// Intern a string, returning its AtomId. If already interned, returns existing id.
31    pub fn intern(&mut self, s: &str) -> AtomId {
32        if let Some(&id) = self.to_id.get(s) {
33            return id;
34        }
35        let id = self.to_str.len() as AtomId;
36        self.to_str.push(s.to_string());
37        self.to_id.insert(s.to_string(), id);
38        id
39    }
40
41    /// Resolve an AtomId back to its string. Panics if id is invalid.
42    pub fn resolve(&self, id: AtomId) -> &str {
43        &self.to_str[id as usize]
44    }
45
46    /// Try to resolve an AtomId, returning None if invalid.
47    pub fn try_resolve(&self, id: AtomId) -> Option<&str> {
48        self.to_str.get(id as usize).map(|s| s.as_str())
49    }
50
51    /// Look up a string without interning it.
52    pub fn lookup(&self, s: &str) -> Option<AtomId> {
53        self.to_id.get(s).copied()
54    }
55
56    pub fn len(&self) -> usize {
57        self.to_str.len()
58    }
59
60    pub fn is_empty(&self) -> bool {
61        self.to_str.is_empty()
62    }
63
64    /// Iterate names in id order (used by codegen to emit the atom table).
65    pub fn iter(&self) -> impl Iterator<Item = &str> {
66        self.to_str.iter().map(|s| s.as_str())
67    }
68}
69
70impl Default for StringInterner {
71    fn default() -> Self {
72        Self::new()
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use crate::atom::{ATOM_DOT, ATOM_NIL, ATOM_TRUE};
80
81    #[test]
82    fn well_known_atoms_have_fixed_ids() {
83        let mut i = StringInterner::new();
84        assert_eq!(i.intern("[]"), ATOM_NIL);
85        assert_eq!(i.intern("."), ATOM_DOT);
86        assert_eq!(i.intern("true"), ATOM_TRUE);
87        assert_eq!(i.resolve(ATOM_NIL), "[]");
88    }
89
90    #[test]
91    fn test_string_interner_basic() {
92        let mut interner = StringInterner::new();
93        let a = interner.intern("hello");
94        let b = interner.intern("world");
95        let c = interner.intern("hello"); // duplicate
96
97        assert_eq!(a, c);
98        assert_ne!(a, b);
99        assert_eq!(interner.resolve(a), "hello");
100        assert_eq!(interner.resolve(b), "world");
101    }
102
103    #[test]
104    fn test_string_interner_lookup() {
105        let mut interner = StringInterner::new();
106        let id = interner.intern("foo");
107
108        assert_eq!(interner.lookup("foo"), Some(id));
109        assert_eq!(interner.lookup("bar"), None);
110    }
111}