Skip to main content

zerodds_idl_java/
emitter.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! AST walker that emits Java 17 source files.
4//!
5//! Block A: header layout (`package`, `import`, class modifiers).
6//! Block B: primitive mapping (delegates to [`crate::type_map`]).
7//! Block C: struct/enum/union/typedef/sequence/array/inheritance.
8//! Block D: exception → `class X extends RuntimeException`.
9//!
10//! Java requires one `.java` file per top-level public class. The
11//! emitter collects exactly one [`JavaFile`] structure per top-level
12//! type during the AST walk.
13
14use std::collections::{BTreeSet, HashMap};
15use std::fmt::Write;
16// zerodds-lint: BTreeSet is used in the emitter for ImportSet + cycle detection.
17
18use zerodds_idl::ast::{
19    Annotation, AnnotationParams, CaseLabel, ConstExpr, ConstrTypeDecl, Declarator, Definition,
20    EnumDef, ExceptDecl, IntegerType, InterfaceDcl, InterfaceDef, Literal, LiteralKind, Member,
21    ScopedName, Specification, StructDcl, StructDef, SwitchTypeSpec, TypeDecl, TypeSpec,
22    TypedefDecl, UnionDcl, UnionDef,
23};
24
25use zerodds_idl::semantics::annotations::PlacementKind;
26
27use crate::JavaGenOptions;
28use crate::annotations::{
29    enum_value_override, has_nested, lower_or_empty, member_annotation_lines, type_annotation_lines,
30};
31use crate::bitset::{emit_bitmask_file, emit_bitset_file};
32use crate::error::JavaGenError;
33use crate::keywords::sanitize_identifier;
34use crate::type_map::{
35    floating_to_java, floating_to_java_boxed, integer_to_java, integer_to_java_boxed, is_unsigned,
36    primitive_to_java, primitive_to_java_boxed,
37};
38use crate::verbatim::emit_verbatim_at;
39
40/// A single generated Java source file.
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct JavaFile {
43    /// Java package path with dot separators (e.g. `org.example.types`).
44    pub package_path: String,
45    /// Class name = file name without the `.java` suffix.
46    pub class_name: String,
47    /// Complete source file (including `package`, imports, class body).
48    pub source: String,
49}
50
51impl JavaFile {
52    /// Returns the relative path for the file (e.g. `org/example/Foo.java`).
53    #[must_use]
54    pub fn relative_path(&self) -> String {
55        let dir = self.package_path.replace('.', "/");
56        if dir.is_empty() {
57            format!("{}.java", self.class_name)
58        } else {
59            format!("{dir}/{}.java", self.class_name)
60        }
61    }
62}
63
64/// Main entry: walks the IDL AST and emits a list of Java files.
65pub(crate) fn emit_files(
66    spec: &Specification,
67    opts: &JavaGenOptions,
68) -> Result<Vec<JavaFile>, JavaGenError> {
69    detect_inheritance_cycles(spec)?;
70
71    // Pre-pass: index every Struct-Name → its (transitive) base-chain
72    // for the Multi-Inheritance Interface-Pattern (C5.4-b §3).
73    let parent_of = collect_base_chain_index(spec);
74
75    let mut files: Vec<JavaFile> = Vec::new();
76    let pkg = sanitize_package(&opts.root_package);
77    let ctx = EmitCtx { parent_of };
78    walk_definitions(&spec.definitions, &pkg, opts, &mut files, &ctx)?;
79    Ok(files)
80}
81
82/// Emitter context held read-only during the AST walk.
83/// Contains the multi-inheritance index plus any future global
84/// lookup tables (e.g. type-name → topic-eligibility).
85#[derive(Debug, Default)]
86pub(crate) struct EmitCtx {
87    /// Mapping `struct name → direct base name` (short form, without
88    /// module prefix). We use the last `.`-separated token
89    /// (see [`scoped_to_short`]).
90    pub parent_of: std::collections::HashMap<String, String>,
91}
92
93fn sanitize_package(p: &str) -> String {
94    p.trim_matches('.').to_string()
95}
96
97/// zerodds-lint: recursion-depth 64 (Parser/AST-Walk; bounded by IDL nesting)
98fn walk_definitions(
99    defs: &[Definition],
100    pkg: &str,
101    opts: &JavaGenOptions,
102    files: &mut Vec<JavaFile>,
103    ctx: &EmitCtx,
104) -> Result<(), JavaGenError> {
105    for d in defs {
106        match d {
107            Definition::Module(m) => {
108                let name = sanitize_identifier(&m.name.text)?.to_lowercase();
109                let sub_pkg = if pkg.is_empty() {
110                    name
111                } else {
112                    format!("{pkg}.{name}")
113                };
114                walk_definitions(&m.definitions, &sub_pkg, opts, files, ctx)?;
115            }
116            Definition::Type(td) => emit_type_decl_top(td, pkg, opts, files, ctx)?,
117            Definition::Const(c) => {
118                let file = emit_const_holder(c, pkg, opts)?;
119                files.push(file);
120            }
121            Definition::Except(e) => {
122                let file = emit_exception_file(e, pkg, opts)?;
123                files.push(file);
124            }
125            Definition::Interface(InterfaceDcl::Def(iface)) => {
126                if is_service_interface(iface) {
127                    emit_service_interface_files(iface, pkg, opts, files)?;
128                } else {
129                    // Spec idl4-java §7.4: IDL interface -> Java public interface.
130                    files.push(emit_non_service_interface_file(iface, pkg, opts)?);
131                }
132            }
133            Definition::Interface(InterfaceDcl::Forward(_)) => {
134                // §7.4.2: forward decl has no Java mapping.
135            }
136            Definition::ValueDef(v) => {
137                let value_files = emit_value_type_files(v, pkg, opts)?;
138                files.extend(value_files);
139            }
140            Definition::ValueBox(_) | Definition::ValueForward(_) => {
141                // ValueBox + ValueForward are no-ops in the foundation.
142            }
143            Definition::TypeId(_)
144            | Definition::TypePrefix(_)
145            | Definition::Import(_)
146            | Definition::Component(_)
147            | Definition::Home(_)
148            | Definition::Event(_)
149            | Definition::Porttype(_)
150            | Definition::Connector(_)
151            | Definition::TemplateModule(_)
152            | Definition::TemplateModuleInst(_) => {
153                return Err(JavaGenError::UnsupportedConstruct {
154                    construct: "corba/ccm/template construct".into(),
155                    context: None,
156                });
157            }
158            Definition::Annotation(_) => {
159                // §7.4.15: user-defined annotation defs are emitted at
160                // the point of application on annotated members,
161                // not as a standalone top-level Java construct.
162            }
163            Definition::VendorExtension(v) => {
164                return Err(JavaGenError::UnsupportedConstruct {
165                    construct: format!("vendor-extension:{}", v.production_name),
166                    context: None,
167                });
168            }
169        }
170    }
171    Ok(())
172}
173
174fn emit_type_decl_top(
175    td: &TypeDecl,
176    pkg: &str,
177    opts: &JavaGenOptions,
178    files: &mut Vec<JavaFile>,
179    ctx: &EmitCtx,
180) -> Result<(), JavaGenError> {
181    match td {
182        TypeDecl::Constr(c) => match c {
183            ConstrTypeDecl::Struct(StructDcl::Def(s)) => {
184                files.push(emit_struct_file(s, pkg, opts, ctx)?);
185                // Multi-inheritance pattern: emit a companion interface
186                // for every struct that is itself a base of another
187                // struct — so a sub-sub-class can include the respective
188                // transitive ancestor via `implements <Anc>Interface`.
189                if ctx.parent_of.values().any(|p| p == &s.name.text) {
190                    files.push(emit_struct_companion_interface(s, pkg, opts)?);
191                }
192                Ok(())
193            }
194            ConstrTypeDecl::Struct(StructDcl::Forward(_)) => {
195                // Forward decls are implicit in Java (the class is
196                // produced separately anyway) — no file needed.
197                Ok(())
198            }
199            ConstrTypeDecl::Union(UnionDcl::Def(u)) => {
200                files.extend(emit_union_files(u, pkg, opts)?);
201                Ok(())
202            }
203            ConstrTypeDecl::Union(UnionDcl::Forward(_)) => Ok(()),
204            ConstrTypeDecl::Enum(e) => {
205                files.push(emit_enum_file(e, pkg, opts)?);
206                Ok(())
207            }
208            ConstrTypeDecl::Bitset(b) => {
209                files.push(emit_bitset_file(b, pkg, opts)?);
210                Ok(())
211            }
212            ConstrTypeDecl::Bitmask(b) => {
213                files.push(emit_bitmask_file(b, pkg, opts)?);
214                Ok(())
215            }
216        },
217        TypeDecl::Typedef(t) => {
218            files.extend(emit_typedef_files(t, pkg, opts)?);
219            Ok(())
220        }
221        // `native X;` — opaque, platform-specific type without an XCDR2
222        // wire representation; not emitted in the DataType codegen.
223        TypeDecl::Native(_) => Ok(()),
224    }
225}
226
227// ---------------------------------------------------------------------------
228// Per-type emitter (one JavaFile each)
229// ---------------------------------------------------------------------------
230
231fn emit_struct_file(
232    s: &StructDef,
233    pkg: &str,
234    opts: &JavaGenOptions,
235    ctx: &EmitCtx,
236) -> Result<JavaFile, JavaGenError> {
237    let class = sanitize_identifier(&s.name.text)?;
238    let mut imports = ImportSet::default();
239    let ind = indent_unit(opts);
240
241    // Pre-Walk: imports sammeln.
242    for m in &s.members {
243        collect_member_imports(m, &mut imports);
244    }
245
246    let mut body = String::new();
247
248    // §7.2.2.4.8 — `@verbatim(placement=BEGIN_FILE)` right at the start
249    // of the body (sits after package + imports in the compilation-unit wrap).
250    emit_verbatim_at(&mut body, "", &s.annotations, PlacementKind::BeginFile)?;
251
252    // §7.2.2.4.8 — `@verbatim(placement=BEFORE_DECLARATION)`.
253    emit_verbatim_at(
254        &mut body,
255        "",
256        &s.annotations,
257        PlacementKind::BeforeDeclaration,
258    )?;
259
260    // Type-level Annotations (`@Nested`, `@Extensibility(...)`).
261    for line in type_annotation_lines(&s.annotations) {
262        writeln!(body, "{line}").map_err(fmt_err)?;
263    }
264
265    let extends = if let Some(base) = &s.base {
266        let base_str = scoped_to_java(base);
267        format!(" extends {base_str}")
268    } else {
269        String::new()
270    };
271
272    // Multi-inheritance pattern: all transitive ancestors *beyond* the
273    // direct base are carried as `implements <X>Interface`. For a simple
274    // hierarchy (single base without a grandparent) this list stays empty.
275    let mut implements: Vec<String> = transitive_ancestors_beyond_base(&s.name.text, ctx)
276        .into_iter()
277        .map(|anc| format!("{anc}Interface"))
278        .collect();
279
280    // TopicType marker: every top-level struct without a `@nested`
281    // marker **and without a base** implements `org.omg.dds.topic.TopicType<Self>`.
282    //
283    // Sub-structs (`struct Child : Base`) inherit the marker from the
284    // parent via the regular `extends` chain — Java forbids
285    // re-implementing it with its own generic param (`TopicType<Child>`
286    // vs. `TopicType<Base>`). This is spec-conformant: in the DDS Java PSM,
287    // `TopicType<T>` is a marker interface whose generic param sits only
288    // at the root type of the inheritance chain. By the inheritance rule a
289    // sub-struct is still `instanceof TopicType<Base>` and thus
290    // registerable as a topic type.
291    //
292    // Findings anchor: TS-3 finding 4 (`internal/test-harness/plan.md`).
293    let lowered_type = lower_or_empty(&s.annotations);
294    if !has_nested(&lowered_type) && s.base.is_none() {
295        implements.push(format!("org.omg.dds.topic.TopicType<{class}>"));
296    }
297    let implements_clause = if implements.is_empty() {
298        String::new()
299    } else {
300        format!(" implements {}", implements.join(", "))
301    };
302
303    writeln!(body, "public class {class}{extends}{implements_clause} {{").map_err(fmt_err)?;
304
305    // §7.2.2.4.8 — `@verbatim(placement=BEGIN_DECLARATION)` as the first
306    // line inside the class body.
307    emit_verbatim_at(
308        &mut body,
309        &ind,
310        &s.annotations,
311        PlacementKind::BeginDeclaration,
312    )?;
313
314    // Felder.
315    for m in &s.members {
316        emit_member_field(&mut body, m, &ind)?;
317    }
318    writeln!(body).map_err(fmt_err)?;
319
320    // Default constructor.
321    writeln!(body, "{ind}public {class}() {{}}").map_err(fmt_err)?;
322    writeln!(body).map_err(fmt_err)?;
323
324    // Bean-Style Getter / Setter.
325    for m in &s.members {
326        emit_member_accessors(&mut body, m, &ind)?;
327    }
328
329    // §7.2.2.4.8 — `@verbatim(placement=END_DECLARATION)` as the last
330    // line before the closing `}`.
331    emit_verbatim_at(
332        &mut body,
333        &ind,
334        &s.annotations,
335        PlacementKind::EndDeclaration,
336    )?;
337
338    writeln!(body, "}}").map_err(fmt_err)?;
339
340    // §7.2.2.4.8 — `@verbatim(placement=AFTER_DECLARATION/END_FILE)`.
341    emit_verbatim_at(
342        &mut body,
343        "",
344        &s.annotations,
345        PlacementKind::AfterDeclaration,
346    )?;
347    emit_verbatim_at(&mut body, "", &s.annotations, PlacementKind::EndFile)?;
348
349    let source = wrap_compilation_unit(pkg, &imports, &body);
350    Ok(JavaFile {
351        package_path: pkg.to_string(),
352        class_name: class,
353        source,
354    })
355}
356
357fn emit_enum_file(e: &EnumDef, pkg: &str, opts: &JavaGenOptions) -> Result<JavaFile, JavaGenError> {
358    let class = sanitize_identifier(&e.name.text)?;
359    let ind = indent_unit(opts);
360    let mut body = String::new();
361
362    emit_verbatim_at(&mut body, "", &e.annotations, PlacementKind::BeginFile)?;
363    emit_verbatim_at(
364        &mut body,
365        "",
366        &e.annotations,
367        PlacementKind::BeforeDeclaration,
368    )?;
369
370    // Type-level annotations (`@Nested`, `@Extensibility(...)`).
371    for line in type_annotation_lines(&e.annotations) {
372        writeln!(body, "{line}").map_err(fmt_err)?;
373    }
374
375    writeln!(body, "public enum {class} {{").map_err(fmt_err)?;
376    emit_verbatim_at(
377        &mut body,
378        &ind,
379        &e.annotations,
380        PlacementKind::BeginDeclaration,
381    )?;
382
383    let count = e.enumerators.len();
384    let mut next_implicit: i64 = 0;
385    for (idx, en) in e.enumerators.iter().enumerate() {
386        let name = sanitize_identifier(&en.name.text)?;
387        let sep = if idx + 1 == count { ';' } else { ',' };
388        // Explicit `@value(N)` overrides the auto-assigned ordinal.
389        // Spec idl4-java-1.0 §7.2 — custom values instead of auto ordinals.
390        let value_lit = match enum_value_override(&en.annotations) {
391            Some(raw) => match raw.parse::<i64>() {
392                Ok(n) => {
393                    next_implicit = n + 1;
394                    n.to_string()
395                }
396                Err(_) => raw,
397            },
398            None => {
399                let n = next_implicit;
400                next_implicit += 1;
401                n.to_string()
402            }
403        };
404        writeln!(body, "{ind}{name}({value_lit}){sep}").map_err(fmt_err)?;
405    }
406    writeln!(body).map_err(fmt_err)?;
407    writeln!(body, "{ind}private final int value;").map_err(fmt_err)?;
408    writeln!(body, "{ind}{class}(int value) {{ this.value = value; }}").map_err(fmt_err)?;
409    writeln!(body, "{ind}public int value() {{ return value; }}").map_err(fmt_err)?;
410    emit_verbatim_at(
411        &mut body,
412        &ind,
413        &e.annotations,
414        PlacementKind::EndDeclaration,
415    )?;
416    writeln!(body, "}}").map_err(fmt_err)?;
417    emit_verbatim_at(
418        &mut body,
419        "",
420        &e.annotations,
421        PlacementKind::AfterDeclaration,
422    )?;
423    emit_verbatim_at(&mut body, "", &e.annotations, PlacementKind::EndFile)?;
424
425    let source = wrap_compilation_unit(pkg, &ImportSet::default(), &body);
426    Ok(JavaFile {
427        package_path: pkg.to_string(),
428        class_name: class,
429        source,
430    })
431}
432
433/// Union → one sealed interface + one Java file per case record.
434/// We return *one* file with the sealed interface + nested case records
435/// (Java allows nested permits in one file). This keeps the file count
436/// deterministic.
437fn emit_union_files(
438    u: &UnionDef,
439    pkg: &str,
440    opts: &JavaGenOptions,
441) -> Result<Vec<JavaFile>, JavaGenError> {
442    let class = sanitize_identifier(&u.name.text)?;
443    let ind = indent_unit(opts);
444    let imports = ImportSet::default();
445
446    let _disc_ty = switch_type_to_java(&u.switch_type)?;
447
448    // Permit list of the case records (unique per member name).
449    let mut permits: Vec<String> = Vec::new();
450    let mut case_records: Vec<(String, String, String)> = Vec::new(); // (record-name, field-ty, field-name)
451    for c in &u.cases {
452        let cpp_ty = type_for_declarator(&c.element.type_spec, &c.element.declarator)?;
453        let field_name = sanitize_identifier(&c.element.declarator.name().text)?;
454        // Record name: CapitalCase from the field name.
455        let record_name = capitalize(&field_name);
456        if !permits.iter().any(|p| p == &record_name) {
457            permits.push(record_name.clone());
458            case_records.push((record_name, cpp_ty, field_name));
459        }
460    }
461    // Java requires qualified names in the `permits` clause for nested
462    // records inside the sealed interface — `permits A, B, C` fails with
463    // `cannot find symbol`; `permits Foo.A, Foo.B, Foo.C` is the correct
464    // form.
465    //
466    // Findings anchor: TS-3 finding 5 (`internal/test-harness/plan.md`).
467    let permits_clause = if permits.is_empty() {
468        String::new()
469    } else {
470        let qualified: Vec<String> = permits.iter().map(|p| format!("{class}.{p}")).collect();
471        format!(" permits {}", qualified.join(", "))
472    };
473
474    let mut body = String::new();
475    emit_verbatim_at(&mut body, "", &u.annotations, PlacementKind::BeginFile)?;
476    emit_verbatim_at(
477        &mut body,
478        "",
479        &u.annotations,
480        PlacementKind::BeforeDeclaration,
481    )?;
482    if opts.java8_compat {
483        // Java-8 compat: `abstract class` instead of `sealed interface`
484        // (Java 17), without `permits`. Pseudo-sealing via a private
485        // constructor — only the nested `static final` subclasses can extend.
486        writeln!(body, "public abstract class {class} {{").map_err(fmt_err)?;
487    } else {
488        writeln!(body, "public sealed interface {class}{permits_clause} {{").map_err(fmt_err)?;
489    }
490    emit_verbatim_at(
491        &mut body,
492        &ind,
493        &u.annotations,
494        PlacementKind::BeginDeclaration,
495    )?;
496    if opts.java8_compat {
497        writeln!(body, "{ind}private {class}() {{}}").map_err(fmt_err)?;
498        writeln!(body).map_err(fmt_err)?;
499    }
500
501    // Default marker for the default branch (a comment; branch labels
502    // are emitted as a comment, not as a Java construct).
503    let mut has_default = false;
504    for c in &u.cases {
505        for label in &c.labels {
506            match label {
507                CaseLabel::Default => {
508                    has_default = true;
509                    writeln!(
510                        body,
511                        "{ind}// case default -> {}",
512                        c.element.declarator.name().text
513                    )
514                    .map_err(fmt_err)?;
515                }
516                CaseLabel::Value(expr) => {
517                    let val = const_expr_to_java(expr);
518                    writeln!(
519                        body,
520                        "{ind}// case {val} -> {}",
521                        c.element.declarator.name().text
522                    )
523                    .map_err(fmt_err)?;
524                }
525            }
526        }
527    }
528    if !has_default {
529        writeln!(body, "{ind}// no explicit 'default:' branch").map_err(fmt_err)?;
530    }
531    writeln!(body).map_err(fmt_err)?;
532
533    // Nested case-Typen.
534    for (record_name, field_ty, field_name) in &case_records {
535        if opts.java8_compat {
536            // Java-8 equivalent of a case record: a `static final` subclass
537            // with a final field + constructor + same-named accessor.
538            writeln!(
539                body,
540                "{ind}public static final class {record_name} extends {class} {{",
541            )
542            .map_err(fmt_err)?;
543            writeln!(body, "{ind}{ind}private final {field_ty} {field_name};").map_err(fmt_err)?;
544            writeln!(
545                body,
546                "{ind}{ind}public {record_name}({field_ty} {field_name}) {{ this.{field_name} = {field_name}; }}",
547            )
548            .map_err(fmt_err)?;
549            writeln!(
550                body,
551                "{ind}{ind}public {field_ty} {field_name}() {{ return {field_name}; }}",
552            )
553            .map_err(fmt_err)?;
554            writeln!(body, "{ind}}}").map_err(fmt_err)?;
555        } else {
556            writeln!(
557                body,
558                "{ind}record {record_name}({field_ty} {field_name}) implements {class} {{}}",
559            )
560            .map_err(fmt_err)?;
561        }
562    }
563    emit_verbatim_at(
564        &mut body,
565        &ind,
566        &u.annotations,
567        PlacementKind::EndDeclaration,
568    )?;
569    writeln!(body, "}}").map_err(fmt_err)?;
570    emit_verbatim_at(
571        &mut body,
572        "",
573        &u.annotations,
574        PlacementKind::AfterDeclaration,
575    )?;
576    emit_verbatim_at(&mut body, "", &u.annotations, PlacementKind::EndFile)?;
577
578    let source = wrap_compilation_unit(pkg, &imports, &body);
579    Ok(vec![JavaFile {
580        package_path: pkg.to_string(),
581        class_name: class,
582        source,
583    }])
584}
585
586fn emit_typedef_files(
587    t: &TypedefDecl,
588    pkg: &str,
589    _opts: &JavaGenOptions,
590) -> Result<Vec<JavaFile>, JavaGenError> {
591    // Java has no `using`/`typedef` — we emit a wrapper class per alias
592    // (1 wrapper field, named `value`).
593    let mut out = Vec::new();
594    for decl in &t.declarators {
595        let alias = sanitize_identifier(&decl.name().text)?;
596        let target = type_for_declarator(&t.type_spec, decl)?;
597        let imports = ImportSet::default();
598
599        let mut body = String::new();
600        writeln!(body, "public final class {alias} {{").map_err(fmt_err)?;
601        writeln!(body, "    private {target} value;").map_err(fmt_err)?;
602        writeln!(body).map_err(fmt_err)?;
603        writeln!(body, "    public {alias}() {{}}").map_err(fmt_err)?;
604        writeln!(
605            body,
606            "    public {alias}({target} value) {{ this.value = value; }}",
607        )
608        .map_err(fmt_err)?;
609        writeln!(body).map_err(fmt_err)?;
610        writeln!(body, "    public {target} value() {{ return value; }}").map_err(fmt_err)?;
611        writeln!(
612            body,
613            "    public void value({target} value) {{ this.value = value; }}",
614        )
615        .map_err(fmt_err)?;
616        writeln!(body, "}}").map_err(fmt_err)?;
617
618        let source = wrap_compilation_unit(pkg, &imports, &body);
619        out.push(JavaFile {
620            package_path: pkg.to_string(),
621            class_name: alias,
622            source,
623        });
624    }
625    Ok(out)
626}
627
628fn emit_exception_file(
629    e: &ExceptDecl,
630    pkg: &str,
631    opts: &JavaGenOptions,
632) -> Result<JavaFile, JavaGenError> {
633    let class = sanitize_identifier(&e.name.text)?;
634    let ind = indent_unit(opts);
635    let mut imports = ImportSet::default();
636    for m in &e.members {
637        collect_member_imports(m, &mut imports);
638    }
639
640    let mut body = String::new();
641    writeln!(body, "public class {class} extends RuntimeException {{").map_err(fmt_err)?;
642    for m in &e.members {
643        emit_member_field(&mut body, m, &ind)?;
644    }
645    writeln!(body).map_err(fmt_err)?;
646    writeln!(body, "{ind}public {class}() {{ super(); }}").map_err(fmt_err)?;
647    writeln!(
648        body,
649        "{ind}public {class}(String message) {{ super(message); }}",
650    )
651    .map_err(fmt_err)?;
652    writeln!(body).map_err(fmt_err)?;
653    for m in &e.members {
654        emit_member_accessors(&mut body, m, &ind)?;
655    }
656    writeln!(body, "}}").map_err(fmt_err)?;
657
658    let source = wrap_compilation_unit(pkg, &imports, &body);
659    Ok(JavaFile {
660        package_path: pkg.to_string(),
661        class_name: class,
662        source,
663    })
664}
665
666fn emit_const_holder(
667    c: &zerodds_idl::ast::ConstDecl,
668    pkg: &str,
669    _opts: &JavaGenOptions,
670) -> Result<JavaFile, JavaGenError> {
671    // IDL `const` → public static final field in a holder class
672    // namens `<NAME>Constant`.
673    let name = sanitize_identifier(&c.name.text)?;
674    let class = format!("{name}Constant");
675    let java_ty = const_type_to_java(&c.type_)?;
676    let val = const_expr_to_java(&c.value);
677    let mut body = String::new();
678    writeln!(body, "public final class {class} {{").map_err(fmt_err)?;
679    writeln!(body, "    public static final {java_ty} {name} = {val};").map_err(fmt_err)?;
680    writeln!(body, "    private {class}() {{}}").map_err(fmt_err)?;
681    writeln!(body, "}}").map_err(fmt_err)?;
682    let source = wrap_compilation_unit(pkg, &ImportSet::default(), &body);
683    Ok(JavaFile {
684        package_path: pkg.to_string(),
685        class_name: class,
686        source,
687    })
688}
689
690// ---------------------------------------------------------------------------
691// Member-Helpers
692// ---------------------------------------------------------------------------
693
694fn emit_member_field(out: &mut String, m: &Member, ind: &str) -> Result<(), JavaGenError> {
695    let optional = has_optional_annotation(&m.annotations);
696    let ann_lines = member_annotation_lines(&m.annotations);
697    for decl in &m.declarators {
698        let java_ty = type_for_declarator(&m.type_spec, decl)?;
699        let name = sanitize_identifier(&decl.name().text)?;
700        let final_ty = if optional {
701            format!("java.util.Optional<{}>", boxed_for_optional(&m.type_spec))
702        } else {
703            java_ty
704        };
705        for ann in &ann_lines {
706            writeln!(out, "{ind}{ann}").map_err(fmt_err)?;
707        }
708        // Doc comment for the unsigned workaround.
709        if let TypeSpec::Primitive(zerodds_idl::ast::PrimitiveType::Integer(i)) = &m.type_spec {
710            if is_unsigned(*i) {
711                writeln!(
712                    out,
713                    "{ind}/** unsigned IDL value (Java unsigned-workaround) */"
714                )
715                .map_err(fmt_err)?;
716            }
717        }
718        writeln!(out, "{ind}private {final_ty} {name};").map_err(fmt_err)?;
719    }
720    Ok(())
721}
722
723fn emit_member_accessors(out: &mut String, m: &Member, ind: &str) -> Result<(), JavaGenError> {
724    let optional = has_optional_annotation(&m.annotations);
725    for decl in &m.declarators {
726        let java_ty = type_for_declarator(&m.type_spec, decl)?;
727        let name = sanitize_identifier(&decl.name().text)?;
728        let cap = capitalize(&name);
729        let final_ty = if optional {
730            format!("java.util.Optional<{}>", boxed_for_optional(&m.type_spec))
731        } else {
732            java_ty.clone()
733        };
734        writeln!(
735            out,
736            "{ind}public {final_ty} get{cap}() {{ return {name}; }}"
737        )
738        .map_err(fmt_err)?;
739        writeln!(
740            out,
741            "{ind}public void set{cap}({final_ty} {name}) {{ this.{name} = {name}; }}",
742        )
743        .map_err(fmt_err)?;
744    }
745    Ok(())
746}
747
748fn boxed_for_optional(ts: &TypeSpec) -> String {
749    match ts {
750        TypeSpec::Primitive(p) => primitive_to_java_boxed(*p).to_string(),
751        TypeSpec::Scoped(s) => scoped_to_java(s),
752        TypeSpec::String(_) => "String".into(),
753        // Aggregates (sequence, map, …) reuse the canonical declared-type
754        // mapping so the `Optional<…>` slot is fully generic. Previously
755        // `map<…>` and nested aggregates fell through to a raw `Object`, which
756        // both lost the static type and produced an `Optional<Object>` field
757        // the codec then could not assign into.
758        TypeSpec::Sequence(_) | TypeSpec::Map(_) => {
759            typespec_to_java(ts).unwrap_or_else(|_| "Object".into())
760        }
761        _ => "Object".into(),
762    }
763}
764
765// ---------------------------------------------------------------------------
766// TypeSpec / Declarator
767// ---------------------------------------------------------------------------
768
769/// Returns the Java type expression for a member (TypeSpec + Declarator).
770pub(crate) fn type_for_declarator(
771    ts: &TypeSpec,
772    decl: &Declarator,
773) -> Result<String, JavaGenError> {
774    let base = typespec_to_java(ts)?;
775    match decl {
776        Declarator::Simple(_) => Ok(base),
777        Declarator::Array(arr) => {
778            let mut suffix = String::new();
779            for _ in &arr.sizes {
780                suffix.push_str("[]");
781            }
782            Ok(format!("{base}{suffix}"))
783        }
784    }
785}
786
787/// zerodds-lint: recursion-depth 64 (Parser/AST-Walk; bounded by IDL nesting)
788pub(crate) fn typespec_to_java(ts: &TypeSpec) -> Result<String, JavaGenError> {
789    match ts {
790        TypeSpec::Primitive(p) => Ok(primitive_to_java(*p).to_string()),
791        TypeSpec::Scoped(s) => Ok(scoped_to_java(s)),
792        TypeSpec::Sequence(s) => {
793            let inner = match &*s.elem {
794                TypeSpec::Primitive(p) => primitive_to_java_boxed(*p).to_string(),
795                TypeSpec::Scoped(sn) => scoped_to_java(sn),
796                TypeSpec::String(_) => "String".into(),
797                other => typespec_to_java(other)?,
798            };
799            Ok(format!("java.util.List<{inner}>"))
800        }
801        TypeSpec::String(_) => Ok("String".into()),
802        TypeSpec::Map(m) => {
803            let k = match &*m.key {
804                TypeSpec::Primitive(p) => primitive_to_java_boxed(*p).to_string(),
805                TypeSpec::Scoped(sn) => scoped_to_java(sn),
806                TypeSpec::String(_) => "String".into(),
807                other => typespec_to_java(other)?,
808            };
809            let v = match &*m.value {
810                TypeSpec::Primitive(p) => primitive_to_java_boxed(*p).to_string(),
811                TypeSpec::Scoped(sn) => scoped_to_java(sn),
812                TypeSpec::String(_) => "String".into(),
813                other => typespec_to_java(other)?,
814            };
815            Ok(format!("java.util.Map<{k}, {v}>"))
816        }
817        TypeSpec::Fixed(_) => {
818            // Spec idl4-java §7.2.4.2.4: fixed<digits,scale> ->
819            // `java.math.BigDecimal` (Range-Check via
820            // `java.lang.ArithmeticException` at runtime, scale via
821            // `setScale(scale)` in the codegen output).
822            Ok("java.math.BigDecimal".into())
823        }
824        TypeSpec::Any => {
825            // Spec idl4-java §7.3: any -> `org.omg.type.Any`. ZeroDDS
826            // mapping choice: `java.lang.Object` (reflection-based, the
827            // spec explicitly says "implementation is middleware
828            // specific"). An org.omg.type.Any wrapper variant is
829            // possible, but Object is enough for the Java-Type-Repr §8 path.
830            Ok("Object".into())
831        }
832    }
833}
834
835pub(crate) fn switch_type_to_java(s: &SwitchTypeSpec) -> Result<String, JavaGenError> {
836    Ok(match s {
837        SwitchTypeSpec::Integer(i) => integer_to_java(*i).to_string(),
838        SwitchTypeSpec::Char => "char".into(),
839        SwitchTypeSpec::Boolean => "boolean".into(),
840        SwitchTypeSpec::Octet => "byte".into(),
841        SwitchTypeSpec::Scoped(s) => scoped_to_java(s),
842    })
843}
844
845fn const_type_to_java(t: &zerodds_idl::ast::ConstType) -> Result<String, JavaGenError> {
846    Ok(match t {
847        zerodds_idl::ast::ConstType::Integer(i) => integer_to_java(*i).to_string(),
848        zerodds_idl::ast::ConstType::Floating(f) => floating_to_java(*f).to_string(),
849        zerodds_idl::ast::ConstType::Boolean => "boolean".into(),
850        zerodds_idl::ast::ConstType::Char => "char".into(),
851        zerodds_idl::ast::ConstType::WideChar => "char".into(),
852        zerodds_idl::ast::ConstType::Octet => "byte".into(),
853        zerodds_idl::ast::ConstType::String { .. } => "String".into(),
854        zerodds_idl::ast::ConstType::Scoped(s) => scoped_to_java(s),
855        zerodds_idl::ast::ConstType::Fixed => "java.math.BigDecimal".into(),
856    })
857}
858
859fn scoped_to_java(s: &ScopedName) -> String {
860    let parts: Vec<String> = s.parts.iter().map(|p| p.text.clone()).collect();
861    parts.join(".")
862}
863
864// ---------------------------------------------------------------------------
865// Imports collection
866// ---------------------------------------------------------------------------
867
868#[derive(Debug, Default, Clone)]
869pub(crate) struct ImportSet {
870    #[allow(dead_code)]
871    imports: BTreeSet<&'static str>,
872}
873
874impl ImportSet {
875    #[allow(dead_code)]
876    fn add(&mut self, fqn: &'static str) {
877        self.imports.insert(fqn);
878    }
879}
880
881/// Hook for C5.4-b: the per-member import collection can be extended
882/// here. C5.4-a uses FQN throughout, hence a no-op.
883#[allow(clippy::needless_pass_by_ref_mut)]
884fn collect_member_imports(_m: &Member, _inc: &mut ImportSet) {
885    // FQN strategy: java.util.List/Optional/Map are referenced inline as
886    // `java.util.<X>`, so no import entries are needed.
887}
888
889// ---------------------------------------------------------------------------
890// Compilation-Unit-Wrapping
891// ---------------------------------------------------------------------------
892
893pub(crate) fn wrap_compilation_unit(pkg: &str, _imports: &ImportSet, body: &str) -> String {
894    let mut out = String::new();
895    let _ = writeln!(out, "// Generated by zerodds idl-java. Do not edit.");
896    if !pkg.is_empty() {
897        let _ = writeln!(out, "package {pkg};");
898        let _ = writeln!(out);
899    }
900    // Imports are currently replaced by FQN — no import statements
901    // required. This keeps the diff stable and avoids conflicts with
902    // type names like `List`/`Map` if the IDL named a type that way.
903    out.push_str(body);
904    out
905}
906
907// ---------------------------------------------------------------------------
908// ConstExpr → Java-Literal
909// ---------------------------------------------------------------------------
910
911/// zerodds-lint: recursion-depth 64 (Parser/AST-Walk; bounded by IDL nesting)
912pub(crate) fn const_expr_to_java(e: &ConstExpr) -> String {
913    match e {
914        ConstExpr::Literal(l) => literal_to_java(l),
915        ConstExpr::Scoped(s) => scoped_to_java(s),
916        ConstExpr::Unary { op, operand, .. } => {
917            let prefix = match op {
918                zerodds_idl::ast::UnaryOp::Plus => "+",
919                zerodds_idl::ast::UnaryOp::Minus => "-",
920                zerodds_idl::ast::UnaryOp::BitNot => "~",
921            };
922            format!("{prefix}{}", const_expr_to_java(operand))
923        }
924        ConstExpr::Binary { op, lhs, rhs, .. } => {
925            let opstr = match op {
926                zerodds_idl::ast::BinaryOp::Or => "|",
927                zerodds_idl::ast::BinaryOp::Xor => "^",
928                zerodds_idl::ast::BinaryOp::And => "&",
929                zerodds_idl::ast::BinaryOp::Shl => "<<",
930                zerodds_idl::ast::BinaryOp::Shr => ">>",
931                zerodds_idl::ast::BinaryOp::Add => "+",
932                zerodds_idl::ast::BinaryOp::Sub => "-",
933                zerodds_idl::ast::BinaryOp::Mul => "*",
934                zerodds_idl::ast::BinaryOp::Div => "/",
935                zerodds_idl::ast::BinaryOp::Mod => "%",
936            };
937            format!(
938                "({} {opstr} {})",
939                const_expr_to_java(lhs),
940                const_expr_to_java(rhs)
941            )
942        }
943    }
944}
945
946fn literal_to_java(l: &Literal) -> String {
947    match l.kind {
948        LiteralKind::Boolean
949        | LiteralKind::Integer
950        | LiteralKind::Floating
951        | LiteralKind::Char
952        | LiteralKind::WideChar
953        | LiteralKind::String
954        | LiteralKind::WideString
955        | LiteralKind::Fixed => l.raw.clone(),
956    }
957}
958
959// ---------------------------------------------------------------------------
960// Annotation-Helpers
961// ---------------------------------------------------------------------------
962
963fn has_optional_annotation(anns: &[Annotation]) -> bool {
964    has_named_annotation(anns, "optional")
965}
966
967fn has_named_annotation(anns: &[Annotation], name: &str) -> bool {
968    anns.iter().any(|a| {
969        a.name.parts.last().is_some_and(|p| p.text == name)
970            && matches!(a.params, AnnotationParams::None | AnnotationParams::Empty)
971    })
972}
973
974// ---------------------------------------------------------------------------
975// Inheritance-Cycle-Detection
976// ---------------------------------------------------------------------------
977
978/// zerodds-lint: recursion-depth 64 (Parser/AST-Walk; bounded by IDL nesting)
979fn collect_inheritance_edges(
980    defs: &[Definition],
981    parents: &mut HashMap<String, String>,
982    prefix: &str,
983) {
984    for d in defs {
985        match d {
986            Definition::Module(m) => {
987                let new_prefix = if prefix.is_empty() {
988                    m.name.text.clone()
989                } else {
990                    format!("{prefix}.{}", m.name.text)
991                };
992                collect_inheritance_edges(&m.definitions, parents, &new_prefix);
993            }
994            Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Def(s)))) => {
995                let key = if prefix.is_empty() {
996                    s.name.text.clone()
997                } else {
998                    format!("{prefix}.{}", s.name.text)
999                };
1000                if let Some(b) = &s.base {
1001                    let base_str = b
1002                        .parts
1003                        .iter()
1004                        .map(|p| p.text.clone())
1005                        .collect::<Vec<_>>()
1006                        .join(".");
1007                    parents.insert(key, base_str);
1008                }
1009            }
1010            _ => {}
1011        }
1012    }
1013}
1014
1015fn detect_inheritance_cycles(spec: &Specification) -> Result<(), JavaGenError> {
1016    let mut parents: HashMap<String, String> = HashMap::new();
1017    collect_inheritance_edges(&spec.definitions, &mut parents, "");
1018
1019    for start in parents.keys() {
1020        let mut current = start.clone();
1021        let mut visited: BTreeSet<String> = BTreeSet::new();
1022        visited.insert(current.clone());
1023        while let Some(p) = parents.get(&current) {
1024            let resolved = parents
1025                .keys()
1026                .find(|k| *k == p || k.ends_with(&format!(".{p}")))
1027                .cloned()
1028                .unwrap_or_else(|| p.clone());
1029            if visited.contains(&resolved) {
1030                return Err(JavaGenError::InheritanceCycle {
1031                    type_name: short_name(&resolved),
1032                });
1033            }
1034            visited.insert(resolved.clone());
1035            if resolved == current {
1036                return Err(JavaGenError::InheritanceCycle {
1037                    type_name: short_name(&resolved),
1038                });
1039            }
1040            current = resolved;
1041            if !parents.contains_key(&current) {
1042                break;
1043            }
1044        }
1045    }
1046    Ok(())
1047}
1048
1049fn short_name(s: &str) -> String {
1050    s.rsplit('.').next().unwrap_or(s).to_string()
1051}
1052
1053// ---------------------------------------------------------------------------
1054// Helpers
1055// ---------------------------------------------------------------------------
1056
1057pub(crate) fn indent_unit(opts: &JavaGenOptions) -> String {
1058    " ".repeat(opts.indent_width)
1059}
1060
1061pub(crate) fn capitalize(s: &str) -> String {
1062    let mut chars = s.chars();
1063    match chars.next() {
1064        Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
1065        None => String::new(),
1066    }
1067}
1068
1069pub(crate) fn fmt_err(_: core::fmt::Error) -> JavaGenError {
1070    JavaGenError::Internal("string formatting failed".into())
1071}
1072
1073/// Wrap helper for bitset/bitmask: wraps header + body into a
1074/// compilation unit. Identical to [`wrap_compilation_unit`] but without
1075/// the import argument.
1076pub(crate) fn wrap_compilation_unit_default(pkg: &str, body: &str) -> String {
1077    wrap_compilation_unit(pkg, &ImportSet::default(), body)
1078}
1079
1080// ---------------------------------------------------------------------------
1081// Multi-Inheritance — Interface-Pattern (C5.4-b §3)
1082// ---------------------------------------------------------------------------
1083
1084/// Collects for each struct definition the (short) name of its direct
1085/// predecessor. IDL4 `struct` inheritance is single — the codegen
1086/// produces the `extends + implements XInterface, YInterface` form from
1087/// the transitive chain.
1088fn collect_base_chain_index(spec: &Specification) -> std::collections::HashMap<String, String> {
1089    let mut out: std::collections::HashMap<String, String> = std::collections::HashMap::new();
1090    /// zerodds-lint: recursion-depth 32
1091    ///
1092    /// Module hierarchy in IDL files: typical depth 2-4
1093    /// (`org::omg::dds::core`), 32 covers edge cases.
1094    fn visit(defs: &[Definition], out: &mut std::collections::HashMap<String, String>) {
1095        for d in defs {
1096            match d {
1097                Definition::Module(m) => visit(&m.definitions, out),
1098                Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Def(s)))) => {
1099                    if let Some(b) = &s.base {
1100                        out.insert(s.name.text.clone(), scoped_to_short(b));
1101                    }
1102                }
1103                _ => {}
1104            }
1105        }
1106    }
1107    visit(&spec.definitions, &mut out);
1108    out
1109}
1110
1111/// Returns the transitive ancestor names *beyond* the direct base
1112/// (all grandparents). For `A : B`, `B : C`, `C : D`,
1113/// `transitive_ancestors_beyond_base("A", ctx) → ["C", "D"]`.
1114fn transitive_ancestors_beyond_base(name: &str, ctx: &EmitCtx) -> Vec<String> {
1115    let mut out: Vec<String> = Vec::new();
1116    let direct = match ctx.parent_of.get(name) {
1117        Some(p) => p.clone(),
1118        None => return out,
1119    };
1120    let mut current = direct;
1121    let mut guard = 0usize;
1122    while let Some(p) = ctx.parent_of.get(&current) {
1123        if guard > 64 {
1124            break;
1125        }
1126        guard += 1;
1127        out.push(p.clone());
1128        current = p.clone();
1129    }
1130    out
1131}
1132
1133fn scoped_to_short(s: &ScopedName) -> String {
1134    s.parts.last().map(|p| p.text.clone()).unwrap_or_default()
1135}
1136
1137/// Emits a companion interface `<Name>Interface.java` with default
1138/// methods that mirror the bean-pattern read-only view of the members.
1139/// This lets sub-sub-classes include the ancestor via
1140/// `implements <Name>Interface` without violating the JVM class-file
1141/// constraints (single `extends`).
1142fn emit_struct_companion_interface(
1143    s: &StructDef,
1144    pkg: &str,
1145    opts: &JavaGenOptions,
1146) -> Result<JavaFile, JavaGenError> {
1147    let class = sanitize_identifier(&s.name.text)?;
1148    let interface_name = format!("{class}Interface");
1149    let ind = indent_unit(opts);
1150    let mut body = String::new();
1151    writeln!(
1152        body,
1153        "/** Companion interface for {class}; lets sub-sub-classes \
1154         participate in the {class} contract via `implements`. */",
1155    )
1156    .map_err(fmt_err)?;
1157    writeln!(body, "public interface {interface_name} {{").map_err(fmt_err)?;
1158    // Default methods — we render the getter signatures with a
1159    // `default` implementation that delegates to the concrete class via
1160    // a cast. Since we know the class at compile time (every `implements`
1161    // site is a subclass of `class`), we produce only abstract methods
1162    // here — the concrete class provides the implementation as a usual
1163    // bean getter.
1164    for m in &s.members {
1165        let opt = has_optional_annotation(&m.annotations);
1166        for decl in &m.declarators {
1167            let java_ty = type_for_declarator(&m.type_spec, decl)?;
1168            let name = sanitize_identifier(&decl.name().text)?;
1169            let cap = capitalize(&name);
1170            let final_ty = if opt {
1171                format!("java.util.Optional<{}>", boxed_for_optional(&m.type_spec))
1172            } else {
1173                java_ty
1174            };
1175            writeln!(body, "{ind}{final_ty} get{cap}();").map_err(fmt_err)?;
1176        }
1177    }
1178    writeln!(body, "}}").map_err(fmt_err)?;
1179    let source = wrap_compilation_unit_default(pkg, &body);
1180    Ok(JavaFile {
1181        package_path: pkg.to_string(),
1182        class_name: interface_name,
1183        source,
1184    })
1185}
1186
1187// Marker so unused imports produce no warnings (e.g.
1188// `IntegerType`/`integer_to_java_boxed`, in case the detailed use is
1189// removed later).
1190#[allow(dead_code)]
1191fn _unused_marker(_i: IntegerType) {
1192    let _ = integer_to_java_boxed;
1193    let _ = floating_to_java_boxed;
1194}
1195
1196// ---------------------------------------------------------------------------
1197// RPC-Service-Bridge (DDS-RPC §7.11.2)
1198// ---------------------------------------------------------------------------
1199
1200/// Spec idl4-java §7.6: valuetype -> 2 Java classes
1201/// (`<Name>Abstract` abstract + `<Name>` non-abstract).
1202/// public state -> public abstract bean accessors; private state ->
1203/// protected abstract accessors; factory -> void method.
1204fn emit_value_type_files(
1205    v: &zerodds_idl::ast::ValueDef,
1206    pkg: &str,
1207    opts: &JavaGenOptions,
1208) -> Result<Vec<JavaFile>, JavaGenError> {
1209    use zerodds_idl::ast::{Export, StateVisibility, ValueElement};
1210
1211    let class = sanitize_identifier(&v.name.text)?;
1212    let abstract_name = format!("{class}Abstract");
1213    let ind = indent_unit(opts);
1214    let imports = ImportSet::default();
1215
1216    // Abstract base class.
1217    let mut body = String::new();
1218    let extends = match &v.inheritance {
1219        Some(inh) if !inh.bases.is_empty() => {
1220            // Java allows only one superclass — we take the first base.
1221            let base = scoped_to_java(&inh.bases[0]);
1222            format!(" extends {base}Abstract")
1223        }
1224        _ => String::new(),
1225    };
1226    let supports = match &v.inheritance {
1227        Some(inh) if !inh.supports.is_empty() => {
1228            let s: Vec<String> = inh.supports.iter().map(scoped_to_java).collect();
1229            format!(" implements {}", s.join(", "))
1230        }
1231        _ => String::new(),
1232    };
1233
1234    writeln!(
1235        body,
1236        "public abstract class {abstract_name}{extends}{supports} {{"
1237    )
1238    .map_err(fmt_err)?;
1239
1240    for el in &v.elements {
1241        match el {
1242            ValueElement::State(s) => {
1243                let ty = typespec_to_java(&s.type_spec)?;
1244                let visibility = match s.visibility {
1245                    StateVisibility::Public => "public",
1246                    StateVisibility::Private => "protected",
1247                };
1248                for d in &s.declarators {
1249                    let n = sanitize_identifier(&d.name().text)?;
1250                    writeln!(body, "{ind}{visibility} abstract {ty} get_{n}();")
1251                        .map_err(fmt_err)?;
1252                    writeln!(body, "{ind}{visibility} abstract void set_{n}({ty} value);")
1253                        .map_err(fmt_err)?;
1254                }
1255            }
1256            ValueElement::Init(i) => {
1257                let params: Vec<String> = i
1258                    .params
1259                    .iter()
1260                    .map(|p| -> Result<String, JavaGenError> {
1261                        let ty = typespec_to_java(&p.type_spec)?;
1262                        let pname = sanitize_identifier(&p.name.text)?;
1263                        Ok(format!("{ty} {pname}"))
1264                    })
1265                    .collect::<Result<_, _>>()?;
1266                writeln!(
1267                    body,
1268                    "{ind}public abstract void {}({});",
1269                    sanitize_identifier(&i.name.text)?,
1270                    params.join(", ")
1271                )
1272                .map_err(fmt_err)?;
1273            }
1274            ValueElement::Export(Export::Op(op)) => {
1275                let ret = match &op.return_type {
1276                    None => "void".to_string(),
1277                    Some(t) => typespec_to_java(t)?,
1278                };
1279                let params: Vec<String> = op
1280                    .params
1281                    .iter()
1282                    .map(|p| -> Result<String, JavaGenError> {
1283                        let ty = typespec_to_java(&p.type_spec)?;
1284                        let pname = sanitize_identifier(&p.name.text)?;
1285                        Ok(format!("{ty} {pname}"))
1286                    })
1287                    .collect::<Result<_, _>>()?;
1288                writeln!(
1289                    body,
1290                    "{ind}public abstract {ret} {}({});",
1291                    sanitize_identifier(&op.name.text)?,
1292                    params.join(", ")
1293                )
1294                .map_err(fmt_err)?;
1295            }
1296            _ => {}
1297        }
1298    }
1299    writeln!(body, "}}").map_err(fmt_err)?;
1300
1301    let abstract_source = wrap_compilation_unit(pkg, &imports, &body);
1302    let abstract_file = JavaFile {
1303        package_path: pkg.to_string(),
1304        class_name: abstract_name.clone(),
1305        source: abstract_source,
1306    };
1307
1308    // Concrete subclass skeleton.
1309    let concrete_body = format!(
1310        "public class {class} extends {abstract_name} {{\n{ind}// User implementation here\n}}\n"
1311    );
1312    let concrete_source = wrap_compilation_unit(pkg, &imports, &concrete_body);
1313    let concrete_file = JavaFile {
1314        package_path: pkg.to_string(),
1315        class_name: class,
1316        source: concrete_source,
1317    };
1318
1319    Ok(vec![abstract_file, concrete_file])
1320}
1321
1322/// Spec idl4-java §7.4: IDL interface -> Java public interface with a
1323/// method per operation (raises -> throws), a property per attribute.
1324fn emit_non_service_interface_file(
1325    iface: &InterfaceDef,
1326    pkg: &str,
1327    opts: &JavaGenOptions,
1328) -> Result<JavaFile, JavaGenError> {
1329    use zerodds_idl::ast::Export;
1330
1331    let class = sanitize_identifier(&iface.name.text)?;
1332    let imports = ImportSet::default();
1333    let ind = indent_unit(opts);
1334    let mut body = String::new();
1335
1336    let extends = if iface.bases.is_empty() {
1337        String::new()
1338    } else {
1339        let bases: Vec<String> = iface.bases.iter().map(scoped_to_java).collect();
1340        format!(" extends {}", bases.join(", "))
1341    };
1342    writeln!(body, "public interface {class}{extends} {{").map_err(fmt_err)?;
1343
1344    for export in &iface.exports {
1345        match export {
1346            Export::Op(op) => {
1347                let ret = match &op.return_type {
1348                    None => "void".to_string(),
1349                    Some(t) => typespec_to_java(t)?,
1350                };
1351                let params: Vec<String> = op
1352                    .params
1353                    .iter()
1354                    .map(|p| -> Result<String, JavaGenError> {
1355                        let ty = typespec_to_java(&p.type_spec)?;
1356                        let pname = sanitize_identifier(&p.name.text)?;
1357                        Ok(format!("{ty} {pname}"))
1358                    })
1359                    .collect::<Result<_, _>>()?;
1360                let throws = if op.raises.is_empty() {
1361                    String::new()
1362                } else {
1363                    let raises: Vec<String> = op.raises.iter().map(scoped_to_java).collect();
1364                    format!(" throws {}", raises.join(", "))
1365                };
1366                writeln!(
1367                    body,
1368                    "{ind}{ret} {}({}){throws};",
1369                    sanitize_identifier(&op.name.text)?,
1370                    params.join(", ")
1371                )
1372                .map_err(fmt_err)?;
1373            }
1374            Export::Attr(attr) => {
1375                let ty = typespec_to_java(&attr.type_spec)?;
1376                let aname = sanitize_identifier(&attr.name.text)?;
1377                writeln!(body, "{ind}{ty} get_{aname}();").map_err(fmt_err)?;
1378                if !attr.readonly {
1379                    writeln!(body, "{ind}void set_{aname}({ty} value);").map_err(fmt_err)?;
1380                }
1381            }
1382            _ => {
1383                // Embedded type/const/exception: not currently implemented.
1384            }
1385        }
1386    }
1387    writeln!(body, "}}").map_err(fmt_err)?;
1388
1389    let source = wrap_compilation_unit(pkg, &imports, &body);
1390    Ok(JavaFile {
1391        package_path: pkg.to_string(),
1392        class_name: class,
1393        source,
1394    })
1395}
1396
1397/// `true` if the interface annotations contain `@service` — then we
1398/// treat the interface as an RPC service and delegate to
1399/// [`crate::rpc`].
1400fn is_service_interface(iface: &InterfaceDef) -> bool {
1401    iface
1402        .annotations
1403        .iter()
1404        .any(|a| a.name.parts.last().is_some_and(|p| p.text == "service"))
1405}
1406
1407/// Emits the five RPC files for a `@service` interface plus the
1408/// `exception` files referenced in the `raises` clauses (declared
1409/// locally in the interface body).
1410fn emit_service_interface_files(
1411    iface: &InterfaceDef,
1412    pkg: &str,
1413    opts: &JavaGenOptions,
1414    files: &mut Vec<JavaFile>,
1415) -> Result<(), JavaGenError> {
1416    use zerodds_idl::ast::Export;
1417    use zerodds_rpc::annotations::lower_rpc_annotations;
1418    use zerodds_rpc::service_mapping::lower_service;
1419
1420    // Inner exceptions — emit as RuntimeException subclasses, via the
1421    // existing exception path.
1422    for export in &iface.exports {
1423        if let Export::Except(e) = export {
1424            files.push(emit_exception_file(e, pkg, opts)?);
1425        }
1426    }
1427
1428    // Lower IDL → ServiceDef.
1429    let lowered = lower_rpc_annotations(&iface.annotations);
1430    let svc = lower_service(iface, &lowered).map_err(|e| JavaGenError::Internal(e.to_string()))?;
1431
1432    // Emit the five Java files.
1433    let svc_files = crate::rpc::emit_service_files(&svc, pkg, opts)?;
1434    files.extend(svc_files);
1435
1436    Ok(())
1437}