Skip to main content

zerodds_lang_rules/emit/
decode.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! C decoder bodies, built from the same layout plan. Spec §5, §6.2.
4//!
5//! The counterpart of [`crate::encode_c`], which walks the plan and
6//! writes. This walks it and reads. Not a second reading of the model,
7//! for the reason the encoder gives: two walks that decided the framing
8//! separately would agree with each other only until one of them
9//! changed.
10//!
11//! ## Why the column needs this at all
12//!
13//! Encoding is measured by comparing bytes, and a byte comparison
14//! passes a decoder that is wrong in the same way its own encoder is
15//! wrong. A round trip through one implementation passes such a defect
16//! every time; a round trip through two does not — and the C decoder
17//! and the Rust decoder are two.
18//! `docs/specs/zerodds-interop-suite-1.0.md` §4, P2.
19//!
20//! ## Where reading needs a decision writing did not
21//!
22//! The same three that `crates/wire-probe/src/decode.rs` names for the
23//! reflective reader, and they are why this file is longer than the
24//! encoder:
25//!
26//! - **A union's member.** The writer takes the member the value names;
27//!   the reader has only the discriminator, so the generated code
28//!   compares it against every case label, falls back to `default:`,
29//!   and reads no member at all where neither matches.
30//! - **A `@mutable` aggregate is driven by its headers and not by the
31//!   plan.** A writer may order the members as it likes and leave any
32//!   `@optional` one out, so the generated code is a dispatch on the
33//!   member id rather than a straight line.
34//!
35//! ## What C forces that Rust does not
36//!
37//! - **Somewhere to put a string.** An IDL `string` is a
38//!   `const char *` and a `sequence<T>` is a count beside a pointer, so
39//!   the read direction needs storage the write direction did not. It
40//!   comes from `zdw_reader_alloc`, out of a buffer the caller set with
41//!   `zdw_reader_set_heap` — the same discipline as the writer's output
42//!   buffer, and for the same reason: an embedded target decides where
43//!   memory comes from.
44//! - **Refusals the runtime cannot see.** An integer that names no
45//!   enumerator and a member that never arrived are refusals of the
46//!   *type*, which `runtime/c` does not know. `zdw_reader_refuse` is
47//!   how the generated code says so; without it the decoder would hand
48//!   its caller a value the type does not admit.
49
50use crate::adapter::c::{ordered_types, ordered_types_from, ordered_types_from_pruning};
51use crate::emit::encode::CodecOptions;
52use crate::emit::{CodecTarget, Constant, Handle, Handoff, Routine, Scalar};
53use crate::encode::EncodeError;
54use zerodds_ast::{
55    Ast, CaseLabel, CharWidth, Extensibility, PrimitiveKind, TryConstruct, TypeKind, TypeNode,
56};
57use zerodds_ast_core::Id;
58use zerodds_ast_layout::key::{
59    ElementPadding, FixedDigitCount, LayoutKey, WcharPosition, WcharWidth, WstringCodeUnits,
60    WstringLengthCounts, WstringLengthPolicy, WstringUnitOrder, WstringUnitWidth,
61};
62use zerodds_ast_layout::plan::{Extent, MemberHeader, ScalarWrite, scalar_write};
63use zerodds_ast_layout::xcdr::DepCollector;
64use zerodds_lang_cst::common::{Doc, Literal};
65
66/// The decoder function name for a type.
67///
68/// Named by the IDL kind for a primitive, for the reason
69/// [`crate::encode_c::codec_name`] gives: `double` and `long double`
70/// share a C type and do not share a wire form.
71#[must_use]
72pub fn decoder_name<T: CodecTarget>(ast: &Ast, id: Id<TypeNode>) -> String {
73    let name = match ast.type_(id).map(|node| node.kind.clone()) {
74        Ok(TypeKind::Primitive(kind)) => {
75            format!("zd_dec_p_{}", kind.as_str().replace(' ', "_"))
76        }
77        _ => format!("zd_dec_{}", T::type_name(ast, id)),
78    };
79    // As `crate::emit::encode::codec_name`: the published spelling, in
80    // one place. See [`CodecTarget::published_name`].
81    T::published_name(name)
82}
83
84/// Build the decoder for every type in the model, under one plan.
85///
86/// Existing data entry points keep their published names and initialization.
87///
88/// # Errors
89/// [`EncodeError`] when the layout cannot be planned or a construct has
90/// no C reader — never a body that leaves a member out.
91pub fn decoder<T: CodecTarget>(
92    ast: &Ast,
93    key: LayoutKey,
94    options: CodecOptions,
95) -> Result<Vec<T::Decl>, EncodeError> {
96    decoder_over::<T>(
97        ast,
98        &ordered_types(ast),
99        &crate::emit::encode::sample_types(ast),
100        key,
101        options,
102    )
103}
104
105/// [`decoder`] without the types in `excluded`. See
106/// [`crate::emit::encode::codec_excluding`].
107///
108/// # Errors
109/// As [`decoder`].
110pub fn decoder_excluding<T: CodecTarget>(
111    ast: &Ast,
112    key: LayoutKey,
113    options: CodecOptions,
114    excluded: &[Id<TypeNode>],
115    also_reaching: &[Id<TypeNode>],
116) -> Result<Vec<T::Decl>, EncodeError> {
117    if excluded.is_empty() {
118        return decoder::<T>(ast, key, options);
119    }
120    let (types, entries) = crate::emit::encode::kept_after_excluding(ast, excluded, also_reaching);
121    decoder_over::<T>(ast, &types, &entries, key, options)
122}
123
124/// The decoder for the types one root reaches, and no others.
125///
126/// What a probe's translation unit wants: a decoder for a type the root
127/// cannot reach is a `static` function nothing calls, and a generated
128/// file that warns is one whose warnings stop being read.
129///
130/// # Errors
131/// As [`decoder`].
132pub fn decoder_from<T: CodecTarget>(
133    ast: &Ast,
134    root: Id<TypeNode>,
135    key: LayoutKey,
136    options: CodecOptions,
137) -> Result<Vec<T::Decl>, EncodeError> {
138    decoder_over::<T>(
139        ast,
140        &ordered_types_from(ast, &[root]),
141        &[root],
142        key,
143        options,
144    )
145}
146
147/// The decoder for the types several roots reach, each function once.
148///
149/// # Errors
150/// As [`decoder`].
151pub fn decoder_from_many<T: CodecTarget>(
152    ast: &Ast,
153    roots: &[Id<TypeNode>],
154    key: LayoutKey,
155    options: CodecOptions,
156) -> Result<Vec<T::Decl>, EncodeError> {
157    decoder_over::<T>(ast, &ordered_types_from(ast, roots), roots, key, options)
158}
159
160/// [`decoder_from_many`] without the types in `excluded`; see
161/// [`crate::emit::encode::codec_from_many_excluding`].
162///
163/// # Errors
164/// As [`decoder`].
165pub fn decoder_from_many_excluding<T: CodecTarget>(
166    ast: &Ast,
167    roots: &[Id<TypeNode>],
168    key: LayoutKey,
169    options: CodecOptions,
170    excluded: &[Id<TypeNode>],
171) -> Result<Vec<T::Decl>, EncodeError> {
172    let types = ordered_types_from_pruning(ast, roots, excluded);
173    decoder_over::<T>(ast, &types, roots, key, options)
174}
175
176/// The distinct name of the bounded inverse key-holder entry.
177#[must_use]
178pub fn key_holder_decoder_name<T: CodecTarget>(ast: &Ast, root: Id<TypeNode>) -> String {
179    T::published_name(format!("zd_keyread_{}", decoder_name::<T>(ast, root)))
180}
181
182/// Read a ForcedFinal PLAIN_CDR2-BE struct holder with scalar/string/enum keys.
183/// The shared key plan decides root selection and member order. Nested
184/// aggregates, aliases, collections and indirect keys explicitly refuse until
185/// their full key context can be carried, never falling through to data reads.
186/// Non-key fields come from the target's existing default materializer.
187///
188/// # Errors
189/// A missing plan, unsupported root/member/dialect or target initialization.
190pub fn key_holder_decoder_from<T: CodecTarget>(
191    ast: &Ast,
192    root: Id<TypeNode>,
193    key: LayoutKey,
194    options: CodecOptions,
195) -> Result<Option<Vec<T::Decl>>, EncodeError> {
196    let refuse = |detail: &str| EncodeError::NoLayout {
197        detail: detail.to_string(),
198    };
199    if key != LayoutKey::key(zerodds_ast_layout::Encoding::PlainCdr2) {
200        return Err(refuse(
201            "key holder inverse supports the default ForcedFinal PLAIN_CDR2-BE key dialect only",
202        ));
203    }
204    if !matches!(
205        ast.type_(root).map(|node| &node.kind),
206        Ok(TypeKind::Structure { .. })
207    ) {
208        return Err(refuse(
209            "key holder inverse currently supports struct roots only",
210        ));
211    }
212    let keys = zerodds_ast_layout::keyhash::key_members_with(
213        ast,
214        root,
215        key.key_holder_nesting,
216        key.key_holder_order,
217    )
218    .map_err(crate::encode::plan_refusal)?;
219    if keys.is_empty() {
220        return Ok(None);
221    }
222    let layout = zerodds_ast_layout::keyhash::key_holder_plan_with(
223        ast,
224        root,
225        key,
226        &mut DepCollector::default(),
227    )
228    .map_err(crate::encode::plan_refusal)?;
229    let members = slots::<T>(ast, root, &layout)?;
230    // key_members_with includes descendant member identities. Diagnose an
231    // unsupported nested context before comparing the direct slot count.
232    for slot in &members {
233        if slot.optional
234            || slot.external
235            || slot.indirect
236            || !matches!(
237                ast.type_(slot.type_).map(|node| &node.kind),
238                Ok(TypeKind::Primitive(_) | TypeKind::String { .. } | TypeKind::Enumeration { .. })
239            )
240        {
241            return Err(refuse(&format!(
242                "key holder inverse member `{}` requires unsupported nested, alias, collection or indirect key context",
243                slot.field
244            )));
245        }
246    }
247    if members.len() != keys.len() || layout.needs_dheader {
248        return Err(refuse(
249            "key holder inverse could not describe every planned key member",
250        ));
251    }
252    let mut declarations = Vec::new();
253    let mut emitted = Vec::new();
254    let mut statements = Vec::new();
255    for slot in &members {
256        let name = key_holder_decoder_name::<T>(ast, slot.type_);
257        if !emitted.contains(&name) {
258            let body = body_of::<T>(ast, slot.type_, key, options, &name)?
259                .ok_or_else(|| refuse("key holder inverse has no member reader"))?;
260            declarations.push(initialized_key_reader::<T>(
261                ast,
262                slot.type_,
263                name.clone(),
264                body,
265            )?);
266            emitted.push(name.clone());
267        }
268        statements.push(T::read_call(
269            &name,
270            arrow::<T>("v", &slot.field),
271            Handoff::Value,
272            T::constant(Constant::TryConstructDiscard),
273        ));
274    }
275    declarations.push(initialized_key_reader::<T>(
276        ast,
277        root,
278        key_holder_decoder_name::<T>(ast, root),
279        T::block(statements),
280    )?);
281    Ok(Some(declarations))
282}
283
284fn initialized_key_reader<T: CodecTarget>(
285    ast: &Ast,
286    id: Id<TypeNode>,
287    name: String,
288    body: T::Block,
289) -> Result<T::Decl, EncodeError> {
290    let initializer = T::default_into(ast, id, deref::<T>("v"))?;
291    T::initialized_reader(
292        Routine {
293            doc: Doc::none(),
294            name,
295            sample: T::value_type(ast, id),
296            held: Handoff::Value,
297            writing: false,
298            tolerate_unused: false,
299            extra: vec![(T::handle(Handle::TryConstruct), "zd_tc".to_string())],
300            body: Some(with_tc_used::<T>(body)),
301        },
302        initializer,
303    )
304    .ok_or_else(|| EncodeError::NoLayout {
305        detail: "target does not support explicit key-holder reader initialization".to_string(),
306    })
307}
308
309fn decoder_over<T: CodecTarget>(
310    ast: &Ast,
311    types: &[Id<TypeNode>],
312    entries: &[Id<TypeNode>],
313    key: LayoutKey,
314    options: CodecOptions,
315) -> Result<Vec<T::Decl>, EncodeError> {
316    crate::encode::axes_the_codecs_carry_for::<T>(key)?;
317    let mut prototypes = Vec::new();
318    let mut bodies = Vec::new();
319    let mut emitted: Vec<String> = Vec::new();
320    // As `crate::emit::encode::codec_over`: an octet decoder only a bulk
321    // read would have called is not emitted.
322    let uncalled = crate::emit::encode::called_only_in_bulk(
323        ast,
324        types,
325        entries,
326        T::bulk_octets_read("v", name::<T>("zd_n")).is_some(),
327    );
328
329    for id in types.iter().copied() {
330        if uncalled.contains(&id) {
331            continue;
332        }
333        let name = decoder_name::<T>(ast, id);
334        if emitted.contains(&name) {
335            continue;
336        }
337        let Some(body) = body_of::<T>(ast, id, key, options, &name)? else {
338            continue;
339        };
340        emitted.push(name.clone());
341        // A target with no forward declarations answers `None` to the
342        // prototype and the loop emits only the body.
343        prototypes.extend(function::<T>(ast, id, key, &name, None));
344        bodies.extend(function::<T>(
345            ast,
346            id,
347            key,
348            &name,
349            Some(with_tc_used::<T>(body)),
350        ));
351    }
352
353    prototypes.extend(bodies);
354    // Not the Table 9 defaulters. Which helper a target needs for an
355    // absent member — and whether it needs one at all — is the
356    // materialiser's answer, and the C binding prepends C's after this
357    // returns.
358    Ok(prototypes)
359}
360
361/// The body, with `zd_tc` marked as read.
362///
363/// Most decoders have nothing to do with the behaviour — a `long` has no
364/// bound to miss — and the C probe is built with `-Wall -Wextra
365/// -Werror`, where an unused parameter fails the build. `(void)zd_tc;`
366/// is valid whether or not the body goes on to use it, so it is emitted
367/// unconditionally rather than from a guess about which bodies read it.
368fn with_tc_used<T: CodecTarget>(body: T::Block) -> T::Block {
369    let mut statements = T::mark_used("zd_tc");
370    statements.extend(T::into_statements(body));
371    T::block(statements)
372}
373
374/// `static void <name>(zdw_reader *r, <T> *v, zdw_try_construct zd_tc)`,
375/// with or without a body.
376///
377/// The value is not `const` here and is in the encoder, which is the
378/// only difference in the two signatures. An array parameter is the
379/// array itself for the reason the encoder gives: C adjusts it to a
380/// pointer to its first element, and `(*v)[i][j]` would otherwise need
381/// parentheses at every use.
382fn function<T: CodecTarget>(
383    ast: &Ast,
384    id: Id<TypeNode>,
385    key: LayoutKey,
386    name: &str,
387    body: Option<T::Block>,
388) -> Option<T::Decl> {
389    T::routine(Routine {
390        doc: Doc::none(),
391        name: name.to_string(),
392        sample: T::value_type(ast, id),
393        held: if is_array(ast, id, 0) {
394            Handoff::Array
395        } else {
396            Handoff::Value
397        },
398        writing: false,
399        // The reading counterpart of `emit::encode::maybe_unused`: a
400        // `Char16` read inline at every element site leaves this
401        // function without a caller under TAO's framing.
402        tolerate_unused: crate::emit::encode::maybe_unused(ast, id, key),
403        // What this *position* does with a value the type cannot hold.
404        // A parameter and not a property of the type, because
405        // DDS-XTypes 1.3 §7.2.2.7 attaches the behaviour to the member,
406        // the element, the map key or the discriminator — and one
407        // `typedef string<5>` may be used at two positions that want
408        // different answers.
409        extra: vec![(T::handle(Handle::TryConstruct), "zd_tc".to_string())],
410        body,
411    })
412}
413
414/// Whether a type is a C array once typedefs are seen through.
415///
416/// zerodds-lint: recursion-depth 64 (typedef chains; bounded by the
417/// `depth > 64` check, past which the answer is `false` — the
418/// destination form a non-array takes)
419fn is_array(ast: &Ast, id: Id<TypeNode>, depth: usize) -> bool {
420    if depth > 64 {
421        return false;
422    }
423    match ast.type_(id).map(|node| node.kind.clone()) {
424        Ok(TypeKind::Array { .. }) => true,
425        Ok(TypeKind::Alias { aliased }) => aliased
426            .resolved()
427            .is_some_and(|target| is_array(ast, target, depth + 1)),
428        _ => false,
429    }
430}
431
432/// How a destination of `id` is handed to its decoder: the array
433/// itself, or anything else in the form [`Handoff::Value`] names.
434const fn destination_form(is_array: bool) -> Handoff {
435    if is_array {
436        Handoff::Array
437    } else {
438        Handoff::Value
439    }
440}
441
442/// One member's read, with its explicit `@default` where the type's
443/// decoder cannot supply it.
444///
445/// `USE_DEFAULT` plus a `@default` is the one combination a
446/// type-keyed decoder cannot answer, so the site asks for DISCARD and
447/// takes the answer back. `zdw_reader_clear_discard` clears that status
448/// and no other: a truncated stream is not a narrowing. The position is
449/// already right, because every narrowing reads the whole value off the
450/// wire before it decides.
451fn member_read<T: CodecTarget>(ast: &Ast, slot: &Slot<T>, place: T::Expr) -> Vec<T::Stmt> {
452    let Some(assign_default) = slot.explicit_default.clone() else {
453        return vec![decode_into::<T>(ast, slot.type_, place, slot.try_construct)];
454    };
455    vec![
456        decode_into::<T>(ast, slot.type_, place, TryConstruct::Discard),
457        T::when(
458            call_expr::<T>("reader_clear_discard", Vec::new()),
459            T::block(assign_default),
460            None,
461        ),
462    ]
463}
464
465/// `zd_dec_<T>(r, <destination>, <behaviour>);`
466/// As [`decode_into`], for a value reached as a collection element.
467///
468/// The decoder generated for a type is one function and cannot ask at
469/// run time where it was called from, so the one primitive whose
470/// framing depends on the position — `Char16` under
471/// [`WcharFraming::GiopLengthPrefixedMemberOnly`] — is read inline here
472/// with the element's own constant. Every other element and every other
473/// framing falls through to the ordinary call, so the generated code is
474/// unchanged outside TAO's dialect.
475fn decode_element_into<T: CodecTarget>(
476    ast: &Ast,
477    id: Id<TypeNode>,
478    place: T::Expr,
479    behaviour: TryConstruct,
480    key: LayoutKey,
481) -> T::Stmt {
482    if key.wchar_framing.has_length_octet(WcharPosition::Member)
483        != key.wchar_framing.has_length_octet(WcharPosition::Element)
484        && matches!(resolve_primitive(ast, id, 0), Some(PrimitiveKind::WChar))
485    {
486        let Some(write) = scalar_write(ast, id, key, WcharPosition::Element) else {
487            return decode_into::<T>(ast, id, place, behaviour);
488        };
489        // `primitive_read_at` already casts a wchar read to the code
490        // point holder; a second cast here survived `store`'s
491        // one-cast folding and Java's `-Xlint:cast -Werror` names the
492        // leftover `(int)` (36 tao/rti element-form cells).
493        return T::store(place, T::value_type(ast, id), primitive_read_at::<T>(write));
494    }
495    decode_into::<T>(ast, id, place, behaviour)
496}
497
498/// The primitive kind `id` is once typedefs are seen through.
499///
500/// zerodds-lint: recursion-depth 32 (typedef chains; bounded by the
501/// `depth > 32` check, past which the answer is `None` — not a
502/// primitive)
503fn resolve_primitive(ast: &Ast, id: Id<TypeNode>, depth: usize) -> Option<PrimitiveKind> {
504    if depth > 32 {
505        return None;
506    }
507    match ast.type_(id).map(|node| node.kind.clone()) {
508        Ok(TypeKind::Primitive(kind)) => Some(kind),
509        Ok(TypeKind::Alias { aliased }) => aliased
510            .resolved()
511            .and_then(|target| resolve_primitive(ast, target, depth + 1)),
512        _ => None,
513    }
514}
515
516fn decode_into<T: CodecTarget>(
517    ast: &Ast,
518    id: Id<TypeNode>,
519    place: T::Expr,
520    behaviour: TryConstruct,
521) -> T::Stmt {
522    T::read_call(
523        &decoder_name::<T>(ast, id),
524        place,
525        destination_form(is_array(ast, id, 0)),
526        T::constant(try_construct_constant(behaviour)),
527    )
528}
529
530/// The `zdw_try_construct` constant one behaviour is spelled by.
531const fn try_construct_constant(behaviour: TryConstruct) -> Constant {
532    match behaviour {
533        TryConstruct::Discard => Constant::TryConstructDiscard,
534        TryConstruct::UseDefault => Constant::TryConstructUseDefault,
535        TryConstruct::Trim => Constant::TryConstructTrim,
536    }
537}
538
539/// `switch (zd_tc)` over the three behaviours, as one statement.
540///
541/// `trim` is what TRIM does where Table 11 defines it and empty where
542/// it does not — the frontend refuses such a declaration, so the arm
543/// falls through to the discard it would otherwise silently become.
544fn narrow<T: CodecTarget>(trim: Vec<T::Stmt>, use_default: Vec<T::Stmt>) -> T::Stmt {
545    let mut discard = vec![refuse::<T>(Constant::StatusDiscarded), T::give_back(None)];
546    let mut default_arm = use_default;
547    default_arm.push(T::give_back(None));
548    let mut trim_arm = trim;
549    if trim_arm.is_empty() {
550        trim_arm = core::mem::take(&mut discard);
551    } else {
552        trim_arm.push(T::give_back(None));
553    }
554    // Written as nested `if`s rather than a `switch`: the generated C is
555    // compiled with `-Wswitch-enum` off but `-Wall -Wextra -Werror` on,
556    // and a `switch` over an enum without a `default` is one warning
557    // away from breaking every build the moment the enum grows.
558    T::when(
559        T::binary(
560            "==",
561            name::<T>("zd_tc"),
562            T::constant(Constant::TryConstructTrim),
563        ),
564        T::block(trim_arm),
565        Some(T::block(vec![T::when(
566            T::binary(
567                "==",
568                name::<T>("zd_tc"),
569                T::constant(Constant::TryConstructUseDefault),
570            ),
571            T::block(default_arm),
572            Some(T::block(if discard.is_empty() {
573                vec![refuse::<T>(Constant::StatusDiscarded), T::give_back(None)]
574            } else {
575                discard
576            })),
577        )])),
578    )
579}
580
581#[allow(clippy::too_many_lines)]
582fn body_of<T: CodecTarget>(
583    ast: &Ast,
584    id: Id<TypeNode>,
585    key: LayoutKey,
586    options: CodecOptions,
587    own_name: &str,
588) -> Result<Option<T::Block>, EncodeError> {
589    let Ok(node) = ast.type_(id) else {
590        return Ok(None);
591    };
592    let statements = match &node.kind {
593        TypeKind::Primitive(_) => {
594            let Some(write) = scalar_write(ast, id, key, WcharPosition::Member) else {
595                return Err(EncodeError::NoWriter {
596                    member: T::type_name(ast, id),
597                    kind: "primitive the layout plan has no scalar write for",
598                });
599            };
600            vec![T::store(
601                deref::<T>("v"),
602                T::value_type(ast, id),
603                primitive_read_at::<T>(write),
604            )]
605        }
606
607        TypeKind::String {
608            width: CharWidth::Char,
609            bound,
610        } => {
611            let mut statements = vec![assign::<T>(
612                deref::<T>("v"),
613                call_expr::<T>("read_string_alloc", Vec::new()),
614            )];
615            statements.extend(string_fit::<T>(ast, id, bound, CharWidth::Char)?);
616            statements
617        }
618        TypeKind::String {
619            width: CharWidth::WChar,
620            bound,
621        } => {
622            let mut statements = vec![assign::<T>(
623                deref::<T>("v"),
624                match scalar_write(ast, id, key, WcharPosition::Member) {
625                    Some(ScalarWrite::Wstr {
626                        policy,
627                        order,
628                        code_units,
629                    }) => match code_units {
630                        WstringCodeUnits::Utf8 => call_expr::<T>(
631                            "read_wstring_utf8_octets",
632                            vec![T::constant(wstring_policy(policy))],
633                        ),
634                        WstringCodeUnits::Utf16 => call_expr::<T>(
635                            "read_wstring_utf8",
636                            vec![
637                                T::constant(wstring_policy(policy)),
638                                T::constant(wstring_unit_order(order)),
639                            ],
640                        ),
641                    },
642                    _ => {
643                        return Err(EncodeError::NoWriter {
644                            member: T::type_name(ast, id),
645                            kind: "wstring the layout plan has no scalar write for",
646                        });
647                    }
648                },
649            )];
650            statements.extend(string_fit::<T>(ast, id, bound, CharWidth::WChar)?);
651            statements
652        }
653
654        TypeKind::Fixed { .. } => {
655            let Some(ScalarWrite::Fixed {
656                digits,
657                scale,
658                count,
659            }) = scalar_write(ast, id, key, WcharPosition::Member)
660            else {
661                return Err(EncodeError::NoWriter {
662                    member: T::type_name(ast, id),
663                    kind: "fixed the layout plan has no scalar write for",
664                });
665            };
666            vec![assign::<T>(
667                deref::<T>("v"),
668                call_expr::<T>(
669                    "read_fixed",
670                    vec![
671                        int::<T>(i128::from(digits)),
672                        int::<T>(i128::from(scale)),
673                        T::constant(match count {
674                            FixedDigitCount::Declared => Constant::FixedDeclared,
675                            FixedDigitCount::ValuePrecision => Constant::FixedValuePrecision,
676                            FixedDigitCount::ValueStringDigits => Constant::FixedValueStringDigits,
677                        }),
678                    ],
679                ),
680            )]
681        }
682
683        TypeKind::Enumeration { .. } => enumeration_body::<T>(ast, id, key),
684        TypeKind::Bitmask { .. } => bitmask_body::<T>(ast, id),
685        TypeKind::Bitset { .. } => {
686            // The whole chain, base-first: see `encode_c`.
687            let fields = &ast.all_bitfields_of(id);
688            let mut deps = DepCollector::default();
689            let layout = zerodds_ast_layout::xcdr::plan(ast, id, key, &mut deps)
690                .map_err(crate::encode::plan_refusal)?;
691            let Extent::Sized(anchor) = layout.extent else {
692                return Err(EncodeError::NoWriter {
693                    member: own_name.to_string(),
694                    kind: "bitset whose anchor the planner does not size",
695                });
696            };
697            let read = call_expr::<T>("read_bitset", vec![int::<T>(i128::from(anchor))]);
698            // A bitset of padding alone still has its anchor on the wire,
699            // and no field to read out of it: the octets are read and not
700            // kept, where a local would be one no statement uses.
701            let named = fields.iter().any(|field| {
702                ast.bitfield(*field)
703                    .is_ok_and(|field_node| field_node.name.is_some())
704            });
705            let mut statements = if named {
706                vec![T::declare(T::scalar(Scalar::UInt64), "zd_bits", Some(read))]
707            } else {
708                vec![T::drop_value(read)]
709            };
710            let mut position: u32 = 0;
711            for field in fields {
712                let Ok(field_node) = ast.bitfield(*field) else {
713                    continue;
714                };
715                let width = u32::from(field_node.bits);
716                if position.saturating_add(width) > 64 {
717                    return Err(EncodeError::NoWriter {
718                        member: own_name.to_string(),
719                        kind: "bitset wider than the sixty-four bits an anchor holds",
720                    });
721                }
722                // An anonymous field is padding: it moves the next
723                // field along and carries no value of its own.
724                if let Some(field_name) = field_node.name.as_ref() {
725                    let mask: u64 = if width >= 64 {
726                        u64::MAX
727                    } else {
728                        (1u64 << width) - 1
729                    };
730                    statements.push(assign::<T>(
731                        arrow::<T>("v", &T::escape(field_name)),
732                        // Into the member's own width and not into a
733                        // `uint64_t`: the anchor is sixty-four bits and
734                        // the field is as wide as `c_holder` made it,
735                        // so the narrowing is a decision written out
736                        // here rather than a conversion the compiler
737                        // makes quietly. The mask above has already
738                        // taken everything the field cannot hold.
739                        cast::<T>(
740                            T::unsigned_holder(field_node.bits),
741                            T::paren(T::binary(
742                                "&",
743                                T::paren(T::binary(
744                                    ">>",
745                                    name::<T>("zd_bits"),
746                                    // The shift distance in the anchor's
747                                    // own width. C and Java shift a
748                                    // 64-bit value by an `int` happily;
749                                    // a TypeScript `bigint` shifted by a
750                                    // `number` is a compile error, and
751                                    // the distance is a property of the
752                                    // anchor rather than of the field.
753                                    T::wide_unsigned(u64::from(position)),
754                                )),
755                                T::wide_unsigned(mask),
756                            )),
757                        ),
758                    ));
759                }
760                position = position.saturating_add(width);
761            }
762            statements
763        }
764
765        TypeKind::Alias { aliased } => {
766            let Some(target) = aliased.resolved() else {
767                return Err(EncodeError::NoWriter {
768                    member: own_name.to_string(),
769                    kind: "typedef with an unresolved target",
770                });
771            };
772            // A typedef is transparent on the wire and the C typedef
773            // makes the two the same type, so the destination goes
774            // straight through.
775            // The behaviour goes through with the value: a typedef is
776            // transparent on the wire and transparent to §7.2.2.7 too,
777            // and the site that called this one is the site the bound
778            // belongs to.
779            let callee = decoder_name::<T>(ast, target);
780            // Unless the alias and its target are one decoder. A target
781            // that folds a `typedef` into the type it names gives the
782            // two the same decoder name — Zig's `pub const NodeRef =
783            // Node;` is the alias, so `Zig::type_name` answers `m_Node`
784            // for both — and a forwarder to itself is not a forwarder:
785            // `recursive-typedef-sequence` built
786            // `fn zd_dec_m_Node(..) { … zd_dec_m_Node(..) }` and the
787            // probe took SIGSEGV on the stack it ran out of. `None`
788            // leaves the name unclaimed and the aliased type emits its
789            // own body under it.
790            if callee == own_name {
791                return Ok(None);
792            }
793            vec![T::read_forward(&callee)]
794        }
795
796        TypeKind::Sequence { element, bound } => {
797            let Some(element) = element.resolved() else {
798                return Err(EncodeError::NoWriter {
799                    member: own_name.to_string(),
800                    kind: "sequence with an unresolved element",
801                });
802            };
803            sequence_body::<T>(ast, id, element, bound, key)?
804        }
805
806        TypeKind::Array { element, dims } => {
807            let Some(element) = element.resolved() else {
808                return Err(EncodeError::NoWriter {
809                    member: own_name.to_string(),
810                    kind: "array with an unresolved element",
811                });
812            };
813            let mut lengths = Vec::new();
814            for dim in dims {
815                lengths.push(ast.eval_integral(*dim).map_err(|_| EncodeError::NoWriter {
816                    member: own_name.to_string(),
817                    kind: "array whose length does not evaluate",
818                })?);
819            }
820            // Table 17 gives an array no member-level behaviour — its
821            // bounds must match the sender's, so only its elements can
822            // fail — and there is no template syntax for its element
823            // slot on a member declarator. `element_try_construct`
824            // therefore lets the enclosing member's own annotation
825            // reach the elements, and `zd_tc` is that annotation.
826            array_body::<T>(
827                ast,
828                id,
829                element,
830                &lengths,
831                key,
832                ast.element_try_construct(id, None),
833            )
834        }
835
836        TypeKind::Map {
837            key: k,
838            value,
839            bound,
840        } => {
841            let (Some(k), Some(value)) = (k.resolved(), value.resolved()) else {
842                return Err(EncodeError::NoWriter {
843                    member: own_name.to_string(),
844                    kind: "map with an unresolved key or value",
845                });
846            };
847            let mut deps = DepCollector::default();
848            let layout = zerodds_ast_layout::xcdr::plan(ast, id, key, &mut deps)
849                .map_err(crate::encode::plan_refusal)?;
850            map_body::<T>(ast, id, k, value, bound, layout.needs_dheader, options)?
851        }
852
853        TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
854            aggregate_body::<T>(ast, id, key)?
855        }
856
857        TypeKind::Union { discriminator, .. } => {
858            union_body::<T>(ast, id, key, discriminator.resolved(), own_name)?
859        }
860
861        // No wire form at all, so there is nothing to read and nothing
862        // is claimed.
863        TypeKind::Native | TypeKind::ObjectRef { .. } | TypeKind::AnnotationType { .. } => {
864            return Ok(None);
865        }
866        TypeKind::ValueBox { .. } => {
867            return Err(EncodeError::NoWriter {
868                member: own_name.to_string(),
869                kind: "boxed value, whose presence flag has no settled wire form",
870            });
871        }
872        // Refused by name rather than passed over. `any` is in the model
873        // because IDL 4.2 has it; DDS-XTypes has no `any` and no
874        // encoding for one, and only CORBA CDR writes it — as a TypeCode
875        // ahead of the value. Returning `Ok(None)` here would generate a
876        // codec that silently omitted the member.
877        TypeKind::Any => {
878            return Err(EncodeError::NoWriter {
879                member: own_name.to_string(),
880                kind: "`any`, which DDS-XTypes has no encoding for; only CORBA CDR writes one, \
881                       as a TypeCode ahead of the value",
882            });
883        }
884        TypeKind::ValueBase => {
885            return Err(EncodeError::NoWriter {
886                member: own_name.to_string(),
887                kind: "`ValueBase`, which DDS-XTypes has no encoding for; only CORBA CDR writes \
888                       one, as a chunked value with its repository id",
889            });
890        }
891    };
892    Ok(Some(T::block(statements)))
893}
894
895/// An enumeration, matched on the bit pattern its holder carries.
896///
897/// The pattern and not the value, because the pattern is what a holder
898/// narrower than the value's range leaves: Cyclone takes `@value(200)`
899/// under `@bit_bound(8)` and writes `c8`, which read as a signed Int8 is
900/// `-56`. Both readings name the same enumerator and only the pattern
901/// says so. `vendor_bytes.md`, `enum_bb8_200`.
902///
903/// A pattern no enumerator has is refused rather than handed back as a
904/// number: the value of an enumeration is a name, and there is no name
905/// for this.
906///
907/// # The pattern is not the value the binding holds
908///
909/// The two part company above the holder's sign bit, and the corpus
910/// caught it: `@value(200)` under `@bit_bound(8)` goes out as `c8`, and
911/// `zdw_read_enum` sign-extends that to `-56` because §7.4.3.5.3 makes
912/// the holder signed. The C binding stores the *declared* value, which
913/// is what the fill side assigns and what the encoder writes, so a
914/// decoder assigning the sign-extended pattern produces an integer no
915/// enumerator constant equals — `enum-bitbound-8-value` came back as
916/// `<enumerator -56>` where `BLUE` went in.
917///
918/// The Rust decoder never meets this: it matches on the pattern and
919/// returns a name, so it has no in-memory integer to disagree about.
920/// Each arm here therefore assigns the constant the materialiser
921/// declared, by name, which is the same one the fill side uses.
922fn enumeration_body<T: CodecTarget>(ast: &Ast, id: Id<TypeNode>, key: LayoutKey) -> Vec<T::Stmt> {
923    // The reader takes the same axis the writer took, through the one
924    // crossing function. `zerodds_ast_layout::enumeration_bit_bound`.
925    let bits = zerodds_ast_layout::plan::enumeration_holder_bits(ast, id, key);
926    let mut statements = vec![T::declare(
927        T::scalar(Scalar::Int64),
928        "zd_raw",
929        Some(call_expr::<T>(
930            "read_enum",
931            vec![int::<T>(i128::from(bits))],
932        )),
933    )];
934
935    // A pattern no enumerator has cannot construct an object of this
936    // type — Table 18: "If the name or value of the T2 object does not
937    // exist in T1, the object cannot construct any object of type T1" —
938    // so it is the *position's* behaviour that decides. TRIM has no
939    // meaning on an enumeration (Table 11) and the frontend refuses to
940    // write it there, so the trim arm is empty and falls through to the
941    // discard rather than inventing a nearest enumerator.
942    let default_arm = crate::emit::default_enumerator_name(ast, id)
943        .map(|literal| T::enumerator_constant(ast, id, &literal))
944        .map(|constant| vec![assign::<T>(deref::<T>("v"), constant)])
945        .unwrap_or_default();
946    // Built from the last arm backwards, so the chain reads in
947    // declaration order and the first enumerator with a given pattern
948    // wins — which is the one the Rust reader also names.
949    let mut chain: Option<T::Block> = Some(T::block(vec![narrow::<T>(Vec::new(), default_arm)]));
950    let mut arms = Vec::new();
951    // The same mapping the writer takes, from the same place: the
952    // pattern on the wire is the enumerator's wire value and the
953    // constant assigned is its declared one, and neither side resolves
954    // it for itself.
955    let resolved = zerodds_ast_layout::plan::enumerator_writes(ast, id, key).unwrap_or_default();
956    for (name, value) in resolved {
957        let Some(pattern) = enumerator_pattern(value, bits) else {
958            continue;
959        };
960        arms.push((pattern, T::enumerator_constant(ast, id, &name)));
961    }
962    for (pattern, constant) in arms.into_iter().rev() {
963        chain = Some(T::block(vec![T::when(
964            T::binary("==", name::<T>("zd_raw"), T::wide_signed(pattern)),
965            T::block(vec![assign::<T>(deref::<T>("v"), constant)]),
966            chain,
967        )]));
968    }
969    if let Some(chain) = chain {
970        statements.extend(T::into_statements(chain));
971    }
972    statements
973}
974
975/// The signed value `zdw_read_enum` returns for an enumerator.
976///
977/// A second copy of the writer's rule and deliberately not a call into
978/// it: the two directions are compared against each other, and a shared
979/// helper would make them agree about a mistake. §7.4.3.5.3 makes the
980/// holder signed, so the top bit of the bound reads back negative.
981fn enumerator_pattern(value: i128, bits: u16) -> Option<i128> {
982    if bits == 0 || bits > 32 {
983        return None;
984    }
985    let width = i128::from(bits);
986    if value < -(1i128 << (width - 1)) || value > (1i128 << width) - 1 {
987        return None;
988    }
989    let masked = value & ((1i128 << width) - 1);
990    Some(if masked >= (1i128 << (width - 1)) {
991        masked - (1i128 << width)
992    } else {
993        masked
994    })
995}
996
997/// A bitmask, reduced to the bits the type declares.
998///
999/// §7.4.1.1.3.2: "the states of the remaining serialized bits are not
1000/// specified, and those bits are not considered to be part of the
1001/// bitmask", and Table 18: "Preserve bit values by index for all bits
1002/// identified in both T1 and T2". A bit the stream sets and the type does
1003/// not name is not part of the value this type reads, so the read keeps
1004/// only the declared positions.
1005fn bitmask_body<T: CodecTarget>(ast: &Ast, id: Id<TypeNode>) -> Vec<T::Stmt> {
1006    let bound = ast.effective_bit_bound(id);
1007    let mut declared: u64 = 0;
1008    for bit in ast.bits_of(id) {
1009        let Some(position) = ast.effective_bit_position(*bit) else {
1010            continue;
1011        };
1012        if position < bound && position < 64 {
1013            declared |= 1u64 << position;
1014        }
1015    }
1016    vec![
1017        T::declare(
1018            T::scalar(Scalar::UInt64),
1019            "zd_bits",
1020            Some(T::paren(T::binary(
1021                "&",
1022                call_expr::<T>("read_bitmask", vec![int::<T>(i128::from(bound))]),
1023                // The holder's own width and not a bare number: both
1024                // sides of the `&` are sixty-four bits wide, and in a
1025                // target where that width is a separate type — a
1026                // TypeScript `bigint` — mixing it with a `number` is a
1027                // compile error rather than a wrong answer.
1028                T::wide_unsigned(declared),
1029            ))),
1030        ),
1031        // `T::bitmask_value` over `T::value_type` — the two are one cast
1032        // everywhere but TypeScript, whose `as` cannot narrow the
1033        // 64-bit read to a `number` holder; the trait doc carries it.
1034        // The value type and not `T::named(T::type_name(..))`: the two
1035        // are the same string in C, where a bitmask's type name *is* its
1036        // C type name, and are not the same anywhere else.
1037        // `Cpp::type_name` answers `a_b_Flags` — the tag a codec function
1038        // is named after — where the type is `::a::b::Flags`, so a cast
1039        // built from the tag named a type no C++ scope declares.
1040        assign::<T>(
1041            deref::<T>("v"),
1042            T::bitmask_value(ast, id, name::<T>("zd_bits")),
1043        ),
1044    ]
1045}
1046
1047/// The limit a bound states, where it states one this column can read.
1048///
1049/// `None` for the unbounded form and for a bound that does not
1050/// evaluate; a negative one is not a length and is refused the same way.
1051/// One function because a `sequence`, a `string` and a `map` all read
1052/// their bound the same, and three copies would be three chances for
1053/// one collection to stop being checked while the others were.
1054fn bound_limit(ast: &Ast, bound: &zerodds_ast::Bound) -> Option<i128> {
1055    bound
1056        .expr()
1057        .and_then(|expr| ast.eval_integral(expr).ok())
1058        .filter(|limit| *limit >= 0)
1059}
1060
1061/// A sequence: the count, then the elements into storage the reader
1062/// allocated.
1063fn sequence_body<T: CodecTarget>(
1064    ast: &Ast,
1065    id: Id<TypeNode>,
1066    element: Id<TypeNode>,
1067    bound: &zerodds_ast::Bound,
1068    key: LayoutKey,
1069) -> Result<Vec<T::Stmt>, EncodeError> {
1070    let delimited = zerodds_ast_layout::collection_is_delimited(ast, element, key);
1071    let element_behaviour = ast.element_try_construct(id, None);
1072    let mut statements = Vec::new();
1073    if delimited {
1074        statements.push(discard_dheader::<T>());
1075    }
1076    statements.push(T::declare(
1077        T::scalar(Scalar::UInt32),
1078        "zd_n",
1079        Some(call_expr::<T>("read_u32", Vec::new())),
1080    ));
1081    // A count is one element or more, so a stream with fewer bytes left
1082    // than elements cannot be honest about it. Checked before the
1083    // allocation, which is how a reader turns a malformed message into
1084    // an out-of-memory when it is not.
1085    statements.push(T::when(
1086        T::binary(
1087            ">",
1088            cast::<T>(T::scalar(Scalar::Size), name::<T>("zd_n")),
1089            remaining::<T>(),
1090        ),
1091        T::block(vec![
1092            refuse::<T>(Constant::StatusImpossibleLength),
1093            T::give_back(None),
1094        ]),
1095        None,
1096    ));
1097    // The bound is applied after every element has been read and not
1098    // before: stopping at the bound would leave the rest of the
1099    // sequence in the stream and put every following member at the
1100    // wrong offset. `zd_over` is what the narrowing at the end reads.
1101    // §7.2.2.7, and OpenDDS does the same and then skips to the
1102    // collection's end.
1103    let limit = bound_limit(ast, bound);
1104    if let Some(limit) = limit {
1105        statements.push(T::declare(
1106            T::scalar(Scalar::Bool),
1107            "zd_over",
1108            Some(T::binary(">", name::<T>("zd_n"), T::unsigned_int(limit))),
1109        ));
1110    }
1111    let element_type = T::value_type(ast, element);
1112    // A run of octets is read in one piece where the target has the call
1113    // for it ([`CodecTarget::bulk_octets_read`]); the same octets as the
1114    // loop, with no per-element call.
1115    // The mirror of the writer: a `sequence` element of a non-`@final`
1116    // type is preceded by its own octet length under
1117    // `SequenceElementDelimiter::BeforeEachNonFinalElement`, and
1118    // `ElementPadding::Packed` counts every element's alignment from the
1119    // element's own first octet.
1120    let packed = key.element_padding == ElementPadding::Packed;
1121    let carries_length = crate::emit::encode::sequence_element_carries_length(ast, element, key)?;
1122    let fill = match crate::emit::encode::bulk_read::<T>(ast, element, name::<T>("zd_n")) {
1123        Some(bulk) => bulk,
1124        None => {
1125            // The element's place, and whatever the target needs around
1126            // it. See [`CodecTarget::sequence_element`]: every target but
1127            // one answers with the subscript and nothing else.
1128            let (before, place, after) =
1129                T::sequence_element("v", name::<T>("zd_i"), element_type.clone());
1130            let mut body: Vec<T::Stmt> = Vec::new();
1131            if packed {
1132                body.push(begin_packed::<T>());
1133            }
1134            if carries_length {
1135                body.extend(read_element_length::<T>());
1136            }
1137            body.extend(before);
1138            body.push(decode_element_into::<T>(
1139                ast,
1140                element,
1141                place,
1142                element_behaviour,
1143                key,
1144            ));
1145            body.extend(after);
1146            if carries_length {
1147                body.push(close_element_length::<T>());
1148            }
1149            if packed {
1150                body.push(finish_packed::<T>());
1151            }
1152            counted_loop::<T>("zd_i", name::<T>("zd_n"), T::block(body))
1153        }
1154    };
1155    statements.extend(T::make_room_of(
1156        ast,
1157        element,
1158        "v",
1159        element_type,
1160        name::<T>("zd_n"),
1161        true,
1162        vec![fill],
1163    ));
1164    if let Some(limit) = limit {
1165        statements.push(T::when(
1166            name::<T>("zd_over"),
1167            T::block(vec![narrow::<T>(
1168                // §7.3.1.2.1.12.3 keeps "the first four elements".
1169                // How a target says so is its own: C sets the
1170                // `_length` beside the buffer and leaves the elements
1171                // past the bound in it, unreferenced; a target whose
1172                // collection owns its storage has to shorten the
1173                // collection, and `v.size() = 5u` is not an
1174                // assignment at all.
1175                T::keep_first("v", T::unsigned_int(limit)),
1176                // Table 9, through this column's one derivation of
1177                // it, which for a sequence is the empty one.
1178                vec![T::default_into(ast, id, deref::<T>("v"))?],
1179            )]),
1180            None,
1181        ));
1182    }
1183    Ok(statements)
1184}
1185
1186/// The narrowing a bounded string needs once it has been read.
1187///
1188/// Nothing at all for an unbounded one: `zdw_read_string_alloc` has
1189/// already produced the value and there is no bound to miss.
1190fn string_fit<T: CodecTarget>(
1191    ast: &Ast,
1192    id: Id<TypeNode>,
1193    bound: &zerodds_ast::Bound,
1194    width: CharWidth,
1195) -> Result<Vec<T::Stmt>, EncodeError> {
1196    let Some(limit) = bound_limit(ast, bound) else {
1197        return Ok(Vec::new());
1198    };
1199    // Octets for a narrow string and UTF-16 code units for a wide one,
1200    // which is what the bound means to every peer that has to hold the
1201    // value — `string<5>` is `char[6]` in the C mapping of all four
1202    // measured implementations.
1203    //
1204    // Both are the runtime's: the measure has no receiver and the trim
1205    // takes one, which is exactly the difference between
1206    // [`CodecTarget::runtime_static`] and [`CodecTarget::runtime`]. They
1207    // were spelled `zdw_utf8_octets` and `zdw_trim_utf8` through
1208    // [`CodecTarget::call`] until a second reader column asked for them,
1209    // and `zdw_` is not a name any other runtime answers to.
1210    let (measure, trim) = match width {
1211        CharWidth::Char => ("utf8_octets", "trim_utf8"),
1212        CharWidth::WChar => ("utf16_units", "trim_utf16"),
1213    };
1214    // `(size_t)` written as a cast and not inside the literal, so that a
1215    // target with no C-style cast writes its own. C renders the two the
1216    // same.
1217    //
1218    // The number goes through `unsigned_int` and not through a
1219    // `format!("{limit}u")`: the `u` suffix is C's spelling of an
1220    // unsigned literal, and a walk that wrote it reached `32u` into a
1221    // Java probe, where `javac` calls it a syntax error. Every target
1222    // spells its own.
1223    let fits = || cast::<T>(T::scalar(Scalar::Size), T::unsigned_int(limit));
1224    let too_long = T::binary(
1225        ">",
1226        T::runtime_static(measure, vec![deref::<T>("v")]),
1227        fits(),
1228    );
1229    Ok(vec![T::when(
1230        // The value may not be there at all in a target whose reader
1231        // allocates and can fail. Where it cannot, the guard is not
1232        // written rather than written against a value that is always
1233        // present: `std::string != std::nullopt` does not compile.
1234        match T::materialised(deref::<T>("v")) {
1235            Some(present) => T::binary("&&", present, too_long),
1236            None => too_long,
1237        },
1238        T::block(vec![narrow::<T>(
1239            vec![assign::<T>(
1240                deref::<T>("v"),
1241                T::runtime("r", trim, vec![deref::<T>("v"), fits()]),
1242            )],
1243            // Table 9, through the one derivation of it this column
1244            // has. `default_c::try_construct_seeds` is what makes
1245            // sure the function exists.
1246            vec![T::default_into(ast, id, deref::<T>("v"))?],
1247        )]),
1248        None,
1249    )])
1250}
1251
1252/// An array: the element count is in the type, so only the elements are
1253/// on the wire. §7.4.3.5 treats a multi-dimensional array as one flat
1254/// run with the first index varying most slowly, which is what nested
1255/// loops in written order already are.
1256fn array_body<T: CodecTarget>(
1257    ast: &Ast,
1258    id: Id<TypeNode>,
1259    element: Id<TypeNode>,
1260    lengths: &[i128],
1261    key: LayoutKey,
1262    element_behaviour: TryConstruct,
1263) -> Vec<T::Stmt> {
1264    let _ = id;
1265    let delimited = zerodds_ast_layout::array_is_delimited(ast, element, key);
1266    if let [length] = lengths {
1267        if let Some(bulk) =
1268            crate::emit::encode::bulk_read::<T>(ast, element, T::unsigned_int(*length))
1269        {
1270            let mut statements = Vec::new();
1271            if delimited {
1272                statements.push(discard_dheader::<T>());
1273            }
1274            statements.extend(T::open_array("v", lengths));
1275            statements.push(bulk);
1276            statements.extend(T::close_array("v", lengths));
1277            return statements;
1278        }
1279    }
1280    let mut access = name::<T>("v");
1281    for level in 0..lengths.len() {
1282        access = T::index(access, name::<T>(&format!("zd_i{level}")));
1283    }
1284    let mut inner = T::block(vec![decode_element_into::<T>(
1285        ast,
1286        element,
1287        access,
1288        element_behaviour,
1289        key,
1290    )]);
1291    // `ElementPadding::Packed`: each element of every dimension is its
1292    // own alignment block, as in the reference's `read_array_dimension`.
1293    let packed = key.element_padding == ElementPadding::Packed;
1294    for (level, length) in lengths.iter().enumerate().rev() {
1295        let mut body: Vec<T::Stmt> = Vec::new();
1296        if packed {
1297            body.push(begin_packed::<T>());
1298        }
1299        body.extend(T::into_statements(inner));
1300        if packed {
1301            body.push(finish_packed::<T>());
1302        }
1303        inner = T::block(vec![counted_loop::<T>(
1304            &format!("zd_i{level}"),
1305            T::unsigned_int(*length),
1306            T::block(body),
1307        )]);
1308    }
1309    let mut statements = Vec::new();
1310    if delimited {
1311        statements.push(discard_dheader::<T>());
1312    }
1313    statements.extend(T::open_array("v", lengths));
1314    statements.extend(T::into_statements(inner));
1315    statements.extend(T::close_array("v", lengths));
1316    statements
1317}
1318
1319/// A map: §7.4.1.1.4's sequence of entries, in whichever of the two
1320/// readings the caller chose.
1321///
1322/// Under `rule-15-omits` a delimited map states no count, and the
1323/// delimiter's extent is the only thing that says where the entries
1324/// stop. The generated code then reads the entries twice: once into a
1325/// scratch entry to count them, then again into storage sized from that
1326/// count. `zdw_reader_restore` puts back both the cursor and the heap,
1327/// so the first pass leaves nothing behind.
1328///
1329/// This is the only reader mark left in the generated code. The other
1330/// one was the `@optional` guess, which rule (20) and §7.4.1.2 retired.
1331///
1332/// The bound is checked exactly as [`sequence_body`] checks a
1333/// sequence's, and for the same clauses: §7.2.2.7 makes an entry count
1334/// over the bound an invalid sample, and §7.3.1.2.1.12.3's TRIM keeps
1335/// the first `bound` entries. It is applied after every entry has been
1336/// read and not before: stopping at the bound would leave the rest of
1337/// the map in the stream and put every following member at the wrong
1338/// offset.
1339///
1340/// It was missing entirely: `map<long, long, 4>` read as many entries
1341/// as the stream stated in every one of the twenty columns, so a
1342/// sample the IDL does not describe reached the application with
1343/// nothing said about it. `sequence<long, 4>` beside it in the same
1344/// struct was checked, which is what made the gap visible.
1345///
1346/// # Errors
1347/// [`EncodeError`] when this column cannot spell Table 9's value for
1348/// the map — the same refusal a bounded sequence's TRIM raises.
1349fn map_body<T: CodecTarget>(
1350    ast: &Ast,
1351    id: Id<TypeNode>,
1352    key_type: Id<TypeNode>,
1353    value_type_: Id<TypeNode>,
1354    bound: &zerodds_ast::Bound,
1355    delimited: bool,
1356    options: CodecOptions,
1357) -> Result<Vec<T::Stmt>, EncodeError> {
1358    // Table 17 gives a map's key no escape — a key that cannot be
1359    // constructed fails the whole map — and the frontend refuses
1360    // anything but DISCARD there. The value has its own slot.
1361    let key_behaviour = ast.key_try_construct(id);
1362    let value_behaviour = ast.element_try_construct(id, None);
1363    let entry_type = T::map_entry_type(ast, id);
1364    let counted = !delimited || options.map_length_with_delimiter;
1365    let mut statements = Vec::new();
1366
1367    if delimited {
1368        statements.push(T::declare(
1369            T::scalar(Scalar::UInt32),
1370            "zd_dl",
1371            Some(call_expr::<T>("read_dheader", Vec::new())),
1372        ));
1373        statements.push(T::declare(
1374            T::scalar(Scalar::Size),
1375            "zd_end",
1376            Some(offset_plus::<T>(position::<T>(), name::<T>("zd_dl"))),
1377        ));
1378    }
1379
1380    let read_entry = |index: T::Expr| {
1381        T::block(vec![
1382            decode_into::<T>(
1383                ast,
1384                key_type,
1385                T::entry_half(T::index(T::elements("v"), index.clone()), true),
1386                key_behaviour,
1387            ),
1388            decode_into::<T>(
1389                ast,
1390                value_type_,
1391                T::entry_half(T::index(T::elements("v"), index), false),
1392                value_behaviour,
1393            ),
1394        ])
1395    };
1396
1397    if counted {
1398        statements.push(T::declare(
1399            T::scalar(Scalar::UInt32),
1400            "zd_n",
1401            Some(call_expr::<T>("read_u32", Vec::new())),
1402        ));
1403    } else {
1404        // The counting pass. A scratch entry rather than a discard,
1405        // because an entry may hold a string and the decoder has to
1406        // have somewhere to put one.
1407        statements.push(T::storage(entry_type.clone(), "zd_scratch"));
1408        statements.push(T::declare(
1409            T::handle(Handle::ReaderMark),
1410            "zd_mark",
1411            Some(call_expr::<T>("reader_save", Vec::new())),
1412        ));
1413        statements.push(T::declare(
1414            T::scalar(Scalar::UInt32),
1415            "zd_n",
1416            Some(T::unsigned_int(0)),
1417        ));
1418        statements.push(T::until(
1419            T::binary(
1420                "&&",
1421                T::binary("<", position::<T>(), name::<T>("zd_end")),
1422                status_is_ok::<T>(),
1423            ),
1424            T::block(vec![
1425                decode_into::<T>(
1426                    ast,
1427                    key_type,
1428                    T::entry_half(name::<T>("zd_scratch"), true),
1429                    key_behaviour,
1430                ),
1431                decode_into::<T>(
1432                    ast,
1433                    value_type_,
1434                    T::entry_half(name::<T>("zd_scratch"), false),
1435                    value_behaviour,
1436                ),
1437                assign::<T>(
1438                    name::<T>("zd_n"),
1439                    T::binary("+", name::<T>("zd_n"), T::unsigned_int(1)),
1440                ),
1441            ]),
1442        ));
1443        statements.push(T::perform(call_expr::<T>(
1444            "reader_restore",
1445            vec![name::<T>("zd_mark")],
1446        )));
1447    }
1448
1449    statements.push(T::when(
1450        T::binary(
1451            ">",
1452            cast::<T>(T::scalar(Scalar::Size), name::<T>("zd_n")),
1453            remaining::<T>(),
1454        ),
1455        T::block(vec![
1456            refuse::<T>(Constant::StatusImpossibleLength),
1457            T::give_back(None),
1458        ]),
1459        None,
1460    ));
1461    // The entry count against the bound, read before the entries are
1462    // and acted on after them. `zd_over` is the same local a bounded
1463    // sequence declares, because it is the same rule over the same
1464    // count.
1465    let limit = bound_limit(ast, bound);
1466    if let Some(limit) = limit {
1467        statements.push(T::declare(
1468            T::scalar(Scalar::Bool),
1469            "zd_over",
1470            Some(T::binary(">", name::<T>("zd_n"), T::unsigned_int(limit))),
1471        ));
1472    }
1473    statements.extend(T::make_room(
1474        "v",
1475        entry_type,
1476        name::<T>("zd_n"),
1477        false,
1478        vec![counted_loop::<T>(
1479            "zd_i",
1480            name::<T>("zd_n"),
1481            read_entry(name::<T>("zd_i")),
1482        )],
1483    ));
1484    // Ahead of the narrowing below and not after it: every arm of
1485    // [`narrow`] leaves the routine, so a map that had to be trimmed
1486    // would return with the cursor short of the delimiter's end and put
1487    // every following member at the wrong offset. The cursor is put
1488    // where the delimiter says it belongs first, and what the sample
1489    // holds is decided after.
1490    if delimited {
1491        statements.push(T::perform(call_expr::<T>(
1492            "reader_skip_to",
1493            vec![name::<T>("zd_end")],
1494        )));
1495    }
1496    if let Some(limit) = limit {
1497        statements.push(T::when(
1498            name::<T>("zd_over"),
1499            T::block(vec![narrow::<T>(
1500                // §7.3.1.2.1.12.3 keeps "the first four elements", and
1501                // an entry is what a map's element is (§7.4.1.1.4).
1502                // How a column shortens the collection is its own; the
1503                // walk says how many to keep.
1504                T::keep_first("v", T::unsigned_int(limit)),
1505                // Table 9, through this column's one derivation of it,
1506                // which for a map is the empty one.
1507                vec![T::default_into(ast, id, deref::<T>("v"))?],
1508            )]),
1509            None,
1510        ));
1511    }
1512    Ok(statements)
1513}
1514
1515/// One member of an aggregate, as the plan and the model together see
1516/// it.
1517struct Slot<T: CodecTarget> {
1518    field: String,
1519    type_: Id<TypeNode>,
1520    optional: bool,
1521    /// Whether the member is `@external` — stored behind the column's
1522    /// own indirection, where it keeps one. Table 21: no octet changes;
1523    /// which seam the access routes through does.
1524    external: bool,
1525    /// Whether the member's value has no room inside the aggregate that
1526    /// declares it — [`zerodds_ast_layout::member_held_inline_closes_a_cycle`].
1527    /// A column whose member is the value has to keep an indirection
1528    /// there whatever the annotations say.
1529    indirect: bool,
1530    /// Whether the column keeps the member behind a reference of its
1531    /// own making — [`CodecTarget::holds_by_reference`].
1532    by_reference: bool,
1533    member_id: u32,
1534    header: MemberHeader,
1535    /// The declaration, for the `@default` it may carry.
1536    member: Id<zerodds_ast::MemberNode>,
1537    /// The behaviour governing this member's value, already resolved
1538    /// through the alias chain of its type. §7.2.2.7.
1539    try_construct: TryConstruct,
1540    /// The statements that assign this member's explicit `@default`,
1541    /// when it has one and its behaviour is `USE_DEFAULT`.
1542    ///
1543    /// The type's decoder cannot answer that combination: it knows
1544    /// Table 9's value for the type and not the literal this member's
1545    /// declaration gave it, and §7.2.2.4.4.4.9 puts the literal first.
1546    /// So the call site asks for DISCARD and assigns the literal where
1547    /// it gets one.
1548    ///
1549    /// `None` where the member has no `@default` or does not take
1550    /// `USE_DEFAULT`; a literal this column cannot spell is a refusal
1551    /// and never a `None`.
1552    explicit_default: Option<Vec<T::Stmt>>,
1553}
1554
1555fn slots<T: CodecTarget>(
1556    ast: &Ast,
1557    id: Id<TypeNode>,
1558    layout: &zerodds_ast_layout::TypeLayout,
1559) -> Result<Vec<Slot<T>>, EncodeError> {
1560    let all = ast.all_members_of(id);
1561    let mut out = Vec::with_capacity(layout.members.len());
1562    for member in &layout.members {
1563        let mut found = None;
1564        for candidate in &all {
1565            let Ok(node) = ast.member(*candidate) else {
1566                continue;
1567            };
1568            if node.node_id == member.member {
1569                found = Some((*candidate, node));
1570                break;
1571            }
1572        }
1573        let Some((handle, node)) = found else {
1574            continue;
1575        };
1576        let Some(member_type) = node.type_.resolved() else {
1577            continue;
1578        };
1579        let try_construct = ast.effective_try_construct(
1580            node.facets.xtypes.as_ref().and_then(|x| x.try_construct),
1581            Some(member_type),
1582        );
1583        // Where the member is reached from: the aggregate, or the base that
1584        // declares it where the column nests the base as `parent`.
1585        let reach = super::reach::<T>(ast, id, member.member, &node.name);
1586        let explicit_default = if try_construct == TryConstruct::UseDefault
1587            && node
1588                .facets
1589                .xtypes
1590                .as_ref()
1591                .is_some_and(|x| x.default_value.is_some())
1592        {
1593            // Reported and not swallowed: a `@default` this column
1594            // cannot spell would otherwise leave the member taking
1595            // Table 9's zero, which is a different sample from the one
1596            // the designer declared.
1597            Some(T::member_default(
1598                ast,
1599                node,
1600                member_type,
1601                arrow::<T>("v", &reach.field),
1602            )?)
1603        } else {
1604            None
1605        };
1606        let optional = node.facets.is_optional();
1607        let external = super::reached_externally::<T>(ast, node);
1608        out.push(Slot::<T> {
1609            indirect: super::behind_pointer::<T>(
1610                ast,
1611                reach.holder,
1612                member_type,
1613                external,
1614                optional,
1615            ),
1616            by_reference: T::holds_by_reference(ast, reach.holder, member_type, optional, external),
1617            field: reach.field,
1618            type_: member_type,
1619            optional,
1620            external,
1621            member_id: member.member_id,
1622            header: member.header,
1623            member: handle,
1624            try_construct,
1625            explicit_default,
1626        });
1627    }
1628    Ok(out)
1629}
1630
1631/// The reader's destination for one slot's member, through the seams
1632/// its annotations and its type pick: `@external` members go through
1633/// [`CodecTarget::external_destination`], `@optional` ones through
1634/// [`CodecTarget::optional_destination`], and a plain member is its
1635/// field — and any of the three goes on through
1636/// [`CodecTarget::make_indirection`] where the member's value has no
1637/// room inside the aggregate that declares it.
1638fn slot_destination<T: CodecTarget>(ast: &Ast, slot: &Slot<T>) -> (Vec<T::Stmt>, T::Expr) {
1639    if slot.by_reference {
1640        return T::reference_destination(ast, slot.type_, "v", &slot.field);
1641    }
1642    let (mut setup, place) = if slot.external {
1643        T::external_destination("v", &slot.field, slot.optional)
1644    } else if slot.optional {
1645        T::optional_destination("v", &slot.field)
1646    } else {
1647        (Vec::new(), arrow::<T>("v", &slot.field))
1648    };
1649    // A member with no room inside its own aggregate is reached through
1650    // the column's indirection, and the indirection points nowhere until
1651    // this makes it point somewhere.
1652    if slot.indirect {
1653        let (made, through) = T::make_indirection(ast, slot.type_, place);
1654        setup.extend(made);
1655        return (setup, through);
1656    }
1657    (setup, place)
1658}
1659
1660/// Where a slot's Table 9 default goes, and what it takes to have
1661/// somewhere to put it.
1662///
1663/// The same seam [`slot_destination`] uses, for the write a *missing*
1664/// member gets: a column that keeps an indirection has nothing to
1665/// default into until the indirection points somewhere. `place` is the
1666/// answer for every member the column holds inline, which is every
1667/// member this does not report.
1668fn slot_default_place<T: CodecTarget>(
1669    ast: &Ast,
1670    slot: &Slot<T>,
1671    place: T::Expr,
1672) -> (Vec<T::Stmt>, T::Expr) {
1673    if slot.indirect {
1674        return T::make_indirection(ast, slot.type_, place);
1675    }
1676    (Vec::new(), place)
1677}
1678
1679/// Say a slot's `@optional` member is present or absent, through the
1680/// external seam where the member is also `@external`.
1681fn slot_set_present<T: CodecTarget>(slot: &Slot<T>, present: bool) -> Vec<T::Stmt> {
1682    // Held by reference: the allocation `reference_destination` makes
1683    // is what makes the member present, and absence is the reference's
1684    // own null.
1685    if slot.by_reference {
1686        return if present {
1687            Vec::new()
1688        } else {
1689            T::reference_absent("v", &slot.field)
1690        };
1691    }
1692    if slot.external {
1693        return T::external_set_present("v", &slot.field, present);
1694    }
1695    T::set_present("v", &slot.field, present)
1696}
1697
1698/// A structure or a valuetype, framed as its plan says.
1699///
1700/// Three shapes, and which one applies is the extensibility, read the
1701/// same way the planner reads it rather than inferred back out of the
1702/// plan it produced.
1703fn aggregate_body<T: CodecTarget>(
1704    ast: &Ast,
1705    id: Id<TypeNode>,
1706    key: LayoutKey,
1707) -> Result<Vec<T::Stmt>, EncodeError> {
1708    let mut deps = DepCollector::default();
1709    let layout = zerodds_ast_layout::xcdr::plan(ast, id, key, &mut deps)
1710        .map_err(crate::encode::plan_refusal)?;
1711    let members = slots::<T>(ast, id, &layout)?;
1712    let extensibility = ast.effective_extensibility(id);
1713    let mutable = extensibility == Extensibility::Mutable;
1714
1715    let mut statements = Vec::new();
1716    // `ElementPadding::Packed`: an aggregate is its own alignment block
1717    // on the read side too. See the writer's `write_aggregate`.
1718    let packed = key.element_padding == ElementPadding::Packed;
1719    if packed {
1720        statements.push(begin_packed::<T>());
1721    }
1722    if layout.needs_dheader {
1723        statements.extend(read_delimiter::<T>());
1724    }
1725    // An `@optional` member's presence flag is set by this decoder and
1726    // never left at whatever the caller's storage held.
1727    for slot in &members {
1728        if slot.optional {
1729            statements.extend(slot_set_present::<T>(slot, false));
1730        }
1731    }
1732
1733    if mutable && key.encoding.is_v1() {
1734        statements.extend(parameter_list::<T>(ast, &members)?);
1735    } else if mutable {
1736        if !layout.needs_dheader {
1737            return Err(EncodeError::NoLayout {
1738                detail: "a mutable type under XCDR2 with no DHEADER planned".to_string(),
1739            });
1740        }
1741        statements.extend(member_headers::<T>(ast, &members)?);
1742    } else {
1743        let tail = Tail {
1744            delimited: layout.needs_dheader,
1745            appendable: extensibility == Extensibility::Appendable,
1746        };
1747        statements.extend(plan_order::<T>(ast, &members, tail)?);
1748    }
1749
1750    if layout.needs_dheader {
1751        statements.extend(close_delimiter::<T>());
1752    }
1753    if packed {
1754        statements.push(finish_packed::<T>());
1755    }
1756    Ok(statements)
1757}
1758
1759/// `uint32_t zd_dl = …; size_t zd_end = position + zd_dl;`
1760fn read_delimiter<T: CodecTarget>() -> Vec<T::Stmt> {
1761    // Two statements and not one expression: C leaves the order of
1762    // evaluation between the call and `zdw_reader_position` unspecified,
1763    // so an end computed in one line would depend on the compiler.
1764    vec![
1765        T::declare(
1766            T::scalar(Scalar::UInt32),
1767            "zd_dl",
1768            Some(call_expr::<T>("read_dheader", Vec::new())),
1769        ),
1770        T::declare(
1771            T::scalar(Scalar::Size),
1772            "zd_end",
1773            Some(offset_plus::<T>(position::<T>(), name::<T>("zd_dl"))),
1774        ),
1775    ]
1776}
1777
1778/// What the delimiter is checked against once the members are read.
1779///
1780/// The tail inside a delimiter is what a peer with fewer members than
1781/// the writer had leaves behind, and skipping it is what the DHEADER is
1782/// for.
1783///
1784/// It used to carry a second check as well: bytes left inside the
1785/// delimiter after every declared member had been read falsified a
1786/// guess that an `@optional` member was absent. There is no guess left
1787/// to falsify — rule (20) and §7.4.1.2 mark the gap and this column
1788/// reads the marker — so the check has nothing to catch and the
1789/// `zd_guessed` flag it needed is gone with it.
1790fn close_delimiter<T: CodecTarget>() -> Vec<T::Stmt> {
1791    let statements = vec![T::when(
1792        T::binary(">", position::<T>(), name::<T>("zd_end")),
1793        T::block(vec![refuse::<T>(Constant::StatusOutOfRange)]),
1794        None,
1795    )];
1796    let mut statements = statements;
1797    statements.push(T::perform(call_expr::<T>(
1798        "reader_skip_to",
1799        vec![name::<T>("zd_end")],
1800    )));
1801    statements
1802}
1803
1804/// The local a body's packed-element origin is saved in.
1805const PACKED_SAVED: &str = "zd_saved";
1806
1807/// `size_t zd_saved = reader_begin_packed_element(r);`
1808///
1809/// [`ElementPadding::Packed`] is HDDS's measured departure from §7.4.1:
1810/// a collection element and an aggregate value count their alignment
1811/// from their own first octet instead of from the encapsulation. The
1812/// generated codec holds the displaced origin and restores it with
1813/// [`finish_packed`]. The `reader_` prefix is the runtime's own, as for
1814/// `reader_save` and `reader_position`, and it is what keeps the read
1815/// direction's pair distinct from the write direction's.
1816fn begin_packed<T: CodecTarget>() -> T::Stmt {
1817    T::declare(
1818        T::scalar(Scalar::Size),
1819        PACKED_SAVED,
1820        Some(call_expr::<T>("reader_begin_packed_element", Vec::new())),
1821    )
1822}
1823
1824/// `reader_finish_packed_element(r, zd_saved);`
1825fn finish_packed<T: CodecTarget>() -> T::Stmt {
1826    T::perform(call_expr::<T>(
1827        "reader_finish_packed_element",
1828        vec![name::<T>(PACKED_SAVED)],
1829    ))
1830}
1831
1832/// The element's own octet length a `sequence` element of a non-`@final`
1833/// type carries.
1834///
1835/// [`zerodds_ast_layout::key::SequenceElementDelimiter::BeforeEachNonFinalElement`]
1836/// writes a four-octet `UInt32` in front of every element. The reader
1837/// checks the element against it rather than skipping to it: an element
1838/// longer or shorter than its own declaration is a stream that disagrees
1839/// with itself, and seeking past it would hide the disagreement.
1840fn read_element_length<T: CodecTarget>() -> Vec<T::Stmt> {
1841    vec![
1842        T::declare(
1843            T::scalar(Scalar::UInt32),
1844            "zd_ell",
1845            Some(call_expr::<T>("read_u32", Vec::new())),
1846        ),
1847        T::declare(
1848            T::scalar(Scalar::Size),
1849            "zd_elend",
1850            Some(offset_plus::<T>(position::<T>(), name::<T>("zd_ell"))),
1851        ),
1852    ]
1853}
1854
1855fn close_element_length<T: CodecTarget>() -> T::Stmt {
1856    ends_where_the_header_said::<T>("zd_elend")
1857}
1858
1859/// A `@final` or `@appendable` aggregate: the members in plan order.
1860///
1861/// Every member is where the plan says it is, including an `@optional`
1862/// one. §7.4.3.5.2 rule (20) puts a BOOLEAN `<is_present>` in front of
1863/// it under XCDR2 and §7.4.1.2 gives it a parameter header with a
1864/// declared length of zero under XCDR1, and all three implementations
1865/// write both — so nothing here is guessed and nothing rewinds.
1866///
1867/// This used to read the header against a `zdw_reader_mark` and rewind
1868/// when the member id was not the one expected. Against a stream that
1869/// marks the gap that guess reads the marker as a header, calls the
1870/// member absent, and takes the *next* member's bytes out of this
1871/// member's place.
1872fn plan_order<T: CodecTarget>(
1873    ast: &Ast,
1874    members: &[Slot<T>],
1875    tail: Tail,
1876) -> Result<Vec<T::Stmt>, EncodeError> {
1877    let mut statements = Vec::new();
1878    // `!members.is_empty()`, because the latch is read only inside the
1879    // loop below and a declaration nothing reads is a
1880    // `-Wunused-variable` error in a probe compiled with `-Werror`. An
1881    // aggregate with no members reaches this only in a column whose
1882    // materialiser can declare one: C refuses `struct S {}` outright —
1883    // C23 §6.7.2.1p8 leaves a structure with no named member undefined —
1884    // and C++ declares it, so the C column never saw the shape.
1885    if tail.possible() && !members.is_empty() {
1886        statements.push(T::declare(
1887            T::scalar(Scalar::Bool),
1888            "zd_tail",
1889            Some(literal::<T>("false")),
1890        ));
1891    }
1892    for (index, slot) in members.iter().enumerate() {
1893        let place = arrow::<T>("v", &slot.field);
1894        if tail.possible() {
1895            // Once the writer's members have run out, every member from
1896            // here on takes its default rather than the bytes of
1897            // whatever follows. Latched, so a member that happens to
1898            // start at the delimiter's end does not un-set it.
1899            //
1900            // The latch half is left off the **first** member: the
1901            // declaration above sets `zd_tail` to `false` on the line
1902            // before, so `!zd_tail` there is true by construction. It
1903            // is not a style point. Elixir threads a rebinding out of a
1904            // block through a tuple, which is enough for its type
1905            // checker to flow-type the literal and report the guard as
1906            // a conditional that always holds — a warning the codec
1907            // gate compiles with warnings as errors. The member reads
1908            // stay uniform, which is what the shared walk is for; only
1909            // the one conjunct that cannot be false is gone.
1910            let reached = if index == 0 {
1911                T::paren(tail.reached::<T>())
1912            } else {
1913                T::binary(
1914                    "&&",
1915                    T::unary("!", name::<T>("zd_tail")),
1916                    T::paren(tail.reached::<T>()),
1917                )
1918            };
1919            statements.push(T::when(
1920                reached,
1921                T::block(vec![assign::<T>(
1922                    name::<T>("zd_tail"),
1923                    literal::<T>("true"),
1924                )]),
1925                None,
1926            ));
1927            let mut defaulted = Vec::new();
1928            if slot.optional {
1929                defaulted.extend(slot_set_present::<T>(slot, false));
1930            } else {
1931                let node = ast
1932                    .member(slot.member)
1933                    .map_err(|error| EncodeError::NoLayout {
1934                        detail: format!("{}: {error}", slot.field),
1935                    })?;
1936                let (room, into) = slot_default_place::<T>(ast, slot, place.clone());
1937                defaulted.extend(room);
1938                defaulted.extend(T::member_default(ast, node, slot.type_, into)?);
1939            }
1940            let read = plan_order_member::<T>(ast, slot, index, place)?;
1941            statements.push(T::when(
1942                name::<T>("zd_tail"),
1943                T::block(defaulted),
1944                Some(T::block(read)),
1945            ));
1946            continue;
1947        }
1948        statements.extend(plan_order_member::<T>(ast, slot, index, place)?);
1949    }
1950    Ok(statements)
1951}
1952
1953/// Where a positional aggregate's members stop, which is not always
1954/// where this reader's plan stops.
1955///
1956/// §7.2.4.4.8, Table 19: "Each non-optional member in a T1 object that
1957/// is not present in the T2 object takes its default value." The two
1958/// ways a writer's members can run out are the two shapes §7.2.4.2
1959/// gives an appendable type — delimited under encoding version 2, and
1960/// not delimited under version 1.
1961///
1962/// `@final` gets neither. Table 19 requires the same set of member IDs
1963/// on both sides of an assignment between final types, so a short stream
1964/// is a type mismatch: Cyclone and RTI refuse it and Fast DDS defaults,
1965/// measured 2026-08-09 as `evo-final-tail`, and the clause is with the
1966/// two.
1967#[derive(Debug, Clone, Copy)]
1968struct Tail {
1969    /// Whether the type carries a DHEADER, which says where the writer
1970    /// stopped.
1971    delimited: bool,
1972    /// Whether the aggregate is `@appendable`.
1973    appendable: bool,
1974}
1975
1976impl Tail {
1977    /// Whether this aggregate can meet a writer with fewer members.
1978    const fn possible(self) -> bool {
1979        self.appendable
1980    }
1981
1982    /// The C condition that says the writer's members have run out.
1983    ///
1984    /// Under XCDR2 the DHEADER is exactly that statement. Under XCDR1
1985    /// there is none, and all three vendors default the tail when the
1986    /// bytes run out instead — `evo-appendable-tail` under `xcdr1`,
1987    /// where Cyclone, RTI and Fast DDS each hand back `c = 0`. The test
1988    /// is `remaining() == 0` and not "fewer bytes than the member
1989    /// needs": an appendable type nested inside another still has the
1990    /// enclosing type's bytes behind it, so nothing fires and the read
1991    /// fails loudly further on, which is what Cyclone and RTI do with
1992    /// the nested case.
1993    fn reached<T: CodecTarget>(self) -> T::Expr {
1994        if self.delimited {
1995            return T::binary(">=", position::<T>(), name::<T>("zd_end"));
1996        }
1997        // A plain zero and not [`CodecTarget::unsigned_int`]'s count:
1998        // `reader_remaining` answers in the target's own size type —
1999        // `int` in the C#, Go, Java and F# runtimes, `usize` in Zig's —
2000        // and a count literal beside one is a comparison between two
2001        // widths. C# and Go let it through by promotion; F# does not,
2002        // and `r.ReaderRemaining() = 0u` is `error FS0001` there.
2003        T::binary("==", remaining::<T>(), T::int(0))
2004    }
2005}
2006
2007/// One member of a positional aggregate, read where the plan says.
2008fn plan_order_member<T: CodecTarget>(
2009    ast: &Ast,
2010    slot: &Slot<T>,
2011    index: usize,
2012    place: T::Expr,
2013) -> Result<Vec<T::Stmt>, EncodeError> {
2014    let mut statements = Vec::new();
2015    {
2016        match slot.header {
2017            MemberHeader::NoHeader => {
2018                if slot.optional {
2019                    let (room, into) = slot_destination::<T>(ast, slot);
2020                    statements.extend(room);
2021                    statements.extend(member_read::<T>(ast, slot, into));
2022                    statements.extend(slot_set_present::<T>(slot, true));
2023                } else if slot.external || slot.indirect {
2024                    // `slot_destination` answers `place` itself for a
2025                    // member the column holds inline, so this arm and
2026                    // the one below differ only where the column keeps
2027                    // something the read has to make first.
2028                    let (room, into) = slot_destination::<T>(ast, slot);
2029                    statements.extend(room);
2030                    statements.extend(member_read::<T>(ast, slot, into));
2031                } else {
2032                    statements.extend(member_read::<T>(ast, slot, place));
2033                }
2034            }
2035            MemberHeader::OptionalFlag => statements.extend(present_flag::<T>(ast, slot)),
2036            MemberHeader::OptionalCdr1 { .. } => {
2037                statements.extend(optional_parameter::<T>(ast, slot, index));
2038            }
2039            // The planner gives these two only to a member of a
2040            // `@mutable` type, which one of the two functions below
2041            // reads. A body that emitted them here would frame the
2042            // member one way and read it another.
2043            MemberHeader::EmHeader { .. } | MemberHeader::PidCdr1 { .. } => {
2044                return Err(EncodeError::NoLayout {
2045                    detail: format!(
2046                        "{}: an MMEMBER header in a type that is not `@mutable`",
2047                        slot.field
2048                    ),
2049                });
2050            }
2051        }
2052    }
2053    Ok(statements)
2054}
2055
2056/// Rule (20): the BOOLEAN `<is_present>`, then the value behind it.
2057///
2058/// The flag lands in a local and the branches say what the structure
2059/// holds, rather than the flag being assigned into the structure
2060/// directly: a target where absence *is* the value has nothing to
2061/// assign to — `v.x != null` is not somewhere a `boolean` can be put.
2062/// `zdw_read_bool` refuses a byte that is neither 0 nor 1, which is
2063/// what keeps a mis-framed stream from reading as "present".
2064fn present_flag<T: CodecTarget>(ast: &Ast, slot: &Slot<T>) -> Vec<T::Stmt> {
2065    let flag = format!("zd_has_{}", slot.field);
2066    let mut arrived = slot_set_present::<T>(slot, true);
2067    let (room, into) = slot_destination::<T>(ast, slot);
2068    arrived.extend(room);
2069    arrived.extend(member_read::<T>(ast, slot, into));
2070    vec![
2071        T::declare(
2072            T::scalar(Scalar::Bool),
2073            &flag,
2074            Some(call_expr::<T>("read_bool", Vec::new())),
2075        ),
2076        T::when(
2077            name::<T>(&flag),
2078            T::block(arrived),
2079            Some(T::block(slot_set_present::<T>(slot, false))),
2080        ),
2081    ]
2082}
2083
2084/// §7.4.1.2: the parameter header, whose declared length is zero when
2085/// the member took no value.
2086///
2087/// A header for another id is a refusal and not a rewind. The member is
2088/// not optional in the sense of "may be missing from the stream" — the
2089/// header is always there — so an id that is not this one means the
2090/// reader and the writer disagree about the type, and every byte after
2091/// it would come out of the wrong place.
2092fn optional_parameter<T: CodecTarget>(ast: &Ast, slot: &Slot<T>, index: usize) -> Vec<T::Stmt> {
2093    let header = format!("zd_p{index}");
2094    let flag = format!("zd_has_{}", slot.field);
2095    let mine = T::binary(
2096        "&&",
2097        T::paren(status_is_ok::<T>()),
2098        T::paren(T::binary(
2099            "&&",
2100            T::paren(T::binary(
2101                "==",
2102                T::header_field(name::<T>(&header), "pid"),
2103                T::unsigned_int(i128::from(slot.member_id)),
2104            )),
2105            T::paren(T::binary(
2106                "==",
2107                T::header_field(name::<T>(&header), "end"),
2108                literal::<T>("false"),
2109            )),
2110        )),
2111    );
2112    let mut arrived = slot_set_present::<T>(slot, true);
2113    let (room, into) = slot_destination::<T>(ast, slot);
2114    arrived.extend(room);
2115    arrived.extend(member_read::<T>(ast, slot, into));
2116    vec![
2117        T::declare(
2118            T::handle(Handle::PidRead),
2119            &header,
2120            Some(call_expr::<T>(
2121                "read_pid_as",
2122                vec![T::constant(Constant::ParameterOptional)],
2123            )),
2124        ),
2125        T::when(
2126            mine,
2127            T::block(vec![
2128                T::declare(
2129                    T::scalar(Scalar::Bool),
2130                    &flag,
2131                    Some(T::binary(
2132                        "!=",
2133                        T::header_field(name::<T>(&header), "length"),
2134                        T::unsigned_int(0),
2135                    )),
2136                ),
2137                T::when(
2138                    name::<T>(&flag),
2139                    T::block(arrived),
2140                    Some(T::block(slot_set_present::<T>(slot, false))),
2141                ),
2142                T::perform(call_expr::<T>("finish_pid_read", vec![name::<T>(&header)])),
2143            ]),
2144            Some(T::block(vec![
2145                refuse::<T>(Constant::StatusOutOfRange),
2146                T::give_back(None),
2147            ])),
2148        ),
2149    ]
2150}
2151
2152/// A `@mutable` aggregate under XCDR2: EMHEADERs until the delimiter's
2153/// extent is used up.
2154///
2155/// Driven by the headers and not by the plan, because that is what the
2156/// headers are for.
2157fn member_headers<T: CodecTarget>(
2158    ast: &Ast,
2159    members: &[Slot<T>],
2160) -> Result<Vec<T::Stmt>, EncodeError> {
2161    let mut statements = Vec::new();
2162    for (index, slot) in members.iter().enumerate() {
2163        let _ = slot;
2164        statements.push(T::declare(
2165            T::scalar(Scalar::Bool),
2166            &format!("zd_seen{index}"),
2167            Some(literal::<T>("false")),
2168        ));
2169    }
2170    statements.push(T::declare(T::handle(Handle::EmHeaderRead), "zd_h", None));
2171    statements.push(T::declare(
2172        T::scalar(Scalar::Size),
2173        "zd_ends",
2174        Some(literal::<T>("0")),
2175    ));
2176
2177    // The unknown-member arm, built first so the known ones can chain
2178    // in front of it. §7.4.3.5.3: a reader that cannot skip a member it
2179    // must understand may not pretend it did.
2180    let mut chain = Some(T::block(vec![T::when(
2181        T::header_field(name::<T>("zd_h"), "must_understand"),
2182        T::block(vec![refuse::<T>(Constant::StatusOutOfRange)]),
2183        Some(T::block(vec![T::perform(call_expr::<T>(
2184            "reader_skip_to",
2185            vec![name::<T>("zd_ends")],
2186        ))])),
2187    )]));
2188
2189    for (index, slot) in members.iter().enumerate().rev() {
2190        let seen = format!("zd_seen{index}");
2191        // §7.4.3.4.2 gives each member one header, so a second one for
2192        // the same id is a stream that says two things.
2193        let mut arm = vec![T::when(
2194            name::<T>(&seen),
2195            T::block(vec![
2196                refuse::<T>(Constant::StatusOutOfRange),
2197                T::give_back(None),
2198            ]),
2199            None,
2200        )];
2201        arm.push(assign::<T>(name::<T>(&seen), literal::<T>("true")));
2202        if slot.optional {
2203            let (room, into) = slot_destination::<T>(ast, slot);
2204            arm.extend(room);
2205            arm.extend(member_read::<T>(ast, slot, into));
2206            arm.extend(slot_set_present::<T>(slot, true));
2207        } else {
2208            let (room, into) = slot_destination::<T>(ast, slot);
2209            arm.extend(room);
2210            arm.extend(member_read::<T>(ast, slot, into));
2211        }
2212        arm.push(ends_where_the_header_said::<T>("zd_ends"));
2213        chain = Some(T::block(vec![T::when(
2214            T::binary(
2215                "==",
2216                T::header_field(name::<T>("zd_h"), "member_id"),
2217                T::unsigned_int(i128::from(slot.member_id)),
2218            ),
2219            T::block(arm),
2220            chain,
2221        )]));
2222    }
2223
2224    let mut loop_body = vec![
2225        assign::<T>(
2226            name::<T>("zd_h"),
2227            call_expr::<T>("read_emheader", Vec::new()),
2228        ),
2229        T::when(
2230            T::binary(
2231                "!=",
2232                call_expr::<T>("reader_status", Vec::new()),
2233                T::constant(Constant::StatusOk),
2234            ),
2235            T::block(vec![T::leave()]),
2236            None,
2237        ),
2238        assign::<T>(
2239            name::<T>("zd_ends"),
2240            offset_plus::<T>(
2241                position::<T>(),
2242                T::header_field(name::<T>("zd_h"), "extent"),
2243            ),
2244        ),
2245    ];
2246    if let Some(chain) = chain {
2247        loop_body.extend(T::into_statements(chain));
2248    }
2249
2250    statements.push(T::until(
2251        T::binary(
2252            "&&",
2253            T::paren(T::binary("<", position::<T>(), name::<T>("zd_end"))),
2254            T::paren(status_is_ok::<T>()),
2255        ),
2256        T::block(loop_body),
2257    ));
2258    statements.extend(defaults_for_absent::<T>(ast, members)?);
2259    Ok(statements)
2260}
2261
2262/// A `@mutable` aggregate under XCDR1: parameters until `PID_LIST_END`.
2263fn parameter_list<T: CodecTarget>(
2264    ast: &Ast,
2265    members: &[Slot<T>],
2266) -> Result<Vec<T::Stmt>, EncodeError> {
2267    let mut statements = Vec::new();
2268    for index in 0..members.len() {
2269        statements.push(T::declare(
2270            T::scalar(Scalar::Bool),
2271            &format!("zd_seen{index}"),
2272            Some(literal::<T>("false")),
2273        ));
2274    }
2275    statements.push(T::declare(T::handle(Handle::PidRead), "zd_p", None));
2276    statements.push(T::declare(
2277        T::scalar(Scalar::Size),
2278        "zd_at",
2279        Some(literal::<T>("0")),
2280    ));
2281
2282    // Stepping over a parameter this type has no member for, in the one
2283    // place three different reasons reach it.
2284    let skip_parameter = T::block(vec![T::perform(call_expr::<T>(
2285        "reader_skip_to",
2286        vec![offset_plus::<T>(
2287            name::<T>("zd_at"),
2288            T::header_field(name::<T>("zd_p"), "length"),
2289        )],
2290    ))]);
2291    // A parameter this type does not declare. PL_CDR1 carries a length
2292    // on every parameter, so skipping is always possible — but
2293    // §7.2.2.4.4.4.6 says a reader that does not understand a
2294    // must-understand member shall discard the whole sample rather than
2295    // step over it. The XCDR2 arm below already refused; this one
2296    // skipped in silence.
2297    let unknown = T::block(vec![T::when(
2298        T::header_field(name::<T>("zd_p"), "must_understand"),
2299        T::block(vec![
2300            refuse::<T>(Constant::StatusOutOfRange),
2301            T::give_back(None),
2302        ]),
2303        Some(skip_parameter.clone()),
2304    )]);
2305    let mut chain = Some(unknown.clone());
2306
2307    for (index, slot) in members.iter().enumerate().rev() {
2308        let seen = format!("zd_seen{index}");
2309        let mut arm = vec![T::when(
2310            name::<T>(&seen),
2311            T::block(vec![
2312                refuse::<T>(Constant::StatusOutOfRange),
2313                T::give_back(None),
2314            ]),
2315            None,
2316        )];
2317        arm.push(assign::<T>(name::<T>(&seen), literal::<T>("true")));
2318        if slot.optional {
2319            let (room, into) = slot_destination::<T>(ast, slot);
2320            arm.extend(room);
2321            arm.extend(member_read::<T>(ast, slot, into));
2322            arm.extend(slot_set_present::<T>(slot, true));
2323        } else {
2324            let (room, into) = slot_destination::<T>(ast, slot);
2325            arm.extend(room);
2326            arm.extend(member_read::<T>(ast, slot, into));
2327        }
2328        chain = Some(T::block(vec![T::when(
2329            T::binary(
2330                "==",
2331                T::header_field(name::<T>("zd_p"), "pid"),
2332                T::unsigned_int(i128::from(slot.member_id)),
2333            ),
2334            T::block(arm),
2335            chain,
2336        )]));
2337    }
2338
2339    // §7.4.1.2.4: the list ends at the sentinel. A stream that runs out
2340    // first is one whose end nobody wrote.
2341    let mut loop_body = vec![
2342        T::when(
2343            T::binary("<", remaining::<T>(), T::int(4)),
2344            T::block(vec![refuse::<T>(Constant::StatusTruncated), T::leave()]),
2345            None,
2346        ),
2347        assign::<T>(name::<T>("zd_p"), call_expr::<T>("read_pid", Vec::new())),
2348        T::when(
2349            T::binary(
2350                "!=",
2351                call_expr::<T>("reader_status", Vec::new()),
2352                T::constant(Constant::StatusOk),
2353            ),
2354            T::block(vec![T::leave()]),
2355            None,
2356        ),
2357        T::when(
2358            T::header_field(name::<T>("zd_p"), "end"),
2359            T::block(vec![T::leave()]),
2360            None,
2361        ),
2362        assign::<T>(name::<T>("zd_at"), position::<T>()),
2363    ];
2364    // The same two gates the Rust and Python columns put in front of
2365    // the id chain, in the same order. `PID_IGNORE` first, because
2366    // §7.4.1.2.2 states no exception for the must-understand bit that
2367    // `unknown` would refuse on; then `zdw_pid_is_user_member`, which is
2368    // false for a vendor-private parameter and for a reserved id.
2369    if let Some(chain) = chain {
2370        loop_body.push(T::when(
2371            T::header_field(name::<T>("zd_p"), "ignore"),
2372            skip_parameter,
2373            Some(T::block(vec![T::when(
2374                T::runtime_static("pid_is_user_member", vec![name::<T>("zd_p")]),
2375                chain,
2376                Some(unknown),
2377            )])),
2378        ));
2379    }
2380    loop_body.push(T::perform(call_expr::<T>(
2381        "finish_pid_read",
2382        vec![name::<T>("zd_p")],
2383    )));
2384
2385    statements.push(T::until(status_is_ok::<T>(), T::block(loop_body)));
2386    statements.extend(defaults_for_absent::<T>(ast, members)?);
2387    Ok(statements)
2388}
2389
2390/// The members the stream did not carry, each taking its default.
2391///
2392/// §7.4.1.2.3: "any mutable structure member's value may be omitted.
2393/// For non-optional members this may be due to the sender having a
2394/// compatible type that does not have that member. In such a case, if
2395/// the receiver's member is not optional, it logically takes its
2396/// default value."
2397///
2398/// This column used to refuse with `ZDW_TRUNCATED`, which made a new
2399/// subscriber unable to receive from an old publisher — the exchange
2400/// `@mutable` exists for. Cyclone, RTI and Fast DDS all hand back the
2401/// default; measured 2026-08-09 as `evo-mutable-tail`,
2402/// `evo-mutable-middle` and `evo-mutable-defaults`, where all three
2403/// agree with Table 9 on every kind in it.
2404///
2405/// `@must_understand` makes no difference here, and that is measured
2406/// too: `evo-mutable-mustunderstand`, all three vendors, both encoding
2407/// versions. `crates/wire-probe/src/decode.rs::require_present` carries
2408/// the argument.
2409fn defaults_for_absent<T: CodecTarget>(
2410    ast: &Ast,
2411    members: &[Slot<T>],
2412) -> Result<Vec<T::Stmt>, EncodeError> {
2413    let mut statements = Vec::new();
2414    for (index, slot) in members.iter().enumerate() {
2415        // "If the member is optional, it takes no value at all." The
2416        // `_has_` flag was set false before the loop that reads, so
2417        // there is nothing to do.
2418        if slot.optional {
2419            continue;
2420        }
2421        let node = ast
2422            .member(slot.member)
2423            .map_err(|error| EncodeError::NoLayout {
2424                detail: format!("{}: {error}", slot.field),
2425            })?;
2426        let (room, into) = slot_default_place::<T>(ast, slot, arrow::<T>("v", &slot.field));
2427        let mut defaulted = room;
2428        defaulted.extend(T::member_default(ast, node, slot.type_, into)?);
2429        statements.push(T::when(
2430            T::binary(
2431                "==",
2432                name::<T>(&format!("zd_seen{index}")),
2433                literal::<T>("false"),
2434            ),
2435            T::block(defaulted),
2436            None,
2437        ));
2438    }
2439    Ok(statements)
2440}
2441
2442/// The member has to end where its header said it would.
2443fn ends_where_the_header_said<T: CodecTarget>(ends: &str) -> T::Stmt {
2444    T::when(
2445        T::binary("!=", position::<T>(), name::<T>(ends)),
2446        T::block(vec![refuse::<T>(Constant::StatusOutOfRange)]),
2447        None,
2448    )
2449}
2450
2451/// A union: the discriminator, then the member its labels select.
2452///
2453/// A discriminator matching no label and a union with no `default:` is
2454/// a legal outcome and reads no member at all —
2455/// `union-default-unselected` in the corpus is that sample.
2456fn union_body<T: CodecTarget>(
2457    ast: &Ast,
2458    id: Id<TypeNode>,
2459    key: LayoutKey,
2460    discriminator: Option<Id<TypeNode>>,
2461    own_name: &str,
2462) -> Result<Vec<T::Stmt>, EncodeError> {
2463    let Some(discriminator) = discriminator else {
2464        return Err(EncodeError::NoWriter {
2465            member: own_name.to_string(),
2466            kind: "union with an unresolved discriminator",
2467        });
2468    };
2469    let mut deps = DepCollector::default();
2470    let layout = zerodds_ast_layout::xcdr::plan(ast, id, key, &mut deps)
2471        .map_err(crate::encode::plan_refusal)?;
2472    // The one crossing [`UnionLabelMatch`] has. Under the default it is
2473    // false for every discriminator and the arm chain below is the
2474    // clause's, unchanged.
2475    let suppress_negative = zerodds_ast_layout::plan::union_negative_selects_nothing(ast, id, key);
2476
2477    let mut statements = Vec::new();
2478    // `ElementPadding::Packed`: a union is its own alignment block on the
2479    // read side too. See the writer's `write_union`.
2480    let packed = key.element_padding == ElementPadding::Packed;
2481    if packed {
2482        statements.push(begin_packed::<T>());
2483    }
2484    if layout.needs_dheader {
2485        statements.extend(read_delimiter::<T>());
2486    }
2487    // Rules (27) and (28): a `@mutable` union frames its discriminator
2488    // as an `MMEMBER` at member id 0, and the discriminator comes first
2489    // because nothing else says which member follows it.
2490    statements.extend(read_mmember::<T>(
2491        layout.discriminator,
2492        0,
2493        "zd_hd",
2494        // §7.3.1.2.1.12.5: the discriminator has a slot of its own,
2495        // and Table 19 admits only DEFAULT there.
2496        decode_into::<T>(
2497            ast,
2498            discriminator,
2499            T::discriminant("v"),
2500            ast.discriminator_try_construct(id),
2501        ),
2502    ));
2503
2504    // The default branch's member is still set from Table 9's default;
2505    // the branch the discriminator selects is the only one left live.
2506    // Every case, or the body is refused: a case skipped here keeps the
2507    // default value it was constructed with, and the decoded union then
2508    // shows a branch its discriminator did not select.
2509    let case_fields: Vec<String> = ast
2510        .cases_of(id)
2511        .iter()
2512        .map(|case| {
2513            ast.case(*case)
2514                .and_then(|case_node| ast.member(case_node.member))
2515                .map(|member| T::escape(&member.name))
2516                .map_err(|_| EncodeError::NoWriter {
2517                    member: own_name.to_string(),
2518                    kind: "union case the model does not hold",
2519                })
2520        })
2521        .collect::<Result<_, _>>()?;
2522    statements.extend(T::clear_union_cases("v", &case_fields));
2523
2524    // Whether the sentinel that ends the parameter list has already
2525    // been consumed, which happens where the writer's union carried no
2526    // member and this one's discriminator selects one.
2527    //
2528    // Declared wherever a **branch takes a parameter header**, and not
2529    // wherever the list has a sentinel. The two used to coincide —
2530    // `needs_sentinel` was `encoding.is_v1() && mutable`, which is
2531    // exactly when a branch gets a `PidCdr1` header — and
2532    // `MutableUnionFraming::BranchesFromZero` separates them: it
2533    // suppresses the sentinel and keeps the headers, and
2534    // `read_union_member` assigns `zd_sent` from the header's `end`
2535    // whatever the list ends with. Gating the declaration on the
2536    // sentinel left the assignment referring to a variable that does not
2537    // exist, and the C column did not compile.
2538    let takes_parameter_headers = layout
2539        .members
2540        .iter()
2541        .any(|plan| matches!(plan.header, MemberHeader::PidCdr1 { .. }));
2542    if takes_parameter_headers {
2543        statements.push(T::declare(
2544            T::scalar(Scalar::Bool),
2545            "zd_sent",
2546            Some(literal::<T>("false")),
2547        ));
2548    }
2549
2550    let mut arms: Vec<(T::Expr, T::Block)> = Vec::new();
2551    let mut fallback: Option<T::Block> = None;
2552    for case in ast.cases_of(id) {
2553        let Ok(case_node) = ast.case(*case) else {
2554            continue;
2555        };
2556        let Ok(member) = ast.member(case_node.member) else {
2557            continue;
2558        };
2559        let Some(member_type) = member.type_.resolved() else {
2560            return Err(EncodeError::NoWriter {
2561                member: member.name.clone(),
2562                kind: "union case with an unresolved type",
2563            });
2564        };
2565        let field = T::escape(&member.name);
2566        let planned = layout
2567            .members
2568            .iter()
2569            .find(|plan| plan.member == member.node_id);
2570        // Table 21: `@external` changes no octet, only where the binding
2571        // keeps the value — a column that keeps an indirection has to
2572        // make storage before there is a value to write into, exactly as
2573        // a structure member does.
2574        let external = super::reached_externally::<T>(ast, member);
2575        let (mut setup, place) = if external {
2576            T::external_case_destination("v", &field)
2577        } else {
2578            (Vec::new(), T::case_member("v", &field))
2579        };
2580        // A branch whose value has no room inside the union is reached
2581        // through the column's indirection, exactly as a structure
2582        // member is.
2583        let place = if super::behind_pointer::<T>(ast, id, member_type, external, false) {
2584            let (made, through) = T::make_indirection(ast, member_type, place);
2585            setup.extend(made);
2586            through
2587        } else {
2588            place
2589        };
2590        let mut arm_statements = setup;
2591        arm_statements.extend(read_union_member::<T>(
2592            planned.map(|plan| plan.header),
2593            planned.map_or(0, |plan| plan.member_id),
2594            &format!("zd_hu{}", planned.map_or(0, |plan| plan.member_id)),
2595            decode_into::<T>(
2596                ast,
2597                member_type,
2598                place.clone(),
2599                ast.effective_try_construct(
2600                    member.facets.xtypes.as_ref().and_then(|x| x.try_construct),
2601                    Some(member_type),
2602                ),
2603            ),
2604            // M11, §7.2.4.4.8, Table 19, the UNION_TYPE row: "If the
2605            // discriminator value does not select any member in O2,
2606            // then: If the discriminator value selects a member m1
2607            // in O1, then m1 is set to its default value according
2608            // to its type."
2609            T::default_into(ast, member_type, place)?,
2610        ));
2611        let read = T::block(arm_statements);
2612
2613        let mut condition: Option<T::Expr> = None;
2614        let mut is_default = false;
2615        for case_label in &case_node.labels {
2616            match case_label {
2617                CaseLabel::Default => is_default = true,
2618                CaseLabel::Value(expr) => {
2619                    let value = ast
2620                        .eval_integral(*expr)
2621                        .map_err(|_| EncodeError::NoWriter {
2622                            member: member.name.clone(),
2623                            kind: "union label that does not evaluate",
2624                        })?;
2625                    // A label a setting says selects nothing gets no arm
2626                    // at all, so the chain falls through to `default:`
2627                    // where the union has one and past the whole union
2628                    // where it has none — which is what the C peer this
2629                    // reproduces does with its own real union.
2630                    // `zerodds_ast_layout::union_negative_matches_nothing`.
2631                    if suppress_negative && value < 0 {
2632                        continue;
2633                    }
2634                    let test = T::binary(
2635                        "==",
2636                        T::discriminant("v"),
2637                        T::case_label(ast, discriminator, value),
2638                    );
2639                    condition = Some(match condition {
2640                        None => test,
2641                        Some(previous) => T::binary("||", previous, test),
2642                    });
2643                }
2644            }
2645        }
2646        if is_default {
2647            fallback = Some(read.clone());
2648        }
2649        if let Some(condition) = condition {
2650            arms.push((condition, read));
2651        }
2652    }
2653
2654    let mut chain: Option<T::Block> = fallback;
2655    for (condition, block) in arms.into_iter().rev() {
2656        chain = Some(T::block(vec![T::when(condition, block, chain)]));
2657    }
2658    if let Some(chain) = chain {
2659        statements.extend(T::into_statements(chain));
2660    }
2661
2662    // The C probe is built with `-Wall -Wextra -Werror`, so a variable
2663    // that is set and never read fails the build. Under a framing that
2664    // writes no sentinel nothing reads `zd_sent`, and `(void)` is what
2665    // says so out loud rather than leaving the declaration out and the
2666    // assignment dangling.
2667    if takes_parameter_headers && !layout.needs_sentinel {
2668        statements.extend(T::mark_used("zd_sent"));
2669    }
2670
2671    // Rule (28): the list ends at its sentinel, selected member or not
2672    // — unless the member's own read already consumed it, which is what
2673    // says the writer wrote no member at all.
2674    if layout.needs_sentinel {
2675        statements.push(T::when(
2676            T::unary("!", name::<T>("zd_sent")),
2677            T::block(vec![
2678                T::declare(
2679                    T::handle(Handle::PidRead),
2680                    "zd_pe",
2681                    Some(call_expr::<T>("read_pid", Vec::new())),
2682                ),
2683                T::when(
2684                    T::binary(
2685                        "==",
2686                        T::header_field(name::<T>("zd_pe"), "end"),
2687                        literal::<T>("false"),
2688                    ),
2689                    T::block(vec![
2690                        refuse::<T>(Constant::StatusOutOfRange),
2691                        T::give_back(None),
2692                    ]),
2693                    None,
2694                ),
2695            ]),
2696            None,
2697        ));
2698    }
2699
2700    if layout.needs_dheader {
2701        statements.extend(close_delimiter::<T>());
2702    }
2703    if packed {
2704        statements.push(finish_packed::<T>());
2705    }
2706    Ok(statements)
2707}
2708
2709/// The member a union's discriminator selects, or its default where the
2710/// stream does not carry one.
2711///
2712/// M11. §7.2.4.4.8, Table 19, the UNION_TYPE row: "If the discriminator
2713/// value does not select any member in O2, then: If the discriminator
2714/// value selects a member m1 in O1, then m1 is set to its default value
2715/// according to its type." Two versions of a union whose newer one gave
2716/// an existing discriminator value a case is exactly that, and RTI is
2717/// the witness — `evo-union-unselected`, `{"_d":2,"b":0}` in both
2718/// encoding versions.
2719///
2720/// Which of the two the stream is saying is read off the **framing** and
2721/// never guessed. Under XCDR1 a `PID_LIST_END` where the member's
2722/// parameter would be is the writer saying it wrote none, and the
2723/// sentinel is consumed by reading it, so `zd_sent` tells the caller not
2724/// to read it a second time. Under XCDR2 the delimiter's extent says the
2725/// union is over. A `@final` union has neither, which is right: Table 19
2726/// requires two final unions to have an identical set of labels, so a
2727/// stream missing the member is a type mismatch and not an evolution.
2728fn read_union_member<T: CodecTarget>(
2729    header: Option<MemberHeader>,
2730    member_id: u32,
2731    handle: &str,
2732    value: T::Stmt,
2733    default: T::Stmt,
2734) -> Vec<T::Stmt> {
2735    match header {
2736        // Rule (28), the parameter list.
2737        Some(MemberHeader::PidCdr1 { .. }) => vec![
2738            T::declare(
2739                T::handle(Handle::PidRead),
2740                handle,
2741                Some(call_expr::<T>("read_pid", Vec::new())),
2742            ),
2743            T::when(
2744                T::header_field(name::<T>(handle), "end"),
2745                T::block(vec![
2746                    default.clone(),
2747                    assign::<T>(name::<T>("zd_sent"), literal::<T>("true")),
2748                ]),
2749                Some(T::block(vec![
2750                    T::when(
2751                        T::binary(
2752                            "!=",
2753                            T::header_field(name::<T>(handle), "pid"),
2754                            T::unsigned_int(i128::from(member_id)),
2755                        ),
2756                        T::block(vec![
2757                            refuse::<T>(Constant::StatusOutOfRange),
2758                            T::give_back(None),
2759                        ]),
2760                        None,
2761                    ),
2762                    value.clone(),
2763                    T::perform(call_expr::<T>("finish_pid_read", vec![name::<T>(handle)])),
2764                ])),
2765            ),
2766        ],
2767        // Rule (27) under a DHEADER.
2768        Some(MemberHeader::EmHeader { .. }) => vec![T::when(
2769            T::binary(">=", position::<T>(), name::<T>("zd_end")),
2770            T::block(vec![default]),
2771            Some(T::block(read_mmember::<T>(
2772                header, member_id, handle, value,
2773            ))),
2774        )],
2775        // No framing to read an absence off, so there is none.
2776        _ => read_mmember::<T>(header, member_id, handle, value),
2777    }
2778}
2779
2780/// One value behind the `MMEMBER` header the plan gave it, or bare.
2781///
2782/// `None` is a union of any extensibility but `@mutable`. The id is
2783/// checked and not trusted: a header for another member is not one this
2784/// decoder can step past, because the value behind it belongs to
2785/// something else and every byte after it would come out of the wrong
2786/// place.
2787fn read_mmember<T: CodecTarget>(
2788    header: Option<MemberHeader>,
2789    member_id: u32,
2790    handle: &str,
2791    value: T::Stmt,
2792) -> Vec<T::Stmt> {
2793    let wrong_id = |mine: T::Expr| {
2794        T::when(
2795            T::unary("!", T::paren(mine)),
2796            T::block(vec![
2797                refuse::<T>(Constant::StatusOutOfRange),
2798                T::give_back(None),
2799            ]),
2800            None,
2801        )
2802    };
2803    match header {
2804        None | Some(MemberHeader::NoHeader) => vec![value],
2805        Some(MemberHeader::PidCdr1 { .. }) => vec![
2806            T::declare(
2807                T::handle(Handle::PidRead),
2808                handle,
2809                Some(call_expr::<T>("read_pid", Vec::new())),
2810            ),
2811            wrong_id(T::binary(
2812                "&&",
2813                T::paren(T::binary(
2814                    "==",
2815                    T::header_field(name::<T>(handle), "pid"),
2816                    T::unsigned_int(i128::from(member_id)),
2817                )),
2818                T::paren(T::binary(
2819                    "==",
2820                    T::header_field(name::<T>(handle), "end"),
2821                    literal::<T>("false"),
2822                )),
2823            )),
2824            value,
2825            T::perform(call_expr::<T>("finish_pid_read", vec![name::<T>(handle)])),
2826        ],
2827        Some(MemberHeader::EmHeader { .. }) => vec![
2828            T::declare(
2829                T::handle(Handle::EmHeaderRead),
2830                handle,
2831                Some(call_expr::<T>("read_emheader", Vec::new())),
2832            ),
2833            wrong_id(T::binary(
2834                "==",
2835                T::header_field(name::<T>(handle), "member_id"),
2836                T::unsigned_int(i128::from(member_id)),
2837            )),
2838            value,
2839        ],
2840        // Neither `OPT_FMEMBER` form reaches a union: a case member is
2841        // selected by the discriminator and is never `@optional`.
2842        Some(MemberHeader::OptionalCdr1 { .. } | MemberHeader::OptionalFlag) => vec![value],
2843    }
2844}
2845
2846/// The call one scalar position reads with.
2847///
2848/// Every policy is already answered: `zerodds_ast_layout::plan::scalar_write`
2849/// resolved them against the key and the position, so nothing here
2850/// reads an axis and nothing here can pass the wrong argument to one.
2851/// The same instruction the writer takes, which is what makes the two
2852/// directions answerable against each other.
2853fn primitive_read_at<T: CodecTarget>(write: ScalarWrite) -> T::Expr {
2854    match write {
2855        ScalarWrite::Bool => call_expr::<T>("read_bool", Vec::new()),
2856        ScalarWrite::U8 => call_expr::<T>("read_u8", Vec::new()),
2857        ScalarWrite::I8 => call_expr::<T>("read_i8", Vec::new()),
2858        // The runtime hands back a code point, which is wider than the
2859        // C storage. The narrowing is written out so that it is a
2860        // decision in the generated code and not a conversion the
2861        // compiler makes quietly — and an IDL `char` is one octet, so
2862        // there is nothing above the low eight bits to lose.
2863        ScalarWrite::Char => cast::<T>(
2864            T::scalar(Scalar::Char),
2865            call_expr::<T>("read_char", Vec::new()),
2866        ),
2867        ScalarWrite::Wchar {
2868            width,
2869            length_octet,
2870            order,
2871        } => cast::<T>(
2872            T::scalar(Scalar::UInt16),
2873            call_expr::<T>(
2874                "read_wchar",
2875                vec![
2876                    T::constant(match width {
2877                        WcharWidth::Two => Constant::WcharWidth2,
2878                        WcharWidth::Four => Constant::WcharWidth4,
2879                    }),
2880                    T::constant(if length_octet {
2881                        Constant::WcharGiopLengthPrefixed
2882                    } else {
2883                        Constant::WcharFixed
2884                    }),
2885                    T::constant(wstring_unit_order(order)),
2886                ],
2887            ),
2888        ),
2889        ScalarWrite::I16 => call_expr::<T>("read_i16", Vec::new()),
2890        ScalarWrite::U16 => call_expr::<T>("read_u16", Vec::new()),
2891        ScalarWrite::I32 => call_expr::<T>("read_i32", Vec::new()),
2892        ScalarWrite::U32 => call_expr::<T>("read_u32", Vec::new()),
2893        ScalarWrite::I64 => call_expr::<T>("read_i64", Vec::new()),
2894        ScalarWrite::U64 => call_expr::<T>("read_u64", Vec::new()),
2895        ScalarWrite::F32 => call_expr::<T>("read_f32", Vec::new()),
2896        ScalarWrite::F64 => call_expr::<T>("read_f64", Vec::new()),
2897        ScalarWrite::F128 => call_expr::<T>("read_f128", Vec::new()),
2898        // The reader frames these itself: a string allocates, a fixed
2899        // takes its digits from the type, an enumeration reads a holder
2900        // and maps it back. They reach this function through their own
2901        // arms in `body_of`.
2902        ScalarWrite::Str
2903        | ScalarWrite::Wstr { .. }
2904        | ScalarWrite::Fixed { .. }
2905        | ScalarWrite::Enum { .. }
2906        | ScalarWrite::Bitmask { .. } => call_expr::<T>("read_u8", Vec::new()),
2907    }
2908}
2909
2910/// The C name of a `wstring` length policy.
2911const fn wstring_policy(policy: WstringLengthPolicy) -> Constant {
2912    match policy {
2913        WstringLengthPolicy::SPECIFICATION => Constant::WstringSpecification,
2914        WstringLengthPolicy::CHARACTER_COUNT => Constant::WstringCharacterCount,
2915        WstringLengthPolicy::CHARACTER_COUNT_TERMINATED => {
2916            Constant::WstringCharacterCountTerminated
2917        }
2918        WstringLengthPolicy::SPECIFICATION_WIDE_UNITS => Constant::WstringSpecificationWideUnits,
2919        WstringLengthPolicy::ROS_TYPE_SUPPORT => Constant::WstringRosTypeSupport,
2920        // The three cells no measured implementation writes. Named on
2921        // both sides so the C emitter can reach every cell the Rust
2922        // model can; a mapping that stopped at the measured five would
2923        // put the emitters back where the enumeration had them.
2924        _ => match (policy.counts, policy.terminator, policy.unit) {
2925            (WstringLengthCounts::Octets, _, WstringUnitWidth::Two) => {
2926                Constant::WstringOctetsTerminated2
2927            }
2928            (WstringLengthCounts::Octets, _, WstringUnitWidth::Four) => {
2929                Constant::WstringOctetsTerminated4
2930            }
2931            _ => Constant::WstringCharactersTerminated2,
2932        },
2933    }
2934}
2935
2936/// The C name of a `wstring` unit order.
2937const fn wstring_unit_order(order: WstringUnitOrder) -> Constant {
2938    match order {
2939        WstringUnitOrder::Stream => Constant::WstringUnitsStream,
2940        WstringUnitOrder::BigEndianAlways => Constant::WstringUnitsBigEndianAlways,
2941        WstringUnitOrder::ByteOrderMark => Constant::WstringUnitsByteOrderMark,
2942        WstringUnitOrder::HostUnswapped => Constant::WstringUnitsHostUnswapped,
2943    }
2944}
2945
2946// -- small builders ------------------------------------------------------
2947
2948/// zerodds-lint: recursion-depth 1 — this does not recurse. The callee
2949/// is `T::name`; `dds_bounded_recursion` compares only the last
2950/// segment of a call path, so it reads as a self-call.
2951fn name<T: CodecTarget>(text: &str) -> T::Expr {
2952    T::name(text)
2953}
2954
2955/// An integer whose written form matters — a `u` suffix, a cast.
2956///
2957/// The suffixed forms are C's and are what this file has always
2958/// emitted; they stay here rather than in the backend because a target
2959/// that does not need them would not know what to drop.
2960///
2961/// zerodds-lint: recursion-depth 1 — this does not recurse. The callee
2962/// is `T::literal`; `dds_bounded_recursion` compares only the last
2963/// segment of a call path, so it reads as a self-call.
2964fn literal<T: CodecTarget>(text: &str) -> T::Expr {
2965    T::literal(Literal::Int(text.to_string()))
2966}
2967
2968/// zerodds-lint: recursion-depth 1 — this does not recurse. The callee
2969/// is `T::int`; `dds_bounded_recursion` compares only the last
2970/// segment of a call path, so it reads as a self-call.
2971fn int<T: CodecTarget>(value: i128) -> T::Expr {
2972    T::int(value)
2973}
2974
2975fn deref<T: CodecTarget>(text: &str) -> T::Expr {
2976    T::whole(text)
2977}
2978
2979fn arrow<T: CodecTarget>(value: &str, field: &str) -> T::Expr {
2980    T::member(value, field)
2981}
2982
2983/// zerodds-lint: recursion-depth 1 — this does not recurse. The callee
2984/// is `T::cast`; `dds_bounded_recursion` compares only the last
2985/// segment of a call path, so it reads as a self-call.
2986fn cast<T: CodecTarget>(type_: T::Type, value: T::Expr) -> T::Expr {
2987    T::cast(type_, value)
2988}
2989
2990fn assign<T: CodecTarget>(place: T::Expr, value: T::Expr) -> T::Stmt {
2991    T::perform(T::binary("=", place, value))
2992}
2993
2994/// A runtime call on the reader.
2995///
2996/// `function_name` is the operation without the runtime's prefix —
2997/// `read_u32`, not `zdw_read_u32` — and the receiver is prepended by
2998/// the backend.
2999fn call_expr<T: CodecTarget>(function_name: &str, args: Vec<T::Expr>) -> T::Expr {
3000    T::runtime("r", function_name, args)
3001}
3002
3003/// The reader's offset into the stream.
3004///
3005/// # The offset domain
3006///
3007/// [`position`], [`remaining`] and the four locals this walk declares
3008/// with [`Scalar::Size`] — `zd_end`, `zd_ends`, `zd_at` and a map's
3009/// `zd_end` — are one width, and it is the target's own size type:
3010/// `size_t` in C, `int` in the C#, Go, Java and F# runtimes, `usize` in
3011/// Zig's, `Long` in Kotlin's. Nothing else is.
3012///
3013/// A DHEADER is a `uint32`, an EMHEADER's extent is a `uint32`, a PID's
3014/// length is a `uint16` and a sentinel's size is a literal — none of the
3015/// four is in the domain, and each has to be brought into it before it
3016/// is put beside one that is. Fifteen of the twenty columns widen the
3017/// narrower side themselves and never notice; F# and Kotlin convert
3018/// nothing implicitly and name a type error.
3019///
3020/// So the domain has exactly three entrances and no fourth:
3021///
3022/// - [`offset_plus`] for an addition — it casts the count;
3023/// - [`cast`] to [`Scalar::Size`] for a comparison against a count;
3024/// - [`CodecTarget::int`] and **not** [`CodecTarget::unsigned_int`] for a
3025///   literal, because `4u` is not a token in every language and is a
3026///   second width in the ones where it is.
3027///
3028/// Measured: `zd_ends <- r.ReaderPosition() + zd_h.Extent` was
3029/// `error FS0001` ("uint32 does not match int") in every F# run and
3030/// `assignment type mismatch: actual type is 'Long', but 'Int' was
3031/// expected` in every Kotlin XCDR2 run, and
3032/// `if r.ReaderRemaining() < 4u then` was the same error a second time
3033/// in the PL_CDR1 arm.
3034fn position<T: CodecTarget>() -> T::Expr {
3035    call_expr::<T>("reader_position", Vec::new())
3036}
3037
3038/// How many octets the reader has left, in the offset domain.
3039///
3040/// See [`position`] for what may be put beside it.
3041fn remaining<T: CodecTarget>() -> T::Expr {
3042    call_expr::<T>("reader_remaining", Vec::new())
3043}
3044
3045/// `base + count`, where `base` is in the offset domain and `count` is a
3046/// length the stream carried.
3047///
3048/// The cast is the whole point: see [`position`].
3049fn offset_plus<T: CodecTarget>(base: T::Expr, count: T::Expr) -> T::Expr {
3050    T::binary("+", base, cast::<T>(T::scalar(Scalar::Size), count))
3051}
3052
3053fn refuse<T: CodecTarget>(status: Constant) -> T::Stmt {
3054    T::perform(call_expr::<T>("reader_refuse", vec![T::constant(status)]))
3055}
3056
3057fn status_is_ok<T: CodecTarget>() -> T::Expr {
3058    T::binary(
3059        "==",
3060        call_expr::<T>("reader_status", Vec::new()),
3061        T::constant(Constant::StatusOk),
3062    )
3063}
3064
3065/// A DHEADER read and not kept.
3066///
3067/// A collection's delimiter says how long it is and the element count
3068/// says how many there are; the count is what the loop runs on, so the
3069/// length is read to move the cursor and nothing else. The same thing
3070/// the Rust reader does with it.
3071fn discard_dheader<T: CodecTarget>() -> T::Stmt {
3072    T::drop_value(call_expr::<T>("read_dheader", Vec::new()))
3073}
3074
3075/// A loop from zero to `count`, exclusive.
3076///
3077/// The backend's, and the same one the encoder uses. The reader spelled
3078/// its bounds `0u` and `1u` where the writer spelled them `0` and `1`;
3079/// two spellings of one loop in one language is a difference nothing
3080/// depended on and nobody chose.
3081fn counted_loop<T: CodecTarget>(index: &str, count: T::Expr, body: T::Block) -> T::Stmt {
3082    T::count_up(index, count, body)
3083}