beck_syntax/
sexpr.rs

1//! The S-expression reader — the canonical surface.
2//!
3//! [`docs/02-syntax.md`](../../../../../docs/02-syntax.md) §2.8: "The S-expression reader is ~300
4//! lines and should exist from week one: it lets you write compiler tests against canonical ASTs
5//! without depending on the Python surface being finished, and it is how you'll dump intermediate
6//! state for the rest of the project's life."
7//!
8//! It reads the notation the original sketch is written in. `(def toggle (params (: t Todo)) ...)`
9//! is a `Node` with head `def` and three arguments, and nothing is desugared on the way in — the
10//! Python parser's job is to arrive at exactly the same tree.
11
12use beck_diag::depth::Nesting;
13use beck_diag::{Diagnostic, Diagnostics, FileId, Span};
14
15use crate::node::{Head, Lit, Node, Symbol};
16
17struct Reader<'a> {
18    file: FileId,
19    src: &'a [u8],
20    text: &'a str,
21    pos: usize,
22    /// The same ceiling the Python surface counts against. `(((…)))` is the S-expression spelling
23    /// of the input `docs/42` §42.2 aborted on, and this reader recurses through `form` → `list`
24    /// exactly as that one recurses through `primary`.
25    nesting: Nesting,
26}
27
28/// Read every form in a source string.
29pub fn read_all(file: FileId, src: &str, diags: &mut Diagnostics) -> Vec<Node> {
30    let mut r = Reader {
31        file,
32        src: src.as_bytes(),
33        text: src,
34        pos: 0,
35        nesting: Nesting::new(),
36    };
37    let mut out = Vec::new();
38    loop {
39        r.skip_trivia();
40        if r.pos >= r.src.len() {
41            break;
42        }
43        match r.form(diags) {
44            Some(n) => out.push(n),
45            None => break,
46        }
47    }
48    out
49}
50
51/// Read exactly one form; anything after it is an error. Used by tests and by `beck ast`.
52pub fn read_one(file: FileId, src: &str, diags: &mut Diagnostics) -> Option<Node> {
53    let forms = read_all(file, src, diags);
54    if forms.len() > 1 {
55        let span = forms[1].span();
56        diags.push(
57            Diagnostic::error("B0110", "expected a single form", span)
58                .with_primary_label("unexpected second form"),
59        );
60    }
61    forms.into_iter().next()
62}
63
64impl<'a> Reader<'a> {
65    fn span(&self, start: usize) -> Span {
66        Span::new(self.file, start..self.pos)
67    }
68
69    fn peek(&self) -> Option<u8> {
70        self.src.get(self.pos).copied()
71    }
72
73    fn skip_trivia(&mut self) {
74        while let Some(c) = self.peek() {
75            match c {
76                b' ' | b'\t' | b'\n' | b'\r' | b',' => self.pos += 1,
77                b';' => {
78                    while let Some(c) = self.peek() {
79                        self.pos += 1;
80                        if c == b'\n' {
81                            break;
82                        }
83                    }
84                }
85                _ => break,
86            }
87        }
88    }
89
90    fn form(&mut self, diags: &mut Diagnostics) -> Option<Node> {
91        self.skip_trivia();
92        let start = self.pos;
93        let c = self.peek()?;
94        match c {
95            b'(' | b'[' | b'{' => self.list(c, diags),
96            b')' | b']' | b'}' => {
97                self.pos += 1;
98                diags.push(
99                    Diagnostic::error("B0111", "unbalanced closing delimiter", self.span(start))
100                        .with_primary_label("no matching opening delimiter"),
101                );
102                None
103            }
104            b'"' => self.string(diags),
105            b'\'' => {
106                // `'form` is `(quote form)` — the reader's one piece of sugar, because dumping
107                // quoted templates without it is unreadable.
108                self.pos += 1;
109                let inner = self.form(diags)?;
110                Some(Node::form(
111                    crate::node::sym::QUOTE,
112                    vec![inner],
113                    self.span(start),
114                ))
115            }
116            _ => self.atom(diags),
117        }
118    }
119
120    fn list(&mut self, open: u8, diags: &mut Diagnostics) -> Option<Node> {
121        if !self.nesting.enter() {
122            if self.nesting.should_report() {
123                let start = self.pos;
124                let note = self.nesting.note();
125                diags.push(
126                    Diagnostic::error("B0121", "nesting is too deep to read", self.span(start))
127                        .with_primary_label("the reader gave up here")
128                        .with_note(note),
129                );
130            }
131            return None;
132        }
133        let out = self.list_inner(open, diags);
134        self.nesting.leave();
135        out
136    }
137
138    fn list_inner(&mut self, open: u8, diags: &mut Diagnostics) -> Option<Node> {
139        let start = self.pos;
140        let close = match open {
141            b'(' => b')',
142            b'[' => b']',
143            _ => b'}',
144        };
145        self.pos += 1;
146        let mut items: Vec<Node> = Vec::new();
147        loop {
148            self.skip_trivia();
149            match self.peek() {
150                None => {
151                    diags.push(
152                        Diagnostic::error("B0112", "unclosed list", self.span(start))
153                            .with_primary_label("opened here, never closed"),
154                    );
155                    return None;
156                }
157                Some(c) if c == close => {
158                    self.pos += 1;
159                    break;
160                }
161                Some(c) if c == b')' || c == b']' || c == b'}' => {
162                    diags.push(
163                        Diagnostic::error(
164                            "B0113",
165                            "mismatched closing delimiter",
166                            Span::new(self.file, self.pos..self.pos + 1),
167                        )
168                        .with_label(self.span(start), "opened here"),
169                    );
170                    self.pos += 1;
171                    return None;
172                }
173                _ => items.push(self.form(diags)?),
174            }
175        }
176        let span = self.span(start);
177
178        // `[a b c]` is a list literal, `{...}` a record literal; only `(...)` is application.
179        match open {
180            b'[' => return Some(Node::form(crate::node::sym::LIST, items, span)),
181            b'{' => return Some(Node::form(crate::node::sym::RECORD, items, span)),
182            _ => {}
183        }
184
185        if items.is_empty() {
186            diags.push(
187                Diagnostic::error("B0114", "empty application", span)
188                    .with_primary_label("`()` has no meaning")
189                    .with_fix("write `unit` for the unit value"),
190            );
191            return None;
192        }
193
194        // The head of an application is its first element, hoisted out of `args` — that is what
195        // makes `Node.head : Sym | Lit`. A computed callee cannot be hoisted, so it stays as an
196        // argument of the reserved `call` head.
197        let head = items.remove(0);
198        match head.head {
199            Head::Sym(s) if head.args.is_empty() => {
200                normalise_typarams(s.as_str(), &mut items, span);
201                Some(Node::form_sym(s, items, span))
202            }
203            _ => {
204                let mut args = vec![head];
205                args.extend(items);
206                Some(Node::form(crate::node::sym::CALL, args, span))
207            }
208        }
209    }
210
211    fn string(&mut self, diags: &mut Diagnostics) -> Option<Node> {
212        let start = self.pos;
213        self.pos += 1;
214        let mut out = String::new();
215        loop {
216            match self.peek() {
217                None => {
218                    diags.push(
219                        Diagnostic::error("B0115", "unclosed string", self.span(start))
220                            .with_primary_label("opened here"),
221                    );
222                    return None;
223                }
224                Some(b'"') => {
225                    self.pos += 1;
226                    break;
227                }
228                Some(b'\\') => {
229                    self.pos += 1;
230                    let c = self.peek()?;
231                    self.pos += 1;
232                    let decoded = match c {
233                        b'n' => '\n',
234                        b't' => '\t',
235                        b'r' => '\r',
236                        b'0' => '\0',
237                        b'\\' => '\\',
238                        b'"' => '"',
239                        other => {
240                            // Unknown escapes survive verbatim, as in the Python surface.
241                            out.push('\\');
242                            other as char
243                        }
244                    };
245                    out.push(decoded);
246                }
247                Some(_) => {
248                    let ch = self.text[self.pos..].chars().next()?;
249                    self.pos += ch.len_utf8();
250                    out.push(ch);
251                }
252            }
253        }
254        Some(Node::lit(Lit::Str(out.into()), self.span(start)))
255    }
256
257    fn atom(&mut self, diags: &mut Diagnostics) -> Option<Node> {
258        let start = self.pos;
259        while let Some(c) = self.peek() {
260            if c.is_ascii_whitespace()
261                || matches!(
262                    c,
263                    b'(' | b')' | b'[' | b']' | b'{' | b'}' | b'"' | b';' | b','
264                )
265            {
266                break;
267            }
268            self.pos += 1;
269        }
270        if self.pos == start {
271            self.pos += 1;
272            diags.push(Diagnostic::error(
273                "B0116",
274                "unreadable character",
275                self.span(start),
276            ));
277            return None;
278        }
279        let text = &self.text[start..self.pos];
280        let span = self.span(start);
281        Some(atom_node(text, span))
282    }
283}
284
285/// Classify an atom. Shared with the Python surface so that `true` means the same thing in both.
286pub fn atom_node(text: &str, span: Span) -> Node {
287    if let Some(kw) = text.strip_prefix(':') {
288        if !kw.is_empty() {
289            return Node::lit(Lit::Keyword(kw.into()), span);
290        }
291    }
292    match text {
293        "true" | "True" => return Node::lit(Lit::Bool(true), span),
294        "false" | "False" => return Node::lit(Lit::Bool(false), span),
295        _ => {}
296    }
297    if let Ok(n) = text.parse::<i64>() {
298        return Node::lit(Lit::Int(n), span);
299    }
300    if text.contains('.') && !text.starts_with('.') {
301        if let Ok(n) = text.parse::<f64>() {
302            return Node::lit(Lit::Float(n), span);
303        }
304    }
305    Node::symbol(Symbol::new(text), span)
306}
307
308/// Give a hand-written declaration the empty type-parameter list the Python surface always writes.
309///
310/// `(def f (params …) (returns …) (uses) body)` is what the S-expression surface has always looked
311/// like, and §2.3 makes that surface a notation people write by hand for macro debugging. Requiring
312/// `(typarams)` on every one of them would be a tax on the notation for a feature most definitions
313/// do not use, so the reader normalises instead — and the AST keeps one shape, which is what every
314/// pass downstream indexes into (`docs/32` §32.7).
315///
316/// The same holds of the four forms that may now be quantified: a `model`, a `union` and a `type`
317/// carry the list in the same position a `def` does, so `(model Todo (field id Id))` still reads.
318fn normalise_typarams(head: &str, items: &mut Vec<Node>, span: beck_diag::Span) {
319    use crate::node::sym;
320    // The minimum length is what distinguishes a form that has a name and something after it from
321    // a truncated one the parser should report rather than silently reshape.
322    let min = match head {
323        sym::DEF => 4,
324        sym::MODEL | sym::UNION => 1,
325        sym::NEWTYPE | sym::TYPE => 2,
326        // `(impl Show (typarams) Point …)` — the list follows the *trait* name in the node even
327        // though the surface writes it before, because `args[1]` is where every other form keeps it.
328        sym::IMPL => 2,
329        _ => return,
330    };
331    let already = items
332        .get(1)
333        .map(|n| n.is_form(sym::TYPARAMS))
334        .unwrap_or(false);
335    if items.len() >= min && !already {
336        items.insert(1, Node::form(sym::TYPARAMS, Vec::new(), span));
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343    use crate::node::sym;
344
345    fn read(src: &str) -> Node {
346        let mut map = beck_diag::SourceMap::new();
347        let f = map.add("t.sx", src);
348        let mut d = Diagnostics::new();
349        let n = read_one(f, src, &mut d).expect("reads");
350        assert!(!d.has_errors(), "{}", d.render(&map));
351        n
352    }
353
354    #[test]
355    fn the_sketchs_notation_reads_as_written() {
356        let n = read("(def apply-event (fn [todos e] (assoc todos id 1)))");
357        assert_eq!(n.head_name(), Some("def"));
358        assert_eq!(n.args.len(), 2);
359        assert_eq!(n.args[0].as_var().unwrap().as_str(), "apply-event");
360        let f = &n.args[1];
361        assert_eq!(f.head_name(), Some("fn"));
362        assert_eq!(f.args[0].head_name(), Some(sym::LIST));
363        assert_eq!(f.args[1].head_name(), Some("assoc"));
364    }
365
366    #[test]
367    fn keywords_and_record_literals() {
368        let n = read("{:id id :text text}");
369        assert_eq!(n.head_name(), Some(sym::RECORD));
370        assert_eq!(n.args[0].as_keyword(), Some("id"));
371        assert_eq!(n.args[1].as_var().unwrap().as_str(), "id");
372    }
373
374    #[test]
375    fn a_computed_callee_becomes_the_call_head() {
376        let n = read("((. f g) x)");
377        assert_eq!(n.head_name(), Some(sym::CALL));
378        assert_eq!(n.args.len(), 2);
379        assert_eq!(n.args[0].head_name(), Some("."));
380    }
381
382    #[test]
383    fn literals_are_classified_not_stringly_typed() {
384        assert_eq!(read("42").as_lit(), Some(&Lit::Int(42)));
385        assert_eq!(read("4.5").as_lit(), Some(&Lit::Float(4.5)));
386        assert_eq!(read("true").as_lit(), Some(&Lit::Bool(true)));
387        assert_eq!(read(r#""hi""#).as_str_lit(), Some("hi"));
388        assert_eq!(read(":done").as_keyword(), Some("done"));
389    }
390
391    #[test]
392    fn quote_sugar_and_comments() {
393        let n = read("; a comment\n'(a b)");
394        assert_eq!(n.head_name(), Some(sym::QUOTE));
395        assert_eq!(n.args[0].head_name(), Some("a"));
396    }
397
398    #[test]
399    fn unbalanced_input_reports_rather_than_panics() {
400        let mut map = beck_diag::SourceMap::new();
401        let src = "(def a";
402        let f = map.add("t.sx", src);
403        let mut d = Diagnostics::new();
404        assert!(read_one(f, src, &mut d).is_none());
405        assert!(d.has_errors());
406    }
407}