Skip to main content

zerodds_lang_rules/adapter/
java.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The Java CST as the other end of a rule. Spec §3.2.
4//!
5//! The counterpart of [`crate::adapter::c`], which does the same for C,
6//! and of [`crate::adapter::cst`], which does it for Rust. Reading two of
7//! them side by side is the point: where they differ, the difference is
8//! the language's, and it is written down here rather than spread through
9//! a renderer.
10//!
11//! Values are resolved from the model, never from the overlay — the same
12//! rule the other two materialisers follow and for the same reason
13//! (spec §2.3).
14//!
15//! ## The four things Java forces that C does not
16//!
17//! 1. **No unsigned integer.** Every Java integer is signed, so the width
18//!    of the IDL type cannot decide the Java type. What decides it is what
19//!    the field has to *hold*: `unsigned short` takes an `int` because
20//!    65535 is not a `short`, and `unsigned long` takes a `long` because
21//!    4294967295 is not an `int`. Only `unsigned long long` has no type
22//!    wide enough, and it takes a `long` holding the **bit pattern**. The
23//!    table below says which of the two every unsigned type is.
24//! 2. **Arrays instead of declared collection types.** C has to declare a
25//!    named struct for `sequence<long>`; Java writes `int[]`. So there is
26//!    no structural-name machinery here and no forward declaration — a
27//!    Java class may name a class declared after it — but
28//!    [`ordered_types`] is still what the walk uses, so two runs over one
29//!    model produce the same file.
30//! 3. **One public top-level type per file.** Everything nests inside one
31//!    outer class; see "Modules" below.
32//! 4. **No typedef.** An IDL `typedef` has no Java declaration at all. It
33//!    is not dropped: the member takes the target's Java type and the
34//!    outer class carries a comment naming the alias and what it resolved
35//!    to.
36//!
37//! ## The integer mapping
38//!
39//! | IDL | Java | what the field holds |
40//! |---|---|---|
41//! | `boolean` | `boolean` | |
42//! | `char` | `char` | one code unit; the wire form is one octet |
43//! | `wchar` | `char` | one UTF-16 code unit |
44//! | `int8` | `byte` | a number, -128..127 |
45//! | `octet` | `byte` | a bit pattern, which the codec writes as the number 0..255 |
46//! | `uint8` | `int` | a **number**, 0..255 |
47//! | `short` | `short` | a number |
48//! | `unsigned short` | `int` | a **number**, 0..65535 |
49//! | `long` | `int` | a number |
50//! | `unsigned long` | `long` | a **number**, 0..4294967295 |
51//! | `long long` | `long` | a number |
52//! | `unsigned long long` | `long` | a **bit pattern**, not a number |
53//! | `float` | `float` | |
54//! | `double` | `double` | |
55//! | `long double` | `org.zerodds.wire.Float128` | never a `double` |
56//!
57//! The unsigned types, stated once more because this is the decision
58//! that reaches the wire: `uint8` and `unsigned short` are numbers in an
59//! `int`; `unsigned long` is a number in a `long`; `unsigned long long`
60//! is a pattern in a `long`, and `writeU64(-1L)` writes
61//! `ffffffffffffffff`, which is 18446744073709551615. `octet` is the
62//! exception, and follows the OMG mapping.
63//!
64//! This is not a choice made here alone. `runtime/java/.../Writer.java`
65//! had to make it first, for its own arguments: `writeU8(int value)`
66//! says "a *number* in [0, 255], not a pattern: the argument is an `int`
67//! because Java's `byte` is signed, and 200 arriving as -56 would be a
68//! caller who has already lost the value"; `writeU32(long value)` says
69//! the same of 3735928559; `writeU64(long pattern)` says the opposite,
70//! because there is no wider type. The generated code calls those
71//! methods, so a field typed against a different rule would either not
72//! compile or arrive already wrong.
73//!
74//! **`octet` is a `byte`.** CORBA 3.3 part 3 §1.5.2 maps `octet` to `byte`,
75//! the JacORB column measured in `crates/wire-probe/src/corba_java.rs`
76//! does the same, and DDS-XTypes 1.3 §7.5.1 reuses that mapping for the
77//! Plain Language Binding. A signed `byte` holds 200 as -56; the codec
78//! says so at both ends: `Writer.writeU8(byte)` writes the number the
79//! eight bits are and the reader's `(byte) readU8()` keeps them, so the
80//! wire form is unchanged. `uint8` has no OMG mapping and stays a number
81//! in an `int`.
82//!
83//! ## The shapes
84//!
85//! | IDL | Java |
86//! |---|---|
87//! | `string`, `wstring` | `java.lang.String` |
88//! | `fixed<P,S>` | `java.math.BigDecimal` |
89//! | `sequence<T>`, `T[N]` | `T[]` |
90//! | `T[N][M]` | `T[][]` |
91//! | `map<K,V>` | `ZdMap_k_v`, a `java.util.LinkedHashMap` of the boxed types (`ZdEntry_k_v[]` for a key Java compares by identity) |
92//! | `union` | a class with `_d` and one field per case |
93//! | `enum` | a class with one instance per enumerator, `value()` and `from_int` (CORBA 3.3 part 3 §1.5.9) |
94//! | `bitmask` | the unsigned holder of its bound, beside constants |
95//! | `bitset` | a class of one field per bitfield |
96//! | `@optional T m` | the boxed `T`, `null` when absent |
97//! | `@external T m` | the boxed `T` for a primitive, the reference otherwise |
98//! | `typedef` | nothing; a comment, and the target's type at each use |
99//!
100//! ### `fixed<P,S>` is a `BigDecimal`
101//!
102//! §1.5.6 of the OMG mapping says `java.math.BigDecimal`, and the JacORB
103//! column follows it. So does this one. The digits a fixed-point value
104//! carries on the wire come from the type (`fixed<P,S>`), and a
105//! `BigDecimal` keeps its scale, so `new BigDecimal("0.00")` writes the
106//! digits `0.00` has; `Writer.writeFixed` takes the decimal text, which
107//! the codec gets from `toPlainString()`, and `Reader.readFixed` hands
108//! back text the codec turns into a `BigDecimal`. What a `BigDecimal` does
109//! not keep is leading zeroes (`precision()` of `0.00` is 1), so the
110//! `FixedDigitCount::ValueStringDigits` axis, which counts the digits of
111//! the text a caller wrote, sees the normalised text.
112//!
113//! ### The collections
114//!
115//! A `sequence<T>` and an IDL array are both a Java `T[]`, which is what
116//! §1.5.7 does and what the JacORB column records ("a Java array — so a
117//! sequence is allocated, not lengthened"). A multi-dimensional array is
118//! a nested Java array, one `[]` per dimension.
119//!
120//! A `map<K,V>` is a `java.util.Map` (XTypes 1.3 §7.5.1.3.5): a generated
121//! class extending `LinkedHashMap` of the boxed key and value types, so
122//! that the order the entries were put in is the order they go on the wire
123//! in; `MapEntryOrder` sorts them when asked to. The codec reads and
124//! writes the entries in the generated entry class, in order
125//! (`ZdMap….entries()` and `ZdMap….of(entries)`), and the class carries
126//! the operations of Table 47 that Java does not have. A key a
127//! `java.util.Map` would compare by identity (a structure, a sequence)
128//! keeps the array of entries.
129//!
130//! ### `@optional` is a boxed type and `null`
131//!
132//! One field, not a field beside a `boolean`. A `boolean _has_m` and an
133//! `m` are two things that can disagree, and nothing in the type system
134//! stops them; `null` cannot disagree with itself. `@optional long m`
135//! therefore takes `java.lang.Long`, because a primitive `long` has no
136//! value that means absent — which is the case that decides this, and the
137//! reason C needs the parallel flag it has.
138//!
139//! ### An enumeration is a class of instances
140//!
141//! CORBA 3.3 part 3 §1.5.9, which DDS-XTypes 1.3 §7.5.1 reuses: a final
142//! class with one `public static final` instance per enumerator, the
143//! `int` constants `_NAME`, `value()` and `from_int`. A member of the
144//! type is an instance of the class and the codec writes `value()`.
145//!
146//! The JacORB column records the same shape. `from_int` refuses a value
147//! no enumerator has; the codec never meets that, because it reads a
148//! value through the chain of enumerators and applies the position's
149//! try-construct behaviour to one it does not know (Table 18), and
150//! assigns the instance of the enumerator it found.
151//!
152//! ### Modules are nested classes, in one file
153//!
154//! Not packages. Two facts decide it. Java requires the public top-level
155//! type of a file to be named after the file, so real packages mean many
156//! files; and this function returns `Vec<java::TypeDecl>` with no file
157//! boundary in it, so a package-per-module mapping could not be expressed
158//! by the signature at all — the package would be lost between here and
159//! the renderer, and `module a { struct P; }` and `module b { struct P; }`
160//! would collide in one scope. That collision is the defect
161//! [`MaterialiseError::NoModule`] was added for in the Rust column.
162//!
163//! The consumer decides it the same way: `tools/wireprobe` compiles one
164//! probe per corpus case, and `runtime/java/run-tests.sh` invokes
165//! `javac --release 8 -Xlint:all,-options -Werror` over a directory of
166//! sources. One generated file with one outer class is one `javac`
167//! argument; it also keeps `-Xlint:auxiliaryclass` quiet, which a file of
168//! several top-level classes would not.
169//!
170//! So everything nests inside [`OUTER`], each IDL module becomes a
171//! `public static final class` of that name, and every reference to a
172//! generated type is written from [`OUTER`] down — `ZdTypes.a.P`. The
173//! qualification is not decoration: JLS §6.5.2 resolves the first
174//! segment of a qualified name to a *variable* before a type, so a field
175//! named `common` in a class that also names the type `common.util.Point`
176//! would be a compile error that only some corpora would show.
177//!
178//! ### Identifiers
179//!
180//! Through [`zerodds_lang_cst::java::escape_keyword`], which appends an
181//! underscore: the module `package` becomes the class `package_` and the
182//! enumeration `class` becomes `class_`. `crates/wire-probe/src/
183//! target_keywords.rs` prefixes one instead — `_package`, `_class` —
184//! because that is what JacORB was measured to emit over
185//! `tests/interop-corpus/identifier-target-keywords`. Both avoid the
186//! keyword and neither reaches the wire; this crate does not depend on
187//! `zerodds-wire-probe`, and the one CST spelling keeps the four
188//! generated columns' identifiers consistent with each other rather than
189//! with one ORB. Only a Java keyword moves, so the enumerators
190//! `template`, `virtual` and `friend` keep their spelling here as they do
191//! there.
192//!
193//! ## What is refused
194//!
195//! `any`, `ValueBase` and an object reference have no Java data form
196//! here and are named as [`MaterialiseError::NoLanguageForm`] rather than
197//! left out of the class. A generator that leaves a member out emits a
198//! type missing a field and reports success, which is the defect class
199//! this whole file is arranged against.
200
201use crate::adapter::c::ordered_types;
202use crate::adapter::constant;
203use crate::adapter::cst::{MaterialiseError, no_spelling_collision};
204use crate::apply::Applied;
205use crate::verbatim;
206use seesaw_tgg::ident::GhostId;
207use zerodds_ast::{Ast, CharWidth, ModuleNode, PrimitiveKind, TypeKind, TypeNode};
208use zerodds_ast_core::{Id, NodeId};
209use zerodds_lang_cst::common::{Doc, Ident, Literal, Path};
210use zerodds_lang_cst::java;
211
212/// The class every generated declaration nests inside.
213///
214/// One public top-level type per file is a Java rule, and this is that
215/// type. A generated IDL type named the same is refused rather than
216/// nested inside it: JLS §8.1 forbids a class sharing the simple name of
217/// an enclosing one, and the file would not compile.
218pub const OUTER: &str = "ZdTypes";
219
220/// The runtime class an IDL `long double` takes.
221///
222/// `org.zerodds.wire.Float128` and never a `double`: Java has no
223/// 128-bit floating type, and a `double` member would put eight bytes on
224/// the wire where the type says sixteen. It is the one primitive the OMG
225/// mapping has no answer for — §1.5.2 maps the others and stops — which
226/// is why `crates/wire-probe/src/corba_java.rs` refuses a case carrying
227/// one rather than approximating it.
228const FLOAT128: &[&str] = &["org", "zerodds", "wire", "Float128"];
229
230/// `java.lang.String`, which four IDL types share.
231const STRING: &[&str] = &["java", "lang", "String"];
232
233/// `fixed<P,S>` is a `java.math.BigDecimal` (CORBA 3.3 part 3 §1.5.6).
234const BIG_DECIMAL: &[&str] = &["java", "math", "BigDecimal"];
235
236/// The interface an unbounded sequence is under [`JavaMapping::Psm`].
237const COLLECTION: &[&str] = &["java", "util", "Collection"];
238
239/// What a new unbounded sequence is under [`JavaMapping::Psm`].
240pub const ARRAY_LIST: &[&str] = &["java", "util", "ArrayList"];
241
242/// The prefix a generated map-entry class carries.
243const ENTRY_PREFIX: &str = "ZdEntry_";
244/// The prefix a generated map class carries.
245pub const MAP_PREFIX: &str = "ZdMap";
246
247/// The top-level names a generated declaration may not take.
248///
249/// Every reference this materialiser writes is qualified from [`OUTER`],
250/// `java.lang` or `org.zerodds.wire`, and JLS §6.5.2 resolves the first
251/// segment against the enclosing scopes first. A nested class named
252/// `java` would therefore capture `java.lang.String`.
253const RESERVED_TOP_LEVEL: &[&str] = &[OUTER, "java", "org"];
254
255/// The shape generated Java types take.
256///
257/// The default of `idlc generate --lang java` is [`JavaMapping::Psm`], Clause 9
258/// of DDS-Java-PSM 1.0, "Improved Plain Language Binding for Java"; the
259/// IDL-to-Java mapping of the OMG ([`JavaMapping::Omg`], public fields and
260/// `T[]` for every sequence) is the named option `--java-mapping omg`.
261/// [`JavaMapping::Psm`] changes two things against it.
262///
263/// 1. §9.1.1: "DDS aggregation types shall be mapped to a final Java
264///    class. Contained attributes shall be encapsulated. Java Bean style
265///    accessors shall be provided." A structure, a union (its
266///    discriminator is the property `discriminator`), a bit set and an
267///    exception become a `public static final class` with private state,
268///    a no-argument constructor and `getX()`/`setX(v)` for each member; a
269///    `boolean` member also has `isX()`. The entry class of a `map` is
270///    not an IDL aggregate and stays a two-field holder.
271/// 2. §9.1.2: "Unbounded DDS sequences are mapped to `Collection<E>`
272///    interface. [...] Bounded sequences and arrays are mapped to Java
273///    arrays." An unbounded `sequence<T>` is a
274///    `java.util.Collection<E>` with `E` the boxed type of `T`.
275///
276/// Nothing else moves: the integer table, `string`, `fixed`, enumerations,
277/// bitmasks, `map` and the optional member are as they are.
278#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
279pub enum JavaMapping {
280    /// IDL-to-Java: public fields, `T[]` for every sequence.
281    Omg,
282    /// DDS-Java-PSM 1.0 Clause 9.
283    #[default]
284    Psm,
285}
286
287impl JavaMapping {
288    /// The name the command line gives it.
289    #[must_use]
290    pub const fn as_str(self) -> &'static str {
291        match self {
292            Self::Omg => "omg",
293            Self::Psm => "psm",
294        }
295    }
296
297    /// The mapping a command-line name selects.
298    #[must_use]
299    pub fn from_name(name: &str) -> Option<Self> {
300        match name {
301            "omg" => Some(Self::Omg),
302            "psm" => Some(Self::Psm),
303            _ => None,
304        }
305    }
306}
307
308std::thread_local! {
309    static MAPPING: std::cell::Cell<JavaMapping> =
310        const { std::cell::Cell::new(JavaMapping::Psm) };
311}
312
313/// The mapping in force on this thread.
314///
315/// The materialiser, the codec target and the defaulters are free
316/// functions and associated functions with no state to carry a choice in
317/// (the same code serves a dozen other columns), so the choice is the
318/// thread's for the duration of [`with_mapping`] and [`JavaMapping::Psm`],
319/// the default of `idlc`, outside it: a caller that needs the IDL-to-Java
320/// shape asks for it with `with_mapping(JavaMapping::Omg, ...)`.
321/// Generation is synchronous on one thread; two threads
322/// generating with different mappings do not see each other.
323#[must_use]
324pub fn mapping() -> JavaMapping {
325    MAPPING.with(std::cell::Cell::get)
326}
327
328/// Run `body` with `mapping` in force, and restore what was before — also
329/// when `body` panics.
330pub fn with_mapping<R>(mapping: JavaMapping, body: impl FnOnce() -> R) -> R {
331    struct Restore(JavaMapping);
332    impl Drop for Restore {
333        fn drop(&mut self) {
334            MAPPING.with(|cell| cell.set(self.0));
335        }
336    }
337    let _restore = Restore(MAPPING.with(|cell| cell.replace(mapping)));
338    body()
339}
340
341/// `getX` for the member `field`, which is the escaped Java name.
342///
343/// The first letter is capitalised and nothing else changes, so the
344/// accessor of `a_b` is `getA_b`. A union's discriminator `_d` is the
345/// property `discriminator`. `getClass` is `Object`'s final method, so a
346/// member `Class` takes `getClass_`.
347#[must_use]
348pub fn getter_name(field: &str) -> String {
349    format!("get{}", property(field))
350}
351
352/// `setX` for the member `field`; see [`getter_name`].
353#[must_use]
354pub fn setter_name(field: &str) -> String {
355    format!("set{}", property(field))
356}
357
358/// `isX` for a `boolean` member `field`; see [`getter_name`].
359#[must_use]
360pub fn boolean_getter_name(field: &str) -> String {
361    format!("is{}", property(field))
362}
363
364fn property(field: &str) -> String {
365    if field == "_d" {
366        return "Discriminator".to_string();
367    }
368    let mut chars = field.chars();
369    let mut out = String::new();
370    if let Some(first) = chars.next() {
371        out.extend(first.to_uppercase());
372    }
373    out.push_str(chars.as_str());
374    if out == "Class" {
375        out.push('_');
376    }
377    out
378}
379
380/// The runtime class the Psm mapping's codec calls for the one thing it
381/// cannot decide by type: indexing, counting, allocating and filling a
382/// sequence that may be an array or a `Collection`. Overload resolution
383/// does it, and the class is `org.zerodds.wire.Sequences`.
384pub const SEQUENCES: &str = "Sequences";
385
386fn is_sequences(receiver: &java::Expr) -> bool {
387    matches!(receiver, java::Expr::Path(path)
388        if matches!(path.segments(), [only] if only.as_str() == SEQUENCES))
389}
390
391fn sequences_call(name: &str, args: Vec<java::Expr>) -> java::Expr {
392    java::Expr::MethodCall {
393        receiver: Some(Box::new(java::Expr::Path(Path::new(&[SEQUENCES])))),
394        name: Ident::new(name),
395        args,
396    }
397}
398
399/// `Sequences.view(v)`: marks the expression the codec indexes into, so
400/// that [`index`] can tell a sequence from a nested array.
401#[must_use]
402pub fn sequence_view(value: java::Expr) -> java::Expr {
403    sequences_call("view", vec![value])
404}
405
406/// `a[i]` for an array, and `Sequences.at(v, i)` for the sequence the
407/// codec marked with [`sequence_view`].
408#[must_use]
409pub fn index(value: java::Expr, at: java::Expr) -> java::Expr {
410    if let java::Expr::MethodCall {
411        receiver: Some(receiver),
412        name,
413        args,
414    } = &value
415        && name.as_str() == "view"
416        && is_sequences(receiver)
417        && let [inner] = args.as_slice()
418    {
419        return sequences_call("at", vec![inner.clone(), at]);
420    }
421    java::Expr::Index {
422        value: Box::new(value),
423        index: Box::new(at),
424    }
425}
426
427/// `Sequences.count(v)`: the length of an array or a `Collection`.
428#[must_use]
429pub fn sequences_count(value: java::Expr) -> java::Expr {
430    sequences_call("count", vec![value])
431}
432
433/// `Sequences.first(v, n)`: the first `n` elements, in a container of the
434/// kind `v` is.
435#[must_use]
436pub fn sequences_first(value: java::Expr, count: java::Expr) -> java::Expr {
437    sequences_call("first", vec![value, count])
438}
439
440/// `Sequences.alloc(v, n)`: a new container of the kind `v` is, with room
441/// for `n` elements.
442#[must_use]
443pub fn sequences_alloc(value: java::Expr, count: java::Expr) -> java::Expr {
444    sequences_call("alloc", vec![value, count])
445}
446
447/// The place an element of a sequence is written into: a pseudo
448/// l-value that [`assign`] turns into `Sequences.put(v, i, value)`.
449#[must_use]
450pub fn sequence_slot(sequence: java::Expr, at: java::Expr) -> java::Expr {
451    sequences_call("slot", vec![sequence, at])
452}
453
454/// `base.getX()`: the read of a member under [`JavaMapping::Psm`], and
455/// the pseudo l-value that [`assign`] turns into `base.setX(value)`.
456#[must_use]
457pub fn accessor(base: java::Expr, field: &str) -> java::Expr {
458    java::Expr::MethodCall {
459        receiver: Some(Box::new(base)),
460        name: Ident::new(&getter_name(field)),
461        args: Vec::new(),
462    }
463}
464
465/// `place = value;`, in the form the mapping in force can write.
466///
467/// Under [`JavaMapping::Omg`] this is the assignment. Under
468/// [`JavaMapping::Psm`] a `place` made by [`accessor`] is the setter call
469/// and a `place` made by [`sequence_slot`] is `Sequences.put`; every
470/// other place (a local, an element of an array, the field of a map
471/// entry) is assigned as before.
472#[must_use]
473pub fn assign(place: java::Expr, value: java::Expr) -> java::Statement {
474    if mapping() == JavaMapping::Psm {
475        if let java::Expr::MethodCall {
476            receiver: Some(receiver),
477            name,
478            args,
479        } = &place
480        {
481            if args.is_empty() && name.as_str().starts_with("get") {
482                return java::Statement::Expr(java::Expr::MethodCall {
483                    receiver: Some(receiver.clone()),
484                    name: Ident::new(&format!("set{}", &name.as_str()["get".len()..])),
485                    args: vec![value],
486                });
487            }
488            if name.as_str() == "slot"
489                && is_sequences(receiver)
490                && let [sequence, at] = args.as_slice()
491            {
492                return java::Statement::Expr(sequences_call(
493                    "put",
494                    vec![sequence.clone(), at.clone(), value],
495                ));
496            }
497        }
498    }
499    java::Statement::Expr(java::Expr::Binary {
500        op: "=",
501        lhs: Box::new(place),
502        rhs: Box::new(value),
503    })
504}
505
506/// A statement the walk built as `place = value` or as the bare
507/// expression `place = value`, rewritten by [`assign`]; anything else is
508/// returned as it is.
509#[must_use]
510pub fn assignment(statement: java::Statement) -> java::Statement {
511    match statement {
512        java::Statement::Expr(java::Expr::Binary { op: "=", lhs, rhs }) => assign(*lhs, *rhs),
513        other => other,
514    }
515}
516
517/// Build the Java declarations a rule application produced.
518///
519/// One element: the outer class, with everything nested inside it. See
520/// the module documentation for why it is one and not many.
521///
522/// # Errors
523/// [`MaterialiseError`] when a value the output has to state does not
524/// evaluate, a type has no Java shape, or a generated name would collide
525/// with the class it nests in — never a silently dropped declaration.
526pub fn java_items(ast: &Ast, applied: &Applied) -> Result<Vec<java::TypeDecl>, MaterialiseError> {
527    // Structural declarations — the map entry classes — sit at the outer
528    // level and not in a module: two identical maps declared in two
529    // modules are one Java class, exactly as two identical anonymous
530    // collections are one C type.
531    let mut structural: Vec<java::Member> = Vec::new();
532    let mut emitted: Vec<String> = Vec::new();
533    let mut items: Vec<Pending> = Vec::new();
534
535    // The same walk the C column uses. Java needs no forward
536    // declarations, so the order is not load-bearing for the compiler;
537    // it is load-bearing for the output, which must not move between two
538    // runs over one model.
539    for id in ordered_types(ast) {
540        let start = items.len();
541        declare(ast, applied, id, &mut items, &mut structural, &mut emitted)?;
542        annotate_declared(ast, id, start, &mut items)?;
543        weave_verbatim(ast, id, start, &mut items);
544    }
545
546    // DDS-XTypes 1.3 §7.5.1.2.6.1: "Each user-defined IDL annotation type
547    // shall be represented by a corresponding Java annotation type."
548    for (module, member) in annotation_types(ast)? {
549        items.push(Pending { module, member });
550    }
551
552    // Constants and exceptions are not types, so they are walked
553    // separately — the model keeps them in arenas of their own, and both
554    // sibling materialisers do the same.
555    for (id, node) in ast.constants() {
556        if ghost_created_for(applied, node.node_id, "JavaConst").is_none() {
557            continue;
558        }
559        // `eval_const` and not `eval_integral`: `const string MODULE_ID`
560        // evaluates perfectly well and is not a number, and asking the
561        // integral question about it answered `Unevaluated` and dropped
562        // the field.
563        let value = ast
564            .eval_const(node.expr)
565            .map_err(|_| MaterialiseError::Unevaluated {
566                construct: "constant's value".to_string(),
567                at: node.name.clone(),
568            })?;
569        let type_ = match node.type_.resolved() {
570            Some(target) => java_member_type(ast, target, &node.name)?,
571            None => java::Type::Primitive(java::Primitive::Int),
572        };
573        let value = match value {
574            // CORBA 3.3 part 3 §1.5.2 (and IDL4-Java): `octet` is a Java
575            // `byte`, which holds the octet's eight bits. An octet above
576            // 127 is that bit pattern: the hex literal of the IDL value,
577            // cast — `(byte) 0xF1` for `0xF1`. Refusing it left no Java form
578            // for the octet constants of XTypes 1.3 Annex B (`EK_MINIMAL`).
579            zerodds_ast::ConstValue::Integer(number)
580                if (128..=255).contains(&number)
581                    && node
582                        .type_
583                        .resolved()
584                        .is_some_and(|target| is_octet(ast, target, 0)) =>
585            {
586                java::Expr::Cast {
587                    type_: Box::new(java::Type::Primitive(java::Primitive::Byte)),
588                    value: Box::new(java::Expr::Literal(Literal::Int(format!("0x{number:02X}")))),
589                }
590            }
591            value => constant_expr(&type_, &value, &node.name)?,
592        };
593        items.push(Pending {
594            module: node.module,
595            member: java::Member::Field(java::Field {
596                doc: Doc::none(),
597                annotations: user_annotations(ast, zerodds_ast::AnyId::Const(id, node.node_id))?,
598                modifiers: vec![
599                    java::Modifier::Public,
600                    java::Modifier::Static,
601                    java::Modifier::Final,
602                ],
603                type_,
604                name: Ident::new(&java::escape_keyword(&node.name)),
605                value: Some(value),
606            }),
607        });
608    }
609
610    for (id, node) in ast.exceptions() {
611        if ghost_created_for(applied, node.node_id, "JavaError").is_none() {
612            continue;
613        }
614        let _ = id;
615        let mut members = Vec::new();
616        for member in &node.members {
617            let Ok(member_node) = ast.member(*member) else {
618                return Err(MaterialiseError::Unevaluated {
619                    construct: "exception member the model listed".to_string(),
620                    at: node.name.clone(),
621                });
622            };
623            let Some(target) = member_node.type_.resolved() else {
624                return Err(MaterialiseError::Unevaluated {
625                    construct: "exception member type".to_string(),
626                    at: member_node.name.clone(),
627                });
628            };
629            members.push(field(
630                java_member_type(ast, target, &member_node.name)?,
631                &member_node.name,
632                Doc::none(),
633            ));
634        }
635        items.push(Pending {
636            module: node.module,
637            member: data_class(
638                &java::escape_keyword(&node.name),
639                members,
640                Doc::lines(&[
641                    "An exception, as the structure its members make.",
642                    "",
643                    "A data holder and not a java.lang.Throwable: a Throwable is",
644                    "Serializable, which under -Xlint:all -Werror demands a",
645                    "serialVersionUID, and nothing about the wire form decides what",
646                    "that number should be.",
647                ]),
648                None,
649                true,
650            )?,
651        });
652    }
653
654    let mut root = Scope::default();
655    for item in items {
656        insert(ast, &mut root, item)?;
657    }
658    let mut members = structural;
659    members.extend(scope_members(ast, root, 0)?);
660    check_names(&members, &[OUTER.to_string()], 0)?;
661
662    // `@verbatim(placement = BEGIN_FILE)` opens the generated file and
663    // `END_FILE` closes it — OMG IDL 4.2 §8.3.5.1 — outside even the
664    // outer class, because the file and not the class is what the
665    // placement names.
666    let (begin_file, end_file) = verbatim::file_slots(ast, verbatim::aliases::JAVA);
667    let mut decls: Vec<java::TypeDecl> = begin_file
668        .into_iter()
669        .map(java::TypeDecl::Verbatim)
670        .collect();
671    decls.push(java::TypeDecl::Class(java::Class {
672        doc: Doc::lines(&[
673            "The Java shape of one IDL file.",
674            "",
675            "Every declaration nests here: Java allows one public top-level type",
676            "per file, and one file is what a generated probe compiles.",
677        ]),
678        annotations: Vec::new(),
679        modifiers: vec![java::Modifier::Public, java::Modifier::Final],
680        name: Ident::new(OUTER),
681        generics: Vec::new(),
682        extends: None,
683        implements: Vec::new(),
684        members,
685    }));
686    decls.extend(end_file.into_iter().map(java::TypeDecl::Verbatim));
687    Ok(decls)
688}
689
690// -- user-defined annotation types (DDS-XTypes 1.3 §7.5.1.2.6.1) --------------
691
692/// The suffix of the Java annotation type that groups several instances of
693/// one annotation on one element.
694const GROUP_SUFFIX: &str = "Group";
695
696/// The Java type of an annotation element, which Java restricts to
697/// primitives, `String` and arrays of them.
698fn element_type(
699    ast: &Ast,
700    member: &zerodds_ast::AnnotationMemberNode,
701    owner: &str,
702) -> Result<java::Type, MaterialiseError> {
703    let at = format!("@{owner}.{}", member.name);
704    let Some(target) = member.type_.resolved() else {
705        return Err(MaterialiseError::Unevaluated {
706            construct: "annotation member type".to_string(),
707            at,
708        });
709    };
710    let type_ = java_member_type(ast, target, &at)?;
711    let allowed = match &type_ {
712        java::Type::Primitive(_) => true,
713        java::Type::Named { path, .. } => {
714            let segments: Vec<&str> = path.segments().iter().map(Ident::as_str).collect();
715            segments == STRING
716        }
717        _ => false,
718    };
719    if !allowed {
720        return Err(MaterialiseError::NoLanguageForm {
721            construct: format!(
722                "an annotation member of the type {}, which a Java annotation element cannot have",
723                zerodds_lang_render::java::type_(&type_)
724            ),
725            language: "Java",
726            at,
727        });
728    }
729    Ok(type_)
730}
731
732/// The path of the Java annotation type that stands for `def`, written
733/// from the outer class down.
734fn annotation_path(
735    ast: &Ast,
736    def: Id<zerodds_ast::AnnotationDefNode>,
737    group: bool,
738) -> Result<Path, MaterialiseError> {
739    let node = ast
740        .annotation_def(def)
741        .map_err(|_| MaterialiseError::Unevaluated {
742            construct: "annotation declaration".to_string(),
743            at: OUTER.to_string(),
744        })?;
745    let mut segments = vec![OUTER.to_string()];
746    for (_, name) in module_chain(ast, node.module)? {
747        segments.push(name);
748    }
749    let mut name = java::escape_keyword(&node.name);
750    if group {
751        name.push_str(GROUP_SUFFIX);
752    }
753    segments.push(name);
754    let refs: Vec<&str> = segments.iter().map(String::as_str).collect();
755    Ok(Path::new(&refs))
756}
757
758/// `public @interface <TypeName> { … }` and `public @interface
759/// <TypeName>Group { <TypeName>[] value(); }` for every annotation the
760/// model declares, each with the module it was declared in.
761///
762/// The elements are the declaration's members in order; "If a default
763/// value is specified for a given member, it shall be reflected in the
764/// Java definition. Otherwise, the Java definition shall have no default
765/// value."
766///
767/// # Errors
768/// [`MaterialiseError`] for a member whose type a Java annotation element
769/// cannot have, or whose default does not evaluate.
770fn annotation_types(ast: &Ast) -> Result<Vec<(Id<ModuleNode>, java::Member)>, MaterialiseError> {
771    let mut out = Vec::new();
772    for (_, def) in ast.annotation_defs() {
773        let in_builtin = ast
774            .module(def.module)
775            .is_ok_and(|module| module.name == zerodds_ast::node::annotation::BUILTIN_MODULE_NAME);
776        if in_builtin {
777            continue;
778        }
779        let name = java::escape_keyword(&def.name);
780        let mut elements = Vec::new();
781        for member_id in &def.members {
782            let member =
783                ast.annotation_member(*member_id)
784                    .map_err(|_| MaterialiseError::Unevaluated {
785                        construct: "annotation member".to_string(),
786                        at: def.name.clone(),
787                    })?;
788            let type_ = element_type(ast, member, &def.name)?;
789            let default = match member.default {
790                Some(expr) => {
791                    let at = format!("@{}.{}", def.name, member.name);
792                    let value =
793                        ast.eval_const(expr)
794                            .map_err(|_| MaterialiseError::Unevaluated {
795                                construct: "annotation member default".to_string(),
796                                at: at.clone(),
797                            })?;
798                    Some(constant_expr(&type_, &value, &at)?)
799                }
800                None => None,
801            };
802            elements.push(java::AnnotationElement {
803                doc: Doc::none(),
804                type_,
805                name: Ident::new(&java::escape_keyword(&member.name)),
806                default,
807            });
808        }
809        out.push((
810            def.module,
811            java::Member::Nested(Box::new(java::TypeDecl::AnnotationType(
812                java::AnnotationType {
813                    doc: Doc::line("A user-defined annotation type (XTypes 1.3 7.5.1.2.6.1)."),
814                    annotations: Vec::new(),
815                    modifiers: vec![java::Modifier::Public],
816                    name: Ident::new(&name),
817                    elements,
818                },
819            ))),
820        ));
821        out.push((
822            def.module,
823            java::Member::Nested(Box::new(java::TypeDecl::AnnotationType(
824                java::AnnotationType {
825                    doc: Doc::line(
826                        "Several instances of the annotation on one element (XTypes 1.3 \
827                         7.5.1.2.6.1).",
828                    ),
829                    annotations: Vec::new(),
830                    modifiers: vec![java::Modifier::Public],
831                    name: Ident::new(&format!("{name}{GROUP_SUFFIX}")),
832                    elements: vec![java::AnnotationElement {
833                        doc: Doc::none(),
834                        type_: java::Type::Array(Box::new(java::Type::named(Path::single(&name)))),
835                        name: Ident::new("value"),
836                        default: None,
837                    }],
838                },
839            ))),
840        ));
841    }
842    Ok(out)
843}
844
845/// Whether `def` is declared in the module of the built-in annotations.
846fn is_builtin_annotation(ast: &Ast, def: Id<zerodds_ast::AnnotationDefNode>) -> bool {
847    ast.annotation_def(def)
848        .ok()
849        .and_then(|node| ast.module(node.module).ok())
850        .is_some_and(|module| module.name == zerodds_ast::node::annotation::BUILTIN_MODULE_NAME)
851}
852
853/// One applied annotation as the Java annotation of the same name.
854fn applied_annotation(
855    ast: &Ast,
856    applied: &zerodds_ast::AppliedAnnotation,
857    def: Id<zerodds_ast::AnnotationDefNode>,
858) -> Result<java::Annotation, MaterialiseError> {
859    let node = ast
860        .annotation_def(def)
861        .map_err(|_| MaterialiseError::Unevaluated {
862            construct: "annotation declaration".to_string(),
863            at: applied.name.clone(),
864        })?;
865    let mut args = Vec::new();
866    for argument in &applied.arguments {
867        let member = argument
868            .name
869            .as_deref()
870            .and_then(|written| {
871                node.members.iter().find_map(|id| {
872                    ast.annotation_member(*id)
873                        .ok()
874                        .filter(|m| m.name.eq_ignore_ascii_case(written))
875                })
876            })
877            .or_else(|| {
878                node.members.iter().find_map(|id| {
879                    ast.annotation_member(*id)
880                        .ok()
881                        .filter(|m| m.is_value_member)
882                })
883            })
884            .ok_or_else(|| MaterialiseError::Unevaluated {
885                construct: "annotation argument naming no member".to_string(),
886                at: applied.name.clone(),
887            })?;
888        let at = format!("@{}.{}", node.name, member.name);
889        let type_ = element_type(ast, member, &node.name)?;
890        let value = ast
891            .eval_const(argument.value)
892            .map_err(|_| MaterialiseError::Unevaluated {
893                construct: "annotation argument".to_string(),
894                at: at.clone(),
895            })?;
896        args.push(java::AnnotationArg::Pair {
897            name: Ident::new(&java::escape_keyword(&member.name)),
898            value: constant_expr(&type_, &value, &at)?,
899        });
900    }
901    Ok(java::Annotation {
902        path: annotation_path(ast, def, false)?,
903        args,
904    })
905}
906
907/// The Java annotations an IDL element carries.
908///
909/// "For each IDL element to which a single instance user-defined
910/// annotation is applied, the corresponding Java element shall be
911/// annotated with the Java annotation of the same name. For each IDL
912/// element to which multiple instances of the annotation are applied, the
913/// corresponding Java element shall be annotated with the generated
914/// annotation bearing the 'Group' suffix; each application of the
915/// user-defined annotation shall correspond to a member of the array in
916/// the group." An annotation the model has no declaration for is not
917/// represented: there is no Java type to name.
918fn user_annotations(
919    ast: &Ast,
920    any: zerodds_ast::AnyId,
921) -> Result<Vec<java::Annotation>, MaterialiseError> {
922    let facets = ast
923        .facets(any)
924        .ok_or_else(|| MaterialiseError::Unevaluated {
925            construct: "the annotations of an element the model listed".to_string(),
926            at: OUTER.to_string(),
927        })?;
928    let mut seen: Vec<Id<zerodds_ast::AnnotationDefNode>> = Vec::new();
929    let mut out = Vec::new();
930    for applied in &facets.annotations {
931        let Some(def) = applied.definition.resolved() else {
932            continue;
933        };
934        // A built-in annotation (the specifications' own, such as the
935        // DDS-RPC ones the model keeps whole) is not a user-defined
936        // annotation type and has no Java declaration here.
937        if is_builtin_annotation(ast, def) {
938            continue;
939        }
940        if seen.contains(&def) {
941            continue;
942        }
943        seen.push(def);
944        let same: Vec<&zerodds_ast::AppliedAnnotation> = facets
945            .annotations
946            .iter()
947            .filter(|other| other.definition.resolved() == Some(def))
948            .collect();
949        if let [only] = same.as_slice() {
950            out.push(applied_annotation(ast, only, def)?);
951            continue;
952        }
953        let mut instances = Vec::new();
954        for each in same {
955            instances.push(java::Expr::Annotation(applied_annotation(ast, each, def)?));
956        }
957        out.push(java::Annotation {
958            path: annotation_path(ast, def, true)?,
959            args: vec![java::AnnotationArg::Value(java::Expr::ArrayInit(instances))],
960        });
961    }
962    Ok(out)
963}
964
965/// Put the user-defined annotations of a type, its members, its
966/// enumerators and its bits on the Java elements `declare` made for them.
967fn annotate_declared(
968    ast: &Ast,
969    id: Id<TypeNode>,
970    start: usize,
971    items: &mut [Pending],
972) -> Result<(), MaterialiseError> {
973    let lost = |construct: &str, at: &str| MaterialiseError::Unevaluated {
974        construct: construct.to_string(),
975        at: at.to_string(),
976    };
977    let node = ast
978        .type_(id)
979        .map_err(|_| lost("a type the model listed", OUTER))?;
980    let type_name = java::escape_keyword(&node.name);
981    let type_annotations = user_annotations(ast, zerodds_ast::AnyId::Type(id, node.node_id))?;
982    // Field annotations by Java field name.
983    let mut by_field: Vec<(String, Vec<java::Annotation>)> = Vec::new();
984    let mut push_field = |name: String, annotations: Vec<java::Annotation>| {
985        if !annotations.is_empty() {
986            by_field.push((name, annotations));
987        }
988    };
989    match &node.kind {
990        TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
991            for member in ast.all_members_of(id) {
992                let member_node = ast
993                    .member(member)
994                    .map_err(|_| lost("a member the model listed", &node.name))?;
995                push_field(
996                    java::escape_keyword(&member_node.name),
997                    user_annotations(ast, zerodds_ast::AnyId::Member(member, member_node.node_id))?,
998                );
999            }
1000        }
1001        TypeKind::Union { .. } => {
1002            for case in ast.cases_of(id) {
1003                let case_node = ast
1004                    .case(*case)
1005                    .map_err(|_| lost("a union case the model listed", &node.name))?;
1006                let member_node = ast
1007                    .member(case_node.member)
1008                    .map_err(|_| lost("the member of a union case", &node.name))?;
1009                push_field(
1010                    java::escape_keyword(&member_node.name),
1011                    user_annotations(
1012                        ast,
1013                        zerodds_ast::AnyId::Member(case_node.member, member_node.node_id),
1014                    )?,
1015                );
1016            }
1017        }
1018        TypeKind::Enumeration { enumerators } => {
1019            for enumerator in enumerators {
1020                let enum_node = ast
1021                    .enumerator(*enumerator)
1022                    .map_err(|_| lost("an enumerator the model listed", &node.name))?;
1023                push_field(
1024                    format!("_{}", enum_node.name),
1025                    user_annotations(
1026                        ast,
1027                        zerodds_ast::AnyId::Enumerator(*enumerator, enum_node.node_id),
1028                    )?,
1029                );
1030            }
1031        }
1032        TypeKind::Bitmask { bits } => {
1033            for bit in bits {
1034                let bit_node = ast
1035                    .bit_value(*bit)
1036                    .map_err(|_| lost("a bitmask bit the model listed", &node.name))?;
1037                push_field(
1038                    java::escape_keyword(&bit_node.name),
1039                    user_annotations(ast, zerodds_ast::AnyId::BitValue(*bit, bit_node.node_id))?,
1040                );
1041            }
1042        }
1043        TypeKind::Bitset { .. } => {
1044            for bitfield in &ast.all_bitfields_of(id) {
1045                let field_node = ast
1046                    .bitfield(*bitfield)
1047                    .map_err(|_| lost("a bitset field the model listed", &node.name))?;
1048                if let Some(name) = field_node.name.as_ref() {
1049                    push_field(
1050                        java::escape_keyword(name),
1051                        user_annotations(
1052                            ast,
1053                            zerodds_ast::AnyId::Bitfield(*bitfield, field_node.node_id),
1054                        )?,
1055                    );
1056                }
1057            }
1058        }
1059        // Collections and aliases declare no Java element of their own.
1060        TypeKind::Primitive(_)
1061        | TypeKind::String { .. }
1062        | TypeKind::Fixed { .. }
1063        | TypeKind::Sequence { .. }
1064        | TypeKind::Array { .. }
1065        | TypeKind::Map { .. }
1066        | TypeKind::Alias { .. }
1067        | TypeKind::ValueBox { .. }
1068        | TypeKind::Native
1069        | TypeKind::Any
1070        | TypeKind::ValueBase
1071        | TypeKind::ObjectRef { .. }
1072        | TypeKind::AnnotationType { .. } => {}
1073    }
1074    for pending in items.iter_mut().skip(start) {
1075        let java::Member::Nested(decl) = &mut pending.member else {
1076            continue;
1077        };
1078        let java::TypeDecl::Class(class) = decl.as_mut() else {
1079            continue;
1080        };
1081        if class.name.as_str() != type_name {
1082            continue;
1083        }
1084        class.annotations.extend(type_annotations.iter().cloned());
1085        for member in &mut class.members {
1086            let java::Member::Field(field) = member else {
1087                continue;
1088            };
1089            for (name, annotations) in &by_field {
1090                if field.name.as_str() == name {
1091                    field.annotations.extend(annotations.iter().cloned());
1092                }
1093            }
1094        }
1095    }
1096    Ok(())
1097}
1098
1099/// Weave one type's `@verbatim` text around and into what `declare`
1100/// just pushed. OMG IDL 4.2 §8.3.5.1; the slot semantics are
1101/// [`crate::verbatim`]'s.
1102///
1103/// `start` is where the type's declarations begin in `items`.
1104/// `BEGIN_DECLARATION` and `END_DECLARATION` land inside the class body
1105/// when the type materialised as one; an enum, a constant or a type
1106/// that declared nothing has no body this tree can hold raw text in, so
1107/// the text lands adjacent to the declaration instead — the nearest
1108/// position the tree can express, never a dropped directive.
1109fn weave_verbatim(ast: &Ast, id: Id<TypeNode>, start: usize, items: &mut Vec<Pending>) {
1110    let slots = verbatim::declaration_slots(ast, id, verbatim::aliases::JAVA);
1111    if slots.before.is_empty()
1112        && slots.begin.is_empty()
1113        && slots.end.is_empty()
1114        && slots.after.is_empty()
1115    {
1116        return;
1117    }
1118    let Ok(node) = ast.type_(id) else {
1119        return;
1120    };
1121    let name = java::escape_keyword(&node.name);
1122    let mut declared = items.split_off(start);
1123    let mut begin = slots.begin;
1124    let mut end = slots.end;
1125    let body = declared
1126        .iter_mut()
1127        .find_map(|pending| match &mut pending.member {
1128            java::Member::Nested(decl) => match decl.as_mut() {
1129                java::TypeDecl::Class(class) if class.name.as_str() == name => Some(class),
1130                _ => None,
1131            },
1132            _ => None,
1133        });
1134    if let Some(class) = body {
1135        for text in begin.drain(..).rev() {
1136            class.members.insert(0, java::Member::Verbatim(text));
1137        }
1138        class
1139            .members
1140            .extend(end.drain(..).map(java::Member::Verbatim));
1141    }
1142    for text in slots.before.into_iter().chain(begin) {
1143        items.push(Pending {
1144            module: node.module,
1145            member: java::Member::Verbatim(text),
1146        });
1147    }
1148    items.append(&mut declared);
1149    for text in end.into_iter().chain(slots.after) {
1150        items.push(Pending {
1151            module: node.module,
1152            member: java::Member::Verbatim(text),
1153        });
1154    }
1155}
1156
1157/// One declaration, kept with the module that declared it.
1158struct Pending {
1159    /// The IDL module it was declared in.
1160    module: Id<ModuleNode>,
1161    /// What it materialised as.
1162    member: java::Member,
1163}
1164
1165/// One nesting level of the output: a module's declarations and the
1166/// modules declared inside it.
1167///
1168/// A tree and not a list of chains. Grouping by module id alone would
1169/// emit `module common { struct Robot; }` and
1170/// `module common { module util { … } }` as two classes called `common`
1171/// in one scope, which is the `tests/interop-corpus/module-issue41-shape`
1172/// arrangement.
1173#[derive(Default)]
1174struct Scope {
1175    /// What this module declares directly, in declaration order.
1176    members: Vec<java::Member>,
1177    /// The modules declared inside it, in first-seen order.
1178    children: Vec<(Id<ModuleNode>, String, Scope)>,
1179}
1180
1181/// How deep a module chain is followed before the model is taken to be
1182/// malformed. IDL nests nowhere near this.
1183const MAX_MODULE_DEPTH: usize = 64;
1184
1185/// How deep a type's Java spelling is followed.
1186///
1187/// A `sequence<sequence<…>>` nests once per level; a chain longer than
1188/// this is a cycle the resolver did not catch, and recursing into it
1189/// would end the process rather than report anything.
1190const MAX_TYPE_DEPTH: usize = 64;
1191
1192/// Put one declaration in the scope its module names.
1193///
1194/// # Errors
1195/// [`MaterialiseError::NoModule`] when the model does not answer for a
1196/// module in the chain, or the chain runs past [`MAX_MODULE_DEPTH`].
1197fn insert(ast: &Ast, root: &mut Scope, item: Pending) -> Result<(), MaterialiseError> {
1198    let chain = module_chain(ast, item.module)?;
1199    let mut scope = root;
1200    for (id, name) in chain {
1201        let at = match scope.children.iter().position(|(seen, _, _)| *seen == id) {
1202            Some(at) => at,
1203            None => {
1204                scope.children.push((id, name, Scope::default()));
1205                scope.children.len() - 1
1206            }
1207        };
1208        scope = &mut scope
1209            .children
1210            .get_mut(at)
1211            .ok_or_else(|| MaterialiseError::NoModule {
1212                at: module_handle(item.module),
1213            })?
1214            .2;
1215    }
1216    scope.members.push(item.member);
1217    Ok(())
1218}
1219
1220/// A scope's members, with each module inside it as a nested class.
1221///
1222/// # Errors
1223/// [`MaterialiseError::NoModule`] when the nesting runs past
1224/// [`MAX_MODULE_DEPTH`], which `insert` has already bounded.
1225///
1226/// zerodds-lint: recursion-depth 64 (module nesting; bounded by `MAX_MODULE_DEPTH`)
1227fn scope_members(
1228    ast: &Ast,
1229    scope: Scope,
1230    depth: usize,
1231) -> Result<Vec<java::Member>, MaterialiseError> {
1232    if depth > MAX_MODULE_DEPTH {
1233        return Err(MaterialiseError::NoModule {
1234            at: format!("the nesting runs past {MAX_MODULE_DEPTH}"),
1235        });
1236    }
1237    let mut out = scope.members;
1238    for (module, name, child) in scope.children {
1239        let mut class = nested_class(
1240            &name,
1241            scope_members(ast, child, depth + 1)?,
1242            Doc::line("An IDL module, as the class its declarations nest in."),
1243        );
1244        let node = ast.module(module).map_err(|_| MaterialiseError::NoModule {
1245            at: module_handle(module),
1246        })?;
1247        let annotations = user_annotations(ast, zerodds_ast::AnyId::Module(module, node.node_id))?;
1248        if let java::Member::Nested(decl) = &mut class
1249            && let java::TypeDecl::Class(inner) = decl.as_mut()
1250        {
1251            inner.annotations = annotations;
1252        }
1253        out.push(class);
1254    }
1255    Ok(out)
1256}
1257
1258/// The modules a declaration nests in, outermost first, with the class
1259/// name each takes.
1260///
1261/// The root module is the anonymous global scope and is not a class.
1262///
1263/// # Errors
1264/// [`MaterialiseError::NoModule`] when the model does not answer for a
1265/// module, or the parent chain runs past [`MAX_MODULE_DEPTH`]. Returning
1266/// the declaration unnested instead would put it in another scope and say
1267/// nothing, which is the same class of substitution as writing the wrong
1268/// octet.
1269fn module_chain(
1270    ast: &Ast,
1271    module: Id<ModuleNode>,
1272) -> Result<Vec<(Id<ModuleNode>, String)>, MaterialiseError> {
1273    let mut out = Vec::new();
1274    let mut current = Some(module);
1275    let mut depth = 0;
1276    while let Some(id) = current {
1277        if depth > MAX_MODULE_DEPTH {
1278            return Err(MaterialiseError::NoModule {
1279                at: format!(
1280                    "{} — the parent chain runs past {MAX_MODULE_DEPTH}",
1281                    module_handle(module)
1282                ),
1283            });
1284        }
1285        let node = ast.module(id).map_err(|_| MaterialiseError::NoModule {
1286            at: module_handle(id),
1287        })?;
1288        // The root has no name and no parent, and produces no class.
1289        if node.parent.is_none() {
1290            break;
1291        }
1292        out.push((id, java::escape_keyword(&node.name)));
1293        current = node.parent;
1294        depth += 1;
1295    }
1296    out.reverse();
1297    Ok(out)
1298}
1299
1300/// The module handle, named without the node the model would not answer
1301/// for. Slot and generation are what the arena was asked for.
1302fn module_handle(module: Id<ModuleNode>) -> String {
1303    format!("#{}@{}", module.slot(), module.generation())
1304}
1305
1306/// Refuse a nested type whose name a Java compiler would reject.
1307///
1308/// Two collisions, both of which produce a file no compiler reads:
1309/// a class sharing the simple name of one it nests in (JLS §8.1), and
1310/// two declarations of one name in one scope. A materialiser that let
1311/// either through would report success for output that does not build.
1312///
1313/// zerodds-lint: recursion-depth 64 (module nesting; bounded by `MAX_MODULE_DEPTH`)
1314fn check_names(
1315    members: &[java::Member],
1316    enclosing: &[String],
1317    depth: usize,
1318) -> Result<(), MaterialiseError> {
1319    if depth > MAX_MODULE_DEPTH {
1320        return Err(MaterialiseError::NoModule {
1321            at: format!("the nesting runs past {MAX_MODULE_DEPTH}"),
1322        });
1323    }
1324    let mut seen: Vec<String> = Vec::new();
1325    for member in members {
1326        let java::Member::Nested(decl) = member else {
1327            continue;
1328        };
1329        let name = decl_name(decl).to_string();
1330        // `@verbatim` text has no name and takes part in no collision.
1331        if name.is_empty() {
1332            continue;
1333        }
1334        if enclosing.contains(&name) {
1335            return Err(MaterialiseError::NoLanguageForm {
1336                construct: format!(
1337                    "a declaration named `{name}` inside a class of that name \
1338                     (JLS 8.1: a nested class may not share the simple name of an \
1339                     enclosing one)"
1340                ),
1341                language: "Java",
1342                at: name,
1343            });
1344        }
1345        if depth == 0 && RESERVED_TOP_LEVEL.contains(&name.as_str()) {
1346            return Err(MaterialiseError::NoLanguageForm {
1347                construct: format!(
1348                    "a declaration named `{name}`, which would capture the first \
1349                     segment of every qualified reference this file writes \
1350                     (JLS 6.5.2)"
1351                ),
1352                language: "Java",
1353                at: name,
1354            });
1355        }
1356        if seen.contains(&name) {
1357            return Err(MaterialiseError::NoLanguageForm {
1358                construct: format!("a second declaration named `{name}` in one scope"),
1359                language: "Java",
1360                at: name,
1361            });
1362        }
1363        let mut inner: Vec<String> = enclosing.to_vec();
1364        inner.push(name.clone());
1365        seen.push(name);
1366        if let java::TypeDecl::Class(class) = decl.as_ref() {
1367            check_names(&class.members, &inner, depth + 1)?;
1368        }
1369    }
1370    Ok(())
1371}
1372
1373/// The name a type declaration carries.
1374fn decl_name(decl: &java::TypeDecl) -> &str {
1375    match decl {
1376        java::TypeDecl::Class(node) => node.name.as_str(),
1377        java::TypeDecl::Interface(node) => node.name.as_str(),
1378        java::TypeDecl::Enum(node) => node.name.as_str(),
1379        java::TypeDecl::Record(node) => node.name.as_str(),
1380        java::TypeDecl::AnnotationType(node) => node.name.as_str(),
1381        // Raw text declares nothing; the empty name is the caller's
1382        // signal to pass it over.
1383        java::TypeDecl::Verbatim(_) => "",
1384    }
1385}
1386
1387/// One type's Java declarations, appended where they belong.
1388#[allow(clippy::too_many_lines)]
1389fn declare(
1390    ast: &Ast,
1391    applied: &Applied,
1392    id: Id<TypeNode>,
1393    items: &mut Vec<Pending>,
1394    structural: &mut Vec<java::Member>,
1395    emitted: &mut Vec<String>,
1396) -> Result<(), MaterialiseError> {
1397    let Ok(node) = ast.type_(id) else {
1398        return Ok(());
1399    };
1400    match &node.kind {
1401        // A primitive, a string and a `fixed` are spellings Java already
1402        // has; a sequence is `T[]`. None of them declares anything.
1403        TypeKind::Primitive(_)
1404        | TypeKind::String { .. }
1405        | TypeKind::Fixed { .. }
1406        | TypeKind::Sequence { .. } => {}
1407
1408        // Neither does an array — but every dimension is evaluated
1409        // anyway. Java's array type does not state a length, so nothing
1410        // here would substitute a zero; what a dimension that does not
1411        // evaluate does mean is that no caller can allocate the array,
1412        // and a file that compiles while the length is unknown is
1413        // exactly the silent success this project refuses. The C column
1414        // fails on the same input, and the two columns' failure sets
1415        // stay comparable.
1416        TypeKind::Array { dims, .. } => {
1417            for dim in dims {
1418                ast.eval_integral(*dim)
1419                    .map_err(|_| MaterialiseError::Unevaluated {
1420                        construct: "array length".to_string(),
1421                        at: named(&node.name, node.node_id),
1422                    })?;
1423            }
1424        }
1425
1426        TypeKind::Map { key, value, .. } => {
1427            let (Some(key), Some(value)) = (key.resolved(), value.resolved()) else {
1428                return Err(MaterialiseError::Unevaluated {
1429                    construct: "map key or value".to_string(),
1430                    at: named(&node.name, node.node_id),
1431                });
1432            };
1433            let name = map_entry_name(ast, id)?;
1434            let at = named(&node.name, node.node_id);
1435            if !emitted.contains(&name) {
1436                emitted.push(name.clone());
1437                structural.push(nested_class(
1438                    &name,
1439                    vec![
1440                        field(java_member_type(ast, key, &at)?, "key", Doc::none()),
1441                        field(java_member_type(ast, value, &at)?, "value", Doc::none()),
1442                    ],
1443                    Doc::line(
1444                        "One entry of a map. XTypes 1.3 7.4.1.1.4 defines a map as a \
1445                         sequence of these, and the rules write the two halves with no \
1446                         framing between them. The codec reads and writes the entries \
1447                         of a map in this form, in the order of the map.",
1448                    ),
1449                ));
1450            }
1451            if map_key_supported(ast, key) {
1452                let class = map_class_name(ast, id)?;
1453                if !emitted.contains(&class) {
1454                    emitted.push(class.clone());
1455                    structural.push(map_class(ast, id, key, value, &class, &name, &at)?);
1456                }
1457            }
1458        }
1459
1460        // A typedef has no Java form. The comment is what keeps it from
1461        // being a silent omission: the name, and what it resolved to.
1462        TypeKind::Alias { aliased } => {
1463            if node.name.is_empty() {
1464                return Ok(());
1465            }
1466            if ghost_created_for(applied, node.node_id, "JavaAlias").is_none() {
1467                return Ok(());
1468            }
1469            let Some(target) = aliased.resolved() else {
1470                return Err(MaterialiseError::Unevaluated {
1471                    construct: "typedef target".to_string(),
1472                    at: node.name.clone(),
1473                });
1474            };
1475            let spelling =
1476                zerodds_lang_render::java::type_(&java_member_type(ast, target, &node.name)?);
1477            items.push(Pending {
1478                module: node.module,
1479                member: java::Member::Comment(Doc::line(&format!(
1480                    "typedef {} {}; — Java has no alias, so every use takes {spelling}.",
1481                    spelling, node.name
1482                ))),
1483            });
1484        }
1485
1486        TypeKind::Enumeration { enumerators } => {
1487            if ghost_created_for(applied, node.node_id, "JavaEnum").is_none() {
1488                return Ok(());
1489            }
1490            let mut listed: Vec<(String, i128)> = Vec::new();
1491            for enumerator in enumerators {
1492                let Ok(enum_node) = ast.enumerator(*enumerator) else {
1493                    return Err(MaterialiseError::Unevaluated {
1494                        construct: "enumerator the model listed".to_string(),
1495                        at: node.name.clone(),
1496                    });
1497                };
1498                if ghost_created_for(applied, enum_node.node_id, "JavaEnumerator").is_none() {
1499                    continue;
1500                }
1501                let value = ast.effective_enumerator_value(*enumerator).ok_or_else(|| {
1502                    MaterialiseError::Unevaluated {
1503                        construct: "enumerator value".to_string(),
1504                        at: enum_node.name.clone(),
1505                    }
1506                })?;
1507                let value = in_range(
1508                    value,
1509                    i128::from(i32::MIN),
1510                    i128::from(u32::MAX),
1511                    "int",
1512                    &enum_node.name,
1513                )?;
1514                // A value above `int`'s range is written as the pattern
1515                // it will occupy, because XTypes 1.3 7.4.3.5.3 makes an
1516                // enumerated literal 32 bits wide and a `@bit_bound(32)`
1517                // enumeration may use all of them. `in_range` above has
1518                // already refused anything outside those 32 bits.
1519                let literal = match i32::try_from(value) {
1520                    Ok(fits) => i128::from(fits),
1521                    Err(_) => value - (i128::from(u32::MAX) + 1),
1522                };
1523                listed.push((java::escape_keyword(&enum_node.name), literal));
1524            }
1525            let class_name = java::escape_keyword(&node.name);
1526            items.push(Pending {
1527                module: node.module,
1528                member: nested_class(
1529                    &class_name,
1530                    vec![java::Member::Verbatim(enumeration_class_body(
1531                        &class_name,
1532                        &listed,
1533                    ))],
1534                    Doc::line(
1535                        "An enumeration, as CORBA 3.3 part 3 §1.5.9 maps it: one instance \
1536                         per enumerator, the int constants `_NAME`, `value()` and \
1537                         `from_int`.",
1538                    ),
1539                ),
1540            });
1541        }
1542
1543        TypeKind::Bitmask { bits } => {
1544            if ghost_created_for(applied, node.node_id, "JavaBitmask").is_none() {
1545                return Ok(());
1546            }
1547            let bound = ast.effective_bit_bound(id);
1548            let holder = unsigned_holder(bound);
1549            let wide = holder == java::Type::Primitive(java::Primitive::Long);
1550            let mut members = Vec::new();
1551            let mut flags: Vec<(String, java::Expr)> = Vec::new();
1552            for bit in bits {
1553                let Ok(bit_node) = ast.bit_value(*bit) else {
1554                    return Err(MaterialiseError::Unevaluated {
1555                        construct: "bitmask bit the model listed".to_string(),
1556                        at: node.name.clone(),
1557                    });
1558                };
1559                if ghost_created_for(applied, bit_node.node_id, "JavaBitConst").is_none() {
1560                    continue;
1561                }
1562                let position = ast.effective_bit_position(*bit).ok_or_else(|| {
1563                    MaterialiseError::Unevaluated {
1564                        construct: "bit position".to_string(),
1565                        at: bit_node.name.clone(),
1566                    }
1567                })?;
1568                // `1L` and not `1` where the holder is a `long`: `1 << 40`
1569                // is an int shift in Java and evaluates to 256, silently.
1570                let flag_value = java::Expr::Binary {
1571                    op: "<<",
1572                    lhs: Box::new(java::Expr::Literal(Literal::Int(
1573                        if wide { "1L" } else { "1" }.to_string(),
1574                    ))),
1575                    rhs: Box::new(java::Expr::Literal(Literal::int(i128::from(position)))),
1576                };
1577                flags.push((bit_node.name.clone(), flag_value.clone()));
1578                members.push(constant(holder.clone(), &bit_node.name, flag_value));
1579            }
1580            // XTypes 1.3 §7.5.1.2.2: "the language binding shall be as if
1581            // there was an enumeration definition like the following:
1582            // @bit_bound(n) enum <TypeName>Bits { @value(1 << p) FLAG, … }".
1583            // Java binds an enumeration as the type-safe class of
1584            // §7.5.1.2.1.3.
1585            items.push(Pending {
1586                module: node.module,
1587                member: type_safe_enumeration(
1588                    &format!("{}Bits", java::escape_keyword(&node.name)),
1589                    holder.clone(),
1590                    flags,
1591                    Doc::line(
1592                        "The enumeration of the flags of a bitmask (XTypes 1.3 \
1593                         7.5.1.2.2): {@code _<label>} is the flag's value, \
1594                         {@code 1 << position}.",
1595                    ),
1596                ),
1597            });
1598            items.push(Pending {
1599                module: node.module,
1600                member: nested_class(
1601                    &java::escape_keyword(&node.name),
1602                    members,
1603                    Doc::line(
1604                        "A bitmask, as the constants its bits name. A member of this \
1605                         type takes the unsigned holder Table 33 of XTypes 1.3 \
1606                         7.4.1.1.3.2 gives for its bound, mapped through the unsigned \
1607                         rule this file states.",
1608                    ),
1609                ),
1610            });
1611        }
1612
1613        TypeKind::Bitset { .. } => {
1614            if ghost_created_for(applied, node.node_id, "JavaBitset").is_none() {
1615                return Ok(());
1616            }
1617            let mut members = Vec::new();
1618            // The base's bitfields are fields of the derived class too:
1619            // an inherited bitset is one flat run.
1620            for bitfield in &ast.all_bitfields_of(id) {
1621                let Ok(field_node) = ast.bitfield(*bitfield) else {
1622                    return Err(MaterialiseError::Unevaluated {
1623                        construct: "bitset field the model listed".to_string(),
1624                        at: node.name.clone(),
1625                    });
1626                };
1627                // An anonymous field is padding: it moves the next field
1628                // along and carries no value of its own.
1629                let Some(field_name) = field_node.name.as_ref() else {
1630                    continue;
1631                };
1632                // The width's holder, not the declared one: `bitfield<1,
1633                // boolean>` names a range and not a wire form, and the C
1634                // column takes the width for the same reason. The
1635                // packing into the anchor is the codec's.
1636                members.push(field(
1637                    unsigned_holder(field_node.bits),
1638                    field_name,
1639                    Doc::none(),
1640                ));
1641            }
1642            items.push(Pending {
1643                module: node.module,
1644                member: data_class(
1645                    &java::escape_keyword(&node.name),
1646                    members,
1647                    Doc::line(
1648                        "A bitset. The fields are separate integers here; the packing \
1649                         into the anchor is the codec's.",
1650                    ),
1651                    None,
1652                    true,
1653                )?,
1654            });
1655        }
1656
1657        TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
1658            let created = if matches!(node.kind, TypeKind::Valuetype { .. }) {
1659                "JavaValue"
1660            } else {
1661                "JavaStruct"
1662            };
1663            if ghost_created_for(applied, node.node_id, created).is_none() {
1664                return Ok(());
1665            }
1666            // XTypes 1.3 §7.5.1.4.1.2: "The Java class corresponding to
1667            // the subtype shall extend the Java class corresponding to the
1668            // supertype." The class declares its own members; the plan
1669            // still puts a base's members on the wire first (§7.4.1.1.5.1),
1670            // and the codec reaches them by name as inherited fields.
1671            //
1672            // Every member is emitted, with no per-member rule lookup:
1673            // a member whose lookup missed would vanish from the class
1674            // and the file would still compile. The C column takes the
1675            // same view.
1676            let base = match &node.kind {
1677                TypeKind::Structure {
1678                    base: Some(base), ..
1679                } => base.resolved(),
1680                _ => None,
1681            };
1682            let own: Vec<_> = ast.members_of(id).to_vec();
1683            let mut members = Vec::new();
1684            let mut spelled = Vec::new();
1685            for member in ast.all_members_of(id) {
1686                let Ok(member_node) = ast.member(member) else {
1687                    return Err(MaterialiseError::Unevaluated {
1688                        construct: "member the model listed".to_string(),
1689                        at: node.name.clone(),
1690                    });
1691                };
1692                let Some(target) = member_node.type_.resolved() else {
1693                    return Err(MaterialiseError::Unevaluated {
1694                        construct: "member type".to_string(),
1695                        at: member_node.name.clone(),
1696                    });
1697                };
1698                let type_ = java_member_type(ast, target, &member_node.name)?;
1699                spelled.push((
1700                    java::escape_keyword(&member_node.name),
1701                    member_node.name.clone(),
1702                ));
1703                if base.is_some() && !own.contains(&member) {
1704                    continue;
1705                }
1706                if member_node.facets.is_optional() {
1707                    members.push(field(
1708                        boxed(type_),
1709                        &member_node.name,
1710                        Doc::line(
1711                            "An @optional member: null when absent. In a @mutable type \
1712                             an absent member is not written at all; outside one its \
1713                             absence is written — a zero is_present byte under XCDR2 \
1714                             (rule 20) or a parameter header declaring a length of zero \
1715                             under XCDR1 (7.4.1.2).",
1716                        ),
1717                    ));
1718                } else if super::annotated_external(member_node) {
1719                    // XTypes 1.3 §7.5.1.2.3.4: "External members shall be
1720                    // represented using object references [...] For IDL
1721                    // types that map to Java primitive types, those Java
1722                    // primitive types shall be replaced by the
1723                    // corresponding object box types from the java.lang
1724                    // package."
1725                    members.push(field(
1726                        boxed(type_),
1727                        &member_node.name,
1728                        Doc::line(
1729                            "An @external member: an object reference, so a primitive \
1730                             is held as its java.lang box (XTypes 1.3 7.5.1.2.3.4).",
1731                        ),
1732                    ));
1733                } else {
1734                    members.push(field(type_, &member_node.name, Doc::none()));
1735                }
1736            }
1737            // The escape is the only layer that holds both spellings at
1738            // once. Left to the target: javac reports a duplicate field, three layers from the IDL.
1739            no_spelling_collision(
1740                spelled
1741                    .iter()
1742                    .map(|(field, declared)| (field.as_str(), declared.as_str())),
1743                "Java",
1744                &node.name,
1745            )?;
1746            let extends = match base {
1747                Some(base) => Some(java::Type::named(qualified(ast, base)?)),
1748                None => None,
1749            };
1750            items.push(Pending {
1751                module: node.module,
1752                member: data_class(
1753                    &java::escape_keyword(&node.name),
1754                    members,
1755                    Doc::none(),
1756                    extends,
1757                    !has_derived(ast, id),
1758                )?,
1759            });
1760        }
1761
1762        TypeKind::Union { discriminator, .. } => {
1763            if ghost_created_for(applied, node.node_id, "JavaUnion").is_none() {
1764                return Ok(());
1765            }
1766            let Some(disc) = discriminator.resolved() else {
1767                return Err(MaterialiseError::Unevaluated {
1768                    construct: "union discriminator".to_string(),
1769                    at: node.name.clone(),
1770                });
1771            };
1772            // A field per case and not one per type: two cases may carry
1773            // the same type, and a Java class has no untagged union to
1774            // overlay them with. Which field is live is what `_d` says.
1775            let mut members = vec![java::Member::Field(java::Field {
1776                doc: Doc::line("The discriminator. Which case field is live is what this says."),
1777                annotations: Vec::new(),
1778                modifiers: vec![java::Modifier::Public],
1779                type_: java_member_type(ast, disc, &node.name)?,
1780                name: Ident::new("_d"),
1781                value: None,
1782            })];
1783            let mut accessed: Vec<UnionAccess> = Vec::new();
1784            for case in ast.cases_of(id) {
1785                let Ok(case_node) = ast.case(*case) else {
1786                    return Err(MaterialiseError::Unevaluated {
1787                        construct: "union case the model listed".to_string(),
1788                        at: node.name.clone(),
1789                    });
1790                };
1791                if ghost_created_for(applied, case_node.node_id, "JavaCase").is_none() {
1792                    continue;
1793                }
1794                let Ok(member) = ast.member(case_node.member) else {
1795                    return Err(MaterialiseError::Unevaluated {
1796                        construct: "member behind a union case the model listed".to_string(),
1797                        at: node.name.clone(),
1798                    });
1799                };
1800                let Some(target) = member.type_.resolved() else {
1801                    return Err(MaterialiseError::Unevaluated {
1802                        construct: "union case type".to_string(),
1803                        at: member.name.clone(),
1804                    });
1805                };
1806                let branch = java_member_type(ast, target, &member.name)?;
1807                // An `@external` branch is an object reference, so a primitive is boxed
1808                // (XTypes 1.3 §7.5.1.2.3.4).
1809                let case_type = if super::annotated_external(member) {
1810                    boxed(branch)
1811                } else {
1812                    branch
1813                };
1814                let mut labels = Vec::new();
1815                for candidate in &case_node.labels {
1816                    if let zerodds_ast::CaseLabel::Value(expr) = candidate {
1817                        let value = ast.eval_integral(*expr).map_err(|_| {
1818                            MaterialiseError::Unevaluated {
1819                                construct: "union case label".to_string(),
1820                                at: node.name.clone(),
1821                            }
1822                        })?;
1823                        labels.push(java_label_text(ast, disc, value));
1824                    }
1825                }
1826                accessed.push(UnionAccess {
1827                    name: java::escape_keyword(&member.name),
1828                    type_: zerodds_lang_render::java::type_(&case_type),
1829                    labels,
1830                });
1831                members.push(field(case_type, &member.name, Doc::none()));
1832            }
1833            // IDL to Java 1.3 §1.5.10 (XTypes 1.3 §7.5.1.4): `discriminator()` and, per case,
1834            // an accessor and a modifier. The fields stay public because the codec writes them.
1835            if mapping() != JavaMapping::Psm {
1836                let disc_type =
1837                    zerodds_lang_render::java::type_(&java_member_type(ast, disc, &node.name)?);
1838                members.push(java::Member::Verbatim(union_accessors(
1839                    &disc_type, &accessed,
1840                )));
1841            }
1842            items.push(Pending {
1843                module: node.module,
1844                member: data_class(
1845                    &java::escape_keyword(&node.name),
1846                    members,
1847                    Doc::line("A union: the discriminator, and one field per case."),
1848                    None,
1849                    true,
1850                )?,
1851            });
1852        }
1853
1854        // A native type is opaque and has no Java declaration either —
1855        // the mapping is the facet's spelling, and a comment records it.
1856        TypeKind::Native => {
1857            if ghost_created_for(applied, node.node_id, "JavaNative").is_none() {
1858                return Ok(());
1859            }
1860            let spelling = zerodds_lang_render::java::type_(&native_type(ast, id));
1861            items.push(Pending {
1862                module: node.module,
1863                member: java::Member::Comment(Doc::line(&format!(
1864                    "native {}; — every use takes {spelling}.",
1865                    node.name
1866                ))),
1867            });
1868        }
1869
1870        // A value box is a reference in Java already: the boxed type,
1871        // null when absent. Nothing to declare.
1872        TypeKind::ValueBox { .. } => {}
1873
1874        // An object reference belongs to the RPC arc and an annotation
1875        // type has no runtime form at all — `zerodds_ast_layout` refuses
1876        // to plan either — so a Java declaration would be a shape nothing
1877        // could serialise. `any` and `ValueBase` are in the model because
1878        // Building Block Any is one of IDL 4.2's fifteen, and what they
1879        // hold is decided at run time. All four are named here rather
1880        // than passed over, and `java_member_type` refuses them by name
1881        // where a member would carry one.
1882        TypeKind::ObjectRef { .. }
1883        | TypeKind::AnnotationType { .. }
1884        | TypeKind::Any
1885        | TypeKind::ValueBase => {}
1886    }
1887    Ok(())
1888}
1889
1890/// The Java type of a value in a position that has to hold one, refusing
1891/// the constructs Java cannot hold.
1892///
1893/// `any` is the first of those. What it holds is decided at run time —
1894/// there is no Java type for it and no wire form to generate a codec
1895/// against. Emitting `Object a;` into the class and reporting success
1896/// produced a field no encoder can write, from a generator that said
1897/// nothing was wrong.
1898///
1899/// # Errors
1900/// [`MaterialiseError::NoLanguageForm`] for `any`, `ValueBase`, an object
1901/// reference and an annotation type;
1902/// [`MaterialiseError::Unevaluated`] for a type the model will not answer
1903/// for.
1904pub fn java_member_type(
1905    ast: &Ast,
1906    target: Id<TypeNode>,
1907    at: &str,
1908) -> Result<java::Type, MaterialiseError> {
1909    type_at(ast, target, at, 0)
1910}
1911
1912/// Whether `id` is `octet`, through any typedefs.
1913///
1914/// zerodds-lint: recursion-depth 64 (typedef chains; bounded by
1915/// `MAX_TYPE_DEPTH`, past which the answer is no)
1916fn is_octet(ast: &Ast, id: Id<TypeNode>, depth: usize) -> bool {
1917    if depth > MAX_TYPE_DEPTH {
1918        return false;
1919    }
1920    match ast.type_(id).map(|node| &node.kind) {
1921        Ok(TypeKind::Primitive(PrimitiveKind::Octet)) => true,
1922        Ok(TypeKind::Alias { aliased }) => aliased
1923            .resolved()
1924            .is_some_and(|target| is_octet(ast, target, depth + 1)),
1925        _ => false,
1926    }
1927}
1928
1929/// [`java_member_type`], carrying how deep the spelling has nested.
1930///
1931/// zerodds-lint: recursion-depth 64 (collection and typedef nesting;
1932/// bounded by `MAX_TYPE_DEPTH`, which returns
1933/// `MaterialiseError::Unevaluated` naming the spelling that nested)
1934fn type_at(
1935    ast: &Ast,
1936    id: Id<TypeNode>,
1937    at: &str,
1938    depth: usize,
1939) -> Result<java::Type, MaterialiseError> {
1940    if depth > MAX_TYPE_DEPTH {
1941        return Err(MaterialiseError::Unevaluated {
1942            construct: format!("type, whose Java spelling nests past {MAX_TYPE_DEPTH}"),
1943            at: at.to_string(),
1944        });
1945    }
1946    let node = ast.type_(id).map_err(|_| MaterialiseError::Unevaluated {
1947        construct: "type".to_string(),
1948        at: at.to_string(),
1949    })?;
1950    let refused = match &node.kind {
1951        TypeKind::Any => Some("any"),
1952        TypeKind::ValueBase => Some("ValueBase"),
1953        // An object reference is a handle into the RPC arc and
1954        // `coverage.rs` says so; there is no Java data type for it.
1955        TypeKind::ObjectRef { .. } => Some("Object"),
1956        // An annotation type has no runtime form, so nothing declares one
1957        // and a reference to it would name a class that is not there.
1958        TypeKind::AnnotationType { .. } => Some("annotation type"),
1959        _ => None,
1960    };
1961    if let Some(construct) = refused {
1962        return Err(MaterialiseError::NoLanguageForm {
1963            construct: construct.to_string(),
1964            language: "Java",
1965            at: at.to_string(),
1966        });
1967    }
1968    Ok(match &node.kind {
1969        // Matched ahead of the arm below, which would reach
1970        // `java_primitive`'s `double` — eight bytes where the member is
1971        // sixteen.
1972        TypeKind::Primitive(PrimitiveKind::Float128) => java::Type::named(Path::new(FLOAT128)),
1973        TypeKind::Primitive(kind) => java::Type::Primitive(java_primitive(*kind)),
1974        // A `wstring` is a String like a `string`: Java strings are
1975        // UTF-16 already, and the surrogate rule of XTypes 1.3
1976        // 7.4.1.1.2 is the runtime's, in `Writer.writeWstring`.
1977        TypeKind::String { .. } => java::Type::named(Path::new(STRING)),
1978        TypeKind::Fixed { .. } => java::Type::named(Path::new(BIG_DECIMAL)),
1979        TypeKind::Sequence { element, bound } => {
1980            let Some(element) = element.resolved() else {
1981                return Err(MaterialiseError::Unevaluated {
1982                    construct: "sequence element".to_string(),
1983                    at: at.to_string(),
1984                });
1985            };
1986            let element = type_at(ast, element, at, depth + 1)?;
1987            // DDS-Java-PSM 1.0 §9.1.2: an unbounded sequence is a
1988            // `Collection<E>`, a bounded one a Java array.
1989            if mapping() == JavaMapping::Psm && matches!(bound, zerodds_ast::Bound::Unbounded) {
1990                java::Type::generic(Path::new(COLLECTION), vec![boxed(element)])
1991            } else {
1992                java::Type::Array(Box::new(element))
1993            }
1994        }
1995        TypeKind::Array { element, dims } => {
1996            let Some(element) = element.resolved() else {
1997                return Err(MaterialiseError::Unevaluated {
1998                    construct: "array element".to_string(),
1999                    at: at.to_string(),
2000                });
2001            };
2002            let mut out = type_at(ast, element, at, depth + 1)?;
2003            // One `[]` per dimension. Java's array type carries no
2004            // length, so `long a[2][3]` and `long a[3][2]` are the same
2005            // Java type; the lengths are the plan's, and `declare`
2006            // evaluates them there.
2007            for _ in dims {
2008                out = java::Type::Array(Box::new(out));
2009            }
2010            out
2011        }
2012        // XTypes 1.3 §7.5.1.3.5: "an implementation of the standard
2013        // java.util.Map interface". The class is declared beside the entry
2014        // class (see [`map_class`]); a key Java cannot compare as an object
2015        // keeps the array of entries, because a `java.util.Map` keyed by it
2016        // would compare by identity.
2017        TypeKind::Map { key, .. }
2018            if key
2019                .resolved()
2020                .is_some_and(|key| map_key_supported(ast, key)) =>
2021        {
2022            java::Type::named(Path::new(&[OUTER, &map_class_name(ast, id)?]))
2023        }
2024        TypeKind::Map { .. } => java::Type::Array(Box::new(java::Type::named(Path::new(&[
2025            OUTER,
2026            &map_entry_name(ast, id)?,
2027        ])))),
2028        // A typedef is transparent: the use takes the target's type.
2029        TypeKind::Alias { aliased } => {
2030            let Some(target) = aliased.resolved() else {
2031                return Err(MaterialiseError::Unevaluated {
2032                    construct: "typedef target".to_string(),
2033                    at: at.to_string(),
2034                });
2035            };
2036            type_at(ast, target, at, depth + 1)?
2037        }
2038        // CORBA 3.3 part 3 §1.5.9: an enumeration is a class with one
2039        // instance per enumerator.
2040        TypeKind::Enumeration { .. } => java::Type::named(qualified(ast, id)?),
2041        TypeKind::Bitmask { .. } => unsigned_holder(ast.effective_bit_bound(id)),
2042        TypeKind::Structure { .. }
2043        | TypeKind::Union { .. }
2044        | TypeKind::Bitset { .. }
2045        | TypeKind::Valuetype { .. } => java::Type::named(qualified(ast, id)?),
2046        TypeKind::Native => native_type(ast, id),
2047        // A boxed value is a reference, absent when null — which is what
2048        // a Java reference already is. A primitive takes its wrapper.
2049        TypeKind::ValueBox { boxed: inner } => {
2050            let Some(target) = inner.resolved() else {
2051                return Err(MaterialiseError::Unevaluated {
2052                    construct: "value box target".to_string(),
2053                    at: at.to_string(),
2054                });
2055            };
2056            boxed(type_at(ast, target, at, depth + 1)?)
2057        }
2058        TypeKind::Any
2059        | TypeKind::ValueBase
2060        | TypeKind::ObjectRef { .. }
2061        | TypeKind::AnnotationType { .. } => {
2062            // Refused above; the arm keeps the match total, so a kind
2063            // added to the model without a Java answer is a compile
2064            // error and not a field that silently takes another's type.
2065            return Err(MaterialiseError::NoLanguageForm {
2066                construct: node.kind.as_str().to_string(),
2067                language: "Java",
2068                at: at.to_string(),
2069            });
2070        }
2071    })
2072}
2073
2074/// The Java primitive an IDL primitive becomes.
2075///
2076/// Exhaustive, so a primitive added to the model without a Java spelling
2077/// is a compile error and not a member that silently takes another's.
2078///
2079/// `long double` is **not** routed here: it takes
2080/// `org.zerodds.wire.Float128`, which is a real binary128. The
2081/// `Float128` arm below exists so the match is total and answers for a
2082/// caller that wants the nearest built-in; `type_at` matches it before
2083/// it reaches this function. A caller that does not is the defect the
2084/// three-column gate found in the C column: `double` is eight bytes
2085/// where the member is sixteen.
2086///
2087/// `char` and `wchar` are both a Java `char`, as §1.5.2 has them. A Java
2088/// `char` is an unsigned 16-bit code unit, so it holds an IDL `char`'s
2089/// 0..255 and a `wchar`'s BMP code point alike; `Writer.writeChar`
2090/// refuses a code point above 255 rather than truncating it.
2091#[must_use]
2092pub const fn java_primitive(kind: PrimitiveKind) -> java::Primitive {
2093    match kind {
2094        PrimitiveKind::Boolean => java::Primitive::Boolean,
2095        PrimitiveKind::Char | PrimitiveKind::WChar => java::Primitive::Char,
2096        PrimitiveKind::Int8 => java::Primitive::Byte,
2097        // CORBA 3.3 part 3 §1.5.2 (the [JAVA-MAP] XTypes 1.3 §7.5.1 reuses): `octet` is `byte`.
2098        PrimitiveKind::Octet => java::Primitive::Byte,
2099        // A number in [0, 255], which a signed `byte` does not hold.
2100        PrimitiveKind::UInt8 => java::Primitive::Int,
2101        PrimitiveKind::Int16 => java::Primitive::Short,
2102        // A number in [0, 65535].
2103        PrimitiveKind::UInt16 => java::Primitive::Int,
2104        PrimitiveKind::Int32 => java::Primitive::Int,
2105        // A number in [0, 4294967295].
2106        PrimitiveKind::UInt32 => java::Primitive::Long,
2107        PrimitiveKind::Int64 => java::Primitive::Long,
2108        // The bit pattern: Java has no integer wider than `long`.
2109        PrimitiveKind::UInt64 => java::Primitive::Long,
2110        PrimitiveKind::Float32 => java::Primitive::Float,
2111        PrimitiveKind::Float64 | PrimitiveKind::Float128 => java::Primitive::Double,
2112    }
2113}
2114
2115/// The Java type an unsigned value of `bits` bits is held in.
2116///
2117/// The same rule as the primitive table, applied to a width the source
2118/// gave as a number: up to 16 bits is a number in an `int`, 17 to 32 a
2119/// number in a `long`, and 33 to 64 a pattern in a `long`. A bitmask of
2120/// bound 32 therefore takes a `long` and not an `int` — an `int` would
2121/// hold all 32 bits, but widening it for `writeBitmask(long, int)` would
2122/// sign-extend bit 31 into the top half.
2123#[must_use]
2124pub fn unsigned_holder(bits: u16) -> java::Type {
2125    if bits <= 16 {
2126        java::Type::Primitive(java::Primitive::Int)
2127    } else {
2128        java::Type::Primitive(java::Primitive::Long)
2129    }
2130}
2131
2132/// A type that can hold `null`, for an `@optional` member.
2133///
2134/// A primitive takes its wrapper class, fully qualified: a generated
2135/// class named `Integer` would otherwise capture the simple name.
2136/// Everything else is a reference already.
2137fn boxed(type_: java::Type) -> java::Type {
2138    match type_ {
2139        java::Type::Primitive(primitive) => {
2140            java::Type::named(Path::new(&["java", "lang", primitive.boxed()]))
2141        }
2142        other => other,
2143    }
2144}
2145
2146/// What a `native` declaration maps to.
2147///
2148/// The facet if a `#pragma` or an annotation gave one, and
2149/// `java.lang.Object` otherwise — never a guess at what the name might
2150/// mean.
2151fn native_type(ast: &Ast, id: Id<TypeNode>) -> java::Type {
2152    let mapping = ast
2153        .any_of_type(id)
2154        .ok()
2155        .and_then(|any| ast.facets(any))
2156        .and_then(|facets| facets.corba.as_ref())
2157        .and_then(|corba| corba.native_mapping.clone());
2158    match mapping {
2159        Some(spelling) => java::Type::named(Path::new(&spelling.split('.').collect::<Vec<&str>>())),
2160        None => java::Type::named(Path::new(&["java", "lang", "Object"])),
2161    }
2162}
2163
2164/// A named type's path, written from [`OUTER`] down.
2165///
2166/// `ZdTypes.a.b.Point` and not `a.b.Point`: JLS §6.5.2 resolves the first
2167/// segment of a qualified name to a variable in scope before a type, so a
2168/// class carrying a field named `a` would turn `a.b.Point` into a compile
2169/// error — and only in the corpora that happen to have such a field.
2170///
2171/// # Errors
2172/// [`MaterialiseError::NoModule`] when the model will not answer for a
2173/// module the type nests in.
2174fn qualified(ast: &Ast, id: Id<TypeNode>) -> Result<Path, MaterialiseError> {
2175    let node = ast.type_(id).map_err(|_| MaterialiseError::Unevaluated {
2176        construct: "type".to_string(),
2177        at: OUTER.to_string(),
2178    })?;
2179    let mut segments = vec![OUTER.to_string()];
2180    for (_, name) in module_chain(ast, node.module)? {
2181        segments.push(name);
2182    }
2183    segments.push(java::escape_keyword(&node.name));
2184    Ok(Path::new(
2185        &segments.iter().map(String::as_str).collect::<Vec<&str>>(),
2186    ))
2187}
2188
2189/// The map entry class a `map<K,V>` needs a name for.
2190///
2191/// # Errors
2192/// As [`type_tag`].
2193pub fn map_entry_name(ast: &Ast, id: Id<TypeNode>) -> Result<String, MaterialiseError> {
2194    let Ok(node) = ast.type_(id) else {
2195        return Err(MaterialiseError::Unevaluated {
2196            construct: "map".to_string(),
2197            at: OUTER.to_string(),
2198        });
2199    };
2200    let TypeKind::Map { key, value, .. } = &node.kind else {
2201        return Err(MaterialiseError::Unevaluated {
2202            construct: "map".to_string(),
2203            at: named(&node.name, node.node_id),
2204        });
2205    };
2206    let (Some(key), Some(value)) = (key.resolved(), value.resolved()) else {
2207        return Err(MaterialiseError::Unevaluated {
2208            construct: "map key or value".to_string(),
2209            at: named(&node.name, node.node_id),
2210        });
2211    };
2212    Ok(format!(
2213        "{ENTRY_PREFIX}{}_{}",
2214        type_tag(ast, key, 0)?,
2215        type_tag(ast, value, 0)?
2216    ))
2217}
2218/// The entry class of the map class `class` (`ZdMap<bound>_<key>_<value>`, the
2219/// bound being absent or `_b<digits>` or `_bx`): `ZdEntry_<key>_<value>`.
2220///
2221/// The inverse of how [`map_class_name`] and [`map_entry_name`] build the
2222/// two names, for a caller that has the class and not the model. A key type
2223/// named like a bound (`b5`) cannot be told apart from one; the bound is
2224/// taken off only when a key and a value remain.
2225#[must_use]
2226pub fn entry_class_of_map_class(class: &str) -> Option<String> {
2227    let rest = class.strip_prefix(MAP_PREFIX)?;
2228    let rest = rest.strip_prefix('_').map_or(rest, |tail| {
2229        // `_b<digits>_` or `_bx_` in front of the key and the value
2230        let after = tail.strip_prefix('b').and_then(|bound| {
2231            let end = bound.find('_')?;
2232            let (digits, remainder) = bound.split_at(end);
2233            let is_bound =
2234                digits == "x" || (!digits.is_empty() && digits.bytes().all(|b| b.is_ascii_digit()));
2235            (is_bound && remainder[1..].contains('_')).then(|| &remainder[1..])
2236        });
2237        after.unwrap_or(tail)
2238    });
2239    // `rest` is `<key>_<value>` (the leading `_` was taken above)
2240    rest.contains('_').then(|| format!("{ENTRY_PREFIX}{rest}"))
2241}
2242
2243/// The class of a `map<K,V>`: `ZdMap<bound>_<key>_<value>`.
2244///
2245/// # Errors
2246/// As [`type_tag`].
2247pub fn map_class_name(ast: &Ast, id: Id<TypeNode>) -> Result<String, MaterialiseError> {
2248    let Ok(node) = ast.type_(id) else {
2249        return Err(MaterialiseError::Unevaluated {
2250            construct: "map".to_string(),
2251            at: OUTER.to_string(),
2252        });
2253    };
2254    let TypeKind::Map { key, value, bound } = &node.kind else {
2255        return Err(MaterialiseError::Unevaluated {
2256            construct: "map".to_string(),
2257            at: named(&node.name, node.node_id),
2258        });
2259    };
2260    let (Some(key), Some(value)) = (key.resolved(), value.resolved()) else {
2261        return Err(MaterialiseError::Unevaluated {
2262            construct: "map key or value".to_string(),
2263            at: named(&node.name, node.node_id),
2264        });
2265    };
2266    Ok(format!(
2267        "{MAP_PREFIX}{}_{}_{}",
2268        bound_tag(ast, bound),
2269        type_tag(ast, key, 0)?,
2270        type_tag(ast, value, 0)?
2271    ))
2272}
2273
2274/// Whether the key type of a map is one a `java.util.Map` can be keyed by:
2275/// a primitive, a string, an enumeration or a bitmask, whose Java objects
2276/// compare by value, and a structure, a union or a bit set, whose generated class
2277/// has `equals` and `hashCode` over its members (see
2278/// `equals_and_hash_code`).
2279#[must_use]
2280pub fn map_key_supported(ast: &Ast, key: Id<TypeNode>) -> bool {
2281    let mut current = key;
2282    for _ in 0..MAX_TYPE_DEPTH {
2283        let Ok(node) = ast.type_(current) else {
2284            return false;
2285        };
2286        match &node.kind {
2287            TypeKind::Primitive(PrimitiveKind::Float128) => return false,
2288            TypeKind::Primitive(_)
2289            | TypeKind::String { .. }
2290            | TypeKind::Enumeration { .. }
2291            | TypeKind::Bitmask { .. }
2292            | TypeKind::Structure { .. }
2293            | TypeKind::Union { .. }
2294            | TypeKind::Bitset { .. } => return true,
2295            TypeKind::Alias { aliased } => match aliased.resolved() {
2296                Some(next) => current = next,
2297                None => return false,
2298            },
2299            _ => return false,
2300        }
2301    }
2302    false
2303}
2304
2305/// The class of a map, which "shall be represented in Java by an
2306/// implementation of the standard java.util.Map interface" (XTypes 1.3
2307/// §7.5.1.3.5): a `LinkedHashMap` of the boxed key and value types, so
2308/// that the order the entries were put in is the order they go on the
2309/// wire in (`MapEntryOrder` sorts them when asked to).
2310///
2311/// What Table 47 asks beyond `java.util.Map` and Java does not have is
2312/// here: the capacity (`getMaxSize`, `setMaxSize`), `copyFrom` with
2313/// `autogrow`, and `insert` with `replace`, which answer with the return
2314/// codes of DDS 1.4. The rest is native: `new` and `initialize` are the
2315/// constructor, `delete` and `finalize` the garbage collector (or
2316/// `clear`), `get_size` is `size`, `clear` is `clear`, `erase` is
2317/// `remove`, `find_element` is `get`, and `get_first`, `get_next`,
2318/// `find_entry` and `get_pair` are the iterator of `entrySet` and
2319/// `Map.Entry`. `entries` and `of` are the codec's: it reads and writes
2320/// the entry class, in order.
2321fn map_class(
2322    ast: &Ast,
2323    id: Id<TypeNode>,
2324    key: Id<TypeNode>,
2325    value: Id<TypeNode>,
2326    class: &str,
2327    entry: &str,
2328    at: &str,
2329) -> Result<java::Member, MaterialiseError> {
2330    let bound = match ast.type_(id).map(|node| &node.kind) {
2331        Ok(TypeKind::Map { bound, .. }) => match bound.expr() {
2332            None => 0,
2333            Some(expr) => ast
2334                .eval_integral(expr)
2335                .ok()
2336                .and_then(|limit| u32::try_from(limit).ok())
2337                .ok_or_else(|| MaterialiseError::Unevaluated {
2338                    construct: "map bound".to_string(),
2339                    at: at.to_string(),
2340                })?,
2341        },
2342        _ => 0,
2343    };
2344    let key_type = zerodds_lang_render::java::type_(&boxed(java_member_type(ast, key, at)?));
2345    let value_type = zerodds_lang_render::java::type_(&boxed(java_member_type(ast, value, at)?));
2346    let entry = format!("{OUTER}.{entry}");
2347    let text = format!(
2348        "    private static final long serialVersionUID = 1L;
2349    /** `map<K, V, N>`: N, or 0 for a map with no bound. */
2350    private static final int ZD_BOUND = {bound};
2351    /** DDS 1.4 2.2.2.1.1, ReturnCode_t. */
2352    public static final int RETCODE_OK = 0;
2353    /** DDS 1.4 2.2.2.1.1, ReturnCode_t. */
2354    public static final int RETCODE_ERROR = 1;
2355    /** DDS 1.4 2.2.2.1.1, ReturnCode_t. */
2356    public static final int RETCODE_BAD_PARAMETER = 3;
2357    /** DDS 1.4 2.2.2.1.1, ReturnCode_t. */
2358    public static final int RETCODE_PRECONDITION_NOT_MET = 4;
2359    private int zdMaximum;
2360
2361    /** Table 47 initialize: the size and the capacity are 0. */
2362    public {class}() {{
2363        super();
2364    }}
2365
2366    /**
2367     * Table 47 get_max_size: how many entries the map may contain. A map
2368     * the codec filled holds the entries it read, so it is never below
2369     * the size.
2370     */
2371    public int getMaxSize() {{
2372        return Math.max(zdMaximum, size());
2373    }}
2374
2375    /**
2376     * Table 47 set_max_size: BAD_PARAMETER if the new maximum is below
2377     * the size or above the bound.
2378     */
2379    public int setMaxSize(int maxSize) {{
2380        if (maxSize < size() || (ZD_BOUND != 0 && maxSize > ZD_BOUND)) {{
2381            return RETCODE_BAD_PARAMETER;
2382        }}
2383        zdMaximum = maxSize;
2384        return RETCODE_OK;
2385    }}
2386
2387    /**
2388     * Table 47 insert (for {{@code replace}} false) and insert_or_assign (for
2389     * true): an existing key is BAD_PARAMETER or has its element replaced, a
2390     * new one past the capacity is PRECONDITION_NOT_MET. A null key or
2391     * element is BAD_PARAMETER.
2392     */
2393    public int insert({key_type} key, {value_type} element, boolean replace) {{
2394        if (key == null || element == null) {{
2395            return RETCODE_BAD_PARAMETER;
2396        }}
2397        if (containsKey(key)) {{
2398            if (!replace) {{
2399                return RETCODE_BAD_PARAMETER;
2400            }}
2401            put(key, element);
2402            return RETCODE_OK;
2403        }}
2404        if (size() >= getMaxSize()) {{
2405            return RETCODE_PRECONDITION_NOT_MET;
2406        }}
2407        put(key, element);
2408        return RETCODE_OK;
2409    }}
2410
2411    /**
2412     * Table 47 copy: overwrite this map with {{@code other}}. Too large for
2413     * the capacity, {{@code autogrow}} decides: the capacity grows, or the
2414     * answer is PRECONDITION_NOT_MET and this map stays as it was.
2415     */
2416    public int copyFrom({class} other, boolean autogrow) {{
2417        if (other == null) {{
2418            return RETCODE_BAD_PARAMETER;
2419        }}
2420        if (other == this) {{
2421            return RETCODE_OK;
2422        }}
2423        if (other.size() > getMaxSize()) {{
2424            if (!autogrow) {{
2425                return RETCODE_PRECONDITION_NOT_MET;
2426            }}
2427            int grown = setMaxSize(other.size());
2428            if (grown != RETCODE_OK) {{
2429                return grown;
2430            }}
2431        }}
2432        clear();
2433        putAll(other);
2434        return RETCODE_OK;
2435    }}
2436
2437    /** The entries, in the order of the map: what the codec writes. */
2438    public {entry}[] entries() {{
2439        {entry}[] out = new {entry}[size()];
2440        int i = 0;
2441        for (java.util.Map.Entry<{key_type}, {value_type}> e : entrySet()) {{
2442            out[i] = new {entry}();
2443            out[i].key = e.getKey();
2444            out[i].value = e.getValue();
2445            i = i + 1;
2446        }}
2447        return out;
2448    }}
2449
2450    /**
2451     * The map of entries, in their order: what the codec reads. A key
2452     * twice is no map and is refused.
2453     */
2454    public static {class} of({entry}[] entries) {{
2455        {class} map = new {class}();
2456        for ({entry} e : entries) {{
2457            if (map.containsKey(e.key)) {{
2458                throw new java.lang.IllegalArgumentException(\"the map has the key \" + e.key + \" twice\");
2459            }}
2460            map.put(e.key, e.value);
2461        }}
2462        map.zdMaximum = map.size();
2463        return map;
2464    }}
2465
2466    /** No entries: where a reader starts. */
2467    public static {entry}[] noEntries() {{
2468        return new {entry}[0];
2469    }}
2470"
2471    );
2472    Ok(java::Member::Nested(Box::new(java::TypeDecl::Class(
2473        java::Class {
2474            doc: Doc::lines(&[
2475                "A map (XTypes 1.3 7.5.1.3.5): a java.util.Map in the order the entries",
2476                "were put, with the operations of Table 47 that Java does not have.",
2477            ]),
2478            annotations: Vec::new(),
2479            modifiers: vec![
2480                java::Modifier::Public,
2481                java::Modifier::Static,
2482                java::Modifier::Final,
2483            ],
2484            name: Ident::new(class),
2485            generics: Vec::new(),
2486            extends: Some(java::Type::generic(
2487                Path::new(&["java", "util", "LinkedHashMap"]),
2488                vec![
2489                    boxed(java_member_type(ast, key, at)?),
2490                    boxed(java_member_type(ast, value, at)?),
2491                ],
2492            )),
2493            implements: Vec::new(),
2494            members: vec![java::Member::Verbatim(text)],
2495        },
2496    ))))
2497}
2498
2499/// Where a declaration sits, as Java has to write it: `ZdTypes.a.b.Name`.
2500///
2501/// [`type_tag`] joins the same segments with `_` and is a *name* — what
2502/// a generated codec method is called after. This is the *path*, which
2503/// is what reaches the class: every declaration nests inside `ZdTypes`
2504/// because Java allows one public top-level type per file, and a caller
2505/// that spelled `ZdTypes.a_b_Name` would name nothing.
2506///
2507/// # Errors
2508/// When the module chain nests past the cap.
2509pub fn declared_path(ast: &Ast, id: Id<TypeNode>) -> Result<String, MaterialiseError> {
2510    let node = ast.type_(id).map_err(|_| MaterialiseError::Unevaluated {
2511        construct: "type".to_string(),
2512        at: OUTER.to_string(),
2513    })?;
2514    let mut segments = vec!["ZdTypes".to_string()];
2515    for (_, name) in module_chain(ast, node.module)? {
2516        segments.push(name);
2517    }
2518    segments.push(java::escape_keyword(&node.name));
2519    Ok(segments.join("."))
2520}
2521
2522/// The bound a `string`, a `sequence` or a `map` carries, as the suffix
2523/// that keeps two of them apart.
2524///
2525/// The tag also names the routine a hoisted collection's codec is
2526/// emitted under, and one body is emitted per name. With the bound left
2527/// out, `sequence<long>` and `sequence<long, 4>` are one name and
2528/// whichever was declared first decides: measured on
2529/// `@final struct S { sequence<long> b; sequence<long, 4> a; };`, the
2530/// generated Java held no `zd_over` check at all, so `a` accepted a
2531/// stream of any length; with the members the other way round both got
2532/// the bounded body, so `b` refused — and truncated — a length the IDL
2533/// puts no limit on. `string<4>` beside `string` and `wstring<4>`
2534/// beside `wstring` collapse the same way, onto the trimming call.
2535/// Either way the reader answered with a value the IDL does not
2536/// describe and said nothing, which is why the bound is part of the
2537/// name. The same suffix the Zig column carries.
2538///
2539/// An `array` needs nothing here: its `dims` are already in the tag,
2540/// under the `Array` arm below.
2541fn bound_tag(ast: &Ast, bound: &zerodds_ast::Bound) -> String {
2542    match bound.expr() {
2543        None => String::new(),
2544        // `_b` and not a bare `_n`: `seq_long_4` is what the `Array`
2545        // arm answers for an array of four `long`, and the two are
2546        // different wire forms.
2547        Some(expr) => match ast.eval_integral(expr) {
2548            Ok(limit) => format!("_b{limit}"),
2549            // As the array's `_x`: a bound that does not evaluate is
2550            // still not the unbounded form, and the materialiser
2551            // reports the expression that would not evaluate.
2552            Err(_) => "_bx".to_string(),
2553        },
2554    }
2555}
2556
2557/// The name component an entry class takes from one of its halves.
2558///
2559/// The IDL kind and not the Java spelling, and this is not cosmetic:
2560/// `octet` and `unsigned short` are both a Java `int`, and they are one
2561/// and two bytes on the wire. A structural name built from the Java type
2562/// would give `map<octet,long>` and `map<unsigned short,long>` one entry
2563/// class, and a codec written against it would put one of the two out at
2564/// the other's width with nothing to say it had. An alias is followed
2565/// through, because a typedef is transparent in Java and the two names
2566/// would otherwise describe the same class twice.
2567///
2568/// # Errors
2569/// [`MaterialiseError`] when a module or an array dimension will not
2570/// answer.
2571///
2572/// zerodds-lint: recursion-depth 64 (collection nesting; bounded by `MAX_TYPE_DEPTH`)
2573pub fn type_tag(ast: &Ast, id: Id<TypeNode>, depth: usize) -> Result<String, MaterialiseError> {
2574    if depth > MAX_TYPE_DEPTH {
2575        return Err(MaterialiseError::Unevaluated {
2576            construct: format!("type, whose name nests past {MAX_TYPE_DEPTH}"),
2577            at: OUTER.to_string(),
2578        });
2579    }
2580    let Ok(node) = ast.type_(id) else {
2581        return Ok("unknown".to_string());
2582    };
2583    Ok(match &node.kind {
2584        TypeKind::Primitive(kind) => kind.as_str().replace(' ', "_"),
2585        TypeKind::String {
2586            width: CharWidth::Char,
2587            bound,
2588        } => format!("string{}", bound_tag(ast, bound)),
2589        TypeKind::String {
2590            width: CharWidth::WChar,
2591            bound,
2592        } => format!("wstring{}", bound_tag(ast, bound)),
2593        TypeKind::Fixed { digits, scale } => format!("fixed_{digits}_{scale}"),
2594        TypeKind::Sequence { element, bound } => match element.resolved() {
2595            Some(element) => format!(
2596                "seq{}_{}",
2597                bound_tag(ast, bound),
2598                type_tag(ast, element, depth + 1)?
2599            ),
2600            None => format!("seq_unknown{}", bound_tag(ast, bound)),
2601        },
2602        TypeKind::Array { element, dims } => {
2603            let mut name = match element.resolved() {
2604                Some(element) => format!("arr_{}", type_tag(ast, element, depth + 1)?),
2605                None => "arr_unknown".to_string(),
2606            };
2607            for dim in dims {
2608                match ast.eval_integral(*dim) {
2609                    Ok(length) => name.push_str(&format!("_{length}")),
2610                    // A dimension that does not evaluate gets no number
2611                    // rather than a zero: the name has to differ from a
2612                    // `[0]` array's, and `declare` reports the failure by
2613                    // name.
2614                    Err(_) => name.push_str("_x"),
2615                }
2616            }
2617            name
2618        }
2619        // The entry class's own name carries no bound — two maps that
2620        // differ only in bound hold the same pairs — but the tag does,
2621        // for the routine-name reason in [`bound_tag`].
2622        TypeKind::Map { key, value, bound } => {
2623            let key = match key.resolved() {
2624                Some(key) => type_tag(ast, key, depth + 1)?,
2625                None => "unknown".to_string(),
2626            };
2627            let value = match value.resolved() {
2628                Some(value) => type_tag(ast, value, depth + 1)?,
2629                None => "unknown".to_string(),
2630            };
2631            format!("map{}_{key}_{value}", bound_tag(ast, bound))
2632        }
2633        TypeKind::Alias { aliased } => match aliased.resolved() {
2634            Some(target) => type_tag(ast, target, depth + 1)?,
2635            None => "unknown".to_string(),
2636        },
2637        TypeKind::ValueBox { boxed: inner } => match inner.resolved() {
2638            Some(target) => format!("box_{}", type_tag(ast, target, depth + 1)?),
2639            None => "box_unknown".to_string(),
2640        },
2641        TypeKind::Structure { .. }
2642        | TypeKind::Union { .. }
2643        | TypeKind::Enumeration { .. }
2644        | TypeKind::Bitmask { .. }
2645        | TypeKind::Bitset { .. }
2646        | TypeKind::Valuetype { .. }
2647        | TypeKind::Native => {
2648            let mut segments = Vec::new();
2649            for (_, name) in module_chain(ast, node.module)? {
2650                segments.push(name);
2651            }
2652            segments.push(java::escape_keyword(&node.name));
2653            segments.join("_")
2654        }
2655        TypeKind::Any => "any".to_string(),
2656        TypeKind::ValueBase => "valuebase".to_string(),
2657        TypeKind::ObjectRef { .. } => "objectref".to_string(),
2658        TypeKind::AnnotationType { .. } => "annotation".to_string(),
2659    })
2660}
2661
2662/// `public static final class <name> { … }`, as a member of its scope.
2663fn nested_class(name: &str, members: Vec<java::Member>, doc: Doc) -> java::Member {
2664    nested_class_extending(name, members, doc, None, true)
2665}
2666
2667/// [`nested_class`] of a class that extends another, and that is not
2668/// `final` where another extends it (a class with a subtype cannot be).
2669fn nested_class_extending(
2670    name: &str,
2671    members: Vec<java::Member>,
2672    doc: Doc,
2673    extends: Option<java::Type>,
2674    final_: bool,
2675) -> java::Member {
2676    let mut modifiers = vec![java::Modifier::Public, java::Modifier::Static];
2677    if final_ {
2678        modifiers.push(java::Modifier::Final);
2679    }
2680    java::Member::Nested(Box::new(java::TypeDecl::Class(java::Class {
2681        doc,
2682        annotations: Vec::new(),
2683        modifiers,
2684        name: Ident::new(name),
2685        generics: Vec::new(),
2686        extends,
2687        implements: Vec::new(),
2688        members,
2689    })))
2690}
2691
2692/// Whether another structure extends this one: the class is not `final` then.
2693pub(crate) fn has_derived(ast: &Ast, id: Id<TypeNode>) -> bool {
2694    ast.types().any(|(_, node)| {
2695        matches!(
2696            &node.kind,
2697            TypeKind::Structure { base: Some(base), .. } if base.resolved() == Some(id)
2698        )
2699    })
2700}
2701
2702/// A class of data members: an IDL structure, union, bit set or exception.
2703///
2704/// [`nested_class`] under [`JavaMapping::Omg`]; under [`JavaMapping::Psm`]
2705/// the class of DDS-Java-PSM 1.0 §9.1.1, see [`encapsulate`].
2706fn data_class(
2707    name: &str,
2708    members: Vec<java::Member>,
2709    doc: Doc,
2710    extends: Option<java::Type>,
2711    final_: bool,
2712) -> Result<java::Member, MaterialiseError> {
2713    let value_methods = equals_and_hash_code(name, &members, extends.is_some());
2714    let mut members = if mapping() == JavaMapping::Psm {
2715        encapsulate(name, members)?
2716    } else {
2717        members
2718    };
2719    members.push(java::Member::Verbatim(value_methods));
2720    Ok(nested_class_extending(name, members, doc, extends, final_))
2721}
2722
2723/// `equals` and `hashCode` of a class of data members: two values are equal
2724/// when every member is, an array by its elements. XTypes 1.3 §7.5.1.3.5
2725/// asks a `java.util.Map` for every key type of IDL, and a structure or a
2726/// union is a key by its members; a `HashMap` finds a key by these two
2727/// methods. A derived class adds its base's.
2728fn equals_and_hash_code(name: &str, members: &[java::Member], derived: bool) -> String {
2729    let fields: Vec<&str> = members
2730        .iter()
2731        .filter_map(|member| match member {
2732            java::Member::Field(data) if !data.modifiers.contains(&java::Modifier::Static) => {
2733                Some(data.name.as_str())
2734            }
2735            _ => None,
2736        })
2737        .collect();
2738    let mut equal = String::new();
2739    if derived {
2740        equal.push_str("super.equals(other)");
2741    }
2742    for field in &fields {
2743        if !equal.is_empty() {
2744            equal.push_str("\n            && ");
2745        }
2746        equal.push_str(&format!(
2747            "java.util.Objects.deepEquals({field}, that.{field})"
2748        ));
2749    }
2750    if equal.is_empty() {
2751        equal.push_str("true");
2752    }
2753    let mut hashed: Vec<String> = Vec::new();
2754    if derived {
2755        hashed.push("super.hashCode()".to_string());
2756    }
2757    hashed.extend(fields.iter().map(|field| (*field).to_string()));
2758    format!(
2759        "    /** Equal when every member is equal; an array by its elements. */
2760    @Override
2761    public boolean equals(Object other) {{
2762        if (this == other) {{
2763            return true;
2764        }}
2765        if (other == null || getClass() != other.getClass()) {{
2766            return false;
2767        }}
2768        {name} that = ({name}) other;
2769        return {equal};
2770    }}
2771
2772    /** Consistent with {{@link #equals(Object)}}. */
2773    @Override
2774    public int hashCode() {{
2775        return java.util.Arrays.deepHashCode(new Object[] {{ {} }});
2776    }}
2777",
2778        hashed.join(", ")
2779    )
2780}
2781
2782/// DDS-Java-PSM 1.0 §9.1.1: "Contained attributes shall be encapsulated.
2783/// Java Bean style accessors shall be provided. [...] The representation
2784/// of internal state shall be private."
2785///
2786/// Every instance field becomes private. The class gets an explicit
2787/// no-argument constructor (§8.6 asks for one of a type a Service
2788/// instantiates reflectively; this is the one the codec calls) and
2789/// `getX()`/`setX(v)` for each field; a `boolean` field has `isX()` as
2790/// well, the "special mapping rule for boolean properties" §9.1.1 allows,
2791/// and `getX()` too, which is what the codec calls because it does not
2792/// know the type.
2793///
2794/// # Errors
2795/// [`MaterialiseError::SpellingCollision`] when two members make one
2796/// accessor name, or a member makes the name of another's accessor.
2797fn encapsulate(
2798    class_name: &str,
2799    members: Vec<java::Member>,
2800) -> Result<Vec<java::Member>, MaterialiseError> {
2801    let mut fields = Vec::new();
2802    let mut accessors = Vec::new();
2803    let mut others = Vec::new();
2804    let mut names: Vec<(String, String)> = Vec::new();
2805    for member in members {
2806        let java::Member::Field(mut data) = member else {
2807            others.push(member);
2808            continue;
2809        };
2810        if data.modifiers.contains(&java::Modifier::Static) {
2811            fields.push(java::Member::Field(data));
2812            continue;
2813        }
2814        let field_name = data.name.as_str().to_string();
2815        let type_ = data.type_.clone();
2816        data.modifiers = vec![java::Modifier::Private];
2817        fields.push(java::Member::Field(data));
2818
2819        let this = |name: &str| java::Expr::Field {
2820            value: Box::new(java::Expr::Path(Path::new(&["this"]))),
2821            name: Ident::new(name),
2822        };
2823        let getter = |name: String| {
2824            java::Member::Method(java::Method {
2825                doc: Doc::none(),
2826                annotations: Vec::new(),
2827                modifiers: vec![java::Modifier::Public],
2828                generics: Vec::new(),
2829                returns: type_.clone(),
2830                name: Ident::new(&name),
2831                params: Vec::new(),
2832                throws: Vec::new(),
2833                body: Some(java::Block {
2834                    statements: vec![java::Statement::Return(Some(this(&field_name)))],
2835                }),
2836            })
2837        };
2838        names.push((getter_name(&field_name), field_name.clone()));
2839        accessors.push(getter(getter_name(&field_name)));
2840        if type_ == java::Type::Primitive(java::Primitive::Boolean) {
2841            names.push((boolean_getter_name(&field_name), field_name.clone()));
2842            accessors.push(getter(boolean_getter_name(&field_name)));
2843        }
2844        names.push((setter_name(&field_name), field_name.clone()));
2845        accessors.push(java::Member::Method(java::Method {
2846            doc: Doc::none(),
2847            annotations: Vec::new(),
2848            modifiers: vec![java::Modifier::Public],
2849            generics: Vec::new(),
2850            returns: java::Type::Void,
2851            name: Ident::new(&setter_name(&field_name)),
2852            params: vec![java::Param {
2853                annotations: Vec::new(),
2854                final_: false,
2855                type_: type_.clone(),
2856                name: Ident::new("v"),
2857                varargs: false,
2858            }],
2859            throws: Vec::new(),
2860            body: Some(java::Block {
2861                statements: vec![java::Statement::Expr(java::Expr::Binary {
2862                    op: "=",
2863                    lhs: Box::new(this(&field_name)),
2864                    rhs: Box::new(java::Expr::Path(Path::new(&["v"]))),
2865                })],
2866            }),
2867        }));
2868    }
2869    no_spelling_collision(
2870        names
2871            .iter()
2872            .map(|(spelled, declared)| (spelled.as_str(), declared.as_str())),
2873        "Java accessor",
2874        class_name,
2875    )?;
2876    let mut out = others;
2877    out.extend(fields);
2878    out.push(java::Member::Constructor(java::Constructor {
2879        doc: Doc::none(),
2880        annotations: Vec::new(),
2881        modifiers: vec![java::Modifier::Public],
2882        name: Ident::new(class_name),
2883        params: Vec::new(),
2884        throws: Vec::new(),
2885        body: java::Block {
2886            statements: Vec::new(),
2887        },
2888    }));
2889    out.extend(accessors);
2890    Ok(out)
2891}
2892
2893/// `public <type> <name>;`
2894///
2895/// Public and not accessor-wrapped: §1.5.10 of the OMG mapping makes a
2896/// struct's members public fields, the JacORB column's fill statements
2897/// assign to them, and a generated codec that had to call a getter would
2898/// be reading a decision nothing on the wire made.
2899fn field(type_: java::Type, name: &str, doc: Doc) -> java::Member {
2900    java::Member::Field(java::Field {
2901        doc,
2902        annotations: Vec::new(),
2903        modifiers: vec![java::Modifier::Public],
2904        type_,
2905        name: Ident::new(&java::escape_keyword(name)),
2906        value: None,
2907    })
2908}
2909
2910/// The pre-Java-5 "type-safe enumeration" class of DDS-XTypes 1.3
2911/// §7.5.1.2.1.3, with the shape [JAVA-MAP] gives it.
2912///
2913/// `public static final int _<label> = <value>;` per literal, and an
2914/// object `public static final <Class> <label>` per literal built from
2915/// it. `value()` answers the number and `from_int` the object, and
2916/// refuses a number the enumeration does not list.
2917///
2918/// `value_type` is `int` for an enumeration; a bitmask's flag
2919/// enumeration takes the holder of its bound, which for 17 to 64 bits is
2920/// a `long`.
2921fn type_safe_enumeration(
2922    class: &str,
2923    value_type: java::Type,
2924    literals: Vec<(String, java::Expr)>,
2925    doc: Doc,
2926) -> java::Member {
2927    let own = || java::Type::named(Path::single(class));
2928    let path = |name: &str| java::Expr::Path(Path::single(name));
2929    let mut members = Vec::new();
2930    for (label, value) in &literals {
2931        members.push(constant(
2932            value_type.clone(),
2933            &format!("_{label}"),
2934            value.clone(),
2935        ));
2936    }
2937    for (label, _) in &literals {
2938        members.push(constant(
2939            own(),
2940            label,
2941            java::Expr::New {
2942                type_: Box::new(own()),
2943                args: vec![path(&format!("_{label}"))],
2944            },
2945        ));
2946    }
2947    members.push(java::Member::Field(java::Field {
2948        doc: Doc::none(),
2949        annotations: Vec::new(),
2950        modifiers: vec![java::Modifier::Private, java::Modifier::Final],
2951        type_: value_type.clone(),
2952        name: Ident::new("__value"),
2953        value: None,
2954    }));
2955    members.push(java::Member::Constructor(java::Constructor {
2956        doc: Doc::none(),
2957        annotations: Vec::new(),
2958        modifiers: vec![java::Modifier::Private],
2959        name: Ident::new(class),
2960        params: vec![java::Param {
2961            annotations: Vec::new(),
2962            final_: false,
2963            type_: value_type.clone(),
2964            name: Ident::new("v"),
2965            varargs: false,
2966        }],
2967        throws: Vec::new(),
2968        body: java::Block {
2969            statements: vec![java::Statement::Expr(java::Expr::Binary {
2970                op: "=",
2971                lhs: Box::new(java::Expr::Field {
2972                    value: Box::new(java::Expr::This),
2973                    name: Ident::new("__value"),
2974                }),
2975                rhs: Box::new(path("v")),
2976            })],
2977        },
2978    }));
2979    members.push(java::Member::Method(java::Method {
2980        doc: Doc::line("The value the IDL gives the literal."),
2981        annotations: Vec::new(),
2982        modifiers: vec![java::Modifier::Public],
2983        generics: Vec::new(),
2984        returns: value_type.clone(),
2985        name: Ident::new("value"),
2986        params: Vec::new(),
2987        throws: Vec::new(),
2988        body: Some(java::Block {
2989            statements: vec![java::Statement::Return(Some(path("__value")))],
2990        }),
2991    }));
2992    let mut lookup: Vec<java::Statement> = Vec::new();
2993    for (label, _) in &literals {
2994        lookup.push(java::Statement::If {
2995            condition: java::Expr::Binary {
2996                op: "==",
2997                lhs: Box::new(path("v")),
2998                rhs: Box::new(path(&format!("_{label}"))),
2999            },
3000            then: java::Block {
3001                statements: vec![java::Statement::Return(Some(path(&java::escape_keyword(
3002                    label,
3003                ))))],
3004            },
3005            otherwise: None,
3006        });
3007    }
3008    lookup.push(java::Statement::Throw(java::Expr::New {
3009        type_: Box::new(java::Type::named(Path::new(&[
3010            "java",
3011            "lang",
3012            "IllegalArgumentException",
3013        ]))),
3014        args: vec![java::Expr::Binary {
3015            op: "+",
3016            lhs: Box::new(java::Expr::Literal(Literal::Str(format!(
3017                "not a value of {class}: "
3018            )))),
3019            rhs: Box::new(path("v")),
3020        }],
3021    }));
3022    members.push(java::Member::Method(java::Method {
3023        doc: Doc::line("The literal that has the value `v`."),
3024        annotations: Vec::new(),
3025        modifiers: vec![java::Modifier::Public, java::Modifier::Static],
3026        generics: Vec::new(),
3027        returns: own(),
3028        name: Ident::new("from_int"),
3029        params: vec![java::Param {
3030            annotations: Vec::new(),
3031            final_: false,
3032            type_: value_type,
3033            name: Ident::new("v"),
3034            varargs: false,
3035        }],
3036        throws: Vec::new(),
3037        body: Some(java::Block { statements: lookup }),
3038    }));
3039    nested_class(class, members, doc)
3040}
3041
3042/// `public static final <type> <name> = <value>;`
3043fn constant(type_: java::Type, name: &str, value: java::Expr) -> java::Member {
3044    java::Member::Field(java::Field {
3045        doc: Doc::none(),
3046        annotations: Vec::new(),
3047        modifiers: vec![
3048            java::Modifier::Public,
3049            java::Modifier::Static,
3050            java::Modifier::Final,
3051        ],
3052        type_,
3053        name: Ident::new(&java::escape_keyword(name)),
3054        value: Some(value),
3055    })
3056}
3057
3058/// The body of the class an enumeration is (CORBA 3.3 part 3 §1.5.9): one instance per
3059/// enumerator named by it, the `int` constants `_NAME`, `value()` and `from_int`, which
3060/// answers the instance of a value and refuses one no enumerator has. The codec reads a
3061/// value through the enumerator chain, so `from_int` is the application's door and the
3062/// codec never meets that refusal.
3063fn enumeration_class_body(class: &str, enumerators: &[(String, i128)]) -> String {
3064    let mut text = format!(
3065        "    private final int zdValue;\n\n    private {class}(int value) {{\n        zdValue = value;\n    }}\n\n"
3066    );
3067    for (name, value) in enumerators {
3068        text.push_str(&format!(
3069            "    /** The value of `{name}`. */\n    public static final int _{name} = {value};\n    /** The instance of `{name}`. */\n    public static final {class} {name} = new {class}(_{name});\n\n"
3070        ));
3071    }
3072    text.push_str("    /** The value of this enumerator. */\n    public int value() {\n        return zdValue;\n    }\n\n");
3073    text.push_str(&format!(
3074        "    /** The enumerator with this value (the first one, where two share it). */\n    public static {class} from_int(int value) {{\n"
3075    ));
3076    for (name, _) in enumerators {
3077        text.push_str(&format!(
3078            "        if (value == _{name}) {{\n            return {name};\n        }}\n"
3079        ));
3080    }
3081    text.push_str(&format!(
3082        "        throw new IllegalArgumentException(\"no enumerator of {class} has the value \" + value);\n    }}\n"
3083    ));
3084    text
3085}
3086
3087/// One case of a union, for [`union_accessors`].
3088struct UnionAccess {
3089    name: String,
3090    type_: String,
3091    /// The Java text of each label of the case; none for a `default:` case.
3092    labels: Vec<String>,
3093}
3094
3095/// The Java text of a union label, in the type of the discriminator.
3096fn java_label_text(ast: &Ast, discriminator: Id<TypeNode>, value: i128) -> String {
3097    let mut current = discriminator;
3098    for _ in 0..MAX_TYPE_DEPTH {
3099        match ast.type_(current).map(|node| &node.kind) {
3100            Ok(TypeKind::Alias { aliased }) => match aliased.resolved() {
3101                Some(next) => current = next,
3102                None => break,
3103            },
3104            Ok(TypeKind::Enumeration { .. }) => {
3105                let owner = declared_path(ast, current)
3106                    .unwrap_or_else(|_| format!("UnresolvedEnum{}", current.slot()));
3107                // A value above `int` is the bit pattern it occupies.
3108                let narrowed = i32::try_from(value).unwrap_or_else(|_| {
3109                    u32::try_from(value & 0xFFFF_FFFF)
3110                        .map_or(0, |bits| i32::from_ne_bytes(bits.to_ne_bytes()))
3111                });
3112                return format!("{owner}.from_int({narrowed})");
3113            }
3114            Ok(TypeKind::Primitive(PrimitiveKind::Boolean)) => {
3115                return if value == 0 { "false" } else { "true" }.to_string();
3116            }
3117            Ok(TypeKind::Primitive(kind)) => {
3118                return match java_primitive(*kind) {
3119                    java::Primitive::Byte => format!("(byte) {value}"),
3120                    java::Primitive::Short => format!("(short) {value}"),
3121                    java::Primitive::Char => format!("(char) {value}"),
3122                    // An `unsigned long long` label past `Long.MAX_VALUE` is the signed
3123                    // decimal with the same sixty-four bits, as in `CodecTarget::wide_int`.
3124                    java::Primitive::Long => {
3125                        #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
3126                        let pattern = if value > i128::from(i64::MAX) {
3127                            value as u64 as i64
3128                        } else {
3129                            value as i64
3130                        };
3131                        format!("{pattern}L")
3132                    }
3133                    _ => format!("{value}"),
3134                };
3135            }
3136            _ => break,
3137        }
3138    }
3139    format!("{value}")
3140}
3141
3142/// `discriminator()`, the accessor and the modifiers of each case (IDL to Java 1.3 §1.5.10).
3143///
3144/// An accessor refuses a case the discriminator does not select; the modifier of a case with one
3145/// label selects it, and every case has the modifier that names the discriminator, which is
3146/// refused when it selects another case.
3147fn union_accessors(discriminator: &str, cases: &[UnionAccess]) -> String {
3148    let all: Vec<&str> = cases
3149        .iter()
3150        .flat_map(|case| case.labels.iter().map(String::as_str))
3151        .collect();
3152    let any_label = |labels: &[&str]| -> String {
3153        if labels.is_empty() {
3154            "false".to_string()
3155        } else {
3156            labels
3157                .iter()
3158                .map(|label| format!("_d == {label}"))
3159                .collect::<Vec<_>>()
3160                .join(" || ")
3161        }
3162    };
3163    let mut text = format!(
3164        "    /** The discriminator. */\n    public {discriminator} discriminator() {{\n        return _d;\n    }}\n\n"
3165    );
3166    for case in cases {
3167        let own: Vec<&str> = case.labels.iter().map(String::as_str).collect();
3168        let selected = if own.is_empty() {
3169            format!("!({})", any_label(&all))
3170        } else {
3171            any_label(&own)
3172        };
3173        let (name, ty) = (&case.name, &case.type_);
3174        text.push_str(&format!(
3175            "    /** The value of `{name}`; refused when the discriminator selects another case. */\n    public {ty} {name}() {{\n        if (!({selected})) {{\n            throw new IllegalStateException(\"the discriminator does not select `{name}`\");\n        }}\n        return {name};\n    }}\n\n"
3176        ));
3177        if let [only] = own.as_slice() {
3178            text.push_str(&format!(
3179                "    /** Sets `{name}` and selects it. */\n    public void {name}({ty} value) {{\n        this.{name} = value;\n        this._d = {only};\n    }}\n\n"
3180            ));
3181        }
3182        text.push_str(&format!(
3183            "    /** Sets `{name}` under the discriminator `disc`, which has to select it. */\n    public void {name}({discriminator} disc, {ty} value) {{\n        {discriminator} previous = _d;\n        _d = disc;\n        if (!({selected})) {{\n            _d = previous;\n            throw new IllegalArgumentException(\"the discriminator does not select `{name}`\");\n        }}\n        this.{name} = value;\n    }}\n\n"
3184        ));
3185    }
3186    text
3187}
3188
3189/// The initialiser a constant of `type_` takes for `value`.
3190///
3191/// # Errors
3192/// [`MaterialiseError::NoLanguageForm`] when the value does not fit the
3193/// type Java gives it — never a truncation;
3194/// [`MaterialiseError::Unevaluated`] when the constant is not an integer,
3195/// which is what `Ast::eval_integral` answers for. A `const string` is
3196/// the case: the C column reports the same, and neither substitutes a
3197/// The Java expression a constant's value takes.
3198///
3199/// The declared type already carries the four string-shaped forms:
3200/// `java_member_type` answers `java.lang.String` for a `string` and a
3201/// `wstring`, so unlike C++ nothing here has to substitute a type. What
3202/// it does have to do is pick the right literal for each, and refuse the
3203/// one Java has no literal for.
3204///
3205/// A `wstring` becomes a `String` because a Java `String` *is* UTF-16 —
3206/// the code points go in directly and the renderer escapes what has to
3207/// be escaped. A `fixed` becomes `new java.math.BigDecimal("<decimal>")`,
3208/// the type the OMG mapping gives it.
3209///
3210/// `long double` is refused by name. Java has no 128-bit floating type,
3211/// `org.zerodds.wire.Float128` is a class with no literal, and a
3212/// `double` would put a different number in the file than the IDL wrote.
3213///
3214/// # Errors
3215/// [`MaterialiseError`] naming the constant and what will not hold it.
3216fn constant_expr(
3217    type_: &java::Type,
3218    value: &zerodds_ast::ConstValue,
3219    at: &str,
3220) -> Result<java::Expr, MaterialiseError> {
3221    use zerodds_ast::ConstValue;
3222    let refuse = |construct: String| MaterialiseError::NoLanguageForm {
3223        construct,
3224        language: "Java",
3225        at: at.to_string(),
3226    };
3227    match value {
3228        ConstValue::Integer(number) => literal_of(type_, *number, at),
3229        ConstValue::Boolean(flag) => Ok(java::Expr::Literal(Literal::Bool(*flag))),
3230        ConstValue::Char(character) => Ok(java::Expr::Literal(Literal::Char(*character))),
3231        // A Java `char` is one UTF-16 code unit, so a code point above
3232        // the Basic Multilingual Plane does not fit one and is refused
3233        // rather than truncated into a lone surrogate.
3234        ConstValue::WChar(point) => match constant::wchar_char(*point)
3235            .filter(|character| u32::from(*character) <= u32::from(u16::MAX))
3236        {
3237            Some(character) => Ok(java::Expr::Literal(Literal::Char(character))),
3238            None => Err(refuse(format!(
3239                "the code point U+{point:04X}, which a Java `char` does not hold"
3240            ))),
3241        },
3242        ConstValue::Float { mantissa, exponent } => {
3243            if !matches!(
3244                type_,
3245                java::Type::Primitive(java::Primitive::Float | java::Primitive::Double)
3246            ) {
3247                return Err(refuse(String::from(
3248                    "a `long double` constant, which has no Java literal",
3249                )));
3250            }
3251            let mut text = constant::float_text(mantissa, exponent.as_deref());
3252            // Without the suffix a decimal literal is a `double`, and
3253            // `float f = 1.5;` is a compile error rather than a
3254            // narrowing Java performs for you.
3255            if matches!(type_, java::Type::Primitive(java::Primitive::Float)) {
3256                text.push('f');
3257            }
3258            Ok(java::Expr::Literal(Literal::Float(text)))
3259        }
3260        ConstValue::Fixed { digits, scale } => Ok(java::Expr::New {
3261            type_: Box::new(java::Type::named(Path::new(BIG_DECIMAL))),
3262            args: vec![java::Expr::Literal(Literal::Str(constant::fixed_decimal(
3263                digits, *scale,
3264            )))],
3265        }),
3266        ConstValue::String(text) => Ok(java::Expr::Literal(Literal::Str(text.clone()))),
3267        ConstValue::WString(points) => match constant::wstring_utf8(points) {
3268            Some(text) => Ok(java::Expr::Literal(Literal::Str(text))),
3269            None => Err(refuse(String::from(
3270                "a `wstring` constant holding a lone surrogate",
3271            ))),
3272        },
3273        // `ConstValue` is `#[non_exhaustive]`, so a form added to the
3274        // model reaches here. Refused by name rather than given a
3275        // plausible default: a constant emitted as the wrong literal is
3276        // a field that disagrees with the IDL and compiles.
3277        _ => Err(refuse(String::from(
3278            "a constant form this binding has no literal for",
3279        ))),
3280    }
3281}
3282
3283/// value.
3284fn literal_of(type_: &java::Type, value: i128, at: &str) -> Result<java::Expr, MaterialiseError> {
3285    let java::Type::Primitive(primitive) = type_ else {
3286        return Err(MaterialiseError::Unevaluated {
3287            construct: "constant's value, which is not an integer".to_string(),
3288            at: at.to_string(),
3289        });
3290    };
3291    Ok(match primitive {
3292        java::Primitive::Boolean => java::Expr::Literal(Literal::Bool(value != 0)),
3293        // A narrowing assignment of a constant expression is legal Java,
3294        // but the cast is written anyway: it is what says the narrowing
3295        // was intended and what keeps the line legible.
3296        java::Primitive::Char => cast(
3297            *primitive,
3298            in_range(value, 0, i128::from(u16::MAX), "char", at)?,
3299        ),
3300        java::Primitive::Byte => cast(*primitive, in_range(value, -128, 127, "byte", at)?),
3301        java::Primitive::Short => cast(
3302            *primitive,
3303            in_range(
3304                value,
3305                i128::from(i16::MIN),
3306                i128::from(i16::MAX),
3307                "short",
3308                at,
3309            )?,
3310        ),
3311        java::Primitive::Int => java::Expr::Literal(Literal::int(in_range(
3312            value,
3313            i128::from(i32::MIN),
3314            i128::from(i32::MAX),
3315            "int",
3316            at,
3317        )?)),
3318        java::Primitive::Long => {
3319            let value = in_range(
3320                value,
3321                i128::from(i64::MIN),
3322                i128::from(i64::MAX),
3323                "long",
3324                at,
3325            )?;
3326            // The `L`: `4294967295` without it is an int literal Java
3327            // refuses, and a `long` field is exactly where an
3328            // `unsigned long` value lands.
3329            java::Expr::Literal(Literal::Int(format!("{value}L")))
3330        }
3331        java::Primitive::Float | java::Primitive::Double => {
3332            java::Expr::Literal(Literal::int(value))
3333        }
3334    })
3335}
3336
3337/// `(<primitive>) <value>`
3338fn cast(primitive: java::Primitive, value: i128) -> java::Expr {
3339    java::Expr::Cast {
3340        type_: Box::new(java::Type::Primitive(primitive)),
3341        value: Box::new(java::Expr::Literal(Literal::int(value))),
3342    }
3343}
3344
3345/// `value`, or the failure that says it does not fit.
3346///
3347/// # Errors
3348/// [`MaterialiseError::NoLanguageForm`], naming the value and the type
3349/// that will not hold it. Truncating instead would put a number nobody
3350/// wrote into the output.
3351fn in_range(
3352    value: i128,
3353    low: i128,
3354    high: i128,
3355    type_: &str,
3356    at: &str,
3357) -> Result<i128, MaterialiseError> {
3358    if value < low || value > high {
3359        return Err(MaterialiseError::NoLanguageForm {
3360            construct: format!("the value {value}, which a Java `{type_}` does not hold"),
3361            language: "Java",
3362            at: at.to_string(),
3363        });
3364    }
3365    Ok(value)
3366}
3367
3368/// A model node's name, or its identity when the node is anonymous.
3369///
3370/// An array type declared inline has no name of its own, and an error
3371/// that said only "the array length does not evaluate" would leave the
3372/// reader to find which array.
3373fn named(name: &str, node: NodeId) -> String {
3374    if name.is_empty() {
3375        format!("{node:?}")
3376    } else {
3377        name.to_string()
3378    }
3379}
3380
3381/// Whether the rules created a node of `type_name` for `node`.
3382///
3383/// The same two-hop walk the other two materialisers do: a correspondence
3384/// is an ordinary node with a link to the anchor and a link to what was
3385/// created.
3386fn ghost_created_for(applied: &Applied, node: NodeId, type_name: &str) -> Option<GhostId> {
3387    let anchor = applied
3388        .origin
3389        .iter()
3390        .find(|(_, model)| **model == node)
3391        .map(|(ghost, _)| *ghost)?;
3392    let typ = applied.graph.types.lookup(type_name)?;
3393    applied
3394        .graph
3395        .nodes_of_type(typ)
3396        .map(|created| created.id)
3397        .find(|created| {
3398            applied.graph.connected(&anchor, created)
3399                || applied
3400                    .graph
3401                    .parts(&anchor)
3402                    .any(|part| applied.graph.connected(&part.other, created))
3403        })
3404}
3405
3406#[cfg(test)]
3407mod tests {
3408    #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
3409    use super::*;
3410    use crate::rule;
3411    use crate::{ModelGraph, apply};
3412    use std::path::{Path as FsPath, PathBuf};
3413
3414    #[test]
3415    fn the_entry_class_is_found_from_the_map_class() {
3416        assert_eq!(
3417            entry_class_of_map_class("ZdMap_long_string").as_deref(),
3418            Some("ZdEntry_long_string")
3419        );
3420        assert_eq!(
3421            entry_class_of_map_class("ZdMap_b8_long_string").as_deref(),
3422            Some("ZdEntry_long_string")
3423        );
3424        assert_eq!(
3425            entry_class_of_map_class("ZdMap_bx_unsigned_short_string").as_deref(),
3426            Some("ZdEntry_unsigned_short_string")
3427        );
3428        assert_eq!(entry_class_of_map_class("Other_long_string"), None);
3429        for idl in [
3430            "@final struct P { map<long, string> m; };",
3431            "@final struct P { map<long, string, 8> m; };",
3432        ] {
3433            let ast = {
3434                let parsed = zerodds_idl_parse::parse(idl);
3435                let lowered = zerodds_idl_lower::lower(&parsed.syntax(), "t.idl");
3436                let mut ast = lowered.ast;
3437                ast.resolve_all_by_name().expect("resolve");
3438                ast
3439            };
3440            let map = ast
3441                .types()
3442                .find(|(_, node)| matches!(node.kind, TypeKind::Map { .. }))
3443                .map(|(id, _)| id)
3444                .expect("a map type");
3445            assert_eq!(
3446                entry_class_of_map_class(&map_class_name(&ast, map).expect("class")),
3447                Some(map_entry_name(&ast, map).expect("entry")),
3448                "{idl}"
3449            );
3450        }
3451    }
3452
3453    /// IDL in, one Java file out. The tests of this module state the
3454    /// properties of the IDL-to-Java mapping (public fields), so they ask
3455    /// for it by name; the default is Clause 9 (`tests/java_psm_mapping.rs`).
3456    fn render(idl: &str, path: &str) -> Result<String, String> {
3457        with_mapping(JavaMapping::Omg, || render_in_force(idl, path))
3458    }
3459
3460    fn render_in_force(idl: &str, path: &str) -> Result<String, String> {
3461        let parsed = zerodds_idl_parse::parse(idl);
3462        if parsed.has_errors() {
3463            return Err(format!("parse: {:?}", parsed.diagnostics()));
3464        }
3465        let lowered = zerodds_idl_lower::lower(&parsed.syntax(), path);
3466        if !lowered.diagnostics.is_empty() {
3467            return Err(format!("lower: {:?}", lowered.diagnostics));
3468        }
3469        let mut ast = lowered.ast;
3470        ast.resolve_all_by_name()
3471            .map_err(|error| format!("resolve: {error}"))?;
3472        let applied = apply(&ModelGraph::of(&ast), &rule::java::catalogue())
3473            .map_err(|error| format!("apply: {error}"))?;
3474        let types = java_items(&ast, &applied).map_err(|error| format!("materialise: {error}"))?;
3475        Ok(zerodds_lang_render::java::file(&java::File {
3476            doc: Doc::lines(&[
3477                "Generated by zerodds-lang-rules. Do not edit.",
3478                "",
3479                "The Java shape of one IDL file. The wire rules are not here — they",
3480                "are in runtime/java, which implements them from DDS-XTypes 1.3.",
3481            ]),
3482            package: None,
3483            imports: Vec::new(),
3484            types,
3485        }))
3486    }
3487
3488    /// The same, for a source with no file behind it.
3489    fn declarations(idl: &str) -> String {
3490        match render(idl, "test.idl") {
3491            Ok(text) => text,
3492            Err(why) => panic!("{why}"),
3493        }
3494    }
3495
3496    /// Every corpus case, by name.
3497    fn corpus() -> Vec<(String, PathBuf)> {
3498        let root = FsPath::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/interop-corpus");
3499        let mut out = Vec::new();
3500        for entry in std::fs::read_dir(&root).expect("the corpus directory") {
3501            let entry = entry.expect("a corpus entry");
3502            let idl = entry.path().join("type.idl");
3503            if idl.is_file() {
3504                out.push((entry.file_name().to_string_lossy().into_owned(), idl));
3505            }
3506        }
3507        out.sort();
3508        out
3509    }
3510
3511    /// The whole corpus, through every layer, with nothing skipped.
3512    ///
3513    /// A rendered file is not a compiled one. `ZD_JAVA_OUT` names a
3514    /// directory to write the sources into, and `javac` over that
3515    /// directory is the second half of the check — a file that does not
3516    /// compile is a materialiser defect, not a corpus defect.
3517    #[test]
3518    fn every_corpus_case_renders_or_names_its_failure() {
3519        let cases = corpus();
3520        assert!(
3521            cases.len() > 300,
3522            "the corpus is not where it was: {} cases",
3523            cases.len()
3524        );
3525        let out = std::env::var("ZD_JAVA_OUT").ok();
3526        let mut failures = Vec::new();
3527        for (name, path) in cases {
3528            let source = std::fs::read_to_string(&path).expect("a corpus source");
3529            match render(&source, &path.display().to_string()) {
3530                Ok(text) => {
3531                    if let Some(dir) = &out {
3532                        let case = FsPath::new(dir).join(&name);
3533                        std::fs::create_dir_all(&case).expect("the output directory");
3534                        std::fs::write(case.join(format!("{OUTER}.java")), text)
3535                            .expect("the output file");
3536                    }
3537                }
3538                Err(why) => failures.push(format!("{name}: {why}")),
3539            }
3540        }
3541        // `resolve-module-named-as-type` is
3542        // `module common { struct common { … }; }`, which JLS 8.1
3543        // forbids — a nested class may not share the simple name of an
3544        // enclosing one. It no longer reaches this adapter at all: the
3545        // front end refuses the file under IDL 4.2 §7.5.2, which is
3546        // where `crate::adapter::REFUSED_BY_THE_FRONT_END` states the
3547        // reason and where the check that it is still refused lives.
3548        let failures = crate::adapter::without_the_front_end_refusals(failures);
3549        assert!(failures.is_empty(), "{failures:#?}");
3550    }
3551
3552    #[test]
3553    fn every_idl_primitive_reaches_the_java_type_that_holds_it() {
3554        let generated = declarations(
3555            "@final struct P {
3556               boolean a; octet b; char c; wchar d;
3557               int8 e; uint8 f; short g; unsigned short h;
3558               long i; unsigned long j; long long k; unsigned long long l;
3559               float m; double n; long double o;
3560             };",
3561        );
3562        for expected in [
3563            "public boolean a;",
3564            "public byte b;",
3565            "public char c;",
3566            "public char d;",
3567            "public byte e;",
3568            "public int f;",
3569            "public short g;",
3570            "public int h;",
3571            "public int i;",
3572            "public long j;",
3573            "public long k;",
3574            "public long l;",
3575            "public float m;",
3576            "public double n;",
3577            "public org.zerodds.wire.Float128 o;",
3578        ] {
3579            assert!(generated.contains(expected), "{expected}\n{generated}");
3580        }
3581    }
3582
3583    #[test]
3584    fn a_sequence_and_an_array_are_both_java_arrays() {
3585        let generated = declarations(
3586            "@final struct P { sequence<long> s; long a[3]; long g[2][3]; sequence<string> t; };",
3587        );
3588        assert!(generated.contains("public int[] s;"), "{generated}");
3589        assert!(generated.contains("public int[] a;"), "{generated}");
3590        assert!(generated.contains("public int[][] g;"), "{generated}");
3591        assert!(
3592            generated.contains("public java.lang.String[] t;"),
3593            "{generated}"
3594        );
3595    }
3596
3597    #[test]
3598    fn a_map_is_a_java_util_map_over_a_generated_entry_class() {
3599        let generated = declarations("@final struct P { map<long, string> m; };");
3600        // The tag is the IDL kind's spelling and not the Java type's:
3601        // `long` is an `int` here, and the tag is the IDL kind's spelling.
3602        assert!(
3603            generated.contains("class ZdEntry_long_string {"),
3604            "{generated}"
3605        );
3606        assert!(generated.contains("public int key;"), "{generated}");
3607        assert!(
3608            generated.contains("public java.lang.String value;"),
3609            "{generated}"
3610        );
3611        // XTypes 1.3 §7.5.1.3.5: "an implementation of the standard
3612        // java.util.Map interface", with the boxed key and value types.
3613        assert!(
3614            generated.contains(
3615                "class ZdMap_long_string extends java.util.LinkedHashMap<java.lang.Integer, java.lang.String> {"
3616            ),
3617            "{generated}"
3618        );
3619        assert!(
3620            generated.contains("public ZdTypes.ZdMap_long_string m;"),
3621            "{generated}"
3622        );
3623        // The operations of Table 47 Java does not have.
3624        for operation in [
3625            "public int getMaxSize()",
3626            "public int setMaxSize(int maxSize)",
3627            "public int insert(java.lang.Integer key, java.lang.String element, boolean replace)",
3628            "public int copyFrom(ZdMap_long_string other, boolean autogrow)",
3629        ] {
3630            assert!(generated.contains(operation), "{operation}:\n{generated}");
3631        }
3632    }
3633
3634    #[test]
3635    fn a_map_keyed_by_a_structure_is_a_java_util_map_over_a_class_with_equals() {
3636        let generated = declarations("struct K { long a; }; @final struct P { map<K, long> m; };");
3637        assert!(
3638            generated.contains("class ZdMap_K_long extends"),
3639            "{generated}"
3640        );
3641        assert!(
3642            generated.contains("public boolean equals(Object other)")
3643                && generated.contains("public int hashCode()"),
3644            "{generated}"
3645        );
3646        assert!(!generated.contains("ZdEntry_K_long[] m;"), "{generated}");
3647    }
3648
3649    #[test]
3650    fn a_map_keyed_by_a_sequence_keeps_its_entries() {
3651        let generated = declarations("@final struct P { map<sequence<long>, long> m; };");
3652        assert!(generated.contains("[] m;"), "{generated}");
3653        assert!(!generated.contains("ZdMap_"), "{generated}");
3654    }
3655
3656    #[test]
3657    fn a_bounded_map_has_a_class_of_its_own_with_the_bound() {
3658        let generated =
3659            declarations("@final struct P { map<long, long, 3> a; map<long, long> b; };");
3660        assert!(
3661            generated.contains("class ZdMap_b3_long_long extends"),
3662            "{generated}"
3663        );
3664        assert!(
3665            generated.contains("class ZdMap_long_long extends"),
3666            "{generated}"
3667        );
3668        assert!(
3669            generated.contains("private static final int ZD_BOUND = 3;"),
3670            "{generated}"
3671        );
3672        // One entry class for both: they hold the same pairs.
3673        assert_eq!(generated.matches("class ZdEntry_long_long {").count(), 1);
3674    }
3675
3676    #[test]
3677    fn two_maps_of_one_shape_are_one_entry_class() {
3678        let generated = declarations("@final struct P { map<long, long> a; map<long, long> b; };");
3679        assert_eq!(generated.matches("class ZdEntry_long_long {").count(), 1);
3680    }
3681
3682    #[test]
3683    fn two_maps_of_one_java_shape_and_two_idl_shapes_are_two_entry_classes() {
3684        // `uint8` and `unsigned short` are both a Java `int` and one and
3685        // two bytes on the wire. One entry class for both would put one
3686        // of them out at the other's width.
3687        let generated =
3688            declarations("@final struct P { map<uint8, long> a; map<unsigned short, long> b; };");
3689        assert!(
3690            generated.contains("class ZdEntry_uint8_long {"),
3691            "{generated}"
3692        );
3693        assert!(
3694            generated.contains("class ZdEntry_unsigned_short_long {"),
3695            "{generated}"
3696        );
3697    }
3698
3699    #[test]
3700    fn an_optional_member_is_boxed_and_a_primitive_one_too() {
3701        let generated = declarations(
3702            "@mutable struct P {
3703               @optional long a; @optional string b; @optional long long c;
3704             };",
3705        );
3706        // An IDL `long` is 32 bits, so the wrapper is `Integer`.
3707        assert!(
3708            generated.contains("public java.lang.Integer a;"),
3709            "{generated}"
3710        );
3711        assert!(
3712            generated.contains("public java.lang.String b;"),
3713            "{generated}"
3714        );
3715        assert!(
3716            generated.contains("public java.lang.Long c;"),
3717            "{generated}"
3718        );
3719    }
3720
3721    #[test]
3722    fn a_user_annotation_is_an_interface_with_its_defaults_and_a_group() {
3723        // §7.5.1.2.6.1: `public @interface <TypeName>` with the members of
3724        // the IDL annotation, a default only where the IDL gives one, and
3725        // `public @interface <TypeName>Group { <TypeName>[] value(); }`.
3726        let generated = declarations(
3727            "@annotation Note { string text default \"none\"; long level; };\n\
3728             @final struct P { long a; };",
3729        );
3730        assert!(
3731            generated.contains("public @interface Note {"),
3732            "{generated}"
3733        );
3734        assert!(
3735            generated.contains("java.lang.String text() default \"none\";"),
3736            "{generated}"
3737        );
3738        assert!(
3739            generated.contains("int level();"),
3740            "no default: {generated}"
3741        );
3742        assert!(
3743            generated.contains("public @interface NoteGroup {"),
3744            "{generated}"
3745        );
3746        assert!(generated.contains("Note[] value();"), "{generated}");
3747    }
3748
3749    #[test]
3750    fn an_applied_annotation_sits_on_the_java_element_and_several_go_through_the_group() {
3751        let generated = declarations(
3752            "@annotation Note { string text default \"none\"; long level; };\n\
3753             @Note(level = 3) @final struct P { @Note(text = \"a\", level = 1) \
3754             @Note(level = 2) long a; long b; };",
3755        );
3756        assert!(
3757            generated.contains("@ZdTypes.Note(level = 3)"),
3758            "{generated}"
3759        );
3760        assert!(
3761            generated.contains(
3762                "@ZdTypes.NoteGroup({@ZdTypes.Note(text = \"a\", level = 1), \
3763                 @ZdTypes.Note(level = 2)})"
3764            ),
3765            "{generated}"
3766        );
3767    }
3768
3769    #[test]
3770    fn an_annotation_on_a_module_and_on_a_constant_reaches_the_java_element() {
3771        let generated = declarations(
3772            "@annotation Note { long level; };\n\
3773             @Note(level = 5) module M { @Note(level = 6) const long LIMIT = 3; \
3774             @final struct P { long a; }; };",
3775        );
3776        assert!(
3777            generated.contains("@ZdTypes.Note(level = 5)"),
3778            "{generated}"
3779        );
3780        assert!(
3781            generated.contains("@ZdTypes.Note(level = 6)"),
3782            "{generated}"
3783        );
3784    }
3785
3786    #[test]
3787    fn an_external_member_is_an_object_reference_and_a_primitive_one_is_boxed() {
3788        let generated = declarations(
3789            "@final struct P { @external short a; @external long b; @external string c; \
3790             @external sequence<long> d; @external boolean e; };",
3791        );
3792        assert!(
3793            generated.contains("public java.lang.Short a;"),
3794            "{generated}"
3795        );
3796        assert!(
3797            generated.contains("public java.lang.Integer b;"),
3798            "{generated}"
3799        );
3800        assert!(
3801            generated.contains("public java.lang.String c;"),
3802            "{generated}"
3803        );
3804        assert!(generated.contains("public int[] d;"), "{generated}");
3805        assert!(
3806            generated.contains("public java.lang.Boolean e;"),
3807            "{generated}"
3808        );
3809    }
3810
3811    #[test]
3812    fn a_union_is_a_class_with_a_discriminator_and_one_field_per_case() {
3813        // Two cases of one type: a field each, which is what a class can
3814        // hold and an overlay could not.
3815        let generated = declarations(
3816            "union Choice switch (long) {
3817               case 1: long whole;
3818               case 2: long other;
3819               default: double fraction;
3820             };
3821             @final struct P { Choice c; };",
3822        );
3823        assert!(generated.contains("public int _d;"), "{generated}");
3824        assert!(generated.contains("public int whole;"), "{generated}");
3825        assert!(generated.contains("public int other;"), "{generated}");
3826        assert!(generated.contains("public double fraction;"), "{generated}");
3827        assert!(
3828            generated.contains("public ZdTypes.Choice c;"),
3829            "{generated}"
3830        );
3831    }
3832
3833    #[test]
3834    fn an_enumeration_is_a_class_of_instances_and_a_member_of_it_is_one() {
3835        let generated = declarations(
3836            "enum Colour { RED, GREEN, BLUE };
3837             @final struct P { Colour c; };",
3838        );
3839        assert!(
3840            generated.contains("public static final int _RED = 0;"),
3841            "{generated}"
3842        );
3843        assert!(
3844            generated.contains("public static final Colour RED = new Colour(_RED);"),
3845            "{generated}"
3846        );
3847        assert!(
3848            generated.contains("public static final int _BLUE = 2;"),
3849            "{generated}"
3850        );
3851        assert!(generated.contains("public int value()"), "{generated}");
3852        assert!(
3853            generated.contains("public static Colour from_int(int value)"),
3854            "{generated}"
3855        );
3856        assert!(
3857            generated.contains("public ZdTypes.Colour c;"),
3858            "{generated}"
3859        );
3860    }
3861
3862    #[test]
3863    fn a_construct_with_no_java_data_form_is_named_and_not_left_out() {
3864        // The member is not dropped and no `Object` is substituted for
3865        // it: a class missing a field that reports success is the defect
3866        // this materialiser is arranged against.
3867        for (idl, construct) in [
3868            ("@final struct P { any a; long tail; };", "any"),
3869            ("@final struct P { Object o; long tail; };", "Object"),
3870        ] {
3871            let failure = render(idl, "test.idl").expect_err(construct);
3872            assert!(failure.contains(construct), "{failure}");
3873            assert!(failure.contains("Java"), "{failure}");
3874        }
3875    }
3876
3877    #[test]
3878    fn an_explicit_enumerator_value_is_written_out() {
3879        let generated = declarations("enum E { @value(-1) LOW, @value(70000) HIGH };");
3880        assert!(
3881            generated.contains("public static final int _LOW = -1;"),
3882            "{generated}"
3883        );
3884        assert!(
3885            generated.contains("public static final int _HIGH = 70000;"),
3886            "{generated}"
3887        );
3888    }
3889
3890    #[test]
3891    fn a_bitmask_takes_the_holder_its_bound_calls_for() {
3892        let narrow =
3893            declarations("@bit_bound(8) bitmask Small { @position(0) A, @position(7) B };");
3894        assert!(
3895            narrow.contains("public static final int B = 1 << 7;"),
3896            "{narrow}"
3897        );
3898        let wide = declarations("@bit_bound(64) bitmask Big { @position(0) A, @position(40) B };");
3899        assert!(
3900            wide.contains("public static final long B = 1L << 40;"),
3901            "{wide}"
3902        );
3903    }
3904
3905    #[test]
3906    fn a_bitset_is_a_class_of_one_field_per_bitfield() {
3907        let generated = declarations(
3908            "bitset Packed { bitfield<3> low; bitfield<5> high; };
3909             @final struct P { Packed value; };",
3910        );
3911        assert!(generated.contains("public int low;"), "{generated}");
3912        assert!(generated.contains("public int high;"), "{generated}");
3913        assert!(
3914            generated.contains("public ZdTypes.Packed value;"),
3915            "{generated}"
3916        );
3917    }
3918
3919    #[test]
3920    fn a_fixed_is_a_bigdecimal() {
3921        let generated = declarations(
3922            "typedef fixed<9,3> Money;
3923             @final struct P { Money amount; };",
3924        );
3925        assert!(
3926            generated.contains("public java.math.BigDecimal amount;"),
3927            "{generated}"
3928        );
3929    }
3930
3931    #[test]
3932    fn a_typedef_declares_nothing_and_says_so() {
3933        let generated = declarations(
3934            "typedef sequence<long> Row;
3935             @final struct P { Row r; };",
3936        );
3937        assert!(generated.contains("public int[] r;"), "{generated}");
3938        assert!(generated.contains("typedef int[] Row;"), "{generated}");
3939    }
3940
3941    #[test]
3942    fn a_module_is_a_nested_class_and_two_modules_do_not_collide() {
3943        let generated = declarations(
3944            "module a { @final struct P { long x; }; };
3945             module b { @final struct P { double y; }; };
3946             @final struct Q { a::P one; b::P two; };",
3947        );
3948        assert!(generated.contains("class a {"), "{generated}");
3949        assert!(generated.contains("class b {"), "{generated}");
3950        assert!(generated.contains("public ZdTypes.a.P one;"), "{generated}");
3951        assert!(generated.contains("public ZdTypes.b.P two;"), "{generated}");
3952    }
3953
3954    #[test]
3955    fn a_module_reopened_beside_a_nested_one_is_one_class() {
3956        // `module common { struct Robot; module util { … } }` — the
3957        // arrangement that produced two classes of one name when the
3958        // grouping was by module id rather than by path.
3959        let generated = declarations(
3960            "module common {
3961               module util { @final struct Location { double x; }; };
3962               @final struct Robot { ::common::util::Location where_; };
3963             };",
3964        );
3965        assert_eq!(
3966            generated.matches("class common {").count(),
3967            1,
3968            "{generated}"
3969        );
3970        assert!(
3971            generated.contains("public ZdTypes.common.util.Location where_;"),
3972            "{generated}"
3973        );
3974    }
3975
3976    #[test]
3977    fn an_identifier_that_is_a_java_keyword_is_escaped() {
3978        let generated = declarations(
3979            "module package {
3980               enum class { template, virtual, friend };
3981               @final struct type { long new; };
3982             };",
3983        );
3984        assert!(generated.contains("class package_ {"), "{generated}");
3985        assert!(generated.contains("class class_ {"), "{generated}");
3986        assert!(generated.contains("class type {"), "{generated}");
3987        assert!(generated.contains("public int new_;"), "{generated}");
3988        // Only a Java keyword moves: these three are C++ keywords and
3989        // keep their spelling, as the JacORB column records.
3990        assert!(
3991            generated.contains("public static final int _template = 0;"),
3992            "{generated}"
3993        );
3994    }
3995
3996    #[test]
3997    fn a_name_that_would_capture_the_qualified_references_is_refused() {
3998        let failure = render("module java { @final struct P { long x; }; };", "test.idl")
3999            .expect_err("a module named java is refused");
4000        assert!(failure.contains("java"), "{failure}");
4001    }
4002
4003    #[test]
4004    fn a_declaration_named_after_the_outer_class_is_refused() {
4005        let failure = render("@final struct ZdTypes { long x; };", "test.idl")
4006            .expect_err("a type named after the outer class is refused");
4007        assert!(failure.contains("ZdTypes"), "{failure}");
4008    }
4009
4010    #[test]
4011    fn an_unevaluated_array_length_is_named_and_not_substituted() {
4012        // The C column fails on the same input; the two columns' failure
4013        // sets stay comparable.
4014        let failure = render("@final struct P { long a[UNKNOWN]; };", "test.idl")
4015            .expect_err("an array whose length does not evaluate is refused");
4016        assert!(!failure.is_empty(), "{failure}");
4017    }
4018}