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 (`docs/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 (`docs/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        TypeSpec::Sequence(s) => {
754            // List<Boxed<T>>
755            let inner = match &*s.elem {
756                TypeSpec::Primitive(p) => primitive_to_java_boxed(*p).to_string(),
757                TypeSpec::Scoped(sn) => scoped_to_java(sn),
758                TypeSpec::String(_) => "String".into(),
759                _ => "Object".into(),
760            };
761            format!("java.util.List<{inner}>")
762        }
763        _ => "Object".into(),
764    }
765}
766
767// ---------------------------------------------------------------------------
768// TypeSpec / Declarator
769// ---------------------------------------------------------------------------
770
771/// Returns the Java type expression for a member (TypeSpec + Declarator).
772pub(crate) fn type_for_declarator(
773    ts: &TypeSpec,
774    decl: &Declarator,
775) -> Result<String, JavaGenError> {
776    let base = typespec_to_java(ts)?;
777    match decl {
778        Declarator::Simple(_) => Ok(base),
779        Declarator::Array(arr) => {
780            let mut suffix = String::new();
781            for _ in &arr.sizes {
782                suffix.push_str("[]");
783            }
784            Ok(format!("{base}{suffix}"))
785        }
786    }
787}
788
789/// zerodds-lint: recursion-depth 64 (Parser/AST-Walk; bounded by IDL nesting)
790pub(crate) fn typespec_to_java(ts: &TypeSpec) -> Result<String, JavaGenError> {
791    match ts {
792        TypeSpec::Primitive(p) => Ok(primitive_to_java(*p).to_string()),
793        TypeSpec::Scoped(s) => Ok(scoped_to_java(s)),
794        TypeSpec::Sequence(s) => {
795            let inner = match &*s.elem {
796                TypeSpec::Primitive(p) => primitive_to_java_boxed(*p).to_string(),
797                TypeSpec::Scoped(sn) => scoped_to_java(sn),
798                TypeSpec::String(_) => "String".into(),
799                other => typespec_to_java(other)?,
800            };
801            Ok(format!("java.util.List<{inner}>"))
802        }
803        TypeSpec::String(_) => Ok("String".into()),
804        TypeSpec::Map(m) => {
805            let k = match &*m.key {
806                TypeSpec::Primitive(p) => primitive_to_java_boxed(*p).to_string(),
807                TypeSpec::Scoped(sn) => scoped_to_java(sn),
808                TypeSpec::String(_) => "String".into(),
809                other => typespec_to_java(other)?,
810            };
811            let v = match &*m.value {
812                TypeSpec::Primitive(p) => primitive_to_java_boxed(*p).to_string(),
813                TypeSpec::Scoped(sn) => scoped_to_java(sn),
814                TypeSpec::String(_) => "String".into(),
815                other => typespec_to_java(other)?,
816            };
817            Ok(format!("java.util.Map<{k}, {v}>"))
818        }
819        TypeSpec::Fixed(_) => {
820            // Spec idl4-java §7.2.4.2.4: fixed<digits,scale> ->
821            // `java.math.BigDecimal` (Range-Check via
822            // `java.lang.ArithmeticException` at runtime, scale via
823            // `setScale(scale)` in the codegen output).
824            Ok("java.math.BigDecimal".into())
825        }
826        TypeSpec::Any => {
827            // Spec idl4-java §7.3: any -> `org.omg.type.Any`. ZeroDDS
828            // mapping choice: `java.lang.Object` (reflection-based, the
829            // spec explicitly says "implementation is middleware
830            // specific"). An org.omg.type.Any wrapper variant is
831            // possible, but Object is enough for the Java-Type-Repr §8 path.
832            Ok("Object".into())
833        }
834    }
835}
836
837pub(crate) fn switch_type_to_java(s: &SwitchTypeSpec) -> Result<String, JavaGenError> {
838    Ok(match s {
839        SwitchTypeSpec::Integer(i) => integer_to_java(*i).to_string(),
840        SwitchTypeSpec::Char => "char".into(),
841        SwitchTypeSpec::Boolean => "boolean".into(),
842        SwitchTypeSpec::Octet => "byte".into(),
843        SwitchTypeSpec::Scoped(s) => scoped_to_java(s),
844    })
845}
846
847fn const_type_to_java(t: &zerodds_idl::ast::ConstType) -> Result<String, JavaGenError> {
848    Ok(match t {
849        zerodds_idl::ast::ConstType::Integer(i) => integer_to_java(*i).to_string(),
850        zerodds_idl::ast::ConstType::Floating(f) => floating_to_java(*f).to_string(),
851        zerodds_idl::ast::ConstType::Boolean => "boolean".into(),
852        zerodds_idl::ast::ConstType::Char => "char".into(),
853        zerodds_idl::ast::ConstType::WideChar => "char".into(),
854        zerodds_idl::ast::ConstType::Octet => "byte".into(),
855        zerodds_idl::ast::ConstType::String { .. } => "String".into(),
856        zerodds_idl::ast::ConstType::Scoped(s) => scoped_to_java(s),
857        zerodds_idl::ast::ConstType::Fixed => "java.math.BigDecimal".into(),
858    })
859}
860
861fn scoped_to_java(s: &ScopedName) -> String {
862    let parts: Vec<String> = s.parts.iter().map(|p| p.text.clone()).collect();
863    parts.join(".")
864}
865
866// ---------------------------------------------------------------------------
867// Imports collection
868// ---------------------------------------------------------------------------
869
870#[derive(Debug, Default, Clone)]
871pub(crate) struct ImportSet {
872    #[allow(dead_code)]
873    imports: BTreeSet<&'static str>,
874}
875
876impl ImportSet {
877    #[allow(dead_code)]
878    fn add(&mut self, fqn: &'static str) {
879        self.imports.insert(fqn);
880    }
881}
882
883/// Hook for C5.4-b: the per-member import collection can be extended
884/// here. C5.4-a uses FQN throughout, hence a no-op.
885#[allow(clippy::needless_pass_by_ref_mut)]
886fn collect_member_imports(_m: &Member, _inc: &mut ImportSet) {
887    // FQN strategy: java.util.List/Optional/Map are referenced inline as
888    // `java.util.<X>`, so no import entries are needed.
889}
890
891// ---------------------------------------------------------------------------
892// Compilation-Unit-Wrapping
893// ---------------------------------------------------------------------------
894
895pub(crate) fn wrap_compilation_unit(pkg: &str, _imports: &ImportSet, body: &str) -> String {
896    let mut out = String::new();
897    let _ = writeln!(out, "// Generated by zerodds idl-java. Do not edit.");
898    if !pkg.is_empty() {
899        let _ = writeln!(out, "package {pkg};");
900        let _ = writeln!(out);
901    }
902    // Imports are currently replaced by FQN — no import statements
903    // required. This keeps the diff stable and avoids conflicts with
904    // type names like `List`/`Map` if the IDL named a type that way.
905    out.push_str(body);
906    out
907}
908
909// ---------------------------------------------------------------------------
910// ConstExpr → Java-Literal
911// ---------------------------------------------------------------------------
912
913/// zerodds-lint: recursion-depth 64 (Parser/AST-Walk; bounded by IDL nesting)
914pub(crate) fn const_expr_to_java(e: &ConstExpr) -> String {
915    match e {
916        ConstExpr::Literal(l) => literal_to_java(l),
917        ConstExpr::Scoped(s) => scoped_to_java(s),
918        ConstExpr::Unary { op, operand, .. } => {
919            let prefix = match op {
920                zerodds_idl::ast::UnaryOp::Plus => "+",
921                zerodds_idl::ast::UnaryOp::Minus => "-",
922                zerodds_idl::ast::UnaryOp::BitNot => "~",
923            };
924            format!("{prefix}{}", const_expr_to_java(operand))
925        }
926        ConstExpr::Binary { op, lhs, rhs, .. } => {
927            let opstr = match op {
928                zerodds_idl::ast::BinaryOp::Or => "|",
929                zerodds_idl::ast::BinaryOp::Xor => "^",
930                zerodds_idl::ast::BinaryOp::And => "&",
931                zerodds_idl::ast::BinaryOp::Shl => "<<",
932                zerodds_idl::ast::BinaryOp::Shr => ">>",
933                zerodds_idl::ast::BinaryOp::Add => "+",
934                zerodds_idl::ast::BinaryOp::Sub => "-",
935                zerodds_idl::ast::BinaryOp::Mul => "*",
936                zerodds_idl::ast::BinaryOp::Div => "/",
937                zerodds_idl::ast::BinaryOp::Mod => "%",
938            };
939            format!(
940                "({} {opstr} {})",
941                const_expr_to_java(lhs),
942                const_expr_to_java(rhs)
943            )
944        }
945    }
946}
947
948fn literal_to_java(l: &Literal) -> String {
949    match l.kind {
950        LiteralKind::Boolean
951        | LiteralKind::Integer
952        | LiteralKind::Floating
953        | LiteralKind::Char
954        | LiteralKind::WideChar
955        | LiteralKind::String
956        | LiteralKind::WideString
957        | LiteralKind::Fixed => l.raw.clone(),
958    }
959}
960
961// ---------------------------------------------------------------------------
962// Annotation-Helpers
963// ---------------------------------------------------------------------------
964
965fn has_optional_annotation(anns: &[Annotation]) -> bool {
966    has_named_annotation(anns, "optional")
967}
968
969fn has_named_annotation(anns: &[Annotation], name: &str) -> bool {
970    anns.iter().any(|a| {
971        a.name.parts.last().is_some_and(|p| p.text == name)
972            && matches!(a.params, AnnotationParams::None | AnnotationParams::Empty)
973    })
974}
975
976// ---------------------------------------------------------------------------
977// Inheritance-Cycle-Detection
978// ---------------------------------------------------------------------------
979
980/// zerodds-lint: recursion-depth 64 (Parser/AST-Walk; bounded by IDL nesting)
981fn collect_inheritance_edges(
982    defs: &[Definition],
983    parents: &mut HashMap<String, String>,
984    prefix: &str,
985) {
986    for d in defs {
987        match d {
988            Definition::Module(m) => {
989                let new_prefix = if prefix.is_empty() {
990                    m.name.text.clone()
991                } else {
992                    format!("{prefix}.{}", m.name.text)
993                };
994                collect_inheritance_edges(&m.definitions, parents, &new_prefix);
995            }
996            Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Def(s)))) => {
997                let key = if prefix.is_empty() {
998                    s.name.text.clone()
999                } else {
1000                    format!("{prefix}.{}", s.name.text)
1001                };
1002                if let Some(b) = &s.base {
1003                    let base_str = b
1004                        .parts
1005                        .iter()
1006                        .map(|p| p.text.clone())
1007                        .collect::<Vec<_>>()
1008                        .join(".");
1009                    parents.insert(key, base_str);
1010                }
1011            }
1012            _ => {}
1013        }
1014    }
1015}
1016
1017fn detect_inheritance_cycles(spec: &Specification) -> Result<(), JavaGenError> {
1018    let mut parents: HashMap<String, String> = HashMap::new();
1019    collect_inheritance_edges(&spec.definitions, &mut parents, "");
1020
1021    for start in parents.keys() {
1022        let mut current = start.clone();
1023        let mut visited: BTreeSet<String> = BTreeSet::new();
1024        visited.insert(current.clone());
1025        while let Some(p) = parents.get(&current) {
1026            let resolved = parents
1027                .keys()
1028                .find(|k| *k == p || k.ends_with(&format!(".{p}")))
1029                .cloned()
1030                .unwrap_or_else(|| p.clone());
1031            if visited.contains(&resolved) {
1032                return Err(JavaGenError::InheritanceCycle {
1033                    type_name: short_name(&resolved),
1034                });
1035            }
1036            visited.insert(resolved.clone());
1037            if resolved == current {
1038                return Err(JavaGenError::InheritanceCycle {
1039                    type_name: short_name(&resolved),
1040                });
1041            }
1042            current = resolved;
1043            if !parents.contains_key(&current) {
1044                break;
1045            }
1046        }
1047    }
1048    Ok(())
1049}
1050
1051fn short_name(s: &str) -> String {
1052    s.rsplit('.').next().unwrap_or(s).to_string()
1053}
1054
1055// ---------------------------------------------------------------------------
1056// Helpers
1057// ---------------------------------------------------------------------------
1058
1059pub(crate) fn indent_unit(opts: &JavaGenOptions) -> String {
1060    " ".repeat(opts.indent_width)
1061}
1062
1063pub(crate) fn capitalize(s: &str) -> String {
1064    let mut chars = s.chars();
1065    match chars.next() {
1066        Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
1067        None => String::new(),
1068    }
1069}
1070
1071pub(crate) fn fmt_err(_: core::fmt::Error) -> JavaGenError {
1072    JavaGenError::Internal("string formatting failed".into())
1073}
1074
1075/// Wrap helper for bitset/bitmask: wraps header + body into a
1076/// compilation unit. Identical to [`wrap_compilation_unit`] but without
1077/// the import argument.
1078pub(crate) fn wrap_compilation_unit_default(pkg: &str, body: &str) -> String {
1079    wrap_compilation_unit(pkg, &ImportSet::default(), body)
1080}
1081
1082// ---------------------------------------------------------------------------
1083// Multi-Inheritance — Interface-Pattern (C5.4-b §3)
1084// ---------------------------------------------------------------------------
1085
1086/// Collects for each struct definition the (short) name of its direct
1087/// predecessor. IDL4 `struct` inheritance is single — the codegen
1088/// produces the `extends + implements XInterface, YInterface` form from
1089/// the transitive chain.
1090fn collect_base_chain_index(spec: &Specification) -> std::collections::HashMap<String, String> {
1091    let mut out: std::collections::HashMap<String, String> = std::collections::HashMap::new();
1092    /// zerodds-lint: recursion-depth 32
1093    ///
1094    /// Module hierarchy in IDL files: typical depth 2-4
1095    /// (`org::omg::dds::core`), 32 covers edge cases.
1096    fn visit(defs: &[Definition], out: &mut std::collections::HashMap<String, String>) {
1097        for d in defs {
1098            match d {
1099                Definition::Module(m) => visit(&m.definitions, out),
1100                Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Def(s)))) => {
1101                    if let Some(b) = &s.base {
1102                        out.insert(s.name.text.clone(), scoped_to_short(b));
1103                    }
1104                }
1105                _ => {}
1106            }
1107        }
1108    }
1109    visit(&spec.definitions, &mut out);
1110    out
1111}
1112
1113/// Returns the transitive ancestor names *beyond* the direct base
1114/// (all grandparents). For `A : B`, `B : C`, `C : D`,
1115/// `transitive_ancestors_beyond_base("A", ctx) → ["C", "D"]`.
1116fn transitive_ancestors_beyond_base(name: &str, ctx: &EmitCtx) -> Vec<String> {
1117    let mut out: Vec<String> = Vec::new();
1118    let direct = match ctx.parent_of.get(name) {
1119        Some(p) => p.clone(),
1120        None => return out,
1121    };
1122    let mut current = direct;
1123    let mut guard = 0usize;
1124    while let Some(p) = ctx.parent_of.get(&current) {
1125        if guard > 64 {
1126            break;
1127        }
1128        guard += 1;
1129        out.push(p.clone());
1130        current = p.clone();
1131    }
1132    out
1133}
1134
1135fn scoped_to_short(s: &ScopedName) -> String {
1136    s.parts.last().map(|p| p.text.clone()).unwrap_or_default()
1137}
1138
1139/// Emits a companion interface `<Name>Interface.java` with default
1140/// methods that mirror the bean-pattern read-only view of the members.
1141/// This lets sub-sub-classes include the ancestor via
1142/// `implements <Name>Interface` without violating the JVM class-file
1143/// constraints (single `extends`).
1144fn emit_struct_companion_interface(
1145    s: &StructDef,
1146    pkg: &str,
1147    opts: &JavaGenOptions,
1148) -> Result<JavaFile, JavaGenError> {
1149    let class = sanitize_identifier(&s.name.text)?;
1150    let interface_name = format!("{class}Interface");
1151    let ind = indent_unit(opts);
1152    let mut body = String::new();
1153    writeln!(
1154        body,
1155        "/** Companion interface for {class}; lets sub-sub-classes \
1156         participate in the {class} contract via `implements`. */",
1157    )
1158    .map_err(fmt_err)?;
1159    writeln!(body, "public interface {interface_name} {{").map_err(fmt_err)?;
1160    // Default methods — we render the getter signatures with a
1161    // `default` implementation that delegates to the concrete class via
1162    // a cast. Since we know the class at compile time (every `implements`
1163    // site is a subclass of `class`), we produce only abstract methods
1164    // here — the concrete class provides the implementation as a usual
1165    // bean getter.
1166    for m in &s.members {
1167        let opt = has_optional_annotation(&m.annotations);
1168        for decl in &m.declarators {
1169            let java_ty = type_for_declarator(&m.type_spec, decl)?;
1170            let name = sanitize_identifier(&decl.name().text)?;
1171            let cap = capitalize(&name);
1172            let final_ty = if opt {
1173                format!("java.util.Optional<{}>", boxed_for_optional(&m.type_spec))
1174            } else {
1175                java_ty
1176            };
1177            writeln!(body, "{ind}{final_ty} get{cap}();").map_err(fmt_err)?;
1178        }
1179    }
1180    writeln!(body, "}}").map_err(fmt_err)?;
1181    let source = wrap_compilation_unit_default(pkg, &body);
1182    Ok(JavaFile {
1183        package_path: pkg.to_string(),
1184        class_name: interface_name,
1185        source,
1186    })
1187}
1188
1189// Marker so unused imports produce no warnings (e.g.
1190// `IntegerType`/`integer_to_java_boxed`, in case the detailed use is
1191// removed later).
1192#[allow(dead_code)]
1193fn _unused_marker(_i: IntegerType) {
1194    let _ = integer_to_java_boxed;
1195    let _ = floating_to_java_boxed;
1196}
1197
1198// ---------------------------------------------------------------------------
1199// RPC-Service-Bridge (DDS-RPC §7.11.2)
1200// ---------------------------------------------------------------------------
1201
1202/// Spec idl4-java §7.6: valuetype -> 2 Java classes
1203/// (`<Name>Abstract` abstract + `<Name>` non-abstract).
1204/// public state -> public abstract bean accessors; private state ->
1205/// protected abstract accessors; factory -> void method.
1206fn emit_value_type_files(
1207    v: &zerodds_idl::ast::ValueDef,
1208    pkg: &str,
1209    opts: &JavaGenOptions,
1210) -> Result<Vec<JavaFile>, JavaGenError> {
1211    use zerodds_idl::ast::{Export, StateVisibility, ValueElement};
1212
1213    let class = sanitize_identifier(&v.name.text)?;
1214    let abstract_name = format!("{class}Abstract");
1215    let ind = indent_unit(opts);
1216    let imports = ImportSet::default();
1217
1218    // Abstract base class.
1219    let mut body = String::new();
1220    let extends = match &v.inheritance {
1221        Some(inh) if !inh.bases.is_empty() => {
1222            // Java allows only one superclass — we take the first base.
1223            let base = scoped_to_java(&inh.bases[0]);
1224            format!(" extends {base}Abstract")
1225        }
1226        _ => String::new(),
1227    };
1228    let supports = match &v.inheritance {
1229        Some(inh) if !inh.supports.is_empty() => {
1230            let s: Vec<String> = inh.supports.iter().map(scoped_to_java).collect();
1231            format!(" implements {}", s.join(", "))
1232        }
1233        _ => String::new(),
1234    };
1235
1236    writeln!(
1237        body,
1238        "public abstract class {abstract_name}{extends}{supports} {{"
1239    )
1240    .map_err(fmt_err)?;
1241
1242    for el in &v.elements {
1243        match el {
1244            ValueElement::State(s) => {
1245                let ty = typespec_to_java(&s.type_spec)?;
1246                let visibility = match s.visibility {
1247                    StateVisibility::Public => "public",
1248                    StateVisibility::Private => "protected",
1249                };
1250                for d in &s.declarators {
1251                    let n = sanitize_identifier(&d.name().text)?;
1252                    writeln!(body, "{ind}{visibility} abstract {ty} get_{n}();")
1253                        .map_err(fmt_err)?;
1254                    writeln!(body, "{ind}{visibility} abstract void set_{n}({ty} value);")
1255                        .map_err(fmt_err)?;
1256                }
1257            }
1258            ValueElement::Init(i) => {
1259                let params: Vec<String> = i
1260                    .params
1261                    .iter()
1262                    .map(|p| -> Result<String, JavaGenError> {
1263                        let ty = typespec_to_java(&p.type_spec)?;
1264                        let pname = sanitize_identifier(&p.name.text)?;
1265                        Ok(format!("{ty} {pname}"))
1266                    })
1267                    .collect::<Result<_, _>>()?;
1268                writeln!(
1269                    body,
1270                    "{ind}public abstract void {}({});",
1271                    sanitize_identifier(&i.name.text)?,
1272                    params.join(", ")
1273                )
1274                .map_err(fmt_err)?;
1275            }
1276            ValueElement::Export(Export::Op(op)) => {
1277                let ret = match &op.return_type {
1278                    None => "void".to_string(),
1279                    Some(t) => typespec_to_java(t)?,
1280                };
1281                let params: Vec<String> = op
1282                    .params
1283                    .iter()
1284                    .map(|p| -> Result<String, JavaGenError> {
1285                        let ty = typespec_to_java(&p.type_spec)?;
1286                        let pname = sanitize_identifier(&p.name.text)?;
1287                        Ok(format!("{ty} {pname}"))
1288                    })
1289                    .collect::<Result<_, _>>()?;
1290                writeln!(
1291                    body,
1292                    "{ind}public abstract {ret} {}({});",
1293                    sanitize_identifier(&op.name.text)?,
1294                    params.join(", ")
1295                )
1296                .map_err(fmt_err)?;
1297            }
1298            _ => {}
1299        }
1300    }
1301    writeln!(body, "}}").map_err(fmt_err)?;
1302
1303    let abstract_source = wrap_compilation_unit(pkg, &imports, &body);
1304    let abstract_file = JavaFile {
1305        package_path: pkg.to_string(),
1306        class_name: abstract_name.clone(),
1307        source: abstract_source,
1308    };
1309
1310    // Concrete subclass skeleton.
1311    let concrete_body = format!(
1312        "public class {class} extends {abstract_name} {{\n{ind}// User implementation here\n}}\n"
1313    );
1314    let concrete_source = wrap_compilation_unit(pkg, &imports, &concrete_body);
1315    let concrete_file = JavaFile {
1316        package_path: pkg.to_string(),
1317        class_name: class,
1318        source: concrete_source,
1319    };
1320
1321    Ok(vec![abstract_file, concrete_file])
1322}
1323
1324/// Spec idl4-java §7.4: IDL interface -> Java public interface with a
1325/// method per operation (raises -> throws), a property per attribute.
1326fn emit_non_service_interface_file(
1327    iface: &InterfaceDef,
1328    pkg: &str,
1329    opts: &JavaGenOptions,
1330) -> Result<JavaFile, JavaGenError> {
1331    use zerodds_idl::ast::Export;
1332
1333    let class = sanitize_identifier(&iface.name.text)?;
1334    let imports = ImportSet::default();
1335    let ind = indent_unit(opts);
1336    let mut body = String::new();
1337
1338    let extends = if iface.bases.is_empty() {
1339        String::new()
1340    } else {
1341        let bases: Vec<String> = iface.bases.iter().map(scoped_to_java).collect();
1342        format!(" extends {}", bases.join(", "))
1343    };
1344    writeln!(body, "public interface {class}{extends} {{").map_err(fmt_err)?;
1345
1346    for export in &iface.exports {
1347        match export {
1348            Export::Op(op) => {
1349                let ret = match &op.return_type {
1350                    None => "void".to_string(),
1351                    Some(t) => typespec_to_java(t)?,
1352                };
1353                let params: Vec<String> = op
1354                    .params
1355                    .iter()
1356                    .map(|p| -> Result<String, JavaGenError> {
1357                        let ty = typespec_to_java(&p.type_spec)?;
1358                        let pname = sanitize_identifier(&p.name.text)?;
1359                        Ok(format!("{ty} {pname}"))
1360                    })
1361                    .collect::<Result<_, _>>()?;
1362                let throws = if op.raises.is_empty() {
1363                    String::new()
1364                } else {
1365                    let raises: Vec<String> = op.raises.iter().map(scoped_to_java).collect();
1366                    format!(" throws {}", raises.join(", "))
1367                };
1368                writeln!(
1369                    body,
1370                    "{ind}{ret} {}({}){throws};",
1371                    sanitize_identifier(&op.name.text)?,
1372                    params.join(", ")
1373                )
1374                .map_err(fmt_err)?;
1375            }
1376            Export::Attr(attr) => {
1377                let ty = typespec_to_java(&attr.type_spec)?;
1378                let aname = sanitize_identifier(&attr.name.text)?;
1379                writeln!(body, "{ind}{ty} get_{aname}();").map_err(fmt_err)?;
1380                if !attr.readonly {
1381                    writeln!(body, "{ind}void set_{aname}({ty} value);").map_err(fmt_err)?;
1382                }
1383            }
1384            _ => {
1385                // Embedded type/const/exception: not currently implemented.
1386            }
1387        }
1388    }
1389    writeln!(body, "}}").map_err(fmt_err)?;
1390
1391    let source = wrap_compilation_unit(pkg, &imports, &body);
1392    Ok(JavaFile {
1393        package_path: pkg.to_string(),
1394        class_name: class,
1395        source,
1396    })
1397}
1398
1399/// `true` if the interface annotations contain `@service` — then we
1400/// treat the interface as an RPC service and delegate to
1401/// [`crate::rpc`].
1402fn is_service_interface(iface: &InterfaceDef) -> bool {
1403    iface
1404        .annotations
1405        .iter()
1406        .any(|a| a.name.parts.last().is_some_and(|p| p.text == "service"))
1407}
1408
1409/// Emits the five RPC files for a `@service` interface plus the
1410/// `exception` files referenced in the `raises` clauses (declared
1411/// locally in the interface body).
1412fn emit_service_interface_files(
1413    iface: &InterfaceDef,
1414    pkg: &str,
1415    opts: &JavaGenOptions,
1416    files: &mut Vec<JavaFile>,
1417) -> Result<(), JavaGenError> {
1418    use zerodds_idl::ast::Export;
1419    use zerodds_rpc::annotations::lower_rpc_annotations;
1420    use zerodds_rpc::service_mapping::lower_service;
1421
1422    // Inner exceptions — emit as RuntimeException subclasses, via the
1423    // existing exception path.
1424    for export in &iface.exports {
1425        if let Export::Except(e) = export {
1426            files.push(emit_exception_file(e, pkg, opts)?);
1427        }
1428    }
1429
1430    // Lower IDL → ServiceDef.
1431    let lowered = lower_rpc_annotations(&iface.annotations);
1432    let svc = lower_service(iface, &lowered).map_err(|e| JavaGenError::Internal(e.to_string()))?;
1433
1434    // Emit the five Java files.
1435    let svc_files = crate::rpc::emit_service_files(&svc, pkg, opts)?;
1436    files.extend(svc_files);
1437
1438    Ok(())
1439}