Skip to main content

alef_codegen/generators/
binding_helpers.rs

1use crate::generators::{AsyncPattern, RustBindingConfig};
2use ahash::AHashSet;
3use alef_core::ir::{ParamDef, TypeDef, TypeRef};
4use std::fmt::Write;
5
6/// Wrap a core-call result for opaque delegation methods.
7///
8/// - `TypeRef::Named(n)` where `n == type_name` → re-wrap in `Self { inner: Arc::new(...) }`
9/// - `TypeRef::Named(n)` where `n` is another opaque type → wrap in `{n} { inner: Arc::new(...) }`
10/// - `TypeRef::Named(n)` where `n` is a non-opaque type → `todo!()` placeholder (From may not exist)
11/// - Everything else (primitives, String, Vec, etc.) → pass through unchanged
12/// - `TypeRef::Unit` → pass through unchanged
13///
14/// When `returns_cow` is true the core method returns `Cow<'_, T>`. `.into_owned()` is emitted
15/// before any further type conversion to obtain an owned `T`.
16pub fn wrap_return(
17    expr: &str,
18    return_type: &TypeRef,
19    type_name: &str,
20    opaque_types: &AHashSet<String>,
21    self_is_opaque: bool,
22    returns_ref: bool,
23    returns_cow: bool,
24) -> String {
25    match return_type {
26        TypeRef::Named(n) if n == type_name && self_is_opaque => {
27            if returns_cow {
28                format!("Self {{ inner: Arc::new({expr}.into_owned()) }}")
29            } else if returns_ref {
30                format!("Self {{ inner: Arc::new({expr}.clone()) }}")
31            } else {
32                format!("Self {{ inner: Arc::new({expr}) }}")
33            }
34        }
35        TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
36            if returns_cow {
37                format!("{n} {{ inner: Arc::new({expr}.into_owned()) }}")
38            } else if returns_ref {
39                format!("{n} {{ inner: Arc::new({expr}.clone()) }}")
40            } else {
41                format!("{n} {{ inner: Arc::new({expr}) }}")
42            }
43        }
44        TypeRef::Named(_) => {
45            // Non-opaque Named return type — use .into() for core→binding From conversion.
46            // When the core returns a Cow, call .into_owned() first to get an owned T.
47            // When the core returns a reference, clone first since From<&T> typically doesn't exist.
48            if returns_cow {
49                format!("{expr}.into_owned().into()")
50            } else if returns_ref {
51                format!("{expr}.clone().into()")
52            } else {
53                format!("{expr}.into()")
54            }
55        }
56        // String/Bytes: only convert when the core returns a reference (&str→String, &[u8]→Vec<u8>).
57        // When owned (returns_ref=false), both sides are already String/Vec<u8> — skip .into().
58        TypeRef::String | TypeRef::Bytes => {
59            if returns_ref {
60                format!("{expr}.into()")
61            } else {
62                expr.to_string()
63            }
64        }
65        // Path: PathBuf→String needs to_string_lossy, &Path→String too
66        TypeRef::Path => format!("{expr}.to_string_lossy().to_string()"),
67        // Duration: core returns std::time::Duration, binding uses u64 (millis)
68        TypeRef::Duration => format!("{expr}.as_millis() as u64"),
69        // Json: serde_json::Value needs serialization to string
70        TypeRef::Json => format!("{expr}.to_string()"),
71        // Optional: wrap inner conversion in .map(...)
72        TypeRef::Optional(inner) => match inner.as_ref() {
73            TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
74                if returns_ref {
75                    format!("{expr}.map(|v| {n} {{ inner: Arc::new(v.clone()) }})")
76                } else {
77                    format!("{expr}.map(|v| {n} {{ inner: Arc::new(v) }})")
78                }
79            }
80            TypeRef::Named(_) => {
81                if returns_ref {
82                    format!("{expr}.map(|v| v.clone().into())")
83                } else {
84                    format!("{expr}.map(Into::into)")
85                }
86            }
87            TypeRef::Path => {
88                format!("{expr}.map(Into::into)")
89            }
90            TypeRef::String | TypeRef::Bytes => {
91                if returns_ref {
92                    format!("{expr}.map(Into::into)")
93                } else {
94                    expr.to_string()
95                }
96            }
97            TypeRef::Duration => format!("{expr}.map(|d| d.as_millis() as u64)"),
98            TypeRef::Json => format!("{expr}.map(ToString::to_string)"),
99            _ => expr.to_string(),
100        },
101        // Vec: map each element through the appropriate conversion
102        TypeRef::Vec(inner) => match inner.as_ref() {
103            TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
104                if returns_ref {
105                    format!("{expr}.into_iter().map(|v| {n} {{ inner: Arc::new(v.clone()) }}).collect()")
106                } else {
107                    format!("{expr}.into_iter().map(|v| {n} {{ inner: Arc::new(v) }}).collect()")
108                }
109            }
110            TypeRef::Named(_) => {
111                if returns_ref {
112                    format!("{expr}.into_iter().map(|v| v.clone().into()).collect()")
113                } else {
114                    format!("{expr}.into_iter().map(Into::into).collect()")
115                }
116            }
117            TypeRef::Path => {
118                format!("{expr}.into_iter().map(Into::into).collect()")
119            }
120            TypeRef::String | TypeRef::Bytes => {
121                if returns_ref {
122                    format!("{expr}.into_iter().map(Into::into).collect()")
123                } else {
124                    expr.to_string()
125                }
126            }
127            _ => expr.to_string(),
128        },
129        _ => expr.to_string(),
130    }
131}
132
133/// Unwrap a newtype return value when `return_newtype_wrapper` is set.
134///
135/// Core function returns a newtype (e.g. `NodeIndex(u32)`), but the binding return type
136/// is the inner type (e.g. `u32`). Access `.0` to unwrap the newtype.
137pub fn apply_return_newtype_unwrap(expr: &str, return_newtype_wrapper: &Option<String>) -> String {
138    match return_newtype_wrapper {
139        Some(_) => format!("({expr}).0"),
140        None => expr.to_string(),
141    }
142}
143
144/// Build call argument expressions from parameters.
145/// - Opaque Named types: unwrap Arc wrapper via `(*param.inner).clone()`
146/// - Non-opaque Named types: `.into()` for From conversion
147/// - String/Path/Bytes: `&param` since core functions typically take `&str`/`&Path`/`&[u8]`
148/// - Params with `newtype_wrapper` set: re-wrap the raw value in the newtype constructor
149///   (e.g., `NodeIndex(parent)`) since the binding resolved `NodeIndex(u32)` → `u32`.
150pub fn gen_call_args(params: &[ParamDef], opaque_types: &AHashSet<String>) -> String {
151    params
152        .iter()
153        .enumerate()
154        .map(|(idx, p)| {
155            let promoted = crate::shared::is_promoted_optional(params, idx);
156            // If a required param was promoted to optional, unwrap it before use
157            let unwrap_suffix = if promoted {
158                format!(".expect(\"'{}' is required\")", p.name)
159            } else {
160                String::new()
161            };
162            // If this param's type was resolved from a newtype (e.g. NodeIndex(u32) → u32),
163            // re-wrap the raw value back into the newtype when calling core.
164            if let Some(newtype_path) = &p.newtype_wrapper {
165                return if p.optional {
166                    format!("{}.map({newtype_path})", p.name)
167                } else if promoted {
168                    format!("{newtype_path}({}{})", p.name, unwrap_suffix)
169                } else {
170                    format!("{newtype_path}({})", p.name)
171                };
172            }
173            match &p.ty {
174                TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
175                    // Opaque type: borrow through Arc to get &CoreType
176                    if p.optional {
177                        format!("{}.as_ref().map(|v| &v.inner)", p.name)
178                    } else if promoted {
179                        format!("{}{}.inner.as_ref()", p.name, unwrap_suffix)
180                    } else {
181                        format!("&{}.inner", p.name)
182                    }
183                }
184                TypeRef::Named(_) => {
185                    if p.optional {
186                        format!("{}.map(Into::into)", p.name)
187                    } else if promoted {
188                        format!("{}{}.into()", p.name, unwrap_suffix)
189                    } else {
190                        format!("{}.into()", p.name)
191                    }
192                }
193                // String → &str for core function calls when is_ref=true,
194                // or pass owned when is_ref=false (core takes String/impl Into<String>).
195                // For optional params: as_deref() when is_ref=true, pass owned when is_ref=false.
196                TypeRef::String | TypeRef::Char => {
197                    if p.optional {
198                        if p.is_ref {
199                            format!("{}.as_deref()", p.name)
200                        } else {
201                            p.name.clone()
202                        }
203                    } else if promoted {
204                        if p.is_ref {
205                            format!("&{}{}", p.name, unwrap_suffix)
206                        } else {
207                            format!("{}{}", p.name, unwrap_suffix)
208                        }
209                    } else if p.is_ref {
210                        format!("&{}", p.name)
211                    } else {
212                        p.name.clone()
213                    }
214                }
215                // Path → PathBuf/&Path for core function calls
216                TypeRef::Path => {
217                    if p.optional && p.is_ref {
218                        format!("{}.as_deref().map(std::path::Path::new)", p.name)
219                    } else if p.optional {
220                        format!("{}.map(std::path::PathBuf::from)", p.name)
221                    } else if promoted {
222                        format!("std::path::PathBuf::from({}{})", p.name, unwrap_suffix)
223                    } else if p.is_ref {
224                        format!("std::path::Path::new(&{})", p.name)
225                    } else {
226                        format!("std::path::PathBuf::from({})", p.name)
227                    }
228                }
229                TypeRef::Bytes => {
230                    if p.optional {
231                        if p.is_ref {
232                            format!("{}.as_deref()", p.name)
233                        } else {
234                            p.name.clone()
235                        }
236                    } else if promoted {
237                        format!("&{}{}", p.name, unwrap_suffix)
238                    } else {
239                        format!("&{}", p.name)
240                    }
241                }
242                // Duration: binding uses u64 (millis), core uses std::time::Duration
243                TypeRef::Duration => {
244                    if p.optional {
245                        format!("{}.map(std::time::Duration::from_millis)", p.name)
246                    } else if promoted {
247                        format!("std::time::Duration::from_millis({}{})", p.name, unwrap_suffix)
248                    } else {
249                        format!("std::time::Duration::from_millis({})", p.name)
250                    }
251                }
252                _ => {
253                    if promoted {
254                        format!("{}{}", p.name, unwrap_suffix)
255                    } else {
256                        p.name.clone()
257                    }
258                }
259            }
260        })
261        .collect::<Vec<_>>()
262        .join(", ")
263}
264
265/// Build call argument expressions using pre-bound let bindings for non-opaque Named params.
266/// Non-opaque Named params use `&{name}_core` references instead of `.into()`.
267pub fn gen_call_args_with_let_bindings(params: &[ParamDef], opaque_types: &AHashSet<String>) -> String {
268    params
269        .iter()
270        .enumerate()
271        .map(|(idx, p)| {
272            let promoted = crate::shared::is_promoted_optional(params, idx);
273            let unwrap_suffix = if promoted {
274                format!(".expect(\"'{}' is required\")", p.name)
275            } else {
276                String::new()
277            };
278            // If this param's type was resolved from a newtype, re-wrap when calling core.
279            if let Some(newtype_path) = &p.newtype_wrapper {
280                return if p.optional {
281                    format!("{}.map({newtype_path})", p.name)
282                } else if promoted {
283                    format!("{newtype_path}({}{})", p.name, unwrap_suffix)
284                } else {
285                    format!("{newtype_path}({})", p.name)
286                };
287            }
288            match &p.ty {
289                TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
290                    if p.optional {
291                        format!("{}.as_ref().map(|v| &v.inner)", p.name)
292                    } else if promoted {
293                        format!("{}{}.inner.as_ref()", p.name, unwrap_suffix)
294                    } else {
295                        format!("&{}.inner", p.name)
296                    }
297                }
298                TypeRef::Named(_) if p.is_ref => {
299                    format!("&{}_core", p.name)
300                }
301                TypeRef::Named(_) => {
302                    format!("{}_core", p.name)
303                }
304                TypeRef::String | TypeRef::Char => {
305                    if p.optional {
306                        if p.is_ref {
307                            format!("{}.as_deref()", p.name)
308                        } else {
309                            p.name.clone()
310                        }
311                    } else if promoted {
312                        if p.is_ref {
313                            format!("&{}{}", p.name, unwrap_suffix)
314                        } else {
315                            format!("{}{}", p.name, unwrap_suffix)
316                        }
317                    } else if p.is_ref {
318                        format!("&{}", p.name)
319                    } else {
320                        p.name.clone()
321                    }
322                }
323                TypeRef::Path => {
324                    if promoted {
325                        format!("std::path::PathBuf::from({}{})", p.name, unwrap_suffix)
326                    } else if p.optional && p.is_ref {
327                        format!("{}.as_deref().map(std::path::Path::new)", p.name)
328                    } else if p.optional {
329                        format!("{}.map(std::path::PathBuf::from)", p.name)
330                    } else if p.is_ref {
331                        format!("std::path::Path::new(&{})", p.name)
332                    } else {
333                        format!("std::path::PathBuf::from({})", p.name)
334                    }
335                }
336                TypeRef::Bytes => {
337                    if p.optional {
338                        if p.is_ref {
339                            format!("{}.as_deref()", p.name)
340                        } else {
341                            p.name.clone()
342                        }
343                    } else if promoted {
344                        format!("&{}{}", p.name, unwrap_suffix)
345                    } else {
346                        format!("&{}", p.name)
347                    }
348                }
349                TypeRef::Duration => {
350                    if p.optional {
351                        format!("{}.map(std::time::Duration::from_millis)", p.name)
352                    } else if promoted {
353                        format!("std::time::Duration::from_millis({}{})", p.name, unwrap_suffix)
354                    } else {
355                        format!("std::time::Duration::from_millis({})", p.name)
356                    }
357                }
358                _ => {
359                    if promoted {
360                        format!("{}{}", p.name, unwrap_suffix)
361                    } else {
362                        p.name.clone()
363                    }
364                }
365            }
366        })
367        .collect::<Vec<_>>()
368        .join(", ")
369}
370
371/// Generate let bindings for non-opaque Named params, converting them to core types.
372pub fn gen_named_let_bindings_pub(params: &[ParamDef], opaque_types: &AHashSet<String>) -> String {
373    gen_named_let_bindings(params, opaque_types)
374}
375
376pub(super) fn gen_named_let_bindings(params: &[ParamDef], opaque_types: &AHashSet<String>) -> String {
377    let mut bindings = String::new();
378    for (idx, p) in params.iter().enumerate() {
379        if let TypeRef::Named(name) = &p.ty {
380            if !opaque_types.contains(name.as_str()) {
381                let promoted = crate::shared::is_promoted_optional(params, idx);
382                if p.optional {
383                    write!(bindings, "let {}_core = {}.map(Into::into);\n    ", p.name, p.name).ok();
384                } else if promoted {
385                    // Promoted-optional: unwrap then convert
386                    write!(
387                        bindings,
388                        "let {}_core = {}.expect(\"'{}' is required\").into();\n    ",
389                        p.name, p.name, p.name
390                    )
391                    .ok();
392                } else {
393                    write!(bindings, "let {}_core = {}.into();\n    ", p.name, p.name).ok();
394                }
395            }
396        }
397    }
398    bindings
399}
400
401/// Generate serde-based let bindings for non-opaque Named params.
402/// Serializes binding types to JSON and deserializes to core types.
403/// Used when From impls don't exist (e.g., types with sanitized fields).
404/// `indent` is the whitespace prefix for each generated line (e.g., "    " for functions, "        " for methods).
405pub fn gen_serde_let_bindings(
406    params: &[ParamDef],
407    opaque_types: &AHashSet<String>,
408    core_import: &str,
409    err_conv: &str,
410    indent: &str,
411) -> String {
412    let mut bindings = String::new();
413    for p in params {
414        if let TypeRef::Named(name) = &p.ty {
415            if !opaque_types.contains(name.as_str()) {
416                let core_path = format!("{}::{}", core_import, name);
417                if p.optional {
418                    write!(
419                        bindings,
420                        "let {name}_core: Option<{core_path}> = {name}.map(|v| {{\n\
421                         {indent}    let json = serde_json::to_string(&v){err_conv}?;\n\
422                         {indent}    serde_json::from_str(&json){err_conv}\n\
423                         {indent}}}).transpose()?;\n{indent}",
424                        name = p.name,
425                        core_path = core_path,
426                        err_conv = err_conv,
427                        indent = indent,
428                    )
429                    .ok();
430                } else {
431                    write!(
432                        bindings,
433                        "let {name}_json = serde_json::to_string(&{name}){err_conv}?;\n\
434                         {indent}let {name}_core: {core_path} = serde_json::from_str(&{name}_json){err_conv}?;\n{indent}",
435                        name = p.name,
436                        core_path = core_path,
437                        err_conv = err_conv,
438                        indent = indent,
439                    )
440                    .ok();
441                }
442            }
443        }
444    }
445    bindings
446}
447
448/// Check if params contain any non-opaque Named types that need let bindings.
449pub fn has_named_params(params: &[ParamDef], opaque_types: &AHashSet<String>) -> bool {
450    params
451        .iter()
452        .any(|p| matches!(&p.ty, TypeRef::Named(name) if !opaque_types.contains(name.as_str())))
453}
454
455/// Check if a param type is safe for non-opaque delegation (no complex conversions needed).
456/// Vec and Map params can cause type mismatches (e.g. Vec<String> vs &[&str]).
457pub fn is_simple_non_opaque_param(ty: &TypeRef) -> bool {
458    match ty {
459        TypeRef::Primitive(_)
460        | TypeRef::String
461        | TypeRef::Char
462        | TypeRef::Bytes
463        | TypeRef::Path
464        | TypeRef::Unit
465        | TypeRef::Duration => true,
466        TypeRef::Optional(inner) => is_simple_non_opaque_param(inner),
467        _ => false,
468    }
469}
470
471/// Generate a lossy binding→core struct literal for non-opaque delegation.
472/// Sanitized fields use `Default::default()`, non-sanitized fields are cloned and converted.
473/// Fields are accessed via `self.` (behind &self), so all non-Copy types need `.clone()`.
474pub fn gen_lossy_binding_to_core_fields(typ: &TypeDef, core_import: &str) -> String {
475    gen_lossy_binding_to_core_fields_inner(typ, core_import, false)
476}
477
478/// Same as `gen_lossy_binding_to_core_fields` but declares `core_self` as mutable.
479pub fn gen_lossy_binding_to_core_fields_mut(typ: &TypeDef, core_import: &str) -> String {
480    gen_lossy_binding_to_core_fields_inner(typ, core_import, true)
481}
482
483fn gen_lossy_binding_to_core_fields_inner(typ: &TypeDef, core_import: &str, needs_mut: bool) -> String {
484    let core_path = crate::conversions::core_type_path(typ, core_import);
485    let mut_kw = if needs_mut { "mut " } else { "" };
486    let mut out = format!("let {mut_kw}core_self = {core_path} {{\n");
487    for field in &typ.fields {
488        let name = &field.name;
489        if field.sanitized {
490            writeln!(out, "            {name}: Default::default(),").ok();
491        } else {
492            let expr = match &field.ty {
493                TypeRef::Primitive(_) => format!("self.{name}"),
494                TypeRef::Duration => {
495                    if field.optional {
496                        format!("self.{name}.map(std::time::Duration::from_secs)")
497                    } else {
498                        format!("std::time::Duration::from_millis(self.{name})")
499                    }
500                }
501                TypeRef::String | TypeRef::Char | TypeRef::Bytes => format!("self.{name}.clone()"),
502                TypeRef::Path => {
503                    if field.optional {
504                        format!("self.{name}.clone().map(Into::into)")
505                    } else {
506                        format!("self.{name}.clone().into()")
507                    }
508                }
509                TypeRef::Named(_) => {
510                    if field.optional {
511                        format!("self.{name}.clone().map(Into::into)")
512                    } else {
513                        format!("self.{name}.clone().into()")
514                    }
515                }
516                TypeRef::Vec(inner) => match inner.as_ref() {
517                    TypeRef::Named(_) => {
518                        if field.optional {
519                            // Option<Vec<Named(T)>>: map over the Option, then convert each element
520                            format!("self.{name}.clone().map(|v| v.into_iter().map(Into::into).collect())")
521                        } else {
522                            format!("self.{name}.clone().into_iter().map(Into::into).collect()")
523                        }
524                    }
525                    _ => format!("self.{name}.clone()"),
526                },
527                TypeRef::Optional(inner) => match inner.as_ref() {
528                    TypeRef::Named(_) => {
529                        format!("self.{name}.clone().map(Into::into)")
530                    }
531                    TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Named(_)) => {
532                        format!("self.{name}.clone().map(|v| v.into_iter().map(Into::into).collect())")
533                    }
534                    _ => format!("self.{name}.clone()"),
535                },
536                TypeRef::Map(_, v) => match v.as_ref() {
537                    TypeRef::Json => {
538                        // HashMap<String, String> (binding) → HashMap<String, Value> (core)
539                        if field.optional {
540                            format!(
541                                "self.{name}.clone().map(|m| m.into_iter().map(|(k, v)| \
542                                 (k, serde_json::from_str(&v).unwrap_or(serde_json::Value::String(v)))).collect())"
543                            )
544                        } else {
545                            format!(
546                                "self.{name}.clone().into_iter().map(|(k, v)| \
547                                 (k, serde_json::from_str(&v).unwrap_or(serde_json::Value::String(v)))).collect()"
548                            )
549                        }
550                    }
551                    // Collect to handle HashMap↔BTreeMap conversion
552                    _ => {
553                        if field.optional {
554                            format!("self.{name}.clone().map(|m| m.into_iter().collect())")
555                        } else {
556                            format!("self.{name}.clone().into_iter().collect()")
557                        }
558                    }
559                },
560                TypeRef::Unit => format!("self.{name}.clone()"),
561                TypeRef::Json => {
562                    // String (binding) → serde_json::Value (core)
563                    if field.optional {
564                        format!("self.{name}.as_ref().and_then(|s| serde_json::from_str(s).ok())")
565                    } else {
566                        format!("serde_json::from_str(&self.{name}).unwrap_or_default()")
567                    }
568                }
569            };
570            // Newtype wrapping: when the field was resolved from a newtype (e.g. NodeIndex → u32),
571            // re-wrap the binding value into the newtype for the core struct literal.
572            // When `optional=true` and `ty` is a plain Primitive (not TypeRef::Optional), the core
573            // field is actually `Option<NewtypeT>`, so we must use `.map(NewtypeT)` not `NewtypeT(...)`.
574            let expr = if let Some(newtype_path) = &field.newtype_wrapper {
575                match &field.ty {
576                    TypeRef::Optional(_) => format!("({expr}).map({newtype_path})"),
577                    TypeRef::Vec(_) => format!("({expr}).into_iter().map({newtype_path}).collect()"),
578                    _ if field.optional => format!("({expr}).map({newtype_path})"),
579                    _ => format!("{newtype_path}({expr})"),
580                }
581            } else {
582                expr
583            };
584            writeln!(out, "            {name}: {expr},").ok();
585        }
586    }
587    // Use ..Default::default() to fill cfg-gated fields stripped from the IR
588    if typ.has_stripped_cfg_fields {
589        out.push_str("            ..Default::default()\n");
590    }
591    out.push_str("        };\n        ");
592    out
593}
594
595/// Generate the body for an async call, unified across methods, static methods, and free functions.
596///
597/// - `core_call`: the expression to await, e.g. `inner.method(args)` or `CoreType::fn(args)`.
598///   For Pyo3FutureIntoPy opaque methods this should reference `inner` (the Arc clone);
599///   for all other patterns it may reference `self.inner` or a static call expression.
600/// - `cfg`: binding configuration (determines which async pattern to emit)
601/// - `has_error`: whether the core call returns a `Result`
602/// - `return_wrap`: expression to produce the binding return value from `result`,
603///   e.g. `"result"` or `"TypeName::from(result)"`
604///
605/// - `is_opaque`: whether the binding type is Arc-wrapped (affects TokioBlockOn wrapping)
606/// - `inner_clone_line`: optional statement emitted before the pattern-specific body,
607///   e.g. `"let inner = self.inner.clone();\n        "` for opaque instance methods, or `""`.
608///   Required when `core_call` references `inner` (Pyo3FutureIntoPy opaque case).
609pub fn gen_async_body(
610    core_call: &str,
611    cfg: &RustBindingConfig,
612    has_error: bool,
613    return_wrap: &str,
614    is_opaque: bool,
615    inner_clone_line: &str,
616    is_unit_return: bool,
617) -> String {
618    let pattern_body = match cfg.async_pattern {
619        AsyncPattern::Pyo3FutureIntoPy => {
620            let result_handling = if has_error {
621                format!(
622                    "let result = {core_call}.await\n            \
623                     .map_err(|e| PyErr::new::<PyRuntimeError, _>(e.to_string()))?;"
624                )
625            } else if is_unit_return {
626                format!("{core_call}.await;")
627            } else {
628                format!("let result = {core_call}.await;")
629            };
630            let ok_expr = if is_unit_return && !has_error {
631                "()"
632            } else {
633                return_wrap
634            };
635            format!(
636                "pyo3_async_runtimes::tokio::future_into_py(py, async move {{\n            \
637                 {result_handling}\n            \
638                 Ok({ok_expr})\n        }})"
639            )
640        }
641        AsyncPattern::WasmNativeAsync => {
642            let result_handling = if has_error {
643                format!(
644                    "let result = {core_call}.await\n        \
645                     .map_err(|e| JsValue::from_str(&e.to_string()))?;"
646                )
647            } else if is_unit_return {
648                format!("{core_call}.await;")
649            } else {
650                format!("let result = {core_call}.await;")
651            };
652            let ok_expr = if is_unit_return && !has_error {
653                "()"
654            } else {
655                return_wrap
656            };
657            format!(
658                "{result_handling}\n        \
659                 Ok({ok_expr})"
660            )
661        }
662        AsyncPattern::NapiNativeAsync => {
663            let result_handling = if has_error {
664                format!(
665                    "let result = {core_call}.await\n            \
666                     .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;"
667                )
668            } else if is_unit_return {
669                format!("{core_call}.await;")
670            } else {
671                format!("let result = {core_call}.await;")
672            };
673            if !has_error && !is_unit_return {
674                // No error type: return value directly without Ok() wrapper
675                format!(
676                    "{result_handling}\n            \
677                     {return_wrap}"
678                )
679            } else {
680                let ok_expr = if is_unit_return && !has_error {
681                    "()"
682                } else {
683                    return_wrap
684                };
685                format!(
686                    "{result_handling}\n            \
687                     Ok({ok_expr})"
688                )
689            }
690        }
691        AsyncPattern::TokioBlockOn => {
692            if has_error {
693                if is_opaque {
694                    format!(
695                        "let rt = tokio::runtime::Runtime::new()?;\n        \
696                         let result = rt.block_on(async {{ {core_call}.await.map_err(|e| e.into()) }})?;\n        \
697                         {return_wrap}"
698                    )
699                } else {
700                    format!(
701                        "let rt = tokio::runtime::Runtime::new()?;\n        \
702                         rt.block_on(async {{ {core_call}.await.map_err(|e| e.into()) }})"
703                    )
704                }
705            } else if is_opaque {
706                if is_unit_return {
707                    format!(
708                        "let rt = tokio::runtime::Runtime::new()?;\n        \
709                         rt.block_on(async {{ {core_call}.await }});"
710                    )
711                } else {
712                    format!(
713                        "let rt = tokio::runtime::Runtime::new()?;\n        \
714                         let result = rt.block_on(async {{ {core_call}.await }});\n        \
715                         {return_wrap}"
716                    )
717                }
718            } else {
719                format!(
720                    "let rt = tokio::runtime::Runtime::new()?;\n        \
721                     rt.block_on(async {{ {core_call}.await }})"
722                )
723            }
724        }
725        AsyncPattern::None => "todo!(\"async not supported by backend\")".to_string(),
726    };
727    if inner_clone_line.is_empty() {
728        pattern_body
729    } else {
730        format!("{inner_clone_line}{pattern_body}")
731    }
732}
733
734/// Generate a compilable body for functions that can't be auto-delegated.
735/// Returns a default value or error instead of `todo!()` which would panic.
736pub fn gen_unimplemented_body(
737    return_type: &TypeRef,
738    fn_name: &str,
739    has_error: bool,
740    cfg: &RustBindingConfig,
741    params: &[ParamDef],
742) -> String {
743    // Suppress unused_variables by binding all params to `_`
744    let suppress = if params.is_empty() {
745        String::new()
746    } else {
747        let names: Vec<&str> = params.iter().map(|p| p.name.as_str()).collect();
748        if names.len() == 1 {
749            format!("let _ = {};\n        ", names[0])
750        } else {
751            format!("let _ = ({});\n        ", names.join(", "))
752        }
753    };
754    let err_msg = format!("Not implemented: {fn_name}");
755    let body = if has_error {
756        // Backend-specific error return
757        match cfg.async_pattern {
758            AsyncPattern::Pyo3FutureIntoPy => {
759                format!("Err(pyo3::exceptions::PyNotImplementedError::new_err(\"{err_msg}\"))")
760            }
761            AsyncPattern::NapiNativeAsync => {
762                format!("Err(napi::Error::new(napi::Status::GenericFailure, \"{err_msg}\"))")
763            }
764            AsyncPattern::WasmNativeAsync => {
765                format!("Err(JsValue::from_str(\"{err_msg}\"))")
766            }
767            _ => format!("Err(\"{err_msg}\".to_string())"),
768        }
769    } else {
770        // Return type-appropriate default
771        match return_type {
772            TypeRef::Unit => "()".to_string(),
773            TypeRef::String | TypeRef::Char | TypeRef::Path => format!("String::from(\"[unimplemented: {fn_name}]\")"),
774            TypeRef::Bytes => "Vec::new()".to_string(),
775            TypeRef::Primitive(p) => match p {
776                alef_core::ir::PrimitiveType::Bool => "false".to_string(),
777                _ => "0".to_string(),
778            },
779            TypeRef::Optional(_) => "None".to_string(),
780            TypeRef::Vec(_) => "Vec::new()".to_string(),
781            TypeRef::Map(_, _) => "Default::default()".to_string(),
782            TypeRef::Duration => "0".to_string(),
783            TypeRef::Named(_) | TypeRef::Json => {
784                // Named return without error type: can't return Err.
785                // Emit compile_error so this is caught at build time rather than panicking at runtime.
786                format!(
787                    "compile_error!(\"alef: {fn_name} returns a Named/Json type but has no error variant — cannot auto-delegate\")"
788                )
789            }
790        }
791    };
792    format!("{suppress}{body}")
793}