Skip to main content

plg_runtime/builtins/
miscops.rs

1//! Miscellaneous deterministic builtins: `succ/2`, `plus/3`,
2//! `unify_with_occurs_check/2`, `write/1`, `writeq/1`, `writeln/1`, `nl/0`.
3//!
4//! Notes:
5//! - `succ/2` both modes; `succ(X, 0)` fails; negatives raise
6//!   `domain_error(not_less_than_zero, _)`; both-unbound raises
7//!   instantiation.
8//! - `plus/3` supports the three integer modes (any one argument
9//!   unbound); fewer than two bound raises instantiation.
10//! - `unify_with_occurs_check/2` uses a LOCAL occurs-checking unifier
11//!   (iterative; does not touch the shared `unify.rs`).
12//! - `write/1` / `writeln/1` render terms (infix operators, `[a, b]`
13//!   lists, whole-valued floats keep their `.0` so they read back as
14//!   floats), printed immediately. `write` adds no newline; `writeln`
15//!   and `nl` add one.
16
17use crate::cell::*;
18use crate::machine::Machine;
19use crate::render::{term_to_string, term_to_string_quoted};
20use crate::unify::unify;
21
22#[inline]
23fn mref<'a>(m: *mut Machine) -> &'a mut Machine {
24    unsafe { &mut *m }
25}
26
27/// Extract an i64 from an integer word (immediate or boxed).
28fn int_of(m: &Machine, w: Word) -> Option<i64> {
29    match tag_of(w) {
30        TAG_INT => Some(int_value(w)),
31        TAG_BIG => Some(m.heap[payload(w) as usize] as i64),
32        _ => None,
33    }
34}
35
36/// Materialize an i64 as a heap word (immediate INT or boxed BIG).
37fn int_word(m: &mut Machine, n: i64) -> Word {
38    if (INT_MIN..=INT_MAX).contains(&n) {
39        make_int(n)
40    } else {
41        let idx = m.heap.len();
42        m.heap.push(n as u64);
43        make(TAG_BIG, idx as u64)
44    }
45}
46
47/// `succ/2`: `S = X + 1` over non-negative integers, both modes.
48#[unsafe(no_mangle)]
49pub extern "C" fn plg_rt_b_succ_2(m: *mut Machine, x: u64, s: u64, site_id: u32) -> i32 {
50    let _site = crate::machine::ErrorSiteGuard::enter(m, site_id);
51    let m = mref(m);
52    let wx = m.deref(x);
53    let ws = m.deref(s);
54    let xi = int_of(m, wx);
55    let si = int_of(m, ws);
56    match (xi, si) {
57        (Some(xv), _) if xv >= 0 => match xv.checked_add(1) {
58            Some(r) => {
59                let rw = int_word(m, r);
60                unify(m, s, rw) as i32
61            }
62            None => {
63                crate::errors::evaluation(m, "int_overflow", "succ/2: integer overflow");
64                0
65            }
66        },
67        (_, Some(sv)) if sv > 0 => {
68            let rw = int_word(m, sv - 1);
69            unify(m, x, rw) as i32
70        }
71        (_, Some(0)) => 0, // succ(X, 0) fails
72        (Some(_), _) => {
73            // X is a negative integer.
74            crate::errors::domain_error(
75                m,
76                "not_less_than_zero",
77                wx,
78                "succ/2: argument must be non-negative",
79            );
80            0
81        }
82        (_, Some(_)) => {
83            // S is a negative integer.
84            crate::errors::domain_error(
85                m,
86                "not_less_than_zero",
87                ws,
88                "succ/2: successor must be non-negative",
89            );
90            0
91        }
92        _ => {
93            crate::errors::instantiation(m, "succ/2: at least one argument must be an integer");
94            0
95        }
96    }
97}
98
99/// `plus/3`: `Z = X + Y` over integers; any single unbound is solved for.
100#[unsafe(no_mangle)]
101pub extern "C" fn plg_rt_b_plus_3(m: *mut Machine, x: u64, y: u64, z: u64, site_id: u32) -> i32 {
102    let _site = crate::machine::ErrorSiteGuard::enter(m, site_id);
103    let m = mref(m);
104    let wx = int_of(m, m.deref(x));
105    let wy = int_of(m, m.deref(y));
106    let wz = int_of(m, m.deref(z));
107    let overflow = |m: &mut Machine| {
108        crate::errors::evaluation(m, "int_overflow", "plus/3: integer overflow");
109        0
110    };
111    match (wx, wy, wz) {
112        (Some(xv), Some(yv), _) => match xv.checked_add(yv) {
113            Some(r) => {
114                let rw = int_word(m, r);
115                unify(m, z, rw) as i32
116            }
117            None => overflow(m),
118        },
119        (Some(xv), _, Some(zv)) => match zv.checked_sub(xv) {
120            Some(r) => {
121                let rw = int_word(m, r);
122                unify(m, y, rw) as i32
123            }
124            None => overflow(m),
125        },
126        (_, Some(yv), Some(zv)) => match zv.checked_sub(yv) {
127            Some(r) => {
128                let rw = int_word(m, r);
129                unify(m, x, rw) as i32
130            }
131            None => overflow(m),
132        },
133        _ => {
134            crate::errors::instantiation(m, "plus/3: at least two arguments must be integers");
135            0
136        }
137    }
138}
139
140/// `unify_with_occurs_check/2`: like `=/2` but fails rather than build a
141/// cyclic term. Local iterative implementation — bindings are trailed so
142/// the caller's choice-point rewind undoes a partial unification.
143#[unsafe(no_mangle)]
144pub extern "C" fn plg_rt_b_unify_with_occurs_check_2(m: *mut Machine, a: u64, b: u64) -> i32 {
145    let m = mref(m);
146    unify_oc(m, a, b) as i32
147}
148
149/// Iterative occurs-checking unification over tagged heap words.
150fn unify_oc(m: &mut Machine, a: Word, b: Word) -> bool {
151    let mut work = vec![(a, b)];
152    while let Some((a, b)) = work.pop() {
153        let a = m.deref(a);
154        let b = m.deref(b);
155        if a == b {
156            continue;
157        }
158        match (tag_of(a), tag_of(b)) {
159            (TAG_REF, _) => {
160                if occurs(m, payload(a) as usize, b) {
161                    return false;
162                }
163                m.bind(payload(a) as usize, b);
164            }
165            (_, TAG_REF) => {
166                if occurs(m, payload(b) as usize, a) {
167                    return false;
168                }
169                m.bind(payload(b) as usize, a);
170            }
171            (TAG_ATOM, TAG_ATOM) | (TAG_INT, TAG_INT) => return false,
172            (TAG_BIG, TAG_BIG) => {
173                if m.heap[payload(a) as usize] as i64 != m.heap[payload(b) as usize] as i64 {
174                    return false;
175                }
176            }
177            (TAG_INT, TAG_BIG) => {
178                if int_value(a) != m.heap[payload(b) as usize] as i64 {
179                    return false;
180                }
181            }
182            (TAG_BIG, TAG_INT) => {
183                if m.heap[payload(a) as usize] as i64 != int_value(b) {
184                    return false;
185                }
186            }
187            (TAG_FLT, TAG_FLT) => {
188                if m.heap[payload(a) as usize] != m.heap[payload(b) as usize] {
189                    return false;
190                }
191            }
192            (TAG_STR, TAG_STR) => {
193                let ia = payload(a) as usize;
194                let ib = payload(b) as usize;
195                let (fa, na) = unpack_functor(m.heap[ia]);
196                let (fb, nb) = unpack_functor(m.heap[ib]);
197                if fa != fb || na != nb {
198                    return false;
199                }
200                for k in 0..na as usize {
201                    work.push((m.heap[ia + 1 + k], m.heap[ib + 1 + k]));
202                }
203            }
204            (TAG_LST, TAG_LST) => {
205                let ia = payload(a) as usize;
206                let ib = payload(b) as usize;
207                work.push((m.heap[ia + 1], m.heap[ib + 1]));
208                work.push((m.heap[ia], m.heap[ib]));
209            }
210            _ => return false,
211        }
212    }
213    true
214}
215
216/// Does the variable at heap index `var` occur within `term`? Iterative
217/// walk following bound refs; structures and lists are descended.
218fn occurs(m: &Machine, var: usize, term: Word) -> bool {
219    let mut work = vec![term];
220    while let Some(w) = work.pop() {
221        let w = m.deref(w);
222        match tag_of(w) {
223            TAG_REF if payload(w) as usize == var => return true,
224            TAG_REF => {}
225            TAG_STR => {
226                let idx = payload(w) as usize;
227                let (_, n) = unpack_functor(m.heap[idx]);
228                for k in 0..n as usize {
229                    work.push(m.heap[idx + 1 + k]);
230                }
231            }
232            TAG_LST => {
233                let idx = payload(w) as usize;
234                work.push(m.heap[idx]);
235                work.push(m.heap[idx + 1]);
236            }
237            _ => {}
238        }
239    }
240    false
241}
242
243/// `write/1`: print the term, no trailing newline.
244/// Output goes through the Machine's sink (stdout for CLI/WASI, a capture
245/// buffer for the reactor) so an isolate with no stdout loses nothing.
246#[unsafe(no_mangle)]
247pub extern "C" fn plg_rt_b_write_1(m: *mut Machine, term: u64) -> i32 {
248    let m = mref(m);
249    let s = term_to_string(m, term);
250    m.write_out(&s);
251    1
252}
253
254/// `writeq/1`: like `write/1` but quotes atoms that wouldn't read back
255/// unquoted (issue #33), e.g. `writeq('hello world')` → `'hello world'`.
256/// No trailing newline. Routed through the Machine's sink like `write/1`.
257#[unsafe(no_mangle)]
258pub extern "C" fn plg_rt_b_writeq_1(m: *mut Machine, term: u64) -> i32 {
259    let m = mref(m);
260    let s = term_to_string_quoted(m, term);
261    m.write_out(&s);
262    1
263}
264
265/// `writeln/1`: `write/1` followed by a newline.
266#[unsafe(no_mangle)]
267pub extern "C" fn plg_rt_b_writeln_1(m: *mut Machine, term: u64) -> i32 {
268    let m = mref(m);
269    let mut s = term_to_string(m, term);
270    s.push('\n');
271    m.write_out(&s);
272    1
273}
274
275/// `nl/0`: print a newline. Always succeeds.
276#[unsafe(no_mangle)]
277pub extern "C" fn plg_rt_b_nl_0(m: *mut Machine) -> i32 {
278    mref(m).write_out("\n");
279    1
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285    use crate::machine::NO_SITE;
286    use plg_shared::StringInterner;
287
288    fn machine() -> Box<Machine> {
289        Machine::new(StringInterner::new(), Vec::new())
290    }
291
292    // Thin wrappers: existing tests exercise behavior, not provenance.
293    fn succ(m: *mut Machine, x: u64, s: u64) -> i32 {
294        plg_rt_b_succ_2(m, x, s, NO_SITE)
295    }
296    fn plus(m: *mut Machine, x: u64, y: u64, z: u64) -> i32 {
297        plg_rt_b_plus_3(m, x, y, z, NO_SITE)
298    }
299
300    fn msg(m: &Machine) -> &str {
301        m.error.as_ref().unwrap().message.as_str()
302    }
303
304    fn str_term(m: &mut Machine, name: &str, args: &[Word]) -> Word {
305        let f = m.atoms.intern(name);
306        let idx = m.heap.len();
307        m.heap.push(pack_functor(f, args.len() as u32));
308        m.heap.extend_from_slice(args);
309        make(TAG_STR, idx as u64)
310    }
311
312    #[test]
313    fn succ_both_modes() {
314        let mut m = machine();
315        let x = m.new_var();
316        let mp = &mut *m as *mut Machine;
317        assert_eq!(succ(mp, make_int(3), x), 1);
318        assert_eq!(int_value(m.deref(x)), 4);
319
320        let y = m.new_var();
321        let mp = &mut *m as *mut Machine;
322        assert_eq!(succ(mp, y, make_int(5)), 1);
323        assert_eq!(int_value(m.deref(y)), 4);
324    }
325
326    #[test]
327    fn succ_zero_and_negative() {
328        let mut m = machine();
329        let y = m.new_var();
330        let mp = &mut *m as *mut Machine;
331        // succ(X, 0) fails (no predecessor)
332        assert_eq!(succ(mp, y, make_int(0)), 0);
333        assert!(m.error.is_none());
334
335        // succ(-1, X) → domain_error
336        let mut m = machine();
337        let x = m.new_var();
338        let mp = &mut *m as *mut Machine;
339        assert_eq!(succ(mp, make_int(-1), x), 0);
340        assert_eq!(
341            msg(&m),
342            "error(domain_error(not_less_than_zero, -1), succ/2: argument must be non-negative)"
343        );
344
345        // succ(X, Y) both unbound → instantiation
346        let mut m = machine();
347        let x = m.new_var();
348        let y = m.new_var();
349        let mp = &mut *m as *mut Machine;
350        assert_eq!(succ(mp, x, y), 0);
351        assert_eq!(
352            msg(&m),
353            "error(instantiation_error, succ/2: at least one argument must be an integer)"
354        );
355    }
356
357    #[test]
358    fn plus_three_modes_and_error() {
359        let mut m = machine();
360        let z = m.new_var();
361        let mp = &mut *m as *mut Machine;
362        assert_eq!(plus(mp, make_int(2), make_int(3), z), 1);
363        assert_eq!(int_value(m.deref(z)), 5);
364
365        let y = m.new_var();
366        let mp = &mut *m as *mut Machine;
367        assert_eq!(plus(mp, make_int(2), y, make_int(5)), 1);
368        assert_eq!(int_value(m.deref(y)), 3);
369
370        let x = m.new_var();
371        let mp = &mut *m as *mut Machine;
372        assert_eq!(plus(mp, x, make_int(3), make_int(5)), 1);
373        assert_eq!(int_value(m.deref(x)), 2);
374
375        // two unbound → instantiation
376        let mut m = machine();
377        let x = m.new_var();
378        let y = m.new_var();
379        let mp = &mut *m as *mut Machine;
380        assert_eq!(plus(mp, x, y, make_int(5)), 0);
381        assert_eq!(
382            msg(&m),
383            "error(instantiation_error, plus/3: at least two arguments must be integers)"
384        );
385    }
386
387    #[test]
388    fn occurs_check_rejects_cycle() {
389        let mut m = machine();
390        let x = m.new_var();
391        // unify_with_occurs_check(X, f(X)) must FAIL.
392        let fx = str_term(&mut m, "f", &[x]);
393        let mp = &mut *m as *mut Machine;
394        assert_eq!(plg_rt_b_unify_with_occurs_check_2(mp, x, fx), 0);
395        assert!(m.error.is_none());
396        // X still unbound (no binding committed).
397        assert_eq!(m.deref(x), x);
398    }
399
400    #[test]
401    fn occurs_check_allows_acyclic() {
402        let mut m = machine();
403        let x = m.new_var();
404        let a = make_atom(m.atoms.intern("a"));
405        let mp = &mut *m as *mut Machine;
406        assert_eq!(plg_rt_b_unify_with_occurs_check_2(mp, x, a), 1);
407        assert_eq!(m.deref(x), a);
408
409        // structural unify with shared subterm but no cycle
410        let mut m = machine();
411        let y = m.new_var();
412        let s1 = str_term(&mut m, "g", &[y, make_int(1)]);
413        let s2 = str_term(&mut m, "g", &[make_int(2), make_int(1)]);
414        let mp = &mut *m as *mut Machine;
415        assert_eq!(plg_rt_b_unify_with_occurs_check_2(mp, s1, s2), 1);
416        assert_eq!(int_value(m.deref(y)), 2);
417    }
418
419    #[test]
420    fn nl_always_succeeds() {
421        let mut m = machine();
422        let mp = &mut *m as *mut Machine;
423        assert_eq!(plg_rt_b_nl_0(mp), 1);
424    }
425
426    #[test]
427    fn write_returns_success() {
428        // We can't easily capture stdout here, but the call must succeed
429        // and not mutate the heap-visible term.
430        let mut m = machine();
431        let s = str_term(&mut m, "+", &[make_int(1), make_int(2)]);
432        let mp = &mut *m as *mut Machine;
433        assert_eq!(plg_rt_b_write_1(mp, s), 1);
434        assert_eq!(plg_rt_b_writeln_1(mp, s), 1);
435        // sanity: rendering matches infix form
436        assert_eq!(term_to_string(&m, s), "1 + 2");
437    }
438}