Skip to main content

alef_codegen/conversions/
helpers.rs

1use ahash::AHashSet;
2use alef_core::ir::{ApiSurface, EnumDef, FieldDef, PrimitiveType, TypeDef, TypeRef};
3
4/// Returns true if a primitive type needs i64 casting (NAPI/PHP — JS/PHP lack native u64).
5pub(crate) fn needs_i64_cast(p: &PrimitiveType) -> bool {
6    matches!(p, PrimitiveType::U64 | PrimitiveType::Usize | PrimitiveType::Isize)
7}
8
9/// Returns the core primitive type string for cast primitives.
10pub(crate) fn core_prim_str(p: &PrimitiveType) -> &'static str {
11    match p {
12        PrimitiveType::U64 => "u64",
13        PrimitiveType::Usize => "usize",
14        PrimitiveType::Isize => "isize",
15        PrimitiveType::F32 => "f32",
16        PrimitiveType::Bool => "bool",
17        PrimitiveType::U8 => "u8",
18        PrimitiveType::U16 => "u16",
19        PrimitiveType::U32 => "u32",
20        PrimitiveType::I8 => "i8",
21        PrimitiveType::I16 => "i16",
22        PrimitiveType::I32 => "i32",
23        PrimitiveType::I64 => "i64",
24        PrimitiveType::F64 => "f64",
25    }
26}
27
28/// Returns the binding primitive type string for cast primitives (core→binding direction).
29pub(crate) fn binding_prim_str(p: &PrimitiveType) -> &'static str {
30    match p {
31        PrimitiveType::U64 | PrimitiveType::Usize | PrimitiveType::Isize => "i64",
32        PrimitiveType::F32 => "f64",
33        PrimitiveType::Bool => "bool",
34        PrimitiveType::U8 | PrimitiveType::U16 | PrimitiveType::U32 => "i32",
35        PrimitiveType::I8 | PrimitiveType::I16 | PrimitiveType::I32 => "i32",
36        PrimitiveType::I64 => "i64",
37        PrimitiveType::F64 => "f64",
38    }
39}
40
41/// Build the set of types that can have core→binding From safely generated.
42/// More permissive than binding→core: allows sanitized fields (uses format!("{:?}"))
43/// and accepts data enums (data discarded with `..` in match arms).
44pub fn core_to_binding_convertible_types(surface: &ApiSurface) -> AHashSet<String> {
45    let convertible_enums: AHashSet<&str> = surface
46        .enums
47        .iter()
48        .filter(|e| can_generate_enum_conversion_from_core(e))
49        .map(|e| e.name.as_str())
50        .collect();
51
52    let opaque_type_names: AHashSet<&str> = surface
53        .types
54        .iter()
55        .filter(|t| t.is_opaque)
56        .map(|t| t.name.as_str())
57        .collect();
58
59    // All non-opaque types are candidates (sanitized fields use format!("{:?}"))
60    let mut convertible: AHashSet<String> = surface
61        .types
62        .iter()
63        .filter(|t| !t.is_opaque)
64        .map(|t| t.name.clone())
65        .collect();
66
67    let mut changed = true;
68    while changed {
69        changed = false;
70        let snapshot: Vec<String> = convertible.iter().cloned().collect();
71        let mut known: AHashSet<&str> = convertible.iter().map(|s| s.as_str()).collect();
72        known.extend(&opaque_type_names);
73        let mut to_remove = Vec::new();
74        for type_name in &snapshot {
75            if let Some(typ) = surface.types.iter().find(|t| t.name == *type_name) {
76                let ok = typ
77                    .fields
78                    .iter()
79                    .all(|f| f.sanitized || is_field_convertible(&f.ty, &convertible_enums, &known));
80                if !ok {
81                    to_remove.push(type_name.clone());
82                }
83            }
84        }
85        for name in to_remove {
86            if convertible.remove(&name) {
87                changed = true;
88            }
89        }
90    }
91    convertible
92}
93
94/// Build the set of types that can have binding→core From safely generated.
95/// Strict: excludes types with sanitized fields (lossy conversion).
96/// This is transitive: a type is convertible only if all its Named field types
97/// are also convertible (or are enums with From/Into support).
98pub fn convertible_types(surface: &ApiSurface) -> AHashSet<String> {
99    // Build set of enums that have From/Into impls (unit-variant enums only)
100    let convertible_enums: AHashSet<&str> = surface
101        .enums
102        .iter()
103        .filter(|e| can_generate_enum_conversion(e))
104        .map(|e| e.name.as_str())
105        .collect();
106
107    // Build set of all known type names (including opaques) — opaque Named fields
108    // are convertible because we wrap/unwrap them via Arc.
109    let _all_type_names: AHashSet<&str> = surface.types.iter().map(|t| t.name.as_str()).collect();
110
111    // Build set of Named types that implement Default — sanitized fields referencing
112    // Named types without Default would cause a compile error in the generated From impl.
113    let default_type_names: AHashSet<&str> = surface
114        .types
115        .iter()
116        .filter(|t| t.has_default)
117        .map(|t| t.name.as_str())
118        .collect();
119
120    // Start with all non-opaque types as candidates.
121    // Types with sanitized fields use Default::default() for the sanitized field
122    // in the binding→core direction — but only if the field type implements Default.
123    let mut convertible: AHashSet<String> = surface
124        .types
125        .iter()
126        .filter(|t| !t.is_opaque)
127        .map(|t| t.name.clone())
128        .collect();
129
130    // Set of opaque type names — Named fields referencing opaques are always convertible
131    // (they use Arc wrap/unwrap), so include them in the known-types check.
132    let opaque_type_names: AHashSet<&str> = surface
133        .types
134        .iter()
135        .filter(|t| t.is_opaque)
136        .map(|t| t.name.as_str())
137        .collect();
138
139    // Iteratively remove types whose fields reference non-convertible Named types.
140    // We check against `convertible ∪ opaque_types` so that types referencing
141    // excluded types (e.g. types with sanitized fields) are transitively removed,
142    // while opaque Named fields remain valid.
143    let mut changed = true;
144    while changed {
145        changed = false;
146        let snapshot: Vec<String> = convertible.iter().cloned().collect();
147        let mut known: AHashSet<&str> = convertible.iter().map(|s| s.as_str()).collect();
148        known.extend(&opaque_type_names);
149        let mut to_remove = Vec::new();
150        for type_name in &snapshot {
151            if let Some(typ) = surface.types.iter().find(|t| t.name == *type_name) {
152                let ok = typ.fields.iter().all(|f| {
153                    if f.sanitized {
154                        // Sanitized fields use Default::default() in the generated From impl.
155                        // If the field type is a Named type without Default, the impl won't compile.
156                        sanitized_field_has_default(&f.ty, &default_type_names)
157                    } else {
158                        is_field_convertible(&f.ty, &convertible_enums, &known)
159                    }
160                });
161                if !ok {
162                    to_remove.push(type_name.clone());
163                }
164            }
165        }
166        for name in to_remove {
167            if convertible.remove(&name) {
168                changed = true;
169            }
170        }
171    }
172    convertible
173}
174
175/// Check if a sanitized field's type can produce a valid `Default::default()` expression.
176/// Primitive types, strings, collections, Options, and Named types with `has_default` are fine.
177/// Named types without `has_default` are not — generating `Default::default()` for them would
178/// fail to compile.
179fn sanitized_field_has_default(ty: &TypeRef, default_types: &AHashSet<&str>) -> bool {
180    match ty {
181        TypeRef::Primitive(_)
182        | TypeRef::String
183        | TypeRef::Char
184        | TypeRef::Bytes
185        | TypeRef::Path
186        | TypeRef::Unit
187        | TypeRef::Duration
188        | TypeRef::Json => true,
189        // Option<T> defaults to None regardless of T
190        TypeRef::Optional(_) => true,
191        // Vec<T> defaults to empty vec regardless of T
192        TypeRef::Vec(_) => true,
193        // Map<K, V> defaults to empty map regardless of K/V
194        TypeRef::Map(_, _) => true,
195        TypeRef::Named(name) => {
196            if is_tuple_type_name(name) {
197                // Tuple types are always passthrough
198                true
199            } else {
200                // Named type must have has_default to be safely used via Default::default()
201                default_types.contains(name.as_str())
202            }
203        }
204    }
205}
206
207/// Check if a specific type is in the convertible set.
208pub fn can_generate_conversion(typ: &TypeDef, convertible: &AHashSet<String>) -> bool {
209    convertible.contains(&typ.name)
210}
211
212pub(crate) fn is_field_convertible(
213    ty: &TypeRef,
214    convertible_enums: &AHashSet<&str>,
215    known_types: &AHashSet<&str>,
216) -> bool {
217    match ty {
218        TypeRef::Primitive(_)
219        | TypeRef::String
220        | TypeRef::Char
221        | TypeRef::Bytes
222        | TypeRef::Path
223        | TypeRef::Unit
224        | TypeRef::Duration => true,
225        TypeRef::Json => true,
226        TypeRef::Optional(inner) | TypeRef::Vec(inner) => is_field_convertible(inner, convertible_enums, known_types),
227        TypeRef::Map(k, v) => {
228            is_field_convertible(k, convertible_enums, known_types)
229                && is_field_convertible(v, convertible_enums, known_types)
230        }
231        // Tuple types are passthrough — always convertible
232        TypeRef::Named(name) if is_tuple_type_name(name) => true,
233        // Unit-variant enums and known types (including opaques, which use Arc wrap/unwrap) are convertible.
234        TypeRef::Named(name) => convertible_enums.contains(name.as_str()) || known_types.contains(name.as_str()),
235    }
236}
237
238/// Check if an enum can have From/Into safely generated (both directions).
239/// All enums are allowed — data variants use Default::default() for non-simple fields
240/// in the binding→core direction.
241pub fn can_generate_enum_conversion(enum_def: &EnumDef) -> bool {
242    !enum_def.variants.is_empty()
243}
244
245/// Check if an enum can have core→binding From safely generated.
246/// This is always possible: unit variants map 1:1, data variants discard data with `..`.
247pub fn can_generate_enum_conversion_from_core(enum_def: &EnumDef) -> bool {
248    // Always possible — data variants are handled by pattern matching with `..`
249    !enum_def.variants.is_empty()
250}
251
252/// Returns true if fields represent a tuple variant (positional: _0, _1, ...).
253pub fn is_tuple_variant(fields: &[FieldDef]) -> bool {
254    !fields.is_empty()
255        && fields[0]
256            .name
257            .strip_prefix('_')
258            .is_some_and(|rest: &str| rest.chars().all(|c: char| c.is_ascii_digit()))
259}
260
261/// Returns true if a TypeDef represents a newtype struct (single unnamed field `_0`).
262pub fn is_newtype(typ: &TypeDef) -> bool {
263    typ.fields.len() == 1 && typ.fields[0].name == "_0"
264}
265
266/// Returns true if a type name looks like a tuple (starts with `(`).
267/// Tuple types are passthrough — no conversion needed.
268pub(crate) fn is_tuple_type_name(name: &str) -> bool {
269    name.starts_with('(')
270}
271
272/// Derive the Rust import path from rust_path, replacing hyphens with underscores.
273pub fn core_type_path(typ: &TypeDef, core_import: &str) -> String {
274    // rust_path is like "liter-llm::tower::RateLimitConfig"
275    // We need "liter_llm::tower::RateLimitConfig"
276    let path = typ.rust_path.replace('-', "_");
277    // If the path starts with the core_import, use it directly
278    if path.starts_with(core_import) {
279        path
280    } else {
281        // Fallback: just use core_import::name
282        format!("{core_import}::{}", typ.name)
283    }
284}
285
286/// Check if a type has any sanitized fields (binding→core conversion is lossy).
287pub fn has_sanitized_fields(typ: &TypeDef) -> bool {
288    typ.fields.iter().any(|f| f.sanitized)
289}
290
291/// Derive the Rust import path for an enum, replacing hyphens with underscores.
292pub fn core_enum_path(enum_def: &EnumDef, core_import: &str) -> String {
293    let path = enum_def.rust_path.replace('-', "_");
294    if path.starts_with(core_import) {
295        path
296    } else {
297        format!("{core_import}::{}", enum_def.name)
298    }
299}
300
301/// Generate a match arm for binding -> core direction.
302/// Binding enums are always unit-variant-only. Core enums may have data variants.
303/// For data variants: `BindingEnum::Variant => CoreEnum::Variant(Default::default(), ...)`
304pub fn binding_to_core_match_arm(binding_prefix: &str, variant_name: &str, fields: &[FieldDef]) -> String {
305    binding_to_core_match_arm_ext(binding_prefix, variant_name, fields, false)
306}
307
308/// Like `binding_to_core_match_arm` but `binding_has_data` controls whether the binding
309/// enum has the variant's fields (true) or is unit-only (false, e.g. Rustler/Elixir).
310pub fn binding_to_core_match_arm_ext(
311    binding_prefix: &str,
312    variant_name: &str,
313    fields: &[FieldDef],
314    binding_has_data: bool,
315) -> String {
316    if fields.is_empty() {
317        format!("{binding_prefix}::{variant_name} => Self::{variant_name},")
318    } else if !binding_has_data {
319        // Binding is unit-only: use Default for core fields
320        if is_tuple_variant(fields) {
321            let defaults: Vec<&str> = fields.iter().map(|_| "Default::default()").collect();
322            format!(
323                "{binding_prefix}::{variant_name} => Self::{variant_name}({}),",
324                defaults.join(", ")
325            )
326        } else {
327            let defaults: Vec<String> = fields
328                .iter()
329                .map(|f| format!("{}: Default::default()", f.name))
330                .collect();
331            format!(
332                "{binding_prefix}::{variant_name} => Self::{variant_name} {{ {} }},",
333                defaults.join(", ")
334            )
335        }
336    } else if is_tuple_variant(fields) {
337        // Binding uses struct syntax with _0, _1 etc., core uses tuple syntax
338        let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
339        let binding_pattern = field_names.join(", ");
340        // Wrap boxed fields with Box::new() and convert Named types with .into()
341        let core_args: Vec<String> = fields
342            .iter()
343            .map(|f| {
344                let name = &f.name;
345                let expr = if matches!(&f.ty, TypeRef::Named(_)) {
346                    format!("{name}.into()")
347                } else {
348                    name.clone()
349                };
350                if f.is_boxed { format!("Box::new({expr})") } else { expr }
351            })
352            .collect();
353        format!(
354            "{binding_prefix}::{variant_name} {{ {binding_pattern} }} => Self::{variant_name}({}),",
355            core_args.join(", ")
356        )
357    } else {
358        // Destructure binding named fields and pass to core, with .into() for Named types
359        let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
360        let pattern = field_names.join(", ");
361        let core_fields: Vec<String> = fields
362            .iter()
363            .map(|f| {
364                if matches!(&f.ty, TypeRef::Named(_)) {
365                    format!("{}: {}.into()", f.name, f.name)
366                } else {
367                    format!("{0}: {0}", f.name)
368                }
369            })
370            .collect();
371        format!(
372            "{binding_prefix}::{variant_name} {{ {pattern} }} => Self::{variant_name} {{ {} }},",
373            core_fields.join(", ")
374        )
375    }
376}
377
378/// Generate a match arm for core -> binding direction.
379/// When the binding also has data variants, destructure and forward fields.
380/// When the binding is unit-variant-only, discard core data with `..`.
381pub fn core_to_binding_match_arm(core_prefix: &str, variant_name: &str, fields: &[FieldDef]) -> String {
382    core_to_binding_match_arm_ext(core_prefix, variant_name, fields, false)
383}
384
385/// Like `core_to_binding_match_arm` but `binding_has_data` controls whether the binding
386/// enum has the variant's fields (true) or is unit-only (false).
387pub fn core_to_binding_match_arm_ext(
388    core_prefix: &str,
389    variant_name: &str,
390    fields: &[FieldDef],
391    binding_has_data: bool,
392) -> String {
393    if fields.is_empty() {
394        format!("{core_prefix}::{variant_name} => Self::{variant_name},")
395    } else if !binding_has_data {
396        // Binding is unit-only: discard core data
397        if is_tuple_variant(fields) {
398            format!("{core_prefix}::{variant_name}(..) => Self::{variant_name},")
399        } else {
400            format!("{core_prefix}::{variant_name} {{ .. }} => Self::{variant_name},")
401        }
402    } else if is_tuple_variant(fields) {
403        // Core uses tuple syntax, binding uses struct syntax with _0, _1 etc.
404        let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
405        let core_pattern = field_names.join(", ");
406        // Unbox and convert Named types with .into()
407        let binding_fields: Vec<String> = fields
408            .iter()
409            .map(|f| {
410                let name = &f.name;
411                let expr = if f.is_boxed && matches!(&f.ty, TypeRef::Named(_)) {
412                    format!("(*{name}).into()")
413                } else if f.is_boxed {
414                    format!("*{name}")
415                } else if matches!(&f.ty, TypeRef::Named(_)) {
416                    format!("{name}.into()")
417                } else {
418                    name.clone()
419                };
420                format!("{name}: {expr}")
421            })
422            .collect();
423        format!(
424            "{core_prefix}::{variant_name}({core_pattern}) => Self::{variant_name} {{ {} }},",
425            binding_fields.join(", ")
426        )
427    } else {
428        let field_names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
429        let pattern = field_names.join(", ");
430        let binding_fields: Vec<String> = fields
431            .iter()
432            .map(|f| {
433                if matches!(&f.ty, TypeRef::Named(_)) {
434                    format!("{}: {}.into()", f.name, f.name)
435                } else {
436                    format!("{0}: {0}", f.name)
437                }
438            })
439            .collect();
440        format!(
441            "{core_prefix}::{variant_name} {{ {pattern} }} => Self::{variant_name} {{ {} }},",
442            binding_fields.join(", ")
443        )
444    }
445}