Skip to main content

doge_runtime/ops/
arith.rs

1use bigdecimal::{BigDecimal, Pow, RoundingMode, Signed, ToPrimitive, Zero};
2use num_bigint::BigInt;
3
4use super::{as_decimal, as_f64, is_decimal, is_float};
5use crate::error::{DogeError, DogeResult};
6use crate::value::Value;
7
8fn type_err_binop(sym: &str, a: &Value, b: &Value) -> DogeError {
9    DogeError::type_error(format!(
10        "cannot {sym} {} and {}",
11        a.describe(),
12        b.describe()
13    ))
14}
15
16fn div_by_zero(sym: &str) -> DogeError {
17    DogeError::division_by_zero(format!("cannot {sym} by zero"))
18}
19
20/// A `Float`/`Decimal` arithmetic mix. Decimal is exact and Float is not, so
21/// silently joining them would corrupt the exact value — the fix is to convert
22/// one side explicitly.
23fn mix_float_decimal(sym: &str, a: &Value, b: &Value) -> DogeError {
24    DogeError::type_error(format!(
25        "cannot {sym} {} and {} — Decimal is exact and Float is not; convert one with dec() or float()",
26        a.describe(),
27        b.describe()
28    ))
29}
30
31/// Shared resolver for `+`/`-`/`*` once the type-specific arms (Int/Int, Str, …)
32/// have been ruled out: exact `Decimal` math when a Decimal is involved (a
33/// Float/Decimal mix is rejected), otherwise `Float` promotion.
34fn numeric_binop(
35    sym: &str,
36    a: &Value,
37    b: &Value,
38    dec_op: impl Fn(BigDecimal, BigDecimal) -> BigDecimal,
39    float_op: impl Fn(f64, f64) -> f64,
40) -> DogeResult {
41    if is_decimal(a) || is_decimal(b) {
42        if is_float(a) || is_float(b) {
43            return Err(mix_float_decimal(sym, a, b));
44        }
45        return match (as_decimal(a), as_decimal(b)) {
46            (Some(x), Some(y)) => Ok(Value::decimal(dec_op(x, y))),
47            _ => Err(type_err_binop(sym, a, b)),
48        };
49    }
50    match (as_f64(a), as_f64(b)) {
51        (Some(x), Some(y)) => Ok(Value::Float(float_op(x, y))),
52        _ => Err(type_err_binop(sym, a, b)),
53    }
54}
55
56/// Floor division on arbitrary-precision integers: truncate toward zero, then
57/// nudge down one when the remainder is non-zero and the signs differ (Python).
58fn bigint_floordiv(x: &BigInt, y: &BigInt) -> BigInt {
59    let q = x / y;
60    let r = x % y;
61    if !r.is_zero() && (r.is_negative() != y.is_negative()) {
62        q - BigInt::from(1)
63    } else {
64        q
65    }
66}
67
68/// `floor(x / y)` as an exact integer-valued `Decimal`.
69fn decimal_floordiv(x: &BigDecimal, y: &BigDecimal) -> BigDecimal {
70    (x / y).with_scale_round(0, RoundingMode::Floor)
71}
72
73/// `+` — Int+Int (arbitrary precision), Float/Decimal promotion, Str/Bytes/List
74/// concatenation, Error-as-message concatenation.
75pub fn add(a: Value, b: Value) -> DogeResult {
76    match (&a, &b) {
77        (Value::Int(x), Value::Int(y)) => Ok(Value::int(x + y)),
78        (Value::Str(x), Value::Str(y)) => Ok(Value::str(format!("{x}{y}"))),
79        (Value::Bytes(x), Value::Bytes(y)) => {
80            let mut joined = Vec::with_capacity(x.len() + y.len());
81            joined.extend_from_slice(x);
82            joined.extend_from_slice(y);
83            Ok(Value::bytes(joined))
84        }
85        // An Error concatenates with a Str as its message, so `"caught: " + err`
86        // reads the same as barking the error. Every other `Str + x` stays a type
87        // error — an Error is special only because its payload is text.
88        (Value::Str(x), Value::Error(e)) => Ok(Value::str(format!("{x}{}", e.message))),
89        (Value::Error(e), Value::Str(y)) => Ok(Value::str(format!("{}{y}", e.message))),
90        (Value::List(x), Value::List(y)) => {
91            let mut joined = x.borrow().clone();
92            joined.extend(y.borrow().iter().cloned());
93            Ok(Value::list(joined))
94        }
95        _ => numeric_binop("+", &a, &b, |x, y| x + y, |x, y| x + y),
96    }
97}
98
99/// `-` — Int-Int (arbitrary precision) or Float/Decimal promotion.
100pub fn sub(a: Value, b: Value) -> DogeResult {
101    if let (Value::Int(x), Value::Int(y)) = (&a, &b) {
102        return Ok(Value::int(x - y));
103    }
104    numeric_binop("-", &a, &b, |x, y| x - y, |x, y| x - y)
105}
106
107/// `*` — Int*Int (arbitrary precision) or Float/Decimal promotion.
108pub fn mul(a: Value, b: Value) -> DogeResult {
109    if let (Value::Int(x), Value::Int(y)) = (&a, &b) {
110        return Ok(Value::int(x * y));
111    }
112    numeric_binop("*", &a, &b, |x, y| x * y, |x, y| x * y)
113}
114
115/// `/` — always a Float for integers (`5 / 2 == 2.5`); exact for decimals
116/// (`Decimal / Decimal` → `Decimal`). A Float/Decimal mix is a type error.
117pub fn div(a: Value, b: Value) -> DogeResult {
118    if is_decimal(&a) || is_decimal(&b) {
119        if is_float(&a) || is_float(&b) {
120            return Err(mix_float_decimal("/", &a, &b));
121        }
122        return match (as_decimal(&a), as_decimal(&b)) {
123            (Some(_), Some(y)) if y.is_zero() => Err(div_by_zero("/")),
124            (Some(x), Some(y)) => Ok(Value::decimal(x / y)),
125            _ => Err(type_err_binop("/", &a, &b)),
126        };
127    }
128    match (as_f64(&a), as_f64(&b)) {
129        (Some(_), Some(0.0)) => Err(div_by_zero("/")),
130        (Some(x), Some(y)) => Ok(Value::Float(x / y)),
131        _ => Err(type_err_binop("/", &a, &b)),
132    }
133}
134
135/// `//` — floor division. Int//Int yields an Int, Decimal//Decimal an exact
136/// integer-valued Decimal, any Float operand a floored Float.
137pub fn floordiv(a: Value, b: Value) -> DogeResult {
138    match (&a, &b) {
139        (Value::Int(x), Value::Int(y)) => {
140            if y.is_zero() {
141                return Err(div_by_zero("//"));
142            }
143            Ok(Value::int(bigint_floordiv(x, y)))
144        }
145        _ if is_decimal(&a) || is_decimal(&b) => {
146            if is_float(&a) || is_float(&b) {
147                return Err(mix_float_decimal("//", &a, &b));
148            }
149            match (as_decimal(&a), as_decimal(&b)) {
150                (Some(_), Some(y)) if y.is_zero() => Err(div_by_zero("//")),
151                (Some(x), Some(y)) => Ok(Value::decimal(decimal_floordiv(&x, &y))),
152                _ => Err(type_err_binop("//", &a, &b)),
153            }
154        }
155        _ => match (as_f64(&a), as_f64(&b)) {
156            (Some(_), Some(0.0)) => Err(div_by_zero("//")),
157            (Some(x), Some(y)) => Ok(Value::Float((x / y).floor())),
158            _ => Err(type_err_binop("//", &a, &b)),
159        },
160    }
161}
162
163/// `%` — modulo whose result takes the sign of the divisor (Python-style), so
164/// that `a == (a // b) * b + (a % b)` holds.
165pub fn rem(a: Value, b: Value) -> DogeResult {
166    match (&a, &b) {
167        (Value::Int(x), Value::Int(y)) => {
168            if y.is_zero() {
169                return Err(div_by_zero("%"));
170            }
171            let r = x % y;
172            let m = if !r.is_zero() && (r.is_negative() != y.is_negative()) {
173                r + y
174            } else {
175                r
176            };
177            Ok(Value::int(m))
178        }
179        _ if is_decimal(&a) || is_decimal(&b) => {
180            if is_float(&a) || is_float(&b) {
181                return Err(mix_float_decimal("%", &a, &b));
182            }
183            match (as_decimal(&a), as_decimal(&b)) {
184                (Some(_), Some(y)) if y.is_zero() => Err(div_by_zero("%")),
185                (Some(x), Some(y)) => {
186                    let f = decimal_floordiv(&x, &y);
187                    Ok(Value::decimal(&x - f * &y))
188                }
189                _ => Err(type_err_binop("%", &a, &b)),
190            }
191        }
192        _ => match (as_f64(&a), as_f64(&b)) {
193            (Some(_), Some(0.0)) => Err(div_by_zero("%")),
194            (Some(x), Some(y)) => {
195                let r = x % y;
196                let m = if r != 0.0 && ((r < 0.0) != (y < 0.0)) {
197                    r + y
198                } else {
199                    r
200                };
201                Ok(Value::Float(m))
202            }
203            _ => Err(type_err_binop("%", &a, &b)),
204        },
205    }
206}
207
208/// `**` — exponentiation. Int raised to a non-negative Int stays an Int
209/// (arbitrary precision); a Decimal raised to a non-negative Int stays an exact
210/// Decimal; a negative exponent or any Float operand promotes to Float. `0 **
211/// <negative>` is a catchable division by zero, and an exponent too large to
212/// materialize is a catchable overflow.
213pub fn pow(a: Value, b: Value) -> DogeResult {
214    match (&a, &b) {
215        (Value::Int(base), Value::Int(exp)) if !exp.is_negative() => {
216            let e = exp
217                .to_u32()
218                .ok_or_else(|| DogeError::overflow(format!("exponent {exp} is too large")))?;
219            Ok(Value::int(Pow::pow(base.clone(), e)))
220        }
221        // A non-negative exponent is handled above, so the only Int base left with
222        // an Int exponent here has a negative exponent: `0 ** <negative>` diverges.
223        (Value::Int(base), Value::Int(_)) if base.is_zero() => Err(div_by_zero("**")),
224        (Value::Decimal(base), Value::Int(exp)) if !exp.is_negative() => {
225            let e = exp
226                .to_u32()
227                .ok_or_else(|| DogeError::overflow(format!("exponent {exp} is too large")))?;
228            let mut result = BigDecimal::from(1);
229            for _ in 0..e {
230                result *= base;
231            }
232            Ok(Value::decimal(result))
233        }
234        _ if is_decimal(&a) || is_decimal(&b) => Err(DogeError::type_error(format!(
235            "cannot raise {} to {} — a Decimal power needs a non-negative Int exponent",
236            a.describe(),
237            b.describe()
238        ))),
239        _ => match (as_f64(&a), as_f64(&b)) {
240            (Some(x), Some(y)) if x == 0.0 && y < 0.0 => Err(div_by_zero("**")),
241            (Some(x), Some(y)) => Ok(Value::Float(x.powf(y))),
242            _ => Err(type_err_binop("**", &a, &b)),
243        },
244    }
245}
246
247/// Unary `-`.
248pub fn neg(a: Value) -> DogeResult {
249    match &a {
250        Value::Int(n) => Ok(Value::int(-n)),
251        Value::Float(f) => Ok(Value::Float(-f)),
252        Value::Decimal(d) => Ok(Value::decimal(-d)),
253        _ => Err(DogeError::type_error(format!(
254            "cannot negate {}",
255            a.describe()
256        ))),
257    }
258}
259
260/// `not` — always succeeds, using Python truthiness.
261pub fn not_(a: Value) -> DogeResult {
262    Ok(Value::Bool(!a.truthy()))
263}