Skip to main content

candle_graph/
discover.rs

1//! Crate-wide model discovery and unified IR assembly.
2//!
3//! Public API boundaries and `VarBuilder` constructors provide conservative component candidates.
4//! Architecture, pipeline, artifact, and optimizer relationships require compiler-resolved value
5//! flow; the former syntax/name-based implementation is quarantined until that frontend exists.
6//! Optional runtime traces refine (but never erase) static facts.
7
8#[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    /// Optional component root, including private/internal model types.
45    pub component_root: Option<String>,
46    /// Analyze expression graphs for source entrypoints. Disable only for a fast symbol scan.
47    pub dataflow: bool,
48    /// Enable name/order-derived architecture, pipeline, artifact, and optimizer heuristics.
49    /// All emitted relationships are tagged `Heuristic` and should not be treated as proven facts.
50    pub heuristic_architecture: bool,
51}
52
53/// Analyze a Cargo crate or source directory into the unified model IR.
54pub 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    // FNV-1a is deliberately fixed here rather than relying on Rust's unstable DefaultHasher.
258    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        // A public tensor boundary is a code-derived entrypoint candidate. Its spelling carries no
297        // semantics; only its trait identity or public tensor signature matters here.
298        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        // Any public type with a VarBuilder constructor is a model component candidate
1454        // (e.g. `my_model::Model`), not only crate-root re-exports.
1455        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(&parameter.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(&param.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 &param.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(&param.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(&parameter.component);
2227            let root_matches = optimizer.builder_roots.contains(&parameter.builder_root)
2228                || optimizer.builder_roots.is_empty()
2229                    && similar_stem(&optimizer.varmap, &parameter.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(&parameter.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(&parameter.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(&parameter.component)
2272                && !optimizer.builder_roots.contains(&parameter.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| &parameter.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        } // execution phase
2622    }
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(&parameter.component)
2690            && parameter.key.split('.').any(|segment| segment == field);
2691        if (modules.contains(&parameter.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(&parameter.builder_root, &parameter.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}