Skip to main content

vla_lang/
reader.rs

1//! The reader: VLA.bas's `Tokenize` and `ParseAll`/`ParseForm`, with the
2//! source lines they remember and the two refusals they raise.
3//!
4//! The shape is the VBA's. `Tokenize` walks the text once, by character:
5//! a line feed counts a line; a space, a tab or a carriage return is
6//! skipped; `(` and `)` are tokens; `;` runs a comment to the end of its
7//! line; `"` opens a string literal whose only escapes are `\"` and `\\`
8//! (any other backslash stands); everything else runs a symbol to the next
9//! delimiter. A string token carries its content behind a leading quote
10//! mark, the VBA's own marker, until the parser makes a [`Form::Str`] of it.
11//! An unterminated string ends at the end of the text, as the VBA's does.
12//!
13//! Lines: every token remembers the raw line it began on, and a list
14//! remembers the line of its `(` less the caller's offset, stored as 0 when
15//! that is below 1 (the prelude's territory; S3.1's explicit zero). A
16//! refusal's location is spelled by `SrcLineTag` and `OpenedAtTag`: ` (vla
17//! line N)` and ` for the list opened at vla line N`, or nothing at all when
18//! the line is the prelude's. The reference also maps lines spliced in by
19//! `(include ...)`; nothing here splices, so every line is the main file's.
20
21use crate::form::{Form, List};
22use crate::messages::{raise, Refusal};
23
24/// One token: its text, with a string literal's content behind a leading
25/// quote mark, and the raw line it began on (1-based).
26#[derive(Clone, Debug, PartialEq)]
27pub struct Token {
28    pub text: String,
29    pub line: u32,
30}
31
32/// VBA `Tokenize`.
33pub fn tokenize(s: &str) -> Vec<Token> {
34    let chars: Vec<char> = s.chars().collect();
35    let n = chars.len();
36    let mut out = Vec::new();
37    let mut i = 0;
38    let mut line: u32 = 1;
39    while i < n {
40        let c = chars[i];
41        match c {
42            '\n' => {
43                line += 1;
44                i += 1;
45            }
46            ' ' | '\t' | '\r' => {
47                i += 1;
48            }
49            '(' | ')' => {
50                out.push(Token {
51                    text: c.to_string(),
52                    line,
53                });
54                i += 1;
55            }
56            ';' => {
57                // A comment runs to the end of its line; the line break is
58                // left for the arm above, which counts it.
59                while i < n && chars[i] != '\r' && chars[i] != '\n' {
60                    i += 1;
61                }
62            }
63            '"' => {
64                let start_line = line;
65                i += 1;
66                let mut buf = String::from("\"");
67                while i < n {
68                    let c = chars[i];
69                    if c == '\\' {
70                        match chars.get(i + 1) {
71                            Some('"') => {
72                                buf.push('"');
73                                i += 2;
74                            }
75                            Some('\\') => {
76                                buf.push('\\');
77                                i += 2;
78                            }
79                            _ => {
80                                buf.push(c);
81                                i += 1;
82                            }
83                        }
84                    } else if c == '"' {
85                        i += 1;
86                        break;
87                    } else {
88                        if c == '\n' {
89                            line += 1;
90                        }
91                        buf.push(c);
92                        i += 1;
93                    }
94                }
95                out.push(Token {
96                    text: buf,
97                    line: start_line,
98                });
99            }
100            _ => {
101                let start = i;
102                while i < n {
103                    let c = chars[i];
104                    if matches!(c, '(' | ')' | ' ' | '\t' | '\r' | '\n' | ';' | '"') {
105                        break;
106                    }
107                    i += 1;
108                }
109                out.push(Token {
110                    text: chars[start..i].iter().collect(),
111                    line,
112                });
113            }
114        }
115    }
116    out
117}
118
119/// VBA `CountLf`: how many line feeds a text holds.
120pub fn count_lf(s: &str) -> usize {
121    s.matches('\n').count()
122}
123
124/// VBA `LabeledLine` with no include map: the line as the user counts it,
125/// or `None` for the prelude's territory.
126fn user_line(raw_line: u32, line_offset: u32) -> Option<u32> {
127    if raw_line > line_offset {
128        Some(raw_line - line_offset)
129    } else {
130        None
131    }
132}
133
134/// VBA `SrcLineTag`: ` (vla line N)`, or nothing for the prelude.
135pub fn src_line_tag(raw_line: u32, line_offset: u32) -> String {
136    match user_line(raw_line, line_offset) {
137        Some(n) => format!(" (vla line {n})"),
138        None => String::new(),
139    }
140}
141
142/// VBA `OpenedAtTag`: ` for the list opened at vla line N`, or nothing.
143pub fn opened_at_tag(raw_line: u32, line_offset: u32) -> String {
144    match user_line(raw_line, line_offset) {
145        Some(n) => format!(" for the list opened at vla line {n}"),
146        None => String::new(),
147    }
148}
149
150/// VBA `ParseAll`: every top-level form of a token list. `line_offset` is
151/// the number of raw lines before the user's line 1 (`mLineOffset`): 0 for
152/// a bare read, the prelude's line count plus one for a compile.
153pub fn parse_all(toks: &[Token], line_offset: u32) -> Result<Vec<Form>, Refusal> {
154    let mut pos = 0;
155    let mut out = Vec::new();
156    while pos < toks.len() {
157        out.push(parse_form(toks, &mut pos, line_offset, 1)?);
158    }
159    Ok(out)
160}
161
162/// The deepest a list may nest, the top-level list being 1 (VLA.1): the
163/// reader refuses a list opened deeper, and so does the expander's walk,
164/// so that no stage after them recurses past it. Six times the deepest
165/// real program (the corpus golden nests 10 deep, `english.vla` 9, the
166/// prelude 6); every stage of the language holds well past it, and the
167/// core's emitter, the shallowest, holds 80 in a debug build.
168pub const FORM_DEPTH_LIMIT: u32 = 64;
169
170/// VBA `ParseForm`, `depth` the depth of the list a `(` here would open.
171fn parse_form(
172    toks: &[Token],
173    pos: &mut usize,
174    line_offset: u32,
175    depth: u32,
176) -> Result<Form, Refusal> {
177    let tok = &toks[*pos];
178    let raw_line = tok.line;
179    *pos += 1;
180    if tok.text == "(" {
181        if depth > FORM_DEPTH_LIMIT {
182            return Err(raise(
183                "vla-form-too-deep",
184                &[
185                    ("limit", &FORM_DEPTH_LIMIT.to_string()),
186                    ("loc", &src_line_tag(raw_line, line_offset)),
187                ],
188            ));
189        }
190        // A list is born on the line of its opening paren; below the
191        // offset that is the prelude, stored as 0.
192        let line = user_line(raw_line, line_offset).unwrap_or(0);
193        let mut items = Vec::new();
194        while *pos < toks.len() {
195            if toks[*pos].text == ")" {
196                *pos += 1;
197                return Ok(Form::List(List::new(items, line)));
198            }
199            items.push(parse_form(toks, pos, line_offset, depth + 1)?);
200        }
201        Err(raise(
202            "vla-unbalanced-parens",
203            &[("loc", &opened_at_tag(raw_line, line_offset))],
204        ))
205    } else if tok.text == ")" {
206        Err(raise(
207            "vla-unexpected-close-paren",
208            &[("loc", &src_line_tag(raw_line, line_offset))],
209        ))
210    } else if let Some(content) = tok.text.strip_prefix('"') {
211        Ok(Form::Str(content.to_string()))
212    } else {
213        Ok(Form::Sym(tok.text.clone()))
214    }
215}
216
217/// VBA `VlaReadForms`: the forms of a text on its own, every list tagged
218/// with its line.
219pub fn read_forms(text: &str) -> Result<Vec<Form>, Refusal> {
220    parse_all(&tokenize(text), 0)
221}
222
223/// VBA `VlaReadFormsWithLines`: the forms, each with its own source line
224/// (0 for an atom at top level, which carries none).
225pub fn read_forms_with_lines(text: &str) -> Result<Vec<(Form, u32)>, Refusal> {
226    Ok(read_forms(text)?
227        .into_iter()
228        .map(|f| {
229            let line = f.as_list().map(|l| l.line).unwrap_or(0);
230            (f, line)
231        })
232        .collect())
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238    use crate::printer::{write_datum, write_pretty};
239
240    fn texts(toks: &[Token]) -> Vec<&str> {
241        toks.iter().map(|t| t.text.as_str()).collect()
242    }
243
244    #[test]
245    fn tokenize_follows_the_vba_character_classes() {
246        let toks = tokenize("(set! x \"a \\\"q\\\" b\\n\")  ; a comment\r\n(y)");
247        assert_eq!(
248            texts(&toks),
249            vec!["(", "set!", "x", "\"a \"q\" b\\n", ")", "(", "y", ")"]
250        );
251        assert_eq!(toks[0].line, 1);
252        assert_eq!(toks[5].line, 2);
253        // A quote ends a symbol; a tab and a CR are whitespace.
254        assert_eq!(
255            texts(&tokenize("abc\"d\"\te\rf")),
256            vec!["abc", "\"d", "e", "f"]
257        );
258        // A line feed inside a string counts a line and stays in the text.
259        let toks = tokenize("\"two\nlines\" after");
260        assert_eq!(toks[0].text, "\"two\nlines");
261        assert_eq!(toks[0].line, 1);
262        assert_eq!(toks[1].line, 2);
263        // An unterminated string ends at the end of the text.
264        assert_eq!(texts(&tokenize("\"open")), vec!["\"open"]);
265        // Nothing to read reads nothing (TER-8).
266        assert!(tokenize("").is_empty());
267        assert!(tokenize("  ; only a comment\n").is_empty());
268    }
269
270    #[test]
271    fn parse_tags_lists_with_their_lines_less_the_offset() {
272        let forms = read_forms("(a\n  (b) c)\n\n(d)").unwrap();
273        assert_eq!(forms.len(), 2);
274        let a = forms[0].as_list().unwrap();
275        assert_eq!(a.line, 1);
276        assert_eq!(a.items[1].as_list().unwrap().line, 2);
277        assert_eq!(forms[1].as_list().unwrap().line, 4);
278        // With an offset, the first lines are the prelude's: stored as 0.
279        let forms = parse_all(&tokenize("(p)\n(q)\n(r)"), 2).unwrap();
280        assert_eq!(forms[0].as_list().unwrap().line, 0);
281        assert_eq!(forms[1].as_list().unwrap().line, 0);
282        assert_eq!(forms[2].as_list().unwrap().line, 1);
283        let with_lines = read_forms_with_lines("x\n(y)").unwrap();
284        assert_eq!(with_lines[0], (Form::sym("x"), 0));
285        assert_eq!(with_lines[1].1, 2);
286    }
287
288    #[test]
289    fn the_two_reader_refusals_name_their_lines() {
290        let r = read_forms("(a\n(b").unwrap_err();
291        assert_eq!(r.id, "vla-unbalanced-parens");
292        assert_eq!(
293            r.text,
294            "unbalanced parentheses: missing ')' for the list opened at vla line 2"
295        );
296        let r = read_forms("(a)\n)").unwrap_err();
297        assert_eq!(r.id, "vla-unexpected-close-paren");
298        assert_eq!(r.text, "unexpected ')' (vla line 2)");
299        // In the prelude's territory the tag is silent.
300        let r = parse_all(&tokenize("(a"), 5).unwrap_err();
301        assert_eq!(r.text, "unbalanced parentheses: missing ')'");
302    }
303
304    #[test]
305    fn a_list_past_the_depth_limit_is_refused_by_name() {
306        // VLA.1, TestFormTooDeep's first pair: 64 nested lists read, and the
307        // 65th is refused at its line, where the reader used to recurse
308        // until the stack gave out; far past it is the same refusal.
309        let nest = |n: usize| format!("{}{}", "(".repeat(n), ")".repeat(n));
310        assert_eq!(read_forms(&nest(64)).unwrap().len(), 1);
311        let r = read_forms(&nest(65)).unwrap_err();
312        assert_eq!(r.id, "vla-form-too-deep");
313        assert_eq!(
314            r.text,
315            "lists nested more than 64 deep (vla line 1) - no program needs that many"
316        );
317        assert_eq!(
318            read_forms(&nest(100_000)).unwrap_err().id,
319            "vla-form-too-deep"
320        );
321    }
322
323    const GOLDEN: &str = include_str!("../testdata/instructions_golden.vla");
324    const PRELUDE: &str = include_str!("../testdata/prelude.vla");
325
326    #[test]
327    fn the_golden_and_the_prelude_read_print_and_read_again() {
328        // The counts are what VlaReadForms(...).Count gives in Excel (the
329        // owner's step 0 reading confirms them).
330        let golden = read_forms(GOLDEN).unwrap();
331        assert_eq!(golden.len(), 248);
332        let prelude = read_forms(PRELUDE).unwrap();
333        assert_eq!(prelude.len(), 46);
334        for forms in [&golden, &prelude] {
335            let flat: String = forms.iter().map(write_datum).collect::<Vec<_>>().join("\n");
336            assert_eq!(&read_forms(&flat).unwrap(), forms);
337            let pretty: String = forms
338                .iter()
339                .map(|f| write_pretty(f, 0))
340                .collect::<Vec<_>>()
341                .join("\n");
342            assert_eq!(&read_forms(&pretty).unwrap(), forms);
343        }
344        assert!(golden[0].head_is("const"));
345        assert!(prelude.iter().all(|f| f.head_is("defmacro")));
346    }
347
348    #[test]
349    fn count_lf_counts_line_feeds_only() {
350        assert_eq!(count_lf("a\r\nb\nc"), 2);
351        assert_eq!(count_lf(""), 0);
352    }
353}