Skip to main content

muon/
parse.rs

1//! Scannerless parser driven by an explicit heap stack.
2//!
3//! Nesting (tuples, lists, blocks, and prefixed items) is walked with an explicit
4//! `Vec` of open frames rather than Rust recursion, so nesting depth is bounded
5//! only by available memory — no call-stack growth, no overflow on adversarial
6//! input, and no depth cap.
7//!
8//! Newlines are significant: ` `, `\r`, `\t` are whitespace, but `\n` and `,`
9//! are separators (`<sep>`). A `\r\n` therefore reads as whitespace `\r`
10//! followed by a `\n` separator, so `\r\n` and `\n` parse identically.
11
12use alloc::boxed::Box;
13use alloc::vec::Vec;
14
15use crate::tree::{Chain, Item, Seq};
16
17/// Parse a whole Muon file: a top-level `<seq>` up to end of input.
18pub fn parse(input: &str) -> Result<Seq<'_>, ParseError> {
19    let mut p = Parser {
20        src: input,
21        bytes: input.as_bytes(),
22        pos: 0,
23    };
24
25    // The container being built is kept in the local `cur` so the hot path (a
26    // chain of leaves) touches it as a register-resident value, not through the
27    // stack. `stack` holds only the *suspended* enclosing containers, pushed on
28    // an opener and resumed on the matching closer. `pending` holds sigils read
29    // but not yet attached to their item (see `<prefixed>`).
30    let mut cur = Frame::top();
31    let mut stack: Vec<Frame> = Vec::new();
32    // Sigils read but not yet attached to their item, plus the byte offset of the
33    // first one (the start of the atom being built — a run glues to the previous
34    // atom only when this start touches its end).
35    let mut pending: Vec<char> = Vec::new();
36    let mut pending_start: Option<usize> = None;
37
38    // A `#!` at byte offset 0 is a shebang: a `<comm>` running to the end of its
39    // line, stored raw with its `#!`. This is the notation's one position-
40    // dependent rule — anywhere else `#` is the `<prefixed>` sigil. The trailing
41    // `\n` (if any) is left for the loop to read as a `<sep>`, exactly as for a
42    // `//` line comment.
43    if p.bytes.starts_with(b"#!") {
44        let start = p.pos;
45        while let Some(c) = p.peek() {
46            if c == b'\n' {
47                break;
48            }
49            p.bump();
50        }
51        cur.push_break(Item::Comm(&p.src[start..p.pos]));
52    }
53
54    loop {
55        p.skip_ws();
56        match p.peek() {
57            // End of input: an open container is unclosed; the top level is done.
58            None => {
59                if let Some(t) = cur.term {
60                    return Err(p.err(Parser::unclosed(t)));
61                }
62                cur.flush_chain();
63                break;
64            }
65            // The closer we were waiting for: finish this container, wrap it in any
66            // sigils it carried, resume the parent, and attach it to its chain —
67            // gluing it into a run if it touches the parent's previous atom.
68            Some(c) if Some(c) == cur.term => {
69                p.bump(); // consume the terminator
70                cur.flush_chain();
71                let start = cur.start;
72                let seq = Seq(core::mem::take(&mut cur.chains));
73                let item = wrap(build_container(c, seq), &cur.sigils);
74                cur = stack.pop().expect("a closer implies an open parent");
75                cur.push_atom(item, start, p.pos);
76            }
77            // A separator ends the current chain (empty slots collapse to nothing).
78            Some(c) if Parser::is_sep(c) => {
79                p.bump();
80                cur.flush_chain();
81            }
82            // A closer that isn't our terminator: stray, or a mismatched bracket.
83            Some(c) if Parser::is_close(c) => {
84                return Err(p.err(Parser::unexpected_close(c)));
85            }
86            // `&` / `#` sigils: each must glue to an item, else it dangles.
87            Some(s @ (b'&' | b'#')) => {
88                let sig = p.pos;
89                p.bump(); // consume the sigil
90                match p.peek() {
91                    Some(c) if Parser::opens_glued_item(c) => {
92                        if pending.is_empty() {
93                            pending_start = Some(sig);
94                        }
95                        pending.push(s as char);
96                    }
97                    _ => return Err(p.err(ErrorKind::DanglingSigil)),
98                }
99            }
100            // `.`: a sigil when right-glued to an item, else a standalone `<punct>`.
101            Some(b'.') => match p.at(1) {
102                Some(c) if Parser::opens_glued_item(c) => {
103                    let sig = p.pos;
104                    p.bump(); // consume '.'
105                    if pending.is_empty() {
106                        pending_start = Some(sig);
107                    }
108                    pending.push('.');
109                }
110                _ => {
111                    let start = p.pos;
112                    p.bump(); // consume '.'
113                              // A punct ends a run; any pending sigils still wrap it (`..`).
114                    let item = wrap(Item::Punct(&p.src[start..p.pos]), &pending);
115                    pending.clear();
116                    pending_start = None;
117                    cur.push_break(item);
118                }
119            },
120            // An opener suspends the current container and starts a fresh one,
121            // inheriting any pending sigils so they wrap it once it closes. The
122            // atom's start (sigil or opener) rides the child frame to its close.
123            Some(c @ (b'(' | b'[' | b'{')) => {
124                let start = pending_start.take().unwrap_or(p.pos);
125                p.bump(); // consume the opener
126                let term = match c {
127                    b'(' => b')',
128                    b'[' => b']',
129                    _ => b'}',
130                };
131                let sigils = core::mem::take(&mut pending);
132                let child = Frame {
133                    term: Some(term),
134                    sigils,
135                    start,
136                    chains: Vec::new(),
137                    items: Vec::new(),
138                    last_end: None,
139                };
140                stack.push(core::mem::replace(&mut cur, child));
141            }
142            // Leaves: parse in place (they never nest) and attach with any sigils,
143            // gluing into a run when they touch the previous atom.
144            Some(b'"') => {
145                let start = pending_start.take().unwrap_or(p.pos);
146                let item = wrap(p.parse_str()?, &pending);
147                pending.clear();
148                cur.push_atom(item, start, p.pos);
149            }
150            // A comment ends a run; any pending sigils still wrap it (`.//c`).
151            Some(b'/') => {
152                let item = wrap(p.parse_comment()?, &pending);
153                pending.clear();
154                pending_start = None;
155                cur.push_break(item);
156            }
157            Some(c) if Parser::is_symchar(c) => {
158                let start = pending_start.take().unwrap_or(p.pos);
159                let item = wrap(p.parse_sym(), &pending);
160                pending.clear();
161                cur.push_atom(item, start, p.pos);
162            }
163            Some(c) if Parser::is_punctchar(c) => {
164                // A punct ends a run; any pending sigils still wrap it (`&:`).
165                let item = wrap(p.parse_punct(), &pending);
166                pending.clear();
167                pending_start = None;
168                cur.push_break(item);
169            }
170            _ => return Err(p.err(ErrorKind::UnexpectedChar)),
171        }
172    }
173
174    Ok(Seq(cur.chains))
175}
176
177/// One open container on the parse stack: the `<seq>` being built inside it, plus
178/// the sigils (if any) that will wrap it once it closes.
179struct Frame<'a> {
180    /// The closing byte to stop at (`)`/`]`/`}`), or `None` for the top level.
181    term: Option<u8>,
182    /// Sigils to wrap this container's `Item` on close; empty at top level.
183    sigils: Vec<char>,
184    /// Byte offset where this container's atom begins (its sigil, or its opener),
185    /// used to glue it into a run when it closes. Unused for the top frame.
186    start: usize,
187    /// Completed chains of the seq.
188    chains: Vec<Chain<'a>>,
189    /// Items of the in-progress chain, flushed into `chains` at a separator/close.
190    items: Vec<Item<'a>>,
191    /// Byte offset just past the last atom pushed to the in-progress chain, or
192    /// `None` when the chain is empty or its last item was a punct/comment (a run
193    /// never spans those). An atom whose start touches this glues into a run.
194    last_end: Option<usize>,
195}
196
197impl<'a> Frame<'a> {
198    fn top() -> Self {
199        Frame {
200            term: None,
201            sigils: Vec::new(),
202            start: 0,
203            chains: Vec::new(),
204            items: Vec::new(),
205            last_end: None,
206        }
207    }
208
209    /// End the in-progress chain, if any. A `<chain>` is never empty, so a run of
210    /// separators (or a container that opened and closed empty) adds no chain.
211    fn flush_chain(&mut self) {
212        if !self.items.is_empty() {
213            self.chains.push(Chain(core::mem::take(&mut self.items)));
214        }
215        self.last_end = None;
216    }
217
218    /// Attach an atom (`start`..`end`) to the current chain. If it touches the
219    /// previous atom (`last_end == Some(start)`) the two glue into a `Run`,
220    /// extending an existing run in place or starting a fresh two-member one;
221    /// otherwise it is a bare item. Records `end` as the new run anchor.
222    fn push_atom(&mut self, item: Item<'a>, start: usize, end: usize) {
223        if self.last_end == Some(start) {
224            match self.items.last_mut() {
225                Some(Item::Run(members)) => members.push(item),
226                _ => {
227                    let prev = self.items.pop().expect("glue implies a previous atom");
228                    let mut members = Vec::with_capacity(4);
229                    members.push(prev);
230                    members.push(item);
231                    self.items.push(Item::Run(members));
232                }
233            }
234        } else {
235            self.items.push(item);
236        }
237        self.last_end = Some(end);
238    }
239
240    /// Attach an item that ends any run in progress and joins none — a `<punct>`
241    /// or a `<comm>`. The next atom cannot glue onto it.
242    fn push_break(&mut self, item: Item<'a>) {
243        self.items.push(item);
244        self.last_end = None;
245    }
246}
247
248/// Wrap `item` in `sigils` outermost-first: the first sigil read (index 0) ends up
249/// the outermost `Prefixed`, so `&.x` is `&(.(x))`.
250fn wrap<'a>(mut item: Item<'a>, sigils: &[char]) -> Item<'a> {
251    for &sigil in sigils.iter().rev() {
252        item = Item::Prefixed {
253            sigil,
254            item: Box::new(item),
255        };
256    }
257    item
258}
259
260/// Build the container `Item` for a `<seq>` from the closer that ended it.
261fn build_container(term: u8, seq: Seq<'_>) -> Item<'_> {
262    match term {
263        b')' => Item::Parens(seq),
264        b']' => Item::Brackets(seq),
265        _ => Item::Braces(seq),
266    }
267}
268
269/// A parse failure, with the byte offset into the input where it was detected.
270#[derive(Debug, Clone, PartialEq, Eq)]
271pub struct ParseError {
272    pub offset: usize,
273    pub kind: ErrorKind,
274}
275
276#[derive(Debug, Clone, PartialEq, Eq)]
277pub enum ErrorKind {
278    /// A character that cannot begin an item (e.g. a lone `/`).
279    UnexpectedChar,
280    /// A `)` with no matching `(`.
281    UnexpectedCloseParen,
282    /// A `]` with no matching `[`.
283    UnexpectedCloseBracket,
284    /// A `}` with no matching `{`.
285    UnexpectedCloseBrace,
286    /// A `(` whose `)` never arrived before end of input.
287    UnclosedParens,
288    /// A `[` whose `]` never arrived before end of input.
289    UnclosedBrackets,
290    /// A `{` whose `}` never arrived before end of input.
291    UnclosedBraces,
292    /// A `"` whose closing `"` never arrived before end of input.
293    UnclosedString,
294    /// A `/*` whose `*/` never arrived before end of input.
295    UnclosedComment,
296    /// A `\` followed by a character other than `" n t r \ / b f`, or a `\u`
297    /// not followed by exactly four hex digits.
298    BadEscape,
299    /// A literal newline inside a string literal.
300    RawNewlineInString,
301    /// A `&` or `#` sigil not immediately followed by an item.
302    DanglingSigil,
303}
304
305struct Parser<'a> {
306    src: &'a str,
307    bytes: &'a [u8],
308    pos: usize,
309}
310
311impl<'a> Parser<'a> {
312    fn peek(&self) -> Option<u8> {
313        self.bytes.get(self.pos).copied()
314    }
315
316    fn at(&self, off: usize) -> Option<u8> {
317        self.bytes.get(self.pos + off).copied()
318    }
319
320    fn bump(&mut self) {
321        self.pos += 1;
322    }
323
324    fn err(&self, kind: ErrorKind) -> ParseError {
325        ParseError {
326            offset: self.pos,
327            kind,
328        }
329    }
330
331    /// Skip inline whitespace only: ` `, `\r`, `\t`. Never `\n` (a separator).
332    fn skip_ws(&mut self) {
333        while let Some(c) = self.peek() {
334            if c == b' ' || c == b'\r' || c == b'\t' {
335                self.pos += 1;
336            } else {
337                break;
338            }
339        }
340    }
341
342    pub(crate) fn is_symchar(c: u8) -> bool {
343        matches!(c,
344            b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z'
345            | b'_' | b'-' | b'+')
346    }
347
348    /// Punctchars stand between items (never inside a literal): `:`, `=`, `|`,
349    /// `>`, `<`. (`>` lets `|>` and a future `=>` munch as one `<punct>`; `<`/`>`
350    /// give Elly the ordering-pattern operators `< e` / `> e`.)
351    fn is_punctchar(c: u8) -> bool {
352        c == b':' || c == b'=' || c == b'|' || c == b'>' || c == b'<'
353    }
354
355    fn is_sep(c: u8) -> bool {
356        c == b',' || c == b'\n'
357    }
358
359    fn is_close(c: u8) -> bool {
360        c == b')' || c == b']' || c == b'}'
361    }
362
363    /// True when `c`, sitting directly after a sigil, opens a glued item — i.e.
364    /// it is not whitespace, a `<sep>`, or a closer. Used to tell a `<prefixed>`
365    /// from a dangling sigil.
366    fn opens_glued_item(c: u8) -> bool {
367        c != b' ' && c != b'\r' && c != b'\t' && !Self::is_sep(c) && !Self::is_close(c)
368    }
369
370    /// The error for a closing bracket with no matching opener (or a mismatch).
371    fn unexpected_close(c: u8) -> ErrorKind {
372        match c {
373            b')' => ErrorKind::UnexpectedCloseParen,
374            b']' => ErrorKind::UnexpectedCloseBracket,
375            _ => ErrorKind::UnexpectedCloseBrace,
376        }
377    }
378
379    /// The error for an opener (whose closer is `term`) that never arrived.
380    fn unclosed(term: u8) -> ErrorKind {
381        match term {
382            b')' => ErrorKind::UnclosedParens,
383            b']' => ErrorKind::UnclosedBrackets,
384            _ => ErrorKind::UnclosedBraces,
385        }
386    }
387
388    fn parse_sym(&mut self) -> Item<'a> {
389        let start = self.pos;
390        while let Some(c) = self.peek() {
391            if Self::is_symchar(c) {
392                self.pos += 1;
393            } else {
394                break;
395            }
396        }
397        Item::Sym(&self.src[start..self.pos])
398    }
399
400    /// A maximal run of punctchars, e.g. `:`, `::`, `=`, `==`, `:=`, `|>`.
401    fn parse_punct(&mut self) -> Item<'a> {
402        let start = self.pos;
403        while let Some(c) = self.peek() {
404            if Self::is_punctchar(c) {
405                self.pos += 1;
406            } else {
407                break;
408            }
409        }
410        Item::Punct(&self.src[start..self.pos])
411    }
412
413    /// A `//` line comment or a `/* */` block comment (non-nesting), stored raw
414    /// with its delimiters. Newlines inside a block comment are not separators.
415    fn parse_comment(&mut self) -> Result<Item<'a>, ParseError> {
416        let start = self.pos;
417        self.bump(); // consume '/'
418        match self.peek() {
419            Some(b'/') => {
420                self.bump();
421                while let Some(c) = self.peek() {
422                    if c == b'\n' {
423                        break;
424                    }
425                    self.pos += 1;
426                }
427                Ok(Item::Comm(&self.src[start..self.pos]))
428            }
429            Some(b'*') => {
430                self.bump();
431                loop {
432                    match self.peek() {
433                        None => {
434                            return Err(ParseError {
435                                offset: start,
436                                kind: ErrorKind::UnclosedComment,
437                            })
438                        }
439                        Some(b'*') if self.at(1) == Some(b'/') => {
440                            self.pos += 2;
441                            break;
442                        }
443                        Some(_) => self.pos += 1,
444                    }
445                }
446                Ok(Item::Comm(&self.src[start..self.pos]))
447            }
448            // A lone '/' is not a symchar, so it can only open a comment.
449            _ => Err(self.err(ErrorKind::UnexpectedChar)),
450        }
451    }
452
453    fn parse_str(&mut self) -> Result<Item<'a>, ParseError> {
454        let open = self.pos;
455        self.bump(); // opening quote
456        let start = self.pos;
457        loop {
458            match self.peek() {
459                None => {
460                    return Err(ParseError {
461                        offset: open,
462                        kind: ErrorKind::UnclosedString,
463                    })
464                }
465                Some(b'"') => {
466                    let inner = &self.src[start..self.pos];
467                    self.bump(); // closing quote
468                    return Ok(Item::Str(inner));
469                }
470                Some(b'\n') => return Err(self.err(ErrorKind::RawNewlineInString)),
471                Some(b'\\') => match self.at(1) {
472                    // Single-character escapes: Muon's own plus JSON's `\/ \b \f`.
473                    Some(b'"') | Some(b'n') | Some(b't') | Some(b'r') | Some(b'\\')
474                    | Some(b'/') | Some(b'b') | Some(b'f') => {
475                        self.pos += 2;
476                    }
477                    // `\uXXXX` — validate exactly four hex digits, nothing more.
478                    // Combining surrogate pairs and decoding the code point is a
479                    // higher-layer job (see docs/todo/elly-json.md); the notation
480                    // layer only accepts the escape *grammar* and stores it raw.
481                    Some(b'u') => {
482                        for off in 2..6 {
483                            match self.at(off) {
484                                Some(c) if c.is_ascii_hexdigit() => {}
485                                _ => return Err(self.err(ErrorKind::BadEscape)),
486                            }
487                        }
488                        self.pos += 6;
489                    }
490                    _ => return Err(self.err(ErrorKind::BadEscape)),
491                },
492                Some(_) => self.pos += 1,
493            }
494        }
495    }
496}
497
498/// Whether `s` is a single Muon `<sym>`: non-empty, every byte a symchar
499/// (`0-9`, `A-Z`, `a-z`, `_`, `-`, `+`).
500pub fn is_sym(s: &str) -> bool {
501    !s.is_empty() && s.bytes().all(Parser::is_symchar)
502}