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;
9
10use camino::Utf8Path;
11use heck::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, FnBinding, ListenerBinding, ParamBinding, StructBinding,
19};
20use weaveffi_core::pkg::{self, ResolvedPackage};
21use weaveffi_core::utils::{
22    c_abi_struct_name, local_type_name, render_json_prelude, render_prelude, render_trailer,
23    wrapper_name, CommentStyle,
24};
25use weaveffi_ir::ir::{Api, TypeRef};
26
27/// Per-target configuration for [`NodeGenerator`].
28#[derive(Debug, Clone, Default, Serialize, Deserialize)]
29#[serde(default)]
30pub struct NodeConfig {
31    /// npm package name (default `"weaveffi"`).
32    pub package_name: Option<String>,
33    /// When `true`, strip the IR module name prefix from emitted
34    /// JS/TS function names.
35    pub strip_module_prefix: bool,
36    /// C ABI symbol prefix (default `"weaveffi"`). Normally set once globally
37    /// via `[global] c_prefix`; honored so the native addon calls the same
38    /// exported symbols the producer emits.
39    pub prefix: Option<String>,
40    /// Basename of the IDL the CLI was invoked with.
41    #[serde(skip)]
42    pub input_basename: Option<String>,
43}
44
45impl NodeConfig {
46    pub fn package_name(&self) -> &str {
47        self.package_name.as_deref().unwrap_or("weaveffi")
48    }
49
50    pub fn prefix(&self) -> &str {
51        self.prefix.as_deref().unwrap_or("weaveffi")
52    }
53
54    pub fn input_basename(&self) -> &str {
55        self.input_basename.as_deref().unwrap_or("weaveffi.yml")
56    }
57}
58
59pub struct NodeGenerator;
60
61impl LanguageBackend for NodeGenerator {
62    type Config = NodeConfig;
63
64    fn name(&self) -> &'static str {
65        "node"
66    }
67
68    fn capabilities(&self) -> TargetCapabilities {
69        TargetCapabilities::full()
70    }
71
72    fn prefix<'a>(&self, config: &'a Self::Config) -> &'a str {
73        config.prefix()
74    }
75
76    fn files(
77        &self,
78        api: &Api,
79        _model: &BindingModel,
80        out_dir: &Utf8Path,
81        config: &Self::Config,
82    ) -> Vec<OutputFile> {
83        let dir = out_dir.join("node");
84        let input_basename = config.input_basename();
85        let prefix = config.prefix();
86        let strip = config.strip_module_prefix;
87        let dbl = CommentStyle::DoubleSlash;
88        vec![
89            OutputFile::new(
90                dir.join("index.js"),
91                format!(
92                    "{}// Prefer the default node-gyp output path; fall back to a\n\
93                     // prebuilt index.node placed next to this file.\n\
94                     let addon;\n\
95                     try {{\n  addon = require('./build/Release/weaveffi.node');\n}} catch (e) {{\n  addon = require('./index.node');\n}}\n\
96                     module.exports = addon;\n\n{}",
97                    render_prelude(dbl, input_basename),
98                    render_trailer(dbl, "index.js"),
99                ),
100            ),
101            OutputFile::new(
102                dir.join("types.d.ts"),
103                render_node_dts(api, prefix, strip, input_basename),
104            ),
105            OutputFile::new(
106                dir.join("package.json"),
107                render_package_json(
108                    &pkg::resolve(
109                        api,
110                        config.package_name.as_deref(),
111                        config.input_basename.as_deref(),
112                    ),
113                    input_basename,
114                ),
115            ),
116            OutputFile::new(dir.join("binding.gyp"), render_binding_gyp(input_basename)),
117            OutputFile::new(
118                dir.join("weaveffi_addon.c"),
119                render_addon_c(api, prefix, strip, input_basename),
120            ),
121        ]
122    }
123}
124
125weaveffi_core::impl_generator_via_backend!(NodeGenerator);
126
127fn render_package_json(package: &ResolvedPackage, input_basename: &str) -> String {
128    let prelude = render_json_prelude(input_basename);
129    let name = &package.name;
130    let version = &package.version;
131    let description = package.description_or_default();
132    let mut optional = String::new();
133    if let Some(license) = &package.license {
134        optional.push_str(&format!("  \"license\": \"{license}\",\n"));
135    }
136    if let Some(author) = package.authors.first() {
137        optional.push_str(&format!("  \"author\": \"{author}\",\n"));
138    }
139    if let Some(homepage) = &package.homepage {
140        optional.push_str(&format!("  \"homepage\": \"{homepage}\",\n"));
141    }
142    if let Some(repository) = &package.repository {
143        optional.push_str(&format!(
144            "  \"repository\": {{ \"type\": \"git\", \"url\": \"{repository}\" }},\n"
145        ));
146    }
147    format!(
148        "{{\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"
149    )
150}
151
152fn render_binding_gyp(input_basename: &str) -> String {
153    let prelude = render_prelude(CommentStyle::Hash, input_basename);
154    let trailer = render_trailer(CommentStyle::Hash, "binding.gyp");
155    format!(
156        "{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}"
157    )
158}
159
160fn is_c_ptr_type(ty: &TypeRef) -> bool {
161    matches!(
162        ty,
163        TypeRef::StringUtf8
164            | TypeRef::Bytes
165            | TypeRef::Struct(_)
166            | TypeRef::List(_)
167            | TypeRef::Map(_, _)
168            | TypeRef::Iterator(_)
169    )
170}
171
172fn c_elem_type(ty: &TypeRef, module: &str, prefix: &str) -> String {
173    match ty {
174        TypeRef::I32 => "int32_t".into(),
175        TypeRef::U32 => "uint32_t".into(),
176        TypeRef::I64 => "int64_t".into(),
177        TypeRef::F64 => "double".into(),
178        TypeRef::Bool => "bool".into(),
179        // A generic `handle` is an opaque integer; a typed `handle<T>` is the C
180        // ABI struct pointer for T (same lowering as a struct value), so it must
181        // carry T's owner-qualified symbol, not the generic integer type.
182        TypeRef::Handle => "weaveffi_handle_t".into(),
183        TypeRef::TypedHandle(s) => format!("{}*", c_abi_struct_name(s, module, prefix)),
184        TypeRef::StringUtf8 | TypeRef::BorrowedStr => "const char*".into(),
185        TypeRef::Bytes | TypeRef::BorrowedBytes => "const uint8_t*".into(),
186        TypeRef::Struct(s) => format!("{}*", c_abi_struct_name(s, module, prefix)),
187        TypeRef::Enum(e) => format!("{prefix}_{module}_{e}"),
188        TypeRef::Optional(inner) | TypeRef::List(inner) | TypeRef::Iterator(inner) => {
189            c_elem_type(inner, module, prefix)
190        }
191        TypeRef::Map(_, _) => "void*".into(),
192    }
193}
194
195fn c_ret_type_str(ty: &TypeRef, module: &str, prefix: &str) -> String {
196    match ty {
197        TypeRef::I32 => "int32_t".into(),
198        TypeRef::U32 => "uint32_t".into(),
199        TypeRef::I64 => "int64_t".into(),
200        TypeRef::F64 => "double".into(),
201        TypeRef::Bool => "bool".into(),
202        TypeRef::StringUtf8 | TypeRef::BorrowedStr => "const char*".into(),
203        TypeRef::Bytes | TypeRef::BorrowedBytes => "const uint8_t*".into(),
204        TypeRef::Handle => "weaveffi_handle_t".into(),
205        TypeRef::TypedHandle(s) => format!("{}*", c_abi_struct_name(s, module, prefix)),
206        TypeRef::Struct(s) => format!("{}*", c_abi_struct_name(s, module, prefix)),
207        TypeRef::Enum(e) => format!("{prefix}_{module}_{e}"),
208        TypeRef::Optional(inner) => {
209            if is_c_ptr_type(inner) {
210                c_ret_type_str(inner, module, prefix)
211            } else {
212                format!("{}*", c_elem_type(inner, module, prefix))
213            }
214        }
215        TypeRef::List(inner) => format!("{}*", c_elem_type(inner, module, prefix)),
216        TypeRef::Map(_, _) => "void".into(),
217        TypeRef::Iterator(_) => "void*".into(),
218    }
219}
220
221fn napi_getter(ty: &TypeRef) -> &'static str {
222    match ty {
223        TypeRef::I32 | TypeRef::Enum(_) => "napi_get_value_int32",
224        TypeRef::U32 => "napi_get_value_uint32",
225        TypeRef::I64 | TypeRef::Handle | TypeRef::TypedHandle(_) | TypeRef::Struct(_) => {
226            "napi_get_value_int64"
227        }
228        TypeRef::F64 => "napi_get_value_double",
229        TypeRef::Bool => "napi_get_value_bool",
230        _ => "napi_get_value_int64",
231    }
232}
233
234fn render_addon_c(
235    api: &Api,
236    prefix: &str,
237    strip_module_prefix: bool,
238    input_basename: &str,
239) -> String {
240    let mut out = render_prelude(CommentStyle::DoubleSlash, input_basename);
241    out.push_str(&format!(
242        "#include <node_api.h>\n#include \"{prefix}.h\"\n#include <stdlib.h>\n#include <string.h>\n\n"
243    ));
244
245    let model = BindingModel::build(api, prefix);
246    let mut all_exports: Vec<(String, String)> = Vec::new();
247    let structs = struct_registry(&model);
248
249    let has_listeners = model.modules.iter().any(|m| !m.listeners.is_empty());
250    if has_listeners {
251        render_listener_support_c(&mut out, prefix);
252    }
253
254    for m in &model.modules {
255        // Callbacks referenced by listeners get a payload struct, a producer-
256        // thread trampoline, and a JS-thread marshaller (threadsafe function).
257        let used_callbacks: Vec<&CallbackBinding> = m
258            .listeners
259            .iter()
260            .filter_map(|l| m.callback(&l.event_callback))
261            .collect();
262        for cb in &used_callbacks {
263            render_cb_payload_struct(&mut out, cb, prefix);
264            render_cb_tramp(&mut out, cb, prefix);
265            render_cb_calljs(&mut out, cb, prefix);
266        }
267        for l in &m.listeners {
268            let Some(cb) = m.callback(&l.event_callback) else {
269                unreachable!("validation guarantees the listener's callback exists");
270            };
271            render_listener_napi_fns(&mut out, l, cb, prefix);
272            all_exports.push((
273                wrapper_name(
274                    &m.path,
275                    &format!("register_{}", l.name),
276                    strip_module_prefix,
277                ),
278                format!("Napi_{}", l.register_symbol),
279            ));
280            all_exports.push((
281                wrapper_name(
282                    &m.path,
283                    &format!("unregister_{}", l.name),
284                    strip_module_prefix,
285                ),
286                format!("Napi_{}", l.unregister_symbol),
287            ));
288        }
289        for f in &m.functions {
290            let c_name = &f.c_base;
291            let napi_name = format!("Napi_{c_name}");
292            let js_name = wrapper_name(&m.path, &f.name, strip_module_prefix);
293            all_exports.push((js_name, napi_name.clone()));
294
295            if f.is_async {
296                render_async_machinery(&mut out, f, c_name, &m.path, prefix, &structs);
297            }
298
299            out.push_str(&format!(
300                "static napi_value {napi_name}(napi_env env, napi_callback_info info) {{\n"
301            ));
302            if f.is_async {
303                render_async_napi_body(&mut out, f, c_name, &m.path, prefix);
304            } else {
305                render_napi_body(&mut out, f, c_name, &m.path, prefix, &structs);
306            }
307            out.push_str("}\n\n");
308        }
309    }
310
311    out.push_str("static napi_value Init(napi_env env, napi_value exports) {\n");
312    if !all_exports.is_empty() {
313        out.push_str("  napi_property_descriptor props[] = {\n");
314        for (js_name, napi_fn) in &all_exports {
315            out.push_str(&format!(
316                "    {{ \"{js_name}\", NULL, {napi_fn}, NULL, NULL, NULL, napi_default, NULL }},\n"
317            ));
318        }
319        out.push_str("  };\n");
320        out.push_str(&format!(
321            "  napi_define_properties(env, exports, {}, props);\n",
322            all_exports.len()
323        ));
324    }
325    out.push_str("  return exports;\n");
326    out.push_str("}\n\n");
327    out.push_str("NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)\n\n");
328    out.push_str(&render_trailer(
329        CommentStyle::DoubleSlash,
330        "weaveffi_addon.c",
331    ));
332    out
333}
334
335/// The listener context + registry shared by every generated listener. The
336/// registry is only mutated from the JS thread (register/unregister are plain
337/// N-API calls), so a simple singly-linked list suffices.
338fn render_listener_support_c(out: &mut String, prefix: &str) {
339    out.push_str(&format!("typedef struct {prefix}_napi_listener_ctx {{\n"));
340    out.push_str("    napi_threadsafe_function tsfn;\n");
341    out.push_str("    uint64_t id;\n");
342    out.push_str(&format!("    struct {prefix}_napi_listener_ctx* next;\n"));
343    out.push_str(&format!("}} {prefix}_napi_listener_ctx;\n\n"));
344    out.push_str(&format!(
345        "static {prefix}_napi_listener_ctx* {prefix}_napi_listeners = NULL;\n\n"
346    ));
347}
348
349fn cb_payload_name(cb: &CallbackBinding) -> String {
350    format!("{}_payload", cb.c_fn_type)
351}
352
353/// The C slot declarations of a callback's parameters (without context).
354fn cb_slot_decls(cb: &CallbackBinding, prefix: &str) -> Vec<String> {
355    cb.params
356        .iter()
357        .flat_map(|p| abi::lower_param(&p.name, &p.ty, "", false))
358        .map(|slot| format!("{} {}", slot.ty.render_c(prefix), slot.name))
359        .collect()
360}
361
362/// The deep-copy payload carried from the producer thread to the JS thread.
363/// Every pointer field is owned by the payload (strdup/memcpy in the
364/// trampoline, freed in the call-js marshaller); struct/handle pointers are
365/// shallow-copied and surface as numeric handles.
366fn render_cb_payload_struct(out: &mut String, cb: &CallbackBinding, prefix: &str) {
367    out.push_str("typedef struct {\n");
368    for p in &cb.params {
369        let slots = abi::lower_param(&p.name, &p.ty, "", false);
370        let n0 = &slots[0].name;
371        match &p.ty {
372            TypeRef::I32
373            | TypeRef::U32
374            | TypeRef::I64
375            | TypeRef::F64
376            | TypeRef::Bool
377            | TypeRef::Handle
378            | TypeRef::Enum(_) => {
379                out.push_str(&format!("    {} {n0};\n", slots[0].ty.render_c(prefix)));
380            }
381            TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
382                out.push_str(&format!("    char* {n0};\n"));
383            }
384            TypeRef::Bytes | TypeRef::BorrowedBytes => {
385                out.push_str(&format!("    uint8_t* {n0};\n"));
386                out.push_str(&format!("    size_t {};\n", slots[1].name));
387            }
388            TypeRef::Struct(_) | TypeRef::TypedHandle(_) => {
389                out.push_str(&format!("    void* {n0};\n"));
390            }
391            TypeRef::Optional(inner) => match inner.as_ref() {
392                TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
393                    out.push_str(&format!("    char* {n0};\n"));
394                }
395                TypeRef::Bytes | TypeRef::BorrowedBytes => {
396                    out.push_str(&format!("    int {n0}_has;\n"));
397                    out.push_str(&format!("    uint8_t* {n0};\n"));
398                    out.push_str(&format!("    size_t {};\n", slots[1].name));
399                }
400                TypeRef::Struct(_) | TypeRef::TypedHandle(_) => {
401                    out.push_str(&format!("    void* {n0};\n"));
402                }
403                other => {
404                    out.push_str(&format!("    int {n0}_has;\n"));
405                    out.push_str(&format!(
406                        "    {} {n0};\n",
407                        abi::element_ctype(other, "").render_c(prefix)
408                    ));
409                }
410            },
411            TypeRef::List(inner) => {
412                let elem = elem_payload_ctype(inner, prefix);
413                out.push_str(&format!("    {elem}* {n0};\n"));
414                out.push_str(&format!("    size_t {};\n", slots[1].name));
415            }
416            TypeRef::Map(k, v) => {
417                let kt = elem_payload_ctype(k, prefix);
418                let vt = elem_payload_ctype(v, prefix);
419                out.push_str(&format!("    {kt}* {n0};\n"));
420                out.push_str(&format!("    {vt}* {};\n", slots[1].name));
421                out.push_str(&format!("    size_t {};\n", slots[2].name));
422            }
423            TypeRef::Iterator(_) => unreachable!("validated: iterator not a callback param"),
424        }
425    }
426    out.push_str(&format!("}} {};\n\n", cb_payload_name(cb)));
427}
428
429/// The payload element type for list/map callback parameters. Strings own
430/// their copies (`char*`); scalar elements keep their C ABI type.
431fn elem_payload_ctype(ty: &TypeRef, prefix: &str) -> String {
432    match ty {
433        TypeRef::StringUtf8 | TypeRef::BorrowedStr => "char*".into(),
434        other => abi::element_ctype(other, "").render_c(prefix),
435    }
436}
437
438/// The producer-thread trampoline: deep-copies the C arguments into a payload
439/// and queues it onto the threadsafe function. Runs on whatever thread the
440/// producer fires the event from; never touches `napi_env`.
441fn render_cb_tramp(out: &mut String, cb: &CallbackBinding, prefix: &str) {
442    let payload = cb_payload_name(cb);
443    let mut decls = cb_slot_decls(cb, prefix);
444    decls.push("void* context".into());
445    out.push_str(&format!(
446        "static void {}_napi_tramp({}) {{\n",
447        cb.c_fn_type,
448        decls.join(", ")
449    ));
450    out.push_str(&format!(
451        "    {prefix}_napi_listener_ctx* ctx = ({prefix}_napi_listener_ctx*)context;\n"
452    ));
453    out.push_str(&format!(
454        "    {payload}* p = ({payload}*)calloc(1, sizeof({payload}));\n"
455    ));
456    for p in &cb.params {
457        let slots = abi::lower_param(&p.name, &p.ty, "", false);
458        let n0 = &slots[0].name;
459        match &p.ty {
460            TypeRef::I32
461            | TypeRef::U32
462            | TypeRef::I64
463            | TypeRef::F64
464            | TypeRef::Bool
465            | TypeRef::Handle
466            | TypeRef::Enum(_) => {
467                out.push_str(&format!("    p->{n0} = {n0};\n"));
468            }
469            TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
470                out.push_str(&format!("    p->{n0} = {n0} ? strdup({n0}) : NULL;\n"));
471            }
472            TypeRef::Bytes | TypeRef::BorrowedBytes => {
473                let n1 = &slots[1].name;
474                out.push_str(&format!("    p->{n1} = {n1};\n"));
475                out.push_str(&format!(
476                    "    if ({n0} != NULL && {n1} > 0) {{ p->{n0} = (uint8_t*)malloc({n1}); memcpy(p->{n0}, {n0}, {n1}); }}\n"
477                ));
478            }
479            TypeRef::Struct(_) | TypeRef::TypedHandle(_) => {
480                out.push_str(&format!("    p->{n0} = (void*){n0};\n"));
481            }
482            TypeRef::Optional(inner) => match inner.as_ref() {
483                TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
484                    out.push_str(&format!("    p->{n0} = {n0} ? strdup({n0}) : NULL;\n"));
485                }
486                TypeRef::Bytes | TypeRef::BorrowedBytes => {
487                    let n1 = &slots[1].name;
488                    out.push_str(&format!("    p->{n0}_has = {n0} != NULL;\n"));
489                    out.push_str(&format!("    p->{n1} = {n1};\n"));
490                    out.push_str(&format!(
491                        "    if ({n0} != NULL && {n1} > 0) {{ p->{n0} = (uint8_t*)malloc({n1}); memcpy(p->{n0}, {n0}, {n1}); }}\n"
492                    ));
493                }
494                TypeRef::Struct(_) | TypeRef::TypedHandle(_) => {
495                    out.push_str(&format!("    p->{n0} = (void*){n0};\n"));
496                }
497                _ => {
498                    out.push_str(&format!("    p->{n0}_has = {n0} != NULL;\n"));
499                    out.push_str(&format!("    if ({n0} != NULL) p->{n0} = *{n0};\n"));
500                }
501            },
502            TypeRef::List(inner) => {
503                let n1 = &slots[1].name;
504                out.push_str(&format!("    p->{n1} = {n1};\n"));
505                match inner.as_ref() {
506                    TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
507                        out.push_str(&format!(
508                            "    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"
509                        ));
510                    }
511                    _ => {
512                        out.push_str(&format!(
513                            "    if ({n0} != NULL && {n1} > 0) {{ p->{n0} = malloc({n1} * sizeof(*p->{n0})); memcpy(p->{n0}, {n0}, {n1} * sizeof(*p->{n0})); }}\n"
514                        ));
515                    }
516                }
517            }
518            TypeRef::Map(k, v) => {
519                let keys = n0;
520                let vals = &slots[1].name;
521                let len = &slots[2].name;
522                out.push_str(&format!("    p->{len} = {len};\n"));
523                for (base, ty) in [(keys, k), (vals, v)] {
524                    match ty.as_ref() {
525                        TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
526                            out.push_str(&format!(
527                                "    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"
528                            ));
529                        }
530                        _ => {
531                            out.push_str(&format!(
532                                "    if ({base} != NULL && {len} > 0) {{ p->{base} = malloc({len} * sizeof(*p->{base})); memcpy(p->{base}, {base}, {len} * sizeof(*p->{base})); }}\n"
533                            ));
534                        }
535                    }
536                }
537            }
538            TypeRef::Iterator(_) => unreachable!("validated: iterator not a callback param"),
539        }
540    }
541    out.push_str("    napi_call_threadsafe_function(ctx->tsfn, p, napi_tsfn_nonblocking);\n");
542    out.push_str("}\n\n");
543}
544
545/// One payload field rendered to a `napi_value` in `argv[idx]` (call-js side).
546fn emit_payload_to_napi(out: &mut String, p: &ParamBinding, idx: usize, prefix: &str) {
547    let slots = abi::lower_param(&p.name, &p.ty, "", false);
548    let n0 = &slots[0].name;
549    let target = format!("argv[{idx}]");
550    let _ = prefix;
551    match &p.ty {
552        TypeRef::I32 => out.push_str(&format!(
553            "        napi_create_int32(env, p->{n0}, &{target});\n"
554        )),
555        TypeRef::U32 => out.push_str(&format!(
556            "        napi_create_uint32(env, p->{n0}, &{target});\n"
557        )),
558        TypeRef::I64 => out.push_str(&format!(
559            "        napi_create_int64(env, p->{n0}, &{target});\n"
560        )),
561        TypeRef::F64 => out.push_str(&format!(
562            "        napi_create_double(env, p->{n0}, &{target});\n"
563        )),
564        TypeRef::Bool => out.push_str(&format!(
565            "        napi_get_boolean(env, p->{n0}, &{target});\n"
566        )),
567        TypeRef::Handle => out.push_str(&format!(
568            "        napi_create_int64(env, (int64_t)p->{n0}, &{target});\n"
569        )),
570        TypeRef::Enum(_) => out.push_str(&format!(
571            "        napi_create_int32(env, (int32_t)p->{n0}, &{target});\n"
572        )),
573        TypeRef::StringUtf8 | TypeRef::BorrowedStr => out.push_str(&format!(
574            "        napi_create_string_utf8(env, p->{n0} ? p->{n0} : \"\", NAPI_AUTO_LENGTH, &{target});\n"
575        )),
576        TypeRef::Bytes | TypeRef::BorrowedBytes => {
577            let n1 = &slots[1].name;
578            out.push_str(&format!(
579                "        napi_create_buffer_copy(env, p->{n1}, p->{n0} ? (const void*)p->{n0} : (const void*)\"\", NULL, &{target});\n"
580            ));
581        }
582        TypeRef::Struct(_) | TypeRef::TypedHandle(_) => out.push_str(&format!(
583            "        napi_create_int64(env, (int64_t)(intptr_t)p->{n0}, &{target});\n"
584        )),
585        TypeRef::Optional(inner) => match inner.as_ref() {
586            TypeRef::StringUtf8 | TypeRef::BorrowedStr => out.push_str(&format!(
587                "        if (p->{n0}) napi_create_string_utf8(env, p->{n0}, NAPI_AUTO_LENGTH, &{target}); else napi_get_null(env, &{target});\n"
588            )),
589            TypeRef::Bytes | TypeRef::BorrowedBytes => {
590                let n1 = &slots[1].name;
591                out.push_str(&format!(
592                    "        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"
593                ));
594            }
595            TypeRef::Struct(_) | TypeRef::TypedHandle(_) => out.push_str(&format!(
596                "        if (p->{n0}) napi_create_int64(env, (int64_t)(intptr_t)p->{n0}, &{target}); else napi_get_null(env, &{target});\n"
597            )),
598            other => {
599                let leaf = payload_leaf_to_napi(other, &format!("p->{n0}"), &target);
600                out.push_str(&format!(
601                    "        if (p->{n0}_has) {{ {leaf} }} else napi_get_null(env, &{target});\n"
602                ));
603            }
604        },
605        TypeRef::List(inner) => {
606            let n1 = &slots[1].name;
607            out.push_str(&format!("        napi_create_array(env, &{target});\n"));
608            out.push_str(&format!(
609                "        for (size_t i = 0; p->{n0} != NULL && i < p->{n1}; i++) {{\n"
610            ));
611            out.push_str("            napi_value elem;\n");
612            let leaf = payload_elem_to_napi(inner, &format!("p->{n0}[i]"), "elem");
613            out.push_str(&format!("            {leaf}\n"));
614            out.push_str(&format!(
615                "            napi_set_element(env, {target}, (uint32_t)i, elem);\n"
616            ));
617            out.push_str("        }\n");
618        }
619        TypeRef::Map(k, v) => {
620            let keys = n0;
621            let vals = &slots[1].name;
622            let len = &slots[2].name;
623            out.push_str(&format!("        napi_create_object(env, &{target});\n"));
624            out.push_str(&format!(
625                "        for (size_t i = 0; p->{keys} != NULL && p->{vals} != NULL && i < p->{len}; i++) {{\n"
626            ));
627            out.push_str("            napi_value mk; napi_value mv;\n");
628            let kc = payload_elem_to_napi(k, &format!("p->{keys}[i]"), "mk");
629            let vc = payload_elem_to_napi(v, &format!("p->{vals}[i]"), "mv");
630            out.push_str(&format!("            {kc}\n"));
631            out.push_str(&format!("            {vc}\n"));
632            out.push_str(&format!(
633                "            napi_set_property(env, {target}, mk, mv);\n"
634            ));
635            out.push_str("        }\n");
636        }
637        TypeRef::Iterator(_) => unreachable!("validated: iterator not a callback param"),
638    }
639}
640
641/// One scalar-ish payload value to a napi_value (single statement).
642fn payload_leaf_to_napi(ty: &TypeRef, expr: &str, target: &str) -> String {
643    match ty {
644        TypeRef::I32 => format!("napi_create_int32(env, {expr}, &{target});"),
645        TypeRef::U32 => format!("napi_create_uint32(env, {expr}, &{target});"),
646        TypeRef::I64 => format!("napi_create_int64(env, {expr}, &{target});"),
647        TypeRef::F64 => format!("napi_create_double(env, {expr}, &{target});"),
648        TypeRef::Bool => format!("napi_get_boolean(env, {expr}, &{target});"),
649        TypeRef::Handle => format!("napi_create_int64(env, (int64_t){expr}, &{target});"),
650        TypeRef::Enum(_) => format!("napi_create_int32(env, (int32_t){expr}, &{target});"),
651        _ => format!("napi_get_null(env, &{target});"),
652    }
653}
654
655/// One list/map element payload value to a napi_value (single statement).
656fn payload_elem_to_napi(ty: &TypeRef, expr: &str, target: &str) -> String {
657    match ty {
658        TypeRef::StringUtf8 | TypeRef::BorrowedStr => format!(
659            "napi_create_string_utf8(env, {expr} ? {expr} : \"\", NAPI_AUTO_LENGTH, &{target});"
660        ),
661        other => payload_leaf_to_napi(other, expr, target),
662    }
663}
664
665/// Frees one payload field after the JS call.
666fn emit_payload_free(out: &mut String, p: &ParamBinding) {
667    let slots = abi::lower_param(&p.name, &p.ty, "", false);
668    let n0 = &slots[0].name;
669    match &p.ty {
670        TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
671            out.push_str(&format!("    free(p->{n0});\n"));
672        }
673        TypeRef::Bytes | TypeRef::BorrowedBytes => {
674            out.push_str(&format!("    free(p->{n0});\n"));
675        }
676        TypeRef::Optional(inner) => match inner.as_ref() {
677            TypeRef::StringUtf8
678            | TypeRef::BorrowedStr
679            | TypeRef::Bytes
680            | TypeRef::BorrowedBytes => {
681                out.push_str(&format!("    free(p->{n0});\n"));
682            }
683            _ => {}
684        },
685        TypeRef::List(inner) => {
686            let n1 = &slots[1].name;
687            if matches!(inner.as_ref(), TypeRef::StringUtf8 | TypeRef::BorrowedStr) {
688                out.push_str(&format!(
689                    "    for (size_t i = 0; p->{n0} != NULL && i < p->{n1}; i++) free(p->{n0}[i]);\n"
690                ));
691            }
692            out.push_str(&format!("    free(p->{n0});\n"));
693        }
694        TypeRef::Map(k, v) => {
695            let keys = n0;
696            let vals = &slots[1].name;
697            let len = &slots[2].name;
698            for (base, ty) in [(keys, k), (vals, v)] {
699                if matches!(ty.as_ref(), TypeRef::StringUtf8 | TypeRef::BorrowedStr) {
700                    out.push_str(&format!(
701                        "    for (size_t i = 0; p->{base} != NULL && i < p->{len}; i++) free(p->{base}[i]);\n"
702                    ));
703                }
704                out.push_str(&format!("    free(p->{base});\n"));
705            }
706        }
707        _ => {}
708    }
709}
710
711/// The JS-thread marshaller invoked by the threadsafe function: converts the
712/// payload into JS arguments, calls the user callback, and frees the payload.
713fn render_cb_calljs(out: &mut String, cb: &CallbackBinding, prefix: &str) {
714    let payload = cb_payload_name(cb);
715    out.push_str(&format!(
716        "static void {}_napi_calljs(napi_env env, napi_value js_cb, void* context, void* data) {{\n",
717        cb.c_fn_type
718    ));
719    out.push_str("    (void)context;\n");
720    out.push_str(&format!("    {payload}* p = ({payload}*)data;\n"));
721    out.push_str("    if (env != NULL) {\n");
722    out.push_str("        napi_value undefined;\n");
723    out.push_str("        napi_get_undefined(env, &undefined);\n");
724    let argc = cb.params.len();
725    if argc > 0 {
726        out.push_str(&format!("        napi_value argv[{argc}];\n"));
727        for (i, p) in cb.params.iter().enumerate() {
728            emit_payload_to_napi(out, p, i, prefix);
729        }
730        out.push_str(&format!(
731            "        napi_call_function(env, undefined, js_cb, {argc}, argv, NULL);\n"
732        ));
733    } else {
734        out.push_str("        napi_call_function(env, undefined, js_cb, 0, NULL, NULL);\n");
735    }
736    out.push_str("    }\n");
737    for p in &cb.params {
738        emit_payload_free(out, p);
739    }
740    out.push_str("    free(p);\n");
741    out.push_str("}\n\n");
742}
743
744/// The `Napi_*` register/unregister entry points for one listener. Register
745/// wraps the JS callback in an unref'd threadsafe function (so live listeners
746/// don't pin the event loop) and stores it in the registry; unregister stops
747/// the producer first, then releases the threadsafe function.
748fn render_listener_napi_fns(
749    out: &mut String,
750    l: &ListenerBinding,
751    cb: &CallbackBinding,
752    prefix: &str,
753) {
754    let register_sym = &l.register_symbol;
755    let unregister_sym = &l.unregister_symbol;
756    let tramp = format!("{}_napi_tramp", cb.c_fn_type);
757    let calljs = format!("{}_napi_calljs", cb.c_fn_type);
758
759    out.push_str(&format!(
760        "static napi_value Napi_{register_sym}(napi_env env, napi_callback_info info) {{\n"
761    ));
762    out.push_str("  size_t argc = 1;\n");
763    out.push_str("  napi_value args[1];\n");
764    out.push_str("  napi_get_cb_info(env, info, &argc, args, NULL, NULL);\n");
765    out.push_str(&format!(
766        "  {prefix}_napi_listener_ctx* ctx = ({prefix}_napi_listener_ctx*)calloc(1, sizeof({prefix}_napi_listener_ctx));\n"
767    ));
768    out.push_str("  napi_value resource_name;\n");
769    out.push_str(&format!(
770        "  napi_create_string_utf8(env, \"{register_sym}\", NAPI_AUTO_LENGTH, &resource_name);\n"
771    ));
772    out.push_str(&format!(
773        "  napi_create_threadsafe_function(env, args[0], NULL, resource_name, 0, 1, NULL, NULL, NULL, {calljs}, &ctx->tsfn);\n"
774    ));
775    out.push_str("  napi_unref_threadsafe_function(env, ctx->tsfn);\n");
776    out.push_str(&format!("  uint64_t id = {register_sym}({tramp}, ctx);\n"));
777    out.push_str("  ctx->id = id;\n");
778    out.push_str(&format!("  ctx->next = {prefix}_napi_listeners;\n"));
779    out.push_str(&format!("  {prefix}_napi_listeners = ctx;\n"));
780    out.push_str("  napi_value ret;\n");
781    out.push_str("  napi_create_double(env, (double)id, &ret);\n");
782    out.push_str("  return ret;\n");
783    out.push_str("}\n\n");
784
785    out.push_str(&format!(
786        "static napi_value Napi_{unregister_sym}(napi_env env, napi_callback_info info) {{\n"
787    ));
788    out.push_str("  size_t argc = 1;\n");
789    out.push_str("  napi_value args[1];\n");
790    out.push_str("  napi_get_cb_info(env, info, &argc, args, NULL, NULL);\n");
791    out.push_str("  double id_d = 0;\n");
792    out.push_str("  napi_get_value_double(env, args[0], &id_d);\n");
793    out.push_str("  uint64_t id = (uint64_t)id_d;\n");
794    // Stop producer-side delivery before tearing down the tsfn so no new
795    // payloads are queued against a released function.
796    out.push_str(&format!("  {unregister_sym}(id);\n"));
797    out.push_str(&format!(
798        "  {prefix}_napi_listener_ctx** link = &{prefix}_napi_listeners;\n"
799    ));
800    out.push_str("  while (*link != NULL) {\n");
801    out.push_str("    if ((*link)->id == id) {\n");
802    out.push_str(&format!(
803        "      {prefix}_napi_listener_ctx* found = *link;\n"
804    ));
805    out.push_str("      *link = found->next;\n");
806    out.push_str("      napi_release_threadsafe_function(found->tsfn, napi_tsfn_release);\n");
807    out.push_str("      free(found);\n");
808    out.push_str("      break;\n");
809    out.push_str("    }\n");
810    out.push_str("    link = &(*link)->next;\n");
811    out.push_str("  }\n");
812    out.push_str("  napi_value ret;\n");
813    out.push_str("  napi_get_undefined(env, &ret);\n");
814    out.push_str("  return ret;\n");
815    out.push_str("}\n\n");
816}
817
818fn async_cb_result_params_node(ret: Option<&TypeRef>, module: &str, prefix: &str) -> String {
819    match ret {
820        None => String::new(),
821        Some(TypeRef::StringUtf8 | TypeRef::BorrowedStr) => ", const char* result".into(),
822        Some(TypeRef::Bytes | TypeRef::BorrowedBytes) => {
823            ", const uint8_t* result, size_t result_len".into()
824        }
825        Some(TypeRef::List(inner)) => {
826            let et = c_elem_type(inner, module, prefix);
827            format!(", {et}* result, size_t result_len")
828        }
829        Some(TypeRef::Map(k, v)) => {
830            let kt = c_elem_type(k, module, prefix);
831            let vt = c_elem_type(v, module, prefix);
832            format!(", {kt}* result_keys, {vt}* result_values, size_t result_len")
833        }
834        Some(t) => format!(", {} result", c_ret_type_str(t, module, prefix)),
835    }
836}
837
838/// Emit the per-async-function machinery: a context struct carrying the
839/// promise + threadsafe function + deep-copied results, the producer-thread
840/// completion callback (which only copies and queues), and the JS-thread
841/// marshaller (which settles the promise).
842///
843/// The completion callback may fire on any thread, so it must never touch
844/// `napi_env`; the ref'd threadsafe function also keeps the event loop alive
845/// until the promise settles.
846fn render_async_machinery(
847    out: &mut String,
848    f: &FnBinding,
849    c_name: &str,
850    module: &str,
851    prefix: &str,
852    structs: &HashMap<String, StructBinding>,
853) {
854    let actx = format!("{c_name}_napi_actx");
855    let cb_name = format!("{c_name}_napi_cb");
856    let calljs = format!("{c_name}_napi_settle");
857    let cb_result = async_cb_result_params_node(f.ret.as_ref(), module, prefix);
858
859    // -- context struct --
860    out.push_str("typedef struct {\n");
861    out.push_str("    napi_deferred deferred;\n");
862    out.push_str("    napi_threadsafe_function tsfn;\n");
863    out.push_str("    int32_t err_code;\n");
864    out.push_str("    char* err_msg;\n");
865    match f.ret.as_ref() {
866        None => {}
867        Some(TypeRef::I32) => out.push_str("    int32_t result;\n"),
868        Some(TypeRef::U32) => out.push_str("    uint32_t result;\n"),
869        Some(TypeRef::I64) => out.push_str("    int64_t result;\n"),
870        Some(TypeRef::F64) => out.push_str("    double result;\n"),
871        Some(TypeRef::Bool) => out.push_str("    bool result;\n"),
872        Some(TypeRef::Enum(_)) => out.push_str("    int32_t result;\n"),
873        Some(TypeRef::StringUtf8 | TypeRef::BorrowedStr) => {
874            out.push_str("    char* result;\n");
875            out.push_str("    int result_null;\n");
876        }
877        Some(TypeRef::Bytes | TypeRef::BorrowedBytes) => {
878            out.push_str("    uint8_t* result;\n");
879            out.push_str("    size_t result_len;\n");
880        }
881        Some(TypeRef::Handle) => out.push_str("    uint64_t result;\n"),
882        Some(TypeRef::TypedHandle(_) | TypeRef::Struct(_) | TypeRef::Iterator(_)) => {
883            out.push_str("    void* result;\n")
884        }
885        Some(TypeRef::Optional(inner)) => match inner.as_ref() {
886            TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
887                out.push_str("    char* result;\n");
888                out.push_str("    int result_null;\n");
889            }
890            TypeRef::Struct(_) | TypeRef::TypedHandle(_) => {
891                out.push_str("    void* result;\n");
892            }
893            other => {
894                out.push_str("    int result_has;\n");
895                out.push_str(&format!(
896                    "    {} result;\n",
897                    c_elem_type(other, module, prefix)
898                ));
899            }
900        },
901        Some(TypeRef::List(inner)) => {
902            let elem = match inner.as_ref() {
903                TypeRef::StringUtf8 | TypeRef::BorrowedStr => "char*".to_string(),
904                other => c_elem_type(other, module, prefix),
905            };
906            out.push_str(&format!("    {elem}* result;\n"));
907            out.push_str("    size_t result_len;\n");
908        }
909        Some(TypeRef::Map(k, v)) => {
910            for (field, ty) in [("result_keys", k), ("result_values", v)] {
911                let elem = match ty.as_ref() {
912                    TypeRef::StringUtf8 | TypeRef::BorrowedStr => "char*".to_string(),
913                    other => c_elem_type(other, module, prefix),
914                };
915                out.push_str(&format!("    {elem}* {field};\n"));
916            }
917            out.push_str("    size_t result_len;\n");
918        }
919    }
920    out.push_str(&format!("}} {actx};\n\n"));
921
922    // -- producer-thread completion callback: deep-copy + queue --
923    out.push_str(&format!(
924        "static void {cb_name}(void* context, weaveffi_error* err{cb_result}) {{\n"
925    ));
926    out.push_str(&format!("    {actx}* ctx = ({actx}*)context;\n"));
927    out.push_str("    if (err != NULL && err->code != 0) {\n");
928    out.push_str("        ctx->err_code = err->code;\n");
929    out.push_str(
930        "        ctx->err_msg = err->message ? strdup(err->message) : strdup(\"unknown error\");\n",
931    );
932    out.push_str("    } else {\n");
933    match f.ret.as_ref() {
934        None => {}
935        Some(
936            TypeRef::I32
937            | TypeRef::U32
938            | TypeRef::I64
939            | TypeRef::F64
940            | TypeRef::Bool
941            | TypeRef::Handle,
942        ) => {
943            out.push_str("        ctx->result = result;\n");
944        }
945        Some(TypeRef::Enum(_)) => {
946            out.push_str("        ctx->result = (int32_t)result;\n");
947        }
948        Some(TypeRef::StringUtf8 | TypeRef::BorrowedStr) => {
949            out.push_str("        ctx->result_null = result == NULL;\n");
950            out.push_str("        ctx->result = result ? strdup(result) : NULL;\n");
951        }
952        Some(TypeRef::Bytes | TypeRef::BorrowedBytes) => {
953            out.push_str("        ctx->result_len = result_len;\n");
954            out.push_str(
955                "        if (result != NULL && result_len > 0) { ctx->result = (uint8_t*)malloc(result_len); memcpy(ctx->result, result, result_len); }\n",
956            );
957        }
958        // Ownership of struct/handle/iterator results transfers to the
959        // receiver, so the pointer stays valid across the thread hop.
960        Some(TypeRef::TypedHandle(_) | TypeRef::Struct(_) | TypeRef::Iterator(_)) => {
961            out.push_str("        ctx->result = (void*)result;\n");
962        }
963        Some(TypeRef::Optional(inner)) => match inner.as_ref() {
964            TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
965                out.push_str("        ctx->result_null = result == NULL;\n");
966                out.push_str("        ctx->result = result ? strdup(result) : NULL;\n");
967            }
968            TypeRef::Struct(_) | TypeRef::TypedHandle(_) => {
969                out.push_str("        ctx->result = (void*)result;\n");
970            }
971            _ => {
972                out.push_str("        ctx->result_has = result != NULL;\n");
973                out.push_str("        if (result != NULL) ctx->result = *result;\n");
974            }
975        },
976        Some(TypeRef::List(inner)) => {
977            out.push_str("        ctx->result_len = result_len;\n");
978            match inner.as_ref() {
979                TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
980                    out.push_str(
981                        "        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",
982                    );
983                }
984                _ => {
985                    out.push_str(
986                        "        if (result != NULL && result_len > 0) { ctx->result = malloc(result_len * sizeof(*ctx->result)); memcpy(ctx->result, result, result_len * sizeof(*ctx->result)); }\n",
987                    );
988                }
989            }
990        }
991        Some(TypeRef::Map(k, v)) => {
992            out.push_str("        ctx->result_len = result_len;\n");
993            for (field, src, ty) in [
994                ("result_keys", "result_keys", k),
995                ("result_values", "result_values", v),
996            ] {
997                match ty.as_ref() {
998                    TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
999                        out.push_str(&format!(
1000                            "        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"
1001                        ));
1002                    }
1003                    _ => {
1004                        out.push_str(&format!(
1005                            "        if ({src} != NULL && result_len > 0) {{ ctx->{field} = malloc(result_len * sizeof(*ctx->{field})); memcpy(ctx->{field}, {src}, result_len * sizeof(*ctx->{field})); }}\n"
1006                        ));
1007                    }
1008                }
1009            }
1010        }
1011    }
1012    out.push_str("    }\n");
1013    out.push_str("    napi_call_threadsafe_function(ctx->tsfn, ctx, napi_tsfn_blocking);\n");
1014    out.push_str("}\n\n");
1015
1016    // -- JS-thread marshaller: settle the promise, free, release --
1017    out.push_str(&format!(
1018        "static void {calljs}(napi_env env, napi_value js_cb, void* context, void* data) {{\n"
1019    ));
1020    out.push_str("    (void)js_cb;\n");
1021    out.push_str("    (void)context;\n");
1022    out.push_str(&format!("    {actx}* ctx = ({actx}*)data;\n"));
1023    out.push_str("    if (env != NULL) {\n");
1024    out.push_str("    if (ctx->err_code != 0) {\n");
1025    out.push_str("        napi_value err_msg;\n");
1026    out.push_str(
1027        "        napi_create_string_utf8(env, ctx->err_msg ? ctx->err_msg : \"\", NAPI_AUTO_LENGTH, &err_msg);\n",
1028    );
1029    out.push_str("        napi_value err_obj;\n");
1030    out.push_str("        napi_create_error(env, NULL, err_msg, &err_obj);\n");
1031    out.push_str("        napi_value err_code;\n");
1032    out.push_str("        napi_create_int32(env, ctx->err_code, &err_code);\n");
1033    out.push_str("        napi_set_named_property(env, err_obj, \"code\", err_code);\n");
1034    out.push_str("        napi_reject_deferred(env, ctx->deferred, err_obj);\n");
1035    out.push_str("    } else {\n");
1036    out.push_str("        napi_value val;\n");
1037    match f.ret.as_ref() {
1038        None => out.push_str("        napi_get_undefined(env, &val);\n"),
1039        Some(TypeRef::I32) => out.push_str("        napi_create_int32(env, ctx->result, &val);\n"),
1040        Some(TypeRef::U32) => out.push_str("        napi_create_uint32(env, ctx->result, &val);\n"),
1041        Some(TypeRef::I64) => out.push_str("        napi_create_int64(env, ctx->result, &val);\n"),
1042        Some(TypeRef::F64) => out.push_str("        napi_create_double(env, ctx->result, &val);\n"),
1043        Some(TypeRef::Bool) => out.push_str("        napi_get_boolean(env, ctx->result, &val);\n"),
1044        Some(TypeRef::Enum(_)) => {
1045            out.push_str("        napi_create_int32(env, ctx->result, &val);\n");
1046        }
1047        Some(TypeRef::StringUtf8 | TypeRef::BorrowedStr) => {
1048            out.push_str(
1049                "        if (ctx->result_null) napi_get_null(env, &val); else napi_create_string_utf8(env, ctx->result ? ctx->result : \"\", NAPI_AUTO_LENGTH, &val);\n",
1050            );
1051        }
1052        Some(TypeRef::Bytes | TypeRef::BorrowedBytes) => {
1053            out.push_str(
1054                "        napi_create_buffer_copy(env, ctx->result_len, ctx->result ? (const void*)ctx->result : (const void*)\"\", NULL, &val);\n",
1055            );
1056        }
1057        Some(TypeRef::Handle) => {
1058            out.push_str("        napi_create_int64(env, (int64_t)ctx->result, &val);\n");
1059        }
1060        Some(TypeRef::TypedHandle(_) | TypeRef::Iterator(_)) => {
1061            out.push_str("        napi_create_int64(env, (int64_t)(intptr_t)ctx->result, &val);\n");
1062        }
1063        Some(TypeRef::Struct(name)) => {
1064            emit_struct_to_object(
1065                out,
1066                "env",
1067                name,
1068                "ctx->result",
1069                "val",
1070                module,
1071                prefix,
1072                structs,
1073                "        ",
1074                true,
1075            );
1076        }
1077        Some(TypeRef::Optional(inner)) => match inner.as_ref() {
1078            TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
1079                out.push_str(
1080                    "        if (ctx->result_null) napi_get_null(env, &val); else napi_create_string_utf8(env, ctx->result ? ctx->result : \"\", NAPI_AUTO_LENGTH, &val);\n",
1081                );
1082            }
1083            TypeRef::Struct(name) => {
1084                out.push_str(
1085                    "        if (ctx->result == NULL) { napi_get_null(env, &val); } else {\n",
1086                );
1087                emit_struct_to_object(
1088                    out,
1089                    "env",
1090                    name,
1091                    "ctx->result",
1092                    "val",
1093                    module,
1094                    prefix,
1095                    structs,
1096                    "            ",
1097                    true,
1098                );
1099                out.push_str("        }\n");
1100            }
1101            TypeRef::TypedHandle(_) => {
1102                out.push_str(
1103                    "        if (ctx->result == NULL) napi_get_null(env, &val); else napi_create_int64(env, (int64_t)(intptr_t)ctx->result, &val);\n",
1104                );
1105            }
1106            other => {
1107                let leaf = payload_leaf_to_napi(other, "ctx->result", "val");
1108                out.push_str(&format!(
1109                    "        if (ctx->result_has) {{ {leaf} }} else napi_get_null(env, &val);\n"
1110                ));
1111            }
1112        },
1113        Some(TypeRef::List(inner)) => {
1114            out.push_str("        napi_create_array(env, &val);\n");
1115            out.push_str(
1116                "        for (size_t i = 0; ctx->result != NULL && i < ctx->result_len; i++) {\n",
1117            );
1118            out.push_str("            napi_value elem;\n");
1119            let leaf = payload_elem_to_napi(inner, "ctx->result[i]", "elem");
1120            out.push_str(&format!("            {leaf}\n"));
1121            out.push_str("            napi_set_element(env, val, (uint32_t)i, elem);\n");
1122            out.push_str("        }\n");
1123        }
1124        Some(TypeRef::Map(k, v)) => {
1125            out.push_str("        napi_create_object(env, &val);\n");
1126            out.push_str(
1127                "        for (size_t i = 0; ctx->result_keys != NULL && ctx->result_values != NULL && i < ctx->result_len; i++) {\n",
1128            );
1129            out.push_str("            napi_value mk; napi_value mv;\n");
1130            let kc = payload_elem_to_napi(k, "ctx->result_keys[i]", "mk");
1131            let vc = payload_elem_to_napi(v, "ctx->result_values[i]", "mv");
1132            out.push_str(&format!("            {kc}\n"));
1133            out.push_str(&format!("            {vc}\n"));
1134            out.push_str("            napi_set_property(env, val, mk, mv);\n");
1135            out.push_str("        }\n");
1136        }
1137    }
1138    out.push_str("        napi_resolve_deferred(env, ctx->deferred, val);\n");
1139    out.push_str("    }\n");
1140    out.push_str("    }\n");
1141    out.push_str("    free(ctx->err_msg);\n");
1142    match f.ret.as_ref() {
1143        Some(
1144            TypeRef::StringUtf8 | TypeRef::BorrowedStr | TypeRef::Bytes | TypeRef::BorrowedBytes,
1145        ) => {
1146            out.push_str("    free(ctx->result);\n");
1147        }
1148        Some(TypeRef::Optional(inner))
1149            if matches!(inner.as_ref(), TypeRef::StringUtf8 | TypeRef::BorrowedStr) =>
1150        {
1151            out.push_str("    free(ctx->result);\n");
1152        }
1153        Some(TypeRef::List(inner)) => {
1154            if matches!(inner.as_ref(), TypeRef::StringUtf8 | TypeRef::BorrowedStr) {
1155                out.push_str(
1156                    "    for (size_t i = 0; ctx->result != NULL && i < ctx->result_len; i++) free(ctx->result[i]);\n",
1157                );
1158            }
1159            out.push_str("    free(ctx->result);\n");
1160        }
1161        Some(TypeRef::Map(k, v)) => {
1162            for (field, ty) in [("result_keys", k), ("result_values", v)] {
1163                if matches!(ty.as_ref(), TypeRef::StringUtf8 | TypeRef::BorrowedStr) {
1164                    out.push_str(&format!(
1165                        "    for (size_t i = 0; ctx->{field} != NULL && i < ctx->result_len; i++) free(ctx->{field}[i]);\n"
1166                    ));
1167                }
1168                out.push_str(&format!("    free(ctx->{field});\n"));
1169            }
1170        }
1171        _ => {}
1172    }
1173    out.push_str("    napi_release_threadsafe_function(ctx->tsfn, napi_tsfn_release);\n");
1174    out.push_str("    free(ctx);\n");
1175    out.push_str("}\n\n");
1176}
1177
1178fn render_async_napi_body(
1179    out: &mut String,
1180    f: &FnBinding,
1181    c_name: &str,
1182    module: &str,
1183    prefix: &str,
1184) {
1185    let n = f.params.len();
1186    if n > 0 {
1187        out.push_str(&format!("  size_t argc = {n};\n"));
1188        out.push_str(&format!("  napi_value args[{n}];\n"));
1189        out.push_str("  napi_get_cb_info(env, info, &argc, args, NULL, NULL);\n");
1190    } else {
1191        out.push_str("  size_t argc = 0;\n");
1192        out.push_str("  napi_get_cb_info(env, info, &argc, NULL, NULL, NULL);\n");
1193    }
1194
1195    let mut c_args: Vec<String> = Vec::new();
1196    let mut cleanups: Vec<String> = Vec::new();
1197    for (i, p) in f.params.iter().enumerate() {
1198        emit_param(
1199            out,
1200            &mut c_args,
1201            &mut cleanups,
1202            &p.ty,
1203            &p.name,
1204            i,
1205            module,
1206            prefix,
1207        );
1208    }
1209
1210    let actx = format!("{c_name}_napi_actx");
1211    out.push_str(&format!(
1212        "  {actx}* ctx = ({actx}*)calloc(1, sizeof({actx}));\n"
1213    ));
1214    out.push_str("  napi_value promise;\n");
1215    out.push_str("  napi_create_promise(env, &ctx->deferred, &promise);\n");
1216    out.push_str("  napi_value resource_name;\n");
1217    out.push_str(&format!(
1218        "  napi_create_string_utf8(env, \"{c_name}\", NAPI_AUTO_LENGTH, &resource_name);\n"
1219    ));
1220    // Ref'd (unlike listeners): a pending promise must keep the loop alive.
1221    out.push_str(&format!(
1222        "  napi_create_threadsafe_function(env, NULL, NULL, resource_name, 0, 1, NULL, NULL, NULL, {c_name}_napi_settle, &ctx->tsfn);\n"
1223    ));
1224
1225    if f.cancellable {
1226        c_args.push("NULL".into());
1227    }
1228
1229    let cb_name = format!("{c_name}_napi_cb");
1230    c_args.push(cb_name);
1231    c_args.push("ctx".into());
1232    let args_str = c_args.join(", ");
1233    out.push_str(&format!("  {c_name}_async({args_str});\n"));
1234
1235    for cleanup in &cleanups {
1236        out.push_str(cleanup);
1237    }
1238
1239    out.push_str("  return promise;\n");
1240}
1241
1242fn render_napi_body(
1243    out: &mut String,
1244    f: &FnBinding,
1245    c_name: &str,
1246    module: &str,
1247    prefix: &str,
1248    structs: &HashMap<String, StructBinding>,
1249) {
1250    let n = f.params.len();
1251    if n > 0 {
1252        out.push_str(&format!("  size_t argc = {n};\n"));
1253        out.push_str(&format!("  napi_value args[{n}];\n"));
1254        out.push_str("  napi_get_cb_info(env, info, &argc, args, NULL, NULL);\n");
1255    } else {
1256        out.push_str("  size_t argc = 0;\n");
1257        out.push_str("  napi_get_cb_info(env, info, &argc, NULL, NULL, NULL);\n");
1258    }
1259
1260    let mut c_args: Vec<String> = Vec::new();
1261    let mut cleanups: Vec<String> = Vec::new();
1262    for (i, p) in f.params.iter().enumerate() {
1263        emit_param(
1264            out,
1265            &mut c_args,
1266            &mut cleanups,
1267            &p.ty,
1268            &p.name,
1269            i,
1270            module,
1271            prefix,
1272        );
1273    }
1274
1275    out.push_str("  weaveffi_error err = {0};\n");
1276
1277    if let Some(ret) = &f.ret {
1278        emit_ret_out_params(out, &mut c_args, ret, module, prefix);
1279    }
1280    c_args.push("&err".to_string());
1281
1282    let args_str = c_args.join(", ");
1283    let ret_type = f.ret.as_ref().map(|r| c_ret_type_str(r, module, prefix));
1284    match &ret_type {
1285        Some(rt) if rt != "void" => {
1286            out.push_str(&format!("  {rt} result = {c_name}({args_str});\n"));
1287        }
1288        _ => {
1289            out.push_str(&format!("  {c_name}({args_str});\n"));
1290        }
1291    }
1292
1293    for cleanup in &cleanups {
1294        out.push_str(cleanup);
1295    }
1296
1297    out.push_str("  if (err.code != 0) {\n");
1298    out.push_str("    napi_throw_error(env, NULL, err.message);\n");
1299    out.push_str("    weaveffi_error_clear(&err);\n");
1300    out.push_str("    return NULL;\n");
1301    out.push_str("  }\n");
1302
1303    match &f.ret {
1304        Some(ret) => emit_ret_to_napi(out, ret, module, prefix, &f.name, structs),
1305        None => {
1306            out.push_str("  napi_value ret;\n");
1307            out.push_str("  napi_get_undefined(env, &ret);\n");
1308            out.push_str("  return ret;\n");
1309        }
1310    }
1311}
1312
1313#[allow(clippy::too_many_arguments)]
1314fn emit_param(
1315    out: &mut String,
1316    c_args: &mut Vec<String>,
1317    cleanups: &mut Vec<String>,
1318    ty: &TypeRef,
1319    name: &str,
1320    idx: usize,
1321    module: &str,
1322    prefix: &str,
1323) {
1324    match ty {
1325        TypeRef::I32 | TypeRef::U32 | TypeRef::I64 | TypeRef::F64 | TypeRef::Bool => {
1326            let ct = c_elem_type(ty, module, prefix);
1327            let getter = napi_getter(ty);
1328            out.push_str(&format!("  {ct} {name};\n"));
1329            out.push_str(&format!("  {getter}(env, args[{idx}], &{name});\n"));
1330            c_args.push(name.into());
1331        }
1332        TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
1333            out.push_str(&format!("  size_t {name}_len;\n"));
1334            out.push_str(&format!(
1335                "  napi_get_value_string_utf8(env, args[{idx}], NULL, 0, &{name}_len);\n"
1336            ));
1337            out.push_str(&format!(
1338                "  char* {name} = (char*)malloc({name}_len + 1);\n"
1339            ));
1340            out.push_str(&format!(
1341                "  napi_get_value_string_utf8(env, args[{idx}], {name}, {name}_len + 1, &{name}_len);\n"
1342            ));
1343            c_args.push(name.into());
1344            cleanups.push(format!("  free({name});\n"));
1345        }
1346        TypeRef::Handle => {
1347            out.push_str(&format!("  int64_t {name}_raw;\n"));
1348            out.push_str(&format!(
1349                "  napi_get_value_int64(env, args[{idx}], &{name}_raw);\n"
1350            ));
1351            c_args.push(format!("(weaveffi_handle_t){name}_raw"));
1352        }
1353        TypeRef::TypedHandle(s) => {
1354            let abi = c_abi_struct_name(s, module, prefix);
1355            out.push_str(&format!("  int64_t {name}_raw;\n"));
1356            out.push_str(&format!(
1357                "  napi_get_value_int64(env, args[{idx}], &{name}_raw);\n"
1358            ));
1359            c_args.push(format!("({abi}*)(intptr_t){name}_raw"));
1360        }
1361        TypeRef::Enum(e) => {
1362            out.push_str(&format!("  int32_t {name};\n"));
1363            out.push_str(&format!(
1364                "  napi_get_value_int32(env, args[{idx}], &{name});\n"
1365            ));
1366            c_args.push(format!("({prefix}_{module}_{e}){name}"));
1367        }
1368        TypeRef::Struct(s) => {
1369            let abi = c_abi_struct_name(s, module, prefix);
1370            out.push_str(&format!("  int64_t {name}_raw;\n"));
1371            out.push_str(&format!(
1372                "  napi_get_value_int64(env, args[{idx}], &{name}_raw);\n"
1373            ));
1374            c_args.push(format!("(const {abi}*)(intptr_t){name}_raw"));
1375        }
1376        TypeRef::Optional(inner) => {
1377            out.push_str(&format!("  napi_valuetype {name}_type;\n"));
1378            out.push_str(&format!("  napi_typeof(env, args[{idx}], &{name}_type);\n"));
1379            emit_optional_param(out, c_args, cleanups, inner, name, idx, module, prefix);
1380        }
1381        TypeRef::List(inner) => {
1382            emit_list_param(out, c_args, cleanups, inner, name, idx, module, prefix);
1383        }
1384        TypeRef::Bytes | TypeRef::BorrowedBytes => {
1385            out.push_str(&format!("  void* {name}_raw;\n"));
1386            out.push_str(&format!("  size_t {name}_len;\n"));
1387            out.push_str(&format!(
1388                "  napi_get_buffer_info(env, args[{idx}], &{name}_raw, &{name}_len);\n"
1389            ));
1390            c_args.push(format!("(const uint8_t*){name}_raw"));
1391            c_args.push(format!("{name}_len"));
1392        }
1393        TypeRef::Map(k, v) => {
1394            emit_map_param(out, c_args, cleanups, k, v, name, idx, module, prefix);
1395        }
1396        TypeRef::Iterator(_) => unreachable!("iterator not valid as parameter"),
1397    }
1398}
1399
1400fn emit_opt_val(
1401    out: &mut String,
1402    c_args: &mut Vec<String>,
1403    c_type: &str,
1404    napi_fn: &str,
1405    name: &str,
1406    idx: usize,
1407) {
1408    out.push_str(&format!("  {c_type} {name}_val;\n"));
1409    out.push_str(&format!("  const {c_type}* {name}_ptr = NULL;\n"));
1410    out.push_str(&format!(
1411        "  if ({name}_type != napi_null && {name}_type != napi_undefined) {{\n"
1412    ));
1413    out.push_str(&format!("    {napi_fn}(env, args[{idx}], &{name}_val);\n"));
1414    out.push_str(&format!("    {name}_ptr = &{name}_val;\n"));
1415    out.push_str("  }\n");
1416    c_args.push(format!("{name}_ptr"));
1417}
1418
1419#[allow(clippy::too_many_arguments)]
1420fn emit_optional_param(
1421    out: &mut String,
1422    c_args: &mut Vec<String>,
1423    cleanups: &mut Vec<String>,
1424    inner: &TypeRef,
1425    name: &str,
1426    idx: usize,
1427    module: &str,
1428    prefix: &str,
1429) {
1430    match inner {
1431        TypeRef::I32 => {
1432            emit_opt_val(out, c_args, "int32_t", "napi_get_value_int32", name, idx);
1433        }
1434        TypeRef::U32 => {
1435            emit_opt_val(out, c_args, "uint32_t", "napi_get_value_uint32", name, idx);
1436        }
1437        TypeRef::I64 => {
1438            emit_opt_val(out, c_args, "int64_t", "napi_get_value_int64", name, idx);
1439        }
1440        TypeRef::F64 => {
1441            emit_opt_val(out, c_args, "double", "napi_get_value_double", name, idx);
1442        }
1443        TypeRef::Bool => {
1444            emit_opt_val(out, c_args, "bool", "napi_get_value_bool", name, idx);
1445        }
1446        TypeRef::Handle => {
1447            out.push_str(&format!("  int64_t {name}_raw = 0;\n"));
1448            out.push_str(&format!("  weaveffi_handle_t {name}_val;\n"));
1449            out.push_str(&format!("  const weaveffi_handle_t* {name}_ptr = NULL;\n"));
1450            out.push_str(&format!(
1451                "  if ({name}_type != napi_null && {name}_type != napi_undefined) {{\n"
1452            ));
1453            out.push_str(&format!(
1454                "    napi_get_value_int64(env, args[{idx}], &{name}_raw);\n"
1455            ));
1456            out.push_str(&format!(
1457                "    {name}_val = (weaveffi_handle_t){name}_raw;\n"
1458            ));
1459            out.push_str(&format!("    {name}_ptr = &{name}_val;\n"));
1460            out.push_str("  }\n");
1461            c_args.push(format!("{name}_ptr"));
1462        }
1463        // A typed handle is a nullable opaque pointer, so an optional one maps to
1464        // the same pointer with NULL standing in for absence — mirroring structs.
1465        TypeRef::TypedHandle(s) => {
1466            let abi = c_abi_struct_name(s, module, prefix);
1467            out.push_str(&format!("  int64_t {name}_raw = 0;\n"));
1468            out.push_str(&format!(
1469                "  if ({name}_type != napi_null && {name}_type != napi_undefined) {{\n"
1470            ));
1471            out.push_str(&format!(
1472                "    napi_get_value_int64(env, args[{idx}], &{name}_raw);\n"
1473            ));
1474            out.push_str("  }\n");
1475            c_args.push(format!("{name}_raw ? ({abi}*)(intptr_t){name}_raw : NULL"));
1476        }
1477        TypeRef::Enum(e) => {
1478            let etype = format!("{prefix}_{module}_{e}");
1479            out.push_str(&format!("  int32_t {name}_raw;\n"));
1480            out.push_str(&format!("  {etype} {name}_val;\n"));
1481            out.push_str(&format!("  const {etype}* {name}_ptr = NULL;\n"));
1482            out.push_str(&format!(
1483                "  if ({name}_type != napi_null && {name}_type != napi_undefined) {{\n"
1484            ));
1485            out.push_str(&format!(
1486                "    napi_get_value_int32(env, args[{idx}], &{name}_raw);\n"
1487            ));
1488            out.push_str(&format!("    {name}_val = ({etype}){name}_raw;\n"));
1489            out.push_str(&format!("    {name}_ptr = &{name}_val;\n"));
1490            out.push_str("  }\n");
1491            c_args.push(format!("{name}_ptr"));
1492        }
1493        TypeRef::StringUtf8 => {
1494            out.push_str(&format!("  char* {name} = NULL;\n"));
1495            out.push_str(&format!(
1496                "  if ({name}_type != napi_null && {name}_type != napi_undefined) {{\n"
1497            ));
1498            out.push_str(&format!("    size_t {name}_len;\n"));
1499            out.push_str(&format!(
1500                "    napi_get_value_string_utf8(env, args[{idx}], NULL, 0, &{name}_len);\n"
1501            ));
1502            out.push_str(&format!("    {name} = (char*)malloc({name}_len + 1);\n"));
1503            out.push_str(&format!(
1504                "    napi_get_value_string_utf8(env, args[{idx}], {name}, {name}_len + 1, &{name}_len);\n"
1505            ));
1506            out.push_str("  }\n");
1507            c_args.push(name.into());
1508            cleanups.push(format!("  free({name});\n"));
1509        }
1510        TypeRef::Struct(s) => {
1511            let abi = c_abi_struct_name(s, module, prefix);
1512            out.push_str(&format!("  int64_t {name}_raw = 0;\n"));
1513            out.push_str(&format!(
1514                "  if ({name}_type != napi_null && {name}_type != napi_undefined) {{\n"
1515            ));
1516            out.push_str(&format!(
1517                "    napi_get_value_int64(env, args[{idx}], &{name}_raw);\n"
1518            ));
1519            out.push_str("  }\n");
1520            c_args.push(format!(
1521                "{name}_raw ? (const {abi}*)(intptr_t){name}_raw : NULL"
1522            ));
1523        }
1524        _ => {
1525            emit_param(out, c_args, cleanups, inner, name, idx, module, prefix);
1526        }
1527    }
1528}
1529
1530#[allow(clippy::too_many_arguments)]
1531fn emit_list_param(
1532    out: &mut String,
1533    c_args: &mut Vec<String>,
1534    cleanups: &mut Vec<String>,
1535    inner: &TypeRef,
1536    name: &str,
1537    idx: usize,
1538    module: &str,
1539    prefix: &str,
1540) {
1541    let et = c_elem_type(inner, module, prefix);
1542    out.push_str(&format!("  uint32_t {name}_count;\n"));
1543    out.push_str(&format!(
1544        "  napi_get_array_length(env, args[{idx}], &{name}_count);\n"
1545    ));
1546    out.push_str(&format!(
1547        "  {et}* {name}_arr = ({et}*)malloc(sizeof({et}) * ({name}_count + 1));\n"
1548    ));
1549    out.push_str(&format!(
1550        "  for (uint32_t {name}_i = 0; {name}_i < {name}_count; {name}_i++) {{\n"
1551    ));
1552    out.push_str(&format!("    napi_value {name}_el;\n"));
1553    out.push_str(&format!(
1554        "    napi_get_element(env, args[{idx}], {name}_i, &{name}_el);\n"
1555    ));
1556
1557    match inner {
1558        TypeRef::I32 | TypeRef::U32 | TypeRef::I64 | TypeRef::F64 | TypeRef::Bool => {
1559            let getter = napi_getter(inner);
1560            out.push_str(&format!(
1561                "    {getter}(env, {name}_el, &{name}_arr[{name}_i]);\n"
1562            ));
1563        }
1564        TypeRef::Handle => {
1565            out.push_str(&format!("    int64_t {name}_h;\n"));
1566            out.push_str(&format!(
1567                "    napi_get_value_int64(env, {name}_el, &{name}_h);\n"
1568            ));
1569            out.push_str(&format!(
1570                "    {name}_arr[{name}_i] = (weaveffi_handle_t){name}_h;\n"
1571            ));
1572        }
1573        TypeRef::TypedHandle(s) => {
1574            let abi = c_abi_struct_name(s, module, prefix);
1575            out.push_str(&format!("    int64_t {name}_h;\n"));
1576            out.push_str(&format!(
1577                "    napi_get_value_int64(env, {name}_el, &{name}_h);\n"
1578            ));
1579            out.push_str(&format!(
1580                "    {name}_arr[{name}_i] = ({abi}*)(intptr_t){name}_h;\n"
1581            ));
1582        }
1583        TypeRef::Enum(_) => {
1584            out.push_str(&format!("    int32_t {name}_ev;\n"));
1585            out.push_str(&format!(
1586                "    napi_get_value_int32(env, {name}_el, &{name}_ev);\n"
1587            ));
1588            out.push_str(&format!("    {name}_arr[{name}_i] = ({et}){name}_ev;\n"));
1589        }
1590        TypeRef::StringUtf8 => {
1591            out.push_str(&format!("    size_t {name}_sl;\n"));
1592            out.push_str(&format!(
1593                "    napi_get_value_string_utf8(env, {name}_el, NULL, 0, &{name}_sl);\n"
1594            ));
1595            out.push_str(&format!(
1596                "    char* {name}_s = (char*)malloc({name}_sl + 1);\n"
1597            ));
1598            out.push_str(&format!(
1599                "    napi_get_value_string_utf8(env, {name}_el, {name}_s, {name}_sl + 1, &{name}_sl);\n"
1600            ));
1601            out.push_str(&format!("    {name}_arr[{name}_i] = {name}_s;\n"));
1602        }
1603        TypeRef::Struct(_) => {
1604            out.push_str(&format!("    int64_t {name}_sp;\n"));
1605            out.push_str(&format!(
1606                "    napi_get_value_int64(env, {name}_el, &{name}_sp);\n"
1607            ));
1608            out.push_str(&format!(
1609                "    {name}_arr[{name}_i] = ({et})(intptr_t){name}_sp;\n"
1610            ));
1611        }
1612        _ => {
1613            let getter = napi_getter(inner);
1614            out.push_str(&format!(
1615                "    {getter}(env, {name}_el, &{name}_arr[{name}_i]);\n"
1616            ));
1617        }
1618    }
1619
1620    out.push_str("  }\n");
1621    c_args.push(format!("{name}_arr"));
1622    c_args.push(format!("(size_t){name}_count"));
1623
1624    if matches!(inner, TypeRef::StringUtf8) {
1625        cleanups.push(format!(
1626            "  for (uint32_t {name}_j = 0; {name}_j < {name}_count; {name}_j++) free((void*){name}_arr[{name}_j]);\n"
1627        ));
1628    }
1629    cleanups.push(format!("  free({name}_arr);\n"));
1630}
1631
1632#[allow(clippy::too_many_arguments)]
1633fn emit_map_param(
1634    out: &mut String,
1635    c_args: &mut Vec<String>,
1636    cleanups: &mut Vec<String>,
1637    k: &TypeRef,
1638    v: &TypeRef,
1639    name: &str,
1640    idx: usize,
1641    module: &str,
1642    prefix: &str,
1643) {
1644    let kt = c_elem_type(k, module, prefix);
1645    let vt = c_elem_type(v, module, prefix);
1646    out.push_str(&format!("  napi_value {name}_keys_napi;\n"));
1647    out.push_str(&format!(
1648        "  napi_get_property_names(env, args[{idx}], &{name}_keys_napi);\n"
1649    ));
1650    out.push_str(&format!("  uint32_t {name}_count;\n"));
1651    out.push_str(&format!(
1652        "  napi_get_array_length(env, {name}_keys_napi, &{name}_count);\n"
1653    ));
1654    out.push_str(&format!(
1655        "  {kt}* {name}_keys = ({kt}*)malloc(sizeof({kt}) * ({name}_count + 1));\n"
1656    ));
1657    out.push_str(&format!(
1658        "  {vt}* {name}_values = ({vt}*)malloc(sizeof({vt}) * ({name}_count + 1));\n"
1659    ));
1660    out.push_str(&format!(
1661        "  for (uint32_t {name}_i = 0; {name}_i < {name}_count; {name}_i++) {{\n"
1662    ));
1663    out.push_str(&format!("    napi_value {name}_k;\n"));
1664    out.push_str(&format!(
1665        "    napi_get_element(env, {name}_keys_napi, {name}_i, &{name}_k);\n"
1666    ));
1667
1668    if matches!(k, TypeRef::StringUtf8) {
1669        out.push_str(&format!("    size_t {name}_kl;\n"));
1670        out.push_str(&format!(
1671            "    napi_get_value_string_utf8(env, {name}_k, NULL, 0, &{name}_kl);\n"
1672        ));
1673        out.push_str(&format!(
1674            "    char* {name}_ks = (char*)malloc({name}_kl + 1);\n"
1675        ));
1676        out.push_str(&format!(
1677            "    napi_get_value_string_utf8(env, {name}_k, {name}_ks, {name}_kl + 1, &{name}_kl);\n"
1678        ));
1679        out.push_str(&format!("    {name}_keys[{name}_i] = {name}_ks;\n"));
1680    } else {
1681        out.push_str(&format!("    napi_value {name}_kn;\n"));
1682        out.push_str(&format!(
1683            "    napi_coerce_to_number(env, {name}_k, &{name}_kn);\n"
1684        ));
1685        let kgetter = napi_getter(k);
1686        out.push_str(&format!(
1687            "    {kgetter}(env, {name}_kn, &{name}_keys[{name}_i]);\n"
1688        ));
1689    }
1690
1691    out.push_str(&format!("    napi_value {name}_v;\n"));
1692    out.push_str(&format!(
1693        "    napi_get_property(env, args[{idx}], {name}_k, &{name}_v);\n"
1694    ));
1695
1696    if matches!(v, TypeRef::StringUtf8) {
1697        out.push_str(&format!("    size_t {name}_vl;\n"));
1698        out.push_str(&format!(
1699            "    napi_get_value_string_utf8(env, {name}_v, NULL, 0, &{name}_vl);\n"
1700        ));
1701        out.push_str(&format!(
1702            "    char* {name}_vs = (char*)malloc({name}_vl + 1);\n"
1703        ));
1704        out.push_str(&format!(
1705            "    napi_get_value_string_utf8(env, {name}_v, {name}_vs, {name}_vl + 1, &{name}_vl);\n"
1706        ));
1707        out.push_str(&format!("    {name}_values[{name}_i] = {name}_vs;\n"));
1708    } else {
1709        let vgetter = napi_getter(v);
1710        out.push_str(&format!(
1711            "    {vgetter}(env, {name}_v, &{name}_values[{name}_i]);\n"
1712        ));
1713    }
1714
1715    out.push_str("  }\n");
1716    c_args.push(format!("{name}_keys"));
1717    c_args.push(format!("{name}_values"));
1718    c_args.push(format!("(size_t){name}_count"));
1719
1720    if matches!(k, TypeRef::StringUtf8) {
1721        cleanups.push(format!(
1722            "  for (uint32_t {name}_j = 0; {name}_j < {name}_count; {name}_j++) free((void*){name}_keys[{name}_j]);\n"
1723        ));
1724    }
1725    cleanups.push(format!("  free({name}_keys);\n"));
1726    if matches!(v, TypeRef::StringUtf8) {
1727        cleanups.push(format!(
1728            "  for (uint32_t {name}_j = 0; {name}_j < {name}_count; {name}_j++) free((void*){name}_values[{name}_j]);\n"
1729        ));
1730    }
1731    cleanups.push(format!("  free({name}_values);\n"));
1732}
1733
1734fn emit_ret_out_params(
1735    out: &mut String,
1736    c_args: &mut Vec<String>,
1737    ty: &TypeRef,
1738    module: &str,
1739    prefix: &str,
1740) {
1741    match ty {
1742        TypeRef::Bytes | TypeRef::List(_) => {
1743            out.push_str("  size_t out_len;\n");
1744            c_args.push("&out_len".into());
1745        }
1746        TypeRef::Map(k, v) => {
1747            let kt = c_elem_type(k, module, prefix);
1748            let vt = c_elem_type(v, module, prefix);
1749            out.push_str(&format!("  {kt}* out_keys = NULL;\n"));
1750            out.push_str(&format!("  {vt}* out_values = NULL;\n"));
1751            out.push_str("  size_t out_len = 0;\n");
1752            c_args.push("out_keys".into());
1753            c_args.push("out_values".into());
1754            c_args.push("&out_len".into());
1755        }
1756        TypeRef::Optional(inner) if is_c_ptr_type(inner) => {
1757            emit_ret_out_params(out, c_args, inner, module, prefix);
1758        }
1759        _ => {}
1760    }
1761}
1762
1763/// Build a `name -> StructDef` registry over every (possibly nested) module so
1764/// that struct-returning functions can materialize a real JS object (matching
1765/// the shape declared in `types.d.ts`) instead of leaking a raw handle number.
1766fn struct_registry(model: &BindingModel) -> HashMap<String, StructBinding> {
1767    model
1768        .modules
1769        .iter()
1770        .flat_map(|m| m.structs.iter())
1771        .map(|s| (s.name.clone(), s.clone()))
1772        .collect()
1773}
1774
1775/// Materialize an *owned* C struct pointer (`ptr_expr`) into a plain JS object
1776/// assigned to `obj_var`, by invoking each generated field getter. The pointer
1777/// is consumed: after the fields are read it is destroyed, because the C ABI
1778/// hands back owned struct handles (the same ownership the other backends free).
1779#[allow(clippy::too_many_arguments)]
1780fn emit_struct_to_object(
1781    out: &mut String,
1782    env: &str,
1783    struct_name: &str,
1784    ptr_expr: &str,
1785    obj_var: &str,
1786    module: &str,
1787    prefix: &str,
1788    structs: &HashMap<String, StructBinding>,
1789    indent: &str,
1790    destroy: bool,
1791) {
1792    let Some(def) = structs.get(local_type_name(struct_name)).cloned() else {
1793        // Unknown struct: fall back to the raw handle rather than emit broken C.
1794        out.push_str(&format!(
1795            "{indent}napi_create_int64({env}, (int64_t)(intptr_t){ptr_expr}, &{obj_var});\n"
1796        ));
1797        return;
1798    };
1799    let abi = &def.c_tag;
1800    let p = format!("{obj_var}_p");
1801    out.push_str(&format!("{indent}{{\n"));
1802    out.push_str(&format!("{indent}  {abi}* {p} = ({abi}*){ptr_expr};\n"));
1803    out.push_str(&format!(
1804        "{indent}  napi_create_object({env}, &{obj_var});\n"
1805    ));
1806    for field in &def.fields {
1807        let getter = &field.getter_symbol;
1808        let fv = format!("{obj_var}_{}", field.name);
1809        out.push_str(&format!("{indent}  napi_value {fv};\n"));
1810        emit_struct_field_to_napi(
1811            out,
1812            env,
1813            &field.ty,
1814            getter,
1815            &p,
1816            &fv,
1817            module,
1818            prefix,
1819            structs,
1820            &format!("{indent}  "),
1821        );
1822        out.push_str(&format!(
1823            "{indent}  napi_set_named_property({env}, {obj_var}, \"{}\", {fv});\n",
1824            field.name
1825        ));
1826    }
1827    if destroy {
1828        out.push_str(&format!("{indent}  {}({p});\n", def.destroy_symbol));
1829    }
1830    out.push_str(&format!("{indent}}}\n"));
1831}
1832
1833/// The C statement that creates a napi value `target` from a leaf C expression
1834/// `expr` (scalars, bools, enums, handles). Strings/structs are handled by
1835/// [`emit_elem_to_napi`], which needs surrounding context.
1836fn napi_create_leaf(env: &str, ty: &TypeRef, expr: &str, target: &str) -> String {
1837    match ty {
1838        TypeRef::I32 => format!("napi_create_int32({env}, {expr}, &{target});"),
1839        TypeRef::U32 => format!("napi_create_uint32({env}, {expr}, &{target});"),
1840        TypeRef::I64 => format!("napi_create_int64({env}, {expr}, &{target});"),
1841        TypeRef::F64 => format!("napi_create_double({env}, {expr}, &{target});"),
1842        TypeRef::Bool => format!("napi_get_boolean({env}, {expr}, &{target});"),
1843        TypeRef::Enum(_) => format!("napi_create_int32({env}, (int32_t)({expr}), &{target});"),
1844        TypeRef::Handle | TypeRef::TypedHandle(_) => {
1845            format!("napi_create_int64({env}, (int64_t)(intptr_t)({expr}), &{target});")
1846        }
1847        _ => format!("napi_get_null({env}, &{target});"),
1848    }
1849}
1850
1851/// Convert a single collection *element* C expression `expr` (a list item or map
1852/// value) into the napi value `target`. Owned element strings are freed after
1853/// the copy, matching the C ABI's transfer-on-return contract.
1854#[allow(clippy::too_many_arguments)]
1855fn emit_elem_to_napi(
1856    out: &mut String,
1857    env: &str,
1858    ty: &TypeRef,
1859    expr: &str,
1860    target: &str,
1861    module: &str,
1862    prefix: &str,
1863    structs: &HashMap<String, StructBinding>,
1864    indent: &str,
1865) {
1866    match ty {
1867        TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
1868            out.push_str(&format!(
1869                "{indent}napi_create_string_utf8({env}, {expr}, NAPI_AUTO_LENGTH, &{target});\n"
1870            ));
1871            if matches!(ty, TypeRef::StringUtf8) {
1872                out.push_str(&format!("{indent}weaveffi_free_string((char*)({expr}));\n"));
1873            }
1874        }
1875        TypeRef::Struct(name) => {
1876            emit_struct_to_object(
1877                out, env, name, expr, target, module, prefix, structs, indent, false,
1878            );
1879        }
1880        _ => out.push_str(&format!(
1881            "{indent}{}\n",
1882            napi_create_leaf(env, ty, expr, target)
1883        )),
1884    }
1885}
1886
1887/// Marshal one struct field, read via `getter(pv)`, into the JS value `fv`.
1888/// Scalars, enums, handles, owned strings, optional strings, nested structs,
1889/// byte buffers, lists, maps, and optional scalars are all materialized.
1890#[allow(clippy::too_many_arguments)]
1891fn emit_struct_field_to_napi(
1892    out: &mut String,
1893    env: &str,
1894    ty: &TypeRef,
1895    getter: &str,
1896    pv: &str,
1897    fv: &str,
1898    module: &str,
1899    prefix: &str,
1900    structs: &HashMap<String, StructBinding>,
1901    indent: &str,
1902) {
1903    match ty {
1904        TypeRef::I32 => out.push_str(&format!(
1905            "{indent}napi_create_int32({env}, {getter}({pv}), &{fv});\n"
1906        )),
1907        TypeRef::U32 => out.push_str(&format!(
1908            "{indent}napi_create_uint32({env}, {getter}({pv}), &{fv});\n"
1909        )),
1910        TypeRef::I64 => out.push_str(&format!(
1911            "{indent}napi_create_int64({env}, {getter}({pv}), &{fv});\n"
1912        )),
1913        TypeRef::F64 => out.push_str(&format!(
1914            "{indent}napi_create_double({env}, {getter}({pv}), &{fv});\n"
1915        )),
1916        TypeRef::Bool => out.push_str(&format!(
1917            "{indent}napi_get_boolean({env}, {getter}({pv}), &{fv});\n"
1918        )),
1919        TypeRef::Enum(_) => out.push_str(&format!(
1920            "{indent}napi_create_int32({env}, (int32_t){getter}({pv}), &{fv});\n"
1921        )),
1922        TypeRef::Handle | TypeRef::TypedHandle(_) => out.push_str(&format!(
1923            "{indent}napi_create_int64({env}, (int64_t)(intptr_t){getter}({pv}), &{fv});\n"
1924        )),
1925        TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
1926            let owned = matches!(ty, TypeRef::StringUtf8);
1927            out.push_str(&format!("{indent}{{\n"));
1928            out.push_str(&format!(
1929                "{indent}  char* {fv}_s = (char*){getter}({pv});\n"
1930            ));
1931            out.push_str(&format!(
1932                "{indent}  napi_create_string_utf8({env}, {fv}_s, NAPI_AUTO_LENGTH, &{fv});\n"
1933            ));
1934            if owned {
1935                out.push_str(&format!("{indent}  weaveffi_free_string({fv}_s);\n"));
1936            }
1937            out.push_str(&format!("{indent}}}\n"));
1938        }
1939        TypeRef::Struct(name) => {
1940            emit_struct_to_object(
1941                out,
1942                env,
1943                name,
1944                &format!("{getter}({pv})"),
1945                fv,
1946                module,
1947                prefix,
1948                structs,
1949                indent,
1950                true,
1951            );
1952        }
1953        TypeRef::Optional(inner)
1954            if matches!(inner.as_ref(), TypeRef::StringUtf8 | TypeRef::BorrowedStr) =>
1955        {
1956            let owned = matches!(inner.as_ref(), TypeRef::StringUtf8);
1957            out.push_str(&format!("{indent}{{\n"));
1958            out.push_str(&format!(
1959                "{indent}  char* {fv}_s = (char*){getter}({pv});\n"
1960            ));
1961            out.push_str(&format!(
1962                "{indent}  if ({fv}_s == NULL) {{ napi_get_null({env}, &{fv}); }}\n"
1963            ));
1964            out.push_str(&format!(
1965                "{indent}  else {{ napi_create_string_utf8({env}, {fv}_s, NAPI_AUTO_LENGTH, &{fv});"
1966            ));
1967            if owned {
1968                out.push_str(&format!(" weaveffi_free_string({fv}_s);"));
1969            }
1970            out.push_str(" }\n");
1971            out.push_str(&format!("{indent}}}\n"));
1972        }
1973        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Struct(_)) => {
1974            let TypeRef::Struct(name) = inner.as_ref() else {
1975                unreachable!()
1976            };
1977            let abi = c_abi_struct_name(name, module, prefix);
1978            out.push_str(&format!("{indent}{{\n"));
1979            out.push_str(&format!("{indent}  {abi}* {fv}_sp = {getter}({pv});\n"));
1980            out.push_str(&format!(
1981                "{indent}  if ({fv}_sp == NULL) {{ napi_get_null({env}, &{fv}); }}\n"
1982            ));
1983            out.push_str(&format!("{indent}  else {{\n"));
1984            emit_struct_to_object(
1985                out,
1986                env,
1987                name,
1988                &format!("{fv}_sp"),
1989                fv,
1990                module,
1991                prefix,
1992                structs,
1993                &format!("{indent}    "),
1994                true,
1995            );
1996            out.push_str(&format!("{indent}  }}\n"));
1997            out.push_str(&format!("{indent}}}\n"));
1998        }
1999        // An optional typed handle lowers to a nullable opaque pointer that the
2000        // field surfaces as the integer handle (or null), like the non-optional
2001        // case but guarded on NULL.
2002        TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::TypedHandle(_)) => {
2003            let TypeRef::TypedHandle(name) = inner.as_ref() else {
2004                unreachable!()
2005            };
2006            let abi = c_abi_struct_name(name, module, prefix);
2007            out.push_str(&format!("{indent}{{\n"));
2008            out.push_str(&format!("{indent}  {abi}* {fv}_h = {getter}({pv});\n"));
2009            out.push_str(&format!(
2010                "{indent}  if ({fv}_h == NULL) {{ napi_get_null({env}, &{fv}); }}\n"
2011            ));
2012            out.push_str(&format!(
2013                "{indent}  else {{ napi_create_int64({env}, (int64_t)(intptr_t){fv}_h, &{fv}); }}\n"
2014            ));
2015            out.push_str(&format!("{indent}}}\n"));
2016        }
2017        // Remaining optionals (scalar/bool/enum/handle) lower to a nullable
2018        // pointer-to-value the getter returns directly.
2019        TypeRef::Optional(inner) => {
2020            let ct = c_elem_type(inner, module, prefix);
2021            out.push_str(&format!("{indent}{{\n"));
2022            out.push_str(&format!("{indent}  {ct}* {fv}_p = {getter}({pv});\n"));
2023            out.push_str(&format!(
2024                "{indent}  if ({fv}_p == NULL) {{ napi_get_null({env}, &{fv}); }}\n"
2025            ));
2026            out.push_str(&format!(
2027                "{indent}  else {{ {} }}\n",
2028                napi_create_leaf(env, inner, &format!("*{fv}_p"), fv)
2029            ));
2030            out.push_str(&format!("{indent}}}\n"));
2031        }
2032        TypeRef::Bytes | TypeRef::BorrowedBytes => {
2033            out.push_str(&format!("{indent}{{\n"));
2034            out.push_str(&format!("{indent}  size_t {fv}_len;\n"));
2035            out.push_str(&format!(
2036                "{indent}  const uint8_t* {fv}_data = (const uint8_t*){getter}({pv}, &{fv}_len);\n"
2037            ));
2038            out.push_str(&format!(
2039                "{indent}  if ({fv}_data == NULL) {{ napi_get_null({env}, &{fv}); }}\n"
2040            ));
2041            out.push_str(&format!(
2042                "{indent}  else {{ void* {fv}_buf; napi_create_buffer_copy({env}, {fv}_len, {fv}_data, &{fv}_buf, &{fv}); }}\n"
2043            ));
2044            out.push_str(&format!("{indent}}}\n"));
2045        }
2046        TypeRef::List(inner) => {
2047            let et = c_elem_type(inner, module, prefix);
2048            out.push_str(&format!("{indent}{{\n"));
2049            out.push_str(&format!("{indent}  size_t {fv}_len;\n"));
2050            out.push_str(&format!(
2051                "{indent}  {et}* {fv}_arr = {getter}({pv}, &{fv}_len);\n"
2052            ));
2053            out.push_str(&format!("{indent}  napi_create_array({env}, &{fv});\n"));
2054            out.push_str(&format!("{indent}  if ({fv}_arr != NULL) {{\n"));
2055            out.push_str(&format!(
2056                "{indent}    for (size_t {fv}_i = 0; {fv}_i < {fv}_len; {fv}_i++) {{\n"
2057            ));
2058            out.push_str(&format!("{indent}      napi_value {fv}_e;\n"));
2059            emit_elem_to_napi(
2060                out,
2061                env,
2062                inner,
2063                &format!("{fv}_arr[{fv}_i]"),
2064                &format!("{fv}_e"),
2065                module,
2066                prefix,
2067                structs,
2068                &format!("{indent}      "),
2069            );
2070            out.push_str(&format!(
2071                "{indent}      napi_set_element({env}, {fv}, (uint32_t){fv}_i, {fv}_e);\n"
2072            ));
2073            out.push_str(&format!("{indent}    }}\n"));
2074            out.push_str(&format!("{indent}  }}\n"));
2075            out.push_str(&format!("{indent}}}\n"));
2076        }
2077        TypeRef::Map(k, v) => {
2078            let kt = c_elem_type(k, module, prefix);
2079            let vt = c_elem_type(v, module, prefix);
2080            out.push_str(&format!("{indent}{{\n"));
2081            out.push_str(&format!("{indent}  {kt}* {fv}_keys = NULL;\n"));
2082            out.push_str(&format!("{indent}  {vt}* {fv}_vals = NULL;\n"));
2083            out.push_str(&format!("{indent}  size_t {fv}_len;\n"));
2084            out.push_str(&format!(
2085                "{indent}  {getter}({pv}, &{fv}_keys, &{fv}_vals, &{fv}_len);\n"
2086            ));
2087            out.push_str(&format!("{indent}  napi_create_object({env}, &{fv});\n"));
2088            out.push_str(&format!(
2089                "{indent}  if ({fv}_keys != NULL && {fv}_vals != NULL) {{\n"
2090            ));
2091            out.push_str(&format!(
2092                "{indent}    for (size_t {fv}_i = 0; {fv}_i < {fv}_len; {fv}_i++) {{\n"
2093            ));
2094            out.push_str(&format!("{indent}      napi_value {fv}_v;\n"));
2095            emit_elem_to_napi(
2096                out,
2097                env,
2098                v,
2099                &format!("{fv}_vals[{fv}_i]"),
2100                &format!("{fv}_v"),
2101                module,
2102                prefix,
2103                structs,
2104                &format!("{indent}      "),
2105            );
2106            match k.as_ref() {
2107                TypeRef::StringUtf8 | TypeRef::BorrowedStr => {
2108                    out.push_str(&format!(
2109                        "{indent}      napi_set_named_property({env}, {fv}, {fv}_keys[{fv}_i], {fv}_v);\n"
2110                    ));
2111                    if matches!(k.as_ref(), TypeRef::StringUtf8) {
2112                        out.push_str(&format!(
2113                            "{indent}      weaveffi_free_string((char*){fv}_keys[{fv}_i]);\n"
2114                        ));
2115                    }
2116                }
2117                other => {
2118                    out.push_str(&format!("{indent}      napi_value {fv}_k;\n"));
2119                    out.push_str(&format!(
2120                        "{indent}      {}\n",
2121                        napi_create_leaf(
2122                            env,
2123                            other,
2124                            &format!("{fv}_keys[{fv}_i]"),
2125                            &format!("{fv}_k")
2126                        )
2127                    ));
2128                    out.push_str(&format!(
2129                        "{indent}      napi_set_property({env}, {fv}, {fv}_k, {fv}_v);\n"
2130                    ));
2131                }
2132            }
2133            out.push_str(&format!("{indent}    }}\n"));
2134            out.push_str(&format!("{indent}  }}\n"));
2135            out.push_str(&format!("{indent}}}\n"));
2136        }
2137        _ => out.push_str(&format!("{indent}napi_get_null({env}, &{fv});\n")),
2138    }
2139}
2140
2141fn emit_ret_to_napi(
2142    out: &mut String,
2143    ty: &TypeRef,
2144    module: &str,
2145    prefix: &str,
2146    fn_name: &str,
2147    structs: &HashMap<String, StructBinding>,
2148) {
2149    out.push_str("  napi_value ret;\n");
2150    match ty {
2151        TypeRef::I32 => out.push_str("  napi_create_int32(env, result, &ret);\n"),
2152        TypeRef::U32 => out.push_str("  napi_create_uint32(env, result, &ret);\n"),
2153        TypeRef::I64 => out.push_str("  napi_create_int64(env, result, &ret);\n"),
2154        TypeRef::F64 => out.push_str("  napi_create_double(env, result, &ret);\n"),
2155        TypeRef::Bool => out.push_str("  napi_get_boolean(env, result, &ret);\n"),
2156        TypeRef::StringUtf8 => {
2157            out.push_str("  napi_create_string_utf8(env, result, NAPI_AUTO_LENGTH, &ret);\n");
2158            out.push_str("  weaveffi_free_string(result);\n");
2159        }
2160        TypeRef::BorrowedStr => {
2161            out.push_str("  napi_create_string_utf8(env, result, NAPI_AUTO_LENGTH, &ret);\n");
2162        }
2163        TypeRef::TypedHandle(_) | TypeRef::Handle => {
2164            out.push_str("  napi_create_int64(env, (int64_t)(intptr_t)result, &ret);\n");
2165        }
2166        TypeRef::Struct(name) => {
2167            emit_struct_to_object(
2168                out, "env", name, "result", "ret", module, prefix, structs, "  ", true,
2169            );
2170        }
2171        TypeRef::Enum(_) => {
2172            out.push_str("  napi_create_int32(env, (int32_t)result, &ret);\n");
2173        }
2174        TypeRef::Bytes => {
2175            out.push_str("  napi_create_buffer_copy(env, out_len, result, NULL, &ret);\n");
2176            out.push_str("  weaveffi_free_bytes((uint8_t*)result, out_len);\n");
2177        }
2178        TypeRef::BorrowedBytes => {
2179            out.push_str("  napi_create_buffer_copy(env, out_len, result, NULL, &ret);\n");
2180        }
2181        TypeRef::Optional(inner) => {
2182            out.push_str("  if (result == NULL) {\n");
2183            out.push_str("    napi_get_null(env, &ret);\n");
2184            out.push_str("  } else {\n");
2185            emit_optional_ret_inner(out, inner, module, prefix, structs);
2186            out.push_str("  }\n");
2187        }
2188        TypeRef::List(inner) => emit_list_ret(out, inner, module, prefix, "  ", structs),
2189        TypeRef::Map(_, _) => {
2190            out.push_str("  napi_create_object(env, &ret);\n");
2191        }
2192        TypeRef::Iterator(inner) => {
2193            let fn_pascal = fn_name.to_upper_camel_case();
2194            let iter_type = format!("{prefix}_{module}_{fn_pascal}Iterator");
2195            let et = c_elem_type(inner, module, prefix);
2196            out.push_str("  napi_create_array(env, &ret);\n");
2197            out.push_str("  uint32_t iter_idx = 0;\n");
2198            out.push_str(&format!("  {et} iter_item;\n"));
2199            // The iterator's `_next` reports per-step faults through a trailing
2200            // error out-param; it is part of the C ABI signature and must be
2201            // threaded through even when we surface drained items as an array.
2202            out.push_str("  weaveffi_error iter_err = {0};\n");
2203            out.push_str(&format!(
2204                "  while ({iter_type}_next(result, &iter_item, &iter_err)) {{\n"
2205            ));
2206            out.push_str("    napi_value elem;\n");
2207            match inner.as_ref() {
2208                TypeRef::I32 => {
2209                    out.push_str("    napi_create_int32(env, iter_item, &elem);\n");
2210                }
2211                TypeRef::U32 => {
2212                    out.push_str("    napi_create_uint32(env, iter_item, &elem);\n");
2213                }
2214                TypeRef::I64 => {
2215                    out.push_str("    napi_create_int64(env, iter_item, &elem);\n");
2216                }
2217                TypeRef::F64 => {
2218                    out.push_str("    napi_create_double(env, iter_item, &elem);\n");
2219                }
2220                TypeRef::Bool => {
2221                    out.push_str("    napi_get_boolean(env, iter_item, &elem);\n");
2222                }
2223                TypeRef::TypedHandle(_) | TypeRef::Handle => {
2224                    out.push_str(
2225                        "    napi_create_int64(env, (int64_t)(intptr_t)iter_item, &elem);\n",
2226                    );
2227                }
2228                TypeRef::StringUtf8 => {
2229                    out.push_str(
2230                        "    napi_create_string_utf8(env, iter_item, NAPI_AUTO_LENGTH, &elem);\n",
2231                    );
2232                    out.push_str("    weaveffi_free_string(iter_item);\n");
2233                }
2234                TypeRef::Struct(_) | TypeRef::Enum(_) => {
2235                    out.push_str(
2236                        "    napi_create_int64(env, (int64_t)(intptr_t)iter_item, &elem);\n",
2237                    );
2238                }
2239                _ => {
2240                    out.push_str("    napi_create_int64(env, (int64_t)iter_item, &elem);\n");
2241                }
2242            }
2243            out.push_str("    napi_set_element(env, ret, iter_idx++, elem);\n");
2244            out.push_str("  }\n");
2245            out.push_str(&format!("  {iter_type}_destroy(result);\n"));
2246        }
2247    }
2248    out.push_str("  return ret;\n");
2249}
2250
2251fn emit_optional_ret_inner(
2252    out: &mut String,
2253    inner: &TypeRef,
2254    module: &str,
2255    prefix: &str,
2256    structs: &HashMap<String, StructBinding>,
2257) {
2258    match inner {
2259        TypeRef::I32 => {
2260            out.push_str("    napi_create_int32(env, *result, &ret);\n");
2261            out.push_str("    free(result);\n");
2262        }
2263        TypeRef::U32 => {
2264            out.push_str("    napi_create_uint32(env, *result, &ret);\n");
2265            out.push_str("    free(result);\n");
2266        }
2267        TypeRef::I64 => {
2268            out.push_str("    napi_create_int64(env, *result, &ret);\n");
2269            out.push_str("    free(result);\n");
2270        }
2271        TypeRef::F64 => {
2272            out.push_str("    napi_create_double(env, *result, &ret);\n");
2273            out.push_str("    free(result);\n");
2274        }
2275        TypeRef::Bool => {
2276            out.push_str("    napi_get_boolean(env, *result, &ret);\n");
2277            out.push_str("    free(result);\n");
2278        }
2279        TypeRef::TypedHandle(_) | TypeRef::Handle => {
2280            out.push_str("    napi_create_int64(env, (int64_t)(intptr_t)*result, &ret);\n");
2281            out.push_str("    free(result);\n");
2282        }
2283        TypeRef::Enum(_) => {
2284            out.push_str("    napi_create_int32(env, (int32_t)*result, &ret);\n");
2285            out.push_str("    free(result);\n");
2286        }
2287        TypeRef::StringUtf8 => {
2288            out.push_str("    napi_create_string_utf8(env, result, NAPI_AUTO_LENGTH, &ret);\n");
2289            out.push_str("    weaveffi_free_string(result);\n");
2290        }
2291        TypeRef::Struct(name) => {
2292            emit_struct_to_object(
2293                out, "env", name, "result", "ret", module, prefix, structs, "    ", true,
2294            );
2295        }
2296        TypeRef::List(li) => emit_list_ret(out, li, module, prefix, "    ", structs),
2297        _ => out.push_str("    napi_get_null(env, &ret);\n"),
2298    }
2299}
2300
2301fn emit_list_ret(
2302    out: &mut String,
2303    inner: &TypeRef,
2304    module: &str,
2305    prefix: &str,
2306    ind: &str,
2307    structs: &HashMap<String, StructBinding>,
2308) {
2309    out.push_str(&format!(
2310        "{ind}napi_create_array_with_length(env, out_len, &ret);\n"
2311    ));
2312    out.push_str(&format!(
2313        "{ind}for (size_t ret_i = 0; ret_i < out_len; ret_i++) {{\n"
2314    ));
2315    out.push_str(&format!("{ind}  napi_value elem;\n"));
2316    match inner {
2317        TypeRef::I32 => out.push_str(&format!(
2318            "{ind}  napi_create_int32(env, result[ret_i], &elem);\n"
2319        )),
2320        TypeRef::U32 => out.push_str(&format!(
2321            "{ind}  napi_create_uint32(env, result[ret_i], &elem);\n"
2322        )),
2323        TypeRef::I64 => out.push_str(&format!(
2324            "{ind}  napi_create_int64(env, result[ret_i], &elem);\n"
2325        )),
2326        TypeRef::F64 => out.push_str(&format!(
2327            "{ind}  napi_create_double(env, result[ret_i], &elem);\n"
2328        )),
2329        TypeRef::Bool => out.push_str(&format!(
2330            "{ind}  napi_get_boolean(env, result[ret_i], &elem);\n"
2331        )),
2332        TypeRef::TypedHandle(_) | TypeRef::Handle => out.push_str(&format!(
2333            "{ind}  napi_create_int64(env, (int64_t)(intptr_t)result[ret_i], &elem);\n"
2334        )),
2335        TypeRef::StringUtf8 => {
2336            out.push_str(&format!(
2337                "{ind}  napi_create_string_utf8(env, result[ret_i], NAPI_AUTO_LENGTH, &elem);\n"
2338            ));
2339            out.push_str(&format!("{ind}  weaveffi_free_string(result[ret_i]);\n"));
2340        }
2341        TypeRef::Enum(_) => out.push_str(&format!(
2342            "{ind}  napi_create_int32(env, (int32_t)result[ret_i], &elem);\n"
2343        )),
2344        TypeRef::Struct(name) => {
2345            let elem_indent = format!("{ind}  ");
2346            emit_struct_to_object(
2347                out,
2348                "env",
2349                name,
2350                "result[ret_i]",
2351                "elem",
2352                module,
2353                prefix,
2354                structs,
2355                &elem_indent,
2356                true,
2357            );
2358        }
2359        _ => out.push_str(&format!(
2360            "{ind}  napi_create_int64(env, (int64_t)result[ret_i], &elem);\n"
2361        )),
2362    }
2363    out.push_str(&format!(
2364        "{ind}  napi_set_element(env, ret, (uint32_t)ret_i, elem);\n"
2365    ));
2366    out.push_str(&format!("{ind}}}\n"));
2367    out.push_str(&format!("{ind}free(result);\n"));
2368}
2369
2370fn ts_type_for(ty: &TypeRef) -> String {
2371    match ty {
2372        TypeRef::I32 | TypeRef::U32 | TypeRef::I64 | TypeRef::F64 => "number".into(),
2373        TypeRef::Bool => "boolean".into(),
2374        TypeRef::StringUtf8 | TypeRef::BorrowedStr => "string".into(),
2375        TypeRef::Bytes | TypeRef::BorrowedBytes => "Buffer".into(),
2376        TypeRef::Handle => "bigint".into(),
2377        // Structs, enums, and typed handles surface as bare local TS names. A
2378        // cross-module reference (e.g. `handle<Store>` resolved to `kv.Store`)
2379        // must annotate the *local* interface `Store`; the qualified IR name is
2380        // not a declared TS type in this module.
2381        TypeRef::TypedHandle(name) => local_type_name(name).to_string(),
2382        TypeRef::Struct(name) => local_type_name(name).to_string(),
2383        TypeRef::Enum(name) => local_type_name(name).to_string(),
2384        TypeRef::Optional(inner) => format!("{} | null", ts_type_for(inner)),
2385        TypeRef::List(inner) => {
2386            let inner_ts = ts_type_for(inner);
2387            if matches!(inner.as_ref(), TypeRef::Optional(_)) {
2388                format!("({inner_ts})[]")
2389            } else {
2390                format!("{inner_ts}[]")
2391            }
2392        }
2393        TypeRef::Map(k, v) => format!("Record<{}, {}>", ts_type_for(k), ts_type_for(v)),
2394        TypeRef::Iterator(inner) => {
2395            let t = ts_type_for(inner);
2396            format!("{t}[]")
2397        }
2398    }
2399}
2400
2401/// Emits a JSDoc comment at `indent`. Single-line docs collapse to
2402/// `/** text */`; multi-line docs expand to a block with ` * ` prefixed lines.
2403fn emit_doc(out: &mut String, doc: &Option<String>, indent: &str) {
2404    common_emit_doc(out, doc, indent, DocCommentStyle::Javadoc);
2405}
2406
2407/// Emits a JSDoc block for a function: function doc, `@param name desc` for
2408/// each documented parameter, and an optional trailing tag list.
2409fn emit_fn_doc(
2410    out: &mut String,
2411    doc: &Option<String>,
2412    params: &[ParamBinding],
2413    indent: &str,
2414    extra_tags: &[String],
2415) {
2416    let has_param_docs = params.iter().any(|p| p.doc.is_some());
2417    let trimmed_doc = doc.as_ref().map(|d| d.trim()).filter(|d| !d.is_empty());
2418    if trimmed_doc.is_none() && !has_param_docs && extra_tags.is_empty() {
2419        return;
2420    }
2421    out.push_str(indent);
2422    out.push_str("/**\n");
2423    if let Some(d) = trimmed_doc {
2424        for line in d.lines() {
2425            out.push_str(indent);
2426            if line.is_empty() {
2427                out.push_str(" *\n");
2428            } else {
2429                out.push_str(" * ");
2430                out.push_str(line);
2431                out.push('\n');
2432            }
2433        }
2434    }
2435    for p in params {
2436        if let Some(pdoc) = &p.doc {
2437            let pdoc = pdoc.trim();
2438            if pdoc.is_empty() {
2439                continue;
2440            }
2441            let mut lines = pdoc.lines();
2442            if let Some(first) = lines.next() {
2443                out.push_str(indent);
2444                out.push_str(&format!(" * @param {} {}\n", p.name, first));
2445            }
2446            for line in lines {
2447                out.push_str(indent);
2448                if line.is_empty() {
2449                    out.push_str(" *\n");
2450                } else {
2451                    out.push_str(" *   ");
2452                    out.push_str(line);
2453                    out.push('\n');
2454                }
2455            }
2456        }
2457    }
2458    for tag in extra_tags {
2459        out.push_str(indent);
2460        out.push_str(" * ");
2461        out.push_str(tag);
2462        out.push('\n');
2463    }
2464    out.push_str(indent);
2465    out.push_str(" */\n");
2466}
2467
2468fn render_struct_builder_dts(out: &mut String, s: &StructBinding) {
2469    let name = &s.name;
2470    emit_doc(out, &s.doc, "");
2471    out.push_str(&format!("export interface {}Builder {{\n", s.name));
2472    for field in &s.fields {
2473        let method = format!("with{}", field.name.to_upper_camel_case());
2474        let ts = ts_type_for(&field.ty);
2475        emit_doc(out, &field.doc, "  ");
2476        out.push_str(&format!("  {method}(value: {ts}): {name}Builder;\n"));
2477    }
2478    out.push_str(&format!("  build(): {name};\n"));
2479    out.push_str("}\n");
2480}
2481
2482fn render_node_dts(
2483    api: &Api,
2484    prefix: &str,
2485    strip_module_prefix: bool,
2486    input_basename: &str,
2487) -> String {
2488    let model = BindingModel::build(api, prefix);
2489    let mut out = render_prelude(CommentStyle::DoubleSlash, input_basename);
2490    out.push_str("// Generated types for WeaveFFI functions\n");
2491    for m in &model.modules {
2492        for s in &m.structs {
2493            emit_doc(&mut out, &s.doc, "");
2494            out.push_str(&format!("export interface {} {{\n", s.name));
2495            for field in &s.fields {
2496                emit_doc(&mut out, &field.doc, "  ");
2497                out.push_str(&format!("  {}: {};\n", field.name, ts_type_for(&field.ty)));
2498            }
2499            out.push_str("}\n");
2500            if s.builder.is_some() {
2501                render_struct_builder_dts(&mut out, s);
2502            }
2503        }
2504        for e in &m.enums {
2505            emit_doc(&mut out, &e.doc, "");
2506            out.push_str(&format!("export enum {} {{\n", e.name));
2507            for v in &e.variants {
2508                emit_doc(&mut out, &v.doc, "  ");
2509                out.push_str(&format!("  {} = {},\n", v.name, v.value));
2510            }
2511            out.push_str("}\n");
2512        }
2513        out.push_str(&format!("// module {}\n", m.path));
2514        for l in &m.listeners {
2515            let Some(cb) = m.callback(&l.event_callback) else {
2516                continue;
2517            };
2518            let cb_params: Vec<String> = cb
2519                .params
2520                .iter()
2521                .map(|p| format!("{}: {}", p.name, ts_type_for(&p.ty)))
2522                .collect();
2523            let register = wrapper_name(
2524                &m.path,
2525                &format!("register_{}", l.name),
2526                strip_module_prefix,
2527            );
2528            let unregister = wrapper_name(
2529                &m.path,
2530                &format!("unregister_{}", l.name),
2531                strip_module_prefix,
2532            );
2533            emit_doc(&mut out, &l.doc, "");
2534            out.push_str(&format!(
2535                "export function {register}(callback: ({}) => void): number\n",
2536                cb_params.join(", ")
2537            ));
2538            out.push_str(&format!("export function {unregister}(id: number): void\n"));
2539        }
2540        for f in &m.functions {
2541            let params: Vec<String> = f
2542                .params
2543                .iter()
2544                .map(|p| format!("{}: {}", p.name, ts_type_for(&p.ty)))
2545                .collect();
2546            let base_ret = match &f.ret {
2547                Some(ty) => ts_type_for(ty),
2548                None => "void".into(),
2549            };
2550            let ret = if f.is_async {
2551                format!("Promise<{base_ret}>")
2552            } else {
2553                base_ret
2554            };
2555            let ts_name = wrapper_name(&m.path, &f.name, strip_module_prefix);
2556            let mut tags = vec![format!("Maps to C function: {}", f.c_base)];
2557            if let Some(msg) = &f.deprecated {
2558                tags.push(format!("@deprecated {}", msg));
2559            }
2560            emit_fn_doc(&mut out, &f.doc, &f.params, "", &tags);
2561            out.push_str(&format!(
2562                "export function {}({}): {}\n",
2563                ts_name,
2564                params.join(", "),
2565                ret
2566            ));
2567        }
2568    }
2569    out.push('\n');
2570    out.push_str(&render_trailer(CommentStyle::DoubleSlash, "types.d.ts"));
2571    out
2572}
2573
2574#[cfg(test)]
2575mod tests {
2576    use super::*;
2577    use weaveffi_core::codegen::Generator;
2578    use weaveffi_ir::ir::{EnumDef, EnumVariant, Function, Module, Param, StructDef, StructField};
2579
2580    fn make_api(modules: Vec<Module>) -> Api {
2581        Api {
2582            version: "0.3.0".into(),
2583            modules,
2584            generators: None,
2585            package: None,
2586        }
2587    }
2588
2589    fn make_module(name: &str) -> Module {
2590        Module {
2591            name: name.into(),
2592            functions: vec![],
2593            structs: vec![],
2594            enums: vec![],
2595            callbacks: vec![],
2596            listeners: vec![],
2597            errors: None,
2598            modules: vec![],
2599        }
2600    }
2601
2602    #[test]
2603    fn listeners_generate_tsfn_register_unregister() {
2604        use weaveffi_ir::ir::{CallbackDef, ListenerDef};
2605        let api = make_api(vec![Module {
2606            name: "events".into(),
2607            functions: vec![],
2608            structs: vec![],
2609            enums: vec![],
2610            callbacks: vec![CallbackDef {
2611                name: "OnMessage".into(),
2612                doc: None,
2613                params: vec![Param {
2614                    name: "message".into(),
2615                    ty: TypeRef::StringUtf8,
2616                    mutable: false,
2617                    doc: None,
2618                }],
2619            }],
2620            listeners: vec![ListenerDef {
2621                name: "message_listener".into(),
2622                event_callback: "OnMessage".into(),
2623                doc: None,
2624            }],
2625            errors: None,
2626            modules: vec![],
2627        }]);
2628        let dir = tempfile::tempdir().unwrap();
2629        let out = Utf8Path::from_path(dir.path()).unwrap();
2630        NodeGenerator
2631            .generate(&api, out, &NodeConfig::default())
2632            .unwrap();
2633        let addon = std::fs::read_to_string(dir.path().join("node/weaveffi_addon.c")).unwrap();
2634        assert!(
2635            addon.contains("napi_create_threadsafe_function"),
2636            "listeners must use threadsafe functions: {addon}"
2637        );
2638        assert!(
2639            addon.contains("Napi_weaveffi_events_register_message_listener"),
2640            "register N-API fn missing: {addon}"
2641        );
2642        assert!(
2643            addon.contains("Napi_weaveffi_events_unregister_message_listener"),
2644            "unregister N-API fn missing: {addon}"
2645        );
2646        assert!(
2647            addon.contains("napi_call_threadsafe_function(ctx->tsfn, p, napi_tsfn_nonblocking)"),
2648            "trampoline must queue payloads: {addon}"
2649        );
2650        assert!(
2651            addon.contains("napi_unref_threadsafe_function"),
2652            "tsfn must be unref'd so listeners don't pin the loop: {addon}"
2653        );
2654        let dts = std::fs::read_to_string(dir.path().join("node/types.d.ts")).unwrap();
2655        assert!(
2656            dts.contains(
2657                "export function events_register_message_listener(callback: (message: string) => void): number"
2658            ),
2659            "register dts missing: {dts}"
2660        );
2661        assert!(
2662            dts.contains("export function events_unregister_message_listener(id: number): void"),
2663            "unregister dts missing: {dts}"
2664        );
2665    }
2666
2667    #[test]
2668    fn ts_type_for_primitives() {
2669        assert_eq!(ts_type_for(&TypeRef::I32), "number");
2670        assert_eq!(ts_type_for(&TypeRef::Bool), "boolean");
2671        assert_eq!(ts_type_for(&TypeRef::StringUtf8), "string");
2672        assert_eq!(ts_type_for(&TypeRef::Bytes), "Buffer");
2673        assert_eq!(ts_type_for(&TypeRef::Handle), "bigint");
2674    }
2675
2676    #[test]
2677    fn ts_type_for_struct_and_enum() {
2678        assert_eq!(ts_type_for(&TypeRef::Struct("Contact".into())), "Contact");
2679        assert_eq!(ts_type_for(&TypeRef::Enum("Color".into())), "Color");
2680        assert_eq!(
2681            ts_type_for(&TypeRef::TypedHandle("Contact".into())),
2682            "Contact"
2683        );
2684    }
2685
2686    #[test]
2687    fn ts_type_for_cross_module_uses_local_name() {
2688        // A typed handle resolved to a parent-module struct (`kv.Store`) must
2689        // emit the bare local interface name, the only TS type in this module.
2690        assert_eq!(
2691            ts_type_for(&TypeRef::TypedHandle("kv.Store".into())),
2692            "Store"
2693        );
2694        assert_eq!(ts_type_for(&TypeRef::Struct("kv.Store".into())), "Store");
2695        assert_eq!(ts_type_for(&TypeRef::Enum("kv.Kind".into())), "Kind");
2696    }
2697
2698    #[test]
2699    fn ts_type_for_optional() {
2700        let ty = TypeRef::Optional(Box::new(TypeRef::StringUtf8));
2701        assert_eq!(ts_type_for(&ty), "string | null");
2702    }
2703
2704    #[test]
2705    fn ts_type_for_list() {
2706        let ty = TypeRef::List(Box::new(TypeRef::I32));
2707        assert_eq!(ts_type_for(&ty), "number[]");
2708    }
2709
2710    #[test]
2711    fn ts_type_for_list_of_optional() {
2712        let ty = TypeRef::List(Box::new(TypeRef::Optional(Box::new(TypeRef::I32))));
2713        assert_eq!(ts_type_for(&ty), "(number | null)[]");
2714    }
2715
2716    #[test]
2717    fn ts_type_for_map() {
2718        let ty = TypeRef::Map(Box::new(TypeRef::StringUtf8), Box::new(TypeRef::I32));
2719        assert_eq!(ts_type_for(&ty), "Record<string, number>");
2720    }
2721
2722    #[test]
2723    fn ts_type_for_optional_list() {
2724        let ty = TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::I32))));
2725        assert_eq!(ts_type_for(&ty), "number[] | null");
2726    }
2727
2728    #[test]
2729    fn generate_node_dts_with_structs() {
2730        let mut m = make_module("contacts");
2731        m.structs.push(StructDef {
2732            name: "Contact".into(),
2733            doc: None,
2734            fields: vec![
2735                StructField {
2736                    name: "name".into(),
2737                    ty: TypeRef::StringUtf8,
2738                    doc: None,
2739                    default: None,
2740                },
2741                StructField {
2742                    name: "age".into(),
2743                    ty: TypeRef::I32,
2744                    doc: None,
2745                    default: None,
2746                },
2747                StructField {
2748                    name: "active".into(),
2749                    ty: TypeRef::Bool,
2750                    doc: None,
2751                    default: None,
2752                },
2753            ],
2754            builder: false,
2755        });
2756        m.enums.push(EnumDef {
2757            name: "Color".into(),
2758            doc: None,
2759            variants: vec![
2760                EnumVariant {
2761                    name: "Red".into(),
2762                    value: 0,
2763                    doc: None,
2764                },
2765                EnumVariant {
2766                    name: "Green".into(),
2767                    value: 1,
2768                    doc: None,
2769                },
2770                EnumVariant {
2771                    name: "Blue".into(),
2772                    value: 2,
2773                    doc: None,
2774                },
2775            ],
2776        });
2777        m.functions.push(Function {
2778            name: "get_contact".into(),
2779            params: vec![Param {
2780                name: "id".into(),
2781                ty: TypeRef::I32,
2782                mutable: false,
2783                doc: None,
2784            }],
2785            returns: Some(TypeRef::Optional(Box::new(TypeRef::Struct(
2786                "Contact".into(),
2787            )))),
2788            doc: None,
2789            r#async: false,
2790            cancellable: false,
2791            deprecated: None,
2792            since: None,
2793        });
2794        m.functions.push(Function {
2795            name: "list_contacts".into(),
2796            params: vec![],
2797            returns: Some(TypeRef::List(Box::new(TypeRef::Struct("Contact".into())))),
2798            doc: None,
2799            r#async: false,
2800            cancellable: false,
2801            deprecated: None,
2802            since: None,
2803        });
2804
2805        let dts = render_node_dts(&make_api(vec![m]), "weaveffi", true, "weaveffi.yml");
2806
2807        assert!(dts.contains("export interface Contact {"));
2808        assert!(dts.contains("  name: string;"));
2809        assert!(dts.contains("  age: number;"));
2810        assert!(dts.contains("  active: boolean;"));
2811        assert!(dts.contains("export enum Color {"));
2812        assert!(dts.contains("  Red = 0,"));
2813        assert!(dts.contains("  Green = 1,"));
2814        assert!(dts.contains("  Blue = 2,"));
2815        assert!(dts.contains("export function get_contact(id: number): Contact | null"));
2816        assert!(dts.contains("export function list_contacts(): Contact[]"));
2817
2818        let iface_pos = dts.find("export interface Contact").unwrap();
2819        let enum_pos = dts.find("export enum Color").unwrap();
2820        let fn_pos = dts.find("export function get_contact").unwrap();
2821        assert!(
2822            iface_pos < fn_pos,
2823            "interface should appear before functions"
2824        );
2825        assert!(enum_pos < fn_pos, "enum should appear before functions");
2826    }
2827
2828    #[test]
2829    fn node_generates_binding_gyp() {
2830        let api = make_api(vec![{
2831            let mut m = make_module("math");
2832            m.functions.push(Function {
2833                name: "add".into(),
2834                params: vec![
2835                    Param {
2836                        name: "a".into(),
2837                        ty: TypeRef::I32,
2838                        mutable: false,
2839                        doc: None,
2840                    },
2841                    Param {
2842                        name: "b".into(),
2843                        ty: TypeRef::I32,
2844                        mutable: false,
2845                        doc: None,
2846                    },
2847                ],
2848                returns: Some(TypeRef::I32),
2849                doc: None,
2850                r#async: false,
2851                cancellable: false,
2852                deprecated: None,
2853                since: None,
2854            });
2855            m
2856        }]);
2857
2858        let tmp = std::env::temp_dir().join("weaveffi_test_node_binding_gyp");
2859        let _ = std::fs::remove_dir_all(&tmp);
2860        std::fs::create_dir_all(&tmp).unwrap();
2861        let out_dir = Utf8Path::from_path(&tmp).expect("temp dir is valid UTF-8");
2862
2863        NodeGenerator
2864            .generate(&api, out_dir, &NodeConfig::default())
2865            .unwrap();
2866
2867        let gyp = std::fs::read_to_string(tmp.join("node").join("binding.gyp")).unwrap();
2868        assert!(
2869            gyp.contains("\"target_name\": \"weaveffi\""),
2870            "missing target_name: {gyp}"
2871        );
2872        assert!(
2873            gyp.contains("weaveffi_addon.c"),
2874            "missing source file: {gyp}"
2875        );
2876
2877        let addon = std::fs::read_to_string(tmp.join("node").join("weaveffi_addon.c")).unwrap();
2878        assert!(
2879            addon.contains("napi_value Init("),
2880            "missing Init function: {addon}"
2881        );
2882        assert!(
2883            addon.contains("weaveffi_math_add"),
2884            "missing C ABI call: {addon}"
2885        );
2886        assert!(
2887            addon.contains("napi_get_cb_info"),
2888            "missing napi_get_cb_info call: {addon}"
2889        );
2890
2891        let pkg = std::fs::read_to_string(tmp.join("node").join("package.json")).unwrap();
2892        assert!(pkg.contains("\"gypfile\": true"), "missing gypfile: {pkg}");
2893        assert!(
2894            pkg.contains("node-gyp rebuild"),
2895            "missing install script: {pkg}"
2896        );
2897
2898        let _ = std::fs::remove_dir_all(&tmp);
2899    }
2900
2901    #[test]
2902    fn generate_node_dts_with_structs_and_enums() {
2903        let api = make_api(vec![Module {
2904            name: "contacts".to_string(),
2905            functions: vec![
2906                Function {
2907                    name: "get_contact".to_string(),
2908                    params: vec![Param {
2909                        name: "id".to_string(),
2910                        ty: TypeRef::I32,
2911                        mutable: false,
2912                        doc: None,
2913                    }],
2914                    returns: Some(TypeRef::Optional(Box::new(TypeRef::Struct(
2915                        "Contact".into(),
2916                    )))),
2917                    doc: None,
2918                    r#async: false,
2919                    cancellable: false,
2920                    deprecated: None,
2921                    since: None,
2922                },
2923                Function {
2924                    name: "list_contacts".to_string(),
2925                    params: vec![],
2926                    returns: Some(TypeRef::List(Box::new(TypeRef::Struct("Contact".into())))),
2927                    doc: None,
2928                    r#async: false,
2929                    cancellable: false,
2930                    deprecated: None,
2931                    since: None,
2932                },
2933                Function {
2934                    name: "set_favorite_color".to_string(),
2935                    params: vec![
2936                        Param {
2937                            name: "contact_id".to_string(),
2938                            ty: TypeRef::I32,
2939                            mutable: false,
2940                            doc: None,
2941                        },
2942                        Param {
2943                            name: "color".to_string(),
2944                            ty: TypeRef::Optional(Box::new(TypeRef::Enum("Color".into()))),
2945                            mutable: false,
2946                            doc: None,
2947                        },
2948                    ],
2949                    returns: None,
2950                    doc: None,
2951                    r#async: false,
2952                    cancellable: false,
2953                    deprecated: None,
2954                    since: None,
2955                },
2956                Function {
2957                    name: "get_tags".to_string(),
2958                    params: vec![Param {
2959                        name: "contact_id".to_string(),
2960                        ty: TypeRef::I32,
2961                        mutable: false,
2962                        doc: None,
2963                    }],
2964                    returns: Some(TypeRef::List(Box::new(TypeRef::StringUtf8))),
2965                    doc: None,
2966                    r#async: false,
2967                    cancellable: false,
2968                    deprecated: None,
2969                    since: None,
2970                },
2971            ],
2972            structs: vec![StructDef {
2973                name: "Contact".to_string(),
2974                doc: None,
2975                fields: vec![
2976                    StructField {
2977                        name: "name".to_string(),
2978                        ty: TypeRef::StringUtf8,
2979                        doc: None,
2980                        default: None,
2981                    },
2982                    StructField {
2983                        name: "email".to_string(),
2984                        ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
2985                        doc: None,
2986                        default: None,
2987                    },
2988                    StructField {
2989                        name: "tags".to_string(),
2990                        ty: TypeRef::List(Box::new(TypeRef::StringUtf8)),
2991                        doc: None,
2992                        default: None,
2993                    },
2994                ],
2995                builder: false,
2996            }],
2997            enums: vec![EnumDef {
2998                name: "Color".to_string(),
2999                doc: None,
3000                variants: vec![
3001                    EnumVariant {
3002                        name: "Red".to_string(),
3003                        value: 0,
3004                        doc: None,
3005                    },
3006                    EnumVariant {
3007                        name: "Green".to_string(),
3008                        value: 1,
3009                        doc: None,
3010                    },
3011                    EnumVariant {
3012                        name: "Blue".to_string(),
3013                        value: 2,
3014                        doc: None,
3015                    },
3016                ],
3017            }],
3018            callbacks: vec![],
3019            listeners: vec![],
3020            errors: None,
3021            modules: vec![],
3022        }]);
3023
3024        let tmp = std::env::temp_dir().join("weaveffi_test_node_structs_and_enums");
3025        let _ = std::fs::remove_dir_all(&tmp);
3026        std::fs::create_dir_all(&tmp).unwrap();
3027        let out_dir = Utf8Path::from_path(&tmp).expect("temp dir is valid UTF-8");
3028
3029        NodeGenerator
3030            .generate(
3031                &api,
3032                out_dir,
3033                &NodeConfig {
3034                    strip_module_prefix: true,
3035                    ..NodeConfig::default()
3036                },
3037            )
3038            .unwrap();
3039
3040        let dts = std::fs::read_to_string(tmp.join("node").join("types.d.ts")).unwrap();
3041
3042        assert!(
3043            dts.contains("export interface Contact {"),
3044            "missing Contact interface: {dts}"
3045        );
3046        assert!(dts.contains("  name: string;"), "missing name field: {dts}");
3047        assert!(
3048            dts.contains("  email: string | null;"),
3049            "missing optional email field: {dts}"
3050        );
3051        assert!(
3052            dts.contains("  tags: string[];"),
3053            "missing list tags field: {dts}"
3054        );
3055
3056        assert!(
3057            dts.contains("export enum Color {"),
3058            "missing Color enum: {dts}"
3059        );
3060        assert!(dts.contains("  Red = 0,"), "missing Red variant: {dts}");
3061        assert!(dts.contains("  Green = 1,"), "missing Green variant: {dts}");
3062        assert!(dts.contains("  Blue = 2,"), "missing Blue variant: {dts}");
3063
3064        assert!(
3065            dts.contains("export function get_contact(id: number): Contact | null"),
3066            "missing get_contact with optional return: {dts}"
3067        );
3068        assert!(
3069            dts.contains("export function list_contacts(): Contact[]"),
3070            "missing list_contacts with list return: {dts}"
3071        );
3072        assert!(
3073            dts.contains(
3074                "export function set_favorite_color(contact_id: number, color: Color | null): void"
3075            ),
3076            "missing set_favorite_color with optional enum param: {dts}"
3077        );
3078        assert!(
3079            dts.contains("export function get_tags(contact_id: number): string[]"),
3080            "missing get_tags with list return: {dts}"
3081        );
3082
3083        let iface_pos = dts.find("export interface Contact").unwrap();
3084        let enum_pos = dts.find("export enum Color").unwrap();
3085        let fn_pos = dts.find("export function get_contact").unwrap();
3086        assert!(
3087            iface_pos < fn_pos,
3088            "interface should appear before functions"
3089        );
3090        assert!(enum_pos < fn_pos, "enum should appear before functions");
3091
3092        let _ = std::fs::remove_dir_all(&tmp);
3093    }
3094
3095    #[test]
3096    fn node_custom_package_name() {
3097        let api = make_api(vec![make_module("math")]);
3098
3099        let tmp = std::env::temp_dir().join("weaveffi_test_node_custom_pkg");
3100        let _ = std::fs::remove_dir_all(&tmp);
3101        std::fs::create_dir_all(&tmp).unwrap();
3102        let out_dir = Utf8Path::from_path(&tmp).expect("temp dir is valid UTF-8");
3103
3104        let config = NodeConfig {
3105            package_name: Some("@myorg/cool-lib".into()),
3106            ..NodeConfig::default()
3107        };
3108        NodeGenerator.generate(&api, out_dir, &config).unwrap();
3109
3110        let pkg = std::fs::read_to_string(tmp.join("node").join("package.json")).unwrap();
3111        assert!(
3112            pkg.contains("\"name\": \"@myorg/cool-lib\""),
3113            "package.json should use custom name: {pkg}"
3114        );
3115        assert!(
3116            !pkg.contains("\"name\": \"weaveffi\""),
3117            "package.json should not contain default name: {pkg}"
3118        );
3119
3120        let _ = std::fs::remove_dir_all(&tmp);
3121    }
3122
3123    #[test]
3124    fn node_dts_has_jsdoc() {
3125        let api = make_api(vec![{
3126            let mut m = make_module("math");
3127            m.functions.push(Function {
3128                name: "add".into(),
3129                params: vec![
3130                    Param {
3131                        name: "a".into(),
3132                        ty: TypeRef::I32,
3133                        mutable: false,
3134                        doc: None,
3135                    },
3136                    Param {
3137                        name: "b".into(),
3138                        ty: TypeRef::I32,
3139                        mutable: false,
3140                        doc: None,
3141                    },
3142                ],
3143                returns: Some(TypeRef::I32),
3144                doc: None,
3145                r#async: false,
3146                cancellable: false,
3147                deprecated: None,
3148                since: None,
3149            });
3150            m.functions.push(Function {
3151                name: "subtract".into(),
3152                params: vec![
3153                    Param {
3154                        name: "a".into(),
3155                        ty: TypeRef::I32,
3156                        mutable: false,
3157                        doc: None,
3158                    },
3159                    Param {
3160                        name: "b".into(),
3161                        ty: TypeRef::I32,
3162                        mutable: false,
3163                        doc: None,
3164                    },
3165                ],
3166                returns: Some(TypeRef::I32),
3167                doc: None,
3168                r#async: false,
3169                cancellable: false,
3170                deprecated: None,
3171                since: None,
3172            });
3173            m
3174        }]);
3175
3176        let dts = render_node_dts(&api, "weaveffi", true, "weaveffi.yml");
3177
3178        assert!(
3179            dts.contains("Maps to C function: weaveffi_math_add"),
3180            "missing JSDoc for add: {dts}"
3181        );
3182        assert!(
3183            dts.contains("Maps to C function: weaveffi_math_subtract"),
3184            "missing JSDoc for subtract: {dts}"
3185        );
3186    }
3187
3188    #[test]
3189    fn node_addon_has_no_todo() {
3190        let api = make_api(vec![{
3191            let mut m = make_module("math");
3192            m.functions.push(Function {
3193                name: "add".into(),
3194                params: vec![
3195                    Param {
3196                        name: "a".into(),
3197                        ty: TypeRef::I32,
3198                        mutable: false,
3199                        doc: None,
3200                    },
3201                    Param {
3202                        name: "b".into(),
3203                        ty: TypeRef::I32,
3204                        mutable: false,
3205                        doc: None,
3206                    },
3207                ],
3208                returns: Some(TypeRef::I32),
3209                doc: None,
3210                r#async: false,
3211                cancellable: false,
3212                deprecated: None,
3213                since: None,
3214            });
3215            m
3216        }]);
3217        let addon = render_addon_c(&api, "weaveffi", true, "weaveffi.yml");
3218        assert!(
3219            !addon.contains("// TODO: implement"),
3220            "generated addon.c should not contain TODO comments: {addon}"
3221        );
3222    }
3223
3224    #[test]
3225    fn node_addon_extracts_args() {
3226        let api = make_api(vec![{
3227            let mut m = make_module("math");
3228            m.functions.push(Function {
3229                name: "add".into(),
3230                params: vec![
3231                    Param {
3232                        name: "a".into(),
3233                        ty: TypeRef::I32,
3234                        mutable: false,
3235                        doc: None,
3236                    },
3237                    Param {
3238                        name: "b".into(),
3239                        ty: TypeRef::I32,
3240                        mutable: false,
3241                        doc: None,
3242                    },
3243                ],
3244                returns: Some(TypeRef::I32),
3245                doc: None,
3246                r#async: false,
3247                cancellable: false,
3248                deprecated: None,
3249                since: None,
3250            });
3251            m
3252        }]);
3253        let addon = render_addon_c(&api, "weaveffi", true, "weaveffi.yml");
3254        assert!(
3255            addon.contains("napi_get_cb_info"),
3256            "generated addon.c should call napi_get_cb_info: {addon}"
3257        );
3258    }
3259
3260    #[test]
3261    fn node_addon_frees_strings() {
3262        let api = make_api(vec![{
3263            let mut m = make_module("greet");
3264            m.functions.push(Function {
3265                name: "hello".into(),
3266                params: vec![Param {
3267                    name: "name".into(),
3268                    ty: TypeRef::StringUtf8,
3269                    mutable: false,
3270                    doc: None,
3271                }],
3272                returns: Some(TypeRef::StringUtf8),
3273                doc: None,
3274                r#async: false,
3275                cancellable: false,
3276                deprecated: None,
3277                since: None,
3278            });
3279            m
3280        }]);
3281        let addon = render_addon_c(&api, "weaveffi", true, "weaveffi.yml");
3282        assert!(
3283            addon.contains("weaveffi_free_string(result)"),
3284            "generated addon should free returned strings: {addon}"
3285        );
3286        assert!(
3287            addon.contains("#include <string.h>"),
3288            "generated addon should include string.h: {addon}"
3289        );
3290        assert!(
3291            addon.contains("#include <stdlib.h>"),
3292            "generated addon should include stdlib.h: {addon}"
3293        );
3294        assert!(
3295            addon.contains("weaveffi_error_clear(&err)"),
3296            "generated addon should clear errors: {addon}"
3297        );
3298    }
3299
3300    #[test]
3301    fn node_custom_prefix_threads_to_user_symbols() {
3302        let api = make_api(vec![{
3303            let mut m = make_module("greet");
3304            m.functions.push(Function {
3305                name: "hello".into(),
3306                params: vec![Param {
3307                    name: "name".into(),
3308                    ty: TypeRef::StringUtf8,
3309                    mutable: false,
3310                    doc: None,
3311                }],
3312                returns: Some(TypeRef::StringUtf8),
3313                doc: None,
3314                r#async: false,
3315                cancellable: false,
3316                deprecated: None,
3317                since: None,
3318            });
3319            m
3320        }]);
3321
3322        let config = NodeConfig {
3323            prefix: Some("myffi".into()),
3324            ..NodeConfig::default()
3325        };
3326
3327        let tmp = std::env::temp_dir().join("weaveffi_test_node_custom_prefix");
3328        let _ = std::fs::remove_dir_all(&tmp);
3329        std::fs::create_dir_all(&tmp).unwrap();
3330        let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8");
3331
3332        NodeGenerator.generate(&api, out_dir, &config).unwrap();
3333
3334        // The output file name is a fixed library artifact name, not the ABI
3335        // prefix, so it stays `weaveffi_addon.c` regardless of `prefix`.
3336        let addon = std::fs::read_to_string(tmp.join("node/weaveffi_addon.c")).unwrap();
3337
3338        // User symbols pick up the configured ABI prefix.
3339        assert!(
3340            addon.contains("myffi_greet_hello"),
3341            "addon should call the prefixed user symbol myffi_greet_hello: {addon}"
3342        );
3343        assert!(
3344            !addon.contains("weaveffi_greet_hello"),
3345            "addon must not emit the hard-coded weaveffi_ user symbol: {addon}"
3346        );
3347        assert!(
3348            addon.contains("#include \"myffi.h\""),
3349            "addon should include the prefixed header myffi.h: {addon}"
3350        );
3351
3352        // Runtime ABI helpers are supplied by weaveffi-abi and stay literal.
3353        assert!(
3354            addon.contains("weaveffi_error"),
3355            "runtime weaveffi_error must remain literal: {addon}"
3356        );
3357        assert!(
3358            addon.contains("weaveffi_free_string"),
3359            "runtime weaveffi_free_string must remain literal: {addon}"
3360        );
3361
3362        let _ = std::fs::remove_dir_all(&tmp);
3363    }
3364
3365    #[test]
3366    fn node_addon_checks_error() {
3367        let api = make_api(vec![{
3368            let mut m = make_module("math");
3369            m.functions.push(Function {
3370                name: "add".into(),
3371                params: vec![
3372                    Param {
3373                        name: "a".into(),
3374                        ty: TypeRef::I32,
3375                        mutable: false,
3376                        doc: None,
3377                    },
3378                    Param {
3379                        name: "b".into(),
3380                        ty: TypeRef::I32,
3381                        mutable: false,
3382                        doc: None,
3383                    },
3384                ],
3385                returns: Some(TypeRef::I32),
3386                doc: None,
3387                r#async: false,
3388                cancellable: false,
3389                deprecated: None,
3390                since: None,
3391            });
3392            m
3393        }]);
3394        let addon = render_addon_c(&api, "weaveffi", true, "weaveffi.yml");
3395        assert!(
3396            addon.contains("err.code"),
3397            "generated addon.c should check err.code: {addon}"
3398        );
3399    }
3400
3401    #[test]
3402    fn node_strip_module_prefix() {
3403        let api = make_api(vec![{
3404            let mut m = make_module("contacts");
3405            m.functions.push(Function {
3406                name: "create_contact".into(),
3407                params: vec![Param {
3408                    name: "name".into(),
3409                    ty: TypeRef::StringUtf8,
3410                    mutable: false,
3411                    doc: None,
3412                }],
3413                returns: Some(TypeRef::I32),
3414                doc: None,
3415                r#async: false,
3416                cancellable: false,
3417                deprecated: None,
3418                since: None,
3419            });
3420            m
3421        }]);
3422
3423        let config = NodeConfig {
3424            strip_module_prefix: true,
3425            ..NodeConfig::default()
3426        };
3427
3428        let tmp = std::env::temp_dir().join("weaveffi_test_node_strip_prefix");
3429        let _ = std::fs::remove_dir_all(&tmp);
3430        std::fs::create_dir_all(&tmp).unwrap();
3431        let out_dir = Utf8Path::from_path(&tmp).expect("valid UTF-8");
3432
3433        NodeGenerator.generate(&api, out_dir, &config).unwrap();
3434
3435        let dts = std::fs::read_to_string(tmp.join("node/types.d.ts")).unwrap();
3436        assert!(
3437            dts.contains("export function create_contact("),
3438            "stripped name should be create_contact: {dts}"
3439        );
3440        assert!(
3441            !dts.contains("export function contacts_create_contact("),
3442            "should not contain module-prefixed name: {dts}"
3443        );
3444
3445        let addon = std::fs::read_to_string(tmp.join("node/weaveffi_addon.c")).unwrap();
3446        assert!(
3447            addon.contains("\"create_contact\""),
3448            "JS export name should be stripped: {addon}"
3449        );
3450        assert!(
3451            addon.contains("weaveffi_contacts_create_contact"),
3452            "C ABI call should still use full name: {addon}"
3453        );
3454
3455        let no_strip = NodeConfig::default();
3456        let tmp2 = std::env::temp_dir().join("weaveffi_test_node_no_strip_prefix");
3457        let _ = std::fs::remove_dir_all(&tmp2);
3458        std::fs::create_dir_all(&tmp2).unwrap();
3459        let out_dir2 = Utf8Path::from_path(&tmp2).expect("valid UTF-8");
3460
3461        NodeGenerator.generate(&api, out_dir2, &no_strip).unwrap();
3462
3463        let dts2 = std::fs::read_to_string(tmp2.join("node/types.d.ts")).unwrap();
3464        assert!(
3465            dts2.contains("export function contacts_create_contact("),
3466            "default should use module-prefixed name: {dts2}"
3467        );
3468
3469        let _ = std::fs::remove_dir_all(&tmp);
3470        let _ = std::fs::remove_dir_all(&tmp2);
3471    }
3472
3473    #[test]
3474    fn node_typed_handle_type() {
3475        let api = make_api(vec![{
3476            let mut m = make_module("contacts");
3477            m.structs.push(StructDef {
3478                name: "Contact".into(),
3479                doc: None,
3480                fields: vec![StructField {
3481                    name: "name".into(),
3482                    ty: TypeRef::StringUtf8,
3483                    doc: None,
3484                    default: None,
3485                }],
3486                builder: false,
3487            });
3488            m.functions.push(Function {
3489                name: "get_info".into(),
3490                params: vec![Param {
3491                    name: "contact".into(),
3492                    ty: TypeRef::TypedHandle("Contact".into()),
3493                    mutable: false,
3494                    doc: None,
3495                }],
3496                returns: None,
3497                doc: None,
3498                r#async: false,
3499                cancellable: false,
3500                deprecated: None,
3501                since: None,
3502            });
3503            m
3504        }]);
3505        let dts = render_node_dts(&api, "weaveffi", true, "weaveffi.yml");
3506        assert!(
3507            dts.contains("contact: Contact"),
3508            "TypedHandle should use class type not bigint: {dts}"
3509        );
3510    }
3511
3512    #[test]
3513    fn node_deeply_nested_optional() {
3514        let api = make_api(vec![Module {
3515            name: "edge".into(),
3516            functions: vec![Function {
3517                name: "process".into(),
3518                params: vec![Param {
3519                    name: "data".into(),
3520                    ty: TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::Optional(
3521                        Box::new(TypeRef::Struct("Contact".into())),
3522                    ))))),
3523                    mutable: false,
3524                    doc: None,
3525                }],
3526                returns: None,
3527                doc: None,
3528                r#async: false,
3529                cancellable: false,
3530                deprecated: None,
3531                since: None,
3532            }],
3533            structs: vec![StructDef {
3534                name: "Contact".into(),
3535                doc: None,
3536                fields: vec![StructField {
3537                    name: "name".into(),
3538                    ty: TypeRef::StringUtf8,
3539                    doc: None,
3540                    default: None,
3541                }],
3542                builder: false,
3543            }],
3544            enums: vec![],
3545            callbacks: vec![],
3546            listeners: vec![],
3547            errors: None,
3548            modules: vec![],
3549        }]);
3550        let dts = render_node_dts(&api, "weaveffi", true, "weaveffi.yml");
3551        assert!(
3552            dts.contains("(Contact | null)[] | null"),
3553            "should contain deeply nested optional type: {dts}"
3554        );
3555    }
3556
3557    #[test]
3558    fn node_map_of_lists() {
3559        let api = make_api(vec![Module {
3560            name: "edge".into(),
3561            functions: vec![Function {
3562                name: "process".into(),
3563                params: vec![Param {
3564                    name: "scores".into(),
3565                    ty: TypeRef::Map(
3566                        Box::new(TypeRef::StringUtf8),
3567                        Box::new(TypeRef::List(Box::new(TypeRef::I32))),
3568                    ),
3569                    mutable: false,
3570                    doc: None,
3571                }],
3572                returns: None,
3573                doc: None,
3574                r#async: false,
3575                cancellable: false,
3576                deprecated: None,
3577                since: None,
3578            }],
3579            structs: vec![],
3580            enums: vec![],
3581            callbacks: vec![],
3582            listeners: vec![],
3583            errors: None,
3584            modules: vec![],
3585        }]);
3586        let dts = render_node_dts(&api, "weaveffi", true, "weaveffi.yml");
3587        assert!(
3588            dts.contains("Record<string, number[]>"),
3589            "should contain map of lists type: {dts}"
3590        );
3591    }
3592
3593    #[test]
3594    fn node_enum_keyed_map() {
3595        let api = make_api(vec![Module {
3596            name: "edge".into(),
3597            functions: vec![Function {
3598                name: "process".into(),
3599                params: vec![Param {
3600                    name: "contacts".into(),
3601                    ty: TypeRef::Map(
3602                        Box::new(TypeRef::Enum("Color".into())),
3603                        Box::new(TypeRef::Struct("Contact".into())),
3604                    ),
3605                    mutable: false,
3606                    doc: None,
3607                }],
3608                returns: None,
3609                doc: None,
3610                r#async: false,
3611                cancellable: false,
3612                deprecated: None,
3613                since: None,
3614            }],
3615            structs: vec![StructDef {
3616                name: "Contact".into(),
3617                doc: None,
3618                fields: vec![StructField {
3619                    name: "name".into(),
3620                    ty: TypeRef::StringUtf8,
3621                    doc: None,
3622                    default: None,
3623                }],
3624                builder: false,
3625            }],
3626            enums: vec![EnumDef {
3627                name: "Color".into(),
3628                doc: None,
3629                variants: vec![
3630                    EnumVariant {
3631                        name: "Red".into(),
3632                        value: 0,
3633                        doc: None,
3634                    },
3635                    EnumVariant {
3636                        name: "Green".into(),
3637                        value: 1,
3638                        doc: None,
3639                    },
3640                    EnumVariant {
3641                        name: "Blue".into(),
3642                        value: 2,
3643                        doc: None,
3644                    },
3645                ],
3646            }],
3647            callbacks: vec![],
3648            listeners: vec![],
3649            errors: None,
3650            modules: vec![],
3651        }]);
3652        let dts = render_node_dts(&api, "weaveffi", true, "weaveffi.yml");
3653        assert!(
3654            dts.contains("Record<Color, Contact>"),
3655            "should contain enum-keyed map type: {dts}"
3656        );
3657    }
3658
3659    #[test]
3660    fn node_no_double_free_on_error() {
3661        let api = make_api(vec![{
3662            let mut m = make_module("contacts");
3663            m.structs.push(StructDef {
3664                name: "Contact".into(),
3665                doc: None,
3666                fields: vec![StructField {
3667                    name: "name".into(),
3668                    ty: TypeRef::StringUtf8,
3669                    doc: None,
3670                    default: None,
3671                }],
3672                builder: false,
3673            });
3674            m.functions.push(Function {
3675                name: "find_contact".into(),
3676                params: vec![Param {
3677                    name: "name".into(),
3678                    ty: TypeRef::StringUtf8,
3679                    mutable: false,
3680                    doc: None,
3681                }],
3682                returns: Some(TypeRef::Struct("Contact".into())),
3683                doc: None,
3684                r#async: false,
3685                cancellable: false,
3686                deprecated: None,
3687                since: None,
3688            });
3689            m
3690        }]);
3691        let addon = render_addon_c(&api, "weaveffi", true, "weaveffi.yml");
3692        assert!(
3693            addon.contains("free(name)"),
3694            "malloc'd JS string copy should be freed after the C call: {addon}"
3695        );
3696        assert!(
3697            !addon.contains("weaveffi_free_string(name)"),
3698            "input string param must not use weaveffi_free_string: {addon}"
3699        );
3700        let free_pos = addon
3701            .find("free(name)")
3702            .expect("free(name) should be present");
3703        let err_pos = addon
3704            .find("if (err.code != 0)")
3705            .expect("err.code check should be present");
3706        assert!(
3707            free_pos < err_pos,
3708            "cleanup should run before error check: free at {free_pos}, err at {err_pos}"
3709        );
3710        let err_block_start = addon
3711            .find("  if (err.code != 0) {\n")
3712            .expect("error if block should be present");
3713        let after_err = &addon[err_block_start..];
3714        let err_block_end_rel = after_err
3715            .find("  }\n  napi_value ret;")
3716            .expect("napi_value ret should follow error block");
3717        let err_block = &addon[err_block_start..err_block_start + err_block_end_rel];
3718        assert!(
3719            !err_block.contains("result"),
3720            "error path should not touch result before return NULL: {err_block}"
3721        );
3722    }
3723
3724    #[test]
3725    fn node_null_check_on_optional_return() {
3726        let api = make_api(vec![{
3727            let mut m = make_module("contacts");
3728            m.structs.push(StructDef {
3729                name: "Contact".into(),
3730                doc: None,
3731                fields: vec![StructField {
3732                    name: "name".into(),
3733                    ty: TypeRef::StringUtf8,
3734                    doc: None,
3735                    default: None,
3736                }],
3737                builder: false,
3738            });
3739            m.functions.push(Function {
3740                name: "find_contact".into(),
3741                params: vec![Param {
3742                    name: "id".into(),
3743                    ty: TypeRef::I32,
3744                    mutable: false,
3745                    doc: None,
3746                }],
3747                returns: Some(TypeRef::Optional(Box::new(TypeRef::Struct(
3748                    "Contact".into(),
3749                )))),
3750                doc: None,
3751                r#async: false,
3752                cancellable: false,
3753                deprecated: None,
3754                since: None,
3755            });
3756            m
3757        }]);
3758        let addon = render_addon_c(&api, "weaveffi", true, "weaveffi.yml");
3759        assert!(
3760            addon.contains("if (result == NULL)"),
3761            "optional struct return should null-check before wrapping: {addon}"
3762        );
3763        assert!(
3764            addon.contains("napi_get_null"),
3765            "optional absent should return JS null via napi_get_null: {addon}"
3766        );
3767    }
3768
3769    #[test]
3770    fn node_async_returns_promise() {
3771        let api = make_api(vec![{
3772            let mut m = make_module("tasks");
3773            m.functions.push(Function {
3774                name: "run".into(),
3775                params: vec![Param {
3776                    name: "id".into(),
3777                    ty: TypeRef::I32,
3778                    mutable: false,
3779                    doc: None,
3780                }],
3781                returns: Some(TypeRef::StringUtf8),
3782                doc: None,
3783                r#async: true,
3784                cancellable: false,
3785                deprecated: None,
3786                since: None,
3787            });
3788            m.functions.push(Function {
3789                name: "fire_and_forget".into(),
3790                params: vec![],
3791                returns: None,
3792                doc: None,
3793                r#async: true,
3794                cancellable: false,
3795                deprecated: None,
3796                since: None,
3797            });
3798            m
3799        }]);
3800        let dts = render_node_dts(&api, "weaveffi", true, "weaveffi.yml");
3801        assert!(
3802            dts.contains("Promise<"),
3803            "async function should return Promise in .d.ts: {dts}"
3804        );
3805        assert!(
3806            dts.contains("): Promise<string>"),
3807            "async string return should be Promise<string>: {dts}"
3808        );
3809        assert!(
3810            dts.contains("): Promise<void>"),
3811            "async void return should be Promise<void>: {dts}"
3812        );
3813    }
3814
3815    #[test]
3816    fn node_addon_creates_promise() {
3817        let api = make_api(vec![{
3818            let mut m = make_module("tasks");
3819            m.functions.push(Function {
3820                name: "run".into(),
3821                params: vec![Param {
3822                    name: "id".into(),
3823                    ty: TypeRef::I32,
3824                    mutable: false,
3825                    doc: None,
3826                }],
3827                returns: Some(TypeRef::I32),
3828                doc: None,
3829                r#async: true,
3830                cancellable: false,
3831                deprecated: None,
3832                since: None,
3833            });
3834            m
3835        }]);
3836        let addon = render_addon_c(&api, "weaveffi", true, "weaveffi.yml");
3837        assert!(
3838            addon.contains("napi_create_promise"),
3839            "async addon should call napi_create_promise: {addon}"
3840        );
3841        assert!(
3842            addon.contains("napi_resolve_deferred"),
3843            "async callback should call napi_resolve_deferred: {addon}"
3844        );
3845        assert!(
3846            addon.contains("napi_reject_deferred"),
3847            "async callback should call napi_reject_deferred: {addon}"
3848        );
3849        assert!(
3850            addon.contains("weaveffi_tasks_run_napi_actx"),
3851            "async addon should define per-fn async context struct: {addon}"
3852        );
3853        assert!(
3854            addon.contains("weaveffi_tasks_run_async("),
3855            "async addon should call the _async C function: {addon}"
3856        );
3857        assert!(
3858            addon.contains("weaveffi_tasks_run_napi_cb"),
3859            "async addon should define the callback: {addon}"
3860        );
3861        // The completion callback may fire on any producer thread, so it must
3862        // queue through a threadsafe function instead of touching napi_env.
3863        assert!(
3864            addon.contains("napi_call_threadsafe_function(ctx->tsfn, ctx, napi_tsfn_blocking)"),
3865            "completion callback must hop to the JS thread via tsfn: {addon}"
3866        );
3867        assert!(
3868            !addon.contains("napi_resolve_deferred(ctx->env"),
3869            "deferred must never be settled from the producer thread: {addon}"
3870        );
3871    }
3872
3873    /// The N-API deferred is created with `napi_create_promise` and settled
3874    /// (on the JS thread) by exactly one of `napi_resolve_deferred` /
3875    /// `napi_reject_deferred`. The per-fn async context that carries the
3876    /// deferred + threadsafe function across threads must be allocated once
3877    /// and freed exactly once, and the tsfn released exactly once.
3878    #[test]
3879    fn node_async_pins_callback_for_lifetime() {
3880        let api = make_api(vec![{
3881            let mut m = make_module("tasks");
3882            m.functions.push(Function {
3883                name: "run".into(),
3884                params: vec![Param {
3885                    name: "id".into(),
3886                    ty: TypeRef::I32,
3887                    mutable: false,
3888                    doc: None,
3889                }],
3890                returns: Some(TypeRef::I32),
3891                doc: None,
3892                r#async: true,
3893                cancellable: false,
3894                deprecated: None,
3895                since: None,
3896            });
3897            m
3898        }]);
3899        let addon = render_addon_c(&api, "weaveffi", true, "weaveffi.yml");
3900        let create_count = addon.matches("napi_create_promise").count();
3901        let resolve_count = addon.matches("napi_resolve_deferred").count();
3902        let reject_count = addon.matches("napi_reject_deferred").count();
3903        let alloc_count = addon
3904            .matches("calloc(1, sizeof(weaveffi_tasks_run_napi_actx))")
3905            .count();
3906        let free_count = addon.matches("free(ctx);").count();
3907        let release_count = addon
3908            .matches("napi_release_threadsafe_function(ctx->tsfn, napi_tsfn_release);")
3909            .count();
3910        assert_eq!(
3911            create_count, 1,
3912            "expected one napi_create_promise per async fn, got {create_count}: {addon}"
3913        );
3914        assert_eq!(
3915            resolve_count, 1,
3916            "expected one napi_resolve_deferred per async fn, got {resolve_count}: {addon}"
3917        );
3918        assert_eq!(
3919            reject_count, 1,
3920            "expected one napi_reject_deferred per async fn, got {reject_count}: {addon}"
3921        );
3922        assert_eq!(
3923            alloc_count, free_count,
3924            "ctx alloc / free must balance per async fn: alloc={alloc_count} free={free_count}: {addon}"
3925        );
3926        assert_eq!(
3927            release_count, 1,
3928            "tsfn must be released exactly once per async fn, got {release_count}: {addon}"
3929        );
3930    }
3931
3932    fn doc_module() -> Module {
3933        Module {
3934            name: "docs".into(),
3935            functions: vec![Function {
3936                name: "do_thing".into(),
3937                params: vec![Param {
3938                    name: "x".into(),
3939                    ty: TypeRef::I32,
3940                    mutable: false,
3941                    doc: Some("the input value".into()),
3942                }],
3943                returns: Some(TypeRef::I32),
3944                doc: Some("Performs a thing.".into()),
3945                r#async: false,
3946                cancellable: false,
3947                deprecated: None,
3948                since: None,
3949            }],
3950            structs: vec![StructDef {
3951                name: "Item".into(),
3952                doc: Some("An item we track.".into()),
3953                fields: vec![StructField {
3954                    name: "id".into(),
3955                    ty: TypeRef::I64,
3956                    doc: Some("Stable id".into()),
3957                    default: None,
3958                }],
3959                builder: false,
3960            }],
3961            enums: vec![EnumDef {
3962                name: "Kind".into(),
3963                doc: Some("Kind of item.".into()),
3964                variants: vec![EnumVariant {
3965                    name: "Small".into(),
3966                    value: 0,
3967                    doc: Some("A small one".into()),
3968                }],
3969            }],
3970            callbacks: vec![],
3971            listeners: vec![],
3972            errors: None,
3973            modules: vec![],
3974        }
3975    }
3976
3977    #[test]
3978    fn node_emits_doc_on_function() {
3979        let dts = render_node_dts(
3980            &make_api(vec![doc_module()]),
3981            "weaveffi",
3982            true,
3983            "weaveffi.yml",
3984        );
3985        assert!(dts.contains("Performs a thing."), "{dts}");
3986    }
3987
3988    #[test]
3989    fn node_emits_doc_on_struct() {
3990        let dts = render_node_dts(
3991            &make_api(vec![doc_module()]),
3992            "weaveffi",
3993            true,
3994            "weaveffi.yml",
3995        );
3996        assert!(dts.contains("/** An item we track. */"), "{dts}");
3997    }
3998
3999    #[test]
4000    fn node_emits_doc_on_enum_variant() {
4001        let dts = render_node_dts(
4002            &make_api(vec![doc_module()]),
4003            "weaveffi",
4004            true,
4005            "weaveffi.yml",
4006        );
4007        assert!(dts.contains("/** Kind of item. */"), "{dts}");
4008        assert!(dts.contains("/** A small one */"), "{dts}");
4009    }
4010
4011    #[test]
4012    fn node_emits_doc_on_field() {
4013        let dts = render_node_dts(
4014            &make_api(vec![doc_module()]),
4015            "weaveffi",
4016            true,
4017            "weaveffi.yml",
4018        );
4019        assert!(dts.contains("/** Stable id */"), "{dts}");
4020    }
4021
4022    #[test]
4023    fn node_emits_doc_on_param() {
4024        let dts = render_node_dts(
4025            &make_api(vec![doc_module()]),
4026            "weaveffi",
4027            true,
4028            "weaveffi.yml",
4029        );
4030        assert!(dts.contains("@param x the input value"), "{dts}");
4031    }
4032}