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