Skip to main content

ridl_syntax/
parser.rs

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