Skip to main content

patch_prolog_core/
term.rs

1use fnv::FnvHashMap;
2use serde::{Deserialize, Serialize};
3
4pub type AtomId = u32;
5pub type VarId = u32;
6
7/// Interned string table: AtomId <-> String mapping.
8/// Atoms are interned at build time so unification compares integers, not strings.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct StringInterner {
11    to_id: FnvHashMap<String, AtomId>,
12    to_str: Vec<String>,
13}
14
15impl StringInterner {
16    pub fn new() -> Self {
17        StringInterner {
18            to_id: FnvHashMap::default(),
19            to_str: Vec::new(),
20        }
21    }
22
23    /// Intern a string, returning its AtomId. If already interned, returns existing id.
24    pub fn intern(&mut self, s: &str) -> AtomId {
25        if let Some(&id) = self.to_id.get(s) {
26            return id;
27        }
28        let id = self.to_str.len() as AtomId;
29        self.to_str.push(s.to_string());
30        self.to_id.insert(s.to_string(), id);
31        id
32    }
33
34    /// Resolve an AtomId back to its string. Panics if id is invalid.
35    pub fn resolve(&self, id: AtomId) -> &str {
36        &self.to_str[id as usize]
37    }
38
39    /// Try to resolve an AtomId, returning None if invalid.
40    pub fn try_resolve(&self, id: AtomId) -> Option<&str> {
41        self.to_str.get(id as usize).map(|s| s.as_str())
42    }
43
44    /// Look up a string without interning it.
45    pub fn lookup(&self, s: &str) -> Option<AtomId> {
46        self.to_id.get(s).copied()
47    }
48
49    pub fn len(&self) -> usize {
50        self.to_str.len()
51    }
52
53    pub fn is_empty(&self) -> bool {
54        self.to_str.is_empty()
55    }
56}
57
58impl Default for StringInterner {
59    fn default() -> Self {
60        Self::new()
61    }
62}
63
64/// Key for first-argument indexing.
65#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
66pub enum FirstArgKey {
67    Atom(AtomId),
68    Integer(i64),
69    Functor(AtomId, usize), // functor atom id + arity
70}
71
72/// Prolog term representation.
73#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
74pub enum Term {
75    Atom(AtomId),
76    Var(VarId),
77    Integer(i64),
78    Float(f64),
79    Compound { functor: AtomId, args: Vec<Term> },
80    List { head: Box<Term>, tail: Box<Term> },
81}
82
83impl Term {
84    /// Extract functor AtomId and arity for a term used as a goal/head.
85    /// - Atom: (atom_id, 0)
86    /// - Compound: (functor, len(args))
87    /// - Others: None
88    pub fn functor_arity(&self) -> Option<(AtomId, usize)> {
89        match self {
90            Term::Atom(id) => Some((*id, 0)),
91            Term::Compound { functor, args } => Some((*functor, args.len())),
92            _ => None,
93        }
94    }
95
96    /// Extract the first-argument indexing key from a term used as a clause head.
97    pub fn first_arg_key(&self) -> Option<FirstArgKey> {
98        let first = match self {
99            Term::Compound { args, .. } if !args.is_empty() => &args[0],
100            _ => return None,
101        };
102        match first {
103            Term::Atom(id) => Some(FirstArgKey::Atom(*id)),
104            Term::Integer(n) => Some(FirstArgKey::Integer(*n)),
105            Term::Compound { functor, args } => Some(FirstArgKey::Functor(*functor, args.len())),
106            _ => None, // Var, Float, List -> not indexable
107        }
108    }
109
110    /// Check if this term is a variable.
111    pub fn is_var(&self) -> bool {
112        matches!(self, Term::Var(_))
113    }
114}
115
116/// A Prolog clause: head :- body.
117/// For facts, body is empty.
118#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
119pub struct Clause {
120    pub head: Term,
121    pub body: Vec<Term>,
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn test_string_interner_basic() {
130        let mut interner = StringInterner::new();
131        let a = interner.intern("hello");
132        let b = interner.intern("world");
133        let c = interner.intern("hello"); // duplicate
134
135        assert_eq!(a, c);
136        assert_ne!(a, b);
137        assert_eq!(interner.resolve(a), "hello");
138        assert_eq!(interner.resolve(b), "world");
139        assert_eq!(interner.len(), 2);
140    }
141
142    #[test]
143    fn test_string_interner_lookup() {
144        let mut interner = StringInterner::new();
145        interner.intern("foo");
146
147        assert_eq!(interner.lookup("foo"), Some(0));
148        assert_eq!(interner.lookup("bar"), None);
149    }
150
151    #[test]
152    fn test_term_functor_arity() {
153        let atom = Term::Atom(0);
154        assert_eq!(atom.functor_arity(), Some((0, 0)));
155
156        let compound = Term::Compound {
157            functor: 1,
158            args: vec![Term::Atom(2), Term::Var(0)],
159        };
160        assert_eq!(compound.functor_arity(), Some((1, 2)));
161
162        let var = Term::Var(0);
163        assert_eq!(var.functor_arity(), None);
164
165        let int = Term::Integer(42);
166        assert_eq!(int.functor_arity(), None);
167    }
168
169    #[test]
170    fn test_first_arg_key() {
171        // Compound with atom first arg
172        let t = Term::Compound {
173            functor: 0,
174            args: vec![Term::Atom(1)],
175        };
176        assert_eq!(t.first_arg_key(), Some(FirstArgKey::Atom(1)));
177
178        // Compound with integer first arg
179        let t = Term::Compound {
180            functor: 0,
181            args: vec![Term::Integer(42)],
182        };
183        assert_eq!(t.first_arg_key(), Some(FirstArgKey::Integer(42)));
184
185        // Compound with variable first arg -> None (not indexable)
186        let t = Term::Compound {
187            functor: 0,
188            args: vec![Term::Var(0)],
189        };
190        assert_eq!(t.first_arg_key(), None);
191
192        // Atom (no args) -> None
193        let t = Term::Atom(0);
194        assert_eq!(t.first_arg_key(), None);
195    }
196
197    #[test]
198    fn test_clause_construction() {
199        let clause = Clause {
200            head: Term::Compound {
201                functor: 0,
202                args: vec![Term::Atom(1), Term::Var(0)],
203            },
204            body: vec![Term::Compound {
205                functor: 2,
206                args: vec![Term::Var(0)],
207            }],
208        };
209        assert_eq!(clause.body.len(), 1);
210        assert_eq!(clause.head.functor_arity(), Some((0, 2)));
211    }
212
213    #[test]
214    fn test_term_serialization() {
215        let term = Term::Compound {
216            functor: 0,
217            args: vec![Term::Atom(1), Term::Integer(42)],
218        };
219        let bytes = bincode::serialize(&term).unwrap();
220        let restored: Term = bincode::deserialize(&bytes).unwrap();
221        assert_eq!(term, restored);
222    }
223}