Skip to main content

alef_codegen/generators/
methods.rs

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