Skip to main content

candle_graph/
load.rs

1//! Crate loading and the symbol table.
2//!
3//! We deliberately do *not* attempt real type inference. The measured property that makes this
4//! sound for candle model code is that such code is monomorphic and uses inherent `fn` methods
5//! on concrete structs — no trait objects, no generic modules. So a crate-local
6//! `struct -> field -> type` table plus `type -> methods` is enough to resolve
7//! `SelfAttention::new(cfg, vb.pp("self_attn"))` to a body.
8//!
9//! Where that assumption breaks we record a diagnostic instead of guessing.
10
11use anyhow::{Context, Result};
12use std::collections::{HashMap, HashSet};
13use std::path::{Path, PathBuf};
14use walkdir::WalkDir;
15
16use crate::ir::SrcSpan;
17
18pub struct SourceFile {
19    pub path: PathBuf,
20    /// Path relative to the scanned root, used in all human-facing output.
21    pub rel: String,
22}
23
24#[derive(Debug, Clone)]
25pub struct LoadDiagnostic {
26    pub path: String,
27    pub message: String,
28}
29
30/// A parsed field type, decomposed just enough to see through the containers candle model code
31/// actually uses.
32#[derive(Debug, Clone)]
33pub struct TypeRef {
34    /// Full source text, e.g. `Vec<Layer>`.
35    pub text: String,
36    /// Innermost named type after peeling containers, e.g. `Layer`.
37    pub base: String,
38    pub container: Container,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum Container {
43    Plain,
44    Vec,
45    Option,
46    Box,
47}
48
49#[derive(Debug, Clone)]
50pub struct FieldDef {
51    pub name: String,
52    pub ty: TypeRef,
53    pub span: SrcSpan,
54}
55
56#[derive(Debug, Clone)]
57pub struct StructDef {
58    pub name: String,
59    /// Rust module containing the definition (`""` means the crate root).
60    pub module_path: String,
61    /// Module-qualified identity, for example `model::encoder::Encoder`.
62    pub qualified_name: String,
63    /// Source spelling of the visibility. Private items use the empty string.
64    pub visibility: String,
65    /// `#[cfg(...)]` predicates inherited from inline modules and attached to this definition.
66    pub cfg_predicates: Vec<String>,
67    pub fields: Vec<FieldDef>,
68    pub span: SrcSpan,
69}
70
71/// An inherent method on a concrete type.
72#[derive(Clone)]
73pub struct ImplFn {
74    pub type_name: String,
75    pub fn_name: String,
76    /// Implemented trait for trait methods (for example `candle_core::Module`).
77    /// `None` denotes an inherent method.
78    pub trait_name: Option<String>,
79    /// Rust module containing the definition (`""` means the crate root).
80    pub module_path: String,
81    /// Qualified owning type for methods. Empty for free functions.
82    pub qualified_type_name: String,
83    /// Fully-qualified function identity.
84    pub qualified_name: String,
85    /// Source spelling of the visibility. Private items use the empty string.
86    pub visibility: String,
87    /// `#[cfg(...)]` predicates inherited from modules/impls and attached to this function.
88    pub cfg_predicates: Vec<String>,
89    /// Parameter names in order; used to find which argument is the `VarBuilder`.
90    pub params: Vec<String>,
91    /// Parameter type source text in the same order as `params`.
92    ///
93    /// Receivers are represented by their source spelling (`self`, `&self`, etc.).
94    pub param_types: Vec<String>,
95    /// Explicit return type source text, or `()` when omitted.
96    pub return_type: String,
97    /// Indices of every parameter whose type mentions `VarBuilder`.
98    ///
99    /// Plural because models routinely take more than one: a constructor may take a frozen
100    /// `base_vb` (mmapped weights) and a trainable `train_vb` (a `VarMap`). Which
101    /// builder a tensor came from *is* the frozen/trainable distinction, so collapsing them
102    /// would discard the thing the gradient analysis will need most.
103    pub vb_params: Vec<usize>,
104    pub block: syn::Block,
105    pub span: SrcSpan,
106}
107
108/// A public `use` leaf. Keeping the syntactic target is useful even when that target cannot be
109/// resolved without Cargo/rustc name resolution.
110#[derive(Debug, Clone, PartialEq, Eq)]
111pub struct PublicReexport {
112    /// Name made public by the re-export (the alias after `as`, when present).
113    pub name: String,
114    /// Qualified name of the public binding.
115    pub qualified_name: String,
116    /// Best-effort module-qualified target path.
117    pub target: String,
118    pub module_path: String,
119    pub span: SrcSpan,
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct ImportBinding {
124    pub module_path: String,
125    pub alias: String,
126    pub target: String,
127}
128
129#[derive(Default)]
130pub struct Crate {
131    pub files: Vec<SourceFile>,
132    /// Filesystem and parse failures retained as structured incomplete-analysis evidence.
133    pub diagnostics: Vec<LoadDiagnostic>,
134    /// Legacy bare-name map. When names collide, deterministic source traversal preserves the
135    /// historical last-definition-wins behavior. Use `struct_candidates` for new analysis.
136    pub structs: HashMap<String, StructDef>,
137    /// `(type_name, fn_name) -> method`.
138    pub methods: HashMap<(String, String), ImplFn>,
139    /// Free functions by name, for `fn build_encoder(vb: VarBuilder) -> ..` helpers.
140    pub functions: HashMap<String, ImplFn>,
141    /// Exact qualified identities. These maps make unambiguous lookups cheap.
142    pub qualified_structs: HashMap<String, StructDef>,
143    pub qualified_methods: HashMap<(String, String), ImplFn>,
144    pub qualified_functions: HashMap<String, ImplFn>,
145    pub public_reexports: Vec<PublicReexport>,
146    pub imports: Vec<ImportBinding>,
147    /// Rust crate identifier to Cargo package name, including dependency renames.
148    pub dependency_aliases: HashMap<String, String>,
149    // Complete definition lists back the collision-safe candidate APIs. In particular, they also
150    // retain cfg-gated duplicate qualified definitions instead of silently overwriting them.
151    all_structs: Vec<StructDef>,
152    all_methods: Vec<ImplFn>,
153    all_functions: Vec<ImplFn>,
154}
155
156impl Crate {
157    /// Every struct definition, including colliding and cfg-gated alternatives.
158    pub fn all_structs(&self) -> impl Iterator<Item = &StructDef> {
159        self.all_structs.iter()
160    }
161
162    /// Every inherent method, including colliding and cfg-gated alternatives.
163    pub fn all_methods(&self) -> impl Iterator<Item = &ImplFn> {
164        self.all_methods.iter()
165    }
166
167    /// Every free function, including colliding and cfg-gated alternatives.
168    pub fn all_functions(&self) -> impl Iterator<Item = &ImplFn> {
169        self.all_functions.iter()
170    }
171
172    pub fn field_type(&self, struct_name: &str, field: &str) -> Option<&TypeRef> {
173        self.structs
174            .get(struct_name)?
175            .fields
176            .iter()
177            .find(|f| f.name == field)
178            .map(|f| &f.ty)
179    }
180
181    pub fn file_label(&self, span: SrcSpan) -> String {
182        match self.files.get(span.file) {
183            Some(f) => format!("{}:{}", f.rel, span.line),
184            None => format!("<unknown>:{}", span.line),
185        }
186    }
187
188    /// All structs matching either a bare or exact qualified name, sorted by qualified identity
189    /// and source location. Unlike the legacy map, collisions are never discarded.
190    pub fn struct_candidates(&self, name: &str) -> Vec<&StructDef> {
191        let qualified = name.contains("::");
192        let mut found: Vec<_> = self
193            .all_structs
194            .iter()
195            .filter(|def| {
196                if qualified {
197                    def.qualified_name == name
198                } else {
199                    def.name == name
200                }
201            })
202            .collect();
203        sort_struct_candidates(&mut found);
204        found
205    }
206
207    /// All free functions matching either a bare or exact qualified name.
208    pub fn function_candidates(&self, name: &str) -> Vec<&ImplFn> {
209        let qualified = name.contains("::");
210        let mut found: Vec<_> = self
211            .all_functions
212            .iter()
213            .filter(|func| {
214                if qualified {
215                    func.qualified_name == name
216                } else {
217                    func.fn_name == name
218                }
219            })
220            .collect();
221        sort_fn_candidates(&mut found);
222        found
223    }
224
225    /// All inherent methods matching a bare or qualified owner and a function name.
226    pub fn method_candidates(&self, type_name: &str, fn_name: &str) -> Vec<&ImplFn> {
227        let qualified = type_name.contains("::");
228        let mut found: Vec<_> = self
229            .all_methods
230            .iter()
231            .filter(|func| {
232                func.fn_name == fn_name
233                    && if qualified {
234                        func.qualified_type_name == type_name
235                    } else {
236                        func.type_name == type_name
237                    }
238            })
239            .collect();
240        // Rust method syntax selects an inherent method ahead of trait methods. Preserve trait
241        // methods when no inherent definition exists so the ubiquitous `impl Module for Model`
242        // entrypoint remains analyzable.
243        if found.iter().any(|func| func.trait_name.is_none()) {
244            found.retain(|func| func.trait_name.is_none());
245        }
246        sort_fn_candidates(&mut found);
247        found
248    }
249
250    /// Public re-export leaves matching a bare binding or exact qualified binding.
251    pub fn reexport_candidates(&self, name: &str) -> Vec<&PublicReexport> {
252        let qualified = name.contains("::");
253        let mut found: Vec<_> = self
254            .public_reexports
255            .iter()
256            .filter(|item| {
257                if qualified {
258                    item.qualified_name == name
259                } else {
260                    item.name == name
261                }
262            })
263            .collect();
264        found.sort_by(|a, b| {
265            (&a.qualified_name, a.span.file, a.span.line, a.span.col).cmp(&(
266                &b.qualified_name,
267                b.span.file,
268                b.span.line,
269                b.span.col,
270            ))
271        });
272        found
273    }
274
275    /// Resolve a source path through an explicit `use` binding in the containing module.
276    pub fn resolve_import_path(&self, module_path: &str, segments: &[String]) -> Vec<String> {
277        let Some(first) = segments.first() else {
278            return Vec::new();
279        };
280        let Some(binding) = self
281            .imports
282            .iter()
283            .find(|binding| binding.module_path == module_path && binding.alias == *first)
284        else {
285            return self.resolve_dependency_alias(segments);
286        };
287        let mut resolved = binding
288            .target
289            .split("::")
290            .map(str::to_string)
291            .collect::<Vec<_>>();
292        resolved.extend(segments.iter().skip(1).cloned());
293        self.resolve_dependency_alias(&resolved)
294    }
295
296    /// Resolve an alias when every binding of that spelling agrees on the same external target.
297    /// This is used by legacy structure extraction where the current inline module is not carried
298    /// through the compact arena.
299    pub fn resolve_unambiguous_import_path(&self, segments: &[String]) -> Vec<String> {
300        let Some(first) = segments.first() else {
301            return Vec::new();
302        };
303        let mut targets = self
304            .imports
305            .iter()
306            .filter(|binding| binding.alias == *first)
307            .map(|binding| binding.target.as_str())
308            .collect::<Vec<_>>();
309        targets.sort_unstable();
310        targets.dedup();
311        let [target] = targets.as_slice() else {
312            return self.resolve_dependency_alias(segments);
313        };
314        let mut resolved = target.split("::").map(str::to_string).collect::<Vec<_>>();
315        resolved.extend(segments.iter().skip(1).cloned());
316        self.resolve_dependency_alias(&resolved)
317    }
318
319    pub fn set_dependency_aliases(&mut self, aliases: impl IntoIterator<Item = (String, String)>) {
320        self.dependency_aliases = aliases.into_iter().collect();
321        for binding in &mut self.imports {
322            let parts = binding.target.split("::").collect::<Vec<_>>();
323            let dependency = parts
324                .first()
325                .and_then(|name| self.dependency_aliases.get(*name))
326                .map(|package| (0usize, package))
327                .or_else(|| {
328                    parts
329                        .last()
330                        .and_then(|name| self.dependency_aliases.get(*name))
331                        .map(|package| (parts.len().saturating_sub(1), package))
332                });
333            if let Some((index, package)) = dependency {
334                let mut resolved = if index == parts.len().saturating_sub(1) {
335                    Vec::new()
336                } else {
337                    parts[..index]
338                        .iter()
339                        .map(|part| (*part).to_string())
340                        .collect()
341                };
342                resolved.push(package.replace('-', "_"));
343                resolved.extend(parts.iter().skip(index + 1).map(|part| (*part).to_string()));
344                binding.target = resolved.join("::");
345            }
346        }
347    }
348
349    fn resolve_dependency_alias(&self, segments: &[String]) -> Vec<String> {
350        let Some(first) = segments.first() else {
351            return Vec::new();
352        };
353        let Some(package) = self.dependency_aliases.get(first) else {
354            return segments.to_vec();
355        };
356        let mut resolved = vec![package.replace('-', "_")];
357        resolved.extend(segments.iter().skip(1).cloned());
358        resolved
359    }
360}
361
362fn sort_struct_candidates(found: &mut Vec<&StructDef>) {
363    found.sort_by(|a, b| {
364        (&a.qualified_name, a.span.file, a.span.line, a.span.col).cmp(&(
365            &b.qualified_name,
366            b.span.file,
367            b.span.line,
368            b.span.col,
369        ))
370    });
371}
372
373fn sort_fn_candidates(found: &mut Vec<&ImplFn>) {
374    found.sort_by(|a, b| {
375        (&a.qualified_name, a.span.file, a.span.line, a.span.col).cmp(&(
376            &b.qualified_name,
377            b.span.file,
378            b.span.line,
379            b.span.col,
380        ))
381    });
382}
383
384/// Parse every `.rs` file under `root`, skipping `target/` and hidden directories.
385pub fn load(root: &Path) -> Result<Crate> {
386    let mut krate = Crate::default();
387
388    for entry in WalkDir::new(root)
389        .sort_by_file_name()
390        .into_iter()
391        .filter_entry(|e| {
392            let name = e.file_name().to_string_lossy();
393            !(matches!(
394                name.as_ref(),
395                "target" | "artifacts" | "runs" | "node_modules"
396            ) || name.starts_with('.'))
397        })
398    {
399        let entry = match entry {
400            Ok(entry) => entry,
401            Err(error) => {
402                krate.diagnostics.push(LoadDiagnostic {
403                    path: error
404                        .path()
405                        .map(|path| path.to_string_lossy().into_owned())
406                        .unwrap_or_else(|| root.to_string_lossy().into_owned()),
407                    message: format!("filesystem traversal failed: {error}"),
408                });
409                continue;
410            }
411        };
412        if !entry.file_type().is_file() {
413            continue;
414        }
415        if entry.path().extension().and_then(|e| e.to_str()) != Some("rs") {
416            continue;
417        }
418
419        let path = entry.path().to_path_buf();
420        let rel = path
421            .strip_prefix(root)
422            .unwrap_or(&path)
423            .to_string_lossy()
424            .to_string();
425        let text = std::fs::read_to_string(&path)
426            .with_context(|| format!("reading {}", path.display()))?;
427
428        // A file that does not parse is reported and skipped rather than aborting the run: a
429        // partial map of a large crate is more useful than no map.
430        let ast = match syn::parse_file(&text) {
431            Ok(ast) => ast,
432            Err(err) => {
433                krate.diagnostics.push(LoadDiagnostic {
434                    path: rel,
435                    message: format!("parse error: {err}"),
436                });
437                continue;
438            }
439        };
440
441        let file_index = krate.files.len();
442        krate.files.push(SourceFile { path, rel });
443        let module_path = file_module_path(&krate.files[file_index].rel);
444        let inherited_cfg = cfg_predicates(&ast.attrs);
445        collect_items(
446            &mut krate,
447            file_index,
448            &module_path,
449            &inherited_cfg,
450            &ast.items,
451        );
452    }
453
454    Ok(krate)
455}
456
457/// Load only Rust modules reachable from the selected Cargo crate roots.
458///
459/// This deliberately excludes sibling tests, examples, benches, and inactive alternative source
460/// trees unless the selected Cargo target names one of them as its root.
461pub fn load_from_roots(root: &Path, crate_roots: &[PathBuf]) -> Result<Crate> {
462    let mut krate = Crate::default();
463    let mut visited = HashSet::new();
464    let normalized_root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
465    for crate_root in crate_roots {
466        load_module_file(
467            &mut krate,
468            &normalized_root,
469            crate_root,
470            "",
471            &[],
472            &mut visited,
473        )?;
474    }
475    Ok(krate)
476}
477
478fn load_module_file(
479    krate: &mut Crate,
480    scan_root: &Path,
481    path: &Path,
482    module_path: &str,
483    inherited_cfg: &[String],
484    visited: &mut HashSet<(PathBuf, String)>,
485) -> Result<()> {
486    let normalized = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
487    if !visited.insert((normalized, module_path.to_string())) {
488        return Ok(());
489    }
490
491    let rel = path
492        .strip_prefix(scan_root)
493        .unwrap_or(path)
494        .to_string_lossy()
495        .to_string();
496    let text = match std::fs::read_to_string(path) {
497        Ok(text) => text,
498        Err(error) => {
499            krate.diagnostics.push(LoadDiagnostic {
500                path: rel,
501                message: format!("read error: {error}"),
502            });
503            return Ok(());
504        }
505    };
506    let ast = match syn::parse_file(&text) {
507        Ok(ast) => ast,
508        Err(error) => {
509            krate.diagnostics.push(LoadDiagnostic {
510                path: rel,
511                message: format!("parse error: {error}"),
512            });
513            return Ok(());
514        }
515    };
516
517    let file_index = krate.files.len();
518    krate.files.push(SourceFile {
519        path: path.to_path_buf(),
520        rel,
521    });
522    let file_cfg = combined_cfg(inherited_cfg, &ast.attrs);
523    collect_items(krate, file_index, module_path, &file_cfg, &ast.items);
524
525    let module_dir = module_directory(path);
526    load_external_modules(
527        krate,
528        scan_root,
529        path,
530        &module_dir,
531        module_path,
532        &file_cfg,
533        &ast.items,
534        visited,
535    )
536}
537
538#[allow(clippy::too_many_arguments)]
539fn load_external_modules(
540    krate: &mut Crate,
541    scan_root: &Path,
542    declaring_file: &Path,
543    module_dir: &Path,
544    module_path: &str,
545    inherited_cfg: &[String],
546    items: &[syn::Item],
547    visited: &mut HashSet<(PathBuf, String)>,
548) -> Result<()> {
549    for item in items {
550        let syn::Item::Mod(module) = item else {
551            continue;
552        };
553        let child_name = module.ident.to_string();
554        let child_module = join_qualified(module_path, &child_name);
555        let child_cfg = combined_cfg(inherited_cfg, &module.attrs);
556        if let Some((_, inner)) = &module.content {
557            load_external_modules(
558                krate,
559                scan_root,
560                declaring_file,
561                &module_dir.join(&child_name),
562                &child_module,
563                &child_cfg,
564                inner,
565                visited,
566            )?;
567            continue;
568        }
569
570        let explicit_path = module.attrs.iter().find_map(|attr| {
571            if !attr.path().is_ident("path") {
572                return None;
573            }
574            let syn::Meta::NameValue(value) = &attr.meta else {
575                return None;
576            };
577            let syn::Expr::Lit(syn::ExprLit {
578                lit: syn::Lit::Str(value),
579                ..
580            }) = &value.value
581            else {
582                return None;
583            };
584            Some(value.value())
585        });
586        let candidates = if let Some(explicit) = explicit_path {
587            vec![declaring_file
588                .parent()
589                .unwrap_or_else(|| Path::new(""))
590                .join(explicit)]
591        } else {
592            vec![
593                module_dir.join(format!("{child_name}.rs")),
594                module_dir.join(&child_name).join("mod.rs"),
595            ]
596        };
597        match candidates.iter().find(|candidate| candidate.is_file()) {
598            Some(source) => {
599                load_module_file(krate, scan_root, source, &child_module, &child_cfg, visited)?
600            }
601            None => krate.diagnostics.push(LoadDiagnostic {
602                path: declaring_file
603                    .strip_prefix(scan_root)
604                    .unwrap_or(declaring_file)
605                    .to_string_lossy()
606                    .into_owned(),
607                message: format!(
608                    "module `{child_module}` was declared but no source file was found"
609                ),
610            }),
611        }
612    }
613    Ok(())
614}
615
616fn module_directory(path: &Path) -> PathBuf {
617    let parent = path.parent().unwrap_or_else(|| Path::new(""));
618    match path.file_name().and_then(|name| name.to_str()) {
619        Some("lib.rs" | "main.rs" | "mod.rs") => parent.to_path_buf(),
620        _ => parent.join(
621            path.file_stem()
622                .and_then(|name| name.to_str())
623                .unwrap_or_default(),
624        ),
625    }
626}
627
628fn collect_items(
629    krate: &mut Crate,
630    file: usize,
631    module_path: &str,
632    inherited_cfg: &[String],
633    items: &[syn::Item],
634) {
635    // Imports are module-scoped regardless of source order. Collect them before definitions so
636    // signature aliases such as `use candle_nn::VarBuilder as VB` resolve everywhere.
637    for item in items {
638        if let syn::Item::Use(item_use) = item {
639            collect_use(krate, file, module_path, item_use, is_public(&item_use.vis));
640        }
641    }
642    for item in items {
643        match item {
644            syn::Item::Struct(s) => {
645                let def = struct_def(file, module_path, inherited_cfg, s);
646                krate.structs.insert(def.name.clone(), def.clone());
647                krate
648                    .qualified_structs
649                    .insert(def.qualified_name.clone(), def.clone());
650                krate.all_structs.push(def);
651            }
652            syn::Item::Impl(imp) => collect_impl(krate, file, module_path, inherited_cfg, imp),
653            syn::Item::Fn(f) => {
654                let predicates = combined_cfg(inherited_cfg, &f.attrs);
655                let mut func = impl_fn(
656                    file,
657                    module_path,
658                    String::new(),
659                    String::new(),
660                    None,
661                    &f.vis,
662                    &f.sig,
663                    &f.block,
664                    predicates,
665                );
666                augment_builder_aliases(krate, module_path, &mut func);
667                krate.functions.insert(func.fn_name.clone(), func.clone());
668                krate
669                    .qualified_functions
670                    .insert(func.qualified_name.clone(), func.clone());
671                krate.all_functions.push(func);
672            }
673            syn::Item::Mod(m) => {
674                if let Some((_, inner)) = &m.content {
675                    let child_module = join_qualified(module_path, &m.ident.to_string());
676                    let child_cfg = combined_cfg(inherited_cfg, &m.attrs);
677                    collect_items(krate, file, &child_module, &child_cfg, inner);
678                }
679            }
680            syn::Item::Use(_) => {}
681            _ => {}
682        }
683    }
684}
685
686fn collect_impl(
687    krate: &mut Crate,
688    file: usize,
689    module_path: &str,
690    inherited_cfg: &[String],
691    imp: &syn::ItemImpl,
692) {
693    let Some(type_name) = type_base_name(&imp.self_ty) else {
694        return;
695    };
696    let trait_name = imp.trait_.as_ref().map(|(_, path, _)| type_text(path));
697    let qualified_type_name = qualify_type(module_path, &imp.self_ty)
698        .unwrap_or_else(|| join_qualified(module_path, &type_name));
699    let impl_cfg = combined_cfg(inherited_cfg, &imp.attrs);
700    for item in &imp.items {
701        if let syn::ImplItem::Fn(f) = item {
702            let predicates = combined_cfg(&impl_cfg, &f.attrs);
703            let mut func = impl_fn(
704                file,
705                module_path,
706                type_name.clone(),
707                qualified_type_name.clone(),
708                trait_name.clone(),
709                &f.vis,
710                &f.sig,
711                &f.block,
712                predicates,
713            );
714            augment_builder_aliases(krate, module_path, &mut func);
715            krate
716                .methods
717                .insert((type_name.clone(), func.fn_name.clone()), func.clone());
718            krate.qualified_methods.insert(
719                (qualified_type_name.clone(), func.fn_name.clone()),
720                func.clone(),
721            );
722            krate.all_methods.push(func);
723        }
724    }
725}
726
727fn augment_builder_aliases(krate: &Crate, module_path: &str, function: &mut ImplFn) {
728    for (index, type_text) in function.param_types.iter().enumerate() {
729        if function.vb_params.contains(&index) {
730            continue;
731        }
732        let Some(builder_alias) = syn::parse_str::<syn::Type>(type_text)
733            .ok()
734            .is_some_and(|ty| contains_resolved_builder_type(&ty, krate, module_path))
735            .then_some(index)
736        else {
737            continue;
738        };
739        function.vb_params.push(builder_alias);
740    }
741    function.vb_params.sort_unstable();
742    function.vb_params.dedup();
743}
744
745fn contains_resolved_builder_type(ty: &syn::Type, krate: &Crate, module_path: &str) -> bool {
746    match ty {
747        syn::Type::Path(path) => path.path.segments.iter().any(|segment| {
748            let source = vec![segment.ident.to_string()];
749            let resolved = krate.resolve_import_path(module_path, &source);
750            matches!(
751                resolved.last().map(String::as_str),
752                Some("VarBuilder" | "VarBuilderArgs")
753            ) || match &segment.arguments {
754                syn::PathArguments::AngleBracketed(arguments) => {
755                    arguments.args.iter().any(|argument| {
756                        matches!(
757                            argument,
758                            syn::GenericArgument::Type(inner)
759                                if contains_resolved_builder_type(inner, krate, module_path)
760                        )
761                    })
762                }
763                syn::PathArguments::Parenthesized(arguments) => arguments
764                    .inputs
765                    .iter()
766                    .any(|inner| contains_resolved_builder_type(inner, krate, module_path)),
767                syn::PathArguments::None => false,
768            }
769        }),
770        syn::Type::Reference(reference) => {
771            contains_resolved_builder_type(&reference.elem, krate, module_path)
772        }
773        syn::Type::Paren(paren) => contains_resolved_builder_type(&paren.elem, krate, module_path),
774        syn::Type::Group(group) => contains_resolved_builder_type(&group.elem, krate, module_path),
775        syn::Type::Tuple(tuple) => tuple
776            .elems
777            .iter()
778            .any(|inner| contains_resolved_builder_type(inner, krate, module_path)),
779        _ => false,
780    }
781}
782
783#[allow(clippy::too_many_arguments)]
784fn impl_fn(
785    file: usize,
786    module_path: &str,
787    type_name: String,
788    qualified_type_name: String,
789    trait_name: Option<String>,
790    visibility: &syn::Visibility,
791    sig: &syn::Signature,
792    block: &syn::Block,
793    cfg_predicates: Vec<String>,
794) -> ImplFn {
795    let mut params = Vec::new();
796    let mut param_types = Vec::new();
797    let mut vb_params = Vec::new();
798
799    for (index, input) in sig.inputs.iter().enumerate() {
800        match input {
801            syn::FnArg::Receiver(receiver) => {
802                params.push("self".to_string());
803                param_types.push(type_text(receiver));
804            }
805            syn::FnArg::Typed(pat) => {
806                let name = match &*pat.pat {
807                    syn::Pat::Ident(id) => id.ident.to_string(),
808                    other => type_text(other),
809                };
810                // Matches `VarBuilder`, `Option<VarBuilder>` and references without accepting
811                // unrelated types such as `MyVarBuilderConfig`.
812                if contains_named_type(&pat.ty, "VarBuilder")
813                    || contains_named_type(&pat.ty, "VarBuilderArgs")
814                {
815                    vb_params.push(index);
816                }
817                params.push(name);
818                param_types.push(type_text(&pat.ty));
819            }
820        }
821    }
822
823    let fn_name = sig.ident.to_string();
824    let qualified_name = if qualified_type_name.is_empty() {
825        join_qualified(module_path, &fn_name)
826    } else {
827        join_qualified(&qualified_type_name, &fn_name)
828    };
829    ImplFn {
830        type_name,
831        fn_name,
832        trait_name,
833        module_path: module_path.to_string(),
834        qualified_type_name,
835        qualified_name,
836        visibility: visibility_text(visibility),
837        cfg_predicates,
838        params,
839        param_types,
840        return_type: match &sig.output {
841            syn::ReturnType::Default => "()".to_string(),
842            syn::ReturnType::Type(_, ty) => type_text(ty),
843        },
844        vb_params,
845        block: block.clone(),
846        span: span_of(file, sig.ident.span()),
847    }
848}
849
850fn struct_def(
851    file: usize,
852    module_path: &str,
853    inherited_cfg: &[String],
854    s: &syn::ItemStruct,
855) -> StructDef {
856    let mut fields = Vec::new();
857    if let syn::Fields::Named(named) = &s.fields {
858        for f in &named.named {
859            let Some(ident) = &f.ident else { continue };
860            fields.push(FieldDef {
861                name: ident.to_string(),
862                ty: type_ref(&f.ty),
863                span: span_of(file, ident.span()),
864            });
865        }
866    }
867    StructDef {
868        name: s.ident.to_string(),
869        module_path: module_path.to_string(),
870        qualified_name: join_qualified(module_path, &s.ident.to_string()),
871        visibility: visibility_text(&s.vis),
872        cfg_predicates: combined_cfg(inherited_cfg, &s.attrs),
873        fields,
874        span: span_of(file, s.ident.span()),
875    }
876}
877
878fn combined_cfg(inherited: &[String], attrs: &[syn::Attribute]) -> Vec<String> {
879    let mut predicates = inherited.to_vec();
880    predicates.extend(cfg_predicates(attrs));
881    predicates.sort();
882    predicates.dedup();
883    predicates
884}
885
886fn cfg_predicates(attrs: &[syn::Attribute]) -> Vec<String> {
887    attrs
888        .iter()
889        .filter_map(|attribute| match &attribute.meta {
890            syn::Meta::List(list) if list.path.is_ident("cfg") => Some(list.tokens.to_string()),
891            _ => None,
892        })
893        .collect()
894}
895
896/// Derive a Rust module path from a path relative to the scan root.
897///
898/// `foo.rs` and `foo/mod.rs` both map to `foo`; `lib.rs` and `main.rs` map to the crate root.
899/// When a whole conventional Cargo crate is scanned, the leading `src/` directory is not a
900/// module.
901fn file_module_path(rel: &str) -> String {
902    let normalized = rel.replace('\\', "/");
903    let mut parts: Vec<&str> = normalized
904        .split('/')
905        .filter(|part| !part.is_empty())
906        .collect();
907    let Some(file) = parts.pop() else {
908        return String::new();
909    };
910    if parts.first() == Some(&"src") {
911        parts.remove(0);
912    }
913    let stem = file.strip_suffix(".rs").unwrap_or(file);
914    if !matches!(stem, "mod" | "lib" | "main") {
915        parts.push(stem);
916    }
917    parts.join("::")
918}
919
920fn join_qualified(module_path: &str, leaf: &str) -> String {
921    if module_path.is_empty() {
922        leaf.to_string()
923    } else if leaf.is_empty() {
924        module_path.to_string()
925    } else {
926        format!("{module_path}::{leaf}")
927    }
928}
929
930fn visibility_text(vis: &syn::Visibility) -> String {
931    type_text(vis).replace("pub (", "pub(").replace(" )", ")")
932}
933
934fn is_public(vis: &syn::Visibility) -> bool {
935    !matches!(vis, syn::Visibility::Inherited)
936}
937
938fn qualify_type(module_path: &str, ty: &syn::Type) -> Option<String> {
939    let syn::Type::Path(path) = ty else {
940        return match ty {
941            syn::Type::Reference(reference) => qualify_type(module_path, &reference.elem),
942            syn::Type::Paren(paren) => qualify_type(module_path, &paren.elem),
943            _ => None,
944        };
945    };
946    let parts: Vec<String> = path
947        .path
948        .segments
949        .iter()
950        .map(|segment| segment.ident.to_string())
951        .collect();
952    qualify_path_parts(module_path, &parts)
953}
954
955fn qualify_path_parts(module_path: &str, parts: &[String]) -> Option<String> {
956    let (first, rest) = parts.split_first()?;
957    match first.as_str() {
958        "crate" => Some(rest.join("::")),
959        "self" => Some(join_qualified(module_path, &rest.join("::"))),
960        "super" => {
961            let mut base: Vec<&str> = module_path
962                .split("::")
963                .filter(|part| !part.is_empty())
964                .collect();
965            let mut remaining = rest;
966            while remaining.first().map(String::as_str) == Some("super") {
967                base.pop();
968                remaining = &remaining[1..];
969            }
970            base.pop();
971            let suffix = remaining.join("::");
972            Some(join_qualified(&base.join("::"), &suffix))
973        }
974        _ if matches!(
975            first.as_str(),
976            "candle"
977                | "candle_core"
978                | "candle_nn"
979                | "candle_transformers"
980                | "std"
981                | "core"
982                | "alloc"
983        ) =>
984        {
985            Some(parts.join("::"))
986        }
987        _ if parts.len() == 1 => Some(join_qualified(module_path, first)),
988        _ => Some(parts.join("::")),
989    }
990}
991
992fn collect_use(
993    krate: &mut Crate,
994    file: usize,
995    module_path: &str,
996    item_use: &syn::ItemUse,
997    public: bool,
998) {
999    let mut leaves = Vec::new();
1000    flatten_use_tree(Vec::new(), &item_use.tree, &mut leaves);
1001    for (path, alias, span) in leaves {
1002        let Some(source_name) = path.last() else {
1003            continue;
1004        };
1005        let name = alias.unwrap_or_else(|| source_name.clone());
1006        let target = qualify_path_parts(module_path, &path).unwrap_or_else(|| path.join("::"));
1007        krate.imports.push(ImportBinding {
1008            module_path: module_path.to_string(),
1009            alias: name.clone(),
1010            target: target.clone(),
1011        });
1012        if public {
1013            krate.public_reexports.push(PublicReexport {
1014                qualified_name: join_qualified(module_path, &name),
1015                name,
1016                target,
1017                module_path: module_path.to_string(),
1018                span: span_of(file, span),
1019            });
1020        }
1021    }
1022}
1023
1024fn flatten_use_tree(
1025    prefix: Vec<String>,
1026    tree: &syn::UseTree,
1027    leaves: &mut Vec<(Vec<String>, Option<String>, proc_macro2::Span)>,
1028) {
1029    match tree {
1030        syn::UseTree::Path(path) => {
1031            let mut next = prefix;
1032            next.push(path.ident.to_string());
1033            flatten_use_tree(next, &path.tree, leaves);
1034        }
1035        syn::UseTree::Name(name) => {
1036            if name.ident == "self" {
1037                if !prefix.is_empty() {
1038                    leaves.push((prefix, None, name.ident.span()));
1039                }
1040            } else {
1041                let mut path = prefix;
1042                path.push(name.ident.to_string());
1043                leaves.push((path, None, name.ident.span()));
1044            }
1045        }
1046        syn::UseTree::Rename(rename) => {
1047            let mut path = prefix;
1048            if rename.ident != "self" {
1049                path.push(rename.ident.to_string());
1050            }
1051            leaves.push((path, Some(rename.rename.to_string()), rename.rename.span()));
1052        }
1053        syn::UseTree::Group(group) => {
1054            for item in &group.items {
1055                flatten_use_tree(prefix.clone(), item, leaves);
1056            }
1057        }
1058        // A glob has no statically enumerable leaf.
1059        syn::UseTree::Glob(_) => {}
1060    }
1061}
1062
1063/// Peel `Vec<T>`, `Option<T>` and `Box<T>` down to the named type inside.
1064pub fn type_ref(ty: &syn::Type) -> TypeRef {
1065    let text = type_text(ty);
1066    let (base, container) = peel(ty);
1067    TypeRef {
1068        text,
1069        base,
1070        container,
1071    }
1072}
1073
1074fn peel(ty: &syn::Type) -> (String, Container) {
1075    let syn::Type::Path(p) = ty else {
1076        return (type_base_name(ty).unwrap_or_default(), Container::Plain);
1077    };
1078    let Some(last) = p.path.segments.last() else {
1079        return (String::new(), Container::Plain);
1080    };
1081    let container = match last.ident.to_string().as_str() {
1082        "Vec" => Container::Vec,
1083        "Option" => Container::Option,
1084        "Box" => Container::Box,
1085        _ => Container::Plain,
1086    };
1087    if container == Container::Plain {
1088        return (last.ident.to_string(), Container::Plain);
1089    }
1090    if let syn::PathArguments::AngleBracketed(args) = &last.arguments {
1091        for arg in &args.args {
1092            if let syn::GenericArgument::Type(inner) = arg {
1093                let (base, _) = peel(inner);
1094                return (base, container);
1095            }
1096        }
1097    }
1098    (last.ident.to_string(), container)
1099}
1100
1101/// Last path segment of a type, e.g. `nn::Linear` -> `Linear`.
1102pub fn type_base_name(ty: &syn::Type) -> Option<String> {
1103    match ty {
1104        syn::Type::Path(p) => p.path.segments.last().map(|s| s.ident.to_string()),
1105        syn::Type::Reference(r) => type_base_name(&r.elem),
1106        syn::Type::Paren(p) => type_base_name(&p.elem),
1107        _ => None,
1108    }
1109}
1110
1111fn contains_named_type(ty: &syn::Type, wanted: &str) -> bool {
1112    match ty {
1113        syn::Type::Path(path) => path.path.segments.iter().any(|segment| {
1114            segment.ident == wanted
1115                || match &segment.arguments {
1116                    syn::PathArguments::AngleBracketed(arguments) => {
1117                        arguments.args.iter().any(|argument| match argument {
1118                            syn::GenericArgument::Type(inner) => contains_named_type(inner, wanted),
1119                            _ => false,
1120                        })
1121                    }
1122                    syn::PathArguments::Parenthesized(arguments) => {
1123                        arguments
1124                            .inputs
1125                            .iter()
1126                            .any(|inner| contains_named_type(inner, wanted))
1127                            || match &arguments.output {
1128                                syn::ReturnType::Default => false,
1129                                syn::ReturnType::Type(_, output) => {
1130                                    contains_named_type(output, wanted)
1131                                }
1132                            }
1133                    }
1134                    syn::PathArguments::None => false,
1135                }
1136        }),
1137        syn::Type::Reference(reference) => contains_named_type(&reference.elem, wanted),
1138        syn::Type::Paren(paren) => contains_named_type(&paren.elem, wanted),
1139        syn::Type::Group(group) => contains_named_type(&group.elem, wanted),
1140        syn::Type::Tuple(tuple) => tuple
1141            .elems
1142            .iter()
1143            .any(|inner| contains_named_type(inner, wanted)),
1144        _ => false,
1145    }
1146}
1147
1148pub fn type_text<T: quote::ToTokens>(t: &T) -> String {
1149    let mut text = quote::quote!(#t).to_string();
1150    // `quote` inserts spaces around punctuation; tighten the common cases so paths read
1151    // naturally in output.
1152    for (from, to) in [(" :: ", "::"), (" < ", "<"), (" > ", ">"), (" , ", ", ")] {
1153        text = text.replace(from, to);
1154    }
1155    text
1156}
1157
1158pub fn span_of(file: usize, span: proc_macro2::Span) -> SrcSpan {
1159    let start = span.start();
1160    SrcSpan {
1161        file,
1162        line: start.line,
1163        col: start.column,
1164    }
1165}