Skip to main content

alef_codegen/generators/
methods.rs

1use crate::generators::binding_helpers::{
2    apply_return_newtype_unwrap, gen_async_body, gen_call_args, gen_call_args_cfg, gen_call_args_with_let_bindings,
3    gen_lossy_binding_to_core_fields, gen_lossy_binding_to_core_fields_mut, gen_named_let_bindings_pub,
4    gen_serde_let_bindings, gen_unimplemented_body, has_named_params, is_simple_non_opaque_param,
5    wrap_return_with_mutex,
6};
7use crate::generators::{AdapterBodies, AsyncPattern, RustBindingConfig};
8use crate::shared::{function_params, function_sig_defaults, partition_methods};
9use crate::type_mapper::TypeMapper;
10use ahash::AHashSet;
11use alef_core::ir::{MethodDef, TypeDef, TypeRef};
12
13/// Returns true when `name` matches a known trait method that would trigger
14/// `clippy::should_implement_trait`.
15pub fn is_trait_method_name(name: &str) -> bool {
16    crate::generators::TRAIT_METHOD_NAMES.contains(&name)
17}
18
19/// Generate a constructor method.
20pub fn gen_constructor(typ: &TypeDef, mapper: &dyn TypeMapper, cfg: &RustBindingConfig) -> String {
21    gen_constructor_with_renames(typ, mapper, cfg, None)
22}
23
24/// Like `gen_constructor` but with field renames for keyword escaping.
25pub fn gen_constructor_with_renames(
26    typ: &TypeDef,
27    mapper: &dyn TypeMapper,
28    cfg: &RustBindingConfig,
29    field_renames: Option<&std::collections::HashMap<String, String>>,
30) -> String {
31    let map_fn = |ty: &alef_core::ir::TypeRef| mapper.map_type(ty);
32
33    // For types with has_default, generate optional kwargs-style constructor
34    let (param_list, sig_defaults, assignments) = if typ.has_default {
35        crate::shared::config_constructor_parts_with_renames(
36            &typ.fields,
37            &map_fn,
38            cfg.option_duration_on_defaults,
39            field_renames,
40        )
41    } else {
42        crate::shared::constructor_parts_with_renames(&typ.fields, &map_fn, field_renames)
43    };
44
45    crate::template_env::render(
46        "generators/methods/constructor.jinja",
47        minijinja::context! {
48            has_too_many_args => typ.fields.len() > 7,
49            needs_signature => cfg.needs_signature,
50            signature_prefix => cfg.signature_prefix,
51            sig_defaults => sig_defaults,
52            signature_suffix => cfg.signature_suffix,
53            constructor_attr => cfg.constructor_attr,
54            param_list => param_list,
55            assignments => assignments,
56        },
57    )
58}
59
60/// Generate an instance method.
61///
62/// When `is_opaque` is true, generates delegation to `self.inner` via Arc clone
63/// instead of converting self to core type.
64///
65/// `opaque_types` is the set of opaque type names, used for correct return wrapping.
66/// `mutex_types` is the subset of opaque types whose `inner` field is `Arc<Mutex<T>>`;
67/// method dispatch uses `.lock().unwrap()` for these types.
68#[allow(clippy::too_many_arguments)]
69pub fn gen_method(
70    method: &MethodDef,
71    mapper: &dyn TypeMapper,
72    cfg: &RustBindingConfig,
73    typ: &TypeDef,
74    is_opaque: bool,
75    opaque_types: &AHashSet<String>,
76    mutex_types: &AHashSet<String>,
77    adapter_bodies: &AdapterBodies,
78) -> String {
79    let type_name = &typ.name;
80    // Use the full rust_path (with hyphens replaced by underscores) for core type references
81    let core_type_path = typ.rust_path.replace('-', "_");
82
83    let map_fn = |ty: &alef_core::ir::TypeRef| mapper.map_type(ty);
84    let params = function_params(&method.params, &map_fn);
85    let return_type = mapper.map_type(&method.return_type);
86    let ret = mapper.wrap_return(&return_type, method.error_type.is_some());
87
88    let core_import = cfg.core_import;
89
90    // When non-opaque Named params have is_ref=true, or Vec<String> params have is_ref=true,
91    // we need let bindings so the converted/intermediate value outlives the borrow.
92    // Use has_named_params which covers both Named types and Vec<String> with is_ref=true.
93    let has_ref_named_params = has_named_params(&method.params, opaque_types);
94    let (call_args, ref_let_bindings) = if has_ref_named_params {
95        (
96            gen_call_args_with_let_bindings(&method.params, opaque_types),
97            gen_named_let_bindings_pub(&method.params, opaque_types, core_import),
98        )
99    } else if cfg.cast_uints_to_i32 || cfg.cast_large_ints_to_f64 {
100        // Use cast-aware call args for backends that remap numeric types (e.g. extendr).
101        (
102            gen_call_args_cfg(
103                &method.params,
104                opaque_types,
105                cfg.cast_uints_to_i32,
106                cfg.cast_large_ints_to_f64,
107            ),
108            String::new(),
109        )
110    } else {
111        (gen_call_args(&method.params, opaque_types), String::new())
112    };
113
114    let is_owned_receiver = matches!(method.receiver.as_ref(), Some(alef_core::ir::ReceiverKind::Owned));
115    let is_ref_mut_receiver = matches!(method.receiver.as_ref(), Some(alef_core::ir::ReceiverKind::RefMut));
116
117    // Detect non-opaque RefMut methods that can use the functional clone-mutate-return pattern.
118    // These cannot use &mut self in frozen PyO3 classes or immutable WASM structs, so instead
119    // we generate: clone self to core, apply mutation, convert back to Self.
120    // Conditions: non-opaque, RefMut receiver, no trait source (trait methods need special handling),
121    // all params delegatable (Named types are allowed — gen_call_args handles them via .into()),
122    // and not sanitized.
123    let is_functional_ref_mut = !is_opaque
124        && is_ref_mut_receiver
125        && !method.sanitized
126        && method.trait_source.is_none()
127        && method
128            .params
129            .iter()
130            .all(|p| !p.sanitized && crate::shared::is_delegatable_param(&p.ty, opaque_types));
131
132    // Methods from trait impls can't be called on Arc<dyn Trait> through deref.
133    // Skip these unless there's an adapter body that can handle them.
134    let is_trait_method = method.trait_source.is_some();
135
136    // Whether this opaque type uses Arc<Mutex<T>> for interior mutability.
137    let self_needs_mutex = is_opaque && mutex_types.contains(type_name.as_str());
138
139    // Auto-delegate opaque methods: unwrap Arc for params, wrap Arc for returns.
140    // Owned receivers require the type to implement Clone (builder pattern).
141    // RefMut receivers normally can't be delegated on Arc<T>, but Arc<Mutex<T>> allows
142    // &mut T via .lock().unwrap(), so mutex types CAN delegate RefMut methods.
143    // Trait methods can't be delegated on opaque types (Arc deref doesn't expose trait methods).
144    // Async methods are allowed — gen_async_body handles them below.
145    let opaque_can_delegate = is_opaque
146        && !method.sanitized
147        && (!is_ref_mut_receiver || self_needs_mutex)
148        && !is_trait_method
149        && (!is_owned_receiver || typ.is_clone)
150        && method
151            .params
152            .iter()
153            .all(|p| !p.sanitized && crate::shared::is_opaque_delegatable_type(&p.ty))
154        && crate::shared::is_opaque_delegatable_type(&method.return_type);
155
156    // Build the core call expression: opaque types delegate to self.inner directly,
157    // non-opaque types convert self to core type first.
158    // For mutex types, acquire the lock before calling the method.
159    let make_core_call = |method_name: &str| -> String {
160        if is_opaque {
161            if is_owned_receiver {
162                // Owned receiver: clone out of Arc/Mutex to get an owned value.
163                // For Mutex types, lock first then clone the inner value.
164                if self_needs_mutex {
165                    format!("self.inner.lock().unwrap().clone().{method_name}({call_args})")
166                } else {
167                    format!("(*self.inner).clone().{method_name}({call_args})")
168                }
169            } else if self_needs_mutex {
170                // Mutex type: lock to get &mut T (works for both &self and &mut self methods).
171                format!("self.inner.lock().unwrap().{method_name}({call_args})")
172            } else {
173                format!("self.inner.{method_name}({call_args})")
174            }
175        } else {
176            format!("{core_type_path}::from(self.clone()).{method_name}({call_args})")
177        }
178    };
179
180    // For async opaque methods, we clone the Arc before moving into the future.
181    // For mutex types, the cloned Arc<Mutex<T>> is locked inside the async block.
182    let make_async_core_call = |method_name: &str| -> String {
183        if is_opaque {
184            if self_needs_mutex {
185                format!("inner.lock().unwrap().{method_name}({call_args})")
186            } else {
187                format!("inner.{method_name}({call_args})")
188            }
189        } else {
190            format!("{core_type_path}::from(self.clone()).{method_name}({call_args})")
191        }
192    };
193
194    // Generate the body: convert self to core type, call method, convert result back
195    //
196    // For opaque types, wrap the return value appropriately:
197    //   - Named(self) → Self { inner: Arc::new(result) }
198    //   - Named(other) → OtherType::from(result)
199    //   - primitives/String/Vec/Unit → pass through
200    let result_expr = apply_return_newtype_unwrap("result", &method.return_newtype_wrapper);
201    let async_result_wrap = if is_opaque {
202        wrap_return_with_mutex(
203            &result_expr,
204            &method.return_type,
205            type_name,
206            opaque_types,
207            mutex_types,
208            is_opaque,
209            method.returns_ref,
210            method.returns_cow,
211        )
212    } else {
213        // For non-opaque types, only use From conversion if the return type is simple
214        // enough. Named return types may not have a From impl.
215        match &method.return_type {
216            TypeRef::Named(_) | TypeRef::Json => format!("{result_expr}.into()"),
217            _ => result_expr.clone(),
218        }
219    };
220
221    let body = if !opaque_can_delegate {
222        // Check if an adapter provides the body
223        let adapter_key_inner = format!("{}.{}", type_name, method.name);
224        if let Some(adapter_body) = adapter_bodies.get(&adapter_key_inner) {
225            adapter_body.clone()
226        } else if cfg.has_serde
227            && is_opaque
228            && !method.sanitized
229            && !is_trait_method
230            && has_named_params(&method.params, opaque_types)
231            && method.error_type.is_some()
232            && crate::shared::is_opaque_delegatable_type(&method.return_type)
233        {
234            // Serde-based param conversion for opaque methods with non-opaque Named params.
235            // NOTE: Only executed when has_serde=true, ensuring serde_json calls are gated.
236            let err_conv = match cfg.async_pattern {
237                AsyncPattern::Pyo3FutureIntoPy => {
238                    ".map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))"
239                }
240                AsyncPattern::NapiNativeAsync => {
241                    ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))"
242                }
243                AsyncPattern::WasmNativeAsync => ".map_err(|e| JsValue::from_str(&e.to_string()))",
244                AsyncPattern::TokioBlockOn => {
245                    ".map_err(|e| extendr_api::Error::Other(e.to_string().replace(\":\", \"_\").replace(\"/\", \"_\").replace(\"-\", \"_\").chars().take(255).collect::<String>()))"
246                }
247                _ => ".map_err(|e| e.to_string())",
248            };
249            let serde_bindings =
250                gen_serde_let_bindings(&method.params, opaque_types, cfg.core_import, err_conv, "        ");
251            let serde_call_args = gen_call_args_with_let_bindings(&method.params, opaque_types);
252            let core_call = if self_needs_mutex {
253                format!("self.inner.lock().unwrap().{}({serde_call_args})", method.name)
254            } else {
255                format!("self.inner.{}({serde_call_args})", method.name)
256            };
257            if matches!(method.return_type, TypeRef::Unit) {
258                format!("{serde_bindings}{core_call}{err_conv}?;\n        Ok(())")
259            } else {
260                let wrap = wrap_return_with_mutex(
261                    "result",
262                    &method.return_type,
263                    type_name,
264                    opaque_types,
265                    mutex_types,
266                    is_opaque,
267                    method.returns_ref,
268                    method.returns_cow,
269                );
270                format!("{serde_bindings}let result = {core_call}{err_conv}?;\n        Ok({wrap})")
271            }
272        } else if is_functional_ref_mut {
273            // Functional clone-mutate-return pattern for non-opaque RefMut methods.
274            // PyO3 frozen classes and WASM structs don't support &mut self, so instead:
275            //   1. Convert binding self to a mutable core type.
276            //   2. Call the mutating core method (which changes core_self in place).
277            //   3. Convert the mutated core type back to the binding type and return Self.
278            //
279            // The generated signature uses &self -> Self (or -> Result<Self, E> if fallible),
280            // making the method work correctly with immutable binding wrappers.
281            let field_conversions = gen_lossy_binding_to_core_fields_mut(
282                typ,
283                cfg.core_import,
284                cfg.option_duration_on_defaults,
285                opaque_types,
286                cfg.cast_uints_to_i32,
287                cfg.cast_large_ints_to_f64,
288                cfg.lossy_skip_types,
289            );
290            let core_call = format!("core_self.{}({call_args})", method.name);
291            if method.error_type.is_some() {
292                let err_conv = match cfg.async_pattern {
293                    AsyncPattern::Pyo3FutureIntoPy => {
294                        ".map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))"
295                    }
296                    AsyncPattern::NapiNativeAsync => {
297                        ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))"
298                    }
299                    AsyncPattern::WasmNativeAsync => ".map_err(|e| JsValue::from_str(&e.to_string()))",
300                    AsyncPattern::TokioBlockOn => {
301                        ".map_err(|e| extendr_api::Error::Other(e.to_string().replace(\":\", \"_\").replace(\"/\", \"_\").replace(\"-\", \"_\").chars().take(255).collect::<String>()))"
302                    }
303                    _ => ".map_err(|e| e.to_string())",
304                };
305                format!("{field_conversions}{core_call}{err_conv}?;\n        Ok(core_self.into())")
306            } else {
307                format!("{field_conversions}{core_call};\n        core_self.into()")
308            }
309        } else if !is_opaque
310            && !method.sanitized
311            && method
312                .params
313                .iter()
314                .all(|p| !p.sanitized && is_simple_non_opaque_param(&p.ty))
315            && crate::shared::is_delegatable_return(&method.return_type)
316        {
317            // Non-opaque delegation: construct core type field-by-field, call method, convert back.
318            // Sanitized fields use Default::default() (lossy but functional for builder pattern).
319            let is_ref_mut = matches!(method.receiver.as_ref(), Some(alef_core::ir::ReceiverKind::RefMut));
320            let field_conversions = if is_ref_mut {
321                gen_lossy_binding_to_core_fields_mut(
322                    typ,
323                    cfg.core_import,
324                    cfg.option_duration_on_defaults,
325                    opaque_types,
326                    cfg.cast_uints_to_i32,
327                    cfg.cast_large_ints_to_f64,
328                    cfg.lossy_skip_types,
329                )
330            } else {
331                gen_lossy_binding_to_core_fields(
332                    typ,
333                    cfg.core_import,
334                    cfg.option_duration_on_defaults,
335                    opaque_types,
336                    cfg.cast_uints_to_i32,
337                    cfg.cast_large_ints_to_f64,
338                    cfg.lossy_skip_types,
339                )
340            };
341            let core_call = format!("core_self.{}({call_args})", method.name);
342            let newtype_suffix = if method.return_newtype_wrapper.is_some() {
343                ".0"
344            } else {
345                ""
346            };
347            let result_wrap = match &method.return_type {
348                // When returns_cow=true the core returns Cow<'_, T>: call .into_owned() to
349                // obtain an owned T before the binding→core From conversion.
350                // When returns_ref=true (or &T / Cow<'_, T> via the old flag), same treatment.
351                TypeRef::Named(n) if n == type_name && (method.returns_cow || method.returns_ref) => {
352                    ".into_owned().into()".to_string()
353                }
354                TypeRef::Named(_) if method.returns_cow || method.returns_ref => ".into_owned().into()".to_string(),
355                TypeRef::Named(n) if n == type_name => ".into()".to_string(),
356                TypeRef::Named(_) => ".into()".to_string(),
357                TypeRef::String => {
358                    if method.returns_ref {
359                        ".to_owned()".to_string()
360                    } else {
361                        String::new()
362                    }
363                }
364                TypeRef::Path => {
365                    if method.returns_ref {
366                        ".to_owned()".to_string()
367                    } else {
368                        ".to_string_lossy().to_string()".to_string()
369                    }
370                }
371                // Bytes: binding uses Vec<u8>. Always use .to_vec() which works for both
372                // &Bytes and owned Bytes (avoids &Bytes→Vec<u8> From trait issues).
373                TypeRef::Bytes => ".to_vec()".to_string(),
374                // Optional<Named>: when core returns Option<&T>, need .map(|v| v.clone().into())
375                TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Named(_)) => {
376                    if method.returns_ref {
377                        ".map(|v| v.clone().into())".to_string()
378                    } else {
379                        ".map(Into::into)".to_string()
380                    }
381                }
382                // Optional<String>: when core returns Option<&str>, need .map(|v| v.to_owned())
383                TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::String | TypeRef::Bytes) => {
384                    if method.returns_ref {
385                        ".map(|v| v.to_owned())".to_string()
386                    } else {
387                        String::new()
388                    }
389                }
390                // Primitive return: cast when the binding uses a different numeric type.
391                // R maps u8/u16/u32/i8/i16 → i32 and u64/i64/usize/isize/f32 → f64.
392                TypeRef::Primitive(p) => {
393                    use crate::conversions::helpers::{needs_f64_cast, needs_i32_cast};
394                    if cfg.cast_uints_to_i32 && needs_i32_cast(p) {
395                        " as i32".to_string()
396                    } else if cfg.cast_large_ints_to_f64 && needs_f64_cast(p) {
397                        " as f64".to_string()
398                    } else {
399                        String::new()
400                    }
401                }
402                // Optional<Primitive>: cast inside map when the binding uses a different type.
403                TypeRef::Optional(inner) => {
404                    if let TypeRef::Primitive(p) = inner.as_ref() {
405                        use crate::conversions::helpers::{needs_f64_cast, needs_i32_cast};
406                        if cfg.cast_uints_to_i32 && needs_i32_cast(p) {
407                            ".map(|v| v as i32)".to_string()
408                        } else if cfg.cast_large_ints_to_f64 && needs_f64_cast(p) {
409                            ".map(|v| v as f64)".to_string()
410                        } else {
411                            String::new()
412                        }
413                    } else {
414                        String::new()
415                    }
416                }
417                _ => String::new(),
418            };
419            if method.error_type.is_some() {
420                let err_conv = match cfg.async_pattern {
421                    AsyncPattern::Pyo3FutureIntoPy => {
422                        ".map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))"
423                    }
424                    AsyncPattern::NapiNativeAsync => {
425                        ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))"
426                    }
427                    AsyncPattern::WasmNativeAsync => ".map_err(|e| JsValue::from_str(&e.to_string()))",
428                    _ => ".map_err(|e| e.to_string())",
429                };
430                format!(
431                    "{field_conversions}let result = {core_call}{err_conv}?;\n        Ok(result{newtype_suffix}{result_wrap})"
432                )
433            } else {
434                format!("{field_conversions}{core_call}{newtype_suffix}{result_wrap}")
435            }
436        } else if is_opaque
437            && !method.sanitized
438            && (!is_ref_mut_receiver || self_needs_mutex)
439            && (!is_owned_receiver || typ.is_clone)
440            && method.error_type.is_none()
441            && method
442                .params
443                .iter()
444                .all(|p| !p.sanitized && crate::shared::is_opaque_delegatable_type(&p.ty))
445            && matches!(&method.return_type, TypeRef::Named(n) if n == type_name)
446        {
447            // Builder pattern for opaque types: method returns Self without error type.
448            // Delegate to core method and wrap result back in Self { inner: Arc::new(...) }.
449            let core_call = if is_owned_receiver {
450                if self_needs_mutex {
451                    format!("self.inner.lock().unwrap().clone().{}({call_args})", method.name)
452                } else {
453                    format!("(*self.inner).clone().{}({call_args})", method.name)
454                }
455            } else if self_needs_mutex {
456                format!("self.inner.lock().unwrap().{}({call_args})", method.name)
457            } else {
458                format!("self.inner.{}({call_args})", method.name)
459            };
460            let unwrapped = apply_return_newtype_unwrap(&core_call, &method.return_newtype_wrapper);
461            let arc_expr = if self_needs_mutex {
462                format!("Arc::new(std::sync::Mutex::new({unwrapped}))")
463            } else {
464                format!("Arc::new({unwrapped})")
465            };
466            format!("Self {{ inner: {arc_expr} }}")
467        } else if !is_opaque
468            && !method.sanitized
469            && !is_ref_mut_receiver
470            && (!is_owned_receiver || typ.is_clone)
471            && method.error_type.is_none()
472            && method
473                .params
474                .iter()
475                .all(|p| !p.sanitized && is_simple_non_opaque_param(&p.ty))
476            && matches!(&method.return_type, TypeRef::Named(n) if n == type_name)
477        {
478            // Builder pattern for non-opaque types: method returns Self without error type.
479            // Construct core type field-by-field, call method, convert result back via .into().
480            let is_ref_mut = matches!(method.receiver.as_ref(), Some(alef_core::ir::ReceiverKind::RefMut));
481            let field_conversions = if is_ref_mut {
482                gen_lossy_binding_to_core_fields_mut(
483                    typ,
484                    cfg.core_import,
485                    cfg.option_duration_on_defaults,
486                    opaque_types,
487                    cfg.cast_uints_to_i32,
488                    cfg.cast_large_ints_to_f64,
489                    cfg.lossy_skip_types,
490                )
491            } else {
492                gen_lossy_binding_to_core_fields(
493                    typ,
494                    cfg.core_import,
495                    cfg.option_duration_on_defaults,
496                    opaque_types,
497                    cfg.cast_uints_to_i32,
498                    cfg.cast_large_ints_to_f64,
499                    cfg.lossy_skip_types,
500                )
501            };
502            let core_call = format!("core_self.{}({call_args})", method.name);
503            let newtype_suffix = if method.return_newtype_wrapper.is_some() {
504                ".0"
505            } else {
506                ""
507            };
508            let result_wrap = if method.returns_cow || method.returns_ref {
509                ".into_owned().into()"
510            } else {
511                ".into()"
512            };
513            format!("{field_conversions}{core_call}{newtype_suffix}{result_wrap}")
514        } else {
515            gen_unimplemented_body(
516                &method.return_type,
517                &format!("{type_name}.{}", method.name),
518                method.error_type.is_some(),
519                cfg,
520                &method.params,
521                opaque_types,
522            )
523        }
524    } else if method.is_async {
525        let inner_clone_line = if is_opaque {
526            "let inner = self.inner.clone();\n        "
527        } else {
528            ""
529        };
530        let core_call_str = make_async_core_call(&method.name);
531        gen_async_body(
532            &core_call_str,
533            cfg,
534            method.error_type.is_some(),
535            &async_result_wrap,
536            is_opaque,
537            inner_clone_line,
538            matches!(method.return_type, TypeRef::Unit),
539            Some(&return_type),
540        )
541    } else {
542        let core_call = make_core_call(&method.name);
543        if method.error_type.is_some() {
544            // Backend-specific error conversion
545            let err_conv = match cfg.async_pattern {
546                AsyncPattern::Pyo3FutureIntoPy => {
547                    ".map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))"
548                }
549                AsyncPattern::NapiNativeAsync => {
550                    ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))"
551                }
552                AsyncPattern::WasmNativeAsync => ".map_err(|e| JsValue::from_str(&e.to_string()))",
553                _ => ".map_err(|e| e.to_string())",
554            };
555            if is_opaque {
556                if matches!(method.return_type, TypeRef::Unit) {
557                    // Unit return: avoid let_unit_value by not binding the result
558                    format!("{core_call}{err_conv}?;\n        Ok(())")
559                } else {
560                    let wrap = wrap_return_with_mutex(
561                        &result_expr,
562                        &method.return_type,
563                        type_name,
564                        opaque_types,
565                        mutex_types,
566                        is_opaque,
567                        method.returns_ref,
568                        method.returns_cow,
569                    );
570                    format!("let result = {core_call}{err_conv}?;\n        Ok({wrap})")
571                }
572            } else {
573                format!("{core_call}{err_conv}")
574            }
575        } else if is_opaque {
576            let unwrapped_call = apply_return_newtype_unwrap(&core_call, &method.return_newtype_wrapper);
577            wrap_return_with_mutex(
578                &unwrapped_call,
579                &method.return_type,
580                type_name,
581                opaque_types,
582                mutex_types,
583                is_opaque,
584                method.returns_ref,
585                method.returns_cow,
586            )
587        } else {
588            core_call
589        }
590    };
591    let adapter_key = format!("{}.{}", type_name, method.name);
592    let has_adapter = adapter_bodies.contains_key(&adapter_key);
593
594    // Prepend let bindings for non-opaque Named ref params (needed for borrow lifetime).
595    // Skip when an adapter body is used: the adapter body is self-contained and already
596    // includes its own parameter conversions (via core_let_bindings). Prepending the
597    // normal {name}_core bindings would produce a duplicate .into() call on a moved value
598    // (E0382 use of moved value).
599    let body = if ref_let_bindings.is_empty() || has_adapter {
600        body
601    } else {
602        format!("{ref_let_bindings}{body}")
603    };
604
605    let needs_py = method.is_async && cfg.async_pattern == AsyncPattern::Pyo3FutureIntoPy;
606
607    // When an async PyO3 method could not be auto-delegated (e.g. sanitized params,
608    // non-delegatable return type, or trait methods), the body was computed for a
609    // synchronous context but the generated signature will be
610    // `PyResult<Bound<'py, PyAny>>`. Return `Err` directly — wrapping in
611    // `future_into_py` would cause E0283 because the async block only returns `Err`
612    // and Rust cannot infer the generic `T` parameter.
613    let body = if needs_py && !opaque_can_delegate && !has_adapter {
614        let err_msg = format!("Not implemented: {type_name}.{}", method.name);
615        // Suppress unused parameter warnings — params are not used in the stub body.
616        let suppress = if method.params.is_empty() {
617            String::new()
618        } else {
619            let names: Vec<&str> = method.params.iter().map(|p| p.name.as_str()).collect();
620            if names.len() == 1 {
621                format!("let _ = {};\n        ", names[0])
622            } else {
623                format!("let _ = ({});\n        ", names.join(", "))
624            }
625        };
626        format!("{suppress}Err(pyo3::exceptions::PyNotImplementedError::new_err(\"{err_msg}\"))")
627    } else {
628        body
629    };
630    let self_param = match (needs_py, params.is_empty()) {
631        (true, true) => "&self, py: Python<'py>",
632        (true, false) => "&self, py: Python<'py>, ",
633        (false, true) => "&self",
634        (false, false) => "&self, ",
635    };
636
637    // For async PyO3 methods, override return type to PyResult<Bound<'py, PyAny>>
638    // and add the 'py lifetime generic on the method name.
639    // For functional RefMut methods, override to Self (or Result<Self, E>) because the
640    // generated body clones self, applies the mutation, and returns the updated value.
641    let ret = if needs_py {
642        "PyResult<Bound<'py, PyAny>>".to_string()
643    } else if is_functional_ref_mut {
644        mapper.wrap_return("Self", method.error_type.is_some())
645    } else {
646        ret
647    };
648    let method_lifetime = if needs_py { "<'py>" } else { "" };
649
650    // Wrap long signature if necessary
651    let (sig_start, sig_params, sig_end) = if self_param.len() + params.len() > 100 {
652        let wrapped_params = method
653            .params
654            .iter()
655            .map(|p| {
656                let ty = if p.optional {
657                    format!("Option<{}>", mapper.map_type(&p.ty))
658                } else {
659                    mapper.map_type(&p.ty)
660                };
661                format!("{}: {}", p.name, ty)
662            })
663            .collect::<Vec<_>>()
664            .join(",\n        ");
665        let py_param = if needs_py { "\n        py: Python<'py>," } else { "" };
666        (
667            format!(
668                "pub fn {}{method_lifetime}(\n        &self,{}\n        ",
669                method.name, py_param
670            ),
671            wrapped_params,
672            "\n    ) -> ".to_string(),
673        )
674    } else {
675        (
676            format!("pub fn {}{method_lifetime}({}", method.name, self_param),
677            params,
678            ") -> ".to_string(),
679        )
680    };
681
682    let total_params = method.params.len() + 1 + if needs_py { 1 } else { 0 };
683    let sig_defaults = if cfg.needs_signature {
684        function_sig_defaults(&method.params)
685    } else {
686        String::new()
687    };
688
689    crate::template_env::render(
690        "generators/methods/method_signature.jinja",
691        minijinja::context! {
692            has_too_many_arguments => total_params > 7,
693            has_missing_errors_doc => method.error_type.is_some(),
694            has_should_implement_trait => is_trait_method_name(&method.name),
695            needs_signature => cfg.needs_signature,
696            signature_prefix => cfg.signature_prefix,
697            sig_defaults => sig_defaults,
698            signature_suffix => cfg.signature_suffix,
699            sig_start => sig_start,
700            sig_params => sig_params,
701            sig_end => sig_end,
702            ret => ret,
703            body => body,
704        },
705    )
706}
707
708/// Generate a static method.
709pub fn gen_static_method(
710    method: &MethodDef,
711    mapper: &dyn TypeMapper,
712    cfg: &RustBindingConfig,
713    typ: &TypeDef,
714    adapter_bodies: &AdapterBodies,
715    opaque_types: &AHashSet<String>,
716    mutex_types: &AHashSet<String>,
717) -> String {
718    let type_name = &typ.name;
719    // Use the full rust_path (with hyphens replaced by underscores) for core type references
720    let core_type_path = typ.rust_path.replace('-', "_");
721    let map_fn = |ty: &alef_core::ir::TypeRef| mapper.map_type(ty);
722    let params = function_params(&method.params, &map_fn);
723    let return_type = mapper.map_type(&method.return_type);
724    let ret = mapper.wrap_return(&return_type, method.error_type.is_some());
725
726    let core_import = cfg.core_import;
727
728    // Use let bindings when any non-opaque Named or Vec<Named> params exist.
729    // This includes Vec<Named> without is_ref=true, which need element conversion.
730    let use_let_bindings = has_named_params(&method.params, opaque_types);
731    let (call_args, ref_let_bindings) = if use_let_bindings {
732        (
733            gen_call_args_with_let_bindings(&method.params, opaque_types),
734            gen_named_let_bindings_pub(&method.params, opaque_types, core_import),
735        )
736    } else {
737        (gen_call_args(&method.params, opaque_types), String::new())
738    };
739
740    let can_delegate = crate::shared::can_auto_delegate(method, opaque_types);
741
742    let body = if !can_delegate {
743        // Check if an adapter provides the body
744        let adapter_key = format!("{}.{}", type_name, method.name);
745        if let Some(adapter_body) = adapter_bodies.get(&adapter_key) {
746            adapter_body.clone()
747        } else {
748            gen_unimplemented_body(
749                &method.return_type,
750                &format!("{type_name}::{}", method.name),
751                method.error_type.is_some(),
752                cfg,
753                &method.params,
754                opaque_types,
755            )
756        }
757    } else if method.is_async {
758        let core_call = format!("{core_type_path}::{}({call_args})", method.name);
759        let return_wrap = format!("{return_type}::from(result)");
760        gen_async_body(
761            &core_call,
762            cfg,
763            method.error_type.is_some(),
764            &return_wrap,
765            false,
766            "",
767            matches!(method.return_type, TypeRef::Unit),
768            Some(&return_type),
769        )
770    } else {
771        let core_call = format!("{core_type_path}::{}({call_args})", method.name);
772        if method.error_type.is_some() {
773            // Backend-specific error conversion
774            let err_conv = match cfg.async_pattern {
775                AsyncPattern::Pyo3FutureIntoPy => {
776                    ".map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))"
777                }
778                AsyncPattern::NapiNativeAsync => {
779                    ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))"
780                }
781                AsyncPattern::WasmNativeAsync => ".map_err(|e| JsValue::from_str(&e.to_string()))",
782                _ => ".map_err(|e| e.to_string())",
783            };
784            // Wrap the Ok value if the return type needs conversion (e.g. PathBuf→String)
785            let val_expr = apply_return_newtype_unwrap("val", &method.return_newtype_wrapper);
786            let wrapped = wrap_return_with_mutex(
787                &val_expr,
788                &method.return_type,
789                type_name,
790                opaque_types,
791                mutex_types,
792                typ.is_opaque,
793                method.returns_ref,
794                method.returns_cow,
795            );
796            if wrapped == val_expr {
797                format!("{core_call}{err_conv}")
798            } else if wrapped == format!("{val_expr}.into()") {
799                format!("{core_call}.map(Into::into){err_conv}")
800            } else if let Some(type_path) = wrapped.strip_suffix(&format!("::from({val_expr})")) {
801                format!("{core_call}.map({type_path}::from){err_conv}")
802            } else {
803                format!("{core_call}.map(|val| {wrapped}){err_conv}")
804            }
805        } else {
806            // Wrap return value for non-error case too (e.g. PathBuf→String)
807            let unwrapped_call = apply_return_newtype_unwrap(&core_call, &method.return_newtype_wrapper);
808            wrap_return_with_mutex(
809                &unwrapped_call,
810                &method.return_type,
811                type_name,
812                opaque_types,
813                mutex_types,
814                typ.is_opaque,
815                method.returns_ref,
816                method.returns_cow,
817            )
818        }
819    };
820    // Prepend let bindings for non-opaque Named ref params
821    let body = if ref_let_bindings.is_empty() {
822        body
823    } else {
824        format!("{ref_let_bindings}{body}")
825    };
826
827    let static_needs_py = method.is_async && cfg.async_pattern == AsyncPattern::Pyo3FutureIntoPy;
828
829    // For async PyO3 static methods, override return type and add lifetime generic.
830    let ret = if static_needs_py {
831        "PyResult<Bound<'py, PyAny>>".to_string()
832    } else {
833        ret
834    };
835    let method_lifetime = if static_needs_py { "<'py>" } else { "" };
836
837    // Wrap long signature if necessary
838    let (sig_start, sig_params, sig_end) = if params.len() > 100 {
839        let wrapped_params = method
840            .params
841            .iter()
842            .map(|p| {
843                let ty = if p.optional {
844                    format!("Option<{}>", mapper.map_type(&p.ty))
845                } else {
846                    mapper.map_type(&p.ty)
847                };
848                format!("{}: {}", p.name, ty)
849            })
850            .collect::<Vec<_>>()
851            .join(",\n        ");
852        // For async PyO3, add py parameter
853        if static_needs_py {
854            (
855                format!("pub fn {}{method_lifetime}(py: Python<'py>,\n        ", method.name),
856                wrapped_params,
857                "\n    ) -> ".to_string(),
858            )
859        } else {
860            (
861                format!("pub fn {}(\n        ", method.name),
862                wrapped_params,
863                "\n    ) -> ".to_string(),
864            )
865        }
866    } else if static_needs_py {
867        (
868            format!("pub fn {}{method_lifetime}(py: Python<'py>, ", method.name),
869            params,
870            ") -> ".to_string(),
871        )
872    } else {
873        (format!("pub fn {}(", method.name), params, ") -> ".to_string())
874    };
875
876    let total_params = method.params.len() + if static_needs_py { 1 } else { 0 };
877    let sig_defaults = if cfg.needs_signature {
878        function_sig_defaults(&method.params)
879    } else {
880        String::new()
881    };
882    let static_attr_str = if let Some(attr) = cfg.static_attr {
883        format!("#[{attr}]")
884    } else {
885        String::new()
886    };
887
888    // For static methods, we need a variant of method_signature template
889    // that handles static attributes. For now, build manually but use render for main block
890    let mut out = String::with_capacity(1024);
891    if total_params > 7 {
892        out.push_str("    #[allow(clippy::too_many_arguments)]\n");
893    }
894    if method.error_type.is_some() {
895        out.push_str("    #[allow(clippy::missing_errors_doc)]\n");
896    }
897    if is_trait_method_name(&method.name) {
898        out.push_str("    #[allow(clippy::should_implement_trait)]\n");
899    }
900    if !static_attr_str.is_empty() {
901        out.push_str(&crate::template_env::render(
902            "generators/methods/static_attr.jinja",
903            minijinja::context! {
904                static_attr_str => static_attr_str,
905            },
906        ));
907    }
908    if cfg.needs_signature {
909        out.push_str(&crate::template_env::render(
910            "generators/methods/signature_attr.jinja",
911            minijinja::context! {
912                signature_prefix => &cfg.signature_prefix,
913                sig_defaults => sig_defaults,
914                signature_suffix => &cfg.signature_suffix,
915            },
916        ));
917    }
918    out.push_str(&crate::template_env::render(
919        "generators/methods/method_body.jinja",
920        minijinja::context! {
921            sig_start => sig_start,
922            sig_params => sig_params,
923            sig_end => sig_end,
924            ret => ret,
925            body => body,
926        },
927    ));
928    out
929}
930
931/// Generate a full methods impl block (non-opaque types).
932pub fn gen_impl_block(
933    typ: &TypeDef,
934    mapper: &dyn TypeMapper,
935    cfg: &RustBindingConfig,
936    adapter_bodies: &AdapterBodies,
937    opaque_types: &AHashSet<String>,
938) -> String {
939    gen_impl_block_with_renames(typ, mapper, cfg, adapter_bodies, opaque_types, None)
940}
941
942/// Like `gen_impl_block` but with field renames for keyword escaping in the constructor.
943pub fn gen_impl_block_with_renames(
944    typ: &TypeDef,
945    mapper: &dyn TypeMapper,
946    cfg: &RustBindingConfig,
947    adapter_bodies: &AdapterBodies,
948    opaque_types: &AHashSet<String>,
949    field_renames: Option<&std::collections::HashMap<String, String>>,
950) -> String {
951    let (instance, statics) = partition_methods(&typ.methods);
952    // Compute effective (non-sanitized or adapter-overridden) method counts for the early-return
953    // check. Sanitized methods without adapters are skipped in the loops below, so they do not
954    // contribute real content to the impl block.
955    let has_emittable_instance = instance
956        .iter()
957        .any(|m| !m.sanitized || adapter_bodies.contains_key(&format!("{}.{}", typ.name, m.name)));
958    let has_emittable_statics = statics
959        .iter()
960        .any(|m| !m.sanitized || adapter_bodies.contains_key(&format!("{}.{}", typ.name, m.name)));
961    if !has_emittable_instance && !has_emittable_statics && typ.fields.is_empty() {
962        return String::new();
963    }
964
965    let prefixed_name = format!("{}{}", cfg.type_name_prefix, typ.name);
966    let mut out = String::with_capacity(2048);
967
968    // Constructor — suppressed when the backend handles construction via a separate free
969    // function (e.g. extendr kwargs constructor) or when there are no fields.
970    if !typ.fields.is_empty() && !cfg.skip_impl_constructor {
971        out.push_str(&gen_constructor_with_renames(typ, mapper, cfg, field_renames));
972        out.push_str("\n\n");
973    }
974
975    // Instance methods
976    let empty_mutex_types: AHashSet<String> = AHashSet::new();
977    for m in &instance {
978        // Skip sanitized methods that have no adapter override — they cannot be delegated
979        // and emitting an unimplemented stub pollutes the public API with dead placeholders.
980        // Adapter bodies are explicit overrides and always take precedence.
981        let adapter_key = format!("{}.{}", typ.name, m.name);
982        if m.sanitized && !adapter_bodies.contains_key(&adapter_key) {
983            continue;
984        }
985        out.push_str(&gen_method(
986            m,
987            mapper,
988            cfg,
989            typ,
990            false,
991            opaque_types,
992            &empty_mutex_types,
993            adapter_bodies,
994        ));
995        out.push_str("\n\n");
996    }
997
998    // Static methods
999    for m in &statics {
1000        // Skip sanitized static methods that have no adapter override.
1001        let adapter_key = format!("{}.{}", typ.name, m.name);
1002        if m.sanitized && !adapter_bodies.contains_key(&adapter_key) {
1003            continue;
1004        }
1005        out.push_str(&gen_static_method(
1006            m,
1007            mapper,
1008            cfg,
1009            typ,
1010            adapter_bodies,
1011            opaque_types,
1012            &empty_mutex_types,
1013        ));
1014        out.push_str("\n\n");
1015    }
1016
1017    // Trim trailing newlines inside impl block
1018    let trimmed = out.trim_end();
1019    let content = trimmed.to_string();
1020
1021    crate::template_env::render(
1022        "generators/methods/impl_block.jinja",
1023        minijinja::context! {
1024            block_attr => cfg.method_block_attr,
1025            prefixed_name => prefixed_name,
1026            content => content,
1027        },
1028    )
1029}
1030
1031/// Generate a full impl block for an opaque type, delegating methods to `self.inner`.
1032///
1033/// `opaque_types` is the set of type names that are opaque wrappers (use `Arc<inner>`).
1034/// This is needed so that return-type wrapping uses the correct pattern for cross-type returns.
1035/// `mutex_types` is the subset of opaque types whose inner field uses `Arc<Mutex<T>>`;
1036/// method dispatch uses `.lock().unwrap()` for these types.
1037pub fn gen_opaque_impl_block(
1038    typ: &TypeDef,
1039    mapper: &dyn TypeMapper,
1040    cfg: &RustBindingConfig,
1041    opaque_types: &AHashSet<String>,
1042    mutex_types: &AHashSet<String>,
1043    adapter_bodies: &AdapterBodies,
1044) -> String {
1045    let (instance, statics) = partition_methods(&typ.methods);
1046    // Compute effective (non-sanitized or adapter-overridden) method counts.
1047    let has_emittable_instance = instance
1048        .iter()
1049        .any(|m| !m.sanitized || adapter_bodies.contains_key(&format!("{}.{}", typ.name, m.name)));
1050    let has_emittable_statics = statics
1051        .iter()
1052        .any(|m| !m.sanitized || adapter_bodies.contains_key(&format!("{}.{}", typ.name, m.name)));
1053    if !has_emittable_instance && !has_emittable_statics {
1054        return String::new();
1055    }
1056
1057    let mut out = String::with_capacity(2048);
1058    let prefixed_name = format!("{}{}", cfg.type_name_prefix, typ.name);
1059
1060    // Instance methods — delegate to self.inner
1061    for m in &instance {
1062        // Skip sanitized methods that have no adapter override — they cannot be delegated
1063        // and emitting an unimplemented stub pollutes the public API with dead placeholders.
1064        let adapter_key = format!("{}.{}", typ.name, m.name);
1065        if m.sanitized && !adapter_bodies.contains_key(&adapter_key) {
1066            continue;
1067        }
1068        out.push_str(&gen_method(
1069            m,
1070            mapper,
1071            cfg,
1072            typ,
1073            true,
1074            opaque_types,
1075            mutex_types,
1076            adapter_bodies,
1077        ));
1078        out.push_str("\n\n");
1079    }
1080
1081    // Static methods
1082    for m in &statics {
1083        // Skip sanitized static methods that have no adapter override.
1084        let adapter_key = format!("{}.{}", typ.name, m.name);
1085        if m.sanitized && !adapter_bodies.contains_key(&adapter_key) {
1086            continue;
1087        }
1088        out.push_str(&gen_static_method(
1089            m,
1090            mapper,
1091            cfg,
1092            typ,
1093            adapter_bodies,
1094            opaque_types,
1095            mutex_types,
1096        ));
1097        out.push_str("\n\n");
1098    }
1099
1100    let trimmed = out.trim_end();
1101    let content = trimmed.to_string();
1102
1103    crate::template_env::render(
1104        "generators/methods/impl_block.jinja",
1105        minijinja::context! {
1106            block_attr => cfg.method_block_attr,
1107            prefixed_name => prefixed_name,
1108            content => content,
1109        },
1110    )
1111}