Skip to main content

tatara_lisp_eval/
repl.rs

1//! Streaming / REPL-style evaluation.
2//!
3//! Phase 2.2 scaffold: public type + signatures. The reader integration
4//! + multi-line continuation detection land in Phase 2.5.
5
6use crate::error::Result;
7use crate::eval::Interpreter;
8use crate::value::Value;
9
10/// A persistent REPL session against an `Interpreter<H>`.
11///
12/// Multiple calls to `eval_str` share the same global env — bindings
13/// introduced by one call are visible to subsequent calls. Errors in one
14/// call do not poison the session.
15pub struct ReplSession<'i, H> {
16    interp: &'i mut Interpreter<H>,
17}
18
19impl<'i, H: 'static> ReplSession<'i, H> {
20    pub fn new(interp: &'i mut Interpreter<H>) -> Self {
21        Self { interp }
22    }
23
24    /// Evaluate one or more forms from `input` in the session's env,
25    /// returning the value of the last form. Phase 2.5 fills this in.
26    pub fn eval_str(&mut self, input: &str, host: &mut H) -> Result<Value> {
27        let forms = tatara_lisp::read_spanned(input)?;
28        let mut last = Value::Nil;
29        for form in &forms {
30            last = self.interp.eval_spanned(form, host)?;
31        }
32        Ok(last)
33    }
34
35    /// Paren-balance check — returns `true` when `input` is a complete set
36    /// of top-level forms (ready to submit) or `false` when the user's
37    /// client should keep collecting more input.
38    ///
39    /// Phase 2.2: minimal heuristic (open-paren count vs close-paren count,
40    /// ignoring string literals). Phase 2.5 refines.
41    pub fn is_complete(input: &str) -> bool {
42        let mut depth: i32 = 0;
43        let mut in_string = false;
44        let mut chars = input.chars().peekable();
45        while let Some(c) = chars.next() {
46            if in_string {
47                if c == '\\' {
48                    chars.next();
49                    continue;
50                }
51                if c == '"' {
52                    in_string = false;
53                }
54                continue;
55            }
56            match c {
57                '"' => in_string = true,
58                '(' => depth += 1,
59                ')' => depth -= 1,
60                ';' => {
61                    for nc in chars.by_ref() {
62                        if nc == '\n' {
63                            break;
64                        }
65                    }
66                }
67                _ => {}
68            }
69            if depth < 0 {
70                return true; // unbalanced close — let the reader surface it
71            }
72        }
73        depth == 0 && !in_string
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    struct NoHost;
82
83    #[test]
84    fn session_evaluates_last_form() {
85        let mut i: Interpreter<NoHost> = Interpreter::new();
86        let mut s = ReplSession::new(&mut i);
87        let mut host = NoHost;
88        let v = s.eval_str("42", &mut host).unwrap();
89        assert!(matches!(v, Value::Int(42)));
90    }
91
92    #[test]
93    fn session_returns_last_of_multiple() {
94        let mut i: Interpreter<NoHost> = Interpreter::new();
95        let mut s = ReplSession::new(&mut i);
96        let mut host = NoHost;
97        let v = s.eval_str("1 2 3", &mut host).unwrap();
98        assert!(matches!(v, Value::Int(3)));
99    }
100
101    #[test]
102    fn is_complete_balanced() {
103        assert!(ReplSession::<()>::is_complete("42"));
104        assert!(ReplSession::<()>::is_complete("(foo bar)"));
105        assert!(ReplSession::<()>::is_complete("(a (b c) d)"));
106        assert!(ReplSession::<()>::is_complete("()"));
107    }
108
109    #[test]
110    fn is_complete_unbalanced_open() {
111        assert!(!ReplSession::<()>::is_complete("(foo"));
112        assert!(!ReplSession::<()>::is_complete("(a (b"));
113    }
114
115    #[test]
116    fn is_complete_ignores_parens_in_strings() {
117        assert!(ReplSession::<()>::is_complete("\"(\""));
118        assert!(ReplSession::<()>::is_complete("(foo \"a (b c)\")"));
119    }
120
121    #[test]
122    fn is_complete_ignores_comments() {
123        assert!(ReplSession::<()>::is_complete("42 ; a ( comment"));
124    }
125}