Skip to main content

alef_codegen/
shared.rs

1use ahash::AHashSet;
2use alef_core::ir::{DefaultValue, FieldDef, MethodDef, ParamDef, PrimitiveType, ReceiverKind, TypeRef};
3use std::collections::HashMap;
4
5/// Returns true if this parameter is required but must be promoted to optional
6/// because it follows an optional parameter in the list.
7/// PyO3 requires that required params come before all optional params.
8pub fn is_promoted_optional(params: &[ParamDef], idx: usize) -> bool {
9    if params[idx].optional {
10        return false; // naturally optional
11    }
12    // Check if any earlier param is optional
13    params[..idx].iter().any(|p| p.optional)
14}
15
16/// Check if a free function can be auto-delegated to the core crate.
17/// Opaque Named params are allowed (unwrapped via Arc). Non-opaque Named params are not
18/// (require From impls that may not exist for types with sanitized fields).
19pub fn can_auto_delegate_function(func: &alef_core::ir::FunctionDef, opaque_types: &AHashSet<String>) -> bool {
20    !func.sanitized
21        && func
22            .params
23            .iter()
24            .all(|p| !p.sanitized && is_delegatable_param(&p.ty, opaque_types) && !is_named_ref_param(p, opaque_types))
25        && is_delegatable_return(&func.return_type)
26}
27
28/// Check if all params and return type are delegatable.
29/// For opaque types, skip methods with RefMut receiver (cannot borrow Arc mutably).
30pub fn can_auto_delegate(method: &MethodDef, opaque_types: &AHashSet<String>) -> bool {
31    // Skip RefMut methods on opaque types (Arc doesn't allow mutable access)
32    if matches!(method.receiver, Some(ReceiverKind::RefMut)) && method.trait_source.is_none() {
33        return false;
34    }
35    !method.sanitized
36        && method
37            .params
38            .iter()
39            .all(|p| !p.sanitized && is_delegatable_param(&p.ty, opaque_types) && !is_named_ref_param(p, opaque_types))
40        && is_delegatable_return(&method.return_type)
41}
42
43/// A Named param with is_ref=true needs a let-binding (can't inline .into() + borrow).
44/// A Vec<String> param with is_ref=true needs conversion to Vec<&str>.
45fn is_named_ref_param(p: &alef_core::ir::ParamDef, opaque_types: &AHashSet<String>) -> bool {
46    if !p.is_ref {
47        return false;
48    }
49    match &p.ty {
50        TypeRef::Named(name) => !opaque_types.contains(name.as_str()),
51        TypeRef::Vec(inner) => matches!(inner.as_ref(), TypeRef::String | TypeRef::Char),
52        _ => false,
53    }
54}
55
56/// A param type is delegatable if it's simple, or a Named type (opaque → Arc unwrap, non-opaque → .into()).
57pub fn is_delegatable_param(ty: &TypeRef, _opaque_types: &AHashSet<String>) -> bool {
58    match ty {
59        TypeRef::Primitive(_)
60        | TypeRef::String
61        | TypeRef::Char
62        | TypeRef::Bytes
63        | TypeRef::Path
64        | TypeRef::Unit
65        | TypeRef::Duration => true,
66        TypeRef::Named(_) => true, // Opaque: &*param.inner; non-opaque: .into()
67        TypeRef::Optional(inner) | TypeRef::Vec(inner) => is_delegatable_param(inner, _opaque_types),
68        TypeRef::Map(k, v) => is_delegatable_param(k, _opaque_types) && is_delegatable_param(v, _opaque_types),
69        TypeRef::Json => false,
70    }
71}
72
73/// Return types are more permissive — Named types work via .into() (core→binding From exists).
74pub fn is_delegatable_return(ty: &TypeRef) -> bool {
75    match ty {
76        TypeRef::Primitive(_)
77        | TypeRef::String
78        | TypeRef::Char
79        | TypeRef::Bytes
80        | TypeRef::Path
81        | TypeRef::Unit
82        | TypeRef::Duration => true,
83        TypeRef::Named(_) => true, // core→binding From impl generated for all convertible types
84        TypeRef::Optional(inner) | TypeRef::Vec(inner) => is_delegatable_return(inner),
85        TypeRef::Map(k, v) => is_delegatable_return(k) && is_delegatable_return(v),
86        TypeRef::Json => false,
87    }
88}
89
90/// A type is delegatable if it can cross the binding boundary without From impls.
91/// Named types are NOT delegatable as function params (may lack From impls).
92/// For opaque methods, Named types are handled separately via Arc wrap/unwrap.
93pub fn is_delegatable_type(ty: &TypeRef) -> bool {
94    match ty {
95        TypeRef::Primitive(_)
96        | TypeRef::String
97        | TypeRef::Char
98        | TypeRef::Bytes
99        | TypeRef::Path
100        | TypeRef::Unit
101        | TypeRef::Duration => true,
102        TypeRef::Named(_) => false, // Requires From impl which may not exist
103        TypeRef::Optional(inner) | TypeRef::Vec(inner) => is_delegatable_type(inner),
104        TypeRef::Map(k, v) => is_delegatable_type(k) && is_delegatable_type(v),
105        TypeRef::Json => false,
106    }
107}
108
109/// Check if a type is delegatable in the opaque method context.
110/// Opaque methods can handle Named params via Arc unwrap and Named returns via Arc wrap.
111pub fn is_opaque_delegatable_type(ty: &TypeRef) -> bool {
112    match ty {
113        TypeRef::Primitive(_)
114        | TypeRef::String
115        | TypeRef::Char
116        | TypeRef::Bytes
117        | TypeRef::Path
118        | TypeRef::Unit
119        | TypeRef::Duration => true,
120        TypeRef::Named(_) => true, // Opaque: Arc unwrap/wrap. Non-opaque: .into()
121        TypeRef::Optional(inner) | TypeRef::Vec(inner) => is_opaque_delegatable_type(inner),
122        TypeRef::Map(k, v) => is_opaque_delegatable_type(k) && is_opaque_delegatable_type(v),
123        TypeRef::Json => false,
124    }
125}
126
127/// Check if a type is "simple" — can be passed without any conversion.
128pub fn is_simple_type(ty: &TypeRef) -> bool {
129    match ty {
130        TypeRef::Primitive(_)
131        | TypeRef::String
132        | TypeRef::Char
133        | TypeRef::Bytes
134        | TypeRef::Path
135        | TypeRef::Unit
136        | TypeRef::Duration => true,
137        TypeRef::Optional(inner) | TypeRef::Vec(inner) => is_simple_type(inner),
138        TypeRef::Map(k, v) => is_simple_type(k) && is_simple_type(v),
139        TypeRef::Named(_) | TypeRef::Json => false,
140    }
141}
142
143/// Partition methods into (instance, static).
144pub fn partition_methods(methods: &[MethodDef]) -> (Vec<&MethodDef>, Vec<&MethodDef>) {
145    let instance: Vec<_> = methods.iter().filter(|m| m.receiver.is_some()).collect();
146    let statics: Vec<_> = methods.iter().filter(|m| m.receiver.is_none()).collect();
147    (instance, statics)
148}
149
150/// Build a constructor parameter list string.
151/// Returns (param_list, signature_with_defaults, field_assignments).
152/// If param_list exceeds 100 chars, uses multiline format with trailing commas.
153pub fn constructor_parts(fields: &[FieldDef], type_mapper: &dyn Fn(&TypeRef) -> String) -> (String, String, String) {
154    constructor_parts_with_renames(fields, type_mapper, None)
155}
156
157/// Like `constructor_parts` but with optional field renames for keyword escaping.
158/// `field_renames` maps original field name → binding field name (e.g. "class" → "class_").
159/// Parameters keep the original name (valid in Rust), struct literal uses the renamed field.
160pub fn constructor_parts_with_renames(
161    fields: &[FieldDef],
162    type_mapper: &dyn Fn(&TypeRef) -> String,
163    field_renames: Option<&HashMap<String, String>>,
164) -> (String, String, String) {
165    // Sort fields: required first, then optional.
166    // Many FFI frameworks (PyO3, NAPI) require required params before optional ones.
167    // Skip cfg-gated fields — they depend on features that may not be enabled.
168    let mut sorted_fields: Vec<&FieldDef> = fields.iter().filter(|f| f.cfg.is_none()).collect();
169    sorted_fields.sort_by_key(|f| f.optional as u8);
170
171    let params: Vec<String> = sorted_fields
172        .iter()
173        .map(|f| {
174            let ty = if f.optional {
175                match &f.ty {
176                    TypeRef::Optional(_) => type_mapper(&f.ty),
177                    _ => format!("Option<{}>", type_mapper(&f.ty)),
178                }
179            } else {
180                type_mapper(&f.ty)
181            };
182            format!("{}: {}", f.name, ty)
183        })
184        .collect();
185
186    let defaults: Vec<String> = sorted_fields
187        .iter()
188        .map(|f| {
189            if f.optional {
190                format!("{}=None", f.name)
191            } else {
192                f.name.clone()
193            }
194        })
195        .collect();
196
197    // Assignments keep original field order (for struct literal), excluding cfg-gated.
198    // When a field is renamed, emit `renamed: original` instead of just `original`.
199    let assignments: Vec<String> = fields
200        .iter()
201        .filter(|f| f.cfg.is_none())
202        .map(|f| {
203            if let Some(renames) = field_renames {
204                if let Some(renamed) = renames.get(&f.name) {
205                    return format!("{}: {}", renamed, f.name);
206                }
207            }
208            f.name.clone()
209        })
210        .collect();
211
212    // Format param_list with line wrapping if needed
213    let single_line = params.join(", ");
214    let param_list = if single_line.len() > 100 {
215        format!("\n        {},\n    ", params.join(",\n        "))
216    } else {
217        single_line
218    };
219
220    (param_list, defaults.join(", "), assignments.join(", "))
221}
222
223/// Build a function parameter list.
224pub fn function_params(params: &[ParamDef], type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
225    // After the first optional param, all subsequent params must also be optional
226    // to satisfy PyO3's signature constraint (required params can't follow optional ones).
227    let mut seen_optional = false;
228    params
229        .iter()
230        .map(|p| {
231            if p.optional {
232                seen_optional = true;
233            }
234            let ty = if p.optional || seen_optional {
235                format!("Option<{}>", type_mapper(&p.ty))
236            } else {
237                type_mapper(&p.ty)
238            };
239            format!("{}: {}", p.name, ty)
240        })
241        .collect::<Vec<_>>()
242        .join(", ")
243}
244
245/// Build a function signature defaults string (for pyo3 signature etc.).
246pub fn function_sig_defaults(params: &[ParamDef]) -> String {
247    // After the first optional param, all subsequent params must also carry a default
248    // to satisfy PyO3's signature constraint (required params can't follow optional ones).
249    // For optional params and Named/non-primitive promoted params: use `=None`.
250    // For promoted non-optional primitive params: use a type-appropriate zero/false default
251    // so PyO3 does not wrap the Rust type in Option<T> (which would cause a `?` unwrap error).
252    let mut seen_optional = false;
253    params
254        .iter()
255        .map(|p| {
256            if p.optional {
257                seen_optional = true;
258            }
259            if p.optional {
260                format!("{}=None", p.name)
261            } else if seen_optional {
262                // Promoted non-optional param: emit a type-appropriate default instead of None
263                // so PyO3 keeps the Rust parameter type as T (not Option<T>).
264                let default = match &p.ty {
265                    TypeRef::Primitive(PrimitiveType::Bool) => "false",
266                    TypeRef::Primitive(_) => "0",
267                    _ => "None",
268                };
269                format!("{}={}", p.name, default)
270            } else {
271                p.name.clone()
272            }
273        })
274        .collect::<Vec<_>>()
275        .join(", ")
276}
277
278/// Format a DefaultValue as Rust code for the target language.
279/// Used by backends generating config constructors with defaults.
280pub fn format_default_value(default: &DefaultValue) -> String {
281    match default {
282        DefaultValue::BoolLiteral(b) => format!("{}", b),
283        DefaultValue::StringLiteral(s) => format!("\"{}\".to_string()", s.escape_default()),
284        DefaultValue::IntLiteral(i) => format!("{}", i),
285        DefaultValue::FloatLiteral(f) => {
286            let s = format!("{}", f);
287            // Ensure the literal is a valid Rust float (must contain '.' or 'e'/'E')
288            if s.contains('.') || s.contains('e') || s.contains('E') {
289                s
290            } else {
291                format!("{s}.0")
292            }
293        }
294        DefaultValue::EnumVariant(v) => v.clone(),
295        DefaultValue::Empty => "Default::default()".to_string(),
296        DefaultValue::None => "None".to_string(),
297    }
298}
299
300/// Generate constructor parameter and assignment lists for types with has_default.
301/// All fields become Option<T> with None defaults for optional fields,
302/// or unwrap_or_else with actual defaults for required fields.
303///
304/// Returns (param_list, signature_defaults, assignments).
305/// This is used by PyO3 and similar backends that need signature annotations.
306/// Like `config_constructor_parts` but with extra options.
307/// When `option_duration_on_defaults` is true, non-optional Duration fields are stored
308/// as `Option<u64>` in the binding struct, so the constructor assignment is a passthrough
309/// (the From conversion will handle the None → core default mapping).
310pub fn config_constructor_parts_with_options(
311    fields: &[FieldDef],
312    type_mapper: &dyn Fn(&TypeRef) -> String,
313    option_duration_on_defaults: bool,
314) -> (String, String, String) {
315    config_constructor_parts_inner(fields, type_mapper, option_duration_on_defaults, None)
316}
317
318/// Like `config_constructor_parts_with_options` but with field renames for keyword escaping.
319pub fn config_constructor_parts_with_renames(
320    fields: &[FieldDef],
321    type_mapper: &dyn Fn(&TypeRef) -> String,
322    option_duration_on_defaults: bool,
323    field_renames: Option<&HashMap<String, String>>,
324) -> (String, String, String) {
325    config_constructor_parts_inner(fields, type_mapper, option_duration_on_defaults, field_renames)
326}
327
328pub fn config_constructor_parts(
329    fields: &[FieldDef],
330    type_mapper: &dyn Fn(&TypeRef) -> String,
331) -> (String, String, String) {
332    config_constructor_parts_inner(fields, type_mapper, false, None)
333}
334
335fn config_constructor_parts_inner(
336    fields: &[FieldDef],
337    type_mapper: &dyn Fn(&TypeRef) -> String,
338    option_duration_on_defaults: bool,
339    field_renames: Option<&HashMap<String, String>>,
340) -> (String, String, String) {
341    let mut sorted_fields: Vec<&FieldDef> = fields.iter().filter(|f| f.cfg.is_none()).collect();
342    sorted_fields.sort_by_key(|f| f.optional as u8);
343
344    let params: Vec<String> = sorted_fields
345        .iter()
346        .map(|f| {
347            let ty = type_mapper(&f.ty);
348            // All fields become Option<T>, but avoid Option<Option<T>> for already-optional fields.
349            // When f.ty is TypeRef::Optional(X), type_mapper already returns "Option<X>".
350            // Wrapping it again would yield Option<Option<X>>, making `None` ambiguous in PyO3
351            // signatures (E0283: type annotations needed).
352            if matches!(f.ty, TypeRef::Optional(_)) {
353                format!("{}: {}", f.name, ty)
354            } else {
355                format!("{}: Option<{}>", f.name, ty)
356            }
357        })
358        .collect();
359
360    // All fields have None default in signature
361    let defaults = sorted_fields
362        .iter()
363        .map(|f| format!("{}=None", f.name))
364        .collect::<Vec<_>>()
365        .join(", ");
366
367    // Assignments use unwrap_or_else with the typed default.
368    // `binding_name` is the struct field name (possibly renamed for keyword escaping),
369    // `f.name` is the original name used as the constructor parameter.
370    let assignments: Vec<String> = fields
371        .iter()
372        .filter(|f| f.cfg.is_none())
373        .map(|f| {
374            let binding_name = field_renames
375                .and_then(|r| r.get(&f.name))
376                .map_or_else(|| f.name.as_str(), |s| s.as_str());
377            // Duration fields on has_default types are stored as Option<u64> when
378            // option_duration_on_defaults is set — treat them as passthrough.
379            if option_duration_on_defaults && matches!(f.ty, TypeRef::Duration) {
380                return format!("{}: {}", binding_name, f.name);
381            }
382            if f.optional || matches!(&f.ty, TypeRef::Optional(_)) {
383                // Optional fields: passthrough (both param and field are Option<T>)
384                format!("{}: {}", binding_name, f.name)
385            } else if let Some(ref typed_default) = f.typed_default {
386                // For EnumVariant and Empty defaults, use unwrap_or_default()
387                // because we can't generate qualified Rust paths here.
388                match typed_default {
389                    DefaultValue::EnumVariant(_) | DefaultValue::Empty => {
390                        format!("{}: {}.unwrap_or_default()", binding_name, f.name)
391                    }
392                    _ => {
393                        let default_val = format_default_value(typed_default);
394                        // Use unwrap_or() for Copy literals (bool, int, float) to avoid
395                        // clippy::unnecessary_lazy_evaluations; use unwrap_or_else for heap types.
396                        match typed_default {
397                            DefaultValue::BoolLiteral(_)
398                            | DefaultValue::IntLiteral(_)
399                            | DefaultValue::FloatLiteral(_) => {
400                                format!("{}: {}.unwrap_or({})", binding_name, f.name, default_val)
401                            }
402                            _ => {
403                                format!("{}: {}.unwrap_or_else(|| {})", binding_name, f.name, default_val)
404                            }
405                        }
406                    }
407                }
408            } else {
409                // All binding types should impl Default (enums default to first variant,
410                // structs default via From<CoreType::default()>). unwrap_or_default() works.
411                format!("{}: {}.unwrap_or_default()", binding_name, f.name)
412            }
413        })
414        .collect();
415
416    let single_line = params.join(", ");
417    let param_list = if single_line.len() > 100 {
418        format!("\n        {},\n    ", params.join(",\n        "))
419    } else {
420        single_line
421    };
422
423    (param_list, defaults, assignments.join(", "))
424}