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