Skip to main content

ronin_core/
parser.rs

1//! The RON parser: tokens → a lossless rowan green tree, wrapped in [`CstDocument`].
2//!
3//! Hand-written recursive-descent (AD-006) feeding [`rowan::GreenNodeBuilder`].
4//! It covers the full RON 0.12 value surface (T014): structs (named + anonymous),
5//! tuples, lists/sequences, maps (incl. non-string keys), enum variants, unit
6//! `()`, `Option`/`implicit_some`, and extension attributes.
7//!
8//! # Trivia (AD-001, T015)
9//!
10//! Trivia is attached per the rule documented in [`crate::syntax`]: leading
11//! trivia (whitespace / comments / BOM) binds to the **following** significant
12//! token; trailing trivia at EOF binds to the [`SyntaxKind::Root`] node. The
13//! parser realizes this by, before consuming any significant token, flushing all
14//! pending trivia tokens into the current open node, then any trailing trivia at
15//! EOF into the still-open `Root` node.
16//!
17//! # Losslessness (INV-1/INV-2)
18//!
19//! Every lexer token — significant or trivia — is emitted into the green tree
20//! exactly once, in source order, so concatenating all token texts reproduces
21//! the input byte-for-byte. This holds for valid input **and** for malformed
22//! input that triggers error recovery (INV-3).
23//!
24//! # Error recovery + diagnostics (OBJ2, T021–T024)
25//!
26//! Malformed or incomplete input never panics and never drops bytes. The parser:
27//!
28//! * wraps unexpected tokens in [`SyntaxKind::Error`] nodes and represents absent
29//!   constructs as missing/empty nodes, using recovery sets on `,` `)` `]` `}`
30//!   and field identifiers (T021), so the tree always covers all input (INV-3);
31//! * emits exactly one [`Diagnostic`] per recovery point with a precise byte
32//!   range (T022, TR-006/TR-013) and enforces the must-consume-a-token invariant
33//!   — every loop iteration consumes ≥ 1 token — so parsing always terminates
34//!   (HINT-004);
35//! * enforces a configurable nesting-depth guard (default 128, [`ParseOptions`])
36//!   that stops descent at the limit, emits a [`DiagnosticCode::NestingDepthExceeded`]
37//!   diagnostic, and still tokenizes the remaining bytes into `Error` nodes so no
38//!   stack overflow occurs and byte coverage holds (T023, INV-5);
39//! * is deterministic — identical input yields an identical tree **and** an
40//!   identical diagnostics set (same codes, order, and ranges), since parsing is
41//!   a pure function of the token stream with no nondeterministic inputs (T024,
42//!   INV-6/TR-012).
43
44use rowan::GreenNodeBuilder;
45
46use crate::diagnostics::{Diagnostic, DiagnosticCode};
47use crate::lexer::{self, LexError, Token};
48use crate::syntax::{SyntaxKind, SyntaxNode, TextRange};
49
50/// The default nesting-depth guard (AD-005 / TR-014). Descent past this many
51/// nested composite values stops and emits an over-limit diagnostic instead of
52/// risking a stack overflow.
53pub const DEFAULT_MAX_DEPTH: usize = 128;
54
55/// Configuration for [`parse_with_options`].
56///
57/// Currently carries only the nesting-depth guard (AD-005). It is
58/// `#[non_exhaustive]` so future knobs can be added without a breaking change;
59/// construct it via [`ParseOptions::default`] and adjust fields, or use the
60/// builder-style [`ParseOptions::with_max_depth`].
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62#[non_exhaustive]
63pub struct ParseOptions {
64    /// Maximum nesting depth of composite values before the depth guard trips
65    /// (default [`DEFAULT_MAX_DEPTH`]). A value of `0` means even the top-level
66    /// composite trips the guard.
67    pub max_depth: usize,
68}
69
70impl Default for ParseOptions {
71    #[inline]
72    fn default() -> Self {
73        Self {
74            max_depth: DEFAULT_MAX_DEPTH,
75        }
76    }
77}
78
79impl ParseOptions {
80    /// Builder-style override of [`ParseOptions::max_depth`].
81    #[inline]
82    #[must_use]
83    pub fn with_max_depth(mut self, max_depth: usize) -> Self {
84        self.max_depth = max_depth;
85        self
86    }
87}
88
89/// The parsed, lossless concrete syntax tree of a RON document.
90///
91/// Holds the green root, any diagnostics produced during parsing (empty for
92/// well-formed input; populated by error recovery), and the byte length of the
93/// accepted source. Round-trip identity (INV-2/INV-3): concatenating all token
94/// texts under the root equals the original source bytes, for valid **and**
95/// error-recovered trees.
96#[derive(Clone)]
97pub struct CstDocument {
98    green: rowan::GreenNode,
99    diagnostics: Vec<Diagnostic>,
100    source_len: usize,
101}
102
103impl CstDocument {
104    /// The root [`SyntaxNode`] of the tree.
105    #[must_use]
106    pub fn root(&self) -> SyntaxNode {
107        SyntaxNode::new_root(self.green.clone())
108    }
109
110    /// Diagnostics produced during parsing (empty for well-formed input).
111    ///
112    /// Deterministic (INV-6): identical input yields an identical diagnostics
113    /// set — same codes, order, and byte ranges — including recovery and
114    /// over-limit diagnostics.
115    #[must_use]
116    pub fn diagnostics(&self) -> &[Diagnostic] {
117        &self.diagnostics
118    }
119
120    /// Byte length of the accepted source.
121    #[must_use]
122    pub fn source_len(&self) -> usize {
123        self.source_len
124    }
125
126    /// Build a [`CstDocument`] from a green tree produced by an edit splice
127    /// (crate-internal; see [`crate::edit`]).
128    ///
129    /// The spliced tree carries no diagnostics (the edit produces a fully
130    /// printable tree, INV-8; re-validation is a later-epic concern). `source_len`
131    /// is recomputed from the new tree's total text length so it stays consistent.
132    #[inline]
133    pub(crate) fn from_green_for_edit(green: rowan::GreenNode) -> Self {
134        let source_len = usize::from(green.text_len());
135        Self {
136            green,
137            diagnostics: Vec::new(),
138            source_len,
139        }
140    }
141}
142
143impl std::fmt::Debug for CstDocument {
144    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145        f.debug_struct("CstDocument")
146            .field("root", &self.root())
147            .field("diagnostics", &self.diagnostics)
148            .field("source_len", &self.source_len)
149            .finish()
150    }
151}
152
153/// Parse a UTF-8 `&str` into a lossless [`CstDocument`] with the default
154/// [`ParseOptions`] (depth guard [`DEFAULT_MAX_DEPTH`]). Never panics.
155#[must_use]
156pub fn parse(src: &str) -> CstDocument {
157    parse_with_options(src, ParseOptions::default())
158}
159
160/// Parse a UTF-8 `&str` into a lossless [`CstDocument`] with explicit
161/// [`ParseOptions`] (e.g. a custom nesting-depth guard). Never panics.
162#[must_use]
163pub fn parse_with_options(src: &str, options: ParseOptions) -> CstDocument {
164    let tokens = lexer::tokenize(src);
165    Parser::new(src.len(), tokens, options).parse_document()
166}
167
168/// Parse raw bytes into a [`CstDocument`], rejecting non-UTF-8 cleanly.
169///
170/// Uses the default [`ParseOptions`].
171///
172/// # Errors
173///
174/// Returns [`LexError`] if `bytes` is not valid UTF-8 (TR-001/AD-008/INV-4).
175pub fn parse_bytes(bytes: &[u8]) -> Result<CstDocument, LexError> {
176    let src = lexer::validate_utf8(bytes)?;
177    Ok(parse(src))
178}
179
180/// A token paired with its absolute byte offset into the source, so the parser
181/// can attach precise byte ranges to diagnostics (TR-006) without re-scanning.
182struct Spanned<'a> {
183    tok: Token<'a>,
184    /// Absolute start offset of `tok.text` in the source.
185    start: usize,
186}
187
188struct Parser<'a> {
189    tokens: Vec<Spanned<'a>>,
190    /// Index of the next unconsumed token.
191    cursor: usize,
192    builder: GreenNodeBuilder<'static>,
193    source_len: usize,
194    diagnostics: Vec<Diagnostic>,
195    options: ParseOptions,
196}
197
198impl<'a> Parser<'a> {
199    fn new(source_len: usize, tokens: Vec<Token<'a>>, options: ParseOptions) -> Self {
200        // Precompute absolute offsets once; tokens cover every byte in order
201        // (INV-1), so the running sum is exact.
202        let mut offset = 0usize;
203        let spanned = tokens
204            .into_iter()
205            .map(|tok| {
206                let start = offset;
207                offset += tok.text.len();
208                Spanned { tok, start }
209            })
210            .collect();
211        Self {
212            tokens: spanned,
213            cursor: 0,
214            builder: GreenNodeBuilder::new(),
215            source_len,
216            diagnostics: Vec::new(),
217            options,
218        }
219    }
220
221    fn parse_document(mut self) -> CstDocument {
222        self.builder.start_node(rowan_kind(SyntaxKind::Root));
223
224        // Leading trivia binds to the following token (here: the top-level value
225        // or any extension attributes), so flush it inside Root first.
226        self.eat_trivia();
227
228        // Zero or more extension attributes `#![enable(...)]` at the top.
229        while self.at(SyntaxKind::Hash) {
230            self.parse_extension_attr();
231            self.eat_trivia();
232        }
233
234        // The single top-level value (absent for empty / trivia-only files).
235        if !self.at_eof() {
236            self.parse_value(0);
237        }
238
239        // Trailing trivia at EOF binds to Root (AD-001). Any remaining
240        // significant tokens are stray top-level content: wrap them in Error
241        // nodes (one diagnostic at the first stray token) so the tree always
242        // covers all input (INV-3).
243        self.eat_trivia();
244        if !self.at_eof() {
245            let start = self.current_offset();
246            let end = self.source_len;
247            self.push_diagnostic(
248                DiagnosticCode::UnexpectedToken,
249                TextRange::new(start, end),
250                "unexpected trailing tokens after the top-level value",
251            );
252            while !self.at_eof() {
253                self.bump_into_error();
254                self.eat_trivia();
255            }
256        }
257
258        self.builder.finish_node(); // Root
259        let green = self.builder.finish();
260        CstDocument {
261            green,
262            diagnostics: self.diagnostics,
263            source_len: self.source_len,
264        }
265    }
266
267    // ---- value parsing ---------------------------------------------------
268
269    /// Parse one value at nesting `depth`. Composite values recurse with
270    /// `depth + 1`; the depth guard (T023) trips before descending past
271    /// `options.max_depth`.
272    fn parse_value(&mut self, depth: usize) {
273        self.eat_trivia();
274        let Some(kind) = self.peek_kind() else {
275            return;
276        };
277        match kind {
278            SyntaxKind::LParen | SyntaxKind::LBracket | SyntaxKind::LBrace
279                if depth >= self.options.max_depth =>
280            {
281                // Depth guard (INV-5): stop recursive descent. Still consume the
282                // remaining bytes into Error nodes so coverage/round-trip hold.
283                self.recover_depth_limit();
284            }
285            SyntaxKind::LParen => self.parse_tuple_or_struct(None, depth),
286            SyntaxKind::LBracket => self.parse_list(depth),
287            SyntaxKind::LBrace => self.parse_map(depth),
288            SyntaxKind::Ident => self.parse_ident_led(depth),
289            SyntaxKind::TrueKw | SyntaxKind::FalseKw => self.parse_literal(),
290            SyntaxKind::Integer
291            | SyntaxKind::Float
292            | SyntaxKind::String
293            | SyntaxKind::RawString
294            | SyntaxKind::Char => self.parse_literal(),
295            // Anything else at value position: an unexpected token. Wrap it in an
296            // Error node (keeping the byte) and emit one diagnostic (T022).
297            _ => {
298                let range = self.current_token_range();
299                self.push_diagnostic(DiagnosticCode::UnexpectedToken, range, "expected a value");
300                self.bump_into_error();
301            }
302        }
303    }
304
305    /// A value starting with an identifier: either an enum variant
306    /// (`Ident`, `Ident(...)`, `Ident{...}`) or a named struct (`Name(...)`),
307    /// or a bare ident value. We classify by the following significant token.
308    fn parse_ident_led(&mut self, depth: usize) {
309        // Look past trivia at the token after the ident.
310        let next_sig = self.peek_kind_after_first_significant();
311        match next_sig {
312            Some(SyntaxKind::LParen) => {
313                // `Name( ... )` — named struct/tuple-struct or variant payload.
314                // We model it as a Struct if it contains `field:` entries, else
315                // a Tuple; decided inside parse_tuple_or_struct by lookahead.
316                let name_checkpoint = self.builder.checkpoint();
317                self.bump(); // ident (the name)
318                self.parse_tuple_or_struct(Some(name_checkpoint), depth);
319            }
320            Some(SyntaxKind::LBrace) => {
321                // `Variant { ... }` — struct-like enum variant.
322                self.builder.start_node(rowan_kind(SyntaxKind::EnumVariant));
323                self.bump(); // variant ident
324                self.eat_trivia();
325                self.parse_map_like_braces(depth);
326                self.builder.finish_node();
327            }
328            _ => {
329                // Bare identifier: a unit enum variant / unit struct name / bool
330                // already handled. Wrap as EnumVariant for a single ident value.
331                self.builder.start_node(rowan_kind(SyntaxKind::EnumVariant));
332                self.bump(); // ident
333                self.builder.finish_node();
334            }
335        }
336    }
337
338    /// Parse `( ... )`. If a leading name checkpoint is given, the open node
339    /// wraps the name + parens. The body is classified as a `Struct` when it
340    /// contains `field:` entries, otherwise a positional `Tuple`. Empty `()` is
341    /// a `Unit`.
342    fn parse_tuple_or_struct(&mut self, name_checkpoint: Option<rowan::Checkpoint>, depth: usize) {
343        // Decide the node kind by scanning the body for a `field :` pattern.
344        let is_struct = self.parens_contain_struct_fields();
345
346        let kind = if is_struct {
347            SyntaxKind::Struct
348        } else {
349            // Distinguish `()` unit from a 1+ element tuple.
350            if self.parens_are_empty() {
351                SyntaxKind::Unit
352            } else {
353                SyntaxKind::Tuple
354            }
355        };
356
357        match name_checkpoint {
358            Some(cp) => self.builder.start_node_at(cp, rowan_kind(kind)),
359            None => self.builder.start_node(rowan_kind(kind)),
360        }
361
362        self.eat_trivia();
363        let open = self.current_offset();
364        self.expect_bump(SyntaxKind::LParen);
365        self.eat_trivia();
366
367        while !self.at_eof() && !self.at(SyntaxKind::RParen) {
368            let before = self.cursor;
369            if is_struct {
370                self.parse_struct_field(depth);
371            } else {
372                self.parse_value(depth + 1);
373            }
374            self.eat_trivia();
375            if self.at(SyntaxKind::Comma) {
376                self.bump();
377                self.eat_trivia();
378            } else if self.at(SyntaxKind::RParen) || self.at_eof() {
379                break;
380            } else {
381                // Neither a separator nor a closer: recover by wrapping the stray
382                // token in an Error node so the loop makes progress (HINT-004).
383                self.recover_unexpected_in_group();
384            }
385            // Must-consume-a-token invariant (HINT-004): if an iteration parsed
386            // nothing, force progress to guarantee termination.
387            if self.cursor == before {
388                self.recover_unexpected_in_group();
389            }
390        }
391
392        self.eat_trivia();
393        self.expect_close(SyntaxKind::RParen, open, "(");
394        self.builder.finish_node();
395    }
396
397    fn parse_struct_field(&mut self, depth: usize) {
398        self.builder.start_node(rowan_kind(SyntaxKind::StructField));
399        self.eat_trivia();
400        // field name
401        if self.at(SyntaxKind::Ident) {
402            self.bump();
403        }
404        self.eat_trivia();
405        if self.at(SyntaxKind::Colon) {
406            self.bump();
407        }
408        // Note: a missing `:` here is tolerated silently — RON struct fields
409        // always carry a `:`, but the recovery contract favors covering bytes
410        // over over-reporting; the enclosing loop's progress guard handles
411        // pathological cases.
412        self.eat_trivia();
413        if !self.at(SyntaxKind::Comma) && !self.at(SyntaxKind::RParen) && !self.at_eof() {
414            self.parse_value(depth + 1);
415        }
416        self.builder.finish_node();
417    }
418
419    fn parse_list(&mut self, depth: usize) {
420        self.builder.start_node(rowan_kind(SyntaxKind::List));
421        self.eat_trivia();
422        let open = self.current_offset();
423        self.expect_bump(SyntaxKind::LBracket);
424        self.eat_trivia();
425        while !self.at_eof() && !self.at(SyntaxKind::RBracket) {
426            let before = self.cursor;
427            self.parse_value(depth + 1);
428            self.eat_trivia();
429            if self.at(SyntaxKind::Comma) {
430                self.bump();
431                self.eat_trivia();
432            } else if self.at(SyntaxKind::RBracket) || self.at_eof() {
433                break;
434            } else {
435                self.recover_unexpected_in_group();
436            }
437            if self.cursor == before {
438                self.recover_unexpected_in_group();
439            }
440        }
441        self.eat_trivia();
442        self.expect_close(SyntaxKind::RBracket, open, "[");
443        self.builder.finish_node();
444    }
445
446    fn parse_map(&mut self, depth: usize) {
447        self.builder.start_node(rowan_kind(SyntaxKind::Map));
448        self.parse_map_like_braces(depth);
449        self.builder.finish_node();
450    }
451
452    /// Parse `{ entry, entry, ... }` assuming the `Map`/`EnumVariant` node is
453    /// already open. Consumes the braces and the entries.
454    fn parse_map_like_braces(&mut self, depth: usize) {
455        self.eat_trivia();
456        let open = self.current_offset();
457        self.expect_bump(SyntaxKind::LBrace);
458        self.eat_trivia();
459        while !self.at_eof() && !self.at(SyntaxKind::RBrace) {
460            let before = self.cursor;
461            self.parse_map_entry(depth);
462            self.eat_trivia();
463            if self.at(SyntaxKind::Comma) {
464                self.bump();
465                self.eat_trivia();
466            } else if self.at(SyntaxKind::RBrace) || self.at_eof() {
467                break;
468            } else {
469                self.recover_unexpected_in_group();
470            }
471            if self.cursor == before {
472                self.recover_unexpected_in_group();
473            }
474        }
475        self.eat_trivia();
476        self.expect_close(SyntaxKind::RBrace, open, "{");
477    }
478
479    fn parse_map_entry(&mut self, depth: usize) {
480        self.builder.start_node(rowan_kind(SyntaxKind::MapEntry));
481        self.eat_trivia();
482        // key — any value (incl. non-string keys: numbers, chars, idents, tuples)
483        if !self.at(SyntaxKind::Colon) && !self.at(SyntaxKind::RBrace) && !self.at_eof() {
484            self.parse_value(depth + 1);
485        }
486        self.eat_trivia();
487        if self.at(SyntaxKind::Colon) {
488            self.bump();
489        }
490        self.eat_trivia();
491        if !self.at(SyntaxKind::Comma) && !self.at(SyntaxKind::RBrace) && !self.at_eof() {
492            self.parse_value(depth + 1);
493        }
494        self.builder.finish_node();
495    }
496
497    fn parse_literal(&mut self) {
498        self.builder.start_node(rowan_kind(SyntaxKind::Literal));
499        self.bump(); // the scalar token
500        self.builder.finish_node();
501    }
502
503    fn parse_extension_attr(&mut self) {
504        self.builder
505            .start_node(rowan_kind(SyntaxKind::ExtensionAttr));
506        // `#` `!` `[` enable ( idents... ) `]` — consume verbatim up to the
507        // matching `]`, preserving unknown extensions as text (TR-004).
508        self.expect_bump(SyntaxKind::Hash);
509        self.eat_trivia();
510        if self.at(SyntaxKind::Bang) {
511            self.bump();
512        }
513        self.eat_trivia();
514        if self.at(SyntaxKind::LBracket) {
515            self.bump();
516            self.eat_trivia();
517            // Consume everything up to and including the matching `]`.
518            let mut depth = 1usize;
519            while !self.at_eof() && depth > 0 {
520                match self.peek_kind() {
521                    Some(SyntaxKind::LBracket) => {
522                        depth += 1;
523                        self.bump();
524                    }
525                    Some(SyntaxKind::RBracket) => {
526                        depth -= 1;
527                        self.bump();
528                    }
529                    Some(_) => self.bump(),
530                    None => break,
531                }
532                if depth > 0 {
533                    self.eat_trivia();
534                }
535            }
536        }
537        self.builder.finish_node();
538    }
539
540    // ---- recovery --------------------------------------------------------
541
542    /// Recover from an unexpected token inside a composite group: wrap it in an
543    /// Error node (one diagnostic) and consume it, guaranteeing loop progress
544    /// (HINT-004). Skips over trivia harmlessly first.
545    fn recover_unexpected_in_group(&mut self) {
546        self.eat_trivia();
547        if self.at_eof() {
548            return;
549        }
550        let range = self.current_token_range();
551        self.push_diagnostic(
552            DiagnosticCode::UnexpectedToken,
553            range,
554            "unexpected token in delimited group",
555        );
556        self.bump_into_error();
557    }
558
559    /// Depth-guard recovery (T023/INV-5): emit one over-limit diagnostic spanning
560    /// the remaining input from the offending open delimiter to EOF, then consume
561    /// every remaining token into Error nodes so the tree still covers all bytes
562    /// and round-trips. Tokenization is unaffected — only recursive descent stops.
563    fn recover_depth_limit(&mut self) {
564        self.eat_trivia();
565        let start = self.current_offset();
566        self.push_diagnostic(
567            DiagnosticCode::NestingDepthExceeded,
568            TextRange::new(start, self.source_len),
569            "nesting depth exceeds the configured limit",
570        );
571        // Consume all remaining significant tokens (and interleaved trivia) into
572        // Error nodes. No recursion → no stack growth (INV-5).
573        while !self.at_eof() {
574            self.bump_into_error();
575            self.eat_trivia();
576        }
577    }
578
579    // ---- lookahead helpers ----------------------------------------------
580
581    /// Kind of the token at `cursor` (trivia included), or `None` at EOF.
582    fn peek_kind(&self) -> Option<SyntaxKind> {
583        self.tokens.get(self.cursor).map(|t| t.tok.kind)
584    }
585
586    /// Is the next significant (non-trivia) token of `kind`?
587    fn at(&self, kind: SyntaxKind) -> bool {
588        self.peek_significant() == Some(kind)
589    }
590
591    /// Kind of the next significant token from `cursor`, skipping trivia.
592    fn peek_significant(&self) -> Option<SyntaxKind> {
593        self.tokens[self.cursor..]
594            .iter()
595            .map(|t| t.tok.kind)
596            .find(|k| !k.is_trivia())
597    }
598
599    /// Kind of the second significant token from `cursor` (skipping the first
600    /// significant token and all trivia). Used to classify ident-led values.
601    fn peek_kind_after_first_significant(&self) -> Option<SyntaxKind> {
602        let mut sig_seen = 0;
603        for t in &self.tokens[self.cursor..] {
604            if t.tok.kind.is_trivia() {
605                continue;
606            }
607            sig_seen += 1;
608            if sig_seen == 2 {
609                return Some(t.tok.kind);
610            }
611        }
612        None
613    }
614
615    /// `true` once all significant tokens are consumed.
616    fn at_eof(&self) -> bool {
617        self.peek_significant().is_none()
618    }
619
620    /// Absolute byte offset of the token at `cursor` (or `source_len` at EOF).
621    fn current_offset(&self) -> usize {
622        self.tokens
623            .get(self.cursor)
624            .map_or(self.source_len, |t| t.start)
625    }
626
627    /// Byte range of the next significant token (skipping trivia), or an empty
628    /// range at `source_len` if none remains.
629    fn current_token_range(&self) -> TextRange {
630        for t in &self.tokens[self.cursor..] {
631            if !t.tok.kind.is_trivia() {
632                return TextRange::new(t.start, t.start + t.tok.text.len());
633            }
634        }
635        TextRange::new(self.source_len, self.source_len)
636    }
637
638    /// Does the upcoming `( ... )` group contain a top-level `ident :` pair
639    /// (i.e. is it a struct rather than a positional tuple)? Scans with bracket
640    /// depth tracking; does not consume.
641    fn parens_contain_struct_fields(&self) -> bool {
642        let mut i = self.cursor;
643        // Skip to the opening paren.
644        while i < self.tokens.len() && self.tokens[i].tok.kind != SyntaxKind::LParen {
645            if !self.tokens[i].tok.kind.is_trivia() {
646                // A non-trivia, non-`(` token before the paren means we are not
647                // at a paren group (shouldn't happen given callers).
648                return false;
649            }
650            i += 1;
651        }
652        if i >= self.tokens.len() {
653            return false;
654        }
655        i += 1; // past `(`
656        let mut depth = 1usize;
657        let mut last_significant: Option<SyntaxKind> = None;
658        while i < self.tokens.len() && depth > 0 {
659            let k = self.tokens[i].tok.kind;
660            match k {
661                SyntaxKind::LParen | SyntaxKind::LBracket | SyntaxKind::LBrace => depth += 1,
662                SyntaxKind::RParen | SyntaxKind::RBracket | SyntaxKind::RBrace => depth -= 1,
663                SyntaxKind::Colon
664                    if depth == 1
665                    // A `:` at the top level of this paren group, immediately
666                    // preceded (ignoring trivia) by an ident → struct field.
667                    && last_significant == Some(SyntaxKind::Ident) =>
668                {
669                    return true;
670                }
671                _ => {}
672            }
673            if !k.is_trivia() && depth >= 1 {
674                last_significant = Some(k);
675            }
676            i += 1;
677        }
678        false
679    }
680
681    /// Is the upcoming paren group empty (`()` with only trivia inside)?
682    fn parens_are_empty(&self) -> bool {
683        let mut i = self.cursor;
684        while i < self.tokens.len() && self.tokens[i].tok.kind != SyntaxKind::LParen {
685            i += 1;
686        }
687        if i >= self.tokens.len() {
688            return false;
689        }
690        i += 1; // past `(`
691        while i < self.tokens.len() {
692            let k = self.tokens[i].tok.kind;
693            if k.is_trivia() {
694                i += 1;
695                continue;
696            }
697            return k == SyntaxKind::RParen;
698        }
699        false
700    }
701
702    // ---- token consumption ----------------------------------------------
703
704    /// Emit all leading trivia tokens at `cursor` into the current open node
705    /// (AD-001: leading trivia binds to the following token).
706    fn eat_trivia(&mut self) {
707        while let Some(spanned) = self.tokens.get(self.cursor) {
708            if spanned.tok.kind.is_trivia() {
709                self.builder
710                    .token(rowan_kind(spanned.tok.kind), spanned.tok.text);
711                self.cursor += 1;
712            } else {
713                break;
714            }
715        }
716    }
717
718    /// Consume one significant token, emitting any preceding trivia first.
719    fn bump(&mut self) {
720        self.eat_trivia();
721        if let Some(spanned) = self.tokens.get(self.cursor) {
722            self.builder
723                .token(rowan_kind(spanned.tok.kind), spanned.tok.text);
724            self.cursor += 1;
725        }
726    }
727
728    /// Consume one token (significant or not) wrapped in an `Error` node, so
729    /// unexpected input still lands in the tree (INV-1/INV-3). Trivia is flushed
730    /// outside the error node to keep error spans tight.
731    fn bump_into_error(&mut self) {
732        self.eat_trivia();
733        if let Some(spanned) = self.tokens.get(self.cursor) {
734            self.builder.start_node(rowan_kind(SyntaxKind::Error));
735            self.builder
736                .token(rowan_kind(spanned.tok.kind), spanned.tok.text);
737            self.builder.finish_node();
738            self.cursor += 1;
739        }
740    }
741
742    /// Consume the expected significant token if present (no-op + lossless if
743    /// absent). The missing-delimiter diagnostic is handled by callers that know
744    /// the open-delimiter span (see [`Parser::expect_close`]).
745    fn expect_bump(&mut self, kind: SyntaxKind) {
746        if self.at(kind) {
747            self.bump();
748        }
749    }
750
751    /// Consume the expected closing delimiter if present; otherwise emit a single
752    /// [`DiagnosticCode::UnclosedDelimiter`] diagnostic spanning from the opening
753    /// delimiter to the current position (T022/TR-006). The tree's byte coverage
754    /// is unaffected — the close is simply synthesized as missing.
755    fn expect_close(&mut self, kind: SyntaxKind, open_offset: usize, open: &str) {
756        if self.at(kind) {
757            self.bump();
758        } else {
759            let end = self.current_offset();
760            self.push_diagnostic(
761                DiagnosticCode::UnclosedDelimiter,
762                TextRange::new(open_offset, end),
763                format!("unclosed delimiter `{open}`"),
764            );
765        }
766    }
767
768    /// Record one diagnostic. Centralized so every recovery point goes through a
769    /// single path (one-per-recovery-point, deterministic order; T022/T024).
770    fn push_diagnostic(
771        &mut self,
772        code: DiagnosticCode,
773        range: TextRange,
774        message: impl Into<String>,
775    ) {
776        debug_assert!(
777            range.start() <= range.end() && range.end() <= self.source_len,
778            "diagnostic range must lie within [0, source_len)"
779        );
780        self.diagnostics.push(Diagnostic::new(code, range, message));
781    }
782}
783
784#[inline]
785fn rowan_kind(kind: SyntaxKind) -> rowan::SyntaxKind {
786    <crate::syntax::kind::RonLang as rowan::Language>::kind_to_raw(kind)
787}
788
789#[cfg(test)]
790mod tests {
791    use super::*;
792    use crate::diagnostics::Severity;
793
794    /// Round-trip helper: concatenate all token texts of the parsed tree.
795    fn roundtrip(src: &str) -> String {
796        let doc = parse(src);
797        doc.root()
798            .descendant_tokens()
799            .map(|t| t.text().to_string())
800            .collect()
801    }
802
803    #[test]
804    fn roundtrip_covers_all_constructs() {
805        let inputs = [
806            "",
807            "   \n\t",
808            "// comment only\n",
809            "/* block */",
810            "42",
811            "-3.14",
812            "true",
813            "false",
814            "'c'",
815            "\"hello\\nworld\"",
816            "r#\"raw \"q\" str\"#",
817            "()",
818            "Unit",
819            "Some(42)",
820            "Foo(x: 1, y: 2.0)",
821            "(1, 2, 3)",
822            "[1, 2, 3,]",
823            "{ \"a\": 1, \"b\": 2, }",
824            "{ 1: \"one\", 'c': true }",
825            "Point(x: 1.0, y: -2.0)",
826            "Enum::A", // `::` is unknown to RON; still must round-trip
827            "Variant { field: 1 }",
828            "#![enable(implicit_some)]\nSome(5)",
829            "#![enable(unwrap_newtypes)]\n#![enable(implicit_some)]\n[1, 2]",
830            "\u{FEFF}42",
831            "1\r\n2\r\n",
832            "  Foo(  a : [ 1 , 2 ] , b : { 'x' : 'y' } )  // trailing\n",
833        ];
834        for src in inputs {
835            assert_eq!(roundtrip(src), src, "round-trip failed for {src:?}");
836        }
837    }
838
839    /// Well-formed input produces zero diagnostics.
840    #[test]
841    fn valid_input_has_no_diagnostics() {
842        for src in [
843            "Foo(x: 1, y: 2.0)",
844            "[1, 2, 3,]",
845            "{ \"a\": 1 }",
846            "Some(())",
847            "#![enable(implicit_some)]\nSome(5)",
848        ] {
849            assert!(
850                parse(src).diagnostics().is_empty(),
851                "unexpected diagnostics for {src:?}: {:?}",
852                parse(src).diagnostics()
853            );
854        }
855    }
856
857    #[test]
858    fn parse_bytes_rejects_non_utf8() {
859        let bad = [0xFFu8, 0x00];
860        assert!(parse_bytes(&bad).is_err());
861    }
862
863    #[test]
864    fn parse_bytes_accepts_bom() {
865        let doc = parse_bytes("\u{FEFF}1".as_bytes()).unwrap();
866        let printed: String = doc
867            .root()
868            .descendant_tokens()
869            .map(|t| t.text().to_string())
870            .collect();
871        assert_eq!(printed, "\u{FEFF}1");
872    }
873
874    #[test]
875    fn source_len_matches() {
876        let src = "Foo(x: 1)";
877        let doc = parse(src);
878        assert_eq!(doc.source_len(), src.len());
879    }
880
881    #[test]
882    fn struct_vs_tuple_classification() {
883        let s = parse("Foo(x: 1)");
884        let has_struct = s.root().descendant_tokens().count() > 0
885            && s.root().children().any(|n| n.kind() == SyntaxKind::Struct);
886        assert!(has_struct, "named struct should produce a Struct node");
887
888        let t = parse("(1, 2)");
889        let has_tuple = t.root().children().any(|n| n.kind() == SyntaxKind::Tuple);
890        assert!(has_tuple, "positional parens should produce a Tuple node");
891
892        let u = parse("()");
893        let has_unit = u.root().children().any(|n| n.kind() == SyntaxKind::Unit);
894        assert!(has_unit, "empty parens should produce a Unit node");
895    }
896
897    // ---- OBJ2: diagnostic contract (T025) -------------------------------
898
899    /// Every diagnostic's byte range must lie within `[0, source_len]` and be
900    /// well-ordered (TR-006/SC-004). Checks across a malformed-sample set.
901    #[test]
902    fn diagnostic_ranges_are_within_source() {
903        for src in [
904            "[1, 2",           // unclosed list
905            "Foo(x: 1",        // unclosed struct
906            "{ \"a\": 1",      // unclosed map
907            "@",               // stray top-level token (lex error)
908            "[1 @ 2]",         // stray token inside a list
909            "1 2 3",           // stray trailing tokens
910            "Foo(x: 1) extra", // trailing content after value
911        ] {
912            let doc = parse(src);
913            for d in doc.diagnostics() {
914                assert!(
915                    d.range().start() <= d.range().end(),
916                    "range ordered for {src:?}"
917                );
918                assert!(
919                    d.range().end() <= doc.source_len(),
920                    "range within source for {src:?}: {:?} (len {})",
921                    d.range(),
922                    doc.source_len()
923                );
924            }
925        }
926    }
927
928    /// Recovery diagnostics carry the expected severity and registry code.
929    #[test]
930    fn recovery_diagnostic_codes_and_severity() {
931        let unclosed = parse("[1, 2");
932        assert!(unclosed
933            .diagnostics()
934            .iter()
935            .any(|d| d.code() == DiagnosticCode::UnclosedDelimiter
936                && d.severity() == Severity::Error));
937
938        let stray = parse("@");
939        assert!(stray.diagnostics().iter().any(
940            |d| d.code() == DiagnosticCode::UnexpectedToken && d.severity() == Severity::Error
941        ));
942    }
943
944    /// One diagnostic per recovery point: a single unclosed delimiter yields
945    /// exactly one diagnostic.
946    #[test]
947    fn one_diagnostic_per_recovery_point() {
948        let doc = parse("[1, 2");
949        let unclosed: Vec<_> = doc
950            .diagnostics()
951            .iter()
952            .filter(|d| d.code() == DiagnosticCode::UnclosedDelimiter)
953            .collect();
954        assert_eq!(
955            unclosed.len(),
956            1,
957            "exactly one unclosed-delimiter diagnostic"
958        );
959
960        // A single stray token yields a single unexpected-token diagnostic.
961        let stray = parse("@");
962        assert_eq!(stray.diagnostics().len(), 1);
963        assert_eq!(
964            stray.diagnostics()[0].code(),
965            DiagnosticCode::UnexpectedToken
966        );
967    }
968
969    // ---- OBJ2: error-node coverage (T026) -------------------------------
970
971    /// Malformed input still round-trips byte-for-byte (INV-3) and never panics.
972    #[test]
973    fn malformed_input_roundtrips() {
974        for src in [
975            "[1, 2",
976            "Foo(x: 1",
977            "{ \"a\": 1",
978            "Some(",
979            "(((",
980            "}]) ",
981            "@#$%",
982            "Foo(x: 1) trailing garbage",
983            "[1 2 3]",    // missing commas
984            "{a 1, b 2}", // missing colons
985            "[1, [2, [3", // nested unclosed
986        ] {
987            assert_eq!(
988                roundtrip(src),
989                src,
990                "malformed round-trip failed for {src:?}"
991            );
992        }
993    }
994
995    /// Malformed input produces at least one `Error` node somewhere in the tree
996    /// (recovery completeness).
997    #[test]
998    fn malformed_input_has_error_nodes() {
999        let doc = parse("@ stray");
1000        let has_error = doc
1001            .root()
1002            .descendant_tokens()
1003            .any(|t| t.parent().map(|p| p.kind()) == Some(SyntaxKind::Error));
1004        assert!(
1005            has_error,
1006            "expected an Error node for stray top-level tokens"
1007        );
1008    }
1009
1010    // ---- OBJ2: determinism (T024) ---------------------------------------
1011
1012    /// Identical input yields identical trees and identical diagnostics.
1013    #[test]
1014    fn parsing_is_deterministic() {
1015        for src in ["Foo(x: 1, y: [2, 3", "@ junk ] ) }", "{a: 1, b: 2,"] {
1016            let a = parse(src);
1017            let b = parse(src);
1018            // Same printed tree.
1019            let ta: String = a
1020                .root()
1021                .descendant_tokens()
1022                .map(|t| t.text().to_string())
1023                .collect();
1024            let tb: String = b
1025                .root()
1026                .descendant_tokens()
1027                .map(|t| t.text().to_string())
1028                .collect();
1029            assert_eq!(ta, tb);
1030            // Same diagnostics (codes, order, ranges).
1031            assert_eq!(
1032                a.diagnostics(),
1033                b.diagnostics(),
1034                "diagnostics differ for {src:?}"
1035            );
1036        }
1037    }
1038
1039    // ---- OBJ2: depth guard (T027 companion; full test in tests/) --------
1040
1041    /// At depth bound+1 the guard trips: no overflow, an over-limit diagnostic is
1042    /// emitted, and the tree still round-trips.
1043    #[test]
1044    fn depth_guard_trips_at_bound_plus_one() {
1045        let depth = 5usize;
1046        let opts = ParseOptions::default().with_max_depth(depth);
1047        // depth+1 nested lists.
1048        let src = format!("{}{}", "[".repeat(depth + 1), "]".repeat(depth + 1));
1049        let doc = parse_with_options(&src, opts);
1050        let printed: String = doc
1051            .root()
1052            .descendant_tokens()
1053            .map(|t| t.text().to_string())
1054            .collect();
1055        assert_eq!(printed, src, "depth-limited tree must round-trip");
1056        assert!(
1057            doc.diagnostics()
1058                .iter()
1059                .any(|d| d.code() == DiagnosticCode::NestingDepthExceeded),
1060            "expected an over-limit diagnostic"
1061        );
1062    }
1063
1064    /// Below the bound, no over-limit diagnostic is emitted.
1065    #[test]
1066    fn depth_guard_silent_below_bound() {
1067        let opts = ParseOptions::default().with_max_depth(10);
1068        let src = "[[[[1]]]]";
1069        let doc = parse_with_options(src, opts);
1070        assert!(!doc
1071            .diagnostics()
1072            .iter()
1073            .any(|d| d.code() == DiagnosticCode::NestingDepthExceeded));
1074    }
1075}