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_serde_let_bindings,
4    gen_unimplemented_body, has_named_params, is_simple_non_opaque_param, wrap_return,
5};
6use crate::generators::{AdapterBodies, AsyncPattern, RustBindingConfig};
7use crate::shared::{constructor_parts, function_params, function_sig_defaults, partition_methods};
8use crate::type_mapper::TypeMapper;
9use ahash::AHashSet;
10use alef_core::ir::{MethodDef, TypeDef, TypeRef};
11use std::fmt::Write;
12
13/// Returns true when `name` matches a known trait method that would trigger
14/// `clippy::should_implement_trait`.
15pub fn is_trait_method_name(name: &str) -> bool {
16    crate::generators::TRAIT_METHOD_NAMES.contains(&name)
17}
18
19/// Generate a constructor method.
20pub fn gen_constructor(typ: &TypeDef, mapper: &dyn TypeMapper, cfg: &RustBindingConfig) -> String {
21    let map_fn = |ty: &alef_core::ir::TypeRef| mapper.map_type(ty);
22
23    // For types with has_default, generate optional kwargs-style constructor
24    let (param_list, sig_defaults, assignments) = if typ.has_default {
25        crate::shared::config_constructor_parts_with_options(&typ.fields, &map_fn, cfg.option_duration_on_defaults)
26    } else {
27        constructor_parts(&typ.fields, &map_fn)
28    };
29
30    let mut out = String::with_capacity(512);
31    // Per-item clippy suppression: too_many_arguments when >7 params
32    if typ.fields.len() > 7 {
33        writeln!(out, "    #[allow(clippy::too_many_arguments)]").ok();
34    }
35    writeln!(out, "    #[must_use]").ok();
36    if cfg.needs_signature {
37        writeln!(
38            out,
39            "    {}{}{}",
40            cfg.signature_prefix, sig_defaults, cfg.signature_suffix
41        )
42        .ok();
43    }
44    write!(
45        out,
46        "    {}\n    pub fn new({param_list}) -> Self {{\n        Self {{ {assignments} }}\n    }}",
47        cfg.constructor_attr
48    )
49    .ok();
50    out
51}
52
53/// Generate an instance method.
54///
55/// When `is_opaque` is true, generates delegation to `self.inner` via Arc clone
56/// instead of converting self to core type.
57///
58/// `opaque_types` is the set of opaque type names, used for correct return wrapping.
59pub fn gen_method(
60    method: &MethodDef,
61    mapper: &dyn TypeMapper,
62    cfg: &RustBindingConfig,
63    typ: &TypeDef,
64    is_opaque: bool,
65    opaque_types: &AHashSet<String>,
66    adapter_bodies: &AdapterBodies,
67) -> String {
68    let type_name = &typ.name;
69    // Use the full rust_path (with hyphens replaced by underscores) for core type references
70    let core_type_path = typ.rust_path.replace('-', "_");
71
72    let map_fn = |ty: &alef_core::ir::TypeRef| mapper.map_type(ty);
73    let params = function_params(&method.params, &map_fn);
74    let return_type = mapper.map_type(&method.return_type);
75    let ret = mapper.wrap_return(&return_type, method.error_type.is_some());
76
77    let call_args = gen_call_args(&method.params, opaque_types);
78
79    let is_owned_receiver = matches!(method.receiver.as_ref(), Some(alef_core::ir::ReceiverKind::Owned));
80
81    // Auto-delegate opaque methods: unwrap Arc for params, wrap Arc for returns.
82    // Owned receivers require the type to implement Clone (builder pattern).
83    // Async methods are allowed — gen_async_body handles them below.
84    let opaque_can_delegate = is_opaque
85        && !method.sanitized
86        && (!is_owned_receiver || typ.is_clone)
87        && method
88            .params
89            .iter()
90            .all(|p| !p.sanitized && crate::shared::is_opaque_delegatable_type(&p.ty))
91        && crate::shared::is_opaque_delegatable_type(&method.return_type);
92
93    // Build the core call expression: opaque types delegate to self.inner directly,
94    // non-opaque types convert self to core type first.
95    let make_core_call = |method_name: &str| -> String {
96        if is_opaque {
97            if is_owned_receiver {
98                // Owned receiver: clone out of Arc to get an owned value
99                format!("(*self.inner).clone().{method_name}({call_args})")
100            } else {
101                format!("self.inner.{method_name}({call_args})")
102            }
103        } else {
104            format!("{core_type_path}::from(self.clone()).{method_name}({call_args})")
105        }
106    };
107
108    // For async opaque methods, we clone the Arc before moving into the future.
109    let make_async_core_call = |method_name: &str| -> String {
110        if is_opaque {
111            format!("inner.{method_name}({call_args})")
112        } else {
113            format!("{core_type_path}::from(self.clone()).{method_name}({call_args})")
114        }
115    };
116
117    // Generate the body: convert self to core type, call method, convert result back
118    //
119    // For opaque types, wrap the return value appropriately:
120    //   - Named(self) → Self { inner: Arc::new(result) }
121    //   - Named(other) → OtherType::from(result)
122    //   - primitives/String/Vec/Unit → pass through
123    let result_expr = apply_return_newtype_unwrap("result", &method.return_newtype_wrapper);
124    let async_result_wrap = if is_opaque {
125        wrap_return(
126            &result_expr,
127            &method.return_type,
128            type_name,
129            opaque_types,
130            is_opaque,
131            method.returns_ref,
132            method.returns_cow,
133        )
134    } else {
135        // For non-opaque types, only use From conversion if the return type is simple
136        // enough. Named return types may not have a From impl.
137        match &method.return_type {
138            TypeRef::Named(_) | TypeRef::Json => format!("{result_expr}.into()"),
139            _ => result_expr.clone(),
140        }
141    };
142
143    let body = if !opaque_can_delegate {
144        // Check if an adapter provides the body
145        let adapter_key = format!("{}.{}", type_name, method.name);
146        if let Some(adapter_body) = adapter_bodies.get(&adapter_key) {
147            adapter_body.clone()
148        } else if cfg.has_serde
149            && is_opaque
150            && !method.sanitized
151            && has_named_params(&method.params, opaque_types)
152            && method.error_type.is_some()
153            && crate::shared::is_opaque_delegatable_type(&method.return_type)
154        {
155            // Serde-based param conversion for opaque methods with non-opaque Named params.
156            let err_conv = match cfg.async_pattern {
157                AsyncPattern::Pyo3FutureIntoPy => {
158                    ".map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))"
159                }
160                AsyncPattern::NapiNativeAsync => {
161                    ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))"
162                }
163                AsyncPattern::WasmNativeAsync => ".map_err(|e| JsValue::from_str(&e.to_string()))",
164                _ => ".map_err(|e| e.to_string())",
165            };
166            let serde_bindings =
167                gen_serde_let_bindings(&method.params, opaque_types, cfg.core_import, err_conv, "        ");
168            let serde_call_args = gen_call_args_with_let_bindings(&method.params, opaque_types);
169            let core_call = format!("self.inner.{}({serde_call_args})", method.name);
170            if matches!(method.return_type, TypeRef::Unit) {
171                format!("{serde_bindings}{core_call}{err_conv}?;\n        Ok(())")
172            } else {
173                let wrap = wrap_return(
174                    "result",
175                    &method.return_type,
176                    type_name,
177                    opaque_types,
178                    is_opaque,
179                    method.returns_ref,
180                    method.returns_cow,
181                );
182                format!("{serde_bindings}let result = {core_call}{err_conv}?;\n        Ok({wrap})")
183            }
184        } else if !is_opaque
185            && !method.sanitized
186            && method
187                .params
188                .iter()
189                .all(|p| !p.sanitized && is_simple_non_opaque_param(&p.ty))
190            && crate::shared::is_delegatable_return(&method.return_type)
191        {
192            // Non-opaque delegation: construct core type field-by-field, call method, convert back.
193            // Sanitized fields use Default::default() (lossy but functional for builder pattern).
194            let is_ref_mut = matches!(method.receiver.as_ref(), Some(alef_core::ir::ReceiverKind::RefMut));
195            let field_conversions = if is_ref_mut {
196                gen_lossy_binding_to_core_fields_mut(typ, cfg.core_import)
197            } else {
198                gen_lossy_binding_to_core_fields(typ, cfg.core_import)
199            };
200            let core_call = format!("core_self.{}({call_args})", method.name);
201            let newtype_suffix = if method.return_newtype_wrapper.is_some() {
202                ".0"
203            } else {
204                ""
205            };
206            let result_wrap = match &method.return_type {
207                // When returns_cow=true the core returns Cow<'_, T>: call .into_owned() to
208                // obtain an owned T before the binding→core From conversion.
209                // When returns_ref=true (or &T / Cow<'_, T> via the old flag), same treatment.
210                TypeRef::Named(n) if n == type_name && (method.returns_cow || method.returns_ref) => {
211                    ".into_owned().into()".to_string()
212                }
213                TypeRef::Named(_) if method.returns_cow || method.returns_ref => ".into_owned().into()".to_string(),
214                TypeRef::Named(n) if n == type_name => ".into()".to_string(),
215                TypeRef::Named(_) => ".into()".to_string(),
216                TypeRef::String | TypeRef::Bytes | TypeRef::Path => {
217                    if method.returns_ref {
218                        ".to_owned()".to_string()
219                    } else {
220                        ".into()".to_string()
221                    }
222                }
223                // Optional<Named>: when core returns Option<&T>, need .map(|v| v.clone().into())
224                TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Named(_)) => {
225                    if method.returns_ref {
226                        ".map(|v| v.clone().into())".to_string()
227                    } else {
228                        ".map(Into::into)".to_string()
229                    }
230                }
231                // Optional<String>: when core returns Option<&str>, need .map(|v| v.to_owned())
232                TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::String | TypeRef::Bytes) => {
233                    if method.returns_ref {
234                        ".map(|v| v.to_owned())".to_string()
235                    } else {
236                        String::new()
237                    }
238                }
239                _ => String::new(),
240            };
241            if method.error_type.is_some() {
242                let err_conv = match cfg.async_pattern {
243                    AsyncPattern::Pyo3FutureIntoPy => {
244                        ".map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))"
245                    }
246                    AsyncPattern::NapiNativeAsync => {
247                        ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))"
248                    }
249                    AsyncPattern::WasmNativeAsync => ".map_err(|e| JsValue::from_str(&e.to_string()))",
250                    _ => ".map_err(|e| e.to_string())",
251                };
252                format!(
253                    "{field_conversions}let result = {core_call}{err_conv}?;\n        Ok(result{newtype_suffix}{result_wrap})"
254                )
255            } else {
256                format!("{field_conversions}{core_call}{newtype_suffix}{result_wrap}")
257            }
258        } else {
259            gen_unimplemented_body(
260                &method.return_type,
261                &format!("{type_name}.{}", method.name),
262                method.error_type.is_some(),
263                cfg,
264                &method.params,
265            )
266        }
267    } else if method.is_async {
268        let inner_clone_line = if is_opaque {
269            "let inner = self.inner.clone();\n        "
270        } else {
271            ""
272        };
273        let core_call_str = make_async_core_call(&method.name);
274        gen_async_body(
275            &core_call_str,
276            cfg,
277            method.error_type.is_some(),
278            &async_result_wrap,
279            is_opaque,
280            inner_clone_line,
281            matches!(method.return_type, TypeRef::Unit),
282        )
283    } else {
284        let core_call = make_core_call(&method.name);
285        if method.error_type.is_some() {
286            // Backend-specific error conversion
287            let err_conv = match cfg.async_pattern {
288                AsyncPattern::Pyo3FutureIntoPy => {
289                    ".map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))"
290                }
291                AsyncPattern::NapiNativeAsync => {
292                    ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))"
293                }
294                AsyncPattern::WasmNativeAsync => ".map_err(|e| JsValue::from_str(&e.to_string()))",
295                _ => ".map_err(|e| e.to_string())",
296            };
297            if is_opaque {
298                if matches!(method.return_type, TypeRef::Unit) {
299                    // Unit return: avoid let_unit_value by not binding the result
300                    format!("{core_call}{err_conv}?;\n        Ok(())")
301                } else {
302                    let wrap = wrap_return(
303                        &result_expr,
304                        &method.return_type,
305                        type_name,
306                        opaque_types,
307                        is_opaque,
308                        method.returns_ref,
309                        method.returns_cow,
310                    );
311                    format!("let result = {core_call}{err_conv}?;\n        Ok({wrap})")
312                }
313            } else {
314                format!("{core_call}{err_conv}")
315            }
316        } else if is_opaque {
317            let unwrapped_call = apply_return_newtype_unwrap(&core_call, &method.return_newtype_wrapper);
318            wrap_return(
319                &unwrapped_call,
320                &method.return_type,
321                type_name,
322                opaque_types,
323                is_opaque,
324                method.returns_ref,
325                method.returns_cow,
326            )
327        } else {
328            core_call
329        }
330    };
331
332    let needs_py = method.is_async && cfg.async_pattern == AsyncPattern::Pyo3FutureIntoPy;
333    let self_param = match (needs_py, params.is_empty()) {
334        (true, true) => "&self, py: Python<'py>",
335        (true, false) => "&self, py: Python<'py>, ",
336        (false, true) => "&self",
337        (false, false) => "&self, ",
338    };
339
340    // For async PyO3 methods, override return type to PyResult<Bound<'py, PyAny>>
341    // and add the 'py lifetime generic on the method name.
342    let ret = if needs_py {
343        "PyResult<Bound<'py, PyAny>>".to_string()
344    } else {
345        ret
346    };
347    let method_lifetime = if needs_py { "<'py>" } else { "" };
348
349    // Wrap long signature if necessary
350    let (sig_start, sig_params, sig_end) = if self_param.len() + params.len() > 100 {
351        let wrapped_params = method
352            .params
353            .iter()
354            .map(|p| {
355                let ty = if p.optional {
356                    format!("Option<{}>", mapper.map_type(&p.ty))
357                } else {
358                    mapper.map_type(&p.ty)
359                };
360                format!("{}: {}", p.name, ty)
361            })
362            .collect::<Vec<_>>()
363            .join(",\n        ");
364        let py_param = if needs_py { "\n        py: Python<'py>," } else { "" };
365        (
366            format!(
367                "pub fn {}{method_lifetime}(\n        &self,{}\n        ",
368                method.name, py_param
369            ),
370            wrapped_params,
371            "\n    ) -> ".to_string(),
372        )
373    } else {
374        (
375            format!("pub fn {}{method_lifetime}({}", method.name, self_param),
376            params,
377            ") -> ".to_string(),
378        )
379    };
380
381    let mut out = String::with_capacity(1024);
382    // Per-item clippy suppression: too_many_arguments when >7 params (including &self and py)
383    let total_params = method.params.len() + 1 + if needs_py { 1 } else { 0 };
384    if total_params > 7 {
385        writeln!(out, "    #[allow(clippy::too_many_arguments)]").ok();
386    }
387    // Per-item clippy suppression: missing_errors_doc for Result-returning methods
388    if method.error_type.is_some() {
389        writeln!(out, "    #[allow(clippy::missing_errors_doc)]").ok();
390    }
391    // Per-item clippy suppression: should_implement_trait for trait-conflicting names
392    if is_trait_method_name(&method.name) {
393        writeln!(out, "    #[allow(clippy::should_implement_trait)]").ok();
394    }
395    if cfg.needs_signature {
396        let sig = function_sig_defaults(&method.params);
397        writeln!(out, "    {}{}{}", cfg.signature_prefix, sig, cfg.signature_suffix).ok();
398    }
399    write!(
400        out,
401        "    {}{}{}{} {{\n        \
402         {body}\n    }}",
403        sig_start, sig_params, sig_end, ret,
404    )
405    .ok();
406    out
407}
408
409/// Generate a static method.
410pub fn gen_static_method(
411    method: &MethodDef,
412    mapper: &dyn TypeMapper,
413    cfg: &RustBindingConfig,
414    typ: &TypeDef,
415    adapter_bodies: &AdapterBodies,
416    opaque_types: &AHashSet<String>,
417) -> String {
418    let type_name = &typ.name;
419    // Use the full rust_path (with hyphens replaced by underscores) for core type references
420    let core_type_path = typ.rust_path.replace('-', "_");
421    let map_fn = |ty: &alef_core::ir::TypeRef| mapper.map_type(ty);
422    let params = function_params(&method.params, &map_fn);
423    let return_type = mapper.map_type(&method.return_type);
424    let ret = mapper.wrap_return(&return_type, method.error_type.is_some());
425
426    let call_args = gen_call_args(&method.params, opaque_types);
427
428    let can_delegate = crate::shared::can_auto_delegate(method, opaque_types);
429
430    let body = if !can_delegate {
431        // Check if an adapter provides the body
432        let adapter_key = format!("{}.{}", type_name, method.name);
433        if let Some(adapter_body) = adapter_bodies.get(&adapter_key) {
434            adapter_body.clone()
435        } else {
436            gen_unimplemented_body(
437                &method.return_type,
438                &format!("{type_name}::{}", method.name),
439                method.error_type.is_some(),
440                cfg,
441                &method.params,
442            )
443        }
444    } else if method.is_async {
445        let core_call = format!("{core_type_path}::{}({call_args})", method.name);
446        let return_wrap = format!("{return_type}::from(result)");
447        gen_async_body(
448            &core_call,
449            cfg,
450            method.error_type.is_some(),
451            &return_wrap,
452            false,
453            "",
454            matches!(method.return_type, TypeRef::Unit),
455        )
456    } else {
457        let core_call = format!("{core_type_path}::{}({call_args})", method.name);
458        if method.error_type.is_some() {
459            // Backend-specific error conversion
460            let err_conv = match cfg.async_pattern {
461                AsyncPattern::Pyo3FutureIntoPy => {
462                    ".map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))"
463                }
464                AsyncPattern::NapiNativeAsync => {
465                    ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))"
466                }
467                AsyncPattern::WasmNativeAsync => ".map_err(|e| JsValue::from_str(&e.to_string()))",
468                _ => ".map_err(|e| e.to_string())",
469            };
470            // Wrap the Ok value if the return type needs conversion (e.g. PathBuf→String)
471            let val_expr = apply_return_newtype_unwrap("val", &method.return_newtype_wrapper);
472            let wrapped = wrap_return(
473                &val_expr,
474                &method.return_type,
475                type_name,
476                opaque_types,
477                typ.is_opaque,
478                method.returns_ref,
479                method.returns_cow,
480            );
481            if wrapped == val_expr {
482                format!("{core_call}{err_conv}")
483            } else {
484                format!("{core_call}.map(|val| {wrapped}){err_conv}")
485            }
486        } else {
487            // Wrap return value for non-error case too (e.g. PathBuf→String)
488            let unwrapped_call = apply_return_newtype_unwrap(&core_call, &method.return_newtype_wrapper);
489            wrap_return(
490                &unwrapped_call,
491                &method.return_type,
492                type_name,
493                opaque_types,
494                typ.is_opaque,
495                method.returns_ref,
496                method.returns_cow,
497            )
498        }
499    };
500
501    let static_needs_py = method.is_async && cfg.async_pattern == AsyncPattern::Pyo3FutureIntoPy;
502
503    // For async PyO3 static methods, override return type and add lifetime generic.
504    let ret = if static_needs_py {
505        "PyResult<Bound<'py, PyAny>>".to_string()
506    } else {
507        ret
508    };
509    let method_lifetime = if static_needs_py { "<'py>" } else { "" };
510
511    // Wrap long signature if necessary
512    let (sig_start, sig_params, sig_end) = if params.len() > 100 {
513        let wrapped_params = method
514            .params
515            .iter()
516            .map(|p| {
517                let ty = if p.optional {
518                    format!("Option<{}>", mapper.map_type(&p.ty))
519                } else {
520                    mapper.map_type(&p.ty)
521                };
522                format!("{}: {}", p.name, ty)
523            })
524            .collect::<Vec<_>>()
525            .join(",\n        ");
526        // For async PyO3, add py parameter
527        if static_needs_py {
528            (
529                format!("pub fn {}{method_lifetime}(py: Python<'py>,\n        ", method.name),
530                wrapped_params,
531                "\n    ) -> ".to_string(),
532            )
533        } else {
534            (
535                format!("pub fn {}(\n        ", method.name),
536                wrapped_params,
537                "\n    ) -> ".to_string(),
538            )
539        }
540    } else if static_needs_py {
541        (
542            format!("pub fn {}{method_lifetime}(py: Python<'py>, ", method.name),
543            params,
544            ") -> ".to_string(),
545        )
546    } else {
547        (format!("pub fn {}(", method.name), params, ") -> ".to_string())
548    };
549
550    let mut out = String::with_capacity(1024);
551    // Per-item clippy suppression: too_many_arguments when >7 params (including py)
552    let total_params = method.params.len() + if static_needs_py { 1 } else { 0 };
553    if total_params > 7 {
554        writeln!(out, "    #[allow(clippy::too_many_arguments)]").ok();
555    }
556    // Per-item clippy suppression: missing_errors_doc for Result-returning methods
557    if method.error_type.is_some() {
558        writeln!(out, "    #[allow(clippy::missing_errors_doc)]").ok();
559    }
560    // Per-item clippy suppression: should_implement_trait for trait-conflicting names
561    if is_trait_method_name(&method.name) {
562        writeln!(out, "    #[allow(clippy::should_implement_trait)]").ok();
563    }
564    if let Some(attr) = cfg.static_attr {
565        writeln!(out, "    #[{attr}]").ok();
566    }
567    if cfg.needs_signature {
568        let sig = function_sig_defaults(&method.params);
569        writeln!(out, "    {}{}{}", cfg.signature_prefix, sig, cfg.signature_suffix).ok();
570    }
571    write!(
572        out,
573        "    {}{}{}{} {{\n        \
574         {body}\n    }}",
575        sig_start, sig_params, sig_end, ret,
576    )
577    .ok();
578    out
579}
580
581/// Generate a full methods impl block (non-opaque types).
582pub fn gen_impl_block(
583    typ: &TypeDef,
584    mapper: &dyn TypeMapper,
585    cfg: &RustBindingConfig,
586    adapter_bodies: &AdapterBodies,
587    opaque_types: &AHashSet<String>,
588) -> String {
589    let (instance, statics) = partition_methods(&typ.methods);
590    if instance.is_empty() && statics.is_empty() && typ.fields.is_empty() {
591        return String::new();
592    }
593
594    let prefixed_name = format!("{}{}", cfg.type_name_prefix, typ.name);
595    let mut out = String::with_capacity(2048);
596    if let Some(block_attr) = cfg.method_block_attr {
597        writeln!(out, "#[{block_attr}]").ok();
598    }
599    writeln!(out, "impl {prefixed_name} {{").ok();
600
601    // Constructor
602    if !typ.fields.is_empty() {
603        out.push_str(&gen_constructor(typ, mapper, cfg));
604        out.push_str("\n\n");
605    }
606
607    // Instance methods
608    for m in &instance {
609        out.push_str(&gen_method(m, mapper, cfg, typ, false, opaque_types, adapter_bodies));
610        out.push_str("\n\n");
611    }
612
613    // Static methods
614    for m in &statics {
615        out.push_str(&gen_static_method(m, mapper, cfg, typ, adapter_bodies, opaque_types));
616        out.push_str("\n\n");
617    }
618
619    // Trim trailing newlines inside impl block
620    let trimmed = out.trim_end();
621    let mut result = trimmed.to_string();
622    result.push_str("\n}");
623    result
624}
625
626/// Generate a full impl block for an opaque type, delegating methods to `self.inner`.
627///
628/// `opaque_types` is the set of type names that are opaque wrappers (use `Arc<inner>`).
629/// This is needed so that return-type wrapping uses the correct pattern for cross-type returns.
630pub fn gen_opaque_impl_block(
631    typ: &TypeDef,
632    mapper: &dyn TypeMapper,
633    cfg: &RustBindingConfig,
634    opaque_types: &AHashSet<String>,
635    adapter_bodies: &AdapterBodies,
636) -> String {
637    let (instance, statics) = partition_methods(&typ.methods);
638    if instance.is_empty() && statics.is_empty() {
639        return String::new();
640    }
641
642    let mut out = String::with_capacity(2048);
643    let prefixed_name = format!("{}{}", cfg.type_name_prefix, typ.name);
644    if let Some(block_attr) = cfg.method_block_attr {
645        writeln!(out, "#[{block_attr}]").ok();
646    }
647    writeln!(out, "impl {prefixed_name} {{").ok();
648
649    // Instance methods — delegate to self.inner
650    for m in &instance {
651        out.push_str(&gen_method(m, mapper, cfg, typ, true, opaque_types, adapter_bodies));
652        out.push_str("\n\n");
653    }
654
655    // Static methods
656    for m in &statics {
657        out.push_str(&gen_static_method(m, mapper, cfg, typ, adapter_bodies, opaque_types));
658        out.push_str("\n\n");
659    }
660
661    let trimmed = out.trim_end();
662    let mut result = trimmed.to_string();
663    result.push_str("\n}");
664    result
665}