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
13pub fn wrap_return(
14    expr: &str,
15    return_type: &TypeRef,
16    type_name: &str,
17    opaque_types: &AHashSet<String>,
18    self_is_opaque: bool,
19    returns_ref: bool,
20) -> String {
21    match return_type {
22        TypeRef::Named(n) if n == type_name && self_is_opaque => {
23            if returns_ref {
24                format!("Self {{ inner: Arc::new({expr}.clone()) }}")
25            } else {
26                format!("Self {{ inner: Arc::new({expr}) }}")
27            }
28        }
29        TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
30            if returns_ref {
31                format!("{n} {{ inner: Arc::new({expr}.clone()) }}")
32            } else {
33                format!("{n} {{ inner: Arc::new({expr}) }}")
34            }
35        }
36        TypeRef::Named(_) => {
37            // Non-opaque Named return type — use .into() for core→binding From conversion.
38            // When the core returns a reference, clone first since From<&T> typically doesn't exist.
39            if returns_ref {
40                format!("{expr}.clone().into()")
41            } else {
42                format!("{expr}.into()")
43            }
44        }
45        // String/Bytes: only convert when the core returns a reference (&str→String, &[u8]→Vec<u8>).
46        // When owned (returns_ref=false), both sides are already String/Vec<u8> — skip .into().
47        TypeRef::String | TypeRef::Bytes => {
48            if returns_ref {
49                format!("{expr}.into()")
50            } else {
51                expr.to_string()
52            }
53        }
54        // Path: PathBuf→String needs to_string_lossy, &Path→String too
55        TypeRef::Path => format!("{expr}.to_string_lossy().to_string()"),
56        // Duration: core returns std::time::Duration, binding uses u64 (secs)
57        TypeRef::Duration => format!("{expr}.as_secs()"),
58        // Json: serde_json::Value needs serialization to string
59        TypeRef::Json => format!("{expr}.to_string()"),
60        // Optional: wrap inner conversion in .map(...)
61        TypeRef::Optional(inner) => match inner.as_ref() {
62            TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
63                if returns_ref {
64                    format!("{expr}.map(|v| {n} {{ inner: Arc::new(v.clone()) }})")
65                } else {
66                    format!("{expr}.map(|v| {n} {{ inner: Arc::new(v) }})")
67                }
68            }
69            TypeRef::Named(_) => {
70                if returns_ref {
71                    format!("{expr}.map(|v| v.clone().into())")
72                } else {
73                    format!("{expr}.map(Into::into)")
74                }
75            }
76            TypeRef::Path => {
77                format!("{expr}.map(Into::into)")
78            }
79            TypeRef::String | TypeRef::Bytes => {
80                if returns_ref {
81                    format!("{expr}.map(Into::into)")
82                } else {
83                    expr.to_string()
84                }
85            }
86            TypeRef::Duration => format!("{expr}.map(|d| d.as_secs())"),
87            TypeRef::Json => format!("{expr}.map(ToString::to_string)"),
88            _ => expr.to_string(),
89        },
90        // Vec: map each element through the appropriate conversion
91        TypeRef::Vec(inner) => match inner.as_ref() {
92            TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
93                if returns_ref {
94                    format!("{expr}.into_iter().map(|v| {n} {{ inner: Arc::new(v.clone()) }}).collect()")
95                } else {
96                    format!("{expr}.into_iter().map(|v| {n} {{ inner: Arc::new(v) }}).collect()")
97                }
98            }
99            TypeRef::Named(_) => {
100                if returns_ref {
101                    format!("{expr}.into_iter().map(|v| v.clone().into()).collect()")
102                } else {
103                    format!("{expr}.into_iter().map(Into::into).collect()")
104                }
105            }
106            TypeRef::Path => {
107                format!("{expr}.into_iter().map(Into::into).collect()")
108            }
109            TypeRef::String | TypeRef::Bytes => {
110                if returns_ref {
111                    format!("{expr}.into_iter().map(Into::into).collect()")
112                } else {
113                    expr.to_string()
114                }
115            }
116            _ => expr.to_string(),
117        },
118        _ => expr.to_string(),
119    }
120}
121
122/// Build call argument expressions from parameters.
123/// - Opaque Named types: unwrap Arc wrapper via `(*param.inner).clone()`
124/// - Non-opaque Named types: `.into()` for From conversion
125/// - String/Path/Bytes: `&param` since core functions typically take `&str`/`&Path`/`&[u8]`
126pub fn gen_call_args(params: &[ParamDef], opaque_types: &AHashSet<String>) -> String {
127    params
128        .iter()
129        .enumerate()
130        .map(|(idx, p)| {
131            let promoted = crate::shared::is_promoted_optional(params, idx);
132            // If a required param was promoted to optional, unwrap it before use
133            let unwrap_suffix = if promoted {
134                format!(".expect(\"'{}' is required\")", p.name)
135            } else {
136                String::new()
137            };
138            match &p.ty {
139                TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
140                    // Opaque type: borrow through Arc to get &CoreType
141                    if p.optional {
142                        format!("{}.as_ref().map(|v| &v.inner)", p.name)
143                    } else if promoted {
144                        format!("{}{}.inner.as_ref()", p.name, unwrap_suffix)
145                    } else {
146                        format!("&{}.inner", p.name)
147                    }
148                }
149                TypeRef::Named(_) => {
150                    if p.optional {
151                        format!("{}.map(Into::into)", p.name)
152                    } else if promoted {
153                        format!("{}{}.into()", p.name, unwrap_suffix)
154                    } else {
155                        format!("{}.into()", p.name)
156                    }
157                }
158                // String → &str for core function calls
159                TypeRef::String | TypeRef::Char => {
160                    if promoted {
161                        format!("&{}{}", p.name, unwrap_suffix)
162                    } else {
163                        format!("&{}", p.name)
164                    }
165                }
166                // Path → PathBuf for core function calls (core expects PathBuf, binding has String)
167                TypeRef::Path => {
168                    if promoted {
169                        format!("std::path::PathBuf::from({}{})", p.name, unwrap_suffix)
170                    } else {
171                        format!("std::path::PathBuf::from({})", p.name)
172                    }
173                }
174                TypeRef::Bytes => {
175                    if promoted {
176                        format!("&{}{}", p.name, unwrap_suffix)
177                    } else {
178                        format!("&{}", p.name)
179                    }
180                }
181                // Duration: binding uses u64 (secs), core uses std::time::Duration
182                TypeRef::Duration => {
183                    if promoted {
184                        format!("std::time::Duration::from_secs({}{})", p.name, unwrap_suffix)
185                    } else {
186                        format!("std::time::Duration::from_secs({})", p.name)
187                    }
188                }
189                _ => {
190                    if promoted {
191                        format!("{}{}", p.name, unwrap_suffix)
192                    } else {
193                        p.name.clone()
194                    }
195                }
196            }
197        })
198        .collect::<Vec<_>>()
199        .join(", ")
200}
201
202/// Build call argument expressions using pre-bound let bindings for non-opaque Named params.
203/// Non-opaque Named params use `&{name}_core` references instead of `.into()`.
204pub fn gen_call_args_with_let_bindings(params: &[ParamDef], opaque_types: &AHashSet<String>) -> String {
205    params
206        .iter()
207        .enumerate()
208        .map(|(idx, p)| {
209            let promoted = crate::shared::is_promoted_optional(params, idx);
210            let unwrap_suffix = if promoted {
211                format!(".expect(\"'{}' is required\")", p.name)
212            } else {
213                String::new()
214            };
215            match &p.ty {
216                TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
217                    if p.optional {
218                        format!("{}.as_ref().map(|v| &v.inner)", p.name)
219                    } else if promoted {
220                        format!("{}{}.inner.as_ref()", p.name, unwrap_suffix)
221                    } else {
222                        format!("&{}.inner", p.name)
223                    }
224                }
225                TypeRef::Named(_) => {
226                    format!("{}_core", p.name)
227                }
228                TypeRef::String | TypeRef::Char => {
229                    if promoted {
230                        format!("&{}{}", p.name, unwrap_suffix)
231                    } else {
232                        format!("&{}", p.name)
233                    }
234                }
235                TypeRef::Path => {
236                    if promoted {
237                        format!("std::path::PathBuf::from({}{})", p.name, unwrap_suffix)
238                    } else {
239                        format!("std::path::PathBuf::from({})", p.name)
240                    }
241                }
242                TypeRef::Bytes => {
243                    if promoted {
244                        format!("&{}{}", p.name, unwrap_suffix)
245                    } else {
246                        format!("&{}", p.name)
247                    }
248                }
249                TypeRef::Duration => {
250                    if promoted {
251                        format!("std::time::Duration::from_secs({}{})", p.name, unwrap_suffix)
252                    } else {
253                        format!("std::time::Duration::from_secs({})", p.name)
254                    }
255                }
256                _ => {
257                    if promoted {
258                        format!("{}{}", p.name, unwrap_suffix)
259                    } else {
260                        p.name.clone()
261                    }
262                }
263            }
264        })
265        .collect::<Vec<_>>()
266        .join(", ")
267}
268
269/// Generate let bindings for non-opaque Named params, converting them to core types.
270pub fn gen_named_let_bindings_pub(params: &[ParamDef], opaque_types: &AHashSet<String>) -> String {
271    gen_named_let_bindings(params, opaque_types)
272}
273
274pub(super) fn gen_named_let_bindings(params: &[ParamDef], opaque_types: &AHashSet<String>) -> String {
275    let mut bindings = String::new();
276    for (idx, p) in params.iter().enumerate() {
277        if let TypeRef::Named(name) = &p.ty {
278            if !opaque_types.contains(name.as_str()) {
279                let promoted = crate::shared::is_promoted_optional(params, idx);
280                if p.optional {
281                    write!(bindings, "let {}_core = {}.map(Into::into);\n    ", p.name, p.name).ok();
282                } else if promoted {
283                    // Promoted-optional: unwrap then convert
284                    write!(
285                        bindings,
286                        "let {}_core = {}.expect(\"'{}' is required\").into();\n    ",
287                        p.name, p.name, p.name
288                    )
289                    .ok();
290                } else {
291                    write!(bindings, "let {}_core = {}.into();\n    ", p.name, p.name).ok();
292                }
293            }
294        }
295    }
296    bindings
297}
298
299/// Generate serde-based let bindings for non-opaque Named params.
300/// Serializes binding types to JSON and deserializes to core types.
301/// Used when From impls don't exist (e.g., types with sanitized fields).
302/// `indent` is the whitespace prefix for each generated line (e.g., "    " for functions, "        " for methods).
303pub fn gen_serde_let_bindings(
304    params: &[ParamDef],
305    opaque_types: &AHashSet<String>,
306    core_import: &str,
307    err_conv: &str,
308    indent: &str,
309) -> String {
310    let mut bindings = String::new();
311    for p in params {
312        if let TypeRef::Named(name) = &p.ty {
313            if !opaque_types.contains(name.as_str()) {
314                let core_path = format!("{}::{}", core_import, name);
315                if p.optional {
316                    write!(
317                        bindings,
318                        "let {name}_core: Option<{core_path}> = {name}.map(|v| {{\n\
319                         {indent}    let json = serde_json::to_string(&v){err_conv}?;\n\
320                         {indent}    serde_json::from_str(&json){err_conv}\n\
321                         {indent}}}).transpose()?;\n{indent}",
322                        name = p.name,
323                        core_path = core_path,
324                        err_conv = err_conv,
325                        indent = indent,
326                    )
327                    .ok();
328                } else {
329                    write!(
330                        bindings,
331                        "let {name}_json = serde_json::to_string(&{name}){err_conv}?;\n\
332                         {indent}let {name}_core: {core_path} = serde_json::from_str(&{name}_json){err_conv}?;\n{indent}",
333                        name = p.name,
334                        core_path = core_path,
335                        err_conv = err_conv,
336                        indent = indent,
337                    )
338                    .ok();
339                }
340            }
341        }
342    }
343    bindings
344}
345
346/// Check if params contain any non-opaque Named types that need let bindings.
347pub fn has_named_params(params: &[ParamDef], opaque_types: &AHashSet<String>) -> bool {
348    params
349        .iter()
350        .any(|p| matches!(&p.ty, TypeRef::Named(name) if !opaque_types.contains(name.as_str())))
351}
352
353/// Check if a param type is safe for non-opaque delegation (no complex conversions needed).
354/// Vec and Map params can cause type mismatches (e.g. Vec<String> vs &[&str]).
355pub fn is_simple_non_opaque_param(ty: &TypeRef) -> bool {
356    match ty {
357        TypeRef::Primitive(_)
358        | TypeRef::String
359        | TypeRef::Char
360        | TypeRef::Bytes
361        | TypeRef::Path
362        | TypeRef::Unit
363        | TypeRef::Duration => true,
364        TypeRef::Optional(inner) => is_simple_non_opaque_param(inner),
365        _ => false,
366    }
367}
368
369/// Generate a lossy binding→core struct literal for non-opaque delegation.
370/// Sanitized fields use `Default::default()`, non-sanitized fields are cloned and converted.
371/// Fields are accessed via `self.` (behind &self), so all non-Copy types need `.clone()`.
372pub fn gen_lossy_binding_to_core_fields(typ: &TypeDef, core_import: &str) -> String {
373    let core_path = crate::conversions::core_type_path(typ, core_import);
374    let mut out = format!("let core_self = {core_path} {{\n");
375    for field in &typ.fields {
376        let name = &field.name;
377        if field.sanitized {
378            writeln!(out, "            {name}: Default::default(),").ok();
379        } else {
380            let expr = match &field.ty {
381                TypeRef::Primitive(_) => format!("self.{name}"),
382                TypeRef::Duration => {
383                    if field.optional {
384                        format!("self.{name}.map(std::time::Duration::from_secs)")
385                    } else {
386                        format!("std::time::Duration::from_secs(self.{name})")
387                    }
388                }
389                TypeRef::String | TypeRef::Char | TypeRef::Bytes => format!("self.{name}.clone()"),
390                TypeRef::Path => {
391                    if field.optional {
392                        format!("self.{name}.clone().map(Into::into)")
393                    } else {
394                        format!("self.{name}.clone().into()")
395                    }
396                }
397                TypeRef::Named(_) => {
398                    if field.optional {
399                        format!("self.{name}.clone().map(Into::into)")
400                    } else {
401                        format!("self.{name}.clone().into()")
402                    }
403                }
404                TypeRef::Vec(inner) => match inner.as_ref() {
405                    TypeRef::Named(_) => {
406                        format!("self.{name}.clone().into_iter().map(Into::into).collect()")
407                    }
408                    _ => format!("self.{name}.clone()"),
409                },
410                TypeRef::Optional(inner) => match inner.as_ref() {
411                    TypeRef::Named(_) => {
412                        format!("self.{name}.clone().map(Into::into)")
413                    }
414                    TypeRef::Vec(vi) if matches!(vi.as_ref(), TypeRef::Named(_)) => {
415                        format!("self.{name}.clone().map(|v| v.into_iter().map(Into::into).collect())")
416                    }
417                    _ => format!("self.{name}.clone()"),
418                },
419                TypeRef::Map(_, _) => format!("self.{name}.clone()"),
420                TypeRef::Unit | TypeRef::Json => format!("self.{name}.clone()"),
421            };
422            writeln!(out, "            {name}: {expr},").ok();
423        }
424    }
425    // Use ..Default::default() to fill cfg-gated fields stripped from the IR
426    if typ.has_stripped_cfg_fields {
427        out.push_str("            ..Default::default()\n");
428    }
429    out.push_str("        };\n        ");
430    out
431}
432
433/// Generate the body for an async call, unified across methods, static methods, and free functions.
434///
435/// - `core_call`: the expression to await, e.g. `inner.method(args)` or `CoreType::fn(args)`.
436///   For Pyo3FutureIntoPy opaque methods this should reference `inner` (the Arc clone);
437///   for all other patterns it may reference `self.inner` or a static call expression.
438/// - `cfg`: binding configuration (determines which async pattern to emit)
439/// - `has_error`: whether the core call returns a `Result`
440/// - `return_wrap`: expression to produce the binding return value from `result`,
441///   e.g. `"result"` or `"TypeName::from(result)"`
442///
443/// - `is_opaque`: whether the binding type is Arc-wrapped (affects TokioBlockOn wrapping)
444/// - `inner_clone_line`: optional statement emitted before the pattern-specific body,
445///   e.g. `"let inner = self.inner.clone();\n        "` for opaque instance methods, or `""`.
446///   Required when `core_call` references `inner` (Pyo3FutureIntoPy opaque case).
447pub fn gen_async_body(
448    core_call: &str,
449    cfg: &RustBindingConfig,
450    has_error: bool,
451    return_wrap: &str,
452    is_opaque: bool,
453    inner_clone_line: &str,
454    is_unit_return: bool,
455) -> String {
456    let pattern_body = match cfg.async_pattern {
457        AsyncPattern::Pyo3FutureIntoPy => {
458            let result_handling = if has_error {
459                format!(
460                    "let result = {core_call}.await\n            \
461                     .map_err(|e| PyErr::new::<PyRuntimeError, _>(e.to_string()))?;"
462                )
463            } else if is_unit_return {
464                format!("{core_call}.await;")
465            } else {
466                format!("let result = {core_call}.await;")
467            };
468            let ok_expr = if is_unit_return && !has_error {
469                "()"
470            } else {
471                return_wrap
472            };
473            format!(
474                "pyo3_async_runtimes::tokio::future_into_py(py, async move {{\n            \
475                 {result_handling}\n            \
476                 Ok({ok_expr})\n        }})"
477            )
478        }
479        AsyncPattern::WasmNativeAsync => {
480            let result_handling = if has_error {
481                format!(
482                    "let result = {core_call}.await\n        \
483                     .map_err(|e| JsValue::from_str(&e.to_string()))?;"
484                )
485            } else if is_unit_return {
486                format!("{core_call}.await;")
487            } else {
488                format!("let result = {core_call}.await;")
489            };
490            let ok_expr = if is_unit_return && !has_error {
491                "()"
492            } else {
493                return_wrap
494            };
495            format!(
496                "{result_handling}\n        \
497                 Ok({ok_expr})"
498            )
499        }
500        AsyncPattern::NapiNativeAsync => {
501            let result_handling = if has_error {
502                format!(
503                    "let result = {core_call}.await\n            \
504                     .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))?;"
505                )
506            } else if is_unit_return {
507                format!("{core_call}.await;")
508            } else {
509                format!("let result = {core_call}.await;")
510            };
511            let ok_expr = if is_unit_return && !has_error {
512                "()"
513            } else {
514                return_wrap
515            };
516            format!(
517                "{result_handling}\n            \
518                 Ok({ok_expr})"
519            )
520        }
521        AsyncPattern::TokioBlockOn => {
522            if has_error {
523                if is_opaque {
524                    format!(
525                        "let rt = tokio::runtime::Runtime::new()?;\n        \
526                         let result = rt.block_on(async {{ {core_call}.await.map_err(|e| e.into()) }})?;\n        \
527                         {return_wrap}"
528                    )
529                } else {
530                    format!(
531                        "let rt = tokio::runtime::Runtime::new()?;\n        \
532                         rt.block_on(async {{ {core_call}.await.map_err(|e| e.into()) }})"
533                    )
534                }
535            } else if is_opaque {
536                if is_unit_return {
537                    format!(
538                        "let rt = tokio::runtime::Runtime::new()?;\n        \
539                         rt.block_on(async {{ {core_call}.await }});"
540                    )
541                } else {
542                    format!(
543                        "let rt = tokio::runtime::Runtime::new()?;\n        \
544                         let result = rt.block_on(async {{ {core_call}.await }});\n        \
545                         {return_wrap}"
546                    )
547                }
548            } else {
549                format!(
550                    "let rt = tokio::runtime::Runtime::new()?;\n        \
551                     rt.block_on(async {{ {core_call}.await }})"
552                )
553            }
554        }
555        AsyncPattern::None => "todo!(\"async not supported by backend\")".to_string(),
556    };
557    if inner_clone_line.is_empty() {
558        pattern_body
559    } else {
560        format!("{inner_clone_line}{pattern_body}")
561    }
562}
563
564/// Generate a compilable body for functions that can't be auto-delegated.
565/// Returns a default value or error instead of `todo!()` which would panic.
566pub fn gen_unimplemented_body(
567    return_type: &TypeRef,
568    fn_name: &str,
569    has_error: bool,
570    cfg: &RustBindingConfig,
571    params: &[ParamDef],
572) -> String {
573    // Suppress unused_variables by binding all params to `_`
574    let suppress = if params.is_empty() {
575        String::new()
576    } else {
577        let names: Vec<&str> = params.iter().map(|p| p.name.as_str()).collect();
578        if names.len() == 1 {
579            format!("let _ = {};\n        ", names[0])
580        } else {
581            format!("let _ = ({});\n        ", names.join(", "))
582        }
583    };
584    let err_msg = format!("Not implemented: {fn_name}");
585    let body = if has_error {
586        // Backend-specific error return
587        match cfg.async_pattern {
588            AsyncPattern::Pyo3FutureIntoPy => {
589                format!("Err(pyo3::exceptions::PyNotImplementedError::new_err(\"{err_msg}\"))")
590            }
591            AsyncPattern::NapiNativeAsync => {
592                format!("Err(napi::Error::new(napi::Status::GenericFailure, \"{err_msg}\"))")
593            }
594            AsyncPattern::WasmNativeAsync => {
595                format!("Err(JsValue::from_str(\"{err_msg}\"))")
596            }
597            _ => format!("Err(\"{err_msg}\".to_string())"),
598        }
599    } else {
600        // Return type-appropriate default
601        match return_type {
602            TypeRef::Unit => "()".to_string(),
603            TypeRef::String | TypeRef::Char | TypeRef::Path => format!("String::from(\"[unimplemented: {fn_name}]\")"),
604            TypeRef::Bytes => "Vec::new()".to_string(),
605            TypeRef::Primitive(p) => match p {
606                alef_core::ir::PrimitiveType::Bool => "false".to_string(),
607                _ => "0".to_string(),
608            },
609            TypeRef::Optional(_) => "None".to_string(),
610            TypeRef::Vec(_) => "Vec::new()".to_string(),
611            TypeRef::Map(_, _) => "Default::default()".to_string(),
612            TypeRef::Duration => "0".to_string(),
613            TypeRef::Named(_) | TypeRef::Json => {
614                // Named return without error type: can't return Err. Generate compilable panic.
615                format!("panic!(\"alef: {fn_name} not auto-delegatable\")")
616            }
617        }
618    };
619    format!("{suppress}{body}")
620}