zerodds_ast_layout/plan.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Layout plans. Spec §7.
4
5use crate::key::Encoding;
6use alloc::vec::Vec;
7use core::fmt;
8use zerodds_ast_core::NodeId;
9
10/// EMHEADER length code. OMG XTypes 1.3 §7.4.2.
11///
12/// The code tells a reader how to skip a member it does not understand,
13/// which is what makes `@mutable` evolution work at all.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
15pub enum LengthCode {
16 /// One byte.
17 Lc0,
18 /// Two bytes.
19 Lc1,
20 /// Four bytes.
21 Lc2,
22 /// Eight bytes.
23 Lc3,
24 /// Length in a following NEXTINT.
25 Lc4,
26 /// Length as the member's own leading word, which doubles as the
27 /// NEXTINT. Required for a variable-length member.
28 Lc5,
29 /// Length is NEXTINT times four.
30 Lc6,
31 /// Length is NEXTINT times eight.
32 Lc7,
33}
34
35impl LengthCode {
36 /// Every code XTypes 1.3 defines.
37 pub const ALL: [Self; 8] = [
38 Self::Lc0,
39 Self::Lc1,
40 Self::Lc2,
41 Self::Lc3,
42 Self::Lc4,
43 Self::Lc5,
44 Self::Lc6,
45 Self::Lc7,
46 ];
47
48 /// The three-bit code as it appears in the EMHEADER.
49 #[must_use]
50 pub const fn bits(self) -> u8 {
51 match self {
52 Self::Lc0 => 0,
53 Self::Lc1 => 1,
54 Self::Lc2 => 2,
55 Self::Lc3 => 3,
56 Self::Lc4 => 4,
57 Self::Lc5 => 5,
58 Self::Lc6 => 6,
59 Self::Lc7 => 7,
60 }
61 }
62
63 /// The fixed payload size a code implies, when it implies one.
64 #[must_use]
65 pub const fn fixed_size(self) -> Option<u32> {
66 match self {
67 Self::Lc0 => Some(1),
68 Self::Lc1 => Some(2),
69 Self::Lc2 => Some(4),
70 Self::Lc3 => Some(8),
71 _ => None,
72 }
73 }
74
75 /// Whether the member's own leading length word serves as the
76 /// NEXTINT, which is what distinguishes LC5 from LC4.
77 #[must_use]
78 pub const fn reuses_leading_length(self) -> bool {
79 matches!(self, Self::Lc5)
80 }
81}
82
83/// How a member is framed.
84///
85/// Five shapes, one per rule. The two `Optional*` ones are what
86/// §7.4.3.5.1 calls an `OPT_FMEMBER` — an `@optional` member of a type
87/// that is **not** `@mutable` — and they are separate variants rather
88/// than a flag on the other two because their absence is written and
89/// an `MMEMBER`'s absence is not. A `@mutable` member that is left out
90/// leaves no trace in the parameter list; an `OPT_FMEMBER` that is left
91/// out still occupies its place in the stream.
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub enum MemberHeader {
94 /// No header: the member's bytes follow directly.
95 ///
96 /// `NOPT_FMEMBER`, XTypes 1.3 §7.4.3.5.3 rule (18).
97 NoHeader,
98 /// PL_CDR1 parameter header for an `MMEMBER`. XTypes 1.3 §7.4.1.2,
99 /// rules (24) and (25).
100 ///
101 /// The short form is `[id:u16][length:u16]`; the extended form is
102 /// `[PID_EXTENDED:u16][8:u16][id:u32][length:u32]`. The length field
103 /// carries the **unpadded** value length; the padding to the next
104 /// four-byte boundary follows the value and is not counted.
105 PidCdr1 {
106 /// Parameter id.
107 pid: u16,
108 /// Whether the extended form is required.
109 ///
110 /// Mandatory when the member id reaches `PID_EXTENDED_THRESHOLD`
111 /// **or** the value is longer than `0xFFFF` bytes — the second
112 /// condition is easy to miss and makes a long member unreadable
113 /// when it is.
114 extended: bool,
115 /// Whether a reader must discard the sample rather than skip
116 /// this member.
117 ///
118 /// §7.4.1.2.2: `FLAG_MUST_UNDERSTAND` "shall be set if and only
119 /// if the must_understand property of the member is set to
120 /// true". Bit 14 of the short header's id, `FLAG_2`
121 /// (`0x40000000`) of the extended header's `eMemberHeader`.
122 must_understand: bool,
123 },
124 /// PL_CDR2 EMHEADER for an `MMEMBER`. XTypes 1.3 §7.4.2, rule (22).
125 EmHeader {
126 /// Length code.
127 lc: LengthCode,
128 /// Whether a reader must fail rather than skip this member.
129 must_understand: bool,
130 },
131 /// An `OPT_FMEMBER` under XCDR1: the same parameter header as an
132 /// `MMEMBER`, and **a declared length of zero when the member takes
133 /// no value**. XTypes 1.3 §7.4.3.5.3 rule (19), §7.4.1.2.
134 ///
135 /// The clause is explicit: "If the member takes no value within that
136 /// particular object, the data length indicated by the header shall
137 /// be zero." Cyclone, RTI and Fast DDS all write the header either
138 /// way — `crates/ast-layout/tests/fixtures/vendor_bytes.md` §1.
139 /// Omitting it instead leaves the sample four bytes short of what
140 /// every peer counts on and puts every following member at the wrong
141 /// offset.
142 OptionalCdr1 {
143 /// Parameter id.
144 pid: u16,
145 /// Whether the extended form is required, decided by the same
146 /// rule [`MemberHeader::PidCdr1`] uses.
147 extended: bool,
148 /// Whether a reader must discard the sample rather than skip
149 /// this member, carried in the same bit and by the same clause
150 /// as [`MemberHeader::PidCdr1`]'s.
151 ///
152 /// §7.4.1.2.2 conditions the flag on the member's property and
153 /// not on the enclosing type's extensibility, so an
154 /// `@optional @must_understand` member of a `@final` type
155 /// carries it too. No vendor row measures that combination;
156 /// this is the reading the clause's "if and only if" gives.
157 must_understand: bool,
158 },
159 /// An `OPT_FMEMBER` under XCDR2: a BOOLEAN `<is_present>`, then the
160 /// value when it is present. XTypes 1.3 §7.4.3.5.2, rule (20).
161 ///
162 /// One byte, not an EMHEADER. The distinction is four bytes wide and
163 /// moves the value: `@final struct { long a; @optional octet b;
164 /// long c; }` puts `b` at offset 5 behind a one-byte flag, where an
165 /// EMHEADER would put it at 8.
166 OptionalFlag,
167}
168
169impl MemberHeader {
170 /// Whether the member's absence is written rather than left out.
171 ///
172 /// True for the two `OPT_FMEMBER` forms and false for everything
173 /// else. An `MMEMBER` that is absent contributes nothing to the
174 /// parameter list; an `OPT_FMEMBER` that is absent contributes its
175 /// header with a length of zero, or its `<is_present>` byte.
176 #[must_use]
177 pub const fn writes_absence(self) -> bool {
178 matches!(self, Self::OptionalCdr1 { .. } | Self::OptionalFlag)
179 }
180}
181
182/// Where a member sits.
183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184pub enum Offset {
185 /// A fixed byte offset from the start of the aggregate.
186 Fixed(u32),
187 /// Determined at runtime, because something before it is
188 /// variable-length.
189 Dynamic,
190}
191
192/// How much space a type takes.
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194pub enum Extent {
195 /// Exactly this many bytes, every time.
196 Sized(u32),
197 /// Depends on the value.
198 Unbounded,
199}
200
201impl Extent {
202 /// The byte count, when the type is fixed-size.
203 #[must_use]
204 pub const fn sized(self) -> Option<u32> {
205 match self {
206 Self::Sized(n) => Some(n),
207 Self::Unbounded => None,
208 }
209 }
210
211 /// Whether the type is fixed-size.
212 #[must_use]
213 pub const fn is_sized(self) -> bool {
214 matches!(self, Self::Sized(_))
215 }
216}
217
218/// One member's place in the wire form.
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
220pub struct MemberPlan {
221 /// The member this plan is for.
222 pub member: NodeId,
223 /// Its effective XTypes member id.
224 pub member_id: u32,
225 /// How it is framed.
226 pub header: MemberHeader,
227 /// Where it sits.
228 pub offset: Offset,
229 /// The alignment it requires.
230 pub align: u8,
231}
232
233/// A type's wire layout under one key.
234#[derive(Debug, Clone, PartialEq, Eq)]
235pub struct TypeLayout {
236 /// The type this plan is for.
237 pub type_: NodeId,
238 /// The alignment the whole type requires.
239 pub align: u8,
240 /// How much space it takes.
241 pub extent: Extent,
242 /// Whether a DHEADER precedes the members. XCDR2 only.
243 pub needs_dheader: bool,
244 /// Whether a `PID_LIST_END` sentinel terminates the parameter list.
245 /// PL_CDR1 only; XTypes 1.3 §7.4.1.2.4.
246 pub needs_sentinel: bool,
247 /// How a `@mutable` union's discriminator is framed, and `None`
248 /// everywhere else.
249 ///
250 /// Rules (27) and (28) frame a `@mutable` union's discriminator
251 /// exactly like a member — an `MMEMBER` at member id 0 — but a
252 /// discriminator is not a member node, so there is nothing for a
253 /// [`MemberPlan`] to name. It gets a field of its own rather than a
254 /// synthetic `MemberPlan`, because a synthetic node id is a thing
255 /// every consumer would have to know not to look up.
256 pub discriminator: Option<MemberHeader>,
257 /// The members, in wire order.
258 ///
259 /// For a union these are the case members, one per case, and only
260 /// the selected one is written. Populated only for a `@mutable`
261 /// union: rules (26) and (30) give a union of any other
262 /// extensibility no per-member framing to plan.
263 pub members: Vec<MemberPlan>,
264}
265
266impl TypeLayout {
267 /// A plan for a type with no members.
268 #[must_use]
269 pub fn scalar(type_: NodeId, align: u8, extent: Extent) -> Self {
270 Self {
271 type_,
272 align,
273 extent,
274 needs_dheader: false,
275 needs_sentinel: false,
276 discriminator: None,
277 members: Vec::new(),
278 }
279 }
280
281 /// The plan for one member, by its identity.
282 #[must_use]
283 pub fn member(&self, node: NodeId) -> Option<&MemberPlan> {
284 self.members.iter().find(|m| m.member == node)
285 }
286}
287
288/// Why a layout could not be computed.
289#[derive(Debug, Clone, PartialEq, Eq)]
290#[non_exhaustive]
291pub enum LayoutError {
292 /// A reference in the type graph is not resolved, so the size and
293 /// alignment behind it are unknown. Guessing would produce a plan
294 /// that is wrong on the wire, which is worse than no plan.
295 Unresolved {
296 /// The type whose reference could not be followed.
297 node: NodeId,
298 },
299 /// The construct has no representation in the requested encoding.
300 UnsupportedForEncoding {
301 /// The offending type.
302 node: NodeId,
303 /// The encoding asked for.
304 encoding: Encoding,
305 /// Why it does not fit.
306 reason: &'static str,
307 },
308 /// A key-holder plan was asked for in little-endian. The key holder
309 /// is defined big-endian; honouring the request would produce a
310 /// wrong hash. DDSI-RTPS 2.5 §9.6.3.8.
311 KeyMustBeBigEndian,
312 /// The type graph nests deeper than the planner will follow.
313 TooDeep {
314 /// Where the descent stopped.
315 node: NodeId,
316 },
317 /// A member's type closes a cycle with nothing on it that can
318 /// terminate a value.
319 ///
320 /// IDL 4.2 §7.4.1.4.4.4.4 admits a recursive structure or union
321 /// "via members that have a sequence type" — "an incomplete type
322 /// can only appear as the element type of a sequence definition" —
323 /// because the element count is what makes a value finite. A map
324 /// carries a count the same way, and an `@optional` member carries
325 /// the same property in the other form: the member can be absent,
326 /// and an absent member ends the value where a zero-length sequence
327 /// would. A cycle over any of the three is planned; a cycle over
328 /// none of them has no finite value at all and is refused here, by
329 /// name rather than by a depth limit that would report a number and
330 /// not a reason. An array is the case that fails it: its element
331 /// count is fixed and non-zero, so nothing about a value can stop
332 /// the descent.
333 ///
334 /// XTypes 1.3 §7.3.1.2.1.4 is not a second ground for this refusal.
335 /// It refuses a *cyclic value* — "if the graph of objects has
336 /// cycles, it is not serializable" — and a recursive type whose
337 /// values are trees has none. That distinction is a value-time
338 /// question and is answered at value time; see
339 /// `zerodds_wire_codec::MAX_VALUE_DEPTH`.
340 RecursionOutsideCollection {
341 /// The member whose type names an enclosing type.
342 node: NodeId,
343 },
344 /// The model rejected a lookup.
345 Model(zerodds_ast::AstError),
346}
347
348impl fmt::Display for LayoutError {
349 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
350 match self {
351 Self::Unresolved { node } => {
352 write!(f, "type {node:?} has an unresolved reference")
353 }
354 Self::UnsupportedForEncoding {
355 node,
356 encoding,
357 reason,
358 } => write!(f, "type {node:?} cannot be {encoding:?}: {reason}"),
359 Self::KeyMustBeBigEndian => {
360 write!(f, "the key holder is defined big-endian")
361 }
362 Self::TooDeep { node } => write!(f, "type graph below {node:?} nests too deeply"),
363 Self::RecursionOutsideCollection { node } => write!(
364 f,
365 "{node:?} closes a type cycle with nothing on it that terminates a value — no \
366 sequence, no map and no @optional member — which IDL 4.2 \
367 \u{a7}7.4.1.4.4.4.4 does not admit; XTypes 1.3 \u{a7}7.3.1.2.1.4 refuses only a \
368 cyclic value, which is a different question and is answered at value time"
369 ),
370 Self::Model(error) => write!(f, "{error}"),
371 }
372 }
373}
374
375impl core::error::Error for LayoutError {}
376
377impl From<zerodds_ast::AstError> for LayoutError {
378 fn from(error: zerodds_ast::AstError) -> Self {
379 Self::Model(error)
380 }
381}
382
383/// How deep the planner follows a type graph.
384///
385/// A recursive type is legal in the model, and its layout is unbounded
386/// by construction; the limit stops the descent rather than letting a
387/// hostile model run it forever.
388pub const MAX_DEPTH: usize = 64;
389
390/// Round `offset` up to the next multiple of `align`.
391#[must_use]
392pub const fn align_to(offset: u32, align: u8) -> u32 {
393 let align = align as u32;
394 if align <= 1 {
395 return offset;
396 }
397 let remainder = offset % align;
398 if remainder == 0 {
399 offset
400 } else {
401 offset.saturating_add(align - remainder)
402 }
403}
404
405// -- the resolved scalar write ------------------------------------------
406
407/// What one scalar position puts on the wire, with every policy already
408/// answered.
409///
410/// The plan's other half. [`MemberPlan`] says where a member sits and
411/// how it is framed; this says what the value itself is, and the two
412/// together leave a code generator nothing to decide.
413///
414/// ## Why this exists
415///
416/// Twelve wire questions used to be answered where the code was
417/// generated. `zerodds-ast-layout` held the *rules* — `enumeration_bit_bound`,
418/// `enumerator_wire_value` and their neighbours — but every generator
419/// had to remember to ask them, and five of them did, and a test counted
420/// the call sites to notice when one stopped.
421///
422/// A counting test catches a site that stops asking. It does not catch a
423/// site that never asked, and it does not catch a site that asks with
424/// the wrong argument — `WcharFraming` is asked ten times, each with a
425/// position, and nine right against one wrong is invisible to a counter
426/// and a different octet on the wire.
427///
428/// So the answer moves here. A generator matches on this and calls the
429/// runtime; there is no axis left for it to read and no argument for it
430/// to get wrong.
431#[derive(Debug, Clone, Copy, PartialEq, Eq)]
432pub enum ScalarWrite {
433 /// One octet, 0 or 1.
434 Bool,
435 /// An unsigned octet.
436 U8,
437 /// A signed octet.
438 I8,
439 /// A code point kept to its low octet.
440 Char,
441 /// A code point, with the three policies a `wchar` carries.
442 ///
443 /// The width is its own setting and not a reading of the `wstring`
444 /// policy: `rmw_cyclonedds_cpp` writes two octets for a scalar
445 /// `wchar` and four inside a `wstring`.
446 Wchar {
447 /// Two octets or four.
448 width: crate::key::WcharWidth,
449 /// Whether GIOP's length octet precedes it **at this position**.
450 /// §7.4.1.1.2; the resolved answer, not the policy.
451 length_octet: bool,
452 /// Which order its code units go in.
453 order: crate::key::WstringUnitOrder,
454 },
455 /// A signed 16-bit integer.
456 I16,
457 /// An unsigned 16-bit integer.
458 U16,
459 /// A signed 32-bit integer.
460 I32,
461 /// An unsigned 32-bit integer.
462 U32,
463 /// A signed 64-bit integer.
464 I64,
465 /// An unsigned 64-bit integer.
466 U64,
467 /// Binary32.
468 F32,
469 /// Binary64.
470 F64,
471 /// Binary128.
472 F128,
473 /// A narrow string: a length and the octets.
474 Str,
475 /// A wide string, with the two policies its length and units carry.
476 Wstr {
477 /// What the length counts and whether a terminator follows.
478 policy: crate::key::WstringLengthPolicy,
479 /// Which order its code units go in.
480 order: crate::key::WstringUnitOrder,
481 /// What its characters are — UTF-16 code units or their own
482 /// UTF-8 octets. Under [`crate::key::WstringCodeUnits::Utf8`]
483 /// the length is the UTF-8 octet count and `policy`'s C1 and C3
484 /// have nothing to say; only the terminator still reaches the
485 /// wire. Resolved here so the walk reads no axis.
486 code_units: crate::key::WstringCodeUnits,
487 },
488 /// A packed decimal.
489 Fixed {
490 /// The digits the type declared.
491 digits: u8,
492 /// The scale the type declared.
493 scale: u8,
494 /// Whether the wire carries the declared digit count or the
495 /// value's own. §9.3.2.8 gives the packing and not the count,
496 /// and the four measured implementations split two and two.
497 count: crate::key::FixedDigitCount,
498 },
499 /// An enumeration, in a holder of the resolved width.
500 Enum {
501 /// How many bits the holder has, after the policy that decides
502 /// whether a front end kept the declared `@bit_bound`.
503 bits: u16,
504 },
505 /// A bitmask, in the holder Table 33 gives for its bound.
506 Bitmask {
507 /// How many bits the holder has.
508 bits: u16,
509 },
510}
511
512/// The scalar write one position asks for, resolved against the key.
513///
514/// `None` for a type that is not a scalar — a string, a collection, an
515/// aggregate — which the caller frames itself and whose parts come back
516/// through here.
517///
518/// `position` is the one thing a caller still supplies, because it is
519/// not a property of the type: the same `wchar` is framed differently as
520/// a member and as an element of a sequence, and only the caller knows
521/// which of the two it is looking at.
522#[must_use]
523pub fn scalar_write(
524 ast: &zerodds_ast::Ast,
525 type_: zerodds_ast_core::Id<zerodds_ast::TypeNode>,
526 key: crate::key::LayoutKey,
527 position: crate::key::WcharPosition,
528) -> Option<ScalarWrite> {
529 use zerodds_ast::{PrimitiveKind, TypeKind};
530 let mut current = type_;
531 // Through the aliases, because a `typedef` writes what its target
532 // writes and a caller that stopped at the alias would find no
533 // scalar where there is one.
534 for _ in 0..32 {
535 let node = ast.type_(current).ok()?;
536 match &node.kind {
537 TypeKind::Alias { aliased } => current = aliased.resolved()?,
538 TypeKind::Primitive(kind) => {
539 return Some(match kind {
540 PrimitiveKind::Boolean => ScalarWrite::Bool,
541 PrimitiveKind::Octet | PrimitiveKind::UInt8 => ScalarWrite::U8,
542 PrimitiveKind::Int8 => ScalarWrite::I8,
543 PrimitiveKind::Char => ScalarWrite::Char,
544 PrimitiveKind::WChar => ScalarWrite::Wchar {
545 width: key.wchar_width,
546 length_octet: key.wchar_framing.has_length_octet(position),
547 order: key.wstring_unit_order,
548 },
549 PrimitiveKind::Int16 => ScalarWrite::I16,
550 PrimitiveKind::UInt16 => ScalarWrite::U16,
551 PrimitiveKind::Int32 => ScalarWrite::I32,
552 PrimitiveKind::UInt32 => ScalarWrite::U32,
553 PrimitiveKind::Int64 => ScalarWrite::I64,
554 PrimitiveKind::UInt64 => ScalarWrite::U64,
555 PrimitiveKind::Float32 => ScalarWrite::F32,
556 PrimitiveKind::Float64 => ScalarWrite::F64,
557 PrimitiveKind::Float128 => ScalarWrite::F128,
558 });
559 }
560 TypeKind::String {
561 width: zerodds_ast::CharWidth::Char,
562 ..
563 } => return Some(ScalarWrite::Str),
564 TypeKind::String {
565 width: zerodds_ast::CharWidth::WChar,
566 ..
567 } => {
568 return Some(ScalarWrite::Wstr {
569 policy: key.wstring_length,
570 order: key.wstring_unit_order,
571 code_units: key.wstring_code_units,
572 });
573 }
574 TypeKind::Fixed { digits, scale } => {
575 return Some(ScalarWrite::Fixed {
576 digits: *digits,
577 scale: *scale,
578 count: key.fixed_digits,
579 });
580 }
581 TypeKind::Enumeration { .. } => {
582 return Some(ScalarWrite::Enum {
583 bits: crate::xcdr::enumeration_bit_bound(ast, current, key.enum_holder),
584 });
585 }
586 TypeKind::Bitmask { .. } => {
587 return Some(ScalarWrite::Bitmask {
588 bits: ast.effective_bit_bound(current),
589 });
590 }
591 _ => return None,
592 }
593 }
594 None
595}
596
597/// What each enumerator of `type_` puts on the wire, resolved.
598///
599/// The mapping and not the policy. `enumerator_wire_value` is the rule
600/// and every generator used to call it with `key.enumerator_value`;
601/// asking here means a caller cannot pass the wrong policy, cannot skip
602/// the crossing, and cannot answer the axis by omission.
603///
604/// Ordered by declaration, which is the order both directions take:
605/// where two enumerators share a value, the first one wins in each.
606///
607/// # Errors
608/// The name of an enumerator whose wire value does not evaluate under
609/// this setting — never a mapping with one silently left out.
610pub fn enumerator_writes(
611 ast: &zerodds_ast::Ast,
612 type_: zerodds_ast_core::Id<zerodds_ast::TypeNode>,
613 key: crate::key::LayoutKey,
614) -> Result<alloc::vec::Vec<(alloc::string::String, i128)>, alloc::string::String> {
615 let mut out = alloc::vec::Vec::new();
616 for enumerator in ast.enumerators_of(type_) {
617 let node = ast.enumerator(*enumerator).map_err(|_| {
618 alloc::string::String::from("an enumerator handle the model does not hold")
619 })?;
620 let on_wire = crate::xcdr::enumerator_wire_value(ast, *enumerator, key.enumerator_value)
621 .ok_or_else(|| node.name.clone())?;
622 out.push((node.name.clone(), on_wire));
623 }
624 Ok(out)
625}
626
627/// Whether an enumeration's declared values reach the wire unchanged.
628///
629/// True where the setting is the specification's, and the reason the
630/// generated code is a single call rather than a chain: every
631/// enumerator already carries the integer it serialises as.
632#[must_use]
633pub fn enumerators_are_declared(key: crate::key::LayoutKey) -> bool {
634 key.enumerator_value.is_default()
635}
636
637/// How a `map`'s entries are ordered on the wire, resolved.
638///
639/// `Ok(None)` is declaration order — the entries go where the sample put
640/// them. `Ok(Some(_))` is the ordering a key comparison gives.
641/// `Err(())` is the one case a caller has to refuse: a setting asked for
642/// ascending order over a key type that has **no value order**.
643///
644/// The refusal is here and not at the generator because it is the same
645/// refusal in every column, and a generator that decided it for itself
646/// would be answering a wire question.
647///
648/// # Errors
649/// [`NoKeyOrder`] when ascending order was asked for and the key type
650/// has none.
651pub fn map_entry_ordering(
652 ast: &zerodds_ast::Ast,
653 type_: zerodds_ast_core::Id<zerodds_ast::TypeNode>,
654 key: crate::key::LayoutKey,
655) -> Result<Option<crate::key::MapKeyOrdering>, NoKeyOrder> {
656 let ordering = crate::map_key_ordering(ast, type_, key.map_entries);
657 if !key.map_entries.is_default() && ordering.is_none() {
658 return Err(NoKeyOrder);
659 }
660 Ok(ordering)
661}
662
663/// A `map` asked for ascending entry order over a key type that has
664/// **no value order**.
665///
666/// Its own type rather than `()`, so a caller that ignores it has to
667/// say so and a reader of the signature knows what went wrong without
668/// finding the body.
669#[derive(Debug, Clone, Copy, PartialEq, Eq)]
670pub struct NoKeyOrder;
671
672/// Whether a negative case label of this union selects nothing.
673///
674/// One setting reproduces a peer whose real `union` narrows the
675/// discriminator before comparing, so a negative label of a signed type
676/// narrower than 32 bits matches no value. The resolved answer, so a
677/// generator neither re-derives it nor passes the wrong axis.
678#[must_use]
679pub fn union_negative_selects_nothing(
680 ast: &zerodds_ast::Ast,
681 type_: zerodds_ast_core::Id<zerodds_ast::TypeNode>,
682 key: crate::key::LayoutKey,
683) -> bool {
684 crate::xcdr::union_negative_matches_nothing(ast, type_, key.union_label)
685}
686
687/// How wide the holder of `type_`'s enumeration is, resolved.
688///
689/// The same answer [`ScalarWrite::Enum`] carries, for the two callers
690/// that need the width before they have an instruction — a reader
691/// sizing the integer it compares patterns against.
692#[must_use]
693pub fn enumeration_holder_bits(
694 ast: &zerodds_ast::Ast,
695 type_: zerodds_ast_core::Id<zerodds_ast::TypeNode>,
696 key: crate::key::LayoutKey,
697) -> u16 {
698 crate::xcdr::enumeration_bit_bound(ast, type_, key.enum_holder)
699}