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