uni_core/primitives/
modulo.rs

1// RUST CONCEPT: Modular primitive organization
2// Each primitive gets its own file with implementation and tests
3use crate::compat::format;
4use crate::interpreter::Interpreter;
5use crate::primitives::numeric_promotion::promote_pair;
6use crate::value::{RuntimeError, Value};
7use num_traits::Zero;
8
9// RUST CONCEPT: Modulo operation with zero checking and type promotion
10// Stack-based modulo: ( n1 n2 -- remainder )
11// Supports all numeric types with automatic promotion
12pub fn mod_builtin(interp: &mut Interpreter) -> Result<(), RuntimeError> {
13    let b = interp.pop()?;
14    let a = interp.pop()?;
15
16    // Check for modulo by zero
17    let is_zero = match &b {
18        Value::Int32(i) => *i == 0,
19        Value::Integer(i) => i.is_zero(),
20        Value::Rational(r) => r.is_zero(),
21        Value::Number(n) => *n == 0.0,
22        _ => false,
23    };
24    if is_zero {
25        return Err(RuntimeError::ModuloByZero);
26    }
27
28    // Promote both values to a common type
29    let (pa, pb) = promote_pair(&a, &b);
30
31    let result = match (&pa, &pb) {
32        (Value::Int32(i1), Value::Int32(i2)) => Value::Int32(i1 % i2),
33        (Value::Integer(i1), Value::Integer(i2)) => Value::Integer(i1 % i2),
34        (Value::Rational(r1), Value::Rational(r2)) => {
35            let result = Value::Rational(r1 % r2);
36            result.demote()
37        }
38        (Value::Number(n1), Value::Number(n2)) => Value::Number(n1 % n2),
39        _ => {
40            return Err(RuntimeError::TypeError(format!(
41                "Cannot compute modulo of {:?} and {:?}",
42                a, b
43            )))
44        }
45    };
46
47    interp.push(result);
48    Ok(())
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54    use crate::value::Value;
55    use num_bigint::BigInt;
56    use num_rational::BigRational;
57
58    fn setup_interpreter() -> Interpreter {
59        Interpreter::new()
60    }
61
62    #[test]
63    fn test_mod_builtin_floats() {
64        let mut interp = setup_interpreter();
65
66        // Test basic modulo: 13.0 % 5.0 = 3.0
67        interp.push(Value::Number(13.0));
68        interp.push(Value::Number(5.0));
69        mod_builtin(&mut interp).unwrap();
70
71        let result = interp.pop().unwrap();
72        assert!(matches!(result, Value::Number(n) if n == 3.0));
73
74        // Test with exact division: 12.0 % 4.0 = 0.0
75        interp.push(Value::Number(12.0));
76        interp.push(Value::Number(4.0));
77        mod_builtin(&mut interp).unwrap();
78
79        let result = interp.pop().unwrap();
80        assert!(matches!(result, Value::Number(n) if n == 0.0));
81
82        // Test with negative numbers: -7.0 % 3.0
83        interp.push(Value::Number(-7.0));
84        interp.push(Value::Number(3.0));
85        mod_builtin(&mut interp).unwrap();
86
87        let result = interp.pop().unwrap();
88        if let Value::Number(n) = result {
89            // Rust's % follows the dividend's sign
90            assert_eq!(n, -1.0);
91        } else {
92            panic!("Expected number");
93        }
94    }
95
96    #[test]
97    fn test_mod_builtin_integers() {
98        let mut interp = setup_interpreter();
99
100        // Test Integer % Integer: 13 % 5 = 3
101        interp.push(Value::Integer(BigInt::from(13)));
102        interp.push(Value::Integer(BigInt::from(5)));
103        mod_builtin(&mut interp).unwrap();
104
105        let result = interp.pop().unwrap();
106        assert!(matches!(result, Value::Integer(ref i) if *i == BigInt::from(3)));
107
108        // Test with exact division: 12 % 4 = 0
109        interp.push(Value::Integer(BigInt::from(12)));
110        interp.push(Value::Integer(BigInt::from(4)));
111        mod_builtin(&mut interp).unwrap();
112
113        let result = interp.pop().unwrap();
114        assert!(matches!(result, Value::Integer(ref i) if *i == BigInt::from(0)));
115
116        // Test with negative: -17 % 5 = -2
117        interp.push(Value::Integer(BigInt::from(-17)));
118        interp.push(Value::Integer(BigInt::from(5)));
119        mod_builtin(&mut interp).unwrap();
120
121        let result = interp.pop().unwrap();
122        assert!(matches!(result, Value::Integer(ref i) if *i == BigInt::from(-2)));
123    }
124
125    #[test]
126    fn test_mod_builtin_rationals() {
127        let mut interp = setup_interpreter();
128
129        // Test Rational % Rational: (7/2) % (3/2) = 1/2
130        interp.push(Value::Rational(BigRational::new(
131            BigInt::from(7),
132            BigInt::from(2),
133        )));
134        interp.push(Value::Rational(BigRational::new(
135            BigInt::from(3),
136            BigInt::from(2),
137        )));
138        mod_builtin(&mut interp).unwrap();
139
140        let result = interp.pop().unwrap();
141        if let Value::Rational(r) = result {
142            assert_eq!(*r.numer(), BigInt::from(1));
143            assert_eq!(*r.denom(), BigInt::from(2));
144        } else {
145            panic!("Expected Rational, got {:?}", result);
146        }
147    }
148
149    #[test]
150    fn test_mod_builtin_mixed_types() {
151        let mut interp = setup_interpreter();
152
153        // Test Integer % Number: 13 % 5.0 = 3.0
154        interp.push(Value::Integer(BigInt::from(13)));
155        interp.push(Value::Number(5.0));
156        mod_builtin(&mut interp).unwrap();
157
158        let result = interp.pop().unwrap();
159        assert!(matches!(result, Value::Number(n) if n == 3.0));
160
161        // Test Rational % Integer: (7/2) % 2 = 3/2
162        interp.push(Value::Rational(BigRational::new(
163            BigInt::from(7),
164            BigInt::from(2),
165        )));
166        interp.push(Value::Integer(BigInt::from(2)));
167        mod_builtin(&mut interp).unwrap();
168
169        let result = interp.pop().unwrap();
170        if let Value::Rational(r) = result {
171            assert_eq!(*r.numer(), BigInt::from(3));
172            assert_eq!(*r.denom(), BigInt::from(2));
173        } else {
174            panic!("Expected Rational, got {:?}", result);
175        }
176    }
177
178    #[test]
179    fn test_mod_builtin_modulo_by_zero() {
180        let mut interp = setup_interpreter();
181
182        // Test modulo by zero with floats
183        interp.push(Value::Number(5.0));
184        interp.push(Value::Number(0.0));
185        let result = mod_builtin(&mut interp);
186        assert!(matches!(result, Err(RuntimeError::ModuloByZero)));
187
188        // Test modulo by zero with integers
189        interp.push(Value::Integer(BigInt::from(5)));
190        interp.push(Value::Integer(BigInt::from(0)));
191        let result = mod_builtin(&mut interp);
192        assert!(matches!(result, Err(RuntimeError::ModuloByZero)));
193
194        // Test modulo by zero with rationals
195        interp.push(Value::Rational(BigRational::new(
196            BigInt::from(5),
197            BigInt::from(1),
198        )));
199        interp.push(Value::Rational(BigRational::new(
200            BigInt::from(0),
201            BigInt::from(1),
202        )));
203        let result = mod_builtin(&mut interp);
204        assert!(matches!(result, Err(RuntimeError::ModuloByZero)));
205    }
206
207    #[test]
208    fn test_mod_builtin_stack_underflow() {
209        let mut interp = setup_interpreter();
210
211        // Test with empty stack
212        let result = mod_builtin(&mut interp);
213        assert!(matches!(result, Err(RuntimeError::StackUnderflow)));
214
215        // Test with only one element
216        interp.push(Value::Number(5.0));
217        let result = mod_builtin(&mut interp);
218        assert!(matches!(result, Err(RuntimeError::StackUnderflow)));
219    }
220
221    #[test]
222    fn test_mod_builtin_type_error() {
223        let mut interp = setup_interpreter();
224
225        // Test with wrong types
226        interp.push(Value::Number(5.0));
227        interp.push(Value::String("not a number".into()));
228        let result = mod_builtin(&mut interp);
229        assert!(matches!(result, Err(RuntimeError::TypeError(_))));
230    }
231}