Skip to main content

doge_runtime/ops/
arith.rs

1use super::as_f64;
2use crate::error::{DogeError, DogeResult};
3use crate::value::Value;
4
5fn type_err_binop(sym: &str, a: &Value, b: &Value) -> DogeError {
6    DogeError::type_error(format!(
7        "cannot {sym} {} and {}",
8        a.describe(),
9        b.describe()
10    ))
11}
12
13fn overflow(sym: &str, x: i64, y: i64) -> DogeError {
14    DogeError::overflow(format!("{x} {sym} {y} overflowed the Int range"))
15}
16
17fn div_by_zero(sym: &str) -> DogeError {
18    DogeError::division_by_zero(format!("cannot {sym} by zero"))
19}
20
21/// Numeric fallback shared by `sub`/`mul`: promote both operands to Float and
22/// apply `op`, or raise a type error if either operand is non-numeric.
23fn float_fallback(sym: &str, a: &Value, b: &Value, op: impl Fn(f64, f64) -> f64) -> DogeResult {
24    match (as_f64(a), as_f64(b)) {
25        (Some(x), Some(y)) => Ok(Value::Float(op(x, y))),
26        _ => Err(type_err_binop(sym, a, b)),
27    }
28}
29
30/// `+` — Int+Int (checked), Float promotion, Str concatenation, List concatenation.
31pub fn add(a: Value, b: Value) -> DogeResult {
32    match (&a, &b) {
33        (Value::Int(x), Value::Int(y)) => x
34            .checked_add(*y)
35            .map(Value::Int)
36            .ok_or_else(|| overflow("+", *x, *y)),
37        (Value::Str(x), Value::Str(y)) => Ok(Value::str(format!("{x}{y}"))),
38        // An Error concatenates with a Str as its message, so `"caught: " + err`
39        // reads the same as barking the error. Every other `Str + x` stays a type
40        // error — an Error is special only because its payload is text.
41        (Value::Str(x), Value::Error(e)) => Ok(Value::str(format!("{x}{}", e.message))),
42        (Value::Error(e), Value::Str(y)) => Ok(Value::str(format!("{}{y}", e.message))),
43        (Value::List(x), Value::List(y)) => {
44            let mut joined = x.borrow().clone();
45            joined.extend(y.borrow().iter().cloned());
46            Ok(Value::list(joined))
47        }
48        _ => match (as_f64(&a), as_f64(&b)) {
49            (Some(x), Some(y)) => Ok(Value::Float(x + y)),
50            _ => Err(type_err_binop("+", &a, &b)),
51        },
52    }
53}
54
55/// `-` — Int-Int (checked) or Float promotion.
56pub fn sub(a: Value, b: Value) -> DogeResult {
57    match (&a, &b) {
58        (Value::Int(x), Value::Int(y)) => x
59            .checked_sub(*y)
60            .map(Value::Int)
61            .ok_or_else(|| overflow("-", *x, *y)),
62        _ => float_fallback("-", &a, &b, |x, y| x - y),
63    }
64}
65
66/// `*` — Int*Int (checked) or Float promotion.
67pub fn mul(a: Value, b: Value) -> DogeResult {
68    match (&a, &b) {
69        (Value::Int(x), Value::Int(y)) => x
70            .checked_mul(*y)
71            .map(Value::Int)
72            .ok_or_else(|| overflow("*", *x, *y)),
73        _ => float_fallback("*", &a, &b, |x, y| x * y),
74    }
75}
76
77/// `/` — always returns a Float (`5 / 2 == 2.5`), per the sharp-edges table in docs/README.md.
78pub fn div(a: Value, b: Value) -> DogeResult {
79    match (as_f64(&a), as_f64(&b)) {
80        (Some(_), Some(0.0)) => Err(div_by_zero("/")),
81        (Some(x), Some(y)) => Ok(Value::Float(x / y)),
82        _ => Err(type_err_binop("/", &a, &b)),
83    }
84}
85
86/// `//` — floor division. Int//Int yields an Int (floored toward negative
87/// infinity, Python-style); any Float operand yields a floored Float.
88pub fn floordiv(a: Value, b: Value) -> DogeResult {
89    match (&a, &b) {
90        (Value::Int(x), Value::Int(y)) => {
91            if *y == 0 {
92                return Err(div_by_zero("//"));
93            }
94            let q = x.checked_div(*y).ok_or_else(|| overflow("//", *x, *y))?;
95            let r = x % y;
96            // Truncated division rounds toward zero; nudge down one when the
97            // remainder is non-zero and operands have opposite signs.
98            let floored = if r != 0 && ((r < 0) != (*y < 0)) {
99                q - 1
100            } else {
101                q
102            };
103            Ok(Value::Int(floored))
104        }
105        _ => match (as_f64(&a), as_f64(&b)) {
106            (Some(_), Some(0.0)) => Err(div_by_zero("//")),
107            (Some(x), Some(y)) => Ok(Value::Float((x / y).floor())),
108            _ => Err(type_err_binop("//", &a, &b)),
109        },
110    }
111}
112
113/// `%` — modulo whose result takes the sign of the divisor (Python-style), so
114/// that `a == (a // b) * b + (a % b)` holds.
115pub fn rem(a: Value, b: Value) -> DogeResult {
116    match (&a, &b) {
117        (Value::Int(x), Value::Int(y)) => {
118            if *y == 0 {
119                return Err(div_by_zero("%"));
120            }
121            let r = x.checked_rem(*y).ok_or_else(|| overflow("%", *x, *y))?;
122            let m = if r != 0 && ((r < 0) != (*y < 0)) {
123                r + y
124            } else {
125                r
126            };
127            Ok(Value::Int(m))
128        }
129        _ => match (as_f64(&a), as_f64(&b)) {
130            (Some(_), Some(0.0)) => Err(div_by_zero("%")),
131            (Some(x), Some(y)) => {
132                let r = x % y;
133                let m = if r != 0.0 && ((r < 0.0) != (y < 0.0)) {
134                    r + y
135                } else {
136                    r
137                };
138                Ok(Value::Float(m))
139            }
140            _ => Err(type_err_binop("%", &a, &b)),
141        },
142    }
143}
144
145/// `**` — exponentiation. Int raised to a non-negative Int stays an Int
146/// (checked, so it overflows catchably); a negative exponent or any Float
147/// operand promotes to Float. `0 ** <negative>` is a catchable division by zero.
148pub fn pow(a: Value, b: Value) -> DogeResult {
149    match (&a, &b) {
150        (Value::Int(base), Value::Int(exp)) if *exp >= 0 => {
151            let e = u32::try_from(*exp).map_err(|_| overflow("**", *base, *exp))?;
152            base.checked_pow(e)
153                .map(Value::Int)
154                .ok_or_else(|| overflow("**", *base, *exp))
155        }
156        (Value::Int(0), Value::Int(_)) => Err(div_by_zero("**")),
157        _ => match (as_f64(&a), as_f64(&b)) {
158            (Some(0.0), Some(y)) if y < 0.0 => Err(div_by_zero("**")),
159            (Some(x), Some(y)) => Ok(Value::Float(x.powf(y))),
160            _ => Err(type_err_binop("**", &a, &b)),
161        },
162    }
163}
164
165/// Unary `-`.
166pub fn neg(a: Value) -> DogeResult {
167    match &a {
168        Value::Int(n) => n
169            .checked_neg()
170            .map(Value::Int)
171            .ok_or_else(|| DogeError::overflow(format!("-{n} overflowed the Int range"))),
172        Value::Float(f) => Ok(Value::Float(-f)),
173        _ => Err(DogeError::type_error(format!(
174            "cannot negate {}",
175            a.describe()
176        ))),
177    }
178}
179
180/// `not` — always succeeds, using Python truthiness.
181pub fn not_(a: Value) -> DogeResult {
182    Ok(Value::Bool(!a.truthy()))
183}