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    for _ in 0..n {
149        let v = m.new_var();
150        m.heap.push(v);
151    }
152    let constructed = make(TAG_STR, base as u64);
153    unify(m, term, constructed) as i32
154}
155
156/// `arg/3`: the N-th argument of a compound. 1 = success, 0 = fail/error.
157#[unsafe(no_mangle)]
158pub extern "C" fn plg_rt_b_arg_3(
159    m: *mut Machine,
160    n: u64,
161    term: u64,
162    result: u64,
163    site_id: u32,
164) -> i32 {
165    let _site = crate::machine::ErrorSiteGuard::enter(m, site_id);
166    let m = mref(m);
167    let wn = m.deref(n);
168    let n_val = match tag_of(wn) {
169        TAG_INT => int_value(wn),
170        TAG_BIG => m.heap[payload(wn) as usize] as i64,
171        _ => {
172            crate::errors::type_error(m, "integer", wn, "arg/3: first argument must be integer");
173            return 0;
174        }
175    };
176    let wt = m.deref(term);
177    match tag_of(wt) {
178        TAG_STR => {
179            let idx = payload(wt) as usize;
180            let (_, arity) = unpack_functor(m.heap[idx]);
181            if n_val >= 1 && (n_val as u64) <= arity as u64 {
182                let arg = m.heap[idx + n_val as usize];
183                unify(m, result, arg) as i32
184            } else {
185                0 // out of range → fail
186            }
187        }
188        TAG_LST => {
189            let idx = payload(wt) as usize;
190            match n_val {
191                1 => unify(m, result, m.heap[idx]) as i32,
192                2 => unify(m, result, m.heap[idx + 1]) as i32,
193                _ => 0,
194            }
195        }
196        _ => {
197            crate::errors::type_error(m, "compound", wt, "arg/3: second argument must be compound");
198            0
199        }
200    }
201}
202
203/// `=../2` (univ): decompose into / build from a list. 1 = success.
204#[unsafe(no_mangle)]
205pub extern "C" fn plg_rt_b_univ_2(m: *mut Machine, term: u64, list: u64, site_id: u32) -> i32 {
206    let _site = crate::machine::ErrorSiteGuard::enter(m, site_id);
207    let m = mref(m);
208    let w = m.deref(term);
209    match tag_of(w) {
210        TAG_REF => univ_construct(m, term, list),
211        TAG_ATOM | TAG_INT | TAG_BIG | TAG_FLT => {
212            let lst = build_list(m, &[w]);
213            unify(m, list, lst) as i32
214        }
215        TAG_STR => {
216            let idx = payload(w) as usize;
217            let (f, n) = unpack_functor(m.heap[idx]);
218            let mut elems = Vec::with_capacity(n as usize + 1);
219            elems.push(make_atom(f));
220            for i in 0..n as usize {
221                elems.push(m.heap[idx + 1 + i]);
222            }
223            let lst = build_list(m, &elems);
224            unify(m, list, lst) as i32
225        }
226        TAG_LST => {
227            let idx = payload(w) as usize;
228            let head = m.heap[idx];
229            let tail = m.heap[idx + 1];
230            let lst = build_list(m, &[make_atom(ATOM_DOT), head, tail]);
231            unify(m, list, lst) as i32
232        }
233        _ => 0,
234    }
235}
236
237/// `=../2` with an unbound first argument: build a term from the list.
238fn univ_construct(m: &mut Machine, term: u64, list: u64) -> i32 {
239    let Some(elems) = collect_list(m, list) else {
240        let culprit = m.deref(list);
241        crate::errors::type_error(m, "list", culprit, "=../2: second argument must be a list");
242        return 0;
243    };
244    if elems.is_empty() {
245        let culprit = m.deref(list);
246        crate::errors::domain_error(
247            m,
248            "non_empty_list",
249            culprit,
250            "=../2: list must not be empty",
251        );
252        return 0;
253    }
254    let head = m.deref(elems[0]);
255    if elems.len() == 1 {
256        // Single element: term unifies with it directly (atom/number).
257        if tag_of(head) == TAG_REF {
258            crate::errors::instantiation(m, "=../2: instantiation error - element must be bound");
259            return 0;
260        }
261        return unify(m, term, head) as i32;
262    }
263    // arity > 0: the functor must be an atom; build a STR.
264    if tag_of(head) != TAG_ATOM {
265        crate::errors::type_error(
266            m,
267            "atom",
268            head,
269            "=../2: functor must be an atom when arity > 0",
270        );
271        return 0;
272    }
273    let f = atom_id(head);
274    let n = (elems.len() - 1) as u32;
275    let base = m.heap.len();
276    m.heap.push(pack_functor(f, n));
277    for &e in &elems[1..] {
278        m.heap.push(e);
279    }
280    let constructed = make(TAG_STR, base as u64);
281    unify(m, term, constructed) as i32
282}
283
284/// `copy_term/2`: a fresh copy of `orig` with consistent renamed vars.
285#[unsafe(no_mangle)]
286pub extern "C" fn plg_rt_b_copy_term_2(m: *mut Machine, orig: u64, copy: u64) -> i32 {
287    let m = mref(m);
288    let buf = crate::copyterm::copy_to_buf(m, orig);
289    let fresh = crate::copyterm::restore_from_buf(m, &buf);
290    unify(m, copy, fresh) as i32
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296    use crate::machine::NO_SITE;
297    use plg_shared::StringInterner;
298
299    fn machine() -> Box<Machine> {
300        Machine::new(StringInterner::new(), Vec::new())
301    }
302
303    // Thin wrappers so the existing tests need no site (they exercise
304    // behavior, not provenance).
305    fn functor3(m: *mut Machine, t: u64, n: u64, a: u64) -> i32 {
306        plg_rt_b_functor_3(m, t, n, a, NO_SITE)
307    }
308    fn arg3(m: *mut Machine, n: u64, t: u64, r: u64) -> i32 {
309        plg_rt_b_arg_3(m, n, t, r, NO_SITE)
310    }
311    fn univ2(m: *mut Machine, t: u64, l: u64) -> i32 {
312        plg_rt_b_univ_2(m, t, l, NO_SITE)
313    }
314
315    fn str_term(m: &mut Machine, name: &str, args: &[Word]) -> Word {
316        let f = m.atoms.intern(name);
317        let idx = m.heap.len();
318        m.heap.push(pack_functor(f, args.len() as u32));
319        m.heap.extend_from_slice(args);
320        make(TAG_STR, idx as u64)
321    }
322
323    fn lst(m: &mut Machine, head: Word, tail: Word) -> Word {
324        let idx = m.heap.len();
325        m.heap.push(head);
326        m.heap.push(tail);
327        make(TAG_LST, idx as u64)
328    }
329
330    fn msg(m: &Machine) -> &str {
331        m.error.as_ref().unwrap().message.as_str()
332    }
333
334    #[test]
335    fn functor_decompose() {
336        let mut m = machine();
337        let s = str_term(&mut m, "foo", &[make_int(1), make_int(2)]);
338        let name = m.new_var();
339        let ar = m.new_var();
340        let mp = &mut *m as *mut Machine;
341        assert_eq!(functor3(mp, s, name, ar), 1);
342        let foo = m.atoms.lookup("foo").unwrap();
343        assert_eq!(m.deref(name), make_atom(foo));
344        assert_eq!(int_value(m.deref(ar)), 2);
345
346        // atom: functor(a, N, A) → N=a, A=0
347        let a = make_atom(m.atoms.intern("a"));
348        let name = m.new_var();
349        let ar = m.new_var();
350        let mp = &mut *m as *mut Machine;
351        assert_eq!(functor3(mp, a, name, ar), 1);
352        assert_eq!(int_value(m.deref(ar)), 0);
353    }
354
355    #[test]
356    fn functor_list_is_dot_2() {
357        let mut m = machine();
358        let nil = make_atom(plg_shared::atom::ATOM_NIL);
359        let l = lst(&mut m, make_int(1), nil);
360        let name = m.new_var();
361        let ar = m.new_var();
362        let mp = &mut *m as *mut Machine;
363        assert_eq!(functor3(mp, l, name, ar), 1);
364        assert_eq!(m.deref(name), make_atom(ATOM_DOT));
365        assert_eq!(int_value(m.deref(ar)), 2);
366    }
367
368    #[test]
369    fn functor_construct_builds_str_even_for_dot() {
370        // functor(T, '.', 2) → STR './2', NOT a list cell (oracle-verified).
371        let mut m = machine();
372        let t = m.new_var();
373        let dot = make_atom(ATOM_DOT);
374        let mp = &mut *m as *mut Machine;
375        assert_eq!(functor3(mp, t, dot, make_int(2)), 1);
376        let w = m.deref(t);
377        assert_eq!(tag_of(w), TAG_STR);
378        let (f, n) = unpack_functor(m.heap[payload(w) as usize]);
379        assert_eq!((f, n), (ATOM_DOT, 2));
380
381        // functor(T, foo, 2) → foo(_,_)
382        let t = m.new_var();
383        let foo = make_atom(m.atoms.intern("foo"));
384        let mp = &mut *m as *mut Machine;
385        assert_eq!(functor3(mp, t, foo, make_int(2)), 1);
386        assert_eq!(tag_of(m.deref(t)), TAG_STR);
387    }
388
389    #[test]
390    fn functor_errors() {
391        // all unbound → instantiation
392        let mut m = machine();
393        let t = m.new_var();
394        let n = m.new_var();
395        let a = m.new_var();
396        let mp = &mut *m as *mut Machine;
397        assert_eq!(functor3(mp, t, n, a), 0);
398        assert_eq!(
399            msg(&m),
400            "error(instantiation_error, functor/3: insufficient arguments)"
401        );
402
403        // negative arity → domain_error
404        let mut m = machine();
405        let t = m.new_var();
406        let foo = make_atom(m.atoms.intern("foo"));
407        let mp = &mut *m as *mut Machine;
408        assert_eq!(functor3(mp, t, foo, make_int(-1)), 0);
409        assert_eq!(
410            msg(&m),
411            "error(domain_error(not_less_than_zero, -1), functor/3: arity must be non-negative)"
412        );
413    }
414
415    #[test]
416    fn arg_in_and_out_of_range() {
417        let mut m = machine();
418        let s = str_term(&mut m, "foo", &[make_atom(7), make_atom(8)]);
419        let x = m.new_var();
420        let mp = &mut *m as *mut Machine;
421        assert_eq!(arg3(mp, make_int(1), s, x), 1);
422        assert_eq!(m.deref(x), make_atom(7));
423        // out of range → fail (no error)
424        let y = m.new_var();
425        let mp = &mut *m as *mut Machine;
426        assert_eq!(arg3(mp, make_int(3), s, y), 0);
427        assert!(m.error.is_none());
428        // arg 0 → fail
429        let mp = &mut *m as *mut Machine;
430        assert_eq!(arg3(mp, make_int(0), s, y), 0);
431        assert!(m.error.is_none());
432    }
433
434    #[test]
435    fn arg_non_compound_errors() {
436        let mut m = machine();
437        let x = m.new_var();
438        let a = make_atom(m.atoms.intern("a"));
439        let mp = &mut *m as *mut Machine;
440        assert_eq!(arg3(mp, make_int(1), a, x), 0);
441        assert_eq!(
442            msg(&m),
443            "error(type_error(compound, a), arg/3: second argument must be compound)"
444        );
445    }
446
447    #[test]
448    fn univ_decompose_and_construct() {
449        let mut m = machine();
450        let s = str_term(&mut m, "foo", &[make_atom(7), make_int(2)]);
451        let l = m.new_var();
452        let mp = &mut *m as *mut Machine;
453        assert_eq!(univ2(mp, s, l), 1);
454        let elems = collect_list(&m, l).unwrap();
455        let foo = m.atoms.lookup("foo").unwrap();
456        assert_eq!(m.deref(elems[0]), make_atom(foo));
457        assert_eq!(elems.len(), 3);
458
459        // construct: T =.. ['.', a, b] builds STR './2'
460        let a = make_atom(m.atoms.intern("a"));
461        let b = make_atom(m.atoms.intern("b"));
462        let dot = make_atom(ATOM_DOT);
463        let inlist = build_list(&mut m, &[dot, a, b]);
464        let t = m.new_var();
465        let mp = &mut *m as *mut Machine;
466        assert_eq!(univ2(mp, t, inlist), 1);
467        let w = m.deref(t);
468        assert_eq!(tag_of(w), TAG_STR);
469        let (f, n) = unpack_functor(m.heap[payload(w) as usize]);
470        assert_eq!((f, n), (ATOM_DOT, 2));
471    }
472
473    #[test]
474    fn univ_single_element_and_errors() {
475        // [42] → term 42
476        let mut m = machine();
477        let inlist = build_list(&mut m, &[make_int(42)]);
478        let t = m.new_var();
479        let mp = &mut *m as *mut Machine;
480        assert_eq!(univ2(mp, t, inlist), 1);
481        assert_eq!(int_value(m.deref(t)), 42);
482
483        // empty list → domain_error(non_empty_list)
484        let mut m = machine();
485        let nil = make_atom(plg_shared::atom::ATOM_NIL);
486        let t = m.new_var();
487        let mp = &mut *m as *mut Machine;
488        assert_eq!(univ2(mp, t, nil), 0);
489        assert_eq!(
490            msg(&m),
491            "error(domain_error(non_empty_list, []), =../2: list must not be empty)"
492        );
493    }
494
495    #[test]
496    fn copy_term_renames_and_shares() {
497        let mut m = machine();
498        let x = m.new_var();
499        // f(X, X)
500        let s = str_term(&mut m, "f", &[x, x]);
501        let c = m.new_var();
502        let mp = &mut *m as *mut Machine;
503        assert_eq!(plg_rt_b_copy_term_2(mp, s, c), 1);
504        let cw = m.deref(c);
505        assert_eq!(tag_of(cw), TAG_STR);
506        let idx = payload(cw) as usize;
507        let a0 = m.deref(m.heap[idx + 1]);
508        let a1 = m.deref(m.heap[idx + 2]);
509        assert_eq!(a0, a1, "shared var preserved");
510        assert_ne!(a0, m.deref(x), "fresh var distinct from original");
511    }
512}