Skip to main content

plg_runtime/
solve.rs

1//! The solve driver: dispatches a parsed goal into compiled predicates
2//! and runs the backtracking loop.
3//!
4//! Forward execution inside compiled code is all `musttail` chains; when
5//! a chain fails it returns 0 all the way back here, and this loop pops
6//! the next choice point, rewinds, and invokes its retry function. The
7//! C stack therefore never grows with Prolog recursion or backtracking
8//! depth — this loop is the trampoline.
9
10use crate::cell::*;
11#[cfg(test)]
12use crate::machine::ContFn;
13use crate::machine::{MAX_ARGS, Machine};
14use crate::render;
15
16pub enum Outcome {
17    /// Search finished. `stopped_early` is true when the solution limit
18    /// cut enumeration short (=> exhausted:false in the output).
19    Done,
20    Error,
21}
22
23/// Solve `goal`, capturing solutions via the print continuation.
24pub fn solve(m: &mut Machine, goal: Word) -> Outcome {
25    m.k_fn = capture_k;
26    m.k_env = 0;
27    m.qbarrier = 0;
28    let r = call_goal(m, goal);
29    drive(m, 0, r);
30    if m.error.is_some() {
31        Outcome::Error
32    } else {
33        Outcome::Done
34    }
35}
36
37/// The backtracking driver: pops choice points down to `floor`,
38/// rewinding and retrying, and unwinds errors to the nearest catch
39/// frame. Shared by the top level and findall/3's bounded sub-search.
40/// Returns 1 if a continuation stopped enumeration; otherwise 0 (CP
41/// stack drained to floor, or an uncaught error left in `m.error` for
42/// the next layer out).
43pub fn drive(m: &mut Machine, floor: usize, mut r: i32) -> i32 {
44    loop {
45        if m.error.is_some() {
46            if m.error.as_ref().is_some_and(|e| e.uncatchable) {
47                return 0;
48            }
49            match unwind_to_catch(m, floor) {
50                Some(r2) => {
51                    r = r2;
52                    continue;
53                }
54                None => return 0, // no catch here; error stays set
55            }
56        }
57        if r == 1 {
58            return 1; // stop (solution limit / committed)
59        }
60        if m.cps.len() <= floor {
61            return 0; // exhausted this window
62        }
63        let cp = m.cps.pop().unwrap();
64        m.rewind_to(cp.trail_mark, cp.heap_mark);
65        r = unsafe { (cp.retry)(m as *mut Machine, cp.env) };
66    }
67}
68
69/// Unwind the CP stack looking for a catch frame whose catcher unifies
70/// with the ball (v1 dispatch_or_return semantics). On a match, clears
71/// the error, restores the catch-time continuation, and runs the
72/// recovery goal, returning its result. `None` if no frame matches
73/// above `floor` (the error remains set).
74fn unwind_to_catch(m: &mut Machine, floor: usize) -> Option<i32> {
75    use crate::machine::CpKind;
76    while m.cps.len() > floor {
77        let cp = m.cps.pop().unwrap();
78        m.rewind_to(cp.trail_mark, cp.heap_mark);
79        if cp.kind != CpKind::Catch {
80            continue;
81        }
82        // Catch frame env: [catcher, recovery, k_fn, k_env, qbarrier] —
83        // allocated before the frame's marks, so it survived the rewind.
84        let f = cp.env as usize;
85        let catcher = m.heap[f];
86        let recovery = m.heap[f + 1];
87        let err = m.error.take().unwrap();
88        let ball_w = crate::copyterm::restore_from_buf(m, &err.ball);
89        let tmark = m.trail.len();
90        if crate::unify::unify(m, catcher, ball_w) {
91            let kf: crate::machine::ContFn = unsafe { std::mem::transmute(m.heap[f + 2] as usize) };
92            m.k_fn = kf;
93            m.k_env = m.heap[f + 3];
94            m.qbarrier = m.heap[f + 4] as usize;
95            return Some(call_goal(m, recovery));
96        }
97        // Catcher doesn't match: undo the attempt, keep unwinding with
98        // the error re-armed. (The restored ball leaks a few heap cells
99        // until the next rewind — harmless.)
100        while m.trail.len() > tmark {
101            let idx = m.trail.pop().unwrap() as usize;
102            m.heap[idx] = make_ref(idx);
103        }
104        m.error = Some(err);
105    }
106    None
107}
108
109/// Success continuation for top-level queries: capture the bindings,
110/// then ask for more solutions (return 0 = force backtracking) until
111/// the limit is reached (return 1 = stop).
112unsafe extern "C" fn capture_k(m: *mut Machine, _env: u64) -> i32 {
113    let m = unsafe { &mut *m };
114    m.solutions.push(render::capture_solution(m));
115    match m.solution_limit {
116        Some(limit) if m.solutions.len() >= limit => 1,
117        _ => 0,
118    }
119}
120
121/// Dispatch a goal term: control constructs and deterministic builtins
122/// go through `control::try_builtin`; everything else is a registry
123/// lookup into compiled code. Also used by control continuations (and
124/// later by call/1 and findall/3).
125///
126/// Wraps the walk in a depth guard (#23): runtime-walked recursion that is
127/// not trampolined (control-construct metacalls, deep `findall/3` or `catch/3`
128/// recovery) fails with an uncatchable resource error instead of overflowing
129/// the native C stack. Compiled tail recursion reaches the callee via the
130/// resolve trampoline and never passes through here.
131pub fn call_goal(m: &mut Machine, goal: Word) -> i32 {
132    // The guard increments now and decrements on every exit path (incl. panic
133    // unwind), so the depth can't leak — see `MetacallDepthGuard`.
134    let _depth = crate::machine::MetacallDepthGuard::enter(m as *mut Machine);
135    if m.metacall_depth > m.metacall_depth_limit {
136        let ctx = format!(
137            "Maximum metacall recursion depth exceeded ({})",
138            m.metacall_depth_limit
139        );
140        crate::errors::resource(m, "metacall_depth", &ctx, true);
141        return 0;
142    }
143    call_goal_inner(m, goal)
144}
145
146fn call_goal_inner(m: &mut Machine, goal: Word) -> i32 {
147    let goal = m.deref(goal);
148    match tag_of(goal) {
149        TAG_ATOM => {
150            let name = m.atoms.resolve(atom_id(goal)).to_string();
151            if let Some(r) = crate::control::try_atom_builtin(m, &name) {
152                return r;
153            }
154            dispatch(m, atom_id(goal), 0, 0)
155        }
156        TAG_STR => {
157            let idx = payload(goal) as usize;
158            let (f, n) = unpack_functor(m.heap[idx]);
159            let name = m.atoms.resolve(f).to_string();
160            if let Some(r) = crate::control::try_builtin(m, &name, idx + 1, n) {
161                return r;
162            }
163            dispatch(m, f, n, idx + 1)
164        }
165        TAG_REF => {
166            crate::errors::instantiation(m, "Goal is an unbound variable");
167            0
168        }
169        _ => {
170            crate::errors::type_error(m, "callable", goal, "Goal is not callable");
171            0
172        }
173    }
174}
175
176/// Resolve a goal's functor/arity to a compiled predicate entry, marshalling
177/// its arguments into the arg registers. Returns `None` when no such predicate
178/// is registered — builtins and control constructs are never in the registry,
179/// so they fall through here too (the caller routes them to the walker).
180///
181/// Single source of the lookup+marshal, shared by `dispatch` (a Rust call) and
182/// `plg_rt_metacall_resolve` (the IR `musttail` trampoline), so the two paths
183/// cannot drift.
184pub(crate) fn resolve_simple(
185    m: &mut Machine,
186    functor: u32,
187    arity: u32,
188    args_idx: usize,
189) -> Option<crate::machine::ContFn> {
190    let f = m.registry_lookup(functor, arity)?;
191    debug_assert!(arity as usize <= MAX_ARGS);
192    for i in 0..arity as usize {
193        m.areg[i] = m.heap[args_idx + i];
194    }
195    Some(f)
196}
197
198fn dispatch(m: &mut Machine, functor: u32, arity: u32, args_idx: usize) -> i32 {
199    match resolve_simple(m, functor, arity, args_idx) {
200        Some(f) => unsafe { f(m as *mut Machine, 0) },
201        None => {
202            let name = m.atoms.resolve(functor).to_string();
203            // Query-side / metacall undefined goals have no compiled call site;
204            // `m.error_site` stays `NO_SITE`, so no provenance suffix is added.
205            crate::errors::existence_procedure(m, &name, arity);
206            0
207        }
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214    use crate::machine::RegistryEntry;
215    use plg_shared::StringInterner;
216
217    /// A hand-written "compiled predicate" standing in for codegen
218    /// output: `p(a). p(b).` — entry pushes a CP for clause 2, then
219    /// tries clause 1, calling the continuation on success.
220    unsafe extern "C" fn p_entry(m: *mut Machine, _env: u64) -> i32 {
221        let mr = unsafe { &mut *m };
222        if !mr.step() {
223            return 0;
224        }
225        // frame: [a0, k_fn, k_env]
226        let f = mr.frame_alloc(3);
227        mr.heap[f] = mr.areg[0];
228        mr.heap[f + 1] = mr.k_fn as usize as u64;
229        mr.heap[f + 2] = mr.k_env;
230        mr.push_cp(p_clause2, f as u64);
231        unsafe { p_clause1(m, f as u64) }
232    }
233
234    unsafe extern "C" fn p_clause1(m: *mut Machine, env: u64) -> i32 {
235        let mr = unsafe { &mut *m };
236        let f = env as usize;
237        let atom_a = mr.atoms.lookup("a").unwrap();
238        if !crate::unify::unify(mr, mr.heap[f], make_atom(atom_a)) {
239            return 0;
240        }
241        let k: ContFn = unsafe { std::mem::transmute(mr.heap[f + 1] as usize) };
242        unsafe { k(m, mr.heap[f + 2]) }
243    }
244
245    unsafe extern "C" fn p_clause2(m: *mut Machine, env: u64) -> i32 {
246        let mr = unsafe { &mut *m };
247        let f = env as usize;
248        let atom_b = mr.atoms.lookup("b").unwrap();
249        if !crate::unify::unify(mr, mr.heap[f], make_atom(atom_b)) {
250            return 0;
251        }
252        let k: ContFn = unsafe { std::mem::transmute(mr.heap[f + 1] as usize) };
253        unsafe { k(m, mr.heap[f + 2]) }
254    }
255
256    fn machine_with_p() -> Box<Machine> {
257        let mut atoms = StringInterner::new();
258        let p = atoms.intern("p");
259        atoms.intern("a");
260        atoms.intern("b");
261        let registry = vec![RegistryEntry {
262            functor: p,
263            arity: 1,
264            f: p_entry,
265        }];
266        Machine::new(atoms, registry)
267    }
268
269    #[test]
270    fn enumerates_both_solutions_via_backtracking() {
271        let mut m = machine_with_p();
272        let goal = crate::query::parse_query(&mut m, "p(X)").unwrap();
273        assert!(matches!(solve(&mut m, goal), Outcome::Done));
274        assert!(m.error.is_none());
275        assert_eq!(m.solutions.len(), 2);
276        assert_eq!(m.solutions[0].bindings[0].2, "a");
277        assert_eq!(m.solutions[1].bindings[0].2, "b");
278    }
279
280    #[test]
281    fn ground_query_checks_membership() {
282        let mut m = machine_with_p();
283        let goal = crate::query::parse_query(&mut m, "p(b)").unwrap();
284        solve(&mut m, goal);
285        assert_eq!(m.solutions.len(), 1);
286
287        let mut m2 = machine_with_p();
288        let goal2 = crate::query::parse_query(&mut m2, "p(zzz)").unwrap();
289        solve(&mut m2, goal2);
290        assert_eq!(m2.solutions.len(), 0);
291    }
292
293    #[test]
294    fn limit_stops_enumeration() {
295        let mut m = machine_with_p();
296        m.solution_limit = Some(1);
297        let goal = crate::query::parse_query(&mut m, "p(X)").unwrap();
298        solve(&mut m, goal);
299        assert_eq!(m.solutions.len(), 1);
300    }
301
302    #[test]
303    fn conjunction_runs_both_goals() {
304        let mut m = machine_with_p();
305        let goal = crate::query::parse_query(&mut m, "p(X), p(Y)").unwrap();
306        solve(&mut m, goal);
307        // 2 x 2 cartesian solutions
308        assert_eq!(m.solutions.len(), 4);
309    }
310
311    #[test]
312    fn unknown_predicate_raises_existence_error() {
313        let mut m = machine_with_p();
314        let goal = crate::query::parse_query(&mut m, "nosuch(X)").unwrap();
315        assert!(matches!(solve(&mut m, goal), Outcome::Error));
316        let msg = &m.error.as_ref().unwrap().message;
317        assert_eq!(
318            msg,
319            "error(existence_error(procedure, /(nosuch, 1)), Undefined procedure: nosuch/1)"
320        );
321    }
322}