Skip to main content

lykn_cli/
reader.rs

1/// lykn s-expression reader
2///
3/// Parses lykn source text into a tree of SExpr nodes.
4
5#[derive(Debug, Clone)]
6pub enum SExpr {
7    Atom(String),
8    Str(String),
9    Num(f64),
10    List(Vec<SExpr>),
11}
12
13pub fn read(source: &str) -> Vec<SExpr> {
14    let chars: Vec<char> = source.chars().collect();
15    let mut pos = 0;
16    let mut exprs = Vec::new();
17
18    skip_ws(&chars, &mut pos);
19    while pos < chars.len() {
20        if let Some(expr) = read_expr(&chars, &mut pos) {
21            exprs.push(expr);
22        }
23        skip_ws(&chars, &mut pos);
24    }
25    exprs
26}
27
28fn skip_ws(chars: &[char], pos: &mut usize) {
29    while *pos < chars.len() {
30        match chars[*pos] {
31            ' ' | '\t' | '\n' | '\r' => *pos += 1,
32            ';' => {
33                while *pos < chars.len() && chars[*pos] != '\n' {
34                    *pos += 1;
35                }
36            }
37            _ => break,
38        }
39    }
40}
41
42fn read_expr(chars: &[char], pos: &mut usize) -> Option<SExpr> {
43    skip_ws(chars, pos);
44    if *pos >= chars.len() {
45        return None;
46    }
47
48    match chars[*pos] {
49        '(' => Some(read_list(chars, pos)),
50        '"' => Some(read_string(chars, pos)),
51        _ => Some(read_atom_or_num(chars, pos)),
52    }
53}
54
55fn read_list(chars: &[char], pos: &mut usize) -> SExpr {
56    *pos += 1; // skip (
57    let mut values = Vec::new();
58    skip_ws(chars, pos);
59    while *pos < chars.len() && chars[*pos] != ')' {
60        if let Some(expr) = read_expr(chars, pos) {
61            values.push(expr);
62        }
63        skip_ws(chars, pos);
64    }
65    if *pos < chars.len() {
66        *pos += 1; // skip )
67    }
68    SExpr::List(values)
69}
70
71fn read_string(chars: &[char], pos: &mut usize) -> SExpr {
72    *pos += 1; // skip opening "
73    let mut value = String::new();
74    while *pos < chars.len() && chars[*pos] != '"' {
75        if chars[*pos] == '\\' && *pos + 1 < chars.len() {
76            *pos += 1;
77            match chars[*pos] {
78                'n' => value.push('\n'),
79                't' => value.push('\t'),
80                '\\' => value.push('\\'),
81                '"' => value.push('"'),
82                c => value.push(c),
83            }
84        } else {
85            value.push(chars[*pos]);
86        }
87        *pos += 1;
88    }
89    if *pos < chars.len() {
90        *pos += 1; // skip closing "
91    }
92    SExpr::Str(value)
93}
94
95fn read_atom_or_num(chars: &[char], pos: &mut usize) -> SExpr {
96    let mut value = String::new();
97    while *pos < chars.len() {
98        match chars[*pos] {
99            ' ' | '\t' | '\n' | '\r' | '(' | ')' | ';' => break,
100            c => {
101                value.push(c);
102                *pos += 1;
103            }
104        }
105    }
106
107    // Try parsing as number
108    if let Ok(n) = value.parse::<f64>() {
109        SExpr::Num(n)
110    } else {
111        SExpr::Atom(value)
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn empty_input() {
121        assert!(read("").is_empty());
122    }
123
124    #[test]
125    fn whitespace_only() {
126        assert!(read("   \t\n  ").is_empty());
127    }
128
129    #[test]
130    fn single_atom() {
131        let exprs = read("hello");
132        assert_eq!(exprs.len(), 1);
133        assert!(matches!(&exprs[0], SExpr::Atom(s) if s == "hello"));
134    }
135
136    #[test]
137    fn integer_number() {
138        let exprs = read("42");
139        assert_eq!(exprs.len(), 1);
140        assert!(matches!(&exprs[0], SExpr::Num(n) if *n == 42.0));
141    }
142
143    #[test]
144    fn float_number() {
145        let exprs = read("3.14");
146        assert_eq!(exprs.len(), 1);
147        assert!(matches!(&exprs[0], SExpr::Num(n) if (*n - 3.14).abs() < f64::EPSILON));
148    }
149
150    #[test]
151    fn negative_number() {
152        let exprs = read("-7");
153        assert_eq!(exprs.len(), 1);
154        assert!(matches!(&exprs[0], SExpr::Num(n) if *n == -7.0));
155    }
156
157    #[test]
158    fn simple_string() {
159        let exprs = read("\"hello world\"");
160        assert_eq!(exprs.len(), 1);
161        assert!(matches!(&exprs[0], SExpr::Str(s) if s == "hello world"));
162    }
163
164    #[test]
165    fn string_escape_newline() {
166        let exprs = read("\"a\\nb\"");
167        assert_eq!(exprs.len(), 1);
168        assert!(matches!(&exprs[0], SExpr::Str(s) if s == "a\nb"));
169    }
170
171    #[test]
172    fn string_escape_tab() {
173        let exprs = read("\"a\\tb\"");
174        assert_eq!(exprs.len(), 1);
175        assert!(matches!(&exprs[0], SExpr::Str(s) if s == "a\tb"));
176    }
177
178    #[test]
179    fn string_escape_backslash() {
180        let exprs = read("\"a\\\\b\"");
181        assert_eq!(exprs.len(), 1);
182        assert!(matches!(&exprs[0], SExpr::Str(s) if s == "a\\b"));
183    }
184
185    #[test]
186    fn string_escape_quote() {
187        let exprs = read("\"a\\\"b\"");
188        assert_eq!(exprs.len(), 1);
189        assert!(matches!(&exprs[0], SExpr::Str(s) if s == "a\"b"));
190    }
191
192    #[test]
193    fn string_unknown_escape() {
194        let exprs = read("\"a\\xb\"");
195        assert_eq!(exprs.len(), 1);
196        assert!(matches!(&exprs[0], SExpr::Str(s) if s == "axb"));
197    }
198
199    #[test]
200    fn simple_list() {
201        let exprs = read("(+ 1 2)");
202        assert_eq!(exprs.len(), 1);
203        match &exprs[0] {
204            SExpr::List(vals) => {
205                assert_eq!(vals.len(), 3);
206                assert!(matches!(&vals[0], SExpr::Atom(s) if s == "+"));
207                assert!(matches!(&vals[1], SExpr::Num(n) if *n == 1.0));
208                assert!(matches!(&vals[2], SExpr::Num(n) if *n == 2.0));
209            }
210            _ => panic!("expected list"),
211        }
212    }
213
214    #[test]
215    fn empty_list() {
216        let exprs = read("()");
217        assert_eq!(exprs.len(), 1);
218        match &exprs[0] {
219            SExpr::List(vals) => assert!(vals.is_empty()),
220            _ => panic!("expected list"),
221        }
222    }
223
224    #[test]
225    fn nested_list() {
226        let exprs = read("(define x (+ 1 2))");
227        assert_eq!(exprs.len(), 1);
228        match &exprs[0] {
229            SExpr::List(vals) => {
230                assert_eq!(vals.len(), 3);
231                assert!(matches!(&vals[2], SExpr::List(_)));
232            }
233            _ => panic!("expected list"),
234        }
235    }
236
237    #[test]
238    fn multiple_top_level() {
239        let exprs = read("a b c");
240        assert_eq!(exprs.len(), 3);
241    }
242
243    #[test]
244    fn line_comment() {
245        let exprs = read("; comment\nhello");
246        assert_eq!(exprs.len(), 1);
247        assert!(matches!(&exprs[0], SExpr::Atom(s) if s == "hello"));
248    }
249
250    #[test]
251    fn inline_comment() {
252        let exprs = read("a ; comment\nb");
253        assert_eq!(exprs.len(), 2);
254    }
255
256    #[test]
257    fn tab_whitespace() {
258        let exprs = read("(a\tb)");
259        assert_eq!(exprs.len(), 1);
260        match &exprs[0] {
261            SExpr::List(vals) => assert_eq!(vals.len(), 2),
262            _ => panic!("expected list"),
263        }
264    }
265
266    #[test]
267    fn unterminated_string() {
268        // Reader just stops at end of input
269        let exprs = read("\"unterminated");
270        assert_eq!(exprs.len(), 1);
271        assert!(matches!(&exprs[0], SExpr::Str(s) if s == "unterminated"));
272    }
273
274    #[test]
275    fn unterminated_list() {
276        // Reader just stops at end of input
277        let exprs = read("(a b");
278        assert_eq!(exprs.len(), 1);
279        match &exprs[0] {
280            SExpr::List(vals) => assert_eq!(vals.len(), 2),
281            _ => panic!("expected list"),
282        }
283    }
284}