Skip to main content

weaveffi_gen_node/
lib.rs

1//! Node.js (N-API) binding generator for WeaveFFI.
2//!
3//! Emits a JavaScript loader plus TypeScript type definitions for the
4//! companion N-API addon. Async functions surface as `Promise`-returning
5//! methods. Implements [`LanguageBackend`]; the shared driver bridges it into
6//! the generator pipeline.
7
8use std::collections::{HashMap, HashSet};
9
10use camino::Utf8Path;
11use heck::{ToLowerCamelCase, ToSnakeCase, ToUpperCamelCase};
12use serde::{Deserialize, Serialize};
13use weaveffi_core::abi;
14use weaveffi_core::backend::{LanguageBackend, OutputFile};
15use weaveffi_core::capabilities::TargetCapabilities;
16use weaveffi_core::codegen::common::{emit_doc as common_emit_doc, DocCommentStyle};
17use weaveffi_core::model::{
18    BindingModel, CallbackBinding, EnumBinding, FnBinding, ListenerBinding, ParamBinding,
19    StructBinding,
20};
21use weaveffi_core::pkg::{self, ResolvedPackage};
22use weaveffi_core::utils::{
23    c_abi_struct_name, local_type_name, render_json_prelude, render_prelude, render_trailer,
24    wrapper_name, CommentStyle,
25};
26use weaveffi_ir::ir::{Api, TypeRef};
27
28/// Per-target configuration for [`NodeGenerator`].
29#[derive(Debug, Clone, Default, Serialize, Deserialize)]
30#[serde(default)]
31pub struct NodeConfig {
32    /// npm package name (default `"weaveffi"`).
33    pub package_name: Option<String>,
34    /// When `true`, strip the IR module name prefix from emitted
35    /// JS/TS function names.
36    pub strip_module_prefix: bool,
37    /// C ABI symbol prefix (default `"weaveffi"`). Normally set once globally
38    /// via `[global] c_prefix`; honored so the native addon calls the same
39    /// exported symbols the producer emits.
40    pub prefix: Option<String>,
41    /// Basename of the IDL the CLI was invoked with.
42    #[serde(skip)]
43    pub input_basename: Option<String>,
44}
45
46impl NodeConfig {
47    pub fn package_name(&self) -> &str {
48        self.package_name.as_deref().unwrap_or("weaveffi")
49    }
50
51    pub fn prefix(&self) -> &str {
52        self.prefix.as_deref().unwrap_or("weaveffi")
53    }
54
55    pub fn input_basename(&self) -> &str {
56        self.input_basename.as_deref().unwrap_or("weaveffi.yml")
57    }
58}
59
60pub struct NodeGenerator;
61
62impl LanguageBackend for NodeGenerator {
63    type Config = NodeConfig;
64
65    fn name(&self) -> &'static str {
66        "node"
67    }
68
69    fn capabilities(&self) -> TargetCapabilities {
70        TargetCapabilities::full()
71    }
72
73    fn prefix<'a>(&self, config: &'a Self::Config) -> &'a str {
74        config.prefix()
75    }
76
77    fn files(
78        &self,
79        api: &Api,
80        _model: &BindingModel,
81        out_dir: &Utf8Path,
82        config: &Self::Config,
83    ) -> Vec<OutputFile> {
84        let dir = out_dir.join("node");
85        let input_basename = config.input_basename();
86        let prefix = config.prefix();
87        let strip = config.strip_module_prefix;
88        vec![
89            OutputFile::new(
90                dir.join("index.js"),
91                render_node_index(api, prefix, strip, input_basename),
92            ),
93            OutputFile::new(
94                dir.join("types.d.ts"),
95                render_node_dts(api, prefix, strip, input_basename),
96            ),
97            OutputFile::new(
98                dir.join("package.json"),
99                render_package_json(
100                    &pkg::resolve(
101                        api,
102                        config.package_name.as_deref(),
103                        config.input_basename.as_deref(),
104                    ),
105                    input_basename,
106                ),
107            ),
108            OutputFile::new(dir.join("binding.gyp"), render_binding_gyp(input_basename)),
109            OutputFile::new(
110                dir.join("weaveffi_addon.c"),
111                render_addon_c(api, prefix, strip, input_basename),
112            ),
113        ]
114    }
115}
116
117weaveffi_core::impl_generator_via_backend!(NodeGenerator);
118
119fn render_package_json(package: &ResolvedPackage, input_basename: &str) -> String {
120    let prelude = render_json_prelude(input_basename);
121    let name = &package.name;
122    let version = &package.version;
123    let description = package.description_or_default();
124    let mut optional = String::new();
125    if let Some(license) = &package.license {
126        optional.push_str(&format!("  \"license\": \"{license}\",\n"));
127    }
128    if let Some(author) = package.authors.first() {
129        optional.push_str(&format!("  \"author\": \"{author}\",\n"));
130    }
131    if let Some(homepage) = &package.homepage {
132        optional.push_str(&format!("  \"homepage\": \"{homepage}\",\n"));
133    }
134    if let Some(repository) = &package.repository {
135        optional.push_str(&format!(
136            "  \"repository\": {{ \"type\": \"git\", \"url\": \"{repository}\" }},\n"
137        ));
138    }
139    format!(
140        "{{\n{prelude}  \"name\": \"{name}\",\n  \"version\": \"{version}\",\n  \"description\": \"{description}\",\n{optional}  \"main\": \"index.js\",\n  \"types\": \"types.d.ts\",\n  \"gypfile\": true,\n  \"scripts\": {{\n    \"install\": \"node-gyp rebuild\"\n  }}\n}}\n"
141    )
142}
143
144fn render_binding_gyp(input_basename: &str) -> String {
145    let prelude = render_prelude(CommentStyle::Hash, input_basename);
146    let trailer = render_trailer(CommentStyle::Hash, "binding.gyp");
147    format!(
148        "{prelude}{{\n  \"targets\": [\n    {{\n      \"target_name\": \"weaveffi\",\n      \"sources\": [\"weaveffi_addon.c\"],\n      \"include_dirs\": [\"../c\"],\n      \"libraries\": [\"-lweaveffi\"]\n    }}\n  ]\n}}\n\n{trailer}"
149    )
150}
151
152fn is_c_ptr_type(ty: &TypeRef) -> bool {
153    matches!(
154        ty,
155        TypeRef::StringUtf8
156            | TypeRef::Bytes
157            | TypeRef::Struct(_)
158            | TypeRef::List(_)
159            | TypeRef::Map(_, _)
160            | TypeRef::Iterator(_)
161    )
162}
163
164fn c_elem_type(ty: &TypeRef, module: &str, prefix: &str) -> String {
165    match ty {
166        TypeRef::I8 => "int8_t".into(),
167        TypeRef::I16 => "int16_t".into(),
168        TypeRef::I32 => "int32_t".into(),
169        TypeRef::I64 => "int64_t".into(),
170        TypeRef::U8 => "uint8_t".into(),
171        TypeRef::U16 => "uint16_t".into(),
172        TypeRef::U32 => "uint32_t".into(),
173        TypeRef::U64 => "uint64_t".into(),
174        TypeRef::F32 => "float".into(),
175        TypeRef::F64 => "double".into(),
176        TypeRef::Bool => "bool".into(),
177        // A generic `handle` is an opaque integer; a typed `handle<T>` is the C
178        // ABI struct pointer for T (same lowering as a struct value), so it must
179        // carry T's owner-qualified symbol, not the generic integer type.
180        TypeRef::Handle => "weaveffi_handle_t".into(),
181        TypeRef::TypedHandle(s) => format!("{}*", c_abi_struct_name(s, module, prefix)),
182        TypeRef::StringUtf8 | TypeRef::BorrowedStr => "const char*".into(),
183        TypeRef::Bytes | TypeRef::BorrowedBytes => "const uint8_t*".into(),
184        TypeRef::Struct(s) => format!("{}*", c_abi_struct_name(s, module, prefix)),
185        TypeRef::Enum(e) => format!("{prefix}_{module}_{e}"),
186        TypeRef::Optional(inner) | TypeRef::List(inner) | TypeRef::Iterator(inner) => {
187            c_elem_type(inner, module, prefix)
188        }
189        TypeRef::Map(_, _) => "void*".into(),
190    }
191}
192
193fn c_ret_type_str(ty: &TypeRef, module: &str, prefix: &str) -> String {
194    match ty {
195        TypeRef::I8 => "int8_t".into(),
196        TypeRef::I16 => "int16_t".into(),
197        TypeRef::I32 => "int32_t".into(),
198        TypeRef::I64 => "int64_t".into(),
199        TypeRef::U8 => "uint8_t".into(),
200        TypeRef::U16 => "uint16_t".into(),
201        TypeRef::U32 => "uint32_t".into(),
202        TypeRef::U64 => "uint64_t".into(),
203        TypeRef::F32 => "float".into(),
204        TypeRef::F64 => "double".into(),
205        TypeRef::Bool => "bool".into(),
206        TypeRef::StringUtf8 | TypeRef::BorrowedStr => "const char*".into(),
207        TypeRef::Bytes | TypeRef::BorrowedBytes => "const uint8_t*".into(),
208        TypeRef::Handle => "weaveffi_handle_t".into(),
209        TypeRef::TypedHandle(s) => format!("{}*", c_abi_struct_name(s, module, prefix)),
210        TypeRef::Struct(s) => format!("{}*", c_abi_struct_name(s, module, prefix)),
211        TypeRef::Enum(e) => format!("{prefix}_{module}_{e}"),
212        TypeRef::Optional(inner) => {
213            if is_c_ptr_type(inner) {
214                c_ret_type_str(inner, module, prefix)
215            } else {
216                format!("{}*", c_elem_type(inner, module, prefix))
217            }
218        }
219        TypeRef::List(inner) => format!("{}*", c_elem_type(inner, module, prefix)),
220        TypeRef::Map(_, _) => "void".into(),
221        TypeRef::Iterator(_) => "void*".into(),
222    }
223}
224
225fn napi_getter(ty: &TypeRef) -> &'static str {
226    match ty {
227        // i8/i16 are read through the 32-bit signed getter (N-API has no
228        // narrower int getter) and narrowed at the use site.
229        TypeRef::I8 | TypeRef::I16 | TypeRef::I32 | TypeRef::Enum(_) => "napi_get_value_int32",
230        TypeRef::U8 | TypeRef::U16 | TypeRef::U32 => "napi_get_value_uint32",
231        // u64 mirrors i64/handle: read as a 64-bit int, reinterpreted as needed.
232        TypeRef::I64
233        | TypeRef::U64
234        | TypeRef::Handle
235        | TypeRef::TypedHandle(_)
236        | TypeRef::Struct(_) => "napi_get_value_int64",
237        // f32 is read as a double then narrowed to float at the use site.
238        TypeRef::F32 | TypeRef::F64 => "napi_get_value_double",
239        TypeRef::Bool => "napi_get_value_bool",
240        _ => "napi_get_value_int64",
241    }
242}
243
244/// The C type of the temporary an N-API getter writes into for a scalar that is
245/// narrower than the getter's natural width. N-API only exposes 32/64-bit int
246/// and `double` getters, so `i8/i16/u8/u16/f32` must be read into a wider
247/// temporary and then narrowed with an explicit cast to the real ABI type
248/// returned by [`c_elem_type`]; `u64` is read as `int64_t` then reinterpreted.
249fn napi_read_tmp_type(ty: &TypeRef) -> &'static str {
250    match ty {
251        TypeRef::I8 | TypeRef::I16 => "int32_t",
252        TypeRef::U8 | TypeRef::U16 => "uint32_t",
253        TypeRef::U64 => "int64_t",
254        TypeRef::F32 => "double",
255        _ => "int64_t",
256    }
257}
258
259/// Whether `ty` is one of the numeric primitives narrower or wider than what an
260/// N-API number getter writes directly, requiring a temporary + cast on read.
261fn needs_narrowing_read(ty: &TypeRef) -> bool {
262    matches!(
263        ty,
264        TypeRef::I8 | TypeRef::I16 | TypeRef::U8 | TypeRef::U16 | TypeRef::U64 | TypeRef::F32
265    )
266}
267
268fn render_addon_c(
269    api: &Api,
270    prefix: &str,
271    strip_module_prefix: bool,
272    input_basename: &str,
273) -> String {
274    let mut out = render_prelude(CommentStyle::DoubleSlash, input_basename);
275    out.push_str(&format!(
276        "#include <node_api.h>\n#include \"{prefix}.h\"\n#include <stdlib.h>\n#include <string.h>\n\n"
277    ));
278
279    let model = BindingModel::build(api, prefix);
280    let mut all_exports: Vec<(String, String)> = Vec::new();
281    let structs = struct_registry(&model);
282
283    let has_listeners = model.modules.iter().any(|m| !m.listeners.is_empty());
284    if has_listeners {
285        render_listener_support_c(&mut out, prefix);
286    }
287
288    for m in &model.modules {
289        // Rich (algebraic) enums cross the ABI as opaque objects, so they get a
290        // struct-like native surface: a tag reader, per-variant constructors and
291        // field getters, and a destructor. (Plain C-style enums cross by value
292        // as int32 and need no native helpers.)
293        for e in &m.enums {
294            if e.is_rich() {
295                render_rich_enum_napi_fns(
296                    &mut out,
297                    e,
298                    &m.path,
299                    prefix,
300                    strip_module_prefix,
301                    &structs,
302                    &mut all_exports,
303                );
304            }
305        }
306        // Callbacks referenced by listeners get a payload struct, a producer-
307        // thread trampoline, and a JS-thread marshaller (threadsafe function).
308        let used_callbacks: Vec<&CallbackBinding> = m
309            .listeners
310            .iter()
311            .filter_map(|l| m.callback(&l.event_callback))
312            .collect();
313        for cb in &used_callbacks {
314            render_cb_payload_struct(&mut out, cb, prefix);
315            render_cb_tramp(&mut out, cb, prefix);
316            render_cb_calljs(&mut out, cb, prefix);
317        }
318        for l in &m.listeners {
319            let Some(cb) = m.callback(&l.event_callback) else {
320                unreachable!("validation guarantees the listener's callback exists");
321            };
322            render_listener_napi_fns(&mut out, l, cb, prefix);
323            all_exports.push((
324                wrapper_name(
325                    &m.path,
326                    &format!("register_{}", l.name),
327                    strip_module_prefix,
328                ),
329                format!("Napi_{}", l.register_symbol),
330            ));
331            all_exports.push((
332                wrapper_name(
333                    &m.path,
334                    &format!("unregister_{}", l.name),
335                    strip_module_prefix,
336                ),
337                format!("Napi_{}", l.unregister_symbol),
338            ));
339        }
340        for f in &m.functions {
341            let c_name = &f.c_base;
342            let napi_name = format!("Napi_{c_name}");
343            let js_name = wrapper_name(&m.path, &f.name, strip_module_prefix);
344            all_exports.push((js_name, napi_name.clone()));
345
346            if f.is_async {
347                render_async_machinery(&mut out, f, c_name, &m.path, prefix, &structs);
348            }
349
350            out.push_str(&format!(
351                "static napi_value {napi_name}(napi_env env, napi_callback_info info) {{\n"
352            ));
353            if f.is_async {
354                render_async_napi_body(&mut out, f, c_name, &m.path, prefix);
355            } else {
356                render_napi_body(&mut out, f, c_name, &m.path, prefix, &structs);
357            }
358            out.push_str("}\n\n");
359        }
360    }
361
362    out.push_str("static napi_value Init(napi_env env, napi_value exports) {\n");
363    if !all_exports.is_empty() {
364        out.push_str("  napi_property_descriptor props[] = {\n");
365        for (js_name, napi_fn) in &all_exports {
366            out.push_str(&format!(
367                "    {{ \"{js_name}\", NULL, {napi_fn}, NULL, NULL, NULL, napi_default, NULL }},\n"
368            ));
369        }
370        out.push_str("  };\n");
371        out.push_str(&format!(
372            "  napi_define_properties(env, exports, {}, props);\n",
373            all_exports.len()
374        ));
375    }
376    out.push_str("  return exports;\n");
377    out.push_str("}\n\n");
378    out.push_str("NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)\n\n");
379    out.push_str(&render_trailer(
380        CommentStyle::DoubleSlash,
381        "weaveffi_addon.c",
382    ));
383    out
384}
385
386// --- Rich (algebraic) enum support -----------------------------------------
387//
388// A rich enum crosses the ABI exactly like a struct: an opaque object pointer
389// surfaced to JS as the same int64 handle structs use. The JS-export base names
390// below are shared by the addon (which exports the native helpers) and the JS
391// loader (whose `Shape` class calls them), so both halves agree by construction.
392
393/// `{Enum}_tag` — the JS-export base for a rich enum's discriminant reader.
394fn rich_tag_base(enum_name: &str) -> String {
395    format!("{enum_name}_tag")
396}
397
398/// `{Enum}_{variant}_new` — the JS-export base for a variant constructor.
399fn rich_ctor_base(enum_name: &str, variant: &str) -> String {
400    format!("{enum_name}_{}_new", variant.to_snake_case())
401}
402
403/// `{Enum}_{variant}_get_{field}` — the JS-export base for a field getter.
404fn rich_getter_base(enum_name: &str, variant: &str, field: &str) -> String {
405    format!("{enum_name}_{}_get_{field}", variant.to_snake_case())
406}
407
408/// `{Enum}_destroy` — the JS-export base for the destructor.
409fn rich_destroy_base(enum_name: &str) -> String {
410    format!("{enum_name}_destroy")
411}
412
413/// Read `args[0]` as the opaque handle and bind it to a typed `self` pointer.
414/// Shared by the tag reader, every field getter, and the destructor.
415fn emit_rich_self_read(out: &mut String, c_tag: &str) {
416    out.push_str("  size_t argc = 1;\n");
417    out.push_str("  napi_value args[1];\n");
418    out.push_str("  napi_get_cb_info(env, info, &argc, args, NULL, NULL);\n");
419    out.push_str("  int64_t self_raw;\n");
420    out.push_str("  napi_get_value_int64(env, args[0], &self_raw);\n");
421    out.push_str(&format!(
422        "  {c_tag}* self = ({c_tag}*)(intptr_t)self_raw;\n"
423    ));
424}
425
426/// Emit the native helpers for one rich enum and register their JS exports:
427/// the tag reader, one constructor per variant, one getter per variant field,
428/// and the destructor. Constructors reuse [`emit_param`] (the struct-create
429/// marshalling) for their arguments; getters reuse [`emit_struct_field_to_napi`]
430/// (the struct-field marshalling), so strings/numerics/bytes/lists are
431/// materialized identically to struct fields.
432#[allow(clippy::too_many_arguments)]
433fn render_rich_enum_napi_fns(
434    out: &mut String,
435    e: &EnumBinding,
436    module: &str,
437    prefix: &str,
438    strip: bool,
439    structs: &HashMap<String, StructBinding>,
440    all_exports: &mut Vec<(String, String)>,
441) {
442    let Some(rich) = &e.rich else {
443        return;
444    };
445    let c_tag = &e.c_tag;
446    let name = &e.name;
447
448    // tag reader: int32 discriminant of the active variant.
449    let napi_tag = format!("Napi_{}", rich.tag_symbol);
450    out.push_str(&format!(
451        "static napi_value {napi_tag}(napi_env env, napi_callback_info info) {{\n"
452    ));
453    emit_rich_self_read(out, c_tag);
454    out.push_str("  napi_value ret;\n");
455    out.push_str(&format!(
456        "  napi_create_int32(env, {}(self), &ret);\n",
457        rich.tag_symbol
458    ));
459    out.push_str("  return ret;\n}\n\n");
460    all_exports.push((wrapper_name(module, &rich_tag_base(name), strip), napi_tag));
461
462    // One constructor per variant: read each variant field as a JS argument
463    // (reusing the struct-create marshalling), call `{Enum}_{V}_new`, and return
464    // the resulting owned pointer as the int64 handle.
465    for v in &rich.variants {
466        let napi_ctor = format!("Napi_{}", v.create.symbol);
467        out.push_str(&format!(
468            "static napi_value {napi_ctor}(napi_env env, napi_callback_info info) {{\n"
469        ));
470        let n = v.fields.len();
471        if n > 0 {
472            out.push_str(&format!("  size_t argc = {n};\n"));
473            out.push_str(&format!("  napi_value args[{n}];\n"));
474            out.push_str("  napi_get_cb_info(env, info, &argc, args, NULL, NULL);\n");
475        } else {
476            out.push_str("  size_t argc = 0;\n");
477            out.push_str("  napi_get_cb_info(env, info, &argc, NULL, NULL, NULL);\n");
478        }
479        let mut c_args: Vec<String> = Vec::new();
480        let mut cleanups: Vec<String> = Vec::new();
481        for (i, f) in v.fields.iter().enumerate() {
482            emit_param(
483                out,
484                &mut c_args,
485                &mut cleanups,
486                &f.ty,
487                &f.name,
488                i,
489                module,
490                prefix,
491            );
492        }
493        out.push_str("  weaveffi_error err = {0};\n");
494        c_args.push("&err".to_string());
495        out.push_str(&format!(
496            "  {c_tag}* result = {}({});\n",
497            v.create.symbol,
498            c_args.join(", ")
499        ));
500        for cleanup in &cleanups {
501            out.push_str(cleanup);
502        }
503        out.push_str("  if (err.code != 0) {\n");
504        out.push_str("    napi_throw_error(env, NULL, err.message);\n");
505        out.push_str("    weaveffi_error_clear(&err);\n");
506        out.push_str("    return NULL;\n");
507        out.push_str("  }\n");
508        out.push_str("  napi_value ret;\n");
509        out.push_str("  napi_create_int64(env, (int64_t)(intptr_t)result, &ret);\n");
510        out.push_str("  return ret;\n}\n\n");
511        all_exports.push((
512            wrapper_name(module, &rich_ctor_base(name, &v.name), strip),
513            napi_ctor,
514        ));
515    }
516
517    // One getter per variant field, namespaced by variant. Reuses the struct
518    // field marshalling, so the active variant's payload surfaces exactly like
519    // a struct field would (string decode + free, Buffer copy, list/array, …).
520    for v in &rich.variants {
521        for f in &v.fields {
522            let napi_getter = format!("Napi_{}", f.getter_symbol);
523            out.push_str(&format!(
524                "static napi_value {napi_getter}(napi_env env, napi_callback_info info) {{\n"
525            ));
526            emit_rich_self_read(out, c_tag);
527            out.push_str("  napi_value ret;\n");
528            emit_struct_field_to_napi(
529                out,
530                "env",
531                &f.ty,
532                &f.getter_symbol,
533                "self",
534                "ret",
535                module,
536                prefix,
537                structs,
538                "  ",
539            );
540            out.push_str("  return ret;\n}\n\n");
541            all_exports.push((
542                wrapper_name(module, &rich_getter_base(name, &v.name, &f.name), strip),
543                napi_getter,
544            ));
545        }
546    }
547
548    // Destructor: free the opaque object behind the handle.
549    let napi_destroy = format!("Napi_{}", rich.destroy_symbol);
550    out.push_str(&format!(
551        "static napi_value {napi_destroy}(napi_env env, napi_callback_info info) {{\n"
552    ));
553    emit_rich_self_read(out, c_tag);
554    out.push_str(&format!("  {}(self);\n", rich.destroy_symbol));
555    out.push_str("  napi_value ret;\n");
556    out.push_str("  napi_get_undefined(env, &ret);\n");
557    out.push_str("  return ret;\n}\n\n");
558    all_exports.push((
559        wrapper_name(module, &rich_destroy_base(name), strip),
560        napi_destroy,
561    ));
562}
563
564/// The listener context + registry shared by every generated listener. The
565/// registry is only mutated from the JS thread (register/unregister are plain
566/// N-API calls), so a simple singly-linked list suffices.
567fn render_listener_support_c(out: &mut String, prefix: &str) {
568    out.push_str(&format!("typedef struct {prefix}_napi_listener_ctx {{\n"));
569    out.push_str("    napi_threadsafe_function tsfn;\n");
570    out.push_str("    uint64_t id;\n");
571    out.push_str(&format!("    struct {prefix}_napi_listener_ctx* next;\n"));
572    out.push_str(&format!("}} {prefix}_napi_listener_ctx;\n\n"));
573    out.push_str(&format!(
574        "static {prefix}_napi_listener_ctx* {prefix}_napi_listeners = NULL;\n\n"
575    ));
576}
577
578fn cb_payload_name(cb: &CallbackBinding) -> String {
579    format!("{}_payload", cb.c_fn_type)
580}
581
582/// The C slot declarations of a callback's parameters (without context).
583fn cb_slot_decls(cb: &CallbackBinding, prefix: &str) -> Vec<String> {
584    cb.params
585        .iter()
586        .flat_map(|p| abi::lower_param(&p.name, &p.ty, "", false))
587        .map(|slot| format!("{} {}", slot.ty.render_c(prefix), slot.name))
588        .collect()
589}
590
591/// The deep-copy payload carried from the producer thread to the JS thread.
592/// Every pointer field is owned by the payload (strdup/memcpy in the
593/// trampoline, freed in the call-js marshaller); struct/handle pointers are
594/// shallow-copied and surface as numeric handles.
595fn render_cb_payload_struct(out: &mut String, cb: &CallbackBinding, prefix: &str) {
596    out.push_str("typedef struct {\n");
597    for p in &cb.params {
598        let slots = abi::lower_param(&p.name, &p.ty, "", false);
599        let n0 = &slots[0].name;
600        match &p.ty {
601            TypeRef::I8
602            | TypeRef::I16
603            | TypeRef::I32
604            | TypeRef::I64
605            | TypeRef::U8
606            | TypeRef::U16
607            | TypeRef::U32
608            | TypeRef::U64
609            | TypeRef::F32
610            | TypeRef::F64
611            | TypeRef::Bool
612            | TypeRef::Handle
613            | TypeRef::Enum(_) => {
614                out.push_str(&format!("    {} {n0};\n", slots[0].ty.render_c(prefix)));
615            }
616            TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
617                out.push_str(&format!("    char* {n0};\n"));
618            }
619            TypeRef::Bytes | TypeRef::BorrowedBytes => {
620                out.push_str(&format!("    uint8_t* {n0};\n"));
621                out.push_str(&format!("    size_t {};\n", slots[1].name));
622            }
623            TypeRef::Struct(_) | TypeRef::TypedHandle(_) => {
624                out.push_str(&format!("    void* {n0};\n"));
625            }
626            TypeRef::Optional(inner) => match inner.as_ref() {
627                TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
628                    out.push_str(&format!("    char* {n0};\n"));
629                }
630                TypeRef::Bytes | TypeRef::BorrowedBytes => {
631                    out.push_str(&format!("    int {n0}_has;\n"));
632                    out.push_str(&format!("    uint8_t* {n0};\n"));
633                    out.push_str(&format!("    size_t {};\n", slots[1].name));
634                }
635                TypeRef::Struct(_) | TypeRef::TypedHandle(_) => {
636                    out.push_str(&format!("    void* {n0};\n"));
637                }
638                other => {
639                    out.push_str(&format!("    int {n0}_has;\n"));
640                    out.push_str(&format!(
641                        "    {} {n0};\n",
642                        abi::element_ctype(other, "").render_c(prefix)
643                    ));
644                }
645            },
646            TypeRef::List(inner) => {
647                let elem = elem_payload_ctype(inner, prefix);
648                out.push_str(&format!("    {elem}* {n0};\n"));
649                out.push_str(&format!("    size_t {};\n", slots[1].name));
650            }
651            TypeRef::Map(k, v) => {
652                let kt = elem_payload_ctype(k, prefix);
653                let vt = elem_payload_ctype(v, prefix);
654                out.push_str(&format!("    {kt}* {n0};\n"));
655                out.push_str(&format!("    {vt}* {};\n", slots[1].name));
656                out.push_str(&format!("    size_t {};\n", slots[2].name));
657            }
658            TypeRef::Iterator(_) => unreachable!("validated: iterator not a callback param"),
659        }
660    }
661    out.push_str(&format!("}} {};\n\n", cb_payload_name(cb)));
662}
663
664/// The payload element type for list/map callback parameters. Strings own
665/// their copies (`char*`); scalar elements keep their C ABI type.
666fn elem_payload_ctype(ty: &TypeRef, prefix: &str) -> String {
667    match ty {
668        TypeRef::StringUtf8 | TypeRef::BorrowedStr => "char*".into(),
669        other => abi::element_ctype(other, "").render_c(prefix),
670    }
671}
672
673/// The producer-thread trampoline: deep-copies the C arguments into a payload
674/// and queues it onto the threadsafe function. Runs on whatever thread the
675/// producer fires the event from; never touches `napi_env`.
676fn render_cb_tramp(out: &mut String, cb: &CallbackBinding, prefix: &str) {
677    let payload = cb_payload_name(cb);
678    let mut decls = cb_slot_decls(cb, prefix);
679    decls.push("void* context".into());
680    out.push_str(&format!(
681        "static void {}_napi_tramp({}) {{\n",
682        cb.c_fn_type,
683        decls.join(", ")
684    ));
685    out.push_str(&format!(
686        "    {prefix}_napi_listener_ctx* ctx = ({prefix}_napi_listener_ctx*)context;\n"
687    ));
688    out.push_str(&format!(
689        "    {payload}* p = ({payload}*)calloc(1, sizeof({payload}));\n"
690    ));
691    for p in &cb.params {
692        let slots = abi::lower_param(&p.name, &p.ty, "", false);
693        let n0 = &slots[0].name;
694        match &p.ty {
695            TypeRef::I8
696            | TypeRef::I16
697            | TypeRef::I32
698            | TypeRef::I64
699            | TypeRef::U8
700            | TypeRef::U16
701            | TypeRef::U32
702            | TypeRef::U64
703            | TypeRef::F32
704            | TypeRef::F64
705            | TypeRef::Bool
706            | TypeRef::Handle
707            | TypeRef::Enum(_) => {
708                out.push_str(&format!("    p->{n0} = {n0};\n"));
709            }
710            TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
711                out.push_str(&format!("    p->{n0} = {n0} ? strdup({n0}) : NULL;\n"));
712            }
713            TypeRef::Bytes | TypeRef::BorrowedBytes => {
714                let n1 = &slots[1].name;
715                out.push_str(&format!("    p->{n1} = {n1};\n"));
716                out.push_str(&format!(
717                    "    if ({n0} != NULL && {n1} > 0) {{ p->{n0} = (uint8_t*)malloc({n1}); memcpy(p->{n0}, {n0}, {n1}); }}\n"
718                ));
719            }
720            TypeRef::Struct(_) | TypeRef::TypedHandle(_) => {
721                out.push_str(&format!("    p->{n0} = (void*){n0};\n"));
722            }
723            TypeRef::Optional(inner) => match inner.as_ref() {
724                TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
725                    out.push_str(&format!("    p->{n0} = {n0} ? strdup({n0}) : NULL;\n"));
726                }
727                TypeRef::Bytes | TypeRef::BorrowedBytes => {
728                    let n1 = &slots[1].name;
729                    out.push_str(&format!("    p->{n0}_has = {n0} != NULL;\n"));
730                    out.push_str(&format!("    p->{n1} = {n1};\n"));
731                    out.push_str(&format!(
732                        "    if ({n0} != NULL && {n1} > 0) {{ p->{n0} = (uint8_t*)malloc({n1}); memcpy(p->{n0}, {n0}, {n1}); }}\n"
733                    ));
734                }
735                TypeRef::Struct(_) | TypeRef::TypedHandle(_) => {
736                    out.push_str(&format!("    p->{n0} = (void*){n0};\n"));
737                }
738                _ => {
739                    out.push_str(&format!("    p->{n0}_has = {n0} != NULL;\n"));
740                    out.push_str(&format!("    if ({n0} != NULL) p->{n0} = *{n0};\n"));
741                }
742            },
743            TypeRef::List(inner) => {
744                let n1 = &slots[1].name;
745                out.push_str(&format!("    p->{n1} = {n1};\n"));
746                match inner.as_ref() {
747                    TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
748                        out.push_str(&format!(
749                            "    if ({n0} != NULL && {n1} > 0) {{\n        p->{n0} = (char**)calloc({n1}, sizeof(char*));\n        for (size_t i = 0; i < {n1}; i++) p->{n0}[i] = {n0}[i] ? strdup({n0}[i]) : NULL;\n    }}\n"
750                        ));
751                    }
752                    _ => {
753                        out.push_str(&format!(
754                            "    if ({n0} != NULL && {n1} > 0) {{ p->{n0} = malloc({n1} * sizeof(*p->{n0})); memcpy(p->{n0}, {n0}, {n1} * sizeof(*p->{n0})); }}\n"
755                        ));
756                    }
757                }
758            }
759            TypeRef::Map(k, v) => {
760                let keys = n0;
761                let vals = &slots[1].name;
762                let len = &slots[2].name;
763                out.push_str(&format!("    p->{len} = {len};\n"));
764                for (base, ty) in [(keys, k), (vals, v)] {
765                    match ty.as_ref() {
766                        TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
767                            out.push_str(&format!(
768                                "    if ({base} != NULL && {len} > 0) {{\n        p->{base} = (char**)calloc({len}, sizeof(char*));\n        for (size_t i = 0; i < {len}; i++) p->{base}[i] = {base}[i] ? strdup({base}[i]) : NULL;\n    }}\n"
769                            ));
770                        }
771                        _ => {
772                            out.push_str(&format!(
773                                "    if ({base} != NULL && {len} > 0) {{ p->{base} = malloc({len} * sizeof(*p->{base})); memcpy(p->{base}, {base}, {len} * sizeof(*p->{base})); }}\n"
774                            ));
775                        }
776                    }
777                }
778            }
779            TypeRef::Iterator(_) => unreachable!("validated: iterator not a callback param"),
780        }
781    }
782    out.push_str("    napi_call_threadsafe_function(ctx->tsfn, p, napi_tsfn_nonblocking);\n");
783    out.push_str("}\n\n");
784}
785
786/// One payload field rendered to a `napi_value` in `argv[idx]` (call-js side).
787fn emit_payload_to_napi(out: &mut String, p: &ParamBinding, idx: usize, prefix: &str) {
788    let slots = abi::lower_param(&p.name, &p.ty, "", false);
789    let n0 = &slots[0].name;
790    let target = format!("argv[{idx}]");
791    let _ = prefix;
792    match &p.ty {
793        TypeRef::I32 => out.push_str(&format!(
794            "        napi_create_int32(env, p->{n0}, &{target});\n"
795        )),
796        TypeRef::U32 => out.push_str(&format!(
797            "        napi_create_uint32(env, p->{n0}, &{target});\n"
798        )),
799        TypeRef::I64 => out.push_str(&format!(
800            "        napi_create_int64(env, p->{n0}, &{target});\n"
801        )),
802        TypeRef::F64 => out.push_str(&format!(
803            "        napi_create_double(env, p->{n0}, &{target});\n"
804        )),
805        TypeRef::I8 | TypeRef::I16 => out.push_str(&format!(
806            "        napi_create_int32(env, p->{n0}, &{target});\n"
807        )),
808        TypeRef::U8 | TypeRef::U16 => out.push_str(&format!(
809            "        napi_create_uint32(env, p->{n0}, &{target});\n"
810        )),
811        TypeRef::U64 => out.push_str(&format!(
812            "        napi_create_int64(env, (int64_t)p->{n0}, &{target});\n"
813        )),
814        TypeRef::F32 => out.push_str(&format!(
815            "        napi_create_double(env, p->{n0}, &{target});\n"
816        )),
817        TypeRef::Bool => out.push_str(&format!(
818            "        napi_get_boolean(env, p->{n0}, &{target});\n"
819        )),
820        TypeRef::Handle => out.push_str(&format!(
821            "        napi_create_int64(env, (int64_t)p->{n0}, &{target});\n"
822        )),
823        TypeRef::Enum(_) => out.push_str(&format!(
824            "        napi_create_int32(env, (int32_t)p->{n0}, &{target});\n"
825        )),
826        TypeRef::StringUtf8 | TypeRef::BorrowedStr => out.push_str(&format!(
827            "        napi_create_string_utf8(env, p->{n0} ? p->{n0} : \"\", NAPI_AUTO_LENGTH, &{target});\n"
828        )),
829        TypeRef::Bytes | TypeRef::BorrowedBytes => {
830            let n1 = &slots[1].name;
831            out.push_str(&format!(
832                "        napi_create_buffer_copy(env, p->{n1}, p->{n0} ? (const void*)p->{n0} : (const void*)\"\", NULL, &{target});\n"
833            ));
834        }
835        TypeRef::Struct(_) | TypeRef::TypedHandle(_) => out.push_str(&format!(
836            "        napi_create_int64(env, (int64_t)(intptr_t)p->{n0}, &{target});\n"
837        )),
838        TypeRef::Optional(inner) => match inner.as_ref() {
839            TypeRef::StringUtf8 | TypeRef::BorrowedStr => out.push_str(&format!(
840                "        if (p->{n0}) napi_create_string_utf8(env, p->{n0}, NAPI_AUTO_LENGTH, &{target}); else napi_get_null(env, &{target});\n"
841            )),
842            TypeRef::Bytes | TypeRef::BorrowedBytes => {
843                let n1 = &slots[1].name;
844                out.push_str(&format!(
845                    "        if (p->{n0}_has) napi_create_buffer_copy(env, p->{n1}, p->{n0} ? (const void*)p->{n0} : (const void*)\"\", NULL, &{target}); else napi_get_null(env, &{target});\n"
846                ));
847            }
848            TypeRef::Struct(_) | TypeRef::TypedHandle(_) => out.push_str(&format!(
849                "        if (p->{n0}) napi_create_int64(env, (int64_t)(intptr_t)p->{n0}, &{target}); else napi_get_null(env, &{target});\n"
850            )),
851            other => {
852                let leaf = payload_leaf_to_napi(other, &format!("p->{n0}"), &target);
853                out.push_str(&format!(
854                    "        if (p->{n0}_has) {{ {leaf} }} else napi_get_null(env, &{target});\n"
855                ));
856            }
857        },
858        TypeRef::List(inner) => {
859            let n1 = &slots[1].name;
860            out.push_str(&format!("        napi_create_array(env, &{target});\n"));
861            out.push_str(&format!(
862                "        for (size_t i = 0; p->{n0} != NULL && i < p->{n1}; i++) {{\n"
863            ));
864            out.push_str("            napi_value elem;\n");
865            let leaf = payload_elem_to_napi(inner, &format!("p->{n0}[i]"), "elem");
866            out.push_str(&format!("            {leaf}\n"));
867            out.push_str(&format!(
868                "            napi_set_element(env, {target}, (uint32_t)i, elem);\n"
869            ));
870            out.push_str("        }\n");
871        }
872        TypeRef::Map(k, v) => {
873            let keys = n0;
874            let vals = &slots[1].name;
875            let len = &slots[2].name;
876            out.push_str(&format!("        napi_create_object(env, &{target});\n"));
877            out.push_str(&format!(
878                "        for (size_t i = 0; p->{keys} != NULL && p->{vals} != NULL && i < p->{len}; i++) {{\n"
879            ));
880            out.push_str("            napi_value mk; napi_value mv;\n");
881            let kc = payload_elem_to_napi(k, &format!("p->{keys}[i]"), "mk");
882            let vc = payload_elem_to_napi(v, &format!("p->{vals}[i]"), "mv");
883            out.push_str(&format!("            {kc}\n"));
884            out.push_str(&format!("            {vc}\n"));
885            out.push_str(&format!(
886                "            napi_set_property(env, {target}, mk, mv);\n"
887            ));
888            out.push_str("        }\n");
889        }
890        TypeRef::Iterator(_) => unreachable!("validated: iterator not a callback param"),
891    }
892}
893
894/// One scalar-ish payload value to a napi_value (single statement).
895fn payload_leaf_to_napi(ty: &TypeRef, expr: &str, target: &str) -> String {
896    match ty {
897        TypeRef::I32 => format!("napi_create_int32(env, {expr}, &{target});"),
898        TypeRef::U32 => format!("napi_create_uint32(env, {expr}, &{target});"),
899        TypeRef::I64 => format!("napi_create_int64(env, {expr}, &{target});"),
900        TypeRef::F64 => format!("napi_create_double(env, {expr}, &{target});"),
901        TypeRef::I8 | TypeRef::I16 => format!("napi_create_int32(env, {expr}, &{target});"),
902        TypeRef::U8 | TypeRef::U16 => format!("napi_create_uint32(env, {expr}, &{target});"),
903        TypeRef::U64 => format!("napi_create_int64(env, (int64_t){expr}, &{target});"),
904        TypeRef::F32 => format!("napi_create_double(env, {expr}, &{target});"),
905        TypeRef::Bool => format!("napi_get_boolean(env, {expr}, &{target});"),
906        TypeRef::Handle => format!("napi_create_int64(env, (int64_t){expr}, &{target});"),
907        TypeRef::Enum(_) => format!("napi_create_int32(env, (int32_t){expr}, &{target});"),
908        _ => format!("napi_get_null(env, &{target});"),
909    }
910}
911
912/// One list/map element payload value to a napi_value (single statement).
913fn payload_elem_to_napi(ty: &TypeRef, expr: &str, target: &str) -> String {
914    match ty {
915        TypeRef::StringUtf8 | TypeRef::BorrowedStr => format!(
916            "napi_create_string_utf8(env, {expr} ? {expr} : \"\", NAPI_AUTO_LENGTH, &{target});"
917        ),
918        other => payload_leaf_to_napi(other, expr, target),
919    }
920}
921
922/// Frees one payload field after the JS call.
923fn emit_payload_free(out: &mut String, p: &ParamBinding) {
924    let slots = abi::lower_param(&p.name, &p.ty, "", false);
925    let n0 = &slots[0].name;
926    match &p.ty {
927        TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
928            out.push_str(&format!("    free(p->{n0});\n"));
929        }
930        TypeRef::Bytes | TypeRef::BorrowedBytes => {
931            out.push_str(&format!("    free(p->{n0});\n"));
932        }
933        TypeRef::Optional(inner) => match inner.as_ref() {
934            TypeRef::StringUtf8
935            | TypeRef::BorrowedStr
936            | TypeRef::Bytes
937            | TypeRef::BorrowedBytes => {
938                out.push_str(&format!("    free(p->{n0});\n"));
939            }
940            _ => {}
941        },
942        TypeRef::List(inner) => {
943            let n1 = &slots[1].name;
944            if matches!(inner.as_ref(), TypeRef::StringUtf8 | TypeRef::BorrowedStr) {
945                out.push_str(&format!(
946                    "    for (size_t i = 0; p->{n0} != NULL && i < p->{n1}; i++) free(p->{n0}[i]);\n"
947                ));
948            }
949            out.push_str(&format!("    free(p->{n0});\n"));
950        }
951        TypeRef::Map(k, v) => {
952            let keys = n0;
953            let vals = &slots[1].name;
954            let len = &slots[2].name;
955            for (base, ty) in [(keys, k), (vals, v)] {
956                if matches!(ty.as_ref(), TypeRef::StringUtf8 | TypeRef::BorrowedStr) {
957                    out.push_str(&format!(
958                        "    for (size_t i = 0; p->{base} != NULL && i < p->{len}; i++) free(p->{base}[i]);\n"
959                    ));
960                }
961                out.push_str(&format!("    free(p->{base});\n"));
962            }
963        }
964        _ => {}
965    }
966}
967
968/// The JS-thread marshaller invoked by the threadsafe function: converts the
969/// payload into JS arguments, calls the user callback, and frees the payload.
970fn render_cb_calljs(out: &mut String, cb: &CallbackBinding, prefix: &str) {
971    let payload = cb_payload_name(cb);
972    out.push_str(&format!(
973        "static void {}_napi_calljs(napi_env env, napi_value js_cb, void* context, void* data) {{\n",
974        cb.c_fn_type
975    ));
976    out.push_str("    (void)context;\n");
977    out.push_str(&format!("    {payload}* p = ({payload}*)data;\n"));
978    out.push_str("    if (env != NULL) {\n");
979    out.push_str("        napi_value undefined;\n");
980    out.push_str("        napi_get_undefined(env, &undefined);\n");
981    let argc = cb.params.len();
982    if argc > 0 {
983        out.push_str(&format!("        napi_value argv[{argc}];\n"));
984        for (i, p) in cb.params.iter().enumerate() {
985            emit_payload_to_napi(out, p, i, prefix);
986        }
987        out.push_str(&format!(
988            "        napi_call_function(env, undefined, js_cb, {argc}, argv, NULL);\n"
989        ));
990    } else {
991        out.push_str("        napi_call_function(env, undefined, js_cb, 0, NULL, NULL);\n");
992    }
993    out.push_str("    }\n");
994    for p in &cb.params {
995        emit_payload_free(out, p);
996    }
997    out.push_str("    free(p);\n");
998    out.push_str("}\n\n");
999}
1000
1001/// The `Napi_*` register/unregister entry points for one listener. Register
1002/// wraps the JS callback in an unref'd threadsafe function (so live listeners
1003/// don't pin the event loop) and stores it in the registry; unregister stops
1004/// the producer first, then releases the threadsafe function.
1005fn render_listener_napi_fns(
1006    out: &mut String,
1007    l: &ListenerBinding,
1008    cb: &CallbackBinding,
1009    prefix: &str,
1010) {
1011    let register_sym = &l.register_symbol;
1012    let unregister_sym = &l.unregister_symbol;
1013    let tramp = format!("{}_napi_tramp", cb.c_fn_type);
1014    let calljs = format!("{}_napi_calljs", cb.c_fn_type);
1015
1016    out.push_str(&format!(
1017        "static napi_value Napi_{register_sym}(napi_env env, napi_callback_info info) {{\n"
1018    ));
1019    out.push_str("  size_t argc = 1;\n");
1020    out.push_str("  napi_value args[1];\n");
1021    out.push_str("  napi_get_cb_info(env, info, &argc, args, NULL, NULL);\n");
1022    out.push_str(&format!(
1023        "  {prefix}_napi_listener_ctx* ctx = ({prefix}_napi_listener_ctx*)calloc(1, sizeof({prefix}_napi_listener_ctx));\n"
1024    ));
1025    out.push_str("  napi_value resource_name;\n");
1026    out.push_str(&format!(
1027        "  napi_create_string_utf8(env, \"{register_sym}\", NAPI_AUTO_LENGTH, &resource_name);\n"
1028    ));
1029    out.push_str(&format!(
1030        "  napi_create_threadsafe_function(env, args[0], NULL, resource_name, 0, 1, NULL, NULL, NULL, {calljs}, &ctx->tsfn);\n"
1031    ));
1032    out.push_str("  napi_unref_threadsafe_function(env, ctx->tsfn);\n");
1033    out.push_str(&format!("  uint64_t id = {register_sym}({tramp}, ctx);\n"));
1034    out.push_str("  ctx->id = id;\n");
1035    out.push_str(&format!("  ctx->next = {prefix}_napi_listeners;\n"));
1036    out.push_str(&format!("  {prefix}_napi_listeners = ctx;\n"));
1037    out.push_str("  napi_value ret;\n");
1038    out.push_str("  napi_create_double(env, (double)id, &ret);\n");
1039    out.push_str("  return ret;\n");
1040    out.push_str("}\n\n");
1041
1042    out.push_str(&format!(
1043        "static napi_value Napi_{unregister_sym}(napi_env env, napi_callback_info info) {{\n"
1044    ));
1045    out.push_str("  size_t argc = 1;\n");
1046    out.push_str("  napi_value args[1];\n");
1047    out.push_str("  napi_get_cb_info(env, info, &argc, args, NULL, NULL);\n");
1048    out.push_str("  double id_d = 0;\n");
1049    out.push_str("  napi_get_value_double(env, args[0], &id_d);\n");
1050    out.push_str("  uint64_t id = (uint64_t)id_d;\n");
1051    // Stop producer-side delivery before tearing down the tsfn so no new
1052    // payloads are queued against a released function.
1053    out.push_str(&format!("  {unregister_sym}(id);\n"));
1054    out.push_str(&format!(
1055        "  {prefix}_napi_listener_ctx** link = &{prefix}_napi_listeners;\n"
1056    ));
1057    out.push_str("  while (*link != NULL) {\n");
1058    out.push_str("    if ((*link)->id == id) {\n");
1059    out.push_str(&format!(
1060        "      {prefix}_napi_listener_ctx* found = *link;\n"
1061    ));
1062    out.push_str("      *link = found->next;\n");
1063    out.push_str("      napi_release_threadsafe_function(found->tsfn, napi_tsfn_release);\n");
1064    out.push_str("      free(found);\n");
1065    out.push_str("      break;\n");
1066    out.push_str("    }\n");
1067    out.push_str("    link = &(*link)->next;\n");
1068    out.push_str("  }\n");
1069    out.push_str("  napi_value ret;\n");
1070    out.push_str("  napi_get_undefined(env, &ret);\n");
1071    out.push_str("  return ret;\n");
1072    out.push_str("}\n\n");
1073}
1074
1075fn async_cb_result_params_node(ret: Option<&TypeRef>, module: &str, prefix: &str) -> String {
1076    match ret {
1077        None => String::new(),
1078        Some(TypeRef::StringUtf8 | TypeRef::BorrowedStr) => ", const char* result".into(),
1079        Some(TypeRef::Bytes | TypeRef::BorrowedBytes) => {
1080            ", const uint8_t* result, size_t result_len".into()
1081        }
1082        Some(TypeRef::List(inner)) => {
1083            let et = c_elem_type(inner, module, prefix);
1084            format!(", {et}* result, size_t result_len")
1085        }
1086        Some(TypeRef::Map(k, v)) => {
1087            let kt = c_elem_type(k, module, prefix);
1088            let vt = c_elem_type(v, module, prefix);
1089            format!(", {kt}* result_keys, {vt}* result_values, size_t result_len")
1090        }
1091        Some(t) => format!(", {} result", c_ret_type_str(t, module, prefix)),
1092    }
1093}
1094
1095/// Emit the per-async-function machinery: a context struct carrying the
1096/// promise + threadsafe function + deep-copied results, the producer-thread
1097/// completion callback (which only copies and queues), and the JS-thread
1098/// marshaller (which settles the promise).
1099///
1100/// The completion callback may fire on any thread, so it must never touch
1101/// `napi_env`; the ref'd threadsafe function also keeps the event loop alive
1102/// until the promise settles.
1103fn render_async_machinery(
1104    out: &mut String,
1105    f: &FnBinding,
1106    c_name: &str,
1107    module: &str,
1108    prefix: &str,
1109    structs: &HashMap<String, StructBinding>,
1110) {
1111    let actx = format!("{c_name}_napi_actx");
1112    let cb_name = format!("{c_name}_napi_cb");
1113    let calljs = format!("{c_name}_napi_settle");
1114    let cb_result = async_cb_result_params_node(f.ret.as_ref(), module, prefix);
1115
1116    // -- context struct --
1117    out.push_str("typedef struct {\n");
1118    out.push_str("    napi_deferred deferred;\n");
1119    out.push_str("    napi_threadsafe_function tsfn;\n");
1120    out.push_str("    int32_t err_code;\n");
1121    out.push_str("    char* err_msg;\n");
1122    match f.ret.as_ref() {
1123        None => {}
1124        Some(TypeRef::I32) => out.push_str("    int32_t result;\n"),
1125        Some(TypeRef::U32) => out.push_str("    uint32_t result;\n"),
1126        Some(TypeRef::I64) => out.push_str("    int64_t result;\n"),
1127        Some(TypeRef::F64) => out.push_str("    double result;\n"),
1128        Some(TypeRef::I8) => out.push_str("    int8_t result;\n"),
1129        Some(TypeRef::I16) => out.push_str("    int16_t result;\n"),
1130        Some(TypeRef::U8) => out.push_str("    uint8_t result;\n"),
1131        Some(TypeRef::U16) => out.push_str("    uint16_t result;\n"),
1132        Some(TypeRef::U64) => out.push_str("    uint64_t result;\n"),
1133        Some(TypeRef::F32) => out.push_str("    float result;\n"),
1134        Some(TypeRef::Bool) => out.push_str("    bool result;\n"),
1135        Some(TypeRef::Enum(_)) => out.push_str("    int32_t result;\n"),
1136        Some(TypeRef::StringUtf8 | TypeRef::BorrowedStr) => {
1137            out.push_str("    char* result;\n");
1138            out.push_str("    int result_null;\n");
1139        }
1140        Some(TypeRef::Bytes | TypeRef::BorrowedBytes) => {
1141            out.push_str("    uint8_t* result;\n");
1142            out.push_str("    size_t result_len;\n");
1143        }
1144        Some(TypeRef::Handle) => out.push_str("    uint64_t result;\n"),
1145        Some(TypeRef::TypedHandle(_) | TypeRef::Struct(_) | TypeRef::Iterator(_)) => {
1146            out.push_str("    void* result;\n")
1147        }
1148        Some(TypeRef::Optional(inner)) => match inner.as_ref() {
1149            TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
1150                out.push_str("    char* result;\n");
1151                out.push_str("    int result_null;\n");
1152            }
1153            TypeRef::Struct(_) | TypeRef::TypedHandle(_) => {
1154                out.push_str("    void* result;\n");
1155            }
1156            other => {
1157                out.push_str("    int result_has;\n");
1158                out.push_str(&format!(
1159                    "    {} result;\n",
1160                    c_elem_type(other, module, prefix)
1161                ));
1162            }
1163        },
1164        Some(TypeRef::List(inner)) => {
1165            let elem = match inner.as_ref() {
1166                TypeRef::StringUtf8 | TypeRef::BorrowedStr => "char*".to_string(),
1167                other => c_elem_type(other, module, prefix),
1168            };
1169            out.push_str(&format!("    {elem}* result;\n"));
1170            out.push_str("    size_t result_len;\n");
1171        }
1172        Some(TypeRef::Map(k, v)) => {
1173            for (field, ty) in [("result_keys", k), ("result_values", v)] {
1174                let elem = match ty.as_ref() {
1175                    TypeRef::StringUtf8 | TypeRef::BorrowedStr => "char*".to_string(),
1176                    other => c_elem_type(other, module, prefix),
1177                };
1178                out.push_str(&format!("    {elem}* {field};\n"));
1179            }
1180            out.push_str("    size_t result_len;\n");
1181        }
1182    }
1183    out.push_str(&format!("}} {actx};\n\n"));
1184
1185    // -- producer-thread completion callback: deep-copy + queue --
1186    out.push_str(&format!(
1187        "static void {cb_name}(void* context, weaveffi_error* err{cb_result}) {{\n"
1188    ));
1189    out.push_str(&format!("    {actx}* ctx = ({actx}*)context;\n"));
1190    out.push_str("    if (err != NULL && err->code != 0) {\n");
1191    out.push_str("        ctx->err_code = err->code;\n");
1192    out.push_str(
1193        "        ctx->err_msg = err->message ? strdup(err->message) : strdup(\"unknown error\");\n",
1194    );
1195    out.push_str("    } else {\n");
1196    match f.ret.as_ref() {
1197        None => {}
1198        Some(
1199            TypeRef::I8
1200            | TypeRef::I16
1201            | TypeRef::I32
1202            | TypeRef::I64
1203            | TypeRef::U8
1204            | TypeRef::U16
1205            | TypeRef::U32
1206            | TypeRef::U64
1207            | TypeRef::F32
1208            | TypeRef::F64
1209            | TypeRef::Bool
1210            | TypeRef::Handle,
1211        ) => {
1212            out.push_str("        ctx->result = result;\n");
1213        }
1214        Some(TypeRef::Enum(_)) => {
1215            out.push_str("        ctx->result = (int32_t)result;\n");
1216        }
1217        Some(TypeRef::StringUtf8 | TypeRef::BorrowedStr) => {
1218            out.push_str("        ctx->result_null = result == NULL;\n");
1219            out.push_str("        ctx->result = result ? strdup(result) : NULL;\n");
1220        }
1221        Some(TypeRef::Bytes | TypeRef::BorrowedBytes) => {
1222            out.push_str("        ctx->result_len = result_len;\n");
1223            out.push_str(
1224                "        if (result != NULL && result_len > 0) { ctx->result = (uint8_t*)malloc(result_len); memcpy(ctx->result, result, result_len); }\n",
1225            );
1226        }
1227        // Ownership of struct/handle/iterator results transfers to the
1228        // receiver, so the pointer stays valid across the thread hop.
1229        Some(TypeRef::TypedHandle(_) | TypeRef::Struct(_) | TypeRef::Iterator(_)) => {
1230            out.push_str("        ctx->result = (void*)result;\n");
1231        }
1232        Some(TypeRef::Optional(inner)) => match inner.as_ref() {
1233            TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
1234                out.push_str("        ctx->result_null = result == NULL;\n");
1235                out.push_str("        ctx->result = result ? strdup(result) : NULL;\n");
1236            }
1237            TypeRef::Struct(_) | TypeRef::TypedHandle(_) => {
1238                out.push_str("        ctx->result = (void*)result;\n");
1239            }
1240            _ => {
1241                out.push_str("        ctx->result_has = result != NULL;\n");
1242                out.push_str("        if (result != NULL) ctx->result = *result;\n");
1243            }
1244        },
1245        Some(TypeRef::List(inner)) => {
1246            out.push_str("        ctx->result_len = result_len;\n");
1247            match inner.as_ref() {
1248                TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
1249                    out.push_str(
1250                        "        if (result != NULL && result_len > 0) {\n            ctx->result = (char**)calloc(result_len, sizeof(char*));\n            for (size_t i = 0; i < result_len; i++) ctx->result[i] = result[i] ? strdup(result[i]) : NULL;\n        }\n",
1251                    );
1252                }
1253                _ => {
1254                    out.push_str(
1255                        "        if (result != NULL && result_len > 0) { ctx->result = malloc(result_len * sizeof(*ctx->result)); memcpy(ctx->result, result, result_len * sizeof(*ctx->result)); }\n",
1256                    );
1257                }
1258            }
1259        }
1260        Some(TypeRef::Map(k, v)) => {
1261            out.push_str("        ctx->result_len = result_len;\n");
1262            for (field, src, ty) in [
1263                ("result_keys", "result_keys", k),
1264                ("result_values", "result_values", v),
1265            ] {
1266                match ty.as_ref() {
1267                    TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
1268                        out.push_str(&format!(
1269                            "        if ({src} != NULL && result_len > 0) {{\n            ctx->{field} = (char**)calloc(result_len, sizeof(char*));\n            for (size_t i = 0; i < result_len; i++) ctx->{field}[i] = {src}[i] ? strdup({src}[i]) : NULL;\n        }}\n"
1270                        ));
1271                    }
1272                    _ => {
1273                        out.push_str(&format!(
1274                            "        if ({src} != NULL && result_len > 0) {{ ctx->{field} = malloc(result_len * sizeof(*ctx->{field})); memcpy(ctx->{field}, {src}, result_len * sizeof(*ctx->{field})); }}\n"
1275                        ));
1276                    }
1277                }
1278            }
1279        }
1280    }
1281    out.push_str("    }\n");
1282    out.push_str("    napi_call_threadsafe_function(ctx->tsfn, ctx, napi_tsfn_blocking);\n");
1283    out.push_str("}\n\n");
1284
1285    // -- JS-thread marshaller: settle the promise, free, release --
1286    out.push_str(&format!(
1287        "static void {calljs}(napi_env env, napi_value js_cb, void* context, void* data) {{\n"
1288    ));
1289    out.push_str("    (void)js_cb;\n");
1290    out.push_str("    (void)context;\n");
1291    out.push_str(&format!("    {actx}* ctx = ({actx}*)data;\n"));
1292    out.push_str("    if (env != NULL) {\n");
1293    out.push_str("    if (ctx->err_code != 0) {\n");
1294    out.push_str("        napi_value err_msg;\n");
1295    out.push_str(
1296        "        napi_create_string_utf8(env, ctx->err_msg ? ctx->err_msg : \"\", NAPI_AUTO_LENGTH, &err_msg);\n",
1297    );
1298    out.push_str("        napi_value err_obj;\n");
1299    out.push_str("        napi_create_error(env, NULL, err_msg, &err_obj);\n");
1300    out.push_str("        napi_value err_code;\n");
1301    out.push_str("        napi_create_int32(env, ctx->err_code, &err_code);\n");
1302    out.push_str("        napi_set_named_property(env, err_obj, \"code\", err_code);\n");
1303    out.push_str("        napi_reject_deferred(env, ctx->deferred, err_obj);\n");
1304    out.push_str("    } else {\n");
1305    out.push_str("        napi_value val;\n");
1306    match f.ret.as_ref() {
1307        None => out.push_str("        napi_get_undefined(env, &val);\n"),
1308        Some(TypeRef::I32) => out.push_str("        napi_create_int32(env, ctx->result, &val);\n"),
1309        Some(TypeRef::U32) => out.push_str("        napi_create_uint32(env, ctx->result, &val);\n"),
1310        Some(TypeRef::I64) => out.push_str("        napi_create_int64(env, ctx->result, &val);\n"),
1311        Some(TypeRef::F64) => out.push_str("        napi_create_double(env, ctx->result, &val);\n"),
1312        Some(TypeRef::I8 | TypeRef::I16) => {
1313            out.push_str("        napi_create_int32(env, ctx->result, &val);\n")
1314        }
1315        Some(TypeRef::U8 | TypeRef::U16) => {
1316            out.push_str("        napi_create_uint32(env, ctx->result, &val);\n")
1317        }
1318        Some(TypeRef::U64) => {
1319            out.push_str("        napi_create_int64(env, (int64_t)ctx->result, &val);\n")
1320        }
1321        Some(TypeRef::F32) => out.push_str("        napi_create_double(env, ctx->result, &val);\n"),
1322        Some(TypeRef::Bool) => out.push_str("        napi_get_boolean(env, ctx->result, &val);\n"),
1323        Some(TypeRef::Enum(_)) => {
1324            out.push_str("        napi_create_int32(env, ctx->result, &val);\n");
1325        }
1326        Some(TypeRef::StringUtf8 | TypeRef::BorrowedStr) => {
1327            out.push_str(
1328                "        if (ctx->result_null) napi_get_null(env, &val); else napi_create_string_utf8(env, ctx->result ? ctx->result : \"\", NAPI_AUTO_LENGTH, &val);\n",
1329            );
1330        }
1331        Some(TypeRef::Bytes | TypeRef::BorrowedBytes) => {
1332            out.push_str(
1333                "        napi_create_buffer_copy(env, ctx->result_len, ctx->result ? (const void*)ctx->result : (const void*)\"\", NULL, &val);\n",
1334            );
1335        }
1336        Some(TypeRef::Handle) => {
1337            out.push_str("        napi_create_int64(env, (int64_t)ctx->result, &val);\n");
1338        }
1339        Some(TypeRef::TypedHandle(_) | TypeRef::Iterator(_)) => {
1340            out.push_str("        napi_create_int64(env, (int64_t)(intptr_t)ctx->result, &val);\n");
1341        }
1342        Some(TypeRef::Struct(name)) => {
1343            emit_struct_to_object(
1344                out,
1345                "env",
1346                name,
1347                "ctx->result",
1348                "val",
1349                module,
1350                prefix,
1351                structs,
1352                "        ",
1353                true,
1354            );
1355        }
1356        Some(TypeRef::Optional(inner)) => match inner.as_ref() {
1357            TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
1358                out.push_str(
1359                    "        if (ctx->result_null) napi_get_null(env, &val); else napi_create_string_utf8(env, ctx->result ? ctx->result : \"\", NAPI_AUTO_LENGTH, &val);\n",
1360                );
1361            }
1362            TypeRef::Struct(name) => {
1363                out.push_str(
1364                    "        if (ctx->result == NULL) { napi_get_null(env, &val); } else {\n",
1365                );
1366                emit_struct_to_object(
1367                    out,
1368                    "env",
1369                    name,
1370                    "ctx->result",
1371                    "val",
1372                    module,
1373                    prefix,
1374                    structs,
1375                    "            ",
1376                    true,
1377                );
1378                out.push_str("        }\n");
1379            }
1380            TypeRef::TypedHandle(_) => {
1381                out.push_str(
1382                    "        if (ctx->result == NULL) napi_get_null(env, &val); else napi_create_int64(env, (int64_t)(intptr_t)ctx->result, &val);\n",
1383                );
1384            }
1385            other => {
1386                let leaf = payload_leaf_to_napi(other, "ctx->result", "val");
1387                out.push_str(&format!(
1388                    "        if (ctx->result_has) {{ {leaf} }} else napi_get_null(env, &val);\n"
1389                ));
1390            }
1391        },
1392        Some(TypeRef::List(inner)) => {
1393            out.push_str("        napi_create_array(env, &val);\n");
1394            out.push_str(
1395                "        for (size_t i = 0; ctx->result != NULL && i < ctx->result_len; i++) {\n",
1396            );
1397            out.push_str("            napi_value elem;\n");
1398            let leaf = payload_elem_to_napi(inner, "ctx->result[i]", "elem");
1399            out.push_str(&format!("            {leaf}\n"));
1400            out.push_str("            napi_set_element(env, val, (uint32_t)i, elem);\n");
1401            out.push_str("        }\n");
1402        }
1403        Some(TypeRef::Map(k, v)) => {
1404            out.push_str("        napi_create_object(env, &val);\n");
1405            out.push_str(
1406                "        for (size_t i = 0; ctx->result_keys != NULL && ctx->result_values != NULL && i < ctx->result_len; i++) {\n",
1407            );
1408            out.push_str("            napi_value mk; napi_value mv;\n");
1409            let kc = payload_elem_to_napi(k, "ctx->result_keys[i]", "mk");
1410            let vc = payload_elem_to_napi(v, "ctx->result_values[i]", "mv");
1411            out.push_str(&format!("            {kc}\n"));
1412            out.push_str(&format!("            {vc}\n"));
1413            out.push_str("            napi_set_property(env, val, mk, mv);\n");
1414            out.push_str("        }\n");
1415        }
1416    }
1417    out.push_str("        napi_resolve_deferred(env, ctx->deferred, val);\n");
1418    out.push_str("    }\n");
1419    out.push_str("    }\n");
1420    out.push_str("    free(ctx->err_msg);\n");
1421    match f.ret.as_ref() {
1422        Some(
1423            TypeRef::StringUtf8 | TypeRef::BorrowedStr | TypeRef::Bytes | TypeRef::BorrowedBytes,
1424        ) => {
1425            out.push_str("    free(ctx->result);\n");
1426        }
1427        Some(TypeRef::Optional(inner))
1428            if matches!(inner.as_ref(), TypeRef::StringUtf8 | TypeRef::BorrowedStr) =>
1429        {
1430            out.push_str("    free(ctx->result);\n");
1431        }
1432        Some(TypeRef::List(inner)) => {
1433            if matches!(inner.as_ref(), TypeRef::StringUtf8 | TypeRef::BorrowedStr) {
1434                out.push_str(
1435                    "    for (size_t i = 0; ctx->result != NULL && i < ctx->result_len; i++) free(ctx->result[i]);\n",
1436                );
1437            }
1438            out.push_str("    free(ctx->result);\n");
1439        }
1440        Some(TypeRef::Map(k, v)) => {
1441            for (field, ty) in [("result_keys", k), ("result_values", v)] {
1442                if matches!(ty.as_ref(), TypeRef::StringUtf8 | TypeRef::BorrowedStr) {
1443                    out.push_str(&format!(
1444                        "    for (size_t i = 0; ctx->{field} != NULL && i < ctx->result_len; i++) free(ctx->{field}[i]);\n"
1445                    ));
1446                }
1447                out.push_str(&format!("    free(ctx->{field});\n"));
1448            }
1449        }
1450        _ => {}
1451    }
1452    out.push_str("    napi_release_threadsafe_function(ctx->tsfn, napi_tsfn_release);\n");
1453    out.push_str("    free(ctx);\n");
1454    out.push_str("}\n\n");
1455}
1456
1457fn render_async_napi_body(
1458    out: &mut String,
1459    f: &FnBinding,
1460    c_name: &str,
1461    module: &str,
1462    prefix: &str,
1463) {
1464    let n = f.params.len();
1465    if n > 0 {
1466        out.push_str(&format!("  size_t argc = {n};\n"));
1467        out.push_str(&format!("  napi_value args[{n}];\n"));
1468        out.push_str("  napi_get_cb_info(env, info, &argc, args, NULL, NULL);\n");
1469    } else {
1470        out.push_str("  size_t argc = 0;\n");
1471        out.push_str("  napi_get_cb_info(env, info, &argc, NULL, NULL, NULL);\n");
1472    }
1473
1474    let mut c_args: Vec<String> = Vec::new();
1475    let mut cleanups: Vec<String> = Vec::new();
1476    for (i, p) in f.params.iter().enumerate() {
1477        emit_param(
1478            out,
1479            &mut c_args,
1480            &mut cleanups,
1481            &p.ty,
1482            &p.name,
1483            i,
1484            module,
1485            prefix,
1486        );
1487    }
1488
1489    let actx = format!("{c_name}_napi_actx");
1490    out.push_str(&format!(
1491        "  {actx}* ctx = ({actx}*)calloc(1, sizeof({actx}));\n"
1492    ));
1493    out.push_str("  napi_value promise;\n");
1494    out.push_str("  napi_create_promise(env, &ctx->deferred, &promise);\n");
1495    out.push_str("  napi_value resource_name;\n");
1496    out.push_str(&format!(
1497        "  napi_create_string_utf8(env, \"{c_name}\", NAPI_AUTO_LENGTH, &resource_name);\n"
1498    ));
1499    // Ref'd (unlike listeners): a pending promise must keep the loop alive.
1500    out.push_str(&format!(
1501        "  napi_create_threadsafe_function(env, NULL, NULL, resource_name, 0, 1, NULL, NULL, NULL, {c_name}_napi_settle, &ctx->tsfn);\n"
1502    ));
1503
1504    if f.cancellable {
1505        c_args.push("NULL".into());
1506    }
1507
1508    let cb_name = format!("{c_name}_napi_cb");
1509    c_args.push(cb_name);
1510    c_args.push("ctx".into());
1511    let args_str = c_args.join(", ");
1512    out.push_str(&format!("  {c_name}_async({args_str});\n"));
1513
1514    for cleanup in &cleanups {
1515        out.push_str(cleanup);
1516    }
1517
1518    out.push_str("  return promise;\n");
1519}
1520
1521fn render_napi_body(
1522    out: &mut String,
1523    f: &FnBinding,
1524    c_name: &str,
1525    module: &str,
1526    prefix: &str,
1527    structs: &HashMap<String, StructBinding>,
1528) {
1529    let n = f.params.len();
1530    if n > 0 {
1531        out.push_str(&format!("  size_t argc = {n};\n"));
1532        out.push_str(&format!("  napi_value args[{n}];\n"));
1533        out.push_str("  napi_get_cb_info(env, info, &argc, args, NULL, NULL);\n");
1534    } else {
1535        out.push_str("  size_t argc = 0;\n");
1536        out.push_str("  napi_get_cb_info(env, info, &argc, NULL, NULL, NULL);\n");
1537    }
1538
1539    let mut c_args: Vec<String> = Vec::new();
1540    let mut cleanups: Vec<String> = Vec::new();
1541    for (i, p) in f.params.iter().enumerate() {
1542        emit_param(
1543            out,
1544            &mut c_args,
1545            &mut cleanups,
1546            &p.ty,
1547            &p.name,
1548            i,
1549            module,
1550            prefix,
1551        );
1552    }
1553
1554    out.push_str("  weaveffi_error err = {0};\n");
1555
1556    if let Some(ret) = &f.ret {
1557        emit_ret_out_params(out, &mut c_args, ret, module, prefix);
1558    }
1559    c_args.push("&err".to_string());
1560
1561    let args_str = c_args.join(", ");
1562    let ret_type = f.ret.as_ref().map(|r| c_ret_type_str(r, module, prefix));
1563    match &ret_type {
1564        Some(rt) if rt != "void" => {
1565            out.push_str(&format!("  {rt} result = {c_name}({args_str});\n"));
1566        }
1567        _ => {
1568            out.push_str(&format!("  {c_name}({args_str});\n"));
1569        }
1570    }
1571
1572    for cleanup in &cleanups {
1573        out.push_str(cleanup);
1574    }
1575
1576    out.push_str("  if (err.code != 0) {\n");
1577    out.push_str("    napi_throw_error(env, NULL, err.message);\n");
1578    out.push_str("    weaveffi_error_clear(&err);\n");
1579    out.push_str("    return NULL;\n");
1580    out.push_str("  }\n");
1581
1582    match &f.ret {
1583        Some(ret) => emit_ret_to_napi(out, ret, module, prefix, &f.name, structs),
1584        None => {
1585            out.push_str("  napi_value ret;\n");
1586            out.push_str("  napi_get_undefined(env, &ret);\n");
1587            out.push_str("  return ret;\n");
1588        }
1589    }
1590}
1591
1592#[allow(clippy::too_many_arguments)]
1593fn emit_param(
1594    out: &mut String,
1595    c_args: &mut Vec<String>,
1596    cleanups: &mut Vec<String>,
1597    ty: &TypeRef,
1598    name: &str,
1599    idx: usize,
1600    module: &str,
1601    prefix: &str,
1602) {
1603    match ty {
1604        TypeRef::I32 | TypeRef::U32 | TypeRef::I64 | TypeRef::F64 | TypeRef::Bool => {
1605            let ct = c_elem_type(ty, module, prefix);
1606            let getter = napi_getter(ty);
1607            out.push_str(&format!("  {ct} {name};\n"));
1608            out.push_str(&format!("  {getter}(env, args[{idx}], &{name});\n"));
1609            c_args.push(name.into());
1610        }
1611        // N-API has no narrower-than-32-bit / float getter, so read into a
1612        // correctly-sized temporary and narrow to the real ABI type.
1613        TypeRef::I8 | TypeRef::I16 | TypeRef::U8 | TypeRef::U16 | TypeRef::U64 | TypeRef::F32 => {
1614            let ct = c_elem_type(ty, module, prefix);
1615            let getter = napi_getter(ty);
1616            let raw = napi_read_tmp_type(ty);
1617            out.push_str(&format!("  {raw} {name}_raw;\n"));
1618            out.push_str(&format!("  {getter}(env, args[{idx}], &{name}_raw);\n"));
1619            c_args.push(format!("({ct}){name}_raw"));
1620        }
1621        TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
1622            out.push_str(&format!("  size_t {name}_len;\n"));
1623            out.push_str(&format!(
1624                "  napi_get_value_string_utf8(env, args[{idx}], NULL, 0, &{name}_len);\n"
1625            ));
1626            out.push_str(&format!(
1627                "  char* {name} = (char*)malloc({name}_len + 1);\n"
1628            ));
1629            out.push_str(&format!(
1630                "  napi_get_value_string_utf8(env, args[{idx}], {name}, {name}_len + 1, &{name}_len);\n"
1631            ));
1632            c_args.push(name.into());
1633            cleanups.push(format!("  free({name});\n"));
1634        }
1635        TypeRef::Handle => {
1636            out.push_str(&format!("  int64_t {name}_raw;\n"));
1637            out.push_str(&format!(
1638                "  napi_get_value_int64(env, args[{idx}], &{name}_raw);\n"
1639            ));
1640            c_args.push(format!("(weaveffi_handle_t){name}_raw"));
1641        }
1642        TypeRef::TypedHandle(s) => {
1643            let abi = c_abi_struct_name(s, module, prefix);
1644            out.push_str(&format!("  int64_t {name}_raw;\n"));
1645            out.push_str(&format!(
1646                "  napi_get_value_int64(env, args[{idx}], &{name}_raw);\n"
1647            ));
1648            c_args.push(format!("({abi}*)(intptr_t){name}_raw"));
1649        }
1650        TypeRef::Enum(e) => {
1651            out.push_str(&format!("  int32_t {name};\n"));
1652            out.push_str(&format!(
1653                "  napi_get_value_int32(env, args[{idx}], &{name});\n"
1654            ));
1655            c_args.push(format!("({prefix}_{module}_{e}){name}"));
1656        }
1657        TypeRef::Struct(s) => {
1658            let abi = c_abi_struct_name(s, module, prefix);
1659            out.push_str(&format!("  int64_t {name}_raw;\n"));
1660            out.push_str(&format!(
1661                "  napi_get_value_int64(env, args[{idx}], &{name}_raw);\n"
1662            ));
1663            c_args.push(format!("(const {abi}*)(intptr_t){name}_raw"));
1664        }
1665        TypeRef::Optional(inner) => {
1666            out.push_str(&format!("  napi_valuetype {name}_type;\n"));
1667            out.push_str(&format!("  napi_typeof(env, args[{idx}], &{name}_type);\n"));
1668            emit_optional_param(out, c_args, cleanups, inner, name, idx, module, prefix);
1669        }
1670        TypeRef::List(inner) => {
1671            emit_list_param(out, c_args, cleanups, inner, name, idx, module, prefix);
1672        }
1673        TypeRef::Bytes | TypeRef::BorrowedBytes => {
1674            out.push_str(&format!("  void* {name}_raw;\n"));
1675            out.push_str(&format!("  size_t {name}_len;\n"));
1676            out.push_str(&format!(
1677                "  napi_get_buffer_info(env, args[{idx}], &{name}_raw, &{name}_len);\n"
1678            ));
1679            c_args.push(format!("(const uint8_t*){name}_raw"));
1680            c_args.push(format!("{name}_len"));
1681        }
1682        TypeRef::Map(k, v) => {
1683            emit_map_param(out, c_args, cleanups, k, v, name, idx, module, prefix);
1684        }
1685        TypeRef::Iterator(_) => unreachable!("iterator not valid as parameter"),
1686    }
1687}
1688
1689fn emit_opt_val(
1690    out: &mut String,
1691    c_args: &mut Vec<String>,
1692    c_type: &str,
1693    napi_fn: &str,
1694    name: &str,
1695    idx: usize,
1696) {
1697    out.push_str(&format!("  {c_type} {name}_val;\n"));
1698    out.push_str(&format!("  const {c_type}* {name}_ptr = NULL;\n"));
1699    out.push_str(&format!(
1700        "  if ({name}_type != napi_null && {name}_type != napi_undefined) {{\n"
1701    ));
1702    out.push_str(&format!("    {napi_fn}(env, args[{idx}], &{name}_val);\n"));
1703    out.push_str(&format!("    {name}_ptr = &{name}_val;\n"));
1704    out.push_str("  }\n");
1705    c_args.push(format!("{name}_ptr"));
1706}
1707
1708#[allow(clippy::too_many_arguments)]
1709fn emit_optional_param(
1710    out: &mut String,
1711    c_args: &mut Vec<String>,
1712    cleanups: &mut Vec<String>,
1713    inner: &TypeRef,
1714    name: &str,
1715    idx: usize,
1716    module: &str,
1717    prefix: &str,
1718) {
1719    match inner {
1720        TypeRef::I32 => {
1721            emit_opt_val(out, c_args, "int32_t", "napi_get_value_int32", name, idx);
1722        }
1723        TypeRef::U32 => {
1724            emit_opt_val(out, c_args, "uint32_t", "napi_get_value_uint32", name, idx);
1725        }
1726        TypeRef::I64 => {
1727            emit_opt_val(out, c_args, "int64_t", "napi_get_value_int64", name, idx);
1728        }
1729        TypeRef::F64 => {
1730            emit_opt_val(out, c_args, "double", "napi_get_value_double", name, idx);
1731        }
1732        TypeRef::Bool => {
1733            emit_opt_val(out, c_args, "bool", "napi_get_value_bool", name, idx);
1734        }
1735        TypeRef::Handle => {
1736            out.push_str(&format!("  int64_t {name}_raw = 0;\n"));
1737            out.push_str(&format!("  weaveffi_handle_t {name}_val;\n"));
1738            out.push_str(&format!("  const weaveffi_handle_t* {name}_ptr = NULL;\n"));
1739            out.push_str(&format!(
1740                "  if ({name}_type != napi_null && {name}_type != napi_undefined) {{\n"
1741            ));
1742            out.push_str(&format!(
1743                "    napi_get_value_int64(env, args[{idx}], &{name}_raw);\n"
1744            ));
1745            out.push_str(&format!(
1746                "    {name}_val = (weaveffi_handle_t){name}_raw;\n"
1747            ));
1748            out.push_str(&format!("    {name}_ptr = &{name}_val;\n"));
1749            out.push_str("  }\n");
1750            c_args.push(format!("{name}_ptr"));
1751        }
1752        // A typed handle is a nullable opaque pointer, so an optional one maps to
1753        // the same pointer with NULL standing in for absence — mirroring structs.
1754        TypeRef::TypedHandle(s) => {
1755            let abi = c_abi_struct_name(s, module, prefix);
1756            out.push_str(&format!("  int64_t {name}_raw = 0;\n"));
1757            out.push_str(&format!(
1758                "  if ({name}_type != napi_null && {name}_type != napi_undefined) {{\n"
1759            ));
1760            out.push_str(&format!(
1761                "    napi_get_value_int64(env, args[{idx}], &{name}_raw);\n"
1762            ));
1763            out.push_str("  }\n");
1764            c_args.push(format!("{name}_raw ? ({abi}*)(intptr_t){name}_raw : NULL"));
1765        }
1766        TypeRef::Enum(e) => {
1767            let etype = format!("{prefix}_{module}_{e}");
1768            out.push_str(&format!("  int32_t {name}_raw;\n"));
1769            out.push_str(&format!("  {etype} {name}_val;\n"));
1770            out.push_str(&format!("  const {etype}* {name}_ptr = NULL;\n"));
1771            out.push_str(&format!(
1772                "  if ({name}_type != napi_null && {name}_type != napi_undefined) {{\n"
1773            ));
1774            out.push_str(&format!(
1775                "    napi_get_value_int32(env, args[{idx}], &{name}_raw);\n"
1776            ));
1777            out.push_str(&format!("    {name}_val = ({etype}){name}_raw;\n"));
1778            out.push_str(&format!("    {name}_ptr = &{name}_val;\n"));
1779            out.push_str("  }\n");
1780            c_args.push(format!("{name}_ptr"));
1781        }
1782        TypeRef::StringUtf8 => {
1783            out.push_str(&format!("  char* {name} = NULL;\n"));
1784            out.push_str(&format!(
1785                "  if ({name}_type != napi_null && {name}_type != napi_undefined) {{\n"
1786            ));
1787            out.push_str(&format!("    size_t {name}_len;\n"));
1788            out.push_str(&format!(
1789                "    napi_get_value_string_utf8(env, args[{idx}], NULL, 0, &{name}_len);\n"
1790            ));
1791            out.push_str(&format!("    {name} = (char*)malloc({name}_len + 1);\n"));
1792            out.push_str(&format!(
1793                "    napi_get_value_string_utf8(env, args[{idx}], {name}, {name}_len + 1, &{name}_len);\n"
1794            ));
1795            out.push_str("  }\n");
1796            c_args.push(name.into());
1797            cleanups.push(format!("  free({name});\n"));
1798        }
1799        TypeRef::Struct(s) => {
1800            let abi = c_abi_struct_name(s, module, prefix);
1801            out.push_str(&format!("  int64_t {name}_raw = 0;\n"));
1802            out.push_str(&format!(
1803                "  if ({name}_type != napi_null && {name}_type != napi_undefined) {{\n"
1804            ));
1805            out.push_str(&format!(
1806                "    napi_get_value_int64(env, args[{idx}], &{name}_raw);\n"
1807            ));
1808            out.push_str("  }\n");
1809            c_args.push(format!(
1810                "{name}_raw ? (const {abi}*)(intptr_t){name}_raw : NULL"
1811            ));
1812        }
1813        // Optional narrow numerics: read through a wider N-API getter into a
1814        // temporary, narrow to the ABI type, then pass a pointer (NULL absent).
1815        TypeRef::I8 | TypeRef::I16 | TypeRef::U8 | TypeRef::U16 | TypeRef::U64 | TypeRef::F32 => {
1816            let ct = c_elem_type(inner, module, prefix);
1817            let getter = napi_getter(inner);
1818            let raw = napi_read_tmp_type(inner);
1819            out.push_str(&format!("  {ct} {name}_val;\n"));
1820            out.push_str(&format!("  const {ct}* {name}_ptr = NULL;\n"));
1821            out.push_str(&format!(
1822                "  if ({name}_type != napi_null && {name}_type != napi_undefined) {{\n"
1823            ));
1824            out.push_str(&format!("    {raw} {name}_raw;\n"));
1825            out.push_str(&format!("    {getter}(env, args[{idx}], &{name}_raw);\n"));
1826            out.push_str(&format!("    {name}_val = ({ct}){name}_raw;\n"));
1827            out.push_str(&format!("    {name}_ptr = &{name}_val;\n"));
1828            out.push_str("  }\n");
1829            c_args.push(format!("{name}_ptr"));
1830        }
1831        _ => {
1832            emit_param(out, c_args, cleanups, inner, name, idx, module, prefix);
1833        }
1834    }
1835}
1836
1837#[allow(clippy::too_many_arguments)]
1838fn emit_list_param(
1839    out: &mut String,
1840    c_args: &mut Vec<String>,
1841    cleanups: &mut Vec<String>,
1842    inner: &TypeRef,
1843    name: &str,
1844    idx: usize,
1845    module: &str,
1846    prefix: &str,
1847) {
1848    let et = c_elem_type(inner, module, prefix);
1849    out.push_str(&format!("  uint32_t {name}_count;\n"));
1850    out.push_str(&format!(
1851        "  napi_get_array_length(env, args[{idx}], &{name}_count);\n"
1852    ));
1853    out.push_str(&format!(
1854        "  {et}* {name}_arr = ({et}*)malloc(sizeof({et}) * ({name}_count + 1));\n"
1855    ));
1856    out.push_str(&format!(
1857        "  for (uint32_t {name}_i = 0; {name}_i < {name}_count; {name}_i++) {{\n"
1858    ));
1859    out.push_str(&format!("    napi_value {name}_el;\n"));
1860    out.push_str(&format!(
1861        "    napi_get_element(env, args[{idx}], {name}_i, &{name}_el);\n"
1862    ));
1863
1864    match inner {
1865        TypeRef::I32 | TypeRef::U32 | TypeRef::I64 | TypeRef::F64 | TypeRef::Bool => {
1866            let getter = napi_getter(inner);
1867            out.push_str(&format!(
1868                "    {getter}(env, {name}_el, &{name}_arr[{name}_i]);\n"
1869            ));
1870        }
1871        // Narrow numerics need a wider read temporary, then a narrowing cast
1872        // into the element slot so the 1/2/8-byte element is not overrun.
1873        TypeRef::I8 | TypeRef::I16 | TypeRef::U8 | TypeRef::U16 | TypeRef::U64 | TypeRef::F32 => {
1874            let getter = napi_getter(inner);
1875            let raw = napi_read_tmp_type(inner);
1876            out.push_str(&format!("    {raw} {name}_nv;\n"));
1877            out.push_str(&format!("    {getter}(env, {name}_el, &{name}_nv);\n"));
1878            out.push_str(&format!("    {name}_arr[{name}_i] = ({et}){name}_nv;\n"));
1879        }
1880        TypeRef::Handle => {
1881            out.push_str(&format!("    int64_t {name}_h;\n"));
1882            out.push_str(&format!(
1883                "    napi_get_value_int64(env, {name}_el, &{name}_h);\n"
1884            ));
1885            out.push_str(&format!(
1886                "    {name}_arr[{name}_i] = (weaveffi_handle_t){name}_h;\n"
1887            ));
1888        }
1889        TypeRef::TypedHandle(s) => {
1890            let abi = c_abi_struct_name(s, module, prefix);
1891            out.push_str(&format!("    int64_t {name}_h;\n"));
1892            out.push_str(&format!(
1893                "    napi_get_value_int64(env, {name}_el, &{name}_h);\n"
1894            ));
1895            out.push_str(&format!(
1896                "    {name}_arr[{name}_i] = ({abi}*)(intptr_t){name}_h;\n"
1897            ));
1898        }
1899        TypeRef::Enum(_) => {
1900            out.push_str(&format!("    int32_t {name}_ev;\n"));
1901            out.push_str(&format!(
1902                "    napi_get_value_int32(env, {name}_el, &{name}_ev);\n"
1903            ));
1904            out.push_str(&format!("    {name}_arr[{name}_i] = ({et}){name}_ev;\n"));
1905        }
1906        TypeRef::StringUtf8 => {
1907            out.push_str(&format!("    size_t {name}_sl;\n"));
1908            out.push_str(&format!(
1909                "    napi_get_value_string_utf8(env, {name}_el, NULL, 0, &{name}_sl);\n"
1910            ));
1911            out.push_str(&format!(
1912                "    char* {name}_s = (char*)malloc({name}_sl + 1);\n"
1913            ));
1914            out.push_str(&format!(
1915                "    napi_get_value_string_utf8(env, {name}_el, {name}_s, {name}_sl + 1, &{name}_sl);\n"
1916            ));
1917            out.push_str(&format!("    {name}_arr[{name}_i] = {name}_s;\n"));
1918        }
1919        TypeRef::Struct(_) => {
1920            out.push_str(&format!("    int64_t {name}_sp;\n"));
1921            out.push_str(&format!(
1922                "    napi_get_value_int64(env, {name}_el, &{name}_sp);\n"
1923            ));
1924            out.push_str(&format!(
1925                "    {name}_arr[{name}_i] = ({et})(intptr_t){name}_sp;\n"
1926            ));
1927        }
1928        _ => {
1929            let getter = napi_getter(inner);
1930            out.push_str(&format!(
1931                "    {getter}(env, {name}_el, &{name}_arr[{name}_i]);\n"
1932            ));
1933        }
1934    }
1935
1936    out.push_str("  }\n");
1937    c_args.push(format!("{name}_arr"));
1938    c_args.push(format!("(size_t){name}_count"));
1939
1940    if matches!(inner, TypeRef::StringUtf8) {
1941        cleanups.push(format!(
1942            "  for (uint32_t {name}_j = 0; {name}_j < {name}_count; {name}_j++) free((void*){name}_arr[{name}_j]);\n"
1943        ));
1944    }
1945    cleanups.push(format!("  free({name}_arr);\n"));
1946}
1947
1948#[allow(clippy::too_many_arguments)]
1949fn emit_map_param(
1950    out: &mut String,
1951    c_args: &mut Vec<String>,
1952    cleanups: &mut Vec<String>,
1953    k: &TypeRef,
1954    v: &TypeRef,
1955    name: &str,
1956    idx: usize,
1957    module: &str,
1958    prefix: &str,
1959) {
1960    let kt = c_elem_type(k, module, prefix);
1961    let vt = c_elem_type(v, module, prefix);
1962    out.push_str(&format!("  napi_value {name}_keys_napi;\n"));
1963    out.push_str(&format!(
1964        "  napi_get_property_names(env, args[{idx}], &{name}_keys_napi);\n"
1965    ));
1966    out.push_str(&format!("  uint32_t {name}_count;\n"));
1967    out.push_str(&format!(
1968        "  napi_get_array_length(env, {name}_keys_napi, &{name}_count);\n"
1969    ));
1970    out.push_str(&format!(
1971        "  {kt}* {name}_keys = ({kt}*)malloc(sizeof({kt}) * ({name}_count + 1));\n"
1972    ));
1973    out.push_str(&format!(
1974        "  {vt}* {name}_values = ({vt}*)malloc(sizeof({vt}) * ({name}_count + 1));\n"
1975    ));
1976    out.push_str(&format!(
1977        "  for (uint32_t {name}_i = 0; {name}_i < {name}_count; {name}_i++) {{\n"
1978    ));
1979    out.push_str(&format!("    napi_value {name}_k;\n"));
1980    out.push_str(&format!(
1981        "    napi_get_element(env, {name}_keys_napi, {name}_i, &{name}_k);\n"
1982    ));
1983
1984    if matches!(k, TypeRef::StringUtf8) {
1985        out.push_str(&format!("    size_t {name}_kl;\n"));
1986        out.push_str(&format!(
1987            "    napi_get_value_string_utf8(env, {name}_k, NULL, 0, &{name}_kl);\n"
1988        ));
1989        out.push_str(&format!(
1990            "    char* {name}_ks = (char*)malloc({name}_kl + 1);\n"
1991        ));
1992        out.push_str(&format!(
1993            "    napi_get_value_string_utf8(env, {name}_k, {name}_ks, {name}_kl + 1, &{name}_kl);\n"
1994        ));
1995        out.push_str(&format!("    {name}_keys[{name}_i] = {name}_ks;\n"));
1996    } else if needs_narrowing_read(k) {
1997        out.push_str(&format!("    napi_value {name}_kn;\n"));
1998        out.push_str(&format!(
1999            "    napi_coerce_to_number(env, {name}_k, &{name}_kn);\n"
2000        ));
2001        let kgetter = napi_getter(k);
2002        let raw = napi_read_tmp_type(k);
2003        out.push_str(&format!("    {raw} {name}_kv;\n"));
2004        out.push_str(&format!("    {kgetter}(env, {name}_kn, &{name}_kv);\n"));
2005        out.push_str(&format!("    {name}_keys[{name}_i] = ({kt}){name}_kv;\n"));
2006    } else {
2007        out.push_str(&format!("    napi_value {name}_kn;\n"));
2008        out.push_str(&format!(
2009            "    napi_coerce_to_number(env, {name}_k, &{name}_kn);\n"
2010        ));
2011        let kgetter = napi_getter(k);
2012        out.push_str(&format!(
2013            "    {kgetter}(env, {name}_kn, &{name}_keys[{name}_i]);\n"
2014        ));
2015    }
2016
2017    out.push_str(&format!("    napi_value {name}_v;\n"));
2018    out.push_str(&format!(
2019        "    napi_get_property(env, args[{idx}], {name}_k, &{name}_v);\n"
2020    ));
2021
2022    if matches!(v, TypeRef::StringUtf8) {
2023        out.push_str(&format!("    size_t {name}_vl;\n"));
2024        out.push_str(&format!(
2025            "    napi_get_value_string_utf8(env, {name}_v, NULL, 0, &{name}_vl);\n"
2026        ));
2027        out.push_str(&format!(
2028            "    char* {name}_vs = (char*)malloc({name}_vl + 1);\n"
2029        ));
2030        out.push_str(&format!(
2031            "    napi_get_value_string_utf8(env, {name}_v, {name}_vs, {name}_vl + 1, &{name}_vl);\n"
2032        ));
2033        out.push_str(&format!("    {name}_values[{name}_i] = {name}_vs;\n"));
2034    } else if needs_narrowing_read(v) {
2035        let vgetter = napi_getter(v);
2036        let raw = napi_read_tmp_type(v);
2037        out.push_str(&format!("    {raw} {name}_vv;\n"));
2038        out.push_str(&format!("    {vgetter}(env, {name}_v, &{name}_vv);\n"));
2039        out.push_str(&format!("    {name}_values[{name}_i] = ({vt}){name}_vv;\n"));
2040    } else {
2041        let vgetter = napi_getter(v);
2042        out.push_str(&format!(
2043            "    {vgetter}(env, {name}_v, &{name}_values[{name}_i]);\n"
2044        ));
2045    }
2046
2047    out.push_str("  }\n");
2048    c_args.push(format!("{name}_keys"));
2049    c_args.push(format!("{name}_values"));
2050    c_args.push(format!("(size_t){name}_count"));
2051
2052    if matches!(k, TypeRef::StringUtf8) {
2053        cleanups.push(format!(
2054            "  for (uint32_t {name}_j = 0; {name}_j < {name}_count; {name}_j++) free((void*){name}_keys[{name}_j]);\n"
2055        ));
2056    }
2057    cleanups.push(format!("  free({name}_keys);\n"));
2058    if matches!(v, TypeRef::StringUtf8) {
2059        cleanups.push(format!(
2060            "  for (uint32_t {name}_j = 0; {name}_j < {name}_count; {name}_j++) free((void*){name}_values[{name}_j]);\n"
2061        ));
2062    }
2063    cleanups.push(format!("  free({name}_values);\n"));
2064}
2065
2066fn emit_ret_out_params(
2067    out: &mut String,
2068    c_args: &mut Vec<String>,
2069    ty: &TypeRef,
2070    module: &str,
2071    prefix: &str,
2072) {
2073    match ty {
2074        TypeRef::Bytes | TypeRef::List(_) => {
2075            out.push_str("  size_t out_len;\n");
2076            c_args.push("&out_len".into());
2077        }
2078        TypeRef::Map(k, v) => {
2079            let kt = c_elem_type(k, module, prefix);
2080            let vt = c_elem_type(v, module, prefix);
2081            out.push_str(&format!("  {kt}* out_keys = NULL;\n"));
2082            out.push_str(&format!("  {vt}* out_values = NULL;\n"));
2083            out.push_str("  size_t out_len = 0;\n");
2084            c_args.push("out_keys".into());
2085            c_args.push("out_values".into());
2086            c_args.push("&out_len".into());
2087        }
2088        TypeRef::Optional(inner) if is_c_ptr_type(inner) => {
2089            emit_ret_out_params(out, c_args, inner, module, prefix);
2090        }
2091        _ => {}
2092    }
2093}
2094
2095/// Build a `name -> StructDef` registry over every (possibly nested) module so
2096/// that struct-returning functions can materialize a real JS object (matching
2097/// the shape declared in `types.d.ts`) instead of leaking a raw handle number.
2098fn struct_registry(model: &BindingModel) -> HashMap<String, StructBinding> {
2099    model
2100        .modules
2101        .iter()
2102        .flat_map(|m| m.structs.iter())
2103        .map(|s| (s.name.clone(), s.clone()))
2104        .collect()
2105}
2106
2107/// Materialize an *owned* C struct pointer (`ptr_expr`) into a plain JS object
2108/// assigned to `obj_var`, by invoking each generated field getter. The pointer
2109/// is consumed: after the fields are read it is destroyed, because the C ABI
2110/// hands back owned struct handles (the same ownership the other backends free).
2111#[allow(clippy::too_many_arguments)]
2112fn emit_struct_to_object(
2113    out: &mut String,
2114    env: &str,
2115    struct_name: &str,
2116    ptr_expr: &str,
2117    obj_var: &str,
2118    module: &str,
2119    prefix: &str,
2120    structs: &HashMap<String, StructBinding>,
2121    indent: &str,
2122    destroy: bool,
2123) {
2124    let Some(def) = structs.get(local_type_name(struct_name)).cloned() else {
2125        // Unknown struct: fall back to the raw handle rather than emit broken C.
2126        out.push_str(&format!(
2127            "{indent}napi_create_int64({env}, (int64_t)(intptr_t){ptr_expr}, &{obj_var});\n"
2128        ));
2129        return;
2130    };
2131    let abi = &def.c_tag;
2132    let p = format!("{obj_var}_p");
2133    out.push_str(&format!("{indent}{{\n"));
2134    out.push_str(&format!("{indent}  {abi}* {p} = ({abi}*){ptr_expr};\n"));
2135    out.push_str(&format!(
2136        "{indent}  napi_create_object({env}, &{obj_var});\n"
2137    ));
2138    for field in &def.fields {
2139        let getter = &field.getter_symbol;
2140        let fv = format!("{obj_var}_{}", field.name);
2141        out.push_str(&format!("{indent}  napi_value {fv};\n"));
2142        emit_struct_field_to_napi(
2143            out,
2144            env,
2145            &field.ty,
2146            getter,
2147            &p,
2148            &fv,
2149            module,
2150            prefix,
2151            structs,
2152            &format!("{indent}  "),
2153        );
2154        out.push_str(&format!(
2155            "{indent}  napi_set_named_property({env}, {obj_var}, \"{}\", {fv});\n",
2156            field.name
2157        ));
2158    }
2159    if destroy {
2160        out.push_str(&format!("{indent}  {}({p});\n", def.destroy_symbol));
2161    }
2162    out.push_str(&format!("{indent}}}\n"));
2163}
2164
2165/// The C statement that creates a napi value `target` from a leaf C expression
2166/// `expr` (scalars, bools, enums, handles). Strings/structs are handled by
2167/// [`emit_elem_to_napi`], which needs surrounding context.
2168fn napi_create_leaf(env: &str, ty: &TypeRef, expr: &str, target: &str) -> String {
2169    match ty {
2170        TypeRef::I32 => format!("napi_create_int32({env}, {expr}, &{target});"),
2171        TypeRef::U32 => format!("napi_create_uint32({env}, {expr}, &{target});"),
2172        TypeRef::I64 => format!("napi_create_int64({env}, {expr}, &{target});"),
2173        TypeRef::F64 => format!("napi_create_double({env}, {expr}, &{target});"),
2174        TypeRef::I8 | TypeRef::I16 => format!("napi_create_int32({env}, {expr}, &{target});"),
2175        TypeRef::U8 | TypeRef::U16 => format!("napi_create_uint32({env}, {expr}, &{target});"),
2176        TypeRef::U64 => format!("napi_create_int64({env}, (int64_t)({expr}), &{target});"),
2177        TypeRef::F32 => format!("napi_create_double({env}, {expr}, &{target});"),
2178        TypeRef::Bool => format!("napi_get_boolean({env}, {expr}, &{target});"),
2179        TypeRef::Enum(_) => format!("napi_create_int32({env}, (int32_t)({expr}), &{target});"),
2180        TypeRef::Handle | TypeRef::TypedHandle(_) => {
2181            format!("napi_create_int64({env}, (int64_t)(intptr_t)({expr}), &{target});")
2182        }
2183        _ => format!("napi_get_null({env}, &{target});"),
2184    }
2185}
2186
2187/// Convert a single collection *element* C expression `expr` (a list item or map
2188/// value) into the napi value `target`. Owned element strings are freed after
2189/// the copy, matching the C ABI's transfer-on-return contract.
2190#[allow(clippy::too_many_arguments)]
2191fn emit_elem_to_napi(
2192    out: &mut String,
2193    env: &str,
2194    ty: &TypeRef,
2195    expr: &str,
2196    target: &str,
2197    module: &str,
2198    prefix: &str,
2199    structs: &HashMap<String, StructBinding>,
2200    indent: &str,
2201) {
2202    match ty {
2203        TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
2204            out.push_str(&format!(
2205                "{indent}napi_create_string_utf8({env}, {expr}, NAPI_AUTO_LENGTH, &{target});\n"
2206            ));
2207            if matches!(ty, TypeRef::StringUtf8) {
2208                out.push_str(&format!("{indent}weaveffi_free_string((char*)({expr}));\n"));
2209            }
2210        }
2211        TypeRef::Struct(name) => {
2212            emit_struct_to_object(
2213                out, env, name, expr, target, module, prefix, structs, indent, false,
2214            );
2215        }
2216        _ => out.push_str(&format!(
2217            "{indent}{}\n",
2218            napi_create_leaf(env, ty, expr, target)
2219        )),
2220    }
2221}
2222
2223/// Marshal one struct field, read via `getter(pv)`, into the JS value `fv`.
2224/// Scalars, enums, handles, owned strings, optional strings, nested structs,
2225/// byte buffers, lists, maps, and optional scalars are all materialized.
2226#[allow(clippy::too_many_arguments)]
2227fn emit_struct_field_to_napi(
2228    out: &mut String,
2229    env: &str,
2230    ty: &TypeRef,
2231    getter: &str,
2232    pv: &str,
2233    fv: &str,
2234    module: &str,
2235    prefix: &str,
2236    structs: &HashMap<String, StructBinding>,
2237    indent: &str,
2238) {
2239    match ty {
2240        TypeRef::I32 => out.push_str(&format!(
2241            "{indent}napi_create_int32({env}, {getter}({pv}), &{fv});\n"
2242        )),
2243        TypeRef::U32 => out.push_str(&format!(
2244            "{indent}napi_create_uint32({env}, {getter}({pv}), &{fv});\n"
2245        )),
2246        TypeRef::I64 => out.push_str(&format!(
2247            "{indent}napi_create_int64({env}, {getter}({pv}), &{fv});\n"
2248        )),
2249        TypeRef::F64 => out.push_str(&format!(
2250            "{indent}napi_create_double({env}, {getter}({pv}), &{fv});\n"
2251        )),
2252        TypeRef::I8 | TypeRef::I16 => out.push_str(&format!(
2253            "{indent}napi_create_int32({env}, {getter}({pv}), &{fv});\n"
2254        )),
2255        TypeRef::U8 | TypeRef::U16 => out.push_str(&format!(
2256            "{indent}napi_create_uint32({env}, {getter}({pv}), &{fv});\n"
2257        )),
2258        TypeRef::U64 => out.push_str(&format!(
2259            "{indent}napi_create_int64({env}, (int64_t){getter}({pv}), &{fv});\n"
2260        )),
2261        TypeRef::F32 => out.push_str(&format!(
2262            "{indent}napi_create_double({env}, {getter}({pv}), &{fv});\n"
2263        )),
2264        TypeRef::Bool => out.push_str(&format!(
2265            "{indent}napi_get_boolean({env}, {getter}({pv}), &{fv});\n"
2266        )),
2267        TypeRef::Enum(_) => out.push_str(&format!(
2268            "{indent}napi_create_int32({env}, (int32_t){getter}({pv}), &{fv});\n"
2269        )),
2270        TypeRef::Handle | TypeRef::TypedHandle(_) => out.push_str(&format!(
2271            "{indent}napi_create_int64({env}, (int64_t)(intptr_t){getter}({pv}), &{fv});\n"
2272        )),
2273        TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
2274            let owned = matches!(ty, TypeRef::StringUtf8);
2275            out.push_str(&format!("{indent}{{\n"));
2276            out.push_str(&format!(
2277                "{indent}  char* {fv}_s = (char*){getter}({pv});\n"
2278            ));
2279            out.push_str(&format!(
2280                "{indent}  napi_create_string_utf8({env}, {fv}_s, NAPI_AUTO_LENGTH, &{fv});\n"
2281            ));
2282            if owned {
2283                out.push_str(&format!("{indent}  weaveffi_free_string({fv}_s);\n"));
2284            }
2285            out.push_str(&format!("{indent}}}\n"));
2286        }
2287        TypeRef::Struct(name) => {
2288            emit_struct_to_object(
2289                out,
2290                env,
2291                name,
2292                &format!("{getter}({pv})"),
2293                fv,
2294                module,
2295                prefix,
2296                structs,
2297                indent,
2298                true,
2299            );
2300        }
2301        TypeRef::Optional(inner)
2302            if matches!(inner.as_ref(), TypeRef::StringUtf8 | TypeRef::BorrowedStr) =>
2303        {
2304            let owned = matches!(inner.as_ref(), TypeRef::StringUtf8);
2305            out.push_str(&format!("{indent}{{\n"));
2306            out.push_str(&format!(
2307                "{indent}  char* {fv}_s = (char*){getter}({pv});\n"
2308            ));
2309            out.push_str(&format!(
2310                "{indent}  if ({fv}_s == NULL) {{ napi_get_null({env}, &{fv}); }}\n"
2311            ));
2312            out.push_str(&format!(
2313                "{indent}  else {{ napi_create_string_utf8({env}, {fv}_s, NAPI_AUTO_LENGTH, &{fv});"
2314            ));
2315            if owned {
2316                out.push_str(&format!(" weaveffi_free_string({fv}_s);"));
2317            }
2318            out.push_str(" }\n");
2319            out.push_str(&format!("{indent}}}\n"));
2320        }
2321        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Struct(_)) => {
2322            let TypeRef::Struct(name) = inner.as_ref() else {
2323                unreachable!()
2324            };
2325            let abi = c_abi_struct_name(name, module, prefix);
2326            out.push_str(&format!("{indent}{{\n"));
2327            out.push_str(&format!("{indent}  {abi}* {fv}_sp = {getter}({pv});\n"));
2328            out.push_str(&format!(
2329                "{indent}  if ({fv}_sp == NULL) {{ napi_get_null({env}, &{fv}); }}\n"
2330            ));
2331            out.push_str(&format!("{indent}  else {{\n"));
2332            emit_struct_to_object(
2333                out,
2334                env,
2335                name,
2336                &format!("{fv}_sp"),
2337                fv,
2338                module,
2339                prefix,
2340                structs,
2341                &format!("{indent}    "),
2342                true,
2343            );
2344            out.push_str(&format!("{indent}  }}\n"));
2345            out.push_str(&format!("{indent}}}\n"));
2346        }
2347        // An optional typed handle lowers to a nullable opaque pointer that the
2348        // field surfaces as the integer handle (or null), like the non-optional
2349        // case but guarded on NULL.
2350        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::TypedHandle(_)) => {
2351            let TypeRef::TypedHandle(name) = inner.as_ref() else {
2352                unreachable!()
2353            };
2354            let abi = c_abi_struct_name(name, module, prefix);
2355            out.push_str(&format!("{indent}{{\n"));
2356            out.push_str(&format!("{indent}  {abi}* {fv}_h = {getter}({pv});\n"));
2357            out.push_str(&format!(
2358                "{indent}  if ({fv}_h == NULL) {{ napi_get_null({env}, &{fv}); }}\n"
2359            ));
2360            out.push_str(&format!(
2361                "{indent}  else {{ napi_create_int64({env}, (int64_t)(intptr_t){fv}_h, &{fv}); }}\n"
2362            ));
2363            out.push_str(&format!("{indent}}}\n"));
2364        }
2365        // Remaining optionals (scalar/bool/enum/handle) lower to a nullable
2366        // pointer-to-value the getter returns directly.
2367        TypeRef::Optional(inner) => {
2368            let ct = c_elem_type(inner, module, prefix);
2369            out.push_str(&format!("{indent}{{\n"));
2370            out.push_str(&format!("{indent}  {ct}* {fv}_p = {getter}({pv});\n"));
2371            out.push_str(&format!(
2372                "{indent}  if ({fv}_p == NULL) {{ napi_get_null({env}, &{fv}); }}\n"
2373            ));
2374            out.push_str(&format!(
2375                "{indent}  else {{ {} }}\n",
2376                napi_create_leaf(env, inner, &format!("*{fv}_p"), fv)
2377            ));
2378            out.push_str(&format!("{indent}}}\n"));
2379        }
2380        TypeRef::Bytes | TypeRef::BorrowedBytes => {
2381            out.push_str(&format!("{indent}{{\n"));
2382            out.push_str(&format!("{indent}  size_t {fv}_len;\n"));
2383            out.push_str(&format!(
2384                "{indent}  const uint8_t* {fv}_data = (const uint8_t*){getter}({pv}, &{fv}_len);\n"
2385            ));
2386            out.push_str(&format!(
2387                "{indent}  if ({fv}_data == NULL) {{ napi_get_null({env}, &{fv}); }}\n"
2388            ));
2389            out.push_str(&format!(
2390                "{indent}  else {{ void* {fv}_buf; napi_create_buffer_copy({env}, {fv}_len, {fv}_data, &{fv}_buf, &{fv}); }}\n"
2391            ));
2392            out.push_str(&format!("{indent}}}\n"));
2393        }
2394        TypeRef::List(inner) => {
2395            let et = c_elem_type(inner, module, prefix);
2396            out.push_str(&format!("{indent}{{\n"));
2397            out.push_str(&format!("{indent}  size_t {fv}_len;\n"));
2398            out.push_str(&format!(
2399                "{indent}  {et}* {fv}_arr = {getter}({pv}, &{fv}_len);\n"
2400            ));
2401            out.push_str(&format!("{indent}  napi_create_array({env}, &{fv});\n"));
2402            out.push_str(&format!("{indent}  if ({fv}_arr != NULL) {{\n"));
2403            out.push_str(&format!(
2404                "{indent}    for (size_t {fv}_i = 0; {fv}_i < {fv}_len; {fv}_i++) {{\n"
2405            ));
2406            out.push_str(&format!("{indent}      napi_value {fv}_e;\n"));
2407            emit_elem_to_napi(
2408                out,
2409                env,
2410                inner,
2411                &format!("{fv}_arr[{fv}_i]"),
2412                &format!("{fv}_e"),
2413                module,
2414                prefix,
2415                structs,
2416                &format!("{indent}      "),
2417            );
2418            out.push_str(&format!(
2419                "{indent}      napi_set_element({env}, {fv}, (uint32_t){fv}_i, {fv}_e);\n"
2420            ));
2421            out.push_str(&format!("{indent}    }}\n"));
2422            out.push_str(&format!("{indent}  }}\n"));
2423            out.push_str(&format!("{indent}}}\n"));
2424        }
2425        TypeRef::Map(k, v) => {
2426            let kt = c_elem_type(k, module, prefix);
2427            let vt = c_elem_type(v, module, prefix);
2428            out.push_str(&format!("{indent}{{\n"));
2429            out.push_str(&format!("{indent}  {kt}* {fv}_keys = NULL;\n"));
2430            out.push_str(&format!("{indent}  {vt}* {fv}_vals = NULL;\n"));
2431            out.push_str(&format!("{indent}  size_t {fv}_len;\n"));
2432            out.push_str(&format!(
2433                "{indent}  {getter}({pv}, &{fv}_keys, &{fv}_vals, &{fv}_len);\n"
2434            ));
2435            out.push_str(&format!("{indent}  napi_create_object({env}, &{fv});\n"));
2436            out.push_str(&format!(
2437                "{indent}  if ({fv}_keys != NULL && {fv}_vals != NULL) {{\n"
2438            ));
2439            out.push_str(&format!(
2440                "{indent}    for (size_t {fv}_i = 0; {fv}_i < {fv}_len; {fv}_i++) {{\n"
2441            ));
2442            out.push_str(&format!("{indent}      napi_value {fv}_v;\n"));
2443            emit_elem_to_napi(
2444                out,
2445                env,
2446                v,
2447                &format!("{fv}_vals[{fv}_i]"),
2448                &format!("{fv}_v"),
2449                module,
2450                prefix,
2451                structs,
2452                &format!("{indent}      "),
2453            );
2454            match k.as_ref() {
2455                TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
2456                    out.push_str(&format!(
2457                        "{indent}      napi_set_named_property({env}, {fv}, {fv}_keys[{fv}_i], {fv}_v);\n"
2458                    ));
2459                    if matches!(k.as_ref(), TypeRef::StringUtf8) {
2460                        out.push_str(&format!(
2461                            "{indent}      weaveffi_free_string((char*){fv}_keys[{fv}_i]);\n"
2462                        ));
2463                    }
2464                }
2465                other => {
2466                    out.push_str(&format!("{indent}      napi_value {fv}_k;\n"));
2467                    out.push_str(&format!(
2468                        "{indent}      {}\n",
2469                        napi_create_leaf(
2470                            env,
2471                            other,
2472                            &format!("{fv}_keys[{fv}_i]"),
2473                            &format!("{fv}_k")
2474                        )
2475                    ));
2476                    out.push_str(&format!(
2477                        "{indent}      napi_set_property({env}, {fv}, {fv}_k, {fv}_v);\n"
2478                    ));
2479                }
2480            }
2481            out.push_str(&format!("{indent}    }}\n"));
2482            out.push_str(&format!("{indent}  }}\n"));
2483            out.push_str(&format!("{indent}}}\n"));
2484        }
2485        _ => out.push_str(&format!("{indent}napi_get_null({env}, &{fv});\n")),
2486    }
2487}
2488
2489fn emit_ret_to_napi(
2490    out: &mut String,
2491    ty: &TypeRef,
2492    module: &str,
2493    prefix: &str,
2494    fn_name: &str,
2495    structs: &HashMap<String, StructBinding>,
2496) {
2497    out.push_str("  napi_value ret;\n");
2498    match ty {
2499        TypeRef::I32 => out.push_str("  napi_create_int32(env, result, &ret);\n"),
2500        TypeRef::U32 => out.push_str("  napi_create_uint32(env, result, &ret);\n"),
2501        TypeRef::I64 => out.push_str("  napi_create_int64(env, result, &ret);\n"),
2502        TypeRef::F64 => out.push_str("  napi_create_double(env, result, &ret);\n"),
2503        TypeRef::I8 | TypeRef::I16 => out.push_str("  napi_create_int32(env, result, &ret);\n"),
2504        TypeRef::U8 | TypeRef::U16 => out.push_str("  napi_create_uint32(env, result, &ret);\n"),
2505        TypeRef::U64 => out.push_str("  napi_create_int64(env, (int64_t)result, &ret);\n"),
2506        TypeRef::F32 => out.push_str("  napi_create_double(env, result, &ret);\n"),
2507        TypeRef::Bool => out.push_str("  napi_get_boolean(env, result, &ret);\n"),
2508        TypeRef::StringUtf8 => {
2509            out.push_str("  napi_create_string_utf8(env, result, NAPI_AUTO_LENGTH, &ret);\n");
2510            out.push_str("  weaveffi_free_string(result);\n");
2511        }
2512        TypeRef::BorrowedStr => {
2513            out.push_str("  napi_create_string_utf8(env, result, NAPI_AUTO_LENGTH, &ret);\n");
2514        }
2515        TypeRef::TypedHandle(_) | TypeRef::Handle => {
2516            out.push_str("  napi_create_int64(env, (int64_t)(intptr_t)result, &ret);\n");
2517        }
2518        TypeRef::Struct(name) => {
2519            emit_struct_to_object(
2520                out, "env", name, "result", "ret", module, prefix, structs, "  ", true,
2521            );
2522        }
2523        TypeRef::Enum(_) => {
2524            out.push_str("  napi_create_int32(env, (int32_t)result, &ret);\n");
2525        }
2526        TypeRef::Bytes => {
2527            out.push_str("  napi_create_buffer_copy(env, out_len, result, NULL, &ret);\n");
2528            out.push_str("  weaveffi_free_bytes((uint8_t*)result, out_len);\n");
2529        }
2530        TypeRef::BorrowedBytes => {
2531            out.push_str("  napi_create_buffer_copy(env, out_len, result, NULL, &ret);\n");
2532        }
2533        TypeRef::Optional(inner) => {
2534            out.push_str("  if (result == NULL) {\n");
2535            out.push_str("    napi_get_null(env, &ret);\n");
2536            out.push_str("  } else {\n");
2537            emit_optional_ret_inner(out, inner, module, prefix, structs);
2538            out.push_str("  }\n");
2539        }
2540        TypeRef::List(inner) => emit_list_ret(out, inner, module, prefix, "  ", structs),
2541        TypeRef::Map(_, _) => {
2542            out.push_str("  napi_create_object(env, &ret);\n");
2543        }
2544        TypeRef::Iterator(inner) => {
2545            let fn_pascal = fn_name.to_upper_camel_case();
2546            let iter_type = format!("{prefix}_{module}_{fn_pascal}Iterator");
2547            let et = c_elem_type(inner, module, prefix);
2548            out.push_str("  napi_create_array(env, &ret);\n");
2549            out.push_str("  uint32_t iter_idx = 0;\n");
2550            out.push_str(&format!("  {et} iter_item;\n"));
2551            // The iterator's `_next` reports per-step faults through a trailing
2552            // error out-param; it is part of the C ABI signature and must be
2553            // threaded through even when we surface drained items as an array.
2554            out.push_str("  weaveffi_error iter_err = {0};\n");
2555            out.push_str(&format!(
2556                "  while ({iter_type}_next(result, &iter_item, &iter_err)) {{\n"
2557            ));
2558            out.push_str("    napi_value elem;\n");
2559            match inner.as_ref() {
2560                TypeRef::I32 => {
2561                    out.push_str("    napi_create_int32(env, iter_item, &elem);\n");
2562                }
2563                TypeRef::U32 => {
2564                    out.push_str("    napi_create_uint32(env, iter_item, &elem);\n");
2565                }
2566                TypeRef::I64 => {
2567                    out.push_str("    napi_create_int64(env, iter_item, &elem);\n");
2568                }
2569                TypeRef::F64 => {
2570                    out.push_str("    napi_create_double(env, iter_item, &elem);\n");
2571                }
2572                TypeRef::I8 | TypeRef::I16 => {
2573                    out.push_str("    napi_create_int32(env, iter_item, &elem);\n");
2574                }
2575                TypeRef::U8 | TypeRef::U16 => {
2576                    out.push_str("    napi_create_uint32(env, iter_item, &elem);\n");
2577                }
2578                TypeRef::U64 => {
2579                    out.push_str("    napi_create_int64(env, (int64_t)iter_item, &elem);\n");
2580                }
2581                TypeRef::F32 => {
2582                    out.push_str("    napi_create_double(env, iter_item, &elem);\n");
2583                }
2584                TypeRef::Bool => {
2585                    out.push_str("    napi_get_boolean(env, iter_item, &elem);\n");
2586                }
2587                TypeRef::TypedHandle(_) | TypeRef::Handle => {
2588                    out.push_str(
2589                        "    napi_create_int64(env, (int64_t)(intptr_t)iter_item, &elem);\n",
2590                    );
2591                }
2592                TypeRef::StringUtf8 => {
2593                    out.push_str(
2594                        "    napi_create_string_utf8(env, iter_item, NAPI_AUTO_LENGTH, &elem);\n",
2595                    );
2596                    out.push_str("    weaveffi_free_string(iter_item);\n");
2597                }
2598                TypeRef::Struct(_) | TypeRef::Enum(_) => {
2599                    out.push_str(
2600                        "    napi_create_int64(env, (int64_t)(intptr_t)iter_item, &elem);\n",
2601                    );
2602                }
2603                _ => {
2604                    out.push_str("    napi_create_int64(env, (int64_t)iter_item, &elem);\n");
2605                }
2606            }
2607            out.push_str("    napi_set_element(env, ret, iter_idx++, elem);\n");
2608            out.push_str("  }\n");
2609            out.push_str(&format!("  {iter_type}_destroy(result);\n"));
2610        }
2611    }
2612    out.push_str("  return ret;\n");
2613}
2614
2615fn emit_optional_ret_inner(
2616    out: &mut String,
2617    inner: &TypeRef,
2618    module: &str,
2619    prefix: &str,
2620    structs: &HashMap<String, StructBinding>,
2621) {
2622    match inner {
2623        TypeRef::I32 => {
2624            out.push_str("    napi_create_int32(env, *result, &ret);\n");
2625            out.push_str("    free(result);\n");
2626        }
2627        TypeRef::U32 => {
2628            out.push_str("    napi_create_uint32(env, *result, &ret);\n");
2629            out.push_str("    free(result);\n");
2630        }
2631        TypeRef::I64 => {
2632            out.push_str("    napi_create_int64(env, *result, &ret);\n");
2633            out.push_str("    free(result);\n");
2634        }
2635        TypeRef::F64 => {
2636            out.push_str("    napi_create_double(env, *result, &ret);\n");
2637            out.push_str("    free(result);\n");
2638        }
2639        TypeRef::I8 | TypeRef::I16 => {
2640            out.push_str("    napi_create_int32(env, *result, &ret);\n");
2641            out.push_str("    free(result);\n");
2642        }
2643        TypeRef::U8 | TypeRef::U16 => {
2644            out.push_str("    napi_create_uint32(env, *result, &ret);\n");
2645            out.push_str("    free(result);\n");
2646        }
2647        TypeRef::U64 => {
2648            out.push_str("    napi_create_int64(env, (int64_t)*result, &ret);\n");
2649            out.push_str("    free(result);\n");
2650        }
2651        TypeRef::F32 => {
2652            out.push_str("    napi_create_double(env, *result, &ret);\n");
2653            out.push_str("    free(result);\n");
2654        }
2655        TypeRef::Bool => {
2656            out.push_str("    napi_get_boolean(env, *result, &ret);\n");
2657            out.push_str("    free(result);\n");
2658        }
2659        TypeRef::TypedHandle(_) | TypeRef::Handle => {
2660            out.push_str("    napi_create_int64(env, (int64_t)(intptr_t)*result, &ret);\n");
2661            out.push_str("    free(result);\n");
2662        }
2663        TypeRef::Enum(_) => {
2664            out.push_str("    napi_create_int32(env, (int32_t)*result, &ret);\n");
2665            out.push_str("    free(result);\n");
2666        }
2667        TypeRef::StringUtf8 => {
2668            out.push_str("    napi_create_string_utf8(env, result, NAPI_AUTO_LENGTH, &ret);\n");
2669            out.push_str("    weaveffi_free_string(result);\n");
2670        }
2671        TypeRef::Struct(name) => {
2672            emit_struct_to_object(
2673                out, "env", name, "result", "ret", module, prefix, structs, "    ", true,
2674            );
2675        }
2676        TypeRef::List(li) => emit_list_ret(out, li, module, prefix, "    ", structs),
2677        _ => out.push_str("    napi_get_null(env, &ret);\n"),
2678    }
2679}
2680
2681fn emit_list_ret(
2682    out: &mut String,
2683    inner: &TypeRef,
2684    module: &str,
2685    prefix: &str,
2686    ind: &str,
2687    structs: &HashMap<String, StructBinding>,
2688) {
2689    out.push_str(&format!(
2690        "{ind}napi_create_array_with_length(env, out_len, &ret);\n"
2691    ));
2692    out.push_str(&format!(
2693        "{ind}for (size_t ret_i = 0; ret_i < out_len; ret_i++) {{\n"
2694    ));
2695    out.push_str(&format!("{ind}  napi_value elem;\n"));
2696    match inner {
2697        TypeRef::I32 => out.push_str(&format!(
2698            "{ind}  napi_create_int32(env, result[ret_i], &elem);\n"
2699        )),
2700        TypeRef::U32 => out.push_str(&format!(
2701            "{ind}  napi_create_uint32(env, result[ret_i], &elem);\n"
2702        )),
2703        TypeRef::I64 => out.push_str(&format!(
2704            "{ind}  napi_create_int64(env, result[ret_i], &elem);\n"
2705        )),
2706        TypeRef::F64 => out.push_str(&format!(
2707            "{ind}  napi_create_double(env, result[ret_i], &elem);\n"
2708        )),
2709        TypeRef::I8 | TypeRef::I16 => out.push_str(&format!(
2710            "{ind}  napi_create_int32(env, result[ret_i], &elem);\n"
2711        )),
2712        TypeRef::U8 | TypeRef::U16 => out.push_str(&format!(
2713            "{ind}  napi_create_uint32(env, result[ret_i], &elem);\n"
2714        )),
2715        TypeRef::U64 => out.push_str(&format!(
2716            "{ind}  napi_create_int64(env, (int64_t)result[ret_i], &elem);\n"
2717        )),
2718        TypeRef::F32 => out.push_str(&format!(
2719            "{ind}  napi_create_double(env, result[ret_i], &elem);\n"
2720        )),
2721        TypeRef::Bool => out.push_str(&format!(
2722            "{ind}  napi_get_boolean(env, result[ret_i], &elem);\n"
2723        )),
2724        TypeRef::TypedHandle(_) | TypeRef::Handle => out.push_str(&format!(
2725            "{ind}  napi_create_int64(env, (int64_t)(intptr_t)result[ret_i], &elem);\n"
2726        )),
2727        TypeRef::StringUtf8 => {
2728            out.push_str(&format!(
2729                "{ind}  napi_create_string_utf8(env, result[ret_i], NAPI_AUTO_LENGTH, &elem);\n"
2730            ));
2731            out.push_str(&format!("{ind}  weaveffi_free_string(result[ret_i]);\n"));
2732        }
2733        TypeRef::Enum(_) => out.push_str(&format!(
2734            "{ind}  napi_create_int32(env, (int32_t)result[ret_i], &elem);\n"
2735        )),
2736        TypeRef::Struct(name) => {
2737            let elem_indent = format!("{ind}  ");
2738            emit_struct_to_object(
2739                out,
2740                "env",
2741                name,
2742                "result[ret_i]",
2743                "elem",
2744                module,
2745                prefix,
2746                structs,
2747                &elem_indent,
2748                true,
2749            );
2750        }
2751        _ => out.push_str(&format!(
2752            "{ind}  napi_create_int64(env, (int64_t)result[ret_i], &elem);\n"
2753        )),
2754    }
2755    out.push_str(&format!(
2756        "{ind}  napi_set_element(env, ret, (uint32_t)ret_i, elem);\n"
2757    ));
2758    out.push_str(&format!("{ind}}}\n"));
2759    out.push_str(&format!("{ind}free(result);\n"));
2760}
2761
2762fn ts_type_for(ty: &TypeRef) -> String {
2763    match ty {
2764        TypeRef::I8
2765        | TypeRef::I16
2766        | TypeRef::U8
2767        | TypeRef::U16
2768        | TypeRef::I32
2769        | TypeRef::U32
2770        | TypeRef::I64
2771        | TypeRef::U64
2772        | TypeRef::F32
2773        | TypeRef::F64 => "number".into(),
2774        TypeRef::Bool => "boolean".into(),
2775        TypeRef::StringUtf8 | TypeRef::BorrowedStr => "string".into(),
2776        TypeRef::Bytes | TypeRef::BorrowedBytes => "Buffer".into(),
2777        TypeRef::Handle => "bigint".into(),
2778        // Structs, enums, and typed handles surface as bare local TS names. A
2779        // cross-module reference (e.g. `handle<Store>` resolved to `kv.Store`)
2780        // must annotate the *local* interface `Store`; the qualified IR name is
2781        // not a declared TS type in this module.
2782        TypeRef::TypedHandle(name) => local_type_name(name).to_string(),
2783        TypeRef::Struct(name) => local_type_name(name).to_string(),
2784        TypeRef::Enum(name) => local_type_name(name).to_string(),
2785        TypeRef::Optional(inner) => format!("{} | null", ts_type_for(inner)),
2786        TypeRef::List(inner) => {
2787            let inner_ts = ts_type_for(inner);
2788            if matches!(inner.as_ref(), TypeRef::Optional(_)) {
2789                format!("({inner_ts})[]")
2790            } else {
2791                format!("{inner_ts}[]")
2792            }
2793        }
2794        TypeRef::Map(k, v) => format!("Record<{}, {}>", ts_type_for(k), ts_type_for(v)),
2795        TypeRef::Iterator(inner) => {
2796            let t = ts_type_for(inner);
2797            format!("{t}[]")
2798        }
2799    }
2800}
2801
2802/// Emits a JSDoc comment at `indent`. Single-line docs collapse to
2803/// `/** text */`; multi-line docs expand to a block with ` * ` prefixed lines.
2804fn emit_doc(out: &mut String, doc: &Option<String>, indent: &str) {
2805    common_emit_doc(out, doc, indent, DocCommentStyle::Javadoc);
2806}
2807
2808/// Emits a JSDoc block for a function: function doc, `@param name desc` for
2809/// each documented parameter, and an optional trailing tag list.
2810fn emit_fn_doc(
2811    out: &mut String,
2812    doc: &Option<String>,
2813    params: &[ParamBinding],
2814    indent: &str,
2815    extra_tags: &[String],
2816) {
2817    let has_param_docs = params.iter().any(|p| p.doc.is_some());
2818    let trimmed_doc = doc.as_ref().map(|d| d.trim()).filter(|d| !d.is_empty());
2819    if trimmed_doc.is_none() && !has_param_docs && extra_tags.is_empty() {
2820        return;
2821    }
2822    out.push_str(indent);
2823    out.push_str("/**\n");
2824    if let Some(d) = trimmed_doc {
2825        for line in d.lines() {
2826            out.push_str(indent);
2827            if line.is_empty() {
2828                out.push_str(" *\n");
2829            } else {
2830                out.push_str(" * ");
2831                out.push_str(line);
2832                out.push('\n');
2833            }
2834        }
2835    }
2836    for p in params {
2837        if let Some(pdoc) = &p.doc {
2838            let pdoc = pdoc.trim();
2839            if pdoc.is_empty() {
2840                continue;
2841            }
2842            let mut lines = pdoc.lines();
2843            if let Some(first) = lines.next() {
2844                out.push_str(indent);
2845                out.push_str(&format!(" * @param {} {}\n", p.name, first));
2846            }
2847            for line in lines {
2848                out.push_str(indent);
2849                if line.is_empty() {
2850                    out.push_str(" *\n");
2851                } else {
2852                    out.push_str(" *   ");
2853                    out.push_str(line);
2854                    out.push('\n');
2855                }
2856            }
2857        }
2858    }
2859    for tag in extra_tags {
2860        out.push_str(indent);
2861        out.push_str(" * ");
2862        out.push_str(tag);
2863        out.push('\n');
2864    }
2865    out.push_str(indent);
2866    out.push_str(" */\n");
2867}
2868
2869/// `.d.ts` for a rich (algebraic) enum: a class with a static factory per
2870/// variant (`Shape.circle(radius)`), a `tag()` discriminant reader, a frozen
2871/// `Tag` discriminant map, per-variant namespaced field getters
2872/// (`circleRadius`), and `destroy()`. Mirrors the JS class in [`render_node_index`].
2873fn render_rich_enum_dts(out: &mut String, e: &EnumBinding) {
2874    let Some(rich) = &e.rich else {
2875        return;
2876    };
2877    let name = &e.name;
2878    emit_doc(out, &e.doc, "");
2879    out.push_str(&format!("export class {name} {{\n"));
2880    for v in &rich.variants {
2881        let factory = v.name.to_lower_camel_case();
2882        let params: Vec<String> = v
2883            .fields
2884            .iter()
2885            .map(|f| format!("{}: {}", f.name, ts_type_for(&f.ty)))
2886            .collect();
2887        emit_doc(out, &v.doc, "  ");
2888        out.push_str(&format!(
2889            "  static {factory}({}): {name};\n",
2890            params.join(", ")
2891        ));
2892    }
2893    out.push_str("  /** The active variant's discriminant. */\n");
2894    out.push_str("  tag(): number;\n");
2895    for v in &rich.variants {
2896        for f in &v.fields {
2897            let getter = format!(
2898                "{}{}",
2899                v.name.to_lower_camel_case(),
2900                f.name.to_upper_camel_case()
2901            );
2902            emit_doc(out, &f.doc, "  ");
2903            out.push_str(&format!("  get {getter}(): {};\n", ts_type_for(&f.ty)));
2904        }
2905    }
2906    out.push_str("  /** Free the underlying native object. */\n");
2907    out.push_str("  destroy(): void;\n");
2908    out.push_str("}\n");
2909    // The discriminant map, e.g. `Shape.Tag.Circle === 1`.
2910    out.push_str(&format!("export namespace {name} {{\n"));
2911    out.push_str("  const Tag: Readonly<{\n");
2912    for v in &e.variants {
2913        out.push_str(&format!("    {}: {},\n", v.name, v.value));
2914    }
2915    out.push_str("  }>;\n");
2916    out.push_str("}\n");
2917}
2918
2919fn render_struct_builder_dts(out: &mut String, s: &StructBinding) {
2920    let name = &s.name;
2921    emit_doc(out, &s.doc, "");
2922    out.push_str(&format!("export interface {}Builder {{\n", s.name));
2923    for field in &s.fields {
2924        let method = format!("with{}", field.name.to_upper_camel_case());
2925        let ts = ts_type_for(&field.ty);
2926        emit_doc(out, &field.doc, "  ");
2927        out.push_str(&format!("  {method}(value: {ts}): {name}Builder;\n"));
2928    }
2929    out.push_str(&format!("  build(): {name};\n"));
2930    out.push_str("}\n");
2931}
2932
2933/// The set of *local* names of every rich (algebraic) enum in the model. Used
2934/// to recognize a rich enum where it surfaces as `TypeRef::Struct` in a
2935/// function signature (rich enums lower to opaque struct pointers).
2936fn rich_enum_names(model: &BindingModel) -> HashSet<String> {
2937    model
2938        .modules
2939        .iter()
2940        .flat_map(|m| m.enums.iter())
2941        .filter(|e| e.is_rich())
2942        .map(|e| e.name.clone())
2943        .collect()
2944}
2945
2946/// If `ty` is a rich enum carried directly (or as an `Optional`), return its
2947/// local class name plus whether it was optional. Deeper nestings (list/map)
2948/// return `None`: those flow through the raw addon binding unwrapped.
2949fn rich_struct_ref(ty: &TypeRef, rich: &HashSet<String>) -> Option<(String, bool)> {
2950    match ty {
2951        TypeRef::Struct(n) if rich.contains(local_type_name(n)) => {
2952            Some((local_type_name(n).to_string(), false))
2953        }
2954        TypeRef::Optional(inner) => match inner.as_ref() {
2955            TypeRef::Struct(n) if rich.contains(local_type_name(n)) => {
2956                Some((local_type_name(n).to_string(), true))
2957            }
2958            _ => None,
2959        },
2960        _ => None,
2961    }
2962}
2963
2964/// The JS loader (`index.js`). Without rich enums it simply re-exports the
2965/// native addon (the historical behavior). With rich enums it layers idiomatic
2966/// wrapper classes — opaque-handle objects with per-variant factories, a `tag()`
2967/// reader, namespaced field getters, and `destroy()` (plus a
2968/// `FinalizationRegistry` safety net) — and rewraps the handful of module
2969/// functions that take or return a rich enum so they speak the class, not the
2970/// raw handle.
2971fn render_node_index(api: &Api, prefix: &str, strip: bool, input_basename: &str) -> String {
2972    let model = BindingModel::build(api, prefix);
2973    let dbl = CommentStyle::DoubleSlash;
2974    let mut out = render_prelude(dbl, input_basename);
2975    out.push_str(
2976        "// Prefer the default node-gyp output path; fall back to a\n\
2977         // prebuilt index.node placed next to this file.\n\
2978         let addon;\n\
2979         try {\n  addon = require('./build/Release/weaveffi.node');\n} catch (e) {\n  addon = require('./index.node');\n}\n",
2980    );
2981
2982    let rich = rich_enum_names(&model);
2983    if rich.is_empty() {
2984        out.push_str("module.exports = addon;\n\n");
2985        out.push_str(&render_trailer(dbl, "index.js"));
2986        return out;
2987    }
2988
2989    // The native bindings are defined as non-enumerable properties, so copy
2990    // them by explicit own-name lookup before layering the idiomatic wrappers.
2991    out.push_str(
2992        "\n// Re-export every native binding, then layer idiomatic wrappers for\n\
2993         // rich (algebraic) enums on top.\n\
2994         const wv = {};\n\
2995         for (const _name of Object.getOwnPropertyNames(addon)) {\n  wv[_name] = addon[_name];\n}\n\n",
2996    );
2997
2998    for m in &model.modules {
2999        for e in &m.enums {
3000            if e.is_rich() {
3001                render_rich_enum_class_js(&mut out, e, &m.path, strip);
3002            }
3003        }
3004    }
3005
3006    // Rewrap module functions whose parameters or return carry a rich enum so
3007    // callers pass and receive the class instead of the raw opaque handle.
3008    for m in &model.modules {
3009        for f in &m.functions {
3010            if f.is_async {
3011                continue;
3012            }
3013            let ret_rich = f.ret.as_ref().and_then(|r| rich_struct_ref(r, &rich));
3014            let param_rich: Vec<Option<(String, bool)>> = f
3015                .params
3016                .iter()
3017                .map(|p| rich_struct_ref(&p.ty, &rich))
3018                .collect();
3019            if ret_rich.is_none() && param_rich.iter().all(Option::is_none) {
3020                continue;
3021            }
3022            let js = wrapper_name(&m.path, &f.name, strip);
3023            let param_names: Vec<String> = f.params.iter().map(|p| p.name.clone()).collect();
3024            let call_args: Vec<String> = f
3025                .params
3026                .iter()
3027                .zip(&param_rich)
3028                .map(|(p, r)| match r {
3029                    Some((en, _)) => {
3030                        format!("{n} instanceof {en} ? {n}._handle : {n}", n = p.name)
3031                    }
3032                    None => p.name.clone(),
3033                })
3034                .collect();
3035            let inner = format!("addon.{js}({})", call_args.join(", "));
3036            out.push_str(&format!(
3037                "wv.{js} = function ({}) {{\n",
3038                param_names.join(", ")
3039            ));
3040            match ret_rich {
3041                Some((en, false)) => {
3042                    out.push_str(&format!("  return new {en}({inner});\n"));
3043                }
3044                Some((en, true)) => {
3045                    out.push_str(&format!("  const _r = {inner};\n"));
3046                    out.push_str(&format!("  return _r == null ? null : new {en}(_r);\n"));
3047                }
3048                None => {
3049                    out.push_str(&format!("  return {inner};\n"));
3050                }
3051            }
3052            out.push_str("};\n");
3053        }
3054    }
3055
3056    out.push_str("\nmodule.exports = wv;\n\n");
3057    out.push_str(&render_trailer(dbl, "index.js"));
3058    out
3059}
3060
3061/// Emit one rich-enum wrapper class onto `wv`. The class owns the opaque handle
3062/// and frees it once — via explicit `destroy()` or a `FinalizationRegistry`
3063/// safety net — mirroring how the other backends free the same object.
3064fn render_rich_enum_class_js(out: &mut String, e: &EnumBinding, module: &str, strip: bool) {
3065    let Some(rich) = &e.rich else {
3066        return;
3067    };
3068    let name = &e.name;
3069    let destroy_js = wrapper_name(module, &rich_destroy_base(name), strip);
3070
3071    out.push_str(&format!("class {name} {{\n"));
3072    out.push_str("  constructor(handle) {\n");
3073    out.push_str("    this._handle = handle;\n");
3074    out.push_str(&format!(
3075        "    {name}._cleanup.register(this, handle, this);\n"
3076    ));
3077    out.push_str("  }\n");
3078
3079    // Per-variant factories (`Shape.circle(radius)`).
3080    for v in &rich.variants {
3081        let factory = v.name.to_lower_camel_case();
3082        let ctor_js = wrapper_name(module, &rich_ctor_base(name, &v.name), strip);
3083        let params: Vec<String> = v.fields.iter().map(|f| f.name.clone()).collect();
3084        let joined = params.join(", ");
3085        out.push_str(&format!(
3086            "  static {factory}({joined}) {{\n    return new {name}(addon.{ctor_js}({joined}));\n  }}\n"
3087        ));
3088    }
3089
3090    // Discriminant reader.
3091    let tag_js = wrapper_name(module, &rich_tag_base(name), strip);
3092    out.push_str(&format!(
3093        "  tag() {{\n    return addon.{tag_js}(this._handle);\n  }}\n"
3094    ));
3095
3096    // Namespaced per-variant field getters (`circleRadius`).
3097    for v in &rich.variants {
3098        for f in &v.fields {
3099            let getter = format!(
3100                "{}{}",
3101                v.name.to_lower_camel_case(),
3102                f.name.to_upper_camel_case()
3103            );
3104            let getter_js = wrapper_name(module, &rich_getter_base(name, &v.name, &f.name), strip);
3105            out.push_str(&format!(
3106                "  get {getter}() {{\n    return addon.{getter_js}(this._handle);\n  }}\n"
3107            ));
3108        }
3109    }
3110
3111    // Explicit cleanup; guarded so a double `destroy()` (or destroy-then-GC) is
3112    // a no-op rather than a double free.
3113    out.push_str("  destroy() {\n");
3114    out.push_str("    if (this._handle) {\n");
3115    out.push_str(&format!("      {name}._cleanup.unregister(this);\n"));
3116    out.push_str(&format!("      addon.{destroy_js}(this._handle);\n"));
3117    out.push_str("      this._handle = 0;\n");
3118    out.push_str("    }\n");
3119    out.push_str("  }\n");
3120    out.push_str("}\n");
3121
3122    out.push_str(&format!(
3123        "{name}._cleanup = new FinalizationRegistry((handle) => {{\n  if (handle) {{ addon.{destroy_js}(handle); }}\n}});\n"
3124    ));
3125
3126    // Frozen discriminant map (`Shape.Tag.Circle === 1`).
3127    let consts: Vec<String> = e
3128        .variants
3129        .iter()
3130        .map(|v| format!("{}: {}", v.name, v.value))
3131        .collect();
3132    out.push_str(&format!(
3133        "{name}.Tag = Object.freeze({{ {} }});\n",
3134        consts.join(", ")
3135    ));
3136    out.push_str(&format!("wv.{name} = {name};\n\n"));
3137}
3138
3139fn render_node_dts(
3140    api: &Api,
3141    prefix: &str,
3142    strip_module_prefix: bool,
3143    input_basename: &str,
3144) -> String {
3145    let model = BindingModel::build(api, prefix);
3146    let mut out = render_prelude(CommentStyle::DoubleSlash, input_basename);
3147    out.push_str("// Generated types for WeaveFFI functions\n");
3148    for m in &model.modules {
3149        for s in &m.structs {
3150            emit_doc(&mut out, &s.doc, "");
3151            out.push_str(&format!("export interface {} {{\n", s.name));
3152            for field in &s.fields {
3153                emit_doc(&mut out, &field.doc, "  ");
3154                out.push_str(&format!("  {}: {};\n", field.name, ts_type_for(&field.ty)));
3155            }
3156            out.push_str("}\n");
3157            if s.builder.is_some() {
3158                render_struct_builder_dts(&mut out, s);
3159            }
3160        }
3161        for e in &m.enums {
3162            // A rich (algebraic) enum is an opaque-object wrapper class, not a
3163            // plain numeric `enum`.
3164            if e.is_rich() {
3165                render_rich_enum_dts(&mut out, e);
3166                continue;
3167            }
3168            emit_doc(&mut out, &e.doc, "");
3169            out.push_str(&format!("export enum {} {{\n", e.name));
3170            for v in &e.variants {
3171                emit_doc(&mut out, &v.doc, "  ");
3172                out.push_str(&format!("  {} = {},\n", v.name, v.value));
3173            }
3174            out.push_str("}\n");
3175        }
3176        out.push_str(&format!("// module {}\n", m.path));
3177        for l in &m.listeners {
3178            let Some(cb) = m.callback(&l.event_callback) else {
3179                continue;
3180            };
3181            let cb_params: Vec<String> = cb
3182                .params
3183                .iter()
3184                .map(|p| format!("{}: {}", p.name, ts_type_for(&p.ty)))
3185                .collect();
3186            let register = wrapper_name(
3187                &m.path,
3188                &format!("register_{}", l.name),
3189                strip_module_prefix,
3190            );
3191            let unregister = wrapper_name(
3192                &m.path,
3193                &format!("unregister_{}", l.name),
3194                strip_module_prefix,
3195            );
3196            emit_doc(&mut out, &l.doc, "");
3197            out.push_str(&format!(
3198                "export function {register}(callback: ({}) => void): number\n",
3199                cb_params.join(", ")
3200            ));
3201            out.push_str(&format!("export function {unregister}(id: number): void\n"));
3202        }
3203        for f in &m.functions {
3204            let params: Vec<String> = f
3205                .params
3206                .iter()
3207                .map(|p| format!("{}: {}", p.name, ts_type_for(&p.ty)))
3208                .collect();
3209            let base_ret = match &f.ret {
3210                Some(ty) => ts_type_for(ty),
3211                None => "void".into(),
3212            };
3213            let ret = if f.is_async {
3214                format!("Promise<{base_ret}>")
3215            } else {
3216                base_ret
3217            };
3218            let ts_name = wrapper_name(&m.path, &f.name, strip_module_prefix);
3219            let mut tags = vec![format!("Maps to C function: {}", f.c_base)];
3220            if let Some(msg) = &f.deprecated {
3221                tags.push(format!("@deprecated {}", msg));
3222            }
3223            emit_fn_doc(&mut out, &f.doc, &f.params, "", &tags);
3224            out.push_str(&format!(
3225                "export function {}({}): {}\n",
3226                ts_name,
3227                params.join(", "),
3228                ret
3229            ));
3230        }
3231    }
3232    out.push('\n');
3233    out.push_str(&render_trailer(CommentStyle::DoubleSlash, "types.d.ts"));
3234    out
3235}
3236
3237#[cfg(test)]
3238mod tests {
3239    use super::*;
3240    use weaveffi_core::codegen::Generator;
3241    use weaveffi_ir::ir::{EnumDef, EnumVariant, Function, Module, Param, StructDef, StructField};
3242
3243    fn make_api(modules: Vec<Module>) -> Api {
3244        Api {
3245            version: "0.4.0".into(),
3246            modules,
3247            generators: None,
3248            package: None,
3249        }
3250    }
3251
3252    fn make_module(name: &str) -> Module {
3253        Module {
3254            name: name.into(),
3255            functions: vec![],
3256            structs: vec![],
3257            enums: vec![],
3258            callbacks: vec![],
3259            listeners: vec![],
3260            errors: None,
3261            modules: vec![],
3262        }
3263    }
3264
3265    #[test]
3266    fn listeners_generate_tsfn_register_unregister() {
3267        use weaveffi_ir::ir::{CallbackDef, ListenerDef};
3268        let api = make_api(vec![Module {
3269            name: "events".into(),
3270            functions: vec![],
3271            structs: vec![],
3272            enums: vec![],
3273            callbacks: vec![CallbackDef {
3274                name: "OnMessage".into(),
3275                doc: None,
3276                params: vec![Param {
3277                    name: "message".into(),
3278                    ty: TypeRef::StringUtf8,
3279                    mutable: false,
3280                    doc: None,
3281                }],
3282            }],
3283            listeners: vec![ListenerDef {
3284                name: "message_listener".into(),
3285                event_callback: "OnMessage".into(),
3286                doc: None,
3287            }],
3288            errors: None,
3289            modules: vec![],
3290        }]);
3291        let dir = tempfile::tempdir().unwrap();
3292        let out = Utf8Path::from_path(dir.path()).unwrap();
3293        NodeGenerator
3294            .generate(&api, out, &NodeConfig::default())
3295            .unwrap();
3296        let addon = std::fs::read_to_string(dir.path().join("node/weaveffi_addon.c")).unwrap();
3297        assert!(
3298            addon.contains("napi_create_threadsafe_function"),
3299            "listeners must use threadsafe functions: {addon}"
3300        );
3301        assert!(
3302            addon.contains("Napi_weaveffi_events_register_message_listener"),
3303            "register N-API fn missing: {addon}"
3304        );
3305        assert!(
3306            addon.contains("Napi_weaveffi_events_unregister_message_listener"),
3307            "unregister N-API fn missing: {addon}"
3308        );
3309        assert!(
3310            addon.contains("napi_call_threadsafe_function(ctx->tsfn, p, napi_tsfn_nonblocking)"),
3311            "trampoline must queue payloads: {addon}"
3312        );
3313        assert!(
3314            addon.contains("napi_unref_threadsafe_function"),
3315            "tsfn must be unref'd so listeners don't pin the loop: {addon}"
3316        );
3317        let dts = std::fs::read_to_string(dir.path().join("node/types.d.ts")).unwrap();
3318        assert!(
3319            dts.contains(
3320                "export function events_register_message_listener(callback: (message: string) => void): number"
3321            ),
3322            "register dts missing: {dts}"
3323        );
3324        assert!(
3325            dts.contains("export function events_unregister_message_listener(id: number): void"),
3326            "unregister dts missing: {dts}"
3327        );
3328    }
3329
3330    #[test]
3331    fn ts_type_for_primitives() {
3332        assert_eq!(ts_type_for(&TypeRef::I32), "number");
3333        assert_eq!(ts_type_for(&TypeRef::Bool), "boolean");
3334        assert_eq!(ts_type_for(&TypeRef::StringUtf8), "string");
3335        assert_eq!(ts_type_for(&TypeRef::Bytes), "Buffer");
3336        assert_eq!(ts_type_for(&TypeRef::Handle), "bigint");
3337    }
3338
3339    #[test]
3340    fn ts_type_for_struct_and_enum() {
3341        assert_eq!(ts_type_for(&TypeRef::Struct("Contact".into())), "Contact");
3342        assert_eq!(ts_type_for(&TypeRef::Enum("Color".into())), "Color");
3343        assert_eq!(
3344            ts_type_for(&TypeRef::TypedHandle("Contact".into())),
3345            "Contact"
3346        );
3347    }
3348
3349    #[test]
3350    fn ts_type_for_cross_module_uses_local_name() {
3351        // A typed handle resolved to a parent-module struct (`kv.Store`) must
3352        // emit the bare local interface name, the only TS type in this module.
3353        assert_eq!(
3354            ts_type_for(&TypeRef::TypedHandle("kv.Store".into())),
3355            "Store"
3356        );
3357        assert_eq!(ts_type_for(&TypeRef::Struct("kv.Store".into())), "Store");
3358        assert_eq!(ts_type_for(&TypeRef::Enum("kv.Kind".into())), "Kind");
3359    }
3360
3361    #[test]
3362    fn ts_type_for_optional() {
3363        let ty = TypeRef::Optional(Box::new(TypeRef::StringUtf8));
3364        assert_eq!(ts_type_for(&ty), "string | null");
3365    }
3366
3367    #[test]
3368    fn ts_type_for_list() {
3369        let ty = TypeRef::List(Box::new(TypeRef::I32));
3370        assert_eq!(ts_type_for(&ty), "number[]");
3371    }
3372
3373    #[test]
3374    fn ts_type_for_list_of_optional() {
3375        let ty = TypeRef::List(Box::new(TypeRef::Optional(Box::new(TypeRef::I32))));
3376        assert_eq!(ts_type_for(&ty), "(number | null)[]");
3377    }
3378
3379    #[test]
3380    fn ts_type_for_map() {
3381        let ty = TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32));
3382        assert_eq!(ts_type_for(&ty), "Record<string, number>");
3383    }
3384
3385    #[test]
3386    fn ts_type_for_optional_list() {
3387        let ty = TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::I32))));
3388        assert_eq!(ts_type_for(&ty), "number[] | null");
3389    }
3390
3391    #[test]
3392    fn generate_node_dts_with_structs() {
3393        let mut m = make_module("contacts");
3394        m.structs.push(StructDef {
3395            name: "Contact".into(),
3396            doc: None,
3397            fields: vec![
3398                StructField {
3399                    name: "name".into(),
3400                    ty: TypeRef::StringUtf8,
3401                    doc: None,
3402                    default: None,
3403                },
3404                StructField {
3405                    name: "age".into(),
3406                    ty: TypeRef::I32,
3407                    doc: None,
3408                    default: None,
3409                },
3410                StructField {
3411                    name: "active".into(),
3412                    ty: TypeRef::Bool,
3413                    doc: None,
3414                    default: None,
3415                },
3416            ],
3417            builder: false,
3418        });
3419        m.enums.push(EnumDef {
3420            name: "Color".into(),
3421            doc: None,
3422            variants: vec![
3423                EnumVariant {
3424                    name: "Red".into(),
3425                    value: 0,
3426                    doc: None,
3427                    fields: vec![],
3428                },
3429                EnumVariant {
3430                    name: "Green".into(),
3431                    value: 1,
3432                    doc: None,
3433                    fields: vec![],
3434                },
3435                EnumVariant {
3436                    name: "Blue".into(),
3437                    value: 2,
3438                    doc: None,
3439                    fields: vec![],
3440                },
3441            ],
3442        });
3443        m.functions.push(Function {
3444            name: "get_contact".into(),
3445            params: vec![Param {
3446                name: "id".into(),
3447                ty: TypeRef::I32,
3448                mutable: false,
3449                doc: None,
3450            }],
3451            returns: Some(TypeRef::Optional(Box::new(TypeRef::Struct(
3452                "Contact".into(),
3453            )))),
3454            doc: None,
3455            r#async: false,
3456            cancellable: false,
3457            deprecated: None,
3458            since: None,
3459        });
3460        m.functions.push(Function {
3461            name: "list_contacts".into(),
3462            params: vec![],
3463            returns: Some(TypeRef::List(Box::new(TypeRef::Struct("Contact".into())))),
3464            doc: None,
3465            r#async: false,
3466            cancellable: false,
3467            deprecated: None,
3468            since: None,
3469        });
3470
3471        let dts = render_node_dts(&make_api(vec![m]), "weaveffi", true, "weaveffi.yml");
3472
3473        assert!(dts.contains("export interface Contact {"));
3474        assert!(dts.contains("  name: string;"));
3475        assert!(dts.contains("  age: number;"));
3476        assert!(dts.contains("  active: boolean;"));
3477        assert!(dts.contains("export enum Color {"));
3478        assert!(dts.contains("  Red = 0,"));
3479        assert!(dts.contains("  Green = 1,"));
3480        assert!(dts.contains("  Blue = 2,"));
3481        assert!(dts.contains("export function get_contact(id: number): Contact | null"));
3482        assert!(dts.contains("export function list_contacts(): Contact[]"));
3483
3484        let iface_pos = dts.find("export interface Contact").unwrap();
3485        let enum_pos = dts.find("export enum Color").unwrap();
3486        let fn_pos = dts.find("export function get_contact").unwrap();
3487        assert!(
3488            iface_pos < fn_pos,
3489            "interface should appear before functions"
3490        );
3491        assert!(enum_pos < fn_pos, "enum should appear before functions");
3492    }
3493
3494    #[test]
3495    fn node_generates_binding_gyp() {
3496        let api = make_api(vec![{
3497            let mut m = make_module("math");
3498            m.functions.push(Function {
3499                name: "add".into(),
3500                params: vec![
3501                    Param {
3502                        name: "a".into(),
3503                        ty: TypeRef::I32,
3504                        mutable: false,
3505                        doc: None,
3506                    },
3507                    Param {
3508                        name: "b".into(),
3509                        ty: TypeRef::I32,
3510                        mutable: false,
3511                        doc: None,
3512                    },
3513                ],
3514                returns: Some(TypeRef::I32),
3515                doc: None,
3516                r#async: false,
3517                cancellable: false,
3518                deprecated: None,
3519                since: None,
3520            });
3521            m
3522        }]);
3523
3524        let tmp = std::env::temp_dir().join("weaveffi_test_node_binding_gyp");
3525        let _ = std::fs::remove_dir_all(&tmp);
3526        std::fs::create_dir_all(&tmp).unwrap();
3527        let out_dir = Utf8Path::from_path(&tmp).expect("temp dir is valid UTF-8");
3528
3529        NodeGenerator
3530            .generate(&api, out_dir, &NodeConfig::default())
3531            .unwrap();
3532
3533        let gyp = std::fs::read_to_string(tmp.join("node").join("binding.gyp")).unwrap();
3534        assert!(
3535            gyp.contains("\"target_name\": \"weaveffi\""),
3536            "missing target_name: {gyp}"
3537        );
3538        assert!(
3539            gyp.contains("weaveffi_addon.c"),
3540            "missing source file: {gyp}"
3541        );
3542
3543        let addon = std::fs::read_to_string(tmp.join("node").join("weaveffi_addon.c")).unwrap();
3544        assert!(
3545            addon.contains("napi_value Init("),
3546            "missing Init function: {addon}"
3547        );
3548        assert!(
3549            addon.contains("weaveffi_math_add"),
3550            "missing C ABI call: {addon}"
3551        );
3552        assert!(
3553            addon.contains("napi_get_cb_info"),
3554            "missing napi_get_cb_info call: {addon}"
3555        );
3556
3557        let pkg = std::fs::read_to_string(tmp.join("node").join("package.json")).unwrap();
3558        assert!(pkg.contains("\"gypfile\": true"), "missing gypfile: {pkg}");
3559        assert!(
3560            pkg.contains("node-gyp rebuild"),
3561            "missing install script: {pkg}"
3562        );
3563
3564        let _ = std::fs::remove_dir_all(&tmp);
3565    }
3566
3567    #[test]
3568    fn generate_node_dts_with_structs_and_enums() {
3569        let api = make_api(vec![Module {
3570            name: "contacts".to_string(),
3571            functions: vec![
3572                Function {
3573                    name: "get_contact".to_string(),
3574                    params: vec![Param {
3575                        name: "id".to_string(),
3576                        ty: TypeRef::I32,
3577                        mutable: false,
3578                        doc: None,
3579                    }],
3580                    returns: Some(TypeRef::Optional(Box::new(TypeRef::Struct(
3581                        "Contact".into(),
3582                    )))),
3583                    doc: None,
3584                    r#async: false,
3585                    cancellable: false,
3586                    deprecated: None,
3587                    since: None,
3588                },
3589                Function {
3590                    name: "list_contacts".to_string(),
3591                    params: vec![],
3592                    returns: Some(TypeRef::List(Box::new(TypeRef::Struct("Contact".into())))),
3593                    doc: None,
3594                    r#async: false,
3595                    cancellable: false,
3596                    deprecated: None,
3597                    since: None,
3598                },
3599                Function {
3600                    name: "set_favorite_color".to_string(),
3601                    params: vec![
3602                        Param {
3603                            name: "contact_id".to_string(),
3604                            ty: TypeRef::I32,
3605                            mutable: false,
3606                            doc: None,
3607                        },
3608                        Param {
3609                            name: "color".to_string(),
3610                            ty: TypeRef::Optional(Box::new(TypeRef::Enum("Color".into()))),
3611                            mutable: false,
3612                            doc: None,
3613                        },
3614                    ],
3615                    returns: None,
3616                    doc: None,
3617                    r#async: false,
3618                    cancellable: false,
3619                    deprecated: None,
3620                    since: None,
3621                },
3622                Function {
3623                    name: "get_tags".to_string(),
3624                    params: vec![Param {
3625                        name: "contact_id".to_string(),
3626                        ty: TypeRef::I32,
3627                        mutable: false,
3628                        doc: None,
3629                    }],
3630                    returns: Some(TypeRef::List(Box::new(TypeRef::StringUtf8))),
3631                    doc: None,
3632                    r#async: false,
3633                    cancellable: false,
3634                    deprecated: None,
3635                    since: None,
3636                },
3637            ],
3638            structs: vec![StructDef {
3639                name: "Contact".to_string(),
3640                doc: None,
3641                fields: vec![
3642                    StructField {
3643                        name: "name".to_string(),
3644                        ty: TypeRef::StringUtf8,
3645                        doc: None,
3646                        default: None,
3647                    },
3648                    StructField {
3649                        name: "email".to_string(),
3650                        ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
3651                        doc: None,
3652                        default: None,
3653                    },
3654                    StructField {
3655                        name: "tags".to_string(),
3656                        ty: TypeRef::List(Box::new(TypeRef::StringUtf8)),
3657                        doc: None,
3658                        default: None,
3659                    },
3660                ],
3661                builder: false,
3662            }],
3663            enums: vec![EnumDef {
3664                name: "Color".to_string(),
3665                doc: None,
3666                variants: vec![
3667                    EnumVariant {
3668                        name: "Red".to_string(),
3669                        value: 0,
3670                        doc: None,
3671                        fields: vec![],
3672                    },
3673                    EnumVariant {
3674                        name: "Green".to_string(),
3675                        value: 1,
3676                        doc: None,
3677                        fields: vec![],
3678                    },
3679                    EnumVariant {
3680                        name: "Blue".to_string(),
3681                        value: 2,
3682                        doc: None,
3683                        fields: vec![],
3684                    },
3685                ],
3686            }],
3687            callbacks: vec![],
3688            listeners: vec![],
3689            errors: None,
3690            modules: vec![],
3691        }]);
3692
3693        let tmp = std::env::temp_dir().join("weaveffi_test_node_structs_and_enums");
3694        let _ = std::fs::remove_dir_all(&tmp);
3695        std::fs::create_dir_all(&tmp).unwrap();
3696        let out_dir = Utf8Path::from_path(&tmp).expect("temp dir is valid UTF-8");
3697
3698        NodeGenerator
3699            .generate(
3700                &api,
3701                out_dir,
3702                &NodeConfig {
3703                    strip_module_prefix: true,
3704                    ..NodeConfig::default()
3705                },
3706            )
3707            .unwrap();
3708
3709        let dts = std::fs::read_to_string(tmp.join("node").join("types.d.ts")).unwrap();
3710
3711        assert!(
3712            dts.contains("export interface Contact {"),
3713            "missing Contact interface: {dts}"
3714        );
3715        assert!(dts.contains("  name: string;"), "missing name field: {dts}");
3716        assert!(
3717            dts.contains("  email: string | null;"),
3718            "missing optional email field: {dts}"
3719        );
3720        assert!(
3721            dts.contains("  tags: string[];"),
3722            "missing list tags field: {dts}"
3723        );
3724
3725        assert!(
3726            dts.contains("export enum Color {"),
3727            "missing Color enum: {dts}"
3728        );
3729        assert!(dts.contains("  Red = 0,"), "missing Red variant: {dts}");
3730        assert!(dts.contains("  Green = 1,"), "missing Green variant: {dts}");
3731        assert!(dts.contains("  Blue = 2,"), "missing Blue variant: {dts}");
3732
3733        assert!(
3734            dts.contains("export function get_contact(id: number): Contact | null"),
3735            "missing get_contact with optional return: {dts}"
3736        );
3737        assert!(
3738            dts.contains("export function list_contacts(): Contact[]"),
3739            "missing list_contacts with list return: {dts}"
3740        );
3741        assert!(
3742            dts.contains(
3743                "export function set_favorite_color(contact_id: number, color: Color | null): void"
3744            ),
3745            "missing set_favorite_color with optional enum param: {dts}"
3746        );
3747        assert!(
3748            dts.contains("export function get_tags(contact_id: number): string[]"),
3749            "missing get_tags with list return: {dts}"
3750        );
3751
3752        let iface_pos = dts.find("export interface Contact").unwrap();
3753        let enum_pos = dts.find("export enum Color").unwrap();
3754        let fn_pos = dts.find("export function get_contact").unwrap();
3755        assert!(
3756            iface_pos < fn_pos,
3757            "interface should appear before functions"
3758        );
3759        assert!(enum_pos < fn_pos, "enum should appear before functions");
3760
3761        let _ = std::fs::remove_dir_all(&tmp);
3762    }
3763
3764    #[test]
3765    fn node_custom_package_name() {
3766        let api = make_api(vec![make_module("math")]);
3767
3768        let tmp = std::env::temp_dir().join("weaveffi_test_node_custom_pkg");
3769        let _ = std::fs::remove_dir_all(&tmp);
3770        std::fs::create_dir_all(&tmp).unwrap();
3771        let out_dir = Utf8Path::from_path(&tmp).expect("temp dir is valid UTF-8");
3772
3773        let config = NodeConfig {
3774            package_name: Some("@myorg/cool-lib".into()),
3775            ..NodeConfig::default()
3776        };
3777        NodeGenerator.generate(&api, out_dir, &config).unwrap();
3778
3779        let pkg = std::fs::read_to_string(tmp.join("node").join("package.json")).unwrap();
3780        assert!(
3781            pkg.contains("\"name\": \"@myorg/cool-lib\""),
3782            "package.json should use custom name: {pkg}"
3783        );
3784        assert!(
3785            !pkg.contains("\"name\": \"weaveffi\""),
3786            "package.json should not contain default name: {pkg}"
3787        );
3788
3789        let _ = std::fs::remove_dir_all(&tmp);
3790    }
3791
3792    #[test]
3793    fn node_dts_has_jsdoc() {
3794        let api = make_api(vec![{
3795            let mut m = make_module("math");
3796            m.functions.push(Function {
3797                name: "add".into(),
3798                params: vec![
3799                    Param {
3800                        name: "a".into(),
3801                        ty: TypeRef::I32,
3802                        mutable: false,
3803                        doc: None,
3804                    },
3805                    Param {
3806                        name: "b".into(),
3807                        ty: TypeRef::I32,
3808                        mutable: false,
3809                        doc: None,
3810                    },
3811                ],
3812                returns: Some(TypeRef::I32),
3813                doc: None,
3814                r#async: false,
3815                cancellable: false,
3816                deprecated: None,
3817                since: None,
3818            });
3819            m.functions.push(Function {
3820                name: "subtract".into(),
3821                params: vec![
3822                    Param {
3823                        name: "a".into(),
3824                        ty: TypeRef::I32,
3825                        mutable: false,
3826                        doc: None,
3827                    },
3828                    Param {
3829                        name: "b".into(),
3830                        ty: TypeRef::I32,
3831                        mutable: false,
3832                        doc: None,
3833                    },
3834                ],
3835                returns: Some(TypeRef::I32),
3836                doc: None,
3837                r#async: false,
3838                cancellable: false,
3839                deprecated: None,
3840                since: None,
3841            });
3842            m
3843        }]);
3844
3845        let dts = render_node_dts(&api, "weaveffi", true, "weaveffi.yml");
3846
3847        assert!(
3848            dts.contains("Maps to C function: weaveffi_math_add"),
3849            "missing JSDoc for add: {dts}"
3850        );
3851        assert!(
3852            dts.contains("Maps to C function: weaveffi_math_subtract"),
3853            "missing JSDoc for subtract: {dts}"
3854        );
3855    }
3856
3857    #[test]
3858    fn node_addon_has_no_todo() {
3859        let api = make_api(vec![{
3860            let mut m = make_module("math");
3861            m.functions.push(Function {
3862                name: "add".into(),
3863                params: vec![
3864                    Param {
3865                        name: "a".into(),
3866                        ty: TypeRef::I32,
3867                        mutable: false,
3868                        doc: None,
3869                    },
3870                    Param {
3871                        name: "b".into(),
3872                        ty: TypeRef::I32,
3873                        mutable: false,
3874                        doc: None,
3875                    },
3876                ],
3877                returns: Some(TypeRef::I32),
3878                doc: None,
3879                r#async: false,
3880                cancellable: false,
3881                deprecated: None,
3882                since: None,
3883            });
3884            m
3885        }]);
3886        let addon = render_addon_c(&api, "weaveffi", true, "weaveffi.yml");
3887        assert!(
3888            !addon.contains("// TODO: implement"),
3889            "generated addon.c should not contain TODO comments: {addon}"
3890        );
3891    }
3892
3893    #[test]
3894    fn node_addon_extracts_args() {
3895        let api = make_api(vec![{
3896            let mut m = make_module("math");
3897            m.functions.push(Function {
3898                name: "add".into(),
3899                params: vec![
3900                    Param {
3901                        name: "a".into(),
3902                        ty: TypeRef::I32,
3903                        mutable: false,
3904                        doc: None,
3905                    },
3906                    Param {
3907                        name: "b".into(),
3908                        ty: TypeRef::I32,
3909                        mutable: false,
3910                        doc: None,
3911                    },
3912                ],
3913                returns: Some(TypeRef::I32),
3914                doc: None,
3915                r#async: false,
3916                cancellable: false,
3917                deprecated: None,
3918                since: None,
3919            });
3920            m
3921        }]);
3922        let addon = render_addon_c(&api, "weaveffi", true, "weaveffi.yml");
3923        assert!(
3924            addon.contains("napi_get_cb_info"),
3925            "generated addon.c should call napi_get_cb_info: {addon}"
3926        );
3927    }
3928
3929    #[test]
3930    fn node_addon_frees_strings() {
3931        let api = make_api(vec![{
3932            let mut m = make_module("greet");
3933            m.functions.push(Function {
3934                name: "hello".into(),
3935                params: vec![Param {
3936                    name: "name".into(),
3937                    ty: TypeRef::StringUtf8,
3938                    mutable: false,
3939                    doc: None,
3940                }],
3941                returns: Some(TypeRef::StringUtf8),
3942                doc: None,
3943                r#async: false,
3944                cancellable: false,
3945                deprecated: None,
3946                since: None,
3947            });
3948            m
3949        }]);
3950        let addon = render_addon_c(&api, "weaveffi", true, "weaveffi.yml");
3951        assert!(
3952            addon.contains("weaveffi_free_string(result)"),
3953            "generated addon should free returned strings: {addon}"
3954        );
3955        assert!(
3956            addon.contains("#include <string.h>"),
3957            "generated addon should include string.h: {addon}"
3958        );
3959        assert!(
3960            addon.contains("#include <stdlib.h>"),
3961            "generated addon should include stdlib.h: {addon}"
3962        );
3963        assert!(
3964            addon.contains("weaveffi_error_clear(&err)"),
3965            "generated addon should clear errors: {addon}"
3966        );
3967    }
3968
3969    #[test]
3970    fn node_custom_prefix_threads_to_user_symbols() {
3971        let api = make_api(vec![{
3972            let mut m = make_module("greet");
3973            m.functions.push(Function {
3974                name: "hello".into(),
3975                params: vec![Param {
3976                    name: "name".into(),
3977                    ty: TypeRef::StringUtf8,
3978                    mutable: false,
3979                    doc: None,
3980                }],
3981                returns: Some(TypeRef::StringUtf8),
3982                doc: None,
3983                r#async: false,
3984                cancellable: false,
3985                deprecated: None,
3986                since: None,
3987            });
3988            m
3989        }]);
3990
3991        let config = NodeConfig {
3992            prefix: Some("myffi".into()),
3993            ..NodeConfig::default()
3994        };
3995
3996        let tmp = std::env::temp_dir().join("weaveffi_test_node_custom_prefix");
3997        let _ = std::fs::remove_dir_all(&tmp);
3998        std::fs::create_dir_all(&tmp).unwrap();
3999        let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8");
4000
4001        NodeGenerator.generate(&api, out_dir, &config).unwrap();
4002
4003        // The output file name is a fixed library artifact name, not the ABI
4004        // prefix, so it stays `weaveffi_addon.c` regardless of `prefix`.
4005        let addon = std::fs::read_to_string(tmp.join("node/weaveffi_addon.c")).unwrap();
4006
4007        // User symbols pick up the configured ABI prefix.
4008        assert!(
4009            addon.contains("myffi_greet_hello"),
4010            "addon should call the prefixed user symbol myffi_greet_hello: {addon}"
4011        );
4012        assert!(
4013            !addon.contains("weaveffi_greet_hello"),
4014            "addon must not emit the hard-coded weaveffi_ user symbol: {addon}"
4015        );
4016        assert!(
4017            addon.contains("#include \"myffi.h\""),
4018            "addon should include the prefixed header myffi.h: {addon}"
4019        );
4020
4021        // Runtime ABI helpers are supplied by weaveffi-abi and stay literal.
4022        assert!(
4023            addon.contains("weaveffi_error"),
4024            "runtime weaveffi_error must remain literal: {addon}"
4025        );
4026        assert!(
4027            addon.contains("weaveffi_free_string"),
4028            "runtime weaveffi_free_string must remain literal: {addon}"
4029        );
4030
4031        let _ = std::fs::remove_dir_all(&tmp);
4032    }
4033
4034    #[test]
4035    fn node_addon_checks_error() {
4036        let api = make_api(vec![{
4037            let mut m = make_module("math");
4038            m.functions.push(Function {
4039                name: "add".into(),
4040                params: vec![
4041                    Param {
4042                        name: "a".into(),
4043                        ty: TypeRef::I32,
4044                        mutable: false,
4045                        doc: None,
4046                    },
4047                    Param {
4048                        name: "b".into(),
4049                        ty: TypeRef::I32,
4050                        mutable: false,
4051                        doc: None,
4052                    },
4053                ],
4054                returns: Some(TypeRef::I32),
4055                doc: None,
4056                r#async: false,
4057                cancellable: false,
4058                deprecated: None,
4059                since: None,
4060            });
4061            m
4062        }]);
4063        let addon = render_addon_c(&api, "weaveffi", true, "weaveffi.yml");
4064        assert!(
4065            addon.contains("err.code"),
4066            "generated addon.c should check err.code: {addon}"
4067        );
4068    }
4069
4070    #[test]
4071    fn node_strip_module_prefix() {
4072        let api = make_api(vec![{
4073            let mut m = make_module("contacts");
4074            m.functions.push(Function {
4075                name: "create_contact".into(),
4076                params: vec![Param {
4077                    name: "name".into(),
4078                    ty: TypeRef::StringUtf8,
4079                    mutable: false,
4080                    doc: None,
4081                }],
4082                returns: Some(TypeRef::I32),
4083                doc: None,
4084                r#async: false,
4085                cancellable: false,
4086                deprecated: None,
4087                since: None,
4088            });
4089            m
4090        }]);
4091
4092        let config = NodeConfig {
4093            strip_module_prefix: true,
4094            ..NodeConfig::default()
4095        };
4096
4097        let tmp = std::env::temp_dir().join("weaveffi_test_node_strip_prefix");
4098        let _ = std::fs::remove_dir_all(&tmp);
4099        std::fs::create_dir_all(&tmp).unwrap();
4100        let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8");
4101
4102        NodeGenerator.generate(&api, out_dir, &config).unwrap();
4103
4104        let dts = std::fs::read_to_string(tmp.join("node/types.d.ts")).unwrap();
4105        assert!(
4106            dts.contains("export function create_contact("),
4107            "stripped name should be create_contact: {dts}"
4108        );
4109        assert!(
4110            !dts.contains("export function contacts_create_contact("),
4111            "should not contain module-prefixed name: {dts}"
4112        );
4113
4114        let addon = std::fs::read_to_string(tmp.join("node/weaveffi_addon.c")).unwrap();
4115        assert!(
4116            addon.contains("\"create_contact\""),
4117            "JS export name should be stripped: {addon}"
4118        );
4119        assert!(
4120            addon.contains("weaveffi_contacts_create_contact"),
4121            "C ABI call should still use full name: {addon}"
4122        );
4123
4124        let no_strip = NodeConfig::default();
4125        let tmp2 = std::env::temp_dir().join("weaveffi_test_node_no_strip_prefix");
4126        let _ = std::fs::remove_dir_all(&tmp2);
4127        std::fs::create_dir_all(&tmp2).unwrap();
4128        let out_dir2 = Utf8Path::from_path(&tmp2).expect("valid UTF-8");
4129
4130        NodeGenerator.generate(&api, out_dir2, &no_strip).unwrap();
4131
4132        let dts2 = std::fs::read_to_string(tmp2.join("node/types.d.ts")).unwrap();
4133        assert!(
4134            dts2.contains("export function contacts_create_contact("),
4135            "default should use module-prefixed name: {dts2}"
4136        );
4137
4138        let _ = std::fs::remove_dir_all(&tmp);
4139        let _ = std::fs::remove_dir_all(&tmp2);
4140    }
4141
4142    #[test]
4143    fn node_typed_handle_type() {
4144        let api = make_api(vec![{
4145            let mut m = make_module("contacts");
4146            m.structs.push(StructDef {
4147                name: "Contact".into(),
4148                doc: None,
4149                fields: vec![StructField {
4150                    name: "name".into(),
4151                    ty: TypeRef::StringUtf8,
4152                    doc: None,
4153                    default: None,
4154                }],
4155                builder: false,
4156            });
4157            m.functions.push(Function {
4158                name: "get_info".into(),
4159                params: vec![Param {
4160                    name: "contact".into(),
4161                    ty: TypeRef::TypedHandle("Contact".into()),
4162                    mutable: false,
4163                    doc: None,
4164                }],
4165                returns: None,
4166                doc: None,
4167                r#async: false,
4168                cancellable: false,
4169                deprecated: None,
4170                since: None,
4171            });
4172            m
4173        }]);
4174        let dts = render_node_dts(&api, "weaveffi", true, "weaveffi.yml");
4175        assert!(
4176            dts.contains("contact: Contact"),
4177            "TypedHandle should use class type not bigint: {dts}"
4178        );
4179    }
4180
4181    #[test]
4182    fn node_deeply_nested_optional() {
4183        let api = make_api(vec![Module {
4184            name: "edge".into(),
4185            functions: vec![Function {
4186                name: "process".into(),
4187                params: vec![Param {
4188                    name: "data".into(),
4189                    ty: TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::Optional(
4190                        Box::new(TypeRef::Struct("Contact".into())),
4191                    ))))),
4192                    mutable: false,
4193                    doc: None,
4194                }],
4195                returns: None,
4196                doc: None,
4197                r#async: false,
4198                cancellable: false,
4199                deprecated: None,
4200                since: None,
4201            }],
4202            structs: vec![StructDef {
4203                name: "Contact".into(),
4204                doc: None,
4205                fields: vec![StructField {
4206                    name: "name".into(),
4207                    ty: TypeRef::StringUtf8,
4208                    doc: None,
4209                    default: None,
4210                }],
4211                builder: false,
4212            }],
4213            enums: vec![],
4214            callbacks: vec![],
4215            listeners: vec![],
4216            errors: None,
4217            modules: vec![],
4218        }]);
4219        let dts = render_node_dts(&api, "weaveffi", true, "weaveffi.yml");
4220        assert!(
4221            dts.contains("(Contact | null)[] | null"),
4222            "should contain deeply nested optional type: {dts}"
4223        );
4224    }
4225
4226    #[test]
4227    fn node_map_of_lists() {
4228        let api = make_api(vec![Module {
4229            name: "edge".into(),
4230            functions: vec![Function {
4231                name: "process".into(),
4232                params: vec![Param {
4233                    name: "scores".into(),
4234                    ty: TypeRef::Map(
4235                        Box::new(TypeRef::StringUtf8),
4236                        Box::new(TypeRef::List(Box::new(TypeRef::I32))),
4237                    ),
4238                    mutable: false,
4239                    doc: None,
4240                }],
4241                returns: None,
4242                doc: None,
4243                r#async: false,
4244                cancellable: false,
4245                deprecated: None,
4246                since: None,
4247            }],
4248            structs: vec![],
4249            enums: vec![],
4250            callbacks: vec![],
4251            listeners: vec![],
4252            errors: None,
4253            modules: vec![],
4254        }]);
4255        let dts = render_node_dts(&api, "weaveffi", true, "weaveffi.yml");
4256        assert!(
4257            dts.contains("Record<string, number[]>"),
4258            "should contain map of lists type: {dts}"
4259        );
4260    }
4261
4262    #[test]
4263    fn node_enum_keyed_map() {
4264        let api = make_api(vec![Module {
4265            name: "edge".into(),
4266            functions: vec![Function {
4267                name: "process".into(),
4268                params: vec![Param {
4269                    name: "contacts".into(),
4270                    ty: TypeRef::Map(
4271                        Box::new(TypeRef::Enum("Color".into())),
4272                        Box::new(TypeRef::Struct("Contact".into())),
4273                    ),
4274                    mutable: false,
4275                    doc: None,
4276                }],
4277                returns: None,
4278                doc: None,
4279                r#async: false,
4280                cancellable: false,
4281                deprecated: None,
4282                since: None,
4283            }],
4284            structs: vec![StructDef {
4285                name: "Contact".into(),
4286                doc: None,
4287                fields: vec![StructField {
4288                    name: "name".into(),
4289                    ty: TypeRef::StringUtf8,
4290                    doc: None,
4291                    default: None,
4292                }],
4293                builder: false,
4294            }],
4295            enums: vec![EnumDef {
4296                name: "Color".into(),
4297                doc: None,
4298                variants: vec![
4299                    EnumVariant {
4300                        name: "Red".into(),
4301                        value: 0,
4302                        doc: None,
4303                        fields: vec![],
4304                    },
4305                    EnumVariant {
4306                        name: "Green".into(),
4307                        value: 1,
4308                        doc: None,
4309                        fields: vec![],
4310                    },
4311                    EnumVariant {
4312                        name: "Blue".into(),
4313                        value: 2,
4314                        doc: None,
4315                        fields: vec![],
4316                    },
4317                ],
4318            }],
4319            callbacks: vec![],
4320            listeners: vec![],
4321            errors: None,
4322            modules: vec![],
4323        }]);
4324        let dts = render_node_dts(&api, "weaveffi", true, "weaveffi.yml");
4325        assert!(
4326            dts.contains("Record<Color, Contact>"),
4327            "should contain enum-keyed map type: {dts}"
4328        );
4329    }
4330
4331    #[test]
4332    fn node_no_double_free_on_error() {
4333        let api = make_api(vec![{
4334            let mut m = make_module("contacts");
4335            m.structs.push(StructDef {
4336                name: "Contact".into(),
4337                doc: None,
4338                fields: vec![StructField {
4339                    name: "name".into(),
4340                    ty: TypeRef::StringUtf8,
4341                    doc: None,
4342                    default: None,
4343                }],
4344                builder: false,
4345            });
4346            m.functions.push(Function {
4347                name: "find_contact".into(),
4348                params: vec![Param {
4349                    name: "name".into(),
4350                    ty: TypeRef::StringUtf8,
4351                    mutable: false,
4352                    doc: None,
4353                }],
4354                returns: Some(TypeRef::Struct("Contact".into())),
4355                doc: None,
4356                r#async: false,
4357                cancellable: false,
4358                deprecated: None,
4359                since: None,
4360            });
4361            m
4362        }]);
4363        let addon = render_addon_c(&api, "weaveffi", true, "weaveffi.yml");
4364        assert!(
4365            addon.contains("free(name)"),
4366            "malloc'd JS string copy should be freed after the C call: {addon}"
4367        );
4368        assert!(
4369            !addon.contains("weaveffi_free_string(name)"),
4370            "input string param must not use weaveffi_free_string: {addon}"
4371        );
4372        let free_pos = addon
4373            .find("free(name)")
4374            .expect("free(name) should be present");
4375        let err_pos = addon
4376            .find("if (err.code != 0)")
4377            .expect("err.code check should be present");
4378        assert!(
4379            free_pos < err_pos,
4380            "cleanup should run before error check: free at {free_pos}, err at {err_pos}"
4381        );
4382        let err_block_start = addon
4383            .find("  if (err.code != 0) {\n")
4384            .expect("error if block should be present");
4385        let after_err = &addon[err_block_start..];
4386        let err_block_end_rel = after_err
4387            .find("  }\n  napi_value ret;")
4388            .expect("napi_value ret should follow error block");
4389        let err_block = &addon[err_block_start..err_block_start + err_block_end_rel];
4390        assert!(
4391            !err_block.contains("result"),
4392            "error path should not touch result before return NULL: {err_block}"
4393        );
4394    }
4395
4396    #[test]
4397    fn node_null_check_on_optional_return() {
4398        let api = make_api(vec![{
4399            let mut m = make_module("contacts");
4400            m.structs.push(StructDef {
4401                name: "Contact".into(),
4402                doc: None,
4403                fields: vec![StructField {
4404                    name: "name".into(),
4405                    ty: TypeRef::StringUtf8,
4406                    doc: None,
4407                    default: None,
4408                }],
4409                builder: false,
4410            });
4411            m.functions.push(Function {
4412                name: "find_contact".into(),
4413                params: vec![Param {
4414                    name: "id".into(),
4415                    ty: TypeRef::I32,
4416                    mutable: false,
4417                    doc: None,
4418                }],
4419                returns: Some(TypeRef::Optional(Box::new(TypeRef::Struct(
4420                    "Contact".into(),
4421                )))),
4422                doc: None,
4423                r#async: false,
4424                cancellable: false,
4425                deprecated: None,
4426                since: None,
4427            });
4428            m
4429        }]);
4430        let addon = render_addon_c(&api, "weaveffi", true, "weaveffi.yml");
4431        assert!(
4432            addon.contains("if (result == NULL)"),
4433            "optional struct return should null-check before wrapping: {addon}"
4434        );
4435        assert!(
4436            addon.contains("napi_get_null"),
4437            "optional absent should return JS null via napi_get_null: {addon}"
4438        );
4439    }
4440
4441    #[test]
4442    fn node_async_returns_promise() {
4443        let api = make_api(vec![{
4444            let mut m = make_module("tasks");
4445            m.functions.push(Function {
4446                name: "run".into(),
4447                params: vec![Param {
4448                    name: "id".into(),
4449                    ty: TypeRef::I32,
4450                    mutable: false,
4451                    doc: None,
4452                }],
4453                returns: Some(TypeRef::StringUtf8),
4454                doc: None,
4455                r#async: true,
4456                cancellable: false,
4457                deprecated: None,
4458                since: None,
4459            });
4460            m.functions.push(Function {
4461                name: "fire_and_forget".into(),
4462                params: vec![],
4463                returns: None,
4464                doc: None,
4465                r#async: true,
4466                cancellable: false,
4467                deprecated: None,
4468                since: None,
4469            });
4470            m
4471        }]);
4472        let dts = render_node_dts(&api, "weaveffi", true, "weaveffi.yml");
4473        assert!(
4474            dts.contains("Promise<"),
4475            "async function should return Promise in .d.ts: {dts}"
4476        );
4477        assert!(
4478            dts.contains("): Promise<string>"),
4479            "async string return should be Promise<string>: {dts}"
4480        );
4481        assert!(
4482            dts.contains("): Promise<void>"),
4483            "async void return should be Promise<void>: {dts}"
4484        );
4485    }
4486
4487    #[test]
4488    fn node_addon_creates_promise() {
4489        let api = make_api(vec![{
4490            let mut m = make_module("tasks");
4491            m.functions.push(Function {
4492                name: "run".into(),
4493                params: vec![Param {
4494                    name: "id".into(),
4495                    ty: TypeRef::I32,
4496                    mutable: false,
4497                    doc: None,
4498                }],
4499                returns: Some(TypeRef::I32),
4500                doc: None,
4501                r#async: true,
4502                cancellable: false,
4503                deprecated: None,
4504                since: None,
4505            });
4506            m
4507        }]);
4508        let addon = render_addon_c(&api, "weaveffi", true, "weaveffi.yml");
4509        assert!(
4510            addon.contains("napi_create_promise"),
4511            "async addon should call napi_create_promise: {addon}"
4512        );
4513        assert!(
4514            addon.contains("napi_resolve_deferred"),
4515            "async callback should call napi_resolve_deferred: {addon}"
4516        );
4517        assert!(
4518            addon.contains("napi_reject_deferred"),
4519            "async callback should call napi_reject_deferred: {addon}"
4520        );
4521        assert!(
4522            addon.contains("weaveffi_tasks_run_napi_actx"),
4523            "async addon should define per-fn async context struct: {addon}"
4524        );
4525        assert!(
4526            addon.contains("weaveffi_tasks_run_async("),
4527            "async addon should call the _async C function: {addon}"
4528        );
4529        assert!(
4530            addon.contains("weaveffi_tasks_run_napi_cb"),
4531            "async addon should define the callback: {addon}"
4532        );
4533        // The completion callback may fire on any producer thread, so it must
4534        // queue through a threadsafe function instead of touching napi_env.
4535        assert!(
4536            addon.contains("napi_call_threadsafe_function(ctx->tsfn, ctx, napi_tsfn_blocking)"),
4537            "completion callback must hop to the JS thread via tsfn: {addon}"
4538        );
4539        assert!(
4540            !addon.contains("napi_resolve_deferred(ctx->env"),
4541            "deferred must never be settled from the producer thread: {addon}"
4542        );
4543    }
4544
4545    /// The N-API deferred is created with `napi_create_promise` and settled
4546    /// (on the JS thread) by exactly one of `napi_resolve_deferred` /
4547    /// `napi_reject_deferred`. The per-fn async context that carries the
4548    /// deferred + threadsafe function across threads must be allocated once
4549    /// and freed exactly once, and the tsfn released exactly once.
4550    #[test]
4551    fn node_async_pins_callback_for_lifetime() {
4552        let api = make_api(vec![{
4553            let mut m = make_module("tasks");
4554            m.functions.push(Function {
4555                name: "run".into(),
4556                params: vec![Param {
4557                    name: "id".into(),
4558                    ty: TypeRef::I32,
4559                    mutable: false,
4560                    doc: None,
4561                }],
4562                returns: Some(TypeRef::I32),
4563                doc: None,
4564                r#async: true,
4565                cancellable: false,
4566                deprecated: None,
4567                since: None,
4568            });
4569            m
4570        }]);
4571        let addon = render_addon_c(&api, "weaveffi", true, "weaveffi.yml");
4572        let create_count = addon.matches("napi_create_promise").count();
4573        let resolve_count = addon.matches("napi_resolve_deferred").count();
4574        let reject_count = addon.matches("napi_reject_deferred").count();
4575        let alloc_count = addon
4576            .matches("calloc(1, sizeof(weaveffi_tasks_run_napi_actx))")
4577            .count();
4578        let free_count = addon.matches("free(ctx);").count();
4579        let release_count = addon
4580            .matches("napi_release_threadsafe_function(ctx->tsfn, napi_tsfn_release);")
4581            .count();
4582        assert_eq!(
4583            create_count, 1,
4584            "expected one napi_create_promise per async fn, got {create_count}: {addon}"
4585        );
4586        assert_eq!(
4587            resolve_count, 1,
4588            "expected one napi_resolve_deferred per async fn, got {resolve_count}: {addon}"
4589        );
4590        assert_eq!(
4591            reject_count, 1,
4592            "expected one napi_reject_deferred per async fn, got {reject_count}: {addon}"
4593        );
4594        assert_eq!(
4595            alloc_count, free_count,
4596            "ctx alloc / free must balance per async fn: alloc={alloc_count} free={free_count}: {addon}"
4597        );
4598        assert_eq!(
4599            release_count, 1,
4600            "tsfn must be released exactly once per async fn, got {release_count}: {addon}"
4601        );
4602    }
4603
4604    fn doc_module() -> Module {
4605        Module {
4606            name: "docs".into(),
4607            functions: vec![Function {
4608                name: "do_thing".into(),
4609                params: vec![Param {
4610                    name: "x".into(),
4611                    ty: TypeRef::I32,
4612                    mutable: false,
4613                    doc: Some("the input value".into()),
4614                }],
4615                returns: Some(TypeRef::I32),
4616                doc: Some("Performs a thing.".into()),
4617                r#async: false,
4618                cancellable: false,
4619                deprecated: None,
4620                since: None,
4621            }],
4622            structs: vec![StructDef {
4623                name: "Item".into(),
4624                doc: Some("An item we track.".into()),
4625                fields: vec![StructField {
4626                    name: "id".into(),
4627                    ty: TypeRef::I64,
4628                    doc: Some("Stable id".into()),
4629                    default: None,
4630                }],
4631                builder: false,
4632            }],
4633            enums: vec![EnumDef {
4634                name: "Kind".into(),
4635                doc: Some("Kind of item.".into()),
4636                variants: vec![EnumVariant {
4637                    name: "Small".into(),
4638                    value: 0,
4639                    doc: Some("A small one".into()),
4640                    fields: vec![],
4641                }],
4642            }],
4643            callbacks: vec![],
4644            listeners: vec![],
4645            errors: None,
4646            modules: vec![],
4647        }
4648    }
4649
4650    #[test]
4651    fn node_emits_doc_on_function() {
4652        let dts = render_node_dts(
4653            &make_api(vec![doc_module()]),
4654            "weaveffi",
4655            true,
4656            "weaveffi.yml",
4657        );
4658        assert!(dts.contains("Performs a thing."), "{dts}");
4659    }
4660
4661    #[test]
4662    fn node_emits_doc_on_struct() {
4663        let dts = render_node_dts(
4664            &make_api(vec![doc_module()]),
4665            "weaveffi",
4666            true,
4667            "weaveffi.yml",
4668        );
4669        assert!(dts.contains("/** An item we track. */"), "{dts}");
4670    }
4671
4672    #[test]
4673    fn node_emits_doc_on_enum_variant() {
4674        let dts = render_node_dts(
4675            &make_api(vec![doc_module()]),
4676            "weaveffi",
4677            true,
4678            "weaveffi.yml",
4679        );
4680        assert!(dts.contains("/** Kind of item. */"), "{dts}");
4681        assert!(dts.contains("/** A small one */"), "{dts}");
4682    }
4683
4684    #[test]
4685    fn node_emits_doc_on_field() {
4686        let dts = render_node_dts(
4687            &make_api(vec![doc_module()]),
4688            "weaveffi",
4689            true,
4690            "weaveffi.yml",
4691        );
4692        assert!(dts.contains("/** Stable id */"), "{dts}");
4693    }
4694
4695    #[test]
4696    fn node_emits_doc_on_param() {
4697        let dts = render_node_dts(
4698            &make_api(vec![doc_module()]),
4699            "weaveffi",
4700            true,
4701            "weaveffi.yml",
4702        );
4703        assert!(dts.contains("@param x the input value"), "{dts}");
4704    }
4705
4706    // --- Rich (algebraic) enum support ------------------------------------
4707
4708    /// A module mirroring `samples/shapes/shapes.yml`: a rich enum `Shape`
4709    /// (unit + f64 + two-f32 + string/u8 variants), a plain enum `Channel`, and
4710    /// the free functions that take/return the rich enum plus a numeric smoke.
4711    fn shapes_module() -> Module {
4712        fn field(name: &str, ty: TypeRef) -> StructField {
4713            StructField {
4714                name: name.into(),
4715                ty,
4716                doc: None,
4717                default: None,
4718            }
4719        }
4720        fn variant(name: &str, value: i32, fields: Vec<StructField>) -> EnumVariant {
4721            EnumVariant {
4722                name: name.into(),
4723                value,
4724                doc: None,
4725                fields,
4726            }
4727        }
4728        Module {
4729            name: "shapes".into(),
4730            functions: vec![
4731                Function {
4732                    name: "describe".into(),
4733                    params: vec![Param {
4734                        name: "shape".into(),
4735                        ty: TypeRef::Struct("Shape".into()),
4736                        mutable: false,
4737                        doc: None,
4738                    }],
4739                    returns: Some(TypeRef::StringUtf8),
4740                    doc: None,
4741                    r#async: false,
4742                    cancellable: false,
4743                    deprecated: None,
4744                    since: None,
4745                },
4746                Function {
4747                    name: "scale".into(),
4748                    params: vec![
4749                        Param {
4750                            name: "shape".into(),
4751                            ty: TypeRef::Struct("Shape".into()),
4752                            mutable: false,
4753                            doc: None,
4754                        },
4755                        Param {
4756                            name: "factor".into(),
4757                            ty: TypeRef::F64,
4758                            mutable: false,
4759                            doc: None,
4760                        },
4761                    ],
4762                    returns: Some(TypeRef::Struct("Shape".into())),
4763                    doc: None,
4764                    r#async: false,
4765                    cancellable: false,
4766                    deprecated: None,
4767                    since: None,
4768                },
4769                Function {
4770                    name: "sum_bytes".into(),
4771                    params: vec![Param {
4772                        name: "values".into(),
4773                        ty: TypeRef::List(Box::new(TypeRef::U8)),
4774                        mutable: false,
4775                        doc: None,
4776                    }],
4777                    returns: Some(TypeRef::U64),
4778                    doc: None,
4779                    r#async: false,
4780                    cancellable: false,
4781                    deprecated: None,
4782                    since: None,
4783                },
4784            ],
4785            structs: vec![],
4786            enums: vec![
4787                EnumDef {
4788                    name: "Shape".into(),
4789                    doc: None,
4790                    variants: vec![
4791                        variant("Empty", 0, vec![]),
4792                        variant("Circle", 1, vec![field("radius", TypeRef::F64)]),
4793                        variant(
4794                            "Rectangle",
4795                            2,
4796                            vec![field("width", TypeRef::F32), field("height", TypeRef::F32)],
4797                        ),
4798                        variant(
4799                            "Labeled",
4800                            3,
4801                            vec![
4802                                field("label", TypeRef::StringUtf8),
4803                                field("count", TypeRef::U8),
4804                            ],
4805                        ),
4806                    ],
4807                },
4808                EnumDef {
4809                    name: "Channel".into(),
4810                    doc: None,
4811                    variants: vec![
4812                        EnumVariant {
4813                            name: "Red".into(),
4814                            value: 0,
4815                            doc: None,
4816                            fields: vec![],
4817                        },
4818                        EnumVariant {
4819                            name: "Green".into(),
4820                            value: 1,
4821                            doc: None,
4822                            fields: vec![],
4823                        },
4824                    ],
4825                },
4826            ],
4827            callbacks: vec![],
4828            listeners: vec![],
4829            errors: None,
4830            modules: vec![],
4831        }
4832    }
4833
4834    #[test]
4835    fn rich_enum_addon_exposes_native_helpers() {
4836        let addon = render_addon_c(
4837            &make_api(vec![shapes_module()]),
4838            "weaveffi",
4839            false,
4840            "shapes.yml",
4841        );
4842
4843        // Tag reader, per-variant constructors, per-variant field getters, and
4844        // the destructor are all defined as native functions over the C ABI.
4845        for sym in [
4846            "Napi_weaveffi_shapes_Shape_tag",
4847            "Napi_weaveffi_shapes_Shape_Empty_new",
4848            "Napi_weaveffi_shapes_Shape_Circle_new",
4849            "Napi_weaveffi_shapes_Shape_Rectangle_new",
4850            "Napi_weaveffi_shapes_Shape_Labeled_new",
4851            "Napi_weaveffi_shapes_Shape_Circle_get_radius",
4852            "Napi_weaveffi_shapes_Shape_Rectangle_get_width",
4853            "Napi_weaveffi_shapes_Shape_Rectangle_get_height",
4854            "Napi_weaveffi_shapes_Shape_Labeled_get_label",
4855            "Napi_weaveffi_shapes_Shape_Labeled_get_count",
4856            "Napi_weaveffi_shapes_Shape_destroy",
4857        ] {
4858            assert!(addon.contains(sym), "missing native helper {sym}: {addon}");
4859        }
4860
4861        // Each is exported under an idiomatic JS name.
4862        for js in [
4863            "\"shapes_Shape_tag\"",
4864            "\"shapes_Shape_empty_new\"",
4865            "\"shapes_Shape_circle_new\"",
4866            "\"shapes_Shape_rectangle_new\"",
4867            "\"shapes_Shape_labeled_new\"",
4868            "\"shapes_Shape_circle_get_radius\"",
4869            "\"shapes_Shape_labeled_get_label\"",
4870            "\"shapes_Shape_labeled_get_count\"",
4871            "\"shapes_Shape_destroy\"",
4872        ] {
4873            assert!(addon.contains(js), "missing JS export {js}: {addon}");
4874        }
4875    }
4876
4877    #[test]
4878    fn rich_enum_addon_calls_c_abi_correctly() {
4879        let addon = render_addon_c(
4880            &make_api(vec![shapes_module()]),
4881            "weaveffi",
4882            false,
4883            "shapes.yml",
4884        );
4885
4886        // Constructors thread out_err and return the owned pointer as a handle.
4887        assert!(
4888            addon.contains(
4889                "weaveffi_shapes_Shape* result = weaveffi_shapes_Shape_Circle_new(radius, &err);"
4890            ),
4891            "circle ctor must call the C constructor: {addon}"
4892        );
4893        // f32 variant fields narrow from the N-API double getter.
4894        assert!(
4895            addon.contains(
4896                "weaveffi_shapes_Shape_Rectangle_new((float)width_raw, (float)height_raw, &err);"
4897            ),
4898            "rectangle ctor must narrow f32 args: {addon}"
4899        );
4900        // string + u8 variant: string copy freed after the call, u8 narrowed.
4901        assert!(
4902            addon.contains("weaveffi_shapes_Shape_Labeled_new(label, (uint8_t)count_raw, &err);"),
4903            "labeled ctor must marshal string + u8: {addon}"
4904        );
4905        assert!(
4906            addon.contains("free(label);"),
4907            "labeled ctor must free its string copy: {addon}"
4908        );
4909        // tag reader returns the int32 discriminant.
4910        assert!(
4911            addon.contains("napi_create_int32(env, weaveffi_shapes_Shape_tag(self), &ret);"),
4912            "tag reader must return the discriminant: {addon}"
4913        );
4914        // String getter frees the owned C string after copying it to JS.
4915        assert!(
4916            addon.contains("weaveffi_free_string(ret_s);"),
4917            "string field getter must free the owned C string: {addon}"
4918        );
4919        // Destructor frees the opaque object.
4920        assert!(
4921            addon.contains("weaveffi_shapes_Shape_destroy(self);"),
4922            "destructor must free the object: {addon}"
4923        );
4924
4925        // Free functions marshal the rich enum as the opaque handle (no attempt
4926        // to materialize it as a plain object), in and out.
4927        assert!(
4928            addon.contains(
4929                "weaveffi_shapes_describe((const weaveffi_shapes_Shape*)(intptr_t)shape_raw, &err);"
4930            ),
4931            "describe must pass the opaque handle: {addon}"
4932        );
4933        assert!(
4934            addon.contains(
4935                "weaveffi_shapes_Shape* result = weaveffi_shapes_scale((const weaveffi_shapes_Shape*)(intptr_t)shape_raw, factor, &err);"
4936            ),
4937            "scale must take and return the opaque handle: {addon}"
4938        );
4939        assert!(
4940            addon.contains("napi_create_int64(env, (int64_t)(intptr_t)result, &ret);"),
4941            "scale must return the opaque handle as int64: {addon}"
4942        );
4943    }
4944
4945    #[test]
4946    fn rich_enum_index_js_exposes_class() {
4947        let index = render_node_index(
4948            &make_api(vec![shapes_module()]),
4949            "weaveffi",
4950            false,
4951            "shapes.yml",
4952        );
4953
4954        assert!(
4955            index.contains("class Shape {"),
4956            "missing Shape class: {index}"
4957        );
4958        // Per-variant static factories.
4959        for factory in [
4960            "static empty() {",
4961            "static circle(radius) {",
4962            "static rectangle(width, height) {",
4963            "static labeled(label, count) {",
4964        ] {
4965            assert!(
4966                index.contains(factory),
4967                "missing factory `{factory}`: {index}"
4968            );
4969        }
4970        // The factories call the native constructors.
4971        assert!(
4972            index.contains("return new Shape(addon.shapes_Shape_circle_new(radius));"),
4973            "circle factory must call the native ctor: {index}"
4974        );
4975        // tag reader + namespaced per-variant getters.
4976        assert!(
4977            index.contains("tag() {") && index.contains("addon.shapes_Shape_tag(this._handle)"),
4978            "missing tag(): {index}"
4979        );
4980        for getter in [
4981            "get circleRadius() {",
4982            "get rectangleWidth() {",
4983            "get rectangleHeight() {",
4984            "get labeledLabel() {",
4985            "get labeledCount() {",
4986        ] {
4987            assert!(index.contains(getter), "missing getter `{getter}`: {index}");
4988        }
4989        // Cleanup: explicit destroy + a FinalizationRegistry safety net.
4990        assert!(
4991            index.contains("destroy() {")
4992                && index.contains("addon.shapes_Shape_destroy(this._handle)"),
4993            "missing destroy(): {index}"
4994        );
4995        assert!(
4996            index.contains("new FinalizationRegistry"),
4997            "missing FinalizationRegistry cleanup: {index}"
4998        );
4999        // Discriminant map.
5000        assert!(
5001            index.contains(
5002                "Shape.Tag = Object.freeze({ Empty: 0, Circle: 1, Rectangle: 2, Labeled: 3 });"
5003            ),
5004            "missing Tag discriminant map: {index}"
5005        );
5006        // Module functions that carry the rich enum are rewrapped to speak the
5007        // class; `scale` returns a wrapped instance, `describe` unwraps its arg.
5008        assert!(
5009            index.contains("wv.shapes_scale = function (shape, factor) {")
5010                && index.contains("return new Shape(addon.shapes_scale(shape instanceof Shape ? shape._handle : shape, factor));"),
5011            "scale must be rewrapped to return a Shape: {index}"
5012        );
5013        assert!(
5014            index.contains(
5015                "return addon.shapes_describe(shape instanceof Shape ? shape._handle : shape);"
5016            ),
5017            "describe must unwrap a Shape argument: {index}"
5018        );
5019        // A function with no rich enum is left as the raw native binding.
5020        assert!(
5021            !index.contains("wv.shapes_sum_bytes = function"),
5022            "sum_bytes must not be rewrapped: {index}"
5023        );
5024    }
5025
5026    #[test]
5027    fn rich_enum_index_js_without_rich_is_plain_reexport() {
5028        // A model with no rich enums keeps the historical `module.exports = addon`.
5029        let mut m = make_module("math");
5030        m.functions.push(Function {
5031            name: "add".into(),
5032            params: vec![Param {
5033                name: "a".into(),
5034                ty: TypeRef::I32,
5035                mutable: false,
5036                doc: None,
5037            }],
5038            returns: Some(TypeRef::I32),
5039            doc: None,
5040            r#async: false,
5041            cancellable: false,
5042            deprecated: None,
5043            since: None,
5044        });
5045        let index = render_node_index(&make_api(vec![m]), "weaveffi", false, "weaveffi.yml");
5046        assert!(
5047            index.contains("module.exports = addon;"),
5048            "no-rich-enum index must re-export the addon directly: {index}"
5049        );
5050        assert!(
5051            !index.contains("class "),
5052            "no class should be emitted: {index}"
5053        );
5054    }
5055
5056    #[test]
5057    fn rich_enum_dts_emits_class_not_enum() {
5058        let dts = render_node_dts(
5059            &make_api(vec![shapes_module()]),
5060            "weaveffi",
5061            false,
5062            "shapes.yml",
5063        );
5064
5065        // Rich enum -> class with factories, tag(), getters, destroy().
5066        assert!(
5067            dts.contains("export class Shape {"),
5068            "rich enum must be a class: {dts}"
5069        );
5070        assert!(
5071            !dts.contains("export enum Shape"),
5072            "rich enum must not be a plain enum: {dts}"
5073        );
5074        assert!(
5075            dts.contains("static circle(radius: number): Shape;"),
5076            "{dts}"
5077        );
5078        assert!(
5079            dts.contains("static labeled(label: string, count: number): Shape;"),
5080            "{dts}"
5081        );
5082        assert!(dts.contains("tag(): number;"), "{dts}");
5083        assert!(dts.contains("get circleRadius(): number;"), "{dts}");
5084        assert!(dts.contains("get labeledLabel(): string;"), "{dts}");
5085        assert!(dts.contains("destroy(): void;"), "{dts}");
5086
5087        // Plain enum still surfaces as a numeric `enum`.
5088        assert!(
5089            dts.contains("export enum Channel {"),
5090            "plain enum stays an enum: {dts}"
5091        );
5092
5093        // Free functions are typed in terms of the class.
5094        assert!(
5095            dts.contains("export function shapes_describe(shape: Shape): string"),
5096            "{dts}"
5097        );
5098        assert!(
5099            dts.contains("export function shapes_scale(shape: Shape, factor: number): Shape"),
5100            "{dts}"
5101        );
5102    }
5103}