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 ("writeq", 1) => miscops::plg_rt_b_writeq_1(mp, a[0]),
135 ("writeln", 1) => miscops::plg_rt_b_writeln_1(mp, a[0]),
136 _ => return None,
137 };
138 Some(r != 0)
139}
140
141pub fn try_atom_builtin(m: &mut Machine, name: &str) -> Option<i32> {
143 match name {
144 "true" => Some(invoke_k(m)),
145 "nl" => {
146 let ok = crate::builtins::miscops::plg_rt_b_nl_0(m as *mut Machine) != 0;
147 Some(det(m, ok))
148 }
149 "fail" | "false" => Some(0),
150 "!" => {
151 let h = m.qbarrier;
155 m.cut_to(h);
156 Some(invoke_k(m))
157 }
158 _ => None,
159 }
160}
161
162fn arith_op(name: &str) -> Option<i32> {
164 Some(match name {
165 "<" => 0,
166 ">" => 1,
167 "=<" => 2,
168 ">=" => 3,
169 "=:=" => 4,
170 "=\\=" => 5,
171 _ => return None,
172 })
173}
174
175fn order_op(name: &str) -> Option<i32> {
176 Some(match name {
177 "==" => 0,
178 "\\==" => 1,
179 "@<" => 2,
180 "@>" => 3,
181 "@=<" => 4,
182 "@>=" => 5,
183 _ => return None,
184 })
185}
186
187fn save_k(m: &mut Machine, frame: usize, at: usize) {
190 m.heap[frame + at] = m.k_fn as usize as u64;
191 m.heap[frame + at + 1] = m.k_env;
192 m.heap[frame + at + 2] = m.qbarrier as u64;
193}
194
195fn load_k(m: &mut Machine, frame: usize, at: usize) -> (ContFn, u64) {
197 let k: ContFn = unsafe { std::mem::transmute(m.heap[frame + at] as usize) };
198 m.qbarrier = m.heap[frame + at + 2] as usize;
199 (k, m.heap[frame + at + 1])
200}
201
202fn conjunction(m: &mut Machine, a: Word, b: Word) -> i32 {
204 let frame = m.frame_alloc(4);
205 m.heap[frame] = b;
206 save_k(m, frame, 1);
207 m.k_fn = conj_k;
208 m.k_env = frame as u64;
209 call_goal(m, a)
210}
211
212unsafe extern "C" fn conj_k(m: *mut Machine, env: u64) -> i32 {
213 let m = unsafe { &mut *m };
214 let frame = env as usize;
215 let b = m.heap[frame];
216 let (kf, ke) = load_k(m, frame, 1);
217 m.k_fn = kf;
218 m.k_env = ke;
219 call_goal(m, b)
220}
221
222fn disjunction(m: &mut Machine, a: Word, b: Word) -> i32 {
224 let frame = m.frame_alloc(4);
225 m.heap[frame] = b;
226 save_k(m, frame, 1);
227 m.push_cp(disj_retry, frame as u64);
228 call_goal(m, a)
229}
230
231unsafe extern "C" fn disj_retry(m: *mut Machine, env: u64) -> i32 {
232 let m = unsafe { &mut *m };
233 let frame = env as usize;
234 let b = m.heap[frame];
235 let (kf, ke) = load_k(m, frame, 1);
236 m.k_fn = kf;
237 m.k_env = ke;
238 call_goal(m, b)
239}
240
241fn if_then_else(m: &mut Machine, c: Word, t: Word, e: Option<Word>) -> i32 {
243 let h = m.cps.len() as u64; if let Some(e) = e {
245 let ef = m.frame_alloc(4);
246 m.heap[ef] = e;
247 save_k(m, ef, 1);
248 m.push_cp(disj_retry, ef as u64);
249 }
250 let tf = m.frame_alloc(5);
251 m.heap[tf] = t;
252 save_k(m, tf, 1);
253 m.heap[tf + 4] = h;
254 m.k_fn = ite_then;
255 m.k_env = tf as u64;
256 m.qbarrier = m.cps.len();
259 call_goal(m, c)
260}
261
262unsafe extern "C" fn ite_then(m: *mut Machine, env: u64) -> i32 {
263 let m = unsafe { &mut *m };
264 let frame = env as usize;
265 let h = m.heap[frame + 4] as usize;
266 m.cps.truncate(h); let t = m.heap[frame];
268 let (kf, ke) = load_k(m, frame, 1); m.k_fn = kf;
270 m.k_env = ke;
271 call_goal(m, t)
272}
273
274fn once(m: &mut Machine, g: Word) -> i32 {
276 let h = m.cps.len() as u64;
277 let frame = m.frame_alloc(4);
278 save_k(m, frame, 0);
279 m.heap[frame + 3] = h;
280 m.k_fn = once_then;
281 m.k_env = frame as u64;
282 m.qbarrier = m.cps.len(); call_goal(m, g)
284}
285
286unsafe extern "C" fn once_then(m: *mut Machine, env: u64) -> i32 {
287 let m = unsafe { &mut *m };
288 let frame = env as usize;
289 let h = m.heap[frame + 3] as usize;
290 m.cps.truncate(h);
291 let (kf, ke) = load_k(m, frame, 0);
292 m.k_fn = kf;
293 m.k_env = ke;
294 invoke_k(m)
295}
296
297fn naf(m: &mut Machine, g: Word) -> i32 {
300 let h = m.cps.len() as u64;
301 let cf = m.frame_alloc(3);
302 save_k(m, cf, 0);
303 m.push_cp(naf_continue, cf as u64);
304 let ff = m.frame_alloc(1);
305 m.heap[ff] = h;
306 m.k_fn = naf_found;
307 m.k_env = ff as u64;
308 m.qbarrier = m.cps.len();
310 call_goal(m, g)
311}
312
313unsafe extern "C" fn naf_continue(m: *mut Machine, env: u64) -> i32 {
314 let m = unsafe { &mut *m };
315 let frame = env as usize;
316 let (kf, ke) = load_k(m, frame, 0);
317 m.k_fn = kf;
318 m.k_env = ke;
319 invoke_k(m)
320}
321
322unsafe extern "C" fn naf_found(m: *mut Machine, env: u64) -> i32 {
323 let m = unsafe { &mut *m };
324 let h = m.heap[env as usize] as usize;
325 m.cps.truncate(h); 0
327}
328
329fn catch_impl(m: &mut Machine, goal: Word, catcher: Word, recovery: Word) -> i32 {
333 let frame = m.frame_alloc(5);
334 m.heap[frame] = catcher;
335 m.heap[frame + 1] = recovery;
336 save_k(m, frame, 2);
337 m.push_catch_cp(catch_retry, frame as u64);
338 call_goal(m, goal)
339}
340
341unsafe extern "C" fn catch_retry(_m: *mut Machine, _env: u64) -> i32 {
344 0
345}
346
347fn findall_impl(m: &mut Machine, template: Word, goal: Word, bag: Word) -> i32 {
352 let floor = m.cps.len();
353 let tmark = m.trail.len();
354 let hmark = m.heap.len();
355 let saved_k = (m.k_fn, m.k_env, m.qbarrier);
356 m.findall_stack.push(Vec::new());
357 let cf = m.frame_alloc(1);
358 m.heap[cf] = template;
359 m.k_fn = findall_collect;
360 m.k_env = cf as u64;
361 m.qbarrier = m.cps.len(); let r = call_goal(m, goal);
363 crate::solve::drive(m, floor, r); let results = m.findall_stack.pop().unwrap();
365 m.k_fn = saved_k.0;
366 m.k_env = saved_k.1;
367 m.qbarrier = saved_k.2;
368 if m.error.is_some() {
369 return 0;
370 }
371 m.rewind_to(tmark, hmark);
373 let mut w = make_atom(plg_shared::atom::ATOM_NIL);
374 for buf in results.iter().rev() {
375 let e = crate::copyterm::restore_from_buf(m, buf);
376 let idx = m.heap.len();
377 m.heap.push(e);
378 m.heap.push(w);
379 w = make(TAG_LST, idx as u64);
380 }
381 let ok = unify(m, bag, w);
382 det(m, ok)
383}
384
385unsafe extern "C" fn findall_collect(m: *mut Machine, env: u64) -> i32 {
386 let m = unsafe { &mut *m };
387 let template = m.heap[env as usize];
388 let buf = crate::copyterm::copy_to_buf(m, template);
389 m.findall_stack
390 .last_mut()
391 .expect("collector outside findall")
392 .push(buf);
393 0 }
395
396fn metacall_extend(m: &mut Machine, goal: Word, extras: &[Word]) -> i32 {
399 let goal = m.deref(goal);
400 m.qbarrier = m.cps.len();
402 if extras.is_empty() {
403 return call_goal(m, goal);
404 }
405 let extended = match tag_of(goal) {
406 TAG_ATOM => {
407 let f = atom_id(goal);
408 let idx = m.heap.len();
409 m.heap.push(pack_functor(f, extras.len() as u32));
410 m.heap.extend_from_slice(extras);
411 make(TAG_STR, idx as u64)
412 }
413 TAG_STR => {
414 let sidx = payload(goal) as usize;
415 let (f, n) = unpack_functor(m.heap[sidx]);
416 let idx = m.heap.len();
417 m.heap.push(pack_functor(f, n + extras.len() as u32));
418 for i in 0..n as usize {
419 let w = m.heap[sidx + 1 + i];
420 m.heap.push(w);
421 }
422 m.heap.extend_from_slice(extras);
423 make(TAG_STR, idx as u64)
424 }
425 TAG_REF => {
426 crate::errors::instantiation(m, "call/N requires a bound goal");
427 return 0;
428 }
429 _ => {
430 crate::errors::type_error(m, "callable", goal, "Goal is not callable");
431 return 0;
432 }
433 };
434 call_goal(m, extended)
435}
436
437fn between_impl(m: &mut Machine, lo_w: Word, hi_w: Word, x_w: Word) -> i32 {
440 let Some(lo) = int_arg(m, lo_w, "between/3") else {
441 return 0;
442 };
443 let Some(hi) = int_arg(m, hi_w, "between/3") else {
444 return 0;
445 };
446 let x = m.deref(x_w);
447 match tag_of(x) {
448 TAG_INT | TAG_BIG => {
449 let xv = if tag_of(x) == TAG_INT {
450 int_value(x)
451 } else {
452 m.heap[payload(x) as usize] as i64
453 };
454 det(m, lo <= xv && xv <= hi)
455 }
456 TAG_REF => {
457 if lo > hi {
458 return 0;
459 }
460 let frame = m.frame_alloc(6);
464 m.heap[frame] = x;
465 m.heap[frame + 1] = lo as u64;
466 m.heap[frame + 2] = hi as u64;
467 save_k(m, frame, 3);
468 if lo < hi {
469 m.push_cp(between_retry, frame as u64);
470 }
471 let w = int_to_word(m, lo);
472 let ok = unify(m, x, w);
473 debug_assert!(ok, "binding a fresh var cannot fail");
474 invoke_k(m)
475 }
476 _ => {
477 crate::errors::type_error(m, "integer", x, "between/3 requires an integer");
478 0
479 }
480 }
481}
482
483unsafe extern "C" fn between_retry(m: *mut Machine, env: u64) -> i32 {
484 let m = unsafe { &mut *m };
485 let frame = env as usize;
486 let x = m.heap[frame];
487 let cur = m.heap[frame + 1] as i64 + 1;
488 let hi = m.heap[frame + 2] as i64;
489 m.heap[frame + 1] = cur as u64;
490 if cur < hi {
491 m.push_cp(between_retry, frame as u64);
492 }
493 let (kf, ke) = load_k(m, frame, 3);
494 m.k_fn = kf;
495 m.k_env = ke;
496 let w = int_to_word(m, cur);
497 let ok = unify(m, x, w);
498 debug_assert!(ok, "x rewound to unbound before retry");
499 invoke_k(m)
500}
501
502fn int_to_word(m: &mut Machine, n: i64) -> Word {
504 if (INT_MIN..=INT_MAX).contains(&n) {
505 make_int(n)
506 } else {
507 let idx = m.heap.len();
508 m.heap.push(n as u64);
509 make(TAG_BIG, idx as u64)
510 }
511}
512
513fn int_arg(m: &mut Machine, w: Word, who: &str) -> Option<i64> {
516 let w = m.deref(w);
517 match tag_of(w) {
518 TAG_INT => Some(int_value(w)),
519 TAG_BIG => Some(m.heap[payload(w) as usize] as i64),
520 TAG_REF => {
521 let ctx = format!("{who} requires bound integer bounds");
522 crate::errors::instantiation(m, &ctx);
523 None
524 }
525 _ => {
526 let ctx = format!("{who} requires an integer");
527 crate::errors::type_error(m, "integer", w, &ctx);
528 None
529 }
530 }
531}
532
533#[unsafe(no_mangle)]
538pub unsafe extern "C" fn plg_rt_pred_between_3(m: *mut Machine, _env: u64) -> i32 {
539 let m = unsafe { &mut *m };
540 let (lo, hi, x) = (m.areg[0], m.areg[1], m.areg[2]);
541 between_impl(m, lo, hi, x)
542}
543
544#[unsafe(no_mangle)]
548pub unsafe extern "C" fn plg_rt_metacall(m: *mut Machine, goal: u64) -> i32 {
549 let m = unsafe { &mut *m };
550 m.qbarrier = m.cps.len();
553 call_goal(m, goal)
554}
555
556#[unsafe(no_mangle)]
573pub unsafe extern "C" fn plg_rt_metacall_resolve(m: *mut Machine, goal: u64) -> u64 {
574 let m = unsafe { &mut *m };
575 m.qbarrier = m.cps.len();
576 match resolve_goal_ptr(m, goal) {
577 Some(f) => f as usize as u64,
578 None => 0,
579 }
580}
581
582fn resolve_goal_ptr(m: &mut Machine, mut goal: Word) -> Option<ContFn> {
583 loop {
584 goal = m.deref(goal);
585 match tag_of(goal) {
586 TAG_ATOM => return crate::solve::resolve_simple(m, atom_id(goal), 0, 0),
587 TAG_STR => {
588 let idx = payload(goal) as usize;
589 let (f, n) = unpack_functor(m.heap[idx]);
590 if n == 1 && m.atoms.resolve(f) == "call" {
596 goal = m.heap[idx + 1];
597 continue;
598 }
599 return crate::solve::resolve_simple(m, f, n, idx + 1);
600 }
601 _ => return None,
602 }
603 }
604}
605
606#[unsafe(no_mangle)]
609pub unsafe extern "C" fn plg_rt_b_catch_3(m: *mut Machine, g: u64, c: u64, r: u64) -> i32 {
610 catch_impl(unsafe { &mut *m }, g, c, r)
611}
612
613#[unsafe(no_mangle)]
616pub unsafe extern "C" fn plg_rt_b_throw_1(m: *mut Machine, ball: u64) -> i32 {
617 crate::errors::throw_term(unsafe { &mut *m }, ball);
618 0
619}
620
621#[unsafe(no_mangle)]
624pub unsafe extern "C" fn plg_rt_b_findall_3(m: *mut Machine, t: u64, g: u64, b: u64) -> i32 {
625 findall_impl(unsafe { &mut *m }, t, g, b)
626}
627
628#[cfg(test)]
629mod tests {
630 use crate::machine::Machine;
631 use crate::query::parse_query;
632 use crate::solve::{Outcome, solve};
633 use plg_shared::StringInterner;
634
635 fn run(query: &str) -> (Vec<String>, Option<String>) {
636 let mut m = Machine::new(StringInterner::new(), Vec::new());
637 let goal = parse_query(&mut m, query).unwrap();
638 let outcome = solve(&mut m, goal);
639 let err = match outcome {
640 Outcome::Error => Some(m.error.take().unwrap().message),
641 Outcome::Done => None,
642 };
643 let sols = m
644 .solutions
645 .iter()
646 .map(|s| {
647 s.bindings
648 .iter()
649 .map(|(n, _, t)| format!("{n}={t}"))
650 .collect::<Vec<_>>()
651 .join(",")
652 })
653 .collect();
654 (sols, err)
655 }
656
657 #[test]
658 fn top_level_is_and_comparison() {
659 assert_eq!(run("X is 2 + 3 * 4").0, vec!["X=14"]);
660 assert_eq!(run("1 < 2").0, vec![""]);
661 assert_eq!(run("2 < 1").0, Vec::<String>::new());
662 }
663
664 #[test]
665 fn top_level_disjunction_enumerates() {
666 assert_eq!(run("(X = 1 ; X = 2)").0, vec!["X=1", "X=2"]);
667 }
668
669 #[test]
670 fn top_level_ite_and_naf() {
671 assert_eq!(run("(1 < 2 -> X = yes ; X = no)").0, vec!["X=yes"]);
672 assert_eq!(run("(2 < 1 -> X = yes ; X = no)").0, vec!["X=no"]);
673 assert_eq!(run("\\+ 2 < 1").0, vec![""]);
674 assert_eq!(run("\\+ 1 < 2").0, Vec::<String>::new());
675 assert_eq!(run("\\+ (X = 1, 2 < 1), X = ok").0, vec!["X=ok"]);
677 }
678
679 #[test]
680 fn top_level_once_commits() {
681 assert_eq!(run("once((X = 1 ; X = 2))").0, vec!["X=1"]);
682 }
683
684 #[test]
685 fn errors_propagate() {
686 let (_, err) = run("X is 1 // 0");
687 assert!(err.unwrap().contains("zero_divisor"));
688 }
689}
690
691#[cfg(test)]
692mod m4_tests {
693 use crate::machine::Machine;
694 use crate::query::parse_query;
695 use crate::solve::{Outcome, solve};
696 use plg_shared::StringInterner;
697
698 fn run(query: &str) -> (Vec<String>, Option<String>) {
699 let mut m = Machine::new(StringInterner::new(), Vec::new());
700 let goal = parse_query(&mut m, query).unwrap();
701 let outcome = solve(&mut m, goal);
702 let err = match outcome {
703 Outcome::Error => Some(m.error.take().unwrap().message),
704 Outcome::Done => None,
705 };
706 let sols = m
707 .solutions
708 .iter()
709 .map(|s| {
710 s.bindings
711 .iter()
712 .map(|(n, _, t)| format!("{n}={t}"))
713 .collect::<Vec<_>>()
714 .join(",")
715 })
716 .collect();
717 (sols, err)
718 }
719
720 #[test]
721 fn throw_uncaught_propagates_with_rendered_ball() {
722 let (sols, err) = run("throw(my_ball)");
723 assert!(sols.is_empty());
724 assert_eq!(err.unwrap(), "my_ball");
725 }
726
727 #[test]
728 fn catch_catches_matching_ball_and_runs_recovery() {
729 let (sols, err) = run("catch(throw(oops(1)), oops(N), X = caught(N))");
730 assert!(err.is_none());
731 assert_eq!(sols, vec!["N=1,X=caught(1)"]);
732 }
733
734 #[test]
735 fn catch_passes_nonmatching_ball_outward() {
736 let (sols, err) = run("catch(throw(other), oops(_), X = no)");
737 assert!(sols.is_empty());
738 assert_eq!(err.unwrap(), "other");
739 }
740
741 #[test]
742 fn nested_catch_inner_first() {
743 let (sols, err) = run("catch(catch(throw(b), a, X = inner_a), b, X = outer_b)");
744 assert!(err.is_none());
745 assert_eq!(sols, vec!["X=outer_b"]);
746 }
747
748 #[test]
749 fn catch_traps_builtin_errors() {
750 let (sols, err) = run("catch(X is 1 // 0, error(evaluation_error(E), _), Y = E)");
751 assert!(err.is_none());
752 assert_eq!(sols, vec!["E=zero_divisor,X=_0,Y=zero_divisor"]);
753 }
754
755 #[test]
756 fn catch_is_transparent_to_normal_backtracking() {
757 let (sols, err) = run("catch((X = 1 ; X = 2), _, fail)");
758 assert!(err.is_none());
759 assert_eq!(sols, vec!["X=1", "X=2"]);
760 }
761
762 #[test]
763 fn step_limit_is_not_catchable() {
764 let mut m = Machine::new(StringInterner::new(), Vec::new());
765 m.step_limit = 1;
766 m.steps = 1; assert!(!m.step());
768 let goal = parse_query(&mut m, "catch(true, _, true)").unwrap();
769 assert!(matches!(solve(&mut m, goal), Outcome::Error));
771 assert!(m.error.as_ref().unwrap().uncatchable);
772 }
773
774 #[test]
775 fn findall_collects_and_rewinds() {
776 let (sols, err) = run("findall(X, (X = 1 ; X = 2 ; X = 3), L)");
777 assert!(err.is_none());
778 assert_eq!(sols, vec!["L=[1, 2, 3],X=_0"]);
779 }
780
781 #[test]
782 fn findall_empty_on_failure() {
783 let (sols, err) = run("findall(X, fail, L)");
784 assert!(err.is_none());
785 assert_eq!(sols, vec!["L=[],X=_0"]);
786 }
787
788 #[test]
789 fn findall_propagates_errors() {
790 let (sols, err) = run("findall(X, throw(bad), L)");
791 assert!(sols.is_empty());
792 assert_eq!(err.unwrap(), "bad");
793 }
794
795 #[test]
796 fn nested_findall() {
797 let (sols, err) = run("findall(L1, (Y = 2, findall(X, (X = 1 ; X = Y), L1)), L)");
798 assert!(err.is_none());
799 assert_eq!(sols.len(), 1);
800 assert!(sols[0].contains("L=[[1, 2]]"), "{sols:?}");
801 }
802
803 #[test]
804 fn call_n_extends_goals() {
805 let (sols, err) = run("call(=, X, 7)");
806 assert!(err.is_none());
807 assert_eq!(sols, vec!["X=7"]);
808 let (sols, _) = run("G = (X = 1 ; X = 2), call(G)");
809 assert_eq!(sols.len(), 2);
810 }
811
812 #[test]
813 fn call_unbound_is_instantiation_error() {
814 let (sols, err) = run("call(X)");
815 assert!(sols.is_empty());
816 assert!(err.unwrap().contains("instantiation_error"));
817 }
818
819 #[test]
820 fn between_enumerates_and_checks() {
821 let (sols, err) = run("between(1, 4, X)");
822 assert!(err.is_none());
823 assert_eq!(sols, vec!["X=1", "X=2", "X=3", "X=4"]);
824 let (sols, _) = run("between(1, 4, 3)");
825 assert_eq!(sols.len(), 1);
826 let (sols, _) = run("between(1, 4, 9)");
827 assert!(sols.is_empty());
828 let (sols, _) = run("between(3, 1, X)");
829 assert!(sols.is_empty());
830 let (sols, _) = run("between(2, 2, X)");
832 assert_eq!(sols, vec!["X=2"]);
833 }
834
835 #[test]
836 fn findall_with_between() {
837 let (sols, err) = run("findall(X, between(1, 5, X), L)");
838 assert!(err.is_none());
839 assert_eq!(sols, vec!["L=[1, 2, 3, 4, 5],X=_0"]);
840 }
841}
842
843#[cfg(test)]
844mod vocab_invariant {
845 use plg_shared::{BUILTINS, builtins::BuiltinKind};
853 use std::collections::BTreeSet;
854
855 #[rustfmt::skip]
858 const DET_DISPATCH: &[(&str, u32)] = &[
859 ("var", 1), ("nonvar", 1), ("atom", 1), ("number", 1), ("integer", 1),
860 ("float", 1), ("compound", 1), ("is_list", 1), ("functor", 3), ("arg", 3),
861 ("=..", 2), ("copy_term", 2), ("atom_length", 2), ("atom_concat", 3),
862 ("atom_chars", 2), ("number_chars", 2), ("number_codes", 2), ("msort", 2),
863 ("sort", 2), ("succ", 2), ("plus", 3), ("unify_with_occurs_check", 2),
864 ("write", 1), ("writeq", 1), ("writeln", 1),
865 ];
866
867 #[test]
868 fn det_dispatch_equals_shared_det_subset() {
869 let dispatch: BTreeSet<(&str, u32)> = DET_DISPATCH.iter().copied().collect();
870 let shared_det: BTreeSet<(&str, u32)> = BUILTINS
871 .iter()
872 .filter(|s| s.kind == BuiltinKind::Det && s.arity > 0)
873 .map(|s| (s.name, s.arity))
874 .collect();
875 assert_eq!(
876 dispatch, shared_det,
877 "runtime det dispatch diverges from BUILTINS Det subset \
878 (left = control.rs, right = shared table)"
879 );
880 }
881}