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    let mut pending: Vec<char> = Vec::new();
33
34    loop {
35        p.skip_ws();
36        match p.peek() {
37            // End of input: an open container is unclosed; the top level is done.
38            None => {
39                if let Some(t) = cur.term {
40                    return Err(p.err(Parser::unclosed(t)));
41                }
42                cur.flush_chain();
43                break;
44            }
45            // The closer we were waiting for: finish this container, wrap it in any
46            // sigils it carried, resume the parent, and attach it to its chain.
47            Some(c) if Some(c) == cur.term => {
48                p.bump(); // consume the terminator
49                cur.flush_chain();
50                let seq = Seq(core::mem::take(&mut cur.chains));
51                let item = wrap(build_container(c, seq), &cur.sigils);
52                cur = stack.pop().expect("a closer implies an open parent");
53                cur.items.push(item);
54            }
55            // A separator ends the current chain (empty slots collapse to nothing).
56            Some(c) if Parser::is_sep(c) => {
57                p.bump();
58                cur.flush_chain();
59            }
60            // A closer that isn't our terminator: stray, or a mismatched bracket.
61            Some(c) if Parser::is_close(c) => {
62                return Err(p.err(Parser::unexpected_close(c)));
63            }
64            // `&` sigil: must glue to an item, else it dangles.
65            Some(b'&') => {
66                p.bump(); // consume '&'
67                match p.peek() {
68                    Some(c) if Parser::opens_glued_item(c) => pending.push('&'),
69                    _ => return Err(p.err(ErrorKind::DanglingSigil)),
70                }
71            }
72            // `.`: a sigil when right-glued to an item, else a standalone `<punct>`.
73            Some(b'.') => match p.at(1) {
74                Some(c) if Parser::opens_glued_item(c) => {
75                    p.bump(); // consume '.'
76                    pending.push('.');
77                }
78                _ => {
79                    let start = p.pos;
80                    p.bump(); // consume '.'
81                    cur.push_item(Item::Punct(&p.src[start..p.pos]), &mut pending);
82                }
83            },
84            // An opener suspends the current container and starts a fresh one,
85            // inheriting any pending sigils so they wrap it once it closes.
86            Some(c @ (b'(' | b'[' | b'{')) => {
87                p.bump(); // consume the opener
88                let term = match c {
89                    b'(' => b')',
90                    b'[' => b']',
91                    _ => b'}',
92                };
93                let sigils = core::mem::take(&mut pending);
94                let child = Frame {
95                    term: Some(term),
96                    sigils,
97                    chains: Vec::new(),
98                    items: Vec::new(),
99                };
100                stack.push(core::mem::replace(&mut cur, child));
101            }
102            // Leaves: parse in place (they never nest) and attach with any sigils.
103            Some(b'"') => {
104                let item = p.parse_str()?;
105                cur.push_item(item, &mut pending);
106            }
107            Some(b'/') => {
108                let item = p.parse_comment()?;
109                cur.push_item(item, &mut pending);
110            }
111            Some(c) if Parser::is_symchar(c) => cur.push_item(p.parse_sym(), &mut pending),
112            Some(c) if Parser::is_punctchar(c) => cur.push_item(p.parse_punct(), &mut pending),
113            _ => return Err(p.err(ErrorKind::UnexpectedChar)),
114        }
115    }
116
117    Ok(Seq(cur.chains))
118}
119
120/// One open container on the parse stack: the `<seq>` being built inside it, plus
121/// the sigils (if any) that will wrap it once it closes.
122struct Frame<'a> {
123    /// The closing byte to stop at (`)`/`]`/`}`), or `None` for the top level.
124    term: Option<u8>,
125    /// Sigils to wrap this container's `Item` on close; empty at top level.
126    sigils: Vec<char>,
127    /// Completed chains of the seq.
128    chains: Vec<Chain<'a>>,
129    /// Items of the in-progress chain, flushed into `chains` at a separator/close.
130    items: Vec<Item<'a>>,
131}
132
133impl<'a> Frame<'a> {
134    fn top() -> Self {
135        Frame {
136            term: None,
137            sigils: Vec::new(),
138            chains: Vec::new(),
139            items: Vec::new(),
140        }
141    }
142
143    /// End the in-progress chain, if any. A `<chain>` is never empty, so a run of
144    /// separators (or a container that opened and closed empty) adds no chain.
145    fn flush_chain(&mut self) {
146        if !self.items.is_empty() {
147            self.chains.push(Chain(core::mem::take(&mut self.items)));
148        }
149    }
150
151    /// Attach a freshly-parsed leaf to the current chain, wrapping it in any
152    /// pending sigils (drained). The common case — no sigils — pushes directly.
153    fn push_item(&mut self, item: Item<'a>, pending: &mut Vec<char>) {
154        if pending.is_empty() {
155            self.items.push(item);
156        } else {
157            self.items.push(wrap(item, pending));
158            pending.clear();
159        }
160    }
161}
162
163/// Wrap `item` in `sigils` outermost-first: the first sigil read (index 0) ends up
164/// the outermost `Prefixed`, so `&.x` is `&(.(x))`.
165fn wrap<'a>(mut item: Item<'a>, sigils: &[char]) -> Item<'a> {
166    for &sigil in sigils.iter().rev() {
167        item = Item::Prefixed {
168            sigil,
169            item: Box::new(item),
170        };
171    }
172    item
173}
174
175/// Build the container `Item` for a `<seq>` from the closer that ended it.
176fn build_container(term: u8, seq: Seq<'_>) -> Item<'_> {
177    match term {
178        b')' => Item::Tuple(seq),
179        b']' => Item::List(seq),
180        _ => Item::Block(seq),
181    }
182}
183
184/// A parse failure, with the byte offset into the input where it was detected.
185#[derive(Debug, Clone, PartialEq, Eq)]
186pub struct ParseError {
187    pub offset: usize,
188    pub kind: ErrorKind,
189}
190
191#[derive(Debug, Clone, PartialEq, Eq)]
192pub enum ErrorKind {
193    /// A character that cannot begin an item (e.g. a lone `/`).
194    UnexpectedChar,
195    /// A `)` with no matching `(`.
196    UnexpectedCloseParen,
197    /// A `]` with no matching `[`.
198    UnexpectedCloseBracket,
199    /// A `}` with no matching `{`.
200    UnexpectedCloseBrace,
201    /// A `(` whose `)` never arrived before end of input.
202    UnclosedTuple,
203    /// A `[` whose `]` never arrived before end of input.
204    UnclosedList,
205    /// A `{` whose `}` never arrived before end of input.
206    UnclosedBlock,
207    /// A `"` whose closing `"` never arrived before end of input.
208    UnclosedString,
209    /// A `/*` whose `*/` never arrived before end of input.
210    UnclosedComment,
211    /// A `\` followed by a character other than `" n t r \ / b f`, or a `\u`
212    /// not followed by exactly four hex digits.
213    BadEscape,
214    /// A literal newline inside a string literal.
215    RawNewlineInString,
216    /// A `&` sigil not immediately followed by an item.
217    DanglingSigil,
218}
219
220struct Parser<'a> {
221    src: &'a str,
222    bytes: &'a [u8],
223    pos: usize,
224}
225
226impl<'a> Parser<'a> {
227    fn peek(&self) -> Option<u8> {
228        self.bytes.get(self.pos).copied()
229    }
230
231    fn at(&self, off: usize) -> Option<u8> {
232        self.bytes.get(self.pos + off).copied()
233    }
234
235    fn bump(&mut self) {
236        self.pos += 1;
237    }
238
239    fn err(&self, kind: ErrorKind) -> ParseError {
240        ParseError {
241            offset: self.pos,
242            kind,
243        }
244    }
245
246    /// Skip inline whitespace only: ` `, `\r`, `\t`. Never `\n` (a separator).
247    fn skip_ws(&mut self) {
248        while let Some(c) = self.peek() {
249            if c == b' ' || c == b'\r' || c == b'\t' {
250                self.pos += 1;
251            } else {
252                break;
253            }
254        }
255    }
256
257    fn is_symchar(c: u8) -> bool {
258        matches!(c,
259            b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z'
260            | b'_' | b'-' | b'+')
261    }
262
263    /// Punctchars stand between items (never inside a literal): `:`, `=`, `|`,
264    /// `>`, `<`. (`>` lets `|>` and a future `=>` munch as one `<punct>`; `<`/`>`
265    /// give Elly the ordering-pattern operators `< e` / `> e`.)
266    fn is_punctchar(c: u8) -> bool {
267        c == b':' || c == b'=' || c == b'|' || c == b'>' || c == b'<'
268    }
269
270    fn is_sep(c: u8) -> bool {
271        c == b',' || c == b'\n'
272    }
273
274    fn is_close(c: u8) -> bool {
275        c == b')' || c == b']' || c == b'}'
276    }
277
278    /// True when `c`, sitting directly after a sigil, opens a glued item — i.e.
279    /// it is not whitespace, a `<sep>`, or a closer. Used to tell a `<prefixed>`
280    /// from a dangling sigil.
281    fn opens_glued_item(c: u8) -> bool {
282        c != b' ' && c != b'\r' && c != b'\t' && !Self::is_sep(c) && !Self::is_close(c)
283    }
284
285    /// The error for a closing bracket with no matching opener (or a mismatch).
286    fn unexpected_close(c: u8) -> ErrorKind {
287        match c {
288            b')' => ErrorKind::UnexpectedCloseParen,
289            b']' => ErrorKind::UnexpectedCloseBracket,
290            _ => ErrorKind::UnexpectedCloseBrace,
291        }
292    }
293
294    /// The error for an opener (whose closer is `term`) that never arrived.
295    fn unclosed(term: u8) -> ErrorKind {
296        match term {
297            b')' => ErrorKind::UnclosedTuple,
298            b']' => ErrorKind::UnclosedList,
299            _ => ErrorKind::UnclosedBlock,
300        }
301    }
302
303    fn parse_sym(&mut self) -> Item<'a> {
304        let start = self.pos;
305        while let Some(c) = self.peek() {
306            if Self::is_symchar(c) {
307                self.pos += 1;
308            } else {
309                break;
310            }
311        }
312        Item::Sym(&self.src[start..self.pos])
313    }
314
315    /// A maximal run of punctchars, e.g. `:`, `::`, `=`, `==`, `:=`, `|>`.
316    fn parse_punct(&mut self) -> Item<'a> {
317        let start = self.pos;
318        while let Some(c) = self.peek() {
319            if Self::is_punctchar(c) {
320                self.pos += 1;
321            } else {
322                break;
323            }
324        }
325        Item::Punct(&self.src[start..self.pos])
326    }
327
328    /// A `//` line comment or a `/* */` block comment (non-nesting), stored raw
329    /// with its delimiters. Newlines inside a block comment are not separators.
330    fn parse_comment(&mut self) -> Result<Item<'a>, ParseError> {
331        let start = self.pos;
332        self.bump(); // consume '/'
333        match self.peek() {
334            Some(b'/') => {
335                self.bump();
336                while let Some(c) = self.peek() {
337                    if c == b'\n' {
338                        break;
339                    }
340                    self.pos += 1;
341                }
342                Ok(Item::Comm(&self.src[start..self.pos]))
343            }
344            Some(b'*') => {
345                self.bump();
346                loop {
347                    match self.peek() {
348                        None => {
349                            return Err(ParseError {
350                                offset: start,
351                                kind: ErrorKind::UnclosedComment,
352                            })
353                        }
354                        Some(b'*') if self.at(1) == Some(b'/') => {
355                            self.pos += 2;
356                            break;
357                        }
358                        Some(_) => self.pos += 1,
359                    }
360                }
361                Ok(Item::Comm(&self.src[start..self.pos]))
362            }
363            // A lone '/' is not a symchar, so it can only open a comment.
364            _ => Err(self.err(ErrorKind::UnexpectedChar)),
365        }
366    }
367
368    fn parse_str(&mut self) -> Result<Item<'a>, ParseError> {
369        let open = self.pos;
370        self.bump(); // opening quote
371        let start = self.pos;
372        loop {
373            match self.peek() {
374                None => {
375                    return Err(ParseError {
376                        offset: open,
377                        kind: ErrorKind::UnclosedString,
378                    })
379                }
380                Some(b'"') => {
381                    let inner = &self.src[start..self.pos];
382                    self.bump(); // closing quote
383                    return Ok(Item::Str(inner));
384                }
385                Some(b'\n') => return Err(self.err(ErrorKind::RawNewlineInString)),
386                Some(b'\\') => match self.at(1) {
387                    // Single-character escapes: Muon's own plus JSON's `\/ \b \f`.
388                    Some(b'"') | Some(b'n') | Some(b't') | Some(b'r') | Some(b'\\')
389                    | Some(b'/') | Some(b'b') | Some(b'f') => {
390                        self.pos += 2;
391                    }
392                    // `\uXXXX` — validate exactly four hex digits, nothing more.
393                    // Combining surrogate pairs and decoding the code point is a
394                    // higher-layer job (see docs/todo/elly-json.md); the notation
395                    // layer only accepts the escape *grammar* and stores it raw.
396                    Some(b'u') => {
397                        for off in 2..6 {
398                            match self.at(off) {
399                                Some(c) if c.is_ascii_hexdigit() => {}
400                                _ => return Err(self.err(ErrorKind::BadEscape)),
401                            }
402                        }
403                        self.pos += 6;
404                    }
405                    _ => return Err(self.err(ErrorKind::BadEscape)),
406                },
407                Some(_) => self.pos += 1,
408            }
409        }
410    }
411}