Skip to main content

alef_backend_java/
gen_bindings.rs

1use crate::type_map::{java_boxed_type, java_ffi_type, java_type};
2use ahash::AHashSet;
3use alef_codegen::naming::{to_class_name, to_java_name};
4use alef_core::backend::{Backend, BuildConfig, Capabilities, GeneratedFile};
5use alef_core::config::{AlefConfig, Language, resolve_output_dir};
6use alef_core::ir::{ApiSurface, EnumDef, FunctionDef, PrimitiveType, TypeDef, TypeRef};
7use heck::{ToLowerCamelCase, ToPascalCase, ToSnakeCase};
8use std::fmt::Write;
9use std::path::PathBuf;
10
11/// Names that conflict with methods on `java.lang.Object` and are therefore
12/// illegal as record component names or method names in generated Java code.
13const JAVA_OBJECT_METHOD_NAMES: &[&str] = &[
14    "wait",
15    "notify",
16    "notifyAll",
17    "getClass",
18    "hashCode",
19    "equals",
20    "toString",
21    "clone",
22    "finalize",
23];
24
25/// Sanitise a field/parameter name that would conflict with `java.lang.Object`
26/// methods.  Conflicting names get a `_` suffix (e.g. `wait` -> `wait_`), which
27/// is then converted to camelCase by `to_java_name`.
28fn safe_java_field_name(name: &str) -> String {
29    let java_name = to_java_name(name);
30    if JAVA_OBJECT_METHOD_NAMES.contains(&java_name.as_str()) {
31        format!("{}Value", java_name)
32    } else {
33        java_name
34    }
35}
36
37pub struct JavaBackend;
38
39impl JavaBackend {
40    /// Convert crate name to main class name (PascalCase + "Rs" suffix).
41    ///
42    /// The "Rs" suffix ensures the raw FFI wrapper class has a distinct name from
43    /// the public facade class (which strips the "Rs" suffix). Without this, the
44    /// facade would delegate to itself, causing infinite recursion.
45    fn resolve_main_class(api: &ApiSurface) -> String {
46        let base = to_class_name(&api.crate_name.replace('-', "_"));
47        if base.ends_with("Rs") {
48            base
49        } else {
50            format!("{}Rs", base)
51        }
52    }
53}
54
55impl Backend for JavaBackend {
56    fn name(&self) -> &str {
57        "java"
58    }
59
60    fn language(&self) -> Language {
61        Language::Java
62    }
63
64    fn capabilities(&self) -> Capabilities {
65        Capabilities {
66            supports_async: true,
67            supports_classes: true,
68            supports_enums: true,
69            supports_option: true,
70            supports_result: true,
71            ..Capabilities::default()
72        }
73    }
74
75    fn generate_bindings(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
76        let package = config.java_package();
77        let prefix = config.ffi_prefix();
78        let main_class = Self::resolve_main_class(api);
79        let package_path = package.replace('.', "/");
80
81        let output_dir = resolve_output_dir(
82            config.output.java.as_ref(),
83            &config.crate_config.name,
84            "packages/java/src/main/java/",
85        );
86
87        let base_path = PathBuf::from(&output_dir).join(&package_path);
88
89        let mut files = Vec::new();
90
91        // 0. package-info.java - required by Checkstyle
92        let description = config
93            .scaffold
94            .as_ref()
95            .and_then(|s| s.description.as_deref())
96            .unwrap_or("High-performance HTML to Markdown converter.");
97        files.push(GeneratedFile {
98            path: base_path.join("package-info.java"),
99            content: format!(
100                "/**\n * {description}\n */\npackage {package};\n",
101                description = description,
102                package = package,
103            ),
104            generated_header: true,
105        });
106
107        // 1. NativeLib.java - FFI method handles
108        files.push(GeneratedFile {
109            path: base_path.join("NativeLib.java"),
110            content: gen_native_lib(api, config, &package, &prefix),
111            generated_header: true,
112        });
113
114        // 2. Main wrapper class
115        files.push(GeneratedFile {
116            path: base_path.join(format!("{}.java", main_class)),
117            content: gen_main_class(api, config, &package, &main_class, &prefix),
118            generated_header: true,
119        });
120
121        // 3. Exception class
122        files.push(GeneratedFile {
123            path: base_path.join(format!("{}Exception.java", main_class)),
124            content: gen_exception_class(&package, &main_class),
125            generated_header: true,
126        });
127
128        // Collect complex enums (enums with data variants and no serde tag) — use Object for these fields.
129        // Tagged unions (serde_tag is set) are now generated as proper sealed interfaces
130        // and can be deserialized as their concrete types, so they are NOT complex_enums.
131        let complex_enums: AHashSet<String> = api
132            .enums
133            .iter()
134            .filter(|e| e.serde_tag.is_none() && e.variants.iter().any(|v| !v.fields.is_empty()))
135            .map(|e| e.name.clone())
136            .collect();
137
138        // Resolve language-level serde rename strategy (always wins over IR type-level).
139        let lang_rename_all = config.serde_rename_all_for_language(Language::Java);
140
141        // 4. Record types
142        for typ in api.types.iter().filter(|typ| !typ.is_trait) {
143            if !typ.is_opaque && !typ.fields.is_empty() {
144                files.push(GeneratedFile {
145                    path: base_path.join(format!("{}.java", typ.name)),
146                    content: gen_record_type(&package, typ, &complex_enums, &lang_rename_all),
147                    generated_header: true,
148                });
149                // Generate builder class for types with defaults
150                if typ.has_default {
151                    files.push(GeneratedFile {
152                        path: base_path.join(format!("{}Builder.java", typ.name)),
153                        content: gen_builder_class(&package, typ),
154                        generated_header: true,
155                    });
156                }
157            }
158        }
159
160        // Collect builder class names generated from record types with defaults,
161        // so we can skip opaque types that would collide with them.
162        let builder_class_names: AHashSet<String> = api
163            .types
164            .iter()
165            .filter(|t| !t.is_opaque && !t.fields.is_empty() && t.has_default)
166            .map(|t| format!("{}Builder", t.name))
167            .collect();
168
169        // 4b. Opaque handle types (skip if a pure-Java builder already covers this name)
170        for typ in api.types.iter().filter(|typ| !typ.is_trait) {
171            if typ.is_opaque && !builder_class_names.contains(&typ.name) {
172                files.push(GeneratedFile {
173                    path: base_path.join(format!("{}.java", typ.name)),
174                    content: gen_opaque_handle_class(&package, typ, &prefix),
175                    generated_header: true,
176                });
177            }
178        }
179
180        // 5. Enums
181        for enum_def in &api.enums {
182            files.push(GeneratedFile {
183                path: base_path.join(format!("{}.java", enum_def.name)),
184                content: gen_enum_class(&package, enum_def),
185                generated_header: true,
186            });
187        }
188
189        // 6. Error exception classes
190        for error in &api.errors {
191            for (class_name, content) in alef_codegen::error_gen::gen_java_error_types(error, &package) {
192                files.push(GeneratedFile {
193                    path: base_path.join(format!("{}.java", class_name)),
194                    content,
195                    generated_header: true,
196                });
197            }
198        }
199
200        // Build adapter body map (consumed by generators via body substitution)
201        let _adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Java)?;
202
203        Ok(files)
204    }
205
206    fn generate_public_api(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
207        let package = config.java_package();
208        let prefix = config.ffi_prefix();
209        let main_class = Self::resolve_main_class(api);
210        let package_path = package.replace('.', "/");
211
212        let output_dir = resolve_output_dir(
213            config.output.java.as_ref(),
214            &config.crate_config.name,
215            "packages/java/src/main/java/",
216        );
217
218        let base_path = PathBuf::from(&output_dir).join(&package_path);
219
220        // Generate a high-level public API class that wraps the raw FFI class.
221        // Class name = main_class without "Rs" suffix (e.g., HtmlToMarkdownRs -> HtmlToMarkdown)
222        let public_class = main_class.trim_end_matches("Rs").to_string();
223        let facade_content = gen_facade_class(api, &package, &public_class, &main_class, &prefix);
224
225        Ok(vec![GeneratedFile {
226            path: base_path.join(format!("{}.java", public_class)),
227            content: facade_content,
228            generated_header: true,
229        }])
230    }
231
232    fn build_config(&self) -> Option<BuildConfig> {
233        Some(BuildConfig {
234            tool: "mvn",
235            crate_suffix: "",
236            depends_on_ffi: true,
237            post_build: vec![],
238        })
239    }
240}
241
242// ---------------------------------------------------------------------------
243// NativeLib.java - FFI method handles
244// ---------------------------------------------------------------------------
245
246fn gen_native_lib(api: &ApiSurface, config: &AlefConfig, package: &str, prefix: &str) -> String {
247    // Generate the class body first, then scan it to determine which imports are needed.
248    let mut body = String::with_capacity(2048);
249    // Derive the native library name from the FFI output path (directory name with hyphens replaced
250    // by underscores), falling back to `{ffi_prefix}_ffi`.
251    let lib_name = config.ffi_lib_name();
252
253    writeln!(body, "final class NativeLib {{").ok();
254    writeln!(body, "    private static final Linker LINKER = Linker.nativeLinker();").ok();
255    writeln!(body, "    private static final SymbolLookup LIB;").ok();
256    writeln!(
257        body,
258        "    private static final String NATIVES_RESOURCE_ROOT = \"/natives\";"
259    )
260    .ok();
261    writeln!(
262        body,
263        "    private static final Object NATIVE_EXTRACT_LOCK = new Object();"
264    )
265    .ok();
266    writeln!(body, "    private static String cachedExtractKey;").ok();
267    writeln!(body, "    private static Path cachedExtractDir;").ok();
268    writeln!(body).ok();
269    writeln!(body, "    static {{").ok();
270    writeln!(body, "        loadNativeLibrary();").ok();
271    writeln!(body, "        LIB = SymbolLookup.loaderLookup();").ok();
272    writeln!(body, "    }}").ok();
273    writeln!(body).ok();
274    writeln!(body, "    private static void loadNativeLibrary() {{").ok();
275    writeln!(
276        body,
277        "        String osName = System.getProperty(\"os.name\", \"\").toLowerCase(java.util.Locale.ROOT);"
278    )
279    .ok();
280    writeln!(
281        body,
282        "        String osArch = System.getProperty(\"os.arch\", \"\").toLowerCase(java.util.Locale.ROOT);"
283    )
284    .ok();
285    writeln!(body).ok();
286    writeln!(body, "        String libName;").ok();
287    writeln!(body, "        String libExt;").ok();
288    writeln!(
289        body,
290        "        if (osName.contains(\"mac\") || osName.contains(\"darwin\")) {{"
291    )
292    .ok();
293    writeln!(body, "            libName = \"lib{}\";", lib_name).ok();
294    writeln!(body, "            libExt = \".dylib\";").ok();
295    writeln!(body, "        }} else if (osName.contains(\"win\")) {{").ok();
296    writeln!(body, "            libName = \"{}\";", lib_name).ok();
297    writeln!(body, "            libExt = \".dll\";").ok();
298    writeln!(body, "        }} else {{").ok();
299    writeln!(body, "            libName = \"lib{}\";", lib_name).ok();
300    writeln!(body, "            libExt = \".so\";").ok();
301    writeln!(body, "        }}").ok();
302    writeln!(body).ok();
303    writeln!(body, "        String nativesRid = resolveNativesRid(osName, osArch);").ok();
304    writeln!(
305        body,
306        "        String nativesDir = NATIVES_RESOURCE_ROOT + \"/\" + nativesRid;"
307    )
308    .ok();
309    writeln!(body).ok();
310    writeln!(
311        body,
312        "        Path extracted = tryExtractAndLoadFromResources(nativesDir, libName, libExt);"
313    )
314    .ok();
315    writeln!(body, "        if (extracted != null) {{").ok();
316    writeln!(body, "            return;").ok();
317    writeln!(body, "        }}").ok();
318    writeln!(body).ok();
319    writeln!(body, "        try {{").ok();
320    writeln!(body, "            System.loadLibrary(\"{}\");", lib_name).ok();
321    writeln!(body, "        }} catch (UnsatisfiedLinkError e) {{").ok();
322    writeln!(
323        body,
324        "            String msg = \"Failed to load {} native library. Expected resource: \" + nativesDir + \"/\" + libName",
325        lib_name
326    ).ok();
327    writeln!(
328        body,
329        "                    + libExt + \" (RID: \" + nativesRid + \"). \""
330    )
331    .ok();
332    writeln!(
333        body,
334        "                    + \"Ensure the library is bundled in the JAR under natives/{{os-arch}}/, \""
335    )
336    .ok();
337    writeln!(
338        body,
339        "                    + \"or place it on the system library path (java.library.path).\";",
340    )
341    .ok();
342    writeln!(
343        body,
344        "            UnsatisfiedLinkError out = new UnsatisfiedLinkError(msg + \" Original error: \" + e.getMessage());"
345    )
346    .ok();
347    writeln!(body, "            out.initCause(e);").ok();
348    writeln!(body, "            throw out;").ok();
349    writeln!(body, "        }}").ok();
350    writeln!(body, "    }}").ok();
351    writeln!(body).ok();
352    writeln!(
353        body,
354        "    private static Path tryExtractAndLoadFromResources(String nativesDir, String libName, String libExt) {{"
355    )
356    .ok();
357    writeln!(
358        body,
359        "        String resourcePath = nativesDir + \"/\" + libName + libExt;"
360    )
361    .ok();
362    writeln!(
363        body,
364        "        URL resource = NativeLib.class.getResource(resourcePath);"
365    )
366    .ok();
367    writeln!(body, "        if (resource == null) {{").ok();
368    writeln!(body, "            return null;").ok();
369    writeln!(body, "        }}").ok();
370    writeln!(body).ok();
371    writeln!(body, "        try {{").ok();
372    writeln!(
373        body,
374        "            Path tempDir = extractOrReuseNativeDirectory(nativesDir);"
375    )
376    .ok();
377    writeln!(body, "            Path libPath = tempDir.resolve(libName + libExt);").ok();
378    writeln!(body, "            if (!Files.exists(libPath)) {{").ok();
379    writeln!(
380        body,
381        "                throw new UnsatisfiedLinkError(\"Missing extracted native library: \" + libPath);"
382    )
383    .ok();
384    writeln!(body, "            }}").ok();
385    writeln!(body, "            System.load(libPath.toAbsolutePath().toString());").ok();
386    writeln!(body, "            return libPath;").ok();
387    writeln!(body, "        }} catch (Exception e) {{").ok();
388    writeln!(body, "            System.err.println(\"[NativeLib] Failed to extract and load native library from resources: \" + e.getMessage());").ok();
389    writeln!(body, "            return null;").ok();
390    writeln!(body, "        }}").ok();
391    writeln!(body, "    }}").ok();
392    writeln!(body).ok();
393    writeln!(
394        body,
395        "    private static Path extractOrReuseNativeDirectory(String nativesDir) throws Exception {{"
396    )
397    .ok();
398    writeln!(
399        body,
400        "        URL location = NativeLib.class.getProtectionDomain().getCodeSource().getLocation();"
401    )
402    .ok();
403    writeln!(body, "        if (location == null) {{").ok();
404    writeln!(
405        body,
406        "            throw new IllegalStateException(\"Missing code source location for {} JAR\");",
407        lib_name
408    )
409    .ok();
410    writeln!(body, "        }}").ok();
411    writeln!(body).ok();
412    writeln!(body, "        Path codePath = Path.of(location.toURI());").ok();
413    writeln!(
414        body,
415        "        String key = codePath.toAbsolutePath() + \"::\" + nativesDir;"
416    )
417    .ok();
418    writeln!(body).ok();
419    writeln!(body, "        synchronized (NATIVE_EXTRACT_LOCK) {{").ok();
420    writeln!(
421        body,
422        "            if (cachedExtractDir != null && key.equals(cachedExtractKey)) {{"
423    )
424    .ok();
425    writeln!(body, "                return cachedExtractDir;").ok();
426    writeln!(body, "            }}").ok();
427    writeln!(
428        body,
429        "            Path tempDir = Files.createTempDirectory(\"{}_native\");",
430        lib_name
431    )
432    .ok();
433    writeln!(body, "            tempDir.toFile().deleteOnExit();").ok();
434    writeln!(
435        body,
436        "            List<Path> extracted = extractNativeDirectory(codePath, nativesDir, tempDir);"
437    )
438    .ok();
439    writeln!(body, "            if (extracted.isEmpty()) {{").ok();
440    writeln!(body, "                throw new IllegalStateException(\"No native files extracted from resources dir: \" + nativesDir);").ok();
441    writeln!(body, "            }}").ok();
442    writeln!(body, "            cachedExtractKey = key;").ok();
443    writeln!(body, "            cachedExtractDir = tempDir;").ok();
444    writeln!(body, "            return tempDir;").ok();
445    writeln!(body, "        }}").ok();
446    writeln!(body, "    }}").ok();
447    writeln!(body).ok();
448    writeln!(body, "    private static List<Path> extractNativeDirectory(Path codePath, String nativesDir, Path destDir) throws Exception {{").ok();
449    writeln!(
450        body,
451        "        if (!Files.exists(destDir) || !Files.isDirectory(destDir)) {{"
452    )
453    .ok();
454    writeln!(
455        body,
456        "            throw new IllegalArgumentException(\"Destination directory does not exist: \" + destDir);"
457    )
458    .ok();
459    writeln!(body, "        }}").ok();
460    writeln!(body).ok();
461    writeln!(
462        body,
463        "        String prefix = nativesDir.startsWith(\"/\") ? nativesDir.substring(1) : nativesDir;"
464    )
465    .ok();
466    writeln!(body, "        if (!prefix.endsWith(\"/\")) {{").ok();
467    writeln!(body, "            prefix = prefix + \"/\";").ok();
468    writeln!(body, "        }}").ok();
469    writeln!(body).ok();
470    writeln!(body, "        if (Files.isDirectory(codePath)) {{").ok();
471    writeln!(body, "            Path nativesPath = codePath.resolve(prefix);").ok();
472    writeln!(
473        body,
474        "            if (!Files.exists(nativesPath) || !Files.isDirectory(nativesPath)) {{"
475    )
476    .ok();
477    writeln!(body, "                return List.of();").ok();
478    writeln!(body, "            }}").ok();
479    writeln!(body, "            return copyDirectory(nativesPath, destDir);").ok();
480    writeln!(body, "        }}").ok();
481    writeln!(body).ok();
482    writeln!(body, "        List<Path> extracted = new ArrayList<>();").ok();
483    writeln!(body, "        try (JarFile jar = new JarFile(codePath.toFile())) {{").ok();
484    writeln!(body, "            Enumeration<JarEntry> entries = jar.entries();").ok();
485    writeln!(body, "            while (entries.hasMoreElements()) {{").ok();
486    writeln!(body, "                JarEntry entry = entries.nextElement();").ok();
487    writeln!(body, "                String name = entry.getName();").ok();
488    writeln!(
489        body,
490        "                if (!name.startsWith(prefix) || entry.isDirectory()) {{"
491    )
492    .ok();
493    writeln!(body, "                    continue;").ok();
494    writeln!(body, "                }}").ok();
495    writeln!(
496        body,
497        "                String relative = name.substring(prefix.length());"
498    )
499    .ok();
500    writeln!(body, "                Path out = safeResolve(destDir, relative);").ok();
501    writeln!(body, "                Files.createDirectories(out.getParent());").ok();
502    writeln!(body, "                try (var in = jar.getInputStream(entry)) {{").ok();
503    writeln!(
504        body,
505        "                    Files.copy(in, out, StandardCopyOption.REPLACE_EXISTING);"
506    )
507    .ok();
508    writeln!(body, "                }}").ok();
509    writeln!(body, "                out.toFile().deleteOnExit();").ok();
510    writeln!(body, "                extracted.add(out);").ok();
511    writeln!(body, "            }}").ok();
512    writeln!(body, "        }}").ok();
513    writeln!(body, "        return extracted;").ok();
514    writeln!(body, "    }}").ok();
515    writeln!(body).ok();
516    writeln!(
517        body,
518        "    private static List<Path> copyDirectory(Path srcDir, Path destDir) throws Exception {{"
519    )
520    .ok();
521    writeln!(body, "        List<Path> copied = new ArrayList<>();").ok();
522    writeln!(body, "        try (var paths = Files.walk(srcDir)) {{").ok();
523    writeln!(body, "            for (Path src : (Iterable<Path>) paths::iterator) {{").ok();
524    writeln!(body, "                if (Files.isDirectory(src)) {{").ok();
525    writeln!(body, "                    continue;").ok();
526    writeln!(body, "                }}").ok();
527    writeln!(body, "                Path relative = srcDir.relativize(src);").ok();
528    writeln!(
529        body,
530        "                Path out = safeResolve(destDir, relative.toString());"
531    )
532    .ok();
533    writeln!(body, "                Files.createDirectories(out.getParent());").ok();
534    writeln!(
535        body,
536        "                Files.copy(src, out, StandardCopyOption.REPLACE_EXISTING);"
537    )
538    .ok();
539    writeln!(body, "                out.toFile().deleteOnExit();").ok();
540    writeln!(body, "                copied.add(out);").ok();
541    writeln!(body, "            }}").ok();
542    writeln!(body, "        }}").ok();
543    writeln!(body, "        return copied;").ok();
544    writeln!(body, "    }}").ok();
545    writeln!(body).ok();
546    writeln!(
547        body,
548        "    private static Path safeResolve(Path destDir, String relative) throws Exception {{"
549    )
550    .ok();
551    writeln!(
552        body,
553        "        Path normalizedDest = destDir.toAbsolutePath().normalize();"
554    )
555    .ok();
556    writeln!(body, "        Path out = normalizedDest.resolve(relative).normalize();").ok();
557    writeln!(body, "        if (!out.startsWith(normalizedDest)) {{").ok();
558    writeln!(body, "            throw new SecurityException(\"Blocked extracting native file outside destination directory: \" + relative);").ok();
559    writeln!(body, "        }}").ok();
560    writeln!(body, "        return out;").ok();
561    writeln!(body, "    }}").ok();
562    writeln!(body).ok();
563    writeln!(
564        body,
565        "    private static String resolveNativesRid(String osName, String osArch) {{"
566    )
567    .ok();
568    writeln!(body, "        String arch;").ok();
569    writeln!(
570        body,
571        "        if (osArch.contains(\"aarch64\") || osArch.contains(\"arm64\")) {{"
572    )
573    .ok();
574    writeln!(body, "            arch = \"arm64\";").ok();
575    writeln!(
576        body,
577        "        }} else if (osArch.contains(\"x86_64\") || osArch.contains(\"amd64\")) {{"
578    )
579    .ok();
580    writeln!(body, "            arch = \"x86_64\";").ok();
581    writeln!(body, "        }} else {{").ok();
582    writeln!(body, "            arch = osArch.replaceAll(\"[^a-z0-9_]+\", \"\");").ok();
583    writeln!(body, "        }}").ok();
584    writeln!(body).ok();
585    writeln!(body, "        String os;").ok();
586    writeln!(
587        body,
588        "        if (osName.contains(\"mac\") || osName.contains(\"darwin\")) {{"
589    )
590    .ok();
591    writeln!(body, "            os = \"macos\";").ok();
592    writeln!(body, "        }} else if (osName.contains(\"win\")) {{").ok();
593    writeln!(body, "            os = \"windows\";").ok();
594    writeln!(body, "        }} else {{").ok();
595    writeln!(body, "            os = \"linux\";").ok();
596    writeln!(body, "        }}").ok();
597    writeln!(body).ok();
598    writeln!(body, "        return os + \"-\" + arch;").ok();
599    writeln!(body, "    }}").ok();
600    writeln!(body).ok();
601
602    // Generate method handles for free functions.
603    // All functions get handles regardless of is_async — the FFI layer always exposes
604    // synchronous C functions, and the Java async wrapper delegates to the sync method.
605    for func in &api.functions {
606        let ffi_name = format!("{}_{}", prefix, func.name.to_lowercase());
607        let return_layout = gen_ffi_layout(&func.return_type);
608        let param_layouts: Vec<String> = func.params.iter().map(|p| gen_ffi_layout(&p.ty)).collect();
609
610        let layout_str = gen_function_descriptor(&return_layout, &param_layouts);
611
612        let handle_name = format!("{}_{}", prefix.to_uppercase(), func.name.to_uppercase());
613
614        writeln!(
615            body,
616            "    static final MethodHandle {} = LINKER.downcallHandle(",
617            handle_name
618        )
619        .ok();
620        writeln!(body, "        LIB.find(\"{}\").orElseThrow(),", ffi_name).ok();
621        writeln!(body, "        {}", layout_str).ok();
622        writeln!(body, "    );").ok();
623    }
624
625    // free_string handle for releasing FFI-allocated strings
626    {
627        let free_name = format!("{}_free_string", prefix);
628        let handle_name = format!("{}_FREE_STRING", prefix.to_uppercase());
629        writeln!(body).ok();
630        writeln!(
631            body,
632            "    static final MethodHandle {} = LINKER.downcallHandle(",
633            handle_name
634        )
635        .ok();
636        writeln!(body, "        LIB.find(\"{}\").orElseThrow(),", free_name).ok();
637        writeln!(body, "        FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)").ok();
638        writeln!(body, "    );").ok();
639    }
640
641    // Error handling — use the FFI's last_error_code and last_error_context symbols
642    {
643        writeln!(
644            body,
645            "    static final MethodHandle {}_LAST_ERROR_CODE = LINKER.downcallHandle(",
646            prefix.to_uppercase()
647        )
648        .ok();
649        writeln!(body, "        LIB.find(\"{}_last_error_code\").orElseThrow(),", prefix).ok();
650        writeln!(body, "        FunctionDescriptor.of(ValueLayout.JAVA_INT)").ok();
651        writeln!(body, "    );").ok();
652
653        writeln!(
654            body,
655            "    static final MethodHandle {}_LAST_ERROR_CONTEXT = LINKER.downcallHandle(",
656            prefix.to_uppercase()
657        )
658        .ok();
659        writeln!(
660            body,
661            "        LIB.find(\"{}_last_error_context\").orElseThrow(),",
662            prefix
663        )
664        .ok();
665        writeln!(body, "        FunctionDescriptor.of(ValueLayout.ADDRESS)").ok();
666        writeln!(body, "    );").ok();
667    }
668
669    // Track emitted free handles to avoid duplicates (a type may appear both as
670    // a function return type AND as an opaque type).
671    let mut emitted_free_handles: AHashSet<String> = AHashSet::new();
672
673    // Build the set of opaque type names so we can pick the right accessor below.
674    let opaque_type_names: AHashSet<String> = api
675        .types
676        .iter()
677        .filter(|t| t.is_opaque)
678        .map(|t| t.name.clone())
679        .collect();
680
681    // Accessor handles for Named return types (struct pointer → field accessor + free)
682    for func in &api.functions {
683        if let TypeRef::Named(name) = &func.return_type {
684            let type_snake = name.to_snake_case();
685            let type_upper = type_snake.to_uppercase();
686            let is_opaque = opaque_type_names.contains(name.as_str());
687
688            if is_opaque {
689                // Opaque handles: the caller wraps the pointer directly, no JSON needed.
690                // No content accessor is emitted for opaque types.
691            } else {
692                // Non-opaque record types: use _to_json to serialize the full struct to JSON,
693                // which the Java side then deserializes with ObjectMapper.
694                // NOTE: _content returns only the markdown string field, not the full JSON.
695                let to_json_handle = format!("{}_{}_TO_JSON", prefix.to_uppercase(), type_upper);
696                let to_json_ffi = format!("{}_{}_to_json", prefix, type_snake);
697                writeln!(body).ok();
698                writeln!(
699                    body,
700                    "    static final MethodHandle {} = LINKER.downcallHandle(",
701                    to_json_handle
702                )
703                .ok();
704                writeln!(body, "        LIB.find(\"{}\").orElseThrow(),", to_json_ffi).ok();
705                writeln!(
706                    body,
707                    "        FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS)"
708                )
709                .ok();
710                writeln!(body, "    );").ok();
711            }
712
713            // _free: (struct_ptr) -> void
714            let free_handle = format!("{}_{}_FREE", prefix.to_uppercase(), type_upper);
715            let free_ffi = format!("{}_{}_free", prefix, type_snake);
716            if emitted_free_handles.insert(free_handle.clone()) {
717                writeln!(body).ok();
718                writeln!(
719                    body,
720                    "    static final MethodHandle {} = LINKER.downcallHandle(",
721                    free_handle
722                )
723                .ok();
724                writeln!(body, "        LIB.find(\"{}\").orElseThrow(),", free_ffi).ok();
725                writeln!(body, "        FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)").ok();
726                writeln!(body, "    );").ok();
727            }
728        }
729    }
730
731    // FROM_JSON + FREE handles for non-opaque Named types used as parameters.
732    // These allow serializing a Java record to JSON and passing it to the FFI.
733    let mut emitted_from_json_handles: AHashSet<String> = AHashSet::new();
734    for func in &api.functions {
735        for param in &func.params {
736            // Handle both Named and Optional<Named> params
737            let inner_name = match &param.ty {
738                TypeRef::Named(n) => Some(n.clone()),
739                TypeRef::Optional(inner) => {
740                    if let TypeRef::Named(n) = inner.as_ref() {
741                        Some(n.clone())
742                    } else {
743                        None
744                    }
745                }
746                _ => None,
747            };
748            if let Some(name) = inner_name {
749                if !opaque_type_names.contains(name.as_str()) {
750                    let type_snake = name.to_snake_case();
751                    let type_upper = type_snake.to_uppercase();
752
753                    // _from_json: (char*) -> struct_ptr
754                    let from_json_handle = format!("{}_{}_FROM_JSON", prefix.to_uppercase(), type_upper);
755                    let from_json_ffi = format!("{}_{}_from_json", prefix, type_snake);
756                    if emitted_from_json_handles.insert(from_json_handle.clone()) {
757                        writeln!(body).ok();
758                        writeln!(
759                            body,
760                            "    static final MethodHandle {} = LINKER.downcallHandle(",
761                            from_json_handle
762                        )
763                        .ok();
764                        writeln!(body, "        LIB.find(\"{}\").orElseThrow(),", from_json_ffi).ok();
765                        writeln!(
766                            body,
767                            "        FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS)"
768                        )
769                        .ok();
770                        writeln!(body, "    );").ok();
771                    }
772
773                    // _free: (struct_ptr) -> void
774                    let free_handle = format!("{}_{}_FREE", prefix.to_uppercase(), type_upper);
775                    let free_ffi = format!("{}_{}_free", prefix, type_snake);
776                    if emitted_free_handles.insert(free_handle.clone()) {
777                        writeln!(body).ok();
778                        writeln!(
779                            body,
780                            "    static final MethodHandle {} = LINKER.downcallHandle(",
781                            free_handle
782                        )
783                        .ok();
784                        writeln!(body, "        LIB.find(\"{}\").orElseThrow(),", free_ffi).ok();
785                        writeln!(body, "        FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)").ok();
786                        writeln!(body, "    );").ok();
787                    }
788                }
789            }
790        }
791    }
792
793    // Collect builder class names from record types with defaults, so we skip
794    // opaque types that are superseded by a pure-Java builder class.
795    let builder_class_names: AHashSet<String> = api
796        .types
797        .iter()
798        .filter(|t| !t.is_opaque && !t.fields.is_empty() && t.has_default)
799        .map(|t| format!("{}Builder", t.name))
800        .collect();
801
802    // Free handles for opaque types (handle pointer → void)
803    for typ in api.types.iter().filter(|typ| !typ.is_trait) {
804        if typ.is_opaque && !builder_class_names.contains(&typ.name) {
805            let type_snake = typ.name.to_snake_case();
806            let type_upper = type_snake.to_uppercase();
807            let free_handle = format!("{}_{}_FREE", prefix.to_uppercase(), type_upper);
808            let free_ffi = format!("{}_{}_free", prefix, type_snake);
809            if emitted_free_handles.insert(free_handle.clone()) {
810                writeln!(body).ok();
811                writeln!(
812                    body,
813                    "    static final MethodHandle {} = LINKER.downcallHandle(",
814                    free_handle
815                )
816                .ok();
817                writeln!(body, "        LIB.find(\"{}\").orElseThrow(),", free_ffi).ok();
818                writeln!(body, "        FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)").ok();
819                writeln!(body, "    );").ok();
820            }
821        }
822    }
823
824    writeln!(body, "}}").ok();
825
826    // Now assemble the file with only the imports that are actually used in the body.
827    let mut out = String::with_capacity(body.len() + 512);
828
829    writeln!(out, "// DO NOT EDIT - auto-generated by alef").ok();
830    writeln!(out, "package {};", package).ok();
831    writeln!(out).ok();
832    if body.contains("Arena") {
833        writeln!(out, "import java.lang.foreign.Arena;").ok();
834    }
835    if body.contains("FunctionDescriptor") {
836        writeln!(out, "import java.lang.foreign.FunctionDescriptor;").ok();
837    }
838    if body.contains("Linker") {
839        writeln!(out, "import java.lang.foreign.Linker;").ok();
840    }
841    if body.contains("MemorySegment") {
842        writeln!(out, "import java.lang.foreign.MemorySegment;").ok();
843    }
844    if body.contains("SymbolLookup") {
845        writeln!(out, "import java.lang.foreign.SymbolLookup;").ok();
846    }
847    if body.contains("ValueLayout") {
848        writeln!(out, "import java.lang.foreign.ValueLayout;").ok();
849    }
850    if body.contains("MethodHandle") {
851        writeln!(out, "import java.lang.invoke.MethodHandle;").ok();
852    }
853    // Imports required by the JAR-extraction native loader (always present).
854    writeln!(out, "import java.net.URL;").ok();
855    writeln!(out, "import java.nio.file.Files;").ok();
856    writeln!(out, "import java.nio.file.Path;").ok();
857    writeln!(out, "import java.nio.file.StandardCopyOption;").ok();
858    writeln!(out, "import java.util.ArrayList;").ok();
859    writeln!(out, "import java.util.Enumeration;").ok();
860    writeln!(out, "import java.util.List;").ok();
861    writeln!(out, "import java.util.jar.JarEntry;").ok();
862    writeln!(out, "import java.util.jar.JarFile;").ok();
863    writeln!(out).ok();
864
865    out.push_str(&body);
866
867    out
868}
869
870// ---------------------------------------------------------------------------
871// Main wrapper class
872// ---------------------------------------------------------------------------
873
874fn gen_main_class(api: &ApiSurface, _config: &AlefConfig, package: &str, class_name: &str, prefix: &str) -> String {
875    // Build the set of opaque type names so we can distinguish opaque handles from records
876    let opaque_types: AHashSet<String> = api
877        .types
878        .iter()
879        .filter(|t| t.is_opaque)
880        .map(|t| t.name.clone())
881        .collect();
882
883    // Generate the class body first, then scan it to determine which imports are needed.
884    let mut body = String::with_capacity(4096);
885
886    writeln!(body, "public final class {} {{", class_name).ok();
887    writeln!(body, "    private {}() {{ }}", class_name).ok();
888    writeln!(body).ok();
889
890    // Generate static methods for free functions
891    for func in &api.functions {
892        // Always generate sync method
893        gen_sync_function_method(&mut body, func, prefix, class_name, &opaque_types);
894        writeln!(body).ok();
895
896        // Also generate async wrapper if marked as async
897        if func.is_async {
898            gen_async_wrapper_method(&mut body, func);
899            writeln!(body).ok();
900        }
901    }
902
903    // Add helper methods only if they are referenced in the body
904    gen_helper_methods(&mut body, prefix, class_name);
905
906    writeln!(body, "}}").ok();
907
908    // Now assemble the file with only the imports that are actually used in the body.
909    let mut out = String::with_capacity(body.len() + 512);
910
911    writeln!(out, "// DO NOT EDIT - auto-generated by alef").ok();
912    writeln!(out, "package {};", package).ok();
913    writeln!(out).ok();
914    if body.contains("Arena") {
915        writeln!(out, "import java.lang.foreign.Arena;").ok();
916    }
917    if body.contains("FunctionDescriptor") {
918        writeln!(out, "import java.lang.foreign.FunctionDescriptor;").ok();
919    }
920    if body.contains("Linker") {
921        writeln!(out, "import java.lang.foreign.Linker;").ok();
922    }
923    if body.contains("MemorySegment") {
924        writeln!(out, "import java.lang.foreign.MemorySegment;").ok();
925    }
926    if body.contains("SymbolLookup") {
927        writeln!(out, "import java.lang.foreign.SymbolLookup;").ok();
928    }
929    if body.contains("ValueLayout") {
930        writeln!(out, "import java.lang.foreign.ValueLayout;").ok();
931    }
932    if body.contains("List<") {
933        writeln!(out, "import java.util.List;").ok();
934    }
935    if body.contains("Map<") {
936        writeln!(out, "import java.util.Map;").ok();
937    }
938    if body.contains("Optional<") {
939        writeln!(out, "import java.util.Optional;").ok();
940    }
941    if body.contains("HashMap<") || body.contains("new HashMap") {
942        writeln!(out, "import java.util.HashMap;").ok();
943    }
944    if body.contains("CompletableFuture") {
945        writeln!(out, "import java.util.concurrent.CompletableFuture;").ok();
946    }
947    if body.contains("CompletionException") {
948        writeln!(out, "import java.util.concurrent.CompletionException;").ok();
949    }
950    // Only import the short name `ObjectMapper` when it's used as a type reference (not just via
951    // `createObjectMapper()` which uses fully qualified names internally).
952    // Check for " ObjectMapper" (space before) which indicates use as a type, not a method name suffix.
953    if body.contains(" ObjectMapper") {
954        writeln!(out, "import com.fasterxml.jackson.databind.ObjectMapper;").ok();
955    }
956    writeln!(out).ok();
957
958    out.push_str(&body);
959
960    out
961}
962
963fn gen_sync_function_method(
964    out: &mut String,
965    func: &FunctionDef,
966    prefix: &str,
967    class_name: &str,
968    opaque_types: &AHashSet<String>,
969) {
970    let params: Vec<String> = func
971        .params
972        .iter()
973        .map(|p| {
974            let ptype = java_type(&p.ty);
975            format!("{} {}", ptype, to_java_name(&p.name))
976        })
977        .collect();
978
979    let return_type = java_type(&func.return_type);
980
981    writeln!(
982        out,
983        "    public static {} {}({}) throws {}Exception {{",
984        return_type,
985        to_java_name(&func.name),
986        params.join(", "),
987        class_name
988    )
989    .ok();
990
991    writeln!(out, "        try (var arena = Arena.ofConfined()) {{").ok();
992
993    // Collect non-opaque Named params that need FFI pointer cleanup after the call.
994    // These are Rust-allocated by _from_json and must be freed with _free.
995    let ffi_ptr_params: Vec<(String, String)> = func
996        .params
997        .iter()
998        .filter_map(|p| {
999            let inner_name = match &p.ty {
1000                TypeRef::Named(n) if !opaque_types.contains(n.as_str()) => Some(n.clone()),
1001                TypeRef::Optional(inner) => {
1002                    if let TypeRef::Named(n) = inner.as_ref() {
1003                        if !opaque_types.contains(n.as_str()) {
1004                            Some(n.clone())
1005                        } else {
1006                            None
1007                        }
1008                    } else {
1009                        None
1010                    }
1011                }
1012                _ => None,
1013            };
1014            inner_name.map(|type_name| {
1015                let cname = "c".to_string() + &to_java_name(&p.name);
1016                let type_snake = type_name.to_snake_case();
1017                let free_handle = format!("NativeLib.{}_{}_FREE", prefix.to_uppercase(), type_snake.to_uppercase());
1018                (cname, free_handle)
1019            })
1020        })
1021        .collect();
1022
1023    // Marshal parameters (use camelCase Java names)
1024    for param in &func.params {
1025        marshal_param_to_ffi(out, &to_java_name(&param.name), &param.ty, opaque_types, prefix);
1026    }
1027
1028    // Call FFI
1029    let ffi_handle = format!("NativeLib.{}_{}", prefix.to_uppercase(), func.name.to_uppercase());
1030
1031    let call_args: Vec<String> = func
1032        .params
1033        .iter()
1034        .map(|p| ffi_param_name(&to_java_name(&p.name), &p.ty, opaque_types))
1035        .collect();
1036
1037    // Emit a helper closure to free FFI-allocated param pointers (e.g. options created by _from_json)
1038    let emit_ffi_ptr_cleanup = |out: &mut String| {
1039        for (cname, free_handle) in &ffi_ptr_params {
1040            writeln!(out, "            if (!{}.equals(MemorySegment.NULL)) {{", cname).ok();
1041            writeln!(out, "                {}.invoke({});", free_handle, cname).ok();
1042            writeln!(out, "            }}").ok();
1043        }
1044    };
1045
1046    if matches!(func.return_type, TypeRef::Unit) {
1047        writeln!(out, "            {}.invoke({});", ffi_handle, call_args.join(", ")).ok();
1048        emit_ffi_ptr_cleanup(out);
1049        writeln!(out, "        }} catch (Throwable e) {{").ok();
1050        writeln!(
1051            out,
1052            "            throw new {}Exception(\"FFI call failed\", e);",
1053            class_name
1054        )
1055        .ok();
1056        writeln!(out, "        }}").ok();
1057    } else if is_ffi_string_return(&func.return_type) {
1058        let free_handle = format!("NativeLib.{}_FREE_STRING", prefix.to_uppercase());
1059        writeln!(
1060            out,
1061            "            var resultPtr = (MemorySegment) {}.invoke({});",
1062            ffi_handle,
1063            call_args.join(", ")
1064        )
1065        .ok();
1066        emit_ffi_ptr_cleanup(out);
1067        writeln!(out, "            if (resultPtr.equals(MemorySegment.NULL)) {{").ok();
1068        writeln!(out, "                checkLastError();").ok();
1069        writeln!(out, "                return null;").ok();
1070        writeln!(out, "            }}").ok();
1071        writeln!(
1072            out,
1073            "            String result = resultPtr.reinterpret(Long.MAX_VALUE).getString(0);"
1074        )
1075        .ok();
1076        writeln!(out, "            {}.invoke(resultPtr);", free_handle).ok();
1077        writeln!(out, "            return result;").ok();
1078        writeln!(out, "        }} catch (Throwable e) {{").ok();
1079        writeln!(
1080            out,
1081            "            throw new {}Exception(\"FFI call failed\", e);",
1082            class_name
1083        )
1084        .ok();
1085        writeln!(out, "        }}").ok();
1086    } else if matches!(func.return_type, TypeRef::Named(_)) {
1087        // Named return types: FFI returns a struct pointer.
1088        let return_type_name = match &func.return_type {
1089            TypeRef::Named(name) => name,
1090            _ => unreachable!(),
1091        };
1092        let is_opaque = opaque_types.contains(return_type_name.as_str());
1093
1094        writeln!(
1095            out,
1096            "            var resultPtr = (MemorySegment) {}.invoke({});",
1097            ffi_handle,
1098            call_args.join(", ")
1099        )
1100        .ok();
1101        emit_ffi_ptr_cleanup(out);
1102        writeln!(out, "            if (resultPtr.equals(MemorySegment.NULL)) {{").ok();
1103        writeln!(out, "                checkLastError();").ok();
1104        writeln!(out, "                return null;").ok();
1105        writeln!(out, "            }}").ok();
1106
1107        if is_opaque {
1108            // Opaque handles: wrap the raw pointer directly, caller owns and will close()
1109            writeln!(out, "            return new {}(resultPtr);", return_type_name).ok();
1110        } else {
1111            // Record types: use _to_json to serialize the full struct to JSON, then deserialize.
1112            // NOTE: _content only returns the markdown string field, not a full JSON object.
1113            let type_snake = return_type_name.to_snake_case();
1114            let free_handle = format!("NativeLib.{}_{}_FREE", prefix.to_uppercase(), type_snake.to_uppercase());
1115            let to_json_handle = format!(
1116                "NativeLib.{}_{}_TO_JSON",
1117                prefix.to_uppercase(),
1118                type_snake.to_uppercase()
1119            );
1120            writeln!(
1121                out,
1122                "            var jsonPtr = (MemorySegment) {}.invoke(resultPtr);",
1123                to_json_handle
1124            )
1125            .ok();
1126            writeln!(out, "            {}.invoke(resultPtr);", free_handle).ok();
1127            writeln!(out, "            if (jsonPtr.equals(MemorySegment.NULL)) {{").ok();
1128            writeln!(out, "                checkLastError();").ok();
1129            writeln!(out, "                return null;").ok();
1130            writeln!(out, "            }}").ok();
1131            writeln!(
1132                out,
1133                "            String json = jsonPtr.reinterpret(Long.MAX_VALUE).getString(0);"
1134            )
1135            .ok();
1136            writeln!(
1137                out,
1138                "            NativeLib.{}_FREE_STRING.invoke(jsonPtr);",
1139                prefix.to_uppercase()
1140            )
1141            .ok();
1142            writeln!(
1143                out,
1144                "            return createObjectMapper().readValue(json, {}.class);",
1145                return_type_name
1146            )
1147            .ok();
1148        }
1149
1150        writeln!(out, "        }} catch (Throwable e) {{").ok();
1151        writeln!(
1152            out,
1153            "            throw new {}Exception(\"FFI call failed\", e);",
1154            class_name
1155        )
1156        .ok();
1157        writeln!(out, "        }}").ok();
1158    } else if matches!(func.return_type, TypeRef::Vec(_)) {
1159        // Vec return types: FFI returns a JSON string pointer; deserialize into List<T>.
1160        let free_handle = format!("NativeLib.{}_FREE_STRING", prefix.to_uppercase());
1161        writeln!(
1162            out,
1163            "            var resultPtr = (MemorySegment) {}.invoke({});",
1164            ffi_handle,
1165            call_args.join(", ")
1166        )
1167        .ok();
1168        emit_ffi_ptr_cleanup(out);
1169        writeln!(out, "            if (resultPtr.equals(MemorySegment.NULL)) {{").ok();
1170        writeln!(out, "                return java.util.List.of();").ok();
1171        writeln!(out, "            }}").ok();
1172        writeln!(
1173            out,
1174            "            String json = resultPtr.reinterpret(Long.MAX_VALUE).getString(0);"
1175        )
1176        .ok();
1177        writeln!(out, "            {}.invoke(resultPtr);", free_handle).ok();
1178        // Determine the element type for deserialization
1179        let element_type = match &func.return_type {
1180            TypeRef::Vec(inner) => java_type(inner),
1181            _ => unreachable!(),
1182        };
1183        writeln!(
1184            out,
1185            "            return createObjectMapper().readValue(json, new com.fasterxml.jackson.core.type.TypeReference<java.util.List<{}>>() {{ }});",
1186            element_type
1187        )
1188        .ok();
1189        writeln!(out, "        }} catch (Throwable e) {{").ok();
1190        writeln!(
1191            out,
1192            "            throw new {}Exception(\"FFI call failed\", e);",
1193            class_name
1194        )
1195        .ok();
1196        writeln!(out, "        }}").ok();
1197    } else {
1198        writeln!(
1199            out,
1200            "            var primitiveResult = ({}) {}.invoke({});",
1201            java_ffi_return_cast(&func.return_type),
1202            ffi_handle,
1203            call_args.join(", ")
1204        )
1205        .ok();
1206        emit_ffi_ptr_cleanup(out);
1207        writeln!(out, "            return primitiveResult;").ok();
1208        writeln!(out, "        }} catch (Throwable e) {{").ok();
1209        writeln!(
1210            out,
1211            "            throw new {}Exception(\"FFI call failed\", e);",
1212            class_name
1213        )
1214        .ok();
1215        writeln!(out, "        }}").ok();
1216    }
1217
1218    writeln!(out, "    }}").ok();
1219}
1220
1221fn gen_async_wrapper_method(out: &mut String, func: &FunctionDef) {
1222    let params: Vec<String> = func
1223        .params
1224        .iter()
1225        .map(|p| {
1226            let ptype = java_type(&p.ty);
1227            format!("{} {}", ptype, to_java_name(&p.name))
1228        })
1229        .collect();
1230
1231    let return_type = match &func.return_type {
1232        TypeRef::Unit => "Void".to_string(),
1233        other => java_boxed_type(other).to_string(),
1234    };
1235
1236    let sync_method_name = to_java_name(&func.name);
1237    let async_method_name = format!("{}Async", sync_method_name);
1238    let param_names: Vec<String> = func.params.iter().map(|p| to_java_name(&p.name)).collect();
1239
1240    writeln!(
1241        out,
1242        "    public static CompletableFuture<{}> {}({}) {{",
1243        return_type,
1244        async_method_name,
1245        params.join(", ")
1246    )
1247    .ok();
1248    writeln!(out, "        return CompletableFuture.supplyAsync(() -> {{").ok();
1249    writeln!(out, "            try {{").ok();
1250    writeln!(
1251        out,
1252        "                return {}({});",
1253        sync_method_name,
1254        param_names.join(", ")
1255    )
1256    .ok();
1257    writeln!(out, "            }} catch (Throwable e) {{").ok();
1258    writeln!(out, "                throw new CompletionException(e);").ok();
1259    writeln!(out, "            }}").ok();
1260    writeln!(out, "        }});").ok();
1261    writeln!(out, "    }}").ok();
1262}
1263
1264// ---------------------------------------------------------------------------
1265// Exception class
1266// ---------------------------------------------------------------------------
1267
1268fn gen_exception_class(package: &str, class_name: &str) -> String {
1269    let mut out = String::with_capacity(512);
1270
1271    writeln!(out, "// DO NOT EDIT - auto-generated by alef").ok();
1272    writeln!(out, "package {};", package).ok();
1273    writeln!(out).ok();
1274
1275    writeln!(out, "public class {}Exception extends Exception {{", class_name).ok();
1276    writeln!(out, "    private final int code;").ok();
1277    writeln!(out).ok();
1278    writeln!(out, "    public {}Exception(int code, String message) {{", class_name).ok();
1279    writeln!(out, "        super(message);").ok();
1280    writeln!(out, "        this.code = code;").ok();
1281    writeln!(out, "    }}").ok();
1282    writeln!(out).ok();
1283    writeln!(
1284        out,
1285        "    public {}Exception(String message, Throwable cause) {{",
1286        class_name
1287    )
1288    .ok();
1289    writeln!(out, "        super(message, cause);").ok();
1290    writeln!(out, "        this.code = -1;").ok();
1291    writeln!(out, "    }}").ok();
1292    writeln!(out).ok();
1293    writeln!(out, "    public int getCode() {{").ok();
1294    writeln!(out, "        return code;").ok();
1295    writeln!(out, "    }}").ok();
1296    writeln!(out, "}}").ok();
1297
1298    out
1299}
1300
1301// ---------------------------------------------------------------------------
1302// High-level facade class (public API)
1303// ---------------------------------------------------------------------------
1304
1305fn gen_facade_class(api: &ApiSurface, package: &str, public_class: &str, raw_class: &str, _prefix: &str) -> String {
1306    let mut body = String::with_capacity(4096);
1307
1308    writeln!(body, "public final class {} {{", public_class).ok();
1309    writeln!(body, "    private {}() {{ }}", public_class).ok();
1310    writeln!(body).ok();
1311
1312    // Generate static methods for free functions
1313    for func in &api.functions {
1314        // Sync method
1315        let params: Vec<String> = func
1316            .params
1317            .iter()
1318            .map(|p| {
1319                let ptype = java_type(&p.ty);
1320                format!("{} {}", ptype, to_java_name(&p.name))
1321            })
1322            .collect();
1323
1324        let return_type = java_type(&func.return_type);
1325
1326        if !func.doc.is_empty() {
1327            writeln!(body, "    /**").ok();
1328            for line in func.doc.lines() {
1329                writeln!(body, "     * {}", line).ok();
1330            }
1331            writeln!(body, "     */").ok();
1332        }
1333
1334        writeln!(
1335            body,
1336            "    public static {} {}({}) throws {}Exception {{",
1337            return_type,
1338            to_java_name(&func.name),
1339            params.join(", "),
1340            raw_class
1341        )
1342        .ok();
1343
1344        // Null checks for required parameters
1345        for param in &func.params {
1346            if !param.optional {
1347                let pname = to_java_name(&param.name);
1348                writeln!(
1349                    body,
1350                    "        java.util.Objects.requireNonNull({}, \"{} must not be null\");",
1351                    pname, pname
1352                )
1353                .ok();
1354            }
1355        }
1356
1357        // Delegate to the raw FFI class
1358        let call_args: Vec<String> = func.params.iter().map(|p| to_java_name(&p.name)).collect();
1359
1360        if matches!(func.return_type, TypeRef::Unit) {
1361            writeln!(
1362                body,
1363                "        {}.{}({});",
1364                raw_class,
1365                to_java_name(&func.name),
1366                call_args.join(", ")
1367            )
1368            .ok();
1369        } else {
1370            writeln!(
1371                body,
1372                "        return {}.{}({});",
1373                raw_class,
1374                to_java_name(&func.name),
1375                call_args.join(", ")
1376            )
1377            .ok();
1378        }
1379
1380        writeln!(body, "    }}").ok();
1381        writeln!(body).ok();
1382
1383        // Generate overload without optional params (convenience method)
1384        let has_optional = func.params.iter().any(|p| p.optional);
1385        if has_optional {
1386            let required_params: Vec<String> = func
1387                .params
1388                .iter()
1389                .filter(|p| !p.optional)
1390                .map(|p| {
1391                    let ptype = java_type(&p.ty);
1392                    format!("{} {}", ptype, to_java_name(&p.name))
1393                })
1394                .collect();
1395
1396            writeln!(
1397                body,
1398                "    public static {} {}({}) throws {}Exception {{",
1399                return_type,
1400                to_java_name(&func.name),
1401                required_params.join(", "),
1402                raw_class
1403            )
1404            .ok();
1405
1406            // Build call with null for optional params
1407            let full_args: Vec<String> = func
1408                .params
1409                .iter()
1410                .map(|p| {
1411                    if p.optional {
1412                        "null".to_string()
1413                    } else {
1414                        to_java_name(&p.name)
1415                    }
1416                })
1417                .collect();
1418
1419            if matches!(func.return_type, TypeRef::Unit) {
1420                writeln!(body, "        {}({});", to_java_name(&func.name), full_args.join(", ")).ok();
1421            } else {
1422                writeln!(
1423                    body,
1424                    "        return {}({});",
1425                    to_java_name(&func.name),
1426                    full_args.join(", ")
1427                )
1428                .ok();
1429            }
1430
1431            writeln!(body, "    }}").ok();
1432            writeln!(body).ok();
1433        }
1434    }
1435
1436    writeln!(body, "}}").ok();
1437
1438    // Now assemble the file with imports
1439    let mut out = String::with_capacity(body.len() + 512);
1440
1441    writeln!(out, "// DO NOT EDIT - auto-generated by alef").ok();
1442    writeln!(out, "package {};", package).ok();
1443
1444    // Check what imports are needed based on content
1445    let has_list = body.contains("List<");
1446    let has_map = body.contains("Map<");
1447    let has_optional = body.contains("Optional<");
1448    let has_imports = has_list || has_map || has_optional;
1449
1450    if has_imports {
1451        writeln!(out).ok();
1452        if has_list {
1453            writeln!(out, "import java.util.List;").ok();
1454        }
1455        if has_map {
1456            writeln!(out, "import java.util.Map;").ok();
1457        }
1458        if has_optional {
1459            writeln!(out, "import java.util.Optional;").ok();
1460        }
1461    }
1462
1463    writeln!(out).ok();
1464    out.push_str(&body);
1465
1466    out
1467}
1468
1469// ---------------------------------------------------------------------------
1470// Opaque handle classes
1471// ---------------------------------------------------------------------------
1472
1473fn gen_opaque_handle_class(package: &str, typ: &TypeDef, prefix: &str) -> String {
1474    let mut out = String::with_capacity(1024);
1475    let class_name = &typ.name;
1476    let type_snake = class_name.to_snake_case();
1477
1478    writeln!(out, "// DO NOT EDIT - auto-generated by alef").ok();
1479    writeln!(out, "package {};", package).ok();
1480    writeln!(out).ok();
1481    writeln!(out, "import java.lang.foreign.MemorySegment;").ok();
1482    writeln!(out).ok();
1483
1484    if !typ.doc.is_empty() {
1485        writeln!(out, "/**").ok();
1486        for line in typ.doc.lines() {
1487            writeln!(out, " * {}", line).ok();
1488        }
1489        writeln!(out, " */").ok();
1490    }
1491
1492    writeln!(out, "public class {} implements AutoCloseable {{", class_name).ok();
1493    writeln!(out, "    private final MemorySegment handle;").ok();
1494    writeln!(out).ok();
1495    writeln!(out, "    {}(MemorySegment handle) {{", class_name).ok();
1496    writeln!(out, "        this.handle = handle;").ok();
1497    writeln!(out, "    }}").ok();
1498    writeln!(out).ok();
1499    writeln!(out, "    MemorySegment handle() {{").ok();
1500    writeln!(out, "        return this.handle;").ok();
1501    writeln!(out, "    }}").ok();
1502    writeln!(out).ok();
1503    writeln!(out, "    @Override").ok();
1504    writeln!(out, "    public void close() {{").ok();
1505    writeln!(
1506        out,
1507        "        if (handle != null && !handle.equals(MemorySegment.NULL)) {{"
1508    )
1509    .ok();
1510    writeln!(out, "            try {{").ok();
1511    writeln!(
1512        out,
1513        "                NativeLib.{}_{}_FREE.invoke(handle);",
1514        prefix.to_uppercase(),
1515        type_snake.to_uppercase()
1516    )
1517    .ok();
1518    writeln!(out, "            }} catch (Throwable e) {{").ok();
1519    writeln!(
1520        out,
1521        "                throw new RuntimeException(\"Failed to free {}: \" + e.getMessage(), e);",
1522        class_name
1523    )
1524    .ok();
1525    writeln!(out, "            }}").ok();
1526    writeln!(out, "        }}").ok();
1527    writeln!(out, "    }}").ok();
1528    writeln!(out, "}}").ok();
1529
1530    out
1531}
1532
1533// ---------------------------------------------------------------------------
1534// Record types (Java records)
1535// ---------------------------------------------------------------------------
1536
1537/// Maximum line length before splitting record fields across multiple lines.
1538/// Checkstyle enforces 120 chars; we split at 100 to leave headroom for indentation.
1539const RECORD_LINE_WRAP_THRESHOLD: usize = 100;
1540
1541fn gen_record_type(package: &str, typ: &TypeDef, complex_enums: &AHashSet<String>, lang_rename_all: &str) -> String {
1542    // Generate the record body first, then scan for needed imports.
1543    // For each field, if the language uses camelCase but the JSON key is snake_case
1544    // (the Rust default), annotate with @JsonProperty so Jackson maps correctly.
1545    let field_list: Vec<String> = typ
1546        .fields
1547        .iter()
1548        .map(|f| {
1549            // Complex enums (tagged unions with data) can't be simple Java enums.
1550            // Use Object for flexible Jackson deserialization.
1551            let is_complex = matches!(&f.ty, TypeRef::Named(n) if complex_enums.contains(n.as_str()));
1552            let ftype = if is_complex {
1553                "Object".to_string()
1554            } else if f.optional {
1555                format!("Optional<{}>", java_boxed_type(&f.ty))
1556            } else {
1557                java_type(&f.ty).to_string()
1558            };
1559            let jname = safe_java_field_name(&f.name);
1560            // When the language convention is camelCase but the JSON wire format uses
1561            // snake_case (the Rust/serde default), add an explicit @JsonProperty annotation
1562            // so Jackson serialises/deserialises using the correct snake_case key.
1563            if lang_rename_all == "camelCase" && f.name.contains('_') {
1564                format!("@JsonProperty(\"{}\") {} {}", f.name, ftype, jname)
1565            } else {
1566                format!("{} {}", ftype, jname)
1567            }
1568        })
1569        .collect();
1570
1571    // Build the single-line form to check length and scan for imports.
1572    let single_line = format!("public record {}({}) {{ }}", typ.name, field_list.join(", "));
1573
1574    // Build the actual record declaration, splitting across lines if too long.
1575    let mut record_block = String::new();
1576    if single_line.len() > RECORD_LINE_WRAP_THRESHOLD && field_list.len() > 1 {
1577        writeln!(record_block, "public record {}(", typ.name).ok();
1578        for (i, field) in field_list.iter().enumerate() {
1579            let comma = if i < field_list.len() - 1 { "," } else { "" };
1580            writeln!(record_block, "    {}{}", field, comma).ok();
1581        }
1582        writeln!(record_block, ") {{").ok();
1583    } else {
1584        writeln!(record_block, "public record {}({}) {{", typ.name, field_list.join(", ")).ok();
1585    }
1586
1587    // Add builder() factory method if type has defaults
1588    if typ.has_default {
1589        writeln!(record_block, "    public static {}Builder builder() {{", typ.name).ok();
1590        writeln!(record_block, "        return new {}Builder();", typ.name).ok();
1591        writeln!(record_block, "    }}").ok();
1592    }
1593
1594    writeln!(record_block, "}}").ok();
1595
1596    // Scan the single-line form to determine which imports are needed
1597    let needs_json_property = field_list.iter().any(|f| f.contains("@JsonProperty("));
1598    let mut out = String::with_capacity(record_block.len() + 512);
1599    writeln!(out, "// DO NOT EDIT - auto-generated by alef").ok();
1600    writeln!(out, "package {};", package).ok();
1601    writeln!(out).ok();
1602    if single_line.contains("List<") {
1603        writeln!(out, "import java.util.List;").ok();
1604    }
1605    if single_line.contains("Map<") {
1606        writeln!(out, "import java.util.Map;").ok();
1607    }
1608    if single_line.contains("Optional<") {
1609        writeln!(out, "import java.util.Optional;").ok();
1610    }
1611    if needs_json_property {
1612        writeln!(out, "import com.fasterxml.jackson.annotation.JsonProperty;").ok();
1613    }
1614    writeln!(out).ok();
1615    write!(out, "{}", record_block).ok();
1616
1617    out
1618}
1619
1620// ---------------------------------------------------------------------------
1621// Enum classes
1622// ---------------------------------------------------------------------------
1623
1624/// Apply a serde `rename_all` strategy to a variant name for Java codegen.
1625fn java_apply_rename_all(name: &str, rename_all: Option<&str>) -> String {
1626    match rename_all {
1627        Some("snake_case") => name.to_snake_case(),
1628        Some("camelCase") => name.to_lower_camel_case(),
1629        Some("PascalCase") => name.to_pascal_case(),
1630        Some("SCREAMING_SNAKE_CASE") => name.to_snake_case().to_uppercase(),
1631        Some("lowercase") => name.to_lowercase(),
1632        Some("UPPERCASE") => name.to_uppercase(),
1633        _ => name.to_lowercase(),
1634    }
1635}
1636
1637fn gen_enum_class(package: &str, enum_def: &EnumDef) -> String {
1638    let has_data_variants = enum_def.variants.iter().any(|v| !v.fields.is_empty());
1639
1640    // Tagged union: enum has a serde tag AND data variants → generate sealed interface hierarchy
1641    if enum_def.serde_tag.is_some() && has_data_variants {
1642        return gen_java_tagged_union(package, enum_def);
1643    }
1644
1645    let mut out = String::with_capacity(1024);
1646
1647    writeln!(out, "// DO NOT EDIT - auto-generated by alef").ok();
1648    writeln!(out, "package {};", package).ok();
1649    writeln!(out).ok();
1650    writeln!(out, "import com.fasterxml.jackson.annotation.JsonCreator;").ok();
1651    writeln!(out, "import com.fasterxml.jackson.annotation.JsonValue;").ok();
1652    writeln!(out).ok();
1653
1654    writeln!(out, "public enum {} {{", enum_def.name).ok();
1655
1656    for (i, variant) in enum_def.variants.iter().enumerate() {
1657        let comma = if i < enum_def.variants.len() - 1 { "," } else { ";" };
1658        // Use serde_rename if available, otherwise apply rename_all strategy
1659        let json_name = variant
1660            .serde_rename
1661            .clone()
1662            .unwrap_or_else(|| java_apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
1663        writeln!(out, "    {}(\"{}\"){}", variant.name, json_name, comma).ok();
1664    }
1665
1666    writeln!(out).ok();
1667    writeln!(out, "    private final String value;").ok();
1668    writeln!(out).ok();
1669    writeln!(out, "    {}(String value) {{", enum_def.name).ok();
1670    writeln!(out, "        this.value = value;").ok();
1671    writeln!(out, "    }}").ok();
1672    writeln!(out).ok();
1673    writeln!(out, "    @JsonValue").ok();
1674    writeln!(out, "    public String getValue() {{").ok();
1675    writeln!(out, "        return value;").ok();
1676    writeln!(out, "    }}").ok();
1677    writeln!(out).ok();
1678    writeln!(out, "    @JsonCreator").ok();
1679    writeln!(out, "    public static {} fromValue(String value) {{", enum_def.name).ok();
1680    writeln!(out, "        for ({} e : values()) {{", enum_def.name).ok();
1681    writeln!(out, "            if (e.value.equalsIgnoreCase(value)) {{").ok();
1682    writeln!(out, "                return e;").ok();
1683    writeln!(out, "            }}").ok();
1684    writeln!(out, "        }}").ok();
1685    writeln!(
1686        out,
1687        "        throw new IllegalArgumentException(\"Unknown value: \" + value);"
1688    )
1689    .ok();
1690    writeln!(out, "    }}").ok();
1691
1692    writeln!(out, "}}").ok();
1693
1694    out
1695}
1696
1697/// Generate a Java sealed interface hierarchy for internally tagged enums.
1698///
1699/// Maps `#[serde(tag = "type_field", rename_all = "snake_case")]` Rust enums to
1700/// `@JsonTypeInfo` / `@JsonSubTypes` Java sealed interfaces with record implementations.
1701fn gen_java_tagged_union(package: &str, enum_def: &EnumDef) -> String {
1702    let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
1703
1704    // Collect variant names to detect Java type name conflicts.
1705    // If a variant is named "List", "Map", or "Optional", using those type names
1706    // inside the sealed interface would refer to the nested record, not java.util.*.
1707    // We use fully qualified names in that case.
1708    let variant_names: std::collections::HashSet<&str> = enum_def.variants.iter().map(|v| v.name.as_str()).collect();
1709    let optional_type = if variant_names.contains("Optional") {
1710        "java.util.Optional"
1711    } else {
1712        "Optional"
1713    };
1714
1715    let mut out = String::with_capacity(2048);
1716    writeln!(out, "// DO NOT EDIT - auto-generated by alef").ok();
1717    writeln!(out, "package {};", package).ok();
1718    writeln!(out).ok();
1719    writeln!(out, "import com.fasterxml.jackson.annotation.JsonProperty;").ok();
1720    writeln!(out, "import com.fasterxml.jackson.annotation.JsonSubTypes;").ok();
1721    writeln!(out, "import com.fasterxml.jackson.annotation.JsonTypeInfo;").ok();
1722
1723    // Check if any field types need list/map/optional imports (only when not conflicting)
1724    let needs_list = !variant_names.contains("List")
1725        && enum_def
1726            .variants
1727            .iter()
1728            .any(|v| v.fields.iter().any(|f| matches!(&f.ty, TypeRef::Vec(_))));
1729    let needs_map = !variant_names.contains("Map")
1730        && enum_def
1731            .variants
1732            .iter()
1733            .any(|v| v.fields.iter().any(|f| matches!(&f.ty, TypeRef::Map(_, _))));
1734    let needs_optional =
1735        !variant_names.contains("Optional") && enum_def.variants.iter().any(|v| v.fields.iter().any(|f| f.optional));
1736    if needs_list {
1737        writeln!(out, "import java.util.List;").ok();
1738    }
1739    if needs_map {
1740        writeln!(out, "import java.util.Map;").ok();
1741    }
1742    if needs_optional {
1743        writeln!(out, "import java.util.Optional;").ok();
1744    }
1745    writeln!(out).ok();
1746
1747    // @JsonTypeInfo and @JsonSubTypes annotations
1748    writeln!(
1749        out,
1750        "@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = \"{tag_field}\", visible = false)"
1751    )
1752    .ok();
1753    writeln!(out, "@JsonSubTypes({{").ok();
1754    for (i, variant) in enum_def.variants.iter().enumerate() {
1755        let discriminator = variant
1756            .serde_rename
1757            .clone()
1758            .unwrap_or_else(|| java_apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
1759        let comma = if i < enum_def.variants.len() - 1 { "," } else { "" };
1760        writeln!(
1761            out,
1762            "    @JsonSubTypes.Type(value = {}.{}.class, name = \"{}\"){}",
1763            enum_def.name, variant.name, discriminator, comma
1764        )
1765        .ok();
1766    }
1767    writeln!(out, "}})").ok();
1768    writeln!(out, "public sealed interface {} {{", enum_def.name).ok();
1769
1770    // Nested records for each variant
1771    for variant in &enum_def.variants {
1772        writeln!(out).ok();
1773        if variant.fields.is_empty() {
1774            // Unit variant
1775            writeln!(out, "    record {}() implements {} {{", variant.name, enum_def.name).ok();
1776            writeln!(out, "    }}").ok();
1777        } else {
1778            // Build field list using fully qualified names where variant names shadow imports
1779            let field_parts: Vec<String> = variant
1780                .fields
1781                .iter()
1782                .map(|f| {
1783                    let json_name = f.name.trim_start_matches('_');
1784                    let ftype = if f.optional {
1785                        let inner = java_boxed_type(&f.ty);
1786                        let inner_str = inner.as_ref();
1787                        // Replace "List"/"Map" with fully qualified if conflicting
1788                        let inner_qualified = if inner_str.starts_with("List<") && variant_names.contains("List") {
1789                            inner_str.replacen("List<", "java.util.List<", 1)
1790                        } else if inner_str.starts_with("Map<") && variant_names.contains("Map") {
1791                            inner_str.replacen("Map<", "java.util.Map<", 1)
1792                        } else {
1793                            inner_str.to_string()
1794                        };
1795                        format!("{optional_type}<{inner_qualified}>")
1796                    } else {
1797                        let t = java_type(&f.ty);
1798                        let t_str = t.as_ref();
1799                        if t_str.starts_with("List<") && variant_names.contains("List") {
1800                            t_str.replacen("List<", "java.util.List<", 1)
1801                        } else if t_str.starts_with("Map<") && variant_names.contains("Map") {
1802                            t_str.replacen("Map<", "java.util.Map<", 1)
1803                        } else {
1804                            t_str.to_string()
1805                        }
1806                    };
1807                    let jname = safe_java_field_name(json_name);
1808                    format!("@JsonProperty(\"{json_name}\") {ftype} {jname}")
1809                })
1810                .collect();
1811
1812            let single = format!(
1813                "    record {}({}) implements {} {{ }}",
1814                variant.name,
1815                field_parts.join(", "),
1816                enum_def.name
1817            );
1818
1819            if single.len() > RECORD_LINE_WRAP_THRESHOLD && field_parts.len() > 1 {
1820                writeln!(out, "    record {}(", variant.name).ok();
1821                for (i, fp) in field_parts.iter().enumerate() {
1822                    let comma = if i < field_parts.len() - 1 { "," } else { "" };
1823                    writeln!(out, "        {}{}", fp, comma).ok();
1824                }
1825                writeln!(out, "    ) implements {} {{", enum_def.name).ok();
1826                writeln!(out, "    }}").ok();
1827            } else {
1828                writeln!(
1829                    out,
1830                    "    record {}({}) implements {} {{ }}",
1831                    variant.name,
1832                    field_parts.join(", "),
1833                    enum_def.name
1834                )
1835                .ok();
1836            }
1837        }
1838    }
1839
1840    writeln!(out).ok();
1841    writeln!(out, "}}").ok();
1842    out
1843}
1844
1845// ---------------------------------------------------------------------------
1846// Helper functions for FFI marshalling
1847// ---------------------------------------------------------------------------
1848
1849fn gen_ffi_layout(ty: &TypeRef) -> String {
1850    match ty {
1851        TypeRef::Primitive(prim) => java_ffi_type(prim).to_string(),
1852        TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "ValueLayout.ADDRESS".to_string(),
1853        TypeRef::Bytes => "ValueLayout.ADDRESS".to_string(),
1854        TypeRef::Optional(inner) => gen_ffi_layout(inner),
1855        TypeRef::Vec(_) => "ValueLayout.ADDRESS".to_string(),
1856        TypeRef::Map(_, _) => "ValueLayout.ADDRESS".to_string(),
1857        TypeRef::Named(_) => "ValueLayout.ADDRESS".to_string(),
1858        TypeRef::Unit => "".to_string(),
1859        TypeRef::Duration => "ValueLayout.JAVA_LONG".to_string(),
1860    }
1861}
1862
1863fn marshal_param_to_ffi(out: &mut String, name: &str, ty: &TypeRef, opaque_types: &AHashSet<String>, prefix: &str) {
1864    match ty {
1865        TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => {
1866            let cname = "c".to_string() + name;
1867            writeln!(out, "            var {} = arena.allocateFrom({});", cname, name).ok();
1868        }
1869        TypeRef::Named(type_name) => {
1870            let cname = "c".to_string() + name;
1871            if opaque_types.contains(type_name.as_str()) {
1872                // Opaque handles: pass the inner MemorySegment via .handle()
1873                writeln!(out, "            var {} = {}.handle();", cname, name).ok();
1874            } else {
1875                // Non-opaque named types: serialize to JSON, call _from_json to get FFI pointer.
1876                // The pointer must be freed after the FFI call with _free.
1877                let type_snake = type_name.to_snake_case();
1878                let from_json_handle = format!(
1879                    "NativeLib.{}_{}_FROM_JSON",
1880                    prefix.to_uppercase(),
1881                    type_snake.to_uppercase()
1882                );
1883                let _free_handle = format!("NativeLib.{}_{}_FREE", prefix.to_uppercase(), type_snake.to_uppercase());
1884                writeln!(
1885                    out,
1886                    "            var {}Json = {} != null ? createObjectMapper().writeValueAsString({}) : null;",
1887                    cname, name, name
1888                )
1889                .ok();
1890                writeln!(
1891                    out,
1892                    "            var {}JsonSeg = {}Json != null ? arena.allocateFrom({}Json) : MemorySegment.NULL;",
1893                    cname, cname, cname
1894                )
1895                .ok();
1896                writeln!(out, "            var {} = {}Json != null", cname, cname).ok();
1897                writeln!(
1898                    out,
1899                    "                ? (MemorySegment) {}.invoke({}JsonSeg)",
1900                    from_json_handle, cname
1901                )
1902                .ok();
1903                writeln!(out, "                : MemorySegment.NULL;").ok();
1904            }
1905        }
1906        TypeRef::Optional(inner) => {
1907            // For optional types, marshal the inner type if not null
1908            match inner.as_ref() {
1909                TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => {
1910                    let cname = "c".to_string() + name;
1911                    writeln!(
1912                        out,
1913                        "            var {} = {} != null ? arena.allocateFrom({}) : MemorySegment.NULL;",
1914                        cname, name, name
1915                    )
1916                    .ok();
1917                }
1918                TypeRef::Named(type_name) => {
1919                    let cname = "c".to_string() + name;
1920                    if opaque_types.contains(type_name.as_str()) {
1921                        writeln!(
1922                            out,
1923                            "            var {} = {} != null ? {}.handle() : MemorySegment.NULL;",
1924                            cname, name, name
1925                        )
1926                        .ok();
1927                    } else {
1928                        // Non-opaque named type in Optional: serialize to JSON and call _from_json
1929                        let type_snake = type_name.to_snake_case();
1930                        let from_json_handle = format!(
1931                            "NativeLib.{}_{}_FROM_JSON",
1932                            prefix.to_uppercase(),
1933                            type_snake.to_uppercase()
1934                        );
1935                        writeln!(
1936                            out,
1937                            "            var {}Json = {} != null ? createObjectMapper().writeValueAsString({}) : null;",
1938                            cname, name, name
1939                        )
1940                        .ok();
1941                        writeln!(out, "            var {}JsonSeg = {}Json != null ? arena.allocateFrom({}Json) : MemorySegment.NULL;", cname, cname, cname).ok();
1942                        writeln!(out, "            var {} = {}Json != null", cname, cname).ok();
1943                        writeln!(
1944                            out,
1945                            "                ? (MemorySegment) {}.invoke({}JsonSeg)",
1946                            from_json_handle, cname
1947                        )
1948                        .ok();
1949                        writeln!(out, "                : MemorySegment.NULL;").ok();
1950                    }
1951                }
1952                _ => {
1953                    // Other optional types (primitives) pass through
1954                }
1955            }
1956        }
1957        TypeRef::Vec(_) | TypeRef::Map(_, _) => {
1958            // Vec/Map types: serialize to JSON string, then pass as a C string via arena.
1959            let cname = "c".to_string() + name;
1960            writeln!(
1961                out,
1962                "            var {}Json = createObjectMapper().writeValueAsString({});",
1963                cname, name
1964            )
1965            .ok();
1966            writeln!(out, "            var {} = arena.allocateFrom({}Json);", cname, cname).ok();
1967        }
1968        _ => {
1969            // Primitives and others pass through directly
1970        }
1971    }
1972}
1973
1974fn ffi_param_name(name: &str, ty: &TypeRef, _opaque_types: &AHashSet<String>) -> String {
1975    match ty {
1976        TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "c".to_string() + name,
1977        TypeRef::Named(_) => "c".to_string() + name,
1978        TypeRef::Vec(_) | TypeRef::Map(_, _) => "c".to_string() + name,
1979        TypeRef::Optional(inner) => match inner.as_ref() {
1980            TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json | TypeRef::Named(_) => {
1981                "c".to_string() + name
1982            }
1983            _ => name.to_string(),
1984        },
1985        _ => name.to_string(),
1986    }
1987}
1988
1989/// Build a `FunctionDescriptor` string for a given return layout and parameter layouts.
1990/// Handles void returns (ofVoid) and non-void returns (of) correctly.
1991fn gen_function_descriptor(return_layout: &str, param_layouts: &[String]) -> String {
1992    if return_layout.is_empty() {
1993        // Void return
1994        if param_layouts.is_empty() {
1995            "FunctionDescriptor.ofVoid()".to_string()
1996        } else {
1997            format!("FunctionDescriptor.ofVoid({})", param_layouts.join(", "))
1998        }
1999    } else {
2000        // Non-void return
2001        if param_layouts.is_empty() {
2002            format!("FunctionDescriptor.of({})", return_layout)
2003        } else {
2004            format!("FunctionDescriptor.of({}, {})", return_layout, param_layouts.join(", "))
2005        }
2006    }
2007}
2008
2009/// Returns true if the given return type maps to an FFI ADDRESS that represents a string
2010/// (i.e. the FFI returns `*mut c_char` which must be unmarshaled and freed).
2011fn is_ffi_string_return(ty: &TypeRef) -> bool {
2012    match ty {
2013        TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => true,
2014        TypeRef::Optional(inner) => is_ffi_string_return(inner),
2015        _ => false,
2016    }
2017}
2018
2019/// Returns the appropriate Java cast type for non-string FFI return values.
2020fn java_ffi_return_cast(ty: &TypeRef) -> &'static str {
2021    match ty {
2022        TypeRef::Primitive(prim) => match prim {
2023            PrimitiveType::Bool => "boolean",
2024            PrimitiveType::U8 | PrimitiveType::I8 => "byte",
2025            PrimitiveType::U16 | PrimitiveType::I16 => "short",
2026            PrimitiveType::U32 | PrimitiveType::I32 => "int",
2027            PrimitiveType::U64 | PrimitiveType::I64 | PrimitiveType::Usize | PrimitiveType::Isize => "long",
2028            PrimitiveType::F32 => "float",
2029            PrimitiveType::F64 => "double",
2030        },
2031        TypeRef::Bytes | TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Named(_) => "MemorySegment",
2032        _ => "MemorySegment",
2033    }
2034}
2035
2036fn gen_helper_methods(out: &mut String, prefix: &str, class_name: &str) {
2037    // Only emit helper methods that are actually called in the generated body.
2038    let needs_check_last_error = out.contains("checkLastError()");
2039    let needs_read_cstring = out.contains("readCString(");
2040    let needs_read_bytes = out.contains("readBytes(");
2041    let needs_create_object_mapper = out.contains("createObjectMapper()");
2042
2043    if !needs_check_last_error && !needs_read_cstring && !needs_read_bytes && !needs_create_object_mapper {
2044        return;
2045    }
2046
2047    writeln!(out, "    // Helper methods for FFI marshalling").ok();
2048    writeln!(out).ok();
2049
2050    if needs_check_last_error {
2051        // Reads the last FFI error code and, if non-zero, reads the error message and throws.
2052        // Called immediately after a null-pointer return from an FFI call.
2053        writeln!(out, "    private static void checkLastError() throws Throwable {{").ok();
2054        writeln!(
2055            out,
2056            "        int errCode = (int) NativeLib.{}_LAST_ERROR_CODE.invoke();",
2057            prefix.to_uppercase()
2058        )
2059        .ok();
2060        writeln!(out, "        if (errCode != 0) {{").ok();
2061        writeln!(
2062            out,
2063            "            var ctxPtr = (MemorySegment) NativeLib.{}_LAST_ERROR_CONTEXT.invoke();",
2064            prefix.to_uppercase()
2065        )
2066        .ok();
2067        writeln!(
2068            out,
2069            "            String msg = ctxPtr.reinterpret(Long.MAX_VALUE).getString(0);"
2070        )
2071        .ok();
2072        writeln!(out, "            throw new {}Exception(errCode, msg);", class_name).ok();
2073        writeln!(out, "        }}").ok();
2074        writeln!(out, "    }}").ok();
2075        writeln!(out).ok();
2076    }
2077
2078    if needs_create_object_mapper {
2079        // Emit a configured ObjectMapper factory:
2080        //   - findAndRegisterModules() to pick up jackson-datatype-jdk8 (Optional support)
2081        //   - ACCEPT_CASE_INSENSITIVE_ENUMS so enum names like "json_ld" match JsonLd, etc.
2082        // Field name mapping relies on explicit @JsonProperty annotations on record components
2083        // (generated by alef for snake_case FFI fields on camelCase Java records).
2084        writeln!(
2085            out,
2086            "    private static com.fasterxml.jackson.databind.ObjectMapper createObjectMapper() {{"
2087        )
2088        .ok();
2089        writeln!(out, "        return new com.fasterxml.jackson.databind.ObjectMapper()").ok();
2090        writeln!(
2091            out,
2092            "            .registerModule(new com.fasterxml.jackson.datatype.jdk8.Jdk8Module())"
2093        )
2094        .ok();
2095        writeln!(out, "            .findAndRegisterModules()").ok();
2096        writeln!(
2097            out,
2098            "            .setSerializationInclusion(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL)"
2099        )
2100        .ok();
2101        writeln!(
2102            out,
2103            "            .configure(com.fasterxml.jackson.databind.MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS, true);"
2104        )
2105        .ok();
2106        writeln!(out, "    }}").ok();
2107        writeln!(out).ok();
2108    }
2109
2110    if needs_read_cstring {
2111        writeln!(out, "    private static String readCString(MemorySegment ptr) {{").ok();
2112        writeln!(out, "        if (ptr == null || ptr.address() == 0) {{").ok();
2113        writeln!(out, "            return null;").ok();
2114        writeln!(out, "        }}").ok();
2115        writeln!(out, "        return ptr.getUtf8String(0);").ok();
2116        writeln!(out, "    }}").ok();
2117        writeln!(out).ok();
2118    }
2119
2120    if needs_read_bytes {
2121        writeln!(
2122            out,
2123            "    private static byte[] readBytes(MemorySegment ptr, long len) {{"
2124        )
2125        .ok();
2126        writeln!(out, "        if (ptr == null || ptr.address() == 0) {{").ok();
2127        writeln!(out, "            return new byte[0];").ok();
2128        writeln!(out, "        }}").ok();
2129        writeln!(out, "        byte[] bytes = new byte[(int) len];").ok();
2130        writeln!(
2131            out,
2132            "        MemorySegment.copy(ptr, ValueLayout.JAVA_BYTE.byteSize() * 0, bytes, 0, (int) len);"
2133        )
2134        .ok();
2135        writeln!(out, "        return bytes;").ok();
2136        writeln!(out, "    }}").ok();
2137    }
2138}
2139
2140// ---------------------------------------------------------------------------
2141// Builder class for types with defaults
2142// ---------------------------------------------------------------------------
2143
2144/// Format a default value for an Optional field, wrapping it in Optional.of()
2145/// with proper Java literal syntax.
2146fn format_optional_value(ty: &TypeRef, default: &str) -> String {
2147    // Check if the default is already wrapped (e.g., "Optional.of(...)" or "Optional.empty()")
2148    if default.contains("Optional.") {
2149        return default.to_string();
2150    }
2151
2152    // Unwrap Optional types to get the inner type
2153    let inner_ty = match ty {
2154        TypeRef::Optional(inner) => inner.as_ref(),
2155        other => other,
2156    };
2157
2158    // Determine the proper literal suffix based on type
2159    let formatted_value = match inner_ty {
2160        TypeRef::Primitive(p) => match p {
2161            PrimitiveType::I64 | PrimitiveType::U64 | PrimitiveType::Isize | PrimitiveType::Usize => {
2162                // Add 'L' suffix for long values if not already present
2163                if default.ends_with('L') || default.ends_with('l') {
2164                    default.to_string()
2165                } else if default.parse::<i64>().is_ok() {
2166                    format!("{}L", default)
2167                } else {
2168                    default.to_string()
2169                }
2170            }
2171            PrimitiveType::F32 => {
2172                // Add 'f' suffix for float values if not already present
2173                if default.ends_with('f') || default.ends_with('F') {
2174                    default.to_string()
2175                } else if default.parse::<f32>().is_ok() {
2176                    format!("{}f", default)
2177                } else {
2178                    default.to_string()
2179                }
2180            }
2181            PrimitiveType::F64 => {
2182                // Double defaults can have optional 'd' suffix, but 0.0 is fine
2183                default.to_string()
2184            }
2185            _ => default.to_string(),
2186        },
2187        _ => default.to_string(),
2188    };
2189
2190    format!("Optional.of({})", formatted_value)
2191}
2192
2193fn gen_builder_class(package: &str, typ: &TypeDef) -> String {
2194    let mut body = String::with_capacity(2048);
2195
2196    writeln!(body, "public class {}Builder {{", typ.name).ok();
2197    writeln!(body).ok();
2198
2199    // Generate field declarations with defaults
2200    for field in &typ.fields {
2201        let field_name = safe_java_field_name(&field.name);
2202
2203        // Skip unnamed tuple fields (name is "_0", "_1", "0", "1", etc.) — Java requires named fields
2204        if field.name.starts_with('_') && field.name[1..].chars().all(|c| c.is_ascii_digit())
2205            || field.name.chars().next().is_none_or(|c| c.is_ascii_digit())
2206        {
2207            continue;
2208        }
2209
2210        // Duration maps to primitive `long` in the public record, but in builder
2211        // classes we use boxed `Long` so that `null` can represent "not set".
2212        let field_type = if field.optional {
2213            format!("Optional<{}>", java_boxed_type(&field.ty))
2214        } else if matches!(field.ty, TypeRef::Duration) {
2215            java_boxed_type(&field.ty).to_string()
2216        } else {
2217            java_type(&field.ty).to_string()
2218        };
2219
2220        let default_value = if field.optional {
2221            // For Optional fields, always use Optional.empty() or Optional.of(value)
2222            if let Some(default) = &field.default {
2223                // If there's an explicit default, wrap it in Optional.of()
2224                format_optional_value(&field.ty, default)
2225            } else {
2226                // If no default, use Optional.empty()
2227                "Optional.empty()".to_string()
2228            }
2229        } else {
2230            // For non-Optional fields, use regular defaults
2231            if let Some(default) = &field.default {
2232                default.clone()
2233            } else {
2234                match &field.ty {
2235                    TypeRef::String | TypeRef::Char | TypeRef::Path => "\"\"".to_string(),
2236                    TypeRef::Json => "null".to_string(),
2237                    TypeRef::Bytes => "new byte[0]".to_string(),
2238                    TypeRef::Primitive(p) => match p {
2239                        PrimitiveType::Bool => "false".to_string(),
2240                        PrimitiveType::F32 | PrimitiveType::F64 => "0.0".to_string(),
2241                        _ => "0".to_string(),
2242                    },
2243                    TypeRef::Vec(_) => "List.of()".to_string(),
2244                    TypeRef::Map(_, _) => "Map.of()".to_string(),
2245                    TypeRef::Optional(_) => "Optional.empty()".to_string(),
2246                    TypeRef::Duration => "null".to_string(),
2247                    _ => "null".to_string(),
2248                }
2249            }
2250        };
2251
2252        writeln!(body, "    private {} {} = {};", field_type, field_name, default_value).ok();
2253    }
2254
2255    writeln!(body).ok();
2256
2257    // Generate withXxx() methods
2258    for field in &typ.fields {
2259        // Skip unnamed tuple fields (name is "_0", "_1", "0", "1", etc.) — Java requires named fields
2260        if field.name.starts_with('_') && field.name[1..].chars().all(|c| c.is_ascii_digit())
2261            || field.name.chars().next().is_none_or(|c| c.is_ascii_digit())
2262        {
2263            continue;
2264        }
2265
2266        let field_name = safe_java_field_name(&field.name);
2267        let field_name_pascal = to_class_name(&field.name);
2268        let field_type = if field.optional {
2269            format!("Optional<{}>", java_boxed_type(&field.ty))
2270        } else if matches!(field.ty, TypeRef::Duration) {
2271            java_boxed_type(&field.ty).to_string()
2272        } else {
2273            java_type(&field.ty).to_string()
2274        };
2275
2276        writeln!(
2277            body,
2278            "    public {}Builder with{}({} value) {{",
2279            typ.name, field_name_pascal, field_type
2280        )
2281        .ok();
2282        writeln!(body, "        this.{} = value;", field_name).ok();
2283        writeln!(body, "        return this;").ok();
2284        writeln!(body, "    }}").ok();
2285        writeln!(body).ok();
2286    }
2287
2288    // Generate build() method
2289    writeln!(body, "    public {} build() {{", typ.name).ok();
2290    writeln!(body, "        return new {}(", typ.name).ok();
2291    let non_tuple_fields: Vec<_> = typ
2292        .fields
2293        .iter()
2294        .filter(|f| {
2295            // Include named fields (skip unnamed tuple fields)
2296            !(f.name.starts_with('_') && f.name[1..].chars().all(|c| c.is_ascii_digit())
2297                || f.name.chars().next().is_none_or(|c| c.is_ascii_digit()))
2298        })
2299        .collect();
2300    for (i, field) in non_tuple_fields.iter().enumerate() {
2301        let field_name = safe_java_field_name(&field.name);
2302        let comma = if i < non_tuple_fields.len() - 1 { "," } else { "" };
2303        writeln!(body, "            {}{}", field_name, comma).ok();
2304    }
2305    writeln!(body, "        );").ok();
2306    writeln!(body, "    }}").ok();
2307
2308    writeln!(body, "}}").ok();
2309
2310    // Now assemble with conditional imports based on what's actually used in the body
2311    let mut out = String::with_capacity(body.len() + 512);
2312
2313    writeln!(out, "// DO NOT EDIT - auto-generated by alef").ok();
2314    writeln!(out, "package {};", package).ok();
2315    writeln!(out).ok();
2316
2317    if body.contains("List<") {
2318        writeln!(out, "import java.util.List;").ok();
2319    }
2320    if body.contains("Map<") {
2321        writeln!(out, "import java.util.Map;").ok();
2322    }
2323    if body.contains("Optional<") {
2324        writeln!(out, "import java.util.Optional;").ok();
2325    }
2326
2327    writeln!(out).ok();
2328    out.push_str(&body);
2329
2330    out
2331}