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}