Skip to main content

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}