Skip to main content

vox_codegen/targets/java/
mod.rs

1//! Java 17 source generation for the supported Vox RPC slice.
2
3use std::collections::HashSet;
4use std::fmt::{self, Write};
5
6use facet_core::{ScalarType, Shape};
7use phon_schema::schema_bundle_to_bytes;
8use vox_types::{
9    EnumInfo, MethodDescriptor, ServiceDescriptor, ShapeKind, StructInfo, VariantKind,
10    classify_shape, classify_variant, is_bytes,
11};
12
13/// One deterministic Java compilation unit.
14#[derive(Clone, Debug, Eq, PartialEq)]
15pub struct JavaFile {
16    pub relative_path: String,
17    pub source: String,
18}
19
20/// A focused generation error. Java generation is deliberately fallible because
21/// the first Java runtime accepts a smaller shape/transport surface than Rust.
22#[derive(Clone, Debug, Eq, PartialEq)]
23pub struct JavaCodegenError {
24    pub service: String,
25    pub method: String,
26    pub detail: String,
27}
28
29impl fmt::Display for JavaCodegenError {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        write!(
32            f,
33            "Java codegen does not support {}.{}: {}",
34            self.service, self.method, self.detail
35        )
36    }
37}
38
39impl std::error::Error for JavaCodegenError {}
40
41/// Generate DTO, descriptor, handler, dispatcher, and caller compilation units.
42///
43/// Files are sorted by relative path and contain no timestamps, so two runs for
44/// the same descriptor are byte-identical.
45pub fn generate_service(service: &ServiceDescriptor) -> Result<Vec<JavaFile>, JavaCodegenError> {
46    validate_service(service)?;
47    let package = "org.facet.vox.generated";
48    let service_name = java_type_name(service.service_name);
49    let named = collect_named_types(service);
50    let mut files = vec![JavaFile {
51        relative_path: "PrimitiveAdapters.java".to_string(),
52        source: generate_primitive_adapters(package),
53    }];
54
55    for (name, shape) in named {
56        files.push(JavaFile {
57            relative_path: format!("{name}.java"),
58            source: generate_named_type(package, &name, shape),
59        });
60    }
61    for method in service.methods {
62        let args_name = args_type_name(&service_name, method);
63        files.push(JavaFile {
64            relative_path: format!("{args_name}.java"),
65            source: generate_args_type(package, &args_name, method),
66        });
67        let response_name = response_type_name(&service_name, method);
68        files.push(JavaFile {
69            relative_path: format!("{response_name}.java"),
70            source: generate_response_type(package, &response_name, method)?,
71        });
72    }
73    files.push(JavaFile {
74        relative_path: format!("{service_name}ServiceDescriptor.java"),
75        source: generate_descriptor(package, &service_name, service),
76    });
77    files.push(JavaFile {
78        relative_path: format!("{service_name}Handler.java"),
79        source: generate_handler(package, &service_name, service),
80    });
81    files.push(JavaFile {
82        relative_path: format!("{service_name}Dispatcher.java"),
83        source: generate_dispatcher(package, &service_name, service),
84    });
85    files.push(JavaFile {
86        relative_path: format!("{service_name}Client.java"),
87        source: generate_client(package, &service_name, service),
88    });
89    files.sort_by(|a, b| a.relative_path.cmp(&b.relative_path));
90    Ok(files)
91}
92
93/// Generate the canonical connection-envelope and handshake schema tables.
94///
95/// The emitted bytes and root ids are derived from the Rust `vox-types` shapes;
96/// Java never re-derives protocol schema identity or duplicates variant order.
97pub fn generate_wire_schemas() -> Result<Vec<JavaFile>, phon::derive::DeriveError> {
98    use facet::Facet;
99    use phon_codegen::Module;
100    use phon_codegen::targets::java::render_schema_table;
101    use vox_types as wire;
102
103    let message = Module::from_shapes(&[<wire::Message<'static> as Facet<'static>>::SHAPE])?;
104    let handshake = Module::from_shapes(&[<wire::HandshakeMessage as Facet<'static>>::SHAPE])?;
105    Ok(vec![
106        JavaFile {
107            relative_path: "HandshakeWireSchemas.java".to_string(),
108            source: render_schema_table(
109                &handshake,
110                "org.facet.vox.generated",
111                "HandshakeWireSchemas",
112            ),
113        },
114        JavaFile {
115            relative_path: "MessageWireSchemas.java".to_string(),
116            source: render_schema_table(&message, "org.facet.vox.generated", "MessageWireSchemas"),
117        },
118    ])
119}
120
121/// Render one Rust-authoritative Phon shape as a Java schema table.
122pub fn generate_schema_table(
123    shape: &'static Shape,
124    class_name: &str,
125) -> Result<JavaFile, phon::derive::DeriveError> {
126    let module = phon_codegen::Module::from_shapes(&[shape])?;
127    Ok(JavaFile {
128        relative_path: format!("{class_name}.java"),
129        source: phon_codegen::targets::java::render_schema_table(
130            &module,
131            "org.facet.vox.generated",
132            class_name,
133        ),
134    })
135}
136
137fn header(package: &str) -> String {
138    format!(
139        "// @generated by vox-codegen; DO NOT EDIT.\n\
140         // Regenerate with `cargo xtask codegen --java`.\n\
141         package {package};\n\n"
142    )
143}
144
145fn validate_service(service: &ServiceDescriptor) -> Result<(), JavaCodegenError> {
146    for method in service.methods {
147        if vox_types::shape_contains_channel(method.return_shape) {
148            return Err(error(method, "channels may not appear in return types"));
149        }
150        for arg in method.args {
151            match channel_arg_direction(arg) {
152                Some(_) => validate_shape(
153                    method,
154                    arg.channel_element.expect("channel element"),
155                    "channel element",
156                )?,
157                None => validate_shape(method, arg.shape, "argument")?,
158            }
159        }
160        validate_shape(method, method.return_shape, "return type")?;
161    }
162    Ok(())
163}
164
165fn validate_shape(
166    method: &MethodDescriptor,
167    shape: &'static Shape,
168    location: &str,
169) -> Result<(), JavaCodegenError> {
170    if is_bytes(shape) {
171        return Ok(());
172    }
173    if matches!(shape.def, facet_core::Def::DynamicValue(_)) {
174        return Err(error(method, &format!("dynamic Facet value in {location}")));
175    }
176    #[cfg(unix)]
177    if vox_types::is_fd(shape) {
178        return Err(error(
179            method,
180            &format!("file descriptor capability in {location}"),
181        ));
182    }
183    match classify_shape(shape) {
184        ShapeKind::Opaque => Err(error(
185            method,
186            &format!(
187                "opaque or external shape `{}` in {location}",
188                shape.type_identifier
189            ),
190        )),
191        ShapeKind::Tx { .. } | ShapeKind::Rx { .. } => {
192            Err(error(method, &format!("channel in {location}")))
193        }
194        ShapeKind::List { element } | ShapeKind::Slice { element } => {
195            validate_shape(method, element, location)
196        }
197        ShapeKind::Set { .. }
198        | ShapeKind::Array { .. }
199        | ShapeKind::Option { .. }
200        | ShapeKind::Map { .. } => Err(error(
201            method,
202            &format!(
203                "container shape `{}` in {location} is not implemented yet",
204                shape.type_identifier
205            ),
206        )),
207        ShapeKind::Pointer { pointee } => validate_shape(method, pointee, location),
208        ShapeKind::Result {
209            ok: key,
210            err: value,
211        } => {
212            validate_shape(method, key, location)?;
213            validate_shape(method, value, location)
214        }
215        ShapeKind::Tuple { elements } => {
216            for element in elements {
217                validate_shape(method, element.shape, location)?;
218            }
219            Ok(())
220        }
221        ShapeKind::TupleStruct { fields } | ShapeKind::Struct(StructInfo { fields, .. }) => {
222            for field in fields {
223                validate_shape(method, field.shape(), location)?;
224            }
225            Ok(())
226        }
227        ShapeKind::Enum(EnumInfo { variants, .. }) => {
228            for variant in variants {
229                match classify_variant(variant) {
230                    VariantKind::Unit => {}
231                    VariantKind::Newtype { .. }
232                    | VariantKind::Tuple { .. }
233                    | VariantKind::Struct { .. } => {
234                        return Err(error(
235                            method,
236                            &format!(
237                                "payload enum `{}` in {location} is not implemented yet",
238                                shape.type_identifier
239                            ),
240                        ));
241                    }
242                }
243            }
244            Ok(())
245        }
246        ShapeKind::Scalar(
247            ScalarType::Bool
248            | ScalarType::U8
249            | ScalarType::U16
250            | ScalarType::U32
251            | ScalarType::I8
252            | ScalarType::I16
253            | ScalarType::I32
254            | ScalarType::I64
255            | ScalarType::F32
256            | ScalarType::F64
257            | ScalarType::Str
258            | ScalarType::String
259            | ScalarType::CowStr
260            | ScalarType::Unit,
261        ) => Ok(()),
262        ShapeKind::Scalar(_) => Err(error(
263            method,
264            &format!(
265                "scalar shape `{}` in {location} is not implemented yet",
266                shape.type_identifier
267            ),
268        )),
269    }
270}
271
272fn error(method: &MethodDescriptor, detail: &str) -> JavaCodegenError {
273    JavaCodegenError {
274        service: method.service_name.to_string(),
275        method: method.method_name.to_string(),
276        detail: detail.to_string(),
277    }
278}
279
280#[derive(Clone, Copy)]
281enum JavaChannelDirection {
282    Tx,
283    Rx,
284}
285
286fn channel_arg_direction(arg: &vox_types::ArgDescriptor) -> Option<JavaChannelDirection> {
287    arg.channel_element?;
288    match classify_shape(arg.shape) {
289        ShapeKind::Tx { .. } => Some(JavaChannelDirection::Tx),
290        ShapeKind::Rx { .. } => Some(JavaChannelDirection::Rx),
291        _ => match arg.shape.type_identifier.rsplit("::").next() {
292            Some("Tx") => Some(JavaChannelDirection::Tx),
293            Some("Rx") => Some(JavaChannelDirection::Rx),
294            _ => None,
295        },
296    }
297}
298
299fn java_arg_type(arg: &vox_types::ArgDescriptor) -> String {
300    match channel_arg_direction(arg) {
301        Some(JavaChannelDirection::Tx) => format!(
302            "VoxTx<{}>",
303            boxed_java_type(arg.channel_element.expect("channel element"))
304        ),
305        Some(JavaChannelDirection::Rx) => format!(
306            "VoxRx<{}>",
307            boxed_java_type(arg.channel_element.expect("channel element"))
308        ),
309        None => java_type(arg.shape, false),
310    }
311}
312
313fn collect_named_types(service: &ServiceDescriptor) -> Vec<(String, &'static Shape)> {
314    let mut seen = HashSet::new();
315    let mut result = Vec::new();
316    for method in service.methods {
317        visit_named(method.args_shape, &mut seen, &mut result);
318        visit_named(method.return_shape, &mut seen, &mut result);
319        for arg in method.args {
320            if let Some(element) = arg.channel_element {
321                visit_named(element, &mut seen, &mut result);
322            }
323        }
324    }
325    result.sort_by(|a, b| a.0.cmp(&b.0));
326    result
327}
328
329fn visit_named(
330    shape: &'static Shape,
331    seen: &mut HashSet<String>,
332    result: &mut Vec<(String, &'static Shape)>,
333) {
334    match classify_shape(shape) {
335        ShapeKind::Struct(StructInfo {
336            name: Some(name),
337            fields,
338            ..
339        }) => {
340            for field in fields {
341                visit_named(field.shape(), seen, result);
342            }
343            if seen.insert(name.to_string()) {
344                result.push((java_type_name(name), shape));
345            }
346        }
347        ShapeKind::Enum(EnumInfo {
348            name: Some(name),
349            variants,
350        }) => {
351            for variant in variants {
352                match classify_variant(variant) {
353                    VariantKind::Unit => {}
354                    VariantKind::Newtype { inner } => visit_named(inner, seen, result),
355                    VariantKind::Tuple { fields } | VariantKind::Struct { fields } => {
356                        for field in fields {
357                            visit_named(field.shape(), seen, result);
358                        }
359                    }
360                }
361            }
362            if seen.insert(name.to_string()) {
363                result.push((java_type_name(name), shape));
364            }
365        }
366        ShapeKind::List { element }
367        | ShapeKind::Slice { element }
368        | ShapeKind::Set { element }
369        | ShapeKind::Array { element, .. }
370        | ShapeKind::Option { inner: element }
371        | ShapeKind::Pointer { pointee: element } => visit_named(element, seen, result),
372        ShapeKind::Map { key, value }
373        | ShapeKind::Result {
374            ok: key,
375            err: value,
376        } => {
377            visit_named(key, seen, result);
378            visit_named(value, seen, result);
379        }
380        ShapeKind::Tuple { elements } => {
381            for element in elements {
382                visit_named(element.shape, seen, result);
383            }
384        }
385        ShapeKind::TupleStruct { fields } => {
386            for field in fields {
387                visit_named(field.shape(), seen, result);
388            }
389        }
390        ShapeKind::Struct(StructInfo {
391            name: None, fields, ..
392        }) => {
393            for field in fields {
394                visit_named(field.shape(), seen, result);
395            }
396        }
397        ShapeKind::Enum(EnumInfo {
398            name: None,
399            variants,
400        }) => {
401            for variant in variants {
402                match classify_variant(variant) {
403                    VariantKind::Unit => {}
404                    VariantKind::Newtype { inner } => visit_named(inner, seen, result),
405                    VariantKind::Tuple { fields } | VariantKind::Struct { fields } => {
406                        for field in fields {
407                            visit_named(field.shape(), seen, result);
408                        }
409                    }
410                }
411            }
412        }
413        ShapeKind::Tx { inner } | ShapeKind::Rx { inner } => visit_named(inner, seen, result),
414        ShapeKind::Scalar(_) | ShapeKind::Opaque => {}
415    }
416}
417
418fn generate_named_type(package: &str, name: &str, shape: &'static Shape) -> String {
419    match classify_shape(shape) {
420        ShapeKind::Struct(StructInfo { fields, .. }) => {
421            let mut out = header(package);
422            out.push_str("import java.util.List;\nimport java.util.Objects;\n");
423            out.push_str("import org.facet.phon.*;\n\n");
424            let _ = writeln!(out, "public final class {name} {{");
425            for field in fields {
426                let _ = writeln!(
427                    out,
428                    "  private final {} {};",
429                    java_type(field.shape(), false),
430                    java_ident(field.name)
431                );
432            }
433            out.push('\n');
434            let args = fields
435                .iter()
436                .map(|f| format!("{} {}", java_type(f.shape(), false), java_ident(f.name)))
437                .collect::<Vec<_>>()
438                .join(", ");
439            let _ = writeln!(out, "  public {name}({args}) {{");
440            for field in fields {
441                let field_name = java_ident(field.name);
442                if is_list(field.shape()) {
443                    let _ = writeln!(
444                        out,
445                        "    this.{field_name} = List.copyOf(Objects.requireNonNull({field_name}, \"{field_name}\"));"
446                    );
447                } else if is_reference(field.shape()) && !is_option(field.shape()) {
448                    let _ = writeln!(
449                        out,
450                        "    this.{field_name} = Objects.requireNonNull({field_name}, \"{field_name}\");"
451                    );
452                } else {
453                    let _ = writeln!(out, "    this.{field_name} = {field_name};");
454                }
455            }
456            out.push_str("  }\n\n");
457            let required_fields = fields
458                .iter()
459                .filter(|field| field.default.is_none())
460                .collect::<Vec<_>>();
461            if required_fields.len() != fields.len() {
462                let args = required_fields
463                    .iter()
464                    .map(|field| {
465                        format!(
466                            "{} {}",
467                            java_type(field.shape(), false),
468                            java_ident(field.name)
469                        )
470                    })
471                    .collect::<Vec<_>>()
472                    .join(", ");
473                let values = fields
474                    .iter()
475                    .map(|field| {
476                        if field.default.is_some() {
477                            java_default_expression(field.shape())
478                        } else {
479                            java_ident(field.name).to_string()
480                        }
481                    })
482                    .collect::<Vec<_>>()
483                    .join(", ");
484                let _ = writeln!(
485                    out,
486                    "  /** Compatibility constructor omitting fields with a Rust default. */\n  public {name}({args}) {{ this({values}); }}\n\n"
487                );
488            }
489            for field in fields {
490                let field_name = java_ident(field.name);
491                let _ = writeln!(
492                    out,
493                    "  public {} {}() {{ return {}; }}",
494                    java_type(field.shape(), false),
495                    field_name,
496                    field_name
497                );
498            }
499            out.push('\n');
500            emit_record_schema_and_adapter(&mut out, name, shape, fields);
501            out.push_str("\n  @Override public boolean equals(Object other) {\n");
502            let _ = writeln!(
503                out,
504                "    if (!(other instanceof {name} that)) return false;"
505            );
506            let comparisons = fields
507                .iter()
508                .map(|f| {
509                    let n = java_ident(f.name);
510                    format!("Objects.deepEquals({n}, that.{n})")
511                })
512                .collect::<Vec<_>>()
513                .join(" && ");
514            let _ = writeln!(
515                out,
516                "    return {};",
517                if comparisons.is_empty() {
518                    "true"
519                } else {
520                    &comparisons
521                }
522            );
523            out.push_str("  }\n");
524            let names = fields
525                .iter()
526                .map(|f| java_ident(f.name))
527                .collect::<Vec<_>>()
528                .join(", ");
529            let _ = writeln!(
530                out,
531                "  @Override public int hashCode() {{ return Objects.hash({names}); }}"
532            );
533            let _ = writeln!(
534                out,
535                "  @Override public String toString() {{ return \"{name}\" + java.util.List.of({names}); }}"
536            );
537            out.push_str("}\n");
538            out
539        }
540        ShapeKind::Enum(EnumInfo { variants, .. })
541            if variants
542                .iter()
543                .all(|v| matches!(classify_variant(v), VariantKind::Unit)) =>
544        {
545            let mut out = header(package);
546            out.push_str("import java.util.List;\nimport org.facet.phon.*;\n\n");
547            let constants = variants
548                .iter()
549                .map(|v| java_enum_constant(v.name))
550                .collect::<Vec<_>>()
551                .join(", ");
552            let _ = writeln!(out, "public enum {name} {{ {constants};");
553            let id = schema_id(shape);
554            let variant_exprs = variants
555                .iter()
556                .enumerate()
557                .map(|(index, variant)| {
558                    format!(
559                        "new Schema.Variant(\"{}\", {index}, Schema.Payload.unit())",
560                        variant.name
561                    )
562                })
563                .collect::<Vec<_>>()
564                .join(", ");
565            let _ = writeln!(
566                out,
567                "  public static final Schema SCHEMA = new Schema(SchemaId.fromLong(0x{id:016x}L), List.of(), new Schema.EnumKind(\"{name}\", List.of({variant_exprs})));"
568            );
569            let _ = writeln!(
570                out,
571                "  public static final PhonAdapter<{name}> ADAPTER = new PhonAdapter<>() {{"
572            );
573            out.push_str("    @Override public SchemaClosure schema() { return SchemaClosure.uncheckedOf(SCHEMA); }\n");
574            out.push_str("    @Override public void encode(PhonEncoder encoder, ");
575            let _ = writeln!(
576                out,
577                "{name} value) throws PhonException {{ encoder.writeU32Unaligned(value.ordinal()); }}"
578            );
579            let _ = writeln!(
580                out,
581                "    @Override public {name} decode(PhonDecoder decoder) throws PhonException {{ long value = decoder.readU32Unaligned(); if (value >= values().length) throw new PhonException(PhonException.Kind.MALFORMED, \"invalid {name} discriminant \" + value); return values()[(int) value]; }}"
582            );
583            out.push_str("  };\n");
584            out.push_str("}\n");
585            out
586        }
587        _ => {
588            let mut out = header(package);
589            let _ = writeln!(
590                out,
591                "// Shape `{name}` is referenced through its generated adapter."
592            );
593            let _ = writeln!(out, "public final class {name} {{ private {name}() {{}} }}");
594            out
595        }
596    }
597}
598
599fn generate_args_type(package: &str, name: &str, method: &MethodDescriptor) -> String {
600    let mut out = header(package);
601    out.push_str("import java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport org.facet.phon.*;\nimport org.facet.vox.*;\n\n");
602    let _ = writeln!(out, "public final class {name} {{");
603    for arg in method.args {
604        let _ = writeln!(
605            out,
606            "  private final {} {};",
607            java_arg_type(arg),
608            java_ident(arg.name)
609        );
610    }
611    let args = method
612        .args
613        .iter()
614        .map(|arg| format!("{} {}", java_arg_type(arg), java_ident(arg.name)))
615        .collect::<Vec<_>>()
616        .join(", ");
617    let _ = writeln!(out, "  public {name}({args}) {{");
618    for arg in method.args {
619        let arg_name = java_ident(arg.name);
620        if is_list(arg.shape) {
621            let _ = writeln!(
622                out,
623                "    this.{arg_name} = List.copyOf(Objects.requireNonNull({arg_name}, \"{arg_name}\"));"
624            );
625        } else if is_reference(arg.shape) && !is_option(arg.shape) {
626            let _ = writeln!(
627                out,
628                "    this.{arg_name} = Objects.requireNonNull({arg_name}, \"{arg_name}\");"
629            );
630        } else {
631            let _ = writeln!(out, "    this.{arg_name} = {arg_name};");
632        }
633    }
634    out.push_str("  }\n");
635    for arg in method.args {
636        let arg_name = java_ident(arg.name);
637        let _ = writeln!(
638            out,
639            "  public {} {}() {{ return {}; }}",
640            java_arg_type(arg),
641            arg_name,
642            arg_name
643        );
644    }
645    let id = schema_id(method.args_shape);
646    let refs = method
647        .args
648        .iter()
649        .map(|arg| match channel_arg_direction(arg) {
650            Some(_) => {
651                // Facet opaque fields are a Phon bytes run whose inner payload is
652                // the canonical u32 channel-table index.
653                "Schema.Ref.concrete(Schema.primitive(Schema.Primitive.BYTES).id())".to_string()
654            }
655            None => schema_ref_expr(arg.shape),
656        })
657        .collect::<Vec<_>>()
658        .join(", ");
659    let _ = writeln!(
660        out,
661        "  public static final Schema SCHEMA = new Schema(SchemaId.fromLong(0x{id:016x}L), List.of(), new Schema.TupleKind(List.of({refs})));"
662    );
663    emit_exact_schema_closure(
664        &mut out,
665        method.args_shape,
666        "SCHEMA_CLOSURE",
667        "schemaClosure",
668    );
669    let _ = writeln!(
670        out,
671        "  public static final PhonAdapter<{name}> ADAPTER = new PhonAdapter<>() {{"
672    );
673    let channel_roots = method
674        .args
675        .iter()
676        .enumerate()
677        .filter_map(|(index, arg)| {
678            let direction = match channel_arg_direction(arg) {
679                Some(JavaChannelDirection::Tx) => "tx",
680                Some(JavaChannelDirection::Rx) => "rx",
681                _ => return None,
682            };
683            let element = arg.channel_element.expect("channel element");
684            Some(format!(
685                "Map.entry(\"channel.arg.{index}.{direction}.element\", {}.schema())",
686                adapter_for(element)
687            ))
688        })
689        .collect::<Vec<_>>()
690        .join(", ");
691    let closure = if channel_roots.is_empty() {
692        "SCHEMA_CLOSURE".to_string()
693    } else {
694        format!(
695            "SchemaClosure.uncheckedWithAuxiliaryClosures(SCHEMA_CLOSURE, Map.ofEntries({channel_roots}))"
696        )
697    };
698    let _ = writeln!(
699        out,
700        "    @Override public SchemaClosure schema() {{ return {closure}; }}"
701    );
702    let _ = writeln!(
703        out,
704        "    @Override public void encode(PhonEncoder encoder, {name} value) throws PhonException {{"
705    );
706    let mut channel_index = 0_u32;
707    for arg in method.args {
708        let statement = match channel_arg_direction(arg) {
709            Some(_) => {
710                let current = channel_index;
711                channel_index += 1;
712                format!("encoder.writeBytes(VoxChannelDecoding.encodeIndex({current}L));")
713            }
714            None => encode_statement(arg.shape, &format!("value.{}()", java_ident(arg.name))),
715        };
716        let _ = writeln!(out, "      {}", statement);
717    }
718    out.push_str("    }\n");
719    let decoded = method
720        .args
721        .iter()
722        .map(|arg| match channel_arg_direction(arg) {
723            Some(JavaChannelDirection::Tx) => {
724                "VoxChannelDecoding.tx(decoder.readBytes())".to_string()
725            }
726            Some(JavaChannelDirection::Rx) => {
727                "VoxChannelDecoding.rx(decoder.readBytes())".to_string()
728            }
729            None => decode_expression(arg.shape),
730        })
731        .collect::<Vec<_>>()
732        .join(", ");
733    let _ = writeln!(
734        out,
735        "    @Override public {name} decode(PhonDecoder decoder) throws PhonException {{ return new {name}({decoded}); }}"
736    );
737    out.push_str("  };\n");
738    out.push_str("}\n");
739    out
740}
741
742fn emit_record_schema_and_adapter(
743    out: &mut String,
744    name: &str,
745    shape: &'static Shape,
746    fields: &[facet_core::Field],
747) {
748    let id = schema_id(shape);
749    let field_exprs = fields
750        .iter()
751        .map(|field| {
752            format!(
753                "new Schema.Field(\"{}\", {}, {})",
754                field.name,
755                schema_ref_expr(field.shape()),
756                field.default.is_none()
757            )
758        })
759        .collect::<Vec<_>>()
760        .join(", ");
761    let _ = writeln!(
762        out,
763        "  public static final Schema SCHEMA = new Schema(SchemaId.fromLong(0x{id:016x}L), List.of(), new Schema.RecordKind(\"{name}\", List.of({field_exprs})));"
764    );
765    emit_exact_schema_closure(out, shape, "SCHEMA_CLOSURE", "schemaClosure");
766    let _ = writeln!(
767        out,
768        "  public static final PhonAdapter<{name}> ADAPTER = new PhonAdapter<>() {{"
769    );
770    let _ = writeln!(
771        out,
772        "    @Override public SchemaClosure schema() {{ return SCHEMA_CLOSURE; }}"
773    );
774    let _ = writeln!(
775        out,
776        "    @Override public void encode(PhonEncoder encoder, {name} value) throws PhonException {{"
777    );
778    for field in fields {
779        let _ = writeln!(
780            out,
781            "      {}",
782            encode_statement(
783                field.shape(),
784                &format!("value.{}()", java_ident(field.name))
785            )
786        );
787    }
788    out.push_str("    }\n");
789    let decoded = fields
790        .iter()
791        .map(|field| decode_expression(field.shape()))
792        .collect::<Vec<_>>()
793        .join(", ");
794    let _ = writeln!(
795        out,
796        "    @Override public {name} decode(PhonDecoder decoder) throws PhonException {{ return new {name}({decoded}); }}"
797    );
798    out.push_str("  };\n");
799}
800
801fn schema_id(shape: &'static Shape) -> u64 {
802    vox_phon::schema_id_for_shape(shape)
803        .unwrap_or_else(|error| {
804            panic!(
805                "Java schema generation failed for `{}`: {error}",
806                shape.type_identifier
807            )
808        })
809        .as_u64()
810}
811
812fn emit_exact_schema_closure(
813    out: &mut String,
814    shape: &'static Shape,
815    field_name: &str,
816    factory_name: &str,
817) {
818    let module = phon_codegen::Module::from_shapes(&[shape]).unwrap_or_else(|error| {
819        panic!(
820            "Java schema closure generation failed for `{}`: {error}",
821            shape.type_identifier
822        )
823    });
824    let root = module
825        .roots
826        .first()
827        .expect("one schema closure root")
828        .id
829        .as_u64();
830    let bytes = schema_bundle_to_bytes(&module.schemas).expect("validated Java schema bundle");
831    let _ = writeln!(out, "  private static final SchemaClosure {field_name} = {factory_name}();");
832    let _ = writeln!(out,
833        "  private static SchemaClosure {factory_name}() {{\n    try {{\n      return SchemaClosure.fromSchemaBundleBytes(SchemaId.fromLong(0x{root:016x}L), {factory_name}Bundle(), PhonLimits.defaults());\n    }} catch (PhonException error) {{\n      throw new ExceptionInInitializerError(error);\n    }}\n  }}");
834    out.push_str(&phon_codegen::targets::java::render_byte_factory(&format!("{factory_name}Bundle"), &bytes));
835}
836
837fn exact_schema_closure_expression(shape: &'static Shape) -> String {
838    let module = phon_codegen::Module::from_shapes(&[shape]).unwrap_or_else(|error| {
839        panic!(
840            "Java schema closure generation failed for `{}`: {error}",
841            shape.type_identifier
842        )
843    });
844    let root = module
845        .roots
846        .first()
847        .expect("one schema closure root")
848        .id
849        .as_u64();
850    let bytes = schema_bundle_to_bytes(&module.schemas).expect("validated Java schema bundle");
851    let factory = phon_codegen::targets::java::render_byte_factory("bundle", &bytes);
852    // A nested supplier gives each inline list closure its own bounded methods
853    // and constant pool instead of inflating the service's initializer.
854    format!("PrimitiveAdapters.schemaClosure(0x{root:016x}L, new java.util.function.Supplier<byte[]>() {{ {factory} public byte[] get() {{ return bundle(); }} }}.get())")
855}
856
857fn schema_ref_expr(shape: &'static Shape) -> String {
858    format!(
859        "Schema.Ref.concrete(SchemaId.fromLong(0x{:016x}L))",
860        schema_id(shape)
861    )
862}
863
864fn encode_statement(shape: &'static Shape, value: &str) -> String {
865    if is_bytes(shape) {
866        return format!("encoder.writeBytes({value});");
867    }
868    match classify_shape(shape) {
869        ShapeKind::Scalar(ScalarType::Bool) => format!("encoder.writeBool({value});"),
870        ShapeKind::Scalar(ScalarType::U8) => format!("encoder.writeU8({value});"),
871        ShapeKind::Scalar(ScalarType::U16) => format!("encoder.writeU16({value});"),
872        ShapeKind::Scalar(ScalarType::U32) => format!("encoder.writeU32({value});"),
873        ShapeKind::Scalar(ScalarType::I8) => format!("encoder.writeI8({value});"),
874        ShapeKind::Scalar(ScalarType::I16) => format!("encoder.writeI16({value});"),
875        ShapeKind::Scalar(ScalarType::I32) => format!("encoder.writeI32({value});"),
876        ShapeKind::Scalar(ScalarType::I64) => format!("encoder.writeI64({value});"),
877        ShapeKind::Scalar(ScalarType::F32) => format!("encoder.writeF32({value});"),
878        ShapeKind::Scalar(ScalarType::F64) => format!("encoder.writeF64({value});"),
879        ShapeKind::Scalar(ScalarType::Char) => format!("encoder.writeChar({value});"),
880        ShapeKind::Scalar(ScalarType::Unit) => String::new(),
881        ShapeKind::Scalar(ScalarType::Str | ScalarType::String | ScalarType::CowStr) => {
882            format!("encoder.writeString({value});")
883        }
884        ShapeKind::Struct(StructInfo {
885            name: Some(name), ..
886        })
887        | ShapeKind::Enum(EnumInfo {
888            name: Some(name), ..
889        }) => format!(
890            "encoder.writeAdapted({}.ADAPTER, {value});",
891            java_type_name(name)
892        ),
893        ShapeKind::List { element } | ShapeKind::Slice { element } => format!(
894            "PrimitiveAdapters.writeList(encoder, {value}, {});",
895            adapter_for(element)
896        ),
897        _ => format!(
898            "throw new PhonException(PhonException.Kind.DECODE, \"generated adapter for {} is not implemented\");",
899            shape.type_identifier.replace('"', "\\\"")
900        ),
901    }
902}
903
904fn decode_expression(shape: &'static Shape) -> String {
905    if is_bytes(shape) {
906        return "decoder.readBytes()".into();
907    }
908    match classify_shape(shape) {
909        ShapeKind::Scalar(ScalarType::Bool) => "decoder.readBool()".into(),
910        ShapeKind::Scalar(ScalarType::U8) => "decoder.readU8()".into(),
911        ShapeKind::Scalar(ScalarType::U16) => "decoder.readU16()".into(),
912        ShapeKind::Scalar(ScalarType::U32) => "decoder.readU32()".into(),
913        ShapeKind::Scalar(ScalarType::I8) => "decoder.readI8()".into(),
914        ShapeKind::Scalar(ScalarType::I16) => "decoder.readI16()".into(),
915        ShapeKind::Scalar(ScalarType::I32) => "decoder.readI32()".into(),
916        ShapeKind::Scalar(ScalarType::I64) => "decoder.readI64()".into(),
917        ShapeKind::Scalar(ScalarType::F32) => "decoder.readF32()".into(),
918        ShapeKind::Scalar(ScalarType::F64) => "decoder.readF64()".into(),
919        ShapeKind::Scalar(ScalarType::Char) => "decoder.readChar()".into(),
920        ShapeKind::Scalar(ScalarType::Unit) => "null".into(),
921        ShapeKind::Scalar(ScalarType::Str | ScalarType::String | ScalarType::CowStr) => {
922            "decoder.readString()".into()
923        }
924        ShapeKind::Struct(StructInfo {
925            name: Some(name), ..
926        })
927        | ShapeKind::Enum(EnumInfo {
928            name: Some(name), ..
929        }) => format!("decoder.readAdapted({}.ADAPTER)", java_type_name(name)),
930        ShapeKind::List { element } | ShapeKind::Slice { element } => format!(
931            "PrimitiveAdapters.readList(decoder, {})",
932            adapter_for(element)
933        ),
934        _ => format!(
935            "unsupportedDecode(\"{}\")",
936            shape.type_identifier.replace('"', "\\\"")
937        ),
938    }
939}
940
941fn generate_descriptor(package: &str, name: &str, service: &ServiceDescriptor) -> String {
942    let mut out = header(package);
943    out.push_str("import java.util.List;\nimport org.facet.vox.*;\n\n");
944    let _ = writeln!(out, "public final class {name}ServiceDescriptor {{");
945    for method in service.methods {
946        let upper = java_constant(method.method_name);
947        let args_adapter = format!("{}.ADAPTER", args_type_name(name, method));
948        let (ok, err) = result_parts(method.return_shape);
949        let ret_adapter = adapter_for(ok);
950        let response_adapter = format!("{}.ADAPTER", response_type_name(name, method));
951        let channels = method
952            .args
953            .iter()
954            .enumerate()
955            .filter_map(|(index, arg)| {
956                let direction = match channel_arg_direction(arg) {
957                    Some(JavaChannelDirection::Tx) => "TX",
958                    Some(JavaChannelDirection::Rx) => "RX",
959                    _ => return None,
960                };
961                let element = arg.channel_element.expect("channel element");
962                Some(format!(
963                    "new ChannelDescriptor({index}, ChannelDescriptor.Direction.{direction}, \"channel.arg.{index}.{}.element\", {})",
964                    direction.to_ascii_lowercase(),
965                    adapter_for(element)
966                ))
967            })
968            .collect::<Vec<_>>()
969            .join(", ");
970        let channel_list = format!("List.of({channels})");
971        let _ = if let Some(err) = err {
972            writeln!(
973                out,
974                "  public static final MethodDescriptor {upper} = new MethodDescriptor(0x{:016x}L, \"{}\", {args_adapter}, {ret_adapter}, {}, {response_adapter}, {channel_list});",
975                crate::method_id(method),
976                method.method_name,
977                adapter_for(err)
978            )
979        } else {
980            writeln!(
981                out,
982                "  public static final MethodDescriptor {upper} = new MethodDescriptor(0x{:016x}L, \"{}\", {args_adapter}, {ret_adapter}, null, {response_adapter}, {channel_list});",
983                crate::method_id(method),
984                method.method_name
985            )
986        };
987    }
988    let methods = service
989        .methods
990        .iter()
991        .map(|m| java_constant(m.method_name))
992        .collect::<Vec<_>>()
993        .join(", ");
994    let _ = writeln!(
995        out,
996        "  public static final ServiceDescriptor INSTANCE = new ServiceDescriptor(\"{}\", List.of({methods}));",
997        service.service_name
998    );
999    let _ = writeln!(out, "  private {name}ServiceDescriptor() {{}}");
1000    out.push_str("}\n");
1001    out
1002}
1003
1004fn generate_handler(package: &str, name: &str, service: &ServiceDescriptor) -> String {
1005    let mut out = header(package);
1006    out.push_str("import java.util.concurrent.CompletableFuture;\nimport org.facet.vox.*;\n\n");
1007    let _ = writeln!(out, "public interface {name}Handler {{");
1008    for method in service.methods {
1009        let args = method
1010            .args
1011            .iter()
1012            .map(|a| format!("{} {}", java_arg_type(a), java_ident(a.name)))
1013            .collect::<Vec<_>>()
1014            .join(", ");
1015        let args = if args.is_empty() {
1016            "CallContext context".to_string()
1017        } else {
1018            format!("CallContext context, {args}")
1019        };
1020        let return_ty = handler_return(method.return_shape);
1021        let _ = writeln!(
1022            out,
1023            "  CompletableFuture<{return_ty}> {}({args});",
1024            java_ident(method.method_name)
1025        );
1026    }
1027    out.push_str("}\n");
1028    out
1029}
1030
1031fn generate_response_type(
1032    package: &str,
1033    name: &str,
1034    method: &MethodDescriptor,
1035) -> Result<String, JavaCodegenError> {
1036    let (ok_shape, error_shape) = result_parts(method.return_shape);
1037    let ok_type = boxed_java_type(ok_shape);
1038    let error_type = error_shape.map_or_else(|| "Void".to_string(), boxed_java_type);
1039    let ok_adapter = adapter_for(ok_shape);
1040    let error_adapter = error_shape.map(adapter_for);
1041    let mut out = header(package);
1042    out.push_str("import org.facet.phon.*;\nimport org.facet.vox.VoxResult;\n\n");
1043    let _ = writeln!(out, "public final class {name} {{");
1044    emit_exact_schema_closure(
1045        &mut out,
1046        method.response_wire_shape,
1047        "SCHEMA",
1048        "responseSchema",
1049    );
1050    let _ = writeln!(
1051        out,
1052        "  public static final PhonAdapter<VoxResult<{ok_type}, {error_type}>> ADAPTER = new PhonAdapter<>() {{"
1053    );
1054    out.push_str("    @Override public SchemaClosure schema() { return SCHEMA; }\n");
1055    let _ = writeln!(
1056        out,
1057        "    @Override public void encode(PhonEncoder encoder, VoxResult<{ok_type}, {error_type}> value) throws PhonException {{"
1058    );
1059    out.push_str(
1060        "      if (value.isSuccess()) {\n        encoder.writeU32Unaligned(0);\n        encoder.writeAdapted(",
1061    );
1062    let _ = writeln!(
1063        out,
1064        "{ok_adapter}, value.success());\n        return;\n      }}"
1065    );
1066    out.push_str("      encoder.writeU32Unaligned(1);\n      switch (value.kind()) {\n");
1067    if let Some(ref error_adapter) = error_adapter {
1068        let _ = writeln!(
1069            out,
1070            "        case APPLICATION_ERROR:\n          encoder.writeU32Unaligned(0);\n          encoder.writeAdapted({error_adapter}, value.applicationError());\n          return;"
1071        );
1072    } else {
1073        out.push_str(
1074            "        case APPLICATION_ERROR:\n          throw new PhonException(PhonException.Kind.ENCODE, \"infallible method cannot encode an application error\");\n",
1075        );
1076    }
1077    out.push_str(
1078        "        case UNKNOWN_METHOD: encoder.writeU32Unaligned(1); return;\n\
1079         case INVALID_PAYLOAD: encoder.writeU32Unaligned(2); encoder.writeString(value.detail()); return;\n\
1080         case CANCELLED: encoder.writeU32Unaligned(3); return;\n\
1081         case CONNECTION_CLOSED: encoder.writeU32Unaligned(4); return;\n\
1082         case CONNECTION_SHUTDOWN: encoder.writeU32Unaligned(5); return;\n\
1083         case SEND_FAILED: encoder.writeU32Unaligned(6); return;\n\
1084         case TIMED_OUT: encoder.writeU32Unaligned(7); return;\n\
1085         case INDETERMINATE: encoder.writeU32Unaligned(8); return;\n\
1086         default: throw new PhonException(PhonException.Kind.ENCODE, \"invalid response result kind\");\n\
1087         }\n    }\n",
1088    );
1089    let _ = writeln!(
1090        out,
1091        "    @Override public VoxResult<{ok_type}, {error_type}> decode(PhonDecoder decoder) throws PhonException {{"
1092    );
1093    out.push_str("      long outer = decoder.readU32Unaligned();\n");
1094    if matches!(
1095        classify_shape(ok_shape),
1096        ShapeKind::Scalar(ScalarType::Unit)
1097    ) {
1098        let _ = writeln!(
1099            out,
1100            "      if (outer == 0) {{ decoder.readAdapted({ok_adapter}); return VoxResult.successUnit(); }}"
1101        );
1102    } else {
1103        let _ = writeln!(
1104            out,
1105            "      if (outer == 0) return VoxResult.success(decoder.readAdapted({ok_adapter}));"
1106        );
1107    }
1108    out.push_str(
1109        "      if (outer != 1) throw new PhonException(PhonException.Kind.MALFORMED, \"invalid Result discriminant\");\n      long error = decoder.readU32Unaligned();\n      switch ((int) error) {\n",
1110    );
1111    if let Some(ref error_adapter) = error_adapter {
1112        let _ = writeln!(
1113            out,
1114            "        case 0: return VoxResult.applicationError(decoder.readAdapted({error_adapter}));"
1115        );
1116    } else {
1117        out.push_str(
1118            "        case 0: throw new PhonException(PhonException.Kind.MALFORMED, \"infallible method received User error\");\n",
1119        );
1120    }
1121    out.push_str(
1122        "        case 1: return VoxResult.infrastructure(VoxResult.Kind.UNKNOWN_METHOD);\n\
1123         case 2: return VoxResult.infrastructure(VoxResult.Kind.INVALID_PAYLOAD, decoder.readString());\n\
1124         case 3: return VoxResult.infrastructure(VoxResult.Kind.CANCELLED);\n\
1125         case 4: return VoxResult.infrastructure(VoxResult.Kind.CONNECTION_CLOSED);\n\
1126         case 5: return VoxResult.infrastructure(VoxResult.Kind.CONNECTION_SHUTDOWN);\n\
1127         case 6: return VoxResult.infrastructure(VoxResult.Kind.SEND_FAILED);\n\
1128         case 7: return VoxResult.infrastructure(VoxResult.Kind.TIMED_OUT);\n\
1129         case 8: return VoxResult.infrastructure(VoxResult.Kind.INDETERMINATE);\n\
1130         default: throw new PhonException(PhonException.Kind.MALFORMED, \"invalid VoxError discriminant\");\n\
1131         }\n    }\n  };\n",
1132    );
1133    let _ = writeln!(out, "  private {name}() {{}}");
1134    out.push_str("}\n");
1135    Ok(out)
1136}
1137
1138fn generate_dispatcher(package: &str, name: &str, service: &ServiceDescriptor) -> String {
1139    let mut out = header(package);
1140    out.push_str("import java.util.List;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport org.facet.phon.*;\nimport org.facet.vox.*;\n\n");
1141    let _ = writeln!(
1142        out,
1143        "public final class {name}Dispatcher implements ServiceDispatcher {{"
1144    );
1145    let _ = writeln!(out, "  private final {name}Handler handler;");
1146    let _ = writeln!(
1147        out,
1148        "  public {name}Dispatcher({name}Handler handler) {{ this.handler = java.util.Objects.requireNonNull(handler); }}"
1149    );
1150    let _ = writeln!(
1151        out,
1152        "  @Override public ServiceDescriptor descriptor() {{ return {name}ServiceDescriptor.INSTANCE; }}"
1153    );
1154    out.push_str(
1155        "  @Override public CompletableFuture<Void> dispatch(InboundCall call) {\n    try {\n",
1156    );
1157    for method in service.methods {
1158        let constant = java_constant(method.method_name);
1159        let tuple_ty = args_type_name(name, method);
1160        let invoke_args = method
1161            .args
1162            .iter()
1163            .map(|arg| format!("args.{}()", java_ident(arg.name)))
1164            .collect::<Vec<_>>()
1165            .join(", ");
1166        let invoke_args = if invoke_args.is_empty() {
1167            "call.context()".to_string()
1168        } else {
1169            format!("call.context(), {invoke_args}")
1170        };
1171        let (_, error_shape) = result_parts(method.return_shape);
1172        let response_ty = response_type_name(name, method);
1173        let _ = writeln!(
1174            out,
1175            "      if (call.method().id() == {name}ServiceDescriptor.{constant}.id()) {{"
1176        );
1177        let _ = writeln!(
1178            out,
1179            "        {tuple_ty} args = call.decodeArguments({tuple_ty}.ADAPTER);"
1180        );
1181        if error_shape.is_some() {
1182            let _ = writeln!(
1183                out,
1184                "        return handler.{}({invoke_args}).thenAccept(value -> {{\n          try {{\n            call.respond(PhonCodec.encode({response_ty}.ADAPTER, value, PhonLimits.defaults()));\n          }} catch (PhonException error) {{\n            throw new CompletionException(error);\n          }}\n        }});",
1185                java_ident(method.method_name)
1186            );
1187        } else {
1188            let success = if matches!(
1189                classify_shape(method.return_shape),
1190                ShapeKind::Scalar(ScalarType::Unit)
1191            ) {
1192                "VoxResult.successUnit()"
1193            } else {
1194                "VoxResult.success(value)"
1195            };
1196            let _ = writeln!(
1197                out,
1198                "        return handler.{}({invoke_args}).thenAccept(value -> {{\n          try {{\n            call.respond(PhonCodec.encode({response_ty}.ADAPTER, {success}, PhonLimits.defaults()));\n          }} catch (PhonException error) {{\n            throw new CompletionException(error);\n          }}\n        }});",
1199                java_ident(method.method_name),
1200            );
1201        }
1202        out.push_str("      }\n");
1203    }
1204    out.push_str("      return CompletableFuture.failedFuture(new VoxException(\"unknown method \" + call.method().id()));\n    } catch (Exception error) {\n      return CompletableFuture.failedFuture(error);\n    }\n  }\n}\n");
1205    out
1206}
1207
1208fn generate_client(package: &str, name: &str, service: &ServiceDescriptor) -> String {
1209    let mut out = header(package);
1210    out.push_str("import java.util.List;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport org.facet.phon.*;\nimport org.facet.vox.*;\n\n");
1211    let _ = writeln!(out, "public final class {name}Client {{");
1212    out.push_str("  private final ServiceLane lane;\n");
1213    let _ = writeln!(
1214        out,
1215        "  public {name}Client(ServiceLane lane) {{ this.lane = java.util.Objects.requireNonNull(lane); }}"
1216    );
1217    for method in service.methods {
1218        let args = method
1219            .args
1220            .iter()
1221            .map(|a| format!("{} {}", java_arg_type(a), java_ident(a.name)))
1222            .collect::<Vec<_>>()
1223            .join(", ");
1224        let return_ty = handler_return(method.return_shape);
1225        let method_name = java_ident(method.method_name);
1226        let constant = java_constant(method.method_name);
1227        let tuple_ty = args_type_name(name, method);
1228        let (_, error_shape) = result_parts(method.return_shape);
1229        let response_ty = response_type_name(name, method);
1230        let wire_result_ty = wire_result_type(method.return_shape);
1231        let channel_arguments = method
1232            .args
1233            .iter()
1234            .enumerate()
1235            .filter_map(|(argument_index, arg)| {
1236                let (factory, argument_name) = match channel_arg_direction(arg) {
1237                    Some(JavaChannelDirection::Tx) => ("tx", java_ident(arg.name)),
1238                    Some(JavaChannelDirection::Rx) => ("rx", java_ident(arg.name)),
1239                    _ => return None,
1240                };
1241                let channel_index = method.args[..argument_index]
1242                    .iter()
1243                    .filter(|previous| channel_arg_direction(previous).is_some())
1244                    .count();
1245                Some(format!(
1246                    "VoxChannelArgument.{factory}({name}ServiceDescriptor.{constant}.channels().get({channel_index}), {argument_name})"
1247                ))
1248            })
1249            .collect::<Vec<_>>()
1250            .join(", ");
1251        let channel_list = format!("List.of({channel_arguments})");
1252        let _ = writeln!(
1253            out,
1254            "  public CompletableFuture<{return_ty}> {method_name}({args}) {{ return {method_name}({}{sep}CallOptions.defaults()); }}",
1255            args_names(method),
1256            sep = if args.is_empty() { "" } else { ", " }
1257        );
1258        let with_opts = if args.is_empty() {
1259            "CallOptions options".to_string()
1260        } else {
1261            format!("{args}, CallOptions options")
1262        };
1263        let _ = writeln!(
1264            out,
1265            "  public CompletableFuture<{return_ty}> {method_name}({with_opts}) {{"
1266        );
1267        let _ = writeln!(
1268            out,
1269            "    try {{\n      byte[] encoded = PhonCodec.encode({tuple_ty}.ADAPTER, new {}({}), PhonLimits.defaults());",
1270            tuple_ty,
1271            args_names(method)
1272        );
1273        if error_shape.is_some() {
1274            let _ = writeln!(
1275                out,
1276                "      return VoxFutures.mapCancellable(lane.call({name}ServiceDescriptor.{constant}, encoded, options, {channel_list}), bytes -> {{\n        try {{\n          {wire_result_ty} result = PhonCodec.decode({response_ty}.ADAPTER, bytes, PhonLimits.defaults());\n          if (result.isInfrastructureError()) throw remoteFailure(result);\n          return result;\n        }} catch (PhonException error) {{\n          throw new CompletionException(error);\n        }}\n      }});\n    }} catch (PhonException error) {{\n      return CompletableFuture.failedFuture(error);\n    }}"
1277            );
1278        } else {
1279            let _ = writeln!(
1280                out,
1281                "      return VoxFutures.mapCancellable(lane.call({name}ServiceDescriptor.{constant}, encoded, options, {channel_list}), bytes -> {{\n        try {{\n          {wire_result_ty} result = PhonCodec.decode({response_ty}.ADAPTER, bytes, PhonLimits.defaults());\n          if (!result.isSuccess()) throw remoteFailure(result);\n          return result.success();\n        }} catch (PhonException error) {{\n          throw new CompletionException(error);\n        }}\n      }});\n    }} catch (PhonException error) {{\n      return CompletableFuture.failedFuture(error);\n    }}"
1282            );
1283        }
1284        out.push_str("  }\n");
1285    }
1286    out.push_str(
1287        "  private static CompletionException remoteFailure(VoxResult<?, ?> result) {\n\
1288         String detail = result.detail() == null ? \"\" : \": \" + result.detail();\n\
1289         return new CompletionException(new VoxException(\"remote Vox error \" + result.kind() + detail));\n\
1290         }\n}\n",
1291    );
1292    out
1293}
1294
1295fn args_names(method: &MethodDescriptor) -> String {
1296    method
1297        .args
1298        .iter()
1299        .map(|a| java_ident(a.name))
1300        .collect::<Vec<_>>()
1301        .join(", ")
1302}
1303
1304fn args_type_name(service_name: &str, method: &MethodDescriptor) -> String {
1305    format!("{service_name}{}Args", upper_camel(method.method_name))
1306}
1307
1308fn response_type_name(service_name: &str, method: &MethodDescriptor) -> String {
1309    format!("{service_name}{}Response", upper_camel(method.method_name))
1310}
1311
1312fn result_parts(shape: &'static Shape) -> (&'static Shape, Option<&'static Shape>) {
1313    match classify_shape(shape) {
1314        ShapeKind::Result { ok, err } => (ok, Some(err)),
1315        _ => (shape, None),
1316    }
1317}
1318
1319fn handler_return(shape: &'static Shape) -> String {
1320    let (ok, err) = result_parts(shape);
1321    match err {
1322        Some(err) => format!(
1323            "VoxResult<{}, {}>",
1324            boxed_java_type(ok),
1325            boxed_java_type(err)
1326        ),
1327        None => boxed_java_type(ok),
1328    }
1329}
1330
1331fn wire_result_type(shape: &'static Shape) -> String {
1332    let (ok, err) = result_parts(shape);
1333    format!(
1334        "VoxResult<{}, {}>",
1335        boxed_java_type(ok),
1336        err.map_or_else(|| "Void".to_string(), boxed_java_type)
1337    )
1338}
1339
1340fn adapter_for(shape: &'static Shape) -> String {
1341    if is_bytes(shape) {
1342        return "PrimitiveAdapters.BYTES".to_string();
1343    }
1344    match classify_shape(shape) {
1345        ShapeKind::Struct(StructInfo {
1346            name: Some(name), ..
1347        })
1348        | ShapeKind::Enum(EnumInfo {
1349            name: Some(name), ..
1350        }) => {
1351            format!("{}.ADAPTER", java_type_name(name))
1352        }
1353        ShapeKind::Scalar(scalar) => {
1354            format!("PrimitiveAdapters.{}", primitive_adapter_name(scalar))
1355        }
1356        ShapeKind::List { element } | ShapeKind::Slice { element } => format!(
1357            "PrimitiveAdapters.list({}, {})",
1358            adapter_for(element),
1359            exact_schema_closure_expression(shape)
1360        ),
1361        _ => format!(
1362            "PrimitiveAdapters.unsupported(\"{}\")",
1363            shape.type_identifier
1364        ),
1365    }
1366}
1367
1368fn generate_primitive_adapters(package: &str) -> String {
1369    let mut out = header(package);
1370    out.push_str(
1371        "import java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\nimport org.facet.phon.*;\n\n",
1372    );
1373    out.push_str("final class PrimitiveAdapters {\n");
1374    let adapters = [
1375        (
1376            "STRING",
1377            "String",
1378            "STRING",
1379            "encoder.writeString(value);",
1380            "decoder.readString()",
1381        ),
1382        (
1383            "BOOL",
1384            "Boolean",
1385            "BOOL",
1386            "encoder.writeBool(value);",
1387            "decoder.readBool()",
1388        ),
1389        (
1390            "I32",
1391            "Integer",
1392            "I32",
1393            "encoder.writeI32(value);",
1394            "decoder.readI32()",
1395        ),
1396        (
1397            "I64",
1398            "Long",
1399            "I64",
1400            "encoder.writeI64(value);",
1401            "decoder.readI64()",
1402        ),
1403        (
1404            "U8",
1405            "Integer",
1406            "U8",
1407            "encoder.writeU8(value);",
1408            "decoder.readU8()",
1409        ),
1410        (
1411            "U16",
1412            "Integer",
1413            "U16",
1414            "encoder.writeU16(value);",
1415            "decoder.readU16()",
1416        ),
1417        (
1418            "U32",
1419            "Long",
1420            "U32",
1421            "encoder.writeU32(value);",
1422            "decoder.readU32()",
1423        ),
1424        (
1425            "F32",
1426            "Float",
1427            "F32",
1428            "encoder.writeF32(value);",
1429            "decoder.readF32()",
1430        ),
1431        (
1432            "F64",
1433            "Double",
1434            "F64",
1435            "encoder.writeF64(value);",
1436            "decoder.readF64()",
1437        ),
1438        (
1439            "BYTES",
1440            "byte[]",
1441            "BYTES",
1442            "encoder.writeBytes(value);",
1443            "decoder.readBytes()",
1444        ),
1445    ];
1446    for (field, ty, primitive, encode, decode) in adapters {
1447        let _ = writeln!(
1448            out,
1449            "  static final PhonAdapter<{ty}> {field} = new PhonAdapter<>() {{"
1450        );
1451        let _ = writeln!(
1452            out,
1453            "    @Override public SchemaClosure schema() {{ return SchemaClosure.uncheckedOf(Schema.primitive(Schema.Primitive.{primitive})); }}"
1454        );
1455        let _ = writeln!(
1456            out,
1457            "    @Override public void encode(PhonEncoder encoder, {ty} value) throws PhonException {{ {encode} }}"
1458        );
1459        let _ = writeln!(
1460            out,
1461            "    @Override public {ty} decode(PhonDecoder decoder) throws PhonException {{ return {decode}; }}"
1462        );
1463        out.push_str("  };\n");
1464    }
1465    out.push_str(
1466        "  static final PhonAdapter<Void> UNIT = new PhonAdapter<>() {\n    private final SchemaClosure schema = SchemaClosure.uncheckedOf(Schema.primitive(Schema.Primitive.UNIT));\n    @Override public SchemaClosure schema() { return schema; }\n    @Override public void encode(PhonEncoder encoder, Void value) throws PhonException {}\n    @Override public Void decode(PhonDecoder decoder) throws PhonException { return null; }\n  };\n",
1467    );
1468    out.push_str(
1469        "  static <T> void writeList(PhonEncoder encoder, List<T> values, PhonAdapter<T> elementAdapter) throws PhonException {\n    Objects.requireNonNull(values, \"values\");\n    Objects.requireNonNull(elementAdapter, \"elementAdapter\");\n    encoder.writeCount(values.size());\n    for (T value : values) encoder.writeAdapted(elementAdapter, Objects.requireNonNull(value, \"list element\"));\n  }\n",
1470    );
1471    out.push_str(
1472        "  static <T> List<T> readList(PhonDecoder decoder, PhonAdapter<T> elementAdapter) throws PhonException {\n    Objects.requireNonNull(elementAdapter, \"elementAdapter\");\n    int count = decoder.readCount();\n    ArrayList<T> values = new ArrayList<>(count);\n    for (int index = 0; index < count; index++) values.add(decoder.readAdapted(elementAdapter));\n    return List.copyOf(values);\n  }\n",
1473    );
1474    out.push_str(
1475        "  static <T> PhonAdapter<List<T>> list(PhonAdapter<T> elementAdapter, SchemaClosure schema) {\n    Objects.requireNonNull(elementAdapter, \"elementAdapter\");\n    Objects.requireNonNull(schema, \"schema\");\n    return new PhonAdapter<>() {\n      @Override public SchemaClosure schema() { return schema; }\n      @Override public void encode(PhonEncoder encoder, List<T> values) throws PhonException { writeList(encoder, values, elementAdapter); }\n      @Override public List<T> decode(PhonDecoder decoder) throws PhonException { return readList(decoder, elementAdapter); }\n    };\n  }\n",
1476    );
1477    out.push_str(
1478        "  static SchemaClosure schemaClosure(long rootId, byte[] schemaBundle) {\n    try {\n      return SchemaClosure.fromSchemaBundleBytes(SchemaId.fromLong(rootId), schemaBundle, PhonLimits.defaults());\n    } catch (PhonException error) {\n      throw new ExceptionInInitializerError(error);\n    }\n  }\n",
1479    );
1480    out.push_str("  static <T> PhonAdapter<T> unsupported(String shape) { throw new IllegalArgumentException(\"unsupported generated adapter \" + shape); }\n");
1481    out.push_str("  private PrimitiveAdapters() {}\n}\n");
1482    out
1483}
1484
1485fn primitive_adapter_name(scalar: ScalarType) -> &'static str {
1486    match scalar {
1487        ScalarType::Bool => "BOOL",
1488        ScalarType::U8 => "U8",
1489        ScalarType::U16 => "U16",
1490        ScalarType::U32 => "U32",
1491        ScalarType::I8 | ScalarType::I16 | ScalarType::I32 => "I32",
1492        ScalarType::I64 => "I64",
1493        ScalarType::F32 => "F32",
1494        ScalarType::F64 => "F64",
1495        ScalarType::Str | ScalarType::String | ScalarType::CowStr | ScalarType::Char => "STRING",
1496        ScalarType::Unit => "UNIT",
1497        _ => "I64",
1498    }
1499}
1500
1501fn java_type(shape: &'static Shape, boxed: bool) -> String {
1502    if is_bytes(shape) {
1503        return "byte[]".to_string();
1504    }
1505    match classify_shape(shape) {
1506        ShapeKind::Scalar(s) => scalar_java(s, boxed),
1507        ShapeKind::Option { inner } => format!("java.util.Optional<{}>", boxed_java_type(inner)),
1508        ShapeKind::List { element }
1509        | ShapeKind::Slice { element }
1510        | ShapeKind::Array { element, .. } => {
1511            format!("java.util.List<{}>", boxed_java_type(element))
1512        }
1513        ShapeKind::Set { element } => format!("java.util.Set<{}>", boxed_java_type(element)),
1514        ShapeKind::Map { key, value } => format!(
1515            "java.util.Map<{}, {}>",
1516            boxed_java_type(key),
1517            boxed_java_type(value)
1518        ),
1519        ShapeKind::Struct(StructInfo {
1520            name: Some(name), ..
1521        })
1522        | ShapeKind::Enum(EnumInfo {
1523            name: Some(name), ..
1524        }) => java_type_name(name),
1525        ShapeKind::Tuple { elements } => format!("Tuple{}", elements.len()),
1526        ShapeKind::TupleStruct { fields } => format!("Tuple{}", fields.len()),
1527        ShapeKind::Result { ok, err } => format!(
1528            "VoxResult<{}, {}>",
1529            boxed_java_type(ok),
1530            boxed_java_type(err)
1531        ),
1532        ShapeKind::Tx { inner } => format!("VoxTx<{}>", boxed_java_type(inner)),
1533        ShapeKind::Rx { inner } => format!("VoxRx<{}>", boxed_java_type(inner)),
1534        ShapeKind::Pointer { pointee } => java_type(pointee, boxed),
1535        _ => "Object".to_string(),
1536    }
1537}
1538
1539fn boxed_java_type(shape: &'static Shape) -> String {
1540    java_type(shape, true)
1541}
1542
1543fn java_default_expression(shape: &'static Shape) -> String {
1544    match classify_shape(shape) {
1545        ShapeKind::Scalar(ScalarType::Bool) => "false".into(),
1546        ShapeKind::Scalar(
1547            ScalarType::U8
1548            | ScalarType::U16
1549            | ScalarType::U32
1550            | ScalarType::I8
1551            | ScalarType::I16
1552            | ScalarType::I32
1553            | ScalarType::I64
1554            | ScalarType::U64
1555            | ScalarType::ISize
1556            | ScalarType::USize,
1557        ) => "0".into(),
1558        ShapeKind::Scalar(ScalarType::F32) => "0.0f".into(),
1559        ShapeKind::Scalar(ScalarType::F64) => "0.0d".into(),
1560        ShapeKind::Scalar(ScalarType::Unit) => "null".into(),
1561        ShapeKind::Scalar(
1562            ScalarType::Char | ScalarType::Str | ScalarType::String | ScalarType::CowStr,
1563        ) => "\"\"".into(),
1564        ShapeKind::Scalar(ScalarType::U128 | ScalarType::I128) => {
1565            "java.math.BigInteger.ZERO".into()
1566        }
1567        ShapeKind::Option { .. } => "java.util.Optional.empty()".into(),
1568        ShapeKind::List { .. } | ShapeKind::Slice { .. } | ShapeKind::Array { .. } => {
1569            "java.util.List.of()".into()
1570        }
1571        ShapeKind::Set { .. } => "java.util.Set.of()".into(),
1572        ShapeKind::Map { .. } => "java.util.Map.of()".into(),
1573        ShapeKind::Struct(StructInfo {
1574            name: Some(name),
1575            fields,
1576            ..
1577        }) => {
1578            let values = fields
1579                .iter()
1580                .map(|field| java_default_expression(field.shape()))
1581                .collect::<Vec<_>>()
1582                .join(", ");
1583            format!("new {}({values})", java_type_name(name))
1584        }
1585        ShapeKind::Enum(EnumInfo {
1586            name: Some(name),
1587            variants,
1588        }) if variants
1589            .iter()
1590            .all(|variant| matches!(classify_variant(variant), VariantKind::Unit)) =>
1591        {
1592            variants.first().map_or_else(
1593                || "null".to_string(),
1594                |variant| {
1595                    format!(
1596                        "{}.{}",
1597                        java_type_name(name),
1598                        java_enum_constant(variant.name)
1599                    )
1600                },
1601            )
1602        }
1603        ShapeKind::Pointer { pointee } => java_default_expression(pointee),
1604        _ => "null".into(),
1605    }
1606}
1607
1608fn scalar_java(scalar: ScalarType, boxed: bool) -> String {
1609    match scalar {
1610        ScalarType::Bool => if boxed { "Boolean" } else { "boolean" }.into(),
1611        ScalarType::U8 | ScalarType::U16 | ScalarType::I8 | ScalarType::I16 | ScalarType::I32 => {
1612            if boxed { "Integer" } else { "int" }.into()
1613        }
1614        ScalarType::U32
1615        | ScalarType::I64
1616        | ScalarType::U64
1617        | ScalarType::ISize
1618        | ScalarType::USize => if boxed { "Long" } else { "long" }.into(),
1619        ScalarType::F32 => if boxed { "Float" } else { "float" }.into(),
1620        ScalarType::F64 => if boxed { "Double" } else { "double" }.into(),
1621        ScalarType::U128 | ScalarType::I128 => "java.math.BigInteger".into(),
1622        ScalarType::Char | ScalarType::Str | ScalarType::String | ScalarType::CowStr => {
1623            "String".into()
1624        }
1625        ScalarType::Unit => "Void".into(),
1626        _ => "Object".into(),
1627    }
1628}
1629
1630fn is_reference(shape: &'static Shape) -> bool {
1631    !matches!(
1632        classify_shape(shape),
1633        ShapeKind::Scalar(
1634            ScalarType::Bool
1635                | ScalarType::U8
1636                | ScalarType::U16
1637                | ScalarType::U32
1638                | ScalarType::U64
1639                | ScalarType::USize
1640                | ScalarType::I8
1641                | ScalarType::I16
1642                | ScalarType::I32
1643                | ScalarType::I64
1644                | ScalarType::ISize
1645                | ScalarType::F32
1646                | ScalarType::F64
1647                | ScalarType::Char
1648        )
1649    )
1650}
1651
1652fn is_option(shape: &'static Shape) -> bool {
1653    matches!(classify_shape(shape), ShapeKind::Option { .. })
1654}
1655
1656fn is_list(shape: &'static Shape) -> bool {
1657    !is_bytes(shape)
1658        && matches!(
1659            classify_shape(shape),
1660            ShapeKind::List { .. } | ShapeKind::Slice { .. }
1661        )
1662}
1663
1664fn java_type_name(name: &str) -> String {
1665    name.rsplit("::")
1666        .next()
1667        .unwrap_or(name)
1668        .chars()
1669        .filter(|c| c.is_ascii_alphanumeric() || *c == '_')
1670        .collect()
1671}
1672
1673fn upper_camel(name: &str) -> String {
1674    name.split('_')
1675        .filter(|part| !part.is_empty())
1676        .map(|part| {
1677            let mut chars = part.chars();
1678            chars
1679                .next()
1680                .map(|c| c.to_ascii_uppercase().to_string() + chars.as_str())
1681                .unwrap_or_default()
1682        })
1683        .collect()
1684}
1685
1686fn java_ident(name: &str) -> String {
1687    let mut out = String::new();
1688    for (i, part) in name.split('_').enumerate() {
1689        if i == 0 {
1690            out.push_str(part);
1691        } else {
1692            let mut chars = part.chars();
1693            if let Some(c) = chars.next() {
1694                out.push(c.to_ascii_uppercase());
1695            }
1696            out.extend(chars);
1697        }
1698    }
1699    match out.as_str() {
1700        "class" | "enum" | "interface" | "return" | "default" | "switch" | "case" | "new" => {
1701            format!("{out}_")
1702        }
1703        _ => out,
1704    }
1705}
1706
1707fn java_constant(name: &str) -> String {
1708    name.chars()
1709        .flat_map(|c| {
1710            if c.is_ascii_uppercase() {
1711                vec!['_', c]
1712            } else {
1713                vec![c.to_ascii_uppercase()]
1714            }
1715        })
1716        .collect::<String>()
1717        .trim_start_matches('_')
1718        .to_string()
1719}
1720
1721fn java_enum_constant(name: &str) -> String {
1722    java_constant(name)
1723}
1724
1725#[cfg(test)]
1726mod tests {
1727    use std::fs;
1728    use std::path::{Path, PathBuf};
1729    use std::process::Command;
1730    use std::time::{SystemTime, UNIX_EPOCH};
1731
1732    use facet::Facet;
1733    use vox_types::{MethodDescriptorOptions, Rx, ServiceDescriptor, method_descriptor};
1734
1735    use super::{generate_service, generate_wire_schemas};
1736
1737    #[derive(Clone, Debug, Facet)]
1738    struct NestedRequest {
1739        message: String,
1740    }
1741
1742    #[derive(Clone, Debug, Facet)]
1743    struct NestedResponse {
1744        echoed: String,
1745    }
1746
1747    #[derive(Clone, Debug, Facet)]
1748    struct DefaultedRequest {
1749        required: String,
1750        #[facet(default)]
1751        optional_count: u32,
1752    }
1753
1754    #[derive(Clone, Debug, PartialEq, Eq, Facet)]
1755    struct ListItem {
1756        ordinal: u32,
1757        label: String,
1758    }
1759
1760    #[derive(Clone, Debug, PartialEq, Eq, Facet)]
1761    struct ListEnvelope {
1762        items: Vec<ListItem>,
1763    }
1764
1765    #[derive(Clone, Debug, Facet)]
1766    #[repr(u8)]
1767    enum DivideByZero {
1768        Zero = 0,
1769    }
1770
1771    #[derive(Clone, Debug, Facet)]
1772    #[repr(u8)]
1773    enum DefaultedMode {
1774        Auto = 0,
1775        Manual = 1,
1776    }
1777
1778    fn fixture_service() -> ServiceDescriptor {
1779        let echo = method_descriptor::<(String,), String>(
1780            "Review",
1781            "echo",
1782            &["value"],
1783            &[None],
1784            MethodDescriptorOptions {
1785                response_wire_shape: <Result<String, vox_types::VoxError> as Facet>::SHAPE,
1786                doc: None,
1787            },
1788        );
1789        let inspect = method_descriptor::<(NestedRequest,), NestedResponse>(
1790            "Review",
1791            "inspect",
1792            &["request"],
1793            &[None],
1794            MethodDescriptorOptions {
1795                response_wire_shape: <Result<NestedResponse, vox_types::VoxError> as Facet>::SHAPE,
1796                doc: None,
1797            },
1798        );
1799        let divide = method_descriptor::<(i64, i64), Result<i64, DivideByZero>>(
1800            "Review",
1801            "divide",
1802            &["dividend", "divisor"],
1803            &[None, None],
1804            MethodDescriptorOptions {
1805                response_wire_shape:
1806                    <Result<i64, vox_types::VoxError<DivideByZero>> as Facet>::SHAPE,
1807                doc: None,
1808            },
1809        );
1810        let methods = Box::leak(vec![echo, inspect, divide].into_boxed_slice());
1811        ServiceDescriptor {
1812            service_name: "Review",
1813            methods,
1814            doc: None,
1815        }
1816    }
1817
1818    fn list_fixture_service() -> ServiceDescriptor {
1819        let inspect = method_descriptor::<(ListEnvelope,), ListEnvelope>(
1820            "ListFixture",
1821            "inspect",
1822            &["request"],
1823            &[None],
1824            MethodDescriptorOptions {
1825                response_wire_shape: <Result<ListEnvelope, vox_types::VoxError> as Facet>::SHAPE,
1826                doc: None,
1827            },
1828        );
1829        let echo_items = method_descriptor::<(Vec<ListItem>,), Vec<ListItem>>(
1830            "ListFixture",
1831            "echo_items",
1832            &["items"],
1833            &[None],
1834            MethodDescriptorOptions {
1835                response_wire_shape: <Result<Vec<ListItem>, vox_types::VoxError> as Facet>::SHAPE,
1836                doc: None,
1837            },
1838        );
1839        let methods = Box::leak(vec![inspect, echo_items].into_boxed_slice());
1840        ServiceDescriptor {
1841            service_name: "ListFixture",
1842            methods,
1843            doc: None,
1844        }
1845    }
1846
1847    #[test]
1848    fn java_generation_is_deterministic_and_emits_frozen_surfaces() {
1849        let service = fixture_service();
1850        let first = generate_service(&service).expect("supported service");
1851        let second = generate_service(&service).expect("repeat generation");
1852        assert_eq!(first, second);
1853        let all = first
1854            .iter()
1855            .map(|file| file.source.as_str())
1856            .collect::<String>();
1857        assert!(all.contains("public final class NestedRequest"));
1858        assert!(all.contains("public static final PhonAdapter<NestedRequest> ADAPTER"));
1859        assert!(all.contains("public interface ReviewHandler"));
1860        assert!(all.contains("CompletableFuture<String> echo("));
1861        assert!(all.contains("CompletableFuture<VoxResult<Long, DivideByZero>> divide("));
1862        assert!(all.contains("public final class ReviewDispatcher implements ServiceDispatcher"));
1863        assert!(all.contains("public final class ReviewClient"));
1864        assert!(all.contains("0x"));
1865    }
1866
1867    #[test]
1868    fn java_generation_preserves_defaulted_field_requiredness() {
1869        let inspect = method_descriptor::<(DefaultedRequest,), String>(
1870            "ReviewDefaults",
1871            "inspect",
1872            &["request"],
1873            &[None],
1874            MethodDescriptorOptions {
1875                response_wire_shape: <Result<String, vox_types::VoxError> as Facet>::SHAPE,
1876                doc: None,
1877            },
1878        );
1879        let methods = Box::leak(vec![inspect].into_boxed_slice());
1880        let service = ServiceDescriptor {
1881            service_name: "ReviewDefaults",
1882            methods,
1883            doc: None,
1884        };
1885        let file = generate_service(&service)
1886            .expect("defaulted service should generate")
1887            .into_iter()
1888            .find(|file| file.relative_path.ends_with("DefaultedRequest.java"))
1889            .expect("defaulted request should be generated");
1890
1891        let required = super::schema_ref_expr(<String as Facet>::SHAPE);
1892        let optional = super::schema_ref_expr(<u32 as Facet>::SHAPE);
1893        assert!(
1894            file.source
1895                .contains(&format!("new Schema.Field(\"required\", {required}, true)"))
1896        );
1897        assert!(file.source.contains(&format!(
1898            "new Schema.Field(\"optional_count\", {optional}, false)"
1899        )));
1900    }
1901
1902    #[test]
1903    fn java_default_expression_uses_first_unit_enum_variant() {
1904        assert_eq!(
1905            super::java_default_expression(DefaultedMode::SHAPE),
1906            "DefaultedMode.AUTO"
1907        );
1908    }
1909
1910    #[test]
1911    fn java_generation_emits_bounded_immutable_record_lists() {
1912        let files = generate_service(&list_fixture_service()).expect("record lists are supported");
1913        let source = |name: &str| {
1914            files
1915                .iter()
1916                .find(|file| file.relative_path == name)
1917                .unwrap_or_else(|| panic!("missing generated {name}"))
1918                .source
1919                .as_str()
1920        };
1921
1922        let envelope = source("ListEnvelope.java");
1923        assert!(envelope.contains("private final java.util.List<ListItem> items;"));
1924        assert!(
1925            envelope
1926                .contains("this.items = List.copyOf(Objects.requireNonNull(items, \"items\"));")
1927        );
1928        assert!(
1929            envelope
1930                .contains("PrimitiveAdapters.writeList(encoder, value.items(), ListItem.ADAPTER);")
1931        );
1932        assert!(envelope.contains("PrimitiveAdapters.readList(decoder, ListItem.ADAPTER)"));
1933        assert!(envelope.contains("SchemaClosure.fromSchemaBundleBytes"));
1934
1935        let primitive = source("PrimitiveAdapters.java");
1936        assert!(primitive.contains("encoder.writeCount(values.size())"));
1937        assert!(primitive.contains("int count = decoder.readCount()"));
1938        assert!(primitive.contains("return List.copyOf(values)"));
1939
1940        let direct_args = source("ListFixtureEchoItemsArgs.java");
1941        assert!(
1942            direct_args
1943                .contains("this.items = List.copyOf(Objects.requireNonNull(items, \"items\"));")
1944        );
1945        let descriptor = source("ListFixtureServiceDescriptor.java");
1946        assert!(descriptor.contains("PrimitiveAdapters.list(ListItem.ADAPTER"));
1947        assert!(descriptor.contains("PrimitiveAdapters.schemaClosure(0x"));
1948        assert!(descriptor.contains("HexFormat.of().parseHex"));
1949    }
1950
1951    #[test]
1952    fn java_wire_schemas_are_derived_deterministically_from_rust_shapes() {
1953        let first = generate_wire_schemas().expect("wire schemas");
1954        let second = generate_wire_schemas().expect("repeat wire schemas");
1955        assert_eq!(first, second);
1956        assert_eq!(
1957            first
1958                .iter()
1959                .map(|file| file.relative_path.as_str())
1960                .collect::<Vec<_>>(),
1961            ["HandshakeWireSchemas.java", "MessageWireSchemas.java"]
1962        );
1963        assert!(
1964            first
1965                .iter()
1966                .all(|file| file.source.contains("SCHEMA_BUNDLE"))
1967        );
1968        assert!(first[0].source.contains("HANDSHAKE_MESSAGE_SCHEMA_ID"));
1969        assert!(first[1].source.contains("MESSAGE_SCHEMA_ID"));
1970    }
1971
1972    #[test]
1973    fn java_generation_emits_typed_channel_bindings() {
1974        let stream = method_descriptor::<(Rx<String>,), String>(
1975            "StreamingService",
1976            "stream",
1977            &["input"],
1978            &[Some(<String as Facet>::SHAPE)],
1979            MethodDescriptorOptions {
1980                response_wire_shape: <Result<String, vox_types::VoxError> as Facet>::SHAPE,
1981                doc: None,
1982            },
1983        );
1984        let methods = Box::leak(vec![stream].into_boxed_slice());
1985        let service = ServiceDescriptor {
1986            service_name: "StreamingService",
1987            methods,
1988            doc: None,
1989        };
1990        let files = generate_service(&service).expect("channels are supported");
1991        let joined = files
1992            .iter()
1993            .map(|file| file.source.as_str())
1994            .collect::<String>();
1995        assert!(joined.contains("VoxRx<String> input"));
1996        assert!(joined.contains("channel.arg.0.rx.element"));
1997        assert!(joined.contains("VoxChannelArgument.rx"));
1998    }
1999
2000    #[test]
2001    fn generated_fixture_compiles_with_javac_release_17() {
2002        let dir = unique_temp_dir();
2003        let generated = dir.join("org/facet/vox/generated");
2004        fs::create_dir_all(&generated).expect("create generated package");
2005        let mut files = generate_service(&fixture_service()).expect("generate fixture");
2006        files.extend(generate_service(&list_fixture_service()).expect("generate list fixture"));
2007        files.extend(
2008            generate_service(spec_proto::terminal::terminal_service_descriptor())
2009                .expect("generate terminal fixture"),
2010        );
2011        for file in files {
2012            fs::write(generated.join(file.relative_path), file.source).expect("write Java file");
2013        }
2014        write_compile_stubs(&dir);
2015
2016        let sources = collect_java_files(&dir);
2017        let classes = dir.join("classes");
2018        fs::create_dir_all(&classes).expect("create classes");
2019        let output = Command::new("javac")
2020            .arg("--release")
2021            .arg("17")
2022            .arg("-d")
2023            .arg(&classes)
2024            .args(&sources)
2025            .output()
2026            .expect("JDK 17+ javac must be available for the Java generator gate");
2027        let compiler_close_only = output.status.code() == Some(3)
2028            && sources.iter().all(|source| {
2029                source
2030                    .strip_prefix(&dir)
2031                    .ok()
2032                    .map(|relative| classes.join(relative).with_extension("class").is_file())
2033                    .unwrap_or(false)
2034            });
2035        if !output.status.success() && !compiler_close_only {
2036            panic!(
2037                "javac --release 17 failed:\nstdout:\n{}\nstderr:\n{}",
2038                String::from_utf8_lossy(&output.stdout),
2039                String::from_utf8_lossy(&output.stderr)
2040            );
2041        }
2042        if compiler_close_only {
2043            eprintln!(
2044                "warning: javac returned 3 after emitting every top-level class; treating the compiler resource-close failure as non-fatal"
2045            );
2046        }
2047        fs::remove_dir_all(&dir).expect("remove Java compile fixture");
2048    }
2049
2050    #[test]
2051    fn record_list_wire_roundtrips_between_rust_and_generated_java() {
2052        let dir = unique_temp_dir();
2053        let generated = dir.join("org/facet/vox/generated");
2054        fs::create_dir_all(&generated).expect("create generated package");
2055        for file in generate_service(&list_fixture_service()).expect("generate list fixture") {
2056            if matches!(
2057                file.relative_path.as_str(),
2058                "PrimitiveAdapters.java" | "ListItem.java" | "ListEnvelope.java"
2059            ) {
2060                fs::write(generated.join(file.relative_path), file.source)
2061                    .expect("write generated list source");
2062            }
2063        }
2064        fs::write(
2065            generated.join("ListRoundtrip.java"),
2066            r#"package org.facet.vox.generated;
2067
2068import java.nio.file.Files;
2069import java.nio.file.Path;
2070import java.util.ArrayList;
2071import java.util.Arrays;
2072import java.util.List;
2073import org.facet.phon.PhonCodec;
2074import org.facet.phon.PhonLimits;
2075
2076public final class ListRoundtrip {
2077  public static void main(String[] args) throws Exception {
2078    byte[] rustBytes = Files.readAllBytes(Path.of(args[0]));
2079    ListEnvelope decoded = PhonCodec.decode(ListEnvelope.ADAPTER, rustBytes, PhonLimits.defaults());
2080    check(decoded.items().equals(List.of(
2081        new ListItem(7L, "alpha"),
2082        new ListItem(11L, "beta"))), "Rust payload decoded in Java");
2083
2084    boolean decodedImmutable = false;
2085    try {
2086      decoded.items().add(new ListItem(13L, "forbidden"));
2087    } catch (UnsupportedOperationException expected) {
2088      decodedImmutable = true;
2089    }
2090    check(decodedImmutable, "decoded list is immutable");
2091
2092    ArrayList<ListItem> mutable = new ArrayList<>(decoded.items());
2093    ListEnvelope copied = new ListEnvelope(mutable);
2094    mutable.clear();
2095    check(copied.items().size() == 2, "constructor makes a defensive immutable copy");
2096
2097    byte[] javaBytes = PhonCodec.encode(ListEnvelope.ADAPTER, copied, PhonLimits.defaults());
2098    check(Arrays.equals(rustBytes, javaBytes), "Java encoding matches Rust encoding");
2099    Files.write(Path.of(args[1]), javaBytes);
2100  }
2101
2102  private static void check(boolean condition, String message) {
2103    if (!condition) throw new AssertionError(message);
2104  }
2105}
2106"#,
2107        )
2108        .expect("write Java roundtrip harness");
2109
2110        let value = ListEnvelope {
2111            items: vec![
2112                ListItem {
2113                    ordinal: 7,
2114                    label: "alpha".to_string(),
2115                },
2116                ListItem {
2117                    ordinal: 11,
2118                    label: "beta".to_string(),
2119                },
2120            ],
2121        };
2122        let rust_wire = phon::api::encode(&value).expect("encode Rust list fixture");
2123        let rust_wire_path = dir.join("rust.phon");
2124        let java_wire_path = dir.join("java.phon");
2125        fs::write(&rust_wire_path, &rust_wire).expect("write Rust wire fixture");
2126
2127        let facet_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
2128        let mut sources = collect_java_files(&facet_root.join("phon/java/runtime/src/main/java"));
2129        sources.extend(collect_java_files(&generated));
2130        sources.sort();
2131        let classes = dir.join("classes");
2132        fs::create_dir_all(&classes).expect("create Java classes directory");
2133        let javac = Command::new("javac")
2134            .arg("--release")
2135            .arg("17")
2136            .arg("-d")
2137            .arg(&classes)
2138            .args(&sources)
2139            .output()
2140            .expect("JDK 17+ javac must be available for the Java wire gate");
2141        let compiler_close_only = javac.status.code() == Some(3)
2142            && sources.iter().all(|source| {
2143                source
2144                    .strip_prefix(&facet_root.join("phon/java/runtime/src/main/java"))
2145                    .or_else(|_| source.strip_prefix(&dir))
2146                    .ok()
2147                    .map(|relative| classes.join(relative).with_extension("class").is_file())
2148                    .unwrap_or(false)
2149            });
2150        if !javac.status.success() && !compiler_close_only {
2151            panic!(
2152                "javac --release 17 failed:\nstdout:\n{}\nstderr:\n{}",
2153                String::from_utf8_lossy(&javac.stdout),
2154                String::from_utf8_lossy(&javac.stderr)
2155            );
2156        }
2157        if compiler_close_only {
2158            eprintln!(
2159                "warning: javac returned 3 after emitting every roundtrip class; treating the compiler resource-close failure as non-fatal"
2160            );
2161        }
2162
2163        let java = Command::new("java")
2164            .arg("-ea")
2165            .arg("-cp")
2166            .arg(&classes)
2167            .arg("org.facet.vox.generated.ListRoundtrip")
2168            .arg(&rust_wire_path)
2169            .arg(&java_wire_path)
2170            .output()
2171            .expect("JDK 17+ java must be available for the Java wire gate");
2172        if !java.status.success() {
2173            panic!(
2174                "Java list roundtrip failed:\nstdout:\n{}\nstderr:\n{}",
2175                String::from_utf8_lossy(&java.stdout),
2176                String::from_utf8_lossy(&java.stderr)
2177            );
2178        }
2179
2180        let java_wire = fs::read(&java_wire_path).expect("read Java wire fixture");
2181        assert_eq!(java_wire, rust_wire);
2182        let decoded: ListEnvelope =
2183            phon::api::decode(&java_wire).expect("decode Java list fixture in Rust");
2184        assert_eq!(decoded, value);
2185        fs::remove_dir_all(&dir).expect("remove Java roundtrip fixture");
2186    }
2187
2188    fn unique_temp_dir() -> PathBuf {
2189        let nonce = SystemTime::now()
2190            .duration_since(UNIX_EPOCH)
2191            .expect("clock")
2192            .as_nanos();
2193        std::env::temp_dir().join(format!("vox-java-codegen-{}-{nonce}", std::process::id()))
2194    }
2195
2196    fn collect_java_files(root: &Path) -> Vec<PathBuf> {
2197        fn visit(path: &Path, files: &mut Vec<PathBuf>) {
2198            for entry in fs::read_dir(path).expect("read fixture dir") {
2199                let path = entry.expect("dir entry").path();
2200                if path.is_dir() {
2201                    visit(&path, files);
2202                } else if path.extension().is_some_and(|ext| ext == "java") {
2203                    files.push(path);
2204                }
2205            }
2206        }
2207        let mut files = Vec::new();
2208        visit(root, &mut files);
2209        files.sort();
2210        files
2211    }
2212
2213    fn write_compile_stubs(root: &Path) {
2214        let files = [
2215            (
2216                "org/facet/phon/PhonAdapter.java",
2217                "package org.facet.phon; public interface PhonAdapter<T> { SchemaClosure schema(); void encode(PhonEncoder e,T v) throws PhonException; T decode(PhonDecoder d) throws PhonException; }",
2218            ),
2219            (
2220                "org/facet/phon/SchemaClosure.java",
2221                "package org.facet.phon; import java.util.*; public final class SchemaClosure { public static SchemaClosure uncheckedOf(Schema s,Schema... r){return new SchemaClosure();} public static SchemaClosure uncheckedWithAuxiliaryClosures(Schema s,List<Schema> r,Map<String,SchemaClosure> a){return new SchemaClosure();} public static SchemaClosure uncheckedWithAuxiliaryClosures(SchemaClosure s,Map<String,SchemaClosure> a){return new SchemaClosure();} public static SchemaClosure fromSchemaBundleBytes(SchemaId id,byte[] b,PhonLimits l)throws PhonException{return new SchemaClosure();} }",
2222            ),
2223            (
2224                "org/facet/phon/SchemaId.java",
2225                "package org.facet.phon; public final class SchemaId { public static SchemaId fromLong(long v){return new SchemaId();} }",
2226            ),
2227            (
2228                "org/facet/phon/Schema.java",
2229                "package org.facet.phon; import java.util.*; public final class Schema { public enum Primitive { STRING,BOOL,I32,I64,U8,U16,U32,F32,F64,BYTES,UNIT } public static Schema primitive(Primitive p){return new Schema(null,List.of(),null);} public Schema(SchemaId i,List<Object> p,Object k){} public SchemaId id(){return null;} public static final class Ref { public static Ref concrete(SchemaId i){return new Ref();} } public static final class Field { public Field(String n,Ref r,boolean q){} } public static final class RecordKind { public RecordKind(String n,List<Field> f){} } public static final class TupleKind { public TupleKind(List<Ref> r){} } public static final class EnumKind { public EnumKind(String n,List<Variant> v){} } public static final class Variant { public Variant(String n,int i,Payload p){} } public static final class Payload { public static Payload unit(){return new Payload();} } }",
2230            ),
2231            (
2232                "org/facet/phon/PhonException.java",
2233                "package org.facet.phon; public class PhonException extends Exception { public enum Kind { ENCODE,DECODE,MALFORMED } public PhonException(Kind k,String m){super(m);} }",
2234            ),
2235            (
2236                "org/facet/phon/PhonEncoder.java",
2237                "package org.facet.phon; public final class PhonEncoder { public void writeBool(boolean v){} public void writeU8(int v){} public void writeU16(int v){} public void writeU32(long v){} public void writeU32Unaligned(long v){} public void writeI8(int v){} public void writeI16(int v){} public void writeI32(int v){} public void writeI64(long v){} public void writeF32(float v){} public void writeF64(double v){} public void writeChar(char v){} public void writeString(String v){} public void writeBytes(byte[] v){} public void writeCount(int v){} public <T> void writeAdapted(PhonAdapter<T> a,T v){} }",
2238            ),
2239            (
2240                "org/facet/phon/PhonDecoder.java",
2241                "package org.facet.phon; public final class PhonDecoder { public boolean readBool(){return false;} public int readU8(){return 0;} public int readU16(){return 0;} public long readU32(){return 0;} public long readU32Unaligned(){return 0;} public int readI8(){return 0;} public int readI16(){return 0;} public int readI32(){return 0;} public long readI64(){return 0;} public float readF32(){return 0;} public double readF64(){return 0;} public char readChar(){return 0;} public String readString(){return null;} public byte[] readBytes(){return null;} public int readCount(){return 0;} public <T> T readAdapted(PhonAdapter<T> a){return null;} }",
2242            ),
2243            (
2244                "org/facet/phon/PhonLimits.java",
2245                "package org.facet.phon; public final class PhonLimits { public static PhonLimits defaults(){return new PhonLimits();} }",
2246            ),
2247            (
2248                "org/facet/phon/PhonCodec.java",
2249                "package org.facet.phon; public final class PhonCodec { public static <T> byte[] encode(PhonAdapter<T> a,T v,PhonLimits l)throws PhonException{return new byte[0];} public static <T> T decode(PhonAdapter<T> a,byte[] b,PhonLimits l)throws PhonException{return null;} }",
2250            ),
2251            (
2252                "org/facet/vox/MethodDescriptor.java",
2253                "package org.facet.vox; import java.util.*; import org.facet.phon.*; public final class MethodDescriptor { private final long id; private final PhonAdapter<?> a,r,w; private final List<ChannelDescriptor> c; public MethodDescriptor(long i,String n,PhonAdapter<?> a,PhonAdapter<?> r){this(i,n,a,r,null,r,List.of());} public MethodDescriptor(long i,String n,PhonAdapter<?> a,PhonAdapter<?> r,PhonAdapter<?> e){this(i,n,a,r,e,r,List.of());} public MethodDescriptor(long i,String n,PhonAdapter<?> a,PhonAdapter<?> r,PhonAdapter<?> e,PhonAdapter<?> w){this(i,n,a,r,e,w,List.of());} public MethodDescriptor(long i,String n,PhonAdapter<?> a,PhonAdapter<?> r,PhonAdapter<?> e,PhonAdapter<?> w,List<ChannelDescriptor> c){id=i;this.a=a;this.r=r;this.w=w;this.c=c;} public long id(){return id;} public PhonAdapter argumentAdapter(){return a;} public PhonAdapter returnAdapter(){return r;} public PhonAdapter responseWireAdapter(){return w;} public List<ChannelDescriptor> channels(){return c;} }",
2254            ),
2255            (
2256                "org/facet/vox/ChannelDescriptor.java",
2257                "package org.facet.vox; import org.facet.phon.*; public final class ChannelDescriptor { public enum Direction { TX,RX } public ChannelDescriptor(int i,Direction d,String r,PhonAdapter<?> a){} }",
2258            ),
2259            (
2260                "org/facet/vox/VoxTx.java",
2261                "package org.facet.vox; public final class VoxTx<T> {}",
2262            ),
2263            (
2264                "org/facet/vox/VoxRx.java",
2265                "package org.facet.vox; public final class VoxRx<T> {}",
2266            ),
2267            (
2268                "org/facet/vox/VoxChannelArgument.java",
2269                "package org.facet.vox; public final class VoxChannelArgument { public static VoxChannelArgument tx(ChannelDescriptor d,VoxTx<?> v){return new VoxChannelArgument();} public static VoxChannelArgument rx(ChannelDescriptor d,VoxRx<?> v){return new VoxChannelArgument();} }",
2270            ),
2271            (
2272                "org/facet/vox/VoxChannelDecoding.java",
2273                "package org.facet.vox; public final class VoxChannelDecoding { public static byte[] encodeIndex(long i){return null;} public static <T> VoxTx<T> tx(byte[] i){return null;} public static <T> VoxRx<T> rx(byte[] i){return null;} }",
2274            ),
2275            (
2276                "org/facet/vox/VoxFutures.java",
2277                "package org.facet.vox; import java.util.concurrent.*; import java.util.function.*; public final class VoxFutures { public static <T,U> CompletableFuture<U> mapCancellable(CompletableFuture<T> f,Function<? super T,? extends U> m){return null;} }",
2278            ),
2279            (
2280                "org/facet/vox/ServiceDescriptor.java",
2281                "package org.facet.vox; import java.util.*; public final class ServiceDescriptor { public ServiceDescriptor(String n,List<MethodDescriptor> m){} }",
2282            ),
2283            (
2284                "org/facet/vox/CallContext.java",
2285                "package org.facet.vox; public final class CallContext {}",
2286            ),
2287            (
2288                "org/facet/vox/VoxResult.java",
2289                "package org.facet.vox; public final class VoxResult<O,E> { public enum Kind { SUCCESS,APPLICATION_ERROR,UNKNOWN_METHOD,INVALID_PAYLOAD,CANCELLED,CONNECTION_CLOSED,CONNECTION_SHUTDOWN,SEND_FAILED,TIMED_OUT,INDETERMINATE } public static <O,E> VoxResult<O,E> success(O v){return new VoxResult<>();} public static <E> VoxResult<Void,E> successUnit(){return new VoxResult<>();} public static <O,E> VoxResult<O,E> applicationError(E v){return new VoxResult<>();} public static <O,E> VoxResult<O,E> infrastructure(Kind k){return new VoxResult<>();} public static <O,E> VoxResult<O,E> infrastructure(Kind k,String d){return new VoxResult<>();} public Kind kind(){return Kind.SUCCESS;} public boolean isSuccess(){return true;} public boolean isInfrastructureError(){return false;} public String detail(){return null;} public O success(){return null;} public E applicationError(){return null;} }",
2290            ),
2291            (
2292                "org/facet/vox/VoxException.java",
2293                "package org.facet.vox; public class VoxException extends Exception { public VoxException(String m){super(m);} }",
2294            ),
2295            (
2296                "org/facet/vox/ServiceDispatcher.java",
2297                "package org.facet.vox; import java.util.concurrent.*; public interface ServiceDispatcher { ServiceDescriptor descriptor(); CompletableFuture<Void> dispatch(InboundCall c); }",
2298            ),
2299            (
2300                "org/facet/vox/InboundCall.java",
2301                "package org.facet.vox; import org.facet.phon.*; public final class InboundCall { public MethodDescriptor method(){return null;} public byte[] encodedArguments(){return null;} public <T>T decodeArguments(PhonAdapter<T>a){return null;} public CallContext context(){return null;} public void respond(byte[] b){} }",
2302            ),
2303            (
2304                "org/facet/vox/ServiceLane.java",
2305                "package org.facet.vox; import java.util.*; import java.util.concurrent.*; public final class ServiceLane { public CompletableFuture<byte[]> call(MethodDescriptor m,byte[] b,CallOptions o){return null;} public CompletableFuture<byte[]> call(MethodDescriptor m,byte[] b,CallOptions o,List<VoxChannelArgument> c){return null;} }",
2306            ),
2307            (
2308                "org/facet/vox/CallOptions.java",
2309                "package org.facet.vox; public final class CallOptions { public static CallOptions defaults(){return new CallOptions();} }",
2310            ),
2311        ];
2312        for (relative, source) in files {
2313            let path = root.join(relative);
2314            fs::create_dir_all(path.parent().expect("parent")).expect("create stub package");
2315            fs::write(path, source).expect("write compile stub");
2316        }
2317    }
2318}