bb_runtime/engine/graph_slot.rs
1//! `GraphSlot` - per-FunctionProto compiled data per
2//! `docs/ENGINE.md` §3.
3//!
4//! Under the runtime-linker model each entry in
5//! `Node.model.functions[]` becomes one `GraphSlot` in
6//! `Engine.graphs`, keyed by the function's
7//! `(domain, name, overload)`. The map IS the symbol table - a call
8//! NodeProto whose `(domain, op_type, overload)` matches a key
9//! resolves to the target installed graph.
10//!
11//! Each NodeProto's dispatch kind is pre-stamped at install time
12//! into `op_dispatch[i]` (parallel to `function.node[i]`). Runtime
13//! invoke is one indexed probe; no HashMap lookups on hot path.
14
15use std::collections::HashMap;
16
17use crate::engine::dispatch_entry::OpDispatch;
18use crate::ids::{NodeSiteId, OpRef};
19use bb_ir::proto::onnx::FunctionProto;
20
21/// Per-FunctionProto compiled data installed on the `Engine`. Keyed
22/// in `Engine.graphs` by `(domain, name, overload)` - the canonical
23/// symbol-table key the linker dedupes on. The map key IS the name,
24/// so `GraphSlot` carries no separate `name` field.
25pub struct GraphSlot {
26 /// The post-analysis FunctionProto body. Source of truth for
27 /// op_type / domain / input + output names. `function.node[i]`
28 /// is the NodeProto for OpRef `pack(graph_idx, i)`.
29 pub function: FunctionProto,
30
31 /// Per-NodeProto dispatch decision, indexed parallel to
32 /// `function.node[]`. Populated by `Engine::resolve_dispatch`
33 /// after install; runtime invoke is `op_dispatch[node_idx]`
34 /// where `node_idx = op_ref.split().1`.
35 pub op_dispatch: Vec<OpDispatch>,
36
37 /// Producer site → downstream consumer ops. Used by
38 /// `write_outputs` to push ready consumers onto the frontier.
39 pub consumers: HashMap<NodeSiteId, Vec<OpRef>>,
40
41 /// Site name → `NodeSiteId` allocation.
42 pub site_names: HashMap<String, NodeSiteId>,
43
44 /// Top-level `function.output` sites, mapped from `NodeSiteId` to
45 /// the declared output name. When a value lands at one of these
46 /// sites and `consumers[site]` is empty, the engine surfaces it
47 /// as `EngineStep::AppEvent { module_name, topic: <output name> }`
48 /// — the "function signature is the engine I/O contract" path.
49 /// Populated only for entry-point GraphSlots (`is_entry_point`).
50 pub top_level_outputs: HashMap<NodeSiteId, String>,
51
52 /// `wire.Recv` payload site → sender site pairing. Recv NodeProtos
53 /// emit two outputs — `(payload, sender)`. The inbound envelope
54 /// delivers its byte payload to the payload site; the engine
55 /// also writes `PeerIdValue(src_peer)` to the sender site on the
56 /// same execution so downstream user ops can read the
57 /// provenance of the received message and reply by sending back
58 /// to the same peer. Populated at install time by scanning
59 /// every `ai.bytesandbrains.wire` Recv NodeProto with two
60 /// outputs.
61 pub recv_sender_sites: HashMap<NodeSiteId, NodeSiteId>,
62
63 /// `wire.Recv` payload site → expected wire-type hash. When the
64 /// compiler stamped `ValueInfoProto.type_node` on a Recv's
65 /// payload output, the runtime carries the producer-side
66 /// `type_hash` here so the typed-receive path can fire a
67 /// `WireReceiveError { kind: TypeMismatch }` when an inbound
68 /// fill's `type_hash` does not match the slot contract.
69 /// Slots without an entry are treated as dynamic / Any: the
70 /// decoder-registry lookup proceeds without the mismatch
71 /// check. Populated at install time alongside
72 /// [`Self::recv_sender_sites`].
73 pub recv_wire_type_hash: HashMap<NodeSiteId, u64>,
74
75 /// `wire.Recv` payload site → bound role slot id. Built at
76 /// install time by reading the compiler-stamped
77 /// `RECV_SLOT_ID_KEY` off each `wire.Recv` NodeProto and pairing
78 /// it with the Recv's allocated `NodeSiteId`. Consumed by
79 /// `decode_typed_fill` to cross from data-plane identity
80 /// (`NodeSiteId`) to binding identity (`slot_id`) before
81 /// dispatching the backend-mediated tensor path. Recv sites whose
82 /// payload does not flow into a role-bound slot are absent.
83 pub recv_site_to_slot_id: HashMap<NodeSiteId, u32>,
84
85 /// `true` if this function is an entry point (a registered
86 /// Module's main partition function). Entry-point GraphSlots
87 /// have `top_level_outputs` populated; sub-function bodies do not
88 /// (their outputs flow through `CallContext.output_forwarding` at
89 /// call time).
90 pub is_entry_point: bool,
91}
92
93impl GraphSlot {
94 /// Test-only constructor. Builds a `GraphSlot` from the
95 /// supplied FunctionProto with empty op_index / consumers /
96 /// site_names tables. Production code uses
97 /// [`Self::from_function`].
98 #[cfg(any(test, feature = "test-components"))]
99 pub fn new_for_test(_name: String, function: FunctionProto) -> Self {
100 Self {
101 function,
102 op_dispatch: Vec::new(),
103 consumers: HashMap::new(),
104 site_names: HashMap::new(),
105 top_level_outputs: HashMap::new(),
106 recv_sender_sites: HashMap::new(),
107 recv_wire_type_hash: HashMap::new(),
108 recv_site_to_slot_id: HashMap::new(),
109 is_entry_point: false,
110 }
111 }
112
113 /// Canonical install path: walks the FunctionProto's
114 /// nodes assigning positional `OpRef`s + fresh `NodeSiteId`s,
115 /// populates `op_index` + `site_names` + `consumers` per
116 /// `docs/ENGINE.md` §3.
117 ///
118 /// `OpRef`s pack as `(graph_idx << 32) | node_idx` so the engine
119 /// hot path resolves `OpRef → NodeProto` via two array accesses
120 /// (the `op_index` HashMap is retained only as a /// migration aide; C8 deletes it). `graph_idx` is the engine's
121 /// number of already-installed graphs at the moment of install,
122 /// passed in by the caller (typically `Engine::install_graph` /
123 /// `install_function_library`). `NodeSiteId`s remain globally
124 /// monotonic via `next_node_site_id` since they cross graphs.
125 pub fn from_function(
126 _name: String,
127 function: FunctionProto,
128 graph_idx: u32,
129 next_node_site_id: &mut u64,
130 ) -> Self {
131 let mut site_names: HashMap<String, NodeSiteId> = HashMap::new();
132 let mut consumers: HashMap<NodeSiteId, Vec<OpRef>> = HashMap::new();
133
134 // First pass: mint NodeSiteIds for every produced value name +
135 // pre-fill op_dispatch with Unresolved sentinels (one slot per
136 // NodeProto in install order). OpRefs are positional:
137 // `OpRef::pack(graph_idx, node_idx)` directly indexes
138 // `function.node[]`.
139 let mut op_refs: Vec<OpRef> = Vec::with_capacity(function.node.len());
140 let mut op_dispatch: Vec<OpDispatch> = Vec::with_capacity(function.node.len());
141 for (idx, node) in function.node.iter().enumerate() {
142 let op_ref = OpRef::pack(graph_idx, idx as u32);
143 op_refs.push(op_ref);
144 op_dispatch.push(OpDispatch::Unresolved);
145 for out in &node.output {
146 if out.is_empty() {
147 continue;
148 }
149 site_names.entry(out.clone()).or_insert_with(|| {
150 let r = NodeSiteId::from(*next_node_site_id);
151 *next_node_site_id = next_node_site_id.saturating_add(1);
152 r
153 });
154 }
155 }
156
157 // Second pass: populate `consumers` - for each non-empty
158 // input on each node, record the consuming op_ref under the
159 // producer's NodeSiteId.
160 for (idx, node) in function.node.iter().enumerate() {
161 let consumer = op_refs[idx];
162 for input in &node.input {
163 if input.is_empty() {
164 continue;
165 }
166 let Some(&site) = site_names.get(input) else {
167 continue;
168 };
169 consumers.entry(site).or_default().push(consumer);
170 }
171 }
172
173 // Resolve top-level output sites for the AppEvent surfacing
174 // path. Each entry in `function.output` is a declared output
175 // port; if it maps to a registered NodeSiteId, that site is
176 // a candidate for `EngineStep::AppEvent` when no downstream
177 // consumer reads it.
178 let mut top_level_outputs: HashMap<NodeSiteId, String> = HashMap::new();
179 for name in &function.output {
180 if let Some(&site) = site_names.get(name) {
181 top_level_outputs.insert(site, name.clone());
182 }
183 }
184
185 // Pair each wire.Recv's payload site with its sender site so
186 // inbound envelope delivery can populate both at the same
187 // ExecId. Recv NodeProtos are emitted by the DSL
188 // `Graph::wire` with `output: [payload, sender]`.
189 //
190 // Also read the compiler-stamped `RECV_SLOT_ID_KEY` off each
191 // Recv node and pair the payload site's `NodeSiteId` with
192 // the downstream role's `slot_id`. The map is consumed by
193 // `decode_typed_fill` to route backend-mediated tensor fills
194 // through the bound backend instance.
195 let mut recv_sender_sites: HashMap<NodeSiteId, NodeSiteId> = HashMap::new();
196 let mut recv_site_to_slot_id: HashMap<NodeSiteId, u32> = HashMap::new();
197 for node in &function.node {
198 if node.domain != "ai.bytesandbrains.wire" || node.op_type != "Recv" {
199 continue;
200 }
201 let payload = node.output.first().and_then(|n| site_names.get(n));
202 let sender = node.output.get(1).and_then(|n| site_names.get(n));
203 if let (Some(&p), Some(&s)) = (payload, sender) {
204 recv_sender_sites.insert(p, s);
205 }
206 if let Some(&payload_site) = payload {
207 let slot_id = node
208 .metadata_props
209 .iter()
210 .find(|kv| kv.key == bb_ir::keys::RECV_SLOT_ID_KEY)
211 .and_then(|kv| kv.value.parse::<u32>().ok());
212 if let Some(slot_id) = slot_id {
213 recv_site_to_slot_id.insert(payload_site, slot_id);
214 }
215 }
216 }
217
218 // `recv_wire_type_hash` stays empty at install time. The
219 // compiler does not yet stamp `ValueInfoProto.type_node` on
220 // Recv payload outputs with a stable hash that matches the
221 // producer-side `SlotValue::type_hash()` derivation (FNV-1a
222 // over the concrete type name); the `TypeNode.wire_hash`
223 // field is a parallel handle-assigned identifier that does
224 // not align with the runtime `type_hash`. Until the
225 // compiler-stamp follow-up lands (per
226 // `docs/internal/superpowers/specs/2026-06-24-wire-recv-typed-receive-and-bundle-bench.md`
227 // §7), the TypeMismatch check is dormant in production:
228 // entries can be inserted by tests + future install passes
229 // without changing the typed-receive happy path.
230 let recv_wire_type_hash: HashMap<NodeSiteId, u64> = HashMap::new();
231
232 let _ = op_refs;
233 Self {
234 function,
235 op_dispatch,
236 consumers,
237 site_names,
238 top_level_outputs,
239 recv_sender_sites,
240 recv_wire_type_hash,
241 recv_site_to_slot_id,
242 is_entry_point: true,
243 }
244 }
245}
246