boomslang_hostgen/
lib.rs

1//! Code generator for typed boomslang host extensions.
2//!
3//! An extension declares host functions that Python code (running on CPython
4//! compiled to WASM) can call into the embedding Java or Rust host. The
5//! declaration lives in the extension crate's `build.rs`, written with the
6//! builder DSL in this crate: [`ExtensionSpec`] describes the extension and
7//! its [functions](FunctionSpec), and [`Build`] selects which artifacts to
8//! emit before [`Build::generate`] writes them out. The primary artifacts are
9//! a Rust guest module (PyO3 wrappers plus WASM imports, written to
10//! `OUT_DIR`) and an ABI JSON contract; the ABI JSON in turn drives host
11//! adapter generation — either via the `boomslang-hostgen` CLI or the
12//! [`generate_java`] / [`generate_rust_host`] functions — so hosts can be
13//! regenerated without rebuilding the guest.
14//!
15//! # Examples
16//!
17//! A typical `build.rs`:
18//!
19//! ```ignore
20//! use boomslang_hostgen::{Build, ExtensionSpec, Type};
21//!
22//! fn main() -> Result<(), Box<dyn std::error::Error>> {
23//!     let ext = ExtensionSpec::new("myext")
24//!         .wasm_module("myext")
25//!         .prewarm(["myext_support"])
26//!         .function("do_thing", |f| {
27//!             f.param("input", Type::String).returns(Type::String)
28//!         });
29//!
30//!     Build::new(ext).emit().generate()
31//! }
32//! ```
33//!
34//! The extension crate then consumes the generated guest module with
35//! `include!(concat!(env!("OUT_DIR"), "/ext_myext.rs"));` and calls the
36//! included `register()` function to add the Python module to CPython's
37//! init table.
38
39use askama::Template;
40use heck::{ToLowerCamelCase, ToSnakeCase, ToUpperCamelCase};
41use serde::{Deserialize, Serialize};
42use std::env;
43use std::error::Error;
44use std::fs;
45use std::path::{Path, PathBuf};
46
47const ABI_VERSION: u32 = 1;
48
49fn default_abi_version() -> u32 {
50    ABI_VERSION
51}
52
53/// The full extension contract: ABI version, extension metadata, and the
54/// list of host functions.
55///
56/// This is the (de)serialized form of the ABI JSON file. Build it with
57/// [`ExtensionSpec`] rather than constructing it by hand, or load one from
58/// disk with [`read_abi`]. Manifests are validated before any code is
59/// generated: the [`abi_version`](Manifest::abi_version) must exactly match
60/// the version this crate supports (currently `1`), every name must be a
61/// plain ASCII identifier that is not a Rust/Java keyword, function names
62/// must not collide with the reserved `__async_*` control names, and async
63/// functions must return [`Type::String`].
64#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
65pub struct Manifest {
66    /// ABI contract version. Generation fails unless this exactly matches
67    /// the version supported by this crate (currently `1`).
68    #[serde(default = "default_abi_version")]
69    pub abi_version: u32,
70    /// Extension-level metadata (name, WASM import module, prewarm list).
71    pub extension: Extension,
72    /// Host functions exposed to Python.
73    #[serde(default)]
74    pub functions: Vec<Function>,
75}
76
77/// Extension-level metadata within a [`Manifest`].
78#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
79pub struct Extension {
80    /// Extension name; must be a valid ASCII identifier. Used to derive the
81    /// Python module name (`_<name>`), generated file names, and host class
82    /// names.
83    pub name: String,
84    /// WASM import module the host functions are linked under. Defaults to
85    /// [`name`](Extension::name) when absent.
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub wasm_module: Option<String>,
88    /// Python modules to import eagerly during Wizer pre-initialization.
89    #[serde(default)]
90    pub prewarm: Vec<String>,
91}
92
93/// A single host function exposed to Python.
94#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
95pub struct Function {
96    /// Function name; must be a valid ASCII identifier and must not be one
97    /// of the reserved `__async_*` control names.
98    pub name: String,
99    /// Typed parameters, in call order.
100    #[serde(default)]
101    pub params: Vec<Param>,
102    /// Return type, or `None` for a void function.
103    #[serde(default)]
104    pub returns: Option<Type>,
105    /// Whether the function is asynchronous. Async functions must currently
106    /// return [`Type::String`]; at the WASM level they return an `i64` token
107    /// that the guest resolves to an awaitable via `boomslang_host.asyncio`.
108    #[serde(default)]
109    pub r#async: bool,
110}
111
112/// A typed parameter of a [`Function`].
113#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
114pub struct Param {
115    /// Parameter name; must be a valid ASCII identifier.
116    pub name: String,
117    /// Parameter type (serialized as `"type"` in ABI JSON).
118    #[serde(rename = "type")]
119    pub ty: Type,
120}
121
122/// Value types supported across the guest/host boundary.
123///
124/// Each variant has a fixed lowering to the WASM ABI. Buffer-typed
125/// ([`String`](Type::String)/[`Bytes`](Type::Bytes)) parameters are passed as
126/// an `i32` pointer plus `i32` length into guest linear memory. Buffer-typed
127/// returns use a caller-provided result buffer: the guest passes a result
128/// pointer and maximum length, and the host writes the payload and returns
129/// the written length as `i32` (negative values signal failure). Async
130/// functions instead return an `i64` token (see
131/// [`Function::r#async`](Function#structfield.async)).
132#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
133#[serde(rename_all = "lowercase")]
134pub enum Type {
135    /// UTF-8 string. Lowered to `i32` pointer + `i32` length as a parameter;
136    /// as a return value, written into the caller's result buffer with the
137    /// written length returned as `i32`.
138    String,
139    /// 32-bit signed integer, lowered to `i32`.
140    Int,
141    /// 64-bit float, lowered to `f64`.
142    Float,
143    /// Raw byte buffer. Lowered to `i32` pointer + `i32` length as a
144    /// parameter; as a return value, written into the caller's result buffer
145    /// with the written length returned as `i32`.
146    Bytes,
147}
148
149/// Builder for an extension [`Manifest`], used from `build.rs`.
150///
151/// Start with [`ExtensionSpec::new`], chain configuration calls, declare
152/// functions with [`function`](ExtensionSpec::function), and hand the result
153/// to [`Build::new`]. See the [crate-level example](crate).
154#[derive(Clone, Debug)]
155pub struct ExtensionSpec {
156    manifest: Manifest,
157}
158
159impl ExtensionSpec {
160    /// Creates a spec for an extension with the given name at the current
161    /// ABI version. The name must be a valid ASCII identifier (validated at
162    /// [`Build::generate`] time).
163    pub fn new(name: impl Into<String>) -> Self {
164        Self {
165            manifest: Manifest {
166                abi_version: ABI_VERSION,
167                extension: Extension {
168                    name: name.into(),
169                    wasm_module: None,
170                    prewarm: Vec::new(),
171                },
172                functions: Vec::new(),
173            },
174        }
175    }
176
177    /// Sets the WASM import module the host functions are linked under.
178    /// Defaults to the extension name when not set.
179    pub fn wasm_module(mut self, wasm_module: impl Into<String>) -> Self {
180        self.manifest.extension.wasm_module = Some(wasm_module.into());
181        self
182    }
183
184    /// Sets the Python modules to import eagerly during Wizer
185    /// pre-initialization, replacing any previously configured list.
186    pub fn prewarm<I, S>(mut self, modules: I) -> Self
187    where
188        I: IntoIterator<Item = S>,
189        S: Into<String>,
190    {
191        self.manifest.extension.prewarm = modules.into_iter().map(Into::into).collect();
192        self
193    }
194
195    /// Declares a host function. The closure configures the function's
196    /// [params](FunctionSpec::param), [return type](FunctionSpec::returns),
197    /// and [asyncness](FunctionSpec#method.async).
198    pub fn function<F>(mut self, name: impl Into<String>, configure: F) -> Self
199    where
200        F: FnOnce(FunctionSpec) -> FunctionSpec,
201    {
202        self.manifest
203            .functions
204            .push(configure(FunctionSpec::new(name)).build());
205        self
206    }
207
208    /// Returns the manifest built so far.
209    pub fn manifest(&self) -> &Manifest {
210        &self.manifest
211    }
212
213    /// Consumes the spec, returning the built manifest.
214    pub fn into_manifest(self) -> Manifest {
215        self.manifest
216    }
217}
218
219/// Builder for a single [`Function`], used inside
220/// [`ExtensionSpec::function`].
221#[derive(Clone, Debug)]
222pub struct FunctionSpec {
223    function: Function,
224}
225
226impl FunctionSpec {
227    fn new(name: impl Into<String>) -> Self {
228        Self {
229            function: Function {
230                name: name.into(),
231                params: Vec::new(),
232                returns: None,
233                r#async: false,
234            },
235        }
236    }
237
238    /// Appends a typed parameter. The name must be a valid ASCII identifier
239    /// (validated at [`Build::generate`] time).
240    pub fn param(mut self, name: impl Into<String>, ty: Type) -> Self {
241        self.function.params.push(Param {
242            name: name.into(),
243            ty,
244        });
245        self
246    }
247
248    /// Sets the return type. Omit for a void function.
249    pub fn returns(mut self, ty: Type) -> Self {
250        self.function.returns = Some(ty);
251        self
252    }
253
254    /// Marks the function asynchronous. Async functions must also declare
255    /// `returns(Type::String)`; validation fails otherwise. See
256    /// [`Function::r#async`](Function#structfield.async) for the runtime
257    /// token protocol.
258    pub fn r#async(mut self) -> Self {
259        self.function.r#async = true;
260        self
261    }
262
263    fn build(self) -> Function {
264        self.function
265    }
266}
267
268/// Selects which artifacts to generate from an [`ExtensionSpec`].
269///
270/// Each `emit_*` method enables an output; nothing is written until
271/// [`generate`](Build::generate) is called. Most `build.rs` files only need
272/// `Build::new(ext).emit().generate()`.
273pub struct Build {
274    manifest: Manifest,
275    rust_guest: bool,
276    abi_json: bool,
277    abi_json_to: Option<PathBuf>,
278    java_host: Option<(PathBuf, String)>,
279    rust_host: Option<PathBuf>,
280}
281
282impl Build {
283    /// Creates a build for the given extension with no outputs enabled.
284    pub fn new(extension: ExtensionSpec) -> Self {
285        Self {
286            manifest: extension.into_manifest(),
287            rust_guest: false,
288            abi_json: false,
289            abi_json_to: None,
290            java_host: None,
291            rust_host: None,
292        }
293    }
294
295    /// Enables the default outputs: shorthand for
296    /// [`emit_rust_guest`](Build::emit_rust_guest) followed by
297    /// [`emit_abi_json`](Build::emit_abi_json).
298    pub fn emit(self) -> Self {
299        self.emit_rust_guest().emit_abi_json()
300    }
301
302    /// Alias for [`emit`](Build::emit).
303    pub fn emit_defaults(self) -> Self {
304        self.emit()
305    }
306
307    /// Enables the Rust guest module, written to
308    /// `$OUT_DIR/ext_<name>.rs` for consumption via `include!`.
309    pub fn emit_rust_guest(mut self) -> Self {
310        self.rust_guest = true;
311        self
312    }
313
314    /// Enables the ABI JSON contract, written to
315    /// `$OUT_DIR/<name>.abi.json`. Use
316    /// [`emit_abi_json_to`](Build::emit_abi_json_to) when a stable,
317    /// build-independent path is needed.
318    pub fn emit_abi_json(mut self) -> Self {
319        self.abi_json = true;
320        self
321    }
322
323    /// Additionally writes the ABI JSON to the given path (parent
324    /// directories are created). Useful for checking the contract into the
325    /// repo or feeding it to the CLI from a stable location.
326    pub fn emit_abi_json_to(mut self, path: impl Into<PathBuf>) -> Self {
327        self.abi_json_to = Some(path.into());
328        self
329    }
330
331    /// Enables the Java (Chicory) host adapter, written as
332    /// `<Name>HostFunctions.java` under `out_dir` in the given package's
333    /// directory layout. Note this writes directly into the given Java
334    /// source tree from `build.rs`; running the `boomslang-hostgen` CLI
335    /// against the emitted ABI JSON after the build is generally preferable.
336    pub fn emit_java_host(
337        mut self,
338        out_dir: impl Into<PathBuf>,
339        package: impl Into<String>,
340    ) -> Self {
341        self.java_host = Some((out_dir.into(), package.into()));
342        self
343    }
344
345    /// Enables the Rust (Wasmtime) host adapter, written as
346    /// `host_<name>.rs` under `out_dir`. As with
347    /// [`emit_java_host`](Build::emit_java_host), generating hosts via the
348    /// CLI after the build is generally preferable.
349    pub fn emit_rust_host(mut self, out_dir: impl Into<PathBuf>) -> Self {
350        self.rust_host = Some(out_dir.into());
351        self
352    }
353
354    /// Validates the manifest and writes all enabled outputs.
355    ///
356    /// Validation enforces the exact ABI version (currently `1`), ASCII
357    /// identifier rules for all names, the reserved `__async_*` function
358    /// names, and that async functions return [`Type::String`].
359    /// Guest and default ABI JSON outputs require the `OUT_DIR` environment
360    /// variable, which Cargo sets when running `build.rs`.
361    pub fn generate(self) -> Result<(), Box<dyn Error>> {
362        validate_manifest(&self.manifest)?;
363
364        if self.rust_guest {
365            let out_dir = out_dir()?;
366            write_rust_guest(&self.manifest, &out_dir)?;
367        }
368
369        if self.abi_json {
370            let out_dir = out_dir()?;
371            write_abi_json(
372                &self.manifest,
373                &out_dir.join(format!("{}.abi.json", self.manifest.extension.name)),
374            )?;
375        }
376
377        if let Some(path) = &self.abi_json_to {
378            write_abi_json(&self.manifest, path)?;
379        }
380
381        if let Some((out_dir, package)) = &self.java_host {
382            write_java_host(&self.manifest, out_dir, package)?;
383        }
384
385        if let Some(out_dir) = &self.rust_host {
386            write_rust_host(&self.manifest, out_dir)?;
387        }
388
389        Ok(())
390    }
391}
392
393fn out_dir() -> Result<PathBuf, Box<dyn Error>> {
394    Ok(PathBuf::from(env::var("OUT_DIR")?))
395}
396
397fn write_rust_guest(manifest: &Manifest, out_dir: &Path) -> Result<(), Box<dyn Error>> {
398    let filename = format!("ext_{}.rs", manifest.extension.name);
399    fs::write(out_dir.join(filename), generate_rust_code(manifest))?;
400    Ok(())
401}
402
403fn write_abi_json(manifest: &Manifest, path: &Path) -> Result<(), Box<dyn Error>> {
404    if let Some(parent) = path
405        .parent()
406        .filter(|parent| !parent.as_os_str().is_empty())
407    {
408        fs::create_dir_all(parent)?;
409    }
410    fs::write(path, serde_json::to_string_pretty(manifest)? + "\n")?;
411    Ok(())
412}
413
414fn write_java_host(
415    manifest: &Manifest,
416    out_dir: &Path,
417    package: &str,
418) -> Result<(), Box<dyn Error>> {
419    let package_dir = out_dir.join(package.replace('.', "/"));
420    fs::create_dir_all(&package_dir)?;
421    let code = generate_java_code(&manifest, package);
422    let classname = format!(
423        "{}HostFunctions",
424        manifest.extension.name.to_upper_camel_case()
425    );
426    fs::write(package_dir.join(format!("{}.java", classname)), code)?;
427    Ok(())
428}
429
430fn write_rust_host(manifest: &Manifest, out_dir: &Path) -> Result<(), Box<dyn Error>> {
431    fs::create_dir_all(out_dir)?;
432    let filename = format!("host_{}.rs", manifest.extension.name);
433    fs::write(out_dir.join(filename), generate_rust_host_code(manifest)?)?;
434    Ok(())
435}
436
437/// Reads and validates an ABI JSON file (see [`Manifest`] for the
438/// validation rules, including the exact ABI version check).
439pub fn read_abi(path: &Path) -> Result<Manifest, Box<dyn Error>> {
440    let content = fs::read_to_string(path)?;
441    let manifest: Manifest = serde_json::from_str(&content)?;
442    validate_manifest(&manifest)?;
443    Ok(manifest)
444}
445
446/// Generates Java (Chicory) host function bindings from an ABI JSON file.
447///
448/// Reads and validates the manifest at `abi_path`, then writes
449/// `<Name>HostFunctions.java` under the Java source root `out_dir`, in the
450/// directory layout implied by the dot-separated `package` name. This is
451/// the library entry point behind the CLI's `--java-out`/`--java-package`
452/// flags.
453pub fn generate_java(abi_path: &str, out_dir: &str, package: &str) -> Result<(), Box<dyn Error>> {
454    let manifest = read_abi(Path::new(abi_path))?;
455    write_java_host(&manifest, Path::new(out_dir), package)
456}
457
458/// Generates Rust (Wasmtime) host function bindings from an ABI JSON file.
459///
460/// Reads and validates the manifest at `abi_path`, then writes
461/// `host_<name>.rs` into `out_dir` (created if needed). This is the library
462/// entry point behind the CLI's `--rust-host-out` flag.
463pub fn generate_rust_host(abi_path: &str, out_dir: &str) -> Result<(), Box<dyn Error>> {
464    let manifest = read_abi(Path::new(abi_path))?;
465    write_rust_host(&manifest, Path::new(out_dir))
466}
467
468fn validate_manifest(manifest: &Manifest) -> Result<(), Box<dyn Error>> {
469    if manifest.abi_version != ABI_VERSION {
470        return Err(format!(
471            "unsupported ABI version {}; expected {}",
472            manifest.abi_version, ABI_VERSION
473        )
474        .into());
475    }
476    if manifest.extension.name.trim().is_empty() {
477        return Err("extension name is required".into());
478    }
479    validate_identifier("extension name", &manifest.extension.name)?;
480    for function in &manifest.functions {
481        if function.name.trim().is_empty() {
482            return Err("function name is required".into());
483        }
484        validate_identifier("function name", &function.name)?;
485        if is_reserved_async_control_name(&function.name) {
486            return Err(format!("function name '{}' is reserved", function.name).into());
487        }
488        if function.r#async && function.returns != Some(Type::String) {
489            return Err(format!(
490                "async function '{}' must currently return string",
491                function.name
492            )
493            .into());
494        }
495        for param in &function.params {
496            if param.name.trim().is_empty() {
497                return Err(format!("parameter name is required for {}", function.name).into());
498            }
499            validate_identifier(
500                &format!("parameter name for function '{}'", function.name),
501                &param.name,
502            )?;
503        }
504    }
505    Ok(())
506}
507
508fn validate_identifier(kind: &str, name: &str) -> Result<(), Box<dyn Error>> {
509    if !is_valid_identifier(name) {
510        return Err(format!(
511            "{} '{}' must be an ASCII Rust/Java identifier: start with a letter or underscore, continue with letters, digits, or underscores, and avoid reserved keywords",
512            kind, name
513        )
514        .into());
515    }
516    Ok(())
517}
518
519fn is_valid_identifier(name: &str) -> bool {
520    let mut chars = name.chars();
521    let Some(first) = chars.next() else {
522        return false;
523    };
524    if !(first == '_' || first.is_ascii_alphabetic()) {
525        return false;
526    }
527    if !chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric()) {
528        return false;
529    }
530    !is_reserved_identifier(name)
531}
532
533fn is_reserved_identifier(name: &str) -> bool {
534    const RESERVED: &[&str] = &[
535        "abstract",
536        "as",
537        "assert",
538        "async",
539        "await",
540        "become",
541        "boolean",
542        "box",
543        "break",
544        "byte",
545        "case",
546        "catch",
547        "char",
548        "class",
549        "const",
550        "continue",
551        "crate",
552        "default",
553        "do",
554        "double",
555        "dyn",
556        "else",
557        "enum",
558        "extends",
559        "extern",
560        "false",
561        "final",
562        "finally",
563        "float",
564        "fn",
565        "for",
566        "gen",
567        "goto",
568        "if",
569        "impl",
570        "implements",
571        "import",
572        "in",
573        "instanceof",
574        "int",
575        "interface",
576        "let",
577        "long",
578        "loop",
579        "macro",
580        "match",
581        "mod",
582        "move",
583        "mut",
584        "native",
585        "new",
586        "null",
587        "override",
588        "package",
589        "priv",
590        "private",
591        "protected",
592        "pub",
593        "public",
594        "ref",
595        "return",
596        "self",
597        "Self",
598        "short",
599        "static",
600        "strictfp",
601        "struct",
602        "super",
603        "switch",
604        "synchronized",
605        "this",
606        "throw",
607        "throws",
608        "trait",
609        "transient",
610        "true",
611        "try",
612        "type",
613        "typeof",
614        "union",
615        "unsafe",
616        "unsized",
617        "use",
618        "virtual",
619        "void",
620        "volatile",
621        "where",
622        "while",
623        "yield",
624    ];
625    RESERVED.contains(&name)
626}
627
628fn is_reserved_async_control_name(name: &str) -> bool {
629    matches!(
630        name,
631        "__async_protocol__"
632            | "__async_start__"
633            | "__async_poll__"
634            | "__async_result__"
635            | "__async_cancel__"
636    )
637}
638
639// ── Rust codegen ──────────────────────────────────────────────
640
641/// Renders the Rust guest module source for a manifest: WASM import
642/// declarations, PyO3 wrapper functions, and the `register()`/`prewarm()`
643/// entry points. [`Build::emit_rust_guest`] writes this to
644/// `$OUT_DIR/ext_<name>.rs`; unlike [`Build::generate`], this function does
645/// not validate the manifest.
646pub fn generate_rust_code(m: &Manifest) -> String {
647    let mod_name = &m.extension.name;
648    let wasm_mod = m.extension.wasm_module.as_deref().unwrap_or(mod_name);
649
650    let mut out = String::new();
651    out.push_str("use pyo3::prelude::*;\n\n");
652
653    // WASM import declarations
654    if !m.functions.is_empty() {
655        out.push_str(&format!("#[link(wasm_import_module = \"{}\")]\n", wasm_mod));
656        out.push_str("unsafe extern \"C\" {\n");
657        for f in &m.functions {
658            out.push_str(&format!("    fn {}(\n", wasm_import_name(f)));
659            for p in &f.params {
660                for (wn, wt) in wasm_params(&p.name, p.ty) {
661                    out.push_str(&format!("        {}: {},\n", wn, wt));
662                }
663            }
664            if !f.r#async && (f.returns == Some(Type::String) || f.returns == Some(Type::Bytes)) {
665                out.push_str("        result_ptr: *mut u8,\n");
666                out.push_str("        result_max_len: i32,\n");
667            }
668            let ret = wasm_return_type(f);
669            out.push_str(&format!("    ) -> {};\n", ret));
670        }
671        out.push_str("}\n\n");
672    }
673
674    out.push_str("const MAX_RESULT: i32 = 1024 * 1024;\n");
675    out.push_str("/// Host ABI sentinel: the result exceeded the caller-provided result buffer.\n");
676    out.push_str("const HOST_CALL_RESULT_TOO_LARGE: i32 = -2;\n\n");
677
678    // PyO3 wrapper functions
679    for f in &m.functions {
680        out.push_str(&generate_rust_pyo3_wrapper(f));
681        out.push_str("\n");
682    }
683
684    // Forward-declare the PyInit function generated by #[pymodule]
685    let py_mod_name = format!("_{}", mod_name);
686    out.push_str(&format!(
687        "unsafe extern \"C\" {{\n    #[allow(non_snake_case)]\n    fn PyInit_{}() -> *mut pyo3::ffi::PyObject;\n}}\n\n",
688        py_mod_name
689    ));
690
691    // Module registration
692    out.push_str(&format!(
693        "pub fn register() {{\n    unsafe {{\n        pyo3::ffi::PyImport_AppendInittab(\n            b\"{}\\0\".as_ptr() as *const i8,\n            Some(PyInit_{}),\n        );\n    }}\n}}\n\n",
694        py_mod_name, py_mod_name
695    ));
696
697    out.push_str(&format!(
698        "pub fn prewarm(py: Python) {{\n    let modules = {:?};\n    for name in modules {{\n        match py.import(name) {{\n            Ok(_) => eprintln!(\"[prewarm] OK: {{}}\", name),\n            Err(e) => eprintln!(\"[prewarm] FAILED: {{}} - {{:?}}\", name, e),\n        }}\n    }}\n}}\n\n",
699        m.extension.prewarm
700    ));
701
702    // #[pymodule]
703    out.push_str(&format!(
704        "#[pymodule]\nfn {}(m: &Bound<'_, PyModule>) -> PyResult<()> {{\n",
705        py_mod_name
706    ));
707    for f in &m.functions {
708        out.push_str(&format!(
709            "    m.add_function(wrap_pyfunction!(py_{}, m)?)?;\n",
710            f.name
711        ));
712    }
713    out.push_str("    Ok(())\n}\n");
714
715    out
716}
717
718/// Emits the shared `ret`-handling preamble for buffer-typed (String/Bytes)
719/// sync host calls. The host ABI signals failure with negative return values
720/// (see the Java and Rust host templates): `-2` (emitted as the
721/// `HOST_CALL_RESULT_TOO_LARGE` const in the generated module) means the payload
722/// exceeded the caller's `result_max_len` buffer; any other negative value is a
723/// host-side failure. A non-negative `ret` is the number of bytes written.
724fn buffer_return_error_handling() -> String {
725    let mut out = String::new();
726    out.push_str("        if ret == HOST_CALL_RESULT_TOO_LARGE {\n");
727    out.push_str("            return Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(\n");
728    out.push_str(
729        "                format!(\"host call result exceeded the {MAX_RESULT}-byte limit\")));\n",
730    );
731    out.push_str("        }\n");
732    out.push_str("        if ret < 0 {\n");
733    out.push_str("            return Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(\"host call failed\"));\n");
734    out.push_str("        }\n");
735    out
736}
737
738fn generate_rust_pyo3_wrapper(f: &Function) -> String {
739    if f.r#async {
740        return generate_rust_async_pyo3_wrapper(f);
741    }
742
743    let mut out = String::new();
744
745    out.push_str(&format!("#[pyfunction]\n#[pyo3(name = \"{}\")]\n", f.name));
746    let ret_type = match f.returns {
747        Some(Type::String) => "PyResult<String>",
748        Some(Type::Int) => "PyResult<i32>",
749        Some(Type::Float) => "PyResult<f64>",
750        Some(Type::Bytes) => "PyResult<Vec<u8>>",
751        None => "PyResult<()>",
752    };
753    out.push_str(&format!("fn py_{}(", f.name));
754    let py_params: Vec<String> = f
755        .params
756        .iter()
757        .map(|p| format!("{}: {}", &p.name, rust_py_type(p.ty)))
758        .collect();
759    out.push_str(&py_params.join(", "));
760    out.push_str(&format!(") -> {} {{\n", ret_type));
761    out.push_str("    unsafe {\n");
762
763    for p in &f.params {
764        if p.ty == Type::String {
765            out.push_str(&format!(
766                "        let {name}_bytes = {name}.as_bytes();\n",
767                name = p.name
768            ));
769        } else if p.ty == Type::Bytes {
770            out.push_str(&format!(
771                "        let {name}_bytes = {name};\n",
772                name = p.name
773            ));
774        }
775    }
776
777    if f.returns == Some(Type::String) || f.returns == Some(Type::Bytes) {
778        out.push_str("        let mut result_buf = vec![0u8; MAX_RESULT as usize];\n");
779    }
780
781    if f.returns.is_some() {
782        out.push_str(&format!("        let ret = {}(\n", wasm_import_name(f)));
783    } else {
784        out.push_str(&format!("        {}(\n", wasm_import_name(f)));
785    }
786    for p in &f.params {
787        match p.ty {
788            Type::String | Type::Bytes => {
789                out.push_str(&format!(
790                    "            {name}_bytes.as_ptr(),\n            {name}_bytes.len() as i32,\n",
791                    name = p.name
792                ));
793            }
794            Type::Int | Type::Float => {
795                out.push_str(&format!("            {},\n", p.name));
796            }
797        }
798    }
799    if f.returns == Some(Type::String) || f.returns == Some(Type::Bytes) {
800        out.push_str("            result_buf.as_mut_ptr(),\n");
801        out.push_str("            MAX_RESULT,\n");
802    }
803    out.push_str("        );\n");
804
805    match f.returns {
806        Some(Type::String) => {
807            out.push_str(&buffer_return_error_handling());
808            out.push_str(
809                "        Ok(String::from_utf8_lossy(&result_buf[..ret as usize]).into_owned())\n",
810            );
811        }
812        Some(Type::Bytes) => {
813            out.push_str(&buffer_return_error_handling());
814            out.push_str("        Ok(result_buf[..ret as usize].to_vec())\n");
815        }
816        Some(Type::Int) => out.push_str("        Ok(ret)\n"),
817        None => out.push_str("        Ok(())\n"),
818        Some(Type::Float) => out.push_str("        Ok(ret)\n"),
819    }
820
821    out.push_str("    }\n}\n");
822    out
823}
824
825fn generate_rust_async_pyo3_wrapper(f: &Function) -> String {
826    validate_async_function(f);
827    let mut out = String::new();
828    out.push_str(&format!("#[pyfunction]\n#[pyo3(name = \"{}\")]\n", f.name));
829    out.push_str(&format!("fn py_{}(py: Python", f.name));
830    for p in &f.params {
831        out.push_str(&format!(", {}: {}", p.name, rust_py_type(p.ty)));
832    }
833    out.push_str(") -> PyResult<Py<PyAny>> {\n");
834    out.push_str("    unsafe {\n");
835    for p in &f.params {
836        if p.ty == Type::String {
837            out.push_str(&format!(
838                "        let {name}_bytes = {name}.as_bytes();\n",
839                name = p.name
840            ));
841        } else if p.ty == Type::Bytes {
842            out.push_str(&format!(
843                "        let {name}_bytes = {name};\n",
844                name = p.name
845            ));
846        }
847    }
848    out.push_str(&format!("        let token = {}(\n", wasm_import_name(f)));
849    for p in &f.params {
850        match p.ty {
851            Type::String | Type::Bytes => {
852                out.push_str(&format!(
853                    "            {name}_bytes.as_ptr(),\n            {name}_bytes.len() as i32,\n",
854                    name = p.name
855                ));
856            }
857            Type::Int | Type::Float => {
858                out.push_str(&format!("            {},\n", p.name));
859            }
860        }
861    }
862    out.push_str("        );\n");
863    // Defense in depth: tokens are always positive, so a negative return means the host could not
864    // even register the call. Fail loudly here rather than handing a bogus token to the event loop
865    // (boomslang_host.asyncio also rejects non-positive tokens).
866    out.push_str("        if token < 0 {\n");
867    out.push_str("            return Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(\"async host call failed\"));\n");
868    out.push_str("        }\n");
869    out.push_str("        let asyncio = py.import(\"boomslang_host.asyncio\")?;\n");
870    out.push_str("        let from_host_token = asyncio.getattr(\"from_host_token\")?;\n");
871    out.push_str("        let future = from_host_token.call1((token,))?;\n");
872    out.push_str("        Ok(future.unbind())\n");
873    out.push_str("    }\n}\n");
874    out
875}
876
877fn validate_async_function(f: &Function) {
878    if f.returns != Some(Type::String) {
879        panic!("Async function '{}' must currently return string", f.name);
880    }
881}
882
883fn wasm_import_name(f: &Function) -> String {
884    f.name.clone()
885}
886
887fn wasm_params(name: &str, ty: Type) -> Vec<(String, &'static str)> {
888    match ty {
889        Type::String | Type::Bytes => vec![
890            (format!("{}_ptr", name), "*const u8"),
891            (format!("{}_len", name), "i32"),
892        ],
893        Type::Int => vec![(name.to_string(), "i32")],
894        Type::Float => vec![(name.to_string(), "f64")],
895    }
896}
897
898fn wasm_return_type(f: &Function) -> &'static str {
899    if f.r#async {
900        validate_async_function(f);
901        return "i64";
902    }
903    match f.returns {
904        Some(Type::String) | Some(Type::Bytes) | Some(Type::Int) => "i32",
905        Some(Type::Float) => "f64",
906        None => "()",
907    }
908}
909
910fn rust_py_type(ty: Type) -> &'static str {
911    match ty {
912        Type::String => "&str",
913        Type::Int => "i32",
914        Type::Float => "f64",
915        Type::Bytes => "&[u8]",
916    }
917}
918
919// ── Rust Wasmtime host codegen ──────────────────────────────────────────────
920
921/// Renders the Rust (Wasmtime) host adapter source for a manifest: a
922/// `<Name>HostFunctions` struct with a builder for wiring typed handler
923/// closures into a Wasmtime linker. Validates the manifest first.
924pub fn generate_rust_host_code(m: &Manifest) -> Result<String, Box<dyn Error>> {
925    validate_manifest(m)?;
926
927    let ext_name = &m.extension.name;
928    let struct_name = rust_host_struct_name(ext_name);
929    let builder_name = format!("{}Builder", struct_name);
930    let template = RustHostTemplate {
931        struct_name,
932        builder_name,
933        extension_name: rust_string_literal(ext_name),
934        wasm_module: rust_string_literal(m.extension.wasm_module.as_deref().unwrap_or(ext_name)),
935        imports: rust_host_imports(m),
936        functions: m
937            .functions
938            .iter()
939            .map(rust_host_function_template)
940            .collect(),
941    };
942
943    Ok(template.render().map(normalize_template_code)?)
944}
945
946#[derive(Template)]
947#[template(path = "rust_host.rs", escape = "none")]
948struct RustHostTemplate {
949    struct_name: String,
950    builder_name: String,
951    extension_name: String,
952    wasm_module: String,
953    imports: String,
954    functions: Vec<RustHostFunctionTemplate>,
955}
956
957struct RustHostFunctionTemplate {
958    name: String,
959    import_name: String,
960    field: String,
961    arc_handler_type: String,
962    fn_bound: String,
963    with_method: String,
964    register_method: String,
965    wasm_params: String,
966    wasm_returns: String,
967    results_name: &'static str,
968    param_reads: String,
969    return_handling: String,
970    error_handling: String,
971    needs_memory: bool,
972}
973
974fn rust_host_function_template(f: &Function) -> RustHostFunctionTemplate {
975    RustHostFunctionTemplate {
976        name: f.name.clone(),
977        import_name: rust_string_literal(&f.name),
978        field: rust_host_field_name(f),
979        arc_handler_type: rust_host_arc_handler_type(f),
980        fn_bound: rust_host_fn_bound(f),
981        with_method: rust_host_with_method(f),
982        register_method: rust_host_register_method(f),
983        wasm_params: rust_host_valtype_vec(rust_host_wasmtime_params(f)),
984        wasm_returns: rust_host_valtype_vec(rust_host_wasmtime_returns(f)),
985        results_name: if f.r#async || f.returns.is_some() {
986            "results"
987        } else {
988            "_results"
989        },
990        param_reads: rust_host_param_reads(f),
991        return_handling: rust_host_return_handling(f),
992        error_handling: rust_host_error_handling(f),
993        needs_memory: needs_memory(f),
994    }
995}
996
997fn rust_host_imports(m: &Manifest) -> String {
998    let ext_name = &m.extension.name;
999    let wasm_module = m.extension.wasm_module.as_deref().unwrap_or(ext_name);
1000    let imports = m
1001        .functions
1002        .iter()
1003        .map(|f| rust_string_literal(&format!("{}::{}", wasm_module, f.name)))
1004        .collect::<Vec<_>>()
1005        .join(", ");
1006    format!("vec![{}]", imports)
1007}
1008
1009fn rust_host_param_reads(f: &Function) -> String {
1010    let mut out = String::new();
1011    let mut arg_idx = 0;
1012    for (param_idx, p) in f.params.iter().enumerate() {
1013        let rust_name = format!("param{}", param_idx);
1014        match p.ty {
1015            Type::String => {
1016                out.push_str(&format!(
1017                    "                    let {name} = read_string(&caller, memory, expect_i32(params, {ptr_idx}, {ptr_name})?, expect_i32(params, {len_idx}, {len_name})?)?;\n",
1018                    name = rust_name,
1019                    ptr_idx = arg_idx,
1020                    ptr_name = rust_string_literal(&format!("{}_ptr", p.name)),
1021                    len_idx = arg_idx + 1,
1022                    len_name = rust_string_literal(&format!("{}_len", p.name))
1023                ));
1024                arg_idx += 2;
1025            }
1026            Type::Bytes => {
1027                out.push_str(&format!(
1028                    "                    let {name} = read_bytes(&caller, memory, expect_i32(params, {ptr_idx}, {ptr_name})?, expect_i32(params, {len_idx}, {len_name})?)?;\n",
1029                    name = rust_name,
1030                    ptr_idx = arg_idx,
1031                    ptr_name = rust_string_literal(&format!("{}_ptr", p.name)),
1032                    len_idx = arg_idx + 1,
1033                    len_name = rust_string_literal(&format!("{}_len", p.name))
1034                ));
1035                arg_idx += 2;
1036            }
1037            Type::Int => {
1038                out.push_str(&format!(
1039                    "                    let {} = expect_i32(params, {}, {})?;\n",
1040                    rust_name,
1041                    arg_idx,
1042                    rust_string_literal(&p.name)
1043                ));
1044                arg_idx += 1;
1045            }
1046            Type::Float => {
1047                out.push_str(&format!(
1048                    "                    let {} = expect_f64(params, {}, {})?;\n",
1049                    rust_name,
1050                    arg_idx,
1051                    rust_string_literal(&p.name)
1052                ));
1053                arg_idx += 1;
1054            }
1055        }
1056    }
1057
1058    if !f.r#async && is_buffer_return(f.returns) {
1059        out.push_str(&format!(
1060            "                    let result_ptr = expect_i32(params, {}, \"result_ptr\")?;\n",
1061            arg_idx
1062        ));
1063        out.push_str(&format!(
1064            "                    let result_max_len = expect_i32(params, {}, \"result_max_len\")?;\n",
1065            arg_idx + 1
1066        ));
1067    }
1068
1069    out
1070}
1071
1072fn rust_host_return_handling(f: &Function) -> String {
1073    let call_args = (0..f.params.len())
1074        .map(|idx| format!("param{}", idx))
1075        .collect::<Vec<_>>()
1076        .join(", ");
1077    let call = format!("handler({})", call_args);
1078
1079    if f.r#async {
1080        validate_async_function(f);
1081        return format!(
1082            "                    let token = {}?;\n                    results[0] = Val::I64(token);\n",
1083            call
1084        );
1085    }
1086
1087    match f.returns {
1088        Some(Type::String) => format!(
1089            "                    let result = {}?;\n                    write_buffer_result(&mut caller, memory, result_ptr, result_max_len, result.as_bytes(), results)?;\n",
1090            call
1091        ),
1092        Some(Type::Bytes) => format!(
1093            "                    let result = {}?;\n                    write_buffer_result(&mut caller, memory, result_ptr, result_max_len, &result, results)?;\n",
1094            call
1095        ),
1096        Some(Type::Int) => format!(
1097            "                    let result = {}?;\n                    results[0] = Val::I32(result);\n",
1098            call
1099        ),
1100        Some(Type::Float) => format!(
1101            "                    let result = {}?;\n                    results[0] = Val::F64(result.to_bits());\n",
1102            call
1103        ),
1104        None => format!("                    {}?;\n", call),
1105    }
1106}
1107
1108fn rust_host_error_handling(f: &Function) -> String {
1109    if f.r#async {
1110        format!(
1111            "                if let Err(error) = result {{\n                    eprintln!(\"async host function {{}}::{} failed: {{error:#}}\", Self::MODULE);\n                    results[0] = Val::I64(-1);\n                }}\n                Ok(())\n",
1112            f.name
1113        )
1114    } else if is_buffer_return(f.returns) {
1115        format!(
1116            "                if let Err(error) = result {{\n                    eprintln!(\"host function {{}}::{} failed: {{error:#}}\", Self::MODULE);\n                    results[0] = Val::I32(-1);\n                }}\n                Ok(())\n",
1117            f.name
1118        )
1119    } else {
1120        "                result.map_err(wasmtime::Error::msg)\n".to_string()
1121    }
1122}
1123
1124fn rust_host_struct_name(ext_name: &str) -> String {
1125    format!("{}HostFunctions", ext_name.to_upper_camel_case())
1126}
1127
1128fn rust_host_field_name(f: &Function) -> String {
1129    format!("{}_handler", f.name.to_snake_case())
1130}
1131
1132fn rust_host_with_method(f: &Function) -> String {
1133    format!("with_{}", f.name.to_snake_case())
1134}
1135
1136fn rust_host_register_method(f: &Function) -> String {
1137    format!("register_{}", f.name.to_snake_case())
1138}
1139
1140fn rust_host_arc_handler_type(f: &Function) -> String {
1141    format!("Arc<dyn {} + Send + Sync>", rust_host_fn_bound(f))
1142}
1143
1144fn rust_host_fn_bound(f: &Function) -> String {
1145    let params = f
1146        .params
1147        .iter()
1148        .map(|p| rust_host_value_type(p.ty))
1149        .collect::<Vec<_>>()
1150        .join(", ");
1151    format!("Fn({}) -> Result<{}>", params, rust_host_return_type(f))
1152}
1153
1154fn rust_host_value_type(ty: Type) -> &'static str {
1155    match ty {
1156        Type::String => "String",
1157        Type::Int => "i32",
1158        Type::Float => "f64",
1159        Type::Bytes => "Vec<u8>",
1160    }
1161}
1162
1163fn rust_host_return_type(f: &Function) -> &'static str {
1164    if f.r#async {
1165        validate_async_function(f);
1166        return "i64";
1167    }
1168    match f.returns {
1169        Some(Type::String) => "String",
1170        Some(Type::Int) => "i32",
1171        Some(Type::Float) => "f64",
1172        Some(Type::Bytes) => "Vec<u8>",
1173        None => "()",
1174    }
1175}
1176
1177fn rust_host_wasmtime_params(f: &Function) -> Vec<&'static str> {
1178    let mut params = Vec::new();
1179    for p in &f.params {
1180        match p.ty {
1181            Type::String | Type::Bytes => {
1182                params.push("ValType::I32");
1183                params.push("ValType::I32");
1184            }
1185            Type::Int => params.push("ValType::I32"),
1186            Type::Float => params.push("ValType::F64"),
1187        }
1188    }
1189    if !f.r#async && is_buffer_return(f.returns) {
1190        params.push("ValType::I32");
1191        params.push("ValType::I32");
1192    }
1193    params
1194}
1195
1196fn rust_host_wasmtime_returns(f: &Function) -> Vec<&'static str> {
1197    if f.r#async {
1198        validate_async_function(f);
1199        return vec!["ValType::I64"];
1200    }
1201    match f.returns {
1202        Some(Type::String) | Some(Type::Bytes) | Some(Type::Int) => vec!["ValType::I32"],
1203        Some(Type::Float) => vec!["ValType::F64"],
1204        None => vec![],
1205    }
1206}
1207
1208fn rust_host_valtype_vec(types: Vec<&'static str>) -> String {
1209    if types.is_empty() {
1210        return "vec![]".to_string();
1211    }
1212    if types.len() <= 3 {
1213        return format!("vec![{}]", types.join(", "));
1214    }
1215    format!(
1216        "vec![\n                {},\n            ]",
1217        types.join(",\n                ")
1218    )
1219}
1220
1221fn rust_string_literal(value: &str) -> String {
1222    format!("{:?}", value)
1223}
1224
1225// ── Java codegen ──────────────────────────────────────────────
1226
1227#[derive(Template)]
1228#[template(path = "java_host_functions.java", escape = "none")]
1229struct JavaHostFunctionsTemplate {
1230    package: String,
1231    class_name: String,
1232    extension_name: String,
1233    wasm_module: String,
1234    has_async: bool,
1235    functions: Vec<JavaFunctionTemplate>,
1236}
1237
1238struct JavaFunctionTemplate {
1239    name: String,
1240    upper_name: String,
1241    field: String,
1242    handler_type: String,
1243    with_method: String,
1244    return_type: String,
1245    interface_params: String,
1246    wasm_params: String,
1247    wasm_returns: String,
1248    param_reads: String,
1249    return_handling: String,
1250    error_handling: &'static str,
1251    #[allow(dead_code)]
1252    is_async: bool,
1253    needs_memory: bool,
1254}
1255
1256/// Renders the Java (Chicory) host adapter source for a manifest: a
1257/// `<Name>HostFunctions` class in the given package, with typed handler
1258/// interfaces and Chicory `HostFunction` registrations. Unlike
1259/// [`generate_java`], this function does not validate the manifest.
1260pub fn generate_java_code(m: &Manifest, package: &str) -> String {
1261    let ext_name = &m.extension.name;
1262    let template = JavaHostFunctionsTemplate {
1263        package: package.to_string(),
1264        class_name: format!("{}HostFunctions", ext_name.to_upper_camel_case()),
1265        extension_name: ext_name.to_string(),
1266        wasm_module: m
1267            .extension
1268            .wasm_module
1269            .as_deref()
1270            .unwrap_or(ext_name)
1271            .to_string(),
1272        has_async: m.functions.iter().any(|f| f.r#async),
1273        functions: m.functions.iter().map(java_function_template).collect(),
1274    };
1275
1276    template
1277        .render()
1278        .map(normalize_template_code)
1279        .expect("failed to render Java host functions template")
1280}
1281
1282fn normalize_template_code(code: String) -> String {
1283    let mut out = Vec::new();
1284    let mut previous_blank = false;
1285
1286    for line in code.lines() {
1287        let line = line.trim_end();
1288        if line.is_empty() {
1289            if !previous_blank {
1290                out.push(String::new());
1291            }
1292            previous_blank = true;
1293            continue;
1294        }
1295
1296        out.push(line.to_string());
1297        previous_blank = false;
1298    }
1299
1300    let mut idx = 1;
1301    while idx < out.len() {
1302        if out[idx - 1].is_empty() && out[idx].trim() == "}" {
1303            out.remove(idx - 1);
1304        } else {
1305            idx += 1;
1306        }
1307    }
1308
1309    out.join("\n") + "\n"
1310}
1311
1312fn java_function_template(f: &Function) -> JavaFunctionTemplate {
1313    let field = f.name.to_lower_camel_case();
1314    JavaFunctionTemplate {
1315        name: f.name.clone(),
1316        upper_name: f.name.to_upper_camel_case(),
1317        field: field.clone(),
1318        handler_type: format!("{}Handler", f.name.to_upper_camel_case()),
1319        with_method: format!("with{}", f.name.to_upper_camel_case()),
1320        return_type: java_handler_return_type(f),
1321        interface_params: java_interface_params(f),
1322        wasm_params: java_value_type_list(java_wasm_params(f)),
1323        wasm_returns: java_value_type_list(java_wasm_returns(f)),
1324        param_reads: java_param_reads(f),
1325        return_handling: java_return_handling(f, &field),
1326        error_handling: java_error_handling(f),
1327        is_async: f.r#async,
1328        needs_memory: needs_memory(f),
1329    }
1330}
1331
1332/// Whether the generated host function body touches WASM linear memory: true when it reads any
1333/// string/bytes argument, or writes a string/bytes result back into a caller-provided buffer.
1334/// Functions with only scalar params and no buffer return (e.g. `add`) must NOT declare the
1335/// `Memory` local, otherwise error-prone's UnusedLocalVariable check fails downstream.
1336fn needs_memory(f: &Function) -> bool {
1337    let reads_buffer = f
1338        .params
1339        .iter()
1340        .any(|p| p.ty == Type::String || p.ty == Type::Bytes);
1341    let writes_buffer = !f.r#async && is_buffer_return(f.returns);
1342    reads_buffer || writes_buffer
1343}
1344
1345fn java_interface_params(f: &Function) -> String {
1346    f.params
1347        .iter()
1348        .map(|p| format!("{} {}", java_type(p.ty), p.name.to_lower_camel_case()))
1349        .collect::<Vec<_>>()
1350        .join(", ")
1351}
1352
1353fn java_wasm_params(f: &Function) -> Vec<&'static str> {
1354    let mut params = Vec::new();
1355    for p in &f.params {
1356        match p.ty {
1357            Type::String | Type::Bytes => {
1358                params.push("ValueType.I32");
1359                params.push("ValueType.I32");
1360            }
1361            Type::Int => params.push("ValueType.I32"),
1362            Type::Float => params.push("ValueType.F64"),
1363        }
1364    }
1365    if !f.r#async && is_buffer_return(f.returns) {
1366        params.push("ValueType.I32");
1367        params.push("ValueType.I32");
1368    }
1369    params
1370}
1371
1372fn java_value_type_list(types: Vec<&'static str>) -> String {
1373    if types.is_empty() {
1374        return "List.of()".to_string();
1375    }
1376    if types.len() <= 3 {
1377        return format!("List.of({})", types.join(", "));
1378    }
1379    format!(
1380        "List.of(\n          {}\n        )",
1381        types.join(",\n          ")
1382    )
1383}
1384
1385fn java_wasm_returns(f: &Function) -> Vec<&'static str> {
1386    if f.r#async {
1387        validate_async_function(f);
1388        return vec!["ValueType.I64"];
1389    }
1390    match f.returns {
1391        Some(Type::String) | Some(Type::Bytes) | Some(Type::Int) => vec!["ValueType.I32"],
1392        Some(Type::Float) => vec!["ValueType.F64"],
1393        None => vec![],
1394    }
1395}
1396
1397fn java_param_reads(f: &Function) -> String {
1398    let mut out = String::new();
1399    let mut arg_idx = 0;
1400    for (param_idx, p) in f.params.iter().enumerate() {
1401        let java_name = format!("param{}", param_idx);
1402        match p.ty {
1403            Type::String => {
1404                out.push_str(&format!(
1405                    "          int {name}Ptr = Math.toIntExact(wasmArgs[{i}]);\n          int {name}Len = Math.toIntExact(wasmArgs[{j}]);\n          String {name} = memory.readString({name}Ptr, {name}Len, StandardCharsets.UTF_8);\n",
1406                    name = java_name,
1407                    i = arg_idx,
1408                    j = arg_idx + 1
1409                ));
1410                arg_idx += 2;
1411            }
1412            Type::Bytes => {
1413                out.push_str(&format!(
1414                    "          int {name}Ptr = Math.toIntExact(wasmArgs[{i}]);\n          int {name}Len = Math.toIntExact(wasmArgs[{j}]);\n          byte[] {name} = memory.readBytes({name}Ptr, {name}Len);\n",
1415                    name = java_name,
1416                    i = arg_idx,
1417                    j = arg_idx + 1
1418                ));
1419                arg_idx += 2;
1420            }
1421            Type::Int => {
1422                out.push_str(&format!(
1423                    "          int {} = Math.toIntExact(wasmArgs[{}]);\n",
1424                    java_name, arg_idx
1425                ));
1426                arg_idx += 1;
1427            }
1428            Type::Float => {
1429                out.push_str(&format!(
1430                    "          double {} = Double.longBitsToDouble(wasmArgs[{}]);\n",
1431                    java_name, arg_idx
1432                ));
1433                arg_idx += 1;
1434            }
1435        }
1436    }
1437
1438    if !f.r#async && is_buffer_return(f.returns) {
1439        out.push_str(&format!(
1440            "          int resultPtr = Math.toIntExact(wasmArgs[{}]);\n          int resultMaxLen = Math.toIntExact(wasmArgs[{}]);\n",
1441            arg_idx,
1442            arg_idx + 1
1443        ));
1444    }
1445
1446    out
1447}
1448
1449fn java_return_handling(f: &Function, field: &str) -> String {
1450    let call_expr = java_handler_call(f, field);
1451    if f.r#async {
1452        validate_async_function(f);
1453        return format!(
1454            "            if (asyncRegistry == null) {{\n              throw new IllegalStateException(\n                \"AsyncHostRegistry is required for async host function \" + MODULE + \"::{name}\"\n              );\n            }}\n            CompletionStage<String> stage = {call_expr};\n            if (stage == null) {{\n              throw new IllegalStateException(\n                \"Host function returned null: \" + MODULE + \"::{name}\"\n              );\n            }}\n            return new long[] {{ asyncRegistry.start(stage) }};",
1455            call_expr = call_expr,
1456            name = f.name
1457        );
1458    }
1459    match f.returns {
1460        Some(Type::String) => format!(
1461            "            String result = {call_expr};\n            if (result == null) {{\n              throw new IllegalStateException(\n                \"Host function returned null: \" + MODULE + \"::{name}\"\n              );\n            }}\n            byte[] resultBytes = result.getBytes(StandardCharsets.UTF_8);\n            if (resultBytes.length > resultMaxLen) {{\n              return new long[] {{ -2 }};\n            }}\n            memory.write(resultPtr, resultBytes);\n            return new long[] {{ resultBytes.length }};",
1462            call_expr = call_expr,
1463            name = f.name
1464        ),
1465        Some(Type::Bytes) => format!(
1466            "            byte[] resultBytes = {call_expr};\n            if (resultBytes == null) {{\n              throw new IllegalStateException(\n                \"Host function returned null: \" + MODULE + \"::{name}\"\n              );\n            }}\n            if (resultBytes.length > resultMaxLen) {{\n              return new long[] {{ -2 }};\n            }}\n            memory.write(resultPtr, resultBytes);\n            return new long[] {{ resultBytes.length }};",
1467            call_expr = call_expr,
1468            name = f.name
1469        ),
1470        Some(Type::Int) => format!(
1471            "            int result = {};\n            return new long[] {{ result }};",
1472            call_expr
1473        ),
1474        Some(Type::Float) => format!(
1475            "            double result = {};\n            return new long[] {{ Double.doubleToRawLongBits(result) }};",
1476            call_expr
1477        ),
1478        None => format!(
1479            "            {};\n            return null;",
1480            call_expr
1481        ),
1482    }
1483}
1484
1485fn java_handler_call(f: &Function, field: &str) -> String {
1486    let call_args = f
1487        .params
1488        .iter()
1489        .enumerate()
1490        .map(|(idx, _)| format!("param{}", idx))
1491        .collect::<Vec<_>>()
1492        .join(", ");
1493    format!("{}.handle({})", field, call_args)
1494}
1495
1496fn java_error_handling(f: &Function) -> &'static str {
1497    if f.r#async {
1498        // Deliver the failure through the normal completion path so the awaiting coroutine raises,
1499        // instead of returning a sentinel token the event loop would wait on forever. Fall back to
1500        // -1 only when there is no registry to record the failure (the client rejects token <= 0).
1501        "            if (asyncRegistry == null) {\n              return new long[] { -1 };\n            }\n            return new long[] { asyncRegistry.startFailed(e) };"
1502    } else if is_buffer_return(f.returns) {
1503        "            return new long[] { -1 };"
1504    } else {
1505        "            throw e;"
1506    }
1507}
1508
1509fn is_buffer_return(ty: Option<Type>) -> bool {
1510    matches!(ty, Some(Type::String) | Some(Type::Bytes))
1511}
1512
1513fn java_type(ty: Type) -> &'static str {
1514    match ty {
1515        Type::String => "String",
1516        Type::Int => "int",
1517        Type::Float => "double",
1518        Type::Bytes => "byte[]",
1519    }
1520}
1521
1522fn java_handler_return_type(f: &Function) -> String {
1523    if f.r#async {
1524        validate_async_function(f);
1525        "CompletionStage<String>".to_string()
1526    } else {
1527        java_return_type(f.returns).to_string()
1528    }
1529}
1530
1531fn java_return_type(ty: Option<Type>) -> &'static str {
1532    match ty {
1533        Some(Type::String) => "String",
1534        Some(Type::Int) => "int",
1535        Some(Type::Float) => "double",
1536        Some(Type::Bytes) => "byte[]",
1537        None => "void",
1538    }
1539}
1540
1541#[cfg(test)]
1542mod tests;