Skip to main content

ridl_syntax/
ast.rs

1//! The typed AST over the rowan tree (ADR-0007
2//! decision 1).
3//!
4//! The node structs and their mechanical accessors live in the `generated`
5//! module,
6//! written by `cargo xtask codegen` from `family.ungram` and committed; an
7//! xtask drift test keeps the two in sync. This module adds the hand-written
8//! layer the generator does not cover:
9//!
10//! - the [`AstNode`] trait and the [`AstChildren`] iterator;
11//! - the enums over pure node alternations ([`Definition`], [`Backing`],
12//!   [`StructMember`], [`FieldType`]);
13//! - the [`HasName`] / [`HasModifiers`] / [`HasDocComments`] trait trio
14//!   shared by the definition kinds of every profile;
15//! - the position-sensitive accessors whose roles are told apart by anchor
16//!   tokens ([`Constraint::min`] and friends, [`ConstDef::regex`]).
17//!
18//! Every accessor walks child kinds and returns `Option`/empty iterators on
19//! a malformed tree — it never panics.
20
21mod generated;
22
23pub use generated::*;
24
25use std::marker::PhantomData;
26
27use rowan::TextRange;
28
29use crate::syntax_kind::{RidlLanguage, SyntaxKind, SyntaxNode, SyntaxToken};
30
31/// A typed view over a [`SyntaxNode`] of a known kind.
32pub trait AstNode: Sized {
33    /// Wraps `syntax` if its kind matches this type; `None` otherwise.
34    fn cast(syntax: SyntaxNode) -> Option<Self>;
35
36    /// The underlying untyped node.
37    fn syntax(&self) -> &SyntaxNode;
38}
39
40/// An iterator over the children of a node that cast to `N`, in source
41/// order.
42#[derive(Debug, Clone)]
43pub struct AstChildren<N> {
44    inner: rowan::SyntaxNodeChildren<RidlLanguage>,
45    _marker: PhantomData<N>,
46}
47
48impl<N> AstChildren<N> {
49    fn new(parent: &SyntaxNode) -> Self {
50        Self {
51            inner: parent.children(),
52            _marker: PhantomData,
53        }
54    }
55}
56
57impl<N: AstNode> Iterator for AstChildren<N> {
58    type Item = N;
59
60    fn next(&mut self) -> Option<N> {
61        self.inner.by_ref().find_map(N::cast)
62    }
63}
64
65/// Child-lookup helpers shared by the generated accessors and the
66/// hand-written ones below.
67mod support {
68    use super::{AstChildren, AstNode};
69    use crate::syntax_kind::{SyntaxKind, SyntaxNode, SyntaxToken};
70
71    /// The first child of `parent` that casts to `N`.
72    pub(super) fn child<N: AstNode>(parent: &SyntaxNode) -> Option<N> {
73        parent.children().find_map(N::cast)
74    }
75
76    /// The `n`-th child of `parent` that casts to `N` (0-based).
77    pub(super) fn nth_child<N: AstNode>(parent: &SyntaxNode, n: usize) -> Option<N> {
78        parent.children().filter_map(N::cast).nth(n)
79    }
80
81    /// All children of `parent` that cast to `N`, in source order.
82    pub(super) fn children<N: AstNode>(parent: &SyntaxNode) -> AstChildren<N> {
83        AstChildren::new(parent)
84    }
85
86    /// The first direct token child of `parent` with the given kind.
87    pub(super) fn token(parent: &SyntaxNode, kind: SyntaxKind) -> Option<SyntaxToken> {
88        parent
89            .children_with_tokens()
90            .filter_map(|element| element.into_token())
91            .find(|token| token.kind() == kind)
92    }
93}
94
95// --- enums over node alternations (`family.ungram` alternation rules) ------
96
97/// One top-level definition — the `Definition` alternation.
98#[derive(Debug, Clone, PartialEq, Eq, Hash)]
99pub enum Definition {
100    Type(TypeDef),
101    Const(ConstDef),
102    Struct(StructDef),
103    Enum(EnumDef),
104    EnumSet(EnumSetDef),
105    Union(UnionDef),
106}
107
108impl AstNode for Definition {
109    fn cast(syntax: SyntaxNode) -> Option<Self> {
110        match syntax.kind() {
111            SyntaxKind::TypeDef => TypeDef::cast(syntax).map(Self::Type),
112            SyntaxKind::ConstDef => ConstDef::cast(syntax).map(Self::Const),
113            SyntaxKind::StructDef => StructDef::cast(syntax).map(Self::Struct),
114            SyntaxKind::EnumDef => EnumDef::cast(syntax).map(Self::Enum),
115            SyntaxKind::EnumSetDef => EnumSetDef::cast(syntax).map(Self::EnumSet),
116            SyntaxKind::UnionDef => UnionDef::cast(syntax).map(Self::Union),
117            _ => None,
118        }
119    }
120
121    fn syntax(&self) -> &SyntaxNode {
122        match self {
123            Self::Type(it) => it.syntax(),
124            Self::Const(it) => it.syntax(),
125            Self::Struct(it) => it.syntax(),
126            Self::Enum(it) => it.syntax(),
127            Self::EnumSet(it) => it.syntax(),
128            Self::Union(it) => it.syntax(),
129        }
130    }
131}
132
133/// The backing of a `type` definition — the `Backing` alternation
134/// (typl reference §4–§5).
135#[derive(Debug, Clone, PartialEq, Eq, Hash)]
136pub enum Backing {
137    Primitive(PrimitiveType),
138    Unit(UnitExpr),
139}
140
141impl AstNode for Backing {
142    fn cast(syntax: SyntaxNode) -> Option<Self> {
143        match syntax.kind() {
144            SyntaxKind::PrimitiveType => PrimitiveType::cast(syntax).map(Self::Primitive),
145            SyntaxKind::UnitExpr => UnitExpr::cast(syntax).map(Self::Unit),
146            _ => None,
147        }
148    }
149
150    fn syntax(&self) -> &SyntaxNode {
151        match self {
152            Self::Primitive(it) => it.syntax(),
153            Self::Unit(it) => it.syntax(),
154        }
155    }
156}
157
158/// One member of a `struct` body — the `StructMember` alternation
159/// (typl reference §7).
160#[derive(Debug, Clone, PartialEq, Eq, Hash)]
161pub enum StructMember {
162    Field(FieldDef),
163    Reserved(ReservedEntry),
164}
165
166impl AstNode for StructMember {
167    fn cast(syntax: SyntaxNode) -> Option<Self> {
168        match syntax.kind() {
169            SyntaxKind::FieldDef => FieldDef::cast(syntax).map(Self::Field),
170            SyntaxKind::ReservedEntry => ReservedEntry::cast(syntax).map(Self::Reserved),
171            _ => None,
172        }
173    }
174
175    fn syntax(&self) -> &SyntaxNode {
176        match self {
177            Self::Field(it) => it.syntax(),
178            Self::Reserved(it) => it.syntax(),
179        }
180    }
181}
182
183/// The type of a field, tuple field, or collection element — the
184/// `FieldType` alternation (typl reference §7, §11–§12).
185#[derive(Debug, Clone, PartialEq, Eq, Hash)]
186pub enum FieldType {
187    Path(PathType),
188    Primitive(PrimitiveType),
189    Tuple(TupleType),
190    Array(ArrayType),
191    Map(MapType),
192    Optional(OptionalType),
193}
194
195impl AstNode for FieldType {
196    fn cast(syntax: SyntaxNode) -> Option<Self> {
197        match syntax.kind() {
198            SyntaxKind::PathType => PathType::cast(syntax).map(Self::Path),
199            SyntaxKind::PrimitiveType => PrimitiveType::cast(syntax).map(Self::Primitive),
200            SyntaxKind::TupleType => TupleType::cast(syntax).map(Self::Tuple),
201            SyntaxKind::ArrayType => ArrayType::cast(syntax).map(Self::Array),
202            SyntaxKind::MapType => MapType::cast(syntax).map(Self::Map),
203            SyntaxKind::OptionalType => OptionalType::cast(syntax).map(Self::Optional),
204            _ => None,
205        }
206    }
207
208    fn syntax(&self) -> &SyntaxNode {
209        match self {
210            Self::Path(it) => it.syntax(),
211            Self::Primitive(it) => it.syntax(),
212            Self::Tuple(it) => it.syntax(),
213            Self::Array(it) => it.syntax(),
214            Self::Map(it) => it.syntax(),
215            Self::Optional(it) => it.syntax(),
216        }
217    }
218}
219
220/// One member of an `interface` body — the `InterfaceMember` alternation
221/// (ridl reference §14.0–§14.1).
222#[derive(Debug, Clone, PartialEq, Eq, Hash)]
223pub enum InterfaceMember {
224    Signal(SignalDef),
225    Event(EventDef),
226    Command(CommandDef),
227    Query(QueryDef),
228    Fixed(FixedDef),
229    Reserved(ReservedEntry),
230}
231
232impl AstNode for InterfaceMember {
233    fn cast(syntax: SyntaxNode) -> Option<Self> {
234        match syntax.kind() {
235            SyntaxKind::SignalDef => SignalDef::cast(syntax).map(Self::Signal),
236            SyntaxKind::EventDef => EventDef::cast(syntax).map(Self::Event),
237            SyntaxKind::CommandDef => CommandDef::cast(syntax).map(Self::Command),
238            SyntaxKind::QueryDef => QueryDef::cast(syntax).map(Self::Query),
239            SyntaxKind::FixedDef => FixedDef::cast(syntax).map(Self::Fixed),
240            SyntaxKind::ReservedEntry => ReservedEntry::cast(syntax).map(Self::Reserved),
241            _ => None,
242        }
243    }
244
245    fn syntax(&self) -> &SyntaxNode {
246        match self {
247            Self::Signal(it) => it.syntax(),
248            Self::Event(it) => it.syntax(),
249            Self::Command(it) => it.syntax(),
250            Self::Query(it) => it.syntax(),
251            Self::Fixed(it) => it.syntax(),
252            Self::Reserved(it) => it.syntax(),
253        }
254    }
255}
256
257/// The type of a command or query parameter — the `ParamType` alternation
258/// (ridl reference §6.1, §7.1, §12): a field type or a stream `<T>`.
259#[derive(Debug, Clone, PartialEq, Eq, Hash)]
260pub enum ParamType {
261    Field(FieldType),
262    Stream(StreamType),
263}
264
265impl AstNode for ParamType {
266    fn cast(syntax: SyntaxNode) -> Option<Self> {
267        if syntax.kind() == SyntaxKind::StreamType {
268            StreamType::cast(syntax).map(Self::Stream)
269        } else {
270            FieldType::cast(syntax).map(Self::Field)
271        }
272    }
273
274    fn syntax(&self) -> &SyntaxNode {
275        match self {
276            Self::Field(it) => it.syntax(),
277            Self::Stream(it) => it.syntax(),
278        }
279    }
280}
281
282/// One expression of the guaranteed subset — the `Expr` alternation
283/// (expr-core specification §3.1, ridl reference §13).
284#[derive(Debug, Clone, PartialEq, Eq, Hash)]
285pub enum Expr {
286    Binary(BinaryExpr),
287    Prefix(PrefixExpr),
288    Member(MemberExpr),
289    Path(PathExpr),
290    Paren(ParenExpr),
291    Literal(LiteralExpr),
292}
293
294impl AstNode for Expr {
295    fn cast(syntax: SyntaxNode) -> Option<Self> {
296        match syntax.kind() {
297            SyntaxKind::BinaryExpr => BinaryExpr::cast(syntax).map(Self::Binary),
298            SyntaxKind::PrefixExpr => PrefixExpr::cast(syntax).map(Self::Prefix),
299            SyntaxKind::MemberExpr => MemberExpr::cast(syntax).map(Self::Member),
300            SyntaxKind::PathExpr => PathExpr::cast(syntax).map(Self::Path),
301            SyntaxKind::ParenExpr => ParenExpr::cast(syntax).map(Self::Paren),
302            SyntaxKind::LiteralExpr => LiteralExpr::cast(syntax).map(Self::Literal),
303            _ => None,
304        }
305    }
306
307    fn syntax(&self) -> &SyntaxNode {
308        match self {
309            Self::Binary(it) => it.syntax(),
310            Self::Prefix(it) => it.syntax(),
311            Self::Member(it) => it.syntax(),
312            Self::Path(it) => it.syntax(),
313            Self::Paren(it) => it.syntax(),
314            Self::Literal(it) => it.syntax(),
315        }
316    }
317}
318
319// --- the shared definition traits ----------------------------------------
320
321/// A node that declares a [`Name`].
322pub trait HasName: AstNode {
323    /// The declared name — the first `Name` child.
324    fn name(&self) -> Option<Name> {
325        support::child(self.syntax())
326    }
327}
328
329/// A node that accepts the `internal` / `error` prefix modifiers
330/// (general form §2; typl reference §3.3, §10.1).
331pub trait HasModifiers: AstNode {
332    /// `true` when the definition carries the `internal` modifier.
333    fn is_internal(&self) -> bool {
334        support::token(self.syntax(), SyntaxKind::InternalKw).is_some()
335    }
336
337    /// `true` when the definition carries the `error` modifier.
338    fn is_error(&self) -> bool {
339        support::token(self.syntax(), SyntaxKind::ErrorKw).is_some()
340    }
341
342    /// `true` when the definition carries the `open` modifier (valid on enum
343    /// and enumset definitions only).
344    fn is_open(&self) -> bool {
345        support::token(self.syntax(), SyntaxKind::OpenKw).is_some()
346    }
347}
348
349/// A node that doc comments attach to (typl reference §14). Doc comments
350/// are trivia tokens, so they sit before the node rather than inside it.
351pub trait HasDocComments: AstNode {
352    /// The `DocComment` tokens in the trivia run immediately preceding
353    /// this node, in source order.
354    fn doc_comments(&self) -> Vec<SyntaxToken> {
355        doc_comments_before(self.syntax())
356    }
357}
358
359/// The `DocComment` tokens in the trivia run immediately preceding `node`,
360/// in source order — the walk behind [`HasDocComments::doc_comments`], for a
361/// pass that reads a carrier off the untyped tree.
362pub fn doc_comments_before(node: &SyntaxNode) -> Vec<SyntaxToken> {
363    let mut out = Vec::new();
364    let mut cursor = node.prev_sibling_or_token();
365    while let Some(rowan::NodeOrToken::Token(token)) = cursor {
366        if !token.kind().is_trivia() {
367            break;
368        }
369        if token.kind() == SyntaxKind::DocComment {
370            out.push(token.clone());
371        }
372        cursor = token.prev_sibling_or_token();
373    }
374    out.reverse();
375    out
376}
377
378/// A carrier of a doc comment (typl reference §14.4), the declarations a lint
379/// directive applies to (ADR-0024 decision 2): a declaration, an interaction, a
380/// member, a call parameter, or an rsdl declaration or body line.
381pub fn is_doc_carrier(kind: SyntaxKind) -> bool {
382    matches!(
383        kind,
384        SyntaxKind::TypeDef
385            | SyntaxKind::ConstDef
386            | SyntaxKind::StructDef
387            | SyntaxKind::EnumDef
388            | SyntaxKind::EnumSetDef
389            | SyntaxKind::UnionDef
390            | SyntaxKind::FieldDef
391            | SyntaxKind::ReservedEntry
392            | SyntaxKind::EnumValue
393            | SyntaxKind::EnumSetBit
394            | SyntaxKind::UnionArm
395            | SyntaxKind::InterfaceDef
396            | SyntaxKind::ServiceDef
397            | SyntaxKind::SignalDef
398            | SyntaxKind::EventDef
399            | SyntaxKind::CommandDef
400            | SyntaxKind::QueryDef
401            | SyntaxKind::FixedDef
402            | SyntaxKind::Param
403            | SyntaxKind::SystemDef
404            | SyntaxKind::ComponentDef
405            | SyntaxKind::ComponentLine
406            | SyntaxKind::DistributionDef
407            | SyntaxKind::DeploymentDef
408            | SyntaxKind::MachineDef
409            | SyntaxKind::MemberLine
410    )
411}
412
413impl HasName for TypeDef {}
414impl HasName for ConstDef {}
415impl HasName for StructDef {}
416impl HasName for EnumDef {}
417impl HasName for EnumSetDef {}
418impl HasName for UnionDef {}
419impl HasName for Definition {}
420impl HasName for InterfaceDef {}
421impl HasName for SignalDef {}
422impl HasName for EventDef {}
423impl HasName for CommandDef {}
424impl HasName for QueryDef {}
425impl HasName for FixedDef {}
426impl HasName for InterfaceMember {}
427
428impl HasName for SystemDef {}
429impl HasName for ComponentDef {}
430impl HasName for DistributionDef {}
431impl HasName for DeploymentDef {}
432impl HasName for MachineDef {}
433
434impl HasModifiers for TypeDef {}
435impl HasModifiers for ConstDef {}
436impl HasModifiers for StructDef {}
437impl HasModifiers for EnumDef {}
438impl HasModifiers for EnumSetDef {}
439impl HasModifiers for UnionDef {}
440impl HasModifiers for Definition {}
441impl HasModifiers for InterfaceDef {}
442
443impl HasDocComments for TypeDef {}
444impl HasDocComments for ConstDef {}
445impl HasDocComments for StructDef {}
446impl HasDocComments for EnumDef {}
447impl HasDocComments for EnumSetDef {}
448impl HasDocComments for UnionDef {}
449impl HasDocComments for Definition {}
450impl HasDocComments for InterfaceDef {}
451impl HasDocComments for SignalDef {}
452impl HasDocComments for EventDef {}
453impl HasDocComments for CommandDef {}
454impl HasDocComments for QueryDef {}
455impl HasDocComments for FixedDef {}
456impl HasDocComments for InterfaceMember {}
457impl HasDocComments for ServiceDef {}
458// The members (typl §14, ADR-0026): a struct field, a `reserved` entry, an
459// enum value, an enumset bit, a union arm and a call parameter.
460impl HasDocComments for FieldDef {}
461impl HasDocComments for ReservedEntry {}
462impl HasDocComments for EnumValue {}
463impl HasDocComments for EnumSetBit {}
464impl HasDocComments for UnionArm {}
465impl HasDocComments for Param {}
466
467impl HasDocComments for SystemDef {}
468impl HasDocComments for ComponentDef {}
469impl HasDocComments for ComponentLine {}
470impl HasDocComments for DistributionDef {}
471impl HasDocComments for DeploymentDef {}
472impl HasDocComments for MachineDef {}
473impl HasDocComments for MemberLine {}
474
475impl DottedName {
476    /// The `lowercase_id` segments of the dotted name, in source order — the
477    /// three `ident` tokens of `veh.adas.cruise` (ridl reference §14.5). The
478    /// dots between them are excluded; a consumer that needs the full dotted
479    /// text calls [`DottedName::text`].
480    pub fn segments(&self) -> impl Iterator<Item = SyntaxToken> + '_ {
481        self.syntax()
482            .children_with_tokens()
483            .filter_map(|element| element.into_token())
484            .filter(|token| token.kind() == SyntaxKind::Ident)
485    }
486
487    /// The dotted name as written — every non-trivia token of the node
488    /// concatenated, so `veh . adas . cruise` reads as `veh.adas.cruise`.
489    /// Empty only on a tree the parser recovered with no tokens at all.
490    pub fn text(&self) -> String {
491        self.syntax()
492            .children_with_tokens()
493            .filter_map(|element| element.into_token())
494            .filter(|token| !token.kind().is_trivia())
495            .map(|token| token.text().to_string())
496            .collect()
497    }
498}
499
500// --- interface shapes (ridl reference §14.0, §14.5) -----------------------
501
502/// One interface shape declared in a file (ridl §14.0): an `interface`
503/// declaration, or the inline shape of a `service` (§14.5).
504///
505/// **[`SourceFile::interfaces`] is not the complete set.** A `service` with an
506/// inline body declares an interface body that is not an `InterfaceDef` and so
507/// never appears in `interfaces()`; a pass that walks `interfaces()` alone
508/// silently skips it. This enum, and [`SourceFile::shapes`] that yields it,
509/// are the AST half of the same guarantee `ridl_ir::v2::Package::shapes` gives
510/// on the IR side — the two halves exist because the blind spot has been found
511/// on both.
512#[derive(Debug, Clone, PartialEq, Eq, Hash)]
513pub enum InterfaceShape {
514    /// A declared `interface` (ridl §14.0).
515    Interface(InterfaceDef),
516    /// The inline shape of a `service` (ridl §14.5).
517    ServiceInline(ServiceDef),
518}
519
520impl InterfaceShape {
521    /// The name this shape is known by: an `interface` declaration's own
522    /// name, or the owning service's **dotted** global name — the identity
523    /// the IR, the diff paths, and the observer-stub scoping all use. `None`
524    /// when the parser recovered the declaration without a name.
525    pub fn identity(&self) -> Option<String> {
526        match self {
527            Self::Interface(def) => Some(def.name()?.ident_token()?.text().to_string()),
528            Self::ServiceInline(def) => {
529                let text = def.name()?.text();
530                (!text.is_empty()).then_some(text)
531            }
532        }
533    }
534
535    /// The source range of the declared name, for a diagnostic that points at
536    /// the shape itself rather than at one of its members. `None` when the
537    /// declaration carries no name node.
538    pub fn identity_range(&self) -> Option<TextRange> {
539        match self {
540            Self::Interface(def) => Some(def.name()?.syntax().text_range()),
541            Self::ServiceInline(def) => Some(def.name()?.syntax().text_range()),
542        }
543    }
544
545    /// The interactions and `reserved` tombstones of the body, in source
546    /// order — [`InterfaceDef::members`] or [`ServiceDef::inline_members`].
547    pub fn members(&self) -> AstChildren<InterfaceMember> {
548        match self {
549            Self::Interface(def) => def.members(),
550            Self::ServiceInline(def) => def.inline_members(),
551        }
552    }
553
554    /// The underlying declaration node.
555    pub fn syntax(&self) -> &SyntaxNode {
556        match self {
557            Self::Interface(def) => def.syntax(),
558            Self::ServiceInline(def) => def.syntax(),
559        }
560    }
561}
562
563impl SourceFile {
564    /// Every interface shape the file declares — its `interface` declarations
565    /// in source order, then the inline shapes of its `service` declarations
566    /// in source order. See [`InterfaceShape`] for why walking
567    /// [`SourceFile::interfaces`] alone is a defect.
568    ///
569    /// A service that names shapes after `:` yields nothing: its targets are
570    /// declared interfaces and are already in the sequence. The test is the
571    /// checker's own — a service with no `:` lowers to an inline shape,
572    /// whether or not that body turned out to hold any member. The `:` token
573    /// is the form discriminator, so a service the parser recovered with
574    /// neither form reads as an inline shape here and in the checker alike.
575    pub fn shapes(&self) -> impl Iterator<Item = InterfaceShape> + '_ {
576        self.interfaces().map(InterfaceShape::Interface).chain(
577            self.services()
578                .filter(|service| service.colon_token().is_none())
579                .map(InterfaceShape::ServiceInline),
580        )
581    }
582}
583
584// --- rsdl lines (rsdl reference §3.2, §4) ----------------------------------
585
586/// Which keyword a [`ComponentLine`] carries (rsdl reference §3.2).
587#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
588pub enum ComponentLineKind {
589    /// `offers` — the line names a service.
590    Offers,
591    /// `requires` — the line names an interface, or an inline-shape service.
592    Requires,
593}
594
595impl ComponentLine {
596    /// The line's keyword. Always `Some` on a tree the parser built: a
597    /// component line starts at its keyword.
598    pub fn kind(&self) -> Option<ComponentLineKind> {
599        if self.offers_token().is_some() {
600            Some(ComponentLineKind::Offers)
601        } else if self.requires_token().is_some() {
602            Some(ComponentLineKind::Requires)
603        } else {
604            None
605        }
606    }
607}
608
609// --- position-sensitive accessors ----------------------------------------
610
611/// The role a scalar literal plays inside a [`Constraint`], decided by the
612/// anchor token last seen before it (`..`, `step`, `match`).
613#[derive(Clone, Copy, PartialEq, Eq)]
614enum ScalarRole {
615    Min,
616    Max,
617    Step,
618    Pattern,
619}
620
621impl Constraint {
622    /// The lower endpoint of a scalar range — the literal before `..`.
623    pub fn min(&self) -> Option<Literal> {
624        self.scalar(ScalarRole::Min)
625    }
626
627    /// The upper endpoint of a scalar range — the literal after `..`.
628    pub fn max(&self) -> Option<Literal> {
629        self.scalar(ScalarRole::Max)
630    }
631
632    /// The step of a scalar range — the literal after the `step` keyword
633    /// (typl reference §4.3).
634    pub fn step(&self) -> Option<Literal> {
635        self.scalar(ScalarRole::Step)
636    }
637
638    /// The pattern after the `match` keyword — a regex literal or a named
639    /// constant reference (typl reference §5.3).
640    pub fn match_pattern(&self) -> Option<Literal> {
641        self.scalar(ScalarRole::Pattern)
642    }
643
644    /// The first direct `Literal` child in the given role. The scalar
645    /// endpoints of a constraint are all `Literal` nodes, so the anchor
646    /// tokens between them decide which is which; the literals of a length
647    /// bound sit inside a [`Bound`] child and never surface here.
648    fn scalar(&self, wanted: ScalarRole) -> Option<Literal> {
649        let mut role = ScalarRole::Min;
650        for element in self.syntax().children_with_tokens() {
651            match element {
652                rowan::NodeOrToken::Token(token) => match token.kind() {
653                    SyntaxKind::DotDot => role = ScalarRole::Max,
654                    SyntaxKind::StepKw => role = ScalarRole::Step,
655                    SyntaxKind::MatchKw => role = ScalarRole::Pattern,
656                    _ => {}
657                },
658                rowan::NodeOrToken::Node(node) => {
659                    if let Some(literal) = Literal::cast(node)
660                        && role == wanted
661                    {
662                        return Some(literal);
663                    }
664                }
665            }
666        }
667        None
668    }
669}
670
671impl ConstDef {
672    /// The regex token of a regex constant (typl reference §6.2), inside
673    /// the value literal.
674    pub fn regex(&self) -> Option<SyntaxToken> {
675        self.value()?.regex_token()
676    }
677}
678
679impl StreamType {
680    /// The raw `string`/`bytes` element keyword (ridl reference §12.2) —
681    /// the bare token child of the stream, present only when the element
682    /// is not a named type ([`StreamType::element_type`]).
683    pub fn element_primitive(&self) -> Option<SyntaxToken> {
684        self.syntax()
685            .children_with_tokens()
686            .filter_map(|element| element.into_token())
687            .find(|token| matches!(token.kind(), SyntaxKind::StringKw | SyntaxKind::BytesKw))
688    }
689}
690
691impl Timing {
692    /// The strict-periodic duration — `@10ms` (ridl reference §9). `None`
693    /// for the range form, whose durations sit inside [`Timing::range`].
694    pub fn duration(&self) -> Option<SyntaxToken> {
695        support::token(self.syntax(), SyntaxKind::Duration)
696    }
697}
698
699/// Which predicate keyword an [`Attribute`] carries (general form §4.2,
700/// ridl reference §13).
701#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
702pub enum PredicateKind {
703    Require,
704    Ensure,
705}
706
707impl Attribute {
708    /// The key's name segments in source order: one for a family key
709    /// (`labels`), two for an rsdl backend key (`linux.cpuset`, rsdl reference
710    /// §5). Empty for a predicate attribute, and for a key the parser held in
711    /// an `ErrorNode` (a reserved word, FORM-105).
712    pub fn key_segments(&self) -> AstChildren<Name> {
713        support::children(self.syntax())
714    }
715
716    /// The predicate keyword of a `require`/`ensure` attribute; `None` for
717    /// the flag and assignment forms.
718    pub fn predicate_kind(&self) -> Option<PredicateKind> {
719        if self.require_token().is_some() {
720            Some(PredicateKind::Require)
721        } else if self.ensure_token().is_some() {
722            Some(PredicateKind::Ensure)
723        } else {
724            None
725        }
726    }
727}
728
729/// Whether `kind` is one of the binary operators of the guaranteed subset
730/// (expr-core specification §3.1).
731fn is_binary_op(kind: SyntaxKind) -> bool {
732    matches!(
733        kind,
734        SyntaxKind::PipePipe
735            | SyntaxKind::AmpAmp
736            | SyntaxKind::EqEq
737            | SyntaxKind::Neq
738            | SyntaxKind::Lt
739            | SyntaxKind::Le
740            | SyntaxKind::Gt
741            | SyntaxKind::Ge
742            | SyntaxKind::Plus
743            | SyntaxKind::Minus
744            | SyntaxKind::Star
745            | SyntaxKind::Slash
746            | SyntaxKind::Percent
747    )
748}
749
750impl BinaryExpr {
751    /// The operator token of this binary step — the one direct token child
752    /// drawn from the subset operator set.
753    pub fn op_token(&self) -> Option<SyntaxToken> {
754        self.syntax()
755            .children_with_tokens()
756            .filter_map(|element| element.into_token())
757            .find(|token| is_binary_op(token.kind()))
758    }
759}
760
761impl PrefixExpr {
762    /// The prefix operator — `!` or `-`.
763    pub fn op_token(&self) -> Option<SyntaxToken> {
764        self.syntax()
765            .children_with_tokens()
766            .filter_map(|element| element.into_token())
767            .find(|token| matches!(token.kind(), SyntaxKind::Bang | SyntaxKind::Minus))
768    }
769}
770
771impl MemberExpr {
772    /// The member name after the `.` — a tuple field or an enum member.
773    pub fn member_token(&self) -> Option<SyntaxToken> {
774        self.ident_token()
775    }
776}
777
778impl PathExpr {
779    /// The referenced name — a parameter, `result`, a constant, or a type.
780    pub fn name_token(&self) -> Option<SyntaxToken> {
781        self.ident_token()
782    }
783}
784
785impl LiteralExpr {
786    /// The literal or duration value token.
787    pub fn token(&self) -> Option<SyntaxToken> {
788        self.syntax()
789            .children_with_tokens()
790            .filter_map(|element| element.into_token())
791            .find(|token| !token.kind().is_trivia())
792    }
793}
794
795impl TimingRange {
796    /// The lower bound — the duration before `..`. `None` on `[..max]`.
797    pub fn min(&self) -> Option<SyntaxToken> {
798        self.endpoint(false)
799    }
800
801    /// The upper bound — the duration after `..`. `None` on `[min..]`.
802    pub fn max(&self) -> Option<SyntaxToken> {
803        self.endpoint(true)
804    }
805
806    /// The first duration token before (`after_dots == false`) or after
807    /// (`after_dots == true`) the `..` anchor — both endpoints are
808    /// `duration` tokens, so position relative to `..` decides the role.
809    fn endpoint(&self, after_dots: bool) -> Option<SyntaxToken> {
810        let mut seen_dots = false;
811        for element in self.syntax().children_with_tokens() {
812            let Some(token) = element.into_token() else {
813                continue;
814            };
815            match token.kind() {
816                SyntaxKind::DotDot => seen_dots = true,
817                SyntaxKind::Duration if seen_dots == after_dots => return Some(token),
818                _ => {}
819            }
820        }
821        None
822    }
823}
824
825#[cfg(test)]
826mod tests {
827    use rowan::GreenNodeBuilder;
828
829    use super::*;
830    use crate::syntax_kind::{SyntaxKind, SyntaxNode};
831
832    fn token(builder: &mut GreenNodeBuilder<'static>, kind: SyntaxKind, text: &str) {
833        builder.token(kind.into(), text);
834    }
835
836    fn literal(builder: &mut GreenNodeBuilder<'static>, kind: SyntaxKind, text: &str) {
837        builder.start_node(SyntaxKind::Literal.into());
838        token(builder, kind, text);
839        builder.finish_node();
840    }
841
842    fn name(builder: &mut GreenNodeBuilder<'static>, text: &str) {
843        builder.start_node(SyntaxKind::Name.into());
844        token(builder, SyntaxKind::Ident, text);
845        builder.finish_node();
846    }
847
848    fn path_type(builder: &mut GreenNodeBuilder<'static>, text: &str) {
849        builder.start_node(SyntaxKind::PathType.into());
850        builder.start_node(SyntaxKind::QualifiedName.into());
851        token(builder, SyntaxKind::Ident, text);
852        builder.finish_node();
853        builder.finish_node();
854    }
855
856    /// `/// Calibrated top speed.` + `internal type Speed: km/h [0.0..250.0
857    /// step 0.5] = 0.0`, built by hand — the full parser is a separate step.
858    fn speed_source_file() -> SyntaxNode {
859        let mut b = GreenNodeBuilder::new();
860        b.start_node(SyntaxKind::SourceFile.into());
861        token(&mut b, SyntaxKind::DocComment, "/// Calibrated top speed.");
862        token(&mut b, SyntaxKind::Whitespace, "\n");
863        b.start_node(SyntaxKind::TypeDef.into());
864        token(&mut b, SyntaxKind::InternalKw, "internal");
865        token(&mut b, SyntaxKind::Whitespace, " ");
866        token(&mut b, SyntaxKind::TypeKw, "type");
867        token(&mut b, SyntaxKind::Whitespace, " ");
868        name(&mut b, "Speed");
869        token(&mut b, SyntaxKind::Colon, ":");
870        token(&mut b, SyntaxKind::Whitespace, " ");
871        b.start_node(SyntaxKind::UnitExpr.into());
872        token(&mut b, SyntaxKind::Ident, "km");
873        token(&mut b, SyntaxKind::Slash, "/");
874        token(&mut b, SyntaxKind::Ident, "h");
875        b.finish_node();
876        token(&mut b, SyntaxKind::Whitespace, " ");
877        b.start_node(SyntaxKind::Constraint.into());
878        token(&mut b, SyntaxKind::LBracket, "[");
879        literal(&mut b, SyntaxKind::FloatNumber, "0.0");
880        token(&mut b, SyntaxKind::DotDot, "..");
881        literal(&mut b, SyntaxKind::FloatNumber, "250.0");
882        token(&mut b, SyntaxKind::Whitespace, " ");
883        token(&mut b, SyntaxKind::StepKw, "step");
884        token(&mut b, SyntaxKind::Whitespace, " ");
885        literal(&mut b, SyntaxKind::FloatNumber, "0.5");
886        token(&mut b, SyntaxKind::RBracket, "]");
887        b.finish_node();
888        token(&mut b, SyntaxKind::Whitespace, " ");
889        b.start_node(SyntaxKind::InitValue.into());
890        token(&mut b, SyntaxKind::Eq, "=");
891        token(&mut b, SyntaxKind::Whitespace, " ");
892        literal(&mut b, SyntaxKind::FloatNumber, "0.0");
893        b.finish_node();
894        b.finish_node();
895        b.finish_node();
896        SyntaxNode::new_root(b.finish())
897    }
898
899    /// Every member carrier of typl §14 (ADR-0026) reads its doc comment off
900    /// the parsed tree: the parser flushes the trivia before a member to the
901    /// body node, so the comment is the member's previous sibling.
902    #[test]
903    fn member_doc_comments_attach_to_their_nodes() {
904        let text = "package app\n\
905            struct S {\n  /// Doc 1.\n  a: boolean\n  /// Doc 2.\n  reserved b\n}\n\
906            enum E {\n  /// Doc 3.\n  A = 1\n}\n\
907            enumset F {\n  /// Doc 4.\n  X = 0\n}\n\
908            union U {\n  /// Doc 5.\n  a: S\n}\n\
909            interface I {\n  command c(\n    /// Doc 6.\n    a: S\n  ) @[..50ms]\n}\n";
910        let parse = crate::parse(text, crate::Profile::Ridl);
911        assert_eq!(parse.errors(), &[], "the fixture parses");
912        let root = parse.syntax();
913        let docs_of = |kind: SyntaxKind| -> Vec<String> {
914            root.descendants()
915                .filter(|node| node.kind() == kind)
916                .flat_map(|node| doc_comments_before(&node))
917                .map(|token| token.text().to_string())
918                .collect()
919        };
920        assert_eq!(docs_of(SyntaxKind::FieldDef), ["/// Doc 1."]);
921        assert_eq!(docs_of(SyntaxKind::ReservedEntry), ["/// Doc 2."]);
922        assert_eq!(docs_of(SyntaxKind::EnumValue), ["/// Doc 3."]);
923        assert_eq!(docs_of(SyntaxKind::EnumSetBit), ["/// Doc 4."]);
924        assert_eq!(docs_of(SyntaxKind::UnionArm), ["/// Doc 5."]);
925        assert_eq!(docs_of(SyntaxKind::Param), ["/// Doc 6."]);
926        let param = root
927            .descendants()
928            .find_map(Param::cast)
929            .expect("the fixture has a parameter");
930        assert_eq!(
931            param.doc_comments().len(),
932            1,
933            "the trait reads the same run"
934        );
935    }
936
937    #[test]
938    fn type_def_cast_round_trips() {
939        let root = speed_source_file();
940        assert_eq!(
941            root.text().to_string(),
942            "/// Calibrated top speed.\ninternal type Speed: km/h [0.0..250.0 step 0.5] = 0.0",
943        );
944
945        let file = SourceFile::cast(root).expect("root is a SourceFile");
946        assert!(file.package_decl().is_none());
947        assert_eq!(file.imports().count(), 0);
948
949        let definitions: Vec<Definition> = file.definitions().collect();
950        assert_eq!(definitions.len(), 1);
951        let Definition::Type(type_def) = &definitions[0] else {
952            panic!("expected Definition::Type, got {:?}", definitions[0]);
953        };
954
955        let name = type_def.name().expect("TypeDef has a Name");
956        assert_eq!(
957            name.ident_token().expect("Name wraps an Ident").text(),
958            "Speed"
959        );
960
961        let Some(Backing::Unit(unit)) = type_def.backing() else {
962            panic!("expected a unit backing");
963        };
964        assert_eq!(unit.syntax().text().to_string(), "km/h");
965
966        let constraint = type_def.constraint().expect("TypeDef has a Constraint");
967        assert_eq!(constraint.min().unwrap().syntax().text().to_string(), "0.0");
968        assert_eq!(
969            constraint.max().unwrap().syntax().text().to_string(),
970            "250.0"
971        );
972        assert_eq!(
973            constraint.step().unwrap().syntax().text().to_string(),
974            "0.5"
975        );
976        assert!(constraint.match_pattern().is_none());
977        assert!(constraint.len().is_none());
978
979        let init = type_def.init_value().expect("TypeDef has an InitValue");
980        assert_eq!(init.literal().unwrap().syntax().text().to_string(), "0.0");
981
982        assert!(type_def.is_internal());
983        assert!(!type_def.is_error());
984
985        let docs = type_def.doc_comments();
986        assert_eq!(docs.len(), 1);
987        assert_eq!(docs[0].text(), "/// Calibrated top speed.");
988
989        // The trait trio also answers through the `Definition` enum.
990        let definition = &definitions[0];
991        assert_eq!(
992            definition.name().unwrap().ident_token().unwrap().text(),
993            "Speed",
994        );
995        assert!(definition.is_internal());
996        assert_eq!(definition.doc_comments().len(), 1);
997    }
998
999    /// `struct DriverProfile { name: Name / reserved legacyChecksum /
1000    /// speed: Speed? }` — the §7.4 tombstone example shape.
1001    fn driver_profile_source_file() -> SyntaxNode {
1002        let mut b = GreenNodeBuilder::new();
1003        b.start_node(SyntaxKind::SourceFile.into());
1004        b.start_node(SyntaxKind::StructDef.into());
1005        token(&mut b, SyntaxKind::StructKw, "struct");
1006        token(&mut b, SyntaxKind::Whitespace, " ");
1007        name(&mut b, "DriverProfile");
1008        token(&mut b, SyntaxKind::Whitespace, " ");
1009        token(&mut b, SyntaxKind::LBrace, "{");
1010        token(&mut b, SyntaxKind::Whitespace, "\n  ");
1011        b.start_node(SyntaxKind::FieldDef.into());
1012        name(&mut b, "name");
1013        token(&mut b, SyntaxKind::Colon, ":");
1014        token(&mut b, SyntaxKind::Whitespace, " ");
1015        path_type(&mut b, "Name");
1016        b.finish_node();
1017        token(&mut b, SyntaxKind::Whitespace, "\n  ");
1018        b.start_node(SyntaxKind::ReservedEntry.into());
1019        token(&mut b, SyntaxKind::ReservedKw, "reserved");
1020        token(&mut b, SyntaxKind::Whitespace, " ");
1021        name(&mut b, "legacyChecksum");
1022        b.finish_node();
1023        token(&mut b, SyntaxKind::Whitespace, "\n  ");
1024        b.start_node(SyntaxKind::FieldDef.into());
1025        name(&mut b, "speed");
1026        token(&mut b, SyntaxKind::Colon, ":");
1027        token(&mut b, SyntaxKind::Whitespace, " ");
1028        b.start_node(SyntaxKind::OptionalType.into());
1029        path_type(&mut b, "Speed");
1030        token(&mut b, SyntaxKind::Question, "?");
1031        b.finish_node();
1032        b.finish_node();
1033        token(&mut b, SyntaxKind::Whitespace, "\n");
1034        token(&mut b, SyntaxKind::RBrace, "}");
1035        b.finish_node();
1036        b.finish_node();
1037        SyntaxNode::new_root(b.finish())
1038    }
1039
1040    #[test]
1041    fn struct_def_cast_round_trips() {
1042        let root = driver_profile_source_file();
1043        assert_eq!(
1044            root.text().to_string(),
1045            "struct DriverProfile {\n  name: Name\n  reserved legacyChecksum\n  speed: Speed?\n}",
1046        );
1047
1048        let file = SourceFile::cast(root).expect("root is a SourceFile");
1049        let definitions: Vec<Definition> = file.definitions().collect();
1050        assert_eq!(definitions.len(), 1);
1051        let Definition::Struct(struct_def) = &definitions[0] else {
1052            panic!("expected Definition::Struct, got {:?}", definitions[0]);
1053        };
1054
1055        assert_eq!(
1056            struct_def.name().unwrap().ident_token().unwrap().text(),
1057            "DriverProfile",
1058        );
1059        assert!(!struct_def.is_internal());
1060        assert!(!struct_def.is_error());
1061
1062        let members: Vec<StructMember> = struct_def.members().collect();
1063        assert_eq!(members.len(), 3);
1064
1065        let StructMember::Field(name_field) = &members[0] else {
1066            panic!("expected a field, got {:?}", members[0]);
1067        };
1068        assert_eq!(
1069            name_field.name().unwrap().ident_token().unwrap().text(),
1070            "name",
1071        );
1072        let Some(FieldType::Path(path)) = name_field.field_type() else {
1073            panic!("expected a path field type");
1074        };
1075        assert_eq!(path.syntax().text().to_string(), "Name");
1076        assert!(name_field.init_value().is_none());
1077
1078        let StructMember::Reserved(tombstone) = &members[1] else {
1079            panic!("expected a reserved entry, got {:?}", members[1]);
1080        };
1081        assert_eq!(
1082            tombstone.name().unwrap().ident_token().unwrap().text(),
1083            "legacyChecksum",
1084        );
1085        assert!(tombstone.literal().is_none());
1086
1087        let StructMember::Field(speed_field) = &members[2] else {
1088            panic!("expected a field, got {:?}", members[2]);
1089        };
1090        let Some(FieldType::Optional(optional)) = speed_field.field_type() else {
1091            panic!("expected an optional field type");
1092        };
1093        assert!(optional.question_token().is_some());
1094        let Some(FieldType::Path(inner)) = optional.field_type() else {
1095            panic!("expected a path inside the optional");
1096        };
1097        assert_eq!(inner.syntax().text().to_string(), "Speed");
1098    }
1099
1100    #[test]
1101    fn const_def_regex_accessor_finds_the_pattern() {
1102        let mut b = GreenNodeBuilder::new();
1103        b.start_node(SyntaxKind::SourceFile.into());
1104        b.start_node(SyntaxKind::ConstDef.into());
1105        token(&mut b, SyntaxKind::ConstKw, "const");
1106        token(&mut b, SyntaxKind::Whitespace, " ");
1107        name(&mut b, "SPEED_PATTERN");
1108        token(&mut b, SyntaxKind::Whitespace, " ");
1109        token(&mut b, SyntaxKind::Eq, "=");
1110        token(&mut b, SyntaxKind::Whitespace, " ");
1111        literal(&mut b, SyntaxKind::Regex, r"/^\d+(\.\d+)?$/");
1112        b.finish_node();
1113        b.finish_node();
1114        let root = SyntaxNode::new_root(b.finish());
1115
1116        let file = SourceFile::cast(root).expect("root is a SourceFile");
1117        let Some(Definition::Const(const_def)) = file.definitions().next() else {
1118            panic!("expected a const definition");
1119        };
1120        assert_eq!(
1121            const_def.name().unwrap().ident_token().unwrap().text(),
1122            "SPEED_PATTERN",
1123        );
1124        assert!(const_def.type_ref().is_none());
1125        assert_eq!(const_def.regex().unwrap().text(), r"/^\d+(\.\d+)?$/");
1126    }
1127
1128    /// `error type Broken: boolean` — the grammar admits a misplaced
1129    /// `error` modifier on every definition kind, so the checker (not the
1130    /// parser) can reject it with TYPL-212; `is_error()` must see it.
1131    #[test]
1132    fn misplaced_error_modifier_is_held_and_found() {
1133        let mut b = GreenNodeBuilder::new();
1134        b.start_node(SyntaxKind::SourceFile.into());
1135        b.start_node(SyntaxKind::TypeDef.into());
1136        token(&mut b, SyntaxKind::ErrorKw, "error");
1137        token(&mut b, SyntaxKind::Whitespace, " ");
1138        token(&mut b, SyntaxKind::TypeKw, "type");
1139        token(&mut b, SyntaxKind::Whitespace, " ");
1140        name(&mut b, "Broken");
1141        token(&mut b, SyntaxKind::Colon, ":");
1142        token(&mut b, SyntaxKind::Whitespace, " ");
1143        b.start_node(SyntaxKind::PrimitiveType.into());
1144        token(&mut b, SyntaxKind::BooleanKw, "boolean");
1145        b.finish_node();
1146        b.finish_node();
1147        b.finish_node();
1148        let root = SyntaxNode::new_root(b.finish());
1149
1150        let file = SourceFile::cast(root).expect("root is a SourceFile");
1151        let Some(Definition::Type(type_def)) = file.definitions().next() else {
1152            panic!("expected a type definition");
1153        };
1154        assert!(type_def.is_error());
1155        assert!(type_def.error_token().is_some());
1156        assert!(!type_def.is_internal());
1157    }
1158
1159    /// Builds a bare `Constraint` node: `[` + the given body + `]`.
1160    fn constraint(build: impl FnOnce(&mut GreenNodeBuilder<'static>)) -> Constraint {
1161        let mut b = GreenNodeBuilder::new();
1162        b.start_node(SyntaxKind::Constraint.into());
1163        token(&mut b, SyntaxKind::LBracket, "[");
1164        build(&mut b);
1165        token(&mut b, SyntaxKind::RBracket, "]");
1166        b.finish_node();
1167        Constraint::cast(SyntaxNode::new_root(b.finish())).expect("Constraint casts")
1168    }
1169
1170    /// A `Bound` node: one literal, or two literals around `..`.
1171    fn bound(builder: &mut GreenNodeBuilder<'static>, min: &str, max: Option<&str>) {
1172        builder.start_node(SyntaxKind::Bound.into());
1173        literal(builder, SyntaxKind::IntNumber, min);
1174        if let Some(max) = max {
1175            token(builder, SyntaxKind::DotDot, "..");
1176            literal(builder, SyntaxKind::IntNumber, max);
1177        }
1178        builder.finish_node();
1179    }
1180
1181    /// Open ranges: the scalar accessors key on the anchor tokens, not on
1182    /// child positions, so `[..255]` has no min and `[0..]` has no max.
1183    #[test]
1184    fn open_ranges_anchor_the_scalar_roles() {
1185        // `[..255]` — the literal after a leading `..` is the max.
1186        let upper_only = constraint(|b| {
1187            token(b, SyntaxKind::DotDot, "..");
1188            literal(b, SyntaxKind::IntNumber, "255");
1189        });
1190        assert!(upper_only.min().is_none());
1191        assert_eq!(upper_only.max().unwrap().syntax().text().to_string(), "255");
1192        assert!(upper_only.step().is_none());
1193        assert!(upper_only.match_pattern().is_none());
1194        assert!(upper_only.len().is_none());
1195
1196        // `[0..]` — the literal before a trailing `..` is the min.
1197        let lower_only = constraint(|b| {
1198            literal(b, SyntaxKind::IntNumber, "0");
1199            token(b, SyntaxKind::DotDot, "..");
1200        });
1201        assert_eq!(lower_only.min().unwrap().syntax().text().to_string(), "0");
1202        assert!(lower_only.max().is_none());
1203        assert!(lower_only.step().is_none());
1204        assert!(lower_only.match_pattern().is_none());
1205    }
1206
1207    /// `[match PATTERN]` — a bare match pattern is not a scalar endpoint.
1208    #[test]
1209    fn bare_match_pattern_is_not_a_scalar_endpoint() {
1210        let bare_match = constraint(|b| {
1211            token(b, SyntaxKind::MatchKw, "match");
1212            token(b, SyntaxKind::Whitespace, " ");
1213            literal(b, SyntaxKind::Ident, "PATTERN");
1214        });
1215        assert!(bare_match.min().is_none());
1216        assert!(bare_match.max().is_none());
1217        assert!(bare_match.step().is_none());
1218        assert!(bare_match.len().is_none());
1219        assert_eq!(
1220            bare_match
1221                .match_pattern()
1222                .unwrap()
1223                .syntax()
1224                .text()
1225                .to_string(),
1226            "PATTERN",
1227        );
1228    }
1229
1230    /// Length bounds nest their literals inside a `Bound` child: they never
1231    /// leak into the scalar accessors, and the `..` inside the bound does
1232    /// not move the anchor-token role tracker.
1233    #[test]
1234    fn length_bound_literals_do_not_leak_into_the_scalar_accessors() {
1235        // `[17 match /re/]` — exact length plus pattern.
1236        let exact_len = constraint(|b| {
1237            bound(b, "17", None);
1238            token(b, SyntaxKind::Whitespace, " ");
1239            token(b, SyntaxKind::MatchKw, "match");
1240            token(b, SyntaxKind::Whitespace, " ");
1241            literal(b, SyntaxKind::Regex, "/re/");
1242        });
1243        assert!(exact_len.min().is_none());
1244        assert!(exact_len.max().is_none());
1245        let len = exact_len.len().expect("the length bound is present");
1246        assert_eq!(len.min().unwrap().syntax().text().to_string(), "17");
1247        assert!(len.max().is_none());
1248        assert_eq!(
1249            exact_len
1250                .match_pattern()
1251                .unwrap()
1252                .syntax()
1253                .text()
1254                .to_string(),
1255            "/re/",
1256        );
1257
1258        // `[3..6 match P]` — length range plus named pattern.
1259        let len_range = constraint(|b| {
1260            bound(b, "3", Some("6"));
1261            token(b, SyntaxKind::Whitespace, " ");
1262            token(b, SyntaxKind::MatchKw, "match");
1263            token(b, SyntaxKind::Whitespace, " ");
1264            literal(b, SyntaxKind::Ident, "P");
1265        });
1266        assert!(len_range.min().is_none());
1267        assert!(len_range.max().is_none());
1268        let len = len_range.len().expect("the length bound is present");
1269        assert_eq!(len.min().unwrap().syntax().text().to_string(), "3");
1270        assert_eq!(len.max().unwrap().syntax().text().to_string(), "6");
1271        assert_eq!(
1272            len_range
1273                .match_pattern()
1274                .unwrap()
1275                .syntax()
1276                .text()
1277                .to_string(),
1278            "P",
1279        );
1280    }
1281
1282    /// The attribute and expression accessors over a parsed tree:
1283    /// the surface tasks 5, 11, 12, and 21 consume.
1284    #[test]
1285    fn attribute_and_expr_accessors_read_a_parsed_contract() {
1286        use crate::{Profile, parse};
1287
1288        let input = "package p\ninterface I {\n  \
1289            command setGear(position: GearPosition) [\n    \
1290            require position != GearPosition.PARK || currentSpeed == 0.0\n    \
1291            someKey = 3\n  ]\n}\n";
1292        let parsed = parse(input, Profile::Ridl);
1293        assert!(parsed.errors().is_empty(), "got: {:?}", parsed.errors());
1294
1295        let attr_block = parsed
1296            .syntax()
1297            .descendants()
1298            .find_map(AttrBlock::cast)
1299            .expect("an AttrBlock parses");
1300        let attributes: Vec<Attribute> = attr_block.attributes().collect();
1301        assert_eq!(attributes.len(), 2);
1302
1303        // The predicate form: kind + expr, no key, no value.
1304        let predicate = &attributes[0];
1305        assert_eq!(predicate.predicate_kind(), Some(PredicateKind::Require));
1306        assert!(predicate.key().is_none());
1307        assert!(predicate.value().is_none());
1308        let Some(Expr::Binary(or)) = predicate.expr() else {
1309            panic!("expected a binary root, got {:?}", predicate.expr());
1310        };
1311        assert_eq!(or.op_token().unwrap().text(), "||");
1312        let Some(Expr::Binary(neq)) = or.lhs() else {
1313            panic!("expected a binary lhs, got {:?}", or.lhs());
1314        };
1315        assert_eq!(neq.op_token().unwrap().text(), "!=");
1316        let Some(Expr::Member(member)) = neq.rhs() else {
1317            panic!("expected a member rhs, got {:?}", neq.rhs());
1318        };
1319        assert_eq!(member.member_token().unwrap().text(), "PARK");
1320        let Some(Expr::Path(base)) = member.base() else {
1321            panic!("expected a path base, got {:?}", member.base());
1322        };
1323        assert_eq!(base.name_token().unwrap().text(), "GearPosition");
1324        let Some(Expr::Binary(eq)) = or.rhs() else {
1325            panic!("expected a binary rhs, got {:?}", or.rhs());
1326        };
1327        let Some(Expr::Literal(zero)) = eq.rhs() else {
1328            panic!("expected a literal rhs, got {:?}", eq.rhs());
1329        };
1330        assert_eq!(zero.token().unwrap().text(), "0.0");
1331
1332        // The assignment form: key + value, no predicate, no expr.
1333        let assignment = &attributes[1];
1334        assert!(assignment.predicate_kind().is_none());
1335        assert!(assignment.expr().is_none());
1336        assert_eq!(
1337            assignment.key().unwrap().ident_token().unwrap().text(),
1338            "someKey",
1339        );
1340        let value = assignment.value().expect("the assignment has a value");
1341        assert_eq!(value.literal().unwrap().syntax().text().to_string(), "3");
1342    }
1343
1344    /// A ridl file holding all three shape-bearing declarations: an
1345    /// `interface`, a `service` naming it, and a `service` with an inline body.
1346    const THREE_SHAPES: &str = "\
1347package app
1348
1349interface VehicleStatus {
1350  signal speed : Speed @10ms
1351  event doorOpened : DoorState
1352}
1353
1354service veh.adas.status : VehicleStatus
1355
1356service veh.adas.logs {
1357  signal level : Level @1s
1358  reserved oldTail
1359  query tailLogs(filter: Filter): Line
1360}
1361";
1362
1363    /// `SourceFile::shapes` yields the `interface` declarations first, then the
1364    /// services carrying an inline body — each under the name it is known by
1365    /// outside the file. A service naming an interface after `:` contributes
1366    /// nothing: its target is already in the walk.
1367    #[test]
1368    fn shapes_walks_interfaces_and_inline_service_bodies() {
1369        use crate::{Profile, parse};
1370
1371        let parsed = parse(THREE_SHAPES, Profile::Ridl);
1372        assert!(parsed.errors().is_empty(), "got: {:?}", parsed.errors());
1373        let file = SourceFile::cast(parsed.syntax()).expect("the root is a SourceFile");
1374
1375        let walk: Vec<(String, usize)> = file
1376            .shapes()
1377            .map(|shape| {
1378                (
1379                    shape.identity().expect("every shape here is named"),
1380                    shape.members().count(),
1381                )
1382            })
1383            .collect();
1384        assert_eq!(
1385            walk,
1386            [
1387                ("VehicleStatus".to_string(), 2),
1388                ("veh.adas.logs".to_string(), 3),
1389            ],
1390            "the interface, then the inline shape under the service's dotted \
1391             name; the reference-form service contributes nothing",
1392        );
1393
1394        // The reference-form service is in the file and is not in the walk.
1395        assert_eq!(file.services().count(), 2);
1396        assert!(
1397            !file
1398                .shapes()
1399                .any(|shape| shape.identity().as_deref() == Some("veh.adas.status")),
1400        );
1401    }
1402
1403    /// The identity range is the declared name, not the whole declaration and
1404    /// not the keyword: it is what a diagnostic about the shape underlines.
1405    #[test]
1406    fn shape_identity_range_covers_the_declared_name() {
1407        use crate::{Profile, parse};
1408
1409        let parsed = parse(THREE_SHAPES, Profile::Ridl);
1410        let file = SourceFile::cast(parsed.syntax()).expect("the root is a SourceFile");
1411
1412        for shape in file.shapes() {
1413            let name = shape.identity().expect("named");
1414            let range = shape.identity_range().expect("named");
1415            let start = usize::from(range.start());
1416            let end = usize::from(range.end());
1417            assert_eq!(
1418                &THREE_SHAPES[start..end],
1419                name,
1420                "the range must cover exactly the declared name",
1421            );
1422        }
1423    }
1424
1425    /// A `service` the parser recovered without a body and without a `:`
1426    /// reference is an inline shape with no member — the same reading the
1427    /// checker's `lower_service` takes, so a downstream walk sees the same set
1428    /// of shapes the IR carries.
1429    #[test]
1430    fn a_service_with_neither_form_is_an_empty_inline_shape() {
1431        use crate::{Profile, parse};
1432
1433        let parsed = parse("package app\nservice veh.adas.broken\n", Profile::Ridl);
1434        let file = SourceFile::cast(parsed.syntax()).expect("the root is a SourceFile");
1435
1436        let shapes: Vec<InterfaceShape> = file.shapes().collect();
1437        assert_eq!(shapes.len(), 1);
1438        assert_eq!(shapes[0].identity().as_deref(), Some("veh.adas.broken"));
1439        assert_eq!(shapes[0].members().count(), 0);
1440    }
1441
1442    /// `DottedName::text` joins the significant tokens, so the written name is
1443    /// read back whatever whitespace surrounds the dots.
1444    #[test]
1445    fn dotted_name_text_joins_the_segments() {
1446        use crate::{Profile, parse};
1447
1448        let parsed = parse(
1449            "package app\nservice veh . adas . logs {\n}\n",
1450            Profile::Ridl,
1451        );
1452        let file = SourceFile::cast(parsed.syntax()).expect("the root is a SourceFile");
1453        let service = file.services().next().expect("a service parses");
1454        assert_eq!(service.name().expect("named").text(), "veh.adas.logs");
1455    }
1456
1457    #[test]
1458    fn enum_set_def_derived_form_exposes_the_backing_ref() {
1459        let mut b = GreenNodeBuilder::new();
1460        b.start_node(SyntaxKind::SourceFile.into());
1461        b.start_node(SyntaxKind::EnumSetDef.into());
1462        token(&mut b, SyntaxKind::EnumsetKw, "enumset");
1463        token(&mut b, SyntaxKind::Whitespace, " ");
1464        name(&mut b, "WarningFlags");
1465        token(&mut b, SyntaxKind::Colon, ":");
1466        token(&mut b, SyntaxKind::Whitespace, " ");
1467        b.start_node(SyntaxKind::PathType.into());
1468        b.start_node(SyntaxKind::QualifiedName.into());
1469        token(&mut b, SyntaxKind::Ident, "Warning");
1470        b.finish_node();
1471        b.finish_node();
1472        b.finish_node();
1473        b.finish_node();
1474        let root = SyntaxNode::new_root(b.finish());
1475
1476        let file = SourceFile::cast(root).expect("root is a SourceFile");
1477        let Some(Definition::EnumSet(enum_set)) = file.definitions().next() else {
1478            panic!("expected an enumset definition");
1479        };
1480        assert_eq!(
1481            enum_set.name().unwrap().ident_token().unwrap().text(),
1482            "WarningFlags",
1483        );
1484        assert_eq!(
1485            enum_set.backing_ref().unwrap().syntax().text().to_string(),
1486            "Warning",
1487        );
1488        assert_eq!(enum_set.bits().count(), 0);
1489    }
1490}