Skip to main content

alef_codegen/generators/
functions.rs

1use crate::generators::binding_helpers::{
2    gen_async_body, gen_call_args, gen_call_args_cfg, gen_call_args_with_let_bindings, gen_named_let_bindings,
3    gen_named_let_bindings_by_ref, gen_serde_let_bindings, 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::{AHashMap, AHashSet};
9use alef_core::ir::{ApiSurface, FunctionDef, TypeRef};
10
11/// Generate a free function.
12pub fn gen_function(
13    func: &FunctionDef,
14    mapper: &dyn TypeMapper,
15    cfg: &RustBindingConfig,
16    adapter_bodies: &AdapterBodies,
17    opaque_types: &AHashSet<String>,
18) -> String {
19    let map_fn = |ty: &alef_core::ir::TypeRef| mapper.map_type(ty);
20    // When named_non_opaque_params_by_ref is true (extendr backend), Named non-opaque struct
21    // params must use references because extendr only generates TryFrom<&Robj> for &T.
22    // - Required params: `&T` (extendr generates TryFrom<&Robj> for &T)
23    // - Optional params: `Nullable<&T>` (extendr's Nullable<T: TryFrom<&Robj>>)
24    // - Promoted-optional (required following optional): `Nullable<&T>` (treated as optional)
25    // After the first optional/Nullable param, all subsequent params are also promoted.
26    let params = if cfg.named_non_opaque_params_by_ref {
27        let mut seen_optional = false;
28        func.params
29            .iter()
30            .enumerate()
31            .map(|(idx, p)| {
32                if p.optional {
33                    seen_optional = true;
34                }
35                let promoted = seen_optional && !p.optional && crate::shared::is_promoted_optional(&func.params, idx);
36                let ty = match &p.ty {
37                    TypeRef::Named(n) if !opaque_types.contains(n.as_str()) => {
38                        if p.optional || seen_optional || promoted {
39                            format!("Nullable<&{}>", map_fn(&p.ty))
40                        } else {
41                            format!("&{}", map_fn(&p.ty))
42                        }
43                    }
44                    _ => {
45                        if p.optional || seen_optional {
46                            format!("Option<{}>", map_fn(&p.ty))
47                        } else {
48                            map_fn(&p.ty)
49                        }
50                    }
51                };
52                format!("{}: {}", p.name, ty)
53            })
54            .collect::<Vec<_>>()
55            .join(", ")
56    } else {
57        function_params(&func.params, &map_fn)
58    };
59    let return_type = mapper.map_type(&func.return_type);
60    let ret = mapper.wrap_return(&return_type, func.error_type.is_some());
61
62    // Use let-binding pattern for non-opaque Named params so core fns can take &CoreType.
63    // When named_non_opaque_params_by_ref is set (extendr), the binding signature uses &T,
64    // so we simulate is_ref=true for Named non-opaque params when generating call args.
65    let effective_params: std::borrow::Cow<[alef_core::ir::ParamDef]> = if cfg.named_non_opaque_params_by_ref {
66        let modified: Vec<alef_core::ir::ParamDef> = func
67            .params
68            .iter()
69            .map(|p| {
70                if matches!(&p.ty, TypeRef::Named(n) if !opaque_types.contains(n.as_str())) {
71                    alef_core::ir::ParamDef {
72                        is_ref: true,
73                        ..p.clone()
74                    }
75                } else {
76                    p.clone()
77                }
78            })
79            .collect();
80        std::borrow::Cow::Owned(modified)
81    } else {
82        std::borrow::Cow::Borrowed(&func.params)
83    };
84    let use_let_bindings = has_named_params(&effective_params, opaque_types);
85    let call_args = if use_let_bindings {
86        gen_call_args_with_let_bindings(&effective_params, opaque_types)
87    } else if cfg.cast_uints_to_i32 || cfg.cast_large_ints_to_f64 {
88        gen_call_args_cfg(
89            &effective_params,
90            opaque_types,
91            cfg.cast_uints_to_i32,
92            cfg.cast_large_ints_to_f64,
93        )
94    } else {
95        gen_call_args(&effective_params, opaque_types)
96    };
97    let core_import = cfg.core_import;
98    let let_bindings = if use_let_bindings {
99        if cfg.named_non_opaque_params_by_ref {
100            // Params are `&T` in the signature — use .clone().into() for conversion.
101            gen_named_let_bindings_by_ref(&func.params, opaque_types, core_import)
102        } else {
103            gen_named_let_bindings(&func.params, opaque_types, core_import)
104        }
105    } else {
106        String::new()
107    };
108
109    // Use the function's rust_path for correct module path resolution
110    let core_fn_path = {
111        let path = func.rust_path.replace('-', "_");
112        if path.starts_with(core_import) {
113            path
114        } else {
115            format!("{core_import}::{}", func.name)
116        }
117    };
118
119    let can_delegate = crate::shared::can_auto_delegate_function(func, opaque_types)
120        || can_delegate_with_named_let_bindings(func, opaque_types);
121
122    // Backend-specific error conversion string for serde bindings
123    let serde_err_conv = match cfg.async_pattern {
124        AsyncPattern::Pyo3FutureIntoPy => ".map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))",
125        AsyncPattern::NapiNativeAsync => ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))",
126        AsyncPattern::WasmNativeAsync => ".map_err(|e| JsValue::from_str(&e.to_string()))",
127        AsyncPattern::TokioBlockOn => {
128            ".map_err(|e| extendr_api::Error::Other(e.to_string().replace(\":\", \"_\").replace(\"/\", \"_\").replace(\"-\", \"_\").chars().take(255).collect::<String>()))"
129        }
130        _ => ".map_err(|e| e.to_string())",
131    };
132
133    // Generate the body based on async pattern
134    let body = if !can_delegate {
135        // Check if an adapter provides the body
136        if let Some(adapter_body) = adapter_bodies.get(&func.name) {
137            adapter_body.clone()
138        } else if cfg.has_serde && use_let_bindings && func.error_type.is_some() {
139            // MARKER_SERDE_PATH
140            // Serde-based param conversion: serialize binding types to JSON, deserialize to core types.
141            // This handles Named params (e.g., ProcessConfig) that lack binding→core From impls.
142            // For async functions with Pyo3FutureIntoPy, serde bindings use indented format.
143            let is_async_pyo3 = func.is_async && cfg.async_pattern == AsyncPattern::Pyo3FutureIntoPy;
144            let (serde_indent, serde_err_async) = if is_async_pyo3 {
145                (
146                    "        ",
147                    ".map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))",
148                )
149            } else {
150                ("    ", serde_err_conv)
151            };
152            let serde_bindings =
153                gen_serde_let_bindings(&func.params, opaque_types, core_import, serde_err_async, serde_indent);
154            let core_call = format!("{core_fn_path}({call_args})");
155
156            // Determine return wrapping strategy for serde async (uses explicit types to avoid E0283)
157            let returns_ref = func.returns_ref;
158            let wrap_return = |expr: &str| -> String {
159                match &func.return_type {
160                    TypeRef::Vec(inner) => {
161                        // Vec<T>: check if elements need conversion
162                        match inner.as_ref() {
163                            TypeRef::Named(_) => {
164                                // Vec<Named>: convert each element using Into::into
165                                format!("{expr}.into_iter().map(Into::into).collect()")
166                            }
167                            _ => expr.to_string(),
168                        }
169                    }
170                    TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
171                        if returns_ref {
172                            format!("{name} {{ inner: Arc::new({expr}.clone()) }}")
173                        } else {
174                            format!("{name} {{ inner: Arc::new({expr}) }}")
175                        }
176                    }
177                    TypeRef::Named(_) => {
178                        // Use explicit type with ::from() to avoid E0283 type inference issues in async context
179                        if returns_ref {
180                            format!("{return_type}::from({expr}.clone())")
181                        } else {
182                            format!("{return_type}::from({expr})")
183                        }
184                    }
185                    // String/Bytes are identity across all backends (String->String,
186                    // Vec<u8>->Vec<u8>) — no .into() needed for owned values.
187                    TypeRef::String | TypeRef::Bytes => expr.to_string(),
188                    TypeRef::Path => format!("{expr}.to_string_lossy().to_string()"),
189                    TypeRef::Json => format!("{expr}.to_string()"),
190                    _ => expr.to_string(),
191                }
192            };
193
194            if is_async_pyo3 {
195                // Async serde path: wrap everything in future_into_py
196                let is_unit = matches!(func.return_type, TypeRef::Unit);
197                let wrapped = wrap_return("result");
198                let core_await = format!(
199                    "{core_call}.await\n            .map_err(|e| PyErr::new::<PyRuntimeError, _>(e.to_string()))?"
200                );
201                let inner_body = if is_unit {
202                    format!("{serde_bindings}{core_await};\n            Ok(())")
203                } else {
204                    // When wrapped contains type conversions like .into() or ::from(),
205                    // bind to a variable to help type inference for the generic future_into_py.
206                    // This avoids E0283 "type annotations needed".
207                    if wrapped.contains(".into()") || wrapped.contains("::from(") || wrapped.contains("Into::into") {
208                        // Add explicit type annotation to help type inference
209                        format!(
210                            "{serde_bindings}let result = {core_await};\n            let wrapped_result: {return_type} = {wrapped};\n            Ok(wrapped_result)"
211                        )
212                    } else {
213                        format!("{serde_bindings}let result = {core_await};\n            Ok({wrapped})")
214                    }
215                };
216                format!("pyo3_async_runtimes::tokio::future_into_py(py, async move {{\n{inner_body}\n        }})")
217            } else if func.is_async {
218                // Async serde path for other backends (NAPI, etc.): use gen_async_body
219                let is_unit = matches!(func.return_type, TypeRef::Unit);
220                let wrapped = wrap_return("result");
221                let async_body = gen_async_body(
222                    &core_call,
223                    cfg,
224                    func.error_type.is_some(),
225                    &wrapped,
226                    false,
227                    "",
228                    is_unit,
229                    Some(&return_type),
230                );
231                format!("{serde_bindings}{async_body}")
232            } else if matches!(func.return_type, TypeRef::Unit) {
233                // Unit return with error: avoid let_unit_value
234                let await_kw = if func.is_async { ".await" } else { "" };
235                let debug_marker = if func.is_async { "/*ASYNC_UNIT*/ " } else { "" };
236                format!("{serde_bindings}{debug_marker}{core_call}{await_kw}{serde_err_conv}?;\n    Ok(())")
237            } else {
238                let wrapped = wrap_return("val");
239                let await_kw = if func.is_async { ".await" } else { "" };
240                if wrapped == "val" {
241                    format!("{serde_bindings}{core_call}{await_kw}{serde_err_conv}")
242                } else if wrapped == "val.into()" {
243                    format!("{serde_bindings}{core_call}{await_kw}.map(Into::into){serde_err_conv}")
244                } else if let Some(type_path) = wrapped.strip_suffix("::from(val)") {
245                    format!("{serde_bindings}{core_call}{await_kw}.map({type_path}::from){serde_err_conv}")
246                } else {
247                    format!("{serde_bindings}{core_call}{await_kw}.map(|val| {wrapped}){serde_err_conv}")
248                }
249            }
250        } else if func.is_async && cfg.async_pattern == AsyncPattern::Pyo3FutureIntoPy {
251            // Async function that can't be auto-delegated — wrap unimplemented body in future_into_py
252            let suppress = if func.params.is_empty() {
253                String::new()
254            } else {
255                let names: Vec<&str> = func.params.iter().map(|p| p.name.as_str()).collect();
256                format!("let _ = ({});\n        ", names.join(", "))
257            };
258            format!(
259                "{suppress}Err(pyo3::exceptions::PyNotImplementedError::new_err(\"not implemented: {}\"))",
260                func.name
261            )
262        } else {
263            // Function can't be auto-delegated — return a default/error based on return type
264            gen_unimplemented_body(
265                &func.return_type,
266                &func.name,
267                func.error_type.is_some(),
268                cfg,
269                &func.params,
270                opaque_types,
271            )
272        }
273    } else if func.is_async {
274        // MARKER_DELEGATE_ASYNC
275        let core_call = format!("{core_fn_path}({call_args})");
276        // In async contexts (future_into_py, etc.), the compiler often can't infer the
277        // target type for .into(). Use explicit From::from() / collect::<Vec<T>>() instead.
278        let return_wrap = match &func.return_type {
279            TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
280                format!("{n} {{ inner: Arc::new(result) }}")
281            }
282            TypeRef::Named(_) => {
283                format!("{return_type}::from(result)")
284            }
285            TypeRef::Vec(inner) => match inner.as_ref() {
286                TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
287                    format!("result.into_iter().map(|v| {n} {{ inner: Arc::new(v) }}).collect::<Vec<_>>()")
288                }
289                TypeRef::Named(_) => {
290                    let inner_mapped = mapper.map_type(inner);
291                    format!("result.into_iter().map({inner_mapped}::from).collect::<Vec<_>>()")
292                }
293                _ => "result".to_string(),
294            },
295            TypeRef::Unit => "result".to_string(),
296            _ => super::binding_helpers::wrap_return(
297                "result",
298                &func.return_type,
299                "",
300                opaque_types,
301                false,
302                func.returns_ref,
303                false,
304            ),
305        };
306        let async_body = gen_async_body(
307            &core_call,
308            cfg,
309            func.error_type.is_some(),
310            &return_wrap,
311            false,
312            "",
313            matches!(func.return_type, TypeRef::Unit),
314            Some(&return_type),
315        );
316        format!("{let_bindings}{async_body}")
317    } else {
318        let core_call = format!("{core_fn_path}({call_args})");
319
320        // Determine return wrapping strategy
321        let returns_ref = func.returns_ref;
322        let wrap_return = |expr: &str| -> String {
323            match &func.return_type {
324                // Opaque type return: wrap in Arc
325                TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
326                    if returns_ref {
327                        format!("{name} {{ inner: Arc::new({expr}.clone()) }}")
328                    } else {
329                        format!("{name} {{ inner: Arc::new({expr}) }}")
330                    }
331                }
332                // Non-opaque Named: use .into() if From impl exists
333                TypeRef::Named(_name) => {
334                    if returns_ref {
335                        format!("{expr}.clone().into()")
336                    } else {
337                        format!("{expr}.into()")
338                    }
339                }
340                // String/Bytes: .into() handles &str→String, skip for owned
341                TypeRef::String | TypeRef::Bytes => {
342                    if returns_ref {
343                        format!("{expr}.into()")
344                    } else {
345                        expr.to_string()
346                    }
347                }
348                // Path: PathBuf→String needs to_string_lossy
349                TypeRef::Path => format!("{expr}.to_string_lossy().to_string()"),
350                // Json: serde_json::Value to string
351                TypeRef::Json => format!("{expr}.to_string()"),
352                // Optional with opaque inner
353                TypeRef::Optional(inner) => match inner.as_ref() {
354                    TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
355                        if returns_ref {
356                            format!("{expr}.map(|v| {name} {{ inner: Arc::new(v.clone()) }})")
357                        } else {
358                            format!("{expr}.map(|v| {name} {{ inner: Arc::new(v) }})")
359                        }
360                    }
361                    TypeRef::Named(_) => {
362                        if returns_ref {
363                            format!("{expr}.map(|v| v.clone().into())")
364                        } else {
365                            format!("{expr}.map(Into::into)")
366                        }
367                    }
368                    TypeRef::Path => {
369                        format!("{expr}.map(|v| v.to_string_lossy().to_string())")
370                    }
371                    TypeRef::String | TypeRef::Bytes => {
372                        if returns_ref {
373                            format!("{expr}.map(Into::into)")
374                        } else {
375                            expr.to_string()
376                        }
377                    }
378                    TypeRef::Vec(vi) => match vi.as_ref() {
379                        TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
380                            format!("{expr}.map(|v| v.into_iter().map(|x| {name} {{ inner: Arc::new(x) }}).collect())")
381                        }
382                        TypeRef::Named(_) => {
383                            format!("{expr}.map(|v| v.into_iter().map(Into::into).collect())")
384                        }
385                        _ => expr.to_string(),
386                    },
387                    _ => expr.to_string(),
388                },
389                // Vec<Named>: map each element through Into
390                TypeRef::Vec(inner) => match inner.as_ref() {
391                    TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
392                        if returns_ref {
393                            format!("{expr}.into_iter().map(|v| {name} {{ inner: Arc::new(v.clone()) }}).collect()")
394                        } else {
395                            format!("{expr}.into_iter().map(|v| {name} {{ inner: Arc::new(v) }}).collect()")
396                        }
397                    }
398                    TypeRef::Named(_) => {
399                        if returns_ref {
400                            format!("{expr}.into_iter().map(|v| v.clone().into()).collect()")
401                        } else {
402                            format!("{expr}.into_iter().map(Into::into).collect()")
403                        }
404                    }
405                    TypeRef::Path => {
406                        format!("{expr}.into_iter().map(|v| v.to_string_lossy().to_string()).collect()")
407                    }
408                    TypeRef::String => {
409                        if returns_ref {
410                            // `&[&str]` → `Vec<String>`. `Into::into` would require
411                            // `impl From<&&str> for String` (which doesn't exist).
412                            format!("{expr}.iter().map(|s| s.to_string()).collect()")
413                        } else {
414                            expr.to_string()
415                        }
416                    }
417                    TypeRef::Bytes => {
418                        if returns_ref {
419                            format!("{expr}.iter().map(|b| b.to_vec()).collect()")
420                        } else {
421                            expr.to_string()
422                        }
423                    }
424                    _ => expr.to_string(),
425                },
426                _ => expr.to_string(),
427            }
428        };
429
430        if func.error_type.is_some() {
431            // Backend-specific error conversion
432            let err_conv = match cfg.async_pattern {
433                AsyncPattern::Pyo3FutureIntoPy => {
434                    ".map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))"
435                }
436                AsyncPattern::NapiNativeAsync => {
437                    ".map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))"
438                }
439                AsyncPattern::WasmNativeAsync => ".map_err(|e| JsValue::from_str(&e.to_string()))",
440                AsyncPattern::TokioBlockOn => {
441                    ".map_err(|e| extendr_api::Error::Other(e.to_string().replace(\":\", \"_\").replace(\"/\", \"_\").replace(\"-\", \"_\").chars().take(255).collect::<String>()))"
442                }
443                _ => ".map_err(|e| e.to_string())",
444            };
445            let wrapped = wrap_return("val");
446            if wrapped == "val" {
447                format!("{core_call}{err_conv}")
448            } else if wrapped == "val.into()" {
449                format!("{core_call}.map(Into::into){err_conv}")
450            } else if let Some(type_path) = wrapped.strip_suffix("::from(val)") {
451                format!("{core_call}.map({type_path}::from){err_conv}")
452            } else {
453                format!("{core_call}.map(|val| {wrapped}){err_conv}")
454            }
455        } else {
456            wrap_return(&core_call)
457        }
458    };
459
460    // Prepend let bindings for non-opaque Named params (sync delegate case).
461    // Only prepend when can_delegate is true — the !can_delegate serde path does its own bindings.
462    // However, always prepend Vec<String> ref bindings (names_refs) since serde path doesn't handle them.
463    let body = if !let_bindings.is_empty() && !func.is_async {
464        if can_delegate {
465            format!("{let_bindings}{body}")
466        } else {
467            // For the !can_delegate path, only prepend Vec<String>+is_ref bindings (names_refs)
468            // since serde bindings handle Named type conversions.
469            let vec_str_bindings: String = func.params.iter().filter(|p| {
470                p.is_ref && matches!(&p.ty, TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::String | TypeRef::Char))
471            }).map(|p| {
472                // Handle both Vec<String> and Option<Vec<String>> parameters.
473                // When p.optional=true, p.ty is the inner type (Vec<String>), so we need to unwrap first.
474                if p.optional {
475                    format!("let {}_refs: Vec<&str> = {}.as_ref().map(|v| v.iter().map(|s| s.as_str()).collect()).unwrap_or_default();\n    ", p.name, p.name)
476                } else {
477                    format!("let {}_refs: Vec<&str> = {}.iter().map(|s| s.as_str()).collect();\n    ", p.name, p.name)
478                }
479            }).collect();
480            if !vec_str_bindings.is_empty() {
481                format!("{vec_str_bindings}{body}")
482            } else {
483                body
484            }
485        }
486    } else {
487        body
488    };
489
490    // Wrap long signature if necessary
491    // TokioBlockOn functions block synchronously inside the body — the generated function
492    // must NOT be `async fn` because extendr's `#[extendr]` cannot return a Future.
493    let async_kw = if func.is_async && cfg.async_pattern != AsyncPattern::TokioBlockOn {
494        "async "
495    } else {
496        ""
497    };
498    let func_needs_py = func.is_async && cfg.async_pattern == AsyncPattern::Pyo3FutureIntoPy;
499
500    // For async PyO3 free functions, override return type and add lifetime generic.
501    let ret = if func_needs_py {
502        "PyResult<Bound<'py, PyAny>>".to_string()
503    } else {
504        ret
505    };
506    let func_lifetime = if func_needs_py { "<'py>" } else { "" };
507
508    let (func_sig, _params_formatted) = if params.len() > 100 {
509        // When formatting for long signatures, promote optional params like function_params() does
510        let mut seen_optional = false;
511        let wrapped_params = func
512            .params
513            .iter()
514            .map(|p| {
515                if p.optional {
516                    seen_optional = true;
517                }
518                let ty = if p.optional || seen_optional {
519                    format!("Option<{}>", mapper.map_type(&p.ty))
520                } else {
521                    mapper.map_type(&p.ty)
522                };
523                format!("{}: {}", p.name, ty)
524            })
525            .collect::<Vec<_>>()
526            .join(",\n    ");
527
528        // For async PyO3, we need special signature handling
529        if func_needs_py {
530            (
531                format!(
532                    "pub fn {}{func_lifetime}(py: Python<'py>,\n    {}\n) -> {ret}",
533                    func.name,
534                    wrapped_params,
535                    ret = ret
536                ),
537                "",
538            )
539        } else {
540            (
541                format!(
542                    "pub {async_kw}fn {}(\n    {}\n) -> {ret}",
543                    func.name,
544                    wrapped_params,
545                    ret = ret
546                ),
547                "",
548            )
549        }
550    } else if func_needs_py {
551        (
552            format!(
553                "pub fn {}{func_lifetime}(py: Python<'py>, {params}) -> {ret}",
554                func.name
555            ),
556            "",
557        )
558    } else {
559        (format!("pub {async_kw}fn {}({params}) -> {ret}", func.name), "")
560    };
561
562    let total_params = func.params.len() + if func_needs_py { 1 } else { 0 };
563    let sig_defaults = if cfg.needs_signature {
564        function_sig_defaults(&func.params)
565    } else {
566        String::new()
567    };
568    let attr_inner = cfg
569        .function_attr
570        .trim_start_matches('#')
571        .trim_start_matches('[')
572        .trim_end_matches(']');
573
574    crate::template_env::render(
575        "generators/functions/function_definition.jinja",
576        minijinja::context! {
577            has_too_many_arguments => total_params > 7,
578            has_missing_errors_doc => func.error_type.is_some(),
579            attr_inner => attr_inner,
580            needs_signature => cfg.needs_signature,
581            signature_prefix => cfg.signature_prefix,
582            sig_defaults => sig_defaults,
583            signature_suffix => cfg.signature_suffix,
584            func_sig => func_sig,
585            body => body,
586        },
587    )
588}
589
590fn can_delegate_with_named_let_bindings(func: &FunctionDef, opaque_types: &AHashSet<String>) -> bool {
591    !func.sanitized
592        && func
593            .params
594            .iter()
595            .all(|p| !p.sanitized && crate::shared::is_delegatable_param(&p.ty, opaque_types))
596        && crate::shared::is_delegatable_return(&func.return_type)
597}
598
599/// Collect all unique trait import paths from types' methods.
600///
601/// Returns a deduplicated, sorted list of trait paths (e.g. `["liter_llm::LlmClient"]`)
602/// that need to be imported in generated binding code so that trait methods can be called.
603/// Both opaque and non-opaque types are scanned because non-opaque wrapper types also
604/// delegate trait method calls to their inner core type.
605pub fn collect_trait_imports(api: &ApiSurface) -> Vec<String> {
606    // Collect all trait paths, then deduplicate by last segment (trait name).
607    // When two paths resolve to the same trait name (e.g. `mylib_core::Dependency`
608    // and `mylib_core::di::Dependency`), only one import is needed. Keep the
609    // shorter (public re-export) path to avoid E0252 duplicate-import errors.
610    let mut traits: AHashSet<String> = AHashSet::new();
611    for typ in api.types.iter().filter(|typ| !typ.is_trait) {
612        for method in &typ.methods {
613            if let Some(ref trait_path) = method.trait_source {
614                traits.insert(trait_path.clone());
615            }
616        }
617    }
618
619    // Deduplicate by last path segment: keep the shortest path for each trait name.
620    let mut by_name: AHashMap<String, String> = AHashMap::new();
621    for path in traits {
622        let name = path.split("::").last().unwrap_or(&path).to_string();
623        let entry = by_name.entry(name).or_insert_with(|| path.clone());
624        // Prefer shorter paths (public re-exports are shorter than internal paths)
625        if path.len() < entry.len() {
626            *entry = path;
627        }
628    }
629
630    let mut sorted: Vec<String> = by_name.into_values().collect();
631    sorted.sort();
632    sorted
633}
634
635/// Check if any type has methods from trait impls whose trait_source could not be resolved.
636///
637/// When true, the binding crate should add a glob import of the core crate (e.g.
638/// `use kreuzberg::*`) to bring all publicly exported traits into scope.
639/// This handles traits defined in private submodules that are re-exported.
640pub fn has_unresolved_trait_methods(api: &ApiSurface) -> bool {
641    // Count method names that appear on multiple non-trait types but lack trait_source.
642    // Such methods likely come from trait impls whose trait path could not be resolved
643    // (e.g. traits defined in private modules but re-exported via `pub use`).
644    let mut method_counts: AHashMap<&str, (usize, usize)> = AHashMap::new(); // (total, with_source)
645    for typ in api.types.iter().filter(|typ| !typ.is_trait) {
646        if typ.is_trait {
647            continue;
648        }
649        for method in &typ.methods {
650            let entry = method_counts.entry(&method.name).or_insert((0, 0));
651            entry.0 += 1;
652            if method.trait_source.is_some() {
653                entry.1 += 1;
654            }
655        }
656    }
657    // A method appearing on 3+ types without trait_source on any is almost certainly a trait method
658    method_counts
659        .values()
660        .any(|&(total, with_source)| total >= 3 && with_source == 0)
661}
662
663/// Collect explicit type and enum names from the API surface for named imports.
664///
665/// Returns a sorted, deduplicated list of type and enum names that should be
666/// imported from the core crate. This replaces glob imports (`use core::*`)
667/// which can cause name conflicts with local binding definitions (e.g. a
668/// `convert` function or `Result` type alias from the core crate shadowing
669/// the binding's own `convert` wrapper or `std::result::Result`).
670///
671/// Only struct/enum names are included — functions and type aliases are
672/// intentionally excluded because they are the source of conflicts.
673pub fn collect_explicit_core_imports(api: &ApiSurface) -> Vec<String> {
674    let mut names = std::collections::BTreeSet::new();
675    for typ in api.types.iter().filter(|typ| !typ.is_trait) {
676        names.insert(typ.name.clone());
677    }
678    for e in &api.enums {
679        names.insert(e.name.clone());
680    }
681    names.into_iter().collect()
682}