Skip to main content

plg_runtime/builtins/
arith.rs

1//! Arithmetic expression evaluation (`is/2`, comparisons).
2//!
3//! Ported byte-for-byte from patch-prolog v1's `builtins.rs` (eval_arith
4//! plus the arith_* helpers). Semantics — floored `mod`, truncating `//`,
5//! float-yielding `/`, checked i64 overflow, NaN/Infinity rejection, and
6//! the exact zero-divisor labels — match v1 so error message text is
7//! identical (verified against the v1 oracle; see unit tests).
8//!
9//! NOTE on the immediate-integer range: cell `INT` is a 61-bit immediate
10//! but `eval` computes in full i64. A result that fits i64 yet overflows
11//! the i61 immediate cannot be boxed until M4; for M3 the `is/2` ABI
12//! reports it as an error (see `pred::plg_rt_b_is`). Evaluation itself
13//! never narrows — it always works in i64.
14
15use crate::cell::*;
16use crate::machine::Machine;
17
18/// An evaluated arithmetic value: integer or float (mirrors v1 ArithVal).
19#[derive(Debug, Clone, Copy, PartialEq)]
20pub enum ArithValue {
21    Int(i64),
22    Float(f64),
23}
24
25// ---- error raising (structured balls; rendered text is v1-identical) ------
26
27fn overflow(m: &mut Machine, operation: &str) {
28    let ctx = format!("Arithmetic error: integer overflow in {operation}");
29    crate::errors::evaluation(m, "int_overflow", &ctx);
30}
31
32fn zero_divisor(m: &mut Machine, label: &str) {
33    let ctx = format!("Division by zero ({label})");
34    crate::errors::evaluation(m, "zero_divisor", &ctx);
35}
36
37/// v1's `int_args_required`: the culprit term is the placeholder atom id 0,
38/// which in v1's interner resolves to "member". We reproduce v1's rendered
39/// string verbatim (the culprit is meaningless — a v1 quirk we mirror so
40/// output stays byte-identical). See report.
41fn int_args_required(m: &mut Machine, op: &str) {
42    let culprit = make_atom(m.atoms.intern("member"));
43    let ctx = format!("{op} requires integer arguments");
44    crate::errors::type_error(m, "integer", culprit, &ctx);
45}
46
47fn shift_undefined(m: &mut Machine, op: &str) {
48    let ctx = format!("Shift {op} requires a non-negative count in [0, 64)");
49    crate::errors::evaluation(m, "undefined", &ctx);
50}
51
52/// NaN/Infinity rejection after a float operation (v1 `check_float`).
53fn check_float(m: &mut Machine, f: f64) -> Result<ArithValue, ()> {
54    if f.is_nan() {
55        crate::errors::evaluation(m, "undefined", "Arithmetic error: NaN result");
56        Err(())
57    } else if f.is_infinite() {
58        crate::errors::evaluation(m, "float_overflow", "Arithmetic error: Infinity result");
59        Err(())
60    } else {
61        Ok(ArithValue::Float(f))
62    }
63}
64
65fn as_f64(v: ArithValue) -> f64 {
66    match v {
67        ArithValue::Int(n) => n as f64,
68        ArithValue::Float(f) => f,
69    }
70}
71
72// ---- value comparison (v1 arith_lt / arith_gt / arith_eq) ----------------
73
74pub fn arith_lt(a: ArithValue, b: ArithValue) -> bool {
75    use ArithValue::*;
76    match (a, b) {
77        (Int(a), Int(b)) => a < b,
78        (Float(a), Float(b)) => a < b,
79        (Int(a), Float(b)) => (a as f64) < b,
80        (Float(a), Int(b)) => a < (b as f64),
81    }
82}
83
84pub fn arith_gt(a: ArithValue, b: ArithValue) -> bool {
85    arith_lt(b, a)
86}
87
88pub fn arith_eq(a: ArithValue, b: ArithValue) -> bool {
89    use ArithValue::*;
90    match (a, b) {
91        (Int(a), Int(b)) => a == b,
92        (Float(a), Float(b)) => a == b,
93        (Int(a), Float(b)) => (a as f64) == b,
94        (Float(a), Int(b)) => a == (b as f64),
95    }
96}
97
98// ---- the evaluator -------------------------------------------------------
99
100/// Build a predicate-indicator term `Name/Arity` on the heap and return its
101/// word. This is the ISO culprit shape for `type_error(evaluable, _)`
102/// (ISO 13211-1 §8.6): the non-evaluable functor named as `Name/Arity`,
103/// including arity 0 for a bare atom.
104fn predicate_indicator(m: &mut Machine, name: u32, arity: i64) -> Word {
105    let slash = m.atoms.intern("/");
106    let pi = m.heap.len();
107    m.heap.push(pack_functor(slash, 2));
108    m.heap.push(make_atom(name));
109    m.heap.push(make_int(arity));
110    make(TAG_STR, pi as u64)
111}
112
113/// Evaluate `expr` (a heap word) to an arithmetic value. On `Err(())`,
114/// `m.error` is already populated with v1-identical message text. The unit
115/// error type is intentional: the rich error lives in `m.error` (the M3 ABI
116/// contract), so callers only need to know success vs failure.
117#[allow(clippy::result_unit_err)]
118pub fn eval(m: &mut Machine, expr: Word) -> Result<ArithValue, ()> {
119    let w = m.deref(expr);
120    match tag_of(w) {
121        TAG_INT => Ok(ArithValue::Int(int_value(w))),
122        TAG_BIG => Ok(ArithValue::Int(m.heap[payload(w) as usize] as i64)),
123        TAG_FLT => Ok(ArithValue::Float(f64::from_bits(
124            m.heap[payload(w) as usize],
125        ))),
126        TAG_REF => {
127            let ctx = format!("Arithmetic error: unbound variable _{}", payload(w));
128            crate::errors::instantiation(m, &ctx);
129            Err(())
130        }
131        TAG_ATOM => {
132            // ISO 8.6: a non-evaluable atom's culprit is the predicate
133            // indicator `Atom/0`, not the bare atom — matching the compound
134            // path below (`foo(1)` → `foo/1`).
135            let culprit = predicate_indicator(m, atom_id(w), 0);
136            crate::errors::type_error(m, "evaluable", culprit, "Cannot evaluate as arithmetic");
137            Err(())
138        }
139        TAG_LST => {
140            // A list literal is non-evaluable; keep the bare-term culprit
141            // (out of scope of the atom-indicator fix).
142            crate::errors::type_error(m, "evaluable", w, "Cannot evaluate as arithmetic");
143            Err(())
144        }
145        TAG_STR => eval_struct(m, w),
146        _ => unreachable!("bad tag in arith eval"),
147    }
148}
149
150fn eval_struct(m: &mut Machine, w: Word) -> Result<ArithValue, ()> {
151    let idx = payload(w) as usize;
152    let (functor, arity) = unpack_functor(m.heap[idx]);
153    let name = m.atoms.resolve(functor).to_string();
154    // Evaluate arguments first (left-to-right), matching v1's recursion.
155    let a0 = m.heap[idx + 1];
156    match (name.as_str(), arity) {
157        ("+", 2) => {
158            let (a, b) = bin(m, idx)?;
159            add(m, a, b)
160        }
161        ("-", 2) => {
162            let (a, b) = bin(m, idx)?;
163            sub(m, a, b)
164        }
165        ("*", 2) => {
166            let (a, b) = bin(m, idx)?;
167            mul(m, a, b)
168        }
169        ("/", 2) => {
170            let (a, b) = bin(m, idx)?;
171            div(m, a, b)
172        }
173        ("//", 2) => {
174            let (a, b) = bin(m, idx)?;
175            int_div(m, a, b)
176        }
177        ("mod", 2) => {
178            let (a, b) = bin(m, idx)?;
179            modulo(m, a, b)
180        }
181        ("rem", 2) => {
182            let (a, b) = bin(m, idx)?;
183            rem(m, a, b)
184        }
185        ("**", 2) => {
186            let (a, b) = bin(m, idx)?;
187            pow_float(m, a, b)
188        }
189        ("^", 2) => {
190            let (a, b) = bin(m, idx)?;
191            pow(m, a, b)
192        }
193        ("<<", 2) => {
194            let (a, b) = bin(m, idx)?;
195            shl(m, a, b)
196        }
197        (">>", 2) => {
198            let (a, b) = bin(m, idx)?;
199            shr(m, a, b)
200        }
201        ("/\\", 2) => {
202            let (a, b) = bin(m, idx)?;
203            bit_and(m, a, b)
204        }
205        ("\\/", 2) => {
206            let (a, b) = bin(m, idx)?;
207            bit_or(m, a, b)
208        }
209        ("xor", 2) => {
210            let (a, b) = bin(m, idx)?;
211            bit_xor(m, a, b)
212        }
213        ("div", 2) => {
214            let (a, b) = bin(m, idx)?;
215            div_floor(m, a, b)
216        }
217        ("min", 2) => {
218            let (a, b) = bin(m, idx)?;
219            Ok(if arith_lt(a, b) { a } else { b })
220        }
221        ("max", 2) => {
222            let (a, b) = bin(m, idx)?;
223            Ok(if arith_lt(a, b) { b } else { a })
224        }
225        ("-", 1) => {
226            let a = eval(m, a0)?;
227            neg(m, a)
228        }
229        ("abs", 1) => {
230            let a = eval(m, a0)?;
231            abs(m, a)
232        }
233        ("sign", 1) => {
234            let a = eval(m, a0)?;
235            Ok(sign(a))
236        }
237        ("\\", 1) => {
238            let a = eval(m, a0)?;
239            bit_not(m, a)
240        }
241        _ => {
242            // Unknown operator → type_error(evaluable, name/arity).
243            let name_id = m.atoms.intern(&name);
244            let culprit = predicate_indicator(m, name_id, arity as i64);
245            let ctx = format!("Unknown arithmetic operator: {name}/{arity}");
246            crate::errors::type_error(m, "evaluable", culprit, &ctx);
247            Err(())
248        }
249    }
250}
251
252/// Evaluate the two arguments of a binary STR at heap `idx`.
253fn bin(m: &mut Machine, idx: usize) -> Result<(ArithValue, ArithValue), ()> {
254    let a = eval(m, m.heap[idx + 1])?;
255    let b = eval(m, m.heap[idx + 2])?;
256    Ok((a, b))
257}
258
259// ---- binary operations (v1 arith_*) --------------------------------------
260
261fn add(m: &mut Machine, a: ArithValue, b: ArithValue) -> Result<ArithValue, ()> {
262    use ArithValue::*;
263    match (a, b) {
264        (Int(a), Int(b)) => a
265            .checked_add(b)
266            .map(Int)
267            .ok_or_else(|| overflow(m, "addition")),
268        (Float(a), Float(b)) => check_float(m, a + b),
269        (Int(a), Float(b)) => check_float(m, a as f64 + b),
270        (Float(a), Int(b)) => check_float(m, a + b as f64),
271    }
272}
273
274fn sub(m: &mut Machine, a: ArithValue, b: ArithValue) -> Result<ArithValue, ()> {
275    use ArithValue::*;
276    match (a, b) {
277        (Int(a), Int(b)) => a
278            .checked_sub(b)
279            .map(Int)
280            .ok_or_else(|| overflow(m, "subtraction")),
281        (Float(a), Float(b)) => check_float(m, a - b),
282        (Int(a), Float(b)) => check_float(m, a as f64 - b),
283        (Float(a), Int(b)) => check_float(m, a - b as f64),
284    }
285}
286
287fn mul(m: &mut Machine, a: ArithValue, b: ArithValue) -> Result<ArithValue, ()> {
288    use ArithValue::*;
289    match (a, b) {
290        (Int(a), Int(b)) => a
291            .checked_mul(b)
292            .map(Int)
293            .ok_or_else(|| overflow(m, "multiplication")),
294        (Float(a), Float(b)) => check_float(m, a * b),
295        (Int(a), Float(b)) => check_float(m, a as f64 * b),
296        (Float(a), Int(b)) => check_float(m, a * b as f64),
297    }
298}
299
300fn div(m: &mut Machine, a: ArithValue, b: ArithValue) -> Result<ArithValue, ()> {
301    use ArithValue::*;
302    // ISO §9.1.4: (/)/2 always yields a float.
303    match (a, b) {
304        (_, Int(0)) => {
305            zero_divisor(m, "float division");
306            Err(())
307        }
308        (_, Float(0.0)) => {
309            zero_divisor(m, "float division");
310            Err(())
311        }
312        (Int(a), Int(b)) => check_float(m, a as f64 / b as f64),
313        (Float(a), Float(b)) => check_float(m, a / b),
314        (Int(a), Float(b)) => check_float(m, a as f64 / b),
315        (Float(a), Int(b)) => check_float(m, a / b as f64),
316    }
317}
318
319fn modulo(m: &mut Machine, a: ArithValue, b: ArithValue) -> Result<ArithValue, ()> {
320    use ArithValue::*;
321    match (a, b) {
322        (Int(_), Int(0)) => {
323            zero_divisor(m, "modulo");
324            Err(())
325        }
326        (Int(_), Int(i64::MIN)) => {
327            overflow(m, "mod");
328            Err(())
329        }
330        (Int(a), Int(b)) => {
331            // ISO mod: result has the sign of the divisor.
332            let r = a.rem_euclid(b.abs());
333            if b < 0 && r != 0 {
334                Ok(Int(r - b.abs()))
335            } else {
336                Ok(Int(r))
337            }
338        }
339        _ => {
340            int_args_required(m, "mod");
341            Err(())
342        }
343    }
344}
345
346fn rem(m: &mut Machine, a: ArithValue, b: ArithValue) -> Result<ArithValue, ()> {
347    use ArithValue::*;
348    match (a, b) {
349        (Int(_), Int(0)) => {
350            zero_divisor(m, "remainder");
351            Err(())
352        }
353        (Int(a), Int(b)) => a.checked_rem(b).map(Int).ok_or_else(|| overflow(m, "rem")),
354        _ => {
355            int_args_required(m, "rem");
356            Err(())
357        }
358    }
359}
360
361fn int_div(m: &mut Machine, a: ArithValue, b: ArithValue) -> Result<ArithValue, ()> {
362    use ArithValue::*;
363    match (a, b) {
364        (Int(_), Int(0)) => {
365            zero_divisor(m, "integer division");
366            Err(())
367        }
368        (Int(a), Int(b)) => a
369            .checked_div(b)
370            .map(Int)
371            .ok_or_else(|| overflow(m, "division")),
372        _ => {
373            int_args_required(m, "//");
374            Err(())
375        }
376    }
377}
378
379fn div_floor(m: &mut Machine, a: ArithValue, b: ArithValue) -> Result<ArithValue, ()> {
380    use ArithValue::*;
381    match (a, b) {
382        (Int(_), Int(0)) => {
383            zero_divisor(m, "floor division");
384            Err(())
385        }
386        (Int(a), Int(b)) => {
387            let q = match a.checked_div(b) {
388                Some(q) => q,
389                None => {
390                    overflow(m, "floor division");
391                    return Err(());
392                }
393            };
394            let r = match a.checked_rem(b) {
395                Some(r) => r,
396                None => {
397                    overflow(m, "floor division");
398                    return Err(());
399                }
400            };
401            if r != 0 && (r < 0) != (b < 0) {
402                q.checked_sub(1)
403                    .map(Int)
404                    .ok_or_else(|| overflow(m, "floor division"))
405            } else {
406                Ok(Int(q))
407            }
408        }
409        _ => {
410            int_args_required(m, "div");
411            Err(())
412        }
413    }
414}
415
416fn pow_float(m: &mut Machine, a: ArithValue, b: ArithValue) -> Result<ArithValue, ()> {
417    check_float(m, as_f64(a).powf(as_f64(b)))
418}
419
420fn pow(m: &mut Machine, a: ArithValue, b: ArithValue) -> Result<ArithValue, ()> {
421    use ArithValue::*;
422    match (a, b) {
423        (Int(base), Int(exp)) if exp >= 0 => {
424            let exp_u32 = match u32::try_from(exp) {
425                Ok(e) => e,
426                Err(_) => {
427                    overflow(m, "integer power");
428                    return Err(());
429                }
430            };
431            base.checked_pow(exp_u32)
432                .map(Int)
433                .ok_or_else(|| overflow(m, "integer power"))
434        }
435        _ => check_float(m, as_f64(a).powf(as_f64(b))),
436    }
437}
438
439fn shl(m: &mut Machine, a: ArithValue, b: ArithValue) -> Result<ArithValue, ()> {
440    use ArithValue::*;
441    match (a, b) {
442        (Int(v), Int(n)) => {
443            let bits = match u32::try_from(n) {
444                Ok(b) => b,
445                Err(_) => {
446                    shift_undefined(m, "<<");
447                    return Err(());
448                }
449            };
450            if bits >= 64 {
451                shift_undefined(m, "<<");
452                return Err(());
453            }
454            v.checked_shl(bits)
455                .map(Int)
456                .ok_or_else(|| overflow(m, "shift_left"))
457        }
458        _ => {
459            int_args_required(m, "<<");
460            Err(())
461        }
462    }
463}
464
465fn shr(m: &mut Machine, a: ArithValue, b: ArithValue) -> Result<ArithValue, ()> {
466    use ArithValue::*;
467    match (a, b) {
468        (Int(v), Int(n)) => {
469            let bits = match u32::try_from(n) {
470                Ok(b) => b,
471                Err(_) => {
472                    shift_undefined(m, ">>");
473                    return Err(());
474                }
475            };
476            if bits >= 64 {
477                shift_undefined(m, ">>");
478                return Err(());
479            }
480            v.checked_shr(bits)
481                .map(Int)
482                .ok_or_else(|| overflow(m, "shift_right"))
483        }
484        _ => {
485            int_args_required(m, ">>");
486            Err(())
487        }
488    }
489}
490
491fn bit_and(m: &mut Machine, a: ArithValue, b: ArithValue) -> Result<ArithValue, ()> {
492    use ArithValue::*;
493    match (a, b) {
494        (Int(a), Int(b)) => Ok(Int(a & b)),
495        _ => {
496            int_args_required(m, "/\\");
497            Err(())
498        }
499    }
500}
501
502fn bit_or(m: &mut Machine, a: ArithValue, b: ArithValue) -> Result<ArithValue, ()> {
503    use ArithValue::*;
504    match (a, b) {
505        (Int(a), Int(b)) => Ok(Int(a | b)),
506        _ => {
507            int_args_required(m, "\\/");
508            Err(())
509        }
510    }
511}
512
513fn bit_xor(m: &mut Machine, a: ArithValue, b: ArithValue) -> Result<ArithValue, ()> {
514    use ArithValue::*;
515    match (a, b) {
516        (Int(a), Int(b)) => Ok(Int(a ^ b)),
517        _ => {
518            int_args_required(m, "xor");
519            Err(())
520        }
521    }
522}
523
524// ---- unary operations ----------------------------------------------------
525
526fn neg(m: &mut Machine, a: ArithValue) -> Result<ArithValue, ()> {
527    use ArithValue::*;
528    match a {
529        Int(n) => n
530            .checked_neg()
531            .map(Int)
532            .ok_or_else(|| overflow(m, "negation")),
533        Float(f) => check_float(m, -f),
534    }
535}
536
537fn abs(m: &mut Machine, a: ArithValue) -> Result<ArithValue, ()> {
538    use ArithValue::*;
539    match a {
540        Int(n) => n.checked_abs().map(Int).ok_or_else(|| overflow(m, "abs")),
541        Float(f) => check_float(m, f.abs()),
542    }
543}
544
545/// `\(I)` — bitwise complement (issue #33). Integer-only, like the binary
546/// bitwise operators; `\ 0` evaluates to `-1`.
547fn bit_not(m: &mut Machine, a: ArithValue) -> Result<ArithValue, ()> {
548    use ArithValue::*;
549    match a {
550        Int(n) => Ok(Int(!n)),
551        Float(_) => {
552            int_args_required(m, "\\");
553            Err(())
554        }
555    }
556}
557
558fn sign(a: ArithValue) -> ArithValue {
559    match a {
560        ArithValue::Int(n) => ArithValue::Int(n.signum()),
561        ArithValue::Float(f) => ArithValue::Float(f.signum()),
562    }
563}
564
565#[cfg(test)]
566mod tests {
567    use super::*;
568    use plg_shared::StringInterner;
569
570    fn machine() -> Box<Machine> {
571        Machine::new(StringInterner::new(), Vec::new())
572    }
573
574    /// Build a binary STR `op(a, b)` on the heap, returning its STR word.
575    fn bin_str(m: &mut Machine, op: &str, a: Word, b: Word) -> Word {
576        let f = m.atoms.intern(op);
577        let idx = m.heap.len();
578        m.heap.push(pack_functor(f, 2));
579        m.heap.push(a);
580        m.heap.push(b);
581        make(TAG_STR, idx as u64)
582    }
583
584    fn un_str(m: &mut Machine, op: &str, a: Word) -> Word {
585        let f = m.atoms.intern(op);
586        let idx = m.heap.len();
587        m.heap.push(pack_functor(f, 1));
588        m.heap.push(a);
589        make(TAG_STR, idx as u64)
590    }
591
592    fn flt(m: &mut Machine, f: f64) -> Word {
593        let idx = m.heap.len();
594        m.heap.push(f.to_bits());
595        make(TAG_FLT, idx as u64)
596    }
597
598    fn msg(m: &Machine) -> &str {
599        m.error.as_ref().unwrap().message.as_str()
600    }
601
602    #[test]
603    fn happy_paths() {
604        let mut m = machine();
605        let e = bin_str(&mut m, "+", make_int(2), make_int(3));
606        assert_eq!(eval(&mut m, e), Ok(ArithValue::Int(5)));
607
608        let e = bin_str(&mut m, "*", make_int(4), make_int(5));
609        assert_eq!(eval(&mut m, e), Ok(ArithValue::Int(20)));
610
611        // 2.0 + 3 = 5.0
612        let two = flt(&mut m, 2.0);
613        let e = bin_str(&mut m, "+", two, make_int(3));
614        assert_eq!(eval(&mut m, e), Ok(ArithValue::Float(5.0)));
615
616        // 2 ** 3 = 8.0 (float power)
617        let e = bin_str(&mut m, "**", make_int(2), make_int(3));
618        assert_eq!(eval(&mut m, e), Ok(ArithValue::Float(8.0)));
619
620        // 2 ^ 3 = 8 (integer power)
621        let e = bin_str(&mut m, "^", make_int(2), make_int(3));
622        assert_eq!(eval(&mut m, e), Ok(ArithValue::Int(8)));
623
624        // floored mod sign-follows-divisor
625        let e = bin_str(&mut m, "mod", make_int(10), make_int(-3));
626        assert_eq!(eval(&mut m, e), Ok(ArithValue::Int(-2)));
627        let e = bin_str(&mut m, "mod", make_int(-10), make_int(3));
628        assert_eq!(eval(&mut m, e), Ok(ArithValue::Int(2)));
629
630        // div floors toward -inf
631        let e = bin_str(&mut m, "div", make_int(10), make_int(-3));
632        assert_eq!(eval(&mut m, e), Ok(ArithValue::Int(-4)));
633
634        let e = un_str(&mut m, "abs", make_int(-5));
635        assert_eq!(eval(&mut m, e), Ok(ArithValue::Int(5)));
636        let e = un_str(&mut m, "sign", make_int(-5));
637        assert_eq!(eval(&mut m, e), Ok(ArithValue::Int(-1)));
638        let e = un_str(&mut m, "-", make_int(3));
639        assert_eq!(eval(&mut m, e), Ok(ArithValue::Int(-3)));
640
641        // \(I): unary bitwise complement (issue #33). \ 0 = -1, \ 5 = -6.
642        let e = un_str(&mut m, "\\", make_int(0));
643        assert_eq!(eval(&mut m, e), Ok(ArithValue::Int(-1)));
644        let e = un_str(&mut m, "\\", make_int(5));
645        assert_eq!(eval(&mut m, e), Ok(ArithValue::Int(-6)));
646
647        let e = bin_str(&mut m, "/\\", make_int(5), make_int(3));
648        assert_eq!(eval(&mut m, e), Ok(ArithValue::Int(1)));
649        let e = bin_str(&mut m, "xor", make_int(3), make_int(5));
650        assert_eq!(eval(&mut m, e), Ok(ArithValue::Int(6)));
651        let e = bin_str(&mut m, "<<", make_int(5), make_int(1));
652        assert_eq!(eval(&mut m, e), Ok(ArithValue::Int(10)));
653
654        // max/min with mixed types preserve operand type
655        let two = flt(&mut m, 2.0);
656        let e = bin_str(&mut m, "max", make_int(1), two);
657        assert_eq!(eval(&mut m, e), Ok(ArithValue::Float(2.0)));
658        let two = flt(&mut m, 2.0);
659        let e = bin_str(&mut m, "min", make_int(1), two);
660        assert_eq!(eval(&mut m, e), Ok(ArithValue::Int(1)));
661    }
662
663    #[test]
664    fn err_zero_divisors() {
665        let cases = [
666            ("//", "integer division"),
667            ("mod", "modulo"),
668            ("rem", "remainder"),
669            ("div", "floor division"),
670        ];
671        for (op, label) in cases {
672            let mut m = machine();
673            let e = bin_str(&mut m, op, make_int(1), make_int(0));
674            assert!(eval(&mut m, e).is_err());
675            assert_eq!(
676                msg(&m),
677                format!("error(evaluation_error(zero_divisor), Division by zero ({label}))")
678            );
679        }
680        // (/)/2 zero divisor reports "float division"
681        let mut m = machine();
682        let e = bin_str(&mut m, "/", make_int(1), make_int(0));
683        assert!(eval(&mut m, e).is_err());
684        assert_eq!(
685            msg(&m),
686            "error(evaluation_error(zero_divisor), Division by zero (float division))"
687        );
688    }
689
690    #[test]
691    fn err_int_overflow() {
692        // INT_MAX is the largest i61 immediate; INT_MAX * INT_MAX ~ 2^120
693        // overflows i64, exercising the checked-mul overflow path. (A bare
694        // i64::MAX cannot be an immediate INT, so we drive overflow through
695        // genuine i61 operands.)
696        let mut m = machine();
697        let e = bin_str(&mut m, "*", make_int(INT_MAX), make_int(INT_MAX));
698        assert!(eval(&mut m, e).is_err());
699        assert_eq!(
700            msg(&m),
701            "error(evaluation_error(int_overflow), Arithmetic error: integer overflow in multiplication)"
702        );
703
704        // Negation overflow uses the "negation" label (i64::MIN-style edge):
705        // nest so the inner value is computed in i64 then negated. INT_MIN is
706        // a valid immediate, and -INT_MIN fits, so drive overflow via mul.
707        let mut m = machine();
708        let e = bin_str(&mut m, "+", make_int(INT_MAX), make_int(INT_MAX));
709        // INT_MAX + INT_MAX = 2^61 - 2, fits i64 — succeeds at eval level.
710        assert_eq!(eval(&mut m, e), Ok(ArithValue::Int(INT_MAX + INT_MAX)));
711    }
712
713    #[test]
714    fn err_type_evaluable_atom_and_compound() {
715        let mut m = machine();
716        let foo = m.atoms.intern("foo");
717        assert!(eval(&mut m, make_atom(foo)).is_err());
718        assert_eq!(
719            msg(&m),
720            "error(type_error(evaluable, /(foo, 0)), Cannot evaluate as arithmetic)"
721        );
722
723        let mut m = machine();
724        let e = un_str(&mut m, "foo", make_int(1));
725        assert!(eval(&mut m, e).is_err());
726        assert_eq!(
727            msg(&m),
728            "error(type_error(evaluable, /(foo, 1)), Unknown arithmetic operator: foo/1)"
729        );
730    }
731
732    #[test]
733    fn err_instantiation() {
734        let mut m = machine();
735        let v = m.new_var();
736        assert!(eval(&mut m, v).is_err());
737        // payload(v) is the heap index of the var cell.
738        let idx = payload(v);
739        assert_eq!(
740            msg(&m),
741            format!("error(instantiation_error, Arithmetic error: unbound variable _{idx})")
742        );
743    }
744
745    #[test]
746    fn err_nan_and_infinity() {
747        // 0.0 / 0.0 is caught as zero_divisor (divisor is zero) before NaN.
748        let mut m = machine();
749        let a = flt(&mut m, 0.0);
750        let b = flt(&mut m, 0.0);
751        let e = bin_str(&mut m, "/", a, b);
752        assert!(eval(&mut m, e).is_err());
753        assert_eq!(
754            msg(&m),
755            "error(evaluation_error(zero_divisor), Division by zero (float division))"
756        );
757
758        // sqrt is not available; force NaN via 0.0 ** ... no — use a NaN input.
759        // Infinity result: 1e308 * 10 overflows to +inf.
760        let mut m = machine();
761        let big = flt(&mut m, 1.0e308);
762        let ten = flt(&mut m, 10.0);
763        let e = bin_str(&mut m, "*", big, ten);
764        assert!(eval(&mut m, e).is_err());
765        assert_eq!(
766            msg(&m),
767            "error(evaluation_error(float_overflow), Arithmetic error: Infinity result)"
768        );
769
770        // NaN propagation: NaN + 1.0 → NaN result.
771        let mut m = machine();
772        let nan = flt(&mut m, f64::NAN);
773        let one = flt(&mut m, 1.0);
774        let e = bin_str(&mut m, "+", nan, one);
775        assert!(eval(&mut m, e).is_err());
776        assert_eq!(
777            msg(&m),
778            "error(evaluation_error(undefined), Arithmetic error: NaN result)"
779        );
780    }
781
782    #[test]
783    fn err_int_args_required() {
784        let mut m = machine();
785        let two = flt(&mut m, 2.0);
786        let e = bin_str(&mut m, "mod", make_int(5), two);
787        assert!(eval(&mut m, e).is_err());
788        assert_eq!(
789            msg(&m),
790            "error(type_error(integer, member), mod requires integer arguments)"
791        );
792    }
793
794    #[test]
795    fn err_shift_undefined() {
796        let mut m = machine();
797        let e = bin_str(&mut m, "<<", make_int(1), make_int(64));
798        assert!(eval(&mut m, e).is_err());
799        assert_eq!(
800            msg(&m),
801            "error(evaluation_error(undefined), Shift << requires a non-negative count in [0, 64))"
802        );
803    }
804
805    #[test]
806    fn mixed_comparison_helpers() {
807        // 1 =:= 1.0 true; 1.0 < 1 false
808        assert!(arith_eq(ArithValue::Int(1), ArithValue::Float(1.0)));
809        assert!(!arith_lt(ArithValue::Float(1.0), ArithValue::Int(1)));
810        assert!(arith_lt(ArithValue::Int(1), ArithValue::Int(2)));
811        assert!(arith_gt(ArithValue::Float(2.0), ArithValue::Int(1)));
812    }
813}