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