Skip to main content

plg_runtime/builtins/
termops.rs

1//! Term-introspection builtins: `functor/3`, `arg/3`, `=../2` (univ),
2//! `copy_term/2`.
3//!
4//! Ported byte-for-byte from patch-prolog v1 (`solver.rs` Functor/Arg/
5//! Univ/CopyTerm arms). Tag decisions verified against the oracle:
6//! - `functor(T, '.', 2)` and `T =.. ['.', a, b]` both build a **STR**
7//!   (functor ".", arity 2), NOT a list cell — v1 always constructs
8//!   `Term::Compound`.
9//! - decomposing a `TAG_LST` yields functor `.` / arity 2 and args
10//!   [Head, Tail] (univ produces `['.', H, T]`).
11//! - errors mirror v1's structured balls (see the captured oracle
12//!   strings in the unit tests).
13
14use crate::cell::*;
15use crate::machine::Machine;
16use crate::unify::unify;
17use plg_shared::atom::ATOM_DOT;
18
19#[inline]
20fn mref<'a>(m: *mut Machine) -> &'a mut Machine {
21    unsafe { &mut *m }
22}
23
24/// Build `[e0, e1, ...]` on the heap, nil-terminated; return its word.
25fn build_list(m: &mut Machine, elems: &[Word]) -> Word {
26    let mut tail = make_atom(plg_shared::atom::ATOM_NIL);
27    for &e in elems.iter().rev() {
28        let idx = m.heap.len();
29        m.heap.push(e);
30        m.heap.push(tail);
31        tail = make(TAG_LST, idx as u64);
32    }
33    tail
34}
35
36/// Collect a proper list's elements; `None` if not nil-terminated.
37fn collect_list(m: &Machine, w: Word) -> Option<Vec<Word>> {
38    let mut out = Vec::new();
39    let mut cur = m.deref(w);
40    loop {
41        match tag_of(cur) {
42            TAG_ATOM if atom_id(cur) == plg_shared::atom::ATOM_NIL => return Some(out),
43            TAG_LST => {
44                let idx = payload(cur) as usize;
45                out.push(m.heap[idx]);
46                cur = m.deref(m.heap[idx + 1]);
47            }
48            _ => return None,
49        }
50    }
51}
52
53/// `functor/3`: decompose or construct. 1 = success, 0 = failure/error.
54#[unsafe(no_mangle)]
55pub extern "C" fn plg_rt_b_functor_3(
56    m: *mut Machine,
57    term: u64,
58    name: u64,
59    arity: u64,
60    site_id: u32,
61) -> i32 {
62    let _site = crate::machine::ErrorSiteGuard::enter(m, site_id);
63    let m = mref(m);
64    let w = m.deref(term);
65    match tag_of(w) {
66        TAG_ATOM => {
67            let ok = unify(m, name, w) && unify(m, arity, make_int(0));
68            ok as i32
69        }
70        TAG_INT | TAG_BIG | TAG_FLT => {
71            let ok = unify(m, name, w) && unify(m, arity, make_int(0));
72            ok as i32
73        }
74        TAG_STR => {
75            let idx = payload(w) as usize;
76            let (f, n) = unpack_functor(m.heap[idx]);
77            let ok = unify(m, name, make_atom(f)) && unify(m, arity, make_int(n as i64));
78            ok as i32
79        }
80        TAG_LST => {
81            // Lists are ./2.
82            let ok = unify(m, name, make_atom(ATOM_DOT)) && unify(m, arity, make_int(2));
83            ok as i32
84        }
85        TAG_REF => functor_construct(m, term, name, arity),
86        _ => 0,
87    }
88}
89
90/// `functor/3` with an unbound first argument: build a term from
91/// name + arity (v1 semantics).
92fn functor_construct(m: &mut Machine, term: u64, name: u64, arity: u64) -> i32 {
93    let wname = m.deref(name);
94    let warity = m.deref(arity);
95    // Arity must be a bound integer to proceed.
96    let arity_val = match tag_of(warity) {
97        TAG_INT => int_value(warity),
98        TAG_BIG => m.heap[payload(warity) as usize] as i64,
99        _ => {
100            crate::errors::instantiation(m, "functor/3: insufficient arguments");
101            return 0;
102        }
103    };
104    // arity 0: the term is the name itself (atom/number).
105    if arity_val == 0 {
106        match tag_of(wname) {
107            TAG_ATOM | TAG_INT | TAG_BIG | TAG_FLT => return unify(m, term, wname) as i32,
108            _ => {
109                crate::errors::instantiation(m, "functor/3: insufficient arguments");
110                return 0;
111            }
112        }
113    }
114    if arity_val < 0 {
115        // The culprit is the negative arity itself (v1 oracle).
116        crate::errors::domain_error(
117            m,
118            "not_less_than_zero",
119            warity,
120            "functor/3: arity must be non-negative",
121        );
122        return 0;
123    }
124    if arity_val > 1024 {
125        // representation_error(max_arity)
126        let re = m.atoms.intern("representation_error");
127        let flag = make_atom(m.atoms.intern("max_arity"));
128        let idx = m.heap.len();
129        m.heap.push(pack_functor(re, 1));
130        m.heap.push(flag);
131        crate::errors::set_formal(
132            m,
133            make(TAG_STR, idx as u64),
134            "functor/3: arity too large (max 1024)",
135            false,
136        );
137        return 0;
138    }
139    // arity > 0: name must be an atom; build name(_,_,...).
140    if tag_of(wname) != TAG_ATOM {
141        crate::errors::instantiation(m, "functor/3: insufficient arguments");
142        return 0;
143    }
144    let f = atom_id(wname);
145    let n = arity_val as u32;
146    let base = m.heap.len();
147    m.heap.push(pack_functor(f, n));
148    // Each argument slot must be its OWN fresh variable. Do NOT use
149    // `m.new_var()` here: it pushes a self-ref cell *and* returns a ref to it,
150    // so pushing that ref would lay down two cells per slot and make every
151    // later arg alias the first variable (issue #31). Write a self-referencing
152    // REF directly into each contiguous arg cell instead.
153    for _ in 0..n {
154        let idx = m.heap.len();
155        m.heap.push(make_ref(idx));
156    }
157    let constructed = make(TAG_STR, base as u64);
158    unify(m, term, constructed) as i32
159}
160
161/// `arg/3`: the N-th argument of a compound. 1 = success, 0 = fail/error.
162#[unsafe(no_mangle)]
163pub extern "C" fn plg_rt_b_arg_3(
164    m: *mut Machine,
165    n: u64,
166    term: u64,
167    result: u64,
168    site_id: u32,
169) -> i32 {
170    let _site = crate::machine::ErrorSiteGuard::enter(m, site_id);
171    let m = mref(m);
172    let wn = m.deref(n);
173    let n_val = match tag_of(wn) {
174        TAG_INT => int_value(wn),
175        TAG_BIG => m.heap[payload(wn) as usize] as i64,
176        _ => {
177            crate::errors::type_error(m, "integer", wn, "arg/3: first argument must be integer");
178            return 0;
179        }
180    };
181    let wt = m.deref(term);
182    match tag_of(wt) {
183        TAG_STR => {
184            let idx = payload(wt) as usize;
185            let (_, arity) = unpack_functor(m.heap[idx]);
186            if n_val >= 1 && (n_val as u64) <= arity as u64 {
187                let arg = m.heap[idx + n_val as usize];
188                unify(m, result, arg) as i32
189            } else {
190                0 // out of range → fail
191            }
192        }
193        TAG_LST => {
194            let idx = payload(wt) as usize;
195            match n_val {
196                1 => unify(m, result, m.heap[idx]) as i32,
197                2 => unify(m, result, m.heap[idx + 1]) as i32,
198                _ => 0,
199            }
200        }
201        _ => {
202            crate::errors::type_error(m, "compound", wt, "arg/3: second argument must be compound");
203            0
204        }
205    }
206}
207
208/// `=../2` (univ): decompose into / build from a list. 1 = success.
209#[unsafe(no_mangle)]
210pub extern "C" fn plg_rt_b_univ_2(m: *mut Machine, term: u64, list: u64, site_id: u32) -> i32 {
211    let _site = crate::machine::ErrorSiteGuard::enter(m, site_id);
212    let m = mref(m);
213    let w = m.deref(term);
214    match tag_of(w) {
215        TAG_REF => univ_construct(m, term, list),
216        TAG_ATOM | TAG_INT | TAG_BIG | TAG_FLT => {
217            let lst = build_list(m, &[w]);
218            unify(m, list, lst) as i32
219        }
220        TAG_STR => {
221            let idx = payload(w) as usize;
222            let (f, n) = unpack_functor(m.heap[idx]);
223            let mut elems = Vec::with_capacity(n as usize + 1);
224            elems.push(make_atom(f));
225            for i in 0..n as usize {
226                elems.push(m.heap[idx + 1 + i]);
227            }
228            let lst = build_list(m, &elems);
229            unify(m, list, lst) as i32
230        }
231        TAG_LST => {
232            let idx = payload(w) as usize;
233            let head = m.heap[idx];
234            let tail = m.heap[idx + 1];
235            let lst = build_list(m, &[make_atom(ATOM_DOT), head, tail]);
236            unify(m, list, lst) as i32
237        }
238        _ => 0,
239    }
240}
241
242/// `=../2` with an unbound first argument: build a term from the list.
243fn univ_construct(m: &mut Machine, term: u64, list: u64) -> i32 {
244    let Some(elems) = collect_list(m, list) else {
245        let culprit = m.deref(list);
246        crate::errors::type_error(m, "list", culprit, "=../2: second argument must be a list");
247        return 0;
248    };
249    if elems.is_empty() {
250        let culprit = m.deref(list);
251        crate::errors::domain_error(
252            m,
253            "non_empty_list",
254            culprit,
255            "=../2: list must not be empty",
256        );
257        return 0;
258    }
259    let head = m.deref(elems[0]);
260    if elems.len() == 1 {
261        // Single element: term unifies with it directly (atom/number).
262        if tag_of(head) == TAG_REF {
263            crate::errors::instantiation(m, "=../2: instantiation error - element must be bound");
264            return 0;
265        }
266        return unify(m, term, head) as i32;
267    }
268    // arity > 0: the functor must be an atom; build a STR.
269    if tag_of(head) != TAG_ATOM {
270        crate::errors::type_error(
271            m,
272            "atom",
273            head,
274            "=../2: functor must be an atom when arity > 0",
275        );
276        return 0;
277    }
278    let f = atom_id(head);
279    let n = (elems.len() - 1) as u32;
280    let base = m.heap.len();
281    m.heap.push(pack_functor(f, n));
282    for &e in &elems[1..] {
283        m.heap.push(e);
284    }
285    let constructed = make(TAG_STR, base as u64);
286    unify(m, term, constructed) as i32
287}
288
289/// `copy_term/2`: a fresh copy of `orig` with consistent renamed vars.
290#[unsafe(no_mangle)]
291pub extern "C" fn plg_rt_b_copy_term_2(m: *mut Machine, orig: u64, copy: u64) -> i32 {
292    let m = mref(m);
293    let buf = crate::copyterm::copy_to_buf(m, orig);
294    let fresh = crate::copyterm::restore_from_buf(m, &buf);
295    unify(m, copy, fresh) as i32
296}
297
298#[cfg(test)]
299mod tests {
300    use super::*;
301    use crate::machine::NO_SITE;
302    use plg_shared::StringInterner;
303
304    fn machine() -> Box<Machine> {
305        Machine::new(StringInterner::new(), Vec::new())
306    }
307
308    // Thin wrappers so the existing tests need no site (they exercise
309    // behavior, not provenance).
310    fn functor3(m: *mut Machine, t: u64, n: u64, a: u64) -> i32 {
311        plg_rt_b_functor_3(m, t, n, a, NO_SITE)
312    }
313    fn arg3(m: *mut Machine, n: u64, t: u64, r: u64) -> i32 {
314        plg_rt_b_arg_3(m, n, t, r, NO_SITE)
315    }
316    fn univ2(m: *mut Machine, t: u64, l: u64) -> i32 {
317        plg_rt_b_univ_2(m, t, l, NO_SITE)
318    }
319
320    fn str_term(m: &mut Machine, name: &str, args: &[Word]) -> Word {
321        let f = m.atoms.intern(name);
322        let idx = m.heap.len();
323        m.heap.push(pack_functor(f, args.len() as u32));
324        m.heap.extend_from_slice(args);
325        make(TAG_STR, idx as u64)
326    }
327
328    fn lst(m: &mut Machine, head: Word, tail: Word) -> Word {
329        let idx = m.heap.len();
330        m.heap.push(head);
331        m.heap.push(tail);
332        make(TAG_LST, idx as u64)
333    }
334
335    fn msg(m: &Machine) -> &str {
336        m.error.as_ref().unwrap().message.as_str()
337    }
338
339    #[test]
340    fn functor_decompose() {
341        let mut m = machine();
342        let s = str_term(&mut m, "foo", &[make_int(1), make_int(2)]);
343        let name = m.new_var();
344        let ar = m.new_var();
345        let mp = &mut *m as *mut Machine;
346        assert_eq!(functor3(mp, s, name, ar), 1);
347        let foo = m.atoms.lookup("foo").unwrap();
348        assert_eq!(m.deref(name), make_atom(foo));
349        assert_eq!(int_value(m.deref(ar)), 2);
350
351        // atom: functor(a, N, A) → N=a, A=0
352        let a = make_atom(m.atoms.intern("a"));
353        let name = m.new_var();
354        let ar = m.new_var();
355        let mp = &mut *m as *mut Machine;
356        assert_eq!(functor3(mp, a, name, ar), 1);
357        assert_eq!(int_value(m.deref(ar)), 0);
358    }
359
360    #[test]
361    fn functor_list_is_dot_2() {
362        let mut m = machine();
363        let nil = make_atom(plg_shared::atom::ATOM_NIL);
364        let l = lst(&mut m, make_int(1), nil);
365        let name = m.new_var();
366        let ar = m.new_var();
367        let mp = &mut *m as *mut Machine;
368        assert_eq!(functor3(mp, l, name, ar), 1);
369        assert_eq!(m.deref(name), make_atom(ATOM_DOT));
370        assert_eq!(int_value(m.deref(ar)), 2);
371    }
372
373    #[test]
374    fn functor_construct_builds_str_even_for_dot() {
375        // functor(T, '.', 2) → STR './2', NOT a list cell (oracle-verified).
376        let mut m = machine();
377        let t = m.new_var();
378        let dot = make_atom(ATOM_DOT);
379        let mp = &mut *m as *mut Machine;
380        assert_eq!(functor3(mp, t, dot, make_int(2)), 1);
381        let w = m.deref(t);
382        assert_eq!(tag_of(w), TAG_STR);
383        let (f, n) = unpack_functor(m.heap[payload(w) as usize]);
384        assert_eq!((f, n), (ATOM_DOT, 2));
385
386        // functor(T, foo, 2) → foo(_,_)
387        let t = m.new_var();
388        let foo = make_atom(m.atoms.intern("foo"));
389        let mp = &mut *m as *mut Machine;
390        assert_eq!(functor3(mp, t, foo, make_int(2)), 1);
391        assert_eq!(tag_of(m.deref(t)), TAG_STR);
392    }
393
394    #[test]
395    fn functor_construct_uses_distinct_fresh_vars() {
396        // Regression for #31: functor(T, point, 2) must build point(A, B) with
397        // two DISTINCT vars, so T = point(3, 4) succeeds (it failed when both
398        // slots aliased one variable, reducing the unify to 3 = 4).
399        let mut m = machine();
400        let t = m.new_var();
401        let point = make_atom(m.atoms.intern("point"));
402        let mp = &mut *m as *mut Machine;
403        assert_eq!(functor3(mp, t, point, make_int(2)), 1);
404
405        let w = m.deref(t);
406        assert_eq!(tag_of(w), TAG_STR);
407        let base = payload(w) as usize;
408        // The two arg cells must be different unbound variables.
409        let a0 = m.deref(m.heap[base + 1]);
410        let a1 = m.deref(m.heap[base + 2]);
411        assert_eq!(tag_of(a0), TAG_REF);
412        assert_eq!(tag_of(a1), TAG_REF);
413        assert_ne!(a0, a1, "argument slots must be distinct variables");
414
415        // The behavioral check: T = point(3, 4) succeeds and binds each slot.
416        let concrete = str_term(&mut m, "point", &[make_int(3), make_int(4)]);
417        assert!(unify(&mut m, t, concrete));
418        let w = m.deref(t);
419        let base = payload(w) as usize;
420        assert_eq!(int_value(m.deref(m.heap[base + 1])), 3);
421        assert_eq!(int_value(m.deref(m.heap[base + 2])), 4);
422    }
423
424    #[test]
425    fn functor_errors() {
426        // all unbound → instantiation
427        let mut m = machine();
428        let t = m.new_var();
429        let n = m.new_var();
430        let a = m.new_var();
431        let mp = &mut *m as *mut Machine;
432        assert_eq!(functor3(mp, t, n, a), 0);
433        assert_eq!(
434            msg(&m),
435            "error(instantiation_error, functor/3: insufficient arguments)"
436        );
437
438        // negative arity → domain_error
439        let mut m = machine();
440        let t = m.new_var();
441        let foo = make_atom(m.atoms.intern("foo"));
442        let mp = &mut *m as *mut Machine;
443        assert_eq!(functor3(mp, t, foo, make_int(-1)), 0);
444        assert_eq!(
445            msg(&m),
446            "error(domain_error(not_less_than_zero, -1), functor/3: arity must be non-negative)"
447        );
448    }
449
450    #[test]
451    fn arg_in_and_out_of_range() {
452        let mut m = machine();
453        let s = str_term(&mut m, "foo", &[make_atom(7), make_atom(8)]);
454        let x = m.new_var();
455        let mp = &mut *m as *mut Machine;
456        assert_eq!(arg3(mp, make_int(1), s, x), 1);
457        assert_eq!(m.deref(x), make_atom(7));
458        // out of range → fail (no error)
459        let y = m.new_var();
460        let mp = &mut *m as *mut Machine;
461        assert_eq!(arg3(mp, make_int(3), s, y), 0);
462        assert!(m.error.is_none());
463        // arg 0 → fail
464        let mp = &mut *m as *mut Machine;
465        assert_eq!(arg3(mp, make_int(0), s, y), 0);
466        assert!(m.error.is_none());
467    }
468
469    #[test]
470    fn arg_non_compound_errors() {
471        let mut m = machine();
472        let x = m.new_var();
473        let a = make_atom(m.atoms.intern("a"));
474        let mp = &mut *m as *mut Machine;
475        assert_eq!(arg3(mp, make_int(1), a, x), 0);
476        assert_eq!(
477            msg(&m),
478            "error(type_error(compound, a), arg/3: second argument must be compound)"
479        );
480    }
481
482    #[test]
483    fn univ_decompose_and_construct() {
484        let mut m = machine();
485        let s = str_term(&mut m, "foo", &[make_atom(7), make_int(2)]);
486        let l = m.new_var();
487        let mp = &mut *m as *mut Machine;
488        assert_eq!(univ2(mp, s, l), 1);
489        let elems = collect_list(&m, l).unwrap();
490        let foo = m.atoms.lookup("foo").unwrap();
491        assert_eq!(m.deref(elems[0]), make_atom(foo));
492        assert_eq!(elems.len(), 3);
493
494        // construct: T =.. ['.', a, b] builds STR './2'
495        let a = make_atom(m.atoms.intern("a"));
496        let b = make_atom(m.atoms.intern("b"));
497        let dot = make_atom(ATOM_DOT);
498        let inlist = build_list(&mut m, &[dot, a, b]);
499        let t = m.new_var();
500        let mp = &mut *m as *mut Machine;
501        assert_eq!(univ2(mp, t, inlist), 1);
502        let w = m.deref(t);
503        assert_eq!(tag_of(w), TAG_STR);
504        let (f, n) = unpack_functor(m.heap[payload(w) as usize]);
505        assert_eq!((f, n), (ATOM_DOT, 2));
506    }
507
508    #[test]
509    fn univ_single_element_and_errors() {
510        // [42] → term 42
511        let mut m = machine();
512        let inlist = build_list(&mut m, &[make_int(42)]);
513        let t = m.new_var();
514        let mp = &mut *m as *mut Machine;
515        assert_eq!(univ2(mp, t, inlist), 1);
516        assert_eq!(int_value(m.deref(t)), 42);
517
518        // empty list → domain_error(non_empty_list)
519        let mut m = machine();
520        let nil = make_atom(plg_shared::atom::ATOM_NIL);
521        let t = m.new_var();
522        let mp = &mut *m as *mut Machine;
523        assert_eq!(univ2(mp, t, nil), 0);
524        assert_eq!(
525            msg(&m),
526            "error(domain_error(non_empty_list, []), =../2: list must not be empty)"
527        );
528    }
529
530    #[test]
531    fn copy_term_renames_and_shares() {
532        let mut m = machine();
533        let x = m.new_var();
534        // f(X, X)
535        let s = str_term(&mut m, "f", &[x, x]);
536        let c = m.new_var();
537        let mp = &mut *m as *mut Machine;
538        assert_eq!(plg_rt_b_copy_term_2(mp, s, c), 1);
539        let cw = m.deref(c);
540        assert_eq!(tag_of(cw), TAG_STR);
541        let idx = payload(cw) as usize;
542        let a0 = m.deref(m.heap[idx + 1]);
543        let a1 = m.deref(m.heap[idx + 2]);
544        assert_eq!(a0, a1, "shared var preserved");
545        assert_ne!(a0, m.deref(x), "fresh var distinct from original");
546    }
547}