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