Skip to main content

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