Skip to main content

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}