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