Skip to main content

pine_interpreter/
num.rs

1//! Pine's two numeric types, and the rule that decides which one an operation
2//! produces.
3//!
4//! Pine overloads on `int` vs `float`: `math.max(int, int)` returns an int,
5//! `math.max(int, float)` a float, and `15 / 2` is `7` where `15 / 2.0` is
6//! `7.5`. That rule is written down once, here, so the interpreter's operators
7//! and the builtins cannot drift apart.
8//!
9//! A builtin opts into the rule by declaring a field as [`Num`]; one that always
10//! returns a float regardless of its input (`ta.sma`, `math.avg`) declares `f64`
11//! instead and never sees an int. The field type *is* the spec's return type.
12
13use std::ops::{Add, Div, Mul, Rem, Sub};
14
15/// A Pine number: either an `int` or a `float`.
16#[derive(Debug, Clone, Copy, PartialEq)]
17pub enum Num {
18    Int(i64),
19    Float(f64),
20}
21
22impl Num {
23    /// The value as a float, for computations that do not care about the type.
24    pub fn as_f64(self) -> f64 {
25        match self {
26            Num::Int(n) => n as f64,
27            Num::Float(n) => n,
28        }
29    }
30
31    pub fn is_int(self) -> bool {
32        matches!(self, Num::Int(_))
33    }
34
35    /// Combine two numbers under Pine's rule: two ints stay an int, anything
36    /// else widens to float. Either closure returns `None` where Pine has no
37    /// result (a zero divisor), which callers turn into `na`.
38    fn combine(
39        self,
40        other: Num,
41        int_op: impl Fn(i64, i64) -> Option<i64>,
42        float_op: impl Fn(f64, f64) -> Option<f64>,
43    ) -> Option<Num> {
44        match (self, other) {
45            (Num::Int(a), Num::Int(b)) => int_op(a, b).map(Num::Int),
46            _ => float_op(self.as_f64(), other.as_f64()).map(Num::Float),
47        }
48    }
49
50    /// The larger of two numbers, keeping the type Pine's overloads specify.
51    pub fn max(self, other: Num) -> Num {
52        self.combine(other, |a, b| Some(a.max(b)), |a, b| Some(a.max(b)))
53            .expect("max is total")
54    }
55
56    /// The smaller of two numbers, keeping the type Pine's overloads specify.
57    pub fn min(self, other: Num) -> Num {
58        self.combine(other, |a, b| Some(a.min(b)), |a, b| Some(a.min(b)))
59            .expect("min is total")
60    }
61
62    /// The magnitude, keeping the type.
63    pub fn abs(self) -> Num {
64        match self {
65            Num::Int(n) => Num::Int(n.abs()),
66            Num::Float(n) => Num::Float(n.abs()),
67        }
68    }
69
70    /// Division, or `None` for a zero divisor — Pine yields `na` rather than
71    /// erroring. Two ints divide as ints, so `15 / 2` is `7`.
72    pub fn checked_div(self, other: Num) -> Option<Num> {
73        self.combine(
74            other,
75            |a, b| (b != 0).then(|| a / b),
76            |a, b| (b != 0.0).then(|| a / b),
77        )
78    }
79
80    /// Remainder, or `None` for a zero divisor.
81    pub fn checked_rem(self, other: Num) -> Option<Num> {
82        self.combine(
83            other,
84            |a, b| (b != 0).then(|| a % b),
85            |a, b| (b != 0.0).then(|| a % b),
86        )
87    }
88}
89
90impl Add for Num {
91    type Output = Num;
92    fn add(self, other: Num) -> Num {
93        self.combine(other, |a, b| Some(a + b), |a, b| Some(a + b))
94            .expect("addition is total")
95    }
96}
97
98impl Sub for Num {
99    type Output = Num;
100    fn sub(self, other: Num) -> Num {
101        self.combine(other, |a, b| Some(a - b), |a, b| Some(a - b))
102            .expect("subtraction is total")
103    }
104}
105
106impl Mul for Num {
107    type Output = Num;
108    fn mul(self, other: Num) -> Num {
109        self.combine(other, |a, b| Some(a * b), |a, b| Some(a * b))
110            .expect("multiplication is total")
111    }
112}
113
114impl Div for Num {
115    type Output = Num;
116    /// Prefer [`Num::checked_div`]; this yields NaN on a zero divisor.
117    fn div(self, other: Num) -> Num {
118        self.checked_div(other).unwrap_or(Num::Float(f64::NAN))
119    }
120}
121
122impl Rem for Num {
123    type Output = Num;
124    /// Prefer [`Num::checked_rem`]; this yields NaN on a zero divisor.
125    fn rem(self, other: Num) -> Num {
126        self.checked_rem(other).unwrap_or(Num::Float(f64::NAN))
127    }
128}
129
130impl From<i64> for Num {
131    fn from(n: i64) -> Self {
132        Num::Int(n)
133    }
134}
135
136impl From<f64> for Num {
137    fn from(n: f64) -> Self {
138        Num::Float(n)
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn two_ints_stay_an_int() {
148        assert_eq!(Num::Int(3) + Num::Int(4), Num::Int(7));
149        assert_eq!(Num::Int(3) * Num::Int(4), Num::Int(12));
150        // The case that started this: Pine's `15 / 2` is 7, not 7.5.
151        assert_eq!(Num::Int(15).checked_div(Num::Int(2)), Some(Num::Int(7)));
152    }
153
154    #[test]
155    fn a_float_operand_widens_the_result() {
156        assert_eq!(
157            Num::Int(15).checked_div(Num::Float(2.0)),
158            Some(Num::Float(7.5))
159        );
160        assert_eq!(
161            Num::Float(15.0).checked_div(Num::Int(2)),
162            Some(Num::Float(7.5))
163        );
164        assert_eq!(Num::Int(3) + Num::Float(0.5), Num::Float(3.5));
165    }
166
167    #[test]
168    fn a_zero_divisor_has_no_result() {
169        assert_eq!(Num::Int(1).checked_div(Num::Int(0)), None);
170        assert_eq!(Num::Float(1.0).checked_div(Num::Float(0.0)), None);
171        assert_eq!(Num::Int(1).checked_rem(Num::Int(0)), None);
172    }
173
174    #[test]
175    fn max_and_min_keep_the_type() {
176        assert_eq!(Num::Int(3).max(Num::Int(4)), Num::Int(4));
177        assert_eq!(Num::Int(3).max(Num::Float(4.0)), Num::Float(4.0));
178        assert_eq!(Num::Int(3).min(Num::Int(4)), Num::Int(3));
179        assert_eq!(Num::Int(-3).abs(), Num::Int(3));
180    }
181}