Skip to main content

ridl_syntax/
parser.rs

1//! The hand-written recursive-descent parser for the typl grammar
2//! (ADR-0004 §2, typl reference Appendix E).
3//!
4//! The parser consumes the flat token stream from [`crate::lex`] and builds a
5//! lossless rowan tree whose nodes match `family.ungram` exactly: every token —
6//! including whitespace and comments — lands in the tree in source order, so
7//! [`Parse::syntax`] round-trips back to the original source for both valid
8//! and broken input. On an unexpected token the parser records a
9//! [`SyntaxError`] and wraps the skipped tokens in an
10//! [`ErrorNode`](SyntaxKind::ErrorNode) rather than dropping them, so recovery
11//! never loses text and never panics.
12//!
13//! Trivia placement: leading trivia — including doc comments — is flushed at
14//! the parent level before a child node starts, so a doc comment sits as a
15//! sibling immediately before the definition it documents
16//! (`ast::HasDocComments` reads it from there).
17//!
18//! # The `[a..b]` bound policy
19//!
20//! `[3..6]` has two legal tree shapes: a pair of scalar `Literal` children
21//! (a value range) or a `Bound` child (a length bound). The parser emits a
22//! `Bound` iff the constraint attaches to a `string`/`bytes` backing and both
23//! endpoints are integer-shaped (no float token); it emits scalar `Literal`s
24//! otherwise — including when a `step` clause follows, which only the scalar
25//! shape can carry. A single literal with no `..` (`[17]`) is always a
26//! `Bound`; a float endpoint is always scalar.
27//!
28//! # Grammar over-approximations
29//!
30//! The grammar deliberately accepts more than the typl reference allows —
31//! constraints on any primitive in field position, arbitrary map key types,
32//! identifier literals in value positions, `step` on open ranges, non-integer
33//! literals in bounds and reserved entries, repeated separator commas with no
34//! member between them (`,,,`), and the `internal`/`error` modifiers on every
35//! definition kind. The checker narrows these later (TYPL-2xx); the parser
36//! must not reject them.
37//!
38//! The interaction grammar follows the same discipline: a `: return_type`
39//! after a command's params parses; timing parses on command, query, and
40//! fixed; an attr block parses on signal, event, and fixed; an init value
41//! parses on event and fixed; a stream `<T>` parses in every type position,
42//! including signal/event payloads and struct fields; a typl definition inside
43//! an interface body whose `}` still lies ahead recovers into one body-local
44//! `ErrorNode` and draws **RIDL-107** here, where the keyword is recognised,
45//! while a body with no `}` ahead reports an unclosed `{` and keeps the
46//! declarations that follow it. The other rejections are checker scope (RIDL-104/-106/-201/-301).
47//!
48//! # Profile boundary
49//!
50//! The parser runs under a [`Profile`] (ADR-0007 decision 10). In a `.typl`
51//! parse, a `Duration` token or a stray `@` anywhere emits **TYPL-302** (typl
52//! reference §2.8) and parsing continues, an interaction keyword at
53//! declaration-start position emits **TYPL-304**, and a `require`/`ensure`
54//! attribute emits **TYPL-303** with the same recovery. In a `.ridl` parse durations and `@` are ordinary tokens,
55//! and a `ReservedWord` at declaration-start position — a word of the
56//! rmdl/rsdl profiles, or a ridl clause word — emits **RIDL-403** (ridl
57//! reference §16.4). In a
58//! `.rsdl` parse a declaration of another profile at declaration-start position
59//! — a typl definition, `interface`, `service`, or any other `ReservedWord` —
60//! emits **RSDL-604** (rsdl reference §2), and an `internal` or `error`
61//! modifier before an rsdl declaration emits FORM-102. The typl and ridl
62//! declaration-start boundaries recover exactly as FORM-105 does; the rsdl one
63//! consumes a `{ … }` body whole. The stream grammar parses under both
64//! profiles: a `<T>` in type position builds a `StreamType` node everywhere,
65//! and in a `.typl` parse it additionally emits **TYPL-301** (`stream type in
66//! typl context`) and parsing continues. Leading zeros in an integer literal
67//! emit **FORM-005**. Every [`SyntaxError`] carries its diagnostic code; the
68//! coded `Diagnostic` model consumes it.
69//!
70//! # Error recovery
71//!
72//! A broken declaration does not derail the rest of the file. When a loop
73//! meets a token it cannot place, it reports one diagnostic and skips forward,
74//! wrapping the skipped run in a single [`ErrorNode`](SyntaxKind::ErrorNode),
75//! until it reaches a resynchronization point: a top-level keyword (`type`,
76//! `const`, `struct`, and the other definition starters) or a closing `}` (a
77//! tuple loop also stops at its `)`). A block body that runs into a top-level
78//! keyword treats its `{` as unclosed (FORM-103) and hands the keyword back to
79//! the top level rather than swallowing the next declaration. A family reserved
80//! word where a declared name is expected is held in an `ErrorNode` and flagged
81//! FORM-105, so the rest of the declaration still parses. Every [`SyntaxError`]
82//! carries the narrowest honest range — the offending token — while the
83//! `ErrorNode` carries the wider skipped span.
84//!
85//! # One diagnostic per source position
86//!
87//! [`Parser::expect`] never consumes on a mismatch, so a stuck cursor could
88//! otherwise re-report at the same offset on every unwind step — a closed
89//! 200-level type nesting once produced over a thousand diagnostics piled on a
90//! single token. The parser records at most one [`SyntaxError`] per token start
91//! offset. Because the cursor only ever moves forward,
92//! [`Parser::error_at_current`] suppressing an error at the offset the previous
93//! error already used is exactly "one diagnostic per source position". A run of
94//! garbage therefore collapses to one diagnostic plus one `ErrorNode`, and the
95//! pathological nesting inputs report a small, constant number of diagnostics
96//! instead of one per unwound level.
97//!
98//! The profile-boundary **TYPL-302** is the one exception: a positional
99//! FORM-101 can land on a duration or `@` token first (`[0..10ms]` fails the
100//! `]` check at `10ms`), and the profile-boundary fact is the higher-value
101//! diagnostic, so both are recorded. A TYPL-302 token is always consumed where
102//! it is raised, so exempting it can never re-introduce a flood.
103
104use rowan::{GreenNode, GreenNodeBuilder, TextRange, TextSize};
105
106use crate::keywords::{self, Profile};
107use crate::lexer::{Token, lex};
108use crate::syntax_kind::{SyntaxKind, SyntaxNode};
109
110/// A parse diagnostic: a message, the stable diagnostic code it will carry in
111/// the coded model (`FORM-…` per ADR-0007 decision 2, `TYPL-302` per typl
112/// reference §16.4), and the source range of the offending token.
113#[derive(Debug, Clone, PartialEq, Eq)]
114pub struct SyntaxError {
115    pub message: String,
116    pub code: &'static str,
117    pub range: TextRange,
118}
119
120/// The result of [`parse`]: the lossless green tree plus any diagnostics.
121///
122/// [`Parse`] compares by the identity of its green tree, which is what the
123/// salsa query graph needs to decide whether a reparse changed
124/// anything downstream.
125#[derive(Debug, Clone)]
126pub struct Parse {
127    green: GreenNode,
128    errors: Vec<SyntaxError>,
129}
130
131impl Parse {
132    /// A fresh red-tree root over the parsed green tree.
133    pub fn syntax(&self) -> SyntaxNode {
134        SyntaxNode::new_root(self.green.clone())
135    }
136
137    /// The diagnostics gathered while parsing, in source order.
138    pub fn errors(&self) -> &[SyntaxError] {
139        &self.errors
140    }
141}
142
143impl PartialEq for Parse {
144    fn eq(&self, other: &Self) -> bool {
145        self.green == other.green
146    }
147}
148
149impl Eq for Parse {}
150
151/// Parses `input` into a lossless [`Parse`] under `profile`.
152pub fn parse(input: &str, profile: Profile) -> Parse {
153    let mut parser = Parser::new(lex(input, profile), profile);
154    parser.source_file();
155    parser.finish()
156}
157
158/// Whether `kind` can be the value token of a `Literal` (`family.ungram` rule
159/// `Literal`, minus the optional leading `-`).
160fn is_value_token(kind: SyntaxKind) -> bool {
161    matches!(
162        kind,
163        SyntaxKind::IntNumber
164            | SyntaxKind::FloatNumber
165            | SyntaxKind::String
166            | SyntaxKind::TrueKw
167            | SyntaxKind::FalseKw
168            | SyntaxKind::Regex
169            | SyntaxKind::Ident
170    )
171}
172
173/// Names the literal shape a `reserved` tombstone refused, for the FORM-102
174/// message. `kind` is a value token other than `IntNumber` — the one shape
175/// typl Appendix E admits after `reserved` alongside a name.
176fn ungrammatical_reserved_noun(kind: SyntaxKind) -> &'static str {
177    match kind {
178        SyntaxKind::FloatNumber => "a floating-point literal",
179        SyntaxKind::String => "a string literal",
180        SyntaxKind::TrueKw | SyntaxKind::FalseKw => "a boolean literal",
181        SyntaxKind::Regex => "a regex literal",
182        SyntaxKind::Ident => "a constant reference",
183        _ => "a literal of that shape",
184    }
185}
186
187/// Whether `kind` starts a top-level construct. These are the
188/// resynchronization points recovery falls back to, both at the file level
189/// and when a block body runs past an unclosed `}` into the next declaration.
190/// `interface` joins the set under the ridl profile; under [`Profile::Typl`] it never
191/// occurs (the word lexes to `ReservedWord` there). The four top-level rsdl
192/// declaration keywords join it with the rsdl grammar; they occur only under
193/// [`Profile::Rsdl`]. `machine` is not among them: a machine is declared only
194/// inside a deployment body (rsdl reference Appendix B).
195fn is_top_level_start(kind: SyntaxKind) -> bool {
196    matches!(
197        kind,
198        SyntaxKind::PackageKw
199            | SyntaxKind::ImportKw
200            | SyntaxKind::InternalKw
201            | SyntaxKind::ErrorKw
202            | SyntaxKind::TypeKw
203            | SyntaxKind::ConstKw
204            | SyntaxKind::StructKw
205            | SyntaxKind::EnumKw
206            | SyntaxKind::EnumsetKw
207            | SyntaxKind::UnionKw
208            | SyntaxKind::InterfaceKw
209            | SyntaxKind::ServiceKw
210            | SyntaxKind::SystemKw
211            | SyntaxKind::ComponentKw
212            | SyntaxKind::DistributionKw
213            | SyntaxKind::DeploymentKw
214    )
215}
216
217/// Whether `kind` starts an rsdl declaration, `machine` included — what an
218/// `internal` or `error` modifier in an `.rsdl` file is refused in front of
219/// (FORM-102, rsdl reference §2).
220fn is_rsdl_declaration_start(kind: SyntaxKind) -> bool {
221    matches!(
222        kind,
223        SyntaxKind::SystemKw
224            | SyntaxKind::ComponentKw
225            | SyntaxKind::DistributionKw
226            | SyntaxKind::DeploymentKw
227            | SyntaxKind::MachineKw
228    )
229}
230
231/// The three body kinds of the rsdl containers (rsdl reference §3, §4): what
232/// starts a line, what parses it, and how the body is named in a diagnostic.
233#[derive(Clone, Copy, PartialEq, Eq)]
234enum RsdlBody {
235    /// A `system`, `distribution` or `machine` body: bare member lines.
236    Members,
237    /// A `component` body: `offers` and `requires` lines.
238    ComponentLines,
239    /// A `deployment` body: `machine` declarations.
240    Machines,
241}
242
243impl RsdlBody {
244    fn starts_line(self, kind: SyntaxKind) -> bool {
245        match self {
246            // A reference starts like a `QualifiedName` ([`Parser::at_path_segment`]).
247            Self::Members => matches!(
248                kind,
249                SyntaxKind::Ident
250                    | SyntaxKind::BooleanKw
251                    | SyntaxKind::IntegerKw
252                    | SyntaxKind::FloatKw
253                    | SyntaxKind::StringKw
254                    | SyntaxKind::BytesKw
255            ),
256            Self::ComponentLines => matches!(kind, SyntaxKind::OffersKw | SyntaxKind::RequiresKw),
257            Self::Machines => kind == SyntaxKind::MachineKw,
258        }
259    }
260}
261
262/// The RSDL-604 message for a declaration of another profile at the top level
263/// of an `.rsdl` file, naming the word that starts it (rsdl reference §2).
264fn rsdl_foreign_declaration_message(word: &str) -> String {
265    format!(
266        "`{word}` starts a declaration of another profile, and an `.rsdl` file declares \
267         `system`, `component`, `distribution` and `deployment` only — a type is declared in \
268         a `.typl` file, an interface or a service in a `.ridl` file (rsdl reference §2)"
269    )
270}
271
272/// Whether `kind` starts an interaction inside an interface body — the five
273/// interaction keywords. They join the recovery sync set inside
274/// interface bodies, so garbage resynchronizes at the next interaction.
275fn is_interaction_start(kind: SyntaxKind) -> bool {
276    matches!(
277        kind,
278        SyntaxKind::SignalKw
279            | SyntaxKind::EventKw
280            | SyntaxKind::CommandKw
281            | SyntaxKind::QueryKw
282            | SyntaxKind::FixedKw
283    )
284}
285
286/// Where recovery from a rejected return type stops: the interface-body sync
287/// set plus `@`, so a timing annotation written after a broken return type is
288/// still parsed into its own node and still draws its own diagnostic.
289fn is_return_sync(kind: SyntaxKind) -> bool {
290    matches!(
291        kind,
292        SyntaxKind::RBrace | SyntaxKind::Comma | SyntaxKind::ReservedKw | SyntaxKind::At
293    ) || is_interaction_start(kind)
294        || is_top_level_start(kind)
295}
296
297/// Whether `kind` is a typl definition keyword. Inside an interface body
298/// whose `}` still lies ahead, these recover into a body-local
299/// [`ErrorNode`](SyntaxKind::ErrorNode) — the ridl reference forbids type
300/// declarations there (§14.1) and reserves RIDL-107 for them (§16.1), which
301/// the body loop raises. In a body with no `}` ahead they signal an unclosed
302/// `{` exactly like the remaining top-level keywords, so a genuinely
303/// unclosed interface keeps the declarations that follow it.
304fn is_typl_definition_start(kind: SyntaxKind) -> bool {
305    matches!(
306        kind,
307        SyntaxKind::TypeKw
308            | SyntaxKind::ConstKw
309            | SyntaxKind::StructKw
310            | SyntaxKind::EnumKw
311            | SyntaxKind::EnumsetKw
312            | SyntaxKind::UnionKw
313    )
314}
315
316/// The deepest field-type, expression, or attribute-value nesting the parser
317/// follows before it stops recursing, and the greatest height of the tree it
318/// builds for one expression — far beyond any real schema, small enough that
319/// a pathological input can overflow neither the parser's stack nor the stack
320/// of a pass that later recurses over the tree.
321const MAX_TYPE_DEPTH: usize = 128;
322
323/// The recursive-descent state: the token stream, a cursor, the tree builder,
324/// and the diagnostics.
325struct Parser<'a> {
326    tokens: Vec<Token<'a>>,
327    /// Byte offset of the start of each token, plus a final entry for the end
328    /// of input; `offsets[i]` is the offset of `tokens[i]`.
329    offsets: Vec<usize>,
330    pos: usize,
331    /// The nesting the cursor is under, bounded by [`MAX_TYPE_DEPTH`].
332    /// Outside an expression it counts the field types and attribute values
333    /// open above the cursor (each `field_type` or `attr_value` call adds
334    /// one). Inside an expression it counts the expression nodes open above
335    /// the cursor — the groups, prefixes and chain wraps the node built next
336    /// will sit under — which is what the height bound is measured against.
337    depth: usize,
338    /// Whether the expression being parsed has already drawn FORM-102. A
339    /// later part of the same expression that does not fit either goes flat
340    /// without a second diagnostic.
341    expr_refused: bool,
342    /// The profile the file parses under — where the profile-boundary
343    /// diagnostics (TYPL-302, TYPL-304, RIDL-403) are drawn.
344    profile: Profile,
345    builder: GreenNodeBuilder<'static>,
346    errors: Vec<SyntaxError>,
347}
348
349impl<'a> Parser<'a> {
350    fn new(tokens: Vec<Token<'a>>, profile: Profile) -> Self {
351        let mut offsets = Vec::with_capacity(tokens.len() + 1);
352        let mut acc = 0;
353        for token in &tokens {
354            offsets.push(acc);
355            acc += token.text.len();
356        }
357        offsets.push(acc);
358        Self {
359            tokens,
360            offsets,
361            pos: 0,
362            depth: 0,
363            expr_refused: false,
364            profile,
365            builder: GreenNodeBuilder::new(),
366            errors: Vec::new(),
367        }
368    }
369
370    fn finish(self) -> Parse {
371        Parse {
372            green: self.builder.finish(),
373            errors: self.errors,
374        }
375    }
376
377    // --- cursor over significant (non-trivia) tokens ---------------------
378
379    /// Index of the next non-trivia token at or after the cursor.
380    fn significant_pos(&self) -> usize {
381        let mut i = self.pos;
382        while self.tokens.get(i).is_some_and(|t| t.kind.is_trivia()) {
383            i += 1;
384        }
385        i
386    }
387
388    /// Kind of the next significant token, or `None` at end of input.
389    fn current(&self) -> Option<SyntaxKind> {
390        self.tokens.get(self.significant_pos()).map(|t| t.kind)
391    }
392
393    /// Kind of the `n`-th significant token ahead (`nth(0)` is [`current`]).
394    fn nth(&self, n: usize) -> Option<SyntaxKind> {
395        let mut i = self.pos;
396        let mut seen = 0;
397        loop {
398            let token = self.tokens.get(i)?;
399            if !token.kind.is_trivia() {
400                if seen == n {
401                    return Some(token.kind);
402                }
403                seen += 1;
404            }
405            i += 1;
406        }
407    }
408
409    fn at(&self, kind: SyntaxKind) -> bool {
410        self.current() == Some(kind)
411    }
412
413    /// Source text of the next significant token, or `""` at end of input —
414    /// for a message that quotes the word the author wrote.
415    fn current_text(&self) -> &'a str {
416        self.tokens
417            .get(self.significant_pos())
418            .map_or("", |token| token.text)
419    }
420
421    // --- token consumption (feeds the tree, keeps losslessness) ----------
422
423    /// Attaches any pending trivia tokens to the current node, in source order.
424    fn eat_trivia(&mut self) {
425        while let Some(token) = self.tokens.get(self.pos) {
426            if token.kind.is_trivia() {
427                self.builder.token(token.kind.into(), token.text);
428                self.pos += 1;
429            } else {
430                break;
431            }
432        }
433    }
434
435    /// Flushes leading trivia to the currently open node, then starts a child
436    /// node — so a definition's doc comment stays a sibling of the definition
437    /// rather than its first child.
438    fn start(&mut self, kind: SyntaxKind) {
439        self.eat_trivia();
440        self.builder.start_node(kind.into());
441    }
442
443    /// Consumes one significant token into the tree, flushing leading trivia
444    /// first. Callers must have confirmed a token is present.
445    fn bump(&mut self) {
446        self.eat_trivia();
447        if let Some(token) = self.tokens.get(self.pos) {
448            self.builder.token(token.kind.into(), token.text);
449            self.pos += 1;
450        }
451    }
452
453    /// Consumes the current token if it is `kind`; reports FORM-101 otherwise.
454    fn expect(&mut self, kind: SyntaxKind) {
455        if self.at(kind) {
456            self.bump();
457        } else {
458            self.error_at_current("FORM-101", format!("expected {kind:?}"));
459        }
460    }
461
462    /// Records an error spanning the current significant token (or the end of
463    /// input), unless a diagnostic already sits at that start offset.
464    ///
465    /// The cursor only ever moves forward, so the start offset here is
466    /// non-decreasing across calls; comparing against the last recorded error
467    /// therefore enforces one diagnostic per source position (see the module
468    /// doc). This is the flood control: on a stuck cursor the repeated,
469    /// no-progress `expect`/`bound` errors during an unwind collapse to the
470    /// first one.
471    ///
472    /// The profile-boundary **TYPL-302** is exempt: it is the higher-value
473    /// diagnostic and must survive a positional FORM-101 that lands on the
474    /// same boundary token first (`[0..10ms]` reports both). A TYPL-302 token
475    /// is always consumed at the site that raises it, so it can never be the
476    /// stuck-cursor flood source the suppression guards against.
477    fn error_at_current(&mut self, code: &'static str, message: String) {
478        let i = self.significant_pos();
479        let start = TextSize::new(self.offsets[i] as u32);
480        if code != "TYPL-302"
481            && self
482                .errors
483                .last()
484                .is_some_and(|last| last.range.start() == start)
485        {
486            return;
487        }
488        let end = self
489            .tokens
490            .get(i)
491            .map_or(start, |t| start + TextSize::of(t.text));
492        self.errors.push(SyntaxError {
493            message,
494            code,
495            range: TextRange::new(start, end),
496        });
497    }
498
499    /// The diagnostic for the current unexpected token: in a `.typl` parse,
500    /// TYPL-302 for the profile-boundary tokens (duration literals and the `@`
501    /// timing sigil, typl reference §2.8); FORM-102 otherwise. Under
502    /// [`Profile::Ridl`] durations and `@` are ordinary tokens, so they draw
503    /// the generic FORM-102 like any other token the grammar cannot place.
504    fn unexpected_token_diag(&self, context: &str) -> (&'static str, String) {
505        if self.profile == Profile::Typl {
506            match self.current() {
507                Some(SyntaxKind::Duration) => {
508                    return ("TYPL-302", "duration literal in typl context".to_string());
509                }
510                Some(SyntaxKind::At) => {
511                    return ("TYPL-302", "timing annotation in typl context".to_string());
512                }
513                _ => {}
514            }
515        }
516        ("FORM-102", format!("unexpected token {context}"))
517    }
518
519    /// Recovery: report the current unexpected token, then skip forward —
520    /// wrapping the skipped run in one [`SyntaxKind::ErrorNode`] — until a
521    /// token `is_sync` accepts (a resynchronization point) or the input ends.
522    /// The first token is always consumed, so the enclosing loop makes
523    /// progress and no text is dropped.
524    fn err_and_recover(&mut self, context: &str, is_sync: impl Fn(SyntaxKind) -> bool) {
525        let (code, message) = self.unexpected_token_diag(context);
526        self.error_at_current(code, message);
527        self.recover(is_sync);
528    }
529
530    /// The recovery half of [`err_and_recover`]: consumes the current token
531    /// and everything up to the next token `is_sync` accepts into one
532    /// [`SyntaxKind::ErrorNode`]. The profile-boundary arms (TYPL-304,
533    /// RIDL-403) share it, so they recover exactly as FORM-102/FORM-105 do.
534    fn recover(&mut self, is_sync: impl Fn(SyntaxKind) -> bool) {
535        self.start(SyntaxKind::ErrorNode);
536        self.bump();
537        while let Some(kind) = self.current() {
538            if is_sync(kind) {
539                break;
540            }
541            self.bump();
542        }
543        self.builder.finish_node();
544    }
545
546    // --- grammar productions --------------------------------------------
547
548    /// `SourceFile = PackageDecl? Import* Definition*`
549    ///
550    /// A missing `package` emits FORM-104 and parsing continues; extra
551    /// `package` declarations parse as further `PackageDecl` nodes (TYPL-001
552    /// is checker scope). Imports and definitions are accepted in any order —
553    /// ordering is surface convention, not tree shape.
554    fn source_file(&mut self) {
555        self.builder.start_node(SyntaxKind::SourceFile.into());
556        self.eat_trivia();
557        if self.at(SyntaxKind::PackageKw) {
558            self.package_decl();
559        } else {
560            self.error_at_current("FORM-104", "missing `package` declaration".to_string());
561        }
562        loop {
563            self.eat_trivia();
564            match self.current() {
565                None => break,
566                Some(SyntaxKind::PackageKw) => self.package_decl(),
567                Some(SyntaxKind::ImportKw) => self.import(),
568                Some(
569                    SyntaxKind::InternalKw
570                    | SyntaxKind::ErrorKw
571                    | SyntaxKind::TypeKw
572                    | SyntaxKind::ConstKw
573                    | SyntaxKind::StructKw
574                    | SyntaxKind::EnumKw
575                    | SyntaxKind::EnumsetKw
576                    | SyntaxKind::UnionKw
577                    | SyntaxKind::InterfaceKw,
578                ) => {
579                    if self.profile == Profile::Rsdl {
580                        self.rsdl_typl_word_at_top_level();
581                    } else {
582                        self.definition();
583                    }
584                }
585                Some(SyntaxKind::Ident) if self.current_text() == "open" => {
586                    if self.profile == Profile::Rsdl && self.open_modifier_at(0) {
587                        self.rsdl_foreign_declaration(1);
588                    } else {
589                        self.definition();
590                    }
591                }
592                Some(SyntaxKind::ServiceKw) => self.service_def(),
593                Some(SyntaxKind::SystemKw) => {
594                    self.rsdl_container(SyntaxKind::SystemDef, RsdlBody::Members);
595                }
596                Some(SyntaxKind::ComponentKw) => {
597                    self.rsdl_container(SyntaxKind::ComponentDef, RsdlBody::ComponentLines);
598                }
599                Some(SyntaxKind::DistributionKw) => {
600                    self.rsdl_container(SyntaxKind::DistributionDef, RsdlBody::Members);
601                }
602                Some(SyntaxKind::DeploymentKw) => self.deployment_def(),
603                Some(SyntaxKind::ReservedWord) => {
604                    if !self.profile_boundary_at_top_level() {
605                        self.err_and_recover("at top level", is_top_level_start);
606                    }
607                }
608                Some(_) => self.err_and_recover("at top level", is_top_level_start),
609            }
610        }
611        self.builder.finish_node();
612    }
613
614    /// The profile boundary at declaration-start position (ADR-0007 decision
615    /// 10, ridl reference §16.4). Called on a `ReservedWord` at the top level:
616    ///
617    /// - in a `.typl` parse, an interaction keyword (one of the nine ridl
618    ///   words) draws **TYPL-304** instead of the generic FORM-102 — except
619    ///   `require`/`ensure`, which are attributes, not declarations, and
620    ///   draw **TYPL-303** (`require/ensure attribute in typl context`,
621    ///   typl reference §16.4);
622    /// - in a `.ridl` parse, any `ReservedWord` — a behaviour or architecture
623    ///   word of the rmdl/rsdl profiles, or a ridl clause word — draws
624    ///   **RIDL-403**;
625    /// - in a `.rsdl` parse, any `ReservedWord` — `interface` and `service`
626    ///   first of all — draws **RSDL-604** (rsdl reference §2), with the
627    ///   brace-aware recovery of [`Parser::rsdl_foreign_declaration`]. The typl
628    ///   definition keywords are active in an `.rsdl` file and reach
629    ///   [`Parser::rsdl_typl_word_at_top_level`] instead.
630    ///
631    /// All recover exactly as FORM-105 does ([`Parser::recover`]: the run
632    /// lands in one `ErrorNode`, resynchronizing at the next top-level
633    /// keyword). Returns `false` — leaving the token untouched for the
634    /// caller's generic recovery — when neither boundary applies (a
635    /// uxdl/rmdl/rsdl word in a `.typl` parse stays a generic FORM-102).
636    fn profile_boundary_at_top_level(&mut self) -> bool {
637        let text = self.tokens[self.significant_pos()].text;
638        let (code, message) = match self.profile {
639            Profile::Typl => match keywords::ridl_keyword(text) {
640                None => return false,
641                Some(SyntaxKind::RequireKw | SyntaxKind::EnsureKw) => (
642                    "TYPL-303",
643                    "require/ensure attribute in typl context".to_string(),
644                ),
645                Some(_) => (
646                    "TYPL-304",
647                    format!("interaction declaration in typl context: `{text}`"),
648                ),
649            },
650            // The message names neither "behaviour" nor "user-interaction" nor
651            // "architecture": those are the catalogue's words for the three
652            // profiles, and the author wrote one word, not a taxonomy. What is
653            // wrong is that the word is spoken for; what is allowed is the two
654            // declarations a `.ridl` file makes.
655            Profile::Ridl => (
656                "RIDL-403",
657                format!(
658                    "`{text}` is reserved by another profile of the family, so it cannot start a \
659                     declaration here — a `.ridl` file declares `interface` and `service`. Move \
660                     the declaration to the profile that owns the word, or, if `{text}` was meant \
661                     as a name, choose one the family does not reserve (ridl reference §16.4)"
662                ),
663            ),
664            Profile::Rsdl => {
665                self.rsdl_foreign_declaration(0);
666                return true;
667            }
668        };
669        self.error_at_current(code, message);
670        self.recover(is_top_level_start);
671        true
672    }
673
674    /// A typl definition keyword or an `internal`/`error` modifier at the top
675    /// level of an `.rsdl` file. A modifier run in front of an rsdl declaration
676    /// keyword is FORM-102 on each modifier (rsdl reference §2: the five
677    /// declarations take no modifier), and the declaration after it still
678    /// parses. Anything else is a typl declaration, RSDL-604: the modifiers,
679    /// the keyword and the rest of the declaration land in one `ErrorNode`
680    /// ([`Parser::rsdl_foreign_declaration`]), so the declaration draws one
681    /// diagnostic, not one per word.
682    fn rsdl_typl_word_at_top_level(&mut self) {
683        let mut modifiers = 0;
684        while matches!(
685            self.nth(modifiers),
686            Some(SyntaxKind::InternalKw | SyntaxKind::ErrorKw)
687        ) {
688            modifiers += 1;
689        }
690        if self.open_modifier_at(modifiers) {
691            modifiers += 1;
692        }
693        if modifiers > 0 && self.nth(modifiers).is_some_and(is_rsdl_declaration_start) {
694            for _ in 0..modifiers {
695                let word = self.current_text();
696                self.error_at_current(
697                    "FORM-102",
698                    format!(
699                        "`{word}` cannot modify an rsdl declaration — the closure is \
700                         workspace-wide, so every declaration it names is visible from the \
701                         system's package (rsdl reference §2)"
702                    ),
703                );
704                self.start(SyntaxKind::ErrorNode);
705                self.bump();
706                self.builder.finish_node();
707            }
708            return;
709        }
710        self.rsdl_foreign_declaration(modifiers);
711    }
712
713    /// RSDL-604 (rsdl reference §2): a declaration of another profile at the
714    /// top level of an `.rsdl` file, starting at the current token after
715    /// `modifiers` `internal`/`error` words. The declaration draws one
716    /// diagnostic, on its first token, naming its keyword. Recovery is
717    /// brace-aware, like [`Parser::recover_definition_in_body`]: a body
718    /// `{ … }` is consumed whole, so a ridl word inside it (`signal`) is not
719    /// taken for the next declaration; at depth 0 recovery stops at a
720    /// top-level keyword or at a reserved word, which starts the next
721    /// declaration of another profile.
722    fn rsdl_foreign_declaration(&mut self, modifiers: usize) {
723        let keyword = self.nth_text(modifiers);
724        self.error_at_current("RSDL-604", rsdl_foreign_declaration_message(keyword));
725        self.start(SyntaxKind::ErrorNode);
726        for _ in 0..=modifiers {
727            self.bump();
728        }
729        let mut depth = 0usize;
730        while let Some(kind) = self.current() {
731            match kind {
732                SyntaxKind::LBrace => depth += 1,
733                SyntaxKind::RBrace if depth > 0 => depth -= 1,
734                _ if depth == 0
735                    && (is_top_level_start(kind) || kind == SyntaxKind::ReservedWord) =>
736                {
737                    break;
738                }
739                _ => {}
740            }
741            self.bump();
742        }
743        self.builder.finish_node();
744    }
745
746    /// Source text of the `n`-th significant token ahead (`nth_text(0)` is
747    /// [`Parser::current_text`]), or `""` past the end of input.
748    fn nth_text(&self, n: usize) -> &'a str {
749        self.tokens[self.pos..]
750            .iter()
751            .filter(|token| !token.kind.is_trivia())
752            .nth(n)
753            .map_or("", |token| token.text)
754    }
755
756    /// The rsdl containers with no relation clause — `system`, `component`,
757    /// `distribution` and `machine` (rsdl reference §3, general form §2 Shape
758    /// 3): `kw Name AttrBlock? '{' lines '}'`.
759    fn rsdl_container(&mut self, kind: SyntaxKind, body: RsdlBody) {
760        self.start(kind);
761        self.bump(); // the declaration keyword
762        self.name();
763        if self.at(SyntaxKind::LBracket) {
764            self.attr_block();
765        }
766        self.rsdl_body(body);
767        self.builder.finish_node();
768    }
769
770    /// `DeploymentDef = 'deployment' Name 'for' system:Reference AttrBlock?
771    /// '{' (machines:MachineDef ','?)* '}'` (rsdl reference §3.4). The clause
772    /// comes before the attributes (R5). A missing `for` is FORM-101 and the
773    /// body still parses.
774    fn deployment_def(&mut self) {
775        self.start(SyntaxKind::DeploymentDef);
776        self.bump(); // 'deployment'
777        self.name();
778        if self.at(SyntaxKind::ForKw) {
779            self.bump(); // 'for'
780            self.reference();
781        } else {
782            self.error_at_current(
783                "FORM-101",
784                "expected `for` and the system this deployment places".to_string(),
785            );
786        }
787        if self.at(SyntaxKind::LBracket) {
788            self.attr_block();
789        }
790        self.rsdl_body(RsdlBody::Machines);
791        self.builder.finish_node();
792    }
793
794    /// The body loop of an rsdl container, the rsdl counterpart of
795    /// [`Parser::block_body`]: lines separated by newlines or commas, a
796    /// trailing comma permitted (R8). A token that starts no line of this body
797    /// is FORM-102 and recovery resynchronizes at the next line, comma or `}`.
798    /// A reference where a component line is expected is the one-reference-
799    /// per-line rule (rsdl reference §3.2), and its message says so.
800    fn rsdl_body(&mut self, body: RsdlBody) {
801        if !self.block_open() {
802            return;
803        }
804        loop {
805            self.eat_trivia();
806            match self.current() {
807                None => {
808                    self.error_at_current("FORM-103", "unclosed `{`".to_string());
809                    break;
810                }
811                Some(SyntaxKind::RBrace) => {
812                    self.bump();
813                    break;
814                }
815                Some(SyntaxKind::Comma) => self.bump(),
816                Some(kind) if body.starts_line(kind) => match body {
817                    RsdlBody::Members => self.member_line(),
818                    RsdlBody::ComponentLines => self.component_line(),
819                    RsdlBody::Machines => {
820                        self.rsdl_container(SyntaxKind::MachineDef, RsdlBody::Members);
821                    }
822                },
823                Some(kind) if is_top_level_start(kind) => {
824                    self.error_at_current("FORM-103", "unclosed `{`".to_string());
825                    break;
826                }
827                Some(_) => {
828                    let sync = move |kind: SyntaxKind| {
829                        matches!(kind, SyntaxKind::RBrace | SyntaxKind::Comma)
830                            || body.starts_line(kind)
831                            || is_top_level_start(kind)
832                    };
833                    if body == RsdlBody::ComponentLines && self.at_path_segment() {
834                        self.error_at_current(
835                            "FORM-102",
836                            format!(
837                                "`{}` is a second reference — a component line takes one \
838                                 reference, so write its own `offers` or `requires` before it \
839                                 (rsdl reference §3.2)",
840                                self.current_text()
841                            ),
842                        );
843                        self.recover(sync);
844                    } else {
845                        let context = match body {
846                            RsdlBody::Members => "in a member list",
847                            RsdlBody::ComponentLines => "in a component body",
848                            RsdlBody::Machines => "in a deployment body",
849                        };
850                        self.err_and_recover(context, sync);
851                    }
852                }
853            }
854        }
855    }
856
857    /// `MemberLine = Reference AttrBlock?` — a bare reference in a `system`,
858    /// `distribution` or `machine` body (rsdl reference §4).
859    fn member_line(&mut self) {
860        self.start(SyntaxKind::MemberLine);
861        self.reference();
862        if self.at(SyntaxKind::LBracket) {
863            self.attr_block();
864        }
865        self.builder.finish_node();
866    }
867
868    /// `ComponentLine = ('offers' | 'requires') Reference AttrBlock?` (rsdl
869    /// reference §3.2).
870    fn component_line(&mut self) {
871        self.start(SyntaxKind::ComponentLine);
872        self.bump(); // 'offers' | 'requires'
873        self.reference();
874        if self.at(SyntaxKind::LBracket) {
875            self.attr_block();
876        }
877        self.builder.finish_node();
878    }
879
880    /// `Reference = QualifiedName` (rsdl reference §4, Appendix B). No node is
881    /// built when the reference is missing.
882    fn reference(&mut self) {
883        if !self.at_path_segment() {
884            self.error_at_current("FORM-101", "expected a reference".to_string());
885            return;
886        }
887        self.start(SyntaxKind::Reference);
888        self.qualified_name();
889        self.builder.finish_node();
890    }
891
892    /// `PackageDecl = 'package' QualifiedName`
893    fn package_decl(&mut self) {
894        self.start(SyntaxKind::PackageDecl);
895        self.bump(); // 'package'
896        self.qualified_name();
897        self.builder.finish_node();
898    }
899
900    /// `Import = 'import' QualifiedName ('as' alias:Name)?`
901    fn import(&mut self) {
902        self.start(SyntaxKind::Import);
903        self.bump(); // 'import'
904        self.qualified_name();
905        if self.at(SyntaxKind::AsKw) {
906            self.bump();
907            self.name();
908        }
909        self.builder.finish_node();
910    }
911
912    /// `Definition = TypeDef | ConstDef | StructDef | EnumDef | EnumSetDef |
913    /// UnionDef`, each with optional leading `internal`/`error` modifiers.
914    /// The definition keyword is found by looking past the modifiers; a
915    /// modifier with no definition keyword after it is an unexpected token.
916    fn definition(&mut self) {
917        let mut n = 0;
918        while matches!(
919            self.nth(n),
920            Some(SyntaxKind::InternalKw | SyntaxKind::ErrorKw)
921        ) {
922            n += 1;
923        }
924        // `open` is the last modifier: it must directly precede `enum` or
925        // `enumset`. Any other `open` at a definition start is a misplaced
926        // modifier (`open struct`, `open error enum`, `open internal enum`).
927        if self.open_modifier_at(n) {
928            n += 1;
929        } else if self.nth(n) == Some(SyntaxKind::Ident) && self.nth_text(n) == "open" {
930            for _ in 0..n {
931                self.bump();
932            }
933            self.error_at_current(
934                "FORM-102",
935                "unexpected token after `open`: expected `enum` or `enumset`, because \
936                 `open` modifies only an enum or an enumset definition and directly \
937                 precedes its keyword"
938                    .to_string(),
939            );
940            self.recover(is_top_level_start);
941            return;
942        }
943        match self.nth(n) {
944            Some(SyntaxKind::TypeKw) => self.type_def(),
945            Some(SyntaxKind::ConstKw) => self.const_def(),
946            Some(SyntaxKind::StructKw) => self.struct_def(),
947            Some(SyntaxKind::EnumKw) => self.enum_def(),
948            Some(SyntaxKind::EnumsetKw) => self.enum_set_def(),
949            Some(SyntaxKind::UnionKw) => self.union_def(),
950            Some(SyntaxKind::InterfaceKw) => self.interface_def(),
951            _ => self.err_and_recover("in a definition", is_top_level_start),
952        }
953    }
954
955    /// Whether the `n`-th significant token ahead is the contextual modifier
956    /// `open`: an identifier spelled `open` whose next significant token is
957    /// `enum` or `enumset`.
958    fn open_modifier_at(&self, n: usize) -> bool {
959        self.nth(n) == Some(SyntaxKind::Ident)
960            && self.nth_text(n) == "open"
961            && matches!(
962                self.nth(n + 1),
963                Some(SyntaxKind::EnumKw | SyntaxKind::EnumsetKw)
964            )
965    }
966
967    /// Consumes the `internal`/`error` modifier run, then the `open` modifier
968    /// as an [`SyntaxKind::OpenKw`] token, into the open node.
969    fn modifiers(&mut self) {
970        while matches!(
971            self.current(),
972            Some(SyntaxKind::InternalKw | SyntaxKind::ErrorKw)
973        ) {
974            self.bump();
975        }
976        if self.open_modifier_at(0) {
977            self.eat_trivia();
978            let token = &self.tokens[self.pos];
979            self.builder.token(SyntaxKind::OpenKw.into(), token.text);
980            self.pos += 1;
981        }
982    }
983
984    /// `TypeDef = 'internal'? 'error'? 'type' Name ':' Backing Constraint?
985    /// InitValue?` — the constraint is a direct child of `TypeDef` here,
986    /// never nested inside `PrimitiveType` (that position is for field
987    /// types).
988    fn type_def(&mut self) {
989        self.start(SyntaxKind::TypeDef);
990        self.modifiers();
991        self.bump(); // 'type'
992        self.name();
993        self.expect(SyntaxKind::Colon);
994        let stringish = self.backing();
995        if self.at(SyntaxKind::LBracket) {
996            self.constraint(stringish);
997        }
998        self.init_value_opt();
999        self.builder.finish_node();
1000    }
1001
1002    /// `Backing = PrimitiveType | UnitExpr` in `type`-definition position.
1003    /// Returns whether the backing is `string`/`bytes` — the bound policy
1004    /// input for the constraint that may follow.
1005    fn backing(&mut self) -> bool {
1006        match self.current() {
1007            Some(
1008                kw @ (SyntaxKind::BooleanKw
1009                | SyntaxKind::IntegerKw
1010                | SyntaxKind::FloatKw
1011                | SyntaxKind::StringKw
1012                | SyntaxKind::BytesKw),
1013            ) => {
1014                self.start(SyntaxKind::PrimitiveType);
1015                self.bump();
1016                self.builder.finish_node();
1017                matches!(kw, SyntaxKind::StringKw | SyntaxKind::BytesKw)
1018            }
1019            Some(SyntaxKind::Ident | SyntaxKind::Slash | SyntaxKind::Dot | SyntaxKind::Percent) => {
1020                // `UnitExpr = ('ident' | '/' | '.' | '%')*` — a UCUM
1021                // expression (`km/h`, `N.m`, `/min`, `%`). Semantic
1022                // validation is the ucum module's job (TYPL-110).
1023                self.start(SyntaxKind::UnitExpr);
1024                while matches!(
1025                    self.current(),
1026                    Some(
1027                        SyntaxKind::Ident
1028                            | SyntaxKind::Slash
1029                            | SyntaxKind::Dot
1030                            | SyntaxKind::Percent
1031                    )
1032                ) {
1033                    self.bump();
1034                }
1035                self.builder.finish_node();
1036                false
1037            }
1038            _ => {
1039                self.error_at_current("FORM-101", "expected a backing type".to_string());
1040                false
1041            }
1042        }
1043    }
1044
1045    /// `ConstDef = 'internal'? 'error'? 'const' Name (':' type_ref:PathType)?
1046    /// '=' value:Literal`
1047    fn const_def(&mut self) {
1048        self.start(SyntaxKind::ConstDef);
1049        self.modifiers();
1050        self.bump(); // 'const'
1051        self.name();
1052        if self.at(SyntaxKind::Colon) {
1053            self.bump();
1054            self.path_type();
1055        }
1056        self.expect(SyntaxKind::Eq);
1057        self.literal();
1058        self.builder.finish_node();
1059    }
1060
1061    /// `StructDef = 'internal'? 'error'? 'struct' Name '{' (members ','?)*
1062    /// '}'` — a member is a `FieldDef` or a `ReservedEntry`.
1063    fn struct_def(&mut self) {
1064        self.start(SyntaxKind::StructDef);
1065        self.modifiers();
1066        self.bump(); // 'struct'
1067        self.name();
1068        self.block_body("in a struct body", true, Self::field_def);
1069        self.builder.finish_node();
1070    }
1071
1072    /// `EnumDef = 'internal'? 'error'? 'enum' Name '{' ((values | reserved)
1073    /// ','?)* '}'`
1074    fn enum_def(&mut self) {
1075        self.start(SyntaxKind::EnumDef);
1076        self.modifiers();
1077        self.bump(); // 'enum'
1078        self.name();
1079        self.block_body("in an enum body", true, Self::enum_value);
1080        self.builder.finish_node();
1081    }
1082
1083    /// `EnumSetDef` — the standalone form (`enumset Name '{' bits '}'`) or
1084    /// the derived form (`enumset Name ':' backing_ref:PathType`), §9.1–§9.2.
1085    fn enum_set_def(&mut self) {
1086        self.start(SyntaxKind::EnumSetDef);
1087        self.modifiers();
1088        self.bump(); // 'enumset'
1089        self.name();
1090        if self.at(SyntaxKind::Colon) {
1091            self.bump();
1092            self.path_type();
1093        } else {
1094            self.block_body("in an enumset body", false, Self::enum_set_bit);
1095        }
1096        self.builder.finish_node();
1097    }
1098
1099    /// `UnionDef = 'internal'? 'error'? 'union' Name '{' ((arms | reserved)
1100    /// ','?)* '}'`
1101    fn union_def(&mut self) {
1102        self.start(SyntaxKind::UnionDef);
1103        self.modifiers();
1104        self.bump(); // 'union'
1105        self.name();
1106        self.block_body("in a union body", true, Self::union_arm);
1107        self.builder.finish_node();
1108    }
1109
1110    /// `UnionArm = Name ':' type_ref:PathType` — arms reference named types
1111    /// only; a primitive keyword still parses as a path segment and the
1112    /// checker rejects it (TYPL-204).
1113    fn union_arm(&mut self) {
1114        self.start(SyntaxKind::UnionArm);
1115        self.name();
1116        self.expect(SyntaxKind::Colon);
1117        self.path_type();
1118        self.builder.finish_node();
1119    }
1120
1121    // --- ridl interaction productions (ridl reference Appendix C) --
1122
1123    /// `InterfaceDef = 'internal'? 'error'? 'interface' Name '{' (members
1124    /// ','?)* '}'` — an interface body holds interactions and `reserved`
1125    /// tombstones only (ridl reference §14.1); a type declaration inside it
1126    /// is RIDL-107, raised by the body loop. The body loop is the interaction
1127    /// counterpart of [`Parser::block_body`]: members are announced by the
1128    /// five interaction keywords instead of an `Ident`, and those keywords
1129    /// are the recovery sync points, so garbage inside a body
1130    /// resynchronizes at the next interaction.
1131    fn interface_def(&mut self) {
1132        self.start(SyntaxKind::InterfaceDef);
1133        self.modifiers();
1134        self.bump(); // 'interface'
1135        self.name();
1136        if !self.block_open() {
1137            self.builder.finish_node();
1138            return;
1139        }
1140        self.interface_body("an interface", "interface");
1141        self.builder.finish_node();
1142    }
1143
1144    /// The interaction-body loop shared by an `interface` body and a
1145    /// `service` inline body: the opening `{` has already been
1146    /// consumed. Members are announced by the five interaction keywords, and
1147    /// those keywords plus the top-level starters are the recovery sync
1148    /// points, so garbage inside a body resynchronizes at the next
1149    /// interaction. A `service` inline body runs the same structural pass as
1150    /// an interface (ridl reference §14.5).
1151    ///
1152    /// `noun` and `shape` name the enclosing construct for the RIDL-107
1153    /// message — `"an interface"`/`"interface"` or `"a service"`/`"service"`.
1154    /// The two callers share this loop, and a reader is told where the stray
1155    /// declaration has to move to.
1156    fn interface_body(&mut self, noun: &str, shape: &str) {
1157        loop {
1158            self.eat_trivia();
1159            match self.current() {
1160                None => {
1161                    self.error_at_current("FORM-103", "unclosed `{`".to_string());
1162                    break;
1163                }
1164                Some(SyntaxKind::RBrace) => {
1165                    self.bump();
1166                    break;
1167                }
1168                Some(SyntaxKind::Comma) => self.bump(),
1169                Some(SyntaxKind::ReservedKw) => self.reserved_entry(),
1170                Some(SyntaxKind::SignalKw) => self.value_interaction(SyntaxKind::SignalDef),
1171                Some(SyntaxKind::EventKw) => self.value_interaction(SyntaxKind::EventDef),
1172                Some(SyntaxKind::CommandKw) => self.callable_interaction(SyntaxKind::CommandDef),
1173                Some(SyntaxKind::QueryKw) => self.callable_interaction(SyntaxKind::QueryDef),
1174                Some(SyntaxKind::FixedKw) => self.value_interaction(SyntaxKind::FixedDef),
1175                // A typl definition keyword inside the body: the body holds
1176                // interactions and tombstones only (ridl §14.1). When the
1177                // body's `}` still lies ahead, the declaration recovers into
1178                // a body-local ErrorNode — brace-aware, so a composite body
1179                // stays inside it and the members after it keep their place.
1180                // With no `}` ahead the guard fails and the keyword falls
1181                // through to the unclosed-`{` arm below, exactly like the
1182                // remaining top-level keywords (`package`, `import`,
1183                // `internal`, `error`, `interface`, `service`).
1184                //
1185                // RIDL-107 is raised here rather than by the checker over the
1186                // recovered node. The parser has already recognised exactly
1187                // what this is, so the generic FORM-102 "unexpected token in
1188                // an interface body" it used to raise was both wrong and a
1189                // second diagnostic for one mistake: every RIDL-107 arrived
1190                // paired with a contradicting FORM-102 at the same span. The
1191                // profile-boundary codes RIDL-403 and TYPL-304 are raised from
1192                // the parser for the same reason.
1193                Some(kind)
1194                    if is_typl_definition_start(kind) && self.interface_body_close_ahead() =>
1195                {
1196                    self.error_at_current(
1197                        "RIDL-107",
1198                        format!(
1199                            "`{}` starts a typl declaration, and {noun} body holds interactions \
1200                             and `reserved` tombstones only — move the declaration to package \
1201                             level, beside the {shape} (ridl reference §14.1)",
1202                            self.current_text(),
1203                        ),
1204                    );
1205                    self.recover_definition_in_body();
1206                }
1207                // An unclosed `{`: the body ran into the next top-level
1208                // declaration. Report the missing brace and hand the
1209                // declaration back, exactly as `block_body` does.
1210                Some(kind) if is_top_level_start(kind) => {
1211                    self.error_at_current("FORM-103", "unclosed `{`".to_string());
1212                    break;
1213                }
1214                Some(_) => self.err_and_recover("in an interface body", |kind| {
1215                    matches!(
1216                        kind,
1217                        SyntaxKind::RBrace | SyntaxKind::Comma | SyntaxKind::ReservedKw
1218                    ) || is_interaction_start(kind)
1219                        || is_top_level_start(kind)
1220                }),
1221            }
1222        }
1223    }
1224
1225    /// `ServiceDef = 'service' DottedName (':' shapes (',' shapes)* ','? |
1226    /// '{' (inline_members ','?)* '}')` — the global published declaration of
1227    /// one or more interfaces (ridl reference §14.5, ADR-0015 decision 12).
1228    /// The named form carries a comma-separated shape list after `:`; the
1229    /// inline form carries an interaction body reusing
1230    /// [`Parser::interface_body`], so the checker runs the same structural pass
1231    /// over it (RIDL-401/-402). Never both forms (ADR-0015 decision 14). A
1232    /// service takes no `internal`/`error` modifiers — it is a global, published
1233    /// contract.
1234    fn service_def(&mut self) {
1235        self.start(SyntaxKind::ServiceDef);
1236        self.bump(); // 'service'
1237        self.dotted_name();
1238        match self.current() {
1239            Some(SyntaxKind::Colon) => {
1240                self.bump(); // ':'
1241                self.service_shape_list();
1242            }
1243            Some(SyntaxKind::LBrace) => {
1244                self.bump(); // '{'
1245                self.interface_body("a service", "service");
1246            }
1247            _ => self.error_at_current(
1248                "FORM-101",
1249                "expected `:` and an interface list, or `{` and an inline shape".to_string(),
1250            ),
1251        }
1252        self.builder.finish_node();
1253    }
1254
1255    /// The comma-separated shape list of a service's named form: one
1256    /// `PathType` per shape (ADR-0015 decision 12).
1257    ///
1258    /// Commas are **required** between shapes, diverging from the family's
1259    /// optional-comma convention, and the reason is structural (ADR-0015
1260    /// decision 13): every other list in the grammar is terminated by a
1261    /// closing token — `}` for a body, `)` for a parameter list, `]` for an
1262    /// attribute block — and this one is not, because it ends where the next
1263    /// declaration begins. It would still parse without commas, since every
1264    /// top-level declaration starts with a keyword and a bare `CamelCase`
1265    /// identifier never does — but it would parse greedily, so a mistyped
1266    /// declaration on a following line (`Struct Foo {` for `struct Foo {`)
1267    /// would be absorbed as another shape and the error would surface at the
1268    /// `{` with no connection to the mistake. Ending the list at the first
1269    /// missing comma instead leaves that identifier to the top-level loop,
1270    /// whose FORM-102 points at it directly. A trailing comma stays optional.
1271    fn service_shape_list(&mut self) {
1272        // One shape: an interface reference. A primitive keyword still parses
1273        // as a path segment and the checker rejects it (RIDL-141), the E1
1274        // lenient-parser discipline. `reserved` is not a shape: a service's
1275        // list is a set with no slot to retire (lock design §9), so the list
1276        // ends before it and the top-level loop reports the stray keyword.
1277        self.path_type();
1278        // `current()` peeks past trivia without consuming it, so a list that
1279        // ends here leaves the trailing trivia outside the ServiceDef node.
1280        while self.at(SyntaxKind::Comma) {
1281            self.bump(); // ','
1282            // A trailing comma: nothing after it starts a shape.
1283            if !self.at_path_segment() {
1284                break;
1285            }
1286            self.path_type();
1287        }
1288    }
1289
1290    /// `DottedName = 'ident' ('.' 'ident')*` — a dotted global service name
1291    /// (`veh.adas.cruise`, ADR-0002 §1). Each segment is a plain identifier;
1292    /// a trailing `.` with no segment after it is FORM-101.
1293    fn dotted_name(&mut self) {
1294        self.start(SyntaxKind::DottedName);
1295        if self.at(SyntaxKind::Ident) {
1296            self.bump();
1297            while self.at(SyntaxKind::Dot) {
1298                self.bump(); // '.'
1299                if self.at(SyntaxKind::Ident) {
1300                    self.bump();
1301                } else {
1302                    self.error_at_current(
1303                        "FORM-101",
1304                        "expected a name segment after `.`".to_string(),
1305                    );
1306                    break;
1307                }
1308            }
1309        } else {
1310            self.error_at_current("FORM-101", "expected a service name".to_string());
1311        }
1312        self.builder.finish_node();
1313    }
1314
1315    /// Whether the interface body being parsed still closes: scans forward,
1316    /// brace-aware, for an `RBrace` at depth 0 before the end of input or an
1317    /// unambiguous new-top-level marker (`package`, `import`, `interface`).
1318    /// Decides the recovery for a typl definition keyword inside a body: a
1319    /// body-local ErrorNode when the body closes (RIDL-107),
1320    /// an unclosed-`{` report when it does not. The scan runs through the
1321    /// definition keywords themselves — several stray declarations inside
1322    /// one closed body must all recover in place.
1323    fn interface_body_close_ahead(&self) -> bool {
1324        let mut depth = 0usize;
1325        for token in self.tokens[self.significant_pos()..]
1326            .iter()
1327            .filter(|token| !token.kind.is_trivia())
1328        {
1329            match token.kind {
1330                SyntaxKind::LBrace => depth += 1,
1331                SyntaxKind::RBrace if depth == 0 => return true,
1332                SyntaxKind::RBrace => depth -= 1,
1333                SyntaxKind::PackageKw
1334                | SyntaxKind::ImportKw
1335                | SyntaxKind::InterfaceKw
1336                | SyntaxKind::ServiceKw
1337                    if depth == 0 =>
1338                {
1339                    return false;
1340                }
1341                _ => {}
1342            }
1343        }
1344        false
1345    }
1346
1347    /// Consumes one stray typl declaration inside an interface body into a
1348    /// single [`ErrorNode`](SyntaxKind::ErrorNode). Brace-aware, unlike
1349    /// [`Parser::recover`]: a composite body (`struct`/`enum`/… with its
1350    /// `{ … }`, commas included) is consumed whole, so the declaration's own
1351    /// `}` is never taken for the interface's closer and the members after
1352    /// it stay in the body. At depth 0 the interface sync set applies. The
1353    /// caller has confirmed the body's `}` lies ahead
1354    /// ([`Parser::interface_body_close_ahead`]), so the loop terminates
1355    /// there at the latest.
1356    fn recover_definition_in_body(&mut self) {
1357        self.start(SyntaxKind::ErrorNode);
1358        self.bump(); // the typl definition keyword
1359        let mut depth = 0usize;
1360        while let Some(kind) = self.current() {
1361            match kind {
1362                SyntaxKind::LBrace => depth += 1,
1363                SyntaxKind::RBrace if depth > 0 => depth -= 1,
1364                _ if depth == 0
1365                    && (matches!(
1366                        kind,
1367                        SyntaxKind::RBrace | SyntaxKind::Comma | SyntaxKind::ReservedKw
1368                    ) || is_interaction_start(kind)
1369                        || is_top_level_start(kind)) =>
1370                {
1371                    break;
1372                }
1373                _ => {}
1374            }
1375            self.bump();
1376        }
1377        self.builder.finish_node();
1378    }
1379
1380    /// The shared `kw Name ':' payload InitValue? annotations` shape of the
1381    /// three value interactions — `SignalDef`, `EventDef`, and `FixedDef`.
1382    /// The bare `= value` init comes before the timing (ADR-0008 decision
1383    /// 2). All three kinds accept an init, a timing, and an attr block, as
1384    /// the reference's Appendix C shows; the checker narrows them (FORM-102
1385    /// for an init on an event or a fixed, RIDL-106/-301).
1386    fn value_interaction(&mut self, kind: SyntaxKind) {
1387        self.start(kind);
1388        self.bump(); // 'signal' | 'event' | 'fixed'
1389        self.name();
1390        self.expect(SyntaxKind::Colon);
1391        self.field_type();
1392        self.init_value_opt();
1393        self.interaction_annotations();
1394        self.builder.finish_node();
1395    }
1396
1397    /// The shared `kw Name '(' params ')' (':' ReturnType)? annotations`
1398    /// shape of the two callable interactions — `CommandDef` and
1399    /// `QueryDef`. A command's return type is lenient here (RIDL-104,
1400    /// checker scope); a query's is mandatory — a missing `:` draws
1401    /// FORM-101 (ridl reference §6.1, §7.1).
1402    fn callable_interaction(&mut self, kind: SyntaxKind) {
1403        self.start(kind);
1404        self.bump(); // 'command' | 'query'
1405        self.name();
1406        self.param_list();
1407        if self.at(SyntaxKind::Colon) {
1408            self.bump();
1409            self.return_type();
1410        } else if kind == SyntaxKind::QueryDef {
1411            self.error_at_current("FORM-101", "expected `:` and a return type".to_string());
1412        }
1413        self.interaction_annotations();
1414        self.builder.finish_node();
1415    }
1416
1417    /// `ParamList = '(' (params:Param ','?)* ')'` — separator commas are
1418    /// direct children of the list node, mirroring the block-body
1419    /// discipline (typl reference §15.2). No node is built when the `(` is
1420    /// missing, so `CommandDef::params()` sees `None`.
1421    fn param_list(&mut self) {
1422        if !self.at(SyntaxKind::LParen) {
1423            self.error_at_current("FORM-101", "expected `(`".to_string());
1424            return;
1425        }
1426        self.start(SyntaxKind::ParamList);
1427        self.bump(); // '('
1428        loop {
1429            self.eat_trivia();
1430            match self.current() {
1431                None => {
1432                    self.error_at_current("FORM-103", "unclosed `(`".to_string());
1433                    break;
1434                }
1435                Some(SyntaxKind::RParen) => {
1436                    self.bump();
1437                    break;
1438                }
1439                Some(SyntaxKind::Comma) => self.bump(),
1440                Some(SyntaxKind::Ident) => self.param(),
1441                // An unclosed `(`: the list ran into an interaction, body,
1442                // or top-level boundary. Report it and hand the token back.
1443                Some(kind)
1444                    if kind == SyntaxKind::RBrace
1445                        || is_interaction_start(kind)
1446                        || is_top_level_start(kind) =>
1447                {
1448                    self.error_at_current("FORM-103", "unclosed `(`".to_string());
1449                    break;
1450                }
1451                Some(_) => self.err_and_recover("in a parameter list", |kind| {
1452                    matches!(
1453                        kind,
1454                        SyntaxKind::RParen
1455                            | SyntaxKind::Comma
1456                            | SyntaxKind::Ident
1457                            | SyntaxKind::RBrace
1458                    ) || is_interaction_start(kind)
1459                        || is_top_level_start(kind)
1460                }),
1461            }
1462        }
1463        self.builder.finish_node();
1464    }
1465
1466    /// `Param = Name ':' (FieldType | StreamType)` — a named typl type, a
1467    /// tuple (typl §11), or a stream `<T>` (ridl reference §12.1); the
1468    /// stream parses through the shared field-type dispatch.
1469    fn param(&mut self) {
1470        self.start(SyntaxKind::Param);
1471        self.name();
1472        self.expect(SyntaxKind::Colon);
1473        self.field_type();
1474        self.builder.finish_node();
1475    }
1476
1477    /// `ReturnType = PathType | TupleType | StreamType | FallibleType` —
1478    /// the four return shapes (ridl reference §7.1). A named type followed
1479    /// by `|` extends into `FallibleType = ok '|' err` (general form §6.1,
1480    /// ADR-0008 decision 1). No node is built when no shape starts here, so
1481    /// `return_type()` accessors see `None`.
1482    fn return_type(&mut self) {
1483        self.eat_trivia();
1484        match self.current() {
1485            Some(SyntaxKind::LParen) => {
1486                self.start(SyntaxKind::ReturnType);
1487                self.tuple_type();
1488                self.builder.finish_node();
1489            }
1490            Some(SyntaxKind::Lt) => {
1491                self.start(SyntaxKind::ReturnType);
1492                self.stream_type();
1493                self.builder.finish_node();
1494            }
1495            _ if self.at_path_segment() => {
1496                self.start(SyntaxKind::ReturnType);
1497                let checkpoint = self.builder.checkpoint();
1498                self.path_type();
1499                if self.at(SyntaxKind::Pipe) {
1500                    self.builder
1501                        .start_node_at(checkpoint, SyntaxKind::FallibleType.into());
1502                    self.bump(); // '|'
1503                    self.path_type();
1504                    self.builder.finish_node();
1505                }
1506                self.builder.finish_node();
1507            }
1508            _ => {
1509                self.error_at_current(
1510                    "FORM-101",
1511                    "a return type must be a named type, a named-field tuple `(a: A, b: B)`, a \
1512                     stream `<T>`, or a fallible `T | E` (ridl reference §7.1)"
1513                        .to_string(),
1514                );
1515                // Consume what stands here into an `ErrorNode` rather than
1516                // leaving it for `interaction_annotations`. A rejected return
1517                // shape most often starts with `[` — a collection, `[Level;
1518                // 0..8]` — and leaving it behind made the annotation parser
1519                // read it as this member's attribute block, so one mistake drew
1520                // a FORM-102 and a FORM-106 "unknown attribute key `Level`" on
1521                // top of the real diagnostic. `@` stays in the sync set so a
1522                // timing annotation written after a broken return is still
1523                // parsed and still reaches its own check.
1524                if self.current().is_some_and(|kind| !is_return_sync(kind)) {
1525                    self.recover(is_return_sync);
1526                }
1527            }
1528        }
1529    }
1530
1531    /// `StreamType = '<' (PathType | 'string' | 'bytes') '>'` — the element
1532    /// is a named type, or a bare raw `string`/`bytes` token (ridl
1533    /// reference §12.2, the one exception to typl §15.3). Callers have
1534    /// confirmed the `<`.
1535    fn stream_type(&mut self) {
1536        self.start(SyntaxKind::StreamType);
1537        self.bump(); // '<'
1538        if matches!(
1539            self.current(),
1540            Some(SyntaxKind::StringKw | SyntaxKind::BytesKw)
1541        ) && self.nth(1) == Some(SyntaxKind::Gt)
1542        {
1543            self.bump(); // the raw element keyword
1544        } else if self.at_path_segment() {
1545            self.path_type();
1546        } else {
1547            self.error_at_current("FORM-101", "expected an element type".to_string());
1548        }
1549        self.expect(SyntaxKind::Gt);
1550        self.builder.finish_node();
1551    }
1552
1553    /// The lenient trailing annotations of an interaction: at most one
1554    /// [`Timing`](SyntaxKind::Timing) and at most one
1555    /// [`AttrBlock`](SyntaxKind::AttrBlock), in either order. Which kinds
1556    /// may carry which annotation is checker scope (RIDL-104/-106/-301); the
1557    /// parser accepts both on every interaction.
1558    fn interaction_annotations(&mut self) {
1559        let mut seen_timing = false;
1560        let mut seen_attrs = false;
1561        loop {
1562            match self.current() {
1563                Some(SyntaxKind::At) if !seen_timing => {
1564                    seen_timing = true;
1565                    self.timing();
1566                }
1567                Some(SyntaxKind::LBracket) if !seen_attrs => {
1568                    seen_attrs = true;
1569                    self.attr_block();
1570                }
1571                _ => break,
1572            }
1573        }
1574    }
1575
1576    /// `Timing = '@' ('duration' | '[' TimingRange ']')` — strict periodic
1577    /// or range (ridl reference §9). Callers have confirmed the `@`.
1578    fn timing(&mut self) {
1579        self.start(SyntaxKind::Timing);
1580        self.bump(); // '@'
1581        if self.at(SyntaxKind::Duration) {
1582            self.bump();
1583        } else if self.at(SyntaxKind::LBracket) {
1584            self.bump();
1585            self.timing_range();
1586            self.expect(SyntaxKind::RBracket);
1587        } else {
1588            self.error_at_current("FORM-101", "expected a duration or `[`".to_string());
1589        }
1590        self.builder.finish_node();
1591    }
1592
1593    /// `TimingRange = 'duration'? '..' 'duration'?` — both bounds or the
1594    /// half-open forms `min..` and `..max` (ridl reference §9). A range
1595    /// with neither bound is a structural error (FORM-101).
1596    fn timing_range(&mut self) {
1597        self.start(SyntaxKind::TimingRange);
1598        let has_min = self.at(SyntaxKind::Duration);
1599        if has_min {
1600            self.bump();
1601        }
1602        self.expect(SyntaxKind::DotDot);
1603        let has_max = self.at(SyntaxKind::Duration);
1604        if has_max {
1605            self.bump();
1606        }
1607        if !has_min && !has_max {
1608            self.error_at_current("FORM-101", "expected a duration".to_string());
1609        }
1610        self.builder.finish_node();
1611    }
1612
1613    /// `AttrBlock = '[' (attributes:Attribute ','?)* ']'` — the single
1614    /// attribute production of the general form (§4.2–§4.3). Separator
1615    /// commas are direct children of the block node, mirroring the
1616    /// block-body discipline. gf §4.2 requires at least one attribute per
1617    /// block: an empty `[]` still parses losslessly into an `AttrBlock`
1618    /// node and FORM-101 (`expected an attribute`) lands on the closing
1619    /// bracket. An unclosed block stops at an interaction or body boundary
1620    /// (FORM-103) instead of swallowing the rest of the interface.
1621    fn attr_block(&mut self) {
1622        self.start(SyntaxKind::AttrBlock);
1623        self.bump(); // '['
1624        let mut saw_attribute = false;
1625        loop {
1626            match self.current() {
1627                None => {
1628                    self.error_at_current("FORM-103", "unclosed `[`".to_string());
1629                    break;
1630                }
1631                Some(SyntaxKind::RBracket) => {
1632                    if !saw_attribute {
1633                        self.error_at_current("FORM-101", "expected an attribute".to_string());
1634                    }
1635                    self.bump();
1636                    break;
1637                }
1638                Some(SyntaxKind::Comma) => self.bump(),
1639                // A `ReservedWord` in key position goes through
1640                // [`Parser::name`], which holds it in an `ErrorNode` with
1641                // FORM-105, so the rest of the attribute still parses.
1642                // The rsdl predicate words are recognized by `attribute`
1643                // instead, for semantic placement checking.
1644                Some(
1645                    SyntaxKind::Ident
1646                    | SyntaxKind::RequireKw
1647                    | SyntaxKind::EnsureKw
1648                    | SyntaxKind::ReservedWord,
1649                ) => {
1650                    saw_attribute = true;
1651                    self.attribute();
1652                }
1653                Some(kind)
1654                    if kind == SyntaxKind::RBrace
1655                        || is_interaction_start(kind)
1656                        || is_top_level_start(kind) =>
1657                {
1658                    self.error_at_current("FORM-103", "unclosed `[`".to_string());
1659                    break;
1660                }
1661                Some(_) => self.err_and_recover("in an attribute block", |kind| {
1662                    matches!(
1663                        kind,
1664                        SyntaxKind::RBracket
1665                            | SyntaxKind::Comma
1666                            | SyntaxKind::Ident
1667                            | SyntaxKind::RequireKw
1668                            | SyntaxKind::EnsureKw
1669                            | SyntaxKind::ReservedWord
1670                            | SyntaxKind::RBrace
1671                    ) || is_interaction_start(kind)
1672                        || is_top_level_start(kind)
1673                }),
1674            }
1675        }
1676        self.builder.finish_node();
1677    }
1678
1679    /// `Attribute = key | key '=' const_value | ('require'|'ensure') expr`
1680    /// — the three forms of gf §4.2. One grammar production for all of
1681    /// them; which keys are legal where is checker scope (gf §4.3,
1682    /// FORM-106/107/108). Callers have confirmed the leading token is an
1683    /// `Ident`, a predicate keyword, or a reserved word.
1684    fn attribute(&mut self) {
1685        self.start(SyntaxKind::Attribute);
1686        // Predicate words stay reserved in rsdl names, but the shared
1687        // attribute grammar leaves their placement to the semantic checker.
1688        let predicate = match self.current() {
1689            Some(kind @ (SyntaxKind::RequireKw | SyntaxKind::EnsureKw)) => Some(kind),
1690            Some(SyntaxKind::ReservedWord) if self.profile == Profile::Rsdl => {
1691                match self.current_text() {
1692                    "require" => Some(SyntaxKind::RequireKw),
1693                    "ensure" => Some(SyntaxKind::EnsureKw),
1694                    _ => None,
1695                }
1696            }
1697            _ => None,
1698        };
1699        if let Some(kind) = predicate {
1700            self.eat_trivia();
1701            let token = &self.tokens[self.pos];
1702            self.builder.token(kind.into(), token.text);
1703            self.pos += 1;
1704            self.expr();
1705        } else {
1706            self.name(); // the attr key
1707            // A backend key `namespace.key` (rsdl reference §5, Appendix
1708            // B `key`). Only the rsdl profile admits the dotted key; under
1709            // typl and ridl the `.` stays an unexpected token in the block.
1710            if self.profile == Profile::Rsdl && self.at(SyntaxKind::Dot) {
1711                self.bump(); // '.'
1712                self.name();
1713            }
1714            if self.at(SyntaxKind::Eq) {
1715                self.bump();
1716                self.attr_value();
1717            }
1718        }
1719        self.builder.finish_node();
1720    }
1721
1722    /// `const_value = literal | SCREAMING_SNAKE_ID | '(' const_value (','
1723    /// const_value)* ')'` (gf §4.2), as an [`AttrValue`](SyntaxKind::AttrValue)
1724    /// node: one `Literal` child (an `ident` value token is the constant
1725    /// reference), or a parenthesised list of nested `AttrValue`s. The
1726    /// depth guard bounds pathological list nesting, sharing the
1727    /// [`MAX_TYPE_DEPTH`] budget with the type and expression grammars.
1728    fn attr_value(&mut self) {
1729        if self.depth >= MAX_TYPE_DEPTH {
1730            self.error_at_current(
1731                "FORM-102",
1732                format!("value nesting deeper than {MAX_TYPE_DEPTH} levels"),
1733            );
1734            self.start(SyntaxKind::ErrorNode);
1735            self.bump();
1736            self.builder.finish_node();
1737            return;
1738        }
1739        self.depth += 1;
1740        self.attr_value_inner();
1741        self.depth -= 1;
1742    }
1743
1744    fn attr_value_inner(&mut self) {
1745        self.eat_trivia();
1746        if self.at(SyntaxKind::LParen) {
1747            self.start(SyntaxKind::AttrValue);
1748            self.bump(); // '('
1749            loop {
1750                self.eat_trivia();
1751                match self.current() {
1752                    None => {
1753                        self.error_at_current("FORM-103", "unclosed `(`".to_string());
1754                        break;
1755                    }
1756                    Some(SyntaxKind::RParen) => {
1757                        self.bump();
1758                        break;
1759                    }
1760                    Some(SyntaxKind::Comma) => self.bump(),
1761                    Some(SyntaxKind::LParen) => self.attr_value(),
1762                    Some(kind)
1763                        if kind == SyntaxKind::RBracket
1764                            || kind == SyntaxKind::RBrace
1765                            || is_interaction_start(kind)
1766                            || is_top_level_start(kind) =>
1767                    {
1768                        self.error_at_current("FORM-103", "unclosed `(`".to_string());
1769                        break;
1770                    }
1771                    _ if self.literal_len_at(0) > 0 => self.attr_value(),
1772                    Some(_) => self.err_and_recover("in an attribute value", |kind| {
1773                        matches!(
1774                            kind,
1775                            SyntaxKind::RParen
1776                                | SyntaxKind::Comma
1777                                | SyntaxKind::LParen
1778                                | SyntaxKind::RBracket
1779                                | SyntaxKind::RBrace
1780                        ) || is_interaction_start(kind)
1781                            || is_top_level_start(kind)
1782                    }),
1783                }
1784            }
1785            self.builder.finish_node();
1786        } else if self.literal_len_at(0) > 0 {
1787            self.start(SyntaxKind::AttrValue);
1788            self.literal();
1789            self.builder.finish_node();
1790        } else {
1791            self.error_at_current("FORM-101", "expected a value".to_string());
1792        }
1793    }
1794
1795    // --- the guaranteed-subset expression grammar ------------------
1796    //
1797    // The expr-core specification §3.1 productions, one function per
1798    // precedence level, loosest to tightest: `||` — `&&` — comparison —
1799    // additive — multiplicative — unary — member access. Binary levels are
1800    // left-associative (each iteration wraps the run so far in a fresh
1801    // BinaryExpr from the same checkpoint); a comparison never chains.
1802    // Inside an expression `<` is always the comparison operator — the
1803    // stream-type reading of `<` exists only in param-type and return-type
1804    // position, and the two positions never overlap.
1805    //
1806    // The height bound (driftsys/ridl#346). A left-associative chain builds
1807    // a tree as high as its term count, and a pass that recurses over the
1808    // tree needs stack in proportion to that height, so the parser bounds
1809    // the height of every expression tree at MAX_TYPE_DEPTH nodes. Every
1810    // function below returns the height of the subtree it built (1 for a
1811    // node whose children are all tokens, 0 when no node was built), and
1812    // `depth` counts the expression nodes open above the cursor. A node is
1813    // built only when `depth + height <= MAX_TYPE_DEPTH` (`expr_fits`): a
1814    // chain checks the wrap before consuming its operator, from the height
1815    // of the run so far plus one, and parses the right operand one level
1816    // deeper, so the operand's own checks account for the wrap above it; a
1817    // member step is checked like a chain wrap; a group or a prefix is
1818    // opened only when it and one leaf below it fit.
1819    // The invariant holds by induction over the build order: each node's
1820    // check covers the node itself, and a child built earlier sits one level
1821    // deeper under a parent whose height is at least the child's plus one.
1822    // A leaf is never checked, because every node above it was admitted
1823    // with room for one — a chain wrap counts its left operand as at least
1824    // one high even when that operand is missing. A
1825    // budget charged per parse call and released on return would not bound
1826    // the tree, because chains closed inside groups, and chains that are the
1827    // first operand of a looser chain, stack on the left spine.
1828    //
1829    // Past the limit the expression draws one FORM-102 and the rest of the
1830    // current group goes flat, as tokens under one ErrorNode: the tree stays
1831    // lossless and a later part of the same expression that does not fit
1832    // either is consumed the same way without a second diagnostic.
1833
1834    /// `expr = or_expr` — the entry point, called from a predicate
1835    /// attribute; a parenthesised group recurses through [`Parser::primary`].
1836    /// An attribute block is never inside a type or a value, so the
1837    /// expression starts at depth 0 and a leaf always fits below it.
1838    fn expr(&mut self) {
1839        self.expr_refused = false;
1840        self.or_expr();
1841    }
1842
1843    /// Whether an expression node `height` levels high fits at the current
1844    /// depth. Past the limit, reports FORM-102 at the current token unless
1845    /// the expression has already drawn one, so that one overlong expression
1846    /// draws one diagnostic whatever its shape. The message says how the
1847    /// levels are counted — one per binary operator, member access,
1848    /// parenthesised group and prefix — because the nesting of a flat
1849    /// operator chain is not visible in the source.
1850    fn expr_fits(&mut self, height: usize) -> bool {
1851        if self.depth + height <= MAX_TYPE_DEPTH {
1852            return true;
1853        }
1854        if !self.expr_refused {
1855            self.expr_refused = true;
1856            self.error_at_current(
1857                "FORM-102",
1858                format!(
1859                    "expression nesting deeper than {MAX_TYPE_DEPTH} levels (each operator, \
1860                     member access, parenthesised group and prefix nests one level)"
1861                ),
1862            );
1863        }
1864        false
1865    }
1866
1867    /// Consumes the rest of the current expression level as flat children
1868    /// of one [`ErrorNode`](SyntaxKind::ErrorNode): every token up to the
1869    /// `)` that closes the enclosing group, the `]` or `,` that ends the
1870    /// attribute, a brace, the end of input, a token that starts another
1871    /// construct, or the start of the next attribute, with or without a
1872    /// comma before it — a `require` or `ensure`, a reserved word in key
1873    /// position, or a key, which is an identifier where the expression has
1874    /// just ended in an operand and no operator can follow. A group opened
1875    /// inside is consumed through its own `)`.
1876    fn skip_expr_rest(&mut self) {
1877        self.start(SyntaxKind::ErrorNode);
1878        self.skip_expr_tokens();
1879        self.builder.finish_node();
1880    }
1881
1882    /// The token loop of [`Parser::skip_expr_rest`], without the node.
1883    fn skip_expr_tokens(&mut self) {
1884        let mut open = 0usize;
1885        let mut after_operand = false;
1886        while let Some(kind) = self.current() {
1887            match kind {
1888                SyntaxKind::RBracket
1889                | SyntaxKind::Comma
1890                | SyntaxKind::LBrace
1891                | SyntaxKind::RBrace
1892                | SyntaxKind::RequireKw
1893                | SyntaxKind::EnsureKw
1894                | SyntaxKind::ReservedWord => break,
1895                SyntaxKind::RParen if open == 0 => break,
1896                SyntaxKind::Ident if after_operand => break,
1897                _ if is_interaction_start(kind) || is_top_level_start(kind) => break,
1898                SyntaxKind::RParen => open -= 1,
1899                SyntaxKind::LParen => open += 1,
1900                _ => {}
1901            }
1902            after_operand = matches!(
1903                kind,
1904                SyntaxKind::Ident
1905                    | SyntaxKind::IntNumber
1906                    | SyntaxKind::FloatNumber
1907                    | SyntaxKind::String
1908                    | SyntaxKind::TrueKw
1909                    | SyntaxKind::FalseKw
1910                    | SyntaxKind::Duration
1911                    | SyntaxKind::RParen
1912            );
1913            self.bump();
1914        }
1915    }
1916
1917    /// The shared loop of the four chaining binary levels: `operand { op
1918    /// operand }`, each `op` a token `is_op` accepts, each iteration wrapping
1919    /// the run so far in a [`BinaryExpr`](SyntaxKind::BinaryExpr) from the
1920    /// same checkpoint. Returns the height of the tree built.
1921    fn chain(&mut self, is_op: fn(SyntaxKind) -> bool, operand: fn(&mut Self) -> usize) -> usize {
1922        self.eat_trivia();
1923        let checkpoint = self.builder.checkpoint();
1924        let mut height = operand(self);
1925        while self.current().is_some_and(is_op) {
1926            // A missing left operand (FORM-101, already reported) has height
1927            // 0; the wrap still holds the right operand, so it is at least
1928            // two high, and the check must say so for the leaf below to fit.
1929            if !self.expr_fits(height.max(1) + 1) {
1930                self.skip_expr_rest();
1931                break;
1932            }
1933            self.builder
1934                .start_node_at(checkpoint, SyntaxKind::BinaryExpr.into());
1935            self.bump();
1936            self.depth += 1;
1937            let rhs = operand(self);
1938            self.depth -= 1;
1939            self.builder.finish_node();
1940            height = 1 + height.max(rhs);
1941        }
1942        height
1943    }
1944
1945    /// `or_expr = and_expr { '||' and_expr }`
1946    fn or_expr(&mut self) -> usize {
1947        self.chain(|kind| kind == SyntaxKind::PipePipe, Self::and_expr)
1948    }
1949
1950    /// `and_expr = cmp_expr { '&&' cmp_expr }`
1951    fn and_expr(&mut self) -> usize {
1952        self.chain(|kind| kind == SyntaxKind::AmpAmp, Self::cmp_expr)
1953    }
1954
1955    /// `cmp_expr = add_expr [ cmp_op add_expr ]` — at most one comparison:
1956    /// `a < b < c` is a parse error (write `a < b && b < c`), reported by
1957    /// whatever context the leftover operator lands in.
1958    fn cmp_expr(&mut self) -> usize {
1959        self.eat_trivia();
1960        let checkpoint = self.builder.checkpoint();
1961        let mut height = self.add_expr();
1962        if matches!(
1963            self.current(),
1964            Some(
1965                SyntaxKind::EqEq
1966                    | SyntaxKind::Neq
1967                    | SyntaxKind::Lt
1968                    | SyntaxKind::Le
1969                    | SyntaxKind::Gt
1970                    | SyntaxKind::Ge
1971            )
1972        ) {
1973            if !self.expr_fits(height.max(1) + 1) {
1974                self.skip_expr_rest();
1975                return height;
1976            }
1977            self.builder
1978                .start_node_at(checkpoint, SyntaxKind::BinaryExpr.into());
1979            self.bump();
1980            self.depth += 1;
1981            let rhs = self.add_expr();
1982            self.depth -= 1;
1983            self.builder.finish_node();
1984            height = 1 + height.max(rhs);
1985        }
1986        height
1987    }
1988
1989    /// `add_expr = mul_expr { ('+' | '-') mul_expr }`
1990    fn add_expr(&mut self) -> usize {
1991        self.chain(
1992            |kind| matches!(kind, SyntaxKind::Plus | SyntaxKind::Minus),
1993            Self::mul_expr,
1994        )
1995    }
1996
1997    /// `mul_expr = unary_expr { ('*' | '/' | '%') unary_expr }`
1998    fn mul_expr(&mut self) -> usize {
1999        self.chain(
2000            |kind| {
2001                matches!(
2002                    kind,
2003                    SyntaxKind::Star | SyntaxKind::Slash | SyntaxKind::Percent
2004                )
2005            },
2006            Self::unary_expr,
2007        )
2008    }
2009
2010    /// `unary_expr = [ '!' | '-' ] postfix_expr` — at most one prefix.
2011    fn unary_expr(&mut self) -> usize {
2012        if matches!(self.current(), Some(SyntaxKind::Bang | SyntaxKind::Minus)) {
2013            if !self.expr_fits(2) {
2014                self.skip_expr_rest();
2015                return 1;
2016            }
2017            self.start(SyntaxKind::PrefixExpr);
2018            self.bump();
2019            self.depth += 1;
2020            let operand = self.postfix_expr();
2021            self.depth -= 1;
2022            self.builder.finish_node();
2023            1 + operand
2024        } else {
2025            self.postfix_expr()
2026        }
2027    }
2028
2029    /// `postfix_expr = primary { '.' member }` — each `.member` step wraps
2030    /// the run so far in a [`MemberExpr`](SyntaxKind::MemberExpr), so
2031    /// `filter.severity` and `GearPosition.PARK` nest left-associatively.
2032    fn postfix_expr(&mut self) -> usize {
2033        self.eat_trivia();
2034        let checkpoint = self.builder.checkpoint();
2035        let mut height = self.primary();
2036        while self.at(SyntaxKind::Dot) {
2037            if !self.expr_fits(height + 1) {
2038                self.skip_expr_rest();
2039                break;
2040            }
2041            self.builder
2042                .start_node_at(checkpoint, SyntaxKind::MemberExpr.into());
2043            self.bump(); // '.'
2044            if self.at(SyntaxKind::Ident) {
2045                self.bump();
2046            } else {
2047                self.error_at_current("FORM-101", "expected a member name".to_string());
2048            }
2049            self.builder.finish_node();
2050            height += 1;
2051        }
2052        height
2053    }
2054
2055    /// `primary = literal | duration_lit | path_head | '(' expr ')'` — a
2056    /// [`LiteralExpr`](SyntaxKind::LiteralExpr) (typl literal or duration —
2057    /// zero durations are legal in expression position, expr-core spec
2058    /// §3.1), a [`PathExpr`](SyntaxKind::PathExpr), or a parenthesised
2059    /// group. No node is built when no primary starts here, so a missing
2060    /// operand reports FORM-101 without consuming the boundary token. The
2061    /// group is where the grammar recurses, so the height bound is also what
2062    /// keeps a run of `(` from overflowing the parser's own stack: a group
2063    /// that does not fit is consumed flat through its `)`.
2064    fn primary(&mut self) -> usize {
2065        match self.current() {
2066            Some(
2067                SyntaxKind::IntNumber
2068                | SyntaxKind::FloatNumber
2069                | SyntaxKind::String
2070                | SyntaxKind::TrueKw
2071                | SyntaxKind::FalseKw
2072                | SyntaxKind::Duration,
2073            ) => {
2074                self.start(SyntaxKind::LiteralExpr);
2075                if self.at(SyntaxKind::IntNumber) {
2076                    self.flag_leading_zeros();
2077                }
2078                self.bump();
2079                self.builder.finish_node();
2080                1
2081            }
2082            Some(SyntaxKind::Ident) => {
2083                self.start(SyntaxKind::PathExpr);
2084                self.bump();
2085                self.builder.finish_node();
2086                1
2087            }
2088            Some(SyntaxKind::LParen) => {
2089                if !self.expr_fits(2) {
2090                    self.start(SyntaxKind::ErrorNode);
2091                    self.bump(); // '('
2092                    self.skip_expr_tokens();
2093                    if self.at(SyntaxKind::RParen) {
2094                        self.bump();
2095                    }
2096                    self.builder.finish_node();
2097                    return 1;
2098                }
2099                self.start(SyntaxKind::ParenExpr);
2100                self.bump();
2101                self.depth += 1;
2102                let inner = self.or_expr();
2103                self.depth -= 1;
2104                self.expect(SyntaxKind::RParen);
2105                self.builder.finish_node();
2106                1 + inner
2107            }
2108            _ => {
2109                self.error_at_current("FORM-101", "expected an expression".to_string());
2110                0
2111            }
2112        }
2113    }
2114
2115    /// `EnumValue = Name '=' value:Literal`
2116    fn enum_value(&mut self) {
2117        self.value_assignment(SyntaxKind::EnumValue);
2118    }
2119
2120    /// `EnumSetBit = Name '=' value:Literal`
2121    fn enum_set_bit(&mut self) {
2122        self.value_assignment(SyntaxKind::EnumSetBit);
2123    }
2124
2125    /// The shared `Name '=' Literal` shape of enum values and enumset bits.
2126    fn value_assignment(&mut self, kind: SyntaxKind) {
2127        self.start(kind);
2128        self.name();
2129        self.expect(SyntaxKind::Eq);
2130        self.literal();
2131        self.builder.finish_node();
2132    }
2133
2134    /// The shared block-body loop (§15.2): members separated by newlines or
2135    /// commas, trailing comma permitted, separator commas placed as direct
2136    /// children of the block node between members. `member` parses the
2137    /// member a leading `Ident` announces; `allow_reserved` admits
2138    /// `ReservedEntry` members (structs, enums, unions — not enumsets).
2139    fn block_body(&mut self, context: &str, allow_reserved: bool, member: fn(&mut Self)) {
2140        if !self.block_open() {
2141            return;
2142        }
2143        loop {
2144            self.eat_trivia();
2145            match self.current() {
2146                None => {
2147                    self.error_at_current("FORM-103", "unclosed `{`".to_string());
2148                    break;
2149                }
2150                Some(SyntaxKind::RBrace) => {
2151                    self.bump();
2152                    break;
2153                }
2154                Some(SyntaxKind::Comma) => self.bump(),
2155                Some(SyntaxKind::ReservedKw) if allow_reserved => self.reserved_entry(),
2156                Some(SyntaxKind::Ident) => member(self),
2157                // An unclosed `{`: the body ran into the next top-level
2158                // declaration. Report the missing brace and let `source_file`
2159                // resynchronize instead of swallowing the declaration.
2160                Some(kind) if is_top_level_start(kind) => {
2161                    self.error_at_current("FORM-103", "unclosed `{`".to_string());
2162                    break;
2163                }
2164                Some(_) => self.err_and_recover(context, |kind| {
2165                    matches!(
2166                        kind,
2167                        SyntaxKind::RBrace | SyntaxKind::Comma | SyntaxKind::Ident
2168                    ) || (allow_reserved && kind == SyntaxKind::ReservedKw)
2169                        || is_top_level_start(kind)
2170                }),
2171            }
2172        }
2173    }
2174
2175    /// Consumes the `{` opening a block body; reports FORM-101 and returns
2176    /// `false` when it is missing, so the caller skips the body loop instead
2177    /// of swallowing the rest of the file.
2178    fn block_open(&mut self) -> bool {
2179        if self.at(SyntaxKind::LBrace) {
2180            self.bump();
2181            true
2182        } else {
2183            self.error_at_current("FORM-101", "expected `{`".to_string());
2184            false
2185        }
2186    }
2187
2188    /// `FieldDef = Name ':' FieldType InitValue?`
2189    fn field_def(&mut self) {
2190        self.start(SyntaxKind::FieldDef);
2191        self.name();
2192        self.expect(SyntaxKind::Colon);
2193        self.field_type();
2194        self.init_value_opt();
2195        self.builder.finish_node();
2196    }
2197
2198    /// `ReservedEntry = 'reserved' (Name | int_lit)` — the tombstone (§7.4).
2199    /// typl Appendix E spells it `reserved = "reserved" ( camelCase_id |
2200    /// int_lit )`. The production is body-agnostic and this parser keeps it
2201    /// that way: both forms parse in every body that admits a tombstone.
2202    ///
2203    /// What each form *means* is per body, and **nothing narrows it** — there
2204    /// is no per-body check anywhere, so this comment describes the language,
2205    /// not an enforced rule. The name form is meaningful everywhere (TYPL-210
2206    /// in a struct, union or enum; RIDL-401 in an interface). The integer form
2207    /// is meaningful in an `enum`, where it retires a wire value (TYPL-210),
2208    /// and in an interaction body, where the nameless spelling records the
2209    /// ordinal it protects (`veh-cluster/cluster/evolution.ridl`). In a
2210    /// `struct` or `union` body it is **inert**: members there carry no
2211    /// explicit value, so nothing can ever match it and no retired name is
2212    /// recorded. `test_data/parser/ok/grammar_overapprox.typl` carries that
2213    /// case as an over-approximation awaiting a checker rule; see
2214    /// `veh-cluster/NOTES` for the residual and what closing it needs.
2215    ///
2216    /// Any other literal — a string, a boolean, a float, a regex, a bare
2217    /// constant reference — is outside the grammar and draws FORM-102 here
2218    /// rather than parsing as a `Literal`. Lowering keeps only a `Name` or an
2219    /// integer, so an ungrammatical literal would otherwise consume the
2220    /// ordinal while recording neither: the tombstone survives but the
2221    /// retired identity it exists to record is destroyed, with no diagnostic.
2222    /// The offending token is held in an `ErrorNode` so the tree stays
2223    /// lossless and the body loop resumes at the next member.
2224    fn reserved_entry(&mut self) {
2225        self.start(SyntaxKind::ReservedEntry);
2226        self.bump(); // 'reserved'
2227        if self.at(SyntaxKind::Ident) {
2228            self.name();
2229        } else if let Some(kind) = self.literal_value_kind_at(0) {
2230            if kind == SyntaxKind::IntNumber {
2231                self.literal();
2232            } else {
2233                self.start(SyntaxKind::ErrorNode);
2234                // A leading `-` belongs to the literal but is not what makes
2235                // it ungrammatical, so it is consumed before the diagnostic is
2236                // raised: the caret then lands on the value token the message
2237                // names, as it already does for every single-token shape.
2238                if self.at(SyntaxKind::Minus) {
2239                    self.bump();
2240                }
2241                self.error_at_current(
2242                    "FORM-102",
2243                    format!(
2244                        "a `reserved` tombstone takes a retired member name or a retired integer value, not {}",
2245                        ungrammatical_reserved_noun(kind),
2246                    ),
2247                );
2248                self.bump();
2249                self.builder.finish_node();
2250            }
2251        } else {
2252            self.error_at_current(
2253                "FORM-101",
2254                "expected a name or an integer value".to_string(),
2255            );
2256        }
2257        self.builder.finish_node();
2258    }
2259
2260    /// `FieldType = PathType | PrimitiveType | TupleType | ArrayType |
2261    /// MapType | OptionalType` — the type of a field, tuple field, or
2262    /// collection element. Each `?` suffix wraps the type parsed so far in an
2263    /// `OptionalType` node. The depth guard keeps a pathological nesting of
2264    /// collection and tuple types from overflowing the stack — the parser
2265    /// must never panic.
2266    fn field_type(&mut self) {
2267        if self.depth >= MAX_TYPE_DEPTH {
2268            self.error_at_current(
2269                "FORM-102",
2270                format!("type nesting deeper than {MAX_TYPE_DEPTH} levels"),
2271            );
2272            self.start(SyntaxKind::ErrorNode);
2273            self.bump();
2274            self.builder.finish_node();
2275            return;
2276        }
2277        self.depth += 1;
2278        self.field_type_inner();
2279        self.depth -= 1;
2280    }
2281
2282    fn field_type_inner(&mut self) {
2283        self.eat_trivia();
2284        let checkpoint = self.builder.checkpoint();
2285        match self.current() {
2286            Some(
2287                kw @ (SyntaxKind::BooleanKw
2288                | SyntaxKind::IntegerKw
2289                | SyntaxKind::FloatKw
2290                | SyntaxKind::StringKw
2291                | SyntaxKind::BytesKw),
2292            ) => {
2293                // The inline constrained primitive (`integer [0..6]`,
2294                // §15.3): in field position the constraint nests inside
2295                // PrimitiveType, unlike the `type`-definition position.
2296                self.start(SyntaxKind::PrimitiveType);
2297                self.bump();
2298                if self.at(SyntaxKind::LBracket) {
2299                    let stringish = matches!(kw, SyntaxKind::StringKw | SyntaxKind::BytesKw);
2300                    self.constraint(stringish);
2301                }
2302                self.builder.finish_node();
2303            }
2304            Some(SyntaxKind::Ident) => self.path_type(),
2305            Some(SyntaxKind::LParen) => self.tuple_type(),
2306            Some(SyntaxKind::LBracket) => self.array_or_map(checkpoint),
2307            Some(SyntaxKind::Lt) => {
2308                // The stream container `<T>` (ridl reference §12) parses in
2309                // every type position under both profiles; in a `.typl`
2310                // parse it is the profile boundary — TYPL-301 — and parsing
2311                // continues (ADR-0007 decision 10). Under `Profile::Ridl`
2312                // the positions where a stream is not allowed (struct
2313                // fields, signal and event payloads) are checker scope.
2314                if self.profile == Profile::Typl {
2315                    self.error_at_current("TYPL-301", "stream type in typl context".to_string());
2316                }
2317                self.stream_type();
2318            }
2319            _ => {
2320                self.error_at_current("FORM-101", "expected a type".to_string());
2321                return;
2322            }
2323        }
2324        // Each `?` wraps one OptionalType node, so an unbounded run would
2325        // build a tree deep enough to overflow the stack when it is later
2326        // traversed or dropped. The wrap count shares the MAX_TYPE_DEPTH
2327        // bound: past it, the remaining `?` tokens land flat in a single
2328        // ErrorNode with one FORM-102.
2329        let mut wraps = 0;
2330        while self.at(SyntaxKind::Question) {
2331            if wraps >= MAX_TYPE_DEPTH {
2332                self.error_at_current(
2333                    "FORM-102",
2334                    format!("type nesting deeper than {MAX_TYPE_DEPTH} levels"),
2335                );
2336                self.start(SyntaxKind::ErrorNode);
2337                while self.at(SyntaxKind::Question) {
2338                    self.bump();
2339                }
2340                self.builder.finish_node();
2341                break;
2342            }
2343            self.builder
2344                .start_node_at(checkpoint, SyntaxKind::OptionalType.into());
2345            self.bump();
2346            self.builder.finish_node();
2347            wraps += 1;
2348        }
2349    }
2350
2351    /// `TupleType = '(' fields:TupleField (',' fields:TupleField)* ')'` —
2352    /// separator commas are direct children of the tuple node.
2353    fn tuple_type(&mut self) {
2354        self.start(SyntaxKind::TupleType);
2355        self.bump(); // '('
2356        loop {
2357            self.eat_trivia();
2358            match self.current() {
2359                None => {
2360                    self.error_at_current("FORM-103", "unclosed `(`".to_string());
2361                    break;
2362                }
2363                Some(SyntaxKind::RParen) => {
2364                    self.bump();
2365                    break;
2366                }
2367                Some(SyntaxKind::Comma) => self.bump(),
2368                Some(SyntaxKind::Ident) => self.tuple_field(),
2369                // An unclosed `(`: the tuple ran into a block or top-level
2370                // boundary. Report it and let the enclosing loop recover.
2371                Some(kind) if kind == SyntaxKind::RBrace || is_top_level_start(kind) => {
2372                    self.error_at_current("FORM-103", "unclosed `(`".to_string());
2373                    break;
2374                }
2375                Some(_) => self.err_and_recover("in a tuple type", |kind| {
2376                    matches!(
2377                        kind,
2378                        SyntaxKind::RParen
2379                            | SyntaxKind::Comma
2380                            | SyntaxKind::Ident
2381                            | SyntaxKind::RBrace
2382                    ) || is_top_level_start(kind)
2383                }),
2384            }
2385        }
2386        self.builder.finish_node();
2387    }
2388
2389    /// `TupleField = Name ':' FieldType`
2390    fn tuple_field(&mut self) {
2391        self.start(SyntaxKind::TupleField);
2392        self.name();
2393        self.expect(SyntaxKind::Colon);
2394        self.field_type();
2395        self.builder.finish_node();
2396    }
2397
2398    /// `ArrayType = '[' element:FieldType ';' Bound ']'` or
2399    /// `MapType = '[' key:FieldType ':' value:FieldType ';' Bound ']'` — the
2400    /// node kind is decided by the token after the first type, then wrapped
2401    /// around everything from the checkpoint (the `[`).
2402    fn array_or_map(&mut self, checkpoint: rowan::Checkpoint) {
2403        self.bump(); // '['
2404        self.field_type(); // the element or key type
2405        if self.at(SyntaxKind::Colon) {
2406            self.builder
2407                .start_node_at(checkpoint, SyntaxKind::MapType.into());
2408            self.bump(); // ':'
2409            self.field_type(); // the value type
2410        } else {
2411            self.builder
2412                .start_node_at(checkpoint, SyntaxKind::ArrayType.into());
2413        }
2414        self.expect(SyntaxKind::Semicolon);
2415        self.bound();
2416        self.expect(SyntaxKind::RBracket);
2417        self.builder.finish_node();
2418    }
2419
2420    /// `Constraint = '[' … ']'` — see the module doc for the bound policy.
2421    fn constraint(&mut self, stringish: bool) {
2422        self.start(SyntaxKind::Constraint);
2423        self.bump(); // '['
2424        self.constraint_body(stringish);
2425        self.expect(SyntaxKind::RBracket);
2426        self.builder.finish_node();
2427    }
2428
2429    /// The three constraint shapes behind the brackets: a scalar range with
2430    /// optional step, a length bound with optional match, and a bare match.
2431    fn constraint_body(&mut self, stringish: bool) {
2432        match self.current() {
2433            Some(SyntaxKind::MatchKw) => {
2434                self.bump();
2435                self.literal();
2436            }
2437            Some(SyntaxKind::DotDot) => {
2438                // `[..max]` — an open lower bound is always the scalar shape.
2439                self.bump();
2440                self.literal();
2441                self.step_opt();
2442            }
2443            _ if self.literal_len_at(0) > 0 => self.constraint_led_by_literal(stringish),
2444            _ => self.error_at_current("FORM-101", "expected a constraint".to_string()),
2445        }
2446    }
2447
2448    /// A constraint whose body starts with a literal: decide between the
2449    /// length-`Bound` shape and the scalar-range shape (module doc policy).
2450    fn constraint_led_by_literal(&mut self, stringish: bool) {
2451        let first_len = self.literal_len_at(0);
2452        match self.nth(first_len) {
2453            // A single literal with no `..` is always a length bound:
2454            // `[17]`, `[8]`, `[17 match P]`.
2455            Some(SyntaxKind::RBracket | SyntaxKind::MatchKw) | None => {
2456                self.bound();
2457                self.match_opt();
2458            }
2459            Some(SyntaxKind::DotDot) => {
2460                let second_len = self.literal_len_at(first_len + 1);
2461                let after_second = self.nth(first_len + 1 + second_len);
2462                let bound_shape = stringish
2463                    && second_len > 0
2464                    && !self.literal_is_float_at(0)
2465                    && !self.literal_is_float_at(first_len + 1)
2466                    && matches!(
2467                        after_second,
2468                        Some(SyntaxKind::RBracket | SyntaxKind::MatchKw) | None
2469                    );
2470                if bound_shape {
2471                    self.bound();
2472                    self.match_opt();
2473                } else {
2474                    self.literal(); // min
2475                    self.bump(); // '..'
2476                    if self.literal_len_at(0) > 0 {
2477                        self.literal(); // max — absent on `[min..]`
2478                    }
2479                    self.step_opt();
2480                }
2481            }
2482            _ => {
2483                // Not a legal shape after the literal; parse the literal as a
2484                // length bound and let the closing-bracket check report.
2485                self.bound();
2486                self.match_opt();
2487            }
2488        }
2489    }
2490
2491    /// `Bound = min:Literal ('..' max:Literal)?`
2492    fn bound(&mut self) {
2493        if self.literal_len_at(0) == 0 {
2494            self.error_at_current("FORM-101", "expected a bound".to_string());
2495            return;
2496        }
2497        self.start(SyntaxKind::Bound);
2498        self.literal();
2499        if self.at(SyntaxKind::DotDot) {
2500            self.bump();
2501            self.literal();
2502        }
2503        self.builder.finish_node();
2504    }
2505
2506    /// `('step' step:Literal)?`
2507    fn step_opt(&mut self) {
2508        if self.at(SyntaxKind::StepKw) {
2509            self.bump();
2510            self.literal();
2511        }
2512    }
2513
2514    /// `('match' match_pattern:Literal)?`
2515    fn match_opt(&mut self) {
2516        if self.at(SyntaxKind::MatchKw) {
2517            self.bump();
2518            self.literal();
2519        }
2520    }
2521
2522    /// `InitValue = '=' Literal`, when present.
2523    fn init_value_opt(&mut self) {
2524        if self.at(SyntaxKind::Eq) {
2525            self.start(SyntaxKind::InitValue);
2526            self.bump();
2527            self.literal();
2528            self.builder.finish_node();
2529        }
2530    }
2531
2532    /// How many significant tokens the literal starting at the `n`-th
2533    /// significant position spans: 2 for `-` + value, 1 for a bare value, 0
2534    /// when no literal starts there.
2535    fn literal_len_at(&self, n: usize) -> usize {
2536        match self.nth(n) {
2537            Some(SyntaxKind::Minus) => match self.nth(n + 1) {
2538                Some(kind) if is_value_token(kind) => 2,
2539                _ => 0,
2540            },
2541            Some(kind) if is_value_token(kind) => 1,
2542            _ => 0,
2543        }
2544    }
2545
2546    /// The value token of the literal starting at the `n`-th significant
2547    /// position, looking past a leading `-`. `None` when no literal starts
2548    /// there. Lets a caller admit one literal shape and refuse the rest.
2549    fn literal_value_kind_at(&self, n: usize) -> Option<SyntaxKind> {
2550        match self.nth(n) {
2551            Some(SyntaxKind::Minus) => self.nth(n + 1).filter(|kind| is_value_token(*kind)),
2552            Some(kind) if is_value_token(kind) => Some(kind),
2553            _ => None,
2554        }
2555    }
2556
2557    /// Whether the literal starting at the `n`-th significant position has a
2558    /// float value token.
2559    fn literal_is_float_at(&self, n: usize) -> bool {
2560        match self.nth(n) {
2561            Some(SyntaxKind::FloatNumber) => true,
2562            Some(SyntaxKind::Minus) => matches!(self.nth(n + 1), Some(SyntaxKind::FloatNumber)),
2563            _ => false,
2564        }
2565    }
2566
2567    /// `Literal = '-'? ('int_number' | 'float_number' | 'string_lit' | 'true'
2568    /// | 'false' | 'regex' | 'ident')` — an `ident` is a constant reference
2569    /// in a value position. A duration literal here in a `.typl` parse is the
2570    /// profile boundary (TYPL-302); an integer with leading zeros is FORM-005.
2571    /// Under [`Profile::Ridl`] a duration is an ordinary token that is simply
2572    /// not a literal, so it falls through to the generic missing-value path.
2573    fn literal(&mut self) {
2574        if self.profile == Profile::Typl && self.at(SyntaxKind::Duration) {
2575            self.error_at_current("TYPL-302", "duration literal in typl context".to_string());
2576            self.start(SyntaxKind::ErrorNode);
2577            self.bump();
2578            self.builder.finish_node();
2579            return;
2580        }
2581        if self.literal_len_at(0) == 0 {
2582            self.error_at_current("FORM-101", "expected a value".to_string());
2583            return;
2584        }
2585        self.start(SyntaxKind::Literal);
2586        if self.at(SyntaxKind::Minus) {
2587            self.bump();
2588        }
2589        if self.at(SyntaxKind::IntNumber) {
2590            self.flag_leading_zeros();
2591        }
2592        self.bump();
2593        self.builder.finish_node();
2594    }
2595
2596    /// FORM-005: an integer literal with leading zeros (typl reference §2.4)
2597    /// lexes as one token and is flagged here.
2598    fn flag_leading_zeros(&mut self) {
2599        let i = self.significant_pos();
2600        let text = self.tokens[i].text;
2601        if text.len() > 1 && text.starts_with('0') {
2602            self.error_at_current("FORM-005", "integer literal has leading zeros".to_string());
2603        }
2604    }
2605
2606    /// A declared name — a single `Ident` wrapped in a `Name` node. A family
2607    /// reserved word is held in an `ErrorNode` and flagged FORM-105; otherwise
2608    /// no node is built when the name is missing, so accessors see `None`
2609    /// rather than an empty `Name`.
2610    fn name(&mut self) {
2611        if self.at(SyntaxKind::Ident) {
2612            self.start(SyntaxKind::Name);
2613            self.bump();
2614            self.builder.finish_node();
2615        } else if self.at(SyntaxKind::ReservedWord) {
2616            // A family reserved word (typl reference §1.4) used where a
2617            // declared name is expected: hold it in an `ErrorNode` so the
2618            // rest of the declaration still parses, and flag FORM-105.
2619            let message = format!(
2620                "reserved word `{}` used as identifier",
2621                self.tokens[self.significant_pos()].text,
2622            );
2623            self.error_at_current("FORM-105", message);
2624            self.start(SyntaxKind::ErrorNode);
2625            self.bump();
2626            self.builder.finish_node();
2627        } else {
2628            self.error_at_current("FORM-101", "expected a name".to_string());
2629        }
2630    }
2631
2632    /// Whether the current token can be a path segment. Primitive keywords
2633    /// are admitted as segments so `const MAX_GEAR : integer = 6` (typl
2634    /// reference Appendix B) parses; the checker narrows where a primitive
2635    /// is not allowed (TYPL-204 and the related composite checks).
2636    fn at_path_segment(&self) -> bool {
2637        matches!(
2638            self.current(),
2639            Some(
2640                SyntaxKind::Ident
2641                    | SyntaxKind::BooleanKw
2642                    | SyntaxKind::IntegerKw
2643                    | SyntaxKind::FloatKw
2644                    | SyntaxKind::StringKw
2645                    | SyntaxKind::BytesKw
2646            )
2647        )
2648    }
2649
2650    /// `PathType = QualifiedName` — a type reference in type position. No
2651    /// node is built when the reference is missing.
2652    fn path_type(&mut self) {
2653        if !self.at_path_segment() {
2654            self.error_at_current("FORM-101", "expected a type name".to_string());
2655            return;
2656        }
2657        self.start(SyntaxKind::PathType);
2658        self.qualified_name();
2659        self.builder.finish_node();
2660    }
2661
2662    /// `QualifiedName = 'ident' ('.' 'ident')*`
2663    fn qualified_name(&mut self) {
2664        if !self.at_path_segment() {
2665            self.error_at_current("FORM-101", "expected a name".to_string());
2666            return;
2667        }
2668        self.start(SyntaxKind::QualifiedName);
2669        self.bump();
2670        while self.at(SyntaxKind::Dot) {
2671            self.bump();
2672            if self.at_path_segment() {
2673                self.bump();
2674            } else {
2675                self.error_at_current("FORM-101", "expected a name after `.`".to_string());
2676                break;
2677            }
2678        }
2679        self.builder.finish_node();
2680    }
2681}
2682
2683#[cfg(test)]
2684mod tests {
2685    use super::*;
2686
2687    const FIXTURE: &str = include_str!("../fixtures/walking_skeleton.typl");
2688
2689    #[test]
2690    fn fixture_round_trips_losslessly() {
2691        let parse = parse(FIXTURE, Profile::Typl);
2692        assert_eq!(
2693            parse.syntax().text().to_string(),
2694            FIXTURE,
2695            "the tree text must reproduce the source byte for byte",
2696        );
2697        assert!(
2698            parse.errors().is_empty(),
2699            "the fixture must parse without errors, got: {:?}",
2700            parse.errors(),
2701        );
2702    }
2703
2704    #[test]
2705    fn mangled_input_round_trips_and_reports_errors() {
2706        let input = "type 123 :: [";
2707        let parse = parse(input, Profile::Typl);
2708        assert_eq!(
2709            parse.syntax().text().to_string(),
2710            input,
2711            "recovery must not drop any text",
2712        );
2713        assert!(
2714            !parse.errors().is_empty(),
2715            "broken input must produce at least one error",
2716        );
2717    }
2718
2719    #[test]
2720    fn parse_equality_is_green_node_identity() {
2721        assert_eq!(
2722            parse(FIXTURE, Profile::Typl),
2723            parse(FIXTURE, Profile::Typl),
2724            "parsing identical input twice must compare equal",
2725        );
2726        assert_ne!(
2727            parse(FIXTURE, Profile::Typl),
2728            parse("package fixtures\ntype X: m", Profile::Typl),
2729            "parsing different input must not compare equal",
2730        );
2731    }
2732
2733    /// The codes of the recorded errors, in source order.
2734    fn error_codes(input: &str) -> Vec<&'static str> {
2735        parse(input, Profile::Typl)
2736            .errors()
2737            .iter()
2738            .map(|e| e.code)
2739            .collect()
2740    }
2741
2742    #[test]
2743    fn missing_package_flags_form_104() {
2744        let codes = error_codes("type Speed: km/h\n");
2745        assert_eq!(codes, vec!["FORM-104"]);
2746    }
2747
2748    #[test]
2749    fn leading_zero_integer_flags_form_005() {
2750        let codes = error_codes("package p\nconst BAD = 042\n");
2751        assert_eq!(codes, vec!["FORM-005"]);
2752    }
2753
2754    #[test]
2755    fn duration_literal_flags_typl_302() {
2756        let parse = parse("package p\nconst BAD = 10ms\n", Profile::Typl);
2757        assert_eq!(
2758            parse.errors().iter().map(|e| e.code).collect::<Vec<_>>(),
2759            vec!["TYPL-302"],
2760        );
2761        assert_eq!(
2762            parse.syntax().text().to_string(),
2763            "package p\nconst BAD = 10ms\n",
2764            "the rejected duration token must stay in the tree",
2765        );
2766    }
2767
2768    #[test]
2769    fn stray_at_sigil_flags_typl_302() {
2770        let codes = error_codes("package p\n@\n");
2771        assert_eq!(codes, vec!["TYPL-302"]);
2772    }
2773
2774    // Under `Profile::Ridl` durations and `@` are ordinary tokens — no TYPL-302
2775    // fires anywhere.
2776    #[test]
2777    fn duration_and_at_draw_no_typl_302_under_ridl() {
2778        let parsed = parse("package p\nconst BAD = 10ms\n", Profile::Ridl);
2779        assert!(
2780            parsed.errors().iter().all(|e| e.code != "TYPL-302"),
2781            "no TYPL-302 under Ridl, got: {:?}",
2782            parsed.errors(),
2783        );
2784        let parsed = parse("package p\n@\n", Profile::Ridl);
2785        assert_eq!(
2786            parsed.errors().iter().map(|e| e.code).collect::<Vec<_>>(),
2787            vec!["FORM-102"],
2788            "a stray `@` under Ridl is a generic unexpected token",
2789        );
2790    }
2791
2792    // An interaction keyword at declaration-start in a `.typl` parse is the
2793    // profile boundary — TYPL-304, recovering like FORM-105 does (ErrorNode,
2794    // resync at the next top-level keyword).
2795    #[test]
2796    fn interaction_keyword_in_typl_flags_typl_304_and_recovers() {
2797        let input = "package p\ninterface X {}\ntype Fine: m\n";
2798        let parsed = parse(input, Profile::Typl);
2799        assert_eq!(
2800            parsed.syntax().text().to_string(),
2801            input,
2802            "TYPL-304 recovery must stay lossless",
2803        );
2804        assert_eq!(
2805            parsed.errors().iter().map(|e| e.code).collect::<Vec<_>>(),
2806            vec!["TYPL-304"],
2807        );
2808        assert_eq!(def_names_in(Profile::Typl, input), vec!["Fine"]);
2809
2810        // The same boundary fires for every interaction kind.
2811        let codes: Vec<_> = parse("package p\nsignal speed: Speed\n", Profile::Typl)
2812            .errors()
2813            .iter()
2814            .map(|e| e.code)
2815            .collect();
2816        assert_eq!(codes, vec!["TYPL-304"]);
2817    }
2818
2819    // The stream grammar parses under both profiles; in a `.typl` parse a `<T>`
2820    // in type position is the profile boundary — TYPL-301 — and the StreamType
2821    // node is still built, losslessly.
2822    #[test]
2823    fn stream_type_in_typl_flags_typl_301_and_keeps_parsing() {
2824        let input = "package p\nstruct S {\n  f : <Frame>\n  g : integer [0..1]\n}\n";
2825        let parsed = parse(input, Profile::Typl);
2826        assert_eq!(
2827            parsed.syntax().text().to_string(),
2828            input,
2829            "the TYPL-301 boundary must stay lossless",
2830        );
2831        assert_eq!(
2832            parsed.errors().iter().map(|e| e.code).collect::<Vec<_>>(),
2833            vec!["TYPL-301"],
2834        );
2835        let has_stream = parsed
2836            .syntax()
2837            .descendants()
2838            .any(|node| node.kind() == SyntaxKind::StreamType);
2839        assert!(has_stream, "the stream still parses into a StreamType node");
2840
2841        // Under `Profile::Ridl` the same field parses with no error at all —
2842        // stream placement is checker scope there.
2843        let parsed = parse(input, Profile::Ridl);
2844        assert!(
2845            parsed.errors().is_empty(),
2846            "no parser error under Ridl, got: {:?}",
2847            parsed.errors(),
2848        );
2849    }
2850
2851    // A reserved word of another profile at declaration-start in a `.ridl` parse
2852    // is RIDL-403, with the same recovery.
2853    #[test]
2854    fn reserved_word_in_ridl_flags_ridl_403_and_recovers() {
2855        let input = "package p\nmodel X {}\ntype Fine: m\n";
2856        let parsed = parse(input, Profile::Ridl);
2857        assert_eq!(
2858            parsed.syntax().text().to_string(),
2859            input,
2860            "RIDL-403 recovery must stay lossless",
2861        );
2862        assert_eq!(
2863            parsed.errors().iter().map(|e| e.code).collect::<Vec<_>>(),
2864            vec!["RIDL-403"],
2865        );
2866        assert_eq!(def_names_in(Profile::Ridl, input), vec!["Fine"]);
2867    }
2868
2869    // rsdl reference §2, the boundary seen from the other two profiles: an
2870    // rsdl declaration in a `.typl` file draws the generic FORM-102 (the typl
2871    // boundary names ridl words only), and in a `.ridl` file RIDL-403. Both
2872    // recover at the next top-level keyword.
2873    #[test]
2874    fn rsdl_declaration_in_typl_and_ridl_draws_their_existing_codes() {
2875        let input = "package p\ndistribution Adas { Cruise }\ntype Fine: m\n";
2876        for (profile, code) in [(Profile::Typl, "FORM-102"), (Profile::Ridl, "RIDL-403")] {
2877            let parsed = parse(input, profile);
2878            assert_eq!(parsed.syntax().text().to_string(), input);
2879            assert_eq!(
2880                parsed.errors().iter().map(|e| e.code).collect::<Vec<_>>(),
2881                vec![code],
2882                "under {profile:?}",
2883            );
2884            assert_eq!(
2885                def_names_in(profile, input),
2886                vec!["Fine"],
2887                "under {profile:?}"
2888            );
2889        }
2890    }
2891
2892    // rsdl reference §2 and §3.1: under the rsdl profile the five declarations
2893    // parse clean, and a declaration of another profile is RSDL-604 — once per
2894    // declaration, with the rsdl declaration after it still parsed.
2895    #[test]
2896    fn rsdl_profile_parses_its_declarations_and_rejects_the_others() {
2897        let clean = "package p\nsystem Vehicle { Cruise, veh.diag.access }\n";
2898        assert_eq!(parse(clean, Profile::Rsdl).errors(), &[]);
2899
2900        for foreign in [
2901            "type Speed: km/h",
2902            "internal type Speed: km/h",
2903            "struct S { a: integer [0..1] }",
2904            "interface I { signal s: S @10ms }",
2905            "service veh.a.b : I",
2906        ] {
2907            let input = format!("package p\n{foreign}\nsystem Vehicle {{ Cruise }}\n");
2908            let parsed = parse(&input, Profile::Rsdl);
2909            assert_eq!(parsed.syntax().text().to_string(), input);
2910            assert_eq!(
2911                parsed.errors().iter().map(|e| e.code).collect::<Vec<_>>(),
2912                vec!["RSDL-604"],
2913                "`{foreign}`",
2914            );
2915            assert!(
2916                parsed
2917                    .syntax()
2918                    .children()
2919                    .any(|node| node.kind() == SyntaxKind::SystemDef),
2920                "the system after `{foreign}` still parses",
2921            );
2922        }
2923    }
2924
2925    // rsdl reference §5: a backend key `namespace.key` is an rsdl attribute
2926    // key. Under ridl the `.` stays an unexpected token.
2927    #[test]
2928    fn a_backend_key_parses_under_rsdl_only() {
2929        let rsdl = "package p\nsystem V [ linux.cpuset = (2, 3), linux.realtime ] { A }\n";
2930        assert_eq!(parse(rsdl, Profile::Rsdl).errors(), &[]);
2931
2932        let ridl = "package p\ninterface I {\n  command c() [ linux.cpuset = 1 ]\n}\n";
2933        assert_eq!(
2934            parse(ridl, Profile::Ridl)
2935                .errors()
2936                .iter()
2937                .map(|e| e.code)
2938                .collect::<Vec<_>>(),
2939            vec!["FORM-102"],
2940        );
2941    }
2942
2943    /// Recovery from a rejected return type stops *before* a sync token — it
2944    /// never consumes the construct that follows.
2945    ///
2946    /// `return_type` reports and then recovers, and `recover` always bumps the
2947    /// current token, so without the `!is_return_sync(current)` guard a
2948    /// `query a() :` written immediately before a `}` eats the interface's own
2949    /// closing brace and the file then reports an unclosed `{`. Each row here
2950    /// is one member of the sync set, and each asserts on what *survived* — a
2951    /// diagnostic-count assertion cannot see over-consumption, because
2952    /// swallowing the `}` produces one extra diagnostic and swallowing an
2953    /// interaction produces none at all.
2954    #[test]
2955    fn a_rejected_return_type_never_consumes_the_construct_after_it() {
2956        for (sync, body, survivor) in [
2957            (
2958                "the next interaction keyword",
2959                "  query broken() :
2960  signal speed : Speed @10ms
2961",
2962                SyntaxKind::SignalDef,
2963            ),
2964            (
2965                "a `reserved` tombstone",
2966                "  query broken() :
2967  reserved legacyTemp
2968",
2969                SyntaxKind::ReservedEntry,
2970            ),
2971            (
2972                "the body's own closing brace",
2973                "  query broken() :
2974",
2975                SyntaxKind::InterfaceDef,
2976            ),
2977        ] {
2978            let input = format!(
2979                "package p
2980interface I {{
2981{body}}}
2982"
2983            );
2984            let parsed = parse(&input, Profile::Ridl);
2985            assert_eq!(
2986                parsed.syntax().text().to_string(),
2987                input,
2988                "{sync}: recovery must stay lossless",
2989            );
2990            assert_eq!(
2991                parsed.errors().iter().map(|e| e.code).collect::<Vec<_>>(),
2992                vec!["FORM-101"],
2993                "{sync}: the broken return type is the only diagnostic — an                  unclosed `{{` here means the `}}` was consumed",
2994            );
2995            assert!(
2996                parsed
2997                    .syntax()
2998                    .descendants()
2999                    .any(|node| node.kind() == survivor),
3000                "{sync}: the {survivor:?} after the broken return type is gone",
3001            );
3002        }
3003    }
3004
3005    #[test]
3006    fn pathological_type_nesting_does_not_panic_or_drop_text() {
3007        let input = format!("package p\nstruct S {{ f : {} }}\n", "[".repeat(300));
3008        let parse = parse(&input, Profile::Typl);
3009        assert_eq!(
3010            parse.syntax().text().to_string(),
3011            input,
3012            "deep nesting must stay lossless",
3013        );
3014        assert!(!parse.errors().is_empty());
3015    }
3016
3017    #[test]
3018    fn pathological_question_repetition_does_not_panic_or_drop_text() {
3019        // Each `?` wraps one OptionalType; an unbounded run used to build a
3020        // tree deep enough to overflow the stack on traversal or drop.
3021        let input = format!("package p\nstruct S {{ f : T{} }}\n", "?".repeat(30_000));
3022        let parse = parse(&input, Profile::Typl);
3023        assert_eq!(
3024            parse.syntax().text().to_string(),
3025            input,
3026            "the capped optional run must stay lossless",
3027        );
3028        assert_eq!(
3029            parse.errors().iter().map(|e| e.code).collect::<Vec<_>>(),
3030            vec!["FORM-102"],
3031            "the cap must report exactly one error",
3032        );
3033    }
3034
3035    #[test]
3036    fn misplaced_error_modifier_parses_clean() {
3037        // TYPL-212 is checker scope: the grammar admits `error` on every
3038        // definition kind.
3039        let codes = error_codes("package p\nerror type Legacy : integer [0..1]\n");
3040        assert_eq!(codes, Vec::<&str>::new());
3041    }
3042
3043    #[test]
3044    fn error_ranges_cover_the_offending_token() {
3045        // `042` sits at bytes 22..25 of the input.
3046        let parse = parse("package p\nconst BAD = 042\n", Profile::Typl);
3047        let error = &parse.errors()[0];
3048        assert_eq!(error.code, "FORM-005");
3049        assert_eq!(
3050            error.range,
3051            TextRange::new(TextSize::new(22), TextSize::new(25)),
3052        );
3053    }
3054
3055    /// The declared names of every top-level definition, in source order —
3056    /// the recovery proof that real declaration nodes survive garbage.
3057    fn def_names(input: &str) -> Vec<String> {
3058        def_names_in(Profile::Typl, input)
3059    }
3060
3061    /// [`def_names`] under an explicit profile.
3062    fn def_names_in(profile: Profile, input: &str) -> Vec<String> {
3063        use crate::ast::{AstNode, HasName, SourceFile};
3064        let file = SourceFile::cast(parse(input, profile).syntax()).expect("root is a SourceFile");
3065        file.definitions()
3066            .filter_map(|def| Some(def.name()?.ident_token()?.text().to_string()))
3067            .collect()
3068    }
3069
3070    #[test]
3071    fn top_level_garbage_resyncs_to_the_next_declaration() {
3072        // Valid, then garbage, then valid: the run of stray tokens is wrapped
3073        // in one ErrorNode and the declaration after it is a real node again.
3074        let input = "package p\ntype Good: km/h\n] } garbage 9\ntype AlsoGood: integer [0..1]\n";
3075        let parse = parse(input, Profile::Typl);
3076        assert_eq!(
3077            parse.syntax().text().to_string(),
3078            input,
3079            "resync must stay lossless",
3080        );
3081        assert_eq!(def_names(input), vec!["Good", "AlsoGood"]);
3082        assert_eq!(error_codes(input), vec!["FORM-102"]);
3083    }
3084
3085    #[test]
3086    fn unclosed_brace_resyncs_to_the_next_declaration() {
3087        // A struct body that never closes hands the next `type` back to the
3088        // top level (FORM-103) instead of swallowing it.
3089        let input = "package p\nstruct Broken {\n  gear: integer\n\ntype After: km/h\n";
3090        let parse = parse(input, Profile::Typl);
3091        assert_eq!(
3092            parse.syntax().text().to_string(),
3093            input,
3094            "unclosed-brace recovery must stay lossless",
3095        );
3096        assert_eq!(def_names(input), vec!["Broken", "After"]);
3097        assert_eq!(error_codes(input), vec!["FORM-103"]);
3098    }
3099
3100    #[test]
3101    fn reserved_word_as_name_flags_form_105_and_keeps_parsing() {
3102        // `signal` is a family reserved word (typl reference §1.4); used as a
3103        // type name it is held in an ErrorNode and the backing still parses.
3104        let input = "package p\ntype signal: integer [0..1]\ntype Fine: integer [0..1]\n";
3105        assert_eq!(error_codes(input), vec!["FORM-105"]);
3106        // Only `Fine` has a real Name; `signal` sits in an ErrorNode.
3107        assert_eq!(def_names(input), vec!["Fine"]);
3108    }
3109
3110    #[test]
3111    fn unclosed_brackets_do_not_flood_diagnostics() {
3112        // 300 unclosed `[` in a field type: before the one-error-per-offset
3113        // rule the deep-recursion unwind piled hundreds of diagnostics on the
3114        // stuck tokens. Recovery now collapses the run to a handful.
3115        let input = format!("package p\nstruct S {{ f : {} }}\n", "[".repeat(300));
3116        let parse = parse(&input, Profile::Typl);
3117        assert_eq!(
3118            parse.syntax().text().to_string(),
3119            input,
3120            "the unclosed-bracket flood must stay lossless",
3121        );
3122        assert!(
3123            parse.errors().len() <= 4,
3124            "expected a bounded diagnostic count, got {}: {:?}",
3125            parse.errors().len(),
3126            parse.errors(),
3127        );
3128    }
3129
3130    #[test]
3131    fn duration_in_constraint_reports_both_form_101_and_typl_302() {
3132        // A duration or `@` inside a constraint fails a positional check on the
3133        // same token first; the profile-boundary TYPL-302 must still fire — it
3134        // is exempt from the one-diagnostic-per-offset suppression, so both
3135        // codes are recorded (matching the honest init-position behavior).
3136        assert_eq!(
3137            error_codes("package p\ntype X: integer [0..10ms]\n"),
3138            vec!["FORM-101", "TYPL-302"],
3139        );
3140        assert_eq!(
3141            error_codes("package p\ntype X: integer [10ms]\n"),
3142            vec!["FORM-101", "TYPL-302"],
3143        );
3144        assert_eq!(
3145            error_codes("package p\ntype X: integer [0..5 @ 3]\n"),
3146            vec!["FORM-101", "TYPL-302"],
3147        );
3148    }
3149
3150    #[test]
3151    fn closed_deep_nesting_does_not_flood_diagnostics() {
3152        // A balanced 200-level nested array, deeper than MAX_TYPE_DEPTH: the
3153        // closed shape once produced over a thousand diagnostics on unwind.
3154        let deep = format!("{}integer{}", "[".repeat(200), ";1]".repeat(200));
3155        let input = format!("package p\nstruct S {{ f : {deep} }}\n");
3156        let parse = parse(&input, Profile::Typl);
3157        assert_eq!(
3158            parse.syntax().text().to_string(),
3159            input,
3160            "the deep-nesting flood must stay lossless",
3161        );
3162        assert!(
3163            parse.errors().len() <= 4,
3164            "expected a bounded diagnostic count, got {}: {:?}",
3165            parse.errors().len(),
3166            parse.errors(),
3167        );
3168    }
3169
3170    /// typl Appendix E: `reserved = "reserved" ( camelCase_id | int_lit )`.
3171    /// The literal shapes outside that production draw FORM-102 in every body
3172    /// that admits a tombstone, under both profiles. Before this rule the
3173    /// entry parsed as an ordinary `Literal`, lowered to a slot with neither a
3174    /// name nor a value, and reported nothing.
3175    #[test]
3176    fn ungrammatical_reserved_literal_flags_form_102_in_every_body() {
3177        let bodies = [
3178            (
3179                Profile::Typl,
3180                "struct S {\n  a : X\n  reserved {}\n  b : X\n}",
3181            ),
3182            (
3183                Profile::Typl,
3184                "enum E {\n  A = 1\n  reserved {}\n  B = 2\n}",
3185            ),
3186            (
3187                Profile::Typl,
3188                "union U {\n  a : X\n  reserved {}\n  b : X\n}",
3189            ),
3190            (
3191                Profile::Ridl,
3192                "interface I {\n  signal a : X\n  reserved {}\n  signal b : X\n}",
3193            ),
3194            (
3195                Profile::Ridl,
3196                "service p.svc {\n  signal a : X\n  reserved {}\n  signal b : X\n}",
3197            ),
3198        ];
3199        for (profile, shape) in bodies {
3200            for literal in ["\"oldName\"", "true", "false", "1.5", "-1.5", "-MAX"] {
3201                let input = format!("package p\n{}\n", shape.replace("{}", literal));
3202                let parsed = parse(&input, profile);
3203                assert_eq!(
3204                    parsed.syntax().text().to_string(),
3205                    input,
3206                    "refusing `reserved {literal}` must stay lossless in {shape}",
3207                );
3208                assert_eq!(
3209                    parsed.errors().iter().map(|e| e.code).collect::<Vec<_>>(),
3210                    vec!["FORM-102"],
3211                    "`reserved {literal}` in {shape} must draw exactly one FORM-102",
3212                );
3213                assert!(
3214                    parsed.errors()[0].message.starts_with(
3215                        "a `reserved` tombstone takes a retired member name or a retired \
3216                         integer value, not "
3217                    ),
3218                    "the message must lead with what a tombstone does take, got {:?}",
3219                    parsed.errors()[0].message,
3220                );
3221                // The entry keeps its place as a member and the refused token
3222                // is held losslessly, so the member after it is still parsed
3223                // in position and no ordinal shifts.
3224                let entry = parsed
3225                    .syntax()
3226                    .descendants()
3227                    .find(|node| node.kind() == SyntaxKind::ReservedEntry)
3228                    .expect("the tombstone still forms a ReservedEntry");
3229                assert!(
3230                    entry
3231                        .children()
3232                        .any(|child| child.kind() == SyntaxKind::ErrorNode),
3233                    "the refused literal is held in an ErrorNode, got {entry:#?}",
3234                );
3235                assert!(
3236                    entry
3237                        .children()
3238                        .all(|child| child.kind() != SyntaxKind::Literal),
3239                    "the refused literal must not form a Literal, got {entry:#?}",
3240                );
3241            }
3242        }
3243    }
3244
3245    /// The two grammatical tombstone forms keep parsing, in every body and
3246    /// under both profiles: a name, a bare integer, and a negative integer
3247    /// (`int_lit = "-"? [0-9]+`). Narrowing the production must not narrow it
3248    /// past what Appendix E writes.
3249    #[test]
3250    fn grammatical_reserved_forms_still_parse_in_every_body() {
3251        let bodies = [
3252            (
3253                Profile::Typl,
3254                "struct S {\n  a : X\n  reserved {}\n  b : X\n}",
3255            ),
3256            (
3257                Profile::Typl,
3258                "enum E {\n  A = 1\n  reserved {}\n  B = 2\n}",
3259            ),
3260            (
3261                Profile::Typl,
3262                "union U {\n  a : X\n  reserved {}\n  b : X\n}",
3263            ),
3264            (
3265                Profile::Ridl,
3266                "interface I {\n  signal a : X\n  reserved {}\n  signal b : X\n}",
3267            ),
3268            (
3269                Profile::Ridl,
3270                "service p.svc {\n  signal a : X\n  reserved {}\n  signal b : X\n}",
3271            ),
3272        ];
3273        for (profile, shape) in bodies {
3274            for literal in ["legacyChecksum", "3", "-3"] {
3275                let input = format!("package p\n{}\n", shape.replace("{}", literal));
3276                let parsed = parse(&input, profile);
3277                assert!(
3278                    parsed.errors().is_empty(),
3279                    "`reserved {literal}` in {shape} must parse clean, got {:?}",
3280                    parsed.errors(),
3281                );
3282                let entry = parsed
3283                    .syntax()
3284                    .descendants()
3285                    .find(|node| node.kind() == SyntaxKind::ReservedEntry)
3286                    .expect("the tombstone forms a ReservedEntry");
3287                let kept = entry
3288                    .children()
3289                    .any(|child| matches!(child.kind(), SyntaxKind::Name | SyntaxKind::Literal));
3290                assert!(
3291                    kept,
3292                    "`reserved {literal}` must keep the retired identity, got {entry:#?}",
3293                );
3294            }
3295        }
3296    }
3297
3298    /// ADR-0015 decision 13: commas are required between service shapes,
3299    /// because the list has no closing token — it ends where the next
3300    /// declaration begins. A missing comma therefore ends the list, and the
3301    /// following bare `CamelCase` identifier is reported at the top level,
3302    /// **at the identifier**: the greedy alternative would absorb a mistyped
3303    /// `Struct Foo {` as another shape and surface the error at the `{` with
3304    /// no connection to the mistake.
3305    #[test]
3306    fn a_missing_comma_ends_the_shape_list_at_the_next_identifier() {
3307        let input = "package p\nservice p.svc : DoorControl\nStruct Foo {\n  a : X\n}\n";
3308        let parsed = parse(input, Profile::Ridl);
3309        assert_eq!(
3310            parsed.syntax().text().to_string(),
3311            input,
3312            "the ended list must stay lossless",
3313        );
3314        // The ServiceDef holds exactly the one shape before the missing comma.
3315        let service = parsed
3316            .syntax()
3317            .descendants()
3318            .find(|node| node.kind() == SyntaxKind::ServiceDef)
3319            .expect("the service parses");
3320        assert_eq!(
3321            service
3322                .children()
3323                .filter(|child| child.kind() == SyntaxKind::PathType)
3324                .count(),
3325            1,
3326            "`Struct` must not be absorbed as a second shape, got {service:#?}",
3327        );
3328        // The error points at `Struct` — the identifier itself, where the
3329        // mistake is — never at the `{` after it.
3330        let struct_offset = input.find("Struct").expect("the fixture spells it") as u32;
3331        let error = &parsed.errors()[0];
3332        assert_eq!(
3333            u32::from(error.range.start()),
3334            struct_offset,
3335            "the diagnostic lands on the identifier, got: {:?}",
3336            parsed.errors(),
3337        );
3338    }
3339
3340    /// The trailing comma stays optional (ADR-0015 decision 13), and every
3341    /// shape is a direct `PathType` child of the ServiceDef. `reserved` is not
3342    /// a shape (lock design §9): the list ends at the comma before it, the
3343    /// stray keyword is reported, and nothing after it joins the list.
3344    #[test]
3345    fn a_shape_list_admits_a_trailing_comma_and_ends_before_reserved() {
3346        let input = "package p\nservice p.svc : DoorControl, MotorControl,\n";
3347        let parsed = parse(input, Profile::Ridl);
3348        assert!(
3349            parsed.errors().is_empty(),
3350            "the list parses clean, got: {:?}",
3351            parsed.errors(),
3352        );
3353        assert_eq!(
3354            shape_kinds(&parsed),
3355            [SyntaxKind::PathType, SyntaxKind::PathType]
3356        );
3357
3358        let input = "package p\nservice p.svc : DoorControl, reserved LegacyDiag, MotorControl,\n";
3359        let parsed = parse(input, Profile::Ridl);
3360        assert!(
3361            !parsed.errors().is_empty(),
3362            "`reserved` in a service's list is a parse error",
3363        );
3364        assert_eq!(
3365            shape_kinds(&parsed),
3366            [SyntaxKind::PathType],
3367            "the list ends before the stray keyword",
3368        );
3369    }
3370
3371    /// The kinds of the shape children of the first ServiceDef in `parsed`.
3372    fn shape_kinds(parsed: &Parse) -> Vec<SyntaxKind> {
3373        parsed
3374            .syntax()
3375            .descendants()
3376            .find(|node| node.kind() == SyntaxKind::ServiceDef)
3377            .expect("the service parses")
3378            .children()
3379            .filter(|child| {
3380                matches!(
3381                    child.kind(),
3382                    SyntaxKind::PathType | SyntaxKind::ReservedEntry
3383                )
3384            })
3385            .map(|child| child.kind())
3386            .collect()
3387    }
3388
3389    /// Nothing at all after `reserved` is a missing token, not a wrong one:
3390    /// FORM-101, naming both admitted forms.
3391    #[test]
3392    fn reserved_with_no_target_flags_form_101() {
3393        let parsed = parse("package p\nstruct S {\n  reserved\n}\n", Profile::Typl);
3394        assert_eq!(
3395            parsed.errors().iter().map(|e| e.code).collect::<Vec<_>>(),
3396            vec!["FORM-101"],
3397        );
3398        assert_eq!(
3399            parsed.errors()[0].message,
3400            "expected a name or an integer value",
3401        );
3402    }
3403
3404    // --- operator chains (driftsys/ridl#346) --------------------------------
3405
3406    /// A contract expression in a minimal ridl file.
3407    fn contract_file(expr: &str) -> String {
3408        format!(
3409            "package app\ntype N : integer [0..10]\ninterface I {{\n  command c(a: N) [ require {expr} ] @[..50ms]\n}}\n"
3410        )
3411    }
3412
3413    /// A flat chain of `terms` operands under `op`, closed into a boolean
3414    /// where the operator is not one already.
3415    fn flat_chain(op: &str, terms: usize) -> String {
3416        match op {
3417            "||" | "&&" => vec!["a == 1"; terms].join(&format!(" {op} ")),
3418            "." => format!("a{} == 1", ".b".repeat(terms - 1)),
3419            _ => format!("{} == 1", vec!["a"; terms].join(&format!(" {op} "))),
3420        }
3421    }
3422
3423    /// The deepest node below `root`, counted without recursion, so that
3424    /// measuring a tree cannot itself exhaust the stack.
3425    fn tree_depth(root: &SyntaxNode) -> usize {
3426        let (mut depth, mut deepest) = (0usize, 0usize);
3427        for event in root.preorder() {
3428            match event {
3429                rowan::WalkEvent::Enter(_) => {
3430                    depth += 1;
3431                    deepest = deepest.max(depth);
3432                }
3433                rowan::WalkEvent::Leave(_) => depth -= 1,
3434            }
3435        }
3436        deepest
3437    }
3438
3439    /// Runs `body` on a thread with a 64 MiB stack. Without the fix a chain
3440    /// builds a tree thousands of levels deep, and dropping it recurses; the
3441    /// large stack keeps that from aborting the whole test binary, so the
3442    /// test fails on its assertions instead.
3443    fn on_a_large_stack(body: impl FnOnce() + Send + 'static) {
3444        std::thread::Builder::new()
3445            .stack_size(64 << 20)
3446            .spawn(body)
3447            .expect("spawn the test thread")
3448            .join()
3449            .expect("the test thread must not panic");
3450    }
3451
3452    /// The bound every expression tree must stay within, measured from the
3453    /// file root. A checker pass that recurses over the tree is sized against
3454    /// it; a flat chain once built a tree as deep as its term count.
3455    const TREE_DEPTH_BOUND: usize = 2 * MAX_TYPE_DEPTH;
3456
3457    #[test]
3458    fn a_flat_operator_chain_past_the_depth_limit_draws_one_form_102() {
3459        on_a_large_stack(|| {
3460            for op in ["||", "&&", "+", "-", "*", "/", "%", "."] {
3461                let input = contract_file(&flat_chain(op, 1000));
3462                let parsed = parse(&input, Profile::Ridl);
3463                let form_102 = parsed
3464                    .errors()
3465                    .iter()
3466                    .filter(|error| error.code == "FORM-102")
3467                    .count();
3468                assert_eq!(
3469                    form_102,
3470                    1,
3471                    "a 1000-term `{op}` chain draws exactly one FORM-102: {:?}",
3472                    parsed.errors(),
3473                );
3474                assert_eq!(
3475                    parsed.syntax().text().to_string(),
3476                    input,
3477                    "the refused `{op}` chain must stay lossless",
3478                );
3479                let depth = tree_depth(&parsed.syntax());
3480                assert!(
3481                    depth <= TREE_DEPTH_BOUND,
3482                    "a 1000-term `{op}` chain builds a tree {depth} levels deep",
3483                );
3484            }
3485        });
3486    }
3487
3488    /// The height of the expression tree under `root`: the most expression
3489    /// nodes on one path from the root to a leaf, counted without recursion.
3490    /// Only the expression kinds count, so the declaration nodes above the
3491    /// attribute and the flat `ErrorNode` of a refusal do not.
3492    fn expr_height(root: &SyntaxNode) -> usize {
3493        let is_expr = |kind: SyntaxKind| {
3494            matches!(
3495                kind,
3496                SyntaxKind::BinaryExpr
3497                    | SyntaxKind::PrefixExpr
3498                    | SyntaxKind::MemberExpr
3499                    | SyntaxKind::PathExpr
3500                    | SyntaxKind::ParenExpr
3501                    | SyntaxKind::LiteralExpr
3502            )
3503        };
3504        let (mut depth, mut deepest) = (0usize, 0usize);
3505        for event in root.preorder() {
3506            match event {
3507                rowan::WalkEvent::Enter(node) if is_expr(node.kind()) => {
3508                    depth += 1;
3509                    deepest = deepest.max(depth);
3510                }
3511                rowan::WalkEvent::Leave(node) if is_expr(node.kind()) => depth -= 1,
3512                _ => {}
3513            }
3514        }
3515        deepest
3516    }
3517
3518    /// The bound as an author meets it, over every shape that raises the
3519    /// height, at every size around the limit and one far past it: the tree
3520    /// built is never higher than `MAX_TYPE_DEPTH`, and FORM-102 is drawn
3521    /// exactly when the complete tree would not fit. Each generator states
3522    /// the height its complete tree has at `n`; where that fits, the built
3523    /// tree must have exactly that height, which checks the formula. The
3524    /// right-leaning shapes are the ones a bound that forgets to parse a
3525    /// right operand one level deeper would let through; the group and
3526    /// prefix towers are the ones a bound that opens a group or a prefix
3527    /// without room for a leaf below it would let through.
3528    #[test]
3529    fn the_expression_height_bound_holds_for_every_shape() {
3530        type Shape = (&'static str, fn(usize) -> String, fn(usize) -> usize);
3531        let shapes: [Shape; 15] = [
3532            ("left chain", |n| vec!["a"; n].join(" + "), |n| n),
3533            (
3534                "`||` chain of comparisons",
3535                |n| vec!["a == 1"; n].join(" || "),
3536                |n| n + 1,
3537            ),
3538            (
3539                "member steps",
3540                |n| format!("a{}", ".b".repeat(n - 1)),
3541                |n| n,
3542            ),
3543            (
3544                "groups around a leaf",
3545                |n| format!("{}a{}", "(".repeat(n - 1), ")".repeat(n - 1)),
3546                |n| n,
3547            ),
3548            (
3549                "groups around a negated leaf",
3550                |n| format!("{}-a{}", "(".repeat(n - 1), ")".repeat(n - 1)),
3551                |n| n + 1,
3552            ),
3553            (
3554                "groups around a `!` leaf",
3555                |n| format!("{}!a{}", "(".repeat(n - 1), ")".repeat(n - 1)),
3556                |n| n + 1,
3557            ),
3558            (
3559                "right-nested chain",
3560                |n| {
3561                    let mut expr = "a".to_string();
3562                    for _ in 1..n {
3563                        expr = format!("a + ({expr})");
3564                    }
3565                    expr
3566                },
3567                |n| 2 * n - 1,
3568            ),
3569            (
3570                "comparison with a grouped chain on the right",
3571                |n| format!("a == ({})", vec!["a"; n].join(" + ")),
3572                |n| n + 2,
3573            ),
3574            (
3575                "`!(` tower",
3576                |n| format!("{}a{}", "!(".repeat(n - 1), ")".repeat(n - 1)),
3577                |n| 2 * n - 1,
3578            ),
3579            (
3580                "`-(` tower",
3581                |n| format!("{}a{}", "-(".repeat(n - 1), ")".repeat(n - 1)),
3582                |n| 2 * n - 1,
3583            ),
3584            (
3585                "prefixed operands in a chain",
3586                |n| vec!["-a"; n].join(" * "),
3587                |n| n + 1,
3588            ),
3589            (
3590                "grouped member steps as a right operand",
3591                |n| format!("a + (a{})", ".b".repeat(n - 1)),
3592                |n| n + 2,
3593            ),
3594            (
3595                "`||` right-nested",
3596                |n| {
3597                    let mut expr = "a == 1".to_string();
3598                    for _ in 1..n {
3599                        expr = format!("a == 1 || ({expr})");
3600                    }
3601                    expr
3602                },
3603                |n| 2 * n,
3604            ),
3605            (
3606                "a deep right operand, then a long left chain",
3607                |n| {
3608                    format!(
3609                        "a + ({}a{}){}",
3610                        "(".repeat(60),
3611                        ")".repeat(60),
3612                        " + a".repeat(n - 1)
3613                    )
3614                },
3615                // The group written around the 60 counts too: 61 groups, a
3616                // leaf, and the wrap, then one level per further term.
3617                |n| n + 62,
3618            ),
3619            (
3620                "a prefix tower, then a chain",
3621                |n| {
3622                    format!(
3623                        "{}a == 1{}{}",
3624                        "!(".repeat(30),
3625                        ")".repeat(30),
3626                        " && a == 1".repeat(n - 1)
3627                    )
3628                },
3629                |n| n + 61,
3630            ),
3631        ];
3632        for (shape, build, complete_height) in shapes {
3633            // The smallest sizes check the formula; the sizes whose complete
3634            // height is within eight levels of the limit check the bound on
3635            // both sides of it; 300 is far past it.
3636            let mut sizes: Vec<usize> = (1..)
3637                .take_while(|&n| complete_height(n) <= MAX_TYPE_DEPTH + 8)
3638                .filter(|&n| n <= 3 || complete_height(n) + 8 >= MAX_TYPE_DEPTH)
3639                .collect();
3640            sizes.push(300);
3641            for n in sizes {
3642                let input = contract_file(&build(n));
3643                let parsed = parse(&input, Profile::Ridl);
3644                let complete = complete_height(n);
3645                let built = expr_height(&parsed.syntax());
3646                let form_102 = parsed
3647                    .errors()
3648                    .iter()
3649                    .filter(|error| error.code == "FORM-102")
3650                    .count();
3651                assert!(
3652                    built <= MAX_TYPE_DEPTH,
3653                    "the {shape} at n = {n} builds a tree {built} high",
3654                );
3655                assert_eq!(
3656                    parsed.syntax().text().to_string(),
3657                    input,
3658                    "{shape} at n = {n}"
3659                );
3660                if complete <= MAX_TYPE_DEPTH {
3661                    assert!(
3662                        parsed.errors().is_empty(),
3663                        "the {shape} at n = {n} fits in {complete} levels: {:?}",
3664                        parsed.errors(),
3665                    );
3666                    assert_eq!(
3667                        built, complete,
3668                        "the {shape} at n = {n} builds its complete tree"
3669                    );
3670                } else {
3671                    assert_eq!(
3672                        form_102,
3673                        1,
3674                        "the {shape} at n = {n} would be {complete} high: {:?}",
3675                        parsed.errors(),
3676                    );
3677                }
3678            }
3679        }
3680    }
3681
3682    /// Past the limit the diagnostics are exactly one FORM-102: the refused
3683    /// group is consumed through its own `)` together with the groups inside
3684    /// it, the chain after the outermost group goes flat without a second
3685    /// diagnostic, and the next interaction parses as usual.
3686    #[test]
3687    fn a_refused_group_draws_only_form_102_and_the_next_interaction_parses() {
3688        use crate::ast::AstNode;
3689        let groups = MAX_TYPE_DEPTH + 2;
3690        let input = format!(
3691            "package app\ntype N : integer [0..10]\ninterface I {{\n  \
3692             command c(a: N) [ require {}(a) + (a){} + a + a ] @[..50ms]\n  \
3693             command d(a: N) [ require a == 1 ] @[..50ms]\n}}\n",
3694            "(".repeat(groups),
3695            ")".repeat(groups),
3696        );
3697        let parsed = parse(&input, Profile::Ridl);
3698        let codes: Vec<&str> = parsed.errors().iter().map(|error| error.code).collect();
3699        assert_eq!(codes, vec!["FORM-102"]);
3700        assert_eq!(parsed.syntax().text().to_string(), input);
3701        let root = parsed.syntax();
3702        assert!(expr_height(&root) <= MAX_TYPE_DEPTH);
3703        assert_eq!(
3704            root.descendants()
3705                .filter(|node| node.kind() == SyntaxKind::CommandDef)
3706                .count(),
3707            2,
3708        );
3709        let last = root
3710            .descendants()
3711            .filter_map(crate::ast::Attribute::cast)
3712            .last()
3713            .expect("the second command's attribute");
3714        assert_eq!(
3715            last.expr().expect("its expression").syntax().text(),
3716            "a == 1",
3717        );
3718    }
3719
3720    /// A chain whose left operand is missing still keeps the bound: the
3721    /// wrap holds only its right operand, and counting the missing side as
3722    /// zero high would let the leaf below the deepest wrap sit one level
3723    /// past the limit. The shapes put the wrap at the deepest group the
3724    /// bound admits.
3725    #[test]
3726    fn a_missing_left_operand_keeps_the_bound() {
3727        for inner in ["+ a", "== a", "|| a == 1", "+ a == a"] {
3728            let groups = MAX_TYPE_DEPTH - 1;
3729            let input = contract_file(&format!(
3730                "{}{inner}{}",
3731                "(".repeat(groups),
3732                ")".repeat(groups)
3733            ));
3734            let parsed = parse(&input, Profile::Ridl);
3735            let built = expr_height(&parsed.syntax());
3736            assert!(
3737                built <= MAX_TYPE_DEPTH,
3738                "`{inner}` under {groups} groups builds a tree {built} high: {:?}",
3739                parsed.errors(),
3740            );
3741            assert!(
3742                parsed.errors().iter().any(|error| error.code == "FORM-101"),
3743                "the missing operand is reported: {:?}",
3744                parsed.errors(),
3745            );
3746            assert_eq!(parsed.syntax().text().to_string(), input);
3747        }
3748    }
3749
3750    /// A refused expression ends where the expression would have ended: the
3751    /// next attribute in the block — after a comma, a key on its own line, or
3752    /// an `ensure` on the same line with no comma — and the later
3753    /// declarations are parsed as usual, including a syntax error in one of
3754    /// them. Each overlong expression draws its own FORM-102. The stop at a
3755    /// `require`, a reserved word and a key after each kind of operand is
3756    /// pinned by the test after this one.
3757    #[test]
3758    fn a_refused_expression_does_not_swallow_what_follows_it() {
3759        use crate::ast::AstNode;
3760        let long = flat_chain("+", 200);
3761        let input = format!(
3762            "package app\ntype N : integer [0..10]\ninterface I {{\n  \
3763             command c(a: N) [ require {long}, require a == 1 ] @[..50ms]\n  \
3764             command d(a: N) [ require {long}\n    key = 1\n    require a == 1 ] @[..50ms]\n  \
3765             command e(a: N) [ require a == ] @[..50ms]\n  \
3766             command f(a: N) [ require {long} ensure a == 1 ] @[..50ms]\n}}\n"
3767        );
3768        let parsed = parse(&input, Profile::Ridl);
3769        assert_eq!(parsed.syntax().text().to_string(), input);
3770        let codes: Vec<&str> = parsed.errors().iter().map(|error| error.code).collect();
3771        assert_eq!(
3772            codes.iter().filter(|code| **code == "FORM-102").count(),
3773            3,
3774            "one FORM-102 per overlong expression: {codes:?}",
3775        );
3776        assert!(
3777            codes.contains(&"FORM-101"),
3778            "the syntax error in the later declaration is still reported: {codes:?}",
3779        );
3780        let spans: Vec<_> = parsed
3781            .errors()
3782            .iter()
3783            .filter(|error| error.code == "FORM-102")
3784            .map(|error| error.range.start())
3785            .collect();
3786        assert!(
3787            spans.windows(2).all(|pair| pair[0] < pair[1]),
3788            "each FORM-102 sits at its own expression: {spans:?}",
3789        );
3790        let root = parsed.syntax();
3791        assert_eq!(
3792            root.descendants()
3793                .filter(|node| node.kind() == SyntaxKind::CommandDef)
3794                .count(),
3795            4,
3796        );
3797        let attributes: Vec<crate::ast::Attribute> = root
3798            .descendants()
3799            .filter_map(crate::ast::Attribute::cast)
3800            .collect();
3801        assert_eq!(attributes.len(), 8, "{attributes:?}");
3802        assert_eq!(
3803            attributes
3804                .iter()
3805                .filter(|attribute| attribute
3806                    .expr()
3807                    .is_some_and(|expr| expr.syntax().text() == "a == 1"))
3808                .count(),
3809            3,
3810            "the `a == 1` after each refused chain is its own attribute",
3811        );
3812        assert!(
3813            attributes.iter().any(|attribute| attribute
3814                .syntax()
3815                .text()
3816                .to_string()
3817                .starts_with("key")),
3818            "the key after the refused chain is its own attribute",
3819        );
3820    }
3821
3822    /// Each way the skipped run of a refused chain can end before the next
3823    /// attribute, with no comma: a `require` directly after the chain, a
3824    /// reserved word in key position (which still draws its own FORM-105),
3825    /// and a key after a chain that ends in a number, a name or a group.
3826    #[test]
3827    fn a_refused_chain_stops_at_every_kind_of_next_attribute() {
3828        use crate::ast::AstNode;
3829        let numbers = flat_chain("+", 200);
3830        let names = vec!["a"; 200].join(" + ");
3831        let groups = vec!["(a)"; 200].join(" + ");
3832        let cases = [
3833            ("a `require`", format!("{numbers}\n    require a == 1")),
3834            ("a reserved word", format!("{numbers}\n    states = 1")),
3835            ("a key after a number", format!("{numbers}\n    key = 1")),
3836            ("a key after a name", format!("{names}\n    key = 1")),
3837            ("a key after a group", format!("{groups}\n    key = 1")),
3838        ];
3839        for (case, body) in cases {
3840            let input = format!(
3841                "package app\ntype N : integer [0..10]\ninterface I {{\n  \
3842                 command c(a: N) [ require {body} ] @[..50ms]\n}}\n"
3843            );
3844            let parsed = parse(&input, Profile::Ridl);
3845            assert_eq!(parsed.syntax().text().to_string(), input, "{case}");
3846            let codes: Vec<&str> = parsed.errors().iter().map(|error| error.code).collect();
3847            let expected: &[&str] = if case == "a reserved word" {
3848                &["FORM-102", "FORM-105"]
3849            } else {
3850                &["FORM-102"]
3851            };
3852            assert_eq!(codes, expected, "{case}");
3853            let attributes: Vec<String> = parsed
3854                .syntax()
3855                .descendants()
3856                .filter_map(crate::ast::Attribute::cast)
3857                .map(|attribute| attribute.syntax().text().to_string())
3858                .collect();
3859            assert_eq!(
3860                attributes.len(),
3861                2,
3862                "{case}: the next attribute is its own node: {attributes:?}",
3863            );
3864            let next = attributes[1].trim();
3865            let starts = match case {
3866                "a `require`" => "require a == 1",
3867                "a reserved word" => "states = 1",
3868                _ => "key = 1",
3869            };
3870            assert_eq!(next, starts, "{case}");
3871        }
3872    }
3873
3874    #[test]
3875    fn a_long_chain_under_the_depth_limit_parses_clean() {
3876        for op in ["||", "&&", "+", "-", "*", "/", "%", "."] {
3877            let input = contract_file(&flat_chain(op, 100));
3878            let parsed = parse(&input, Profile::Ridl);
3879            assert!(
3880                parsed.errors().is_empty(),
3881                "a 100-term `{op}` chain is accepted: {:?}",
3882                parsed.errors(),
3883            );
3884        }
3885    }
3886
3887    #[test]
3888    fn chains_stacked_through_parentheses_and_precedence_stay_bounded() {
3889        on_a_large_stack(|| {
3890            // Each parenthesis level closes a chain of its own, so a bound
3891            // that is charged while a chain is parsed and released when it
3892            // returns lets the chains stack on the left spine.
3893            let mut nested = "a".to_string();
3894            for _ in 0..120 {
3895                nested = format!("({nested}{})", " + a".repeat(119));
3896            }
3897            // One chain per precedence level, each the first operand of the
3898            // next: member, multiplicative, additive, comparison, `&&`, `||`.
3899            let ladder = format!(
3900                "a{}{}{} == a{}{}",
3901                ".b".repeat(119),
3902                " * a".repeat(119),
3903                " + a".repeat(119),
3904                " && a == a".repeat(119),
3905                " || a == a".repeat(119),
3906            );
3907            for (shape, expr) in [("nested", format!("{nested} == 1")), ("ladder", ladder)] {
3908                let input = contract_file(&expr);
3909                let parsed = parse(&input, Profile::Ridl);
3910                assert!(
3911                    parsed.errors().iter().any(|error| error.code == "FORM-102"),
3912                    "the {shape} shape draws FORM-102: {:?}",
3913                    parsed.errors(),
3914                );
3915                assert!(
3916                    parsed.errors().len() <= 4,
3917                    "the {shape} shape floods {} diagnostics",
3918                    parsed.errors().len(),
3919                );
3920                assert_eq!(parsed.syntax().text().to_string(), input);
3921                let depth = tree_depth(&parsed.syntax());
3922                assert!(
3923                    depth <= TREE_DEPTH_BOUND,
3924                    "the {shape} shape builds a tree {depth} levels deep",
3925                );
3926            }
3927        });
3928    }
3929}