oxur_cli/repl/
sexp_validator.rs1use reedline::{ValidationResult, Validator};
8
9#[derive(Clone)]
18pub struct SExpValidator;
19
20impl SExpValidator {
21 pub fn new() -> Self {
23 Self
24 }
25
26 fn is_balanced(line: &str) -> bool {
30 let mut paren_count = 0;
31 let mut bracket_count = 0;
32 let mut brace_count = 0;
33 let mut in_string = false;
34 let mut escape_next = false;
35
36 for ch in line.chars() {
37 if escape_next {
38 escape_next = false;
39 continue;
40 }
41
42 match ch {
43 '\\' if in_string => escape_next = true,
44 '"' => in_string = !in_string,
45 '(' if !in_string => paren_count += 1,
46 ')' if !in_string => paren_count -= 1,
47 '[' if !in_string => bracket_count += 1,
48 ']' if !in_string => bracket_count -= 1,
49 '{' if !in_string => brace_count += 1,
50 '}' if !in_string => brace_count -= 1,
51 _ => {}
52 }
53
54 if paren_count < 0 || bracket_count < 0 || brace_count < 0 {
56 return false;
57 }
58 }
59
60 paren_count == 0 && bracket_count == 0 && brace_count == 0 && !in_string
62 }
63}
64
65impl Default for SExpValidator {
66 fn default() -> Self {
67 Self::new()
68 }
69}
70
71impl Validator for SExpValidator {
72 fn validate(&self, line: &str) -> ValidationResult {
73 if Self::is_balanced(line) {
74 ValidationResult::Complete
75 } else {
76 ValidationResult::Incomplete
77 }
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[test]
86 fn test_balanced_simple_expression() {
87 assert!(SExpValidator::is_balanced("(+ 1 2)"));
88 }
89
90 #[test]
91 fn test_balanced_nested_parens() {
92 assert!(SExpValidator::is_balanced("((+ 1 2) (* 3 4))"));
93 }
94
95 #[test]
96 fn test_balanced_with_brackets() {
97 assert!(SExpValidator::is_balanced("[1 2 3]"));
98 }
99
100 #[test]
101 fn test_balanced_with_braces() {
102 assert!(SExpValidator::is_balanced("{:a 1 :b 2}"));
103 }
104
105 #[test]
106 fn test_balanced_mixed_brackets() {
107 assert!(SExpValidator::is_balanced("(let [x 1] {:result x})"));
108 }
109
110 #[test]
111 fn test_balanced_with_string() {
112 assert!(SExpValidator::is_balanced(r#"(print "hello")"#));
113 }
114
115 #[test]
116 fn test_balanced_with_escaped_quote() {
117 assert!(SExpValidator::is_balanced(r#"(print "say \"hi\"")"#));
118 }
119
120 #[test]
121 fn test_unbalanced_open_paren() {
122 assert!(!SExpValidator::is_balanced("(+ 1 2"));
123 }
124
125 #[test]
126 fn test_unbalanced_close_paren() {
127 assert!(!SExpValidator::is_balanced("+ 1 2)"));
128 }
129
130 #[test]
131 fn test_unbalanced_nested() {
132 assert!(!SExpValidator::is_balanced("((+ 1 2)"));
133 }
134
135 #[test]
136 fn test_unbalanced_bracket() {
137 assert!(!SExpValidator::is_balanced("[1 2 3"));
138 }
139
140 #[test]
141 fn test_unbalanced_brace() {
142 assert!(!SExpValidator::is_balanced("{:a 1"));
143 }
144
145 #[test]
146 fn test_unclosed_string() {
147 assert!(!SExpValidator::is_balanced(r#"(print "hello"#));
148 }
149
150 #[test]
151 fn test_string_with_paren_inside() {
152 assert!(SExpValidator::is_balanced(r#"(print "foo(bar)")"#));
153 }
154
155 #[test]
156 fn test_empty_string() {
157 assert!(SExpValidator::is_balanced(""));
158 }
159
160 #[test]
161 fn test_validator_complete() {
162 let validator = SExpValidator::new();
163 assert!(matches!(validator.validate("(+ 1 2)"), ValidationResult::Complete));
164 }
165
166 #[test]
167 fn test_validator_incomplete() {
168 let validator = SExpValidator::new();
169 assert!(matches!(validator.validate("(+ 1 2"), ValidationResult::Incomplete));
170 }
171
172 #[test]
173 fn test_multiline_complete() {
174 let input = "(deffn square [x]\n (* x x))";
175 assert!(SExpValidator::is_balanced(input));
176 }
177
178 #[test]
179 fn test_multiline_incomplete() {
180 let input = "(deffn square [x]\n (* x x)";
181 assert!(!SExpValidator::is_balanced(input));
182 }
183}