Skip to main content

sim_lib_logic/
query.rs

1use std::{
2    collections::{BTreeMap, VecDeque},
3    fmt,
4    sync::{Arc, Mutex},
5};
6
7use sim_kernel::{Cx, Error, Expr, Result, Sequence, ShapeMatch, Symbol, Value, shape_match_value};
8use sim_lib_sequence::{LazySequence, SequenceProducer};
9
10use crate::{
11    builtins::{BuiltinCtx, BuiltinTable},
12    clause::{
13        is_cut_expr, is_goal_expr, normalize_goal_expr, predicate_symbol, rename_clause_apart,
14    },
15    cut::{CutPrompt, raise_cut_prompt},
16    db::LogicDb,
17    env::LogicEnv,
18    error::logic_eval_error,
19    model::{LogicConfig, SearchStrategy},
20    naf::{NafDemand, naf_inner_goal},
21    stream::LogicStream,
22    unify::occurs_check,
23};
24
25/// A goal paired with the [`LogicConfig`] under which it should be resolved.
26#[derive(Clone, Debug)]
27pub struct LogicQuery {
28    /// The goal expression to prove.
29    pub goal: Expr,
30    /// Tuning applied while resolving the goal.
31    pub config: LogicConfig,
32}
33
34/// Resolves `goal` against `db` and returns the answers as a logic stream.
35///
36/// Resolution is bounded by `config`; the answer count is capped at
37/// `config.limits.max_answers`.
38///
39/// # Examples
40///
41/// ```
42/// use std::sync::Arc;
43/// use sim_kernel::{Cx, DefaultFactory, EagerPolicy, Expr, Symbol};
44/// use sim_lib_logic::{LogicConfig, LogicDb, query};
45///
46/// let mut cx = Cx::new(Arc::new(EagerPolicy), Arc::new(DefaultFactory));
47/// let mut db = LogicDb::new();
48/// db.assert_clause_expr(Expr::List(vec![
49///     Expr::Symbol(Symbol::new("fact")),
50///     Expr::List(vec![
51///         Expr::Symbol(Symbol::new("parent")),
52///         Expr::Symbol(Symbol::new("alice")),
53///         Expr::Symbol(Symbol::new("bob")),
54///     ]),
55/// ]))
56/// .unwrap();
57///
58/// let goal = Expr::List(vec![
59///     Expr::Symbol(Symbol::new("parent")),
60///     Expr::Symbol(Symbol::new("alice")),
61///     Expr::Local(Symbol::new("who")),
62/// ]);
63/// let stream = query(&mut cx, &db, &LogicConfig::default(), goal).unwrap();
64/// let answers = stream.collect(&mut cx, None).unwrap();
65/// assert_eq!(answers.len(), 1);
66/// ```
67pub fn query(cx: &mut Cx, db: &LogicDb, config: &LogicConfig, goal: Expr) -> Result<LogicStream> {
68    let _ = cx;
69    let engine = SequenceEngine::new(db.clone(), config.clone(), goal, config.limits.max_answers)?;
70    Ok(LogicStream::from_engine(engine, config.stream_buffer))
71}
72
73/// Resolves `goal` against `db` and returns all answers up to `limit`.
74///
75/// When `limit` is `None`, the answer count is capped by
76/// `config.limits.max_answers`. Each answer is a [`ShapeMatch`] whose captures
77/// contain the variables bound while proving the goal.
78pub fn query_all(
79    cx: &mut Cx,
80    db: &LogicDb,
81    config: &LogicConfig,
82    goal: Expr,
83    limit: Option<usize>,
84) -> Result<Vec<ShapeMatch>> {
85    let answer_limit = limit.or(config.limits.max_answers);
86    let mut engine = SequenceEngine::new(db.clone(), config.clone(), goal, answer_limit)?;
87    engine.force(cx, answer_limit)
88}
89
90/// Resolves `goal` with an explicit builtin table.
91///
92/// This is the open-registry query surface for callers that install additional
93/// builtin bindings as data. It uses the same resolver as [`query_all`], but
94/// starts the stream with `builtins` instead of [`BuiltinTable::standard`].
95pub fn query_all_with_builtins(
96    cx: &mut Cx,
97    db: &LogicDb,
98    config: &LogicConfig,
99    goal: Expr,
100    limit: Option<usize>,
101    builtins: BuiltinTable,
102) -> Result<Vec<ShapeMatch>> {
103    let answer_limit = limit.or(config.limits.max_answers);
104    let mut engine = SequenceEngine::new_with_builtins(
105        db.clone(),
106        config.clone(),
107        goal,
108        answer_limit,
109        builtins,
110    )?;
111    engine.force(cx, answer_limit)
112}
113
114pub(crate) struct SequenceEngine {
115    sequence: LazySequence,
116    state: Arc<Mutex<SequenceEngineState>>,
117}
118
119impl SequenceEngine {
120    pub(crate) fn new(
121        db: LogicDb,
122        config: LogicConfig,
123        goal: Expr,
124        answer_limit: Option<usize>,
125    ) -> Result<Self> {
126        Self::new_with_builtins(db, config, goal, answer_limit, BuiltinTable::standard())
127    }
128
129    pub(crate) fn new_with_builtins(
130        db: LogicDb,
131        config: LogicConfig,
132        goal: Expr,
133        answer_limit: Option<usize>,
134        builtins: BuiltinTable,
135    ) -> Result<Self> {
136        let state = Arc::new(Mutex::new(SequenceEngineState::new(
137            db,
138            config,
139            goal,
140            answer_limit,
141            builtins,
142        )?));
143        let producer = sequence_producer(Arc::clone(&state));
144        Ok(Self {
145            sequence: LazySequence::new(producer),
146            state,
147        })
148    }
149
150    pub(crate) fn next_match(&self, cx: &mut Cx) -> Result<Option<ShapeMatch>> {
151        let Some(item) = self.sequence.next_item(cx)? else {
152            return Ok(None);
153        };
154        let _ = item.into_value(cx)?;
155        self.pop_emitted()
156    }
157
158    pub(crate) fn next_value(&self, cx: &mut Cx) -> Result<Option<Value>> {
159        let Some(item) = self.sequence.next_item(cx)? else {
160            return Ok(None);
161        };
162        let value = item.into_value(cx)?;
163        let _ = self.pop_emitted()?;
164        Ok(Some(value))
165    }
166
167    pub(crate) fn close(&self, cx: &mut Cx) -> Result<()> {
168        self.sequence.close(cx)
169    }
170
171    fn force(&mut self, cx: &mut Cx, limit: Option<usize>) -> Result<Vec<ShapeMatch>> {
172        let bound = limit.unwrap_or(usize::MAX);
173        let mut answers = Vec::new();
174        while answers.len() < bound {
175            let Some(answer) = self.next_match(cx)? else {
176                break;
177            };
178            answers.push(answer);
179        }
180        Ok(answers)
181    }
182
183    fn pop_emitted(&self) -> Result<Option<ShapeMatch>> {
184        self.state
185            .lock()
186            .map_err(|_| Error::PoisonedLock("logic sequence"))?
187            .emitted
188            .pop_front()
189            .map(Some)
190            .ok_or_else(|| logic_eval_error("logic sequence emitted no answer"))
191    }
192}
193
194impl Clone for SequenceEngine {
195    fn clone(&self) -> Self {
196        Self {
197            sequence: self.sequence.clone(),
198            state: Arc::clone(&self.state),
199        }
200    }
201}
202
203impl fmt::Debug for SequenceEngine {
204    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
205        formatter.write_str("SequenceEngine")
206    }
207}
208
209#[derive(Debug)]
210struct SequenceEngineState {
211    db: LogicDb,
212    config: LogicConfig,
213    frames: Vec<ResolveFrame>,
214    clause_scans: usize,
215    seen: BTreeMap<String, usize>,
216    builtins: BuiltinTable,
217    emitted: VecDeque<ShapeMatch>,
218    answers_emitted: usize,
219    answer_limit: Option<usize>,
220    next_choice_frame_id: u64,
221}
222
223impl SequenceEngineState {
224    fn new(
225        db: LogicDb,
226        config: LogicConfig,
227        goal: Expr,
228        answer_limit: Option<usize>,
229        builtins: BuiltinTable,
230    ) -> Result<Self> {
231        let goal = normalize_goal_expr(&goal);
232        if !is_goal_expr(&goal) {
233            return Err(logic_eval_error("query goal must be a call-shaped list"));
234        }
235        Ok(Self {
236            db,
237            config,
238            frames: vec![ResolveFrame {
239                goals: vec![goal],
240                env: LogicEnv::new(),
241                depth: 0,
242                choice_frame_id: 0,
243                choice_parent: 0,
244            }],
245            clause_scans: 0,
246            seen: BTreeMap::new(),
247            builtins,
248            emitted: VecDeque::new(),
249            answers_emitted: 0,
250            answer_limit,
251            next_choice_frame_id: 0,
252        })
253    }
254
255    fn step(&mut self, cx: &mut Cx) -> Result<Option<ShapeMatch>> {
256        if self
257            .answer_limit
258            .is_some_and(|limit| self.answers_emitted >= limit)
259        {
260            return Ok(None);
261        }
262        while let Some(frame) = pop_frame(&mut self.frames, self.config.strategy) {
263            if frame.depth > self.config.limits.max_depth {
264                return Err(logic_eval_error(format!(
265                    "logic query exceeded max_depth {}",
266                    self.config.limits.max_depth
267                )));
268            }
269            if frame.goals.len() > self.config.limits.max_goals {
270                return Err(logic_eval_error(format!(
271                    "logic query exceeded max_goals {}",
272                    self.config.limits.max_goals
273                )));
274            }
275            if frame.goals.is_empty() {
276                self.answers_emitted += 1;
277                return frame.env.as_shape_match(cx).map(Some);
278            }
279
280            let goal = frame.env.apply(&frame.goals[0]);
281            let rest = frame.goals[1..].to_vec();
282            if let Some(inner) = naf_inner_goal(&goal)? {
283                let grounded = frame.env.apply(inner);
284                if !frame.env.free_vars(&grounded).is_empty() {
285                    return Err(logic_eval_error(
286                        "negation-as-failure flounders on unbound variables",
287                    ));
288                }
289                let demand = NafDemand::new(grounded);
290                let _tag = demand.tag();
291                let has_answer = self.naf_has_answer(cx, demand.goal().clone())?;
292                if !has_answer {
293                    push_frame(
294                        &mut self.frames,
295                        ResolveFrame {
296                            goals: rest,
297                            env: frame.env,
298                            depth: frame.depth,
299                            choice_frame_id: frame.choice_frame_id,
300                            choice_parent: frame.choice_parent,
301                        },
302                        self.config.strategy,
303                    );
304                }
305                continue;
306            }
307            if is_cut_expr(&goal) {
308                raise_cut_prompt(
309                    cx,
310                    CutPrompt {
311                        cut_parent: frame.choice_parent,
312                    },
313                )?;
314                self.frames
315                    .retain(|pending| pending.choice_frame_id <= frame.choice_parent);
316                push_frame(
317                    &mut self.frames,
318                    ResolveFrame {
319                        goals: rest,
320                        env: frame.env,
321                        depth: frame.depth,
322                        choice_frame_id: frame.choice_frame_id,
323                        choice_parent: frame.choice_parent,
324                    },
325                    self.config.strategy,
326                );
327                continue;
328            }
329
330            let table_key = format!("{:?}", normalize_goal_expr(&goal).canonical_key());
331            let visits = self.seen.entry(table_key).or_default();
332            *visits += 1;
333            if self.config.enable_tabling && *visits > self.config.limits.max_depth {
334                return Err(logic_eval_error(format!(
335                    "logic query exceeded max_depth {}",
336                    self.config.limits.max_depth
337                )));
338            }
339
340            if let Some(symbol) = builtin_head(&goal)
341                && let Some(solve) = self
342                    .builtins
343                    .get(&symbol)
344                    .map(|binding| Arc::clone(&binding.solve))
345            {
346                let args = goal_args(&goal)?;
347                let ctx = BuiltinCtx {
348                    db: &self.db,
349                    config: &self.config,
350                    answer_limit: self.answer_limit,
351                };
352                let choice_parent = frame.choice_frame_id;
353                let frames = solve(cx, &ctx, args, &frame.env)?
354                    .into_iter()
355                    .map(|next_env| ResolveFrame {
356                        goals: rest.clone(),
357                        env: next_env,
358                        depth: frame.depth + 1,
359                        choice_frame_id: self.next_choice_id(),
360                        choice_parent,
361                    })
362                    .collect();
363                push_choice_frames(&mut self.frames, frames, self.config.strategy);
364                continue;
365            }
366
367            let candidates = self
368                .db
369                .clauses_for_goal(&goal, self.config.enable_indexing)?
370                .into_iter()
371                .cloned()
372                .collect::<Vec<_>>();
373            let choice_parent = frame.choice_frame_id;
374            let mut next_frames = Vec::new();
375            for clause in &candidates {
376                self.clause_scans += 1;
377                if self.clause_scans > self.config.limits.max_clause_scan {
378                    return Err(logic_eval_error(format!(
379                        "logic query exceeded max_clause_scan {}",
380                        self.config.limits.max_clause_scan
381                    )));
382                }
383                let clause = rename_clause_apart(clause, frame.depth + 1);
384                let mut next_env = frame.env.clone();
385                if !next_env.unify(&goal, &clause.head, occurs_check(&self.config))? {
386                    continue;
387                }
388                let mut goals = clause.body.clone();
389                goals.extend(rest.clone());
390                next_frames.push(ResolveFrame {
391                    goals,
392                    env: next_env,
393                    depth: frame.depth + 1,
394                    choice_frame_id: self.next_choice_id(),
395                    choice_parent,
396                });
397            }
398            push_choice_frames(&mut self.frames, next_frames, self.config.strategy);
399        }
400
401        Ok(None)
402    }
403
404    fn naf_has_answer(&self, cx: &mut Cx, goal: Expr) -> Result<bool> {
405        let mut child_config = self.config.clone();
406        child_config.limits.max_answers = Some(1);
407        let child = SequenceEngine::new(self.db.clone(), child_config, goal, Some(1))?;
408        child.next_match(cx).map(|answer| answer.is_some())
409    }
410
411    fn next_choice_id(&mut self) -> u64 {
412        self.next_choice_frame_id += 1;
413        self.next_choice_frame_id
414    }
415}
416
417fn sequence_producer(state: Arc<Mutex<SequenceEngineState>>) -> SequenceProducer {
418    Arc::new(move |cx, _index| {
419        let answer = {
420            state
421                .lock()
422                .map_err(|_| Error::PoisonedLock("logic sequence"))?
423                .step(cx)?
424        };
425        let Some(answer) = answer else {
426            return Ok(None);
427        };
428        let value = shape_match_value(cx, answer.clone())?;
429        state
430            .lock()
431            .map_err(|_| Error::PoisonedLock("logic sequence"))?
432            .emitted
433            .push_back(answer);
434        Ok(Some(value))
435    })
436}
437
438pub fn query_one(
439    cx: &mut Cx,
440    db: &LogicDb,
441    config: &LogicConfig,
442    goal: Expr,
443) -> Result<Option<ShapeMatch>> {
444    Ok(query_all(cx, db, config, goal, Some(1))?.into_iter().next())
445}
446
447pub fn query_bool(cx: &mut Cx, db: &LogicDb, config: &LogicConfig, goal: Expr) -> Result<bool> {
448    Ok(query_one(cx, db, config, goal)?.is_some())
449}
450
451#[derive(Clone, Debug)]
452struct ResolveFrame {
453    goals: Vec<Expr>,
454    env: LogicEnv,
455    depth: usize,
456    choice_frame_id: u64,
457    choice_parent: u64,
458}
459
460fn builtin_head(goal: &Expr) -> Option<Symbol> {
461    predicate_symbol(goal).ok()
462}
463
464fn goal_args(goal: &Expr) -> Result<&[Expr]> {
465    match goal {
466        Expr::List(items) => Ok(&items[1..]),
467        Expr::Call { args, .. } => Ok(args),
468        _ => Err(logic_eval_error("builtin goal must be call-shaped")),
469    }
470}
471
472fn pop_frame(frames: &mut Vec<ResolveFrame>, strategy: SearchStrategy) -> Option<ResolveFrame> {
473    match strategy {
474        SearchStrategy::Dfs => frames.pop(),
475        SearchStrategy::Bfs | SearchStrategy::Fair => {
476            (!frames.is_empty()).then(|| frames.remove(0))
477        }
478    }
479}
480
481fn push_frame(frames: &mut Vec<ResolveFrame>, frame: ResolveFrame, strategy: SearchStrategy) {
482    match strategy {
483        SearchStrategy::Dfs | SearchStrategy::Bfs | SearchStrategy::Fair => frames.push(frame),
484    }
485}
486
487fn push_choice_frames(
488    frames: &mut Vec<ResolveFrame>,
489    mut next_frames: Vec<ResolveFrame>,
490    strategy: SearchStrategy,
491) {
492    if matches!(strategy, SearchStrategy::Dfs) {
493        next_frames.reverse();
494    }
495    for frame in next_frames {
496        push_frame(frames, frame, strategy);
497    }
498}