Skip to main content

zerodds_lang_rules/emit/
encode.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Encoder bodies, built from the layout plan. Spec §5, §6.2.
4//!
5//! One walk, emitted through a [`CodecTarget`]. Which member is written,
6//! in what order, under which header and with which length code is
7//! decided here and once; how that is spelled is the backend's.
8//!
9//! The counterpart of [`crate::encode`], which does the same for Rust
10//! against the Rust CST directly and has not been brought over.
11//! Every framing decision is read from `zerodds-ast-layout` and none is
12//! re-derived: the plan is where the length-code policy and the XCDR2
13//! alignment rule live, and a second derivation would be a second place
14//! to get them wrong.
15//!
16//! What the emitted code does *not* delegate is the wire rule itself.
17//! Alignment, the string length convention, the UTF-16 count, the BCD
18//! packing, the enumerator's signed holder and the DHEADER, EMHEADER and
19//! PID framing are the target runtime's — `runtime/c`, `runtime/java`
20//! and the rest each implement them from the clause rather than calling
21//! the Rust one. That is what makes a column a second reading of the
22//! specification: where two disagree, one of them has misread it.
23//!
24//! ## What this walk assumes of every target
25//!
26//! - **A routine per type.** Rust's emitter names a writer method on the
27//!   element type; C has no methods, so every type gets its own routine
28//!   and collections call it by name. A target that would rather emit a
29//!   method says so in [`CodecTarget::routine`].
30//! - **Forward declarations are the target's business.** C needs them
31//!   ahead of the bodies so a recursive type does not depend on visit
32//!   order; a target with no such thing answers `None` and the loop
33//!   emits only bodies.
34//! - **A `switch` is an if-chain.** The union's branch is selected by
35//!   comparing the discriminator against each case's labels, which every
36//!   target can hold and which sidesteps the languages whose `switch`
37//!   will not take a 64-bit label.
38
39use crate::adapter::c::{ordered_types, ordered_types_from, ordered_types_from_pruning};
40use crate::emit::{CodecTarget, Constant, Handle, Handoff, Routine, Scalar};
41use crate::encode::EncodeError;
42use zerodds_ast::{Ast, Bound, CaseLabel, PrimitiveKind, TypeKind, TypeNode};
43use zerodds_ast_core::{Id, NodeId};
44use zerodds_ast_layout::key::{
45    ElementPadding, LayoutKey, PlCdr1HeaderPolicy, Purpose, WcharPosition, WcharWidth,
46    WstringCodeUnits, WstringLengthCounts, WstringLengthPolicy, WstringUnitOrder, WstringUnitWidth,
47};
48use zerodds_ast_layout::key::{FixedDigitCount, MapKeyOrdering};
49use zerodds_ast_layout::plan::{Extent, MemberHeader, ScalarWrite, scalar_write};
50use zerodds_ast_layout::xcdr::DepCollector;
51use zerodds_lang_cst::common::{Doc, Literal};
52
53/// The one framing question the layout plan does not answer.
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub struct CodecOptions {
56    /// Whether a delimited `map` also carries its entry count.
57    ///
58    /// XTypes 1.3 says it twice and differently: §7.4.1.1.4 defines a
59    /// map by equivalence to a sequence, which has both a `DHEADER` and
60    /// an `O.length`, while rule (15) of §7.4.3.5.3 writes the
61    /// `DHEADER` and the pairs with no count. Both are decodable, so
62    /// neither can be ruled out from the stream alone, and the caller
63    /// chooses. `true` is the equivalence reading.
64    pub map_length_with_delimiter: bool,
65}
66
67impl Default for CodecOptions {
68    fn default() -> Self {
69        Self {
70            map_length_with_delimiter: true,
71        }
72    }
73}
74
75/// The parameter that carries a sample's RTI PL_CDR1 step decision down
76/// to a nested type's encoder.
77///
78/// RTI frames a sample under XCDR1 with the short parameter header until
79/// its type's maximum serialized size passes a step, and from there
80/// gives `PID_EXTENDED` to every member whose type is not a primitive or
81/// an enumeration. The step is a property of the **sample's** type and
82/// not of the type that declares a member, so a generated type cannot
83/// bake the answer in: the same nested `@mutable` type is framed short
84/// inside a small sample and extended inside a large one
85/// (`crate::emit::encode`'s `body_name` doc and
86/// `zerodds_ast_layout::key::PlCdr1HeaderPolicy::ExtendedForConstructedInLargeSamples`).
87/// The flag is threaded at run time; the sample's root supplies it.
88const EXTENDED_PARAM: &str = "zd_extended";
89
90/// Whether the plan's PL_CDR1 header form is decided per sample, so every
91/// generated encoder takes [`EXTENDED_PARAM`] and threads it to the
92/// types it calls.
93fn per_sample(key: LayoutKey) -> bool {
94    key.pl_cdr1_headers.decided_per_sample() && key.encoding.is_v1()
95}
96
97/// The name of the encoder **body** of `name`: the function that takes
98/// [`EXTENDED_PARAM`], or `name` itself when the form is fixed.
99///
100/// Under [`per_sample`] one type needs two functions. The body carries
101/// the sample's decision as a parameter, so one type's codec can frame
102/// its constructed members short in a small sample and extended in a
103/// large one; the public entry under `name` bakes the decision in for
104/// the sample whose root it is and calls the body. Without the split,
105/// the nested `Inner` of `crate::emit::encode`'s `Inner` row could not
106/// serve both its samples.
107fn body_name(name: &str, key: LayoutKey) -> String {
108    if per_sample(key) {
109        format!("{name}_zd_extended")
110    } else {
111        name.to_string()
112    }
113}
114
115/// A call to the encoder of a nested type: its body's name, and the
116/// sample's flag when [`per_sample`] holds.
117fn nested_call<T: CodecTarget>(
118    ast: &Ast,
119    id: Id<TypeNode>,
120    key: LayoutKey,
121    args: Vec<T::Expr>,
122) -> T::Expr {
123    nested_call_with::<T>(ast, id, key, args, T::name(EXTENDED_PARAM))
124}
125
126/// [`nested_call`] with the flag spelled by the caller.
127///
128/// The key holder passes the constant `false`. Under
129/// [`PlCdr1HeaderPolicy::ExtendedForConstructedInLargeSamples`] a key
130/// holder keeps the short form whatever the sample: the choice of
131/// `PID_EXTENDED` for a constructed member is a data-sample decision —
132/// [`LayoutKey::for_sample`] is asked for one — and the reflective
133/// planner keeps a key holder short for that same reason. The data path
134/// resolves the choice once at the root and hands it down; the holder
135/// has no root decision to hand down and states the constant.
136fn nested_call_with<T: CodecTarget>(
137    ast: &Ast,
138    id: Id<TypeNode>,
139    key: LayoutKey,
140    mut args: Vec<T::Expr>,
141    flag: T::Expr,
142) -> T::Expr {
143    let name = body_name(&member_codec_name::<T>(ast, id, key), key);
144    if per_sample(key) {
145        args.push(flag);
146    }
147    T::call(&name, args)
148}
149
150/// Whether a sample of `root` passes RTI's short-header step, the
151/// generation-time half of the per-sample decision.
152///
153/// The same count [`zerodds_ast_layout::key::LayoutKey::for_sample`]
154/// makes: a `decided_per_sample` policy becomes
155/// [`PlCdr1HeaderPolicy::ExtendedForConstructed`] when the sample's type
156/// can exceed the short form's length. `for_sample` is asked rather than
157/// `exceeds_short_length` directly so that the generated entry and the
158/// reflective path read one answer.
159fn sample_past_step(ast: &Ast, root: Id<TypeNode>, key: LayoutKey) -> Result<bool, EncodeError> {
160    let resolved = key
161        .for_sample(ast, root)
162        .map_err(crate::encode::plan_refusal)?;
163    Ok(resolved.pl_cdr1_headers == PlCdr1HeaderPolicy::ExtendedForConstructed)
164}
165
166/// The types a whole-model codec needs a public entry for: the named
167/// structures, unions and value types that are not `@nested`, which are
168/// what a caller can hold a sample of.
169///
170/// A type that is only ever a member is reached through its parent's
171/// body and needs no entry; emitting one would be a `static` function
172/// nothing calls, which every C consumer built with `-Werror` refuses.
173/// [`codec_from`] knows its root and does not ask this.
174pub(crate) fn sample_types(ast: &Ast) -> Vec<Id<TypeNode>> {
175    ast.types()
176        .filter(|(_, node)| {
177            !node.name.is_empty()
178                && matches!(
179                    node.kind,
180                    TypeKind::Structure { .. }
181                        | TypeKind::Union { .. }
182                        | TypeKind::Valuetype { .. }
183                )
184                && !node
185                    .facets
186                    .xtypes
187                    .as_ref()
188                    .is_some_and(|xtypes| xtypes.is_nested)
189        })
190        .map(|(id, _)| id)
191        .collect()
192}
193
194/// The body with `zd_extended` marked as read.
195///
196/// A primitive's or an enumeration's body has no parameter header and
197/// never reads the flag; the C probe is built with `-Wunused-parameter
198/// -Werror`, where that fails the build. `(void)zd_extended;` is valid
199/// whether or not the body goes on to use it, so it is emitted
200/// unconditionally rather than from a guess about which bodies read it —
201/// the same shape `crate::emit::decode::with_tc_used` uses.
202fn with_extended_used<T: CodecTarget>(body: T::Block) -> T::Block {
203    let mut statements = T::mark_used(EXTENDED_PARAM);
204    statements.extend(T::into_statements(body));
205    T::block(statements)
206}
207
208/// The prefix an encoder function takes, by what the plan is for.
209///
210/// A key holder is a different plan over the same types — only the
211/// `@key` members, big-endian — so it needs its own set of functions
212/// rather than a flag threaded through the data ones.
213const fn prefix(purpose: Purpose) -> &'static str {
214    match purpose {
215        Purpose::Data => "zd_enc_",
216        Purpose::Key => "zd_kenc_",
217    }
218}
219
220/// The encoder a member or element of `member_type` is written by,
221/// under the key its enclosing body was generated with.
222///
223/// Not `codec_name(ast, member_type, key.purpose)`. §7.6.8 Step 1
224/// removes the non-key members of a nested aggregate only "if there are
225/// any key members", and one dialect reads the consequence differently:
226/// under [`KeyHolderNesting::KeylessNestedWhole`] — RTI under XCDR1 —
227/// the subtree below a keyless aggregate stops being filtered at all
228/// and is taken as the data has it. `keyhash::member_key` is where that
229/// rule lives; asking it here is what makes the holder call
230/// `zd_enc_Middle` rather than `zd_kenc_Middle` for a keyless `Middle`,
231/// and everything under it follows because the key threads down.
232///
233/// Inert outside a key holder: the rule only ever turns `Purpose::Key`
234/// into `Purpose::Data`, so a payload run gets the name it always had.
235fn member_codec_name<T: CodecTarget>(
236    ast: &Ast,
237    member_type: Id<TypeNode>,
238    key: LayoutKey,
239) -> String {
240    codec_name::<T>(
241        ast,
242        member_type,
243        zerodds_ast_layout::keyhash::member_key(ast, member_type, key).purpose,
244    )
245}
246
247/// The encoder function name for a type.
248#[must_use]
249pub fn codec_name<T: CodecTarget>(ast: &Ast, id: Id<TypeNode>, purpose: Purpose) -> String {
250    let head = prefix(purpose);
251    let name = match ast.type_(id).map(|node| node.kind.clone()) {
252        // Named by the IDL kind and not by the target's spelling:
253        // `double` and `long double` share a C type and do not share a
254        // wire form, and a name taken from the target type would give
255        // them one function.
256        Ok(TypeKind::Primitive(kind)) => {
257            format!("{head}p_{}", kind.as_str().replace(' ', "_"))
258        }
259        _ => format!("{head}{}", T::type_name(ast, id)),
260    };
261    // The published spelling, once and here. See
262    // [`CodecTarget::published_name`].
263    T::published_name(name)
264}
265
266/// Build the encoder for every type in the model, under one plan.
267///
268/// Under `per_sample` the named non-`@nested` aggregates additionally
269/// get a public entry that bakes their own sample decision in; every
270/// other type is reached through the body of the type that declares it.
271///
272/// # Errors
273/// [`EncodeError`] when the layout cannot be planned or a construct has
274/// no C writer — never a body that leaves a member out.
275pub fn codec<T: CodecTarget>(
276    ast: &Ast,
277    key: LayoutKey,
278    options: CodecOptions,
279) -> Result<Vec<T::Decl>, EncodeError> {
280    codec_over::<T>(ast, &ordered_types(ast), key, options, &sample_types(ast))
281}
282
283/// [`codec`] without the types in `excluded`, and with what the types in
284/// `also_reaching` reach.
285///
286/// For a model whose declarations of some types are provided elsewhere —
287/// DDS-RPC 1.0 §7.5.1.1.1's common types come with the runtime — and whose
288/// request and reply types (§7.3.1.4) are encoded by a hand of the RPC
289/// binding that writes the header the runtime's way.
290///
291/// # Errors
292/// As [`codec`].
293pub fn codec_excluding<T: CodecTarget>(
294    ast: &Ast,
295    key: LayoutKey,
296    options: CodecOptions,
297    excluded: &[Id<TypeNode>],
298    also_reaching: &[Id<TypeNode>],
299) -> Result<Vec<T::Decl>, EncodeError> {
300    if excluded.is_empty() {
301        return codec::<T>(ast, key, options);
302    }
303    let (types, entries) = kept_after_excluding(ast, excluded, also_reaching);
304    codec_over::<T>(ast, &types, key, options, &entries)
305}
306
307/// The types, and the sample types among them, that remain once `excluded`
308/// is left out: what the other sample types reach. A helper only an
309/// excluded type needed is a `static` function nothing calls.
310pub(crate) fn kept_after_excluding(
311    ast: &Ast,
312    excluded: &[Id<TypeNode>],
313    also_reaching: &[Id<TypeNode>],
314) -> (Vec<Id<TypeNode>>, Vec<Id<TypeNode>>) {
315    let mut entries: Vec<Id<TypeNode>> = sample_types(ast)
316        .into_iter()
317        .filter(|id| !excluded.contains(id))
318        .collect();
319    for id in also_reaching {
320        if !entries.contains(id) {
321            entries.push(*id);
322        }
323    }
324    let types = ordered_types_from_pruning(ast, &entries, excluded);
325    (types, entries)
326}
327
328/// The encoder for the types one root reaches, and no others.
329///
330/// What a probe or a topic's translation unit wants: an encoder for a
331/// type the root cannot reach is a `static` function nothing calls, and
332/// a generated file that warns is one whose warnings stop being read.
333/// Under `per_sample` the root gets the public entry and every type it
334/// reaches gets a body.
335///
336/// # Errors
337/// As [`codec`].
338pub fn codec_from<T: CodecTarget>(
339    ast: &Ast,
340    root: Id<TypeNode>,
341    key: LayoutKey,
342    options: CodecOptions,
343) -> Result<Vec<T::Decl>, EncodeError> {
344    codec_over::<T>(
345        ast,
346        &ordered_types_from(ast, &[root]),
347        key,
348        options,
349        &[root],
350    )
351}
352
353/// The encoder for the types several roots reach, each function once.
354///
355/// What a translation unit that encodes members of several types wants:
356/// one call per root would define the functions the roots share twice.
357///
358/// # Errors
359/// As [`codec`].
360pub fn codec_from_many<T: CodecTarget>(
361    ast: &Ast,
362    roots: &[Id<TypeNode>],
363    key: LayoutKey,
364    options: CodecOptions,
365) -> Result<Vec<T::Decl>, EncodeError> {
366    codec_over::<T>(ast, &ordered_types_from(ast, roots), key, options, roots)
367}
368
369/// [`codec_from_many`] without the types in `excluded`, whose codecs are
370/// written by hand elsewhere (DDS-RPC 1.0 §7.5.1.1.1's common types come
371/// with the runtime in a shape of their own).
372///
373/// # Errors
374/// As [`codec`].
375pub fn codec_from_many_excluding<T: CodecTarget>(
376    ast: &Ast,
377    roots: &[Id<TypeNode>],
378    key: LayoutKey,
379    options: CodecOptions,
380    excluded: &[Id<TypeNode>],
381) -> Result<Vec<T::Decl>, EncodeError> {
382    let types = ordered_types_from_pruning(ast, roots, excluded);
383    codec_over::<T>(ast, &types, key, options, roots)
384}
385
386fn codec_over<T: CodecTarget>(
387    ast: &Ast,
388    types: &[Id<TypeNode>],
389    key: LayoutKey,
390    options: CodecOptions,
391    entries: &[Id<TypeNode>],
392) -> Result<Vec<T::Decl>, EncodeError> {
393    crate::encode::axes_the_codecs_carry_for::<T>(key)?;
394    let mut prototypes = Vec::new();
395    let mut bodies = Vec::new();
396    let mut emitted: Vec<String> = Vec::new();
397    let framed = per_sample(key);
398    let uncalled = called_only_in_bulk(
399        ast,
400        types,
401        entries,
402        T::bulk_octets_write("v", T::name("zd_n")).is_some(),
403    );
404
405    for id in types.iter().copied() {
406        if uncalled.contains(&id) {
407            continue;
408        }
409        let name = codec_name::<T>(ast, id, key.purpose);
410        if emitted.contains(&name) {
411            continue;
412        }
413        let body = body_name(&name, key);
414        let Some(statements) = body_of::<T>(ast, id, key, options, &body)? else {
415            continue;
416        };
417        emitted.push(name.clone());
418        // A target with no forward declarations answers `None` to the
419        // prototype and the loop emits only the body.
420        if !framed {
421            prototypes.extend(function::<T>(ast, id, key, &name, None, false));
422            bodies.extend(function::<T>(ast, id, key, &name, Some(statements), false));
423            continue;
424        }
425        prototypes.extend(function::<T>(ast, id, key, &body, None, true));
426        bodies.extend(function::<T>(ast, id, key, &body, Some(statements), true));
427        // The public entry, for the samples whose root this type is:
428        // it bakes the type's own step decision in and hands it to the
429        // body, which threads it to every type the body calls.
430        if entries.contains(&id) {
431            let past = sample_past_step(ast, id, key)?;
432            let call = T::perform(T::call(
433                &body,
434                vec![T::name("w"), T::name("v"), T::literal(Literal::Bool(past))],
435            ));
436            let entry = T::block(vec![call]);
437            prototypes.extend(entry_function::<T>(ast, id, &name, None));
438            bodies.extend(entry_function::<T>(ast, id, &name, Some(entry)));
439        }
440    }
441
442    prototypes.extend(bodies);
443    Ok(prototypes)
444}
445
446/// The key holder for a keyed type, and nothing for one with no `@key`.
447///
448/// The holder is the `@key` members alone, in member-id order, and it is
449/// big-endian whatever the sample is: two peers that disagree about
450/// either compute different hashes for the same instance.
451/// DDSI-RTPS 2.5 §9.6.3.8, XTypes 1.3 §7.6.8.
452///
453/// `key` is the holder's own layout key, and it is taken rather than
454/// assembled here because it carries the two things this function
455/// cannot work out for itself: `key_holder_nesting`, which decides how
456/// far below a keyless aggregate the key set reaches, and the purpose
457/// each member's encoder is named under.
458///
459/// # Errors
460/// [`EncodeError::NoLayout`] when the key set cannot be worked out.
461pub fn key_holder<T: CodecTarget>(
462    ast: &Ast,
463    root: Id<TypeNode>,
464    key: LayoutKey,
465) -> Result<Option<T::Decl>, EncodeError> {
466    let keys = zerodds_ast_layout::keyhash::key_members_with(
467        ast,
468        root,
469        key.key_holder_nesting,
470        key.key_holder_order,
471    )
472    .map_err(crate::encode::plan_refusal)?;
473    if keys.is_empty() {
474        // §7.6.8 Step 1 over a union keyed by its discriminator: the
475        // branches are not keys, so the holder is the discriminator alone,
476        // as `zerodds_ast_layout::xcdr` plans it. `key_members_with` walks
477        // members and answers the empty list for such a union.
478        let Some(discriminator) = zerodds_ast_layout::keyhash::union_key_discriminator(ast, root)
479        else {
480            return Ok(None);
481        };
482        let statements = vec![T::perform(nested_call_with::<T>(
483            ast,
484            discriminator,
485            key,
486            vec![
487                T::name("w"),
488                argument::<T>(ast, discriminator, T::discriminant("v")),
489            ],
490            T::literal(Literal::Bool(false)),
491        ))];
492        return Ok(key_holder_routine::<T>(
493            ast,
494            root,
495            &[
496                "The key holder: the discriminator of a union keyed by it.",
497                "Big-endian whatever the sample is — DDSI-RTPS 2.5 9.6.3.8.",
498            ],
499            statements,
500        ));
501    }
502
503    let mut statements = Vec::new();
504    for node in &keys {
505        // A key member of a nested aggregate is written by that
506        // aggregate's own key encoder, which the nested plan already
507        // filters; only the root's own members are named here.
508        let Some((name, member_type)) = member_by_node(ast, root, *node) else {
509            continue;
510        };
511        let reach = super::reach::<T>(ast, root, *node, &name);
512        statements.push(T::perform(nested_call_with::<T>(
513            ast,
514            member_type,
515            key,
516            vec![
517                T::name("w"),
518                argument::<T>(ast, member_type, arrow::<T>("v", &reach.field)),
519            ],
520            T::literal(Literal::Bool(false)),
521        )));
522    }
523
524    Ok(key_holder_routine::<T>(
525        ast,
526        root,
527        &[
528            "The key holder: the @key members alone, in member-id order.",
529            "Big-endian whatever the sample is — DDSI-RTPS 2.5 9.6.3.8.",
530        ],
531        statements,
532    ))
533}
534
535/// The routine of a key holder of `root` with the body `statements`.
536fn key_holder_routine<T: CodecTarget>(
537    ast: &Ast,
538    root: Id<TypeNode>,
539    doc: &[&str],
540    statements: Vec<T::Stmt>,
541) -> Option<T::Decl> {
542    T::routine(Routine {
543        doc: Doc::lines(doc),
544        name: format!("zd_keyholder_{}", T::type_name(ast, root)),
545        // The sample's own type, as every codec routine takes it. The
546        // flat `type_name` is a name for routines, not a type: in a
547        // module it read `m_S` where C++ declares `::m::S`, and the
548        // holder did not compile in C++, C# or TypeScript.
549        sample: T::value_type(ast, root),
550        held: Handoff::Value,
551        writing: true,
552        // Always emitted and always called; only the `Char16` codec's
553        // caller depends on a setting. See `maybe_unused`.
554        tolerate_unused: false,
555        extra: Vec::new(),
556        body: Some(T::block(statements)),
557    })
558}
559
560/// The codec routine for a type, with or without a
561/// body.
562///
563/// `framed` says this is a body under [`per_sample`]: it takes
564/// [`EXTENDED_PARAM`] after the sample and marks it read, so a target
565/// that warns on an unread parameter still compiles.
566fn function<T: CodecTarget>(
567    ast: &Ast,
568    id: Id<TypeNode>,
569    key: LayoutKey,
570    name: &str,
571    body: Option<T::Block>,
572    framed: bool,
573) -> Option<T::Decl> {
574    let _ = key.purpose;
575    T::routine(Routine {
576        doc: Doc::none(),
577        name: name.to_string(),
578        sample: T::value_type(ast, id),
579        held: if is_array(ast, id, 0) {
580            Handoff::Array
581        } else {
582            Handoff::Value
583        },
584        writing: true,
585        // A `Char16` whose element form is written inline at the call
586        // site has no caller left when the type graph reaches one only
587        // through a collection. Whether this function is used is then a
588        // property of the dialect, not of the model, and the generated
589        // code is compiled with an uncalled function as an error.
590        tolerate_unused: maybe_unused(ast, id, key),
591        extra: if framed {
592            vec![(T::scalar(Scalar::Bool), EXTENDED_PARAM.to_string())]
593        } else {
594            Vec::new()
595        },
596        body: if framed {
597            body.map(with_extended_used::<T>)
598        } else {
599            body
600        },
601    })
602}
603
604/// The public entry of a type under [`per_sample`]: the sample's own
605/// step decision baked in, handed to the body.
606///
607/// The entry takes no [`EXTENDED_PARAM`] because the sample whose root
608/// this type is decides once, at generation time; every nested type is
609/// reached through a body that takes the decision at run time.
610fn entry_function<T: CodecTarget>(
611    ast: &Ast,
612    id: Id<TypeNode>,
613    name: &str,
614    body: Option<T::Block>,
615) -> Option<T::Decl> {
616    T::routine(Routine {
617        doc: Doc::none(),
618        name: name.to_string(),
619        sample: T::value_type(ast, id),
620        held: if is_array(ast, id, 0) {
621            Handoff::Array
622        } else {
623            Handoff::Value
624        },
625        writing: true,
626        tolerate_unused: false,
627        extra: Vec::new(),
628        body,
629    })
630}
631
632/// Whether the codec generated for `id` may legitimately have no caller.
633///
634/// True only for a `Char16` under a framing whose member and element
635/// forms differ, which is TAO's alone. Everywhere else an uncalled
636/// generated codec is a generator defect and the warning must stay a
637/// warning.
638pub(crate) fn maybe_unused(ast: &Ast, id: Id<TypeNode>, key: LayoutKey) -> bool {
639    // The RPC binding of an interface or of a pair of types calls the codecs
640    // of the types it carries and of nothing else (DDS-RPC 1.0 §7.5.1.1,
641    // §7.3.1.4); a type declared beside it keeps its codec and has no caller
642    // in the header.
643    if ast.interfaces().next().is_some() || crate::emit::type_pair_cpp::has_pair_types(ast) {
644        return true;
645    }
646    // A base structure is reached through the members of the structure derived from it
647    // (`v->parent.m`), and its own codec has no caller where nothing else holds it.
648    if is_a_base(ast, id) {
649        return true;
650    }
651    key.wchar_framing.has_length_octet(WcharPosition::Member)
652        != key.wchar_framing.has_length_octet(WcharPosition::Element)
653        && matches!(resolve_primitive(ast, id, 0), Some(PrimitiveKind::WChar))
654}
655
656/// Whether another structure in the model derives from `id`.
657fn is_a_base(ast: &Ast, id: Id<TypeNode>) -> bool {
658    ast.all_nodes().any(|any| match any {
659        zerodds_ast::AnyId::Type(other, _) => {
660            crate::adapter::c::struct_base(ast, other) == Some(id)
661        }
662        _ => false,
663    })
664}
665
666/// Whether a type is a C array once typedefs are seen through.
667///
668/// zerodds-lint: recursion-depth 64 (typedef chains; bounded by the
669/// `depth > 64` check, past which the answer is `false` — the
670/// handoff form a non-array takes)
671fn is_array(ast: &Ast, id: Id<TypeNode>, depth: usize) -> bool {
672    if depth > 64 {
673        return false;
674    }
675    match ast.type_(id).map(|node| node.kind.clone()) {
676        Ok(TypeKind::Array { .. }) => true,
677        Ok(TypeKind::Alias { aliased }) => aliased
678            .resolved()
679            .is_some_and(|target| is_array(ast, target, depth + 1)),
680        _ => false,
681    }
682}
683
684/// How a value of `id` is handed to its encoder: the array itself, or
685/// the address of anything else.
686fn argument<T: CodecTarget>(ast: &Ast, id: Id<TypeNode>, value: T::Expr) -> T::Expr {
687    if is_array(ast, id, 0) {
688        value
689    } else {
690        T::handoff(value, Handoff::Value)
691    }
692}
693
694#[allow(clippy::too_many_lines)]
695fn body_of<T: CodecTarget>(
696    ast: &Ast,
697    id: Id<TypeNode>,
698    key: LayoutKey,
699    options: CodecOptions,
700    own_name: &str,
701) -> Result<Option<T::Block>, EncodeError> {
702    let Ok(node) = ast.type_(id) else {
703        return Ok(None);
704    };
705    let statements = match &node.kind {
706        TypeKind::Primitive(_) => {
707            let Some(write) = scalar_write(ast, id, key, WcharPosition::Member) else {
708                return Err(EncodeError::NoWriter {
709                    member: T::type_name(ast, id),
710                    kind: "primitive the layout plan has no scalar write for",
711                });
712            };
713            vec![primitive_write_at::<T>(write, deref::<T>("v"))]
714        }
715
716        // A bounded `string` or `wstring` writes through
717        // `bounded_string_write`, which calls the runtime's own
718        // `write_string_bounded`/`write_wstring_bounded` and lets it
719        // refuse an over-bound value — the same refusal
720        // `crates/wire::Writer::write_string_bounded` raises for Rust,
721        // and the same reason `write_string_bounded`'s own remarks give:
722        // the bound is a promise about the source and not a difference
723        // on the wire, so writing an over-bound value as if it fit, or
724        // cutting it at a character boundary, would put a sample on the
725        // wire nobody wrote.
726        TypeKind::String { bound, .. } => {
727            let Some(write) = scalar_write(ast, id, key, WcharPosition::Member) else {
728                return Err(EncodeError::NoWriter {
729                    member: own_name.to_string(),
730                    kind: "value the layout plan has no scalar write for",
731                });
732            };
733            vec![match bound_limit(ast, bound) {
734                Some(limit) => bounded_string_write::<T>(own_name, write, deref::<T>("v"), limit)?,
735                None => primitive_write_at::<T>(write, deref::<T>("v")),
736            }]
737        }
738
739        TypeKind::Fixed { .. } => {
740            let Some(write) = scalar_write(ast, id, key, WcharPosition::Member) else {
741                return Err(EncodeError::NoWriter {
742                    member: own_name.to_string(),
743                    kind: "value the layout plan has no scalar write for",
744                });
745            };
746            vec![primitive_write_at::<T>(write, deref::<T>("v"))]
747        }
748
749        // Two axes on one construct, and they are separate questions:
750        // `enumeration_bit_bound` says how wide the holder is and
751        // `enumerator_wire_value` says which integer goes into it. The
752        // bitmask arm below is a different clause that neither reaches.
753        TypeKind::Enumeration { .. } => enumeration_write::<T>(ast, id, key)?,
754
755        TypeKind::Bitmask { .. } => {
756            let Some(write) = scalar_write(ast, id, key, WcharPosition::Member) else {
757                return Err(EncodeError::NoWriter {
758                    member: own_name.to_string(),
759                    kind: "bitmask the layout plan has no scalar write for",
760                });
761            };
762            vec![primitive_write_at::<T>(write, deref::<T>("v"))]
763        }
764
765        TypeKind::Bitset { .. } => {
766            // The whole chain, base-first: the planner sizes the anchor
767            // over it and the one implementation that parses an
768            // inherited bitset packs it that way.
769            // `docs/refused-constructs.md` has the measurement.
770            let fields = &ast.all_bitfields_of(id);
771            let mut deps = DepCollector::default();
772            let layout = zerodds_ast_layout::xcdr::plan(ast, id, key, &mut deps)
773                .map_err(crate::encode::plan_refusal)?;
774            let Extent::Sized(anchor) = layout.extent else {
775                return Err(EncodeError::NoWriter {
776                    member: own_name.to_string(),
777                    kind: "bitset whose anchor the planner does not size",
778                });
779            };
780            let mut statements = vec![T::declare(
781                T::scalar(Scalar::UInt64),
782                "bits",
783                Some(int::<T>(0)),
784            )];
785            let mut position: u32 = 0;
786            for field in fields {
787                let Ok(field_node) = ast.bitfield(*field) else {
788                    continue;
789                };
790                let width = u32::from(field_node.bits);
791                // An anonymous field is padding: it moves the next field
792                // along and carries no value of its own.
793                if let Some(field_name) = field_node.name.as_ref() {
794                    statements.push(T::perform(T::binary(
795                        "=",
796                        T::name("bits"),
797                        T::binary(
798                            "|",
799                            T::name("bits"),
800                            call_expr::<T>(
801                                "bitfield",
802                                vec![
803                                    cast::<T>(
804                                        T::scalar(Scalar::UInt64),
805                                        arrow::<T>("v", &T::escape(field_name)),
806                                    ),
807                                    int::<T>(i128::from(width)),
808                                    int::<T>(i128::from(position)),
809                                ],
810                            ),
811                        ),
812                    )));
813                }
814                position = position.saturating_add(width);
815            }
816            statements.push(call::<T>(
817                "write_bitset",
818                vec![int::<T>(i128::from(anchor)), T::name("bits")],
819            ));
820            statements
821        }
822
823        TypeKind::Alias { aliased } => {
824            let Some(target) = aliased.resolved() else {
825                return Err(EncodeError::NoWriter {
826                    member: own_name.to_string(),
827                    kind: "typedef with an unresolved target",
828                });
829            };
830            // A typedef is transparent on the wire, and the C typedef
831            // makes the two the same type, so the value goes straight
832            // through.
833            let callee = body_name(&member_codec_name::<T>(ast, target, key), key);
834            // Unless the alias and its target are one encoder. A target
835            // that folds a `typedef` into the type it names gives the
836            // two the same encoder name — Zig's `pub const NodeRef =
837            // Node;` is the alias, so `Zig::type_name` answers `m_Node`
838            // for both — and a forwarder to itself is not a forwarder:
839            // `recursive-typedef-sequence` built
840            // `fn zd_enc_m_Node(..) { try zd_enc_m_Node(..); }` and the
841            // probe took SIGSEGV on the stack it ran out of. `None`
842            // leaves the name unclaimed and the aliased type emits its
843            // own body under it.
844            if callee == own_name {
845                return Ok(None);
846            }
847            let mut args = vec![T::name("w"), T::name("v")];
848            if per_sample(key) {
849                args.push(T::name(EXTENDED_PARAM));
850            }
851            vec![T::perform(T::call(&callee, args))]
852        }
853
854        TypeKind::Sequence { element, bound } => {
855            let Some(element) = element.resolved() else {
856                return Err(EncodeError::NoWriter {
857                    member: own_name.to_string(),
858                    kind: "sequence with an unresolved element",
859                });
860            };
861            let delimited = zerodds_ast_layout::collection_is_delimited(ast, element, key);
862            // Checked before the DHEADER, and not after: `sequence<T,
863            // N>` bounds the count `O.length` puts on the wire, and a
864            // sender that wrote more elements than its own declaration
865            // allows has written a value of a type nobody declared.
866            // `crates/wire::Writer::check_sequence_bound` checks the
867            // same thing at the same point, for the same reason.
868            let mut statements = Vec::new();
869            if let Some(limit) = bound_limit(ast, bound) {
870                statements.push(bound_check::<T>(
871                    "check_sequence_bound",
872                    T::count_of("v", None),
873                    limit,
874                ));
875            }
876            statements.push(dheader_declaration::<T>(delimited));
877            statements.push(call::<T>("write_u32", vec![T::count_of("v", None)]));
878            if let Some(bulk) = bulk_write::<T>(ast, element, T::count_of("v", None)) {
879                statements.push(bulk);
880                statements.push(finish_dheader::<T>());
881                return Ok(Some(T::block(statements)));
882            }
883            // A `sequence` element of a non-`@final` type carries its own
884            // octet length under
885            // `SequenceElementDelimiter::BeforeEachNonFinalElement`, and
886            // every element counts its alignment from its own first octet
887            // under `ElementPadding::Packed`. The order is the reference's:
888            // the origin is moved first, then the element's length, then
889            // the element, and both are undone in reverse.
890            let packed = key.element_padding == ElementPadding::Packed;
891            let carries_length = sequence_element_carries_length(ast, element, key)?;
892            let at = T::index(T::elements("v"), T::name("i"));
893            let mut body: Vec<T::Stmt> = Vec::new();
894            if packed {
895                body.push(begin_packed::<T>());
896            }
897            if carries_length {
898                body.push(element_length_declaration::<T>());
899            }
900            body.push(
901                element_write::<T>(ast, element, key, at.clone()).unwrap_or_else(|| {
902                    T::perform(nested_call::<T>(
903                        ast,
904                        element,
905                        key,
906                        vec![T::name("w"), argument::<T>(ast, element, at)],
907                    ))
908                }),
909            );
910            if carries_length {
911                body.push(finish_element_length::<T>());
912            }
913            if packed {
914                body.push(finish_packed::<T>());
915            }
916            statements.push(T::count_up("i", T::count_of("v", None), T::block(body)));
917            statements.push(finish_dheader::<T>());
918            statements
919        }
920
921        TypeKind::Array { element, dims } => {
922            let Some(element) = element.resolved() else {
923                return Err(EncodeError::NoWriter {
924                    member: own_name.to_string(),
925                    kind: "array with an unresolved element",
926                });
927            };
928            let mut lengths = Vec::new();
929            for dim in dims {
930                let length = ast.eval_integral(*dim).map_err(|_| EncodeError::NoWriter {
931                    member: own_name.to_string(),
932                    kind: "array whose length does not evaluate",
933                })?;
934                lengths.push(length);
935            }
936            let delimited = zerodds_ast_layout::array_is_delimited(ast, element, key);
937            // §7.4.3.5 treats a multi-dimensional array as one flat run
938            // with the first index varying most slowly, which is what
939            // nested loops in written order already are.
940            let mut access = T::name("v");
941            for level in 0..lengths.len() {
942                access = T::index(access, T::name(&format!("i{level}")));
943            }
944            if let [length] = lengths.as_slice() {
945                if let Some(bulk) = bulk_write::<T>(ast, element, int::<T>(*length)) {
946                    return Ok(Some(T::block(vec![
947                        dheader_declaration::<T>(delimited),
948                        bulk,
949                        finish_dheader::<T>(),
950                    ])));
951                }
952            }
953            let mut inner = T::block(vec![
954                element_write::<T>(ast, element, key, access.clone()).unwrap_or_else(|| {
955                    T::perform(nested_call::<T>(
956                        ast,
957                        element,
958                        key,
959                        vec![T::name("w"), argument::<T>(ast, element, access)],
960                    ))
961                }),
962            ]);
963            // Under `ElementPadding::Packed` each element of every
964            // dimension is its own alignment block, as in the reference's
965            // `write_array_dimension`.
966            let packed = key.element_padding == ElementPadding::Packed;
967            for (level, length) in lengths.iter().enumerate().rev() {
968                let index = format!("i{level}");
969                let mut body: Vec<T::Stmt> = Vec::new();
970                if packed {
971                    body.push(begin_packed::<T>());
972                }
973                body.extend(T::into_statements(inner));
974                if packed {
975                    body.push(finish_packed::<T>());
976                }
977                inner = T::block(vec![T::count_up(&index, int::<T>(*length), T::block(body))]);
978            }
979            let mut statements = vec![dheader_declaration::<T>(delimited)];
980            statements.extend(T::into_statements(inner));
981            statements.push(finish_dheader::<T>());
982            statements
983        }
984
985        TypeKind::Map {
986            key: k,
987            value,
988            bound,
989        } => {
990            let (Some(k), Some(value)) = (k.resolved(), value.resolved()) else {
991                return Err(EncodeError::NoWriter {
992                    member: own_name.to_string(),
993                    kind: "map with an unresolved key or value",
994                });
995            };
996            let mut deps = DepCollector::default();
997            let layout = zerodds_ast_layout::xcdr::plan(ast, id, key, &mut deps)
998                .map_err(crate::encode::plan_refusal)?;
999            let entry = T::map_entry_name(ast, id);
1000            let _ = &entry;
1001            // The order the entries go out in is a setting, and a key
1002            // type the crossing has no order for is a refusal rather
1003            // than a quiet fallback to declaration order.
1004            let Ok(ordering) = zerodds_ast_layout::plan::map_entry_ordering(ast, id, key) else {
1005                return Err(EncodeError::NoWriter {
1006                    member: own_name.to_string(),
1007                    kind: "a map whose key type has no value order, under \
1008                           --map-entries ascending-key",
1009                });
1010            };
1011            // Checked before the DHEADER, as the sequence's own bound
1012            // is: `map<K, V, N>` bounds the **entry** count, and
1013            // `crates/wire::Writer::check_map_bound` checks it at the
1014            // same point and names it in entries and not in halves, for
1015            // the reason its own remarks give.
1016            let mut statements = Vec::new();
1017            if let Some(limit) = bound_limit(ast, bound) {
1018                statements.push(bound_check::<T>(
1019                    "check_map_bound",
1020                    T::count_of("v", None),
1021                    limit,
1022                ));
1023            }
1024            statements.push(dheader_declaration::<T>(layout.needs_dheader));
1025            if !layout.needs_dheader || options.map_length_with_delimiter {
1026                statements.push(call::<T>("write_u32", vec![T::count_of("v", None)]));
1027            }
1028            let element = |key: bool| T::entry_half(T::index(T::elements("v"), T::name("i")), key);
1029            let pair = T::block(vec![
1030                T::perform(nested_call::<T>(
1031                    ast,
1032                    k,
1033                    key,
1034                    vec![T::name("w"), argument::<T>(ast, k, element(true))],
1035                )),
1036                T::perform(nested_call::<T>(
1037                    ast,
1038                    value,
1039                    key,
1040                    vec![T::name("w"), argument::<T>(ast, value, element(false))],
1041                )),
1042            ]);
1043            statements.push(match ordering {
1044                None => over_the_entries::<T>("i", pair),
1045                Some(ordering) => ascending_by_key::<T>(ast, k, ordering, pair),
1046            });
1047            statements.push(finish_dheader::<T>());
1048            statements
1049        }
1050
1051        TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
1052            aggregate_body::<T>(ast, id, key)?
1053        }
1054
1055        TypeKind::Union { discriminator, .. } => {
1056            union_body::<T>(ast, id, key, discriminator.resolved(), own_name)?
1057        }
1058
1059        // No wire form at all — `zerodds_ast_layout` refuses to plan
1060        // one — so there is nothing to emit and nothing is claimed.
1061        TypeKind::Native | TypeKind::ObjectRef { .. } | TypeKind::AnnotationType { .. } => {
1062            return Ok(None);
1063        }
1064        TypeKind::ValueBox { .. } => {
1065            return Err(EncodeError::NoWriter {
1066                member: own_name.to_string(),
1067                kind: "boxed value, whose presence flag has no settled wire form",
1068            });
1069        }
1070        // Refused by name rather than passed over. `any` is in the model
1071        // because IDL 4.2 has it; DDS-XTypes has no `any` and no
1072        // encoding for one, and only CORBA CDR writes it — as a TypeCode
1073        // ahead of the value. Returning `Ok(None)` here would generate a
1074        // codec that silently omitted the member.
1075        TypeKind::Any => {
1076            return Err(EncodeError::NoWriter {
1077                member: own_name.to_string(),
1078                kind: "`any`, which DDS-XTypes has no encoding for; only CORBA CDR writes one, \
1079                       as a TypeCode ahead of the value",
1080            });
1081        }
1082        TypeKind::ValueBase => {
1083            return Err(EncodeError::NoWriter {
1084                member: own_name.to_string(),
1085                kind: "`ValueBase`, which DDS-XTypes has no encoding for; only CORBA CDR writes \
1086                       one, as a chunked value with its repository id",
1087            });
1088        }
1089    };
1090    Ok(Some(T::block(statements)))
1091}
1092
1093fn aggregate_body<T: CodecTarget>(
1094    ast: &Ast,
1095    id: Id<TypeNode>,
1096    key: LayoutKey,
1097) -> Result<Vec<T::Stmt>, EncodeError> {
1098    let mut deps = DepCollector::default();
1099    let layout = zerodds_ast_layout::xcdr::plan(ast, id, key, &mut deps)
1100        .map_err(crate::encode::plan_refusal)?;
1101
1102    // `ElementPadding::Packed`: an aggregate is its own alignment block,
1103    // so its members count from its own first octet and it is not padded
1104    // up to its own alignment before it starts. Measured on
1105    // `typedef-of-struct`, whose second `Cell` puts its `long` at offset
1106    // six where the clause puts it at eight. `saved` is the whole body's,
1107    // as in the reference's `write_aggregate`.
1108    let packed = key.element_padding == ElementPadding::Packed;
1109    let mut statements = Vec::new();
1110    if packed {
1111        statements.push(begin_packed::<T>());
1112    }
1113    statements.push(dheader_declaration::<T>(layout.needs_dheader));
1114    for (index, member) in layout.members.iter().enumerate() {
1115        let Some((name, member_type)) = member_by_node(ast, id, member.member) else {
1116            continue;
1117        };
1118        let reach = super::reach::<T>(ast, id, member.member, &name);
1119        let field = reach.field.clone();
1120        let node = ast
1121            .all_members_of(id)
1122            .into_iter()
1123            .filter_map(|m| ast.member(m).ok())
1124            .find(|m| m.node_id == member.member);
1125        let optional = node.as_ref().is_some_and(|m| m.facets.is_optional());
1126        // Table 21: `@external` changes no octet, only where the
1127        // binding keeps the value — which is exactly what the seam it
1128        // routes through decides.
1129        let external = node
1130            .as_ref()
1131            .is_some_and(|m| super::reached_externally::<T>(ast, m));
1132
1133        // Whether the member's value has room inside `id` at all. A
1134        // column whose member is the value itself has none where the
1135        // member's type reaches `id` back over inline edges, and the
1136        // seam is where it says what it keeps instead.
1137        let indirect =
1138            super::behind_pointer::<T>(ast, reach.holder, member_type, external, optional);
1139        // And whether the column keeps the member behind a reference of
1140        // its own making. `CodecTarget::holds_by_reference`.
1141        let by_reference =
1142            T::holds_by_reference(ast, reach.holder, member_type, optional, external);
1143
1144        // XTypes 1.3 §7.5.1.2.3.1.1 and §7.5.1.2.3.2.1: an `@external`
1145        // member that is not `@optional` may not be unset at the time of
1146        // a write. Only the columns that have an unset state say
1147        // anything here.
1148        if external && !optional && !by_reference {
1149            statements.extend(T::refuse_unset_external(T::external_present("v", &field)));
1150        }
1151
1152        statements.extend(framed_member::<T>(
1153            ast,
1154            member,
1155            index,
1156            member_type,
1157            &field,
1158            key,
1159            optional,
1160            external,
1161            indirect,
1162            by_reference,
1163        )?);
1164    }
1165    if layout.needs_sentinel {
1166        statements.push(call::<T>("write_sentinel", Vec::new()));
1167    }
1168    statements.push(finish_dheader::<T>());
1169    if packed {
1170        statements.push(finish_packed::<T>());
1171    }
1172    Ok(statements)
1173}
1174
1175/// One member with the header the plan gave it.
1176///
1177/// `optional` is the model's answer and the header is the plan's, and
1178/// the two together decide what an absent member looks like. A
1179/// `@mutable` member that is absent contributes nothing — the parameter
1180/// list is what says which members are there. An `OPT_FMEMBER` that is
1181/// absent contributes its marker: a `<is_present>` of zero under XCDR2
1182/// (rule 20) or a parameter header declaring a length of zero under
1183/// XCDR1 (§7.4.1.2). All three implementations write both.
1184#[allow(clippy::too_many_arguments)]
1185fn framed_member<T: CodecTarget>(
1186    ast: &Ast,
1187    member: &zerodds_ast_layout::plan::MemberPlan,
1188    index: usize,
1189    member_type: Id<TypeNode>,
1190    field: &str,
1191    key: LayoutKey,
1192    optional: bool,
1193    external: bool,
1194    indirect: bool,
1195    by_reference: bool,
1196) -> Result<Vec<T::Stmt>, EncodeError> {
1197    let present = if by_reference {
1198        T::reference_present("v", field)
1199    } else if external {
1200        T::external_present("v", field)
1201    } else {
1202        T::present("v", field)
1203    };
1204    /// A member written only when the generated `_has_` flag is set.
1205    fn guarded<B: CodecTarget>(present: B::Expr, statements: Vec<B::Stmt>) -> B::Stmt {
1206        B::when(present, B::block(statements), None)
1207    }
1208    // An `@optional` member is reached through the target's own
1209    // accessor: C's is a plain `T` beside a flag and C++'s is a
1210    // `std::optional<T>` the codec cannot take whole. An `@external`
1211    // one is reached through its own seam — an indirection in the
1212    // column that keeps one, the member itself everywhere else — and
1213    // `@optional @external` is the external form: the indirection
1214    // already carries the absence.
1215    let held = if by_reference {
1216        T::reference_value(ast, member_type, "v", field)
1217    } else if external {
1218        T::external_value(ast, member_type, "v", field, optional)
1219    } else if optional {
1220        T::optional_value(ast, member_type, "v", field)
1221    } else {
1222        arrow::<T>("v", field)
1223    };
1224    // And through the indirection where the column had to put one:
1225    // `member_held_inline_closes_a_cycle` said the value has no room in
1226    // its own aggregate, so what the accessor above reaches is the
1227    // indirection and not the value.
1228    let held = if indirect && !by_reference {
1229        T::behind_indirection(ast, member_type, held)
1230    } else {
1231        held
1232    };
1233    let value = T::perform(nested_call::<T>(
1234        ast,
1235        member_type,
1236        key,
1237        vec![T::name("w"), argument::<T>(ast, member_type, held)],
1238    ));
1239
1240    // A distinct name per member: two headers at the same block scope
1241    // cannot share one, and the plan may give every member a header.
1242    let handle = format!("h{index}");
1243    Ok(match member.header {
1244        MemberHeader::NoHeader => vec![value],
1245        MemberHeader::PidCdr1 {
1246            pid,
1247            extended,
1248            must_understand,
1249        } => {
1250            let mut body = open_pid_header::<T>(
1251                ast,
1252                key,
1253                &handle,
1254                pid,
1255                member.member_id,
1256                extended,
1257                Constant::ParameterMember,
1258                must_understand,
1259                member_type,
1260            )?;
1261            body.push(value);
1262            body.push(call::<T>("finish_pid", vec![T::name(&handle)]));
1263            if optional {
1264                vec![guarded::<T>(present, body)]
1265            } else {
1266                body
1267            }
1268        }
1269        MemberHeader::EmHeader {
1270            lc,
1271            must_understand,
1272        } => {
1273            let body = vec![
1274                T::declare(
1275                    T::handle(Handle::EmHeader),
1276                    &handle,
1277                    Some(call_expr::<T>(
1278                        "write_emheader",
1279                        vec![
1280                            int::<T>(i128::from(member.member_id)),
1281                            int::<T>(i128::from(lc.bits())),
1282                            T::literal(Literal::Bool(must_understand)),
1283                        ],
1284                    )),
1285                ),
1286                value,
1287                call::<T>("finish_emheader", vec![T::name(&handle)]),
1288            ];
1289            if optional {
1290                vec![guarded::<T>(present, body)]
1291            } else {
1292                body
1293            }
1294        }
1295        // Rule (19) and §7.4.1.2: the header goes out either way, and a
1296        // declared length of zero is what says the member did not.
1297        MemberHeader::OptionalCdr1 {
1298            pid,
1299            extended,
1300            must_understand,
1301        } => {
1302            let mut body = open_pid_header::<T>(
1303                ast,
1304                key,
1305                &handle,
1306                pid,
1307                member.member_id,
1308                extended,
1309                Constant::ParameterOptional,
1310                must_understand,
1311                member_type,
1312            )?;
1313            body.push(guarded::<T>(present, vec![value]));
1314            body.push(call::<T>("finish_pid", vec![T::name(&handle)]));
1315            body
1316        }
1317        // Rule (20), §7.4.3.5.2: the BOOLEAN `<is_present>`, then the
1318        // value behind it.
1319        MemberHeader::OptionalFlag => vec![
1320            call::<T>("write_bool", vec![present.clone()]),
1321            guarded::<T>(present, vec![value]),
1322        ],
1323    })
1324}
1325
1326fn union_body<T: CodecTarget>(
1327    ast: &Ast,
1328    id: Id<TypeNode>,
1329    key: LayoutKey,
1330    discriminator: Option<Id<TypeNode>>,
1331    own_name: &str,
1332) -> Result<Vec<T::Stmt>, EncodeError> {
1333    let Some(discriminator) = discriminator else {
1334        return Err(EncodeError::NoWriter {
1335            member: own_name.to_string(),
1336            kind: "union with an unresolved discriminator",
1337        });
1338    };
1339    let mut deps = DepCollector::default();
1340    let layout = zerodds_ast_layout::xcdr::plan(ast, id, key, &mut deps)
1341        .map_err(crate::encode::plan_refusal)?;
1342    // The one crossing [`UnionLabelMatch`] has. Under the default it is
1343    // false for every discriminator and the arm chain below is the
1344    // clause's, unchanged.
1345    let suppress_negative = zerodds_ast_layout::plan::union_negative_selects_nothing(ast, id, key);
1346
1347    // `ElementPadding::Packed`: a union is its own alignment block too.
1348    // `annotation-external-union-branch` records one that starts where
1349    // the previous member ended.
1350    let packed = key.element_padding == ElementPadding::Packed;
1351    let mut statements = Vec::new();
1352    if packed {
1353        statements.push(begin_packed::<T>());
1354    }
1355    statements.push(dheader_declaration::<T>(layout.needs_dheader));
1356    let write_discriminator = T::perform(nested_call::<T>(
1357        ast,
1358        discriminator,
1359        key,
1360        vec![
1361            T::name("w"),
1362            argument::<T>(ast, discriminator, T::discriminant("v")),
1363        ],
1364    ));
1365    // Rules (27) and (28): a `@mutable` union frames its discriminator
1366    // as an `MMEMBER` at member id 0.
1367    statements.extend(mmember::<T>(
1368        ast,
1369        key,
1370        layout.discriminator,
1371        discriminator,
1372        0,
1373        "hd",
1374        write_discriminator,
1375    )?);
1376
1377    // The branch, selected by comparing the discriminator against each
1378    // case's labels. `default:` is the else of the chain, so a value
1379    // that matches no label and has no default writes nothing after the
1380    // discriminator — which is what the type says.
1381    let mut arms: Vec<(Option<T::Expr>, T::Block)> = Vec::new();
1382    let mut fallback: Option<T::Block> = None;
1383    // The labels the setting says select nothing, with the member each
1384    // one would have selected: the overlay below still needs them.
1385    let mut suppressed: Vec<(i128, Id<TypeNode>, String, bool)> = Vec::new();
1386    let mut default_case: Option<(Id<TypeNode>, NodeId)> = None;
1387    for case in ast.cases_of(id) {
1388        let Ok(case_node) = ast.case(*case) else {
1389            continue;
1390        };
1391        let Ok(member) = ast.member(case_node.member) else {
1392            continue;
1393        };
1394        let Some(member_type) = member.type_.resolved() else {
1395            return Err(EncodeError::NoWriter {
1396                member: member.name.clone(),
1397                kind: "union case with an unresolved type",
1398            });
1399        };
1400        let field = T::escape(&member.name);
1401        // Table 21: `@external` changes no octet, only where the binding
1402        // keeps the value — the same seam a structure member routes
1403        // through, with the branch accessor instead of the member one.
1404        let external = super::reached_externally::<T>(ast, member);
1405        let held = if external {
1406            T::external_case_value(ast, member_type, "v", &field)
1407        } else {
1408            T::case_member("v", &field)
1409        };
1410        // A branch closes a cycle the same way a member does, and the
1411        // column keeps it the same way. §7.2.2.4.4.4.7 makes no union
1412        // member `@optional`, so a branch on a cycle is one the
1413        // enclosing type reaches back through some *other* type's
1414        // `@optional` member.
1415        let held = if super::behind_pointer::<T>(ast, id, member_type, external, false) {
1416            T::behind_indirection(ast, member_type, held)
1417        } else {
1418            held
1419        };
1420        let value = T::perform(nested_call::<T>(
1421            ast,
1422            member_type,
1423            key,
1424            vec![T::name("w"), argument::<T>(ast, member_type, held)],
1425        ));
1426        // The selected member takes its own `MMEMBER` header under
1427        // `@mutable`, and nothing under any other extensibility.
1428        let planned = layout
1429            .members
1430            .iter()
1431            .find(|plan| plan.member == member.node_id);
1432        let write = T::block(mmember::<T>(
1433            ast,
1434            key,
1435            planned.map(|plan| plan.header),
1436            member_type,
1437            planned.map_or(0, |plan| plan.member_id),
1438            &format!("hu{}", planned.map_or(0, |plan| plan.member_id)),
1439            value,
1440        )?);
1441
1442        let mut condition: Option<T::Expr> = None;
1443        let mut is_default = false;
1444        for case_label in &case_node.labels {
1445            match case_label {
1446                CaseLabel::Default => is_default = true,
1447                CaseLabel::Value(expr) => {
1448                    let value = ast
1449                        .eval_integral(*expr)
1450                        .map_err(|_| EncodeError::NoWriter {
1451                            member: member.name.clone(),
1452                            kind: "union label that does not evaluate",
1453                        })?;
1454                    // A label a setting says selects nothing gets no arm
1455                    // at all, so the chain falls through to `default:`
1456                    // where the union has one and past the whole union
1457                    // where it has none — which is what the C peer this
1458                    // reproduces does with its own real union.
1459                    // `zerodds_ast_layout::union_negative_matches_nothing`.
1460                    if suppress_negative && value < 0 {
1461                        suppressed.push((value, member_type, field.clone(), external));
1462                        continue;
1463                    }
1464                    let test = T::binary(
1465                        "==",
1466                        T::discriminant("v"),
1467                        T::case_label(ast, discriminator, value),
1468                    );
1469                    condition = Some(match condition {
1470                        None => test,
1471                        Some(previous) => T::binary("||", previous, test),
1472                    });
1473                }
1474            }
1475        }
1476        if is_default {
1477            fallback = Some(write.clone());
1478            default_case = Some((member_type, member.node_id));
1479        }
1480        if let Some(condition) = condition {
1481            arms.push((Some(condition), write));
1482        }
1483    }
1484
1485    // The overlay the suppression owes. The peer this reproduces holds
1486    // its arms in one C storage: a discriminator carrying a suppressed
1487    // label falls through to `default:` there and writes the *storage*
1488    // as the default arm — the selected member's octets, reinterpreted.
1489    // A target with one field per arm would write the never-filled
1490    // default field instead, so each suppressed label gets a branch
1491    // ahead of the default write that puts the selected member's bit
1492    // pattern out at the default arm's width, through the runtime's
1493    // `write_union_overlay`. Mirrors `write_selected_branch` in
1494    // `zerodds-wire-codec`; a union without `default:` writes nothing
1495    // after the discriminator, exactly as the plain suppression does.
1496    if let (Some((default_type, default_node)), false) = (default_case, suppressed.is_empty()) {
1497        let width = overlay_width(ast, default_type, key, "default:")?;
1498        let planned = layout
1499            .members
1500            .iter()
1501            .find(|plan| plan.member == default_node);
1502        let mut chain = fallback;
1503        for (value, source_type, source_field, source_external) in suppressed.into_iter().rev() {
1504            let source = bit_pattern_source(ast, source_type, key, &source_field)?;
1505            // The overlay reproduces the peer's *storage*, which in the
1506            // peer's C union is the value itself — an `@external` arm's
1507            // pointer bytes are this process's addresses, not octets any
1508            // wire form has, so the dereference is taken first.
1509            let member = if source_external {
1510                T::external_case_value(ast, source_type, "v", &source_field)
1511            } else {
1512                T::case_member("v", &source_field)
1513            };
1514            // And behind the indirection where the branch has one, for
1515            // the reason the selected write gives.
1516            let member = if super::behind_pointer::<T>(ast, id, source_type, source_external, false)
1517            {
1518                T::behind_indirection(ast, source_type, member)
1519            } else {
1520                member
1521            };
1522            // An integer form crosses the target's `Int64` cast first —
1523            // the seam the enumeration codec reads its value through —
1524            // so an enum arm needs no spelling of its own and a target
1525            // with no unsigned types receives its widest integer.
1526            let member = match source {
1527                crate::emit::BitPatternSource::Int { .. } => cast::<T>(
1528                    T::scalar(Scalar::Int64),
1529                    T::enumerator_ordinal(ast, source_type, member),
1530                ),
1531                _ => member,
1532            };
1533            let pattern = T::bit_pattern(source, member);
1534            let write = T::perform(T::runtime(
1535                "w",
1536                "write_union_overlay",
1537                vec![pattern, int::<T>(i128::from(width))],
1538            ));
1539            let overlay = T::block(mmember::<T>(
1540                ast,
1541                key,
1542                planned.map(|plan| plan.header),
1543                source_type,
1544                planned.map_or(0, |plan| plan.member_id),
1545                &format!("ho{}", planned.map_or(0, |plan| plan.member_id)),
1546                write,
1547            )?);
1548            let test = T::binary(
1549                "==",
1550                T::discriminant("v"),
1551                T::case_label(ast, discriminator, value),
1552            );
1553            chain = Some(T::block(vec![T::when(test, overlay, chain)]));
1554        }
1555        fallback = chain;
1556    }
1557
1558    // Built from the last arm backwards, so the chain nests the way it
1559    // was declared.
1560    let mut chain: Option<T::Block> = fallback;
1561    for (condition, block) in arms.into_iter().rev() {
1562        let Some(condition) = condition else {
1563            continue;
1564        };
1565        chain = Some(T::block(vec![T::when(condition, block, chain)]));
1566    }
1567    if let Some(chain) = chain {
1568        statements.extend(T::into_statements(chain));
1569    }
1570
1571    // Rule (28): the parameter list a `@mutable` union writes under
1572    // XCDR1 ends at its sentinel, the same as a structure's.
1573    if layout.needs_sentinel {
1574        statements.push(call::<T>("write_sentinel", Vec::new()));
1575    }
1576    statements.push(finish_dheader::<T>());
1577    if packed {
1578        statements.push(finish_packed::<T>());
1579    }
1580    Ok(statements)
1581}
1582
1583/// The width the union overlay writes at — the arm's wire extent,
1584/// where it is fixed at 1, 2, 4 or 8 octets.
1585///
1586/// `zerodds-wire-codec`'s `storage_width`, asked of the same plan. The
1587/// refusal is loud rather than silent because
1588/// `union_negative_selects_nothing` answers `true` only for unions
1589/// whose arms all have such a width; reaching the error means that
1590/// crossing and this reading of it have diverged.
1591fn overlay_width(
1592    ast: &Ast,
1593    type_: Id<TypeNode>,
1594    key: LayoutKey,
1595    member: &str,
1596) -> Result<u8, EncodeError> {
1597    let mut deps = DepCollector::default();
1598    let width = zerodds_ast_layout::xcdr::plan(ast, type_, key, &mut deps)
1599        .ok()
1600        .and_then(|layout| layout.extent.sized())
1601        .filter(|width| matches!(width, 1 | 2 | 4 | 8));
1602    match width {
1603        #[allow(clippy::cast_possible_truncation)]
1604        Some(width) => Ok(width as u8),
1605        None => Err(EncodeError::NoWriter {
1606            member: member.to_string(),
1607            kind: "a union branch with no fixed-width storage to overlay",
1608        }),
1609    }
1610}
1611
1612/// What the overlay reads the suppressed arm's bits through.
1613///
1614/// The scalar instruction decides: an integer, character, boolean,
1615/// enumeration or bitmask holder has storage bits, a float has its
1616/// IEEE 754 bits, and anything else — a string, a `fixed`, a `wchar`
1617/// that carries GIOP's length octet — has no fixed-width pattern and
1618/// is refused with the same sentence `overlay_width` uses.
1619fn bit_pattern_source(
1620    ast: &Ast,
1621    type_: Id<TypeNode>,
1622    key: LayoutKey,
1623    member: &str,
1624) -> Result<crate::emit::BitPatternSource, EncodeError> {
1625    use crate::emit::BitPatternSource as Source;
1626    let no_pattern = || EncodeError::NoWriter {
1627        member: member.to_string(),
1628        kind: "a union branch with no fixed-width storage to overlay",
1629    };
1630    let octets_of = |bits: u16| match bits {
1631        1..=8 => 1,
1632        9..=16 => 2,
1633        17..=32 => 4,
1634        _ => 8,
1635    };
1636    match scalar_write(ast, type_, key, WcharPosition::Member).ok_or_else(no_pattern)? {
1637        ScalarWrite::Bool => Ok(Source::Bool),
1638        ScalarWrite::U8 | ScalarWrite::I8 | ScalarWrite::Char => Ok(Source::Int { octets: 1 }),
1639        ScalarWrite::I16 | ScalarWrite::U16 => Ok(Source::Int { octets: 2 }),
1640        ScalarWrite::I32 | ScalarWrite::U32 => Ok(Source::Int { octets: 4 }),
1641        ScalarWrite::I64 | ScalarWrite::U64 => Ok(Source::Int { octets: 8 }),
1642        ScalarWrite::F32 => Ok(Source::Float32),
1643        ScalarWrite::F64 => Ok(Source::Float64),
1644        ScalarWrite::Wchar {
1645            width,
1646            length_octet: false,
1647            ..
1648        } => Ok(Source::Int {
1649            octets: match width {
1650                WcharWidth::Two => 2,
1651                WcharWidth::Four => 4,
1652            },
1653        }),
1654        ScalarWrite::Enum { bits } | ScalarWrite::Bitmask { bits } => Ok(Source::Int {
1655            octets: octets_of(bits),
1656        }),
1657        _ => Err(no_pattern()),
1658    }
1659}
1660
1661/// One value behind the `MMEMBER` header the plan gave it, or bare.
1662///
1663/// `None` is a union of any extensibility but `@mutable`, where rules
1664/// (26) and (30) frame nothing per member.
1665///
1666/// `member_type` is the member's type, which the per-sample header form
1667/// needs: a constructed member takes `PID_EXTENDED` in a sample past
1668/// RTI's step and the short form in one below it.
1669#[allow(clippy::too_many_arguments)]
1670fn mmember<T: CodecTarget>(
1671    ast: &Ast,
1672    key: LayoutKey,
1673    header: Option<MemberHeader>,
1674    member_type: Id<TypeNode>,
1675    member_id: u32,
1676    handle: &str,
1677    value: T::Stmt,
1678) -> Result<Vec<T::Stmt>, EncodeError> {
1679    Ok(match header {
1680        None | Some(MemberHeader::NoHeader) => vec![value],
1681        Some(MemberHeader::PidCdr1 {
1682            pid,
1683            extended,
1684            must_understand,
1685        }) => {
1686            let mut statements = open_pid_header::<T>(
1687                ast,
1688                key,
1689                handle,
1690                pid,
1691                member_id,
1692                extended,
1693                Constant::ParameterMember,
1694                must_understand,
1695                member_type,
1696            )?;
1697            statements.push(value);
1698            statements.push(call::<T>("finish_pid", vec![T::name(handle)]));
1699            statements
1700        }
1701        Some(MemberHeader::EmHeader {
1702            lc,
1703            must_understand,
1704        }) => vec![
1705            T::declare(
1706                T::handle(Handle::EmHeader),
1707                handle,
1708                Some(call_expr::<T>(
1709                    "write_emheader",
1710                    vec![
1711                        int::<T>(i128::from(member_id)),
1712                        int::<T>(i128::from(lc.bits())),
1713                        T::literal(Literal::Bool(must_understand)),
1714                    ],
1715                )),
1716            ),
1717            value,
1718            call::<T>("finish_emheader", vec![T::name(handle)]),
1719        ],
1720        // Neither `OPT_FMEMBER` form reaches a union: a case member is
1721        // selected by the discriminator and is never `@optional`.
1722        Some(MemberHeader::OptionalCdr1 { .. } | MemberHeader::OptionalFlag) => vec![value],
1723    })
1724}
1725
1726/// The statements that open a PL_CDR1 parameter header for a member.
1727///
1728/// The form is fixed for every plan but
1729/// [`per_sample`], and under that plan it is fixed too for a member whose
1730/// id or length forces the extended form and for one whose type keeps the
1731/// short form ([`zerodds_ast_layout::keeps_short_header`]). The remaining
1732/// case — a constructed member of a sample whose type decides — writes a
1733/// run-time branch on [`EXTENDED_PARAM`], which is the decision the
1734/// sample's root handed down.
1735#[allow(clippy::too_many_arguments)]
1736fn open_pid_header<T: CodecTarget>(
1737    ast: &Ast,
1738    key: LayoutKey,
1739    handle: &str,
1740    pid: u16,
1741    member_id: u32,
1742    extended: bool,
1743    role: Constant,
1744    must_understand: bool,
1745    member_type: Id<TypeNode>,
1746) -> Result<Vec<T::Stmt>, EncodeError> {
1747    let flag = T::literal(Literal::Bool(must_understand));
1748    let extended_call = call_expr::<T>(
1749        "write_pid_extended",
1750        vec![
1751            int::<T>(i128::from(member_id)),
1752            T::constant(role),
1753            flag.clone(),
1754        ],
1755    );
1756    let short_call = call_expr::<T>(
1757        "write_pid",
1758        vec![int::<T>(i128::from(pid)), T::constant(role), flag],
1759    );
1760    if !per_sample(key) || extended {
1761        return Ok(vec![T::declare(
1762            T::handle(Handle::Pid),
1763            handle,
1764            Some(if extended { extended_call } else { short_call }),
1765        )]);
1766    }
1767    if zerodds_ast_layout::keeps_short_header(ast, member_type)
1768        .map_err(crate::encode::plan_refusal)?
1769    {
1770        return Ok(vec![T::declare(
1771            T::handle(Handle::Pid),
1772            handle,
1773            Some(short_call),
1774        )]);
1775    }
1776    let choose = |call: T::Expr| T::block(vec![T::perform(T::binary("=", T::name(handle), call))]);
1777    Ok(vec![
1778        // A local the body **assigns** before it reads, in both arms of
1779        // the `when` below — not [`CodecTarget::storage`], which a Java
1780        // local the body writes *into* needs and which allocates. A
1781        // Java handle class is constructed by the runtime and carries no
1782        // accessible no-argument constructor: `runtime/java`'s
1783        // `Writer.Pid(int, boolean, int, ParameterRole, int, boolean)` is
1784        // package-private, so `new Writer.Pid()` is not a program javac
1785        // accepts, and the `declare` spelling is what leaves it
1786        // definitely assigned here.
1787        T::declare(T::handle(Handle::Pid), handle, None),
1788        T::when(
1789            T::name(EXTENDED_PARAM),
1790            choose(extended_call),
1791            Some(choose(short_call)),
1792        ),
1793    ])
1794}
1795
1796/// The name, and resolved type, of the member with this identity.
1797///
1798/// `all_members_of`, so a member the aggregate inherited is found here
1799/// and not skipped as though the plan had never named it.
1800fn member_by_node(
1801    ast: &Ast,
1802    aggregate: Id<TypeNode>,
1803    node: NodeId,
1804) -> Option<(String, Id<TypeNode>)> {
1805    let id = ast
1806        .all_members_of(aggregate)
1807        .into_iter()
1808        .find(|id| ast.member(*id).is_ok_and(|m| m.node_id == node))?;
1809    let member = ast.member(id).ok()?;
1810    Some((member.name.clone(), member.type_.resolved()?))
1811}
1812
1813/// The handle a `DHEADER` is opened into, or the no-op form.
1814///
1815/// Declared either way so that the body's shape does not depend on the
1816/// plan's answer; the no-op handle writes nothing and finishes nothing.
1817fn dheader_declaration<T: CodecTarget>(present: bool) -> T::Stmt {
1818    T::declare(
1819        T::handle(Handle::DHeader),
1820        "d",
1821        Some(if present {
1822            T::runtime("w", "write_dheader", Vec::new())
1823        } else {
1824            T::runtime_static("no_dheader", Vec::new())
1825        }),
1826    )
1827}
1828
1829fn finish_dheader<T: CodecTarget>() -> T::Stmt {
1830    call::<T>("finish_dheader", vec![T::name("d")])
1831}
1832
1833/// The local a body's packed-element origin is saved in.
1834///
1835/// A distinct name from the DHEADER handle `d` and the element-length
1836/// handle `zd_el`, because all three can sit in one body: a packed
1837/// sequence writes an element length inside a packed origin.
1838const PACKED_SAVED: &str = "zd_saved";
1839
1840/// `size_t zd_saved = begin_packed_element(w);`
1841///
1842/// [`ElementPadding::Packed`] is HDDS's measured departure from §7.4.1:
1843/// a collection element and an aggregate value count their alignment
1844/// from their own first octet instead of from the encapsulation. The
1845/// generated codec holds the displaced origin and restores it with
1846/// [`finish_packed`].
1847fn begin_packed<T: CodecTarget>() -> T::Stmt {
1848    T::declare(
1849        T::scalar(Scalar::Size),
1850        PACKED_SAVED,
1851        Some(T::runtime("w", "begin_packed_element", Vec::new())),
1852    )
1853}
1854
1855/// `finish_packed_element(w, zd_saved);`
1856fn finish_packed<T: CodecTarget>() -> T::Stmt {
1857    T::perform(T::runtime(
1858        "w",
1859        "finish_packed_element",
1860        vec![T::name(PACKED_SAVED)],
1861    ))
1862}
1863
1864/// The handle a sequence element's own octet length is opened into.
1865///
1866/// [`zerodds_ast_layout::key::SequenceElementDelimiter::BeforeEachNonFinalElement`]
1867/// writes a four-octet `UInt32` in front of every element of a
1868/// `sequence` whose element type is not `@final`, under both encoding
1869/// versions. It is a DHEADER in form — the length is the element's own
1870/// octet count — so the runtime's `write_dheader` / `finish_dheader`
1871/// pair writes it.
1872fn element_length_declaration<T: CodecTarget>() -> T::Stmt {
1873    T::declare(
1874        T::handle(Handle::DHeader),
1875        "zd_el",
1876        Some(T::runtime("w", "write_dheader", Vec::new())),
1877    )
1878}
1879
1880fn finish_element_length<T: CodecTarget>() -> T::Stmt {
1881    T::perform(T::runtime("w", "finish_dheader", vec![T::name("zd_el")]))
1882}
1883
1884/// Whether an element of a `sequence` is preceded by its own octet
1885/// length under `key`.
1886///
1887/// `zerodds_ast_layout::xcdr::sequence_element_carries_length`, the rule
1888/// `zerodds_wire_codec` and the generated Rust ask too: a four-octet `UInt32` in front of every
1889/// element whose type is not `@final`. A type with no extensibility of
1890/// its own — a primitive, a string, an enumeration, a `typedef` — is
1891/// `@final` by construction and takes none; only the four aggregate
1892/// kinds can be `@appendable` or `@mutable`.
1893///
1894/// # Errors
1895/// [`EncodeError::NoLayout`] for an element id the model does not hold:
1896/// whether the element carries a length cannot be told, and guessing
1897/// either way frames every element of the sequence wrong.
1898pub(crate) fn sequence_element_carries_length(
1899    ast: &Ast,
1900    element: Id<TypeNode>,
1901    key: LayoutKey,
1902) -> Result<bool, EncodeError> {
1903    // The planner's rule, called rather than restated, so the generated
1904    // codecs, `zerodds-wire-codec` and the generated Rust cannot answer
1905    // differently about the same element.
1906    zerodds_ast_layout::xcdr::sequence_element_carries_length(ast, element, key).ok_or_else(|| {
1907        EncodeError::NoLayout {
1908            detail: format!(
1909                "the sequence element {element:?} is not a type the model holds, so whether it \
1910                 carries its own length cannot be told"
1911            ),
1912        }
1913    })
1914}
1915
1916/// The runtime call one scalar position asks for.
1917///
1918/// Every policy is already answered: `zerodds_ast_layout::scalar_write`
1919/// resolved them against the key and the position, so nothing here
1920/// reads an axis and nothing here can pass the wrong argument to one.
1921fn primitive_write_at<T: CodecTarget>(write: ScalarWrite, value: T::Expr) -> T::Stmt {
1922    match write {
1923        ScalarWrite::Bool => call::<T>("write_bool", vec![value]),
1924        ScalarWrite::U8 => call::<T>("write_u8", vec![value]),
1925        ScalarWrite::I8 => call::<T>("write_i8", vec![value]),
1926        // The runtime takes a code point and keeps the low byte, which
1927        // is where the narrowing belongs: a caller that truncated first
1928        // would put a different character on the wire.
1929        ScalarWrite::Char => call::<T>("write_char", vec![T::code_point(value, true)]),
1930        ScalarWrite::Wchar {
1931            width,
1932            length_octet,
1933            order,
1934        } => call::<T>(
1935            "write_wchar",
1936            vec![
1937                T::code_point(value, false),
1938                T::constant(match width {
1939                    WcharWidth::Two => Constant::WcharWidth2,
1940                    WcharWidth::Four => Constant::WcharWidth4,
1941                }),
1942                T::constant(if length_octet {
1943                    Constant::WcharGiopLengthPrefixed
1944                } else {
1945                    Constant::WcharFixed
1946                }),
1947                T::constant(wstring_unit_order(order)),
1948            ],
1949        ),
1950        ScalarWrite::I16 => call::<T>("write_i16", vec![value]),
1951        ScalarWrite::U16 => call::<T>("write_u16", vec![value]),
1952        ScalarWrite::I32 => call::<T>("write_i32", vec![value]),
1953        ScalarWrite::U32 => call::<T>("write_u32", vec![value]),
1954        ScalarWrite::I64 => call::<T>("write_i64", vec![value]),
1955        ScalarWrite::U64 => call::<T>("write_u64", vec![value]),
1956        ScalarWrite::F32 => call::<T>("write_f32", vec![value]),
1957        ScalarWrite::F64 => call::<T>("write_f64", vec![value]),
1958        ScalarWrite::F128 => call::<T>("write_f128", vec![value]),
1959        ScalarWrite::Str => call::<T>("write_string", vec![value]),
1960        ScalarWrite::Wstr {
1961            policy,
1962            order,
1963            code_units,
1964        } => match code_units {
1965            // `WstringCodeUnits::Utf8`: the characters' own octets, with
1966            // the length the UTF-8 octet count and only C2 — the
1967            // terminator — still reaching the wire. The order is inert
1968            // and is not passed.
1969            WstringCodeUnits::Utf8 => call::<T>(
1970                "write_wstring_utf8_octets",
1971                vec![value, T::constant(wstring_policy(policy))],
1972            ),
1973            WstringCodeUnits::Utf16 => call::<T>(
1974                "write_wstring_utf8",
1975                vec![
1976                    value,
1977                    T::constant(wstring_policy(policy)),
1978                    T::constant(wstring_unit_order(order)),
1979                ],
1980            ),
1981        },
1982        ScalarWrite::Fixed {
1983            digits,
1984            scale,
1985            count,
1986        } => call::<T>(
1987            "write_fixed",
1988            vec![
1989                value,
1990                int::<T>(i128::from(digits)),
1991                int::<T>(i128::from(scale)),
1992                T::constant(fixed_digit_count(count)),
1993            ],
1994        ),
1995        ScalarWrite::Enum { bits } => {
1996            call::<T>("write_enum", vec![int::<T>(i128::from(bits)), value])
1997        }
1998        ScalarWrite::Bitmask { bits } => call::<T>(
1999            "write_bitmask",
2000            vec![
2001                int::<T>(i128::from(bits)),
2002                cast::<T>(T::scalar(Scalar::UInt64), value),
2003            ],
2004        ),
2005    }
2006}
2007
2008const fn wstring_policy(policy: WstringLengthPolicy) -> Constant {
2009    match policy {
2010        WstringLengthPolicy::SPECIFICATION => Constant::WstringSpecification,
2011        WstringLengthPolicy::CHARACTER_COUNT => Constant::WstringCharacterCount,
2012        WstringLengthPolicy::CHARACTER_COUNT_TERMINATED => {
2013            Constant::WstringCharacterCountTerminated
2014        }
2015        WstringLengthPolicy::SPECIFICATION_WIDE_UNITS => Constant::WstringSpecificationWideUnits,
2016        WstringLengthPolicy::ROS_TYPE_SUPPORT => Constant::WstringRosTypeSupport,
2017        // The three cells no measured implementation writes. Named on
2018        // both sides so the C emitter can reach every cell the Rust
2019        // model can; a mapping that stopped at the measured five would
2020        // put the emitters back where the enumeration had them.
2021        _ => match (policy.counts, policy.terminator, policy.unit) {
2022            (WstringLengthCounts::Octets, _, WstringUnitWidth::Two) => {
2023                Constant::WstringOctetsTerminated2
2024            }
2025            (WstringLengthCounts::Octets, _, WstringUnitWidth::Four) => {
2026                Constant::WstringOctetsTerminated4
2027            }
2028            _ => Constant::WstringCharactersTerminated2,
2029        },
2030    }
2031}
2032
2033/// Whether a codec call for `element` has to be replaced by an inline
2034/// write because the element's `wchar` framing is not the member's.
2035///
2036/// The generated codec for a type is one function, named for the type
2037/// and not for where it is called from, so a `Char16` element under
2038/// [`WcharFraming::GiopLengthPrefixedMemberOnly`] cannot reach the right
2039/// framing through `zd_enc_p_wchar`. Rather than emit a second function
2040/// for the one primitive that needs one, the collection emitters write
2041/// the call out here with the element's own constant.
2042///
2043/// `None` for every other element and every other framing, so the
2044/// generated code is unchanged wherever the two positions agree — which
2045/// is every dialect but TAO's.
2046fn element_write<T: CodecTarget>(
2047    ast: &Ast,
2048    element: Id<TypeNode>,
2049    key: LayoutKey,
2050    value: T::Expr,
2051) -> Option<T::Stmt> {
2052    if key.wchar_framing.has_length_octet(WcharPosition::Member)
2053        == key.wchar_framing.has_length_octet(WcharPosition::Element)
2054    {
2055        return None;
2056    }
2057    if !matches!(
2058        resolve_primitive(ast, element, 0),
2059        Some(PrimitiveKind::WChar)
2060    ) {
2061        return None;
2062    }
2063    let write = scalar_write(ast, element, key, WcharPosition::Element)?;
2064    Some(primitive_write_at::<T>(write, value))
2065}
2066
2067/// Whether the elements of a collection of `element` are octets a target
2068/// may copy as one run: `octet` or `uint8`, through typedefs. `int8` is
2069/// left to the loop, since its storage is a signed type in the targets
2070/// that have one.
2071pub(crate) fn octet_elements(ast: &Ast, element: Id<TypeNode>) -> bool {
2072    matches!(
2073        resolve_primitive(ast, element, 0),
2074        Some(PrimitiveKind::Octet | PrimitiveKind::UInt8)
2075    )
2076}
2077
2078/// [`CodecTarget::bulk_octets_write`] for `count` elements of `v`, when
2079/// they are octets ([`octet_elements`]) and the target has the call.
2080/// `None` keeps the element loop.
2081fn bulk_write<T: CodecTarget>(ast: &Ast, element: Id<TypeNode>, count: T::Expr) -> Option<T::Stmt> {
2082    if octet_elements(ast, element) {
2083        T::bulk_octets_write("v", count)
2084    } else {
2085        None
2086    }
2087}
2088
2089/// The read side of [`bulk_write`], for `crate::emit::decode`.
2090pub(crate) fn bulk_read<T: CodecTarget>(
2091    ast: &Ast,
2092    element: Id<TypeNode>,
2093    count: T::Expr,
2094) -> Option<T::Stmt> {
2095    if octet_elements(ast, element) {
2096        T::bulk_octets_read("v", count)
2097    } else {
2098        None
2099    }
2100}
2101
2102/// The octet types among `types` whose codec nothing calls once `bulk`
2103/// holds: an `octet` or `uint8`, or a typedef of one, reached only as
2104/// the elements of a `sequence` or a one-dimensional array that the
2105/// target copies in one piece ([`CodecTarget::bulk_octets_write`]).
2106///
2107/// Such a codec is left out rather than emitted and excused: a `static`
2108/// function nothing calls is `-Wunused-function` under clang, in a
2109/// header too, and [`maybe_unused`] is the one codec that may have no
2110/// caller. The walk starts from every type that is not an octet type —
2111/// each of those is emitted as before — and follows the edges a codec
2112/// calls along ([`crate::adapter::c::dependencies`]), minus the ones a
2113/// bulk copy replaced. An octet type it reaches is called; the rest are
2114/// the answer. Empty when `bulk` does not hold.
2115pub(crate) fn called_only_in_bulk(
2116    ast: &Ast,
2117    types: &[Id<TypeNode>],
2118    entries: &[Id<TypeNode>],
2119    bulk: bool,
2120) -> Vec<Id<TypeNode>> {
2121    if !bulk {
2122        return Vec::new();
2123    }
2124    let octets: Vec<Id<TypeNode>> = types
2125        .iter()
2126        .copied()
2127        .filter(|id| octet_elements(ast, *id))
2128        .collect();
2129    if octets.is_empty() {
2130        return Vec::new();
2131    }
2132    // An octet type the caller of the codec names (`entries`) is called by
2133    // that caller: the RPC binding calls the codec of an `octet` parameter.
2134    let mut called: Vec<Id<TypeNode>> = octets
2135        .iter()
2136        .copied()
2137        .filter(|id| entries.contains(id))
2138        .collect();
2139    let mut pending: Vec<Id<TypeNode>> = types
2140        .iter()
2141        .copied()
2142        .filter(|id| !octets.contains(id))
2143        .collect();
2144    while let Some(caller) = pending.pop() {
2145        let copied_in_bulk = match ast.type_(caller).map(|node| &node.kind) {
2146            Ok(TypeKind::Sequence { element, .. }) => element
2147                .resolved()
2148                .is_some_and(|element| octet_elements(ast, element)),
2149            Ok(TypeKind::Array { element, dims }) => {
2150                dims.len() == 1
2151                    && element
2152                        .resolved()
2153                        .is_some_and(|element| octet_elements(ast, element))
2154            }
2155            _ => false,
2156        };
2157        if copied_in_bulk {
2158            continue;
2159        }
2160        for callee in crate::adapter::c::dependencies(ast, caller) {
2161            if octets.contains(&callee) && !called.contains(&callee) {
2162                called.push(callee);
2163                pending.push(callee);
2164            }
2165        }
2166    }
2167    octets
2168        .into_iter()
2169        .filter(|id| !called.contains(id))
2170        .collect()
2171}
2172
2173/// The primitive kind `id` is once typedefs are seen through, if it is
2174/// one.
2175///
2176/// zerodds-lint: recursion-depth 32 (typedef chains; bounded by the
2177/// `depth > 32` check, past which the answer is `None` — not a
2178/// primitive)
2179pub(crate) fn resolve_primitive(
2180    ast: &Ast,
2181    id: Id<TypeNode>,
2182    depth: usize,
2183) -> Option<PrimitiveKind> {
2184    if depth > 32 {
2185        return None;
2186    }
2187    match ast.type_(id).map(|node| node.kind.clone()) {
2188        Ok(TypeKind::Primitive(kind)) => Some(kind),
2189        Ok(TypeKind::Alias { aliased }) => aliased
2190            .resolved()
2191            .and_then(|target| resolve_primitive(ast, target, depth + 1)),
2192        _ => None,
2193    }
2194}
2195
2196/// The C name of a `wstring` unit order.
2197const fn wstring_unit_order(order: WstringUnitOrder) -> Constant {
2198    match order {
2199        WstringUnitOrder::Stream => Constant::WstringUnitsStream,
2200        WstringUnitOrder::BigEndianAlways => Constant::WstringUnitsBigEndianAlways,
2201        WstringUnitOrder::ByteOrderMark => Constant::WstringUnitsByteOrderMark,
2202        WstringUnitOrder::HostUnswapped => Constant::WstringUnitsHostUnswapped,
2203    }
2204}
2205
2206/// The runtime's name for a [`FixedDigitCount`].
2207///
2208/// The C runtime implements the count from [CDR] §9.3.2.8 itself, the
2209/// same way it implements the packing; this only names which of the three
2210/// the caller asked for.
2211const fn fixed_digit_count(policy: FixedDigitCount) -> Constant {
2212    match policy {
2213        FixedDigitCount::Declared => Constant::FixedDeclared,
2214        FixedDigitCount::ValuePrecision => Constant::FixedValuePrecision,
2215        FixedDigitCount::ValueStringDigits => Constant::FixedValueStringDigits,
2216    }
2217}
2218
2219// -- a map's entry order -------------------------------------------------
2220
2221/// `for (uint32_t <index> = 0; <index> < v->_length; ...) { body }`.
2222///
2223/// One builder, because the entry order emits three of these nested and
2224/// three literal copies is how the three would come to disagree about
2225/// the bound.
2226fn over_the_entries<T: CodecTarget>(index: &str, body: T::Block) -> T::Stmt {
2227    T::count_up(index, T::count_of("v", None), body)
2228}
2229
2230/// The entries in ascending key order, without a scratch array.
2231///
2232/// ```text
2233/// for (uint32_t zd_rank = 0; zd_rank < v->_length; …)
2234///     for (uint32_t i = 0; i < v->_length; …) {
2235///         uint32_t zd_before = 0;
2236///         for (uint32_t zd_k = 0; zd_k < v->_length; …)
2237///             if (<entry zd_k sorts before entry i>) zd_before = zd_before + 1;
2238///         if (zd_before == zd_rank) { <write the pair at i> }
2239///     }
2240/// ```
2241///
2242/// A rank and not a sort. The generated writer has no allocation and no
2243/// variable-length array — `runtime/c` allocates nothing in the write
2244/// path and an embedded target links it — so there is nowhere to put a
2245/// permutation. Computing each entry's rank in place needs no storage at
2246/// all, and a corpus map has at most three entries.
2247///
2248/// "Sorts before" carries the index as the tie-break, so two entries
2249/// with equal keys keep the order the sample declared: the same stable
2250/// order `encode.rs::map_entry_order` and Python's `sorted` give.
2251fn ascending_by_key<T: CodecTarget>(
2252    ast: &Ast,
2253    key_type: Id<TypeNode>,
2254    ordering: MapKeyOrdering,
2255    pair: T::Block,
2256) -> T::Stmt {
2257    let before = map_key_before::<T>(ast, key_type, ordering, "zd_k", "i");
2258    let count = over_the_entries::<T>(
2259        "zd_k",
2260        T::block(vec![T::when(
2261            before,
2262            T::block(vec![T::perform(increment::<T>("zd_before"))]),
2263            None,
2264        )]),
2265    );
2266    let select = over_the_entries::<T>(
2267        "i",
2268        T::block(vec![
2269            T::declare(T::scalar(Scalar::UInt32), "zd_before", Some(int::<T>(0))),
2270            count,
2271            T::when(
2272                T::binary("==", T::name("zd_before"), T::name("zd_rank")),
2273                pair,
2274                None,
2275            ),
2276        ]),
2277    );
2278    over_the_entries::<T>("zd_rank", T::block(vec![select]))
2279}
2280
2281/// Whether the entry at `left` sorts before the entry at `right`.
2282///
2283/// `less(left, right) || (!less(right, left) && left < right)` — the key
2284/// order with the declared position as the tie-break.
2285fn map_key_before<T: CodecTarget>(
2286    ast: &Ast,
2287    key_type: Id<TypeNode>,
2288    ordering: MapKeyOrdering,
2289    left: &str,
2290    right: &str,
2291) -> T::Expr {
2292    let less = |a: &str, b: &str| map_key_less::<T>(ast, key_type, ordering, a, b);
2293    T::binary(
2294        "||",
2295        T::paren(less(left, right)),
2296        T::paren(T::binary(
2297            "&&",
2298            T::unary("!", T::paren(less(right, left))),
2299            T::paren(T::binary("<", T::name(left), T::name(right))),
2300        )),
2301    )
2302}
2303
2304/// Whether the key at `left` is less than the key at `right`.
2305///
2306/// The comparison says the signedness out loud rather than taking the C
2307/// type's. An IDL `char` is a C `char`, whose signedness is
2308/// implementation-defined; the corpus was recorded on a host where it is
2309/// signed, and the cast pins that answer so the three columns still
2310/// agree on a host where it is not. Every other key type has a C
2311/// counterpart whose own `<` is the order the crossing named — `uint8_t`
2312/// and `uint16_t` and `bool` unsigned, the sized signed integers signed,
2313/// a generated `enum` by its declared values. A `string` and a `wstring`
2314/// are both a `const char *` holding UTF-8, and `strcmp` compares as
2315/// `unsigned char` (C11 §7.24.4), which is `std::string`'s order and,
2316/// UTF-8 being order preserving, `std::wstring`'s too.
2317fn map_key_less<T: CodecTarget>(
2318    ast: &Ast,
2319    key_type: Id<TypeNode>,
2320    ordering: MapKeyOrdering,
2321    left: &str,
2322    right: &str,
2323) -> T::Expr {
2324    let key_at = |index: &str| {
2325        let half = T::entry_half(T::index(T::elements("v"), T::name(index)), true);
2326        // An enumeration key is ordered by its enumerator *value*, and
2327        // a column whose enumeration is a string at the language
2328        // surface would otherwise compare the names. The identity
2329        // default leaves every other column's comparison where it was.
2330        if ordering == MapKeyOrdering::EnumeratorValue {
2331            return T::enumerator_ordinal(ast, key_type, half);
2332        }
2333        half
2334    };
2335    if ordering == MapKeyOrdering::CodeUnits {
2336        return T::binary(
2337            "<",
2338            T::call("strcmp", vec![key_at(left), key_at(right)]),
2339            int::<T>(0),
2340        );
2341    }
2342    let signed_char = ordering == MapKeyOrdering::SignedInteger
2343        && matches!(
2344            resolve_primitive(ast, key_type, 0),
2345            Some(PrimitiveKind::Char)
2346        );
2347    let read = |index: &str| {
2348        if signed_char {
2349            T::paren(cast::<T>(T::named("signed char"), key_at(index)))
2350        } else {
2351            key_at(index)
2352        }
2353    };
2354    // An eight-octet unsigned key crosses the target's own seam: a
2355    // column that holds `unsigned long long` as a bit pattern in its
2356    // widest signed type has no `<` that answers the unsigned order.
2357    // `CodecTarget::unsigned_less` says which columns those are.
2358    if ordering == MapKeyOrdering::UnsignedInteger
2359        && matches!(
2360            resolve_primitive(ast, key_type, 0),
2361            Some(PrimitiveKind::UInt64)
2362        )
2363    {
2364        return T::unsigned_less(8, read(left), read(right));
2365    }
2366    // A `boolean` key crosses the same kind of seam as the eight-octet
2367    // unsigned one: not every target's `<` takes two booleans.
2368    // `CodecTarget::bool_less` says how each column spells the order.
2369    if matches!(
2370        resolve_primitive(ast, key_type, 0),
2371        Some(PrimitiveKind::Boolean)
2372    ) {
2373        return T::bool_less(read(left), read(right));
2374    }
2375    T::binary("<", read(left), read(right))
2376}
2377
2378/// An enumeration's write body: the holder's width, and the integer that
2379/// goes into it.
2380///
2381/// The generated C `enum` keeps its **declared** constants — that is the
2382/// language mapping and not a wire question — so under
2383/// [`zerodds_ast_layout::EnumeratorValue::Ordinal`] the value in the
2384/// variable and the value on the wire are different integers, and a
2385/// chain maps one to the other. Under `Declared` the chain is the
2386/// identity and is not emitted at all, so the default column's C is
2387/// unchanged.
2388///
2389/// The reverse chain is `decode_c.rs::enumeration_body`, which compares
2390/// the wire pattern and assigns the declared constant.
2391///
2392/// # Why an enumerator this cannot answer for is an error
2393///
2394/// `zd_wire` is initialised to the **declared** value and each arm
2395/// overwrites it, so an enumerator that reaches no arm is written with
2396/// the value the variable already holds. Under `Declared` that is the
2397/// right integer and the chain is not emitted at all; under `Ordinal`
2398/// it is a different integer, and the generated C would write the
2399/// declared value, return success, and disagree with every other column
2400/// on the same setting. The reader's chain in
2401/// `decode_c.rs::enumeration_body` can skip an arm because a pattern
2402/// with no arm is refused by name — an absent arm there is a refusal
2403/// and here it is a wrong value, which is why the two sides answer
2404/// differently.
2405///
2406/// # Errors
2407/// [`EncodeError::NoEnumeratorValue`] for an enumerator whose handle the
2408/// model no longer holds, or whose wire value the setting cannot
2409/// compute.
2410fn enumeration_write<T: CodecTarget>(
2411    ast: &Ast,
2412    id: Id<TypeNode>,
2413    key: LayoutKey,
2414) -> Result<Vec<T::Stmt>, EncodeError> {
2415    let Some(write) = scalar_write(ast, id, key, WcharPosition::Member) else {
2416        return Err(EncodeError::NoWriter {
2417            member: T::type_name(ast, id),
2418            kind: "enumeration the layout plan has no scalar write for",
2419        });
2420    };
2421    // Through the enumerator seam: a column whose enumeration is not a
2422    // number at the language surface has no integer in the variable,
2423    // and the identity default leaves every other column's output where
2424    // it was. `CodecTarget::enumerator_ordinal`.
2425    let holder = cast::<T>(
2426        T::scalar(Scalar::Int64),
2427        T::enumerator_ordinal(ast, id, deref::<T>("v")),
2428    );
2429    // The declared values reach the wire unchanged under the
2430    // specification's setting, so the generated code is one call and
2431    // the chain below is what a front end that drops `@value` needs.
2432    if zerodds_ast_layout::plan::enumerators_are_declared(key) {
2433        return Ok(vec![primitive_write_at::<T>(write, holder)]);
2434    }
2435
2436    let enumeration = ast
2437        .type_(id)
2438        .map_err(|error| EncodeError::NoLayout {
2439            detail: format!("{error}"),
2440        })?
2441        .name
2442        .clone();
2443    // The mapping and not the policy: `enumerator_writes` resolved every
2444    // enumerator against the key, so nothing here can pass the wrong
2445    // setting or answer the axis by omission.
2446    let arms = zerodds_ast_layout::plan::enumerator_writes(ast, id, key).map_err(|enumerator| {
2447        EncodeError::NoEnumeratorValue {
2448            enumeration: enumeration.clone(),
2449            enumerator,
2450            detail: "its wire value does not evaluate under this --enumerator-value setting",
2451        }
2452    })?;
2453
2454    let mut statements = vec![T::declare(
2455        T::scalar(Scalar::Int64),
2456        "zd_wire",
2457        Some(holder),
2458    )];
2459    // Built from the last enumerator backwards so the chain reads in
2460    // declaration order, which is the order the reader's chain takes
2461    // too: where two enumerators share a declared value the first one
2462    // wins in both directions.
2463    let mut chain: Option<T::Block> = None;
2464    for (name, on_wire) in arms.into_iter().rev() {
2465        // Through the enumerator seam and not through
2466        // `module_prefix` plus `escape`, which was the same leak the
2467        // C++ reader found in the other direction. The two are one
2468        // string in C, where an enumerator sits in the enclosing scope
2469        // and is not folded; they are two different strings in every
2470        // column that scopes its enumerations or renames its members.
2471        // The C# column is both — `global::a.Colour.RED` against a
2472        // member fold that would have written `Red`.
2473        let constant = T::enumerator_constant(ast, id, &name);
2474        chain = Some(T::block(vec![T::when(
2475            T::binary("==", deref::<T>("v"), constant),
2476            // `wide_int` and not `int`: `zd_wire` is the Int64 holder,
2477            // and the assigned literal has to be one of that width —
2478            // TypeScript's `bigint` takes `0n` and not `0` (TS2322 on
2479            // every ordinal-mapped enumeration), and a declared value
2480            // above `i32::MAX` needs the suffix in Java, C and C++
2481            // too. For every value inside `i32` each of those columns
2482            // spells `wide_int` exactly as it spelt `int`, so their
2483            // generated files do not move.
2484            T::block(vec![T::perform(T::binary(
2485                "=",
2486                T::name("zd_wire"),
2487                T::wide_int(on_wire),
2488            ))]),
2489            chain,
2490        )]));
2491    }
2492    if let Some(chain) = chain {
2493        statements.extend(T::into_statements(chain));
2494    }
2495    statements.push(primitive_write_at::<T>(write, T::name("zd_wire")));
2496    Ok(statements)
2497}
2498
2499// -- small builders ------------------------------------------------------
2500//
2501// Each of these is now one call into the C backend of
2502// [`crate::emit::CodecTarget`]. They stay as free functions because the
2503// walk above reads better for it and because the argument order is this
2504// file's, not the trait's; what they no longer do is build a C node.
2505// When the walk itself becomes generic these become the trait's methods
2506// at their use sites and disappear.
2507
2508/// A runtime call, evaluated for its effect.
2509///
2510/// `name` is the operation without the runtime's prefix — `write_u32`,
2511/// not `zdw_write_u32` — and the receiver is prepended by the backend.
2512fn call<T: CodecTarget>(name: &str, args: Vec<T::Expr>) -> T::Stmt {
2513    T::perform(T::runtime("w", name, args))
2514}
2515
2516/// The same call where the result is kept.
2517///
2518/// A header-opening operation hands back the position it wrote at, which
2519/// the matching close needs; those bind rather than discard.
2520fn call_expr<T: CodecTarget>(name: &str, args: Vec<T::Expr>) -> T::Expr {
2521    T::runtime("w", name, args)
2522}
2523
2524/// The limit a bound states, where it states one this column can write.
2525///
2526/// `None` for the unbounded form and for a bound that does not
2527/// evaluate; a negative one is not a length and is refused the same
2528/// way. The decoder's own copy is
2529/// [`crate::emit::decode::bound_limit`] — one function because a
2530/// `sequence`, a `string`, a `wstring` and a `map` all read their bound
2531/// the same, and three copies in this file would be three chances for
2532/// one collection to stop being checked while the others were.
2533fn bound_limit(ast: &Ast, bound: &Bound) -> Option<i128> {
2534    bound
2535        .expr()
2536        .and_then(|expr| ast.eval_integral(expr).ok())
2537        .filter(|limit| *limit >= 0)
2538}
2539
2540/// The write for a bounded `string` or a bounded `wstring`.
2541///
2542/// The runtime's `write_string_bounded`/`write_wstring_bounded` takes
2543/// the bound as an argument and refuses a value that overruns it —
2544/// [`crate::encode::MemberWrite::String`] emits the same call for Rust,
2545/// against [`crate::encode`]'s own `write_string_bounded`, and
2546/// `crates/wire::Writer::write_string_bounded`'s remarks are why: the
2547/// bound is a promise about the source and not a difference on the
2548/// wire, so accepting an over-bound value and writing it as if it fit
2549/// would put a sample on the wire nobody wrote, and cutting it at a
2550/// character boundary would put a different one there with nothing to
2551/// say so.
2552///
2553/// # Errors
2554/// [`EncodeError::NoWriter`] for a bound on a scalar write that is
2555/// neither a string nor a wstring — never reached while
2556/// [`zerodds_ast_layout::plan::scalar_write`] answers a `string` type
2557/// with one of the two, which is the only way this is called.
2558fn bounded_string_write<T: CodecTarget>(
2559    own_name: &str,
2560    write: ScalarWrite,
2561    value: T::Expr,
2562    limit: i128,
2563) -> Result<T::Stmt, EncodeError> {
2564    match write {
2565        ScalarWrite::Str => Ok(call::<T>(
2566            "write_string_bounded",
2567            vec![value, T::unsigned_int(limit)],
2568        )),
2569        ScalarWrite::Wstr {
2570            policy,
2571            order,
2572            code_units,
2573        } => Ok(match code_units {
2574            // The bound still counts UTF-16 code units, whatever the wire
2575            // carries; the runtime's octet form measures them and writes
2576            // the UTF-8 octets. See `write_wstring_utf8_octets_bounded`.
2577            WstringCodeUnits::Utf8 => call::<T>(
2578                "write_wstring_utf8_octets_bounded",
2579                vec![
2580                    value,
2581                    T::unsigned_int(limit),
2582                    T::constant(wstring_policy(policy)),
2583                ],
2584            ),
2585            WstringCodeUnits::Utf16 => call::<T>(
2586                "write_wstring_bounded",
2587                vec![
2588                    value,
2589                    T::unsigned_int(limit),
2590                    T::constant(wstring_policy(policy)),
2591                    T::constant(wstring_unit_order(order)),
2592                ],
2593            ),
2594        }),
2595        _ => Err(EncodeError::NoWriter {
2596            member: own_name.to_string(),
2597            kind: "a bound on a scalar write that is not a string or a wstring",
2598        }),
2599    }
2600}
2601
2602/// A collection's count, checked against its bound before anything of
2603/// the collection is written.
2604///
2605/// `method` is `check_sequence_bound` or `check_map_bound` — the
2606/// runtime's own names, matching
2607/// [`crate::encode`]'s Rust codegen and `crates/wire::Writer`'s two
2608/// checks of the same shape. Both take the count and the bound and
2609/// refuse loudly, in whatever their language's own mechanism is,
2610/// before the DHEADER or the length word goes out — a sender that
2611/// wrote more than its own declaration allows has written a value of a
2612/// type nobody declared, and stopping after the length word would
2613/// leave that word on the wire ahead of the refusal.
2614fn bound_check<T: CodecTarget>(method: &str, count: T::Expr, limit: i128) -> T::Stmt {
2615    call::<T>(method, vec![count, T::unsigned_int(limit)])
2616}
2617
2618fn deref<T: CodecTarget>(name: &str) -> T::Expr {
2619    T::whole(name)
2620}
2621
2622fn arrow<T: CodecTarget>(value: &str, field: &str) -> T::Expr {
2623    T::member(value, field)
2624}
2625
2626/// zerodds-lint: recursion-depth 1 — this does not recurse. The callee
2627/// is `T::cast`; `dds_bounded_recursion` compares only the last
2628/// segment of a call path, so it reads as a self-call.
2629fn cast<T: CodecTarget>(type_: T::Type, value: T::Expr) -> T::Expr {
2630    T::cast(type_, value)
2631}
2632
2633/// zerodds-lint: recursion-depth 1 — this does not recurse. The callee
2634/// is `T::int`; `dds_bounded_recursion` compares only the last
2635/// segment of a call path, so it reads as a self-call.
2636fn int<T: CodecTarget>(value: i128) -> T::Expr {
2637    T::int(value)
2638}
2639
2640fn increment<T: CodecTarget>(name: &str) -> T::Expr {
2641    T::binary(
2642        "=",
2643        T::name(name),
2644        T::binary("+", T::name(name), int::<T>(1)),
2645    )
2646}