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