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