Skip to main content

brink_syntax/parser/
mod.rs

1mod choice;
2mod content;
3mod declaration;
4mod divert;
5mod expression;
6mod gather;
7mod inline;
8mod knot;
9mod logic;
10mod story;
11mod tag;
12mod types;
13
14use crate::SyntaxKind::{
15    self, BLOCK_COMMENT, COLON, EOF, ERROR, IDENT, L_BRACE, LINE_COMMENT, NEWLINE, PIPE, R_BRACE,
16};
17// `IDENT` above is also used by `at_kw_text`/`nth_text` (soft-keyword lookup).
18use crate::lexer;
19use rowan::GreenNode;
20
21/// Result of parsing an Ink source file.
22///
23/// `PartialEq` compares the green tree structurally (rowan `GreenNode`
24/// equality is content-based) plus the error list — used by the salsa
25/// `parse` query in `brink-db` for early-cutoff backdating.
26#[derive(Clone, PartialEq, Eq)]
27pub struct Parse {
28    green: GreenNode,
29    errors: Vec<ParseError>,
30}
31
32impl Parse {
33    /// The root green node of the lossless CST.
34    #[must_use]
35    pub fn green(&self) -> &GreenNode {
36        &self.green
37    }
38
39    /// The root syntax node (typed wrapper around the green tree).
40    #[must_use]
41    pub fn syntax(&self) -> crate::SyntaxNode {
42        crate::SyntaxNode::new_root(self.green.clone())
43    }
44
45    /// Parse errors encountered.
46    #[must_use]
47    pub fn errors(&self) -> &[ParseError] {
48        &self.errors
49    }
50}
51
52/// A parse error with a message and the source range it points at.
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct ParseError {
55    pub message: String,
56    /// Byte range in the source that the error points at.
57    pub range: rowan::TextRange,
58}
59
60/// Parse an Ink source string into a lossless CST.
61#[must_use]
62pub fn parse(source: &str) -> Parse {
63    let raw_tokens = lexer::lex(source);
64    let mut p = Parser::new(&raw_tokens);
65    story::source_file(&mut p);
66    let green = p.builder.finish();
67    Parse {
68        green,
69        errors: p.errors,
70    }
71}
72
73/// Parse with a shared [`rowan::NodeCache`] for green-node interning.
74///
75/// Re-parsing the same source through the same cache produces structurally
76/// identical subtrees that share the same `Arc` allocation, enabling O(1)
77/// pointer-equality checks via `GreenNode::eq`.
78pub fn parse_with_cache(source: &str, cache: &mut rowan::NodeCache) -> Parse {
79    let raw_tokens = lexer::lex(source);
80    let mut p = Parser::with_cache(&raw_tokens, cache);
81    story::source_file(&mut p);
82    let green = p.builder.finish();
83    Parse {
84        green,
85        errors: p.errors,
86    }
87}
88
89// ── Parser internals ────────────────────────────────────────────────
90
91/// Maximum nesting depth for recursive grammar rules (inline logic, expressions,
92/// parenthesized groups). Prevents stack overflow and superlinear parse time on
93/// pathological input. 256 matches Rust's default `recursion_limit`.
94const MAX_DEPTH: u32 = 256;
95
96/// The parser. Holds a token stream and a `GreenNodeBuilder`.
97pub(crate) struct Parser<'t, 'c> {
98    tokens: &'t [(SyntaxKind, &'t str)],
99    pos: usize,
100    depth: u32,
101    /// Pre-computed scan results for each `{` token. Indexed by raw token
102    /// position. For positions that are not `L_BRACE`, the value is meaningless.
103    /// For `L_BRACE` positions, stores `PIPE`, `COLON`, or `EOF` indicating
104    /// which delimiter appears first at depth-0 inside that brace pair.
105    brace_scan: Vec<SyntaxKind>,
106    /// Pre-computed non-trivia token indices. `non_trivia[k]` is the raw
107    /// token index of the k-th non-trivia token. Enables O(1) `nth(n)`.
108    non_trivia: Vec<usize>,
109    builder: rowan::GreenNodeBuilder<'c>,
110    errors: Vec<ParseError>,
111}
112
113impl<'t> Parser<'t, 'static> {
114    fn new(tokens: &'t [(SyntaxKind, &'t str)]) -> Self {
115        let brace_scan = Self::build_brace_scan(tokens);
116        let non_trivia = Self::build_non_trivia(tokens);
117        Self {
118            tokens,
119            pos: 0,
120            depth: 0,
121            brace_scan,
122            non_trivia,
123            builder: rowan::GreenNodeBuilder::new(),
124            errors: Vec::new(),
125        }
126    }
127}
128
129impl<'t, 'c> Parser<'t, 'c> {
130    fn with_cache(tokens: &'t [(SyntaxKind, &'t str)], cache: &'c mut rowan::NodeCache) -> Self {
131        let brace_scan = Self::build_brace_scan(tokens);
132        let non_trivia = Self::build_non_trivia(tokens);
133        Self {
134            tokens,
135            pos: 0,
136            depth: 0,
137            brace_scan,
138            non_trivia,
139            builder: rowan::GreenNodeBuilder::with_cache(cache),
140            errors: Vec::new(),
141        }
142    }
143
144    /// O(n) pre-pass: collect the raw indices of all non-trivia tokens.
145    /// Enables O(1) `nth(n)` lookup during parsing.
146    fn build_non_trivia(tokens: &[(SyntaxKind, &str)]) -> Vec<usize> {
147        tokens
148            .iter()
149            .enumerate()
150            .filter(|(_, (k, _))| !k.is_trivia())
151            .map(|(i, _)| i)
152            .collect()
153    }
154
155    /// O(n) pre-pass: for each `L_BRACE`, classify the brace pair as `COLON`
156    /// (conditional), `PIPE` (sequence), or `EOF` (bare expression).
157    ///
158    /// Classification rules (`||`-aware):
159    ///  1. If a **single** `|` (not part of `||`) appears at depth-0 →
160    ///     sequence (`PIPE`), regardless of any COLON.
161    ///  2. Else if `COLON` appears at depth-0 → conditional (`COLON`).
162    ///  3. Else if `||` appears (no single `|`, no COLON) → sequence (`PIPE`),
163    ///     since `||` without a conditional colon means two separators.
164    ///  4. Neither → bare expression (`EOF`).
165    ///
166    /// Examples:
167    ///  - `{a|b:c}` — single `|` → sequence (rule 1)
168    ///  - `{x || y: body}` — no single `|`, has COLON → conditional (rule 2)
169    ///  - `{a||b}` — `||` only, no COLON → sequence (rule 3)
170    ///  - `{x}` — neither → bare expression (rule 4)
171    fn build_brace_scan(tokens: &[(SyntaxKind, &str)]) -> Vec<SyntaxKind> {
172        // Stack entries track what we've seen at depth-0 inside each brace pair.
173        // `single_pipe_before_colon` is the key signal: a lone `|` that appears
174        // before any `:` means this brace pair is a sequence, not a conditional
175        // (the `|` is a separator, not part of a conditional body like `{x: a|b}`).
176        struct Entry {
177            brace_pos: usize,
178            has_colon: bool,
179            has_pipe: bool,
180            single_pipe_before_colon: bool,
181        }
182
183        fn classify(e: &Entry) -> SyntaxKind {
184            if e.single_pipe_before_colon {
185                PIPE // rule 1: single `|` before `:` → sequence
186            } else if e.has_colon {
187                COLON // rule 2: colon (with only `||` or no pipe before it) → conditional
188            } else if e.has_pipe {
189                PIPE // rule 3: `||` without colon → sequence separators
190            } else {
191                EOF // rule 4: bare expression
192            }
193        }
194
195        let n = tokens.len();
196        let mut result = vec![EOF; n];
197
198        // Precompute: for each token position, the next non-trivia token index.
199        let next_nt = {
200            let mut v = vec![n; n];
201            let mut last = n;
202            for i in (0..n).rev() {
203                v[i] = last;
204                if !tokens[i].0.is_trivia() {
205                    last = i;
206                }
207            }
208            v
209        };
210
211        let mut stack: Vec<Entry> = Vec::new();
212        let mut prev_nt = EOF;
213
214        for (i, &(kind, _)) in tokens.iter().enumerate() {
215            if kind.is_trivia() {
216                continue;
217            }
218            match kind {
219                L_BRACE => {
220                    stack.push(Entry {
221                        brace_pos: i,
222                        has_colon: false,
223                        has_pipe: false,
224                        single_pipe_before_colon: false,
225                    });
226                    prev_nt = L_BRACE;
227                }
228                R_BRACE => {
229                    if let Some(entry) = stack.pop() {
230                        result[entry.brace_pos] = classify(&entry);
231                    }
232                    prev_nt = R_BRACE;
233                }
234                COLON => {
235                    if let Some(e) = stack.last_mut() {
236                        e.has_colon = true;
237                    }
238                    prev_nt = COLON;
239                }
240                PIPE => {
241                    if let Some(e) = stack.last_mut() {
242                        e.has_pipe = true;
243                        // Determine if this is a single `|` (not part of `||`).
244                        let next_is_pipe = next_nt[i] < n && tokens[next_nt[i]].0 == PIPE;
245                        let prev_is_pipe = prev_nt == PIPE;
246                        let is_single = !next_is_pipe && !prev_is_pipe;
247                        // Only matters if we haven't seen COLON yet.
248                        if is_single && !e.has_colon {
249                            e.single_pipe_before_colon = true;
250                        }
251                    }
252                    prev_nt = PIPE;
253                }
254                NEWLINE => {
255                    while let Some(entry) = stack.pop() {
256                        result[entry.brace_pos] = classify(&entry);
257                    }
258                    prev_nt = NEWLINE;
259                }
260                _ => {
261                    prev_nt = kind;
262                }
263            }
264        }
265
266        for entry in stack {
267            result[entry.brace_pos] = classify(&entry);
268        }
269
270        result
271    }
272
273    /// Returns `true` if the nesting depth limit has been reached.
274    fn at_depth_limit(&self) -> bool {
275        self.depth >= MAX_DEPTH
276    }
277
278    /// Look up the pre-computed scan result for a `{` token at the given raw
279    /// position. Returns `PIPE`, `COLON`, or `EOF`.
280    fn brace_scan_at(&self, raw_pos: usize) -> SyntaxKind {
281        self.brace_scan.get(raw_pos).copied().unwrap_or(EOF)
282    }
283
284    // ── Lookahead ───────────────────────────────────────────────
285
286    /// The kind of the current token (or `EOF` if past the end).
287    fn current(&self) -> SyntaxKind {
288        self.nth(0)
289    }
290
291    /// Lookahead by `n` tokens, skipping trivia (WHITESPACE, comments).
292    /// `nth(0)` returns the current non-trivia token.
293    ///
294    /// Uses the pre-computed `non_trivia` index for O(log n + 1) lookup
295    /// (binary search to find our position, then constant-time indexing).
296    fn nth(&self, n: usize) -> SyntaxKind {
297        // Find the first non-trivia index >= self.pos via binary search.
298        let start = self.non_trivia.partition_point(|&idx| idx < self.pos);
299        let target = start + n;
300        if target < self.non_trivia.len() {
301            self.tokens[self.non_trivia[target]].0
302        } else {
303            EOF
304        }
305    }
306
307    /// Lookahead by `n` tokens WITHOUT skipping trivia.
308    fn nth_raw(&self, n: usize) -> SyntaxKind {
309        self.tokens.get(self.pos + n).map_or(EOF, |&(k, _)| k)
310    }
311
312    /// The source text of the `n`-th non-trivia token ahead (skipping
313    /// trivia), or `""` past end-of-file. Used to recognize T1b's
314    /// contextual block keywords (`if`, `while`, `for`, `in`, `break`,
315    /// `continue`) without reserving them globally — they stay plain
316    /// `IDENT` tokens everywhere outside a `~ { … }` block, so existing ink
317    /// content using those words as identifiers is byte-for-byte unaffected.
318    fn nth_text(&self, n: usize) -> &'t str {
319        let start = self.non_trivia.partition_point(|&idx| idx < self.pos);
320        let target = start + n;
321        if target < self.non_trivia.len() {
322            self.tokens[self.non_trivia[target]].1
323        } else {
324            ""
325        }
326    }
327
328    /// Returns `true` if the current token is `IDENT` with exactly this text
329    /// — a contextual (soft) keyword check. See [`Parser::nth_text`].
330    fn at_kw_text(&self, text: &str) -> bool {
331        self.current() == IDENT && self.nth_text(0) == text
332    }
333
334    /// Returns `true` if the current non-trivia token matches `kind`.
335    fn at(&self, kind: SyntaxKind) -> bool {
336        self.current() == kind
337    }
338
339    /// Returns `true` if we're at end-of-file.
340    fn at_eof(&self) -> bool {
341        self.pos >= self.tokens.len()
342    }
343
344    /// Current position in the token stream (for loop-progress checks).
345    fn pos(&self) -> usize {
346        self.pos
347    }
348
349    // ── Consumption ─────────────────────────────────────────────
350
351    /// Emit the current token to the builder and advance.
352    fn bump(&mut self) {
353        if self.pos < self.tokens.len() {
354            let (kind, text) = self.tokens[self.pos];
355            self.builder.token(rowan::SyntaxKind(kind as u16), text);
356            self.pos += 1;
357        }
358    }
359
360    /// Bump the current token, which every call site's own dispatch has
361    /// already checked (via `nth`) to be `kind`.
362    ///
363    /// That check can be invalidated by trivia a caller forgot to flush
364    /// with `skip_ws()` before dispatching: `nth`/`current` skip trivia for
365    /// lookahead, but `self.pos` — and so this raw `bump()` — does not move
366    /// until something actually consumes it. A mismatch here is therefore a
367    /// parser bug, not a property of the input, but malformed/adversarial
368    /// input must never turn a parser bug into a panic. Degrade to a parse
369    /// error and still consume one token, preserving the forward-progress
370    /// invariant every caller relies on (no `bump_assert` call site may
371    /// become a stuck point that only the top-level `source_file` recovery
372    /// loop can un-stick).
373    fn bump_assert(&mut self, kind: SyntaxKind) {
374        if self.nth_raw(0) != kind {
375            self.error(format!("expected {kind:?}"));
376        }
377        self.bump();
378    }
379
380    /// If the current non-trivia token matches `kind`, eat trivia then bump it.
381    /// Returns `true` if consumed.
382    fn eat(&mut self, kind: SyntaxKind) -> bool {
383        if self.current() == kind {
384            self.skip_ws();
385            self.bump();
386            true
387        } else {
388            false
389        }
390    }
391
392    /// Expect the current non-trivia token to be `kind`. If it is, eat trivia
393    /// and bump. Otherwise, emit an error.
394    fn expect(&mut self, kind: SyntaxKind) {
395        if !self.eat(kind) {
396            self.error(format!("expected {kind:?}"));
397        }
398    }
399
400    /// Returns `true` if the current non-trivia token is `IDENT` or a keyword.
401    ///
402    /// Ink keywords are contextual — they may appear as identifiers in some
403    /// positions (e.g. list member names like `or`, `and`, `not`).
404    fn at_ident_or_keyword(&self) -> bool {
405        self.current() == IDENT || self.current().is_keyword()
406    }
407
408    /// If the current non-trivia token is `IDENT` or a keyword, eat trivia
409    /// then bump it. Returns `true` if consumed.
410    fn eat_ident_or_keyword(&mut self) -> bool {
411        if self.at_ident_or_keyword() {
412            self.skip_ws();
413            self.bump();
414            true
415        } else {
416            false
417        }
418    }
419
420    /// Expect the current non-trivia token to be `IDENT` or a keyword.
421    /// If not, emit an error.
422    fn expect_ident_or_keyword(&mut self) {
423        if !self.eat_ident_or_keyword() {
424            self.error("expected IDENT".into());
425        }
426    }
427
428    /// Consume all trivia (`WHITESPACE`, `LINE_COMMENT`, `BLOCK_COMMENT`).
429    fn skip_ws(&mut self) {
430        while self.pos < self.tokens.len() && self.tokens[self.pos].0.is_trivia() {
431            self.bump();
432        }
433    }
434
435    /// Consume a run of comment tokens (`LINE_COMMENT`, `BLOCK_COMMENT`) at
436    /// the raw position, WITHOUT consuming adjacent `WHITESPACE`.
437    ///
438    /// Used by `content::mixed_content`'s zero-progress recovery: a mid-line
439    /// comment is elided from the output, but the whitespace touching it on
440    /// either side is real content that must survive so the surrounding
441    /// `TEXT` runs fold back together correctly. Unlike `skip_ws` (which
442    /// treats `WHITESPACE` and comments as one contiguous trivia blob and
443    /// would swallow that whitespace too), this stops the instant it sees
444    /// anything that isn't a comment token — including `WHITESPACE` — so a
445    /// caller can retry `text_content` and pick that whitespace back up as
446    /// ordinary text. Confirmed against inklecate's own output: the
447    /// `astrochili__narrator` corpus's `Before comment ... /* A comment */
448    /// ... and after.` compiles to `Before comment ...  ... and after.`
449    /// (the double space from both sides' whitespace surviving, only the
450    /// comment span itself removed) — see
451    /// `tests/tests_github/astrochili__narrator/test/units/comments.ink.json`.
452    fn skip_comment_tokens(&mut self) {
453        while matches!(self.nth_raw(0), LINE_COMMENT | BLOCK_COMMENT) {
454            self.bump();
455        }
456    }
457
458    // ── Nodes ───────────────────────────────────────────────────
459
460    /// Start a new CST node.
461    fn start_node(&mut self, kind: SyntaxKind) {
462        self.builder.start_node(rowan::SyntaxKind(kind as u16));
463    }
464
465    /// Start a new CST node at a previously saved checkpoint.
466    fn start_node_at(&mut self, checkpoint: rowan::Checkpoint, kind: SyntaxKind) {
467        self.builder
468            .start_node_at(checkpoint, rowan::SyntaxKind(kind as u16));
469    }
470
471    /// Finish the current CST node.
472    fn finish_node(&mut self) {
473        self.builder.finish_node();
474    }
475
476    /// Save the current position as a checkpoint for `start_node_at`.
477    fn checkpoint(&self) -> rowan::Checkpoint {
478        self.builder.checkpoint()
479    }
480
481    // ── Errors ──────────────────────────────────────────────────
482
483    /// Record a parse error at the current position.
484    fn error(&mut self, message: String) {
485        // Byte offset of the current token = total length of all preceding
486        // tokens (the lexer emits contiguous tokens covering the whole source).
487        let upto = self.pos.min(self.tokens.len());
488        let start: usize = self.tokens[..upto].iter().map(|(_, t)| t.len()).sum();
489        let len: usize = self.tokens.get(self.pos).map_or(0, |(_, t)| t.len());
490        let start = rowan::TextSize::from(u32::try_from(start).unwrap_or(u32::MAX));
491        let len = rowan::TextSize::from(u32::try_from(len).unwrap_or(u32::MAX));
492        self.errors.push(ParseError {
493            message,
494            range: rowan::TextRange::at(start, len),
495        });
496    }
497
498    /// Wrap the current token in an `ERROR` node and advance.
499    ///
500    /// Used by grammar rules that need to recover from unexpected tokens
501    /// without losing the rest of the input.
502    fn error_recover(&mut self, message: &str) {
503        self.error(message.to_owned());
504        self.start_node(ERROR);
505        self.bump();
506        self.finish_node();
507    }
508}
509
510#[cfg(test)]
511mod tests;