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