Skip to main content

doge_runtime/stdlib/
nerd.rs

1//! The `nerd` module: numeric helpers over Int, Float, and Decimal. Consts
2//! `pi`/`e` never reach here — codegen emits them as `Value::Float` literals.
3
4use std::cmp::Ordering;
5
6use bigdecimal::{FromPrimitive, RoundingMode, Signed, ToPrimitive};
7use num_bigint::BigInt;
8
9use crate::error::{DogeError, DogeResult};
10use crate::value::Value;
11
12/// A numeric argument as `f64`, or a catchable type error naming the function.
13/// Used by the always-Float helpers (`sqrt`); exactness is lost on purpose.
14fn numeric(fname: &str, v: &Value) -> DogeResult<f64> {
15    let f = match v {
16        Value::Int(n) => n.to_f64(),
17        Value::Float(f) => Some(*f),
18        Value::Decimal(d) => d.to_f64(),
19        _ => {
20            return Err(DogeError::type_error(format!(
21                "nerd.{fname} needs a number, got {}",
22                v.describe()
23            )))
24        }
25    };
26    f.ok_or_else(|| DogeError::overflow(format!("{v} is too large for nerd.{fname}")))
27}
28
29/// Reject a non-numeric argument for the type-preserving helpers (`min`/`max`).
30fn ensure_number(fname: &str, v: &Value) -> DogeResult<()> {
31    if matches!(v, Value::Int(_) | Value::Float(_) | Value::Decimal(_)) {
32        Ok(())
33    } else {
34        Err(DogeError::type_error(format!(
35            "nerd.{fname} needs a number, got {}",
36            v.describe()
37        )))
38    }
39}
40
41/// Turn a `f64` result back into an Int. Any finite value converts (Int is
42/// arbitrary precision); only an infinity or NaN — which a floor/ceil/round can
43/// never legitimately produce from a finite input — is a catchable Overflow.
44fn float_to_int(f: f64) -> DogeResult {
45    if f.is_finite() {
46        BigInt::from_f64(f)
47            .map(Value::Int)
48            .ok_or_else(|| DogeError::overflow("the result is outside the Int range"))
49    } else {
50        Err(DogeError::overflow("the result is outside the Int range"))
51    }
52}
53
54/// Shared by floor/ceil/round: an Int passes straight through, a Float is
55/// transformed and narrowed back to an Int, a Decimal is rounded exactly to an
56/// integer using `mode`.
57fn round_like(
58    fname: &str,
59    x: &Value,
60    float_op: impl Fn(f64) -> f64,
61    mode: RoundingMode,
62) -> DogeResult {
63    match x {
64        Value::Int(n) => Ok(Value::Int(n.clone())),
65        Value::Float(f) => float_to_int(float_op(*f)),
66        Value::Decimal(d) => {
67            let (digits, _) = d.with_scale_round(0, mode).into_bigint_and_exponent();
68            Ok(Value::Int(digits))
69        }
70        _ => Err(DogeError::type_error(format!(
71            "nerd.{fname} needs a number, got {}",
72            x.describe()
73        ))),
74    }
75}
76
77/// `nerd.abs(x)` — magnitude, keeping the argument's own type. An Int stays an
78/// Int (and never overflows — it is arbitrary precision), a Float a Float, a
79/// Decimal a Decimal.
80pub fn nerd_abs(x: &Value) -> DogeResult {
81    match x {
82        Value::Int(n) => Ok(Value::Int(n.abs())),
83        Value::Float(f) => Ok(Value::Float(f.abs())),
84        Value::Decimal(d) => Ok(Value::decimal(d.abs())),
85        _ => Err(DogeError::type_error(format!(
86            "nerd.abs needs a number, got {}",
87            x.describe()
88        ))),
89    }
90}
91
92/// `nerd.sqrt(x)` — always a Float. A negative input is a catchable ValueError.
93pub fn nerd_sqrt(x: &Value) -> DogeResult {
94    let f = numeric("sqrt", x)?;
95    if f < 0.0 {
96        return Err(DogeError::value_error("cannot sqrt a negative number"));
97    }
98    Ok(Value::Float(f.sqrt()))
99}
100
101/// `nerd.floor(x)` — round toward negative infinity, yielding an Int.
102pub fn nerd_floor(x: &Value) -> DogeResult {
103    round_like("floor", x, f64::floor, RoundingMode::Floor)
104}
105
106/// `nerd.ceil(x)` — round toward positive infinity, yielding an Int.
107pub fn nerd_ceil(x: &Value) -> DogeResult {
108    round_like("ceil", x, f64::ceil, RoundingMode::Ceiling)
109}
110
111/// `nerd.round(x)` — round half away from zero, yielding an Int.
112pub fn nerd_round(x: &Value) -> DogeResult {
113    round_like("round", x, f64::round, RoundingMode::HalfUp)
114}
115
116/// `nerd.min(a, b)` — the smaller of two numbers, keeping the winner's own type;
117/// a tie returns `a`. Compares exactly across Int/Decimal, via `f64` once a Float
118/// is involved.
119pub fn nerd_min(a: &Value, b: &Value) -> DogeResult {
120    ensure_number("min", a)?;
121    ensure_number("min", b)?;
122    Ok(if crate::ops::order(a, b)? == Ordering::Greater {
123        b.clone()
124    } else {
125        a.clone()
126    })
127}
128
129/// `nerd.max(a, b)` — the larger of two numbers, keeping the winner's own type;
130/// a tie returns `a`.
131pub fn nerd_max(a: &Value, b: &Value) -> DogeResult {
132    ensure_number("max", a)?;
133    ensure_number("max", b)?;
134    Ok(if crate::ops::order(a, b)? == Ordering::Less {
135        b.clone()
136    } else {
137        a.clone()
138    })
139}
140
141/// `nerd.pow(base, exponent)` — the same rule as the `**` operator: Int^Int (a
142/// non-negative exponent) stays an arbitrary-precision Int, Decimal^Int an exact
143/// Decimal, and any other numeric mix returns a Float.
144pub fn nerd_pow(a: &Value, b: &Value) -> DogeResult {
145    crate::ops::pow(a.clone(), b.clone())
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use crate::error::ErrorKind;
152    use crate::ops::values_equal;
153
154    fn dec(s: &str) -> Value {
155        Value::decimal(s.parse().unwrap())
156    }
157
158    #[test]
159    fn abs_keeps_type_and_never_overflows() {
160        assert!(values_equal(
161            &nerd_abs(&Value::int(-5)).unwrap(),
162            &Value::int(5)
163        ));
164        assert!(matches!(nerd_abs(&Value::Float(-2.5)).unwrap(), Value::Float(f) if f == 2.5));
165        assert!(values_equal(&nerd_abs(&dec("-2.5")).unwrap(), &dec("2.5")));
166        // abs(i64::MIN) no longer overflows — it grows past the range.
167        let expected = Value::Int(-BigInt::from(i64::MIN));
168        assert!(values_equal(
169            &nerd_abs(&Value::int(i64::MIN)).unwrap(),
170            &expected
171        ));
172        assert_eq!(
173            nerd_abs(&Value::str("x")).unwrap_err().kind,
174            ErrorKind::TypeError
175        );
176    }
177
178    #[test]
179    fn sqrt_is_float_and_rejects_negatives() {
180        assert!(matches!(nerd_sqrt(&Value::int(16)).unwrap(), Value::Float(f) if f == 4.0));
181        assert_eq!(
182            nerd_sqrt(&Value::int(-1)).unwrap_err().kind,
183            ErrorKind::ValueError
184        );
185    }
186
187    #[test]
188    fn floor_ceil_round_yield_ints() {
189        assert!(values_equal(
190            &nerd_floor(&Value::Float(2.9)).unwrap(),
191            &Value::int(2)
192        ));
193        assert!(values_equal(
194            &nerd_ceil(&Value::Float(2.1)).unwrap(),
195            &Value::int(3)
196        ));
197        assert!(values_equal(
198            &nerd_round(&Value::Float(2.5)).unwrap(),
199            &Value::int(3)
200        ));
201        assert!(values_equal(
202            &nerd_floor(&Value::int(7)).unwrap(),
203            &Value::int(7)
204        ));
205        // Decimals round exactly.
206        assert!(values_equal(
207            &nerd_floor(&dec("2.9")).unwrap(),
208            &Value::int(2)
209        ));
210        assert!(values_equal(
211            &nerd_ceil(&dec("-2.9")).unwrap(),
212            &Value::int(-2)
213        ));
214        assert!(values_equal(
215            &nerd_round(&dec("2.5")).unwrap(),
216            &Value::int(3)
217        ));
218    }
219
220    #[test]
221    fn round_of_a_non_finite_float_is_overflow() {
222        assert_eq!(
223            nerd_floor(&Value::Float(f64::INFINITY)).unwrap_err().kind,
224            ErrorKind::Overflow
225        );
226    }
227
228    #[test]
229    fn min_max_keep_the_winners_type() {
230        assert!(
231            matches!(nerd_min(&Value::int(3), &Value::Float(2.5)).unwrap(), Value::Float(f) if f == 2.5)
232        );
233        assert!(values_equal(
234            &nerd_max(&Value::int(3), &Value::Float(2.5)).unwrap(),
235            &Value::int(3)
236        ));
237        // A Decimal can win and keeps its type.
238        assert!(values_equal(
239            &nerd_min(&Value::int(3), &dec("1.5")).unwrap(),
240            &dec("1.5")
241        ));
242        // Ties return the first argument, unchanged.
243        assert!(values_equal(
244            &nerd_min(&Value::int(4), &Value::Float(4.0)).unwrap(),
245            &Value::int(4)
246        ));
247        assert_eq!(
248            nerd_min(&Value::str("a"), &Value::str("b"))
249                .unwrap_err()
250                .kind,
251            ErrorKind::TypeError
252        );
253    }
254
255    #[test]
256    fn pow_is_int_for_int_base_and_grows_past_i64() {
257        assert!(values_equal(
258            &nerd_pow(&Value::int(2), &Value::int(10)).unwrap(),
259            &Value::int(1024)
260        ));
261        assert!(matches!(
262            nerd_pow(&Value::int(2), &Value::Float(0.5)).unwrap(),
263            Value::Float(_)
264        ));
265        // 10^100 is exact now, never an overflow.
266        let expected = Value::Int(BigInt::from(10).pow(100u32));
267        assert!(values_equal(
268            &nerd_pow(&Value::int(10), &Value::int(100)).unwrap(),
269            &expected
270        ));
271    }
272}