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