1use crate::builtins::pred;
13use crate::cell::*;
14use crate::machine::{ContFn, Machine, NO_SITE};
15use crate::solve::call_goal;
16use crate::unify::unify;
17
18fn invoke_k(m: &mut Machine) -> i32 {
20 let k = m.k_fn;
21 let e = m.k_env;
22 unsafe { k(m as *mut Machine, e) }
23}
24
25fn det(m: &mut Machine, ok: bool) -> i32 {
26 if ok { invoke_k(m) } else { 0 }
27}
28
29pub fn try_builtin(m: &mut Machine, name: &str, args_idx: usize, arity: u32) -> Option<i32> {
32 let mp = m as *mut Machine;
33 let a: Vec<Word> = (0..arity as usize).map(|i| m.heap[args_idx + i]).collect();
36 let arg = |i: usize| -> Word { a[i] };
37 let r = match (name, arity) {
38 (",", 2) => conjunction(m, arg(0), arg(1)),
39 (";", 2) => {
40 let lhs = m.deref(arg(0));
42 if tag_of(lhs) == TAG_STR {
43 let idx = payload(lhs) as usize;
44 let (f, n) = unpack_functor(m.heap[idx]);
45 if n == 2 && m.atoms.resolve(f) == "->" {
46 let (c, t) = (m.heap[idx + 1], m.heap[idx + 2]);
47 return Some(if_then_else(m, c, t, Some(arg(1))));
48 }
49 }
50 disjunction(m, arg(0), arg(1))
51 }
52 ("->", 2) => if_then_else(m, arg(0), arg(1), None),
53 ("\\+", 1) => naf(m, arg(0)),
54 ("once", 1) => once(m, arg(0)),
55 ("catch", 3) => catch_impl(m, arg(0), arg(1), arg(2)),
56 ("throw", 1) => {
57 crate::errors::throw_term(m, arg(0));
58 0
59 }
60 ("findall", 3) => findall_impl(m, arg(0), arg(1), arg(2)),
61 ("call", n) if n >= 1 => metacall_extend(m, arg(0), &a[1..]),
62 ("between", 3) => between_impl(m, arg(0), arg(1), arg(2)),
63 ("=", 2) => {
64 let ok = unify(m, arg(0), arg(1));
65 det(m, ok)
66 }
67 ("\\=", 2) => {
68 let ok = pred::plg_rt_b_neq(mp, arg(0), arg(1)) != 0;
69 det(m, ok)
70 }
71 ("is", 2) => {
72 let ok = pred::plg_rt_b_is(mp, arg(0), arg(1), crate::machine::NO_SITE) != 0;
74 det(m, ok)
75 }
76 ("compare", 3) => {
77 let ok = pred::plg_rt_b_compare(mp, arg(0), arg(1), arg(2)) != 0;
78 det(m, ok)
79 }
80 (op, 2) if arith_op(op).is_some() => {
81 let ok = pred::plg_rt_b_arith_cmp(
82 mp,
83 arith_op(op).unwrap(),
84 arg(0),
85 arg(1),
86 crate::machine::NO_SITE,
87 ) != 0;
88 det(m, ok)
89 }
90 (op, 2) if order_op(op).is_some() => {
91 let ok = pred::plg_rt_b_term_cmp(mp, order_op(op).unwrap(), arg(0), arg(1)) != 0;
92 det(m, ok)
93 }
94 _ => {
95 let ok = det_builtin(mp, name, arity, &a)?;
96 det(m, ok)
97 }
98 };
99 Some(r)
100}
101
102fn det_builtin(mp: *mut Machine, name: &str, arity: u32, a: &[Word]) -> Option<bool> {
106 use crate::builtins::{atomops, miscops, sortops, termops, typecheck};
107 let r = match (name, arity) {
108 ("var", 1) => typecheck::plg_rt_b_var_1(mp, a[0]),
109 ("nonvar", 1) => typecheck::plg_rt_b_nonvar_1(mp, a[0]),
110 ("atom", 1) => typecheck::plg_rt_b_atom_1(mp, a[0]),
111 ("number", 1) => typecheck::plg_rt_b_number_1(mp, a[0]),
112 ("integer", 1) => typecheck::plg_rt_b_integer_1(mp, a[0]),
113 ("float", 1) => typecheck::plg_rt_b_float_1(mp, a[0]),
114 ("compound", 1) => typecheck::plg_rt_b_compound_1(mp, a[0]),
115 ("is_list", 1) => typecheck::plg_rt_b_is_list_1(mp, a[0]),
116 ("functor", 3) => termops::plg_rt_b_functor_3(mp, a[0], a[1], a[2], NO_SITE),
118 ("arg", 3) => termops::plg_rt_b_arg_3(mp, a[0], a[1], a[2], NO_SITE),
119 ("=..", 2) => termops::plg_rt_b_univ_2(mp, a[0], a[1], NO_SITE),
120 ("copy_term", 2) => termops::plg_rt_b_copy_term_2(mp, a[0], a[1]),
121 ("atom_length", 2) => atomops::plg_rt_b_atom_length_2(mp, a[0], a[1], NO_SITE),
122 ("atom_concat", 3) => atomops::plg_rt_b_atom_concat_3(mp, a[0], a[1], a[2], NO_SITE),
123 ("atom_chars", 2) => atomops::plg_rt_b_atom_chars_2(mp, a[0], a[1], NO_SITE),
124 ("number_chars", 2) => atomops::plg_rt_b_number_chars_2(mp, a[0], a[1], NO_SITE),
125 ("number_codes", 2) => atomops::plg_rt_b_number_codes_2(mp, a[0], a[1], NO_SITE),
126 ("msort", 2) => sortops::plg_rt_b_msort_2(mp, a[0], a[1], NO_SITE),
127 ("sort", 2) => sortops::plg_rt_b_sort_2(mp, a[0], a[1], NO_SITE),
128 ("succ", 2) => miscops::plg_rt_b_succ_2(mp, a[0], a[1], NO_SITE),
129 ("plus", 3) => miscops::plg_rt_b_plus_3(mp, a[0], a[1], a[2], NO_SITE),
130 ("unify_with_occurs_check", 2) => {
131 miscops::plg_rt_b_unify_with_occurs_check_2(mp, a[0], a[1])
132 }
133 ("write", 1) => miscops::plg_rt_b_write_1(mp, a[0]),
134 ("writeln", 1) => miscops::plg_rt_b_writeln_1(mp, a[0]),
135 _ => return None,
136 };
137 Some(r != 0)
138}
139
140pub fn try_atom_builtin(m: &mut Machine, name: &str) -> Option<i32> {
142 match name {
143 "true" => Some(invoke_k(m)),
144 "nl" => {
145 let ok = crate::builtins::miscops::plg_rt_b_nl_0(m as *mut Machine) != 0;
146 Some(det(m, ok))
147 }
148 "fail" | "false" => Some(0),
149 "!" => {
150 let h = m.qbarrier;
154 m.cut_to(h);
155 Some(invoke_k(m))
156 }
157 _ => None,
158 }
159}
160
161fn arith_op(name: &str) -> Option<i32> {
163 Some(match name {
164 "<" => 0,
165 ">" => 1,
166 "=<" => 2,
167 ">=" => 3,
168 "=:=" => 4,
169 "=\\=" => 5,
170 _ => return None,
171 })
172}
173
174fn order_op(name: &str) -> Option<i32> {
175 Some(match name {
176 "==" => 0,
177 "\\==" => 1,
178 "@<" => 2,
179 "@>" => 3,
180 "@=<" => 4,
181 "@>=" => 5,
182 _ => return None,
183 })
184}
185
186fn save_k(m: &mut Machine, frame: usize, at: usize) {
189 m.heap[frame + at] = m.k_fn as usize as u64;
190 m.heap[frame + at + 1] = m.k_env;
191 m.heap[frame + at + 2] = m.qbarrier as u64;
192}
193
194fn load_k(m: &mut Machine, frame: usize, at: usize) -> (ContFn, u64) {
196 let k: ContFn = unsafe { std::mem::transmute(m.heap[frame + at] as usize) };
197 m.qbarrier = m.heap[frame + at + 2] as usize;
198 (k, m.heap[frame + at + 1])
199}
200
201fn conjunction(m: &mut Machine, a: Word, b: Word) -> i32 {
203 let frame = m.frame_alloc(4);
204 m.heap[frame] = b;
205 save_k(m, frame, 1);
206 m.k_fn = conj_k;
207 m.k_env = frame as u64;
208 call_goal(m, a)
209}
210
211unsafe extern "C" fn conj_k(m: *mut Machine, env: u64) -> i32 {
212 let m = unsafe { &mut *m };
213 let frame = env as usize;
214 let b = m.heap[frame];
215 let (kf, ke) = load_k(m, frame, 1);
216 m.k_fn = kf;
217 m.k_env = ke;
218 call_goal(m, b)
219}
220
221fn disjunction(m: &mut Machine, a: Word, b: Word) -> i32 {
223 let frame = m.frame_alloc(4);
224 m.heap[frame] = b;
225 save_k(m, frame, 1);
226 m.push_cp(disj_retry, frame as u64);
227 call_goal(m, a)
228}
229
230unsafe extern "C" fn disj_retry(m: *mut Machine, env: u64) -> i32 {
231 let m = unsafe { &mut *m };
232 let frame = env as usize;
233 let b = m.heap[frame];
234 let (kf, ke) = load_k(m, frame, 1);
235 m.k_fn = kf;
236 m.k_env = ke;
237 call_goal(m, b)
238}
239
240fn if_then_else(m: &mut Machine, c: Word, t: Word, e: Option<Word>) -> i32 {
242 let h = m.cps.len() as u64; if let Some(e) = e {
244 let ef = m.frame_alloc(4);
245 m.heap[ef] = e;
246 save_k(m, ef, 1);
247 m.push_cp(disj_retry, ef as u64);
248 }
249 let tf = m.frame_alloc(5);
250 m.heap[tf] = t;
251 save_k(m, tf, 1);
252 m.heap[tf + 4] = h;
253 m.k_fn = ite_then;
254 m.k_env = tf as u64;
255 m.qbarrier = m.cps.len();
258 call_goal(m, c)
259}
260
261unsafe extern "C" fn ite_then(m: *mut Machine, env: u64) -> i32 {
262 let m = unsafe { &mut *m };
263 let frame = env as usize;
264 let h = m.heap[frame + 4] as usize;
265 m.cps.truncate(h); let t = m.heap[frame];
267 let (kf, ke) = load_k(m, frame, 1); m.k_fn = kf;
269 m.k_env = ke;
270 call_goal(m, t)
271}
272
273fn once(m: &mut Machine, g: Word) -> i32 {
275 let h = m.cps.len() as u64;
276 let frame = m.frame_alloc(4);
277 save_k(m, frame, 0);
278 m.heap[frame + 3] = h;
279 m.k_fn = once_then;
280 m.k_env = frame as u64;
281 m.qbarrier = m.cps.len(); call_goal(m, g)
283}
284
285unsafe extern "C" fn once_then(m: *mut Machine, env: u64) -> i32 {
286 let m = unsafe { &mut *m };
287 let frame = env as usize;
288 let h = m.heap[frame + 3] as usize;
289 m.cps.truncate(h);
290 let (kf, ke) = load_k(m, frame, 0);
291 m.k_fn = kf;
292 m.k_env = ke;
293 invoke_k(m)
294}
295
296fn naf(m: &mut Machine, g: Word) -> i32 {
299 let h = m.cps.len() as u64;
300 let cf = m.frame_alloc(3);
301 save_k(m, cf, 0);
302 m.push_cp(naf_continue, cf as u64);
303 let ff = m.frame_alloc(1);
304 m.heap[ff] = h;
305 m.k_fn = naf_found;
306 m.k_env = ff as u64;
307 m.qbarrier = m.cps.len();
309 call_goal(m, g)
310}
311
312unsafe extern "C" fn naf_continue(m: *mut Machine, env: u64) -> i32 {
313 let m = unsafe { &mut *m };
314 let frame = env as usize;
315 let (kf, ke) = load_k(m, frame, 0);
316 m.k_fn = kf;
317 m.k_env = ke;
318 invoke_k(m)
319}
320
321unsafe extern "C" fn naf_found(m: *mut Machine, env: u64) -> i32 {
322 let m = unsafe { &mut *m };
323 let h = m.heap[env as usize] as usize;
324 m.cps.truncate(h); 0
326}
327
328fn catch_impl(m: &mut Machine, goal: Word, catcher: Word, recovery: Word) -> i32 {
332 let frame = m.frame_alloc(5);
333 m.heap[frame] = catcher;
334 m.heap[frame + 1] = recovery;
335 save_k(m, frame, 2);
336 m.push_catch_cp(catch_retry, frame as u64);
337 call_goal(m, goal)
338}
339
340unsafe extern "C" fn catch_retry(_m: *mut Machine, _env: u64) -> i32 {
343 0
344}
345
346fn findall_impl(m: &mut Machine, template: Word, goal: Word, bag: Word) -> i32 {
351 let floor = m.cps.len();
352 let tmark = m.trail.len();
353 let hmark = m.heap.len();
354 let saved_k = (m.k_fn, m.k_env, m.qbarrier);
355 m.findall_stack.push(Vec::new());
356 let cf = m.frame_alloc(1);
357 m.heap[cf] = template;
358 m.k_fn = findall_collect;
359 m.k_env = cf as u64;
360 m.qbarrier = m.cps.len(); let r = call_goal(m, goal);
362 crate::solve::drive(m, floor, r); let results = m.findall_stack.pop().unwrap();
364 m.k_fn = saved_k.0;
365 m.k_env = saved_k.1;
366 m.qbarrier = saved_k.2;
367 if m.error.is_some() {
368 return 0;
369 }
370 m.rewind_to(tmark, hmark);
372 let mut w = make_atom(plg_shared::atom::ATOM_NIL);
373 for buf in results.iter().rev() {
374 let e = crate::copyterm::restore_from_buf(m, buf);
375 let idx = m.heap.len();
376 m.heap.push(e);
377 m.heap.push(w);
378 w = make(TAG_LST, idx as u64);
379 }
380 let ok = unify(m, bag, w);
381 det(m, ok)
382}
383
384unsafe extern "C" fn findall_collect(m: *mut Machine, env: u64) -> i32 {
385 let m = unsafe { &mut *m };
386 let template = m.heap[env as usize];
387 let buf = crate::copyterm::copy_to_buf(m, template);
388 m.findall_stack
389 .last_mut()
390 .expect("collector outside findall")
391 .push(buf);
392 0 }
394
395fn metacall_extend(m: &mut Machine, goal: Word, extras: &[Word]) -> i32 {
398 let goal = m.deref(goal);
399 m.qbarrier = m.cps.len();
401 if extras.is_empty() {
402 return call_goal(m, goal);
403 }
404 let extended = match tag_of(goal) {
405 TAG_ATOM => {
406 let f = atom_id(goal);
407 let idx = m.heap.len();
408 m.heap.push(pack_functor(f, extras.len() as u32));
409 m.heap.extend_from_slice(extras);
410 make(TAG_STR, idx as u64)
411 }
412 TAG_STR => {
413 let sidx = payload(goal) as usize;
414 let (f, n) = unpack_functor(m.heap[sidx]);
415 let idx = m.heap.len();
416 m.heap.push(pack_functor(f, n + extras.len() as u32));
417 for i in 0..n as usize {
418 let w = m.heap[sidx + 1 + i];
419 m.heap.push(w);
420 }
421 m.heap.extend_from_slice(extras);
422 make(TAG_STR, idx as u64)
423 }
424 TAG_REF => {
425 crate::errors::instantiation(m, "call/N requires a bound goal");
426 return 0;
427 }
428 _ => {
429 crate::errors::type_error(m, "callable", goal, "Goal is not callable");
430 return 0;
431 }
432 };
433 call_goal(m, extended)
434}
435
436fn between_impl(m: &mut Machine, lo_w: Word, hi_w: Word, x_w: Word) -> i32 {
439 let Some(lo) = int_arg(m, lo_w, "between/3") else {
440 return 0;
441 };
442 let Some(hi) = int_arg(m, hi_w, "between/3") else {
443 return 0;
444 };
445 let x = m.deref(x_w);
446 match tag_of(x) {
447 TAG_INT | TAG_BIG => {
448 let xv = if tag_of(x) == TAG_INT {
449 int_value(x)
450 } else {
451 m.heap[payload(x) as usize] as i64
452 };
453 det(m, lo <= xv && xv <= hi)
454 }
455 TAG_REF => {
456 if lo > hi {
457 return 0;
458 }
459 let frame = m.frame_alloc(6);
463 m.heap[frame] = x;
464 m.heap[frame + 1] = lo as u64;
465 m.heap[frame + 2] = hi as u64;
466 save_k(m, frame, 3);
467 if lo < hi {
468 m.push_cp(between_retry, frame as u64);
469 }
470 let w = int_to_word(m, lo);
471 let ok = unify(m, x, w);
472 debug_assert!(ok, "binding a fresh var cannot fail");
473 invoke_k(m)
474 }
475 _ => {
476 crate::errors::type_error(m, "integer", x, "between/3 requires an integer");
477 0
478 }
479 }
480}
481
482unsafe extern "C" fn between_retry(m: *mut Machine, env: u64) -> i32 {
483 let m = unsafe { &mut *m };
484 let frame = env as usize;
485 let x = m.heap[frame];
486 let cur = m.heap[frame + 1] as i64 + 1;
487 let hi = m.heap[frame + 2] as i64;
488 m.heap[frame + 1] = cur as u64;
489 if cur < hi {
490 m.push_cp(between_retry, frame as u64);
491 }
492 let (kf, ke) = load_k(m, frame, 3);
493 m.k_fn = kf;
494 m.k_env = ke;
495 let w = int_to_word(m, cur);
496 let ok = unify(m, x, w);
497 debug_assert!(ok, "x rewound to unbound before retry");
498 invoke_k(m)
499}
500
501fn int_to_word(m: &mut Machine, n: i64) -> Word {
503 if (INT_MIN..=INT_MAX).contains(&n) {
504 make_int(n)
505 } else {
506 let idx = m.heap.len();
507 m.heap.push(n as u64);
508 make(TAG_BIG, idx as u64)
509 }
510}
511
512fn int_arg(m: &mut Machine, w: Word, who: &str) -> Option<i64> {
515 let w = m.deref(w);
516 match tag_of(w) {
517 TAG_INT => Some(int_value(w)),
518 TAG_BIG => Some(m.heap[payload(w) as usize] as i64),
519 TAG_REF => {
520 let ctx = format!("{who} requires bound integer bounds");
521 crate::errors::instantiation(m, &ctx);
522 None
523 }
524 _ => {
525 let ctx = format!("{who} requires an integer");
526 crate::errors::type_error(m, "integer", w, &ctx);
527 None
528 }
529 }
530}
531
532#[unsafe(no_mangle)]
537pub unsafe extern "C" fn plg_rt_pred_between_3(m: *mut Machine, _env: u64) -> i32 {
538 let m = unsafe { &mut *m };
539 let (lo, hi, x) = (m.areg[0], m.areg[1], m.areg[2]);
540 between_impl(m, lo, hi, x)
541}
542
543#[unsafe(no_mangle)]
547pub unsafe extern "C" fn plg_rt_metacall(m: *mut Machine, goal: u64) -> i32 {
548 let m = unsafe { &mut *m };
549 m.qbarrier = m.cps.len();
552 call_goal(m, goal)
553}
554
555#[unsafe(no_mangle)]
558pub unsafe extern "C" fn plg_rt_b_catch_3(m: *mut Machine, g: u64, c: u64, r: u64) -> i32 {
559 catch_impl(unsafe { &mut *m }, g, c, r)
560}
561
562#[unsafe(no_mangle)]
565pub unsafe extern "C" fn plg_rt_b_throw_1(m: *mut Machine, ball: u64) -> i32 {
566 crate::errors::throw_term(unsafe { &mut *m }, ball);
567 0
568}
569
570#[unsafe(no_mangle)]
573pub unsafe extern "C" fn plg_rt_b_findall_3(m: *mut Machine, t: u64, g: u64, b: u64) -> i32 {
574 findall_impl(unsafe { &mut *m }, t, g, b)
575}
576
577#[cfg(test)]
578mod tests {
579 use crate::machine::Machine;
580 use crate::query::parse_query;
581 use crate::solve::{Outcome, solve};
582 use plg_shared::StringInterner;
583
584 fn run(query: &str) -> (Vec<String>, Option<String>) {
585 let mut m = Machine::new(StringInterner::new(), Vec::new());
586 let goal = parse_query(&mut m, query).unwrap();
587 let outcome = solve(&mut m, goal);
588 let err = match outcome {
589 Outcome::Error => Some(m.error.take().unwrap().message),
590 Outcome::Done => None,
591 };
592 let sols = m
593 .solutions
594 .iter()
595 .map(|s| {
596 s.bindings
597 .iter()
598 .map(|(n, _, t)| format!("{n}={t}"))
599 .collect::<Vec<_>>()
600 .join(",")
601 })
602 .collect();
603 (sols, err)
604 }
605
606 #[test]
607 fn top_level_is_and_comparison() {
608 assert_eq!(run("X is 2 + 3 * 4").0, vec!["X=14"]);
609 assert_eq!(run("1 < 2").0, vec![""]);
610 assert_eq!(run("2 < 1").0, Vec::<String>::new());
611 }
612
613 #[test]
614 fn top_level_disjunction_enumerates() {
615 assert_eq!(run("(X = 1 ; X = 2)").0, vec!["X=1", "X=2"]);
616 }
617
618 #[test]
619 fn top_level_ite_and_naf() {
620 assert_eq!(run("(1 < 2 -> X = yes ; X = no)").0, vec!["X=yes"]);
621 assert_eq!(run("(2 < 1 -> X = yes ; X = no)").0, vec!["X=no"]);
622 assert_eq!(run("\\+ 2 < 1").0, vec![""]);
623 assert_eq!(run("\\+ 1 < 2").0, Vec::<String>::new());
624 assert_eq!(run("\\+ (X = 1, 2 < 1), X = ok").0, vec!["X=ok"]);
626 }
627
628 #[test]
629 fn top_level_once_commits() {
630 assert_eq!(run("once((X = 1 ; X = 2))").0, vec!["X=1"]);
631 }
632
633 #[test]
634 fn errors_propagate() {
635 let (_, err) = run("X is 1 // 0");
636 assert!(err.unwrap().contains("zero_divisor"));
637 }
638}
639
640#[cfg(test)]
641mod m4_tests {
642 use crate::machine::Machine;
643 use crate::query::parse_query;
644 use crate::solve::{Outcome, solve};
645 use plg_shared::StringInterner;
646
647 fn run(query: &str) -> (Vec<String>, Option<String>) {
648 let mut m = Machine::new(StringInterner::new(), Vec::new());
649 let goal = parse_query(&mut m, query).unwrap();
650 let outcome = solve(&mut m, goal);
651 let err = match outcome {
652 Outcome::Error => Some(m.error.take().unwrap().message),
653 Outcome::Done => None,
654 };
655 let sols = m
656 .solutions
657 .iter()
658 .map(|s| {
659 s.bindings
660 .iter()
661 .map(|(n, _, t)| format!("{n}={t}"))
662 .collect::<Vec<_>>()
663 .join(",")
664 })
665 .collect();
666 (sols, err)
667 }
668
669 #[test]
670 fn throw_uncaught_propagates_with_rendered_ball() {
671 let (sols, err) = run("throw(my_ball)");
672 assert!(sols.is_empty());
673 assert_eq!(err.unwrap(), "my_ball");
674 }
675
676 #[test]
677 fn catch_catches_matching_ball_and_runs_recovery() {
678 let (sols, err) = run("catch(throw(oops(1)), oops(N), X = caught(N))");
679 assert!(err.is_none());
680 assert_eq!(sols, vec!["N=1,X=caught(1)"]);
681 }
682
683 #[test]
684 fn catch_passes_nonmatching_ball_outward() {
685 let (sols, err) = run("catch(throw(other), oops(_), X = no)");
686 assert!(sols.is_empty());
687 assert_eq!(err.unwrap(), "other");
688 }
689
690 #[test]
691 fn nested_catch_inner_first() {
692 let (sols, err) = run("catch(catch(throw(b), a, X = inner_a), b, X = outer_b)");
693 assert!(err.is_none());
694 assert_eq!(sols, vec!["X=outer_b"]);
695 }
696
697 #[test]
698 fn catch_traps_builtin_errors() {
699 let (sols, err) = run("catch(X is 1 // 0, error(evaluation_error(E), _), Y = E)");
700 assert!(err.is_none());
701 assert_eq!(sols, vec!["E=zero_divisor,X=_0,Y=zero_divisor"]);
702 }
703
704 #[test]
705 fn catch_is_transparent_to_normal_backtracking() {
706 let (sols, err) = run("catch((X = 1 ; X = 2), _, fail)");
707 assert!(err.is_none());
708 assert_eq!(sols, vec!["X=1", "X=2"]);
709 }
710
711 #[test]
712 fn step_limit_is_not_catchable() {
713 let mut m = Machine::new(StringInterner::new(), Vec::new());
714 m.step_limit = 1;
715 m.steps = 1; assert!(!m.step());
717 let goal = parse_query(&mut m, "catch(true, _, true)").unwrap();
718 assert!(matches!(solve(&mut m, goal), Outcome::Error));
720 assert!(m.error.as_ref().unwrap().uncatchable);
721 }
722
723 #[test]
724 fn findall_collects_and_rewinds() {
725 let (sols, err) = run("findall(X, (X = 1 ; X = 2 ; X = 3), L)");
726 assert!(err.is_none());
727 assert_eq!(sols, vec!["L=[1, 2, 3],X=_0"]);
728 }
729
730 #[test]
731 fn findall_empty_on_failure() {
732 let (sols, err) = run("findall(X, fail, L)");
733 assert!(err.is_none());
734 assert_eq!(sols, vec!["L=[],X=_0"]);
735 }
736
737 #[test]
738 fn findall_propagates_errors() {
739 let (sols, err) = run("findall(X, throw(bad), L)");
740 assert!(sols.is_empty());
741 assert_eq!(err.unwrap(), "bad");
742 }
743
744 #[test]
745 fn nested_findall() {
746 let (sols, err) = run("findall(L1, (Y = 2, findall(X, (X = 1 ; X = Y), L1)), L)");
747 assert!(err.is_none());
748 assert_eq!(sols.len(), 1);
749 assert!(sols[0].contains("L=[[1, 2]]"), "{sols:?}");
750 }
751
752 #[test]
753 fn call_n_extends_goals() {
754 let (sols, err) = run("call(=, X, 7)");
755 assert!(err.is_none());
756 assert_eq!(sols, vec!["X=7"]);
757 let (sols, _) = run("G = (X = 1 ; X = 2), call(G)");
758 assert_eq!(sols.len(), 2);
759 }
760
761 #[test]
762 fn call_unbound_is_instantiation_error() {
763 let (sols, err) = run("call(X)");
764 assert!(sols.is_empty());
765 assert!(err.unwrap().contains("instantiation_error"));
766 }
767
768 #[test]
769 fn between_enumerates_and_checks() {
770 let (sols, err) = run("between(1, 4, X)");
771 assert!(err.is_none());
772 assert_eq!(sols, vec!["X=1", "X=2", "X=3", "X=4"]);
773 let (sols, _) = run("between(1, 4, 3)");
774 assert_eq!(sols.len(), 1);
775 let (sols, _) = run("between(1, 4, 9)");
776 assert!(sols.is_empty());
777 let (sols, _) = run("between(3, 1, X)");
778 assert!(sols.is_empty());
779 let (sols, _) = run("between(2, 2, X)");
781 assert_eq!(sols, vec!["X=2"]);
782 }
783
784 #[test]
785 fn findall_with_between() {
786 let (sols, err) = run("findall(X, between(1, 5, X), L)");
787 assert!(err.is_none());
788 assert_eq!(sols, vec!["L=[1, 2, 3, 4, 5],X=_0"]);
789 }
790}
791
792#[cfg(test)]
793mod vocab_invariant {
794 use plg_shared::{BUILTINS, builtins::BuiltinKind};
802 use std::collections::BTreeSet;
803
804 #[rustfmt::skip]
807 const DET_DISPATCH: &[(&str, u32)] = &[
808 ("var", 1), ("nonvar", 1), ("atom", 1), ("number", 1), ("integer", 1),
809 ("float", 1), ("compound", 1), ("is_list", 1), ("functor", 3), ("arg", 3),
810 ("=..", 2), ("copy_term", 2), ("atom_length", 2), ("atom_concat", 3),
811 ("atom_chars", 2), ("number_chars", 2), ("number_codes", 2), ("msort", 2),
812 ("sort", 2), ("succ", 2), ("plus", 3), ("unify_with_occurs_check", 2),
813 ("write", 1), ("writeln", 1),
814 ];
815
816 #[test]
817 fn det_dispatch_equals_shared_det_subset() {
818 let dispatch: BTreeSet<(&str, u32)> = DET_DISPATCH.iter().copied().collect();
819 let shared_det: BTreeSet<(&str, u32)> = BUILTINS
820 .iter()
821 .filter(|s| s.kind == BuiltinKind::Det && s.arity > 0)
822 .map(|s| (s.name, s.arity))
823 .collect();
824 assert_eq!(
825 dispatch, shared_det,
826 "runtime det dispatch diverges from BUILTINS Det subset \
827 (left = control.rs, right = shared table)"
828 );
829 }
830}