Skip to main content

plg_runtime/
control.rs

1//! Query-level control constructs and deterministic builtins.
2//!
3//! Clause bodies compile control flow to native code; this module only
4//! serves goals built at RUNTIME — the `--query` string today, call/1
5//! metacalls in M4. It walks goal TERMS, never clauses (the rule in
6//! docs/design/LESSONS_FROM_V1.md stays intact).
7//!
8//! The implementations mirror the compiled lowering exactly (same
9//! choice-point shapes, same commit heights), so a goal behaves
10//! identically whether it appears in a clause body or a query.
11
12use crate::builtins::pred;
13use crate::cell::*;
14use crate::machine::{ContFn, Machine, NO_SITE};
15use crate::solve::call_goal;
16use crate::unify::unify;
17
18/// Invoke the current continuation (a goal succeeded deterministically).
19fn 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
29/// Try to handle `name/arity` as a control construct or deterministic
30/// builtin. Returns None when it's an ordinary predicate call.
31pub fn try_builtin(m: &mut Machine, name: &str, args_idx: usize, arity: u32) -> Option<i32> {
32    let mp = m as *mut Machine;
33    // Snapshot the argument words: goal args are read-only here and the
34    // heap may grow (frames) before they're consumed.
35    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            // `(C -> T ; E)` is if-then-else.
41            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            // Runtime-walked (query/metacall): no compiled call site.
73            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
102/// Query-side dispatch for the deterministic builtin vocabulary —
103/// mirrors codegen's DET_BUILTINS table (lower.rs); the diff corpus
104/// guards the pair. Returns None for non-builtins.
105fn 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        // Runtime-walked (query/metacall): raising builtins get NO_SITE.
117        ("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
141/// Atom-only goals (`true`, `fail`, `!`).
142pub 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            // Cut to the walker barrier: 0 at the query top level,
152            // local inside call-like constructs (\+, once, ->-condition,
153            // call/N, findall goals). cut_to stops at catch frames.
154            let h = m.qbarrier;
155            m.cut_to(h);
156            Some(invoke_k(m))
157        }
158        _ => None,
159    }
160}
161
162/// ABI op codes — must match codegen's lower.rs tables.
163fn 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
187/// Snapshot the continuation AND the walker cut barrier (3 cells:
188/// k_fn, k_env, qbarrier) — the runtime mirror of compiled cut slots.
189fn 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
195/// Restore continuation + barrier from a frame.
196fn 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
202/// `,`/2: run A with a continuation that runs B.
203fn 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
222/// `(A ; B)`: push a CP retrying B (with the current k restored), run A.
223fn 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
241/// `(C -> T ; E)` / `(C -> T)`: commit to C's first solution.
242fn if_then_else(m: &mut Machine, c: Word, t: Word, e: Option<Word>) -> i32 {
243    let h = m.cps.len() as u64; // BEFORE the else CP
244    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    // The condition is call-like: `!` inside it is local (cuts to the
257    // height AFTER the else CP).
258    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); // commit: kill E and C's alternatives
267    let t = m.heap[frame];
268    let (kf, ke) = load_k(m, frame, 1); // also restores the outer barrier
269    m.k_fn = kf;
270    m.k_env = ke;
271    call_goal(m, t)
272}
273
274/// `once(G)`: commit to G's first solution, then continue.
275fn 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-like: `!` inside G is local
283    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
297/// `\+ G`: push a CP that CONTINUES (driver rewind undoes G's
298/// bindings); if G succeeds, cut to the pre-NAF height and fail.
299fn 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    // Call-like: `!` inside G is local (cannot prune the continue CP).
309    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); // removes the continue-CP and G's alternatives
326    0
327}
328
329/// `catch(Goal, Catcher, Recovery)`: push a catch frame (transparent to
330/// normal backtracking, a target for error unwinding in solve::drive
331/// and a barrier for cut), then run Goal with the current continuation.
332fn 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
341/// Backtracking INTO a catch frame (no error in flight) is transparent:
342/// the frame offers no alternatives.
343unsafe extern "C" fn catch_retry(_m: *mut Machine, _env: u64) -> i32 {
344    0
345}
346
347/// `findall(Template, Goal, Bag)`: run Goal to exhaustion in a bounded
348/// sub-search, snapshotting Template per solution; rewind everything;
349/// unify Bag with the collected list. Errors propagate out (a catch
350/// inside Goal is handled by the shared driver within our floor).
351fn 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(); // call-like: `!` inside Goal is local
362    let r = call_goal(m, goal);
363    crate::solve::drive(m, floor, r); // collector always returns 0
364    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    // Discard the goal's bindings and workspace, then build the bag.
372    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 // force backtracking into the next solution
394}
395
396/// `call/N`: extend the goal with extra arguments (ISO 7.8.3 partial
397/// application), then dispatch it.
398fn metacall_extend(m: &mut Machine, goal: Word, extras: &[Word]) -> i32 {
399    let goal = m.deref(goal);
400    // call/N is opaque to cut (ISO): `!` inside the called goal is local.
401    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
437/// `between(Low, High, X)` — the one nondeterministic builtin: with X
438/// unbound it enumerates Low..=High via a runtime-retried choice point.
439fn 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            // Frame: [x, current, high, k_fn, k_env, qbarrier].
461            // `current` is mutated in place across retries (untrailed
462            // by design — the frame is control state, not a term).
463            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
502/// Build an integer word: immediate when it fits i61, boxed otherwise.
503fn 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
513/// Deref an integer argument for between/3 (v1: bounds must be bound
514/// integers).
515fn 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/// Compiled-code entry for between/3 (uniform predicate signature,
534/// arguments in the A registers — dispatched like a user predicate).
535/// # Safety
536/// Called from generated code with the live Machine pointer.
537#[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/// Compiled-code entries for control builtins taking goal terms.
545/// # Safety
546/// Called from generated code with the live Machine pointer.
547#[unsafe(no_mangle)]
548pub unsafe extern "C" fn plg_rt_metacall(m: *mut Machine, goal: u64) -> i32 {
549    let m = unsafe { &mut *m };
550    // Goals reaching the walker from compiled code are call-like:
551    // a runtime-walked `!` inside them is local.
552    m.qbarrier = m.cps.len();
553    call_goal(m, goal)
554}
555
556/// Fast-path resolver for the metacall trampoline (#23): if `goal` is a
557/// simple compiled-predicate call (after peeling a single `call/1` wrapper),
558/// marshal its arguments and return the entry function pointer as an integer
559/// for generated IR to `musttail` into — giving `call(pred(...))` tail
560/// recursion constant C stack, like a direct call. Returns 0 for anything the
561/// full walker must handle (builtins, control constructs, `call/N` with extra
562/// args, variables, undefined predicates); the caller then falls back to
563/// `plg_rt_metacall`, which is bounded by the depth guard in `call_goal`.
564///
565/// Sets `qbarrier` exactly as `plg_rt_metacall` does, so cut-transparency of
566/// `call/N` is identical on both paths. The fast path does NOT bump
567/// `metacall_depth`: a `musttail` into the resolved entry leaves no walker
568/// frame to bound (only the slow path re-enters `call_goal`, which guards).
569///
570/// # Safety
571/// Called from generated code with the live Machine pointer.
572#[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                // Peel `call/1` wrappers *iteratively* so `call(pred(..))`
591                // trampolines — and so even a pathological `call(call(...))`
592                // chain can't overflow the resolver's own stack. `call/N` with
593                // extras (and other shapes) go to the walker, which builds the
594                // extended goal in `metacall_extend`.
595                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/// # Safety
607/// Called from generated code with the live Machine pointer.
608#[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/// # Safety
614/// Called from generated code with the live Machine pointer.
615#[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/// # Safety
622/// Called from generated code with the live Machine pointer.
623#[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        // NAF undoes inner bindings.
676        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; // next step() trips the limit
767        assert!(!m.step());
768        let goal = parse_query(&mut m, "catch(true, _, true)").unwrap();
769        // error pre-armed and uncatchable: solve returns Error untouched
770        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        // single-value range: no choice point churn
831        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    //! Runtime half of the `plg-shared::builtins` invariant
846    //! (docs/design/BUILTIN_VOCAB.md). `det_builtin` above is the
847    //! query-side mirror of codegen's `DET_BUILTINS`; this asserts the
848    //! arms it handles are EXACTLY the arity>0 `Det` rows of `BUILTINS`
849    //! (`nl/0` is Det but dispatched via `try_atom_builtin`, so it is
850    //! excluded here). Referenced only under `#[cfg(test)]` — the `doc`
851    //! strings never reach a compiled program binary.
852    use plg_shared::{BUILTINS, builtins::BuiltinKind};
853    use std::collections::BTreeSet;
854
855    /// Hand mirror of the `det_builtin` match arms. Adding an arm there
856    /// without updating this (or `BUILTINS`) turns the test red.
857    #[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}