Skip to main content

zerodds_idl_cpp/
c_mode.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! IDL → C99 codegen mode (vendor spec `zerodds-xcdr2-c-1.0`).
5//!
6//! For each IDL specification, this module emits:
7//! - C99 `typedef struct` definitions per `struct` (with a module prefix
8//!   in the type name, because C99 has no namespaces).
9//! - Static `zerodds_typesupport_t` tables with XCDR2 encoder/
10//!   decoder/key-hash/free function pointers.
11//! - Inline body implementations of the encoders/decoders that produce
12//!   the XCDR2 wire format (XTypes 1.3 §7.4) byte-exactly.
13//!
14//! ## Scope
15//!
16//! Supported:
17//! - Structs with `@final`/`@appendable`/`@mutable` extensibility.
18//! - Primitive types (boolean, octet, short/long/long long + unsigned,
19//!   float, double).
20//! - `string` (unbounded).
21//! - `sequence<T>` (unbounded; nested sequences allowed).
22//! - Nested modules → type-name prefix with `::` (cross-language
23//!   convention) and identifier prefix with `_` (C99-conformant).
24//! - `@key` members → key-hash routine via `PlainCdr2BeKeyHolder`.
25//! - `@id(N)` for @mutable.
26//! - **Enums** → `int32_t` alias + prefixed enumerator constants (Bug C).
27//! - **Typedefs** → resolved to the aliased type at every reference (Bug C).
28//! - **Nested struct** members → inline-embedded by value (Bug C).
29//! - **Fixed arrays** (`T x[N][M]…`) → C array, no length prefix (Bug C).
30//! - **`@optional`** members → `<name>_present` companion flag + a wire
31//!   presence boolean (Bug C2; no longer flattened to mandatory).
32//! - **Array bound by named constant** (`long v[N]`) → the bound is resolved
33//!   through the const symbol table (#43, const-array-bound).
34//! - **`sequence<T>`** for primitive, string, enum, nested struct and nested
35//!   `sequence<…>` element types (#43, sequence widening).
36//! - **`map<K,V>`** → parallel `keys[]`/`vals[]` arrays + DHEADER/count codec
37//!   (#43, map).
38//! - **`wstring`** → NUL-terminated `uint16_t*`, UTF-16-LE on the wire
39//!   (XTypes §7.4.4.6) (#43, wstring).
40//! - **Discriminated `union`** → C tagged union `struct { D _d; union {…} _u; }`
41//!   with a switch-on-discriminator encode/decode (#43, union). A top-level
42//!   union additionally gets a `TypeSupport` so it can be a topic type.
43//! - **`typedef`-to-aggregate** (alias of a struct / sequence / map) → resolved
44//!   to the aliased aggregate at every reference (#43, typedef-to-aggregate).
45//! - **typedef ALIAS CHAIN + typedef-to-array** (`typedef A B; typedef long
46//!   M[3][3];`) → resolved through the chain to the root type; an array-alias
47//!   contributes its dims at the use site (#43, typedefs).
48//! - **`bitmask`** → smallest holder uint typedef (default `@bit_bound(32)` →
49//!   `uint32_t`) + `<LABEL>` flag-bit constants; **`bitset`** → packed holder
50//!   uint typedef + per-field SHIFT/MASK accessor macros. Both serialize as
51//!   their holder integer (XTypes §7.3.1.2) (#43, bitset/bitmask).
52//! - **Self-/mutually-recursive types** (self-referential through a sequence,
53//!   or mutual `@external`) → a heap-indirected C type (pointer-to-tag element)
54//!   plus a runtime `_write_body`/`_read_body` helper that splices the recursion
55//!   at RUNTIME (XTypes §7.4.5) (#43, recursion / forward-decl).
56//! - **Forward-declared** struct/union (then defined) → aggregate typedefs are
57//!   emitted in by-value dependency order so a later definition is reachable.
58//!
59//! Out of scope (errors at the codegen level, honest partial):
60//! - `fixed<M,N>` decimal, `any`, `long double` — these legitimately stay out
61//!   of the C profile (no native C fixed-decimal / variant type).
62//! - A genuinely **infinite-size** type (a by-value self/mutual cycle, e.g.
63//!   `struct Node { Node n; };`) is rejected cleanly — the only legal
64//!   self-reference is heap-indirected (through a sequence).
65//! - `@optional` *inside* a nested struct, nested `@appendable`/`@mutable`
66//!   struct splice (only the inline `@final` form is wired), nested-aggregate
67//!   union arms inside a `@mutable` member, and freeing of heap-owning members
68//!   reached only through a fixed array.
69//!
70//! ## Invocation
71//!
72//! ```rust
73//! use zerodds_idl::config::ParserConfig;
74//! use zerodds_idl_cpp::{generate_c_header, CGenOptions};
75//!
76//! let ast = zerodds_idl::parse("@final struct Point { long x; long y; };",
77//!                              &ParserConfig::default()).unwrap();
78//! let header = generate_c_header(&ast, &CGenOptions::default()).unwrap();
79//! assert!(header.contains("typedef struct Point_s"));
80//! assert!(header.contains("Point_typesupport"));
81//! ```
82
83#![allow(clippy::module_name_repetitions)]
84
85use core::fmt::Write as _;
86use std::collections::{BTreeMap, BTreeSet};
87
88use zerodds_idl::ast::{
89    Annotation, AnnotationParams, BitmaskDecl, BitsetDecl, Case, CaseLabel, ConstExpr,
90    ConstrTypeDecl, Declarator, Definition, EnumDef, FloatingType, IntegerType, LiteralKind,
91    ModuleDef, PrimitiveType, ScopedName, Specification, StructDcl, StructDef, SwitchTypeSpec,
92    TypeDecl, TypeSpec, UnionDcl, UnionDef,
93};
94use zerodds_idl::semantics::const_eval::{Symbol, SymbolTable, evaluate, evaluate_positive_int};
95
96use crate::error::CppGenError;
97
98/// Codegen-scoped type registry for the C backend (Bug C scope widening).
99/// Resolves a `Scoped` member to its referent kind so the emitter can pick the
100/// right C type name + XCDR2 codec instead of rejecting all non-flat members.
101#[derive(Default)]
102struct TypeReg {
103    /// IDL simple enum name → its C identifier (module-prefixed).
104    enums: BTreeMap<String, String>,
105    /// IDL simple enum name → signed wire holder width in BYTES (1/2/4) from
106    /// `@bit_bound` (XTypes §7.4.5.1): N≤8 → 1, N≤16 → 2, else 4. The C in-memory
107    /// type stays `int32_t`; only the wire write/read narrows.
108    enum_bytes: BTreeMap<String, u32>,
109    /// IDL simple typedef name → the aliased TypeSpec.
110    typedefs: BTreeMap<String, TypeSpec>,
111    /// IDL simple typedef name → its declarator array dimensions, for a
112    /// `typedef long Matrix3[3][3];` style array-alias (#43, typedef-to-array).
113    /// A reference to such a typedef inherits these dims at the use site.
114    typedef_arrays: BTreeMap<String, Vec<u64>>,
115    /// IDL simple bitmask name → (C identifier, holder uint C type). A bitmask
116    /// serializes as its smallest holder uint (XTypes §7.3.1.2.2, default
117    /// `@bit_bound(32)` → `uint32_t`).
118    bitmasks: BTreeMap<String, (String, &'static str)>,
119    /// IDL simple bitset name → (C identifier, packed holder uint C type). A
120    /// bitset serializes as one packed integer sized to the sum of its bitfield
121    /// widths (XTypes §7.3.1.2.1).
122    bitsets: BTreeMap<String, (String, &'static str)>,
123    /// IDL simple struct name → its C identifier (module-prefixed) + def.
124    structs: BTreeMap<String, (String, StructDef)>,
125    /// IDL simple union name → its C identifier (module-prefixed) + def.
126    unions: BTreeMap<String, (String, UnionDef)>,
127    /// Struct/union simple names that participate in a reference cycle
128    /// (self-referential through a sequence, or mutually recursive). Members of
129    /// this set are spliced via a generated `_write_body`/`_read_body` helper
130    /// (runtime recursion) instead of being inline-emitted, which would recurse
131    /// infinitely at codegen time (XTypes §7.4.5 / Bug G, C backend).
132    recursive: BTreeSet<String>,
133    /// Constant symbol table, keyed by BOTH the fully-qualified path
134    /// (`Mod::Sub::N`) and the simple name (`N`), so an array bound written
135    /// as a named constant (`long v[N]`) resolves to its literal the way every
136    /// other backend does (Bug C #43, const-array-bound). Enumerator ordinals
137    /// are folded in too (case labels / bounds frequently reference them).
138    consts: SymbolTable,
139}
140
141impl TypeReg {
142    fn build(defs: &[Definition]) -> Self {
143        let mut r = TypeReg::default();
144        collect_types(defs, &[], &mut r);
145        r.compute_recursive();
146        r
147    }
148
149    /// Compute the set of struct/union names that participate in a reference
150    /// cycle. An aggregate is recursive if it can reach itself by following
151    /// member references — directly, or (the only legal self-reference) through
152    /// a sequence/map element, a union arm, or a typedef alias. Such types must
153    /// be spliced via a runtime helper, not inline-emitted (codegen would
154    /// otherwise recurse forever — Bug G for the C backend).
155    fn compute_recursive(&mut self) {
156        let names: Vec<String> = self
157            .structs
158            .keys()
159            .chain(self.unions.keys())
160            .cloned()
161            .collect();
162        let mut recursive = BTreeSet::new();
163        for start in &names {
164            let mut seen = BTreeSet::new();
165            if self.reaches(start, start, &mut seen) {
166                recursive.insert(start.clone());
167            }
168        }
169        self.recursive = recursive;
170    }
171
172    /// True if aggregate `from` can reach `target` by following member /
173    /// element / arm references. `seen` guards the traversal itself against
174    /// cycles. zerodds-lint: recursion-depth 256 (bounded by distinct type set)
175    fn reaches(&self, from: &str, target: &str, seen: &mut BTreeSet<String>) -> bool {
176        if !seen.insert(from.to_string()) {
177            return false;
178        }
179        let mut refs: Vec<String> = Vec::new();
180        if let Some((_, sdef)) = self.structs.get(from) {
181            for m in &sdef.members {
182                self.collect_aggregate_refs(&m.type_spec, &mut refs);
183            }
184        }
185        if let Some((_, udef)) = self.unions.get(from) {
186            self.collect_aggregate_refs(&switch_type_spec(&udef.switch_type), &mut refs);
187            for c in &udef.cases {
188                self.collect_aggregate_refs(&c.element.type_spec, &mut refs);
189            }
190        }
191        for r in refs {
192            if r == target {
193                return true;
194            }
195            if self.reaches(&r, target, seen) {
196                return true;
197            }
198        }
199        false
200    }
201
202    /// Collect the simple names of struct/union aggregates referenced by a
203    /// type-spec (through sequences, maps, and typedef aliases). Enums, bitsets,
204    /// bitmasks and primitives terminate (they cannot close a cycle).
205    /// zerodds-lint: recursion-depth 64 (bounded by AST nesting)
206    fn collect_aggregate_refs(&self, ts: &TypeSpec, out: &mut Vec<String>) {
207        let resolved = resolve_alias(self, ts);
208        match &resolved {
209            TypeSpec::Scoped(sc) => {
210                if let Some(last) = scoped_last(sc) {
211                    if self.structs.contains_key(&last) || self.unions.contains_key(&last) {
212                        out.push(last);
213                    }
214                }
215            }
216            TypeSpec::Sequence(seq) => self.collect_aggregate_refs(&seq.elem, out),
217            TypeSpec::Map(m) => {
218                self.collect_aggregate_refs(&m.key, out);
219                self.collect_aggregate_refs(&m.value, out);
220            }
221            _ => {}
222        }
223    }
224
225    fn is_recursive(&self, name: &str) -> bool {
226        self.recursive.contains(name)
227    }
228}
229
230/// zerodds-lint: recursion-depth 64 (module/type tree; bounded by IDL nesting)
231fn collect_types(defs: &[Definition], scope: &[String], r: &mut TypeReg) {
232    for d in defs {
233        match d {
234            Definition::Module(m) => {
235                let mut s = scope.to_vec();
236                s.push(m.name.text.clone());
237                collect_types(&m.definitions, &s, r);
238            }
239            Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Enum(e))) => {
240                r.enums
241                    .insert(e.name.text.clone(), c_identifier(scope, &e.name.text));
242                let ebound = extract_int_annotation_c(&e.annotations, "bit_bound")
243                    .filter(|&v| (1..=32).contains(&v))
244                    .unwrap_or(32);
245                let ebytes = if ebound <= 8 {
246                    1
247                } else if ebound <= 16 {
248                    2
249                } else {
250                    4
251                };
252                r.enum_bytes.insert(e.name.text.clone(), ebytes);
253                // Register enumerator ordinals (both simple + scoped) so a
254                // const-expression that references an enumerator (array bound or
255                // union case label) resolves.
256                let type_name = scope_join(scope, &e.name.text, "::");
257                for (i, en) in e.enumerators.iter().enumerate() {
258                    let sym = Symbol::EnumValue {
259                        type_name: type_name.clone(),
260                        value: i32::try_from(i).unwrap_or(0),
261                    };
262                    r.consts.insert(en.name.text.clone(), sym.clone());
263                    r.consts.insert(scope_join(scope, &en.name.text, "::"), sym);
264                }
265            }
266            Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Def(s)))) => {
267                r.structs.insert(
268                    s.name.text.clone(),
269                    (c_identifier(scope, &s.name.text), s.clone()),
270                );
271            }
272            Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Union(UnionDcl::Def(u)))) => {
273                r.unions.insert(
274                    u.name.text.clone(),
275                    (c_identifier(scope, &u.name.text), u.clone()),
276                );
277            }
278            Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Bitmask(b))) => {
279                let holder = bitmask_holder_c_type(b);
280                r.bitmasks.insert(
281                    b.name.text.clone(),
282                    (c_identifier(scope, &b.name.text), holder),
283                );
284            }
285            Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Bitset(b))) => {
286                let holder = bitset_holder_c_type(b);
287                r.bitsets.insert(
288                    b.name.text.clone(),
289                    (c_identifier(scope, &b.name.text), holder),
290                );
291            }
292            Definition::Type(TypeDecl::Typedef(td)) => {
293                for decl in &td.declarators {
294                    match decl {
295                        Declarator::Simple(n) => {
296                            r.typedefs.insert(n.text.clone(), td.type_spec.clone());
297                        }
298                        Declarator::Array(arr) => {
299                            // `typedef long Matrix3[3][3];` — the alias carries
300                            // both the element type AND fixed dims; a use site
301                            // inherits the dims (#43, typedef-to-array).
302                            r.typedefs
303                                .insert(arr.name.text.clone(), td.type_spec.clone());
304                            let dims: Vec<u64> = arr
305                                .sizes
306                                .iter()
307                                .map(|sz| const_expr_to_u64_pre(&r.consts, sz).unwrap_or(0))
308                                .collect();
309                            r.typedef_arrays.insert(arr.name.text.clone(), dims);
310                        }
311                    }
312                }
313            }
314            Definition::Const(c) => {
315                // Resolve the const value against whatever's already collected
316                // (forward refs to later consts are rare for array bounds, and
317                // we make a best-effort pass).
318                if let Ok(v) = evaluate(&c.value, &r.consts) {
319                    r.consts
320                        .insert(c.name.text.clone(), Symbol::Const(v.clone()));
321                    r.consts
322                        .insert(scope_join(scope, &c.name.text, "::"), Symbol::Const(v));
323                }
324            }
325            _ => {}
326        }
327    }
328}
329
330/// Resolve a TypeSpec through typedef chains to its effective type.
331/// zerodds-lint: recursion-depth 16
332fn resolve_alias(reg: &TypeReg, ts: &TypeSpec) -> TypeSpec {
333    let mut cur = ts.clone();
334    for _ in 0..16 {
335        let TypeSpec::Scoped(s) = &cur else { break };
336        let Some(last) = s.parts.last() else { break };
337        let Some(aliased) = reg.typedefs.get(&last.text).cloned() else {
338            break;
339        };
340        cur = aliased;
341    }
342    cur
343}
344
345fn scoped_last(s: &ScopedName) -> Option<String> {
346    s.parts.last().map(|p| p.text.clone())
347}
348
349fn unsupported(kind: &'static str) -> CppGenError {
350    CppGenError::UnsupportedConstruct {
351        construct: kind.to_string(),
352        context: None,
353    }
354}
355
356/// Codegen options (parallel to `CppGenOptions`).
357#[derive(Debug, Clone, Default)]
358pub struct CGenOptions {
359    /// Optional include-guard name (default: `ZERODDS_GENERATED_H`).
360    pub include_guard: Option<String>,
361    /// Optional file-header comment.
362    pub file_header: Option<String>,
363}
364
365/// Produces a complete C99 header from an IDL specification.
366///
367/// # Errors
368/// - [`CppGenError::UnsupportedConstruct`]: out-of-scope IDL constructs that
369///   legitimately stay outside the C profile (`fixed<M,N>` decimal, `any`,
370///   `long double`), or a genuinely infinite-size by-value self/mutual
371///   recursion. Structs, enums, typedefs (incl. alias chains + to-array),
372///   bitsets/bitmasks, arrays (literal or const-bound), sequences, maps,
373///   `wstring`, discriminated unions, forward declarations and
374///   sequence-indirected recursive types are all supported.
375pub fn generate_c_header(ast: &Specification, opts: &CGenOptions) -> Result<String, CppGenError> {
376    let reg = TypeReg::build(&ast.definitions);
377    let mut ctx = Ctx::new(opts, &reg);
378    ctx.emit_preamble();
379    // Emit enum typedefs first (referenced by struct fields as int32 aliases).
380    ctx.emit_enums(&ast.definitions, &[]);
381    // Bitset/bitmask holder typedefs + bitmask position constants (#43).
382    ctx.emit_bits(&ast.definitions, &[]);
383    // Aggregate (struct + union) typedefs in by-value dependency order, so a
384    // by-value embed sees its referent's complete C type, and a recursive type's
385    // body helper sees its own typedef (#43, recursion / forward-decl). A
386    // recursive self-reference is always behind a pointer (sequence element) so
387    // it does not constrain the order.
388    ctx.emit_all_aggregate_typedefs(&ast.definitions)?;
389    // Forward-declare + define the runtime body helpers for recursive types
390    // (so a self-/mutually-recursive reference becomes a runtime call instead of
391    // an infinite codegen recursion — Bug G for the C backend).
392    ctx.emit_recursive_helpers()?;
393    ctx.walk_definitions(&ast.definitions, &[])?;
394    ctx.emit_postamble();
395    Ok(ctx.out)
396}
397
398// ============================================================================
399// Internals
400// ============================================================================
401
402struct Ctx<'a> {
403    out: String,
404    opts: &'a CGenOptions,
405    reg: &'a TypeReg,
406    /// Nesting depth of collection (sequence/map) element loops, so each loop
407    /// gets a unique counter variable (`i0`, `i1`, …). Without this, a
408    /// `sequence<sequence<T>>` reuses `i` and the inner loop shadows the outer
409    /// index, corrupting addressing (segfault). Bumped around each element body.
410    coll_depth: u32,
411}
412
413#[derive(Debug, Clone, Copy, PartialEq, Eq)]
414enum Extensibility {
415    Final,
416    Appendable,
417    Mutable,
418}
419
420impl<'a> Ctx<'a> {
421    fn new(opts: &'a CGenOptions, reg: &'a TypeReg) -> Self {
422        Self {
423            out: String::new(),
424            opts,
425            reg,
426            coll_depth: 0,
427        }
428    }
429
430    /// Emit each IDL enum as a C `enum` + an int32 typedef so struct fields can
431    /// reference it by name and the codec can treat it as int32 (Spec §7.4.1.4.2).
432    /// zerodds-lint: recursion-depth 64 (module tree; bounded by IDL nesting)
433    fn emit_enums(&mut self, defs: &[Definition], scope: &[String]) {
434        for d in defs {
435            match d {
436                Definition::Module(m) => {
437                    let mut s = scope.to_vec();
438                    s.push(m.name.text.clone());
439                    self.emit_enums(&m.definitions, &s);
440                }
441                Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Enum(e))) => {
442                    self.emit_enum(e, scope);
443                }
444                _ => {}
445            }
446        }
447    }
448
449    fn emit_enum(&mut self, e: &EnumDef, scope: &[String]) {
450        let c_name = c_identifier(scope, &e.name.text);
451        let _ = writeln!(self.out, "typedef int32_t {c_name}_t;");
452        for (i, en) in e.enumerators.iter().enumerate() {
453            // Enumerator constants are module+enum prefixed to stay unique in C's
454            // flat namespace.
455            let _ = writeln!(
456                self.out,
457                "enum {{ {c_name}_{label} = {i} }};",
458                label = en.name.text
459            );
460        }
461        let _ = writeln!(self.out);
462    }
463
464    /// Emit each IDL bitset/bitmask as a C holder-integer typedef. A bitmask
465    /// additionally gets a `<C>_<LABEL>` flag-bit constant per value; a bitset
466    /// gets `<C>_<field>_SHIFT` / `<C>_<field>_MASK` accessor macros (XTypes
467    /// §7.3.1.2). The wire form is the holder integer in both cases (#43).
468    /// zerodds-lint: recursion-depth 64 (module tree; bounded by IDL nesting)
469    fn emit_bits(&mut self, defs: &[Definition], scope: &[String]) {
470        for d in defs {
471            match d {
472                Definition::Module(m) => {
473                    let mut s = scope.to_vec();
474                    s.push(m.name.text.clone());
475                    self.emit_bits(&m.definitions, &s);
476                }
477                Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Bitmask(b))) => {
478                    self.emit_bitmask(b, scope);
479                }
480                Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Bitset(b))) => {
481                    self.emit_bitset(b, scope);
482                }
483                _ => {}
484            }
485        }
486    }
487
488    fn emit_bitmask(&mut self, b: &BitmaskDecl, scope: &[String]) {
489        let c_name = c_identifier(scope, &b.name.text);
490        let holder = bitmask_holder_c_type(b);
491        let _ = writeln!(self.out, "typedef {holder} {c_name}_t;");
492        let mut next_pos: u32 = 0;
493        for v in &b.values {
494            let pos = extract_int_annotation_c(&v.annotations, "position").unwrap_or(next_pos);
495            next_pos = pos.saturating_add(1);
496            // Flag bit value (`1 << position`) as a C enum constant — keeps it a
497            // compile-time constant of the holder width.
498            let _ = writeln!(
499                self.out,
500                "enum {{ {c_name}_{label} = (1u << {pos}) }};",
501                label = v.name.text
502            );
503        }
504        let _ = writeln!(self.out);
505    }
506
507    fn emit_bitset(&mut self, b: &BitsetDecl, scope: &[String]) {
508        let c_name = c_identifier(scope, &b.name.text);
509        let holder = bitset_holder_c_type(b);
510        let _ = writeln!(self.out, "typedef {holder} {c_name}_t;");
511        // Per named bitfield: SHIFT (offset) + MASK accessor macros so a caller
512        // can pack/unpack the field within the holder integer.
513        let mut offset: u32 = 0;
514        for f in &b.bitfields {
515            let width = match &f.spec.width {
516                ConstExpr::Literal(l) if l.kind == LiteralKind::Integer => {
517                    l.raw.trim().parse::<u32>().unwrap_or(0)
518                }
519                _ => 0,
520            };
521            if let Some(name) = &f.name {
522                let mask: u64 = if width >= 64 {
523                    u64::MAX
524                } else {
525                    (1u64 << width) - 1
526                };
527                let _ = writeln!(
528                    self.out,
529                    "enum {{ {c_name}_{field}_SHIFT = {offset} }};",
530                    field = name.text
531                );
532                let _ = writeln!(
533                    self.out,
534                    "#define {c_name}_{field}_MASK 0x{mask:X}u",
535                    field = name.text
536                );
537            }
538            offset = offset.saturating_add(width);
539        }
540        let _ = writeln!(self.out);
541    }
542
543    /// Emit one IDL union as a C tagged union `typedef struct { D _d; union
544    /// { ... } _u; }` so struct members can embed it by value and the codec can
545    /// switch on `_d` (#43, union). Called from the aggregate-typedef pre-pass.
546    fn emit_union_typedef(&mut self, u: &UnionDef, scope: &[String]) -> Result<(), CppGenError> {
547        let c_name = c_identifier(scope, &u.name.text);
548        let disc_ts = switch_type_spec(&u.switch_type);
549        let disc_c = c_type_for(self.reg, &disc_ts)?;
550        let _ = writeln!(self.out, "typedef struct {c_name}_s {{");
551        let _ = writeln!(self.out, "    {disc_c} _d;");
552        let _ = writeln!(self.out, "    union {{");
553        for case in &u.cases {
554            let field = union_case_field(case);
555            let c_type = c_type_for(self.reg, &case.element.type_spec)?;
556            let dims =
557                effective_array_dims(self.reg, &case.element.type_spec, &case.element.declarator)?;
558            if dims.is_empty() {
559                let _ = writeln!(self.out, "        {c_type} {field};");
560            } else {
561                let suffix: String = dims.iter().map(|n| format!("[{n}]")).collect();
562                let _ = writeln!(self.out, "        {c_type} {field}{suffix};");
563            }
564        }
565        let _ = writeln!(self.out, "    }} _u;");
566        let _ = writeln!(self.out, "}} {c_name}_t;");
567        let _ = writeln!(self.out);
568        Ok(())
569    }
570
571    /// Emit encode/decode/free/typesupport for a TOP-LEVEL union (so it can be
572    /// a topic type). XCDR2 unions are appendable by default → DHEADER wrap.
573    fn emit_union_typesupport(
574        &mut self,
575        u: &UnionDef,
576        scope: &[String],
577    ) -> Result<(), CppGenError> {
578        let c_name = c_identifier(scope, &u.name.text);
579        let dds_name = dds_type_name(scope, &u.name.text);
580        let ext = extensibility_of(&u.annotations);
581
582        let _ = writeln!(
583            self.out,
584            "static int {c_name}_encode(const void* sample, uint8_t* out_buf, size_t out_cap, size_t* out_len);"
585        );
586        let _ = writeln!(
587            self.out,
588            "static int {c_name}_decode(const uint8_t* buf, size_t len, void* out_sample);\nstatic int {c_name}_decode_e(const uint8_t* buf, size_t len, void* out_sample, int zd_be);"
589        );
590        let _ = writeln!(
591            self.out,
592            "static int {c_name}_decode_repr(const uint8_t* buf, size_t len, uint8_t representation, void* out_sample);"
593        );
594        let _ = writeln!(self.out, "static void {c_name}_sample_free(void* sample);");
595        let _ = writeln!(self.out);
596        let _ = writeln!(
597            self.out,
598            "static const char {c_name}_type_name[] = \"{dds_name}\";"
599        );
600        let _ = writeln!(
601            self.out,
602            "static const zerodds_typesupport_t {c_name}_typesupport = {{"
603        );
604        let _ = writeln!(self.out, "    .type_hash = {{0}},");
605        let _ = writeln!(self.out, "    .type_name = {c_name}_type_name,");
606        let _ = writeln!(self.out, "    .is_keyed = 0,");
607        let _ = writeln!(self.out, "    .extensibility = {},", ext.as_u8());
608        let _ = writeln!(self.out, "    ._reserved = {{0}},");
609        let _ = writeln!(self.out, "    .encode = {c_name}_encode,");
610        let _ = writeln!(self.out, "    .decode = {c_name}_decode,");
611        let _ = writeln!(self.out, "    .key_hash = NULL,");
612        let _ = writeln!(self.out, "    .sample_free = {c_name}_sample_free,");
613        let _ = writeln!(self.out, "    .decode_repr = {c_name}_decode_repr,");
614        let _ = writeln!(self.out, "}};");
615        let _ = writeln!(self.out);
616
617        // ---- encode body ----
618        let _ = writeln!(
619            self.out,
620            "static int {c_name}_encode_repr(const void* sample, uint8_t* out_buf, size_t out_cap, size_t* out_len, int representation);"
621        );
622        let _ = writeln!(
623            self.out,
624            "static int {c_name}_encode(const void* sample, uint8_t* out_buf, size_t out_cap, size_t* out_len) {{ return {c_name}_encode_repr(sample, out_buf, out_cap, out_len, 1); }}"
625        );
626        let _ = writeln!(
627            self.out,
628            "static int {c_name}_encode_repr(const void* sample, uint8_t* out_buf, size_t out_cap, size_t* out_len, int representation) {{"
629        );
630        let _ = writeln!(
631            self.out,
632            "    const {c_name}_t* s = (const {c_name}_t*)sample;"
633        );
634        let _ = writeln!(self.out, "    (void)s;");
635        let _ = writeln!(
636            self.out,
637            "    size_t zd_ma = representation ? 4 : 8; (void)zd_ma;"
638        );
639        let _ = writeln!(self.out, "    uint8_t* w_buf = NULL;");
640        let _ = writeln!(self.out, "    size_t w_len = 0;");
641        let _ = writeln!(self.out, "    size_t w_cap = 0;");
642        let _ = writeln!(
643            self.out,
644            "    if (out_buf == NULL && out_cap > 0) goto fail;"
645        );
646        // The union's own DHEADER (appendable/mutable) is emitted by emit_union_write,
647        // so it is identical whether the union is top-level or a nested element.
648        let _ = ext;
649        self.emit_union_write("(*s)", u)?;
650        let _ = writeln!(self.out, "    if (out_len) *out_len = w_len;");
651        let _ = writeln!(
652            self.out,
653            "    if (out_buf == NULL || out_cap < w_len) {{ free(w_buf); return -13; }}"
654        );
655        let _ = writeln!(
656            self.out,
657            "    if (w_len > 0) memcpy(out_buf, w_buf, w_len);"
658        );
659        let _ = writeln!(self.out, "    free(w_buf);");
660        let _ = writeln!(self.out, "    return 0;");
661        let _ = writeln!(self.out, "fail:");
662        let _ = writeln!(self.out, "    free(w_buf);");
663        let _ = writeln!(self.out, "    return -1;");
664        let _ = writeln!(self.out, "}}");
665        let _ = writeln!(self.out);
666
667        // ---- decode body ----
668        let _ = writeln!(
669            self.out,
670            "static int {c_name}_decode_core(const uint8_t* buf, size_t len, void* out_sample, int zd_be, int representation);"
671        );
672        let _ = writeln!(
673            self.out,
674            "static int {c_name}_decode(const uint8_t* buf, size_t len, void* out_sample) {{ return {c_name}_decode_core(buf, len, out_sample, 0, 1); }}"
675        );
676        let _ = writeln!(
677            self.out,
678            "static int {c_name}_decode_e(const uint8_t* buf, size_t len, void* out_sample, int zd_be) {{ return {c_name}_decode_core(buf, len, out_sample, zd_be, 1); }}"
679        );
680        let _ = writeln!(
681            self.out,
682            "static int {c_name}_decode_repr(const uint8_t* buf, size_t len, uint8_t representation, void* out_sample) {{ return {c_name}_decode_core(buf, len, out_sample, 0, representation ? 1 : 0); }}"
683        );
684        let _ = writeln!(
685            self.out,
686            "static int {c_name}_decode_core(const uint8_t* buf, size_t len, void* out_sample, int zd_be, int representation) {{"
687        );
688        let _ = writeln!(self.out, "    {c_name}_t* s = ({c_name}_t*)out_sample;");
689        let _ = writeln!(self.out, "    size_t pos = 0;");
690        let _ = writeln!(
691            self.out,
692            "    size_t zd_ma = representation ? 4 : 8; (void)zd_ma;"
693        );
694        // The union DHEADER (appendable/mutable) is read by emit_union_read itself.
695        self.emit_union_read("(*s)", u)?;
696        let _ = writeln!(self.out, "    (void)s;");
697        let _ = writeln!(self.out, "    return 0;");
698        let _ = writeln!(self.out, "}}");
699        let _ = writeln!(self.out);
700
701        // ---- free body (free heap-owning union arms) ----
702        let _ = writeln!(
703            self.out,
704            "static void {c_name}_sample_free(void* sample) {{"
705        );
706        let _ = writeln!(self.out, "    if (sample == NULL) return;");
707        let _ = writeln!(self.out, "    {c_name}_t* s = ({c_name}_t*)sample;");
708        let _ = writeln!(self.out, "    (void)s;");
709        let _ = writeln!(self.out, "}}");
710        let _ = writeln!(self.out);
711        Ok(())
712    }
713
714    fn emit_preamble(&mut self) {
715        let guard = self
716            .opts
717            .include_guard
718            .clone()
719            .unwrap_or_else(|| "ZERODDS_GENERATED_H".to_string());
720        if let Some(h) = &self.opts.file_header {
721            for line in h.lines() {
722                let _ = writeln!(self.out, "/* {line} */");
723            }
724        } else {
725            let _ = writeln!(
726                self.out,
727                "/* Generated by zerodds idl-cpp c_mode. Do not edit. */"
728            );
729        }
730        let _ = writeln!(self.out, "#ifndef {guard}");
731        let _ = writeln!(self.out, "#define {guard}");
732        let _ = writeln!(self.out, "#include <stddef.h>");
733        let _ = writeln!(self.out, "#include <stdint.h>");
734        let _ = writeln!(self.out, "#include <string.h>");
735        let _ = writeln!(self.out, "#include <stdlib.h>");
736        // Bug C2: only `zerodds_xcdr2.h` is needed — it declares
737        // `zerodds_typesupport_t` and every `zerodds_xcdr2_c_*` helper the
738        // generated body calls. The cbindgen-generated `zerodds.h` redeclares
739        // ~6 typed-FFI functions (`zerodds_topic_create_typed`, …) with names
740        // that conflict with the handwritten prototypes in `zerodds_xcdr2.h`
741        // (`struct zerodds_ZeroDdsRuntime*` vs `struct zerodds_ZeroDdsRuntime *`
742        // plus other signature drift), so including both broke
743        // `gcc -fsyntax-only <gen>.h`. The generated header uses none of the
744        // `zerodds.h`-only symbols, so it is dropped.
745        let _ = writeln!(self.out, "#include \"zerodds_xcdr2.h\"");
746        let _ = writeln!(self.out, "#ifdef __cplusplus");
747        let _ = writeln!(self.out, "extern \"C\" {{");
748        let _ = writeln!(self.out, "#endif");
749        let _ = writeln!(self.out);
750        // XCDR2 8-byte primitives align to MAXALIGN = min(sizeof, 4) = 4, never
751        // 8 (OMG DDS-XTypes 1.3 §7.4.1.1.1 / §7.4.3.2.3 INIT MAXALIGN(2)=4 —
752        // matches the cross-vendor `zerodds-cdr` core, crates/cdr). The shared
753        // `zerodds_xcdr2.h` `write_u64`/`read_u64` helpers pad to 8 (classic CDR1
754        // / XCDR1 semantics), so the XCDR2 codec uses these locally-emitted
755        // align-4 variants for u64/i64/f64 instead.
756        let _ = writeln!(self.out, "#ifndef ZERODDS_X2_ALIGN4_8BYTE");
757        let _ = writeln!(self.out, "#define ZERODDS_X2_ALIGN4_8BYTE");
758        let _ = writeln!(
759            self.out,
760            "static inline int zd_x2_write_u64(uint8_t** buf, size_t* len, size_t* cap, uint64_t v) {{"
761        );
762        let _ = writeln!(
763            self.out,
764            "    if (zerodds_xcdr2_c_pad_to(buf, len, cap, 4) != 0) return -1;"
765        );
766        let _ = writeln!(
767            self.out,
768            "    if (zerodds_xcdr2_c_grow(buf, cap, *len + 8) != 0) return -1;"
769        );
770        let _ = writeln!(
771            self.out,
772            "    for (int i = 0; i < 8; ++i) {{ (*buf)[(*len)++] = (uint8_t)((v >> (8 * i)) & 0xFFu); }}"
773        );
774        let _ = writeln!(self.out, "    return 0;");
775        let _ = writeln!(self.out, "}}");
776        let _ = writeln!(
777            self.out,
778            "static inline int zd_x2_write_i64(uint8_t** buf, size_t* len, size_t* cap, int64_t v) {{ return zd_x2_write_u64(buf, len, cap, (uint64_t)v); }}"
779        );
780        let _ = writeln!(
781            self.out,
782            "static inline int zd_x2_write_f64(uint8_t** buf, size_t* len, size_t* cap, double v) {{ uint64_t u; memcpy(&u, &v, sizeof(u)); return zd_x2_write_u64(buf, len, cap, u); }}"
783        );
784        let _ = writeln!(
785            self.out,
786            "static inline int zd_x2_read_u64(const uint8_t* buf, size_t len, size_t* pos, uint64_t* out, int big_endian) {{"
787        );
788        let _ = writeln!(
789            self.out,
790            "    if (zerodds_xcdr2_c_pad_read(buf, len, pos, 4) != 0) return -1;"
791        );
792        let _ = writeln!(self.out, "    if (*pos + 8 > len) return -1;");
793        let _ = writeln!(self.out, "    uint64_t v = 0;");
794        let _ = writeln!(
795            self.out,
796            "    for (int i = 0; i < 8; ++i) {{ int sh = big_endian ? (8 * (7 - i)) : (8 * i); v |= (uint64_t)buf[*pos + (size_t)i] << sh; }}"
797        );
798        let _ = writeln!(self.out, "    *pos += 8; *out = v; return 0;");
799        let _ = writeln!(self.out, "}}");
800        let _ = writeln!(
801            self.out,
802            "static inline int zd_x2_read_i64(const uint8_t* buf, size_t len, size_t* pos, int64_t* out, int big_endian) {{ uint64_t u; int rc = zd_x2_read_u64(buf, len, pos, &u, big_endian); if (rc != 0) return rc; *out = (int64_t)u; return 0; }}"
803        );
804        let _ = writeln!(
805            self.out,
806            "static inline int zd_x2_read_f64(const uint8_t* buf, size_t len, size_t* pos, double* out, int big_endian) {{ uint64_t u; int rc = zd_x2_read_u64(buf, len, pos, &u, big_endian); if (rc != 0) return rc; memcpy(out, &u, sizeof(*out)); return 0; }}"
807        );
808        let _ = writeln!(self.out, "#endif");
809        let _ = writeln!(self.out);
810    }
811
812    /// Map a primitive write/read helper suffix to the XCDR2-align-4 prefix.
813    /// The 8-byte primitives (`u64`/`i64`/`f64`) route through the locally
814    /// emitted `zd_x2_*` helpers (align 4, §7.4.1.1.1); everything else uses the
815    /// shared `zerodds_xcdr2_c_*` helpers (which already pad to their own size,
816    /// capped at 4).
817    fn helper_call_prefix(helper: &str) -> &'static str {
818        match helper {
819            "u64" | "i64" | "f64" => "zd_x2_",
820            _ => "zerodds_xcdr2_c_",
821        }
822    }
823
824    /// If `name` is a bitmask or bitset, return the XCDR2 primitive helper
825    /// suffix for its holder integer (`u8`/`u16`/`u32`/`u64`). Both serialize as
826    /// their unsigned holder integer (XTypes §7.3.1.2).
827    fn bits_helper(&self, name: &str) -> Option<&'static str> {
828        let holder = self
829            .reg
830            .bitmasks
831            .get(name)
832            .or_else(|| self.reg.bitsets.get(name))
833            .map(|(_, h)| *h)?;
834        Some(match holder {
835            "uint8_t" => "u8",
836            "uint16_t" => "u16",
837            "uint32_t" => "u32",
838            _ => "u64",
839        })
840    }
841
842    fn emit_postamble(&mut self) {
843        let _ = writeln!(self.out, "#ifdef __cplusplus");
844        let _ = writeln!(self.out, "}}");
845        let _ = writeln!(self.out, "#endif");
846        let _ = writeln!(self.out, "#endif");
847    }
848
849    fn walk_definitions(
850        &mut self,
851        defs: &[Definition],
852        scope: &[String],
853    ) -> Result<(), CppGenError> {
854        for d in defs {
855            match d {
856                Definition::Module(m) => self.walk_module(m, scope)?,
857                Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Struct(sd))) => {
858                    if let zerodds_idl::ast::StructDcl::Def(def) = sd {
859                        self.emit_struct(def, scope)?;
860                    }
861                }
862                // Enums are emitted in the pre-pass (as int32 typedefs);
863                // bitsets/bitmasks in the pre-pass (as holder-int typedefs);
864                // typedefs are resolved inline at every reference site. All are
865                // no-ops here (Bug C / #43: no longer rejected).
866                Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Enum(_)))
867                | Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Bitmask(_)))
868                | Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Bitset(_)))
869                | Definition::Type(TypeDecl::Typedef(_)) => {}
870                Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Union(ud))) => {
871                    // The tagged-union typedef is emitted in the pre-pass; a
872                    // top-level union additionally gets a TypeSupport so it can
873                    // be a topic type (#43, union).
874                    if let UnionDcl::Def(def) = ud {
875                        self.emit_union_typesupport(def, scope)?;
876                    }
877                }
878                Definition::Type(_) => {
879                    return Err(unsupported("non-struct type"));
880                }
881                // Constants/annotations occur at top level.
882                Definition::Const(_)
883                | Definition::Annotation(_)
884                | Definition::TypeId(_)
885                | Definition::TypePrefix(_)
886                | Definition::Import(_) => {
887                    // Ignore — no C output needed.
888                }
889                _ => {
890                    return Err(unsupported("non-struct definition"));
891                }
892            }
893        }
894        Ok(())
895    }
896
897    fn walk_module(&mut self, m: &ModuleDef, scope: &[String]) -> Result<(), CppGenError> {
898        let mut new_scope = scope.to_vec();
899        new_scope.push(m.name.text.clone());
900        self.walk_definitions(&m.definitions, &new_scope)
901    }
902
903    /// Emit just the `typedef struct {…} <C>_t;` for a struct. Split out so all
904    /// struct typedefs can be emitted in a pre-pass — a recursive struct's body
905    /// helper (and a struct that splices another struct) needs every aggregate's
906    /// C type already in scope (#43, recursion / forward-decl).
907    /// zerodds-lint: recursion-depth 64 (member walk; bounded by IDL nesting)
908    fn emit_struct_typedef(
909        &mut self,
910        def: &StructDef,
911        scope: &[String],
912    ) -> Result<(), CppGenError> {
913        let c_name = c_identifier(scope, &def.name.text);
914        let _ = writeln!(self.out, "typedef struct {c_name}_s {{");
915        for member in &def.members {
916            let optional = is_optional(&member.annotations);
917            for decl in &member.declarators {
918                let m_name = decl.name();
919                let c_type = c_type_for(self.reg, &member.type_spec)?;
920                // Fixed array declarator → C array suffix `[N][M]…`. A
921                // typedef-to-array alias contributes leading dims (#43).
922                let dims = effective_array_dims(self.reg, &member.type_spec, decl)?;
923                if dims.is_empty() {
924                    let _ = writeln!(self.out, "    {c_type} {field};", field = m_name.text);
925                } else {
926                    let suffix: String = dims.iter().map(|n| format!("[{n}]")).collect();
927                    let _ = writeln!(
928                        self.out,
929                        "    {c_type} {field}{suffix};",
930                        field = m_name.text
931                    );
932                }
933                // @optional: a presence companion flag (Bug C2: no longer
934                // flattened to a mandatory member).
935                if optional {
936                    let _ = writeln!(
937                        self.out,
938                        "    uint8_t {field}_present;",
939                        field = m_name.text
940                    );
941                }
942            }
943        }
944        if def.members.is_empty() {
945            // C99 forbids empty structs; dummy padding member.
946            let _ = writeln!(self.out, "    uint8_t _zerodds_empty;");
947        }
948        let _ = writeln!(self.out, "}} {c_name}_t;");
949        let _ = writeln!(self.out);
950        Ok(())
951    }
952
953    /// Emit ALL aggregate typedefs (structs + unions) in by-value dependency
954    /// order. A struct/union embedded BY VALUE (not behind a sequence/map
955    /// pointer) must have its complete C type emitted first; a recursive
956    /// self-reference is a pointer and imposes no order.
957    ///
958    /// A *by-value* cycle (e.g. `struct Node { Node n; };`) is a genuinely
959    /// infinite-size type — the only legal self-reference is heap-indirected
960    /// (through a sequence). Such a cycle is rejected cleanly here rather than
961    /// emitting a non-compilable infinite C struct (XTypes §7.4.5, #43).
962    fn emit_all_aggregate_typedefs(&mut self, defs: &[Definition]) -> Result<(), CppGenError> {
963        // Gather every aggregate with its scope, keyed by simple name.
964        let mut order: Vec<(String, Vec<String>, AggDef)> = Vec::new();
965        collect_aggregates(defs, &[], &mut order);
966        // Reject infinite-size by-value self/mutual recursion.
967        for (name, _, agg) in &order {
968            let mut seen = BTreeSet::new();
969            if self.by_value_reaches(agg, name, &mut seen) {
970                return Err(CppGenError::UnsupportedConstruct {
971                    construct: "infinite-size type: a struct/union member references its own \
972                                type by value (a self-reference is only legal heap-indirected, \
973                                e.g. through a sequence)"
974                        .to_string(),
975                    context: Some(name.clone()),
976                });
977            }
978        }
979        // Topological sort on the by-value dependency edges.
980        let mut emitted: BTreeSet<String> = BTreeSet::new();
981        let mut remaining: Vec<(String, Vec<String>, AggDef)> = order;
982        // Bounded fixpoint: each pass emits at least one node (acyclic by-value
983        // graph), so at most N passes.
984        let max_passes = remaining.len() + 1;
985        for _ in 0..max_passes {
986            if remaining.is_empty() {
987                break;
988            }
989            let mut progressed = false;
990            let mut next: Vec<(String, Vec<String>, AggDef)> = Vec::new();
991            for (name, scope, agg) in remaining.drain(..) {
992                let deps = self.by_value_agg_deps(&agg);
993                if deps.iter().all(|d| emitted.contains(d) || d == &name) {
994                    match &agg {
995                        AggDef::Struct(s) => self.emit_struct_typedef(s, &scope)?,
996                        AggDef::Union(u) => self.emit_union_typedef(u, &scope)?,
997                    }
998                    emitted.insert(name);
999                    progressed = true;
1000                } else {
1001                    next.push((name, scope, agg));
1002                }
1003            }
1004            remaining = next;
1005            if !progressed {
1006                // Defensive: a residual (should not happen for valid IDL) — emit
1007                // in declaration order rather than dropping types.
1008                for (name, scope, agg) in remaining.drain(..) {
1009                    match &agg {
1010                        AggDef::Struct(s) => self.emit_struct_typedef(s, &scope)?,
1011                        AggDef::Union(u) => self.emit_union_typedef(u, &scope)?,
1012                    }
1013                    emitted.insert(name);
1014                }
1015                break;
1016            }
1017        }
1018        Ok(())
1019    }
1020
1021    /// The simple names of aggregates a struct/union embeds BY VALUE (members
1022    /// that are directly a struct/union, or a fixed array / typedef-to-aggregate
1023    /// of one). Sequence/map element refs are pointers and excluded.
1024    fn by_value_agg_deps(&self, agg: &AggDef) -> Vec<String> {
1025        let mut out = Vec::new();
1026        let push_ts = |ts: &TypeSpec, out: &mut Vec<String>| {
1027            let resolved = resolve_alias(self.reg, ts);
1028            if let TypeSpec::Scoped(sc) = &resolved {
1029                if let Some(last) = scoped_last(sc) {
1030                    if self.reg.structs.contains_key(&last) || self.reg.unions.contains_key(&last) {
1031                        out.push(last);
1032                    }
1033                }
1034            }
1035        };
1036        match agg {
1037            AggDef::Struct(s) => {
1038                for m in &s.members {
1039                    push_ts(&m.type_spec, &mut out);
1040                }
1041            }
1042            AggDef::Union(u) => {
1043                for c in &u.cases {
1044                    push_ts(&c.element.type_spec, &mut out);
1045                }
1046            }
1047        }
1048        out
1049    }
1050
1051    /// True if `agg` can reach `target` following ONLY by-value member edges —
1052    /// i.e. `target` is embedded (transitively) as a non-pointer field, which
1053    /// makes the type infinite-size. `seen` guards the walk against cycles.
1054    /// zerodds-lint: recursion-depth 256 (bounded by distinct type set)
1055    fn by_value_reaches(&self, agg: &AggDef, target: &str, seen: &mut BTreeSet<String>) -> bool {
1056        for dep in self.by_value_agg_deps(agg) {
1057            if dep == target {
1058                return true;
1059            }
1060            if !seen.insert(dep.clone()) {
1061                continue;
1062            }
1063            let next = self
1064                .reg
1065                .structs
1066                .get(&dep)
1067                .map(|(_, s)| AggDef::Struct(s.clone()))
1068                .or_else(|| {
1069                    self.reg
1070                        .unions
1071                        .get(&dep)
1072                        .map(|(_, u)| AggDef::Union(u.clone()))
1073                });
1074            if let Some(next) = next {
1075                if self.by_value_reaches(&next, target, seen) {
1076                    return true;
1077                }
1078            }
1079        }
1080        false
1081    }
1082
1083    /// Forward-declare and define a runtime body helper for every recursive
1084    /// struct/union. A recursive reference (self / mutual) is emitted as a call
1085    /// to `<C>_write_body` / `<C>_read_body`, which threads the SAME growing
1086    /// buffer (`w_buf`/`w_len`/`w_cap`) / read cursor (`pos`) by pointer — so
1087    /// recursion happens at RUNTIME, not at codegen time (Bug G, C backend).
1088    fn emit_recursive_helpers(&mut self) -> Result<(), CppGenError> {
1089        // Collect (c_name, AggDef) for recursive aggregates, in a stable order.
1090        let mut recs: Vec<(String, AggDef)> = Vec::new();
1091        for name in self.reg.recursive.clone() {
1092            if let Some((cn, s)) = self.reg.structs.get(&name).cloned() {
1093                recs.push((cn, AggDef::Struct(s)));
1094            } else if let Some((cn, u)) = self.reg.unions.get(&name).cloned() {
1095                recs.push((cn, AggDef::Union(u)));
1096            }
1097        }
1098        if recs.is_empty() {
1099            return Ok(());
1100        }
1101        // Forward declarations (so mutually recursive bodies can call each other).
1102        for (cn, _) in &recs {
1103            let _ = writeln!(
1104                self.out,
1105                "static int {cn}_write_body(const {cn}_t* v, uint8_t** w_buf_pp, size_t* w_len_pp, size_t* w_cap_pp, int representation);"
1106            );
1107            let _ = writeln!(
1108                self.out,
1109                "static int {cn}_read_body(const uint8_t* buf, size_t len, size_t* pos_pp, {cn}_t* v, int zd_be, int representation);"
1110            );
1111        }
1112        let _ = writeln!(self.out);
1113        // Bodies.
1114        for (cn, agg) in &recs {
1115            self.emit_recursive_write_body(cn, agg)?;
1116            self.emit_recursive_read_body(cn, agg)?;
1117        }
1118        Ok(())
1119    }
1120
1121    /// `<C>_write_body`: the inline write templates, but with `w_buf`/`w_len`/
1122    /// `w_cap` macro-aliased to the caller's buffer cursor (passed by pointer),
1123    /// so a nested recursive call (`&w_buf` == the same `w_buf_pp`) keeps the
1124    /// one shared output buffer. The templates' `goto fail` reaches a local
1125    /// `fail:` returning -1.
1126    fn emit_recursive_write_body(&mut self, cn: &str, agg: &AggDef) -> Result<(), CppGenError> {
1127        let _ = writeln!(
1128            self.out,
1129            "static int {cn}_write_body(const {cn}_t* v, uint8_t** w_buf_pp, size_t* w_len_pp, size_t* w_cap_pp, int representation) {{"
1130        );
1131        let _ = writeln!(self.out, "#define w_buf (*w_buf_pp)");
1132        let _ = writeln!(self.out, "#define w_len (*w_len_pp)");
1133        let _ = writeln!(self.out, "#define w_cap (*w_cap_pp)");
1134        let _ = writeln!(self.out, "    const {cn}_t* s = v; (void)s;");
1135        let _ = writeln!(
1136            self.out,
1137            "    size_t zd_ma = representation ? 4 : 8; (void)zd_ma;"
1138        );
1139        match agg {
1140            AggDef::Struct(s) => self.emit_struct_body_writes(s)?,
1141            AggDef::Union(u) => self.emit_union_write("(*s)", u)?,
1142        }
1143        let _ = writeln!(self.out, "    return 0;");
1144        let _ = writeln!(self.out, "fail:");
1145        let _ = writeln!(self.out, "    return -1;");
1146        let _ = writeln!(self.out, "#undef w_buf");
1147        let _ = writeln!(self.out, "#undef w_len");
1148        let _ = writeln!(self.out, "#undef w_cap");
1149        let _ = writeln!(self.out, "}}");
1150        let _ = writeln!(self.out);
1151        Ok(())
1152    }
1153
1154    /// `<C>_read_body`: the inline read templates with `pos` macro-aliased to the
1155    /// caller's read cursor (passed by pointer); templates' `return -7` is the
1156    /// error path.
1157    fn emit_recursive_read_body(&mut self, cn: &str, agg: &AggDef) -> Result<(), CppGenError> {
1158        let _ = writeln!(
1159            self.out,
1160            "static int {cn}_read_body(const uint8_t* buf, size_t len, size_t* pos_pp, {cn}_t* v, int zd_be, int representation) {{"
1161        );
1162        let _ = writeln!(self.out, "#define pos (*pos_pp)");
1163        let _ = writeln!(self.out, "    {cn}_t* s = v; (void)s;");
1164        let _ = writeln!(
1165            self.out,
1166            "    size_t zd_ma = representation ? 4 : 8; (void)zd_ma;"
1167        );
1168        match agg {
1169            AggDef::Struct(s) => self.emit_struct_body_reads(s)?,
1170            AggDef::Union(u) => self.emit_union_read("(*s)", u)?,
1171        }
1172        let _ = writeln!(self.out, "    return 0;");
1173        let _ = writeln!(self.out, "#undef pos");
1174        let _ = writeln!(self.out, "}}");
1175        let _ = writeln!(self.out);
1176        Ok(())
1177    }
1178
1179    fn emit_struct(&mut self, def: &StructDef, scope: &[String]) -> Result<(), CppGenError> {
1180        let ext = extensibility_of(&def.annotations);
1181        let c_name = c_identifier(scope, &def.name.text);
1182        let dds_name = dds_type_name(scope, &def.name.text);
1183
1184        // ---- typedef struct ---- emitted in the aggregate-typedef pre-pass
1185        // (`emit_all_aggregate_typedefs`) so by-value embeds and recursive body
1186        // helpers see complete C types; here we emit only the codec.
1187
1188        // ---- encode/decode/free/key_hash declarations ----
1189        let _ = writeln!(
1190            self.out,
1191            "static int {c_name}_encode(const void* sample, uint8_t* out_buf, size_t out_cap, size_t* out_len);"
1192        );
1193        let _ = writeln!(
1194            self.out,
1195            "static int {c_name}_decode(const uint8_t* buf, size_t len, void* out_sample);\nstatic int {c_name}_decode_e(const uint8_t* buf, size_t len, void* out_sample, int zd_be);"
1196        );
1197        let _ = writeln!(
1198            self.out,
1199            "static int {c_name}_decode_repr(const uint8_t* buf, size_t len, uint8_t representation, void* out_sample);"
1200        );
1201        let _ = writeln!(self.out, "static void {c_name}_sample_free(void* sample);");
1202        let has_key = struct_has_key(def);
1203        if has_key {
1204            let _ = writeln!(
1205                self.out,
1206                "static int {c_name}_key_hash(const void* sample, uint8_t out_hash[16]);"
1207            );
1208        }
1209        let _ = writeln!(self.out);
1210
1211        // ---- typesupport static const ----
1212        let _ = writeln!(
1213            self.out,
1214            "static const char {c_name}_type_name[] = \"{dds_name}\";"
1215        );
1216        let _ = writeln!(
1217            self.out,
1218            "static const zerodds_typesupport_t {c_name}_typesupport = {{"
1219        );
1220        let _ = writeln!(self.out, "    .type_hash = {{0}},");
1221        let _ = writeln!(self.out, "    .type_name = {c_name}_type_name,");
1222        let _ = writeln!(self.out, "    .is_keyed = {},", if has_key { 1 } else { 0 });
1223        let _ = writeln!(self.out, "    .extensibility = {},", ext.as_u8());
1224        let _ = writeln!(self.out, "    ._reserved = {{0}},");
1225        let _ = writeln!(self.out, "    .encode = {c_name}_encode,");
1226        let _ = writeln!(self.out, "    .decode = {c_name}_decode,");
1227        if has_key {
1228            let _ = writeln!(self.out, "    .key_hash = {c_name}_key_hash,");
1229        } else {
1230            let _ = writeln!(self.out, "    .key_hash = NULL,");
1231        }
1232        let _ = writeln!(self.out, "    .sample_free = {c_name}_sample_free,");
1233        let _ = writeln!(self.out, "    .decode_repr = {c_name}_decode_repr,");
1234        let _ = writeln!(self.out, "}};");
1235        let _ = writeln!(self.out);
1236
1237        // ---- encode body ----
1238        self.emit_encode_body(&c_name, def, ext)?;
1239        // ---- decode body ----
1240        self.emit_decode_body(&c_name, def, ext)?;
1241        // ---- free body ----
1242        self.emit_free_body(&c_name, def);
1243        // ---- key_hash body ----
1244        if has_key {
1245            self.emit_key_hash_body(&c_name, def);
1246        }
1247        Ok(())
1248    }
1249
1250    fn emit_encode_body(
1251        &mut self,
1252        c_name: &str,
1253        def: &StructDef,
1254        ext: Extensibility,
1255    ) -> Result<(), CppGenError> {
1256        // `_encode` keeps the XCDR2 default ABI (typesupport .encode pointer);
1257        // `_encode_repr` is the representation-aware body. `representation`:
1258        // 0=XCDR1 (classic CDR, max_align 8, no DHEADER, @mutable=PL_CDR1),
1259        // 1=XCDR2 (DHEADER + EMHEADER). c_mode is little-endian only.
1260        let _ = writeln!(
1261            self.out,
1262            "static int {c_name}_encode_repr(const void* sample, uint8_t* out_buf, size_t out_cap, size_t* out_len, int representation);"
1263        );
1264        let _ = writeln!(
1265            self.out,
1266            "static int {c_name}_encode(const void* sample, uint8_t* out_buf, size_t out_cap, size_t* out_len) {{ return {c_name}_encode_repr(sample, out_buf, out_cap, out_len, 1); }}"
1267        );
1268        let _ = writeln!(
1269            self.out,
1270            "static int {c_name}_encode_repr(const void* sample, uint8_t* out_buf, size_t out_cap, size_t* out_len, int representation) {{"
1271        );
1272        let _ = writeln!(
1273            self.out,
1274            "    const {c_name}_t* s = (const {c_name}_t*)sample;"
1275        );
1276        let _ = writeln!(self.out, "    (void)s;");
1277        // XCDR1 caps 8-byte primitives at MAXALIGN 8, XCDR2 at 4 (§7.4.1.1.1).
1278        let _ = writeln!(
1279            self.out,
1280            "    size_t zd_ma = representation ? 4 : 8; (void)zd_ma;"
1281        );
1282        let _ = writeln!(self.out, "    /* Two-pass: grow the buffer, then copy. */");
1283        let _ = writeln!(self.out, "    uint8_t* w_buf = NULL;");
1284        let _ = writeln!(self.out, "    size_t w_len = 0;");
1285        let _ = writeln!(self.out, "    size_t w_cap = 0;");
1286        let _ = writeln!(
1287            self.out,
1288            "    if (out_buf == NULL && out_cap > 0) goto fail;"
1289        );
1290        match ext {
1291            Extensibility::Final => {
1292                self.emit_struct_body_writes(def)?;
1293            }
1294            Extensibility::Appendable => {
1295                // XCDR2: DHEADER reserved, body-writes, then length patch.
1296                // XCDR1: NO DHEADER — body starts at offset 0 (max_align 8).
1297                let _ = writeln!(self.out, "    size_t dheader_pos = 0; (void)dheader_pos;");
1298                let _ = writeln!(self.out, "    if (representation) {{");
1299                let _ = writeln!(
1300                    self.out,
1301                    "        if (zerodds_xcdr2_c_grow(&w_buf, &w_cap, w_len + 4) != 0) goto fail;"
1302                );
1303                let _ = writeln!(self.out, "        dheader_pos = w_len; w_len += 4;");
1304                let _ = writeln!(self.out, "    }}");
1305                let _ = writeln!(self.out, "    size_t body_start = w_len;");
1306                self.emit_struct_body_writes(def)?;
1307                let _ = writeln!(self.out, "    if (representation) {{");
1308                let _ = writeln!(
1309                    self.out,
1310                    "        zerodds_xcdr2_c_put_u32_at(w_buf, dheader_pos, (uint32_t)(w_len - body_start));"
1311                );
1312                let _ = writeln!(self.out, "    }}");
1313            }
1314            Extensibility::Mutable => {
1315                // XCDR2: DHEADER + EMHEADER per member. XCDR1: PL_CDR1 parameter
1316                // list (no DHEADER), terminated by the PID_LIST_END sentinel.
1317                let _ = writeln!(self.out, "    if (representation) {{");
1318                let _ = writeln!(
1319                    self.out,
1320                    "        if (zerodds_xcdr2_c_grow(&w_buf, &w_cap, w_len + 4) != 0) goto fail;"
1321                );
1322                let _ = writeln!(self.out, "        size_t dheader_pos = w_len; w_len += 4;");
1323                let _ = writeln!(self.out, "        size_t mut_body_start = w_len;");
1324                self.emit_mutable_member_writes(def)?;
1325                let _ = writeln!(
1326                    self.out,
1327                    "        zerodds_xcdr2_c_put_u32_at(w_buf, dheader_pos, (uint32_t)(w_len - mut_body_start));"
1328                );
1329                let _ = writeln!(self.out, "    }} else {{");
1330                self.emit_pl_cdr1_member_writes(def)?;
1331                let _ = writeln!(self.out, "    }}");
1332            }
1333        }
1334        // Copy the output.
1335        let _ = writeln!(self.out, "    if (out_len) *out_len = w_len;");
1336        let _ = writeln!(
1337            self.out,
1338            "    if (out_buf == NULL || out_cap < w_len) {{ free(w_buf); return -13; }}"
1339        );
1340        let _ = writeln!(
1341            self.out,
1342            "    if (w_len > 0) memcpy(out_buf, w_buf, w_len);"
1343        );
1344        let _ = writeln!(self.out, "    free(w_buf);");
1345        let _ = writeln!(self.out, "    return 0;");
1346        let _ = writeln!(self.out, "fail:");
1347        let _ = writeln!(self.out, "    free(w_buf);");
1348        let _ = writeln!(self.out, "    return -1;");
1349        let _ = writeln!(self.out, "}}");
1350        let _ = writeln!(self.out);
1351        Ok(())
1352    }
1353
1354    fn emit_struct_body_writes(&mut self, def: &StructDef) -> Result<(), CppGenError> {
1355        for member in &def.members {
1356            let optional = is_optional(&member.annotations);
1357            for decl in &member.declarators {
1358                let f = &decl.name().text;
1359                let dims = effective_array_dims(self.reg, &member.type_spec, decl)?;
1360                if optional {
1361                    // XCDR2 non-mutable optional: a boolean presence flag, then
1362                    // the value only if present (XTypes §7.4.3 optional members).
1363                    let _ = writeln!(
1364                        self.out,
1365                        "    if (zerodds_xcdr2_c_write_u8(&w_buf, &w_len, &w_cap, s->{f}_present ? 1 : 0) != 0) goto fail;"
1366                    );
1367                    let _ = writeln!(self.out, "    if (s->{f}_present) {{");
1368                    self.emit_array_or_scalar_write(&format!("s->{f}"), &member.type_spec, &dims)?;
1369                    let _ = writeln!(self.out, "    }}");
1370                } else {
1371                    self.emit_array_or_scalar_write(&format!("s->{f}"), &member.type_spec, &dims)?;
1372                }
1373            }
1374        }
1375        Ok(())
1376    }
1377
1378    /// Write a member that may be a fixed array (N nested loops over the C array)
1379    /// or a scalar/aggregate. Fixed arrays carry no length prefix (XTypes §7.4.3).
1380    fn emit_array_or_scalar_write(
1381        &mut self,
1382        var: &str,
1383        type_spec: &TypeSpec,
1384        dims: &[u64],
1385    ) -> Result<(), CppGenError> {
1386        if dims.is_empty() {
1387            return self.emit_member_write(var, type_spec);
1388        }
1389        // Bug XV-arr: a fixed array gets ONE collection DHEADER only when its
1390        // ELEMENT type is NON-primitive (1-D array of struct/string/sequence). An
1391        // array of a PRIMITIVE element is a PARRAY (XTypes 1.3 §7.4.3.5 rule 8) —
1392        // PLAIN-collection regardless of dimensionality, so it carries NO DHEADER
1393        // even when multi-dimensional (`long grid[2][3]`). Byte-identical to the
1394        // corrected rust golden (grid: 6×i32 tight, NO DHEADER; shape: DHEADER=16).
1395        let needs_dheader = !self.seq_elem_is_primitive(type_spec);
1396        if needs_dheader {
1397            // Own brace scope so two array members in one struct do not collide
1398            // on the `arr_dheader_pos`/`arr_body_start` locals. The collection
1399            // DHEADER exists only under XCDR2 — XCDR1 (classic CDR) has none.
1400            let _ = writeln!(self.out, "    {{");
1401            let _ = writeln!(
1402                self.out,
1403                "    size_t arr_dheader_pos = 0; (void)arr_dheader_pos;"
1404            );
1405            let _ = writeln!(self.out, "    if (representation) {{");
1406            let _ = writeln!(
1407                self.out,
1408                "        if (zerodds_xcdr2_c_grow(&w_buf, &w_cap, w_len + 4) != 0) goto fail;"
1409            );
1410            let _ = writeln!(self.out, "        arr_dheader_pos = w_len; w_len += 4;");
1411            let _ = writeln!(self.out, "    }}");
1412            let _ = writeln!(
1413                self.out,
1414                "    size_t arr_body_start = w_len; (void)arr_body_start;"
1415            );
1416        }
1417        let mut acc = var.to_string();
1418        for (d, _n) in dims.iter().enumerate() {
1419            let iv = format!("ai{d}");
1420            let _ = writeln!(
1421                self.out,
1422                "    for (uint32_t {iv} = 0; {iv} < {n}; ++{iv}) {{",
1423                n = dims[d]
1424            );
1425            acc = format!("{acc}[{iv}]");
1426        }
1427        self.emit_member_write(&acc, type_spec)?;
1428        for _ in dims {
1429            let _ = writeln!(self.out, "    }}");
1430        }
1431        if needs_dheader {
1432            let _ = writeln!(
1433                self.out,
1434                "    if (representation) {{ zerodds_xcdr2_c_put_u32_at(w_buf, arr_dheader_pos, (uint32_t)(w_len - arr_body_start)); }}"
1435            );
1436            let _ = writeln!(self.out, "    }}"); // close the array DHEADER scope
1437        }
1438        Ok(())
1439    }
1440
1441    /// zerodds-lint: recursion-depth 64 (nested struct members; bounded by IDL)
1442    fn emit_member_write(&mut self, var: &str, type_spec: &TypeSpec) -> Result<(), CppGenError> {
1443        // Resolve typedef aliases to the effective type first (Bug C).
1444        let resolved = resolve_alias(self.reg, type_spec);
1445        match &resolved {
1446            TypeSpec::Primitive(p) => self.emit_primitive_write(var, *p),
1447            TypeSpec::String(st) => {
1448                if st.wide {
1449                    return self.emit_wstring_write(var, st.bound.as_ref());
1450                }
1451                // Bounded string<N>: enforce the bound (XTypes §7.4.13.4.2) — a
1452                // string longer than N must be rejected, not silently corrupt.
1453                if let Some(n) = self.eval_bound(st.bound.as_ref()) {
1454                    let _ = writeln!(
1455                        self.out,
1456                        "    if (({var}) != NULL && strlen({var}) > {n}u) goto fail;"
1457                    );
1458                }
1459                let _ = writeln!(
1460                    self.out,
1461                    "    if (zerodds_xcdr2_c_write_string(&w_buf, &w_len, &w_cap, {var}) != 0) goto fail;"
1462                );
1463                Ok(())
1464            }
1465            TypeSpec::Sequence(seq) => self.emit_sequence_write(var, &seq.elem, seq.bound.as_ref()),
1466            TypeSpec::Map(m) => self.emit_map_write(var, &m.key, &m.value, m.bound.as_ref()),
1467            TypeSpec::Scoped(sc) => {
1468                let last = scoped_last(sc).ok_or_else(|| unsupported("empty scoped name"))?;
1469                if self.reg.enums.contains_key(&last) {
1470                    // enum → signed wire holder of its @bit_bound width
1471                    // (XTypes §7.4.5.1). u8/u16 carry the byte image of the
1472                    // signed value; the int32 case is the spec default.
1473                    let bytes = self.reg.enum_bytes.get(&last).copied().unwrap_or(4);
1474                    let line = match bytes {
1475                        1 => format!(
1476                            "    if (zerodds_xcdr2_c_write_u8(&w_buf, &w_len, &w_cap, (uint8_t)({var})) != 0) goto fail;"
1477                        ),
1478                        2 => format!(
1479                            "    if (zerodds_xcdr2_c_write_u16(&w_buf, &w_len, &w_cap, (uint16_t)({var})) != 0) goto fail;"
1480                        ),
1481                        _ => format!(
1482                            "    if (zerodds_xcdr2_c_write_i32(&w_buf, &w_len, &w_cap, (int32_t)({var})) != 0) goto fail;"
1483                        ),
1484                    };
1485                    let _ = writeln!(self.out, "{line}");
1486                    return Ok(());
1487                }
1488                // bitmask / bitset → write the holder integer (XTypes §7.3.1.2).
1489                if let Some(helper) = self.bits_helper(&last) {
1490                    let prefix = Self::helper_call_prefix(helper);
1491                    let _ = writeln!(
1492                        self.out,
1493                        "    if ({prefix}write_{helper}(&w_buf, &w_len, &w_cap, {var}) != 0) goto fail;"
1494                    );
1495                    return Ok(());
1496                }
1497                if let Some((cn, ndef)) = self.reg.structs.get(&last).cloned() {
1498                    // A recursive struct (self-referential through a sequence,
1499                    // or mutually recursive) is spliced through its own runtime
1500                    // body helper — inlining would recurse forever at codegen
1501                    // time (XTypes §7.4.5 / Bug G). Non-recursive structs are
1502                    // inline-encoded by value (no DHEADER: @final inline form).
1503                    if self.reg.is_recursive(&last) {
1504                        // A recursive struct spliced as a member/sequence-element
1505                        // is a self-contained value. Under XCDR2 a NON-final
1506                        // (@appendable/@mutable) aggregate is DELIMITED — it
1507                        // carries its own DHEADER (XTypes §7.4.3.5 / §7.4.4.4),
1508                        // exactly as its top-level `_encode` would emit. The
1509                        // body-only `_write_body` must therefore be wrapped in a
1510                        // DHEADER here, so each `sequence<Tree>` element matches
1511                        // the rust golden (per-node DHEADER). @final splices the
1512                        // bare body (no DHEADER).
1513                        let ext = extensibility_of(&ndef.annotations);
1514                        if matches!(ext, Extensibility::Final) {
1515                            let _ = writeln!(
1516                                self.out,
1517                                "    if ({cn}_write_body(&({var}), &w_buf, &w_len, &w_cap, representation) != 0) goto fail;"
1518                            );
1519                        } else {
1520                            // Per-node DHEADER under XCDR2 only; XCDR1 splices the
1521                            // bare recursive body (no delimiter).
1522                            let _ = writeln!(self.out, "    {{");
1523                            let _ = writeln!(
1524                                self.out,
1525                                "    size_t rec_dheader_pos = 0; (void)rec_dheader_pos;"
1526                            );
1527                            let _ = writeln!(self.out, "    if (representation) {{");
1528                            let _ = writeln!(
1529                                self.out,
1530                                "        if (zerodds_xcdr2_c_grow(&w_buf, &w_cap, w_len + 4) != 0) goto fail;"
1531                            );
1532                            let _ =
1533                                writeln!(self.out, "        rec_dheader_pos = w_len; w_len += 4;");
1534                            let _ = writeln!(self.out, "    }}");
1535                            let _ = writeln!(
1536                                self.out,
1537                                "    size_t rec_body_start = w_len; (void)rec_body_start;"
1538                            );
1539                            let _ = writeln!(
1540                                self.out,
1541                                "    if ({cn}_write_body(&({var}), &w_buf, &w_len, &w_cap, representation) != 0) goto fail;"
1542                            );
1543                            let _ = writeln!(
1544                                self.out,
1545                                "    if (representation) {{ zerodds_xcdr2_c_put_u32_at(w_buf, rec_dheader_pos, (uint32_t)(w_len - rec_body_start)); }}"
1546                            );
1547                            let _ = writeln!(self.out, "    }}");
1548                        }
1549                        return Ok(());
1550                    }
1551                    return self.emit_nested_struct_write(var, &ndef);
1552                }
1553                if let Some((cn, udef)) = self.reg.unions.get(&last).cloned() {
1554                    if self.reg.is_recursive(&last) {
1555                        let _ = writeln!(
1556                            self.out,
1557                            "    if ({cn}_write_body(&({var}), &w_buf, &w_len, &w_cap, representation) != 0) goto fail;"
1558                        );
1559                        return Ok(());
1560                    }
1561                    return self.emit_union_write(var, &udef);
1562                }
1563                Err(unsupported("unresolved scoped member (C backend)"))
1564            }
1565            TypeSpec::Fixed(f) => {
1566                // fixed<P,S>: write the (P+2)/2 raw BCD octets (CORBA §9.3.2.7),
1567                // no alignment, no length prefix, endian-independent.
1568                let p = const_expr_to_u64(self.reg, &f.digits)
1569                    .ok_or_else(|| unsupported("fixed: non-constant digit count"))?;
1570                let n = (p + 2) / 2;
1571                let _ = writeln!(
1572                    self.out,
1573                    "    for (size_t __fi = 0; __fi < {n}; __fi++) {{ if (zerodds_xcdr2_c_write_u8(&w_buf, &w_len, &w_cap, (uint8_t)(({var}).bcd[__fi])) != 0) goto fail; }}"
1574                );
1575                Ok(())
1576            }
1577            TypeSpec::Any => Err(unsupported("any")),
1578        }
1579    }
1580
1581    /// XCDR2 wstring (§7.4.4.6): uint32 byte-length (= 2*code-units), then the
1582    /// UTF-16-LE code units, no NUL. C side holds a NUL-terminated `uint16_t*`.
1583    fn emit_wstring_write(
1584        &mut self,
1585        var: &str,
1586        bound: Option<&ConstExpr>,
1587    ) -> Result<(), CppGenError> {
1588        let _ = writeln!(self.out, "    {{");
1589        let _ = writeln!(
1590            self.out,
1591            "        const uint16_t* ws_p = (const uint16_t*)({var});"
1592        );
1593        let _ = writeln!(self.out, "        uint32_t ws_n = 0;");
1594        let _ = writeln!(self.out, "        if (ws_p) while (ws_p[ws_n]) ++ws_n;");
1595        // Bounded wstring<N>: reject a string longer than N code units.
1596        if let Some(n) = self.eval_bound(bound) {
1597            let _ = writeln!(self.out, "        if (ws_n > {n}u) goto fail;");
1598        }
1599        let _ = writeln!(
1600            self.out,
1601            "        if (zerodds_xcdr2_c_write_u32(&w_buf, &w_len, &w_cap, ws_n * 2u) != 0) goto fail;"
1602        );
1603        let _ = writeln!(
1604            self.out,
1605            "        for (uint32_t wi = 0; wi < ws_n; ++wi) {{ if (zerodds_xcdr2_c_write_u16(&w_buf, &w_len, &w_cap, ws_p[wi]) != 0) goto fail; }}"
1606        );
1607        let _ = writeln!(self.out, "    }}");
1608        Ok(())
1609    }
1610
1611    /// map<K,V> (§7.4.4.7): XCDR2 serializes a map as a sequence of (K,V)
1612    /// pairs — DHEADER (byte-length) + uint32 entry-count, then each entry's
1613    /// key followed by its value. C side: parallel `keys[]`/`vals[]` arrays.
1614    fn emit_map_write(
1615        &mut self,
1616        var: &str,
1617        key: &TypeSpec,
1618        value: &TypeSpec,
1619        bound: Option<&ConstExpr>,
1620    ) -> Result<(), CppGenError> {
1621        let _ = writeln!(self.out, "    {{");
1622        // Bounded map<K,V,N>: reject more than N entries.
1623        if let Some(n) = self.eval_bound(bound) {
1624            let _ = writeln!(self.out, "    if (({var}).len > {n}u) goto fail;");
1625        }
1626        // The map DHEADER is a uint32 -> 4-align before writing it (a map after a
1627        // sub-4-byte member, e.g. a 2-byte @bit_bound enum, would otherwise land
1628        // the DHEADER unaligned). Matches the rust reference + XCDR2 §7.4.1.
1629        let _ = writeln!(
1630            self.out,
1631            "    if (zerodds_xcdr2_c_pad_to(&w_buf, &w_len, &w_cap, 4) != 0) goto fail;"
1632        );
1633        // XCDR2 §7.4.3.5: a map carries a DHEADER only when its (key,value) element
1634        // is NON-primitive. `map<long,long>` (both primitive) omits it — matching
1635        // cdr-core `needs_collection_dheader(.., K::IS_PRIMITIVE && V::IS_PRIMITIVE)`
1636        // and FastDDS/OpenDDS. (Same rule as the primitive-array PARRAY above.)
1637        let map_dh = !(self.seq_elem_is_primitive(key) && self.seq_elem_is_primitive(value));
1638        let _ = writeln!(
1639            self.out,
1640            "    size_t map_dheader_pos = 0; (void)map_dheader_pos;"
1641        );
1642        if map_dh {
1643            let _ = writeln!(self.out, "    if (representation) {{");
1644            let _ = writeln!(
1645                self.out,
1646                "        if (zerodds_xcdr2_c_grow(&w_buf, &w_cap, w_len + 4) != 0) goto fail;"
1647            );
1648            let _ = writeln!(self.out, "        map_dheader_pos = w_len; w_len += 4;");
1649            let _ = writeln!(self.out, "    }}");
1650        }
1651        let _ = writeln!(
1652            self.out,
1653            "    size_t map_body_start = w_len; (void)map_body_start;"
1654        );
1655        let _ = writeln!(
1656            self.out,
1657            "    if (zerodds_xcdr2_c_write_u32(&w_buf, &w_len, &w_cap, ({var}).len) != 0) goto fail;"
1658        );
1659        let mv = format!("mi{}", self.coll_depth);
1660        let _ = writeln!(
1661            self.out,
1662            "    for (uint32_t {mv} = 0; {mv} < ({var}).len; ++{mv}) {{"
1663        );
1664        self.coll_depth += 1;
1665        let rk = self.emit_member_write(&format!("({var}).keys[{mv}]"), key);
1666        let rv = if rk.is_ok() {
1667            self.emit_member_write(&format!("({var}).vals[{mv}]"), value)
1668        } else {
1669            Ok(())
1670        };
1671        self.coll_depth -= 1;
1672        rk.and(rv)?;
1673        let _ = writeln!(self.out, "    }}");
1674        if map_dh {
1675            let _ = writeln!(
1676                self.out,
1677                "    if (representation) {{ zerodds_xcdr2_c_put_u32_at(w_buf, map_dheader_pos, (uint32_t)(w_len - map_body_start)); }}"
1678            );
1679        }
1680        let _ = writeln!(self.out, "    }}");
1681        Ok(())
1682    }
1683
1684    /// Discriminated union (§7.4.4.5): write the discriminator, then the member
1685    /// selected by it. C side: a tagged union `struct { Disc _d; union {...} _u; }`.
1686    fn emit_union_write(&mut self, var: &str, udef: &UnionDef) -> Result<(), CppGenError> {
1687        // A union honours its extensibility wherever it appears (top-level OR as a
1688        // member / sequence element): @appendable/@mutable carry a 4-aligned uint32
1689        // DHEADER over [disc + branch]; @final does not. Previously only the
1690        // top-level TypeSupport wrapped the DHEADER, so a union *element* (e.g.
1691        // sequence<Sel>) was emitted without one — cross-PSM wire divergence.
1692        let u_dheader = !matches!(extensibility_of(&udef.annotations), Extensibility::Final);
1693        if u_dheader {
1694            let _ = writeln!(self.out, "    {{");
1695            let _ = writeln!(
1696                self.out,
1697                "    size_t u_dheader_pos = 0; (void)u_dheader_pos;"
1698            );
1699            let _ = writeln!(self.out, "    if (representation) {{");
1700            let _ = writeln!(
1701                self.out,
1702                "        if (zerodds_xcdr2_c_pad_to(&w_buf, &w_len, &w_cap, 4) != 0) goto fail;"
1703            );
1704            let _ = writeln!(
1705                self.out,
1706                "        if (zerodds_xcdr2_c_grow(&w_buf, &w_cap, w_len + 4) != 0) goto fail;"
1707            );
1708            let _ = writeln!(self.out, "        u_dheader_pos = w_len; w_len += 4;");
1709            let _ = writeln!(self.out, "    }}");
1710            let _ = writeln!(
1711                self.out,
1712                "    size_t u_body_start = w_len; (void)u_body_start;"
1713            );
1714        }
1715        // Discriminator.
1716        let disc_ts = switch_type_spec(&udef.switch_type);
1717        self.emit_member_write(&format!("({var})._d"), &disc_ts)?;
1718        let _ = writeln!(self.out, "    switch (({var})._d) {{");
1719        let mut default_case: Option<&Case> = None;
1720        for case in &udef.cases {
1721            let field = union_case_field(case);
1722            let mut has_value_label = false;
1723            for label in &case.labels {
1724                match label {
1725                    CaseLabel::Value(expr) => {
1726                        has_value_label = true;
1727                        let lit = self.case_label_literal(expr)?;
1728                        let _ = writeln!(self.out, "    case {lit}:");
1729                    }
1730                    CaseLabel::Default => {
1731                        default_case = Some(case);
1732                    }
1733                }
1734            }
1735            if has_value_label {
1736                self.emit_member_write(&format!("({var})._u.{field}"), &case.element.type_spec)?;
1737                let _ = writeln!(self.out, "        break;");
1738            }
1739        }
1740        if let Some(case) = default_case {
1741            let field = union_case_field(case);
1742            let _ = writeln!(self.out, "    default:");
1743            self.emit_member_write(&format!("({var})._u.{field}"), &case.element.type_spec)?;
1744            let _ = writeln!(self.out, "        break;");
1745        } else {
1746            let _ = writeln!(self.out, "    default: break;");
1747        }
1748        let _ = writeln!(self.out, "    }}");
1749        if u_dheader {
1750            let _ = writeln!(
1751                self.out,
1752                "    if (representation) {{ zerodds_xcdr2_c_put_u32_at(w_buf, u_dheader_pos, (uint32_t)(w_len - u_body_start)); }}"
1753            );
1754            let _ = writeln!(self.out, "    }}");
1755        }
1756        Ok(())
1757    }
1758
1759    /// Inline-encode a nested struct's members by value (Plain-CDR2, no DHEADER).
1760    /// zerodds-lint: recursion-depth 64 (bounded by IDL nesting)
1761    fn emit_nested_struct_write(&mut self, var: &str, ndef: &StructDef) -> Result<(), CppGenError> {
1762        // FINDING T1b: a NON-`@final` nested struct is delimited — it carries its
1763        // own leading DHEADER, exactly as its top-level `_encode` would emit
1764        // (XTypes §7.4.3.5 / §7.4.4.4). When such a struct is a @mutable member
1765        // or a sequence element, its DHEADER doubles as the EMHEADER NEXTINT
1766        // (LengthCode 5, picked by `mutable_compact_lc`). `@mutable` additionally
1767        // frames each of its own members with an EMHEADER; `@appendable` splices
1768        // its members tight after the DHEADER. A `@final` nested struct has no
1769        // DHEADER and tight-packs its body (the original inline form).
1770        let ext = extensibility_of(&ndef.annotations);
1771        if !matches!(ext, Extensibility::Final) {
1772            // XCDR2: the non-@final nested struct carries its own DHEADER.
1773            // XCDR1: no DHEADER (classic CDR) — tight-packed body. (A nested
1774            // @mutable struct under XCDR1 would need PL_CDR1 framing; the corpus
1775            // has only @appendable nested structs, which splice tight.)
1776            let _ = writeln!(self.out, "    {{");
1777            let _ = writeln!(
1778                self.out,
1779                "    size_t nst_dheader_pos = 0; (void)nst_dheader_pos;"
1780            );
1781            let _ = writeln!(self.out, "    if (representation) {{");
1782            let _ = writeln!(
1783                self.out,
1784                "        if (zerodds_xcdr2_c_grow(&w_buf, &w_cap, w_len + 4) != 0) goto fail;"
1785            );
1786            let _ = writeln!(self.out, "        nst_dheader_pos = w_len; w_len += 4;");
1787            let _ = writeln!(self.out, "    }}");
1788            let _ = writeln!(
1789                self.out,
1790                "    size_t nst_body_start = w_len; (void)nst_body_start;"
1791            );
1792            if matches!(ext, Extensibility::Mutable) {
1793                self.emit_mutable_member_writes_base(ndef, &format!("({var})."))?;
1794            } else {
1795                self.emit_nested_struct_inline_write(var, ndef)?;
1796            }
1797            let _ = writeln!(
1798                self.out,
1799                "    if (representation) {{ zerodds_xcdr2_c_put_u32_at(w_buf, nst_dheader_pos, (uint32_t)(w_len - nst_body_start)); }}"
1800            );
1801            let _ = writeln!(self.out, "    }}");
1802            return Ok(());
1803        }
1804        self.emit_nested_struct_inline_write(var, ndef)
1805    }
1806
1807    /// Tight-packed (`@final`) inline write of a nested struct's members by
1808    /// value — Plain-CDR2, no DHEADER, no per-member EMHEADER. Also serves as the
1809    /// `@appendable` body (members after the DHEADER frame).
1810    /// zerodds-lint: recursion-depth 64 (bounded by IDL nesting)
1811    fn emit_nested_struct_inline_write(
1812        &mut self,
1813        var: &str,
1814        ndef: &StructDef,
1815    ) -> Result<(), CppGenError> {
1816        for member in &ndef.members {
1817            let optional = is_optional(&member.annotations);
1818            for decl in &member.declarators {
1819                let f = &decl.name().text;
1820                let dims = effective_array_dims(self.reg, &member.type_spec, decl)?;
1821                if optional {
1822                    // Plain-CDR2 optional (XTypes §7.4.3): a boolean presence
1823                    // flag, then the value only if present. The nested struct's
1824                    // own typedef carries the `<f>_present` companion (emitted by
1825                    // emit_struct), so the field access is symmetric.
1826                    let _ = writeln!(
1827                        self.out,
1828                        "    if (zerodds_xcdr2_c_write_u8(&w_buf, &w_len, &w_cap, ({var}).{f}_present ? 1 : 0) != 0) goto fail;"
1829                    );
1830                    let _ = writeln!(self.out, "    if (({var}).{f}_present) {{");
1831                    self.emit_array_or_scalar_write(
1832                        &format!("({var}).{f}"),
1833                        &member.type_spec,
1834                        &dims,
1835                    )?;
1836                    let _ = writeln!(self.out, "    }}");
1837                } else {
1838                    self.emit_array_or_scalar_write(
1839                        &format!("({var}).{f}"),
1840                        &member.type_spec,
1841                        &dims,
1842                    )?;
1843                }
1844            }
1845        }
1846        Ok(())
1847    }
1848
1849    fn emit_primitive_write(&mut self, var: &str, p: PrimitiveType) -> Result<(), CppGenError> {
1850        let helper = match p {
1851            PrimitiveType::Boolean | PrimitiveType::Octet => "u8",
1852            PrimitiveType::Char => "u8",
1853            PrimitiveType::WideChar => "u16",
1854            PrimitiveType::Integer(IntegerType::Short)
1855            | PrimitiveType::Integer(IntegerType::Int16) => "i16",
1856            PrimitiveType::Integer(IntegerType::UShort)
1857            | PrimitiveType::Integer(IntegerType::UInt16) => "u16",
1858            PrimitiveType::Integer(IntegerType::Long)
1859            | PrimitiveType::Integer(IntegerType::Int32) => "i32",
1860            PrimitiveType::Integer(IntegerType::ULong)
1861            | PrimitiveType::Integer(IntegerType::UInt32) => "u32",
1862            PrimitiveType::Integer(IntegerType::LongLong)
1863            | PrimitiveType::Integer(IntegerType::Int64) => "i64",
1864            PrimitiveType::Integer(IntegerType::ULongLong)
1865            | PrimitiveType::Integer(IntegerType::UInt64) => "u64",
1866            PrimitiveType::Integer(IntegerType::Int8) => "i8",
1867            PrimitiveType::Integer(IntegerType::UInt8) => "u8",
1868            PrimitiveType::Floating(FloatingType::Float) => "f32",
1869            PrimitiveType::Floating(FloatingType::Double) => "f64",
1870            PrimitiveType::Floating(FloatingType::LongDouble) => {
1871                return Err(unsupported("long double"));
1872            }
1873        };
1874        if matches!(helper, "u64" | "i64" | "f64") {
1875            // 8-byte primitive: XCDR2 caps alignment at 4 (`zd_x2_*`), XCDR1 uses
1876            // the shared align-8 helper (classic CDR, MAXALIGN 8). `representation`
1877            // is in scope in `_encode_repr` / `_write_body`.
1878            let _ = writeln!(
1879                self.out,
1880                "    if (representation) {{ if (zd_x2_write_{helper}(&w_buf, &w_len, &w_cap, {var}) != 0) goto fail; }}"
1881            );
1882            let _ = writeln!(
1883                self.out,
1884                "    else {{ if (zerodds_xcdr2_c_write_{helper}(&w_buf, &w_len, &w_cap, {var}) != 0) goto fail; }}"
1885            );
1886            return Ok(());
1887        }
1888        let prefix = Self::helper_call_prefix(helper);
1889        let _ = writeln!(
1890            self.out,
1891            "    if ({prefix}write_{helper}(&w_buf, &w_len, &w_cap, {var}) != 0) goto fail;"
1892        );
1893        Ok(())
1894    }
1895
1896    fn emit_sequence_write(
1897        &mut self,
1898        var: &str,
1899        elem: &TypeSpec,
1900        bound: Option<&ConstExpr>,
1901    ) -> Result<(), CppGenError> {
1902        // Sequence-Repraesentation in C: `struct { uint32_t len; T* elems; }`.
1903        // XCDR2 §7.4.3.5: non-primitive elements (string, struct, nested
1904        // sequence, …) get a DHEADER (uint32 = byte length of
1905        // [count + elements]) prepended; primitives (incl. enum→int32) do not.
1906        // Cyclone-DDS-verified.
1907        let non_primitive = !self.seq_elem_is_primitive(elem);
1908        // Block-scopes the DHEADER locals (multiple sequences per struct).
1909        let _ = writeln!(self.out, "    {{");
1910        // Bounded sequence<T,N>: reject more than N elements (XTypes §7.4.13.4.2)
1911        // — over-bound must error, not silently corrupt the wire.
1912        if let Some(n) = self.eval_bound(bound) {
1913            let _ = writeln!(self.out, "    if (({var}).len > {n}u) goto fail;");
1914        }
1915        if non_primitive {
1916            // DHEADER only under XCDR2; XCDR1 (classic CDR) has no collection
1917            // delimiter — just the count + elements.
1918            let _ = writeln!(
1919                self.out,
1920                "    size_t seq_dheader_pos = 0; (void)seq_dheader_pos;"
1921            );
1922            let _ = writeln!(self.out, "    if (representation) {{");
1923            let _ = writeln!(
1924                self.out,
1925                "        if (zerodds_xcdr2_c_grow(&w_buf, &w_cap, w_len + 4) != 0) goto fail;"
1926            );
1927            let _ = writeln!(self.out, "        seq_dheader_pos = w_len; w_len += 4;");
1928            let _ = writeln!(self.out, "    }}");
1929            let _ = writeln!(
1930                self.out,
1931                "    size_t seq_body_start = w_len; (void)seq_body_start;"
1932            );
1933        }
1934        let _ = writeln!(
1935            self.out,
1936            "    if (zerodds_xcdr2_c_write_u32(&w_buf, &w_len, &w_cap, ({var}).len) != 0) goto fail;"
1937        );
1938        let iv = format!("i{}", self.coll_depth);
1939        let _ = writeln!(
1940            self.out,
1941            "    for (uint32_t {iv} = 0; {iv} < ({var}).len; ++{iv}) {{"
1942        );
1943        // Delegate the element body to the generic member writer so structs,
1944        // enums and nested sequences (sequence<sequence<T>>) all work (#43,
1945        // sequence<non-primitive T>). The element loop counter is depth-scoped
1946        // so a nested sequence does not shadow the outer index.
1947        self.coll_depth += 1;
1948        let r = self.emit_member_write(&format!("({var}).elems[{iv}]"), elem);
1949        self.coll_depth -= 1;
1950        r?;
1951        let _ = writeln!(self.out, "    }}");
1952        if non_primitive {
1953            let _ = writeln!(
1954                self.out,
1955                "    if (representation) {{ zerodds_xcdr2_c_put_u32_at(w_buf, seq_dheader_pos, (uint32_t)(w_len - seq_body_start)); }}"
1956            );
1957        }
1958        let _ = writeln!(self.out, "    }}");
1959        Ok(())
1960    }
1961
1962    fn emit_mutable_member_writes(&mut self, def: &StructDef) -> Result<(), CppGenError> {
1963        self.emit_mutable_member_writes_base(def, "s->")
1964    }
1965
1966    /// Emits the per-member EMHEADER-framed writes of a @mutable struct, with the
1967    /// member accessors prefixed by `base` (`"s->"` for a top-level encode, or
1968    /// `"(<var>)."` when this struct is a nested @mutable member / sequence
1969    /// element). Splits out so a nested @mutable struct reuses the SAME framing.
1970    fn emit_mutable_member_writes_base(
1971        &mut self,
1972        def: &StructDef,
1973        base: &str,
1974    ) -> Result<(), CppGenError> {
1975        // XTypes 1.3 §7.3.4.3: `@autoid` defaults to SEQUENTIAL — a @mutable member
1976        // without explicit `@id(N)` takes the next 0-based declaration-order id
1977        // (vendor-confirmed byte-identical to CycloneDDS). Explicit `@id(N)` sets
1978        // the id and resets the counter to N+1. (Previously the C backend rejected
1979        // auto-id @mutable members outright.)
1980        let mut auto_id: u32 = 0;
1981        for member in &def.members {
1982            let id = id_annotation(&member.annotations).unwrap_or(auto_id);
1983            auto_id = id + 1;
1984            let dims_per_decl: Vec<Vec<u64>> = member
1985                .declarators
1986                .iter()
1987                .map(|d| effective_array_dims(self.reg, &member.type_spec, d))
1988                .collect::<Result<_, _>>()?;
1989            let optional = is_optional(&member.annotations);
1990            for (decl, dims) in member.declarators.iter().zip(dims_per_decl.iter()) {
1991                let f = format!("{base}{}", decl.name().text);
1992                let f = f.as_str();
1993                // Bug XV-mut: pick the COMPACT length code (XTypes 1.3 §7.4.3.4.2)
1994                // mirroring the Rust reference (`mutable_member_length_code`):
1995                //   * a fixed-size scalar primitive uses LC by wire size
1996                //     (1→LC0, 2→LC1, 4→LC2, 8→LC3) — NO NEXTINT, body inline;
1997                //   * a string/wstring uses LC5 — its own uint32 length prefix
1998                //     doubles as the NEXTINT (no separate NEXTINT);
1999                //   * everything else (arrays, sequences, maps, nested aggregates,
2000                //     long double) falls back to the universal LC4 + NEXTINT.
2001                let compact_lc = mutable_compact_lc(self.reg, &member.type_spec, dims, optional);
2002                match compact_lc {
2003                    Some(lc) => {
2004                        // LC0..3 (fixed primitive) and LC5 (string): EMHEADER then
2005                        // the body straight into w_buf, NO separate NEXTINT.
2006                        let emheader: u32 = (u32::from(lc) << 28) | id;
2007                        let _ = writeln!(self.out, "    {{");
2008                        let _ = writeln!(
2009                            self.out,
2010                            "        if (zerodds_xcdr2_c_write_u32(&w_buf, &w_len, &w_cap, 0x{emheader:08X}u) != 0) goto fail;"
2011                        );
2012                        self.emit_array_or_scalar_write(f, &member.type_spec, dims)?;
2013                        let _ = writeln!(self.out, "    }}");
2014                    }
2015                    None => {
2016                        // Universal LC4 (NEXTINT = body byte length), back-patched.
2017                        let emheader: u32 = (4u32 << 28) | id;
2018                        let _ = writeln!(self.out, "    {{");
2019                        let _ = writeln!(
2020                            self.out,
2021                            "        if (zerodds_xcdr2_c_write_u32(&w_buf, &w_len, &w_cap, 0x{emheader:08X}u) != 0) goto fail;"
2022                        );
2023                        let _ = writeln!(
2024                            self.out,
2025                            "        if (zerodds_xcdr2_c_grow(&w_buf, &w_cap, w_len + 4) != 0) goto fail;"
2026                        );
2027                        let _ = writeln!(self.out, "        size_t m_nextint_pos = w_len;");
2028                        let _ = writeln!(self.out, "        w_len += 4;");
2029                        let _ = writeln!(self.out, "        size_t m_body_start = w_len;");
2030                        self.emit_array_or_scalar_write(f, &member.type_spec, dims)?;
2031                        let _ = writeln!(
2032                            self.out,
2033                            "        uint32_t m_body_len = (uint32_t)(w_len - m_body_start);"
2034                        );
2035                        let _ = writeln!(
2036                            self.out,
2037                            "        zerodds_xcdr2_c_put_u32_at(w_buf, m_nextint_pos, m_body_len);"
2038                        );
2039                        let _ = writeln!(self.out, "    }}");
2040                    }
2041                }
2042            }
2043        }
2044        Ok(())
2045    }
2046
2047    /// Emits the @mutable members under XCDR1 as a PL_CDR1 parameter list. The C
2048    /// runtime aligns relative to the buffer start, but a PL_CDR1 member body
2049    /// must align param-relative (origin 0) — so each member body is encoded
2050    /// into a FRESH temp buffer (save/swap of `w_buf`), then spliced through
2051    /// `pl1_write_member` (PID header + body + pad-to-4). Ends with the sentinel.
2052    /// Mirrors `emit_mutable_member_writes_base`'s id assignment.
2053    fn emit_pl_cdr1_member_writes(&mut self, def: &StructDef) -> Result<(), CppGenError> {
2054        let mut auto_id: u32 = 0;
2055        for member in &def.members {
2056            let id = id_annotation(&member.annotations).unwrap_or(auto_id);
2057            auto_id = id + 1;
2058            let dims_per_decl: Vec<Vec<u64>> = member
2059                .declarators
2060                .iter()
2061                .map(|d| effective_array_dims(self.reg, &member.type_spec, d))
2062                .collect::<Result<_, _>>()?;
2063            let optional = is_optional(&member.annotations);
2064            for (decl, dims) in member.declarators.iter().zip(dims_per_decl.iter()) {
2065                let fname = decl.name().text.clone();
2066                let f = format!("s->{fname}");
2067                if optional {
2068                    // PL_CDR1 optional: present -> emit the parameter; absent ->
2069                    // omit it (no present flag; absence = not in the list).
2070                    let _ = writeln!(self.out, "    if (s->{fname}_present) {{");
2071                }
2072                let _ = writeln!(self.out, "    {{");
2073                // Save the main buffer; redirect w_* to a fresh temp so the body
2074                // is encoded param-relative (origin 0).
2075                let _ = writeln!(
2076                    self.out,
2077                    "        uint8_t* m_buf = w_buf; size_t m_len = w_len; size_t m_cap = w_cap;"
2078                );
2079                let _ = writeln!(self.out, "        w_buf = NULL; w_len = 0; w_cap = 0;");
2080                self.emit_array_or_scalar_write(&f, &member.type_spec, dims)?;
2081                // Splice [PID header][temp body][pad] into the saved main buffer.
2082                let _ = writeln!(
2083                    self.out,
2084                    "        if (zerodds_xcdr2_c_pl1_write_member(&m_buf, &m_len, &m_cap, {id}u, w_buf, w_len) != 0) {{ free(w_buf); w_buf = m_buf; w_len = m_len; w_cap = m_cap; goto fail; }}"
2085                );
2086                let _ = writeln!(
2087                    self.out,
2088                    "        free(w_buf); w_buf = m_buf; w_len = m_len; w_cap = m_cap;"
2089                );
2090                let _ = writeln!(self.out, "    }}");
2091                if optional {
2092                    let _ = writeln!(self.out, "    }}");
2093                }
2094            }
2095        }
2096        // PID_LIST_END sentinel terminates the parameter list.
2097        let _ = writeln!(
2098            self.out,
2099            "    if (zerodds_xcdr2_c_pl1_sentinel(&w_buf, &w_len, &w_cap) != 0) goto fail;"
2100        );
2101        Ok(())
2102    }
2103
2104    /// Reads a @mutable struct under XCDR1 as a PL_CDR1 parameter list: read each
2105    /// member header, dispatch on its id, and decode the body from the parameter
2106    /// sub-buffer (`buf + body_start`, local pos 0 → param-relative alignment),
2107    /// then advance past the (padded) parameter. Symmetric to
2108    /// `emit_pl_cdr1_member_writes`; mirrors its id assignment.
2109    fn emit_pl_cdr1_member_reads(&mut self, def: &StructDef) -> Result<(), CppGenError> {
2110        let _ = writeln!(self.out, "    while (pos + 4 <= len) {{");
2111        let _ = writeln!(
2112            self.out,
2113            "        uint32_t pl_id = 0; size_t pl_blen = 0; int pl_end = 0;"
2114        );
2115        let _ = writeln!(
2116            self.out,
2117            "        if (zerodds_xcdr2_c_pl1_read_header(buf, len, &pos, &pl_id, &pl_blen, &pl_end, zd_be) != 0) return -7;"
2118        );
2119        let _ = writeln!(self.out, "        if (pl_end) break;");
2120        let _ = writeln!(self.out, "        if (pos + pl_blen > len) return -7;");
2121        let _ = writeln!(self.out, "        size_t pl_body_start = pos;");
2122        let _ = writeln!(self.out, "        switch (pl_id) {{");
2123        let mut auto_id: u32 = 0;
2124        for member in &def.members {
2125            let id = id_annotation(&member.annotations).unwrap_or(auto_id);
2126            auto_id = id + 1;
2127            let dims_per_decl: Vec<Vec<u64>> = member
2128                .declarators
2129                .iter()
2130                .map(|d| effective_array_dims(self.reg, &member.type_spec, d))
2131                .collect::<Result<_, _>>()?;
2132            let optional = is_optional(&member.annotations);
2133            for (decl, dims) in member.declarators.iter().zip(dims_per_decl.iter()) {
2134                let fname = decl.name().text.clone();
2135                let f = format!("s->{fname}");
2136                let _ = writeln!(self.out, "        case {id}u: {{");
2137                // Redirect the cursor to the parameter body (origin 0).
2138                let _ = writeln!(
2139                    self.out,
2140                    "            const uint8_t* mb = buf; size_t ml = len; size_t mp = pos;"
2141                );
2142                let _ = writeln!(
2143                    self.out,
2144                    "            buf = mb + pl_body_start; len = pl_blen; pos = 0;"
2145                );
2146                if optional {
2147                    let _ = writeln!(self.out, "            s->{fname}_present = 1;");
2148                }
2149                self.emit_array_or_scalar_read(&f, &member.type_spec, dims)?;
2150                let _ = writeln!(self.out, "            buf = mb; len = ml; pos = mp;");
2151                let _ = writeln!(self.out, "            break;");
2152                let _ = writeln!(self.out, "        }}");
2153            }
2154        }
2155        let _ = writeln!(self.out, "        default: break;");
2156        let _ = writeln!(self.out, "        }}");
2157        let _ = writeln!(self.out, "        pos = pl_body_start + pl_blen;");
2158        // Skip the trailing pad to the next 4-byte boundary.
2159        let _ = writeln!(
2160            self.out,
2161            "        {{ size_t pl_pad = (4u - (pl_blen % 4u)) % 4u; pos += pl_pad; if (pos > len) pos = len; }}"
2162        );
2163        let _ = writeln!(self.out, "    }}");
2164        Ok(())
2165    }
2166
2167    fn emit_decode_body(
2168        &mut self,
2169        c_name: &str,
2170        def: &StructDef,
2171        ext: Extensibility,
2172    ) -> Result<(), CppGenError> {
2173        // The decoder is symmetric to the encoder; builds the Rust-stack-style
2174        // BufferReader via helper inline functions from `zerodds_xcdr2.h`.
2175        // `_decode`/`_decode_e` keep the XCDR2 ABI (typesupport .decode pointer);
2176        // `_decode_repr` is the representation-aware entry (header typedef
2177        // `zerodds_decode_repr_fn`); all funnel into `_decode_core(buf,len,out,
2178        // zd_be,representation)`. representation: 0=XCDR1 (no DHEADER, max_align 8,
2179        // @mutable=PL_CDR1), 1=XCDR2.
2180        let _ = writeln!(
2181            self.out,
2182            "static int {c_name}_decode_core(const uint8_t* buf, size_t len, void* out_sample, int zd_be, int representation);"
2183        );
2184        let _ = writeln!(
2185            self.out,
2186            "static int {c_name}_decode(const uint8_t* buf, size_t len, void* out_sample) {{ return {c_name}_decode_core(buf, len, out_sample, 0, 1); }}"
2187        );
2188        let _ = writeln!(
2189            self.out,
2190            "static int {c_name}_decode_e(const uint8_t* buf, size_t len, void* out_sample, int zd_be) {{ return {c_name}_decode_core(buf, len, out_sample, zd_be, 1); }}"
2191        );
2192        let _ = writeln!(
2193            self.out,
2194            "static int {c_name}_decode_repr(const uint8_t* buf, size_t len, uint8_t representation, void* out_sample) {{ return {c_name}_decode_core(buf, len, out_sample, 0, representation ? 1 : 0); }}"
2195        );
2196        let _ = writeln!(
2197            self.out,
2198            "static int {c_name}_decode_core(const uint8_t* buf, size_t len, void* out_sample, int zd_be, int representation) {{"
2199        );
2200        let _ = writeln!(self.out, "    {c_name}_t* s = ({c_name}_t*)out_sample;");
2201        let _ = writeln!(self.out, "    size_t pos = 0;");
2202        let _ = writeln!(
2203            self.out,
2204            "    size_t zd_ma = representation ? 4 : 8; (void)zd_ma;"
2205        );
2206        match ext {
2207            Extensibility::Final => {
2208                self.emit_struct_body_reads(def)?;
2209            }
2210            Extensibility::Appendable => {
2211                // XCDR2 reads the DHEADER; XCDR1 has none (body runs to `len`).
2212                let _ = writeln!(self.out, "    size_t body_end = len;");
2213                let _ = writeln!(self.out, "    if (representation) {{");
2214                let _ = writeln!(self.out, "        uint32_t dheader = 0;");
2215                let _ = writeln!(
2216                    self.out,
2217                    "        if (zerodds_xcdr2_c_read_u32(buf, len, &pos, &dheader, zd_be) != 0) return -7;"
2218                );
2219                let _ = writeln!(self.out, "        body_end = pos + dheader;");
2220                let _ = writeln!(self.out, "        if (body_end > len) return -7;");
2221                let _ = writeln!(self.out, "    }}");
2222                self.emit_struct_body_reads(def)?;
2223                let _ = writeln!(self.out, "    if (representation) pos = body_end;");
2224            }
2225            Extensibility::Mutable => {
2226                let _ = writeln!(self.out, "    if (!representation) {{");
2227                self.emit_pl_cdr1_member_reads(def)?;
2228                let _ = writeln!(self.out, "    }} else {{");
2229                let _ = writeln!(self.out, "    uint32_t dheader = 0;");
2230                let _ = writeln!(
2231                    self.out,
2232                    "    if (zerodds_xcdr2_c_read_u32(buf, len, &pos, &dheader, zd_be) != 0) return -7;"
2233                );
2234                let _ = writeln!(self.out, "    size_t body_end = pos + dheader;");
2235                let _ = writeln!(self.out, "    if (body_end > len) return -7;");
2236                let _ = writeln!(self.out, "    while (pos < body_end) {{");
2237                let _ = writeln!(self.out, "        uint32_t emheader = 0;");
2238                let _ = writeln!(
2239                    self.out,
2240                    "        if (zerodds_xcdr2_c_read_u32(buf, len, &pos, &emheader, zd_be) != 0) return -7;"
2241                );
2242                let _ = writeln!(self.out, "        uint32_t mid = emheader & 0x0FFFFFFFu;");
2243                let _ = writeln!(self.out, "        uint32_t lc = (emheader >> 28) & 0x7u;");
2244                let _ = writeln!(self.out, "        uint32_t nextint = 0;");
2245                let _ = writeln!(self.out, "        size_t body_len = 0;");
2246                // Bug XV-mut: compact length codes. LC0..3 = fixed 1/2/4/8-byte
2247                // bodies, NO NEXTINT. LC4/6/7 carry a separate NEXTINT (consumed
2248                // here). LC5 = the body's OWN leading uint32 length word doubles as
2249                // the NEXTINT — so it must NOT be consumed separately; we PEEK it
2250                // (read via a scratch position) and leave `pos` at the body start so
2251                // the member reader sees its length prefix intact.
2252                let _ = writeln!(
2253                    self.out,
2254                    "        if (lc == 0) body_len = 1; else if (lc == 1) body_len = 2; else if (lc == 2) body_len = 4; else if (lc == 3) body_len = 8;"
2255                );
2256                let _ = writeln!(self.out, "        else if (lc == 5) {{");
2257                let _ = writeln!(self.out, "            size_t peek_pos = pos;");
2258                let _ = writeln!(
2259                    self.out,
2260                    "            if (zerodds_xcdr2_c_read_u32(buf, len, &peek_pos, &nextint, zd_be) != 0) return -7;"
2261                );
2262                let _ = writeln!(self.out, "            body_len = 4u + (size_t)nextint;");
2263                let _ = writeln!(self.out, "        }} else {{");
2264                let _ = writeln!(
2265                    self.out,
2266                    "            if (zerodds_xcdr2_c_read_u32(buf, len, &pos, &nextint, zd_be) != 0) return -7;"
2267                );
2268                let _ = writeln!(self.out, "            body_len = nextint;");
2269                let _ = writeln!(self.out, "        }}");
2270                let _ = writeln!(
2271                    self.out,
2272                    "        if (pos + body_len > body_end) return -7;"
2273                );
2274                self.emit_mutable_member_dispatch(def)?;
2275                let _ = writeln!(self.out, "    }}"); // close while
2276                let _ = writeln!(self.out, "    }}"); // close the XCDR2 `else`
2277            }
2278        }
2279        let _ = writeln!(self.out, "    (void)s;");
2280        let _ = writeln!(self.out, "    return 0;");
2281        let _ = writeln!(self.out, "}}");
2282        let _ = writeln!(self.out);
2283        Ok(())
2284    }
2285
2286    fn emit_struct_body_reads(&mut self, def: &StructDef) -> Result<(), CppGenError> {
2287        for member in &def.members {
2288            let optional = is_optional(&member.annotations);
2289            for decl in &member.declarators {
2290                let f = &decl.name().text;
2291                let dims = effective_array_dims(self.reg, &member.type_spec, decl)?;
2292                if optional {
2293                    let _ = writeln!(self.out, "    {{");
2294                    let _ = writeln!(self.out, "        uint8_t present = 0;");
2295                    let _ = writeln!(
2296                        self.out,
2297                        "        if (zerodds_xcdr2_c_read_u8(buf, len, &pos, &present, zd_be) != 0) return -7;"
2298                    );
2299                    let _ = writeln!(self.out, "        s->{f}_present = present;");
2300                    let _ = writeln!(self.out, "        if (present) {{");
2301                    self.emit_array_or_scalar_read(&format!("s->{f}"), &member.type_spec, &dims)?;
2302                    let _ = writeln!(self.out, "        }}");
2303                    let _ = writeln!(self.out, "    }}");
2304                } else {
2305                    self.emit_array_or_scalar_read(&format!("s->{f}"), &member.type_spec, &dims)?;
2306                }
2307            }
2308        }
2309        Ok(())
2310    }
2311
2312    fn emit_array_or_scalar_read(
2313        &mut self,
2314        var: &str,
2315        type_spec: &TypeSpec,
2316        dims: &[u64],
2317    ) -> Result<(), CppGenError> {
2318        if dims.is_empty() {
2319            return self.emit_member_read(var, type_spec);
2320        }
2321        // Bug XV-arr: symmetric to the encode — only a NON-primitive-element array
2322        // carries a collection DHEADER. A PARRAY (primitive element, any dims) has
2323        // none, so nothing to read+discard.
2324        let needs_dheader = !self.seq_elem_is_primitive(type_spec);
2325        if needs_dheader {
2326            let _ = writeln!(
2327                self.out,
2328                "    if (representation) {{ uint32_t arr_dheader = 0; if (zerodds_xcdr2_c_read_u32(buf, len, &pos, &arr_dheader, zd_be) != 0) return -7; (void)arr_dheader; }}"
2329            );
2330        }
2331        let mut acc = var.to_string();
2332        for (d, _n) in dims.iter().enumerate() {
2333            let iv = format!("ri{d}");
2334            let _ = writeln!(
2335                self.out,
2336                "    for (uint32_t {iv} = 0; {iv} < {n}; ++{iv}) {{",
2337                n = dims[d]
2338            );
2339            acc = format!("{acc}[{iv}]");
2340        }
2341        self.emit_member_read(&acc, type_spec)?;
2342        for _ in dims {
2343            let _ = writeln!(self.out, "    }}");
2344        }
2345        Ok(())
2346    }
2347
2348    /// zerodds-lint: recursion-depth 64 (nested struct members; bounded by IDL)
2349    fn emit_member_read(&mut self, var: &str, type_spec: &TypeSpec) -> Result<(), CppGenError> {
2350        let resolved = resolve_alias(self.reg, type_spec);
2351        match &resolved {
2352            TypeSpec::Primitive(p) => {
2353                let helper = primitive_helper(*p)?;
2354                if matches!(helper, "u64" | "i64" | "f64") {
2355                    // 8-byte primitive: XCDR2 caps alignment at 4 (`zd_x2_*`),
2356                    // XCDR1 reads at MAXALIGN 8 via the shared helper.
2357                    let _ = writeln!(
2358                        self.out,
2359                        "    if (representation) {{ if (zd_x2_read_{helper}(buf, len, &pos, &({var}), zd_be) != 0) return -7; }}"
2360                    );
2361                    let _ = writeln!(
2362                        self.out,
2363                        "    else {{ if (zerodds_xcdr2_c_read_{helper}(buf, len, &pos, &({var}), zd_be) != 0) return -7; }}"
2364                    );
2365                    return Ok(());
2366                }
2367                let prefix = Self::helper_call_prefix(helper);
2368                let _ = writeln!(
2369                    self.out,
2370                    "    if ({prefix}read_{helper}(buf, len, &pos, &({var}), zd_be) != 0) return -7;"
2371                );
2372                Ok(())
2373            }
2374            TypeSpec::String(st) => {
2375                if st.wide {
2376                    return self.emit_wstring_read(var);
2377                }
2378                let _ = writeln!(
2379                    self.out,
2380                    "    if (zerodds_xcdr2_c_read_string(buf, len, &pos, &({var}), zd_be) != 0) return -7;"
2381                );
2382                Ok(())
2383            }
2384            TypeSpec::Sequence(seq) => self.emit_sequence_read(var, &seq.elem),
2385            TypeSpec::Map(m) => self.emit_map_read(var, &m.key, &m.value),
2386            TypeSpec::Scoped(sc) => {
2387                let last = scoped_last(sc).ok_or_else(|| unsupported("empty scoped name"))?;
2388                if self.reg.enums.contains_key(&last) {
2389                    // Read the @bit_bound-width holder, sign-extend to the int32
2390                    // in-memory enum (XTypes §7.4.5.1).
2391                    let bytes = self.reg.enum_bytes.get(&last).copied().unwrap_or(4);
2392                    let line = match bytes {
2393                        1 => format!(
2394                            "    {{ uint8_t zd_e8 = 0; if (zerodds_xcdr2_c_read_u8(buf, len, &pos, &zd_e8, zd_be) != 0) return -7; ({var}) = (int32_t)(int8_t)zd_e8; }}"
2395                        ),
2396                        2 => format!(
2397                            "    {{ uint16_t zd_e16 = 0; if (zerodds_xcdr2_c_read_u16(buf, len, &pos, &zd_e16, zd_be) != 0) return -7; ({var}) = (int32_t)(int16_t)zd_e16; }}"
2398                        ),
2399                        _ => format!(
2400                            "    if (zerodds_xcdr2_c_read_i32(buf, len, &pos, (int32_t*)&({var}), zd_be) != 0) return -7;"
2401                        ),
2402                    };
2403                    let _ = writeln!(self.out, "{line}");
2404                    return Ok(());
2405                }
2406                // bitmask / bitset → read the holder integer (XTypes §7.3.1.2).
2407                if let Some(helper) = self.bits_helper(&last) {
2408                    let prefix = Self::helper_call_prefix(helper);
2409                    let _ = writeln!(
2410                        self.out,
2411                        "    if ({prefix}read_{helper}(buf, len, &pos, &({var}), zd_be) != 0) return -7;"
2412                    );
2413                    return Ok(());
2414                }
2415                if let Some((cn, ndef)) = self.reg.structs.get(&last).cloned() {
2416                    if self.reg.is_recursive(&last) {
2417                        // Symmetric to the encode: a NON-final recursive struct is
2418                        // DHEADER-delimited — read+discard its DHEADER before its
2419                        // body. (XTypes §7.4.3.5 / §7.4.4.4.)
2420                        let ext = extensibility_of(&ndef.annotations);
2421                        if !matches!(ext, Extensibility::Final) {
2422                            let _ = writeln!(
2423                                self.out,
2424                                "    if (representation) {{ uint32_t rec_dheader = 0; if (zerodds_xcdr2_c_read_u32(buf, len, &pos, &rec_dheader, zd_be) != 0) return -7; (void)rec_dheader; }}"
2425                            );
2426                        }
2427                        let _ = writeln!(
2428                            self.out,
2429                            "    if ({cn}_read_body(buf, len, &pos, &({var}), zd_be, representation) != 0) return -7;"
2430                        );
2431                        return Ok(());
2432                    }
2433                    return self.emit_nested_struct_read(var, &ndef);
2434                }
2435                if let Some((cn, udef)) = self.reg.unions.get(&last).cloned() {
2436                    if self.reg.is_recursive(&last) {
2437                        let _ = writeln!(
2438                            self.out,
2439                            "    if ({cn}_read_body(buf, len, &pos, &({var}), zd_be, representation) != 0) return -7;"
2440                        );
2441                        return Ok(());
2442                    }
2443                    return self.emit_union_read(var, &udef);
2444                }
2445                Err(unsupported("unresolved scoped member read (C backend)"))
2446            }
2447            TypeSpec::Fixed(f) => {
2448                // fixed<P,S>: read the (P+2)/2 raw BCD octets into `.bcd[]`.
2449                let p = const_expr_to_u64(self.reg, &f.digits)
2450                    .ok_or_else(|| unsupported("fixed: non-constant digit count"))?;
2451                let n = (p + 2) / 2;
2452                let _ = writeln!(
2453                    self.out,
2454                    "    for (size_t __fi = 0; __fi < {n}; __fi++) {{ uint8_t __fb; if (zerodds_xcdr2_c_read_u8(buf, len, &pos, &__fb, zd_be) != 0) return -7; ({var}).bcd[__fi] = __fb; }}"
2455                );
2456                Ok(())
2457            }
2458            _ => Err(unsupported("complex member read")),
2459        }
2460    }
2461
2462    /// zerodds-lint: recursion-depth 64 (bounded by IDL nesting)
2463    fn emit_nested_struct_read(&mut self, var: &str, ndef: &StructDef) -> Result<(), CppGenError> {
2464        // Symmetric to `emit_nested_struct_write`: a NON-`@final` nested struct is
2465        // DHEADER-delimited. `@mutable` parses the EMHEADER member loop bounded by
2466        // its own DHEADER; `@appendable` reads its members tight after the
2467        // DHEADER; `@final` reads the bare inline body. The depth counter scopes
2468        // the local names so nested @mutable structs don't collide.
2469        let ext = extensibility_of(&ndef.annotations);
2470        if !matches!(ext, Extensibility::Final) {
2471            let d = self.coll_depth;
2472            self.coll_depth += 1;
2473            let r = self.emit_nested_struct_read_delimited(var, ndef, ext, d);
2474            self.coll_depth -= 1;
2475            return r;
2476        }
2477        self.emit_nested_struct_inline_read(var, ndef)
2478    }
2479
2480    /// Reads a DHEADER-delimited nested struct (`@appendable` / `@mutable`). For
2481    /// `@mutable`, runs the EMHEADER member-id dispatch loop bounded by the
2482    /// struct's own DHEADER; for `@appendable`, reads members tight then snaps to
2483    /// the DHEADER end. `depth` scopes the C local names.
2484    fn emit_nested_struct_read_delimited(
2485        &mut self,
2486        var: &str,
2487        ndef: &StructDef,
2488        ext: Extensibility,
2489        depth: u32,
2490    ) -> Result<(), CppGenError> {
2491        let _ = writeln!(self.out, "    {{");
2492        // XCDR2: read the nested struct's DHEADER (bounds the body). XCDR1: no
2493        // DHEADER — the body runs inline (no separate bound). (A @mutable nested
2494        // struct under XCDR1 would be PL_CDR1; the corpus has only @appendable
2495        // nested structs.)
2496        let _ = writeln!(
2497            self.out,
2498            "        size_t nst_end{depth} = len; (void)nst_end{depth};"
2499        );
2500        let _ = writeln!(self.out, "        if (representation) {{");
2501        let _ = writeln!(self.out, "        uint32_t nst_dheader{depth} = 0;");
2502        let _ = writeln!(
2503            self.out,
2504            "        if (zerodds_xcdr2_c_read_u32(buf, len, &pos, &nst_dheader{depth}, zd_be) != 0) return -7;"
2505        );
2506        let _ = writeln!(
2507            self.out,
2508            "        nst_end{depth} = pos + nst_dheader{depth};"
2509        );
2510        let _ = writeln!(self.out, "        if (nst_end{depth} > len) return -7;");
2511        let _ = writeln!(self.out, "        }}");
2512        if matches!(ext, Extensibility::Mutable) {
2513            let _ = writeln!(
2514                self.out,
2515                "        while (representation && pos < nst_end{depth}) {{"
2516            );
2517            let _ = writeln!(self.out, "            uint32_t emheader{depth} = 0;");
2518            let _ = writeln!(
2519                self.out,
2520                "            if (zerodds_xcdr2_c_read_u32(buf, len, &pos, &emheader{depth}, zd_be) != 0) return -7;"
2521            );
2522            let _ = writeln!(
2523                self.out,
2524                "            uint32_t mid{depth} = emheader{depth} & 0x0FFFFFFFu;"
2525            );
2526            let _ = writeln!(
2527                self.out,
2528                "            uint32_t lc{depth} = (emheader{depth} >> 28) & 0x7u;"
2529            );
2530            let _ = writeln!(self.out, "            uint32_t nextint{depth} = 0;");
2531            let _ = writeln!(self.out, "            size_t body_len{depth} = 0;");
2532            let _ = writeln!(
2533                self.out,
2534                "            if (lc{depth} == 0) body_len{depth} = 1; else if (lc{depth} == 1) body_len{depth} = 2; else if (lc{depth} == 2) body_len{depth} = 4; else if (lc{depth} == 3) body_len{depth} = 8;"
2535            );
2536            let _ = writeln!(self.out, "            else if (lc{depth} == 5) {{");
2537            let _ = writeln!(self.out, "                size_t peek_pos{depth} = pos;");
2538            let _ = writeln!(
2539                self.out,
2540                "                if (zerodds_xcdr2_c_read_u32(buf, len, &peek_pos{depth}, &nextint{depth}, zd_be) != 0) return -7;"
2541            );
2542            let _ = writeln!(
2543                self.out,
2544                "                body_len{depth} = 4u + (size_t)nextint{depth};"
2545            );
2546            let _ = writeln!(self.out, "            }} else {{");
2547            let _ = writeln!(
2548                self.out,
2549                "                if (zerodds_xcdr2_c_read_u32(buf, len, &pos, &nextint{depth}, zd_be) != 0) return -7;"
2550            );
2551            let _ = writeln!(
2552                self.out,
2553                "                body_len{depth} = nextint{depth};"
2554            );
2555            let _ = writeln!(self.out, "            }}");
2556            let _ = writeln!(
2557                self.out,
2558                "            if (pos + body_len{depth} > nst_end{depth}) return -7;"
2559            );
2560            self.emit_mutable_member_dispatch_base(
2561                ndef,
2562                &format!("({var})."),
2563                &format!("mid{depth}"),
2564                &format!("body_len{depth}"),
2565            )?;
2566            let _ = writeln!(self.out, "        }}");
2567        } else {
2568            self.emit_nested_struct_inline_read(var, ndef)?;
2569            let _ = writeln!(
2570                self.out,
2571                "        if (representation) pos = nst_end{depth};"
2572            );
2573        }
2574        let _ = writeln!(self.out, "    }}");
2575        Ok(())
2576    }
2577
2578    /// Tight-packed (`@final`) inline read of a nested struct's members. Also the
2579    /// `@appendable` body (members after the DHEADER).
2580    /// zerodds-lint: recursion-depth 64 (bounded by IDL nesting)
2581    fn emit_nested_struct_inline_read(
2582        &mut self,
2583        var: &str,
2584        ndef: &StructDef,
2585    ) -> Result<(), CppGenError> {
2586        for member in &ndef.members {
2587            let optional = is_optional(&member.annotations);
2588            for decl in &member.declarators {
2589                let f = &decl.name().text;
2590                let dims = effective_array_dims(self.reg, &member.type_spec, decl)?;
2591                if optional {
2592                    let _ = writeln!(self.out, "    {{");
2593                    let _ = writeln!(self.out, "        uint8_t present = 0;");
2594                    let _ = writeln!(
2595                        self.out,
2596                        "        if (zerodds_xcdr2_c_read_u8(buf, len, &pos, &present, zd_be) != 0) return -7;"
2597                    );
2598                    let _ = writeln!(self.out, "        ({var}).{f}_present = present;");
2599                    let _ = writeln!(self.out, "        if (present) {{");
2600                    self.emit_array_or_scalar_read(
2601                        &format!("({var}).{f}"),
2602                        &member.type_spec,
2603                        &dims,
2604                    )?;
2605                    let _ = writeln!(self.out, "        }}");
2606                    let _ = writeln!(self.out, "    }}");
2607                } else {
2608                    self.emit_array_or_scalar_read(
2609                        &format!("({var}).{f}"),
2610                        &member.type_spec,
2611                        &dims,
2612                    )?;
2613                }
2614            }
2615        }
2616        Ok(())
2617    }
2618
2619    /// Read an XCDR2 wstring into a freshly-malloc'd NUL-terminated `uint16_t*`.
2620    fn emit_wstring_read(&mut self, var: &str) -> Result<(), CppGenError> {
2621        let _ = writeln!(self.out, "    {{");
2622        let _ = writeln!(self.out, "        uint32_t ws_bytes = 0;");
2623        let _ = writeln!(
2624            self.out,
2625            "        if (zerodds_xcdr2_c_read_u32(buf, len, &pos, &ws_bytes, zd_be) != 0) return -7;"
2626        );
2627        let _ = writeln!(self.out, "        uint32_t ws_n = ws_bytes / 2u;");
2628        let _ = writeln!(
2629            self.out,
2630            "        uint16_t* ws_p = (uint16_t*)malloc(((size_t)ws_n + 1) * sizeof(uint16_t));"
2631        );
2632        let _ = writeln!(self.out, "        if (ws_p == NULL) return -7;");
2633        let _ = writeln!(
2634            self.out,
2635            "        for (uint32_t wi = 0; wi < ws_n; ++wi) {{ if (zerodds_xcdr2_c_read_u16(buf, len, &pos, &ws_p[wi], zd_be) != 0) {{ free(ws_p); return -7; }} }}"
2636        );
2637        let _ = writeln!(self.out, "        ws_p[ws_n] = 0;");
2638        let _ = writeln!(self.out, "        ({var}) = ws_p;");
2639        let _ = writeln!(self.out, "    }}");
2640        Ok(())
2641    }
2642
2643    /// Read an XCDR2 map: skip DHEADER, read count, allocate parallel
2644    /// key/value arrays, read each pair.
2645    fn emit_map_read(
2646        &mut self,
2647        var: &str,
2648        key: &TypeSpec,
2649        value: &TypeSpec,
2650    ) -> Result<(), CppGenError> {
2651        let kc = c_type_for(self.reg, key)?;
2652        let vc = c_type_for(self.reg, value)?;
2653        let _ = writeln!(self.out, "    {{");
2654        // Symmetric to the encoder: the map DHEADER is 4-aligned and present
2655        // ONLY for a non-primitive (key,value) element (XCDR2 §7.4.3.5).
2656        let _ = writeln!(
2657            self.out,
2658            "        if (zerodds_xcdr2_c_pad_read(buf, len, &pos, 4) != 0) return -1;"
2659        );
2660        if !(self.seq_elem_is_primitive(key) && self.seq_elem_is_primitive(value)) {
2661            let _ = writeln!(self.out, "        if (representation) {{");
2662            let _ = writeln!(self.out, "        uint32_t map_dheader = 0;");
2663            let _ = writeln!(
2664                self.out,
2665                "        if (zerodds_xcdr2_c_read_u32(buf, len, &pos, &map_dheader, zd_be) != 0) return -7;"
2666            );
2667            let _ = writeln!(self.out, "        (void)map_dheader;");
2668            let _ = writeln!(self.out, "        }}");
2669        }
2670        let _ = writeln!(self.out, "        uint32_t map_len = 0;");
2671        let _ = writeln!(
2672            self.out,
2673            "        if (zerodds_xcdr2_c_read_u32(buf, len, &pos, &map_len, zd_be) != 0) return -7;"
2674        );
2675        let _ = writeln!(self.out, "        ({var}).len = map_len;");
2676        let _ = writeln!(
2677            self.out,
2678            "        ({var}).keys = ({kc}*)calloc(map_len ? map_len : 1, sizeof({kc}));"
2679        );
2680        let _ = writeln!(
2681            self.out,
2682            "        ({var}).vals = ({vc}*)calloc(map_len ? map_len : 1, sizeof({vc}));"
2683        );
2684        let _ = writeln!(
2685            self.out,
2686            "        if (((({var}).keys == NULL) || (({var}).vals == NULL)) && map_len > 0) return -7;"
2687        );
2688        let mv = format!("mi{}", self.coll_depth);
2689        let _ = writeln!(
2690            self.out,
2691            "        for (uint32_t {mv} = 0; {mv} < map_len; ++{mv}) {{"
2692        );
2693        self.coll_depth += 1;
2694        let rk = self.emit_member_read(&format!("({var}).keys[{mv}]"), key);
2695        let rv = if rk.is_ok() {
2696            self.emit_member_read(&format!("({var}).vals[{mv}]"), value)
2697        } else {
2698            Ok(())
2699        };
2700        self.coll_depth -= 1;
2701        rk.and(rv)?;
2702        let _ = writeln!(self.out, "        }}");
2703        let _ = writeln!(self.out, "    }}");
2704        Ok(())
2705    }
2706
2707    /// Read a discriminated union: read the discriminator, then the selected
2708    /// member into the `_u` arm.
2709    fn emit_union_read(&mut self, var: &str, udef: &UnionDef) -> Result<(), CppGenError> {
2710        // Symmetric to emit_union_write: appendable/mutable unions carry a
2711        // 4-aligned DHEADER over [disc + branch]; @final does not.
2712        let u_dheader = !matches!(extensibility_of(&udef.annotations), Extensibility::Final);
2713        if u_dheader {
2714            // XCDR2: 4-align + the union DHEADER. XCDR1: no DHEADER (the
2715            // discriminator read below does its own alignment).
2716            let _ = writeln!(self.out, "    if (representation) {{");
2717            let _ = writeln!(
2718                self.out,
2719                "    if (zerodds_xcdr2_c_pad_read(buf, len, &pos, 4) != 0) return -1;"
2720            );
2721            let _ = writeln!(
2722                self.out,
2723                "    {{ uint32_t u_dh = 0; if (zerodds_xcdr2_c_read_u32(buf, len, &pos, &u_dh, zd_be) != 0) return -7; (void)u_dh; }}"
2724            );
2725            let _ = writeln!(self.out, "    }}");
2726        }
2727        let disc_ts = switch_type_spec(&udef.switch_type);
2728        self.emit_member_read(&format!("({var})._d"), &disc_ts)?;
2729        let _ = writeln!(self.out, "    switch (({var})._d) {{");
2730        let mut default_case: Option<&Case> = None;
2731        for case in &udef.cases {
2732            let field = union_case_field(case);
2733            let mut has_value_label = false;
2734            for label in &case.labels {
2735                match label {
2736                    CaseLabel::Value(expr) => {
2737                        has_value_label = true;
2738                        let lit = self.case_label_literal(expr)?;
2739                        let _ = writeln!(self.out, "    case {lit}:");
2740                    }
2741                    CaseLabel::Default => {
2742                        default_case = Some(case);
2743                    }
2744                }
2745            }
2746            if has_value_label {
2747                self.emit_member_read(&format!("({var})._u.{field}"), &case.element.type_spec)?;
2748                let _ = writeln!(self.out, "        break;");
2749            }
2750        }
2751        if let Some(case) = default_case {
2752            let field = union_case_field(case);
2753            let _ = writeln!(self.out, "    default:");
2754            self.emit_member_read(&format!("({var})._u.{field}"), &case.element.type_spec)?;
2755            let _ = writeln!(self.out, "        break;");
2756        } else {
2757            let _ = writeln!(self.out, "    default: break;");
2758        }
2759        let _ = writeln!(self.out, "    }}");
2760        Ok(())
2761    }
2762
2763    /// Fold a collection bound (`sequence<T,N>` / `string<N>` / `map<K,V,N>`)
2764    /// to its positive integer value, resolving named constants/enumerators via
2765    /// the symbol table. `None` for an unbounded collection.
2766    fn eval_bound(&self, bound: Option<&ConstExpr>) -> Option<u64> {
2767        let expr = bound?;
2768        evaluate(expr, &self.reg.consts)
2769            .ok()
2770            .and_then(|v| v.as_i64())
2771            .filter(|n| *n >= 0)
2772            .map(|n| n as u64)
2773    }
2774
2775    /// Evaluate a union case label to a C-printable integer literal. Handles
2776    /// integer/char/boolean discriminators plus enum constants.
2777    fn case_label_literal(&self, expr: &ConstExpr) -> Result<String, CppGenError> {
2778        // integer / char / boolean / enumerator → integer C `case` value.
2779        // `as_i64` already covers Bool, Char, WChar and Enum ordinals.
2780        if let Ok(v) = evaluate(expr, &self.reg.consts) {
2781            if let Some(i) = v.as_i64() {
2782                return Ok(i.to_string());
2783            }
2784        }
2785        Err(unsupported("non-integer union case label (C backend)"))
2786    }
2787
2788    fn emit_sequence_read(&mut self, var: &str, elem: &TypeSpec) -> Result<(), CppGenError> {
2789        let _ = writeln!(self.out, "    {{");
2790        // XCDR2 §7.4.3.5: for non-primitive elements, a DHEADER (uint32 before
2791        // [count + elements]) is present — skip it. enum→int32 is primitive.
2792        if !self.seq_elem_is_primitive(elem) {
2793            let _ = writeln!(self.out, "        if (representation) {{");
2794            let _ = writeln!(self.out, "        uint32_t seq_dheader = 0;");
2795            let _ = writeln!(
2796                self.out,
2797                "        if (zerodds_xcdr2_c_read_u32(buf, len, &pos, &seq_dheader, zd_be) != 0) return -7;"
2798            );
2799            let _ = writeln!(self.out, "        (void)seq_dheader;");
2800            let _ = writeln!(self.out, "        }}");
2801        }
2802        let _ = writeln!(self.out, "        uint32_t seq_len = 0;");
2803        let _ = writeln!(
2804            self.out,
2805            "        if (zerodds_xcdr2_c_read_u32(buf, len, &pos, &seq_len, zd_be) != 0) return -7;"
2806        );
2807        let elem_c = c_type_for(self.reg, elem)?;
2808        let _ = writeln!(self.out, "        ({var}).len = seq_len;");
2809        let _ = writeln!(
2810            self.out,
2811            "        ({var}).elems = ({elem_c}*)calloc(seq_len ? seq_len : 1, sizeof({elem_c}));"
2812        );
2813        let _ = writeln!(
2814            self.out,
2815            "        if (({var}).elems == NULL && seq_len > 0) return -7;"
2816        );
2817        let iv = format!("i{}", self.coll_depth);
2818        let _ = writeln!(
2819            self.out,
2820            "        for (uint32_t {iv} = 0; {iv} < seq_len; ++{iv}) {{"
2821        );
2822        // Delegate to the generic member reader so structs, enums and nested
2823        // sequences all decode (#43, sequence<non-primitive T>). Depth-scoped
2824        // loop counter avoids inner/outer index shadowing.
2825        self.coll_depth += 1;
2826        let r = self.emit_member_read(&format!("({var}).elems[{iv}]"), elem);
2827        self.coll_depth -= 1;
2828        r?;
2829        let _ = writeln!(self.out, "        }}");
2830        let _ = writeln!(self.out, "    }}");
2831        Ok(())
2832    }
2833
2834    /// True if a sequence element serializes as a plain primitive (no DHEADER):
2835    /// IDL primitives, and enum members (which become int32). Resolves through
2836    /// typedef aliases first.
2837    fn seq_elem_is_primitive(&self, elem: &TypeSpec) -> bool {
2838        let resolved = resolve_alias(self.reg, elem);
2839        match &resolved {
2840            TypeSpec::Primitive(_) => true,
2841            TypeSpec::Scoped(sc) => scoped_last(sc).is_some_and(|last| {
2842                // enum (→int32), bitmask/bitset (→holder uint) all serialize as
2843                // a plain primitive integer, so no element DHEADER.
2844                self.reg.enums.contains_key(&last)
2845                    || self.reg.bitmasks.contains_key(&last)
2846                    || self.reg.bitsets.contains_key(&last)
2847            }),
2848            _ => false,
2849        }
2850    }
2851
2852    fn emit_mutable_member_dispatch(&mut self, def: &StructDef) -> Result<(), CppGenError> {
2853        self.emit_mutable_member_dispatch_base(def, "s->", "mid", "body_len")
2854    }
2855
2856    /// EMHEADER member-id `switch` for a @mutable decode, with member accessors
2857    /// prefixed by `base` and the member-id / body-length expressions named by
2858    /// `mid_var` / `body_len_var` (so a nested @mutable struct can run its own
2859    /// depth-scoped loop).
2860    fn emit_mutable_member_dispatch_base(
2861        &mut self,
2862        def: &StructDef,
2863        base: &str,
2864        mid_var: &str,
2865        body_len_var: &str,
2866    ) -> Result<(), CppGenError> {
2867        let _ = writeln!(self.out, "        switch ({mid_var}) {{");
2868        // Sequential auto-id (XTypes §7.3.4.3 default), mirroring the encoder.
2869        let mut auto_id: u32 = 0;
2870        for member in &def.members {
2871            let id = id_annotation(&member.annotations).unwrap_or(auto_id);
2872            auto_id = id + 1;
2873            for decl in &member.declarators {
2874                let f = format!("{base}{}", decl.name().text);
2875                let dims = effective_array_dims(self.reg, &member.type_spec, decl)?;
2876                let _ = writeln!(self.out, "        case {id}: {{");
2877                self.emit_array_or_scalar_read(&f, &member.type_spec, &dims)?;
2878                let _ = writeln!(self.out, "            break;");
2879                let _ = writeln!(self.out, "        }}");
2880            }
2881        }
2882        let _ = writeln!(self.out, "        default: pos += {body_len_var}; break;");
2883        let _ = writeln!(self.out, "        }}");
2884        Ok(())
2885    }
2886
2887    fn emit_free_body(&mut self, c_name: &str, def: &StructDef) {
2888        let _ = writeln!(
2889            self.out,
2890            "static void {c_name}_sample_free(void* sample) {{"
2891        );
2892        let _ = writeln!(self.out, "    if (sample == NULL) return;");
2893        let _ = writeln!(self.out, "    {c_name}_t* s = ({c_name}_t*)sample;");
2894        let _ = writeln!(self.out, "    (void)s;");
2895        for member in &def.members {
2896            let resolved = resolve_alias(self.reg, &member.type_spec);
2897            for decl in &member.declarators {
2898                let f = &decl.name().text;
2899                let dims =
2900                    effective_array_dims(self.reg, &member.type_spec, decl).unwrap_or_default();
2901                if dims.is_empty() {
2902                    // Scalar member.
2903                    self.emit_free_member(&format!("s->{f}"), &resolved);
2904                } else {
2905                    // Heap-owning members reached THROUGH a fixed array (e.g.
2906                    // `string names[3]`, `sequence<long> rows[2]`) must still be
2907                    // freed per element — previously skipped → leak.
2908                    let mut acc = format!("s->{f}");
2909                    for (d, n) in dims.iter().enumerate() {
2910                        let iv = format!("fi{d}");
2911                        let _ = writeln!(
2912                            self.out,
2913                            "    for (uint32_t {iv} = 0; {iv} < {n}; ++{iv}) {{"
2914                        );
2915                        acc = format!("{acc}[{iv}]");
2916                    }
2917                    self.emit_free_member(&acc, &resolved);
2918                    for _ in &dims {
2919                        let _ = writeln!(self.out, "    }}");
2920                    }
2921                }
2922            }
2923        }
2924        let _ = writeln!(self.out, "}}");
2925        let _ = writeln!(self.out);
2926    }
2927
2928    /// Free the heap-owned payload of a single member instance, reached via the
2929    /// accessor expression `acc` (e.g. `s->name`, `s->names[fi0]`). Resolved
2930    /// type drives the shape; primitives/enums/nested structs hold no heap.
2931    fn emit_free_member(&mut self, acc: &str, resolved: &TypeSpec) {
2932        match resolved {
2933            TypeSpec::String(_) => {
2934                // Both string (char*) and wstring (uint16_t*) are heap.
2935                let _ = writeln!(self.out, "    free({acc}); {acc} = NULL;");
2936            }
2937            TypeSpec::Sequence(seq) => {
2938                if matches!(resolve_alias(self.reg, &seq.elem), TypeSpec::String(_)) {
2939                    let _ = writeln!(
2940                        self.out,
2941                        "    for (uint32_t fsi = 0; fsi < ({acc}).len; ++fsi) free(({acc}).elems[fsi]);"
2942                    );
2943                }
2944                let _ = writeln!(
2945                    self.out,
2946                    "    free(({acc}).elems); ({acc}).elems = NULL; ({acc}).len = 0;"
2947                );
2948            }
2949            TypeSpec::Map(m) => {
2950                if matches!(resolve_alias(self.reg, &m.key), TypeSpec::String(_)) {
2951                    let _ = writeln!(
2952                        self.out,
2953                        "    for (uint32_t fki = 0; fki < ({acc}).len; ++fki) free(({acc}).keys[fki]);"
2954                    );
2955                }
2956                if matches!(resolve_alias(self.reg, &m.value), TypeSpec::String(_)) {
2957                    let _ = writeln!(
2958                        self.out,
2959                        "    for (uint32_t fvi = 0; fvi < ({acc}).len; ++fvi) free(({acc}).vals[fvi]);"
2960                    );
2961                }
2962                let _ = writeln!(self.out, "    free(({acc}).keys); ({acc}).keys = NULL;");
2963                let _ = writeln!(
2964                    self.out,
2965                    "    free(({acc}).vals); ({acc}).vals = NULL; ({acc}).len = 0;"
2966                );
2967            }
2968            _ => {}
2969        }
2970    }
2971
2972    fn emit_key_hash_body(&mut self, c_name: &str, def: &StructDef) {
2973        // Spec §7.6.8: collects  members in PlainCdr2BeKeyHolder, then
2974        // either zero-pad or MD5. We use the XCDR2 C helpers.
2975        let _ = writeln!(
2976            self.out,
2977            "static int {c_name}_key_hash(const void* sample, uint8_t out_hash[16]) {{"
2978        );
2979        let _ = writeln!(
2980            self.out,
2981            "    const {c_name}_t* s = (const {c_name}_t*)sample;"
2982        );
2983        let _ = writeln!(self.out, "    uint8_t* kh_buf = NULL;");
2984        let _ = writeln!(self.out, "    size_t kh_len = 0;");
2985        let _ = writeln!(self.out, "    size_t kh_cap = 0;");
2986        for member in &def.members {
2987            if !is_key(&member.annotations) {
2988                continue;
2989            }
2990            for decl in &member.declarators {
2991                let f = &decl.name().text;
2992                match &member.type_spec {
2993                    TypeSpec::Primitive(p) => {
2994                        let helper = match primitive_helper(*p) {
2995                            Ok(h) => h,
2996                            Err(_) => continue,
2997                        };
2998                        let _ = writeln!(
2999                            self.out,
3000                            "    if (zerodds_xcdr2_c_kh_write_{helper}(&kh_buf, &kh_len, &kh_cap, s->{f}) != 0) {{ free(kh_buf); return -1; }}"
3001                        );
3002                    }
3003                    TypeSpec::String(_) => {
3004                        let _ = writeln!(
3005                            self.out,
3006                            "    if (zerodds_xcdr2_c_kh_write_string(&kh_buf, &kh_len, &kh_cap, s->{f}) != 0) {{ free(kh_buf); return -1; }}"
3007                        );
3008                    }
3009                    _ => {}
3010                }
3011            }
3012        }
3013        let _ = writeln!(
3014            self.out,
3015            "    zerodds_xcdr2_c_compute_key_hash(kh_buf, kh_len, /*max_size_static=*/0, out_hash);"
3016        );
3017        let _ = writeln!(self.out, "    free(kh_buf);");
3018        let _ = writeln!(self.out, "    return 0;");
3019        let _ = writeln!(self.out, "}}");
3020        let _ = writeln!(self.out);
3021    }
3022}
3023
3024// ============================================================================
3025// Helpers
3026// ============================================================================
3027
3028/// An aggregate definition (struct or union) for the typedef-ordering pre-pass.
3029#[derive(Clone)]
3030enum AggDef {
3031    Struct(StructDef),
3032    Union(UnionDef),
3033}
3034
3035/// Collect every struct/union definition with its module scope, in declaration
3036/// order, for the aggregate-typedef pre-pass.
3037/// zerodds-lint: recursion-depth 64 (module tree; bounded by IDL nesting)
3038fn collect_aggregates(
3039    defs: &[Definition],
3040    scope: &[String],
3041    out: &mut Vec<(String, Vec<String>, AggDef)>,
3042) {
3043    for d in defs {
3044        match d {
3045            Definition::Module(m) => {
3046                let mut s = scope.to_vec();
3047                s.push(m.name.text.clone());
3048                collect_aggregates(&m.definitions, &s, out);
3049            }
3050            Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Def(s)))) => {
3051                out.push((
3052                    s.name.text.clone(),
3053                    scope.to_vec(),
3054                    AggDef::Struct(s.clone()),
3055                ));
3056            }
3057            Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Union(UnionDcl::Def(u)))) => {
3058                out.push((
3059                    u.name.text.clone(),
3060                    scope.to_vec(),
3061                    AggDef::Union(u.clone()),
3062                ));
3063            }
3064            _ => {}
3065        }
3066    }
3067}
3068
3069fn dds_type_name(scope: &[String], name: &str) -> String {
3070    if scope.is_empty() {
3071        name.to_string()
3072    } else {
3073        let mut s = scope.join("::");
3074        s.push_str("::");
3075        s.push_str(name);
3076        s
3077    }
3078}
3079
3080/// The TypeSpec a union discriminator maps to (so the discriminator can reuse
3081/// the generic member writer/reader + C type mapping).
3082fn switch_type_spec(s: &SwitchTypeSpec) -> TypeSpec {
3083    match s {
3084        SwitchTypeSpec::Integer(it) => TypeSpec::Primitive(PrimitiveType::Integer(*it)),
3085        SwitchTypeSpec::Char => TypeSpec::Primitive(PrimitiveType::Char),
3086        SwitchTypeSpec::Boolean => TypeSpec::Primitive(PrimitiveType::Boolean),
3087        SwitchTypeSpec::Octet => TypeSpec::Primitive(PrimitiveType::Octet),
3088        SwitchTypeSpec::Scoped(sn) => TypeSpec::Scoped(sn.clone()),
3089    }
3090}
3091
3092/// The C field name for a union case's member (the case's declarator name).
3093fn union_case_field(case: &Case) -> String {
3094    case.element.declarator.name().text.clone()
3095}
3096
3097/// Join a scope + name with the given separator (`::` for IDL paths).
3098fn scope_join(scope: &[String], name: &str, sep: &str) -> String {
3099    if scope.is_empty() {
3100        name.to_string()
3101    } else {
3102        let mut s = scope.join(sep);
3103        s.push_str(sep);
3104        s.push_str(name);
3105        s
3106    }
3107}
3108
3109fn c_identifier(scope: &[String], name: &str) -> String {
3110    if scope.is_empty() {
3111        name.to_string()
3112    } else {
3113        let mut s = scope.join("_");
3114        s.push('_');
3115        s.push_str(name);
3116        s
3117    }
3118}
3119
3120impl Extensibility {
3121    fn as_u8(self) -> u8 {
3122        match self {
3123            Self::Final => 0,
3124            Self::Appendable => 1,
3125            Self::Mutable => 2,
3126        }
3127    }
3128}
3129
3130fn extensibility_of(annotations: &[Annotation]) -> Extensibility {
3131    for a in annotations {
3132        if let Some(name) = a.name.parts.last() {
3133            match name.text.as_str() {
3134                "final" | "Final" => return Extensibility::Final,
3135                "appendable" | "Appendable" => return Extensibility::Appendable,
3136                "mutable" | "Mutable" => return Extensibility::Mutable,
3137                _ => {}
3138            }
3139        }
3140    }
3141    // Un-annotated default: FINAL. XTypes 1.3 §7.2.2.4.4 leaves the
3142    // extensibility of an un-annotated aggregate implementation-defined; the
3143    // canonical zerodds choice — anchored to the `zerodds-cdr` core and the
3144    // idl-rust reference (whose hardcoded default is Final, see
3145    // tools/idlc/src/default_ext.rs) — is FINAL, matching the rust golden:
3146    // SX2: spec default for an unannotated aggregate is APPENDABLE (§7.3.3.1).
3147    // `--default-extensibility final` opts back to the no-DHEADER form.
3148    Extensibility::Appendable
3149}
3150
3151fn is_optional(annotations: &[Annotation]) -> bool {
3152    annotations.iter().any(|a| {
3153        a.name
3154            .parts
3155            .last()
3156            .is_some_and(|p| p.text == "optional" || p.text == "Optional")
3157    })
3158}
3159
3160/// XCDR2 wire byte size of a primitive scalar (XTypes 1.3 §7.4.1). Used to pick
3161/// the compact EMHEADER length code for a `@mutable` member.
3162fn primitive_wire_bytes(p: PrimitiveType) -> u32 {
3163    match p {
3164        PrimitiveType::Boolean | PrimitiveType::Octet | PrimitiveType::Char => 1,
3165        PrimitiveType::WideChar => 2,
3166        PrimitiveType::Integer(IntegerType::Int8 | IntegerType::UInt8) => 1,
3167        PrimitiveType::Integer(
3168            IntegerType::Short | IntegerType::Int16 | IntegerType::UShort | IntegerType::UInt16,
3169        ) => 2,
3170        PrimitiveType::Integer(
3171            IntegerType::Long | IntegerType::Int32 | IntegerType::ULong | IntegerType::UInt32,
3172        ) => 4,
3173        PrimitiveType::Integer(
3174            IntegerType::LongLong
3175            | IntegerType::Int64
3176            | IntegerType::ULongLong
3177            | IntegerType::UInt64,
3178        ) => 8,
3179        PrimitiveType::Floating(FloatingType::Float) => 4,
3180        PrimitiveType::Floating(FloatingType::Double) => 8,
3181        // long double (16 bytes) is not a compact code → falls back to LC4.
3182        PrimitiveType::Floating(FloatingType::LongDouble) => 16,
3183    }
3184}
3185
3186/// Picks the compact XTypes 1.3 §7.4.3.4.2 length code for a `@mutable` member,
3187/// mirroring the Rust reference `mutable_member_length_code` (Bug XV-mut). Only
3188/// non-array, non-optional scalar primitives (LC0..3 by wire size) and
3189/// strings/wstrings (LC5, reusing the leading uint32 length prefix as the
3190/// NEXTINT) are eligible. Returns `None` for everything else → universal LC4.
3191fn mutable_compact_lc(
3192    reg: &TypeReg,
3193    type_spec: &TypeSpec,
3194    dims: &[u64],
3195    optional: bool,
3196) -> Option<u8> {
3197    if !dims.is_empty() || optional {
3198        return None;
3199    }
3200    let resolved = resolve_alias(reg, type_spec);
3201    match &resolved {
3202        TypeSpec::Primitive(p) => match primitive_wire_bytes(*p) {
3203            1 => Some(0),
3204            2 => Some(1),
3205            4 => Some(2),
3206            8 => Some(3),
3207            _ => None, // long double → LC4
3208        },
3209        // FINDING T1b (mirrors idl-rust `mutable_member_length_code` +
3210        // `member_body_has_leading_dheader`): a member whose XCDR2 body begins
3211        // with a 4-byte length word — a string/wstring length prefix, a
3212        // non-primitive sequence/map DHEADER, or a nested @appendable/@mutable
3213        // struct's DHEADER — uses LC5 to REUSE that word as the NEXTINT (no
3214        // redundant NEXTINT), matching CycloneDDS / RTI / FastDDS. A @final
3215        // nested struct (no DHEADER) and a sequence<primitive> (bare element
3216        // count, not a byte length) fall through to the universal LC4.
3217        _ if member_body_has_leading_dheader_c(reg, &resolved) => Some(5),
3218        _ => None,
3219    }
3220}
3221
3222/// `true` if `type_spec`'s XCDR2 body begins with a 4-byte length/DHEADER word,
3223/// making it eligible for EMHEADER LengthCode 5 (the leading word doubles as the
3224/// NEXTINT). Mirrors idl-rust `type_map::member_body_has_leading_dheader`:
3225///   * string / wstring — leading uint32 octet-length prefix,
3226///   * map<K,V> — always a non-primitive aggregate → DHEADER,
3227///   * sequence<E> with NON-primitive `E` — XCDR2 DHEADER (sequence<primitive>
3228///     has only a bare element count, not a byte length → stays LC4),
3229///   * a nested struct (chasing typedef) whose extensibility is @appendable /
3230///     @mutable — self-delimits with a leading DHEADER; a @final nested struct
3231///     tight-packs its body (no DHEADER) → LC4.
3232///
3233/// zerodds-lint: recursion-depth 16
3234fn member_body_has_leading_dheader_c(reg: &TypeReg, type_spec: &TypeSpec) -> bool {
3235    match resolve_alias(reg, type_spec) {
3236        TypeSpec::String(_) => true,
3237        TypeSpec::Map(_) => true,
3238        TypeSpec::Sequence(seq) => !seq_elem_is_primitive_reg(reg, &seq.elem),
3239        TypeSpec::Scoped(sc) => scoped_last(&sc)
3240            .and_then(|last| reg.structs.get(&last).cloned())
3241            .is_some_and(|(_, ndef)| {
3242                !matches!(extensibility_of(&ndef.annotations), Extensibility::Final)
3243            }),
3244        _ => false,
3245    }
3246}
3247
3248/// Free-function twin of `CSink::seq_elem_is_primitive` (a sequence element is
3249/// "primitive" — and carries no DHEADER — when it is an IDL primitive, enum,
3250/// bitmask or bitset). Kept standalone so the length-code picker can run without
3251/// a `&self` borrow.
3252fn seq_elem_is_primitive_reg(reg: &TypeReg, elem: &TypeSpec) -> bool {
3253    match resolve_alias(reg, elem) {
3254        TypeSpec::Primitive(_) => true,
3255        TypeSpec::Scoped(sc) => scoped_last(&sc).is_some_and(|last| {
3256            reg.enums.contains_key(&last)
3257                || reg.bitmasks.contains_key(&last)
3258                || reg.bitsets.contains_key(&last)
3259        }),
3260        _ => false,
3261    }
3262}
3263
3264/// Fixed-array dimensions of a declarator (empty for a simple declarator).
3265///
3266/// A bound may be a literal **or a named constant** (`long v[N]`) — the latter
3267/// is resolved through the const symbol table the way every other backend does
3268/// (Bug C #43, const-array-bound).
3269fn array_dims(reg: &TypeReg, decl: &Declarator) -> Result<Vec<u64>, CppGenError> {
3270    match decl {
3271        Declarator::Simple(_) => Ok(Vec::new()),
3272        Declarator::Array(arr) => {
3273            let mut dims = Vec::with_capacity(arr.sizes.len());
3274            for sz in &arr.sizes {
3275                let n = const_expr_to_u64(reg, sz)
3276                    .ok_or_else(|| unsupported("non-literal array bound"))?;
3277                dims.push(n);
3278            }
3279            Ok(dims)
3280        }
3281    }
3282}
3283
3284/// Effective fixed-array dimensions for a member, combining a typedef-to-array
3285/// alias's dims with the declarator's own dims. `Matrix3 transform;` where
3286/// `typedef long Matrix3[3][3];` yields `[3,3]` even though the member's own
3287/// declarator is simple (#43, typedef-to-array). Alias dims come first
3288/// (outermost), then the declarator dims, matching C array nesting order.
3289fn effective_array_dims(
3290    reg: &TypeReg,
3291    type_spec: &TypeSpec,
3292    decl: &Declarator,
3293) -> Result<Vec<u64>, CppGenError> {
3294    let mut dims = Vec::new();
3295    if let TypeSpec::Scoped(sc) = type_spec {
3296        if let Some(last) = scoped_last(sc) {
3297            if let Some(td) = reg.typedef_arrays.get(&last) {
3298                dims.extend_from_slice(td);
3299            }
3300        }
3301    }
3302    dims.extend(array_dims(reg, decl)?);
3303    Ok(dims)
3304}
3305
3306/// Smallest `uintN_t` holder for a bit width (XTypes §7.3.1.2 / Tab. 7.12).
3307fn holder_uint_c_type(bits: u32) -> &'static str {
3308    match bits {
3309        0..=8 => "uint8_t",
3310        9..=16 => "uint16_t",
3311        17..=32 => "uint32_t",
3312        _ => "uint64_t",
3313    }
3314}
3315
3316/// The C holder type for a bitmask: smallest uint that fits `@bit_bound`
3317/// (default 32). XTypes §7.3.1.2.2 — a bitmask serializes as this integer.
3318fn bitmask_holder_c_type(b: &BitmaskDecl) -> &'static str {
3319    // Holder width: an explicit `@bit_bound(N)` is authoritative; otherwise the
3320    // spec DEFAULT is @bit_bound=32 (Bug XV-bits, XTypes 1.3 §7.3.1.2.1.6) → a
3321    // UInt32 (4-byte) holder, NOT a width sized to the value count. Byte-identical
3322    // wire to the corrected rust golden (Perm => uint32_t).
3323    let bound = extract_int_annotation_c(&b.annotations, "bit_bound").unwrap_or(32);
3324    holder_uint_c_type(bound)
3325}
3326
3327/// The C holder type for a bitset: smallest uint that fits the sum of all
3328/// bitfield widths. XTypes §7.3.1.2.1 — a bitset serializes as one packed
3329/// integer (over 64 bits is out of the C/XTypes profile and saturates to u64).
3330fn bitset_holder_c_type(b: &BitsetDecl) -> &'static str {
3331    let mut total: u32 = 0;
3332    for f in &b.bitfields {
3333        if let ConstExpr::Literal(l) = &f.spec.width {
3334            if l.kind == LiteralKind::Integer {
3335                if let Ok(w) = l.raw.trim().parse::<u32>() {
3336                    total = total.saturating_add(w);
3337                }
3338            }
3339        }
3340    }
3341    holder_uint_c_type(total)
3342}
3343
3344/// Extract a single integer annotation parameter (`@bit_bound(32)` /
3345/// `@position(3)`) — used by the bitset/bitmask holder sizing.
3346fn extract_int_annotation_c(anns: &[Annotation], name: &str) -> Option<u32> {
3347    let a = anns
3348        .iter()
3349        .find(|a| a.name.parts.last().map(|p| p.text.as_str()) == Some(name))?;
3350    if let AnnotationParams::Single(ConstExpr::Literal(l)) = &a.params {
3351        if l.kind == LiteralKind::Integer {
3352            return l.raw.trim().parse::<u32>().ok();
3353        }
3354    }
3355    None
3356}
3357
3358/// Best-effort literal/const fold during the collection pre-pass (the const
3359/// table is still being built, so this only resolves literals + already-seen
3360/// consts). Used to size a `typedef T A[N];` array-alias.
3361fn const_expr_to_u64_pre(consts: &SymbolTable, e: &ConstExpr) -> Option<u64> {
3362    if let ConstExpr::Literal(l) = e {
3363        if l.kind == LiteralKind::Integer {
3364            let raw = l.raw.trim();
3365            return if let Some(hex) = raw.strip_prefix("0x").or_else(|| raw.strip_prefix("0X")) {
3366                u64::from_str_radix(hex, 16).ok()
3367            } else {
3368                raw.parse::<u64>().ok()
3369            };
3370        }
3371    }
3372    evaluate_positive_int(e, consts, e.span()).ok()
3373}
3374
3375fn const_expr_to_u64(reg: &TypeReg, e: &ConstExpr) -> Option<u64> {
3376    // Literal fast path (also covers hex).
3377    if let ConstExpr::Literal(l) = e {
3378        if l.kind == LiteralKind::Integer {
3379            let raw = l.raw.trim();
3380            let parsed =
3381                if let Some(hex) = raw.strip_prefix("0x").or_else(|| raw.strip_prefix("0X")) {
3382                    u64::from_str_radix(hex, 16).ok()
3383                } else {
3384                    raw.parse::<u64>().ok()
3385                };
3386            if let Some(v) = parsed {
3387                return Some(v);
3388            }
3389        }
3390    }
3391    // Named-constant / expression path: evaluate against the const symbol table
3392    // (handles `const long N = 4; long v[N];` and arithmetic like `N*2`).
3393    evaluate_positive_int(e, &reg.consts, e.span()).ok()
3394}
3395
3396fn is_key(annotations: &[Annotation]) -> bool {
3397    annotations.iter().any(|a| {
3398        a.name
3399            .parts
3400            .last()
3401            .is_some_and(|p| p.text == "key" || p.text == "Key")
3402    })
3403}
3404
3405fn struct_has_key(def: &StructDef) -> bool {
3406    def.members.iter().any(|m| is_key(&m.annotations))
3407}
3408
3409fn id_annotation(annotations: &[Annotation]) -> Option<u32> {
3410    for a in annotations {
3411        let last = a.name.parts.last()?;
3412        if last.text != "id" && last.text != "Id" {
3413            continue;
3414        }
3415        if let AnnotationParams::Single(ConstExpr::Literal(lit)) = &a.params {
3416            if lit.kind == LiteralKind::Integer {
3417                if let Ok(v) = lit.raw.parse::<u32>() {
3418                    return Some(v);
3419                }
3420            }
3421        }
3422    }
3423    None
3424}
3425/// zerodds-lint: recursion-depth 64 (c_type_for bounded by AST depth)
3426fn c_type_for(reg: &TypeReg, type_spec: &TypeSpec) -> Result<String, CppGenError> {
3427    let s = match type_spec {
3428        TypeSpec::Scoped(sc) => {
3429            let last = scoped_last(sc).ok_or_else(|| unsupported("empty scoped name"))?;
3430            // enum → int32 alias.
3431            if let Some(cn) = reg.enums.get(&last) {
3432                return Ok(format!("{cn}_t"));
3433            }
3434            // bitmask / bitset → holder-integer typedef.
3435            if let Some((cn, _)) = reg.bitmasks.get(&last) {
3436                return Ok(format!("{cn}_t"));
3437            }
3438            if let Some((cn, _)) = reg.bitsets.get(&last) {
3439                return Ok(format!("{cn}_t"));
3440            }
3441            // typedef → resolve to underlying type. A typedef-to-array alias
3442            // (`typedef long Matrix3[3][3];`) carries the element type here; the
3443            // dims are applied separately at the member declarator via
3444            // `effective_array_dims` (#43, typedef-to-array).
3445            if reg.typedefs.contains_key(&last) {
3446                let resolved = resolve_alias(reg, type_spec);
3447                return c_type_for(reg, &resolved);
3448            }
3449            // nested struct → embed the C struct by value. A RECURSIVE struct
3450            // can only be referenced behind a pointer (sequence element); use
3451            // the struct tag `struct <C>_s*` so the type is valid INSIDE its own
3452            // typedef, before the `<C>_t` alias name exists (#43, recursion).
3453            if let Some((cn, _)) = reg.structs.get(&last) {
3454                if reg.is_recursive(&last) {
3455                    return Ok(format!("struct {cn}_s"));
3456                }
3457                return Ok(format!("{cn}_t"));
3458            }
3459            // union → embed the C tagged-union by value (tag form if recursive).
3460            if let Some((cn, _)) = reg.unions.get(&last) {
3461                if reg.is_recursive(&last) {
3462                    return Ok(format!("struct {cn}_s"));
3463                }
3464                return Ok(format!("{cn}_t"));
3465            }
3466            return Err(unsupported("unresolved scoped type (C backend)"));
3467        }
3468        TypeSpec::Primitive(p) => match p {
3469            PrimitiveType::Boolean => "uint8_t",
3470            PrimitiveType::Octet => "uint8_t",
3471            PrimitiveType::Char => "char",
3472            PrimitiveType::WideChar => "uint16_t",
3473            PrimitiveType::Integer(IntegerType::Short)
3474            | PrimitiveType::Integer(IntegerType::Int16) => "int16_t",
3475            PrimitiveType::Integer(IntegerType::UShort)
3476            | PrimitiveType::Integer(IntegerType::UInt16) => "uint16_t",
3477            PrimitiveType::Integer(IntegerType::Long)
3478            | PrimitiveType::Integer(IntegerType::Int32) => "int32_t",
3479            PrimitiveType::Integer(IntegerType::ULong)
3480            | PrimitiveType::Integer(IntegerType::UInt32) => "uint32_t",
3481            PrimitiveType::Integer(IntegerType::LongLong)
3482            | PrimitiveType::Integer(IntegerType::Int64) => "int64_t",
3483            PrimitiveType::Integer(IntegerType::ULongLong)
3484            | PrimitiveType::Integer(IntegerType::UInt64) => "uint64_t",
3485            PrimitiveType::Integer(IntegerType::Int8) => "int8_t",
3486            PrimitiveType::Integer(IntegerType::UInt8) => "uint8_t",
3487            PrimitiveType::Floating(FloatingType::Float) => "float",
3488            PrimitiveType::Floating(FloatingType::Double) => "double",
3489            PrimitiveType::Floating(FloatingType::LongDouble) => {
3490                return Err(unsupported("long double"));
3491            }
3492        },
3493        TypeSpec::String(st) => {
3494            if st.wide {
3495                // wstring → NUL-terminated UTF-16 (uint16) buffer (#43, wstring).
3496                "uint16_t*"
3497            } else {
3498                "char*"
3499            }
3500        }
3501        TypeSpec::Sequence(seq) => {
3502            let elem = c_type_for(reg, &seq.elem)?;
3503            return Ok(format!("struct {{ uint32_t len; {elem}* elems; }}"));
3504        }
3505        TypeSpec::Map(m) => {
3506            // map<K,V> → parallel key/value arrays + count (#43, map<K,V>).
3507            let kc = c_type_for(reg, &m.key)?;
3508            let vc = c_type_for(reg, &m.value)?;
3509            return Ok(format!(
3510                "struct {{ uint32_t len; {kc}* keys; {vc}* vals; }}"
3511            ));
3512        }
3513        TypeSpec::Fixed(f) => {
3514            // fixed<P,S>: CORBA/GIOP §9.3.2.7 packed BCD, (P+2)/2 raw octets.
3515            // C has no decimal type, so the field IS the BCD byte array; the
3516            // user fills/reads `.bcd[]` (helpers can convert to/from a string).
3517            let p = const_expr_to_u64(reg, &f.digits)
3518                .ok_or_else(|| unsupported("fixed: non-constant digit count"))?;
3519            let n = (p + 2) / 2;
3520            return Ok(format!("struct {{ uint8_t bcd[{n}]; }}"));
3521        }
3522        _ => {
3523            return Err(unsupported("non-primitive type in field"));
3524        }
3525    };
3526    Ok(s.to_string())
3527}
3528
3529fn primitive_helper(p: PrimitiveType) -> Result<&'static str, CppGenError> {
3530    Ok(match p {
3531        PrimitiveType::Boolean | PrimitiveType::Octet => "u8",
3532        PrimitiveType::Char => "u8",
3533        PrimitiveType::WideChar => "u16",
3534        PrimitiveType::Integer(IntegerType::Short) | PrimitiveType::Integer(IntegerType::Int16) => {
3535            "i16"
3536        }
3537        PrimitiveType::Integer(IntegerType::UShort)
3538        | PrimitiveType::Integer(IntegerType::UInt16) => "u16",
3539        PrimitiveType::Integer(IntegerType::Long) | PrimitiveType::Integer(IntegerType::Int32) => {
3540            "i32"
3541        }
3542        PrimitiveType::Integer(IntegerType::ULong)
3543        | PrimitiveType::Integer(IntegerType::UInt32) => "u32",
3544        PrimitiveType::Integer(IntegerType::LongLong)
3545        | PrimitiveType::Integer(IntegerType::Int64) => "i64",
3546        PrimitiveType::Integer(IntegerType::ULongLong)
3547        | PrimitiveType::Integer(IntegerType::UInt64) => "u64",
3548        PrimitiveType::Integer(IntegerType::Int8) => "i8",
3549        PrimitiveType::Integer(IntegerType::UInt8) => "u8",
3550        PrimitiveType::Floating(FloatingType::Float) => "f32",
3551        PrimitiveType::Floating(FloatingType::Double) => "f64",
3552        PrimitiveType::Floating(FloatingType::LongDouble) => {
3553            return Err(unsupported("long double"));
3554        }
3555    })
3556}
3557
3558#[cfg(test)]
3559mod tests {
3560    #![allow(clippy::expect_used)]
3561    use super::*;
3562    use zerodds_idl::config::ParserConfig;
3563
3564    fn gen_c(src: &str) -> String {
3565        let ast = zerodds_idl::parse(src, &ParserConfig::default()).expect("parse ok");
3566        generate_c_header(&ast, &CGenOptions::default()).expect("c-gen ok")
3567    }
3568
3569    #[test]
3570    fn empty_final_struct_emits_typedef_and_typesupport() {
3571        let h = gen_c("@final struct Empty {};");
3572        assert!(h.contains("typedef struct Empty_s"));
3573        assert!(h.contains("Empty_typesupport"));
3574        assert!(h.contains(".extensibility = 0"));
3575    }
3576
3577    #[test]
3578    fn primitive_struct_maps_types() {
3579        let h = gen_c("@final struct Point { long x; long y; };");
3580        assert!(h.contains("int32_t x;"));
3581        assert!(h.contains("int32_t y;"));
3582        assert!(h.contains("\"Point\""));
3583    }
3584
3585    #[test]
3586    fn nested_module_yields_scoped_type_name() {
3587        let h = gen_c("module Outer { module Inner { @final struct S { long x; }; }; };");
3588        assert!(h.contains("typedef struct Outer_Inner_S_s"));
3589        assert!(h.contains("\"Outer::Inner::S\""));
3590    }
3591
3592    #[test]
3593    fn appendable_default_when_no_annotation() {
3594        // SX2: un-annotated aggregates default to APPENDABLE (extensibility = 1,
3595        // XTypes 1.3 §7.3.3.1), self-delimited with a DHEADER, matching the
3596        // TypeObject path + FastDDS/Cyclone.
3597        let h = gen_c("struct V { long a; long b; };");
3598        assert!(h.contains(".extensibility = 1"));
3599    }
3600
3601    #[test]
3602    fn mutable_struct_marks_extensibility() {
3603        let h = gen_c("@mutable struct M { @id(1) long a; };");
3604        assert!(h.contains(".extensibility = 2"));
3605    }
3606
3607    #[test]
3608    fn key_member_sets_is_keyed_and_emits_key_hash() {
3609        let h = gen_c("@final struct Sensor { @key long id; double value; };");
3610        assert!(h.contains(".is_keyed = 1"));
3611        assert!(h.contains("Sensor_key_hash"));
3612    }
3613
3614    // ---- Bug C: scope widening (no longer rejected) ----
3615
3616    #[test]
3617    fn enum_member_no_longer_rejected() {
3618        // Previously: `non-struct type` / `nested struct reference` error.
3619        let h = gen_c("enum Color { RED, GREEN, BLUE }; @final struct S { Color c; };");
3620        assert!(h.contains("typedef int32_t Color_t;"));
3621        assert!(h.contains("Color_GREEN = 1"));
3622        // field typed as the int32 alias, encoded as i32.
3623        assert!(h.contains("Color_t c;"));
3624        assert!(h.contains("zerodds_xcdr2_c_write_i32(&w_buf, &w_len, &w_cap, (int32_t)(s->c))"));
3625        assert!(h.contains("zerodds_xcdr2_c_read_i32(buf, len, &pos, (int32_t*)&(s->c), zd_be)"));
3626    }
3627
3628    #[test]
3629    fn typedef_member_resolves_to_underlying() {
3630        let h = gen_c("typedef double Amps; @final struct S { Amps battery; };");
3631        assert!(h.contains("double battery;"));
3632        // 8-byte primitives use the XCDR2 align-4 helper `zd_x2_write_f64`
3633        // (MAXALIGN=min(8,4)=4, §7.4.1.1.1), NOT the shared align-8 `write_f64`.
3634        assert!(h.contains("zd_x2_write_f64(&w_buf, &w_len, &w_cap, s->battery)"));
3635        assert!(h.contains("static inline int zd_x2_write_f64"));
3636    }
3637
3638    #[test]
3639    fn nested_struct_member_inlined() {
3640        let h = gen_c(
3641            "@final struct Point { long x; long y; }; @final struct Line { Point a; Point b; };",
3642        );
3643        assert!(h.contains("Point_t a;"));
3644        assert!(h.contains("Point_t b;"));
3645        // inline-encoded by value (sub-member access through the field).
3646        assert!(h.contains("(s->a).x"));
3647        assert!(h.contains("(s->b).y"));
3648    }
3649
3650    #[test]
3651    fn fixed_array_member_no_length_prefix() {
3652        let h = gen_c("@final struct G { long grid[3]; };");
3653        assert!(h.contains("int32_t grid[3];"));
3654        // a loop over the fixed extent, no u32 length write.
3655        assert!(h.contains("ai0 = 0; ai0 < 3"));
3656    }
3657
3658    // ---- Bug C2: @optional + header conflict ----
3659
3660    #[test]
3661    fn optional_member_gets_presence_flag() {
3662        let h = gen_c("@final struct O { @optional long maybe; };");
3663        assert!(h.contains("int32_t maybe;"));
3664        assert!(h.contains("uint8_t maybe_present;"));
3665        // wire: presence boolean, then the value only if present.
3666        assert!(h.contains("s->maybe_present ? 1 : 0"));
3667        assert!(h.contains("if (s->maybe_present) {"));
3668    }
3669
3670    #[test]
3671    fn header_does_not_include_conflicting_zerodds_h() {
3672        // Bug C2: the cbindgen `zerodds.h` redeclares typed-FFI fns with names
3673        // conflicting with `zerodds_xcdr2.h`; only the latter is needed.
3674        let h = gen_c("@final struct S { long x; };");
3675        assert!(h.contains("#include \"zerodds_xcdr2.h\""));
3676        assert!(!h.contains("#include \"zerodds.h\""));
3677    }
3678
3679    // ---- #43: C-Foundation widening — union / map / wstring / const-bound ----
3680
3681    #[test]
3682    fn union_emits_tagged_union_codec() {
3683        // Unions are now in scope (#43): a tagged-union typedef + TypeSupport.
3684        let h = gen_c("union U switch (long) { case 1: long a; default: long b; };");
3685        assert!(h.contains("typedef struct U_s"));
3686        assert!(h.contains("} _u;"));
3687        assert!(h.contains("U_typesupport"));
3688        // Discriminator is switched on in encode/decode.
3689        assert!(h.contains("._d) {"));
3690        assert!(h.contains("case 1:"));
3691        assert!(h.contains("default:"));
3692    }
3693
3694    #[test]
3695    fn const_array_bound_resolves_to_literal() {
3696        // `long v[N]` with `const long N = 4` resolves the bound (#43).
3697        let h = gen_c("const long N = 4; @final struct A { long v[N]; };");
3698        assert!(
3699            h.contains("int32_t v[4];"),
3700            "named const bound not folded:\n{h}"
3701        );
3702    }
3703
3704    #[test]
3705    fn map_member_emits_parallel_arrays_and_codec() {
3706        let h = gen_c("@final struct M { map<string, long> counters; };");
3707        assert!(h.contains("keys;"));
3708        assert!(h.contains("vals;"));
3709        // DHEADER + count emitted for the map body.
3710        assert!(h.contains("map_dheader_pos"));
3711    }
3712
3713    #[test]
3714    fn wstring_member_maps_to_uint16_ptr() {
3715        let h = gen_c("@final struct W { wstring label; };");
3716        assert!(h.contains("uint16_t* label;"));
3717        // wire: byte-length then uint16 code units.
3718        assert!(h.contains("ws_n * 2u"));
3719    }
3720
3721    #[test]
3722    fn sequence_of_struct_resolves_element_type() {
3723        let h = gen_c("@final struct P { long x; }; @final struct S { sequence<P> ps; };");
3724        assert!(h.contains("P_t* elems; } ps;"));
3725        // element body inline-encodes the struct sub-member.
3726        assert!(h.contains(".elems[i0]).x") || h.contains(".elems[i0])"));
3727    }
3728
3729    #[test]
3730    fn typedef_to_aggregate_resolves_to_struct() {
3731        let h = gen_c(
3732            "@final struct Point { long x; }; typedef Point Position; \
3733             @final struct S { Position p; };",
3734        );
3735        assert!(h.contains("Point_t p;"));
3736    }
3737
3738    // ---- #43: the last four Foundation fixtures (typedefs / bits / recursion
3739    // / forward-decl) ----
3740
3741    #[test]
3742    fn typedef_alias_chain_resolves_to_root() {
3743        let h = gen_c("typedef double A; typedef A B; @final struct S { B v; };");
3744        // The alias-of-alias resolves to the root primitive `double`.
3745        assert!(h.contains("double v;"), "alias chain not resolved:\n{h}");
3746    }
3747
3748    #[test]
3749    fn typedef_to_array_contributes_dims_at_use_site() {
3750        let h = gen_c("typedef long Matrix3[3][3]; @final struct S { Matrix3 m; };");
3751        assert!(
3752            h.contains("int32_t m[3][3];"),
3753            "typedef-to-array dims lost:\n{h}"
3754        );
3755    }
3756
3757    #[test]
3758    fn bitmask_maps_to_holder_uint_and_flag_constants() {
3759        let h = gen_c(
3760            "bitmask Permissions { READ, WRITE, EXECUTE }; @final struct S { Permissions p; };",
3761        );
3762        // No explicit @bit_bound → spec DEFAULT @bit_bound=32 (Bug XV-bits,
3763        // XTypes 1.3 §7.3.1.2.1.6) → a UInt32 holder, NOT a width sized to the
3764        // value count. flag = 1 << position.
3765        assert!(
3766            h.contains("typedef uint32_t Permissions_t;"),
3767            "holder uint:\n{h}"
3768        );
3769        assert!(
3770            h.contains("Permissions_WRITE = (1u << 1)"),
3771            "flag bit:\n{h}"
3772        );
3773        assert!(h.contains("Permissions_t p;"), "member type:\n{h}");
3774        // Serialized as the holder integer (u32), no DHEADER.
3775        assert!(h.contains("zerodds_xcdr2_c_write_u32(&w_buf, &w_len, &w_cap, s->p)"));
3776    }
3777
3778    #[test]
3779    fn bitset_maps_to_packed_holder_and_accessors() {
3780        let h = gen_c(
3781            "bitset Flags { bitfield<3> kind; bitfield<1> active; bitfield<4> priority; }; \
3782             @final struct S { Flags f; };",
3783        );
3784        // 3+1+4 = 8 bits → uint8 holder; SHIFT/MASK accessor per named field.
3785        assert!(
3786            h.contains("typedef uint8_t Flags_t;"),
3787            "packed holder:\n{h}"
3788        );
3789        assert!(h.contains("Flags_active_SHIFT = 3"), "field offset:\n{h}");
3790        assert!(
3791            h.contains("#define Flags_priority_MASK 0xFu"),
3792            "field mask:\n{h}"
3793        );
3794        assert!(h.contains("zerodds_xcdr2_c_write_u8(&w_buf, &w_len, &w_cap, s->f)"));
3795    }
3796
3797    #[test]
3798    fn recursive_type_through_sequence_splices_via_helper() {
3799        let h = gen_c("struct TreeNode { long value; sequence<TreeNode> children; };");
3800        // Self-reference is a pointer-to-tag, spliced through a runtime helper.
3801        assert!(
3802            h.contains("struct TreeNode_s* elems;"),
3803            "pointer-to-tag elem:\n{h}"
3804        );
3805        assert!(
3806            h.contains("static int TreeNode_write_body("),
3807            "write helper:\n{h}"
3808        );
3809        assert!(
3810            h.contains("static int TreeNode_read_body("),
3811            "read helper:\n{h}"
3812        );
3813        assert!(
3814            h.contains("TreeNode_write_body(&((s->children).elems[i0])"),
3815            "recursive splice call:\n{h}"
3816        );
3817    }
3818
3819    #[test]
3820    fn forward_declared_then_defined_struct_and_union_generate() {
3821        // Forward decls then defs; the union embeds the (recursive) struct by
3822        // value, so the struct typedef must precede the union typedef.
3823        let h = gen_c(
3824            "module conf { \
3825                struct Node; union Variant; \
3826                struct Node { long value; sequence<Node> next; }; \
3827                union Variant switch (long) { case 0: long a; case 1: Node n; }; \
3828             };",
3829        );
3830        assert!(
3831            h.contains("typedef struct conf_Node_s"),
3832            "node typedef:\n{h}"
3833        );
3834        assert!(
3835            h.contains("typedef struct conf_Variant_s"),
3836            "variant typedef:\n{h}"
3837        );
3838        // Node typedef must come before Variant typedef (by-value embed order).
3839        let ni = h
3840            .find("typedef struct conf_Node_s")
3841            .expect("Node typedef present");
3842        let vi = h
3843            .find("typedef struct conf_Variant_s")
3844            .expect("Variant typedef present");
3845        assert!(ni < vi, "Node typedef must precede Variant typedef");
3846    }
3847
3848    #[test]
3849    fn direct_by_value_self_membership_is_rejected() {
3850        // `struct Node { Node n; };` is an infinite-size type — rejected cleanly.
3851        let ast = zerodds_idl::parse("struct Node { long v; Node n; };", &ParserConfig::default());
3852        if let Ok(ast) = ast {
3853            assert!(
3854                generate_c_header(&ast, &CGenOptions::default()).is_err(),
3855                "infinite by-value self-membership must be rejected"
3856            );
3857        }
3858    }
3859
3860    #[test]
3861    fn fixed_decimal_is_a_bcd_byte_field() {
3862        // fixed<P,S> now maps to a (P+2)/2-octet BCD byte field (CORBA §9.3.2.7)
3863        // with a raw encode/decode loop. `any` stays out of the C profile.
3864        let ast = zerodds_idl::parse(
3865            "@final struct S { fixed<5,2> price; };",
3866            &ParserConfig::default(),
3867        )
3868        .expect("parse");
3869        let c = generate_c_header(&ast, &CGenOptions::default()).expect("gen");
3870        assert!(
3871            c.contains("uint8_t bcd[3]"),
3872            "fixed<5,2> -> 3-octet BCD field:\n{c}"
3873        );
3874        assert!(
3875            c.contains("zerodds_xcdr2_c_write_u8") && c.contains("zerodds_xcdr2_c_read_u8"),
3876            "fixed must emit a raw BCD write/read loop"
3877        );
3878    }
3879
3880    #[test]
3881    fn any_remains_out_of_scope() {
3882        // `any` (CORBA TypeCode + dynamic value) is NOT in the C profile.
3883        let ast = zerodds_idl::parse("@final struct S { any value; };", &ParserConfig::default())
3884            .expect("parse");
3885        assert!(generate_c_header(&ast, &CGenOptions::default()).is_err());
3886    }
3887}