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}