Skip to main content

brink_syntax_native/parser/
mod.rs

1mod annotation;
2mod block;
3mod choice;
4mod content;
5mod decl;
6mod divert;
7mod doc_comment;
8mod expr;
9mod family;
10mod source_file;
11#[cfg(test)]
12mod tests;
13
14use crate::SyntaxKind::{self, ERROR};
15use crate::lexer;
16use rowan::GreenNode;
17
18/// Result of parsing a `.brink` source file.
19///
20/// `PartialEq` compares the green tree structurally (rowan `GreenNode`
21/// equality is content-based) plus the error list.
22#[derive(Clone, PartialEq, Eq)]
23pub struct Parse {
24    green: GreenNode,
25    errors: Vec<ParseError>,
26}
27
28impl Parse {
29    /// The root green node of the lossless CST.
30    #[must_use]
31    pub fn green(&self) -> &GreenNode {
32        &self.green
33    }
34
35    /// The root syntax node (typed wrapper around the green tree).
36    #[must_use]
37    pub fn syntax(&self) -> crate::SyntaxNode {
38        crate::SyntaxNode::new_root(self.green.clone())
39    }
40
41    /// Parse errors encountered.
42    #[must_use]
43    pub fn errors(&self) -> &[ParseError] {
44        &self.errors
45    }
46}
47
48/// A parse error with a message and the source range it points at.
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct ParseError {
51    pub message: String,
52    /// Byte range in the source that the error points at.
53    pub range: rowan::TextRange,
54}
55
56/// Parse a `.brink` source string into a lossless CST.
57#[must_use]
58pub fn parse(source: &str) -> Parse {
59    let raw_tokens = lexer::lex(source);
60    let mut p = Parser::new(&raw_tokens);
61    source_file::source_file(&mut p);
62    let green = p.builder.finish();
63    Parse {
64        green,
65        errors: p.errors,
66    }
67}
68
69/// Parse with a shared [`rowan::NodeCache`] for green-node interning.
70pub fn parse_with_cache(source: &str, cache: &mut rowan::NodeCache) -> Parse {
71    let raw_tokens = lexer::lex(source);
72    let mut p = Parser::with_cache(&raw_tokens, cache);
73    source_file::source_file(&mut p);
74    let green = p.builder.finish();
75    Parse {
76        green,
77        errors: p.errors,
78    }
79}
80
81// ── Parser internals ────────────────────────────────────────────────
82
83/// Maximum nesting depth for recursive grammar rules (blocks, expressions,
84/// parenthesized groups). Prevents stack overflow and superlinear parse
85/// time on pathological/adversarial input. 256 matches Rust's default
86/// `recursion_limit`.
87const MAX_DEPTH: u32 = 256;
88
89/// The parser. Holds a token stream and a `GreenNodeBuilder`.
90pub(crate) struct Parser<'t, 'c> {
91    tokens: &'t [(SyntaxKind, &'t str)],
92    pos: usize,
93    depth: u32,
94    /// Pre-computed non-trivia token indices. `non_trivia[k]` is the raw
95    /// token index of the k-th non-trivia token. Enables O(1) `nth(n)`
96    /// instead of an O(n) rescan per lookahead — this parser calls `nth`
97    /// in hot loops (block/content dispatch), so an un-indexed scan would
98    /// make parsing a large file superlinear.
99    non_trivia: Vec<usize>,
100    builder: rowan::GreenNodeBuilder<'c>,
101    errors: Vec<ParseError>,
102}
103
104impl<'t> Parser<'t, 'static> {
105    fn new(tokens: &'t [(SyntaxKind, &'t str)]) -> Self {
106        let non_trivia = Self::build_non_trivia(tokens);
107        Self {
108            tokens,
109            pos: 0,
110            depth: 0,
111            non_trivia,
112            builder: rowan::GreenNodeBuilder::new(),
113            errors: Vec::new(),
114        }
115    }
116}
117
118impl<'t, 'c> Parser<'t, 'c> {
119    fn with_cache(tokens: &'t [(SyntaxKind, &'t str)], cache: &'c mut rowan::NodeCache) -> Self {
120        let non_trivia = Self::build_non_trivia(tokens);
121        Self {
122            tokens,
123            pos: 0,
124            depth: 0,
125            non_trivia,
126            builder: rowan::GreenNodeBuilder::with_cache(cache),
127            errors: Vec::new(),
128        }
129    }
130
131    /// O(n) pre-pass: collect the raw indices of all non-trivia tokens.
132    fn build_non_trivia(tokens: &[(SyntaxKind, &str)]) -> Vec<usize> {
133        tokens
134            .iter()
135            .enumerate()
136            .filter(|(_, (k, _))| !k.is_trivia())
137            .map(|(i, _)| i)
138            .collect()
139    }
140
141    /// Enter one level of recursive-grammar nesting. Returns `false` (and
142    /// records an error) if `MAX_DEPTH` would be exceeded — callers must
143    /// bail out without recursing further, still consuming forward
144    /// progress via `error_recover`. Every mutually-recursive entry point
145    /// (blocks, the annotated-brace family, expressions) pairs this with
146    /// `exit_depth` so pathological/adversarial nesting can never blow the
147    /// stack (CLAUDE.md: "guard against unbounded growth").
148    fn enter_depth(&mut self) -> bool {
149        if self.depth >= MAX_DEPTH {
150            self.error("maximum nesting depth exceeded".into());
151            false
152        } else {
153            self.depth += 1;
154            true
155        }
156    }
157
158    /// Leave one level entered by `enter_depth`.
159    fn exit_depth(&mut self) {
160        self.depth -= 1;
161    }
162
163    // ── Lookahead ───────────────────────────────────────────────
164
165    /// The kind of the current token (or `EOF` if past the end).
166    fn current(&self) -> SyntaxKind {
167        self.nth(0)
168    }
169
170    /// Lookahead by `n` tokens, skipping trivia (WHITESPACE, comments).
171    /// `nth(0)` returns the current non-trivia token.
172    fn nth(&self, n: usize) -> SyntaxKind {
173        let start = self.non_trivia.partition_point(|&idx| idx < self.pos);
174        let target = start + n;
175        if target < self.non_trivia.len() {
176            self.tokens[self.non_trivia[target]].0
177        } else {
178            SyntaxKind::EOF
179        }
180    }
181
182    /// Lookahead by `n` tokens WITHOUT skipping trivia.
183    fn nth_raw(&self, n: usize) -> SyntaxKind {
184        self.tokens
185            .get(self.pos + n)
186            .map_or(SyntaxKind::EOF, |&(k, _)| k)
187    }
188
189    /// Returns `true` if the current non-trivia token matches `kind`.
190    fn at(&self, kind: SyntaxKind) -> bool {
191        self.current() == kind
192    }
193
194    /// Returns `true` if we're at end-of-file.
195    fn at_eof(&self) -> bool {
196        self.current() == SyntaxKind::EOF
197    }
198
199    /// Current position in the raw token stream (for loop-progress checks).
200    fn pos(&self) -> usize {
201        self.pos
202    }
203
204    // ── Consumption ─────────────────────────────────────────────
205
206    /// Emit the current token to the builder and advance.
207    fn bump(&mut self) {
208        if self.pos < self.tokens.len() {
209            let (kind, text) = self.tokens[self.pos];
210            self.builder.token(rowan::SyntaxKind(kind as u16), text);
211            self.pos += 1;
212        }
213    }
214
215    /// If the current non-trivia token matches `kind`, eat trivia then bump it.
216    /// Returns `true` if consumed.
217    fn eat(&mut self, kind: SyntaxKind) -> bool {
218        // Flush leading trivia *unconditionally*, before the check — not
219        // only on a match. Two correctness properties depend on this:
220        // (1) trailing trivia with nothing meaningful after it (a final
221        // comment, trailing whitespace at EOF) would otherwise never get
222        // flushed into the tree at all, since every loop-continuation
223        // check (`at_eof`, `at(R_BRACE)`, …) trivia-skips to decide
224        // "nothing left to do" without ever having called `bump` on the
225        // trivia itself — found by `proptest_native`'s
226        // `arbitrary_garbage_never_panics` (`"#//"` lost its trailing
227        // `//`) and `truncated_input_never_panics_and_roundtrips` (a
228        // truncated `flow a_a_() ` lost its trailing space). (2) it makes
229        // every `eat`/`expect` call site safe to follow with a raw
230        // `bump()` for a *different* token regardless of whether pending
231        // trivia sat between them — the class of bug this crate's parser
232        // tests caught repeatedly during development (e.g. `annotation_arg`
233        // bumping a stray space instead of the next `IDENT`).
234        self.skip_ws();
235        if self.current() == kind {
236            self.bump();
237            true
238        } else {
239            false
240        }
241    }
242
243    /// Expect the current non-trivia token to be `kind`. If it is, eat
244    /// trivia and bump. Otherwise, emit an error (no token consumed —
245    /// callers that need forward progress on mismatch should follow up
246    /// with `error_recover`).
247    fn expect(&mut self, kind: SyntaxKind) {
248        if !self.eat(kind) {
249            self.error(format!("expected {kind:?}, found {:?}", self.current()));
250        }
251    }
252
253    /// Consume all trivia (`WHITESPACE`, `LINE_COMMENT`, `BLOCK_COMMENT`).
254    fn skip_ws(&mut self) {
255        while self.pos < self.tokens.len() && self.tokens[self.pos].0.is_trivia() {
256            self.bump();
257        }
258    }
259
260    /// Consume all trivia **and** `NEWLINE` tokens.
261    ///
262    /// `NEWLINE` is deliberately not trivia (it terminates content
263    /// lines/diverts/etc. at body-item position) — but inside an
264    /// explicitly bracket/brace-delimited list (param lists, struct
265    /// fields, annotation args, `use`-tree lists, match arms, …), a line
266    /// break is pure formatting, exactly the case the charter's "whitespace
267    /// never load-bearing" ground rule (§2) describes. Every such list
268    /// loop calls this instead of `skip_ws` so multi-line lists parse.
269    fn skip_ws_and_newlines(&mut self) {
270        while self.pos < self.tokens.len()
271            && (self.tokens[self.pos].0.is_trivia()
272                || self.tokens[self.pos].0 == SyntaxKind::NEWLINE)
273        {
274            self.bump();
275        }
276    }
277
278    /// Look at the next significant token, skipping trivia **and**
279    /// `NEWLINE` (read-only — does not move `pos`). The lookahead half of
280    /// [`Self::skip_ws_and_newlines`]'s policy, for list loops that need to
281    /// check a closing delimiter before deciding whether to recurse.
282    fn peek_skip_nl(&self) -> SyntaxKind {
283        let mut i = self.pos;
284        while i < self.tokens.len()
285            && (self.tokens[i].0.is_trivia() || self.tokens[i].0 == SyntaxKind::NEWLINE)
286        {
287            i += 1;
288        }
289        self.tokens.get(i).map_or(SyntaxKind::EOF, |&(k, _)| k)
290    }
291
292    // ── Nodes ───────────────────────────────────────────────────
293
294    /// Start a new CST node.
295    fn start_node(&mut self, kind: SyntaxKind) {
296        self.builder.start_node(rowan::SyntaxKind(kind as u16));
297    }
298
299    /// Start a new CST node at a previously saved checkpoint.
300    fn start_node_at(&mut self, checkpoint: rowan::Checkpoint, kind: SyntaxKind) {
301        self.builder
302            .start_node_at(checkpoint, rowan::SyntaxKind(kind as u16));
303    }
304
305    /// Finish the current CST node.
306    fn finish_node(&mut self) {
307        self.builder.finish_node();
308    }
309
310    /// Save the current position as a checkpoint for `start_node_at`.
311    fn checkpoint(&self) -> rowan::Checkpoint {
312        self.builder.checkpoint()
313    }
314
315    // ── Errors ──────────────────────────────────────────────────
316
317    /// Record a parse error at the current position.
318    fn error(&mut self, message: String) {
319        let upto = self.pos.min(self.tokens.len());
320        let start: usize = self.tokens[..upto].iter().map(|(_, t)| t.len()).sum();
321        let len: usize = self.tokens.get(self.pos).map_or(0, |(_, t)| t.len());
322        let start = rowan::TextSize::from(u32::try_from(start).unwrap_or(u32::MAX));
323        let len = rowan::TextSize::from(u32::try_from(len).unwrap_or(u32::MAX));
324        self.errors.push(ParseError {
325            message,
326            range: rowan::TextRange::at(start, len),
327        });
328    }
329
330    /// Wrap the current token in an `ERROR` node and advance.
331    ///
332    /// Used by grammar rules that need to recover from unexpected tokens
333    /// without losing the rest of the input. Guarantees forward progress
334    /// even at EOF-adjacent malformed input, as long as at least one raw
335    /// token remains — callers at the very top (`source_file`) additionally
336    /// guard against a zero-progress spin when even that isn't true.
337    fn error_recover(&mut self, message: &str) {
338        self.error(message.to_owned());
339        self.start_node(ERROR);
340        if self.pos < self.tokens.len() {
341            self.bump();
342        }
343        self.finish_node();
344    }
345}