Skip to main content

oxilean_std/logic_programming/
types.rs

1//! Types for Prolog-style logic programming engine.
2
3use std::collections::HashMap;
4
5// ── Terms ─────────────────────────────────────────────────────────────────────
6
7/// A Prolog-style term.
8#[derive(Debug, Clone, PartialEq)]
9pub enum LpTerm {
10    /// Atom: a ground constant, e.g. `foo`, `[]`, `true`.
11    Atom(String),
12    /// Variable: begins with uppercase or `_`, e.g. `X`, `_Y`.
13    Var(String),
14    /// Compound term: `f(t1, ..., tn)`.
15    Compound {
16        /// The functor name.
17        functor: String,
18        /// Arguments.
19        args: Vec<LpTerm>,
20    },
21    /// Integer literal.
22    Integer(i64),
23    /// Float literal.
24    Float(f64),
25    /// List `[h1, h2, ... | Tail]` — proper list when `tail` is `None`.
26    List(Vec<LpTerm>, Option<Box<LpTerm>>),
27}
28
29impl LpTerm {
30    /// Construct an atom.
31    pub fn atom(s: impl Into<String>) -> Self {
32        Self::Atom(s.into())
33    }
34
35    /// Construct a variable.
36    pub fn var(s: impl Into<String>) -> Self {
37        Self::Var(s.into())
38    }
39
40    /// Construct a compound term.
41    pub fn compound(functor: impl Into<String>, args: Vec<LpTerm>) -> Self {
42        Self::Compound {
43            functor: functor.into(),
44            args,
45        }
46    }
47
48    /// Construct a proper list (no tail variable).
49    pub fn list(items: Vec<LpTerm>) -> Self {
50        Self::List(items, None)
51    }
52
53    /// Construct a partial list with tail variable.
54    pub fn list_with_tail(items: Vec<LpTerm>, tail: LpTerm) -> Self {
55        Self::List(items, Some(Box::new(tail)))
56    }
57
58    /// Return true if this term is ground (contains no variables).
59    pub fn is_ground(&self) -> bool {
60        match self {
61            Self::Var(_) => false,
62            Self::Atom(_) | Self::Integer(_) | Self::Float(_) => true,
63            Self::Compound { args, .. } => args.iter().all(|a| a.is_ground()),
64            Self::List(items, tail) => {
65                items.iter().all(|a| a.is_ground()) && tail.as_ref().map_or(true, |t| t.is_ground())
66            }
67        }
68    }
69
70    /// Collect all variable names in this term (without deduplication).
71    pub fn variables(&self) -> Vec<String> {
72        let mut vars = Vec::new();
73        self.collect_vars(&mut vars);
74        vars
75    }
76
77    fn collect_vars(&self, out: &mut Vec<String>) {
78        match self {
79            Self::Var(v) => out.push(v.clone()),
80            Self::Compound { args, .. } => {
81                for a in args {
82                    a.collect_vars(out);
83                }
84            }
85            Self::List(items, tail) => {
86                for a in items {
87                    a.collect_vars(out);
88                }
89                if let Some(t) = tail {
90                    t.collect_vars(out);
91                }
92            }
93            _ => {}
94        }
95    }
96}
97
98// ── Clauses & Database ────────────────────────────────────────────────────────
99
100/// A Horn clause: `head :- body1, body2, ...`  or a fact: `head.` (empty body).
101#[derive(Debug, Clone)]
102pub struct LpClause {
103    /// The head of the clause.
104    pub head: LpTerm,
105    /// The body (conjunction of goals). Empty for facts.
106    pub body: Vec<LpTerm>,
107}
108
109impl LpClause {
110    /// Construct a fact (no body).
111    pub fn fact(head: LpTerm) -> Self {
112        Self { head, body: vec![] }
113    }
114
115    /// Construct a rule.
116    pub fn rule(head: LpTerm, body: Vec<LpTerm>) -> Self {
117        Self { head, body }
118    }
119
120    /// True if this clause has no body (is a fact).
121    pub fn is_fact(&self) -> bool {
122        self.body.is_empty()
123    }
124}
125
126/// A logic programming database of Horn clauses.
127#[derive(Debug, Clone, Default)]
128pub struct LpDatabase {
129    /// All clauses, in order of assertion.
130    pub clauses: Vec<LpClause>,
131}
132
133impl LpDatabase {
134    /// Create an empty database.
135    pub fn new() -> Self {
136        Self { clauses: vec![] }
137    }
138
139    /// Add a fact to the database.
140    pub fn add_fact(&mut self, head: LpTerm) {
141        self.clauses.push(LpClause::fact(head));
142    }
143
144    /// Add a rule to the database.
145    pub fn add_rule(&mut self, head: LpTerm, body: Vec<LpTerm>) {
146        self.clauses.push(LpClause::rule(head, body));
147    }
148
149    /// Return all clauses whose head functor/arity matches the given goal.
150    pub fn matching_clauses(&self, goal: &LpTerm) -> Vec<&LpClause> {
151        let (goal_f, goal_a) = functor_arity(goal);
152        self.clauses
153            .iter()
154            .filter(|c| {
155                let (hf, ha) = functor_arity(&c.head);
156                hf == goal_f && ha == goal_a
157            })
158            .collect()
159    }
160}
161
162/// Extract (functor_name, arity) for unification index.
163pub(super) fn functor_arity(t: &LpTerm) -> (&str, usize) {
164    match t {
165        LpTerm::Atom(s) => (s.as_str(), 0),
166        LpTerm::Compound { functor, args } => (functor.as_str(), args.len()),
167        LpTerm::Integer(_) => ("$int", 0),
168        LpTerm::Float(_) => ("$float", 0),
169        LpTerm::Var(_) => ("$var", 0),
170        LpTerm::List(items, None) => ("$list", items.len()),
171        LpTerm::List(items, Some(_)) => ("$list_partial", items.len()),
172    }
173}
174
175// ── Substitution ──────────────────────────────────────────────────────────────
176
177/// A substitution: a finite map from variable names to terms.
178#[derive(Debug, Clone, Default)]
179pub struct Substitution {
180    /// Variable bindings.
181    pub bindings: HashMap<String, LpTerm>,
182}
183
184impl Substitution {
185    /// Create an empty substitution.
186    pub fn new() -> Self {
187        Self {
188            bindings: HashMap::new(),
189        }
190    }
191
192    /// Bind a variable to a term.
193    pub fn bind(&mut self, var: impl Into<String>, term: LpTerm) {
194        self.bindings.insert(var.into(), term);
195    }
196
197    /// Extend this substitution with all bindings from `other`.
198    pub fn extend(&self, other: &Substitution) -> Substitution {
199        let mut result = self.clone();
200        for (k, v) in &other.bindings {
201            result.bindings.insert(k.clone(), v.clone());
202        }
203        result
204    }
205
206    /// Look up the current binding of a variable (may need further dereferencing).
207    pub fn lookup(&self, var: &str) -> Option<&LpTerm> {
208        self.bindings.get(var)
209    }
210
211    /// Dereference a variable to its ultimate binding (or back to a variable if unbound).
212    pub fn deref(&self, var: &str) -> LpTerm {
213        match self.bindings.get(var) {
214            None => LpTerm::Var(var.to_string()),
215            Some(LpTerm::Var(v2)) if v2 != var => self.deref(v2),
216            Some(t) => t.clone(),
217        }
218    }
219}
220
221// ── Query ─────────────────────────────────────────────────────────────────────
222
223/// A conjunctive query: a list of goals to be solved simultaneously.
224#[derive(Debug, Clone)]
225pub struct Query {
226    /// Goals in left-to-right order.
227    pub goals: Vec<LpTerm>,
228}
229
230impl Query {
231    /// Construct a query from a list of goals.
232    pub fn new(goals: Vec<LpTerm>) -> Self {
233        Self { goals }
234    }
235
236    /// Construct a single-goal query.
237    pub fn single(goal: LpTerm) -> Self {
238        Self { goals: vec![goal] }
239    }
240}
241
242// ── Resolution result ─────────────────────────────────────────────────────────
243
244/// The outcome of resolving a query.
245#[derive(Debug, Clone)]
246pub enum ResolutionResult {
247    /// A successful substitution answering the query.
248    Success(Substitution),
249    /// The query has no solution.
250    Failure,
251    /// An error occurred during resolution (e.g., depth limit exceeded).
252    Error(String),
253}
254
255// ── Solver configuration ──────────────────────────────────────────────────────
256
257/// Configuration for the SLD resolution engine.
258#[derive(Debug, Clone)]
259pub struct SolveConfig {
260    /// Maximum recursion depth before failing with an error.
261    pub max_depth: usize,
262    /// Maximum number of solutions to collect.
263    pub max_solutions: usize,
264    /// Whether to perform the occurs check during unification.
265    pub occurs_check: bool,
266}
267
268impl Default for SolveConfig {
269    fn default() -> Self {
270        Self {
271            max_depth: 512,
272            max_solutions: 1024,
273            occurs_check: false,
274        }
275    }
276}
277
278impl SolveConfig {
279    /// Create a default configuration.
280    pub fn new() -> Self {
281        Self::default()
282    }
283}