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