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}