Skip to main content

alef_codegen/generators/
binding_helpers.rs

1use crate::conversions::helpers::{core_prim_str, needs_f64_cast, needs_i32_cast};
2use crate::generators::{AsyncPattern, RustBindingConfig};
3use crate::type_mapper::TypeMapper;
4use ahash::AHashSet;
5use alef_core::ir::{CoreWrapper, ParamDef, TypeDef, TypeRef};
6
7/// Helper: wrap an opaque inner value in the correct smart pointer expression.
8///
9/// - Plain opaque types use `Arc::new(val)`.
10/// - Mutex-wrapped opaque types use `Arc::new(std::sync::Mutex::new(val))`.
11fn arc_wrap(val: &str, name: &str, mutex_types: &AHashSet<String>) -> String {
12    let needs_mutex = mutex_types.contains(name);
13    crate::template_env::render(
14        "binding_helpers/arc_wrap.jinja",
15        minijinja::context! {
16            val => val,
17            needs_mutex => needs_mutex,
18        },
19    )
20    .trim_end_matches('\n')
21    .to_string()
22}
23
24/// Detect whether the core-call expression already evaluates to `Arc<T>` (or the
25/// equivalent mutex-wrapped variant) for the binding's `inner` field.
26///
27/// When the method body forwards `self.inner` (e.g. `Node::clone` whose core impl
28/// calls `Arc::clone(&self.tree)` internally), the expression is already an
29/// `Arc<Node>` and must not be wrapped in another `Arc::new(...)`.
30fn expr_is_already_arc(expr: &str) -> bool {
31    let trimmed = expr.trim();
32    trimmed == "self.inner" || trimmed == "self.inner.clone()" || trimmed == "self.inner.as_ref().clone()"
33}
34
35/// Wrap a core-call result for opaque delegation methods.
36///
37/// - `TypeRef::Named(n)` where `n == type_name` → re-wrap in `Self { inner: Arc::new(...) }`
38/// - `TypeRef::Named(n)` where `n` is another opaque type → wrap in `{n} { inner: Arc::new(...) }`
39/// - `TypeRef::Named(n)` where `n` is a non-opaque type → `todo!()` placeholder (From may not exist)
40/// - Everything else (primitives, String, Vec, etc.) → pass through unchanged
41/// - `TypeRef::Unit` → pass through unchanged
42///
43/// When `returns_cow` is true the core method returns `Cow<'_, T>`. `.into_owned()` is emitted
44/// before any further type conversion to obtain an owned `T`.
45///
46/// `mutex_types` identifies opaque types that use `Arc<Mutex<T>>` instead of `Arc<T>`, so
47/// constructor expressions use `Arc::new(Mutex::new(...))` where needed.
48#[allow(clippy::too_many_arguments)]
49pub fn wrap_return_with_mutex(
50    expr: &str,
51    return_type: &TypeRef,
52    type_name: &str,
53    opaque_types: &AHashSet<String>,
54    mutex_types: &AHashSet<String>,
55    self_is_opaque: bool,
56    returns_ref: bool,
57    returns_cow: bool,
58) -> String {
59    wrap_return_with_mutex_mapped(
60        expr,
61        return_type,
62        type_name,
63        opaque_types,
64        mutex_types,
65        self_is_opaque,
66        returns_ref,
67        returns_cow,
68        &crate::type_mapper::IdentityMapper,
69    )
70}
71
72#[allow(clippy::too_many_arguments)]
73pub fn wrap_return_with_mutex_mapped(
74    expr: &str,
75    return_type: &TypeRef,
76    type_name: &str,
77    opaque_types: &AHashSet<String>,
78    mutex_types: &AHashSet<String>,
79    self_is_opaque: bool,
80    returns_ref: bool,
81    returns_cow: bool,
82    mapper: &dyn TypeMapper,
83) -> String {
84    let self_arc = arc_wrap("", type_name, mutex_types); // used for pattern matching only
85    let _ = self_arc; // just to reference mutex_types in context
86    match return_type {
87        TypeRef::Named(n) if n == type_name && self_is_opaque => {
88            // If the expression already evaluates to `Arc<T>` (e.g. `self.inner.clone()`
89            // where `inner: Arc<T>`), don't wrap in another Arc — pass through.
90            if expr_is_already_arc(expr) {
91                return format!("Self {{ inner: {expr} }}");
92            }
93            let inner = if returns_cow {
94                format!("{expr}.into_owned()")
95            } else if returns_ref {
96                format!("{expr}.clone()")
97            } else {
98                expr.to_string()
99            };
100            format!("Self {{ inner: {} }}", arc_wrap(&inner, type_name, mutex_types))
101        }
102        TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
103            let mapped_n = mapper.named(n);
104            // Same already-Arc guard as the Self branch above.
105            if expr_is_already_arc(expr) {
106                return format!("{mapped_n} {{ inner: {expr} }}");
107            }
108            let inner = if returns_cow {
109                format!("{expr}.into_owned()")
110            } else if returns_ref {
111                format!("{expr}.clone()")
112            } else {
113                expr.to_string()
114            };
115            format!("{mapped_n} {{ inner: {} }}", arc_wrap(&inner, n, mutex_types))
116        }
117        TypeRef::Named(_) => {
118            // Non-opaque Named return type — use .into() for core→binding From conversion.
119            // When the core returns a Cow, call .into_owned() first to get an owned T.
120            // When the core returns a reference, clone first since From<&T> typically doesn't exist.
121            // NOTE: If this type was sanitized to String in the binding, From won't exist.
122            // The calling backend should check method.sanitized before delegating.
123            // This code assumes non-sanitized Named types have From impls.
124            if returns_cow {
125                format!("{expr}.into_owned().into()")
126            } else if returns_ref {
127                format!("{expr}.clone().into()")
128            } else {
129                format!("{expr}.into()")
130            }
131        }
132        // String: only convert when the core returns a reference (&str→String).
133        TypeRef::String => {
134            if returns_ref {
135                format!("{expr}.into()")
136            } else {
137                expr.to_string()
138            }
139        }
140        // Bytes: always use .to_vec() which works for both &Bytes and owned Bytes.
141        // &Bytes does not implement From<&Bytes> for Vec<u8>, so .into() fails.
142        TypeRef::Bytes => format!("{expr}.to_vec()"),
143        // Path: PathBuf→String needs to_string_lossy, &Path→String too
144        TypeRef::Path => format!("{expr}.to_string_lossy().to_string()"),
145        // Duration: core returns std::time::Duration, binding uses u64 (millis)
146        TypeRef::Duration => format!("{expr}.as_millis() as u64"),
147        // Json: serde_json::Value needs serialization to string
148        TypeRef::Json => format!("{expr}.to_string()"),
149        // Optional: wrap inner conversion in .map(...)
150        TypeRef::Optional(inner) => match inner.as_ref() {
151            TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
152                let mapped_n = mapper.named(n);
153                let wrap = arc_wrap("v", n, mutex_types);
154                if returns_ref {
155                    format!(
156                        "{expr}.map(|v| {mapped_n} {{ inner: {} }})",
157                        arc_wrap("v.clone()", n, mutex_types)
158                    )
159                } else {
160                    format!("{expr}.map(|v| {mapped_n} {{ inner: {wrap} }})")
161                }
162            }
163            TypeRef::Named(_) => {
164                if returns_ref {
165                    format!("{expr}.map(|v| v.clone().into())")
166                } else {
167                    format!("{expr}.map(Into::into)")
168                }
169            }
170            TypeRef::Path => {
171                format!("{expr}.map(Into::into)")
172            }
173            TypeRef::String | TypeRef::Bytes => {
174                if returns_ref {
175                    format!("{expr}.map(Into::into)")
176                } else {
177                    expr.to_string()
178                }
179            }
180            TypeRef::Duration => format!("{expr}.map(|d| d.as_millis() as u64)"),
181            TypeRef::Json => format!("{expr}.map(ToString::to_string)"),
182            // Optional<Vec<Named>>: convert each element in the inner Vec
183            TypeRef::Vec(vec_inner) => match vec_inner.as_ref() {
184                TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
185                    let mapped_n = mapper.named(n);
186                    if returns_ref {
187                        let wrap = arc_wrap("x.clone()", n, mutex_types);
188                        format!("{expr}.map(|v| v.into_iter().map(|x| {mapped_n} {{ inner: {wrap} }}).collect())")
189                    } else {
190                        let wrap = arc_wrap("x", n, mutex_types);
191                        format!("{expr}.map(|v| v.into_iter().map(|x| {mapped_n} {{ inner: {wrap} }}).collect())")
192                    }
193                }
194                TypeRef::Named(_) => {
195                    if returns_ref {
196                        format!("{expr}.map(|v| v.into_iter().map(|x| x.clone().into()).collect())")
197                    } else {
198                        format!("{expr}.map(|v| v.into_iter().map(Into::into).collect())")
199                    }
200                }
201                _ => expr.to_string(),
202            },
203            _ => expr.to_string(),
204        },
205        // Vec: map each element through the appropriate conversion
206        TypeRef::Vec(inner) => match inner.as_ref() {
207            TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
208                let mapped_n = mapper.named(n);
209                if returns_ref {
210                    let wrap = arc_wrap("v.clone()", n, mutex_types);
211                    format!("{expr}.into_iter().map(|v| {mapped_n} {{ inner: {wrap} }}).collect()")
212                } else {
213                    let wrap = arc_wrap("v", n, mutex_types);
214                    format!("{expr}.into_iter().map(|v| {mapped_n} {{ inner: {wrap} }}).collect()")
215                }
216            }
217            TypeRef::Named(_) => {
218                if returns_ref {
219                    format!("{expr}.into_iter().map(|v| v.clone().into()).collect()")
220                } else {
221                    format!("{expr}.into_iter().map(Into::into).collect()")
222                }
223            }
224            TypeRef::Path => {
225                format!("{expr}.into_iter().map(Into::into).collect()")
226            }
227            TypeRef::String => {
228                if returns_ref {
229                    // Core returns `&[&str]` / `&[String]`; `.iter()` yields `&&str` / `&String`.
230                    // `Into::into` on those fails (`From<&&str>` is not implemented). Force
231                    // an explicit ToString hop so the binding always sees owned `String`s.
232                    format!("{expr}.iter().map(|s| s.to_string()).collect()")
233                } else {
234                    expr.to_string()
235                }
236            }
237            TypeRef::Bytes => {
238                if returns_ref {
239                    format!("{expr}.iter().map(|b| b.to_vec()).collect()")
240                } else {
241                    expr.to_string()
242                }
243            }
244            _ => expr.to_string(),
245        },
246        _ => expr.to_string(),
247    }
248}
249
250/// Wrap a core-call result for opaque delegation methods.
251///
252/// This is the backward-compatible wrapper that passes an empty `mutex_types` set.
253/// Use `wrap_return_with_mutex` when the type set contains mutex-wrapped opaque types.
254pub fn wrap_return(
255    expr: &str,
256    return_type: &TypeRef,
257    type_name: &str,
258    opaque_types: &AHashSet<String>,
259    self_is_opaque: bool,
260    returns_ref: bool,
261    returns_cow: bool,
262) -> String {
263    wrap_return_with_mutex(
264        expr,
265        return_type,
266        type_name,
267        opaque_types,
268        &AHashSet::new(),
269        self_is_opaque,
270        returns_ref,
271        returns_cow,
272    )
273}
274
275/// Unwrap a newtype return value when `return_newtype_wrapper` is set.
276///
277/// Core function returns a newtype (e.g. `NodeIndex(u32)`), but the binding return type
278/// is the inner type (e.g. `u32`). Access `.0` to unwrap the newtype.
279pub fn apply_return_newtype_unwrap(expr: &str, return_newtype_wrapper: &Option<String>) -> String {
280    match return_newtype_wrapper {
281        Some(_) => crate::template_env::render(
282            "binding_helpers/return_newtype_unwrap.jinja",
283            minijinja::context! {
284                expr => expr,
285            },
286        )
287        .trim_end_matches('\n')
288        .to_string(),
289        None => expr.to_string(),
290    }
291}
292
293/// Build call argument expressions from parameters.
294/// - Opaque Named types: unwrap Arc wrapper via `(*param.inner).clone()`
295/// - Non-opaque Named types: `.into()` for From conversion
296/// - String/Path/Bytes: `&param` since core functions typically take `&str`/`&Path`/`&[u8]`
297/// - Params with `newtype_wrapper` set: re-wrap the raw value in the newtype constructor
298///   (e.g., `NodeIndex(parent)`) since the binding resolved `NodeIndex(u32)` → `u32`.
299///
300/// NOTE: This function does not perform serde-based conversion. For Named params that lack
301/// From impls (e.g., due to sanitized fields), use `gen_serde_let_bindings` instead when
302/// `cfg.has_serde` is true, or fall back to `gen_unimplemented_body`.
303pub fn gen_call_args(params: &[ParamDef], opaque_types: &AHashSet<String>) -> String {
304    params
305        .iter()
306        .enumerate()
307        .map(|(idx, p)| {
308            let promoted = crate::shared::is_promoted_optional(params, idx);
309            // If a required param was promoted to optional, unwrap it before use.
310            // Note: promoted params that are not Optional<T> will NOT call .expect() because
311            // promoted refers to the PyO3 signature constraint, not the actual Rust type.
312            // The function_params logic wraps promoted params in Option<T>, making them truly optional.
313            let unwrap_suffix = if promoted && p.optional {
314                format!(".expect(\"'{}' is required\")", p.name)
315            } else {
316                String::new()
317            };
318            // If this param's type was resolved from a newtype (e.g. NodeIndex(u32) → u32),
319            // re-wrap the raw value back into the newtype when calling core.
320            if let Some(newtype_path) = &p.newtype_wrapper {
321                return if p.optional {
322                    format!("{}.map({newtype_path})", p.name)
323                } else if promoted {
324                    format!("{newtype_path}({}{})", p.name, unwrap_suffix)
325                } else {
326                    format!("{newtype_path}({})", p.name)
327                };
328            }
329            match &p.ty {
330                TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
331                    // Opaque type: borrow through Arc to get &CoreType
332                    if p.optional {
333                        format!("{}.as_ref().map(|v| &v.inner)", p.name)
334                    } else if promoted {
335                        format!("{}{}.inner.as_ref()", p.name, unwrap_suffix)
336                    } else {
337                        format!("&{}.inner", p.name)
338                    }
339                }
340                TypeRef::Named(_) => {
341                    if p.optional {
342                        if p.is_ref {
343                            // Option<T> (binding) -> Option<&CoreT>: use as_ref() only
344                            // The Into conversion must happen in a let binding to avoid E0716
345                            format!("{}.as_ref()", p.name)
346                        } else {
347                            format!("{}.map(Into::into)", p.name)
348                        }
349                    } else if promoted {
350                        format!("{}{}.into()", p.name, unwrap_suffix)
351                    } else {
352                        format!("{}.into()", p.name)
353                    }
354                }
355                // String → &str for core function calls when is_ref=true,
356                // or pass owned when is_ref=false (core takes String/impl Into<String>).
357                // For optional params: as_deref() when is_ref=true, pass owned when is_ref=false.
358                TypeRef::String | TypeRef::Char => {
359                    if p.optional {
360                        if p.is_ref {
361                            format!("{}.as_deref()", p.name)
362                        } else {
363                            p.name.clone()
364                        }
365                    } else if promoted {
366                        if p.is_ref {
367                            format!("&{}{}", p.name, unwrap_suffix)
368                        } else {
369                            format!("{}{}", p.name, unwrap_suffix)
370                        }
371                    } else if p.is_ref {
372                        format!("&{}", p.name)
373                    } else {
374                        p.name.clone()
375                    }
376                }
377                // Path → PathBuf/&Path for core function calls
378                TypeRef::Path => {
379                    if p.optional && p.is_ref {
380                        format!("{}.as_deref().map(std::path::Path::new)", p.name)
381                    } else if p.optional {
382                        format!("{}.map(std::path::PathBuf::from)", p.name)
383                    } else if promoted {
384                        format!("std::path::PathBuf::from({}{})", p.name, unwrap_suffix)
385                    } else if p.is_ref {
386                        format!("std::path::Path::new(&{})", p.name)
387                    } else {
388                        format!("std::path::PathBuf::from({})", p.name)
389                    }
390                }
391                TypeRef::Bytes => {
392                    if p.optional {
393                        if p.is_ref {
394                            format!("{}.as_deref()", p.name)
395                        } else {
396                            p.name.clone()
397                        }
398                    } else if promoted {
399                        // is_ref=true: pass &Vec<u8> (core takes &[u8])
400                        // is_ref=false: pass Vec<u8> (core takes owned Vec<u8>)
401                        if p.is_ref {
402                            format!("&{}{}", p.name, unwrap_suffix)
403                        } else {
404                            format!("{}{}", p.name, unwrap_suffix)
405                        }
406                    } else {
407                        // is_ref=true: pass &Vec<u8> (core takes &[u8])
408                        // is_ref=false: pass Vec<u8> (core takes owned Vec<u8>)
409                        if p.is_ref {
410                            format!("&{}", p.name)
411                        } else {
412                            p.name.clone()
413                        }
414                    }
415                }
416                // Duration: binding uses u64 (millis), core uses std::time::Duration
417                TypeRef::Duration => {
418                    if p.optional {
419                        format!("{}.map(std::time::Duration::from_millis)", p.name)
420                    } else if promoted {
421                        format!("std::time::Duration::from_millis({}{})", p.name, unwrap_suffix)
422                    } else {
423                        format!("std::time::Duration::from_millis({})", p.name)
424                    }
425                }
426                TypeRef::Json => {
427                    // JSON params: binding has String, core expects serde_json::Value
428                    if p.optional {
429                        format!("{}.as_ref().and_then(|s| serde_json::from_str(s).ok())", p.name)
430                    } else if promoted {
431                        format!("serde_json::from_str(&{}{}).unwrap_or_default()", p.name, unwrap_suffix)
432                    } else {
433                        format!("serde_json::from_str(&{}).unwrap_or_default()", p.name)
434                    }
435                }
436                TypeRef::Vec(inner) => {
437                    // Vec<Named>: convert each element via Into::into when used with let bindings
438                    if matches!(inner.as_ref(), TypeRef::Named(_)) {
439                        if p.optional {
440                            if p.is_ref {
441                                format!("{}.as_deref()", p.name)
442                            } else {
443                                p.name.clone()
444                            }
445                        } else if promoted {
446                            if p.is_ref {
447                                format!("&{}{}", p.name, unwrap_suffix)
448                            } else {
449                                format!("{}{}", p.name, unwrap_suffix)
450                            }
451                        } else if p.is_ref {
452                            format!("&{}", p.name)
453                        } else {
454                            p.name.clone()
455                        }
456                    } else if promoted {
457                        format!("{}{}", p.name, unwrap_suffix)
458                    } else if p.is_mut && p.optional {
459                        format!("{}.as_deref_mut()", p.name)
460                    } else if p.is_mut {
461                        format!("&mut {}", p.name)
462                    } else if p.is_ref && p.optional {
463                        format!("{}.as_deref()", p.name)
464                    } else if p.is_ref {
465                        format!("&{}", p.name)
466                    } else {
467                        p.name.clone()
468                    }
469                }
470                _ => {
471                    if promoted {
472                        format!("{}{}", p.name, unwrap_suffix)
473                    } else if p.is_mut && p.optional {
474                        format!("{}.as_deref_mut()", p.name)
475                    } else if p.is_mut {
476                        format!("&mut {}", p.name)
477                    } else if p.is_ref && p.optional {
478                        // Optional ref params: use as_deref() for slice/str coercion
479                        // Option<Vec<T>> -> Option<&[T]>, Option<String> -> Option<&str>
480                        format!("{}.as_deref()", p.name)
481                    } else if p.is_ref {
482                        format!("&{}", p.name)
483                    } else {
484                        p.name.clone()
485                    }
486                }
487            }
488        })
489        .collect::<Vec<_>>()
490        .join(", ")
491}
492
493/// Build call argument expressions with primitive type casting for backends that remap
494/// numeric types (e.g. extendr maps `f32`/`usize`/`u64` to `f64` and `u32` to `i32`).
495///
496/// For `TypeRef::Primitive` params whose binding type differs from the core type, emits
497/// `name as core_ty` (or `.map(|v| v as core_ty)` for optional params). All other params
498/// fall back to the same logic as `gen_call_args`.
499pub fn gen_call_args_cfg(
500    params: &[ParamDef],
501    opaque_types: &AHashSet<String>,
502    cast_uints_to_i32: bool,
503    cast_large_ints_to_f64: bool,
504) -> String {
505    params
506        .iter()
507        .enumerate()
508        .map(|(idx, p)| {
509            let promoted = crate::shared::is_promoted_optional(params, idx);
510            let unwrap_suffix = if promoted && p.optional {
511                format!(".expect(\"'{}' is required\")", p.name)
512            } else {
513                String::new()
514            };
515            // Newtype params are handled the same as in gen_call_args.
516            if p.newtype_wrapper.is_some() {
517                // Delegate newtype handling to the standard gen_call_args helper by
518                // collecting a one-element slice and extracting the result.
519                return gen_call_args(std::slice::from_ref(p), opaque_types);
520            }
521            // For primitive params that need a cast, emit the cast expression.
522            if let TypeRef::Primitive(prim) = &p.ty {
523                let core_ty = core_prim_str(prim);
524                let needs_cast =
525                    (cast_uints_to_i32 && needs_i32_cast(prim)) || (cast_large_ints_to_f64 && needs_f64_cast(prim));
526                if needs_cast {
527                    return if p.optional {
528                        format!("{}.map(|v| v as {core_ty})", p.name)
529                    } else if promoted {
530                        format!("({}{}) as {core_ty}", p.name, unwrap_suffix)
531                    } else {
532                        format!("{} as {core_ty}", p.name)
533                    };
534                }
535            }
536            // Delegate all other types to gen_call_args.
537            gen_call_args(std::slice::from_ref(p), opaque_types)
538        })
539        .collect::<Vec<_>>()
540        .join(", ")
541}
542
543/// Build call argument expressions using pre-bound let bindings for non-opaque Named params.
544/// Non-opaque Named params use `&{name}_core` references instead of `.into()`.
545pub fn gen_call_args_with_let_bindings(params: &[ParamDef], opaque_types: &AHashSet<String>) -> String {
546    params
547        .iter()
548        .enumerate()
549        .map(|(idx, p)| {
550            let promoted = crate::shared::is_promoted_optional(params, idx);
551            let unwrap_suffix = if promoted {
552                format!(".expect(\"'{}' is required\")", p.name)
553            } else {
554                String::new()
555            };
556            // If this param's type was resolved from a newtype, re-wrap when calling core.
557            if let Some(newtype_path) = &p.newtype_wrapper {
558                return if p.optional {
559                    format!("{}.map({newtype_path})", p.name)
560                } else if promoted {
561                    format!("{newtype_path}({}{})", p.name, unwrap_suffix)
562                } else {
563                    format!("{newtype_path}({})", p.name)
564                };
565            }
566            match &p.ty {
567                TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
568                    if p.optional {
569                        format!("{}.as_ref().map(|v| &v.inner)", p.name)
570                    } else if promoted {
571                        format!("{}{}.inner.as_ref()", p.name, unwrap_suffix)
572                    } else {
573                        format!("&{}.inner", p.name)
574                    }
575                }
576                TypeRef::Named(_) => {
577                    if p.optional && p.is_ref {
578                        // Let binding already created Option<&T> via .as_ref()
579                        format!("{}_core", p.name)
580                    } else if p.is_ref {
581                        // Let binding created T, need reference for call
582                        format!("&{}_core", p.name)
583                    } else {
584                        format!("{}_core", p.name)
585                    }
586                }
587                TypeRef::String | TypeRef::Char => {
588                    if p.optional {
589                        if p.is_ref {
590                            format!("{}.as_deref()", p.name)
591                        } else {
592                            p.name.clone()
593                        }
594                    } else if promoted {
595                        if p.is_ref {
596                            format!("&{}{}", p.name, unwrap_suffix)
597                        } else {
598                            format!("{}{}", p.name, unwrap_suffix)
599                        }
600                    } else if p.is_ref {
601                        format!("&{}", p.name)
602                    } else {
603                        p.name.clone()
604                    }
605                }
606                TypeRef::Path => {
607                    if promoted {
608                        format!("std::path::PathBuf::from({}{})", p.name, unwrap_suffix)
609                    } else if p.optional && p.is_ref {
610                        format!("{}.as_deref().map(std::path::Path::new)", p.name)
611                    } else if p.optional {
612                        format!("{}.map(std::path::PathBuf::from)", p.name)
613                    } else if p.is_ref {
614                        format!("std::path::Path::new(&{})", p.name)
615                    } else {
616                        format!("std::path::PathBuf::from({})", p.name)
617                    }
618                }
619                TypeRef::Bytes => {
620                    if p.optional {
621                        if p.is_ref {
622                            format!("{}.as_deref()", p.name)
623                        } else {
624                            p.name.clone()
625                        }
626                    } else if promoted {
627                        // is_ref=true: pass &Vec<u8> (core takes &[u8])
628                        // is_ref=false: pass Vec<u8> (core takes owned Vec<u8>)
629                        if p.is_ref {
630                            format!("&{}{}", p.name, unwrap_suffix)
631                        } else {
632                            format!("{}{}", p.name, unwrap_suffix)
633                        }
634                    } else {
635                        // is_ref=true: pass &Vec<u8> (core takes &[u8])
636                        // is_ref=false: pass Vec<u8> (core takes owned Vec<u8>)
637                        if p.is_ref {
638                            format!("&{}", p.name)
639                        } else {
640                            p.name.clone()
641                        }
642                    }
643                }
644                TypeRef::Duration => {
645                    if p.optional {
646                        format!("{}.map(std::time::Duration::from_millis)", p.name)
647                    } else if promoted {
648                        format!("std::time::Duration::from_millis({}{})", p.name, unwrap_suffix)
649                    } else {
650                        format!("std::time::Duration::from_millis({})", p.name)
651                    }
652                }
653                TypeRef::Vec(inner) => {
654                    // Sanitized Vec<tuple>: binding accepts Vec<String> (JSON-encoded tuples).
655                    // Let binding created {name}_core via JSON deserialization.
656                    if matches!(inner.as_ref(), TypeRef::String) && p.sanitized && p.original_type.is_some() {
657                        if p.optional && p.is_ref {
658                            format!("{}_core.as_deref()", p.name)
659                        } else if p.optional {
660                            format!("{}_core", p.name)
661                        } else if p.is_ref {
662                            format!("&{}_core", p.name)
663                        } else {
664                            format!("{}_core", p.name)
665                        }
666                    } else if matches!(inner.as_ref(), TypeRef::Named(_)) {
667                        // Vec<Named>: use let binding that converts each element
668                        if p.optional && p.is_ref {
669                            // Let binding creates Option<Vec<CoreType>>, use as_deref() to get Option<&[CoreType]>
670                            format!("{}_core.as_deref()", p.name)
671                        } else if p.optional {
672                            // Let binding creates Option<Vec<CoreType>>, no ref needed
673                            format!("{}_core", p.name)
674                        } else if p.is_ref {
675                            format!("&{}_core", p.name)
676                        } else {
677                            format!("{}_core", p.name)
678                        }
679                    } else if matches!(inner.as_ref(), TypeRef::String | TypeRef::Char) && p.is_ref {
680                        // Vec<String> with is_ref=true: core expects &[&str].
681                        // Convert via _refs intermediate binding.
682                        if p.optional {
683                            format!("{}.as_deref()", p.name)
684                        } else {
685                            format!("&{}_refs", p.name)
686                        }
687                    } else if promoted {
688                        format!("{}{}", p.name, unwrap_suffix)
689                    } else if p.is_ref && p.optional {
690                        format!("{}.as_deref()", p.name)
691                    } else if p.is_ref {
692                        format!("&{}", p.name)
693                    } else {
694                        p.name.clone()
695                    }
696                }
697                _ => {
698                    if promoted {
699                        format!("{}{}", p.name, unwrap_suffix)
700                    } else if p.is_ref && p.optional {
701                        format!("{}.as_deref()", p.name)
702                    } else if p.is_ref {
703                        format!("&{}", p.name)
704                    } else {
705                        p.name.clone()
706                    }
707                }
708            }
709        })
710        .collect::<Vec<_>>()
711        .join(", ")
712}
713
714/// Generate let bindings for non-opaque Named params, converting them to core types.
715pub fn gen_named_let_bindings_pub(params: &[ParamDef], opaque_types: &AHashSet<String>, core_import: &str) -> String {
716    gen_named_let_bindings(params, opaque_types, core_import)
717}
718
719/// Like `gen_named_let_bindings_pub` but without optional-promotion semantics.
720/// Use this for backends (e.g. WASM) that do not promote non-optional params to `Option<T>`.
721pub fn gen_named_let_bindings_no_promote(
722    params: &[ParamDef],
723    opaque_types: &AHashSet<String>,
724    core_import: &str,
725) -> String {
726    gen_named_let_bindings_inner(params, opaque_types, core_import, false)
727}
728
729pub(super) fn gen_named_let_bindings(
730    params: &[ParamDef],
731    opaque_types: &AHashSet<String>,
732    core_import: &str,
733) -> String {
734    gen_named_let_bindings_inner(params, opaque_types, core_import, true)
735}
736
737/// Variant of `gen_named_let_bindings` for backends where Named non-opaque params
738/// are passed by reference (`&T`) in the function signature (e.g. extendr).
739/// Uses `.clone().into()` instead of `.into()` to convert the borrowed value.
740pub(super) fn gen_named_let_bindings_by_ref(
741    params: &[ParamDef],
742    opaque_types: &AHashSet<String>,
743    core_import: &str,
744) -> String {
745    let mut bindings = String::new();
746    for (idx, p) in params.iter().enumerate() {
747        match &p.ty {
748            TypeRef::Named(name) if !opaque_types.contains(name.as_str()) => {
749                let promoted = crate::shared::is_promoted_optional(params, idx);
750                let core_type_path = format!("{core_import}::{name}");
751                let binding = if p.optional {
752                    crate::template_env::render(
753                        "binding_helpers/named_let_binding_by_ref_optional.jinja",
754                        minijinja::context! {
755                            name => &p.name,
756                            core_type_path => &core_type_path,
757                        },
758                    )
759                } else if promoted {
760                    crate::template_env::render(
761                        "binding_helpers/named_let_binding_by_ref_promoted.jinja",
762                        minijinja::context! {
763                            name => &p.name,
764                            core_type_path => &core_type_path,
765                        },
766                    )
767                } else {
768                    crate::template_env::render(
769                        "binding_helpers/named_let_binding_by_ref_simple.jinja",
770                        minijinja::context! {
771                            name => &p.name,
772                            core_type_path => &core_type_path,
773                        },
774                    )
775                };
776                bindings.push_str(&binding);
777                bindings.push_str("\n    ");
778            }
779            TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Named(n) if !opaque_types.contains(n.as_str())) => {
780                let binding = if p.optional {
781                    crate::template_env::render(
782                        "binding_helpers/vec_named_let_binding_by_ref_optional.jinja",
783                        minijinja::context! {
784                            name => &p.name,
785                        },
786                    )
787                } else {
788                    let promoted = crate::shared::is_promoted_optional(params, idx);
789                    if promoted {
790                        crate::template_env::render(
791                            "binding_helpers/vec_named_let_binding_by_ref_promoted.jinja",
792                            minijinja::context! {
793                                name => &p.name,
794                            },
795                        )
796                    } else {
797                        crate::template_env::render(
798                            "binding_helpers/vec_named_let_binding_by_ref_simple.jinja",
799                            minijinja::context! {
800                                name => &p.name,
801                            },
802                        )
803                    }
804                };
805                bindings.push_str(&binding);
806                bindings.push_str("\n    ");
807            }
808            TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::String | TypeRef::Char) && p.is_ref => {
809                let binding = if p.optional {
810                    crate::template_env::render(
811                        "binding_helpers/vec_string_refs_binding_optional.jinja",
812                        minijinja::context! {
813                            name => &p.name,
814                        },
815                    )
816                } else {
817                    crate::template_env::render(
818                        "binding_helpers/vec_string_refs_binding_simple.jinja",
819                        minijinja::context! {
820                            name => &p.name,
821                        },
822                    )
823                };
824                bindings.push_str(&binding);
825                bindings.push_str("\n    ");
826            }
827            _ => {}
828        }
829    }
830    bindings
831}
832
833fn gen_named_let_bindings_inner(
834    params: &[ParamDef],
835    opaque_types: &AHashSet<String>,
836    core_import: &str,
837    promote: bool,
838) -> String {
839    let mut bindings = String::new();
840    for (idx, p) in params.iter().enumerate() {
841        match &p.ty {
842            TypeRef::Named(name) if !opaque_types.contains(name.as_str()) => {
843                let promoted = promote && crate::shared::is_promoted_optional(params, idx);
844                let core_type_path = format!("{}::{}", core_import, name);
845                let binding = if p.optional {
846                    if p.is_ref {
847                        crate::template_env::render(
848                            "binding_helpers/named_let_binding_optional_ref.jinja",
849                            minijinja::context! {
850                                name => &p.name,
851                                core_type_path => &core_type_path,
852                            },
853                        )
854                    } else {
855                        crate::template_env::render(
856                            "binding_helpers/named_let_binding_optional.jinja",
857                            minijinja::context! {
858                                name => &p.name,
859                                core_type_path => &core_type_path,
860                            },
861                        )
862                    }
863                } else if promoted {
864                    crate::template_env::render(
865                        "binding_helpers/named_let_binding_promoted.jinja",
866                        minijinja::context! {
867                            name => &p.name,
868                            core_type_path => &core_type_path,
869                        },
870                    )
871                } else {
872                    crate::template_env::render(
873                        "binding_helpers/named_let_binding_simple.jinja",
874                        minijinja::context! {
875                            name => &p.name,
876                            core_type_path => &core_type_path,
877                        },
878                    )
879                };
880                bindings.push_str(&binding);
881                bindings.push_str("\n    ");
882            }
883            TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::Named(n) if !opaque_types.contains(n.as_str())) => {
884                let promoted = promote && crate::shared::is_promoted_optional(params, idx);
885                let binding = if p.optional && p.is_ref {
886                    crate::template_env::render(
887                        "binding_helpers/vec_named_let_binding_optional.jinja",
888                        minijinja::context! {
889                            name => &p.name,
890                        },
891                    )
892                } else if p.optional {
893                    crate::template_env::render(
894                        "binding_helpers/vec_named_let_binding_optional_no_ref.jinja",
895                        minijinja::context! {
896                            name => &p.name,
897                        },
898                    )
899                } else if promoted {
900                    crate::template_env::render(
901                        "binding_helpers/vec_named_let_binding_promoted.jinja",
902                        minijinja::context! {
903                            name => &p.name,
904                        },
905                    )
906                } else {
907                    crate::template_env::render(
908                        "binding_helpers/vec_named_let_binding_simple.jinja",
909                        minijinja::context! {
910                            name => &p.name,
911                        },
912                    )
913                };
914                bindings.push_str(&binding);
915                bindings.push_str("\n    ");
916            }
917            // Vec<String> with is_ref=true: core expects &[&str].
918            // Convert Vec<String> to Vec<&str> via intermediate binding.
919            TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::String | TypeRef::Char) && p.is_ref => {
920                let binding = if p.optional {
921                    crate::template_env::render(
922                        "binding_helpers/vec_string_refs_binding_optional.jinja",
923                        minijinja::context! {
924                            name => &p.name,
925                        },
926                    )
927                } else {
928                    crate::template_env::render(
929                        "binding_helpers/vec_string_refs_binding_simple.jinja",
930                        minijinja::context! {
931                            name => &p.name,
932                        },
933                    )
934                };
935                bindings.push_str(&binding);
936                bindings.push_str("\n    ");
937            }
938            // Sanitized Vec<String> (originally Vec<tuple>): deserialize each JSON string.
939            TypeRef::Vec(inner)
940                if matches!(inner.as_ref(), TypeRef::String) && p.sanitized && p.original_type.is_some() =>
941            {
942                let template = if p.optional {
943                    "binding_helpers/sanitized_vec_string_filter_optional.jinja"
944                } else {
945                    "binding_helpers/sanitized_vec_string_filter_simple.jinja"
946                };
947                bindings.push_str(&crate::template_env::render(
948                    template,
949                    minijinja::context! {
950                        name => &p.name,
951                    },
952                ));
953            }
954            _ => {}
955        }
956    }
957    bindings
958}
959
960/// Generate serde-based let bindings for non-opaque Named params.
961/// Serializes binding types to JSON and deserializes to core types.
962/// Used when From impls don't exist (e.g., types with sanitized fields).
963/// `indent` is the whitespace prefix for each generated line (e.g., "    " for functions, "        " for methods).
964/// NOTE: This function should only be called when `cfg.has_serde` is true.
965/// The caller (functions.rs, methods.rs) must gate the call behind a `has_serde` check.
966pub fn gen_serde_let_bindings(
967    params: &[ParamDef],
968    opaque_types: &AHashSet<String>,
969    core_import: &str,
970    err_conv: &str,
971    indent: &str,
972) -> String {
973    let mut bindings = String::new();
974    for (idx, p) in params.iter().enumerate() {
975        let promoted = crate::shared::is_promoted_optional(params, idx);
976        match &p.ty {
977            TypeRef::Named(name) if !opaque_types.contains(name.as_str()) => {
978                let core_path = format!("{}::{}", core_import, name);
979                if p.optional {
980                    bindings.push_str(&crate::template_env::render(
981                        "binding_helpers/serde_named_let_binding_optional.jinja",
982                        minijinja::context! {
983                            name => &p.name,
984                            core_path => core_path,
985                            err_conv => err_conv,
986                            indent => indent,
987                        },
988                    ));
989                } else if promoted {
990                    // Promoted-optional: param is required in core but wrapped in Option<T>
991                    // in the binding because an earlier param is optional. Use unwrap_or_default()
992                    // so JS callers can omit it (pass undefined/null) to get default behaviour.
993                    bindings.push_str(&crate::template_env::render(
994                        "binding_helpers/serde_named_let_binding_promoted.jinja",
995                        minijinja::context! {
996                            name => &p.name,
997                            core_path => core_path,
998                            err_conv => err_conv,
999                            indent => indent,
1000                        },
1001                    ));
1002                } else {
1003                    bindings.push_str(&crate::template_env::render(
1004                        "binding_helpers/serde_named_let_binding_simple.jinja",
1005                        minijinja::context! {
1006                            name => &p.name,
1007                            core_path => core_path,
1008                            err_conv => err_conv,
1009                            indent => indent,
1010                        },
1011                    ));
1012                }
1013            }
1014            TypeRef::Vec(inner) => {
1015                if let TypeRef::Named(name) = inner.as_ref() {
1016                    if !opaque_types.contains(name.as_str()) {
1017                        let core_path = format!("{}::{}", core_import, name);
1018                        if p.optional {
1019                            bindings.push_str(&crate::template_env::render(
1020                                "binding_helpers/serde_vec_named_optional.jinja",
1021                                minijinja::context! {
1022                                    name => &p.name,
1023                                    core_path => core_path,
1024                                    err_conv => err_conv,
1025                                    indent => indent,
1026                                },
1027                            ));
1028                        } else {
1029                            bindings.push_str(&crate::template_env::render(
1030                                "binding_helpers/serde_vec_named_simple.jinja",
1031                                minijinja::context! {
1032                                    name => &p.name,
1033                                    core_path => core_path,
1034                                    err_conv => err_conv,
1035                                    indent => indent,
1036                                },
1037                            ));
1038                        }
1039                    }
1040                } else if matches!(inner.as_ref(), TypeRef::String) && p.sanitized && p.original_type.is_some() {
1041                    // Sanitized Vec<tuple>: binding accepts Vec<String> (JSON-encoded tuple items).
1042                    // Deserialize each JSON string as a tuple using serde_json.
1043                    let template = if p.optional {
1044                        "binding_helpers/serde_sanitized_vec_string_optional.jinja"
1045                    } else {
1046                        "binding_helpers/serde_sanitized_vec_string_simple.jinja"
1047                    };
1048                    bindings.push_str(&crate::template_env::render(
1049                        template,
1050                        minijinja::context! {
1051                            name => &p.name,
1052                            err_conv => err_conv,
1053                            indent => indent,
1054                        },
1055                    ));
1056                }
1057            }
1058            _ => {}
1059        }
1060    }
1061    bindings
1062}
1063
1064/// Check if params contain any non-opaque Named types that need let bindings.
1065/// This includes direct Named types, Vec<Named> types, Vec<String> params
1066/// with is_ref=true (which need a Vec<&str> intermediate to pass as &[&str]),
1067/// and sanitized Vec<String> params (which are JSON-deserialized to tuples).
1068pub fn has_named_params(params: &[ParamDef], opaque_types: &AHashSet<String>) -> bool {
1069    params.iter().any(|p| match &p.ty {
1070        TypeRef::Named(name) if !opaque_types.contains(name.as_str()) => true,
1071        TypeRef::Vec(inner) => {
1072            // Vec<Named> always needs a conversion let binding.
1073            // Sanitized Vec<String> needs JSON deserialization via let binding.
1074            // Vec<String> with is_ref=true needs a _refs let binding for &[&str] conversion.
1075            matches!(inner.as_ref(), TypeRef::Named(name) if !opaque_types.contains(name.as_str()))
1076                || (matches!(inner.as_ref(), TypeRef::String | TypeRef::Char) && p.is_ref)
1077                || (matches!(inner.as_ref(), TypeRef::String) && p.sanitized && p.original_type.is_some())
1078        }
1079        _ => false,
1080    })
1081}
1082
1083/// Check if a param type is safe for non-opaque delegation (no complex conversions needed).
1084/// Vec and Map params can cause type mismatches (e.g. Vec<String> vs &[&str]).
1085pub fn is_simple_non_opaque_param(ty: &TypeRef) -> bool {
1086    match ty {
1087        TypeRef::Primitive(_)
1088        | TypeRef::String
1089        | TypeRef::Char
1090        | TypeRef::Bytes
1091        | TypeRef::Path
1092        | TypeRef::Unit
1093        | TypeRef::Duration => true,
1094        TypeRef::Optional(inner) => is_simple_non_opaque_param(inner),
1095        _ => false,
1096    }
1097}
1098
1099/// Generate a lossy binding→core struct literal for non-opaque delegation.
1100/// Sanitized fields use `Default::default()`, non-sanitized fields are cloned and converted.
1101/// Fields are accessed via `self.` (behind &self), so all non-Copy types need `.clone()`.
1102///
1103/// `opaque_types` is the set of opaque type names (Arc-wrapped handles, trait bridge aliases,
1104/// etc.). Fields whose `TypeRef::Named` type is in this set have no `From` impl in the binding
1105/// layer, so `Default::default()` is emitted for them instead of `.clone().into()`.
1106///
1107/// NOTE: This assumes all binding struct fields implement Clone. If a field type does not
1108/// implement Clone (e.g., `Mutex<T>`), it should be marked as `sanitized=true` so that
1109/// `Default::default()` is used instead of calling `.clone()`. Backends that exclude types
1110/// should mark such fields appropriately.
1111pub fn gen_lossy_binding_to_core_fields(
1112    typ: &TypeDef,
1113    core_import: &str,
1114    option_duration_on_defaults: bool,
1115    opaque_types: &AHashSet<String>,
1116    cast_uints_to_i32: bool,
1117    cast_large_ints_to_f64: bool,
1118    skip_types: &[String],
1119) -> String {
1120    gen_lossy_binding_to_core_fields_inner(
1121        typ,
1122        core_import,
1123        false,
1124        option_duration_on_defaults,
1125        opaque_types,
1126        cast_uints_to_i32,
1127        cast_large_ints_to_f64,
1128        skip_types,
1129    )
1130}
1131
1132/// Same as `gen_lossy_binding_to_core_fields` but declares `core_self` as mutable.
1133pub fn gen_lossy_binding_to_core_fields_mut(
1134    typ: &TypeDef,
1135    core_import: &str,
1136    option_duration_on_defaults: bool,
1137    opaque_types: &AHashSet<String>,
1138    cast_uints_to_i32: bool,
1139    cast_large_ints_to_f64: bool,
1140    skip_types: &[String],
1141) -> String {
1142    gen_lossy_binding_to_core_fields_inner(
1143        typ,
1144        core_import,
1145        true,
1146        option_duration_on_defaults,
1147        opaque_types,
1148        cast_uints_to_i32,
1149        cast_large_ints_to_f64,
1150        skip_types,
1151    )
1152}
1153
1154#[allow(clippy::too_many_arguments)]
1155fn gen_lossy_binding_to_core_fields_inner(
1156    typ: &TypeDef,
1157    core_import: &str,
1158    needs_mut: bool,
1159    option_duration_on_defaults: bool,
1160    opaque_types: &AHashSet<String>,
1161    cast_uints_to_i32: bool,
1162    cast_large_ints_to_f64: bool,
1163    skip_types: &[String],
1164) -> String {
1165    let core_path = crate::conversions::core_type_path(typ, core_import);
1166    let mut_kw = if needs_mut { "mut " } else { "" };
1167    // When has_stripped_cfg_fields is true we emit ..Default::default() at the end of the
1168    // struct literal to fill cfg-gated fields that were stripped from the binding IR.
1169    // Suppress clippy::needless_update because the fields only exist when the corresponding
1170    // feature is enabled — without the feature, clippy thinks the spread is redundant.
1171    let allow = if typ.has_stripped_cfg_fields {
1172        "#[allow(clippy::needless_update)]\n        "
1173    } else {
1174        ""
1175    };
1176    let mut out = format!("{allow}let {mut_kw}core_self = {core_path} {{\n");
1177    for field in &typ.fields {
1178        // Skip cfg-gated fields — they are absent from the binding struct.
1179        // The ..Default::default() spread below fills them when the feature is enabled.
1180        if field.cfg.is_some() {
1181            continue;
1182        }
1183        let name = &field.name;
1184        if field.sanitized && field.core_wrapper != CoreWrapper::Cow {
1185            out.push_str(&crate::template_env::render(
1186                "binding_helpers/struct_field_default.jinja",
1187                minijinja::context! {
1188                    name => &field.name,
1189                },
1190            ));
1191            out.push('\n');
1192            continue;
1193        }
1194        // Opaque-type fields (Arc-wrapped handles, trait bridge aliases) have no From impl
1195        // in the binding layer. Emit Default::default() so the apply_update / clone-mutate
1196        // paths compile without needing From<Arc<Py<PyAny>>> for VisitorHandle, etc.
1197        // This covers both bare Named opaque fields and Optional<Named opaque> fields.
1198        let is_opaque_named = match &field.ty {
1199            TypeRef::Named(n) => opaque_types.contains(n.as_str()),
1200            TypeRef::Optional(inner) => {
1201                matches!(inner.as_ref(), TypeRef::Named(n) if opaque_types.contains(n.as_str()))
1202            }
1203            _ => false,
1204        };
1205        if is_opaque_named {
1206            out.push_str(&crate::template_env::render(
1207                "binding_helpers/struct_field_default.jinja",
1208                minijinja::context! {
1209                    name => &field.name,
1210                },
1211            ));
1212            out.push('\n');
1213            continue;
1214        }
1215        // Skip types: output-only types (e.g. flat data enums) that have no From impl
1216        // from the binding layer. Emit Default::default() so method body compiles.
1217        let is_skip_named = match &field.ty {
1218            TypeRef::Named(n) => skip_types.contains(n),
1219            TypeRef::Optional(inner) => {
1220                matches!(inner.as_ref(), TypeRef::Named(n) if skip_types.contains(n))
1221            }
1222            _ => false,
1223        };
1224        if is_skip_named {
1225            out.push_str(&crate::template_env::render(
1226                "binding_helpers/default_field.jinja",
1227                minijinja::context! {
1228                    name => &name,
1229                },
1230            ));
1231            continue;
1232        }
1233        let expr = match &field.ty {
1234            TypeRef::Primitive(p) if cast_uints_to_i32 && needs_i32_cast(p) => {
1235                let core_ty = core_prim_str(p);
1236                if field.optional {
1237                    format!("self.{name}.map(|v| v as {core_ty})")
1238                } else {
1239                    format!("self.{name} as {core_ty}")
1240                }
1241            }
1242            TypeRef::Primitive(p) if cast_large_ints_to_f64 && needs_f64_cast(p) => {
1243                let core_ty = core_prim_str(p);
1244                if field.optional {
1245                    format!("self.{name}.map(|v| v as {core_ty})")
1246                } else {
1247                    format!("self.{name} as {core_ty}")
1248                }
1249            }
1250            TypeRef::Primitive(_) => format!("self.{name}"),
1251            TypeRef::Duration => {
1252                if field.optional {
1253                    format!("self.{name}.map(std::time::Duration::from_millis)")
1254                } else if option_duration_on_defaults && typ.has_default {
1255                    // When option_duration_on_defaults is true, non-optional Duration fields
1256                    // on has_default types are stored as Option<u64> in the binding struct.
1257                    // Use .map(...).unwrap_or_default() so that None falls back to the core
1258                    // type's Default (e.g. Duration::from_secs(30)) rather than Duration::ZERO.
1259                    format!("self.{name}.map(std::time::Duration::from_millis).unwrap_or_default()")
1260                } else {
1261                    format!("std::time::Duration::from_millis(self.{name})")
1262                }
1263            }
1264            TypeRef::String => {
1265                if field.core_wrapper == CoreWrapper::Cow {
1266                    format!("self.{name}.clone().into()")
1267                } else {
1268                    format!("self.{name}.clone()")
1269                }
1270            }
1271            // Bytes: binding stores Vec<u8>. When core_wrapper == Bytes, core expects
1272            // bytes::Bytes so we must call .into() to convert Vec<u8> → Bytes.
1273            // When core_wrapper == None, the core field is also Vec<u8> (plain clone).
1274            TypeRef::Bytes => {
1275                if field.core_wrapper == CoreWrapper::Bytes {
1276                    format!("self.{name}.clone().into()")
1277                } else {
1278                    format!("self.{name}.clone()")
1279                }
1280            }
1281            TypeRef::Char => {
1282                if field.optional {
1283                    format!("self.{name}.as_ref().and_then(|s| s.chars().next())")
1284                } else {
1285                    format!("self.{name}.chars().next().unwrap_or('*')")
1286                }
1287            }
1288            TypeRef::Path => {
1289                if field.optional {
1290                    format!("self.{name}.clone().map(Into::into)")
1291                } else {
1292                    format!("self.{name}.clone().into()")
1293                }
1294            }
1295            TypeRef::Named(_) => {
1296                if field.optional {
1297                    format!("self.{name}.clone().map(Into::into)")
1298                } else {
1299                    format!("self.{name}.clone().into()")
1300                }
1301            }
1302            TypeRef::Vec(inner) => match inner.as_ref() {
1303                TypeRef::Named(_) => {
1304                    if field.optional {
1305                        // Option<Vec<Named(T)>>: map over the Option, then convert each element
1306                        format!("self.{name}.clone().map(|v| v.into_iter().map(Into::into).collect())")
1307                    } else {
1308                        format!("self.{name}.clone().into_iter().map(Into::into).collect()")
1309                    }
1310                }
1311                // Vec<u8/u16/u32/i8/i16> stored as Vec<i32> in binding → cast each element back
1312                TypeRef::Primitive(p) if cast_uints_to_i32 && needs_i32_cast(p) => {
1313                    let core_ty = core_prim_str(p);
1314                    if field.optional {
1315                        format!("self.{name}.clone().map(|v| v.into_iter().map(|x| x as {core_ty}).collect())")
1316                    } else {
1317                        format!("self.{name}.clone().into_iter().map(|v| v as {core_ty}).collect()")
1318                    }
1319                }
1320                // Vec<usize/u64/i64/isize/f32> stored as Vec<f64> in binding → cast each element back
1321                TypeRef::Primitive(p) if cast_large_ints_to_f64 && needs_f64_cast(p) => {
1322                    let core_ty = core_prim_str(p);
1323                    if field.optional {
1324                        format!("self.{name}.clone().map(|v| v.into_iter().map(|x| x as {core_ty}).collect())")
1325                    } else {
1326                        format!("self.{name}.clone().into_iter().map(|v| v as {core_ty}).collect()")
1327                    }
1328                }
1329                _ => format!("self.{name}.clone()"),
1330            },
1331            TypeRef::Optional(inner) => {
1332                // When field.optional is also true, the binding field was flattened from
1333                // Option<Option<T>> to Option<T>. Core expects Option<Option<T>>, so wrap
1334                // with .map(Some) to reconstruct the double-optional.
1335                let base = match inner.as_ref() {
1336                    TypeRef::Named(_) => {
1337                        format!("self.{name}.clone().map(Into::into)")
1338                    }
1339                    TypeRef::Duration => {
1340                        format!("self.{name}.map(|v| std::time::Duration::from_millis(v as u64))")
1341                    }
1342                    TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Named(_)) => {
1343                        format!("self.{name}.clone().map(|v| v.into_iter().map(Into::into).collect())")
1344                    }
1345                    // Option<Vec<u8/u16/u32/i8/i16>> stored as Option<Vec<i32>> → cast elements back
1346                    TypeRef::Vec(vi) => match vi.as_ref() {
1347                        TypeRef::Primitive(p) if cast_uints_to_i32 && needs_i32_cast(p) => {
1348                            let core_ty = core_prim_str(p);
1349                            format!("self.{name}.clone().map(|v| v.into_iter().map(|x| x as {core_ty}).collect())")
1350                        }
1351                        // Option<Vec<usize/u64/i64/f32>> stored as Option<Vec<f64>> → cast elements back
1352                        TypeRef::Primitive(p) if cast_large_ints_to_f64 && needs_f64_cast(p) => {
1353                            let core_ty = core_prim_str(p);
1354                            format!("self.{name}.clone().map(|v| v.into_iter().map(|x| x as {core_ty}).collect())")
1355                        }
1356                        _ => format!("self.{name}.clone()"),
1357                    },
1358                    _ => format!("self.{name}.clone()"),
1359                };
1360                if field.optional {
1361                    format!("({base}).map(Some)")
1362                } else {
1363                    base
1364                }
1365            }
1366            TypeRef::Map(_, v) => match v.as_ref() {
1367                TypeRef::Json => {
1368                    // HashMap<String, String> (binding) → HashMap<K, Value> (core).
1369                    // Emit `k.into()` so wrapped string keys (`Cow`, `Box<str>`, `Arc<str>`)
1370                    // — which the type resolver collapses to `TypeRef::String` — convert
1371                    // correctly. For a real `String` core key it is a no-op.
1372                    if field.optional {
1373                        format!(
1374                            "self.{name}.clone().map(|m| m.into_iter().map(|(k, v)| \
1375                                 (k.into(), serde_json::from_str(&v).unwrap_or(serde_json::Value::String(v)))).collect())"
1376                        )
1377                    } else {
1378                        format!(
1379                            "self.{name}.clone().into_iter().map(|(k, v)| \
1380                                 (k.into(), serde_json::from_str(&v).unwrap_or(serde_json::Value::String(v)))).collect()"
1381                        )
1382                    }
1383                }
1384                // Named values: each value needs Into conversion to bridge the binding wrapper
1385                // type into the core type (e.g. PyExtractionPattern → ExtractionPattern).
1386                TypeRef::Named(_) => {
1387                    if field.optional {
1388                        format!(
1389                            "self.{name}.clone().map(|m| m.into_iter().map(|(k, v)| (k.into(), v.into())).collect())"
1390                        )
1391                    } else {
1392                        format!("self.{name}.clone().into_iter().map(|(k, v)| (k.into(), v.into())).collect()")
1393                    }
1394                }
1395                // Map values that are u8/u16/u32/i8/i16 stored as i32 in binding → cast back
1396                TypeRef::Primitive(p) if cast_uints_to_i32 && needs_i32_cast(p) => {
1397                    let core_ty = core_prim_str(p);
1398                    if field.optional {
1399                        format!(
1400                            "self.{name}.clone().map(|m| m.into_iter().map(|(k, v)| (k.into(), v as {core_ty})).collect())"
1401                        )
1402                    } else {
1403                        format!("self.{name}.clone().into_iter().map(|(k, v)| (k.into(), v as {core_ty})).collect()")
1404                    }
1405                }
1406                // Map values that are usize/u64/i64/isize/f32 stored as f64 in binding → cast back
1407                TypeRef::Primitive(p) if cast_large_ints_to_f64 && needs_f64_cast(p) => {
1408                    let core_ty = core_prim_str(p);
1409                    if field.optional {
1410                        format!(
1411                            "self.{name}.clone().map(|m| m.into_iter().map(|(k, v)| (k.into(), v as {core_ty})).collect())"
1412                        )
1413                    } else {
1414                        format!("self.{name}.clone().into_iter().map(|(k, v)| (k.into(), v as {core_ty})).collect()")
1415                    }
1416                }
1417                // Collect to handle HashMap↔BTreeMap conversion
1418                _ => {
1419                    if field.optional {
1420                        format!("self.{name}.clone().map(|m| m.into_iter().map(|(k, v)| (k.into(), v)).collect())")
1421                    } else {
1422                        format!("self.{name}.clone().into_iter().map(|(k, v)| (k.into(), v)).collect()")
1423                    }
1424                }
1425            },
1426            TypeRef::Unit => format!("self.{name}.clone()"),
1427            TypeRef::Json => {
1428                // String (binding) → serde_json::Value (core)
1429                if field.optional {
1430                    format!("self.{name}.as_ref().and_then(|s| serde_json::from_str(s).ok())")
1431                } else {
1432                    format!("serde_json::from_str(&self.{name}).unwrap_or_default()")
1433                }
1434            }
1435        };
1436        // Newtype wrapping: when the field was resolved from a newtype (e.g. NodeIndex → u32),
1437        // re-wrap the binding value into the newtype for the core struct literal.
1438        // When `optional=true` and `ty` is a plain Primitive (not TypeRef::Optional), the core
1439        // field is actually `Option<NewtypeT>`, so we must use `.map(NewtypeT)` not `NewtypeT(...)`.
1440        let expr = if let Some(newtype_path) = &field.newtype_wrapper {
1441            match &field.ty {
1442                TypeRef::Optional(_) => format!("({expr}).map({newtype_path})"),
1443                TypeRef::Vec(_) => format!("({expr}).into_iter().map({newtype_path}).collect::<Vec<_>>()"),
1444                _ if field.optional => format!("({expr}).map({newtype_path})"),
1445                _ => format!("{newtype_path}({expr})"),
1446            }
1447        } else {
1448            expr
1449        };
1450        out.push_str(&crate::template_env::render(
1451            "binding_helpers/struct_field_line.jinja",
1452            minijinja::context! {
1453                name => &field.name,
1454                expr => &expr,
1455            },
1456        ));
1457        out.push('\n');
1458    }
1459    // Use ..Default::default() to fill cfg-gated fields stripped from the IR
1460    if typ.has_stripped_cfg_fields {
1461        out.push_str("            ..Default::default()\n");
1462    }
1463    out.push_str("        };\n        ");
1464    out
1465}
1466
1467/// Generate the body for an async call, unified across methods, static methods, and free functions.
1468///
1469/// - `core_call`: the expression to await, e.g. `inner.method(args)` or `CoreType::fn(args)`.
1470///   For Pyo3FutureIntoPy opaque methods this should reference `inner` (the Arc clone);
1471///   for all other patterns it may reference `self.inner` or a static call expression.
1472/// - `cfg`: binding configuration (determines which async pattern to emit)
1473/// - `has_error`: whether the core call returns a `Result`
1474/// - `return_wrap`: expression to produce the binding return value from `result`,
1475///   e.g. `"result"` or `"TypeName::from(result)"`
1476///
1477/// - `is_opaque`: whether the binding type is Arc-wrapped (affects TokioBlockOn wrapping)
1478/// - `inner_clone_line`: optional statement emitted before the pattern-specific body,
1479///   e.g. `"let inner = self.inner.clone();\n        "` for opaque instance methods, or `""`.
1480///   Required when `core_call` references `inner` (Pyo3FutureIntoPy opaque case).
1481#[allow(clippy::too_many_arguments)]
1482pub fn gen_async_body(
1483    core_call: &str,
1484    cfg: &RustBindingConfig,
1485    has_error: bool,
1486    return_wrap: &str,
1487    is_opaque: bool,
1488    inner_clone_line: &str,
1489    is_unit_return: bool,
1490    return_type: Option<&str>,
1491) -> String {
1492    let pattern_body = match cfg.async_pattern {
1493        AsyncPattern::Pyo3FutureIntoPy => {
1494            let result_handling = if has_error {
1495                format!(
1496                    "let result = {core_call}.await\n            \
1497                     .map_err(|e| PyErr::new::<PyRuntimeError, _>(e.to_string()))?;"
1498                )
1499            } else if is_unit_return {
1500                format!("{core_call}.await;")
1501            } else {
1502                format!("let result = {core_call}.await;")
1503            };
1504            let (ok_expr, extra_binding) = if is_unit_return && !has_error {
1505                ("()".to_string(), String::new())
1506            } else if return_wrap.contains(".into()") || return_wrap.contains("::from(") {
1507                // When return_wrap contains type conversions like .into() or ::from(),
1508                // bind to a variable to help type inference for the generic future_into_py.
1509                // This avoids E0283 "type annotations needed".
1510                let wrapped_var = "wrapped_result";
1511                let binding = if let Some(ret_type) = return_type {
1512                    // Add explicit type annotation to help type inference
1513                    format!("let {wrapped_var}: {ret_type} = {return_wrap};\n            ")
1514                } else {
1515                    format!("let {wrapped_var} = {return_wrap};\n            ")
1516                };
1517                (wrapped_var.to_string(), binding)
1518            } else {
1519                (return_wrap.to_string(), String::new())
1520            };
1521            crate::template_env::render(
1522                "binding_helpers/async_body_pyo3.jinja",
1523                minijinja::context! {
1524                    result_handling => result_handling,
1525                    extra_binding => extra_binding,
1526                    ok_expr => ok_expr,
1527                },
1528            )
1529        }
1530        AsyncPattern::WasmNativeAsync => {
1531            let result_handling = if has_error {
1532                format!(
1533                    "let result = {core_call}.await\n        \
1534                     .map_err(|e| JsValue::from_str(&e.to_string()))?;"
1535                )
1536            } else if is_unit_return {
1537                format!("{core_call}.await;")
1538            } else {
1539                format!("let result = {core_call}.await;")
1540            };
1541            let ok_expr = if is_unit_return && !has_error {
1542                "()"
1543            } else {
1544                return_wrap
1545            };
1546            crate::template_env::render(
1547                "binding_helpers/async_body_wasm.jinja",
1548                minijinja::context! {
1549                    result_handling => result_handling,
1550                    ok_expr => ok_expr,
1551                },
1552            )
1553        }
1554        AsyncPattern::NapiNativeAsync => {
1555            let result_handling = if has_error {
1556                format!(
1557                    "let result = {core_call}.await\n            \
1558                     .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;"
1559                )
1560            } else if is_unit_return {
1561                format!("{core_call}.await;")
1562            } else {
1563                format!("let result = {core_call}.await;")
1564            };
1565            let (needs_ok_wrapper, ok_expr) = if !has_error && !is_unit_return {
1566                // No error type: return value directly without Ok() wrapper
1567                (false, return_wrap.to_string())
1568            } else {
1569                let expr = if is_unit_return && !has_error {
1570                    "()".to_string()
1571                } else {
1572                    return_wrap.to_string()
1573                };
1574                (true, expr)
1575            };
1576            crate::template_env::render(
1577                "binding_helpers/async_body_napi.jinja",
1578                minijinja::context! {
1579                    result_handling => result_handling,
1580                    needs_ok_wrapper => needs_ok_wrapper,
1581                    ok_expr => ok_expr,
1582                    return_wrap => return_wrap,
1583                },
1584            )
1585        }
1586        AsyncPattern::TokioBlockOn => {
1587            let rt_new = "tokio::runtime::Runtime::new()\
1588                          .map_err(|e| extendr_api::Error::Other(e.to_string()))?";
1589            crate::template_env::render(
1590                "binding_helpers/async_body_tokio.jinja",
1591                minijinja::context! {
1592                    has_error => has_error,
1593                    is_opaque => is_opaque,
1594                    is_unit_return => is_unit_return,
1595                    core_call => core_call,
1596                    return_wrap => return_wrap,
1597                    rt_new => rt_new,
1598                },
1599            )
1600        }
1601        AsyncPattern::None => "todo!(\"async not supported by backend\")".to_string(),
1602    };
1603    if inner_clone_line.is_empty() {
1604        pattern_body
1605    } else {
1606        format!("{inner_clone_line}{pattern_body}")
1607    }
1608}
1609
1610/// Generate a compilable body for functions that can't be auto-delegated.
1611/// Returns a default value or error instead of `todo!()` which would panic.
1612///
1613/// `opaque_types` is the set of opaque type names (Arc-wrapped). Opaque types do not
1614/// implement `Default`, so returning `Default::default()` for their Named return types
1615/// would fail to compile. For those cases a `todo!()` body is emitted instead.
1616pub fn gen_unimplemented_body(
1617    return_type: &TypeRef,
1618    fn_name: &str,
1619    has_error: bool,
1620    cfg: &RustBindingConfig,
1621    params: &[ParamDef],
1622    opaque_types: &AHashSet<String>,
1623) -> String {
1624    // Suppress unused_variables by binding all params to `_`
1625    let suppress = if params.is_empty() {
1626        String::new()
1627    } else {
1628        let names: Vec<&str> = params.iter().map(|p| p.name.as_str()).collect();
1629        if names.len() == 1 {
1630            format!("let _ = {};\n        ", names[0])
1631        } else {
1632            format!("let _ = ({});\n        ", names.join(", "))
1633        }
1634    };
1635    let err_msg = format!("Not implemented: {fn_name}");
1636    let body = if has_error {
1637        // Backend-specific error return
1638        match cfg.async_pattern {
1639            AsyncPattern::Pyo3FutureIntoPy => {
1640                format!("Err(pyo3::exceptions::PyNotImplementedError::new_err(\"{err_msg}\"))")
1641            }
1642            AsyncPattern::NapiNativeAsync => {
1643                format!("Err(napi::Error::new(napi::Status::GenericFailure, \"{err_msg}\"))")
1644            }
1645            AsyncPattern::WasmNativeAsync => {
1646                format!("Err(JsValue::from_str(\"{err_msg}\"))")
1647            }
1648            // extendr uses `Result<T, extendr_api::Error>`; cast_uints_to_i32 is a flag
1649            // unique to the extendr backend, so use it as a sentinel for the Err wrapping.
1650            _ if cfg.cast_uints_to_i32 => {
1651                format!("Err(extendr_api::Error::Other(\"{err_msg}\".to_string()))")
1652            }
1653            _ => format!("Err(\"{err_msg}\".to_string())"),
1654        }
1655    } else {
1656        // Return type-appropriate default
1657        match return_type {
1658            TypeRef::Unit => "()".to_string(),
1659            TypeRef::String | TypeRef::Char | TypeRef::Path => format!("String::from(\"[unimplemented: {fn_name}]\")"),
1660            TypeRef::Bytes => "Vec::new()".to_string(),
1661            TypeRef::Primitive(p) => match p {
1662                alef_core::ir::PrimitiveType::Bool => "false".to_string(),
1663                alef_core::ir::PrimitiveType::F32 => "0.0f32".to_string(),
1664                alef_core::ir::PrimitiveType::F64 => "0.0f64".to_string(),
1665                _ => "0".to_string(),
1666            },
1667            TypeRef::Optional(_) => "None".to_string(),
1668            TypeRef::Vec(_) => "Vec::new()".to_string(),
1669            TypeRef::Map(_, _) => "Default::default()".to_string(),
1670            TypeRef::Duration => "0".to_string(),
1671            TypeRef::Named(name) => {
1672                // Opaque types (Arc-wrapped) do not implement Default — use todo!() to
1673                // produce a compilable placeholder that panics at runtime if called.
1674                // Non-opaque Named types (config structs) do derive Default, so use that.
1675                if opaque_types.contains(name.as_str()) {
1676                    format!("todo!(\"{err_msg}\")")
1677                } else {
1678                    "Default::default()".to_string()
1679                }
1680            }
1681            TypeRef::Json => {
1682                // Json return without error type: return Default::default()
1683                "Default::default()".to_string()
1684            }
1685        }
1686    };
1687    format!("{suppress}{body}")
1688}