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