Skip to main content

plg_runtime/builtins/
order.rs

1//! Standard order of terms (ISO 8.4.2), ported from patch-prolog v1's
2//! `term_compare`.
3//!
4//! Order:   Var < Number < Atom < Compound
5//!   - vars among themselves by heap-cell index (v1: VarId)
6//!   - numbers by value; Float < Integer when numerically equal; NaN last
7//!   - atoms alphabetically by name
8//!   - compounds by arity, then functor name, then args left-to-right
9//!
10//! A `LST` cell IS the compound `'.'(Head, Tail)` — arity 2, functor ".".
11//! We treat it as such so a `LST` and a `STR` with functor "." / arity 2
12//! compare structurally equal, exactly as v1 (which has a distinct
13//! `Term::List` but compares it against `Compound('.', [h, t])` as equal —
14//! see the List-vs-Compound arms in v1 `term_compare`).
15
16use crate::cell::*;
17use crate::machine::Machine;
18use plg_shared::atom::ATOM_DOT;
19use std::cmp::Ordering;
20
21/// Top-level type rank: Var=0, Number=1, Atom=2, Compound(incl. list)=3.
22fn type_rank(w: Word) -> u8 {
23    match tag_of(w) {
24        TAG_REF => 0,
25        TAG_INT | TAG_FLT | TAG_BIG => 1,
26        TAG_ATOM => 2,
27        TAG_STR | TAG_LST => 3,
28        _ => unreachable!("bad tag in term order"),
29    }
30}
31
32/// A number word normalized for comparison: INT and boxed BIG are both
33/// integers; FLT is a float.
34enum Num {
35    I(i64),
36    F(f64),
37}
38
39fn num_of(m: &Machine, w: Word) -> Num {
40    match tag_of(w) {
41        TAG_INT => Num::I(int_value(w)),
42        TAG_BIG => Num::I(m.heap[payload(w) as usize] as i64),
43        TAG_FLT => Num::F(f64::from_bits(m.heap[payload(w) as usize])),
44        _ => unreachable!(),
45    }
46}
47
48/// Compare two numbers. Float < Integer at numeric equality; NaN sorts
49/// after every other float (v1 rule). Cross-type comparison goes through
50/// f64 like v1 (documented precision caveat for huge i64).
51fn compare_numbers(m: &Machine, a: Word, b: Word) -> Ordering {
52    match (num_of(m, a), num_of(m, b)) {
53        (Num::I(ia), Num::I(ib)) => ia.cmp(&ib),
54        (Num::F(fa), Num::F(fb)) => {
55            fa.partial_cmp(&fb)
56                .unwrap_or_else(|| match (fa.is_nan(), fb.is_nan()) {
57                    (true, true) => Ordering::Equal,
58                    (true, false) => Ordering::Greater,
59                    (false, true) => Ordering::Less,
60                    (false, false) => unreachable!(),
61                })
62        }
63        (Num::I(ia), Num::F(fb)) => {
64            if fb.is_nan() {
65                return Ordering::Less; // NaN sorts after everything
66            }
67            match (ia as f64).partial_cmp(&fb).unwrap_or(Ordering::Less) {
68                Ordering::Equal => Ordering::Greater, // integer > float at equality
69                other => other,
70            }
71        }
72        (Num::F(fa), Num::I(ib)) => {
73            if fa.is_nan() {
74                return Ordering::Greater;
75            }
76            match fa.partial_cmp(&(ib as f64)).unwrap_or(Ordering::Greater) {
77                Ordering::Equal => Ordering::Less, // float < integer at equality
78                other => other,
79            }
80        }
81    }
82}
83
84/// Functor name id and arity for a compound-like word (STR or LST). For a
85/// `LST` this yields (ATOM_DOT, 2). Returns also the heap base of the args.
86fn compound_view(m: &Machine, w: Word) -> (u32, u32, ArgsKind) {
87    match tag_of(w) {
88        TAG_STR => {
89            let idx = payload(w) as usize;
90            let (f, n) = unpack_functor(m.heap[idx]);
91            (f, n, ArgsKind::Str(idx))
92        }
93        TAG_LST => (ATOM_DOT, 2, ArgsKind::Lst(payload(w) as usize)),
94        _ => unreachable!(),
95    }
96}
97
98enum ArgsKind {
99    Str(usize), // heap index of the functor header; args at idx+1..
100    Lst(usize), // heap index of [head, tail]
101}
102
103impl ArgsKind {
104    /// The i-th argument word (0-based).
105    fn arg(&self, m: &Machine, i: usize) -> Word {
106        match self {
107            ArgsKind::Str(idx) => m.heap[idx + 1 + i],
108            ArgsKind::Lst(idx) => m.heap[idx + i],
109        }
110    }
111}
112
113/// Total standard order over two heap words (after dereferencing).
114pub fn compare_terms(m: &Machine, a: Word, b: Word) -> Ordering {
115    // Iterative worklist to avoid C-stack blowups on deep/long terms; each
116    // entry is a pair still to compare. We process in order and short-circuit
117    // on the first non-equal result.
118    let mut work = vec![(a, b)];
119    while let Some((a, b)) = work.pop() {
120        let a = m.deref(a);
121        let b = m.deref(b);
122        if a == b {
123            continue; // identical word: same var cell, atom, int, or structure
124        }
125        let ra = type_rank(a);
126        let rb = type_rank(b);
127        if ra != rb {
128            return ra.cmp(&rb);
129        }
130        let c = match ra {
131            0 => {
132                // both vars: order by heap-cell index (v1 VarId)
133                payload(a).cmp(&payload(b))
134            }
135            1 => compare_numbers(m, a, b),
136            2 => m.atoms.resolve(atom_id(a)).cmp(m.atoms.resolve(atom_id(b))),
137            3 => {
138                let (fa, na, ka) = compound_view(m, a);
139                let (fb, nb, kb) = compound_view(m, b);
140                let head = (na as usize)
141                    .cmp(&(nb as usize))
142                    .then_with(|| m.atoms.resolve(fa).cmp(m.atoms.resolve(fb)));
143                if head != Ordering::Equal {
144                    return head;
145                }
146                // Same arity and functor: compare args left-to-right. Push in
147                // reverse so the leftmost arg is compared first (LIFO stack).
148                for i in (0..na as usize).rev() {
149                    work.push((ka.arg(m, i), kb.arg(m, i)));
150                }
151                Ordering::Equal
152            }
153            _ => unreachable!(),
154        };
155        if c != Ordering::Equal {
156            return c;
157        }
158    }
159    Ordering::Equal
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165    use plg_shared::StringInterner;
166
167    fn machine() -> Box<Machine> {
168        let mut atoms = StringInterner::new();
169        // ATOM_NIL/DOT/TRUE pre-interned by StringInterner::new.
170        atoms.intern("a");
171        atoms.intern("b");
172        atoms.intern("f");
173        atoms.intern("g");
174        Machine::new(atoms, Vec::new())
175    }
176
177    fn atom(m: &Machine, name: &str) -> Word {
178        make_atom(m.atoms.lookup(name).unwrap())
179    }
180
181    fn flt(m: &mut Machine, f: f64) -> Word {
182        let idx = m.heap.len();
183        m.heap.push(f.to_bits());
184        make(TAG_FLT, idx as u64)
185    }
186
187    fn str_term(m: &mut Machine, name: &str, args: &[Word]) -> Word {
188        let f = m.atoms.intern(name);
189        let idx = m.heap.len();
190        m.heap.push(pack_functor(f, args.len() as u32));
191        m.heap.extend_from_slice(args);
192        make(TAG_STR, idx as u64)
193    }
194
195    fn list(m: &mut Machine, head: Word, tail: Word) -> Word {
196        let idx = m.heap.len();
197        m.heap.push(head);
198        m.heap.push(tail);
199        make(TAG_LST, idx as u64)
200    }
201
202    #[test]
203    fn rank_var_num_atom_compound() {
204        let mut m = machine();
205        let v = m.new_var();
206        let n = make_int(1);
207        let at = atom(&m, "a");
208        let c = str_term(&mut m, "f", &[make_int(1)]);
209        assert_eq!(compare_terms(&m, v, n), Ordering::Less);
210        assert_eq!(compare_terms(&m, n, at), Ordering::Less);
211        assert_eq!(compare_terms(&m, at, c), Ordering::Less);
212        assert_eq!(compare_terms(&m, c, v), Ordering::Greater);
213    }
214
215    #[test]
216    fn float_less_than_int_at_equality() {
217        let mut m = machine();
218        let f = flt(&mut m, 1.0);
219        let i = make_int(1);
220        assert_eq!(compare_terms(&m, f, i), Ordering::Less);
221        assert_eq!(compare_terms(&m, i, f), Ordering::Greater);
222        // by value otherwise
223        let f2 = flt(&mut m, 0.5);
224        assert_eq!(compare_terms(&m, f2, i), Ordering::Less);
225    }
226
227    #[test]
228    fn nan_sorts_after_floats() {
229        let mut m = machine();
230        let nan = flt(&mut m, f64::NAN);
231        let big = flt(&mut m, 1.0e9);
232        assert_eq!(compare_terms(&m, nan, big), Ordering::Greater);
233        assert_eq!(compare_terms(&m, big, nan), Ordering::Less);
234    }
235
236    #[test]
237    fn atoms_alphabetical() {
238        let m = machine();
239        let a = atom(&m, "a");
240        let b = atom(&m, "b");
241        assert_eq!(compare_terms(&m, a, b), Ordering::Less);
242        assert_eq!(compare_terms(&m, b, a), Ordering::Greater);
243        assert_eq!(compare_terms(&m, a, a), Ordering::Equal);
244    }
245
246    #[test]
247    fn compounds_arity_then_name_then_args() {
248        let mut m = machine();
249        // arity differs: f(1) < g(1,2)
250        let f1 = str_term(&mut m, "f", &[make_int(1)]);
251        let g2 = str_term(&mut m, "g", &[make_int(1), make_int(2)]);
252        assert_eq!(compare_terms(&m, f1, g2), Ordering::Less);
253        // same arity, name differs: f(1) < g(1)
254        let g1 = str_term(&mut m, "g", &[make_int(1)]);
255        let f1b = str_term(&mut m, "f", &[make_int(1)]);
256        assert_eq!(compare_terms(&m, f1b, g1), Ordering::Less);
257        // same arity+name, args differ: f(1) < f(2)
258        let fa = str_term(&mut m, "f", &[make_int(1)]);
259        let fb = str_term(&mut m, "f", &[make_int(2)]);
260        assert_eq!(compare_terms(&m, fa, fb), Ordering::Less);
261    }
262
263    #[test]
264    fn list_equals_dot_struct() {
265        // [1,2] structurally equals '.'(1, '.'(2, [])).
266        let mut m = machine();
267        let nil = make_atom(plg_shared::atom::ATOM_NIL);
268        let lst = {
269            let inner = list(&mut m, make_int(2), nil);
270            list(&mut m, make_int(1), inner)
271        };
272        let dotstruct = {
273            let inner = str_term(&mut m, ".", &[make_int(2), nil]);
274            str_term(&mut m, ".", &[make_int(1), inner])
275        };
276        assert_eq!(compare_terms(&m, lst, dotstruct), Ordering::Equal);
277        assert_eq!(compare_terms(&m, dotstruct, lst), Ordering::Equal);
278    }
279
280    #[test]
281    fn list_vs_improper_dot() {
282        // [1,2] vs '.'(1, 2): tails [2] (compound) vs 2 (integer) → list >.
283        let mut m = machine();
284        let nil = make_atom(plg_shared::atom::ATOM_NIL);
285        let lst = {
286            let inner = list(&mut m, make_int(2), nil);
287            list(&mut m, make_int(1), inner)
288        };
289        let improper = str_term(&mut m, ".", &[make_int(1), make_int(2)]);
290        assert_eq!(compare_terms(&m, lst, improper), Ordering::Greater);
291        assert_eq!(compare_terms(&m, improper, lst), Ordering::Less);
292    }
293
294    #[test]
295    fn vars_ordered_by_index() {
296        let mut m = machine();
297        let v0 = m.new_var();
298        let v1 = m.new_var();
299        assert_eq!(compare_terms(&m, v0, v1), Ordering::Less);
300        assert_eq!(compare_terms(&m, v1, v0), Ordering::Greater);
301    }
302}