Skip to main content

zerodds_lang_rules/adapter/
cpp.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The C++ CST as the other end of a rule. Spec §3.2.
4//!
5//! The counterpart of [`crate::adapter::c`], function for function.
6//! Reading the two side by side is the point: where they differ, the
7//! difference is C++'s, and it is written down here rather than spread
8//! through a renderer.
9//!
10//! Values are resolved from the model, never from the overlay — the same
11//! rule the Rust and the C materialiser follow and for the same reason
12//! (spec §2.3).
13//!
14//! ## The four things C++ gives back that C does not
15//!
16//! 1. **Generics, so most structural typedefs disappear.** C has to
17//!    declare a named `zd_seq_int32_t` because it cannot write an
18//!    anonymous aggregate twice and have the two be one type;
19//!    `std::vector<std::int32_t>` is that type wherever it is written.
20//!    The whole of the C column's structural-name machinery — the
21//!    `zd_seq_`, `zd_arr_`, `zd_map_` declarations and the collision
22//!    hazard that came with them — is gone from the output. It is
23//!    **not** gone from the naming: see [`cpp_type_tag`].
24//! 2. **Namespaces, so a module is a scope and not a prefix.** C
25//!    flattens `a::b::Foo` to `a_b_Foo`; here the IDL scoping survives as
26//!    `namespace a { namespace b { struct Foo … } }`, and every
27//!    reference to a declared type is written from the root — `::a::b::Foo`
28//!    — so a member never depends on which namespace it is read in.
29//! 3. **An enumeration wide enough.** A C enumeration constant is an
30//!    `int` (C11 §6.7.2.2), which is why the C column emits a
31//!    fixed-width typedef and a run of `#define`s. C++11's scoped
32//!    enumeration takes an explicit underlying type, so `@bit_bound(32)`
33//!    — which admits enumerator values up to `2^32 - 1` — can be held by
34//!    the type itself. This is the one place C++ is simpler.
35//! 4. **A plain pointer for an absent member.** The C column writes a
36//!    `T *` and a NULL for an `@optional` member, and so does this one:
37//!    DDS-XTypes 1.3 §7.5.1.2.4.2 asks for "plain pointers rather than
38//!    automatic values or smart pointers", NULL for an omitted value. The
39//!    class owns what the pointer points at — the destructor deletes it,
40//!    a copy is deep (§7.5.1.2.3.2, "the same as specified for C") — and
41//!    the member functions that say so are generated beside the members
42//!    (`owning_members`).
43//!
44//! ## The memory shapes
45//!
46//! | IDL | C++ |
47//! |---|---|
48//! | `string` | `std::string` |
49//! | `wstring` | `std::string`, holding UTF-8 |
50//! | `fixed<P,S>` | `std::string`, holding the decimal |
51//! | `long double` | `zerodds::wire::Float128` |
52//! | `sequence<T>` | `std::vector<T>` |
53//! | `T a[2][3]` | `std::array<std::array<T, 3>, 2>` |
54//! | `map<K,V>` | `zerodds::Map<K, V>`, a `std::vector<std::pair<K, V>>` with the operations of Table 47 |
55//! | `union` | a class with `_d()` and one accessor per case (IDL4 to C++ 1.0) over a discriminator and one storage per case |
56//! | `@optional T m` | `T *m`, NULL when absent |
57//!
58//! A `wstring` holds UTF-8 rather than `char16_t` units for the reason
59//! the C column gives: it puts the §7.4.1.1.2 conversion — the surrogate
60//! rule the byte count depends on — inside `runtime/cpp`, where it is a
61//! second implementation of the clause and not a copy of the Rust one's
62//! answer.
63//!
64//! A `map<K,V>` is `zerodds::Map` — XTypes 1.3
65//! §7.5.1.3.3, a class with the operations of Table 47 — over a **vector of
66//! pairs** and not a `std::map`. The order a map's entries go on the wire
67//! is a policy axis — `zerodds_ast_layout::MapEntryOrder`, declaration
68//! order against ascending key — and `std::map` would impose its own
69//! ordering on every dialect before the encoder ever saw the value. A
70//! container that cannot hold declaration order cannot reproduce the four
71//! implementations that write it. The Modern C++ binding of §7.5.1.3.4,
72//! `std::map`, is [`CppMapping::Psm`].
73//!
74//! ## Where the IDL spelling of a member is kept
75//!
76//! [`cxx_name`] moves a colliding name (§7.11.1.1.2 rule 8) — `long static;`
77//! becomes `std::int32_t cxx_static;` — and C++ has more keywords to
78//! collide with than C, so it moves more names. XTypes 1.3 §7.3.1.2.1.1
79//! computes a member's ID under `@autoid(HASH)` and under `@hashid`
80//! from the first four octets of the MD5 of its **IDL** name, so a
81//! header that only knows `cxx_static` cannot compute the ID for `static`
82//! and the member it writes lands under an ID the peer does not have.
83//!
84//! Every declaration with named parts carries them beside itself, in
85//! IDL declaration order — the order the member IDs are assigned in:
86//!
87//! ```cpp
88//! inline constexpr std::array<std::pair<const char*, const char*>, 2>
89//!     Sample_idl_names = {{{"impl", "impl"}, {"cxx_static", "static"}}};
90//! ```
91//!
92//! An **`inline constexpr` variable and not the C column's macro**, and
93//! for the reason the constants above are one too: C reaches for the
94//! preprocessor because an object in a header is a separate object in
95//! every translation unit that includes it, and C++17's inline variable
96//! is one object across the program. So the mapping can be a typed name
97//! — iterable with `.size()`, usable in a `constexpr` context, and
98//! reachable as `::a::b::Sample_idl_names` from anywhere — where the C
99//! column has to hand a caller a brace-enclosed initialiser and let it
100//! declare the array itself. Measured unused, under
101//! `-std=c++20 -Wall -Wextra -Werror`, across all 376 headers the
102//! corpus renders: no diagnostic.
103//!
104//! `std::pair` and not a generated record type: a record would be a
105//! declaration whose name could collide with an IDL type, in a header
106//! that has no other reason to declare one, and `<utility>` is already
107//! in [`includes`].
108//!
109//! A union is a **class**, not a `union` and not a `std::variant`. Two
110//! cases may carry the same type — the corpus has such types — and a
111//! `std::variant<T, T>` cannot say which of the two is live without an
112//! index the discriminator already is. The class has the accessor
113//! functions of IDL4 to C++ 1.0 §7.5.1.4 (`_d()`, and per case a getter, a
114//! reference getter and two setters, a setter selecting its case) over
115//! one storage member per case and one for the discriminator, which is
116//! the C column's `_d` plus `_u` with the anonymous union opened out; the
117//! generated codec reads and writes the storage (`zd_d`, `zd_<case>`). A
118//! data member is never warned about for being unused, so this compiles
119//! under `-Wall -Wextra -Werror`.
120
121use crate::adapter::c::ordered_types;
122use crate::adapter::constant;
123use crate::adapter::cst::{MaterialiseError, no_spelling_collision};
124use crate::apply::Applied;
125use crate::verbatim;
126use seesaw_tgg::ident::GhostId;
127use zerodds_ast::{Ast, CaseLabel, ModuleNode, PrimitiveKind, TypeKind, TypeNode};
128use zerodds_ast_core::{Id, NodeId};
129use zerodds_lang_cst::c::Include;
130use zerodds_lang_cst::common::{Doc, Ident, Literal, Path};
131use zerodds_lang_cst::cpp;
132
133/// The C++ spelling of `long double`.
134///
135/// `zerodds::wire::Float128` and **never** the compiler's `long double`:
136/// that type is x87 80-bit extended on x86-64, binary128 on aarch64 and
137/// a plain `double` on macOS arm64, so a generated class declaring it
138/// would have a different wire format on each. `runtime/cpp/include/
139/// zerodds/wire.hpp` carries the sixteen-byte type and the conversions
140/// that report the values which do not fit; `crate::adapter::c::FLOAT128`
141/// is the same decision in the C column.
142pub const FLOAT128: &str = "zerodds::wire::Float128";
143
144/// The headers a materialised translation unit needs.
145///
146/// The C column's counterpart is [`crate::adapter::c::prelude`], which
147/// declares support *types* because C has no other way to name a string
148/// or a sequence. C++ needs no support types — the shapes are `std::`
149/// templates — but it does need the headers those templates come from,
150/// and a caller that assembled the `#include` list by hand would drift
151/// from what the materialiser emits. `<cstddef>` is not among them:
152/// nothing below writes a `size_t`.
153#[must_use]
154pub fn includes() -> Vec<Include> {
155    vec![
156        Include::system("array"),
157        Include::system("cstdint"),
158        Include::system("memory"),
159        Include::system("optional"),
160        Include::system("string"),
161        Include::system("utility"),
162        Include::system("vector"),
163        Include::local("zerodds/map.hpp"),
164        Include::local("zerodds/wire.hpp"),
165    ]
166}
167
168/// Build the C++ declarations a rule application produced.
169///
170/// The walk is [`ordered_types`] — a post-order over the type graph, the
171/// same one the C column takes. The declaration-order problem is not C's
172/// alone: a C++ class whose member names a class not yet defined does not
173/// compile either, and `std::optional<T>` and `std::array<T, N>` both
174/// require `T` complete.
175///
176/// # Errors
177/// [`MaterialiseError`] when a value the output has to state does not
178/// evaluate, or a type has no C++ shape — never a silently dropped
179/// declaration.
180pub fn cpp_items(ast: &Ast, applied: &Applied) -> Result<Vec<cpp::Decl>, MaterialiseError> {
181    let mut out: Vec<(Id<ModuleNode>, cpp::Decl)> = Vec::new();
182    let mut emitted: Vec<String> = Vec::new();
183
184    // `@verbatim(placement = BEGIN_FILE)`, wherever it was written,
185    // opens the file — OMG IDL 4.2 §8.3.5.1 — so it stands before even
186    // the forward declarations. Paired with the root module, so
187    // `nest_by_module` leaves it at file scope.
188    let (begin_file, end_file) = verbatim::file_slots(ast, verbatim::aliases::CPP);
189    for text in begin_file {
190        out.push((ast.root_module(), cpp::Decl::Verbatim(text)));
191    }
192
193    // A forward declaration for every aggregate, ahead of every
194    // definition. A post-order over the type graph has no valid order
195    // for two types that name each other — `recursive-mutual` is
196    // `struct A { sequence<B> bs; }` beside `struct B { sequence<A> as; }`
197    // — and one of the two definitions always names a class the compiler
198    // has not seen. `std::vector<T>` admits an incomplete `T` since
199    // C++17, which is what makes the declaration enough.
200    for id in ordered_types(ast) {
201        let Ok(node) = ast.type_(id) else { continue };
202        if !matches!(
203            node.kind,
204            TypeKind::Structure { .. } | TypeKind::Union { .. } | TypeKind::Valuetype { .. }
205        ) {
206            continue;
207        }
208        out.push((
209            node.module,
210            cpp::Decl::Forward {
211                is_struct: true,
212                name: Ident::new(&cxx_name(&node.name)),
213            },
214        ));
215    }
216
217    no_bits_name_collision(ast)?;
218
219    for id in ordered_types(ast) {
220        let start = out.len();
221        declare(ast, applied, id, &mut out, &mut emitted)?;
222        weave_verbatim(ast, id, start, &mut out);
223        maps_alongside(ast, id, &mut out, &emitted);
224    }
225
226    // Constants and exceptions are not types, so they are walked
227    // separately — the model keeps them in arenas of their own, and both
228    // the Rust and the C materialiser do the same. Interfaces are not
229    // walked at all, exactly as `c_items` does not walk them: an
230    // interface has no data form, its operations belong to the RPC arc,
231    // and `crate::coverage` is where that is written down.
232    for (id, node) in ast.constants() {
233        if ghost_created_for(applied, node.node_id, "CppConst").is_none() {
234            continue;
235        }
236        let _ = id;
237        // `eval_const` and not `eval_integral`: `const string MODULE_ID`
238        // evaluates perfectly well and is not a number, and asking the
239        // integral question about it answered `Unevaluated` and dropped
240        // the declaration.
241        let value = ast
242            .eval_const(node.expr)
243            .map_err(|_| MaterialiseError::Unevaluated {
244                construct: "constant's value".to_string(),
245                at: node.name.clone(),
246            })?;
247        // `inline constexpr` and not the C column's `#define`. The
248        // reason C reached for the preprocessor is C-specific — a
249        // `static const` in a header is a separate object in every
250        // translation unit that includes it, and every one that does not
251        // use it warns — and C++17's inline variable is one object
252        // across the program with no such warning, so the constant can
253        // be a typed name instead of a token substitution.
254        let declared = match node.type_.resolved() {
255            Some(target) => cpp_value_type(ast, target, &node.name)?,
256            None => {
257                return Err(MaterialiseError::Unevaluated {
258                    construct: "constant's type".to_string(),
259                    at: node.name.clone(),
260                });
261            }
262        };
263        let (type_, literal) = cpp_constant(&value, declared, &node.name)?;
264        out.push((
265            node.module,
266            cpp::Decl::Variable(cpp::Variable {
267                doc: Doc::none(),
268                type_,
269                name: Ident::new(&cxx_name(&node.name)),
270                value: Some(cpp::Expr::Literal(literal)),
271                is_static: false,
272                is_constexpr: true,
273                is_inline: true,
274            }),
275        ));
276    }
277
278    for (id, node) in ast.exceptions() {
279        if ghost_created_for(applied, node.node_id, "CppError").is_none() {
280            continue;
281        }
282        let _ = id;
283        let mut fields = Vec::new();
284        let mut names = Vec::new();
285        for member in &node.members {
286            let Ok(member_node) = ast.member(*member) else {
287                return Err(MaterialiseError::Unevaluated {
288                    construct: "exception member the model listed".to_string(),
289                    at: node.name.clone(),
290                });
291            };
292            let Some(target) = member_node.type_.resolved() else {
293                return Err(MaterialiseError::Unevaluated {
294                    construct: "exception member type".to_string(),
295                    at: member_node.name.clone(),
296                });
297            };
298            names.push((cxx_name(&member_node.name), member_node.name.clone()));
299            fields.push(field(
300                cpp_value_type(ast, target, &member_node.name)?,
301                &member_node.name,
302                Doc::none(),
303            ));
304        }
305        let exception_name = cxx_name(&node.name);
306        out.push((
307            node.module,
308            cpp::Decl::Class(class(
309                &exception_name,
310                fields,
311                Doc::line("An exception, as the class its members make."),
312            )),
313        ));
314        out.extend(
315            idl_names(&exception_name, &names)?
316                .into_iter()
317                .map(|decl| (node.module, decl)),
318        );
319        out.push((node.module, idl_name(&exception_name, &node.name)));
320    }
321
322    // `@verbatim(placement = END_FILE)` closes the file, after even the
323    // constants and exceptions. OMG IDL 4.2 §8.3.5.1.
324    for text in end_file {
325        out.push((ast.root_module(), cpp::Decl::Verbatim(text)));
326    }
327
328    nest_by_module(ast, out)
329}
330
331/// Weave one type's `@verbatim` text around and into what `declare`
332/// just pushed. OMG IDL 4.2 §8.3.5.1; the slot semantics are
333/// [`crate::verbatim`]'s.
334///
335/// `start` is where the type's declarations begin in `out`.
336/// `BEGIN_DECLARATION` and `END_DECLARATION` land inside the class body
337/// when the type materialised as one; an enum, an alias or a type that
338/// declared nothing has no body a C++ tree can hold raw text in, so the
339/// text lands adjacent to the declaration instead — the nearest
340/// position the tree can express, never a dropped directive.
341fn weave_verbatim(
342    ast: &Ast,
343    id: Id<TypeNode>,
344    start: usize,
345    out: &mut Vec<(Id<ModuleNode>, cpp::Decl)>,
346) {
347    let slots = verbatim::declaration_slots(ast, id, verbatim::aliases::CPP);
348    if slots.before.is_empty()
349        && slots.begin.is_empty()
350        && slots.end.is_empty()
351        && slots.after.is_empty()
352    {
353        return;
354    }
355    let Ok(node) = ast.type_(id) else {
356        return;
357    };
358    let name = cxx_name(&node.name);
359    let mut declared = out.split_off(start);
360    let mut begin = slots.begin;
361    let mut end = slots.end;
362    let body = declared.iter_mut().find_map(|(_, decl)| match decl {
363        cpp::Decl::Class(class) if class.name.as_str() == name => Some(class),
364        _ => None,
365    });
366    if let Some(class) = body {
367        if class.sections.is_empty() {
368            class.sections.push(cpp::Section {
369                access: cpp::Access::Public,
370                members: Vec::new(),
371            });
372        }
373        if let Some(first) = class.sections.first_mut() {
374            for text in begin.drain(..).rev() {
375                first.members.insert(0, cpp::ClassMember::Verbatim(text));
376            }
377        }
378        if let Some(last) = class.sections.last_mut() {
379            for text in end.drain(..) {
380                last.members.push(cpp::ClassMember::Verbatim(text));
381            }
382        }
383    }
384    for text in slots.before.into_iter().chain(begin) {
385        out.push((node.module, cpp::Decl::Verbatim(text)));
386    }
387    out.append(&mut declared);
388    for text in end.into_iter().chain(slots.after) {
389        out.push((node.module, cpp::Decl::Verbatim(text)));
390    }
391}
392
393/// The declarations, each inside the `namespace` its IDL module names.
394///
395/// Public for the reason the Rust column's `nest_by_module` is: the
396/// codecs are a second call, once per type, and they have to land in the
397/// same namespace as the declaration they refer to.
398///
399/// Unlike the Rust column's, this one folds **consecutive runs** rather
400/// than gathering every item of a module at the position of its first.
401/// The order handed in is a topological one, and gathering would move a
402/// declaration back in front of a type it names. A C++ namespace may be
403/// reopened, so a run-wise fold costs nothing but a repeated
404/// `namespace a {` line and keeps the order the walk computed.
405///
406/// # Errors
407/// [`MaterialiseError::NoModule`] when the model does not answer for a
408/// module an item names, or the parent chain runs past
409/// `MAX_MODULE_DEPTH`. Returning the item unwrapped instead would put
410/// it in the enclosing scope and say nothing — `module a { struct P; };`
411/// and `module b { struct P; };` are two types.
412pub fn nest_by_module(
413    ast: &Ast,
414    items: Vec<(Id<ModuleNode>, cpp::Decl)>,
415) -> Result<Vec<cpp::Decl>, MaterialiseError> {
416    let mut chained: Vec<(Vec<String>, cpp::Decl)> = Vec::with_capacity(items.len());
417    for (module, decl) in items {
418        // `@verbatim` at file scope is the user's text and stays there, whatever namespace
419        // the declarations around it are in.
420        let chain = if matches!(decl, cpp::Decl::Verbatim(_)) && module == ast.root_module() {
421            Vec::new()
422        } else {
423            module_chain(ast, module)?
424        };
425        chained.push((chain, decl));
426    }
427    Ok(nest(&chained, 0))
428}
429
430/// As [`nest_by_module`], for declarations of the two kinds
431/// [`CppMapping::Psm`] tells apart: `native` ones stand in
432/// [`NATIVE_NAMESPACE`], the others in the namespaces of the IDL.
433///
434/// # Errors
435/// As [`nest_by_module`].
436pub fn nest_by_module_placed(
437    ast: &Ast,
438    items: Vec<(Id<ModuleNode>, bool, cpp::Decl)>,
439) -> Result<Vec<cpp::Decl>, MaterialiseError> {
440    let mut chained: Vec<(Vec<String>, cpp::Decl)> = Vec::with_capacity(items.len());
441    for (module, native, decl) in items {
442        let chain = if matches!(decl, cpp::Decl::Verbatim(_)) && module == ast.root_module() {
443            Vec::new()
444        } else if native {
445            module_chain(ast, module)?
446        } else {
447            without_native_prefix(|| module_chain(ast, module))?
448        };
449        chained.push((chain, decl));
450    }
451    Ok(nest(&chained, 0))
452}
453
454/// How the C++ column spells topic types.
455///
456/// [`CppMapping::Psm`] is the default: the mapping DDS-PSM-Cxx 1.0 §7.4 and §8.1 ask for —
457/// classes with accessors, `dds::core::optional`, `dds::core::array`, `wchar_t`, `std::wstring`,
458/// `long double`, `std::map`. It is a layer over the native types and not a second codec: the
459/// native types move into the namespace [`NATIVE_NAMESPACE`], the PSM types take the names of the
460/// IDL, and `zerodds::psm::Conv` converts between them on the way into the codec and out of it.
461///
462/// [`CppMapping::Native`] is the named option that the wire codec, the endpoint gates and every
463/// other consumer of the shape of a generated header ask for explicitly: structures with public
464/// fields, a plain pointer for `@optional`, `std::array`, a `map` as a vector of pairs.
465#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
466pub enum CppMapping {
467    /// Public fields, the types the codec reads and writes (`--cpp-mapping native`).
468    Native,
469    /// DDS-PSM-Cxx 1.0 §7.4 and §8.1, over the native types (the default).
470    #[default]
471    Psm,
472    /// The classic OMG IDL to C++ 1.3 mapping, in C++03, over the native types
473    /// (see [`super::cpp_classic`]).
474    Classic,
475}
476
477/// The namespace the native types stand in under [`CppMapping::Psm`].
478pub const NATIVE_NAMESPACE: &str = "zd_native";
479
480thread_local! {
481    static MAPPING: std::cell::Cell<CppMapping> = const { std::cell::Cell::new(CppMapping::Psm) };
482    static PREFIX_SUSPENDED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
483}
484
485/// Selects the C++ mapping on the current thread until dropped.
486///
487/// A scope and not a parameter because the mapping decides names in
488/// every function that spells a type path, and threading an argument
489/// through all of them would change signatures other columns share.
490#[derive(Debug)]
491pub struct CppMappingScope {
492    previous: CppMapping,
493}
494
495impl CppMappingScope {
496    /// Selects `mapping` on this thread.
497    #[must_use]
498    pub fn enter(mapping: CppMapping) -> Self {
499        let previous = MAPPING.with(|cell| cell.replace(mapping));
500        Self { previous }
501    }
502}
503
504impl Drop for CppMappingScope {
505    fn drop(&mut self) {
506        MAPPING.with(|cell| cell.set(self.previous));
507    }
508}
509
510/// The mapping selected on this thread.
511#[must_use]
512pub fn cpp_mapping() -> CppMapping {
513    MAPPING.with(std::cell::Cell::get)
514}
515
516/// The namespace the native types are in right now: [`NATIVE_NAMESPACE`] under
517/// [`CppMapping::Psm`], none otherwise and while the PSM layer spells its own
518/// names.
519fn native_prefix() -> Option<&'static str> {
520    (cpp_mapping() != CppMapping::Native && !PREFIX_SUSPENDED.with(std::cell::Cell::get))
521        .then_some(NATIVE_NAMESPACE)
522}
523
524/// The enclosing module names of `module`, outermost first, without the native namespace.
525pub(crate) fn module_names(
526    ast: &Ast,
527    module: Id<ModuleNode>,
528) -> Result<Vec<String>, MaterialiseError> {
529    without_native_prefix(|| module_chain(ast, module))
530}
531
532/// Runs `f` with the native namespace taken off every path it spells, which is how the
533/// PSM layer names the types it declares itself.
534pub(crate) fn without_native_prefix<R>(f: impl FnOnce() -> R) -> R {
535    let previous = PREFIX_SUSPENDED.with(|cell| cell.replace(true));
536    let result = f();
537    PREFIX_SUSPENDED.with(|cell| cell.set(previous));
538    result
539}
540
541/// How deep a module chain is followed before the model is taken to be
542/// malformed. IDL nests nowhere near this.
543const MAX_MODULE_DEPTH: usize = 64;
544
545/// How deep a type spelling is followed before the model is taken to
546/// hold a cycle. A `sequence<sequence<…>>` nests nowhere near this.
547const MAX_TYPE_DEPTH: usize = 64;
548
549/// The module handle, named without the node the model would not answer
550/// for. Slot and generation are what the arena was asked for.
551fn module_handle(module: Id<ModuleNode>) -> String {
552    format!("#{}@{}", module.slot(), module.generation())
553}
554
555/// The enclosing module names, outermost first, keyword-escaped.
556///
557/// The root is the anonymous global scope and contributes no segment.
558fn module_chain(ast: &Ast, module: Id<ModuleNode>) -> Result<Vec<String>, MaterialiseError> {
559    let mut out = Vec::new();
560    let mut current = Some(module);
561    let mut depth = 0usize;
562    while let Some(id) = current {
563        if depth > MAX_MODULE_DEPTH {
564            return Err(MaterialiseError::NoModule {
565                at: format!(
566                    "{} — the parent chain runs past {MAX_MODULE_DEPTH}",
567                    module_handle(module)
568                ),
569            });
570        }
571        depth += 1;
572        let node = ast.module(id).map_err(|_| MaterialiseError::NoModule {
573            at: module_handle(id),
574        })?;
575        if !node.name.is_empty() {
576            out.push(cxx_name(&node.name));
577        }
578        current = node.parent;
579    }
580    out.reverse();
581    if let Some(prefix) = native_prefix() {
582        out.insert(0, prefix.to_string());
583    }
584    Ok(out)
585}
586
587/// One level of the namespace fold.
588///
589/// zerodds-lint: recursion-depth 64 (namespace nesting; every chain is
590/// built by `module_chain`, which is bounded by `MAX_MODULE_DEPTH`)
591fn nest(items: &[(Vec<String>, cpp::Decl)], depth: usize) -> Vec<cpp::Decl> {
592    let mut out = Vec::new();
593    let mut index = 0;
594    while index < items.len() {
595        let Some((chain, decl)) = items.get(index) else {
596            break;
597        };
598        let Some(name) = chain.get(depth) else {
599            out.push(decl.clone());
600            index += 1;
601            continue;
602        };
603        let name = name.clone();
604        let start = index;
605        while items
606            .get(index)
607            .is_some_and(|(chain, _)| chain.get(depth) == Some(&name))
608        {
609            index += 1;
610        }
611        out.push(cpp::Decl::Namespace(cpp::Namespace {
612            doc: Doc::none(),
613            name: Ident::new(&name),
614            decls: nest(&items[start..index], depth + 1),
615        }));
616    }
617    out
618}
619
620/// The name component a collection or a codec function takes from a
621/// type.
622///
623/// The IDL kind and **not** the C++ spelling, and this is not cosmetic.
624/// C++ has generics, so the type mapping is allowed to collapse things
625/// the wire keeps apart, and it does:
626///
627/// - `octet` is `zerodds::wire::Octet` and `uint8` is `std::uint8_t`, which
628///   the tag keeps apart anyway: the two are one wire form but two names;
629/// - `wchar` and `unsigned short` are both `std::uint16_t`;
630/// - `string`, `wstring` and `fixed<P,S>` are all `std::string`, and the
631///   three are framed differently on the wire — a length in bytes, a
632///   length in code units after a §7.4.1.1.2 conversion, and a decimal
633///   in `P` digits;
634/// - `double` and `long double` would both be `double` if the tag were
635///   built from a C++ built-in, and they are eight and sixteen bytes.
636///
637/// The C column met the last of those as a real defect: the structural
638/// name gave `sequence<double>` and `sequence<long double>` one
639/// declaration and one encoder, and the second went out at the first's
640/// width with nothing to say it had. C++ does not collapse the
641/// *declarations* — there are none to collapse — but a codec emitter that
642/// named its functions from the C++ spelling would collapse the
643/// *functions*, which is the same defect one layer along. So the tag is
644/// built here, from the model.
645#[must_use]
646pub fn cpp_type_tag(ast: &Ast, id: Id<TypeNode>) -> String {
647    type_tag(ast, id, 0)
648}
649
650/// The bound a `string`, a `sequence` or a `map` carries, as the suffix
651/// that keeps two of them apart.
652///
653/// The tag names the routine a hoisted collection's codec is emitted
654/// under, and one body is emitted per name. With the bound left out,
655/// `sequence<long>` and `sequence<long, 4>` are one name and whichever
656/// was declared first decides: measured on
657/// `@final struct S { sequence<long> b; sequence<long, 4> a; };`, the
658/// generated C++ held no `zd_over` check at all, so `a` accepted a
659/// stream of any length; with the members the other way round both got
660/// the bounded body, so `b` refused — and truncated — a length the IDL
661/// puts no limit on. `string<4>` beside `string` and `wstring<4>`
662/// beside `wstring` collapse the same way, onto the trimming call.
663/// Either way the reader answered with a value the IDL does not
664/// describe and said nothing, which is why the bound is part of the
665/// name. The same suffix the Zig column carries.
666///
667/// Only the *functions* split here. There is no declaration to split:
668/// `sequence<long>` and `sequence<long, 4>` are both
669/// `std::vector<std::int32_t>`, and [`cpp_value_type`] — not this — is
670/// what a member is spelled through. That is the difference from the C
671/// column, where the tag also names a declared struct.
672///
673/// An `array` needs nothing here: its `dims` are already in the tag,
674/// under the `Array` arm below.
675fn bound_tag(ast: &Ast, bound: &zerodds_ast::Bound) -> String {
676    match bound.expr() {
677        None => String::new(),
678        // `_b` and not a bare `_n`: `seq_long_4` is what the `Array`
679        // arm answers for an array of four `long`, and the two are
680        // different wire forms.
681        Some(expr) => match ast.eval_integral(expr) {
682            Ok(limit) => format!("_b{limit}"),
683            // As the array's `_x`: a bound that does not evaluate is
684            // still not the unbounded form, and the materialiser
685            // reports the expression that would not evaluate.
686            Err(_) => "_bx".to_string(),
687        },
688    }
689}
690
691/// zerodds-lint: recursion-depth 64 (element types; bounded by
692/// `MAX_TYPE_DEPTH`)
693fn type_tag(ast: &Ast, id: Id<TypeNode>, depth: usize) -> String {
694    if depth > MAX_TYPE_DEPTH {
695        return "deep".to_string();
696    }
697    let Ok(node) = ast.type_(id) else {
698        return "void".to_string();
699    };
700    match &node.kind {
701        TypeKind::Primitive(kind) => kind.as_str().replace(' ', "_"),
702        TypeKind::String {
703            width: zerodds_ast::CharWidth::Char,
704            bound,
705        } => format!("string{}", bound_tag(ast, bound)),
706        TypeKind::String {
707            width: zerodds_ast::CharWidth::WChar,
708            bound,
709        } => format!("wstring{}", bound_tag(ast, bound)),
710        TypeKind::Fixed { digits, scale } => format!("fixed_{digits}_{scale}"),
711        TypeKind::Sequence { element, bound } => match element.resolved() {
712            Some(element) => format!(
713                "seq{}_{}",
714                bound_tag(ast, bound),
715                type_tag(ast, element, depth + 1)
716            ),
717            None => "void".to_string(),
718        },
719        TypeKind::Array { element, dims } => match element.resolved() {
720            Some(element) => {
721                let mut name = format!("arr_{}", type_tag(ast, element, depth + 1));
722                for dim in dims {
723                    match ast.eval_integral(*dim) {
724                        Ok(length) => name.push_str(&format!("_{length}")),
725                        // A dimension that does not evaluate gets no
726                        // number rather than a zero: the name has to
727                        // differ from a `[0]` array's, and the
728                        // materialiser reports the failure by name.
729                        Err(_) => name.push_str("_x"),
730                    }
731                }
732                name
733            }
734            None => "void".to_string(),
735        },
736        TypeKind::Map { key, value, bound } => match (key.resolved(), value.resolved()) {
737            (Some(key), Some(value)) => format!(
738                "map{}_{}_{}",
739                bound_tag(ast, bound),
740                type_tag(ast, key, depth + 1),
741                type_tag(ast, value, depth + 1)
742            ),
743            _ => "void".to_string(),
744        },
745        // A typedef with no name is one the frontend made for an inline
746        // type; it is transparent, so the tag is the target's.
747        TypeKind::Alias { aliased } if node.name.is_empty() => match aliased.resolved() {
748            Some(target) => type_tag(ast, target, depth + 1),
749            None => "void".to_string(),
750        },
751        _ => flattened(ast, id),
752    }
753}
754
755/// The name a module or a type declared in IDL has in C++: the identifier,
756/// or `cxx_` in front of it where it is a C++ reserved word. DDS-RPC 1.0
757/// §7.11.1.1.2 rule 8: "Any identifier (e.g., parameter, operation name,
758/// exception, interface, module) in an IDL file that is a reserved word in
759/// C++ shall be prefixed by "cxx_"."
760#[must_use]
761pub fn cxx_name(name: &str) -> String {
762    if cpp::escape_keyword(name) == name {
763        name.to_string()
764    } else {
765        format!("cxx_{name}")
766    }
767}
768
769/// A named type's scoped name with the separators flattened onto it.
770///
771/// Not what a C++ *reference* to the type is written as — that is
772/// [`cpp_type_path`], which keeps the scoping — but what a generated
773/// function name is built from, where `::` is not an identifier
774/// character.
775#[must_use]
776pub fn flattened(ast: &Ast, id: Id<TypeNode>) -> String {
777    let Ok(any) = ast.any_of_type(id) else {
778        return "zd_unnamed".to_string();
779    };
780    match ast.scoped_name_of(any) {
781        Some(scoped) => scoped
782            .segments()
783            .iter()
784            .map(|segment| cxx_name(segment))
785            .collect::<Vec<_>>()
786            .join("_"),
787        None => "zd_unnamed".to_string(),
788    }
789}
790
791/// The rooted path a reference to a declared type is written as.
792///
793/// `::a::b::Foo` and never `Foo`: a member of a type in another module
794/// is written inside whichever namespace its own declaration sits in,
795/// and an unqualified name would resolve against that scope first. The
796/// segments are keyword-escaped, the same escape `module_chain` and
797/// `declare` apply, so the reference and the declaration spell the
798/// same name.
799#[must_use]
800pub fn cpp_type_path(ast: &Ast, id: Id<TypeNode>) -> Path {
801    let Ok(any) = ast.any_of_type(id) else {
802        return Path::single("zd_unnamed");
803    };
804    let Some(scoped) = ast.scoped_name_of(any) else {
805        return Path::single("zd_unnamed");
806    };
807    let escaped: Vec<String> = scoped
808        .segments()
809        .iter()
810        .map(|segment| cxx_name(segment))
811        .collect();
812    let mut borrowed: Vec<&str> = escaped.iter().map(String::as_str).collect();
813    if let Some(prefix) = native_prefix() {
814        borrowed.insert(0, prefix);
815    }
816    Path::rooted(&borrowed)
817}
818
819/// The C++ type a value of `id` is held in.
820///
821/// The one answer, for every emitter — a class's field, a codec's
822/// parameter, the element type a `std::vector` is instantiated at. The C
823/// column learned that the hard way: three private copies of this
824/// question in `encode_c`, `decode_c` and `default_c` let `long double`
825/// diverge, with a `const double *` pointing at a sixteen-byte member.
826///
827/// Fallible where the C column's `c_value_type` is total, because C++
828/// has nowhere to put a fudge. C writes an array's length into a *name*
829/// (`zd_arr_int32_t_x` for a dimension that does not evaluate) and
830/// reports the failure from the declaration site; here the length is in
831/// the type itself, `std::array<T, N>`, so there is no name to spell
832/// approximately and the refusal happens where the type is asked for.
833///
834/// `at` is where the type sits, as far as the model names it, and is
835/// carried only so a refusal can say so.
836///
837/// # Errors
838/// [`MaterialiseError::NoLanguageForm`] for `any`, `ValueBase` and an
839/// object reference, at whatever depth they appear — `sequence<any>` is
840/// refused as firmly as a bare `any` member.
841/// [`MaterialiseError::Unevaluated`] for an array length that does not
842/// evaluate, or is negative, and for a type nesting past
843/// `MAX_TYPE_DEPTH`.
844pub fn cpp_value_type(
845    ast: &Ast,
846    id: Id<TypeNode>,
847    at: &str,
848) -> Result<cpp::Type, MaterialiseError> {
849    value_type(ast, id, at, 0)
850}
851
852/// zerodds-lint: recursion-depth 64 (element types; bounded by
853/// `MAX_TYPE_DEPTH`)
854fn value_type(
855    ast: &Ast,
856    id: Id<TypeNode>,
857    at: &str,
858    depth: usize,
859) -> Result<cpp::Type, MaterialiseError> {
860    if depth > MAX_TYPE_DEPTH {
861        return Err(MaterialiseError::Unevaluated {
862            construct: format!("type spelling — it nests past {MAX_TYPE_DEPTH}"),
863            at: at.to_string(),
864        });
865    }
866    let Ok(node) = ast.type_(id) else {
867        return Ok(cpp::Type::Void);
868    };
869    let type_ = match &node.kind {
870        // Matched ahead of the arm below, which would reach
871        // `cpp_primitive`'s `double` — eight bytes where the member is
872        // sixteen. See [`FLOAT128`].
873        TypeKind::Primitive(PrimitiveKind::Float128) => float128(),
874        TypeKind::Primitive(kind) => cpp_primitive(*kind),
875        // Both string widths and `fixed<P,S>` are `std::string`. What
876        // separates them is the framing, which is the codec's, and
877        // [`cpp_type_tag`] is what keeps the codec's names apart.
878        TypeKind::String { .. } | TypeKind::Fixed { .. } => std_type("string", Vec::new()),
879        TypeKind::Sequence { element, .. } => std_type(
880            "vector",
881            vec![element_type(ast, element.resolved(), at, depth)?],
882        ),
883        TypeKind::Array { element, dims } => {
884            let mut out = element_type(ast, element.resolved(), at, depth)?;
885            // Innermost dimension first, so `long a[2][3]` becomes
886            // `std::array<std::array<std::int32_t, 3>, 2>` — the outer
887            // dimension outermost, which is the order the subscripts are
888            // written in and the order the elements lie in.
889            for dim in dims.iter().rev() {
890                let length =
891                    ast.eval_integral(*dim)
892                        .map_err(|_| MaterialiseError::Unevaluated {
893                            construct: "array length".to_string(),
894                            at: at.to_string(),
895                        })?;
896                if length < 0 {
897                    return Err(MaterialiseError::Unevaluated {
898                        construct: format!("array length — it evaluates to {length}"),
899                        at: at.to_string(),
900                    });
901                }
902                out = std_type("array", vec![out, length_arg(length)]);
903            }
904            out
905        }
906        // XTypes 1.3 §7.5.1.3.3: a class with the operations of Table 47. It is
907        // the vector of pairs the codec reads and writes, in the order the entries
908        // were inserted, so `zerodds::Map<K, V>` is a `std::vector<std::pair<K, V>>`
909        // too (runtime/cpp/include/zerodds/map.hpp). A bound is its third argument.
910        TypeKind::Map { key, value, bound } => {
911            let mut arguments = vec![
912                element_type(ast, key.resolved(), at, depth)?,
913                element_type(ast, value.resolved(), at, depth)?,
914            ];
915            if let Some(expr) = bound.expr() {
916                let limit = ast
917                    .eval_integral(expr)
918                    .map_err(|_| MaterialiseError::Unevaluated {
919                        construct: "map bound".to_string(),
920                        at: at.to_string(),
921                    })?;
922                arguments.push(length_arg(limit));
923            }
924            cpp::Type::template(Path::rooted(&["zerodds", "Map"]), arguments)
925        }
926        // A typedef with no name is one the frontend made for an inline
927        // type; it is transparent, so the C++ type is the target's.
928        TypeKind::Alias { aliased } if node.name.is_empty() => {
929            element_type(ast, aliased.resolved(), at, depth)?
930        }
931        // A boxed value is a nullable indirection. `std::shared_ptr` and
932        // not a raw pointer: the class stays copyable, which every other
933        // materialised type is, and nothing generated has to own or free
934        // it by hand.
935        TypeKind::ValueBox { boxed } => std_type(
936            "shared_ptr",
937            vec![element_type(ast, boxed.resolved(), at, depth)?],
938        ),
939        // `any` is in the model because Building Block Any is one of IDL
940        // 4.2's fifteen, and what it holds is decided at run time — there
941        // is no C++ type for it and no wire form to generate a codec
942        // against. An object reference is a handle into the RPC arc and
943        // `coverage.rs` says so. Both used to be reachable as a member
944        // type; emitting the IDL spelling into a header produced a file
945        // no compiler reads, from a generator that said nothing was
946        // wrong.
947        TypeKind::Any => return Err(no_form("any", at)),
948        TypeKind::ValueBase => return Err(no_form("ValueBase", at)),
949        TypeKind::ObjectRef { .. } => return Err(no_form("Object", at)),
950        TypeKind::Alias { .. }
951        | TypeKind::Structure { .. }
952        | TypeKind::Union { .. }
953        | TypeKind::Enumeration { .. }
954        | TypeKind::Bitmask { .. }
955        | TypeKind::Bitset { .. }
956        | TypeKind::Valuetype { .. }
957        | TypeKind::Native
958        | TypeKind::AnnotationType { .. } => cpp::Type::named(cpp_type_path(ast, id)),
959    };
960    Ok(type_)
961}
962
963/// The type of a resolved-or-not element, key, value or alias target.
964///
965/// zerodds-lint: recursion-depth 64 (collection nesting, via
966/// `value_type`; bounded by `MAX_TYPE_DEPTH`, which returns
967/// `MaterialiseError::Unevaluated`)
968fn element_type(
969    ast: &Ast,
970    target: Option<Id<TypeNode>>,
971    at: &str,
972    depth: usize,
973) -> Result<cpp::Type, MaterialiseError> {
974    match target {
975        Some(target) => value_type(ast, target, at, depth + 1),
976        // An unresolved reference is a frontend failure the model
977        // already reports, and it is reported again here rather than
978        // spelled `void`: a `std::vector<void>` would be a file no
979        // compiler reads.
980        None => Err(MaterialiseError::Unevaluated {
981            construct: "element type".to_string(),
982            at: at.to_string(),
983        }),
984    }
985}
986
987fn no_form(construct: &str, at: &str) -> MaterialiseError {
988    MaterialiseError::NoLanguageForm {
989        construct: construct.to_string(),
990        language: "C++",
991        at: at.to_string(),
992    }
993}
994
995/// The C++ type an IDL primitive becomes.
996///
997/// Exhaustive, so a primitive added to the model without a C++ spelling
998/// is a compile error and not a member that silently takes another's.
999///
1000/// The fixed-width integers are written **`std::`-qualified**, not as
1001/// [`cpp::Primitive::Int32`] and its neighbours, which render the bare
1002/// `int32_t`. `<cstdint>` declares those names in namespace `std`; the
1003/// standard leaves it unspecified whether it also declares them at
1004/// global scope, and a generated header that names the unqualified form
1005/// is relying on an implementation's habit. The `Primitive` variants
1006/// stay right for a hand-written tree; they are not right for output
1007/// that has to compile wherever it is sent.
1008///
1009/// `char` is one byte and `wchar` two, which is what the wire form is.
1010/// XTypes 1.3 §7.5.1.1.1 leaves the C++ spelling of `Char16` to the
1011/// binding, and a fixed-width integer is the only choice that does not
1012/// depend on the platform's `wchar_t` — which is 16 bits on Windows and
1013/// 32 on Linux.
1014///
1015/// `long double` is **not** routed here: it takes [`FLOAT128`]. The
1016/// `Float128` arm below exists so the match is total and answers for a
1017/// caller that wants the nearest built-in; `value_type` matches
1018/// `Float128` before it reaches this. A caller that does not is the
1019/// defect the three-column gate found in the C column.
1020#[must_use]
1021pub fn cpp_primitive(kind: PrimitiveKind) -> cpp::Type {
1022    match kind {
1023        PrimitiveKind::Boolean => cpp::Type::Primitive(cpp::Primitive::Bool),
1024        PrimitiveKind::Char => cpp::Type::Primitive(cpp::Primitive::Char),
1025        PrimitiveKind::Float32 => cpp::Type::Primitive(cpp::Primitive::Float),
1026        PrimitiveKind::Float64 | PrimitiveKind::Float128 => {
1027            cpp::Type::Primitive(cpp::Primitive::Double)
1028        }
1029        // XTypes 1.3 §7.5.1.1.2: only Boolean, Char8 and Byte "may all map
1030        // to the same underlying C++ type"; every other type of Table 43 is
1031        // to be told from every other by an overload. `uint8` is
1032        // `std::uint8_t`, so `octet`, the Byte, is the runtime's one-octet
1033        // class (`zerodds::wire::Octet`), which converts to and from it.
1034        PrimitiveKind::Octet => octet(),
1035        PrimitiveKind::UInt8 => std_type("uint8_t", Vec::new()),
1036        PrimitiveKind::Int8 => std_type("int8_t", Vec::new()),
1037        PrimitiveKind::WChar | PrimitiveKind::UInt16 => std_type("uint16_t", Vec::new()),
1038        PrimitiveKind::Int16 => std_type("int16_t", Vec::new()),
1039        PrimitiveKind::Int32 => std_type("int32_t", Vec::new()),
1040        PrimitiveKind::UInt32 => std_type("uint32_t", Vec::new()),
1041        PrimitiveKind::Int64 => std_type("int64_t", Vec::new()),
1042        PrimitiveKind::UInt64 => std_type("uint64_t", Vec::new()),
1043    }
1044}
1045
1046/// The unsigned integer a bit width calls for.
1047///
1048/// Public because the codec emitter has to cast into exactly this type:
1049/// a bitfield is unpacked out of a 64-bit anchor and assigned to a member
1050/// whose width is decided here, and a second copy of the rule would
1051/// narrow into a type this one had moved on from.
1052#[must_use]
1053pub fn cpp_holder(bits: u16) -> cpp::Type {
1054    match bits {
1055        0..=8 => std_type("uint8_t", Vec::new()),
1056        9..=16 => std_type("uint16_t", Vec::new()),
1057        17..=32 => std_type("uint32_t", Vec::new()),
1058        _ => std_type("uint64_t", Vec::new()),
1059    }
1060}
1061
1062/// `std::name<args…>`.
1063/// The documentation a `std::shared_ptr` member carries.
1064///
1065/// One paragraph, written once, because the struct member and the union
1066/// branch are the same declaration and the same reason.
1067fn external_doc() -> Doc {
1068    Doc::lines(&[
1069        "Held behind an indirection — @external, DDS-XTypes 1.3 Table 21. The",
1070        "annotation changes no octet on the wire; it says where the binding",
1071        "keeps the value. A `std::shared_ptr` and not the value itself,",
1072        "because a member that reaches its own enclosing class back has no",
1073        "size at all held inline — `std::optional<T>` and a plain `T` member",
1074        "both require T complete, and clang answers `field has incomplete",
1075        "type` and six `incomplete type ... used in type trait expression`",
1076        "for `tests/interop-corpus/recursive-optional-external`. A shared_ptr",
1077        "is complete whatever it points at.",
1078        "",
1079        "Where the member is also @optional the pointer is the absence too",
1080        "(7.2.2.4.4.4.7): a null pointer is the member not being there, and a",
1081        "std::optional around it would carry a present-but-null state the",
1082        "wire has no encoding for.",
1083    ])
1084}
1085
1086fn std_type(name: &str, args: Vec<cpp::Type>) -> cpp::Type {
1087    cpp::Type::template(Path::new(&["std", name]), args)
1088}
1089
1090fn float128() -> cpp::Type {
1091    cpp::Type::named(Path::rooted(&["zerodds", "wire", "Float128"]))
1092}
1093
1094/// `zerodds::wire::Octet`, the Byte (see [`cpp_primitive`]).
1095fn octet() -> cpp::Type {
1096    cpp::Type::named(Path::rooted(&["zerodds", "wire", "Octet"]))
1097}
1098
1099/// A non-type template argument, as this tree can carry one.
1100///
1101/// `cpp::Type::Named` holds template *types*; `std::array<T, 3>`'s second
1102/// argument is a value, and the tree has no variant for one. The length
1103/// goes through as a path segment spelled with its digits, which is what
1104/// the renderer writes and what the compiler reads. It is a borrowed slot
1105/// and it is named as one here rather than left for a reader to discover:
1106/// adding a `Type::Value(Expr)` variant is a change to
1107/// `zerodds-lang-cst` and belongs in that crate's own commit.
1108fn length_arg(length: i128) -> cpp::Type {
1109    cpp::Type::named(Path::single(&format!("{length}")))
1110}
1111
1112/// An integer literal that has a type wherever it is written.
1113///
1114/// A decimal literal with no suffix takes the first of `int`, `long` and
1115/// `long long` that holds it; a value above `i64::MAX` holds in none of
1116/// them, and clang answers with `-Wimplicitly-unsigned-literal`, which
1117/// under `-Werror` is a header that does not compile. An enumerator of a
1118/// `@bit_bound(64)` enumeration reaches that range, so the suffix is
1119/// written.
1120///
1121/// The other end has no suffix that helps it: −2^63 is not a literal in
1122/// C++ at all, and `zerodds_lang_render::cpp::integer_text` carries
1123/// both the clause and the form that spells it.
1124fn int_literal(value: i128) -> Literal {
1125    if value > i128::from(i64::MAX) {
1126        return Literal::Int(format!("{value}u"));
1127    }
1128    Literal::Int(zerodds_lang_render::cpp::integer_text(value))
1129}
1130
1131/// The default member initializer IDL4 to C++ 1.0 §7.2.4.3.1 asks of a
1132/// struct's default constructor: "initializes all built-in data types to
1133/// their default value [0, `false`], enumerators to their first value,
1134/// and the rest of members using their default constructor".
1135///
1136/// A built-in gets `{}` (value initialization, which is that zero); an
1137/// enumeration its first enumerator by name, because an `@value` can put
1138/// something other than 0 there; an array `{}` as well, so its built-in
1139/// elements are not left indeterminate either. Everything else — string,
1140/// sequence, map, struct, union — has a default constructor of its own
1141/// and gets no initializer. Without one, `Point p;` left every built-in
1142/// member indeterminate.
1143fn member_default(
1144    ast: &Ast,
1145    target: Id<TypeNode>,
1146    spelled: &cpp::Type,
1147    at: &str,
1148) -> Result<Option<cpp::Expr>, MaterialiseError> {
1149    // Through the typedefs to what they name.
1150    let mut id = target;
1151    for _ in 0..=MAX_TYPE_DEPTH {
1152        let Ok(node) = ast.type_(id) else {
1153            return Err(MaterialiseError::Unevaluated {
1154                construct: "member type".to_string(),
1155                at: at.to_string(),
1156            });
1157        };
1158        match &node.kind {
1159            TypeKind::Alias { aliased } => match aliased.resolved() {
1160                Some(next) => id = next,
1161                None => return Ok(None),
1162            },
1163            TypeKind::Primitive(_) | TypeKind::Array { .. } => {
1164                return Ok(Some(cpp::Expr::InitList(Vec::new())));
1165            }
1166            TypeKind::Enumeration { enumerators } => {
1167                let first = enumerators
1168                    .first()
1169                    .and_then(|first| ast.enumerator(*first).ok())
1170                    .ok_or_else(|| MaterialiseError::Unevaluated {
1171                        construct: "the first enumerator of the member's enumeration".to_string(),
1172                        at: at.to_string(),
1173                    })?;
1174                let cpp::Type::Named { path, .. } = spelled else {
1175                    return Err(MaterialiseError::Unevaluated {
1176                        construct: "the C++ name of the member's enumeration".to_string(),
1177                        at: at.to_string(),
1178                    });
1179                };
1180                return Ok(Some(cpp::Expr::Path(path.join(&cxx_name(&first.name)))));
1181            }
1182            _ => return Ok(None),
1183        }
1184    }
1185    Err(MaterialiseError::Unevaluated {
1186        construct: format!("typedef chain — it nests past {MAX_TYPE_DEPTH}"),
1187        at: at.to_string(),
1188    })
1189}
1190
1191/// A type the model declares under the name `<TypeName>Bits` of a
1192/// bitmask's flag enumeration, in the namespace of the bitmask.
1193///
1194/// DDS-XTypes 1.3 §7.5.1.2.2 names that enumeration as part of the
1195/// binding. Two declarations of one name in one namespace are a
1196/// redefinition the C++ compiler reports; the refusal names both.
1197///
1198/// # Errors
1199/// [`MaterialiseError::SpellingCollision`] when a declaration is named
1200/// like the flag enumeration of a bitmask of the same module.
1201fn no_bits_name_collision(ast: &Ast) -> Result<(), MaterialiseError> {
1202    let mut spelled: Vec<(String, String)> = Vec::new();
1203    for id in ordered_types(ast) {
1204        let Ok(node) = ast.type_(id) else { continue };
1205        if node.name.is_empty() || !has_idl_name(&node.kind) {
1206            continue;
1207        }
1208        let scope = format!("{}@{}", node.module.slot(), node.module.generation());
1209        spelled.push((
1210            format!("{scope}::{}", cxx_name(&node.name)),
1211            node.name.clone(),
1212        ));
1213        if matches!(node.kind, TypeKind::Bitmask { .. }) {
1214            spelled.push((
1215                format!("{scope}::{}Bits", cxx_name(&node.name)),
1216                format!("{}Bits (the flag type of bitmask {})", node.name, node.name),
1217            ));
1218        }
1219    }
1220    no_spelling_collision(
1221        spelled
1222            .iter()
1223            .map(|(spelled, declared)| (spelled.as_str(), declared.as_str())),
1224        "C++",
1225        "the types of one header",
1226    )
1227}
1228
1229/// How the native C++ mapping holds one member.
1230#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1231pub enum Held {
1232    /// The value itself.
1233    Value,
1234    /// A `std::shared_ptr<T>`: an `@external` member, or a member that
1235    /// closes a cycle and is neither `@optional` nor annotated.
1236    Shared,
1237    /// A plain `T *`, NULL when absent: an `@optional` member. DDS-XTypes
1238    /// 1.3 §7.5.1.2.4.2: "Optional members shall be represented using
1239    /// plain pointers rather than automatic values or smart pointers."
1240    Plain,
1241}
1242
1243/// [`Held`] for `member`.
1244///
1245/// `@external` comes first: "A member that is both external and optional
1246/// shall be mapped as if it was just external" (§7.5.1.2.3.2.1). Then
1247/// `@optional`, whether or not it closes a cycle, and then a member that
1248/// only closes one.
1249pub fn held(ast: &Ast, member: &zerodds_ast::MemberNode) -> Held {
1250    if super::annotated_external(member) {
1251        Held::Shared
1252    } else if member.facets.is_optional() {
1253        Held::Plain
1254    } else if super::held_externally(ast, member) {
1255        Held::Shared
1256    } else {
1257        Held::Value
1258    }
1259}
1260
1261/// The member functions a class needs when it owns what a pointer member
1262/// points at.
1263///
1264/// DDS-XTypes 1.3 §7.5.1.2.3.2: "The behavior of the constructor,
1265/// destructor, and copy functions shall be the same as specified for C"
1266/// — NULL after construction, the object deleted by the destructor, a deep
1267/// copy — and the same holds for the plain pointer of an `@optional`
1268/// member. `values` are the C++ names of the members that are copied as
1269/// they are and `pointers` those that are held behind a pointer; an empty
1270/// `pointers` means the implicit functions are right and nothing is
1271/// written.
1272///
1273/// Every pointer is handled by a function template of the runtime
1274/// (`clone`, `destroy`, `take`), whose instantiation is at the end of the
1275/// translation unit: a pointee declared after the class that holds it is
1276/// complete by then. The members start from their default member
1277/// initialisers, so the bodies assign.
1278fn owning_members(
1279    class: &str,
1280    base: Option<&str>,
1281    values: &[String],
1282    pointers: &[String],
1283) -> Vec<cpp::ClassMember> {
1284    if pointers.is_empty() {
1285        return Vec::new();
1286    }
1287    let wire = |function: &str| cpp::Expr::Path(Path::new(&["zerodds", "wire", function]));
1288    let call = |callee: cpp::Expr, args: Vec<cpp::Expr>| cpp::Expr::Call {
1289        callee: Box::new(callee),
1290        args,
1291    };
1292    let assign = |place: cpp::Expr, value: cpp::Expr| {
1293        cpp::Statement::Expr(cpp::Expr::Binary {
1294            op: "=",
1295            lhs: Box::new(place),
1296            rhs: Box::new(value),
1297        })
1298    };
1299    let mine = |name: &str| cpp::Expr::Path(Path::single(name));
1300    let theirs = |name: &str| cpp::Expr::Member {
1301        value: Box::new(cpp::Expr::Path(Path::single("zd_other"))),
1302        name: Ident::new(name),
1303    };
1304    let moved = |value: cpp::Expr| call(cpp::Expr::Path(Path::new(&["std", "move"])), vec![value]);
1305    let own_type = || cpp::Type::named(Path::single(class));
1306    let param = |type_: cpp::Type| cpp::Param {
1307        type_,
1308        name: Some(Ident::new("zd_other")),
1309        default: None,
1310    };
1311    let const_ref = || cpp::Type::Ref(Box::new(cpp::Type::Const(Box::new(own_type()))));
1312    let rvalue = || cpp::Type::RvalueRef(Box::new(own_type()));
1313    let not_self = |body: Vec<cpp::Statement>| cpp::Statement::If {
1314        condition: cpp::Expr::Binary {
1315            op: "!=",
1316            lhs: Box::new(cpp::Expr::Path(Path::single("this"))),
1317            rhs: Box::new(cpp::Expr::AddressOf(Box::new(mine("zd_other")))),
1318        },
1319        then: cpp::Block { statements: body },
1320        otherwise: None,
1321    };
1322    let give_self = || {
1323        cpp::Statement::Return(Some(cpp::Expr::Deref(Box::new(cpp::Expr::Path(
1324            Path::single("this"),
1325        )))))
1326    };
1327    let constructor = |doc: &str,
1328                       params: Vec<cpp::Param>,
1329                       initialisers: Vec<(Ident, cpp::Expr)>,
1330                       body: cpp::FunctionBody| {
1331        cpp::ClassMember::Constructor(cpp::Constructor {
1332            doc: Doc::line(doc),
1333            name: Ident::new(class),
1334            params,
1335            initialisers,
1336            is_explicit: false,
1337            is_constexpr: false,
1338            is_noexcept: false,
1339            body,
1340        })
1341    };
1342    let operator = |doc: &str, param_type: cpp::Type, body: Vec<cpp::Statement>| {
1343        cpp::ClassMember::Method(cpp::Method {
1344            doc: Doc::line(doc),
1345            returns: cpp::Type::Ref(Box::new(own_type())),
1346            name: Ident::new("operator="),
1347            params: vec![param(param_type)],
1348            is_const: false,
1349            is_static: false,
1350            is_virtual: false,
1351            is_pure: false,
1352            is_constexpr: false,
1353            is_noexcept: false,
1354            is_override: false,
1355            body: Some(cpp::Block { statements: body }),
1356        })
1357    };
1358
1359    let mut copy = Vec::new();
1360    let mut copy_assign = Vec::new();
1361    let mut move_ctor = Vec::new();
1362    let mut move_assign = Vec::new();
1363    let mut release = Vec::new();
1364    // The functions written here replace the implicit ones, which copy and move the base class
1365    // too (XTypes 1.3 §7.5.1.4.1.1): a derived class passes `zd_other` on to its base.
1366    let (base_copy, base_move) = base.map_or((Vec::new(), Vec::new()), |base| {
1367        (
1368            vec![(Ident::new(base), mine("zd_other"))],
1369            vec![(Ident::new(base), moved(mine("zd_other")))],
1370        )
1371    });
1372    if let Some(base) = base {
1373        let assign_base = |value: cpp::Expr| {
1374            cpp::Statement::Expr(call(
1375                cpp::Expr::Path(Path::single(&format!("{base}::operator="))),
1376                vec![value],
1377            ))
1378        };
1379        copy_assign.push(assign_base(mine("zd_other")));
1380        move_assign.push(assign_base(moved(mine("zd_other"))));
1381    }
1382    for name in values {
1383        copy.push(assign(mine(name), theirs(name)));
1384        copy_assign.push(assign(mine(name), theirs(name)));
1385        move_ctor.push(assign(mine(name), moved(theirs(name))));
1386        move_assign.push(assign(mine(name), moved(theirs(name))));
1387    }
1388    for name in pointers {
1389        let cloned = call(wire("clone"), vec![theirs(name)]);
1390        let taken = call(wire("take"), vec![theirs(name)]);
1391        let destroy = || cpp::Statement::Expr(call(wire("destroy"), vec![mine(name)]));
1392        copy.push(assign(mine(name), cloned.clone()));
1393        // The copy is made before the old object goes, so a self-contained
1394        // graph survives its own replacement.
1395        copy_assign.push(cpp::Statement::Declare {
1396            type_: cpp::Type::Auto,
1397            name: Ident::new(&format!("zd_new_{name}")),
1398            value: Some(cloned),
1399        });
1400        copy_assign.push(destroy());
1401        copy_assign.push(assign(mine(name), mine(&format!("zd_new_{name}"))));
1402        move_ctor.push(assign(mine(name), taken.clone()));
1403        move_assign.push(destroy());
1404        move_assign.push(assign(mine(name), taken));
1405        release.push(destroy());
1406    }
1407    // The copy is made first and every pointer is assigned after the values,
1408    // so a copy that throws leaves the destination as it was.
1409    copy_assign.sort_by_key(|statement| !matches!(statement, cpp::Statement::Declare { .. }));
1410    vec![
1411        constructor(
1412            "NULL pointers (XTypes 1.3 7.5.1.2.3.2, as in C).",
1413            Vec::new(),
1414            Vec::new(),
1415            cpp::FunctionBody::Defaulted,
1416        ),
1417        constructor(
1418            "A deep copy of what the pointer members point at.",
1419            vec![param(const_ref())],
1420            base_copy,
1421            cpp::FunctionBody::Block(cpp::Block { statements: copy }),
1422        ),
1423        constructor(
1424            "Takes the pointers over and leaves NULL behind.",
1425            vec![param(rvalue())],
1426            base_move,
1427            cpp::FunctionBody::Block(cpp::Block {
1428                statements: move_ctor,
1429            }),
1430        ),
1431        operator("A deep copy.", const_ref(), {
1432            let mut body = vec![not_self(copy_assign)];
1433            body.push(give_self());
1434            body
1435        }),
1436        operator("Takes the pointers over.", rvalue(), {
1437            let mut body = vec![not_self(move_assign)];
1438            body.push(give_self());
1439            body
1440        }),
1441        cpp::ClassMember::Destructor(cpp::Destructor {
1442            doc: Doc::line("Deletes what the non-NULL pointer members point at."),
1443            name: Ident::new(class),
1444            is_virtual: false,
1445            body: cpp::FunctionBody::Block(cpp::Block {
1446                statements: release,
1447            }),
1448        }),
1449    ]
1450}
1451
1452/// One data member.
1453fn field(type_: cpp::Type, name: &str, doc: Doc) -> cpp::ClassMember {
1454    cpp::ClassMember::Field(cpp::Field {
1455        doc,
1456        type_,
1457        name: Ident::new(&cxx_name(name)),
1458        dims: Vec::new(),
1459        default: None,
1460        is_static: false,
1461        is_constexpr: false,
1462        bits: None,
1463    })
1464}
1465
1466/// The storage of a union's discriminator, and the prefix of a case's.
1467///
1468/// The accessor functions `_d()` and `x()` of IDL4 to C++ 1.0 take the
1469/// names an IDL union gives them, so the storage behind them has others;
1470/// the generated codec reads and writes the storage directly.
1471pub(crate) const UNION_DISCRIMINATOR: &str = "zd_d";
1472
1473/// The storage of the case `field` (see [`UNION_DISCRIMINATOR`]).
1474pub(crate) fn union_storage(field: &str) -> String {
1475    format!("zd_{field}")
1476}
1477
1478/// One case of a union class: its spelled name, its type and the text of
1479/// the discriminator value its setter selects (`None` for a case that has
1480/// no value of its own, such as the `default` one).
1481pub(crate) struct UnionCase {
1482    /// The spelling of the case member.
1483    pub(crate) name: String,
1484    /// The C++ type of the case.
1485    pub(crate) type_: cpp::Type,
1486    /// The discriminator value the setter of the case selects, as C++ text.
1487    pub(crate) label: Option<String>,
1488    /// What the storage of the case says about itself.
1489    pub(crate) doc: Doc,
1490}
1491
1492/// The members of a union class: the accessor functions of IDL4 to C++ 1.0
1493/// (`_d()` and `_d(value)`; per case `const T& x() const`, `T& x()`,
1494/// `void x(const T&)` and `void x(T&&)`, a setter selecting the case), then
1495/// the storage they stand on. Which storage is live is what the
1496/// discriminator says.
1497pub(crate) fn union_members(
1498    discriminator: &cpp::Type,
1499    cases: &[UnionCase],
1500) -> Vec<cpp::ClassMember> {
1501    let disc = zerodds_lang_render::cpp::type_(discriminator);
1502    let mut text = format!(
1503        "    /// The discriminator.\n    {disc} _d() const {{ return {UNION_DISCRIMINATOR}; }}\n    void _d({disc} value) {{ {UNION_DISCRIMINATOR} = value; }}\n"
1504    );
1505    for case in cases {
1506        let ty = zerodds_lang_render::cpp::type_(&case.type_);
1507        let storage = union_storage(&case.name);
1508        let select = case.label.as_ref().map_or(String::new(), |label| {
1509            format!(" {UNION_DISCRIMINATOR} = static_cast<{disc}>({label});")
1510        });
1511        text.push_str(&format!(
1512            "    const {ty}& {name}() const {{ return {storage}; }}\n    {ty}& {name}() {{ return {storage}; }}\n    void {name}(const {ty}& value) {{ {storage} = value;{select} }}\n    void {name}({ty}&& value) {{ {storage} = std::move(value);{select} }}\n",
1513            name = case.name
1514        ));
1515    }
1516    let mut members = vec![cpp::ClassMember::Verbatim(text)];
1517    members.push(field(
1518        discriminator.clone(),
1519        UNION_DISCRIMINATOR,
1520        Doc::line("The storage of `_d()`. Which storage below is live is what it says."),
1521    ));
1522    for case in cases {
1523        members.push(cpp::ClassMember::Field(cpp::Field {
1524            doc: case.doc.clone(),
1525            type_: case.type_.clone(),
1526            name: Ident::new(&union_storage(&case.name)),
1527            dims: Vec::new(),
1528            default: None,
1529            is_static: false,
1530            is_constexpr: false,
1531            bits: None,
1532        }));
1533    }
1534    members
1535}
1536
1537/// The parameter name of the generated `swap`, which no IDL member is
1538/// likely to be spelled: a member of the same name would be shadowed.
1539const SWAP_PARAMETER: &str = "zd_other";
1540
1541/// `void swap(T & zd_other) { std::swap(a, zd_other.a); … }`
1542fn swap_member(name: &str, names: &[(String, String)]) -> cpp::ClassMember {
1543    let statements: Vec<_> = names
1544        .iter()
1545        .map(|(spelled, _)| {
1546            cpp::Statement::Expr(cpp::Expr::Call {
1547                callee: Box::new(cpp::Expr::Path(Path::new(&["std", "swap"]))),
1548                args: vec![
1549                    cpp::Expr::Path(Path::single(spelled)),
1550                    cpp::Expr::Member {
1551                        value: Box::new(cpp::Expr::Path(Path::single(SWAP_PARAMETER))),
1552                        name: Ident::new(spelled),
1553                    },
1554                ],
1555            })
1556        })
1557        .collect();
1558    cpp::ClassMember::Method(cpp::Method {
1559        doc: Doc::line("Exchanges every member with `zd_other` (DDS-PSM-Cxx 1.0 7.12)."),
1560        returns: cpp::Type::Void,
1561        name: Ident::new("swap"),
1562        params: vec![cpp::Param {
1563            type_: cpp::Type::Ref(Box::new(cpp::Type::named(Path::single(name)))),
1564            // A struct with no member leaves the parameter unused (-Wunused-parameter).
1565            name: (!statements.is_empty()).then(|| Ident::new(SWAP_PARAMETER)),
1566            default: None,
1567        }],
1568        is_const: false,
1569        is_static: false,
1570        is_virtual: false,
1571        is_pure: false,
1572        is_constexpr: false,
1573        is_noexcept: false,
1574        is_override: false,
1575        body: Some(cpp::Block { statements }),
1576    })
1577}
1578
1579/// `operator==` and `operator!=` of a bitset, field by field. Under
1580/// `--cpp-mapping psm` a bitset is this native type (`cpp_psm`), and the
1581/// equality of a PSM class compares every member with `==`: without them a
1582/// structure with a bitset member does not compile.
1583fn bitset_equality(name: &str, names: &[(String, String)]) -> cpp::ClassMember {
1584    // A bitset that is all padding compares equal and leaves the parameter
1585    // unnamed (-Wunused-parameter).
1586    let (parameter, terms) = if names.is_empty() {
1587        (String::new(), "true".to_string())
1588    } else {
1589        let terms: Vec<String> = names
1590            .iter()
1591            .map(|(spelled, _)| format!("{spelled} == {SWAP_PARAMETER}.{spelled}"))
1592            .collect();
1593        (format!(" {SWAP_PARAMETER}"), terms.join(" && "))
1594    };
1595    cpp::ClassMember::Verbatim(format!(
1596        "    bool operator==(const {name}&{parameter}) const {{ return {terms}; }}\n    \
1597         bool operator!=(const {name}& {SWAP_PARAMETER}) const {{ return !(*this == {SWAP_PARAMETER}); }}\n"
1598    ))
1599}
1600
1601/// `inline void swap(T & a, T & b) { a.swap(b); }`, in the type's namespace.
1602fn swap_function(name: &str) -> cpp::Decl {
1603    let reference = || cpp::Type::Ref(Box::new(cpp::Type::named(Path::single(name))));
1604    cpp::Decl::Function(cpp::Function {
1605        doc: Doc::line("Exchanges two values (DDS-PSM-Cxx 1.0 7.12)."),
1606        returns: cpp::Type::Void,
1607        name: Ident::new("swap"),
1608        params: vec![
1609            cpp::Param {
1610                type_: reference(),
1611                name: Some(Ident::new("zd_a")),
1612                default: None,
1613            },
1614            cpp::Param {
1615                type_: reference(),
1616                name: Some(Ident::new("zd_b")),
1617                default: None,
1618            },
1619        ],
1620        is_inline: true,
1621        is_static: false,
1622        is_constexpr: false,
1623        is_noexcept: false,
1624        is_maybe_unused: false,
1625        body: cpp::FunctionBody::Block(cpp::Block {
1626            statements: vec![cpp::Statement::Expr(cpp::Expr::MethodCall {
1627                receiver: Box::new(cpp::Expr::Path(Path::single("zd_a"))),
1628                name: Ident::new("swap"),
1629                args: vec![cpp::Expr::Path(Path::single("zd_b"))],
1630            })],
1631        }),
1632    })
1633}
1634
1635/// The suffix of the variable carrying a declaration's IDL member names.
1636///
1637/// Appended to the declaration's own C++ name, inside the declaration's
1638/// own namespace — so `a::b::Sample` is answered for by
1639/// `a::b::Sample_idl_names` and two declarations cannot reach one
1640/// variable unless their names already collided. A name this does
1641/// collide with is a redefinition the compiler reports, never a mapping
1642/// one of the two lost.
1643const IDL_NAMES_SUFFIX: &str = "_idl_names";
1644
1645/// The suffix of the variable carrying a declaration's own IDL name.
1646///
1647/// A reserved word moves a type too — `struct class` becomes `cxx_class`
1648/// (DDS-RPC 1.0 §7.11.1.1.2 rule 8) — and a type's name reaches the wire
1649/// twice over: as `TypeObject`'s `type_name` and as the topic type name a
1650/// peer matches on. The variable is named after the C++ spelling, so
1651/// `struct class` is `cxx_class_idl_name`. The C++ spelling cannot be
1652/// inverted back to the IDL name, because an IDL type that is already
1653/// called `cxx_class` arrives at the same identifier.
1654const IDL_NAME_SUFFIX: &str = "_idl_name";
1655
1656/// `const char *`, the spelling both halves of an entry take.
1657fn text_pointer() -> cpp::Type {
1658    cpp::Type::Pointer(Box::new(cpp::Type::Const(Box::new(cpp::Type::Primitive(
1659        cpp::Primitive::Char,
1660    )))))
1661}
1662
1663/// The `<name>_idl_names` table for a declaration's named parts.
1664///
1665/// `pairs` is `(the C++ spelling, the IDL spelling)` in IDL declaration
1666/// order — the order the member IDs are assigned in (XTypes 1.3
1667/// §7.3.1.2.1.1) — and an entry is written even where the two spellings
1668/// are the same, because a caller computing a member ID needs every
1669/// member's IDL name and not only the ones a keyword moved.
1670///
1671/// [`None`] for a declaration with no named parts. `std::array<T, 0>` is
1672/// a legal type and an empty table answers nothing, and the two
1673/// declarations that reach it — an enumeration with no enumerators, a
1674/// bitset that is all padding — have no member to be asked about.
1675/// The table only exists where the escape is invertible.
1676///
1677/// Two members whose IDL names differ and whose spellings do not are
1678/// two entries with the same left half — the table would record the
1679/// collision rather than refuse it, and a reader recovering the IDL
1680/// name would get whichever entry it hit first. The check is here
1681/// because this is the function that holds both spellings for a whole
1682/// scope; nothing upstream sees more than one member at a time.
1683///
1684/// # Errors
1685/// [`MaterialiseError::SpellingCollision`] when two members reach one
1686/// spelling.
1687fn idl_names(
1688    name: &str,
1689    pairs: &[(String, String)],
1690) -> Result<Option<cpp::Decl>, MaterialiseError> {
1691    no_spelling_collision(
1692        pairs
1693            .iter()
1694            .map(|(spelled, declared)| (spelled.as_str(), declared.as_str())),
1695        "C++",
1696        name,
1697    )?;
1698    if pairs.is_empty() {
1699        return Ok(None);
1700    }
1701    let entries: Vec<cpp::Expr> = pairs
1702        .iter()
1703        .map(|(spelling, idl)| {
1704            cpp::Expr::InitList(vec![
1705                cpp::Expr::Literal(Literal::Str(spelling.clone())),
1706                cpp::Expr::Literal(Literal::Str(idl.clone())),
1707            ])
1708        })
1709        .collect();
1710    Ok(Some(cpp::Decl::Variable(cpp::Variable {
1711        doc: Doc::lines(&[
1712            "The IDL spelling of each member, beside the spelling this header",
1713            "uses, in IDL declaration order.",
1714            "",
1715            "Load-bearing, not documentation. A member's ID under @autoid(HASH)",
1716            "and under @hashid is the first four octets of the MD5 of its IDL",
1717            "name (XTypes 1.3 7.3.1.2.1.1), so a caller holding only the C++",
1718            "spelling — which a keyword collision moved — cannot compute the ID",
1719            "of the member it is writing.",
1720        ]),
1721        type_: std_type(
1722            "array",
1723            vec![
1724                std_type("pair", vec![text_pointer(), text_pointer()]),
1725                // The count's digits, straight from the entry list.
1726                // [`length_arg`] is the same borrowed slot for a length
1727                // the model computed; this one is the length of a `Vec`
1728                // and reaches the output without a conversion that
1729                // could answer a different number than it holds.
1730                cpp::Type::named(Path::single(&pairs.len().to_string())),
1731            ],
1732        ),
1733        name: Ident::new(&format!("{name}{IDL_NAMES_SUFFIX}")),
1734        // The outer brace is `std::array`'s own aggregate and the inner
1735        // one is the C array inside it. Written out rather than left to
1736        // brace elision, which clang reports as `-Wmissing-braces` and
1737        // `-Werror` then refuses.
1738        value: Some(cpp::Expr::InitList(vec![cpp::Expr::InitList(entries)])),
1739        is_static: false,
1740        is_constexpr: true,
1741        is_inline: true,
1742    })))
1743}
1744
1745/// The `<name>_idl_name` variable for a declaration's own IDL name.
1746fn idl_name(name: &str, idl: &str) -> cpp::Decl {
1747    cpp::Decl::Variable(cpp::Variable {
1748        doc: Doc::line("The IDL spelling of the declaration above."),
1749        type_: text_pointer(),
1750        name: Ident::new(&format!("{name}{IDL_NAME_SUFFIX}")),
1751        value: Some(cpp::Expr::Literal(Literal::Str(idl.to_string()))),
1752        is_static: false,
1753        is_constexpr: true,
1754        is_inline: true,
1755    })
1756}
1757
1758/// Whether a type's C++ name was made from an IDL identifier.
1759///
1760/// The collections are the reason this is asked: `std::vector<T>` is
1761/// written where it is used and names nothing, and the kinds below all
1762/// take their C++ name from [`cxx_name`] applied to an
1763/// identifier the source wrote.
1764const fn has_idl_name(kind: &TypeKind) -> bool {
1765    matches!(
1766        kind,
1767        TypeKind::Structure { .. }
1768            | TypeKind::Valuetype { .. }
1769            | TypeKind::Union { .. }
1770            | TypeKind::Enumeration { .. }
1771            | TypeKind::Bitmask { .. }
1772            | TypeKind::Bitset { .. }
1773            | TypeKind::Alias { .. }
1774            | TypeKind::Native
1775            | TypeKind::ValueBox { .. }
1776    )
1777}
1778
1779/// `struct Name { public: … };`
1780///
1781/// A `struct` and not a `class`: every member of a generated data type is
1782/// public, and a `class` would need the access label to say so. The
1783/// section is written all the same — the tree groups by access and the
1784/// renderer writes the label once — and is left out entirely when there
1785/// is nothing in it, so an empty IDL structure produces `struct S {\n};`
1786/// and not a stray `public:`.
1787fn class(name: &str, members: Vec<cpp::ClassMember>, doc: Doc) -> cpp::Class {
1788    let sections = if members.is_empty() {
1789        Vec::new()
1790    } else {
1791        vec![cpp::Section {
1792            access: cpp::Access::Public,
1793            members,
1794        }]
1795    };
1796    cpp::Class {
1797        doc,
1798        is_struct: true,
1799        name: Ident::new(name),
1800        bases: Vec::new(),
1801        sections,
1802    }
1803}
1804
1805/// The three maps of a type. DDS-XTypes 1.3 §7.5.1.3.2.1: "For any type T,
1806/// the declaration and implementation of the map types having element type
1807/// T and key types UInt32, UInt64, and String shall be generated alongside
1808/// the implementation code for element type T", and §7.5.1.3.3.1 places the
1809/// class "in the same namespace as the element type declaration":
1810/// `UInt32MyElementTypeMap`, `UInt64MyElementTypeMap`,
1811/// `StringMyElementTypeMap`, each a `zerodds::Map`.
1812///
1813/// Under [`CppMapping::Psm`] a map is a `std::map` (§7.5.1.3.4) and the
1814/// classes are not written.
1815fn maps_alongside(
1816    ast: &Ast,
1817    id: Id<TypeNode>,
1818    out: &mut Vec<(Id<ModuleNode>, cpp::Decl)>,
1819    emitted: &[String],
1820) {
1821    if cpp_mapping() == CppMapping::Psm {
1822        return;
1823    }
1824    let Ok(node) = ast.type_(id) else { return };
1825    let declared = match &node.kind {
1826        TypeKind::Structure { .. }
1827        | TypeKind::Union { .. }
1828        | TypeKind::Enumeration { .. }
1829        | TypeKind::Bitmask { .. }
1830        | TypeKind::Bitset { .. } => true,
1831        TypeKind::Alias { .. } => !node.name.is_empty(),
1832        _ => false,
1833    };
1834    if !declared {
1835        return;
1836    }
1837    let key = cpp_type_path(ast, id)
1838        .segments()
1839        .iter()
1840        .map(Ident::as_str)
1841        .collect::<Vec<_>>()
1842        .join("::");
1843    if !emitted.contains(&key) {
1844        return;
1845    }
1846    let name = cxx_name(&node.name);
1847    for (key_name, key_type) in [
1848        ("UInt32", std_type("uint32_t", Vec::new())),
1849        ("UInt64", std_type("uint64_t", Vec::new())),
1850        ("String", std_type("string", Vec::new())),
1851    ] {
1852        out.push((
1853            node.module,
1854            cpp::Decl::Alias(cpp::Alias {
1855                doc: Doc::none(),
1856                name: Ident::new(&format!("{key_name}{name}Map")),
1857                type_: cpp::Type::template(
1858                    Path::rooted(&["zerodds", "Map"]),
1859                    vec![key_type, cpp::Type::named(cpp_type_path(ast, id))],
1860                ),
1861            }),
1862        ));
1863    }
1864}
1865
1866#[allow(clippy::too_many_lines)]
1867fn declare(
1868    ast: &Ast,
1869    applied: &Applied,
1870    id: Id<TypeNode>,
1871    out: &mut Vec<(Id<ModuleNode>, cpp::Decl)>,
1872    emitted: &mut Vec<String>,
1873) -> Result<(), MaterialiseError> {
1874    let Ok(node) = ast.type_(id) else {
1875        return Ok(());
1876    };
1877
1878    match &node.kind {
1879        // C++ writes these where they are used, so none of them declares
1880        // anything. This is where the C column's `zd_seq_`, `zd_arr_`,
1881        // `zd_map_`, `zd_string` and `zd_fixed_` declarations were, and
1882        // with them the collision hazard a structural name carries.
1883        TypeKind::Primitive(_)
1884        | TypeKind::String { .. }
1885        | TypeKind::Fixed { .. }
1886        | TypeKind::Sequence { .. }
1887        | TypeKind::Array { .. }
1888        | TypeKind::Map { .. } => return Ok(()),
1889        // An object reference belongs to the RPC arc and an annotation
1890        // type has no runtime form at all — `zerodds_ast_layout` refuses
1891        // to plan either — so a C++ declaration would be a shape nothing
1892        // could serialise. `any` and `ValueBase` for the same reason and
1893        // a different one: their content is decided at run time. Named
1894        // here rather than passed over, so the coverage table and this
1895        // file say the same thing. Every one of them is refused by name
1896        // where it is *used*, in `value_type`.
1897        TypeKind::ObjectRef { .. }
1898        | TypeKind::AnnotationType { .. }
1899        | TypeKind::Any
1900        | TypeKind::ValueBase => return Ok(()),
1901        _ => {}
1902    }
1903
1904    let name = cxx_name(&node.name);
1905    let key = cpp_type_path(ast, id)
1906        .segments()
1907        .iter()
1908        .map(Ident::as_str)
1909        .collect::<Vec<_>>()
1910        .join("::");
1911    if emitted.contains(&key) {
1912        return Ok(());
1913    }
1914
1915    match &node.kind {
1916        TypeKind::Alias { aliased } => {
1917            if node.name.is_empty() {
1918                return Ok(());
1919            }
1920            if ghost_created_for(applied, node.node_id, "CppAlias").is_none() {
1921                return Ok(());
1922            }
1923            let Some(target) = aliased.resolved() else {
1924                return Err(MaterialiseError::Unevaluated {
1925                    construct: "typedef target".to_string(),
1926                    at: name,
1927                });
1928            };
1929            emitted.push(key);
1930            out.push((
1931                node.module,
1932                cpp::Decl::Alias(cpp::Alias {
1933                    doc: Doc::none(),
1934                    name: Ident::new(&name),
1935                    type_: cpp_value_type(ast, target, &name)?,
1936                }),
1937            ));
1938        }
1939
1940        TypeKind::Enumeration { enumerators } => {
1941            if ghost_created_for(applied, node.node_id, "CppEnum").is_none() {
1942                return Ok(());
1943            }
1944            let mut values = Vec::new();
1945            let mut names = Vec::new();
1946            for enumerator in enumerators {
1947                let Ok(enum_node) = ast.enumerator(*enumerator) else {
1948                    return Err(MaterialiseError::Unevaluated {
1949                        construct: "enumerator the model listed".to_string(),
1950                        at: node.name.clone(),
1951                    });
1952                };
1953                if ghost_created_for(applied, enum_node.node_id, "CppEnumerator").is_none() {
1954                    continue;
1955                }
1956                let value = ast.effective_enumerator_value(*enumerator).ok_or_else(|| {
1957                    MaterialiseError::Unevaluated {
1958                        construct: "enumerator value".to_string(),
1959                        at: enum_node.name.clone(),
1960                    }
1961                })?;
1962                values.push((cxx_name(&enum_node.name), value));
1963                names.push((cxx_name(&enum_node.name), enum_node.name.clone()));
1964            }
1965            emitted.push(key);
1966            out.push((
1967                node.module,
1968                cpp::Decl::Enum(cpp::Enum {
1969                    doc: Doc::lines(&[
1970                        "An enumeration. The underlying type is chosen from the",
1971                        "@bit_bound and from the values themselves — a C enumeration",
1972                        "constant is an `int` and could hold neither end of",
1973                        "@bit_bound(32), which is why the C column emits a typedef and",
1974                        "a run of #defines instead.",
1975                        "",
1976                        "Scoped, so the enumerators do not leak into the namespace. IDL",
1977                        "4.2 7.4.1.4.2 names them from the scope enclosing the",
1978                        "enumeration — `M::medium`, never `M::Size::medium` — and this",
1979                        "is the one place the C++ mapping does not reproduce the IDL",
1980                        "spelling: an unscoped `enum` would put every enumerator of",
1981                        "every enumeration of a module in one namespace, where two",
1982                        "IDL-legal names can collide.",
1983                    ]),
1984                    is_scoped: true,
1985                    name: Ident::new(&name),
1986                    underlying: Some(enum_underlying(ast.effective_bit_bound(id), &values)),
1987                    enumerators: values
1988                        .into_iter()
1989                        .map(|(name, value)| cpp::Enumerator {
1990                            doc: Doc::none(),
1991                            name: Ident::new(&name),
1992                            // Written out, never left to position: a C++
1993                            // enumeration renumbers silently when an
1994                            // enumerator is inserted, and the wire format
1995                            // is the IDL's numbering.
1996                            value: Some(cpp::Expr::Literal(int_literal(value))),
1997                        })
1998                        .collect(),
1999                }),
2000            ));
2001            out.extend(
2002                idl_names(&name, &names)?
2003                    .into_iter()
2004                    .map(|decl| (node.module, decl)),
2005            );
2006        }
2007
2008        TypeKind::Bitmask { bits } => {
2009            if ghost_created_for(applied, node.node_id, "CppBitmask").is_none() {
2010                return Ok(());
2011            }
2012            emitted.push(key);
2013            let bound = ast.effective_bit_bound(id);
2014            let mut names = Vec::new();
2015            let mut flag_values: Vec<(String, i128)> = Vec::new();
2016            // An **integer alias with constants**, not a scoped
2017            // enumeration, and the reason is what a bitmask value is: the
2018            // OR of two bits. `Flags::A | Flags::B` is not a value of a
2019            // scoped enumeration — the operator is not even declared for
2020            // one — and a value outside an enumeration's range is
2021            // unspecified once its bit-field cannot hold it
2022            // ([dcl.enum]/8). An enumeration whose values are meant to be
2023            // combined is an enumeration used for something it does not
2024            // model. The alias also puts the C++ and the C column on the
2025            // same shape, so their constants compare directly.
2026            out.push((
2027                node.module,
2028                cpp::Decl::Alias(cpp::Alias {
2029                    doc: Doc::line(
2030                        "A bitmask, as the unsigned integer Table 33 of XTypes 1.3 \
2031                         7.4.1.1.3.2 gives for its bound.",
2032                    ),
2033                    name: Ident::new(&name),
2034                    type_: cpp_holder(bound),
2035                }),
2036            ));
2037            for bit in bits {
2038                let Ok(bit_node) = ast.bit_value(*bit) else {
2039                    return Err(MaterialiseError::Unevaluated {
2040                        construct: "bitmask bit the model listed".to_string(),
2041                        at: node.name.clone(),
2042                    });
2043                };
2044                if ghost_created_for(applied, bit_node.node_id, "CppBitConst").is_none() {
2045                    continue;
2046                }
2047                let position = ast.effective_bit_position(*bit).ok_or_else(|| {
2048                    MaterialiseError::Unevaluated {
2049                        construct: "bit position".to_string(),
2050                        at: bit_node.name.clone(),
2051                    }
2052                })?;
2053                // `Flags{1} << 63` and not `1 << 63`: the second shifts
2054                // an `int` past its width, which is undefined behaviour
2055                // and reachable from a @bit_bound(64) bitmask. The
2056                // `static_cast` back is what makes the initialiser the
2057                // alias's own type rather than the `int` the shift
2058                // promotes to.
2059                let one = cpp::Expr::Construct {
2060                    type_: Box::new(cpp::Type::named(cpp_type_path(ast, id))),
2061                    args: vec![cpp::Expr::Literal(Literal::int(1))],
2062                    braced: true,
2063                };
2064                names.push((cxx_name(&bit_node.name), bit_node.name.clone()));
2065                out.push((
2066                    node.module,
2067                    cpp::Decl::Variable(cpp::Variable {
2068                        doc: Doc::none(),
2069                        type_: cpp::Type::named(cpp_type_path(ast, id)),
2070                        name: Ident::new(&cxx_name(&bit_node.name)),
2071                        value: Some(cpp::Expr::Cast {
2072                            kind: cpp::CastKind::Static,
2073                            type_: Box::new(cpp::Type::named(cpp_type_path(ast, id))),
2074                            value: Box::new(cpp::Expr::Paren(Box::new(cpp::Expr::Binary {
2075                                op: "<<",
2076                                lhs: Box::new(one),
2077                                rhs: Box::new(cpp::Expr::Literal(Literal::int(i128::from(
2078                                    position,
2079                                )))),
2080                            }))),
2081                        }),
2082                        is_static: false,
2083                        is_constexpr: true,
2084                        is_inline: true,
2085                    }),
2086                ));
2087                flag_values.push((cxx_name(&bit_node.name), 1i128 << position));
2088            }
2089            // XTypes 1.3 §7.5.1.2.2: "the language binding shall be as if
2090            // there was an enumeration definition like the following:
2091            // @bit_bound(n) enum <TypeName>Bits { @value(1 << p) FLAG, … }".
2092            // Scoped, like every enumeration of this column, so its
2093            // enumerators do not meet the constants above.
2094            out.push((
2095                node.module,
2096                cpp::Decl::Enum(cpp::Enum {
2097                    doc: Doc::line(
2098                        "The enumeration of the flags of a bitmask (XTypes 1.3 \
2099                         7.5.1.2.2): one enumerator per flag, valued 1 << position.",
2100                    ),
2101                    is_scoped: true,
2102                    name: Ident::new(&format!("{name}Bits")),
2103                    underlying: Some(cpp_holder(bound)),
2104                    enumerators: flag_values
2105                        .into_iter()
2106                        .map(|(flag, value)| cpp::Enumerator {
2107                            doc: Doc::none(),
2108                            name: Ident::new(&flag),
2109                            value: Some(cpp::Expr::Literal(int_literal(value))),
2110                        })
2111                        .collect(),
2112                }),
2113            ));
2114            out.extend(
2115                idl_names(&name, &names)?
2116                    .into_iter()
2117                    .map(|decl| (node.module, decl)),
2118            );
2119        }
2120
2121        TypeKind::Bitset { .. } => {
2122            if ghost_created_for(applied, node.node_id, "CppBitset").is_none() {
2123                return Ok(());
2124            }
2125            emitted.push(key);
2126            let mut members = Vec::new();
2127            let mut names = Vec::new();
2128            // The base's bitfields are members of the derived class too:
2129            // an inherited bitset is one flat run.
2130            for bitfield in &ast.all_bitfields_of(id) {
2131                let Ok(field_node) = ast.bitfield(*bitfield) else {
2132                    return Err(MaterialiseError::Unevaluated {
2133                        construct: "bitset field the model listed".to_string(),
2134                        at: node.name.clone(),
2135                    });
2136                };
2137                // An anonymous field is padding: it moves the next field
2138                // along and carries no value of its own.
2139                let Some(field_name) = field_node.name.as_ref() else {
2140                    continue;
2141                };
2142                // Not a C++ bit-field: the allocation order of bit-fields
2143                // within an allocation unit is implementation-defined
2144                // ([class.bit]/1), and the order on the wire is the
2145                // declaration's. Packing is the codec's, where it can be
2146                // seen.
2147                names.push((cxx_name(field_name), field_name.clone()));
2148                members.push(field(cpp_holder(field_node.bits), field_name, Doc::none()));
2149            }
2150            members.push(bitset_equality(&name, &names));
2151            out.push((
2152                node.module,
2153                cpp::Decl::Class(class(
2154                    &name,
2155                    members,
2156                    Doc::line(
2157                        "A bitset. The fields are separate integers here; the packing \
2158                         into the anchor is the codec's, because C++ leaves bit-field \
2159                         allocation order to the implementation.",
2160                    ),
2161                )),
2162            ));
2163            out.extend(
2164                idl_names(&name, &names)?
2165                    .into_iter()
2166                    .map(|decl| (node.module, decl)),
2167            );
2168        }
2169
2170        TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
2171            let created = if matches!(node.kind, TypeKind::Valuetype { .. }) {
2172                "CppValue"
2173            } else {
2174                "CppStruct"
2175            };
2176            if ghost_created_for(applied, node.node_id, created).is_none() {
2177                return Ok(());
2178            }
2179            emitted.push(key);
2180            // XTypes 1.3 §7.5.1.4.1.1: "The C++ struct corresponding to the
2181            // subtype shall publicly inherit from the C++ struct
2182            // corresponding to the supertype." So the class declares its own
2183            // members and has the base's by inheritance; the plan still puts
2184            // a base's members on the wire first (§7.4.1.1.5.1), and the
2185            // codec reaches them by name, as members of the derived class.
2186            // `names` lists all of them, the base's first: the table of
2187            // IDL names and `swap` are about everything the class has.
2188            let base = match &node.kind {
2189                TypeKind::Structure {
2190                    base: Some(base), ..
2191                } => base.resolved(),
2192                _ => None,
2193            };
2194            let own: Vec<_> = ast.members_of(id).to_vec();
2195            let mut members = Vec::new();
2196            let mut names = Vec::new();
2197            let mut values: Vec<String> = Vec::new();
2198            let mut pointers: Vec<String> = Vec::new();
2199            for member in ast.all_members_of(id) {
2200                let Ok(member_node) = ast.member(member) else {
2201                    return Err(MaterialiseError::Unevaluated {
2202                        construct: "member the model listed".to_string(),
2203                        at: node.name.clone(),
2204                    });
2205                };
2206                names.push((cxx_name(&member_node.name), member_node.name.clone()));
2207                if base.is_some() && !own.contains(&member) {
2208                    continue;
2209                }
2210                let Some(target) = member_node.type_.resolved() else {
2211                    return Err(MaterialiseError::Unevaluated {
2212                        construct: "member type".to_string(),
2213                        at: member_node.name.clone(),
2214                    });
2215                };
2216                let inner = cpp_value_type(ast, target, &member_node.name)?;
2217                match held(ast, member_node) {
2218                    Held::Shared => {
2219                        members.push(field(
2220                            std_type("shared_ptr", vec![inner]),
2221                            &member_node.name,
2222                            external_doc(),
2223                        ));
2224                        pointers.push(cxx_name(&member_node.name));
2225                    }
2226                    Held::Plain => {
2227                        let mut member = field(
2228                            cpp::Type::Pointer(Box::new(inner)),
2229                            &member_node.name,
2230                            Doc::lines(&[
2231                                "An @optional member: a plain pointer, NULL for an omitted",
2232                                "value. DDS-XTypes 1.3 7.5.1.2.4.2: \"Optional members shall be",
2233                                "represented using plain pointers rather than automatic values",
2234                                "or smart pointers [...] A NULL pointer shall indicate an",
2235                                "omitted value.\" The class owns what it points at: the",
2236                                "destructor deletes it and a copy is deep, as 7.5.1.2.3.2 asks",
2237                                "of an external member. In a @mutable type an absent member is",
2238                                "not written at all; outside one its absence is written — a",
2239                                "zero <is_present> byte under XCDR2 (rule 20) or a parameter",
2240                                "header declaring a length of zero under XCDR1 (7.4.1.2).",
2241                            ]),
2242                        );
2243                        if let cpp::ClassMember::Field(f) = &mut member {
2244                            f.default = Some(cpp::Expr::Path(Path::new(&["nullptr"])));
2245                        }
2246                        members.push(member);
2247                        pointers.push(cxx_name(&member_node.name));
2248                    }
2249                    Held::Value => {
2250                        let default = member_default(ast, target, &inner, &member_node.name)?;
2251                        let mut member = field(inner, &member_node.name, Doc::none());
2252                        if let cpp::ClassMember::Field(f) = &mut member {
2253                            f.default = default;
2254                        }
2255                        members.push(member);
2256                        values.push(cxx_name(&member_node.name));
2257                    }
2258                }
2259            }
2260            let base_text = base.map(|base| {
2261                let path = cpp_type_path(ast, base);
2262                let spelled: Vec<&str> = path.segments().iter().map(|part| part.as_str()).collect();
2263                format!("::{}", spelled.join("::"))
2264            });
2265            members.extend(owning_members(
2266                &name,
2267                base_text.as_deref(),
2268                &values,
2269                &pointers,
2270            ));
2271            // DDS-PSM-Cxx 1.0 §7.12: a topic type has `swap` as a member and
2272            // in its namespace. Not generated for a member that is itself
2273            // spelled `swap`, which the member function would collide with.
2274            let swappable = names.iter().all(|(spelled, _)| spelled != "swap");
2275            if swappable {
2276                members.push(swap_member(&name, &names));
2277            }
2278            let mut declared = class(&name, members, Doc::none());
2279            if let Some(base) = base {
2280                declared.bases.push(cpp::Base {
2281                    access: cpp::Access::Public,
2282                    virtual_: false,
2283                    path: cpp_type_path(ast, base),
2284                });
2285            }
2286            out.push((node.module, cpp::Decl::Class(declared)));
2287            if swappable {
2288                out.push((node.module, swap_function(&name)));
2289            }
2290            out.extend(
2291                idl_names(&name, &names)?
2292                    .into_iter()
2293                    .map(|decl| (node.module, decl)),
2294            );
2295        }
2296
2297        TypeKind::Union { discriminator, .. } => {
2298            if ghost_created_for(applied, node.node_id, "CppUnion").is_none() {
2299                return Ok(());
2300            }
2301            emitted.push(key);
2302            let Some(disc) = discriminator.resolved() else {
2303                return Err(MaterialiseError::Unevaluated {
2304                    construct: "union discriminator".to_string(),
2305                    at: name,
2306                });
2307            };
2308            let disc_type = cpp_value_type(ast, disc, &name)?;
2309            // The branches and not `_d`: the discriminator is this
2310            // binding's accessor name, and IDL 4.2 §7.4.1.4.4.1 gives it
2311            // no member name of its own to record.
2312            let mut names = Vec::new();
2313            let mut cases: Vec<UnionCase> = Vec::new();
2314            for case in ast.cases_of(id) {
2315                let Ok(case_node) = ast.case(*case) else {
2316                    return Err(MaterialiseError::Unevaluated {
2317                        construct: "union case the model listed".to_string(),
2318                        at: node.name.clone(),
2319                    });
2320                };
2321                if ghost_created_for(applied, case_node.node_id, "CppCase").is_none() {
2322                    continue;
2323                }
2324                let Ok(member) = ast.member(case_node.member) else {
2325                    return Err(MaterialiseError::Unevaluated {
2326                        construct: "member behind a union case the model listed".to_string(),
2327                        at: node.name.clone(),
2328                    });
2329                };
2330                let Some(target) = member.type_.resolved() else {
2331                    return Err(MaterialiseError::Unevaluated {
2332                        construct: "union case type".to_string(),
2333                        at: member.name.clone(),
2334                    });
2335                };
2336                names.push((cxx_name(&member.name), member.name.clone()));
2337                // The first label selects the case in its setter; one that
2338                // cannot be evaluated would leave the setter selecting
2339                // nothing.
2340                let mut label = None;
2341                for candidate in &case_node.labels {
2342                    if let CaseLabel::Value(expr) = candidate {
2343                        let value = ast.eval_integral(*expr).map_err(|_| {
2344                            MaterialiseError::Unevaluated {
2345                                construct: "union case label".to_string(),
2346                                at: node.name.clone(),
2347                            }
2348                        })?;
2349                        // A value above `long long` is an unsigned constant: C++ gives
2350                        // the decimal no type of its own and warns, so it takes `ULL`.
2351                        label = Some(if value > i128::from(i64::MAX) {
2352                            format!("{value}ULL")
2353                        } else {
2354                            zerodds_lang_render::cpp::integer_text(value)
2355                        });
2356                        break;
2357                    }
2358                }
2359                let branch = cpp_value_type(ast, target, &member.name)?;
2360                if super::held_externally(ast, member) {
2361                    cases.push(UnionCase {
2362                        name: cxx_name(&member.name),
2363                        type_: std_type("shared_ptr", vec![branch]),
2364                        label,
2365                        doc: external_doc(),
2366                    });
2367                    continue;
2368                }
2369                cases.push(UnionCase {
2370                    name: cxx_name(&member.name),
2371                    type_: branch,
2372                    label,
2373                    doc: Doc::none(),
2374                });
2375            }
2376            out.push((
2377                node.module,
2378                cpp::Decl::Class(class(
2379                    &name,
2380                    union_members(&disc_type, &cases),
2381                    Doc::lines(&[
2382                        "A union: the accessor functions of IDL4 to C++ 1.0 (`_d()` and one",
2383                        "accessor per case) over a discriminator and one storage per branch.",
2384                        "Not a C++ `union` — a branch may be a std::string or a std::vector,",
2385                        "which a union member may be only with the lifetime managed by hand —",
2386                        "and not a std::variant, because two branches may carry the same",
2387                        "type and a variant could not then say which is live.",
2388                        "",
2389                        "Only the branch `_d()` selects is written. The others hold",
2390                        "whatever they were left holding and cost the sample their",
2391                        "storage, which is the price of not allocating.",
2392                    ]),
2393                )),
2394            ));
2395            out.extend(
2396                idl_names(&name, &names)?
2397                    .into_iter()
2398                    .map(|decl| (node.module, decl)),
2399            );
2400        }
2401
2402        TypeKind::Native => {
2403            if ghost_created_for(applied, node.node_id, "CppNative").is_none() {
2404                return Ok(());
2405            }
2406            emitted.push(key);
2407            // Opaque, never a guess at what the name might mean: the
2408            // concrete type comes from the facet a `#pragma` or an
2409            // annotation set, and a `void *` when there is none.
2410            let mapping = ast
2411                .any_of_type(id)
2412                .ok()
2413                .and_then(|any| ast.facets(any))
2414                .and_then(|facets| facets.corba.as_ref())
2415                .and_then(|corba| corba.native_mapping.clone());
2416            out.push((
2417                node.module,
2418                cpp::Decl::Alias(cpp::Alias {
2419                    doc: Doc::line("A native type. Opaque unless a facet named its mapping."),
2420                    name: Ident::new(&name),
2421                    type_: match mapping {
2422                        Some(spelling) => cpp::Type::named(Path::single(&spelling)),
2423                        None => cpp::Type::Pointer(Box::new(cpp::Type::Void)),
2424                    },
2425                }),
2426            ));
2427        }
2428
2429        TypeKind::ValueBox { boxed } => {
2430            let Some(target) = boxed.resolved() else {
2431                return Ok(());
2432            };
2433            emitted.push(key);
2434            out.push((
2435                node.module,
2436                cpp::Decl::Alias(cpp::Alias {
2437                    doc: Doc::line("A boxed value: a shared handle, empty when null."),
2438                    name: Ident::new(&name),
2439                    type_: std_type("shared_ptr", vec![cpp_value_type(ast, target, &name)?]),
2440                }),
2441            ));
2442        }
2443
2444        // Answered above, before the name was taken.
2445        TypeKind::Primitive(_)
2446        | TypeKind::String { .. }
2447        | TypeKind::Fixed { .. }
2448        | TypeKind::Sequence { .. }
2449        | TypeKind::Array { .. }
2450        | TypeKind::Map { .. }
2451        | TypeKind::ObjectRef { .. }
2452        | TypeKind::AnnotationType { .. }
2453        | TypeKind::Any
2454        | TypeKind::ValueBase => return Ok(()),
2455    }
2456    // The declaration's own IDL name, in one place rather than in every
2457    // arm above: an arm that emitted nothing has already returned, so
2458    // reaching here is what "this declaration was written" means.
2459    if has_idl_name(&node.kind) && !node.name.is_empty() {
2460        out.push((node.module, idl_name(&name, &node.name)));
2461    }
2462    Ok(())
2463}
2464
2465/// The underlying type a scoped enumeration is given.
2466///
2467/// From the `@bit_bound` **and** from the values, which is not the same
2468/// question twice. The bound says how wide the holder is on the wire;
2469/// the values say whether the type can hold them at all, and an
2470/// enumerator of a fixed-underlying-type enumeration has to be a
2471/// converted constant expression of that type ([dcl.enum]/5) — a
2472/// narrowing initialiser is not one, so `= -1` under a `std::uint32_t`
2473/// underlying type is a header that does not compile.
2474///
2475/// So: signed when any enumerator is negative and unsigned otherwise,
2476/// and wide enough for the larger of the bound and the values. XTypes
2477/// 1.3 §7.4.3.5.3 makes the wire holder signed, which is why the C
2478/// column's typedef is an `int32_t`; that is a statement about the
2479/// stream and the runtime applies it, and it does not oblige a
2480/// `@bit_bound(32)` enumeration whose largest value is `2^32 - 1` to be
2481/// declared in a type that cannot hold it.
2482fn enum_underlying(bound: u16, values: &[(String, i128)]) -> cpp::Type {
2483    let signed = values.iter().any(|(_, value)| *value < 0);
2484    let mut bits = bound.max(1);
2485    for (_, value) in values {
2486        bits = bits.max(bits_for(*value, signed));
2487    }
2488    if signed {
2489        return match bits {
2490            0..=8 => std_type("int8_t", Vec::new()),
2491            9..=16 => std_type("int16_t", Vec::new()),
2492            17..=32 => std_type("int32_t", Vec::new()),
2493            _ => std_type("int64_t", Vec::new()),
2494        };
2495    }
2496    cpp_holder(bits)
2497}
2498
2499/// How many bits a value needs, in the signedness it will be written in.
2500fn bits_for(value: i128, signed: bool) -> u16 {
2501    for bits in [8u16, 16, 32] {
2502        let fits = if signed {
2503            let limit = 1i128 << (bits - 1);
2504            value >= -limit && value < limit
2505        } else {
2506            value >= 0 && value < (1i128 << bits)
2507        };
2508        if fits {
2509            return bits;
2510        }
2511    }
2512    64
2513}
2514
2515/// Whether the rules created a node of `type_name` for `node`.
2516///
2517/// The same two-hop walk the Rust and the C materialiser do: a
2518/// correspondence is an ordinary node with a link to the anchor and a
2519/// link to what was created.
2520fn ghost_created_for(applied: &Applied, node: NodeId, type_name: &str) -> Option<GhostId> {
2521    let anchor = applied
2522        .origin
2523        .iter()
2524        .find(|(_, model)| **model == node)
2525        .map(|(ghost, _)| *ghost)?;
2526    let typ = applied.graph.types.lookup(type_name)?;
2527    applied
2528        .graph
2529        .nodes_of_type(typ)
2530        .map(|created| created.id)
2531        .find(|created| {
2532            applied.graph.connected(&anchor, created)
2533                || applied
2534                    .graph
2535                    .parts(&anchor)
2536                    .any(|part| applied.graph.connected(&part.other, created))
2537        })
2538}
2539
2540/// The type and the literal a C++ constant takes.
2541///
2542/// The type is not always the member type. `cpp_value_type` answers
2543/// `std::string` for a `string`, a `wstring` and a `fixed`, and C++17
2544/// has no `constexpr std::string` — the class gained constexpr
2545/// constructors in C++20, and this project builds at 17. The three
2546/// therefore become `const char *`, which is `constexpr`, converts to
2547/// `std::string` at every use, and is the same UTF-8 the member type
2548/// holds. The alternative was `inline const std::string`, which is a
2549/// dynamic initialisation at program start for a value the compiler
2550/// already has.
2551///
2552/// `long double` is refused by name: `zerodds::wire::Float128` is a
2553/// class with no literal, and the compiler's own `long double` is three
2554/// different formats across the platforms this project builds for.
2555fn cpp_constant(
2556    value: &zerodds_ast::ConstValue,
2557    declared: cpp::Type,
2558    at: &str,
2559) -> Result<(cpp::Type, Literal), MaterialiseError> {
2560    use zerodds_ast::ConstValue;
2561    let refuse = |construct: &str| MaterialiseError::NoLanguageForm {
2562        construct: construct.to_string(),
2563        language: "C++",
2564        at: at.to_string(),
2565    };
2566    /// `const char *`, the type the three string-shaped forms take.
2567    fn text_type() -> cpp::Type {
2568        cpp::Type::Pointer(Box::new(cpp::Type::Const(Box::new(cpp::Type::Primitive(
2569            cpp::Primitive::Char,
2570        )))))
2571    }
2572    Ok(match value {
2573        ConstValue::Boolean(flag) => (declared, Literal::Bool(*flag)),
2574        ConstValue::Integer(number) => (declared, int_literal(*number)),
2575        ConstValue::Char(character) => (declared, Literal::Char(*character)),
2576        // The code point as a number. `cpp_primitive` gives `wchar` a
2577        // fixed-width integer for the reason the C column gives — the
2578        // platform's `wchar_t` is not the wire's code unit — and a
2579        // character literal would be the execution character set.
2580        ConstValue::WChar(point) => (declared, int_literal(i128::from(*point))),
2581        ConstValue::Float { mantissa, exponent } => {
2582            if matches!(&declared, cpp::Type::Named { path, .. } if path
2583                .segments()
2584                .last()
2585                .is_some_and(|last| last.as_str() == "Float128"))
2586            {
2587                return Err(refuse("a `long double` constant, which has no C++ literal"));
2588            }
2589            let mut text = constant::float_text(mantissa, exponent.as_deref());
2590            // Every generated header is compiled under `-Wconversion`,
2591            // and a `double` initialiser narrowing into a `float` is
2592            // what it reports.
2593            if matches!(&declared, cpp::Type::Primitive(cpp::Primitive::Float)) {
2594                text.push('f');
2595            }
2596            (declared, Literal::Float(text))
2597        }
2598        ConstValue::Fixed { digits, scale } => (
2599            text_type(),
2600            Literal::Str(constant::fixed_decimal(digits, *scale)),
2601        ),
2602        ConstValue::String(text) => (text_type(), Literal::Str(text.clone())),
2603        ConstValue::WString(points) => (
2604            text_type(),
2605            Literal::Str(
2606                constant::wstring_utf8(points)
2607                    .ok_or_else(|| refuse("a `wstring` constant holding a lone surrogate"))?,
2608            ),
2609        ),
2610        // `ConstValue` is `#[non_exhaustive]`, so a form added to the
2611        // model reaches here. Refused by name rather than given a
2612        // plausible default.
2613        _ => return Err(refuse("a constant form this binding has no literal for")),
2614    })
2615}
2616
2617#[cfg(test)]
2618mod tests {
2619    #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
2620    use super::*;
2621    use crate::rule;
2622    use crate::{ModelGraph, apply};
2623    use std::path::{Path as FsPath, PathBuf};
2624
2625    /// Where the interop corpus sits, from this crate's manifest.
2626    fn corpus() -> PathBuf {
2627        FsPath::new(env!("CARGO_MANIFEST_DIR"))
2628            .join("..")
2629            .join("..")
2630            .join("tests")
2631            .join("interop-corpus")
2632    }
2633
2634    /// Parse, lower and resolve one IDL file.
2635    fn model(source: &str, path: &str) -> Result<Ast, String> {
2636        let parsed = zerodds_idl_parse::parse(source);
2637        if parsed.has_errors() {
2638            return Err(format!("parse: {:?}", parsed.diagnostics()));
2639        }
2640        let lowered = zerodds_idl_lower::lower(&parsed.syntax(), path);
2641        if !lowered.diagnostics.is_empty() {
2642            return Err(format!("lower: {:?}", lowered.diagnostics));
2643        }
2644        let mut ast = lowered.ast;
2645        let report = ast
2646            .resolve_all_by_name()
2647            .map_err(|error| error.to_string())?;
2648        if !report.is_clean() {
2649            return Err(format!("resolve: {:?}", report.broken));
2650        }
2651        Ok(ast)
2652    }
2653
2654    /// One IDL file, all the way to rendered C++.
2655    fn header(source: &str, path: &str) -> Result<String, String> {
2656        let ast = model(source, path)?;
2657        let graph = ModelGraph::of(&ast);
2658        let applied = apply(&graph, &rule::cpp::catalogue()).map_err(|error| error.to_string())?;
2659        let decls = cpp_items(&ast, &applied).map_err(|error| error.to_string())?;
2660        Ok(zerodds_lang_render::cpp::file(&cpp::File {
2661            doc: Doc::line("Generated by zerodds-lang-rules. Do not edit."),
2662            pragma_once: true,
2663            includes: includes(),
2664            decls,
2665        }))
2666    }
2667
2668    /// Every case of the interop corpus, parsed, lowered, applied,
2669    /// materialised and rendered.
2670    ///
2671    /// A case that fails has to fail with a [`MaterialiseError`] this
2672    /// asserts by name; a case that renders has to render something. The
2673    /// second half of the gate is not here and cannot be: a header that
2674    /// renders and does not compile is a materialiser defect, and only a
2675    /// C++ compiler knows which. Set `ZERODDS_CPP_OUT` to a directory and
2676    /// this writes every rendered header into it, one `<case>.hpp` per
2677    /// case, for `c++ -std=c++17 -fsyntax-only -Wall -Wextra -Wpedantic
2678    /// -Werror -I runtime/cpp/include` to read.
2679    #[test]
2680    fn every_corpus_case_renders_or_names_its_refusal() {
2681        let dump = std::env::var("ZERODDS_CPP_OUT").ok();
2682        if let Some(directory) = &dump {
2683            std::fs::create_dir_all(directory).expect("the dump directory");
2684        }
2685        let mut cases: Vec<PathBuf> = std::fs::read_dir(corpus())
2686            .expect("the interop corpus")
2687            .filter_map(|entry| entry.ok().map(|entry| entry.path()))
2688            .filter(|path| path.join("type.idl").is_file())
2689            .collect();
2690        cases.sort();
2691        assert!(
2692            cases.len() > 300,
2693            "the corpus has {} cases, which is not it",
2694            cases.len()
2695        );
2696
2697        let mut rendered = 0usize;
2698        let mut refused: Vec<String> = Vec::new();
2699        for case in &cases {
2700            let name = case
2701                .file_name()
2702                .and_then(|name| name.to_str())
2703                .unwrap_or("?")
2704                .to_string();
2705            let idl = case.join("type.idl");
2706            let source = std::fs::read_to_string(&idl).expect("the case's IDL");
2707            match header(&source, &idl.display().to_string()) {
2708                Ok(text) => {
2709                    assert!(!text.is_empty(), "{name} rendered nothing");
2710                    if let Some(directory) = &dump {
2711                        std::fs::write(FsPath::new(directory).join(format!("{name}.hpp")), &text)
2712                            .expect("the dumped header");
2713                    }
2714                    rendered += 1;
2715                }
2716                Err(message) => refused.push(format!("{name}: {message}")),
2717            }
2718        }
2719
2720        assert_eq!(
2721            rendered + refused.len(),
2722            cases.len(),
2723            "every case is either rendered or refused"
2724        );
2725        let refused = crate::adapter::without_the_front_end_refusals(refused);
2726        assert!(
2727            refused.is_empty(),
2728            "{} of {} cases were refused:\n{}",
2729            refused.len(),
2730            cases.len(),
2731            refused.join("\n")
2732        );
2733    }
2734
2735    #[test]
2736    fn an_optional_member_is_a_plain_pointer_and_not_a_second_field() {
2737        let text = header(
2738            "@appendable struct S { @optional long a; long b; };",
2739            "t.idl",
2740        )
2741        .expect("renders");
2742        assert!(text.contains("std::int32_t* a = nullptr;"), "{text}");
2743        assert!(!text.contains("std::optional"), "{text}");
2744        assert!(!text.contains("_has_a"), "{text}");
2745        // The class owns the object: destructor, deep copy, move.
2746        assert!(text.contains("~S()"), "{text}");
2747        assert!(text.contains("S(const S& zd_other)"), "{text}");
2748        assert!(text.contains("zerodds::wire::clone(zd_other.a)"), "{text}");
2749        assert!(text.contains("zerodds::wire::destroy(a);"), "{text}");
2750    }
2751
2752    #[test]
2753    fn a_module_is_a_namespace_and_not_a_prefix() {
2754        let text =
2755            header("module a { module b { struct P { long x; }; }; };", "t.idl").expect("renders");
2756        assert!(text.contains("namespace a {"), "{text}");
2757        assert!(text.contains("namespace b {"), "{text}");
2758        assert!(!text.contains("a_b_P"), "{text}");
2759    }
2760
2761    #[test]
2762    fn a_reference_across_modules_is_written_from_the_root() {
2763        let text = header(
2764            "module a { struct P { long x; }; }; module b { struct Q { a::P p; }; };",
2765            "t.idl",
2766        )
2767        .expect("renders");
2768        assert!(text.contains("::a::P p;"), "{text}");
2769    }
2770
2771    #[test]
2772    fn a_sequence_is_a_vector_and_a_map_is_a_class_over_a_vector_of_pairs() {
2773        let text = header(
2774            "struct S { sequence<long> items; map<long, double> m; };",
2775            "t.idl",
2776        )
2777        .expect("renders");
2778        assert!(text.contains("std::vector<std::int32_t> items;"), "{text}");
2779        // Not a std::map: the entry order is a policy axis and a
2780        // std::map would decide it before the encoder ran. The class of
2781        // XTypes 1.3 §7.5.1.3.3 is the vector of pairs with the operations
2782        // of Table 47 (runtime/cpp/include/zerodds/map.hpp).
2783        assert!(
2784            text.contains("::zerodds::Map<std::int32_t, double> m;"),
2785            "{text}"
2786        );
2787        assert!(!text.contains("std::map<"), "{text}");
2788    }
2789
2790    #[test]
2791    fn a_structure_has_swap_as_a_member_and_in_its_namespace() {
2792        let text = header("struct S { long a; long b; };", "t.idl").expect("renders");
2793        // DDS-PSM-Cxx 1.0 7.12: `swap` as a member and next to the type.
2794        assert!(text.contains("zd_other.a"), "{text}");
2795        assert!(text.contains("zd_other.b"), "{text}");
2796        assert!(text.contains("zd_a.swap(zd_b)"), "{text}");
2797        // A member spelled `swap` would collide with the member function.
2798        let colliding = header("struct T { long swap; };", "t.idl").expect("renders");
2799        assert!(!colliding.contains("zd_other"), "{colliding}");
2800    }
2801
2802    #[test]
2803    fn an_array_nests_with_the_outer_dimension_outermost() {
2804        let text = header("struct S { long a[2][3]; };", "t.idl").expect("renders");
2805        assert!(
2806            text.contains("std::array<std::array<std::int32_t, 3>, 2> a = {};"),
2807            "{text}"
2808        );
2809    }
2810
2811    #[test]
2812    fn a_union_is_a_class_with_a_discriminator_and_one_field_per_case() {
2813        let text = header(
2814            "union U switch (long) { case 1: long a; case 2: long b; };",
2815            "t.idl",
2816        )
2817        .expect("renders");
2818        assert!(text.contains("struct U {"), "{text}");
2819        assert!(text.contains("std::int32_t _d() const"), "{text}");
2820        assert!(text.contains("void _d(std::int32_t value)"), "{text}");
2821        assert!(text.contains("std::int32_t zd_d;"), "{text}");
2822        assert!(text.contains("const std::int32_t& a() const"), "{text}");
2823        assert!(text.contains("std::int32_t& a()"), "{text}");
2824        assert!(text.contains("void a(const std::int32_t& value)"), "{text}");
2825        assert!(text.contains("void a(std::int32_t&& value)"), "{text}");
2826        // The setter of a case selects it.
2827        assert!(
2828            text.contains("zd_a = value; zd_d = static_cast<std::int32_t>(1);"),
2829            "{text}"
2830        );
2831        assert!(
2832            text.contains("zd_b = value; zd_d = static_cast<std::int32_t>(2);"),
2833            "{text}"
2834        );
2835        // Two cases of the same type, which is what rules out a variant.
2836        assert!(text.contains("std::int32_t zd_a;"), "{text}");
2837        assert!(text.contains("std::int32_t zd_b;"), "{text}");
2838        // The prose above the class names both of the shapes that were
2839        // not chosen, so these look for the declarations they would have
2840        // produced and not for the words.
2841        assert!(!text.contains("union U"), "{text}");
2842        assert!(!text.contains("std::variant<"), "{text}");
2843    }
2844
2845    #[test]
2846    fn an_enumeration_takes_an_underlying_type_from_its_bit_bound() {
2847        let narrow = header("@bit_bound(8) enum E { A, B };", "t.idl").expect("renders");
2848        assert!(narrow.contains("enum class E : std::uint8_t {"), "{narrow}");
2849        let wide = header("enum W { A, B };", "t.idl").expect("renders");
2850        assert!(wide.contains("enum class W : std::uint32_t {"), "{wide}");
2851    }
2852
2853    #[test]
2854    fn a_long_double_is_the_runtime_type_and_never_the_compiler_s() {
2855        let text = header("struct S { long double d; double e; };", "t.idl").expect("renders");
2856        // IDL4 to C++ 1.0 §7.2.4.3.1: built-in members start at their
2857        // default value, which `= {}` value-initializes.
2858        assert!(text.contains("::zerodds::wire::Float128 d = {};"), "{text}");
2859        assert!(text.contains("double e = {};"), "{text}");
2860        assert!(!text.contains("long double"), "{text}");
2861    }
2862
2863    #[test]
2864    fn the_tag_keeps_apart_what_the_cpp_spelling_collapses() {
2865        let ast = model(
2866            "struct S { octet a; uint8 b; double c; long double d; string e; wstring f; };",
2867            "t.idl",
2868        )
2869        .expect("model");
2870        let mut tags = Vec::new();
2871        let mut spellings = Vec::new();
2872        let members = ast
2873            .types()
2874            .find_map(|(id, node)| {
2875                matches!(node.kind, TypeKind::Structure { .. }).then(|| ast.all_members_of(id))
2876            })
2877            .expect("the structure");
2878        for member in members {
2879            let node = ast.member(member).expect("member");
2880            let target = node.type_.resolved().expect("resolved");
2881            tags.push(cpp_type_tag(&ast, target));
2882            spellings.push(zerodds_lang_render::cpp::type_(
2883                &cpp_value_type(&ast, target, "t").expect("type"),
2884            ));
2885        }
2886        assert_eq!(tags.len(), 6);
2887        let mut unique = tags.clone();
2888        unique.sort();
2889        unique.dedup();
2890        assert_eq!(unique.len(), 6, "the tags collapse: {tags:?}");
2891        // And the C++ spellings do collapse, which is the whole point of
2892        // the tag existing.
2893        let mut collapsed = spellings.clone();
2894        collapsed.sort();
2895        collapsed.dedup();
2896        assert!(
2897            collapsed.len() < 6,
2898            "the spellings do not collapse, so the tag would be redundant: {spellings:?}"
2899        );
2900    }
2901
2902    /// §7.5.1.1.2, Table 43: "On platforms compliant with the C99
2903    /// specification, the implementation may generate equivalent
2904    /// C99-compatible types" — the generated class spells the integers
2905    /// with the fixed-width types of `<cstdint>`, which are the C99 column
2906    /// of the table, and no `int` or `long`.
2907    #[test]
2908    fn the_integers_are_spelled_with_the_c99_types_of_table_43() {
2909        let text = header(
2910            "struct S { int8 a; uint8 b; short c; unsigned short d; long e; unsigned long f; \
2911             long long g; unsigned long long h; };",
2912            "t.idl",
2913        )
2914        .expect("renders");
2915        for expected in [
2916            "std::int8_t a ",
2917            "std::uint8_t b ",
2918            "std::int16_t c ",
2919            "std::uint16_t d ",
2920            "std::int32_t e ",
2921            "std::uint32_t f ",
2922            "std::int64_t g ",
2923            "std::uint64_t h ",
2924        ] {
2925            assert!(text.contains(expected), "{expected}:\n{text}");
2926        }
2927    }
2928
2929    /// §7.5.1.1.2: "All other mappings for basic types shall be
2930    /// distinguishable for the purposes of overloading" — only Boolean,
2931    /// Char8 and Byte may coincide. `octet` is the runtime's one-octet class,
2932    /// `uint8` is `std::uint8_t`, `int8` is `std::int8_t` and `char` is `char`.
2933    #[test]
2934    fn the_octet_is_not_the_type_of_uint8_or_int8_or_char() {
2935        let text = header(
2936            "struct S { octet a; uint8 b; int8 c; char d; boolean e; };",
2937            "t.idl",
2938        )
2939        .expect("renders");
2940        assert!(text.contains("::zerodds::wire::Octet a "), "{text}");
2941        assert!(text.contains("std::uint8_t b "), "{text}");
2942        assert!(text.contains("std::int8_t c "), "{text}");
2943        assert!(text.contains("char d "), "{text}");
2944        assert!(text.contains("bool e "), "{text}");
2945    }
2946
2947    #[test]
2948    fn an_any_member_is_refused_by_name_at_any_depth() {
2949        let bare = header("struct S { any a; };", "t.idl").expect_err("refused");
2950        assert!(bare.contains("C++ has no form for `any`"), "{bare}");
2951        let nested = header("struct S { sequence<any> a; };", "t.idl").expect_err("refused");
2952        assert!(nested.contains("C++ has no form for `any`"), "{nested}");
2953    }
2954}