uni_core/primitives/
roll.rs

1// RUST CONCEPT: Modular primitive organization
2// Each primitive gets its own file with implementation and tests
3use crate::interpreter::Interpreter;
4use crate::value::RuntimeError;
5
6// RUST CONCEPT: ANS Forth roll primitive
7// roll ( n -- ) - Move the nth item from stack to top (destructive)
8// n=0: no-op, n=1: swap top two items, n=2: move third item to top
9// Example: 1 2 3 4  2 roll  ->  1 3 4 2 (item at depth 2 moved to top)
10pub fn roll_builtin(interp: &mut Interpreter) -> Result<(), RuntimeError> {
11    let n = interp.pop_integer()?;
12
13    // RUST CONCEPT: Bounds checking
14    // We need at least n+1 items on the stack
15    if interp.stack.len() < n + 1 {
16        return Err(RuntimeError::StackUnderflow);
17    }
18
19    if n == 0 {
20        // n=0: no operation
21        return Ok(());
22    }
23
24    // RUST CONCEPT: Vec manipulation
25    // Remove the item at position n from the end (0-indexed from top)
26    let stack_len = interp.stack.len();
27    let item = interp.stack.remove(stack_len - n - 1);
28
29    // Push it to the top
30    interp.stack.push(item);
31
32    Ok(())
33}
34
35#[cfg(test)]
36mod tests {
37    use super::*;
38    use crate::value::Value;
39
40    fn setup_interpreter() -> Interpreter {
41        Interpreter::new()
42    }
43
44    #[test]
45    fn test_roll_builtin_basic() {
46        let mut interp = setup_interpreter();
47
48        // Test: 1 2 3 4  2 roll  ->  1 3 4 2
49        interp.push(Value::Number(1.0));
50        interp.push(Value::Number(2.0));
51        interp.push(Value::Number(3.0));
52        interp.push(Value::Number(4.0));
53        interp.push(Value::Number(2.0));
54        roll_builtin(&mut interp).unwrap();
55
56        // Stack should now be: 1 3 4 2 (from bottom to top)
57        let top = interp.pop().unwrap();
58        assert!(matches!(top, Value::Number(n) if n == 2.0));
59
60        let second = interp.pop().unwrap();
61        assert!(matches!(second, Value::Number(n) if n == 4.0));
62
63        let third = interp.pop().unwrap();
64        assert!(matches!(third, Value::Number(n) if n == 3.0));
65
66        let fourth = interp.pop().unwrap();
67        assert!(matches!(fourth, Value::Number(n) if n == 1.0));
68    }
69
70    #[test]
71    fn test_roll_builtin_no_op() {
72        let mut interp = setup_interpreter();
73
74        // Test: 1 2 3 4  0 roll  ->  1 2 3 4 (no change)
75        interp.push(Value::Number(1.0));
76        interp.push(Value::Number(2.0));
77        interp.push(Value::Number(3.0));
78        interp.push(Value::Number(4.0));
79        interp.push(Value::Number(0.0));
80        roll_builtin(&mut interp).unwrap();
81
82        // Stack should be unchanged: 1 2 3 4
83        let top = interp.pop().unwrap();
84        assert!(matches!(top, Value::Number(n) if n == 4.0));
85
86        let second = interp.pop().unwrap();
87        assert!(matches!(second, Value::Number(n) if n == 3.0));
88
89        let third = interp.pop().unwrap();
90        assert!(matches!(third, Value::Number(n) if n == 2.0));
91
92        let fourth = interp.pop().unwrap();
93        assert!(matches!(fourth, Value::Number(n) if n == 1.0));
94    }
95
96    #[test]
97    fn test_roll_builtin_swap() {
98        let mut interp = setup_interpreter();
99
100        // Test: 1 2  1 roll  ->  2 1 (swap top two items)
101        interp.push(Value::Number(1.0));
102        interp.push(Value::Number(2.0));
103        interp.push(Value::Number(1.0));
104        roll_builtin(&mut interp).unwrap();
105
106        let top = interp.pop().unwrap();
107        assert!(matches!(top, Value::Number(n) if n == 1.0));
108
109        let second = interp.pop().unwrap();
110        assert!(matches!(second, Value::Number(n) if n == 2.0));
111    }
112
113    #[test]
114    fn test_roll_builtin_mixed_types() {
115        let mut interp = setup_interpreter();
116
117        // Test with mixed value types
118        interp.push(Value::String("hello".into()));
119        interp.push(Value::Boolean(true));
120        interp.push(Value::Number(42.0));
121        interp.push(Value::Number(2.0)); // roll depth
122        roll_builtin(&mut interp).unwrap();
123
124        // Should move "hello" to top
125        let top = interp.pop().unwrap();
126        assert!(matches!(top, Value::String(s) if s.as_ref() == "hello"));
127
128        let second = interp.pop().unwrap();
129        assert!(matches!(second, Value::Number(n) if n == 42.0));
130
131        let third = interp.pop().unwrap();
132        assert!(matches!(third, Value::Boolean(true)));
133    }
134
135    #[test]
136    fn test_roll_builtin_stack_underflow() {
137        let mut interp = setup_interpreter();
138
139        // Test with empty stack
140        let result = roll_builtin(&mut interp);
141        assert!(matches!(result, Err(RuntimeError::StackUnderflow)));
142
143        // Test when n > available items
144        interp.push(Value::Number(1.0));
145        interp.push(Value::Number(2.0)); // Want to roll 2 items but only have 1
146        let result = roll_builtin(&mut interp);
147        assert!(matches!(result, Err(RuntimeError::StackUnderflow)));
148    }
149
150    #[test]
151    fn test_roll_builtin_type_error() {
152        let mut interp = setup_interpreter();
153
154        // Test with non-number roll count
155        interp.push(Value::Number(1.0));
156        interp.push(Value::Number(2.0));
157        interp.push(Value::String("not a number".into()));
158        let result = roll_builtin(&mut interp);
159        assert!(matches!(result, Err(RuntimeError::TypeError(_))));
160    }
161}