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