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        _ => unreachable!(),
17    }
18}
19
20/// Returns the binding primitive type string for cast primitives (core→binding direction).
21pub(crate) fn binding_prim_str(p: &PrimitiveType) -> &'static str {
22    match p {
23        PrimitiveType::U64 | PrimitiveType::Usize | PrimitiveType::Isize => "i64",
24        _ => unreachable!(),
25    }
26}
27
28/// Build the set of types that can have core→binding From safely generated.
29/// More permissive than binding→core: allows sanitized fields (uses format!("{:?}"))
30/// and accepts data enums (data discarded with `..` in match arms).
31pub fn core_to_binding_convertible_types(surface: &ApiSurface) -> AHashSet<String> {
32    let convertible_enums: AHashSet<&str> = surface
33        .enums
34        .iter()
35        .filter(|e| can_generate_enum_conversion_from_core(e))
36        .map(|e| e.name.as_str())
37        .collect();
38
39    let opaque_type_names: AHashSet<&str> = surface
40        .types
41        .iter()
42        .filter(|t| t.is_opaque)
43        .map(|t| t.name.as_str())
44        .collect();
45
46    // All non-opaque types are candidates (sanitized fields use format!("{:?}"))
47    let mut convertible: AHashSet<String> = surface
48        .types
49        .iter()
50        .filter(|t| !t.is_opaque)
51        .map(|t| t.name.clone())
52        .collect();
53
54    let mut changed = true;
55    while changed {
56        changed = false;
57        let snapshot: Vec<String> = convertible.iter().cloned().collect();
58        let mut known: AHashSet<&str> = convertible.iter().map(|s| s.as_str()).collect();
59        known.extend(&opaque_type_names);
60        let mut to_remove = Vec::new();
61        for type_name in &snapshot {
62            if let Some(typ) = surface.types.iter().find(|t| t.name == *type_name) {
63                let ok = typ
64                    .fields
65                    .iter()
66                    .all(|f| f.sanitized || is_field_convertible(&f.ty, &convertible_enums, &known));
67                if !ok {
68                    to_remove.push(type_name.clone());
69                }
70            }
71        }
72        for name in to_remove {
73            if convertible.remove(&name) {
74                changed = true;
75            }
76        }
77    }
78    convertible
79}
80
81/// Build the set of types that can have binding→core From safely generated.
82/// Strict: excludes types with sanitized fields (lossy conversion).
83/// This is transitive: a type is convertible only if all its Named field types
84/// are also convertible (or are enums with From/Into support).
85pub fn convertible_types(surface: &ApiSurface) -> AHashSet<String> {
86    // Build set of enums that have From/Into impls (unit-variant enums only)
87    let convertible_enums: AHashSet<&str> = surface
88        .enums
89        .iter()
90        .filter(|e| can_generate_enum_conversion(e))
91        .map(|e| e.name.as_str())
92        .collect();
93
94    // Build set of all known type names (including opaques) — opaque Named fields
95    // are convertible because we wrap/unwrap them via Arc.
96    let _all_type_names: AHashSet<&str> = surface.types.iter().map(|t| t.name.as_str()).collect();
97
98    // Start with all non-opaque types as candidates.
99    // Types with sanitized fields use Default::default() for the sanitized field
100    // in the binding→core direction (lossy but functional).
101    let mut convertible: AHashSet<String> = surface
102        .types
103        .iter()
104        .filter(|t| !t.is_opaque)
105        .map(|t| t.name.clone())
106        .collect();
107
108    // Set of opaque type names — Named fields referencing opaques are always convertible
109    // (they use Arc wrap/unwrap), so include them in the known-types check.
110    let opaque_type_names: AHashSet<&str> = surface
111        .types
112        .iter()
113        .filter(|t| t.is_opaque)
114        .map(|t| t.name.as_str())
115        .collect();
116
117    // Iteratively remove types whose fields reference non-convertible Named types.
118    // We check against `convertible ∪ opaque_types` so that types referencing
119    // excluded types (e.g. types with sanitized fields) are transitively removed,
120    // while opaque Named fields remain valid.
121    let mut changed = true;
122    while changed {
123        changed = false;
124        let snapshot: Vec<String> = convertible.iter().cloned().collect();
125        let mut known: AHashSet<&str> = convertible.iter().map(|s| s.as_str()).collect();
126        known.extend(&opaque_type_names);
127        let mut to_remove = Vec::new();
128        for type_name in &snapshot {
129            if let Some(typ) = surface.types.iter().find(|t| t.name == *type_name) {
130                let ok = typ
131                    .fields
132                    .iter()
133                    .all(|f| is_field_convertible(&f.ty, &convertible_enums, &known));
134                if !ok {
135                    to_remove.push(type_name.clone());
136                }
137            }
138        }
139        for name in to_remove {
140            if convertible.remove(&name) {
141                changed = true;
142            }
143        }
144    }
145    convertible
146}
147
148/// Check if a specific type is in the convertible set.
149pub fn can_generate_conversion(typ: &TypeDef, convertible: &AHashSet<String>) -> bool {
150    convertible.contains(&typ.name)
151}
152
153pub(crate) fn is_field_convertible(
154    ty: &TypeRef,
155    convertible_enums: &AHashSet<&str>,
156    known_types: &AHashSet<&str>,
157) -> bool {
158    match ty {
159        TypeRef::Primitive(_)
160        | TypeRef::String
161        | TypeRef::Char
162        | TypeRef::Bytes
163        | TypeRef::Path
164        | TypeRef::Unit
165        | TypeRef::Duration => true,
166        TypeRef::Json => false,
167        TypeRef::Optional(inner) | TypeRef::Vec(inner) => is_field_convertible(inner, convertible_enums, known_types),
168        TypeRef::Map(k, v) => {
169            is_field_convertible(k, convertible_enums, known_types)
170                && is_field_convertible(v, convertible_enums, known_types)
171        }
172        // Unit-variant enums and known types (including opaques, which use Arc wrap/unwrap) are convertible.
173        TypeRef::Named(name) => convertible_enums.contains(name.as_str()) || known_types.contains(name.as_str()),
174    }
175}
176
177/// Check if an enum can have From/Into safely generated (both directions).
178/// Supports unit-variant enums and enums whose data variants contain only
179/// simple convertible field types (primitives, String, Bytes, Path, Unit).
180pub fn can_generate_enum_conversion(enum_def: &EnumDef) -> bool {
181    enum_def
182        .variants
183        .iter()
184        .all(|v| v.fields.iter().all(|f| is_simple_type(&f.ty)))
185}
186
187/// Check if an enum can have core→binding From safely generated.
188/// This is always possible: unit variants map 1:1, data variants discard data with `..`.
189pub fn can_generate_enum_conversion_from_core(enum_def: &EnumDef) -> bool {
190    // Always possible — data variants are handled by pattern matching with `..`
191    !enum_def.variants.is_empty()
192}
193
194/// Returns true for types that are trivially convertible without needing
195/// to consult the convertible_enums/known_types sets.
196pub(crate) fn is_simple_type(ty: &TypeRef) -> bool {
197    match ty {
198        TypeRef::Primitive(_)
199        | TypeRef::String
200        | TypeRef::Char
201        | TypeRef::Bytes
202        | TypeRef::Path
203        | TypeRef::Unit
204        | TypeRef::Duration => true,
205        TypeRef::Optional(inner) | TypeRef::Vec(inner) => is_simple_type(inner),
206        TypeRef::Map(k, v) => is_simple_type(k) && is_simple_type(v),
207        TypeRef::Named(_) | TypeRef::Json => false,
208    }
209}
210
211/// Returns true if fields represent a tuple variant (positional: _0, _1, ...).
212pub fn is_tuple_variant(fields: &[FieldDef]) -> bool {
213    !fields.is_empty()
214        && fields[0]
215            .name
216            .strip_prefix('_')
217            .is_some_and(|rest: &str| rest.chars().all(|c: char| c.is_ascii_digit()))
218}
219
220/// Derive the Rust import path from rust_path, replacing hyphens with underscores.
221pub fn core_type_path(typ: &TypeDef, core_import: &str) -> String {
222    // rust_path is like "liter-llm::tower::RateLimitConfig"
223    // We need "liter_llm::tower::RateLimitConfig"
224    let path = typ.rust_path.replace('-', "_");
225    // If the path starts with the core_import, use it directly
226    if path.starts_with(core_import) {
227        path
228    } else {
229        // Fallback: just use core_import::name
230        format!("{core_import}::{}", typ.name)
231    }
232}
233
234/// Check if a type has any sanitized fields (binding→core conversion is lossy).
235pub fn has_sanitized_fields(typ: &TypeDef) -> bool {
236    typ.fields.iter().any(|f| f.sanitized)
237}
238
239/// Derive the Rust import path for an enum, replacing hyphens with underscores.
240pub fn core_enum_path(enum_def: &EnumDef, core_import: &str) -> String {
241    let path = enum_def.rust_path.replace('-', "_");
242    if path.starts_with(core_import) {
243        path
244    } else {
245        format!("{core_import}::{}", enum_def.name)
246    }
247}
248
249/// Generate a match arm for binding -> core direction.
250/// Binding enums are always unit-variant-only. Core enums may have data variants.
251/// For data variants: `BindingEnum::Variant => CoreEnum::Variant(Default::default(), ...)`
252pub fn binding_to_core_match_arm(binding_prefix: &str, variant_name: &str, fields: &[FieldDef]) -> String {
253    if fields.is_empty() {
254        format!("{binding_prefix}::{variant_name} => Self::{variant_name},")
255    } else if is_tuple_variant(fields) {
256        let defaults: Vec<&str> = fields.iter().map(|_| "Default::default()").collect();
257        format!(
258            "{binding_prefix}::{variant_name} => Self::{variant_name}({}),",
259            defaults.join(", ")
260        )
261    } else {
262        let defaults: Vec<String> = fields
263            .iter()
264            .map(|f| format!("{}: Default::default()", f.name))
265            .collect();
266        format!(
267            "{binding_prefix}::{variant_name} => Self::{variant_name} {{ {} }},",
268            defaults.join(", ")
269        )
270    }
271}
272
273/// Generate a match arm for core -> binding direction.
274/// Core enums may have data variants; binding enums are always unit-variant-only.
275/// For data variants: `CoreEnum::Variant(..) => Self::Variant`
276pub fn core_to_binding_match_arm(core_prefix: &str, variant_name: &str, fields: &[FieldDef]) -> String {
277    if fields.is_empty() {
278        format!("{core_prefix}::{variant_name} => Self::{variant_name},")
279    } else if is_tuple_variant(fields) {
280        format!("{core_prefix}::{variant_name}(..) => Self::{variant_name},")
281    } else {
282        format!("{core_prefix}::{variant_name} {{ .. }} => Self::{variant_name},")
283    }
284}