Skip to main content

zerodds_ast/facet/
xtypes.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! XTypes annotations. Spec §4.1.
4
5use crate::facet::applied::AppliedAnnotation;
6use crate::node::constant::ConstExprNode;
7use alloc::string::String;
8use alloc::vec::Vec;
9use zerodds_ast_core::Id;
10
11/// Struct extensibility. DDS-XTypes 1.3 §7.2.4.4.
12///
13/// Ordered so that it can sit in a layout key, which the plan cache
14/// keys on: the order is the declaration order and carries no meaning
15/// of its own.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
17pub enum Extensibility {
18    /// `@final` — no evolution.
19    Final,
20    /// `@appendable` — members may be appended.
21    Appendable,
22    /// `@mutable` — members carry ids and may be reordered.
23    Mutable,
24}
25
26impl Extensibility {
27    /// Every extensibility XTypes defines.
28    pub const ALL: [Self; 3] = [Self::Final, Self::Appendable, Self::Mutable];
29
30    /// Source spelling.
31    #[must_use]
32    pub const fn as_str(self) -> &'static str {
33        match self {
34            Self::Final => "final",
35            Self::Appendable => "appendable",
36            Self::Mutable => "mutable",
37        }
38    }
39
40    /// Parse a configuration value, in the spelling [`Self::as_str`]
41    /// produces.
42    ///
43    /// An unknown value returns `None` rather than falling back to a
44    /// default: the setting decides whether a DHEADER goes on the wire,
45    /// and a typo in it must be loud.
46    #[must_use]
47    pub fn parse(value: &str) -> Option<Self> {
48        match value {
49            "final" => Some(Self::Final),
50            "appendable" => Some(Self::Appendable),
51            "mutable" => Some(Self::Mutable),
52            _ => None,
53        }
54    }
55}
56
57/// `@try_construct` behaviour. DDS-XTypes 1.3 §7.2.2.7, §7.3.1.2.1.12.
58///
59/// What a receiver does when an object of the sender's type cannot
60/// construct any object of the receiver's type — an over-long string, a
61/// sequence with more elements than the bound holds, an enumerator the
62/// receiver does not declare.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
64pub enum TryConstruct {
65    /// The failure propagates: the containing collection or aggregate
66    /// cannot be constructed either, and at the top level the sample is
67    /// discarded.
68    Discard,
69    /// The element or member takes the default value of its type
70    /// (Table 9) and the container is constructed.
71    UseDefault,
72    /// A string, wstring, sequence or map is truncated to what the
73    /// receiver's bound holds, preserving order. On any other type the
74    /// clause leaves the behaviour unspecified.
75    Trim,
76}
77
78impl TryConstruct {
79    /// Every behaviour XTypes defines.
80    pub const ALL: [Self; 3] = [Self::Discard, Self::UseDefault, Self::Trim];
81
82    /// What applies where nothing was written.
83    ///
84    /// §7.2.2.7: "The default behavior unless otherwise specified using
85    /// the TryConstruct annotation is DISCARD." Note that this is the
86    /// default for an *absent* annotation. The annotation's own
87    /// parameter defaults to `USE_DEFAULT` (§7.3.1.2.1.12), so a bare
88    /// `@try_construct` is not the same as no annotation at all.
89    pub const ABSENT: Self = Self::Discard;
90
91    /// What a bare `@try_construct`, written with no argument, means.
92    ///
93    /// §7.3.1.2.1.12 declares `TryConstructFailAction value default
94    /// USE_DEFAULT` and spells it out: "If the annotation is specified
95    /// without a value, or if the value is set to USE_DEFAULT, then the
96    /// behavior is set to DEFAULT". Measured: OpenDDS constructs the
97    /// empty string for `@try_construct string<5>` against a writer's
98    /// twelve-character value —
99    /// `crates/ast-layout/tests/fixtures/try_construct_behaviour.md`.
100    pub const BARE: Self = Self::UseDefault;
101
102    /// The spelling the annotation argument uses.
103    #[must_use]
104    pub const fn as_str(self) -> &'static str {
105        match self {
106            Self::Discard => "DISCARD",
107            Self::UseDefault => "USE_DEFAULT",
108            Self::Trim => "TRIM",
109        }
110    }
111
112    /// Parse an annotation argument, in the spelling [`Self::as_str`]
113    /// produces.
114    ///
115    /// An unknown value returns `None` rather than the default: the
116    /// setting decides whether a sample survives, and a typo in it must
117    /// be loud.
118    #[must_use]
119    pub fn parse(value: &str) -> Option<Self> {
120        Self::ALL
121            .into_iter()
122            .find(|behaviour| behaviour.as_str() == value)
123    }
124
125    /// Whether `TRIM` is defined for this type kind.
126    ///
127    /// Table 11: "This option only applies to elements or members of
128    /// type string, wide string, sequence, or map. The behavior when
129    /// applied to other element/member types is unspecified and may be
130    /// treated as an error."
131    #[must_use]
132    pub const fn trim_is_defined_for(kind: &crate::TypeKind) -> bool {
133        matches!(
134            kind,
135            crate::TypeKind::String { .. }
136                | crate::TypeKind::Sequence { .. }
137                | crate::TypeKind::Map { .. }
138        )
139    }
140}
141
142/// Where an effective member id came from.
143///
144/// Needed alongside the value: layout uses the number, a collision
145/// diagnostic uses the origin to point at the declaration responsible.
146/// Spec §4.1.
147#[derive(Debug, Clone, PartialEq, Eq)]
148pub enum MemberIdOrigin {
149    /// `@id(n)` on the member itself.
150    Declared,
151    /// `@hashid` or `@hashid("name")`.
152    Hashed {
153        /// The name that was hashed, which is the member name unless an
154        /// explicit string was given.
155        source_name: String,
156    },
157    /// `@autoid(SEQUENTIAL)` on the enclosing aggregate.
158    AutoSequential,
159    /// `@autoid(HASH)` on the enclosing aggregate.
160    AutoHash,
161    /// The default rule: the previous member's id plus one.
162    PreviousPlusOne,
163}
164
165impl MemberIdOrigin {
166    /// Short name, for diagnostics.
167    #[must_use]
168    pub const fn as_str(&self) -> &'static str {
169        match self {
170            Self::Declared => "@id",
171            Self::Hashed { .. } => "@hashid",
172            Self::AutoSequential => "@autoid(SEQUENTIAL)",
173            Self::AutoHash => "@autoid(HASH)",
174            Self::PreviousPlusOne => "previous member + 1",
175        }
176    }
177}
178
179/// The effective member id together with where it came from. Spec §4.1.
180#[derive(Debug, Clone, PartialEq, Eq)]
181pub struct MemberIdAssignment {
182    /// The id the wire format uses.
183    pub effective: u32,
184    /// How that id was arrived at.
185    pub origin: MemberIdOrigin,
186}
187
188/// Where `@verbatim` text is placed. IDL 4.2 §8.3.5.1.
189#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
190pub enum VerbatimPlacement {
191    /// Start of the generated file.
192    BeginFile,
193    /// Immediately before the declaration.
194    BeforeDeclaration,
195    /// Inside the declaration, at its start.
196    BeginDeclaration,
197    /// Inside the declaration, at its end.
198    EndDeclaration,
199    /// Immediately after the declaration.
200    AfterDeclaration,
201    /// End of the generated file.
202    EndFile,
203}
204
205impl VerbatimPlacement {
206    /// Every placement IDL 4.2 defines.
207    pub const ALL: [Self; 6] = [
208        Self::BeginFile,
209        Self::BeforeDeclaration,
210        Self::BeginDeclaration,
211        Self::EndDeclaration,
212        Self::AfterDeclaration,
213        Self::EndFile,
214    ];
215}
216
217/// One `@verbatim` directive.
218#[derive(Debug, Clone, PartialEq, Eq)]
219pub struct VerbatimDirective {
220    /// Target language, or `*` for all.
221    pub language: String,
222    /// Where the text goes.
223    pub placement: VerbatimPlacement,
224    /// The text itself.
225    pub text: String,
226}
227
228/// `@range`, `@min` and `@max` combined. DDS-XTypes 1.3 Annex B.
229#[derive(Debug, Clone, Copy, PartialEq, Eq)]
230pub struct RangeConstraint {
231    /// Lower bound expression.
232    pub min: Option<Id<ConstExprNode>>,
233    /// Upper bound expression.
234    pub max: Option<Id<ConstExprNode>>,
235}
236
237/// XTypes information attached to a node. Spec §4.1.
238///
239/// Absent rather than defaulted when a node carries no XTypes
240/// information: a node with no extensibility annotation must not be
241/// taken to claim `Final`.
242#[derive(Debug, Clone, Default, PartialEq, Eq)]
243pub struct XTypesFacet {
244    /// `@final` / `@appendable` / `@mutable`.
245    pub extensibility: Option<Extensibility>,
246    /// `@key`.
247    pub is_key: bool,
248    /// `@optional`.
249    pub is_optional: bool,
250    /// `@external`.
251    pub is_external: bool,
252    /// `@must_understand`.
253    pub is_must_understand: bool,
254    /// The declaration spelled the name with the escape underscore —
255    /// `_long` for `long`.
256    ///
257    /// IDL 4.2 §7.2.3.2 makes the underscore "a purely lexical
258    /// convention" that is not part of the name, so the member name is
259    /// stored stripped and this flag is the only trace of the spelling.
260    /// One consumer: member-id hashing under
261    /// `HashedNameSpelling::AsWritten`, the measured RTI Connext
262    /// reading, which hashes `_long` and `_foo` as written — keyword or
263    /// not (rtiddsgen 7.7.0, TypeCode member ids 79659193 for `_long`
264    /// against 56906255 for `long`). §7.2.3 admits no identifier that
265    /// starts with an underscore, so the spelling is always exactly
266    /// `_` plus the stored name and a flag carries it whole.
267    pub escaped: bool,
268    /// `@non_serialized`.
269    pub is_non_serialized: bool,
270    /// `@nested`.
271    pub is_nested: bool,
272    /// `@external` written on the element of a sequence or array, or on
273    /// the value of a map — `sequence<@external Foo, 42>`.
274    ///
275    /// DDS-XTypes 1.3 §7.3.1.2.1.4: "To declare the elements of a
276    /// collection type external, apply the annotation to the collection
277    /// declaration"; it is the `element_external` property of the Type
278    /// System Model and the `IS_EXTERNAL` bit of the `CollectionElementFlag`
279    /// of the collection's `TypeIdentifier`. Set on the collection's own
280    /// node, like `element_try_construct`.
281    pub element_is_external: bool,
282    /// `@external` written on the key of a map —
283    /// `map<@external Key, Value>`. The `IS_EXTERNAL` bit of the map
284    /// identifier's `key_flags`.
285    pub key_is_external: bool,
286    /// `@nested` was written on this node, with whichever value.
287    ///
288    /// `@nested(FALSE)` on a type in a module annotated
289    /// `@default_nested(TRUE)` has to be told from a type that says
290    /// nothing (DDS-XTypes 1.3 §7.3.1.2.1.7): both have
291    /// `is_nested == false`, and only the first one is not nested.
292    pub nested_explicit: bool,
293    /// `@default_literal` on an enumerator.
294    pub is_default_literal: bool,
295    /// `@try_construct` written on this node itself.
296    ///
297    /// On a member or a union case it is that member's behaviour. On an
298    /// alias it is the behaviour a use of the alias takes when the use
299    /// site says nothing — see
300    /// [`crate::Ast::effective_try_construct`].
301    pub try_construct: Option<TryConstruct>,
302    /// `@try_construct` written on a collection's element.
303    ///
304    /// `sequence<@try_construct(TRIM) String5, 10>` is §7.3.1.2.1.12.4's
305    /// own example. It lives on the collection and not on the element
306    /// type, because the element type is shared and the behaviour is
307    /// this collection's: two sequences of one `String5` may want
308    /// different answers. For a `map` this is the **value**; the key has
309    /// its own field.
310    ///
311    /// For an array declared as a member the annotation is written in
312    /// front of the member's type, and Table 17's `ARRAY_TYPE` row knows
313    /// no member-level behaviour at all — only "the TryConstruct
314    /// behavior associated with T1 element type" — so an array member's
315    /// annotation lands here rather than in `try_construct`.
316    pub element_try_construct: Option<TryConstruct>,
317    /// `@try_construct` written on a `map`'s key.
318    ///
319    /// Table 17 admits only `DISCARD` here: "If a key element of O2
320    /// cannot construct the corresponding key type of T1 the entire map
321    /// O2 cannot construct any object of type T1", with no TryConstruct
322    /// escape, and `opendds_idl` refuses anything else outright. The
323    /// field exists so that a declaration that writes `DISCARD`
324    /// explicitly round-trips, and so the validation rule has something
325    /// to read.
326    pub key_try_construct: Option<TryConstruct>,
327    /// `@default_nested` written on a module — DDS-XTypes 1.3
328    /// §7.3.1.2.1.7.
329    ///
330    /// ```idl
331    /// @annotation default_nested { boolean value default TRUE; };
332    /// ```
333    ///
334    /// "Applied to a module ... the annotation indicates whether the
335    /// types defined within it are nested by default. ... value defaults
336    /// to that of the enclosing module. If a top-level module is not
337    /// annotated, the default is FALSE."
338    ///
339    /// `None` is "the module says nothing", which is what makes the
340    /// inheritance from the enclosing module work — a module annotated
341    /// `@default_nested(FALSE)` inside one annotated
342    /// `@default_nested(TRUE)` has to be distinguishable from one that
343    /// was not annotated at all. [`crate::Ast::effective_nested`] walks
344    /// the chain.
345    pub default_nested: Option<bool>,
346    /// `@key` written on a union's discriminator —
347    /// `@try_construct` written on a union's discriminator.
348    ///
349    /// `union U switch (@try_construct K)` — §7.3.1.2.1.12.5. Table 19
350    /// admits only `DEFAULT` for the discriminator: a discriminator is
351    /// not a string, sequence or map, so `TRIM` is Table 11's
352    /// unspecified case.
353    pub discriminator_try_construct: Option<TryConstruct>,
354    /// The user-declared annotations written in front of the element type
355    /// of a collection — `sequence<@Ann long, 4>`, `map<string, @Ann
356    /// long, 4>` — or after an array declarator's name, `long a @Ann [4]`.
357    ///
358    /// DDS-XTypes 1.3 §7.2.2.6: an annotation is attached to "an
359    /// element/member/literal of an aggregated/collection/enumerated
360    /// type". The collection is anonymous, so the annotations of its
361    /// element sit on the collection node, beside the element's
362    /// `@try_construct` and `@external`; they reach the `ann_custom` of
363    /// `CompleteCollectionElement` and make the collection a non-plain
364    /// type (§7.3.4.1).
365    pub element_annotations: Vec<AppliedAnnotation>,
366    /// The user-declared annotations written in front of a map's key
367    /// type: `map<@Ann string, long, 4>`. As [`Self::element_annotations`],
368    /// for the key.
369    pub key_annotations: Vec<AppliedAnnotation>,
370    /// The user-declared annotations written in front of a union's
371    /// discriminator type: `union U switch (@Ann long)`.
372    ///
373    /// DDS-XTypes 1.3 §7.3.1.2.5: "Annotations can be applied to the
374    /// implicit discriminator member of a union type by applying them to
375    /// the discriminator type declaration in the header of the union
376    /// type's definition". They reach the `ann_custom` of
377    /// `CompleteDiscriminatorMember`.
378    pub discriminator_annotations: Vec<AppliedAnnotation>,
379    /// `@key` written on a union's discriminator.
380    ///
381    /// `union MyCommand switch (@key CommandKind)` — DDS-XTypes 1.3
382    /// Table 21 lists `@key` against "Structure Members, Union
383    /// discriminator" and §7.2.2.4.4.4.8 gives that declaration as its
384    /// own example, so the clause permits it outright.
385    ///
386    /// It sits on the union rather than on a member because a
387    /// discriminator is not a `MemberNode`: it is a type reference the
388    /// union holds, and `is_key` on a member has nothing to attach to.
389    /// A union that carries this is a keyed topic type whose key holder
390    /// is the discriminator alone.
391    ///
392    /// **What reads it.** It reaches `UnionDiscriminatorFlag::IS_KEY` in
393    /// the TypeObject, which is where Table 19's `UNION_TYPE` rule —
394    /// "Either the discriminators of both T1 and T2 are keys or neither
395    /// are keys" — can see it, and it reaches
396    /// `zerodds_ast_layout::keyhash::union_key_discriminator`, which
397    /// builds the key holder from the discriminator alone.
398    ///
399    /// Filled by the front end unless it was asked to refuse the
400    /// annotation — `zerodds_idl_lower::KeyOnUnionDiscriminator::Refused`.
401    /// The measured toolchains refuse the declaration (Cyclone's `idlc`,
402    /// `rtiddsgen`) or generate no topic type for a union
403    /// (`fastddsgen`), so a KeyHash over a union discriminator is one
404    /// none of them computes; the clause permits it all the same.
405    pub discriminator_is_key: bool,
406    /// Effective member id and its origin.
407    pub member_id: Option<MemberIdAssignment>,
408    /// `@bit_bound`.
409    pub bit_bound: Option<u16>,
410    /// `@default`.
411    pub default_value: Option<Id<ConstExprNode>>,
412    /// `@range` / `@min` / `@max`.
413    pub range: Option<RangeConstraint>,
414    /// `@unit`.
415    pub unit: Option<String>,
416    /// `@verbatim`, in declaration order.
417    pub verbatim: Vec<VerbatimDirective>,
418    /// `@autoid` on an aggregate, which decides how its members without
419    /// an `@id` are numbered.
420    ///
421    /// Kept alongside each member's own [`MemberIdOrigin`]: the origin
422    /// says how one id was arrived at, this says what the declaration
423    /// asked for, and a backend regenerating IDL needs the latter.
424    pub autoid: Option<AutoId>,
425    /// `@topic`.
426    pub topic: Option<TopicAnnotation>,
427    /// `@data_representation`, as the mask of allowed representations.
428    pub data_representation: Option<DataRepresentationMask>,
429    /// `@ignore_literal_names` on an enumeration.
430    pub ignore_literal_names: bool,
431}
432
433/// `@autoid` policy. DDS-XTypes 1.3 §7.3.1.2.1.1.
434#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
435pub enum AutoId {
436    /// Ids follow declaration order.
437    Sequential,
438    /// Ids are the hash of the member name.
439    Hash,
440}
441
442impl AutoId {
443    /// Both policies XTypes defines.
444    pub const ALL: [Self; 2] = [Self::Sequential, Self::Hash];
445
446    /// Source spelling of the argument.
447    #[must_use]
448    pub const fn as_str(self) -> &'static str {
449        match self {
450            Self::Sequential => "SEQUENTIAL",
451            Self::Hash => "HASH",
452        }
453    }
454}
455
456/// `@topic(name = "...", platform = "...")`. DDS-XTypes 1.3 Annex B.
457#[derive(Debug, Clone, PartialEq, Eq, Default)]
458pub struct TopicAnnotation {
459    /// The topic name, when one was given.
460    pub name: Option<String>,
461    /// The platform the annotation applies to, `*` for all.
462    pub platform: Option<String>,
463}
464
465/// The data representations a type allows.
466///
467/// A mask rather than one value: `@data_representation(XCDR1 | XCDR2)`
468/// is legal and means both. DDS-XTypes 1.3 §7.6.3.1.1.
469#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
470pub struct DataRepresentationMask {
471    /// `XCDR1` is allowed.
472    pub xcdr1: bool,
473    /// `XCDR2` is allowed.
474    pub xcdr2: bool,
475    /// `XML` is allowed.
476    pub xml: bool,
477}
478
479impl DataRepresentationMask {
480    /// The `DataRepresentationMask` value of a type that carries no
481    /// `@data_representation`: "interpreted as if the
482    /// DataRepresentationMask value was set to 0xffffffff" (DDS-XTypes
483    /// 1.3 §7.3.1.2.1.15), which allows every representation.
484    pub const ALL: Self = Self {
485        xcdr1: true,
486        xcdr2: true,
487        xml: true,
488    };
489
490    /// The value `0xffffffff` of the annotation-less case, as the 32-bit
491    /// mask of §7.3.1.2.1.15.
492    pub const ALL_BITS: u32 = 0xffff_ffff;
493
494    /// The positions of the `DataRepresentationMask` bitmask:
495    /// `XCDR1` 0, `XML` 1, `XCDR2` 2 (§7.3.1.2.1.15).
496    #[must_use]
497    pub const fn bits(self) -> u32 {
498        (self.xcdr1 as u32) | ((self.xml as u32) << 1) | ((self.xcdr2 as u32) << 2)
499    }
500
501    /// Whether the mask allows nothing, which no declaration should
502    /// produce and a validation rule reports.
503    #[must_use]
504    pub const fn is_empty(self) -> bool {
505        !self.xcdr1 && !self.xcdr2 && !self.xml
506    }
507}
508
509impl XTypesFacet {
510    /// The effective member id, if one has been assigned.
511    #[must_use]
512    pub fn effective_member_id(&self) -> Option<u32> {
513        self.member_id.as_ref().map(|a| a.effective)
514    }
515}