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