Skip to main content

alef_codegen/generators/
functions.rs

1use crate::generators::binding_helpers::{
2    gen_async_body, gen_call_args, gen_call_args_with_let_bindings, gen_named_let_bindings, gen_serde_let_bindings,
3    gen_unimplemented_body, has_named_params,
4};
5use crate::generators::{AdapterBodies, AsyncPattern, RustBindingConfig};
6use crate::shared::{function_params, function_sig_defaults};
7use crate::type_mapper::TypeMapper;
8use ahash::AHashSet;
9use alef_core::ir::{ApiSurface, FunctionDef, TypeRef};
10use std::fmt::Write;
11
12/// Generate a free function.
13pub fn gen_function(
14    func: &FunctionDef,
15    mapper: &dyn TypeMapper,
16    cfg: &RustBindingConfig,
17    adapter_bodies: &AdapterBodies,
18    opaque_types: &AHashSet<String>,
19) -> String {
20    let map_fn = |ty: &alef_core::ir::TypeRef| mapper.map_type(ty);
21    let params = function_params(&func.params, &map_fn);
22    let return_type = mapper.map_type(&func.return_type);
23    let ret = mapper.wrap_return(&return_type, func.error_type.is_some());
24
25    // Use let-binding pattern for non-opaque Named params so core fns can take &CoreType
26    let use_let_bindings = has_named_params(&func.params, opaque_types);
27    let call_args = if use_let_bindings {
28        gen_call_args_with_let_bindings(&func.params, opaque_types)
29    } else {
30        gen_call_args(&func.params, opaque_types)
31    };
32    let let_bindings = if use_let_bindings {
33        gen_named_let_bindings(&func.params, opaque_types)
34    } else {
35        String::new()
36    };
37    let core_import = cfg.core_import;
38
39    // Use the function's rust_path for correct module path resolution
40    let core_fn_path = {
41        let path = func.rust_path.replace('-', "_");
42        if path.starts_with(core_import) {
43            path
44        } else {
45            format!("{core_import}::{}", func.name)
46        }
47    };
48
49    let can_delegate = crate::shared::can_auto_delegate_function(func, opaque_types);
50
51    // Backend-specific error conversion string for serde bindings
52    let serde_err_conv = match cfg.async_pattern {
53        AsyncPattern::Pyo3FutureIntoPy => ".map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))",
54        AsyncPattern::NapiNativeAsync => ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))",
55        AsyncPattern::WasmNativeAsync => ".map_err(|e| JsValue::from_str(&e.to_string()))",
56        _ => ".map_err(|e| e.to_string())",
57    };
58
59    // Generate the body based on async pattern
60    let body = if !can_delegate {
61        // Check if an adapter provides the body
62        if let Some(adapter_body) = adapter_bodies.get(&func.name) {
63            adapter_body.clone()
64        } else if cfg.has_serde && use_let_bindings && func.error_type.is_some() {
65            // Serde-based param conversion: serialize binding types to JSON, deserialize to core types.
66            // This handles Named params (e.g., ProcessConfig) that lack binding→core From impls.
67            let serde_bindings =
68                gen_serde_let_bindings(&func.params, opaque_types, core_import, serde_err_conv, "    ");
69            let core_call = format!("{core_fn_path}({call_args})");
70
71            // Determine return wrapping strategy (same as delegatable case)
72            let returns_ref = func.returns_ref;
73            let wrap_return = |expr: &str| -> String {
74                match &func.return_type {
75                    TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
76                        if returns_ref {
77                            format!("{name} {{ inner: Arc::new({expr}.clone()) }}")
78                        } else {
79                            format!("{name} {{ inner: Arc::new({expr}) }}")
80                        }
81                    }
82                    TypeRef::Named(_name) => {
83                        if returns_ref {
84                            format!("{expr}.clone().into()")
85                        } else {
86                            format!("{expr}.into()")
87                        }
88                    }
89                    TypeRef::String | TypeRef::Bytes => format!("{expr}.into()"),
90                    TypeRef::Path => format!("{expr}.to_string_lossy().to_string()"),
91                    TypeRef::Json => format!("{expr}.to_string()"),
92                    _ => expr.to_string(),
93                }
94            };
95
96            if matches!(func.return_type, TypeRef::Unit) {
97                // Unit return with error: avoid let_unit_value
98                format!("{serde_bindings}{core_call}{serde_err_conv}?;\n    Ok(())")
99            } else {
100                let wrapped = wrap_return("val");
101                if wrapped == "val" {
102                    format!("{serde_bindings}{core_call}{serde_err_conv}")
103                } else {
104                    format!("{serde_bindings}{core_call}.map(|val| {wrapped}){serde_err_conv}")
105                }
106            }
107        } else {
108            // Function can't be auto-delegated — return a default/error based on return type
109            gen_unimplemented_body(
110                &func.return_type,
111                &func.name,
112                func.error_type.is_some(),
113                cfg,
114                &func.params,
115            )
116        }
117    } else if func.is_async {
118        let core_call = format!("{core_fn_path}({call_args})");
119        let return_wrap = format!("{return_type}::from(result)");
120        let async_body = gen_async_body(
121            &core_call,
122            cfg,
123            func.error_type.is_some(),
124            &return_wrap,
125            false,
126            "",
127            matches!(func.return_type, TypeRef::Unit),
128        );
129        format!("{let_bindings}{async_body}")
130    } else {
131        let core_call = format!("{core_fn_path}({call_args})");
132
133        // Determine return wrapping strategy
134        let returns_ref = func.returns_ref;
135        let wrap_return = |expr: &str| -> String {
136            match &func.return_type {
137                // Opaque type return: wrap in Arc
138                TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
139                    if returns_ref {
140                        format!("{name} {{ inner: Arc::new({expr}.clone()) }}")
141                    } else {
142                        format!("{name} {{ inner: Arc::new({expr}) }}")
143                    }
144                }
145                // Non-opaque Named: use .into() if From impl exists
146                TypeRef::Named(_name) => {
147                    if returns_ref {
148                        format!("{expr}.clone().into()")
149                    } else {
150                        format!("{expr}.into()")
151                    }
152                }
153                // String/Bytes: .into() handles &str→String etc.
154                TypeRef::String | TypeRef::Bytes => format!("{expr}.into()"),
155                // Path: PathBuf→String needs to_string_lossy
156                TypeRef::Path => format!("{expr}.to_string_lossy().to_string()"),
157                // Json: serde_json::Value to string
158                TypeRef::Json => format!("{expr}.to_string()"),
159                // Optional with opaque inner
160                TypeRef::Optional(inner) => match inner.as_ref() {
161                    TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
162                        if returns_ref {
163                            format!("{expr}.map(|v| {name} {{ inner: Arc::new(v.clone()) }})")
164                        } else {
165                            format!("{expr}.map(|v| {name} {{ inner: Arc::new(v) }})")
166                        }
167                    }
168                    TypeRef::Named(_) => {
169                        if returns_ref {
170                            format!("{expr}.map(|v| v.clone().into())")
171                        } else {
172                            format!("{expr}.map(Into::into)")
173                        }
174                    }
175                    TypeRef::String | TypeRef::Bytes | TypeRef::Path => {
176                        format!("{expr}.map(Into::into)")
177                    }
178                    _ => expr.to_string(),
179                },
180                // Vec<Named>: map each element through Into
181                TypeRef::Vec(inner) => match inner.as_ref() {
182                    TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
183                        if returns_ref {
184                            format!("{expr}.into_iter().map(|v| {name} {{ inner: Arc::new(v.clone()) }}).collect()")
185                        } else {
186                            format!("{expr}.into_iter().map(|v| {name} {{ inner: Arc::new(v) }}).collect()")
187                        }
188                    }
189                    TypeRef::Named(_) => {
190                        if returns_ref {
191                            format!("{expr}.into_iter().map(|v| v.clone().into()).collect()")
192                        } else {
193                            format!("{expr}.into_iter().map(Into::into).collect()")
194                        }
195                    }
196                    TypeRef::String | TypeRef::Bytes | TypeRef::Path => {
197                        format!("{expr}.into_iter().map(Into::into).collect()")
198                    }
199                    _ => expr.to_string(),
200                },
201                _ => expr.to_string(),
202            }
203        };
204
205        if func.error_type.is_some() {
206            // Backend-specific error conversion
207            let err_conv = match cfg.async_pattern {
208                AsyncPattern::Pyo3FutureIntoPy => {
209                    ".map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))"
210                }
211                AsyncPattern::NapiNativeAsync => {
212                    ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))"
213                }
214                AsyncPattern::WasmNativeAsync => ".map_err(|e| JsValue::from_str(&e.to_string()))",
215                _ => ".map_err(|e| e.to_string())",
216            };
217            let wrapped = wrap_return("val");
218            if wrapped == "val" {
219                format!("{core_call}{err_conv}")
220            } else {
221                format!("{core_call}.map(|val| {wrapped}){err_conv}")
222            }
223        } else {
224            wrap_return(&core_call)
225        }
226    };
227
228    // Prepend let bindings for non-opaque Named params (sync non-adapter case)
229    let body = if !let_bindings.is_empty() && can_delegate && !func.is_async {
230        format!("{let_bindings}{body}")
231    } else {
232        body
233    };
234
235    // Wrap long signature if necessary
236    let async_kw = if func.is_async { "async " } else { "" };
237    let func_needs_py = func.is_async && cfg.async_pattern == AsyncPattern::Pyo3FutureIntoPy;
238
239    // For async PyO3 free functions, override return type and add lifetime generic.
240    let ret = if func_needs_py {
241        "PyResult<Bound<'py, PyAny>>".to_string()
242    } else {
243        ret
244    };
245    let func_lifetime = if func_needs_py { "<'py>" } else { "" };
246
247    let (func_sig, _params_formatted) = if params.len() > 100 {
248        let wrapped_params = func
249            .params
250            .iter()
251            .map(|p| {
252                let ty = if p.optional {
253                    format!("Option<{}>", mapper.map_type(&p.ty))
254                } else {
255                    mapper.map_type(&p.ty)
256                };
257                format!("{}: {}", p.name, ty)
258            })
259            .collect::<Vec<_>>()
260            .join(",\n    ");
261
262        // For async PyO3, we need special signature handling
263        if func_needs_py {
264            (
265                format!(
266                    "pub fn {}{func_lifetime}(py: Python<'py>,\n    {}\n) -> {ret}",
267                    func.name,
268                    wrapped_params,
269                    ret = ret
270                ),
271                "",
272            )
273        } else {
274            (
275                format!(
276                    "pub {async_kw}fn {}(\n    {}\n) -> {ret}",
277                    func.name,
278                    wrapped_params,
279                    ret = ret
280                ),
281                "",
282            )
283        }
284    } else if func_needs_py {
285        (
286            format!(
287                "pub fn {}{func_lifetime}(py: Python<'py>, {params}) -> {ret}",
288                func.name
289            ),
290            "",
291        )
292    } else {
293        (format!("pub {async_kw}fn {}({params}) -> {ret}", func.name), "")
294    };
295
296    let mut out = String::with_capacity(1024);
297    // Per-item clippy suppression: too_many_arguments when >7 params (including py)
298    let total_params = func.params.len() + if func_needs_py { 1 } else { 0 };
299    if total_params > 7 {
300        writeln!(out, "#[allow(clippy::too_many_arguments)]").ok();
301    }
302    // Per-item clippy suppression: missing_errors_doc for Result-returning functions
303    if func.error_type.is_some() {
304        writeln!(out, "#[allow(clippy::missing_errors_doc)]").ok();
305    }
306    let attr_inner = cfg
307        .function_attr
308        .trim_start_matches('#')
309        .trim_start_matches('[')
310        .trim_end_matches(']');
311    writeln!(out, "#[{attr_inner}]").ok();
312    if cfg.needs_signature {
313        let sig = function_sig_defaults(&func.params);
314        writeln!(out, "{}{}{}", cfg.signature_prefix, sig, cfg.signature_suffix).ok();
315    }
316    write!(out, "{} {{\n    {body}\n}}", func_sig,).ok();
317    out
318}
319
320/// Collect all unique trait import paths from opaque types' methods.
321///
322/// Returns a deduplicated, sorted list of trait paths (e.g. `["liter_llm::LlmClient"]`)
323/// that need to be imported in generated binding code so that trait methods can be called.
324pub fn collect_trait_imports(api: &ApiSurface) -> Vec<String> {
325    let mut traits: AHashSet<String> = AHashSet::new();
326    for typ in &api.types {
327        if !typ.is_opaque {
328            continue;
329        }
330        for method in &typ.methods {
331            if let Some(ref trait_path) = method.trait_source {
332                traits.insert(trait_path.clone());
333            }
334        }
335    }
336    let mut sorted: Vec<String> = traits.into_iter().collect();
337    sorted.sort();
338    sorted
339}