Skip to main content

zerodds_wire_probe/
corba_cpp.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The fill and the dump for the OMG IDL-to-C++ mapping.
4//!
5//! Two of the three ORBs generate against it — omniORB 4.3.3 and TAO
6//! 2.5.24 — and they generate the *same* API, because CORBA 3.3 part 3
7//! §1.2 is a specification and not a suggestion. One backend serves
8//! both; only the marshalling call differs, and that lives in the
9//! templates (`tools/wireprobe/omniorb-template.cpp`,
10//! `tao-template.cpp`), never here.
11//!
12//! ```text
13//! sample.x = 1;                      // struct member, array or sequence element
14//! sample.picked.whole(1);            // union case member — §1.2.9's modifier
15//! sample.picked.inner().x = 1;       // …whose accessor returns a reference
16//! ```
17//!
18//! ## Why this is not a self-oracle
19//!
20//! The same reason `fill.rs` is not. The statements place values into
21//! the *ORB's* structures and read them back out of them; the bytes come
22//! from the ORB's own `cdrMemoryStream` or `TAO_OutputCDR`. A statement
23//! that placed a value wrongly shows up as a disagreement between the
24//! probes on that case, never as a false agreement.
25//!
26//! ## Where the two mappings part company, and where they do not
27//!
28//! | construct | C++ mapping | why it matters here |
29//! |---|---|---|
30//! | `string` | `String_member` / `String_Manager`, both `operator const char*` | assigned through `CORBA::string_dup` in both |
31//! | `wstring` | `WString_member` over `CORBA::WChar` | `CORBA::WChar` is **two** bytes in omniORB and **four** in TAO, so the units are copied rather than named |
32//! | `sequence<T>` | `length(n)` and `operator[]` | §1.2.11, identical in both |
33//! | `T x[N]` | a plain C array | §1.2.10 |
34//! | `union` | modifier and accessor per case, `_d()` for the discriminator | §1.2.9 |
35//! | `fixed<P,S>` | `CORBA::Fixed`, `Fixed(const char*)` and `to_string()` | §1.14. **TAO refuses the IDL** — "fixed data types are not supported" — so only omniORB reaches this arm |
36//! | `octet` | `CORBA::Octet`, an unsigned char | which is also where `int8` and `uint8` land; see `corba_idl.rs` |
37//!
38//! ## Identifiers the C++ front ends rename
39//!
40//! An IDL member called `new` is legal IDL and not a C++ declaration,
41//! so both front ends rename it — `_cxx_new`, measured 2026-08-11 from
42//! omniidl's and `tao_idl`'s output for
43//! `tests/interop-corpus/identifier-target-keywords`. An IDL member
44//! called `_long` is the keyword escaped (IDL 4.2 §7.2.3.2) and both
45//! front ends strip the escape before renaming what remains. Every
46//! identifier this file pastes into an identifier position therefore
47//! goes through `cpp_id` first, and only those: the JSON keys the
48//! dump prints and the paths in a [`ProbeError`] carry the name the
49//! IDL wrote, because the wire and the matrix are keyed on that one.
50
51use crate::contract::{self, Consumed};
52use crate::corba_idl::corba_scoped_name;
53use crate::encode::ProbeError;
54use crate::target_keywords::cxx;
55use serde_json::Value;
56use std::borrow::Cow;
57use std::fmt::Write as _;
58use zerodds_ast::{Ast, CharWidth, PrimitiveKind, TypeKind, TypeNode};
59use zerodds_ast_core::Id;
60
61/// The C++ spelling of one IDL identifier, the escape stripped first.
62///
63/// IDL 4.2 §7.2.3.2: a leading underscore escapes a keyword and is not
64/// part of the identifier, so `_long` names `long`. Both front ends
65/// strip the escape and then apply their keyword rename — measured
66/// 2026-08-22 from `tao_idl` 2.5.24 over `identifier-escaped-keyword`:
67/// the generated struct carries `_cxx_long`, `_cxx_double`,
68/// `_cxx_switch` where the IDL wrote `_long`, `_double`, `_switch`,
69/// which is [`cxx`] applied to the bare name. The JSON keys and the
70/// paths keep the IDL spelling, same as [`cxx`] itself.
71fn cpp_id(name: &str) -> Cow<'_, str> {
72    match name.strip_prefix('_') {
73        Some(bare) => Cow::Owned(cxx(bare).into_owned()),
74        None => cxx(name),
75    }
76}
77
78/// Where one value goes, in the two shapes the C++ mapping offers.
79enum Place {
80    /// A plain lvalue: a structure member, an array element, a sequence
81    /// element, or a union case member reached through its accessor.
82    Lvalue(String),
83    /// A union case member of a type whose accessor returns by value.
84    /// §1.2.9 gives it a modifier instead: `object.name(value)`.
85    Modifier {
86        /// The expression naming the union.
87        object: String,
88        /// The case member's name, which is also the modifier's.
89        name: String,
90    },
91}
92
93impl Place {
94    /// The statement that writes `literal` here.
95    fn assign(&self, literal: &str) -> String {
96        match self {
97            Self::Lvalue(expression) => format!("{expression} = {literal};"),
98            Self::Modifier { object, name } => format!("{object}.{name}({literal});"),
99        }
100    }
101
102    /// The expression that names what is here, for descending into it.
103    ///
104    /// Only reached for the kinds whose accessor returns a reference —
105    /// a structure, a union, a sequence or an array. The scalar kinds
106    /// take [`Self::assign`] and never come here.
107    fn reference(&self) -> String {
108        match self {
109            Self::Lvalue(expression) => expression.clone(),
110            Self::Modifier { object, name } => format!("{object}.{name}()"),
111        }
112    }
113}
114
115/// The statements, with the declarations that have to precede them.
116#[derive(Debug, Default)]
117struct Cpp {
118    declarations: String,
119    statements: String,
120    next: usize,
121    seen: Consumed,
122}
123
124impl Cpp {
125    fn into_source(self) -> String {
126        let mut out = self.declarations;
127        out.push_str(&self.statements);
128        out
129    }
130
131    fn temporary(&mut self, stem: &str) -> String {
132        self.next += 1;
133        format!("zd_{stem}_{}", self.next)
134    }
135}
136
137/// Emit the C++ statements that fill `sample` with `values`.
138///
139/// # Errors
140/// [`ProbeError`], naming the path in the value.
141pub fn fill_corba_cpp(
142    ast: &Ast,
143    type_: Id<TypeNode>,
144    values: &Value,
145) -> Result<String, ProbeError> {
146    fill_corba_cpp_recording(ast, type_, values, &mut Consumed::new())
147}
148
149/// As [`fill_corba_cpp`], recording which scalars of the value were
150/// read.
151///
152/// # Errors
153/// As [`fill_corba_cpp`].
154pub fn fill_corba_cpp_recording(
155    ast: &Ast,
156    type_: Id<TypeNode>,
157    values: &Value,
158    seen: &mut Consumed,
159) -> Result<String, ProbeError> {
160    // Before the first hop; see `contract::reject_alias_cycle`.
161    contract::reject_alias_cycle(ast)?;
162    let mut out = Cpp::default();
163    let root = contract::root_path(ast, type_);
164    let result = fill_at(
165        ast,
166        type_,
167        values,
168        &Place::Lvalue(String::from("sample")),
169        &mut out,
170        &root,
171    );
172    *seen = core::mem::take(&mut out.seen);
173    result?;
174    Ok(out.into_source())
175}
176
177/// Whether a type's union accessor returns a reference that can be
178/// descended into, or a value that only a modifier can set.
179///
180/// CORBA 3.3 part 3 §1.2.9: a case member of a "variable-length or
181/// constructed" type gets a reference accessor; a scalar gets a value
182/// one. Getting this wrong does not compile, which is the reason it is
183/// a function and not a comment.
184fn accessor_is_a_reference(ast: &Ast, type_: Id<TypeNode>) -> bool {
185    matches!(
186        ast.type_(contract::resolve_alias(ast, type_))
187            .map(|node| node.kind.clone()),
188        Ok(TypeKind::Structure { .. }
189            | TypeKind::Valuetype { .. }
190            | TypeKind::Union { .. }
191            | TypeKind::Sequence { .. }
192            | TypeKind::Array { .. })
193    )
194}
195
196/// A union case member that is an array, which §1.2.9 gives a slice
197/// **pointer** accessor rather than a reference.
198fn array_after_aliases(ast: &Ast, type_: Id<TypeNode>) -> Option<TypeKind> {
199    let kind = ast
200        .type_(contract::resolve_alias(ast, type_))
201        .map(|node| node.kind.clone())
202        .ok()?;
203    matches!(kind, TypeKind::Array { .. }).then_some(kind)
204}
205
206/// zerodds-lint: recursion-depth 128 — the sample's own nesting. Every arm
207/// but the typedef one descends into a strictly smaller part of the value,
208/// so the walk ends because the sample is finite: a recursive type has
209/// finite values, because the sequence that closes its cycle is eventually
210/// empty. The typedef arm keeps the value and is finite because
211/// `contract::reject_alias_cycle` at the entry point has refused a chain
212/// that returns to itself. A value file is `serde_json`'s to parse and its
213/// `RECURSION_LIMIT` is 128.
214#[allow(clippy::too_many_lines)]
215fn fill_at(
216    ast: &Ast,
217    type_: Id<TypeNode>,
218    value: &Value,
219    place: &Place,
220    out: &mut Cpp,
221    path: &str,
222) -> Result<(), ProbeError> {
223    let node = ast.type_(type_).map_err(|error| ProbeError::NoLayout {
224        detail: format!("{error}"),
225    })?;
226    match &node.kind {
227        TypeKind::Primitive(kind) => {
228            let literal = primitive_literal(*kind, value, path)?;
229            out.seen.scalar(path);
230            let _ = writeln!(out.statements, "{}", place.assign(&literal));
231            Ok(())
232        }
233        TypeKind::String {
234            width: CharWidth::Char,
235            ..
236        } => {
237            let text = contract::text(value, path)?;
238            out.seen.scalar(path);
239            let _ = writeln!(
240                out.statements,
241                "{}",
242                place.assign(&format!("CORBA::string_dup({})", cpp_string(text)))
243            );
244            Ok(())
245        }
246        TypeKind::String {
247            width: CharWidth::WChar,
248            ..
249        } => {
250            let text = contract::text(value, path)?;
251            out.seen.scalar(path);
252            // Not an `L"…"` literal. `wchar_t` is four bytes on Linux
253            // and `CORBA::WChar` is two in omniORB, so a wide literal is
254            // the wrong array against one of the two ORBs — and with a
255            // cast in front of it, wrong without saying so. An array of
256            // `CORBA::WChar` initialised from plain integers is right
257            // against both, whatever the ORB made the type.
258            //
259            // The units are UTF-16, which is what §7.4.1.1.2 puts on the
260            // wire. Where `CORBA::WChar` is four bytes the ORB is handed
261            // the surrogates themselves; that is a measurement and not a
262            // mistake — see `docs/corba-column.md`.
263            let buffer = out.temporary("wstr");
264            let mut units = String::new();
265            for unit in text.encode_utf16() {
266                let _ = write!(units, "0x{unit:04x}, ");
267            }
268            let _ = writeln!(
269                out.declarations,
270                "static CORBA::WChar {buffer}[] = {{ {units}0x0000 }};"
271            );
272            let _ = writeln!(
273                out.statements,
274                "{}",
275                place.assign(&format!("CORBA::wstring_dup({buffer})"))
276            );
277            Ok(())
278        }
279        TypeKind::Fixed { .. } => {
280            let text = contract::text(value, path)?;
281            out.seen.scalar(path);
282            // §1.14's `Fixed(const char *)`. The decimal goes in as the
283            // sample wrote it and the ORB rounds it to the declared
284            // scale, which is the ORB's rule to apply and not this
285            // harness's to pre-apply.
286            let _ = writeln!(
287                out.statements,
288                "{}",
289                place.assign(&format!("CORBA::Fixed({})", cpp_string(text)))
290            );
291            Ok(())
292        }
293        TypeKind::Enumeration { .. } => {
294            let (name, _) = contract::enumerator(ast, type_, value, path)?;
295            out.seen.scalar(path);
296            let _ = writeln!(
297                out.statements,
298                "{}",
299                place.assign(&enumerator_name(ast, type_, &name))
300            );
301            Ok(())
302        }
303        TypeKind::Alias { aliased } => {
304            let target = aliased.resolved().ok_or_else(|| ProbeError::NoLayout {
305                detail: format!("{path}: the typedef target is unresolved"),
306            })?;
307            fill_at(ast, target, value, place, out, path)
308        }
309        TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
310            let object = contract::object(value, path)?;
311            let members = contract::members(ast, type_);
312            let declared: Vec<String> = members.iter().map(|member| member.name.clone()).collect();
313            contract::reject_unknown_keys(object, &declared, path)?;
314            let target = place.reference();
315            for member in &members {
316                let Some(member_value) = contract::member_value(object, member, path)? else {
317                    continue;
318                };
319                fill_at(
320                    ast,
321                    member.type_,
322                    member_value,
323                    // The member as C++ has it on the left, the member as
324                    // the IDL wrote it in the path on the right. They
325                    // differ exactly when the name is a C++ keyword.
326                    &Place::Lvalue(format!("{target}.{}", cpp_id(&member.name))),
327                    out,
328                    &contract::join(path, &member.name),
329                )?;
330            }
331            Ok(())
332        }
333        TypeKind::Union { discriminator, .. } => {
334            let object = contract::object(value, path)?;
335            let discriminant = contract::discriminator(object, path)?;
336            let target = place.reference();
337            for (name, member_value) in object {
338                if name == "_d" {
339                    continue;
340                }
341                let Some(member_type) = union_member(ast, type_, name) else {
342                    return Err(ProbeError::Unknown {
343                        path: contract::join(path, name),
344                        declared: union_member_names(ast, type_),
345                    });
346                };
347                contract::reject_null(member_value, &contract::join(path, name))?;
348                if let Some(TypeKind::Array { element, dims }) =
349                    array_after_aliases(ast, member_type)
350                {
351                    // §1.2.9's array case member: the accessor returns a
352                    // slice pointer, the modifier takes the array type
353                    // itself, which decays to that same slice pointer —
354                    // `void block(const _0RL_block _value)` in omniORB's
355                    // gen.hh and `void block(_block)` in TAO's genC.h,
356                    // both measured on `array-in-union-branch`. A *named*
357                    // array of the slice type fits both, filled first and
358                    // handed over whole; the modifier deep-copies. The
359                    // accessor's pointee names the slice type without the
360                    // emitter knowing the ORB's spelling of it, the same
361                    // reason the reference branch below uses `decltype`;
362                    // `remove_const` because omniORB's only accessor is
363                    // the `const` one.
364                    let element = element.resolved().ok_or_else(|| ProbeError::NoLayout {
365                        detail: format!("{path}: the array element is unresolved"),
366                    })?;
367                    let counts =
368                        contract::array_dimensions(ast, &dims, &contract::join(path, name))?;
369                    let temporary = out.temporary("case");
370                    let accessor = cpp_id(name);
371                    let _ = writeln!(
372                        out.statements,
373                        "std::remove_const<std::remove_pointer<decltype({target}.{accessor}())>::type>::type {temporary}[{}];",
374                        counts[0]
375                    );
376                    fill_array(
377                        ast,
378                        element,
379                        member_value,
380                        &temporary,
381                        out,
382                        &contract::join(path, name),
383                        &counts,
384                    )?;
385                    let _ = writeln!(out.statements, "{target}.{accessor}({temporary});");
386                } else if accessor_is_a_reference(ast, member_type) {
387                    // Built in a temporary and handed to the modifier,
388                    // never through the accessor. §1.2.9's reference
389                    // accessor is only defined once the branch is
390                    // *active*, and nothing has activated it yet: TAO
391                    // returns a reference to storage it has not
392                    // constructed and the probe takes SIGSEGV —
393                    // measured 2026-08-10 on `union-member-sequence`.
394                    // omniORB happens to survive it, which is exactly
395                    // why the shape had to be decided by the mapping
396                    // and not by whether it ran.
397                    //
398                    // `decltype` names the member's type without the
399                    // emitter knowing it, which matters because an
400                    // anonymous `sequence<long>` inside a union is
401                    // `Choice::_ids_seq` in omniORB and something else
402                    // in TAO. `std::remove_reference` turns the
403                    // accessor's reference back into the value type.
404                    let temporary = out.temporary("case");
405                    let accessor = cpp_id(name);
406                    let _ = writeln!(
407                        out.statements,
408                        "std::remove_reference<decltype({target}.{accessor}())>::type {temporary};"
409                    );
410                    fill_at(
411                        ast,
412                        member_type,
413                        member_value,
414                        &Place::Lvalue(temporary.clone()),
415                        out,
416                        &contract::join(path, name),
417                    )?;
418                    let _ = writeln!(out.statements, "{target}.{accessor}({temporary});");
419                } else {
420                    fill_at(
421                        ast,
422                        member_type,
423                        member_value,
424                        &Place::Modifier {
425                            object: target.clone(),
426                            name: cpp_id(name).into_owned(),
427                        },
428                        out,
429                        &contract::join(path, name),
430                    )?;
431                }
432            }
433            // After the member and never before it. §1.2.9's modifier
434            // sets the discriminator to the *first* label of the case it
435            // belongs to, so a case with two labels, or a `default:`
436            // reached by a value of its own, would carry a
437            // discriminator the sample did not ask for. Setting `_d`
438            // last is what makes the two agree.
439            let discriminator = discriminator
440                .resolved()
441                .ok_or_else(|| ProbeError::NoLayout {
442                    detail: format!("{path}: the discriminator is unresolved"),
443                })?;
444            fill_at(
445                ast,
446                discriminator,
447                discriminant,
448                &Place::Modifier {
449                    object: target,
450                    name: String::from("_d"),
451                },
452                out,
453                &contract::join(path, "_d"),
454            )
455        }
456        TypeKind::Array { element, dims } => {
457            let element = element.resolved().ok_or_else(|| ProbeError::NoLayout {
458                detail: format!("{path}: the array element is unresolved"),
459            })?;
460            let counts = contract::array_dimensions(ast, dims, path)?;
461            fill_array(ast, element, value, &place.reference(), out, path, &counts)
462        }
463        TypeKind::Sequence { element, bound } => {
464            let element = element.resolved().ok_or_else(|| ProbeError::NoLayout {
465                detail: format!("{path}: the sequence element is unresolved"),
466            })?;
467            let items = contract::bounded_items(ast, value, *bound, "elements", path)?;
468            let target = place.reference();
469            let _ = writeln!(out.statements, "{target}.length({});", items.len());
470            for (index, item) in items.iter().enumerate() {
471                fill_at(
472                    ast,
473                    element,
474                    item,
475                    &Place::Lvalue(format!("{target}[{index}]")),
476                    out,
477                    &format!("{path}[{index}]"),
478                )?;
479            }
480            Ok(())
481        }
482        TypeKind::Bitmask { .. } => Err(ProbeError::Unsupported {
483            path: path.to_string(),
484            kind: "a bitmask, which is XTypes 1.3 §7.3.1.2.1.4 and not CORBA IDL",
485        }),
486        TypeKind::Bitset { .. } => Err(ProbeError::Unsupported {
487            path: path.to_string(),
488            kind: "a bitset, which is XTypes 1.3 §7.3.1.2.1.3 and not CORBA IDL",
489        }),
490        TypeKind::Map {
491            key,
492            value: entry,
493            bound,
494        } => {
495            // `map<K, V>` is IDL 4.2 and no CORBA construct, but TAO
496            // 2.5.24 parses it and maps it to `std::map<K, V>` — its
497            // generated header cites `be_visitor_map/map_ch.cpp` — so
498            // an entry is made by subscripting. omniidl still refuses
499            // the keyword ("Error in look-up of 'map': 'map' not
500            // found", measured), so the omniorb column dies at omniidl
501            // and never reaches this arm; the opendds and tao columns
502            // both take it — `wireprobe-corba-idl` prints the map
503            // rather than refusing it, and each front end answers for
504            // itself.
505            let key_type = key.resolved().ok_or_else(|| ProbeError::NoLayout {
506                detail: format!("{path}: the map key type is unresolved"),
507            })?;
508            let value_type = entry.resolved().ok_or_else(|| ProbeError::NoLayout {
509                detail: format!("{path}: the map value type is unresolved"),
510            })?;
511            // An array of pairs and not an object, the same shape every
512            // other probe reads and writes: a JSON object keys on a
513            // string and `map<long, long>` does not.
514            let entries = contract::bounded_items(ast, value, *bound, "entries", path)?;
515            let target = place.reference();
516            for (index, pair) in entries.iter().enumerate() {
517                let entry_path = format!("{path}[{index}]");
518                let (entry_key, entry_value) = contract::map_entry(pair, &entry_path)?;
519                let key_path = contract::join(&entry_path, "key");
520                let key_literal = map_key_literal(ast, key_type, entry_key, &key_path, out)?;
521                out.seen.scalar(&key_path);
522                fill_at(
523                    ast,
524                    value_type,
525                    entry_value,
526                    &Place::Lvalue(format!("{target}[{key_literal}]")),
527                    out,
528                    &contract::join(&entry_path, "value"),
529                )?;
530            }
531            Ok(())
532        }
533        other => Err(ProbeError::Unfilled {
534            path: path.to_string(),
535            kind: other.as_str(),
536        }),
537    }
538}
539
540/// The C++ expression that names one map key.
541///
542/// A scalar key reuses the member literals. A string key is the C++
543/// literal itself: `std::map`'s subscript builds the
544/// `TAO::String_Manager` from it, and TAO's own `operator<`
545/// (`tao/String_Manager_T.h:137`) orders by content, not by pointer. A
546/// wide string key goes through the same unit-array declaration as the
547/// wstring member fill, and for the same reason: a `L"…"` literal is
548/// the wrong array against one of the two ORBs.
549fn map_key_literal(
550    ast: &Ast,
551    key_type: Id<TypeNode>,
552    value: &Value,
553    path: &str,
554    out: &mut Cpp,
555) -> Result<String, ProbeError> {
556    let resolved = contract::resolve_alias(ast, key_type);
557    let node = ast.type_(resolved).map_err(|error| ProbeError::NoLayout {
558        detail: format!("{error}"),
559    })?;
560    match &node.kind {
561        TypeKind::Primitive(kind) => primitive_literal(*kind, value, path),
562        TypeKind::String {
563            width: CharWidth::Char,
564            ..
565        } => {
566            let text = contract::text(value, path)?;
567            Ok(cpp_string(text))
568        }
569        TypeKind::String {
570            width: CharWidth::WChar,
571            ..
572        } => {
573            let text = contract::text(value, path)?;
574            let buffer = out.temporary("wkey");
575            let mut units = String::new();
576            for unit in text.encode_utf16() {
577                let _ = write!(units, "0x{unit:04x}, ");
578            }
579            let _ = writeln!(
580                out.declarations,
581                "static CORBA::WChar {buffer}[] = {{ {units}0x0000 }};"
582            );
583            Ok(buffer)
584        }
585        TypeKind::Enumeration { .. } => {
586            let (name, _) = contract::enumerator(ast, resolved, value, path)?;
587            Ok(enumerator_name(ast, resolved, &name))
588        }
589        other => Err(ProbeError::Unfilled {
590            path: path.to_string(),
591            kind: other.as_str(),
592        }),
593    }
594}
595
596/// zerodds-lint: recursion-depth 8 — one frame per array dimension.
597/// `counts` drops its head at each frame and the innermost element goes to
598/// `fill_at`, which is bounded by the sample.
599#[allow(clippy::too_many_arguments)]
600fn fill_array(
601    ast: &Ast,
602    element: Id<TypeNode>,
603    value: &Value,
604    lvalue: &str,
605    out: &mut Cpp,
606    path: &str,
607    counts: &[usize],
608) -> Result<(), ProbeError> {
609    let Some((&count, rest)) = counts.split_first() else {
610        return fill_at(
611            ast,
612            element,
613            value,
614            &Place::Lvalue(lvalue.to_string()),
615            out,
616            path,
617        );
618    };
619    let items = contract::array(value, path)?;
620    if items.len() != count {
621        return Err(ProbeError::Mismatch {
622            path: path.to_string(),
623            expected: format!("exactly {count} elements"),
624            found: format!("{}", items.len()),
625        });
626    }
627    for (index, item) in items.iter().enumerate() {
628        fill_array(
629            ast,
630            element,
631            item,
632            &format!("{lvalue}[{index}]"),
633            out,
634            &format!("{path}[{index}]"),
635            rest,
636        )?;
637    }
638    Ok(())
639}
640
641// ---------------------------------------------------------------- dump
642
643/// The statements, ready to `#include` into a decode template.
644#[derive(Debug, Default)]
645struct Dump {
646    body: String,
647    depth: usize,
648    next: usize,
649    /// The aggregates on a cycle, each of which gets a function of its
650    /// own — `zd_rec::dump_<position>` — instead of an inline
651    /// expansion, which for a cycle has no end. See
652    /// [`recursive_aggregates`].
653    rec: Vec<Id<TypeNode>>,
654}
655
656impl Dump {
657    fn line(&mut self, text: &str) {
658        for _ in 0..self.depth {
659            self.body.push_str("    ");
660        }
661        self.body.push_str(text);
662        self.body.push('\n');
663    }
664
665    fn open(&mut self, text: &str) {
666        self.line(text);
667        self.depth += 1;
668    }
669
670    fn close(&mut self, text: &str) {
671        self.depth = self.depth.saturating_sub(1);
672        self.line(text);
673    }
674
675    fn raw(&mut self, literal: &str) {
676        let escaped = literal.replace('\\', "\\\\").replace('"', "\\\"");
677        self.line(&format!("zdj_raw(\"{escaped}\");"));
678    }
679
680    /// A distinct name per loop, so a nested sequence does not shadow
681    /// its own outer counter — see `dump.rs::Dump::temporary`.
682    fn temporary(&mut self, stem: &str) -> String {
683        self.next += 1;
684        format!("zd_{stem}_{}", self.next)
685    }
686}
687
688/// Emit the C++ statements that print `sample` as canonical JSON.
689///
690/// # Errors
691/// [`ProbeError`], naming the path in the type.
692pub fn dump_corba_cpp(ast: &Ast, type_: Id<TypeNode>) -> Result<String, ProbeError> {
693    // Before `recursive_aggregates`; see `contract::reject_alias_cycle`.
694    contract::reject_alias_cycle(ast)?;
695    let mut out = Dump {
696        rec: crate::recursion::recursive_aggregates(ast, type_),
697        ..Dump::default()
698    };
699    if !out.rec.is_empty() {
700        // A local struct, because `dump.inc` lands inside `main` and a
701        // plain function cannot be declared there; static members can,
702        // and they see each other regardless of order, which is what a
703        // mutual cycle needs. The bodies are the same expansion the
704        // inline arms write, with every aggregate on a cycle turned
705        // into a call.
706        out.open("struct zd_rec {");
707        for position in 0..out.rec.len() {
708            let id = out.rec[position];
709            let name = corba_cpp_type(ast, id)?;
710            out.open(&format!(
711                "static void dump_{position}(const {name} &zdv) {{"
712            ));
713            dump_aggregate(ast, id, "zdv", &mut out, &format!("<{name}>"))?;
714            out.close("}");
715        }
716        out.close("};");
717    }
718    dump_at(ast, type_, "sample", &mut out, "")?;
719    out.line("zdj_raw(\"\\n\");");
720    Ok(out.body)
721}
722
723/// The C++ name of an aggregate, absolute.
724fn corba_cpp_type(ast: &Ast, type_: Id<TypeNode>) -> Result<String, ProbeError> {
725    let segments = corba_scoped_name(ast, type_).ok_or_else(|| ProbeError::NoLayout {
726        detail: String::from("a type on a cycle has no scoped name"),
727    })?;
728    let mut out = String::new();
729    for segment in &segments {
730        out.push_str("::");
731        out.push_str(&cpp_id(segment));
732    }
733    Ok(out)
734}
735
736/// zerodds-lint: recursion-depth 64 — the type graph's own nesting. There
737/// is no sample here to end the walk, so the cycle is cut instead: an
738/// aggregate named by `crate::recursion::recursive_aggregates` becomes a
739/// call to the function emitted for it and is a leaf of this walk, which
740/// therefore descends an acyclic graph. The typedef arm is finite because
741/// `contract::reject_alias_cycle` at the entry point has refused a chain
742/// that returns to itself. The corpus's deepest nesting is four.
743#[allow(clippy::too_many_lines)]
744fn dump_at(
745    ast: &Ast,
746    type_: Id<TypeNode>,
747    expr: &str,
748    out: &mut Dump,
749    path: &str,
750) -> Result<(), ProbeError> {
751    let node = ast.type_(type_).map_err(|error| ProbeError::NoLayout {
752        detail: format!("{error}"),
753    })?;
754    match &node.kind {
755        TypeKind::Primitive(kind) => {
756            out.line(&primitive_print(*kind, expr, path)?);
757            Ok(())
758        }
759        TypeKind::String {
760            width: CharWidth::Char,
761            ..
762        } => {
763            out.line(&format!("zdj_string((const char *)({expr}));"));
764            Ok(())
765        }
766        TypeKind::String {
767            width: CharWidth::WChar,
768            ..
769        } => {
770            out.line(&format!(
771                "zdj_wstring_corba((const CORBA::WChar *)({expr}));"
772            ));
773            Ok(())
774        }
775        // `to_string` allocates through the ORB's own allocator, which
776        // is why the helper frees it rather than the emitter leaking one
777        // per member.
778        TypeKind::Fixed { .. } => {
779            out.line(&format!("zdj_fixed_corba({expr});"));
780            Ok(())
781        }
782        TypeKind::Enumeration { .. } => {
783            dump_enumeration(ast, type_, expr, out);
784            Ok(())
785        }
786        TypeKind::Alias { aliased } => {
787            let target = aliased.resolved().ok_or_else(|| ProbeError::NoLayout {
788                detail: format!("{path}: the typedef target is unresolved"),
789            })?;
790            dump_at(ast, target, expr, out, path)
791        }
792        TypeKind::Structure { .. } | TypeKind::Valuetype { .. } | TypeKind::Union { .. } => {
793            // A type on a cycle is a call, never an inline expansion —
794            // the expansion of a cycle does not end.
795            if let Some(position) = out.rec.iter().position(|id| *id == type_) {
796                out.line(&format!("zd_rec::dump_{position}({expr});"));
797                return Ok(());
798            }
799            dump_aggregate(ast, type_, expr, out, path)
800        }
801        TypeKind::Array { element, dims } => {
802            let element = element.resolved().ok_or_else(|| ProbeError::NoLayout {
803                detail: format!("{path}: the array element is unresolved"),
804            })?;
805            let counts = contract::array_dimensions(ast, dims, path)?;
806            dump_array(ast, element, expr, out, path, &counts)
807        }
808        TypeKind::Sequence { element, .. } => {
809            let element = element.resolved().ok_or_else(|| ProbeError::NoLayout {
810                detail: format!("{path}: the sequence element is unresolved"),
811            })?;
812            let count = out.temporary("n");
813            let index = out.temporary("i");
814            out.open("{");
815            out.line(&format!("CORBA::ULong {count} = {expr}.length();"));
816            out.raw("[");
817            out.open(&format!(
818                "for (CORBA::ULong {index} = 0; {index} < {count}; {index}++) {{"
819            ));
820            out.line(&format!("if ({index} > 0) {{ zdj_raw(\",\"); }}"));
821            dump_at(
822                ast,
823                element,
824                &format!("{expr}[{index}]"),
825                out,
826                &format!("{path}[]"),
827            )?;
828            out.close("}");
829            out.raw("]");
830            out.close("}");
831            Ok(())
832        }
833        TypeKind::Map {
834            key, value: entry, ..
835        } => {
836            // The same `std::map` the fill subscripts; see the fill's
837            // map arm for why only the opendds column reaches this.
838            let key_type = key.resolved().ok_or_else(|| ProbeError::NoLayout {
839                detail: format!("{path}: the map key type is unresolved"),
840            })?;
841            let value_type = entry.resolved().ok_or_else(|| ProbeError::NoLayout {
842                detail: format!("{path}: the map value type is unresolved"),
843            })?;
844            let first = out.temporary("first");
845            let pair = out.temporary("entry");
846            // An array of pairs and not an object, the same shape every
847            // other probe reads and writes: a JSON object keys on a
848            // string and `map<long, long>` does not. The order is
849            // `std::map`'s, which is also the order OpenDDS put the
850            // entries on the wire.
851            out.open("{");
852            out.line(&format!("int {first} = 1;"));
853            out.raw("[");
854            out.open(&format!("for (const auto &{pair} : {expr}) {{"));
855            out.line(&format!("if (!{first}) {{ zdj_raw(\",\"); }} {first} = 0;"));
856            out.raw("[");
857            dump_at(
858                ast,
859                key_type,
860                &format!("{pair}.first"),
861                out,
862                &format!("{path}[].key"),
863            )?;
864            out.raw(",");
865            dump_at(
866                ast,
867                value_type,
868                &format!("{pair}.second"),
869                out,
870                &format!("{path}[].value"),
871            )?;
872            out.raw("]");
873            out.close("}");
874            out.raw("]");
875            out.close("}");
876            Ok(())
877        }
878        TypeKind::Bitmask { .. } => Err(ProbeError::Unsupported {
879            path: path.to_string(),
880            kind: "a bitmask, which is XTypes 1.3 §7.3.1.2.1.4 and not CORBA IDL",
881        }),
882        TypeKind::Bitset { .. } => Err(ProbeError::Unsupported {
883            path: path.to_string(),
884            kind: "a bitset, which is XTypes 1.3 §7.3.1.2.1.3 and not CORBA IDL",
885        }),
886        other => Err(ProbeError::Unfilled {
887            path: path.to_string(),
888            kind: other.as_str(),
889        }),
890    }
891}
892
893/// One aggregate's dump body — the expansion [`dump_at`] inlines for a
894/// type off every cycle and `zd_rec`'s functions wrap for one on a
895/// cycle.
896///
897/// zerodds-lint: recursion-depth 64 — the type graph's own nesting, below
898/// `dump_at`, which carries the argument: every member goes back through
899/// it, and an aggregate on a cycle is a call to `zd_rec::dump_<n>` there
900/// rather than an expansion here.
901#[allow(clippy::too_many_lines)]
902fn dump_aggregate(
903    ast: &Ast,
904    type_: Id<TypeNode>,
905    expr: &str,
906    out: &mut Dump,
907    path: &str,
908) -> Result<(), ProbeError> {
909    let node = ast.type_(type_).map_err(|error| ProbeError::NoLayout {
910        detail: format!("{error}"),
911    })?;
912    match &node.kind {
913        TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
914            out.open("{");
915            out.line("int zd_first = 1;");
916            out.raw("{");
917            for member in contract::members(ast, type_) {
918                // The key is the IDL name and the read is the C++ one.
919                // A renamed key would make this column disagree with
920                // every other over a member name that is not on the wire.
921                out.line(&format!("zdj_key(&zd_first, \"{}\");", member.name));
922                dump_at(
923                    ast,
924                    member.type_,
925                    &format!("{expr}.{}", cpp_id(&member.name)),
926                    out,
927                    &contract::join(path, &member.name),
928                )?;
929            }
930            out.raw("}");
931            out.close("}");
932            Ok(())
933        }
934        TypeKind::Union { discriminator, .. } => {
935            let discriminator = discriminator
936                .resolved()
937                .ok_or_else(|| ProbeError::NoLayout {
938                    detail: format!("{path}: the discriminator is unresolved"),
939                })?;
940            out.open("{");
941            out.line("int zd_first = 1;");
942            out.raw("{");
943            out.line("zdj_key(&zd_first, \"_d\");");
944            dump_at(
945                ast,
946                discriminator,
947                &format!("{expr}._d()"),
948                out,
949                &format!("{path}._d"),
950            )?;
951            // The width is the discriminator's, not always `long long`:
952            // `union Choice switch (unsigned long long) { case 16045690984833335998: … }`
953            // is a corpus case whose label does not fit a signed 64-bit
954            // constant, and a `switch` cast to `long long` would compare it
955            // wrongly even where the constant is accepted. `discriminator_c_width`
956            // answers both halves at once.
957            let (width, suffix) = contract::discriminator_c_width(ast, discriminator);
958            out.open(&format!("switch (({width})({expr}._d())) {{"));
959            for id in ast.cases_of(type_) {
960                let Ok(case) = ast.case(*id) else { continue };
961                let Ok(member) = ast.member(case.member) else {
962                    continue;
963                };
964                let Some(member_type) = member.type_.resolved() else {
965                    continue;
966                };
967                for label in &case.labels {
968                    match label.expr() {
969                        Some(expression) => match ast.eval_integral(expression) {
970                            Ok(value) => out.line(&format!("case {value}{suffix}:")),
971                            Err(error) => {
972                                return Err(ProbeError::NoLayout {
973                                    detail: format!(
974                                        "{path}: a case label does not evaluate: {error}"
975                                    ),
976                                });
977                            }
978                        },
979                        None => out.line("default:"),
980                    }
981                }
982                out.depth += 1;
983                out.line(&format!("zdj_key(&zd_first, \"{}\");", member.name));
984                dump_at(
985                    ast,
986                    member_type,
987                    &format!("{expr}.{}()", cpp_id(&member.name)),
988                    out,
989                    &contract::join(path, &member.name),
990                )?;
991                out.line("break;");
992                out.depth = out.depth.saturating_sub(1);
993            }
994            out.close("}");
995            out.raw("}");
996            out.close("}");
997            Ok(())
998        }
999        other => Err(ProbeError::NoLayout {
1000            detail: format!("{path}: not an aggregate: {}", other.as_str()),
1001        }),
1002    }
1003}
1004
1005/// zerodds-lint: recursion-depth 8 — one frame per array dimension.
1006/// `counts` drops its head at each frame and the innermost element goes to
1007/// `dump_at`.
1008fn dump_array(
1009    ast: &Ast,
1010    element: Id<TypeNode>,
1011    expr: &str,
1012    out: &mut Dump,
1013    path: &str,
1014    counts: &[usize],
1015) -> Result<(), ProbeError> {
1016    let Some((&count, rest)) = counts.split_first() else {
1017        return dump_at(ast, element, expr, out, path);
1018    };
1019    out.raw("[");
1020    for index in 0..count {
1021        if index > 0 {
1022            out.raw(",");
1023        }
1024        dump_array(
1025            ast,
1026            element,
1027            &format!("{expr}[{index}]"),
1028            out,
1029            &format!("{path}[{index}]"),
1030            rest,
1031        )?;
1032    }
1033    out.raw("]");
1034    Ok(())
1035}
1036
1037/// An enumeration, printed as the enumerator's name.
1038///
1039/// The same `switch` `dump.rs` writes and for the same reason: the value
1040/// is the ORB's, and one that names no enumerator is a finding rather
1041/// than a number that would read as some other member's value.
1042fn dump_enumeration(ast: &Ast, type_: Id<TypeNode>, expr: &str, out: &mut Dump) {
1043    out.open(&format!("switch ((long long)({expr})) {{"));
1044    for id in ast.enumerators_of(type_) {
1045        let Ok(node) = ast.enumerator(*id) else {
1046            continue;
1047        };
1048        let scoped = enumerator_name(ast, type_, &node.name);
1049        out.line(&format!("case (long long){scoped}:"));
1050        out.line(&format!("    zdj_raw(\"\\\"{}\\\"\"); break;", node.name));
1051    }
1052    out.line(&format!(
1053        "default: zdj_enum_unknown((long long)({expr})); break;"
1054    ));
1055    out.close("}");
1056}
1057
1058// ------------------------------------------------------------- helpers
1059
1060/// An enumerator, in the C++ scope the mapping puts it in.
1061///
1062/// CORBA 3.3 part 3 §1.2.5 puts an IDL enumeration's enumerators in the
1063/// **enclosing** scope, not inside a type of their own — so `RED`, and
1064/// `::common::RED` where the enumeration sits in a module. Absolute, so
1065/// a same-named identifier nearer the use site cannot win.
1066///
1067/// Every segment goes through [`cxx`], the enumerator included: measured
1068/// 2026-08-11, omniidl and `tao_idl` generate
1069/// `enum _cxx_class { _cxx_template, _cxx_virtual, _cxx_friend }` for
1070/// the corpus case `identifier-target-keywords`, so a namespace segment
1071/// or an enumerator that is a C++ keyword is renamed wherever it is
1072/// referred to and not only where it is declared. The enumeration's own
1073/// name is not in the result — §1.2.5 leaves it out of the scope — which
1074/// is why this walks `scope` and not the whole scoped name.
1075fn enumerator_name(ast: &Ast, type_: Id<TypeNode>, enumerator: &str) -> String {
1076    let Some(scoped) = corba_scoped_name(ast, type_) else {
1077        return format!("::{}", cpp_id(enumerator));
1078    };
1079    let Some((_, scope)) = scoped.split_last() else {
1080        return format!("::{}", cpp_id(enumerator));
1081    };
1082    let mut out = String::from("::");
1083    for segment in scope {
1084        out.push_str(&cpp_id(segment));
1085        out.push_str("::");
1086    }
1087    out.push_str(&cpp_id(enumerator));
1088    out
1089}
1090
1091/// The C++ literal for one primitive.
1092///
1093/// Floating point goes out with seventeen significant digits, which is
1094/// what a `double` round-trips through.
1095fn primitive_literal(kind: PrimitiveKind, value: &Value, path: &str) -> Result<String, ProbeError> {
1096    Ok(match kind {
1097        PrimitiveKind::Boolean => {
1098            // `1` and `0` rather than `true` and `false`: omniORB's
1099            // `CORBA::Boolean` is an `unsigned char` and TAO's is a
1100            // `bool`, and the integers convert to both.
1101            if contract::boolean(value, path)? {
1102                "1".to_string()
1103            } else {
1104                "0".to_string()
1105            }
1106        }
1107        // `char` is one octet, so a code point above U+00FF is a value
1108        // the type cannot hold; `character_for` refuses it here rather
1109        // than letting the compiler narrow it in silence.
1110        PrimitiveKind::Char => {
1111            let code = contract::character_for(kind, value, path)?;
1112            format!("(CORBA::Char)0x{:02x}", code as u32)
1113        }
1114        // A `CORBA::WChar` is two bytes in omniORB and four in TAO. The
1115        // integer converts to either; a `L'x'` literal would be four
1116        // bytes in both compilers and wrong against omniORB.
1117        PrimitiveKind::WChar => {
1118            let code = contract::character_for(kind, value, path)?;
1119            format!("(CORBA::WChar)0x{:04x}", code as u32)
1120        }
1121        // `int8` and `uint8` are spelled `octet` in the printed IDL —
1122        // Table 31 gives all three size 1 and alignment 1 — so the
1123        // literal is the octet the value occupies, computed here rather
1124        // than left to a narrowing conversion.
1125        PrimitiveKind::Octet | PrimitiveKind::UInt8 | PrimitiveKind::Int8 => {
1126            let integer = contract::integer_for(kind, value, path)?;
1127            #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
1128            let octet = (integer as i64) as u8;
1129            format!("(CORBA::Octet)0x{octet:02x}")
1130        }
1131        PrimitiveKind::Float32 => format!("{:.17e}F", contract::float(value, path)?),
1132        PrimitiveKind::Float64 => format!("{:.17e}", contract::float(value, path)?),
1133        PrimitiveKind::Int64 => {
1134            let n = contract::integer_for(kind, value, path)?;
1135            // C++ has no literal for INT64_MIN either:
1136            // `-9223372036854775808LL` is a positive constant too large
1137            // for `long long`, parsed unsigned and then negated — g++
1138            // warns "integer constant is so large that it is unsigned",
1139            // measured on `integer-extremes` in the omniorb and tao
1140            // columns. The same subtraction `<cstdint>` uses spells the
1141            // value exactly; `fill.rs` already does this.
1142            if n == i128::from(i64::MIN) {
1143                "(-9223372036854775807LL - 1)".to_string()
1144            } else {
1145                format!("{n}LL")
1146            }
1147        }
1148        PrimitiveKind::UInt64 => format!("{}ULL", contract::integer_for(kind, value, path)?),
1149        PrimitiveKind::Float128 => {
1150            return Err(ProbeError::Unsupported {
1151                path: path.to_string(),
1152                kind: "a long double, whose C++ mapping is x87 extended and not §9.3.1.3's binary128",
1153            });
1154        }
1155        _ => format!("{}", contract::integer_for(kind, value, path)?),
1156    })
1157}
1158
1159/// The call that prints one primitive as canonical JSON.
1160fn primitive_print(kind: PrimitiveKind, expr: &str, path: &str) -> Result<String, ProbeError> {
1161    Ok(match kind {
1162        PrimitiveKind::Boolean => format!("zdj_bool((int)({expr}));"),
1163        PrimitiveKind::Char => format!("zdj_char((char)({expr}));"),
1164        PrimitiveKind::WChar => format!("zdj_wchar((unsigned long)({expr}));"),
1165        // The octet came back as the ORB's `CORBA::Octet`, which is
1166        // unsigned. An `int8` member's value is the same octet read
1167        // signed, and printing it unsigned would hand back 251 where the
1168        // sample wrote −5.
1169        PrimitiveKind::Int8 => format!("zdj_i64((long long)(signed char)({expr}));"),
1170        PrimitiveKind::Octet | PrimitiveKind::UInt8 => {
1171            format!("zdj_u64((unsigned long long)(unsigned char)({expr}));")
1172        }
1173        PrimitiveKind::Int16 | PrimitiveKind::Int32 => {
1174            format!("zdj_i64((long long)({expr}));")
1175        }
1176        PrimitiveKind::UInt16 | PrimitiveKind::UInt32 => {
1177            format!("zdj_u64((unsigned long long)({expr}));")
1178        }
1179        // A JSON number is a double, so the low bits of a 64-bit integer
1180        // do not survive one. The canonical form is a string.
1181        PrimitiveKind::Int64 => format!("zdj_i64_string((long long)({expr}));"),
1182        PrimitiveKind::UInt64 => format!("zdj_u64_string((unsigned long long)({expr}));"),
1183        PrimitiveKind::Float32 => format!("zdj_f32((float)({expr}));"),
1184        PrimitiveKind::Float64 => format!("zdj_f64((double)({expr}));"),
1185        PrimitiveKind::Float128 => {
1186            return Err(ProbeError::Unsupported {
1187                path: path.to_string(),
1188                kind: "a long double, whose C++ mapping is x87 extended and not §9.3.1.3's binary128",
1189            });
1190        }
1191    })
1192}
1193
1194fn union_member(ast: &Ast, union: Id<TypeNode>, name: &str) -> Option<Id<TypeNode>> {
1195    contract::union_cases(ast, union)
1196        .into_iter()
1197        .find(|case| case.name == name)
1198        .map(|case| case.type_)
1199}
1200
1201fn union_member_names(ast: &Ast, union: Id<TypeNode>) -> Vec<String> {
1202    contract::union_cases(ast, union)
1203        .into_iter()
1204        .map(|case| case.name)
1205        .collect()
1206}
1207
1208/// A C++ string literal.
1209///
1210/// Every byte outside printable ASCII goes out as a three-digit octal
1211/// escape. Not hex: `\xHH` is greedy in C++ and `"\xc3a"` reads as one
1212/// character with an out-of-range value, so a UTF-8 string followed by a
1213/// hex digit would change meaning. Three-digit octal cannot run on.
1214fn cpp_string(text: &str) -> String {
1215    let mut out = String::from("\"");
1216    for byte in text.as_bytes() {
1217        match byte {
1218            b'"' => out.push_str("\\\""),
1219            b'\\' => out.push_str("\\\\"),
1220            b'\n' => out.push_str("\\n"),
1221            b'\r' => out.push_str("\\r"),
1222            b'\t' => out.push_str("\\t"),
1223            0x20..=0x7E => out.push(char::from(*byte)),
1224            other => {
1225                let _ = write!(out, "\\{other:03o}");
1226            }
1227        }
1228    }
1229    out.push('"');
1230    out
1231}