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