Skip to main content

phon_codegen/targets/java/
mod.rs

1//! Deterministic Java 17 schema-table generation.
2//!
3//! Vox's Java target owns ergonomic DTOs and adapters. This lower-level target
4//! emits the canonical Phon bytes and root ids those adapters are derived from.
5
6use std::fmt::Write;
7
8use phon_schema::schema_bundle_to_bytes;
9
10use crate::Module;
11
12#[must_use]
13pub fn render_schema_table(module: &Module, package: &str, class_name: &str) -> String {
14    let mut out = format!(
15        "// @generated by phon-codegen; DO NOT EDIT.\npackage {package};\n\n\
16         public final class {class_name} {{\n"
17    );
18    for root in &module.roots {
19        let _ = writeln!(
20            out,
21            "  public static final long {}_SCHEMA_ID = 0x{:016x}L;",
22            java_constant(&root.name),
23            root.id.as_u64()
24        );
25    }
26    let bytes = schema_bundle_to_bytes(&module.schemas).expect("validated codegen schema bundle");
27    out.push_str("  public static final byte[] SCHEMA_BUNDLE = schemaBundle();\n");
28    out.push_str(&render_byte_factory("schemaBundle", &bytes));
29    let _ = writeln!(out, "  private {class_name}() {{}}");
30    out.push_str("}\n");
31    out
32}
33
34/// Emit bounded UTF-8 constants instead of one bytecode instruction per byte.
35/// 16 KiB chunks stay below the JVM's 65,535-byte constant and method limits,
36/// including for bundles at the runtime's 64 MiB admission limit.
37#[must_use]
38pub fn render_byte_factory(name: &str, bytes: &[u8]) -> String {
39    let mut out = format!(
40        "  private static byte[] {name}() {{\n    var out = new java.io.ByteArrayOutputStream({});\n",
41        bytes.len()
42    );
43    for chunk in bytes.chunks(16 * 1024) {
44        out.push_str("    out.writeBytes(java.util.HexFormat.of().parseHex(\"");
45        for byte in chunk {
46            let _ = write!(out, "{byte:02x}");
47        }
48        out.push_str("\"));\n");
49    }
50    out.push_str("    return out.toByteArray();\n  }\n");
51    out
52}
53
54fn java_constant(name: &str) -> String {
55    name.chars()
56        .flat_map(|character| {
57            if character.is_ascii_uppercase() {
58                vec!['_', character]
59            } else if character.is_ascii_alphanumeric() {
60                vec![character.to_ascii_uppercase()]
61            } else {
62                vec!['_']
63            }
64        })
65        .collect::<String>()
66        .trim_start_matches('_')
67        .to_string()
68}
69
70#[cfg(test)]
71mod tests {
72    use facet::Facet;
73
74    use crate::Builder;
75
76    use super::render_schema_table;
77
78    #[derive(Facet)]
79    struct Message {
80        text: String,
81    }
82
83    #[test]
84    fn large_byte_factory_compiles_and_preserves_every_byte() {
85        let bytes: Vec<u8> = (0..262_147).map(|i| (i % 251) as u8).collect();
86        let factory = super::render_byte_factory("bundle", &bytes);
87        assert_eq!(factory, super::render_byte_factory("bundle", &bytes));
88        let directory =
89            std::env::temp_dir().join(format!("phon-java-bundle-{}", std::process::id()));
90        std::fs::create_dir_all(&directory).unwrap();
91        let source = format!(
92            "public class LargeBundle {{ {factory} public static void main(String[] args) throws Exception {{ java.nio.file.Files.write(java.nio.file.Path.of(args[0]), bundle()); }} }}"
93        );
94        std::fs::write(directory.join("LargeBundle.java"), source).unwrap();
95        let compile = std::process::Command::new("javac")
96            .args(["--release", "17"])
97            .arg(directory.join("LargeBundle.java"))
98            .output()
99            .expect("JDK required for Java generation qualification");
100        assert!(
101            compile.status.success(),
102            "{}",
103            String::from_utf8_lossy(&compile.stderr)
104        );
105        let output = directory.join("bundle.bin");
106        assert!(
107            std::process::Command::new("java")
108                .arg("-cp")
109                .arg(&directory)
110                .arg("LargeBundle")
111                .arg(&output)
112                .status()
113                .unwrap()
114                .success()
115        );
116        assert_eq!(std::fs::read(output).unwrap(), bytes);
117        std::fs::remove_dir_all(directory).unwrap();
118    }
119
120    #[test]
121    fn schema_table_is_deterministic_and_contains_canonical_root() {
122        let module = Builder::new()
123            .add::<Message>()
124            .build()
125            .expect("derive schema");
126        let first = render_schema_table(&module, "org.facet.phon.generated", "FixtureSchemas");
127        let second = render_schema_table(&module, "org.facet.phon.generated", "FixtureSchemas");
128        assert_eq!(first, second);
129        assert!(first.contains("MESSAGE_SCHEMA_ID = 0x"));
130        assert!(first.contains("HexFormat.of().parseHex"));
131        assert!(first.contains("private FixtureSchemas()"));
132    }
133}