Skip to main content

alef_backend_napi/
gen_bindings.rs

1use crate::type_map::NapiMapper;
2use ahash::AHashSet;
3use alef_codegen::builder::{ImplBuilder, RustFileBuilder, StructBuilder};
4use alef_codegen::generators::{self, AsyncPattern, RustBindingConfig};
5use alef_codegen::naming::to_node_name;
6use alef_codegen::shared::{can_auto_delegate, function_params, partition_methods};
7use alef_codegen::type_mapper::TypeMapper;
8use alef_core::backend::{Backend, BuildConfig, Capabilities, GeneratedFile, PostBuildStep};
9use alef_core::config::{AlefConfig, Language, resolve_output_dir};
10use alef_core::hash::{self, CommentStyle};
11use alef_core::ir::{ApiSurface, EnumDef, FunctionDef, MethodDef, ParamDef, TypeDef, TypeRef};
12use std::path::PathBuf;
13
14pub struct NapiBackend;
15
16impl NapiBackend {
17    fn binding_config<'a>(core_import: &'a str, prefix: &'a str, has_serde: bool) -> RustBindingConfig<'a> {
18        RustBindingConfig {
19            struct_attrs: &["napi"],
20            field_attrs: &[],
21            struct_derives: &["Clone"],
22            method_block_attr: Some("napi"),
23            constructor_attr: "#[napi(constructor)]",
24            static_attr: None,
25            function_attr: "#[napi]",
26            enum_attrs: &["napi(string_enum)"],
27            enum_derives: &["Clone"],
28            needs_signature: false,
29            signature_prefix: "",
30            signature_suffix: "",
31            core_import,
32            async_pattern: AsyncPattern::NapiNativeAsync,
33            has_serde,
34            // NAPI napi(object) structs don't derive Serialize — disable serde bridge
35            type_name_prefix: prefix,
36            option_duration_on_defaults: true,
37            opaque_type_names: &[],
38        }
39    }
40}
41
42impl Backend for NapiBackend {
43    fn name(&self) -> &str {
44        "napi"
45    }
46
47    fn language(&self) -> Language {
48        Language::Node
49    }
50
51    fn capabilities(&self) -> Capabilities {
52        Capabilities {
53            supports_async: true,
54            supports_classes: true,
55            supports_enums: true,
56            supports_option: true,
57            supports_result: true,
58            ..Capabilities::default()
59        }
60    }
61
62    fn generate_bindings(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
63        let prefix = config.node_type_prefix();
64        let mapper = NapiMapper::new(prefix.clone());
65        let core_import = config.core_import();
66
67        // Detect serde availability from the output crate's Cargo.toml
68        let output_dir = resolve_output_dir(
69            config.output.node.as_ref(),
70            &config.crate_config.name,
71            "crates/{name}-node/src/",
72        );
73        let has_serde = alef_core::config::detect_serde_available(&output_dir);
74        let cfg = Self::binding_config(&core_import, &prefix, has_serde);
75
76        let mut builder = RustFileBuilder::new().with_generated_header();
77        builder.add_inner_attribute("allow(dead_code, unused_imports, unused_variables)");
78        builder.add_inner_attribute("allow(clippy::too_many_arguments, clippy::let_unit_value, clippy::needless_borrow, clippy::map_identity, clippy::just_underscores_and_digits, clippy::unnecessary_cast, clippy::unused_unit, clippy::unwrap_or_default, clippy::derivable_impls, clippy::needless_borrows_for_generic_args, clippy::unnecessary_fallible_conversions)");
79        builder.add_import("napi::*");
80        builder.add_import("napi_derive::napi");
81
82        // Always import serde_json for type conversion in From/Into impls,
83        // even if the binding crate doesn't explicitly list it as a dependency.
84        // serde_json is needed for conversions of types with serde-serializable fields.
85        builder.add_import("serde_json");
86
87        // Import traits needed for trait method dispatch
88        for trait_path in generators::collect_trait_imports(api) {
89            builder.add_import(&trait_path);
90        }
91
92        // Only import HashMap when Map-typed fields or returns are present
93        let has_maps = api
94            .types
95            .iter()
96            .any(|t| t.fields.iter().any(|f| matches!(&f.ty, TypeRef::Map(_, _))))
97            || api
98                .functions
99                .iter()
100                .any(|f| matches!(&f.return_type, TypeRef::Map(_, _)));
101        if has_maps {
102            builder.add_import("std::collections::HashMap");
103        }
104
105        // Note: custom_modules for Node are TypeScript-only re-exports
106        // (used in generate_public_api), not Rust module declarations.
107
108        // Check if any function or method is async
109        let has_async =
110            api.functions.iter().any(|f| f.is_async) || api.types.iter().any(|t| t.methods.iter().any(|m| m.is_async));
111
112        if has_async {
113            builder.add_item(&gen_tokio_runtime());
114        }
115
116        // Check if we have opaque types and add Arc import if needed
117        let opaque_types: AHashSet<String> = api
118            .types
119            .iter()
120            .filter(|t| t.is_opaque)
121            .map(|t| t.name.clone())
122            .collect();
123        if !opaque_types.is_empty() {
124            builder.add_import("std::sync::Arc");
125        }
126
127        let exclude_types: ahash::AHashSet<String> = config
128            .node
129            .as_ref()
130            .map(|c| c.exclude_types.iter().cloned().collect())
131            .unwrap_or_default();
132
133        // Build adapter body map before type iteration so bodies are available for method generation.
134        let adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Node)?;
135
136        // Emit adapter-generated standalone items (streaming iterators, callback bridges).
137        for adapter in &config.adapters {
138            match adapter.pattern {
139                alef_core::config::AdapterPattern::Streaming => {
140                    let key = format!("{}.__stream_struct__", adapter.item_type.as_deref().unwrap_or(""));
141                    if let Some(struct_code) = adapter_bodies.get(&key) {
142                        builder.add_item(struct_code);
143                    }
144                }
145                alef_core::config::AdapterPattern::CallbackBridge => {
146                    let struct_key = format!("{}.__bridge_struct__", adapter.name);
147                    let impl_key = format!("{}.__bridge_impl__", adapter.name);
148                    if let Some(struct_code) = adapter_bodies.get(&struct_key) {
149                        builder.add_item(struct_code);
150                    }
151                    if let Some(impl_code) = adapter_bodies.get(&impl_key) {
152                        builder.add_item(impl_code);
153                    }
154                }
155                _ => {}
156            }
157        }
158
159        // NAPI has some unique patterns: Js-prefixed names, Option-wrapped fields,
160        // and custom constructor. Use shared generators for enums and functions,
161        // but keep struct/method generation custom.
162        for typ in api
163            .types
164            .iter()
165            .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
166        {
167            if typ.is_opaque {
168                builder.add_item(&alef_codegen::generators::gen_opaque_struct_prefixed(
169                    typ, &cfg, &prefix,
170                ));
171                builder.add_item(&gen_opaque_struct_methods(
172                    typ,
173                    &mapper,
174                    &cfg,
175                    &opaque_types,
176                    &prefix,
177                    &adapter_bodies,
178                ));
179            } else {
180                // Non-opaque structs use #[napi(object)] — plain JS objects without methods.
181                // napi(object) structs cannot have #[napi] impl blocks.
182                // gen_struct adds Default to derives when typ.has_default is true.
183                builder.add_item(&gen_struct(typ, &mapper, &prefix, has_serde));
184            }
185        }
186
187        // Collect struct names so tagged enum codegen knows which Named types have binding structs
188        let struct_names: ahash::AHashSet<String> = api.types.iter().map(|t| t.name.clone()).collect();
189
190        for enum_def in &api.enums {
191            builder.add_item(&gen_enum(enum_def, &prefix, has_serde));
192        }
193
194        let exclude_functions: ahash::AHashSet<String> = config
195            .node
196            .as_ref()
197            .map(|c| c.exclude_functions.iter().cloned().collect())
198            .unwrap_or_default();
199
200        for func in &api.functions {
201            if exclude_functions.contains(&func.name) {
202                continue;
203            }
204            // Skip sanitized functions — they cannot be auto-delegated and emitting a stub
205            // would expose a broken placeholder in the public API.
206            if func.sanitized {
207                continue;
208            }
209            let bridge_param = crate::trait_bridge::find_bridge_param(func, &config.trait_bridges);
210            if let Some((param_idx, bridge_cfg)) = bridge_param {
211                builder.add_item(&crate::trait_bridge::gen_bridge_function(
212                    func,
213                    param_idx,
214                    bridge_cfg,
215                    &mapper,
216                    &cfg,
217                    &Default::default(),
218                    &opaque_types,
219                    &core_import,
220                ));
221            } else {
222                builder.add_item(&gen_function(func, &mapper, &cfg, &opaque_types, &prefix));
223            }
224        }
225
226        // Trait bridge wrappers — generate NAPI bridge structs that delegate to JS objects
227        for bridge_cfg in &config.trait_bridges {
228            if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
229                let bridge = crate::trait_bridge::gen_trait_bridge(
230                    trait_type,
231                    bridge_cfg,
232                    &core_import,
233                    &config.error_type(),
234                    &config.error_constructor(),
235                    api,
236                );
237                for imp in &bridge.imports {
238                    builder.add_import(imp);
239                }
240                builder.add_item(&bridge.code);
241            }
242        }
243
244        let binding_to_core = alef_codegen::conversions::convertible_types(api);
245        let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
246        let input_types = alef_codegen::conversions::input_type_names(api);
247        let napi_conv_config = alef_codegen::conversions::ConversionConfig {
248            type_name_prefix: &prefix,
249            cast_large_ints_to_i64: true,
250            cast_f32_to_f64: true,
251            // optionalize_defaults: For types with has_default, conversion generators
252            // make all fields Option<T> and apply defaults via FromNapiValue,
253            // enabling JS users to pass partial objects and omit fields they want defaults for.
254            optionalize_defaults: true,
255            option_duration_on_defaults: true,
256            include_cfg_metadata: true,
257            ..Default::default()
258        };
259        // From/Into conversions using shared parameterized generators
260        for typ in api.types.iter().filter(|typ| !typ.is_trait) {
261            if input_types.contains(&typ.name)
262                && alef_codegen::conversions::can_generate_conversion(typ, &binding_to_core)
263            {
264                builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
265                    typ,
266                    &core_import,
267                    &napi_conv_config,
268                ));
269            }
270            if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
271                builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
272                    typ,
273                    &core_import,
274                    &opaque_types,
275                    &napi_conv_config,
276                ));
277            }
278        }
279        for e in &api.enums {
280            let is_tagged_data_enum = e.serde_tag.is_some() && e.variants.iter().any(|v| !v.fields.is_empty());
281            if is_tagged_data_enum {
282                // Tagged data enums use flattened struct — generate custom conversions
283                builder.add_item(&gen_tagged_enum_binding_to_core(
284                    e,
285                    &core_import,
286                    &prefix,
287                    &struct_names,
288                ));
289                builder.add_item(&gen_tagged_enum_core_to_binding(
290                    e,
291                    &core_import,
292                    &prefix,
293                    &struct_names,
294                ));
295            } else {
296                if input_types.contains(&e.name) && alef_codegen::conversions::can_generate_enum_conversion(e) {
297                    builder.add_item(&alef_codegen::conversions::gen_enum_from_binding_to_core_cfg(
298                        e,
299                        &core_import,
300                        &napi_conv_config,
301                    ));
302                }
303                if alef_codegen::conversions::can_generate_enum_conversion_from_core(e) {
304                    builder.add_item(&alef_codegen::conversions::gen_enum_from_core_to_binding_cfg(
305                        e,
306                        &core_import,
307                        &napi_conv_config,
308                    ));
309                }
310            }
311        }
312
313        // Error types (variant name constants + converter functions)
314        for error in &api.errors {
315            builder.add_item(&alef_codegen::error_gen::gen_napi_error_types(error));
316            builder.add_item(&alef_codegen::error_gen::gen_napi_error_converter(error, &core_import));
317        }
318
319        let content = builder.build();
320
321        let output_dir = resolve_output_dir(
322            config.output.node.as_ref(),
323            &config.crate_config.name,
324            "crates/{name}-node/src/",
325        );
326
327        Ok(vec![GeneratedFile {
328            path: PathBuf::from(&output_dir).join("lib.rs"),
329            content,
330            generated_header: false,
331        }])
332    }
333
334    fn generate_public_api(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
335        let prefix = config.node_type_prefix();
336
337        // Separate exports into functions (plain export) and types (export type)
338        let mut type_exports = vec![];
339        let mut function_exports = vec![];
340
341        // Collect all types (exported with prefix from native module) - export type
342        for typ in api.types.iter().filter(|typ| !typ.is_trait) {
343            type_exports.push(format!("{prefix}{}", typ.name));
344        }
345
346        // Collect all enums as type exports.
347        // With verbatimModuleSyntax enabled, re-exporting const enums as values causes
348        // TS2748/TS1205; using `export type` avoids both errors.
349        for enum_def in &api.enums {
350            type_exports.push(format!("{prefix}{}", enum_def.name));
351        }
352
353        // NAPI errors are thrown as native JS Error objects, not exported as TS types.
354        // Skip error types in the public API re-exports.
355
356        // Collect all functions (exported from native module) - plain export
357        for func in &api.functions {
358            // Convert snake_case to camelCase for JavaScript naming
359            let js_name = to_node_name(&func.name);
360            function_exports.push(js_name);
361        }
362
363        // Sort for consistent output
364        type_exports.sort();
365        function_exports.sort();
366
367        // Generate the index.ts re-export file using a single export block
368        // with inline `type` annotations for verbatimModuleSyntax compatibility.
369        let mut lines = vec![
370            "// This file is auto-generated by alef. DO NOT EDIT.".to_string(),
371            "".to_string(),
372        ];
373
374        // Separate value and type exports for verbatimModuleSyntax compatibility.
375        // Value exports (functions) in one block, type exports (structs + enums) in another.
376        if !function_exports.is_empty() {
377            lines.push("export {".to_string());
378            for name in &function_exports {
379                lines.push(format!("  {name},"));
380            }
381            lines.push(format!("}} from '{}';", config.node_package_name()));
382            lines.push("".to_string());
383        }
384        if !type_exports.is_empty() {
385            lines.push("export type {".to_string());
386            for name in &type_exports {
387                lines.push(format!("  {name},"));
388            }
389            lines.push(format!("}} from '{}';", config.node_package_name()));
390        }
391
392        // Append re-exports for custom modules (from [custom_modules] node = [...])
393        let custom_mods = config.custom_modules.for_language(Language::Node);
394        for module_name in custom_mods {
395            lines.push(format!("export * from './{module_name}';"));
396        }
397
398        let content = lines.join("\n");
399
400        // Output path: packages/typescript/src/index.ts
401        let output_path = PathBuf::from("packages/typescript/src/index.ts");
402
403        Ok(vec![GeneratedFile {
404            path: output_path,
405            content,
406            generated_header: false,
407        }])
408    }
409
410    fn generate_type_stubs(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
411        let prefix = config.node_type_prefix();
412        let content = gen_dts(api, &prefix);
413
414        // `config.output.node` points to the `src/` directory (e.g., `crates/{name}-node/src/`).
415        // `index.d.ts` belongs at the crate root, one level up from `src/`.
416        // When the configured path ends in `src/` or `src`, strip that suffix to get the crate root.
417        // Falls back to `crates/{name}-node/` if no node output is configured.
418        let src_dir = resolve_output_dir(
419            config.output.node.as_ref(),
420            &config.crate_config.name,
421            "crates/{name}-node/src/",
422        );
423        let crate_root = {
424            let p = PathBuf::from(&src_dir);
425            match p.file_name().and_then(|n| n.to_str()) {
426                Some("src") => p.parent().map(|parent| parent.to_path_buf()).unwrap_or(p),
427                _ => p,
428            }
429        };
430
431        Ok(vec![GeneratedFile {
432            path: crate_root.join("index.d.ts"),
433            content,
434            generated_header: false,
435        }])
436    }
437
438    fn build_config(&self) -> Option<BuildConfig> {
439        Some(BuildConfig {
440            tool: "napi",
441            crate_suffix: "-node",
442            depends_on_ffi: false,
443            post_build: vec![PostBuildStep::PatchFile {
444                path: "index.d.ts",
445                find: "export declare const enum",
446                replace: "export declare enum",
447            }],
448        })
449    }
450}
451
452/// Generate a NAPI struct with Js-prefixed name and fields wrapped in Option only if optional.
453fn gen_struct(typ: &TypeDef, mapper: &NapiMapper, prefix: &str, has_serde: bool) -> String {
454    let mut struct_builder = StructBuilder::new(&format!("{prefix}{}", typ.name));
455    // Use napi(object) so the struct can be used as function/method parameters (FromNapiValue)
456    struct_builder.add_attr("napi(object)");
457    struct_builder.add_derive("Clone");
458    // Binding types always derive Default, Serialize, and Deserialize.
459    // Default: enables using unwrap_or_default() in constructors for types with has_default.
460    // Serialize/Deserialize: required for FFI/type conversion across binding boundaries.
461    struct_builder.add_derive("Default");
462    // Only derive serde traits when the binding crate has serde as a dependency.
463    // Generating these derives unconditionally causes compile errors in crates
464    // that don't list serde in their Cargo.toml.
465    if has_serde {
466        struct_builder.add_derive("serde::Serialize");
467        struct_builder.add_derive("serde::Deserialize");
468    }
469
470    for field in &typ.fields {
471        let mapped_type = mapper.map_type(&field.ty);
472        // For types with Default, make all fields optional so JS callers
473        // can pass partial objects (missing fields get defaults).
474        // When field.ty is already Optional(T), mapped_type is already Option<T> — don't double-wrap.
475        let field_type = if (field.optional || typ.has_default) && !matches!(field.ty, TypeRef::Optional(_)) {
476            format!("Option<{}>", mapped_type)
477        } else {
478            mapped_type
479        };
480        let js_name = to_node_name(&field.name);
481        let attrs = if js_name != field.name {
482            vec![format!("napi(js_name = \"{}\")", js_name)]
483        } else {
484            vec![]
485        };
486        struct_builder.add_field(&field.name, &field_type, attrs);
487    }
488
489    struct_builder.build()
490}
491
492/// Generate NAPI methods for an opaque struct (delegates to self.inner).
493fn gen_opaque_struct_methods(
494    typ: &TypeDef,
495    mapper: &NapiMapper,
496    cfg: &RustBindingConfig,
497    opaque_types: &AHashSet<String>,
498    prefix: &str,
499    adapter_bodies: &alef_adapters::AdapterBodies,
500) -> String {
501    let mut impl_builder = ImplBuilder::new(&format!("{prefix}{}", typ.name));
502    impl_builder.add_attr("napi");
503
504    let (instance, statics) = partition_methods(&typ.methods);
505
506    for method in &instance {
507        // Skip sanitized methods that have no adapter override — they cannot be delegated
508        // and emitting an unimplemented stub pollutes the public API with dead placeholders.
509        let adapter_key = format!("{}.{}", typ.name, method.name);
510        if method.sanitized && !adapter_bodies.contains_key(&adapter_key) {
511            continue;
512        }
513        impl_builder.add_method(&gen_opaque_instance_method(
514            method,
515            mapper,
516            typ,
517            cfg,
518            opaque_types,
519            prefix,
520            adapter_bodies,
521        ));
522    }
523    for method in &statics {
524        // Skip sanitized static methods that have no adapter override.
525        let adapter_key = format!("{}.{}", typ.name, method.name);
526        if method.sanitized && !adapter_bodies.contains_key(&adapter_key) {
527            continue;
528        }
529        impl_builder.add_method(&gen_static_method(method, mapper, typ, cfg, opaque_types, prefix));
530    }
531
532    impl_builder.build()
533}
534
535/// Generate an opaque instance method that delegates to self.inner.
536fn gen_opaque_instance_method(
537    method: &MethodDef,
538    mapper: &NapiMapper,
539    typ: &TypeDef,
540    cfg: &RustBindingConfig,
541    opaque_types: &AHashSet<String>,
542    prefix: &str,
543    adapter_bodies: &alef_adapters::AdapterBodies,
544) -> String {
545    let params = function_params(&method.params, &|ty| mapper.map_type(ty));
546    let return_type = mapper.map_type(&method.return_type);
547    let return_annotation = mapper.wrap_return(&return_type, method.error_type.is_some());
548
549    let js_name = to_node_name(&method.name);
550    let js_name_attr = if js_name != method.name {
551        format!("(js_name = \"{}\")", js_name)
552    } else {
553        String::new()
554    };
555
556    let async_kw = if method.is_async { "async " } else { "" };
557
558    let type_name = &typ.name;
559    let is_owned_receiver = matches!(method.receiver.as_ref(), Some(alef_core::ir::ReceiverKind::Owned));
560    let is_ref_mut_receiver = matches!(method.receiver.as_ref(), Some(alef_core::ir::ReceiverKind::RefMut));
561    let call_args = napi_gen_call_args(&method.params, opaque_types);
562
563    // Use the shared can_auto_delegate check for opaque instance methods.
564    // Skip delegation if the receiver is RefMut, since Arc<T> doesn't support &mut T.
565    let opaque_can_delegate = !method.sanitized
566        && !is_ref_mut_receiver
567        && (!is_owned_receiver || typ.is_clone)
568        && method
569            .params
570            .iter()
571            .all(|p| !p.sanitized && alef_codegen::shared::is_delegatable_param(&p.ty, opaque_types))
572        && alef_codegen::shared::is_opaque_delegatable_type(&method.return_type);
573
574    let make_async_core_call = |method_name: &str| -> String { format!("inner.{method_name}({call_args})") };
575
576    let async_result_wrap = napi_wrap_return(
577        "result",
578        &method.return_type,
579        type_name,
580        opaque_types,
581        true,
582        method.returns_ref,
583        prefix,
584    );
585
586    let adapter_key = format!("{type_name}.{}", method.name);
587    let body = if let Some(adapter_body) = adapter_bodies.get(&adapter_key) {
588        adapter_body.clone()
589    } else if !opaque_can_delegate {
590        // Try serde-based param conversion for methods with non-opaque Named params
591        if cfg.has_serde
592            && !method.sanitized
593            && generators::has_named_params(&method.params, opaque_types)
594            && method.error_type.is_some()
595            && alef_codegen::shared::is_opaque_delegatable_type(&method.return_type)
596        {
597            let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
598            let serde_bindings =
599                generators::gen_serde_let_bindings(&method.params, opaque_types, cfg.core_import, err_conv, "        ");
600            let serde_call_args = generators::gen_call_args_with_let_bindings(&method.params, opaque_types);
601            let core_call = format!("self.inner.{}({serde_call_args})", method.name);
602            if matches!(method.return_type, TypeRef::Unit) {
603                format!("{serde_bindings}{core_call}{err_conv}?;\n    Ok(())")
604            } else {
605                let wrap = napi_wrap_return(
606                    "result",
607                    &method.return_type,
608                    type_name,
609                    opaque_types,
610                    true,
611                    method.returns_ref,
612                    prefix,
613                );
614                format!("{serde_bindings}let result = {core_call}{err_conv}?;\n    Ok({wrap})")
615            }
616        } else {
617            generators::gen_unimplemented_body(
618                &method.return_type,
619                &format!("{type_name}.{}", method.name),
620                method.error_type.is_some(),
621                cfg,
622                &method.params,
623                opaque_types,
624            )
625        }
626    } else if method.is_async {
627        let inner_clone_line = "let inner = self.inner.clone();\n    ";
628        let core_call_str = make_async_core_call(&method.name);
629        generators::gen_async_body(
630            &core_call_str,
631            cfg,
632            method.error_type.is_some(),
633            &async_result_wrap,
634            true,
635            inner_clone_line,
636            matches!(method.return_type, TypeRef::Unit),
637            Some(&return_type),
638        )
639    } else {
640        // When any non-opaque Named param has is_ref=true, generate let-bindings before the call
641        // to avoid E0716 ("temporary value dropped while borrowed"). The inline `.into()` pattern
642        // creates a temporary that Rust can't borrow for the duration of the call expression.
643        let use_let_bindings = generators::has_named_params(&method.params, opaque_types);
644        let (let_bindings, call_args_for_call) = if use_let_bindings {
645            let bindings = generators::gen_named_let_bindings_pub(&method.params, opaque_types, cfg.core_import);
646            let args = napi_apply_primitive_casts_to_call_args(
647                &generators::gen_call_args_with_let_bindings(&method.params, opaque_types),
648                &method.params,
649            );
650            (bindings, args)
651        } else {
652            (String::new(), napi_gen_call_args(&method.params, opaque_types))
653        };
654        let core_call = if is_owned_receiver {
655            format!("(*self.inner).clone().{}({})", method.name, call_args_for_call)
656        } else {
657            format!("self.inner.{}({})", method.name, call_args_for_call)
658        };
659        if method.error_type.is_some() {
660            let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
661            if matches!(method.return_type, TypeRef::Unit) {
662                format!("{let_bindings}{core_call}{err_conv}?;\n    Ok(())")
663            } else {
664                let wrap = napi_wrap_return(
665                    "result",
666                    &method.return_type,
667                    type_name,
668                    opaque_types,
669                    true,
670                    method.returns_ref,
671                    prefix,
672                );
673                format!("{let_bindings}let result = {core_call}{err_conv}?;\n    Ok({wrap})")
674            }
675        } else {
676            format!(
677                "{let_bindings}{}",
678                napi_wrap_return(
679                    &core_call,
680                    &method.return_type,
681                    type_name,
682                    opaque_types,
683                    true,
684                    method.returns_ref,
685                    prefix,
686                )
687            )
688        }
689    };
690
691    let mut attrs = String::new();
692    // Per-item clippy suppression: too_many_arguments when >7 params (including &self)
693    if method.params.len() + 1 > 7 {
694        attrs.push_str("#[allow(clippy::too_many_arguments)]\n");
695    }
696    // Per-item clippy suppression: missing_errors_doc for Result-returning methods
697    if method.error_type.is_some() {
698        attrs.push_str("#[allow(clippy::missing_errors_doc)]\n");
699    }
700    // Per-item clippy suppression: should_implement_trait for trait-conflicting names
701    if generators::is_trait_method_name(&method.name) {
702        attrs.push_str("#[allow(clippy::should_implement_trait)]\n");
703    }
704    format!(
705        "{attrs}#[napi{js_name_attr}]\npub {async_kw}fn {}(&self, {params}) -> {return_annotation} {{\n    \
706         {body}\n}}",
707        method.name
708    )
709}
710
711/// Generate a static method binding.
712fn gen_static_method(
713    method: &MethodDef,
714    mapper: &NapiMapper,
715    typ: &TypeDef,
716    cfg: &RustBindingConfig,
717    opaque_types: &AHashSet<String>,
718    prefix: &str,
719) -> String {
720    let params = function_params(&method.params, &|ty| mapper.map_type(ty));
721    let return_type = mapper.map_type(&method.return_type);
722    let return_annotation = mapper.wrap_return(&return_type, method.error_type.is_some());
723
724    let js_name = to_node_name(&method.name);
725    let js_name_attr = if js_name != method.name {
726        format!("(js_name = \"{}\")", js_name)
727    } else {
728        String::new()
729    };
730
731    let type_name = &typ.name;
732    let core_type_path = typ.rust_path.replace('-', "_");
733    let call_args = napi_gen_call_args(&method.params, opaque_types);
734    let can_delegate_static = can_auto_delegate(method, opaque_types);
735
736    let async_kw = if method.is_async { "async " } else { "" };
737
738    let body = if !can_delegate_static {
739        generators::gen_unimplemented_body(
740            &method.return_type,
741            &format!("{type_name}::{}", method.name),
742            method.error_type.is_some(),
743            cfg,
744            &method.params,
745            opaque_types,
746        )
747    } else if method.is_async {
748        let core_call = format!("{core_type_path}::{}({call_args})", method.name);
749        let return_wrap = napi_wrap_return(
750            "result",
751            &method.return_type,
752            type_name,
753            opaque_types,
754            typ.is_opaque,
755            method.returns_ref,
756            prefix,
757        );
758        generators::gen_async_body(
759            &core_call,
760            cfg,
761            method.error_type.is_some(),
762            &return_wrap,
763            false,
764            "",
765            matches!(method.return_type, TypeRef::Unit),
766            Some(&return_type),
767        )
768    } else {
769        let core_call = format!("{core_type_path}::{}({call_args})", method.name);
770        if method.error_type.is_some() {
771            let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
772            let wrapped = napi_wrap_return(
773                "val",
774                &method.return_type,
775                type_name,
776                opaque_types,
777                typ.is_opaque,
778                method.returns_ref,
779                prefix,
780            );
781            if wrapped == "val" {
782                format!("{core_call}{err_conv}")
783            } else {
784                format!("{core_call}.map(|val| {wrapped}){err_conv}")
785            }
786        } else {
787            napi_wrap_return(
788                &core_call,
789                &method.return_type,
790                type_name,
791                opaque_types,
792                typ.is_opaque,
793                method.returns_ref,
794                prefix,
795            )
796        }
797    };
798
799    let mut attrs = String::new();
800    // Per-item clippy suppression: too_many_arguments when >7 params
801    if method.params.len() > 7 {
802        attrs.push_str("#[allow(clippy::too_many_arguments)]\n");
803    }
804    // Per-item clippy suppression: missing_errors_doc for Result-returning methods
805    if method.error_type.is_some() {
806        attrs.push_str("#[allow(clippy::missing_errors_doc)]\n");
807    }
808    // Per-item clippy suppression: should_implement_trait for trait-conflicting names
809    if generators::is_trait_method_name(&method.name) {
810        attrs.push_str("#[allow(clippy::should_implement_trait)]\n");
811    }
812    format!(
813        "{attrs}#[napi{js_name_attr}]\npub {async_kw}fn {}({params}) -> {return_annotation} {{\n    \
814         {body}\n}}",
815        method.name
816    )
817}
818
819/// Generate a NAPI enum definition using string_enum with Js prefix.
820/// Generate a NAPI enum definition.
821/// For simple enums (no variant fields): generates `#[napi(string_enum)]`.
822/// For tagged enums with data fields: generates a flattened `#[napi(object)]` struct
823/// with a discriminant field and all variant fields as optional.
824fn gen_enum(enum_def: &EnumDef, prefix: &str, has_serde: bool) -> String {
825    let is_tagged_data_enum = enum_def.serde_tag.is_some() && enum_def.variants.iter().any(|v| !v.fields.is_empty());
826
827    if is_tagged_data_enum {
828        return gen_tagged_enum_as_object(enum_def, prefix, has_serde);
829    }
830
831    // Simple string enum
832    let napi_case = enum_def.serde_rename_all.as_deref().and_then(|s| match s {
833        "snake_case" => Some("snake_case"),
834        "camelCase" => Some("camelCase"),
835        "kebab-case" => Some("kebab-case"),
836        "SCREAMING_SNAKE_CASE" => Some("UPPER_SNAKE"),
837        "lowercase" => Some("lowercase"),
838        "UPPERCASE" => Some("UPPERCASE"),
839        "PascalCase" => Some("PascalCase"),
840        _ => None,
841    });
842
843    let string_enum_attr = match napi_case {
844        Some(case) => format!("#[napi(string_enum = \"{case}\")]"),
845        None => "#[napi(string_enum)]".to_string(),
846    };
847
848    let derives = if has_serde {
849        "#[derive(Clone, serde::Serialize, serde::Deserialize)]".to_string()
850    } else {
851        "#[derive(Clone)]".to_string()
852    };
853    let mut lines = vec![
854        string_enum_attr,
855        derives,
856        format!("pub enum {prefix}{} {{", enum_def.name),
857    ];
858
859    for variant in &enum_def.variants {
860        lines.push(format!("    {},", variant.name));
861    }
862
863    lines.push("}".to_string());
864
865    // Default impl for config constructor unwrap_or_default()
866    if let Some(first) = enum_def.variants.first() {
867        lines.push(String::new());
868        lines.push("#[allow(clippy::derivable_impls)]".to_string());
869        lines.push(format!("impl Default for {prefix}{} {{", enum_def.name));
870        lines.push(format!("    fn default() -> Self {{ Self::{} }}", first.name));
871        lines.push("}".to_string());
872    }
873
874    lines.join("\n")
875}
876
877/// Generate a tagged enum as a flattened `#[napi(object)]` struct.
878/// E.g. `AuthConfig { Basic { username, password }, Bearer { token } }` becomes:
879/// ```rust,ignore
880/// #[napi(object)]
881/// struct JsAuthConfig {
882///     #[napi(js_name = "type")]
883///     pub auth_type: String,
884///     pub username: Option<String>,
885///     pub password: Option<String>,
886///     pub token: Option<String>,
887/// }
888/// ```
889fn gen_tagged_enum_as_object(enum_def: &EnumDef, prefix: &str, has_serde: bool) -> String {
890    use alef_codegen::type_mapper::TypeMapper;
891    let mapper = NapiMapper::new(prefix.to_string());
892
893    let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
894
895    let derive = if has_serde {
896        "#[derive(Clone, serde::Serialize, serde::Deserialize)]"
897    } else {
898        "#[derive(Clone)]"
899    };
900    let mut lines = vec![
901        derive.to_string(),
902        "#[napi(object)]".to_string(),
903        format!("pub struct {prefix}{} {{", enum_def.name),
904        format!("    #[napi(js_name = \"{tag_field}\")]"),
905        format!("    pub {tag_field}_tag: String,"),
906    ];
907
908    // Fields that appear in multiple variants with different Named types cannot be represented
909    // as a single concrete JsXxx type. Store them as String (JSON) instead, and convert
910    // per-variant via serde_json in the From impls.
911    let mixed_named_fields = tagged_enum_mixed_named_fields(enum_def);
912
913    // Collect all unique fields across all variants (all made optional)
914    let mut seen_fields: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
915    for variant in &enum_def.variants {
916        for field in &variant.fields {
917            if seen_fields.insert(field.name.clone()) {
918                // Sanitized fields and mixed-type Named fields are represented as String
919                // and converted via serde_json in From/Into impls
920                let field_type = if (field.sanitized || mixed_named_fields.contains(&field.name))
921                    && matches!(&field.ty, TypeRef::Named(_))
922                {
923                    "String".to_string()
924                } else {
925                    mapper.map_type(&field.ty).to_string()
926                };
927                let js_name = alef_codegen::naming::to_node_name(&field.name);
928                if js_name != field.name {
929                    lines.push(format!("    #[napi(js_name = \"{js_name}\")]"));
930                }
931                lines.push(format!("    pub {}: Option<{field_type}>,", field.name));
932            }
933        }
934    }
935
936    lines.push("}".to_string());
937
938    // Default impl
939    lines.push(String::new());
940    lines.push("#[allow(clippy::derivable_impls)]".to_string());
941    lines.push(format!("impl Default for {prefix}{} {{", enum_def.name));
942    lines.push(format!(
943        "    fn default() -> Self {{ Self {{ {tag_field}_tag: String::new(), {} }} }}",
944        seen_fields
945            .iter()
946            .map(|f| format!("{f}: None"))
947            .collect::<Vec<_>>()
948            .join(", ")
949    ));
950    lines.push("}".to_string());
951
952    lines.join("\n")
953}
954
955/// Generate a free function binding.
956fn gen_function(
957    func: &FunctionDef,
958    mapper: &NapiMapper,
959    cfg: &RustBindingConfig,
960    opaque_types: &AHashSet<String>,
961    prefix: &str,
962) -> String {
963    let params = function_params(&func.params, &|ty| {
964        // Opaque Named params must be received by reference since NAPI opaque
965        // structs don't implement FromNapiValue (they use Arc<T> internally).
966        if let TypeRef::Named(n) = ty {
967            if opaque_types.contains(n.as_str()) {
968                return format!("&{prefix}{n}");
969            }
970        }
971        mapper.map_type(ty)
972    });
973    let return_type = mapper.map_type(&func.return_type);
974    let return_annotation = mapper.wrap_return(&return_type, func.error_type.is_some());
975
976    let js_name = to_node_name(&func.name);
977    let js_name_attr = if js_name != func.name {
978        format!("(js_name = \"{}\")", js_name)
979    } else {
980        String::new()
981    };
982
983    let core_import = cfg.core_import;
984    let core_fn_path = {
985        let path = func.rust_path.replace('-', "_");
986        if path.starts_with(core_import) {
987            path
988        } else {
989            format!("{core_import}::{}", func.name)
990        }
991    };
992
993    // Use let-binding pattern for non-opaque Named params, or for Vec<f32> params that need conversion
994    let use_let_bindings = generators::has_named_params(&func.params, opaque_types)
995        || func.params.iter().any(|p| needs_vec_f32_conversion(&p.ty));
996    let call_args = if use_let_bindings {
997        let base_args = generators::gen_call_args_with_let_bindings(&func.params, opaque_types);
998        napi_apply_primitive_casts_to_call_args(&base_args, &func.params)
999    } else {
1000        napi_gen_call_args(&func.params, opaque_types)
1001    };
1002
1003    let can_delegate_fn = alef_codegen::shared::can_auto_delegate_function(func, opaque_types);
1004
1005    let err_conv = ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))";
1006
1007    let async_kw = if func.is_async { "async " } else { "" };
1008
1009    let body = if !can_delegate_fn {
1010        // Try serde-based conversion for non-delegatable functions with Named params
1011        // Only use serde conversion if cfg.has_serde is true (binding crate has serde deps)
1012        if cfg.has_serde && use_let_bindings && func.error_type.is_some() {
1013            let serde_bindings =
1014                generators::gen_serde_let_bindings(&func.params, opaque_types, core_import, err_conv, "    ");
1015            // Also generate Vec<String>+is_ref bindings (names_refs) since serde doesn't handle them
1016            let vec_str_bindings: String = func.params.iter().filter(|p| {
1017                p.is_ref && matches!(&p.ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::String | TypeRef::Char))
1018            }).map(|p| {
1019                format!("let {}_refs: Vec<&str> = {}.iter().map(|s| s.as_str()).collect();\n    ", p.name, p.name)
1020            }).collect();
1021            let core_call = format!("{core_fn_path}({call_args})");
1022            let await_kw = if func.is_async { ".await" } else { "" };
1023
1024            if matches!(func.return_type, TypeRef::Unit) {
1025                format!("{vec_str_bindings}{serde_bindings}{core_call}{await_kw}{err_conv}?;\n    Ok(())")
1026            } else {
1027                let wrapped = napi_wrap_return_fn("val", &func.return_type, opaque_types, func.returns_ref, prefix);
1028                if wrapped == "val" {
1029                    format!("{vec_str_bindings}{serde_bindings}{core_call}{await_kw}{err_conv}")
1030                } else {
1031                    format!("{vec_str_bindings}{serde_bindings}{core_call}{await_kw}.map(|val| {wrapped}){err_conv}")
1032                }
1033            }
1034        } else {
1035            generators::gen_unimplemented_body(
1036                &func.return_type,
1037                &func.name,
1038                func.error_type.is_some(),
1039                cfg,
1040                &func.params,
1041                opaque_types,
1042            )
1043        }
1044    } else if func.is_async {
1045        // For async delegatable functions, generate let bindings if needed before the async call
1046        let mut let_bindings = if use_let_bindings {
1047            generators::gen_named_let_bindings_pub(&func.params, opaque_types, core_import)
1048        } else {
1049            String::new()
1050        };
1051        // Add Vec<f32> conversion bindings for parameters not already handled
1052        let_bindings.push_str(&gen_vec_f32_conversion_bindings(&func.params));
1053        let core_call = format!("{core_fn_path}({call_args})");
1054        let return_wrap = napi_wrap_return_fn("result", &func.return_type, opaque_types, func.returns_ref, prefix);
1055        let return_type = mapper.map_type(&func.return_type);
1056        generators::gen_async_body(
1057            &core_call,
1058            cfg,
1059            func.error_type.is_some(),
1060            &return_wrap,
1061            false,
1062            &let_bindings,
1063            matches!(func.return_type, TypeRef::Unit),
1064            Some(&return_type),
1065        )
1066    } else {
1067        let core_call = format!("{core_fn_path}({call_args})");
1068        // Generate let bindings for Named params if needed
1069        let mut let_bindings = if use_let_bindings {
1070            generators::gen_named_let_bindings_pub(&func.params, opaque_types, core_import)
1071        } else {
1072            String::new()
1073        };
1074        // Add Vec<f32> conversion bindings for parameters not already handled
1075        let_bindings.push_str(&gen_vec_f32_conversion_bindings(&func.params));
1076
1077        if func.error_type.is_some() {
1078            let wrapped = napi_wrap_return_fn("val", &func.return_type, opaque_types, func.returns_ref, prefix);
1079            if wrapped == "val" {
1080                format!("{let_bindings}{core_call}{err_conv}")
1081            } else {
1082                format!("{let_bindings}{core_call}.map(|val| {wrapped}){err_conv}")
1083            }
1084        } else {
1085            format!(
1086                "{let_bindings}{}",
1087                napi_wrap_return_fn(&core_call, &func.return_type, opaque_types, func.returns_ref, prefix)
1088            )
1089        }
1090    };
1091
1092    let mut attrs = String::new();
1093    // Per-item clippy suppression: too_many_arguments when >7 params
1094    if func.params.len() > 7 {
1095        attrs.push_str("#[allow(clippy::too_many_arguments)]\n");
1096    }
1097    // Per-item clippy suppression: missing_errors_doc for Result-returning functions
1098    if func.error_type.is_some() {
1099        attrs.push_str("#[allow(clippy::missing_errors_doc)]\n");
1100    }
1101    format!(
1102        "{attrs}#[napi{js_name_attr}]\npub {async_kw}fn {}({params}) -> {return_annotation} {{\n    \
1103         {body}\n}}",
1104        func.name
1105    )
1106}
1107
1108/// Apply NAPI-specific primitive casts to the call args generated by the generic let-binding handler.
1109/// Adds i64→usize, i64→isize, f64→f32 casts where needed.
1110fn napi_apply_primitive_casts_to_call_args(generic_args: &str, params: &[ParamDef]) -> String {
1111    // Split args by comma and match with params to apply casting
1112    let args_list: Vec<&str> = generic_args.split(',').map(|s| s.trim()).collect();
1113    args_list
1114        .iter()
1115        .zip(params.iter())
1116        .map(|(arg, p)| {
1117            // Special case: Vec<f32> param with is_ref uses the converted variable
1118            if needs_vec_f32_conversion(&p.ty) && p.is_ref {
1119                return format!("&{}_f32", p.name);
1120            }
1121            match &p.ty {
1122                TypeRef::Primitive(prim) if needs_napi_cast(prim) => {
1123                    let core_ty = core_prim_str(prim);
1124                    if p.optional {
1125                        // Optional: arg might be like "param.map(...)" so re-apply map
1126                        if arg.contains(".map(") || arg.contains(".as_") {
1127                            // Already handled, keep as is
1128                            arg.to_string()
1129                        } else {
1130                            format!("{}.map(|v| v as {})", arg, core_ty)
1131                        }
1132                    } else {
1133                        // Non-optional: simple cast
1134                        format!("{} as {}", arg, core_ty)
1135                    }
1136                }
1137                _ => arg.to_string(),
1138            }
1139        })
1140        .collect::<Vec<_>>()
1141        .join(", ")
1142}
1143
1144/// Generate let bindings for Vec<f32> parameters that need f64→f32 conversion.
1145/// This handles the case where NAPI maps f32→f64, but a function param is Vec<f32> taking a reference.
1146fn gen_vec_f32_conversion_bindings(params: &[ParamDef]) -> String {
1147    let mut bindings = String::new();
1148    for p in params {
1149        if needs_vec_f32_conversion(&p.ty) && p.is_ref {
1150            let conv_name = format!("{}_f32", p.name);
1151            bindings.push_str(&format!(
1152                "    let {conv_name}: Vec<f32> = {}.iter().map(|&x| x as f32).collect();\n",
1153                p.name
1154            ));
1155        }
1156    }
1157    bindings
1158}
1159
1160/// NAPI-specific call args that casts i64 params to u64/usize where the core expects it.
1161/// Properly handles is_ref for reference parameters and complex type conversions.
1162fn napi_gen_call_args(params: &[ParamDef], opaque_types: &AHashSet<String>) -> String {
1163    params
1164        .iter()
1165        .map(|p| {
1166            // Special case: Vec<f32> param with is_ref uses the converted variable
1167            if needs_vec_f32_conversion(&p.ty) && p.is_ref {
1168                return format!("&{}_f32", p.name);
1169            }
1170            match &p.ty {
1171                TypeRef::Primitive(prim) if needs_napi_cast(prim) => {
1172                    let core_ty = core_prim_str(prim);
1173                    if p.optional {
1174                        format!("{}.map(|v| v as {})", p.name, core_ty)
1175                    } else {
1176                        format!("{} as {}", p.name, core_ty)
1177                    }
1178                }
1179                TypeRef::Duration => {
1180                    if p.optional {
1181                        format!("{}.map(|v| std::time::Duration::from_millis(v.max(0) as u64))", p.name)
1182                    } else {
1183                        format!("std::time::Duration::from_millis({}.max(0) as u64)", p.name)
1184                    }
1185                }
1186                TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
1187                    if p.optional {
1188                        format!("{}.as_ref().map(|v| &v.inner)", p.name)
1189                    } else {
1190                        format!("&{}.inner", p.name)
1191                    }
1192                }
1193                TypeRef::Named(_) => {
1194                    if p.optional {
1195                        if p.is_ref {
1196                            format!("{}.as_ref()", p.name)
1197                        } else {
1198                            format!("{}.map(Into::into)", p.name)
1199                        }
1200                    } else {
1201                        format!("{}.into()", p.name)
1202                    }
1203                }
1204                TypeRef::String | TypeRef::Char => {
1205                    if p.optional {
1206                        if p.is_ref {
1207                            format!("{}.as_deref()", p.name)
1208                        } else {
1209                            p.name.clone()
1210                        }
1211                    } else if p.is_ref {
1212                        format!("&{}", p.name)
1213                    } else {
1214                        p.name.clone()
1215                    }
1216                }
1217                TypeRef::Path => {
1218                    if p.optional {
1219                        if p.is_ref {
1220                            format!("{}.as_deref().map(std::path::Path::new)", p.name)
1221                        } else {
1222                            format!("{}.map(std::path::PathBuf::from)", p.name)
1223                        }
1224                    } else if p.is_ref {
1225                        format!("std::path::Path::new(&{})", p.name)
1226                    } else {
1227                        format!("std::path::PathBuf::from({})", p.name)
1228                    }
1229                }
1230                TypeRef::Bytes => {
1231                    if p.optional {
1232                        if p.is_ref {
1233                            format!("{}.as_deref()", p.name)
1234                        } else {
1235                            p.name.clone()
1236                        }
1237                    } else if p.is_ref {
1238                        format!("&{}", p.name)
1239                    } else {
1240                        p.name.clone()
1241                    }
1242                }
1243                TypeRef::Vec(inner) => {
1244                    if p.optional {
1245                        if p.is_ref {
1246                            format!("{}.as_deref()", p.name)
1247                        } else {
1248                            p.name.clone()
1249                        }
1250                    } else if p.is_ref && matches!(inner.as_ref(), TypeRef::String | TypeRef::Char) {
1251                        format!("&{}_refs", p.name)
1252                    } else if p.is_ref {
1253                        format!("&{}", p.name)
1254                    } else {
1255                        p.name.clone()
1256                    }
1257                }
1258                TypeRef::Map(_, _) => {
1259                    if p.optional {
1260                        if p.is_ref {
1261                            format!("{}.as_ref()", p.name)
1262                        } else {
1263                            p.name.clone()
1264                        }
1265                    } else if p.is_ref {
1266                        format!("&{}", p.name)
1267                    } else {
1268                        p.name.clone()
1269                    }
1270                }
1271                _ => p.name.clone(),
1272            }
1273        })
1274        .collect::<Vec<_>>()
1275        .join(", ")
1276}
1277
1278/// NAPI-specific return wrapping for opaque instance methods.
1279/// Extends the shared `wrap_return` with i64 casts for u64/usize/isize primitives.
1280fn napi_wrap_return(
1281    expr: &str,
1282    return_type: &TypeRef,
1283    type_name: &str,
1284    opaque_types: &AHashSet<String>,
1285    self_is_opaque: bool,
1286    returns_ref: bool,
1287    prefix: &str,
1288) -> String {
1289    match return_type {
1290        TypeRef::Primitive(p) if needs_napi_cast(p) => {
1291            format!("{expr} as i64")
1292        }
1293        TypeRef::Duration => format!("{expr}.as_millis() as i64"),
1294        // Opaque Named returns need prefix
1295        TypeRef::Named(n) if n == type_name && self_is_opaque => {
1296            if returns_ref {
1297                format!("Self {{ inner: Arc::new({expr}.clone()) }}")
1298            } else {
1299                format!("Self {{ inner: Arc::new({expr}) }}")
1300            }
1301        }
1302        TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
1303            if returns_ref {
1304                format!("{prefix}{n} {{ inner: Arc::new({expr}.clone()) }}")
1305            } else {
1306                format!("{prefix}{n} {{ inner: Arc::new({expr}) }}")
1307            }
1308        }
1309        TypeRef::Named(_) => {
1310            if returns_ref {
1311                format!("{expr}.clone().into()")
1312            } else {
1313                format!("{expr}.into()")
1314            }
1315        }
1316        _ => generators::wrap_return(
1317            expr,
1318            return_type,
1319            type_name,
1320            opaque_types,
1321            self_is_opaque,
1322            returns_ref,
1323            false,
1324        ),
1325    }
1326}
1327
1328/// NAPI-specific return wrapping for free functions (no type_name context).
1329fn napi_wrap_return_fn(
1330    expr: &str,
1331    return_type: &TypeRef,
1332    opaque_types: &AHashSet<String>,
1333    returns_ref: bool,
1334    prefix: &str,
1335) -> String {
1336    match return_type {
1337        TypeRef::Primitive(p) if needs_napi_cast(p) => {
1338            format!("{expr} as i64")
1339        }
1340        TypeRef::Duration => format!("{expr}.as_millis() as i64"),
1341        TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
1342            if returns_ref {
1343                format!("{prefix}{n} {{ inner: Arc::new({expr}.clone()) }}")
1344            } else {
1345                format!("{prefix}{n} {{ inner: Arc::new({expr}) }}")
1346            }
1347        }
1348        TypeRef::Named(_) => {
1349            if returns_ref {
1350                format!("{expr}.clone().into()")
1351            } else {
1352                format!("{expr}.into()")
1353            }
1354        }
1355        TypeRef::String | TypeRef::Char | TypeRef::Bytes => {
1356            if returns_ref {
1357                format!("{expr}.into()")
1358            } else {
1359                expr.to_string()
1360            }
1361        }
1362        TypeRef::Path => format!("{expr}.to_string_lossy().to_string()"),
1363        TypeRef::Json => format!("{expr}.to_string()"),
1364        TypeRef::Optional(inner) => match inner.as_ref() {
1365            TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
1366                if returns_ref {
1367                    format!("{expr}.map(|v| {prefix}{name} {{ inner: Arc::new(v.clone()) }})")
1368                } else {
1369                    format!("{expr}.map(|v| {prefix}{name} {{ inner: Arc::new(v) }})")
1370                }
1371            }
1372            TypeRef::Named(_) => {
1373                if returns_ref {
1374                    format!("{expr}.map(|v| v.clone().into())")
1375                } else {
1376                    format!("{expr}.map(Into::into)")
1377                }
1378            }
1379            TypeRef::Vec(inner) => match inner.as_ref() {
1380                TypeRef::Named(_) => {
1381                    if returns_ref {
1382                        format!("{expr}.map(|v| v.into_iter().map(|x| x.clone().into()).collect())")
1383                    } else {
1384                        format!("{expr}.map(|v| v.into_iter().map(Into::into).collect())")
1385                    }
1386                }
1387                _ => expr.to_string(),
1388            },
1389            TypeRef::Path => {
1390                format!("{expr}.map(Into::into)")
1391            }
1392            TypeRef::String | TypeRef::Char | TypeRef::Bytes => {
1393                if returns_ref {
1394                    format!("{expr}.map(Into::into)")
1395                } else {
1396                    expr.to_string()
1397                }
1398            }
1399            _ => expr.to_string(),
1400        },
1401        TypeRef::Vec(inner) => match inner.as_ref() {
1402            TypeRef::Primitive(p) if needs_napi_cast(p) => {
1403                // Vec<usize>, Vec<f32>, etc. need element-wise casting to i64 or f64
1404                let target_ty = match p {
1405                    alef_core::ir::PrimitiveType::F32 => "f64",
1406                    _ => "i64", // u64, usize, isize, u32
1407                };
1408                format!("{expr}.into_iter().map(|v| v as {target_ty}).collect()")
1409            }
1410            TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
1411                if returns_ref {
1412                    format!("{expr}.into_iter().map(|v| {prefix}{name} {{ inner: Arc::new(v.clone()) }}).collect()")
1413                } else {
1414                    format!("{expr}.into_iter().map(|v| {prefix}{name} {{ inner: Arc::new(v) }}).collect()")
1415                }
1416            }
1417            TypeRef::Named(_) => {
1418                if returns_ref {
1419                    format!("{expr}.into_iter().map(|v| v.clone().into()).collect()")
1420                } else {
1421                    format!("{expr}.into_iter().map(Into::into).collect()")
1422                }
1423            }
1424            TypeRef::Path => {
1425                format!("{expr}.into_iter().map(Into::into).collect()")
1426            }
1427            TypeRef::String | TypeRef::Char | TypeRef::Bytes => {
1428                if returns_ref {
1429                    format!("{expr}.into_iter().map(Into::into).collect()")
1430                } else {
1431                    expr.to_string()
1432                }
1433            }
1434            _ => expr.to_string(),
1435        },
1436        _ => expr.to_string(),
1437    }
1438}
1439
1440/// Check if a type is Vec<f32> which needs element-wise conversion from f64 in NAPI.
1441fn needs_vec_f32_conversion(ty: &TypeRef) -> bool {
1442    matches!(ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Primitive(alef_core::ir::PrimitiveType::F32)))
1443}
1444
1445fn needs_napi_cast(p: &alef_core::ir::PrimitiveType) -> bool {
1446    // U32 maps to u32 in both NAPI and core, so no cast needed.
1447    // U64/Usize/Isize map to i64 in NAPI but u64/usize/isize in core.
1448    // F32 maps to f64 in NAPI but f32 in core.
1449    matches!(
1450        p,
1451        alef_core::ir::PrimitiveType::U64
1452            | alef_core::ir::PrimitiveType::Usize
1453            | alef_core::ir::PrimitiveType::Isize
1454            | alef_core::ir::PrimitiveType::F32
1455    )
1456}
1457
1458fn core_prim_str(p: &alef_core::ir::PrimitiveType) -> &'static str {
1459    match p {
1460        alef_core::ir::PrimitiveType::U64 => "u64",
1461        alef_core::ir::PrimitiveType::Usize => "usize",
1462        alef_core::ir::PrimitiveType::Isize => "isize",
1463        alef_core::ir::PrimitiveType::F32 => "f32",
1464        _ => unreachable!(),
1465    }
1466}
1467
1468/// Generate a global Tokio runtime for NAPI async support.
1469fn gen_tokio_runtime() -> String {
1470    "static WORKER_POOL: std::sync::LazyLock<tokio::runtime::Runtime> = std::sync::LazyLock::new(|| {
1471    tokio::runtime::Builder::new_multi_thread()
1472        .enable_all()
1473        .build()
1474        .expect(\"Failed to create Tokio runtime\")
1475});"
1476    .to_string()
1477}
1478
1479/// Generate an `index.d.ts` file for the NAPI binding crate.
1480///
1481/// NAPI-RS generates `const enum` in its auto-generated `.d.ts`, which is incompatible
1482/// with `verbatimModuleSyntax` (const enums cannot be re-exported as values). This
1483/// function produces an equivalent `.d.ts` with `export declare enum` (regular enum)
1484/// so the file can be committed and used directly without a post-build patch step.
1485///
1486/// The output format matches what NAPI-RS would generate after patching, using the same
1487/// alphabetical ordering and type declarations seen in the committed `index.d.ts` files.
1488fn gen_dts(api: &ApiSurface, prefix: &str) -> String {
1489    let header = hash::header(CommentStyle::DoubleSlash);
1490    let mut lines: Vec<String> = header.lines().map(|l| l.to_string()).collect();
1491    lines.push("/* eslint-disable */".to_string());
1492
1493    // Collect all declarations: opaque types (classes), plain structs (interfaces), enums, functions.
1494    // Sort each group alphabetically to produce stable, deterministic output.
1495
1496    // Opaque types → `export declare class`
1497    let mut opaque_types: Vec<&TypeDef> = api.types.iter().filter(|t| t.is_opaque).collect();
1498    opaque_types.sort_by(|a, b| a.name.cmp(&b.name));
1499
1500    // Plain structs → `export interface`
1501    let mut plain_types: Vec<&TypeDef> = api.types.iter().filter(|t| !t.is_opaque).collect();
1502    plain_types.sort_by(|a, b| a.name.cmp(&b.name));
1503
1504    // Enums → `export declare enum`
1505    let mut sorted_enums: Vec<&EnumDef> = api.enums.iter().collect();
1506    sorted_enums.sort_by(|a, b| a.name.cmp(&b.name));
1507
1508    // Functions → `export declare function`
1509    let mut sorted_fns: Vec<&FunctionDef> = api.functions.iter().collect();
1510    sorted_fns.sort_by(|a, b| a.name.cmp(&b.name));
1511
1512    // Build a merged list of all declarations sorted by their Js-prefixed name so the
1513    // output is fully alphabetical (matching the committed index.d.ts format).
1514    enum Decl<'a> {
1515        Class(&'a TypeDef),
1516        Interface(&'a TypeDef),
1517        Enum(&'a EnumDef),
1518        Function(&'a FunctionDef),
1519    }
1520
1521    let mut all_decls: Vec<(String, Decl<'_>)> = Vec::new();
1522    for t in &opaque_types {
1523        all_decls.push((format!("{prefix}{}", t.name), Decl::Class(t)));
1524    }
1525    for t in &plain_types {
1526        all_decls.push((format!("{prefix}{}", t.name), Decl::Interface(t)));
1527    }
1528    for e in &sorted_enums {
1529        all_decls.push((format!("{prefix}{}", e.name), Decl::Enum(e)));
1530    }
1531    for f in &sorted_fns {
1532        all_decls.push((to_node_name(&f.name), Decl::Function(f)));
1533    }
1534    all_decls.sort_by_key(|a| a.0.to_lowercase());
1535
1536    for (_, decl) in &all_decls {
1537        lines.push(String::new());
1538        match decl {
1539            Decl::Class(typ) => {
1540                lines.extend(format_jsdoc(&typ.doc, ""));
1541                lines.push(format!("export declare class {prefix}{} {{", typ.name));
1542                for method in &typ.methods {
1543                    let js_name = to_node_name(&method.name);
1544                    let params = dts_params(&method.params, prefix);
1545                    let ret = dts_return_type(
1546                        &method.return_type,
1547                        method.error_type.is_some(),
1548                        method.is_async,
1549                        prefix,
1550                    );
1551                    lines.extend(format_jsdoc(&method.doc, "  "));
1552                    if method.is_static {
1553                        lines.push(format!("  static {js_name}({params}): {ret}"));
1554                    } else {
1555                        lines.push(format!("  {js_name}({params}): {ret}"));
1556                    }
1557                }
1558                lines.push("}".to_string());
1559            }
1560            Decl::Interface(typ) => {
1561                lines.extend(format_jsdoc(&typ.doc, ""));
1562                lines.push(format!("export interface {prefix}{} {{", typ.name));
1563                for field in &typ.fields {
1564                    let js_name = to_node_name(&field.name);
1565                    let ts_ty = dts_type(&field.ty, prefix);
1566                    lines.extend(format_jsdoc(&field.doc, "  "));
1567                    // Only mark a field optional when the underlying Rust type is Option<T>.
1568                    // Required fields must not carry `?` — callers are expected to provide them.
1569                    if matches!(field.ty, TypeRef::Optional(_)) {
1570                        lines.push(format!("  {js_name}?: {ts_ty}"));
1571                    } else {
1572                        lines.push(format!("  {js_name}: {ts_ty}"));
1573                    }
1574                }
1575                lines.push("}".to_string());
1576            }
1577            Decl::Enum(e) => {
1578                let is_data_enum = e.serde_tag.is_some() && e.variants.iter().any(|v| !v.fields.is_empty());
1579                lines.extend(format_jsdoc(&e.doc, ""));
1580                if is_data_enum {
1581                    // Discriminated union: emit a type alias instead of an enum declaration.
1582                    // Each variant becomes an object literal type with the tag field and its own fields.
1583                    let tag_field = e.serde_tag.as_deref().unwrap_or("type");
1584                    let mut member_lines: Vec<String> = Vec::new();
1585                    for variant in &e.variants {
1586                        let tag_value = variant
1587                            .serde_rename
1588                            .as_deref()
1589                            .map(|s| s.to_string())
1590                            .unwrap_or_else(|| apply_rename_all(&variant.name, e.serde_rename_all.as_deref()));
1591                        let mut obj_fields: Vec<String> = vec![format!("{tag_field}: '{tag_value}'")];
1592                        for field in &variant.fields {
1593                            let js_name = to_node_name(&field.name);
1594                            let ts_ty = dts_type(&field.ty, prefix);
1595                            if matches!(field.ty, TypeRef::Optional(_)) {
1596                                obj_fields.push(format!("{js_name}?: {ts_ty}"));
1597                            } else {
1598                                obj_fields.push(format!("{js_name}: {ts_ty}"));
1599                            }
1600                        }
1601                        member_lines.push(format!("  | {{ {} }}", obj_fields.join("; ")));
1602                    }
1603                    lines.push(format!("export type {prefix}{} =", e.name));
1604                    lines.extend(member_lines);
1605                } else {
1606                    lines.push(format!("export declare enum {prefix}{} {{", e.name));
1607                    for variant in &e.variants {
1608                        // NAPI string_enum: variant values follow serde_rename_all casing.
1609                        // Prefer explicit serde_rename, then apply rename_all, then fall back to variant name.
1610                        let value = variant
1611                            .serde_rename
1612                            .as_deref()
1613                            .map(|s| s.to_string())
1614                            .unwrap_or_else(|| apply_rename_all(&variant.name, e.serde_rename_all.as_deref()));
1615                        lines.extend(format_jsdoc(&variant.doc, "  "));
1616                        lines.push(format!("  {} = \"{}\",", variant.name, value));
1617                    }
1618                    lines.push("}".to_string());
1619                }
1620            }
1621            Decl::Function(func) => {
1622                let js_name = to_node_name(&func.name);
1623                let params = dts_params(&func.params, prefix);
1624                let ret = dts_return_type(&func.return_type, func.error_type.is_some(), func.is_async, prefix);
1625                lines.extend(format_jsdoc(&func.doc, ""));
1626                lines.push(format!("export declare function {js_name}({params}): {ret};"));
1627            }
1628        }
1629    }
1630
1631    lines.push(String::new());
1632    lines.join("\n")
1633}
1634
1635/// Format a rustdoc string as JSDoc comment lines with the given `indent` prefix.
1636///
1637/// Returns an empty `Vec` when `doc` is empty. For a single-line doc, emits
1638/// `["/** Description */"]`. For multi-line docs, emits the block form:
1639/// `["/**", " * line1", " * line2", " */"]`, each prefixed by `indent`.
1640fn format_jsdoc(doc: &str, indent: &str) -> Vec<String> {
1641    let doc = doc.trim();
1642    if doc.is_empty() {
1643        return vec![];
1644    }
1645    let lines: Vec<&str> = doc.lines().collect();
1646    if lines.len() == 1 {
1647        vec![format!("{indent}/** {} */", lines[0].trim())]
1648    } else {
1649        let mut out = Vec::with_capacity(lines.len() + 2);
1650        out.push(format!("{indent}/**"));
1651        for line in &lines {
1652            let trimmed = line.trim();
1653            if trimmed.is_empty() {
1654                out.push(format!("{indent} *"));
1655            } else {
1656                out.push(format!("{indent} * {trimmed}"));
1657            }
1658        }
1659        out.push(format!("{indent} */"));
1660        out
1661    }
1662}
1663
1664/// Map an IR `TypeRef` to its TypeScript equivalent for `.d.ts` generation.
1665fn dts_type(ty: &TypeRef, prefix: &str) -> String {
1666    match ty {
1667        TypeRef::Primitive(p) => match p {
1668            alef_core::ir::PrimitiveType::Bool => "boolean".to_string(),
1669            alef_core::ir::PrimitiveType::U8
1670            | alef_core::ir::PrimitiveType::U16
1671            | alef_core::ir::PrimitiveType::U32
1672            | alef_core::ir::PrimitiveType::I8
1673            | alef_core::ir::PrimitiveType::I16
1674            | alef_core::ir::PrimitiveType::I32
1675            | alef_core::ir::PrimitiveType::F32
1676            | alef_core::ir::PrimitiveType::F64 => "number".to_string(),
1677            // NAPI maps u64/usize/isize to i64 on the Rust side; JS sees it as number.
1678            alef_core::ir::PrimitiveType::U64
1679            | alef_core::ir::PrimitiveType::I64
1680            | alef_core::ir::PrimitiveType::Usize
1681            | alef_core::ir::PrimitiveType::Isize => "number".to_string(),
1682        },
1683        TypeRef::String | TypeRef::Char | TypeRef::Path => "string".to_string(),
1684        TypeRef::Bytes => "Uint8Array".to_string(),
1685        TypeRef::Json => "unknown".to_string(),
1686        TypeRef::Duration => "number".to_string(),
1687        TypeRef::Unit => "void".to_string(),
1688        TypeRef::Optional(inner) => format!("{} | undefined | null", dts_type(inner, prefix)),
1689        TypeRef::Vec(inner) => format!("Array<{}>", dts_type(inner, prefix)),
1690        TypeRef::Map(k, v) => format!("Record<{}, {}>", dts_type(k, prefix), dts_type(v, prefix)),
1691        TypeRef::Named(name) => format!("{prefix}{name}"),
1692    }
1693}
1694
1695/// Render a list of parameters as a TypeScript parameter string for `.d.ts`.
1696fn dts_params(params: &[ParamDef], prefix: &str) -> String {
1697    params
1698        .iter()
1699        .map(|p| {
1700            let js_name = to_node_name(&p.name);
1701            let ts_ty = dts_type(&p.ty, prefix);
1702            if p.optional {
1703                format!("{js_name}?: {ts_ty} | undefined | null")
1704            } else {
1705                format!("{js_name}: {ts_ty}")
1706            }
1707        })
1708        .collect::<Vec<_>>()
1709        .join(", ")
1710}
1711
1712/// Render the TypeScript return type for a function/method in `.d.ts`.
1713///
1714/// Async functions return `Promise<T>`. Functions that can error still return `T`
1715/// (NAPI throws JS exceptions on error, so the `.d.ts` signature just shows the success type).
1716fn dts_return_type(ret: &TypeRef, _has_error: bool, is_async: bool, prefix: &str) -> String {
1717    let base = match ret {
1718        TypeRef::Unit => "void".to_string(),
1719        other => dts_type(other, prefix),
1720    };
1721    if is_async { format!("Promise<{base}>") } else { base }
1722}
1723
1724/// Apply a serde `rename_all` rule to a PascalCase variant name, returning the serialized string.
1725///
1726/// NAPI `string_enum` serializes variant names using the same rule as serde's `rename_all`.
1727/// When a variant has no explicit `serde_rename`, the enum-level `rename_all` applies.
1728fn apply_rename_all(variant_name: &str, rename_all: Option<&str>) -> String {
1729    match rename_all {
1730        Some("snake_case") => {
1731            // PascalCase → snake_case: insert underscore before each uppercase letter (after the first)
1732            let mut out = String::with_capacity(variant_name.len() + 4);
1733            for (i, c) in variant_name.chars().enumerate() {
1734                if c.is_uppercase() && i > 0 {
1735                    out.push('_');
1736                }
1737                out.extend(c.to_lowercase());
1738            }
1739            out
1740        }
1741        Some("camelCase") => {
1742            // PascalCase → camelCase: lowercase the first character only
1743            let mut chars = variant_name.chars();
1744            match chars.next() {
1745                None => String::new(),
1746                Some(first) => first.to_lowercase().collect::<String>() + chars.as_str(),
1747            }
1748        }
1749        Some("kebab-case") => {
1750            let mut out = String::with_capacity(variant_name.len() + 4);
1751            for (i, c) in variant_name.chars().enumerate() {
1752                if c.is_uppercase() && i > 0 {
1753                    out.push('-');
1754                }
1755                out.extend(c.to_lowercase());
1756            }
1757            out
1758        }
1759        Some("SCREAMING_SNAKE_CASE") => {
1760            let mut out = String::with_capacity(variant_name.len() + 4);
1761            for (i, c) in variant_name.chars().enumerate() {
1762                if c.is_uppercase() && i > 0 {
1763                    out.push('_');
1764                }
1765                out.extend(c.to_uppercase());
1766            }
1767            out
1768        }
1769        Some("lowercase") => variant_name.to_lowercase(),
1770        Some("UPPERCASE") => variant_name.to_uppercase(),
1771        // PascalCase and unknown rules: use the variant name as-is
1772        _ => variant_name.to_string(),
1773    }
1774}
1775
1776/// Generate `From<JsTaggedEnum> for core::TaggedEnum` for a flattened struct representation.
1777fn gen_tagged_enum_binding_to_core(
1778    enum_def: &EnumDef,
1779    core_import: &str,
1780    prefix: &str,
1781    struct_names: &ahash::AHashSet<String>,
1782) -> String {
1783    use alef_core::ir::TypeRef;
1784    use std::fmt::Write;
1785    let core_path = alef_codegen::conversions::core_enum_path(enum_def, core_import);
1786    let binding_name = format!("{prefix}{}", enum_def.name);
1787    let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
1788
1789    // Determine which Named fields use binding structs vs serde JSON String.
1790    // A field uses a binding struct only if: (1) it has a binding struct in struct_names,
1791    // (2) it's not sanitized, and (3) the field name maps to a single Named type across
1792    // all variants (not shared with different types).
1793    let fields_with_binding_struct = tagged_enum_binding_struct_fields(enum_def, struct_names);
1794    // Fields with different Named types across variants are stored as String (JSON) in the
1795    // binding struct and must be deserialized per-variant via serde_json.
1796    let mixed_named_fields = tagged_enum_mixed_named_fields(enum_def);
1797
1798    let mut out = String::with_capacity(512);
1799    writeln!(out, "impl From<{binding_name}> for {core_path} {{").ok();
1800    writeln!(out, "    fn from(val: {binding_name}) -> Self {{").ok();
1801    writeln!(out, "        match val.{tag_field}_tag.as_str() {{").ok();
1802
1803    for variant in &enum_def.variants {
1804        let default_tag = variant.name.to_lowercase();
1805        let tag_value = variant.serde_rename.as_deref().unwrap_or(&default_tag);
1806        if variant.fields.is_empty() {
1807            writeln!(out, "            \"{tag_value}\" => Self::{},", variant.name).ok();
1808        } else {
1809            let is_tuple = alef_codegen::conversions::is_tuple_variant(&variant.fields);
1810            let field_exprs: Vec<String> = variant
1811                .fields
1812                .iter()
1813                .map(|f| {
1814                    let has_binding = fields_with_binding_struct.contains(f.name.as_str());
1815                    let is_mixed = mixed_named_fields.contains(&f.name);
1816                    if f.optional {
1817                        match &f.ty {
1818                            TypeRef::Path => {
1819                                format!("val.{}.map(std::path::PathBuf::from)", f.name)
1820                            }
1821                            TypeRef::Named(n) if is_mixed => {
1822                                // Mixed-type field: stored as String (JSON), deserialize per variant
1823                                let core_type = format!("{core_import}::{n}");
1824                                format!(
1825                                    "val.{}.and_then(|s| serde_json::from_str::<{core_type}>(&s).ok())",
1826                                    f.name
1827                                )
1828                            }
1829                            TypeRef::Named(_) if has_binding => {
1830                                format!("val.{}.map(|v| v.into())", f.name)
1831                            }
1832                            // Non-sanitized Named fields with a single consistent type are stored
1833                            // as Option<JsXxx> in the binding struct, so use .into() conversion.
1834                            TypeRef::Named(_) => {
1835                                format!("val.{}.map(|v| v.into())", f.name)
1836                            }
1837                            TypeRef::Primitive(p) if needs_napi_cast(p) => {
1838                                let core_ty = core_prim_str(p);
1839                                format!("val.{}.map(|v| v as {core_ty})", f.name)
1840                            }
1841                            _ => {
1842                                format!("val.{}", f.name)
1843                            }
1844                        }
1845                    } else if f.sanitized {
1846                        let expr = "Default::default()".to_string();
1847                        if f.is_boxed { format!("Box::new({expr})") } else { expr }
1848                    } else {
1849                        let expr = match &f.ty {
1850                            TypeRef::Named(n) if is_mixed => {
1851                                // Mixed-type field: stored as String (JSON), deserialize per variant
1852                                let core_type = format!("{core_import}::{n}");
1853                                format!(
1854                                    "val.{}.and_then(|s| serde_json::from_str::<{core_type}>(&s).ok()).unwrap_or_default()",
1855                                    f.name
1856                                )
1857                            }
1858                            TypeRef::Named(_) if has_binding => {
1859                                format!("val.{}.map(|v| v.into()).unwrap_or_default()", f.name)
1860                            }
1861                            // Non-sanitized Named fields with a single consistent type are stored
1862                            // as Option<JsXxx> in the binding struct, so use .into() conversion.
1863                            TypeRef::Named(_) => {
1864                                format!("val.{}.map(|v| v.into()).unwrap_or_default()", f.name)
1865                            }
1866                            TypeRef::Path => {
1867                                format!("val.{}.map(std::path::PathBuf::from).unwrap_or_default()", f.name)
1868                            }
1869                            TypeRef::Primitive(p) if needs_napi_cast(p) => {
1870                                let core_ty = core_prim_str(p);
1871                                format!("val.{}.map(|v| v as {core_ty}).unwrap_or_default()", f.name)
1872                            }
1873                            _ => {
1874                                format!("val.{}.unwrap_or_default()", f.name)
1875                            }
1876                        };
1877                        if f.is_boxed { format!("Box::new({expr})") } else { expr }
1878                    }
1879                })
1880                .collect();
1881            if is_tuple {
1882                writeln!(
1883                    out,
1884                    "            \"{tag_value}\" => Self::{}({}),",
1885                    variant.name,
1886                    field_exprs.join(", ")
1887                )
1888                .ok();
1889            } else {
1890                let field_inits: Vec<String> = variant
1891                    .fields
1892                    .iter()
1893                    .zip(field_exprs.iter())
1894                    .map(|(f, expr)| format!("{}: {expr}", f.name))
1895                    .collect();
1896                writeln!(
1897                    out,
1898                    "            \"{tag_value}\" => Self::{} {{ {} }},",
1899                    variant.name,
1900                    field_inits.join(", ")
1901                )
1902                .ok();
1903            }
1904        }
1905    }
1906
1907    // Default fallback to first variant
1908    if let Some(first) = enum_def.variants.first() {
1909        if first.fields.is_empty() {
1910            writeln!(out, "            _ => Self::{},", first.name).ok();
1911        } else {
1912            let is_tuple = alef_codegen::conversions::is_tuple_variant(&first.fields);
1913            if is_tuple {
1914                let defaults: Vec<&str> = first.fields.iter().map(|_| "Default::default()").collect();
1915                writeln!(out, "            _ => Self::{}({}),", first.name, defaults.join(", ")).ok();
1916            } else {
1917                let defaults: Vec<String> = first
1918                    .fields
1919                    .iter()
1920                    .map(|f| format!("{}: Default::default()", f.name))
1921                    .collect();
1922                writeln!(
1923                    out,
1924                    "            _ => Self::{} {{ {} }},",
1925                    first.name,
1926                    defaults.join(", ")
1927                )
1928                .ok();
1929            }
1930        }
1931    }
1932
1933    writeln!(out, "        }}").ok();
1934    writeln!(out, "    }}").ok();
1935    write!(out, "}}").ok();
1936    out
1937}
1938
1939/// Generate `From<core::TaggedEnum> for JsTaggedEnum` for a flattened struct representation.
1940fn gen_tagged_enum_core_to_binding(
1941    enum_def: &EnumDef,
1942    core_import: &str,
1943    prefix: &str,
1944    struct_names: &ahash::AHashSet<String>,
1945) -> String {
1946    use std::fmt::Write;
1947    let core_path = alef_codegen::conversions::core_enum_path(enum_def, core_import);
1948    let binding_name = format!("{prefix}{}", enum_def.name);
1949    let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
1950    let fields_with_binding_struct = tagged_enum_binding_struct_fields(enum_def, struct_names);
1951    // Fields with different Named types across variants are stored as String (JSON) in the
1952    // binding struct and must be serialized per-variant via serde_json.
1953    let mixed_named_fields = tagged_enum_mixed_named_fields(enum_def);
1954
1955    // Collect all field names across all variants
1956    let all_fields: Vec<String> = {
1957        let mut fields = std::collections::BTreeSet::new();
1958        for v in &enum_def.variants {
1959            for f in &v.fields {
1960                fields.insert(f.name.clone());
1961            }
1962        }
1963        fields.into_iter().collect()
1964    };
1965
1966    let mut out = String::with_capacity(512);
1967    writeln!(out, "impl From<{core_path}> for {binding_name} {{").ok();
1968    writeln!(out, "    fn from(val: {core_path}) -> Self {{").ok();
1969    writeln!(out, "        match val {{").ok();
1970
1971    for variant in &enum_def.variants {
1972        let default_tag = variant.name.to_lowercase();
1973        let tag_value = variant.serde_rename.as_deref().unwrap_or(&default_tag);
1974        let _variant_field_names: std::collections::BTreeSet<String> =
1975            variant.fields.iter().map(|f| f.name.clone()).collect();
1976
1977        if variant.fields.is_empty() {
1978            writeln!(
1979                out,
1980                "            {core_path}::{} => Self {{ {tag_field}_tag: \"{tag_value}\".to_string(), {} }},",
1981                variant.name,
1982                all_fields
1983                    .iter()
1984                    .map(|f| format!("{f}: None"))
1985                    .collect::<Vec<_>>()
1986                    .join(", ")
1987            )
1988            .ok();
1989        } else {
1990            use alef_core::ir::TypeRef;
1991            let is_tuple = alef_codegen::conversions::is_tuple_variant(&variant.fields);
1992            let variant_field_map: std::collections::BTreeMap<&str, &alef_core::ir::FieldDef> =
1993                variant.fields.iter().map(|f| (f.name.as_str(), f)).collect();
1994            let destructured: Vec<String> = variant
1995                .fields
1996                .iter()
1997                .map(|f| {
1998                    if f.sanitized {
1999                        if is_tuple {
2000                            format!("_{}", f.name)
2001                        } else {
2002                            format!("{}: _{}", f.name, f.name)
2003                        }
2004                    } else {
2005                        f.name.clone()
2006                    }
2007                })
2008                .collect();
2009            let field_inits: Vec<String> = all_fields
2010                .iter()
2011                .map(|f| {
2012                    if let Some(field) = variant_field_map.get(f.as_str()) {
2013                        let has_binding = fields_with_binding_struct.contains(f.as_str());
2014                        let is_mixed = mixed_named_fields.contains(f.as_str());
2015                        if field.optional {
2016                            match &field.ty {
2017                                TypeRef::Path => format!("{f}: {f}.map(|p| p.to_string_lossy().to_string())"),
2018                                TypeRef::Named(_) if is_mixed => {
2019                                    // Mixed-type field: serialize to JSON String for the binding struct
2020                                    format!("{f}: {f}.and_then(|v| serde_json::to_string(&v).ok())")
2021                                }
2022                                TypeRef::Named(_) if has_binding => {
2023                                    format!("{f}: {f}.map(|v| v.into())")
2024                                }
2025                                // Non-sanitized Named fields with a single consistent type are stored
2026                                // as Option<JsXxx> in the binding struct, so use .into() conversion.
2027                                TypeRef::Named(_) => {
2028                                    format!("{f}: {f}.map(|v| v.into())")
2029                                }
2030                                _ => format!("{f}: {f}"),
2031                            }
2032                        } else if field.sanitized {
2033                            format!("{f}: None")
2034                        } else {
2035                            match &field.ty {
2036                                TypeRef::Named(_) if is_mixed => {
2037                                    // Mixed-type field: serialize to JSON String for the binding struct
2038                                    format!("{f}: serde_json::to_string(&{f}).ok()")
2039                                }
2040                                TypeRef::Named(_) if has_binding => format!("{f}: Some({f}.into())"),
2041                                // Non-sanitized Named fields with a single consistent type are stored
2042                                // as Option<JsXxx> in the binding struct, so use .into() conversion.
2043                                TypeRef::Named(_) => format!("{f}: Some({f}.into())"),
2044                                TypeRef::Path => format!("{f}: Some({f}.to_string_lossy().to_string())"),
2045                                TypeRef::Primitive(p) if needs_napi_cast(p) => {
2046                                    match p {
2047                                        alef_core::ir::PrimitiveType::F32 => format!("{f}: Some({f} as f64)"),
2048                                        alef_core::ir::PrimitiveType::U64
2049                                        | alef_core::ir::PrimitiveType::Usize
2050                                        | alef_core::ir::PrimitiveType::Isize => format!("{f}: Some({f} as i64)"),
2051                                        // U32 stays as-is in NAPI
2052                                        _ => format!("{f}: Some({f})"),
2053                                    }
2054                                }
2055                                _ => format!("{f}: Some({f})"),
2056                            }
2057                        }
2058                    } else {
2059                        format!("{f}: None")
2060                    }
2061                })
2062                .collect();
2063            if is_tuple {
2064                writeln!(
2065                    out,
2066                    "            {core_path}::{}({}) => Self {{ {tag_field}_tag: \"{tag_value}\".to_string(), {} }},",
2067                    variant.name,
2068                    destructured.join(", "),
2069                    field_inits.join(", ")
2070                )
2071                .ok();
2072            } else {
2073                writeln!(
2074                    out,
2075                    "            {core_path}::{} {{ {} }} => Self {{ {tag_field}_tag: \"{tag_value}\".to_string(), {} }},",
2076                    variant.name,
2077                    destructured.join(", "),
2078                    field_inits.join(", ")
2079                )
2080                .ok();
2081            }
2082        }
2083    }
2084
2085    writeln!(out, "        }}").ok();
2086    writeln!(out, "    }}").ok();
2087    write!(out, "}}").ok();
2088    out
2089}
2090
2091/// Determine which Named fields in a tagged enum have **different** Named types across variants.
2092/// These fields cannot use a single `JsXxx` binding type, so they are stored as `String` (JSON)
2093/// and converted via `serde_json` per variant in the From impls.
2094fn tagged_enum_mixed_named_fields(enum_def: &EnumDef) -> ahash::AHashSet<String> {
2095    use alef_core::ir::TypeRef;
2096    let mut field_types: std::collections::HashMap<&str, ahash::AHashSet<&str>> = std::collections::HashMap::new();
2097
2098    for variant in &enum_def.variants {
2099        for field in &variant.fields {
2100            if field.sanitized {
2101                continue;
2102            }
2103            if let TypeRef::Named(n) = &field.ty {
2104                field_types.entry(&field.name).or_default().insert(n.as_str());
2105            }
2106        }
2107    }
2108
2109    field_types
2110        .into_iter()
2111        .filter(|(_, types)| types.len() > 1)
2112        .map(|(name, _)| name.to_string())
2113        .collect()
2114}
2115
2116/// Determine which Named fields in a tagged enum use binding structs (Into conversion)
2117/// vs serde JSON String flattening. A field uses a binding struct only if:
2118/// 1. The field name maps to a single Named type across all variants
2119/// 2. That Named type has a binding struct (in struct_names)
2120/// 3. The field is not sanitized
2121fn tagged_enum_binding_struct_fields<'a>(
2122    enum_def: &'a EnumDef,
2123    struct_names: &ahash::AHashSet<String>,
2124) -> ahash::AHashSet<&'a str> {
2125    use alef_core::ir::TypeRef;
2126    let mut field_types: std::collections::HashMap<&str, Vec<&str>> = std::collections::HashMap::new();
2127    let mut sanitized_fields: ahash::AHashSet<&str> = ahash::AHashSet::new();
2128
2129    for variant in &enum_def.variants {
2130        for field in &variant.fields {
2131            if field.sanitized {
2132                sanitized_fields.insert(&field.name);
2133            }
2134            if let TypeRef::Named(n) = &field.ty {
2135                field_types.entry(&field.name).or_default().push(n);
2136            }
2137        }
2138    }
2139
2140    let mut result = ahash::AHashSet::new();
2141    for (field_name, types) in &field_types {
2142        if sanitized_fields.contains(field_name) {
2143            continue;
2144        }
2145        // All variants sharing this field name must have the same Named type
2146        if types.iter().all(|t| *t == types[0]) && struct_names.contains(types[0]) {
2147            result.insert(*field_name);
2148        }
2149    }
2150    result
2151}