Skip to main content

plg_runtime/builtins/
arith.rs

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