Skip to main content

zerodds_wire_probe/
corba_java.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The fill and the dump for the OMG IDL-to-Java mapping.
4//!
5//! JacORB 3.9 generates against CORBA 3.3 part 3 §1.5. It is the third
6//! ORB and the only one of the three that is not C++, which is the
7//! reason it is worth having: where omniORB and TAO could share a
8//! mistake through the C++ mapping, a Java witness cannot.
9//!
10//! ```text
11//! sample.x = 1;                        // struct member — §1.5.10, public fields
12//! sample.picked.whole(1);              // union case member — §1.5.11
13//! sample.rows = new int[3];            // sequence and array are both Java arrays
14//! ```
15//!
16//! ## Why this is not a self-oracle
17//!
18//! The statements place values into JacORB's own generated classes and
19//! read them back out; the bytes come from JacORB's `CDROutputStream`
20//! through the generated `Helper.write`. Nothing here decides a wire
21//! question.
22//!
23//! ## What the Java mapping makes of each construct
24//!
25//! | IDL | Java | consequence here |
26//! |---|---|---|
27//! | `octet` | `byte`, **signed** | which is exactly what `int8` wants, and what `uint8` has to mask |
28//! | `char`, `wchar` | `char` | one UTF-16 code unit either way |
29//! | `string`, `wstring` | `java.lang.String` | |
30//! | `sequence<T>`, `T[N]` | a Java array | so a sequence is allocated, not lengthened |
31//! | `enum` | a final class with one constant per enumerator | `Colour.RED`, and `value()` for the ordinal |
32//! | `union` | a class with a modifier per case and `discriminator()` | §1.5.11 |
33//! | `fixed<P,S>` | `java.math.BigDecimal` | §1.5.6 |
34//!
35//! ## The one thing the Java mapping cannot express
36//!
37//! There is no `long double`. §1.5.2 maps every other primitive and
38//! stops there, so a case with one is refused rather than approximated
39//! into a `double` — which would put eight bytes on the wire where the
40//! type says sixteen.
41//!
42//! ## Identifiers JacORB renames
43//!
44//! An IDL module called `package` is not a Java package declaration, so
45//! JacORB emits `_package` instead; the enumeration `class` becomes the
46//! class `_class`. Measured 2026-08-11 over
47//! `tests/interop-corpus/identifier-target-keywords`. Every identifier
48//! this file pastes into an identifier position therefore goes through
49//! [`crate::target_keywords::java`] — and only a Java keyword moves, so
50//! the enumerators `template`, `virtual` and `friend`, which the two C++
51//! front ends do rename, keep their spelling here. The JSON keys the
52//! dump prints and the paths in a [`ProbeError`] stay as the IDL wrote
53//! them.
54
55use crate::contract::{self, Consumed};
56use crate::corba_idl::java_name;
57use crate::encode::ProbeError;
58use crate::target_keywords::java;
59use serde_json::Value;
60use std::fmt::Write as _;
61use zerodds_ast::{Ast, CharWidth, PrimitiveKind, TypeKind, TypeNode};
62use zerodds_ast_core::Id;
63
64/// Where one value goes.
65enum Place {
66    /// A public field, or an array element.
67    Field(String),
68    /// A union case member: `object.name(value)`.
69    Modifier {
70        /// The expression naming the union.
71        object: String,
72        /// The case member's name.
73        name: String,
74    },
75    /// §1.5.11's second modifier, `object.name(discriminator, value)`,
76    /// which a case with more than one label or a `default:` needs.
77    ModifierWithDiscriminator {
78        /// The expression naming the union.
79        object: String,
80        /// The case member's name.
81        name: String,
82        /// The discriminator literal to write beside the value.
83        discriminator: String,
84    },
85}
86
87impl Place {
88    fn assign(&self, literal: &str) -> String {
89        match self {
90            Self::Field(expression) => format!("{expression} = {literal};"),
91            Self::Modifier { object, name } => format!("{object}.{name}({literal});"),
92            Self::ModifierWithDiscriminator {
93                object,
94                name,
95                discriminator,
96            } => format!("{object}.{name}({discriminator}, {literal});"),
97        }
98    }
99}
100
101/// The statements, plus a counter for the temporaries a nested
102/// allocation needs.
103#[derive(Debug, Default)]
104struct Java {
105    statements: String,
106    next: usize,
107    seen: Consumed,
108}
109
110impl Java {
111    fn temporary(&mut self, stem: &str) -> String {
112        self.next += 1;
113        format!("zd_{stem}_{}", self.next)
114    }
115
116    fn line(&mut self, text: &str) {
117        self.statements.push_str(text);
118        self.statements.push('\n');
119    }
120}
121
122/// Emit the Java statements that fill `sample` with `values`.
123///
124/// # Errors
125/// [`ProbeError`], naming the path in the value.
126pub fn fill_corba_java(
127    ast: &Ast,
128    type_: Id<TypeNode>,
129    values: &Value,
130) -> Result<String, ProbeError> {
131    fill_corba_java_recording(ast, type_, values, &mut Consumed::new())
132}
133
134/// As [`fill_corba_java`], recording which scalars of the value were
135/// read.
136///
137/// # Errors
138/// As [`fill_corba_java`].
139pub fn fill_corba_java_recording(
140    ast: &Ast,
141    type_: Id<TypeNode>,
142    values: &Value,
143    seen: &mut Consumed,
144) -> Result<String, ProbeError> {
145    // Before the first hop; see `contract::reject_alias_cycle`.
146    contract::reject_alias_cycle(ast)?;
147    let mut out = Java::default();
148    let root = contract::root_path(ast, type_);
149    let result = fill_at(
150        ast,
151        type_,
152        values,
153        &Place::Field(String::from("sample")),
154        &mut out,
155        &root,
156    );
157    *seen = core::mem::take(&mut out.seen);
158    result?;
159    Ok(out.statements)
160}
161
162/// The Java type of one IDL type, for `new` and for a local.
163///
164/// # Errors
165/// [`ProbeError::Unsupported`] for a construct with no Java binding.
166///
167/// zerodds-lint: recursion-depth 64 — the type graph's own nesting. An
168/// aggregate is a leaf here: a struct, a union, an enumeration and a
169/// valuetype are all spelled by `java_name` and never descended into, so
170/// only the collection and typedef arms recurse. A collection nests as
171/// deeply as the IDL text writes it, and a typedef chain that returns to
172/// itself has been refused by `contract::reject_alias_cycle` at the entry
173/// point.
174fn java_type(ast: &Ast, type_: Id<TypeNode>, path: &str) -> Result<String, ProbeError> {
175    let node = ast.type_(type_).map_err(|error| ProbeError::NoLayout {
176        detail: format!("{error}"),
177    })?;
178    Ok(match &node.kind {
179        TypeKind::Primitive(kind) => java_primitive(*kind, path)?.to_string(),
180        TypeKind::String { .. } => "java.lang.String".to_string(),
181        TypeKind::Fixed { .. } => "java.math.BigDecimal".to_string(),
182        TypeKind::Sequence { element, .. } | TypeKind::Array { element, .. } => {
183            let element = element.resolved().ok_or_else(|| ProbeError::NoLayout {
184                detail: format!("{path}: the element type is unresolved"),
185            })?;
186            format!("{}[]", java_type(ast, element, path)?)
187        }
188        TypeKind::Alias { aliased } => {
189            let target = aliased.resolved().ok_or_else(|| ProbeError::NoLayout {
190                detail: format!("{path}: the typedef target is unresolved"),
191            })?;
192            java_type(ast, target, path)?
193        }
194        TypeKind::Structure { .. }
195        | TypeKind::Union { .. }
196        | TypeKind::Enumeration { .. }
197        | TypeKind::Valuetype { .. } => java_name(ast, type_),
198        // The same refusal the direct-member arms state, so a map as an
199        // array or sequence element names JacORB's grammar gap instead
200        // of falling into the catch-all — `array-1d-map` and
201        // `sequence-map` reached this arm through the element type.
202        TypeKind::Map { .. } => {
203            return Err(ProbeError::Unsupported {
204                path: path.to_string(),
205                kind: "a map, which is IDL 4.2 §7.4.13.4.3 and outside JacORB 3.9's grammar — its idl says `Syntax error` at the keyword, measured on the test host",
206            });
207        }
208        other => {
209            return Err(ProbeError::Unfilled {
210                path: path.to_string(),
211                kind: other.as_str(),
212            });
213        }
214    })
215}
216
217/// zerodds-lint: recursion-depth 128 — the sample's own nesting. Every arm
218/// but the typedef one descends into a strictly smaller part of the value,
219/// so the walk ends because the sample is finite: a recursive type has
220/// finite values, because the sequence that closes its cycle is eventually
221/// empty. The typedef arm keeps the value and is finite because
222/// `contract::reject_alias_cycle` at the entry point has refused a chain
223/// that returns to itself. A value file is `serde_json`'s to parse and its
224/// `RECURSION_LIMIT` is 128.
225#[allow(clippy::too_many_lines)]
226fn fill_at(
227    ast: &Ast,
228    type_: Id<TypeNode>,
229    value: &Value,
230    place: &Place,
231    out: &mut Java,
232    path: &str,
233) -> Result<(), ProbeError> {
234    let node = ast.type_(type_).map_err(|error| ProbeError::NoLayout {
235        detail: format!("{error}"),
236    })?;
237    match &node.kind {
238        TypeKind::Primitive(kind) => {
239            let literal = primitive_literal(*kind, value, path)?;
240            out.seen.scalar(path);
241            let statement = place.assign(&literal);
242            out.line(&statement);
243            Ok(())
244        }
245        // A `wstring` is a `String` too. The units the sample carries
246        // are UTF-16 and a Java `String` *is* UTF-16, so a code point
247        // above the BMP goes in as its surrogate pair without the
248        // emitter arranging one.
249        TypeKind::String { .. } => {
250            let text = contract::text(value, path)?;
251            out.seen.scalar(path);
252            let statement = place.assign(&java_string(text));
253            out.line(&statement);
254            Ok(())
255        }
256        TypeKind::Fixed { .. } => {
257            let text = contract::text(value, path)?;
258            out.seen.scalar(path);
259            let statement =
260                place.assign(&format!("new java.math.BigDecimal({})", java_string(text)));
261            out.line(&statement);
262            Ok(())
263        }
264        TypeKind::Enumeration { .. } => {
265            let (name, _) = contract::enumerator(ast, type_, value, path)?;
266            out.seen.scalar(path);
267            // §1.5.9's constant, whose name is the enumerator's — so it
268            // is renamed exactly when the enumerator is a Java keyword,
269            // which none of the corpus's three are.
270            let statement = place.assign(&format!("{}.{}", java_name(ast, type_), java(&name)));
271            out.line(&statement);
272            Ok(())
273        }
274        TypeKind::Alias { aliased } => {
275            let target = aliased.resolved().ok_or_else(|| ProbeError::NoLayout {
276                detail: format!("{path}: the typedef target is unresolved"),
277            })?;
278            fill_at(ast, target, value, place, out, path)
279        }
280        TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
281            let object = contract::object(value, path)?;
282            let members = contract::members(ast, type_);
283            let declared: Vec<String> = members.iter().map(|member| member.name.clone()).collect();
284            contract::reject_unknown_keys(object, &declared, path)?;
285            // A Java aggregate is a reference, so it is built in a local
286            // and assigned once. That is also what makes a union case
287            // member reachable: the modifier takes a whole object and
288            // there is no reference to descend into.
289            let class = java_type(ast, type_, path)?;
290            let local = out.temporary("obj");
291            out.line(&format!("{class} {local} = new {class}();"));
292            for member in &members {
293                let Some(member_value) = contract::member_value(object, member, path)? else {
294                    continue;
295                };
296                fill_at(
297                    ast,
298                    member.type_,
299                    member_value,
300                    // §1.5.10's public field, under the name Java has it
301                    // by; the path keeps the name the IDL wrote.
302                    &Place::Field(format!("{local}.{}", java(&member.name))),
303                    out,
304                    &contract::join(path, &member.name),
305                )?;
306            }
307            let statement = place.assign(&local);
308            out.line(&statement);
309            Ok(())
310        }
311        TypeKind::Union { discriminator, .. } => {
312            let object = contract::object(value, path)?;
313            let discriminant = contract::discriminator(object, path)?;
314            let class = java_type(ast, type_, path)?;
315            let local = out.temporary("union");
316            out.line(&format!("{class} {local} = new {class}();"));
317            let discriminator = discriminator
318                .resolved()
319                .ok_or_else(|| ProbeError::NoLayout {
320                    detail: format!("{path}: the discriminator is unresolved"),
321                })?;
322            let disc_literal = discriminator_literal(ast, discriminator, discriminant, path)?;
323
324            let mut selected = false;
325            for (name, member_value) in object {
326                if name == "_d" {
327                    continue;
328                }
329                let Some(case) = union_case(ast, type_, name) else {
330                    return Err(ProbeError::Unknown {
331                        path: contract::join(path, name),
332                        declared: union_member_names(ast, type_),
333                    });
334                };
335                contract::reject_null(member_value, &contract::join(path, name))?;
336                selected = true;
337                // §1.5.11 gives a case member one modifier, and a second
338                // taking the discriminator, "if the branch has more than
339                // one case label or is the default". The one-argument
340                // form sets the discriminator to the case's *first*
341                // label — measured in JacORB 3.9's own output — so a
342                // sample whose `_d` is the second label of a case, or a
343                // value that reaches `default:`, needs the second form
344                // or the discriminator on the wire would be the wrong
345                // one.
346                let member_place = if case.explicit_labels == 1 && !case.is_default {
347                    Place::Modifier {
348                        object: local.clone(),
349                        name: java(name).into_owned(),
350                    }
351                } else {
352                    Place::ModifierWithDiscriminator {
353                        object: local.clone(),
354                        name: java(name).into_owned(),
355                        discriminator: disc_literal.clone(),
356                    }
357                };
358                fill_at(
359                    ast,
360                    case.type_,
361                    member_value,
362                    &member_place,
363                    out,
364                    &contract::join(path, name),
365                )?;
366            }
367            if !selected {
368                // A discriminator that selects no member at all. §1.5.11
369                // gives the union `__default(int)` for exactly this, and
370                // there is no other way in — every modifier is attached
371                // to a member.
372                out.line(&format!("{local}.__default({disc_literal});"));
373            }
374            let statement = place.assign(&local);
375            out.line(&statement);
376            Ok(())
377        }
378        TypeKind::Array { element, dims } => {
379            let element = element.resolved().ok_or_else(|| ProbeError::NoLayout {
380                detail: format!("{path}: the array element is unresolved"),
381            })?;
382            let counts = contract::array_dimensions(ast, dims, path)?;
383            let local = out.temporary("arr");
384            let brackets: String = counts.iter().map(|count| format!("[{count}]")).collect();
385            let (declared, allocation) =
386                java_array(&java_type(ast, element, path)?, counts.len(), &brackets);
387            out.line(&format!("{declared} {local} = {allocation};"));
388            fill_array(ast, element, value, &local, out, path, &counts)?;
389            let statement = place.assign(&local);
390            out.line(&statement);
391            Ok(())
392        }
393        TypeKind::Sequence { element, bound } => {
394            let element = element.resolved().ok_or_else(|| ProbeError::NoLayout {
395                detail: format!("{path}: the sequence element is unresolved"),
396            })?;
397            let items = contract::bounded_items(ast, value, *bound, "elements", path)?;
398            let local = out.temporary("seq");
399            let (declared, allocation) = java_array(
400                &java_type(ast, element, path)?,
401                1,
402                &format!("[{}]", items.len()),
403            );
404            out.line(&format!("{declared} {local} = {allocation};"));
405            for (index, item) in items.iter().enumerate() {
406                fill_at(
407                    ast,
408                    element,
409                    item,
410                    &Place::Field(format!("{local}[{index}]")),
411                    out,
412                    &format!("{path}[{index}]"),
413                )?;
414            }
415            let statement = place.assign(&local);
416            out.line(&statement);
417            Ok(())
418        }
419        TypeKind::Bitmask { .. } => Err(ProbeError::Unsupported {
420            path: path.to_string(),
421            kind: "a bitmask, which is XTypes 1.3 §7.3.1.2.1.4 and not CORBA IDL",
422        }),
423        TypeKind::Bitset { .. } => Err(ProbeError::Unsupported {
424            path: path.to_string(),
425            kind: "a bitset, which is XTypes 1.3 §7.3.1.2.1.3 and not CORBA IDL",
426        }),
427        TypeKind::Map { .. } => Err(ProbeError::Unsupported {
428            path: path.to_string(),
429            kind: "a map, which is IDL 4.2 §7.4.13.4.3 and outside JacORB 3.9's grammar — its idl says `Syntax error` at the keyword, measured on the test host",
430        }),
431        other => Err(ProbeError::Unfilled {
432            path: path.to_string(),
433            kind: other.as_str(),
434        }),
435    }
436}
437
438/// zerodds-lint: recursion-depth 8 — one frame per array dimension.
439/// `counts` drops its head at each frame and the innermost element goes to
440/// `fill_at`, which is bounded by the sample.
441#[allow(clippy::too_many_arguments)]
442fn fill_array(
443    ast: &Ast,
444    element: Id<TypeNode>,
445    value: &Value,
446    lvalue: &str,
447    out: &mut Java,
448    path: &str,
449    counts: &[usize],
450) -> Result<(), ProbeError> {
451    let Some((&count, rest)) = counts.split_first() else {
452        return fill_at(
453            ast,
454            element,
455            value,
456            &Place::Field(lvalue.to_string()),
457            out,
458            path,
459        );
460    };
461    let items = contract::array(value, path)?;
462    if items.len() != count {
463        return Err(ProbeError::Mismatch {
464            path: path.to_string(),
465            expected: format!("exactly {count} elements"),
466            found: format!("{}", items.len()),
467        });
468    }
469    for (index, item) in items.iter().enumerate() {
470        fill_array(
471            ast,
472            element,
473            item,
474            &format!("{lvalue}[{index}]"),
475            out,
476            &format!("{path}[{index}]"),
477            rest,
478        )?;
479    }
480    Ok(())
481}
482
483// ---------------------------------------------------------------- dump
484
485/// The statements, ready to paste into a decode driver.
486#[derive(Debug, Default)]
487struct Dump {
488    body: String,
489    depth: usize,
490    next: usize,
491    /// The aggregates on a cycle, which this walk refuses rather than
492    /// expands. See [`crate::recursion::recursive_aggregates`] and the
493    /// arms in [`dump_at`] that read it.
494    rec: Vec<Id<TypeNode>>,
495}
496
497impl Dump {
498    fn line(&mut self, text: &str) {
499        for _ in 0..self.depth {
500            self.body.push_str("    ");
501        }
502        self.body.push_str(text);
503        self.body.push('\n');
504    }
505
506    fn open(&mut self, text: &str) {
507        self.line(text);
508        self.depth += 1;
509    }
510
511    fn close(&mut self, text: &str) {
512        self.depth = self.depth.saturating_sub(1);
513        self.line(text);
514    }
515
516    fn raw(&mut self, literal: &str) {
517        self.line(&format!("Zdj.raw({});", java_string(literal)));
518    }
519
520    fn temporary(&mut self, stem: &str) -> String {
521        self.next += 1;
522        format!("zd_{stem}_{}", self.next)
523    }
524}
525
526/// Emit the Java statements that print `sample` as canonical JSON.
527///
528/// # Errors
529/// [`ProbeError`], naming the path in the type.
530pub fn dump_corba_java(ast: &Ast, type_: Id<TypeNode>) -> Result<String, ProbeError> {
531    // Before `recursive_aggregates`; see `contract::reject_alias_cycle`.
532    contract::reject_alias_cycle(ast)?;
533    let mut out = Dump {
534        rec: crate::recursion::recursive_aggregates(ast, type_),
535        ..Dump::default()
536    };
537    dump_at(ast, type_, "sample", &mut out, "")?;
538    out.line("Zdj.raw(\"\\n\");");
539    Ok(out.body)
540}
541
542/// zerodds-lint: recursion-depth 64 — the type graph's own nesting,
543/// which is finite because the walk never enters a cycle: an aggregate
544/// on one is refused in the two arms below, and a typedef chain that
545/// returns to itself has been refused by `reject_alias_cycle` before the
546/// first hop. Every other arm strictly descends the graph, so the depth
547/// is the IDL's own and the corpus's deepest is four.
548///
549/// Refused and not expanded into a function, as `corba_cpp.rs` does for
550/// the same walk in C++: `dump.inc` lands inside a Java method body,
551/// where a method cannot be declared, and `corba_idl` refuses a
552/// recursive type for this column anyway — "a recursive type, which
553/// CORBA CDR frames its own way". Until this arm existed, this emitter
554/// did not refuse it either: it followed
555/// `A → sequence<B> → B → sequence<A>` until the
556/// stack ran out, which the corpus case `recursive-mutual` reproduces.
557/// The C++ half of the same file carries the `rec` machinery; this half
558/// was left behind when it was added.
559#[allow(clippy::too_many_lines)]
560fn dump_at(
561    ast: &Ast,
562    type_: Id<TypeNode>,
563    expr: &str,
564    out: &mut Dump,
565    path: &str,
566) -> Result<(), ProbeError> {
567    let node = ast.type_(type_).map_err(|error| ProbeError::NoLayout {
568        detail: format!("{error}"),
569    })?;
570    match &node.kind {
571        TypeKind::Primitive(kind) => {
572            out.line(&primitive_print(*kind, expr, path)?);
573            Ok(())
574        }
575        TypeKind::String {
576            width: CharWidth::Char,
577            ..
578        } => {
579            out.line(&format!("Zdj.string({expr});"));
580            Ok(())
581        }
582        TypeKind::String {
583            width: CharWidth::WChar,
584            ..
585        } => {
586            out.line(&format!("Zdj.wstring({expr});"));
587            Ok(())
588        }
589        TypeKind::Fixed { .. } => {
590            out.line(&format!("Zdj.fixed({expr});"));
591            Ok(())
592        }
593        TypeKind::Enumeration { .. } => {
594            dump_enumeration(ast, type_, expr, out);
595            Ok(())
596        }
597        TypeKind::Alias { aliased } => {
598            let target = aliased.resolved().ok_or_else(|| ProbeError::NoLayout {
599                detail: format!("{path}: the typedef target is unresolved"),
600            })?;
601            dump_at(ast, target, expr, out, path)
602        }
603        TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
604            if out.rec.contains(&type_) {
605                return Err(recursive_type(path));
606            }
607            let first = out.temporary("first");
608            out.open("{");
609            out.line(&format!("boolean[] {first} = {{ true }};"));
610            out.raw("{");
611            for member in contract::members(ast, type_) {
612                // The key is the IDL name and the read is the Java one.
613                out.line(&format!("Zdj.key({first}, \"{}\");", member.name));
614                dump_at(
615                    ast,
616                    member.type_,
617                    &format!("{expr}.{}", java(&member.name)),
618                    out,
619                    &contract::join(path, &member.name),
620                )?;
621            }
622            out.raw("}");
623            out.close("}");
624            Ok(())
625        }
626        TypeKind::Union { discriminator, .. } => {
627            if out.rec.contains(&type_) {
628                return Err(recursive_type(path));
629            }
630            let discriminator = discriminator
631                .resolved()
632                .ok_or_else(|| ProbeError::NoLayout {
633                    detail: format!("{path}: the discriminator is unresolved"),
634                })?;
635            let first = out.temporary("first");
636            out.open("{");
637            out.line(&format!("boolean[] {first} = {{ true }};"));
638            out.raw("{");
639            out.line(&format!("Zdj.key({first}, \"_d\");"));
640            dump_at(
641                ast,
642                discriminator,
643                &format!("{expr}.discriminator()"),
644                out,
645                &format!("{path}._d"),
646            )?;
647            // What the `switch` selects on. §1.5.9 maps an enumeration to
648            // a class whose ordinal is `value()`, and §1.5.2 maps
649            // `boolean` to Java's own — neither is a numeric type a cast
650            // reaches: `javac` refused both, "incompatible types: Colour
651            // cannot be converted to int" on `union-discriminator-enum`
652            // and "boolean cannot be converted to int" on
653            // `union-discriminator-boolean`, measured in the jacorb
654            // column. The labels come out of `eval_integral` (TRUE is 1,
655            // FALSE is 0, an enumerator its ordinal), so the selector has
656            // to reach the same integers.
657            let resolved = contract::resolve_alias(ast, discriminator);
658            let selector = match ast.type_(resolved).map(|node| node.kind.clone()) {
659                Ok(TypeKind::Enumeration { .. }) => {
660                    format!("{expr}.discriminator().value()")
661                }
662                Ok(TypeKind::Primitive(PrimitiveKind::Boolean)) => {
663                    format!("{expr}.discriminator() ? 1 : 0")
664                }
665                _ => format!("(int)({expr}.discriminator())"),
666            };
667            out.open(&format!("switch ({selector}) {{"));
668            for id in ast.cases_of(type_) {
669                let Ok(case) = ast.case(*id) else { continue };
670                let Ok(member) = ast.member(case.member) else {
671                    continue;
672                };
673                let Some(member_type) = member.type_.resolved() else {
674                    continue;
675                };
676                for label in &case.labels {
677                    match label.expr() {
678                        Some(expression) => match ast.eval_integral(expression) {
679                            Ok(value) => out.line(&format!("case {}:", value as i32)),
680                            Err(error) => {
681                                return Err(ProbeError::NoLayout {
682                                    detail: format!(
683                                        "{path}: a case label does not evaluate: {error}"
684                                    ),
685                                });
686                            }
687                        },
688                        None => out.line("default:"),
689                    }
690                }
691                out.depth += 1;
692                out.line(&format!("Zdj.key({first}, \"{}\");", member.name));
693                dump_at(
694                    ast,
695                    member_type,
696                    &format!("{expr}.{}()", java(&member.name)),
697                    out,
698                    &contract::join(path, &member.name),
699                )?;
700                out.line("break;");
701                out.depth = out.depth.saturating_sub(1);
702            }
703            out.close("}");
704            out.raw("}");
705            out.close("}");
706            Ok(())
707        }
708        TypeKind::Array { element, dims } => {
709            let element = element.resolved().ok_or_else(|| ProbeError::NoLayout {
710                detail: format!("{path}: the array element is unresolved"),
711            })?;
712            let counts = contract::array_dimensions(ast, dims, path)?;
713            dump_array(ast, element, expr, out, path, &counts)
714        }
715        TypeKind::Sequence { element, .. } => {
716            let element = element.resolved().ok_or_else(|| ProbeError::NoLayout {
717                detail: format!("{path}: the sequence element is unresolved"),
718            })?;
719            let index = out.temporary("i");
720            out.open("{");
721            out.raw("[");
722            out.open(&format!(
723                "for (int {index} = 0; {index} < {expr}.length; {index}++) {{"
724            ));
725            out.line(&format!("if ({index} > 0) {{ Zdj.raw(\",\"); }}"));
726            dump_at(
727                ast,
728                element,
729                &format!("{expr}[{index}]"),
730                out,
731                &format!("{path}[]"),
732            )?;
733            out.close("}");
734            out.raw("]");
735            out.close("}");
736            Ok(())
737        }
738        TypeKind::Bitmask { .. } => Err(ProbeError::Unsupported {
739            path: path.to_string(),
740            kind: "a bitmask, which is XTypes 1.3 §7.3.1.2.1.4 and not CORBA IDL",
741        }),
742        TypeKind::Bitset { .. } => Err(ProbeError::Unsupported {
743            path: path.to_string(),
744            kind: "a bitset, which is XTypes 1.3 §7.3.1.2.1.3 and not CORBA IDL",
745        }),
746        TypeKind::Map { .. } => Err(ProbeError::Unsupported {
747            path: path.to_string(),
748            kind: "a map, which is IDL 4.2 §7.4.13.4.3 and outside JacORB 3.9's grammar — its idl says `Syntax error` at the keyword, measured on the test host",
749        }),
750        other => Err(ProbeError::Unfilled {
751            path: path.to_string(),
752            kind: other.as_str(),
753        }),
754    }
755}
756
757/// The refusal for an aggregate on a cycle, in the words `corba_idl`
758/// uses for the same type in the same column.
759fn recursive_type(path: &str) -> ProbeError {
760    ProbeError::Unsupported {
761        path: path.to_string(),
762        kind: "a recursive type, which CORBA CDR frames its own way — the same refusal \
763               `corba_idl` makes, said here because the expansion of a cycle does not end",
764    }
765}
766
767/// zerodds-lint: recursion-depth 8 — one frame per array dimension.
768/// `counts` is the dimension list `contract::array_dimensions` built and
769/// each frame drops its head, so the walk ends at the empty slice; the
770/// innermost element goes to [`dump_at`], which is bounded separately.
771fn dump_array(
772    ast: &Ast,
773    element: Id<TypeNode>,
774    expr: &str,
775    out: &mut Dump,
776    path: &str,
777    counts: &[usize],
778) -> Result<(), ProbeError> {
779    let Some((&count, rest)) = counts.split_first() else {
780        return dump_at(ast, element, expr, out, path);
781    };
782    out.raw("[");
783    for index in 0..count {
784        if index > 0 {
785            out.raw(",");
786        }
787        dump_array(
788            ast,
789            element,
790            &format!("{expr}[{index}]"),
791            out,
792            &format!("{path}[{index}]"),
793            rest,
794        )?;
795    }
796    out.raw("]");
797    Ok(())
798}
799
800/// An enumeration, printed as the enumerator's name.
801///
802/// §1.5.9 makes the generated class carry `value()`, the ordinal, and
803/// one constant per enumerator. The switch is over the ordinal so that a
804/// value naming no enumerator reaches the default arm rather than
805/// throwing out of `from_int`.
806///
807/// The `int` constant is `_` and then the enumerator's name, which is
808/// §1.5.9's spelling and not JacORB's invention. Where the enumerator is
809/// itself a Java keyword the two underscores stack — `__new` for `new`,
810/// because the object constant is already `_new`. Composed rather than
811/// measured: none of the corpus's enumerators is a Java keyword.
812fn dump_enumeration(ast: &Ast, type_: Id<TypeNode>, expr: &str, out: &mut Dump) {
813    let class = java_name(ast, type_);
814    out.open(&format!("switch ({expr}.value()) {{"));
815    for id in ast.enumerators_of(type_) {
816        let Ok(node) = ast.enumerator(*id) else {
817            continue;
818        };
819        out.line(&format!("case {class}._{}:", java(&node.name)));
820        out.line(&format!("    Zdj.raw(\"\\\"{}\\\"\"); break;", node.name));
821    }
822    out.line(&format!("default: Zdj.enumUnknown({expr}.value()); break;"));
823    out.close("}");
824}
825
826// ------------------------------------------------------------- helpers
827
828/// The declared type and the `new` expression for an array of
829/// `element`.
830///
831/// Java puts the sizes on the **innermost** dimensions and leaves the
832/// element's own dimensions empty: an array of two `int[]` is
833/// `new int[2][]` and not `new int[][2]`, which is a syntax error. The
834/// element type's trailing brackets therefore have to move behind the
835/// sizes rather than in front of them — `sequence-of-array` and the
836/// three cases beside it were the ones that did not compile.
837fn java_array(element: &str, depth: usize, sizes: &str) -> (String, String) {
838    let base = element.trim_end_matches("[]");
839    let suffix = &element[base.len()..];
840    let declared = format!("{base}{}{suffix}", "[]".repeat(depth));
841    (declared, format!("new {base}{sizes}{suffix}"))
842}
843
844/// The Java type §1.5.2 gives one primitive.
845fn java_primitive(kind: PrimitiveKind, path: &str) -> Result<&'static str, ProbeError> {
846    Ok(match kind {
847        PrimitiveKind::Boolean => "boolean",
848        // `int8` and `uint8` are spelled `octet` in the printed IDL, and
849        // §1.5.2 makes an `octet` a signed `byte`.
850        PrimitiveKind::Octet | PrimitiveKind::Int8 | PrimitiveKind::UInt8 => "byte",
851        PrimitiveKind::Char | PrimitiveKind::WChar => "char",
852        PrimitiveKind::Int16 | PrimitiveKind::UInt16 => "short",
853        PrimitiveKind::Int32 | PrimitiveKind::UInt32 => "int",
854        PrimitiveKind::Int64 | PrimitiveKind::UInt64 => "long",
855        PrimitiveKind::Float32 => "float",
856        PrimitiveKind::Float64 => "double",
857        PrimitiveKind::Float128 => {
858            return Err(ProbeError::Unsupported {
859                path: path.to_string(),
860                kind: "a long double, which the IDL-to-Java mapping §1.5.2 has no type for",
861            });
862        }
863    })
864}
865
866/// The Java literal for one primitive.
867///
868/// Java has no unsigned integers, so an `unsigned short` is a `short`
869/// and an `unsigned long` an `int` — §1.5.2 says so outright. The
870/// literal is therefore the same bit pattern read signed, computed here
871/// rather than left to a narrowing cast the compiler would refuse.
872fn primitive_literal(kind: PrimitiveKind, value: &Value, path: &str) -> Result<String, ProbeError> {
873    Ok(match kind {
874        PrimitiveKind::Boolean => contract::boolean(value, path)?.to_string(),
875        PrimitiveKind::Char | PrimitiveKind::WChar => {
876            let code = contract::character_for(kind, value, path)?;
877            format!("(char)0x{:04x}", code as u32)
878        }
879        PrimitiveKind::Octet | PrimitiveKind::Int8 | PrimitiveKind::UInt8 => {
880            let integer = contract::integer_for(kind, value, path)?;
881            #[allow(clippy::cast_possible_truncation)]
882            let octet = (integer as i64) as u8;
883            format!("(byte)0x{octet:02x}")
884        }
885        PrimitiveKind::Int16 | PrimitiveKind::UInt16 => {
886            let integer = contract::integer_for(kind, value, path)?;
887            #[allow(clippy::cast_possible_truncation)]
888            let bits = (integer as i64) as u16;
889            format!("(short)0x{bits:04x}")
890        }
891        PrimitiveKind::Int32 | PrimitiveKind::UInt32 => {
892            let integer = contract::integer_for(kind, value, path)?;
893            #[allow(clippy::cast_possible_truncation)]
894            let bits = (integer as i64) as u32;
895            format!("0x{bits:08x}")
896        }
897        PrimitiveKind::Int64 | PrimitiveKind::UInt64 => {
898            let integer = contract::integer_for(kind, value, path)?;
899            #[allow(clippy::cast_possible_truncation)]
900            let bits = integer as u64;
901            format!("0x{bits:016x}L")
902        }
903        // `Float.intBitsToFloat` and not a decimal literal: the sample's
904        // value is exact and a decimal that the Java compiler rounds
905        // differently from `contract::float` would put different bytes
906        // on the wire than every other probe.
907        PrimitiveKind::Float32 => {
908            #[allow(clippy::cast_possible_truncation)]
909            let bits = (contract::float(value, path)? as f32).to_bits();
910            format!("Float.intBitsToFloat(0x{bits:08x})")
911        }
912        PrimitiveKind::Float64 => {
913            let bits = contract::float(value, path)?.to_bits();
914            format!("Double.longBitsToDouble(0x{bits:016x}L)")
915        }
916        PrimitiveKind::Float128 => {
917            return Err(ProbeError::Unsupported {
918                path: path.to_string(),
919                kind: "a long double, which the IDL-to-Java mapping §1.5.2 has no type for",
920            });
921        }
922    })
923}
924
925/// The literal a union's discriminator takes in `__set_discriminator`.
926fn discriminator_literal(
927    ast: &Ast,
928    discriminator: Id<TypeNode>,
929    value: &Value,
930    path: &str,
931) -> Result<String, ProbeError> {
932    let resolved = contract::resolve_alias(ast, discriminator);
933    let node = ast.type_(resolved).map_err(|error| ProbeError::NoLayout {
934        detail: format!("{error}"),
935    })?;
936    match &node.kind {
937        TypeKind::Enumeration { .. } => {
938            let (name, _) = contract::enumerator(ast, resolved, value, path)?;
939            Ok(format!("{}.{}", java_name(ast, resolved), java(&name)))
940        }
941        TypeKind::Primitive(kind) => primitive_literal(*kind, value, path),
942        other => Err(ProbeError::Unfilled {
943            path: path.to_string(),
944            kind: other.as_str(),
945        }),
946    }
947}
948
949/// The call that prints one primitive as canonical JSON.
950///
951/// Every unsigned type arrives in a signed Java one, so the sign has to
952/// be undone before it is printed — an `unsigned long` of 0xDEADBEEF is
953/// an `int` of −559038737 in Java and the canonical form wants
954/// 3735928559.
955fn primitive_print(kind: PrimitiveKind, expr: &str, path: &str) -> Result<String, ProbeError> {
956    Ok(match kind {
957        PrimitiveKind::Boolean => format!("Zdj.bool({expr});"),
958        PrimitiveKind::Char => format!("Zdj.ch({expr});"),
959        PrimitiveKind::WChar => format!("Zdj.wch({expr});"),
960        PrimitiveKind::Int8 => format!("Zdj.i64((long)({expr}));"),
961        PrimitiveKind::Octet | PrimitiveKind::UInt8 => {
962            format!("Zdj.u64((long)({expr}) & 0xFFL);")
963        }
964        PrimitiveKind::Int16 => format!("Zdj.i64((long)({expr}));"),
965        PrimitiveKind::UInt16 => format!("Zdj.u64((long)({expr}) & 0xFFFFL);"),
966        PrimitiveKind::Int32 => format!("Zdj.i64((long)({expr}));"),
967        PrimitiveKind::UInt32 => format!("Zdj.u64((long)({expr}) & 0xFFFFFFFFL);"),
968        PrimitiveKind::Int64 => format!("Zdj.i64String({expr});"),
969        // No wider Java integer to widen into, so the unsigned decimal
970        // is produced by `Long.toUnsignedString` inside the helper.
971        PrimitiveKind::UInt64 => format!("Zdj.u64String({expr});"),
972        PrimitiveKind::Float32 => format!("Zdj.f32({expr});"),
973        PrimitiveKind::Float64 => format!("Zdj.f64({expr});"),
974        PrimitiveKind::Float128 => {
975            return Err(ProbeError::Unsupported {
976                path: path.to_string(),
977                kind: "a long double, which the IDL-to-Java mapping §1.5.2 has no type for",
978            });
979        }
980    })
981}
982
983/// One case of a union, with what §1.5.11 needs to know about its
984/// labels.
985struct UnionCase {
986    /// The case member's type.
987    type_: Id<TypeNode>,
988    /// How many `case <value>:` labels it carries.
989    explicit_labels: usize,
990    /// Whether it also carries `default:`.
991    is_default: bool,
992}
993
994fn union_case(ast: &Ast, union: Id<TypeNode>, name: &str) -> Option<UnionCase> {
995    for id in ast.cases_of(union) {
996        let case = ast.case(*id).ok()?;
997        let member = ast.member(case.member).ok()?;
998        if member.name != name {
999            continue;
1000        }
1001        return Some(UnionCase {
1002            type_: member.type_.resolved()?,
1003            explicit_labels: case
1004                .labels
1005                .iter()
1006                .filter(|label| label.expr().is_some())
1007                .count(),
1008            is_default: case.labels.iter().any(|label| label.expr().is_none()),
1009        });
1010    }
1011    None
1012}
1013
1014fn union_member_names(ast: &Ast, union: Id<TypeNode>) -> Vec<String> {
1015    contract::union_cases(ast, union)
1016        .into_iter()
1017        .map(|case| case.name)
1018        .collect()
1019}
1020
1021/// A Java string literal, with every non-ASCII character escaped.
1022///
1023/// `\uXXXX` and not raw UTF-8: `javac` reads the source in the platform
1024/// encoding unless told otherwise, and a probe whose string depended on
1025/// a locale would produce different bytes on two machines.
1026fn java_string(text: &str) -> String {
1027    let mut out = String::from("\"");
1028    for ch in text.chars() {
1029        match ch {
1030            '"' => out.push_str("\\\""),
1031            '\\' => out.push_str("\\\\"),
1032            '\n' => out.push_str("\\n"),
1033            '\r' => out.push_str("\\r"),
1034            '\t' => out.push_str("\\t"),
1035            other if other.is_ascii_graphic() || other == ' ' => out.push(other),
1036            other => {
1037                let mut buffer = [0u16; 2];
1038                for unit in other.encode_utf16(&mut buffer) {
1039                    let _ = write!(out, "\\u{unit:04x}");
1040                }
1041            }
1042        }
1043    }
1044    out.push('"');
1045    out
1046}