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#[derive(Clone, Debug)]
27pub struct LogicQuery {
28 pub goal: Expr,
30 pub config: LogicConfig,
32}
33
34pub 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
73pub 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
90pub 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}