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