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;
12
13use crate::SyntaxKind::{self, COLON, EOF, ERROR, IDENT, L_BRACE, NEWLINE, PIPE, R_BRACE};
14use crate::lexer;
15use rowan::GreenNode;
16
17/// Result of parsing an Ink source file.
18pub struct Parse {
19    green: GreenNode,
20    errors: Vec<ParseError>,
21}
22
23impl Parse {
24    /// The root green node of the lossless CST.
25    #[must_use]
26    pub fn green(&self) -> &GreenNode {
27        &self.green
28    }
29
30    /// The root syntax node (typed wrapper around the green tree).
31    #[must_use]
32    pub fn syntax(&self) -> crate::SyntaxNode {
33        crate::SyntaxNode::new_root(self.green.clone())
34    }
35
36    /// Parse errors encountered.
37    #[must_use]
38    pub fn errors(&self) -> &[ParseError] {
39        &self.errors
40    }
41}
42
43/// A parse error with a message and the source range it points at.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct ParseError {
46    pub message: String,
47    /// Byte range in the source that the error points at.
48    pub range: rowan::TextRange,
49}
50
51/// Parse an Ink source string into a lossless CST.
52#[must_use]
53pub fn parse(source: &str) -> Parse {
54    let raw_tokens = lexer::lex(source);
55    let mut p = Parser::new(&raw_tokens);
56    story::source_file(&mut p);
57    let green = p.builder.finish();
58    Parse {
59        green,
60        errors: p.errors,
61    }
62}
63
64/// Parse with a shared [`rowan::NodeCache`] for green-node interning.
65///
66/// Re-parsing the same source through the same cache produces structurally
67/// identical subtrees that share the same `Arc` allocation, enabling O(1)
68/// pointer-equality checks via `GreenNode::eq`.
69pub fn parse_with_cache(source: &str, cache: &mut rowan::NodeCache) -> Parse {
70    let raw_tokens = lexer::lex(source);
71    let mut p = Parser::with_cache(&raw_tokens, cache);
72    story::source_file(&mut p);
73    let green = p.builder.finish();
74    Parse {
75        green,
76        errors: p.errors,
77    }
78}
79
80// ── Parser internals ────────────────────────────────────────────────
81
82/// Maximum nesting depth for recursive grammar rules (inline logic, expressions,
83/// parenthesized groups). Prevents stack overflow and superlinear parse time on
84/// pathological input. 256 matches Rust's default `recursion_limit`.
85const MAX_DEPTH: u32 = 256;
86
87/// The parser. Holds a token stream and a `GreenNodeBuilder`.
88pub(crate) struct Parser<'t, 'c> {
89    tokens: &'t [(SyntaxKind, &'t str)],
90    pos: usize,
91    depth: u32,
92    /// Pre-computed scan results for each `{` token. Indexed by raw token
93    /// position. For positions that are not `L_BRACE`, the value is meaningless.
94    /// For `L_BRACE` positions, stores `PIPE`, `COLON`, or `EOF` indicating
95    /// which delimiter appears first at depth-0 inside that brace pair.
96    brace_scan: Vec<SyntaxKind>,
97    /// Pre-computed non-trivia token indices. `non_trivia[k]` is the raw
98    /// token index of the k-th non-trivia token. Enables O(1) `nth(n)`.
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 brace_scan = Self::build_brace_scan(tokens);
107        let non_trivia = Self::build_non_trivia(tokens);
108        Self {
109            tokens,
110            pos: 0,
111            depth: 0,
112            brace_scan,
113            non_trivia,
114            builder: rowan::GreenNodeBuilder::new(),
115            errors: Vec::new(),
116        }
117    }
118}
119
120impl<'t, 'c> Parser<'t, 'c> {
121    fn with_cache(tokens: &'t [(SyntaxKind, &'t str)], cache: &'c mut rowan::NodeCache) -> Self {
122        let brace_scan = Self::build_brace_scan(tokens);
123        let non_trivia = Self::build_non_trivia(tokens);
124        Self {
125            tokens,
126            pos: 0,
127            depth: 0,
128            brace_scan,
129            non_trivia,
130            builder: rowan::GreenNodeBuilder::with_cache(cache),
131            errors: Vec::new(),
132        }
133    }
134
135    /// O(n) pre-pass: collect the raw indices of all non-trivia tokens.
136    /// Enables O(1) `nth(n)` lookup during parsing.
137    fn build_non_trivia(tokens: &[(SyntaxKind, &str)]) -> Vec<usize> {
138        tokens
139            .iter()
140            .enumerate()
141            .filter(|(_, (k, _))| !k.is_trivia())
142            .map(|(i, _)| i)
143            .collect()
144    }
145
146    /// O(n) pre-pass: for each `L_BRACE`, classify the brace pair as `COLON`
147    /// (conditional), `PIPE` (sequence), or `EOF` (bare expression).
148    ///
149    /// Classification rules (`||`-aware):
150    ///  1. If a **single** `|` (not part of `||`) appears at depth-0 →
151    ///     sequence (`PIPE`), regardless of any COLON.
152    ///  2. Else if `COLON` appears at depth-0 → conditional (`COLON`).
153    ///  3. Else if `||` appears (no single `|`, no COLON) → sequence (`PIPE`),
154    ///     since `||` without a conditional colon means two separators.
155    ///  4. Neither → bare expression (`EOF`).
156    ///
157    /// Examples:
158    ///  - `{a|b:c}` — single `|` → sequence (rule 1)
159    ///  - `{x || y: body}` — no single `|`, has COLON → conditional (rule 2)
160    ///  - `{a||b}` — `||` only, no COLON → sequence (rule 3)
161    ///  - `{x}` — neither → bare expression (rule 4)
162    fn build_brace_scan(tokens: &[(SyntaxKind, &str)]) -> Vec<SyntaxKind> {
163        // Stack entries track what we've seen at depth-0 inside each brace pair.
164        // `single_pipe_before_colon` is the key signal: a lone `|` that appears
165        // before any `:` means this brace pair is a sequence, not a conditional
166        // (the `|` is a separator, not part of a conditional body like `{x: a|b}`).
167        struct Entry {
168            brace_pos: usize,
169            has_colon: bool,
170            has_pipe: bool,
171            single_pipe_before_colon: bool,
172        }
173
174        fn classify(e: &Entry) -> SyntaxKind {
175            if e.single_pipe_before_colon {
176                PIPE // rule 1: single `|` before `:` → sequence
177            } else if e.has_colon {
178                COLON // rule 2: colon (with only `||` or no pipe before it) → conditional
179            } else if e.has_pipe {
180                PIPE // rule 3: `||` without colon → sequence separators
181            } else {
182                EOF // rule 4: bare expression
183            }
184        }
185
186        let n = tokens.len();
187        let mut result = vec![EOF; n];
188
189        // Precompute: for each token position, the next non-trivia token index.
190        let next_nt = {
191            let mut v = vec![n; n];
192            let mut last = n;
193            for i in (0..n).rev() {
194                v[i] = last;
195                if !tokens[i].0.is_trivia() {
196                    last = i;
197                }
198            }
199            v
200        };
201
202        let mut stack: Vec<Entry> = Vec::new();
203        let mut prev_nt = EOF;
204
205        for (i, &(kind, _)) in tokens.iter().enumerate() {
206            if kind.is_trivia() {
207                continue;
208            }
209            match kind {
210                L_BRACE => {
211                    stack.push(Entry {
212                        brace_pos: i,
213                        has_colon: false,
214                        has_pipe: false,
215                        single_pipe_before_colon: false,
216                    });
217                    prev_nt = L_BRACE;
218                }
219                R_BRACE => {
220                    if let Some(entry) = stack.pop() {
221                        result[entry.brace_pos] = classify(&entry);
222                    }
223                    prev_nt = R_BRACE;
224                }
225                COLON => {
226                    if let Some(e) = stack.last_mut() {
227                        e.has_colon = true;
228                    }
229                    prev_nt = COLON;
230                }
231                PIPE => {
232                    if let Some(e) = stack.last_mut() {
233                        e.has_pipe = true;
234                        // Determine if this is a single `|` (not part of `||`).
235                        let next_is_pipe = next_nt[i] < n && tokens[next_nt[i]].0 == PIPE;
236                        let prev_is_pipe = prev_nt == PIPE;
237                        let is_single = !next_is_pipe && !prev_is_pipe;
238                        // Only matters if we haven't seen COLON yet.
239                        if is_single && !e.has_colon {
240                            e.single_pipe_before_colon = true;
241                        }
242                    }
243                    prev_nt = PIPE;
244                }
245                NEWLINE => {
246                    while let Some(entry) = stack.pop() {
247                        result[entry.brace_pos] = classify(&entry);
248                    }
249                    prev_nt = NEWLINE;
250                }
251                _ => {
252                    prev_nt = kind;
253                }
254            }
255        }
256
257        for entry in stack {
258            result[entry.brace_pos] = classify(&entry);
259        }
260
261        result
262    }
263
264    /// Returns `true` if the nesting depth limit has been reached.
265    fn at_depth_limit(&self) -> bool {
266        self.depth >= MAX_DEPTH
267    }
268
269    /// Look up the pre-computed scan result for a `{` token at the given raw
270    /// position. Returns `PIPE`, `COLON`, or `EOF`.
271    fn brace_scan_at(&self, raw_pos: usize) -> SyntaxKind {
272        self.brace_scan.get(raw_pos).copied().unwrap_or(EOF)
273    }
274
275    // ── Lookahead ───────────────────────────────────────────────
276
277    /// The kind of the current token (or `EOF` if past the end).
278    fn current(&self) -> SyntaxKind {
279        self.nth(0)
280    }
281
282    /// Lookahead by `n` tokens, skipping trivia (WHITESPACE, comments).
283    /// `nth(0)` returns the current non-trivia token.
284    ///
285    /// Uses the pre-computed `non_trivia` index for O(log n + 1) lookup
286    /// (binary search to find our position, then constant-time indexing).
287    fn nth(&self, n: usize) -> SyntaxKind {
288        // Find the first non-trivia index >= self.pos via binary search.
289        let start = self.non_trivia.partition_point(|&idx| idx < self.pos);
290        let target = start + n;
291        if target < self.non_trivia.len() {
292            self.tokens[self.non_trivia[target]].0
293        } else {
294            EOF
295        }
296    }
297
298    /// Lookahead by `n` tokens WITHOUT skipping trivia.
299    fn nth_raw(&self, n: usize) -> SyntaxKind {
300        self.tokens.get(self.pos + n).map_or(EOF, |&(k, _)| k)
301    }
302
303    /// Returns `true` if the current non-trivia token matches `kind`.
304    fn at(&self, kind: SyntaxKind) -> bool {
305        self.current() == kind
306    }
307
308    /// Returns `true` if we're at end-of-file.
309    fn at_eof(&self) -> bool {
310        self.pos >= self.tokens.len()
311    }
312
313    /// Current position in the token stream (for loop-progress checks).
314    fn pos(&self) -> usize {
315        self.pos
316    }
317
318    // ── Consumption ─────────────────────────────────────────────
319
320    /// Emit the current token to the builder and advance.
321    fn bump(&mut self) {
322        if self.pos < self.tokens.len() {
323            let (kind, text) = self.tokens[self.pos];
324            self.builder.token(rowan::SyntaxKind(kind as u16), text);
325            self.pos += 1;
326        }
327    }
328
329    /// Bump the current token, asserting its kind matches `kind`.
330    fn bump_assert(&mut self, kind: SyntaxKind) {
331        debug_assert_eq!(self.nth_raw(0), kind);
332        self.bump();
333    }
334
335    /// If the current non-trivia token matches `kind`, eat trivia then bump it.
336    /// Returns `true` if consumed.
337    fn eat(&mut self, kind: SyntaxKind) -> bool {
338        if self.current() == kind {
339            self.skip_ws();
340            self.bump();
341            true
342        } else {
343            false
344        }
345    }
346
347    /// Expect the current non-trivia token to be `kind`. If it is, eat trivia
348    /// and bump. Otherwise, emit an error.
349    fn expect(&mut self, kind: SyntaxKind) {
350        if !self.eat(kind) {
351            self.error(format!("expected {kind:?}"));
352        }
353    }
354
355    /// Returns `true` if the current non-trivia token is `IDENT` or a keyword.
356    ///
357    /// Ink keywords are contextual — they may appear as identifiers in some
358    /// positions (e.g. list member names like `or`, `and`, `not`).
359    fn at_ident_or_keyword(&self) -> bool {
360        self.current() == IDENT || self.current().is_keyword()
361    }
362
363    /// If the current non-trivia token is `IDENT` or a keyword, eat trivia
364    /// then bump it. Returns `true` if consumed.
365    fn eat_ident_or_keyword(&mut self) -> bool {
366        if self.at_ident_or_keyword() {
367            self.skip_ws();
368            self.bump();
369            true
370        } else {
371            false
372        }
373    }
374
375    /// Expect the current non-trivia token to be `IDENT` or a keyword.
376    /// If not, emit an error.
377    fn expect_ident_or_keyword(&mut self) {
378        if !self.eat_ident_or_keyword() {
379            self.error("expected IDENT".into());
380        }
381    }
382
383    /// Consume all trivia (`WHITESPACE`, `LINE_COMMENT`, `BLOCK_COMMENT`).
384    fn skip_ws(&mut self) {
385        while self.pos < self.tokens.len() && self.tokens[self.pos].0.is_trivia() {
386            self.bump();
387        }
388    }
389
390    // ── Nodes ───────────────────────────────────────────────────
391
392    /// Start a new CST node.
393    fn start_node(&mut self, kind: SyntaxKind) {
394        self.builder.start_node(rowan::SyntaxKind(kind as u16));
395    }
396
397    /// Start a new CST node at a previously saved checkpoint.
398    fn start_node_at(&mut self, checkpoint: rowan::Checkpoint, kind: SyntaxKind) {
399        self.builder
400            .start_node_at(checkpoint, rowan::SyntaxKind(kind as u16));
401    }
402
403    /// Finish the current CST node.
404    fn finish_node(&mut self) {
405        self.builder.finish_node();
406    }
407
408    /// Save the current position as a checkpoint for `start_node_at`.
409    fn checkpoint(&self) -> rowan::Checkpoint {
410        self.builder.checkpoint()
411    }
412
413    // ── Errors ──────────────────────────────────────────────────
414
415    /// Record a parse error at the current position.
416    fn error(&mut self, message: String) {
417        // Byte offset of the current token = total length of all preceding
418        // tokens (the lexer emits contiguous tokens covering the whole source).
419        let upto = self.pos.min(self.tokens.len());
420        let start: usize = self.tokens[..upto].iter().map(|(_, t)| t.len()).sum();
421        let len: usize = self.tokens.get(self.pos).map_or(0, |(_, t)| t.len());
422        let start = rowan::TextSize::from(u32::try_from(start).unwrap_or(u32::MAX));
423        let len = rowan::TextSize::from(u32::try_from(len).unwrap_or(u32::MAX));
424        self.errors.push(ParseError {
425            message,
426            range: rowan::TextRange::at(start, len),
427        });
428    }
429
430    /// Wrap the current token in an `ERROR` node and advance.
431    ///
432    /// Used by grammar rules that need to recover from unexpected tokens
433    /// without losing the rest of the input.
434    fn error_recover(&mut self, message: &str) {
435        self.error(message.to_owned());
436        self.start_node(ERROR);
437        self.bump();
438        self.finish_node();
439    }
440}
441
442#[cfg(test)]
443mod tests;