Skip to main content

zerodds_idl_cpp/
emitter.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! AST walker that emits C++17 headers.
4//!
5//! Block-A: Header layout (`#pragma once`, includes, namespaces).
6//! Block-B: Primitive mapping (delegated to [`crate::type_map`]).
7//! Block-C: struct/enum/union/typedef/sequence/array/inheritance.
8//! Block-D: Exception → `class X : public std::exception`.
9//! Block-E: Time/Duration via DDS::Time_t / DDS::Duration_t.
10//!
11//! Emission is single-pass: all required standard includes are collected
12//! via a pre-walk, then the body is emitted. This keeps the header
13//! preamble deterministic (alphabetically sorted).
14
15use core::cell::RefCell;
16use std::collections::{BTreeMap, BTreeSet};
17use std::fmt::Write;
18
19use zerodds_idl::ast::{
20    Annotation, AnnotationParams, Case, CaseLabel, ConstExpr, ConstrTypeDecl, Declarator,
21    Definition, EnumDef, ExceptDecl, Export, InterfaceDcl, InterfaceDef, Literal, LiteralKind,
22    Member, ModuleDef, OpDecl, ParamAttribute, PrimitiveType, ScopedName, Specification,
23    StateVisibility, StructDcl, StructDef, SwitchTypeSpec, TypeDecl, TypeSpec, TypedefDecl,
24    UnionDcl, UnionDef, ValueDef, ValueElement,
25};
26
27use zerodds_idl::semantics::annotations::PlacementKind;
28
29use crate::bitset::{emit_bitmask, emit_bitset};
30use crate::error::CppGenError;
31use crate::type_map::{check_identifier, is_reserved, primitive_to_cpp};
32use crate::verbatim::emit_verbatim_at;
33use crate::{CppGenOptions, TIME_DURATION_TYPES};
34
35/// Known header includes. Order is stably alphabetical.
36#[derive(Debug, Default, Clone)]
37struct Includes {
38    headers: BTreeSet<&'static str>,
39}
40
41impl Includes {
42    fn add(&mut self, h: &'static str) {
43        self.headers.insert(h);
44    }
45}
46
47/// Main entry point: generates the complete C++ header.
48/// `true` if `ts` is (or, through sequence nesting, contains) a bounded
49/// collection that the encoder enforces: a bounded `sequence<T, N>` or a bounded
50/// narrow `string<N>`. Drives the conditional `<stdexcept>` include.
51///
52/// zerodds-lint: recursion-depth 64 (bounded by IDL nesting)
53fn type_has_bounded_collection(ts: &TypeSpec) -> bool {
54    match ts {
55        TypeSpec::Sequence(s) => s.bound.is_some() || type_has_bounded_collection(&s.elem),
56        // narrow `string<N>` AND wide `wstring<N>` both throw on over-bound.
57        TypeSpec::String(s) => s.bound.is_some(),
58        _ => false,
59    }
60}
61
62/// `true` if any topic struct in `spec` has a member with a bounded collection.
63fn spec_has_bounded_collection(spec: &Specification) -> bool {
64    let mut structs: Vec<(String, &StructDef)> = Vec::new();
65    collect_topic_structs(&spec.definitions, "", &mut structs);
66    structs.iter().any(|(_, s)| {
67        s.members
68            .iter()
69            .any(|m| type_has_bounded_collection(&m.type_spec))
70    })
71}
72
73pub(crate) fn emit_header(
74    spec: &Specification,
75    opts: &CppGenOptions,
76) -> Result<String, CppGenError> {
77    // 1. Detect colliding inheritance cycles (before emission).
78    detect_inheritance_cycles(spec)?;
79
80    // 1b. Codegen-scoped type registry (enum/struct simple-names) so the XCDR2
81    //     member encoder can classify a `Scoped` member as an enum.
82    set_type_registry(spec);
83
84    // 2. Walk pre-pass: collect includes.
85    let mut includes = Includes::default();
86    includes.add("<cstdint>"); // stdint.h is always present (uint8_t etc.).
87    collect_includes(spec, &mut includes);
88    // Bounded collections throw std::length_error on over-bound encode
89    // (DDS-XTypes §7.4.3) — pull <stdexcept> only when needed, so headers
90    // without bounded types stay byte-identical.
91    if spec_has_bounded_collection(spec) {
92        includes.add("<stdexcept>");
93    }
94
95    // 3. Output buffer.
96    let mut out = String::new();
97    write_header_preamble(&mut out, opts, &includes)?;
98
99    // 4. Optional top-level namespace wrapping (`namespace_prefix`).
100    let mut ctx = EmitCtx::new(opts);
101    let outer_prefix: Option<&str> = opts.namespace_prefix.as_deref().filter(|p| !p.is_empty());
102    if let Some(prefix) = outer_prefix {
103        ctx.open_namespace(&mut out, prefix)?;
104    }
105
106    // 4b. §7.2.2.4.8 — `@verbatim(placement=BEGIN_FILE)` from all
107    // top-level defs. The spec says nothing about ordering for multiple —
108    // we use source order.
109    for d in &spec.definitions {
110        if let Some(anns) = top_level_annotations(d) {
111            emit_verbatim_at(&mut out, "", anns, PlacementKind::BeginFile)?;
112        }
113    }
114
115    // 4c. If the spec contains at least one top-level or module-nested
116    //     `struct` definition, we also emit `dds/topic/TopicTraits.hpp`
117    //     after the standard includes — that header provides
118    //     `topic_type_support<T>` and ByteSeq/string defaults — as well as
119    //     `dds/topic/xcdr2.hpp` and `dds/topic/xcdr2_md5.hpp` for the
120    //     XCDR2 wire-encoder/MD5 helpers that the later-emitted
121    //     specializations depend on.
122    let mut probe_structs: Vec<(String, &StructDef)> = Vec::new();
123    collect_topic_structs(&spec.definitions, "", &mut probe_structs);
124    let mut have_unions = Vec::new();
125    collect_topic_unions(&spec.definitions, "", &mut have_unions);
126    if !probe_structs.is_empty() || !have_unions.is_empty() {
127        // <array> is needed by key_hash(), <vector>/<string> by
128        // encode/decode(). If the standard walks have not already pulled
129        // them in, they are covered transitively via TopicTraits.hpp —
130        // but we emit them explicitly here so the header remains
131        // syntactically valid without the topic helpers.
132        writeln!(&mut out, "#include \"dds/topic/TopicTraits.hpp\"").map_err(fmt_err)?;
133        writeln!(&mut out, "#include \"dds/topic/xcdr2.hpp\"").map_err(fmt_err)?;
134        writeln!(&mut out, "#include \"dds/topic/xcdr2_md5.hpp\"").map_err(fmt_err)?;
135        // `dds::core::Fixed<P,S>` — runtime BCD type for `fixed` members
136        // (header-only; harmless when no fixed member is present).
137        writeln!(&mut out, "#include \"dds/core/Fixed.hpp\"").map_err(fmt_err)?;
138        writeln!(&mut out).map_err(fmt_err)?;
139    }
140
141    // 5. Emit definitions.
142    for d in &spec.definitions {
143        emit_definition(&mut out, &mut ctx, d)?;
144    }
145
146    // 5b. §7.2.2.4.8 — `@verbatim(placement=END_FILE)` from all
147    // top-level defs.
148    for d in &spec.definitions {
149        if let Some(anns) = top_level_annotations(d) {
150            emit_verbatim_at(&mut out, "", anns, PlacementKind::EndFile)?;
151        }
152    }
153
154    // 6. Optionally close the top-level namespace.
155    if let Some(prefix) = outer_prefix {
156        ctx.close_namespace(&mut out, prefix)?;
157    }
158
159    // 7. `topic_type_support<T>` specializations for all struct defs.
160    //    Lives in the `::dds::topic` namespace (global scope), so emitted
161    //    after the `outer_prefix` close with fully-qualified T.
162    let mut probe_unions: Vec<(String, &UnionDef)> = Vec::new();
163    collect_topic_unions(&spec.definitions, "", &mut probe_unions);
164    if !probe_structs.is_empty() || !probe_unions.is_empty() {
165        emit_topic_type_support_specs(&mut out, opts, &probe_structs, &probe_unions)?;
166    }
167
168    Ok(out)
169}
170
171/// Returns the annotation list of a top-level `Definition`, if the
172/// variant carries one. Used for file-level verbatim emission.
173fn top_level_annotations(d: &Definition) -> Option<&[Annotation]> {
174    match d {
175        Definition::Module(m) => Some(&m.annotations),
176        Definition::Type(TypeDecl::Constr(c)) => match c {
177            ConstrTypeDecl::Struct(StructDcl::Def(s)) => Some(&s.annotations),
178            ConstrTypeDecl::Union(UnionDcl::Def(u)) => Some(&u.annotations),
179            ConstrTypeDecl::Enum(e) => Some(&e.annotations),
180            _ => None,
181        },
182        Definition::Type(TypeDecl::Typedef(t)) => Some(&t.annotations),
183        Definition::Const(c) => Some(&c.annotations),
184        Definition::Except(e) => Some(&e.annotations),
185        _ => None,
186    }
187}
188
189/// Writes `// Generated...`, `#pragma once`, and standard includes.
190fn write_header_preamble(
191    out: &mut String,
192    opts: &CppGenOptions,
193    includes: &Includes,
194) -> Result<(), CppGenError> {
195    writeln!(out, "// Generated by zerodds idl-cpp. Do not edit.").map_err(fmt_err)?;
196    writeln!(out, "#pragma once").map_err(fmt_err)?;
197    if let Some(guard) = opts.include_guard_prefix.as_deref() {
198        if !guard.is_empty() {
199            // Optional guard comment for tools that only accept classical
200            // include guards. `#pragma once` remains primary.
201            writeln!(out, "// guard-prefix: {guard}").map_err(fmt_err)?;
202        }
203    }
204    writeln!(out).map_err(fmt_err)?;
205    for h in &includes.headers {
206        writeln!(out, "#include {h}").map_err(fmt_err)?;
207    }
208    writeln!(out).map_err(fmt_err)?;
209    Ok(())
210}
211
212/// Walks the AST and collects required `<...>` includes.
213fn collect_includes(spec: &Specification, inc: &mut Includes) {
214    for d in &spec.definitions {
215        collect_in_def(d, inc);
216    }
217}
218
219/// zerodds-lint: recursion-depth 64 (Parser/AST-Walk; bounded by IDL nesting)
220fn collect_in_def(d: &Definition, inc: &mut Includes) {
221    match d {
222        Definition::Module(m) => {
223            for sub in &m.definitions {
224                collect_in_def(sub, inc);
225            }
226        }
227        Definition::Type(td) => collect_in_typedecl(td, inc),
228        Definition::Const(_) => {}
229        Definition::Except(e) => {
230            inc.add("<exception>");
231            for m in &e.members {
232                collect_in_typespec(&m.type_spec, inc);
233                for decl in &m.declarators {
234                    if matches!(decl, Declarator::Array(_)) {
235                        inc.add("<array>");
236                    }
237                }
238            }
239        }
240        Definition::Interface(_)
241        | Definition::ValueBox(_)
242        | Definition::ValueForward(_)
243        | Definition::ValueDef(_)
244        | Definition::TypeId(_)
245        | Definition::TypePrefix(_)
246        | Definition::Import(_)
247        | Definition::Component(_)
248        | Definition::Home(_)
249        | Definition::Event(_)
250        | Definition::Porttype(_)
251        | Definition::Connector(_)
252        | Definition::TemplateModule(_)
253        | Definition::TemplateModuleInst(_)
254        | Definition::Annotation(_)
255        | Definition::VendorExtension(_) => {
256            // Recognised as UnsupportedConstruct in emit_definition;
257            // no includes to collect here.
258        }
259    }
260}
261
262fn collect_in_typedecl(td: &TypeDecl, inc: &mut Includes) {
263    match td {
264        TypeDecl::Constr(c) => match c {
265            ConstrTypeDecl::Struct(StructDcl::Def(s)) => {
266                // Field-order constructor uses std::move (<utility>) when the
267                // struct has at least one member.
268                if !s.members.is_empty() {
269                    inc.add("<utility>");
270                }
271                for m in &s.members {
272                    collect_in_typespec(&m.type_spec, inc);
273                    for decl in &m.declarators {
274                        if matches!(decl, Declarator::Array(_)) {
275                            inc.add("<array>");
276                        }
277                    }
278                    if has_optional_annotation(&m.annotations) {
279                        inc.add("<optional>");
280                    }
281                    if has_shared_annotation(&m.annotations) {
282                        inc.add("<memory>");
283                    }
284                }
285            }
286            ConstrTypeDecl::Struct(StructDcl::Forward(_)) => {}
287            ConstrTypeDecl::Union(UnionDcl::Def(u)) => {
288                inc.add("<variant>");
289                for c in &u.cases {
290                    collect_in_typespec(&c.element.type_spec, inc);
291                    if matches!(c.element.declarator, Declarator::Array(_)) {
292                        inc.add("<array>");
293                    }
294                }
295            }
296            ConstrTypeDecl::Union(UnionDcl::Forward(_)) => {}
297            ConstrTypeDecl::Enum(_) | ConstrTypeDecl::Bitset(_) | ConstrTypeDecl::Bitmask(_) => {}
298        },
299        TypeDecl::Typedef(t) => {
300            collect_in_typespec(&t.type_spec, inc);
301            for decl in &t.declarators {
302                if matches!(decl, Declarator::Array(_)) {
303                    inc.add("<array>");
304                }
305            }
306        }
307        // `native X;` — opaque type, no includes.
308        TypeDecl::Native(_) => {}
309    }
310}
311
312/// zerodds-lint: recursion-depth 64 (Parser/AST-Walk; bounded by IDL nesting)
313fn collect_in_typespec(ts: &TypeSpec, inc: &mut Includes) {
314    match ts {
315        TypeSpec::Primitive(_) => {}
316        TypeSpec::Scoped(_) => {}
317        TypeSpec::Sequence(s) => {
318            inc.add("<vector>");
319            collect_in_typespec(&s.elem, inc);
320        }
321        TypeSpec::String(_) => {
322            // Both `string` and `wstring` are defined in `<string>`.
323            inc.add("<string>");
324        }
325        TypeSpec::Map(m) => {
326            inc.add("<map>");
327            collect_in_typespec(&m.key, inc);
328            collect_in_typespec(&m.value, inc);
329        }
330        TypeSpec::Fixed(_) => {
331            // §7.2.4.2.4 — `fixed<digits, scale>` -> `dds::core::Fixed`.
332            // We emit a forward-declared wrapper class
333            // (runtime implementation comes with the `dds-core` crate).
334            inc.add("<cstdint>");
335        }
336        TypeSpec::Any => {
337            // §7.3 — `any` -> `dds::core::Any`. Runtime in dds-core.
338            inc.add("<cstdint>");
339        }
340    }
341}
342
343/// Emit context (indentation, output).
344struct EmitCtx<'o> {
345    opts: &'o CppGenOptions,
346    indent_level: usize,
347}
348
349impl<'o> EmitCtx<'o> {
350    fn new(opts: &'o CppGenOptions) -> Self {
351        Self {
352            opts,
353            indent_level: 0,
354        }
355    }
356
357    fn indent(&self) -> String {
358        " ".repeat(self.indent_level * self.opts.indent_width)
359    }
360
361    fn open_namespace(&mut self, out: &mut String, name: &str) -> Result<(), CppGenError> {
362        writeln!(out, "{}namespace {name} {{", self.indent()).map_err(fmt_err)?;
363        self.indent_level += 1;
364        Ok(())
365    }
366
367    fn close_namespace(&mut self, out: &mut String, name: &str) -> Result<(), CppGenError> {
368        self.indent_level = self.indent_level.saturating_sub(1);
369        writeln!(out, "{}}} // namespace {name}", self.indent()).map_err(fmt_err)?;
370        Ok(())
371    }
372}
373
374/// zerodds-lint: recursion-depth 64 (Parser/AST-Walk; bounded by IDL nesting)
375fn emit_definition(
376    out: &mut String,
377    ctx: &mut EmitCtx<'_>,
378    def: &Definition,
379) -> Result<(), CppGenError> {
380    match def {
381        Definition::Module(m) => emit_module(out, ctx, m),
382        Definition::Type(td) => emit_type_decl(out, ctx, td),
383        Definition::Const(c) => emit_const_decl(out, ctx, c),
384        Definition::Except(e) => emit_exception(out, ctx, e),
385        Definition::Interface(InterfaceDcl::Def(iface)) => {
386            // Spec idl4-cpp §7.4: IDL interface -> C++ pure-virtual class.
387            // `@service` interfaces go on via the RPC codegen path
388            // (see `crate::rpc`); here is the legacy CORBA stub path
389            // for non-service interfaces.
390            emit_interface_stub(out, ctx, iface)
391        }
392        Definition::Interface(InterfaceDcl::Forward(f)) => {
393            check_identifier(&f.name.text)?;
394            writeln!(out, "{}class {};", ctx.indent(), f.name.text).map_err(fmt_err)?;
395            Ok(())
396        }
397        Definition::ValueDef(v) => emit_value_type(out, ctx, v),
398        Definition::ValueBox(_) | Definition::ValueForward(_) => {
399            // ValueBox + ValueForward are rare CORBA constructs;
400            // the foundation leaves them as a no-op (spec §7.6.x allows
401            // a forward decl with later resolution).
402            Ok(())
403        }
404        Definition::TypeId(_)
405        | Definition::TypePrefix(_)
406        | Definition::Import(_)
407        | Definition::Component(_)
408        | Definition::Home(_)
409        | Definition::Event(_)
410        | Definition::Porttype(_)
411        | Definition::Connector(_)
412        | Definition::TemplateModule(_)
413        | Definition::TemplateModuleInst(_) => Err(CppGenError::UnsupportedConstruct {
414            construct: "corba/ccm/template construct".into(),
415            context: None,
416        }),
417        Definition::Annotation(_) => {
418            // §7.4.15 annotation defs do not become C++ code directly —
419            // applications are emitted at the annotated members via
420            // annotation bridges (e.g. @key).
421            Ok(())
422        }
423        Definition::VendorExtension(v) => Err(CppGenError::UnsupportedConstruct {
424            construct: format!("vendor-extension:{}", v.production_name),
425            context: None,
426        }),
427    }
428}
429
430/// zerodds-lint: recursion-depth 64 (Parser/AST-Walk; bounded by IDL nesting)
431fn emit_module(out: &mut String, ctx: &mut EmitCtx<'_>, m: &ModuleDef) -> Result<(), CppGenError> {
432    check_identifier(&m.name.text)?;
433    ctx.open_namespace(out, &m.name.text)?;
434    for d in &m.definitions {
435        emit_definition(out, ctx, d)?;
436    }
437    ctx.close_namespace(out, &m.name.text)?;
438    Ok(())
439}
440
441fn emit_type_decl(
442    out: &mut String,
443    ctx: &mut EmitCtx<'_>,
444    td: &TypeDecl,
445) -> Result<(), CppGenError> {
446    match td {
447        TypeDecl::Constr(c) => match c {
448            ConstrTypeDecl::Struct(StructDcl::Def(s)) => emit_struct(out, ctx, s),
449            ConstrTypeDecl::Struct(StructDcl::Forward(f)) => {
450                check_identifier(&f.name.text)?;
451                writeln!(out, "{}class {};", ctx.indent(), f.name.text).map_err(fmt_err)?;
452                Ok(())
453            }
454            ConstrTypeDecl::Union(UnionDcl::Def(u)) => emit_union(out, ctx, u),
455            ConstrTypeDecl::Union(UnionDcl::Forward(f)) => {
456                check_identifier(&f.name.text)?;
457                writeln!(out, "{}class {};", ctx.indent(), f.name.text).map_err(fmt_err)?;
458                Ok(())
459            }
460            ConstrTypeDecl::Enum(e) => emit_enum(out, ctx, e),
461            ConstrTypeDecl::Bitset(b) => {
462                check_identifier(&b.name.text)?;
463                let ind = ctx.indent();
464                let inner = " ".repeat((ctx.indent_level + 1) * ctx.opts.indent_width);
465                emit_bitset(out, &ind, &inner, b)
466            }
467            ConstrTypeDecl::Bitmask(b) => {
468                check_identifier(&b.name.text)?;
469                let ind = ctx.indent();
470                let inner = " ".repeat((ctx.indent_level + 1) * ctx.opts.indent_width);
471                emit_bitmask(out, &ind, &inner, b)
472            }
473        },
474        TypeDecl::Typedef(t) => emit_typedef(out, ctx, t),
475        // `native X;` — opaque, platform-specific type without a wire
476        // representation; not emitted in the DataType codegen.
477        TypeDecl::Native(_) => Ok(()),
478    }
479}
480
481fn emit_struct(out: &mut String, ctx: &mut EmitCtx<'_>, s: &StructDef) -> Result<(), CppGenError> {
482    check_identifier(&s.name.text)?;
483    let ind = ctx.indent();
484
485    // §7.2.2.4.8 — `@verbatim(placement=BEFORE_DECLARATION)` before the
486    // class header line.
487    emit_verbatim_at(out, &ind, &s.annotations, PlacementKind::BeforeDeclaration)?;
488
489    // Class header with an optional inheritance clause.
490    if let Some(base) = &s.base {
491        let base_str = scoped_to_cpp(base);
492        writeln!(out, "{ind}class {} : public {} {{", s.name.text, base_str).map_err(fmt_err)?;
493    } else {
494        writeln!(out, "{ind}class {} {{", s.name.text).map_err(fmt_err)?;
495    }
496    writeln!(out, "{ind}public:").map_err(fmt_err)?;
497
498    let inner = " ".repeat((ctx.indent_level + 1) * ctx.opts.indent_width);
499
500    // §7.2.2.4.8 — `@verbatim(placement=BEGIN_DECLARATION)` as the first
501    // line inside the `public:` block.
502    emit_verbatim_at(out, &inner, &s.annotations, PlacementKind::BeginDeclaration)?;
503
504    // Default constructor.
505    writeln!(out, "{inner}{}() = default;", s.name.text).map_err(fmt_err)?;
506    writeln!(out, "{inner}~{}() = default;", s.name.text).map_err(fmt_err)?;
507
508    // Field-order constructor — one parameter per member in declaration
509    // order, member-initialised (parameters passed by value + std::move so
510    // strings/sequences/nested aggregates bind cheaply; scalar move == copy).
511    // Enables brace/aggregate-style init `T t{a, b};`. Skipped for the
512    // zero-field case so it does not collide with the defaulted ctor.
513    emit_field_order_ctor(out, ctx, s, &inner)?;
514    writeln!(out).map_err(fmt_err)?;
515
516    // Member fields as private storage.
517    writeln!(out, "{ind}private:").map_err(fmt_err)?;
518    for m in &s.members {
519        emit_struct_member_field(out, ctx, m)?;
520    }
521    writeln!(out).map_err(fmt_err)?;
522
523    // Reference-Pattern Getter (mutable + const).
524    writeln!(out, "{ind}public:").map_err(fmt_err)?;
525    for m in &s.members {
526        emit_struct_member_accessors(out, ctx, m)?;
527    }
528
529    // §7.2.2.4.8 — `@verbatim(placement=END_DECLARATION)` as the last
530    // line before `};`.
531    emit_verbatim_at(out, &inner, &s.annotations, PlacementKind::EndDeclaration)?;
532
533    writeln!(out, "{ind}}};").map_err(fmt_err)?;
534
535    // §7.2.2.4.8 — `@verbatim(placement=AFTER_DECLARATION)` direkt
536    // after the closing `};`.
537    emit_verbatim_at(out, &ind, &s.annotations, PlacementKind::AfterDeclaration)?;
538
539    writeln!(out).map_err(fmt_err)?;
540    Ok(())
541}
542
543/// Storage type for a member declarator, applying `@shared` (-> shared_ptr)
544/// and `@optional` (-> std::optional) wrapping exactly as the field/accessor
545/// emitters do. Returned type matches the private `_`-suffixed member field.
546fn member_storage_type(m: &Member, decl: &Declarator) -> Result<String, CppGenError> {
547    let cpp_ty = type_for_declarator(&m.type_spec, decl)?;
548    let core_ty = if has_shared_annotation(&m.annotations) {
549        format!("std::shared_ptr<{cpp_ty}>")
550    } else {
551        cpp_ty
552    };
553    if has_optional_annotation(&m.annotations) {
554        Ok(format!("std::optional<{core_ty}>"))
555    } else {
556        Ok(core_ty)
557    }
558}
559
560/// Emit the field-order constructor: one parameter per member (in declaration
561/// order, mirroring the member list exactly, including multi-declarator
562/// members like `long a, b;`). Parameters are taken by value and moved into
563/// the corresponding `_`-suffixed field. No constructor is emitted for a
564/// zero-field struct (it would be ambiguous with the defaulted default ctor).
565fn emit_field_order_ctor(
566    out: &mut String,
567    _ctx: &EmitCtx<'_>,
568    s: &StructDef,
569    inner: &str,
570) -> Result<(), CppGenError> {
571    // Flatten members -> (storage_type, field_name) in declaration order.
572    let mut fields: Vec<(String, String)> = Vec::new();
573    for m in &s.members {
574        for decl in &m.declarators {
575            let name = decl.name();
576            check_identifier(&name.text)?;
577            fields.push((member_storage_type(m, decl)?, name.text.clone()));
578        }
579    }
580    if fields.is_empty() {
581        return Ok(());
582    }
583
584    let params = fields
585        .iter()
586        .map(|(ty, name)| format!("{ty} {name}"))
587        .collect::<Vec<_>>()
588        .join(", ");
589    let inits = fields
590        .iter()
591        .map(|(_, name)| format!("{name}_(std::move({name}))"))
592        .collect::<Vec<_>>()
593        .join(", ");
594
595    writeln!(
596        out,
597        "{inner}{}({params})\n{inner}    : {inits} {{}}",
598        s.name.text
599    )
600    .map_err(fmt_err)?;
601    Ok(())
602}
603
604fn emit_struct_member_field(
605    out: &mut String,
606    ctx: &EmitCtx<'_>,
607    m: &Member,
608) -> Result<(), CppGenError> {
609    let inner = " ".repeat((ctx.indent_level + 1) * ctx.opts.indent_width);
610    let optional = has_optional_annotation(&m.annotations);
611    let shared = has_shared_annotation(&m.annotations);
612    for decl in &m.declarators {
613        let cpp_ty = type_for_declarator(&m.type_spec, decl)?;
614        let name = decl.name();
615        check_identifier(&name.text)?;
616        let key_marker = if has_key_annotation(&m.annotations) {
617            " // @key"
618        } else {
619            ""
620        };
621        // §8.1.5 `@shared` -> `std::shared_ptr<T>`. Combination with
622        // `@optional` yields `std::optional<std::shared_ptr<T>>`.
623        let core_ty = if shared {
624            format!("std::shared_ptr<{cpp_ty}>")
625        } else {
626            cpp_ty
627        };
628        if optional {
629            writeln!(
630                out,
631                "{inner}std::optional<{core_ty}> {}_;{key_marker}",
632                name.text
633            )
634            .map_err(fmt_err)?;
635        } else {
636            writeln!(out, "{inner}{core_ty} {}_;{key_marker}", name.text).map_err(fmt_err)?;
637        }
638    }
639    Ok(())
640}
641
642fn emit_struct_member_accessors(
643    out: &mut String,
644    ctx: &EmitCtx<'_>,
645    m: &Member,
646) -> Result<(), CppGenError> {
647    let inner = " ".repeat((ctx.indent_level + 1) * ctx.opts.indent_width);
648    let optional = has_optional_annotation(&m.annotations);
649    let shared = has_shared_annotation(&m.annotations);
650    for decl in &m.declarators {
651        let cpp_ty = type_for_declarator(&m.type_spec, decl)?;
652        let name = &decl.name().text;
653        let core_ty = if shared {
654            format!("std::shared_ptr<{cpp_ty}>")
655        } else {
656            cpp_ty.clone()
657        };
658        let storage_ty = if optional {
659            format!("std::optional<{core_ty}>")
660        } else {
661            core_ty
662        };
663        writeln!(out, "{inner}{storage_ty}& {name}() {{ return {name}_; }}").map_err(fmt_err)?;
664        writeln!(
665            out,
666            "{inner}const {storage_ty}& {name}() const {{ return {name}_; }}"
667        )
668        .map_err(fmt_err)?;
669        writeln!(
670            out,
671            "{inner}void {name}(const {storage_ty}& value) {{ {name}_ = value; }}"
672        )
673        .map_err(fmt_err)?;
674    }
675    Ok(())
676}
677
678fn emit_union(out: &mut String, ctx: &mut EmitCtx<'_>, u: &UnionDef) -> Result<(), CppGenError> {
679    check_identifier(&u.name.text)?;
680    let ind = ctx.indent();
681    emit_verbatim_at(out, &ind, &u.annotations, PlacementKind::BeforeDeclaration)?;
682    writeln!(out, "{ind}class {} {{", u.name.text).map_err(fmt_err)?;
683    writeln!(out, "{ind}public:").map_err(fmt_err)?;
684    let inner = " ".repeat((ctx.indent_level + 1) * ctx.opts.indent_width);
685    emit_verbatim_at(out, &inner, &u.annotations, PlacementKind::BeginDeclaration)?;
686
687    let disc_ty = switch_type_to_cpp(&u.switch_type)?;
688
689    // Build the variant list from distinct element types.
690    let mut variant_types: Vec<String> = Vec::new();
691    for c in &u.cases {
692        let cpp_ty = type_for_declarator(&c.element.type_spec, &c.element.declarator)?;
693        if !variant_types.iter().any(|t| t == &cpp_ty) {
694            variant_types.push(cpp_ty);
695        }
696    }
697    let variant_str = if variant_types.is_empty() {
698        "std::monostate".to_string()
699    } else {
700        variant_types.join(", ")
701    };
702
703    writeln!(
704        out,
705        "{inner}using value_type = std::variant<{variant_str}>;"
706    )
707    .map_err(fmt_err)?;
708    writeln!(out, "{inner}{}() = default;", u.name.text).map_err(fmt_err)?;
709    writeln!(out, "{inner}~{}() = default;", u.name.text).map_err(fmt_err)?;
710    writeln!(out).map_err(fmt_err)?;
711
712    // Discriminator.
713    writeln!(
714        out,
715        "{inner}{disc_ty} _d() const {{ return discriminator_; }}"
716    )
717    .map_err(fmt_err)?;
718    writeln!(out, "{inner}void _d({disc_ty} d) {{ discriminator_ = d; }}").map_err(fmt_err)?;
719    writeln!(out, "{inner}value_type& value() {{ return value_; }}").map_err(fmt_err)?;
720    writeln!(
721        out,
722        "{inner}const value_type& value() const {{ return value_; }}"
723    )
724    .map_err(fmt_err)?;
725    writeln!(out).map_err(fmt_err)?;
726
727    // Branch markers as comments (discriminator values).
728    let mut has_default = false;
729    for c in &u.cases {
730        emit_union_case_comment(out, &inner, c, &mut has_default)?;
731    }
732    if !has_default {
733        writeln!(out, "{inner}// no explicit 'default:' branch").map_err(fmt_err)?;
734    }
735
736    writeln!(out).map_err(fmt_err)?;
737    writeln!(out, "{ind}private:").map_err(fmt_err)?;
738    writeln!(out, "{inner}{disc_ty} discriminator_{{}};").map_err(fmt_err)?;
739    writeln!(out, "{inner}value_type value_{{}};").map_err(fmt_err)?;
740    emit_verbatim_at(out, &inner, &u.annotations, PlacementKind::EndDeclaration)?;
741    writeln!(out, "{ind}}};").map_err(fmt_err)?;
742    emit_verbatim_at(out, &ind, &u.annotations, PlacementKind::AfterDeclaration)?;
743    writeln!(out).map_err(fmt_err)?;
744    Ok(())
745}
746
747fn emit_union_case_comment(
748    out: &mut String,
749    inner: &str,
750    c: &Case,
751    has_default: &mut bool,
752) -> Result<(), CppGenError> {
753    for label in &c.labels {
754        match label {
755            CaseLabel::Default => {
756                *has_default = true;
757                writeln!(
758                    out,
759                    "{inner}// case default -> {}",
760                    declarator_name(&c.element.declarator)
761                )
762                .map_err(fmt_err)?;
763            }
764            CaseLabel::Value(expr) => {
765                let val = const_expr_to_cpp(expr);
766                writeln!(
767                    out,
768                    "{inner}// case {val} -> {}",
769                    declarator_name(&c.element.declarator)
770                )
771                .map_err(fmt_err)?;
772            }
773        }
774    }
775    Ok(())
776}
777
778pub(crate) fn declarator_name(d: &Declarator) -> &str {
779    &d.name().text
780}
781
782fn emit_enum(out: &mut String, ctx: &mut EmitCtx<'_>, e: &EnumDef) -> Result<(), CppGenError> {
783    check_identifier(&e.name.text)?;
784    let ind = ctx.indent();
785    emit_verbatim_at(out, &ind, &e.annotations, PlacementKind::BeforeDeclaration)?;
786    writeln!(out, "{ind}enum class {} : int32_t {{", e.name.text).map_err(fmt_err)?;
787    let inner = " ".repeat((ctx.indent_level + 1) * ctx.opts.indent_width);
788    emit_verbatim_at(out, &inner, &e.annotations, PlacementKind::BeginDeclaration)?;
789    for en in &e.enumerators {
790        check_identifier(&en.name.text)?;
791        writeln!(out, "{inner}{},", en.name.text).map_err(fmt_err)?;
792    }
793    emit_verbatim_at(out, &inner, &e.annotations, PlacementKind::EndDeclaration)?;
794    writeln!(out, "{ind}}};").map_err(fmt_err)?;
795    emit_verbatim_at(out, &ind, &e.annotations, PlacementKind::AfterDeclaration)?;
796    writeln!(out).map_err(fmt_err)?;
797    Ok(())
798}
799
800fn emit_interface_stub(
801    out: &mut String,
802    ctx: &mut EmitCtx<'_>,
803    iface: &InterfaceDef,
804) -> Result<(), CppGenError> {
805    let name = &iface.name.text;
806    check_identifier(name)?;
807    let ind = ctx.indent();
808    let inner = " ".repeat((ctx.indent_level + 1) * ctx.opts.indent_width);
809
810    emit_verbatim_at(
811        out,
812        &ind,
813        &iface.annotations,
814        PlacementKind::BeforeDeclaration,
815    )?;
816
817    // Bases via public virtual inheritance (CORBA pattern for diamonds).
818    if iface.bases.is_empty() {
819        writeln!(out, "{ind}class {name} {{").map_err(fmt_err)?;
820    } else {
821        let bases: Vec<String> = iface
822            .bases
823            .iter()
824            .map(|b| format!("public virtual {}", scoped_to_cpp(b)))
825            .collect();
826        writeln!(out, "{ind}class {name} : {} {{", bases.join(", ")).map_err(fmt_err)?;
827    }
828    writeln!(out, "{ind}public:").map_err(fmt_err)?;
829    writeln!(out, "{inner}virtual ~{name}() = default;").map_err(fmt_err)?;
830
831    for export in &iface.exports {
832        match export {
833            Export::Op(op) => emit_interface_op(out, &inner, op)?,
834            Export::Attr(attr) => emit_interface_attr(out, &inner, attr)?,
835            Export::Type(td) => emit_type_decl(out, ctx, td)?,
836            Export::Const(c) => emit_const_decl(out, ctx, c)?,
837            Export::Except(e) => emit_exception(out, ctx, e)?,
838        }
839    }
840
841    writeln!(out, "{ind}}};").map_err(fmt_err)?;
842    emit_verbatim_at(
843        out,
844        &ind,
845        &iface.annotations,
846        PlacementKind::AfterDeclaration,
847    )?;
848    writeln!(out).map_err(fmt_err)?;
849    Ok(())
850}
851
852fn emit_interface_op(out: &mut String, inner: &str, op: &OpDecl) -> Result<(), CppGenError> {
853    check_identifier(&op.name.text)?;
854    let ret = match &op.return_type {
855        None => "void".to_string(),
856        Some(t) => typespec_to_cpp(t)?,
857    };
858    let params: Vec<String> = op
859        .params
860        .iter()
861        .map(|p| -> Result<String, CppGenError> {
862            let ty = typespec_to_cpp(&p.type_spec)?;
863            // Spec §7.4.5: in -> const T& (or T for primitives),
864            // out/inout -> T&. For the foundation: const T&/T& consistent.
865            let qual = match p.attribute {
866                ParamAttribute::In => format!("const {ty}&"),
867                ParamAttribute::Out | ParamAttribute::InOut => format!("{ty}&"),
868            };
869            Ok(format!("{qual} {}", p.name.text))
870        })
871        .collect::<Result<_, _>>()?;
872    let raises_comment = if op.raises.is_empty() {
873        String::new()
874    } else {
875        let raises: Vec<String> = op.raises.iter().map(scoped_to_cpp).collect();
876        format!(" /* throws {} */", raises.join(", "))
877    };
878    writeln!(
879        out,
880        "{inner}virtual {ret} {}({}) = 0;{raises_comment}",
881        op.name.text,
882        params.join(", ")
883    )
884    .map_err(fmt_err)?;
885    Ok(())
886}
887
888fn emit_interface_attr(
889    out: &mut String,
890    inner: &str,
891    attr: &zerodds_idl::ast::AttrDecl,
892) -> Result<(), CppGenError> {
893    check_identifier(&attr.name.text)?;
894    let ty = typespec_to_cpp(&attr.type_spec)?;
895    // Getter (every attribute has one).
896    writeln!(out, "{inner}virtual {ty} {}() const = 0;", attr.name.text).map_err(fmt_err)?;
897    // Setter only for non-readonly.
898    if !attr.readonly {
899        writeln!(
900            out,
901            "{inner}virtual void {}(const {ty}& value) = 0;",
902            attr.name.text
903        )
904        .map_err(fmt_err)?;
905    }
906    Ok(())
907}
908
909fn emit_value_type(
910    out: &mut String,
911    ctx: &mut EmitCtx<'_>,
912    v: &ValueDef,
913) -> Result<(), CppGenError> {
914    let name = &v.name.text;
915    check_identifier(name)?;
916    let ind = ctx.indent();
917    let inner = " ".repeat((ctx.indent_level + 1) * ctx.opts.indent_width);
918
919    emit_verbatim_at(out, &ind, &v.annotations, PlacementKind::BeforeDeclaration)?;
920
921    // Spec idl4-cpp §7.6: valuetype -> C++ class with pure-virtual
922    // public/protected accessors (state) + factory class.
923    // Inheritance: public virtual for all bases + supports interfaces.
924    let mut bases: Vec<String> = Vec::new();
925    if let Some(inh) = &v.inheritance {
926        for b in &inh.bases {
927            bases.push(format!("public virtual {}", scoped_to_cpp(b)));
928        }
929        for s in &inh.supports {
930            bases.push(format!("public virtual {}", scoped_to_cpp(s)));
931        }
932    }
933    if bases.is_empty() {
934        writeln!(out, "{ind}class {name} {{").map_err(fmt_err)?;
935    } else {
936        writeln!(out, "{ind}class {name} : {} {{", bases.join(", ")).map_err(fmt_err)?;
937    }
938    writeln!(out, "{ind}public:").map_err(fmt_err)?;
939    writeln!(out, "{inner}virtual ~{name}() = default;").map_err(fmt_err)?;
940
941    // Public state + methods.
942    let mut has_protected = false;
943    for el in &v.elements {
944        match el {
945            ValueElement::State(s) if matches!(s.visibility, StateVisibility::Public) => {
946                let ty = typespec_to_cpp(&s.type_spec)?;
947                for d in &s.declarators {
948                    let n = &d.name().text;
949                    writeln!(out, "{inner}virtual const {ty}& {n}() const = 0;")
950                        .map_err(fmt_err)?;
951                    writeln!(out, "{inner}virtual void {n}(const {ty}& value) = 0;")
952                        .map_err(fmt_err)?;
953                }
954            }
955            ValueElement::State(s) if matches!(s.visibility, StateVisibility::Private) => {
956                has_protected = true;
957            }
958            ValueElement::Export(Export::Op(op)) => emit_interface_op(out, &inner, op)?,
959            ValueElement::Export(Export::Attr(a)) => emit_interface_attr(out, &inner, a)?,
960            _ => {}
961        }
962    }
963
964    // Protected-State (private members in IDL -> protected in C++ per Spec).
965    if has_protected {
966        writeln!(out, "{ind}protected:").map_err(fmt_err)?;
967        for el in &v.elements {
968            if let ValueElement::State(s) = el {
969                if matches!(s.visibility, StateVisibility::Private) {
970                    let ty = typespec_to_cpp(&s.type_spec)?;
971                    for d in &s.declarators {
972                        let n = &d.name().text;
973                        writeln!(out, "{inner}virtual const {ty}& {n}() const = 0;")
974                            .map_err(fmt_err)?;
975                        writeln!(out, "{inner}virtual void {n}(const {ty}& value) = 0;")
976                            .map_err(fmt_err)?;
977                    }
978                }
979            }
980        }
981    }
982
983    writeln!(out, "{ind}}};").map_err(fmt_err)?;
984
985    // Factory-Class pro factory-init (Spec §7.6: <ValueTypeName>_factory).
986    let factories: Vec<&zerodds_idl::ast::InitDcl> = v
987        .elements
988        .iter()
989        .filter_map(|e| {
990            if let ValueElement::Init(i) = e {
991                Some(i)
992            } else {
993                None
994            }
995        })
996        .collect();
997    if !factories.is_empty() {
998        writeln!(out, "{ind}class {name}_factory {{").map_err(fmt_err)?;
999        writeln!(out, "{ind}public:").map_err(fmt_err)?;
1000        writeln!(out, "{inner}virtual ~{name}_factory() = default;").map_err(fmt_err)?;
1001        for f in &factories {
1002            check_identifier(&f.name.text)?;
1003            let params: Vec<String> = f
1004                .params
1005                .iter()
1006                .map(|p| -> Result<String, CppGenError> {
1007                    let ty = typespec_to_cpp(&p.type_spec)?;
1008                    let qual = match p.attribute {
1009                        ParamAttribute::In => format!("const {ty}&"),
1010                        ParamAttribute::Out | ParamAttribute::InOut => format!("{ty}&"),
1011                    };
1012                    Ok(format!("{qual} {}", p.name.text))
1013                })
1014                .collect::<Result<_, _>>()?;
1015            writeln!(
1016                out,
1017                "{inner}virtual std::shared_ptr<{name}> {}({}) = 0;",
1018                f.name.text,
1019                params.join(", ")
1020            )
1021            .map_err(fmt_err)?;
1022        }
1023        writeln!(out, "{ind}}};").map_err(fmt_err)?;
1024    }
1025
1026    emit_verbatim_at(out, &ind, &v.annotations, PlacementKind::AfterDeclaration)?;
1027    writeln!(out).map_err(fmt_err)?;
1028    Ok(())
1029}
1030
1031fn emit_typedef(
1032    out: &mut String,
1033    ctx: &mut EmitCtx<'_>,
1034    t: &TypedefDecl,
1035) -> Result<(), CppGenError> {
1036    let ind = ctx.indent();
1037    emit_verbatim_at(out, &ind, &t.annotations, PlacementKind::BeforeDeclaration)?;
1038    for decl in &t.declarators {
1039        let alias = &decl.name().text;
1040        check_identifier(alias)?;
1041        let target_ty = type_for_declarator(&t.type_spec, decl)?;
1042        writeln!(out, "{ind}using {alias} = {target_ty};").map_err(fmt_err)?;
1043    }
1044    emit_verbatim_at(out, &ind, &t.annotations, PlacementKind::AfterDeclaration)?;
1045    writeln!(out).map_err(fmt_err)?;
1046    Ok(())
1047}
1048
1049fn emit_const_decl(
1050    out: &mut String,
1051    ctx: &mut EmitCtx<'_>,
1052    c: &zerodds_idl::ast::ConstDecl,
1053) -> Result<(), CppGenError> {
1054    check_identifier(&c.name.text)?;
1055    let ind = ctx.indent();
1056    let cpp_ty = match &c.type_ {
1057        zerodds_idl::ast::ConstType::Integer(i) => crate::type_map::integer_to_cpp(*i).to_string(),
1058        zerodds_idl::ast::ConstType::Floating(f) => {
1059            crate::type_map::floating_to_cpp(*f).to_string()
1060        }
1061        zerodds_idl::ast::ConstType::Boolean => "bool".into(),
1062        zerodds_idl::ast::ConstType::Char => "char".into(),
1063        zerodds_idl::ast::ConstType::WideChar => "wchar_t".into(),
1064        zerodds_idl::ast::ConstType::Octet => "uint8_t".into(),
1065        zerodds_idl::ast::ConstType::String { wide: false } => "std::string".into(),
1066        zerodds_idl::ast::ConstType::String { wide: true } => "std::wstring".into(),
1067        zerodds_idl::ast::ConstType::Scoped(s) => scoped_to_cpp(s),
1068        zerodds_idl::ast::ConstType::Fixed => {
1069            // §7.2.4.2.4 — fixed constant without a digits/scale annotation;
1070            // we emit it as an opaque wrapper (the caller annotates the type
1071            // via a separate `typedef fixed<D,S> Name;`).
1072            "::dds::core::Fixed<31, 0>".into()
1073        }
1074    };
1075    let val = const_expr_to_cpp(&c.value);
1076    writeln!(out, "{ind}constexpr {cpp_ty} {} = {val};", c.name.text).map_err(fmt_err)?;
1077    Ok(())
1078}
1079
1080fn emit_exception(
1081    out: &mut String,
1082    ctx: &mut EmitCtx<'_>,
1083    e: &ExceptDecl,
1084) -> Result<(), CppGenError> {
1085    check_identifier(&e.name.text)?;
1086    let ind = ctx.indent();
1087    writeln!(out, "{ind}class {} : public std::exception {{", e.name.text).map_err(fmt_err)?;
1088    writeln!(out, "{ind}public:").map_err(fmt_err)?;
1089    let inner = " ".repeat((ctx.indent_level + 1) * ctx.opts.indent_width);
1090    writeln!(out, "{inner}{}() = default;", e.name.text).map_err(fmt_err)?;
1091    writeln!(out, "{inner}~{}() override = default;", e.name.text).map_err(fmt_err)?;
1092    writeln!(
1093        out,
1094        "{inner}const char* what() const noexcept override {{ return \"{}\"; }}",
1095        e.name.text
1096    )
1097    .map_err(fmt_err)?;
1098    writeln!(out).map_err(fmt_err)?;
1099    writeln!(out, "{ind}private:").map_err(fmt_err)?;
1100    for m in &e.members {
1101        for decl in &m.declarators {
1102            let cpp_ty = type_for_declarator(&m.type_spec, decl)?;
1103            let name = &decl.name().text;
1104            check_identifier(name)?;
1105            writeln!(out, "{inner}{cpp_ty} {name}_;").map_err(fmt_err)?;
1106        }
1107    }
1108    writeln!(out, "{ind}}};").map_err(fmt_err)?;
1109    writeln!(out).map_err(fmt_err)?;
1110    Ok(())
1111}
1112
1113// ---------------------------------------------------------------------------
1114// TypeSpec → C++-Type-Ausdruck
1115// ---------------------------------------------------------------------------
1116
1117/// Returns the C++ type expression for a member (TypeSpec + Declarator).
1118/// Array declarators become `std::array<T, N>` (multidimensionally nested).
1119pub(crate) fn type_for_declarator(ts: &TypeSpec, decl: &Declarator) -> Result<String, CppGenError> {
1120    let base = typespec_to_cpp(ts)?;
1121    match decl {
1122        Declarator::Simple(_) => Ok(base),
1123        Declarator::Array(arr) => {
1124            // Wrap inside-out: arr.sizes[0] is the outermost dimension.
1125            // `int x[2][3]` → `std::array<std::array<int, 3>, 2>`.
1126            let mut out = base;
1127            for size in arr.sizes.iter().rev() {
1128                let n = const_expr_to_usize(size).unwrap_or_default();
1129                out = format!("std::array<{out}, {n}>");
1130            }
1131            Ok(out)
1132        }
1133    }
1134}
1135
1136/// zerodds-lint: recursion-depth 64 (Parser/AST-Walk; bounded by IDL nesting)
1137pub(crate) fn typespec_to_cpp(ts: &TypeSpec) -> Result<String, CppGenError> {
1138    match ts {
1139        TypeSpec::Primitive(p) => Ok(primitive_to_cpp(*p).to_string()),
1140        TypeSpec::Scoped(s) => Ok(scoped_to_cpp(s)),
1141        TypeSpec::Sequence(s) => {
1142            let inner = typespec_to_cpp(&s.elem)?;
1143            Ok(format!("std::vector<{inner}>"))
1144        }
1145        TypeSpec::String(s) => {
1146            if s.wide {
1147                Ok("std::wstring".into())
1148            } else {
1149                Ok("std::string".into())
1150            }
1151        }
1152        TypeSpec::Map(m) => {
1153            let k = typespec_to_cpp(&m.key)?;
1154            let v = typespec_to_cpp(&m.value)?;
1155            Ok(format!("std::map<{k}, {v}>"))
1156        }
1157        TypeSpec::Fixed(f) => {
1158            // Spec idl4-cpp §7.2.4.2.4: fixed<digits, scale> ->
1159            // `omg::types::fixed<D, S>` (spec) resp. `dds::core::Fixed<D,S>`
1160            // (ZeroDDS-equivalent form). Digits/scale from the AST.
1161            let digits = const_expr_to_u32(&f.digits).unwrap_or(0);
1162            let scale = const_expr_to_u32(&f.scale).unwrap_or(0);
1163            Ok(format!("::dds::core::Fixed<{digits}, {scale}>"))
1164        }
1165        TypeSpec::Any => {
1166            // `any` is a CORBA type (TypeCode + dynamic value), absent from the
1167            // DDS-XTypes type system, and has NO XCDR wire codec in ZeroDDS yet.
1168            // Emitting a `::dds::core::Any` field (no such runtime type) produced
1169            // code that did not compile AND dropped the value on the wire. Reject
1170            // it cleanly at codegen — exactly like the C and Python backends —
1171            // instead of a cryptic downstream error. (Tracked: dedicated `any`
1172            // wire-codec follow-up; CORBA interfaces keep their own `any`.)
1173            Err(CppGenError::UnsupportedConstruct {
1174                construct: "any (no DDS-XTypes wire form / no TypeObject)".to_string(),
1175                context: None,
1176            })
1177        }
1178    }
1179}
1180
1181pub(crate) fn switch_type_to_cpp(s: &SwitchTypeSpec) -> Result<String, CppGenError> {
1182    Ok(match s {
1183        SwitchTypeSpec::Integer(i) => crate::type_map::integer_to_cpp(*i).to_string(),
1184        SwitchTypeSpec::Char => "char".into(),
1185        SwitchTypeSpec::Boolean => "bool".into(),
1186        SwitchTypeSpec::Octet => "uint8_t".into(),
1187        SwitchTypeSpec::Scoped(s) => scoped_to_cpp(s),
1188    })
1189}
1190
1191pub(crate) fn scoped_to_cpp(s: &ScopedName) -> String {
1192    // Mapping for Time/Duration (block E).
1193    if s.parts.len() == 1 {
1194        if let Some(mapped) = TIME_DURATION_TYPES
1195            .iter()
1196            .find(|(idl, _)| *idl == s.parts[0].text)
1197            .map(|(_, cpp)| *cpp)
1198        {
1199            return mapped.to_string();
1200        }
1201    }
1202    // Absolutely qualify known user types so member references resolve at any
1203    // scope — serializer helpers live at global scope, where a bare single-part
1204    // name (an intra-module IDL reference) would not resolve.
1205    if let Some(last) = s.parts.last() {
1206        if let Some(path) = TYPE_PATHS.with(|r| r.borrow().get(&last.text).cloned()) {
1207            return path;
1208        }
1209    }
1210    let parts: Vec<String> = s.parts.iter().map(|p| p.text.clone()).collect();
1211    let joined = parts.join("::");
1212    if s.absolute {
1213        format!("::{joined}")
1214    } else {
1215        joined
1216    }
1217}
1218
1219// ---------------------------------------------------------------------------
1220// ConstExpr → C++ literal string (best-effort for the foundation)
1221// ---------------------------------------------------------------------------
1222
1223fn const_expr_to_u32(e: &ConstExpr) -> Option<u32> {
1224    if let ConstExpr::Literal(l) = e {
1225        if matches!(l.kind, LiteralKind::Integer) {
1226            return l.raw.parse::<u32>().ok();
1227        }
1228    }
1229    None
1230}
1231
1232pub(crate) fn const_expr_to_cpp(e: &ConstExpr) -> String {
1233    match e {
1234        ConstExpr::Literal(l) => literal_to_cpp(l),
1235        ConstExpr::Scoped(s) => scoped_to_cpp(s),
1236        ConstExpr::Unary { op, operand, .. } => {
1237            let prefix = match op {
1238                zerodds_idl::ast::UnaryOp::Plus => "+",
1239                zerodds_idl::ast::UnaryOp::Minus => "-",
1240                zerodds_idl::ast::UnaryOp::BitNot => "~",
1241            };
1242            format!("{prefix}{}", const_expr_to_cpp(operand))
1243        }
1244        ConstExpr::Binary { op, lhs, rhs, .. } => {
1245            let opstr = match op {
1246                zerodds_idl::ast::BinaryOp::Or => "|",
1247                zerodds_idl::ast::BinaryOp::Xor => "^",
1248                zerodds_idl::ast::BinaryOp::And => "&",
1249                zerodds_idl::ast::BinaryOp::Shl => "<<",
1250                zerodds_idl::ast::BinaryOp::Shr => ">>",
1251                zerodds_idl::ast::BinaryOp::Add => "+",
1252                zerodds_idl::ast::BinaryOp::Sub => "-",
1253                zerodds_idl::ast::BinaryOp::Mul => "*",
1254                zerodds_idl::ast::BinaryOp::Div => "/",
1255                zerodds_idl::ast::BinaryOp::Mod => "%",
1256            };
1257            format!(
1258                "({} {opstr} {})",
1259                const_expr_to_cpp(lhs),
1260                const_expr_to_cpp(rhs)
1261            )
1262        }
1263    }
1264}
1265
1266fn literal_to_cpp(l: &Literal) -> String {
1267    match l.kind {
1268        LiteralKind::Boolean => l.raw.clone(),
1269        LiteralKind::Integer | LiteralKind::Floating => l.raw.clone(),
1270        LiteralKind::Char => l.raw.clone(),
1271        LiteralKind::WideChar => l.raw.clone(),
1272        LiteralKind::String => l.raw.clone(),
1273        LiteralKind::WideString => l.raw.clone(),
1274        LiteralKind::Fixed => l.raw.clone(),
1275    }
1276}
1277
1278fn const_expr_to_usize(e: &ConstExpr) -> Option<usize> {
1279    match e {
1280        ConstExpr::Literal(l) if l.kind == LiteralKind::Integer => l.raw.parse::<usize>().ok(),
1281        _ => None,
1282    }
1283}
1284
1285// ---------------------------------------------------------------------------
1286// Annotation-Helpers
1287// ---------------------------------------------------------------------------
1288
1289fn has_key_annotation(anns: &[Annotation]) -> bool {
1290    has_named_annotation(anns, "key")
1291}
1292
1293fn has_optional_annotation(anns: &[Annotation]) -> bool {
1294    has_named_annotation(anns, "optional")
1295}
1296
1297fn has_shared_annotation(anns: &[Annotation]) -> bool {
1298    has_named_annotation(anns, "shared")
1299}
1300
1301fn has_named_annotation(anns: &[Annotation], name: &str) -> bool {
1302    anns.iter().any(|a| {
1303        a.name.parts.last().is_some_and(|p| p.text == name)
1304            && matches!(a.params, AnnotationParams::None | AnnotationParams::Empty)
1305    })
1306}
1307
1308/// Looks for a `@<name>(N)` and returns the uint32 value; otherwise None.
1309fn find_uint_annotation(anns: &[Annotation], name: &str) -> Option<u32> {
1310    for a in anns {
1311        if a.name.parts.last().is_some_and(|p| p.text == name) {
1312            if let AnnotationParams::Single(expr) = &a.params {
1313                if let Some(v) = const_expr_as_u32(expr) {
1314                    return Some(v);
1315                }
1316            }
1317        }
1318    }
1319    None
1320}
1321/// zerodds-lint: recursion-depth 64 (const_expr_as_u32 bounded by AST depth)
1322/// Attempts to interpret a ConstExpr as a positive u32 (only an integer
1323/// literal or unary plus on an integer literal).
1324fn const_expr_as_u32(e: &ConstExpr) -> Option<u32> {
1325    match e {
1326        ConstExpr::Literal(Literal {
1327            kind: LiteralKind::Integer,
1328            raw,
1329            ..
1330        }) => parse_int_literal(raw).and_then(|v| u32::try_from(v).ok()),
1331        ConstExpr::Unary {
1332            op: zerodds_idl::ast::UnaryOp::Plus,
1333            operand,
1334            ..
1335        } => const_expr_as_u32(operand),
1336        _ => None,
1337    }
1338}
1339
1340/// Parser for integer literals (decimal, hex `0x`, octal `0...`).
1341fn parse_int_literal(raw: &str) -> Option<u64> {
1342    let s = raw.trim_end_matches(|c: char| matches!(c, 'l' | 'L' | 'u' | 'U'));
1343    if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
1344        u64::from_str_radix(hex, 16).ok()
1345    } else if s.len() > 1 && s.starts_with('0') {
1346        u64::from_str_radix(&s[1..], 8).ok()
1347    } else {
1348        s.parse::<u64>().ok()
1349    }
1350}
1351
1352/// Extensibility mode of a struct from its annotations.
1353#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1354enum Extensibility {
1355    Final,
1356    Appendable,
1357    Mutable,
1358}
1359
1360fn struct_extensibility(anns: &[Annotation]) -> Extensibility {
1361    if has_named_annotation(anns, "final") {
1362        Extensibility::Final
1363    } else if has_named_annotation(anns, "mutable") {
1364        Extensibility::Mutable
1365    } else if has_named_annotation(anns, "appendable") {
1366        Extensibility::Appendable
1367    } else {
1368        // Un-annotated default: FINAL. XTypes 1.3 §7.2.2.4.4 leaves the
1369        // extensibility of an un-annotated aggregate implementation-defined
1370        // (Fast/Cyclone expose it via -default_extensibility / -x; idlc via
1371        // `--default-extensibility`). The canonical zerodds choice — anchored
1372        // to the cross-vendor-validated `zerodds-cdr` core and the idl-rust
1373        // reference (whose hardcoded default is also Final, see
1374        // tools/idlc/src/default_ext.rs) — is FINAL, so a nested un-annotated
1375        // `@final` struct/union recurses inline (Plain-CDR2, NO per-element /
1376        // per-member DHEADER, §7.4.3.4.1). Only types explicitly annotated
1377        // SX2: spec default for an unannotated aggregate is APPENDABLE
1378        // (§7.3.3.1); `--default-extensibility final` opts back to no-DHEADER.
1379        Extensibility::Appendable
1380    }
1381}
1382
1383// ---------------------------------------------------------------------------
1384// Inheritance-Cycle-Detection (reine Self/Direct-Loops im Top-Level-Scope).
1385// ---------------------------------------------------------------------------
1386
1387/// Walks the AST and collects `child → parent` edges (FQN strings).
1388///
1389/// zerodds-lint: recursion-depth 64 (Parser/AST-Walk; bounded by IDL nesting)
1390fn collect_inheritance_edges(
1391    defs: &[Definition],
1392    parents: &mut std::collections::HashMap<String, String>,
1393    prefix: &str,
1394) {
1395    for d in defs {
1396        match d {
1397            Definition::Module(m) => {
1398                let new_prefix = if prefix.is_empty() {
1399                    m.name.text.clone()
1400                } else {
1401                    format!("{prefix}::{}", m.name.text)
1402                };
1403                collect_inheritance_edges(&m.definitions, parents, &new_prefix);
1404            }
1405            Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Def(s)))) => {
1406                let key = if prefix.is_empty() {
1407                    s.name.text.clone()
1408                } else {
1409                    format!("{prefix}::{}", s.name.text)
1410                };
1411                if let Some(b) = &s.base {
1412                    let base_str = b
1413                        .parts
1414                        .iter()
1415                        .map(|p| p.text.clone())
1416                        .collect::<Vec<_>>()
1417                        .join("::");
1418                    parents.insert(key, base_str);
1419                }
1420            }
1421            _ => {}
1422        }
1423    }
1424}
1425
1426fn detect_inheritance_cycles(spec: &Specification) -> Result<(), CppGenError> {
1427    use std::collections::HashMap;
1428
1429    let mut parents: HashMap<String, String> = HashMap::new();
1430    collect_inheritance_edges(&spec.definitions, &mut parents, "");
1431
1432    // Cycle detection via a visited set per node.
1433    for start in parents.keys() {
1434        let mut current = start.clone();
1435        let mut visited: BTreeSet<String> = BTreeSet::new();
1436        visited.insert(current.clone());
1437        while let Some(p) = parents.get(&current) {
1438            // Match flexibly: full key or suffix match.
1439            let resolved = parents
1440                .keys()
1441                .find(|k| *k == p || k.ends_with(&format!("::{p}")))
1442                .cloned()
1443                .unwrap_or_else(|| p.clone());
1444            if visited.contains(&resolved) {
1445                return Err(CppGenError::InheritanceCycle {
1446                    type_name: short_name(&resolved),
1447                });
1448            }
1449            visited.insert(resolved.clone());
1450            // Direktes Self-Reference (Parent == Self):
1451            if resolved == current {
1452                return Err(CppGenError::InheritanceCycle {
1453                    type_name: short_name(&resolved),
1454                });
1455            }
1456            current = resolved;
1457            if !parents.contains_key(&current) {
1458                break;
1459            }
1460        }
1461    }
1462    Ok(())
1463}
1464
1465fn short_name(s: &str) -> String {
1466    s.rsplit("::").next().unwrap_or(s).to_string()
1467}
1468
1469// ---------------------------------------------------------------------------
1470// topic_type_support<T> — DDS-PSM-Cxx Topic-Trait-Spezialisierung
1471// ---------------------------------------------------------------------------
1472//
1473// Collects all top-level and module-nested structs and emits per struct
1474// a `dds::topic::topic_type_support<FQN>` specialization with type_name(),
1475// encode(), encode_be(), decode(), key_hash(), is_keyed() and extensibility().
1476//
1477// The wire format is full XCDR2 (XTypes 1.3 §7.4):
1478//   * Plain-CDR2 LE with alignment relative to the encapsulation start.
1479//   * `@final`           -> no DHEADER.
1480//   * `@appendable`(def) -> DHEADER (4 byte body-size) prefixed.
1481//   * `@mutable`         -> DHEADER + EMHEADER per member (PL_CDR2).
1482//   * `@key`             -> member goes into the key hash (MD5 over BE-Plain-CDR2).
1483//   * `@id(N)`           -> EMHEADER member-id.
1484//   * `@optional`        -> EMHEADER skip if absent (mutable);
1485//                            1-byte present-flag for final/appendable.
1486//   * `@must_understand` -> EMHEADER MU-Flag.
1487//
1488// Konformanz: docs/specs/zerodds-xcdr2-bindings-conformance-1.0.md (V-1..V-12).
1489
1490/// zerodds-lint: recursion-depth 64 (Parser/AST-Walk; bounded by IDL nesting)
1491fn collect_topic_structs<'a>(
1492    defs: &'a [Definition],
1493    prefix: &str,
1494    out: &mut Vec<(String, &'a StructDef)>,
1495) {
1496    for d in defs {
1497        match d {
1498            Definition::Module(m) => {
1499                let np = if prefix.is_empty() {
1500                    m.name.text.clone()
1501                } else {
1502                    format!("{prefix}::{}", m.name.text)
1503                };
1504                collect_topic_structs(&m.definitions, &np, out);
1505            }
1506            Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Def(s)))) => {
1507                let fqn = if prefix.is_empty() {
1508                    s.name.text.clone()
1509                } else {
1510                    format!("{prefix}::{}", s.name.text)
1511                };
1512                out.push((fqn, s));
1513            }
1514            _ => {}
1515        }
1516    }
1517}
1518
1519/// Collect all top-level + module-nested union definitions (FQN, def), so the
1520/// emitter can produce a `topic_type_support<Union>` specialization for each —
1521/// the union member splice path depends on it (Bug R3).
1522/// zerodds-lint: recursion-depth 64 (codegen AST walk; bounded by IDL nesting).
1523fn collect_topic_unions<'a>(
1524    defs: &'a [Definition],
1525    prefix: &str,
1526    out: &mut Vec<(String, &'a UnionDef)>,
1527) {
1528    for d in defs {
1529        match d {
1530            Definition::Module(m) => {
1531                let np = if prefix.is_empty() {
1532                    m.name.text.clone()
1533                } else {
1534                    format!("{prefix}::{}", m.name.text)
1535                };
1536                collect_topic_unions(&m.definitions, &np, out);
1537            }
1538            Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Union(UnionDcl::Def(u)))) => {
1539                let fqn = if prefix.is_empty() {
1540                    u.name.text.clone()
1541                } else {
1542                    format!("{prefix}::{}", u.name.text)
1543                };
1544                out.push((fqn, u));
1545            }
1546            _ => {}
1547        }
1548    }
1549}
1550
1551fn emit_topic_type_support_specs(
1552    out: &mut String,
1553    opts: &CppGenOptions,
1554    structs: &[(String, &StructDef)],
1555    unions: &[(String, &UnionDef)],
1556) -> Result<(), CppGenError> {
1557    writeln!(out).map_err(fmt_err)?;
1558    writeln!(
1559        out,
1560        "// DDS-PSM-Cxx topic_type_support<T> -- auto-generiert (XCDR2 Wire, XTypes 1.3 7.4)."
1561    )
1562    .map_err(fmt_err)?;
1563    writeln!(out, "namespace dds {{").map_err(fmt_err)?;
1564    writeln!(out, "namespace topic {{").map_err(fmt_err)?;
1565    writeln!(out).map_err(fmt_err)?;
1566
1567    let user_prefix = opts
1568        .namespace_prefix
1569        .as_deref()
1570        .filter(|p| !p.is_empty())
1571        .unwrap_or("");
1572    // Bug G2 (mutual recursion, e.g. @external Vertex<->Edge): emit ALL
1573    // specializations as declarations first (full class, method signatures
1574    // only), THEN all method bodies out-of-line. This way a body that
1575    // references another specialization (`topic_type_support<Edge>::encode`)
1576    // always sees a complete declaration of it, instead of an implicit
1577    // instantiation of a not-yet-defined template. Unions are emitted before
1578    // structs so a struct that splices a union sees the union's declaration.
1579    let union_fqn = |fqn: &str| -> String {
1580        if user_prefix.is_empty() {
1581            format!("::{fqn}")
1582        } else {
1583            format!("::{user_prefix}::{fqn}")
1584        }
1585    };
1586
1587    // Phase 1: declarations.
1588    for (fqn, u) in unions {
1589        emit_union_topic_type_support(out, &union_fqn(fqn), fqn, u, TtsPhase::Decl)?;
1590    }
1591    for (fqn, s) in structs {
1592        emit_topic_type_support_for(out, &union_fqn(fqn), fqn, s, TtsPhase::Decl)?;
1593    }
1594    // Phase 2: out-of-line definitions.
1595    for (fqn, u) in unions {
1596        emit_union_topic_type_support(out, &union_fqn(fqn), fqn, u, TtsPhase::Def)?;
1597    }
1598    for (fqn, s) in structs {
1599        emit_topic_type_support_for(out, &union_fqn(fqn), fqn, s, TtsPhase::Def)?;
1600    }
1601
1602    writeln!(out, "}} // namespace topic").map_err(fmt_err)?;
1603    writeln!(out, "}} // namespace dds").map_err(fmt_err)?;
1604    Ok(())
1605}
1606
1607/// Two-phase emission for `topic_type_support<T>` specializations (Bug G2):
1608/// `Decl` emits the class with method *signatures only*; `Def` emits the method
1609/// bodies out-of-line. The split lets mutually recursive specializations see one
1610/// another's complete declaration before any body instantiates the other.
1611#[derive(Clone, Copy, PartialEq, Eq)]
1612enum TtsPhase {
1613    Decl,
1614    Def,
1615}
1616
1617/// Returns true if the member annotations are encode-safe (the codegen
1618/// can produce wire bytes). False for `@shared` (heap indirection, not
1619/// yet supported). `@optional` is allowed.
1620fn member_codegen_supported(m: &Member) -> bool {
1621    !has_shared_annotation(&m.annotations)
1622}
1623
1624/// Returns true if a type spec is understood by the XCDR2 codegen.
1625///
1626/// zerodds-lint: recursion-depth 64 (type-spec walk; bounded by IDL nesting)
1627fn typespec_supported(ts: &TypeSpec) -> bool {
1628    match ts {
1629        TypeSpec::Primitive(_) => true,
1630        // narrow `string` AND wide `wstring` (conformance §9.1, UTF-16 wire).
1631        TypeSpec::String(_) => true,
1632        // Sequence elements: primitives, narrow + wide string, enum (-> int32),
1633        // nested struct of ANY extensibility (@final → recursed inline;
1634        // @appendable/@mutable → 4-aligned splice per element, see emit loops),
1635        // nested sequence (recursed, own inner DHEADER) and map (recursed, own
1636        // DHEADER) are all wired.
1637        TypeSpec::Sequence(seq) => match &*seq.elem {
1638            TypeSpec::Primitive(_) => true,
1639            TypeSpec::String(_) => true,
1640            // enum (-> int32), nested struct, AND union (spliced via its own
1641            // DHEADER-framed TypeSupport, Bug R3) — a sequence<union> was
1642            // previously silently dropped from the wire (data loss).
1643            TypeSpec::Scoped(s) => {
1644                scoped_is_enum(s) || scoped_struct(s).is_some() || scoped_union(s).is_some()
1645            }
1646            TypeSpec::Sequence(_) => typespec_supported(&seq.elem),
1647            TypeSpec::Map(m) => typespec_supported(&m.key) && typespec_supported(&m.value),
1648            _ => false,
1649        },
1650        // A `Scoped` member resolving to an enum (→ int32) or to a directly-
1651        // encodable struct of ANY extensibility (@final → recursed inline;
1652        // @appendable/@mutable → spliced, see `scoped_struct`) is supported.
1653        // The Sequence-element arm above mirrors this (each non-final element is
1654        // 4-aligned + spliced/sub-decoded per its own DHEADER).
1655        TypeSpec::Scoped(s) => {
1656            scoped_is_enum(s)
1657                || scoped_struct(s).is_some()
1658                || scoped_union(s).is_some()
1659                || scoped_bitholder(s).is_some()
1660        }
1661        // map<K,V>: supported iff both key and value are themselves supported
1662        // (encode/decode recurse through emit_value_write/read per entry).
1663        TypeSpec::Map(m) => typespec_supported(&m.key) && typespec_supported(&m.value),
1664        // fixed<P,S>: raw BCD octets (CORBA §9.3.2.7 / XCDR2 §7.4.4.5), wired
1665        // via `::dds::core::Fixed<P,S>`. Not an XTypes type (no TypeObject) but
1666        // carried on the wire across every ZeroDDS binding.
1667        TypeSpec::Fixed(_) => true,
1668        _ => false,
1669    }
1670}
1671
1672// Codegen-scoped type registry (thread-local = correct under concurrent header
1673// generation; rebuilt per `emit_header`). Holds the SIMPLE names of all enums
1674// and all structs, so a `Scoped` member can be classified as an enum WITHOUT a
1675// full name resolver: a name is treated as an enum only if it is a known enum
1676// simple-name AND NOT a known struct simple-name — never mis-classifying a
1677// struct (which would emit a broken int32 cast). Ambiguous/relative names fall
1678// back to "skip", i.e. no regression.
1679thread_local! {
1680    static ENUM_NAMES: RefCell<BTreeSet<String>> = const { RefCell::new(BTreeSet::new()) };
1681    static STRUCT_NAMES: RefCell<BTreeSet<String>> = const { RefCell::new(BTreeSet::new()) };
1682    static STRUCT_DEFS: RefCell<BTreeMap<String, StructDef>> = const { RefCell::new(BTreeMap::new()) };
1683    /// Simple union name set + union defs, so a `Scoped` member resolving to a
1684    /// union can be classified (Bug R3) and routed through the splice path (the
1685    /// union's own DHEADER-framed `topic_type_support<Union>::encode/decode`)
1686    /// instead of being silently dropped from the wire (data loss).
1687    static UNION_NAMES: RefCell<BTreeSet<String>> = const { RefCell::new(BTreeSet::new()) };
1688    static UNION_DEFS: RefCell<BTreeMap<String, UnionDef>> = const { RefCell::new(BTreeMap::new()) };
1689    /// Simple type name → fully-qualified absolute C++ path (`::mod::Sub::Name`).
1690    /// Serializer helpers live at global scope, so member type references must be
1691    /// absolutely qualified (e.g. `::nga::LinearVelocity2DType`) — a bare
1692    /// single-part name (an intra-module IDL reference) would not resolve there.
1693    static TYPE_PATHS: RefCell<BTreeMap<String, String>> = const { RefCell::new(BTreeMap::new()) };
1694    /// Simple typedef name → aliased type, for resolving typedef-to-primitive
1695    /// members (otherwise silently skipped → wire data loss).
1696    static TYPEDEF_MAP: RefCell<BTreeMap<String, TypeSpec>> = const { RefCell::new(BTreeMap::new()) };
1697    /// Simple bitmask/bitset name → holder width in BYTES (1/2/4/8). A
1698    /// bitmask/bitset member serializes its holder integer at this width; the
1699    /// width matches the cross-vendor cdr-core reference (Rust `bitset_storage_type`):
1700    /// bitmask = smallest int fitting the #values (or explicit `@bit_bound`),
1701    /// bitset = smallest int fitting the total bitfield width. Previously such
1702    /// members were silently skipped from the wire (data loss).
1703    static BITHOLDER_BYTES: RefCell<BTreeMap<String, u32>> = const { RefCell::new(BTreeMap::new()) };
1704    /// Simple names that are bitsets (a `struct{ uintN value; }`), as opposed to
1705    /// bitmasks (an `enum class : uintN`). Needed to pick `.value` vs a cast at
1706    /// the bitmask/bitset member encode/decode site.
1707    static BITSET_NAMES: RefCell<BTreeSet<String>> = const { RefCell::new(BTreeSet::new()) };
1708    /// Simple enum name → signed wire holder width in BYTES (1/2/4), from
1709    /// `@bit_bound` (XTypes §7.4.5.1). An enum member/element serializes at this
1710    /// width instead of a fixed int32.
1711    static ENUM_BYTES: RefCell<BTreeMap<String, u32>> = const { RefCell::new(BTreeMap::new()) };
1712    // Monotonic counter for unique nested-struct decode temp-var names
1713    // (`zd_ns<N>`), so nested-nested decodes do not shadow each other.
1714    static NEST_CTR: core::cell::Cell<u32> = const { core::cell::Cell::new(0) };
1715    // Struct simple-names currently on the `scoped_struct` analysis stack.
1716    // Self-referential / mutually recursive types (XTypes §7.4.5, e.g.
1717    // `struct Node { sequence<Node> next; }`) would otherwise loop forever
1718    // through `scoped_struct` → `typespec_supported` → `scoped_struct` and
1719    // overflow the stack. A name already being visited is reported as
1720    // NOT inline-encodable, so the member uses the heap/splice path (its own
1721    // `topic_type_support` encode), which terminates at runtime on the data.
1722    static VISITING: RefCell<BTreeSet<String>> = const { RefCell::new(BTreeSet::new()) };
1723}
1724
1725fn next_nest_id() -> u32 {
1726    NEST_CTR.with(|c| {
1727        let v = c.get();
1728        c.set(v.wrapping_add(1));
1729        v
1730    })
1731}
1732
1733fn set_type_registry(spec: &Specification) {
1734    let mut r = Registry::default();
1735    let mut scope: Vec<String> = Vec::new();
1736    collect_type_names(&spec.definitions, &mut scope, &mut r);
1737    ENUM_NAMES.with(|c| *c.borrow_mut() = r.enums);
1738    STRUCT_NAMES.with(|c| *c.borrow_mut() = r.structs);
1739    STRUCT_DEFS.with(|c| *c.borrow_mut() = r.struct_defs);
1740    UNION_NAMES.with(|c| *c.borrow_mut() = r.unions);
1741    UNION_DEFS.with(|c| *c.borrow_mut() = r.union_defs);
1742    TYPE_PATHS.with(|c| *c.borrow_mut() = r.paths);
1743    TYPEDEF_MAP.with(|c| *c.borrow_mut() = r.typedefs);
1744    BITHOLDER_BYTES.with(|c| *c.borrow_mut() = r.bitholders);
1745    ENUM_BYTES.with(|c| *c.borrow_mut() = r.enum_bytes);
1746    BITSET_NAMES.with(|c| *c.borrow_mut() = r.bitsets);
1747}
1748
1749#[derive(Default)]
1750struct Registry {
1751    enums: BTreeSet<String>,
1752    structs: BTreeSet<String>,
1753    struct_defs: BTreeMap<String, StructDef>,
1754    unions: BTreeSet<String>,
1755    union_defs: BTreeMap<String, UnionDef>,
1756    paths: BTreeMap<String, String>,
1757    typedefs: BTreeMap<String, TypeSpec>,
1758    bitholders: BTreeMap<String, u32>,
1759    bitsets: BTreeSet<String>,
1760    /// Simple enum name → signed wire holder width in BYTES (1/2/4) selected by
1761    /// `@bit_bound` (XTypes 1.3 §7.4.5.1 / §7.3.1.2.1.2): N≤8 → 1, N≤16 → 2,
1762    /// else 4. Cyclone honours this; the prior fixed-int32 path dropped it.
1763    enum_bytes: BTreeMap<String, u32>,
1764}
1765
1766/// The unsigned C++ holder type for a holder width in bytes.
1767fn holder_uint_for_bytes(bytes: u32) -> &'static str {
1768    match bytes {
1769        1 => "uint8_t",
1770        2 => "uint16_t",
1771        4 => "uint32_t",
1772        _ => "uint64_t",
1773    }
1774}
1775
1776/// Holder width in BYTES (1/2/4/8) for a given holder *bit* width — mirrors the
1777/// cdr-core reference `bitset_storage_type` (u8/u16/u32/u64). Clamped to 8.
1778fn holder_bytes_for_bits(bits: u32) -> u32 {
1779    match bits {
1780        0..=8 => 1,
1781        9..=16 => 2,
1782        17..=32 => 4,
1783        _ => 8,
1784    }
1785}
1786
1787/// Absolute C++ path `::a::b::Name` for a type named `name` in module `scope`.
1788fn abs_path(scope: &[String], name: &str) -> String {
1789    let mut p = String::from("::");
1790    for s in scope {
1791        p.push_str(s);
1792        p.push_str("::");
1793    }
1794    p.push_str(name);
1795    p
1796}
1797
1798/// zerodds-lint: recursion-depth 64 (module/type tree; bounded by IDL nesting)
1799fn collect_type_names(defs: &[Definition], scope: &mut Vec<String>, r: &mut Registry) {
1800    for d in defs {
1801        match d {
1802            Definition::Module(m) => {
1803                scope.push(m.name.text.clone());
1804                collect_type_names(&m.definitions, scope, r);
1805                scope.pop();
1806            }
1807            Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Enum(e))) => {
1808                r.enums.insert(e.name.text.clone());
1809                r.paths
1810                    .insert(e.name.text.clone(), abs_path(scope, &e.name.text));
1811                // @bit_bound → signed wire holder width (default 32 → 4 bytes).
1812                let bound = find_uint_annotation(&e.annotations, "bit_bound")
1813                    .filter(|&v| (1..=32).contains(&v))
1814                    .unwrap_or(32);
1815                let bytes = if bound <= 8 {
1816                    1
1817                } else if bound <= 16 {
1818                    2
1819                } else {
1820                    4
1821                };
1822                r.enum_bytes.insert(e.name.text.clone(), bytes);
1823            }
1824            Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Def(s)))) => {
1825                r.structs.insert(s.name.text.clone());
1826                r.struct_defs.insert(s.name.text.clone(), s.clone());
1827                r.paths
1828                    .insert(s.name.text.clone(), abs_path(scope, &s.name.text));
1829            }
1830            Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Union(UnionDcl::Def(u)))) => {
1831                // Bug R3: register unions so a union-typed struct member is
1832                // classified + spliced (not dropped from the wire).
1833                r.unions.insert(u.name.text.clone());
1834                r.union_defs.insert(u.name.text.clone(), u.clone());
1835                r.paths
1836                    .insert(u.name.text.clone(), abs_path(scope, &u.name.text));
1837            }
1838            Definition::Type(TypeDecl::Typedef(td)) => {
1839                for decl in &td.declarators {
1840                    if let Declarator::Simple(n) = decl {
1841                        r.typedefs.insert(n.text.clone(), td.type_spec.clone());
1842                    }
1843                }
1844            }
1845            Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Bitmask(b))) => {
1846                // Holder bits = explicit `@bit_bound`, else the spec DEFAULT of 32
1847                // (Bug XV-bits): XTypes 1.3 §7.3.1.2.1.6 — a bitmask with no
1848                // `@bit_bound` defaults to @bit_bound=32 → a UInt32 (4-byte) holder,
1849                // NOT a width sized to the value count. Cross-vendor-validated
1850                // against the Rust reference (`bitmask_bit_bound`).
1851                let bits = find_uint_annotation(&b.annotations, "bit_bound").unwrap_or(32);
1852                r.bitholders
1853                    .insert(b.name.text.clone(), holder_bytes_for_bits(bits));
1854                r.paths
1855                    .insert(b.name.text.clone(), abs_path(scope, &b.name.text));
1856            }
1857            Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Bitset(b))) => {
1858                // Holder bits = sum of all bitfield widths (cdr-core ref).
1859                let total: u32 = b
1860                    .bitfields
1861                    .iter()
1862                    .filter_map(|f| const_expr_to_u32(&f.spec.width))
1863                    .sum();
1864                r.bitholders
1865                    .insert(b.name.text.clone(), holder_bytes_for_bits(total));
1866                r.bitsets.insert(b.name.text.clone());
1867                r.paths
1868                    .insert(b.name.text.clone(), abs_path(scope, &b.name.text));
1869            }
1870            _ => {}
1871        }
1872    }
1873}
1874
1875/// Resolves a member's type through typedef chains to the effective type. A
1876/// typedef-to-primitive member would otherwise match neither the enum nor the
1877/// struct classifier and be silently skipped (XCDR2 wire data loss).
1878/// zerodds-lint: recursion-depth 16
1879fn resolve_typedef_spec(ts: &TypeSpec) -> TypeSpec {
1880    let mut cur = ts.clone();
1881    for _ in 0..16 {
1882        let TypeSpec::Scoped(s) = &cur else { break };
1883        let Some(last) = s.parts.last() else { break };
1884        let Some(aliased) = TYPEDEF_MAP.with(|r| r.borrow().get(&last.text).cloned()) else {
1885            break;
1886        };
1887        cur = aliased;
1888    }
1889    cur
1890}
1891
1892/// Returns `m` with its type resolved through typedef chains, so the XCDR2
1893/// encoder/decoder dispatches on the effective type (a typedef-to-primitive is
1894/// no longer mis-classified and silently skipped). Cheap clone — members are small.
1895fn normalize_member(m: &Member) -> Member {
1896    let mut m2 = m.clone();
1897    m2.type_spec = resolve_typedef_spec(&m.type_spec);
1898    m2
1899}
1900
1901/// If `s` resolves to a union, return its [`UnionDef`]. A union member is wired
1902/// (Bug R3) via the union's own DHEADER-framed `topic_type_support<Union>`
1903/// encode/decode (splice path, identical to an `@appendable` nested struct).
1904fn scoped_union(s: &ScopedName) -> Option<UnionDef> {
1905    let last = s.parts.last()?.text.clone();
1906    UNION_DEFS.with(|r| r.borrow().get(&last).cloned())
1907}
1908
1909/// If `s` names a registered bitmask or bitset, return its holder width in
1910/// BYTES (1/2/4/8). Such a member serializes its holder integer at this width
1911/// (cdr-core reference) — previously the whole member was skipped (wire data
1912/// loss). The struct exposes `enum class : uintN` (bitmask) or `struct{ uint64_t
1913/// value; }` (bitset); both narrow to the holder width on the wire.
1914fn scoped_bitholder(s: &ScopedName) -> Option<u32> {
1915    let last = s.parts.last()?.text.clone();
1916    BITHOLDER_BYTES.with(|r| r.borrow().get(&last).copied())
1917}
1918
1919/// Signed wire holder width in BYTES (1/2/4) of an enum named by `s`, from its
1920/// `@bit_bound` (XTypes §7.4.5.1). Defaults to 4 for an unregistered name.
1921fn scoped_enum_bytes(s: &ScopedName) -> u32 {
1922    let Some(last) = s.parts.last().map(|p| p.text.clone()) else {
1923        return 4;
1924    };
1925    ENUM_BYTES
1926        .with(|r| r.borrow().get(&last).copied())
1927        .unwrap_or(4)
1928}
1929
1930/// The signed C++ integer wire type for an enum holder of `bytes` width.
1931fn enum_wire_ctype(bytes: u32) -> &'static str {
1932    match bytes {
1933        1 => "int8_t",
1934        2 => "int16_t",
1935        _ => "int32_t",
1936    }
1937}
1938
1939/// `true` if `s` names a registered bitset (struct holder), vs a bitmask
1940/// (enum-class holder). Drives `.value` access vs an enum-class cast.
1941fn scoped_is_bitset(s: &ScopedName) -> bool {
1942    let Some(last) = s.parts.last().map(|p| p.text.clone()) else {
1943        return false;
1944    };
1945    BITSET_NAMES.with(|r| r.borrow().contains(&last))
1946}
1947
1948/// `true` if `s` (a member's scoped type name) unambiguously names an enum.
1949fn scoped_is_enum(s: &ScopedName) -> bool {
1950    let Some(last) = s.parts.last().map(|p| p.text.clone()) else {
1951        return false;
1952    };
1953    let is_enum = ENUM_NAMES.with(|r| r.borrow().contains(&last));
1954    let is_struct = STRUCT_NAMES.with(|r| r.borrow().contains(&last));
1955    is_enum && !is_struct
1956}
1957
1958/// If `s` resolves to a `@final` struct whose members are ALL directly encodable
1959/// (single Simple declarator + a type the XCDR2 member encoder handles), returns
1960/// the [`StructDef`] so the encoder can recurse into it inline (Plain-CDR2: no
1961/// DHEADER for `@final`, Spec §7.4.3.4.1). Appendable/mutable nested structs +
1962/// arrays/sub-structs inside the nested struct fall back to "not supported"
1963/// (whole member skipped — never a partial encode).
1964fn scoped_final_struct(s: &ScopedName) -> Option<StructDef> {
1965    match scoped_struct(s) {
1966        Some((def, Extensibility::Final)) => Some(def),
1967        _ => None,
1968    }
1969}
1970
1971/// Like [`scoped_final_struct`] but for **any** extensibility: returns the
1972/// [`StructDef`] + its [`Extensibility`] when `s` resolves to a struct whose
1973/// members are all directly encodable. The member encoder picks the wire form
1974/// per extensibility — `@final` recurses inline (no DHEADER, alignment relative
1975/// to the outer origin), while `@appendable`/`@mutable` are **spliced** from the
1976/// nested type's own `topic_type_support<...>::encode`/`decode`. The latter is
1977/// byte-correct because the nested struct's own DHEADER (Plain-CDR2 for
1978/// appendable, the mutable scope for mutable) forces 4-alignment, and under
1979/// XCDR2 (`max_align == 4`) a 4-aligned splice point preserves every member's
1980/// relative alignment (Spec §7.4.3.4.2).
1981///
1982/// zerodds-lint: recursion-depth 64 (via typespec_supported; bounded by IDL nesting)
1983fn scoped_struct(s: &ScopedName) -> Option<(StructDef, Extensibility)> {
1984    let last = s.parts.last()?.text.clone();
1985    let def = STRUCT_DEFS.with(|r| r.borrow().get(&last).cloned())?;
1986    let ext = effective_extensibility(&last, &def.annotations);
1987    // Cycle guard: a recursive type (directly or mutually recursive, XTypes
1988    // §7.4.5 — e.g. `struct Node { sequence<Node> next; }`) would otherwise loop
1989    // forever through `scoped_struct` → `typespec_supported` → `scoped_struct`
1990    // and overflow the stack. When a name is already on the analysis stack,
1991    // report it as a supported struct WITHOUT re-walking its members. It is
1992    // `effective_extensibility`-coerced to non-`@final`, so every emit site
1993    // routes it through the splice path (its own `topic_type_support`
1994    // encode/decode, length-delimited by a DHEADER), which terminates at runtime
1995    // on the data — no inline expansion, no member dropped (no wire data loss).
1996    if VISITING.with(|v| v.borrow().contains(&last)) {
1997        return Some((def, ext));
1998    }
1999    VISITING.with(|v| {
2000        v.borrow_mut().insert(last.clone());
2001    });
2002    let all_encodable = def.members.iter().all(|m| {
2003        m.declarators.len() == 1
2004            && matches!(m.declarators.first(), Some(Declarator::Simple(_)))
2005            && typespec_supported(&m.type_spec)
2006    });
2007    VISITING.with(|v| {
2008        v.borrow_mut().remove(&last);
2009    });
2010    if all_encodable {
2011        Some((def, ext))
2012    } else {
2013        None
2014    }
2015}
2016
2017/// Declared extensibility of a struct, coerced to `@appendable` when the struct
2018/// is recursive (directly or mutually). A recursive type cannot be inlined
2019/// (infinite expansion), so it must be serialized in a length-delimited form
2020/// (DHEADER) and spliced via its own `topic_type_support`. `@final` recursive
2021/// types are therefore promoted to `@appendable` *consistently* — at every
2022/// member/element splice site AND in the type's own standalone serializer — so
2023/// the DHEADER the splice-decode reads is exactly the one the standalone encode
2024/// wrote (XTypes §7.4.3.4.2 + §7.4.5). Non-recursive types keep their declared
2025/// extensibility, so this never changes a non-recursive wire format.
2026fn effective_extensibility(name: &str, anns: &[Annotation]) -> Extensibility {
2027    let declared = struct_extensibility(anns);
2028    if matches!(declared, Extensibility::Final) && struct_is_recursive(name) {
2029        Extensibility::Appendable
2030    } else {
2031        declared
2032    }
2033}
2034
2035/// `true` if the struct named `root` can reach itself through the member-type
2036/// graph (directly, via a nested struct, or through sequence/array/map element
2037/// types). Its own visited set bounds the search, so it terminates even on
2038/// recursive types — unlike the `scoped_struct` encodability walk it guards.
2039fn struct_is_recursive(root: &str) -> bool {
2040    let Some(def) = STRUCT_DEFS.with(|r| r.borrow().get(root).cloned()) else {
2041        return false;
2042    };
2043    let mut visited = BTreeSet::new();
2044    visited.insert(root.to_string());
2045    def.members
2046        .iter()
2047        .any(|m| type_reaches(root, &m.type_spec, &mut visited))
2048}
2049
2050/// zerodds-lint: recursion-depth 64 (member-graph walk; bounded by `visited`)
2051fn type_reaches(target: &str, ts: &TypeSpec, visited: &mut BTreeSet<String>) -> bool {
2052    match resolve_typedef_spec(ts) {
2053        TypeSpec::Scoped(s) => {
2054            let Some(name) = s.parts.last().map(|p| p.text.clone()) else {
2055                return false;
2056            };
2057            if name == target {
2058                return true;
2059            }
2060            if !visited.insert(name.clone()) {
2061                return false;
2062            }
2063            let Some(def) = STRUCT_DEFS.with(|r| r.borrow().get(&name).cloned()) else {
2064                return false;
2065            };
2066            def.members
2067                .iter()
2068                .any(|m| type_reaches(target, &m.type_spec, visited))
2069        }
2070        TypeSpec::Sequence(seq) => type_reaches(target, &seq.elem, visited),
2071        TypeSpec::Map(m) => {
2072            type_reaches(target, &m.key, visited) || type_reaches(target, &m.value, visited)
2073        }
2074        _ => false,
2075    }
2076}
2077
2078fn emit_topic_type_support_for(
2079    out: &mut String,
2080    cpp_fqn: &str,
2081    type_name: &str,
2082    s: &StructDef,
2083    phase: TtsPhase,
2084) -> Result<(), CppGenError> {
2085    // Coerced ext: a `@final` recursive type is promoted to `@appendable` here
2086    // too, so its standalone serializer writes the same DHEADER the splice-decode
2087    // at every reference site reads back (see `effective_extensibility`).
2088    let ext = effective_extensibility(&s.name.text, &s.annotations);
2089    let is_keyed = s.members.iter().any(|m| has_key_annotation(&m.annotations));
2090
2091    if phase == TtsPhase::Decl {
2092        writeln!(out, "template <>").map_err(fmt_err)?;
2093        writeln!(out, "struct topic_type_support<{cpp_fqn}> {{").map_err(fmt_err)?;
2094        writeln!(
2095            out,
2096            "    static const char* type_name() {{ return \"{type_name}\"; }}"
2097        )
2098        .map_err(fmt_err)?;
2099        writeln!(
2100            out,
2101            "    static constexpr bool is_keyed() {{ return {}; }}",
2102            if is_keyed { "true" } else { "false" }
2103        )
2104        .map_err(fmt_err)?;
2105        let ext_lit = match ext {
2106            Extensibility::Final => "FINAL",
2107            Extensibility::Appendable => "APPENDABLE",
2108            Extensibility::Mutable => "MUTABLE",
2109        };
2110        writeln!(
2111            out,
2112            "    static constexpr ::dds::topic::core::policy::DataRepresentationKind extensibility() {{ return ::dds::topic::core::policy::DataRepresentationKind::{ext_lit}; }}"
2113        )
2114        .map_err(fmt_err)?;
2115        // method signatures only.
2116        emit_encode_fn(out, cpp_fqn, s, ext, /*be=*/ false, TtsPhase::Decl)?;
2117        emit_encode_fn(out, cpp_fqn, s, ext, /*be=*/ true, TtsPhase::Decl)?;
2118        emit_decode_fn(out, cpp_fqn, s, ext, TtsPhase::Decl)?;
2119        emit_key_hash_fn(out, cpp_fqn, s, is_keyed, TtsPhase::Decl)?;
2120        writeln!(out, "}};").map_err(fmt_err)?;
2121        writeln!(out).map_err(fmt_err)?;
2122        return Ok(());
2123    }
2124
2125    // Def phase: out-of-line bodies.
2126    // encode (LE)
2127    emit_encode_fn(out, cpp_fqn, s, ext, /*be=*/ false, TtsPhase::Def)?;
2128    // encode_be (BE)
2129    emit_encode_fn(out, cpp_fqn, s, ext, /*be=*/ true, TtsPhase::Def)?;
2130    // decode (LE)
2131    emit_decode_fn(out, cpp_fqn, s, ext, TtsPhase::Def)?;
2132    // key_hash (BE Plain-CDR2 of @key members + MD5)
2133    emit_key_hash_fn(out, cpp_fqn, s, is_keyed, TtsPhase::Def)?;
2134    writeln!(out).map_err(fmt_err)?;
2135    Ok(())
2136}
2137
2138/// Discriminator type of a union as a TypeSpec for the value-writer/reader.
2139/// Enum + integer switches wire as their primitive; char/octet/bool too.
2140fn switch_type_spec(s: &SwitchTypeSpec) -> TypeSpec {
2141    match s {
2142        SwitchTypeSpec::Integer(i) => TypeSpec::Primitive(PrimitiveType::Integer(*i)),
2143        SwitchTypeSpec::Char => TypeSpec::Primitive(PrimitiveType::Char),
2144        SwitchTypeSpec::Boolean => TypeSpec::Primitive(PrimitiveType::Boolean),
2145        SwitchTypeSpec::Octet => TypeSpec::Primitive(PrimitiveType::Octet),
2146        SwitchTypeSpec::Scoped(sc) => TypeSpec::Scoped(sc.clone()),
2147    }
2148}
2149
2150/// Bug R3: a union is wired as an `@appendable`-style aggregate (XTypes §7.4.3):
2151/// DHEADER, then the discriminator value, then the selected branch's value. The
2152/// `topic_type_support<Union>` specialization splices exactly like an appendable
2153/// nested struct, so a union-typed struct member round-trips over the wire.
2154fn emit_union_topic_type_support(
2155    out: &mut String,
2156    cpp_fqn: &str,
2157    type_name: &str,
2158    u: &UnionDef,
2159    phase: TtsPhase,
2160) -> Result<(), CppGenError> {
2161    let disc_ts = switch_type_spec(&u.switch_type);
2162    let disc_cpp = switch_type_to_cpp(&u.switch_type)?;
2163    // A union honours its extensibility just like a struct (XTypes §7.4.4.5):
2164    // @final = `discriminator + selected branch` with NO DHEADER (rule (26),
2165    // vendor-confirmed 8 B byte-identical to CycloneDDS); @appendable/@mutable
2166    // prepend a DHEADER. Previously the top-level union TypeSupport hard-coded
2167    // APPENDABLE + an unconditional DHEADER, dropping the @final wire form.
2168    let ext = union_extensibility(&u.annotations);
2169    let has_dheader = !matches!(ext, Extensibility::Final);
2170    let kind = match ext {
2171        Extensibility::Final => "FINAL",
2172        Extensibility::Mutable => "MUTABLE",
2173        Extensibility::Appendable => "APPENDABLE",
2174    };
2175
2176    if phase == TtsPhase::Decl {
2177        writeln!(out, "template <>").map_err(fmt_err)?;
2178        writeln!(out, "struct topic_type_support<{cpp_fqn}> {{").map_err(fmt_err)?;
2179        writeln!(
2180            out,
2181            "    static const char* type_name() {{ return \"{type_name}\"; }}"
2182        )
2183        .map_err(fmt_err)?;
2184        writeln!(
2185            out,
2186            "    static constexpr bool is_keyed() {{ return false; }}"
2187        )
2188        .map_err(fmt_err)?;
2189        writeln!(
2190            out,
2191            "    static constexpr ::dds::topic::core::policy::DataRepresentationKind extensibility() {{ return ::dds::topic::core::policy::DataRepresentationKind::{kind}; }}"
2192        )
2193        .map_err(fmt_err)?;
2194        writeln!(
2195            out,
2196            "    static std::vector<uint8_t> encode(const {cpp_fqn}& zd_v);"
2197        )
2198        .map_err(fmt_err)?;
2199        writeln!(
2200            out,
2201            "    static std::vector<uint8_t> encode(const {cpp_fqn}& zd_v, ::dds::topic::xcdr2::XcdrVersion zd_repr);"
2202        )
2203        .map_err(fmt_err)?;
2204        writeln!(
2205            out,
2206            "    static std::vector<uint8_t> encode_be(const {cpp_fqn}& zd_v);"
2207        )
2208        .map_err(fmt_err)?;
2209        writeln!(
2210            out,
2211            "    static std::vector<uint8_t> encode_be(const {cpp_fqn}& zd_v, ::dds::topic::xcdr2::XcdrVersion zd_repr);"
2212        )
2213        .map_err(fmt_err)?;
2214        writeln!(
2215            out,
2216            "    static {cpp_fqn} decode(const uint8_t* zd_buf, size_t zd_len, ::dds::topic::xcdr2::XcdrVersion zd_repr, bool zd_be = false);"
2217        )
2218        .map_err(fmt_err)?;
2219        writeln!(
2220            out,
2221            "    static std::array<uint8_t, 16> key_hash(const {cpp_fqn}& zd_v);"
2222        )
2223        .map_err(fmt_err)?;
2224        writeln!(out, "}};").map_err(fmt_err)?;
2225        writeln!(out).map_err(fmt_err)?;
2226        return Ok(());
2227    }
2228
2229    // Def phase: out-of-line bodies. encode (LE delegator + repr-aware) + BE.
2230    for be in [false, true] {
2231        let endian = if be { "be" } else { "le" };
2232        let beb = if be { "true" } else { "false" };
2233        if be {
2234            writeln!(
2235                out,
2236                "inline std::vector<uint8_t> topic_type_support<{cpp_fqn}>::encode_be(const {cpp_fqn}& zd_v) {{"
2237            )
2238            .map_err(fmt_err)?;
2239            writeln!(
2240                out,
2241                "        return encode_be(zd_v, ::dds::topic::xcdr2::XcdrVersion::Xcdr2);"
2242            )
2243            .map_err(fmt_err)?;
2244            writeln!(out, "    }}").map_err(fmt_err)?;
2245            writeln!(
2246                out,
2247                "inline std::vector<uint8_t> topic_type_support<{cpp_fqn}>::encode_be(const {cpp_fqn}& zd_v, ::dds::topic::xcdr2::XcdrVersion zd_repr) {{"
2248            )
2249            .map_err(fmt_err)?;
2250        } else {
2251            writeln!(
2252                out,
2253                "inline std::vector<uint8_t> topic_type_support<{cpp_fqn}>::encode(const {cpp_fqn}& zd_v) {{"
2254            )
2255            .map_err(fmt_err)?;
2256            writeln!(
2257                out,
2258                "        return encode(zd_v, ::dds::topic::xcdr2::XcdrVersion::Xcdr2);"
2259            )
2260            .map_err(fmt_err)?;
2261            writeln!(out, "    }}").map_err(fmt_err)?;
2262            writeln!(
2263                out,
2264                "inline std::vector<uint8_t> topic_type_support<{cpp_fqn}>::encode(const {cpp_fqn}& zd_v, ::dds::topic::xcdr2::XcdrVersion zd_repr) {{"
2265            )
2266            .map_err(fmt_err)?;
2267        }
2268        writeln!(out, "        std::vector<uint8_t> zd_out;").map_err(fmt_err)?;
2269        writeln!(out, "        (void)zd_v;").map_err(fmt_err)?;
2270        // Both LE (`encode`) and BE (`encode_be`) carry `zd_repr` now, so the
2271        // alignment cap and DHEADER framing are identical apart from byte order.
2272        writeln!(
2273            out,
2274            "        const size_t zd_max_align = ::dds::topic::xcdr2::xcdr_max_align(zd_repr);"
2275        )
2276        .map_err(fmt_err)?;
2277        writeln!(out, "        (void)zd_max_align;").map_err(fmt_err)?;
2278        // @appendable/@mutable: DHEADER then origin at body start (XCDR2 only —
2279        // XCDR1 / classic CDR has no DHEADER). @final: no DHEADER in any repr.
2280        if has_dheader {
2281            writeln!(
2282                out,
2283                "        size_t zd_dh = ::dds::topic::xcdr2::DHEADER_NONE; (void)zd_dh;"
2284            )
2285            .map_err(fmt_err)?;
2286            writeln!(
2287                out,
2288                "        if (zd_repr != ::dds::topic::xcdr2::XcdrVersion::Xcdr1) {{ zd_dh = ::dds::topic::xcdr2::dheader_begin(zd_out); }}"
2289            )
2290            .map_err(fmt_err)?;
2291        }
2292        writeln!(out, "        const size_t zd_origin = zd_out.size();").map_err(fmt_err)?;
2293        writeln!(out, "        (void)zd_origin;").map_err(fmt_err)?;
2294        // discriminator.
2295        emit_value_write(out, &disc_ts, "zd_v._d()", endian, "zd_origin", "        ")?;
2296        // branch selection on the discriminator.
2297        emit_union_branch_switch(out, u, &disc_cpp, /*decode=*/ false, endian)?;
2298        if has_dheader {
2299            writeln!(
2300                out,
2301                "        ::dds::topic::xcdr2::dheader_end_r(zd_out, zd_dh, {beb}, zd_repr);"
2302            )
2303            .map_err(fmt_err)?;
2304        }
2305        writeln!(out, "        return zd_out;").map_err(fmt_err)?;
2306        writeln!(out, "    }}").map_err(fmt_err)?;
2307    }
2308
2309    // decode.
2310    writeln!(
2311        out,
2312        "inline {cpp_fqn} topic_type_support<{cpp_fqn}>::decode(const uint8_t* zd_buf, size_t zd_len, ::dds::topic::xcdr2::XcdrVersion zd_repr, bool zd_be) {{"
2313    )
2314    .map_err(fmt_err)?;
2315    writeln!(out, "        size_t zd_pos = 0;").map_err(fmt_err)?;
2316    writeln!(out, "        {cpp_fqn} zd_v;").map_err(fmt_err)?;
2317    writeln!(
2318        out,
2319        "        const size_t zd_max_align = ::dds::topic::xcdr2::xcdr_max_align(zd_repr);"
2320    )
2321    .map_err(fmt_err)?;
2322    writeln!(
2323        out,
2324        "        (void)zd_buf; (void)zd_len; (void)zd_pos; (void)zd_max_align; (void)zd_be;"
2325    )
2326    .map_err(fmt_err)?;
2327    if has_dheader {
2328        // @appendable/@mutable union: a DHEADER under XCDR2, NONE under XCDR1.
2329        writeln!(out, "        size_t zd_end = zd_len; (void)zd_end;").map_err(fmt_err)?;
2330        writeln!(
2331            out,
2332            "        if (zd_repr != ::dds::topic::xcdr2::XcdrVersion::Xcdr1) {{ const auto zd_dh = ::dds::topic::xcdr2::dheader_read(zd_buf, zd_pos, zd_len, zd_be); zd_end = zd_pos + zd_dh; }}"
2333        )
2334        .map_err(fmt_err)?;
2335        writeln!(out, "        const size_t zd_origin = zd_pos;").map_err(fmt_err)?;
2336    } else {
2337        // @final: no DHEADER; body starts at the current position (0).
2338        writeln!(out, "        const size_t zd_origin = zd_pos;").map_err(fmt_err)?;
2339    }
2340    writeln!(out, "        {disc_cpp} zd_disc{{}};").map_err(fmt_err)?;
2341    emit_value_read(out, &disc_ts, "zd_disc =", "zd_origin", "        ", false)?;
2342    writeln!(out, "        zd_v._d(zd_disc);").map_err(fmt_err)?;
2343    emit_union_branch_switch(out, u, &disc_cpp, /*decode=*/ true, "le")?;
2344    if has_dheader {
2345        writeln!(
2346            out,
2347            "        if (zd_repr != ::dds::topic::xcdr2::XcdrVersion::Xcdr1 && zd_pos < zd_end) zd_pos = zd_end;"
2348        )
2349        .map_err(fmt_err)?;
2350    }
2351    writeln!(out, "        return zd_v;").map_err(fmt_err)?;
2352    writeln!(out, "    }}").map_err(fmt_err)?;
2353
2354    // key_hash: unions are not keyed → zero hash.
2355    writeln!(
2356        out,
2357        "inline std::array<uint8_t, 16> topic_type_support<{cpp_fqn}>::key_hash(const {cpp_fqn}& zd_v) {{"
2358    )
2359    .map_err(fmt_err)?;
2360    writeln!(out, "        (void)zd_v;").map_err(fmt_err)?;
2361    writeln!(
2362        out,
2363        "        return std::array<uint8_t, 16>{{{{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}}}};"
2364    )
2365    .map_err(fmt_err)?;
2366    writeln!(out, "    }}").map_err(fmt_err)?;
2367    writeln!(out).map_err(fmt_err)?;
2368    Ok(())
2369}
2370
2371/// Render a union case label. For a scoped-enum discriminator, a bare label
2372/// (`K_A`) is an `enum class` enumerator and must be qualified by the enum type
2373/// (`::conf::Kind::K_A`); other labels (integers, chars, already-qualified
2374/// enumerators) pass through `const_expr_to_cpp`.
2375fn render_case_label(expr: &ConstExpr, enum_switch: bool, disc_cpp: &str) -> String {
2376    if enum_switch {
2377        if let ConstExpr::Scoped(s) = expr {
2378            if s.parts.len() == 1 {
2379                return format!("{disc_cpp}::{}", s.parts[0].text);
2380            }
2381        }
2382    }
2383    const_expr_to_cpp(expr)
2384}
2385
2386/// Emit the discriminator `switch` that encodes/decodes the active union branch.
2387/// On encode it reads the branch from `std::get<T>(zd_v.value())`; on decode it
2388/// reads the branch type and assigns `zd_v.value() = <decoded>`.
2389fn emit_union_branch_switch(
2390    out: &mut String,
2391    u: &UnionDef,
2392    disc_cpp: &str,
2393    decode: bool,
2394    endian: &str,
2395) -> Result<(), CppGenError> {
2396    emit_union_branch_switch_at(out, u, disc_cpp, decode, endian, "zd_v", "zd_origin")
2397}
2398
2399/// Like [`emit_union_branch_switch`] but parameterized on the C++ access
2400/// expression for the union value (`access`, e.g. `zd_v` standalone or
2401/// `zd_v.reading()` when inlined as a `@final` member) and the alignment origin
2402/// (`origin`, the outer struct origin when inlined). Used to inline a `@final`
2403/// union member (XTypes 1.3 §7.4.3.4.1 / rule (26) FUNION_TYPE: disc + selected
2404/// member, NO DHEADER) instead of splicing its own DHEADER-framed serializer.
2405fn emit_union_branch_switch_at(
2406    out: &mut String,
2407    u: &UnionDef,
2408    disc_cpp: &str,
2409    decode: bool,
2410    endian: &str,
2411    access: &str,
2412    origin: &str,
2413) -> Result<(), CppGenError> {
2414    // For a scoped-enum discriminator, an unqualified case label (`K_A`) names an
2415    // enumerator of an `enum class` and must be qualified (`::conf::Kind::K_A`).
2416    let enum_switch = matches!(&u.switch_type, SwitchTypeSpec::Scoped(s) if scoped_is_enum(s));
2417    writeln!(
2418        out,
2419        "        switch (static_cast<{disc_cpp}>({access}._d())) {{"
2420    )
2421    .map_err(fmt_err)?;
2422    let mut default_case: Option<&Case> = None;
2423    for c in &u.cases {
2424        let mut is_default = false;
2425        let mut had_label = false;
2426        for label in &c.labels {
2427            match label {
2428                CaseLabel::Default => is_default = true,
2429                CaseLabel::Value(expr) => {
2430                    let val = render_case_label(expr, enum_switch, disc_cpp);
2431                    writeln!(out, "        case static_cast<{disc_cpp}>({val}):")
2432                        .map_err(fmt_err)?;
2433                    had_label = true;
2434                }
2435            }
2436        }
2437        if is_default {
2438            default_case = Some(c);
2439            continue;
2440        }
2441        if !had_label {
2442            continue;
2443        }
2444        writeln!(out, "        {{").map_err(fmt_err)?;
2445        emit_union_branch_body(out, c, decode, endian, access, origin)?;
2446        writeln!(out, "            break;").map_err(fmt_err)?;
2447        writeln!(out, "        }}").map_err(fmt_err)?;
2448    }
2449    if let Some(c) = default_case {
2450        writeln!(out, "        default: {{").map_err(fmt_err)?;
2451        emit_union_branch_body(out, c, decode, endian, access, origin)?;
2452        writeln!(out, "            break;").map_err(fmt_err)?;
2453        writeln!(out, "        }}").map_err(fmt_err)?;
2454    } else {
2455        writeln!(out, "        default: break;").map_err(fmt_err)?;
2456    }
2457    writeln!(out, "        }}").map_err(fmt_err)?;
2458    Ok(())
2459}
2460
2461/// Encode/decode one union branch's value. The branch value lives in the
2462/// union's `std::variant value_` keyed by the branch C++ type. `access` is the
2463/// union value expression and `origin` the alignment origin (see
2464/// [`emit_union_branch_switch_at`]).
2465fn emit_union_branch_body(
2466    out: &mut String,
2467    c: &Case,
2468    decode: bool,
2469    endian: &str,
2470    access: &str,
2471    origin: &str,
2472) -> Result<(), CppGenError> {
2473    let cpp_ty = type_for_declarator(&c.element.type_spec, &c.element.declarator)?;
2474    let ts = &c.element.type_spec;
2475    if decode {
2476        writeln!(out, "            {cpp_ty} zd_bv{{}};").map_err(fmt_err)?;
2477        emit_value_read(out, ts, "zd_bv =", origin, "            ", false)?;
2478        writeln!(out, "            {access}.value() = zd_bv;").map_err(fmt_err)?;
2479    } else {
2480        writeln!(
2481            out,
2482            "            const {cpp_ty}& zd_bv = std::get<{cpp_ty}>({access}.value());"
2483        )
2484        .map_err(fmt_err)?;
2485        emit_value_write(out, ts, "zd_bv", endian, origin, "            ")?;
2486    }
2487    Ok(())
2488}
2489
2490fn emit_encode_fn(
2491    out: &mut String,
2492    cpp_fqn: &str,
2493    s: &StructDef,
2494    ext: Extensibility,
2495    be: bool,
2496    phase: TtsPhase,
2497) -> Result<(), CppGenError> {
2498    // Suffix for write helpers: write_le or write_be, write_string or write_string_be.
2499    let endian_suffix = if be { "be" } else { "le" };
2500    let beb = if be { "true" } else { "false" };
2501
2502    // Decl phase (Bug G2): emit method signatures only, terminated by `;`.
2503    if phase == TtsPhase::Decl {
2504        if be {
2505            writeln!(
2506                out,
2507                "    static std::vector<uint8_t> encode_be(const {cpp_fqn}& zd_v);"
2508            )
2509            .map_err(fmt_err)?;
2510            writeln!(
2511                out,
2512                "    static std::vector<uint8_t> encode_be(const {cpp_fqn}& zd_v, \
2513                 ::dds::topic::xcdr2::XcdrVersion zd_repr);"
2514            )
2515            .map_err(fmt_err)?;
2516        } else {
2517            writeln!(
2518                out,
2519                "    static std::vector<uint8_t> encode(const {cpp_fqn}& zd_v);"
2520            )
2521            .map_err(fmt_err)?;
2522            writeln!(
2523                out,
2524                "    static std::vector<uint8_t> encode(const {cpp_fqn}& zd_v, \
2525                 ::dds::topic::xcdr2::XcdrVersion zd_repr);"
2526            )
2527            .map_err(fmt_err)?;
2528        }
2529        return Ok(());
2530    }
2531
2532    // Def phase: out-of-line bodies (`inline ... topic_type_support<T>::...`).
2533    if be {
2534        // `encode_be(v)` keeps the XCDR2-BE default; `encode_be(v, repr)` is the
2535        // representation-aware BE encoder (XCDR1-BE = classic CDR, big-endian).
2536        writeln!(
2537            out,
2538            "inline std::vector<uint8_t> topic_type_support<{cpp_fqn}>::encode_be(const {cpp_fqn}& zd_v) {{"
2539        )
2540        .map_err(fmt_err)?;
2541        writeln!(
2542            out,
2543            "        return encode_be(zd_v, ::dds::topic::xcdr2::XcdrVersion::Xcdr2);"
2544        )
2545        .map_err(fmt_err)?;
2546        writeln!(out, "    }}").map_err(fmt_err)?;
2547        writeln!(
2548            out,
2549            "inline std::vector<uint8_t> topic_type_support<{cpp_fqn}>::encode_be(const {cpp_fqn}& zd_v, \
2550             ::dds::topic::xcdr2::XcdrVersion zd_repr) {{"
2551        )
2552        .map_err(fmt_err)?;
2553    } else {
2554        // XCDR2 default delegator + version-aware encode. XCDR2 caps
2555        // 8-byte primitive alignment to 4 (XTypes 1.3 §7.4.3.4.2)
2556        // — symmetric to `decode(.., XcdrVersion)`. XCDR2 is the
2557        // ZeroDDS system default (= dcps DEFAULT_OFFER [XCDR2], encap
2558        // 0x07/0x09/0x0b); for legacy XCDR1 call
2559        // `encode(zd_v, XcdrVersion::Xcdr1)` explicitly.
2560        writeln!(
2561            out,
2562            "inline std::vector<uint8_t> topic_type_support<{cpp_fqn}>::encode(const {cpp_fqn}& zd_v) {{"
2563        )
2564        .map_err(fmt_err)?;
2565        writeln!(
2566            out,
2567            "        return encode(zd_v, ::dds::topic::xcdr2::XcdrVersion::Xcdr2);"
2568        )
2569        .map_err(fmt_err)?;
2570        writeln!(out, "    }}").map_err(fmt_err)?;
2571        writeln!(
2572            out,
2573            "inline std::vector<uint8_t> topic_type_support<{cpp_fqn}>::encode(const {cpp_fqn}& zd_v, \
2574             ::dds::topic::xcdr2::XcdrVersion zd_repr) {{"
2575        )
2576        .map_err(fmt_err)?;
2577    }
2578    writeln!(out, "        std::vector<uint8_t> zd_out;").map_err(fmt_err)?;
2579    writeln!(out, "        (void)zd_v;").map_err(fmt_err)?;
2580    // Both LE (`encode`) and BE (`encode_be`) now carry the `zd_repr` param, so
2581    // the alignment cap (XCDR2 -> 4, XCDR1 -> 8) and the representation-aware
2582    // DHEADER/PL_CDR1 framing are identical apart from the byte order.
2583    writeln!(
2584        out,
2585        "        const size_t zd_max_align = ::dds::topic::xcdr2::xcdr_max_align(zd_repr);"
2586    )
2587    .map_err(fmt_err)?;
2588    writeln!(out, "        (void)zd_max_align;").map_err(fmt_err)?;
2589
2590    match ext {
2591        Extensibility::Final => {
2592            // Plain-CDR2, no DHEADER, alignment relative to buffer start.
2593            // origin = 0.
2594            writeln!(out, "        const size_t zd_origin = 0;").map_err(fmt_err)?;
2595            writeln!(out, "        (void)zd_origin;").map_err(fmt_err)?;
2596            for m in &s.members {
2597                emit_plain_member_encode(out, m, endian_suffix, "zd_origin")?;
2598            }
2599        }
2600        Extensibility::Appendable => {
2601            // XCDR2: a DHEADER (4-byte body size) prefixes the members (origin
2602            // post-DHEADER, max_align 4). XCDR1 (classic CDR, either byte order):
2603            // NO DHEADER — plain positional members at origin = stream start with
2604            // max_align 8 (`zd_max_align` already reflects `zd_repr`). Identical
2605            // member emit; only the DHEADER framing differs.
2606            writeln!(
2607                out,
2608                "        const bool zd_x1 = (zd_repr == ::dds::topic::xcdr2::XcdrVersion::Xcdr1);"
2609            )
2610            .map_err(fmt_err)?;
2611            writeln!(out, "        size_t zd_dh = 0; (void)zd_dh;").map_err(fmt_err)?;
2612            writeln!(
2613                out,
2614                "        if (!zd_x1) {{ zd_dh = ::dds::topic::xcdr2::dheader_begin(zd_out); }}"
2615            )
2616            .map_err(fmt_err)?;
2617            writeln!(out, "        const size_t zd_origin = zd_out.size();").map_err(fmt_err)?;
2618            writeln!(out, "        (void)zd_origin;").map_err(fmt_err)?;
2619            for m in &s.members {
2620                emit_plain_member_encode(out, m, endian_suffix, "zd_origin")?;
2621            }
2622            writeln!(
2623                out,
2624                "        if (!zd_x1) {{ ::dds::topic::xcdr2::dheader_end(zd_out, zd_dh, {beb}); }}"
2625            )
2626            .map_err(fmt_err)?;
2627        }
2628        Extensibility::Mutable => {
2629            // XCDR2: PL_CDR2 — outer DHEADER + a 32-bit EMHEADER per member.
2630            // XCDR1 (either byte order): PL_CDR1 — NO DHEADER, each member a
2631            // 16/32-bit PID parameter (UNPADDED length, body padded to 4),
2632            // terminated by the PID_LIST_END sentinel.
2633            writeln!(
2634                out,
2635                "        if (zd_repr == ::dds::topic::xcdr2::XcdrVersion::Xcdr1) {{"
2636            )
2637            .map_err(fmt_err)?;
2638            let mut zd_pl_id = 0u32;
2639            for m in &s.members {
2640                emit_pl_cdr1_member_encode(out, m, &mut zd_pl_id, endian_suffix)?;
2641            }
2642            writeln!(
2643                out,
2644                "            ::dds::topic::xcdr2::pl_cdr1_write_sentinel(zd_out, {beb});"
2645            )
2646            .map_err(fmt_err)?;
2647            writeln!(out, "        }} else {{").map_err(fmt_err)?;
2648            writeln!(
2649                out,
2650                "        const auto zd_scope = ::dds::topic::xcdr2::mutable_begin(zd_out);"
2651            )
2652            .map_err(fmt_err)?;
2653            writeln!(out, "        const size_t zd_origin = zd_scope.origin;").map_err(fmt_err)?;
2654            writeln!(out, "        (void)zd_origin;").map_err(fmt_err)?;
2655            let mut zd_next_id = 0u32;
2656            for m in &s.members {
2657                emit_mutable_member_encode(out, m, endian_suffix, &mut zd_next_id)?;
2658            }
2659            writeln!(
2660                out,
2661                "        ::dds::topic::xcdr2::mutable_end(zd_out, zd_scope, {beb});"
2662            )
2663            .map_err(fmt_err)?;
2664            writeln!(out, "        }}").map_err(fmt_err)?;
2665        }
2666    }
2667
2668    writeln!(out, "        return zd_out;").map_err(fmt_err)?;
2669    writeln!(out, "    }}").map_err(fmt_err)?;
2670    Ok(())
2671}
2672
2673/// Emit Plain-CDR2 (LE/BE) encoding for one member at the current
2674/// position; alignment relative to `origin`.
2675fn emit_plain_member_encode(
2676    out: &mut String,
2677    m: &Member,
2678    endian: &str,
2679    origin: &str,
2680) -> Result<(), CppGenError> {
2681    let m = &normalize_member(m);
2682    let beb = if endian == "be" { "true" } else { "false" };
2683    if !member_codegen_supported(m) {
2684        for decl in &m.declarators {
2685            let name = &decl.name().text;
2686            writeln!(
2687                out,
2688                "        // xcdr2: @shared member '{name}' not supported (skip)"
2689            )
2690            .map_err(fmt_err)?;
2691        }
2692        return Ok(());
2693    }
2694    let is_optional = has_optional_annotation(&m.annotations);
2695    for decl in &m.declarators {
2696        let name = &decl.name().text;
2697        // 1-D fixed array of a leaf type (primitive / string / wstring): XCDR2
2698        // encodes N contiguous elements, no length prefix. Multi-dim arrays and
2699        // arrays of struct/sequence remain a follow-up (need the type registry /
2700        // recursion — see idl-cpp-xcdr2-encoder-gaps.md).
2701        if let Declarator::Array(arr) = decl {
2702            let prim = matches!(m.type_spec, TypeSpec::Primitive(_));
2703            let leaf_1d = arr.sizes.len() == 1
2704                && matches!(m.type_spec, TypeSpec::Primitive(_) | TypeSpec::String(_));
2705            if leaf_1d {
2706                writeln!(out, "        for (const auto& zd_ae : zd_v.{name}()) {{")
2707                    .map_err(fmt_err)?;
2708                emit_value_write(out, &m.type_spec, "zd_ae", endian, origin, "        ")?;
2709                writeln!(out, "        }}").map_err(fmt_err)?;
2710            } else if prim && arr.sizes.len() >= 2 {
2711                // Multi-dim array of a PRIMITIVE element (Bug XV-arr): this is a
2712                // PARRAY (XTypes 1.3 §7.4.3.5 rule 8) — a primitive array is
2713                // PLAIN-collection regardless of dimensionality, so it carries NO
2714                // collection DHEADER. Emit the row-major elements tight-packed,
2715                // exactly like the 1-D leaf path. (Earlier code wrongly wrapped a
2716                // DHEADER here; the corrected Rust golden `long grid[2][3]` is 6×i32
2717                // back-to-back with no length prefix — cross-vendor-validated.)
2718                let n = arr.sizes.len();
2719                let mut acc = format!("zd_v.{name}()");
2720                let mut ind = String::from("        ");
2721                for d in 0..n {
2722                    let lv = format!("zd_a{d}");
2723                    writeln!(out, "{ind}for (const auto& {lv} : {acc}) {{").map_err(fmt_err)?;
2724                    acc = lv;
2725                    ind.push_str("    ");
2726                }
2727                emit_value_write(out, &m.type_spec, &acc, endian, origin, &ind)?;
2728                for _ in 0..n {
2729                    ind.truncate(ind.len() - 4);
2730                    writeln!(out, "{ind}}}").map_err(fmt_err)?;
2731                }
2732            } else if matches!(&m.type_spec, TypeSpec::Scoped(s) if scoped_is_enum(s) || scoped_struct(s).is_some() || scoped_union(s).is_some())
2733                || (matches!(m.type_spec, TypeSpec::String(_)) && arr.sizes.len() >= 2)
2734            {
2735                // Array of NON-primitive elements (enum / struct / union, any dims;
2736                // string only for >=2 dims — 1-D string keeps the legacy no-DHEADER
2737                // leaf path above): one DHEADER (XTypes §7.4.3.5) wrapping N
2738                // elements inline, row-major, NO count. N nested range-for loops.
2739                // `emit_value_write` picks the per-element form: @final structs
2740                // recurse inline; @appendable/@mutable structs and unions splice
2741                // their own DHEADER-framed `topic_type_support<...>::encode`.
2742                let n = arr.sizes.len();
2743                writeln!(out, "        {{").map_err(fmt_err)?;
2744                writeln!(
2745                    out,
2746                    "        const auto zd_arr_dh = ::dds::topic::xcdr2::dheader_begin_r(zd_out, zd_repr);"
2747                )
2748                .map_err(fmt_err)?;
2749                let mut acc = format!("zd_v.{name}()");
2750                let mut ind = String::from("        ");
2751                for d in 0..n {
2752                    let lv = format!("zd_a{d}");
2753                    writeln!(out, "{ind}for (const auto& {lv} : {acc}) {{").map_err(fmt_err)?;
2754                    acc = lv;
2755                    ind.push_str("    ");
2756                }
2757                emit_value_write(out, &m.type_spec, &acc, endian, origin, &ind)?;
2758                for _ in 0..n {
2759                    ind.truncate(ind.len() - 4);
2760                    writeln!(out, "{ind}}}").map_err(fmt_err)?;
2761                }
2762                writeln!(
2763                    out,
2764                    "        ::dds::topic::xcdr2::dheader_end_r(zd_out, zd_arr_dh, {beb}, zd_repr);"
2765                )
2766                .map_err(fmt_err)?;
2767                writeln!(out, "        }}").map_err(fmt_err)?;
2768            } else {
2769                writeln!(
2770                    out,
2771                    "        // xcdr2: array member '{name}' (multi-dim string-1D-only / unsupported elem) not supported (skip)"
2772                )
2773                .map_err(fmt_err)?;
2774            }
2775            continue;
2776        }
2777        if !typespec_supported(&m.type_spec) {
2778            writeln!(
2779                out,
2780                "        // xcdr2: member '{name}' not supported (nested/enum/map; skip)"
2781            )
2782            .map_err(fmt_err)?;
2783            continue;
2784        }
2785        if is_optional {
2786            // Final/appendable: 1-byte present-flag, then the value if present.
2787            writeln!(out, "        if (zd_v.{name}().has_value()) {{").map_err(fmt_err)?;
2788            writeln!(out, "            zd_out.push_back(uint8_t{{1}});").map_err(fmt_err)?;
2789            emit_value_write(
2790                out,
2791                &m.type_spec,
2792                &format!("(*zd_v.{name}())"),
2793                endian,
2794                origin,
2795                "        ",
2796            )?;
2797            writeln!(out, "        }} else {{").map_err(fmt_err)?;
2798            writeln!(out, "            zd_out.push_back(uint8_t{{0}});").map_err(fmt_err)?;
2799            writeln!(out, "        }}").map_err(fmt_err)?;
2800        } else {
2801            emit_value_write(
2802                out,
2803                &m.type_spec,
2804                &format!("zd_v.{name}()"),
2805                endian,
2806                origin,
2807                "    ",
2808            )?;
2809        }
2810    }
2811    Ok(())
2812}
2813
2814/// Emit a single value write at `access` using LE or BE convention.
2815///
2816/// zerodds-lint: recursion-depth 64 (nested type emit; bounded by IDL nesting)
2817fn emit_value_write(
2818    out: &mut String,
2819    ts: &TypeSpec,
2820    access: &str,
2821    endian: &str,
2822    origin: &str,
2823    indent: &str,
2824) -> Result<(), CppGenError> {
2825    let pre = format!("{indent}    ");
2826    let beb = if endian == "be" { "true" } else { "false" };
2827    match ts {
2828        TypeSpec::Primitive(PrimitiveType::Boolean) => {
2829            writeln!(
2830                out,
2831                "{pre}::dds::topic::xcdr2::write_bool(zd_out, {access});"
2832            )
2833            .map_err(fmt_err)?;
2834        }
2835        TypeSpec::Primitive(PrimitiveType::Octet) => {
2836            writeln!(out, "{pre}::dds::topic::xcdr2::write_u8(zd_out, {access});")
2837                .map_err(fmt_err)?;
2838        }
2839        TypeSpec::Primitive(p) => {
2840            let cpp_ty = primitive_to_cpp(*p);
2841            if endian == "be" {
2842                // BE: representation-aware too (XCDR2 caps 8-byte align at 4).
2843                writeln!(
2844                    out,
2845                    "{pre}::dds::topic::xcdr2::write_be_origin<{cpp_ty}>(zd_out, {origin}, {access}, zd_max_align);"
2846                )
2847                .map_err(fmt_err)?;
2848            } else {
2849                // LE: representation-aware (XCDR2 deckelt 8-Byte-Align auf 4).
2850                writeln!(
2851                    out,
2852                    "{pre}::dds::topic::xcdr2::write_le_origin<{cpp_ty}>(zd_out, {origin}, {access}, zd_max_align);"
2853                )
2854                .map_err(fmt_err)?;
2855            }
2856        }
2857        TypeSpec::String(s) if !s.wide => {
2858            // Bounded narrow `string<N>` (DDS-XTypes §7.4.3): byte-length check
2859            // (std::string::size = bytes = CDR wire length).
2860            if let Some(b) = &s.bound {
2861                let bv = const_expr_to_cpp(b);
2862                writeln!(
2863                    out,
2864                    "{pre}if ({access}.size() > {bv}) throw std::length_error(\"bounded string length exceeds its IDL bound ({bv})\");"
2865                )
2866                .map_err(fmt_err)?;
2867            }
2868            if endian == "be" {
2869                writeln!(
2870                    out,
2871                    "{pre}::dds::topic::xcdr2::write_string_be(zd_out, {access});"
2872                )
2873                .map_err(fmt_err)?;
2874            } else {
2875                writeln!(
2876                    out,
2877                    "{pre}::dds::topic::xcdr2::write_string_origin(zd_out, {origin}, {access}, zd_max_align);"
2878                )
2879                .map_err(fmt_err)?;
2880            }
2881        }
2882        TypeSpec::String(s) if s.wide => {
2883            // Bounded `wstring<N>` (DDS-XTypes §7.4.3): bound is in wide chars
2884            // (std::wstring::size). Wire = UTF-16 (conformance §9.1).
2885            if let Some(b) = &s.bound {
2886                let bv = const_expr_to_cpp(b);
2887                writeln!(
2888                    out,
2889                    "{pre}if ({access}.size() > {bv}) throw std::length_error(\"bounded wstring length exceeds its IDL bound ({bv})\");"
2890                )
2891                .map_err(fmt_err)?;
2892            }
2893            if endian == "be" {
2894                writeln!(
2895                    out,
2896                    "{pre}::dds::topic::xcdr2::write_wstring_be(zd_out, {access});"
2897                )
2898                .map_err(fmt_err)?;
2899            } else {
2900                writeln!(
2901                    out,
2902                    "{pre}::dds::topic::xcdr2::write_wstring_origin(zd_out, {origin}, {access}, zd_max_align);"
2903                )
2904                .map_err(fmt_err)?;
2905            }
2906        }
2907        TypeSpec::Sequence(seq) => {
2908            // Bounded `sequence<T, N>` (DDS-XTypes §7.4.3): over-bound = encode
2909            // error. The encode returns a vector (no Result channel), so this
2910            // throws — strict vendors (OpenDDS) reject on the wire likewise.
2911            if let Some(b) = &seq.bound {
2912                let bv = const_expr_to_cpp(b);
2913                writeln!(
2914                    out,
2915                    "{pre}if ({access}.size() > {bv}) throw std::length_error(\"bounded sequence length exceeds its IDL bound ({bv})\");"
2916                )
2917                .map_err(fmt_err)?;
2918            }
2919            if matches!(&*seq.elem, TypeSpec::Primitive(PrimitiveType::Octet)) {
2920                // sequence<octet>: u32 length + raw byte block, no per-byte loop.
2921                if endian == "be" {
2922                    writeln!(out, "{pre}::dds::topic::xcdr2::write_be<uint32_t>(zd_out, static_cast<uint32_t>({access}.size()));").map_err(fmt_err)?;
2923                } else {
2924                    writeln!(out, "{pre}::dds::topic::xcdr2::write_le_origin<uint32_t>(zd_out, {origin}, static_cast<uint32_t>({access}.size()), zd_max_align);").map_err(fmt_err)?;
2925                }
2926                writeln!(
2927                    out,
2928                    "{pre}zd_out.insert(zd_out.end(), {access}.begin(), {access}.end());"
2929                )
2930                .map_err(fmt_err)?;
2931                return Ok(());
2932            }
2933            // XCDR2 §7.4.3.5: sequences with NON-primitive elements
2934            // (string, struct, …) get a DHEADER (uint32 = byte length of
2935            // [count + elements]) prepended; primitives do not.
2936            // Cyclone-DDS-verified (V-5 without, V-6 with).
2937            let seq_non_primitive = !matches!(&*seq.elem, TypeSpec::Primitive(_));
2938            if seq_non_primitive {
2939                writeln!(out, "{pre}{{").map_err(fmt_err)?;
2940                // The sequence DHEADER is a uint32 -> 4-align to origin before it
2941                // (same class of bug as the map DHEADER: a non-primitive sequence
2942                // after a sub-4-byte member would otherwise land it unaligned).
2943                writeln!(
2944                    out,
2945                    "{pre}::dds::topic::xcdr2::pad_to_from_origin(zd_out, {origin}, 4);"
2946                )
2947                .map_err(fmt_err)?;
2948                writeln!(
2949                    out,
2950                    "{pre}const auto zd_seq_dh = ::dds::topic::xcdr2::dheader_begin_r(zd_out, zd_repr);"
2951                )
2952                .map_err(fmt_err)?;
2953            }
2954            let count_call = if endian == "be" {
2955                format!(
2956                    "{pre}::dds::topic::xcdr2::write_be<uint32_t>(zd_out, static_cast<uint32_t>({access}.size()));"
2957                )
2958            } else {
2959                format!(
2960                    "{pre}::dds::topic::xcdr2::write_le_origin<uint32_t>(zd_out, {origin}, static_cast<uint32_t>({access}.size()), zd_max_align);"
2961                )
2962            };
2963            writeln!(out, "{count_call}").map_err(fmt_err)?;
2964            writeln!(out, "{pre}for (const auto& zd_e : {access}) {{").map_err(fmt_err)?;
2965            let elem_indent = format!("{pre}    ");
2966            match &*seq.elem {
2967                TypeSpec::Primitive(PrimitiveType::Boolean) => {
2968                    writeln!(
2969                        out,
2970                        "{elem_indent}::dds::topic::xcdr2::write_bool(zd_out, zd_e);"
2971                    )
2972                    .map_err(fmt_err)?;
2973                }
2974                TypeSpec::Primitive(PrimitiveType::Octet) => {
2975                    writeln!(
2976                        out,
2977                        "{elem_indent}::dds::topic::xcdr2::write_u8(zd_out, zd_e);"
2978                    )
2979                    .map_err(fmt_err)?;
2980                }
2981                TypeSpec::Primitive(p) => {
2982                    let cpp_ty = primitive_to_cpp(*p);
2983                    if endian == "be" {
2984                        writeln!(
2985                            out,
2986                            "{elem_indent}::dds::topic::xcdr2::write_be<{cpp_ty}>(zd_out, zd_e);"
2987                        )
2988                        .map_err(fmt_err)?;
2989                    } else {
2990                        writeln!(
2991                            out,
2992                            "{elem_indent}::dds::topic::xcdr2::write_le_origin<{cpp_ty}>(zd_out, {origin}, zd_e, zd_max_align);"
2993                        )
2994                        .map_err(fmt_err)?;
2995                    }
2996                }
2997                TypeSpec::String(s) if !s.wide => {
2998                    if endian == "be" {
2999                        writeln!(
3000                            out,
3001                            "{elem_indent}::dds::topic::xcdr2::write_string_be(zd_out, zd_e);"
3002                        )
3003                        .map_err(fmt_err)?;
3004                    } else {
3005                        writeln!(
3006                            out,
3007                            "{elem_indent}::dds::topic::xcdr2::write_string_origin(zd_out, {origin}, zd_e, zd_max_align);"
3008                        )
3009                        .map_err(fmt_err)?;
3010                    }
3011                }
3012                // wide string (wstring): recurse for the BOM/octet-length wire form.
3013                TypeSpec::String(_) => {
3014                    emit_value_write(out, &seq.elem, "zd_e", endian, origin, &elem_indent)?;
3015                }
3016                // enum (-> int32) and nested struct of ANY extensibility: recurse
3017                // through emit_value_write, identical to member-level encoding —
3018                // @final inlines (no DHEADER), @appendable/@mutable pad-to-4 +
3019                // splice the element's own [DHEADER+body] (XTypes §7.4.3.5).
3020                TypeSpec::Scoped(sc) if scoped_is_enum(sc) || scoped_struct(sc).is_some() => {
3021                    emit_value_write(out, &seq.elem, "zd_e", endian, origin, &elem_indent)?;
3022                }
3023                // union element (sequence<union>, Bug R3): recurse — emit_value_write
3024                // splices each element's own DHEADER-framed TypeSupport encode.
3025                TypeSpec::Scoped(sc) if scoped_union(sc).is_some() => {
3026                    emit_value_write(out, &seq.elem, "zd_e", endian, origin, &elem_indent)?;
3027                }
3028                // nested sequence (sequence<sequence<...>>): recurse — the inner
3029                // sequence emits its own DHEADER (XTypes §7.4.3.5).
3030                TypeSpec::Sequence(_) => {
3031                    emit_value_write(out, &seq.elem, "zd_e", endian, origin, &elem_indent)?;
3032                }
3033                // map element (sequence<map<K,V>>): recurse — the map emits its
3034                // own DHEADER.
3035                TypeSpec::Map(_) => {
3036                    emit_value_write(out, &seq.elem, "zd_e", endian, origin, &elem_indent)?;
3037                }
3038                _ => {
3039                    writeln!(
3040                        out,
3041                        "{elem_indent}// xcdr2: nested sequence-element not supported"
3042                    )
3043                    .map_err(fmt_err)?;
3044                }
3045            }
3046            writeln!(out, "{pre}}}").map_err(fmt_err)?;
3047            if seq_non_primitive {
3048                writeln!(
3049                    out,
3050                    "{pre}::dds::topic::xcdr2::dheader_end_r(zd_out, zd_seq_dh, {beb}, zd_repr);"
3051                )
3052                .map_err(fmt_err)?;
3053                writeln!(out, "{pre}}}").map_err(fmt_err)?;
3054            }
3055        }
3056        // map<K,V> member (XTypes §7.4.4.6): a non-primitive collection -> DHEADER
3057        // (uint32 byte-len of [count + entries]); uint32 count; then each entry as
3058        // key.encode + value.encode in key-sorted order. std::map iterates in
3059        // ascending key order, matching the Rust BTreeMap reference encoder
3060        // (crates/cdr/src/composite.rs §7.4.4.6) byte-for-byte.
3061        TypeSpec::Map(m) => {
3062            if let Some(b) = &m.bound {
3063                let bv = const_expr_to_cpp(b);
3064                writeln!(
3065                    out,
3066                    "{pre}if ({access}.size() > {bv}) throw std::length_error(\"bounded map length exceeds its IDL bound ({bv})\");"
3067                )
3068                .map_err(fmt_err)?;
3069            }
3070            writeln!(out, "{pre}{{").map_err(fmt_err)?;
3071            // The map DHEADER is a uint32 and must be 4-aligned relative to the
3072            // aggregate origin BEFORE it is written — otherwise a map preceded by
3073            // a sub-4-byte member (e.g. a @bit_bound(16) enum) lands the DHEADER
3074            // at an unaligned offset. (Bug surfaced by the MapEnum cross-PSM probe;
3075            // rust/Cyclone pad here.)
3076            writeln!(
3077                out,
3078                "{pre}::dds::topic::xcdr2::pad_to_from_origin(zd_out, {origin}, 4);"
3079            )
3080            .map_err(fmt_err)?;
3081            let map_dh = !map_pair_is_primitive(&m.key, &m.value);
3082            if map_dh {
3083                writeln!(
3084                    out,
3085                    "{pre}const auto zd_map_dh = ::dds::topic::xcdr2::dheader_begin_r(zd_out, zd_repr);"
3086                )
3087                .map_err(fmt_err)?;
3088            }
3089            if endian == "be" {
3090                writeln!(out, "{pre}::dds::topic::xcdr2::write_be<uint32_t>(zd_out, static_cast<uint32_t>({access}.size()));").map_err(fmt_err)?;
3091            } else {
3092                writeln!(out, "{pre}::dds::topic::xcdr2::write_le_origin<uint32_t>(zd_out, {origin}, static_cast<uint32_t>({access}.size()), zd_max_align);").map_err(fmt_err)?;
3093            }
3094            writeln!(out, "{pre}for (const auto& zd_kv : {access}) {{").map_err(fmt_err)?;
3095            let kv_indent = format!("{pre}    ");
3096            emit_value_write(out, &m.key, "zd_kv.first", endian, origin, &kv_indent)?;
3097            emit_value_write(out, &m.value, "zd_kv.second", endian, origin, &kv_indent)?;
3098            writeln!(out, "{pre}}}").map_err(fmt_err)?;
3099            if map_dh {
3100                writeln!(
3101                    out,
3102                    "{pre}::dds::topic::xcdr2::dheader_end_r(zd_out, zd_map_dh, {beb}, zd_repr);"
3103                )
3104                .map_err(fmt_err)?;
3105            }
3106            writeln!(out, "{pre}}}").map_err(fmt_err)?;
3107        }
3108        // bitmask / bitset member: serialize the holder integer at the holder
3109        // width (cdr-core reference: bitmask = #values, bitset = total bits).
3110        // A bitmask is `enum class : uintN` (cast to the holder), a bitset is
3111        // `struct{ uint64_t value; }` (use `.value`, narrowed). XTypes §7.4.x.
3112        TypeSpec::Scoped(s) if scoped_bitholder(s).is_some() => {
3113            let bytes = scoped_bitholder(s).unwrap_or(1);
3114            let holder = holder_uint_for_bytes(bytes);
3115            let is_bitset = scoped_is_bitset(s);
3116            let raw = if is_bitset {
3117                format!("static_cast<{holder}>({access}.value)")
3118            } else {
3119                format!("static_cast<{holder}>({access})")
3120            };
3121            if endian == "be" {
3122                writeln!(
3123                    out,
3124                    "{pre}::dds::topic::xcdr2::write_be_origin<{holder}>(zd_out, {origin}, {raw});"
3125                )
3126                .map_err(fmt_err)?;
3127            } else {
3128                writeln!(
3129                    out,
3130                    "{pre}::dds::topic::xcdr2::write_le_origin<{holder}>(zd_out, {origin}, {raw}, zd_max_align);"
3131                )
3132                .map_err(fmt_err)?;
3133            }
3134        }
3135        // enum member: encode as its int32 underlying type (Spec §7.4.1.4.2).
3136        TypeSpec::Scoped(s) if scoped_is_enum(s) => {
3137            // T2: the enum holder narrows to its @bit_bound width (§7.4.5.1).
3138            let ec = enum_wire_ctype(scoped_enum_bytes(s));
3139            if endian == "be" {
3140                writeln!(
3141                    out,
3142                    "{pre}::dds::topic::xcdr2::write_be<{ec}>(zd_out, static_cast<{ec}>({access}));"
3143                )
3144                .map_err(fmt_err)?;
3145            } else {
3146                writeln!(
3147                    out,
3148                    "{pre}::dds::topic::xcdr2::write_le_origin<{ec}>(zd_out, {origin}, static_cast<{ec}>({access}), zd_max_align);"
3149                )
3150                .map_err(fmt_err)?;
3151            }
3152        }
3153        // nested struct member. @final: recurse, encoding each sub-member inline
3154        // (Plain-CDR2, no DHEADER, Spec §7.4.3.4.1). @appendable/@mutable: splice
3155        // the nested type's own encoding — its DHEADER forces 4-alignment, so a
3156        // 4-aligned splice point is byte-identical to standalone under XCDR2.
3157        TypeSpec::Scoped(sc) if scoped_struct(sc).is_some() => {
3158            let Some((def, ext)) = scoped_struct(sc) else {
3159                return Ok(());
3160            };
3161            match ext {
3162                Extensibility::Final => {
3163                    for sm in &def.members {
3164                        let sm_name = &sm.declarators[0].name().text;
3165                        emit_value_write(
3166                            out,
3167                            &sm.type_spec,
3168                            &format!("{access}.{sm_name}()"),
3169                            endian,
3170                            origin,
3171                            &pre,
3172                        )?;
3173                    }
3174                }
3175                Extensibility::Appendable | Extensibility::Mutable => {
3176                    let cpp = scoped_to_cpp(sc);
3177                    let id = next_nest_id();
3178                    writeln!(out, "{pre}{{").map_err(fmt_err)?;
3179                    writeln!(
3180                        out,
3181                        "{pre}    ::dds::topic::xcdr2::pad_to_from_origin(zd_out, {origin}, 4);"
3182                    )
3183                    .map_err(fmt_err)?;
3184                    if endian == "be" {
3185                        writeln!(
3186                            out,
3187                            "{pre}    auto zd_nsb{id} = ::dds::topic::topic_type_support<{cpp}>::encode_be({access}, zd_repr);"
3188                        )
3189                        .map_err(fmt_err)?;
3190                    } else {
3191                        writeln!(
3192                            out,
3193                            "{pre}    auto zd_nsb{id} = ::dds::topic::topic_type_support<{cpp}>::encode({access}, zd_repr);"
3194                        )
3195                        .map_err(fmt_err)?;
3196                    }
3197                    writeln!(
3198                        out,
3199                        "{pre}    zd_out.insert(zd_out.end(), zd_nsb{id}.begin(), zd_nsb{id}.end());"
3200                    )
3201                    .map_err(fmt_err)?;
3202                    writeln!(out, "{pre}}}").map_err(fmt_err)?;
3203                }
3204            }
3205        }
3206        // Bug R3: union member — splice via its own DHEADER-framed
3207        // `topic_type_support<Union>::encode` (4-aligned, identical to an
3208        // appendable nested struct), so the active branch reaches the wire.
3209        TypeSpec::Scoped(sc) if scoped_union(sc).is_some() => {
3210            let Some(u) = scoped_union(sc) else {
3211                return Ok(());
3212            };
3213            match union_extensibility(&u.annotations) {
3214                // @final union (rule (26) FUNION_TYPE): inline disc + selected
3215                // member, NO DHEADER (XTypes 1.3 §7.4.3.4.1). Identical to the
3216                // union's own standalone serializer body, but written into the
3217                // outer buffer at the outer origin so 8-byte members align to
3218                // min(8,4)=4 relative to the top-level DHEADER.
3219                Extensibility::Final => {
3220                    let disc_ts = switch_type_spec(&u.switch_type);
3221                    let disc_cpp = switch_type_to_cpp(&u.switch_type)?;
3222                    emit_value_write(
3223                        out,
3224                        &disc_ts,
3225                        &format!("{access}._d()"),
3226                        endian,
3227                        origin,
3228                        &pre,
3229                    )?;
3230                    emit_union_branch_switch_at(
3231                        out, &u, &disc_cpp, /*decode=*/ false, endian, access, origin,
3232                    )?;
3233                }
3234                // @appendable/@mutable union: splice its own DHEADER-framed
3235                // serializer (4-aligned splice point preserves member alignment
3236                // under XCDR2 max_align=4, §7.4.3.4.2).
3237                Extensibility::Appendable | Extensibility::Mutable => {
3238                    let cpp = scoped_to_cpp(sc);
3239                    let id = next_nest_id();
3240                    writeln!(out, "{pre}{{").map_err(fmt_err)?;
3241                    writeln!(
3242                        out,
3243                        "{pre}    ::dds::topic::xcdr2::pad_to_from_origin(zd_out, {origin}, 4);"
3244                    )
3245                    .map_err(fmt_err)?;
3246                    if endian == "be" {
3247                        writeln!(
3248                            out,
3249                            "{pre}    auto zd_nub{id} = ::dds::topic::topic_type_support<{cpp}>::encode_be({access}, zd_repr);"
3250                        )
3251                        .map_err(fmt_err)?;
3252                    } else {
3253                        writeln!(
3254                            out,
3255                            "{pre}    auto zd_nub{id} = ::dds::topic::topic_type_support<{cpp}>::encode({access}, zd_repr);"
3256                        )
3257                        .map_err(fmt_err)?;
3258                    }
3259                    writeln!(
3260                        out,
3261                        "{pre}    zd_out.insert(zd_out.end(), zd_nub{id}.begin(), zd_nub{id}.end());"
3262                    )
3263                    .map_err(fmt_err)?;
3264                    writeln!(out, "{pre}}}").map_err(fmt_err)?;
3265                }
3266            }
3267        }
3268        TypeSpec::Fixed(_) => {
3269            // fixed<P,S>: raw BCD octets (CORBA §9.3.2.7), alignment 1, no
3270            // length prefix, endian-independent. `::dds::core::Fixed<P,S>`
3271            // stores exactly (P+2)/2 octets — splice them verbatim.
3272            writeln!(
3273                out,
3274                "{pre}{{ const auto& zd_bcd = {access}.bcd_bytes(); zd_out.insert(zd_out.end(), zd_bcd.begin(), zd_bcd.end()); }}"
3275            )
3276            .map_err(fmt_err)?;
3277        }
3278        _ => {
3279            writeln!(out, "{pre}// xcdr2: member type not supported (skip)").map_err(fmt_err)?;
3280        }
3281    }
3282    Ok(())
3283}
3284
3285/// Extensibility of a union from its annotations. Mirrors
3286/// [`struct_extensibility`]: un-annotated → FINAL (the canonical zerodds /
3287/// idl-rust default), so a nested un-annotated union recurses inline with no
3288/// DHEADER (rule (26)).
3289fn union_extensibility(anns: &[Annotation]) -> Extensibility {
3290    struct_extensibility(anns)
3291}
3292
3293/// Emit Mutable-EMHEADER + body for one member. `next_id` is the running
3294/// sequential auto-id counter (XTypes 1.3 §7.3.4.3: `@autoid` defaults to
3295/// SEQUENTIAL — member id = declaration order, vendor-confirmed byte-identical
3296/// to CycloneDDS). An explicit `@id(N)` sets the id and resets the counter to
3297/// N+1. Previously this used an FNV name-hash, which diverged from rust/python/
3298/// ts/csharp + Cyclone on the wire whenever a @mutable member lacked an @id.
3299fn emit_mutable_member_encode(
3300    out: &mut String,
3301    m: &Member,
3302    endian: &str,
3303    next_id: &mut u32,
3304) -> Result<(), CppGenError> {
3305    let m = &normalize_member(m);
3306    if !member_codegen_supported(m) {
3307        for decl in &m.declarators {
3308            let name = &decl.name().text;
3309            *next_id += 1; // consume id slot (kept in lockstep with the decoder)
3310            writeln!(
3311                out,
3312                "        // xcdr2: @shared member '{name}' not supported (skip)"
3313            )
3314            .map_err(fmt_err)?;
3315        }
3316        return Ok(());
3317    }
3318    let is_optional = has_optional_annotation(&m.annotations);
3319    let must_understand = has_named_annotation(&m.annotations, "must_understand");
3320    let id_override = find_uint_annotation(&m.annotations, "id");
3321    let mu_lit = if must_understand { "true" } else { "false" };
3322
3323    for (idx, decl) in m.declarators.iter().enumerate() {
3324        let name = &decl.name().text;
3325        // Resolve the member id FIRST and advance the counter, even on the skip
3326        // paths below — ids are assigned by declaration order regardless of
3327        // whether codegen emits the member, so a later member keeps its id
3328        // (encode + decode skip identically, so they stay in lockstep).
3329        let this_id = match id_override {
3330            Some(id) => id + idx as u32,
3331            None => *next_id,
3332        };
3333        *next_id = this_id + 1;
3334        if !matches!(decl, Declarator::Simple(_)) {
3335            writeln!(
3336                out,
3337                "        // xcdr2: array member '{name}' not supported (skip)"
3338            )
3339            .map_err(fmt_err)?;
3340            continue;
3341        }
3342        if !typespec_supported(&m.type_spec) {
3343            writeln!(
3344                out,
3345                "        // xcdr2: member '{name}' not supported (skip)"
3346            )
3347            .map_err(fmt_err)?;
3348            continue;
3349        }
3350        let id_expr = format!("0x{this_id:x}u");
3351        if is_optional {
3352            // Mutable + optional: skip EMHEADER if absent.
3353            writeln!(out, "        if (zd_v.{name}().has_value()) {{").map_err(fmt_err)?;
3354            emit_mutable_value_emit(
3355                out,
3356                &m.type_spec,
3357                &format!("(*zd_v.{name}())"),
3358                &id_expr,
3359                mu_lit,
3360                endian,
3361                "            ",
3362            )?;
3363            writeln!(out, "        }}").map_err(fmt_err)?;
3364        } else {
3365            emit_mutable_value_emit(
3366                out,
3367                &m.type_spec,
3368                &format!("zd_v.{name}()"),
3369                &id_expr,
3370                mu_lit,
3371                endian,
3372                "        ",
3373            )?;
3374        }
3375    }
3376    Ok(())
3377}
3378
3379/// Emits a single `@mutable` member under XCDR1 / PL_CDR1: a PID-framed
3380/// parameter whose body is the member's plain (positional) field encoding,
3381/// origin-relative to the parameter body start (max_align 8 under XCDR1).
3382/// Mirrors `emit_mutable_member_encode`'s id assignment so the parameter ids
3383/// equal the EMHEADER ids of the XCDR2 path. LE only — PL_CDR1 IS the XCDR1
3384/// framing; `encode_be` is always XCDR2.
3385fn emit_pl_cdr1_member_encode(
3386    out: &mut String,
3387    m: &Member,
3388    next_id: &mut u32,
3389    endian: &str,
3390) -> Result<(), CppGenError> {
3391    let beb = if endian == "be" { "true" } else { "false" };
3392    let m = &normalize_member(m);
3393    if !member_codegen_supported(m) {
3394        for decl in &m.declarators {
3395            *next_id += 1;
3396            let name = &decl.name().text;
3397            writeln!(
3398                out,
3399                "            // xcdr1: @shared member '{name}' not supported (skip)"
3400            )
3401            .map_err(fmt_err)?;
3402        }
3403        return Ok(());
3404    }
3405    let is_optional = has_optional_annotation(&m.annotations);
3406    let id_override = find_uint_annotation(&m.annotations, "id");
3407    for (idx, decl) in m.declarators.iter().enumerate() {
3408        let name = &decl.name().text;
3409        // Resolve the id and advance the counter on every path (lockstep with
3410        // the XCDR2 EMHEADER ids), even when codegen skips the member body.
3411        let this_id = match id_override {
3412            Some(id) => id + idx as u32,
3413            None => *next_id,
3414        };
3415        *next_id = this_id + 1;
3416        if !matches!(decl, Declarator::Simple(_)) {
3417            writeln!(
3418                out,
3419                "            // xcdr1: array member '{name}' not supported (skip)"
3420            )
3421            .map_err(fmt_err)?;
3422            continue;
3423        }
3424        if !typespec_supported(&m.type_spec) {
3425            writeln!(
3426                out,
3427                "            // xcdr1: member '{name}' not supported (skip)"
3428            )
3429            .map_err(fmt_err)?;
3430            continue;
3431        }
3432        let access = if is_optional {
3433            format!("(*zd_v.{name}())")
3434        } else {
3435            format!("zd_v.{name}()")
3436        };
3437        let ind = if is_optional {
3438            "                "
3439        } else {
3440            "            "
3441        };
3442        if is_optional {
3443            // PL_CDR1 optional: present -> emit the parameter; absent -> omit it
3444            // entirely (no present-flag; absence = the parameter is not in the
3445            // list), exactly like the XCDR2 EMHEADER-skip.
3446            writeln!(out, "            if (zd_v.{name}().has_value()) {{").map_err(fmt_err)?;
3447        }
3448        writeln!(out, "{ind}{{").map_err(fmt_err)?;
3449        writeln!(
3450            out,
3451            "{ind}    auto zd_pm = ::dds::topic::xcdr2::pl_cdr1_member_begin(zd_out, 0x{this_id:x}u, {beb});"
3452        )
3453        .map_err(fmt_err)?;
3454        writeln!(
3455            out,
3456            "{ind}    const size_t zd_origin = zd_pm.body_start; (void)zd_origin;"
3457        )
3458        .map_err(fmt_err)?;
3459        emit_value_write(out, &m.type_spec, &access, endian, "zd_origin", ind)?;
3460        writeln!(
3461            out,
3462            "{ind}    ::dds::topic::xcdr2::pl_cdr1_member_end(zd_out, zd_pm, {beb});"
3463        )
3464        .map_err(fmt_err)?;
3465        writeln!(out, "{ind}}}").map_err(fmt_err)?;
3466        if is_optional {
3467            writeln!(out, "            }}").map_err(fmt_err)?;
3468        }
3469    }
3470    Ok(())
3471}
3472
3473fn emit_mutable_value_emit(
3474    out: &mut String,
3475    ts: &TypeSpec,
3476    access: &str,
3477    id_expr: &str,
3478    mu_lit: &str,
3479    endian: &str,
3480    indent: &str,
3481) -> Result<(), CppGenError> {
3482    let beb = if endian == "be" { "true" } else { "false" };
3483    match ts {
3484        TypeSpec::Primitive(PrimitiveType::Boolean) => {
3485            writeln!(
3486                out,
3487                "{indent}::dds::topic::xcdr2::emheader_u8(zd_out, zd_origin, {id_expr}, {mu_lit}, static_cast<uint8_t>({access} ? 1 : 0), {beb});"
3488            )
3489            .map_err(fmt_err)?;
3490        }
3491        TypeSpec::Primitive(PrimitiveType::Octet) => {
3492            writeln!(
3493                out,
3494                "{indent}::dds::topic::xcdr2::emheader_u8(zd_out, zd_origin, {id_expr}, {mu_lit}, {access}, {beb});"
3495            )
3496            .map_err(fmt_err)?;
3497        }
3498        TypeSpec::Primitive(p) => {
3499            let cpp_ty = primitive_to_cpp(*p);
3500            let size = primitive_size(*p);
3501            // XTypes 1.3 §7.4.3.4.2 (Bug XV-mut): a fixed-size primitive @mutable
3502            // member uses the COMPACT length code by wire size — NOT the universal
3503            // LC=4 NEXTINT frame. 2-byte → LC=1 (`emheader_2`), 4-byte → LC=2
3504            // (`emheader_4`), 8-byte → LC=3 (`emheader_8`); none of them serialize
3505            // a NEXTINT. This matches the Rust reference (`MutableStructEncoder`
3506            // via `LengthCode`) and is cross-vendor-validated against
3507            // CycloneDDS/RTI/FastDDS. BE: the helper writes little-endian member-id
3508            // EMHEADER (ambient-LE per §7.4.3.4.5) but the body endian must follow
3509            // the stream — for BE we emit the EMHEADER + raw body inline.
3510            let helper = match size {
3511                2 => Some("emheader_2"),
3512                4 => Some("emheader_4"),
3513                8 => Some("emheader_8"),
3514                _ => None,
3515            };
3516            match helper {
3517                Some(h) if endian != "be" => {
3518                    writeln!(
3519                        out,
3520                        "{indent}::dds::topic::xcdr2::{h}<{cpp_ty}>(zd_out, zd_origin, {id_expr}, {mu_lit}, {access});"
3521                    )
3522                    .map_err(fmt_err)?;
3523                }
3524                Some(_) => {
3525                    // BE: replicate the compact-EMHEADER helper inline so the
3526                    // primitive body is big-endian (the helpers are LE-only).
3527                    let lc = match size {
3528                        2 => 1,
3529                        4 => 2,
3530                        _ => 3,
3531                    };
3532                    writeln!(
3533                        out,
3534                        "{indent}{{ ::dds::topic::xcdr2::pad_to_from_origin(zd_out, zd_origin, 4);"
3535                    )
3536                    .map_err(fmt_err)?;
3537                    writeln!(
3538                        out,
3539                        "{indent}    ::dds::topic::xcdr2::emheader_write(zd_out, ::dds::topic::xcdr2::emheader_make({lc}u, {id_expr}, {mu_lit}), {beb});"
3540                    )
3541                    .map_err(fmt_err)?;
3542                    writeln!(
3543                        out,
3544                        "{indent}    ::dds::topic::xcdr2::write_be_raw<{cpp_ty}>(zd_out, {access}); }}"
3545                    )
3546                    .map_err(fmt_err)?;
3547                }
3548                None => {
3549                    writeln!(
3550                        out,
3551                        "{indent}// xcdr2: unexpected primitive size {size} (skip)"
3552                    )
3553                    .map_err(fmt_err)?;
3554                }
3555            }
3556        }
3557        TypeSpec::String(s) if !s.wide => {
3558            // Bounded narrow `string<N>` (DDS-XTypes §7.4.3): byte-length check.
3559            if let Some(b) = &s.bound {
3560                let bv = const_expr_to_cpp(b);
3561                writeln!(
3562                    out,
3563                    "{indent}if ({access}.size() > {bv}) throw std::length_error(\"bounded string length exceeds its IDL bound ({bv})\");"
3564                )
3565                .map_err(fmt_err)?;
3566            }
3567            // EMHEADER LC=5 (Bug XV-mut): a string member REUSES its own leading
3568            // uint32 length prefix as the NEXTINT — XTypes 1.3 §7.4.3.4.2, EMHEADER
3569            // bits 30-28 = 101. So we write the EMHEADER with LC=5 and then the
3570            // string body (length prefix + bytes) directly; NO separate NEXTINT is
3571            // serialized (the prefix doubles as it). Cross-vendor-validated against
3572            // the Rust reference (`LengthCode::Lc5` / `reuses_leading_len`).
3573            writeln!(
3574                out,
3575                "{indent}{{ ::dds::topic::xcdr2::pad_to_from_origin(zd_out, zd_origin, 4);"
3576            )
3577            .map_err(fmt_err)?;
3578            writeln!(
3579                out,
3580                "{indent}    ::dds::topic::xcdr2::emheader_write(zd_out, ::dds::topic::xcdr2::emheader_make(5u, {id_expr}, {mu_lit}), {beb});"
3581            )
3582            .map_err(fmt_err)?;
3583            // The length prefix sits at the (4-aligned) EMHEADER body start; use it
3584            // as the origin for the string-len alignment count.
3585            writeln!(
3586                out,
3587                "{indent}    {{ const auto zd_body_origin = zd_out.size(); (void)zd_body_origin;"
3588            )
3589            .map_err(fmt_err)?;
3590            if endian == "be" {
3591                writeln!(
3592                    out,
3593                    "{indent}      ::dds::topic::xcdr2::write_string_be(zd_out, {access});"
3594                )
3595                .map_err(fmt_err)?;
3596            } else {
3597                writeln!(
3598                    out,
3599                    "{indent}      ::dds::topic::xcdr2::write_string_origin(zd_out, zd_body_origin, {access}, zd_max_align);"
3600                )
3601                .map_err(fmt_err)?;
3602            }
3603            writeln!(out, "{indent}    }} }}").map_err(fmt_err)?;
3604        }
3605        TypeSpec::String(s) if s.wide => {
3606            // Bounded `wstring<N>` (DDS-XTypes §7.4.3): wide-char-length check.
3607            if let Some(b) = &s.bound {
3608                let bv = const_expr_to_cpp(b);
3609                writeln!(
3610                    out,
3611                    "{indent}if ({access}.size() > {bv}) throw std::length_error(\"bounded wstring length exceeds its IDL bound ({bv})\");"
3612                )
3613                .map_err(fmt_err)?;
3614            }
3615            // EMHEADER LC=5 (Bug XV-mut): like a narrow string, a `wstring`
3616            // serializes a leading uint32 octet-length prefix, which LC=5 reuses as
3617            // the NEXTINT (no separate NEXTINT). Mirrors the Rust reference
3618            // (`TypeSpec::String(_) => Lc5`).
3619            writeln!(
3620                out,
3621                "{indent}{{ ::dds::topic::xcdr2::pad_to_from_origin(zd_out, zd_origin, 4);"
3622            )
3623            .map_err(fmt_err)?;
3624            writeln!(
3625                out,
3626                "{indent}    ::dds::topic::xcdr2::emheader_write(zd_out, ::dds::topic::xcdr2::emheader_make(5u, {id_expr}, {mu_lit}), {beb});"
3627            )
3628            .map_err(fmt_err)?;
3629            writeln!(
3630                out,
3631                "{indent}    {{ const auto zd_body_origin = zd_out.size(); (void)zd_body_origin;"
3632            )
3633            .map_err(fmt_err)?;
3634            if endian == "be" {
3635                writeln!(
3636                    out,
3637                    "{indent}      ::dds::topic::xcdr2::write_wstring_be(zd_out, {access});"
3638                )
3639                .map_err(fmt_err)?;
3640            } else {
3641                writeln!(
3642                    out,
3643                    "{indent}      ::dds::topic::xcdr2::write_wstring_origin(zd_out, zd_body_origin, {access}, zd_max_align);"
3644                )
3645                .map_err(fmt_err)?;
3646            }
3647            writeln!(out, "{indent}    }} }}").map_err(fmt_err)?;
3648        }
3649        TypeSpec::Sequence(seq) => {
3650            // Bounded `sequence<T, N>` (DDS-XTypes §7.4.3): over-bound = throw.
3651            if let Some(b) = &seq.bound {
3652                let bv = const_expr_to_cpp(b);
3653                writeln!(
3654                    out,
3655                    "{indent}if ({access}.size() > {bv}) throw std::length_error(\"bounded sequence length exceeds its IDL bound ({bv})\");"
3656                )
3657                .map_err(fmt_err)?;
3658            }
3659            // FINDING T1b: a non-primitive-element sequence's body BEGINS with
3660            // its own DHEADER (a 4-byte length word). XTypes 1.3 §7.4.3.4.2: such
3661            // a member uses EMHEADER LengthCode-5, which REUSES that leading
3662            // DHEADER as the NEXTINT — NO separate NEXTINT is serialized. A
3663            // `sequence<primitive>` has a bare element count (not a byte length)
3664            // and so stays on the universal LC=4 NEXTINT frame. This mirrors the
3665            // Rust reference (`member_body_has_leading_dheader` → `LengthCode::Lc5`)
3666            // and CycloneDDS / RTI / FastDDS byte-for-byte.
3667            let seq_inner_dh = !matches!(&*seq.elem, TypeSpec::Primitive(_));
3668            if seq_inner_dh {
3669                // LC=5: EMHEADER with no NEXTINT; the seq DHEADER below doubles
3670                // as the NEXTINT (length word). body_origin = start of that word.
3671                writeln!(
3672                    out,
3673                    "{indent}{{ ::dds::topic::xcdr2::pad_to_from_origin(zd_out, zd_origin, 4);"
3674                )
3675                .map_err(fmt_err)?;
3676                writeln!(
3677                    out,
3678                    "{indent}    ::dds::topic::xcdr2::emheader_write(zd_out, ::dds::topic::xcdr2::emheader_make(5u, {id_expr}, {mu_lit}), {beb});"
3679                )
3680                .map_err(fmt_err)?;
3681                writeln!(
3682                    out,
3683                    "{indent}    {{ const auto zd_body_origin = zd_out.size(); (void)zd_body_origin;"
3684                )
3685                .map_err(fmt_err)?;
3686            } else {
3687                // LC=4: universal NEXTINT frame (bare element count body).
3688                writeln!(
3689                    out,
3690                    "{indent}{{ const auto zd_sub = ::dds::topic::xcdr2::emheader_nextint_begin(zd_out, zd_origin, {id_expr}, {mu_lit}, {beb});"
3691                )
3692                .map_err(fmt_err)?;
3693                writeln!(
3694                    out,
3695                    "{indent}    {{ const auto zd_body_origin = zd_sub.body_start; (void)zd_body_origin;"
3696                )
3697                .map_err(fmt_err)?;
3698            }
3699            if seq_inner_dh {
3700                writeln!(
3701                    out,
3702                    "{indent}      const auto zd_seq_dh = ::dds::topic::xcdr2::dheader_begin_r(zd_out, zd_repr);"
3703                )
3704                .map_err(fmt_err)?;
3705            }
3706            if endian == "be" {
3707                writeln!(
3708                    out,
3709                    "{indent}      ::dds::topic::xcdr2::write_be<uint32_t>(zd_out, static_cast<uint32_t>({access}.size()));"
3710                )
3711                .map_err(fmt_err)?;
3712            } else {
3713                writeln!(
3714                    out,
3715                    "{indent}      ::dds::topic::xcdr2::write_le_origin<uint32_t>(zd_out, zd_body_origin, static_cast<uint32_t>({access}.size()), zd_max_align);"
3716                )
3717                .map_err(fmt_err)?;
3718            }
3719            if matches!(&*seq.elem, TypeSpec::Primitive(PrimitiveType::Octet)) {
3720                // sequence<octet>: raw byte block instead of a per-byte loop.
3721                writeln!(
3722                    out,
3723                    "{indent}      zd_out.insert(zd_out.end(), {access}.begin(), {access}.end());"
3724                )
3725                .map_err(fmt_err)?;
3726            } else {
3727                writeln!(out, "{indent}      for (const auto& zd_e : {access}) {{")
3728                    .map_err(fmt_err)?;
3729                match &*seq.elem {
3730                    TypeSpec::Primitive(PrimitiveType::Boolean) => {
3731                        writeln!(
3732                            out,
3733                            "{indent}        ::dds::topic::xcdr2::write_bool(zd_out, zd_e);"
3734                        )
3735                        .map_err(fmt_err)?;
3736                    }
3737                    TypeSpec::Primitive(PrimitiveType::Octet) => {
3738                        writeln!(
3739                            out,
3740                            "{indent}        ::dds::topic::xcdr2::write_u8(zd_out, zd_e);"
3741                        )
3742                        .map_err(fmt_err)?;
3743                    }
3744                    TypeSpec::Primitive(p) => {
3745                        let cpp_ty = primitive_to_cpp(*p);
3746                        if endian == "be" {
3747                            writeln!(
3748                            out,
3749                            "{indent}        ::dds::topic::xcdr2::write_be<{cpp_ty}>(zd_out, zd_e);"
3750                        )
3751                        .map_err(fmt_err)?;
3752                        } else {
3753                            writeln!(out, "{indent}        ::dds::topic::xcdr2::write_le_origin<{cpp_ty}>(zd_out, zd_body_origin, zd_e, zd_max_align);").map_err(fmt_err)?;
3754                        }
3755                    }
3756                    TypeSpec::String(s) if !s.wide => {
3757                        if endian == "be" {
3758                            writeln!(
3759                                out,
3760                                "{indent}        ::dds::topic::xcdr2::write_string_be(zd_out, zd_e);"
3761                            )
3762                            .map_err(fmt_err)?;
3763                        } else {
3764                            writeln!(out, "{indent}        ::dds::topic::xcdr2::write_string_origin(zd_out, zd_body_origin, zd_e, zd_max_align);").map_err(fmt_err)?;
3765                        }
3766                    }
3767                    // wstring / enum / nested struct (any extensibility) elements:
3768                    // recurse with the EMHEADER body-origin (identical to the
3769                    // plain-path arms; non-final elements pad-to-4 + splice).
3770                    TypeSpec::String(_) => {
3771                        emit_value_write(
3772                            out,
3773                            &seq.elem,
3774                            "zd_e",
3775                            endian,
3776                            "zd_body_origin",
3777                            &format!("{indent}        "),
3778                        )?;
3779                    }
3780                    TypeSpec::Scoped(sc)
3781                        if scoped_is_enum(sc)
3782                            || scoped_struct(sc).is_some()
3783                            || scoped_union(sc).is_some() =>
3784                    {
3785                        emit_value_write(
3786                            out,
3787                            &seq.elem,
3788                            "zd_e",
3789                            endian,
3790                            "zd_body_origin",
3791                            &format!("{indent}        "),
3792                        )?;
3793                    }
3794                    // nested sequence / map element (each emits its own DHEADER).
3795                    TypeSpec::Sequence(_) | TypeSpec::Map(_) => {
3796                        emit_value_write(
3797                            out,
3798                            &seq.elem,
3799                            "zd_e",
3800                            endian,
3801                            "zd_body_origin",
3802                            &format!("{indent}        "),
3803                        )?;
3804                    }
3805                    _ => {
3806                        writeln!(
3807                            out,
3808                            "{indent}        // xcdr2: nested seq-elem not supported"
3809                        )
3810                        .map_err(fmt_err)?;
3811                    }
3812                }
3813                writeln!(out, "{indent}      }}").map_err(fmt_err)?;
3814            }
3815            if seq_inner_dh {
3816                writeln!(
3817                    out,
3818                    "{indent}      ::dds::topic::xcdr2::dheader_end_r(zd_out, zd_seq_dh, {beb}, zd_repr);"
3819                )
3820                .map_err(fmt_err)?;
3821            }
3822            writeln!(out, "{indent}    }}").map_err(fmt_err)?;
3823            if seq_inner_dh {
3824                // LC=5: no NEXTINT to patch (the seq DHEADER was the NEXTINT).
3825                writeln!(out, "{indent}}}").map_err(fmt_err)?;
3826            } else {
3827                writeln!(
3828                    out,
3829                    "{indent}    ::dds::topic::xcdr2::emheader_nextint_end(zd_out, zd_sub, {beb}); }}"
3830                )
3831                .map_err(fmt_err)?;
3832            }
3833        }
3834        // enum member: 4-byte int32 -> compact LC=2 EMHEADER (no NEXTINT).
3835        TypeSpec::Scoped(s) if scoped_is_enum(s) => {
3836            let ec = enum_wire_ctype(scoped_enum_bytes(s));
3837            writeln!(
3838                out,
3839                "{indent}::dds::topic::xcdr2::emheader_4<{ec}>(zd_out, zd_origin, {id_expr}, {mu_lit}, static_cast<{ec}>({access}));"
3840            )
3841            .map_err(fmt_err)?;
3842        }
3843        // nested struct member as a @mutable member. FINDING T1b: a @final
3844        // nested struct has NO inner DHEADER → its body does NOT begin with a
3845        // length word, so it stays on the universal LC=4 NEXTINT frame. A
3846        // nested @appendable/@mutable struct's encoding BEGINS with its own
3847        // DHEADER (a 4-byte length word); XTypes 1.3 §7.4.3.4.2 LengthCode-5
3848        // REUSES that leading DHEADER as the NEXTINT — NO separate NEXTINT is
3849        // serialized. This mirrors the Rust reference
3850        // (`member_body_has_leading_dheader` → `LengthCode::Lc5`) and
3851        // CycloneDDS / RTI / FastDDS byte-for-byte.
3852        TypeSpec::Scoped(sc) if scoped_struct(sc).is_some() => {
3853            let Some((def, ext)) = scoped_struct(sc) else {
3854                return Ok(());
3855            };
3856            match ext {
3857                Extensibility::Final => {
3858                    // LC=4: NEXTINT frame around the tight-packed (no DHEADER) body.
3859                    writeln!(
3860                        out,
3861                        "{indent}{{ const auto zd_sub = ::dds::topic::xcdr2::emheader_nextint_begin(zd_out, zd_origin, {id_expr}, {mu_lit}, {beb});"
3862                    )
3863                    .map_err(fmt_err)?;
3864                    writeln!(
3865                        out,
3866                        "{indent}    {{ const auto zd_body_origin = zd_sub.body_start; (void)zd_body_origin;"
3867                    )
3868                    .map_err(fmt_err)?;
3869                    for sm in &def.members {
3870                        let sm_name = &sm.declarators[0].name().text;
3871                        emit_value_write(
3872                            out,
3873                            &sm.type_spec,
3874                            &format!("{access}.{sm_name}()"),
3875                            endian,
3876                            "zd_body_origin",
3877                            &format!("{indent}      "),
3878                        )?;
3879                    }
3880                    writeln!(out, "{indent}    }}").map_err(fmt_err)?;
3881                    writeln!(
3882                        out,
3883                        "{indent}    ::dds::topic::xcdr2::emheader_nextint_end(zd_out, zd_sub, {beb}); }}"
3884                    )
3885                    .map_err(fmt_err)?;
3886                }
3887                Extensibility::Appendable | Extensibility::Mutable => {
3888                    // LC=5: EMHEADER with no NEXTINT; the spliced nested encoding
3889                    // BEGINS with its own DHEADER, which doubles as the NEXTINT.
3890                    let cpp = scoped_to_cpp(sc);
3891                    let id = next_nest_id();
3892                    writeln!(
3893                        out,
3894                        "{indent}{{ ::dds::topic::xcdr2::pad_to_from_origin(zd_out, zd_origin, 4);"
3895                    )
3896                    .map_err(fmt_err)?;
3897                    writeln!(
3898                        out,
3899                        "{indent}    ::dds::topic::xcdr2::emheader_write(zd_out, ::dds::topic::xcdr2::emheader_make(5u, {id_expr}, {mu_lit}), {beb});"
3900                    )
3901                    .map_err(fmt_err)?;
3902                    if endian == "be" {
3903                        writeln!(
3904                            out,
3905                            "{indent}    auto zd_nsb{id} = ::dds::topic::topic_type_support<{cpp}>::encode_be({access}, zd_repr);"
3906                        )
3907                        .map_err(fmt_err)?;
3908                    } else {
3909                        writeln!(
3910                            out,
3911                            "{indent}    auto zd_nsb{id} = ::dds::topic::topic_type_support<{cpp}>::encode({access}, zd_repr);"
3912                        )
3913                        .map_err(fmt_err)?;
3914                    }
3915                    writeln!(
3916                        out,
3917                        "{indent}    zd_out.insert(zd_out.end(), zd_nsb{id}.begin(), zd_nsb{id}.end()); }}"
3918                    )
3919                    .map_err(fmt_err)?;
3920                }
3921            }
3922        }
3923        // map<K,V> member: FINDING T1b. A map is always a non-primitive
3924        // collection whose body BEGINS with its own DHEADER (a 4-byte length
3925        // word), so per XTypes 1.3 §7.4.3.4.2 it uses LengthCode-5, REUSING that
3926        // leading DHEADER as the NEXTINT — NO separate NEXTINT. Mirrors the Rust
3927        // reference (`TypeSpec::Map(_)` → `LengthCode::Lc5`) and the vendors.
3928        TypeSpec::Map(m) => {
3929            // FINDING (primitive-map): a `map<primitive,primitive>` body has NO
3930            // leading DHEADER (XTypes 1.3 §7.4.3.5) — like a `sequence<primitive>`
3931            // it carries a bare element count, so it stays on the universal LC=4
3932            // NEXTINT frame. A `map<long,Pt>` (non-primitive element) DOES begin
3933            // with its own DHEADER and uses LengthCode-5, REUSING that leading
3934            // DHEADER as the NEXTINT. Mirrors the @mutable sequence arm above.
3935            let map_inner_dh = !map_pair_is_primitive(&m.key, &m.value);
3936            if map_inner_dh {
3937                writeln!(
3938                    out,
3939                    "{indent}{{ ::dds::topic::xcdr2::pad_to_from_origin(zd_out, zd_origin, 4);"
3940                )
3941                .map_err(fmt_err)?;
3942                writeln!(
3943                    out,
3944                    "{indent}    ::dds::topic::xcdr2::emheader_write(zd_out, ::dds::topic::xcdr2::emheader_make(5u, {id_expr}, {mu_lit}), {beb});"
3945                )
3946                .map_err(fmt_err)?;
3947                writeln!(
3948                    out,
3949                    "{indent}    {{ const auto zd_body_origin = zd_out.size(); (void)zd_body_origin;"
3950                )
3951                .map_err(fmt_err)?;
3952                writeln!(
3953                    out,
3954                    "{indent}      const auto zd_map_dh = ::dds::topic::xcdr2::dheader_begin_r(zd_out, zd_repr);"
3955                )
3956                .map_err(fmt_err)?;
3957            } else {
3958                // LC=4: universal NEXTINT frame (bare element count body).
3959                writeln!(
3960                    out,
3961                    "{indent}{{ const auto zd_sub = ::dds::topic::xcdr2::emheader_nextint_begin(zd_out, zd_origin, {id_expr}, {mu_lit}, {beb});"
3962                )
3963                .map_err(fmt_err)?;
3964                writeln!(
3965                    out,
3966                    "{indent}    {{ const auto zd_body_origin = zd_sub.body_start; (void)zd_body_origin;"
3967                )
3968                .map_err(fmt_err)?;
3969            }
3970            if endian == "be" {
3971                writeln!(out, "{indent}      ::dds::topic::xcdr2::write_be<uint32_t>(zd_out, static_cast<uint32_t>({access}.size()));").map_err(fmt_err)?;
3972            } else {
3973                writeln!(out, "{indent}      ::dds::topic::xcdr2::write_le_origin<uint32_t>(zd_out, zd_body_origin, static_cast<uint32_t>({access}.size()), zd_max_align);").map_err(fmt_err)?;
3974            }
3975            writeln!(out, "{indent}      for (const auto& zd_kv : {access}) {{")
3976                .map_err(fmt_err)?;
3977            let kv_indent = format!("{indent}        ");
3978            emit_value_write(
3979                out,
3980                &m.key,
3981                "zd_kv.first",
3982                endian,
3983                "zd_body_origin",
3984                &kv_indent,
3985            )?;
3986            emit_value_write(
3987                out,
3988                &m.value,
3989                "zd_kv.second",
3990                endian,
3991                "zd_body_origin",
3992                &kv_indent,
3993            )?;
3994            writeln!(out, "{indent}      }}").map_err(fmt_err)?;
3995            if map_inner_dh {
3996                writeln!(
3997                    out,
3998                    "{indent}      ::dds::topic::xcdr2::dheader_end_r(zd_out, zd_map_dh, {beb}, zd_repr);"
3999                )
4000                .map_err(fmt_err)?;
4001                writeln!(out, "{indent}    }}").map_err(fmt_err)?;
4002                // LC=5: no NEXTINT to patch (the map DHEADER was the NEXTINT).
4003                writeln!(out, "{indent}}}").map_err(fmt_err)?;
4004            } else {
4005                writeln!(out, "{indent}    }}").map_err(fmt_err)?;
4006                writeln!(
4007                    out,
4008                    "{indent}    ::dds::topic::xcdr2::emheader_nextint_end(zd_out, zd_sub, {beb}); }}"
4009                )
4010                .map_err(fmt_err)?;
4011            }
4012        }
4013        TypeSpec::Fixed(_) => {
4014            // fixed<P,S> @mutable member: raw BCD body (no leading length word),
4015            // so the universal LC=4 NEXTINT frame (matching the Rust reference's
4016            // `encode_member` default — see `mutable_member_length_code` → None).
4017            writeln!(
4018                out,
4019                "{indent}{{ const auto zd_sub = ::dds::topic::xcdr2::emheader_nextint_begin(zd_out, zd_origin, {id_expr}, {mu_lit}, {beb});"
4020            )
4021            .map_err(fmt_err)?;
4022            writeln!(
4023                out,
4024                "{indent}    {{ const auto& zd_bcd = {access}.bcd_bytes(); zd_out.insert(zd_out.end(), zd_bcd.begin(), zd_bcd.end()); }}"
4025            )
4026            .map_err(fmt_err)?;
4027            writeln!(
4028                out,
4029                "{indent}    ::dds::topic::xcdr2::emheader_nextint_end(zd_out, zd_sub, {beb}); }}"
4030            )
4031            .map_err(fmt_err)?;
4032        }
4033        _ => {
4034            writeln!(out, "{indent}// xcdr2: unsupported member type").map_err(fmt_err)?;
4035        }
4036    }
4037    Ok(())
4038}
4039
4040/// `true` when BOTH the map key and value are XCDR `IS_PRIMITIVE` scalars, so the
4041/// map carries NO collection DHEADER (XTypes 1.3 §7.4.3.5; mirrors cdr-core
4042/// `needs_collection_dheader(.., K::IS_PRIMITIVE && V::IS_PRIMITIVE)` and the
4043/// PARRAY rule already applied to primitive arrays above). `map<long,Pt>` keeps
4044/// its DHEADER; `map<long,long>` omits it (FastDDS/OpenDDS-confirmed).
4045fn map_pair_is_primitive(key: &TypeSpec, value: &TypeSpec) -> bool {
4046    matches!(key, TypeSpec::Primitive(_)) && matches!(value, TypeSpec::Primitive(_))
4047}
4048
4049fn primitive_size(p: PrimitiveType) -> usize {
4050    use zerodds_idl::ast::{FloatingType, IntegerType};
4051    match p {
4052        PrimitiveType::Boolean => 1,
4053        PrimitiveType::Octet => 1,
4054        PrimitiveType::Char => 1,
4055        PrimitiveType::WideChar => 2,
4056        PrimitiveType::Integer(i) => match i {
4057            IntegerType::Int8 | IntegerType::UInt8 => 1,
4058            IntegerType::Short | IntegerType::UShort | IntegerType::Int16 | IntegerType::UInt16 => {
4059                2
4060            }
4061            IntegerType::Long | IntegerType::ULong | IntegerType::Int32 | IntegerType::UInt32 => 4,
4062            IntegerType::LongLong
4063            | IntegerType::ULongLong
4064            | IntegerType::Int64
4065            | IntegerType::UInt64 => 8,
4066        },
4067        PrimitiveType::Floating(f) => match f {
4068            FloatingType::Float => 4,
4069            FloatingType::Double => 8,
4070            FloatingType::LongDouble => 16,
4071        },
4072    }
4073}
4074
4075fn emit_decode_fn(
4076    out: &mut String,
4077    cpp_fqn: &str,
4078    s: &StructDef,
4079    ext: Extensibility,
4080    phase: TtsPhase,
4081) -> Result<(), CppGenError> {
4082    if phase == TtsPhase::Decl {
4083        writeln!(
4084            out,
4085            "    static {cpp_fqn} decode(const uint8_t* zd_buf, size_t zd_len, \
4086             ::dds::topic::xcdr2::XcdrVersion zd_repr, bool zd_be = false);"
4087        )
4088        .map_err(fmt_err)?;
4089        return Ok(());
4090    }
4091    writeln!(
4092        out,
4093        "inline {cpp_fqn} topic_type_support<{cpp_fqn}>::decode(const uint8_t* zd_buf, size_t zd_len, \
4094         ::dds::topic::xcdr2::XcdrVersion zd_repr, bool zd_be) {{"
4095    )
4096    .map_err(fmt_err)?;
4097    writeln!(out, "        size_t zd_pos = 0;").map_err(fmt_err)?;
4098    writeln!(out, "        {cpp_fqn} zd_v;").map_err(fmt_err)?;
4099    // The XCDR version controls alignment: XCDR2 caps 8-byte primitives
4100    // to 4-byte boundaries (XTypes 1.3 §7.4.3.4.2), XCDR1 does not. `zd_be`
4101    // selects the wire byte order (false = little-endian, the canonical wire).
4102    writeln!(
4103        out,
4104        "        const size_t zd_max_align = ::dds::topic::xcdr2::xcdr_max_align(zd_repr);"
4105    )
4106    .map_err(fmt_err)?;
4107    writeln!(
4108        out,
4109        "        (void)zd_buf; (void)zd_len; (void)zd_pos; (void)zd_max_align; (void)zd_be;"
4110    )
4111    .map_err(fmt_err)?;
4112
4113    match ext {
4114        Extensibility::Final => {
4115            writeln!(out, "        const size_t zd_origin = 0;").map_err(fmt_err)?;
4116            writeln!(out, "        (void)zd_origin;").map_err(fmt_err)?;
4117            for m in &s.members {
4118                emit_plain_member_decode(out, m, "zd_origin")?;
4119            }
4120        }
4121        Extensibility::Appendable => {
4122            // XCDR1 has NO DHEADER (origin = stream start, max_align 8); XCDR2
4123            // reads the 4-byte DHEADER first (origin after, max_align 4). The
4124            // member reads are identical; only the DHEADER framing differs.
4125            writeln!(
4126                out,
4127                "        const bool zd_x1 = (zd_repr == ::dds::topic::xcdr2::XcdrVersion::Xcdr1);"
4128            )
4129            .map_err(fmt_err)?;
4130            writeln!(out, "        size_t zd_end = zd_len;").map_err(fmt_err)?;
4131            writeln!(out, "        if (!zd_x1) {{").map_err(fmt_err)?;
4132            writeln!(
4133                out,
4134                "            const auto zd_dh = ::dds::topic::xcdr2::dheader_read(zd_buf, zd_pos, zd_len, zd_be);"
4135            )
4136            .map_err(fmt_err)?;
4137            writeln!(out, "            zd_end = zd_pos + zd_dh;").map_err(fmt_err)?;
4138            writeln!(out, "        }}").map_err(fmt_err)?;
4139            writeln!(out, "        const size_t zd_origin = zd_pos;").map_err(fmt_err)?;
4140            writeln!(out, "        (void)zd_end;").map_err(fmt_err)?;
4141            for m in &s.members {
4142                emit_plain_member_decode(out, m, "zd_origin")?;
4143            }
4144            // Skip trailing bytes (forward-compat with appendable extension);
4145            // only meaningful under XCDR2 where the DHEADER bounded the body.
4146            writeln!(
4147                out,
4148                "        if (!zd_x1 && zd_pos < zd_end) zd_pos = zd_end;"
4149            )
4150            .map_err(fmt_err)?;
4151        }
4152        Extensibility::Mutable => {
4153            // XCDR1 -> PL_CDR1 (no DHEADER, PID-framed parameters to a sentinel);
4154            // XCDR2 -> PL_CDR2 (DHEADER + EMHEADER per member). LE and BE share
4155            // the PL_CDR2 path; only XCDR1-LE takes PL_CDR1.
4156            writeln!(
4157                out,
4158                "        if (zd_repr == ::dds::topic::xcdr2::XcdrVersion::Xcdr1) {{"
4159            )
4160            .map_err(fmt_err)?;
4161            writeln!(out, "            while (zd_pos + 4 <= zd_len) {{").map_err(fmt_err)?;
4162            writeln!(
4163                out,
4164                "                const auto zd_ph = ::dds::topic::xcdr2::pl_cdr1_read_header(zd_buf, zd_pos, zd_len, zd_be);"
4165            )
4166            .map_err(fmt_err)?;
4167            writeln!(out, "                if (zd_ph.is_end) break;").map_err(fmt_err)?;
4168            writeln!(
4169                out,
4170                "                const size_t zd_pl_origin = zd_pos; (void)zd_pl_origin;"
4171            )
4172            .map_err(fmt_err)?;
4173            writeln!(
4174                out,
4175                "                const size_t zd_pl_end = zd_pos + zd_ph.body_len;"
4176            )
4177            .map_err(fmt_err)?;
4178            writeln!(out, "                switch (zd_ph.member_id) {{").map_err(fmt_err)?;
4179            let mut zd_pl_id = 0u32;
4180            for m in &s.members {
4181                emit_pl_cdr1_member_decode_case(out, m, &mut zd_pl_id)?;
4182            }
4183            writeln!(out, "                    default: break;").map_err(fmt_err)?;
4184            writeln!(out, "                }}").map_err(fmt_err)?;
4185            writeln!(
4186                out,
4187                "                if (zd_pos < zd_pl_end) zd_pos = zd_pl_end;"
4188            )
4189            .map_err(fmt_err)?;
4190            writeln!(out, "                ::dds::topic::xcdr2::pl_cdr1_skip_pad(zd_pos, zd_len, zd_ph.body_len);").map_err(fmt_err)?;
4191            writeln!(out, "            }}").map_err(fmt_err)?;
4192            writeln!(out, "            return zd_v;").map_err(fmt_err)?;
4193            writeln!(out, "        }}").map_err(fmt_err)?;
4194            writeln!(
4195                out,
4196                "        const auto zd_dh = ::dds::topic::xcdr2::dheader_read(zd_buf, zd_pos, zd_len, zd_be);"
4197            )
4198            .map_err(fmt_err)?;
4199            writeln!(out, "        const size_t zd_origin = zd_pos;").map_err(fmt_err)?;
4200            writeln!(out, "        const size_t zd_end = zd_origin + zd_dh;").map_err(fmt_err)?;
4201            writeln!(out, "        while (zd_pos + 4 <= zd_end) {{").map_err(fmt_err)?;
4202            writeln!(
4203                out,
4204                "            const auto zd_h = ::dds::topic::xcdr2::emheader_read(zd_buf, zd_pos, zd_len, zd_origin, zd_be);"
4205            )
4206            .map_err(fmt_err)?;
4207            writeln!(out, "            switch (zd_h.member_id) {{").map_err(fmt_err)?;
4208            let mut zd_next_id = 0u32;
4209            for m in &s.members {
4210                emit_mutable_member_decode_case(out, m, &mut zd_next_id)?;
4211            }
4212            writeln!(out, "                default: {{").map_err(fmt_err)?;
4213            writeln!(
4214                out,
4215                "                    // Unknown member: per-LC skip per XTypes 1.3"
4216            )
4217            .map_err(fmt_err)?;
4218            writeln!(
4219                out,
4220                "                    // §7.4.3.4.2 (LengthCode::body_len). LC0..3 are"
4221            )
4222            .map_err(fmt_err)?;
4223            writeln!(
4224                out,
4225                "                    // fixed 1/2/4/8-byte bodies WITHOUT NEXTINT; LC4/5 NEXTINT="
4226            )
4227            .map_err(fmt_err)?;
4228            writeln!(
4229                out,
4230                "                    // byte length; LC6/7 NEXTINT=element count (4 + 4n / 4 + 8n)."
4231            )
4232            .map_err(fmt_err)?;
4233            writeln!(
4234                out,
4235                "                    if (zd_h.lc == 0) {{ zd_pos += 1; }}"
4236            )
4237            .map_err(fmt_err)?;
4238            writeln!(
4239                out,
4240                "                    else if (zd_h.lc == 1) {{ zd_pos += 2; }}"
4241            )
4242            .map_err(fmt_err)?;
4243            writeln!(
4244                out,
4245                "                    else if (zd_h.lc == 2) {{ zd_pos += 4; }}"
4246            )
4247            .map_err(fmt_err)?;
4248            writeln!(
4249                out,
4250                "                    else if (zd_h.lc == 3) {{ zd_pos += 8; }}"
4251            )
4252            .map_err(fmt_err)?;
4253            writeln!(
4254                out,
4255                "                    else if (zd_h.lc == 4 || zd_h.lc == 5) {{ auto zd_n = ::dds::topic::xcdr2::emheader_nextint_read(zd_buf, zd_pos, zd_len, zd_be); zd_pos += zd_n; }}"
4256            )
4257            .map_err(fmt_err)?;
4258            writeln!(
4259                out,
4260                "                    else if (zd_h.lc == 6) {{ auto zd_c = ::dds::topic::xcdr2::emheader_nextint_read(zd_buf, zd_pos, zd_len, zd_be); zd_pos += 4 + 4 * static_cast<size_t>(zd_c); }}"
4261            )
4262            .map_err(fmt_err)?;
4263            writeln!(
4264                out,
4265                "                    else {{ auto zd_c = ::dds::topic::xcdr2::emheader_nextint_read(zd_buf, zd_pos, zd_len, zd_be); zd_pos += 4 + 8 * static_cast<size_t>(zd_c); }}"
4266            )
4267            .map_err(fmt_err)?;
4268            writeln!(out, "                    break;").map_err(fmt_err)?;
4269            writeln!(out, "                }}").map_err(fmt_err)?;
4270            writeln!(out, "            }}").map_err(fmt_err)?;
4271            writeln!(out, "        }}").map_err(fmt_err)?;
4272            writeln!(out, "        if (zd_pos < zd_end) zd_pos = zd_end;").map_err(fmt_err)?;
4273        }
4274    }
4275
4276    writeln!(out, "        return zd_v;").map_err(fmt_err)?;
4277    writeln!(out, "    }}").map_err(fmt_err)?;
4278    Ok(())
4279}
4280
4281fn emit_plain_member_decode(out: &mut String, m: &Member, origin: &str) -> Result<(), CppGenError> {
4282    let m = &normalize_member(m);
4283    if !member_codegen_supported(m) {
4284        for decl in &m.declarators {
4285            let name = &decl.name().text;
4286            writeln!(
4287                out,
4288                "        // xcdr2: @shared member '{name}' not supported (skip)"
4289            )
4290            .map_err(fmt_err)?;
4291        }
4292        return Ok(());
4293    }
4294    let is_optional = has_optional_annotation(&m.annotations);
4295    for decl in &m.declarators {
4296        let name = &decl.name().text;
4297        // 1-D fixed array of a leaf type — read N elements in place (symmetric to
4298        // the plain-encode array path). Multi-dim / array-of-struct: follow-up.
4299        if let Declarator::Array(arr) = decl {
4300            let prim = matches!(m.type_spec, TypeSpec::Primitive(_));
4301            let leaf_1d = arr.sizes.len() == 1
4302                && matches!(m.type_spec, TypeSpec::Primitive(_) | TypeSpec::String(_));
4303            let prim_read_expr = || -> String {
4304                match &m.type_spec {
4305                    TypeSpec::Primitive(PrimitiveType::Boolean) => {
4306                        "::dds::topic::xcdr2::read_bool(zd_buf, zd_pos, zd_len)".to_string()
4307                    }
4308                    TypeSpec::Primitive(PrimitiveType::Octet) => {
4309                        "::dds::topic::xcdr2::read_u8(zd_buf, zd_pos, zd_len)".to_string()
4310                    }
4311                    TypeSpec::Primitive(p) => format!(
4312                        "::dds::topic::xcdr2::read_le_origin<{}>(zd_buf, zd_pos, zd_len, {origin}, zd_max_align, zd_be)",
4313                        primitive_to_cpp(*p)
4314                    ),
4315                    TypeSpec::String(s) if s.wide => format!(
4316                        "::dds::topic::xcdr2::read_wstring_origin(zd_buf, zd_pos, zd_len, {origin}, zd_max_align, zd_be)"
4317                    ),
4318                    _ => format!(
4319                        "::dds::topic::xcdr2::read_string_origin(zd_buf, zd_pos, zd_len, {origin}, zd_max_align, zd_be)"
4320                    ),
4321                }
4322            };
4323            if leaf_1d {
4324                let read_expr = prim_read_expr();
4325                writeln!(out, "        {{").map_err(fmt_err)?;
4326                writeln!(out, "            auto zd_arr = zd_v.{name}();").map_err(fmt_err)?;
4327                writeln!(
4328                    out,
4329                    "            for (auto& zd_ae : zd_arr) {{ zd_ae = {read_expr}; }}"
4330                )
4331                .map_err(fmt_err)?;
4332                writeln!(out, "            zd_v.{name}(zd_arr);").map_err(fmt_err)?;
4333                writeln!(out, "        }}").map_err(fmt_err)?;
4334            } else if prim && arr.sizes.len() >= 2 {
4335                // Multi-dim PRIMITIVE array = PARRAY (Bug XV-arr): NO collection
4336                // DHEADER (XTypes 1.3 §7.4.3.5 rule 8), symmetric to the encode.
4337                // Read the row-major elements tight-packed directly.
4338                let read_expr = prim_read_expr();
4339                let n = arr.sizes.len();
4340                writeln!(out, "        {{").map_err(fmt_err)?;
4341                writeln!(out, "            auto zd_arr = zd_v.{name}();").map_err(fmt_err)?;
4342                let mut acc = String::from("zd_arr");
4343                let mut ind = String::from("            ");
4344                for d in 0..n {
4345                    let lv = format!("zd_a{d}");
4346                    writeln!(out, "{ind}for (auto& {lv} : {acc}) {{").map_err(fmt_err)?;
4347                    acc = lv;
4348                    ind.push_str("    ");
4349                }
4350                writeln!(out, "{ind}{acc} = {read_expr};").map_err(fmt_err)?;
4351                for _ in 0..n {
4352                    ind.truncate(ind.len() - 4);
4353                    writeln!(out, "{ind}}}").map_err(fmt_err)?;
4354                }
4355                writeln!(out, "            zd_v.{name}(zd_arr);").map_err(fmt_err)?;
4356                writeln!(out, "        }}").map_err(fmt_err)?;
4357            } else if matches!(&m.type_spec, TypeSpec::Scoped(s) if scoped_is_enum(s) || scoped_struct(s).is_some() || scoped_union(s).is_some())
4358                || (matches!(m.type_spec, TypeSpec::String(_)) && arr.sizes.len() >= 2)
4359            {
4360                // Array of non-primitive elements (enum / struct / union, any dims;
4361                // string only >=2-D): read the array DHEADER, then read N elements
4362                // in place via N nested loops (symmetric to the encode; fixed size,
4363                // no count). `emit_value_read` picks the per-element form: @final
4364                // structs recurse inline; @appendable/@mutable structs and unions
4365                // splice their own DHEADER-framed `topic_type_support<...>::decode`.
4366                let n = arr.sizes.len();
4367                writeln!(out, "        {{").map_err(fmt_err)?;
4368                writeln!(out, "        const auto zd_arr_dh = ::dds::topic::xcdr2::dheader_read_r(zd_buf, zd_pos, zd_len, zd_be, zd_repr); (void)zd_arr_dh;").map_err(fmt_err)?;
4369                writeln!(out, "        auto zd_arr = zd_v.{name}();").map_err(fmt_err)?;
4370                let mut acc = String::from("zd_arr");
4371                let mut ind = String::from("        ");
4372                for d in 0..n {
4373                    let lv = format!("zd_a{d}");
4374                    writeln!(out, "{ind}for (auto& {lv} : {acc}) {{").map_err(fmt_err)?;
4375                    acc = lv;
4376                    ind.push_str("    ");
4377                }
4378                emit_value_read(out, &m.type_spec, &format!("{acc} ="), origin, &ind, false)?;
4379                for _ in 0..n {
4380                    ind.truncate(ind.len() - 4);
4381                    writeln!(out, "{ind}}}").map_err(fmt_err)?;
4382                }
4383                writeln!(out, "        zd_v.{name}(zd_arr);").map_err(fmt_err)?;
4384                writeln!(out, "        }}").map_err(fmt_err)?;
4385            } else {
4386                writeln!(
4387                    out,
4388                    "        // xcdr2: array member '{name}' (1-D string only / unsupported elem) not supported (skip)"
4389                )
4390                .map_err(fmt_err)?;
4391            }
4392            continue;
4393        }
4394        if !typespec_supported(&m.type_spec) {
4395            writeln!(
4396                out,
4397                "        // xcdr2: member '{name}' not supported (skip)"
4398            )
4399            .map_err(fmt_err)?;
4400            continue;
4401        }
4402        if is_optional {
4403            writeln!(out, "        {{").map_err(fmt_err)?;
4404            writeln!(
4405                out,
4406                "            uint8_t zd_present = ::dds::topic::xcdr2::read_u8(zd_buf, zd_pos, zd_len);"
4407            )
4408            .map_err(fmt_err)?;
4409            writeln!(out, "            if (zd_present) {{").map_err(fmt_err)?;
4410            emit_value_read(
4411                out,
4412                &m.type_spec,
4413                &format!("zd_v.{name}"),
4414                origin,
4415                "                ",
4416                true,
4417            )?;
4418            writeln!(out, "            }} else {{").map_err(fmt_err)?;
4419            writeln!(out, "                zd_v.{name}(std::nullopt);").map_err(fmt_err)?;
4420            writeln!(out, "            }}").map_err(fmt_err)?;
4421            writeln!(out, "        }}").map_err(fmt_err)?;
4422        } else {
4423            emit_value_read(
4424                out,
4425                &m.type_spec,
4426                &format!("zd_v.{name}"),
4427                origin,
4428                "        ",
4429                false,
4430            )?;
4431        }
4432    }
4433    Ok(())
4434}
4435
4436/// zerodds-lint: recursion-depth 64 (nested type emit; bounded by IDL nesting)
4437fn emit_value_read(
4438    out: &mut String,
4439    ts: &TypeSpec,
4440    setter: &str,
4441    origin: &str,
4442    indent: &str,
4443    is_opt: bool,
4444) -> Result<(), CppGenError> {
4445    let wrap_opt = |v: String| -> String {
4446        if is_opt {
4447            format!("std::optional<decltype({v})>({v})")
4448        } else {
4449            v
4450        }
4451    };
4452    let _ = wrap_opt;
4453    match ts {
4454        TypeSpec::Primitive(PrimitiveType::Boolean) => {
4455            writeln!(
4456                out,
4457                "{indent}{setter}(::dds::topic::xcdr2::read_bool(zd_buf, zd_pos, zd_len));"
4458            )
4459            .map_err(fmt_err)?;
4460        }
4461        TypeSpec::Primitive(PrimitiveType::Octet) => {
4462            writeln!(
4463                out,
4464                "{indent}{setter}(::dds::topic::xcdr2::read_u8(zd_buf, zd_pos, zd_len));"
4465            )
4466            .map_err(fmt_err)?;
4467        }
4468        TypeSpec::Primitive(p) => {
4469            let cpp_ty = primitive_to_cpp(*p);
4470            writeln!(
4471                out,
4472                "{indent}{setter}(::dds::topic::xcdr2::read_le_origin<{cpp_ty}>(zd_buf, zd_pos, zd_len, {origin}, zd_max_align, zd_be));"
4473            )
4474            .map_err(fmt_err)?;
4475        }
4476        TypeSpec::String(s) if !s.wide => {
4477            writeln!(
4478                out,
4479                "{indent}{setter}(::dds::topic::xcdr2::read_string_origin(zd_buf, zd_pos, zd_len, {origin}, zd_max_align, zd_be));"
4480            )
4481            .map_err(fmt_err)?;
4482        }
4483        TypeSpec::String(s) if s.wide => {
4484            writeln!(
4485                out,
4486                "{indent}{setter}(::dds::topic::xcdr2::read_wstring_origin(zd_buf, zd_pos, zd_len, {origin}, zd_max_align, zd_be));"
4487            )
4488            .map_err(fmt_err)?;
4489        }
4490        TypeSpec::Sequence(seq) => {
4491            if matches!(&*seq.elem, TypeSpec::Primitive(PrimitiveType::Octet)) {
4492                // sequence<octet>: raw byte block directly from the buffer.
4493                writeln!(out, "{indent}{{").map_err(fmt_err)?;
4494                writeln!(out, "{indent}    auto zd_cnt = ::dds::topic::xcdr2::read_le_origin<uint32_t>(zd_buf, zd_pos, zd_len, {origin}, zd_max_align, zd_be);").map_err(fmt_err)?;
4495                writeln!(
4496                    out,
4497                    "{indent}    ::dds::topic::xcdr2::check_avail(zd_pos, zd_cnt, zd_len);"
4498                )
4499                .map_err(fmt_err)?;
4500                writeln!(
4501                    out,
4502                    "{indent}    std::vector<uint8_t> zd_seq(zd_buf + zd_pos, zd_buf + zd_pos + zd_cnt);"
4503                )
4504                .map_err(fmt_err)?;
4505                writeln!(out, "{indent}    zd_pos += zd_cnt;").map_err(fmt_err)?;
4506                writeln!(out, "{indent}    {setter}(std::move(zd_seq));").map_err(fmt_err)?;
4507                writeln!(out, "{indent}}}").map_err(fmt_err)?;
4508                return Ok(());
4509            }
4510            let elem_cpp_ty: String = match &*seq.elem {
4511                TypeSpec::Primitive(PrimitiveType::Boolean) => "bool".to_string(),
4512                TypeSpec::Primitive(p) => primitive_to_cpp(*p).to_string(),
4513                TypeSpec::String(s) if !s.wide => "std::string".to_string(),
4514                // wide string element -> std::wstring (narrow caught above).
4515                TypeSpec::String(_) => "std::wstring".to_string(),
4516                // enum (-> underlying int32, but the vector holds the enum) and
4517                // nested struct elements (any extensibility) use their C++ type.
4518                TypeSpec::Scoped(s) if scoped_is_enum(s) => scoped_to_cpp(s),
4519                TypeSpec::Scoped(s) if scoped_struct(s).is_some() => scoped_to_cpp(s),
4520                // union element (sequence<union>, Bug R3) -> the union's C++ type;
4521                // each element is spliced/sub-decoded by its own TypeSupport.
4522                TypeSpec::Scoped(s) if scoped_union(s).is_some() => scoped_to_cpp(s),
4523                // nested sequence element -> std::vector<inner>.
4524                TypeSpec::Sequence(_) => typespec_to_cpp(&seq.elem)?,
4525                // map element -> std::map<K,V>.
4526                TypeSpec::Map(_) => typespec_to_cpp(&seq.elem)?,
4527                _ => {
4528                    writeln!(
4529                        out,
4530                        "{indent}// xcdr2: nested seq-elem not supported (skip)"
4531                    )
4532                    .map_err(fmt_err)?;
4533                    return Ok(());
4534                }
4535            };
4536            writeln!(out, "{indent}{{").map_err(fmt_err)?;
4537            // XCDR2 §7.4.3.5: for non-primitive elements skip the DHEADER (4-aligned).
4538            if !matches!(&*seq.elem, TypeSpec::Primitive(_)) {
4539                writeln!(
4540                    out,
4541                    "{indent}    ::dds::topic::xcdr2::skip_pad_from_origin(zd_pos, {origin}, 4);"
4542                )
4543                .map_err(fmt_err)?;
4544                writeln!(out, "{indent}    const auto zd_seq_dh = ::dds::topic::xcdr2::dheader_read_r(zd_buf, zd_pos, zd_len, zd_be, zd_repr); (void)zd_seq_dh;").map_err(fmt_err)?;
4545            }
4546            writeln!(out, "{indent}    auto zd_cnt = ::dds::topic::xcdr2::read_le_origin<uint32_t>(zd_buf, zd_pos, zd_len, {origin}, zd_max_align, zd_be);").map_err(fmt_err)?;
4547            writeln!(out, "{indent}    std::vector<{elem_cpp_ty}> zd_seq;").map_err(fmt_err)?;
4548            writeln!(out, "{indent}    zd_seq.reserve(zd_cnt);").map_err(fmt_err)?;
4549            writeln!(
4550                out,
4551                "{indent}    for (uint32_t zd_i = 0; zd_i < zd_cnt; ++zd_i) {{"
4552            )
4553            .map_err(fmt_err)?;
4554            match &*seq.elem {
4555                TypeSpec::Primitive(PrimitiveType::Boolean) => {
4556                    writeln!(out, "{indent}        zd_seq.push_back(::dds::topic::xcdr2::read_bool(zd_buf, zd_pos, zd_len));").map_err(fmt_err)?;
4557                }
4558                TypeSpec::Primitive(PrimitiveType::Octet) => {
4559                    writeln!(out, "{indent}        zd_seq.push_back(::dds::topic::xcdr2::read_u8(zd_buf, zd_pos, zd_len));").map_err(fmt_err)?;
4560                }
4561                TypeSpec::Primitive(p) => {
4562                    let cpp_ty = primitive_to_cpp(*p);
4563                    writeln!(out, "{indent}        zd_seq.push_back(::dds::topic::xcdr2::read_le_origin<{cpp_ty}>(zd_buf, zd_pos, zd_len, {origin}, zd_max_align, zd_be));").map_err(fmt_err)?;
4564                }
4565                TypeSpec::String(s) if !s.wide => {
4566                    writeln!(out, "{indent}        zd_seq.push_back(::dds::topic::xcdr2::read_string_origin(zd_buf, zd_pos, zd_len, {origin}, zd_max_align, zd_be));").map_err(fmt_err)?;
4567                }
4568                // wide string element.
4569                TypeSpec::String(_) => {
4570                    writeln!(out, "{indent}        zd_seq.push_back(::dds::topic::xcdr2::read_wstring_origin(zd_buf, zd_pos, zd_len, {origin}, zd_max_align, zd_be));").map_err(fmt_err)?;
4571                }
4572                // enum element: read int32, cast back to the enum type.
4573                TypeSpec::Scoped(s) if scoped_is_enum(s) => {
4574                    let cpp_ty = scoped_to_cpp(s);
4575                    let ec = enum_wire_ctype(scoped_enum_bytes(s));
4576                    writeln!(out, "{indent}        zd_seq.push_back(static_cast<{cpp_ty}>(::dds::topic::xcdr2::read_le_origin<{ec}>(zd_buf, zd_pos, zd_len, {origin}, zd_max_align, zd_be)));").map_err(fmt_err)?;
4577                }
4578                // nested @final struct element: read each sub-member into a fresh
4579                // temp, push the whole object (symmetric to the inline encode).
4580                TypeSpec::Scoped(sc) if scoped_final_struct(sc).is_some() => {
4581                    if let Some(def) = scoped_final_struct(sc) {
4582                        let cpp_ty = scoped_to_cpp(sc);
4583                        let var = format!("zd_se{}", next_nest_id());
4584                        let binner = format!("{indent}        ");
4585                        writeln!(out, "{binner}{cpp_ty} {var}{{}};").map_err(fmt_err)?;
4586                        for sm in &def.members {
4587                            let sm_name = &sm.declarators[0].name().text;
4588                            emit_value_read(
4589                                out,
4590                                &sm.type_spec,
4591                                &format!("{var}.{sm_name}"),
4592                                origin,
4593                                &binner,
4594                                false,
4595                            )?;
4596                        }
4597                        writeln!(out, "{binner}zd_seq.push_back({var});").map_err(fmt_err)?;
4598                    }
4599                }
4600                // nested @appendable/@mutable struct element: 4-align, read the
4601                // element's own DHEADER, sub-decode the [DHEADER+body] slice via
4602                // the nested type's `decode`, advance the cursor past it, push it.
4603                TypeSpec::Scoped(sc) if scoped_struct(sc).is_some() => {
4604                    let cpp_ty = scoped_to_cpp(sc);
4605                    let id = next_nest_id();
4606                    let var = format!("zd_se{id}");
4607                    let binner = format!("{indent}        ");
4608                    writeln!(
4609                        out,
4610                        "{binner}::dds::topic::xcdr2::skip_pad_from_origin(zd_pos, {origin}, 4);"
4611                    )
4612                    .map_err(fmt_err)?;
4613                    emit_nested_span_decode(out, &cpp_ty, &var, id, &binner, true)?;
4614                    writeln!(out, "{binner}zd_seq.push_back(std::move({var}));")
4615                        .map_err(fmt_err)?;
4616                }
4617                // union element (sequence<union>, Bug R3): 4-align, read the
4618                // union's own DHEADER, sub-decode the [DHEADER+body] slice via the
4619                // union's own `decode`, advance the cursor, push it (symmetric to
4620                // the appendable-struct splice above; previously dropped → data
4621                // loss / empty sequence).
4622                TypeSpec::Scoped(sc) if scoped_union(sc).is_some() => {
4623                    let Some(u) = scoped_union(sc) else {
4624                        return Ok(());
4625                    };
4626                    let cpp_ty = scoped_to_cpp(sc);
4627                    let id = next_nest_id();
4628                    let var = format!("zd_se{id}");
4629                    let binner = format!("{indent}        ");
4630                    match union_extensibility(&u.annotations) {
4631                        // @final union element: inline disc + selected member,
4632                        // NO per-element DHEADER (rule (26), symmetric to the
4633                        // inline encode).
4634                        Extensibility::Final => {
4635                            let disc_ts = switch_type_spec(&u.switch_type);
4636                            let disc_cpp = switch_type_to_cpp(&u.switch_type)?;
4637                            writeln!(out, "{binner}{cpp_ty} {var};").map_err(fmt_err)?;
4638                            writeln!(out, "{binner}{disc_cpp} zd_disc{id}{{}};")
4639                                .map_err(fmt_err)?;
4640                            emit_value_read(
4641                                out,
4642                                &disc_ts,
4643                                &format!("zd_disc{id} ="),
4644                                origin,
4645                                &binner,
4646                                false,
4647                            )?;
4648                            writeln!(out, "{binner}{var}._d(zd_disc{id});").map_err(fmt_err)?;
4649                            emit_union_branch_switch_at(
4650                                out, &u, &disc_cpp, /*decode=*/ true, "le", &var, origin,
4651                            )?;
4652                            writeln!(out, "{binner}zd_seq.push_back(std::move({var}));")
4653                                .map_err(fmt_err)?;
4654                        }
4655                        // @appendable/@mutable union element: 4-align, read its
4656                        // own DHEADER, sub-decode the [DHEADER+body] slice.
4657                        Extensibility::Appendable | Extensibility::Mutable => {
4658                            writeln!(
4659                                out,
4660                                "{binner}::dds::topic::xcdr2::skip_pad_from_origin(zd_pos, {origin}, 4);"
4661                            )
4662                            .map_err(fmt_err)?;
4663                            emit_nested_span_decode(out, &cpp_ty, &var, id, &binner, true)?;
4664                            writeln!(out, "{binner}zd_seq.push_back(std::move({var}));")
4665                                .map_err(fmt_err)?;
4666                        }
4667                    }
4668                }
4669                // nested sequence element: read the inner sequence into a temp
4670                // via the assignment-setter form, then push it.
4671                TypeSpec::Sequence(_) => {
4672                    let inner_ty = typespec_to_cpp(&seq.elem)?;
4673                    let var = format!("zd_se{}", next_nest_id());
4674                    let binner = format!("{indent}        ");
4675                    writeln!(out, "{binner}{inner_ty} {var}{{}};").map_err(fmt_err)?;
4676                    emit_value_read(out, &seq.elem, &format!("{var} ="), origin, &binner, false)?;
4677                    writeln!(out, "{binner}zd_seq.push_back(std::move({var}));")
4678                        .map_err(fmt_err)?;
4679                }
4680                // map element: read the inner map into a temp, then push it.
4681                TypeSpec::Map(_) => {
4682                    let inner_ty = typespec_to_cpp(&seq.elem)?;
4683                    let var = format!("zd_se{}", next_nest_id());
4684                    let binner = format!("{indent}        ");
4685                    writeln!(out, "{binner}{inner_ty} {var}{{}};").map_err(fmt_err)?;
4686                    emit_value_read(out, &seq.elem, &format!("{var} ="), origin, &binner, false)?;
4687                    writeln!(out, "{binner}zd_seq.push_back(std::move({var}));")
4688                        .map_err(fmt_err)?;
4689                }
4690                _ => {}
4691            }
4692            writeln!(out, "{indent}    }}").map_err(fmt_err)?;
4693            writeln!(out, "{indent}    {setter}(std::move(zd_seq));").map_err(fmt_err)?;
4694            writeln!(out, "{indent}}}").map_err(fmt_err)?;
4695        }
4696        // map<K,V> member: read DHEADER, count, then count interleaved key/value
4697        // pairs (symmetric to the encode); insert into a std::map. Key/value are
4698        // read into fresh temps via the assignment-setter form `zd_k =(...)`
4699        // (emit_value_read always ends in `{setter}(final)`, so `zd_k =` yields a
4700        // plain assignment that works for primitive/string/enum/struct values).
4701        TypeSpec::Map(m) => {
4702            let k_ty = typespec_to_cpp(&m.key)?;
4703            let v_ty = typespec_to_cpp(&m.value)?;
4704            let id = next_nest_id();
4705            let mapv = format!("zd_map{id}");
4706            let kv = format!("zd_mk{id}");
4707            let vv = format!("zd_mv{id}");
4708            let inner = format!("{indent}    ");
4709            let li = format!("{inner}    ");
4710            writeln!(out, "{indent}{{").map_err(fmt_err)?;
4711            // Symmetric to the encoder: the map DHEADER is 4-aligned to origin,
4712            // and present ONLY for a non-primitive (key,value) element.
4713            writeln!(
4714                out,
4715                "{inner}::dds::topic::xcdr2::skip_pad_from_origin(zd_pos, {origin}, 4);"
4716            )
4717            .map_err(fmt_err)?;
4718            if !map_pair_is_primitive(&m.key, &m.value) {
4719                writeln!(out, "{inner}const auto zd_map_dh = ::dds::topic::xcdr2::dheader_read_r(zd_buf, zd_pos, zd_len, zd_be, zd_repr); (void)zd_map_dh;").map_err(fmt_err)?;
4720            }
4721            writeln!(out, "{inner}auto zd_mcnt = ::dds::topic::xcdr2::read_le_origin<uint32_t>(zd_buf, zd_pos, zd_len, {origin}, zd_max_align, zd_be);").map_err(fmt_err)?;
4722            writeln!(out, "{inner}std::map<{k_ty}, {v_ty}> {mapv};").map_err(fmt_err)?;
4723            writeln!(
4724                out,
4725                "{inner}for (uint32_t zd_i = 0; zd_i < zd_mcnt; ++zd_i) {{"
4726            )
4727            .map_err(fmt_err)?;
4728            writeln!(out, "{li}{k_ty} {kv}{{}};").map_err(fmt_err)?;
4729            writeln!(out, "{li}{v_ty} {vv}{{}};").map_err(fmt_err)?;
4730            emit_value_read(out, &m.key, &format!("{kv} ="), origin, &li, false)?;
4731            emit_value_read(out, &m.value, &format!("{vv} ="), origin, &li, false)?;
4732            writeln!(out, "{li}{mapv}.emplace(std::move({kv}), std::move({vv}));")
4733                .map_err(fmt_err)?;
4734            writeln!(out, "{inner}}}").map_err(fmt_err)?;
4735            writeln!(out, "{inner}{setter}(std::move({mapv}));").map_err(fmt_err)?;
4736            writeln!(out, "{indent}}}").map_err(fmt_err)?;
4737        }
4738        // bitmask / bitset member: read the holder integer (cdr-core holder
4739        // width) then reconstruct. Bitmask -> `enum class : uintN` cast; bitset
4740        // -> `Flags{ value }` aggregate init. XTypes §7.4.x.
4741        TypeSpec::Scoped(s) if scoped_bitholder(s).is_some() => {
4742            let bytes = scoped_bitholder(s).unwrap_or(1);
4743            let holder = holder_uint_for_bytes(bytes);
4744            let cpp_ty = scoped_to_cpp(s);
4745            let read = format!(
4746                "::dds::topic::xcdr2::read_le_origin<{holder}>(zd_buf, zd_pos, zd_len, {origin}, zd_max_align, zd_be)"
4747            );
4748            let built = if scoped_is_bitset(s) {
4749                format!("{cpp_ty}{{ static_cast<uint64_t>({read}) }}")
4750            } else {
4751                format!("static_cast<{cpp_ty}>({read})")
4752            };
4753            writeln!(out, "{indent}{setter}({});", wrap_opt(built)).map_err(fmt_err)?;
4754        }
4755        // enum member: read its @bit_bound-width underlying value, cast to enum.
4756        TypeSpec::Scoped(s) if scoped_is_enum(s) => {
4757            let cpp_ty = scoped_to_cpp(s);
4758            let ec = enum_wire_ctype(scoped_enum_bytes(s));
4759            writeln!(
4760                out,
4761                "{indent}{setter}(static_cast<{cpp_ty}>(::dds::topic::xcdr2::read_le_origin<{ec}>(zd_buf, zd_pos, zd_len, {origin}, zd_max_align, zd_be)));"
4762            )
4763            .map_err(fmt_err)?;
4764        }
4765        // nested struct member. @final: read each sub-member into a fresh temp
4766        // (symmetric to the inline encode). @appendable/@mutable: read the
4767        // nested DHEADER length, then sub-decode the [DHEADER+body] slice via the
4768        // nested type's own `decode` and advance the cursor past it.
4769        TypeSpec::Scoped(sc) if scoped_struct(sc).is_some() => {
4770            let Some((def, ext)) = scoped_struct(sc) else {
4771                return Ok(());
4772            };
4773            let cpp_ty = scoped_to_cpp(sc);
4774            let id = next_nest_id();
4775            let var = format!("zd_ns{id}");
4776            let inner = format!("{indent}    ");
4777            writeln!(out, "{indent}{{").map_err(fmt_err)?;
4778            writeln!(out, "{inner}{cpp_ty} {var}{{}};").map_err(fmt_err)?;
4779            match ext {
4780                Extensibility::Final => {
4781                    for sm in &def.members {
4782                        let sm_name = &sm.declarators[0].name().text;
4783                        emit_value_read(
4784                            out,
4785                            &sm.type_spec,
4786                            &format!("{var}.{sm_name}"),
4787                            origin,
4788                            &inner,
4789                            false,
4790                        )?;
4791                    }
4792                }
4793                Extensibility::Appendable | Extensibility::Mutable => {
4794                    writeln!(
4795                        out,
4796                        "{inner}::dds::topic::xcdr2::skip_pad_from_origin(zd_pos, {origin}, 4);"
4797                    )
4798                    .map_err(fmt_err)?;
4799                    emit_nested_span_decode(out, &cpp_ty, &var, id, &inner, false)?;
4800                }
4801            }
4802            writeln!(out, "{inner}{setter}({var});").map_err(fmt_err)?;
4803            writeln!(out, "{indent}}}").map_err(fmt_err)?;
4804        }
4805        // Bug R3: union member — 4-align, read the union's own DHEADER, sub-decode
4806        // the [DHEADER+body] slice via `topic_type_support<Union>::decode`, then
4807        // advance the cursor past it (symmetric to the appendable-struct splice).
4808        TypeSpec::Scoped(sc) if scoped_union(sc).is_some() => {
4809            let Some(u) = scoped_union(sc) else {
4810                return Ok(());
4811            };
4812            let cpp_ty = scoped_to_cpp(sc);
4813            let id = next_nest_id();
4814            let var = format!("zd_nu{id}");
4815            let inner = format!("{indent}    ");
4816            writeln!(out, "{indent}{{").map_err(fmt_err)?;
4817            match union_extensibility(&u.annotations) {
4818                // @final union: inline disc + selected member, NO DHEADER
4819                // (rule (26) FUNION_TYPE, §7.4.3.4.1). Read into a local union
4820                // built at the OUTER origin (8-byte members align to 4 relative
4821                // to the top-level DHEADER), then hand it to the setter.
4822                Extensibility::Final => {
4823                    let disc_ts = switch_type_spec(&u.switch_type);
4824                    let disc_cpp = switch_type_to_cpp(&u.switch_type)?;
4825                    writeln!(out, "{inner}{cpp_ty} {var};").map_err(fmt_err)?;
4826                    writeln!(out, "{inner}{disc_cpp} zd_disc{id}{{}};").map_err(fmt_err)?;
4827                    emit_value_read(
4828                        out,
4829                        &disc_ts,
4830                        &format!("zd_disc{id} ="),
4831                        origin,
4832                        &inner,
4833                        false,
4834                    )?;
4835                    writeln!(out, "{inner}{var}._d(zd_disc{id});").map_err(fmt_err)?;
4836                    emit_union_branch_switch_at(
4837                        out, &u, &disc_cpp, /*decode=*/ true, "le", &var, origin,
4838                    )?;
4839                    writeln!(out, "{inner}{setter}({var});").map_err(fmt_err)?;
4840                }
4841                // @appendable/@mutable union: 4-align, read its own DHEADER,
4842                // sub-decode the [DHEADER+body] slice via the union's `decode`.
4843                Extensibility::Appendable | Extensibility::Mutable => {
4844                    writeln!(
4845                        out,
4846                        "{inner}::dds::topic::xcdr2::skip_pad_from_origin(zd_pos, {origin}, 4);"
4847                    )
4848                    .map_err(fmt_err)?;
4849                    emit_nested_span_decode(out, &cpp_ty, &var, id, &inner, true)?;
4850                    writeln!(out, "{inner}{setter}({var});").map_err(fmt_err)?;
4851                }
4852            }
4853            writeln!(out, "{indent}}}").map_err(fmt_err)?;
4854        }
4855        TypeSpec::Fixed(_) => {
4856            // fixed<P,S>: read exactly (P+2)/2 raw BCD octets (no align/endian),
4857            // construct a `::dds::core::Fixed<P,S>` from them.
4858            let cpp_ty = typespec_to_cpp(ts)?;
4859            writeln!(out, "{indent}{{").map_err(fmt_err)?;
4860            writeln!(
4861                out,
4862                "{indent}    ::dds::topic::xcdr2::check_avail(zd_pos, {cpp_ty}::kByteCount, zd_len);"
4863            )
4864            .map_err(fmt_err)?;
4865            writeln!(out, "{indent}    {cpp_ty} zd_fx(zd_buf + zd_pos);").map_err(fmt_err)?;
4866            writeln!(out, "{indent}    zd_pos += {cpp_ty}::kByteCount;").map_err(fmt_err)?;
4867            writeln!(out, "{indent}    {setter}(std::move(zd_fx));").map_err(fmt_err)?;
4868            writeln!(out, "{indent}}}").map_err(fmt_err)?;
4869        }
4870        _ => {}
4871    }
4872    Ok(())
4873}
4874
4875/// Emits one PL_CDR1 (`@mutable` XCDR1) decode `case`: dispatch on the parameter
4876/// member id and decode the body as a plain positional value, origin-relative to
4877/// the parameter body start (`zd_pl_origin`, max_align 8). Optional presence is
4878/// implied by the parameter being in the list, so there is no present-flag — an
4879/// absent optional simply never matches a case and stays default-constructed.
4880/// Mirrors `emit_pl_cdr1_member_encode`'s id assignment.
4881fn emit_pl_cdr1_member_decode_case(
4882    out: &mut String,
4883    m: &Member,
4884    next_id: &mut u32,
4885) -> Result<(), CppGenError> {
4886    let m = &normalize_member(m);
4887    if !member_codegen_supported(m) {
4888        for _ in &m.declarators {
4889            *next_id += 1;
4890        }
4891        return Ok(());
4892    }
4893    let id_override = find_uint_annotation(&m.annotations, "id");
4894    let is_optional = has_optional_annotation(&m.annotations);
4895    for (idx, decl) in m.declarators.iter().enumerate() {
4896        let name = &decl.name().text;
4897        let this_id = match id_override {
4898            Some(id) => id + idx as u32,
4899            None => *next_id,
4900        };
4901        *next_id = this_id + 1;
4902        if !matches!(decl, Declarator::Simple(_)) {
4903            continue;
4904        }
4905        if !typespec_supported(&m.type_spec) {
4906            continue;
4907        }
4908        writeln!(out, "                    case 0x{this_id:x}u: {{").map_err(fmt_err)?;
4909        emit_value_read(
4910            out,
4911            &m.type_spec,
4912            &format!("zd_v.{name}"),
4913            "zd_pl_origin",
4914            "                        ",
4915            is_optional,
4916        )?;
4917        writeln!(out, "                        break;").map_err(fmt_err)?;
4918        writeln!(out, "                    }}").map_err(fmt_err)?;
4919    }
4920    Ok(())
4921}
4922
4923/// Emits the repr-aware decode of a nested `@appendable`/`@mutable` element
4924/// (sequence / array / map element) that advances `zd_pos` past it. Under XCDR2
4925/// the element is DHEADER-delimited, so the cursor jumps `4 + dheader`. Under
4926/// XCDR1 (classic CDR) there is NO delimiter — decode from the cursor to the
4927/// buffer end and advance by the element's re-encoded byte length (XCDR1 encode
4928/// is byte-identical to the wire for canonical data). `declare_var` emits the
4929/// `cpp_ty var;` slot (default-constructed); pass false when the caller already
4930/// declared it. Requires `cpp_ty` to have a repr-aware `encode(v, repr)` (true
4931/// for structs; union elements use the dedicated union path).
4932fn emit_nested_span_decode(
4933    out: &mut String,
4934    cpp_ty: &str,
4935    var: &str,
4936    id: u32,
4937    indent: &str,
4938    declare_var: bool,
4939) -> Result<(), CppGenError> {
4940    writeln!(out, "{indent}const size_t zd_nss{id} = zd_pos;").map_err(fmt_err)?;
4941    if declare_var {
4942        writeln!(out, "{indent}{cpp_ty} {var};").map_err(fmt_err)?;
4943    }
4944    writeln!(
4945        out,
4946        "{indent}if (zd_repr == ::dds::topic::xcdr2::XcdrVersion::Xcdr1) {{"
4947    )
4948    .map_err(fmt_err)?;
4949    writeln!(
4950        out,
4951        "{indent}    {var} = ::dds::topic::topic_type_support<{cpp_ty}>::decode(zd_buf + zd_nss{id}, zd_len - zd_nss{id}, zd_repr, zd_be);"
4952    )
4953    .map_err(fmt_err)?;
4954    writeln!(
4955        out,
4956        "{indent}    zd_pos = zd_nss{id} + ::dds::topic::topic_type_support<{cpp_ty}>::encode({var}, zd_repr).size();"
4957    )
4958    .map_err(fmt_err)?;
4959    writeln!(out, "{indent}}} else {{").map_err(fmt_err)?;
4960    writeln!(out, "{indent}    size_t zd_npk{id} = zd_pos;").map_err(fmt_err)?;
4961    writeln!(
4962        out,
4963        "{indent}    const uint32_t zd_nl{id} = ::dds::topic::xcdr2::dheader_read(zd_buf, zd_npk{id}, zd_len, zd_be);"
4964    )
4965    .map_err(fmt_err)?;
4966    writeln!(
4967        out,
4968        "{indent}    {var} = ::dds::topic::topic_type_support<{cpp_ty}>::decode(zd_buf + zd_nss{id}, 4u + zd_nl{id}, zd_repr, zd_be);"
4969    )
4970    .map_err(fmt_err)?;
4971    writeln!(out, "{indent}    zd_pos = zd_nss{id} + 4u + zd_nl{id};").map_err(fmt_err)?;
4972    writeln!(out, "{indent}}}").map_err(fmt_err)?;
4973    Ok(())
4974}
4975
4976fn emit_mutable_member_decode_case(
4977    out: &mut String,
4978    m: &Member,
4979    next_id: &mut u32,
4980) -> Result<(), CppGenError> {
4981    let m = &normalize_member(m);
4982    if !member_codegen_supported(m) {
4983        // Still consume id slots so the sequential counter stays aligned with
4984        // the encoder (which advances on its own skip paths).
4985        for _ in &m.declarators {
4986            *next_id += 1;
4987        }
4988        return Ok(());
4989    }
4990    let id_override = find_uint_annotation(&m.annotations, "id");
4991    let is_optional = has_optional_annotation(&m.annotations);
4992    let _ = is_optional; // mutable optional: same path; absent member just skips this case.
4993    for (idx, decl) in m.declarators.iter().enumerate() {
4994        let name = &decl.name().text;
4995        let this_id = match id_override {
4996            Some(id) => id + idx as u32,
4997            None => *next_id,
4998        };
4999        *next_id = this_id + 1;
5000        if !matches!(decl, Declarator::Simple(_)) {
5001            continue;
5002        }
5003        if !typespec_supported(&m.type_spec) {
5004            continue;
5005        }
5006        let id_expr = format!("0x{this_id:x}u");
5007        writeln!(out, "                case {id_expr}: {{").map_err(fmt_err)?;
5008        match &m.type_spec {
5009            TypeSpec::Primitive(PrimitiveType::Boolean) => {
5010                // boolean/octet stay compact LC=0 (1-byte body, no NEXTINT).
5011                writeln!(out, "                    uint8_t zd_b = ::dds::topic::xcdr2::read_u8(zd_buf, zd_pos, zd_len);").map_err(fmt_err)?;
5012                writeln!(
5013                    out,
5014                    "                    zd_v.{name}(static_cast<bool>(zd_b));"
5015                )
5016                .map_err(fmt_err)?;
5017            }
5018            TypeSpec::Primitive(PrimitiveType::Octet) => {
5019                writeln!(out, "                    zd_v.{name}(::dds::topic::xcdr2::read_u8(zd_buf, zd_pos, zd_len));").map_err(fmt_err)?;
5020            }
5021            TypeSpec::Primitive(p) => {
5022                // 2/4/8-byte @mutable primitives are framed with the COMPACT
5023                // length code (LC=1/2/3) by the encoder (Bug XV-mut) — NO NEXTINT
5024                // precedes the body. Read the value directly. (XTypes §7.4.3.4.2.)
5025                let cpp_ty = primitive_to_cpp(*p);
5026                writeln!(out, "                    zd_v.{name}(::dds::topic::xcdr2::read_le_raw<{cpp_ty}>(zd_buf, zd_pos, zd_len, zd_be));").map_err(fmt_err)?;
5027            }
5028            TypeSpec::String(s) if !s.wide => {
5029                // EMHEADER LC=5 (Bug XV-mut): the string's own uint32 length prefix
5030                // doubled as the NEXTINT — there is NO separate NEXTINT to skip.
5031                // Read the string body directly from the EMHEADER body origin.
5032                writeln!(out, "                    auto zd_body_origin = zd_pos;")
5033                    .map_err(fmt_err)?;
5034                writeln!(out, "                    zd_v.{name}(::dds::topic::xcdr2::read_string_origin(zd_buf, zd_pos, zd_len, zd_body_origin, zd_max_align, zd_be));").map_err(fmt_err)?;
5035            }
5036            TypeSpec::String(s) if s.wide => {
5037                // EMHEADER LC=5 (Bug XV-mut): wstring octet-length prefix = NEXTINT.
5038                writeln!(out, "                    auto zd_body_origin = zd_pos;")
5039                    .map_err(fmt_err)?;
5040                writeln!(out, "                    zd_v.{name}(::dds::topic::xcdr2::read_wstring_origin(zd_buf, zd_pos, zd_len, zd_body_origin, zd_max_align, zd_be));").map_err(fmt_err)?;
5041            }
5042            TypeSpec::Sequence(seq) => {
5043                // FINDING T1b: a sequence<primitive> is LC=4 (separate NEXTINT to
5044                // consume); a non-primitive-element sequence is LC=5 — its body
5045                // BEGINS with the seq DHEADER which doubled as the NEXTINT, so
5046                // there is NO separate NEXTINT. The DHEADER itself is read below.
5047                if matches!(&*seq.elem, TypeSpec::Primitive(_)) {
5048                    writeln!(out, "                    auto zd_n = ::dds::topic::xcdr2::emheader_nextint_read(zd_buf, zd_pos, zd_len, zd_be);").map_err(fmt_err)?;
5049                    writeln!(out, "                    (void)zd_n;").map_err(fmt_err)?;
5050                }
5051                writeln!(out, "                    auto zd_body_origin = zd_pos;")
5052                    .map_err(fmt_err)?;
5053                // LC=5 non-primitive-element sequence: read the inner DHEADER
5054                // (= the reused NEXTINT length word) before the count.
5055                if !matches!(&*seq.elem, TypeSpec::Primitive(_)) {
5056                    writeln!(out, "                    {{ const auto zd_seq_dh = ::dds::topic::xcdr2::dheader_read_r(zd_buf, zd_pos, zd_len, zd_be, zd_repr); (void)zd_seq_dh; }}").map_err(fmt_err)?;
5057                }
5058                writeln!(out, "                    auto zd_cnt = ::dds::topic::xcdr2::read_le_origin<uint32_t>(zd_buf, zd_pos, zd_len, zd_body_origin, zd_max_align, zd_be);").map_err(fmt_err)?;
5059                if matches!(&*seq.elem, TypeSpec::Primitive(PrimitiveType::Octet)) {
5060                    // sequence<octet>: raw byte block directly from the buffer.
5061                    writeln!(
5062                        out,
5063                        "                    ::dds::topic::xcdr2::check_avail(zd_pos, zd_cnt, zd_len);"
5064                    )
5065                    .map_err(fmt_err)?;
5066                    writeln!(out, "                    std::vector<uint8_t> zd_seq(zd_buf + zd_pos, zd_buf + zd_pos + zd_cnt);").map_err(fmt_err)?;
5067                    writeln!(out, "                    zd_pos += zd_cnt;").map_err(fmt_err)?;
5068                    writeln!(out, "                    zd_v.{name}(std::move(zd_seq));")
5069                        .map_err(fmt_err)?;
5070                } else {
5071                    let elem_cpp_ty: String = match &*seq.elem {
5072                        TypeSpec::Primitive(PrimitiveType::Boolean) => "bool".to_string(),
5073                        TypeSpec::Primitive(p) => primitive_to_cpp(*p).to_string(),
5074                        TypeSpec::String(s) if !s.wide => "std::string".to_string(),
5075                        TypeSpec::String(_) => "std::wstring".to_string(),
5076                        TypeSpec::Scoped(s) if scoped_is_enum(s) => scoped_to_cpp(s),
5077                        TypeSpec::Scoped(s) if scoped_struct(s).is_some() => scoped_to_cpp(s),
5078                        TypeSpec::Sequence(_) | TypeSpec::Map(_) => typespec_to_cpp(&seq.elem)?,
5079                        _ => "uint8_t".to_string(),
5080                    };
5081                    writeln!(
5082                        out,
5083                        "                    std::vector<{elem_cpp_ty}> zd_seq;"
5084                    )
5085                    .map_err(fmt_err)?;
5086                    writeln!(out, "                    zd_seq.reserve(zd_cnt);")
5087                        .map_err(fmt_err)?;
5088                    writeln!(
5089                        out,
5090                        "                    for (uint32_t zd_i = 0; zd_i < zd_cnt; ++zd_i) {{"
5091                    )
5092                    .map_err(fmt_err)?;
5093                    match &*seq.elem {
5094                        TypeSpec::Primitive(PrimitiveType::Boolean) => {
5095                            writeln!(out, "                        zd_seq.push_back(::dds::topic::xcdr2::read_bool(zd_buf, zd_pos, zd_len));").map_err(fmt_err)?;
5096                        }
5097                        TypeSpec::Primitive(p) => {
5098                            let cpp_ty = primitive_to_cpp(*p);
5099                            writeln!(out, "                        zd_seq.push_back(::dds::topic::xcdr2::read_le_origin<{cpp_ty}>(zd_buf, zd_pos, zd_len, zd_body_origin, zd_max_align, zd_be));").map_err(fmt_err)?;
5100                        }
5101                        TypeSpec::String(s) if !s.wide => {
5102                            writeln!(out, "                        zd_seq.push_back(::dds::topic::xcdr2::read_string_origin(zd_buf, zd_pos, zd_len, zd_body_origin, zd_max_align, zd_be));").map_err(fmt_err)?;
5103                        }
5104                        TypeSpec::String(_) => {
5105                            writeln!(out, "                        zd_seq.push_back(::dds::topic::xcdr2::read_wstring_origin(zd_buf, zd_pos, zd_len, zd_body_origin, zd_max_align, zd_be));").map_err(fmt_err)?;
5106                        }
5107                        TypeSpec::Scoped(s) if scoped_is_enum(s) => {
5108                            let cpp_ty = scoped_to_cpp(s);
5109                            let ec = enum_wire_ctype(scoped_enum_bytes(s));
5110                            writeln!(out, "                        zd_seq.push_back(static_cast<{cpp_ty}>(::dds::topic::xcdr2::read_le_origin<{ec}>(zd_buf, zd_pos, zd_len, zd_body_origin, zd_max_align, zd_be)));").map_err(fmt_err)?;
5111                        }
5112                        TypeSpec::Scoped(sc) if scoped_final_struct(sc).is_some() => {
5113                            if let Some(def) = scoped_final_struct(sc) {
5114                                let cpp_ty = scoped_to_cpp(sc);
5115                                let var = format!("zd_se{}", next_nest_id());
5116                                writeln!(out, "                        {cpp_ty} {var}{{}};")
5117                                    .map_err(fmt_err)?;
5118                                for sm in &def.members {
5119                                    let sm_name = &sm.declarators[0].name().text;
5120                                    emit_value_read(
5121                                        out,
5122                                        &sm.type_spec,
5123                                        &format!("{var}.{sm_name}"),
5124                                        "zd_body_origin",
5125                                        "                        ",
5126                                        false,
5127                                    )?;
5128                                }
5129                                writeln!(out, "                        zd_seq.push_back({var});")
5130                                    .map_err(fmt_err)?;
5131                            }
5132                        }
5133                        // nested @appendable/@mutable struct element: 4-align,
5134                        // read the element DHEADER, sub-decode the [DHEADER+body]
5135                        // slice via the nested type's `decode`, advance, push.
5136                        TypeSpec::Scoped(sc) if scoped_struct(sc).is_some() => {
5137                            let cpp_ty = scoped_to_cpp(sc);
5138                            let id = next_nest_id();
5139                            let var = format!("zd_se{id}");
5140                            writeln!(out, "                        ::dds::topic::xcdr2::skip_pad_from_origin(zd_pos, zd_body_origin, 4);").map_err(fmt_err)?;
5141                            writeln!(
5142                                out,
5143                                "                        const size_t zd_nss{id} = zd_pos;"
5144                            )
5145                            .map_err(fmt_err)?;
5146                            writeln!(out, "                        size_t zd_npk{id} = zd_pos;")
5147                                .map_err(fmt_err)?;
5148                            writeln!(out, "                        const uint32_t zd_nl{id} = ::dds::topic::xcdr2::dheader_read(zd_buf, zd_npk{id}, zd_len, zd_be);").map_err(fmt_err)?;
5149                            writeln!(out, "                        {cpp_ty} {var} = ::dds::topic::topic_type_support<{cpp_ty}>::decode(zd_buf + zd_nss{id}, 4u + zd_nl{id}, zd_repr, zd_be);").map_err(fmt_err)?;
5150                            writeln!(
5151                                out,
5152                                "                        zd_pos = zd_nss{id} + 4u + zd_nl{id};"
5153                            )
5154                            .map_err(fmt_err)?;
5155                            writeln!(
5156                                out,
5157                                "                        zd_seq.push_back(std::move({var}));"
5158                            )
5159                            .map_err(fmt_err)?;
5160                        }
5161                        // nested sequence / map element: read into a temp, push.
5162                        TypeSpec::Sequence(_) | TypeSpec::Map(_) => {
5163                            let inner_ty = typespec_to_cpp(&seq.elem)?;
5164                            let var = format!("zd_se{}", next_nest_id());
5165                            writeln!(out, "                        {inner_ty} {var}{{}};")
5166                                .map_err(fmt_err)?;
5167                            emit_value_read(
5168                                out,
5169                                &seq.elem,
5170                                &format!("{var} ="),
5171                                "zd_body_origin",
5172                                "                        ",
5173                                false,
5174                            )?;
5175                            writeln!(
5176                                out,
5177                                "                        zd_seq.push_back(std::move({var}));"
5178                            )
5179                            .map_err(fmt_err)?;
5180                        }
5181                        _ => {}
5182                    }
5183                    writeln!(out, "                    }}").map_err(fmt_err)?;
5184                    writeln!(out, "                    zd_v.{name}(std::move(zd_seq));")
5185                        .map_err(fmt_err)?;
5186                }
5187            }
5188            // enum member: 4-byte int32 read directly (encoded via compact LC=2).
5189            TypeSpec::Scoped(s) if scoped_is_enum(s) => {
5190                let cpp_ty = scoped_to_cpp(s);
5191                let ec = enum_wire_ctype(scoped_enum_bytes(s));
5192                writeln!(out, "                    zd_v.{name}(static_cast<{cpp_ty}>(::dds::topic::xcdr2::read_le_raw<{ec}>(zd_buf, zd_pos, zd_len, zd_be)));").map_err(fmt_err)?;
5193            }
5194            // nested struct member. FINDING T1b: a @final nested struct is LC=4
5195            // (a separate NEXTINT precedes its tight-packed body). A nested
5196            // @appendable/@mutable struct is LC=5 — its body BEGINS with its own
5197            // DHEADER which doubled as the NEXTINT, so there is NO separate
5198            // NEXTINT (the DHEADER is read by the sub-decode below).
5199            TypeSpec::Scoped(sc) if scoped_struct(sc).is_some() => {
5200                if let Some((def, ext)) = scoped_struct(sc) {
5201                    let cpp_ty = scoped_to_cpp(sc);
5202                    let id = next_nest_id();
5203                    let var = format!("zd_ns{id}");
5204                    if matches!(ext, Extensibility::Final) {
5205                        writeln!(out, "                    auto zd_n = ::dds::topic::xcdr2::emheader_nextint_read(zd_buf, zd_pos, zd_len, zd_be); (void)zd_n;").map_err(fmt_err)?;
5206                    }
5207                    writeln!(
5208                        out,
5209                        "                    auto zd_body_origin = zd_pos; (void)zd_body_origin;"
5210                    )
5211                    .map_err(fmt_err)?;
5212                    writeln!(out, "                    {cpp_ty} {var}{{}};").map_err(fmt_err)?;
5213                    match ext {
5214                        Extensibility::Final => {
5215                            for sm in &def.members {
5216                                let sm_name = &sm.declarators[0].name().text;
5217                                emit_value_read(
5218                                    out,
5219                                    &sm.type_spec,
5220                                    &format!("{var}.{sm_name}"),
5221                                    "zd_body_origin",
5222                                    "                    ",
5223                                    false,
5224                                )?;
5225                            }
5226                        }
5227                        Extensibility::Appendable | Extensibility::Mutable => {
5228                            writeln!(out, "                    const size_t zd_nss{id} = zd_pos;")
5229                                .map_err(fmt_err)?;
5230                            writeln!(out, "                    size_t zd_npk{id} = zd_pos;")
5231                                .map_err(fmt_err)?;
5232                            writeln!(out, "                    const uint32_t zd_nl{id} = ::dds::topic::xcdr2::dheader_read(zd_buf, zd_npk{id}, zd_len, zd_be);").map_err(fmt_err)?;
5233                            writeln!(out, "                    {var} = ::dds::topic::topic_type_support<{cpp_ty}>::decode(zd_buf + zd_nss{id}, 4u + zd_nl{id}, zd_repr, zd_be);").map_err(fmt_err)?;
5234                            writeln!(
5235                                out,
5236                                "                    zd_pos = zd_nss{id} + 4u + zd_nl{id};"
5237                            )
5238                            .map_err(fmt_err)?;
5239                        }
5240                    }
5241                    writeln!(out, "                    zd_v.{name}({var});").map_err(fmt_err)?;
5242                }
5243            }
5244            // map<K,V> member: FINDING T1b — LC=5. The body BEGINS with the map
5245            // DHEADER which doubled as the NEXTINT, so there is NO separate
5246            // NEXTINT; the DHEADER is read below before the count + entries
5247            // (symmetric to the mutable encode).
5248            TypeSpec::Map(m) => {
5249                let k_ty = typespec_to_cpp(&m.key)?;
5250                let v_ty = typespec_to_cpp(&m.value)?;
5251                let id = next_nest_id();
5252                let mapv = format!("zd_map{id}");
5253                let kv = format!("zd_mk{id}");
5254                let vv = format!("zd_mv{id}");
5255                writeln!(out, "                    auto zd_body_origin = zd_pos;")
5256                    .map_err(fmt_err)?;
5257                // Every @mutable member carries a 4-byte length word right after its
5258                // EMHEADER (the dispatcher reads only the EMHEADER). For a non-prim
5259                // map (LC=5) it is the map's own DHEADER; for a primitive map (LC=4)
5260                // it is the NEXTINT byte length. Positionally identical — consume it
5261                // unconditionally, then read the count. (See encode arm above.)
5262                writeln!(out, "                    {{ const auto zd_map_dh = ::dds::topic::xcdr2::dheader_read_r(zd_buf, zd_pos, zd_len, zd_be, zd_repr); (void)zd_map_dh; }}").map_err(fmt_err)?;
5263                writeln!(out, "                    auto zd_mcnt = ::dds::topic::xcdr2::read_le_origin<uint32_t>(zd_buf, zd_pos, zd_len, zd_body_origin, zd_max_align, zd_be);").map_err(fmt_err)?;
5264                writeln!(out, "                    std::map<{k_ty}, {v_ty}> {mapv};")
5265                    .map_err(fmt_err)?;
5266                writeln!(
5267                    out,
5268                    "                    for (uint32_t zd_i = 0; zd_i < zd_mcnt; ++zd_i) {{"
5269                )
5270                .map_err(fmt_err)?;
5271                writeln!(out, "                        {k_ty} {kv}{{}};").map_err(fmt_err)?;
5272                writeln!(out, "                        {v_ty} {vv}{{}};").map_err(fmt_err)?;
5273                emit_value_read(
5274                    out,
5275                    &m.key,
5276                    &format!("{kv} ="),
5277                    "zd_body_origin",
5278                    "                        ",
5279                    false,
5280                )?;
5281                emit_value_read(
5282                    out,
5283                    &m.value,
5284                    &format!("{vv} ="),
5285                    "zd_body_origin",
5286                    "                        ",
5287                    false,
5288                )?;
5289                writeln!(
5290                    out,
5291                    "                        {mapv}.emplace(std::move({kv}), std::move({vv}));"
5292                )
5293                .map_err(fmt_err)?;
5294                writeln!(out, "                    }}").map_err(fmt_err)?;
5295                writeln!(out, "                    zd_v.{name}(std::move({mapv}));")
5296                    .map_err(fmt_err)?;
5297            }
5298            TypeSpec::Fixed(_) => {
5299                // @mutable fixed member: encoder framed it LC=4, so a separate
5300                // NEXTINT (byte length) precedes the raw BCD body — consume it,
5301                // then read (P+2)/2 octets into a `::dds::core::Fixed<P,S>`.
5302                let cpp_ty = typespec_to_cpp(&m.type_spec)?;
5303                writeln!(out, "                    {{ auto zd_n = ::dds::topic::xcdr2::emheader_nextint_read(zd_buf, zd_pos, zd_len, zd_be); (void)zd_n; }}").map_err(fmt_err)?;
5304                writeln!(out, "                    ::dds::topic::xcdr2::check_avail(zd_pos, {cpp_ty}::kByteCount, zd_len);").map_err(fmt_err)?;
5305                writeln!(out, "                    {{ {cpp_ty} zd_fx(zd_buf + zd_pos); zd_pos += {cpp_ty}::kByteCount; zd_v.{name}(std::move(zd_fx)); }}").map_err(fmt_err)?;
5306            }
5307            _ => {}
5308        }
5309        writeln!(out, "                    break;").map_err(fmt_err)?;
5310        writeln!(out, "                }}").map_err(fmt_err)?;
5311    }
5312    Ok(())
5313}
5314
5315fn emit_key_hash_fn(
5316    out: &mut String,
5317    cpp_fqn: &str,
5318    s: &StructDef,
5319    is_keyed: bool,
5320    phase: TtsPhase,
5321) -> Result<(), CppGenError> {
5322    if phase == TtsPhase::Decl {
5323        writeln!(
5324            out,
5325            "    static std::array<uint8_t, 16> key_hash(const {cpp_fqn}& zd_v);"
5326        )
5327        .map_err(fmt_err)?;
5328        return Ok(());
5329    }
5330    writeln!(
5331        out,
5332        "inline std::array<uint8_t, 16> topic_type_support<{cpp_fqn}>::key_hash(const {cpp_fqn}& zd_v) {{"
5333    )
5334    .map_err(fmt_err)?;
5335    writeln!(out, "        (void)zd_v;").map_err(fmt_err)?;
5336    if !is_keyed {
5337        writeln!(
5338            out,
5339            "        return std::array<uint8_t, 16>{{{{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}}}};"
5340        )
5341        .map_err(fmt_err)?;
5342        writeln!(out, "    }}").map_err(fmt_err)?;
5343        return Ok(());
5344    }
5345    writeln!(out, "        std::vector<uint8_t> zd_out;").map_err(fmt_err)?;
5346    writeln!(out, "        const size_t zd_origin = 0;").map_err(fmt_err)?;
5347    writeln!(out, "        (void)zd_origin;").map_err(fmt_err)?;
5348    // KeyHash serializes @key members as big-endian XCDR1 (RTPS §9.6.3.8 / XTypes
5349    // §7.6.8) — full natural alignment, NO XCDR2 8->4 cap. xcdr_max_align(Xcdr1)
5350    // = 8, so capped_align(sizeof(T), 8) == sizeof(T): byte-identical to the
5351    // previous uncapped key serialization.
5352    writeln!(
5353        out,
5354        "        const size_t zd_max_align = ::dds::topic::xcdr2::xcdr_max_align(::dds::topic::xcdr2::XcdrVersion::Xcdr1);"
5355    )
5356    .map_err(fmt_err)?;
5357    writeln!(out, "        (void)zd_max_align;").map_err(fmt_err)?;
5358    for m in &s.members {
5359        if !has_key_annotation(&m.annotations) {
5360            continue;
5361        }
5362        emit_plain_member_encode(out, m, "be", "zd_origin")?;
5363    }
5364    // XTypes 1.3 §7.6.8.4: holder ≤ 16 octets -> zero-pad; otherwise MD5.
5365    writeln!(out, "        std::array<uint8_t, 16> zd_h{{}};").map_err(fmt_err)?;
5366    writeln!(out, "        if (zd_out.size() <= 16) {{").map_err(fmt_err)?;
5367    writeln!(
5368        out,
5369        "            std::memcpy(zd_h.data(), zd_out.data(), zd_out.size());"
5370    )
5371    .map_err(fmt_err)?;
5372    writeln!(out, "            return zd_h;").map_err(fmt_err)?;
5373    writeln!(out, "        }}").map_err(fmt_err)?;
5374    writeln!(out, "        return ::dds::topic::xcdr2_md5::md5(zd_out);").map_err(fmt_err)?;
5375    writeln!(out, "    }}").map_err(fmt_err)?;
5376    Ok(())
5377}
5378
5379// ---------------------------------------------------------------------------
5380// Helpers: fmt-Error-Bridge
5381// ---------------------------------------------------------------------------
5382
5383fn fmt_err(_: core::fmt::Error) -> CppGenError {
5384    CppGenError::Internal("string formatting failed".into())
5385}
5386
5387#[allow(dead_code)]
5388fn _ensure_used() {
5389    // is_reserved is used by check_identifier — a compiler hint.
5390    let _ = is_reserved("int");
5391}