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