1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
//! Pass 11 - `resolve_slots` + pre-flight. Final check before a
//! ModelProto is emitted
//!
//! implements **only the AmbiguousRole detection** per
//! `docs/internal/IMPLEMENTATION_PLAN.md` :
//!
//! For each role domain (`ai.bytesandbrains.role.<role>`), collect
//! the `concrete_type` providers AND the `(required_trait, slot_id)`
//! providers visible in the NodeProtos. If BOTH kinds appear under
//! the same role → reject with `CompileError::AmbiguousRole`.
//!
//! The runner (`runner.rs`) maps the `AmbiguousRole` variant to
//! `BuildError::AmbiguousRole` for the public Module::build() surface
//! per the plan.
//!
//! Other §14 checks (slot binding existence, decoder availability,
//! opset coverage) require bindings + wire types;
//! they're deferred.
use std::collections::BTreeMap;
use crate::error::CompileError;
use bb_ir::proto::onnx::FunctionProto;
/// Resolve slots + run pre-flight. Pure per ANALYSIS.md §3.2.
pub fn resolve_slots(function: &FunctionProto) -> Result<(), CompileError> {
// Per role: collect distinct concrete_type providers + distinct
// (required_trait, slot_id) generic providers.
let mut role_providers: BTreeMap<String, RoleProviders> = BTreeMap::new();
for node in &function.node {
if !node.domain.starts_with("ai.bytesandbrains.role.") {
continue;
}
let providers = role_providers.entry(node.domain.clone()).or_default();
if let Some(concrete) = meta_value(node, "ai.bytesandbrains.concrete_type") {
providers.concrete_types.insert(concrete.to_string());
}
if let (Some(rt), Some(sid)) = (
meta_value(node, "ai.bytesandbrains.required_trait"),
meta_value(node, "ai.bytesandbrains.slot_id"),
) {
if let Ok(id) = sid.parse::<u32>() {
providers.generic_slots.insert(id, rt.to_string());
}
}
}
for (role, providers) in role_providers {
if !providers.concrete_types.is_empty() && !providers.generic_slots.is_empty() {
// First concrete + first generic slot are surfaced as
// canonical witnesses. The pass is deterministic because
// BTreeMap iteration is ordered.
let concrete_type = providers
.concrete_types
.iter()
.next()
.cloned()
.unwrap_or_default();
let (slot_id, _trait_name) = providers
.generic_slots
.iter()
.next()
.map(|(k, v)| (*k, v.clone()))
.unwrap_or_default();
return Err(CompileError::AmbiguousRole {
role,
concrete_type,
generic_slot_id: slot_id,
});
}
}
Ok(())
}
#[derive(Default)]
struct RoleProviders {
concrete_types: std::collections::BTreeSet<String>,
generic_slots: BTreeMap<u32, String>,
}
fn meta_value<'a>(node: &'a bb_ir::proto::onnx::NodeProto, key: &str) -> Option<&'a str> {
node.metadata_props
.iter()
.find(|p| p.key == key)
.map(|p| p.value.as_str())
}