Skip to main content

logicaffeine_compile/semantics/
arith.rs

1//! Arithmetic, logical, and bitwise operators.
2
3use std::rc::Rc;
4
5use logicaffeine_base::{BigInt, Complex, Decimal, Modular, Rational, WordVal};
6
7use crate::ast::stmt::BinaryOpKind;
8use crate::interpreter::RuntimeValue;
9
10use super::compare::{compare, values_equal};
11use super::temporal::date_add_span;
12
13/// View an integer value — narrow `Int` or wide `BigInt` — as a `BigInt`, for the
14/// exact-arithmetic path that the overflow-safe operators promote into.
15fn big_of(v: &RuntimeValue) -> Option<BigInt> {
16    match v {
17        RuntimeValue::Int(n) => Some(BigInt::from_i64(*n)),
18        RuntimeValue::BigInt(b) => Some((**b).clone()),
19        _ => None,
20    }
21}
22
23/// View an exact number — `Int`, `BigInt`, `Rational`, or `Decimal` — as a `Rational`,
24/// for the exact-arithmetic path that integer division "overflows" into. `Float` is
25/// inexact by choice and returns `None`.
26fn rat_of(v: &RuntimeValue) -> Option<Rational> {
27    match v {
28        RuntimeValue::Int(n) => Some(Rational::from_i64(*n)),
29        RuntimeValue::BigInt(b) => Some(Rational::from_bigint((**b).clone())),
30        RuntimeValue::Rational(r) => Some((**r).clone()),
31        RuntimeValue::Decimal(d) => Some(d.to_rational()),
32        _ => None,
33    }
34}
35
36/// View an integer-or-decimal value as a `Decimal`, for the decimal-PRESERVING path:
37/// `Decimal ∘ {Decimal, Int, BigInt}` stays an exact `Decimal` (money keeps its scale).
38/// `Rational`/`Float` return `None` so those operands route to the rational/float paths.
39fn dec_of(v: &RuntimeValue) -> Option<Decimal> {
40    match v {
41        RuntimeValue::Int(n) => Some(Decimal::from_i64(*n)),
42        RuntimeValue::BigInt(b) => Some(Decimal::from_bigint((**b).clone())),
43        RuntimeValue::Decimal(d) => Some((**d).clone()),
44        _ => None,
45    }
46}
47
48/// View any number — exact or `Float` — as an `f64`, for the inexact path a `Float`
49/// operand forces. `None` for non-numbers.
50fn num_f64(v: &RuntimeValue) -> Option<f64> {
51    match v {
52        RuntimeValue::Int(n) => Some(*n as f64),
53        RuntimeValue::BigInt(b) => Some(b.to_f64()),
54        RuntimeValue::Rational(r) => Some(r.to_f64()),
55        RuntimeValue::Decimal(d) => Some(d.to_rational().to_f64()),
56        RuntimeValue::Float(f) => Some(*f),
57        _ => None,
58    }
59}
60
61/// View an exact number — `Int`, `BigInt`, `Rational`, `Decimal`, or `Complex` — as a
62/// `Complex`. `Float` is inexact and returns `None` (an exact `Complex` does not absorb a
63/// float), so `Complex ∘ Float` is a typed error rather than a silent lossy coercion.
64fn complex_of(v: &RuntimeValue) -> Option<Complex> {
65    match v {
66        RuntimeValue::Int(n) => Some(Complex::from_i64(*n)),
67        RuntimeValue::BigInt(b) => Some(Complex::from_rational(Rational::from_bigint((**b).clone()))),
68        RuntimeValue::Rational(r) => Some(Complex::from_rational((**r).clone())),
69        RuntimeValue::Decimal(d) => Some(Complex::from_rational(d.to_rational())),
70        RuntimeValue::Complex(c) => Some((**c).clone()),
71        _ => None,
72    }
73}
74
75/// Modular-operand dispatch shared by `add`/`subtract`/`multiply`: a `Modular` combines ONLY
76/// with another `Modular` of the same modulus (no auto-lift — a bare integer has no modulus).
77/// `None` when neither side is a `Modular`; `Some(Err)` on a non-Modular operand or a modulus
78/// mismatch (`f` returns `None` for a mismatch, like a Word width mismatch).
79fn modular_binop(
80    left: &RuntimeValue,
81    right: &RuntimeValue,
82    op_name: &str,
83    f: impl Fn(&Modular, &Modular) -> Option<Modular>,
84) -> Option<Result<RuntimeValue, String>> {
85    if !matches!(left, RuntimeValue::Modular(_)) && !matches!(right, RuntimeValue::Modular(_)) {
86        return None;
87    }
88    Some(match (left, right) {
89        (RuntimeValue::Modular(a), RuntimeValue::Modular(b)) => match f(a, b) {
90            Some(r) => Ok(RuntimeValue::Modular(Rc::new(r))),
91            None => Err(format!("cannot {op_name} values in different modular rings")),
92        },
93        _ => Err(format!(
94            "Cannot {} {} and {} (modular arithmetic needs two ℤ/nℤ values of the same modulus)",
95            op_name,
96            left.type_name(),
97            right.type_name()
98        )),
99    })
100}
101
102/// Complex-operand dispatch shared by `add`/`subtract`/`multiply`: when either side is a
103/// `Complex`, the result is `Complex` (a real embeds as `re + 0i`). `None` when no operand
104/// is a `Complex` (fall through); `Some(Err)` when an operand is inexact (`Float`).
105fn complex_binop(
106    left: &RuntimeValue,
107    right: &RuntimeValue,
108    op_name: &str,
109    f: impl Fn(&Complex, &Complex) -> Complex,
110) -> Option<Result<RuntimeValue, String>> {
111    if !matches!(left, RuntimeValue::Complex(_)) && !matches!(right, RuntimeValue::Complex(_)) {
112        return None;
113    }
114    Some(match (complex_of(left), complex_of(right)) {
115        (Some(a), Some(b)) => Ok(RuntimeValue::Complex(Rc::new(f(&a, &b)))),
116        _ => Err(format!(
117            "Cannot {} {} and {} (Complex combines only with exact numbers)",
118            op_name,
119            left.type_name(),
120            right.type_name()
121        )),
122    })
123}
124
125/// Decimal-operand dispatch shared by `add`/`subtract`/`multiply`: when either side is a
126/// `Decimal`, `Decimal ∘ {Decimal,Int,BigInt}` stays exact `Decimal` (via `dec`), a
127/// `Rational` operand promotes to exact `Rational`, and a `Float` operand yields `Float`.
128/// Returns `None` when no operand is a `Decimal` (fall through to the existing paths).
129fn decimal_binop(
130    left: &RuntimeValue,
131    right: &RuntimeValue,
132    dec: impl Fn(&Decimal, &Decimal) -> Decimal,
133    rat: impl Fn(&Rational, &Rational) -> Rational,
134    flt: impl Fn(f64, f64) -> f64,
135) -> Option<RuntimeValue> {
136    if !matches!(left, RuntimeValue::Decimal(_)) && !matches!(right, RuntimeValue::Decimal(_)) {
137        return None;
138    }
139    if let (Some(a), Some(b)) = (dec_of(left), dec_of(right)) {
140        return Some(RuntimeValue::Decimal(Rc::new(dec(&a, &b))));
141    }
142    if let (Some(a), Some(b)) = (rat_of(left), rat_of(right)) {
143        return Some(RuntimeValue::from_rational(rat(&a, &b)));
144    }
145    let (a, b) = (num_f64(left)?, num_f64(right)?);
146    Some(RuntimeValue::Float(flt(a, b)))
147}
148
149/// Physical-quantity arithmetic. `+ −` require equal dimensions (the result keeps the LEFT operand's
150/// display unit), `× ÷` combine dimensions (the result is shown in SI/dimension form), and a quantity
151/// may be scaled by a dimensionless number under `× ÷` (its unit preserved). Returns `None` when
152/// neither operand is a `Quantity` (so the normal numeric dispatch runs); `Some(Err)` on a dimension
153/// mismatch or division by zero. The magnitude rides the exact rational tower, so it never drifts.
154fn quantity_binop(left: &RuntimeValue, right: &RuntimeValue, op: char) -> Option<Result<RuntimeValue, String>> {
155    use crate::interpreter::QuantityValue;
156    use logicaffeine_base::{Quantity, Unit};
157    if !matches!(left, RuntimeValue::Quantity(_)) && !matches!(right, RuntimeValue::Quantity(_)) {
158        return None;
159    }
160    let mk = |q: Quantity, unit: Unit| RuntimeValue::Quantity(Rc::new(QuantityValue { q, unit }));
161    // A synthetic SI-base unit (empty symbol) for a combined dimension — `display` renders it as the
162    // magnitude plus the dimension signature until a named compound unit is chosen.
163    let si_unit = |q: &Quantity| Unit::linear("", q.dimension(), Rational::one());
164    Some(match (left, right) {
165        (RuntimeValue::Quantity(a), RuntimeValue::Quantity(b)) => match op {
166            '+' => a.q.add(&b.q).map(|q| mk(q, a.unit.clone())).ok_or_else(|| {
167                format!("cannot add quantities of different dimensions ({} vs {})", a.q.dimension(), b.q.dimension())
168            }),
169            '-' => a.q.sub(&b.q).map(|q| mk(q, a.unit.clone())).ok_or_else(|| {
170                format!("cannot subtract quantities of different dimensions ({} vs {})", a.q.dimension(), b.q.dimension())
171            }),
172            '*' => {
173                let q = a.q.mul(&b.q);
174                let u = si_unit(&q);
175                Ok(mk(q, u))
176            }
177            '/' => match a.q.div(&b.q) {
178                Some(q) => {
179                    let u = si_unit(&q);
180                    Ok(mk(q, u))
181                }
182                None => Err("cannot divide by a zero quantity".to_string()),
183            },
184            _ => unreachable!("quantity_binop only handles + - * /"),
185        },
186        // Scale a quantity by a dimensionless number, preserving its unit: `q * k`, `q / k`.
187        (RuntimeValue::Quantity(a), scalar) if matches!(op, '*' | '/') => match rat_of(scalar) {
188            Some(k) => {
189                let mag = if op == '*' {
190                    a.q.magnitude_si().mul(&k)
191                } else {
192                    match a.q.magnitude_si().div(&k) {
193                        Some(m) => m,
194                        None => return Some(Err("cannot divide a quantity by zero".to_string())),
195                    }
196                };
197                Ok(mk(Quantity::si(mag, a.q.dimension()), a.unit.clone()))
198            }
199            None => return None,
200        },
201        // `k * q` — scalar on the left (multiplication commutes).
202        (scalar, RuntimeValue::Quantity(b)) if op == '*' => match rat_of(scalar) {
203            Some(k) => Ok(mk(Quantity::si(b.q.magnitude_si().mul(&k), b.q.dimension()), b.unit.clone())),
204            None => return None,
205        },
206        _ => return None,
207    })
208}
209
210/// Money arithmetic. `+ −` require the SAME currency (a typed error otherwise, like a dimension
211/// mismatch); `×` and `÷` scale by an exact number (Int/Decimal), re-quantised to the currency's
212/// minor unit; a same-currency `Money ÷ Money` is the dimensionless ratio. Returns `None` when
213/// neither operand is Money (so the normal numeric dispatch runs); `Some(Err)` on a currency mismatch.
214fn money_binop(left: &RuntimeValue, right: &RuntimeValue, op: char) -> Option<Result<RuntimeValue, String>> {
215    use logicaffeine_base::{Decimal, Money, RoundingMode};
216    if !matches!(left, RuntimeValue::Money(_)) && !matches!(right, RuntimeValue::Money(_)) {
217        return None;
218    }
219    // An exact base-10 scalar (Int or Decimal); a Float or non-terminating Rational is refused.
220    let dec_of = |v: &RuntimeValue| -> Option<Decimal> {
221        match v {
222            RuntimeValue::Int(n) => Some(Decimal::from_i64(*n)),
223            RuntimeValue::Decimal(d) => Some((**d).clone()),
224            _ => None,
225        }
226    };
227    let mk = |m: Money| RuntimeValue::Money(Rc::new(m));
228    let mismatch = |a: &Money, b: &Money, verb: &str| {
229        format!("cannot {verb} money of different currencies ({} vs {})", a.currency.code, b.currency.code)
230    };
231    Some(match (left, right) {
232        (RuntimeValue::Money(a), RuntimeValue::Money(b)) => match op {
233            '+' => a.add(b).map(mk).ok_or_else(|| mismatch(a, b, "add")),
234            '-' => a.sub(b).map(mk).ok_or_else(|| mismatch(a, b, "subtract")),
235            '/' => match a.ratio(b) {
236                Some(r) => Ok(RuntimeValue::from_rational(r)),
237                None if a.currency != b.currency => Err(mismatch(a, b, "compare")),
238                None => Err("cannot divide money by a zero amount".to_string()),
239            },
240            '*' => Err("cannot multiply money by money — scale by a number instead".to_string()),
241            _ => unreachable!("money_binop only handles + - * /"),
242        },
243        // Scale money by an exact number (`19.99 USD × 3`, `price × 1.5`), re-quantised; `×` commutes.
244        (RuntimeValue::Money(a), scalar) | (scalar, RuntimeValue::Money(a)) if op == '*' => {
245            match dec_of(scalar) {
246                Some(s) => Ok(mk(Money::of(a.amount.mul(&s), a.currency))),
247                None => Err(format!("cannot multiply money by {}", scalar.type_name())),
248            }
249        }
250        // Divide money by an exact number (e.g. split a bill), re-quantised to the currency.
251        (RuntimeValue::Money(a), scalar) if op == '/' => match dec_of(scalar) {
252            Some(s) => match a.amount.div(&s, a.currency.scale, RoundingMode::HalfEven) {
253                Some(d) => Ok(mk(Money::of(d, a.currency))),
254                None => Err("cannot divide money by zero".to_string()),
255            },
256            None => Err(format!("cannot divide money by {}", scalar.type_name())),
257        },
258        _ => return None,
259    })
260}
261
262/// Apply a binary operator to two already-evaluated values.
263///
264/// NOTE on `And`/`Or`: these are the *eager* semantics (both operands already
265/// evaluated) — bitwise for Int×Int, truthiness otherwise. Short-circuit
266/// evaluation order is the engine's responsibility: evaluate the left operand,
267/// and only consult the right when the left is an Int (bitwise) or when
268/// truthiness requires it.
269/// The error for combining two words of different widths — a type error, never a coercion.
270fn word_width_err(a: WordVal, b: WordVal) -> String {
271    format!("cannot combine Word{} and Word{} — width mismatch", a.width(), b.width())
272}
273
274/// Extract a shift count as `u32` from an `Int` or `Word`.
275fn shift_count(v: &RuntimeValue) -> Option<u32> {
276    match v {
277        RuntimeValue::Int(n) => Some(*n as u32),
278        RuntimeValue::Word(w) => Some(w.to_u64() as u32),
279        _ => None,
280    }
281}
282
283/// Word-operand dispatch for [`binary_op`]: `Some(result)` when handled as a ring-of-ℤ/2ᵏ op,
284/// `None` to fall through to the generic numeric/comparison path, `Err` on a width mismatch.
285/// Arithmetic/bitwise require both operands at the same width; shifts take an integer count.
286fn word_binary_op(
287    op: BinaryOpKind,
288    left: &RuntimeValue,
289    right: &RuntimeValue,
290) -> Result<Option<RuntimeValue>, String> {
291    use BinaryOpKind::*;
292    if matches!(op, Shl | Shr) {
293        if let RuntimeValue::Word(a) = left {
294            let n = shift_count(right).ok_or_else(|| "shift count must be an integer".to_string())?;
295            let r = if matches!(op, Shl) { a.shl(n) } else { a.shr(n) };
296            return Ok(Some(RuntimeValue::Word(r)));
297        }
298        return Ok(None);
299    }
300    let (RuntimeValue::Word(a), RuntimeValue::Word(b)) = (left, right) else {
301        return Ok(None);
302    };
303    let combined = match op {
304        // Add/Subtract/Multiply are handled in `add`/`subtract`/`multiply` (the VM calls those
305        // directly), so both tiers share ONE Word path; fall through to them here.
306        And => a.bitand(*b),
307        Or => a.bitor(*b),
308        BitXor => a.bitxor(*b),
309        _ => return Ok(None),
310    };
311    match combined {
312        Some(w) => Ok(Some(RuntimeValue::Word(w))),
313        None => Err(word_width_err(*a, *b)),
314    }
315}
316
317/// Lane-operand dispatch for [`binary_op`]: a SIMD lane vector op is the same operator applied to
318/// every lane (the scalar-lane spec; AOT lowers it to the matching AVX2 intrinsic). `None` falls
319/// through, `Err` on a lane-config mismatch.
320fn lanes_binary_op(
321    op: BinaryOpKind,
322    left: &RuntimeValue,
323    right: &RuntimeValue,
324) -> Result<Option<RuntimeValue>, String> {
325    let (RuntimeValue::Lanes(a), RuntimeValue::Lanes(b)) = (left, right) else {
326        return Ok(None);
327    };
328    let combined = match op {
329        BinaryOpKind::BitXor => a.bitxor(**b),
330        _ => return Ok(None),
331    };
332    match combined {
333        Some(v) => Ok(Some(RuntimeValue::Lanes(std::rc::Rc::new(v)))),
334        None => Err(format!(
335            "cannot combine {} and {} — lane-config mismatch",
336            a.type_name(),
337            b.type_name()
338        )),
339    }
340}
341
342pub fn binary_op(
343    op: BinaryOpKind,
344    left: RuntimeValue,
345    right: RuntimeValue,
346) -> Result<RuntimeValue, String> {
347    // Fixed-width wrapping fast path: an op on words stays in the ring ℤ/2ᵏ (wrapping, never
348    // promoting). Equality/comparison fall through to the generic path below.
349    if let Some(result) = word_binary_op(op, &left, &right)? {
350        return Ok(result);
351    }
352    // SIMD lane vectors: the op applies lane-wise (the scalar-lane spec).
353    if let Some(result) = lanes_binary_op(op, &left, &right)? {
354        return Ok(result);
355    }
356    match op {
357        BinaryOpKind::Add => add(left, right),
358        BinaryOpKind::Subtract => subtract(left, right),
359        BinaryOpKind::Multiply => multiply(left, right),
360        BinaryOpKind::Pow => power(left, right),
361        BinaryOpKind::Divide => divide(left, right),
362        BinaryOpKind::ExactDivide => exact_divide(left, right),
363        BinaryOpKind::FloorDivide => floor_divide(left, right),
364        BinaryOpKind::Modulo => modulo(left, right),
365        BinaryOpKind::Eq => Ok(RuntimeValue::Bool(values_equal(&left, &right))),
366        BinaryOpKind::NotEq => Ok(RuntimeValue::Bool(!values_equal(&left, &right))),
367        BinaryOpKind::ApproxEq => approx_eq(left, right),
368        BinaryOpKind::Lt | BinaryOpKind::Gt | BinaryOpKind::LtEq | BinaryOpKind::GtEq => {
369            compare(op, &left, &right)
370        }
371        // Logical words: truthiness in, Bool out. The bitwise spellings are `&`/`|` (BitAnd/BitOr).
372        BinaryOpKind::And => Ok(RuntimeValue::Bool(left.is_truthy() && right.is_truthy())),
373        BinaryOpKind::Or => Ok(RuntimeValue::Bool(left.is_truthy() || right.is_truthy())),
374        BinaryOpKind::Concat => concat(left, right),
375        BinaryOpKind::SeqConcat => seq_concat(left, right),
376        BinaryOpKind::BitXor => match (&left, &right) {
377            (RuntimeValue::Int(a), RuntimeValue::Int(b)) => Ok(RuntimeValue::Int(a ^ b)),
378            // Boolean XOR (`a ≠ b`) — matches the codegen's `a ^ b` on Rust `bool`, so the tiers
379            // agree. (Word and lane XOR are taken on the fast paths above.)
380            (RuntimeValue::Bool(a), RuntimeValue::Bool(b)) => Ok(RuntimeValue::Bool(a ^ b)),
381            // On Sets, `^` is the symmetric difference.
382            (RuntimeValue::Set(_), RuntimeValue::Set(_)) => set_binop(&left, &right, SetOp::SymmetricDifference),
383            _ => Err("Bitwise XOR requires integer, boolean, or Set operands".to_string()),
384        },
385        // `&` — bitwise AND on Int, intersection on Sets.
386        BinaryOpKind::BitAnd => match (&left, &right) {
387            (RuntimeValue::Int(a), RuntimeValue::Int(b)) => Ok(RuntimeValue::Int(a & b)),
388            (RuntimeValue::Bool(a), RuntimeValue::Bool(b)) => Ok(RuntimeValue::Bool(a & b)),
389            (RuntimeValue::Set(_), RuntimeValue::Set(_)) => set_binop(&left, &right, SetOp::Intersection),
390            _ => Err("`&` requires integer, boolean, or Set operands".to_string()),
391        },
392        // `|` — bitwise OR on Int, union on Sets.
393        BinaryOpKind::BitOr => match (&left, &right) {
394            (RuntimeValue::Int(a), RuntimeValue::Int(b)) => Ok(RuntimeValue::Int(a | b)),
395            (RuntimeValue::Bool(a), RuntimeValue::Bool(b)) => Ok(RuntimeValue::Bool(a | b)),
396            (RuntimeValue::Set(_), RuntimeValue::Set(_)) => set_binop(&left, &right, SetOp::Union),
397            _ => Err("`|` requires integer, boolean, or Set operands".to_string()),
398        },
399        // Shift counts are truncated to u32 and masked mod 64 (the wrapping spec).
400        BinaryOpKind::Shl => match (left, right) {
401            (RuntimeValue::Int(a), RuntimeValue::Int(b)) => {
402                Ok(RuntimeValue::Int(a.wrapping_shl(b as u32)))
403            }
404            _ => Err("Left shift requires integer operands".to_string()),
405        },
406        BinaryOpKind::Shr => match (left, right) {
407            (RuntimeValue::Int(a), RuntimeValue::Int(b)) => {
408                Ok(RuntimeValue::Int(a.wrapping_shr(b as u32)))
409            }
410            _ => Err("Right shift requires integer operands".to_string()),
411        },
412    }
413}
414
415pub fn add(left: RuntimeValue, right: RuntimeValue) -> Result<RuntimeValue, String> {
416    // Words add in the ring ℤ/2ᵏ (wrapping). Here as well as in `binary_op` so the VM, which
417    // calls `add` directly, stays byte-identical to the tree-walker on the Word path.
418    if let (RuntimeValue::Word(a), RuntimeValue::Word(b)) = (&left, &right) {
419        return a.add(*b).map(RuntimeValue::Word).ok_or_else(|| word_width_err(*a, *b));
420    }
421    // SIMD lane vectors add lane-wise in ℤ/2³² (the ChaCha quarter-round's `a += b`).
422    if let (RuntimeValue::Lanes(a), RuntimeValue::Lanes(b)) = (&left, &right) {
423        return a.add(**b).map(|v| RuntimeValue::Lanes(std::rc::Rc::new(v))).ok_or_else(|| {
424            format!("cannot add {} and {} — lane-config mismatch", a.type_name(), b.type_name())
425        });
426    }
427    if let Some(r) = quantity_binop(&left, &right, '+') {
428        return r;
429    }
430    if let Some(r) = money_binop(&left, &right, '+') {
431        return r;
432    }
433    if let Some(r) = modular_binop(&left, &right, "add", |a, b| a.add(b)) {
434        return r;
435    }
436    if let Some(r) = complex_binop(&left, &right, "add", |a, b| a.add(b)) {
437        return r;
438    }
439    if let Some(r) = decimal_binop(&left, &right, |a, b| a.add(b), |a, b| a.add(b), |a, b| a + b) {
440        return Ok(r);
441    }
442    match (&left, &right) {
443        // Integer addition is EXACT: on i64 overflow it promotes to BigInt instead of
444        // wrapping (the silent `i64::MAX + 1 == i64::MIN` corruption is the bug this
445        // fixes). The result downsizes back to Int whenever it fits (`from_bigint`).
446        (RuntimeValue::Int(a), RuntimeValue::Int(b)) => Ok(match a.checked_add(*b) {
447            Some(s) => RuntimeValue::Int(s),
448            None => RuntimeValue::from_bigint(BigInt::from_i64(*a).add(&BigInt::from_i64(*b))),
449        }),
450        (RuntimeValue::BigInt(a), RuntimeValue::BigInt(b)) => Ok(RuntimeValue::from_bigint(a.add(b))),
451        (RuntimeValue::BigInt(a), RuntimeValue::Int(b)) => {
452            Ok(RuntimeValue::from_bigint(a.add(&BigInt::from_i64(*b))))
453        }
454        (RuntimeValue::Int(a), RuntimeValue::BigInt(b)) => {
455            Ok(RuntimeValue::from_bigint(BigInt::from_i64(*a).add(b)))
456        }
457        (RuntimeValue::Float(a), RuntimeValue::Float(b)) => Ok(RuntimeValue::Float(a + b)),
458        (RuntimeValue::Int(a), RuntimeValue::Float(b)) => Ok(RuntimeValue::Float(*a as f64 + b)),
459        (RuntimeValue::Float(a), RuntimeValue::Int(b)) => Ok(RuntimeValue::Float(a + *b as f64)),
460        (RuntimeValue::BigInt(a), RuntimeValue::Float(b)) => Ok(RuntimeValue::Float(a.to_f64() + b)),
461        (RuntimeValue::Float(a), RuntimeValue::BigInt(b)) => Ok(RuntimeValue::Float(a + b.to_f64())),
462        // Rational keeps arithmetic EXACT: any Int/BigInt/Rational mix involving a
463        // Rational stays exact (downsized to Int when it reduces to a whole number); a
464        // Float operand makes the result Float (floats are inexact by choice).
465        (RuntimeValue::Rational(a), RuntimeValue::Rational(b)) => {
466            Ok(RuntimeValue::from_rational(a.add(b)))
467        }
468        (RuntimeValue::Rational(_), RuntimeValue::Int(_))
469        | (RuntimeValue::Int(_), RuntimeValue::Rational(_))
470        | (RuntimeValue::Rational(_), RuntimeValue::BigInt(_))
471        | (RuntimeValue::BigInt(_), RuntimeValue::Rational(_)) => {
472            Ok(RuntimeValue::from_rational(rat_of(&left).unwrap().add(&rat_of(&right).unwrap())))
473        }
474        (RuntimeValue::Rational(r), RuntimeValue::Float(b)) => Ok(RuntimeValue::Float(r.to_f64() + b)),
475        (RuntimeValue::Float(a), RuntimeValue::Rational(r)) => Ok(RuntimeValue::Float(a + r.to_f64())),
476        (RuntimeValue::Text(a), RuntimeValue::Text(b)) => {
477            Ok(RuntimeValue::Text(Rc::new(format!("{}{}", a, b))))
478        }
479        (RuntimeValue::Text(a), other) => {
480            Ok(RuntimeValue::Text(Rc::new(format!("{}{}", a, other.to_display_string()))))
481        }
482        (other, RuntimeValue::Text(b)) => {
483            Ok(RuntimeValue::Text(Rc::new(format!("{}{}", other.to_display_string(), b))))
484        }
485        (RuntimeValue::Duration(a), RuntimeValue::Duration(b)) => {
486            Ok(RuntimeValue::Duration(a.wrapping_add(*b)))
487        }
488        (RuntimeValue::Date(days), RuntimeValue::Span { months, days: span_days }) => {
489            Ok(RuntimeValue::Date(date_add_span(*days, *months, *span_days)))
490        }
491        // A Moment plus a CIVIL Span is calendar arithmetic (`m + 1 month` clamps end-of-month,
492        // respects leap years, keeps the wall time); commutes. Contrast the physical Duration below.
493        (RuntimeValue::Moment(nanos), RuntimeValue::Span { months, days: span_days })
494        | (RuntimeValue::Span { months, days: span_days }, RuntimeValue::Moment(nanos)) => Ok(
495            RuntimeValue::Moment(super::temporal::moment_add_span(*nanos, *months, *span_days)),
496        ),
497        // A Moment plus a physical Duration is a later Moment (e.g. `m + 90 seconds`); commutes.
498        (RuntimeValue::Moment(nanos), RuntimeValue::Duration(d))
499        | (RuntimeValue::Duration(d), RuntimeValue::Moment(nanos)) => {
500            Ok(RuntimeValue::Moment(nanos.wrapping_add(*d)))
501        }
502        // `xs + ys` concatenates sequences — exactly `xs followed by ys`
503        // (a fresh sequence; neither operand is mutated).
504        (RuntimeValue::List(_), RuntimeValue::List(_)) => {
505            seq_concat(left.clone(), right.clone())
506        }
507        _ => Err(format!("Cannot add {} and {}", left.type_name(), right.type_name())),
508    }
509}
510
511pub fn concat(left: RuntimeValue, right: RuntimeValue) -> Result<RuntimeValue, String> {
512    Ok(RuntimeValue::Text(Rc::new(format!(
513        "{}{}",
514        left.to_display_string(),
515        right.to_display_string()
516    ))))
517}
518
519/// The Set operations behind `| & ^ -` on Set operands. Results are FRESH
520/// sets in first-operand-then-second insertion order, deduped by
521/// `values_equal` — the same equality the language uses everywhere.
522enum SetOp {
523    Union,
524    Intersection,
525    Difference,
526    SymmetricDifference,
527}
528
529fn set_binop(left: &RuntimeValue, right: &RuntimeValue, op: SetOp) -> Result<RuntimeValue, String> {
530    let (RuntimeValue::Set(a), RuntimeValue::Set(b)) = (left, right) else {
531        return Err("set operation requires two Sets".to_string());
532    };
533    let (a, b) = (a.borrow(), b.borrow());
534    let contains = |xs: &[RuntimeValue], v: &RuntimeValue| xs.iter().any(|x| values_equal(x, v));
535    let mut out: Vec<RuntimeValue> = Vec::new();
536    match op {
537        SetOp::Union => {
538            out.extend(a.iter().cloned());
539            for v in b.iter() {
540                if !contains(&out, v) {
541                    out.push(v.clone());
542                }
543            }
544        }
545        SetOp::Intersection => {
546            for v in a.iter() {
547                if contains(&b, v) {
548                    out.push(v.clone());
549                }
550            }
551        }
552        SetOp::Difference => {
553            for v in a.iter() {
554                if !contains(&b, v) {
555                    out.push(v.clone());
556                }
557            }
558        }
559        SetOp::SymmetricDifference => {
560            for v in a.iter() {
561                if !contains(&b, v) {
562                    out.push(v.clone());
563                }
564            }
565            for v in b.iter() {
566                if !contains(&a, v) {
567                    out.push(v.clone());
568                }
569            }
570        }
571    }
572    Ok(RuntimeValue::Set(Rc::new(std::cell::RefCell::new(out))))
573}
574
575/// `a is approximately b` — the TOLERANT numeric comparison (`==` is IEEE
576/// bit-exact). Both operands coerce to f64 (approximation is inherently
577/// tolerant, so the lossy view is correct here) and compare with the ONE
578/// shared isclose definition (`logicaffeine_data::ops::logos_approx_eq`).
579pub fn approx_eq(left: RuntimeValue, right: RuntimeValue) -> Result<RuntimeValue, String> {
580    let as_f64 = |v: &RuntimeValue| -> Option<f64> {
581        match v {
582            RuntimeValue::Float(f) => Some(*f),
583            RuntimeValue::Int(n) => Some(*n as f64),
584            RuntimeValue::BigInt(b) => Some(b.to_f64()),
585            RuntimeValue::Rational(r) => Some(r.to_f64()),
586            _ => None,
587        }
588    };
589    match (as_f64(&left), as_f64(&right)) {
590        (Some(a), Some(b)) => Ok(RuntimeValue::Bool(logicaffeine_data::ops::logos_approx_eq(a, b))),
591        _ => Err(format!(
592            "`is approximately` compares numbers, got {} and {}",
593            left.type_name(),
594            right.type_name()
595        )),
596    }
597}
598
599/// `a followed by b` — merge two sequences into one fresh sequence. Element order is preserved:
600/// all of `a`, then all of `b`. Operands must both be sequences.
601pub fn seq_concat(left: RuntimeValue, right: RuntimeValue) -> Result<RuntimeValue, String> {
602    use crate::interpreter::ListRepr;
603    match (&left, &right) {
604        (RuntimeValue::List(a), RuntimeValue::List(b)) => {
605            let mut items = a.borrow().to_values();
606            items.extend(b.borrow().to_values());
607            Ok(RuntimeValue::List(Rc::new(std::cell::RefCell::new(ListRepr::from_values(items)))))
608        }
609        _ => Err("`followed by` requires two sequences (merge two sequences into one)".to_string()),
610    }
611}
612
613pub fn subtract(left: RuntimeValue, right: RuntimeValue) -> Result<RuntimeValue, String> {
614    // `a - b` on Sets is the difference (`a without b` is the English form).
615    if matches!((&left, &right), (RuntimeValue::Set(_), RuntimeValue::Set(_))) {
616        return set_binop(&left, &right, SetOp::Difference);
617    }
618    if let (RuntimeValue::Word(a), RuntimeValue::Word(b)) = (&left, &right) {
619        return a.sub(*b).map(RuntimeValue::Word).ok_or_else(|| word_width_err(*a, *b));
620    }
621    // SIMD lane vectors subtract lane-wise (the NTT butterfly's `a - t` over Word16 lanes).
622    if let (RuntimeValue::Lanes(a), RuntimeValue::Lanes(b)) = (&left, &right) {
623        return a.sub(**b).map(|v| RuntimeValue::Lanes(std::rc::Rc::new(v))).ok_or_else(|| {
624            format!("cannot subtract {} and {} — lane-config mismatch", a.type_name(), b.type_name())
625        });
626    }
627    if let Some(r) = quantity_binop(&left, &right, '-') {
628        return r;
629    }
630    if let Some(r) = money_binop(&left, &right, '-') {
631        return r;
632    }
633    if let Some(r) = modular_binop(&left, &right, "subtract", |a, b| a.sub(b)) {
634        return r;
635    }
636    if let Some(r) = complex_binop(&left, &right, "subtract", |a, b| a.sub(b)) {
637        return r;
638    }
639    if let Some(r) = decimal_binop(&left, &right, |a, b| a.sub(b), |a, b| a.sub(b), |a, b| a - b) {
640        return Ok(r);
641    }
642    match (&left, &right) {
643        (RuntimeValue::Int(a), RuntimeValue::Int(b)) => Ok(match a.checked_sub(*b) {
644            Some(s) => RuntimeValue::Int(s),
645            None => RuntimeValue::from_bigint(BigInt::from_i64(*a).sub(&BigInt::from_i64(*b))),
646        }),
647        (RuntimeValue::BigInt(a), RuntimeValue::BigInt(b)) => Ok(RuntimeValue::from_bigint(a.sub(b))),
648        (RuntimeValue::BigInt(a), RuntimeValue::Int(b)) => {
649            Ok(RuntimeValue::from_bigint(a.sub(&BigInt::from_i64(*b))))
650        }
651        (RuntimeValue::Int(a), RuntimeValue::BigInt(b)) => {
652            Ok(RuntimeValue::from_bigint(BigInt::from_i64(*a).sub(b)))
653        }
654        (RuntimeValue::Float(a), RuntimeValue::Float(b)) => Ok(RuntimeValue::Float(a - b)),
655        (RuntimeValue::Int(a), RuntimeValue::Float(b)) => Ok(RuntimeValue::Float(*a as f64 - b)),
656        (RuntimeValue::Float(a), RuntimeValue::Int(b)) => Ok(RuntimeValue::Float(a - *b as f64)),
657        (RuntimeValue::BigInt(a), RuntimeValue::Float(b)) => Ok(RuntimeValue::Float(a.to_f64() - b)),
658        (RuntimeValue::Float(a), RuntimeValue::BigInt(b)) => Ok(RuntimeValue::Float(a - b.to_f64())),
659        (RuntimeValue::Rational(a), RuntimeValue::Rational(b)) => {
660            Ok(RuntimeValue::from_rational(a.sub(b)))
661        }
662        (RuntimeValue::Rational(_), RuntimeValue::Int(_))
663        | (RuntimeValue::Int(_), RuntimeValue::Rational(_))
664        | (RuntimeValue::Rational(_), RuntimeValue::BigInt(_))
665        | (RuntimeValue::BigInt(_), RuntimeValue::Rational(_)) => {
666            Ok(RuntimeValue::from_rational(rat_of(&left).unwrap().sub(&rat_of(&right).unwrap())))
667        }
668        (RuntimeValue::Rational(r), RuntimeValue::Float(b)) => Ok(RuntimeValue::Float(r.to_f64() - b)),
669        (RuntimeValue::Float(a), RuntimeValue::Rational(r)) => Ok(RuntimeValue::Float(a - r.to_f64())),
670        (RuntimeValue::Duration(a), RuntimeValue::Duration(b)) => {
671            Ok(RuntimeValue::Duration(a.wrapping_sub(*b)))
672        }
673        (RuntimeValue::Date(days), RuntimeValue::Span { months, days: span_days }) => {
674            Ok(RuntimeValue::Date(date_add_span(*days, -*months, -*span_days)))
675        }
676        // A Moment minus a CIVIL Span steps the calendar backward (`m - 1 month`), mirroring the add.
677        (RuntimeValue::Moment(nanos), RuntimeValue::Span { months, days: span_days }) => Ok(
678            RuntimeValue::Moment(super::temporal::moment_add_span(*nanos, -*months, -*span_days)),
679        ),
680        // A Moment minus a Duration is an earlier Moment. (Moment − Moment → elapsed Duration is
681        // deferred until Duration has a signed i64-nanos AOT representation, so the tiers stay in
682        // lock-step; `the seconds between a and b` already covers elapsed time naturally.)
683        (RuntimeValue::Moment(nanos), RuntimeValue::Duration(d)) => {
684            Ok(RuntimeValue::Moment(nanos.wrapping_sub(*d)))
685        }
686        _ => Err(format!(
687            "Cannot subtract {} from {}",
688            right.type_name(),
689            left.type_name()
690        )),
691    }
692}
693
694/// `base ** exp` — exponentiation. Integer power is EXACT (i64 fast path,
695/// promoting to BigInt on overflow); a Float operand takes the `f64::powf`
696/// path; a Rational base with an integer exponent stays exact; a NEGATIVE
697/// integer exponent on an integer base is a loud error (an Int can't hold the
698/// fractional result — use a Float base). ONE definition, shared by tw/VM/JIT.
699pub fn power(base: RuntimeValue, exp: RuntimeValue) -> Result<RuntimeValue, String> {
700    // Any Float operand → inexact f64 power.
701    if matches!(base, RuntimeValue::Float(_)) || matches!(exp, RuntimeValue::Float(_)) {
702        let b = num_f64(&base)
703            .ok_or_else(|| format!("cannot raise {} to a power", base.type_name()))?;
704        let e = num_f64(&exp)
705            .ok_or_else(|| format!("cannot raise to a {} power", exp.type_name()))?;
706        return Ok(RuntimeValue::Float(b.powf(e)));
707    }
708    match (&base, &exp) {
709        (RuntimeValue::Int(b), RuntimeValue::Int(e)) => int_power(*b, *e),
710        (RuntimeValue::BigInt(b), RuntimeValue::Int(e)) => {
711            let ue = u32::try_from(*e)
712                .map_err(|_| "negative or too-large exponent on an integer (use a Float base)".to_string())?;
713            Ok(RuntimeValue::from_bigint(b.pow(ue)))
714        }
715        (RuntimeValue::Rational(b), RuntimeValue::Int(e)) => {
716            let ie = i32::try_from(*e).map_err(|_| "exponent too large".to_string())?;
717            b.pow(ie)
718                .map(RuntimeValue::from_rational)
719                .ok_or_else(|| "zero raised to a negative power".to_string())
720        }
721        _ => Err(format!(
722            "cannot raise {} to the {} power",
723            base.type_name(),
724            exp.type_name()
725        )),
726    }
727}
728
729/// Exact `base^exp` for non-negative integer exponents; promotes to BigInt on
730/// i64 overflow. A negative exponent is a loud error.
731fn int_power(base: i64, exp: i64) -> Result<RuntimeValue, String> {
732    if exp < 0 {
733        return Err(
734            "negative exponent on an integer (an Int can't hold a fraction — use a Float base)"
735                .to_string(),
736        );
737    }
738    let e = u32::try_from(exp).map_err(|_| "exponent too large".to_string())?;
739    match base.checked_pow(e) {
740        Some(r) => Ok(RuntimeValue::Int(r)),
741        None => Ok(RuntimeValue::from_bigint(BigInt::from_i64(base).pow(e))),
742    }
743}
744
745pub fn multiply(left: RuntimeValue, right: RuntimeValue) -> Result<RuntimeValue, String> {
746    if let (RuntimeValue::Word(a), RuntimeValue::Word(b)) = (&left, &right) {
747        return a.mul(*b).map(RuntimeValue::Word).ok_or_else(|| word_width_err(*a, *b));
748    }
749    // SIMD lane vectors multiply lane-wise low-16 (`vpmullw`, the NTT's `*` over Word16 lanes).
750    if let (RuntimeValue::Lanes(a), RuntimeValue::Lanes(b)) = (&left, &right) {
751        return a.mullo(**b).map(|v| RuntimeValue::Lanes(std::rc::Rc::new(v))).ok_or_else(|| {
752            format!("cannot multiply {} and {} — lane op undefined", a.type_name(), b.type_name())
753        });
754    }
755    if let Some(r) = quantity_binop(&left, &right, '*') {
756        return r;
757    }
758    if let Some(r) = money_binop(&left, &right, '*') {
759        return r;
760    }
761    if let Some(r) = modular_binop(&left, &right, "multiply", |a, b| a.mul(b)) {
762        return r;
763    }
764    if let Some(r) = complex_binop(&left, &right, "multiply", |a, b| a.mul(b)) {
765        return r;
766    }
767    if let Some(r) = decimal_binop(&left, &right, |a, b| a.mul(b), |a, b| a.mul(b), |a, b| a * b) {
768        return Ok(r);
769    }
770    match (&left, &right) {
771        (RuntimeValue::Int(a), RuntimeValue::Int(b)) => Ok(match a.checked_mul(*b) {
772            Some(p) => RuntimeValue::Int(p),
773            None => RuntimeValue::from_bigint(BigInt::from_i64(*a).mul(&BigInt::from_i64(*b))),
774        }),
775        (RuntimeValue::BigInt(a), RuntimeValue::BigInt(b)) => Ok(RuntimeValue::from_bigint(a.mul(b))),
776        (RuntimeValue::BigInt(a), RuntimeValue::Int(b)) => {
777            Ok(RuntimeValue::from_bigint(a.mul(&BigInt::from_i64(*b))))
778        }
779        (RuntimeValue::Int(a), RuntimeValue::BigInt(b)) => {
780            Ok(RuntimeValue::from_bigint(BigInt::from_i64(*a).mul(b)))
781        }
782        (RuntimeValue::Float(a), RuntimeValue::Float(b)) => Ok(RuntimeValue::Float(a * b)),
783        (RuntimeValue::Int(a), RuntimeValue::Float(b)) => Ok(RuntimeValue::Float(*a as f64 * b)),
784        (RuntimeValue::Float(a), RuntimeValue::Int(b)) => Ok(RuntimeValue::Float(a * *b as f64)),
785        (RuntimeValue::BigInt(a), RuntimeValue::Float(b)) => Ok(RuntimeValue::Float(a.to_f64() * b)),
786        (RuntimeValue::Float(a), RuntimeValue::BigInt(b)) => Ok(RuntimeValue::Float(a * b.to_f64())),
787        (RuntimeValue::Rational(a), RuntimeValue::Rational(b)) => {
788            Ok(RuntimeValue::from_rational(a.mul(b)))
789        }
790        (RuntimeValue::Rational(_), RuntimeValue::Int(_))
791        | (RuntimeValue::Int(_), RuntimeValue::Rational(_))
792        | (RuntimeValue::Rational(_), RuntimeValue::BigInt(_))
793        | (RuntimeValue::BigInt(_), RuntimeValue::Rational(_)) => {
794            Ok(RuntimeValue::from_rational(rat_of(&left).unwrap().mul(&rat_of(&right).unwrap())))
795        }
796        (RuntimeValue::Rational(r), RuntimeValue::Float(b)) => Ok(RuntimeValue::Float(r.to_f64() * b)),
797        (RuntimeValue::Float(a), RuntimeValue::Rational(r)) => Ok(RuntimeValue::Float(a * r.to_f64())),
798        // `xs * n` / `n * xs` — repeat a sequence n times into a FRESH
799        // sequence. Each slot deep-copies its element (a repeated inner
800        // collection is n INDEPENDENT rows, never n aliases of one — the
801        // classic `[[0]]*3` footgun is designed out). n ≤ 0 is empty.
802        (RuntimeValue::List(items), RuntimeValue::Int(n))
803        | (RuntimeValue::Int(n), RuntimeValue::List(items)) => {
804            use crate::interpreter::ListRepr;
805            let src = items.borrow().to_values();
806            let count = (*n).max(0) as usize;
807            let mut out = Vec::with_capacity(src.len() * count);
808            for _ in 0..count {
809                out.extend(src.iter().map(|v| v.deep_clone()));
810            }
811            Ok(RuntimeValue::List(Rc::new(std::cell::RefCell::new(ListRepr::from_values(out)))))
812        }
813        _ => Err(format!(
814            "Cannot multiply {} and {}",
815            left.type_name(),
816            right.type_name()
817        )),
818    }
819}
820
821pub fn divide(left: RuntimeValue, right: RuntimeValue) -> Result<RuntimeValue, String> {
822    if let Some(r) = quantity_binop(&left, &right, '/') {
823        return r;
824    }
825    if let Some(r) = money_binop(&left, &right, '/') {
826        return r;
827    }
828    // Modular division = multiply by the modular inverse: the operands must share a modulus
829    // and the divisor must be a unit (coprime to the modulus), else there is no inverse.
830    if matches!(left, RuntimeValue::Modular(_)) || matches!(right, RuntimeValue::Modular(_)) {
831        return match (&left, &right) {
832            (RuntimeValue::Modular(a), RuntimeValue::Modular(b)) => {
833                if a.modulus() != b.modulus() {
834                    Err("cannot divide values in different modular rings".to_string())
835                } else {
836                    a.div(b).map(|r| RuntimeValue::Modular(Rc::new(r))).ok_or_else(|| {
837                        "modular divisor has no inverse (not coprime to the modulus)".to_string()
838                    })
839                }
840            }
841            _ => Err(format!("Cannot divide {} by {}", left.type_name(), right.type_name())),
842        };
843    }
844    // Complex division stays Complex (the field is closed); `None` on a zero divisor.
845    if matches!(left, RuntimeValue::Complex(_)) || matches!(right, RuntimeValue::Complex(_)) {
846        return match (complex_of(&left), complex_of(&right)) {
847            (Some(a), Some(b)) => a
848                .div(&b)
849                .map(|c| RuntimeValue::Complex(Rc::new(c)))
850                .ok_or_else(|| "Division by zero".to_string()),
851            _ => Err(format!("Cannot divide {} by {}", left.type_name(), right.type_name())),
852        };
853    }
854    // A Decimal divides EXACTLY into the Rational tower (base-10 division need not
855    // terminate, so it does not stay a Decimal); a Float operand divides as Float.
856    if matches!(left, RuntimeValue::Decimal(_)) || matches!(right, RuntimeValue::Decimal(_)) {
857        if let (Some(a), Some(b)) = (rat_of(&left), rat_of(&right)) {
858            return a
859                .div(&b)
860                .map(RuntimeValue::from_rational)
861                .ok_or_else(|| "Division by zero".to_string());
862        }
863        if let (Some(a), Some(b)) = (num_f64(&left), num_f64(&right)) {
864            if b == 0.0 {
865                return Err("Division by zero".to_string());
866            }
867            return Ok(RuntimeValue::Float(a / b));
868        }
869    }
870    match (&left, &right) {
871        (RuntimeValue::Int(a), RuntimeValue::Int(b)) => {
872            if *b == 0 {
873                return Err("Division by zero".to_string());
874            }
875            // checked_div is None only for i64::MIN / -1, whose true quotient
876            // (2^63) overflows i64 → promote rather than wrap.
877            Ok(match a.checked_div(*b) {
878                Some(q) => RuntimeValue::Int(q),
879                None => RuntimeValue::from_bigint(int_div(*a, *b).0),
880            })
881        }
882        (RuntimeValue::BigInt(a), RuntimeValue::BigInt(b)) => {
883            Ok(RuntimeValue::from_bigint(a.div_rem(b).expect("BigInt divisor is never zero").0))
884        }
885        (RuntimeValue::BigInt(a), RuntimeValue::Int(b)) => {
886            if *b == 0 {
887                return Err("Division by zero".to_string());
888            }
889            Ok(RuntimeValue::from_bigint(a.div_rem(&BigInt::from_i64(*b)).unwrap().0))
890        }
891        (RuntimeValue::Int(a), RuntimeValue::BigInt(b)) => {
892            Ok(RuntimeValue::from_bigint(BigInt::from_i64(*a).div_rem(b).unwrap().0))
893        }
894        (RuntimeValue::Float(a), RuntimeValue::Float(b)) => {
895            if *b == 0.0 {
896                return Err("Division by zero".to_string());
897            }
898            Ok(RuntimeValue::Float(a / b))
899        }
900        (RuntimeValue::Int(a), RuntimeValue::Float(b)) => {
901            if *b == 0.0 {
902                return Err("Division by zero".to_string());
903            }
904            Ok(RuntimeValue::Float(*a as f64 / b))
905        }
906        (RuntimeValue::Float(a), RuntimeValue::Int(b)) => {
907            if *b == 0 {
908                return Err("Division by zero".to_string());
909            }
910            Ok(RuntimeValue::Float(a / *b as f64))
911        }
912        (RuntimeValue::BigInt(a), RuntimeValue::Float(b)) => {
913            if *b == 0.0 {
914                return Err("Division by zero".to_string());
915            }
916            Ok(RuntimeValue::Float(a.to_f64() / b))
917        }
918        (RuntimeValue::Float(a), RuntimeValue::BigInt(b)) => Ok(RuntimeValue::Float(a / b.to_f64())),
919        // Exact division on Rational operands: the quotient is exact (downsized to an
920        // Int when it reduces to a whole number). A Float operand makes it Float.
921        (RuntimeValue::Rational(_), RuntimeValue::Rational(_))
922        | (RuntimeValue::Rational(_), RuntimeValue::Int(_))
923        | (RuntimeValue::Int(_), RuntimeValue::Rational(_))
924        | (RuntimeValue::Rational(_), RuntimeValue::BigInt(_))
925        | (RuntimeValue::BigInt(_), RuntimeValue::Rational(_)) => rat_of(&left)
926            .unwrap()
927            .div(&rat_of(&right).unwrap())
928            .map(RuntimeValue::from_rational)
929            .ok_or_else(|| "Division by zero".to_string()),
930        (RuntimeValue::Rational(r), RuntimeValue::Float(b)) => {
931            if *b == 0.0 {
932                return Err("Division by zero".to_string());
933            }
934            Ok(RuntimeValue::Float(r.to_f64() / b))
935        }
936        (RuntimeValue::Float(a), RuntimeValue::Rational(r)) => Ok(RuntimeValue::Float(a / r.to_f64())),
937        _ => Err(format!(
938            "Cannot divide {} by {}",
939            left.type_name(),
940            right.type_name()
941        )),
942    }
943}
944
945/// Exact integer division of two `i64`s as a BigInt `(quotient, remainder)` — used on
946/// the one overflowing case, `i64::MIN / -1`.
947fn int_div(a: i64, b: i64) -> (BigInt, BigInt) {
948    BigInt::from_i64(a).div_rem(&BigInt::from_i64(b)).expect("nonzero divisor")
949}
950
951/// FLOOR division — the runtime of [`BinaryOpKind::FloorDivide`] (`a // b`), the quotient
952/// rounded toward NEGATIVE INFINITY. On exact operands (Int/BigInt/Rational/Decimal) the
953/// result is the exact quotient's floor as an integer (`-7 // 2 → -4`, `10^30 // 7` exact);
954/// a Float operand floors the float quotient but stays Float (`7.5 // 2 → 3.0`, Python
955/// semantics). Distinct from [`divide`], which truncates toward zero. Zero divisor is loud.
956pub fn floor_divide(left: RuntimeValue, right: RuntimeValue) -> Result<RuntimeValue, String> {
957    // Float domain: floor the float quotient, staying inexact.
958    if matches!(left, RuntimeValue::Float(_)) || matches!(right, RuntimeValue::Float(_)) {
959        if let (Some(a), Some(b)) = (num_f64(&left), num_f64(&right)) {
960            if b == 0.0 {
961                return Err("Division by zero".to_string());
962            }
963            return Ok(RuntimeValue::Float((a / b).floor()));
964        }
965    }
966    // Exact domain: the exact rational quotient, floored to an integer (BigInt-promoting,
967    // downsized to Int when it fits). `rat_of` covers Int/BigInt/Rational/Decimal.
968    if let (Some(a), Some(b)) = (rat_of(&left), rat_of(&right)) {
969        return a
970            .div(&b)
971            .map(|q| RuntimeValue::from_bigint(q.floor()))
972            .ok_or_else(|| "Division by zero".to_string());
973    }
974    Err(format!(
975        "Cannot floor-divide {} by {}",
976        left.type_name(),
977        right.type_name()
978    ))
979}
980
981/// EXACT division — the runtime of [`BinaryOpKind::ExactDivide`], the type-directed
982/// sibling of [`divide`]. An evenly-dividing integer pair stays an `Int`/`BigInt`;
983/// otherwise the quotient is an exact `Rational` (`7 / 2 → 7/2`) — it NEVER truncates.
984/// A `Float` operand divides as `Float` (floats are inexact by choice). This only ever
985/// runs where the type says the result is a `Rational`, so floor code is untouched.
986pub fn exact_divide(left: RuntimeValue, right: RuntimeValue) -> Result<RuntimeValue, String> {
987    match (&left, &right) {
988        // Every Int/BigInt/Rational combination divides to an EXACT value — downsized to
989        // an Int/BigInt when it reduces to a whole number, else an exact Rational.
990        (a, b) if rat_of(a).is_some() && rat_of(b).is_some() => rat_of(&left)
991            .unwrap()
992            .div(&rat_of(&right).unwrap())
993            .map(RuntimeValue::from_rational)
994            .ok_or_else(|| "Division by zero".to_string()),
995        // A Float operand makes it Float — defer to the Float-aware `divide` (whose Float
996        // arms don't truncate anyway).
997        (RuntimeValue::Float(_), _) | (_, RuntimeValue::Float(_)) => divide(left, right),
998        _ => Err(format!("Cannot divide {} by {}", left.type_name(), right.type_name())),
999    }
1000}
1001
1002pub fn modulo(left: RuntimeValue, right: RuntimeValue) -> Result<RuntimeValue, String> {
1003    match (&left, &right) {
1004        (RuntimeValue::Int(a), RuntimeValue::Int(b)) => {
1005            if *b == 0 {
1006                return Err("Modulo by zero".to_string());
1007            }
1008            // checked_rem is None only for i64::MIN % -1, whose true remainder is 0.
1009            Ok(RuntimeValue::Int(a.checked_rem(*b).unwrap_or(0)))
1010        }
1011        (RuntimeValue::BigInt(a), RuntimeValue::BigInt(b)) => {
1012            Ok(RuntimeValue::from_bigint(a.div_rem(b).expect("BigInt divisor is never zero").1))
1013        }
1014        (RuntimeValue::BigInt(a), RuntimeValue::Int(b)) => {
1015            if *b == 0 {
1016                return Err("Modulo by zero".to_string());
1017            }
1018            Ok(RuntimeValue::from_bigint(a.div_rem(&BigInt::from_i64(*b)).unwrap().1))
1019        }
1020        (RuntimeValue::Int(a), RuntimeValue::BigInt(b)) => {
1021            Ok(RuntimeValue::from_bigint(BigInt::from_i64(*a).div_rem(b).unwrap().1))
1022        }
1023        _ => Err(format!(
1024            "Cannot compute modulo of {} and {}",
1025            left.type_name(),
1026            right.type_name()
1027        )),
1028    }
1029}
1030
1031/// One CRDT counter step: `current + amount`. A CRDT grow-counter is intentionally
1032/// `wrapping` (modular) — convergence is defined over the cyclic group, so overflow
1033/// is a feature here, NOT the silent-corruption footgun that ordinary integer math
1034/// now promotes away. An absent or Nothing field starts from zero.
1035pub fn crdt_counter_bump(
1036    current: RuntimeValue,
1037    amount: i64,
1038    field_name: &str,
1039) -> Result<RuntimeValue, String> {
1040    match current {
1041        RuntimeValue::Int(n) => Ok(RuntimeValue::Int(n.wrapping_add(amount))),
1042        RuntimeValue::Nothing => Ok(RuntimeValue::Int(amount)),
1043        _ => Err(format!("Field '{}' is not a counter", field_name)),
1044    }
1045}
1046
1047/// Merge one CRDT field by its convergence rule:
1048/// - `Int` + `Int` — a grow-counter, so add (intentionally `wrapping`/modular);
1049/// - `Set` + `Set` — a grow-set, so union (dedup by value);
1050/// - anything else (LWW register: Bool/Text/Float/…) — last writer wins ⇒ take
1051///   the incoming value.
1052/// The struct type tag the interpreter uses for a state-based, gossip-safe G-Counter: a
1053/// map of `replica id → that replica's monotonic count`. Distinct from a plain counter
1054/// (a bare `Int` whose op-based `add` merge is NOT idempotent under redelivery).
1055pub const GCOUNTER_TAG: &str = "__GCounter";
1056
1057/// The total of a [`GCOUNTER_TAG`] counter — the sum of every replica's count. `None` for
1058/// a value that is not a G-Counter.
1059pub fn gcounter_value(v: &RuntimeValue) -> Option<i64> {
1060    match v {
1061        RuntimeValue::Struct(s) if s.type_name == GCOUNTER_TAG => Some(
1062            s.fields
1063                .values()
1064                .map(|c| if let RuntimeValue::Int(n) = c { *n } else { 0 })
1065                .fold(0i64, i64::wrapping_add),
1066        ),
1067        _ => None,
1068    }
1069}
1070
1071pub fn crdt_merge_field(current: &RuntimeValue, incoming: RuntimeValue) -> RuntimeValue {
1072    match (current, &incoming) {
1073        // A state-based G-Counter: per-replica MAX. Unlike the bare-Int `add` below, MAX is
1074        // IDEMPOTENT (max of a value with itself is itself), so a redelivered or duplicated
1075        // counter state never double-counts — gossip/lossy-network safe. Commutative and
1076        // associative too, so all replicas converge to the same total.
1077        (RuntimeValue::Struct(a), RuntimeValue::Struct(b))
1078            if a.type_name == GCOUNTER_TAG && b.type_name == GCOUNTER_TAG =>
1079        {
1080            let mut fields = a.fields.clone();
1081            for (replica, count) in &b.fields {
1082                let next = match (fields.get(replica), count) {
1083                    (Some(RuntimeValue::Int(x)), RuntimeValue::Int(y)) => RuntimeValue::Int((*x).max(*y)),
1084                    _ => count.clone(),
1085                };
1086                fields.insert(replica.clone(), next);
1087            }
1088            RuntimeValue::Struct(Box::new(crate::interpreter::StructValue {
1089                type_name: GCOUNTER_TAG.to_string(),
1090                fields,
1091            }))
1092        }
1093        (RuntimeValue::Int(a), RuntimeValue::Int(b)) => RuntimeValue::Int(a.wrapping_add(*b)),
1094        (RuntimeValue::Set(_), RuntimeValue::Set(_)) => {
1095            crate::semantics::collections::union(current, &incoming).unwrap_or(incoming)
1096        }
1097        // A CRDT MAP — "shared memory" over the network: the union of keys, each shared
1098        // key's values merged RECURSIVELY through this same join. The merge inherits the
1099        // value type's laws: a map of sets (or of nested maps) is commutative, associative,
1100        // AND idempotent, so replicas converge no matter the order or duplication of merges.
1101        (RuntimeValue::Map(a), RuntimeValue::Map(b)) => {
1102            let mut out = a.borrow().clone();
1103            for (k, v) in b.borrow().iter() {
1104                let merged = match out.get(k) {
1105                    Some(cur) => crdt_merge_field(cur, v.clone()),
1106                    None => v.clone(),
1107                };
1108                out.insert(k.clone(), merged);
1109            }
1110            RuntimeValue::Map(std::rc::Rc::new(std::cell::RefCell::new(out)))
1111        }
1112        // A live CRDT (OR-Set / RGA / MV-register) converges through its own state-based
1113        // join. The result is a FRESH replica (the current state deep-copied, then the
1114        // incoming state merged into it), so neither operand is aliased into the result —
1115        // the join inherits the data-crate type's commutativity/associativity/idempotence.
1116        (RuntimeValue::Crdt(a), RuntimeValue::Crdt(b)) => {
1117            let mut merged = a.borrow().clone();
1118            let _ = merged.merge(&b.borrow());
1119            RuntimeValue::Crdt(std::rc::Rc::new(std::cell::RefCell::new(merged)))
1120        }
1121        _ => incoming,
1122    }
1123}
1124
1125/// One CRDT field value → its JSON wire form. Scalars map directly; a `Set`
1126/// becomes a JSON array of its (scalar) members. Returns `None` for values that
1127/// are not CRDT-syncable.
1128fn field_to_json(value: &RuntimeValue) -> Option<serde_json::Value> {
1129    use serde_json::{json, Value};
1130    Some(match value {
1131        RuntimeValue::Int(n) => json!(n),
1132        RuntimeValue::Bool(b) => json!(b),
1133        RuntimeValue::Float(f) => json!(f),
1134        RuntimeValue::Text(s) => json!(s.as_str()),
1135        RuntimeValue::Nothing => Value::Null,
1136        RuntimeValue::Set(items) => {
1137            Value::Array(items.borrow().iter().filter_map(field_to_json).collect())
1138        }
1139        // A map ships as a TAGGED array of [key, value] pairs so it is unambiguous from a
1140        // plain Set array, and any key type (not just strings) round-trips.
1141        RuntimeValue::Map(m) => {
1142            let pairs: Vec<Value> = m
1143                .borrow()
1144                .iter()
1145                .filter_map(|(k, v)| Some(Value::Array(vec![field_to_json(k)?, field_to_json(v)?])))
1146                .collect();
1147            json!({ "__map": pairs })
1148        }
1149        _ => return None,
1150    })
1151}
1152
1153/// A JSON wire value → a CRDT field value. The inverse of [`field_to_json`]; a
1154/// JSON array reconstructs a `Set`.
1155fn field_from_json(value: &serde_json::Value) -> RuntimeValue {
1156    use serde_json::Value;
1157    match value {
1158        Value::Bool(b) => RuntimeValue::Bool(*b),
1159        Value::String(s) => RuntimeValue::Text(std::rc::Rc::new(s.clone())),
1160        Value::Number(n) => match n.as_i64() {
1161            Some(i) => RuntimeValue::Int(i),
1162            None => RuntimeValue::Float(n.as_f64().unwrap_or(0.0)),
1163        },
1164        Value::Array(items) => RuntimeValue::Set(std::rc::Rc::new(std::cell::RefCell::new(
1165            items.iter().map(field_from_json).collect(),
1166        ))),
1167        // A tagged `{"__map": [[k,v],…]}` reconstructs a map (the inverse of `field_to_json`).
1168        Value::Object(o) if o.contains_key("__map") => {
1169            let mut map = crate::interpreter::MapStorage::default();
1170            if let Some(Value::Array(pairs)) = o.get("__map") {
1171                for p in pairs {
1172                    if let Value::Array(kv) = p {
1173                        if kv.len() == 2 {
1174                            map.insert(field_from_json(&kv[0]), field_from_json(&kv[1]));
1175                        }
1176                    }
1177                }
1178            }
1179            RuntimeValue::Map(std::rc::Rc::new(std::cell::RefCell::new(map)))
1180        }
1181        _ => RuntimeValue::Nothing,
1182    }
1183}
1184
1185/// Encode a CRDT value for the relay wire: a JSON object mapping each Int field
1186/// to its value. A bare `Int` counter uses the empty field name. `Nothing` (and
1187/// any non-counter value) has nothing to publish. The format is browser-friendly
1188/// and field-addressable, so structs merge field-by-field on the other side.
1189pub fn crdt_to_wire(value: &RuntimeValue) -> Option<Vec<u8>> {
1190    use serde_json::{Map, Value};
1191    let mut map = Map::new();
1192    match value {
1193        RuntimeValue::Nothing => return None,
1194        RuntimeValue::Struct(s) => {
1195            for (k, v) in &s.fields {
1196                if let Some(j) = field_to_json(v) {
1197                    map.insert(k.clone(), j);
1198                }
1199            }
1200        }
1201        // A bare counter / register / set uses the unnamed field.
1202        other => match field_to_json(other) {
1203            Some(j) => {
1204                map.insert(String::new(), j);
1205            }
1206            None => return None,
1207        },
1208    }
1209    serde_json::to_vec(&Value::Object(map)).ok()
1210}
1211
1212/// Merge a wire-encoded CRDT value (from [`crdt_to_wire`]) into `local`, field by
1213/// field through [`crdt_merge_field`] — counters add, sets union, registers take
1214/// the latest. A struct merges each named field; a bare value the unnamed field.
1215/// Malformed bytes leave `local` unchanged.
1216pub fn crdt_merge_wire(local: RuntimeValue, bytes: &[u8]) -> RuntimeValue {
1217    let Ok(serde_json::Value::Object(map)) = serde_json::from_slice::<serde_json::Value>(bytes)
1218    else {
1219        return local;
1220    };
1221    match local {
1222        RuntimeValue::Struct(mut s) => {
1223            for (k, v) in map {
1224                let incoming = field_from_json(&v);
1225                let current = s.fields.get(&k).cloned().unwrap_or(RuntimeValue::Nothing);
1226                s.fields.insert(k, crdt_merge_field(&current, incoming));
1227            }
1228            RuntimeValue::Struct(s)
1229        }
1230        other => match map.get("") {
1231            Some(v) => crdt_merge_field(&other, field_from_json(v)),
1232            None => other,
1233        },
1234    }
1235}
1236
1237/// `not x` — logical negation of truthiness (Bool out). The bitwise complement is `~`.
1238pub fn not_value(val: RuntimeValue) -> Result<RuntimeValue, String> {
1239    Ok(RuntimeValue::Bool(!val.is_truthy()))
1240}
1241
1242#[cfg(test)]
1243mod tests {
1244    use super::*;
1245    use crate::interpreter::StructValue;
1246    use std::collections::HashMap;
1247
1248    #[test]
1249    fn money_arithmetic_and_comparison_are_exact_and_currency_safe() {
1250        use crate::ast::stmt::BinaryOpKind;
1251        use crate::semantics::builtins::{call_builtin, BuiltinId};
1252        use crate::semantics::compare::compare;
1253        let money = |s: &str, code: &str| {
1254            call_builtin(
1255                BuiltinId::Money,
1256                vec![
1257                    RuntimeValue::Decimal(Rc::new(logicaffeine_base::Decimal::parse(s).unwrap())),
1258                    RuntimeValue::Text(Rc::new(code.to_string())),
1259                ],
1260            )
1261            .unwrap()
1262        };
1263        let show = |v: &RuntimeValue| v.to_display_string();
1264
1265        // Construction + display at the currency's minor unit.
1266        assert_eq!(show(&money("19.99", "USD")), "19.99 USD");
1267        // Same-currency add/sub are EXACT (no float drift) and keep the currency.
1268        assert_eq!(show(&add(money("0.10", "USD"), money("0.20", "USD")).unwrap()), "0.30 USD");
1269        assert_eq!(show(&add(money("19.99", "USD"), money("5.00", "USD")).unwrap()), "24.99 USD");
1270        assert_eq!(show(&subtract(money("24.99", "USD"), money("5.00", "USD")).unwrap()), "19.99 USD");
1271        // Cross-currency add/sub are a typed error (no common meaning).
1272        assert!(add(money("5.00", "USD"), money("1.00", "EUR")).is_err());
1273        assert!(subtract(money("5.00", "USD"), money("1.00", "EUR")).is_err());
1274        // Scale by a number (commutes); divide a bill by a number.
1275        assert_eq!(show(&multiply(money("19.99", "USD"), RuntimeValue::Int(3)).unwrap()), "59.97 USD");
1276        assert_eq!(show(&multiply(RuntimeValue::Int(3), money("19.99", "USD")).unwrap()), "59.97 USD");
1277        assert_eq!(show(&divide(money("10.00", "USD"), RuntimeValue::Int(4)).unwrap()), "2.50 USD");
1278        // Same-currency ratio (30/10 = 3, narrows to Int); money × money is refused.
1279        assert!(matches!(
1280            divide(money("30.00", "USD"), money("10.00", "USD")).unwrap(),
1281            RuntimeValue::Int(3) | RuntimeValue::Rational(_)
1282        ));
1283        assert!(multiply(money("2.00", "USD"), money("2.00", "USD")).is_err());
1284        // Ordering within a currency; ordering across currencies is a typed error.
1285        assert_eq!(
1286            compare(BinaryOpKind::Gt, &money("5.00", "USD"), &money("1.00", "USD")).unwrap(),
1287            RuntimeValue::Bool(true)
1288        );
1289        assert!(compare(BinaryOpKind::Lt, &money("5.00", "USD"), &money("1.00", "EUR")).is_err());
1290        // `money()` refuses an inexact amount and an unknown currency.
1291        assert!(call_builtin(
1292            BuiltinId::Money,
1293            vec![RuntimeValue::Float(1.5), RuntimeValue::Text(Rc::new("USD".to_string()))]
1294        )
1295        .is_err());
1296        assert!(call_builtin(
1297            BuiltinId::Money,
1298            vec![RuntimeValue::Int(5), RuntimeValue::Text(Rc::new("XYZ".to_string()))]
1299        )
1300        .is_err());
1301    }
1302
1303    #[test]
1304    fn crdt_wire_int_into_nothing_takes_value() {
1305        let bytes = crdt_to_wire(&RuntimeValue::Int(7)).unwrap();
1306        assert!(matches!(crdt_merge_wire(RuntimeValue::Nothing, &bytes), RuntimeValue::Int(7)));
1307    }
1308
1309    #[test]
1310    fn crdt_wire_int_merge_adds() {
1311        let bytes = crdt_to_wire(&RuntimeValue::Int(5)).unwrap();
1312        assert!(matches!(crdt_merge_wire(RuntimeValue::Int(3), &bytes), RuntimeValue::Int(8)));
1313    }
1314
1315    #[test]
1316    fn crdt_wire_struct_merges_fieldwise() {
1317        let mut fields = HashMap::new();
1318        fields.insert("a".to_string(), RuntimeValue::Int(2));
1319        fields.insert("b".to_string(), RuntimeValue::Int(4));
1320        let incoming = RuntimeValue::Struct(Box::new(StructValue {
1321            type_name: "Counter".into(),
1322            fields,
1323        }));
1324        let bytes = crdt_to_wire(&incoming).unwrap();
1325
1326        let mut local_fields = HashMap::new();
1327        local_fields.insert("a".to_string(), RuntimeValue::Int(1)); // b absent ⇒ 0
1328        let local = RuntimeValue::Struct(Box::new(StructValue {
1329            type_name: "Counter".into(),
1330            fields: local_fields,
1331        }));
1332        match crdt_merge_wire(local, &bytes) {
1333            RuntimeValue::Struct(s) => {
1334                assert!(matches!(s.fields.get("a"), Some(RuntimeValue::Int(3))), "1 + 2");
1335                assert!(matches!(s.fields.get("b"), Some(RuntimeValue::Int(4))), "0 + 4");
1336            }
1337            other => panic!("expected a struct, got {other:?}"),
1338        }
1339    }
1340
1341    #[test]
1342    fn crdt_wire_nothing_has_nothing_to_publish() {
1343        assert!(crdt_to_wire(&RuntimeValue::Nothing).is_none());
1344    }
1345
1346    #[test]
1347    fn crdt_wire_bool_lww_takes_incoming() {
1348        let bytes = crdt_to_wire(&RuntimeValue::Bool(true)).unwrap();
1349        assert!(matches!(
1350            crdt_merge_wire(RuntimeValue::Bool(false), &bytes),
1351            RuntimeValue::Bool(true)
1352        ));
1353    }
1354
1355    #[test]
1356    fn crdt_wire_text_lww() {
1357        let bytes = crdt_to_wire(&RuntimeValue::Text(std::rc::Rc::new("hi".into()))).unwrap();
1358        match crdt_merge_wire(RuntimeValue::Nothing, &bytes) {
1359            RuntimeValue::Text(s) => assert_eq!(&*s, "hi"),
1360            other => panic!("expected Text, got {other:?}"),
1361        }
1362    }
1363
1364    #[test]
1365    fn crdt_wire_float_roundtrips() {
1366        let bytes = crdt_to_wire(&RuntimeValue::Float(2.5)).unwrap();
1367        match crdt_merge_wire(RuntimeValue::Nothing, &bytes) {
1368            RuntimeValue::Float(f) => assert_eq!(f, 2.5),
1369            other => panic!("expected Float, got {other:?}"),
1370        }
1371    }
1372
1373    fn set_of(ns: &[i64]) -> RuntimeValue {
1374        RuntimeValue::Set(std::rc::Rc::new(std::cell::RefCell::new(
1375            ns.iter().map(|n| RuntimeValue::Int(*n)).collect(),
1376        )))
1377    }
1378
1379    #[test]
1380    fn crdt_wire_set_unions() {
1381        let bytes = crdt_to_wire(&set_of(&[2, 3])).unwrap();
1382        match crdt_merge_wire(set_of(&[1, 2]), &bytes) {
1383            RuntimeValue::Set(items) => {
1384                let v = items.borrow();
1385                assert_eq!(v.len(), 3, "{{1,2}} ∪ {{2,3}} = {{1,2,3}}");
1386                for n in [1, 2, 3] {
1387                    assert!(
1388                        v.iter().any(|x| matches!(x, RuntimeValue::Int(m) if *m == n)),
1389                        "missing {n}"
1390                    );
1391                }
1392            }
1393            other => panic!("expected Set, got {other:?}"),
1394        }
1395    }
1396
1397    #[test]
1398    fn crdt_wire_struct_mixed_types_merge_each_by_its_rule() {
1399        let mut fields = HashMap::new();
1400        fields.insert("hits".to_string(), RuntimeValue::Int(3));
1401        fields.insert("title".to_string(), RuntimeValue::Text(std::rc::Rc::new("v2".into())));
1402        fields.insert("tags".to_string(), set_of(&[9]));
1403        let incoming = RuntimeValue::Struct(Box::new(StructValue {
1404            type_name: "Page".into(),
1405            fields,
1406        }));
1407        let bytes = crdt_to_wire(&incoming).unwrap();
1408
1409        let mut local = HashMap::new();
1410        local.insert("hits".to_string(), RuntimeValue::Int(1));
1411        local.insert("title".to_string(), RuntimeValue::Text(std::rc::Rc::new("v1".into())));
1412        local.insert("tags".to_string(), set_of(&[7]));
1413        let local = RuntimeValue::Struct(Box::new(StructValue {
1414            type_name: "Page".into(),
1415            fields: local,
1416        }));
1417
1418        match crdt_merge_wire(local, &bytes) {
1419            RuntimeValue::Struct(s) => {
1420                assert!(matches!(s.fields.get("hits"), Some(RuntimeValue::Int(4))), "counter 1+3");
1421                assert!(
1422                    matches!(s.fields.get("title"), Some(RuntimeValue::Text(t)) if &***t == "v2"),
1423                    "LWW register"
1424                );
1425                match s.fields.get("tags") {
1426                    Some(RuntimeValue::Set(items)) => {
1427                        assert_eq!(items.borrow().len(), 2, "set union {{7}} ∪ {{9}}")
1428                    }
1429                    other => panic!("tags not a set: {other:?}"),
1430                }
1431            }
1432            other => panic!("expected struct, got {other:?}"),
1433        }
1434    }
1435
1436    #[test]
1437    fn error_messages_are_canonical() {
1438        let e = add(RuntimeValue::Bool(true), RuntimeValue::Nothing).unwrap_err();
1439        assert_eq!(e, "Cannot add Bool and Nothing");
1440        let e = subtract(RuntimeValue::Bool(true), RuntimeValue::Nothing).unwrap_err();
1441        assert_eq!(e, "Cannot subtract Nothing from Bool");
1442        let e = multiply(RuntimeValue::Bool(true), RuntimeValue::Nothing).unwrap_err();
1443        assert_eq!(e, "Cannot multiply Bool and Nothing");
1444        let e = divide(RuntimeValue::Int(1), RuntimeValue::Int(0)).unwrap_err();
1445        assert_eq!(e, "Division by zero");
1446        let e = divide(RuntimeValue::Float(1.0), RuntimeValue::Float(0.0)).unwrap_err();
1447        assert_eq!(e, "Division by zero");
1448        let e = modulo(RuntimeValue::Int(1), RuntimeValue::Int(0)).unwrap_err();
1449        assert_eq!(e, "Modulo by zero");
1450        // `not` is total — logical negation of truthiness, never an error.
1451        let r = not_value(RuntimeValue::Nothing).unwrap();
1452        assert!(matches!(r, RuntimeValue::Bool(true)));
1453    }
1454
1455    #[test]
1456    fn text_add_stringifies_either_side() {
1457        let r = add(
1458            RuntimeValue::Text(Rc::new("n=".to_string())),
1459            RuntimeValue::Int(4),
1460        )
1461        .unwrap();
1462        assert!(matches!(&r, RuntimeValue::Text(s) if **s == "n=4"));
1463        let r = add(
1464            RuntimeValue::Int(4),
1465            RuntimeValue::Text(Rc::new("!".to_string())),
1466        )
1467        .unwrap();
1468        assert!(matches!(&r, RuntimeValue::Text(s) if **s == "4!"));
1469    }
1470
1471    #[test]
1472    fn date_plus_span_is_calendar_aware() {
1473        // 2024-01-31 (day 19753) + 1 month = 2024-02-29.
1474        let r = add(
1475            RuntimeValue::Date(19753),
1476            RuntimeValue::Span { months: 1, days: 0 },
1477        )
1478        .unwrap();
1479        assert!(matches!(r, RuntimeValue::Date(19782)));
1480        // And subtraction inverts the span sign.
1481        let r = subtract(
1482            RuntimeValue::Date(19782),
1483            RuntimeValue::Span { months: 0, days: 1 },
1484        )
1485        .unwrap();
1486        assert!(matches!(r, RuntimeValue::Date(19781)));
1487    }
1488
1489    #[test]
1490    fn decimal_arithmetic_stays_exact_and_promotes_correctly() {
1491        let d = |s: &str| RuntimeValue::Decimal(Rc::new(Decimal::parse(s).unwrap()));
1492        // +,-,* stay exact Decimal — money keeps its scale, no f64 drift.
1493        assert_eq!(add(d("19.99"), d("0.01")).unwrap().to_display_string(), "20.00");
1494        assert_eq!(subtract(d("20.00"), d("0.01")).unwrap().to_display_string(), "19.99");
1495        assert_eq!(multiply(d("1.1"), d("1.1")).unwrap().to_display_string(), "1.21");
1496        assert_eq!(add(d("0.1"), d("0.2")).unwrap().to_display_string(), "0.3");
1497        // Decimal ∘ Int stays Decimal (a count scales money).
1498        assert_eq!(multiply(d("19.99"), RuntimeValue::Int(3)).unwrap().to_display_string(), "59.97");
1499        assert!(matches!(add(d("1.50"), RuntimeValue::Int(1)).unwrap(), RuntimeValue::Decimal(_)));
1500        // Division promotes to the EXACT Rational tower (base-10 division need not terminate).
1501        let q = divide(d("1"), d("3")).unwrap();
1502        assert!(matches!(q, RuntimeValue::Rational(_)));
1503        assert_eq!(q.to_display_string(), "1/3");
1504        // A Rational operand promotes to exact Rational; a Float operand yields Float.
1505        let third = RuntimeValue::from_rational(Rational::from_ratio_i64(1, 3).unwrap());
1506        assert!(matches!(add(d("0.5"), third).unwrap(), RuntimeValue::Rational(_)));
1507        assert!(matches!(add(d("0.5"), RuntimeValue::Float(0.25)).unwrap(), RuntimeValue::Float(_)));
1508        // Cross-type ordering: 19.99 (Decimal) > 10 (Int).
1509        assert!(matches!(
1510            compare(BinaryOpKind::Gt, &d("19.99"), &RuntimeValue::Int(10)).unwrap(),
1511            RuntimeValue::Bool(true)
1512        ));
1513    }
1514
1515    #[test]
1516    fn complex_arithmetic_is_exact_closed_and_unordered() {
1517        let c = |re: i64, im: i64| {
1518            RuntimeValue::Complex(Rc::new(Complex::new(Rational::from_i64(re), Rational::from_i64(im))))
1519        };
1520        let cq = |rn: i64, rd: i64, in_: i64, id: i64| {
1521            RuntimeValue::Complex(Rc::new(Complex::new(
1522                Rational::from_ratio_i64(rn, rd).unwrap(),
1523                Rational::from_ratio_i64(in_, id).unwrap(),
1524            )))
1525        };
1526        let i = c(0, 1);
1527        // The headline: i·i = −1, exact.
1528        assert_eq!(multiply(i.clone(), i.clone()).unwrap(), c(-1, 0));
1529        // Addition / subtraction.
1530        assert_eq!(add(c(2, 3), c(1, -1)).unwrap(), c(3, 2));
1531        assert_eq!(subtract(c(5, 2), c(1, 7)).unwrap(), c(4, -5));
1532        // (1+i)(1−i) = 2.
1533        assert_eq!(multiply(c(1, 1), c(1, -1)).unwrap(), c(2, 0));
1534        // (2+3i)(4+5i) = −7 + 22i.
1535        assert_eq!(multiply(c(2, 3), c(4, 5)).unwrap(), c(-7, 22));
1536        // Division stays Complex (closed field): (3+4i)/(1+2i) = (11−2i)/5; z/z = 1.
1537        assert_eq!(divide(c(3, 4), c(1, 2)).unwrap(), cq(11, 5, -2, 5));
1538        assert_eq!(divide(c(2, 3), c(2, 3)).unwrap(), c(1, 0));
1539        // A real embeds (re + 0i): Complex ∘ Int / BigInt / Rational stays Complex.
1540        assert_eq!(add(c(2, 3), RuntimeValue::Int(5)).unwrap(), c(7, 3));
1541        assert_eq!(multiply(i.clone(), RuntimeValue::Int(3)).unwrap(), c(0, 3)); // 3i
1542        let half = RuntimeValue::from_rational(Rational::from_ratio_i64(1, 2).unwrap());
1543        assert_eq!(add(c(1, 0), half).unwrap(), cq(3, 2, 0, 1));
1544        // Equality is exact and structural; reduced parts compare equal.
1545        assert!(matches!(
1546            binary_op(BinaryOpKind::Eq, c(3, 4), c(3, 4)).unwrap(),
1547            RuntimeValue::Bool(true)
1548        ));
1549        assert!(matches!(
1550            binary_op(BinaryOpKind::Eq, c(3, 4), c(3, -4)).unwrap(),
1551            RuntimeValue::Bool(false)
1552        ));
1553        // An inexact Float operand is REFUSED (an exact Complex never silently absorbs a float).
1554        assert!(add(c(1, 1), RuntimeValue::Float(0.5)).is_err());
1555        assert!(multiply(RuntimeValue::Float(2.0), i.clone()).is_err());
1556        // Division by zero is a clean error, never a panic.
1557        assert!(divide(c(1, 1), c(0, 0)).is_err());
1558        // Complex has NO total order: every relational comparison is a typed error.
1559        for op in [BinaryOpKind::Lt, BinaryOpKind::Gt, BinaryOpKind::LtEq, BinaryOpKind::GtEq] {
1560            assert!(compare(op, &c(1, 1), &c(2, 2)).is_err(), "complex is unordered under {op:?}");
1561        }
1562        // Display forms.
1563        assert_eq!(c(3, 4).to_display_string(), "3+4i");
1564        assert_eq!(c(0, 1).to_display_string(), "i");
1565        assert_eq!(c(0, -1).to_display_string(), "-i");
1566        assert_eq!(c(-1, 0).to_display_string(), "-1");
1567    }
1568
1569    /// The variant kind (NOT `type_name`, which folds BigInt into "Int").
1570    fn kind(v: &RuntimeValue) -> &'static str {
1571        match v {
1572            RuntimeValue::Int(_) => "Int",
1573            RuntimeValue::BigInt(_) => "BigInt",
1574            RuntimeValue::Rational(_) => "Rational",
1575            RuntimeValue::Decimal(_) => "Decimal",
1576            RuntimeValue::Complex(_) => "Complex",
1577            RuntimeValue::Modular(_) => "Modular",
1578            RuntimeValue::Float(_) => "Float",
1579            _ => "other", // the gauntlet only uses numeric values; this keeps the return 'static
1580        }
1581    }
1582
1583    /// THE GAUNTLET: every numeric type × every type × {+, −, ×, ÷}, asserting the promotion
1584    /// KIND and the exact VALUE (for the exact types) or a typed ERROR for the incompatible
1585    /// cells. The promotion precedence is: Float and Complex are mutually exclusive (an exact
1586    /// Complex refuses a Float); otherwise Complex > Rational > Decimal > Int, and Decimal ÷
1587    /// widens to Rational. Modular combines only with Modular of the same modulus.
1588    #[test]
1589    fn numeric_tower_cross_type_promotion_gauntlet() {
1590        let int = || RuntimeValue::Int(2);
1591        let rat = || RuntimeValue::from_rational(Rational::from_ratio_i64(1, 3).unwrap());
1592        let dec = || RuntimeValue::Decimal(Rc::new(Decimal::parse("0.5").unwrap()));
1593        let cpx = || RuntimeValue::Complex(Rc::new(Complex::new(Rational::from_i64(2), Rational::from_i64(3))));
1594        let flt = || RuntimeValue::Float(2.0);
1595
1596        // ---- ADD: kind + exact value where the result is an exact type ----
1597        let g = |a: RuntimeValue, b: RuntimeValue| add(a, b);
1598        assert_eq!(kind(&g(int(), int()).unwrap()), "Int");
1599        assert_eq!(g(int(), rat()).unwrap().to_display_string(), "7/3"); // 2 + 1/3
1600        assert_eq!(kind(&g(int(), rat()).unwrap()), "Rational");
1601        assert_eq!(g(int(), dec()).unwrap().to_display_string(), "2.5"); // 2 + 0.5
1602        assert_eq!(kind(&g(int(), dec()).unwrap()), "Decimal");
1603        assert_eq!(g(int(), cpx()).unwrap().to_display_string(), "4+3i"); // 2 + (2+3i)
1604        assert_eq!(kind(&g(int(), cpx()).unwrap()), "Complex");
1605        assert_eq!(kind(&g(int(), flt()).unwrap()), "Float");
1606        assert_eq!(g(rat(), dec()).unwrap().to_display_string(), "5/6"); // 1/3 + 1/2
1607        assert_eq!(kind(&g(rat(), dec()).unwrap()), "Rational"); // Rational beats Decimal
1608        assert_eq!(g(rat(), cpx()).unwrap().to_display_string(), "7/3+3i"); // 1/3 + (2+3i)
1609        assert_eq!(kind(&g(rat(), cpx()).unwrap()), "Complex");
1610        assert_eq!(kind(&g(rat(), flt()).unwrap()), "Float");
1611        assert_eq!(g(dec(), dec()).unwrap().to_display_string(), "1.0"); // 0.5 + 0.5
1612        assert_eq!(kind(&g(dec(), dec()).unwrap()), "Decimal");
1613        assert_eq!(g(dec(), cpx()).unwrap().to_display_string(), "5/2+3i"); // 0.5 + (2+3i)
1614        assert_eq!(kind(&g(dec(), cpx()).unwrap()), "Complex");
1615        assert_eq!(kind(&g(dec(), flt()).unwrap()), "Float");
1616        assert_eq!(g(cpx(), cpx()).unwrap().to_display_string(), "4+6i");
1617        assert_eq!(kind(&g(flt(), flt()).unwrap()), "Float");
1618        // The one incompatible cell: an exact Complex refuses an inexact Float (both orders).
1619        assert!(g(cpx(), flt()).is_err(), "Complex + Float must be a typed error");
1620        assert!(g(flt(), cpx()).is_err(), "Float + Complex must be a typed error");
1621
1622        // ---- Commutativity of the PROMOTION across the matrix (a∘b kind == b∘a kind) ----
1623        for a in [int(), rat(), dec(), cpx(), flt()] {
1624            for b in [int(), rat(), dec(), cpx(), flt()] {
1625                match (add(a.clone(), b.clone()), add(b.clone(), a.clone())) {
1626                    (Ok(ab), Ok(ba)) => assert_eq!(kind(&ab), kind(&ba), "promotion commutes: {} {}", kind(&a), kind(&b)),
1627                    (Err(_), Err(_)) => {} // both refuse (the Complex/Float cell)
1628                    other => panic!("promotion asymmetry for {} and {}: {other:?}", kind(&a), kind(&b)),
1629                }
1630            }
1631        }
1632
1633        // ---- DIVIDE: Decimal widens to Rational; Complex stays Complex ----
1634        assert_eq!(divide(dec(), int()).unwrap().to_display_string(), "1/4"); // 0.5 / 2
1635        assert_eq!(kind(&divide(dec(), int()).unwrap()), "Rational");
1636        assert_eq!(divide(dec(), dec()).unwrap().to_display_string(), "1"); // 0.5 / 0.5 → 1 (Int)
1637        assert_eq!(divide(cpx(), int()).unwrap().to_display_string(), "1+3/2i"); // (2+3i)/2
1638        assert_eq!(kind(&divide(cpx(), int()).unwrap()), "Complex");
1639        assert_eq!(divide(cpx(), cpx()).unwrap().to_display_string(), "1"); // z/z
1640        assert_eq!(divide(rat(), int()).unwrap().to_display_string(), "1/6"); // (1/3)/2
1641        // Division by zero is a clean error across the exact types.
1642        assert!(divide(dec(), RuntimeValue::Int(0)).is_err());
1643        assert!(divide(cpx(), RuntimeValue::from_rational(Rational::zero())).is_err());
1644    }
1645
1646    #[test]
1647    fn modular_arithmetic_wraps_and_requires_a_shared_ring() {
1648        let m = |v: i64, n: i64| {
1649            RuntimeValue::Modular(Rc::new(Modular::from_i64(v, n).unwrap()))
1650        };
1651        // Add/sub/mul wrap in ℤ/nℤ.
1652        assert_eq!(add(m(5, 7), m(4, 7)).unwrap(), m(2, 7)); // 9 ≡ 2
1653        assert_eq!(subtract(m(3, 7), m(5, 7)).unwrap(), m(5, 7)); // −2 ≡ 5
1654        assert_eq!(multiply(m(4, 7), m(5, 7)).unwrap(), m(6, 7)); // 20 ≡ 6
1655        // Division is by the modular inverse: 1/3 ≡ 5 (mod 7).
1656        assert_eq!(divide(m(1, 7), m(3, 7)).unwrap(), m(5, 7));
1657        // A non-invertible divisor (gcd ≠ 1) is a clean error, not a panic.
1658        assert!(divide(m(1, 4), m(2, 4)).is_err());
1659        // A modulus mismatch is refused on every op (no silent cross-ring math).
1660        assert!(add(m(3, 7), m(3, 5)).is_err());
1661        assert!(multiply(m(3, 7), m(3, 5)).is_err());
1662        assert!(divide(m(3, 7), m(3, 5)).is_err());
1663        // Mixing a Modular with a bare Int is refused (an Int has no modulus).
1664        assert!(add(m(3, 7), RuntimeValue::Int(5)).is_err());
1665        // Equality is per-ring; ℤ/nℤ is unordered so comparison is a typed error.
1666        assert!(matches!(binary_op(BinaryOpKind::Eq, m(3, 7), m(10, 7)).unwrap(), RuntimeValue::Bool(true)));
1667        assert!(matches!(binary_op(BinaryOpKind::Eq, m(3, 7), m(3, 5)).unwrap(), RuntimeValue::Bool(false)));
1668        assert!(compare(BinaryOpKind::Lt, &m(1, 7), &m(2, 7)).is_err());
1669        assert_eq!(m(3, 7).to_display_string(), "3 (mod 7)");
1670    }
1671
1672    #[test]
1673    fn int_arithmetic_is_exact_in_every_build_profile() {
1674        // The LOGOS Int spec: EXACT (promoting) i64 — identical in debug AND release.
1675        let r = add(RuntimeValue::Int(i64::MAX), RuntimeValue::Int(1)).unwrap();
1676        assert_eq!(r.to_display_string(), "9223372036854775808"); // not wrapped i64::MIN
1677        let r = subtract(RuntimeValue::Int(i64::MIN), RuntimeValue::Int(1)).unwrap();
1678        assert_eq!(r.to_display_string(), "-9223372036854775809");
1679        let r = multiply(RuntimeValue::Int(i64::MAX), RuntimeValue::Int(2)).unwrap();
1680        assert_eq!(r.to_display_string(), "18446744073709551614");
1681        // MIN / -1 = 2^63 (the division-overflow edge) promotes exactly; MIN % -1 = 0.
1682        let r = divide(RuntimeValue::Int(i64::MIN), RuntimeValue::Int(-1)).unwrap();
1683        assert_eq!(r.to_display_string(), "9223372036854775808");
1684        let r = modulo(RuntimeValue::Int(i64::MIN), RuntimeValue::Int(-1)).unwrap();
1685        assert!(matches!(r, RuntimeValue::Int(0)));
1686        // Duration is a temporal quantity, NOT part of the integer tower — it still
1687        // wraps (its arithmetic is modular by construction).
1688        let r = add(RuntimeValue::Duration(i64::MAX), RuntimeValue::Duration(1)).unwrap();
1689        assert!(matches!(r, RuntimeValue::Duration(i64::MIN)));
1690        let r = subtract(RuntimeValue::Duration(i64::MIN), RuntimeValue::Duration(1)).unwrap();
1691        assert!(matches!(r, RuntimeValue::Duration(i64::MAX)));
1692    }
1693
1694    #[test]
1695    fn shifts_mask_their_count_modulo_64() {
1696        // wrapping_shl/shr(b as u32): the count is truncated to u32 then masked
1697        // mod 64, so `1 << 64 == 1` and a negative count becomes (b as u32) & 63.
1698        let r = binary_op(BinaryOpKind::Shl, RuntimeValue::Int(1), RuntimeValue::Int(64)).unwrap();
1699        assert!(matches!(r, RuntimeValue::Int(1)));
1700        let r = binary_op(BinaryOpKind::Shl, RuntimeValue::Int(1), RuntimeValue::Int(63)).unwrap();
1701        assert!(matches!(r, RuntimeValue::Int(i64::MIN)));
1702        // -1 as u32 == u32::MAX; masked mod 64 → 63.
1703        let r = binary_op(BinaryOpKind::Shl, RuntimeValue::Int(1), RuntimeValue::Int(-1)).unwrap();
1704        assert!(matches!(r, RuntimeValue::Int(i64::MIN)));
1705        let r = binary_op(BinaryOpKind::Shr, RuntimeValue::Int(i64::MIN), RuntimeValue::Int(63)).unwrap();
1706        assert!(matches!(r, RuntimeValue::Int(-1)));
1707        let r = binary_op(BinaryOpKind::Shr, RuntimeValue::Int(8), RuntimeValue::Int(64)).unwrap();
1708        assert!(matches!(r, RuntimeValue::Int(8)));
1709    }
1710
1711    #[test]
1712    fn crdt_counter_bump_wraps() {
1713        let r = crdt_counter_bump(RuntimeValue::Int(i64::MAX), 1, "n").unwrap();
1714        assert!(matches!(r, RuntimeValue::Int(i64::MIN)));
1715        let r = crdt_counter_bump(RuntimeValue::Nothing, 5, "n").unwrap();
1716        assert!(matches!(r, RuntimeValue::Int(5)));
1717        let e = crdt_counter_bump(RuntimeValue::Bool(true), 1, "score").unwrap_err();
1718        assert_eq!(e, "Field 'score' is not a counter");
1719    }
1720
1721    #[test]
1722    fn eager_and_or_are_logical_truthiness_to_bool() {
1723        // `and`/`or` are LOGICAL: truthiness in, Bool out — `&`/`|` are the
1724        // bitwise spellings (BitAnd/BitOr below).
1725        let r = binary_op(BinaryOpKind::And, RuntimeValue::Int(6), RuntimeValue::Int(3)).unwrap();
1726        assert!(matches!(r, RuntimeValue::Bool(true)));
1727        let r = binary_op(BinaryOpKind::Or, RuntimeValue::Int(0), RuntimeValue::Int(7)).unwrap();
1728        assert!(matches!(r, RuntimeValue::Bool(true)));
1729        let r = binary_op(BinaryOpKind::And, RuntimeValue::Int(1), RuntimeValue::Bool(false)).unwrap();
1730        assert!(matches!(r, RuntimeValue::Bool(false)));
1731        let r = binary_op(BinaryOpKind::Or, RuntimeValue::Bool(false), RuntimeValue::Bool(true)).unwrap();
1732        assert!(matches!(r, RuntimeValue::Bool(true)));
1733        let r = binary_op(BinaryOpKind::BitAnd, RuntimeValue::Int(6), RuntimeValue::Int(3)).unwrap();
1734        assert!(matches!(r, RuntimeValue::Int(2)));
1735        let r = binary_op(BinaryOpKind::BitOr, RuntimeValue::Int(6), RuntimeValue::Int(3)).unwrap();
1736        assert!(matches!(r, RuntimeValue::Int(7)));
1737    }
1738}
1739
1740/// Exhaustive coverage of integer arithmetic at and beyond the i64 boundary: math is
1741/// EXACT on the tree-walker (overflow promotes to BigInt; in-range results downsize
1742/// to Int), every operand mix (Int/BigInt/Float) is covered, and a dense differential
1743/// against i128 proves we equal the machine's exact answer wherever it fits.
1744#[cfg(test)]
1745mod bigint_exact_arithmetic {
1746    use super::*;
1747    use logicaffeine_base::BigInt;
1748
1749    fn int(n: i64) -> RuntimeValue {
1750        RuntimeValue::Int(n)
1751    }
1752    /// `i64::MAX + 1` = 2^63, the smallest value that does not fit i64 — our canonical
1753    /// "just past the boundary" BigInt.
1754    fn two_pow_63() -> RuntimeValue {
1755        add(int(i64::MAX), int(1)).unwrap()
1756    }
1757    fn disp(v: &RuntimeValue) -> String {
1758        v.to_display_string()
1759    }
1760    fn is_big(v: &RuntimeValue) -> bool {
1761        matches!(v, RuntimeValue::BigInt(_))
1762    }
1763
1764    #[test]
1765    fn add_at_the_boundary_promotes_not_wraps() {
1766        // Arrange: the classic overflow.  Act:
1767        let r = add(int(i64::MAX), int(1)).unwrap();
1768        // Assert: the EXACT value, never the wrapped i64::MIN.
1769        assert!(is_big(&r), "i64::MAX + 1 must be a BigInt, not a wrapped Int");
1770        assert_eq!(disp(&r), "9223372036854775808");
1771        assert_ne!(r, int(i64::MIN), "the JSON/2's-complement footgun must be gone");
1772    }
1773
1774    #[test]
1775    fn subtract_below_the_boundary_promotes() {
1776        let r = subtract(int(i64::MIN), int(1)).unwrap();
1777        assert!(is_big(&r));
1778        assert_eq!(disp(&r), "-9223372036854775809");
1779    }
1780
1781    #[test]
1782    fn multiply_overflow_promotes() {
1783        let r = multiply(int(i64::MAX), int(2)).unwrap();
1784        assert_eq!(disp(&r), "18446744073709551614");
1785        assert!(is_big(&r));
1786    }
1787
1788    #[test]
1789    fn results_that_fit_downsize_back_to_int() {
1790        // Arrange a BigInt, then bring it back into range.
1791        let big = two_pow_63(); // 2^63
1792        // 2^63 - 1 == i64::MAX, which fits → must be a narrow Int again.
1793        let back = subtract(big.clone(), int(1)).unwrap();
1794        assert_eq!(back, int(i64::MAX), "must downsize to Int");
1795        assert!(!is_big(&back));
1796        // big + (-big) == 0 → Int(0).
1797        let zero = add(big.clone(), subtract(int(0), big).unwrap()).unwrap();
1798        assert_eq!(zero, int(0));
1799    }
1800
1801    #[test]
1802    fn every_operand_mix_is_handled_for_add_sub_mul() {
1803        let big = two_pow_63(); // 2^63 = 9223372036854775808
1804        // Int ∘ BigInt, BigInt ∘ Int, BigInt ∘ BigInt — add.
1805        assert_eq!(disp(&add(int(1), big.clone()).unwrap()), "9223372036854775809");
1806        assert_eq!(disp(&add(big.clone(), int(1)).unwrap()), "9223372036854775809");
1807        assert_eq!(disp(&add(big.clone(), big.clone()).unwrap()), "18446744073709551616"); // 2^64
1808        // subtract.
1809        assert_eq!(disp(&subtract(big.clone(), big.clone()).unwrap()), "0");
1810        // multiply (2^63 * 2 = 2^64).
1811        assert_eq!(disp(&multiply(big.clone(), int(2)).unwrap()), "18446744073709551616");
1812        assert_eq!(disp(&multiply(int(2), big.clone()).unwrap()), "18446744073709551616");
1813    }
1814
1815    #[test]
1816    fn divide_and_modulo_cover_the_overflow_and_big_operands() {
1817        // i64::MIN / -1 = 2^63 (the one i64 division overflow) → promotes.
1818        let q = divide(int(i64::MIN), int(-1)).unwrap();
1819        assert_eq!(disp(&q), "9223372036854775808");
1820        assert_eq!(modulo(int(i64::MIN), int(-1)).unwrap(), int(0));
1821        // BigInt / Int, BigInt % Int.
1822        let big = two_pow_63(); // 9223372036854775808
1823        assert_eq!(divide(big.clone(), int(2)).unwrap(), int(4611686018427387904));
1824        assert_eq!(modulo(big.clone(), int(2)).unwrap(), int(0));
1825        // Int / BigInt: a small number over a huge one truncates to 0.
1826        assert_eq!(divide(int(5), big.clone()).unwrap(), int(0));
1827        assert_eq!(modulo(int(5), big).unwrap(), int(5));
1828        // Division by zero is still an error, never a panic.
1829        assert!(divide(int(1), int(0)).is_err());
1830        assert!(modulo(int(1), int(0)).is_err());
1831    }
1832
1833    #[test]
1834    fn mixing_a_bigint_with_a_float_yields_a_float() {
1835        let big = two_pow_63();
1836        match add(big.clone(), RuntimeValue::Float(1.0)).unwrap() {
1837            RuntimeValue::Float(f) => assert!((f - 9223372036854775809.0).abs() < 1e9),
1838            other => panic!("expected Float, got {}", other.type_name()),
1839        }
1840        assert!(matches!(multiply(RuntimeValue::Float(2.0), big).unwrap(), RuntimeValue::Float(_)));
1841    }
1842
1843    #[test]
1844    fn comparison_orders_across_the_narrow_wide_boundary() {
1845        let big = two_pow_63(); // > every i64
1846        let neg_big = subtract(int(i64::MIN), int(1)).unwrap(); // < every i64
1847        let lt = |a: &RuntimeValue, b: &RuntimeValue| {
1848            matches!(
1849                super::super::compare::compare(BinaryOpKind::Lt, a, b).unwrap(),
1850                RuntimeValue::Bool(true)
1851            )
1852        };
1853        assert!(lt(&int(i64::MAX), &big), "i64::MAX < 2^63");
1854        assert!(lt(&neg_big, &int(i64::MIN)), "-(2^63+1) < i64::MIN");
1855        assert!(lt(&neg_big, &big), "huge negative < huge positive");
1856        // equal BigInts are not less-than each other.
1857        assert!(!lt(&big.clone(), &big));
1858    }
1859
1860    #[test]
1861    fn equality_and_hashing_are_consistent_for_bigints() {
1862        use std::collections::HashSet;
1863        let a = two_pow_63();
1864        let b = add(int(1), int(i64::MAX)).unwrap(); // same value, different path
1865        assert_eq!(a, b, "equal BigInts compare equal");
1866        assert_ne!(a, int(0), "a BigInt is never equal to an Int");
1867        // Hash agreement: a HashSet dedups two equal BigInts to one entry.
1868        let mut set = HashSet::new();
1869        set.insert(a);
1870        set.insert(b);
1871        assert_eq!(set.len(), 1, "equal BigInts must hash-collapse");
1872    }
1873
1874    #[test]
1875    fn dense_differential_against_i128_through_the_arith_layer() {
1876        // For every pair whose true result fits i128, our Int/BigInt arithmetic — with
1877        // promotion AND downsizing — must equal the machine's exact answer.
1878        let xs: [i64; 9] = [0, 1, -1, 7, -7, i32::MAX as i64, i32::MIN as i64, i64::MAX, i64::MIN];
1879        for &x in &xs {
1880            for &y in &xs {
1881                assert_eq!(disp(&add(int(x), int(y)).unwrap()), (x as i128 + y as i128).to_string(), "{x}+{y}");
1882                assert_eq!(disp(&subtract(int(x), int(y)).unwrap()), (x as i128 - y as i128).to_string(), "{x}-{y}");
1883                assert_eq!(disp(&multiply(int(x), int(y)).unwrap()), (x as i128 * y as i128).to_string(), "{x}*{y}");
1884                if y != 0 {
1885                    assert_eq!(disp(&divide(int(x), int(y)).unwrap()), (x as i128 / y as i128).to_string(), "{x}/{y}");
1886                    assert_eq!(disp(&modulo(int(x), int(y)).unwrap()), (x as i128 % y as i128).to_string(), "{x}%{y}");
1887                }
1888            }
1889        }
1890    }
1891
1892    #[test]
1893    fn promoted_values_round_trip_through_a_negation_chain() {
1894        // A stress walk: repeatedly add 1 starting from i64::MAX-1, crossing the
1895        // boundary, then subtract back down — every value exact, Int↔BigInt seamless.
1896        let mut v = int(i64::MAX - 1);
1897        for _ in 0..4 {
1898            v = add(v, int(1)).unwrap();
1899        }
1900        assert_eq!(disp(&v), "9223372036854775810"); // i64::MAX-1 + 4 = 2^63 + 1
1901        assert!(is_big(&v));
1902        for _ in 0..4 {
1903            v = subtract(v, int(1)).unwrap();
1904        }
1905        assert_eq!(v, int(i64::MAX - 1), "back to the exact narrow value");
1906        assert!(!is_big(&v));
1907    }
1908
1909    #[test]
1910    fn fuzz_promotion_layer_matches_i128_and_obeys_algebra() {
1911        // Deterministic SplitMix64 — reproducible, no external dependency.
1912        let mut state = 0x5DEE_CE66_1357_2468u64;
1913        let mut next = || {
1914            state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
1915            let mut z = state;
1916            z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
1917            z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
1918            z ^ (z >> 31)
1919        };
1920        for _ in 0..4000 {
1921            let (x, y) = (next() as i64, next() as i64);
1922            // Differential against i128 THROUGH the promote/downsize layer.
1923            assert_eq!(disp(&add(int(x), int(y)).unwrap()), (x as i128 + y as i128).to_string(), "{x}+{y}");
1924            assert_eq!(disp(&subtract(int(x), int(y)).unwrap()), (x as i128 - y as i128).to_string(), "{x}-{y}");
1925            assert_eq!(disp(&multiply(int(x), int(y)).unwrap()), (x as i128 * y as i128).to_string(), "{x}*{y}");
1926            if y != 0 {
1927                assert_eq!(disp(&divide(int(x), int(y)).unwrap()), (x as i128 / y as i128).to_string(), "{x}/{y}");
1928                assert_eq!(disp(&modulo(int(x), int(y)).unwrap()), (x as i128 % y as i128).to_string(), "{x}%{y}");
1929            }
1930            // Commutativity survives promotion.
1931            assert_eq!(add(int(x), int(y)).unwrap(), add(int(y), int(x)).unwrap());
1932            assert_eq!(multiply(int(x), int(y)).unwrap(), multiply(int(y), int(x)).unwrap());
1933            // The downsizing invariant: a sum is a BigInt IFF it does not fit i64.
1934            let exact = x as i128 + y as i128;
1935            let fits = (i64::MIN as i128..=i64::MAX as i128).contains(&exact);
1936            assert_eq!(is_big(&add(int(x), int(y)).unwrap()), !fits, "BigInt iff out of i64 range for {x}+{y}");
1937        }
1938    }
1939
1940    #[test]
1941    fn from_bigint_constructor_maintains_the_downsizing_invariant() {
1942        // A BigInt that fits i64 must NOT be wrapped in the BigInt variant.
1943        assert_eq!(RuntimeValue::from_bigint(BigInt::from_i64(42)), int(42));
1944        assert!(!is_big(&RuntimeValue::from_bigint(BigInt::from_i64(i64::MIN))));
1945        // One that does not fit stays BigInt.
1946        assert!(is_big(&RuntimeValue::from_bigint(BigInt::from_i64(i64::MAX).add(&BigInt::from_i64(1)))));
1947    }
1948}
1949
1950/// Phase R1: `RuntimeValue::Rational` as a first-class exact value. Arithmetic on
1951/// Rational operands is exact (an integer-valued result downsizes back to `Int`),
1952/// a Float operand makes the expression Float, and eq/compare/display are exact.
1953/// (The `Int / Int → Rational` flip itself is Phase R2 — here `/` still truncates,
1954/// so these tests construct Rationals directly via `from_rational`.)
1955#[cfg(test)]
1956mod rational_exact_arithmetic {
1957    use super::*;
1958    use crate::interpreter::StructValue;
1959    use logicaffeine_base::Rational;
1960    use std::collections::HashMap;
1961
1962    fn rat(n: i64, d: i64) -> RuntimeValue {
1963        RuntimeValue::from_rational(Rational::from_ratio_i64(n, d).unwrap())
1964    }
1965    fn int(n: i64) -> RuntimeValue {
1966        RuntimeValue::Int(n)
1967    }
1968    fn is_rat(v: &RuntimeValue) -> bool {
1969        matches!(v, RuntimeValue::Rational(_))
1970    }
1971
1972    #[test]
1973    fn from_rational_downsizes_whole_values_to_int() {
1974        // 6/2 reduces to the whole number 3 → Int, NOT a Rational.
1975        assert_eq!(rat(6, 2), int(3));
1976        assert!(!is_rat(&rat(6, 2)));
1977        assert_eq!(rat(0, 5), int(0));
1978        // 7/2 is not whole → stays a Rational, displayed as a fraction.
1979        assert!(is_rat(&rat(7, 2)));
1980        assert_eq!(rat(7, 2).to_display_string(), "7/2");
1981        assert_eq!(rat(7, 2).type_name(), "Rational");
1982    }
1983
1984    #[test]
1985    fn rational_arithmetic_is_exact_and_downsizes() {
1986        // 1/2 + 1/2 = 1 (downsizes to Int).
1987        assert_eq!(add(rat(1, 2), rat(1, 2)).unwrap(), int(1));
1988        // 1/3 + 1/6 = 1/2 (stays exact).
1989        assert_eq!(add(rat(1, 3), rat(1, 6)).unwrap().to_display_string(), "1/2");
1990        // 1/2 - 1/3 = 1/6 ; 2/3 * 3/4 = 1/2 ; (1/2)/(3/4) = 2/3.
1991        assert_eq!(subtract(rat(1, 2), rat(1, 3)).unwrap().to_display_string(), "1/6");
1992        assert_eq!(multiply(rat(2, 3), rat(3, 4)).unwrap().to_display_string(), "1/2");
1993        assert_eq!(divide(rat(1, 2), rat(3, 4)).unwrap().to_display_string(), "2/3");
1994    }
1995
1996    #[test]
1997    fn rational_mixes_with_int_and_bigint_exactly() {
1998        // 1/2 + 1 = 3/2 ; 3 * (1/2) = 3/2 ; (3/2) / 3 = 1/2.
1999        assert_eq!(add(rat(1, 2), int(1)).unwrap().to_display_string(), "3/2");
2000        assert_eq!(multiply(int(3), rat(1, 2)).unwrap().to_display_string(), "3/2");
2001        assert_eq!(divide(rat(3, 2), int(3)).unwrap(), rat(1, 2));
2002        // 1/3 + 2/3 = 1 (Int).
2003        assert_eq!(add(rat(1, 3), rat(2, 3)).unwrap(), int(1));
2004    }
2005
2006    #[test]
2007    fn rational_with_a_float_operand_becomes_float() {
2008        // 1/2 + 0.5 → Float 1.0 (a Float operand opts the whole expression into floats).
2009        assert!(matches!(add(rat(1, 2), RuntimeValue::Float(0.5)).unwrap(), RuntimeValue::Float(f) if (f - 1.0).abs() < 1e-12));
2010        assert!(matches!(divide(rat(1, 2), RuntimeValue::Float(2.0)).unwrap(), RuntimeValue::Float(f) if (f - 0.25).abs() < 1e-12));
2011    }
2012
2013    #[test]
2014    fn rational_equality_and_ordering_are_exact() {
2015        // Never equal to an Int; equal to a structurally-equal Rational.
2016        assert!(!values_equal(&rat(7, 2), &int(3)));
2017        assert!(values_equal(&rat(7, 2), &rat(7, 2)));
2018        // 1/3 < 1/2 < 2/3 by exact cross-multiplication (no rounding).
2019        let lt = |a, b| matches!(compare(BinaryOpKind::Lt, &a, &b).unwrap(), RuntimeValue::Bool(true));
2020        assert!(lt(rat(1, 3), rat(1, 2)));
2021        assert!(lt(rat(1, 2), rat(2, 3)));
2022        // 1/2 < 1 (Int) and 0 < 1/2.
2023        assert!(lt(rat(1, 2), int(1)));
2024        assert!(lt(int(0), rat(1, 2)));
2025    }
2026
2027    #[test]
2028    fn dividing_a_rational_by_zero_errors() {
2029        assert_eq!(divide(rat(1, 2), int(0)).unwrap_err(), "Division by zero");
2030        assert_eq!(divide(rat(1, 2), rat(0, 1)).unwrap_err(), "Division by zero");
2031    }
2032
2033    // ===== G7: the CRDT laws, fuzzed to absurdity =====
2034    // A state-based CRDT (CvRDT) converges iff its merge is COMMUTATIVE, ASSOCIATIVE, and
2035    // IDEMPOTENT. We prove all three for set-union and the map-of-sets join over thousands of
2036    // random states, then prove the corollary that matters on a real network: replicas reach
2037    // the same value no matter the ORDER or DUPLICATION of merges (a gossip/lossy link).
2038
2039    struct Rng(u64);
2040    impl Rng {
2041        fn new(seed: u64) -> Self {
2042            Rng(seed)
2043        }
2044        fn next(&mut self) -> u64 {
2045            self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
2046            let mut z = self.0;
2047            z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
2048            z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
2049            z ^ (z >> 31)
2050        }
2051        fn upto(&mut self, n: u64) -> u64 {
2052            self.next() % n.max(1)
2053        }
2054    }
2055
2056    /// Structural CRDT equality: sets compared AS SETS (order-independent), maps AS MAPS,
2057    /// scalars directly — the right notion of "converged to the same state".
2058    fn crdt_eq(a: &RuntimeValue, b: &RuntimeValue) -> bool {
2059        match (a, b) {
2060            (RuntimeValue::Int(x), RuntimeValue::Int(y)) => x == y,
2061            (RuntimeValue::Text(x), RuntimeValue::Text(y)) => x == y,
2062            (RuntimeValue::Bool(x), RuntimeValue::Bool(y)) => x == y,
2063            (RuntimeValue::Nothing, RuntimeValue::Nothing) => true,
2064            (RuntimeValue::Set(x), RuntimeValue::Set(y)) => {
2065                let (xb, yb) = (x.borrow(), y.borrow());
2066                xb.len() == yb.len() && xb.iter().all(|e| yb.iter().any(|f| crdt_eq(e, f)))
2067            }
2068            (RuntimeValue::Map(x), RuntimeValue::Map(y)) => {
2069                let (xb, yb) = (x.borrow(), y.borrow());
2070                xb.len() == yb.len() && xb.iter().all(|(k, v)| yb.get(k).map_or(false, |w| crdt_eq(v, w)))
2071            }
2072            (RuntimeValue::Struct(x), RuntimeValue::Struct(y)) => {
2073                x.type_name == y.type_name
2074                    && x.fields.len() == y.fields.len()
2075                    && x.fields.iter().all(|(k, v)| y.fields.get(k).map_or(false, |w| crdt_eq(v, w)))
2076            }
2077            _ => false,
2078        }
2079    }
2080
2081    fn rand_gcounter(rng: &mut Rng) -> RuntimeValue {
2082        let mut fields: HashMap<String, RuntimeValue> = HashMap::new();
2083        for _ in 0..rng.upto(4) {
2084            let r = format!("r{}", rng.upto(4));
2085            let c = rng.upto(20) as i64;
2086            let cur = if let Some(RuntimeValue::Int(x)) = fields.get(&r) { *x } else { 0 };
2087            fields.insert(r, RuntimeValue::Int(cur.max(c)));
2088        }
2089        RuntimeValue::Struct(Box::new(StructValue { type_name: GCOUNTER_TAG.to_string(), fields }))
2090    }
2091
2092    fn gcounter(pairs: &[(&str, i64)]) -> RuntimeValue {
2093        let mut fields = HashMap::new();
2094        for (r, c) in pairs {
2095            fields.insert(r.to_string(), RuntimeValue::Int(*c));
2096        }
2097        RuntimeValue::Struct(Box::new(StructValue { type_name: GCOUNTER_TAG.to_string(), fields }))
2098    }
2099
2100    #[test]
2101    fn gcounter_is_gossip_safe_max_per_replica_and_idempotent_under_redelivery() {
2102        // The fix for the #1 gap: per-replica MAX, not op-based add. A REDELIVERED counter
2103        // state must not double-count — the crux the old `add` counter fails on a lossy link.
2104        let a = gcounter(&[("alice", 3), ("bob", 1)]);
2105        let b = gcounter(&[("alice", 2), ("carol", 5)]);
2106        let ab = crdt_merge_field(&a, b.clone());
2107        assert_eq!(gcounter_value(&ab), Some(3 + 1 + 5), "MAX per replica → alice=3, bob=1, carol=5");
2108        let abb = crdt_merge_field(&ab, b.clone());
2109        assert_eq!(gcounter_value(&abb), Some(9), "REDELIVERY is a no-op — never double-counts");
2110        assert!(crdt_eq(&ab, &abb), "the state is unchanged under redelivery (idempotent)");
2111    }
2112
2113    #[test]
2114    fn gcounter_merge_obeys_the_crdt_laws() {
2115        assert_crdt_laws(&mut Rng::new(0x06C0_0117), rand_gcounter, "g-counter");
2116    }
2117
2118    #[test]
2119    fn gcounter_replicas_converge_under_any_order_and_duplication() {
2120        // Replicas increment independently, gossip in shuffled+duplicated order, and every
2121        // one ends at the same total — a genuinely gossip-safe distributed counter.
2122        let mut rng = Rng::new(0x06C0_F1A6);
2123        for _ in 0..500 {
2124            let states: Vec<RuntimeValue> = (0..4).map(|_| rand_gcounter(&mut rng)).collect();
2125            let mut truth = states[0].clone();
2126            for s in &states[1..] {
2127                truth = crdt_merge_field(&truth, s.clone());
2128            }
2129            let mut deliveries: Vec<usize> = (0..states.len()).chain(0..states.len()).collect();
2130            for i in (1..deliveries.len()).rev() {
2131                let j = rng.upto((i + 1) as u64) as usize;
2132                deliveries.swap(i, j);
2133            }
2134            let mut replica = states[rng.upto(states.len() as u64) as usize].clone();
2135            for &d in &deliveries {
2136                replica = crdt_merge_field(&replica, states[d].clone());
2137            }
2138            assert!(crdt_eq(&replica, &truth), "g-counter replicas converge despite order/duplication");
2139            assert_eq!(gcounter_value(&replica), gcounter_value(&truth), "and agree on the total");
2140        }
2141    }
2142
2143    fn set_of(items: &[i64]) -> RuntimeValue {
2144        RuntimeValue::Set(std::rc::Rc::new(std::cell::RefCell::new(
2145            items.iter().map(|&i| RuntimeValue::Int(i)).collect(),
2146        )))
2147    }
2148
2149    fn rand_set(rng: &mut Rng) -> RuntimeValue {
2150        let mut s = set_of(&[]);
2151        for _ in 0..rng.upto(6) {
2152            s = crate::semantics::collections::union(&s, &set_of(&[rng.upto(8) as i64])).unwrap();
2153        }
2154        s
2155    }
2156
2157    fn rand_map_of_sets(rng: &mut Rng) -> RuntimeValue {
2158        let mut m = crate::interpreter::MapStorage::default();
2159        for _ in 0..rng.upto(5) {
2160            m.insert(RuntimeValue::Int(rng.upto(5) as i64), rand_set(rng));
2161        }
2162        RuntimeValue::Map(std::rc::Rc::new(std::cell::RefCell::new(m)))
2163    }
2164
2165    fn assert_crdt_laws(rng: &mut Rng, gen: impl Fn(&mut Rng) -> RuntimeValue, what: &str) {
2166        for _ in 0..2000 {
2167            let (a, b, c) = (gen(rng), gen(rng), gen(rng));
2168            assert!(
2169                crdt_eq(&crdt_merge_field(&a, b.clone()), &crdt_merge_field(&b, a.clone())),
2170                "{what} merge must be COMMUTATIVE"
2171            );
2172            assert!(crdt_eq(&crdt_merge_field(&a, a.clone()), &a), "{what} merge must be IDEMPOTENT");
2173            let ab = crdt_merge_field(&a, b.clone());
2174            assert!(crdt_eq(&crdt_merge_field(&ab, b.clone()), &ab), "{what}: re-merging a seen value is a no-op");
2175            let l = crdt_merge_field(&crdt_merge_field(&a, b.clone()), c.clone());
2176            let r = crdt_merge_field(&a, crdt_merge_field(&b, c.clone()));
2177            assert!(crdt_eq(&l, &r), "{what} merge must be ASSOCIATIVE");
2178        }
2179    }
2180
2181    #[test]
2182    fn crdt_set_merge_obeys_the_crdt_laws() {
2183        assert_crdt_laws(&mut Rng::new(0x00C0_FFEE), rand_set, "set");
2184    }
2185
2186    #[test]
2187    fn crdt_map_of_sets_merge_obeys_the_crdt_laws() {
2188        // A CRDT map of CRDT sets — "shared memory" over the network — is itself a CRDT.
2189        assert_crdt_laws(&mut Rng::new(0x0000_BEEF), rand_map_of_sets, "map-of-sets");
2190    }
2191
2192    #[test]
2193    fn crdt_replicas_converge_under_any_order_and_duplication() {
2194        // The property that matters on a real gossip/lossy network: a replica that receives
2195        // every state in a SHUFFLED order, some delivered TWICE, still ends at the exact LUB.
2196        let mut rng = Rng::new(0x0000_5EED);
2197        for _ in 0..500 {
2198            let states: Vec<RuntimeValue> = (0..4).map(|_| rand_map_of_sets(&mut rng)).collect();
2199            let mut truth = states[0].clone();
2200            for s in &states[1..] {
2201                truth = crdt_merge_field(&truth, s.clone());
2202            }
2203            // Deliver each state once, then a duplicate of each, in a shuffled order.
2204            let mut deliveries: Vec<usize> = (0..states.len()).chain(0..states.len()).collect();
2205            for i in (1..deliveries.len()).rev() {
2206                let j = rng.upto((i + 1) as u64) as usize;
2207                deliveries.swap(i, j);
2208            }
2209            let mut replica = states[rng.upto(states.len() as u64) as usize].clone();
2210            for &d in &deliveries {
2211                replica = crdt_merge_field(&replica, states[d].clone());
2212            }
2213            assert!(crdt_eq(&replica, &truth), "every replica must converge to the LUB despite order/duplication");
2214        }
2215    }
2216
2217    #[test]
2218    fn crdt_map_round_trips_and_merges_through_the_wire() {
2219        // The CRDT map survives the JSON relay wire (the tagged `__map` form) and the wire
2220        // merge equals the in-memory merge — sync-over-network for shared memory.
2221        let mut rng = Rng::new(0x0057_17E0);
2222        for _ in 0..400 {
2223            let local = rand_map_of_sets(&mut rng);
2224            let incoming = rand_map_of_sets(&mut rng);
2225            let expected = crdt_merge_field(&local, incoming.clone());
2226            let bytes = crdt_to_wire(&incoming).expect("a map publishes to the wire");
2227            let merged = crdt_merge_wire(local.clone(), &bytes);
2228            assert!(crdt_eq(&merged, &expected), "wire merge must equal the in-memory merge");
2229        }
2230    }
2231}
2232
2233#[cfg(test)]
2234mod word_tests {
2235    use super::*;
2236    use logicaffeine_base::{Word32, Word64, WordVal};
2237
2238    fn w32(n: u32) -> RuntimeValue {
2239        RuntimeValue::Word(WordVal::W32(Word32(n)))
2240    }
2241    fn w64(n: u64) -> RuntimeValue {
2242        RuntimeValue::Word(WordVal::W64(Word64(n)))
2243    }
2244
2245    #[test]
2246    fn word_arithmetic_wraps_through_binary_op() {
2247        // The ring ℤ/2³²: MAX + 1 wraps to 0 and never promotes to BigInt.
2248        assert_eq!(binary_op(BinaryOpKind::Add, w32(0xFFFF_FFFF), w32(1)).unwrap(), w32(0));
2249        assert_eq!(binary_op(BinaryOpKind::Subtract, w32(0), w32(1)).unwrap(), w32(0xFFFF_FFFF));
2250        assert_eq!(binary_op(BinaryOpKind::Multiply, w32(0x1000_0000), w32(0x10)).unwrap(), w32(0));
2251        assert_eq!(binary_op(BinaryOpKind::BitXor, w32(0xFF00), w32(0x0FF0)).unwrap(), w32(0xF0F0));
2252        assert_eq!(binary_op(BinaryOpKind::And, w32(0xFF00), w32(0x0FF0)).unwrap(), w32(0x0F00));
2253        assert_eq!(binary_op(BinaryOpKind::Or, w32(0xFF00), w32(0x0FF0)).unwrap(), w32(0xFFF0));
2254    }
2255
2256    #[test]
2257    fn word_shift_takes_an_integer_count() {
2258        assert_eq!(binary_op(BinaryOpKind::Shl, w32(1), RuntimeValue::Int(8)).unwrap(), w32(0x100));
2259        assert_eq!(binary_op(BinaryOpKind::Shr, w32(0x100), RuntimeValue::Int(8)).unwrap(), w32(1));
2260    }
2261
2262    #[test]
2263    fn word_equality_and_width_mismatch_is_typed() {
2264        assert!(values_equal(&w32(5), &w32(5)));
2265        assert!(!values_equal(&w32(5), &w64(5)), "different widths are never equal");
2266        assert!(
2267            binary_op(BinaryOpKind::Add, w32(1), w64(1)).is_err(),
2268            "Word32 + Word64 must error, not silently coerce"
2269        );
2270    }
2271}