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