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 => ".map_err(|e| extendr_api::Error::Other(e.to_string()))",
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(
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                );
277                format!("{serde_bindings}let result = {core_call}{err_conv}?;\n        Ok({wrap})")
278            }
279        } else if is_functional_ref_mut {
280            // Functional clone-mutate-return pattern for non-opaque RefMut methods.
281            // PyO3 frozen classes and WASM structs don't support &mut self, so instead:
282            //   1. Convert binding self to a mutable core type.
283            //   2. Call the mutating core method (which changes core_self in place).
284            //   3. Convert the mutated core type back to the binding type and return Self.
285            //
286            // The generated signature uses &self -> Self (or -> Result<Self, E> if fallible),
287            // making the method work correctly with immutable binding wrappers.
288            let field_conversions = gen_lossy_binding_to_core_fields_mut(
289                typ,
290                cfg.core_import,
291                cfg.option_duration_on_defaults,
292                opaque_types,
293                cfg.cast_uints_to_i32,
294                cfg.cast_large_ints_to_f64,
295                cfg.lossy_skip_types,
296            );
297            let core_call = format!("core_self.{}({call_args})", method.name);
298            if method.error_type.is_some() {
299                let err_conv = match cfg.async_pattern {
300                    AsyncPattern::Pyo3FutureIntoPy => {
301                        ".map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))"
302                    }
303                    AsyncPattern::NapiNativeAsync => {
304                        ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))"
305                    }
306                    AsyncPattern::WasmNativeAsync => ".map_err(|e| JsValue::from_str(&e.to_string()))",
307                    AsyncPattern::TokioBlockOn => ".map_err(|e| extendr_api::Error::Other(e.to_string()))",
308                    _ => ".map_err(|e| e.to_string())",
309                };
310                format!("{field_conversions}{core_call}{err_conv}?;\n        Ok(core_self.into())")
311            } else {
312                format!("{field_conversions}{core_call};\n        core_self.into()")
313            }
314        } else if !is_opaque
315            && !method.sanitized
316            && method
317                .params
318                .iter()
319                .all(|p| !p.sanitized && is_simple_non_opaque_param(&p.ty))
320            && crate::shared::is_delegatable_return(&method.return_type)
321        {
322            // Non-opaque delegation: construct core type field-by-field, call method, convert back.
323            // Sanitized fields use Default::default() (lossy but functional for builder pattern).
324            let is_ref_mut = matches!(method.receiver.as_ref(), Some(alef_core::ir::ReceiverKind::RefMut));
325            let field_conversions = if is_ref_mut {
326                gen_lossy_binding_to_core_fields_mut(
327                    typ,
328                    cfg.core_import,
329                    cfg.option_duration_on_defaults,
330                    opaque_types,
331                    cfg.cast_uints_to_i32,
332                    cfg.cast_large_ints_to_f64,
333                    cfg.lossy_skip_types,
334                )
335            } else {
336                gen_lossy_binding_to_core_fields(
337                    typ,
338                    cfg.core_import,
339                    cfg.option_duration_on_defaults,
340                    opaque_types,
341                    cfg.cast_uints_to_i32,
342                    cfg.cast_large_ints_to_f64,
343                    cfg.lossy_skip_types,
344                )
345            };
346            let core_call = format!("core_self.{}({call_args})", method.name);
347            let newtype_suffix = if method.return_newtype_wrapper.is_some() {
348                ".0"
349            } else {
350                ""
351            };
352            let result_wrap = match &method.return_type {
353                // When returns_cow=true the core returns Cow<'_, T>: call .into_owned() to
354                // obtain an owned T before the binding→core From conversion.
355                // When returns_ref=true (or &T / Cow<'_, T> via the old flag), same treatment.
356                TypeRef::Named(n) if n == type_name && (method.returns_cow || method.returns_ref) => {
357                    ".into_owned().into()".to_string()
358                }
359                TypeRef::Named(_) if method.returns_cow || method.returns_ref => ".into_owned().into()".to_string(),
360                TypeRef::Named(n) if n == type_name => ".into()".to_string(),
361                TypeRef::Named(_) => ".into()".to_string(),
362                TypeRef::String => {
363                    if method.returns_ref {
364                        ".to_owned()".to_string()
365                    } else {
366                        String::new()
367                    }
368                }
369                TypeRef::Path => {
370                    if method.returns_ref {
371                        ".to_owned()".to_string()
372                    } else {
373                        ".to_string_lossy().to_string()".to_string()
374                    }
375                }
376                // Bytes: binding uses Vec<u8>. Always use .to_vec() which works for both
377                // &Bytes and owned Bytes (avoids &Bytes→Vec<u8> From trait issues).
378                TypeRef::Bytes => ".to_vec()".to_string(),
379                // Optional<Named>: when core returns Option<&T>, need .map(|v| v.clone().into())
380                TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Named(_)) => {
381                    if method.returns_ref {
382                        ".map(|v| v.clone().into())".to_string()
383                    } else {
384                        ".map(Into::into)".to_string()
385                    }
386                }
387                // Optional<String>: when core returns Option<&str>, need .map(|v| v.to_owned())
388                TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::String | TypeRef::Bytes) => {
389                    if method.returns_ref {
390                        ".map(|v| v.to_owned())".to_string()
391                    } else {
392                        String::new()
393                    }
394                }
395                // Primitive return: cast when the binding uses a different numeric type.
396                // R maps u8/u16/u32/i8/i16 → i32 and u64/i64/usize/isize/f32 → f64.
397                TypeRef::Primitive(p) => {
398                    use crate::conversions::helpers::{needs_f64_cast, needs_i32_cast};
399                    if cfg.cast_uints_to_i32 && needs_i32_cast(p) {
400                        " as i32".to_string()
401                    } else if cfg.cast_large_ints_to_f64 && needs_f64_cast(p) {
402                        " as f64".to_string()
403                    } else {
404                        String::new()
405                    }
406                }
407                // Optional<Primitive>: cast inside map when the binding uses a different type.
408                TypeRef::Optional(inner) => {
409                    if let TypeRef::Primitive(p) = inner.as_ref() {
410                        use crate::conversions::helpers::{needs_f64_cast, needs_i32_cast};
411                        if cfg.cast_uints_to_i32 && needs_i32_cast(p) {
412                            ".map(|v| v as i32)".to_string()
413                        } else if cfg.cast_large_ints_to_f64 && needs_f64_cast(p) {
414                            ".map(|v| v as f64)".to_string()
415                        } else {
416                            String::new()
417                        }
418                    } else {
419                        String::new()
420                    }
421                }
422                _ => String::new(),
423            };
424            if method.error_type.is_some() {
425                let err_conv = match cfg.async_pattern {
426                    AsyncPattern::Pyo3FutureIntoPy => {
427                        ".map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))"
428                    }
429                    AsyncPattern::NapiNativeAsync => {
430                        ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))"
431                    }
432                    AsyncPattern::WasmNativeAsync => ".map_err(|e| JsValue::from_str(&e.to_string()))",
433                    _ => ".map_err(|e| e.to_string())",
434                };
435                format!(
436                    "{field_conversions}let result = {core_call}{err_conv}?;\n        Ok(result{newtype_suffix}{result_wrap})"
437                )
438            } else {
439                format!("{field_conversions}{core_call}{newtype_suffix}{result_wrap}")
440            }
441        } else if is_opaque
442            && !method.sanitized
443            && (!is_ref_mut_receiver || self_needs_mutex)
444            && (!is_owned_receiver || typ.is_clone)
445            && method.error_type.is_none()
446            && method
447                .params
448                .iter()
449                .all(|p| !p.sanitized && crate::shared::is_opaque_delegatable_type(&p.ty))
450            && matches!(&method.return_type, TypeRef::Named(n) if n == type_name)
451        {
452            // Builder pattern for opaque types: method returns Self without error type.
453            // Delegate to core method and wrap result back in Self { inner: Arc::new(...) }.
454            let core_call = if is_owned_receiver {
455                if self_needs_mutex {
456                    format!("self.inner.lock().unwrap().clone().{}({call_args})", method.name)
457                } else {
458                    format!("(*self.inner).clone().{}({call_args})", method.name)
459                }
460            } else if self_needs_mutex {
461                format!("self.inner.lock().unwrap().{}({call_args})", method.name)
462            } else {
463                format!("self.inner.{}({call_args})", method.name)
464            };
465            let unwrapped = apply_return_newtype_unwrap(&core_call, &method.return_newtype_wrapper);
466            let arc_expr = if self_needs_mutex {
467                format!("Arc::new(std::sync::Mutex::new({unwrapped}))")
468            } else {
469                format!("Arc::new({unwrapped})")
470            };
471            format!("Self {{ inner: {arc_expr} }}")
472        } else if !is_opaque
473            && !method.sanitized
474            && !is_ref_mut_receiver
475            && (!is_owned_receiver || typ.is_clone)
476            && method.error_type.is_none()
477            && method
478                .params
479                .iter()
480                .all(|p| !p.sanitized && is_simple_non_opaque_param(&p.ty))
481            && matches!(&method.return_type, TypeRef::Named(n) if n == type_name)
482        {
483            // Builder pattern for non-opaque types: method returns Self without error type.
484            // Construct core type field-by-field, call method, convert result back via .into().
485            let is_ref_mut = matches!(method.receiver.as_ref(), Some(alef_core::ir::ReceiverKind::RefMut));
486            let field_conversions = if is_ref_mut {
487                gen_lossy_binding_to_core_fields_mut(
488                    typ,
489                    cfg.core_import,
490                    cfg.option_duration_on_defaults,
491                    opaque_types,
492                    cfg.cast_uints_to_i32,
493                    cfg.cast_large_ints_to_f64,
494                    cfg.lossy_skip_types,
495                )
496            } else {
497                gen_lossy_binding_to_core_fields(
498                    typ,
499                    cfg.core_import,
500                    cfg.option_duration_on_defaults,
501                    opaque_types,
502                    cfg.cast_uints_to_i32,
503                    cfg.cast_large_ints_to_f64,
504                    cfg.lossy_skip_types,
505                )
506            };
507            let core_call = format!("core_self.{}({call_args})", method.name);
508            let newtype_suffix = if method.return_newtype_wrapper.is_some() {
509                ".0"
510            } else {
511                ""
512            };
513            let result_wrap = if method.returns_cow || method.returns_ref {
514                ".into_owned().into()"
515            } else {
516                ".into()"
517            };
518            format!("{field_conversions}{core_call}{newtype_suffix}{result_wrap}")
519        } else {
520            gen_unimplemented_body(
521                &method.return_type,
522                &format!("{type_name}.{}", method.name),
523                method.error_type.is_some(),
524                cfg,
525                &method.params,
526                opaque_types,
527            )
528        }
529    } else if method.is_async {
530        let inner_clone_line = if is_opaque {
531            "let inner = self.inner.clone();\n        "
532        } else {
533            ""
534        };
535        let core_call_str = make_async_core_call(&method.name);
536        gen_async_body(
537            &core_call_str,
538            cfg,
539            method.error_type.is_some(),
540            &async_result_wrap,
541            is_opaque,
542            inner_clone_line,
543            matches!(method.return_type, TypeRef::Unit),
544            Some(&return_type),
545        )
546    } else {
547        let core_call = make_core_call(&method.name);
548        if method.error_type.is_some() {
549            // Backend-specific error conversion
550            let err_conv = match cfg.async_pattern {
551                AsyncPattern::Pyo3FutureIntoPy => {
552                    ".map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))"
553                }
554                AsyncPattern::NapiNativeAsync => {
555                    ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))"
556                }
557                AsyncPattern::WasmNativeAsync => ".map_err(|e| JsValue::from_str(&e.to_string()))",
558                _ => ".map_err(|e| e.to_string())",
559            };
560            if is_opaque {
561                if matches!(method.return_type, TypeRef::Unit) {
562                    // Unit return: avoid let_unit_value by not binding the result
563                    format!("{core_call}{err_conv}?;\n        Ok(())")
564                } else {
565                    let wrap = wrap_return_with_mutex(
566                        &result_expr,
567                        &method.return_type,
568                        type_name,
569                        opaque_types,
570                        mutex_types,
571                        is_opaque,
572                        method.returns_ref,
573                        method.returns_cow,
574                    );
575                    format!("let result = {core_call}{err_conv}?;\n        Ok({wrap})")
576                }
577            } else {
578                format!("{core_call}{err_conv}")
579            }
580        } else if is_opaque {
581            let unwrapped_call = apply_return_newtype_unwrap(&core_call, &method.return_newtype_wrapper);
582            wrap_return_with_mutex(
583                &unwrapped_call,
584                &method.return_type,
585                type_name,
586                opaque_types,
587                mutex_types,
588                is_opaque,
589                method.returns_ref,
590                method.returns_cow,
591            )
592        } else {
593            core_call
594        }
595    };
596    let adapter_key = format!("{}.{}", type_name, method.name);
597    let has_adapter = adapter_bodies.contains_key(&adapter_key);
598
599    // Prepend let bindings for non-opaque Named ref params (needed for borrow lifetime).
600    // Skip when an adapter body is used: the adapter body is self-contained and already
601    // includes its own parameter conversions (via core_let_bindings). Prepending the
602    // normal {name}_core bindings would produce a duplicate .into() call on a moved value
603    // (E0382 use of moved value).
604    let body = if ref_let_bindings.is_empty() || has_adapter {
605        body
606    } else {
607        format!("{ref_let_bindings}{body}")
608    };
609
610    let needs_py = method.is_async && cfg.async_pattern == AsyncPattern::Pyo3FutureIntoPy;
611
612    // When an async PyO3 method could not be auto-delegated (e.g. sanitized params,
613    // non-delegatable return type, or trait methods), the body was computed for a
614    // synchronous context but the generated signature will be
615    // `PyResult<Bound<'py, PyAny>>`. Return `Err` directly — wrapping in
616    // `future_into_py` would cause E0283 because the async block only returns `Err`
617    // and Rust cannot infer the generic `T` parameter.
618    let body = if needs_py && !opaque_can_delegate && !has_adapter {
619        let err_msg = format!("Not implemented: {type_name}.{}", method.name);
620        // Suppress unused parameter warnings — params are not used in the stub body.
621        let suppress = if method.params.is_empty() {
622            String::new()
623        } else {
624            let names: Vec<&str> = method.params.iter().map(|p| p.name.as_str()).collect();
625            if names.len() == 1 {
626                format!("let _ = {};\n        ", names[0])
627            } else {
628                format!("let _ = ({});\n        ", names.join(", "))
629            }
630        };
631        format!("{suppress}Err(pyo3::exceptions::PyNotImplementedError::new_err(\"{err_msg}\"))")
632    } else {
633        body
634    };
635    let self_param = match (needs_py, params.is_empty()) {
636        (true, true) => "&self, py: Python<'py>",
637        (true, false) => "&self, py: Python<'py>, ",
638        (false, true) => "&self",
639        (false, false) => "&self, ",
640    };
641
642    // For async PyO3 methods, override return type to PyResult<Bound<'py, PyAny>>
643    // and add the 'py lifetime generic on the method name.
644    // For functional RefMut methods, override to Self (or Result<Self, E>) because the
645    // generated body clones self, applies the mutation, and returns the updated value.
646    let ret = if needs_py {
647        "PyResult<Bound<'py, PyAny>>".to_string()
648    } else if is_functional_ref_mut {
649        mapper.wrap_return("Self", method.error_type.is_some())
650    } else {
651        ret
652    };
653    let method_lifetime = if needs_py { "<'py>" } else { "" };
654
655    // Wrap long signature if necessary
656    let (sig_start, sig_params, sig_end) = if self_param.len() + params.len() > 100 {
657        let wrapped_params = method
658            .params
659            .iter()
660            .map(|p| {
661                let ty = if p.optional {
662                    format!("Option<{}>", mapper.map_type(&p.ty))
663                } else {
664                    mapper.map_type(&p.ty)
665                };
666                format!("{}: {}", p.name, ty)
667            })
668            .collect::<Vec<_>>()
669            .join(",\n        ");
670        let py_param = if needs_py { "\n        py: Python<'py>," } else { "" };
671        (
672            format!(
673                "pub fn {}{method_lifetime}(\n        &self,{}\n        ",
674                method.name, py_param
675            ),
676            wrapped_params,
677            "\n    ) -> ".to_string(),
678        )
679    } else {
680        (
681            format!("pub fn {}{method_lifetime}({}", method.name, self_param),
682            params,
683            ") -> ".to_string(),
684        )
685    };
686
687    let mut out = String::with_capacity(1024);
688    // Per-item clippy suppression: too_many_arguments when >7 params (including &self and py)
689    let total_params = method.params.len() + 1 + if needs_py { 1 } else { 0 };
690    if total_params > 7 {
691        writeln!(out, "    #[allow(clippy::too_many_arguments)]").ok();
692    }
693    // Per-item clippy suppression: missing_errors_doc for Result-returning methods
694    if method.error_type.is_some() {
695        writeln!(out, "    #[allow(clippy::missing_errors_doc)]").ok();
696    }
697    // Per-item clippy suppression: should_implement_trait for trait-conflicting names
698    if is_trait_method_name(&method.name) {
699        writeln!(out, "    #[allow(clippy::should_implement_trait)]").ok();
700    }
701    if cfg.needs_signature {
702        let sig = function_sig_defaults(&method.params);
703        writeln!(out, "    {}{}{}", cfg.signature_prefix, sig, cfg.signature_suffix).ok();
704    }
705    write!(
706        out,
707        "    {}{}{}{} {{\n        \
708         {body}\n    }}",
709        sig_start, sig_params, sig_end, ret,
710    )
711    .ok();
712    out
713}
714
715/// Generate a static method.
716pub fn gen_static_method(
717    method: &MethodDef,
718    mapper: &dyn TypeMapper,
719    cfg: &RustBindingConfig,
720    typ: &TypeDef,
721    adapter_bodies: &AdapterBodies,
722    opaque_types: &AHashSet<String>,
723    mutex_types: &AHashSet<String>,
724) -> String {
725    let type_name = &typ.name;
726    // Use the full rust_path (with hyphens replaced by underscores) for core type references
727    let core_type_path = typ.rust_path.replace('-', "_");
728    let map_fn = |ty: &alef_core::ir::TypeRef| mapper.map_type(ty);
729    let params = function_params(&method.params, &map_fn);
730    let return_type = mapper.map_type(&method.return_type);
731    let ret = mapper.wrap_return(&return_type, method.error_type.is_some());
732
733    let core_import = cfg.core_import;
734
735    // Use let bindings when any non-opaque Named or Vec<Named> params exist.
736    // This includes Vec<Named> without is_ref=true, which need element conversion.
737    let use_let_bindings = has_named_params(&method.params, opaque_types);
738    let (call_args, ref_let_bindings) = if use_let_bindings {
739        (
740            gen_call_args_with_let_bindings(&method.params, opaque_types),
741            gen_named_let_bindings_pub(&method.params, opaque_types, core_import),
742        )
743    } else {
744        (gen_call_args(&method.params, opaque_types), String::new())
745    };
746
747    let can_delegate = crate::shared::can_auto_delegate(method, opaque_types);
748
749    let body = if !can_delegate {
750        // Check if an adapter provides the body
751        let adapter_key = format!("{}.{}", type_name, method.name);
752        if let Some(adapter_body) = adapter_bodies.get(&adapter_key) {
753            adapter_body.clone()
754        } else {
755            gen_unimplemented_body(
756                &method.return_type,
757                &format!("{type_name}::{}", method.name),
758                method.error_type.is_some(),
759                cfg,
760                &method.params,
761                opaque_types,
762            )
763        }
764    } else if method.is_async {
765        let core_call = format!("{core_type_path}::{}({call_args})", method.name);
766        let return_wrap = format!("{return_type}::from(result)");
767        gen_async_body(
768            &core_call,
769            cfg,
770            method.error_type.is_some(),
771            &return_wrap,
772            false,
773            "",
774            matches!(method.return_type, TypeRef::Unit),
775            Some(&return_type),
776        )
777    } else {
778        let core_call = format!("{core_type_path}::{}({call_args})", method.name);
779        if method.error_type.is_some() {
780            // Backend-specific error conversion
781            let err_conv = match cfg.async_pattern {
782                AsyncPattern::Pyo3FutureIntoPy => {
783                    ".map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))"
784                }
785                AsyncPattern::NapiNativeAsync => {
786                    ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))"
787                }
788                AsyncPattern::WasmNativeAsync => ".map_err(|e| JsValue::from_str(&e.to_string()))",
789                _ => ".map_err(|e| e.to_string())",
790            };
791            // Wrap the Ok value if the return type needs conversion (e.g. PathBuf→String)
792            let val_expr = apply_return_newtype_unwrap("val", &method.return_newtype_wrapper);
793            let wrapped = wrap_return_with_mutex(
794                &val_expr,
795                &method.return_type,
796                type_name,
797                opaque_types,
798                mutex_types,
799                typ.is_opaque,
800                method.returns_ref,
801                method.returns_cow,
802            );
803            if wrapped == val_expr {
804                format!("{core_call}{err_conv}")
805            } else if wrapped == format!("{val_expr}.into()") {
806                format!("{core_call}.map(Into::into){err_conv}")
807            } else if let Some(type_path) = wrapped.strip_suffix(&format!("::from({val_expr})")) {
808                format!("{core_call}.map({type_path}::from){err_conv}")
809            } else {
810                format!("{core_call}.map(|val| {wrapped}){err_conv}")
811            }
812        } else {
813            // Wrap return value for non-error case too (e.g. PathBuf→String)
814            let unwrapped_call = apply_return_newtype_unwrap(&core_call, &method.return_newtype_wrapper);
815            wrap_return_with_mutex(
816                &unwrapped_call,
817                &method.return_type,
818                type_name,
819                opaque_types,
820                mutex_types,
821                typ.is_opaque,
822                method.returns_ref,
823                method.returns_cow,
824            )
825        }
826    };
827    // Prepend let bindings for non-opaque Named ref params
828    let body = if ref_let_bindings.is_empty() {
829        body
830    } else {
831        format!("{ref_let_bindings}{body}")
832    };
833
834    let static_needs_py = method.is_async && cfg.async_pattern == AsyncPattern::Pyo3FutureIntoPy;
835
836    // For async PyO3 static methods, override return type and add lifetime generic.
837    let ret = if static_needs_py {
838        "PyResult<Bound<'py, PyAny>>".to_string()
839    } else {
840        ret
841    };
842    let method_lifetime = if static_needs_py { "<'py>" } else { "" };
843
844    // Wrap long signature if necessary
845    let (sig_start, sig_params, sig_end) = if params.len() > 100 {
846        let wrapped_params = method
847            .params
848            .iter()
849            .map(|p| {
850                let ty = if p.optional {
851                    format!("Option<{}>", mapper.map_type(&p.ty))
852                } else {
853                    mapper.map_type(&p.ty)
854                };
855                format!("{}: {}", p.name, ty)
856            })
857            .collect::<Vec<_>>()
858            .join(",\n        ");
859        // For async PyO3, add py parameter
860        if static_needs_py {
861            (
862                format!("pub fn {}{method_lifetime}(py: Python<'py>,\n        ", method.name),
863                wrapped_params,
864                "\n    ) -> ".to_string(),
865            )
866        } else {
867            (
868                format!("pub fn {}(\n        ", method.name),
869                wrapped_params,
870                "\n    ) -> ".to_string(),
871            )
872        }
873    } else if static_needs_py {
874        (
875            format!("pub fn {}{method_lifetime}(py: Python<'py>, ", method.name),
876            params,
877            ") -> ".to_string(),
878        )
879    } else {
880        (format!("pub fn {}(", method.name), params, ") -> ".to_string())
881    };
882
883    let mut out = String::with_capacity(1024);
884    // Per-item clippy suppression: too_many_arguments when >7 params (including py)
885    let total_params = method.params.len() + if static_needs_py { 1 } else { 0 };
886    if total_params > 7 {
887        writeln!(out, "    #[allow(clippy::too_many_arguments)]").ok();
888    }
889    // Per-item clippy suppression: missing_errors_doc for Result-returning methods
890    if method.error_type.is_some() {
891        writeln!(out, "    #[allow(clippy::missing_errors_doc)]").ok();
892    }
893    // Per-item clippy suppression: should_implement_trait for trait-conflicting names
894    if is_trait_method_name(&method.name) {
895        writeln!(out, "    #[allow(clippy::should_implement_trait)]").ok();
896    }
897    if let Some(attr) = cfg.static_attr {
898        writeln!(out, "    #[{attr}]").ok();
899    }
900    if cfg.needs_signature {
901        let sig = function_sig_defaults(&method.params);
902        writeln!(out, "    {}{}{}", cfg.signature_prefix, sig, cfg.signature_suffix).ok();
903    }
904    write!(
905        out,
906        "    {}{}{}{} {{\n        \
907         {body}\n    }}",
908        sig_start, sig_params, sig_end, ret,
909    )
910    .ok();
911    out
912}
913
914/// Generate a full methods impl block (non-opaque types).
915pub fn gen_impl_block(
916    typ: &TypeDef,
917    mapper: &dyn TypeMapper,
918    cfg: &RustBindingConfig,
919    adapter_bodies: &AdapterBodies,
920    opaque_types: &AHashSet<String>,
921) -> String {
922    gen_impl_block_with_renames(typ, mapper, cfg, adapter_bodies, opaque_types, None)
923}
924
925/// Like `gen_impl_block` but with field renames for keyword escaping in the constructor.
926pub fn gen_impl_block_with_renames(
927    typ: &TypeDef,
928    mapper: &dyn TypeMapper,
929    cfg: &RustBindingConfig,
930    adapter_bodies: &AdapterBodies,
931    opaque_types: &AHashSet<String>,
932    field_renames: Option<&std::collections::HashMap<String, String>>,
933) -> String {
934    let (instance, statics) = partition_methods(&typ.methods);
935    // Compute effective (non-sanitized or adapter-overridden) method counts for the early-return
936    // check. Sanitized methods without adapters are skipped in the loops below, so they do not
937    // contribute real content to the impl block.
938    let has_emittable_instance = instance
939        .iter()
940        .any(|m| !m.sanitized || adapter_bodies.contains_key(&format!("{}.{}", typ.name, m.name)));
941    let has_emittable_statics = statics
942        .iter()
943        .any(|m| !m.sanitized || adapter_bodies.contains_key(&format!("{}.{}", typ.name, m.name)));
944    if !has_emittable_instance && !has_emittable_statics && typ.fields.is_empty() {
945        return String::new();
946    }
947
948    let prefixed_name = format!("{}{}", cfg.type_name_prefix, typ.name);
949    let mut out = String::with_capacity(2048);
950    if let Some(block_attr) = cfg.method_block_attr {
951        writeln!(out, "#[{block_attr}]").ok();
952    }
953    writeln!(out, "impl {prefixed_name} {{").ok();
954
955    // Constructor — suppressed when the backend handles construction via a separate free
956    // function (e.g. extendr kwargs constructor) or when there are no fields.
957    if !typ.fields.is_empty() && !cfg.skip_impl_constructor {
958        out.push_str(&gen_constructor_with_renames(typ, mapper, cfg, field_renames));
959        out.push_str("\n\n");
960    }
961
962    // Instance methods
963    let empty_mutex_types: AHashSet<String> = AHashSet::new();
964    for m in &instance {
965        // Skip sanitized methods that have no adapter override — they cannot be delegated
966        // and emitting an unimplemented stub pollutes the public API with dead placeholders.
967        // Adapter bodies are explicit overrides and always take precedence.
968        let adapter_key = format!("{}.{}", typ.name, m.name);
969        if m.sanitized && !adapter_bodies.contains_key(&adapter_key) {
970            continue;
971        }
972        out.push_str(&gen_method(
973            m,
974            mapper,
975            cfg,
976            typ,
977            false,
978            opaque_types,
979            &empty_mutex_types,
980            adapter_bodies,
981        ));
982        out.push_str("\n\n");
983    }
984
985    // Static methods
986    for m in &statics {
987        // Skip sanitized static methods that have no adapter override.
988        let adapter_key = format!("{}.{}", typ.name, m.name);
989        if m.sanitized && !adapter_bodies.contains_key(&adapter_key) {
990            continue;
991        }
992        out.push_str(&gen_static_method(
993            m,
994            mapper,
995            cfg,
996            typ,
997            adapter_bodies,
998            opaque_types,
999            &empty_mutex_types,
1000        ));
1001        out.push_str("\n\n");
1002    }
1003
1004    // Trim trailing newlines inside impl block
1005    let trimmed = out.trim_end();
1006    let mut result = trimmed.to_string();
1007    result.push_str("\n}");
1008    result
1009}
1010
1011/// Generate a full impl block for an opaque type, delegating methods to `self.inner`.
1012///
1013/// `opaque_types` is the set of type names that are opaque wrappers (use `Arc<inner>`).
1014/// This is needed so that return-type wrapping uses the correct pattern for cross-type returns.
1015/// `mutex_types` is the subset of opaque types whose inner field uses `Arc<Mutex<T>>`;
1016/// method dispatch uses `.lock().unwrap()` for these types.
1017pub fn gen_opaque_impl_block(
1018    typ: &TypeDef,
1019    mapper: &dyn TypeMapper,
1020    cfg: &RustBindingConfig,
1021    opaque_types: &AHashSet<String>,
1022    mutex_types: &AHashSet<String>,
1023    adapter_bodies: &AdapterBodies,
1024) -> String {
1025    let (instance, statics) = partition_methods(&typ.methods);
1026    // Compute effective (non-sanitized or adapter-overridden) method counts.
1027    let has_emittable_instance = instance
1028        .iter()
1029        .any(|m| !m.sanitized || adapter_bodies.contains_key(&format!("{}.{}", typ.name, m.name)));
1030    let has_emittable_statics = statics
1031        .iter()
1032        .any(|m| !m.sanitized || adapter_bodies.contains_key(&format!("{}.{}", typ.name, m.name)));
1033    if !has_emittable_instance && !has_emittable_statics {
1034        return String::new();
1035    }
1036
1037    let mut out = String::with_capacity(2048);
1038    let prefixed_name = format!("{}{}", cfg.type_name_prefix, typ.name);
1039    if let Some(block_attr) = cfg.method_block_attr {
1040        writeln!(out, "#[{block_attr}]").ok();
1041    }
1042    writeln!(out, "impl {prefixed_name} {{").ok();
1043
1044    // Instance methods — delegate to self.inner
1045    for m in &instance {
1046        // Skip sanitized methods that have no adapter override — they cannot be delegated
1047        // and emitting an unimplemented stub pollutes the public API with dead placeholders.
1048        let adapter_key = format!("{}.{}", typ.name, m.name);
1049        if m.sanitized && !adapter_bodies.contains_key(&adapter_key) {
1050            continue;
1051        }
1052        out.push_str(&gen_method(
1053            m,
1054            mapper,
1055            cfg,
1056            typ,
1057            true,
1058            opaque_types,
1059            mutex_types,
1060            adapter_bodies,
1061        ));
1062        out.push_str("\n\n");
1063    }
1064
1065    // Static methods
1066    for m in &statics {
1067        // Skip sanitized static methods that have no adapter override.
1068        let adapter_key = format!("{}.{}", typ.name, m.name);
1069        if m.sanitized && !adapter_bodies.contains_key(&adapter_key) {
1070            continue;
1071        }
1072        out.push_str(&gen_static_method(
1073            m,
1074            mapper,
1075            cfg,
1076            typ,
1077            adapter_bodies,
1078            opaque_types,
1079            mutex_types,
1080        ));
1081        out.push_str("\n\n");
1082    }
1083
1084    let trimmed = out.trim_end();
1085    let mut result = trimmed.to_string();
1086    result.push_str("\n}");
1087    result
1088}