Skip to main content

alef_codegen/generators/
methods.rs

1use crate::generators::binding_helpers::{
2    gen_async_body, gen_call_args, gen_call_args_with_let_bindings, gen_lossy_binding_to_core_fields,
3    gen_serde_let_bindings, gen_unimplemented_body, has_named_params, is_simple_non_opaque_param, wrap_return,
4};
5use crate::generators::{AdapterBodies, AsyncPattern, RustBindingConfig};
6use crate::shared::{constructor_parts, function_params, function_sig_defaults, partition_methods};
7use crate::type_mapper::TypeMapper;
8use ahash::AHashSet;
9use alef_core::ir::{MethodDef, TypeDef, TypeRef};
10use std::fmt::Write;
11
12/// Returns true when `name` matches a known trait method that would trigger
13/// `clippy::should_implement_trait`.
14pub fn is_trait_method_name(name: &str) -> bool {
15    crate::generators::TRAIT_METHOD_NAMES.contains(&name)
16}
17
18/// Generate a constructor method.
19pub fn gen_constructor(typ: &TypeDef, mapper: &dyn TypeMapper, cfg: &RustBindingConfig) -> String {
20    let map_fn = |ty: &alef_core::ir::TypeRef| mapper.map_type(ty);
21
22    // For types with has_default, generate optional kwargs-style constructor
23    let (param_list, sig_defaults, assignments) = if typ.has_default {
24        crate::shared::config_constructor_parts(&typ.fields, &map_fn)
25    } else {
26        constructor_parts(&typ.fields, &map_fn)
27    };
28
29    let mut out = String::with_capacity(512);
30    // Per-item clippy suppression: too_many_arguments when >7 params
31    if typ.fields.len() > 7 {
32        writeln!(out, "    #[allow(clippy::too_many_arguments)]").ok();
33    }
34    writeln!(out, "    #[must_use]").ok();
35    if cfg.needs_signature {
36        writeln!(
37            out,
38            "    {}{}{}",
39            cfg.signature_prefix, sig_defaults, cfg.signature_suffix
40        )
41        .ok();
42    }
43    write!(
44        out,
45        "    {}\n    pub fn new({param_list}) -> Self {{\n        Self {{ {assignments} }}\n    }}",
46        cfg.constructor_attr
47    )
48    .ok();
49    out
50}
51
52/// Generate an instance method.
53///
54/// When `is_opaque` is true, generates delegation to `self.inner` via Arc clone
55/// instead of converting self to core type.
56///
57/// `opaque_types` is the set of opaque type names, used for correct return wrapping.
58pub fn gen_method(
59    method: &MethodDef,
60    mapper: &dyn TypeMapper,
61    cfg: &RustBindingConfig,
62    typ: &TypeDef,
63    is_opaque: bool,
64    opaque_types: &AHashSet<String>,
65    adapter_bodies: &AdapterBodies,
66) -> String {
67    let type_name = &typ.name;
68    // Use the full rust_path (with hyphens replaced by underscores) for core type references
69    let core_type_path = typ.rust_path.replace('-', "_");
70
71    let map_fn = |ty: &alef_core::ir::TypeRef| mapper.map_type(ty);
72    let params = function_params(&method.params, &map_fn);
73    let return_type = mapper.map_type(&method.return_type);
74    let ret = mapper.wrap_return(&return_type, method.error_type.is_some());
75
76    let call_args = gen_call_args(&method.params, opaque_types);
77
78    let is_owned_receiver = matches!(method.receiver.as_ref(), Some(alef_core::ir::ReceiverKind::Owned));
79
80    // Auto-delegate opaque methods: unwrap Arc for params, wrap Arc for returns.
81    // Owned receivers require the type to implement Clone (builder pattern).
82    // Async methods are allowed — gen_async_body handles them below.
83    let opaque_can_delegate = is_opaque
84        && !method.sanitized
85        && (!is_owned_receiver || typ.is_clone)
86        && method
87            .params
88            .iter()
89            .all(|p| !p.sanitized && crate::shared::is_opaque_delegatable_type(&p.ty))
90        && crate::shared::is_opaque_delegatable_type(&method.return_type);
91
92    // Build the core call expression: opaque types delegate to self.inner directly,
93    // non-opaque types convert self to core type first.
94    let make_core_call = |method_name: &str| -> String {
95        if is_opaque {
96            if is_owned_receiver {
97                // Owned receiver: clone out of Arc to get an owned value
98                format!("(*self.inner).clone().{method_name}({call_args})")
99            } else {
100                format!("self.inner.{method_name}({call_args})")
101            }
102        } else {
103            format!("{core_type_path}::from(self.clone()).{method_name}({call_args})")
104        }
105    };
106
107    // For async opaque methods, we clone the Arc before moving into the future.
108    let make_async_core_call = |method_name: &str| -> String {
109        if is_opaque {
110            format!("inner.{method_name}({call_args})")
111        } else {
112            format!("{core_type_path}::from(self.clone()).{method_name}({call_args})")
113        }
114    };
115
116    // Generate the body: convert self to core type, call method, convert result back
117    //
118    // For opaque types, wrap the return value appropriately:
119    //   - Named(self) → Self { inner: Arc::new(result) }
120    //   - Named(other) → OtherType::from(result)
121    //   - primitives/String/Vec/Unit → pass through
122    let async_result_wrap = if is_opaque {
123        wrap_return(
124            "result",
125            &method.return_type,
126            type_name,
127            opaque_types,
128            is_opaque,
129            method.returns_ref,
130        )
131    } else {
132        // For non-opaque types, only use From conversion if the return type is simple
133        // enough. Named return types may not have a From impl.
134        match &method.return_type {
135            TypeRef::Named(_) | TypeRef::Json => "result.into()".to_string(),
136            _ => "result".to_string(),
137        }
138    };
139
140    let body = if !opaque_can_delegate {
141        // Check if an adapter provides the body
142        let adapter_key = format!("{}.{}", type_name, method.name);
143        if let Some(adapter_body) = adapter_bodies.get(&adapter_key) {
144            adapter_body.clone()
145        } else if cfg.has_serde
146            && is_opaque
147            && !method.sanitized
148            && has_named_params(&method.params, opaque_types)
149            && method.error_type.is_some()
150            && crate::shared::is_opaque_delegatable_type(&method.return_type)
151        {
152            // Serde-based param conversion for opaque methods with non-opaque Named params.
153            let err_conv = match cfg.async_pattern {
154                AsyncPattern::Pyo3FutureIntoPy => {
155                    ".map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))"
156                }
157                AsyncPattern::NapiNativeAsync => {
158                    ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))"
159                }
160                AsyncPattern::WasmNativeAsync => ".map_err(|e| JsValue::from_str(&e.to_string()))",
161                _ => ".map_err(|e| e.to_string())",
162            };
163            let serde_bindings =
164                gen_serde_let_bindings(&method.params, opaque_types, cfg.core_import, err_conv, "        ");
165            let serde_call_args = gen_call_args_with_let_bindings(&method.params, opaque_types);
166            let core_call = format!("self.inner.{}({serde_call_args})", method.name);
167            if matches!(method.return_type, TypeRef::Unit) {
168                format!("{serde_bindings}{core_call}{err_conv}?;\n        Ok(())")
169            } else {
170                let wrap = wrap_return(
171                    "result",
172                    &method.return_type,
173                    type_name,
174                    opaque_types,
175                    is_opaque,
176                    method.returns_ref,
177                );
178                format!("{serde_bindings}let result = {core_call}{err_conv}?;\n        Ok({wrap})")
179            }
180        } else if !is_opaque
181            && !method.sanitized
182            && method
183                .params
184                .iter()
185                .all(|p| !p.sanitized && is_simple_non_opaque_param(&p.ty))
186            && crate::shared::is_delegatable_return(&method.return_type)
187        {
188            // Non-opaque delegation: construct core type field-by-field, call method, convert back.
189            // Sanitized fields use Default::default() (lossy but functional for builder pattern).
190            let field_conversions = gen_lossy_binding_to_core_fields(typ, cfg.core_import);
191            let core_call = format!("core_self.{}({call_args})", method.name);
192            let result_wrap = match &method.return_type {
193                TypeRef::Named(n) if n == type_name => ".into()".to_string(),
194                TypeRef::Named(_) => ".into()".to_string(),
195                TypeRef::String | TypeRef::Bytes | TypeRef::Path => ".into()".to_string(),
196                _ => String::new(),
197            };
198            if method.error_type.is_some() {
199                let err_conv = match cfg.async_pattern {
200                    AsyncPattern::Pyo3FutureIntoPy => {
201                        ".map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))"
202                    }
203                    AsyncPattern::NapiNativeAsync => {
204                        ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))"
205                    }
206                    AsyncPattern::WasmNativeAsync => ".map_err(|e| JsValue::from_str(&e.to_string()))",
207                    _ => ".map_err(|e| e.to_string())",
208                };
209                format!("{field_conversions}let result = {core_call}{err_conv}?;\n        Ok(result{result_wrap})")
210            } else {
211                format!("{field_conversions}{core_call}{result_wrap}")
212            }
213        } else {
214            gen_unimplemented_body(
215                &method.return_type,
216                &format!("{type_name}.{}", method.name),
217                method.error_type.is_some(),
218                cfg,
219                &method.params,
220            )
221        }
222    } else if method.is_async {
223        let inner_clone_line = if is_opaque {
224            "let inner = self.inner.clone();\n        "
225        } else {
226            ""
227        };
228        let core_call_str = make_async_core_call(&method.name);
229        gen_async_body(
230            &core_call_str,
231            cfg,
232            method.error_type.is_some(),
233            &async_result_wrap,
234            is_opaque,
235            inner_clone_line,
236            matches!(method.return_type, TypeRef::Unit),
237        )
238    } else {
239        let core_call = make_core_call(&method.name);
240        if method.error_type.is_some() {
241            // Backend-specific error conversion
242            let err_conv = match cfg.async_pattern {
243                AsyncPattern::Pyo3FutureIntoPy => {
244                    ".map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))"
245                }
246                AsyncPattern::NapiNativeAsync => {
247                    ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))"
248                }
249                AsyncPattern::WasmNativeAsync => ".map_err(|e| JsValue::from_str(&e.to_string()))",
250                _ => ".map_err(|e| e.to_string())",
251            };
252            if is_opaque {
253                if matches!(method.return_type, TypeRef::Unit) {
254                    // Unit return: avoid let_unit_value by not binding the result
255                    format!("{core_call}{err_conv}?;\n        Ok(())")
256                } else {
257                    let wrap = wrap_return(
258                        "result",
259                        &method.return_type,
260                        type_name,
261                        opaque_types,
262                        is_opaque,
263                        method.returns_ref,
264                    );
265                    format!("let result = {core_call}{err_conv}?;\n        Ok({wrap})")
266                }
267            } else {
268                format!("{core_call}{err_conv}")
269            }
270        } else if is_opaque {
271            wrap_return(
272                &core_call,
273                &method.return_type,
274                type_name,
275                opaque_types,
276                is_opaque,
277                method.returns_ref,
278            )
279        } else {
280            core_call
281        }
282    };
283
284    let needs_py = method.is_async && cfg.async_pattern == AsyncPattern::Pyo3FutureIntoPy;
285    let self_param = match (needs_py, params.is_empty()) {
286        (true, true) => "&self, py: Python<'py>",
287        (true, false) => "&self, py: Python<'py>, ",
288        (false, true) => "&self",
289        (false, false) => "&self, ",
290    };
291
292    // For async PyO3 methods, override return type to PyResult<Bound<'py, PyAny>>
293    // and add the 'py lifetime generic on the method name.
294    let ret = if needs_py {
295        "PyResult<Bound<'py, PyAny>>".to_string()
296    } else {
297        ret
298    };
299    let method_lifetime = if needs_py { "<'py>" } else { "" };
300
301    // Wrap long signature if necessary
302    let (sig_start, sig_params, sig_end) = if self_param.len() + params.len() > 100 {
303        let wrapped_params = method
304            .params
305            .iter()
306            .map(|p| {
307                let ty = if p.optional {
308                    format!("Option<{}>", mapper.map_type(&p.ty))
309                } else {
310                    mapper.map_type(&p.ty)
311                };
312                format!("{}: {}", p.name, ty)
313            })
314            .collect::<Vec<_>>()
315            .join(",\n        ");
316        let py_param = if needs_py { "\n        py: Python<'py>," } else { "" };
317        (
318            format!(
319                "pub fn {}{method_lifetime}(\n        &self,{}\n        ",
320                method.name, py_param
321            ),
322            wrapped_params,
323            "\n    ) -> ".to_string(),
324        )
325    } else {
326        (
327            format!("pub fn {}{method_lifetime}({}", method.name, self_param),
328            params,
329            ") -> ".to_string(),
330        )
331    };
332
333    let mut out = String::with_capacity(1024);
334    // Per-item clippy suppression: too_many_arguments when >7 params (including &self and py)
335    let total_params = method.params.len() + 1 + if needs_py { 1 } else { 0 };
336    if total_params > 7 {
337        writeln!(out, "    #[allow(clippy::too_many_arguments)]").ok();
338    }
339    // Per-item clippy suppression: missing_errors_doc for Result-returning methods
340    if method.error_type.is_some() {
341        writeln!(out, "    #[allow(clippy::missing_errors_doc)]").ok();
342    }
343    // Per-item clippy suppression: should_implement_trait for trait-conflicting names
344    if is_trait_method_name(&method.name) {
345        writeln!(out, "    #[allow(clippy::should_implement_trait)]").ok();
346    }
347    if cfg.needs_signature {
348        let sig = function_sig_defaults(&method.params);
349        writeln!(out, "    {}{}{}", cfg.signature_prefix, sig, cfg.signature_suffix).ok();
350    }
351    write!(
352        out,
353        "    {}{}{}{} {{\n        \
354         {body}\n    }}",
355        sig_start, sig_params, sig_end, ret,
356    )
357    .ok();
358    out
359}
360
361/// Generate a static method.
362pub fn gen_static_method(
363    method: &MethodDef,
364    mapper: &dyn TypeMapper,
365    cfg: &RustBindingConfig,
366    typ: &TypeDef,
367    adapter_bodies: &AdapterBodies,
368    opaque_types: &AHashSet<String>,
369) -> String {
370    let type_name = &typ.name;
371    // Use the full rust_path (with hyphens replaced by underscores) for core type references
372    let core_type_path = typ.rust_path.replace('-', "_");
373    let map_fn = |ty: &alef_core::ir::TypeRef| mapper.map_type(ty);
374    let params = function_params(&method.params, &map_fn);
375    let return_type = mapper.map_type(&method.return_type);
376    let ret = mapper.wrap_return(&return_type, method.error_type.is_some());
377
378    let call_args = gen_call_args(&method.params, opaque_types);
379
380    let can_delegate = crate::shared::can_auto_delegate(method, opaque_types);
381
382    let body = if !can_delegate {
383        // Check if an adapter provides the body
384        let adapter_key = format!("{}.{}", type_name, method.name);
385        if let Some(adapter_body) = adapter_bodies.get(&adapter_key) {
386            adapter_body.clone()
387        } else {
388            gen_unimplemented_body(
389                &method.return_type,
390                &format!("{type_name}::{}", method.name),
391                method.error_type.is_some(),
392                cfg,
393                &method.params,
394            )
395        }
396    } else if method.is_async {
397        let core_call = format!("{core_type_path}::{}({call_args})", method.name);
398        let return_wrap = format!("{return_type}::from(result)");
399        gen_async_body(
400            &core_call,
401            cfg,
402            method.error_type.is_some(),
403            &return_wrap,
404            false,
405            "",
406            matches!(method.return_type, TypeRef::Unit),
407        )
408    } else {
409        let core_call = format!("{core_type_path}::{}({call_args})", method.name);
410        if method.error_type.is_some() {
411            // Backend-specific error conversion
412            let err_conv = match cfg.async_pattern {
413                AsyncPattern::Pyo3FutureIntoPy => {
414                    ".map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))"
415                }
416                AsyncPattern::NapiNativeAsync => {
417                    ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))"
418                }
419                AsyncPattern::WasmNativeAsync => ".map_err(|e| JsValue::from_str(&e.to_string()))",
420                _ => ".map_err(|e| e.to_string())",
421            };
422            // Wrap the Ok value if the return type needs conversion (e.g. PathBuf→String)
423            let wrapped = wrap_return(
424                "val",
425                &method.return_type,
426                type_name,
427                opaque_types,
428                typ.is_opaque,
429                method.returns_ref,
430            );
431            if wrapped == "val" {
432                format!("{core_call}{err_conv}")
433            } else {
434                format!("{core_call}.map(|val| {wrapped}){err_conv}")
435            }
436        } else {
437            // Wrap return value for non-error case too (e.g. PathBuf→String)
438            wrap_return(
439                &core_call,
440                &method.return_type,
441                type_name,
442                opaque_types,
443                typ.is_opaque,
444                method.returns_ref,
445            )
446        }
447    };
448
449    let static_needs_py = method.is_async && cfg.async_pattern == AsyncPattern::Pyo3FutureIntoPy;
450
451    // For async PyO3 static methods, override return type and add lifetime generic.
452    let ret = if static_needs_py {
453        "PyResult<Bound<'py, PyAny>>".to_string()
454    } else {
455        ret
456    };
457    let method_lifetime = if static_needs_py { "<'py>" } else { "" };
458
459    // Wrap long signature if necessary
460    let (sig_start, sig_params, sig_end) = if params.len() > 100 {
461        let wrapped_params = method
462            .params
463            .iter()
464            .map(|p| {
465                let ty = if p.optional {
466                    format!("Option<{}>", mapper.map_type(&p.ty))
467                } else {
468                    mapper.map_type(&p.ty)
469                };
470                format!("{}: {}", p.name, ty)
471            })
472            .collect::<Vec<_>>()
473            .join(",\n        ");
474        // For async PyO3, add py parameter
475        if static_needs_py {
476            (
477                format!("pub fn {}{method_lifetime}(py: Python<'py>,\n        ", method.name),
478                wrapped_params,
479                "\n    ) -> ".to_string(),
480            )
481        } else {
482            (
483                format!("pub fn {}(\n        ", method.name),
484                wrapped_params,
485                "\n    ) -> ".to_string(),
486            )
487        }
488    } else if static_needs_py {
489        (
490            format!("pub fn {}{method_lifetime}(py: Python<'py>, ", method.name),
491            params,
492            ") -> ".to_string(),
493        )
494    } else {
495        (format!("pub fn {}(", method.name), params, ") -> ".to_string())
496    };
497
498    let mut out = String::with_capacity(1024);
499    // Per-item clippy suppression: too_many_arguments when >7 params (including py)
500    let total_params = method.params.len() + if static_needs_py { 1 } else { 0 };
501    if total_params > 7 {
502        writeln!(out, "    #[allow(clippy::too_many_arguments)]").ok();
503    }
504    // Per-item clippy suppression: missing_errors_doc for Result-returning methods
505    if method.error_type.is_some() {
506        writeln!(out, "    #[allow(clippy::missing_errors_doc)]").ok();
507    }
508    // Per-item clippy suppression: should_implement_trait for trait-conflicting names
509    if is_trait_method_name(&method.name) {
510        writeln!(out, "    #[allow(clippy::should_implement_trait)]").ok();
511    }
512    if let Some(attr) = cfg.static_attr {
513        writeln!(out, "    #[{attr}]").ok();
514    }
515    if cfg.needs_signature {
516        let sig = function_sig_defaults(&method.params);
517        writeln!(out, "    {}{}{}", cfg.signature_prefix, sig, cfg.signature_suffix).ok();
518    }
519    write!(
520        out,
521        "    {}{}{}{} {{\n        \
522         {body}\n    }}",
523        sig_start, sig_params, sig_end, ret,
524    )
525    .ok();
526    out
527}
528
529/// Generate a full methods impl block (non-opaque types).
530pub fn gen_impl_block(
531    typ: &TypeDef,
532    mapper: &dyn TypeMapper,
533    cfg: &RustBindingConfig,
534    adapter_bodies: &AdapterBodies,
535    opaque_types: &AHashSet<String>,
536) -> String {
537    let (instance, statics) = partition_methods(&typ.methods);
538    if instance.is_empty() && statics.is_empty() && typ.fields.is_empty() {
539        return String::new();
540    }
541
542    let prefixed_name = format!("{}{}", cfg.type_name_prefix, typ.name);
543    let mut out = String::with_capacity(2048);
544    if let Some(block_attr) = cfg.method_block_attr {
545        writeln!(out, "#[{block_attr}]").ok();
546    }
547    writeln!(out, "impl {prefixed_name} {{").ok();
548
549    // Constructor
550    if !typ.fields.is_empty() {
551        out.push_str(&gen_constructor(typ, mapper, cfg));
552        out.push_str("\n\n");
553    }
554
555    // Instance methods
556    for m in &instance {
557        out.push_str(&gen_method(m, mapper, cfg, typ, false, opaque_types, adapter_bodies));
558        out.push_str("\n\n");
559    }
560
561    // Static methods
562    for m in &statics {
563        out.push_str(&gen_static_method(m, mapper, cfg, typ, adapter_bodies, opaque_types));
564        out.push_str("\n\n");
565    }
566
567    // Trim trailing newlines inside impl block
568    let trimmed = out.trim_end();
569    let mut result = trimmed.to_string();
570    result.push_str("\n}");
571    result
572}
573
574/// Generate a full impl block for an opaque type, delegating methods to `self.inner`.
575///
576/// `opaque_types` is the set of type names that are opaque wrappers (use `Arc<inner>`).
577/// This is needed so that return-type wrapping uses the correct pattern for cross-type returns.
578pub fn gen_opaque_impl_block(
579    typ: &TypeDef,
580    mapper: &dyn TypeMapper,
581    cfg: &RustBindingConfig,
582    opaque_types: &AHashSet<String>,
583    adapter_bodies: &AdapterBodies,
584) -> String {
585    let (instance, statics) = partition_methods(&typ.methods);
586    if instance.is_empty() && statics.is_empty() {
587        return String::new();
588    }
589
590    let mut out = String::with_capacity(2048);
591    let prefixed_name = format!("{}{}", cfg.type_name_prefix, typ.name);
592    if let Some(block_attr) = cfg.method_block_attr {
593        writeln!(out, "#[{block_attr}]").ok();
594    }
595    writeln!(out, "impl {prefixed_name} {{").ok();
596
597    // Instance methods — delegate to self.inner
598    for m in &instance {
599        out.push_str(&gen_method(m, mapper, cfg, typ, true, opaque_types, adapter_bodies));
600        out.push_str("\n\n");
601    }
602
603    // Static methods
604    for m in &statics {
605        out.push_str(&gen_static_method(m, mapper, cfg, typ, adapter_bodies, opaque_types));
606        out.push_str("\n\n");
607    }
608
609    let trimmed = out.trim_end();
610    let mut result = trimmed.to_string();
611    result.push_str("\n}");
612    result
613}