Skip to main content

plg_runtime/builtins/
pred.rs

1//! C-ABI surface for the M3 builtins: arithmetic (`is/2`, comparisons),
2//! term comparison (`==`, `@<`, `compare/3`), structural inequality
3//! (`\=/2`), cut, and a few codegen helpers. Mirrors the style of
4//! `abi.rs` (`#[unsafe(no_mangle)] pub extern "C" fn plg_rt_*`).
5
6use crate::builtins::arith::{self, ArithValue};
7use crate::builtins::order::compare_terms;
8use crate::cell::{self, INT_MAX, INT_MIN, Word};
9use crate::machine::Machine;
10use std::cmp::Ordering;
11
12#[inline]
13fn mref<'a>(m: *mut Machine) -> &'a mut Machine {
14    unsafe { &mut *m }
15}
16
17/// Materialize an evaluated value as a heap word.
18///
19/// Integers that fit the i61 immediate become an INT word. An integer that
20/// fits i64 but NOT i61 cannot be boxed until M4; for M3 we raise an error
21/// (see the deviation note in the milestone report). Floats are boxed via a
22/// FLT cell, like `plg_rt_put_float`.
23fn value_to_word(m: &mut Machine, v: ArithValue) -> Option<Word> {
24    match v {
25        ArithValue::Int(n) => {
26            if (INT_MIN..=INT_MAX).contains(&n) {
27                Some(cell::make_int(n))
28            } else {
29                // M4: full i64 range via a boxed BIG cell (v1 parity).
30                let idx = m.heap.len();
31                m.heap.push(n as u64);
32                Some(cell::make(cell::TAG_BIG, idx as u64))
33            }
34        }
35        ArithValue::Float(f) => {
36            let idx = m.heap.len();
37            m.heap.push(f.to_bits());
38            Some(cell::make(cell::TAG_FLT, idx as u64))
39        }
40    }
41}
42
43/// `is/2`: evaluate `expr`, unify `lhs` with the result. 1 = success,
44/// 0 = failure or error (error already set in `m.error`). `site_id` carries
45/// source provenance (SPANS.md Layer 3): set around the eval so any error
46/// constructor it reaches appends ` at file:line:col` via `set_formal`.
47#[unsafe(no_mangle)]
48pub extern "C" fn plg_rt_b_is(m: *mut Machine, lhs: u64, expr: u64, site_id: u32) -> i32 {
49    let _site = crate::machine::ErrorSiteGuard::enter(m, site_id);
50    let m = mref(m);
51    match arith::eval(m, expr) {
52        Err(()) => 0,
53        Ok(v) => match value_to_word(m, v) {
54            None => 0,
55            Some(w) => crate::unify::unify(m, lhs, w) as i32,
56        },
57    }
58}
59
60/// Arithmetic comparison. op: 0:'<' 1:'>' 2:'=<' 3:'>=' 4:'=:=' 5:'=\\='.
61/// 1 = holds, 0 = does not hold or error. `site_id`: see `plg_rt_b_is`.
62#[unsafe(no_mangle)]
63pub extern "C" fn plg_rt_b_arith_cmp(
64    m: *mut Machine,
65    op: i32,
66    a: u64,
67    b: u64,
68    site_id: u32,
69) -> i32 {
70    let _site = crate::machine::ErrorSiteGuard::enter(m, site_id);
71    let m = mref(m);
72    let av = match arith::eval(m, a) {
73        Ok(v) => v,
74        Err(()) => return 0,
75    };
76    let bv = match arith::eval(m, b) {
77        Ok(v) => v,
78        Err(()) => return 0,
79    };
80    let holds = match op {
81        0 => arith::arith_lt(av, bv),
82        1 => arith::arith_gt(av, bv),
83        2 => !arith::arith_gt(av, bv), // =< : not greater
84        3 => !arith::arith_lt(av, bv), // >= : not less
85        4 => arith::arith_eq(av, bv),  // =:=
86        5 => !arith::arith_eq(av, bv), // =\=
87        _ => return 0,
88    };
89    holds as i32
90}
91
92/// `\=/2`: succeed iff `a` and `b` do NOT unify. Always undoes any bindings
93/// made during the trial unification (rewinds the trail to the entry mark);
94/// the heap top is left untouched (no terms are constructed here).
95#[unsafe(no_mangle)]
96pub extern "C" fn plg_rt_b_neq(m: *mut Machine, a: u64, b: u64) -> i32 {
97    let m = mref(m);
98    let trail_mark = m.trail.len();
99    let unified = crate::unify::unify(m, a, b);
100    // Undo bindings unconditionally (do not truncate the heap).
101    while m.trail.len() > trail_mark {
102        let idx = m.trail.pop().unwrap() as usize;
103        m.heap[idx] = cell::make_ref(idx);
104    }
105    (!unified) as i32
106}
107
108/// Term comparison via standard order. op: 0:'==' 1:'\\==' 2:'@<' 3:'@>'
109/// 4:'@=<' 5:'@>='. 1 = holds, 0 = does not.
110#[unsafe(no_mangle)]
111pub extern "C" fn plg_rt_b_term_cmp(m: *mut Machine, op: i32, a: u64, b: u64) -> i32 {
112    let m = mref(m);
113    let ord = compare_terms(m, a, b);
114    let holds = match op {
115        0 => ord == Ordering::Equal,   // ==
116        1 => ord != Ordering::Equal,   // \==
117        2 => ord == Ordering::Less,    // @<
118        3 => ord == Ordering::Greater, // @>
119        4 => ord != Ordering::Greater, // @=<
120        5 => ord != Ordering::Less,    // @>=
121        _ => return 0,
122    };
123    holds as i32
124}
125
126/// `compare/3`: unify `order` with the atom '<', '=', or '>' according to
127/// the standard order of `a` and `b`. 1 = success, 0 = failure.
128#[unsafe(no_mangle)]
129pub extern "C" fn plg_rt_b_compare(m: *mut Machine, order: u64, a: u64, b: u64) -> i32 {
130    let m = mref(m);
131    let name = match compare_terms(m, a, b) {
132        Ordering::Less => "<",
133        Ordering::Equal => "=",
134        Ordering::Greater => ">",
135    };
136    let id = m.atoms.intern(name);
137    crate::unify::unify(m, order, cell::make_atom(id)) as i32
138}
139
140/// Cut: truncate the choice-point stack to `height`.
141///
142/// M4 will make this catch-frame aware (a cut must not discard a CATCH
143/// barrier installed by `catch/3`); for M3 it is a plain truncate, matching
144/// the absence of exception frames in the current runtime.
145#[unsafe(no_mangle)]
146pub extern "C" fn plg_rt_cut(m: *mut Machine, height: u64) {
147    // Stops at catch frames (v1 rule: catch/3 is opaque to cut).
148    mref(m).cut_to(height as usize);
149}
150
151/// Current choice-point stack height (the cut barrier to capture on entry).
152#[unsafe(no_mangle)]
153pub extern "C" fn plg_rt_cp_top(m: *mut Machine) -> u64 {
154    mref(m).cps.len() as u64
155}
156
157/// Dereference a word through REF chains (exposes `Machine::deref`).
158#[unsafe(no_mangle)]
159pub extern "C" fn plg_rt_deref(m: *mut Machine, w: u64) -> u64 {
160    mref(m).deref(w)
161}
162
163/// First-argument indexing key for a DEREFED word: for an STR, the packed
164/// functor cell (`heap[idx]`, i.e. `pack_functor(functor, arity)`); for
165/// anything else, `u64::MAX`. Codegen switches on this to pick a clause set.
166#[unsafe(no_mangle)]
167pub extern "C" fn plg_rt_str_key(m: *mut Machine, w: u64) -> u64 {
168    let m = mref(m);
169    if cell::tag_of(w) == cell::TAG_STR {
170        m.heap[cell::payload(w) as usize]
171    } else {
172        u64::MAX
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179    use crate::cell::*;
180    use plg_shared::StringInterner;
181
182    fn machine() -> Box<Machine> {
183        Machine::new(StringInterner::new(), Vec::new())
184    }
185
186    // These exercise arithmetic, not provenance; call with no site.
187    fn is_(m: *mut Machine, lhs: u64, expr: u64) -> i32 {
188        plg_rt_b_is(m, lhs, expr, crate::machine::NO_SITE)
189    }
190    fn cmp_(m: *mut Machine, op: i32, a: u64, b: u64) -> i32 {
191        plg_rt_b_arith_cmp(m, op, a, b, crate::machine::NO_SITE)
192    }
193
194    fn bin_str(m: &mut Machine, op: &str, a: Word, b: Word) -> Word {
195        let f = m.atoms.intern(op);
196        let idx = m.heap.len();
197        m.heap.push(pack_functor(f, 2));
198        m.heap.push(a);
199        m.heap.push(b);
200        make(TAG_STR, idx as u64)
201    }
202
203    #[test]
204    fn b_is_unifies_bound_lhs() {
205        let mut m = machine();
206        let mp = &mut *m as *mut Machine;
207        // 5 is 2+3 → success
208        let e = bin_str(&mut m, "+", make_int(2), make_int(3));
209        assert_eq!(is_(mp, make_int(5), e), 1);
210        // 6 is 2+3 → fail
211        let e = bin_str(&mut m, "+", make_int(2), make_int(3));
212        assert_eq!(is_(mp, make_int(6), e), 0);
213    }
214
215    #[test]
216    fn b_is_binds_unbound_lhs() {
217        let mut m = machine();
218        let v = m.new_var();
219        let e = bin_str(&mut m, "*", make_int(4), make_int(5));
220        let mp = &mut *m as *mut Machine;
221        assert_eq!(is_(mp, v, e), 1);
222        assert_eq!(int_value(m.deref(v)), 20);
223    }
224
225    #[test]
226    fn b_is_boxes_float() {
227        let mut m = machine();
228        let v = m.new_var();
229        // 2.0 yields a float via /(1.0)... use 1/2 = 0.5 (always float).
230        let e = bin_str(&mut m, "/", make_int(1), make_int(2));
231        let mp = &mut *m as *mut Machine;
232        assert_eq!(is_(mp, v, e), 1);
233        let d = m.deref(v);
234        assert_eq!(tag_of(d), TAG_FLT);
235        assert_eq!(f64::from_bits(m.heap[payload(d) as usize]), 0.5);
236    }
237
238    #[test]
239    fn b_is_boxes_big_integers() {
240        // M4: results beyond the i61 immediate box to a BIG cell — the
241        // full i64 range works, matching v1.
242        let mut m = machine();
243        let v = m.new_var();
244        let big = INT_MAX; // 2^60 - 1, the largest immediate
245        let e = bin_str(&mut m, "+", make_int(big), make_int(big));
246        let mp = &mut *m as *mut Machine;
247        assert_eq!(is_(mp, v, e), 1);
248        assert!(m.error.is_none());
249        let d = m.deref(v);
250        assert_eq!(tag_of(d), TAG_BIG);
251        assert_eq!(m.heap[payload(d) as usize] as i64, 2 * big);
252    }
253
254    #[test]
255    fn arith_cmp_ops() {
256        let mut m = machine();
257        let mp = &mut *m as *mut Machine;
258        assert_eq!(cmp_(mp, 0, make_int(1), make_int(2)), 1); // <
259        assert_eq!(cmp_(mp, 0, make_int(2), make_int(1)), 0);
260        assert_eq!(cmp_(mp, 1, make_int(2), make_int(1)), 1); // >
261        assert_eq!(cmp_(mp, 2, make_int(1), make_int(1)), 1); // =<
262        assert_eq!(cmp_(mp, 3, make_int(1), make_int(1)), 1); // >=
263        assert_eq!(cmp_(mp, 4, make_int(3), make_int(3)), 1); // =:=
264        assert_eq!(cmp_(mp, 5, make_int(3), make_int(4)), 1); // =\=
265    }
266
267    #[test]
268    fn arith_cmp_mixed_int_float() {
269        // 1 =:= 1.0 holds; 1.0 < 1 does not.
270        let mut m = machine();
271        let one_f = {
272            let idx = m.heap.len();
273            m.heap.push(1.0f64.to_bits());
274            make(TAG_FLT, idx as u64)
275        };
276        let mp = &mut *m as *mut Machine;
277        assert_eq!(cmp_(mp, 4, make_int(1), one_f), 1);
278        assert_eq!(cmp_(mp, 0, one_f, make_int(1)), 0);
279    }
280
281    #[test]
282    fn arith_cmp_propagates_error() {
283        let mut m = machine();
284        let v = m.new_var();
285        let mp = &mut *m as *mut Machine;
286        assert_eq!(cmp_(mp, 0, make_int(1), v), 0);
287        assert!(m.error.is_some());
288    }
289
290    #[test]
291    fn b_neq_undoes_bindings() {
292        let mut m = machine();
293        let x = m.new_var();
294        let t0 = m.trail.len();
295        let h0 = m.heap.len();
296        let mp = &mut *m as *mut Machine;
297        // X \= a : X and a unify, so \= fails (0), and X is left unbound.
298        assert_eq!(plg_rt_b_neq(mp, x, make_atom(7)), 0);
299        assert_eq!(m.deref(x), x, "binding undone");
300        assert_eq!(m.trail.len(), t0, "trail rewound");
301        assert_eq!(m.heap.len(), h0, "heap top untouched");
302        // a \= b : distinct atoms do not unify → succeeds (1).
303        assert_eq!(plg_rt_b_neq(mp, make_atom(7), make_atom(8)), 1);
304    }
305
306    #[test]
307    fn term_cmp_ops() {
308        let mut m = machine();
309        let a = m.atoms.intern("a");
310        let b = m.atoms.intern("b");
311        let mp = &mut *m as *mut Machine;
312        assert_eq!(plg_rt_b_term_cmp(mp, 0, make_atom(a), make_atom(a)), 1); // ==
313        assert_eq!(plg_rt_b_term_cmp(mp, 1, make_atom(a), make_atom(b)), 1); // \==
314        assert_eq!(plg_rt_b_term_cmp(mp, 2, make_atom(a), make_atom(b)), 1); // @<
315        assert_eq!(plg_rt_b_term_cmp(mp, 3, make_atom(b), make_atom(a)), 1); // @>
316        assert_eq!(plg_rt_b_term_cmp(mp, 4, make_atom(a), make_atom(a)), 1); // @=<
317        assert_eq!(plg_rt_b_term_cmp(mp, 5, make_atom(a), make_atom(a)), 1); // @>=
318    }
319
320    #[test]
321    fn compare3_binds_order_atom() {
322        let mut m = machine();
323        let o = m.new_var();
324        let mp = &mut *m as *mut Machine;
325        assert_eq!(plg_rt_b_compare(mp, o, make_int(1), make_int(2)), 1);
326        let lt = m.atoms.lookup("<").unwrap();
327        assert_eq!(m.deref(o), make_atom(lt));
328
329        let o2 = m.new_var();
330        let mp = &mut *m as *mut Machine;
331        assert_eq!(plg_rt_b_compare(mp, o2, make_int(5), make_int(5)), 1);
332        let eq = m.atoms.lookup("=").unwrap();
333        assert_eq!(m.deref(o2), make_atom(eq));
334    }
335
336    #[test]
337    fn cut_truncates_cps() {
338        let mut m = machine();
339        // push three dummy choice points
340        unsafe extern "C" fn dummy(_m: *mut Machine, _e: u64) -> i32 {
341            0
342        }
343        for _ in 0..3 {
344            m.push_cp(dummy, 0);
345        }
346        let mp = &mut *m as *mut Machine;
347        assert_eq!(plg_rt_cp_top(mp), 3);
348        plg_rt_cut(mp, 1);
349        assert_eq!(plg_rt_cp_top(mp), 1);
350    }
351
352    #[test]
353    fn deref_and_str_key() {
354        let mut m = machine();
355        let f = m.atoms.intern("foo");
356        let idx = m.heap.len();
357        m.heap.push(pack_functor(f, 2));
358        m.heap.push(make_int(1));
359        m.heap.push(make_int(2));
360        let s = make(TAG_STR, idx as u64);
361        let mp = &mut *m as *mut Machine;
362        assert_eq!(plg_rt_str_key(mp, s), pack_functor(f, 2));
363        assert_eq!(plg_rt_str_key(mp, make_int(9)), u64::MAX);
364        assert_eq!(plg_rt_str_key(mp, make_atom(f)), u64::MAX);
365
366        // deref follows a bound var chain
367        let mut m = machine();
368        let v = m.new_var();
369        m.bind(payload(v) as usize, make_int(42));
370        let mp = &mut *m as *mut Machine;
371        assert_eq!(int_value(plg_rt_deref(mp, v)), 42);
372    }
373}