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;