Skip to main content

zerodds_lang_rules/
encode.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Encoder and decoder bodies, built from the layout plan. Spec §5, §6.2.
4//!
5//! The body of an `encode` is a language CST — statements, calls, casts —
6//! not a template with holes for names. A template would put the
7//! per-construct decisions back where no diff of two languages can see
8//! them, one hole at a time, which is what this rebuild took them out
9//! of.
10//!
11//! Every framing decision is read from `zerodds-ast-layout`, never
12//! re-derived here. The plan is where the length-code policy and the
13//! XCDR2 alignment rule live; a second derivation would be a second
14//! place to get them wrong.
15
16use zerodds_ast::{Ast, CharWidth, PrimitiveKind, TryConstruct, TypeKind, TypeNode};
17use zerodds_ast_core::{Id, NodeId};
18use zerodds_ast_layout::key::{
19    ElementPadding, FixedDigitCount, LayoutKey, PlCdr1HeaderPolicy, Purpose, WcharPosition,
20    WirePosition, WstringCodeUnits, WstringLengthCounts, WstringLengthPolicy, WstringTerminator,
21    WstringUnitOrder, WstringUnitWidth,
22};
23use zerodds_ast_layout::plan::{
24    Extent, LayoutError, MemberHeader, MemberPlan, ScalarWrite, TypeLayout,
25};
26use zerodds_ast_layout::xcdr::DepCollector;
27use zerodds_lang_cst::common::{Doc, Ident, Literal, Path, Visibility};
28use zerodds_lang_cst::rust;
29
30/// Why an encoder could not be built.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum EncodeError {
33    /// The layout could not be planned, with the plan's own reason.
34    NoLayout {
35        /// What the layout crate said.
36        detail: String,
37    },
38    /// The specification itself does not admit the type, in the
39    /// planner's own words and by its clause.
40    ///
41    /// Kept apart from [`Self::NoLayout`] because the two answer
42    /// different questions and `docs/specs/zerodds-verdicts-1.0.md`
43    /// gives them different exit codes: a gap is "a tool of **ours**
44    /// has not written it" (exit 6, `harness-gap`), and this is "it was
45    /// asked and said no" (exit 1, `refused`). Reporting a clause of
46    /// IDL 4.2 as a gap of this tree states something about this tree
47    /// that is not true.
48    ///
49    /// The distinction was a string until 2026-08-26, and twelve
50    /// generators each classified the same refusal on their own:
51    /// measured over `recursive-array`, ada and d answered exit 3, zig
52    /// answered 1, and twelve answered 6.
53    SpecRefuses {
54        /// The clause and what it refuses, from the planner.
55        detail: String,
56    },
57    /// A member's type has no writer.
58    ///
59    /// Reported rather than skipped: a member left out of an encoder is
60    /// a wire format that silently disagrees with its own plan.
61    NoWriter {
62        /// The member.
63        member: String,
64        /// What kind of type it is.
65        kind: &'static str,
66    },
67    /// An enumerator's wire value could not be computed.
68    ///
69    /// Reported rather than skipped, for the same reason as
70    /// [`Self::NoWriter`] and with a sharper edge. The C encoder's
71    /// enumeration chain initialises its holder to the **declared**
72    /// value and lets each arm overwrite it, so an enumerator that
73    /// reaches no arm is not absent from the stream — it is written
74    /// with the declared integer. Under
75    /// `zerodds_ast_layout::EnumeratorValue::Ordinal` that is a
76    /// different integer from the one every other column writes, and
77    /// nothing at run time would say so.
78    NoEnumeratorValue {
79        /// The enumeration the enumerator belongs to.
80        enumeration: String,
81        /// The enumerator.
82        enumerator: String,
83        /// Why its wire value could not be computed.
84        detail: &'static str,
85    },
86    /// A member's PL_CDR1 header form is not one the sample's flag can
87    /// express.
88    ///
89    /// Under a [per-sample](`PlCdr1HeaderPolicy::decided_per_sample`)
90    /// policy the header is chosen at run time, and the choice is read
91    /// by planning the type under the two policies the flag stands
92    /// between — [`PlCdr1HeaderPolicy::Specification`] and
93    /// [`PlCdr1HeaderPolicy::ExtendedForConstructed`]. A member whose
94    /// two plans differ in `extended` alone is one the flag decides; a
95    /// member whose two plans differ in anything else is a case where
96    /// `zd_extended` is not the whole of the answer, and picking one of
97    /// the two plans would write bytes nothing measured.
98    AmbiguousHeader {
99        /// The member the two plans disagree about.
100        member: String,
101        /// What each plan said, so the difference can be read.
102        detail: String,
103    },
104}
105
106impl EncodeError {
107    /// Whether the specification refused the type, rather than this tree
108    /// not having written a writer for it.
109    ///
110    /// The one place a generator asks the question, so that sixteen of
111    /// them do not each answer it from the error's text.
112    #[must_use]
113    pub const fn is_spec_refusal(&self) -> bool {
114        matches!(self, Self::SpecRefuses { .. })
115    }
116}
117
118/// Refuses a layout key whose axes the **non-Rust** generated codecs do
119/// not carry.
120///
121/// [`zerodds_ast_layout::Dialect::Hdds`] selects three writer axes the
122/// planner and `zerodds-wire-codec` carry: collection elements packed
123/// without alignment, a length in front of each element of a sequence of a
124/// non-`@final` type, and a `wstring` in UTF-8. The Rust codec carries all
125/// three — see [`axes_the_rust_codec_carries`], which [`rust_codec`] and
126/// [`rust_codec_pair`] call instead. A generated codec of another column
127/// asks [`axes_the_codecs_carry_for`], which passes for a target whose
128/// runtime carries them and refuses the rest: generating their default
129/// form under them would write the specification's octets where the key
130/// asked for another implementation's. The refusal is a gap of this tree
131/// (`NoLayout`, exit 6), named axis by axis; this function is it for a
132/// caller that names no target.
133///
134/// # Errors
135/// [`EncodeError::NoLayout`] naming every such axis the key selects.
136pub fn axes_the_codecs_carry(key: LayoutKey) -> Result<(), EncodeError> {
137    axes_the_codecs_carry_named(key, false)
138}
139
140/// [`axes_the_codecs_carry`] for a generated codec, which carries the
141/// axes its runtime does.
142///
143/// A target whose runtime has the packed-element origin, the element
144/// length in DHEADER form and a UTF-8 `wstring` — the three axes
145/// [`zerodds_ast_layout::Dialect::Hdds`] selects — answers
146/// [`crate::emit::CodecTarget::carries_hdds_axes`] `true` and passes;
147/// one that has not been given them is refused with the axes named.
148///
149/// # Errors
150/// [`EncodeError::NoLayout`] naming every axis the key selects that the
151/// target does not carry.
152pub fn axes_the_codecs_carry_for<T: crate::emit::CodecTarget>(
153    key: LayoutKey,
154) -> Result<(), EncodeError> {
155    axes_the_codecs_carry_named(key, T::carries_hdds_axes())
156}
157
158fn axes_the_codecs_carry_named(key: LayoutKey, carries: bool) -> Result<(), EncodeError> {
159    if carries {
160        return Ok(());
161    }
162    let mut missing: Vec<&str> = Vec::new();
163    if !key.element_padding.is_default() {
164        missing.push(key.element_padding.as_str());
165    }
166    if !key.sequence_element_delimiter.is_default() {
167        missing.push(key.sequence_element_delimiter.as_str());
168    }
169    if !key.wstring_code_units.is_default() {
170        missing.push(key.wstring_code_units.as_str());
171    }
172    if missing.is_empty() {
173        Ok(())
174    } else {
175        Err(EncodeError::NoLayout {
176            detail: format!(
177                "the generated codecs do not carry {} yet",
178                missing.join(", ")
179            ),
180        })
181    }
182}
183
184/// The same check for the Rust target, which carries every HDDS axis.
185///
186/// [`ElementPadding::Packed`],
187/// [`zerodds_ast_layout::SequenceElementDelimiter::BeforeEachNonFinalElement`]
188/// and [`WstringCodeUnits::Utf8`] are emitted by [`rust_codec`] and
189/// [`rust_codec_pair`] exactly as `zerodds-wire-codec` writes them, so
190/// this target has no axis left to refuse. The function exists so that the
191/// two targets ask one question through one name rather than the Rust
192/// path skipping the check by having no call at all.
193///
194/// # Errors
195/// Nothing today; the signature matches [`axes_the_codecs_carry_for`],
196/// the check of the other columns.
197pub fn axes_the_rust_codec_carries(_key: LayoutKey) -> Result<(), EncodeError> {
198    Ok(())
199}
200
201impl core::fmt::Display for EncodeError {
202    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
203        match self {
204            Self::NoLayout { detail } => write!(f, "the layout could not be planned: {detail}"),
205            Self::SpecRefuses { detail } => write!(f, "{detail}"),
206            Self::NoWriter { member, kind } => {
207                write!(f, "`{member}` is a {kind}, which has no writer yet")
208            }
209            Self::NoEnumeratorValue {
210                enumeration,
211                enumerator,
212                detail,
213            } => write!(
214                f,
215                "`{enumeration}::{enumerator}` has no wire value: {detail}"
216            ),
217            Self::AmbiguousHeader { member, detail } => write!(
218                f,
219                "`{member}` has no single PL_CDR1 header form to choose between: {detail}"
220            ),
221        }
222    }
223}
224
225impl core::error::Error for EncodeError {}
226
227/// The name the **second** codec pair on a type carries.
228///
229/// A Rust type may hold only one inherent function per name, so a type
230/// that carries both of its XCDR streams needs two names. The XCDR
231/// version is what earns one: XTypes 1.3 §7.4 frames the two versions
232/// differently — no DHEADER under version 1, PID headers instead of
233/// EMHEADERs for `@mutable`, `MAXALIGN` eight instead of four — so the
234/// two bodies are different code.
235///
236/// The byte order does not earn one. Nothing in this emitter reads
237/// [`LayoutKey::endianness`]: the order is the `Writer`'s and the
238/// `Reader`'s, so one body serves both and a suffix per order would
239/// duplicate identical code.
240///
241/// [`rust_codec`] keeps the bare names whatever encoding it is given, so
242/// every consumer that generates one codec per type is unaffected by
243/// this existing; only [`rust_codec_pair`], which exists to put a second
244/// one beside it, spells them out.
245pub const SECONDARY_SUFFIX: &str = "_xcdr1";
246
247/// The parameter that carries a sample's RTI PL_CDR1 step decision down
248/// to a nested type's encoder.
249///
250/// RTI frames a sample under XCDR1 with the short parameter header until
251/// its type's maximum serialized size passes a step, and from there
252/// gives `PID_EXTENDED` to every member whose type is not a primitive or
253/// an enumeration (XTypes 1.3 §7.4.1.2, in RTI's measured reading of it).
254/// The step is a property of the **sample's** type and not of the type
255/// that declares a member, so a generated type cannot bake the answer in:
256/// the same nested `@mutable` type is framed short inside a small sample
257/// and extended inside a large one. The flag is threaded at run time; the
258/// sample's root supplies it through [`sample_past_step`], which asks the
259/// same crossing the reflective encoder asks.
260const EXTENDED_PARAM: &str = "zd_extended";
261
262/// The suffix the encoder **body** takes under a [per-sample](`per_sample`)
263/// plan.
264///
265/// A Rust type may hold only one inherent function per name, so a type
266/// that needs both a body taking the flag and a public entry does so the
267/// same way [`SECONDARY_SUFFIX`] splits the two encodings.
268const EXTENDED_SUFFIX: &str = "_zd_extended";
269
270/// Whether the plan's PL_CDR1 header form is decided per sample, so every
271/// generated encoder takes [`EXTENDED_PARAM`] and threads it to the types
272/// it calls.
273fn per_sample(key: LayoutKey) -> bool {
274    key.pl_cdr1_headers.decided_per_sample() && key.encoding.is_v1()
275}
276
277/// Whether a sample of `root` passes RTI's short-header step, the
278/// generation-time half of the per-sample decision.
279///
280/// The same count [`LayoutKey::for_sample`] makes, asked through it
281/// rather than through `exceeds_short_length` so that the generated
282/// entry and the reflective path read one answer.
283fn sample_past_step(ast: &Ast, root: Id<TypeNode>, key: LayoutKey) -> Result<bool, EncodeError> {
284    let resolved = key.for_sample(ast, root).map_err(plan_refusal)?;
285    Ok(resolved.pl_cdr1_headers == PlCdr1HeaderPolicy::ExtendedForConstructed)
286}
287
288/// The name of the encoder **body** for a codec suffix: the method that
289/// takes [`EXTENDED_PARAM`], or the public entry itself when the plan's
290/// header form is fixed.
291///
292/// Under [`per_sample`] one type needs two functions. The body carries
293/// the sample's decision as a parameter, so one type's codec can frame
294/// its constructed members short in a small sample and extended in a
295/// large one; the public entry bakes the decision in for the sample whose
296/// root this type is. Without the split, the nested `Inner` of
297/// `tools/idlc/tests/dialect_byte_equality.rs`'s `rti_step_cases` could
298/// not serve both its samples.
299fn body_name(codec: &str, key: LayoutKey) -> String {
300    if per_sample(key) {
301        framed_body(codec)
302    } else {
303        format!("encode{codec}")
304    }
305}
306
307/// The name of an encoder body that takes the sample's decision, for a
308/// nested call that carries `extended`.
309fn nested_method(codec: &str, extended: Extended) -> String {
310    match extended {
311        Extended::Fixed => format!("encode{codec}"),
312        Extended::Inherited | Extended::Constant(_) => framed_body(codec),
313    }
314}
315
316/// [`nested_method`]'s name on the other side of the split.
317fn framed_body(codec: &str) -> String {
318    format!("encode{codec}{EXTENDED_SUFFIX}")
319}
320
321/// The sample's RTI PL_CDR1 step decision, as a nested type's call
322/// carries it.
323///
324/// A data sample's body passes its own flag down, because the sample's
325/// root resolved it; a key holder passes [`Self::Constant`] `false`. A
326/// key holder keeps the short form whatever the sample: the choice of
327/// `PID_EXTENDED` for a constructed member is a data-sample decision —
328/// [`LayoutKey::for_sample`] is asked for one, and
329/// `zerodds_ast_layout::xcdr::extended_as_constructed` refuses the key
330/// purpose for the same reason — and the reflective planner keeps a key
331/// holder short. The data path resolves the choice once at the root and
332/// hands it down; the holder has no root decision to hand down and states
333/// the constant.
334///
335/// `pub(crate)`: it is a field of [`MemberWrite::Nested`], which is.
336#[derive(Debug, Clone, Copy, PartialEq, Eq)]
337pub(crate) enum Extended {
338    /// The plan fixes the header form: the call names the public entry,
339    /// which takes no flag. Every plan outside [`per_sample`].
340    Fixed,
341    /// Hand the enclosing body's flag to the nested call.
342    Inherited,
343    /// Pass this constant instead.
344    Constant(bool),
345}
346
347impl Extended {
348    /// The answer for a codec written under `key`.
349    ///
350    /// The key holder's constant is what `crate::emit::encode`'s
351    /// `nested_call_with` passes for the same reason.
352    #[must_use]
353    fn of(key: LayoutKey) -> Self {
354        if !per_sample(key) {
355            Self::Fixed
356        } else if key.purpose == Purpose::Data {
357            Self::Inherited
358        } else {
359            Self::Constant(false)
360        }
361    }
362
363    /// The flag argument the nested call passes, if it passes one.
364    #[must_use]
365    fn argument(self) -> Option<rust::Expr> {
366        match self {
367            Self::Fixed => None,
368            Self::Inherited => Some(rust::Expr::Path(Path::single(EXTENDED_PARAM))),
369            Self::Constant(value) => Some(rust::Expr::Literal(Literal::Bool(value))),
370        }
371    }
372}
373
374/// Which of one type's members take the sample's step decision at run
375/// time.
376///
377/// A member is one exactly when the two policies a per-sample plan is
378/// between disagree about its `extended` bit:
379/// [`PlCdr1HeaderPolicy::Specification`] frames a sample that stays
380/// within the short form's length,
381/// [`PlCdr1HeaderPolicy::ExtendedForConstructed`] one that can exceed it.
382/// Keyed by member identity and not by position, because a union finds
383/// its case's plan in the layout by identity.
384struct StepHeaders {
385    /// Whether the plan decides the form per sample at all, and the
386    /// entries below mean anything.
387    per_sample: bool,
388    /// One entry per member of the layout: does the flag choose its
389    /// header form?
390    members: Vec<(NodeId, bool)>,
391    /// The same for a `@mutable` union's discriminator, which rules (27)
392    /// and (28) frame as an `MMEMBER` at member id 0 and which is
393    /// therefore a header the flag can choose too.
394    discriminator: bool,
395}
396
397impl StepHeaders {
398    /// No member takes the flag, which is what every plan outside
399    /// [`per_sample`] plans.
400    fn none() -> Self {
401        Self {
402            per_sample: false,
403            members: Vec::new(),
404            discriminator: false,
405        }
406    }
407
408    /// Whether the flag chooses `plan`'s member's header form.
409    ///
410    /// # Errors
411    /// [`EncodeError::NoLayout`] for a member this set does not name —
412    /// a plan that lost one would leave a member whose header nothing
413    /// wrote.
414    fn member(&self, plan: &MemberPlan, name: &str) -> Result<bool, EncodeError> {
415        if !self.per_sample {
416            return Ok(false);
417        }
418        self.members
419            .iter()
420            .find(|(id, _)| *id == plan.member)
421            .map(|(_, chosen)| *chosen)
422            .ok_or_else(|| EncodeError::NoLayout {
423                detail: format!(
424                    "the PL_CDR1 plans for a sample past RTI's step name no member `{name}`, \
425                     and its header form would be left unwritten"
426                ),
427            })
428    }
429}
430
431/// The plan `type_` takes under one PL_CDR1 header policy.
432///
433/// # Errors
434/// [`EncodeError`] when the type cannot be planned under that policy.
435fn plan_under(
436    ast: &Ast,
437    type_: Id<TypeNode>,
438    key: LayoutKey,
439    policy: PlCdr1HeaderPolicy,
440) -> Result<TypeLayout, EncodeError> {
441    zerodds_ast_layout::xcdr::plan(
442        ast,
443        type_,
444        key.with_pl_cdr1_headers(policy),
445        &mut DepCollector::default(),
446    )
447    .map_err(plan_refusal)
448}
449
450/// Whether the flag chooses this member's header form, from the two plans
451/// a per-sample decision stands between.
452///
453/// `framed` is the plan the codec is written from — the unresolved key's,
454/// which frames short because the choice belongs to the sample and not to
455/// the type — `below` the one a sample under the step gets and `past` the
456/// one a sample over it gets. Where `below` and `past` agree, the form is
457/// fixed and nothing is threaded; where they differ in `extended` alone,
458/// the flag picks; anything else is a case `zd_extended` cannot express
459/// and is refused rather than answered with one of the two plans.
460///
461/// `framed` is checked against `below` too. The unresolved policy is
462/// neither of the two, so a member it framed differently from a small
463/// sample would mean the two are not the ends of one decision.
464///
465/// # Errors
466/// [`EncodeError::AmbiguousHeader`] naming the member when two of the
467/// three plans disagree about more than `extended`.
468fn header_is_chosen(
469    member: &str,
470    framed: MemberHeader,
471    below: MemberHeader,
472    past: MemberHeader,
473) -> Result<bool, EncodeError> {
474    let refuse = || ambiguous(member, framed, below, past);
475    if below == past {
476        if framed == below {
477            return Ok(false);
478        }
479        return Err(refuse());
480    }
481    // `MemberHeader`'s own equality has already settled that the two
482    // differ in nothing but `extended`, so what is left is which way and
483    // which header: both forms of a PL_CDR1 parameter header take the
484    // same branch, `write_pid` below the step and `write_pid_extended`
485    // past it, and nothing else does.
486    match (below, past) {
487        (
488            MemberHeader::PidCdr1 {
489                extended: false, ..
490            },
491            MemberHeader::PidCdr1 { extended: true, .. },
492        )
493        | (
494            MemberHeader::OptionalCdr1 {
495                extended: false, ..
496            },
497            MemberHeader::OptionalCdr1 { extended: true, .. },
498        ) if framed == below => Ok(true),
499        _ => Err(refuse()),
500    }
501}
502
503/// The refusal [`header_is_chosen`] makes, naming all three plans.
504fn ambiguous(
505    member: &str,
506    framed: MemberHeader,
507    below: MemberHeader,
508    past: MemberHeader,
509) -> EncodeError {
510    EncodeError::AmbiguousHeader {
511        member: member.to_string(),
512        detail: format!(
513            "the codec's own plan frames it {framed:?}, a sample within RTI's short-header step \
514             {below:?} and a sample past it {past:?}, and one flag cannot choose between them"
515        ),
516    }
517}
518
519/// Every member of `type_` whose PL_CDR1 header form the sample decides,
520/// under `framed`.
521///
522/// [`StepHeaders::none`] for every plan outside [`per_sample`], so a
523/// dialect whose header form is fixed never pays for two more plans and
524/// generates exactly the code it generated before.
525///
526/// # Errors
527/// [`EncodeError`] when one of the two plans cannot be made, when they do
528/// not name the same members as `framed`, or when they differ in anything
529/// but `extended`.
530fn step_headers(
531    ast: &Ast,
532    type_: Id<TypeNode>,
533    key: LayoutKey,
534    framed: &TypeLayout,
535) -> Result<StepHeaders, EncodeError> {
536    if !per_sample(key) {
537        return Ok(StepHeaders::none());
538    }
539    let name = ast
540        .type_(type_)
541        .map_or_else(|_| "<unresolved>".to_string(), |node| node.name.clone());
542    let below = plan_under(ast, type_, key, PlCdr1HeaderPolicy::Specification)?;
543    let past = plan_under(ast, type_, key, PlCdr1HeaderPolicy::ExtendedForConstructed)?;
544    if below.members.len() != framed.members.len() || past.members.len() != framed.members.len() {
545        return Err(EncodeError::NoLayout {
546            detail: format!(
547                "`{name}`: the PL_CDR1 plans for the two sides of RTI's step name {} and {} \
548                 members where the codec's own plan names {}",
549                below.members.len(),
550                past.members.len(),
551                framed.members.len()
552            ),
553        });
554    }
555    let mut members = Vec::with_capacity(framed.members.len());
556    for ((own, below), past) in framed.members.iter().zip(&below.members).zip(&past.members) {
557        let member = member_name(ast, type_, own.member);
558        members.push((
559            own.member,
560            header_is_chosen(&member, own.header, below.header, past.header)?,
561        ));
562    }
563    let discriminator = match (
564        framed.discriminator,
565        below.discriminator,
566        past.discriminator,
567    ) {
568        (None, None, None) => false,
569        (Some(framed), Some(below), Some(past)) => {
570            header_is_chosen(&format!("{name}::discriminator"), framed, below, past)?
571        }
572        _ => {
573            return Err(EncodeError::NoLayout {
574                detail: format!(
575                    "`{name}`: the PL_CDR1 plans for the two sides of RTI's step disagree about \
576                     whether its discriminator carries a header at all"
577                ),
578            });
579        }
580    };
581    Ok(StepHeaders {
582        per_sample: true,
583        members,
584        discriminator,
585    })
586}
587
588/// The name a member node has inside `type_`, for an error that names it.
589fn member_name(ast: &Ast, type_: Id<TypeNode>, member: NodeId) -> String {
590    ast.all_members_of(type_)
591        .into_iter()
592        .find(|id| ast.member(*id).is_ok_and(|node| node.node_id == member))
593        .and_then(|id| ast.member(id).ok())
594        .map_or_else(|| alloc_format(&member), |node| node.name.clone())
595}
596
597/// Build the `encode` and `decode` functions for one type, with the
598/// `Default` impl and — for a keyed type — the key-holder pair.
599///
600/// This column writes a member's collection where the member is, in the
601/// parent's own body and through a closure, and never hoists it into a
602/// routine named after its element type. So the bound-in-the-name
603/// defect the other columns carry — `sequence<long>` and
604/// `sequence<long, 4>` sharing one hoisted routine, the first declared
605/// deciding whether the check exists — cannot reach here: every bound
606/// travels as an argument at the member's own call site
607/// (`write_string_bounded`, `write_sequence_bounded`, and the `fit_…`
608/// pair on the read side). Measured on
609/// `@final struct S { sequence<long> b; sequence<long, 4> a; };` in
610/// both declaration orders: both members keep exactly the bound their
611/// declaration gives them. `crates/lang-rules/src/emit/zig.rs`'s
612/// `bound_tag` is the fix in the columns that do hoist.
613///
614/// # Errors
615/// [`EncodeError`] when the layout cannot be planned or a member has no
616/// writer — never a body that leaves a member out.
617pub fn rust_codec(
618    ast: &Ast,
619    type_: Id<TypeNode>,
620    key: LayoutKey,
621) -> Result<Vec<rust::Item>, EncodeError> {
622    codec_items(ast, type_, key, Companions::Emit)
623}
624
625/// Build only the `encode` and `decode` pair for one type.
626///
627/// For a **second** plan over a type that already has one: the `Default`
628/// impl and the key holder do not belong to an encoding and would be
629/// emitted twice under the same name, which Rust refuses. The key holder
630/// in particular is not a per-encoding thing at all — XTypes 1.3 §7.6.8
631/// fixes it as PLAIN_CDR2 big-endian whatever the sample's encoding is —
632/// so a second copy of it would be the same bytes under a name that
633/// suggests otherwise.
634///
635/// # Errors
636/// As [`rust_codec`].
637pub fn rust_codec_pair(
638    ast: &Ast,
639    type_: Id<TypeNode>,
640    key: LayoutKey,
641) -> Result<Vec<rust::Item>, EncodeError> {
642    codec_items(ast, type_, key, Companions::Omit)
643}
644
645/// Whether [`rust_codec`] gives a type of this kind a codec of its own:
646/// an aggregate (structure, union, valuetype), an enumeration, a bitmask
647/// or a bitset. A bitmask's answer is empty, since its Rust form is an
648/// alias of a primitive and its member site writes the holder. Every
649/// other kind — a primitive, a string, a collection, an alias — is
650/// written where it is used and has no `impl` to hang a pair on.
651#[must_use]
652pub const fn has_codec(kind: &TypeKind) -> bool {
653    matches!(
654        kind,
655        TypeKind::Structure { .. }
656            | TypeKind::Union { .. }
657            | TypeKind::Valuetype { .. }
658            | TypeKind::Enumeration { .. }
659            | TypeKind::Bitmask { .. }
660            | TypeKind::Bitset { .. }
661    )
662}
663
664/// Whether a codec run also emits what does not belong to an encoding.
665#[derive(Debug, Clone, Copy, PartialEq, Eq)]
666enum Companions {
667    /// The `Default` impl and the key holder come with the codec.
668    Emit,
669    /// Only the codec pair.
670    Omit,
671}
672
673fn codec_items(
674    ast: &Ast,
675    type_: Id<TypeNode>,
676    key: LayoutKey,
677    companions: Companions,
678) -> Result<Vec<rust::Item>, EncodeError> {
679    axes_the_rust_codec_carries(key)?;
680    // Which set of names this run writes, and — through
681    // `MemberWrite::Nested` — which set it calls on a member. The two
682    // have to be the same set: a nested call that named the XCDR2
683    // encoder from inside the XCDR1 body would frame the member under
684    // version 2 in a version 1 stream, and the sample would decode into
685    // wrong values with nothing reporting it.
686    let codec = match companions {
687        Companions::Emit => "",
688        Companions::Omit => SECONDARY_SUFFIX,
689    };
690    // Asked for a kind with no codec of its own, the generic path below
691    // emitted `impl long { … }` or `impl Count { … }` for a
692    // `typedef long Count;` — not Rust (E0390), and an `encode` that wrote
693    // nothing. Every caller filtered the kinds it asked for; the refusal
694    // is here so none has to.
695    let node = ast.type_(type_).map_err(|error| EncodeError::NoLayout {
696        detail: alloc_format(&error),
697    })?;
698    if !has_codec(&node.kind) {
699        return Err(EncodeError::NoWriter {
700            member: node.name.clone(),
701            kind: "a type with no codec of its own; it is written where it is used",
702        });
703    }
704    let mut deps = DepCollector::default();
705    let layout =
706        zerodds_ast_layout::xcdr::plan(ast, type_, key, &mut deps).map_err(plan_refusal)?;
707    // Which of this type's members the sample's PL_CDR1 step decides. One
708    // set per codec, shared by every encoder below, because they are all
709    // the same type under the same plan.
710    let steps = step_headers(ast, type_, key, &layout)?;
711
712    // An enumeration has no layout **members**, and the generic path
713    // below walks exactly those. It therefore produced
714    //
715    //     pub fn encode(&self, w: &mut Writer) -> Result<(), WireError> { Ok(()) }
716    //     pub fn decode(r: &mut Reader) -> Result<Self, WireError> { Ok(Self { }) }
717    //
718    // — an encoder that puts zero octets on the wire for a value that
719    // has a wire form, and a decoder that is not Rust at all: `Self { }`
720    // is a struct literal and the type is an enum. It was invisible
721    // because an enumeration is normally reached as a *member*, where
722    // `MemberWrite::Enumeration` writes its holder, and nothing in the
723    // tree asked an enumeration for its own codec until a CORBA
724    // operation took one as a parameter — an argument has no enclosing
725    // aggregate to be a member of.
726    if let TypeKind::Enumeration { enumerators } = &node.kind {
727        let mut items = enumeration_encode_fns(ast, type_, enumerators, key, codec)?;
728        items.push(enumeration_decode_fn(ast, type_, enumerators, key, codec)?);
729        return Ok(vec![rust::Item::Impl(rust::Impl {
730            doc: Doc::none(),
731            attrs: Vec::new(),
732            generics: Vec::new(),
733            trait_: None,
734            self_type: rust::Type::named(Path::single(&node.name)),
735            items,
736        })]);
737    }
738
739    // A bitmask has **no** codec of its own, and cannot have one.
740    // `crate::adapter::cst` materialises it as `pub type Flags = u16;`,
741    // and `impl Flags` on a type alias to a primitive is E0390 —
742    // "cannot define inherent `impl` for primitive types". The generic
743    // path below emitted exactly that, with an `encode` that put zero
744    // octets on the wire and a `decode` returning `Ok(Self { })`: a
745    // file that did not compile, so a model with a `bitmask` in it had
746    // no Rust output at all. `MemberWrite::Bitmask` writes the holder
747    // at the member site instead, which is why nothing has to be
748    // emitted here and why an empty answer is not a member left out.
749    //
750    // The name stays in `generate::rust_codec_pairs`' list because the
751    // other twenty columns do give a bitmask a codec function; this is
752    // the one column whose language mapping has nowhere to put it.
753    if matches!(node.kind, TypeKind::Bitmask { .. }) {
754        return Ok(Vec::new());
755    }
756
757    // A bitset has a codec and had an empty one. It materialises as a
758    // struct with one field per named bitfield, so there is an `impl`
759    // to hang the pair on — but a bitset's layout has no **members**,
760    // and the generic path walks exactly those. What came out was an
761    // `encode` that wrote nothing at all for a type with a wire form
762    // and a `decode` returning `Ok(Self { })` for a struct with two
763    // fields. The same hole as the enumeration's above, and the same
764    // fix: the construct's own pair.
765    if matches!(node.kind, TypeKind::Bitset { .. }) {
766        let mut items = bitset_encode_fns(ast, type_, &layout, key, codec)?;
767        items.push(bitset_decode_fn(ast, type_, &layout, codec)?);
768        let mut out = vec![rust::Item::Impl(rust::Impl {
769            doc: Doc::none(),
770            attrs: Vec::new(),
771            generics: Vec::new(),
772            trait_: None,
773            self_type: rust::Type::named(Path::single(&node.name)),
774            items,
775        })];
776        if companions == Companions::Emit
777            && let Some(default) = default_impl(ast, type_)?
778        {
779            out.push(default);
780        }
781        return Ok(out);
782    }
783
784    // A union has the same shape of hole as an enumeration, one level
785    // up: `crate::adapter::cst` materialises it as a Rust enum with one
786    // payload variant per case, and the generic path below builds a
787    // struct literal over the layout's members. It produced an `encode`
788    // that wrote nothing and a `decode` that returned `Ok(Self { })`.
789    if let TypeKind::Union {
790        discriminator,
791        cases,
792        ..
793    } = &node.kind
794    {
795        let items = union_codec(
796            ast,
797            type_,
798            &layout,
799            &steps,
800            discriminator,
801            cases,
802            key,
803            codec,
804        )?;
805        let mut out = vec![rust::Item::Impl(rust::Impl {
806            doc: Doc::none(),
807            attrs: Vec::new(),
808            generics: Vec::new(),
809            trait_: None,
810            self_type: rust::Type::named(Path::single(&node.name)),
811            items,
812        })];
813        // A union returns from here and not through the tail, so its
814        // `impl Default` has to be added on this path too. It was not,
815        // and the omission was silent: the type simply had no `Default`,
816        // which shows up wherever a caller writes `Default::default()`
817        // for a member of one.
818        if companions == Companions::Emit
819            && let Some(default) = default_impl(ast, type_)?
820        {
821            out.push(default);
822        }
823        return Ok(out);
824    }
825
826    let mut items = encode_fns(ast, type_, &layout, &steps, key, codec)?;
827    items.push(decode_fn(ast, type_, &layout, key, codec)?);
828    if companions == Companions::Emit {
829        items.append(&mut key_holder_fns(ast, type_, key)?);
830    }
831
832    let mut out = vec![rust::Item::Impl(rust::Impl {
833        doc: Doc::none(),
834        attrs: Vec::new(),
835        generics: Vec::new(),
836        trait_: None,
837        self_type: rust::Type::named(Path::single(&node.name)),
838        items,
839    })];
840    // The specification's default values, as an `impl Default`. A member
841    // that has none — a type this column cannot give a default for —
842    // leaves the impl out rather than substituting one.
843    if companions == Companions::Emit
844        && let Some(default) = default_impl(ast, type_)?
845    {
846        out.push(default);
847    }
848    Ok(out)
849}
850
851/// `encode` and `decode` for a union. §7.4.3.5 rule (10): the
852/// discriminator, then the member the discriminator selects — and around
853/// them the framing the plan gives the union's extensibility: rule (11)'s
854/// DHEADER for an `@appendable` one, rules (27) and (28)'s member headers
855/// for a `@mutable` one, the discriminator under member id 0. This column
856/// wrote rule (10) for every union, so an `@appendable` union went out
857/// without the DHEADER the C, Java and C# columns and every vendor write
858/// (measured: `idl-conformance/_interop` `mapenum`, a `sequence<Sel>` of
859/// an `@appendable` union — 44 octets here, 48 in C).
860///
861/// The Rust form is `crate::adapter::cst`'s — an enum with one payload
862/// variant per case — so `encode` matches the variant and writes the
863/// label that selects it, and `decode` reads the discriminator and
864/// matches its value back.
865///
866/// A discriminator value that selects no case and no `default:` leaves
867/// the member absent (§7.4.3.5); it is read into the variant
868/// `crate::rust_union::NO_MEMBER`, which holds the discriminator alone.
869/// A variant that carries its discriminator is checked before it is
870/// written: a pairing the type does not allow is refused.
871#[allow(clippy::too_many_arguments)]
872fn union_codec(
873    ast: &Ast,
874    type_: Id<TypeNode>,
875    layout: &TypeLayout,
876    steps: &StepHeaders,
877    discriminator: &zerodds_ast_core::Ref<TypeNode>,
878    cases: &[Id<zerodds_ast::CaseNode>],
879    key: LayoutKey,
880    codec: &'static str,
881) -> Result<Vec<rust::Item>, EncodeError> {
882    let node = ast.type_(type_).map_err(|error| EncodeError::NoLayout {
883        detail: alloc_format(&error),
884    })?;
885    let name = node.name.clone();
886    let discriminator = discriminator.resolved().ok_or(EncodeError::NoWriter {
887        member: name.clone(),
888        kind: "a union with an unresolved discriminator",
889    })?;
890    // The discriminator is written as its *value* and not through the
891    // enumeration's own codec: what a union writes here is the label the
892    // selected branch carries, which the emitter already has as an
893    // integer. A member of an enumeration type is the other case and
894    // goes through `<path>::encode` — see the arm in `write_of`.
895    let discriminator_write = match ast.type_(discriminator).map(|d| &d.kind) {
896        Ok(TypeKind::Enumeration { .. }) => MemberWrite::Enumeration {
897            bits: zerodds_ast_layout::enumeration_bit_bound(ast, discriminator, key.enum_holder),
898        },
899        _ => write_of(
900            ast,
901            node.module,
902            discriminator,
903            key,
904            codec,
905            Position {
906                behaviour: zerodds_ast::TryConstruct::Discard,
907                written: None,
908            },
909        ),
910    };
911
912    // The union as this column holds it: which variant carries its
913    // discriminator, whether one holds a discriminator alone, and how a
914    // label is spelled. `crate::rust_union`, shared with the declaration
915    // and the default.
916    let shape = crate::rust_union::shape(ast, type_).map_err(|kind| EncodeError::NoWriter {
917        member: name.clone(),
918        kind,
919    })?;
920    let refuse_label = |_| EncodeError::NoWriter {
921        member: name.clone(),
922        kind: "a union label with no source form in this column",
923    };
924    // A label as the discriminator's *write* takes it: the integer an
925    // enumeration's holder carries, the literal otherwise.
926    let written_label = |value: i128| -> Result<rust::Expr, EncodeError> {
927        Ok(match shape.kind {
928            crate::rust_union::DiscriminatorKind::Enumeration(_) => {
929                rust::Expr::Literal(Literal::int(value))
930            }
931            kind => crate::rust_union::label_literal(ast, node.module, kind, value)
932                .map_err(refuse_label)?
933                .expr(),
934        })
935    };
936    // A label as a pattern over the discriminator a variant carries, and
937    // over the one `decode` reads: an enumeration's is converted to the
938    // enumeration first.
939    // An enumeration discriminator's own name, for a refusal that names it.
940    let enumeration_name = |enumeration: Id<TypeNode>| {
941        ast.type_(enumeration)
942            .map_or_else(|_| name.clone(), |node| node.name.clone())
943    };
944    let label_pattern = |value: i128| -> Result<rust::Pattern, EncodeError> {
945        Ok(
946            crate::rust_union::label_literal(ast, node.module, shape.kind, value)
947                .map_err(refuse_label)?
948                .pattern(),
949        )
950    };
951    let refused =
952        |what: String, sink: &str| rust::Expr::Return(Some(Box::new(refused_err(sink, &what))));
953    // A carried discriminator as a match scrutinee: the binding, which a
954    // `match` on `&self` makes a reference, dereferenced for the copyable
955    // primitives; an enumeration is matched through the reference.
956    let carried_scrutinee = || match shape.kind {
957        crate::rust_union::DiscriminatorKind::Enumeration(_) => {
958            rust::Expr::Path(Path::single("discriminator"))
959        }
960        _ => rust::Expr::Deref(Box::new(rust::Expr::Path(Path::single("discriminator")))),
961    };
962    // A carried discriminator written: an enumeration through its holder
963    // integer, every other kind as itself.
964    let write_carried = || -> Result<rust::Expr, EncodeError> {
965        match shape.kind {
966            crate::rust_union::DiscriminatorKind::Enumeration(enumeration) => {
967                let mut arms = Vec::new();
968                for enumerator in ast.enumerators_of(enumeration) {
969                    // The enumerator by name, and the integer the wire
970                    // carries for it — which the setting decides.
971                    let (_, value) = enumerator_variant_and_value(
972                        ast,
973                        &enumeration_name(enumeration),
974                        *enumerator,
975                        key,
976                    )?;
977                    arms.push(rust::MatchArm {
978                        pattern: rust::Pattern::Path(
979                            crate::rust_union::enumerator_path_of(
980                                ast,
981                                node.module,
982                                enumeration,
983                                *enumerator,
984                            )
985                            .map_err(refuse_label)?,
986                        ),
987                        guard: None,
988                        body: rust::Expr::Literal(Literal::int(value)),
989                    });
990                }
991                write_expr_for(
992                    &discriminator_write,
993                    rust::Expr::Match {
994                        scrutinee: Box::new(rust::Expr::Path(Path::single("discriminator"))),
995                        arms,
996                    },
997                    &name,
998                    false,
999                )
1000            }
1001            _ => write_expr_for(
1002                &discriminator_write,
1003                rust::Expr::Path(Path::single("discriminator")),
1004                &name,
1005                true,
1006            ),
1007        }
1008    };
1009    // The check ahead of a carried discriminator's write: one of `own`
1010    // where the variant is a case with several labels; none of every
1011    // case's labels where it is the `default:` case or the variant that
1012    // holds no member. A pairing the type does not allow is refused, not
1013    // written: a peer would read another branch than the one the value
1014    // names.
1015    let check_carried =
1016        |own: Option<&[i128]>, what: String| -> Result<rust::Statement, EncodeError> {
1017            let mut arms = Vec::new();
1018            match own {
1019                Some(labels) => {
1020                    for value in labels {
1021                        arms.push(rust::MatchArm {
1022                            pattern: label_pattern(*value)?,
1023                            guard: None,
1024                            body: rust::Expr::Block(rust::Block {
1025                                statements: Vec::new(),
1026                            }),
1027                        });
1028                    }
1029                    arms.push(rust::MatchArm {
1030                        pattern: rust::Pattern::Wildcard,
1031                        guard: None,
1032                        body: refused(what, "w"),
1033                    });
1034                }
1035                None => {
1036                    for value in shape.all_labels() {
1037                        arms.push(rust::MatchArm {
1038                            pattern: label_pattern(value)?,
1039                            guard: None,
1040                            body: refused(what.clone(), "w"),
1041                        });
1042                    }
1043                    arms.push(rust::MatchArm {
1044                        pattern: rust::Pattern::Wildcard,
1045                        guard: None,
1046                        body: rust::Expr::Block(rust::Block {
1047                            statements: Vec::new(),
1048                        }),
1049                    });
1050                }
1051            }
1052            Ok(rust::Statement::Semi(rust::Expr::Match {
1053                scrutinee: Box::new(carried_scrutinee()),
1054                arms,
1055            }))
1056        };
1057
1058    // The discriminator's header, as a plan the header helpers take: only
1059    // a `@mutable` union has one, under member id 0.
1060    let discriminator_plan = layout.discriminator.map(|header| MemberPlan {
1061        member: layout.type_,
1062        member_id: 0,
1063        header,
1064        offset: zerodds_ast_layout::plan::Offset::Dynamic,
1065        align: 1,
1066    });
1067
1068    let mut encode_arms = Vec::new();
1069    let mut decode_arms = Vec::new();
1070    let mut default_arm = None;
1071
1072    for case in cases {
1073        let case_node = ast.case(*case).map_err(|error| EncodeError::NoLayout {
1074            detail: alloc_format(&error),
1075        })?;
1076        let member = ast
1077            .member(case_node.member)
1078            .map_err(|error| EncodeError::NoLayout {
1079                detail: alloc_format(&error),
1080            })?;
1081        let rust_case = shape
1082            .cases
1083            .iter()
1084            .find(|c| c.member.node_id == member.node_id)
1085            .ok_or(EncodeError::NoWriter {
1086                member: member.name.clone(),
1087                kind: "a union case the shape does not hold",
1088            })?;
1089        let variant = rust_case.variant.clone();
1090        let carries = rust_case.carries_discriminator();
1091        let member_type = member.type_.resolved().ok_or(EncodeError::NoWriter {
1092            member: member.name.clone(),
1093            kind: "a union case with an unresolved type",
1094        })?;
1095        let member_write = write_of(
1096            ast,
1097            node.module,
1098            member_type,
1099            key,
1100            codec,
1101            Position {
1102                behaviour: ast.effective_try_construct(None, Some(member_type)),
1103                written: None,
1104            },
1105        );
1106        let member_write = external_write(ast, member, member_write);
1107        let label_values = rust_case.labels.clone();
1108        let is_default = rust_case.is_default;
1109
1110        // The case's own header, which only a `@mutable` union's plan has.
1111        let case_plan = layout
1112            .members
1113            .iter()
1114            .find(|plan| plan.member == member.node_id)
1115            .copied();
1116
1117        // `Self::Variant(value)`, or `Self::Variant(discriminator, value)`
1118        // where the variant carries what `decode` read.
1119        let construct = |value: rust::Expr| {
1120            let mut args = Vec::new();
1121            if carries {
1122                args.push(rust::Expr::Path(Path::single("discriminator")));
1123            }
1124            args.push(value);
1125            rust::Expr::Call {
1126                callee: Box::new(rust::Expr::Path(Path::single("Ok"))),
1127                args: vec![rust::Expr::Call {
1128                    callee: Box::new(rust::Expr::Path(Path::new(&["Self", &variant]))),
1129                    args,
1130                }],
1131            }
1132        };
1133        let read_member = read_for(&member.name, &member_write)?;
1134        let build = match case_plan {
1135            None => construct(rust::Expr::Try(Box::new(read_member))),
1136            Some(plan) => {
1137                let mut statements = read_header(&plan);
1138                statements.push(let_statement(
1139                    "value",
1140                    rust::Expr::Try(Box::new(read_member)),
1141                ));
1142                statements.extend(finish_read_header(&plan));
1143                statements.push(rust::Statement::Tail(construct(rust::Expr::Path(
1144                    Path::single("value"),
1145                ))));
1146                rust::Expr::Block(rust::Block { statements })
1147            }
1148        };
1149
1150        if is_default {
1151            default_arm = Some(build.clone());
1152        } else {
1153            for value in &label_values {
1154                decode_arms.push(rust::MatchArm {
1155                    pattern: label_pattern(*value)?,
1156                    guard: None,
1157                    body: build.clone(),
1158                });
1159            }
1160        }
1161
1162        // `Self::Variant(value) => { <discriminator write>; <member write>; }`
1163        // with the discriminator the case fixes, or
1164        // `Self::Variant(discriminator, value) => { <check>; <discriminator
1165        // write>; <member write>; }` with the one the variant carries.
1166        let mut statements = Vec::new();
1167        if carries {
1168            let own = (!is_default).then_some(label_values.as_slice());
1169            let what = if is_default {
1170                format!("a discriminator no label of `{name}` takes, for its `default:` member")
1171            } else {
1172                format!("a label of the member `{}` of `{name}`", member.name)
1173            };
1174            statements.push(check_carried(own, what)?);
1175        }
1176        if let Some(plan) = &discriminator_plan {
1177            statements.extend(open_header(plan, steps.discriminator));
1178        }
1179        let discriminator_write_expr = if carries {
1180            write_carried()?
1181        } else {
1182            // A case that does not carry its discriminator has exactly one
1183            // label: `carries_discriminator` is true for every other.
1184            let label = label_values.first().copied().ok_or(EncodeError::NoWriter {
1185                member: member.name.clone(),
1186                kind: "a union case carrying no case label",
1187            })?;
1188            write_expr_for(&discriminator_write, written_label(label)?, &name, false)?
1189        };
1190        statements.push(rust::Statement::Semi(discriminator_write_expr));
1191        if let Some(plan) = &discriminator_plan {
1192            statements.extend(close_header(plan));
1193        }
1194        if let Some(plan) = &case_plan {
1195            statements.extend(open_header(plan, steps.member(plan, &member.name)?));
1196        }
1197        // `value` is the variant's binding, which a `match` on `&self`
1198        // makes a reference.
1199        statements.push(rust::Statement::Semi(write_expr_for(
1200            &member_write,
1201            rust::Expr::Path(Path::single("value")),
1202            &member.name,
1203            true,
1204        )?));
1205        if let Some(plan) = &case_plan {
1206            statements.extend(close_header(plan));
1207        }
1208        let mut elements = Vec::new();
1209        if carries {
1210            elements.push(rust::Pattern::Binding(Ident::new("discriminator")));
1211        }
1212        elements.push(rust::Pattern::Binding(Ident::new("value")));
1213        encode_arms.push(rust::MatchArm {
1214            pattern: rust::Pattern::TupleStruct {
1215                path: Path::new(&["Self", &variant]),
1216                elements,
1217            },
1218            guard: None,
1219            body: rust::Expr::Block(rust::Block { statements }),
1220        });
1221    }
1222
1223    // The variant that holds a discriminator selecting no member
1224    // (§7.4.3.5: the member is absent): the discriminator and nothing
1225    // after it, under the same check as the `default:` case's.
1226    if shape.no_member {
1227        let mut statements = vec![check_carried(
1228            None,
1229            format!("a discriminator no label of `{name}` takes, for no member"),
1230        )?];
1231        if let Some(plan) = &discriminator_plan {
1232            statements.extend(open_header(plan, steps.discriminator));
1233        }
1234        statements.push(rust::Statement::Semi(write_carried()?));
1235        if let Some(plan) = &discriminator_plan {
1236            statements.extend(close_header(plan));
1237        }
1238        encode_arms.push(rust::MatchArm {
1239            pattern: rust::Pattern::TupleStruct {
1240                path: Path::new(&["Self", crate::rust_union::NO_MEMBER]),
1241                elements: vec![rust::Pattern::Binding(Ident::new("discriminator"))],
1242            },
1243            guard: None,
1244            body: rust::Expr::Block(rust::Block { statements }),
1245        });
1246    }
1247
1248    // Past the labels: the `default:` case, or the variant that holds no
1249    // member. A Rust `char` has more values than an IDL `char` or `wchar`
1250    // labels can cover, so a character discriminator always has the arm;
1251    // an integer, `bool` or enumeration discriminator whose labels cover
1252    // its type has no value left for it.
1253    let rest = default_arm.or_else(|| {
1254        shape.no_member.then(|| rust::Expr::Call {
1255            callee: Box::new(rust::Expr::Path(Path::single("Ok"))),
1256            args: vec![rust::Expr::Call {
1257                callee: Box::new(rust::Expr::Path(Path::new(&[
1258                    "Self",
1259                    crate::rust_union::NO_MEMBER,
1260                ]))),
1261                args: vec![rust::Expr::Path(Path::single("discriminator"))],
1262            }],
1263        })
1264    });
1265    match rest {
1266        Some(body) => decode_arms.push(rust::MatchArm {
1267            pattern: rust::Pattern::Wildcard,
1268            guard: None,
1269            body,
1270        }),
1271        None if shape.kind == crate::rust_union::DiscriminatorKind::Character => {
1272            decode_arms.push(refusal_arm(&format!(
1273                "a discriminator selecting a case of `{name}`"
1274            )));
1275        }
1276        None => {}
1277    }
1278
1279    // An enumeration discriminator is read as its holder integer and
1280    // turned into the enumerator here, so the arms above match — and the
1281    // variants carry — the enumeration and not its holder. A holder no
1282    // enumerator has is refused, as the enumeration's own `decode` does.
1283    let to_enumeration = match shape.kind {
1284        crate::rust_union::DiscriminatorKind::Enumeration(enumeration) => {
1285            let mut arms = Vec::new();
1286            for enumerator in ast.enumerators_of(enumeration) {
1287                let (_, value) = enumerator_variant_and_value(
1288                    ast,
1289                    &enumeration_name(enumeration),
1290                    *enumerator,
1291                    key,
1292                )?;
1293                arms.push(rust::MatchArm {
1294                    pattern: rust::Pattern::Literal(Literal::int(value)),
1295                    guard: None,
1296                    body: rust::Expr::Path(
1297                        crate::rust_union::enumerator_path_of(
1298                            ast,
1299                            node.module,
1300                            enumeration,
1301                            *enumerator,
1302                        )
1303                        .map_err(refuse_label)?,
1304                    ),
1305                });
1306            }
1307            arms.push(rust::MatchArm {
1308                pattern: rust::Pattern::Wildcard,
1309                guard: None,
1310                body: refused(
1311                    format!("an enumerator of the discriminator of `{name}`"),
1312                    "r",
1313                ),
1314            });
1315            Some(rust::Statement::Let {
1316                name: Ident::new("discriminator"),
1317                type_: None,
1318                value: Some(rust::Expr::Match {
1319                    scrutinee: Box::new(rust::Expr::Path(Path::single("discriminator"))),
1320                    arms,
1321                }),
1322                mutable: false,
1323            })
1324        }
1325        _ => None,
1326    };
1327
1328    let mut encode_statements = Vec::new();
1329    if per_sample(key) {
1330        // As in `encode_fns`: the flag is read only by a case member or
1331        // by the discriminator whose header form the sample decides.
1332        encode_statements.push(rust::Statement::Let {
1333            name: Ident::new("_"),
1334            type_: None,
1335            value: Some(rust::Expr::Path(Path::single(EXTENDED_PARAM))),
1336            mutable: false,
1337        });
1338    }
1339    if layout.needs_dheader {
1340        encode_statements.push(let_statement("dheader", {
1341            rust::Expr::MethodCall {
1342                receiver: Box::new(rust::Expr::Path(Path::single("w"))),
1343                method: Ident::new("write_dheader"),
1344                args: Vec::new(),
1345            }
1346        }));
1347    }
1348    encode_statements.push(rust::Statement::Semi(rust::Expr::Match {
1349        scrutinee: Box::new(rust::Expr::Path(Path::single("self"))),
1350        arms: encode_arms,
1351    }));
1352    if layout.needs_sentinel {
1353        encode_statements.push(call_statement("write_sentinel", Vec::new()));
1354    }
1355    if layout.needs_dheader {
1356        encode_statements.push(call_statement(
1357            "finish_dheader",
1358            vec![rust::Expr::Path(Path::single("dheader"))],
1359        ));
1360    }
1361    encode_statements.push(rust::Statement::Tail(rust::Expr::Call {
1362        callee: Box::new(rust::Expr::Path(Path::single("Ok"))),
1363        args: vec![rust::Expr::Tuple(Vec::new())],
1364    }));
1365    // As in `encode_fns`: under `ElementPadding::Packed` a union value is
1366    // its own alignment block. See `zerodds-wire-codec`'s `write_union`.
1367    let encode_statements = wrap_packed_body(
1368        encode_statements,
1369        "w",
1370        key.element_padding == ElementPadding::Packed,
1371    );
1372    let encode = encode_function(&body_name(codec, key), &encode_statements, per_sample(key));
1373
1374    let decode = rust::Item::Function(rust::Function {
1375        doc: Doc::none(),
1376        attrs: Vec::new(),
1377        visibility: Visibility::Public,
1378        name: Ident::new(&format!("decode{codec}")),
1379        generics: Vec::new(),
1380        params: vec![rust::Param {
1381            name: Some(Ident::new("r")),
1382            type_: rust::Type::Ref {
1383                target: Box::new(rust::Type::named(Path::single("Reader"))),
1384                mutable: true,
1385                lifetime: None,
1386            },
1387            receiver: None,
1388        }],
1389        returns: Some(rust::Type::generic(
1390            Path::single("Result"),
1391            vec![
1392                rust::Type::named(Path::single("Self")),
1393                rust::Type::named(Path::single("WireError")),
1394            ],
1395        )),
1396        body: Some(rust::Block {
1397            statements: wrap_packed_body(
1398                union_decode_body(
1399                    layout,
1400                    discriminator_plan,
1401                    &discriminator_write,
1402                    to_enumeration,
1403                    decode_arms,
1404                )?,
1405                "r",
1406                key.element_padding == ElementPadding::Packed,
1407            ),
1408        }),
1409    });
1410
1411    let mut out = with_entry(encode, encode_entry(ast, type_, key, codec)?);
1412    out.push(decode);
1413    Ok(out)
1414}
1415
1416/// The body of a union's `decode`: the framing the plan gives it around
1417/// the discriminator's read and the match that reads the branch.
1418///
1419/// A `@final` union keeps the shape it always had — the discriminator,
1420/// then the match as the tail — so its generated code does not change.
1421fn union_decode_body(
1422    layout: &TypeLayout,
1423    discriminator_plan: Option<MemberPlan>,
1424    discriminator_write: &MemberWrite,
1425    to_enumeration: Option<rust::Statement>,
1426    decode_arms: Vec<rust::MatchArm>,
1427) -> Result<Vec<rust::Statement>, EncodeError> {
1428    let read_discriminator = rust::Statement::Let {
1429        name: Ident::new("discriminator"),
1430        type_: None,
1431        value: Some(rust::Expr::Try(Box::new(read_for(
1432            "discriminator",
1433            discriminator_write,
1434        )?))),
1435        mutable: false,
1436    };
1437    let branch = rust::Expr::Match {
1438        scrutinee: Box::new(rust::Expr::Path(Path::single("discriminator"))),
1439        arms: decode_arms,
1440    };
1441    if !layout.needs_dheader && !layout.needs_sentinel && discriminator_plan.is_none() {
1442        let mut statements = vec![read_discriminator];
1443        statements.extend(to_enumeration);
1444        statements.push(rust::Statement::Tail(branch));
1445        return Ok(statements);
1446    }
1447
1448    let mut statements = Vec::new();
1449    if layout.needs_dheader {
1450        // Where the writer's union ends: a newer writer's branch may
1451        // carry more than this reader's type knows, and the reader steps
1452        // over it rather than reading the next value from inside it.
1453        statements.push(let_statement(
1454            "end",
1455            rust::Expr::Binary {
1456                op: rust::BinaryOp::Add,
1457                lhs: Box::new(rust::Expr::Try(Box::new(call("read_dheader", Vec::new())))),
1458                rhs: Box::new(call("position", Vec::new())),
1459            },
1460        ));
1461    }
1462    if let Some(plan) = &discriminator_plan {
1463        statements.extend(read_header(plan));
1464    }
1465    statements.push(read_discriminator);
1466    if let Some(plan) = &discriminator_plan {
1467        statements.extend(finish_read_header(plan));
1468    }
1469    statements.extend(to_enumeration);
1470    statements.push(let_statement("value", rust::Expr::Try(Box::new(branch))));
1471    if layout.needs_sentinel {
1472        statements.extend(read_sentinel());
1473    }
1474    if layout.needs_dheader {
1475        statements.push(rust::Statement::Semi(rust::Expr::Try(Box::new(call(
1476            "skip_to",
1477            vec![rust::Expr::Path(Path::single("end"))],
1478        )))));
1479    }
1480    statements.push(rust::Statement::Tail(rust::Expr::Call {
1481        callee: Box::new(rust::Expr::Path(Path::single("Ok"))),
1482        args: vec![rust::Expr::Path(Path::single("value"))],
1483    }));
1484    Ok(statements)
1485}
1486
1487/// Rule (23)'s `PID_SENTINEL` at the end of a PL_CDR1 parameter list,
1488/// read and checked: a parameter list that ends anywhere else is refused.
1489fn read_sentinel() -> Vec<rust::Statement> {
1490    vec![
1491        let_statement(
1492            "sentinel",
1493            rust::Expr::Try(Box::new(call("read_pid", Vec::new()))),
1494        ),
1495        rust::Statement::Semi(rust::Expr::If {
1496            condition: Box::new(rust::Expr::Not(Box::new(rust::Expr::Field {
1497                receiver: Box::new(rust::Expr::Path(Path::single("sentinel"))),
1498                name: Ident::new("end"),
1499            }))),
1500            then: rust::Block {
1501                statements: vec![rust::Statement::Semi(rust::Expr::Return(Some(Box::new(
1502                    rust::Expr::Call {
1503                        callee: Box::new(rust::Expr::Path(Path::single("Err"))),
1504                        args: vec![rust::Expr::Call {
1505                            callee: Box::new(rust::Expr::Path(Path::new(&[
1506                                "WireError",
1507                                "refused",
1508                            ]))),
1509                            args: vec![
1510                                call("position", Vec::new()),
1511                                rust::Expr::Literal(Literal::string(
1512                                    "PID_SENTINEL closing the parameter list, per rule (23)",
1513                                )),
1514                            ],
1515                        }],
1516                    },
1517                ))))],
1518            },
1519            otherwise: None,
1520        }),
1521    ]
1522}
1523
1524/// A member header's read, before the value, for a member read where
1525/// the stream puts it — the decoder's half of [`open_header`].
1526fn read_header(plan: &MemberPlan) -> Vec<rust::Statement> {
1527    match plan.header {
1528        MemberHeader::NoHeader | MemberHeader::OptionalFlag => Vec::new(),
1529        MemberHeader::PidCdr1 { .. } | MemberHeader::OptionalCdr1 { .. } => vec![let_statement(
1530            "pid",
1531            rust::Expr::Try(Box::new(call("read_pid", Vec::new()))),
1532        )],
1533        MemberHeader::EmHeader { .. } => vec![let_statement(
1534            "_emheader",
1535            rust::Expr::Try(Box::new(call("read_emheader", Vec::new()))),
1536        )],
1537    }
1538}
1539
1540/// What closes a header after its value on the read side — the
1541/// decoder's half of [`close_header`].
1542fn finish_read_header(plan: &MemberPlan) -> Vec<rust::Statement> {
1543    match plan.header {
1544        MemberHeader::PidCdr1 { .. } | MemberHeader::OptionalCdr1 { .. } => {
1545            vec![rust::Statement::Semi(rust::Expr::Try(Box::new(call(
1546                "finish_pid",
1547                vec![rust::Expr::Path(Path::single("pid"))],
1548            ))))]
1549        }
1550        MemberHeader::NoHeader | MemberHeader::OptionalFlag | MemberHeader::EmHeader { .. } => {
1551            Vec::new()
1552        }
1553    }
1554}
1555
1556/// The write of one value, as an expression, with its `?` where the
1557/// write can refuse.
1558fn write_expr_for(
1559    kind: &MemberWrite,
1560    value: rust::Expr,
1561    at: &str,
1562    borrowed: bool,
1563) -> Result<rust::Expr, EncodeError> {
1564    let (expr, fallible) = write_call("w", SinkShape::Reference, at, kind, value, borrowed)?;
1565    Ok(if fallible {
1566        rust::Expr::Try(Box::new(expr))
1567    } else {
1568        expr
1569    })
1570}
1571
1572/// The unsigned Rust type an enumeration's holder takes, and the
1573/// `Writer`/`Reader` method for it.
1574///
1575/// `zerodds_ast_layout::enumeration_bit_bound` decides the width, the
1576/// same crossing function the member position asks — a second table
1577/// here would be a second place for a holder to be the wrong size.
1578const fn enum_holder(bits: u16) -> (rust::Primitive, &'static str, &'static str) {
1579    match bits {
1580        0..=8 => (rust::Primitive::U8, "write_u8", "read_u8"),
1581        9..=16 => (rust::Primitive::U16, "write_u16", "read_u16"),
1582        17..=32 => (rust::Primitive::U32, "write_u32", "read_u32"),
1583        _ => (rust::Primitive::U64, "write_u64", "read_u64"),
1584    }
1585}
1586
1587/// The encoder body for an enumeration: its holder, at the width its bit
1588/// bound gives, and — under [`per_sample`] — the public entry beside it.
1589///
1590/// An enumeration's body reads no [`EXTENDED_PARAM`] and never writes a
1591/// parameter header: `keeps_short_header` counts an enumeration among the
1592/// types that keep the short form in a sample past RTI's step. The body
1593/// takes the flag anyway because a `MemberWrite::Nested` call on an
1594/// enumeration names it, and the same name under every plan is what keeps
1595/// a call from framing an enumeration member with the wrong encoder.
1596///
1597/// A `match` and not `*self as uN`. The cast is a move, and
1598/// `crate::adapter::cst` derives nothing on what it declares — not
1599/// `Copy` either — so the cast form did not compile. The match is also
1600/// the honest one: it names each enumerator's wire value where a
1601/// reader can compare it with `decode`'s.
1602fn enumeration_encode_fns(
1603    ast: &Ast,
1604    type_: Id<TypeNode>,
1605    enumerators: &[Id<zerodds_ast::EnumeratorNode>],
1606    key: LayoutKey,
1607    codec: &'static str,
1608) -> Result<Vec<rust::Item>, EncodeError> {
1609    let node = ast.type_(type_).map_err(|error| EncodeError::NoLayout {
1610        detail: alloc_format(&error),
1611    })?;
1612    let bits = zerodds_ast_layout::enumeration_bit_bound(ast, type_, key.enum_holder);
1613    let (primitive, writer, _) = enum_holder(bits);
1614
1615    let mut arms = Vec::new();
1616    for id in enumerators {
1617        let (name, value) = enumerator_variant_and_value(ast, &node.name, *id, key)?;
1618        arms.push(rust::MatchArm {
1619            pattern: rust::Pattern::Path(Path::new(&["Self", &name])),
1620            guard: None,
1621            body: rust::Expr::Literal(Literal::int(value)),
1622        });
1623    }
1624
1625    let mut statements = Vec::new();
1626    if per_sample(key) {
1627        statements.push(rust::Statement::Let {
1628            name: Ident::new("_"),
1629            type_: None,
1630            value: Some(rust::Expr::Path(Path::single(EXTENDED_PARAM))),
1631            mutable: false,
1632        });
1633    }
1634    statements.extend([
1635        rust::Statement::Let {
1636            name: Ident::new("value"),
1637            type_: Some(rust::Type::Primitive(primitive)),
1638            value: Some(rust::Expr::Match {
1639                scrutinee: Box::new(rust::Expr::Path(Path::single("self"))),
1640                arms,
1641            }),
1642            mutable: false,
1643        },
1644        rust::Statement::Semi(rust::Expr::MethodCall {
1645            receiver: Box::new(rust::Expr::Path(Path::single("w"))),
1646            method: Ident::new(writer),
1647            args: vec![rust::Expr::Path(Path::single("value"))],
1648        }),
1649        rust::Statement::Tail(rust::Expr::Call {
1650            callee: Box::new(rust::Expr::Path(Path::single("Ok"))),
1651            args: vec![rust::Expr::Tuple(Vec::new())],
1652        }),
1653    ]);
1654    Ok(with_entry(
1655        encode_function(&body_name(codec, key), &statements, per_sample(key)),
1656        encode_entry(ast, type_, key, codec)?,
1657    ))
1658}
1659
1660/// One enumerator's Rust variant name and the integer it serialises as.
1661///
1662/// The value goes through
1663/// [`zerodds_ast_layout::enumerator_wire_value`] and not through
1664/// `Ast::effective_enumerator_value`. The two answer differently under
1665/// [`zerodds_ast_layout::EnumeratorValue::Ordinal`], and this integer is
1666/// the one that leaves the process: `enumeration_encode_fn` puts it in
1667/// the holder and `enumeration_decode_fn` matches an incoming holder
1668/// against it. Reading the declared value here wrote `@value(200)`
1669/// where a front end that drops the annotation writes `1`, in both
1670/// directions, with nothing at run time saying so — the Rust column
1671/// carries no corpus row that reaches this code, so only
1672/// `crates/wire-probe/tests/enumerator_value.rs::the_sites_go_through_the_crossing`
1673/// stands between it and a silent bypass.
1674///
1675/// The Rust *discriminant* is a different question and stays declared:
1676/// `crate::adapter::cst` writes it, and it is the language mapping.
1677fn enumerator_variant_and_value(
1678    ast: &Ast,
1679    enumeration: &str,
1680    id: Id<zerodds_ast::EnumeratorNode>,
1681    key: LayoutKey,
1682) -> Result<(String, i128), EncodeError> {
1683    let enumerator = ast.enumerator(id).map_err(|error| EncodeError::NoLayout {
1684        detail: alloc_format(&error),
1685    })?;
1686    let value = zerodds_ast_layout::enumerator_wire_value(ast, id, key.enumerator_value)
1687        .ok_or_else(|| EncodeError::NoEnumeratorValue {
1688            enumeration: enumeration.to_string(),
1689            enumerator: enumerator.name.clone(),
1690            detail: "the enumerator has no computable wire value",
1691        })?;
1692    // `escape_keyword` and not `variant_name`: the declaration is
1693    // `crate::adapter::cst`'s and it writes the enumerator's own name.
1694    // `variant_name` capitalises, which is what a *union* case takes —
1695    // it is built from a member name — and using it here produced
1696    // `Self::Missing_node` for an enumerator declared `missing_node`.
1697    Ok((rust::escape_keyword(&enumerator.name), value))
1698}
1699
1700/// `fn decode(r: &mut Reader) -> Result<Self, WireError>` for an
1701/// enumeration.
1702///
1703/// A value that names no enumerator is **refused** and not mapped onto
1704/// the first variant. §7.2.2.4.1.2 gives an enumeration a fixed set of
1705/// literals; a decoder that answered `RED` for a value nobody wrote
1706/// would hand the caller a sample that never existed, which is the
1707/// silent half of the drop class.
1708fn enumeration_decode_fn(
1709    ast: &Ast,
1710    type_: Id<TypeNode>,
1711    enumerators: &[Id<zerodds_ast::EnumeratorNode>],
1712    key: LayoutKey,
1713    codec: &'static str,
1714) -> Result<rust::Item, EncodeError> {
1715    let node = ast.type_(type_).map_err(|error| EncodeError::NoLayout {
1716        detail: alloc_format(&error),
1717    })?;
1718    let bits = zerodds_ast_layout::enumeration_bit_bound(ast, type_, key.enum_holder);
1719    let (_, _, reader) = enum_holder(bits);
1720
1721    let mut arms = Vec::new();
1722    for id in enumerators {
1723        let (name, value) = enumerator_variant_and_value(ast, &node.name, *id, key)?;
1724        arms.push(rust::MatchArm {
1725            pattern: rust::Pattern::Literal(Literal::int(value)),
1726            guard: None,
1727            body: rust::Expr::Call {
1728                callee: Box::new(rust::Expr::Path(Path::single("Ok"))),
1729                args: vec![rust::Expr::Path(Path::new(&["Self", &name]))],
1730            },
1731        });
1732    }
1733    arms.push(refusal_arm(&format!("an enumerator of `{}`", node.name)));
1734
1735    let statements = vec![
1736        rust::Statement::Let {
1737            name: Ident::new("value"),
1738            type_: None,
1739            value: Some(rust::Expr::Try(Box::new(rust::Expr::MethodCall {
1740                receiver: Box::new(rust::Expr::Path(Path::single("r"))),
1741                method: Ident::new(reader),
1742                args: Vec::new(),
1743            }))),
1744            mutable: false,
1745        },
1746        rust::Statement::Tail(rust::Expr::Match {
1747            scrutinee: Box::new(rust::Expr::Path(Path::single("value"))),
1748            arms,
1749        }),
1750    ];
1751
1752    Ok(rust::Item::Function(rust::Function {
1753        doc: Doc::none(),
1754        attrs: Vec::new(),
1755        visibility: Visibility::Public,
1756        name: Ident::new(&format!("decode{codec}")),
1757        generics: Vec::new(),
1758        params: vec![rust::Param {
1759            name: Some(Ident::new("r")),
1760            type_: rust::Type::Ref {
1761                target: Box::new(rust::Type::named(Path::single("Reader"))),
1762                mutable: true,
1763                lifetime: None,
1764            },
1765            receiver: None,
1766        }],
1767        returns: Some(rust::Type::generic(
1768            Path::single("Result"),
1769            vec![
1770                rust::Type::named(Path::single("Self")),
1771                rust::Type::named(Path::single("WireError")),
1772            ],
1773        )),
1774        body: Some(rust::Block { statements }),
1775    }))
1776}
1777
1778/// The anchor a bitset packs into, in octets.
1779///
1780/// The **plan's** answer and not a sum of the widths: an anchor is one,
1781/// two, four or eight octets whatever the bitfields add up to, and
1782/// `zerodds_ast_layout::xcdr::plan` is the crossing the other twenty
1783/// columns ask through `Extent::Sized`.
1784fn bitset_anchor(layout: &TypeLayout, name: &str) -> Result<u32, EncodeError> {
1785    match layout.extent {
1786        Extent::Sized(anchor) => Ok(anchor),
1787        Extent::Unbounded => Err(EncodeError::NoWriter {
1788            member: name.to_string(),
1789            kind: "bitset whose anchor the planner does not size",
1790        }),
1791    }
1792}
1793
1794/// Each named bitfield with the width and the position it takes in the
1795/// anchor.
1796///
1797/// Positions run cumulatively from zero in declaration order, **padding
1798/// included**: an anonymous `bitfield<4>` carries no value and still
1799/// moves the next field along. The whole inheritance chain, base first,
1800/// because that is what the planner sizes the anchor over —
1801/// `crates/lang-rules/src/emit/encode.rs` reads the same list the same
1802/// way.
1803///
1804/// # Errors
1805/// [`EncodeError::NoWriter`] for a chain that runs past the sixty-four
1806/// bits an anchor holds — never a field silently dropped off the end.
1807fn bitset_fields(
1808    ast: &Ast,
1809    type_: Id<TypeNode>,
1810    name: &str,
1811) -> Result<Vec<(String, u16, u32)>, EncodeError> {
1812    let mut out = Vec::new();
1813    let mut position: u32 = 0;
1814    for field in &ast.all_bitfields_of(type_) {
1815        // Refused and not skipped: a skipped field takes its width out
1816        // of the running position, so every field after it would be
1817        // packed at an offset the peer does not read it from — a sample
1818        // that encodes, reports success and means something else.
1819        let Ok(field_node) = ast.bitfield(*field) else {
1820            return Err(EncodeError::NoWriter {
1821                member: name.to_string(),
1822                kind: "bitset with a stale bitfield handle",
1823            });
1824        };
1825        let width = u32::from(field_node.bits);
1826        if position.saturating_add(width) > 64 {
1827            return Err(EncodeError::NoWriter {
1828                member: name.to_string(),
1829                kind: "bitset wider than the sixty-four bits an anchor holds",
1830            });
1831        }
1832        if let Some(field_name) = field_node.name.as_ref() {
1833            out.push((
1834                rust::escape_keyword(field_name).to_string(),
1835                field_node.bits,
1836                position,
1837            ));
1838        }
1839        position = position.saturating_add(width);
1840    }
1841    Ok(out)
1842}
1843
1844/// A bitset's encoder body — each field shifted into the anchor, then the
1845/// anchor — and, under [`per_sample`], the public entry beside it.
1846///
1847/// As `enumeration_encode_fns`'s: the body reads no [`EXTENDED_PARAM`]
1848/// and takes it anyway, because a `MemberWrite::Nested` call on a bitset
1849/// names it.
1850///
1851/// `Writer::bitfield` does the shift and refuses a value the field's
1852/// width cannot hold. The generated field is as wide as its **holder**
1853/// — `bitfield<3> a` is a `u8` — so a value that does not fit is a
1854/// program error the writer reports rather than an octet it masks off.
1855fn bitset_encode_fns(
1856    ast: &Ast,
1857    type_: Id<TypeNode>,
1858    layout: &TypeLayout,
1859    key: LayoutKey,
1860    codec: &'static str,
1861) -> Result<Vec<rust::Item>, EncodeError> {
1862    let node = ast.type_(type_).map_err(|error| EncodeError::NoLayout {
1863        detail: alloc_format(&error),
1864    })?;
1865    let anchor = bitset_anchor(layout, &node.name)?;
1866    let mut statements = Vec::new();
1867    if per_sample(key) {
1868        statements.push(rust::Statement::Let {
1869            name: Ident::new("_"),
1870            type_: None,
1871            value: Some(rust::Expr::Path(Path::single(EXTENDED_PARAM))),
1872            mutable: false,
1873        });
1874    }
1875    statements.push(rust::Statement::Let {
1876        name: Ident::new("bits"),
1877        type_: Some(rust::Type::Primitive(rust::Primitive::U64)),
1878        value: Some(rust::Expr::Literal(Literal::int(0))),
1879        mutable: true,
1880    });
1881    for (field, bits, position) in bitset_fields(ast, type_, &node.name)? {
1882        let value = rust::Expr::Field {
1883            receiver: Box::new(rust::Expr::Path(Path::single("self"))),
1884            name: Ident::new(&field),
1885        };
1886        statements.push(rust::Statement::Semi(rust::Expr::Assign {
1887            target: Box::new(rust::Expr::Path(Path::single("bits"))),
1888            value: Box::new(rust::Expr::Binary {
1889                op: rust::BinaryOp::BitOr,
1890                lhs: Box::new(rust::Expr::Path(Path::single("bits"))),
1891                rhs: Box::new(rust::Expr::Try(Box::new(rust::Expr::MethodCall {
1892                    receiver: Box::new(rust::Expr::Path(Path::single("w"))),
1893                    method: Ident::new("bitfield"),
1894                    args: vec![
1895                        widen_to_u64(value, bits),
1896                        rust::Expr::Literal(Literal::int(i128::from(bits))),
1897                        rust::Expr::Literal(Literal::int(i128::from(position))),
1898                    ],
1899                }))),
1900            }),
1901        }));
1902    }
1903    statements.push(rust::Statement::Semi(rust::Expr::Try(Box::new(
1904        rust::Expr::MethodCall {
1905            receiver: Box::new(rust::Expr::Path(Path::single("w"))),
1906            method: Ident::new("write_bitset"),
1907            args: vec![
1908                rust::Expr::Literal(Literal::int(i128::from(anchor))),
1909                rust::Expr::Path(Path::single("bits")),
1910            ],
1911        },
1912    ))));
1913    statements.push(rust::Statement::Tail(rust::Expr::Call {
1914        callee: Box::new(rust::Expr::Path(Path::single("Ok"))),
1915        args: vec![rust::Expr::Tuple(Vec::new())],
1916    }));
1917    Ok(with_entry(
1918        encode_function(&body_name(codec, key), &statements, per_sample(key)),
1919        encode_entry(ast, type_, key, codec)?,
1920    ))
1921}
1922
1923/// A bitset's `decode`: the anchor, then each field shifted and masked
1924/// back out of it.
1925///
1926/// The mask is written out rather than left to the narrowing cast: the
1927/// anchor is sixty-four bits and the field is as wide as its holder, so
1928/// `bitfield<3> a` in a `u8` would otherwise take five bits that belong
1929/// to the field beside it.
1930fn bitset_decode_fn(
1931    ast: &Ast,
1932    type_: Id<TypeNode>,
1933    layout: &TypeLayout,
1934    codec: &'static str,
1935) -> Result<rust::Item, EncodeError> {
1936    let node = ast.type_(type_).map_err(|error| EncodeError::NoLayout {
1937        detail: alloc_format(&error),
1938    })?;
1939    let anchor = bitset_anchor(layout, &node.name)?;
1940    let mut statements = vec![rust::Statement::Let {
1941        name: Ident::new("bits"),
1942        type_: None,
1943        value: Some(rust::Expr::Try(Box::new(rust::Expr::MethodCall {
1944            receiver: Box::new(rust::Expr::Path(Path::single("r"))),
1945            method: Ident::new("read_bitset"),
1946            args: vec![rust::Expr::Literal(Literal::int(i128::from(anchor)))],
1947        }))),
1948        mutable: false,
1949    }];
1950    let mut fields = Vec::new();
1951    for (field, bits, position) in bitset_fields(ast, type_, &node.name)? {
1952        let mask: u64 = if bits >= 64 {
1953            u64::MAX
1954        } else {
1955            (1u64 << bits) - 1
1956        };
1957        let masked = rust::Expr::Binary {
1958            op: rust::BinaryOp::BitAnd,
1959            lhs: Box::new(rust::Expr::Binary {
1960                op: rust::BinaryOp::Shr,
1961                lhs: Box::new(rust::Expr::Path(Path::single("bits"))),
1962                rhs: Box::new(rust::Expr::Literal(Literal::int(i128::from(position)))),
1963            }),
1964            rhs: Box::new(rust::Expr::Literal(Literal::int(i128::from(mask)))),
1965        };
1966        let holder = crate::adapter::cst::bitmask_primitive(bits);
1967        let value = if holder == rust::Primitive::U64 {
1968            masked
1969        } else {
1970            rust::Expr::Cast {
1971                value: Box::new(masked),
1972                type_: Box::new(rust::Type::Primitive(holder)),
1973            }
1974        };
1975        fields.push((Ident::new(&field), value));
1976    }
1977    statements.push(rust::Statement::Tail(rust::Expr::Call {
1978        callee: Box::new(rust::Expr::Path(Path::single("Ok"))),
1979        args: vec![rust::Expr::StructLit {
1980            path: Path::single("Self"),
1981            fields,
1982            base: None,
1983        }],
1984    }));
1985    Ok(rust::Item::Function(rust::Function {
1986        doc: Doc::none(),
1987        attrs: Vec::new(),
1988        visibility: Visibility::Public,
1989        name: Ident::new(&format!("decode{codec}")),
1990        generics: Vec::new(),
1991        params: vec![rust::Param {
1992            name: Some(Ident::new("r")),
1993            type_: rust::Type::Ref {
1994                target: Box::new(rust::Type::named(Path::single("Reader"))),
1995                mutable: true,
1996                lifetime: None,
1997            },
1998            receiver: None,
1999        }],
2000        returns: Some(rust::Type::generic(
2001            Path::single("Result"),
2002            vec![
2003                rust::Type::named(Path::single("Self")),
2004                rust::Type::named(Path::single("WireError")),
2005            ],
2006        )),
2007        body: Some(rust::Block { statements }),
2008    }))
2009}
2010
2011/// The encoder **body** for a structure or a valuetype, and — under
2012/// [`per_sample`] — the public entry beside it.
2013///
2014/// The statements follow the plan's member order, which is wire order
2015/// and not necessarily declaration order.
2016///
2017/// Fallible even for a type with nothing fallible in it, so the
2018/// signature is the type's and not its members'. `char` and `wchar`
2019/// refuse a code point they cannot carry, and an `encode` whose
2020/// signature changed with the member list would make that a source
2021/// break for every caller the day someone added one.
2022///
2023/// `steps` names the members whose PL_CDR1 header form the sample
2024/// decides, and the body reads [`EXTENDED_PARAM`] for each of them.
2025fn encode_fns(
2026    ast: &Ast,
2027    type_: Id<TypeNode>,
2028    layout: &TypeLayout,
2029    steps: &StepHeaders,
2030    key: LayoutKey,
2031    codec: &'static str,
2032) -> Result<Vec<rust::Item>, EncodeError> {
2033    let mut statements = Vec::new();
2034    // The flag is read by the members whose header form the sample
2035    // decides, and by nothing else: a `@final` aggregate of primitives
2036    // takes it and never looks at it, which is `unused_variables` in
2037    // every crate that includes the generated text. `let _ =` is valid
2038    // whether or not the body goes on to use it, so it is emitted
2039    // unconditionally rather than from a guess about which bodies read it
2040    // — the same shape `crate::emit::encode`'s `with_extended_used` uses
2041    // for the twenty other languages.
2042    if per_sample(key) {
2043        statements.push(rust::Statement::Let {
2044            name: Ident::new("_"),
2045            type_: None,
2046            value: Some(rust::Expr::Path(Path::single(EXTENDED_PARAM))),
2047            mutable: false,
2048        });
2049    }
2050
2051    // The DHEADER the plan asks for, and nothing more: whether an
2052    // appendable type carries one is the plan's decision, made once.
2053    if layout.needs_dheader {
2054        // The length is not known until the members are written, so the
2055        // header is a placeholder the runtime patches. A generator that
2056        // wrote a length it computed itself would be computing the
2057        // layout a second time.
2058        statements.push(rust::Statement::Let {
2059            name: Ident::new("dheader"),
2060            type_: None,
2061            value: Some(rust::Expr::MethodCall {
2062                receiver: Box::new(rust::Expr::Path(Path::single("w"))),
2063                method: Ident::new("write_dheader"),
2064                args: Vec::new(),
2065            }),
2066            mutable: false,
2067        });
2068    }
2069
2070    for plan in &layout.members {
2071        let Some(member) = member_of(ast, type_, plan.member, key, codec) else {
2072            continue;
2073        };
2074        let name = member.0;
2075        let kind = member.1;
2076        let optional = member_node_of(ast, type_, plan.member)
2077            .is_some_and(|member| member.facets.is_optional());
2078
2079        let receiver = holder_of(ast, type_, plan.member);
2080        if optional {
2081            statements.extend(write_optional_member(
2082                receiver,
2083                plan,
2084                &name,
2085                &kind,
2086                steps.member(plan, &name)?,
2087            )?);
2088            continue;
2089        }
2090
2091        // The header the plan chose, including the length code — this
2092        // is where `LengthCodePolicy` becomes visible in the output. A
2093        // member the sample's step decides gets the run-time branch
2094        // instead, because the plan named the short side of a decision
2095        // the sample makes.
2096        statements.extend(open_header(plan, steps.member(plan, &name)?));
2097        statements.push(write_for(receiver, &name, &kind)?);
2098        // The header's length is patched after the value, because that
2099        // is when it is known.
2100        statements.extend(close_header(plan));
2101    }
2102
2103    // The sentinel a PL_CDR1 parameter list ends with. A reader that
2104    // stops at the last member it recognises instead of at the sentinel
2105    // misreads whatever follows.
2106    if layout.needs_sentinel {
2107        statements.push(call_statement("write_sentinel", Vec::new()));
2108    }
2109
2110    if layout.needs_dheader {
2111        statements.push(call_statement(
2112            "finish_dheader",
2113            vec![rust::Expr::Path(Path::single("dheader"))],
2114        ));
2115    }
2116
2117    statements.push(rust::Statement::Tail(rust::Expr::Call {
2118        callee: Box::new(rust::Expr::Path(Path::single("Ok"))),
2119        args: vec![rust::Expr::Tuple(Vec::new())],
2120    }));
2121
2122    // An aggregate value is its own alignment block under
2123    // `ElementPadding::Packed`; see `zerodds-wire-codec`'s
2124    // `write_aggregate`.
2125    let statements = wrap_packed_body(
2126        statements,
2127        "w",
2128        key.element_padding == ElementPadding::Packed,
2129    );
2130
2131    Ok(with_entry(
2132        encode_function(&body_name(codec, key), &statements, per_sample(key)),
2133        encode_entry(ast, type_, key, codec)?,
2134    ))
2135}
2136
2137/// A generated `encode`, under either name the per-sample split gives it.
2138///
2139/// `flagged` is the body, which takes [`EXTENDED_PARAM`] after the writer;
2140/// the public entry takes no flag, because the sample whose root this type
2141/// is resolves the decision itself and passes the constant it resolved.
2142fn encode_function(name: &str, statements: &[rust::Statement], flagged: bool) -> rust::Item {
2143    let mut params = vec![
2144        rust::Param {
2145            name: None,
2146            type_: rust::Type::unit(),
2147            receiver: Some(rust::Receiver::Ref),
2148        },
2149        rust::Param {
2150            name: Some(Ident::new("w")),
2151            type_: rust::Type::Ref {
2152                target: Box::new(rust::Type::named(Path::single("Writer"))),
2153                mutable: true,
2154                lifetime: None,
2155            },
2156            receiver: None,
2157        },
2158    ];
2159    // After the writer, so the two the entry passes read in that order:
2160    // the sink, then the sample's decision.
2161    if flagged {
2162        params.push(rust::Param {
2163            name: Some(Ident::new(EXTENDED_PARAM)),
2164            type_: rust::Type::Primitive(rust::Primitive::Bool),
2165            receiver: None,
2166        });
2167    }
2168    rust::Item::Function(rust::Function {
2169        doc: Doc::none(),
2170        attrs: Vec::new(),
2171        visibility: Visibility::Public,
2172        name: Ident::new(name),
2173        generics: Vec::new(),
2174        params,
2175        returns: Some(rust::Type::generic(
2176            Path::single("Result"),
2177            vec![
2178                rust::Type::unit(),
2179                rust::Type::named(Path::single("WireError")),
2180            ],
2181        )),
2182        body: Some(rust::Block {
2183            statements: statements.to_vec(),
2184        }),
2185    })
2186}
2187
2188/// The body, and the public entry that bakes this type's own step
2189/// decision in beside it — or the body alone, for every plan whose header
2190/// form is fixed.
2191fn with_entry(body: rust::Item, entry: Option<rust::Item>) -> Vec<rust::Item> {
2192    match entry {
2193        Some(entry) => vec![body, entry],
2194        None => vec![body],
2195    }
2196}
2197
2198/// The public `encode(&self, w)` of a type under [`per_sample`], and
2199/// `None` without it.
2200///
2201/// RTI decides the PL_CDR1 header form for the **sample's** type, so the
2202/// sample whose root this type is resolves the decision once, here, and
2203/// hands it to the body — which threads it to every type the body calls.
2204/// The same nested `@mutable` type is then framed short inside a small
2205/// sample and extended inside a large one, which a single `encode` per
2206/// type cannot be.
2207///
2208/// The decision itself is [`sample_past_step`]'s, which asks
2209/// [`LayoutKey::for_sample`] — the crossing the reflective encoder makes
2210/// for the same sample, and the one that keeps
2211/// `crates/wire-codec`'s encoder and this codec writing the same octets.
2212///
2213/// # Errors
2214/// [`EncodeError`] when the sample's type cannot be sized, which is what
2215/// [`LayoutKey::for_sample`] reports.
2216fn encode_entry(
2217    ast: &Ast,
2218    type_: Id<TypeNode>,
2219    key: LayoutKey,
2220    codec: &'static str,
2221) -> Result<Option<rust::Item>, EncodeError> {
2222    if !per_sample(key) {
2223        return Ok(None);
2224    }
2225    let past = sample_past_step(ast, type_, key)?;
2226    let statements = vec![rust::Statement::Tail(rust::Expr::MethodCall {
2227        receiver: Box::new(rust::Expr::Path(Path::single("self"))),
2228        method: Ident::new(&body_name(codec, key)),
2229        args: vec![
2230            rust::Expr::Path(Path::single("w")),
2231            rust::Expr::Literal(Literal::Bool(past)),
2232        ],
2233    })];
2234    Ok(Some(encode_function(
2235        &format!("encode{codec}"),
2236        &statements,
2237        false,
2238    )))
2239}
2240
2241/// The statements that open one member's framing, before its value.
2242///
2243/// Split out of the loop because an `@optional` member writes some of
2244/// them inside a `match` arm and some outside it, and a second copy of
2245/// the header shapes would be a second place for a length code to be
2246/// wrong.
2247///
2248/// `chosen` says that the sample, not the plan, picks the PL_CDR1 header
2249/// form, and the statements are then a run-time branch on
2250/// [`EXTENDED_PARAM`] between `write_pid` and `write_pid_extended`. It is
2251/// what [`StepHeaders`] records for the member and what the enclosing
2252/// body threads down from the sample's root.
2253fn open_header(plan: &zerodds_ast_layout::plan::MemberPlan, chosen: bool) -> Vec<rust::Statement> {
2254    match plan.header {
2255        MemberHeader::NoHeader => Vec::new(),
2256        MemberHeader::PidCdr1 {
2257            pid,
2258            extended,
2259            must_understand,
2260        } => vec![bind_pid_header(
2261            chosen,
2262            (pid, plan.member_id),
2263            extended,
2264            "Member",
2265            must_understand,
2266        )],
2267        // Rule (19) and §7.4.1.2. The `Optional` role is what makes
2268        // the declared length follow the optional convention rather
2269        // than the `@mutable` one; the two differ in Cyclone.
2270        MemberHeader::OptionalCdr1 {
2271            pid,
2272            extended,
2273            must_understand,
2274        } => vec![bind_pid_header(
2275            chosen,
2276            (pid, plan.member_id),
2277            extended,
2278            "Optional",
2279            must_understand,
2280        )],
2281        // Rule (20), §7.4.3.5.2: a BOOLEAN `<is_present>`. Written by
2282        // `write_optional_member`, which is the only caller that reaches
2283        // this arm and knows whether the member is there.
2284        MemberHeader::OptionalFlag => Vec::new(),
2285        MemberHeader::EmHeader {
2286            lc,
2287            must_understand,
2288        } => {
2289            // The member id and the length code both come from the
2290            // plan: the id is the completion pass's, the code is the
2291            // policy's, and neither is decided again here.
2292            vec![bind_header(
2293                "emheader",
2294                "write_emheader",
2295                vec![
2296                    rust::Expr::Literal(Literal::int(i128::from(plan.member_id))),
2297                    rust::Expr::Literal(Literal::int(i128::from(lc as u8))),
2298                    rust::Expr::Literal(Literal::Bool(must_understand)),
2299                ],
2300            )]
2301        }
2302    }
2303}
2304
2305/// The `let pid = …` that opens a PL_CDR1 parameter header, in the form
2306/// `plan` names or in the one the sample's flag chooses.
2307///
2308/// Both forms call the same two runtime methods, so the branch is over
2309/// the *call* and not over anything else: `finish_pid` patches whichever
2310/// header was opened and reads its form back off the stream
2311/// (§7.4.1.2.3), and the id, the role and the flag are the plan's in
2312/// either arm. `chosen` with a plan that already names the extended form
2313/// cannot happen: [`StepHeaders`] records a member as chosen only where
2314/// the two sides of the step disagree, and the plan the codec is written
2315/// from is the short side.
2316fn bind_pid_header(
2317    chosen: bool,
2318    (pid, member_id): (u16, u32),
2319    extended: bool,
2320    role: &str,
2321    must_understand: bool,
2322) -> rust::Statement {
2323    // The short header carries the 16-bit parameter id; the extended one
2324    // the member id in 28 bits (XTypes 1.3 §7.4.1.2.2). The plan's `pid`
2325    // saturates at `u16::MAX` for a member id past 16 bits — the extended
2326    // form is then mandatory and takes `member_id`, or a `@hashid` member
2327    // goes out as member 65535.
2328    let writer = |method: &str| {
2329        let id = if method == "write_pid_extended" {
2330            i128::from(member_id)
2331        } else {
2332            i128::from(pid)
2333        };
2334        rust::Expr::Try(Box::new(rust::Expr::MethodCall {
2335            receiver: Box::new(rust::Expr::Path(Path::single("w"))),
2336            method: Ident::new(method),
2337            args: vec![
2338                rust::Expr::Literal(Literal::int(id)),
2339                parameter_role(role),
2340                rust::Expr::Literal(Literal::Bool(must_understand)),
2341            ],
2342        }))
2343    };
2344    let value = if !chosen {
2345        writer(if extended {
2346            "write_pid_extended"
2347        } else {
2348            "write_pid"
2349        })
2350    } else {
2351        // `zd_extended` is the sample's answer, resolved once by
2352        // [`sample_past_step`] in the entry that called this body and
2353        // threaded down to here.
2354        rust::Expr::If {
2355            condition: Box::new(rust::Expr::Path(Path::single(EXTENDED_PARAM))),
2356            then: rust::Block {
2357                statements: vec![rust::Statement::Tail(writer("write_pid_extended"))],
2358            },
2359            otherwise: Some(rust::Block {
2360                statements: vec![rust::Statement::Tail(writer("write_pid"))],
2361            }),
2362        }
2363    };
2364    rust::Statement::Let {
2365        name: Ident::new("pid"),
2366        type_: None,
2367        value: Some(value),
2368        mutable: false,
2369    }
2370}
2371
2372/// The statements that close one member's framing, after its value.
2373fn close_header(plan: &zerodds_ast_layout::plan::MemberPlan) -> Vec<rust::Statement> {
2374    match plan.header {
2375        MemberHeader::NoHeader | MemberHeader::OptionalFlag => Vec::new(),
2376        MemberHeader::PidCdr1 { .. } | MemberHeader::OptionalCdr1 { .. } => vec![call_statement(
2377            "finish_pid",
2378            vec![rust::Expr::Path(Path::single("pid"))],
2379        )],
2380        // `?`: under a length code of 0 to 3 the code is the member's
2381        // length, and `finish_emheader` refuses a value of another size
2382        // rather than let the header misdeclare it (§7.4.3.4.2).
2383        MemberHeader::EmHeader { .. } => vec![try_call_statement(
2384            "finish_emheader",
2385            vec![rust::Expr::Path(Path::single("emheader"))],
2386        )],
2387    }
2388}
2389
2390/// One `@optional` member's write, over the `Option<T>` field.
2391///
2392/// Three shapes, one per way the specification marks the absence, and
2393/// the *field* is what says which one this sample wants:
2394///
2395/// - rule (20), XCDR2 outside a `@mutable` type: a BOOLEAN
2396///   `<is_present>` either way, and the value only when it is `true`;
2397/// - rule (19), XCDR1 outside a `@mutable` type: the parameter header
2398///   either way, with a declared length of zero for the absence
2399///   (§7.4.1.2), which is what `finish_pid` computes from the bytes
2400///   that follow;
2401/// - inside a `@mutable` type: no parameter at all. §7.4.1.2.3 —
2402///   a member the stream does not carry is one the reader defaults, and
2403///   for an `@optional` member the default is the absence itself.
2404///
2405/// The match is over `&self.<name>`, so the binding is a borrow and the
2406/// value goes to the runtime the same way a borrowed field does.
2407///
2408/// `chosen` is [`open_header`]'s, passed on: an `@optional` member of a
2409/// `@mutable` type is framed as an `MMEMBER` and takes the same run-time
2410/// branch as every other member of the type.
2411fn write_optional_member(
2412    receiver: rust::Expr,
2413    plan: &zerodds_ast_layout::plan::MemberPlan,
2414    name: &str,
2415    kind: &MemberWrite,
2416    chosen: bool,
2417) -> Result<Vec<rust::Statement>, EncodeError> {
2418    let bound = rust::escape_keyword(name);
2419    let value = rust::Expr::Path(Path::single(&bound));
2420
2421    let (mut present, absent) = match plan.header {
2422        MemberHeader::OptionalFlag => (
2423            vec![call_statement(
2424                "write_bool",
2425                vec![rust::Expr::Literal(Literal::Bool(true))],
2426            )],
2427            vec![call_statement(
2428                "write_bool",
2429                vec![rust::Expr::Literal(Literal::Bool(false))],
2430            )],
2431        ),
2432        // The header is outside the match: it is written whether or not
2433        // the value is, and its declared length is the marker.
2434        MemberHeader::OptionalCdr1 { .. } => (Vec::new(), Vec::new()),
2435        // A parameter list simply omits the parameter, so the header
2436        // goes inside the arm with the value.
2437        _ => (open_header(plan, chosen), Vec::new()),
2438    };
2439    present.push(write_value_to("w", name, kind, value, true)?);
2440    if !matches!(plan.header, MemberHeader::OptionalCdr1 { .. }) {
2441        present.extend(close_header(plan));
2442    }
2443
2444    let matched = rust::Statement::Semi(rust::Expr::Match {
2445        scrutinee: Box::new(rust::Expr::Ref {
2446            target: Box::new(rust::Expr::Field {
2447                receiver: Box::new(receiver),
2448                name: Ident::new(&bound),
2449            }),
2450            mutable: false,
2451        }),
2452        arms: vec![
2453            rust::MatchArm {
2454                pattern: rust::Pattern::TupleStruct {
2455                    path: Path::single("Some"),
2456                    elements: vec![rust::Pattern::Binding(Ident::new(&bound))],
2457                },
2458                guard: None,
2459                body: rust::Expr::Block(rust::Block {
2460                    statements: present,
2461                }),
2462            },
2463            rust::MatchArm {
2464                pattern: rust::Pattern::Path(Path::single("None")),
2465                guard: None,
2466                body: rust::Expr::Block(rust::Block { statements: absent }),
2467            },
2468        ],
2469    });
2470
2471    if matches!(plan.header, MemberHeader::OptionalCdr1 { .. }) {
2472        let mut all = open_header(plan, chosen);
2473        all.push(matched);
2474        all.extend(close_header(plan));
2475        return Ok(all);
2476    }
2477    Ok(vec![matched])
2478}
2479
2480/// `fn key_holder_be(&self) -> Result<Vec<u8>, WireError>`,
2481/// `fn key_from_holder_be(r: &mut Reader) -> Result<Self, WireError>` and
2482/// `fn key_hash(&self) -> Result<[u8; 16], WireError>`, for a keyed type.
2483///
2484/// The two are one function apart — §7.6.8 steps 1 to 4 build the
2485/// holder, step 5 turns it into the hash — and they are emitted together
2486/// so that no caller can reach the hash through a holder written by
2487/// anything but this plan.
2488///
2489/// Empty for a type with no `@key` member: a KeyHash over nothing is
2490/// not a KeyHash, and emitting one would give every instance of that
2491/// type the same handle. Settled in favour of this position after
2492/// `wireprobe-zerodds --emit keyhash` was found answering a keyless type
2493/// with sixteen zero bytes, or with the MD5 of nothing when the type was
2494/// `@mutable`. Cyclone, RTI and Fast DDS all decide on "no keys" before
2495/// any hash is computed and produce none;
2496/// `crates/wire-probe/src/encode.rs::key_hash` carries the measurement
2497/// and now refuses as well.
2498///
2499/// The holder is always big-endian — DDSI-RTPS 2.5 §9.6.3.8 — and that
2500/// fact comes from `key_holder_plan_with`, not from a second reading of
2501/// the model here.
2502///
2503/// What the holder *contains* and in what order does take the dialect.
2504/// This column used to plan the holder under `LayoutKey::key`, whose
2505/// comment said "§7.6.8 leaves no room for a dialect"; three of the
2506/// holder's rules are measured divergences and the other three columns
2507/// have selected them for a while — `KeyHolderExtensibility` (RTI keeps
2508/// the type's own kind under XCDR1), `KeyHolderNesting` (RTI stops
2509/// filtering below a keyless aggregate under XCDR1) and
2510/// `KeyHolderOrder` (RTI under XCDR1 keeps declaration order).
2511/// A column that ignored all three answered `--dialect rti` with
2512/// Cyclone's instance handle.
2513fn key_holder_fns(
2514    ast: &Ast,
2515    type_: Id<TypeNode>,
2516    data_key: LayoutKey,
2517) -> Result<Vec<rust::Item>, EncodeError> {
2518    // The holder's own key, not the data key: big-endian and the key
2519    // purpose, both of which §7.6.8 fixes, over the dialect the data
2520    // plan was built with.
2521    let key = LayoutKey {
2522        purpose: zerodds_ast_layout::Purpose::Key,
2523        endianness: zerodds_ast_layout::Endianness::Big,
2524        ..data_key
2525    };
2526    let keys = zerodds_ast_layout::keyhash::key_members_with(
2527        ast,
2528        type_,
2529        key.key_holder_nesting,
2530        key.key_holder_order,
2531    )
2532    .map_err(|error| EncodeError::NoLayout {
2533        detail: alloc_format(&error),
2534    })?;
2535    if keys.is_empty() {
2536        return Ok(Vec::new());
2537    }
2538
2539    let form =
2540        zerodds_ast_layout::keyhash::key_hash_form_for(ast, type_, key).map_err(|error| {
2541            EncodeError::NoLayout {
2542                detail: alloc_format(&error),
2543            }
2544        })?;
2545
2546    let mut statements = vec![rust::Statement::Let {
2547        name: Ident::new("holder"),
2548        type_: None,
2549        // An associated function, not a method: the constructor takes
2550        // both the order and the alignment cap, and `Writer.with_…()` is
2551        // not Rust.
2552        //
2553        // The cap is four and not the `Writer`'s own default of eight.
2554        // XTypes 1.3 §7.6.8 step 4 serialises the holder as PLAIN_CDR2,
2555        // whose `MAXALIGN` §7.4.1 gives as four, whatever version the
2556        // sample itself goes out in. `Writer::big_endian()` stood here
2557        // and left the cap at eight, so a holder whose plan put a
2558        // `@key double` at offset four had it written at offset eight —
2559        // a KeyHash that disagrees with the plan that sized it, and with
2560        // every peer, for every keyed type with an eight-octet key
2561        // member behind a four-octet one. Nothing reported it: a
2562        // KeyHash is sixteen opaque octets and a wrong one only shows up
2563        // as an instance that never matches.
2564        //
2565        // The order is written as an absolute path rather than through
2566        // an import, because the generated file's import list is built
2567        // by each consumer of this emitter and a new name in it would
2568        // have to be added in each one.
2569        value: Some(rust::Expr::Call {
2570            callee: Box::new(rust::Expr::Path(Path::new(&["Writer", "with_max_align"]))),
2571            args: vec![
2572                rust::Expr::Path(Path::new(&["zerodds_wire", "Endianness", "Big"])),
2573                // PLAIN_CDR2's four, unless the dialect aligns the holder
2574                // to its data encoding or packs it (`KeyHolderAlignment`).
2575                rust::Expr::Literal(Literal::int(i128::from(
2576                    key.key_holder_alignment.max_alignment(key.encoding),
2577                ))),
2578            ],
2579        }),
2580        mutable: true,
2581    }];
2582
2583    // A nested key member is written through the **bare** codec, not
2584    // through a versioned one: XTypes 1.3 §7.6.8 step 4 serialises the
2585    // holder as PLAIN_CDR2 whatever version the sample goes out in, so
2586    // there is no XCDR1 key holder for an XCDR1 codec to call.
2587    let codec = "";
2588
2589    // Only the key members, in the order `key_members_with` gives —
2590    // which is the dialect's holder order, and the same order
2591    // `plan_aggregate` laid the holder out in.
2592    for node in &keys {
2593        let Some((name, kind)) = member_of(ast, type_, *node, key, codec) else {
2594            continue;
2595        };
2596        statements.push(write_value_to_sink(
2597            "holder",
2598            SinkShape::Owned,
2599            &name,
2600            &kind,
2601            rust::Expr::Field {
2602                receiver: Box::new(holder_of(ast, type_, *node)),
2603                name: Ident::new(&rust::escape_keyword(&name)),
2604            },
2605            // A key member is never `@optional` — §7.2.2.4.4.4.8, and
2606            // `idl-lower::validate::member_rules` refuses the pair — so
2607            // the field here is the member's own type and not an
2608            // `Option` of it.
2609            false,
2610        )?);
2611    }
2612
2613    // The octets of the holder itself, before step 5 turns them into a
2614    // hash. A `DdsType` impl needs exactly these: the runtime's
2615    // instance path writes the holder into a `PlainCdr2BeKeyHolder` and
2616    // decides the branch of step 5 from the type's declared maximum, so
2617    // handing it a finished hash would be handing it the wrong thing.
2618    let mut holder_statements = statements.clone();
2619    holder_statements.push(rust::Statement::Tail(rust::Expr::Call {
2620        callee: Box::new(rust::Expr::Path(Path::single("Ok"))),
2621        args: vec![rust::Expr::MethodCall {
2622            receiver: Box::new(rust::Expr::Path(Path::single("holder"))),
2623            method: Ident::new("finish"),
2624            args: Vec::new(),
2625        }],
2626    }));
2627    let key_holder_be = rust::Item::Function(rust::Function {
2628        doc: Doc::none(),
2629        attrs: Vec::new(),
2630        visibility: Visibility::Public,
2631        name: Ident::new("key_holder_be"),
2632        generics: Vec::new(),
2633        params: vec![rust::Param {
2634            name: None,
2635            type_: rust::Type::unit(),
2636            receiver: Some(rust::Receiver::Ref),
2637        }],
2638        // Fallible for the same reason `key_hash` is.
2639        returns: Some(rust::Type::generic(
2640            Path::single("Result"),
2641            vec![
2642                rust::Type::generic(
2643                    Path::single("Vec"),
2644                    vec![rust::Type::Primitive(rust::Primitive::U8)],
2645                ),
2646                rust::Type::named(Path::single("WireError")),
2647            ],
2648        )),
2649        body: Some(rust::Block {
2650            statements: holder_statements,
2651        }),
2652    });
2653
2654    // The inverse of `key_holder_be`: the same key members, read in the
2655    // same order the same plan wrote them.
2656    //
2657    // XTypes 1.3 §7.6.8 builds a holder that is not the type's own
2658    // stream — big-endian, `MAXALIGN` four, only the key members, no
2659    // DHEADER and no member headers — so `decode` cannot read one, and a
2660    // type that only had `encode_key_holder_be` could serialise an
2661    // instance's key and never read it back. DDS 1.4 §2.2.2.5.1.13 needs
2662    // exactly that read: on a dispose/unregister sample
2663    // `SampleInfo.valid_data` is `false` and "only the fields that form
2664    // the key" are meaningful, and those fields have to come from
2665    // somewhere.
2666    //
2667    // The members outside the holder are not in the stream at all and
2668    // take their §7.3.1.2.1.10 defaults through `Default::default()` —
2669    // the same values `default_impl` emits, so the two cannot drift.
2670    let mut from_holder = Vec::new();
2671    let mut key_fields = Vec::new();
2672    // A key member this emitter cannot describe is not skipped here. The
2673    // writer above skips it — a judged silent drop, allowlisted in
2674    // `tools/silent-drop-guard.sh` — but a *reader* that skipped one would
2675    // read the following member off the octets of the one it left out and
2676    // hand back a key that is wrong rather than absent. The whole reader
2677    // refuses instead, and says which member it could not read.
2678    let mut undescribed = None;
2679    for node in &keys {
2680        match member_of(ast, type_, *node, key, codec) {
2681            Some((name, kind)) => {
2682                let field = rust::escape_keyword(&name);
2683                from_holder.push(let_statement(
2684                    &field,
2685                    rust::Expr::Try(Box::new(read_for(&name, &kind)?)),
2686                ));
2687                key_fields.push((
2688                    *node,
2689                    (Ident::new(&field), rust::Expr::Path(Path::single(&field))),
2690                ));
2691            }
2692            None => undescribed = Some(*node),
2693        }
2694    }
2695    if let Some(node) = undescribed {
2696        from_holder = vec![rust::Statement::Tail(refused_err(
2697            "r",
2698            &format!("a key holder with a member this emitter cannot read ({node:?})"),
2699        ))];
2700    } else {
2701        from_holder.push(rust::Statement::Tail(rust::Expr::Call {
2702            callee: Box::new(rust::Expr::Path(Path::single("Ok"))),
2703            args: vec![nested_struct_literal(ast, type_, key_fields, &|| {
2704                Some(Box::new(rust::Expr::Call {
2705                    callee: Box::new(rust::Expr::Path(Path::new(&["Default", "default"]))),
2706                    args: Vec::new(),
2707                }))
2708            })],
2709        }));
2710    }
2711    let key_from_holder_be = rust::Item::Function(rust::Function {
2712        doc: Doc::none(),
2713        attrs: Vec::new(),
2714        visibility: Visibility::Public,
2715        name: Ident::new("key_from_holder_be"),
2716        generics: Vec::new(),
2717        params: vec![rust::Param {
2718            name: Some(Ident::new("r")),
2719            type_: rust::Type::Ref {
2720                target: Box::new(rust::Type::named(Path::single("Reader"))),
2721                mutable: true,
2722                lifetime: None,
2723            },
2724            receiver: None,
2725        }],
2726        returns: Some(rust::Type::generic(
2727            Path::single("Result"),
2728            vec![
2729                rust::Type::named(Path::single("Self")),
2730                rust::Type::named(Path::single("WireError")),
2731            ],
2732        )),
2733        body: Some(rust::Block {
2734            statements: from_holder,
2735        }),
2736    });
2737
2738    // A declared maximum of sixteen bytes or fewer: zero-pad the holder.
2739    // A larger or unbounded maximum: MD5. Variable length alone does not
2740    // select MD5; the shared maximum-size calculation decides.
2741    let finish = match form {
2742        zerodds_ast_layout::keyhash::KeyHashForm::ZeroPadded => "zero_padded",
2743        zerodds_ast_layout::keyhash::KeyHashForm::Md5 => "md5",
2744    };
2745    statements.push(rust::Statement::Tail(rust::Expr::Call {
2746        callee: Box::new(rust::Expr::Path(Path::single("Ok"))),
2747        args: vec![rust::Expr::MethodCall {
2748            receiver: Box::new(rust::Expr::Path(Path::single("holder"))),
2749            method: Ident::new(finish),
2750            args: Vec::new(),
2751        }],
2752    }));
2753
2754    Ok(vec![
2755        key_holder_be,
2756        key_from_holder_be,
2757        rust::Item::Function(rust::Function {
2758            doc: Doc::none(),
2759            attrs: Vec::new(),
2760            visibility: Visibility::Public,
2761            name: Ident::new("key_hash"),
2762            generics: Vec::new(),
2763            params: vec![rust::Param {
2764                name: None,
2765                type_: rust::Type::unit(),
2766                receiver: Some(rust::Receiver::Ref),
2767            }],
2768            // Fallible for the reason `encode` is: a `@key char` refuses
2769            // a code point above U+00FF, and a KeyHash that swallowed
2770            // the refusal would hash a character nobody wrote.
2771            returns: Some(rust::Type::generic(
2772                Path::single("Result"),
2773                vec![
2774                    rust::Type::Array {
2775                        element: Box::new(rust::Type::Primitive(rust::Primitive::U8)),
2776                        length: Box::new(rust::Expr::Literal(Literal::int(16))),
2777                    },
2778                    rust::Type::named(Path::single("WireError")),
2779                ],
2780            )),
2781            body: Some(rust::Block { statements }),
2782        }),
2783    ])
2784}
2785
2786/// `fn decode(r: &mut Reader) -> Self`.
2787///
2788/// The mirror of the encoder, member for member and header for header,
2789/// built from the same plan — which is what makes the two agree by
2790/// construction rather than by review.
2791fn decode_fn(
2792    ast: &Ast,
2793    type_: Id<TypeNode>,
2794    layout: &TypeLayout,
2795    key: LayoutKey,
2796    codec: &'static str,
2797) -> Result<rust::Item, EncodeError> {
2798    let statements = if ast.effective_extensibility(type_) == zerodds_ast::Extensibility::Mutable {
2799        decode_by_member_id(ast, type_, layout, key, codec)?
2800    } else {
2801        decode_in_plan_order(ast, type_, layout, key, codec)?
2802    };
2803    // The read side of `encode_fns`'s block: an aggregate value's members
2804    // align from its own first octet under `ElementPadding::Packed`.
2805    let statements = wrap_packed_body(
2806        statements,
2807        "r",
2808        key.element_padding == ElementPadding::Packed,
2809    );
2810
2811    Ok(rust::Item::Function(rust::Function {
2812        doc: Doc::none(),
2813        attrs: Vec::new(),
2814        visibility: Visibility::Public,
2815        name: Ident::new(&format!("decode{codec}")),
2816        generics: Vec::new(),
2817        params: vec![rust::Param {
2818            name: Some(Ident::new("r")),
2819            type_: rust::Type::Ref {
2820                target: Box::new(rust::Type::named(Path::single("Reader"))),
2821                mutable: true,
2822                lifetime: None,
2823            },
2824            receiver: None,
2825        }],
2826        returns: Some(rust::Type::generic(
2827            Path::single("Result"),
2828            vec![
2829                rust::Type::named(Path::single("Self")),
2830                rust::Type::named(Path::single("WireError")),
2831            ],
2832        )),
2833        body: Some(rust::Block { statements }),
2834    }))
2835}
2836
2837/// A `@final` or `@appendable` aggregate: the members where the plan
2838/// says, because that is where the writer put them.
2839///
2840/// §7.4.1.2.3 makes only a `@mutable` list unordered. A positional
2841/// aggregate that read by id would be inventing a framing its own
2842/// encoder does not write.
2843fn decode_in_plan_order(
2844    ast: &Ast,
2845    type_: Id<TypeNode>,
2846    layout: &TypeLayout,
2847    key: LayoutKey,
2848    codec: &'static str,
2849) -> Result<Vec<rust::Statement>, EncodeError> {
2850    // A writer with fewer members than this reader stops early, and
2851    // §7.2.4.4.8, Table 19, says "each non-optional member in a T1
2852    // object that is not present in the T2 object takes its default
2853    // value". Only an `@appendable` type may meet one: Table 19 requires
2854    // two final types to have the same set of member IDs.
2855    let appendable = ast.effective_extensibility(type_) == zerodds_ast::Extensibility::Appendable;
2856    let mut statements = Vec::new();
2857    if layout.needs_dheader {
2858        // The DHEADER is what says where the writer's members stopped —
2859        // "skipping the members an older reader does not know is what
2860        // the DHEADER is for". `end` is that offset; a type that cannot
2861        // meet a shorter writer still reads it, to move the cursor.
2862        if appendable {
2863            statements.push(let_statement(
2864                "end",
2865                rust::Expr::Binary {
2866                    op: rust::BinaryOp::Add,
2867                    lhs: Box::new(rust::Expr::Try(Box::new(call("read_dheader", Vec::new())))),
2868                    rhs: Box::new(call("position", Vec::new())),
2869                },
2870            ));
2871        } else {
2872            statements.push(let_statement(
2873                "_dheader",
2874                rust::Expr::Try(Box::new(call("read_dheader", Vec::new()))),
2875            ));
2876        }
2877    }
2878
2879    let mut fields = Vec::new();
2880    for plan in &layout.members {
2881        let Some((name, kind)) = member_of(ast, type_, plan.member, key, codec) else {
2882            continue;
2883        };
2884        // The appendable tail. Under encoding version 2 the delimiter
2885        // says where the writer stopped; under version 1 §7.2.4.2 leaves
2886        // an appendable type undelimited and the payload's end is what
2887        // all three vendors use instead — Cyclone, RTI and Fast DDS each
2888        // default the tail of a short XCDR1 stream, measured 2026-08-09
2889        // as `evo-appendable-tail`.
2890        if appendable && matches!(plan.header, MemberHeader::NoHeader) {
2891            let Some(member) = member_node_of(ast, type_, plan.member) else {
2892                continue;
2893            };
2894            let Some(member_type) = member.type_.resolved() else {
2895                continue;
2896            };
2897            let reached = if layout.needs_dheader {
2898                rust::Expr::Binary {
2899                    op: rust::BinaryOp::Ge,
2900                    lhs: Box::new(call("position", Vec::new())),
2901                    rhs: Box::new(rust::Expr::Path(Path::single("end"))),
2902                }
2903            } else {
2904                rust::Expr::Binary {
2905                    op: rust::BinaryOp::Eq,
2906                    lhs: Box::new(call("remaining", Vec::new())),
2907                    rhs: Box::new(rust::Expr::Literal(Literal::int(0))),
2908                }
2909            };
2910            statements.push(let_statement(
2911                &name,
2912                rust::Expr::If {
2913                    condition: Box::new(reached),
2914                    then: rust::Block {
2915                        statements: vec![rust::Statement::Tail(
2916                            crate::default_rust::member_default(
2917                                ast,
2918                                module_of(ast, type_),
2919                                member,
2920                                member_type,
2921                            )?,
2922                        )],
2923                    },
2924                    otherwise: Some(rust::Block {
2925                        statements: vec![rust::Statement::Tail(rust::Expr::Try(Box::new(
2926                            read_for(&name, &kind)?,
2927                        )))],
2928                    }),
2929                },
2930            ));
2931            fields.push((plan.member, field_binding(&name)));
2932            continue;
2933        }
2934        // An `@optional` member reads into an `Option<T>`, so the marker
2935        // the specification puts on the wire decides between `Some` and
2936        // `None` instead of between a value and a refusal.
2937        if member_node_of(ast, type_, plan.member).is_some_and(|member| member.facets.is_optional())
2938        {
2939            statements.extend(read_optional_member(plan, &name, &kind)?);
2940            fields.push((plan.member, field_binding(&name)));
2941            continue;
2942        }
2943        match plan.header {
2944            MemberHeader::NoHeader | MemberHeader::OptionalFlag => {}
2945            MemberHeader::PidCdr1 { .. } | MemberHeader::OptionalCdr1 { .. } => {
2946                statements.push(let_statement(
2947                    "pid",
2948                    rust::Expr::Try(Box::new(call("read_pid", Vec::new()))),
2949                ));
2950            }
2951            MemberHeader::EmHeader { .. } => {
2952                statements.push(let_statement(
2953                    "emheader",
2954                    rust::Expr::Try(Box::new(call("read_emheader", Vec::new()))),
2955                ));
2956            }
2957        }
2958        // Every read is fallible, so every read is followed by `?`. A
2959        // decoder that could not fail would have to guess at bytes that
2960        // are not there, and a guess is a value nobody sent.
2961        statements.push(let_statement(
2962            &name,
2963            rust::Expr::Try(Box::new(read_for(&name, &kind)?)),
2964        ));
2965        // The mirror of the encoder's patch step. `Reader::read_pid`
2966        // makes the parameter's value its own alignment origin — §7.4.1.2
2967        // calls each parameter "a CDR-encapsulated block of data" — and
2968        // `finish_pid` is the only thing that puts the origin back and
2969        // steps over the padding to the next four-byte boundary.
2970        //
2971        // Without it the origin stayed at the last parameter's value
2972        // start for the rest of the aggregate. A parameter whose value
2973        // begins at an offset that is 4 modulo 8 then aligns an
2974        // eight-byte member after it four bytes late:
2975        // `@final struct S { @optional octet b; double c; };` writes `c`
2976        // at payload offset 8 and read it at 12. It also skipped
2977        // `check_declared_length`, so a peer's length that disagreed
2978        // with the value that followed it was accepted in silence.
2979        match plan.header {
2980            MemberHeader::NoHeader | MemberHeader::OptionalFlag | MemberHeader::EmHeader { .. } => {
2981            }
2982            MemberHeader::PidCdr1 { .. } | MemberHeader::OptionalCdr1 { .. } => {
2983                statements.push(rust::Statement::Semi(rust::Expr::Try(Box::new(call(
2984                    "finish_pid",
2985                    vec![rust::Expr::Path(Path::single("pid"))],
2986                )))));
2987            }
2988        }
2989        fields.push((plan.member, field_binding(&name)));
2990    }
2991
2992    // Rule (23): a PL_CDR1 parameter list ends at `PID_SENTINEL`, not at
2993    // the last member the reader recognises. The encoder writes one and
2994    // the decoder left it in the stream, which puts every byte after the
2995    // aggregate four positions early for any caller that reads on.
2996    if layout.needs_sentinel {
2997        statements.extend(read_sentinel());
2998    }
2999
3000    statements.push(rust::Statement::Tail(rust::Expr::Call {
3001        callee: Box::new(rust::Expr::Path(Path::single("Ok"))),
3002        args: vec![nested_struct_literal(ast, type_, fields, &|| None)],
3003    }));
3004
3005    Ok(statements)
3006}
3007
3008/// A `@mutable` aggregate: the members its headers name, in the order
3009/// the stream names them.
3010///
3011/// XTypes 1.3 §7.4.1.2.3 — "any mutable structure member's value may be
3012/// omitted" — and §7.4.3.4.2, which gives every member an EMHEADER
3013/// carrying its id. A `@mutable` list is unordered by construction and
3014/// the ids are what orders it; this column read it in *plan* order
3015/// instead, so a member the stream omitted took the next member's bytes
3016/// and a member the peer reordered was read as the wrong member. Both
3017/// produce a sample and neither reports anything. The reflective, C and
3018/// Python columns have read by id since the evolution run.
3019///
3020/// Two loops, one per encoding version, because the two have different
3021/// ends: rule (21) bounds the list with the DHEADER's extent and rule
3022/// (23) with `PID_SENTINEL`.
3023///
3024/// A member the stream did not carry takes its default afterwards —
3025/// §7.4.1.2.3, "if the receiver's member is not optional, it logically
3026/// takes its default value" — and an `@optional` one takes the absence,
3027/// which is what its slot already holds.
3028fn decode_by_member_id(
3029    ast: &Ast,
3030    type_: Id<TypeNode>,
3031    layout: &TypeLayout,
3032    key: LayoutKey,
3033    codec: &'static str,
3034) -> Result<Vec<rust::Statement>, EncodeError> {
3035    let mut slots = Vec::new();
3036    for plan in &layout.members {
3037        let Some((name, kind)) = member_of(ast, type_, plan.member, key, codec) else {
3038            continue;
3039        };
3040        slots.push(MutableSlot {
3041            name,
3042            kind,
3043            member_id: plan.member_id,
3044        });
3045    }
3046
3047    let mut statements = Vec::new();
3048    // Rule (21): under encoding version 2 the DHEADER is what says where
3049    // the list stops. Under version 1 rule (23) uses `PID_SENTINEL`
3050    // instead and there is no delimiter to read.
3051    if layout.needs_dheader {
3052        statements.push(let_statement(
3053            "end",
3054            rust::Expr::Binary {
3055                op: rust::BinaryOp::Add,
3056                lhs: Box::new(rust::Expr::Try(Box::new(call("read_dheader", Vec::new())))),
3057                rhs: Box::new(call("position", Vec::new())),
3058            },
3059        ));
3060    }
3061    // One slot per member, empty until a header names it. `Option` is
3062    // also the "seen" flag: a second header for the same id is a stream
3063    // that says two things about one member, and §7.4.3.4.2 gives each
3064    // one header.
3065    for slot in &slots {
3066        statements.push(rust::Statement::Let {
3067            name: Ident::new(&rust::escape_keyword(&slot.name)),
3068            type_: None,
3069            value: Some(rust::Expr::Path(Path::single("None"))),
3070            mutable: true,
3071        });
3072    }
3073
3074    if key.encoding.is_v1() {
3075        statements.extend(parameter_list_loop(&slots)?);
3076    } else {
3077        statements.extend(member_header_loop(&slots)?);
3078    }
3079
3080    // The members no header named. Written after the loop and not
3081    // inside it, because "did not arrive" is only knowable once the
3082    // list has ended.
3083    let mut fields = Vec::new();
3084    for plan in &layout.members {
3085        let Some((name, _)) = member_of(ast, type_, plan.member, key, codec) else {
3086            continue;
3087        };
3088        let bound = rust::escape_keyword(&name);
3089        let optional = member_node_of(ast, type_, plan.member)
3090            .is_some_and(|member| member.facets.is_optional());
3091        if !optional {
3092            let Some(member) = member_node_of(ast, type_, plan.member) else {
3093                continue;
3094            };
3095            let Some(member_type) = member.type_.resolved() else {
3096                continue;
3097            };
3098            statements.push(let_statement(
3099                &name,
3100                or_default(
3101                    &bound,
3102                    crate::default_rust::member_default(
3103                        ast,
3104                        module_of(ast, type_),
3105                        member,
3106                        member_type,
3107                    )?,
3108                ),
3109            ));
3110        }
3111        fields.push((
3112            plan.member,
3113            (Ident::new(&bound), rust::Expr::Path(Path::single(&bound))),
3114        ));
3115    }
3116
3117    statements.push(rust::Statement::Tail(rust::Expr::Call {
3118        callee: Box::new(rust::Expr::Path(Path::single("Ok"))),
3119        args: vec![nested_struct_literal(ast, type_, fields, &|| None)],
3120    }));
3121    Ok(statements)
3122}
3123
3124/// The value of a member no header named: `bound` if one did, its
3125/// default if not.
3126///
3127/// Spelled the way clippy asks for, so a crate including the generated
3128/// text needs no allowance: `unwrap_or_default()` where Table 9's value
3129/// is also Rust's `Default` (a zero, `false`, `'\0'`, an empty `String`,
3130/// `Vec` or `BTreeMap`), `unwrap_or(value)` where the value is a constant
3131/// expression, and `unwrap_or_else(|| value)` where building it calls
3132/// something. A `match` with a `None` arm was all three, and
3133/// `manual_unwrap_or{,_default}` fired once per member.
3134fn or_default(bound: &str, value: rust::Expr) -> rust::Expr {
3135    let receiver = Box::new(rust::Expr::Path(Path::single(bound)));
3136    if is_rust_default(&value) {
3137        return rust::Expr::MethodCall {
3138            receiver,
3139            method: Ident::new("unwrap_or_default"),
3140            args: Vec::new(),
3141        };
3142    }
3143    if is_constant(&value) {
3144        return rust::Expr::MethodCall {
3145            receiver,
3146            method: Ident::new("unwrap_or"),
3147            args: vec![value],
3148        };
3149    }
3150    rust::Expr::MethodCall {
3151        receiver,
3152        method: Ident::new("unwrap_or_else"),
3153        args: vec![rust::Expr::Closure {
3154            params: Vec::new(),
3155            body: Box::new(value),
3156        }],
3157    }
3158}
3159
3160/// Whether `value` is what `Default::default()` gives for its type.
3161fn is_rust_default(value: &rust::Expr) -> bool {
3162    match value {
3163        rust::Expr::Literal(Literal::Int(text)) => text == "0",
3164        rust::Expr::Literal(Literal::Float(text)) => {
3165            matches!(text.as_str(), "0.0" | "0.0f32" | "0.0f64")
3166        }
3167        rust::Expr::Literal(Literal::Bool(flag)) => !flag,
3168        rust::Expr::Literal(Literal::Char(ch)) => *ch == '\0',
3169        rust::Expr::Call { callee, args } if args.is_empty() => {
3170            let rust::Expr::Path(path) = callee.as_ref() else {
3171                return false;
3172            };
3173            let names: Vec<&str> = path.segments().iter().map(Ident::as_str).collect();
3174            matches!(
3175                names.as_slice(),
3176                ["String", "new"] | ["Vec", "new"] | ["BTreeMap", "new"]
3177            )
3178        }
3179        _ => false,
3180    }
3181}
3182
3183/// Whether `value` is built without calling anything: literals, paths,
3184/// and aggregates of them.
3185///
3186/// zerodds-lint: recursion-depth = the default value's nesting, one level
3187/// per composite literal; `default_rust` bounds it at 64.
3188fn is_constant(value: &rust::Expr) -> bool {
3189    match value {
3190        rust::Expr::Literal(_) | rust::Expr::Path(_) => true,
3191        rust::Expr::Array(elements) | rust::Expr::Tuple(elements) => {
3192            elements.iter().all(is_constant)
3193        }
3194        rust::Expr::Repeat { value, .. } => is_constant(value),
3195        rust::Expr::StructLit { fields, base, .. } => {
3196            base.is_none() && fields.iter().all(|(_, field)| is_constant(field))
3197        }
3198        rust::Expr::Cast { value, .. } => is_constant(value),
3199        _ => false,
3200    }
3201}
3202
3203/// `return Err(WireError::refused(r.position(), "…"));`
3204fn refuse_statement(why: &str) -> rust::Statement {
3205    rust::Statement::Semi(rust::Expr::Return(Some(Box::new(rust::Expr::Call {
3206        callee: Box::new(rust::Expr::Path(Path::single("Err"))),
3207        args: vec![rust::Expr::Call {
3208            callee: Box::new(rust::Expr::Path(Path::new(&["WireError", "refused"]))),
3209            args: vec![
3210                call("position", Vec::new()),
3211                rust::Expr::Literal(Literal::string(why)),
3212            ],
3213        }],
3214    }))))
3215}
3216
3217/// `if <slot>.is_some() { return Err(…); }`
3218fn refuse_a_second_header(slot: &str, member_id: u32) -> rust::Statement {
3219    rust::Statement::Semi(rust::Expr::If {
3220        condition: Box::new(rust::Expr::MethodCall {
3221            receiver: Box::new(rust::Expr::Path(Path::single(slot))),
3222            method: Ident::new("is_some"),
3223            args: Vec::new(),
3224        }),
3225        then: rust::Block {
3226            statements: vec![refuse_statement(&format!(
3227                "one header per member, and member id {member_id} has two"
3228            ))],
3229        },
3230        otherwise: None,
3231    })
3232}
3233
3234/// Rule (21): EMHEADERs until the delimiter's extent is used up.
3235fn member_header_loop(slots: &[MutableSlot]) -> Result<Vec<rust::Statement>, EncodeError> {
3236    let mut body = vec![
3237        let_statement(
3238            "header",
3239            rust::Expr::Try(Box::new(call("read_emheader", Vec::new()))),
3240        ),
3241        // Where this member ends, whatever it turns out to be. Read
3242        // before the value, because after it the length code's own
3243        // rounding is no longer recoverable.
3244        let_statement(
3245            "ends",
3246            rust::Expr::Binary {
3247                op: rust::BinaryOp::Add,
3248                lhs: Box::new(call("position", Vec::new())),
3249                rhs: Box::new(rust::Expr::Field {
3250                    receiver: Box::new(rust::Expr::Path(Path::single("header"))),
3251                    name: Ident::new("extent"),
3252                }),
3253            },
3254        ),
3255    ];
3256
3257    // §7.2.2.4.4.4.6: a member this type does not declare is skipped
3258    // when the writer said it may be, and discards the whole sample
3259    // when it said it may not.
3260    let mut chain = Some(rust::Block {
3261        statements: vec![rust::Statement::Semi(rust::Expr::If {
3262            condition: Box::new(rust::Expr::Field {
3263                receiver: Box::new(rust::Expr::Path(Path::single("header"))),
3264                name: Ident::new("must_understand"),
3265            }),
3266            then: rust::Block {
3267                statements: vec![refuse_statement(
3268                    "a must-understand member this type does not declare, per §7.2.2.4.4.4.6",
3269                )],
3270            },
3271            otherwise: Some(rust::Block {
3272                statements: vec![rust::Statement::Semi(rust::Expr::Try(Box::new(call(
3273                    "skip_to",
3274                    vec![rust::Expr::Path(Path::single("ends"))],
3275                ))))],
3276            }),
3277        })],
3278    });
3279
3280    for slot in slots.iter().rev() {
3281        let bound = rust::escape_keyword(&slot.name);
3282        let mut arm = vec![refuse_a_second_header(&bound, slot.member_id)];
3283        arm.push(rust::Statement::Semi(rust::Expr::Assign {
3284            target: Box::new(rust::Expr::Path(Path::single(&bound))),
3285            value: Box::new(rust::Expr::Call {
3286                callee: Box::new(rust::Expr::Path(Path::single("Some"))),
3287                args: vec![rust::Expr::Try(Box::new(read_for(&slot.name, &slot.kind)?))],
3288            }),
3289        }));
3290        // The header said how long the member is; a value that did not
3291        // fill it means the two disagree, and reading on would take the
3292        // next header out of this member's bytes.
3293        arm.push(rust::Statement::Semi(rust::Expr::If {
3294            condition: Box::new(rust::Expr::Binary {
3295                op: rust::BinaryOp::Ne,
3296                lhs: Box::new(call("position", Vec::new())),
3297                rhs: Box::new(rust::Expr::Path(Path::single("ends"))),
3298            }),
3299            then: rust::Block {
3300                statements: vec![refuse_statement(
3301                    "a member filling the extent its EMHEADER declared",
3302                )],
3303            },
3304            otherwise: None,
3305        }));
3306        chain = Some(rust::Block {
3307            statements: vec![rust::Statement::Semi(rust::Expr::If {
3308                condition: Box::new(rust::Expr::Binary {
3309                    op: rust::BinaryOp::Eq,
3310                    lhs: Box::new(rust::Expr::Field {
3311                        receiver: Box::new(rust::Expr::Path(Path::single("header"))),
3312                        name: Ident::new("member_id"),
3313                    }),
3314                    rhs: Box::new(rust::Expr::Literal(Literal::int(i128::from(
3315                        slot.member_id,
3316                    )))),
3317                }),
3318                then: rust::Block { statements: arm },
3319                otherwise: chain,
3320            })],
3321        });
3322    }
3323    if let Some(chain) = chain {
3324        body.extend(chain.statements);
3325    }
3326
3327    Ok(vec![rust::Statement::Semi(rust::Expr::While {
3328        condition: Box::new(rust::Expr::Binary {
3329            op: rust::BinaryOp::Lt,
3330            lhs: Box::new(call("position", Vec::new())),
3331            rhs: Box::new(rust::Expr::Path(Path::single("end"))),
3332        }),
3333        body: rust::Block { statements: body },
3334    })])
3335}
3336
3337/// Rule (23): parameters until `PID_SENTINEL`.
3338fn parameter_list_loop(slots: &[MutableSlot]) -> Result<Vec<rust::Statement>, EncodeError> {
3339    // A sentinel is the only thing that ends the list, so the loop
3340    // condition is a flag the sentinel sets rather than a count.
3341    let mut statements = vec![rust::Statement::Let {
3342        name: Ident::new("ended"),
3343        type_: None,
3344        value: Some(rust::Expr::Literal(Literal::Bool(false))),
3345        mutable: true,
3346    }];
3347
3348    // Stepping over a parameter this type has no member for, in the
3349    // one place three different reasons reach it: an id the model does
3350    // not declare, a vendor-private parameter and one inside
3351    // §7.4.1.2.2's reserved range.
3352    let skip_parameter_block = rust::Block {
3353        statements: vec![rust::Statement::Semi(rust::Expr::Try(Box::new(call(
3354            "skip_parameter",
3355            vec![rust::Expr::Path(Path::single("pid"))],
3356        ))))],
3357    };
3358    let unknown = rust::Block {
3359        statements: vec![rust::Statement::Semi(rust::Expr::If {
3360            condition: Box::new(rust::Expr::Field {
3361                receiver: Box::new(rust::Expr::Path(Path::single("pid"))),
3362                name: Ident::new("must_understand"),
3363            }),
3364            then: rust::Block {
3365                statements: vec![refuse_statement(
3366                    "a must-understand parameter this type does not declare, per §7.2.2.4.4.4.6",
3367                )],
3368            },
3369            otherwise: Some(skip_parameter_block.clone()),
3370        })],
3371    };
3372    let mut chain = Some(unknown.clone());
3373
3374    for slot in slots.iter().rev() {
3375        let bound = rust::escape_keyword(&slot.name);
3376        let mut arm = vec![refuse_a_second_header(&bound, slot.member_id)];
3377        arm.push(rust::Statement::Semi(rust::Expr::Assign {
3378            target: Box::new(rust::Expr::Path(Path::single(&bound))),
3379            value: Box::new(rust::Expr::Call {
3380                callee: Box::new(rust::Expr::Path(Path::single("Some"))),
3381                args: vec![rust::Expr::Try(Box::new(read_for(&slot.name, &slot.kind)?))],
3382            }),
3383        }));
3384        arm.push(rust::Statement::Semi(rust::Expr::Try(Box::new(call(
3385            "finish_pid",
3386            vec![rust::Expr::Path(Path::single("pid"))],
3387        )))));
3388        chain = Some(rust::Block {
3389            statements: vec![rust::Statement::Semi(rust::Expr::If {
3390                condition: Box::new(rust::Expr::Binary {
3391                    op: rust::BinaryOp::Eq,
3392                    lhs: Box::new(rust::Expr::Field {
3393                        receiver: Box::new(rust::Expr::Path(Path::single("pid"))),
3394                        name: Ident::new("pid"),
3395                    }),
3396                    rhs: Box::new(rust::Expr::Literal(Literal::int(i128::from(
3397                        slot.member_id,
3398                    )))),
3399                }),
3400                then: rust::Block { statements: arm },
3401                otherwise: chain,
3402            })],
3403        });
3404    }
3405
3406    // Three gates in front of the id chain, in the order the clauses
3407    // rank them.
3408    //
3409    // `PID_IGNORE` first: §7.4.1.2.2 says "All consumers of this Data
3410    // Representation shall ignore parameters with this ID" and states no
3411    // exception, so it outranks the must-understand bit that `unknown`
3412    // would refuse on.
3413    //
3414    // Then `is_user_member`, which is false for a parameter carrying
3415    // `FLAG_IMPL_EXTENSION` and for one inside the reserved range. Both
3416    // are legal for a peer to send and neither is a member of this type,
3417    // so both take the unknown path instead of being matched by their
3418    // low bits against a member that happens to share them.
3419    let matched = rust::Block {
3420        statements: chain.map_or_else(Vec::new, |chain| chain.statements),
3421    };
3422    let tail = rust::Block {
3423        statements: vec![rust::Statement::Semi(rust::Expr::If {
3424            condition: Box::new(rust::Expr::Field {
3425                receiver: Box::new(rust::Expr::Path(Path::single("pid"))),
3426                name: Ident::new("ignore"),
3427            }),
3428            then: skip_parameter_block,
3429            otherwise: Some(rust::Block {
3430                statements: vec![rust::Statement::Semi(rust::Expr::If {
3431                    condition: Box::new(rust::Expr::MethodCall {
3432                        receiver: Box::new(rust::Expr::Path(Path::single("pid"))),
3433                        method: Ident::new("is_user_member"),
3434                        args: Vec::new(),
3435                    }),
3436                    then: matched,
3437                    otherwise: Some(unknown),
3438                })],
3439            }),
3440        })],
3441    };
3442    let body = vec![
3443        let_statement(
3444            "pid",
3445            rust::Expr::Try(Box::new(call("read_pid", Vec::new()))),
3446        ),
3447        rust::Statement::Semi(rust::Expr::If {
3448            condition: Box::new(rust::Expr::Field {
3449                receiver: Box::new(rust::Expr::Path(Path::single("pid"))),
3450                name: Ident::new("end"),
3451            }),
3452            then: rust::Block {
3453                statements: vec![rust::Statement::Semi(rust::Expr::Assign {
3454                    target: Box::new(rust::Expr::Path(Path::single("ended"))),
3455                    value: Box::new(rust::Expr::Literal(Literal::Bool(true))),
3456                })],
3457            },
3458            otherwise: Some(tail),
3459        }),
3460    ];
3461
3462    statements.push(rust::Statement::Semi(rust::Expr::While {
3463        condition: Box::new(rust::Expr::Not(Box::new(rust::Expr::Path(Path::single(
3464            "ended",
3465        ))))),
3466        body: rust::Block { statements: body },
3467    }));
3468    Ok(statements)
3469}
3470
3471/// What the two `@mutable` loops need of a member: the id its header
3472/// carries, the name its slot takes, and how to read it.
3473struct MutableSlot {
3474    /// The member's name, as the source wrote it.
3475    name: String,
3476    /// What reading it takes.
3477    kind: MemberWrite,
3478    /// The id its header carries.
3479    member_id: u32,
3480}
3481
3482/// The write call one member takes.
3483///
3484/// One arm per primitive rather than a formatted name: the writer for a
3485/// `u32` and the writer for an `i32` are different functions, and a
3486/// name assembled from a string would hide a wrong one until the bytes
3487/// came out different.
3488fn write_for(
3489    receiver: rust::Expr,
3490    name: &str,
3491    kind: &MemberWrite,
3492) -> Result<rust::Statement, EncodeError> {
3493    let value = rust::Expr::Field {
3494        receiver: Box::new(receiver),
3495        name: Ident::new(&rust::escape_keyword(name)),
3496    };
3497    write_value_to("w", name, kind, value, false)
3498}
3499
3500/// The same, naming the writer it goes to.
3501///
3502/// The KeyHash builds its own holder rather than writing to the
3503/// sample's writer: the holder is big-endian regardless of what the
3504/// sample is, and sharing one writer would make the hash depend on the
3505/// sample's byte order.
3506///
3507/// `borrowed` says the expression is already a `&T` — a binding out of
3508/// `match &self.<name> { Some(v) => … }`, or a closure's element —
3509/// rather than the field itself. It decides one thing per shape and no
3510/// more: a primitive is dereferenced, a string is not re-borrowed.
3511/// Passing the wrong one does not compile, which is the point of
3512/// carrying it rather than guessing from the shape of the expression.
3513///
3514/// `pub(crate)`: `emit::interface_rust` hand-assembles the DDS-RPC 1.0
3515/// §7.5.1.1 topic types the same way this file assembles an aggregate's
3516/// codec, member by member, and calls this rather than re-deriving how
3517/// one value goes onto a `Writer`.
3518pub(crate) fn write_value_to(
3519    sink: &str,
3520    name: &str,
3521    kind: &MemberWrite,
3522    value: rust::Expr,
3523    borrowed: bool,
3524) -> Result<rust::Statement, EncodeError> {
3525    write_value_to_sink(sink, SinkShape::Reference, name, kind, value, borrowed)
3526}
3527
3528/// Whether the name the writes go to is already a `&mut Writer`.
3529///
3530/// Every arm but one calls a method on the sink, and a method call
3531/// auto-references. `MemberWrite::Nested` hands it over as an *argument* —
3532/// `self.k.encode(sink)` — and there Rust does not: a local `Writer` where a
3533/// `&mut Writer` is wanted is `error[E0308]`, in generated code, which is the
3534/// worst place for it. The key-holder path is the one with a local, so it says
3535/// so instead of the emitter guessing from the name.
3536#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3537enum SinkShape {
3538    /// A `&mut Writer` parameter — pass it along as it is.
3539    Reference,
3540    /// A local `Writer` binding — take a reference where one is needed.
3541    Owned,
3542}
3543
3544fn write_value_to_sink(
3545    sink: &str,
3546    shape: SinkShape,
3547    name: &str,
3548    kind: &MemberWrite,
3549    value: rust::Expr,
3550    borrowed: bool,
3551) -> Result<rust::Statement, EncodeError> {
3552    let (call, fallible) = write_call(sink, shape, name, kind, value, borrowed)?;
3553    // A write that can refuse is followed by `?`. Dropping the `Result`
3554    // would compile — it is only a warning — and would put the narrowed
3555    // character on the wire, which is the defect the runtime stopped
3556    // committing.
3557    Ok(rust::Statement::Semi(if fallible {
3558        rust::Expr::Try(Box::new(call))
3559    } else {
3560        call
3561    }))
3562}
3563
3564/// The call one value's write is, and whether it returns a `Result`.
3565///
3566/// One arm per shape, and the shape decides three things at once: which
3567/// runtime function, how the value is handed over, and whether the
3568/// result has to be looked at.
3569///
3570/// zerodds-lint: recursion-depth 64 (collection nesting of the owned
3571/// `MemberWrite` tree, via `element_writer`). That tree is built by
3572/// `write_of_at`, whose `MAX_WRITE_DEPTH` bounds its height, and it
3573/// is owned, so it holds no cycle of its own.
3574fn write_call(
3575    sink: &str,
3576    shape: SinkShape,
3577    name: &str,
3578    kind: &MemberWrite,
3579    value: rust::Expr,
3580    borrowed: bool,
3581) -> Result<(rust::Expr, bool), EncodeError> {
3582    let sink_expr = || rust::Expr::Path(Path::single(sink));
3583    let deref = |value: rust::Expr| {
3584        if borrowed {
3585            rust::Expr::Deref(Box::new(value))
3586        } else {
3587            value
3588        }
3589    };
3590    // A `String` field goes in by reference and a `&String` binding is
3591    // one already; both coerce to the `&str` the runtime takes. A
3592    // collection is the same question one type up — `&Vec<T>` and
3593    // `&[T; N]` both coerce to the `&[T]` the runtime takes.
3594    let by_ref = |value: rust::Expr| {
3595        if borrowed {
3596            value
3597        } else {
3598            rust::Expr::Ref {
3599                target: Box::new(value),
3600                mutable: false,
3601            }
3602        }
3603    };
3604    Ok(match kind {
3605        MemberWrite::External(inner) => {
3606            return write_call(
3607                sink,
3608                shape,
3609                name,
3610                inner,
3611                rust::Expr::MethodCall {
3612                    receiver: Box::new(value),
3613                    method: Ident::new("as_ref"),
3614                    args: Vec::new(),
3615                },
3616                true,
3617            );
3618        }
3619        MemberWrite::Primitive(primitive) => (
3620            rust::Expr::MethodCall {
3621                receiver: Box::new(sink_expr()),
3622                method: Ident::new(writer_name(*primitive)),
3623                args: vec![deref(value)],
3624            },
3625            // `char` is one byte and `wchar` two, and a code point that
3626            // fits neither is refused rather than narrowed —
3627            // §7.2.2.2.1.2.2 and §7.2.2.2.1.2.5.
3628            matches!(primitive, PrimitiveKind::Char | PrimitiveKind::WChar),
3629        ),
3630        MemberWrite::String { bound, .. } => {
3631            // A bounded string writes the same bytes as an unbounded one
3632            // of the same length; the bound is a promise about the
3633            // source, not a difference on the wire. The runtime takes it
3634            // as a second argument and not as part of the name — and
3635            // refuses a value that overruns it, which is why the bounded
3636            // form is fallible and the plain one is not.
3637            let (method, mut args, fallible) = match bound {
3638                Some(bound) => (
3639                    "write_string_bounded",
3640                    vec![rust::Expr::Literal(Literal::int(*bound))],
3641                    true,
3642                ),
3643                None => ("write_string", Vec::new(), false),
3644            };
3645            let mut all = vec![by_ref(value)];
3646            all.append(&mut args);
3647            (
3648                rust::Expr::MethodCall {
3649                    receiver: Box::new(sink_expr()),
3650                    method: Ident::new(method),
3651                    args: all,
3652                },
3653                fallible,
3654            )
3655        }
3656        MemberWrite::Fixed {
3657            digits,
3658            scale,
3659            count,
3660        } => (
3661            rust::Expr::MethodCall {
3662                receiver: Box::new(sink_expr()),
3663                method: Ident::new("write_fixed"),
3664                args: vec![
3665                    by_ref(value),
3666                    rust::Expr::Literal(Literal::int(i128::from(*digits))),
3667                    rust::Expr::Literal(Literal::int(i128::from(*scale))),
3668                    fixed_digits_expr(*count),
3669                ],
3670            },
3671            // Always fallible: a value the declaration cannot carry is
3672            // refused rather than truncated, and a truncated decimal is the
3673            // wrong number with nothing to notice it by.
3674            true,
3675        ),
3676        MemberWrite::WString {
3677            bound,
3678            policy,
3679            units,
3680            order,
3681            position,
3682            ..
3683        } => {
3684            // Every write here is fallible, bounded or not: the
3685            // `CHARACTER_COUNT` cell refuses a code point outside the
3686            // BMP rather than truncating it to sixteen bits the way
3687            // Fast CDR does, so even an unbounded `wstring` can say no.
3688            //
3689            // `WstringCodeUnits::Utf8` names the runtime's UTF-8 pair,
3690            // which takes no order: UTF-8 octets have no byte order of
3691            // their own, and C1 and C3 are vacuous because there is no
3692            // fixed-width code unit for either to speak about.
3693            let (method, mut args) = match (units, bound) {
3694                (WstringCodeUnits::Utf8, Some(bound)) => (
3695                    "write_wstring_utf8_bounded",
3696                    vec![rust::Expr::Literal(Literal::int(*bound))],
3697                ),
3698                (WstringCodeUnits::Utf8, None) => ("write_wstring_utf8", Vec::new()),
3699                (WstringCodeUnits::Utf16, Some(bound)) => (
3700                    "write_wstring_bounded",
3701                    vec![rust::Expr::Literal(Literal::int(*bound))],
3702                ),
3703                (WstringCodeUnits::Utf16, None) => ("write_wstring", Vec::new()),
3704            };
3705            let mut all = vec![by_ref(value)];
3706            all.append(&mut args);
3707            all.push(wstring_policy_expr(*policy));
3708            if *units == WstringCodeUnits::Utf16 {
3709                all.push(wstring_order_expr(*order));
3710            }
3711            all.push(wire_position_expr(name, *position)?);
3712            (
3713                rust::Expr::MethodCall {
3714                    receiver: Box::new(sink_expr()),
3715                    method: Ident::new(method),
3716                    args: all,
3717                },
3718                true,
3719            )
3720        }
3721        MemberWrite::Enumeration { bits } => (
3722            rust::Expr::MethodCall {
3723                receiver: Box::new(sink_expr()),
3724                method: Ident::new(enum_writer(*bits)),
3725                args: vec![deref(value)],
3726            },
3727            false,
3728        ),
3729        // The bound goes into the call and the holder is widened to the
3730        // `u64` the runtime takes. Fallible: a value with a bit set
3731        // above the bound is refused rather than masked, which is what
3732        // `runtime/c/src/writer.c` does with `ZDW_RANGE`.
3733        MemberWrite::Bitmask { bits, .. } => (
3734            rust::Expr::MethodCall {
3735                receiver: Box::new(sink_expr()),
3736                method: Ident::new("write_bitmask"),
3737                args: vec![
3738                    rust::Expr::Literal(Literal::int(i128::from(*bits))),
3739                    widen_to_u64(deref(value), *bits),
3740                ],
3741            },
3742            true,
3743        ),
3744        // An aggregate encodes itself. There is no `Writer::write_nested`
3745        // and there was never going to be one: the nested type's own
3746        // `encode` is what carries its plan, and a runtime function
3747        // could not have known it.
3748        //
3749        // Under a per-sample plan the call names the nested type's
3750        // **body** and hands it the sample's decision: the same nested
3751        // type is framed short in a sample below RTI's step and extended
3752        // in one past it, so a call through the public entry would frame
3753        // it by the nested type's own size instead of the sample's.
3754        MemberWrite::Nested {
3755            codec, extended, ..
3756        } => {
3757            let mut args = vec![match shape {
3758                SinkShape::Reference => sink_expr(),
3759                SinkShape::Owned => rust::Expr::Ref {
3760                    target: Box::new(sink_expr()),
3761                    mutable: true,
3762                },
3763            }];
3764            args.extend(extended.argument());
3765            (
3766                rust::Expr::MethodCall {
3767                    receiver: Box::new(value),
3768                    method: Ident::new(&nested_method(codec, *extended)),
3769                    // The one place the sink is an argument and not a
3770                    // receiver, so the one place auto-referencing does
3771                    // not happen for us.
3772                    args,
3773                },
3774                true,
3775            )
3776        }
3777        MemberWrite::Sequence {
3778            element,
3779            bound,
3780            delimited,
3781            packed,
3782            carries_length,
3783            ..
3784        } => {
3785            // `sequence<T, N>`: the count is checked before a byte is
3786            // written. Table 17 makes `O2.length <= T1.length` the
3787            // condition for constructing one, the wire carries no bound
3788            // to compare against, and a sender that wrote more elements
3789            // than its own declaration allows has written a value of a
3790            // type nobody declared. G16, on the write side.
3791            let method = match (bound, delimited) {
3792                (Some(_), true) => "write_delimited_sequence_bounded",
3793                (Some(_), false) => "write_sequence_bounded",
3794                (None, true) => "write_delimited_sequence",
3795                (None, false) => "write_sequence",
3796            };
3797            let mut args = vec![by_ref(value)];
3798            if let Some(bound) = bound {
3799                args.push(rust::Expr::Literal(Literal::int(*bound)));
3800            }
3801            args.push(sequence_element_writer(
3802                name,
3803                element,
3804                *packed,
3805                *carries_length,
3806            )?);
3807            (
3808                rust::Expr::MethodCall {
3809                    receiver: Box::new(sink_expr()),
3810                    method: Ident::new(method),
3811                    args,
3812                },
3813                true,
3814            )
3815        }
3816        MemberWrite::Map {
3817            key,
3818            value: entry_value,
3819            bound,
3820            delimited,
3821            ascending,
3822            ..
3823        } => {
3824            // The count on the wire is the **entry** count and not the
3825            // number of halves — §7.4.1.1.4 defines a `map<K, V>` by
3826            // equivalence to a `sequence` whose element is the pair —
3827            // and the runtime writes it from `BTreeMap::len`, so there
3828            // is nothing to count here. `map<K, V, N>` bounds that same
3829            // number, which is why the bounded form passes it through
3830            // unchanged from the sequence's shape.
3831            let method = match (bound, delimited) {
3832                (Some(_), true) => "write_delimited_map_bounded",
3833                (Some(_), false) => "write_map_bounded",
3834                (None, true) => "write_delimited_map",
3835                (None, false) => "write_map",
3836            };
3837            // Held as its entries, the map goes out in the order it is held; ascending by key
3838            // is a view over them, and a `BTreeMap` is ascending already.
3839            let held = match crate::adapter::cst::rust_map_form() {
3840                crate::adapter::cst::RustMapForm::Entries if *ascending => rust::Expr::Ref {
3841                    target: Box::new(rust::Expr::Call {
3842                        callee: Box::new(rust::Expr::Path(Path::new(&[
3843                            "zerodds_wire",
3844                            "AscendingKey",
3845                        ]))),
3846                        args: vec![by_ref(value)],
3847                    }),
3848                    mutable: false,
3849                },
3850                _ => by_ref(value),
3851            };
3852            let mut args = vec![held];
3853            if let Some(bound) = bound {
3854                args.push(rust::Expr::Literal(Literal::int(*bound)));
3855            }
3856            args.push(element_writer(name, key)?);
3857            args.push(element_writer(name, entry_value)?);
3858            (
3859                rust::Expr::MethodCall {
3860                    receiver: Box::new(sink_expr()),
3861                    method: Ident::new(method),
3862                    args,
3863                },
3864                true,
3865            )
3866        }
3867        MemberWrite::Array {
3868            element,
3869            dims,
3870            delimited,
3871            packed,
3872        } => {
3873            // No length: an array's element count is in the type, not on
3874            // the wire. XTypes 1.3 §7.4.3.5. A multi-dimensional array is
3875            // one flat run (G4), so the delimiter — where rule (9) asks
3876            // for one — goes round the whole thing and the inner
3877            // dimensions are plain.
3878            (
3879                rust::Expr::MethodCall {
3880                    receiver: Box::new(sink_expr()),
3881                    method: Ident::new(if *delimited {
3882                        "write_delimited_array"
3883                    } else {
3884                        "write_array"
3885                    }),
3886                    args: vec![
3887                        by_ref(value),
3888                        array_element_writer(name, element, dims.len().saturating_sub(1), *packed)?,
3889                    ],
3890                },
3891                true,
3892            )
3893        }
3894        MemberWrite::Unsupported(kind) => {
3895            return Err(EncodeError::NoWriter {
3896                member: name.to_string(),
3897                kind,
3898            });
3899        }
3900    })
3901}
3902
3903/// The digit-count axis as the generated code names it.
3904///
3905/// A path and not a boolean: the emitted call says `FixedDigits::Declared`
3906/// or `FixedDigits::ValuePrecision`, so a reader of the generated file sees
3907/// which reading of CORBA 3.3 part 2 §9.3.2.8 it writes without going back
3908/// to the plan that chose it.
3909fn fixed_digits_expr(count: FixedDigitCount) -> rust::Expr {
3910    rust::Expr::Path(Path::new(&[
3911        "zerodds_wire",
3912        "FixedDigits",
3913        match count {
3914            FixedDigitCount::Declared => "Declared",
3915            FixedDigitCount::ValuePrecision => "ValuePrecision",
3916            FixedDigitCount::ValueStringDigits => "ValueStringDigits",
3917        },
3918    ]))
3919}
3920
3921/// `|w, v| <the element's write>`, always returning a `Result`.
3922///
3923/// A closure and not a named path. `Writer::write_i32` takes an `i32` by
3924/// value where an element function takes a `&i32`; a nested aggregate's
3925/// write is a method on the *value*; and a collection's write takes its
3926/// own element function as a second argument, which no path can supply.
3927/// The runtime's element functions were made `Fn`-generic and the CST
3928/// gained `Expr::Closure` for exactly this, and then nothing emitted
3929/// one — `element_writer_name` kept naming paths and refused the two
3930/// shapes that have none. That refusal is `AL-9`, and this is it closed.
3931///
3932/// zerodds-lint: recursion-depth 64 (collection nesting of the owned
3933/// `MemberWrite` tree, via `write_call`). That tree is built by
3934/// `write_of_at`, whose `MAX_WRITE_DEPTH` bounds its height, and it
3935/// is owned, so it holds no cycle of its own.
3936fn element_writer(at: &str, element: &MemberWrite) -> Result<rust::Expr, EncodeError> {
3937    // A `wstring` element is written at the element position, which is
3938    // substituted here for the same reason the `wchar` element's
3939    // framing is: `write_call` was handed a member's shape, and a
3940    // caller that could leave the position alone would take the
3941    // member's answer for an element by not asking.
3942    let substituted = as_element(element);
3943    let element = substituted.as_ref().unwrap_or(element);
3944    // A `wchar` element takes the element framing, which under TAO's
3945    // dialect is not the member's. `write_call` names the member's
3946    // method, so the element's is substituted here rather than passed
3947    // down through five shapes that do not care.
3948    if matches!(element, MemberWrite::Primitive(PrimitiveKind::WChar)) {
3949        return Ok(rust::Expr::Path(Path::new(&[
3950            "Writer",
3951            "write_wchar_element",
3952        ])));
3953    }
3954    let (call, fallible) = write_call(
3955        "w",
3956        SinkShape::Reference,
3957        at,
3958        element,
3959        rust::Expr::Path(Path::single("v")),
3960        true,
3961    )?;
3962    Ok(rust::Expr::Closure {
3963        params: vec![Ident::new("w"), Ident::new("v")],
3964        // An infallible write followed by `Ok(())`: one element-function
3965        // shape rather than two, because three of the four element kinds
3966        // can refuse and a generator choosing between two shapes would
3967        // be choosing per construct. Not `Ok(call)`: that hands the
3968        // write's `()` to a function, which is `clippy::unit_arg` in
3969        // every crate that includes the generated text.
3970        body: Box::new(if fallible {
3971            call
3972        } else {
3973            rust::Expr::Block(rust::Block {
3974                statements: vec![
3975                    rust::Statement::Semi(call),
3976                    rust::Statement::Tail(rust::Expr::Call {
3977                        callee: Box::new(rust::Expr::Path(Path::single("Ok"))),
3978                        args: vec![rust::Expr::Tuple(Vec::new())],
3979                    }),
3980                ],
3981            })
3982        }),
3983    })
3984}
3985
3986/// The element function for an array with `remaining` dimensions still
3987/// to unwrap.
3988///
3989/// `long grid[2][3]` is `[[i32; 3]; 2]`, so the outer `write_array`
3990/// hands each row to a function that writes a row.
3991///
3992/// `packed` wraps each level's element in a
3993/// `Writer::begin_packed_element` / `finish_packed_element` pair, at the
3994/// same site `zerodds-wire-codec`'s `write_array_dimension` writes them:
3995/// the row and every cell inside it each begin their own alignment block.
3996///
3997/// zerodds-lint: recursion-depth 8 (one level per dimension of the array
3998/// declarator: `remaining` falls by one per call and the cell ends it; the
3999/// depth is the number of dimensions the IDL writes, not a property of a
4000/// sample)
4001fn array_element_writer(
4002    at: &str,
4003    element: &MemberWrite,
4004    remaining: usize,
4005    packed: bool,
4006) -> Result<rust::Expr, EncodeError> {
4007    if remaining == 0 {
4008        // A cell: the element position and the packed block, and never an
4009        // element length — only a `sequence` has that axis.
4010        return sequence_element_writer(at, element, packed, false);
4011    }
4012    let inner = array_element_writer(at, element, remaining - 1, packed)?;
4013    let call = rust::Expr::MethodCall {
4014        receiver: Box::new(rust::Expr::Path(Path::single("w"))),
4015        method: Ident::new("write_array"),
4016        args: vec![rust::Expr::Path(Path::single("v")), inner],
4017    };
4018    if !packed {
4019        return Ok(rust::Expr::Closure {
4020            params: vec![Ident::new("w"), Ident::new("v")],
4021            body: Box::new(call),
4022        });
4023    }
4024    Ok(rust::Expr::Closure {
4025        params: vec![Ident::new("w"), Ident::new("v")],
4026        body: Box::new(rust::Expr::Block(rust::Block {
4027            statements: vec![
4028                let_statement("zd_origin", begin_packed("w")),
4029                rust::Statement::Semi(rust::Expr::Try(Box::new(call))),
4030                rust::Statement::Semi(finish_packed("w", "zd_origin")),
4031                rust::Statement::Tail(ok_unit()),
4032            ],
4033        })),
4034    })
4035}
4036
4037/// The element function a `sequence`'s write takes, with the two HDDS
4038/// axes the reference codec puts around each element.
4039///
4040/// With neither axis selected this is [`element_writer`] unchanged, so
4041/// every other dialect's emitted code is the same text it was.
4042fn sequence_element_writer(
4043    at: &str,
4044    element: &MemberWrite,
4045    packed: bool,
4046    carries_length: bool,
4047) -> Result<rust::Expr, EncodeError> {
4048    if !packed && !carries_length {
4049        return element_writer(at, element);
4050    }
4051    let (call, fallible) = element_write_call(at, element)?;
4052    let mut statements = Vec::new();
4053    if packed {
4054        statements.push(let_statement("zd_origin", begin_packed("w")));
4055    }
4056    if carries_length {
4057        statements.push(let_statement(
4058            "zd_length",
4059            rust::Expr::MethodCall {
4060                receiver: Box::new(rust::Expr::Path(Path::single("w"))),
4061                method: Ident::new("write_dheader"),
4062                args: Vec::new(),
4063            },
4064        ));
4065    }
4066    statements.push(rust::Statement::Semi(if fallible {
4067        rust::Expr::Try(Box::new(call))
4068    } else {
4069        call
4070    }));
4071    if carries_length {
4072        statements.push(rust::Statement::Semi(finish_dheader("w", "zd_length")));
4073    }
4074    if packed {
4075        statements.push(rust::Statement::Semi(finish_packed("w", "zd_origin")));
4076    }
4077    statements.push(rust::Statement::Tail(ok_unit()));
4078    Ok(rust::Expr::Closure {
4079        params: vec![Ident::new("w"), Ident::new("v")],
4080        body: Box::new(rust::Expr::Block(rust::Block { statements })),
4081    })
4082}
4083
4084/// The write one collection element is, at the element position, and
4085/// whether it returns a `Result`.
4086///
4087/// [`write_call`] names a `wchar`'s *member* framing; a collection
4088/// element takes the other one, so it is substituted here. A `wstring`
4089/// element has its position rewritten by [`as_element`] first, for the
4090/// same reason.
4091fn element_write_call(at: &str, element: &MemberWrite) -> Result<(rust::Expr, bool), EncodeError> {
4092    let substituted = as_element(element);
4093    let element = substituted.as_ref().unwrap_or(element);
4094    if matches!(element, MemberWrite::Primitive(PrimitiveKind::WChar)) {
4095        return Ok((
4096            rust::Expr::Call {
4097                callee: Box::new(rust::Expr::Path(Path::new(&[
4098                    "Writer",
4099                    "write_wchar_element",
4100                ]))),
4101                args: vec![
4102                    rust::Expr::Path(Path::single("w")),
4103                    rust::Expr::Path(Path::single("v")),
4104                ],
4105            },
4106            true,
4107        ));
4108    }
4109    write_call(
4110        "w",
4111        SinkShape::Reference,
4112        at,
4113        element,
4114        rust::Expr::Path(Path::single("v")),
4115        true,
4116    )
4117}
4118
4119/// `w.begin_packed_element()`.
4120fn begin_packed(sink: &str) -> rust::Expr {
4121    rust::Expr::MethodCall {
4122        receiver: Box::new(rust::Expr::Path(Path::single(sink))),
4123        method: Ident::new("begin_packed_element"),
4124        args: Vec::new(),
4125    }
4126}
4127
4128/// `w.finish_packed_element(saved)`.
4129fn finish_packed(sink: &str, saved: &str) -> rust::Expr {
4130    rust::Expr::MethodCall {
4131        receiver: Box::new(rust::Expr::Path(Path::single(sink))),
4132        method: Ident::new("finish_packed_element"),
4133        args: vec![rust::Expr::Path(Path::single(saved))],
4134    }
4135}
4136
4137/// `w.finish_dheader(header)`.
4138fn finish_dheader(sink: &str, header: &str) -> rust::Expr {
4139    rust::Expr::MethodCall {
4140        receiver: Box::new(rust::Expr::Path(Path::single(sink))),
4141        method: Ident::new("finish_dheader"),
4142        args: vec![rust::Expr::Path(Path::single(header))],
4143    }
4144}
4145
4146/// `Ok(())`.
4147fn ok_unit() -> rust::Expr {
4148    rust::Expr::Call {
4149        callee: Box::new(rust::Expr::Path(Path::single("Ok"))),
4150        args: vec![rust::Expr::Tuple(Vec::new())],
4151    }
4152}
4153
4154/// Wrap a generated `encode`/`decode` body in a packed-element block.
4155///
4156/// `ElementPadding::Packed` makes every aggregate and union value its own
4157/// alignment block: its members align from its own first octet, not from
4158/// the encapsulation, and the value is not padded up to its own alignment
4159/// before it starts. `sink` is `w` on the write side and `r` on the read
4160/// side.
4161///
4162/// The `finish` goes before the body's final statement — the `Ok(…)` the
4163/// body builds — so the restored origin is in effect for whatever the
4164/// caller writes after the value. A body that returns early on an error
4165/// leaves the origin displaced, which the caller discards with the
4166/// buffer: the same shape `zerodds-wire-codec`'s `?` between begin and
4167/// finish has.
4168fn wrap_packed_body(
4169    mut statements: Vec<rust::Statement>,
4170    sink: &str,
4171    packed: bool,
4172) -> Vec<rust::Statement> {
4173    if !packed {
4174        return statements;
4175    }
4176    let tail = statements.pop();
4177    let mut out = Vec::with_capacity(statements.len() + 3);
4178    out.push(let_statement("zd_origin", begin_packed(sink)));
4179    out.extend(statements);
4180    out.push(rust::Statement::Semi(finish_packed(sink, "zd_origin")));
4181    if let Some(tail) = tail {
4182        out.push(tail);
4183    }
4184    out
4185}
4186
4187/// The same shape, reached as a collection element rather than as a
4188/// member.
4189///
4190/// `None` where the shape carries no position, which is every shape but
4191/// one. A `Some` is a shape whose emitted call names a position and
4192/// therefore has to be rewritten before the call is built — not a
4193/// difference in the octets today, because all nine measured
4194/// implementations answer the two positions alike on all four `wstring`
4195/// axes, and the rewrite is what keeps that a measurement rather than a
4196/// thing nobody asked.
4197fn as_element(element: &MemberWrite) -> Option<MemberWrite> {
4198    match element {
4199        MemberWrite::WString {
4200            bound,
4201            narrow,
4202            policy,
4203            units,
4204            order,
4205            position: _,
4206        } => Some(MemberWrite::WString {
4207            bound: *bound,
4208            narrow: narrow.clone(),
4209            policy: *policy,
4210            units: *units,
4211            order: *order,
4212            position: WirePosition::Element,
4213        }),
4214        _ => None,
4215    }
4216}
4217
4218/// `zerodds_wire::WstringLengthPolicy::new(C1, C2, C3)`.
4219///
4220/// The constructor and not one of the five named constants, for every
4221/// cell alike. A table from cell to name is a table that can be wrong
4222/// about a cell, and the three cells no implementation writes have no
4223/// name to look up — which is exactly the shape that left the ROS cell
4224/// unreachable while the names were the values.
4225///
4226/// Absolute paths rather than imports, because the generated file's
4227/// import list is built by each consumer of this emitter and a new name
4228/// in it would have to be added in each one.
4229fn wstring_policy_expr(policy: WstringLengthPolicy) -> rust::Expr {
4230    let counts = match policy.counts {
4231        WstringLengthCounts::Octets => "Octets",
4232        WstringLengthCounts::Characters => "Characters",
4233    };
4234    let terminator = match policy.terminator {
4235        WstringTerminator::Absent => "Absent",
4236        WstringTerminator::Present => "Present",
4237    };
4238    let unit = match policy.unit {
4239        WstringUnitWidth::Two => "Two",
4240        WstringUnitWidth::Four => "Four",
4241    };
4242    rust::Expr::Call {
4243        callee: Box::new(rust::Expr::Path(Path::new(&[
4244            "zerodds_wire",
4245            "WstringLengthPolicy",
4246            "new",
4247        ]))),
4248        args: vec![
4249            rust::Expr::Path(Path::new(&["zerodds_wire", "WstringLengthCounts", counts])),
4250            rust::Expr::Path(Path::new(&[
4251                "zerodds_wire",
4252                "WstringTerminator",
4253                terminator,
4254            ])),
4255            rust::Expr::Path(Path::new(&["zerodds_wire", "WstringUnitWidth", unit])),
4256        ],
4257    }
4258}
4259
4260/// `zerodds_wire::WstringUnitOrder::<C4>` — the fourth axis.
4261fn wstring_order_expr(order: WstringUnitOrder) -> rust::Expr {
4262    rust::Expr::Path(Path::new(&[
4263        "zerodds_wire",
4264        "WstringUnitOrder",
4265        match order {
4266            WstringUnitOrder::Stream => "Stream",
4267            WstringUnitOrder::BigEndianAlways => "BigEndianAlways",
4268            WstringUnitOrder::ByteOrderMark => "ByteOrderMark",
4269            WstringUnitOrder::HostUnswapped => "HostUnswapped",
4270        },
4271    ]))
4272}
4273
4274/// `zerodds_wire::WirePosition::<position>`.
4275///
4276/// The two positions a `wstring` can be reached at, and an error for
4277/// the four it cannot. Not a fallback to `Member`: a position that
4278/// silently became the member's is precisely how a `wchar` element got
4279/// a member's framing through five emitters, and the runtime's own
4280/// enumeration has no name for a `wstring` in a parameter sentinel to
4281/// map onto.
4282fn wire_position_expr(member: &str, position: WirePosition) -> Result<rust::Expr, EncodeError> {
4283    let name = match position {
4284        WirePosition::Member => "Member",
4285        WirePosition::Element => "Element",
4286        WirePosition::Discriminator
4287        | WirePosition::KeyHolderMember
4288        | WirePosition::Sentinel
4289        | WirePosition::ExtendedMarker => {
4290            return Err(EncodeError::NoWriter {
4291                member: member.to_string(),
4292                kind: "a wstring at a position the wire runtime has no name for",
4293            });
4294        }
4295    };
4296    Ok(rust::Expr::Path(Path::new(&[
4297        "zerodds_wire",
4298        "WirePosition",
4299        name,
4300    ])))
4301}
4302
4303/// The integer writer an enumeration's bit bound calls for.
4304const fn enum_writer(bits: u16) -> &'static str {
4305    match bits {
4306        0..=8 => "write_u8",
4307        9..=16 => "write_u16",
4308        17..=32 => "write_u32",
4309        _ => "write_u64",
4310    }
4311}
4312
4313/// The expression that reads one value, of type `Result<T, WireError>`.
4314///
4315/// The narrowing §7.2.2.7 asks for is part of this expression and not a
4316/// step after it, so a member and a collection element get the same
4317/// treatment from one place — the element position is the one
4318/// §7.3.1.2.1.12.4 names and the one OpenDDS implements separately from
4319/// the collection's own bound.
4320///
4321/// zerodds-lint: recursion-depth 64 — one frame per external-member nesting
4322/// level; the shared lowering bounds that nesting before it reaches here.
4323///
4324/// `pub(crate)`: see [`write_value_to`].
4325pub(crate) fn read_for(name: &str, kind: &MemberWrite) -> Result<rust::Expr, EncodeError> {
4326    let read = read_whole(name, kind)?;
4327    match narrowing_of(kind) {
4328        Some(narrow) => narrowed(read, kind, narrow, name),
4329        None => Ok(read),
4330    }
4331}
4332
4333/// The narrowing of a shape, where the shape has one.
4334const fn narrowing_of(kind: &MemberWrite) -> Option<&Narrowing> {
4335    match kind {
4336        MemberWrite::String { narrow, .. }
4337        | MemberWrite::WString { narrow, .. }
4338        | MemberWrite::Sequence { narrow, .. }
4339        | MemberWrite::Map { narrow, .. } => narrow.as_ref(),
4340        _ => None,
4341    }
4342}
4343
4344/// `{ let v = <read>?; match r.fit_…(v, bound, TryConstruct::X)? { … } }`
4345///
4346/// The whole value comes off the wire first and the bound is applied
4347/// afterwards. A reader that stopped at its own bound would leave the
4348/// rest in the stream and put every following member at the wrong
4349/// offset — OpenDDS reads all twelve characters of `"Hello World!"`
4350/// before it defaults or truncates, and the members after it come back
4351/// correct, which is the property `try_construct_behaviour.md` singles
4352/// out.
4353fn narrowed(
4354    read: rust::Expr,
4355    kind: &MemberWrite,
4356    narrow: &Narrowing,
4357    name: &str,
4358) -> Result<rust::Expr, EncodeError> {
4359    let fit = match kind {
4360        MemberWrite::String { .. } => "fit_string",
4361        // The bound counts UTF-16 code units and not octets, so the
4362        // runtime has its own narrowing rather than `fit_string` with
4363        // another argument: one function taking "octets or units" would
4364        // be a call site deciding which by getting an argument right.
4365        MemberWrite::WString { .. } => "fit_wstring",
4366        MemberWrite::Sequence { .. } => "fit_sequence",
4367        // Table 11 names `map` in the same breath as `sequence`, and
4368        // the bound counts entries, so the runtime has its own `fit_map`
4369        // rather than a `fit_sequence` over the halves.
4370        MemberWrite::Map { .. } => match crate::adapter::cst::rust_map_form() {
4371            crate::adapter::cst::RustMapForm::Entries => "fit_map_entries",
4372            crate::adapter::cst::RustMapForm::BTreeMap => "fit_map",
4373        },
4374        // Table 11: TRIM "only applies to elements or members of type
4375        // string, wide string, sequence, or map", and the front end
4376        // refuses the annotation anywhere else
4377        // (`validate.rs::refuse_trim`). No other shape carries a
4378        // `Narrowing` at all, so this arm is a model that did not come
4379        // through the front end.
4380        _ => {
4381            return Err(EncodeError::NoWriter {
4382                member: name.to_string(),
4383                kind: "a bound on a type that cannot have one",
4384            });
4385        }
4386    };
4387    let bound = rust::escape_keyword(name);
4388    // USE_DEFAULT is the only behaviour that needs a value here; under
4389    // DISCARD `fit_…` has already returned `Err(WireError::Discarded)`
4390    // and under TRIM it has returned the trimmed value.
4391    let default = match (&narrow.default, narrow.behaviour) {
4392        (Some(default), _) => default.clone(),
4393        (None, TryConstruct::UseDefault) => {
4394            return Err(EncodeError::NoWriter {
4395                member: name.to_string(),
4396                kind: "a `@try_construct(USE_DEFAULT)` position whose default this column \
4397                       cannot spell",
4398            });
4399        }
4400        // Under DISCARD and TRIM the runtime never answers `Default`,
4401        // so the arm exists only because the match has to be
4402        // exhaustive. A refusal and not a panic: a panic in a decoder is
4403        // a denial of service a peer can send, and this one would mean
4404        // the generated code and the runtime disagree about what the
4405        // behaviour was.
4406        (None, _) => rust::Expr::Path(Path::single("__unreachable")),
4407    };
4408    Ok(rust::Expr::Block(rust::Block {
4409        statements: vec![
4410            let_statement(name, rust::Expr::Try(Box::new(read))),
4411            rust::Statement::Tail(rust::Expr::Match {
4412                scrutinee: Box::new(rust::Expr::Try(Box::new(call(
4413                    fit,
4414                    vec![
4415                        rust::Expr::Path(Path::single(&bound)),
4416                        rust::Expr::Literal(Literal::int(narrow.bound)),
4417                        rust::Expr::Path(Path::new(&[
4418                            "TryConstruct",
4419                            try_construct_variant(narrow.behaviour),
4420                        ])),
4421                    ],
4422                )))),
4423                arms: vec![
4424                    rust::MatchArm {
4425                        pattern: rust::Pattern::TupleStruct {
4426                            path: Path::new(&["Narrowed", "Value"]),
4427                            elements: vec![rust::Pattern::Binding(Ident::new(&bound))],
4428                        },
4429                        guard: None,
4430                        body: rust::Expr::Call {
4431                            callee: Box::new(rust::Expr::Path(Path::single("Ok"))),
4432                            args: vec![rust::Expr::Path(Path::single(&bound))],
4433                        },
4434                    },
4435                    rust::MatchArm {
4436                        pattern: rust::Pattern::Path(Path::new(&["Narrowed", "Default"])),
4437                        guard: None,
4438                        body: if narrow.behaviour == TryConstruct::UseDefault {
4439                            rust::Expr::Call {
4440                                callee: Box::new(rust::Expr::Path(Path::single("Ok"))),
4441                                args: vec![default],
4442                            }
4443                        } else {
4444                            rust::Expr::Call {
4445                                callee: Box::new(rust::Expr::Path(Path::single("Err"))),
4446                                args: vec![rust::Expr::Call {
4447                                    callee: Box::new(rust::Expr::Path(Path::new(&[
4448                                        "WireError",
4449                                        "refused",
4450                                    ]))),
4451                                    args: vec![
4452                                        call("position", Vec::new()),
4453                                        rust::Expr::Literal(Literal::string(
4454                                            "a narrowing that asked for no default and got one",
4455                                        )),
4456                                    ],
4457                                }],
4458                            }
4459                        },
4460                    },
4461                ],
4462            }),
4463        ],
4464    }))
4465}
4466
4467/// The runtime's spelling of one behaviour.
4468const fn try_construct_variant(behaviour: TryConstruct) -> &'static str {
4469    match behaviour {
4470        TryConstruct::Discard => "Discard",
4471        TryConstruct::UseDefault => "UseDefault",
4472        TryConstruct::Trim => "Trim",
4473    }
4474}
4475
4476/// The read itself, before any bound is applied.
4477///
4478/// zerodds-lint: recursion-depth 64 — one frame per external-member nesting
4479/// level; the shared lowering bounds that nesting before it reaches here.
4480fn read_whole(name: &str, kind: &MemberWrite) -> Result<rust::Expr, EncodeError> {
4481    Ok(match kind {
4482        MemberWrite::External(inner) => rust::Expr::MethodCall {
4483            receiver: Box::new(read_for(name, inner)?),
4484            method: Ident::new("map"),
4485            args: vec![rust::Expr::Path(Path::new(&["Box", "new"]))],
4486        },
4487        MemberWrite::Primitive(primitive) => call(reader_name(*primitive), Vec::new()),
4488        MemberWrite::String { .. } => call("read_string", Vec::new()),
4489        MemberWrite::Fixed {
4490            digits,
4491            scale,
4492            count,
4493        } => call(
4494            "read_fixed",
4495            vec![
4496                rust::Expr::Literal(Literal::int(i128::from(*digits))),
4497                rust::Expr::Literal(Literal::int(i128::from(*scale))),
4498                fixed_digits_expr(*count),
4499            ],
4500        ),
4501        MemberWrite::WString {
4502            policy,
4503            units,
4504            order,
4505            position,
4506            ..
4507        } => {
4508            // The mirror of `write_call`'s branch: `Utf8` names the
4509            // runtime's UTF-8 reader, which takes no order.
4510            if *units == WstringCodeUnits::Utf8 {
4511                call(
4512                    "read_wstring_utf8",
4513                    vec![
4514                        wstring_policy_expr(*policy),
4515                        wire_position_expr(name, *position)?,
4516                    ],
4517                )
4518            } else {
4519                call(
4520                    "read_wstring",
4521                    vec![
4522                        wstring_policy_expr(*policy),
4523                        wstring_order_expr(*order),
4524                        wire_position_expr(name, *position)?,
4525                    ],
4526                )
4527            }
4528        }
4529        MemberWrite::Enumeration { bits } => call(enum_reader(*bits), Vec::new()),
4530        // `read_bitmask` answers in the `u64` it reads into, and the
4531        // field is the holder the bound calls for, so the narrowing
4532        // rides on the `Result` rather than after it: `read_for` may
4533        // wrap this expression in a `try_construct` arm, and a cast
4534        // written outside would have been applied to the wrong thing.
4535        MemberWrite::Bitmask { bits, declared } => narrow_from_u64(
4536            call(
4537                "read_bitmask",
4538                vec![
4539                    rust::Expr::Literal(Literal::int(i128::from(*bits))),
4540                    rust::Expr::Literal(Literal::int(i128::from(*declared))),
4541                ],
4542            ),
4543            *bits,
4544        ),
4545        // The mirror of the encoder's `self.<field>.encode(w)`. The read
4546        // side needs no `extended`: `Reader::read_pid_as` takes the
4547        // header form off the wire, so one decoder serves both forms a
4548        // per-sample plan writes.
4549        MemberWrite::Nested {
4550            type_name, codec, ..
4551        } => rust::Expr::Call {
4552            callee: Box::new(rust::Expr::Path(Path::new(&[
4553                type_name.as_str(),
4554                &format!("decode{codec}"),
4555            ]))),
4556            args: vec![rust::Expr::Path(Path::single("r"))],
4557        },
4558        MemberWrite::Sequence {
4559            element,
4560            delimited,
4561            packed,
4562            carries_length,
4563            ..
4564        } => call(
4565            if *delimited {
4566                "read_delimited_sequence"
4567            } else {
4568                "read_sequence"
4569            },
4570            vec![sequence_element_reader(
4571                name,
4572                element,
4573                *packed,
4574                *carries_length,
4575            )?],
4576        ),
4577        MemberWrite::Map {
4578            key,
4579            value,
4580            delimited,
4581            ..
4582        } => call(
4583            match (crate::adapter::cst::rust_map_form(), *delimited) {
4584                (crate::adapter::cst::RustMapForm::Entries, true) => "read_delimited_map_entries",
4585                (crate::adapter::cst::RustMapForm::Entries, false) => "read_map_entries",
4586                (crate::adapter::cst::RustMapForm::BTreeMap, true) => "read_delimited_map",
4587                (crate::adapter::cst::RustMapForm::BTreeMap, false) => "read_map",
4588            },
4589            vec![element_reader(name, key)?, element_reader(name, value)?],
4590        ),
4591        MemberWrite::Array {
4592            element,
4593            dims,
4594            delimited,
4595            packed,
4596        } => call(
4597            // `_n`, not the `Vec` form: the field an array materialises
4598            // to is `[T; N]`, and `N` is inferred from it.
4599            if *delimited {
4600                "read_delimited_array_n"
4601            } else {
4602                "read_array_n"
4603            },
4604            vec![array_element_reader(
4605                name,
4606                element,
4607                dims.len().saturating_sub(1),
4608                *packed,
4609            )?],
4610        ),
4611        MemberWrite::Unsupported(kind) => {
4612            return Err(EncodeError::NoWriter {
4613                member: name.to_string(),
4614                kind,
4615            });
4616        }
4617    })
4618}
4619
4620/// The element function a collection's read takes.
4621///
4622/// A path where one has the right shape — `Reader::read_i32` and
4623/// `Cell::decode` are both `fn(&mut Reader) -> Result<T, WireError>` —
4624/// and a closure where the read needs an argument of its own.
4625fn element_reader(at: &str, element: &MemberWrite) -> Result<rust::Expr, EncodeError> {
4626    // The element position, substituted for the reason
4627    // `element_writer` gives.
4628    let substituted = as_element(element);
4629    let element = substituted.as_ref().unwrap_or(element);
4630    // An element with a bound of its own takes a closure whatever its
4631    // shape: §7.3.1.2.1.12.4 gives the element position its own
4632    // behaviour, and a named path has nowhere to put it. This is the
4633    // split Table 17 draws and OpenDDS implements — the sequence's
4634    // length overflow and an element's bound overflow fail separately.
4635    if narrowing_of(element).is_some() {
4636        return Ok(rust::Expr::Closure {
4637            params: vec![Ident::new("r")],
4638            body: Box::new(read_for(at, element)?),
4639        });
4640    }
4641    Ok(match element {
4642        // As `element_writer`: the element's `wchar` reader, not the
4643        // member's.
4644        MemberWrite::Primitive(PrimitiveKind::WChar) => {
4645            rust::Expr::Path(Path::new(&["Reader", "read_wchar_element"]))
4646        }
4647        MemberWrite::Primitive(primitive) => {
4648            rust::Expr::Path(Path::new(&["Reader", reader_name(*primitive)]))
4649        }
4650        MemberWrite::String { .. } => rust::Expr::Path(Path::new(&["Reader", "read_string"])),
4651        // A closure and not a path, for the reason `wstring` needs one:
4652        // `read_fixed` takes the declaration and the digit count as
4653        // arguments, so there is no bare
4654        // `fn(&mut Reader) -> Result<T, WireError>` to hand over.
4655        MemberWrite::Fixed { .. } => rust::Expr::Closure {
4656            params: vec![Ident::new("r")],
4657            body: Box::new(read_for(at, element)?),
4658        },
4659        // A closure and not a path: `read_wstring` takes the cell and
4660        // the order as arguments, so there is no bare
4661        // `fn(&mut Reader) -> Result<T, WireError>` to hand over.
4662        MemberWrite::WString { .. } => rust::Expr::Closure {
4663            params: vec![Ident::new("r")],
4664            body: Box::new(read_for(at, element)?),
4665        },
4666        MemberWrite::Enumeration { bits } => {
4667            rust::Expr::Path(Path::new(&["Reader", enum_reader(*bits)]))
4668        }
4669        MemberWrite::Nested {
4670            type_name, codec, ..
4671        } => rust::Expr::Path(Path::new(&[type_name.as_str(), &format!("decode{codec}")])),
4672        // A closure for the reason `read_wstring` needs one: the bound
4673        // and the declared mask are arguments, so there is no bare
4674        // `fn(&mut Reader) -> Result<T, WireError>` to name.
4675        MemberWrite::Bitmask { .. }
4676        | MemberWrite::External(_)
4677        | MemberWrite::Sequence { .. }
4678        | MemberWrite::Array { .. }
4679        | MemberWrite::Map { .. } => rust::Expr::Closure {
4680            params: vec![Ident::new("r")],
4681            body: Box::new(read_for(at, element)?),
4682        },
4683        MemberWrite::Unsupported(kind) => {
4684            return Err(EncodeError::NoWriter {
4685                member: at.to_string(),
4686                kind,
4687            });
4688        }
4689    })
4690}
4691
4692/// A bitmask's holder widened to the `u64` `Writer::write_bitmask`
4693/// takes.
4694///
4695/// `as u64` and not `u64::from`: the 64-bit holder is already a `u64`
4696/// and `u64::from(x)` on one is a type error, while the cast is
4697/// accepted for all four widths. Widening is exact in every one of
4698/// them, so nothing can be lost here.
4699fn widen_to_u64(value: rust::Expr, bits: u16) -> rust::Expr {
4700    if crate::adapter::cst::bitmask_primitive(bits) == rust::Primitive::U64 {
4701        return value;
4702    }
4703    rust::Expr::Cast {
4704        value: Box::new(value),
4705        type_: Box::new(rust::Type::Primitive(rust::Primitive::U64)),
4706    }
4707}
4708
4709/// `Reader::read_bitmask`'s `u64` narrowed back to the holder the field
4710/// is declared as.
4711///
4712/// Exact for the same reason the writer's widening is: the runtime has
4713/// already refused every bit above the bound, so nothing this cast drops
4714/// could have been set.
4715fn narrow_from_u64(read: rust::Expr, bits: u16) -> rust::Expr {
4716    let holder = crate::adapter::cst::bitmask_primitive(bits);
4717    if holder == rust::Primitive::U64 {
4718        return read;
4719    }
4720    rust::Expr::MethodCall {
4721        receiver: Box::new(read),
4722        method: Ident::new("map"),
4723        args: vec![rust::Expr::Closure {
4724            params: vec![Ident::new("v")],
4725            body: Box::new(rust::Expr::Cast {
4726                value: Box::new(rust::Expr::Path(Path::single("v"))),
4727                type_: Box::new(rust::Type::Primitive(holder)),
4728            }),
4729        }],
4730    }
4731}
4732
4733/// The element function for an array with `remaining` dimensions left.
4734///
4735/// `packed` wraps each level's element in a
4736/// `Reader::begin_packed_element` / `finish_packed_element` pair, the
4737/// read side of `array_element_writer`'s: the row and every cell inside
4738/// it each begin their own alignment block.
4739///
4740/// zerodds-lint: recursion-depth 8 (as `array_element_writer`: one level
4741/// per dimension of the array declarator)
4742fn array_element_reader(
4743    at: &str,
4744    element: &MemberWrite,
4745    remaining: usize,
4746    packed: bool,
4747) -> Result<rust::Expr, EncodeError> {
4748    if remaining == 0 {
4749        // A cell: the element position and the packed block, and never an
4750        // element length.
4751        return sequence_element_reader(at, element, packed, false);
4752    }
4753    let inner = array_element_reader(at, element, remaining - 1, packed)?;
4754    let call = rust::Expr::MethodCall {
4755        receiver: Box::new(rust::Expr::Path(Path::single("r"))),
4756        method: Ident::new("read_array_n"),
4757        args: vec![inner],
4758    };
4759    if !packed {
4760        return Ok(rust::Expr::Closure {
4761            params: vec![Ident::new("r")],
4762            body: Box::new(call),
4763        });
4764    }
4765    Ok(rust::Expr::Closure {
4766        params: vec![Ident::new("r")],
4767        body: Box::new(rust::Expr::Block(rust::Block {
4768            statements: vec![
4769                let_statement("zd_origin", begin_packed("r")),
4770                let_statement("value", rust::Expr::Try(Box::new(call))),
4771                rust::Statement::Semi(finish_packed("r", "zd_origin")),
4772                rust::Statement::Tail(rust::Expr::Call {
4773                    callee: Box::new(rust::Expr::Path(Path::single("Ok"))),
4774                    args: vec![rust::Expr::Path(Path::single("value"))],
4775                }),
4776            ],
4777        })),
4778    })
4779}
4780
4781/// The element function a `sequence`'s read takes, with the two HDDS
4782/// axes the reference codec puts around each element.
4783///
4784/// With neither axis selected this is [`element_reader`] unchanged, so
4785/// every other dialect's emitted code is the same text it was.
4786///
4787/// The element's own length is checked against what reading it consumed,
4788/// exactly as `zerodds-wire-codec`'s sequence walk does: the declared
4789/// count is a peer's claim, and a value that did not fill it leaves the
4790/// next element to be read from inside this one.
4791fn sequence_element_reader(
4792    at: &str,
4793    element: &MemberWrite,
4794    packed: bool,
4795    carries_length: bool,
4796) -> Result<rust::Expr, EncodeError> {
4797    if !packed && !carries_length {
4798        return element_reader(at, element);
4799    }
4800    let read = element_read_expr(at, element)?;
4801    let mut statements = Vec::new();
4802    if packed {
4803        statements.push(let_statement("zd_origin", begin_packed("r")));
4804    }
4805    if carries_length {
4806        statements.push(let_statement(
4807            "zd_declared",
4808            rust::Expr::Cast {
4809                value: Box::new(rust::Expr::Try(Box::new(call("read_u32", Vec::new())))),
4810                type_: Box::new(rust::Type::named(Path::single("usize"))),
4811            },
4812        ));
4813        statements.push(let_statement("zd_before", call("position", Vec::new())));
4814    }
4815    statements.push(let_statement("value", rust::Expr::Try(Box::new(read))));
4816    if carries_length {
4817        statements.push(rust::Statement::Semi(rust::Expr::If {
4818            condition: Box::new(rust::Expr::Binary {
4819                op: rust::BinaryOp::Ne,
4820                lhs: Box::new(rust::Expr::Binary {
4821                    op: rust::BinaryOp::Sub,
4822                    lhs: Box::new(call("position", Vec::new())),
4823                    rhs: Box::new(rust::Expr::Path(Path::single("zd_before"))),
4824                }),
4825                rhs: Box::new(rust::Expr::Path(Path::single("zd_declared"))),
4826            }),
4827            then: rust::Block {
4828                statements: vec![rust::Statement::Semi(rust::Expr::Return(Some(Box::new(
4829                    refused_err(
4830                        "r",
4831                        "a sequence element of the octet count its own length declared",
4832                    ),
4833                ))))],
4834            },
4835            otherwise: None,
4836        }));
4837    }
4838    if packed {
4839        statements.push(rust::Statement::Semi(finish_packed("r", "zd_origin")));
4840    }
4841    statements.push(rust::Statement::Tail(rust::Expr::Call {
4842        callee: Box::new(rust::Expr::Path(Path::single("Ok"))),
4843        args: vec![rust::Expr::Path(Path::single("value"))],
4844    }));
4845    Ok(rust::Expr::Closure {
4846        params: vec![Ident::new("r")],
4847        body: Box::new(rust::Expr::Block(rust::Block { statements })),
4848    })
4849}
4850
4851/// The read one collection element is, at the element position.
4852///
4853/// [`read_for`] names a `wchar`'s *member* framing; a collection element
4854/// takes the other one, so it is substituted here. A `wstring` element
4855/// has its position rewritten by [`as_element`] first.
4856fn element_read_expr(at: &str, element: &MemberWrite) -> Result<rust::Expr, EncodeError> {
4857    let substituted = as_element(element);
4858    let element = substituted.as_ref().unwrap_or(element);
4859    if matches!(element, MemberWrite::Primitive(PrimitiveKind::WChar)) {
4860        return Ok(call("read_wchar_element", Vec::new()));
4861    }
4862    read_for(at, element)
4863}
4864
4865/// The integer reader an enumeration's bit bound calls for.
4866const fn enum_reader(bits: u16) -> &'static str {
4867    match bits {
4868        0..=8 => "read_u8",
4869        9..=16 => "read_u16",
4870        17..=32 => "read_u32",
4871        _ => "read_u64",
4872    }
4873}
4874
4875/// What kind of write a member needs.
4876///
4877/// `pub(crate)`: see [`write_value_to`]. `emit::interface_rust` never
4878/// constructs a variant beside [`Self::Primitive`] — it gets every other
4879/// shape from [`write_of`] — so it names the type and this one variant
4880/// only.
4881#[derive(Debug, Clone, PartialEq, Eq)]
4882pub(crate) enum MemberWrite {
4883    /// Owning indirection in the Rust binding, with unchanged wire layout.
4884    External(Box<MemberWrite>),
4885    /// A primitive, with its own writer.
4886    Primitive(PrimitiveKind),
4887    /// A string, with its bound when it has one.
4888    ///
4889    /// The bound is written into the call rather than checked at
4890    /// runtime by the writer: a bounded string that overruns is a
4891    /// program error, and the bound is what the IDL said, not a
4892    /// parameter of the wire format.
4893    String {
4894        /// The bound, absent when unbounded.
4895        bound: Option<i128>,
4896        /// What this *position* does with a value the bound cannot hold.
4897        /// Absent when the string is unbounded, because there is then
4898        /// nothing a value can fail to fit.
4899        narrow: Option<Narrowing>,
4900    },
4901    /// A `fixed<P, S>`, with its declaration and the digit-count axis.
4902    ///
4903    /// The value is a decimal **string** in this column — `cst.rs` gives
4904    /// `fixed` the Rust spelling `String` and `default_rust.rs` gives it a
4905    /// zero at the declared scale — for the reason the other twenty columns
4906    /// carry it as text too: Rust has no decimal type in `core`, and a
4907    /// `fixed<31, 2>` does not fit any integer Rust has. A binary float
4908    /// would be the wrong answer rather than an approximate one, because
4909    /// `fixed` exists to be exact.
4910    ///
4911    /// Three fields and not one: `P` and `S` are the declaration, and
4912    /// `count` is the dialect axis over CORBA 3.3 part 2 §9.3.2.8's open
4913    /// question of how many digit nibbles go out. All three go into the
4914    /// emitted call, so the generated codec states the cell it writes.
4915    Fixed {
4916        /// `P` — the total digits the declaration names.
4917        digits: u8,
4918        /// `S` — the digits after the point.
4919        scale: u8,
4920        /// Whether `P` digits go out whatever the value is, or as many as
4921        /// the value has. `zerodds_ast_layout::key::FixedDigitCount`.
4922        count: FixedDigitCount,
4923    },
4924    /// A `wstring`, with its bound and the four axes that decide its
4925    /// octets.
4926    ///
4927    /// Five fields and not one, because the cell is a product: C1, C2
4928    /// and C3 are [`WstringLengthPolicy`]'s three coordinates, C4 is
4929    /// [`WstringUnitOrder`], and the position is what keeps the claim
4930    /// that the two positions agree a measurement rather than an
4931    /// assumption. All four come out of the [`LayoutKey`] and go into
4932    /// the emitted call, so the generated codec states the cell it
4933    /// writes instead of inheriting one from the runtime.
4934    WString {
4935        /// The bound in **UTF-16 code units**, absent when unbounded.
4936        ///
4937        /// Units and not octets: `wstring<N>` is an `N`-unit buffer in
4938        /// every measured mapping, and an octet bound would mean two
4939        /// different capacities under C3's two widths.
4940        bound: Option<i128>,
4941        /// What this *position* does with a value the bound cannot
4942        /// hold. Absent when the `wstring` is unbounded.
4943        narrow: Option<Narrowing>,
4944        /// C1 × C2 × C3 — what the length counts, whether a terminator
4945        /// joins the run, how wide one code unit is.
4946        policy: WstringLengthPolicy,
4947        /// What the content's characters are made of. Under
4948        /// [`WstringCodeUnits::Utf8`] the characters go out as their own
4949        /// UTF-8 octets and C1 and C3 are vacuous, so the emitted call
4950        /// names the runtime's UTF-8 entry point instead.
4951        units: WstringCodeUnits,
4952        /// C4 — in what byte order the units go out, and whether a
4953        /// byte-order mark leads them.
4954        order: WstringUnitOrder,
4955        /// Where the `wstring` is reached from.
4956        position: WirePosition,
4957    },
4958    /// A sequence, with its element and its bound.
4959    Sequence {
4960        /// What each element is.
4961        element: Box<MemberWrite>,
4962        /// The bound, absent when unbounded.
4963        bound: Option<i128>,
4964        /// As [`MemberWrite::String`]'s.
4965        narrow: Option<Narrowing>,
4966        /// Whether a `DHEADER` precedes the length. Rule (12), decided
4967        /// by `zerodds_ast_layout::collection_is_delimited` so that the
4968        /// emitted bytes and the planned layout cannot disagree.
4969        delimited: bool,
4970        /// Whether each element begins its own alignment block.
4971        /// `ElementPadding::Packed`'s writer half: this column wraps the
4972        /// element function in a `Writer::begin_packed_element` /
4973        /// `finish_packed_element` pair, at the same site
4974        /// `zerodds-wire-codec` writes them.
4975        packed: bool,
4976        /// Whether each element is preceded by its own four-octet
4977        /// length. `SequenceElementDelimiter::BeforeEachNonFinalElement`,
4978        /// decided by `zerodds_ast_layout::xcdr::sequence_element_carries_length`
4979        /// so the two walkers ask one crossing.
4980        carries_length: bool,
4981    },
4982    /// A map, with both halves and its bound.
4983    ///
4984    /// Two element functions and not one: a `map<K, V>` is a sequence of
4985    /// pairs (§7.4.1.1.4) and the halves are two different types, so the
4986    /// runtime takes a function per half.
4987    ///
4988    /// # The entry order is the container's and is not decided here
4989    ///
4990    /// `adapter::cst` materialises an IDL `map` as a `BTreeMap<K, V>`,
4991    /// which iterates ascending by key, and
4992    /// `zerodds_ast_layout::MapEntryOrder::AscendingKey` is defined as
4993    /// what a binding that holds a `map` in an ordered container writes.
4994    /// So this column answers the axis by the type it already emits.
4995    /// There is no walk here to bypass it and no second reading to keep
4996    /// in step — which is why this shape carries no ordering field, and
4997    /// why `crates/lang-rules/src/encode.rs` is not one of the sites
4998    /// `wire-probe`'s `map_entry_order.rs` counts crossings in.
4999    Map {
5000        /// What each entry's key is.
5001        key: Box<MemberWrite>,
5002        /// What each entry's value is.
5003        value: Box<MemberWrite>,
5004        /// The bound, in **entries**, absent when unbounded.
5005        bound: Option<i128>,
5006        /// As [`MemberWrite::String`]'s. Table 11 names `map` among the
5007        /// forms a TRIM may be written on.
5008        narrow: Option<Narrowing>,
5009        /// Whether a `DHEADER` precedes the entry count. Rule (14)
5010        /// exempts a map only when both halves are primitive and rule
5011        /// (15) delimits the rest, decided by
5012        /// `zerodds_ast_layout::xcdr::plan` through `MapDelimiter` so
5013        /// that the emitted bytes and the planned layout cannot
5014        /// disagree.
5015        delimited: bool,
5016        /// Whether the entries are written ascending by key: the map is held as its entries
5017        /// (`adapter::cst::RustMapForm::Entries`), which keep the order they were put in, and
5018        /// `MapEntryOrder::AscendingKey` asked for the other one.
5019        ascending: bool,
5020    },
5021    /// An array, with its element and its dimensions outermost first.
5022    Array {
5023        /// What each element is.
5024        element: Box<MemberWrite>,
5025        /// The dimensions, outermost first.
5026        dims: Vec<i128>,
5027        /// Whether a `DHEADER` precedes the elements. Rule (9).
5028        delimited: bool,
5029        /// Whether each element begins its own alignment block, as
5030        /// [`MemberWrite::Sequence`]'s. Arrays never carry an element
5031        /// length: only `sequence` has that axis.
5032        packed: bool,
5033    },
5034    /// An enumeration, written as the integer its bit bound calls for.
5035    Enumeration {
5036        /// The bit bound.
5037        bits: u16,
5038    },
5039    /// A bitmask, written in the holder Table 33 of XTypes 1.3
5040    /// §7.4.1.1.3.2 gives for its `@bit_bound`.
5041    ///
5042    /// # Not `Enumeration`, and not `Nested`
5043    ///
5044    /// Not [`MemberWrite::Enumeration`], although both are an integer in
5045    /// a holder: an enumeration's holder width goes through
5046    /// `EnumHolderWidth`, the axis M1 measured, and a bitmask's does
5047    /// not — `zerodds_ast_layout::xcdr::enumeration_bit_bound` says so
5048    /// in as many words. Sharing the shape would have let a setting
5049    /// meant for one construct move the other's octets.
5050    ///
5051    /// Not [`MemberWrite::Nested`] either, although the other twenty
5052    /// columns give a bitmask its own codec function. `adapter::cst`
5053    /// materialises a bitmask as `pub type Flags = u16;` — a type alias
5054    /// to a primitive, because a Rust `enum` cannot hold two bits at
5055    /// once — and there is no `impl` block to hang an `encode` on.
5056    Bitmask {
5057        /// The bound Table 33 sizes the holder by, from
5058        /// `zerodds_ast_layout::plan::scalar_write`.
5059        bits: u16,
5060        /// The positions the type names, one bit per position. A bit set
5061        /// inside the bound that this mask does not carry is refused on
5062        /// the way in.
5063        declared: u64,
5064    },
5065    /// Another aggregate, which encodes itself.
5066    ///
5067    /// The name is carried because the decode side has to write it:
5068    /// `Cell::decode(r)` names the type and `r.read_nested()` cannot.
5069    Nested {
5070        /// The generated Rust type's name.
5071        type_name: String,
5072        /// The suffix the nested type's codec functions carry: empty
5073        /// for the type's first codec, [`SECONDARY_SUFFIX`] for the
5074        /// second one beside it.
5075        ///
5076        /// Carried rather than recomputed at the call site: `write_call`
5077        /// and `read_whole` are handed a `MemberWrite` and no key, and a
5078        /// nested call that named `encode` while the enclosing body was
5079        /// the XCDR1 one would frame the member under version 2 inside a
5080        /// version 1 stream — a sample that decodes into wrong values
5081        /// with nothing reporting it.
5082        codec: &'static str,
5083        /// How the sample's PL_CDR1 step decision reaches this call, and
5084        /// which of the nested type's two methods it names under
5085        /// [`Extended::Fixed`].
5086        ///
5087        /// Carried for the same reason as `codec`, and read from the same
5088        /// key: `write_call` is handed the shape and nothing else, so a
5089        /// call that could not say whether the flag was threaded would
5090        /// frame every nested type of a per-sample plan with the short
5091        /// header, also inside a sample past RTI's step.
5092        extended: Extended,
5093    },
5094    /// A kind with no writer yet, named so the error can say which.
5095    Unsupported(&'static str),
5096}
5097
5098/// What one position does with a value its bound cannot hold.
5099///
5100/// DDS-XTypes 1.3 §7.2.2.7 attaches the behaviour to the position — a
5101/// member, a collection element, a map key, a union case — and not to
5102/// the type, so it is carried alongside the shape rather than derived
5103/// from it. One `typedef string<5>` may be used at two positions that
5104/// want different answers.
5105#[derive(Debug, Clone, PartialEq, Eq)]
5106pub(crate) struct Narrowing {
5107    /// What the receiving type holds.
5108    bound: i128,
5109    /// Which of the three §7.3.1.2.1.12 behaviours this position asks
5110    /// for.
5111    behaviour: TryConstruct,
5112    /// The value `USE_DEFAULT` substitutes, when that is the behaviour.
5113    ///
5114    /// Computed only for `USE_DEFAULT`: a structure's default is a walk
5115    /// of its own members, and computing one for every bounded string
5116    /// in the model would be a walk per string for a value nothing
5117    /// reads.
5118    default: Option<rust::Expr>,
5119}
5120
5121/// Where a type is being used, for the purpose of `try_construct`.
5122///
5123/// `pub(crate)`: see [`write_value_to`]. `emit::interface_rust` builds
5124/// one per field from `Ast::effective_try_construct` itself — an
5125/// operation parameter has no member declarator, so it always passes
5126/// `written: None`.
5127#[derive(Debug, Clone, Copy)]
5128pub(crate) struct Position {
5129    /// The behaviour this position asks for, already resolved through
5130    /// the alias chain by `Ast::effective_try_construct`.
5131    pub(crate) behaviour: TryConstruct,
5132    /// What the *member declarator* wrote, which `element_try_construct`
5133    /// needs for the one case §7.3.1.2.1.12 lets a member's annotation
5134    /// reach the elements: an array declared on the member.
5135    pub(crate) written: Option<TryConstruct>,
5136}
5137
5138fn member_of(
5139    ast: &Ast,
5140    type_: Id<TypeNode>,
5141    node: zerodds_ast_core::NodeId,
5142    key: LayoutKey,
5143    codec: &'static str,
5144) -> Option<(String, MemberWrite)> {
5145    // `all_members_of`, so a member the aggregate inherited is found
5146    // here instead of being skipped as though the plan had not named it.
5147    let member = ast
5148        .all_members_of(type_)
5149        .into_iter()
5150        .find(|id| ast.member(*id).is_ok_and(|m| m.node_id == node))?;
5151    let member = ast.member(member).ok()?;
5152    // The member's own `@try_construct`, resolved through the alias
5153    // chain — §7.2.2.7 puts the behaviour on the position, and this is
5154    // the member position.
5155    let written = member.facets.xtypes.as_ref().and_then(|x| x.try_construct);
5156    let position = Position {
5157        behaviour: ast.effective_try_construct(written, member.type_.resolved()),
5158        written,
5159    };
5160    // Types this member names are written relative to the module the
5161    // aggregate itself lands in.
5162    let from = module_of(ast, type_);
5163    let mut write = member.type_.resolved().map_or(
5164        MemberWrite::Unsupported("member with an unresolved type"),
5165        |target| write_of(ast, from, target, key, codec, position),
5166    );
5167    // §7.2.2.4.4.4.9 puts an explicit `@default` ahead of Table 9's, and
5168    // only the member position can carry one — a collection element has
5169    // no declarator to write it on.
5170    if let Some(target) = member.type_.resolved() {
5171        if let Some(narrow) = narrowing_of_mut(&mut write) {
5172            if narrow.behaviour == TryConstruct::UseDefault {
5173                narrow.default = crate::default_rust::member_value_default(
5174                    ast,
5175                    module_of(ast, type_),
5176                    member,
5177                    target,
5178                )
5179                .ok();
5180            }
5181        }
5182    }
5183    Some((member.name.clone(), external_write(ast, member, write)))
5184}
5185
5186fn external_write(ast: &Ast, member: &zerodds_ast::MemberNode, write: MemberWrite) -> MemberWrite {
5187    if crate::adapter::cst::is_external(ast, member) {
5188        MemberWrite::External(Box::new(write))
5189    } else {
5190        write
5191    }
5192}
5193
5194/// The narrowing of a shape, where the shape has one.
5195fn narrowing_of_mut(write: &mut MemberWrite) -> Option<&mut Narrowing> {
5196    match write {
5197        MemberWrite::String { narrow, .. }
5198        | MemberWrite::WString { narrow, .. }
5199        | MemberWrite::Sequence { narrow, .. }
5200        | MemberWrite::Map { narrow, .. } => narrow.as_mut(),
5201        _ => None,
5202    }
5203}
5204
5205/// The declaration one plan entry names.
5206///
5207/// `member_of` answers with the name and the writer shape; the default
5208/// value needs the declaration itself, for the `@default` it may carry.
5209fn member_node_of(
5210    ast: &Ast,
5211    type_: Id<TypeNode>,
5212    node: zerodds_ast_core::NodeId,
5213) -> Option<&zerodds_ast::MemberNode> {
5214    let member = ast
5215        .all_members_of(type_)
5216        .into_iter()
5217        .find(|id| ast.member(*id).is_ok_and(|m| m.node_id == node))?;
5218    ast.member(member).ok()
5219}
5220
5221/// What writing one type takes.
5222///
5223/// Recursive, because a sequence of sequences is written by writing the
5224/// outer length and then each inner sequence — the shape of the writer
5225/// follows the shape of the type, which is what keeps the two from
5226/// drifting apart.
5227///
5228/// `pub(crate)`: see [`write_value_to`]. Unlike [`member_of`], this
5229/// takes a bare `Id<TypeNode>` and not a member of some aggregate — an
5230/// operation parameter or an exception member is neither, and DDS-RPC
5231/// 1.0 §7.5.1.1.4-7 synthesizes structures for them that never reach the
5232/// model at all.
5233pub(crate) fn write_of(
5234    ast: &Ast,
5235    from: Id<zerodds_ast::ModuleNode>,
5236    target: Id<TypeNode>,
5237    key: LayoutKey,
5238    codec: &'static str,
5239    at: Position,
5240) -> MemberWrite {
5241    write_of_at(ast, from, target, key, codec, at, 0)
5242}
5243
5244/// How deep a member's writer may nest before the walk refuses the type.
5245///
5246/// The same 64 every reference walk in the tree carries;
5247/// `zerodds-wire-codec`'s `default.rs` and `zerodds-types`'
5248/// `dynamic/type_.rs` both stop there.
5249const MAX_WRITE_DEPTH: usize = 64;
5250
5251/// [`write_of`], carrying how many collection and typedef steps the walk
5252/// has already taken.
5253///
5254/// An aggregate is a leaf here — it writes through its own generated
5255/// codec under [`MemberWrite::Nested`] — so only the connectors can come
5256/// back round: a `sequence`'s element, an array's element, a `map`'s two
5257/// halves, a `typedef`'s target. A cycle among those is a finding the
5258/// model's invariant pass reports rather than a shape it rejects, and
5259/// without the counter this function read one forever.
5260///
5261/// zerodds-lint: recursion-depth 64 (sequence, array, map and typedef
5262/// nesting; bounded by `MAX_WRITE_DEPTH`, which returns
5263/// [`MemberWrite::Unsupported`] naming the construct — the same channel
5264/// every other refusal in this walk uses, so the emitter reports it by
5265/// name instead of writing a member it cannot spell)
5266fn write_of_at(
5267    ast: &Ast,
5268    from: Id<zerodds_ast::ModuleNode>,
5269    target: Id<TypeNode>,
5270    key: LayoutKey,
5271    codec: &'static str,
5272    at: Position,
5273    depth: usize,
5274) -> MemberWrite {
5275    if depth > MAX_WRITE_DEPTH {
5276        return MemberWrite::Unsupported("a type whose writer nests past the recursion bound");
5277    }
5278    let Ok(node) = ast.type_(target) else {
5279        return MemberWrite::Unsupported("member with a stale type handle");
5280    };
5281    // The narrowing a bound calls for, computed once for both bounded
5282    // shapes. `None` for an unbounded one: there is nothing a value can
5283    // fail to fit.
5284    let narrowing = |bound: Option<i128>| {
5285        bound.map(|bound| Narrowing {
5286            bound,
5287            behaviour: at.behaviour,
5288            default: if at.behaviour == TryConstruct::UseDefault {
5289                crate::default_rust::default_of(ast, from, target, &node.name).ok()
5290            } else {
5291                None
5292            },
5293        })
5294    };
5295    match &node.kind {
5296        TypeKind::Primitive(kind) => MemberWrite::Primitive(*kind),
5297        // A `wstring`. The default cell is a UInt32 **octet** count and
5298        // UTF-16 code units with no NUL — XTypes 1.3 §7.4.1.1.2 — and
5299        // it is one cell of eight, not one form of three. The three
5300        // binary axes are what a length prefix counts (C1), whether a
5301        // terminating NUL joins the run and is counted (C2), and how
5302        // wide one code unit is (C3); `WstringLengthPolicy` is their
5303        // product, `WstringLengthPolicy::ALL_CELLS` enumerates it, and
5304        // five of the eight cells carry the name of an implementation
5305        // measured writing them. The byte **order** of the units is a
5306        // fourth axis, `WstringUnitOrder`, with four points of its own,
5307        // because it crosses the length convention rather than
5308        // extending it.
5309        //
5310        // All four come out of the key, the way `MapDelimiter` and
5311        // `MapEntryOrder` do, and go into the emitted call as
5312        // arguments. A runtime default would be a second place the wire
5313        // format lives.
5314        //
5315        // The bound counts UTF-16 code units and not octets, which is
5316        // `Reader::fit_wstring` and not `Reader::fit_string`.
5317        TypeKind::String {
5318            width: CharWidth::WChar,
5319            bound,
5320        } => {
5321            let bound = bound.expr().and_then(|expr| ast.eval_integral(expr).ok());
5322            MemberWrite::WString {
5323                bound,
5324                narrow: narrowing(bound),
5325                policy: key.wstring_length,
5326                units: key.wstring_code_units,
5327                order: key.wstring_unit_order,
5328                // The member position. A `wstring` reached as a
5329                // collection element is rewritten to
5330                // `WirePosition::Element` by `element_writer` and
5331                // `element_reader`, which is where the `wchar` framing's
5332                // element answer is substituted as well — the nine
5333                // measured implementations answer the two positions
5334                // alike on all four `wstring` axes, and passing the
5335                // position is what keeps that a measurement.
5336                position: WirePosition::Member,
5337            }
5338        }
5339        TypeKind::String { bound, .. } => {
5340            let bound = bound.expr().and_then(|expr| ast.eval_integral(expr).ok());
5341            MemberWrite::String {
5342                bound,
5343                narrow: narrowing(bound),
5344            }
5345        }
5346        // The digit count comes out of the key the way `MapEntryOrder` and
5347        // the four `wstring` axes do, and goes into the emitted call as an
5348        // argument. A runtime default would be a second place the wire
5349        // format lives.
5350        TypeKind::Fixed { digits, scale } => MemberWrite::Fixed {
5351            digits: *digits,
5352            scale: *scale,
5353            count: key.fixed_digits,
5354        },
5355        TypeKind::Sequence { element, bound } => element.resolved().map_or_else(
5356            || MemberWrite::Unsupported("sequence with an unresolved element"),
5357            |element| {
5358                let bound = bound.expr().and_then(|expr| ast.eval_integral(expr).ok());
5359                // Whether each element carries its own length is the same
5360                // crossing `zerodds-wire-codec`'s sequence walk asks, so
5361                // the emitted bytes and the reference codec cannot part.
5362                let Some(carries_length) =
5363                    zerodds_ast_layout::xcdr::sequence_element_carries_length(ast, element, key)
5364                else {
5365                    return MemberWrite::Unsupported(
5366                        "a sequence element whose own length cannot be told",
5367                    );
5368                };
5369                MemberWrite::Sequence {
5370                    element: Box::new(write_of_at(
5371                        ast,
5372                        from,
5373                        element,
5374                        key,
5375                        codec,
5376                        inside(ast, target, at),
5377                        depth + 1,
5378                    )),
5379                    bound,
5380                    narrow: narrowing(bound),
5381                    delimited: zerodds_ast_layout::collection_is_delimited(ast, element, key),
5382                    packed: key.element_padding == ElementPadding::Packed,
5383                    carries_length,
5384                }
5385            },
5386        ),
5387        TypeKind::Array { element, dims } => element.resolved().map_or_else(
5388            || MemberWrite::Unsupported("array with an unresolved element"),
5389            |element| {
5390                // A dimension that does not evaluate is not a zero-length
5391                // array. Substituting one wrote no elements at all and
5392                // said the sample had been encoded.
5393                let Ok(dims) = dims
5394                    .iter()
5395                    .map(|dim| ast.eval_integral(*dim))
5396                    .collect::<Result<Vec<_>, _>>()
5397                else {
5398                    return MemberWrite::Unsupported("array whose length does not evaluate");
5399                };
5400                MemberWrite::Array {
5401                    element: Box::new(write_of_at(
5402                        ast,
5403                        from,
5404                        element,
5405                        key,
5406                        codec,
5407                        inside(ast, target, at),
5408                        depth + 1,
5409                    )),
5410                    delimited: zerodds_ast_layout::array_is_delimited(ast, element, key),
5411                    dims,
5412                    packed: key.element_padding == ElementPadding::Packed,
5413                }
5414            },
5415        ),
5416        // A `map<K, V>` is a sequence of pairs, §7.4.1.1.4, and the two
5417        // halves are two different types — so both are walked, at the
5418        // element position §7.3.1.2.1.12.4 gives them.
5419        TypeKind::Map {
5420            key: k,
5421            value,
5422            bound,
5423        } => {
5424            let (Some(k), Some(value)) = (k.resolved(), value.resolved()) else {
5425                return MemberWrite::Unsupported("map with an unresolved key or value");
5426            };
5427            // The same refusal the C emitter makes, and the plan's own:
5428            // a setting that asked for ascending entries over a key type
5429            // whose values have no order cannot be honoured, and falling
5430            // back to declaration order would be a flag that reported
5431            // success and changed no octet. This column reaches it only
5432            // through the plan — the order it writes is its `BTreeMap`'s
5433            // — which is why the crossing itself is not named here.
5434            if zerodds_ast_layout::plan::map_entry_ordering(ast, target, key).is_err() {
5435                return MemberWrite::Unsupported(
5436                    "a map whose key type has no value order, under --map-entries ascending-key",
5437                );
5438            }
5439            let bound = bound.expr().and_then(|expr| ast.eval_integral(expr).ok());
5440            let mut deps = zerodds_ast_layout::xcdr::DepCollector::default();
5441            let Ok(layout) = zerodds_ast_layout::xcdr::plan(ast, target, key, &mut deps) else {
5442                return MemberWrite::Unsupported("map whose layout does not plan");
5443            };
5444            let inside = inside(ast, target, at);
5445            let ascending = matches!(
5446                zerodds_ast_layout::plan::map_entry_ordering(ast, target, key),
5447                Ok(Some(_))
5448            );
5449            MemberWrite::Map {
5450                key: Box::new(write_of_at(ast, from, k, key, codec, inside, depth + 1)),
5451                value: Box::new(write_of_at(ast, from, value, key, codec, inside, depth + 1)),
5452                bound,
5453                ascending,
5454                narrow: narrowing(bound),
5455                // Rule (14) exempts a map only when both halves are
5456                // primitive; the plan reads that through `MapDelimiter`,
5457                // and asking it here is what keeps the emitted bytes and
5458                // the planned extent from disagreeing.
5459                delimited: layout.needs_dheader,
5460            }
5461        }
5462        // A bitmask is the unsigned integer Table 33 of §7.4.1.1.3.2
5463        // gives for its `@bit_bound`, and the width is the **plan's**
5464        // answer: `plan::scalar_write` is the crossing every other
5465        // column asks — `crates/lang-rules/src/emit/encode.rs` reaches
5466        // `ScalarWrite::Bitmask` through the same call — so the twenty
5467        // other columns' octets and this one's cannot part company by
5468        // this column computing a width of its own.
5469        //
5470        // Unlike an enumeration this does **not** write through a
5471        // generated codec: `adapter::cst` materialises a bitmask as
5472        // `pub type Flags = u16;`, and a type alias to a primitive has
5473        // no `impl` block for an `encode` to live in.
5474        TypeKind::Bitmask { .. } => {
5475            let Some(ScalarWrite::Bitmask { bits }) =
5476                zerodds_ast_layout::plan::scalar_write(ast, target, key, WcharPosition::Member)
5477            else {
5478                return MemberWrite::Unsupported("a bitmask the layout plan gives no holder for");
5479            };
5480            let Some(declared) = declared_bits(ast, target, bits) else {
5481                return MemberWrite::Unsupported(
5482                    "a bitmask with a bit position that does not evaluate",
5483                );
5484            };
5485            MemberWrite::Bitmask { bits, declared }
5486        }
5487        // An enumeration writes through its own generated codec, like any
5488        // other named type. Writing the holder straight from here emitted
5489        // `w.write_u32(self.coord_convention)` — the field is the
5490        // enumeration and not its holder, so the generated file did not
5491        // compile, and no enumeration had ever been a struct member in a
5492        // tree this emitter had been pointed at. `enumeration_encode_fn`
5493        // already produces the `match self { Self::X => 0, .. }` that
5494        // respects `@value`, and it takes the holder's width from the
5495        // same `key`, so nothing about the wire changes.
5496        TypeKind::Enumeration { .. } => match crate::adapter::cst::relative_path(ast, from, node) {
5497            Ok(path) => MemberWrite::Nested {
5498                type_name: path
5499                    .segments()
5500                    .iter()
5501                    .map(|s| s.as_str().to_string())
5502                    .collect::<Vec<_>>()
5503                    .join("::"),
5504                codec,
5505                extended: Extended::of(key),
5506            },
5507            Err(_) => MemberWrite::Unsupported("an enumeration the model has no module for"),
5508        },
5509        // The behaviour was already resolved through the alias chain by
5510        // `Ast::effective_try_construct`, so the position carries over
5511        // unchanged.
5512        TypeKind::Alias { aliased } => aliased.resolved().map_or_else(
5513            || MemberWrite::Unsupported("typedef with an unresolved target"),
5514            |aliased| write_of_at(ast, from, aliased, key, codec, at, depth + 1),
5515        ),
5516        // A union is written the same way, and for the same reason: it
5517        // has a generated codec of its own, and `union_codec` gives it
5518        // exactly the `encode(&self, w)` and `decode(r)` that
5519        // `MemberWrite::Nested` calls. Leaving it out of this arm did not
5520        // make a union member unwritable in some deeper sense — it made
5521        // the emitter refuse a type it had already generated the codec
5522        // for, which is what `NoWriter { member: "cov", kind: "union" }`
5523        // was: `FramedPose.cov` is a `CovMatrix`, an `@appendable` union
5524        // over an enumerated discriminator, and every one of its own
5525        // cases emitted fine.
5526        // A bitset joins them: `adapter::cst` materialises it as a
5527        // struct with one field per named bitfield, and `bitset_encode_fn`
5528        // gives that struct the `encode(&self, w)` and `decode(r)` this
5529        // shape calls. It was in the `other` arm below, which is what
5530        // `NoWriter { kind: "bitset" }` was.
5531        TypeKind::Structure { .. }
5532        | TypeKind::Valuetype { .. }
5533        | TypeKind::Union { .. }
5534        | TypeKind::Bitset { .. } => {
5535            // The path, not the name: `Nested` becomes `<path>::decode`
5536            // in the reader, and a sibling module's type does not resolve
5537            // by its simple name from here.
5538            match crate::adapter::cst::relative_path(ast, from, node) {
5539                Ok(path) => MemberWrite::Nested {
5540                    type_name: path
5541                        .segments()
5542                        .iter()
5543                        .map(|s| s.as_str().to_string())
5544                        .collect::<Vec<_>>()
5545                        .join("::"),
5546                    codec,
5547                    extended: Extended::of(key),
5548                },
5549                Err(_) => MemberWrite::Unsupported("a type the model has no module for"),
5550            }
5551        }
5552        other => MemberWrite::Unsupported(other.as_str()),
5553    }
5554}
5555
5556/// The positions a bitmask names, one bit per position.
5557///
5558/// The mask `Reader::read_bitmask` reduces a stream to. §7.4.1.1.3.2
5559/// puts a bit above the bound outside the bitmask, so a position at or
5560/// beyond the bound is not part of the mask; a bit set inside the bound
5561/// that the type does not name is not part of the value either, and the
5562/// reader keeps only the declared positions.
5563///
5564/// The same rule `bitmask_body` in `crates/lang-rules/src/emit/decode.rs`
5565/// emits inline for the other twenty columns — a second copy rather than
5566/// a shared call, because the two are compared against each other and a
5567/// shared helper would make them agree about a mistake.
5568fn declared_bits(ast: &Ast, type_: Id<TypeNode>, bound: u16) -> Option<u64> {
5569    let mut declared = 0u64;
5570    for bit in ast.bits_of(type_) {
5571        // `None` and not `continue`: a bit whose position does not
5572        // evaluate is a bit this mask cannot name, and leaving it out
5573        // would make the reader drop a bit the type does declare, for a
5574        // reason nothing states. The caller
5575        // turns this into `MemberWrite::Unsupported` naming the
5576        // construct.
5577        let position = ast.effective_bit_position(*bit)?;
5578        if position < bound && position < 64 {
5579            declared |= 1u64 << position;
5580        }
5581    }
5582    Some(declared)
5583}
5584
5585/// The position a collection's elements sit in.
5586///
5587/// §7.3.1.2.1.12.4 gives the elements their own behaviour —
5588/// `sequence<@try_construct(TRIM) String5, 10>` — and Table 17 keeps it
5589/// separate from the collection's own: the sequence's length overflow
5590/// and an element's bound overflow are two different failures, which is
5591/// the split OpenDDS's generated element loop implements.
5592fn inside(ast: &Ast, collection: Id<TypeNode>, at: Position) -> Position {
5593    Position {
5594        behaviour: ast.element_try_construct(collection, at.written),
5595        written: None,
5596    }
5597}
5598
5599/// The writer a primitive takes.
5600///
5601/// Exhaustive, so a primitive added to the model without a writer is a
5602/// compile error rather than a member that silently takes another one's.
5603const fn writer_name(kind: PrimitiveKind) -> &'static str {
5604    match kind {
5605        PrimitiveKind::Boolean => "write_bool",
5606        PrimitiveKind::Octet | PrimitiveKind::UInt8 => "write_u8",
5607        PrimitiveKind::Char => "write_char",
5608        // Member and element take different octets under TAO's framing,
5609        // so there is no positionless `write_wchar` to name here. This
5610        // arm is the member's; `element_writer_name` has the element's.
5611        PrimitiveKind::WChar => "write_wchar_member",
5612        PrimitiveKind::Int8 => "write_i8",
5613        PrimitiveKind::Int16 => "write_i16",
5614        PrimitiveKind::UInt16 => "write_u16",
5615        PrimitiveKind::Int32 => "write_i32",
5616        PrimitiveKind::UInt32 => "write_u32",
5617        PrimitiveKind::Int64 => "write_i64",
5618        PrimitiveKind::UInt64 => "write_u64",
5619        PrimitiveKind::Float32 => "write_f32",
5620        PrimitiveKind::Float64 => "write_f64",
5621        PrimitiveKind::Float128 => "write_f128",
5622    }
5623}
5624
5625const fn reader_name(kind: PrimitiveKind) -> &'static str {
5626    match kind {
5627        PrimitiveKind::Boolean => "read_bool",
5628        PrimitiveKind::Octet | PrimitiveKind::UInt8 => "read_u8",
5629        PrimitiveKind::Char => "read_char",
5630        // As `writer_name`: the member's reader.
5631        PrimitiveKind::WChar => "read_wchar_member",
5632        PrimitiveKind::Int8 => "read_i8",
5633        PrimitiveKind::Int16 => "read_i16",
5634        PrimitiveKind::UInt16 => "read_u16",
5635        PrimitiveKind::Int32 => "read_i32",
5636        PrimitiveKind::UInt32 => "read_u32",
5637        PrimitiveKind::Int64 => "read_i64",
5638        PrimitiveKind::UInt64 => "read_u64",
5639        PrimitiveKind::Float32 => "read_f32",
5640        PrimitiveKind::Float64 => "read_f64",
5641        PrimitiveKind::Float128 => "read_f128",
5642    }
5643}
5644
5645fn call(method: &str, args: Vec<rust::Expr>) -> rust::Expr {
5646    rust::Expr::MethodCall {
5647        receiver: Box::new(rust::Expr::Path(Path::single("r"))),
5648        method: Ident::new(method),
5649        args,
5650    }
5651}
5652
5653/// Bind a framing header so it can be patched once its value is written.
5654/// Bind an opened header, propagating the writer's refusal.
5655///
5656/// The three operations this opens — `write_pid`, `write_pid_extended`
5657/// and `write_emheader` — refuse a member id their header's field cannot
5658/// carry (XTypes 1.3 §7.2.2.4.4.4.4 for the 28 bits, §7.4.1.2.2 for the
5659/// reserved short ids), so each returns a `Result` and the `?` carries
5660/// the refusal out of the generated `encode`. Nothing here can trip it
5661/// today, because the plan chose the id and the form together; it is
5662/// there because a generated codec must not be the one place a masked
5663/// id reaches the wire.
5664fn bind_header(name: &str, method: &str, args: Vec<rust::Expr>) -> rust::Statement {
5665    rust::Statement::Let {
5666        name: Ident::new(name),
5667        type_: None,
5668        value: Some(rust::Expr::Try(Box::new(rust::Expr::MethodCall {
5669            receiver: Box::new(rust::Expr::Path(Path::single("w"))),
5670            method: Ident::new(method),
5671            args,
5672        }))),
5673        mutable: false,
5674    }
5675}
5676
5677fn call_statement(method: &str, args: Vec<rust::Expr>) -> rust::Statement {
5678    rust::Statement::Semi(rust::Expr::MethodCall {
5679        receiver: Box::new(rust::Expr::Path(Path::single("w"))),
5680        method: Ident::new(method),
5681        args,
5682    })
5683}
5684
5685/// The same call, with the `?` a fallible one needs.
5686fn try_call_statement(method: &str, args: Vec<rust::Expr>) -> rust::Statement {
5687    rust::Statement::Semi(rust::Expr::Try(Box::new(rust::Expr::MethodCall {
5688        receiver: Box::new(rust::Expr::Path(Path::single("w"))),
5689        method: Ident::new(method),
5690        args,
5691    })))
5692}
5693
5694/// `ParameterRole::Member` or `ParameterRole::Optional`, as a path.
5695fn parameter_role(role: &str) -> rust::Expr {
5696    rust::Expr::Path(Path::new(&["ParameterRole", role]))
5697}
5698
5699/// One `@optional` member's read, into the `Option<T>` field.
5700///
5701/// The mirror of [`write_optional_member`], marker for marker:
5702///
5703/// - rule (20): the BOOLEAN `<is_present>` decides;
5704/// - rule (19): a declared parameter length of zero is the absence
5705///   (§7.4.1.2), and `finish_pid` still runs either way — the parameter
5706///   occupies its place in the list whether or not it carries a value,
5707///   and skipping the close would leave the alignment origin inside it;
5708/// - inside a `@mutable` type: the parameter is present, because a
5709///   parameter list that omitted it would not have brought the reader
5710///   here at all.
5711fn read_optional_member(
5712    plan: &zerodds_ast_layout::plan::MemberPlan,
5713    name: &str,
5714    kind: &MemberWrite,
5715) -> Result<Vec<rust::Statement>, EncodeError> {
5716    let some = |value: rust::Expr| rust::Expr::Call {
5717        callee: Box::new(rust::Expr::Path(Path::single("Some"))),
5718        args: vec![value],
5719    };
5720    let read = rust::Expr::Try(Box::new(read_for(name, kind)?));
5721    match plan.header {
5722        MemberHeader::OptionalFlag => Ok(vec![let_statement(
5723            name,
5724            rust::Expr::If {
5725                condition: Box::new(rust::Expr::Try(Box::new(call("read_bool", Vec::new())))),
5726                then: rust::Block {
5727                    statements: vec![rust::Statement::Tail(some(read))],
5728                },
5729                otherwise: Some(rust::Block {
5730                    statements: vec![rust::Statement::Tail(rust::Expr::Path(Path::single(
5731                        "None",
5732                    )))],
5733                }),
5734            },
5735        )]),
5736        MemberHeader::OptionalCdr1 { .. } => Ok(vec![
5737            let_statement(
5738                "pid",
5739                rust::Expr::Try(Box::new(call(
5740                    "read_pid_as",
5741                    vec![parameter_role("Optional")],
5742                ))),
5743            ),
5744            let_statement(
5745                name,
5746                rust::Expr::If {
5747                    condition: Box::new(rust::Expr::Binary {
5748                        op: rust::BinaryOp::Eq,
5749                        lhs: Box::new(rust::Expr::Field {
5750                            receiver: Box::new(rust::Expr::Path(Path::single("pid"))),
5751                            name: Ident::new("length"),
5752                        }),
5753                        rhs: Box::new(rust::Expr::Literal(Literal::int(0))),
5754                    }),
5755                    then: rust::Block {
5756                        statements: vec![rust::Statement::Tail(rust::Expr::Path(Path::single(
5757                            "None",
5758                        )))],
5759                    },
5760                    otherwise: Some(rust::Block {
5761                        statements: vec![rust::Statement::Tail(some(read))],
5762                    }),
5763                },
5764            ),
5765            rust::Statement::Semi(rust::Expr::Try(Box::new(call(
5766                "finish_pid",
5767                vec![rust::Expr::Path(Path::single("pid"))],
5768            )))),
5769        ]),
5770        _ => {
5771            let mut out = match plan.header {
5772                MemberHeader::PidCdr1 { .. } => vec![let_statement(
5773                    "pid",
5774                    rust::Expr::Try(Box::new(call("read_pid", Vec::new()))),
5775                )],
5776                MemberHeader::EmHeader { .. } => vec![let_statement(
5777                    "emheader",
5778                    rust::Expr::Try(Box::new(call("read_emheader", Vec::new()))),
5779                )],
5780                _ => Vec::new(),
5781            };
5782            out.push(let_statement(name, some(read)));
5783            if matches!(plan.header, MemberHeader::PidCdr1 { .. }) {
5784                out.push(rust::Statement::Semi(rust::Expr::Try(Box::new(call(
5785                    "finish_pid",
5786                    vec![rust::Expr::Path(Path::single("pid"))],
5787                )))));
5788            }
5789            Ok(out)
5790        }
5791    }
5792}
5793
5794/// One `field: local` pair of the struct literal a decoder ends with.
5795///
5796/// Both halves go through `escape_keyword`, and that is the whole point:
5797/// the field is a member named in IDL and the local is the binding
5798/// `let_statement` made for it, so a member called `type` needs `r#type`
5799/// on both sides. Escaping only the field produced `r#type: type`, which
5800/// is a Rust syntax error — the generated file did not compile, and the
5801/// three call sites that built this pair by hand had each got it the
5802/// same way wrong.
5803fn field_binding(name: &str) -> (Ident, rust::Expr) {
5804    let escaped = rust::escape_keyword(name);
5805    (
5806        Ident::new(&escaped),
5807        rust::Expr::Path(Path::single(&escaped)),
5808    )
5809}
5810
5811fn let_statement(name: &str, value: rust::Expr) -> rust::Statement {
5812    rust::Statement::Let {
5813        name: Ident::new(&rust::escape_keyword(name)),
5814        type_: None,
5815        value: Some(value),
5816        mutable: false,
5817    }
5818}
5819
5820/// `#[allow(<lint>)]`.
5821fn allow_lint(lint: &str) -> rust::Attr {
5822    rust::Attr {
5823        path: Path::single("allow"),
5824        args: vec![rust::AttrArg::Word(Ident::new(lint))],
5825    }
5826}
5827
5828/// `impl Default` for an aggregate, from the values XTypes 1.3 Table 9
5829/// and `@default` give its members.
5830///
5831/// Not `#[derive(Default)]`: Rust's default and the specification's are
5832/// two different things wherever a member is an enumeration — Rust has
5833/// none without a `#[default]` attribute, and §7.3.1.2.1.10 names the
5834/// first enumerator or the one `@default_literal` picks. The emitter
5835/// already computes the right value for every member, and this is where
5836/// a caller can reach it.
5837///
5838/// # Errors
5839/// [`EncodeError`] naming the member whose default the model cannot
5840/// give.
5841fn default_impl(ast: &Ast, type_: Id<TypeNode>) -> Result<Option<rust::Item>, EncodeError> {
5842    let Ok(node) = ast.type_(type_) else {
5843        return Ok(None);
5844    };
5845    // A bitset joins them, and for the same reason: it materialises as
5846    // a struct, so `Default::default()` on one is a thing a caller can
5847    // write and used to be `the trait bound `BS: Default` is not
5848    // satisfied`. A valuetype is deliberately not here — it was not
5849    // before — and a bitmask cannot be: `impl Default for u16` is not
5850    // this crate's to write.
5851    if !matches!(
5852        node.kind,
5853        TypeKind::Structure { .. } | TypeKind::Union { .. } | TypeKind::Bitset { .. }
5854    ) {
5855        return Ok(None);
5856    }
5857    let from = node.module;
5858    // Named from inside its own module, which is the plain name.
5859    let self_type = rust::Type::named(Path::single(&rust::escape_keyword(&node.name)));
5860    let value = crate::default_rust::default_of(ast, from, type_, &node.name)?;
5861    Ok(Some(rust::Item::Impl(rust::Impl {
5862        doc: Doc::none(),
5863        // Where every member's Table 9 value happens to be Rust's own
5864        // default, clippy sees a derivable impl. It is written out on
5865        // purpose (see above), so the allowance is the generator's and
5866        // sits on the impl, not in each crate that includes the text.
5867        attrs: vec![allow_lint("clippy::derivable_impls")],
5868        generics: Vec::new(),
5869        trait_: Some(rust::Type::named(Path::single("Default"))),
5870        self_type: self_type.clone(),
5871        items: vec![rust::Item::Function(rust::Function {
5872            doc: Doc::none(),
5873            attrs: Vec::new(),
5874            visibility: Visibility::Private,
5875            name: Ident::new("default"),
5876            generics: Vec::new(),
5877            params: Vec::new(),
5878            returns: Some(rust::Type::named(Path::single("Self"))),
5879            body: Some(rust::Block {
5880                statements: vec![rust::Statement::Tail(value)],
5881            }),
5882        })],
5883    })))
5884}
5885
5886/// The module a type is declared in, for naming other types relative to
5887/// it. A stale handle falls back to the root, where a relative path is
5888/// the plain name — the same thing the emitter wrote before it named
5889/// anything relatively.
5890/// `self`, then `.parent` once per structure between `type_` and the one that declares
5891/// `member`: the base is the first member `parent` of the derived structure (XTypes 1.3
5892/// §7.5.1.4.1.3).
5893fn holder_of(ast: &Ast, type_: Id<TypeNode>, member: NodeId) -> rust::Expr {
5894    let mut place = rust::Expr::Path(Path::single("self"));
5895    for _ in 0..crate::emit::inheritance_depth(ast, type_, member) {
5896        place = rust::Expr::Field {
5897            receiver: Box::new(place),
5898            name: Ident::new(crate::adapter::cst::PARENT),
5899        };
5900    }
5901    place
5902}
5903
5904/// A literal of `type_` from the fields of its own members and those of its bases, each
5905/// level nested in the `parent` of the one above it. `rest` is what a level not every
5906/// member of which is named takes the others from.
5907fn nested_struct_literal(
5908    ast: &Ast,
5909    type_: Id<TypeNode>,
5910    entries: Vec<(NodeId, (Ident, rust::Expr))>,
5911    rest: &impl Fn() -> Option<Box<rust::Expr>>,
5912) -> rust::Expr {
5913    let deepest = entries
5914        .iter()
5915        .map(|(node, _)| crate::emit::inheritance_depth(ast, type_, *node))
5916        .max()
5917        .unwrap_or(0);
5918    let mut inner: Option<rust::Expr> = None;
5919    for depth in (0..=deepest).rev() {
5920        let mut fields: Vec<(Ident, rust::Expr)> = Vec::new();
5921        if let Some(parent) = inner.take() {
5922            fields.push((Ident::new(crate::adapter::cst::PARENT), parent));
5923        }
5924        for (node, field) in &entries {
5925            if crate::emit::inheritance_depth(ast, type_, *node) == depth {
5926                fields.push(field.clone());
5927            }
5928        }
5929        let path = if depth == 0 {
5930            Path::single("Self")
5931        } else {
5932            let owner = crate::emit::base_at(ast, type_, depth);
5933            ast.type_(owner)
5934                .ok()
5935                .and_then(|node| {
5936                    crate::adapter::cst::relative_path(ast, module_of(ast, type_), node).ok()
5937                })
5938                .unwrap_or_else(|| Path::single("Self"))
5939        };
5940        inner = Some(rust::Expr::StructLit {
5941            path,
5942            fields,
5943            base: rest(),
5944        });
5945    }
5946    inner.unwrap_or(rust::Expr::StructLit {
5947        path: Path::single("Self"),
5948        fields: Vec::new(),
5949        base: rest(),
5950    })
5951}
5952
5953fn module_of(ast: &Ast, type_: Id<TypeNode>) -> Id<zerodds_ast::ModuleNode> {
5954    ast.type_(type_)
5955        .map_or_else(|_| ast.root_module(), |node| node.module)
5956}
5957
5958fn alloc_format(error: &impl core::fmt::Debug) -> String {
5959    format!("{error:?}")
5960}
5961
5962/// Classify what the planner said: a clause of the specification, or a
5963/// gap of this tree.
5964///
5965/// The planner already carries the distinction in its own type —
5966/// `LayoutError::RecursionOutsideCollection` names IDL 4.2
5967/// §7.4.1.4.4.4.4 — and it was lost here, where every reason became one
5968/// string. Read the variant, never the message: a classification that
5969/// greps its own error text breaks the first time the text is reworded.
5970pub(crate) fn plan_refusal(error: zerodds_ast_layout::plan::LayoutError) -> EncodeError {
5971    match error {
5972        LayoutError::RecursionOutsideCollection { .. } => EncodeError::SpecRefuses {
5973            detail: format!("{error}"),
5974        },
5975        other => EncodeError::NoLayout {
5976            detail: alloc_format(&other),
5977        },
5978    }
5979}
5980
5981/// `_ => Err(WireError::refused(r.position(), "<what>"))`: the arm a
5982/// generated decoder ends a match with when the octets name nothing the
5983/// type declares.
5984fn refusal_arm(what: &str) -> rust::MatchArm {
5985    rust::MatchArm {
5986        pattern: rust::Pattern::Wildcard,
5987        guard: None,
5988        body: refused_err("r", what),
5989    }
5990}
5991
5992/// `Err(WireError::refused(<sink>.position(), "<what>"))`, the value a
5993/// generated codec returns for octets or a sample it cannot carry.
5994fn refused_err(sink: &str, what: &str) -> rust::Expr {
5995    rust::Expr::Call {
5996        callee: Box::new(rust::Expr::Path(Path::single("Err"))),
5997        args: vec![rust::Expr::Call {
5998            callee: Box::new(rust::Expr::Path(Path::new(&["WireError", "refused"]))),
5999            args: vec![
6000                rust::Expr::MethodCall {
6001                    receiver: Box::new(rust::Expr::Path(Path::single(sink))),
6002                    method: Ident::new("position"),
6003                    args: Vec::new(),
6004                },
6005                rust::Expr::Literal(Literal::string(what)),
6006            ],
6007        }],
6008    }
6009}