Skip to main content

plg_runtime/builtins/
order.rs

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