Skip to main content

alef_backend_napi/
trait_bridge.rs

1//! NAPI-RS-specific trait bridge code generation.
2//!
3//! Generates Rust wrapper structs that implement Rust traits by delegating
4//! to JavaScript objects via NAPI-RS.
5
6use alef_codegen::generators::trait_bridge::{
7    BridgeOutput, TraitBridgeGenerator, TraitBridgeSpec, bridge_param_type as param_type, gen_bridge_all,
8    to_camel_case, visitor_param_type,
9};
10use alef_core::config::TraitBridgeConfig;
11use alef_core::ir::{ApiSurface, MethodDef, TypeDef, TypeRef};
12use std::collections::HashMap;
13use std::fmt::Write;
14
15/// Find the first parameter index and bridge config where the parameter's named type
16/// matches a trait bridge's `type_alias`.
17///
18/// Returns `None` when no bridge applies.
19pub use alef_codegen::generators::trait_bridge::find_bridge_param;
20
21/// NAPI-specific trait bridge generator.
22/// Implements code generation for bridging JavaScript objects to Rust traits.
23pub struct NapiBridgeGenerator {
24    /// Core crate import path (e.g., `"kreuzberg"`).
25    pub core_import: String,
26    /// Map of type name → fully-qualified Rust path for type references.
27    pub type_paths: HashMap<String, String>,
28    /// Error type name (e.g., `"KreuzbergError"`).
29    pub error_type: String,
30}
31
32impl TraitBridgeGenerator for NapiBridgeGenerator {
33    fn foreign_object_type(&self) -> &str {
34        "napi::bindgen_prelude::Object<'static>"
35    }
36
37    fn bridge_imports(&self) -> Vec<String> {
38        vec![
39            "napi::bindgen_prelude::{JsObjectValue, ToNapiValue, Unknown, Object}".to_string(),
40            "napi::JsValue".to_string(),
41            "std::sync::Arc".to_string(),
42        ]
43    }
44
45    fn gen_sync_method_body(&self, method: &MethodDef, spec: &TraitBridgeSpec) -> String {
46        let name = &method.name;
47        let has_error = method.error_type.is_some();
48        let mut out = String::with_capacity(512);
49
50        // Get the JS function from the object
51        let js_args_exprs = build_napi_args(method);
52        let args_tuple_ty = unknown_tuple_type(js_args_exprs.len());
53
54        writeln!(
55            out,
56            "let func: napi::bindgen_prelude::Function<{args_tuple_ty}, napi::bindgen_prelude::Unknown> = match self.inner.get_named_property(\"{name}\") {{"
57        )
58        .ok();
59        writeln!(out, "    Ok(f) => f,").ok();
60        if has_error {
61            let err = spec.make_error("format!(\"Method '{}' not found on bridge object: {}\", self.cached_name, e)");
62            writeln!(out, "    Err(e) => return Err({err}),").ok();
63        } else {
64            writeln!(out, "    Err(_) => return Default::default(),").ok();
65        }
66        writeln!(out, "}};").ok();
67
68        // Build and call with args
69        if js_args_exprs.is_empty() {
70            writeln!(out, "let result = func.call(());").ok();
71        } else {
72            // Pass args directly as tuple without intermediate variables
73            let tuple_str = if js_args_exprs.len() == 1 {
74                format!("({},)", js_args_exprs[0])
75            } else {
76                format!("({})", js_args_exprs.join(", "))
77            };
78            writeln!(out, "let result = func.call({tuple_str});").ok();
79        }
80
81        // Parse result
82        if has_error {
83            writeln!(out, "match result {{").ok();
84            let err = spec.make_error(&format!(
85                "format!(\"Plugin '{{}}' method '{}' failed: {{}}\", self.cached_name, e)",
86                name
87            ));
88            writeln!(out, "    Err(e) => Err({err}),").ok();
89            writeln!(out, "    Ok(val) => {{").ok();
90            if matches!(method.return_type, TypeRef::Unit) {
91                writeln!(out, "        Ok(())").ok();
92            } else {
93                // For most return types, attempt string coercion from the JS value
94                writeln!(out, "        // Convert JS value to Rust type via string coercion").ok();
95                writeln!(out, "        let s = val.coerce_to_string()").ok();
96                writeln!(out, "            .and_then(|s| s.into_utf8())").ok();
97                writeln!(out, "            .and_then(|s| s.into_owned())").ok();
98                writeln!(out, "            .map_err(|e| {{").ok();
99                let err = spec.make_error(&format!(
100                    "format!(\"Failed to extract return value from method '{}': {{}}\", e)",
101                    name
102                ));
103                writeln!(out, "                {err}").ok();
104                writeln!(out, "            }})?;").ok();
105                writeln!(out, "        match s.as_str() {{").ok();
106                writeln!(out, "            \"\" | \"null\" => Ok(Default::default()),").ok();
107                writeln!(out, "            _ => {{").ok();
108                writeln!(out, "                serde_json::from_str::<_>(&s).map_err(|_| {{").ok();
109                let err = spec.make_error(&format!(
110                    "format!(\"Failed to parse return value for method '{}'\", {})",
111                    name, "self.cached_name"
112                ));
113                writeln!(out, "                    {err}").ok();
114                writeln!(out, "                }})").ok();
115                writeln!(out, "            }}").ok();
116                writeln!(out, "        }}").ok();
117            }
118            writeln!(out, "    }}").ok();
119            writeln!(out, "}}").ok();
120        } else {
121            // Non-Result return: must not use ? or wrap in Ok()
122            writeln!(out, "match result {{").ok();
123            writeln!(out, "    Err(_) => Default::default(),").ok();
124            writeln!(out, "    Ok(val) => {{").ok();
125            if matches!(method.return_type, TypeRef::Unit) {
126                writeln!(out, "        ()").ok();
127            } else {
128                // For most return types, attempt string coercion from the JS value
129                writeln!(out, "        // Convert JS value to Rust type via string coercion").ok();
130                writeln!(out, "        let s = val.coerce_to_string()").ok();
131                writeln!(out, "            .and_then(|s| s.into_utf8())").ok();
132                writeln!(out, "            .and_then(|s| s.into_owned())").ok();
133                writeln!(out, "            .unwrap_or_default();").ok();
134                writeln!(out, "        match s.as_str() {{").ok();
135                writeln!(out, "            \"\" | \"null\" => Default::default(),").ok();
136                writeln!(
137                    out,
138                    "            _ => serde_json::from_str::<_>(&s).unwrap_or_default()"
139                )
140                .ok();
141                writeln!(out, "        }}").ok();
142            }
143            writeln!(out, "    }}").ok();
144            writeln!(out, "}}").ok();
145        }
146        out
147    }
148
149    fn gen_async_method_body(&self, method: &MethodDef, spec: &TraitBridgeSpec) -> String {
150        let name = &method.name;
151        let mut out = String::with_capacity(1024);
152
153        // NAPI has native async support via BoxPromise
154        writeln!(out, "let cached_name = self.cached_name.clone();").ok();
155
156        // Build the JS function call
157        let js_args_exprs = build_napi_args(method);
158        let args_tuple_ty = unknown_tuple_type(js_args_exprs.len());
159
160        writeln!(
161            out,
162            "let func: napi::bindgen_prelude::Function<{args_tuple_ty}, napi::bindgen_prelude::Unknown> = match self.inner.get_named_property(\"{name}\") {{"
163        )
164        .ok();
165        writeln!(out, "    Ok(f) => f,").ok();
166        writeln!(out, "    Err(e) => {{").ok();
167        let err = spec.make_error("format!(\"Method '{}' not found on bridge object: {}\", cached_name, e)");
168        writeln!(out, "        return Err({err});").ok();
169        writeln!(out, "    }}").ok();
170        writeln!(out, "}};").ok();
171
172        // Pass args directly without intermediate variables to avoid lifetime issues with env
173        let tuple_str = if js_args_exprs.is_empty() {
174            "()".to_string()
175        } else {
176            if js_args_exprs.len() == 1 {
177                format!("({},)", js_args_exprs[0])
178            } else {
179                format!("({})", js_args_exprs.join(", "))
180            }
181        };
182
183        writeln!(out, "let result = func.call({tuple_str});").ok();
184        writeln!(out, "match result {{").ok();
185        let err = spec.make_error(&format!(
186            "format!(\"Plugin '{{}}' method '{}' failed: {{}}\", cached_name, e)",
187            name
188        ));
189        writeln!(out, "    Err(e) => Err({err}),").ok();
190        writeln!(out, "    Ok(val) => {{").ok();
191        if matches!(method.return_type, TypeRef::Unit) {
192            writeln!(out, "        Ok(())").ok();
193        } else {
194            // For most return types, attempt string coercion from the JS value
195            writeln!(out, "        // Convert JS value to Rust type via string coercion").ok();
196            writeln!(out, "        let s = val.coerce_to_string()").ok();
197            writeln!(out, "            .and_then(|s| s.into_utf8())").ok();
198            writeln!(out, "            .and_then(|s| s.into_owned())").ok();
199            writeln!(out, "            .map_err(|e| {{").ok();
200            let err = spec.make_error(&format!(
201                "format!(\"Failed to extract return value from method '{}': {{}}\", e)",
202                name
203            ));
204            writeln!(out, "                {err}").ok();
205            writeln!(out, "            }})?;").ok();
206            // Try JSON parsing first, then default
207            writeln!(out, "        match s.as_str() {{").ok();
208            writeln!(out, "            \"\" | \"null\" => Ok(Default::default()),").ok();
209            writeln!(out, "            _ => serde_json::from_str::<_>(&s).map_err(|_| {{").ok();
210            let err = spec.make_error(&format!(
211                "\"Failed to parse return value for method '{}'\".to_string()",
212                name
213            ));
214            writeln!(out, "                {err}").ok();
215            writeln!(out, "            }})").ok();
216            writeln!(out, "        }}").ok();
217        }
218        writeln!(out, "    }}").ok();
219        writeln!(out, "}}").ok();
220        out
221    }
222
223    fn gen_constructor(&self, spec: &TraitBridgeSpec) -> String {
224        let wrapper = spec.wrapper_name();
225        let mut out = String::with_capacity(512);
226
227        writeln!(out, "impl {wrapper} {{").ok();
228        writeln!(out, "    /// Create a new bridge wrapping a NAPI Object.").ok();
229        writeln!(out, "    ///").ok();
230        writeln!(out, "    /// Validates that the object provides all required methods.").ok();
231        writeln!(
232            out,
233            "    pub fn new(js_obj: napi::bindgen_prelude::Object<'_>) -> napi::Result<Self> {{"
234        )
235        .ok();
236
237        // Validate all required methods exist
238        for req_method in spec.required_methods() {
239            writeln!(
240                out,
241                "        if !js_obj.has_named_property(\"{}\").unwrap_or(false) {{",
242                req_method.name
243            )
244            .ok();
245            writeln!(out, "            return Err(napi::Error::new(").ok();
246            writeln!(out, "                napi::Status::GenericFailure,").ok();
247            writeln!(
248                out,
249                "                format!(\"Object missing required method: {{}}\", \"{}\")",
250                req_method.name
251            )
252            .ok();
253            writeln!(out, "            ));").ok();
254            writeln!(out, "        }}").ok();
255        }
256
257        // Transmute Object<'_> to Object<'static> for the stored field
258        writeln!(
259            out,
260            "        // SAFETY: The JS object is owned by the Node.js runtime and lives for"
261        )
262        .ok();
263        writeln!(
264            out,
265            "        // the duration of the enclosing #[napi] call. The bridge is only used"
266        )
267        .ok();
268        writeln!(
269            out,
270            "        // synchronously during that same call, so 'static is safe here."
271        )
272        .ok();
273        writeln!(
274            out,
275            "        let js_obj: napi::bindgen_prelude::Object<'static> = unsafe {{"
276        )
277        .ok();
278        writeln!(out, "            std::mem::transmute(js_obj)").ok();
279        writeln!(out, "        }};").ok();
280
281        // Try to extract name from the object
282        writeln!(
283            out,
284            "        let cached_name = match js_obj.get_named_property::<String>(\"name\") {{"
285        )
286        .ok();
287        writeln!(out, "            Ok(n) => n,").ok();
288        writeln!(out, "            Err(_) => \"unknown\".to_string(),").ok();
289        writeln!(out, "        }};").ok();
290
291        writeln!(out, "        Ok(Self {{").ok();
292        writeln!(out, "            inner: js_obj,").ok();
293        writeln!(out, "            cached_name,").ok();
294        writeln!(out, "        }})").ok();
295        writeln!(out, "    }}").ok();
296        writeln!(out).ok();
297        writeln!(out, "    /// Extract napi::Env from the stored Object.").ok();
298        writeln!(out, "    fn env(&self) -> napi::Env {{").ok();
299        writeln!(
300            out,
301            "        // SAFETY: Object<'static> is 3 pointer-sized words; first word is napi_env."
302        )
303        .ok();
304        writeln!(
305            out,
306            "        let raw: [*mut std::ffi::c_void; 3] = unsafe {{ std::mem::transmute_copy(&self.inner) }};"
307        )
308        .ok();
309        writeln!(out, "        napi::Env::from_raw(raw[0] as napi::sys::napi_env)").ok();
310        writeln!(out, "    }}").ok();
311        writeln!(out, "}}").ok();
312        writeln!(out).ok();
313        writeln!(
314            out,
315            "// SAFETY: The bridge is created from a NAPI Object that is pinned to the"
316        )
317        .ok();
318        writeln!(
319            out,
320            "// Node.js event loop thread. All access occurs on that thread. Send+Sync"
321        )
322        .ok();
323        writeln!(
324            out,
325            "// are required by the Plugin trait but the bridge is never actually moved"
326        )
327        .ok();
328        writeln!(out, "// across threads.").ok();
329        writeln!(out, "unsafe impl Send for {wrapper} {{}}").ok();
330        writeln!(out, "unsafe impl Sync for {wrapper} {{}}").ok();
331        out
332    }
333
334    fn gen_registration_fn(&self, spec: &TraitBridgeSpec) -> String {
335        let Some(register_fn) = spec.bridge_config.register_fn.as_deref() else {
336            return String::new();
337        };
338        let Some(registry_getter) = spec.bridge_config.registry_getter.as_deref() else {
339            return String::new();
340        };
341        let wrapper = spec.wrapper_name();
342        let trait_path = spec.trait_path();
343
344        let mut out = String::with_capacity(1024);
345
346        writeln!(out, "#[napi]").ok();
347        writeln!(
348            out,
349            "pub fn {register_fn}(obj: napi::bindgen_prelude::Object) -> napi::Result<()> {{"
350        )
351        .ok();
352
353        // Create and validate the bridge
354        writeln!(out, "    let bridge = {wrapper}::new(obj)?;").ok();
355        writeln!(out, "    let arc: Arc<dyn {trait_path}> = Arc::new(bridge);").ok();
356
357        // Register in the plugin registry (synchronous, no GC needed for NAPI)
358        let extra = spec
359            .bridge_config
360            .register_extra_args
361            .as_deref()
362            .map(|a| format!(", {a}"))
363            .unwrap_or_default();
364        writeln!(out, "    let registry = {registry_getter}();").ok();
365        writeln!(out, "    let mut registry = registry.write();").ok();
366        writeln!(out, "    registry.register(arc{extra}).map_err(|e| napi::Error::new(").ok();
367        writeln!(out, "        napi::Status::GenericFailure,").ok();
368        writeln!(out, "        format!(\"Failed to register backend: {{}}\", e)").ok();
369        writeln!(out, "    ))").ok();
370        writeln!(out, "}}").ok();
371        out
372    }
373}
374
375/// Generate all trait bridge code for a given trait type and bridge config.
376pub fn gen_trait_bridge(
377    trait_type: &TypeDef,
378    bridge_cfg: &TraitBridgeConfig,
379    core_import: &str,
380    error_type: &str,
381    error_constructor: &str,
382    api: &ApiSurface,
383) -> BridgeOutput {
384    // Build type name → rust_path lookup (converted to String-owned HashMap)
385    let type_paths: HashMap<String, String> = api
386        .types
387        .iter()
388        .map(|t| (t.name.clone(), t.rust_path.replace('-', "_")))
389        .chain(
390            api.enums
391                .iter()
392                .map(|e| (e.name.clone(), e.rust_path.replace('-', "_"))),
393        )
394        .collect();
395
396    // Visitor-style bridge: all methods have defaults, no registry, no super-trait.
397    let is_visitor_bridge = bridge_cfg.type_alias.is_some()
398        && bridge_cfg.register_fn.is_none()
399        && bridge_cfg.super_trait.is_none()
400        && trait_type.methods.iter().all(|m| m.has_default_impl);
401
402    if is_visitor_bridge {
403        let struct_name = format!("Js{}Bridge", bridge_cfg.trait_name);
404        let trait_path = trait_type.rust_path.replace('-', "_");
405        let code = gen_visitor_bridge(
406            trait_type,
407            bridge_cfg,
408            &struct_name,
409            &trait_path,
410            core_import,
411            &type_paths,
412        );
413        BridgeOutput { imports: vec![], code }
414    } else {
415        // Use the IR-driven TraitBridgeGenerator infrastructure
416        let generator = NapiBridgeGenerator {
417            core_import: core_import.to_string(),
418            type_paths: type_paths.clone(),
419            error_type: error_type.to_string(),
420        };
421        let spec = TraitBridgeSpec {
422            trait_def: trait_type,
423            bridge_config: bridge_cfg,
424            core_import,
425            wrapper_prefix: "Js",
426            type_paths,
427            error_type: error_type.to_string(),
428            error_constructor: error_constructor.to_string(),
429        };
430        gen_bridge_all(&spec, &generator)
431    }
432}
433
434/// Generate a visitor-style bridge wrapping a `napi::bindgen_prelude::Object`.
435///
436/// Every trait method checks if the JS object has a matching camelCase property,
437/// then calls it with converted arguments and maps the JS return value to `VisitResult`.
438fn gen_visitor_bridge(
439    trait_type: &TypeDef,
440    _bridge_cfg: &TraitBridgeConfig,
441    struct_name: &str,
442    trait_path: &str,
443    core_crate: &str,
444    type_paths: &HashMap<String, String>,
445) -> String {
446    let mut out = String::with_capacity(8192);
447    // Emit trait imports needed by the generated bridge code.
448    // napi::* glob does not re-export JsObjectValue or JsValue from bindgen_prelude.
449    writeln!(out, "#[allow(unused_imports)]").unwrap();
450    writeln!(
451        out,
452        "use napi::bindgen_prelude::{{JsObjectValue, ToNapiValue, Unknown, Object}};"
453    )
454    .unwrap();
455    writeln!(out, "#[allow(unused_imports)]").unwrap();
456    writeln!(out, "use napi::JsValue;").unwrap();
457    writeln!(out).unwrap();
458
459    // Helper: convert NodeContext to a JS object
460    writeln!(out, "fn nodecontext_to_js_object<'e>(").unwrap();
461    writeln!(out, "    env: &'e napi::Env,").unwrap();
462    writeln!(out, "    ctx: &{core_crate}::visitor::NodeContext,").unwrap();
463    writeln!(out, ") -> napi::Result<napi::bindgen_prelude::Object<'e>> {{").unwrap();
464    writeln!(out, "    let mut obj = napi::bindgen_prelude::Object::new(env)?;").unwrap();
465    writeln!(
466        out,
467        "    obj.set_named_property(\"nodeType\", env.create_string(&format!(\"{{:?}}\", ctx.node_type))?)?;"
468    )
469    .unwrap();
470    writeln!(
471        out,
472        "    obj.set_named_property(\"tagName\", env.create_string(&ctx.tag_name)?)?;"
473    )
474    .unwrap();
475    writeln!(
476        out,
477        "    obj.set_named_property(\"depth\", env.create_uint32(ctx.depth as u32)?)?;"
478    )
479    .unwrap();
480    writeln!(
481        out,
482        "    obj.set_named_property(\"indexInParent\", env.create_uint32(ctx.index_in_parent as u32)?)?;"
483    )
484    .unwrap();
485    writeln!(out, "    obj.set_named_property(\"isInline\", ctx.is_inline)?;").unwrap();
486    writeln!(out, "    let parent_tag = match &ctx.parent_tag {{").unwrap();
487    writeln!(out, "        Some(s) => env.create_string(s)?.to_unknown(),").unwrap();
488    writeln!(out, "        None => {{").unwrap();
489    writeln!(
490        out,
491        "            // SAFETY: napi_get_null returns a valid napi_value for the given env."
492    )
493    .unwrap();
494    writeln!(
495        out,
496        "            let raw = unsafe {{ napi::bindgen_prelude::ToNapiValue::to_napi_value(env.raw(), napi::bindgen_prelude::Null)? }};"
497    )
498    .unwrap();
499    writeln!(
500        out,
501        "            unsafe {{ napi::bindgen_prelude::Unknown::from_raw_unchecked(env.raw(), raw) }}"
502    )
503    .unwrap();
504    writeln!(out, "        }}").unwrap();
505    writeln!(out, "    }};").unwrap();
506    writeln!(out, "    obj.set_named_property(\"parentTag\", parent_tag)?;").unwrap();
507    writeln!(out, "    let mut attrs = napi::bindgen_prelude::Object::new(env)?;").unwrap();
508    writeln!(out, "    for (k, v) in &ctx.attributes {{").unwrap();
509    writeln!(out, "        attrs.set_named_property(k, env.create_string(v)?)?;").unwrap();
510    writeln!(out, "    }}").unwrap();
511    writeln!(out, "    obj.set_named_property(\"attributes\", attrs)?;").unwrap();
512    writeln!(out, "    Ok(obj)").unwrap();
513    writeln!(out, "}}").unwrap();
514    writeln!(out).unwrap();
515
516    // Bridge struct: store Object<'static> to avoid Object<'env> lifetime constraints.
517    // SAFETY invariant: the Object is kept alive by the JS caller for the duration of the
518    // #[napi] function that created the bridge, and by extension for all visitor callbacks.
519    writeln!(out, "pub struct {struct_name} {{").unwrap();
520    writeln!(out, "    obj: napi::bindgen_prelude::Object<'static>,").unwrap();
521    writeln!(out, "}}").unwrap();
522    writeln!(out).unwrap();
523
524    // Manual Debug impl (Object doesn't implement Debug)
525    writeln!(out, "impl std::fmt::Debug for {struct_name} {{").unwrap();
526    writeln!(
527        out,
528        "    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{"
529    )
530    .unwrap();
531    writeln!(out, "        write!(f, \"{struct_name}\")").unwrap();
532    writeln!(out, "    }}").unwrap();
533    writeln!(out, "}}").unwrap();
534    writeln!(out).unwrap();
535
536    // Constructor: transmute Object<'_> to Object<'static> to bypass the lifetime.
537    writeln!(out, "impl {struct_name} {{").unwrap();
538    writeln!(
539        out,
540        "    pub fn new(js_obj: napi::bindgen_prelude::Object<'_>) -> Self {{"
541    )
542    .unwrap();
543    writeln!(
544        out,
545        "        // SAFETY: The JS object is owned by the Node.js runtime and lives for"
546    )
547    .unwrap();
548    writeln!(
549        out,
550        "        // the duration of the enclosing #[napi] call. The bridge is only used"
551    )
552    .unwrap();
553    writeln!(
554        out,
555        "        // synchronously during that same call, so 'static is safe here."
556    )
557    .unwrap();
558    writeln!(
559        out,
560        "        let obj: napi::bindgen_prelude::Object<'static> = unsafe {{ std::mem::transmute(js_obj) }};"
561    )
562    .unwrap();
563    writeln!(out, "        Self {{ obj }}").unwrap();
564    writeln!(out, "    }}").unwrap();
565    writeln!(out).unwrap();
566
567    // Helper: extract napi_env from the Object. Object<'static> stores napi_env as its
568    // first pointer-sized field. This is an internal layout assumption for napi-rs v3.
569    writeln!(out, "    fn env(&self) -> napi::Env {{").unwrap();
570    writeln!(
571        out,
572        "        // SAFETY: Object<'static> is 3 pointer-sized words; first word is napi_env."
573    )
574    .unwrap();
575    writeln!(
576        out,
577        "        let raw: [*mut std::ffi::c_void; 3] = unsafe {{ std::mem::transmute_copy(&self.obj) }};"
578    )
579    .unwrap();
580    writeln!(out, "        napi::Env::from_raw(raw[0] as napi::sys::napi_env)").unwrap();
581    writeln!(out, "    }}").unwrap();
582    writeln!(out, "}}").unwrap();
583    writeln!(out).unwrap();
584
585    // Trait impl
586    writeln!(out, "impl {trait_path} for {struct_name} {{").unwrap();
587    for method in &trait_type.methods {
588        if method.trait_source.is_some() {
589            continue;
590        }
591        gen_visitor_method_napi(&mut out, method, trait_path, core_crate, type_paths);
592    }
593    writeln!(out, "}}").unwrap();
594    writeln!(out).unwrap();
595    out
596}
597
598/// Build the Function args tuple type string for a given number of Unknown args.
599fn unknown_tuple_type(count: usize) -> String {
600    if count == 0 {
601        return "()".to_string();
602    }
603    let parts = vec!["napi::bindgen_prelude::Unknown"; count];
604    format!("({}{})", parts.join(", "), if count == 1 { "," } else { "" })
605}
606
607/// Generate a single visitor method that checks for a camelCase JS property and calls it.
608fn gen_visitor_method_napi(
609    out: &mut String,
610    method: &MethodDef,
611    _trait_path: &str,
612    _core_crate: &str,
613    type_paths: &HashMap<String, String>,
614) {
615    let name = &method.name;
616
617    // Convert snake_case method name to camelCase for JS
618    let js_name = to_camel_case(name);
619
620    let mut sig_parts = vec!["&mut self".to_string()];
621    for p in &method.params {
622        let ty_str = visitor_param_type(&p.ty, p.is_ref, p.optional, type_paths);
623        sig_parts.push(format!("{}: {}", p.name, ty_str));
624    }
625    let sig = sig_parts.join(", ");
626
627    let ret_ty = match &method.return_type {
628        TypeRef::Named(n) => type_paths
629            .get(n.as_str())
630            .map(|p| p.replace('-', "_"))
631            .unwrap_or_else(|| n.clone()),
632        other => param_type(other, "", false, type_paths),
633    };
634
635    writeln!(out, "    fn {name}({sig}) -> {ret_ty} {{").unwrap();
636
637    // Check if JS object has the method
638    writeln!(
639        out,
640        "        let has_method = self.obj.has_named_property(\"{js_name}\").unwrap_or(false);"
641    )
642    .unwrap();
643    writeln!(out, "        if !has_method {{").unwrap();
644    writeln!(out, "            return {ret_ty}::Continue;").unwrap();
645    writeln!(out, "        }}").unwrap();
646
647    // Get the JS function with the correct tuple arg type
648    let arg_count = method.params.len();
649    let args_tuple_ty = unknown_tuple_type(arg_count);
650    writeln!(
651        out,
652        "        let func: napi::bindgen_prelude::Function<{args_tuple_ty}, napi::bindgen_prelude::Unknown> = match self.obj.get_named_property(\"{js_name}\") {{"
653    )
654    .unwrap();
655    writeln!(out, "            Ok(f) => f,").unwrap();
656    writeln!(out, "            Err(_) => return {ret_ty}::Continue,").unwrap();
657    writeln!(out, "        }};").unwrap();
658
659    // Build and call with args
660    let js_args_exprs = build_napi_args(method);
661    if arg_count == 0 {
662        writeln!(out, "        let result = func.call(());").unwrap();
663    } else {
664        // Bind env to a named variable so borrows from it outlive the statement.
665        writeln!(out, "        let __env = self.env();").unwrap();
666        // Emit each arg as a let binding, then call with tuple
667        for (i, expr) in js_args_exprs.iter().enumerate() {
668            // Replace __ENV__ placeholder with the bound variable
669            let expr = expr.replace("self.env()", "__env");
670            writeln!(out, "        let arg_{i}: napi::bindgen_prelude::Unknown = {expr};").unwrap();
671        }
672        let tuple_args: Vec<String> = (0..arg_count).map(|i| format!("arg_{i}")).collect();
673        let tuple_str = if arg_count == 1 {
674            format!("({},)", tuple_args[0])
675        } else {
676            format!("({})", tuple_args.join(", "))
677        };
678        writeln!(out, "        let result = func.call({tuple_str});").unwrap();
679    }
680
681    // Parse result
682    writeln!(out, "        match result {{").unwrap();
683    writeln!(out, "            Err(_) => {ret_ty}::Continue,").unwrap();
684    writeln!(out, "            Ok(val) => {{").unwrap();
685    writeln!(
686        out,
687        "                if let Ok(s) = val.coerce_to_string().and_then(|s| s.into_utf8()).and_then(|s| s.into_owned()) {{"
688    )
689    .unwrap();
690    writeln!(out, "                    match s.to_lowercase().as_str() {{").unwrap();
691    writeln!(out, "                        \"continue\" => {ret_ty}::Continue,").unwrap();
692    writeln!(out, "                        \"skip\" => {ret_ty}::Skip,").unwrap();
693    writeln!(
694        out,
695        "                        \"preserve_html\" | \"preservehtml\" => {ret_ty}::PreserveHtml,"
696    )
697    .unwrap();
698    writeln!(
699        out,
700        "                        other => {ret_ty}::Custom(other.to_string()),"
701    )
702    .unwrap();
703    writeln!(out, "                    }}").unwrap();
704    writeln!(out, "                }} else {{").unwrap();
705    writeln!(out, "                    {ret_ty}::Continue").unwrap();
706    writeln!(out, "                }}").unwrap();
707    writeln!(out, "            }}").unwrap();
708    writeln!(out, "        }}").unwrap();
709    writeln!(out, "    }}").unwrap();
710    writeln!(out).unwrap();
711}
712
713/// Build NAPI argument expressions for a visitor method.
714///
715/// Returns one expression per parameter, each producing a `napi::bindgen_prelude::Unknown`.
716fn build_napi_args(method: &MethodDef) -> Vec<String> {
717    method
718        .params
719        .iter()
720
721        .map(|p| {
722            if let TypeRef::Named(n) = &p.ty {
723                if n == "NodeContext" {
724                    return format!(
725                        "match nodecontext_to_js_object(&self.env(), {}{}) {{ Ok(o) => o.to_unknown(), Err(_) => unsafe {{ \
726                         let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(std::ptr::null_mut(), napi::bindgen_prelude::Null).unwrap_or(std::ptr::null_mut()); \
727                         napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r) }} \
728                        }}",
729                        if p.is_ref { "" } else { "&" },
730                        p.name
731                    );
732                }
733            }
734            // Option<&str>
735            if p.optional && matches!(&p.ty, TypeRef::String) && p.is_ref {
736                return format!(
737                    "match {name} {{ \
738                     Some(s) => match self.env().create_string(s) {{ \
739                       Ok(v) => v.to_unknown(), \
740                       Err(_) => unsafe {{ \
741                       let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(std::ptr::null_mut(), napi::bindgen_prelude::Null).unwrap_or(std::ptr::null_mut()); \
742                       napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r) }} \
743                     }}, \
744                     None => unsafe {{ \
745                       let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(std::ptr::null_mut(), napi::bindgen_prelude::Null).unwrap_or(std::ptr::null_mut()); \
746                       napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r) }} \
747                    }}",
748                    name = p.name
749                );
750            }
751            // &str
752            if matches!(&p.ty, TypeRef::String) && p.is_ref {
753                return format!(
754                    "match self.env().create_string({name}) {{ \
755                     Ok(s) => s.to_unknown(), \
756                     Err(_) => unsafe {{ \
757                     let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(std::ptr::null_mut(), napi::bindgen_prelude::Null).unwrap_or(std::ptr::null_mut()); \
758                     napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r) }} \
759                    }}",
760                    name = p.name
761                );
762            }
763            // String (owned)
764            if matches!(&p.ty, TypeRef::String) {
765                return format!(
766                    "match self.env().create_string({name}.as_str()) {{ \
767                     Ok(s) => s.to_unknown(), \
768                     Err(_) => unsafe {{ \
769                     let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(std::ptr::null_mut(), napi::bindgen_prelude::Null).unwrap_or(std::ptr::null_mut()); \
770                     napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r) }} \
771                    }}",
772                    name = p.name
773                );
774            }
775            // Bool
776            if matches!(&p.ty, TypeRef::Primitive(alef_core::ir::PrimitiveType::Bool)) {
777                return format!(
778                    "unsafe {{ \
779                     let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(self.env().raw(), {name}).unwrap_or(std::ptr::null_mut()); \
780                     napi::bindgen_prelude::Unknown::from_raw_unchecked(self.env().raw(), r) }}",
781                    name = p.name
782                );
783            }
784            // u32 / usize: create_uint32 needs a u32; usize requires the cast but u32 does not.
785            if matches!(&p.ty, TypeRef::Primitive(alef_core::ir::PrimitiveType::U32)) {
786                return format!(
787                    "match self.env().create_uint32({name}) {{ Ok(n) => n.to_unknown(), Err(_) => unsafe {{ \
788                     let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(std::ptr::null_mut(), napi::bindgen_prelude::Null).unwrap_or(std::ptr::null_mut()); \
789                     napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r) }} \
790                    }}",
791                    name = p.name
792                );
793            }
794            if matches!(&p.ty, TypeRef::Primitive(alef_core::ir::PrimitiveType::Usize)) {
795                return format!(
796                    "match self.env().create_uint32({name} as u32) {{ Ok(n) => n.to_unknown(), Err(_) => unsafe {{ \
797                     let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(std::ptr::null_mut(), napi::bindgen_prelude::Null).unwrap_or(std::ptr::null_mut()); \
798                     napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r) }} \
799                    }}",
800                    name = p.name
801                );
802            }
803            // Vec<String> or &[String] - serialize to JSON string as fallback
804            // Default: serialize as debug string
805            format!(
806                "match self.env().create_string(&format!(\"{{:?}}\", {name})) {{ Ok(s) => s.to_unknown(), Err(_) => unsafe {{ \
807                 let r = napi::bindgen_prelude::ToNapiValue::to_napi_value(std::ptr::null_mut(), napi::bindgen_prelude::Null).unwrap_or(std::ptr::null_mut()); \
808                 napi::bindgen_prelude::Unknown::from_raw_unchecked(std::ptr::null_mut(), r) }} \
809                }}",
810                name = p.name
811            )
812        })
813        .collect()
814}
815
816/// Generate a NAPI free function that has one parameter replaced by
817/// `Option<napi::bindgen_prelude::Object>` (a trait bridge). The bridge is constructed
818/// before calling the core function.
819#[allow(clippy::too_many_arguments)]
820pub fn gen_bridge_function(
821    func: &alef_core::ir::FunctionDef,
822    bridge_param_idx: usize,
823    bridge_cfg: &TraitBridgeConfig,
824    mapper: &dyn alef_codegen::type_mapper::TypeMapper,
825    _cfg: &alef_codegen::generators::RustBindingConfig<'_>,
826    _adapter_bodies: &alef_codegen::generators::AdapterBodies,
827    opaque_types: &ahash::AHashSet<String>,
828    core_import: &str,
829) -> String {
830    use alef_core::ir::TypeRef;
831
832    let struct_name = format!("Js{}Bridge", bridge_cfg.trait_name);
833    let handle_path = format!("{core_import}::visitor::VisitorHandle");
834    let param_name = &func.params[bridge_param_idx].name;
835    let bridge_param = &func.params[bridge_param_idx];
836    let is_optional = bridge_param.optional || matches!(&bridge_param.ty, TypeRef::Optional(_));
837
838    // Build parameter list: bridge param becomes Option<Object>, no explicit env param
839    // (napi v3 does not implement FromNapiValue for Env; env is obtained from the Object)
840    let mut sig_parts = vec![];
841    for (idx, p) in func.params.iter().enumerate() {
842        if idx == bridge_param_idx {
843            if is_optional {
844                sig_parts.push(format!("{}: Option<napi::bindgen_prelude::Object>", p.name));
845            } else {
846                sig_parts.push(format!("{}: napi::bindgen_prelude::Object", p.name));
847            }
848        } else {
849            let promoted = idx > bridge_param_idx || func.params[..idx].iter().any(|pp| pp.optional);
850            let ty = if p.optional || promoted {
851                format!("Option<{}>", mapper.map_type(&p.ty))
852            } else {
853                mapper.map_type(&p.ty)
854            };
855            sig_parts.push(format!("{}: {}", p.name, ty));
856        }
857    }
858
859    let params_str = sig_parts.join(", ");
860    let return_type = mapper.map_type(&func.return_type);
861    let ret = mapper.wrap_return(&return_type, func.error_type.is_some());
862
863    let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
864
865    // Bridge wrapping code: constructor is infallible (transmute-based).
866    let bridge_wrap = if is_optional {
867        format!(
868            "let {param_name} = {param_name}.map(|v| {{\n        \
869             let bridge = {struct_name}::new(v);\n        \
870             std::rc::Rc::new(std::cell::RefCell::new(bridge)) as {handle_path}\n    \
871             }});"
872        )
873    } else {
874        format!(
875            "let {param_name} = {{\n        \
876             let bridge = {struct_name}::new({param_name});\n        \
877             std::rc::Rc::new(std::cell::RefCell::new(bridge)) as {handle_path}\n    \
878             }};"
879        )
880    };
881
882    // Use From/Into for non-bridge Named params — the generated bindings have From impls.
883    let serde_bindings: String = func
884        .params
885        .iter()
886        .enumerate()
887        .filter(|(idx, p)| {
888            if *idx == bridge_param_idx {
889                return false;
890            }
891            let named = match &p.ty {
892                TypeRef::Named(n) => Some(n.as_str()),
893                TypeRef::Optional(inner) => {
894                    if let TypeRef::Named(n) = inner.as_ref() {
895                        Some(n.as_str())
896                    } else {
897                        None
898                    }
899                }
900                _ => None,
901            };
902            named.is_some_and(|n| !opaque_types.contains(n))
903        })
904        .map(|(_, p)| {
905            let name = &p.name;
906            let core_path = format!(
907                "{core_import}::{}",
908                match &p.ty {
909                    TypeRef::Named(n) => n.clone(),
910                    TypeRef::Optional(inner) =>
911                        if let TypeRef::Named(n) = inner.as_ref() {
912                            n.clone()
913                        } else {
914                            String::new()
915                        },
916                    _ => String::new(),
917                }
918            );
919            if p.optional || matches!(&p.ty, TypeRef::Optional(_)) {
920                format!("let {name}_core: Option<{core_path}> = {name}.map(|v| v.into());\n    ")
921            } else {
922                format!("let {name}_core: {core_path} = {name}.into();\n    ")
923            }
924        })
925        .collect();
926
927    // Build call args
928    let call_args: Vec<String> = func
929        .params
930        .iter()
931        .enumerate()
932        .map(|(idx, p)| {
933            if idx == bridge_param_idx {
934                return p.name.clone();
935            }
936            match &p.ty {
937                TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
938                    if p.optional {
939                        format!("{}.as_ref().map(|v| &v.inner)", p.name)
940                    } else {
941                        format!("&{}.inner", p.name)
942                    }
943                }
944                TypeRef::Named(_) => format!("{}_core", p.name),
945                TypeRef::Optional(inner) => {
946                    if let TypeRef::Named(n) = inner.as_ref() {
947                        if opaque_types.contains(n.as_str()) {
948                            format!("{}.as_ref().map(|v| &v.inner)", p.name)
949                        } else {
950                            format!("{}_core", p.name)
951                        }
952                    } else {
953                        p.name.clone()
954                    }
955                }
956                TypeRef::String | TypeRef::Char => {
957                    if p.is_ref {
958                        format!("&{}", p.name)
959                    } else {
960                        p.name.clone()
961                    }
962                }
963                _ => p.name.clone(),
964            }
965        })
966        .collect();
967    let call_args_str = call_args.join(", ");
968
969    let core_fn_path = {
970        let path = func.rust_path.replace('-', "_");
971        if path.starts_with(core_import) {
972            path
973        } else {
974            format!("{core_import}::{}", func.name)
975        }
976    };
977    let core_call = format!("{core_fn_path}({call_args_str})");
978
979    let return_wrap = match &func.return_type {
980        TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
981            format!("{name} {{ inner: std::sync::Arc::new(val) }}")
982        }
983        TypeRef::Named(_) => "val.into()".to_string(),
984        TypeRef::String | TypeRef::Bytes => "val.into()".to_string(),
985        _ => "val".to_string(),
986    };
987
988    let body = if func.error_type.is_some() {
989        if return_wrap == "val" {
990            format!("{bridge_wrap}\n    {serde_bindings}{core_call}{err_conv}")
991        } else {
992            format!("{bridge_wrap}\n    {serde_bindings}{core_call}.map(|val| {return_wrap}){err_conv}")
993        }
994    } else {
995        format!("{bridge_wrap}\n    {serde_bindings}{core_call}")
996    };
997
998    let js_name = {
999        let mut result = String::with_capacity(func.name.len());
1000        let mut capitalize_next = false;
1001        for (i, c) in func.name.chars().enumerate() {
1002            if c == '_' {
1003                capitalize_next = true;
1004            } else if capitalize_next {
1005                result.extend(c.to_uppercase());
1006                capitalize_next = false;
1007            } else if i == 0 {
1008                result.extend(c.to_lowercase());
1009            } else {
1010                result.push(c);
1011            }
1012        }
1013        result
1014    };
1015    let js_name_attr = if js_name != func.name {
1016        format!("(js_name = \"{}\")", js_name)
1017    } else {
1018        String::new()
1019    };
1020
1021    let mut out = String::with_capacity(1024);
1022    if func.error_type.is_some() {
1023        writeln!(out, "#[allow(clippy::missing_errors_doc)]").ok();
1024    }
1025    writeln!(out, "#[napi{js_name_attr}]").ok();
1026    let func_name = &func.name;
1027    writeln!(out, "pub fn {func_name}({params_str}) -> {ret} {{").ok();
1028    writeln!(out, "    {body}").ok();
1029    writeln!(out, "}}").ok();
1030
1031    out
1032}