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