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