zerodds_wire_probe/fill_cpp.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The value-filling code the Fast DDS probe needs.
4//!
5//! `fill.rs` writes C, for the two generators whose output is a plain
6//! `struct`. Fast DDS generates a C++ class: every member is a pair of
7//! accessors, so nothing can be assigned through a dotted path and the
8//! statements have a shape of their own.
9//!
10//! ```text
11//! sample.x = 1; // Cyclone, RTI
12//! sample.x(1); // Fast DDS
13//! sample.location.x = 1.0; // Cyclone, RTI
14//! sample.location().x(1.0); // Fast DDS
15//! ```
16//!
17//! ## Why this is not a self-oracle
18//!
19//! The same reason `fill.rs` is not: the statements place values into
20//! the *vendor's* structures and the bytes still come from the vendor's
21//! own serialiser. A misplaced value shows up as a disagreement between
22//! the three probes on that case, never as a false agreement.
23//!
24//! ## Accessors, not temporaries
25//!
26//! `fastddsgen` emits a non-`const` reference getter beside every
27//! setter, so a collection or a nested aggregate is reachable in place:
28//! `sample.items()[0].a(1)`. That keeps the recursion the same shape as
29//! the C one — an expression that names the destination — instead of
30//! building a temporary per collection and handing it to the setter.
31//! Only the terminal assignment differs, and only in two forms:
32//!
33//! | reached through | assignment |
34//! |---|---|
35//! | a member of an aggregate | `object.name(value);` |
36//! | an element of an array or a sequence | `expression = value;` |
37//! | a subscript of a map | `expression = value;` |
38//!
39//! ## Two places a statement has to reach before it can write
40//!
41//! The last row above writes like the middle one and does not *exist*
42//! like it: `std::map::operator[]` inserts the entry, so a value that
43//! emits no statement leaves the entry out of the sample the peer
44//! serialises. And a union branch is reachable only after its setter
45//! has selected it — `fastddsgen`'s getter throws until then. Both are
46//! handled at one place each, `emit` and the `Union` arm, because
47//! both were once handled in the arm that happened to hit them and both
48//! were recorded as Fast DDS divergences.
49
50use crate::contract::{self, Consumed};
51use crate::encode::ProbeError;
52use serde_json::Value;
53use std::fmt::Write as _;
54use zerodds_ast::{Ast, CharWidth, PrimitiveKind, TypeKind, TypeNode};
55use zerodds_ast_core::Id;
56
57/// Where one value goes, in the shapes Fast DDS's generated code
58/// offers.
59///
60/// Two of the three merely *reach* a place that already exists. The
61/// third **creates** it, and that difference is what
62/// [`emit`] enforces rather than each arm remembering it.
63enum Place {
64 /// A member of an aggregate: `object.name(value)` to write it,
65 /// `object.name()` to descend into it.
66 Accessor {
67 /// The expression naming the aggregate.
68 object: String,
69 /// The member's name, which is also the accessor's name.
70 name: String,
71 },
72 /// An element of a `std::array` or a `std::vector`, which is a plain
73 /// lvalue in both directions. The element exists before the
74 /// statement runs — the array declares it and `resize` creates it.
75 Element(String),
76 /// A subscript of a `std::map`, which is a plain lvalue in both
77 /// directions and exists **only once a statement has named it**:
78 /// `std::map::operator[]` is what inserts the entry.
79 MapEntry(String),
80}
81
82impl Place {
83 /// The statement that writes `literal` here.
84 fn assign(&self, literal: &str) -> String {
85 match self {
86 Self::Accessor { object, name } => format!("{object}.{name}({literal});"),
87 Self::Element(expression) | Self::MapEntry(expression) => {
88 format!("{expression} = {literal};")
89 }
90 }
91 }
92
93 /// The expression that names what is here, for descending into it.
94 fn reference(&self) -> String {
95 match self {
96 Self::Accessor { object, name } => format!("{object}.{name}()"),
97 Self::Element(expression) | Self::MapEntry(expression) => expression.clone(),
98 }
99 }
100}
101
102/// The statements, and a note of every scalar the walk took out of the
103/// value document.
104///
105/// The recorder rides with the output buffer for the same reason the
106/// serialiser's does: one mutable thing to thread, so a new arm cannot
107/// forget it.
108#[derive(Debug, Default)]
109struct Cpp {
110 source: String,
111 seen: Consumed,
112}
113
114/// Emit the C++ statements that fill `sample` with `values`.
115///
116/// # Errors
117/// [`ProbeError`], naming the path in the value.
118pub fn fill_fastdds(ast: &Ast, type_: Id<TypeNode>, values: &Value) -> Result<String, ProbeError> {
119 fill_fastdds_recording(ast, type_, values, &mut Consumed::new())
120}
121
122/// As [`fill_fastdds`], recording which scalars of the value were read.
123///
124/// # Errors
125/// As [`fill_fastdds`].
126pub fn fill_fastdds_recording(
127 ast: &Ast,
128 type_: Id<TypeNode>,
129 values: &Value,
130 seen: &mut Consumed,
131) -> Result<String, ProbeError> {
132 // Before the first hop; see `contract::reject_alias_cycle`.
133 contract::reject_alias_cycle(ast)?;
134 let mut out = Cpp::default();
135 let root = contract::root_path(ast, type_);
136 let result = emit(
137 ast,
138 type_,
139 values,
140 &Place::Element(String::from("sample")),
141 &mut out,
142 &root,
143 );
144 *seen = core::mem::take(&mut out.seen);
145 result?;
146 Ok(out.source)
147}
148
149/// The C++ name a type takes in `fastddsgen`'s output.
150///
151/// The module path stays a namespace path, so `::common::util::Location`
152/// is `common::util::Location`. This is what `--print-c-name` prints for
153/// the Fast DDS probe, and it is also the enum's scope: `enum class`
154/// means an enumerator is `Colour::RED`, never a bare `RED`.
155#[must_use]
156pub fn cpp_name(ast: &Ast, type_: Id<TypeNode>) -> String {
157 ast.any_of_type(type_)
158 .ok()
159 .and_then(|any| ast.scoped_name_of(any))
160 .map_or_else(
161 || String::from("UNKNOWN"),
162 |scoped| scoped.segments().join("::"),
163 )
164}
165
166/// One value into one place, and the guarantee that the place exists
167/// afterwards.
168///
169/// Every arm below writes through [`Place::assign`] or
170/// [`Place::reference`], so any statement at all names the place and, at
171/// a [`Place::MapEntry`], calls `std::map::operator[]` and inserts the
172/// entry. The converse is what this function is for: a value whose
173/// filled form is **empty** reaches its arm and emits no statement, the
174/// subscript is never written, the entry is never created, and the peer
175/// serialises a map one entry short of the sample. Measured at
176/// `b0bc71313`: `map-value-sequence`'s entry `(1, [])` and
177/// `map-nested`'s entry `(2, {})` had no statement, and the 16 Fast DDS
178/// rows that recorded were a defect in this emitter and not a
179/// measurement of Fast DDS.
180///
181/// The class is wider than the one arm that was found. An empty
182/// sequence, an empty inner map, a structure with no member to write —
183/// an `@optional` absent from every member, or no members at all — a
184/// bitset with no declared field, and an array whose every element is
185/// one of those, all emit nothing. So the guard is here, at the one
186/// place that knows which kind of place it is, rather than in each arm
187/// that can be empty.
188///
189/// zerodds-lint: recursion-depth 128 — the sample's own nesting. This adds
190/// the empty-place guard and hands the value straight to `emit_value`,
191/// which carries the argument.
192fn emit(
193 ast: &Ast,
194 type_: Id<TypeNode>,
195 value: &Value,
196 place: &Place,
197 out: &mut Cpp,
198 path: &str,
199) -> Result<(), ProbeError> {
200 let before = out.source.len();
201 emit_value(ast, type_, value, place, out, path)?;
202 if let Place::MapEntry(subscript) = place {
203 if out.source.len() == before {
204 // A discarded subscript, which is a call to `operator[]` and
205 // therefore the insertion. The cast is what keeps
206 // `-Wunused-value` quiet about the discarded reference.
207 let _ = writeln!(out.source, "(void){subscript};");
208 }
209 }
210 Ok(())
211}
212
213/// zerodds-lint: recursion-depth 128 — the sample's own nesting. Every arm
214/// but the typedef one descends into a strictly smaller part of the value,
215/// so the walk ends because the sample is finite: a recursive type has
216/// finite values, because the sequence that closes its cycle is eventually
217/// empty. The typedef arm keeps the value and is finite because
218/// `contract::reject_alias_cycle` at the entry point has refused a chain
219/// that returns to itself. A value file is `serde_json`'s to parse and its
220/// `RECURSION_LIMIT` is 128.
221#[allow(clippy::too_many_lines)]
222fn emit_value(
223 ast: &Ast,
224 type_: Id<TypeNode>,
225 value: &Value,
226 place: &Place,
227 out: &mut Cpp,
228 path: &str,
229) -> Result<(), ProbeError> {
230 let node = ast.type_(type_).map_err(|error| ProbeError::NoLayout {
231 detail: format!("{error}"),
232 })?;
233 match &node.kind {
234 // Measured, Fast DDS with FastCDR `FASTCDR_SIZEOF_LONG_DOUBLE
235 // 16` on Debian 13 x86-64: `Cdr::serialize(long double)` copies
236 // the object's sixteen bytes, which are x87 80-bit extended plus
237 // six bytes the compiler never wrote. The same source on aarch64
238 // emits binary128, because there `long double` *is* binary128.
239 // A literal here would pick one architecture's wire format and
240 // record it as Fast DDS's; `docs/float128-column.md` §2 has the
241 // bytes from both.
242 TypeKind::Primitive(PrimitiveKind::Float128) => Err(ProbeError::Unsupported {
243 path: path.to_string(),
244 kind: "a long double, whose Fast DDS bytes differ between x86-64 and aarch64",
245 }),
246 TypeKind::Primitive(kind) => {
247 let literal = primitive_literal(*kind, value, path)?;
248 out.seen.scalar(path);
249 let _ = writeln!(out.source, "{}", place.assign(&literal));
250 Ok(())
251 }
252 TypeKind::String {
253 width: CharWidth::Char,
254 ..
255 } => {
256 let text = contract::text(value, path)?;
257 out.seen.scalar(path);
258 let _ = writeln!(out.source, "{}", place.assign(&cpp_string(text)));
259 Ok(())
260 }
261 TypeKind::String {
262 width: CharWidth::WChar,
263 ..
264 } => {
265 let text = contract::text(value, path)?;
266 out.seen.scalar(path);
267 emit_wstring(text, place, out);
268 Ok(())
269 }
270 TypeKind::Bitmask { .. } => {
271 let set = contract::bit_names(ast, type_, value, path)?;
272 let literal = bitmask_literal(ast, type_, &set, out, path);
273 let _ = writeln!(out.source, "{}", place.assign(&literal));
274 Ok(())
275 }
276 TypeKind::Bitset { .. } => {
277 let object = contract::object(value, path)?;
278 // The base's bitfields too: `fastddsgen` flattens an
279 // inherited bitset into a single struct, so
280 // `sample.value().low` addresses an inherited field with no
281 // change to the statement shape.
282 let declared = contract::bitfield_names(ast, &ast.all_bitfields_of(type_));
283 contract::reject_unknown_keys(object, &declared, path)?;
284 let here = place.reference();
285 // Every declared field, in declaration order, and not only
286 // the keys the object happens to hold: a bitfield with no
287 // value is a field left at whatever the C++ default gave it,
288 // which is the sample the serialiser then writes.
289 for name in &declared {
290 let field_path = contract::join(path, name);
291 let Some(field_value) = object.get(name) else {
292 return Err(ProbeError::Missing { path: field_path });
293 };
294 let raw = contract::integer(field_value, &field_path)?;
295 out.seen.scalar(&field_path);
296 // `fastddsgen` renders a bitset as a plain struct of C++
297 // bit-fields with public members, not as an accessor
298 // pair, so these are ordinary assignments.
299 let _ = writeln!(out.source, "{here}.{name} = {raw};");
300 }
301 Ok(())
302 }
303 TypeKind::Map {
304 key,
305 value: entry,
306 bound,
307 } => {
308 let key_type = key.resolved().ok_or_else(|| ProbeError::NoLayout {
309 detail: format!("{path}: the map key type is unresolved"),
310 })?;
311 let value_type = entry.resolved().ok_or_else(|| ProbeError::NoLayout {
312 detail: format!("{path}: the map value type is unresolved"),
313 })?;
314 // An array of pairs and not an object, the same shape the
315 // ZeroDDS probe reads: a JSON object keys on a string and
316 // `map<long, float>` does not.
317 let entries = contract::bounded_items(ast, value, *bound, "entries", path)?;
318 let here = place.reference();
319 for (index, pair) in entries.iter().enumerate() {
320 let entry_path = format!("{path}[{index}]");
321 let (entry_key, entry_value) = contract::map_entry(pair, &entry_path)?;
322 let key_path = contract::join(&entry_path, "key");
323 let key_literal = scalar_literal(ast, key_type, entry_key, &key_path)?;
324 out.seen.scalar(&key_path);
325 emit(
326 ast,
327 value_type,
328 entry_value,
329 &Place::MapEntry(format!("{here}[{key_literal}]")),
330 out,
331 &contract::join(&entry_path, "value"),
332 )?;
333 }
334 Ok(())
335 }
336 TypeKind::Fixed { .. } => Err(ProbeError::Unsupported {
337 path: path.to_string(),
338 kind: "a fixed, which fastddsgen rejects in the IDL before any \
339 statement could be written for it",
340 }),
341 TypeKind::Enumeration { .. } => {
342 // Checked against the declared set before it is pasted into
343 // an identifier, the same as the C emitter and the
344 // serialiser.
345 let (name, _) = contract::enumerator(ast, type_, value, path)?;
346 out.seen.scalar(path);
347 let literal = format!("{}::{name}", cpp_name(ast, type_));
348 let _ = writeln!(out.source, "{}", place.assign(&literal));
349 Ok(())
350 }
351 TypeKind::Alias { aliased } => {
352 let target = aliased.resolved().ok_or_else(|| ProbeError::NoLayout {
353 detail: format!("{path}: the typedef target is unresolved"),
354 })?;
355 emit(ast, target, value, place, out, path)
356 }
357 TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
358 let object = contract::object(value, path)?;
359 let here = place.reference();
360 let members = contract::members(ast, type_);
361 let declared: Vec<String> = members.iter().map(|member| member.name.clone()).collect();
362 // A key that names no member is an error and not a silent
363 // skip. An emitter that ignored it would drop part of the
364 // sample and still produce bytes, which is the one failure
365 // this whole file exists to make impossible.
366 contract::reject_unknown_keys(object, &declared, path)?;
367 for member in &members {
368 // An absent `@optional` has nothing to fill, the same
369 // as in the C emitter.
370 let Some(member_value) = contract::member_value(object, member, path)? else {
371 continue;
372 };
373 // `fastddsgen` renders `@external` as
374 // `eprosima::fastcdr::external<T>` and the generated
375 // default constructor leaves it empty, so the pointee
376 // is allocated first and the value written through the
377 // dereferenced getter — the read side
378 // (`dump_cpp::is_external`) mirrors this. `decltype`
379 // names the pointee so no C++ type spelling is needed.
380 // `@optional @external` stacks the two wrappers:
381 // `optional<external<T>>`. Engaging the optional with a
382 // freshly allocated external is one assignment through
383 // `optional::operator=(T&&)`; the pointee is then
384 // filled through `value()`. The `decltype` operand is
385 // unevaluated, so naming `value()` on the still
386 // disengaged optional is well-defined.
387 if crate::dump_cpp::is_external(ast, type_, &member.name) && member.optional {
388 let getter = format!("{here}.{}()", member.name);
389 let _ = writeln!(
390 out.source,
391 "{{ using zd_t = std::remove_reference_t<decltype(*({getter}.value()))>; \
392{getter} = eprosima::fastcdr::external<zd_t>(std::make_shared<zd_t>()); }}"
393 );
394 emit(
395 ast,
396 member.type_,
397 member_value,
398 &Place::Element(format!("(*{getter}.value())")),
399 out,
400 &contract::join(path, &member.name),
401 )?;
402 continue;
403 }
404 if crate::dump_cpp::is_external(ast, type_, &member.name) && !member.optional {
405 let getter = format!("{here}.{}()", member.name);
406 let _ = writeln!(
407 out.source,
408 "{{ using zd_t = std::remove_reference_t<decltype(*{getter})>; \
409{getter} = eprosima::fastcdr::external<zd_t>(std::make_shared<zd_t>()); }}"
410 );
411 emit(
412 ast,
413 member.type_,
414 member_value,
415 &Place::Element(format!("(*{getter})")),
416 out,
417 &contract::join(path, &member.name),
418 )?;
419 continue;
420 }
421 // A present `@optional` string cannot go through the
422 // setter: it takes `const optional<std::string>&`, and
423 // `const char*` → `std::string` → `optional` is two
424 // user conversions, which C++ refuses (measured:
425 // `no matching function for call to 'Sample::note(const
426 // char [6])'`, matrix fastdds annotation-must-understand
427 // and annotation-optional-mixed). `optional::operator=
428 // (const T&)` exists (fast-cdr
429 // include/fastcdr/xcdr/optional.hpp:234), so the getter
430 // assignment needs only the one conversion.
431 if member.optional && crate::fill::is_string_type(ast, member.type_) {
432 emit(
433 ast,
434 member.type_,
435 member_value,
436 &Place::Element(format!("{here}.{}()", member.name)),
437 out,
438 &contract::join(path, &member.name),
439 )?;
440 continue;
441 }
442 emit(
443 ast,
444 member.type_,
445 member_value,
446 &Place::Accessor {
447 object: here.clone(),
448 name: member.name.clone(),
449 },
450 out,
451 &contract::join(path, &member.name),
452 )?;
453 }
454 Ok(())
455 }
456 TypeKind::Union { .. } => {
457 let object = contract::object(value, path)?;
458 let discriminant = contract::discriminator(object, path)?;
459 let here = place.reference();
460 // The members go first and `_d` last. A Fast DDS member
461 // setter moves the discriminator to that member's own label,
462 // so an explicit `_d` written before it would be overwritten;
463 // written after it, it pins the exact value — and the
464 // generated `_d` setter refuses a value that would select a
465 // different member, which is the error one wants.
466 for (name, member_value) in object {
467 if name == "_d" {
468 continue;
469 }
470 let Some(member_type) = union_member(ast, type_, name) else {
471 return Err(ProbeError::Unknown {
472 path: contract::join(path, name),
473 declared: union_member_names(ast, type_),
474 });
475 };
476 contract::reject_null(member_value, &contract::join(path, name))?;
477 // A branch that is filled *through* its getter has to
478 // be selected first, and only the setter selects it.
479 //
480 // Measured, `fastddsgen` 4.3.0 over
481 // `union Choice switch (short) { case -1: sequence<long>
482 // ids; default: string text; }`: the default
483 // constructor sets `selected_member_` to the default
484 // branch, `ids()` throws
485 // `BadParamException("This member has not been
486 // selected")` while it is not the selection, and
487 // `ids(v)` both assigns and sets `m__d = -1`. So
488 // `sample.picked().ids().resize(2)` — what this emitted
489 // — threw before a value was written, and
490 // `union-member-sequence`, `union-member-struct` and
491 // their twins stood in the record as sixteen Fast DDS
492 // refusals that were this emitter's.
493 //
494 // `{}` and not a typed literal: the setter's parameter
495 // names the type, so the emitter does not have to. It
496 // is overwritten immediately by the walk below, and the
497 // `_d` written after the loop pins the discriminator.
498 // An `@external` branch is `eprosima::fastcdr::external<T>`,
499 // empty when default-constructed: the setter selects the
500 // branch with a freshly allocated pointee, and the value
501 // is written through it — what the structure-member arm
502 // above does for an `@external` member. Through the
503 // getter alone, `annotation-external-union-branch` and its
504 // twin stood in the record as Fast DDS refusals that were
505 // this emitter's: "'class eprosima::fastcdr::external<…>'
506 // has no member named 'a'".
507 if crate::dump_cpp::is_external(ast, type_, name) {
508 let getter = format!("{here}.{name}()");
509 let _ = writeln!(
510 out.source,
511 "{{ using zd_t = std::remove_reference_t<decltype(*{getter})>; \
512{here}.{name}(eprosima::fastcdr::external<zd_t>(std::make_shared<zd_t>())); }}"
513 );
514 emit(
515 ast,
516 member_type,
517 member_value,
518 &Place::Element(format!("(*{getter})")),
519 out,
520 &contract::join(path, name),
521 )?;
522 continue;
523 }
524 if reached_through_the_getter(ast, member_type) {
525 let _ = writeln!(out.source, "{here}.{name}({{}});");
526 }
527 emit(
528 ast,
529 member_type,
530 member_value,
531 &Place::Accessor {
532 object: here.clone(),
533 name: name.to_string(),
534 },
535 out,
536 &contract::join(path, name),
537 )?;
538 }
539 let TypeKind::Union { discriminator, .. } = &node.kind else {
540 return Err(ProbeError::Unsupported {
541 path: path.to_string(),
542 kind: "a union whose kind changed under the emitter",
543 });
544 };
545 let discriminator = discriminator
546 .resolved()
547 .ok_or_else(|| ProbeError::NoLayout {
548 detail: format!("{path}: the discriminator is unresolved"),
549 })?;
550 emit(
551 ast,
552 discriminator,
553 discriminant,
554 &Place::Accessor {
555 object: here,
556 name: String::from("_d"),
557 },
558 out,
559 &contract::join(path, "_d"),
560 )
561 }
562 TypeKind::Array { element, dims } => {
563 let element = element.resolved().ok_or_else(|| ProbeError::NoLayout {
564 detail: format!("{path}: the array element is unresolved"),
565 })?;
566 let counts = contract::array_dimensions(ast, dims, path)?;
567 emit_array(ast, element, value, &place.reference(), out, path, &counts)
568 }
569 TypeKind::Sequence { element, bound } => {
570 let element = element.resolved().ok_or_else(|| ProbeError::NoLayout {
571 detail: format!("{path}: the sequence element is unresolved"),
572 })?;
573 let items = contract::bounded_items(ast, value, *bound, "elements", path)?;
574 // `std::vector`, so the length is a resize. `resize(0)` on a
575 // vector that is already empty is a no-op, and writing it
576 // unconditionally is what keeps the statement count from
577 // depending on the value: the omission this used to carry
578 // was reasoned at a member — "the member starts empty" — and
579 // was false at a map subscript, where the omitted statement
580 // was also the entry's only creation. `emit` now guarantees
581 // that place separately; the unconditional form is here so
582 // that the two guards do not have to agree.
583 let here = place.reference();
584 let _ = writeln!(out.source, "{here}.resize({});", items.len());
585 for (index, item) in items.iter().enumerate() {
586 emit(
587 ast,
588 element,
589 item,
590 &Place::Element(format!("{here}[{index}]")),
591 out,
592 &format!("{path}[{index}]"),
593 )?;
594 }
595 Ok(())
596 }
597 other => Err(ProbeError::Unfilled {
598 path: path.to_string(),
599 kind: other.as_str(),
600 }),
601 }
602}
603
604/// zerodds-lint: recursion-depth 8 — one frame per array dimension.
605/// `counts` drops its head at each frame and the innermost element goes to
606/// `emit`, which is bounded by the sample.
607#[allow(clippy::too_many_arguments)]
608fn emit_array(
609 ast: &Ast,
610 element: Id<TypeNode>,
611 value: &Value,
612 here: &str,
613 out: &mut Cpp,
614 path: &str,
615 counts: &[usize],
616) -> Result<(), ProbeError> {
617 let Some((&count, rest)) = counts.split_first() else {
618 return emit(
619 ast,
620 element,
621 value,
622 &Place::Element(here.to_string()),
623 out,
624 path,
625 );
626 };
627 let items = contract::array(value, path)?;
628 if items.len() != count {
629 return Err(ProbeError::Mismatch {
630 path: path.to_string(),
631 expected: format!("exactly {count} elements"),
632 found: format!("{}", items.len()),
633 });
634 }
635 for (index, item) in items.iter().enumerate() {
636 emit_array(
637 ast,
638 element,
639 item,
640 &format!("{here}[{index}]"),
641 out,
642 &format!("{path}[{index}]"),
643 rest,
644 )?;
645 }
646 Ok(())
647}
648
649/// A `wstring` field, as a `std::wstring` built from code points.
650///
651/// The C emitter cannot name the element type, because XTypes 1.3
652/// §7.5.1.1.1 leaves the C spelling of `Char16` to the implementation.
653/// The C++ mapping does not: §7.5.2 fixes `wstring` as `std::wstring`,
654/// whose element is `wchar_t`. So the type is known and only its width
655/// is not, and `wchar_t` is a code-point holder — four bytes on Linux,
656/// two on Windows.
657///
658/// The initialiser is therefore code points, not UTF-16 code units. A
659/// code point outside the BMP does not fit a sixteen-bit `wchar_t`, and
660/// the `static_assert` says so at compile time rather than truncating.
661///
662/// Not an `L"..."` literal: that would put the text through the
663/// compiler's source and execution character sets, and the point of the
664/// probe is that the sample is exactly the one in the JSON.
665fn emit_wstring(text: &str, place: &Place, out: &mut Cpp) {
666 if text.chars().any(|ch| ch as u32 > 0xFFFF) {
667 let _ = writeln!(
668 out.source,
669 "static_assert(sizeof(wchar_t) >= 4, \
670 \"this sample has a code point outside the BMP\");"
671 );
672 }
673 let mut units = String::new();
674 for ch in text.chars() {
675 let _ = write!(units, "static_cast<wchar_t>(0x{:04x}), ", ch as u32);
676 }
677 let _ = units.pop();
678 let _ = units.pop();
679 let _ = writeln!(
680 out.source,
681 "{}",
682 place.assign(&format!("std::wstring{{{units}}}"))
683 );
684}
685
686/// A bitmask value, as the flag names the binding declares, OR'ed.
687///
688/// XTypes 1.3 §7.5.2.2 gives the same shape the C binding has:
689/// `fastddsgen` emits `enum <TypeName>Bits : <holder> { FLAG = 0x01, … }`
690/// beside `typedef <holder> <TypeName>`, and the enumeration is not an
691/// `enum class`, so a flag is named at the enclosing namespace.
692///
693/// Naming the flags rather than computing the integer keeps the
694/// position-to-bit mapping on the vendor's side of the line.
695fn bitmask_literal(
696 ast: &Ast,
697 type_: Id<TypeNode>,
698 set: &[(String, u16)],
699 out: &mut Cpp,
700 path: &str,
701) -> String {
702 let prefix = namespace_prefix(ast, type_);
703 let mut flags = Vec::with_capacity(set.len());
704 for (index, (name, _)) in set.iter().enumerate() {
705 out.seen.scalar(&format!("{path}[{index}]"));
706 flags.push(format!("{prefix}{name}"));
707 }
708
709 // No flags set is the integer zero, not an empty expression.
710 if flags.is_empty() {
711 "0".to_string()
712 } else {
713 flags.join(" | ")
714 }
715}
716
717/// The literal for a map key, which XTypes 1.3 §7.4.1.1.4 restricts to a
718/// scalar. An aggregate key is refused rather than guessed at.
719///
720/// zerodds-lint: recursion-depth 64 — one frame per typedef hop,
721/// `contract::MAX_ALIAS_HOPS`. The typedef arm is the only recursive one,
722/// and a chain that returns to itself has been refused by
723/// `contract::reject_alias_cycle` at the entry of every walk that reaches
724/// here.
725fn scalar_literal(
726 ast: &Ast,
727 type_: Id<TypeNode>,
728 value: &Value,
729 path: &str,
730) -> Result<String, ProbeError> {
731 let node = ast.type_(type_).map_err(|error| ProbeError::NoLayout {
732 detail: format!("{error}"),
733 })?;
734 match &node.kind {
735 TypeKind::Primitive(kind) => primitive_literal(*kind, value, path),
736 TypeKind::String {
737 width: CharWidth::Char,
738 ..
739 } => Ok(cpp_string(contract::text(value, path)?)),
740 TypeKind::Enumeration { .. } => {
741 let (name, _) = contract::enumerator(ast, type_, value, path)?;
742 Ok(format!("{}::{name}", cpp_name(ast, type_)))
743 }
744 TypeKind::Alias { aliased } => {
745 let target = aliased.resolved().ok_or_else(|| ProbeError::NoLayout {
746 detail: format!("{path}: the typedef target is unresolved"),
747 })?;
748 scalar_literal(ast, target, value, path)
749 }
750 other => Err(ProbeError::Unfilled {
751 path: path.to_string(),
752 kind: other.as_str(),
753 }),
754 }
755}
756
757/// The namespace a type's members are named through, with the trailing
758/// `::` when there is one.
759fn namespace_prefix(ast: &Ast, type_: Id<TypeNode>) -> String {
760 let Some(scoped) = ast
761 .any_of_type(type_)
762 .ok()
763 .and_then(|any| ast.scoped_name_of(any))
764 else {
765 return String::new();
766 };
767 let segments = scoped.segments();
768 if segments.len() <= 1 {
769 return String::new();
770 }
771 let mut prefix = segments[..segments.len() - 1].join("::");
772 prefix.push_str("::");
773 prefix
774}
775
776/// Whether a value of this type is filled by descending through
777/// [`Place::reference`] rather than by writing it with
778/// [`Place::assign`].
779///
780/// The arms of [`emit_value`] that call `reference`: `Bitset`, `Map`,
781/// `Structure`, `Valuetype`, `Union`, `Array` and `Sequence`. The ones
782/// that call `assign`: `Primitive`, both `String` widths, `Bitmask` and
783/// `Enumeration`. `Fixed` and `Float128` are refused before either.
784///
785/// It matters at one place — a union branch, where the getter throws
786/// until the setter has selected the branch — and the list is here
787/// rather than at that place so that an arm which changes how it writes
788/// has one function to be checked against.
789///
790/// zerodds-lint: recursion-depth 64 — one frame per typedef hop,
791/// `contract::MAX_ALIAS_HOPS`. The typedef arm is the only recursive one,
792/// and a chain that returns to itself has been refused by
793/// `contract::reject_alias_cycle` at the entry of every walk that reaches
794/// here.
795fn reached_through_the_getter(ast: &Ast, type_: Id<TypeNode>) -> bool {
796 match ast.type_(type_).map(|node| node.kind.clone()) {
797 Ok(TypeKind::Alias { aliased }) => aliased
798 .resolved()
799 .is_some_and(|target| reached_through_the_getter(ast, target)),
800 Ok(
801 TypeKind::Bitset { .. }
802 | TypeKind::Map { .. }
803 | TypeKind::Structure { .. }
804 | TypeKind::Valuetype { .. }
805 | TypeKind::Union { .. }
806 | TypeKind::Array { .. }
807 | TypeKind::Sequence { .. },
808 ) => true,
809 _ => false,
810 }
811}
812
813fn union_member(ast: &Ast, union: Id<TypeNode>, name: &str) -> Option<Id<TypeNode>> {
814 ast.cases_of(union)
815 .iter()
816 .filter_map(|id| ast.case(*id).ok())
817 .filter_map(|case| ast.member(case.member).ok())
818 .find(|member| member.name == name)
819 .and_then(|member| member.type_.resolved())
820}
821
822fn union_member_names(ast: &Ast, union: Id<TypeNode>) -> Vec<String> {
823 ast.cases_of(union)
824 .iter()
825 .filter_map(|id| ast.case(*id).ok())
826 .filter_map(|case| ast.member(case.member).ok())
827 .map(|member| member.name.clone())
828 .collect()
829}
830
831/// The C++ literal for one primitive.
832///
833/// The same seventeen significant digits the C emitter writes, for the
834/// same reason: a shorter literal would put different bytes on the wire
835/// than the ZeroDDS probe's.
836fn primitive_literal(kind: PrimitiveKind, value: &Value, path: &str) -> Result<String, ProbeError> {
837 Ok(match kind {
838 PrimitiveKind::Boolean => {
839 if contract::boolean(value, path)? {
840 "true".to_string()
841 } else {
842 "false".to_string()
843 }
844 }
845 PrimitiveKind::Char => character(kind, value, path, 2)?,
846 PrimitiveKind::WChar => character(kind, value, path, 4)?,
847 PrimitiveKind::Float32 => {
848 format!("{:.17e}f", contract::float(value, path)?)
849 }
850 PrimitiveKind::Float64 | PrimitiveKind::Float128 => {
851 format!("{:.17e}", contract::float(value, path)?)
852 }
853 PrimitiveKind::Int64 => {
854 let n = contract::integer_for(kind, value, path)?;
855 // As in `fill.rs` and `fill_ros.rs`: C++ has no literal for
856 // INT64_MIN either. `-9223372036854775808LL` is a positive
857 // constant too large for `long long`, and what a compiler
858 // does with it after the unary minus is an
859 // implementation-defined conversion rather than the value
860 // the case declares.
861 if n == i128::from(i64::MIN) {
862 "(-9223372036854775807LL - 1)".to_string()
863 } else {
864 format!("{n}LL")
865 }
866 }
867 PrimitiveKind::UInt64 => format!("{}ULL", contract::integer_for(kind, value, path)?),
868 // The range check is the contract's: a `300` written into a
869 // `uint8_t` accessor is `44` on the wire and no compiler this
870 // harness uses makes a noise about it.
871 _ => format!("{}", contract::integer_for(kind, value, path)?),
872 })
873}
874
875/// One character as a C++ integer literal.
876///
877/// An integer and not a character constant, which is
878/// `corba_cpp::primitive_literal`'s rule and `fill::primitive_literal`'s.
879/// `'x'` is the narrow execution character set, so anything above
880/// U+007F is a multi-character constant — `0xC3A4` for U+00E4 on the
881/// recording host — and `L'x'` is `wchar_t`, four octets on Linux,
882/// where a generated `char16_t` accessor takes two. The integer
883/// converts to whichever the accessor declares and says what it is.
884///
885/// `digits` is how wide the literal is printed, which is presentation
886/// only: it makes a `wchar` read as a code point beside the `wstring`
887/// units the same file emits.
888///
889/// `character_for` and not `character`: a code point above the member's
890/// width is refused here rather than narrowed in silence, and the
891/// serialiser refuses the same value — the three backends have to read
892/// one value file the same way.
893fn character(
894 kind: PrimitiveKind,
895 value: &Value,
896 path: &str,
897 digits: usize,
898) -> Result<String, ProbeError> {
899 let code = contract::character_for(kind, value, path)? as u32;
900 Ok(format!("0x{code:0digits$x}"))
901}
902
903/// A C++ string literal, escaped as `corba_cpp::cpp_string` escapes
904/// one: three-digit octal for every byte outside printable ASCII,
905/// because `\xHH` is greedy and three-digit octal cannot run on. Raw
906/// UTF-8 octets, which this used to emit, compile only where the
907/// compiler's narrow execution character set is UTF-8.
908fn cpp_string(text: &str) -> String {
909 let mut out = String::from("\"");
910 for byte in text.as_bytes() {
911 match byte {
912 b'"' => out.push_str("\\\""),
913 b'\\' => out.push_str("\\\\"),
914 b'\n' => out.push_str("\\n"),
915 b'\r' => out.push_str("\\r"),
916 b'\t' => out.push_str("\\t"),
917 0x20..=0x7E => out.push(char::from(*byte)),
918 other => {
919 let _ = write!(out, "\\{other:03o}");
920 }
921 }
922 }
923 out.push('"');
924 out
925}