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