Skip to main content

bb_compiler/
resolve_component_dependencies.rs

1//! `resolve_component_dependencies` pass.
2//!
3//! Walks every bound concrete in a [`CompiledArtifact`]'s
4//! `BindingSpec`, reads the concrete's declared
5//! [`DependencyDecl`]s through the inventory carrier, and verifies
6//! each required slot is bound to a concrete whose role matches the
7//! dependency's required role. On success, every NodeProto
8//! contributed by a concrete (identified by its `concrete_type`
9//! metadata) gets its declared deps stamped via
10//! [`bb_ir::keys::stamp_dependency_metadata`] so downstream passes +
11//! the runtime can recover the wiring from the IR alone.
12//!
13//! Surfaces:
14//! - [`CompileError::UnboundDependency`] when the required slot has
15//!   no binding.
16//! - [`CompileError::DependencyRoleMismatch`] when the slot is bound
17//!   to a concrete whose role set does not include the required role.
18
19use bb_ir::component::DependencyDecl;
20use bb_ir::keys::{stamp_dependency_metadata, CONCRETE_TYPE_KEY};
21use bb_ir::proto::onnx::ModelProto;
22use bb_ir::registry::find_concrete_component;
23
24use crate::artifact::{BindingSlot, BindingSpec};
25use crate::error::CompileError;
26
27/// Run the pass over the artifact's spec + IR. On success, mutates
28/// every concrete-bearing NodeProto in `models` to carry its
29/// declared dep metadata.
30pub(crate) fn resolve_component_dependencies(
31    spec: &BindingSpec,
32    models: &mut [ModelProto],
33) -> Result<(), CompileError> {
34    for slot in &spec.slots {
35        let concrete_type = slot.concrete_type_name.as_str();
36        if concrete_type.is_empty() {
37            // Generic placeholder slot — no concrete bound, no deps
38            // to verify. The Compiler::bind chain in T8 fills these.
39            continue;
40        }
41        let entry = match find_concrete_component(concrete_type) {
42            Some(e) => e,
43            None => {
44                // Concrete isn't in this binary's inventory —
45                // `validate_runtime_complete` surfaces it.
46                // Skip so this pass stays a pure dep-graph check.
47                continue;
48            }
49        };
50        verify_deps(slot, entry.dependencies, spec)?;
51    }
52
53    stamp_dep_metadata_across_models(spec, models);
54    Ok(())
55}
56
57fn verify_deps(
58    component_slot: &BindingSlot,
59    deps: &[DependencyDecl],
60    spec: &BindingSpec,
61) -> Result<(), CompileError> {
62    for dep in deps {
63        let target = spec
64            .get(dep.slot)
65            .ok_or_else(|| CompileError::UnboundDependency {
66                component: component_slot.concrete_type_name.clone(),
67                bound_at_slot: component_slot.slot.clone(),
68                required_role: dep.role.to_string(),
69                required_slot: dep.slot.to_string(),
70            })?;
71        if !role_matches(&target.role, dep.role) {
72            return Err(CompileError::DependencyRoleMismatch {
73                component: component_slot.concrete_type_name.clone(),
74                bound_at_slot: component_slot.slot.clone(),
75                required_role: dep.role.to_string(),
76                required_slot: dep.slot.to_string(),
77                provided_role: target.role.clone(),
78            });
79        }
80    }
81    Ok(())
82}
83
84/// `BindingSlot.role` historically uses the engine-side trait name
85/// (e.g. `"BackendRuntime"`); `DependencyDecl.role` uses the
86/// canonical Contract role name (e.g. `"Backend"`). Normalize both
87/// to the bare PascalCase role identifier before comparison.
88fn role_matches(provided: &str, required: &str) -> bool {
89    normalize_role(provided) == normalize_role(required)
90}
91
92fn normalize_role(role: &str) -> &str {
93    role.strip_suffix("Runtime").unwrap_or(role)
94}
95
96fn stamp_dep_metadata_across_models(spec: &BindingSpec, models: &mut [ModelProto]) {
97    for model in models {
98        // Walk the graph + each function for any NodeProto whose
99        // `concrete_type` metadata names a bound concrete; stamp
100        // the concrete's declared deps onto its metadata_props.
101        if let Some(graph) = model.graph.as_mut() {
102            for node in &mut graph.node {
103                stamp_for_node(spec, node);
104            }
105        }
106        for function in &mut model.functions {
107            for node in &mut function.node {
108                stamp_for_node(spec, node);
109            }
110        }
111    }
112}
113
114fn stamp_for_node(spec: &BindingSpec, node: &mut bb_ir::proto::onnx::NodeProto) {
115    let Some(concrete_type) = node
116        .metadata_props
117        .iter()
118        .find(|e| e.key == CONCRETE_TYPE_KEY)
119        .map(|e| e.value.clone())
120    else {
121        return;
122    };
123    let Some(_bound_slot) = spec
124        .slots
125        .iter()
126        .find(|s| s.concrete_type_name == concrete_type)
127    else {
128        return;
129    };
130    let Some(entry) = find_concrete_component(&concrete_type) else {
131        return;
132    };
133    if entry.dependencies.is_empty() {
134        return;
135    }
136    stamp_dependency_metadata(node, entry.dependencies);
137}
138