Skip to main content

alef_codegen/generators/
binding_helpers.rs

1use crate::generators::{AsyncPattern, RustBindingConfig};
2use ahash::AHashSet;
3use alef_core::ir::{CoreWrapper, ParamDef, TypeDef, TypeRef};
4use std::fmt::Write;
5
6/// Helper: wrap an opaque inner value in the correct smart pointer expression.
7///
8/// - Plain opaque types use `Arc::new(val)`.
9/// - Mutex-wrapped opaque types use `Arc::new(std::sync::Mutex::new(val))`.
10fn arc_wrap(val: &str, name: &str, mutex_types: &AHashSet<String>) -> String {
11    if mutex_types.contains(name) {
12        format!("Arc::new(std::sync::Mutex::new({val}))")
13    } else {
14        format!("Arc::new({val})")
15    }
16}
17
18/// Wrap a core-call result for opaque delegation methods.
19///
20/// - `TypeRef::Named(n)` where `n == type_name` → re-wrap in `Self { inner: Arc::new(...) }`
21/// - `TypeRef::Named(n)` where `n` is another opaque type → wrap in `{n} { inner: Arc::new(...) }`
22/// - `TypeRef::Named(n)` where `n` is a non-opaque type → `todo!()` placeholder (From may not exist)
23/// - Everything else (primitives, String, Vec, etc.) → pass through unchanged
24/// - `TypeRef::Unit` → pass through unchanged
25///
26/// When `returns_cow` is true the core method returns `Cow<'_, T>`. `.into_owned()` is emitted
27/// before any further type conversion to obtain an owned `T`.
28///
29/// `mutex_types` identifies opaque types that use `Arc<Mutex<T>>` instead of `Arc<T>`, so
30/// constructor expressions use `Arc::new(Mutex::new(...))` where needed.
31#[allow(clippy::too_many_arguments)]
32pub fn wrap_return_with_mutex(
33    expr: &str,
34    return_type: &TypeRef,
35    type_name: &str,
36    opaque_types: &AHashSet<String>,
37    mutex_types: &AHashSet<String>,
38    self_is_opaque: bool,
39    returns_ref: bool,
40    returns_cow: bool,
41) -> String {
42    let self_arc = arc_wrap("", type_name, mutex_types); // used for pattern matching only
43    let _ = self_arc; // just to reference mutex_types in context
44    match return_type {
45        TypeRef::Named(n) if n == type_name && self_is_opaque => {
46            let inner = if returns_cow {
47                format!("{expr}.into_owned()")
48            } else if returns_ref {
49                format!("{expr}.clone()")
50            } else {
51                expr.to_string()
52            };
53            format!("Self {{ inner: {} }}", arc_wrap(&inner, type_name, mutex_types))
54        }
55        TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
56            let inner = if returns_cow {
57                format!("{expr}.into_owned()")
58            } else if returns_ref {
59                format!("{expr}.clone()")
60            } else {
61                expr.to_string()
62            };
63            format!("{n} {{ inner: {} }}", arc_wrap(&inner, n, mutex_types))
64        }
65        TypeRef::Named(_) => {
66            // Non-opaque Named return type — use .into() for core→binding From conversion.
67            // When the core returns a Cow, call .into_owned() first to get an owned T.
68            // When the core returns a reference, clone first since From<&T> typically doesn't exist.
69            // NOTE: If this type was sanitized to String in the binding, From won't exist.
70            // The calling backend should check method.sanitized before delegating.
71            // This code assumes non-sanitized Named types have From impls.
72            if returns_cow {
73                format!("{expr}.into_owned().into()")
74            } else if returns_ref {
75                format!("{expr}.clone().into()")
76            } else {
77                format!("{expr}.into()")
78            }
79        }
80        // String/Bytes: only convert when the core returns a reference (&str→String, &[u8]→Vec<u8>).
81        // When owned (returns_ref=false), both sides are already String/Vec<u8> — skip .into().
82        TypeRef::String | TypeRef::Bytes => {
83            if returns_ref {
84                format!("{expr}.into()")
85            } else {
86                expr.to_string()
87            }
88        }
89        // Path: PathBuf→String needs to_string_lossy, &Path→String too
90        TypeRef::Path => format!("{expr}.to_string_lossy().to_string()"),
91        // Duration: core returns std::time::Duration, binding uses u64 (millis)
92        TypeRef::Duration => format!("{expr}.as_millis() as u64"),
93        // Json: serde_json::Value needs serialization to string
94        TypeRef::Json => format!("{expr}.to_string()"),
95        // Optional: wrap inner conversion in .map(...)
96        TypeRef::Optional(inner) => match inner.as_ref() {
97            TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
98                let wrap = arc_wrap("v", n, mutex_types);
99                if returns_ref {
100                    format!(
101                        "{expr}.map(|v| {n} {{ inner: {} }})",
102                        arc_wrap("v.clone()", n, mutex_types)
103                    )
104                } else {
105                    format!("{expr}.map(|v| {n} {{ inner: {wrap} }})")
106                }
107            }
108            TypeRef::Named(_) => {
109                if returns_ref {
110                    format!("{expr}.map(|v| v.clone().into())")
111                } else {
112                    format!("{expr}.map(Into::into)")
113                }
114            }
115            TypeRef::Path => {
116                format!("{expr}.map(Into::into)")
117            }
118            TypeRef::String | TypeRef::Bytes => {
119                if returns_ref {
120                    format!("{expr}.map(Into::into)")
121                } else {
122                    expr.to_string()
123                }
124            }
125            TypeRef::Duration => format!("{expr}.map(|d| d.as_millis() as u64)"),
126            TypeRef::Json => format!("{expr}.map(ToString::to_string)"),
127            // Optional<Vec<Named>>: convert each element in the inner Vec
128            TypeRef::Vec(vec_inner) => match vec_inner.as_ref() {
129                TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
130                    if returns_ref {
131                        let wrap = arc_wrap("x.clone()", n, mutex_types);
132                        format!("{expr}.map(|v| v.into_iter().map(|x| {n} {{ inner: {wrap} }}).collect())")
133                    } else {
134                        let wrap = arc_wrap("x", n, mutex_types);
135                        format!("{expr}.map(|v| v.into_iter().map(|x| {n} {{ inner: {wrap} }}).collect())")
136                    }
137                }
138                TypeRef::Named(_) => {
139                    if returns_ref {
140                        format!("{expr}.map(|v| v.into_iter().map(|x| x.clone().into()).collect())")
141                    } else {
142                        format!("{expr}.map(|v| v.into_iter().map(Into::into).collect())")
143                    }
144                }
145                _ => expr.to_string(),
146            },
147            _ => expr.to_string(),
148        },
149        // Vec: map each element through the appropriate conversion
150        TypeRef::Vec(inner) => match inner.as_ref() {
151            TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
152                if returns_ref {
153                    let wrap = arc_wrap("v.clone()", n, mutex_types);
154                    format!("{expr}.into_iter().map(|v| {n} {{ inner: {wrap} }}).collect()")
155                } else {
156                    let wrap = arc_wrap("v", n, mutex_types);
157                    format!("{expr}.into_iter().map(|v| {n} {{ inner: {wrap} }}).collect()")
158                }
159            }
160            TypeRef::Named(_) => {
161                if returns_ref {
162                    format!("{expr}.into_iter().map(|v| v.clone().into()).collect()")
163                } else {
164                    format!("{expr}.into_iter().map(Into::into).collect()")
165                }
166            }
167            TypeRef::Path => {
168                format!("{expr}.into_iter().map(Into::into).collect()")
169            }
170            TypeRef::String | TypeRef::Bytes => {
171                if returns_ref {
172                    format!("{expr}.into_iter().map(Into::into).collect()")
173                } else {
174                    expr.to_string()
175                }
176            }
177            _ => expr.to_string(),
178        },
179        _ => expr.to_string(),
180    }
181}
182
183/// Wrap a core-call result for opaque delegation methods.
184///
185/// This is the backward-compatible wrapper that passes an empty `mutex_types` set.
186/// Use `wrap_return_with_mutex` when the type set contains mutex-wrapped opaque types.
187pub fn wrap_return(
188    expr: &str,
189    return_type: &TypeRef,
190    type_name: &str,
191    opaque_types: &AHashSet<String>,
192    self_is_opaque: bool,
193    returns_ref: bool,
194    returns_cow: bool,
195) -> String {
196    wrap_return_with_mutex(
197        expr,
198        return_type,
199        type_name,
200        opaque_types,
201        &AHashSet::new(),
202        self_is_opaque,
203        returns_ref,
204        returns_cow,
205    )
206}
207
208/// Unwrap a newtype return value when `return_newtype_wrapper` is set.
209///
210/// Core function returns a newtype (e.g. `NodeIndex(u32)`), but the binding return type
211/// is the inner type (e.g. `u32`). Access `.0` to unwrap the newtype.
212pub fn apply_return_newtype_unwrap(expr: &str, return_newtype_wrapper: &Option<String>) -> String {
213    match return_newtype_wrapper {
214        Some(_) => format!("({expr}).0"),
215        None => expr.to_string(),
216    }
217}
218
219/// Build call argument expressions from parameters.
220/// - Opaque Named types: unwrap Arc wrapper via `(*param.inner).clone()`
221/// - Non-opaque Named types: `.into()` for From conversion
222/// - String/Path/Bytes: `&param` since core functions typically take `&str`/`&Path`/`&[u8]`
223/// - Params with `newtype_wrapper` set: re-wrap the raw value in the newtype constructor
224///   (e.g., `NodeIndex(parent)`) since the binding resolved `NodeIndex(u32)` → `u32`.
225///
226/// NOTE: This function does not perform serde-based conversion. For Named params that lack
227/// From impls (e.g., due to sanitized fields), use `gen_serde_let_bindings` instead when
228/// `cfg.has_serde` is true, or fall back to `gen_unimplemented_body`.
229pub fn gen_call_args(params: &[ParamDef], opaque_types: &AHashSet<String>) -> String {
230    params
231        .iter()
232        .enumerate()
233        .map(|(idx, p)| {
234            let promoted = crate::shared::is_promoted_optional(params, idx);
235            // If a required param was promoted to optional, unwrap it before use.
236            // Note: promoted params that are not Optional<T> will NOT call .expect() because
237            // promoted refers to the PyO3 signature constraint, not the actual Rust type.
238            // The function_params logic wraps promoted params in Option<T>, making them truly optional.
239            let unwrap_suffix = if promoted && p.optional {
240                format!(".expect(\"'{}' is required\")", p.name)
241            } else {
242                String::new()
243            };
244            // If this param's type was resolved from a newtype (e.g. NodeIndex(u32) → u32),
245            // re-wrap the raw value back into the newtype when calling core.
246            if let Some(newtype_path) = &p.newtype_wrapper {
247                return if p.optional {
248                    format!("{}.map({newtype_path})", p.name)
249                } else if promoted {
250                    format!("{newtype_path}({}{})", p.name, unwrap_suffix)
251                } else {
252                    format!("{newtype_path}({})", p.name)
253                };
254            }
255            match &p.ty {
256                TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
257                    // Opaque type: borrow through Arc to get &CoreType
258                    if p.optional {
259                        format!("{}.as_ref().map(|v| &v.inner)", p.name)
260                    } else if promoted {
261                        format!("{}{}.inner.as_ref()", p.name, unwrap_suffix)
262                    } else {
263                        format!("&{}.inner", p.name)
264                    }
265                }
266                TypeRef::Named(_) => {
267                    if p.optional {
268                        if p.is_ref {
269                            // Option<T> (binding) -> Option<&CoreT>: use as_ref() only
270                            // The Into conversion must happen in a let binding to avoid E0716
271                            format!("{}.as_ref()", p.name)
272                        } else {
273                            format!("{}.map(Into::into)", p.name)
274                        }
275                    } else if promoted {
276                        format!("{}{}.into()", p.name, unwrap_suffix)
277                    } else {
278                        format!("{}.into()", p.name)
279                    }
280                }
281                // String → &str for core function calls when is_ref=true,
282                // or pass owned when is_ref=false (core takes String/impl Into<String>).
283                // For optional params: as_deref() when is_ref=true, pass owned when is_ref=false.
284                TypeRef::String | TypeRef::Char => {
285                    if p.optional {
286                        if p.is_ref {
287                            format!("{}.as_deref()", p.name)
288                        } else {
289                            p.name.clone()
290                        }
291                    } else if promoted {
292                        if p.is_ref {
293                            format!("&{}{}", p.name, unwrap_suffix)
294                        } else {
295                            format!("{}{}", p.name, unwrap_suffix)
296                        }
297                    } else if p.is_ref {
298                        format!("&{}", p.name)
299                    } else {
300                        p.name.clone()
301                    }
302                }
303                // Path → PathBuf/&Path for core function calls
304                TypeRef::Path => {
305                    if p.optional && p.is_ref {
306                        format!("{}.as_deref().map(std::path::Path::new)", p.name)
307                    } else if p.optional {
308                        format!("{}.map(std::path::PathBuf::from)", p.name)
309                    } else if promoted {
310                        format!("std::path::PathBuf::from({}{})", p.name, unwrap_suffix)
311                    } else if p.is_ref {
312                        format!("std::path::Path::new(&{})", p.name)
313                    } else {
314                        format!("std::path::PathBuf::from({})", p.name)
315                    }
316                }
317                TypeRef::Bytes => {
318                    if p.optional {
319                        if p.is_ref {
320                            format!("{}.as_deref()", p.name)
321                        } else {
322                            p.name.clone()
323                        }
324                    } else if promoted {
325                        // is_ref=true: pass &Vec<u8> (core takes &[u8])
326                        // is_ref=false: pass Vec<u8> (core takes owned Vec<u8>)
327                        if p.is_ref {
328                            format!("&{}{}", p.name, unwrap_suffix)
329                        } else {
330                            format!("{}{}", p.name, unwrap_suffix)
331                        }
332                    } else {
333                        // is_ref=true: pass &Vec<u8> (core takes &[u8])
334                        // is_ref=false: pass Vec<u8> (core takes owned Vec<u8>)
335                        if p.is_ref {
336                            format!("&{}", p.name)
337                        } else {
338                            p.name.clone()
339                        }
340                    }
341                }
342                // Duration: binding uses u64 (millis), core uses std::time::Duration
343                TypeRef::Duration => {
344                    if p.optional {
345                        format!("{}.map(std::time::Duration::from_millis)", p.name)
346                    } else if promoted {
347                        format!("std::time::Duration::from_millis({}{})", p.name, unwrap_suffix)
348                    } else {
349                        format!("std::time::Duration::from_millis({})", p.name)
350                    }
351                }
352                TypeRef::Json => {
353                    // JSON params: binding has String, core expects serde_json::Value
354                    if p.optional {
355                        format!("{}.as_ref().and_then(|s| serde_json::from_str(s).ok())", p.name)
356                    } else if promoted {
357                        format!("serde_json::from_str(&{}{}).unwrap_or_default()", p.name, unwrap_suffix)
358                    } else {
359                        format!("serde_json::from_str(&{}).unwrap_or_default()", p.name)
360                    }
361                }
362                TypeRef::Vec(inner) => {
363                    // Vec<Named>: convert each element via Into::into when used with let bindings
364                    if matches!(inner.as_ref(), TypeRef::Named(_)) {
365                        if p.optional {
366                            if p.is_ref {
367                                format!("{}.as_deref()", p.name)
368                            } else {
369                                p.name.clone()
370                            }
371                        } else if promoted {
372                            if p.is_ref {
373                                format!("&{}{}", p.name, unwrap_suffix)
374                            } else {
375                                format!("{}{}", p.name, unwrap_suffix)
376                            }
377                        } else if p.is_ref {
378                            format!("&{}", p.name)
379                        } else {
380                            p.name.clone()
381                        }
382                    } else if promoted {
383                        format!("{}{}", p.name, unwrap_suffix)
384                    } else if p.is_mut && p.optional {
385                        format!("{}.as_deref_mut()", p.name)
386                    } else if p.is_mut {
387                        format!("&mut {}", p.name)
388                    } else if p.is_ref && p.optional {
389                        format!("{}.as_deref()", p.name)
390                    } else if p.is_ref {
391                        format!("&{}", p.name)
392                    } else {
393                        p.name.clone()
394                    }
395                }
396                _ => {
397                    if promoted {
398                        format!("{}{}", p.name, unwrap_suffix)
399                    } else if p.is_mut && p.optional {
400                        format!("{}.as_deref_mut()", p.name)
401                    } else if p.is_mut {
402                        format!("&mut {}", p.name)
403                    } else if p.is_ref && p.optional {
404                        // Optional ref params: use as_deref() for slice/str coercion
405                        // Option<Vec<T>> -> Option<&[T]>, Option<String> -> Option<&str>
406                        format!("{}.as_deref()", p.name)
407                    } else if p.is_ref {
408                        format!("&{}", p.name)
409                    } else {
410                        p.name.clone()
411                    }
412                }
413            }
414        })
415        .collect::<Vec<_>>()
416        .join(", ")
417}
418
419/// Build call argument expressions using pre-bound let bindings for non-opaque Named params.
420/// Non-opaque Named params use `&{name}_core` references instead of `.into()`.
421pub fn gen_call_args_with_let_bindings(params: &[ParamDef], opaque_types: &AHashSet<String>) -> String {
422    params
423        .iter()
424        .enumerate()
425        .map(|(idx, p)| {
426            let promoted = crate::shared::is_promoted_optional(params, idx);
427            let unwrap_suffix = if promoted {
428                format!(".expect(\"'{}' is required\")", p.name)
429            } else {
430                String::new()
431            };
432            // If this param's type was resolved from a newtype, re-wrap when calling core.
433            if let Some(newtype_path) = &p.newtype_wrapper {
434                return if p.optional {
435                    format!("{}.map({newtype_path})", p.name)
436                } else if promoted {
437                    format!("{newtype_path}({}{})", p.name, unwrap_suffix)
438                } else {
439                    format!("{newtype_path}({})", p.name)
440                };
441            }
442            match &p.ty {
443                TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
444                    if p.optional {
445                        format!("{}.as_ref().map(|v| &v.inner)", p.name)
446                    } else if promoted {
447                        format!("{}{}.inner.as_ref()", p.name, unwrap_suffix)
448                    } else {
449                        format!("&{}.inner", p.name)
450                    }
451                }
452                TypeRef::Named(_) => {
453                    if p.optional && p.is_ref {
454                        // Let binding already created Option<&T> via .as_ref()
455                        format!("{}_core", p.name)
456                    } else if p.is_ref {
457                        // Let binding created T, need reference for call
458                        format!("&{}_core", p.name)
459                    } else {
460                        format!("{}_core", p.name)
461                    }
462                }
463                TypeRef::String | TypeRef::Char => {
464                    if p.optional {
465                        if p.is_ref {
466                            format!("{}.as_deref()", p.name)
467                        } else {
468                            p.name.clone()
469                        }
470                    } else if promoted {
471                        if p.is_ref {
472                            format!("&{}{}", p.name, unwrap_suffix)
473                        } else {
474                            format!("{}{}", p.name, unwrap_suffix)
475                        }
476                    } else if p.is_ref {
477                        format!("&{}", p.name)
478                    } else {
479                        p.name.clone()
480                    }
481                }
482                TypeRef::Path => {
483                    if promoted {
484                        format!("std::path::PathBuf::from({}{})", p.name, unwrap_suffix)
485                    } else if p.optional && p.is_ref {
486                        format!("{}.as_deref().map(std::path::Path::new)", p.name)
487                    } else if p.optional {
488                        format!("{}.map(std::path::PathBuf::from)", p.name)
489                    } else if p.is_ref {
490                        format!("std::path::Path::new(&{})", p.name)
491                    } else {
492                        format!("std::path::PathBuf::from({})", p.name)
493                    }
494                }
495                TypeRef::Bytes => {
496                    if p.optional {
497                        if p.is_ref {
498                            format!("{}.as_deref()", p.name)
499                        } else {
500                            p.name.clone()
501                        }
502                    } else if promoted {
503                        // is_ref=true: pass &Vec<u8> (core takes &[u8])
504                        // is_ref=false: pass Vec<u8> (core takes owned Vec<u8>)
505                        if p.is_ref {
506                            format!("&{}{}", p.name, unwrap_suffix)
507                        } else {
508                            format!("{}{}", p.name, unwrap_suffix)
509                        }
510                    } else {
511                        // is_ref=true: pass &Vec<u8> (core takes &[u8])
512                        // is_ref=false: pass Vec<u8> (core takes owned Vec<u8>)
513                        if p.is_ref {
514                            format!("&{}", p.name)
515                        } else {
516                            p.name.clone()
517                        }
518                    }
519                }
520                TypeRef::Duration => {
521                    if p.optional {
522                        format!("{}.map(std::time::Duration::from_millis)", p.name)
523                    } else if promoted {
524                        format!("std::time::Duration::from_millis({}{})", p.name, unwrap_suffix)
525                    } else {
526                        format!("std::time::Duration::from_millis({})", p.name)
527                    }
528                }
529                TypeRef::Vec(inner) => {
530                    // Sanitized Vec<tuple>: binding accepts Vec<String> (JSON-encoded tuples).
531                    // Let binding created {name}_core via JSON deserialization.
532                    if matches!(inner.as_ref(), TypeRef::String) && p.sanitized && p.original_type.is_some() {
533                        if p.optional && p.is_ref {
534                            format!("{}_core.as_deref()", p.name)
535                        } else if p.optional {
536                            format!("{}_core", p.name)
537                        } else if p.is_ref {
538                            format!("&{}_core", p.name)
539                        } else {
540                            format!("{}_core", p.name)
541                        }
542                    } else if matches!(inner.as_ref(), TypeRef::Named(_)) {
543                        // Vec<Named>: use let binding that converts each element
544                        if p.optional && p.is_ref {
545                            // Let binding creates Option<Vec<CoreType>>, use as_deref() to get Option<&[CoreType]>
546                            format!("{}_core.as_deref()", p.name)
547                        } else if p.optional {
548                            // Let binding creates Option<Vec<CoreType>>, no ref needed
549                            format!("{}_core", p.name)
550                        } else if p.is_ref {
551                            format!("&{}_core", p.name)
552                        } else {
553                            format!("{}_core", p.name)
554                        }
555                    } else if matches!(inner.as_ref(), TypeRef::String | TypeRef::Char) && p.is_ref {
556                        // Vec<String> with is_ref=true: core expects &[&str].
557                        // Convert via _refs intermediate binding.
558                        if p.optional {
559                            format!("{}.as_deref()", p.name)
560                        } else {
561                            format!("&{}_refs", p.name)
562                        }
563                    } else if promoted {
564                        format!("{}{}", p.name, unwrap_suffix)
565                    } else if p.is_ref && p.optional {
566                        format!("{}.as_deref()", p.name)
567                    } else if p.is_ref {
568                        format!("&{}", p.name)
569                    } else {
570                        p.name.clone()
571                    }
572                }
573                _ => {
574                    if promoted {
575                        format!("{}{}", p.name, unwrap_suffix)
576                    } else if p.is_ref && p.optional {
577                        format!("{}.as_deref()", p.name)
578                    } else if p.is_ref {
579                        format!("&{}", p.name)
580                    } else {
581                        p.name.clone()
582                    }
583                }
584            }
585        })
586        .collect::<Vec<_>>()
587        .join(", ")
588}
589
590/// Generate let bindings for non-opaque Named params, converting them to core types.
591pub fn gen_named_let_bindings_pub(params: &[ParamDef], opaque_types: &AHashSet<String>, core_import: &str) -> String {
592    gen_named_let_bindings(params, opaque_types, core_import)
593}
594
595/// Like `gen_named_let_bindings_pub` but without optional-promotion semantics.
596/// Use this for backends (e.g. WASM) that do not promote non-optional params to `Option<T>`.
597pub fn gen_named_let_bindings_no_promote(
598    params: &[ParamDef],
599    opaque_types: &AHashSet<String>,
600    core_import: &str,
601) -> String {
602    gen_named_let_bindings_inner(params, opaque_types, core_import, false)
603}
604
605pub(super) fn gen_named_let_bindings(
606    params: &[ParamDef],
607    opaque_types: &AHashSet<String>,
608    core_import: &str,
609) -> String {
610    gen_named_let_bindings_inner(params, opaque_types, core_import, true)
611}
612
613fn gen_named_let_bindings_inner(
614    params: &[ParamDef],
615    opaque_types: &AHashSet<String>,
616    core_import: &str,
617    promote: bool,
618) -> String {
619    let mut bindings = String::new();
620    for (idx, p) in params.iter().enumerate() {
621        match &p.ty {
622            TypeRef::Named(name) if !opaque_types.contains(name.as_str()) => {
623                let promoted = promote && crate::shared::is_promoted_optional(params, idx);
624                let core_type_path = format!("{}::{}", core_import, name);
625                if p.optional {
626                    if p.is_ref {
627                        // Option<T> (binding) -> Option<&CoreT> (core expects reference to core type)
628                        // Split into two bindings to avoid temporary value dropped while borrowed (E0716)
629                        write!(
630                            bindings,
631                            "let {name}_owned: Option<{core_type_path}> = {name}.map(Into::into);\n    let {name}_core = {name}_owned.as_ref();\n    ",
632                            name = p.name
633                        )
634                        .ok();
635                    } else {
636                        write!(
637                            bindings,
638                            "let {}_core: Option<{core_type_path}> = {}.map(Into::into);\n    ",
639                            p.name, p.name
640                        )
641                        .ok();
642                    }
643                } else if promoted {
644                    // Promoted-optional: unwrap then convert. Add explicit type annotation to help type inference.
645                    write!(
646                        bindings,
647                        "let {}_core: {core_type_path} = {}.expect(\"'{}' is required\").into();\n    ",
648                        p.name, p.name, p.name
649                    )
650                    .ok();
651                } else {
652                    // Non-optional: add explicit type annotation to help type inference
653                    write!(
654                        bindings,
655                        "let {}_core: {core_type_path} = {}.into();\n    ",
656                        p.name, p.name
657                    )
658                    .ok();
659                }
660            }
661            TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Named(n) if !opaque_types.contains(n.as_str())) => {
662                let promoted = promote && crate::shared::is_promoted_optional(params, idx);
663                if p.optional && p.is_ref {
664                    // Option<Vec<Named>> with is_ref: convert to Option<Vec<CoreType>>, then use as_deref()
665                    // This ensures elements are converted from binding to core type.
666                    write!(
667                        bindings,
668                        "let {}_core: Option<Vec<_>> = {}.as_ref().map(|v| v.iter().map(|x| x.clone().into()).collect());\n    ",
669                        p.name, p.name
670                    )
671                    .ok();
672                } else if p.optional {
673                    // Option<Vec<Named>> without is_ref: convert to concrete Vec
674                    write!(
675                        bindings,
676                        "let {}_core = {}.as_ref().map(|v| v.iter().map(|x| x.clone().into()).collect()).unwrap_or_default();\n    ",
677                        p.name, p.name
678                    )
679                    .ok();
680                } else if promoted {
681                    // Promoted-optional: unwrap then convert
682                    write!(
683                        bindings,
684                        "let {}_core: Vec<_> = {}.expect(\"'{}' is required\").into_iter().map(Into::into).collect();\n    ",
685                        p.name, p.name, p.name
686                    )
687                    .ok();
688                } else if p.is_ref {
689                    // Non-optional Vec<Named> with is_ref=true: generate let binding for conversion
690                    write!(
691                        bindings,
692                        "let {}_core: Vec<_> = {}.into_iter().map(Into::into).collect();\n    ",
693                        p.name, p.name
694                    )
695                    .ok();
696                } else {
697                    // Vec<Named>: convert each element
698                    write!(
699                        bindings,
700                        "let {}_core: Vec<_> = {}.into_iter().map(Into::into).collect();\n    ",
701                        p.name, p.name
702                    )
703                    .ok();
704                }
705            }
706            // Vec<String> with is_ref=true: core expects &[&str].
707            // Convert Vec<String> to Vec<&str> via intermediate binding.
708            TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::String | TypeRef::Char) && p.is_ref => {
709                write!(
710                    bindings,
711                    "let {n}_refs: Vec<&str> = {n}.iter().map(|s| s.as_str()).collect();\n    ",
712                    n = p.name,
713                )
714                .ok();
715            }
716            // Sanitized Vec<String> (originally Vec<tuple>): deserialize each JSON string.
717            TypeRef::Vec(inner)
718                if matches!(inner.as_ref(), TypeRef::String) && p.sanitized && p.original_type.is_some() =>
719            {
720                if p.optional {
721                    write!(
722                        bindings,
723                        "let {n}_core: Option<Vec<_>> = {n}.map(|strs| \
724                         strs.into_iter()\n    \
725                         .filter_map(|s| serde_json::from_str(&s).ok())\n    \
726                         .collect()\n    \
727                         );\n    ",
728                        n = p.name,
729                    )
730                    .ok();
731                } else {
732                    write!(
733                        bindings,
734                        "let {n}_core: Vec<_> = {n}.into_iter()\n    \
735                         .filter_map(|s| serde_json::from_str(&s).ok())\n    \
736                         .collect();\n    ",
737                        n = p.name,
738                    )
739                    .ok();
740                }
741            }
742            _ => {}
743        }
744    }
745    bindings
746}
747
748/// Generate serde-based let bindings for non-opaque Named params.
749/// Serializes binding types to JSON and deserializes to core types.
750/// Used when From impls don't exist (e.g., types with sanitized fields).
751/// `indent` is the whitespace prefix for each generated line (e.g., "    " for functions, "        " for methods).
752/// NOTE: This function should only be called when `cfg.has_serde` is true.
753/// The caller (functions.rs, methods.rs) must gate the call behind a `has_serde` check.
754pub fn gen_serde_let_bindings(
755    params: &[ParamDef],
756    opaque_types: &AHashSet<String>,
757    core_import: &str,
758    err_conv: &str,
759    indent: &str,
760) -> String {
761    let mut bindings = String::new();
762    for p in params {
763        match &p.ty {
764            TypeRef::Named(name) if !opaque_types.contains(name.as_str()) => {
765                let core_path = format!("{}::{}", core_import, name);
766                if p.optional {
767                    write!(
768                        bindings,
769                        "let {name}_core: Option<{core_path}> = {name}.map(|v| {{\n\
770                         {indent}    let json = serde_json::to_string(&v){err_conv}?;\n\
771                         {indent}    serde_json::from_str(&json){err_conv}\n\
772                         {indent}}}).transpose()?;\n{indent}",
773                        name = p.name,
774                        core_path = core_path,
775                        err_conv = err_conv,
776                        indent = indent,
777                    )
778                    .ok();
779                } else {
780                    write!(
781                        bindings,
782                        "let {name}_json = serde_json::to_string(&{name}){err_conv}?;\n\
783                         {indent}let {name}_core: {core_path} = serde_json::from_str(&{name}_json){err_conv}?;\n{indent}",
784                        name = p.name,
785                        core_path = core_path,
786                        err_conv = err_conv,
787                        indent = indent,
788                    )
789                    .ok();
790                }
791            }
792            TypeRef::Vec(inner) => {
793                if let TypeRef::Named(name) = inner.as_ref() {
794                    if !opaque_types.contains(name.as_str()) {
795                        let core_path = format!("{}::{}", core_import, name);
796                        if p.optional {
797                            write!(
798                                bindings,
799                                "let {name}_core: Option<Vec<{core_path}>> = {name}.map(|v| {{\n\
800                                 {indent}    let json = serde_json::to_string(&v){err_conv}?;\n\
801                                 {indent}    serde_json::from_str(&json){err_conv}\n\
802                                 {indent}}}).transpose()?;\n{indent}",
803                                name = p.name,
804                                core_path = core_path,
805                                err_conv = err_conv,
806                                indent = indent,
807                            )
808                            .ok();
809                        } else {
810                            write!(
811                                bindings,
812                                "let {name}_json = serde_json::to_string(&{name}){err_conv}?;\n\
813                                 {indent}let {name}_core: Vec<{core_path}> = serde_json::from_str(&{name}_json){err_conv}?;\n{indent}",
814                                name = p.name,
815                                core_path = core_path,
816                                err_conv = err_conv,
817                                indent = indent,
818                            )
819                            .ok();
820                        }
821                    }
822                } else if matches!(inner.as_ref(), TypeRef::String) && p.sanitized && p.original_type.is_some() {
823                    // Sanitized Vec<tuple>: binding accepts Vec<String> (JSON-encoded tuple items).
824                    // Deserialize each JSON string as a tuple using serde_json.
825                    if p.optional {
826                        write!(
827                            bindings,
828                            "let {n}_core: Option<Vec<_>> = {n}.map(|strs| {{\n\
829                             {indent}    strs.into_iter()\n\
830                             {indent}    .map(|s| serde_json::from_str::<_>(&s){err_conv})\n\
831                             {indent}    .collect::<Result<Vec<_>, _>>()\n\
832                             {indent}}}).transpose()?;\n{indent}",
833                            n = p.name,
834                            err_conv = err_conv,
835                            indent = indent,
836                        )
837                        .ok();
838                    } else {
839                        write!(
840                            bindings,
841                            "let {n}_core: Vec<_> = {n}.into_iter()\n\
842                             {indent}.map(|s| serde_json::from_str::<_>(&s){err_conv})\n\
843                             {indent}.collect::<Result<Vec<_>, _>>()?;\n{indent}",
844                            n = p.name,
845                            err_conv = err_conv,
846                            indent = indent,
847                        )
848                        .ok();
849                    }
850                }
851            }
852            _ => {}
853        }
854    }
855    bindings
856}
857
858/// Check if params contain any non-opaque Named types that need let bindings.
859/// This includes direct Named types, Vec<Named> types, Vec<String> params
860/// with is_ref=true (which need a Vec<&str> intermediate to pass as &[&str]),
861/// and sanitized Vec<String> params (which are JSON-deserialized to tuples).
862pub fn has_named_params(params: &[ParamDef], opaque_types: &AHashSet<String>) -> bool {
863    params.iter().any(|p| match &p.ty {
864        TypeRef::Named(name) if !opaque_types.contains(name.as_str()) => true,
865        TypeRef::Vec(inner) => {
866            // Vec<Named> always needs a conversion let binding.
867            // Vec<String> with is_ref=true needs a Vec<&str> intermediate for &[&str] coercion.
868            // Sanitized Vec<String> needs JSON deserialization via let binding.
869            matches!(inner.as_ref(), TypeRef::Named(name) if !opaque_types.contains(name.as_str()))
870                || (matches!(inner.as_ref(), TypeRef::String | TypeRef::Char) && p.is_ref)
871                || (matches!(inner.as_ref(), TypeRef::String) && p.sanitized && p.original_type.is_some())
872        }
873        _ => false,
874    })
875}
876
877/// Check if a param type is safe for non-opaque delegation (no complex conversions needed).
878/// Vec and Map params can cause type mismatches (e.g. Vec<String> vs &[&str]).
879pub fn is_simple_non_opaque_param(ty: &TypeRef) -> bool {
880    match ty {
881        TypeRef::Primitive(_)
882        | TypeRef::String
883        | TypeRef::Char
884        | TypeRef::Bytes
885        | TypeRef::Path
886        | TypeRef::Unit
887        | TypeRef::Duration => true,
888        TypeRef::Optional(inner) => is_simple_non_opaque_param(inner),
889        _ => false,
890    }
891}
892
893/// Generate a lossy binding→core struct literal for non-opaque delegation.
894/// Sanitized fields use `Default::default()`, non-sanitized fields are cloned and converted.
895/// Fields are accessed via `self.` (behind &self), so all non-Copy types need `.clone()`.
896///
897/// NOTE: This assumes all binding struct fields implement Clone. If a field type does not
898/// implement Clone (e.g., `Mutex<T>`), it should be marked as `sanitized=true` so that
899/// `Default::default()` is used instead of calling `.clone()`. Backends that exclude types
900/// should mark such fields appropriately.
901pub fn gen_lossy_binding_to_core_fields(typ: &TypeDef, core_import: &str, option_duration_on_defaults: bool) -> String {
902    gen_lossy_binding_to_core_fields_inner(typ, core_import, false, option_duration_on_defaults)
903}
904
905/// Same as `gen_lossy_binding_to_core_fields` but declares `core_self` as mutable.
906pub fn gen_lossy_binding_to_core_fields_mut(
907    typ: &TypeDef,
908    core_import: &str,
909    option_duration_on_defaults: bool,
910) -> String {
911    gen_lossy_binding_to_core_fields_inner(typ, core_import, true, option_duration_on_defaults)
912}
913
914fn gen_lossy_binding_to_core_fields_inner(
915    typ: &TypeDef,
916    core_import: &str,
917    needs_mut: bool,
918    option_duration_on_defaults: bool,
919) -> String {
920    let core_path = crate::conversions::core_type_path(typ, core_import);
921    let mut_kw = if needs_mut { "mut " } else { "" };
922    // When has_stripped_cfg_fields is true we emit ..Default::default() at the end of the
923    // struct literal to fill cfg-gated fields that were stripped from the binding IR.
924    // Suppress clippy::needless_update because the fields only exist when the corresponding
925    // feature is enabled — without the feature, clippy thinks the spread is redundant.
926    let allow = if typ.has_stripped_cfg_fields {
927        "#[allow(clippy::needless_update)]\n        "
928    } else {
929        ""
930    };
931    let mut out = format!("{allow}let {mut_kw}core_self = {core_path} {{\n");
932    for field in &typ.fields {
933        let name = &field.name;
934        if field.sanitized {
935            writeln!(out, "            {name}: Default::default(),").ok();
936        } else {
937            let expr = match &field.ty {
938                TypeRef::Primitive(_) => format!("self.{name}"),
939                TypeRef::Duration => {
940                    if field.optional {
941                        format!("self.{name}.map(std::time::Duration::from_millis)")
942                    } else if option_duration_on_defaults && typ.has_default {
943                        // When option_duration_on_defaults is true, non-optional Duration fields
944                        // on has_default types are stored as Option<u64> in the binding struct.
945                        // Use .map(...).unwrap_or_default() so that None falls back to the core
946                        // type's Default (e.g. Duration::from_secs(30)) rather than Duration::ZERO.
947                        format!("self.{name}.map(std::time::Duration::from_millis).unwrap_or_default()")
948                    } else {
949                        format!("std::time::Duration::from_millis(self.{name})")
950                    }
951                }
952                TypeRef::String => format!("self.{name}.clone()"),
953                // Bytes: binding stores Vec<u8>. When core_wrapper == Bytes, core expects
954                // bytes::Bytes so we must call .into() to convert Vec<u8> → Bytes.
955                // When core_wrapper == None, the core field is also Vec<u8> (plain clone).
956                TypeRef::Bytes => {
957                    if field.core_wrapper == CoreWrapper::Bytes {
958                        format!("self.{name}.clone().into()")
959                    } else {
960                        format!("self.{name}.clone()")
961                    }
962                }
963                TypeRef::Char => {
964                    if field.optional {
965                        format!("self.{name}.as_ref().and_then(|s| s.chars().next())")
966                    } else {
967                        format!("self.{name}.chars().next().unwrap_or('*')")
968                    }
969                }
970                TypeRef::Path => {
971                    if field.optional {
972                        format!("self.{name}.clone().map(Into::into)")
973                    } else {
974                        format!("self.{name}.clone().into()")
975                    }
976                }
977                TypeRef::Named(_) => {
978                    if field.optional {
979                        format!("self.{name}.clone().map(Into::into)")
980                    } else {
981                        format!("self.{name}.clone().into()")
982                    }
983                }
984                TypeRef::Vec(inner) => match inner.as_ref() {
985                    TypeRef::Named(_) => {
986                        if field.optional {
987                            // Option<Vec<Named(T)>>: map over the Option, then convert each element
988                            format!("self.{name}.clone().map(|v| v.into_iter().map(Into::into).collect())")
989                        } else {
990                            format!("self.{name}.clone().into_iter().map(Into::into).collect()")
991                        }
992                    }
993                    _ => format!("self.{name}.clone()"),
994                },
995                TypeRef::Optional(inner) => {
996                    // When field.optional is also true, the binding field was flattened from
997                    // Option<Option<T>> to Option<T>. Core expects Option<Option<T>>, so wrap
998                    // with .map(Some) to reconstruct the double-optional.
999                    let base = match inner.as_ref() {
1000                        TypeRef::Named(_) => {
1001                            format!("self.{name}.clone().map(Into::into)")
1002                        }
1003                        TypeRef::Duration => {
1004                            format!("self.{name}.map(|v| std::time::Duration::from_millis(v as u64))")
1005                        }
1006                        TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Named(_)) => {
1007                            format!("self.{name}.clone().map(|v| v.into_iter().map(Into::into).collect())")
1008                        }
1009                        _ => format!("self.{name}.clone()"),
1010                    };
1011                    if field.optional {
1012                        format!("({base}).map(Some)")
1013                    } else {
1014                        base
1015                    }
1016                }
1017                TypeRef::Map(_, v) => match v.as_ref() {
1018                    TypeRef::Json => {
1019                        // HashMap<String, String> (binding) → HashMap<String, Value> (core)
1020                        if field.optional {
1021                            format!(
1022                                "self.{name}.clone().map(|m| m.into_iter().map(|(k, v)| \
1023                                 (k, serde_json::from_str(&v).unwrap_or(serde_json::Value::String(v)))).collect())"
1024                            )
1025                        } else {
1026                            format!(
1027                                "self.{name}.clone().into_iter().map(|(k, v)| \
1028                                 (k, serde_json::from_str(&v).unwrap_or(serde_json::Value::String(v)))).collect()"
1029                            )
1030                        }
1031                    }
1032                    // Collect to handle HashMap↔BTreeMap conversion
1033                    _ => {
1034                        if field.optional {
1035                            format!("self.{name}.clone().map(|m| m.into_iter().collect())")
1036                        } else {
1037                            format!("self.{name}.clone().into_iter().collect()")
1038                        }
1039                    }
1040                },
1041                TypeRef::Unit => format!("self.{name}.clone()"),
1042                TypeRef::Json => {
1043                    // String (binding) → serde_json::Value (core)
1044                    if field.optional {
1045                        format!("self.{name}.as_ref().and_then(|s| serde_json::from_str(s).ok())")
1046                    } else {
1047                        format!("serde_json::from_str(&self.{name}).unwrap_or_default()")
1048                    }
1049                }
1050            };
1051            // Newtype wrapping: when the field was resolved from a newtype (e.g. NodeIndex → u32),
1052            // re-wrap the binding value into the newtype for the core struct literal.
1053            // When `optional=true` and `ty` is a plain Primitive (not TypeRef::Optional), the core
1054            // field is actually `Option<NewtypeT>`, so we must use `.map(NewtypeT)` not `NewtypeT(...)`.
1055            let expr = if let Some(newtype_path) = &field.newtype_wrapper {
1056                match &field.ty {
1057                    TypeRef::Optional(_) => format!("({expr}).map({newtype_path})"),
1058                    TypeRef::Vec(_) => format!("({expr}).into_iter().map({newtype_path}).collect()"),
1059                    _ if field.optional => format!("({expr}).map({newtype_path})"),
1060                    _ => format!("{newtype_path}({expr})"),
1061                }
1062            } else {
1063                expr
1064            };
1065            writeln!(out, "            {name}: {expr},").ok();
1066        }
1067    }
1068    // Use ..Default::default() to fill cfg-gated fields stripped from the IR
1069    if typ.has_stripped_cfg_fields {
1070        out.push_str("            ..Default::default()\n");
1071    }
1072    out.push_str("        };\n        ");
1073    out
1074}
1075
1076/// Generate the body for an async call, unified across methods, static methods, and free functions.
1077///
1078/// - `core_call`: the expression to await, e.g. `inner.method(args)` or `CoreType::fn(args)`.
1079///   For Pyo3FutureIntoPy opaque methods this should reference `inner` (the Arc clone);
1080///   for all other patterns it may reference `self.inner` or a static call expression.
1081/// - `cfg`: binding configuration (determines which async pattern to emit)
1082/// - `has_error`: whether the core call returns a `Result`
1083/// - `return_wrap`: expression to produce the binding return value from `result`,
1084///   e.g. `"result"` or `"TypeName::from(result)"`
1085///
1086/// - `is_opaque`: whether the binding type is Arc-wrapped (affects TokioBlockOn wrapping)
1087/// - `inner_clone_line`: optional statement emitted before the pattern-specific body,
1088///   e.g. `"let inner = self.inner.clone();\n        "` for opaque instance methods, or `""`.
1089///   Required when `core_call` references `inner` (Pyo3FutureIntoPy opaque case).
1090#[allow(clippy::too_many_arguments)]
1091pub fn gen_async_body(
1092    core_call: &str,
1093    cfg: &RustBindingConfig,
1094    has_error: bool,
1095    return_wrap: &str,
1096    is_opaque: bool,
1097    inner_clone_line: &str,
1098    is_unit_return: bool,
1099    return_type: Option<&str>,
1100) -> String {
1101    let pattern_body = match cfg.async_pattern {
1102        AsyncPattern::Pyo3FutureIntoPy => {
1103            let result_handling = if has_error {
1104                format!(
1105                    "let result = {core_call}.await\n            \
1106                     .map_err(|e| PyErr::new::<PyRuntimeError, _>(e.to_string()))?;"
1107                )
1108            } else if is_unit_return {
1109                format!("{core_call}.await;")
1110            } else {
1111                format!("let result = {core_call}.await;")
1112            };
1113            let (ok_expr, extra_binding) = if is_unit_return && !has_error {
1114                ("()".to_string(), String::new())
1115            } else if return_wrap.contains(".into()") || return_wrap.contains("::from(") {
1116                // When return_wrap contains type conversions like .into() or ::from(),
1117                // bind to a variable to help type inference for the generic future_into_py.
1118                // This avoids E0283 "type annotations needed".
1119                let wrapped_var = "wrapped_result";
1120                let binding = if let Some(ret_type) = return_type {
1121                    // Add explicit type annotation to help type inference
1122                    format!("let {wrapped_var}: {ret_type} = {return_wrap};\n            ")
1123                } else {
1124                    format!("let {wrapped_var} = {return_wrap};\n            ")
1125                };
1126                (wrapped_var.to_string(), binding)
1127            } else {
1128                (return_wrap.to_string(), String::new())
1129            };
1130            format!(
1131                "pyo3_async_runtimes::tokio::future_into_py(py, async move {{\n            \
1132                 {result_handling}\n            \
1133                 {extra_binding}Ok({ok_expr})\n        }})"
1134            )
1135        }
1136        AsyncPattern::WasmNativeAsync => {
1137            let result_handling = if has_error {
1138                format!(
1139                    "let result = {core_call}.await\n        \
1140                     .map_err(|e| JsValue::from_str(&e.to_string()))?;"
1141                )
1142            } else if is_unit_return {
1143                format!("{core_call}.await;")
1144            } else {
1145                format!("let result = {core_call}.await;")
1146            };
1147            let ok_expr = if is_unit_return && !has_error {
1148                "()"
1149            } else {
1150                return_wrap
1151            };
1152            format!(
1153                "{result_handling}\n        \
1154                 Ok({ok_expr})"
1155            )
1156        }
1157        AsyncPattern::NapiNativeAsync => {
1158            let result_handling = if has_error {
1159                format!(
1160                    "let result = {core_call}.await\n            \
1161                     .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;"
1162                )
1163            } else if is_unit_return {
1164                format!("{core_call}.await;")
1165            } else {
1166                format!("let result = {core_call}.await;")
1167            };
1168            if !has_error && !is_unit_return {
1169                // No error type: return value directly without Ok() wrapper
1170                format!(
1171                    "{result_handling}\n            \
1172                     {return_wrap}"
1173                )
1174            } else {
1175                let ok_expr = if is_unit_return && !has_error {
1176                    "()"
1177                } else {
1178                    return_wrap
1179                };
1180                format!(
1181                    "{result_handling}\n            \
1182                     Ok({ok_expr})"
1183                )
1184            }
1185        }
1186        AsyncPattern::TokioBlockOn => {
1187            if has_error {
1188                if is_opaque {
1189                    format!(
1190                        "let rt = tokio::runtime::Runtime::new()?;\n        \
1191                         let result = rt.block_on(async {{ {core_call}.await.map_err(|e| e.into()) }})?;\n        \
1192                         {return_wrap}"
1193                    )
1194                } else {
1195                    format!(
1196                        "let rt = tokio::runtime::Runtime::new()?;\n        \
1197                         rt.block_on(async {{ {core_call}.await.map_err(|e| e.into()) }})"
1198                    )
1199                }
1200            } else if is_opaque {
1201                if is_unit_return {
1202                    format!(
1203                        "let rt = tokio::runtime::Runtime::new()?;\n        \
1204                         rt.block_on(async {{ {core_call}.await }});"
1205                    )
1206                } else {
1207                    format!(
1208                        "let rt = tokio::runtime::Runtime::new()?;\n        \
1209                         let result = rt.block_on(async {{ {core_call}.await }});\n        \
1210                         {return_wrap}"
1211                    )
1212                }
1213            } else {
1214                format!(
1215                    "let rt = tokio::runtime::Runtime::new()?;\n        \
1216                     rt.block_on(async {{ {core_call}.await }})"
1217                )
1218            }
1219        }
1220        AsyncPattern::None => "todo!(\"async not supported by backend\")".to_string(),
1221    };
1222    if inner_clone_line.is_empty() {
1223        pattern_body
1224    } else {
1225        format!("{inner_clone_line}{pattern_body}")
1226    }
1227}
1228
1229/// Generate a compilable body for functions that can't be auto-delegated.
1230/// Returns a default value or error instead of `todo!()` which would panic.
1231///
1232/// `opaque_types` is the set of opaque type names (Arc-wrapped). Opaque types do not
1233/// implement `Default`, so returning `Default::default()` for their Named return types
1234/// would fail to compile. For those cases a `todo!()` body is emitted instead.
1235pub fn gen_unimplemented_body(
1236    return_type: &TypeRef,
1237    fn_name: &str,
1238    has_error: bool,
1239    cfg: &RustBindingConfig,
1240    params: &[ParamDef],
1241    opaque_types: &AHashSet<String>,
1242) -> String {
1243    // Suppress unused_variables by binding all params to `_`
1244    let suppress = if params.is_empty() {
1245        String::new()
1246    } else {
1247        let names: Vec<&str> = params.iter().map(|p| p.name.as_str()).collect();
1248        if names.len() == 1 {
1249            format!("let _ = {};\n        ", names[0])
1250        } else {
1251            format!("let _ = ({});\n        ", names.join(", "))
1252        }
1253    };
1254    let err_msg = format!("Not implemented: {fn_name}");
1255    let body = if has_error {
1256        // Backend-specific error return
1257        match cfg.async_pattern {
1258            AsyncPattern::Pyo3FutureIntoPy => {
1259                format!("Err(pyo3::exceptions::PyNotImplementedError::new_err(\"{err_msg}\"))")
1260            }
1261            AsyncPattern::NapiNativeAsync => {
1262                format!("Err(napi::Error::new(napi::Status::GenericFailure, \"{err_msg}\"))")
1263            }
1264            AsyncPattern::WasmNativeAsync => {
1265                format!("Err(JsValue::from_str(\"{err_msg}\"))")
1266            }
1267            _ => format!("Err(\"{err_msg}\".to_string())"),
1268        }
1269    } else {
1270        // Return type-appropriate default
1271        match return_type {
1272            TypeRef::Unit => "()".to_string(),
1273            TypeRef::String | TypeRef::Char | TypeRef::Path => format!("String::from(\"[unimplemented: {fn_name}]\")"),
1274            TypeRef::Bytes => "Vec::new()".to_string(),
1275            TypeRef::Primitive(p) => match p {
1276                alef_core::ir::PrimitiveType::Bool => "false".to_string(),
1277                alef_core::ir::PrimitiveType::F32 => "0.0f32".to_string(),
1278                alef_core::ir::PrimitiveType::F64 => "0.0f64".to_string(),
1279                _ => "0".to_string(),
1280            },
1281            TypeRef::Optional(_) => "None".to_string(),
1282            TypeRef::Vec(_) => "Vec::new()".to_string(),
1283            TypeRef::Map(_, _) => "Default::default()".to_string(),
1284            TypeRef::Duration => "0".to_string(),
1285            TypeRef::Named(name) => {
1286                // Opaque types (Arc-wrapped) do not implement Default — use todo!() to
1287                // produce a compilable placeholder that panics at runtime if called.
1288                // Non-opaque Named types (config structs) do derive Default, so use that.
1289                if opaque_types.contains(name.as_str()) {
1290                    format!("todo!(\"{err_msg}\")")
1291                } else {
1292                    "Default::default()".to_string()
1293                }
1294            }
1295            TypeRef::Json => {
1296                // Json return without error type: return Default::default()
1297                "Default::default()".to_string()
1298            }
1299        }
1300    };
1301    format!("{suppress}{body}")
1302}