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}