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