Skip to main content

ridl_syntax/
ast.rs

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