Skip to main content

doge_runtime/stdlib/
nerd.rs

1//! The `nerd` module: numeric helpers over Int and Float. Consts `pi`/`e` never
2//! reach here — codegen emits them as `Value::Float` literals directly.
3
4use crate::error::{DogeError, DogeResult};
5use crate::value::Value;
6
7/// A numeric argument as `f64`, or a catchable type error naming the function.
8fn numeric(fname: &str, v: &Value) -> DogeResult<f64> {
9    match v {
10        Value::Int(n) => Ok(*n as f64),
11        Value::Float(f) => Ok(*f),
12        _ => Err(DogeError::type_error(format!(
13            "nerd.{fname} needs a number, got {}",
14            v.describe()
15        ))),
16    }
17}
18
19/// Turn a `f64` result back into an Int, or a catchable Overflow when it will not
20/// fit — so a floor/ceil/round that lands outside the Int range fails cleanly.
21fn float_to_int(f: f64) -> DogeResult {
22    if f.is_finite() && f >= i64::MIN as f64 && f < i64::MAX as f64 {
23        Ok(Value::Int(f as i64))
24    } else {
25        Err(DogeError::overflow("the result is outside the Int range"))
26    }
27}
28
29/// Shared by floor/ceil/round: an Int passes straight through, a Float is
30/// transformed and narrowed back to an Int.
31fn round_like(fname: &str, x: &Value, op: impl Fn(f64) -> f64) -> DogeResult {
32    match x {
33        Value::Int(n) => Ok(Value::Int(*n)),
34        Value::Float(f) => float_to_int(op(*f)),
35        _ => Err(DogeError::type_error(format!(
36            "nerd.{fname} needs a number, got {}",
37            x.describe()
38        ))),
39    }
40}
41
42/// `nerd.abs(x)` — magnitude. An Int stays an Int (and `abs(i64::MIN)` overflows
43/// catchably); a Float stays a Float.
44pub fn nerd_abs(x: &Value) -> DogeResult {
45    match x {
46        Value::Int(n) => {
47            if *n < 0 {
48                n.checked_neg()
49                    .map(Value::Int)
50                    .ok_or_else(|| DogeError::overflow("the result is outside the Int range"))
51            } else {
52                Ok(Value::Int(*n))
53            }
54        }
55        Value::Float(f) => Ok(Value::Float(f.abs())),
56        _ => Err(DogeError::type_error(format!(
57            "nerd.abs needs a number, got {}",
58            x.describe()
59        ))),
60    }
61}
62
63/// `nerd.sqrt(x)` — always a Float. A negative input is a catchable ValueError.
64pub fn nerd_sqrt(x: &Value) -> DogeResult {
65    let f = numeric("sqrt", x)?;
66    if f < 0.0 {
67        return Err(DogeError::value_error("cannot sqrt a negative number"));
68    }
69    Ok(Value::Float(f.sqrt()))
70}
71
72/// `nerd.floor(x)` — round toward negative infinity, yielding an Int.
73pub fn nerd_floor(x: &Value) -> DogeResult {
74    round_like("floor", x, f64::floor)
75}
76
77/// `nerd.ceil(x)` — round toward positive infinity, yielding an Int.
78pub fn nerd_ceil(x: &Value) -> DogeResult {
79    round_like("ceil", x, f64::ceil)
80}
81
82/// `nerd.round(x)` — round half away from zero, yielding an Int.
83pub fn nerd_round(x: &Value) -> DogeResult {
84    round_like("round", x, f64::round)
85}
86
87/// `nerd.min(a, b)` — the smaller of two numbers, keeping the winner's own type;
88/// a tie returns `a`.
89pub fn nerd_min(a: &Value, b: &Value) -> DogeResult {
90    let (x, y) = (numeric("min", a)?, numeric("min", b)?);
91    Ok(if y < x { b.clone() } else { a.clone() })
92}
93
94/// `nerd.max(a, b)` — the larger of two numbers, keeping the winner's own type;
95/// a tie returns `a`.
96pub fn nerd_max(a: &Value, b: &Value) -> DogeResult {
97    let (x, y) = (numeric("max", a)?, numeric("max", b)?);
98    Ok(if y > x { b.clone() } else { a.clone() })
99}
100
101/// `nerd.pow(base, exponent)` — Int^Int (non-negative exponent) stays an Int and
102/// overflows catchably; any other numeric mix returns a Float.
103pub fn nerd_pow(a: &Value, b: &Value) -> DogeResult {
104    match (a, b) {
105        (Value::Int(base), Value::Int(exp)) if *exp >= 0 => {
106            let exp = u32::try_from(*exp)
107                .map_err(|_| DogeError::overflow("the result is outside the Int range"))?;
108            base.checked_pow(exp)
109                .map(Value::Int)
110                .ok_or_else(|| DogeError::overflow("the result is outside the Int range"))
111        }
112        _ => {
113            let (x, y) = (numeric("pow", a)?, numeric("pow", b)?);
114            Ok(Value::Float(x.powf(y)))
115        }
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use crate::error::ErrorKind;
123
124    #[test]
125    fn abs_keeps_type_and_overflows_catchably() {
126        assert!(matches!(nerd_abs(&Value::Int(-5)).unwrap(), Value::Int(5)));
127        assert!(matches!(nerd_abs(&Value::Float(-2.5)).unwrap(), Value::Float(f) if f == 2.5));
128        assert_eq!(
129            nerd_abs(&Value::Int(i64::MIN)).unwrap_err().kind,
130            ErrorKind::Overflow
131        );
132        assert_eq!(
133            nerd_abs(&Value::str("x")).unwrap_err().kind,
134            ErrorKind::TypeError
135        );
136    }
137
138    #[test]
139    fn sqrt_is_float_and_rejects_negatives() {
140        assert!(matches!(nerd_sqrt(&Value::Int(16)).unwrap(), Value::Float(f) if f == 4.0));
141        assert_eq!(
142            nerd_sqrt(&Value::Int(-1)).unwrap_err().kind,
143            ErrorKind::ValueError
144        );
145    }
146
147    #[test]
148    fn floor_ceil_round_yield_ints() {
149        assert!(matches!(
150            nerd_floor(&Value::Float(2.9)).unwrap(),
151            Value::Int(2)
152        ));
153        assert!(matches!(
154            nerd_ceil(&Value::Float(2.1)).unwrap(),
155            Value::Int(3)
156        ));
157        assert!(matches!(
158            nerd_round(&Value::Float(2.5)).unwrap(),
159            Value::Int(3)
160        ));
161        assert!(matches!(nerd_floor(&Value::Int(7)).unwrap(), Value::Int(7)));
162    }
163
164    #[test]
165    fn round_out_of_range_is_overflow() {
166        assert_eq!(
167            nerd_floor(&Value::Float(1e300)).unwrap_err().kind,
168            ErrorKind::Overflow
169        );
170    }
171
172    #[test]
173    fn min_max_keep_the_winners_type() {
174        assert!(
175            matches!(nerd_min(&Value::Int(3), &Value::Float(2.5)).unwrap(), Value::Float(f) if f == 2.5)
176        );
177        assert!(matches!(
178            nerd_max(&Value::Int(3), &Value::Float(2.5)).unwrap(),
179            Value::Int(3)
180        ));
181        // Ties return the first argument, unchanged.
182        assert!(matches!(
183            nerd_min(&Value::Int(4), &Value::Float(4.0)).unwrap(),
184            Value::Int(4)
185        ));
186    }
187
188    #[test]
189    fn pow_is_int_for_int_base_and_overflows_catchably() {
190        assert!(matches!(
191            nerd_pow(&Value::Int(2), &Value::Int(10)).unwrap(),
192            Value::Int(1024)
193        ));
194        assert!(matches!(
195            nerd_pow(&Value::Int(2), &Value::Float(0.5)).unwrap(),
196            Value::Float(_)
197        ));
198        assert_eq!(
199            nerd_pow(&Value::Int(10), &Value::Int(100))
200                .unwrap_err()
201                .kind,
202            ErrorKind::Overflow
203        );
204    }
205}