uni_core/
interpreter.rs

1use crate::compat::{Rc, String, Vec, Box, ToString};
2use crate::tokenizer::SourcePos;
3use crate::value::{RuntimeError, Value};
4use crate::output::Output;
5use crate::time_source::TimeSource;
6use num_traits::Zero;
7
8#[cfg(target_os = "none")]
9use num_traits::Float;
10
11#[cfg(not(target_os = "none"))]
12use std::collections::HashMap;
13#[cfg(target_os = "none")]
14use alloc::collections::BTreeMap as HashMap;
15
16// RUST CONCEPT: Dictionary entry with metadata
17// Each entry contains the value and a flag indicating execution behavior
18#[derive(Debug, Clone)]
19pub struct DictEntry {
20    pub value: Value,
21    pub is_executable: bool, // true = execute lists (def), false = push as data (val)
22    pub doc: Option<Rc<str>>, // Optional documentation string for help
23}
24
25pub struct Interpreter {
26    pub stack: Vec<Value>,
27    pub return_stack: Vec<Value>, // RUST CONCEPT: Return stack for Forth-like operations
28    pub dictionary: HashMap<Rc<str>, DictEntry>,
29    pub atoms: HashMap<String, Rc<str>>,
30    pub local_frames: Vec<HashMap<Rc<str>, Value>>, // RUST CONCEPT: Stack of local variable frames for lexical scoping
31    pub current_pos: Option<SourcePos>, // Track current execution position for error messages
32    pending_doc_target: Option<Rc<str>>, // Remember most recent definition for doc
33    output: Option<Box<dyn Output>>, // Optional output for print/display (REPL mode)
34    pub time_source: Option<Box<dyn TimeSource>>, // Optional time source for datetime operations (public for primitive access)
35
36    // Hardware peripherals (micro:bit only)
37    #[cfg(feature = "hardware-microbit")]
38    pub buttons: Option<microbit::board::Buttons>,
39    #[cfg(feature = "hardware-microbit")]
40    pub display_buffer: [[u8; 5]; 5],  // Raw pixel buffer for LED matrix
41}
42
43impl Interpreter {
44    pub fn new() -> Self {
45        let mut interpreter = Self {
46            stack: Vec::new(),
47            return_stack: Vec::new(), // RUST CONCEPT: Initialize empty return stack
48            dictionary: HashMap::new(),
49            atoms: HashMap::new(),
50            local_frames: Vec::new(), // RUST CONCEPT: Initialize empty local frames stack
51            current_pos: None, // No position initially
52            pending_doc_target: None,
53            output: None, // No output by default (for file execution, tests)
54            time_source: None, // No time source by default (platform must inject)
55
56            // Hardware peripherals start as None, set by platform initialization
57            #[cfg(feature = "hardware-microbit")]
58            buttons: None,
59            #[cfg(feature = "hardware-microbit")]
60            display_buffer: [[0u8; 5]; 5],  // All LEDs off initially
61        };
62
63        // RUST CONCEPT: Automatic initialization
64        // Load builtins first (primitives and core operations)
65        crate::builtins::register_builtins(&mut interpreter);
66
67        // Then load prelude (higher-level operations built on primitives)
68        if let Err(_e) = crate::prelude::load_prelude(&mut interpreter) {
69            // In a constructor, we can't easily return errors
70            // For now, just continue without prelude
71            // TODO: Better error handling for prelude loading
72        }
73
74        interpreter
75    }
76
77    pub fn intern_atom(&mut self, text: &str) -> Rc<str> {
78        if let Some(existing) = self.atoms.get(text) {
79            existing.clone()
80        } else {
81            let atom: Rc<str> = text.into();
82            self.atoms.insert(text.to_string(), atom.clone());
83            atom
84        }
85    }
86
87    pub fn set_pending_doc_target(&mut self, atom: Rc<str>) {
88        self.pending_doc_target = Some(atom);
89    }
90
91    pub fn take_pending_doc_target(&mut self) -> Option<Rc<str>> {
92        self.pending_doc_target.take()
93    }
94
95    pub fn attach_doc(&mut self, atom: &Rc<str>, doc: Rc<str>) -> Result<(), RuntimeError> {
96        if let Some(entry) = self.dictionary.get_mut(atom) {
97            entry.doc = Some(doc);
98            Ok(())
99        } else {
100            Err(RuntimeError::UndefinedWord(atom.to_string()))
101        }
102    }
103
104    pub fn push(&mut self, value: Value) {
105        self.stack.push(value);
106    }
107
108    pub fn pop(&mut self) -> Result<Value, RuntimeError> {
109        self.stack.pop().ok_or(RuntimeError::StackUnderflow)
110    }
111
112    pub fn pop_number(&mut self) -> Result<f64, RuntimeError> {
113        let value = self.pop()?;
114        match value {
115            Value::Number(n) => Ok(n),
116            Value::Int32(i) => Ok(i as f64),
117            _ => Err(RuntimeError::TypeError("Expected number".to_string())),
118        }
119    }
120
121    pub fn pop_integer(&mut self) -> Result<usize, RuntimeError> {
122        use num_traits::ToPrimitive;
123        let value = self.pop()?;
124        match value {
125            Value::Int32(i) => {
126                if i >= 0 {
127                    Ok(i as usize)
128                } else {
129                    Err(RuntimeError::TypeError("Expected non-negative integer".to_string()))
130                }
131            }
132            Value::Integer(i) => i.to_usize().ok_or_else(|| {
133                RuntimeError::TypeError("Integer value too large for index".to_string())
134            }),
135            Value::Number(n) => {
136                if n.fract() == 0.0 && n >= 0.0 && n.is_finite() {
137                    Ok(n as usize)
138                } else {
139                    Err(RuntimeError::TypeError("Expected non-negative integer".to_string()))
140                }
141            }
142            _ => Err(RuntimeError::TypeError("Expected integer".to_string())),
143        }
144    }
145
146    pub fn make_list(&self, items: Vec<Value>) -> Value {
147        items.into_iter().rev().fold(Value::Nil, |acc, item| {
148            Value::Pair(Rc::new(item), Rc::new(acc))
149        })
150    }
151
152    pub fn make_array(&self, items: Vec<Value>) -> Value {
153        #[cfg(not(target_os = "none"))]
154        {
155            Value::Array(Rc::new(std::cell::RefCell::new(items)))
156        }
157        #[cfg(target_os = "none")]
158        {
159            use core::cell::RefCell;
160            Value::Array(Rc::new(RefCell::new(items)))
161        }
162    }
163
164    pub fn is_null(&self, value: &Value) -> bool {
165        matches!(value, Value::Null)
166    }
167
168    // RUST CONCEPT: Defensive truthiness check
169    // Following JS rules: check for falsy cases explicitly, everything else is truthy
170    // This is more maintainable - new types automatically become truthy by default
171    pub fn is_truthy(&self, value: &Value) -> bool {
172        match value {
173            // Falsy cases only:
174            Value::Boolean(false) => false,    // false is falsy
175            Value::Null => false,              // null is falsy (like JS)
176            Value::String(s) if s.is_empty() => false, // "" is falsy (like JS)
177
178            // Zero in all numeric representations is falsy
179            Value::Int32(0) => false,
180            Value::Number(n) if *n == 0.0 || n.is_nan() => false, // 0 and NaN are falsy (like JS)
181            Value::Integer(i) if i.is_zero() => false,
182            Value::Rational(r) if r.is_zero() => false,
183            #[cfg(feature = "complex_numbers")]
184            Value::GaussianInt(re, im) if re.is_zero() && im.is_zero() => false, // 0+0i
185            #[cfg(feature = "complex_numbers")]
186            Value::Complex(c) if (c.re == 0.0 && c.im == 0.0) || c.re.is_nan() || c.im.is_nan() => false, // 0+0i or NaN parts
187
188            // Everything else is truthy (including Nil, atoms, pairs, arrays, buffers, etc.)
189            _ => true,
190        }
191    }
192
193    // RUST CONCEPT: Return stack operations for Forth-like control structures
194    // These operations enable temporary storage of values outside the main computation stack
195
196    pub fn push_return(&mut self, value: Value) {
197        self.return_stack.push(value);
198    }
199
200    pub fn pop_return(&mut self) -> Result<Value, RuntimeError> {
201        self.return_stack.pop().ok_or(RuntimeError::StackUnderflow)
202    }
203
204    pub fn peek_return(&self) -> Result<&Value, RuntimeError> {
205        self.return_stack.last().ok_or(RuntimeError::StackUnderflow)
206    }
207
208    // TODO: Position management for error context - uncomment when connecting to execution pipeline
209    // pub fn set_position(&mut self, pos: SourcePos) {
210    //     self.current_pos = Some(pos);
211    // }
212
213    // TODO: Method for clearing position context - uncomment when needed for multi-statement execution
214    // pub fn clear_position(&mut self) {
215    //     self.current_pos = None;
216    // }
217
218    // Position-aware pop method for better error messages
219    pub fn pop_with_context(&mut self, context: &str) -> Result<Value, RuntimeError> {
220        if let Some(pos) = &self.current_pos {
221            self.stack
222                .pop()
223                .ok_or_else(|| RuntimeError::StackUnderflowAt {
224                    pos: pos.clone(),
225                    context: context.to_string(),
226                })
227        } else {
228            self.stack.pop().ok_or(RuntimeError::StackUnderflow)
229        }
230    }
231
232    // Output management (used by print/help builtins)
233    pub fn set_output(&mut self, output: Box<dyn Output>) {
234        self.output = Some(output);
235    }
236
237    #[allow(dead_code)]
238    pub fn has_output(&self) -> bool {
239        self.output.is_some()
240    }
241
242    pub fn set_time_source(&mut self, time_source: Box<dyn TimeSource>) {
243        self.time_source = Some(time_source);
244    }
245
246    #[allow(dead_code)]
247    pub fn has_time_source(&self) -> bool {
248        self.time_source.is_some()
249    }
250
251    // Write a line to the output if available
252    pub fn writeln(&mut self, text: &str) -> Result<(), ()> {
253        if let Some(output) = &mut self.output {
254            output.write(text.as_bytes())?;
255            // Use platform-appropriate line ending
256            #[cfg(not(target_os = "none"))]
257            output.write(b"\n")?;
258            #[cfg(target_os = "none")]
259            output.write(b"\r\n")?;
260            output.flush()?;
261        }
262        Ok(())
263    }
264
265    // Write text to the output without a newline
266    pub fn write_str(&mut self, text: &str) -> Result<(), ()> {
267        if let Some(output) = &mut self.output {
268            output.write(text.as_bytes())?;
269            output.flush()?;
270        }
271        Ok(())
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278
279    // Test-only helper methods
280    impl Interpreter {
281        fn pop_string(&mut self) -> Result<Rc<str>, RuntimeError> {
282            let value = self.pop()?;
283            match value {
284                Value::String(s) => Ok(s),
285                _ => Err(RuntimeError::TypeError("Expected string".to_string())),
286            }
287        }
288
289        fn pop_boolean(&mut self) -> Result<bool, RuntimeError> {
290            let value = self.pop()?;
291            match value {
292                Value::Boolean(b) => Ok(b),
293                _ => Err(RuntimeError::TypeError("Expected boolean".to_string())),
294            }
295        }
296    }
297
298    #[test]
299    fn test_atom_interning() {
300        let mut interp = Interpreter::new();
301
302        let atom1 = interp.intern_atom("hello");
303        let atom2 = interp.intern_atom("hello");
304
305        assert!(Rc::ptr_eq(&atom1, &atom2));
306    }
307
308    #[test]
309    fn test_stack_operations() {
310        let mut interp = Interpreter::new();
311
312        interp.push(Value::Number(42.0));
313        let popped = interp.pop().unwrap();
314
315        match popped {
316            Value::Number(n) => assert_eq!(n, 42.0),
317            _ => panic!("Expected number"),
318        }
319
320        assert!(interp.pop().is_err());
321    }
322
323    #[test]
324    fn test_list_construction() {
325        let interp = Interpreter::new();
326
327        let empty = interp.make_list(vec![]);
328        match empty {
329            Value::Nil => (),
330            _ => panic!("Expected Nil for empty list"),
331        }
332
333        let single = interp.make_list(vec![Value::Number(42.0)]);
334        match single {
335            Value::Pair(car, cdr) => match (car.as_ref(), cdr.as_ref()) {
336                (Value::Number(n), Value::Nil) => assert_eq!(*n, 42.0),
337                _ => panic!("Expected Pair(42, Nil)"),
338            },
339            _ => panic!("Expected Pair for single element list"),
340        }
341
342        // Test multi-element list
343        let multi = interp.make_list(vec![
344            Value::Number(1.0),
345            Value::Number(2.0),
346            Value::Number(3.0),
347        ]);
348        match multi {
349            Value::Pair(car1, cdr1) => match (car1.as_ref(), cdr1.as_ref()) {
350                (Value::Number(n1), Value::Pair(car2, cdr2)) => {
351                    assert_eq!(*n1, 1.0);
352                    match (car2.as_ref(), cdr2.as_ref()) {
353                        (Value::Number(n2), Value::Pair(car3, cdr3)) => {
354                            assert_eq!(*n2, 2.0);
355                            match (car3.as_ref(), cdr3.as_ref()) {
356                                (Value::Number(n3), Value::Nil) => assert_eq!(*n3, 3.0),
357                                _ => panic!("Expected third element to be 3.0 followed by Nil"),
358                            }
359                        }
360                        _ => panic!("Expected second element to be 2.0"),
361                    }
362                }
363                _ => panic!("Expected first element to be 1.0"),
364            },
365            _ => panic!("Expected Pair for multi-element list"),
366        }
367    }
368
369    #[test]
370    fn test_pop_number_success() {
371        let mut interp = Interpreter::new();
372        interp.push(Value::Number(42.0));
373        assert_eq!(interp.pop_number().unwrap(), 42.0);
374    }
375
376    #[test]
377    fn test_pop_number_type_error() {
378        let mut interp = Interpreter::new();
379        interp.push(Value::Nil);
380        assert!(matches!(
381            interp.pop_number(),
382            Err(RuntimeError::TypeError(msg)) if msg == "Expected number"
383        ));
384    }
385
386    #[test]
387    fn test_pop_number_underflow() {
388        let mut interp = Interpreter::new();
389        assert!(matches!(
390            interp.pop_number(),
391            Err(RuntimeError::StackUnderflow)
392        ));
393    }
394
395    #[test]
396    fn test_dictionary_operations() {
397        let mut interp = Interpreter::new();
398
399        // Test inserting and retrieving from dictionary
400        let key = interp.intern_atom("test");
401        let entry = DictEntry {
402            value: Value::Number(99.0),
403            is_executable: false, // Constants are not executable
404            doc: None,
405        };
406        interp.dictionary.insert(key.clone(), entry);
407
408        match interp.dictionary.get(&key) {
409            Some(dict_entry) => {
410                match &dict_entry.value {
411                    Value::Number(n) => assert_eq!(*n, 99.0),
412                    _ => panic!("Expected to find Number(99.0) in dictionary entry"),
413                }
414                assert!(!dict_entry.is_executable);
415            }
416            _ => panic!("Expected to find dictionary entry"),
417        }
418
419        // Test that non-existent keys return None
420        let missing = interp.intern_atom("missing");
421        assert!(interp.dictionary.get(&missing).is_none());
422    }
423
424    #[test]
425    fn test_atom_interning_different_atoms() {
426        let mut interp = Interpreter::new();
427
428        let atom1 = interp.intern_atom("hello");
429        let atom2 = interp.intern_atom("world");
430        let atom3 = interp.intern_atom("hello");
431
432        // Same text should return same Rc
433        assert!(Rc::ptr_eq(&atom1, &atom3));
434
435        // Different text should return different Rc
436        assert!(!Rc::ptr_eq(&atom1, &atom2));
437
438        // Verify the actual content
439        assert_eq!(&*atom1, "hello");
440        assert_eq!(&*atom2, "world");
441    }
442
443    #[test]
444    fn test_pop_string() {
445        let mut interp = Interpreter::new();
446
447        // Test successful pop_string
448        let string_val: Rc<str> = "hello world".into();
449        interp.push(Value::String(string_val));
450        let s = interp.pop_string().unwrap();
451        assert_eq!(&*s, "hello world");
452
453        // Test type error when popping non-string
454        interp.push(Value::Number(42.0));
455        assert!(matches!(
456            interp.pop_string(),
457            Err(RuntimeError::TypeError(msg)) if msg == "Expected string"
458        ));
459
460        // Test stack underflow
461        assert!(matches!(
462            interp.pop_string(),
463            Err(RuntimeError::StackUnderflow)
464        ));
465    }
466
467    #[test]
468    fn test_string_vs_atom_distinction() {
469        let mut interp = Interpreter::new();
470
471        // Strings are not interned - each is separate
472        let string1 = Value::String("hello".into());
473        let string2 = Value::String("hello".into());
474
475        // Atoms are interned - same text gives same Rc
476        let atom1 = Value::Atom(interp.intern_atom("hello"));
477        let atom2 = Value::Atom(interp.intern_atom("hello"));
478
479        // Strings with same content are different Rc objects (not interned)
480        if let (Value::String(s1), Value::String(s2)) = (&string1, &string2) {
481            assert_eq!(s1, s2); // Same content
482            assert!(!Rc::ptr_eq(s1, s2)); // Different Rc objects
483        }
484
485        // Atoms with same content share the same object
486        if let (Value::Atom(a1), Value::Atom(a2)) = (&atom1, &atom2) {
487            assert_eq!(a1, a2); // Same content
488            assert!(Rc::ptr_eq(a1, a2)); // Same object
489        }
490    }
491
492    #[test]
493    fn test_pop_boolean() {
494        let mut interp = Interpreter::new();
495
496        // Test successful pop_boolean
497        interp.push(Value::Boolean(true));
498        assert_eq!(interp.pop_boolean().unwrap(), true);
499
500        interp.push(Value::Boolean(false));
501        assert_eq!(interp.pop_boolean().unwrap(), false);
502
503        // Test type error when popping non-boolean
504        interp.push(Value::Number(42.0));
505        assert!(matches!(
506            interp.pop_boolean(),
507            Err(RuntimeError::TypeError(msg)) if msg == "Expected boolean"
508        ));
509
510        // Test stack underflow
511        assert!(matches!(
512            interp.pop_boolean(),
513            Err(RuntimeError::StackUnderflow)
514        ));
515    }
516
517    #[test]
518    fn test_is_null() {
519        let interp = Interpreter::new();
520
521        assert!(interp.is_null(&Value::Null));
522        assert!(!interp.is_null(&Value::Nil));
523        assert!(!interp.is_null(&Value::Boolean(false)));
524        assert!(!interp.is_null(&Value::Number(0.0)));
525    }
526
527    #[test]
528    fn test_is_truthy() {
529        let interp = Interpreter::new();
530
531        // Boolean values
532        assert!(interp.is_truthy(&Value::Boolean(true)));
533        assert!(!interp.is_truthy(&Value::Boolean(false)));
534
535        // Null is falsy, Nil (empty list) is truthy (like JS)
536        assert!(!interp.is_truthy(&Value::Null));
537        assert!(interp.is_truthy(&Value::Nil));
538
539        // Numbers: 0 is falsy, NaN is falsy, everything else is truthy
540        assert!(!interp.is_truthy(&Value::Number(0.0)));
541        assert!(!interp.is_truthy(&Value::Number(f64::NAN))); // NaN is falsy (like JS)
542        assert!(interp.is_truthy(&Value::Number(42.0)));
543        assert!(interp.is_truthy(&Value::Number(-1.0)));
544        assert!(interp.is_truthy(&Value::Number(f64::INFINITY)));
545        assert!(interp.is_truthy(&Value::Number(f64::NEG_INFINITY)));
546
547        // Strings: empty is falsy, non-empty is truthy
548        assert!(!interp.is_truthy(&Value::String("".into())));
549        assert!(interp.is_truthy(&Value::String("hello".into())));
550
551        // Atoms and QuotedAtoms are always truthy
552        assert!(interp.is_truthy(&Value::Atom("hello".into())));
553        assert!(interp.is_truthy(&Value::QuotedAtom("hello".into())));
554
555        // Pairs are always truthy
556        assert!(interp.is_truthy(&Value::Pair(
557            Rc::new(Value::Number(1.0)),
558            Rc::new(Value::Nil)
559        )));
560    }
561}