Skip to main content

plg_runtime/
machine.rs

1//! The Machine: the single runtime context every compiled predicate
2//! receives (`%M` in generated IR). Owns the term heap, trail,
3//! choice-point stack, argument registers, the current success
4//! continuation, step accounting, and the runtime atom table.
5
6use crate::cell::{self, Word};
7use plg_shared::StringInterner;
8
9/// Uniform signature shared by every compiled predicate, continuation,
10/// and retry function — and by runtime-provided continuations. The
11/// uniform C-ABI prototype is what makes `musttail` transfers valid.
12/// Returns 0 = fail/exhausted (driver backtracks), 1 = stop (limit hit
13/// or final success).
14pub type ContFn = unsafe extern "C" fn(*mut Machine, u64) -> i32;
15
16/// Maximum predicate arity passed via argument registers (v1 had no
17/// practical limit; raise alongside a frame-passing scheme if ever hit).
18pub const MAX_ARGS: usize = 16;
19
20/// Registry row emitted by codegen as `{ i32, i32, ptr }`, sorted by
21/// (functor, arity) for binary search.
22#[repr(C)]
23#[derive(Clone, Copy)]
24pub struct RegistryEntry {
25    pub functor: u32,
26    pub arity: u32,
27    pub f: ContFn,
28}
29
30/// One source-location row of the codegen-emitted `@plg_srcmap` side-table
31/// (SPANS.md Layer 3). A raising call site passes its index (`site_id`) and
32/// the error path resolves it to `file:line:col`. Layout mirrors the IR's
33/// `{i32, i32, i32}`.
34#[repr(C)]
35#[derive(Clone, Copy)]
36pub struct SrcLoc {
37    pub file: u32,
38    pub line: u32,
39    pub col: u32,
40}
41
42/// `site_id` sentinel meaning "no source location" — runtime-internal
43/// raises (query-side undefined goals) and any binary built without
44/// provenance. The error message gets no `at file:line:col` suffix.
45///
46/// ABI contract: MUST equal `plg_compiler::codegen::NO_SITE` (codegen emits
47/// this value as the `site_id` arg). Separate consts in separate crates;
48/// each pins `== u32::MAX` in a unit test to flag a one-sided renumber.
49pub const NO_SITE: u32 = u32::MAX;
50
51/// RAII guard for the in-flight raise's site (SPANS.md Layer 3). A raising
52/// compiled builtin creates one at its ABI boundary; `Drop` **restores the
53/// previous value** (not `NO_SITE`), so the set/clear can't be forgotten and
54/// nested raises — an outer builtin whose work reaches an inner raising
55/// builtin — each see their own site without a save/restore stack. Use this
56/// instead of hand-writing `m.error_site = site; ...; m.error_site = NO_SITE`.
57///
58/// FOOTGUN: bind to a **named** variable — `let _site = ...;`. `let _ = ...`
59/// drops the guard immediately, so the body runs with the site already
60/// restored (no provenance, or the caller's site). `#[must_use]` does not
61/// catch the `let _` form.
62#[must_use = "binding to `let _` drops the guard immediately; use `let _site = ...`"]
63pub(crate) struct ErrorSiteGuard {
64    m: *mut Machine,
65    saved: u32,
66}
67
68impl ErrorSiteGuard {
69    pub(crate) fn enter(m: *mut Machine, site_id: u32) -> Self {
70        // SAFETY: `m` is the valid Machine pointer the builtin was called
71        // with. We touch only `error_site`, and the `Drop` write runs after
72        // the builtin's `&mut Machine` borrows have ended.
73        let saved = unsafe { (*m).error_site };
74        unsafe { (*m).error_site = site_id };
75        ErrorSiteGuard { m, saved }
76    }
77}
78
79impl Drop for ErrorSiteGuard {
80    fn drop(&mut self) {
81        unsafe { (*self.m).error_site = self.saved };
82    }
83}
84
85/// Catch frames participate in error unwinding (drive() in solve.rs)
86/// and stop cut truncation (v1 rule: catch is opaque to cut).
87#[derive(Clone, Copy, PartialEq)]
88pub enum CpKind {
89    Normal,
90    Catch,
91}
92
93pub struct ChoicePoint {
94    pub trail_mark: usize,
95    pub heap_mark: usize,
96    pub retry: ContFn,
97    pub env: u64,
98    pub kind: CpKind,
99}
100
101/// A runtime error in flight. The ball is a relocatable copy (it must
102/// survive heap rewinding on the way to a catch frame); the message is
103/// its v1-format rendering for top-level output (exit code 3).
104pub struct RtError {
105    pub ball: crate::copyterm::TermBuf,
106    pub message: String,
107    pub uncatchable: bool,
108}
109
110pub struct Machine {
111    pub heap: Vec<Word>,
112    pub trail: Vec<u64>, // heap indices of bound REF cells
113    pub cps: Vec<ChoicePoint>,
114    pub areg: [Word; MAX_ARGS],
115    /// Build registers for `plg_rt_put_struct` (separate from areg so
116    /// argument setup and term construction never clobber each other).
117    pub breg: [Word; MAX_ARGS],
118    pub k_fn: ContFn,
119    pub k_env: u64,
120    pub steps: u64,
121    pub step_limit: u64,
122    pub error: Option<RtError>,
123    pub atoms: StringInterner,
124    pub registry: Vec<RegistryEntry>,
125    /// Source-location side-table (SPANS.md Layer 3), handed over by codegen
126    /// at init. Empty for binaries built without provenance.
127    pub srcmap: Vec<SrcLoc>,
128    /// `file_id` → filename, parallel to `srcmap`'s `file` field.
129    pub files: Vec<String>,
130    /// `site_id` of the raise currently in flight (SPANS.md Layer 3).
131    /// `set_formal` appends ` at file:line:col` from it. `NO_SITE` (the
132    /// default, and the value for runtime-internal/query-side raises) means no
133    /// suffix — keeping those messages byte-identical to v1.
134    ///
135    /// INVARIANT: a raising compiled builtin must set this to its site only
136    /// around its own work and restore it after — always via `ErrorSiteGuard`,
137    /// which makes the set/restore impossible to forget and keeps nested
138    /// raises correct (each restores the caller's site, not `NO_SITE`).
139    pub error_site: u32,
140    /// Query variables in source order: (name, heap index of the cell).
141    pub query_vars: Vec<(String, usize)>,
142    /// findall/3 collector stack (a stack because findall can nest):
143    /// each level accumulates relocatable copies of template instances.
144    pub findall_stack: Vec<Vec<crate::copyterm::TermBuf>>,
145    /// Cut barrier for `!` in RUNTIME-WALKED goals (queries, metacalls).
146    /// Call-like constructs set it for their inner goal; every walker
147    /// continuation frame snapshots and restores it (the runtime mirror
148    /// of the compiled cut_slot). Compiled `!` never reads this.
149    pub qbarrier: usize,
150    /// Solutions captured by the print continuation, already rendered.
151    pub solutions: Vec<crate::render::RenderedSolution>,
152    pub solution_limit: Option<usize>,
153}
154
155unsafe extern "C" fn no_continuation(_m: *mut Machine, _env: u64) -> i32 {
156    // Reaching the continuation with none installed is a codegen bug.
157    debug_assert!(false, "no continuation installed");
158    0
159}
160
161impl Machine {
162    pub fn new(atoms: StringInterner, registry: Vec<RegistryEntry>) -> Box<Machine> {
163        Box::new(Machine {
164            heap: Vec::with_capacity(4096),
165            trail: Vec::with_capacity(256),
166            cps: Vec::with_capacity(64),
167            areg: [0; MAX_ARGS],
168            breg: [0; MAX_ARGS],
169            k_fn: no_continuation,
170            k_env: 0,
171            steps: 0,
172            step_limit: 10_000, // v1 default
173            error: None,
174            atoms,
175            registry,
176            srcmap: Vec::new(),
177            files: Vec::new(),
178            error_site: NO_SITE,
179            query_vars: Vec::new(),
180            findall_stack: Vec::new(),
181            qbarrier: 0,
182            solutions: Vec::new(),
183            solution_limit: None,
184        })
185    }
186
187    /// Allocate a fresh unbound variable cell; returns its REF word.
188    pub fn new_var(&mut self) -> Word {
189        let idx = self.heap.len();
190        self.heap.push(cell::make_ref(idx)); // unbound = self-reference
191        cell::make_ref(idx)
192    }
193
194    /// Allocate `n` raw cells (a frame); returns the base index.
195    /// Frames hold continuation state — they are not terms and must
196    /// never be unified.
197    pub fn frame_alloc(&mut self, n: usize) -> usize {
198        let idx = self.heap.len();
199        self.heap.resize(idx + n, 0);
200        idx
201    }
202
203    /// Bind an unbound REF cell to a value, recording it on the trail.
204    pub fn bind(&mut self, ref_idx: usize, value: Word) {
205        debug_assert_eq!(
206            self.heap[ref_idx],
207            cell::make_ref(ref_idx),
208            "bind target must be unbound"
209        );
210        self.heap[ref_idx] = value;
211        self.trail.push(ref_idx as u64);
212    }
213
214    /// Follow REF chains to the representative word. Bound chains are
215    /// acyclic (we only ever bind unbound cells), so this terminates.
216    pub fn deref(&self, mut w: Word) -> Word {
217        while cell::tag_of(w) == cell::TAG_REF {
218            let idx = cell::payload(w) as usize;
219            let c = self.heap[idx];
220            if c == w {
221                return w; // unbound
222            }
223            w = c;
224        }
225        w
226    }
227
228    pub fn push_cp(&mut self, retry: ContFn, env: u64) {
229        self.cps.push(ChoicePoint {
230            trail_mark: self.trail.len(),
231            heap_mark: self.heap.len(),
232            retry,
233            env,
234            kind: CpKind::Normal,
235        });
236    }
237
238    /// Push a choice point whose backtrack restore-point is an EXPLICIT mark,
239    /// captured before the current alternative bound anything — rather than
240    /// the live heap/trail top. Lets a nondeterministic builtin bind a
241    /// solution and still record the pre-binding state for the next
242    /// alternative, without a rewind-then-rebind.
243    ///
244    /// **CALLER MUST GUARANTEE** `trail_mark <= self.trail.len()` and
245    /// `heap_mark <= self.heap.len()`. An out-of-range mark is caught only by
246    /// the debug assertion below; in release it silently no-ops the backtrack
247    /// truncation (`Vec::truncate(n)` with `n > len` does nothing), leaking
248    /// bindings from a supposedly-undone alternative into the next.
249    pub fn push_cp_at(&mut self, retry: ContFn, env: u64, trail_mark: usize, heap_mark: usize) {
250        debug_assert!(trail_mark <= self.trail.len() && heap_mark <= self.heap.len());
251        self.cps.push(ChoicePoint {
252            trail_mark,
253            heap_mark,
254            retry,
255            env,
256            kind: CpKind::Normal,
257        });
258    }
259
260    pub fn push_catch_cp(&mut self, retry: ContFn, env: u64) {
261        self.cps.push(ChoicePoint {
262            trail_mark: self.trail.len(),
263            heap_mark: self.heap.len(),
264            retry,
265            env,
266            kind: CpKind::Catch,
267        });
268    }
269
270    /// Cut: truncate the CP stack to `height`, but stop at a catch
271    /// frame (v1 rule: catch/3 is opaque to cut — `!` inside catch's
272    /// goal cannot prune the catch frame or anything below it).
273    pub fn cut_to(&mut self, height: usize) {
274        while self.cps.len() > height {
275            if self.cps.last().is_some_and(|cp| cp.kind == CpKind::Catch) {
276                break;
277            }
278            self.cps.pop();
279        }
280    }
281
282    /// Rewind bindings and heap to a popped choice point's marks.
283    pub fn rewind_to(&mut self, trail_mark: usize, heap_mark: usize) {
284        while self.trail.len() > trail_mark {
285            let idx = self.trail.pop().unwrap() as usize;
286            self.heap[idx] = cell::make_ref(idx);
287        }
288        self.heap.truncate(heap_mark);
289    }
290
291    /// Bump the step counter; on exceeding the limit set the uncatchable
292    /// resource error (v1: step limit cannot be trapped by catch/3).
293    /// v1 wording, byte-for-byte (solver.rs step_limit_thrown).
294    pub fn step(&mut self) -> bool {
295        self.steps += 1;
296        if self.steps > self.step_limit {
297            let context = format!("Maximum step limit exceeded ({})", self.step_limit);
298            crate::errors::resource(self, "steps", &context, true);
299            return false;
300        }
301        true
302    }
303
304    pub fn registry_lookup(&self, functor: u32, arity: u32) -> Option<ContFn> {
305        self.registry
306            .binary_search_by_key(&(functor, arity), |e| (e.functor, e.arity))
307            .ok()
308            .map(|i| self.registry[i].f)
309    }
310
311    /// Install the codegen-emitted source-location side-table (SPANS.md
312    /// Layer 3). Called once from `plg_rt_init`.
313    pub fn set_provenance(&mut self, srcmap: Vec<SrcLoc>, files: Vec<String>) {
314        self.srcmap = srcmap;
315        self.files = files;
316    }
317
318    /// Resolve a `site_id` to `(filename, line, col)`, or `None` for the
319    /// `NO_SITE` sentinel / a binary built without provenance. Owned so the
320    /// (cold) error path can mutate `self.error` without a borrow conflict.
321    pub fn site_location(&self, site_id: u32) -> Option<(String, u32, u32)> {
322        if site_id == NO_SITE {
323            return None;
324        }
325        let loc = self.srcmap.get(site_id as usize)?;
326        let file = self.files.get(loc.file as usize)?;
327        Some((file.clone(), loc.line, loc.col))
328    }
329}
330
331#[cfg(test)]
332mod tests {
333    use super::*;
334    use crate::cell::*;
335
336    fn machine() -> Box<Machine> {
337        Machine::new(StringInterner::new(), Vec::new())
338    }
339
340    #[test]
341    fn new_var_is_unbound_self_ref() {
342        let mut m = machine();
343        let v = m.new_var();
344        assert_eq!(tag_of(v), TAG_REF);
345        assert_eq!(m.deref(v), v);
346    }
347
348    #[test]
349    fn no_site_sentinel_value_is_pinned() {
350        // ABI contract with plg_compiler::codegen::NO_SITE (see its docs).
351        assert_eq!(NO_SITE, u32::MAX);
352    }
353
354    #[test]
355    fn bind_and_rewind() {
356        let mut m = machine();
357        let v = m.new_var();
358        let tmark = m.trail.len();
359        let hmark = m.heap.len();
360        m.bind(payload(v) as usize, make_atom(7));
361        assert_eq!(m.deref(v), make_atom(7));
362        m.rewind_to(tmark, hmark);
363        assert_eq!(m.deref(v), v, "binding undone");
364    }
365
366    #[test]
367    fn deref_follows_chains() {
368        let mut m = machine();
369        let a = m.new_var();
370        let b = m.new_var();
371        m.bind(payload(a) as usize, b);
372        m.bind(payload(b) as usize, make_int(-5));
373        assert_eq!(int_value(m.deref(a)), -5);
374    }
375
376    #[test]
377    fn step_limit_sets_uncatchable_error() {
378        let mut m = machine();
379        m.step_limit = 2;
380        assert!(m.step());
381        assert!(m.step());
382        assert!(!m.step());
383        assert!(m.error.as_ref().unwrap().uncatchable);
384    }
385}