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}