Skip to main content

brink_syntax_native/parser/
mod.rs

1mod annotation;
2mod binding;
3mod block;
4mod choice;
5mod content;
6mod control_flow;
7mod decl;
8mod divert;
9mod doc_comment;
10mod element;
11mod expr;
12mod family;
13mod markup;
14mod source_file;
15mod stmt;
16#[cfg(test)]
17mod tests;
18mod types;
19
20use crate::SyntaxKind::{self, ERROR};
21use crate::lexer;
22use rowan::GreenNode;
23
24/// Result of parsing a `.brink` source file.
25///
26/// `PartialEq` compares the green tree structurally (rowan `GreenNode`
27/// equality is content-based) plus the error list.
28#[derive(Clone, PartialEq, Eq)]
29pub struct Parse {
30    green: GreenNode,
31    errors: Vec<ParseError>,
32}
33
34impl Parse {
35    /// The root green node of the lossless CST.
36    #[must_use]
37    pub fn green(&self) -> &GreenNode {
38        &self.green
39    }
40
41    /// The root syntax node (typed wrapper around the green tree).
42    #[must_use]
43    pub fn syntax(&self) -> crate::SyntaxNode {
44        crate::SyntaxNode::new_root(self.green.clone())
45    }
46
47    /// Parse errors encountered.
48    #[must_use]
49    pub fn errors(&self) -> &[ParseError] {
50        &self.errors
51    }
52}
53
54/// A parse diagnostic's severity — whether it blocks compilation.
55///
56/// This crate has no `brink-ir` dependency (peer-crate rule, `lib.rs`'s
57/// doc comment), so this stays a small local enum rather than reusing
58/// `brink_ir::Severity` — consumers (`brink-db`'s `lower_native_file`) map
59/// it onto the appropriate `DiagnosticCode` (`E037` for `Error`, a
60/// dedicated Warning-severity code for `Warning`) at the seam where the two
61/// diagnostic vocabularies meet.
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum ParseSeverity {
64    /// Malformed source — blocks compilation.
65    Error,
66    /// Advisory only — surfaced to the user but never blocks compilation
67    /// (issue #1263: `<-` outside a choice point *can* be literal dialogue,
68    /// so a hard error would be wrong).
69    Warning,
70}
71
72/// A parse error with a message and the source range it points at.
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct ParseError {
75    pub message: String,
76    /// Byte range in the source that the error points at.
77    pub range: rowan::TextRange,
78    /// Whether this diagnostic blocks compilation. Defaults to `Error` for
79    /// every existing diagnostic (`Parser::error`); only `Parser::warning`
80    /// produces `Warning`.
81    pub severity: ParseSeverity,
82}
83
84/// Parse a `.brink` source string into a lossless CST.
85#[must_use]
86pub fn parse(source: &str) -> Parse {
87    let raw_tokens = lexer::lex(source);
88    let mut p = Parser::new(&raw_tokens);
89    source_file::source_file(&mut p);
90    let green = p.builder.finish();
91    Parse {
92        green,
93        errors: p.errors,
94    }
95}
96
97/// Parse with a shared [`rowan::NodeCache`] for green-node interning.
98pub fn parse_with_cache(source: &str, cache: &mut rowan::NodeCache) -> Parse {
99    let raw_tokens = lexer::lex(source);
100    let mut p = Parser::with_cache(&raw_tokens, cache);
101    source_file::source_file(&mut p);
102    let green = p.builder.finish();
103    Parse {
104        green,
105        errors: p.errors,
106    }
107}
108
109// ── Parser internals ────────────────────────────────────────────────
110
111/// Maximum nesting depth for recursive grammar rules (blocks, expressions,
112/// parenthesized groups). Prevents stack overflow and superlinear parse
113/// time on pathological/adversarial input. 256 matches Rust's default
114/// `recursion_limit`.
115const MAX_DEPTH: u32 = 256;
116
117/// The parser. Holds a token stream and a `GreenNodeBuilder`.
118pub(crate) struct Parser<'t, 'c> {
119    tokens: &'t [(SyntaxKind, &'t str)],
120    pos: usize,
121    depth: u32,
122    /// Pre-computed non-trivia token indices. `non_trivia[k]` is the raw
123    /// token index of the k-th non-trivia token. Enables O(1) `nth(n)`
124    /// instead of an O(n) rescan per lookahead — this parser calls `nth`
125    /// in hot loops (block/content dispatch), so an un-indexed scan would
126    /// make parsing a large file superlinear.
127    non_trivia: Vec<usize>,
128    builder: rowan::GreenNodeBuilder<'c>,
129    errors: Vec<ParseError>,
130    /// When `true`, a `PATH` followed by `{` is **not** read as a
131    /// `TypeName { … }` construction literal (B5, issue #1464) — the brace
132    /// belongs to the enclosing construct's body instead.
133    ///
134    /// Set only in the four head positions where an expression is directly
135    /// followed by a block opener and the two readings are genuinely
136    /// ambiguous: `if`/`while`/`for … in` heads (`parser::control_flow`)
137    /// and the content-ground `{if …}`/`{match …}` heads
138    /// (`parser::family::conditional_body`). Rust's own
139    /// `no-struct-literal` restriction is the precedent; `(…)`, an
140    /// argument list and a construction literal's own entry list all clear
141    /// it again, so `if (Point { x: 1 }) == p { … }` still parses.
142    no_construct_literal: bool,
143    /// Whether a **cue chain** is currently live at body-item position —
144    /// the prose dialect's chain rule (`docs/prose-dialect-spec.md` §3.1:
145    /// "chain rules (dialogue is the line after a cue/parenthetical)", the
146    /// shipped `brink_ir::dialect` classifier's mechanism promoted to the
147    /// grammar). Set by a cue, carried across that cue's parentheticals
148    /// and dialogue lines, and cleared by a blank line or any other item
149    /// (`dialect.rs`: "blank lines always break a chain").
150    ///
151    /// Only [`element::at_parenthetical`] consults it, and only to decide
152    /// whether a whole-line `( … )` is a parenthetical or an ordinary
153    /// content line — so outside a cue chain the G-1 `(label)` spelling
154    /// (`content::at_content_label`) is reached exactly as before.
155    cue_chain: bool,
156}
157
158impl<'t> Parser<'t, 'static> {
159    fn new(tokens: &'t [(SyntaxKind, &'t str)]) -> Self {
160        let non_trivia = Self::build_non_trivia(tokens);
161        Self {
162            tokens,
163            pos: 0,
164            depth: 0,
165            non_trivia,
166            builder: rowan::GreenNodeBuilder::new(),
167            errors: Vec::new(),
168            no_construct_literal: false,
169            cue_chain: false,
170        }
171    }
172}
173
174impl<'t, 'c> Parser<'t, 'c> {
175    fn with_cache(tokens: &'t [(SyntaxKind, &'t str)], cache: &'c mut rowan::NodeCache) -> Self {
176        let non_trivia = Self::build_non_trivia(tokens);
177        Self {
178            tokens,
179            pos: 0,
180            depth: 0,
181            non_trivia,
182            builder: rowan::GreenNodeBuilder::with_cache(cache),
183            errors: Vec::new(),
184            no_construct_literal: false,
185            cue_chain: false,
186        }
187    }
188
189    /// O(n) pre-pass: collect the raw indices of all non-trivia tokens.
190    fn build_non_trivia(tokens: &[(SyntaxKind, &str)]) -> Vec<usize> {
191        tokens
192            .iter()
193            .enumerate()
194            .filter(|(_, (k, _))| !k.is_trivia())
195            .map(|(i, _)| i)
196            .collect()
197    }
198
199    /// Enter one level of recursive-grammar nesting. Returns `false` (and
200    /// records an error) if `MAX_DEPTH` would be exceeded — callers must
201    /// bail out without recursing further, still consuming forward
202    /// progress via `error_recover`. Every mutually-recursive entry point
203    /// (blocks, the annotated-brace family, expressions) pairs this with
204    /// `exit_depth` so pathological/adversarial nesting can never blow the
205    /// stack (CLAUDE.md: "guard against unbounded growth").
206    fn enter_depth(&mut self) -> bool {
207        if self.depth >= MAX_DEPTH {
208            self.error("maximum nesting depth exceeded".into());
209            false
210        } else {
211            self.depth += 1;
212            true
213        }
214    }
215
216    /// Leave one level entered by `enter_depth`.
217    fn exit_depth(&mut self) {
218        self.depth -= 1;
219    }
220
221    /// Set the [`Self::no_construct_literal`] restriction, returning the
222    /// previous value so the caller can restore it — a save/restore pair
223    /// rather than a plain `= false` reset, so nested heads (`if a { if b
224    /// { … } }`, `{if x: {match y { … }}}`) each unwind to whatever their
225    /// own enclosing context was.
226    fn set_no_construct_literal(&mut self, value: bool) -> bool {
227        std::mem::replace(&mut self.no_construct_literal, value)
228    }
229
230    /// Whether a `PATH` at the current position may be followed by a
231    /// `TypeName { … }` construction literal (see
232    /// [`Self::no_construct_literal`]).
233    fn construct_literals_allowed(&self) -> bool {
234        !self.no_construct_literal
235    }
236
237    /// Set the [`Self::cue_chain`] flag, returning the previous value — a
238    /// save/restore pair like [`Self::set_no_construct_literal`], so a
239    /// nested body can start a fresh chain and unwind to whatever its
240    /// enclosing body was in the middle of.
241    fn set_cue_chain(&mut self, value: bool) -> bool {
242        std::mem::replace(&mut self.cue_chain, value)
243    }
244
245    /// Whether a cue chain is live at the current body-item position (see
246    /// [`Self::cue_chain`]).
247    fn in_cue_chain(&self) -> bool {
248        self.cue_chain
249    }
250
251    // ── Lookahead ───────────────────────────────────────────────
252
253    /// The kind of the current token (or `EOF` if past the end).
254    fn current(&self) -> SyntaxKind {
255        self.nth(0)
256    }
257
258    /// Lookahead by `n` tokens, skipping trivia (WHITESPACE, comments).
259    /// `nth(0)` returns the current non-trivia token.
260    fn nth(&self, n: usize) -> SyntaxKind {
261        let start = self.non_trivia.partition_point(|&idx| idx < self.pos);
262        let target = start + n;
263        if target < self.non_trivia.len() {
264            self.tokens[self.non_trivia[target]].0
265        } else {
266            SyntaxKind::EOF
267        }
268    }
269
270    /// The source text of the `n`-th non-trivia token ahead (`""` past the
271    /// end). The text counterpart of [`Self::nth`], for the handful of
272    /// guards that recognize a *specific word* rather than a token kind —
273    /// today only the prose dialect's `INT.`/`EXT.` scene-heading prefix
274    /// (`parser::element::at_scene_heading`), which is a declared
275    /// line-shape pattern, not a reserved keyword.
276    fn nth_text(&self, n: usize) -> &'t str {
277        let start = self.non_trivia.partition_point(|&idx| idx < self.pos);
278        self.non_trivia
279            .get(start + n)
280            .map_or("", |&idx| self.tokens[idx].1)
281    }
282
283    /// True when the `n`-th and `n+1`-th non-trivia tokens ahead are
284    /// *directly adjacent* in the source — no whitespace or comment
285    /// between them. Used by sigil guards whose spelling is tight by
286    /// construction (`@NAME` — a lone `@` followed by a space stays plain
287    /// prose, per `SyntaxKind::AT`'s doc comment).
288    fn nth_adjacent(&self, n: usize) -> bool {
289        let start = self.non_trivia.partition_point(|&idx| idx < self.pos);
290        match (
291            self.non_trivia.get(start + n),
292            self.non_trivia.get(start + n + 1),
293        ) {
294            (Some(&a), Some(&b)) => b == a + 1,
295            _ => false,
296        }
297    }
298
299    /// Lookahead by `n` tokens WITHOUT skipping trivia.
300    fn nth_raw(&self, n: usize) -> SyntaxKind {
301        self.tokens
302            .get(self.pos + n)
303            .map_or(SyntaxKind::EOF, |&(k, _)| k)
304    }
305
306    /// Returns `true` if the current non-trivia token matches `kind`.
307    fn at(&self, kind: SyntaxKind) -> bool {
308        self.current() == kind
309    }
310
311    /// Returns `true` if we're at end-of-file.
312    fn at_eof(&self) -> bool {
313        self.current() == SyntaxKind::EOF
314    }
315
316    /// Current position in the raw token stream (for loop-progress checks).
317    fn pos(&self) -> usize {
318        self.pos
319    }
320
321    // ── Consumption ─────────────────────────────────────────────
322
323    /// Emit the current token to the builder and advance.
324    fn bump(&mut self) {
325        if self.pos < self.tokens.len() {
326            let (kind, text) = self.tokens[self.pos];
327            self.builder.token(rowan::SyntaxKind(kind as u16), text);
328            self.pos += 1;
329        }
330    }
331
332    /// If the current non-trivia token matches `kind`, eat trivia then bump it.
333    /// Returns `true` if consumed.
334    fn eat(&mut self, kind: SyntaxKind) -> bool {
335        // Flush leading trivia *unconditionally*, before the check — not
336        // only on a match. Two correctness properties depend on this:
337        // (1) trailing trivia with nothing meaningful after it (a final
338        // comment, trailing whitespace at EOF) would otherwise never get
339        // flushed into the tree at all, since every loop-continuation
340        // check (`at_eof`, `at(R_BRACE)`, …) trivia-skips to decide
341        // "nothing left to do" without ever having called `bump` on the
342        // trivia itself — found by `proptest_native`'s
343        // `arbitrary_garbage_never_panics` (`"#//"` lost its trailing
344        // `//`) and `truncated_input_never_panics_and_roundtrips` (a
345        // truncated `flow a_a_() ` lost its trailing space). (2) it makes
346        // every `eat`/`expect` call site safe to follow with a raw
347        // `bump()` for a *different* token regardless of whether pending
348        // trivia sat between them — the class of bug this crate's parser
349        // tests caught repeatedly during development (e.g. `annotation_arg`
350        // bumping a stray space instead of the next `IDENT`).
351        self.skip_ws();
352        if self.current() == kind {
353            self.bump();
354            true
355        } else {
356            false
357        }
358    }
359
360    /// Expect the current non-trivia token to be `kind`. If it is, eat
361    /// trivia and bump. Otherwise, emit an error (no token consumed —
362    /// callers that need forward progress on mismatch should follow up
363    /// with `error_recover`).
364    fn expect(&mut self, kind: SyntaxKind) {
365        if !self.eat(kind) {
366            self.error(format!("expected {kind:?}, found {:?}", self.current()));
367        }
368    }
369
370    /// Consume all trivia (`WHITESPACE`, `LINE_COMMENT`, `BLOCK_COMMENT`).
371    fn skip_ws(&mut self) {
372        while self.pos < self.tokens.len() && self.tokens[self.pos].0.is_trivia() {
373            self.bump();
374        }
375    }
376
377    /// Consume all trivia **and** `NEWLINE` tokens.
378    ///
379    /// `NEWLINE` is deliberately not trivia (it terminates content
380    /// lines/diverts/etc. at body-item position) — but inside an
381    /// explicitly bracket/brace-delimited list (param lists, struct
382    /// fields, annotation args, `use`-tree lists, match arms, …), a line
383    /// break is pure formatting, exactly the case the charter's "whitespace
384    /// never load-bearing" ground rule (§2) describes. Every such list
385    /// loop calls this instead of `skip_ws` so multi-line lists parse.
386    fn skip_ws_and_newlines(&mut self) {
387        while self.pos < self.tokens.len()
388            && (self.tokens[self.pos].0.is_trivia()
389                || self.tokens[self.pos].0 == SyntaxKind::NEWLINE)
390        {
391            self.bump();
392        }
393    }
394
395    /// Look at the next significant token, skipping trivia **and**
396    /// `NEWLINE` (read-only — does not move `pos`). The lookahead half of
397    /// [`Self::skip_ws_and_newlines`]'s policy, for list loops that need to
398    /// check a closing delimiter before deciding whether to recurse.
399    fn peek_skip_nl(&self) -> SyntaxKind {
400        let mut i = self.pos;
401        while i < self.tokens.len()
402            && (self.tokens[i].0.is_trivia() || self.tokens[i].0 == SyntaxKind::NEWLINE)
403        {
404            i += 1;
405        }
406        self.tokens.get(i).map_or(SyntaxKind::EOF, |&(k, _)| k)
407    }
408
409    // ── Nodes ───────────────────────────────────────────────────
410
411    /// Start a new CST node.
412    fn start_node(&mut self, kind: SyntaxKind) {
413        self.builder.start_node(rowan::SyntaxKind(kind as u16));
414    }
415
416    /// Start a new CST node at a previously saved checkpoint.
417    fn start_node_at(&mut self, checkpoint: rowan::Checkpoint, kind: SyntaxKind) {
418        self.builder
419            .start_node_at(checkpoint, rowan::SyntaxKind(kind as u16));
420    }
421
422    /// Finish the current CST node.
423    fn finish_node(&mut self) {
424        self.builder.finish_node();
425    }
426
427    /// Save the current position as a checkpoint for `start_node_at`.
428    fn checkpoint(&self) -> rowan::Checkpoint {
429        self.builder.checkpoint()
430    }
431
432    // ── Errors ──────────────────────────────────────────────────
433
434    /// Record a parse diagnostic at the current position with the given
435    /// severity. Shared implementation for [`Self::error`]/[`Self::warning`].
436    fn push_diagnostic(&mut self, message: String, severity: ParseSeverity) {
437        let upto = self.pos.min(self.tokens.len());
438        let start: usize = self.tokens[..upto].iter().map(|(_, t)| t.len()).sum();
439        let len: usize = self.tokens.get(self.pos).map_or(0, |(_, t)| t.len());
440        let start = rowan::TextSize::from(u32::try_from(start).unwrap_or(u32::MAX));
441        let len = rowan::TextSize::from(u32::try_from(len).unwrap_or(u32::MAX));
442        self.errors.push(ParseError {
443            message,
444            range: rowan::TextRange::at(start, len),
445            severity,
446        });
447    }
448
449    /// Record a parse error at the current position. Blocks compilation
450    /// (`ParseSeverity::Error`).
451    fn error(&mut self, message: String) {
452        self.push_diagnostic(message, ParseSeverity::Error);
453    }
454
455    /// Record a warning-severity diagnostic at the current position.
456    /// Advisory only — never blocks compilation (`ParseSeverity::Warning`).
457    fn warning(&mut self, message: String) {
458        self.push_diagnostic(message, ParseSeverity::Warning);
459    }
460
461    /// Wrap the current token in an `ERROR` node and advance.
462    ///
463    /// Used by grammar rules that need to recover from unexpected tokens
464    /// without losing the rest of the input. Guarantees forward progress
465    /// even at EOF-adjacent malformed input, as long as at least one raw
466    /// token remains — callers at the very top (`source_file`) additionally
467    /// guard against a zero-progress spin when even that isn't true.
468    fn error_recover(&mut self, message: &str) {
469        self.error(message.to_owned());
470        self.start_node(ERROR);
471        if self.pos < self.tokens.len() {
472            self.bump();
473        }
474        self.finish_node();
475    }
476}