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