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).
125pub fn call_goal(m: &mut Machine, goal: Word) -> i32 {
126    let goal = m.deref(goal);
127    match tag_of(goal) {
128        TAG_ATOM => {
129            let name = m.atoms.resolve(atom_id(goal)).to_string();
130            if let Some(r) = crate::control::try_atom_builtin(m, &name) {
131                return r;
132            }
133            dispatch(m, atom_id(goal), 0, 0)
134        }
135        TAG_STR => {
136            let idx = payload(goal) as usize;
137            let (f, n) = unpack_functor(m.heap[idx]);
138            let name = m.atoms.resolve(f).to_string();
139            if let Some(r) = crate::control::try_builtin(m, &name, idx + 1, n) {
140                return r;
141            }
142            dispatch(m, f, n, idx + 1)
143        }
144        TAG_REF => {
145            crate::errors::instantiation(m, "Goal is an unbound variable");
146            0
147        }
148        _ => {
149            crate::errors::type_error(m, "callable", goal, "Goal is not callable");
150            0
151        }
152    }
153}
154
155fn dispatch(m: &mut Machine, functor: u32, arity: u32, args_idx: usize) -> i32 {
156    let Some(f) = m.registry_lookup(functor, arity) else {
157        let name = m.atoms.resolve(functor).to_string();
158        // Query-side / metacall undefined goals have no compiled call site;
159        // `m.error_site` stays `NO_SITE`, so no provenance suffix is added.
160        crate::errors::existence_procedure(m, &name, arity);
161        return 0;
162    };
163    debug_assert!(arity as usize <= MAX_ARGS);
164    for i in 0..arity as usize {
165        m.areg[i] = m.heap[args_idx + i];
166    }
167    unsafe { f(m as *mut Machine, 0) }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use crate::machine::RegistryEntry;
174    use plg_shared::StringInterner;
175
176    /// A hand-written "compiled predicate" standing in for codegen
177    /// output: `p(a). p(b).` — entry pushes a CP for clause 2, then
178    /// tries clause 1, calling the continuation on success.
179    unsafe extern "C" fn p_entry(m: *mut Machine, _env: u64) -> i32 {
180        let mr = unsafe { &mut *m };
181        if !mr.step() {
182            return 0;
183        }
184        // frame: [a0, k_fn, k_env]
185        let f = mr.frame_alloc(3);
186        mr.heap[f] = mr.areg[0];
187        mr.heap[f + 1] = mr.k_fn as usize as u64;
188        mr.heap[f + 2] = mr.k_env;
189        mr.push_cp(p_clause2, f as u64);
190        unsafe { p_clause1(m, f as u64) }
191    }
192
193    unsafe extern "C" fn p_clause1(m: *mut Machine, env: u64) -> i32 {
194        let mr = unsafe { &mut *m };
195        let f = env as usize;
196        let atom_a = mr.atoms.lookup("a").unwrap();
197        if !crate::unify::unify(mr, mr.heap[f], make_atom(atom_a)) {
198            return 0;
199        }
200        let k: ContFn = unsafe { std::mem::transmute(mr.heap[f + 1] as usize) };
201        unsafe { k(m, mr.heap[f + 2]) }
202    }
203
204    unsafe extern "C" fn p_clause2(m: *mut Machine, env: u64) -> i32 {
205        let mr = unsafe { &mut *m };
206        let f = env as usize;
207        let atom_b = mr.atoms.lookup("b").unwrap();
208        if !crate::unify::unify(mr, mr.heap[f], make_atom(atom_b)) {
209            return 0;
210        }
211        let k: ContFn = unsafe { std::mem::transmute(mr.heap[f + 1] as usize) };
212        unsafe { k(m, mr.heap[f + 2]) }
213    }
214
215    fn machine_with_p() -> Box<Machine> {
216        let mut atoms = StringInterner::new();
217        let p = atoms.intern("p");
218        atoms.intern("a");
219        atoms.intern("b");
220        let registry = vec![RegistryEntry {
221            functor: p,
222            arity: 1,
223            f: p_entry,
224        }];
225        Machine::new(atoms, registry)
226    }
227
228    #[test]
229    fn enumerates_both_solutions_via_backtracking() {
230        let mut m = machine_with_p();
231        let goal = crate::query::parse_query(&mut m, "p(X)").unwrap();
232        assert!(matches!(solve(&mut m, goal), Outcome::Done));
233        assert!(m.error.is_none());
234        assert_eq!(m.solutions.len(), 2);
235        assert_eq!(m.solutions[0].bindings[0].2, "a");
236        assert_eq!(m.solutions[1].bindings[0].2, "b");
237    }
238
239    #[test]
240    fn ground_query_checks_membership() {
241        let mut m = machine_with_p();
242        let goal = crate::query::parse_query(&mut m, "p(b)").unwrap();
243        solve(&mut m, goal);
244        assert_eq!(m.solutions.len(), 1);
245
246        let mut m2 = machine_with_p();
247        let goal2 = crate::query::parse_query(&mut m2, "p(zzz)").unwrap();
248        solve(&mut m2, goal2);
249        assert_eq!(m2.solutions.len(), 0);
250    }
251
252    #[test]
253    fn limit_stops_enumeration() {
254        let mut m = machine_with_p();
255        m.solution_limit = Some(1);
256        let goal = crate::query::parse_query(&mut m, "p(X)").unwrap();
257        solve(&mut m, goal);
258        assert_eq!(m.solutions.len(), 1);
259    }
260
261    #[test]
262    fn conjunction_runs_both_goals() {
263        let mut m = machine_with_p();
264        let goal = crate::query::parse_query(&mut m, "p(X), p(Y)").unwrap();
265        solve(&mut m, goal);
266        // 2 x 2 cartesian solutions
267        assert_eq!(m.solutions.len(), 4);
268    }
269
270    #[test]
271    fn unknown_predicate_raises_existence_error() {
272        let mut m = machine_with_p();
273        let goal = crate::query::parse_query(&mut m, "nosuch(X)").unwrap();
274        assert!(matches!(solve(&mut m, goal), Outcome::Error));
275        let msg = &m.error.as_ref().unwrap().message;
276        assert_eq!(
277            msg,
278            "error(existence_error(procedure, /(nosuch, 1)), Undefined procedure: nosuch/1)"
279        );
280    }
281}