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