alef-codegen 0.15.15

Shared codegen utilities for the alef polyglot binding generator
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
use ahash::AHashSet;
use alef_core::ir::{DefaultValue, FieldDef, MethodDef, ParamDef, PrimitiveType, ReceiverKind, TypeRef};
use std::collections::HashMap;

/// Returns true if this parameter is required but must be promoted to optional
/// because it follows an optional parameter in the list.
/// PyO3 requires that required params come before all optional params.
pub fn is_promoted_optional(params: &[ParamDef], idx: usize) -> bool {
    if params[idx].optional {
        return false; // naturally optional
    }
    // Check if any earlier param is optional
    params[..idx].iter().any(|p| p.optional)
}

/// Check if a free function can be auto-delegated to the core crate.
/// Opaque Named params are allowed (unwrapped via Arc). Non-opaque Named params are not
/// (require From impls that may not exist for types with sanitized fields).
pub fn can_auto_delegate_function(func: &alef_core::ir::FunctionDef, opaque_types: &AHashSet<String>) -> bool {
    !func.sanitized
        && func
            .params
            .iter()
            .all(|p| !p.sanitized && is_delegatable_param(&p.ty, opaque_types) && !is_named_ref_param(p, opaque_types))
        && is_delegatable_return(&func.return_type)
}

/// Check if all params and return type are delegatable.
/// For opaque types, skip methods with RefMut receiver (cannot borrow Arc mutably).
pub fn can_auto_delegate(method: &MethodDef, opaque_types: &AHashSet<String>) -> bool {
    // Skip RefMut methods on opaque types (Arc doesn't allow mutable access)
    if matches!(method.receiver, Some(ReceiverKind::RefMut)) && method.trait_source.is_none() {
        return false;
    }
    !method.sanitized
        && method
            .params
            .iter()
            .all(|p| !p.sanitized && is_delegatable_param(&p.ty, opaque_types) && !is_named_ref_param(p, opaque_types))
        && is_delegatable_return(&method.return_type)
}

/// A Named param with is_ref=true needs a let-binding (can't inline .into() + borrow).
/// A Vec<String> param with is_ref=true needs conversion to Vec<&str>.
fn is_named_ref_param(p: &alef_core::ir::ParamDef, opaque_types: &AHashSet<String>) -> bool {
    if !p.is_ref {
        return false;
    }
    match &p.ty {
        TypeRef::Named(name) => !opaque_types.contains(name.as_str()),
        TypeRef::Vec(inner) => matches!(inner.as_ref(), TypeRef::String | TypeRef::Char),
        _ => false,
    }
}

/// A param type is delegatable if it's simple, or a Named type (opaque → Arc unwrap, non-opaque → .into()).
pub fn is_delegatable_param(ty: &TypeRef, _opaque_types: &AHashSet<String>) -> bool {
    match ty {
        TypeRef::Primitive(_)
        | TypeRef::String
        | TypeRef::Char
        | TypeRef::Bytes
        | TypeRef::Path
        | TypeRef::Unit
        | TypeRef::Duration => true,
        TypeRef::Named(_) => true, // Opaque: &*param.inner; non-opaque: .into()
        TypeRef::Optional(inner) | TypeRef::Vec(inner) => is_delegatable_param(inner, _opaque_types),
        TypeRef::Map(k, v) => is_delegatable_param(k, _opaque_types) && is_delegatable_param(v, _opaque_types),
        TypeRef::Json => false,
    }
}

/// Return types are more permissive — Named types work via .into() (core→binding From exists).
pub fn is_delegatable_return(ty: &TypeRef) -> bool {
    match ty {
        TypeRef::Primitive(_)
        | TypeRef::String
        | TypeRef::Char
        | TypeRef::Bytes
        | TypeRef::Path
        | TypeRef::Unit
        | TypeRef::Duration => true,
        TypeRef::Named(_) => true, // core→binding From impl generated for all convertible types
        TypeRef::Optional(inner) | TypeRef::Vec(inner) => is_delegatable_return(inner),
        TypeRef::Map(k, v) => is_delegatable_return(k) && is_delegatable_return(v),
        TypeRef::Json => false,
    }
}

/// A type is delegatable if it can cross the binding boundary without From impls.
/// Named types are NOT delegatable as function params (may lack From impls).
/// For opaque methods, Named types are handled separately via Arc wrap/unwrap.
pub fn is_delegatable_type(ty: &TypeRef) -> bool {
    match ty {
        TypeRef::Primitive(_)
        | TypeRef::String
        | TypeRef::Char
        | TypeRef::Bytes
        | TypeRef::Path
        | TypeRef::Unit
        | TypeRef::Duration => true,
        TypeRef::Named(_) => false, // Requires From impl which may not exist
        TypeRef::Optional(inner) | TypeRef::Vec(inner) => is_delegatable_type(inner),
        TypeRef::Map(k, v) => is_delegatable_type(k) && is_delegatable_type(v),
        TypeRef::Json => false,
    }
}

/// Check if a type is delegatable in the opaque method context.
/// Opaque methods can handle Named params via Arc unwrap and Named returns via Arc wrap.
pub fn is_opaque_delegatable_type(ty: &TypeRef) -> bool {
    match ty {
        TypeRef::Primitive(_)
        | TypeRef::String
        | TypeRef::Char
        | TypeRef::Bytes
        | TypeRef::Path
        | TypeRef::Unit
        | TypeRef::Duration => true,
        TypeRef::Named(_) => true, // Opaque: Arc unwrap/wrap. Non-opaque: .into()
        TypeRef::Optional(inner) | TypeRef::Vec(inner) => is_opaque_delegatable_type(inner),
        TypeRef::Map(k, v) => is_opaque_delegatable_type(k) && is_opaque_delegatable_type(v),
        TypeRef::Json => false,
    }
}

/// Check if a type is "simple" — can be passed without any conversion.
pub fn is_simple_type(ty: &TypeRef) -> bool {
    match ty {
        TypeRef::Primitive(_)
        | TypeRef::String
        | TypeRef::Char
        | TypeRef::Bytes
        | TypeRef::Path
        | TypeRef::Unit
        | TypeRef::Duration => true,
        TypeRef::Optional(inner) | TypeRef::Vec(inner) => is_simple_type(inner),
        TypeRef::Map(k, v) => is_simple_type(k) && is_simple_type(v),
        TypeRef::Named(_) | TypeRef::Json => false,
    }
}

/// Partition methods into (instance, static).
pub fn partition_methods(methods: &[MethodDef]) -> (Vec<&MethodDef>, Vec<&MethodDef>) {
    let instance: Vec<_> = methods.iter().filter(|m| m.receiver.is_some()).collect();
    let statics: Vec<_> = methods.iter().filter(|m| m.receiver.is_none()).collect();
    (instance, statics)
}

/// Build a constructor parameter list string.
/// Returns (param_list, signature_with_defaults, field_assignments).
/// If param_list exceeds 100 chars, uses multiline format with trailing commas.
pub fn constructor_parts(fields: &[FieldDef], type_mapper: &dyn Fn(&TypeRef) -> String) -> (String, String, String) {
    constructor_parts_with_renames(fields, type_mapper, None)
}

/// Like `constructor_parts` but with optional field renames for keyword escaping.
/// `field_renames` maps original field name → binding field name (e.g. "class" → "class_").
/// Parameters keep the original name (valid in Rust), struct literal uses the renamed field.
pub fn constructor_parts_with_renames(
    fields: &[FieldDef],
    type_mapper: &dyn Fn(&TypeRef) -> String,
    field_renames: Option<&HashMap<String, String>>,
) -> (String, String, String) {
    // Sort fields: required first, then optional.
    // Many FFI frameworks (PyO3, NAPI) require required params before optional ones.
    // Skip cfg-gated fields — they depend on features that may not be enabled.
    let mut sorted_fields: Vec<&FieldDef> = fields.iter().filter(|f| f.cfg.is_none()).collect();
    sorted_fields.sort_by_key(|f| f.optional as u8);

    let params: Vec<String> = sorted_fields
        .iter()
        .map(|f| {
            let ty = if f.optional {
                match &f.ty {
                    TypeRef::Optional(_) => type_mapper(&f.ty),
                    _ => format!("Option<{}>", type_mapper(&f.ty)),
                }
            } else {
                type_mapper(&f.ty)
            };
            format!("{}: {}", f.name, ty)
        })
        .collect();

    let defaults: Vec<String> = sorted_fields
        .iter()
        .map(|f| {
            if f.optional {
                format!("{}=None", f.name)
            } else {
                f.name.clone()
            }
        })
        .collect();

    // Assignments keep original field order (for struct literal), excluding cfg-gated.
    // When a field is renamed, emit `renamed: original` instead of just `original`.
    let assignments: Vec<String> = fields
        .iter()
        .filter(|f| f.cfg.is_none())
        .map(|f| {
            if let Some(renames) = field_renames {
                if let Some(renamed) = renames.get(&f.name) {
                    return format!("{}: {}", renamed, f.name);
                }
            }
            f.name.clone()
        })
        .collect();

    // Format param_list with line wrapping if needed
    let single_line = params.join(", ");
    let param_list = if single_line.len() > 100 {
        format!("\n        {},\n    ", params.join(",\n        "))
    } else {
        single_line
    };

    (param_list, defaults.join(", "), assignments.join(", "))
}

/// Build a function parameter list.
pub fn function_params(params: &[ParamDef], type_mapper: &dyn Fn(&TypeRef) -> String) -> String {
    // After the first optional param, all subsequent params must also be optional
    // to satisfy PyO3's signature constraint (required params can't follow optional ones).
    let mut seen_optional = false;
    params
        .iter()
        .map(|p| {
            if p.optional {
                seen_optional = true;
            }
            let ty = if p.optional || seen_optional {
                format!("Option<{}>", type_mapper(&p.ty))
            } else {
                type_mapper(&p.ty)
            };
            format!("{}: {}", p.name, ty)
        })
        .collect::<Vec<_>>()
        .join(", ")
}

/// Build a function signature defaults string (for pyo3 signature etc.).
pub fn function_sig_defaults(params: &[ParamDef]) -> String {
    // After the first optional param, all subsequent params must also carry a default
    // to satisfy PyO3's signature constraint (required params can't follow optional ones).
    // For optional params and Named/non-primitive promoted params: use `=None`.
    // For promoted non-optional primitive params: use a type-appropriate zero/false default
    // so PyO3 does not wrap the Rust type in Option<T> (which would cause a `?` unwrap error).
    let mut seen_optional = false;
    params
        .iter()
        .map(|p| {
            if p.optional {
                seen_optional = true;
            }
            if p.optional {
                format!("{}=None", p.name)
            } else if seen_optional {
                // Promoted non-optional param: emit a type-appropriate default instead of None
                // so PyO3 keeps the Rust parameter type as T (not Option<T>).
                let default = match &p.ty {
                    TypeRef::Primitive(PrimitiveType::Bool) => "false",
                    TypeRef::Primitive(_) => "0",
                    _ => "None",
                };
                format!("{}={}", p.name, default)
            } else {
                p.name.clone()
            }
        })
        .collect::<Vec<_>>()
        .join(", ")
}

/// Format a DefaultValue as Rust code for the target language.
/// Used by backends generating config constructors with defaults.
pub fn format_default_value(default: &DefaultValue) -> String {
    match default {
        DefaultValue::BoolLiteral(b) => format!("{}", b),
        DefaultValue::StringLiteral(s) => format!("\"{}\".to_string()", s.escape_default()),
        DefaultValue::IntLiteral(i) => format!("{}", i),
        DefaultValue::FloatLiteral(f) => {
            let s = format!("{}", f);
            // Ensure the literal is a valid Rust float (must contain '.' or 'e'/'E')
            if s.contains('.') || s.contains('e') || s.contains('E') {
                s
            } else {
                format!("{s}.0")
            }
        }
        DefaultValue::EnumVariant(v) => v.clone(),
        DefaultValue::Empty => "Default::default()".to_string(),
        DefaultValue::None => "None".to_string(),
    }
}

/// Generate constructor parameter and assignment lists for types with has_default.
/// All fields become Option<T> with None defaults for optional fields,
/// or unwrap_or_else with actual defaults for required fields.
///
/// Returns (param_list, signature_defaults, assignments).
/// This is used by PyO3 and similar backends that need signature annotations.
/// Like `config_constructor_parts` but with extra options.
/// When `option_duration_on_defaults` is true, non-optional Duration fields are stored
/// as `Option<u64>` in the binding struct, so the constructor assignment is a passthrough
/// (the From conversion will handle the None → core default mapping).
pub fn config_constructor_parts_with_options(
    fields: &[FieldDef],
    type_mapper: &dyn Fn(&TypeRef) -> String,
    option_duration_on_defaults: bool,
) -> (String, String, String) {
    config_constructor_parts_inner(fields, type_mapper, option_duration_on_defaults, None)
}

/// Like `config_constructor_parts_with_options` but with field renames for keyword escaping.
pub fn config_constructor_parts_with_renames(
    fields: &[FieldDef],
    type_mapper: &dyn Fn(&TypeRef) -> String,
    option_duration_on_defaults: bool,
    field_renames: Option<&HashMap<String, String>>,
) -> (String, String, String) {
    config_constructor_parts_inner(fields, type_mapper, option_duration_on_defaults, field_renames)
}

pub fn config_constructor_parts(
    fields: &[FieldDef],
    type_mapper: &dyn Fn(&TypeRef) -> String,
) -> (String, String, String) {
    config_constructor_parts_inner(fields, type_mapper, false, None)
}

fn config_constructor_parts_inner(
    fields: &[FieldDef],
    type_mapper: &dyn Fn(&TypeRef) -> String,
    option_duration_on_defaults: bool,
    field_renames: Option<&HashMap<String, String>>,
) -> (String, String, String) {
    let mut sorted_fields: Vec<&FieldDef> = fields.iter().filter(|f| f.cfg.is_none()).collect();
    sorted_fields.sort_by_key(|f| f.optional as u8);

    let params: Vec<String> = sorted_fields
        .iter()
        .map(|f| {
            let ty = type_mapper(&f.ty);
            // All fields become Option<T>, but avoid Option<Option<T>> for already-optional fields.
            // When f.ty is TypeRef::Optional(X), type_mapper already returns "Option<X>".
            // Wrapping it again would yield Option<Option<X>>, making `None` ambiguous in PyO3
            // signatures (E0283: type annotations needed).
            if matches!(f.ty, TypeRef::Optional(_)) {
                format!("{}: {}", f.name, ty)
            } else {
                format!("{}: Option<{}>", f.name, ty)
            }
        })
        .collect();

    // All fields have None default in signature
    let defaults = sorted_fields
        .iter()
        .map(|f| format!("{}=None", f.name))
        .collect::<Vec<_>>()
        .join(", ");

    // Assignments use unwrap_or_else with the typed default.
    // `binding_name` is the struct field name (possibly renamed for keyword escaping),
    // `f.name` is the original name used as the constructor parameter.
    let assignments: Vec<String> = fields
        .iter()
        .filter(|f| f.cfg.is_none())
        .map(|f| {
            let binding_name = field_renames
                .and_then(|r| r.get(&f.name))
                .map_or_else(|| f.name.as_str(), |s| s.as_str());
            // Duration fields on has_default types are stored as Option<u64> when
            // option_duration_on_defaults is set — treat them as passthrough.
            if option_duration_on_defaults && matches!(f.ty, TypeRef::Duration) {
                return format!("{}: {}", binding_name, f.name);
            }
            if f.optional || matches!(&f.ty, TypeRef::Optional(_)) {
                // Optional fields: passthrough (both param and field are Option<T>)
                format!("{}: {}", binding_name, f.name)
            } else if let Some(ref typed_default) = f.typed_default {
                // For EnumVariant and Empty defaults, use unwrap_or_default()
                // because we can't generate qualified Rust paths here.
                match typed_default {
                    DefaultValue::EnumVariant(_) | DefaultValue::Empty => {
                        format!("{}: {}.unwrap_or_default()", binding_name, f.name)
                    }
                    _ => {
                        let default_val = format_default_value(typed_default);
                        // Use unwrap_or() for Copy literals (bool, int, float) to avoid
                        // clippy::unnecessary_lazy_evaluations; use unwrap_or_else for heap types.
                        match typed_default {
                            DefaultValue::BoolLiteral(_)
                            | DefaultValue::IntLiteral(_)
                            | DefaultValue::FloatLiteral(_) => {
                                format!("{}: {}.unwrap_or({})", binding_name, f.name, default_val)
                            }
                            _ => {
                                format!("{}: {}.unwrap_or_else(|| {})", binding_name, f.name, default_val)
                            }
                        }
                    }
                }
            } else {
                // All binding types should impl Default (enums default to first variant,
                // structs default via From<CoreType::default()>). unwrap_or_default() works.
                format!("{}: {}.unwrap_or_default()", binding_name, f.name)
            }
        })
        .collect();

    let single_line = params.join(", ");
    let param_list = if single_line.len() > 100 {
        format!("\n        {},\n    ", params.join(",\n        "))
    } else {
        single_line
    };

    (param_list, defaults, assignments.join(", "))
}