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