Skip to main content

alef_codegen/conversions/
helpers.rs

1use ahash::{AHashMap, AHashSet};
2use alef_core::ir::{ApiSurface, EnumDef, FieldDef, PrimitiveType, TypeDef, TypeRef};
3
4use crate::conversions::ConversionConfig;
5
6/// Collect all Named type names that appear in the API surface — both as
7/// function/method input parameters AND as function/method return types.
8/// These are types that need binding→core `From` impls.
9///
10/// Return types need binding→core From impls because:
11/// - Users may construct binding types and convert them to core types
12/// - Generated code may use `.into()` on nested Named fields in From impls
13/// - Round-trip conversion completeness ensures the API is fully usable
14///
15/// The result includes transitive dependencies: if `ConversionResult` is a
16/// return type and it has a field `metadata: HtmlMetadata`, then `HtmlMetadata`
17/// is also included.
18pub fn input_type_names(surface: &ApiSurface) -> AHashSet<String> {
19    let mut names = AHashSet::new();
20
21    // Collect Named types from function params
22    for func in &surface.functions {
23        for param in &func.params {
24            collect_named_types(&param.ty, &mut names);
25        }
26    }
27    // Collect Named types from method params
28    for typ in surface.types.iter().filter(|typ| !typ.is_trait) {
29        for method in &typ.methods {
30            for param in &method.params {
31                collect_named_types(&param.ty, &mut names);
32            }
33        }
34    }
35    // Collect Named types from function return types.
36    // Return types and their transitive field types need binding→core From impls
37    // for round-trip conversion completeness.
38    for func in &surface.functions {
39        collect_named_types(&func.return_type, &mut names);
40    }
41    // Collect Named types from method return types.
42    for typ in surface.types.iter().filter(|typ| !typ.is_trait) {
43        for method in &typ.methods {
44            collect_named_types(&method.return_type, &mut names);
45        }
46    }
47    // Collect Named types from fields of non-opaque types that have methods.
48    // When a non-opaque type has methods, codegen generates binding→core struct conversion
49    // (gen_lossy_binding_to_core_fields) which calls `.into()` on Named fields.
50    // Those field types need binding→core From impls.
51    for typ in surface.types.iter().filter(|typ| !typ.is_trait) {
52        if !typ.is_opaque && !typ.methods.is_empty() {
53            for field in &typ.fields {
54                if !field.sanitized {
55                    collect_named_types(&field.ty, &mut names);
56                }
57            }
58        }
59    }
60
61    // Collect Named types from enum variant fields — enum variants that are
62    // used as input types need From impls for their nested Named fields too.
63    for e in &surface.enums {
64        if names.contains(&e.name) {
65            for variant in &e.variants {
66                for field in &variant.fields {
67                    collect_named_types(&field.ty, &mut names);
68                }
69            }
70        }
71    }
72
73    // Transitive closure: if type A is an input and has field of type B, B is also an input
74    let mut changed = true;
75    while changed {
76        changed = false;
77        let snapshot: Vec<String> = names.iter().cloned().collect();
78        for name in &snapshot {
79            if let Some(typ) = surface.types.iter().find(|t| t.name == *name) {
80                for field in &typ.fields {
81                    let mut field_names = AHashSet::new();
82                    collect_named_types(&field.ty, &mut field_names);
83                    for n in field_names {
84                        if names.insert(n) {
85                            changed = true;
86                        }
87                    }
88                }
89            }
90            // Also walk enum variant fields in the transitive closure
91            if let Some(e) = surface.enums.iter().find(|e| e.name == *name) {
92                for variant in &e.variants {
93                    for field in &variant.fields {
94                        let mut field_names = AHashSet::new();
95                        collect_named_types(&field.ty, &mut field_names);
96                        for n in field_names {
97                            if names.insert(n) {
98                                changed = true;
99                            }
100                        }
101                    }
102                }
103            }
104        }
105    }
106
107    names
108}
109
110/// Recursively collect all `Named(name)` from a TypeRef.
111fn collect_named_types(ty: &TypeRef, out: &mut AHashSet<String>) {
112    match ty {
113        TypeRef::Named(name) => {
114            out.insert(name.clone());
115        }
116        TypeRef::Optional(inner) | TypeRef::Vec(inner) => collect_named_types(inner, out),
117        TypeRef::Map(k, v) => {
118            collect_named_types(k, out);
119            collect_named_types(v, out);
120        }
121        _ => {}
122    }
123}
124
125/// Check if a TypeRef references a Named type that is in the exclude list.
126/// Used to skip fields whose types were excluded from binding generation,
127/// preventing references to non-existent wrapper types (e.g. Js* in WASM).
128pub fn field_references_excluded_type(ty: &TypeRef, exclude_types: &[String]) -> bool {
129    match ty {
130        TypeRef::Named(name) => exclude_types.iter().any(|e| e == name),
131        TypeRef::Optional(inner) | TypeRef::Vec(inner) => field_references_excluded_type(inner, exclude_types),
132        TypeRef::Map(k, v) => {
133            field_references_excluded_type(k, exclude_types) || field_references_excluded_type(v, exclude_types)
134        }
135        _ => false,
136    }
137}
138
139/// Returns true if a primitive type needs i64 casting (NAPI/PHP — JS/PHP lack native u64).
140pub(crate) fn needs_i64_cast(p: &PrimitiveType) -> bool {
141    matches!(p, PrimitiveType::U64 | PrimitiveType::Usize | PrimitiveType::Isize)
142}
143
144/// Returns true if a primitive type needs i32 casting (extendr — R maps small ints to i32).
145pub(crate) fn needs_i32_cast(p: &PrimitiveType) -> bool {
146    matches!(
147        p,
148        PrimitiveType::U8 | PrimitiveType::U16 | PrimitiveType::U32 | PrimitiveType::I8 | PrimitiveType::I16
149    )
150}
151
152/// Returns true if a primitive type needs f64 casting (extendr — R maps large ints and f32 to f64).
153///
154/// Includes `I64` because the extendr `TypeMapper` maps `PrimitiveType::I64` to `"f64"` (R has
155/// no native i64), so binding structs store `i64` fields as `f64`. Cast conversions must mirror
156/// this mapping in both binding→core and core→binding From impls.
157pub(crate) fn needs_f64_cast(p: &PrimitiveType) -> bool {
158    matches!(
159        p,
160        PrimitiveType::U64 | PrimitiveType::I64 | PrimitiveType::Usize | PrimitiveType::Isize | PrimitiveType::F32
161    )
162}
163
164/// Returns the core primitive type string for cast primitives.
165pub(crate) fn core_prim_str(p: &PrimitiveType) -> &'static str {
166    match p {
167        PrimitiveType::U64 => "u64",
168        PrimitiveType::Usize => "usize",
169        PrimitiveType::Isize => "isize",
170        PrimitiveType::F32 => "f32",
171        PrimitiveType::Bool => "bool",
172        PrimitiveType::U8 => "u8",
173        PrimitiveType::U16 => "u16",
174        PrimitiveType::U32 => "u32",
175        PrimitiveType::I8 => "i8",
176        PrimitiveType::I16 => "i16",
177        PrimitiveType::I32 => "i32",
178        PrimitiveType::I64 => "i64",
179        PrimitiveType::F64 => "f64",
180    }
181}
182
183/// Returns the binding primitive type string for cast primitives (core→binding direction).
184pub(crate) fn binding_prim_str(p: &PrimitiveType) -> &'static str {
185    match p {
186        PrimitiveType::U64 | PrimitiveType::Usize | PrimitiveType::Isize => "i64",
187        PrimitiveType::F32 => "f64",
188        PrimitiveType::Bool => "bool",
189        PrimitiveType::U8 | PrimitiveType::U16 | PrimitiveType::U32 => "i32",
190        PrimitiveType::I8 | PrimitiveType::I16 | PrimitiveType::I32 => "i32",
191        PrimitiveType::I64 => "i64",
192        PrimitiveType::F64 => "f64",
193    }
194}
195
196/// Build the set of types that can have core→binding From safely generated.
197/// More permissive than binding→core: allows sanitized fields (uses format!("{:?}"))
198/// and accepts data enums (data discarded with `..` in match arms).
199pub fn core_to_binding_convertible_types(surface: &ApiSurface) -> AHashSet<String> {
200    let convertible_enums: AHashSet<&str> = surface
201        .enums
202        .iter()
203        .filter(|e| can_generate_enum_conversion_from_core(e))
204        .map(|e| e.name.as_str())
205        .collect();
206
207    let opaque_type_names: AHashSet<&str> = surface
208        .types
209        .iter()
210        .filter(|t| t.is_opaque)
211        .map(|t| t.name.as_str())
212        .collect();
213
214    // Data enums (enums with data variants) are generated as opaque wrappers in most
215    // backends. They don't have From conversions but are still "known" types — structs
216    // containing them can still generate core→binding conversions (the field uses the
217    // opaque wrapper's From impl or format!("{:?}")).
218    let data_enum_names: AHashSet<&str> = surface
219        .enums
220        .iter()
221        .filter(|e| e.variants.iter().any(|v| !v.fields.is_empty()))
222        .map(|e| e.name.as_str())
223        .collect();
224
225    // Build rust_path maps for detecting type_rust_path mismatches.
226    let (enum_paths, type_paths) = build_rust_path_maps(surface);
227
228    // All non-opaque types are candidates (sanitized fields use format!("{:?}"))
229    let mut convertible: AHashSet<String> = surface
230        .types
231        .iter()
232        .filter(|t| !t.is_opaque)
233        .map(|t| t.name.clone())
234        .collect();
235
236    let mut changed = true;
237    while changed {
238        changed = false;
239        let snapshot: Vec<String> = convertible.iter().cloned().collect();
240        let mut known: AHashSet<&str> = convertible.iter().map(|s| s.as_str()).collect();
241        known.extend(&opaque_type_names);
242        known.extend(&data_enum_names);
243        let mut to_remove = Vec::new();
244        for type_name in &snapshot {
245            if let Some(typ) = surface.types.iter().find(|t| t.name == *type_name) {
246                let ok = typ.fields.iter().all(|f| {
247                    if f.sanitized {
248                        true
249                    } else if field_has_path_mismatch(f, &enum_paths, &type_paths) {
250                        false
251                    } else {
252                        is_field_convertible(&f.ty, &convertible_enums, &known)
253                    }
254                });
255                if !ok {
256                    to_remove.push(type_name.clone());
257                }
258            }
259        }
260        for name in to_remove {
261            if convertible.remove(&name) {
262                changed = true;
263            }
264        }
265    }
266    convertible
267}
268
269/// Build the set of types that can have binding→core From safely generated.
270/// Strict: excludes types with sanitized fields (lossy conversion).
271/// This is transitive: a type is convertible only if all its Named field types
272/// are also convertible (or are enums with From/Into support).
273pub fn convertible_types(surface: &ApiSurface) -> AHashSet<String> {
274    // Build set of enums that have From/Into impls (unit-variant enums only)
275    let convertible_enums: AHashSet<&str> = surface
276        .enums
277        .iter()
278        .filter(|e| can_generate_enum_conversion(e))
279        .map(|e| e.name.as_str())
280        .collect();
281
282    // Build set of all known type names (including opaques) — opaque Named fields
283    // are convertible because we wrap/unwrap them via Arc.
284    let _all_type_names: AHashSet<&str> = surface.types.iter().map(|t| t.name.as_str()).collect();
285
286    // Build set of Named types that implement Default — sanitized fields referencing
287    // Named types without Default would cause a compile error in the generated From impl.
288    let default_type_names: AHashSet<&str> = surface
289        .types
290        .iter()
291        .filter(|t| t.has_default)
292        .map(|t| t.name.as_str())
293        .collect();
294
295    // Start with all non-opaque types as candidates.
296    // Types with sanitized fields use Default::default() for the sanitized field
297    // in the binding→core direction — but only if the field type implements Default.
298    let mut convertible: AHashSet<String> = surface
299        .types
300        .iter()
301        .filter(|t| !t.is_opaque)
302        .map(|t| t.name.clone())
303        .collect();
304
305    // Set of opaque type names — Named fields referencing opaques are always convertible
306    // (they use Arc wrap/unwrap), so include them in the known-types check.
307    let opaque_type_names: AHashSet<&str> = surface
308        .types
309        .iter()
310        .filter(|t| t.is_opaque)
311        .map(|t| t.name.as_str())
312        .collect();
313
314    // Data enums (enums with data variants) are generated as opaque wrappers — include
315    // them in the known set so structs containing them remain convertible.
316    let data_enum_names: AHashSet<&str> = surface
317        .enums
318        .iter()
319        .filter(|e| e.variants.iter().any(|v| !v.fields.is_empty()))
320        .map(|e| e.name.as_str())
321        .collect();
322
323    // Build rust_path maps for detecting type_rust_path mismatches.
324    let (enum_paths, type_paths) = build_rust_path_maps(surface);
325
326    // Iteratively remove types whose fields reference non-convertible Named types.
327    // We check against `convertible ∪ opaque_types ∪ data_enums` so that types
328    // referencing excluded types are transitively removed, while opaque and data
329    // enum Named fields remain valid.
330    let mut changed = true;
331    while changed {
332        changed = false;
333        let snapshot: Vec<String> = convertible.iter().cloned().collect();
334        let mut known: AHashSet<&str> = convertible.iter().map(|s| s.as_str()).collect();
335        known.extend(&opaque_type_names);
336        known.extend(&data_enum_names);
337        let mut to_remove = Vec::new();
338        for type_name in &snapshot {
339            if let Some(typ) = surface.types.iter().find(|t| t.name == *type_name) {
340                let ok = typ.fields.iter().all(|f| {
341                    if f.sanitized {
342                        sanitized_field_has_default(&f.ty, &default_type_names)
343                    } else if field_has_path_mismatch(f, &enum_paths, &type_paths) {
344                        false
345                    } else {
346                        is_field_convertible(&f.ty, &convertible_enums, &known)
347                    }
348                });
349                if !ok {
350                    to_remove.push(type_name.clone());
351                }
352            }
353        }
354        for name in to_remove {
355            if convertible.remove(&name) {
356                changed = true;
357            }
358        }
359    }
360    convertible
361}
362
363/// Check if a sanitized field's type can produce a valid `Default::default()` expression.
364/// Primitive types, strings, collections, Options, and Named types with `has_default` are fine.
365/// Named types without `has_default` are not — generating `Default::default()` for them would
366/// fail to compile.
367fn sanitized_field_has_default(ty: &TypeRef, default_types: &AHashSet<&str>) -> bool {
368    match ty {
369        TypeRef::Primitive(_)
370        | TypeRef::String
371        | TypeRef::Char
372        | TypeRef::Bytes
373        | TypeRef::Path
374        | TypeRef::Unit
375        | TypeRef::Duration
376        | TypeRef::Json => true,
377        // Option<T> defaults to None regardless of T
378        TypeRef::Optional(_) => true,
379        // Vec<T> defaults to empty vec regardless of T
380        TypeRef::Vec(_) => true,
381        // Map<K, V> defaults to empty map regardless of K/V
382        TypeRef::Map(_, _) => true,
383        TypeRef::Named(name) => {
384            if is_tuple_type_name(name) {
385                // Tuple types are always passthrough
386                true
387            } else {
388                // Named type must have has_default to be safely used via Default::default()
389                default_types.contains(name.as_str())
390            }
391        }
392    }
393}
394
395/// Check if a specific type is in the convertible set.
396pub fn can_generate_conversion(typ: &TypeDef, convertible: &AHashSet<String>) -> bool {
397    convertible.contains(&typ.name)
398}
399
400pub(crate) fn is_field_convertible(
401    ty: &TypeRef,
402    convertible_enums: &AHashSet<&str>,
403    known_types: &AHashSet<&str>,
404) -> bool {
405    match ty {
406        TypeRef::Primitive(_)
407        | TypeRef::String
408        | TypeRef::Char
409        | TypeRef::Bytes
410        | TypeRef::Path
411        | TypeRef::Unit
412        | TypeRef::Duration => true,
413        TypeRef::Json => true,
414        TypeRef::Optional(inner) | TypeRef::Vec(inner) => is_field_convertible(inner, convertible_enums, known_types),
415        TypeRef::Map(k, v) => {
416            is_field_convertible(k, convertible_enums, known_types)
417                && is_field_convertible(v, convertible_enums, known_types)
418        }
419        // Tuple types are passthrough — always convertible
420        TypeRef::Named(name) if is_tuple_type_name(name) => true,
421        // Unit-variant enums and known types (including opaques, which use Arc wrap/unwrap) are convertible.
422        TypeRef::Named(name) => convertible_enums.contains(name.as_str()) || known_types.contains(name.as_str()),
423    }
424}
425
426/// Check if a field's `type_rust_path` is compatible with the known type/enum rust_paths.
427///
428/// When a struct field has a `type_rust_path` that differs from the `rust_path` of the
429/// enum or type with the same short name, the `.into()` conversion will fail because
430/// the `From` impl targets a different type. This detects such mismatches.
431fn field_has_path_mismatch(
432    field: &FieldDef,
433    enum_rust_paths: &AHashMap<&str, &str>,
434    type_rust_paths: &AHashMap<&str, &str>,
435) -> bool {
436    let name = match &field.ty {
437        TypeRef::Named(n) => n.as_str(),
438        TypeRef::Optional(inner) | TypeRef::Vec(inner) => match inner.as_ref() {
439            TypeRef::Named(n) => n.as_str(),
440            _ => return false,
441        },
442        _ => return false,
443    };
444
445    if let Some(field_path) = &field.type_rust_path {
446        if let Some(enum_path) = enum_rust_paths.get(name) {
447            if !paths_compatible(field_path, enum_path) {
448                return true;
449            }
450        }
451        if let Some(type_path) = type_rust_paths.get(name) {
452            if !paths_compatible(field_path, type_path) {
453                return true;
454            }
455        }
456    }
457    false
458}
459
460/// Check if two rust paths refer to the same type.
461///
462/// Handles re-exports: `crate::module::Type` and `crate::Type` are compatible
463/// when they share the same crate root and type name (the type is re-exported).
464fn paths_compatible(a: &str, b: &str) -> bool {
465    if a == b {
466        return true;
467    }
468    // Normalize dashes to underscores for crate name comparison
469    // (Cargo uses dashes in package names, Rust uses underscores in crate names)
470    let a_norm = a.replace('-', "_");
471    let b_norm = b.replace('-', "_");
472    if a_norm == b_norm {
473        return true;
474    }
475    // Direct suffix match (e.g., "foo::Bar" ends_with "Bar")
476    if a_norm.ends_with(&b_norm) || b_norm.ends_with(&a_norm) {
477        return true;
478    }
479    // Same crate root + same short name → likely a re-export
480    let a_root = a_norm.split("::").next().unwrap_or("");
481    let b_root = b_norm.split("::").next().unwrap_or("");
482    let a_name = a_norm.rsplit("::").next().unwrap_or("");
483    let b_name = b_norm.rsplit("::").next().unwrap_or("");
484    a_root == b_root && a_name == b_name
485}
486
487/// Build maps of name -> rust_path for enums and types in the API surface.
488fn build_rust_path_maps(surface: &ApiSurface) -> (AHashMap<&str, &str>, AHashMap<&str, &str>) {
489    let enum_paths: AHashMap<&str, &str> = surface
490        .enums
491        .iter()
492        .map(|e| (e.name.as_str(), e.rust_path.as_str()))
493        .collect();
494    let type_paths: AHashMap<&str, &str> = surface
495        .types
496        .iter()
497        .map(|t| (t.name.as_str(), t.rust_path.as_str()))
498        .collect();
499    (enum_paths, type_paths)
500}
501
502/// Check if an enum can have From/Into safely generated (both directions).
503/// All enums are allowed — data variants use Default::default() for non-simple fields
504/// in the binding→core direction.
505pub fn can_generate_enum_conversion(enum_def: &EnumDef) -> bool {
506    !enum_def.variants.is_empty()
507}
508
509/// Check if an enum can have core→binding From safely generated.
510/// This is always possible: unit variants map 1:1, data variants discard data with `..`.
511pub fn can_generate_enum_conversion_from_core(enum_def: &EnumDef) -> bool {
512    // Always possible — data variants are handled by pattern matching with `..`
513    !enum_def.variants.is_empty()
514}
515
516/// Returns true if fields represent a tuple variant (positional: _0, _1, ...).
517pub fn is_tuple_variant(fields: &[FieldDef]) -> bool {
518    !fields.is_empty()
519        && fields[0]
520            .name
521            .strip_prefix('_')
522            .is_some_and(|rest: &str| rest.chars().all(|c: char| c.is_ascii_digit()))
523}
524
525/// Returns true if a TypeDef represents a newtype struct (single unnamed field `_0`).
526pub fn is_newtype(typ: &TypeDef) -> bool {
527    typ.fields.len() == 1 && typ.fields[0].name == "_0"
528}
529
530/// Returns true if a type name looks like a tuple (starts with `(`).
531/// Tuple types are passthrough — no conversion needed.
532pub(crate) fn is_tuple_type_name(name: &str) -> bool {
533    name.starts_with('(')
534}
535
536/// Derive the Rust import path from rust_path, replacing hyphens with underscores.
537///
538/// Prefers `original_rust_path` (the path before auto_path_mappings rewriting)
539/// so that `From` impls reference the actual defining crate, avoiding orphan
540/// rule violations when `core_import` is a re-export facade.
541pub fn core_type_path(typ: &TypeDef, core_import: &str) -> String {
542    core_type_path_remapped(typ, core_import, &[])
543}
544
545/// Like [`core_type_path`] but rewrites the leading crate segment when it matches
546/// a known source→override mapping.
547///
548/// When `core_crate_override` is set for a language, IR `rust_path` values still
549/// contain the original source crate prefix (e.g. `mylib_core::Method`). The
550/// `remaps` slice contains `(original_crate, override_crate)` pairs; when the
551/// leading crate segment of `rust_path` matches `original_crate`, it is replaced
552/// with `override_crate`.
553pub fn core_type_path_remapped(typ: &TypeDef, core_import: &str, remaps: &[(&str, &str)]) -> String {
554    // Always use rust_path (post path-mapping) — this is the import name that
555    // binding crates can actually resolve. The original_rust_path preserves the
556    // pre-mapping crate name (e.g. "html_to_markdown") which may not be importable
557    // when the dependency is renamed (e.g. "html-to-markdown-rs" in Cargo.toml).
558    let path = typ.rust_path.replace('-', "_");
559    if path.contains("::") {
560        apply_crate_remaps(&path, remaps)
561    } else {
562        format!("{core_import}::{}", typ.name)
563    }
564}
565
566/// Apply source→override crate remaps to a fully-qualified Rust path.
567///
568/// If the leading crate segment of `path` (the part before the first `::`)
569/// matches any entry in `remaps`, that segment is replaced with the override.
570/// Returns `path` unchanged when no remap applies.
571pub fn apply_crate_remaps(path: &str, remaps: &[(&str, &str)]) -> String {
572    if remaps.is_empty() {
573        return path.to_string();
574    }
575    if let Some(sep) = path.find("::") {
576        let leading = &path[..sep];
577        if let Some(&(_, override_crate)) = remaps.iter().find(|(orig, _)| *orig == leading) {
578            return format!("{override_crate}{}", &path[sep..]);
579        }
580    }
581    path.to_string()
582}
583
584/// Check if a type has any sanitized fields (binding→core conversion is lossy).
585pub fn has_sanitized_fields(typ: &TypeDef) -> bool {
586    typ.fields.iter().any(|f| f.sanitized)
587}
588
589/// Derive the Rust import path for an enum, replacing hyphens with underscores.
590pub fn core_enum_path(enum_def: &EnumDef, core_import: &str) -> String {
591    core_enum_path_remapped(enum_def, core_import, &[])
592}
593
594/// Like [`core_enum_path`] but rewrites the leading crate segment when it matches
595/// a known source→override mapping. See [`core_type_path_remapped`] for details.
596pub fn core_enum_path_remapped(enum_def: &EnumDef, core_import: &str, remaps: &[(&str, &str)]) -> String {
597    let path = enum_def.rust_path.replace('-', "_");
598    if path.starts_with(core_import) || path.contains("::") {
599        // Path is already fully-qualified — apply remaps and return.
600        apply_crate_remaps(&path, remaps)
601    } else {
602        // Bare unqualified name: prefix with the facade crate.
603        format!("{core_import}::{}", enum_def.name)
604    }
605}
606
607/// Build a map from type/enum short name to full rust_path.
608///
609/// Used by backends to resolve `TypeRef::Named(name)` to the correct qualified path
610/// instead of assuming `core_import::name` (which fails for types not re-exported at crate root).
611pub fn build_type_path_map(surface: &ApiSurface, core_import: &str) -> AHashMap<String, String> {
612    let mut map = AHashMap::new();
613    for typ in surface.types.iter().filter(|typ| !typ.is_trait) {
614        let path = typ.rust_path.replace('-', "_");
615        let resolved = if path.starts_with(core_import) {
616            path
617        } else {
618            format!("{core_import}::{}", typ.name)
619        };
620        map.insert(typ.name.clone(), resolved);
621    }
622    for en in &surface.enums {
623        let path = en.rust_path.replace('-', "_");
624        let resolved = if path.starts_with(core_import) {
625            path
626        } else {
627            format!("{core_import}::{}", en.name)
628        };
629        map.insert(en.name.clone(), resolved);
630    }
631    map
632}
633
634/// Resolve a `TypeRef::Named` short name to its full qualified path.
635///
636/// If the name is in the path map, returns the full path; otherwise falls back
637/// to `core_import::name`.
638pub fn resolve_named_path(name: &str, core_import: &str, path_map: &AHashMap<String, String>) -> String {
639    if let Some(path) = path_map.get(name) {
640        path.clone()
641    } else {
642        format!("{core_import}::{name}")
643    }
644}
645
646/// Generate a match arm for binding -> core direction.
647/// Binding enums are always unit-variant-only. Core enums may have data variants.
648/// For data variants: `BindingEnum::Variant => CoreEnum::Variant(Default::default(), ...)`
649pub fn binding_to_core_match_arm(binding_prefix: &str, variant_name: &str, fields: &[FieldDef]) -> String {
650    binding_to_core_match_arm_ext(binding_prefix, variant_name, fields, false)
651}
652
653/// Like `binding_to_core_match_arm` but `binding_has_data` controls whether the binding
654/// enum has the variant's fields (true) or is unit-only (false, e.g. Rustler/Elixir).
655/// Generate match arm for binding->core conversion with config (handles type conversions).
656pub fn binding_to_core_match_arm_ext_cfg(
657    binding_prefix: &str,
658    variant_name: &str,
659    fields: &[FieldDef],
660    binding_has_data: bool,
661    config: &ConversionConfig,
662) -> String {
663    use super::binding_to_core::field_conversion_to_core_cfg;
664
665    if fields.is_empty() {
666        format!("{binding_prefix}::{variant_name} => Self::{variant_name},")
667    } else if !binding_has_data {
668        // Binding is unit-only: use Default for core fields
669        if is_tuple_variant(fields) {
670            let defaults: Vec<&str> = fields.iter().map(|_| "Default::default()").collect();
671            format!(
672                "{binding_prefix}::{variant_name} => Self::{variant_name}({}),",
673                defaults.join(", ")
674            )
675        } else {
676            let defaults: Vec<String> = fields
677                .iter()
678                .map(|f| format!("{}: Default::default()", f.name))
679                .collect();
680            format!(
681                "{binding_prefix}::{variant_name} => Self::{variant_name} {{ {} }},",
682                defaults.join(", ")
683            )
684        }
685    } else if is_tuple_variant(fields) {
686        let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
687        let binding_pattern = field_names.join(", ");
688        let core_args: Vec<String> = fields
689            .iter()
690            .map(|f| {
691                let name = &f.name;
692                // Sanitized fields: binding uses String, use serde_json to deserialize back.
693                if f.sanitized {
694                    let expr = if let TypeRef::Vec(_) = &f.ty {
695                        // Vec<String> sanitized: each element is a debug-formatted value,
696                        // parse each one individually.
697                        format!("{name}.iter().filter_map(|s| serde_json::from_str(s).ok()).collect()")
698                    } else {
699                        format!("serde_json::from_str(&{name}).unwrap_or_default()")
700                    };
701                    return if f.is_boxed { format!("Box::new({expr})") } else { expr };
702                }
703                // Fields referencing excluded types: they appear as String in the binding.
704                // Use serde_json deserialization to convert back to the core type.
705                if !config.exclude_types.is_empty() && field_references_excluded_type(&f.ty, config.exclude_types) {
706                    let expr = format!("serde_json::from_str(&{name}).unwrap_or_default()");
707                    return if f.is_boxed { format!("Box::new({expr})") } else { expr };
708                }
709                // Use the conversion logic from field_conversion_to_core_cfg.
710                // In an enum match arm, fields are bound by destructuring (not via `val.field`),
711                // so replace `val.{name}` with just `{name}` in the generated expression.
712                let conv = field_conversion_to_core_cfg(name, &f.ty, f.optional, config);
713                // Extract the RHS from "name: expr" format
714                let expr = if let Some(expr) = conv.strip_prefix(&format!("{name}: ")) {
715                    let expr = expr.replace(&format!("val.{name}"), name);
716                    expr.to_string()
717                } else {
718                    conv
719                };
720                if f.is_boxed { format!("Box::new({expr})") } else { expr }
721            })
722            .collect();
723        format!(
724            "{binding_prefix}::{variant_name} {{ {binding_pattern} }} => Self::{variant_name}({}),",
725            core_args.join(", ")
726        )
727    } else {
728        let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
729        let pattern = field_names.join(", ");
730        let core_fields: Vec<String> = fields
731            .iter()
732            .map(|f| {
733                // Sanitized fields: the binding stores a simplified type (String for any complex type
734                // like Vec<(String,String)>, Vec<Named>, etc.). Use serde_json to deserialize back.
735                if f.sanitized {
736                    // For Vec<String> sanitized (representing Vec<Tuple> like Vec<(String, String)>),
737                    // each element is a debug-formatted string. Cannot safely use serde_json::from_str
738                    // on Vec<String>. Default to empty collection.
739                    if let TypeRef::Vec(_) = &f.ty {
740                        return format!(
741                            "{}: {}.iter().filter_map(|s| serde_json::from_str(s).ok()).collect()",
742                            f.name, f.name
743                        );
744                    }
745                    return format!("{}: serde_json::from_str(&{}).unwrap_or_default()", f.name, f.name);
746                }
747                // Use the conversion logic from field_conversion_to_core_cfg.
748                // In an enum match arm, fields are bound by destructuring (not via `val.field`),
749                // so replace `val.{name}` with just `{name}` in the generated expression.
750                let conv = field_conversion_to_core_cfg(&f.name, &f.ty, f.optional, config);
751                // Extract the RHS from "name: expr" format
752                if let Some(expr) = conv.strip_prefix(&format!("{}: ", f.name)) {
753                    let expr = expr.replace(&format!("val.{}", f.name), &f.name);
754                    format!("{}: {}", f.name, expr)
755                } else {
756                    conv
757                }
758            })
759            .collect();
760        format!(
761            "{binding_prefix}::{variant_name} {{ {pattern} }} => Self::{variant_name} {{ {} }},",
762            core_fields.join(", ")
763        )
764    }
765}
766
767pub fn binding_to_core_match_arm_ext(
768    binding_prefix: &str,
769    variant_name: &str,
770    fields: &[FieldDef],
771    binding_has_data: bool,
772) -> String {
773    if fields.is_empty() {
774        format!("{binding_prefix}::{variant_name} => Self::{variant_name},")
775    } else if !binding_has_data {
776        // Binding is unit-only: use Default for core fields
777        if is_tuple_variant(fields) {
778            let defaults: Vec<&str> = fields.iter().map(|_| "Default::default()").collect();
779            format!(
780                "{binding_prefix}::{variant_name} => Self::{variant_name}({}),",
781                defaults.join(", ")
782            )
783        } else {
784            let defaults: Vec<String> = fields
785                .iter()
786                .map(|f| format!("{}: Default::default()", f.name))
787                .collect();
788            format!(
789                "{binding_prefix}::{variant_name} => Self::{variant_name} {{ {} }},",
790                defaults.join(", ")
791            )
792        }
793    } else if is_tuple_variant(fields) {
794        // Binding uses struct syntax with _0, _1 etc., core uses tuple syntax
795        let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
796        let binding_pattern = field_names.join(", ");
797        // Wrap boxed fields with Box::new() and convert Named types with .into()
798        let core_args: Vec<String> = fields
799            .iter()
800            .map(|f| {
801                let name = &f.name;
802                let expr = if matches!(&f.ty, TypeRef::Named(_)) {
803                    format!("{name}.into()")
804                } else if f.sanitized {
805                    format!("serde_json::from_str(&{name}).unwrap_or_default()")
806                } else {
807                    name.clone()
808                };
809                if f.is_boxed { format!("Box::new({expr})") } else { expr }
810            })
811            .collect();
812        format!(
813            "{binding_prefix}::{variant_name} {{ {binding_pattern} }} => Self::{variant_name}({}),",
814            core_args.join(", ")
815        )
816    } else {
817        // Destructure binding named fields and pass to core, with .into() for Named types
818        let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
819        let pattern = field_names.join(", ");
820        let core_fields: Vec<String> = fields
821            .iter()
822            .map(|f| {
823                if matches!(&f.ty, TypeRef::Named(_)) {
824                    format!("{}: {}.into()", f.name, f.name)
825                } else if f.sanitized {
826                    // Sanitized fields have a simplified type in the binding (e.g. String)
827                    // but the core type is complex (e.g. Vec<(String,String)>).
828                    // Deserialize from JSON string for the binding→core conversion.
829                    format!("{}: serde_json::from_str(&{}).unwrap_or_default()", f.name, f.name)
830                } else {
831                    format!("{0}: {0}", f.name)
832                }
833            })
834            .collect();
835        format!(
836            "{binding_prefix}::{variant_name} {{ {pattern} }} => Self::{variant_name} {{ {} }},",
837            core_fields.join(", ")
838        )
839    }
840}
841
842/// Generate a match arm for core -> binding direction.
843/// When the binding also has data variants, destructure and forward fields.
844/// When the binding is unit-variant-only, discard core data with `..`.
845pub fn core_to_binding_match_arm(core_prefix: &str, variant_name: &str, fields: &[FieldDef]) -> String {
846    core_to_binding_match_arm_ext(core_prefix, variant_name, fields, false)
847}
848
849/// Like `core_to_binding_match_arm` but `binding_has_data` controls whether the binding
850/// enum has the variant's fields (true) or is unit-only (false).
851/// Generate match arm for core->binding conversion with config (handles type conversions).
852pub fn core_to_binding_match_arm_ext_cfg(
853    core_prefix: &str,
854    variant_name: &str,
855    fields: &[FieldDef],
856    binding_has_data: bool,
857    config: &ConversionConfig,
858) -> String {
859    use super::core_to_binding::field_conversion_from_core_cfg;
860    use ahash::AHashSet;
861
862    if fields.is_empty() {
863        format!("{core_prefix}::{variant_name} => Self::{variant_name},")
864    } else if !binding_has_data {
865        // Binding is unit-only: discard core data
866        if is_tuple_variant(fields) {
867            format!("{core_prefix}::{variant_name}(..) => Self::{variant_name},")
868        } else {
869            format!("{core_prefix}::{variant_name} {{ .. }} => Self::{variant_name},")
870        }
871    } else if is_tuple_variant(fields) {
872        let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
873        let core_pattern = field_names.join(", ");
874        let binding_fields: Vec<String> = fields
875            .iter()
876            .map(|f| {
877                // Use the conversion logic from field_conversion_from_core_cfg.
878                // In an enum match arm, fields are bound by destructuring (not via `val.field`),
879                // so replace `val.{name}` with just `{name}` in the generated expression.
880                let conv =
881                    field_conversion_from_core_cfg(&f.name, &f.ty, f.optional, f.sanitized, &AHashSet::new(), config);
882                // Extract the RHS from "name: expr" format
883                if let Some(expr) = conv.strip_prefix(&format!("{}: ", f.name)) {
884                    let mut expr = expr.replace(&format!("val.{}", f.name), &f.name);
885                    // Boxed fields in core tuple variants need dereferencing before conversion
886                    if f.is_boxed {
887                        expr = expr.replace(&format!("{}.into()", f.name), &format!("(*{}).into()", f.name));
888                    }
889                    format!("{}: {}", f.name, expr)
890                } else {
891                    conv
892                }
893            })
894            .collect();
895        format!(
896            "{core_prefix}::{variant_name}({core_pattern}) => Self::{variant_name} {{ {} }},",
897            binding_fields.join(", ")
898        )
899    } else {
900        let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
901        let pattern = field_names.join(", ");
902        let binding_fields: Vec<String> = fields
903            .iter()
904            .map(|f| {
905                // Use the conversion logic from field_conversion_from_core_cfg.
906                // In an enum match arm, fields are bound by destructuring (not via `val.field`),
907                // so replace `val.{name}` with just `{name}` in the generated expression.
908                let conv =
909                    field_conversion_from_core_cfg(&f.name, &f.ty, f.optional, f.sanitized, &AHashSet::new(), config);
910                // Extract the RHS from "name: expr" format
911                if let Some(expr) = conv.strip_prefix(&format!("{}: ", f.name)) {
912                    let expr = expr.replace(&format!("val.{}", f.name), &f.name);
913                    format!("{}: {}", f.name, expr)
914                } else {
915                    conv
916                }
917            })
918            .collect();
919        format!(
920            "{core_prefix}::{variant_name} {{ {pattern} }} => Self::{variant_name} {{ {} }},",
921            binding_fields.join(", ")
922        )
923    }
924}
925
926pub fn core_to_binding_match_arm_ext(
927    core_prefix: &str,
928    variant_name: &str,
929    fields: &[FieldDef],
930    binding_has_data: bool,
931) -> String {
932    if fields.is_empty() {
933        format!("{core_prefix}::{variant_name} => Self::{variant_name},")
934    } else if !binding_has_data {
935        // Binding is unit-only: discard core data
936        if is_tuple_variant(fields) {
937            format!("{core_prefix}::{variant_name}(..) => Self::{variant_name},")
938        } else {
939            format!("{core_prefix}::{variant_name} {{ .. }} => Self::{variant_name},")
940        }
941    } else if is_tuple_variant(fields) {
942        // Core uses tuple syntax, binding uses struct syntax with _0, _1 etc.
943        let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
944        let core_pattern = field_names.join(", ");
945        // Unbox and convert Named types with .into()
946        let binding_fields: Vec<String> = fields
947            .iter()
948            .map(|f| {
949                let name = &f.name;
950                let expr = if f.is_boxed && matches!(&f.ty, TypeRef::Named(_)) {
951                    format!("(*{name}).into()")
952                } else if f.is_boxed {
953                    format!("*{name}")
954                } else if matches!(&f.ty, TypeRef::Named(_)) {
955                    format!("{name}.into()")
956                } else if f.sanitized {
957                    format!("serde_json::to_string(&{name}).unwrap_or_default()")
958                } else {
959                    name.clone()
960                };
961                format!("{name}: {expr}")
962            })
963            .collect();
964        format!(
965            "{core_prefix}::{variant_name}({core_pattern}) => Self::{variant_name} {{ {} }},",
966            binding_fields.join(", ")
967        )
968    } else {
969        let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
970        let pattern = field_names.join(", ");
971        let binding_fields: Vec<String> = fields
972            .iter()
973            .map(|f| {
974                if matches!(&f.ty, TypeRef::Named(_)) {
975                    format!("{}: {}.into()", f.name, f.name)
976                } else if f.sanitized {
977                    // Sanitized fields have a simplified type in the binding (e.g. String)
978                    // but the core type is complex (e.g. Vec<(String,String)>).
979                    // Serialize to JSON string for the conversion.
980                    format!("{}: serde_json::to_string(&{}).unwrap_or_default()", f.name, f.name)
981                } else {
982                    format!("{0}: {0}", f.name)
983                }
984            })
985            .collect();
986        format!(
987            "{core_prefix}::{variant_name} {{ {pattern} }} => Self::{variant_name} {{ {} }},",
988            binding_fields.join(", ")
989        )
990    }
991}