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