Skip to main content

sim_lib_logic/
clause.rs

1//! Clause model: the facts and rules that make up a logic program.
2//!
3//! A [`Clause`] is the parsed form of a `(fact ...)` or `(rule head body)`
4//! surface expression; [`parse_clause_expr`] performs that parse. Clauses are
5//! the concrete behavior this organ adds on top of the kernel `Expr` graph; see
6//! the [`README`](https://docs.rs/sim-runtime).
7
8use std::collections::BTreeMap;
9
10use sim_kernel::{Expr, Origin, Result, Symbol};
11
12use crate::error::{ensure, logic_eval_error};
13
14/// Stable identifier assigned to a clause when it is added to a database.
15#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub struct ClauseId(pub usize);
17
18/// A parsed logic clause: a head goal plus an optional list of body goals.
19///
20/// A clause with an empty `body` is a fact; one with body goals is a rule.
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub struct Clause {
23    /// Identifier of this clause within its database.
24    pub id: ClauseId,
25    /// The head goal (the conclusion the clause proves).
26    pub head: Expr,
27    /// Body goals that must all hold for the head to hold; empty for a fact.
28    pub body: Vec<Expr>,
29    /// Source origin of the clause, when known.
30    pub source: Option<Origin>,
31}
32
33impl Clause {
34    /// Returns the predicate symbol of the clause head.
35    pub fn predicate(&self) -> Result<Symbol> {
36        predicate_symbol(&self.head)
37    }
38
39    /// Returns the arity (argument count) of the clause head.
40    pub fn arity(&self) -> Result<usize> {
41        goal_arity(&self.head)
42    }
43
44    /// Renders the clause back to its canonical `(fact ...)` or `(rule ...)`
45    /// surface expression.
46    pub fn fact_expr(&self) -> Expr {
47        if self.body.is_empty() {
48            Expr::List(vec![
49                Expr::Symbol(Symbol::new("fact")),
50                normalize_goal_expr(&self.head),
51            ])
52        } else {
53            Expr::List(vec![
54                Expr::Symbol(Symbol::new("rule")),
55                normalize_goal_expr(&self.head),
56                Expr::List(self.body.iter().map(normalize_goal_expr).collect()),
57            ])
58        }
59    }
60}
61
62/// Parses a `(fact head)` or `(rule head body)` expression into a [`Clause`].
63///
64/// The expression must be a list whose first element is the symbol `fact` or
65/// `rule`; any other form is rejected with a logic eval error.
66///
67/// # Examples
68///
69/// ```
70/// use sim_kernel::{Expr, Symbol};
71/// use sim_lib_logic::{ClauseId, parse_clause_expr};
72///
73/// let fact = Expr::List(vec![
74///     Expr::Symbol(Symbol::new("fact")),
75///     Expr::List(vec![
76///         Expr::Symbol(Symbol::new("parent")),
77///         Expr::Symbol(Symbol::new("alice")),
78///         Expr::Symbol(Symbol::new("bob")),
79///     ]),
80/// ]);
81/// let clause = parse_clause_expr(ClauseId(1), fact).unwrap();
82/// assert!(clause.body.is_empty());
83/// assert_eq!(clause.predicate().unwrap(), Symbol::new("parent"));
84/// assert_eq!(clause.arity().unwrap(), 2);
85/// ```
86pub fn parse_clause_expr(id: ClauseId, expr: Expr) -> Result<Clause> {
87    let Expr::List(items) = expr else {
88        return Err(logic_eval_error("logic clause must be a list"));
89    };
90    let Some((head, tail)) = items.split_first() else {
91        return Err(logic_eval_error("logic clause cannot be empty"));
92    };
93    match head {
94        Expr::Symbol(symbol) if symbol.name.as_ref() == "fact" => parse_fact(id, tail),
95        Expr::Symbol(symbol) if symbol.name.as_ref() == "rule" => parse_rule(id, tail),
96        _ => Err(logic_eval_error(
97            "logic clause must start with fact or rule",
98        )),
99    }
100}
101
102pub fn parse_goal_expr(expr: &Expr) -> Result<Expr> {
103    ensure(is_goal_expr(expr), "logic goal must be a call-shaped list")?;
104    Ok(normalize_goal_expr(expr))
105}
106
107pub(crate) fn is_cut_expr(expr: &Expr) -> bool {
108    matches!(expr, Expr::Symbol(symbol) if symbol.namespace.is_none() && symbol.name.as_ref() == "!")
109}
110
111pub fn is_goal_expr(expr: &Expr) -> bool {
112    match expr {
113        Expr::List(items) => matches!(items.first(), Some(Expr::Symbol(_))),
114        Expr::Call { operator, .. } => matches!(operator.as_ref(), Expr::Symbol(_)),
115        _ => false,
116    }
117}
118
119pub fn predicate_symbol(expr: &Expr) -> Result<Symbol> {
120    match expr {
121        Expr::List(items) => match items.first() {
122            Some(Expr::Symbol(symbol)) => Ok(symbol.clone()),
123            _ => Err(logic_eval_error("goal head operator must be a symbol")),
124        },
125        Expr::Call { operator, .. } => match operator.as_ref() {
126            Expr::Symbol(symbol) => Ok(symbol.clone()),
127            _ => Err(logic_eval_error("goal head operator must be a symbol")),
128        },
129        _ => Err(logic_eval_error("goal must be call-shaped")),
130    }
131}
132
133pub fn goal_arity(expr: &Expr) -> Result<usize> {
134    match expr {
135        Expr::List(items) => Ok(items.len().saturating_sub(1)),
136        Expr::Call { args, .. } => Ok(args.len()),
137        _ => Err(logic_eval_error("goal must be call-shaped")),
138    }
139}
140
141pub fn goal_first_arg(expr: &Expr) -> Option<&Expr> {
142    match expr {
143        Expr::List(items) => items.get(1),
144        Expr::Call { args, .. } => args.first(),
145        _ => None,
146    }
147}
148
149pub fn normalize_goal_expr(expr: &Expr) -> Expr {
150    match expr {
151        Expr::Call { operator, args } => {
152            let mut items = Vec::with_capacity(args.len() + 1);
153            items.push((**operator).clone());
154            items.extend(args.iter().cloned());
155            Expr::List(items)
156        }
157        other => other.clone(),
158    }
159}
160
161pub fn rename_clause_apart(clause: &Clause, depth: usize) -> Clause {
162    let mut renamed = BTreeMap::new();
163    let mut anonymous_index = 0usize;
164    let suffix = format!("{}:{}", clause.id.0, depth);
165    Clause {
166        id: clause.id,
167        head: rename_expr(&clause.head, &suffix, &mut renamed, &mut anonymous_index),
168        body: clause
169            .body
170            .iter()
171            .map(|goal| rename_expr(goal, &suffix, &mut renamed, &mut anonymous_index))
172            .collect(),
173        source: clause.source.clone(),
174    }
175}
176
177fn parse_fact(id: ClauseId, tail: &[Expr]) -> Result<Clause> {
178    let [head] = tail else {
179        return Err(logic_eval_error("fact expects one call head"));
180    };
181    let head = parse_goal_expr(head)?;
182    Ok(Clause {
183        id,
184        head,
185        body: Vec::new(),
186        source: None,
187    })
188}
189
190fn parse_rule(id: ClauseId, tail: &[Expr]) -> Result<Clause> {
191    let [head, body] = tail else {
192        return Err(logic_eval_error("rule expects head plus body list"));
193    };
194    let head = parse_goal_expr(head)?;
195    let Expr::List(goals) = body else {
196        return Err(logic_eval_error("rule body must be a list of goals"));
197    };
198    let body = goals
199        .iter()
200        .map(parse_rule_body_goal)
201        .collect::<Result<Vec<_>>>()?;
202    Ok(Clause {
203        id,
204        head,
205        body,
206        source: None,
207    })
208}
209
210fn parse_rule_body_goal(expr: &Expr) -> Result<Expr> {
211    if is_cut_expr(expr) {
212        return Ok(expr.clone());
213    }
214    parse_goal_expr(expr)
215}
216
217fn rename_expr(
218    expr: &Expr,
219    suffix: &str,
220    renamed: &mut BTreeMap<Symbol, Symbol>,
221    anonymous_index: &mut usize,
222) -> Expr {
223    match expr {
224        Expr::Local(var) if var.name.as_ref() == "_" => {
225            *anonymous_index += 1;
226            Expr::Local(Symbol::new(format!("__anon_{suffix}_{anonymous_index}")))
227        }
228        Expr::Local(var) => {
229            let name = renamed
230                .entry(var.clone())
231                .or_insert_with(|| Symbol::new(format!("{}@{suffix}", var.name)))
232                .clone();
233            Expr::Local(name)
234        }
235        Expr::List(items) => Expr::List(
236            items
237                .iter()
238                .map(|item| rename_expr(item, suffix, renamed, anonymous_index))
239                .collect(),
240        ),
241        Expr::Vector(items) => Expr::Vector(
242            items
243                .iter()
244                .map(|item| rename_expr(item, suffix, renamed, anonymous_index))
245                .collect(),
246        ),
247        Expr::Map(entries) => Expr::Map(
248            entries
249                .iter()
250                .map(|(key, value)| {
251                    (
252                        rename_expr(key, suffix, renamed, anonymous_index),
253                        rename_expr(value, suffix, renamed, anonymous_index),
254                    )
255                })
256                .collect(),
257        ),
258        Expr::Set(items) => Expr::Set(
259            items
260                .iter()
261                .map(|item| rename_expr(item, suffix, renamed, anonymous_index))
262                .collect(),
263        ),
264        Expr::Call { operator, args } => Expr::Call {
265            operator: Box::new(rename_expr(operator, suffix, renamed, anonymous_index)),
266            args: args
267                .iter()
268                .map(|arg| rename_expr(arg, suffix, renamed, anonymous_index))
269                .collect(),
270        },
271        Expr::Infix {
272            operator,
273            left,
274            right,
275        } => Expr::Infix {
276            operator: operator.clone(),
277            left: Box::new(rename_expr(left, suffix, renamed, anonymous_index)),
278            right: Box::new(rename_expr(right, suffix, renamed, anonymous_index)),
279        },
280        Expr::Prefix { operator, arg } => Expr::Prefix {
281            operator: operator.clone(),
282            arg: Box::new(rename_expr(arg, suffix, renamed, anonymous_index)),
283        },
284        Expr::Postfix { operator, arg } => Expr::Postfix {
285            operator: operator.clone(),
286            arg: Box::new(rename_expr(arg, suffix, renamed, anonymous_index)),
287        },
288        Expr::Block(items) => Expr::Block(
289            items
290                .iter()
291                .map(|item| rename_expr(item, suffix, renamed, anonymous_index))
292                .collect(),
293        ),
294        Expr::Quote { mode, expr } => Expr::Quote {
295            mode: *mode,
296            expr: Box::new(rename_expr(expr, suffix, renamed, anonymous_index)),
297        },
298        Expr::Annotated { expr, annotations } => Expr::Annotated {
299            expr: Box::new(rename_expr(expr, suffix, renamed, anonymous_index)),
300            annotations: annotations
301                .iter()
302                .map(|(name, value)| {
303                    (
304                        name.clone(),
305                        rename_expr(value, suffix, renamed, anonymous_index),
306                    )
307                })
308                .collect(),
309        },
310        Expr::Extension { tag, payload } => Expr::Extension {
311            tag: tag.clone(),
312            payload: Box::new(rename_expr(payload, suffix, renamed, anonymous_index)),
313        },
314        other => other.clone(),
315    }
316}