1use std::collections::{BTreeSet, HashMap, HashSet};
9use std::path::{Path, PathBuf};
10
11use anyhow::{Context, Result};
12use quote::ToTokens;
13use syn::visit::{self, Visit};
14
15use crate::cargo_context::{CargoContext, CargoOptions};
16use crate::dataflow::{self, GradState, NodeKind, NumericImpact};
17use crate::extract::Extractor;
18use crate::ir::{Acquisition, Certainty, CheckpointMatch};
19use crate::load::{self, Crate, ImplFn, StructDef};
20use crate::model_ir::{
21 ArchitectureEdge, Artifact, ArtifactKind, AssemblySite, BuilderNamespace, BuilderRole,
22 BuilderSourceKind, CargoSummary, Component, Confidence, DeviceFact, Evidence, EvidenceKind,
23 Finding, FindingSeverity, Function, FunctionParameter, LayoutFact, ModelIr, Module, Operation,
24 OptimizerMembership, Parameter, ParameterRole, PipelineStage, RuntimeSummary, ShapeFact,
25 StableId, StageDispatchKind, StageKind, TensorContract, TensorRole, Visibility,
26};
27use crate::op_semantics::{self, GradFlow};
28use crate::runtime::{self, ExpectedIdentity, GradientState, RuntimeTrace};
29
30#[derive(Debug, Clone, Default)]
31pub struct ScanOptions {
32 pub cargo: CargoOptions,
33 pub runtime_trace: Option<PathBuf>,
34 pub component_root: Option<String>,
36 pub dataflow: bool,
38 pub heuristic_architecture: bool,
41}
42
43pub fn analyze(path: impl AsRef<Path>, options: &ScanOptions) -> Result<ModelIr> {
45 let path = path.as_ref();
46 let cargo_result = CargoContext::discover(path, &options.cargo);
47 let mut krate = match cargo_result.as_ref() {
48 Ok(context) => {
49 let roots = context.selected_source_roots(options.cargo.package_target.as_deref())?;
50 load::load_from_roots(path, &roots)?
51 }
52 Err(_) => load::load(path)?,
53 };
54 if let Ok(context) = cargo_result.as_ref() {
55 krate.set_dependency_aliases(context.dependency_aliases.clone());
56 }
57 if krate.all_structs().next().is_none() {
58 anyhow::bail!("no Rust structs found under {}", path.display());
59 }
60
61 let analysis_id = match cargo_result.as_ref() {
62 Ok(context) => StableId::new(
63 "analysis",
64 [cargo_build_id(
65 context,
66 options.cargo.package_target.as_deref(),
67 )],
68 ),
69 Err(_) => StableId::new("analysis", [canonical_label(path)]),
70 };
71 let mut model = ModelIr::empty(analysis_id);
72
73 let cargo = match cargo_result {
74 Ok(context) => {
75 model.cargo = Some(cargo_summary(
76 &context,
77 options.cargo.package_target.as_deref(),
78 ));
79 Some(context)
80 }
81 Err(error) => {
82 push_finding(
83 &mut model,
84 "cargo-context",
85 FindingSeverity::Warning,
86 Confidence::Proven,
87 format!("Cargo context unavailable: {error:#}"),
88 None,
89 Vec::new(),
90 );
91 None
92 }
93 };
94 for diagnostic in &krate.diagnostics {
95 push_finding(
96 &mut model,
97 "source-load",
98 FindingSeverity::Warning,
99 Confidence::Unknown,
100 format!("incomplete source analysis: {}", diagnostic.message),
101 Some(diagnostic.path.clone()),
102 Vec::new(),
103 );
104 }
105
106 let function_lookup = build_functions(&krate, cargo.as_ref(), &mut model);
107 link_calls(&krate, &function_lookup, &mut model);
108 discover_components(
109 &krate,
110 cargo.as_ref(),
111 options.component_root.as_deref(),
112 options.heuristic_architecture,
113 &mut model,
114 );
115 discover_composition_edges(&krate, &mut model);
116 discover_assembly_sites(&krate, &mut model);
117 add_contracts(&krate, &mut model);
118 if options.heuristic_architecture {
119 discover_architecture_edges(&krate, &mut model);
120 discover_subprocess_pipeline(&krate, &function_lookup, &mut model);
121 discover_pipeline_and_artifacts(&krate, &function_lookup, &mut model);
122 discover_optimizers(&krate, &mut model);
123 apply_optimizer_roles(&mut model);
124 } else {
125 push_finding(
126 &mut model,
127 "compiler-semantic-evidence",
128 FindingSeverity::Information,
129 Confidence::Unknown,
130 "architecture, pipeline, artifact, and optimizer relationships are unavailable until \
131 compiler-resolved value-flow evidence is implemented; use --heuristic-architecture \
132 for exploratory name- and call-order leads"
133 .to_string(),
134 None,
135 Vec::new(),
136 );
137 }
138
139 if options.dataflow {
140 add_dataflow(&krate, &mut model);
141 }
142
143 if let Some(path) = options.runtime_trace.as_deref() {
144 let text = std::fs::read_to_string(path)
145 .with_context(|| format!("reading runtime trace {}", path.display()))?;
146 let trace = runtime::parse(&text)
147 .with_context(|| format!("parsing runtime trace {}", path.display()))?;
148 merge_runtime(&mut model, &trace);
149 }
150
151 if let Some(cargo) = cargo {
152 flag_candle_semantics_version(&mut model, &cargo);
153 }
154 model.normalize();
155 Ok(model)
156}
157
158fn canonical_label(path: &Path) -> String {
159 path.canonicalize()
160 .unwrap_or_else(|_| path.to_path_buf())
161 .to_string_lossy()
162 .into_owned()
163}
164
165fn selected_target(context: &CargoContext, requested_target: Option<&str>) -> Option<String> {
166 requested_target.map(str::to_string).or_else(|| {
167 context
168 .targets
169 .iter()
170 .find(|target| target.kind.iter().any(|kind| kind == "lib"))
171 .or_else(|| {
172 context
173 .targets
174 .iter()
175 .find(|target| target.kind.iter().any(|kind| kind == "bin"))
176 })
177 .map(|target| target.name.clone())
178 })
179}
180
181fn cargo_build_id(context: &CargoContext, requested_target: Option<&str>) -> String {
182 let target = selected_target(context, requested_target).unwrap_or_else(|| "unknown".into());
183 let mut identity = String::from("candle-graph/build/1\0");
184 for part in [
185 context.package_name.as_str(),
186 context.package_version.as_str(),
187 target.as_str(),
188 ] {
189 identity.push_str(part);
190 identity.push('\0');
191 }
192 for feature in &context.active_features {
193 identity.push_str("feature=");
194 identity.push_str(feature);
195 identity.push('\0');
196 }
197 for cfg in &context.cfgs {
198 identity.push_str("cfg=");
199 identity.push_str(cfg);
200 identity.push('\0');
201 }
202 for (package, version) in &context.candle_versions {
203 identity.push_str("candle=");
204 identity.push_str(package);
205 identity.push('@');
206 identity.push_str(version);
207 identity.push('\0');
208 }
209
210 let hash = identity.bytes().fold(0xcbf29ce484222325_u64, |hash, byte| {
212 (hash ^ u64::from(byte)).wrapping_mul(0x100000001b3)
213 });
214 format!("candle-graph/build/1:{hash:016x}")
215}
216
217fn cargo_summary(context: &CargoContext, requested_target: Option<&str>) -> CargoSummary {
218 CargoSummary {
219 build_id: cargo_build_id(context, requested_target),
220 workspace_root: context.workspace_root.to_string_lossy().into_owned(),
221 manifest_path: context.manifest_path.to_string_lossy().into_owned(),
222 package_name: context.package_name.clone(),
223 package_version: context.package_version.clone(),
224 selected_target: selected_target(context, requested_target),
225 active_features: context.active_features.clone(),
226 active_cfg: context.cfgs.clone(),
227 candle_packages: context.candle_versions.clone(),
228 }
229}
230
231fn build_functions(
232 krate: &Crate,
233 cargo: Option<&CargoContext>,
234 model: &mut ModelIr,
235) -> HashMap<String, StableId> {
236 let mut lookup = HashMap::new();
237 for func in krate.all_functions().chain(krate.all_methods()) {
238 let cfg_active = cargo.and_then(|context| {
239 crate::cargo_context::cfg_predicates_active(&func.cfg_predicates, &context.cfgs)
240 });
241 if cfg_active == Some(false) {
242 continue;
243 }
244 let id = function_id(func);
245 lookup.insert(func.qualified_name.clone(), id.clone());
246 let tensor_signature = func.param_types.iter().any(|ty| is_tensor_type(ty))
247 || is_tensor_type(&func.return_type);
248 let is_loss = has_explicit_candle_loss_call(krate, func);
249 let is_entrypoint =
252 is_candle_module_entry(func) || is_loss || tensor_signature && func.visibility == "pub";
253 model.functions.push(Function {
254 id,
255 name: func.fn_name.clone(),
256 qualified_name: func.qualified_name.clone(),
257 owner_type: (!func.qualified_type_name.is_empty())
258 .then(|| func.qualified_type_name.clone()),
259 visibility: visibility(&func.visibility),
260 parameters: func
261 .params
262 .iter()
263 .zip(&func.param_types)
264 .map(|(name, type_name)| FunctionParameter {
265 name: name.clone(),
266 type_name: type_name.clone(),
267 })
268 .collect(),
269 return_type: (func.return_type != "()").then(|| func.return_type.clone()),
270 cfg_predicates: func.cfg_predicates.clone(),
271 cfg_active,
272 source: krate.file_label(func.span),
273 calls: Vec::new(),
274 tensor_inputs: Vec::new(),
275 tensor_outputs: Vec::new(),
276 is_entrypoint,
277 is_loss,
278 });
279 }
280 lookup
281}
282
283fn is_candle_module_entry(func: &ImplFn) -> bool {
284 let Some(trait_name) = func.trait_name.as_deref() else {
285 return false;
286 };
287 let trait_leaf = trait_name.rsplit("::").next().unwrap_or(trait_name);
288 matches!(
289 (trait_leaf, func.fn_name.as_str()),
290 ("Module", "forward")
291 | ("ModuleT", "forward_t")
292 | ("ModuleWithArgs", "forward")
293 | ("ModuleTWithArgs", "forward_t")
294 )
295}
296
297fn has_explicit_candle_loss_call(krate: &Crate, func: &ImplFn) -> bool {
298 let mut collector = CallCollector::default();
299 collector.visit_block(&func.block);
300 collector.calls.iter().any(|call| {
301 let segments = call.split("::").map(str::to_string).collect::<Vec<_>>();
302 let resolved = krate
303 .resolve_import_path(&func.module_path, &segments)
304 .join("::");
305 resolved
306 .strip_prefix("candle_nn::loss::")
307 .is_some_and(|leaf| {
308 matches!(
309 leaf,
310 "nll" | "cross_entropy" | "mse" | "binary_cross_entropy_with_logit" | "huber"
311 )
312 })
313 })
314}
315
316fn link_calls(krate: &Crate, lookup: &HashMap<String, StableId>, model: &mut ModelIr) {
317 let mut by_bare: HashMap<String, Vec<StableId>> = HashMap::new();
318 for function in &model.functions {
319 by_bare
320 .entry(function.name.clone())
321 .or_default()
322 .push(function.id.clone());
323 }
324 let index: HashMap<StableId, usize> = model
325 .functions
326 .iter()
327 .enumerate()
328 .map(|(index, function)| (function.id.clone(), index))
329 .collect();
330
331 for func in krate.all_functions().chain(krate.all_methods()) {
332 let mut collector = CallCollector::default();
333 collector.visit_block(&func.block);
334 let caller = function_id(func);
335 let Some(&caller_index) = index.get(&caller) else {
336 continue;
337 };
338 let mut calls = Vec::new();
339 for call in collector.calls {
340 if let Some(id) = resolve_call(&call, func, lookup, &by_bare) {
341 calls.push(id);
342 }
343 }
344 calls.sort();
345 calls.dedup();
346 model.functions[caller_index].calls = calls;
347 }
348}
349
350fn resolve_call(
351 call: &str,
352 caller: &ImplFn,
353 exact: &HashMap<String, StableId>,
354 bare: &HashMap<String, Vec<StableId>>,
355) -> Option<StableId> {
356 let clean = call.trim_start_matches("crate::");
357 for candidate in [
358 clean.to_string(),
359 qualify(&caller.module_path, clean),
360 clean
361 .strip_prefix("self::")
362 .map(|rest| qualify(&caller.module_path, rest))
363 .unwrap_or_default(),
364 ] {
365 if let Some(id) = exact.get(&candidate) {
366 return Some(id.clone());
367 }
368 }
369 if clean.contains("::") {
370 let suffix = format!("::{clean}");
371 let mut matches = exact
372 .iter()
373 .filter(|(name, _)| name.ends_with(&suffix))
374 .map(|(_, id)| id);
375 let first = matches.next().cloned();
376 if first.is_some() && matches.next().is_none() {
377 return first;
378 }
379 }
380 let leaf = clean.rsplit("::").next().unwrap_or(clean);
381 match bare.get(leaf).map(Vec::as_slice) {
382 Some([id]) => Some(id.clone()),
383 _ => None,
384 }
385}
386
387fn discover_architecture_edges(krate: &Crate, model: &mut ModelIr) {
388 let component_by_type: HashMap<String, StableId> = model
389 .components
390 .iter()
391 .flat_map(|component| {
392 [
393 (component.name.clone(), component.id.clone()),
394 (component.qualified_name.clone(), component.id.clone()),
395 ]
396 })
397 .collect();
398
399 let mut seen = HashSet::new();
400 for function in krate.all_functions().chain(krate.all_methods()) {
401 if !is_production_source(krate, function) {
402 continue;
403 }
404 let owner_fields = krate
405 .struct_candidates(&function.qualified_type_name)
406 .into_iter()
407 .next()
408 .map(|owner| {
409 owner
410 .fields
411 .iter()
412 .filter_map(|field| {
413 component_by_type
414 .get(&field.ty.base)
415 .cloned()
416 .map(|component| (field.name.clone(), component))
417 })
418 .collect()
419 })
420 .unwrap_or_default();
421 let mut collector = ComponentFlowCollector {
422 component_by_type: &component_by_type,
423 owner_fields,
424 locals: HashMap::new(),
425 sequence: Vec::new(),
426 };
427 let signature_sequence: Vec<StableId> = function
428 .param_types
429 .iter()
430 .filter_map(|type_name| {
431 type_base_from_text(type_name)
432 .and_then(|base| component_by_type.get(&base).cloned())
433 })
434 .fold(Vec::new(), |mut sequence, component| {
435 if sequence.last() != Some(&component) {
436 sequence.push(component);
437 }
438 sequence
439 });
440 collector.visit_block(&function.block);
441 let from_signature = signature_sequence.len() > collector.sequence.len();
442 let sequence = if from_signature {
443 signature_sequence
444 } else {
445 collector.sequence
446 };
447 for pair in sequence.windows(2) {
448 if pair[0] == pair[1] {
449 continue;
450 }
451 let key = (pair[0].clone(), pair[1].clone(), function_id(function));
452 if !seen.insert(key.clone()) {
453 continue;
454 }
455 model.architecture_edges.push(ArchitectureEdge {
456 id: StableId::new(
457 "architecture-edge",
458 [key.0 .0.as_str(), key.1 .0.as_str(), key.2 .0.as_str()],
459 ),
460 from: key.0,
461 to: key.1,
462 via_function: key.2,
463 source: krate.file_label(function.span),
464 evidence: vec![heuristic_source_evidence(
465 krate.file_label(function.span),
466 if from_signature {
467 "component-typed parameters establish this interface order"
468 } else {
469 "typed component receiver calls occur in this source order"
470 },
471 )],
472 });
473 }
474 }
475}
476
477fn infer_builder_role(name: &str) -> (BuilderRole, Confidence) {
478 let lower = name.to_ascii_lowercase();
479 if lower.contains("train") || lower == "adapter_vb" {
480 return (BuilderRole::Trainable, Confidence::Heuristic);
481 }
482 if lower.contains("base")
483 || lower.contains("frozen")
484 || lower.contains("mmap")
485 || lower.contains("pretrained")
486 {
487 return (BuilderRole::Frozen, Confidence::Heuristic);
488 }
489 if lower.contains("state") || lower.contains("running") {
490 return (BuilderRole::State, Confidence::Heuristic);
491 }
492 (BuilderRole::Unknown, Confidence::Unknown)
493}
494
495fn discover_composition_edges(krate: &Crate, model: &mut ModelIr) {
496 let component_by_type: HashMap<String, StableId> = model
497 .components
498 .iter()
499 .flat_map(|component| {
500 [
501 (component.name.clone(), component.id.clone()),
502 (component.qualified_name.clone(), component.id.clone()),
503 ]
504 })
505 .collect();
506
507 let mut seen = HashSet::new();
508 for component in model.components.clone() {
509 let Some(def) = krate
510 .struct_candidates(&component.name)
511 .into_iter()
512 .find(|def| def.qualified_name == component.qualified_name)
513 else {
514 continue;
515 };
516 for field in &def.fields {
517 let child = unique_qualified_struct(krate, &field.ty.base)
518 .and_then(|qualified| component_by_type.get(&qualified).cloned())
519 .or_else(|| component_by_type.get(&field.ty.base).cloned());
520 let Some(child) = child else {
521 continue;
522 };
523 if child == component.id {
524 continue;
525 }
526 let key = (component.id.clone(), child.clone(), field.name.clone());
527 if !seen.insert(key.clone()) {
528 continue;
529 }
530 model.architecture_edges.push(ArchitectureEdge {
531 id: StableId::new(
532 "composition-edge",
533 [
534 component.id.0.as_str(),
535 child.0.as_str(),
536 field.name.as_str(),
537 ],
538 ),
539 from: component.id.clone(),
540 to: child,
541 via_function: component.constructor.clone(),
542 source: krate.file_label(field.span),
543 evidence: vec![Evidence {
544 kind: EvidenceKind::Source,
545 confidence: Confidence::Heuristic,
546 source: Some(krate.file_label(field.span)),
547 detail: format!(
548 "struct field `{}` embeds component type `{}`",
549 field.name, field.ty.base
550 ),
551 }],
552 });
553 }
554 }
555}
556
557fn discover_assembly_sites(krate: &Crate, model: &mut ModelIr) {
558 let constructor_functions: HashMap<StableId, &Function> = model
559 .functions
560 .iter()
561 .map(|candidate| (candidate.id.clone(), candidate))
562 .collect();
563 let specs: Vec<ConstructorSpec> = model
564 .components
565 .iter()
566 .filter_map(|component| {
567 let constructor = constructor_functions.get(&component.constructor)?;
568 let builders = constructor
569 .parameters
570 .iter()
571 .enumerate()
572 .filter(|(_, parameter)| parameter.type_name.contains("VarBuilder"))
573 .zip(component.builders.iter())
574 .map(|((index, _), builder)| (index, builder.name.clone()))
575 .collect();
576 Some(ConstructorSpec {
577 component: component.id.clone(),
578 owner: component.name.clone(),
579 builders,
580 })
581 })
582 .collect();
583 if specs.is_empty() {
584 return;
585 }
586
587 for func in krate.all_functions().chain(krate.all_methods()) {
588 if !is_production_source(krate, func) {
589 continue;
590 }
591 let function_id = function_id(func);
592 let mut collector = AssemblyCollector {
593 specs: &specs,
594 function_id: function_id.clone(),
595 function_name: func.qualified_name.clone(),
596 source: krate.file_label(func.span),
597 varmap_checkpoints: HashMap::new(),
598 vb_bindings: HashMap::new(),
599 sites: Vec::new(),
600 };
601 collector.visit_block(&func.block);
602 for site in &mut collector.sites {
603 if site.checkpoint_load.is_none() {
604 site.checkpoint_load = site
605 .varmap
606 .as_ref()
607 .and_then(|varmap| collector.varmap_checkpoints.get(varmap).cloned());
608 }
609 }
610 model.assembly_sites.extend(collector.sites);
611 }
612}
613
614struct AssemblyCollector<'a> {
615 specs: &'a [ConstructorSpec],
616 function_id: StableId,
617 function_name: String,
618 source: String,
619 varmap_checkpoints: HashMap<String, String>,
620 vb_bindings: HashMap<String, BuilderExpressionFacts>,
621 sites: Vec<AssemblySite>,
622}
623
624impl AssemblyCollector<'_> {
625 fn resolve_builder_facts(&self, expression: &syn::Expr) -> BuilderExpressionFacts {
626 match expression {
627 syn::Expr::Path(path) if path.path.segments.len() == 1 => self
628 .vb_bindings
629 .get(&path.path.segments[0].ident.to_string())
630 .cloned()
631 .unwrap_or_else(empty_builder_facts),
632 _ => builder_expression_facts(expression, &self.vb_bindings),
633 }
634 }
635}
636
637impl<'ast> Visit<'ast> for AssemblyCollector<'_> {
638 fn visit_local(&mut self, node: &'ast syn::Local) {
639 if let (Some(name), Some(init)) = (pat_ident(&node.pat), node.init.as_ref()) {
640 let facts = builder_expression_facts(&init.expr, &self.vb_bindings);
641 if facts.source_kind != BuilderSourceKind::Unknown
642 || !facts.prefix_chain.is_empty()
643 || facts.varmap.is_some()
644 {
645 self.vb_bindings.insert(name, facts);
646 }
647 }
648 visit::visit_local(self, node);
649 }
650
651 fn visit_expr_call(&mut self, node: &'ast syn::ExprCall) {
652 if let syn::Expr::Path(path) = &*node.func {
653 let leaf = path
654 .path
655 .segments
656 .last()
657 .map(|segment| segment.ident.to_string())
658 .unwrap_or_default();
659 if leaf == "load_varmap_checked" || leaf.ends_with("load_varmap_checked") {
660 if let (Some(varmap), Some(checkpoint)) = (
661 node.args.first().and_then(load_varmap_target),
662 node.args
663 .get(1)
664 .map(|arg| arg.to_token_stream().to_string()),
665 ) {
666 self.varmap_checkpoints.insert(varmap, checkpoint);
667 }
668 }
669 let segments: Vec<_> = path
670 .path
671 .segments
672 .iter()
673 .map(|segment| segment.ident.to_string())
674 .collect();
675 if let Some(owner) = segments.get(segments.len().saturating_sub(2)) {
676 for spec in self.specs.iter().filter(|spec| &spec.owner == owner) {
677 for (index, builder_root) in &spec.builders {
678 let Some(argument) = node.args.iter().nth(*index) else {
679 continue;
680 };
681 let facts = self.resolve_builder_facts(argument);
682 let varmap = facts.varmap.clone();
683 let checkpoint = varmap
684 .as_ref()
685 .and_then(|name| self.varmap_checkpoints.get(name).cloned());
686 self.sites.push(AssemblySite {
687 id: StableId::new(
688 "assembly-site",
689 [
690 self.function_id.0.as_str(),
691 spec.component.0.as_str(),
692 builder_root.as_str(),
693 &facts.prefix_chain.join("/"),
694 ],
695 ),
696 function: self.function_id.clone(),
697 function_name: self.function_name.clone(),
698 component: spec.component.clone(),
699 component_name: spec.owner.clone(),
700 builder_root: builder_root.clone(),
701 prefix_chain: facts.prefix_chain,
702 varmap,
703 source_kind: facts.source_kind,
704 role: facts.role,
705 checkpoint_load: checkpoint,
706 source: self.source.clone(),
707 evidence: vec![Evidence {
708 kind: EvidenceKind::Source,
709 confidence: Confidence::Heuristic,
710 source: Some(self.source.clone()),
711 detail: format!(
712 "`{owner}::new` wired through `{builder_root}` in `{}`",
713 self.function_name
714 ),
715 }],
716 });
717 }
718 }
719 }
720 }
721 visit::visit_expr_call(self, node);
722 }
723}
724
725#[derive(Clone)]
726struct BuilderExpressionFacts {
727 prefix_chain: Vec<String>,
728 varmap: Option<String>,
729 source_kind: BuilderSourceKind,
730 role: BuilderRole,
731}
732
733fn empty_builder_facts() -> BuilderExpressionFacts {
734 BuilderExpressionFacts {
735 prefix_chain: Vec::new(),
736 varmap: None,
737 source_kind: BuilderSourceKind::Unknown,
738 role: BuilderRole::Unknown,
739 }
740}
741
742fn builder_expression_facts(
743 expression: &syn::Expr,
744 vb_bindings: &HashMap<String, BuilderExpressionFacts>,
745) -> BuilderExpressionFacts {
746 match expression {
747 syn::Expr::Path(path) if path.path.segments.len() == 1 => vb_bindings
748 .get(&path.path.segments[0].ident.to_string())
749 .cloned()
750 .unwrap_or_else(empty_builder_facts),
751 syn::Expr::MethodCall(call) if call.method == "pp" => {
752 let mut facts = builder_expression_facts(&call.receiver, vb_bindings);
753 if let Some(syn::Expr::Lit(lit)) = call.args.first() {
754 if let syn::Lit::Str(text) = &lit.lit {
755 facts.prefix_chain.insert(0, text.value());
756 }
757 }
758 facts
759 }
760 syn::Expr::Call(call) => {
761 let leaf = call_leaf_name(&call.func);
762 match leaf.as_deref() {
763 Some("from_varmap") => BuilderExpressionFacts {
764 prefix_chain: Vec::new(),
765 varmap: call.args.first().and_then(expr_identifier).or_else(|| {
766 call.args.first().and_then(|arg| match arg {
767 syn::Expr::Reference(reference) => expr_identifier(&reference.expr),
768 _ => None,
769 })
770 }),
771 source_kind: BuilderSourceKind::VarMap,
772 role: BuilderRole::Trainable,
773 },
774 Some("from_mmaped_safetensors") | Some("from_buffered_safetensors") => {
775 BuilderExpressionFacts {
776 prefix_chain: Vec::new(),
777 varmap: None,
778 source_kind: if leaf.as_deref() == Some("from_mmaped_safetensors") {
779 BuilderSourceKind::MmapSafetensors
780 } else {
781 BuilderSourceKind::BufferedSafetensors
782 },
783 role: BuilderRole::Frozen,
784 }
785 }
786 Some("from_tensors") => BuilderExpressionFacts {
787 prefix_chain: Vec::new(),
788 varmap: None,
789 source_kind: BuilderSourceKind::FromTensors,
790 role: BuilderRole::Frozen,
791 },
792 _ => BuilderExpressionFacts {
793 prefix_chain: Vec::new(),
794 varmap: varmap_sources(expression, &HashMap::new())
795 .into_iter()
796 .next(),
797 source_kind: BuilderSourceKind::Unknown,
798 role: BuilderRole::Unknown,
799 },
800 }
801 }
802 syn::Expr::Reference(reference) => builder_expression_facts(&reference.expr, vb_bindings),
803 syn::Expr::Try(value) => builder_expression_facts(&value.expr, vb_bindings),
804 syn::Expr::Await(value) => builder_expression_facts(&value.base, vb_bindings),
805 syn::Expr::Unsafe(value) => value
806 .block
807 .stmts
808 .iter()
809 .find_map(|statement| match statement {
810 syn::Stmt::Expr(expression, _) => {
811 Some(builder_expression_facts(expression, vb_bindings))
812 }
813 _ => None,
814 })
815 .unwrap_or_else(empty_builder_facts),
816 syn::Expr::Block(block) => block
817 .block
818 .stmts
819 .iter()
820 .find_map(|statement| match statement {
821 syn::Stmt::Expr(expression, _) => {
822 Some(builder_expression_facts(expression, vb_bindings))
823 }
824 _ => None,
825 })
826 .unwrap_or_else(empty_builder_facts),
827 syn::Expr::Paren(paren) => builder_expression_facts(&paren.expr, vb_bindings),
828 syn::Expr::Group(group) => builder_expression_facts(&group.expr, vb_bindings),
829 _ => empty_builder_facts(),
830 }
831}
832
833fn call_leaf_name(expression: &syn::Expr) -> Option<String> {
834 match expression {
835 syn::Expr::Path(path) => path
836 .path
837 .segments
838 .last()
839 .map(|segment| segment.ident.to_string()),
840 _ => None,
841 }
842}
843
844fn load_varmap_target(expression: &syn::Expr) -> Option<String> {
845 match expression {
846 syn::Expr::Reference(reference) => expr_identifier(&reference.expr),
847 _ => expr_identifier(expression),
848 }
849}
850
851#[derive(Clone)]
852struct SubprocessInvocation {
853 wrapper_function: StableId,
854 subprocess_key: String,
855 cli_flags: Vec<String>,
856 launcher: String,
857 source: String,
858}
859
860fn discover_subprocess_pipeline(
861 krate: &Crate,
862 functions: &HashMap<String, StableId>,
863 model: &mut ModelIr,
864) {
865 let launchers = subprocess_launcher_functions(krate);
866 if launchers.is_empty() {
867 return;
868 }
869
870 let function_by_id: HashMap<StableId, Function> = model
871 .functions
872 .iter()
873 .map(|function| (function.id.clone(), function.clone()))
874 .collect();
875 let by_bare: HashMap<String, Vec<StableId>> =
876 model
877 .functions
878 .iter()
879 .fold(HashMap::new(), |mut grouped, function| {
880 grouped
881 .entry(function.name.clone())
882 .or_default()
883 .push(function.id.clone());
884 grouped
885 });
886
887 let mut invocations = Vec::new();
888 for func in krate.all_functions().chain(krate.all_methods()) {
889 if !is_production_source(krate, func) {
890 continue;
891 }
892 let wrapper_id = function_id(func);
893 let mut collector = SubprocessInvocationCollector {
894 launchers: &launchers,
895 wrapper_id,
896 source: krate.file_label(func.span),
897 invocations: Vec::new(),
898 };
899 collector.visit_block(&func.block);
900 invocations.extend(collector.invocations);
901 }
902
903 let orchestrator = discover_orchestrator_order(krate, functions, model, &by_bare);
904 if orchestrator.is_empty() && invocations.is_empty() {
905 return;
906 }
907
908 let mut seen_functions = model
909 .stages
910 .iter()
911 .map(|stage| stage.function.clone())
912 .collect::<HashSet<_>>();
913 let order_base = model.stages.len();
914
915 for (order, (function_id, orchestrator_name)) in orchestrator.into_iter().enumerate() {
916 if seen_functions.contains(&function_id) {
917 continue;
918 }
919 let Some(function) = function_by_id.get(&function_id) else {
920 continue;
921 };
922 let invocation = invocations
923 .iter()
924 .find(|item| item.wrapper_function == function_id);
925 push_subprocess_stage(
926 model,
927 function,
928 invocation,
929 order_base + order,
930 Some(orchestrator_name),
931 );
932 seen_functions.insert(function_id);
933 }
934
935 for invocation in invocations {
936 if seen_functions.contains(&invocation.wrapper_function) {
937 if let Some(stage) = model
938 .stages
939 .iter_mut()
940 .find(|stage| stage.function == invocation.wrapper_function)
941 {
942 stage.dispatch = StageDispatchKind::Subprocess;
943 if stage.subprocess_key.is_none() {
944 stage.subprocess_key = Some(invocation.subprocess_key.clone());
945 }
946 stage.name = stage
947 .subprocess_key
948 .clone()
949 .unwrap_or_else(|| stage.name.clone());
950 stage.cli_flags.extend(invocation.cli_flags.iter().cloned());
951 stage.cli_flags.sort();
952 stage.cli_flags.dedup();
953 stage.launcher = Some(invocation.launcher.clone());
954 stage.evidence.push(Evidence {
955 kind: EvidenceKind::Source,
956 confidence: Confidence::Heuristic,
957 source: Some(invocation.source.clone()),
958 detail: format!(
959 "subprocess relaunch via `{}` with stage key `{}`",
960 invocation.launcher, invocation.subprocess_key
961 ),
962 });
963 }
964 continue;
965 }
966 let Some(function) = function_by_id.get(&invocation.wrapper_function) else {
967 continue;
968 };
969 push_subprocess_stage(
970 model,
971 function,
972 Some(&invocation),
973 order_base + seen_functions.len(),
974 None,
975 );
976 seen_functions.insert(invocation.wrapper_function.clone());
977 }
978}
979
980fn push_subprocess_stage(
981 model: &mut ModelIr,
982 function: &Function,
983 invocation: Option<&SubprocessInvocation>,
984 order: usize,
985 orchestrator: Option<String>,
986) {
987 let name = invocation
988 .map(|item| item.subprocess_key.clone())
989 .unwrap_or_else(|| stage_display_name(function));
990 let id = StableId::new("subprocess-stage", [&function.qualified_name, &name]);
991 let mut evidence = vec![Evidence {
992 kind: EvidenceKind::Source,
993 confidence: Confidence::Heuristic,
994 source: Some(function.source.clone()),
995 detail: if let Some(orchestrator) = orchestrator.as_deref() {
996 format!(
997 "orchestrator `{orchestrator}` calls `{}` in source order",
998 function.name
999 )
1000 } else {
1001 format!(
1002 "wrapper `{}` relaunches the current executable",
1003 function.name
1004 )
1005 },
1006 }];
1007 if let Some(item) = invocation {
1008 evidence.push(Evidence {
1009 kind: EvidenceKind::Source,
1010 confidence: Confidence::Heuristic,
1011 source: Some(item.source.clone()),
1012 detail: format!(
1013 "subprocess relaunch via `{}` with stage key `{}`",
1014 item.launcher, item.subprocess_key
1015 ),
1016 });
1017 }
1018 model.stages.push(PipelineStage {
1019 id,
1020 name,
1021 kind: stage_kind(&function.name),
1022 function: function.id.clone(),
1023 order: Some(order),
1024 components: reachable_components(function, model),
1025 consumes: Vec::new(),
1026 produces: Vec::new(),
1027 depends_on: Vec::new(),
1028 source: function.source.clone(),
1029 evidence,
1030 dispatch: if invocation.is_some() {
1031 StageDispatchKind::Subprocess
1032 } else {
1033 StageDispatchKind::Inline
1034 },
1035 subprocess_key: invocation.map(|item| item.subprocess_key.clone()),
1036 cli_flags: invocation
1037 .map(|item| item.cli_flags.clone())
1038 .unwrap_or_default(),
1039 launcher: invocation.map(|item| item.launcher.clone()),
1040 orchestrator,
1041 });
1042}
1043
1044fn discover_orchestrator_order(
1045 krate: &Crate,
1046 functions: &HashMap<String, StableId>,
1047 model: &ModelIr,
1048 by_bare: &HashMap<String, Vec<StableId>>,
1049) -> Vec<(StableId, String)> {
1050 let mut best = Vec::new();
1051 for pipeline in krate
1052 .all_functions()
1053 .filter(|function| function.fn_name == "run_pipeline")
1054 {
1055 let mut collector = OrderedCallCollector::default();
1056 collector.visit_block(&pipeline.block);
1057 let orchestrator_name = pipeline.qualified_name.clone();
1058 let mut ordered = Vec::new();
1059 for call in collector.calls {
1060 let Some(callee) = resolve_call(&call, pipeline, functions, by_bare) else {
1061 continue;
1062 };
1063 let Some(function) = model
1064 .functions
1065 .iter()
1066 .find(|function| function.id == callee)
1067 else {
1068 continue;
1069 };
1070 if is_orchestrator_stage_call(&function.name) {
1071 ordered.push((callee, orchestrator_name.clone()));
1072 }
1073 }
1074 if ordered.len() > best.len() {
1075 best = ordered;
1076 }
1077 }
1078 best
1079}
1080
1081fn subprocess_launcher_functions(krate: &Crate) -> HashSet<String> {
1082 let mut launchers = HashSet::new();
1083 launchers.insert("run_stage_command".to_string());
1084 for func in krate.all_functions().chain(krate.all_methods()) {
1085 let text = func.block.to_token_stream().to_string();
1086 if text.contains("current_exe") && text.contains("Command") {
1087 launchers.insert(func.fn_name.clone());
1088 }
1089 }
1090 launchers
1091}
1092
1093struct SubprocessInvocationCollector<'a> {
1094 launchers: &'a HashSet<String>,
1095 wrapper_id: StableId,
1096 source: String,
1097 invocations: Vec<SubprocessInvocation>,
1098}
1099
1100impl<'ast> Visit<'ast> for SubprocessInvocationCollector<'_> {
1101 fn visit_expr_call(&mut self, node: &'ast syn::ExprCall) {
1102 if let Some(leaf) = call_leaf_name(&node.func) {
1103 if self.launchers.contains(&leaf) {
1104 if let Some(subprocess_key) = node.args.first().and_then(string_literal_expr) {
1105 let cli_flags = node
1106 .args
1107 .get(1)
1108 .map(extract_cli_flags_from_expr)
1109 .unwrap_or_default();
1110 self.invocations.push(SubprocessInvocation {
1111 wrapper_function: self.wrapper_id.clone(),
1112 subprocess_key,
1113 cli_flags,
1114 launcher: leaf.clone(),
1115 source: self.source.clone(),
1116 });
1117 }
1118 } else if leaf == "run_training_stage_with_oom_recovery" {
1119 if let Some(subprocess_key) = node.args.get(2).and_then(string_literal_expr) {
1120 let mut cli_flags = node
1121 .args
1122 .get(7)
1123 .map(extract_cli_flags_from_expr)
1124 .unwrap_or_default();
1125 if cli_flags.is_empty() {
1126 cli_flags = node
1127 .args
1128 .iter()
1129 .flat_map(extract_cli_flags_from_expr)
1130 .collect::<BTreeSet<_>>()
1131 .into_iter()
1132 .collect();
1133 }
1134 self.invocations.push(SubprocessInvocation {
1135 wrapper_function: self.wrapper_id.clone(),
1136 subprocess_key,
1137 cli_flags,
1138 launcher: "run_stage_command".to_string(),
1139 source: self.source.clone(),
1140 });
1141 }
1142 }
1143 }
1144 visit::visit_expr_call(self, node);
1145 }
1146}
1147
1148#[derive(Default)]
1149struct OrderedCallCollector {
1150 calls: Vec<String>,
1151}
1152
1153impl<'ast> Visit<'ast> for OrderedCallCollector {
1154 fn visit_expr_call(&mut self, node: &'ast syn::ExprCall) {
1155 if let syn::Expr::Path(path) = &*node.func {
1156 self.calls
1157 .push(path.path.to_token_stream().to_string().replace(' ', ""));
1158 }
1159 visit::visit_expr_call(self, node);
1160 }
1161
1162 fn visit_expr_method_call(&mut self, node: &'ast syn::ExprMethodCall) {
1163 self.calls.push(node.method.to_string());
1164 visit::visit_expr_method_call(self, node);
1165 }
1166}
1167
1168fn is_orchestrator_stage_call(name: &str) -> bool {
1169 is_pipeline_stage_call(name) || name.starts_with("preflight_")
1170}
1171
1172fn string_literal_expr(expression: &syn::Expr) -> Option<String> {
1173 match expression {
1174 syn::Expr::Lit(lit) => match &lit.lit {
1175 syn::Lit::Str(text) => Some(text.value()),
1176 _ => None,
1177 },
1178 syn::Expr::Reference(reference) => string_literal_expr(&reference.expr),
1179 syn::Expr::Group(group) => string_literal_expr(&group.expr),
1180 syn::Expr::Paren(paren) => string_literal_expr(&paren.expr),
1181 _ => None,
1182 }
1183}
1184
1185fn extract_cli_flags_from_expr(expression: &syn::Expr) -> Vec<String> {
1186 match expression {
1187 syn::Expr::Closure(closure) => extract_cli_flags_from_expr(&closure.body),
1188 syn::Expr::Block(block) => {
1189 let mut flags = block
1190 .block
1191 .stmts
1192 .iter()
1193 .flat_map(|statement| match statement {
1194 syn::Stmt::Expr(expr, _) => extract_cli_flags_from_expr(expr),
1195 syn::Stmt::Macro(macro_stmt) => {
1196 flags_from_tokens(macro_stmt.mac.tokens.clone())
1197 }
1198 _ => Vec::new(),
1199 })
1200 .collect::<Vec<_>>();
1201 flags.sort();
1202 flags.dedup();
1203 flags
1204 }
1205 syn::Expr::Macro(expr_macro) => flags_from_tokens(expr_macro.mac.tokens.clone()),
1206 _ => extract_cli_flags(expression),
1207 }
1208}
1209
1210fn flags_from_tokens(tokens: proc_macro2::TokenStream) -> Vec<String> {
1211 if let Ok(expression) = syn::parse2::<syn::Expr>(tokens.clone()) {
1212 let flags = extract_cli_flags(&expression);
1213 if !flags.is_empty() {
1214 return flags;
1215 }
1216 }
1217 let mut flags = tokens
1218 .to_string()
1219 .split(|character: char| {
1220 !(character.is_ascii_alphanumeric() || character == '-' || character == '_')
1221 })
1222 .map(|token| token.trim_matches('"'))
1223 .filter(|token| token.starts_with("--") && token.len() > 2)
1224 .map(str::to_string)
1225 .collect::<Vec<_>>();
1226 flags.sort();
1227 flags.dedup();
1228 flags
1229}
1230
1231fn extract_cli_flags(expression: &syn::Expr) -> Vec<String> {
1232 let mut flags = string_literals(expression)
1233 .into_iter()
1234 .filter(|value| value.starts_with("--") && value.len() > 2)
1235 .collect::<Vec<_>>();
1236 flags.sort();
1237 flags.dedup();
1238 flags
1239}
1240
1241struct ComponentFlowCollector<'a> {
1242 component_by_type: &'a HashMap<String, StableId>,
1243 owner_fields: HashMap<String, StableId>,
1244 locals: HashMap<String, StableId>,
1245 sequence: Vec<StableId>,
1246}
1247
1248impl<'ast> Visit<'ast> for ComponentFlowCollector<'_> {
1249 fn visit_local(&mut self, node: &'ast syn::Local) {
1250 if let (Some(name), Some(init)) = (pat_ident(&node.pat), node.init.as_ref()) {
1251 if let Some(component) = constructor_component(&init.expr, self.component_by_type)
1252 .or_else(|| receiver_component(&init.expr, &self.locals, &self.owner_fields))
1253 {
1254 self.locals.insert(name, component);
1255 }
1256 }
1257 visit::visit_local(self, node);
1258 }
1259
1260 fn visit_expr_method_call(&mut self, node: &'ast syn::ExprMethodCall) {
1261 if is_model_entry_name(&node.method.to_string()) {
1262 if let Some(component) =
1263 receiver_component(&node.receiver, &self.locals, &self.owner_fields)
1264 {
1265 if self.sequence.last() != Some(&component) {
1266 self.sequence.push(component);
1267 }
1268 }
1269 }
1270 visit::visit_expr_method_call(self, node);
1271 }
1272}
1273
1274fn pat_ident(pattern: &syn::Pat) -> Option<String> {
1275 match pattern {
1276 syn::Pat::Ident(ident) => Some(ident.ident.to_string()),
1277 syn::Pat::Type(typed) => pat_ident(&typed.pat),
1278 _ => None,
1279 }
1280}
1281
1282fn constructor_component(
1283 expression: &syn::Expr,
1284 components: &HashMap<String, StableId>,
1285) -> Option<StableId> {
1286 match expression {
1287 syn::Expr::Call(call) => {
1288 let syn::Expr::Path(path) = &*call.func else {
1289 return None;
1290 };
1291 let segments: Vec<_> = path
1292 .path
1293 .segments
1294 .iter()
1295 .map(|segment| segment.ident.to_string())
1296 .collect();
1297 let owner = segments
1298 .get(segments.len().checked_sub(2)?)
1299 .map(String::as_str)?;
1300 components.get(owner).cloned()
1301 }
1302 syn::Expr::Try(value) => constructor_component(&value.expr, components),
1303 syn::Expr::Await(value) => constructor_component(&value.base, components),
1304 syn::Expr::Paren(value) => constructor_component(&value.expr, components),
1305 syn::Expr::Group(value) => constructor_component(&value.expr, components),
1306 _ => None,
1307 }
1308}
1309
1310fn receiver_component(
1311 expression: &syn::Expr,
1312 locals: &HashMap<String, StableId>,
1313 owner_fields: &HashMap<String, StableId>,
1314) -> Option<StableId> {
1315 match expression {
1316 syn::Expr::Path(path) if path.path.segments.len() == 1 => locals
1317 .get(&path.path.segments[0].ident.to_string())
1318 .cloned(),
1319 syn::Expr::Field(field)
1320 if matches!(
1321 &*field.base,
1322 syn::Expr::Path(path)
1323 if path.path.segments.len() == 1 && path.path.segments[0].ident == "self"
1324 ) =>
1325 {
1326 let syn::Member::Named(name) = &field.member else {
1327 return None;
1328 };
1329 owner_fields.get(&name.to_string()).cloned()
1330 }
1331 syn::Expr::Reference(reference) => {
1332 receiver_component(&reference.expr, locals, owner_fields)
1333 }
1334 syn::Expr::Try(value) => receiver_component(&value.expr, locals, owner_fields),
1335 syn::Expr::Await(value) => receiver_component(&value.base, locals, owner_fields),
1336 syn::Expr::MethodCall(call) => receiver_component(&call.receiver, locals, owner_fields),
1337 syn::Expr::Paren(paren) => receiver_component(&paren.expr, locals, owner_fields),
1338 syn::Expr::Group(group) => receiver_component(&group.expr, locals, owner_fields),
1339 _ => None,
1340 }
1341}
1342
1343fn type_base_from_text(text: &str) -> Option<String> {
1344 syn::parse_str::<syn::Type>(text)
1345 .ok()
1346 .and_then(|type_name| load::type_base_name(&type_name))
1347}
1348
1349fn discover_components(
1350 krate: &Crate,
1351 cargo: Option<&CargoContext>,
1352 selected_root: Option<&str>,
1353 heuristic_architecture: bool,
1354 model: &mut ModelIr,
1355) {
1356 let exported: HashSet<String> = krate
1357 .public_reexports
1358 .iter()
1359 .map(|item| item.name.clone())
1360 .collect();
1361 let mut candidates = Vec::new();
1362 for def in krate.all_structs() {
1363 if let Some(selected) = selected_root {
1364 if def.name != selected && def.qualified_name != selected {
1365 continue;
1366 }
1367 }
1368 if cargo.and_then(|context| {
1369 crate::cargo_context::cfg_predicates_active(&def.cfg_predicates, &context.cfgs)
1370 }) == Some(false)
1371 {
1372 continue;
1373 }
1374 let methods = krate.method_candidates(&def.qualified_name, "new");
1375 let mut constructors = if methods.is_empty() {
1376 krate.method_candidates(&def.qualified_name, "load")
1377 } else {
1378 methods
1379 };
1380 if constructors.is_empty() {
1381 constructors = krate
1382 .all_methods()
1383 .filter(|method| {
1384 method.qualified_type_name == def.qualified_name
1385 && method.trait_name.is_none()
1386 && !method.vb_params.is_empty()
1387 && return_mentions_type(&method.return_type, &def.name)
1388 })
1389 .collect();
1390 constructors.sort_by(|a, b| a.qualified_name.cmp(&b.qualified_name));
1391 }
1392 let Some(ctor) = constructors.into_iter().find(|method| {
1393 !method.vb_params.is_empty()
1394 && cargo.and_then(|context| {
1395 crate::cargo_context::cfg_predicates_active(
1396 &method.cfg_predicates,
1397 &context.cfgs,
1398 )
1399 }) != Some(false)
1400 }) else {
1401 continue;
1402 };
1403 let is_api_boundary =
1404 exported.contains(&def.name) || def.module_path.is_empty() && def.visibility == "pub";
1405 let is_varbuilder_component =
1408 def.visibility == "pub" && !ctor.vb_params.is_empty();
1409 if selected_root.is_some() || is_api_boundary || is_varbuilder_component {
1410 candidates.push((def, ctor));
1411 }
1412 }
1413 candidates.sort_by(|(a, _), (b, _)| a.qualified_name.cmp(&b.qualified_name));
1414 candidates.dedup_by(|(a, _), (b, _)| a.qualified_name == b.qualified_name);
1415
1416 for (def, ctor) in candidates {
1417 if selected_root.is_some() {
1418 for function in &mut model.functions {
1419 if function.owner_type.as_deref() == Some(def.qualified_name.as_str())
1420 && (function
1421 .parameters
1422 .iter()
1423 .any(|parameter| is_tensor_type(¶meter.type_name))
1424 || function.return_type.as_deref().is_some_and(is_tensor_type))
1425 {
1426 function.is_entrypoint = true;
1427 }
1428 }
1429 }
1430 let candle_version = cargo.and_then(|context| {
1431 op_semantics::matched_candle_version(
1432 context
1433 .candle_versions
1434 .get("candle-core")
1435 .map(String::as_str),
1436 context.candle_versions.get("candle-nn").map(String::as_str),
1437 )
1438 });
1439 add_component(
1440 krate,
1441 model,
1442 def,
1443 ctor,
1444 candle_version,
1445 heuristic_architecture,
1446 );
1447 }
1448}
1449
1450fn return_mentions_type(return_type: &str, type_name: &str) -> bool {
1451 return_type
1452 .split(|character: char| !character.is_alphanumeric() && character != '_')
1453 .any(|part| part == "Self" || part == type_name)
1454}
1455
1456fn add_component(
1457 krate: &Crate,
1458 model: &mut ModelIr,
1459 def: &StructDef,
1460 ctor: &ImplFn,
1461 candle_version: Option<&str>,
1462 heuristic_architecture: bool,
1463) {
1464 let component_id = StableId::new("component", [&def.qualified_name]);
1465 let constructor_id = function_id(ctor);
1466 let mut component = Component {
1467 id: component_id.clone(),
1468 name: def.name.clone(),
1469 qualified_name: def.qualified_name.clone(),
1470 source: krate.file_label(def.span),
1471 constructor: constructor_id,
1472 builders: ctor
1473 .vb_params
1474 .iter()
1475 .filter_map(|index| ctor.params.get(*index))
1476 .map(|name| {
1477 let (role, confidence) = if heuristic_architecture {
1478 infer_builder_role(name)
1479 } else {
1480 (BuilderRole::Unknown, Confidence::Unknown)
1481 };
1482 let mut evidence = vec![source_evidence(
1483 krate.file_label(ctor.span),
1484 format!("constructor parameter `{name}` has VarBuilder type"),
1485 )];
1486 if role != BuilderRole::Unknown {
1487 evidence.push(Evidence {
1488 kind: EvidenceKind::Inferred,
1489 confidence,
1490 source: Some(krate.file_label(ctor.span)),
1491 detail: format!("builder name `{name}` suggests role `{role:?}`"),
1492 });
1493 }
1494 BuilderNamespace {
1495 name: name.clone(),
1496 role,
1497 evidence,
1498 }
1499 })
1500 .collect(),
1501 modules: Vec::new(),
1502 parameters: Vec::new(),
1503 entrypoints: model
1504 .functions
1505 .iter()
1506 .filter(|function| {
1507 function
1508 .owner_type
1509 .as_deref()
1510 .is_some_and(|owner| owner == def.qualified_name)
1511 && function.is_entrypoint
1512 })
1513 .map(|function| function.id.clone())
1514 .collect(),
1515 evidence: vec![source_evidence(
1516 krate.file_label(def.span),
1517 "public model API type with a VarBuilder constructor",
1518 )],
1519 };
1520
1521 match Extractor::for_candle_version(krate, candle_version)
1522 .run(&def.qualified_name, Some(&ctor.fn_name))
1523 {
1524 Ok(structure) => {
1525 let mut module_ids = HashMap::new();
1526 for instance in &structure.instances {
1527 let module_def = structure.def(instance.def);
1528 let id = StableId::new(
1529 "module",
1530 [
1531 component_id.0.as_str(),
1532 instance.root.as_str(),
1533 instance.prefix.to_string().as_str(),
1534 module_def.name.as_str(),
1535 instance.id.0.to_string().as_str(),
1536 ],
1537 );
1538 module_ids.insert(instance.id, id.clone());
1539 component.modules.push(id.clone());
1540 model.modules.push(Module {
1541 id,
1542 component: component_id.clone(),
1543 parent: instance
1544 .parent
1545 .and_then(|parent| module_ids.get(&parent).cloned()),
1546 type_name: module_def.name.clone(),
1547 qualified_type: unique_qualified_struct(krate, &module_def.name),
1548 field: instance.via_field.clone(),
1549 builder_root: instance.root.clone(),
1550 prefix: instance.prefix.to_string(),
1551 repeat: instance
1552 .repeat
1553 .as_ref()
1554 .map(|repeat| format!("{} in 0..{}", repeat.var, repeat.bound)),
1555 source: krate.file_label(instance.origin),
1556 confidence: certainty_confidence(&instance.certainty),
1557 });
1558 }
1559 for param in &structure.params {
1560 let site = structure.site(param.site);
1561 let Some(module) = module_ids.get(¶m.owner).cloned() else {
1562 continue;
1563 };
1564 let key = param.key.to_string();
1565 let id = StableId::new(
1566 "parameter",
1567 [component_id.0.as_str(), param.root.as_str(), key.as_str()],
1568 );
1569 component.parameters.push(id.clone());
1570 let (checkpoint_shape, checkpoint_dtype) = match ¶m.checkpoint {
1571 CheckpointMatch::Found { shape, dtype, .. } => {
1572 (Some(shape.clone()), Some(dtype.clone()))
1573 }
1574 _ => (None, None),
1575 };
1576 model.parameters.push(Parameter {
1577 id,
1578 component: component_id.clone(),
1579 module,
1580 key,
1581 builder_root: param.root.clone(),
1582 role: match site.kind {
1583 crate::known::ParamKind::RunningMean
1584 | crate::known::ParamKind::RunningVar => ParameterRole::RunningState,
1585 _ => ParameterRole::Unknown,
1586 },
1587 kind: acquisition_label(&site.acquisition),
1588 symbolic_shape: site.shape.clone(),
1589 checkpoint_shape,
1590 checkpoint_dtype,
1591 source: krate.file_label(site.span),
1592 uses: Vec::new(),
1593 optimizer_memberships: Vec::new(),
1594 evidence: vec![Evidence {
1595 kind: EvidenceKind::Source,
1596 confidence: certainty_confidence(¶m.certainty),
1597 source: Some(krate.file_label(site.span)),
1598 detail: format!(
1599 "parameter registered through {}",
1600 acquisition_label(&site.acquisition)
1601 ),
1602 }],
1603 });
1604 }
1605 for diagnostic in structure.diagnostics {
1606 push_finding(
1607 model,
1608 "structure-unresolved",
1609 FindingSeverity::Warning,
1610 Confidence::Proven,
1611 diagnostic.message,
1612 Some(krate.file_label(diagnostic.span)),
1613 vec![component_id.clone()],
1614 );
1615 }
1616 }
1617 Err(error) => push_finding(
1618 model,
1619 "component-expansion",
1620 FindingSeverity::Warning,
1621 Confidence::Proven,
1622 format!("could not expand {}: {error:#}", def.qualified_name),
1623 Some(krate.file_label(ctor.span)),
1624 vec![component_id.clone()],
1625 ),
1626 }
1627 model.components.push(component);
1628}
1629
1630fn add_contracts(krate: &Crate, model: &mut ModelIr) {
1631 let analysis = crate::contracts::analyze(krate);
1632 let owners: HashMap<String, StableId> = model
1633 .functions
1634 .iter()
1635 .map(|function| (function.qualified_name.clone(), function.id.clone()))
1636 .collect();
1637 let function_indices: HashMap<StableId, usize> = model
1638 .functions
1639 .iter()
1640 .enumerate()
1641 .map(|(index, function)| (function.id.clone(), index))
1642 .collect();
1643
1644 for contracts in analysis.functions {
1645 let Some(owner) = owners.get(&contracts.qualified_name).cloned() else {
1646 continue;
1647 };
1648 for mut tensor in contracts.tensors {
1649 tensor.owner_function = owner.clone();
1650 tensor.id = StableId::new("tensor", [owner.0.as_str(), tensor.name.as_str()]);
1651 if tensor.dtype.eq_ignore_ascii_case("unknown") {
1652 tensor.dtype = "Unknown".to_string();
1653 }
1654 let id = tensor.id.clone();
1655 let role = tensor.role.clone();
1656 if let Some(&index) = function_indices.get(&owner) {
1657 if matches!(role, TensorRole::Input) {
1658 model.functions[index].tensor_inputs.push(id.clone());
1659 }
1660 if matches!(role, TensorRole::Output | TensorRole::Loss) {
1661 model.functions[index].tensor_outputs.push(id.clone());
1662 }
1663 }
1664 model.tensors.push(tensor);
1665 }
1666 }
1667}
1668
1669fn discover_pipeline_and_artifacts(
1670 krate: &Crate,
1671 functions: &HashMap<String, StableId>,
1672 model: &mut ModelIr,
1673) {
1674 let function_by_id: HashMap<StableId, &Function> = model
1675 .functions
1676 .iter()
1677 .map(|function| (function.id.clone(), function))
1678 .collect();
1679 let by_bare: HashMap<String, Vec<StableId>> =
1680 model
1681 .functions
1682 .iter()
1683 .fold(HashMap::new(), |mut grouped, function| {
1684 grouped
1685 .entry(function.name.clone())
1686 .or_default()
1687 .push(function.id.clone());
1688 grouped
1689 });
1690 let mut ordered_stage_functions: Vec<(StableId, Option<String>)> = Vec::new();
1691 for pipeline in krate
1692 .all_functions()
1693 .filter(|function| function.fn_name == "run_pipeline")
1694 {
1695 let mut collector = CallCollector::default();
1696 collector.visit_block(&pipeline.block);
1697 for call in collector.calls {
1698 let Some(callee) = resolve_call(&call, pipeline, functions, &by_bare) else {
1699 continue;
1700 };
1701 let Some(target) = function_by_id.get(&callee) else {
1702 continue;
1703 };
1704 if !is_pipeline_stage_call(&target.name) {
1705 continue;
1706 }
1707 let variants = stage_variants(krate, &callee);
1708 if variants.is_empty() {
1709 ordered_stage_functions.push((callee, None));
1710 } else {
1711 ordered_stage_functions.extend(
1712 variants
1713 .into_iter()
1714 .map(|variant| (callee.clone(), Some(variant))),
1715 );
1716 }
1717 }
1718 }
1719 if ordered_stage_functions.is_empty() {
1720 ordered_stage_functions.extend(
1721 model
1722 .functions
1723 .iter()
1724 .filter(|function| is_stage_entry_name(&function.name))
1725 .map(|function| (function.id.clone(), None)),
1726 );
1727 }
1728 ordered_stage_functions.dedup();
1729
1730 let mut stage_by_function = HashMap::new();
1731 for (order, (function_id, variant)) in ordered_stage_functions.into_iter().enumerate() {
1732 let Some(function) = function_by_id.get(&function_id) else {
1733 continue;
1734 };
1735 let name = variant.unwrap_or_else(|| stage_display_name(function));
1736 let id = StableId::new("stage", [&function.qualified_name, &name]);
1737 stage_by_function
1738 .entry(function_id.clone())
1739 .or_insert_with(|| id.clone());
1740 let related_components = reachable_components(function, model);
1741 model.stages.push(PipelineStage {
1742 id,
1743 name,
1744 kind: stage_kind(&function.name),
1745 function: function_id,
1746 order: Some(order),
1747 components: related_components,
1748 consumes: Vec::new(),
1749 produces: Vec::new(),
1750 depends_on: Vec::new(),
1751 source: function.source.clone(),
1752 evidence: vec![heuristic_source_evidence(
1753 function.source.clone(),
1754 "stage entrypoint discovered from pipeline call order",
1755 )],
1756 dispatch: StageDispatchKind::Unknown,
1757 subprocess_key: None,
1758 cli_flags: Vec::new(),
1759 launcher: None,
1760 orchestrator: None,
1761 });
1762 }
1763
1764 let mut facts = Vec::new();
1765 for func in krate.all_functions().chain(krate.all_methods()) {
1766 if !is_production_source(krate, func) {
1767 continue;
1768 }
1769 let mut visitor = ArtifactVisitor::default();
1770 visitor.visit_block(&func.block);
1771 let owner = function_id(func);
1772 for observed in visitor.observed {
1773 facts.push((owner.clone(), krate.file_label(func.span), observed));
1774 }
1775 }
1776 facts.sort_by(|a, b| a.2.path_expr.cmp(&b.2.path_expr));
1777
1778 let mut artifact_index: HashMap<String, usize> = HashMap::new();
1779 for (function_id, source, observed) in facts {
1780 let identity = artifact_identity(&observed.path_expr);
1781 let index = match artifact_index.get(&identity).copied() {
1782 Some(index) => index,
1783 None => {
1784 let id = StableId::new("artifact", [&identity]);
1785 let index = model.artifacts.len();
1786 model.artifacts.push(Artifact {
1787 id,
1788 name: observed.label.clone(),
1789 kind: artifact_kind(&observed.label),
1790 path_expr: observed.path_expr.clone(),
1791 produced_by: Vec::new(),
1792 consumed_by: Vec::new(),
1793 source: source.clone(),
1794 evidence: vec![heuristic_source_evidence(
1795 source.clone(),
1796 format!("path passed to `{}`", observed.operation),
1797 )],
1798 });
1799 artifact_index.insert(identity, index);
1800 index
1801 }
1802 };
1803 let stage = stage_for_function(&function_id, &stage_by_function, model);
1804 if let Some(stage_id) = stage {
1805 let artifact = &mut model.artifacts[index];
1806 if observed.produced {
1807 artifact.produced_by.push(stage_id.clone());
1808 } else {
1809 artifact.consumed_by.push(stage_id.clone());
1810 }
1811 }
1812 }
1813
1814 for artifact in &mut model.artifacts {
1815 artifact.produced_by.sort();
1816 artifact.produced_by.dedup();
1817 artifact.consumed_by.sort();
1818 artifact.consumed_by.dedup();
1819 }
1820 infer_artifact_stage_links(model);
1821 let stage_orders: HashMap<StableId, usize> = model
1822 .stages
1823 .iter()
1824 .map(|stage| (stage.id.clone(), stage.order.unwrap_or(usize::MAX)))
1825 .collect();
1826 for stage in &mut model.stages {
1827 let stage_order = stage.order.unwrap_or(usize::MAX);
1828 for artifact in &model.artifacts {
1829 if artifact.produced_by.contains(&stage.id) {
1830 stage.produces.push(artifact.id.clone());
1831 }
1832 if artifact.consumed_by.contains(&stage.id) {
1833 stage.consumes.push(artifact.id.clone());
1834 stage.depends_on.extend(
1835 artifact
1836 .produced_by
1837 .iter()
1838 .filter(|producer| {
1839 **producer != stage.id
1840 && stage_orders
1841 .get(*producer)
1842 .is_some_and(|producer_order| *producer_order < stage_order)
1843 })
1844 .cloned(),
1845 );
1846 }
1847 }
1848 stage.consumes.sort();
1849 stage.consumes.dedup();
1850 stage.produces.sort();
1851 stage.produces.dedup();
1852 stage.depends_on.sort();
1853 stage.depends_on.dedup();
1854 }
1855
1856 let _ = functions;
1857}
1858
1859fn is_production_source(krate: &Crate, function: &ImplFn) -> bool {
1860 !function
1861 .module_path
1862 .split("::")
1863 .any(|segment| segment == "tests" || segment == "test")
1864 && krate.files.get(function.span.file).is_some_and(|file| {
1865 let rel = file.rel.replace('\\', "/");
1866 rel.starts_with("src/") || !rel.contains('/')
1867 })
1868}
1869
1870#[derive(Clone)]
1871struct ConstructorSpec {
1872 component: StableId,
1873 owner: String,
1874 builders: Vec<(usize, String)>,
1875}
1876
1877fn component_varmap_bindings(
1878 _krate: &Crate,
1879 function: &ImplFn,
1880 model: &ModelIr,
1881) -> HashMap<String, Vec<(StableId, String)>> {
1882 let constructor_functions: HashMap<StableId, &Function> = model
1883 .functions
1884 .iter()
1885 .map(|candidate| (candidate.id.clone(), candidate))
1886 .collect();
1887 let specs: Vec<ConstructorSpec> = model
1888 .components
1889 .iter()
1890 .filter_map(|component| {
1891 let constructor = constructor_functions.get(&component.constructor)?;
1892 let builders = constructor
1893 .parameters
1894 .iter()
1895 .enumerate()
1896 .filter(|(_, parameter)| parameter.type_name.contains("VarBuilder"))
1897 .zip(component.builders.iter())
1898 .map(|((index, _), builder)| (index, builder.name.clone()))
1899 .collect();
1900 Some(ConstructorSpec {
1901 component: component.id.clone(),
1902 owner: component.name.clone(),
1903 builders,
1904 })
1905 })
1906 .collect();
1907
1908 let mut builder_collector = BuilderSourceCollector::default();
1909 builder_collector.visit_block(&function.block);
1910 let mut collector = ConstructorBindingCollector {
1911 specs: &specs,
1912 builder_sources: &builder_collector.sources,
1913 bindings: HashMap::new(),
1914 };
1915 collector.visit_block(&function.block);
1916 for bindings in collector.bindings.values_mut() {
1917 bindings.sort();
1918 bindings.dedup();
1919 }
1920 collector.bindings
1921}
1922
1923#[derive(Default)]
1924struct BuilderSourceCollector {
1925 sources: HashMap<String, BTreeSet<String>>,
1926}
1927
1928impl<'ast> Visit<'ast> for BuilderSourceCollector {
1929 fn visit_local(&mut self, node: &'ast syn::Local) {
1930 if let (Some(name), Some(init)) = (pat_ident(&node.pat), node.init.as_ref()) {
1931 let varmaps = varmap_sources(&init.expr, &self.sources);
1932 if !varmaps.is_empty() {
1933 self.sources.entry(name).or_default().extend(varmaps);
1934 }
1935 }
1936 visit::visit_local(self, node);
1937 }
1938}
1939
1940struct ConstructorBindingCollector<'a> {
1941 specs: &'a [ConstructorSpec],
1942 builder_sources: &'a HashMap<String, BTreeSet<String>>,
1943 bindings: HashMap<String, Vec<(StableId, String)>>,
1944}
1945
1946impl<'ast> Visit<'ast> for ConstructorBindingCollector<'_> {
1947 fn visit_expr_call(&mut self, node: &'ast syn::ExprCall) {
1948 if let syn::Expr::Path(path) = &*node.func {
1949 let segments: Vec<_> = path
1950 .path
1951 .segments
1952 .iter()
1953 .map(|segment| segment.ident.to_string())
1954 .collect();
1955 if let Some(owner) = segments.get(segments.len().saturating_sub(2)) {
1956 for spec in self.specs.iter().filter(|spec| &spec.owner == owner) {
1957 for (index, root) in &spec.builders {
1958 let Some(argument) = node.args.iter().nth(*index) else {
1959 continue;
1960 };
1961 for varmap in varmap_sources(argument, self.builder_sources) {
1962 self.bindings
1963 .entry(varmap)
1964 .or_default()
1965 .push((spec.component.clone(), root.clone()));
1966 }
1967 }
1968 }
1969 }
1970 }
1971 visit::visit_expr_call(self, node);
1972 }
1973}
1974
1975fn varmap_sources(
1976 expression: &syn::Expr,
1977 builder_sources: &HashMap<String, BTreeSet<String>>,
1978) -> BTreeSet<String> {
1979 match expression {
1980 syn::Expr::Path(path) if path.path.segments.len() == 1 => {
1981 let name = path.path.segments[0].ident.to_string();
1982 builder_sources.get(&name).cloned().unwrap_or_default()
1983 }
1984 syn::Expr::Call(call) => {
1985 let is_from_varmap = matches!(
1986 &*call.func,
1987 syn::Expr::Path(path)
1988 if path.path.segments.last().is_some_and(|segment| segment.ident == "from_varmap")
1989 );
1990 if is_from_varmap {
1991 call.args
1992 .first()
1993 .and_then(expr_identifier)
1994 .into_iter()
1995 .collect()
1996 } else {
1997 BTreeSet::new()
1998 }
1999 }
2000 syn::Expr::MethodCall(call) => varmap_sources(&call.receiver, builder_sources),
2001 syn::Expr::Reference(reference) => varmap_sources(&reference.expr, builder_sources),
2002 syn::Expr::Try(value) => varmap_sources(&value.expr, builder_sources),
2003 syn::Expr::Paren(paren) => varmap_sources(&paren.expr, builder_sources),
2004 syn::Expr::Group(group) => varmap_sources(&group.expr, builder_sources),
2005 _ => BTreeSet::new(),
2006 }
2007}
2008
2009fn discover_optimizers(krate: &Crate, model: &mut ModelIr) {
2010 let stage_functions: Vec<(StableId, StableId, String)> = model
2011 .stages
2012 .iter()
2013 .map(|stage| {
2014 (
2015 stage.id.clone(),
2016 stage.function.clone(),
2017 stage.source.clone(),
2018 )
2019 })
2020 .collect();
2021 for func in krate.all_functions().chain(krate.all_methods()) {
2022 if !is_production_source(krate, func) {
2023 continue;
2024 }
2025 let text = func.block.to_token_stream().to_string();
2026 if !text.contains("all_vars") && !text.contains("named_train_vars") {
2027 continue;
2028 }
2029 let mut visitor = OptimizerVisitor::default();
2030 visitor.visit_block(&func.block);
2031 visitor.excludes.sort();
2032 visitor.excludes.dedup();
2033 visitor.includes.sort();
2034 visitor.includes.dedup();
2035 if visitor.optimizer.is_none() {
2036 continue;
2037 }
2038 let component_bindings = component_varmap_bindings(krate, func, model);
2039 let function_id = function_id(func);
2040 let exact_stage = stage_functions
2041 .iter()
2042 .find(|(_, stage_function, _)| stage_function == &function_id)
2043 .map(|(stage, _, _)| stage.clone())
2044 .or_else(|| {
2045 stage_functions
2046 .iter()
2047 .find(|(_, _, source)| {
2048 source.split(':').next()
2049 == Some(
2050 krate
2051 .file_label(func.span)
2052 .split(':')
2053 .next()
2054 .unwrap_or_default(),
2055 )
2056 })
2057 .map(|(stage, _, _)| stage.clone())
2058 });
2059 let mut stages: Vec<StableId> = exact_stage.into_iter().collect();
2060 if stages.is_empty() {
2061 stages.push(StableId::new("stage", [&func.qualified_name]));
2062 }
2063
2064 for varmap in visitor.varmaps {
2065 let bindings = component_bindings.get(&varmap).cloned().unwrap_or_default();
2066 let components: Vec<StableId> = bindings
2067 .iter()
2068 .map(|(component, _)| component.clone())
2069 .collect();
2070 let components = expand_nested_components(model, components);
2071 let roots: Vec<String> = bindings.iter().map(|(_, root)| root.clone()).collect();
2072 for stage in &stages {
2073 let id = StableId::new(
2074 "optimizer-membership",
2075 [
2076 stage.0.as_str(),
2077 varmap.as_str(),
2078 func.qualified_name.as_str(),
2079 ],
2080 );
2081 model.optimizers.push(OptimizerMembership {
2082 id,
2083 stage: stage.clone(),
2084 optimizer: visitor
2085 .optimizer
2086 .clone()
2087 .unwrap_or_else(|| "optimizer".to_string()),
2088 varmap: varmap.clone(),
2089 components: components.clone(),
2090 builder_roots: roots.clone(),
2091 include_patterns: visitor.includes.clone(),
2092 exclude_patterns: visitor.excludes.clone(),
2093 conditional: (stages.len() > 1)
2094 .then(|| "pipeline stage/configuration dependent".to_string()),
2095 source: krate.file_label(func.span),
2096 evidence: vec![heuristic_source_evidence(
2097 krate.file_label(func.span),
2098 "optimizer consumes variables returned by named_train_vars/all_vars",
2099 )],
2100 });
2101 }
2102 }
2103 }
2104
2105 let components_by_stage = model.optimizers.iter().fold(
2106 HashMap::<StableId, Vec<StableId>>::new(),
2107 |mut map, optimizer| {
2108 map.entry(optimizer.stage.clone())
2109 .or_default()
2110 .extend(optimizer.components.iter().cloned());
2111 map
2112 },
2113 );
2114 for stage in &mut model.stages {
2115 if let Some(components) = components_by_stage.get(&stage.id) {
2116 stage.components.extend(components.iter().cloned());
2117 stage.components.sort();
2118 stage.components.dedup();
2119 }
2120 }
2121}
2122
2123fn expand_nested_components(model: &ModelIr, mut components: Vec<StableId>) -> Vec<StableId> {
2124 let qualified_components: HashMap<&str, &StableId> = model
2125 .components
2126 .iter()
2127 .map(|component| (component.qualified_name.as_str(), &component.id))
2128 .collect();
2129 loop {
2130 let mut added = Vec::new();
2131 for module in model
2132 .modules
2133 .iter()
2134 .filter(|module| components.contains(&module.component))
2135 {
2136 let Some(qualified_type) = module.qualified_type.as_deref() else {
2137 continue;
2138 };
2139 if let Some(component) = qualified_components.get(qualified_type) {
2140 if !components.contains(component) {
2141 added.push((*component).clone());
2142 }
2143 }
2144 }
2145 if added.is_empty() {
2146 break;
2147 }
2148 components.extend(added);
2149 components.sort();
2150 components.dedup();
2151 }
2152 components
2153}
2154
2155fn apply_optimizer_roles(model: &mut ModelIr) {
2156 let component_names: HashMap<StableId, String> = model
2157 .components
2158 .iter()
2159 .map(|component| (component.id.clone(), component.name.to_ascii_lowercase()))
2160 .collect();
2161 for parameter in &mut model.parameters {
2162 let mut matched = Vec::new();
2163 let mut excluded = false;
2164 let mut conditionally_excluded_component = false;
2165 for optimizer in &model.optimizers {
2166 let component_matches = optimizer.components.is_empty()
2167 || optimizer.components.contains(¶meter.component);
2168 let root_matches = optimizer.builder_roots.contains(¶meter.builder_root)
2169 || optimizer.builder_roots.is_empty()
2170 && similar_stem(&optimizer.varmap, ¶meter.builder_root);
2171 if !component_matches || !root_matches {
2172 continue;
2173 }
2174 if optimizer
2175 .exclude_patterns
2176 .iter()
2177 .any(|pattern| parameter.key.contains(pattern))
2178 {
2179 excluded = true;
2180 continue;
2181 }
2182 if optimizer.exclude_patterns.iter().any(|pattern| {
2183 let prefix = pattern
2184 .trim_matches(|character: char| !character.is_alphanumeric())
2185 .to_ascii_lowercase();
2186 !prefix.is_empty()
2187 && component_names
2188 .get(¶meter.component)
2189 .is_some_and(|component| component.contains(&prefix))
2190 }) {
2191 conditionally_excluded_component = true;
2192 }
2193 if optimizer.include_patterns.is_empty()
2194 || optimizer
2195 .include_patterns
2196 .iter()
2197 .any(|pattern| parameter.key.contains(pattern))
2198 {
2199 matched.push(optimizer.id.clone());
2200 }
2201 }
2202 parameter.optimizer_memberships = matched;
2203 parameter.role = if is_running_state(¶meter.key) {
2204 ParameterRole::RunningState
2205 } else if !parameter.optimizer_memberships.is_empty() && conditionally_excluded_component {
2206 ParameterRole::Conditional
2207 } else if !parameter.optimizer_memberships.is_empty() {
2208 ParameterRole::Optimized
2209 } else if excluded {
2210 ParameterRole::Excluded
2211 } else if model.optimizers.iter().any(|optimizer| {
2212 optimizer.components.contains(¶meter.component)
2213 && !optimizer.builder_roots.contains(¶meter.builder_root)
2214 }) {
2215 ParameterRole::Frozen
2216 } else {
2217 ParameterRole::Unknown
2218 };
2219 parameter.evidence.push(Evidence {
2220 kind: EvidenceKind::Source,
2221 confidence: if parameter.optimizer_memberships.is_empty() {
2222 Confidence::Conditional
2223 } else {
2224 Confidence::Proven
2225 },
2226 source: Some(parameter.source.clone()),
2227 detail: format!(
2228 "role {:?} derived from optimizer membership, not constructor naming",
2229 parameter.role
2230 ),
2231 });
2232 }
2233
2234 for component in &mut model.components {
2235 for builder in &mut component.builders {
2236 let roles: Vec<_> = model
2237 .parameters
2238 .iter()
2239 .filter(|parameter| {
2240 parameter.component == component.id && parameter.builder_root == builder.name
2241 })
2242 .map(|parameter| ¶meter.role)
2243 .collect();
2244 builder.role = if roles
2245 .iter()
2246 .any(|role| matches!(role, ParameterRole::Optimized))
2247 {
2248 BuilderRole::Trainable
2249 } else if roles.iter().any(|role| {
2250 matches!(
2251 role,
2252 ParameterRole::Frozen | ParameterRole::Excluded | ParameterRole::RunningState
2253 )
2254 }) {
2255 BuilderRole::Frozen
2256 } else {
2257 BuilderRole::Unknown
2258 };
2259 }
2260 }
2261}
2262
2263fn add_dataflow(krate: &Crate, model: &mut ModelIr) {
2264 let candle_nn_version = model.cargo.as_ref().and_then(|cargo| {
2265 op_semantics::matched_candle_version(
2266 cargo.candle_packages.get("candle-core").map(String::as_str),
2267 cargo.candle_packages.get("candle-nn").map(String::as_str),
2268 )
2269 .map(str::to_string)
2270 });
2271 let selected: Vec<(StableId, String, String, bool)> = model
2272 .functions
2273 .iter()
2274 .filter(|function| function.is_entrypoint && function.cfg_active != Some(false))
2275 .map(|function| {
2276 (
2277 function.id.clone(),
2278 function.name.clone(),
2279 function.qualified_name.clone(),
2280 function.is_loss,
2281 )
2282 })
2283 .collect();
2284
2285 let function_indices: HashMap<StableId, usize> = model
2286 .functions
2287 .iter()
2288 .enumerate()
2289 .map(|(index, function)| (function.id.clone(), index))
2290 .collect();
2291 for (function_id, _name, entry, is_loss) in selected {
2292 let graph = match dataflow::analyze_with_candle_version(
2293 krate,
2294 &entry,
2295 candle_nn_version.as_deref(),
2296 ) {
2297 Ok(graph) => graph,
2298 Err(error) => {
2299 push_finding(
2300 model,
2301 "dataflow-entrypoint",
2302 FindingSeverity::Information,
2303 Confidence::Proven,
2304 format!("could not analyze `{entry}`: {error:#}"),
2305 None,
2306 vec![function_id.clone()],
2307 );
2308 continue;
2309 }
2310 };
2311 let mut tensor_ids = vec![None; graph.nodes.len()];
2312 for node_id in &graph.tensor_nodes {
2313 let node = graph.node(*node_id);
2314 let id = StableId::new(
2315 "tensor",
2316 [function_id.0.as_str(), node.id.0.to_string().as_str()],
2317 );
2318 let name = node_name(&node.kind);
2319 let role = match &node.kind {
2320 NodeKind::Param { .. } => TensorRole::Input,
2321 NodeKind::Return => TensorRole::Output,
2322 _ if graph.loss_nodes.contains(&node.id) || is_loss => TensorRole::Loss,
2323 _ => TensorRole::Activation,
2324 };
2325 model.tensors.push(TensorContract {
2326 id: id.clone(),
2327 name,
2328 role,
2329 owner_function: function_id.clone(),
2330 parameter: None,
2331 shape: ShapeFact {
2332 rank: shape_rank(node.shape.as_deref()),
2333 dimensions: Vec::new(),
2334 source_expr: node.shape.clone(),
2335 },
2336 dtype: node.dtype.to_string(),
2337 device: DeviceFact::Unknown,
2338 layout: layout_for_node(&node.kind),
2339 requires_grad: match node.grad {
2340 GradState::Trainable | GradState::Differentiable => Some(true),
2341 GradState::Frozen | GradState::Severed => Some(false),
2342 _ => None,
2343 },
2344 evidence: vec![source_evidence(
2345 krate.file_label(node.span),
2346 format!(
2347 "expression-level static dataflow; source type {}",
2348 node.type_name.as_deref().unwrap_or("Tensor")
2349 ),
2350 )],
2351 });
2352 tensor_ids[node.id.0] = Some(id);
2353 }
2354 if let Some(&function_index) = function_indices.get(&function_id) {
2355 model.functions[function_index].tensor_inputs = graph
2356 .nodes
2357 .iter()
2358 .filter(|node| matches!(node.kind, NodeKind::Param { .. }))
2359 .filter_map(|node| tensor_ids[node.id.0].clone())
2360 .collect();
2361 model.functions[function_index].tensor_outputs = graph
2362 .entry_return
2363 .and_then(|node| tensor_ids[node.0].clone())
2364 .map(|tensor| vec![tensor])
2365 .unwrap_or_default();
2366 }
2367 for node in &graph.nodes {
2368 let NodeKind::Call { callee } = &node.kind else {
2369 continue;
2370 };
2371 let effect = op_semantics::lookup_resolved(callee, candle_nn_version.as_deref());
2372 if matches!(effect.grad, GradFlow::Unknown)
2373 && matches!(effect.dtype, crate::op_semantics::DtypeRule::Unknown)
2374 {
2375 continue;
2376 }
2377 let Some(output) = tensor_ids[node.id.0].clone() else {
2378 continue;
2379 };
2380 let inputs = graph
2381 .edges
2382 .iter()
2383 .filter(|edge| edge.to == node.id)
2384 .filter_map(|edge| tensor_ids[edge.from.0].clone())
2385 .collect();
2386 let operation_id = StableId::new(
2387 "operation",
2388 [function_id.0.as_str(), node.id.0.to_string().as_str()],
2389 );
2390 model.operations.push(Operation {
2391 id: operation_id.clone(),
2392 function: function_id.clone(),
2393 name: effect.name.clone(),
2394 qualified_name: callee.contains("::").then(|| callee.clone()),
2395 inputs,
2396 output,
2397 source: krate.file_label(node.span),
2398 dtype_rule: format!("{:?}", effect.dtype),
2399 gradient_rule: format!("{:?}", effect.grad),
2400 device_rule: if effect.name == "to_device" {
2401 "explicit".to_string()
2402 } else {
2403 "preserve".to_string()
2404 },
2405 shape_rule: shape_rule(&effect.name).to_string(),
2406 domain_rule: effect.domain_rule_label(),
2407 evidence: vec![Evidence {
2408 kind: EvidenceKind::Source,
2409 confidence: Confidence::Proven,
2410 source: Some(krate.file_label(node.span)),
2411 detail: effect.note.unwrap_or("Candle operation rule").to_string(),
2412 }],
2413 });
2414 link_implicit_parameter_reads(model, &graph, node.id, &function_id, &operation_id);
2415 }
2416 let dead_params = graph.dead_params();
2417 for conflict in &graph.dtype_conflicts {
2418 push_finding(
2419 model,
2420 "dtype-conflict",
2421 FindingSeverity::Error,
2422 Confidence::Proven,
2423 conflict.message.clone(),
2424 Some(krate.file_label(conflict.span)),
2425 vec![function_id.clone()],
2426 );
2427 }
2428 for risk in &graph.dtype_risks {
2429 push_finding(
2430 model,
2431 "dtype-risk",
2432 FindingSeverity::Warning,
2433 Confidence::Conditional,
2434 risk.message.clone(),
2435 Some(krate.file_label(risk.span)),
2436 vec![function_id.clone()],
2437 );
2438 }
2439 let inference_entrypoint = !is_loss;
2440 for violation in &graph.numeric_domain_violations {
2441 let (severity, confidence) =
2442 numeric_finding_severity(violation.proven, violation.impact, inference_entrypoint);
2443 push_finding(
2444 model,
2445 "numeric-domain-violation",
2446 severity,
2447 confidence,
2448 violation.message.clone(),
2449 Some(krate.file_label(violation.span)),
2450 vec![function_id.clone()],
2451 );
2452 }
2453 for nan_shape in &graph.zero_times_infinity {
2454 let (severity, confidence) =
2455 numeric_finding_severity(true, nan_shape.impact, inference_entrypoint);
2456 push_finding(
2457 model,
2458 "zero-times-infinity",
2459 severity,
2460 confidence,
2461 nan_shape.message.clone(),
2462 Some(krate.file_label(nan_shape.span)),
2463 vec![function_id.clone()],
2464 );
2465 if nan_shape.library_cite.is_some()
2466 && matches!(
2467 nan_shape.impact,
2468 NumericImpact::TrainingLossNaN | NumericImpact::GradientPoison
2469 )
2470 {
2471 push_finding(
2472 model,
2473 "unstable-library-loss",
2474 FindingSeverity::Error,
2475 Confidence::Proven,
2476 nan_shape.message.clone(),
2477 Some(krate.file_label(nan_shape.span)),
2478 vec![function_id.clone()],
2479 );
2480 }
2481 }
2482 for dead in dead_params {
2483 let mut related = vec![function_id.clone()];
2484 if let Some(tensor) = tensor_ids[dead.0].clone() {
2485 related.push(tensor);
2486 }
2487 push_finding(
2488 model,
2489 "dead-gradient-path",
2490 FindingSeverity::Warning,
2491 Confidence::Conditional,
2492 format!(
2493 "trainable expression `{}` has no differentiable path to a loss",
2494 node_name(&graph.node(dead).kind)
2495 ),
2496 Some(krate.file_label(graph.node(dead).span)),
2497 related,
2498 );
2499 }
2500 }
2501}
2502
2503fn link_implicit_parameter_reads(
2504 model: &mut ModelIr,
2505 graph: &dataflow::ExprGraph,
2506 operation: dataflow::NodeId,
2507 function_id: &StableId,
2508 operation_id: &StableId,
2509) {
2510 let Some(module_node) = graph
2511 .edges
2512 .iter()
2513 .find(|edge| edge.to == operation && edge.label.as_deref() == Some("module"))
2514 .map(|edge| graph.node(edge.from))
2515 else {
2516 return;
2517 };
2518 let NodeKind::Local { name } = &module_node.kind else {
2519 return;
2520 };
2521 let field = name.strip_prefix('.').unwrap_or(name);
2522 let Some(type_name) = module_node
2523 .type_name
2524 .as_deref()
2525 .and_then(|name| name.rsplit("::").next())
2526 else {
2527 return;
2528 };
2529 let modules = model
2530 .modules
2531 .iter()
2532 .filter(|module| {
2533 module.field.as_deref() == Some(field)
2534 && module.type_name.rsplit("::").next() == Some(type_name)
2535 })
2536 .map(|module| module.id.clone())
2537 .collect::<HashSet<_>>();
2538 let owner_type = model
2539 .functions
2540 .iter()
2541 .find(|function| &function.id == function_id)
2542 .and_then(|function| function.owner_type.as_deref());
2543 let mut components = model
2544 .components
2545 .iter()
2546 .filter(|component| Some(component.qualified_name.as_str()) == owner_type)
2547 .map(|component| component.id.clone())
2548 .collect::<HashSet<_>>();
2549 if let Some(owner_type) = owner_type {
2550 let owner_leaf = owner_type.rsplit("::").next().unwrap_or(owner_type);
2551 components.extend(
2552 model
2553 .modules
2554 .iter()
2555 .filter(|module| {
2556 module.type_name.rsplit("::").next() == Some(owner_leaf)
2557 || module
2558 .qualified_type
2559 .as_deref()
2560 .and_then(|name| name.rsplit("::").next())
2561 == Some(owner_leaf)
2562 })
2563 .map(|module| module.component.clone()),
2564 );
2565 }
2566 for parameter in &mut model.parameters {
2567 let field_prefix_match = components.contains(¶meter.component)
2568 && parameter.key.split('.').any(|segment| segment == field);
2569 if (modules.contains(¶meter.module) || field_prefix_match)
2570 && !parameter.uses.contains(operation_id)
2571 {
2572 parameter.uses.push(operation_id.clone());
2573 }
2574 }
2575}
2576
2577fn merge_runtime(model: &mut ModelIr, trace: &RuntimeTrace) {
2578 let expected = ExpectedIdentity {
2579 analysis_id: Some(model.analysis_id.0.clone()),
2580 build_id: model.cargo.as_ref().map(|cargo| cargo.build_id.clone()),
2581 };
2582 let audit = trace.audit_with_identity(Some(&expected));
2583 let identity_matches = audit.identity_mismatches.is_empty();
2584
2585 if identity_matches {
2586 let static_ids = trace
2587 .tensors
2588 .iter()
2589 .filter_map(|observation| observation.static_id.as_deref())
2590 .collect::<BTreeSet<_>>();
2591 let mut unknown_static_ids = 0usize;
2592 for static_id in static_ids {
2593 let Some(observation) = trace.agreed_tensor(static_id) else {
2594 continue;
2595 };
2596 let Some(tensor) = model
2597 .tensors
2598 .iter_mut()
2599 .find(|tensor| tensor.id.0 == static_id)
2600 else {
2601 unknown_static_ids += 1;
2602 continue;
2603 };
2604 tensor.shape.rank = Some(observation.shape.len());
2605 tensor.shape.dimensions = observation
2606 .shape
2607 .iter()
2608 .map(|dimension| crate::model_ir::Dimension {
2609 name: None,
2610 expr: dimension.to_string(),
2611 })
2612 .collect();
2613 tensor.dtype = observation.dtype.clone();
2614 tensor.device = parse_device(&observation.device);
2615 tensor.layout = if observation.contiguous {
2616 LayoutFact::Contiguous
2617 } else {
2618 LayoutFact::NonContiguous
2619 };
2620 tensor.requires_grad = Some(observation.requires_grad);
2621 tensor.evidence.push(Evidence {
2622 kind: EvidenceKind::Runtime,
2623 confidence: Confidence::Proven,
2624 source: observation.source.clone(),
2625 detail: format!("agreed runtime observation {}", observation.event_id),
2626 });
2627 }
2628 if unknown_static_ids > 0 {
2629 push_finding(
2630 model,
2631 "runtime-unmapped",
2632 FindingSeverity::Information,
2633 Confidence::Unknown,
2634 format!(
2635 "{unknown_static_ids} runtime tensor identities did not exist in this analysis"
2636 ),
2637 None,
2638 Vec::new(),
2639 );
2640 }
2641
2642 for parameter in &mut model.parameters {
2643 if let Some(gradient) = trace.gradient(¶meter.builder_root, ¶meter.key) {
2644 parameter.evidence.push(Evidence {
2645 kind: EvidenceKind::Runtime,
2646 confidence: Confidence::Proven,
2647 source: None,
2648 detail: format!("gradient {:?}, norm {:?}", gradient.state, gradient.norm),
2649 });
2650 }
2651 }
2652 for observation in &trace.operations {
2653 let Some(static_id) = observation.static_id.as_deref() else {
2654 continue;
2655 };
2656 if let Some(operation) = model
2657 .operations
2658 .iter_mut()
2659 .find(|operation| operation.id.0 == static_id)
2660 {
2661 operation.evidence.push(Evidence {
2662 kind: EvidenceKind::Runtime,
2663 confidence: Confidence::Proven,
2664 source: observation.source.clone(),
2665 detail: format!(
2666 "runtime operation {} observed as {}",
2667 observation.event_id, observation.op
2668 ),
2669 });
2670 }
2671 }
2672 for identity in audit
2673 .missing_gradients
2674 .iter()
2675 .chain(&audit.zero_gradients)
2676 .chain(&audit.non_finite_gradients)
2677 {
2678 let Some(gradient) = trace.gradient(&identity.root, &identity.key) else {
2679 continue;
2680 };
2681 let severity = if gradient.state == GradientState::NonFinite {
2682 FindingSeverity::Error
2683 } else {
2684 FindingSeverity::Warning
2685 };
2686 push_finding(
2687 model,
2688 "runtime-gradient",
2689 severity,
2690 Confidence::Proven,
2691 format!(
2692 "{}:{} runtime gradient is {:?}",
2693 gradient.root, gradient.key, gradient.state
2694 ),
2695 None,
2696 model
2697 .parameters
2698 .iter()
2699 .filter(|parameter| {
2700 parameter.builder_root == gradient.root && parameter.key == gradient.key
2701 })
2702 .map(|parameter| parameter.id.clone())
2703 .collect(),
2704 );
2705 }
2706 }
2707 for conflict in &audit.tensor_conflicts {
2708 push_finding(
2709 model,
2710 "runtime-tensor-conflict",
2711 FindingSeverity::Error,
2712 Confidence::Unknown,
2713 format!(
2714 "{} has conflicting runtime {:?}: {}",
2715 conflict.static_id,
2716 conflict.kind,
2717 conflict.values.join(", ")
2718 ),
2719 None,
2720 vec![StableId(conflict.static_id.clone())],
2721 );
2722 }
2723 for conflict in &audit.gradient_conflicts {
2724 push_finding(
2725 model,
2726 "runtime-gradient-conflict",
2727 FindingSeverity::Error,
2728 Confidence::Unknown,
2729 format!(
2730 "{}:{} has contradictory gradient observations: {}",
2731 conflict.identity.root,
2732 conflict.identity.key,
2733 conflict.states.join(", ")
2734 ),
2735 None,
2736 model
2737 .parameters
2738 .iter()
2739 .filter(|parameter| {
2740 parameter.builder_root == conflict.identity.root
2741 && parameter.key == conflict.identity.key
2742 })
2743 .map(|parameter| parameter.id.clone())
2744 .collect(),
2745 );
2746 }
2747 for mismatch in &audit.identity_mismatches {
2748 push_finding(
2749 model,
2750 "runtime-identity",
2751 FindingSeverity::Error,
2752 Confidence::Unknown,
2753 format!(
2754 "runtime {} mismatch: expected {}, observed {}; trace was not merged",
2755 mismatch.field, mismatch.expected, mismatch.observed
2756 ),
2757 None,
2758 Vec::new(),
2759 );
2760 }
2761 model.coverage.runtime_observations = trace.tensors.len()
2762 + trace.operations.len()
2763 + trace.gradients.len()
2764 + trace.values.len();
2765 model.runtime = Some(RuntimeSummary {
2766 trace_schema: trace.schema.clone(),
2767 entrypoint: Some(trace.run.entrypoint.clone()),
2768 profile: Some(trace.run.profile.clone()),
2769 tensor_observations: trace.tensors.len(),
2770 operation_observations: trace.operations.len(),
2771 gradient_observations: trace.gradients.len(),
2772 missing_gradients: audit.missing_gradients.len(),
2773 zero_gradients: audit.zero_gradients.len(),
2774 non_finite_gradients: audit.non_finite_gradients.len(),
2775 tensor_conflicts: audit.tensor_conflicts.len(),
2776 gradient_conflicts: audit.gradient_conflicts.len(),
2777 identity_mismatches: audit.identity_mismatches.len(),
2778 first_non_finite_step: audit.first_non_finite_step,
2779 saturating_activations: audit.saturating_activations.len(),
2780 value_observations: trace.values.len(),
2781 });
2782}
2783
2784fn flag_candle_semantics_version(model: &mut ModelIr, cargo: &CargoContext) {
2785 for (package, version) in &cargo.candle_versions {
2786 if package == "candle-core" || package == "candle-nn" {
2787 let supported = op_semantics::is_audited_candle_version(version);
2788 if !supported {
2789 push_finding(
2790 model,
2791 "candle-semantics-version",
2792 FindingSeverity::Warning,
2793 Confidence::Proven,
2794 format!(
2795 "{package} {version} differs from the audited operation catalogs \
2796 {}; \
2797 unknown or changed operations remain explicit",
2798 op_semantics::AUDITED_CANDLE_VERSION
2799 ),
2800 Some(cargo.manifest_path.to_string_lossy().into_owned()),
2801 Vec::new(),
2802 );
2803 }
2804 }
2805 }
2806 let core = cargo.candle_versions.get("candle-core");
2807 let nn = cargo.candle_versions.get("candle-nn");
2808 if let (Some(core), Some(nn)) = (core, nn) {
2809 if core != nn {
2810 push_finding(
2811 model,
2812 "candle-semantics-version",
2813 FindingSeverity::Warning,
2814 Confidence::Proven,
2815 format!(
2816 "candle-core {core} and candle-nn {nn} do not match; version-sensitive \
2817 constructor and autograd rules remain Unknown"
2818 ),
2819 Some(cargo.manifest_path.to_string_lossy().into_owned()),
2820 Vec::new(),
2821 );
2822 }
2823 }
2824}
2825
2826#[derive(Default)]
2827struct CallCollector {
2828 calls: Vec<String>,
2829}
2830
2831impl<'ast> Visit<'ast> for CallCollector {
2832 fn visit_expr_call(&mut self, node: &'ast syn::ExprCall) {
2833 if let syn::Expr::Path(path) = &*node.func {
2834 self.calls
2835 .push(path.path.to_token_stream().to_string().replace(' ', ""));
2836 }
2837 visit::visit_expr_call(self, node);
2838 }
2839
2840 fn visit_expr_method_call(&mut self, node: &'ast syn::ExprMethodCall) {
2841 self.calls.push(node.method.to_string());
2842 visit::visit_expr_method_call(self, node);
2843 }
2844}
2845
2846#[derive(Debug)]
2847struct ArtifactObservation {
2848 operation: String,
2849 path_expr: String,
2850 label: String,
2851 produced: bool,
2852}
2853
2854#[derive(Default)]
2855struct ArtifactVisitor {
2856 observed: Vec<ArtifactObservation>,
2857}
2858
2859impl<'ast> Visit<'ast> for ArtifactVisitor {
2860 fn visit_local(&mut self, node: &'ast syn::Local) {
2861 if let Some(init) = &node.init {
2862 let path_expr = format!(
2863 "{} = {}",
2864 node.pat.to_token_stream(),
2865 init.expr.to_token_stream()
2866 );
2867 let strings = string_literals(&init.expr);
2868 if let Some(label) = strings
2869 .iter()
2870 .rev()
2871 .find(|value| is_artifact_literal(value))
2872 .cloned()
2873 .filter(|_| path_expr.len() <= 256)
2874 {
2875 self.observed.push(ArtifactObservation {
2876 operation: "path_binding".to_string(),
2877 path_expr,
2878 label,
2879 produced: false,
2880 });
2881 }
2882 }
2883 visit::visit_local(self, node);
2884 }
2885
2886 fn visit_expr_struct(&mut self, node: &'ast syn::ExprStruct) {
2887 for field in &node.fields {
2888 let strings = string_literals(&field.expr);
2889 if let Some(label) = strings
2890 .iter()
2891 .rev()
2892 .find(|value| is_artifact_literal(value))
2893 .cloned()
2894 {
2895 self.observed.push(ArtifactObservation {
2896 operation: "path_field".to_string(),
2897 path_expr: format!(
2898 "{} = {}",
2899 field.member.to_token_stream(),
2900 field.expr.to_token_stream()
2901 ),
2902 label,
2903 produced: false,
2904 });
2905 }
2906 }
2907 visit::visit_expr_struct(self, node);
2908 }
2909
2910 fn visit_expr_call(&mut self, node: &'ast syn::ExprCall) {
2911 let op = match &*node.func {
2912 syn::Expr::Path(path) => path.path.to_token_stream().to_string().replace(' ', ""),
2913 _ => String::new(),
2914 };
2915 if is_artifact_operation(&op) {
2916 for argument in &node.args {
2917 self.observe(&op, argument);
2918 }
2919 }
2920 visit::visit_expr_call(self, node);
2921 }
2922
2923 fn visit_expr_method_call(&mut self, node: &'ast syn::ExprMethodCall) {
2924 let op = node.method.to_string();
2925 if is_artifact_operation(&op) {
2926 self.observe(&op, &node.receiver);
2927 for argument in &node.args {
2928 self.observe(&op, argument);
2929 }
2930 }
2931 visit::visit_expr_method_call(self, node);
2932 }
2933}
2934
2935impl ArtifactVisitor {
2936 fn observe(&mut self, operation: &str, expr: &syn::Expr) {
2937 let text = expr.to_token_stream().to_string();
2938 let strings = string_literals(expr);
2939 let Some(label) = strings
2940 .iter()
2941 .rev()
2942 .find(|value| is_artifact_literal(value))
2943 .cloned()
2944 else {
2945 return;
2946 };
2947 self.observed.push(ArtifactObservation {
2948 operation: operation.to_string(),
2949 path_expr: text,
2950 label,
2951 produced: is_producer_operation(operation),
2952 });
2953 }
2954}
2955
2956#[derive(Default)]
2957struct StringCollector {
2958 values: Vec<String>,
2959}
2960
2961impl<'ast> Visit<'ast> for StringCollector {
2962 fn visit_lit_str(&mut self, node: &'ast syn::LitStr) {
2963 self.values.push(node.value());
2964 }
2965}
2966
2967fn string_literals(expr: &syn::Expr) -> Vec<String> {
2968 let mut collector = StringCollector::default();
2969 collector.visit_expr(expr);
2970 collector.values
2971}
2972
2973#[derive(Default)]
2974struct OptimizerVisitor {
2975 varmaps: BTreeSet<String>,
2976 includes: Vec<String>,
2977 excludes: Vec<String>,
2978 optimizer: Option<String>,
2979}
2980
2981impl<'ast> Visit<'ast> for OptimizerVisitor {
2982 fn visit_expr_method_call(&mut self, node: &'ast syn::ExprMethodCall) {
2983 let method = node.method.to_string();
2984 if method == "all_vars" {
2985 self.varmaps
2986 .insert(node.receiver.to_token_stream().to_string().replace(' ', ""));
2987 }
2988 if method == "retain" {
2989 let mut patterns = RetainPatternVisitor::default();
2990 for argument in &node.args {
2991 patterns.visit_expr(argument);
2992 }
2993 self.includes.extend(patterns.includes);
2994 self.excludes.extend(patterns.excludes);
2995 }
2996 visit::visit_expr_method_call(self, node);
2997 }
2998
2999 fn visit_expr_call(&mut self, node: &'ast syn::ExprCall) {
3000 if let syn::Expr::Path(path) = &*node.func {
3001 let name = path.path.to_token_stream().to_string().replace(' ', "");
3002 if name.ends_with("named_train_vars") {
3003 if let Some(varmap) = node.args.first().and_then(expr_identifier) {
3004 self.varmaps.insert(varmap);
3005 }
3006 }
3007 if name.contains("Adam") || name.contains("Optimizer") {
3008 self.optimizer = Some(name);
3009 }
3010 }
3011 visit::visit_expr_call(self, node);
3012 }
3013}
3014
3015#[derive(Default)]
3016struct RetainPatternVisitor {
3017 negated: bool,
3018 includes: Vec<String>,
3019 excludes: Vec<String>,
3020}
3021
3022impl<'ast> Visit<'ast> for RetainPatternVisitor {
3023 fn visit_expr_method_call(&mut self, node: &'ast syn::ExprMethodCall) {
3024 if matches!(
3025 node.method.to_string().as_str(),
3026 "contains" | "starts_with" | "ends_with"
3027 ) {
3028 if let Some(syn::Expr::Lit(syn::ExprLit {
3029 lit: syn::Lit::Str(value),
3030 ..
3031 })) = node.args.first()
3032 {
3033 if self.negated {
3034 self.excludes.push(value.value());
3035 } else {
3036 self.includes.push(value.value());
3037 }
3038 }
3039 }
3040 visit::visit_expr_method_call(self, node);
3041 }
3042
3043 fn visit_expr_unary(&mut self, node: &'ast syn::ExprUnary) {
3044 if matches!(node.op, syn::UnOp::Not(_)) {
3045 self.negated = !self.negated;
3046 self.visit_expr(&node.expr);
3047 self.negated = !self.negated;
3048 } else {
3049 visit::visit_expr_unary(self, node);
3050 }
3051 }
3052}
3053
3054fn expr_identifier(expression: &syn::Expr) -> Option<String> {
3055 match expression {
3056 syn::Expr::Path(path) if path.path.segments.len() == 1 => {
3057 Some(path.path.segments[0].ident.to_string())
3058 }
3059 syn::Expr::Reference(reference) => expr_identifier(&reference.expr),
3060 syn::Expr::Paren(paren) => expr_identifier(&paren.expr),
3061 syn::Expr::Group(group) => expr_identifier(&group.expr),
3062 syn::Expr::Try(value) => expr_identifier(&value.expr),
3063 _ => None,
3064 }
3065}
3066
3067fn function_id(func: &ImplFn) -> StableId {
3068 StableId::new(
3069 "function",
3070 [
3071 func.qualified_name.as_str(),
3072 func.cfg_predicates.join(" && ").as_str(),
3073 ],
3074 )
3075}
3076
3077fn unique_qualified_struct(krate: &Crate, name: &str) -> Option<String> {
3078 match krate.struct_candidates(name).as_slice() {
3079 [def] => Some(def.qualified_name.clone()),
3080 _ => None,
3081 }
3082}
3083
3084fn source_evidence(source: String, detail: impl Into<String>) -> Evidence {
3085 Evidence {
3086 kind: EvidenceKind::Source,
3087 confidence: Confidence::Proven,
3088 source: Some(source),
3089 detail: detail.into(),
3090 }
3091}
3092
3093fn heuristic_source_evidence(source: String, detail: impl Into<String>) -> Evidence {
3094 Evidence {
3095 kind: EvidenceKind::Source,
3096 confidence: Confidence::Heuristic,
3097 source: Some(source),
3098 detail: detail.into(),
3099 }
3100}
3101
3102fn numeric_finding_severity(
3103 proven: bool,
3104 impact: NumericImpact,
3105 inference_entrypoint: bool,
3106) -> (FindingSeverity, Confidence) {
3107 if !proven {
3108 return (FindingSeverity::Warning, Confidence::Unknown);
3109 }
3110 match impact {
3111 NumericImpact::TrainingLossNaN | NumericImpact::GradientPoison => {
3112 (FindingSeverity::Error, Confidence::Proven)
3113 }
3114 NumericImpact::InferenceOutputRisk if inference_entrypoint => {
3115 (FindingSeverity::Error, Confidence::Proven)
3116 }
3117 NumericImpact::InferenceOutputRisk | NumericImpact::LocalOnly => {
3118 (FindingSeverity::Warning, Confidence::Proven)
3119 }
3120 }
3121}
3122
3123fn push_finding(
3124 model: &mut ModelIr,
3125 rule: &str,
3126 severity: FindingSeverity,
3127 confidence: Confidence,
3128 message: String,
3129 source: Option<String>,
3130 related: Vec<StableId>,
3131) {
3132 model.findings.push(Finding {
3133 id: StableId::new(
3134 "finding",
3135 [rule, source.as_deref().unwrap_or(""), message.as_str()],
3136 ),
3137 rule: rule.to_string(),
3138 severity,
3139 confidence,
3140 message,
3141 source,
3142 related,
3143 evidence: Vec::new(),
3144 });
3145}
3146
3147fn visibility(text: &str) -> Visibility {
3148 match text {
3149 "" => Visibility::Private,
3150 "pub" => Visibility::Public,
3151 "pub(crate)" => Visibility::Crate,
3152 value if value.starts_with("pub(") => Visibility::Restricted,
3153 _ => Visibility::Unknown,
3154 }
3155}
3156
3157fn certainty_confidence(certainty: &Certainty) -> Confidence {
3158 match certainty {
3159 Certainty::Certain => Confidence::Proven,
3160 Certainty::Conditional(_) => Confidence::Conditional,
3161 Certainty::Unknown(_) => Confidence::Unknown,
3162 }
3163}
3164
3165fn is_tensor_type(ty: &str) -> bool {
3166 ty.split(|character: char| !character.is_alphanumeric() && character != '_')
3167 .any(|segment| segment == "Tensor")
3168}
3169
3170fn is_model_entry_name(name: &str) -> bool {
3171 matches!(
3172 name,
3173 "forward"
3174 | "forward_diff"
3175 | "forward_features"
3176 | "forward_t"
3177 | "encode"
3178 | "predict"
3179 | "compress"
3180 | "conditioning"
3181 | "condition"
3182 | "generate"
3183 | "decode"
3184 | "loss"
3185 ) || name.ends_with("_forward")
3186 || name.ends_with("_loss")
3187}
3188
3189fn is_stage_entry_name(name: &str) -> bool {
3190 name == "prepare"
3191 || name.starts_with("try_run_")
3192 || name.starts_with("run_")
3193 && (name.contains("train") || name.contains("eval") || name.contains("prepare"))
3194}
3195
3196fn is_pipeline_stage_call(name: &str) -> bool {
3197 name == "prepare_data"
3198 || name == "final_eval"
3199 || name.starts_with("train_")
3200 || name.starts_with("evaluate_")
3201 || name.starts_with("export_")
3202}
3203
3204fn stage_variants(krate: &Crate, target: &StableId) -> Vec<String> {
3205 let _ = (krate, target);
3206 Vec::new()
3207}
3208
3209fn stage_kind(name: &str) -> StageKind {
3210 if name.contains("eval") {
3211 StageKind::Evaluate
3212 } else if name.contains("prepare") || name.contains("data") {
3213 StageKind::Prepare
3214 } else if name.contains("train")
3215 || name.contains("bridge")
3216 || name == "latent"
3217 || name == "world"
3218 {
3219 StageKind::Train
3220 } else if name.contains("export") {
3221 StageKind::Export
3222 } else if name.starts_with("preflight_") {
3223 StageKind::Probe
3224 } else {
3225 StageKind::Unknown
3226 }
3227}
3228
3229fn stage_display_name(function: &Function) -> String {
3230 let module = function
3231 .qualified_name
3232 .rsplit_once("::")
3233 .map(|(module, _)| module.rsplit("::").next().unwrap_or(module))
3234 .unwrap_or_default();
3235 if module.is_empty() || function.name.contains(module) {
3236 function.name.clone()
3237 } else {
3238 format!("{module}:{}", function.name)
3239 }
3240}
3241
3242fn reachable_components(function: &Function, model: &ModelIr) -> Vec<StableId> {
3243 let mut components = Vec::new();
3244 let mut queue = vec![function.id.clone()];
3245 let mut seen = HashSet::new();
3246 while let Some(id) = queue.pop() {
3247 if !seen.insert(id.clone()) {
3248 continue;
3249 }
3250 if let Some(func) = model.functions.iter().find(|func| func.id == id) {
3251 if let Some(owner) = func.owner_type.as_deref() {
3252 components.extend(
3253 model
3254 .components
3255 .iter()
3256 .filter(|component| component.qualified_name == owner)
3257 .map(|component| component.id.clone()),
3258 );
3259 }
3260 for edge in model
3261 .architecture_edges
3262 .iter()
3263 .filter(|edge| edge.via_function == id)
3264 {
3265 components.push(edge.from.clone());
3266 components.push(edge.to.clone());
3267 }
3268 queue.extend(func.calls.iter().cloned());
3269 }
3270 }
3271 components.sort();
3272 components.dedup();
3273 components
3274}
3275
3276fn stage_for_function(
3277 function: &StableId,
3278 stages: &HashMap<StableId, StableId>,
3279 model: &ModelIr,
3280) -> Option<StableId> {
3281 if let Some(stage) = stages.get(function) {
3282 return Some(stage.clone());
3283 }
3284 model.stages.iter().find_map(|stage| {
3285 reachable_function(&stage.function, function, model, &mut HashSet::new())
3286 .then(|| stage.id.clone())
3287 })
3288}
3289
3290fn reachable_function(
3291 current: &StableId,
3292 target: &StableId,
3293 model: &ModelIr,
3294 seen: &mut HashSet<StableId>,
3295) -> bool {
3296 if current == target {
3297 return true;
3298 }
3299 if !seen.insert(current.clone()) {
3300 return false;
3301 }
3302 model
3303 .functions
3304 .iter()
3305 .find(|function| &function.id == current)
3306 .is_some_and(|function| {
3307 function
3308 .calls
3309 .iter()
3310 .any(|callee| reachable_function(callee, target, model, seen))
3311 })
3312}
3313
3314fn qualify(module: &str, path: &str) -> String {
3315 if module.is_empty() {
3316 path.to_string()
3317 } else if path.is_empty() {
3318 module.to_string()
3319 } else {
3320 format!("{module}::{path}")
3321 }
3322}
3323
3324fn acquisition_label(acquisition: &Acquisition) -> String {
3325 match acquisition {
3326 Acquisition::Constructor { func, .. } => func.clone(),
3327 Acquisition::RawGet { method } => method.clone(),
3328 }
3329}
3330
3331fn similar_stem(left: &str, right: &str) -> bool {
3332 fn stem(value: &str) -> String {
3333 value
3334 .replace("varmap", "")
3335 .replace("var_map", "")
3336 .replace("vb", "")
3337 .replace(['_', '&'], "")
3338 }
3339 let left = stem(left);
3340 let right = stem(right);
3341 !left.is_empty() && !right.is_empty() && (left.contains(&right) || right.contains(&left))
3342}
3343
3344fn is_running_state(key: &str) -> bool {
3345 key.contains("running_mean")
3346 || key.contains("running_var")
3347 || key.contains("num_batches_tracked")
3348}
3349
3350fn is_artifact_operation(name: &str) -> bool {
3351 let leaf = name.rsplit("::").next().unwrap_or(name);
3352 matches!(
3353 leaf,
3354 "load"
3355 | "save"
3356 | "read"
3357 | "write"
3358 | "open"
3359 | "mmap"
3360 | "from_mmaped_safetensors"
3361 | "from_buffered_safetensors"
3362 | "serialize_to_file"
3363 | "load_buffer"
3364 )
3365}
3366
3367fn is_producer_operation(name: &str) -> bool {
3368 let leaf = name.rsplit("::").next().unwrap_or(name);
3369 matches!(leaf, "save" | "write" | "serialize_to_file")
3370}
3371
3372fn is_artifact_literal(value: &str) -> bool {
3373 let lower = value.to_ascii_lowercase();
3374 [
3375 ".safetensors",
3376 ".safetensor",
3377 ".json",
3378 ".jsonl",
3379 ".bin",
3380 ".pt",
3381 ".pth",
3382 ".gguf",
3383 ".parquet",
3384 ".arrow",
3385 ]
3386 .iter()
3387 .any(|suffix| lower.ends_with(suffix))
3388}
3389
3390fn artifact_identity(path_expr: &str) -> String {
3391 path_expr
3392 .split_once('=')
3393 .map(|(binding, _)| binding.trim().replace(' ', ""))
3394 .filter(|binding| {
3395 !binding.is_empty()
3396 && binding
3397 .chars()
3398 .all(|character| character.is_alphanumeric() || character == '_')
3399 })
3400 .unwrap_or_else(|| path_expr.to_string())
3401}
3402
3403fn infer_artifact_stage_links(model: &mut ModelIr) {
3404 let _ = model;
3405}
3406
3407fn artifact_kind(label: &str) -> ArtifactKind {
3408 let lower = label.to_ascii_lowercase();
3409 if lower.ends_with(".safetensors")
3410 || lower.ends_with(".bin")
3411 || lower.ends_with(".pt")
3412 || lower.ends_with(".pth")
3413 || lower.ends_with(".gguf")
3414 {
3415 ArtifactKind::Checkpoint
3416 } else if lower.contains("eval") || lower.contains("report") || lower.contains("metric") {
3417 ArtifactKind::EvaluationReport
3418 } else if lower.ends_with(".parquet") || lower.ends_with(".arrow") {
3419 ArtifactKind::Dataset
3420 } else if lower.ends_with(".json") {
3421 ArtifactKind::Configuration
3422 } else {
3423 ArtifactKind::Unknown
3424 }
3425}
3426
3427fn node_name(kind: &NodeKind) -> String {
3428 match kind {
3429 NodeKind::Param { name } | NodeKind::Local { name } => name.clone(),
3430 NodeKind::Call { callee } => format!("result_of_{callee}"),
3431 NodeKind::Literal { text } => text.clone(),
3432 NodeKind::Phi => "branch_join".to_string(),
3433 NodeKind::Return => "return".to_string(),
3434 NodeKind::Unknown { reason } => format!("unknown:{reason}"),
3435 }
3436}
3437
3438fn shape_rank(shape: Option<&str>) -> Option<usize> {
3439 let shape = shape?;
3440 let trimmed = shape.trim();
3441 if (trimmed.starts_with('(') && trimmed.ends_with(')'))
3442 || (trimmed.starts_with('[') && trimmed.ends_with(']'))
3443 {
3444 Some(
3445 trimmed[1..trimmed.len() - 1]
3446 .split(',')
3447 .filter(|part| !part.trim().is_empty())
3448 .count(),
3449 )
3450 } else {
3451 None
3452 }
3453}
3454
3455fn layout_for_node(kind: &NodeKind) -> LayoutFact {
3456 match kind {
3457 NodeKind::Call { callee } if callee.ends_with("contiguous") => LayoutFact::Contiguous,
3458 NodeKind::Call { callee }
3459 if ["transpose", "permute", "narrow", "t"]
3460 .iter()
3461 .any(|op| callee.ends_with(op)) =>
3462 {
3463 LayoutFact::Strided
3464 }
3465 _ => LayoutFact::Unknown,
3466 }
3467}
3468
3469fn shape_rule(op: &str) -> &'static str {
3470 match op {
3471 "reshape" | "broadcast_as" | "expand" => "explicit_argument",
3472 "flatten_all" | "flatten_to" | "flatten_from" => "flatten",
3473 "squeeze" => "remove_dimension",
3474 "unsqueeze" => "insert_dimension",
3475 "transpose" | "permute" | "t" => "permute_dimensions",
3476 "narrow" => "slice_dimension",
3477 "matmul" | "broadcast_matmul" => "matrix_product",
3478 _ => "preserve_or_operation_defined",
3479 }
3480}
3481
3482fn parse_device(device: &str) -> DeviceFact {
3483 let lower = device.to_ascii_lowercase();
3484 if lower == "cpu" {
3485 DeviceFact::Cpu
3486 } else if lower.starts_with("cuda") {
3487 let ordinal = lower
3488 .split([':', '(', ')'])
3489 .find_map(|part| part.parse::<u32>().ok());
3490 DeviceFact::Cuda { ordinal }
3491 } else if lower.starts_with("metal") {
3492 DeviceFact::Metal
3493 } else {
3494 DeviceFact::Unknown
3495 }
3496}