bb_runtime/slot_value.rs
1//! The universal `SlotValue` trait — every value flowing through slot
2//! sites (DSL outputs, wire payloads, syscall returns, role-method
3//! returns) implements it via the blanket `impl<T: Tensor> SlotValue`
4//! and per-primitive impls.
5//!
6//! Canonical home for `bb-runtime`. Also hosts engine-side carriers
7//! that step outside the serde-driven blanket impl path (today:
8//! [`BackendTensorCarrier`], which holds a type-erased backend-owned
9//! tensor handle).
10
11use std::any::Any;
12
13pub use bb_ir::slot_value::*;
14
15use crate::ids::ComponentRef;
16
17/// Engine-internal `SlotValue` wrapping a backend-native tensor
18/// behind a type-erased handle. Built by the engine's wire-decode
19/// path (`decode_typed_fill` backend-mediated branch) from the
20/// `Backend::materialize_from_wire` result; downstream graph ops
21/// downcast `inner` to the backend's `Self::Tensor` to read the
22/// value.
23///
24/// Lifecycle:
25///
26/// 1. Engine reads an inbound tensor `SlotFill`, charges its bytes
27/// against `NodeConfig::ingress_byte_budget`, and hands the
28/// `Vec<u8>` to the backend bound to the destination slot via
29/// [`crate::roles::BackendRuntime::materialize_from_wire`].
30/// 2. Backend returns a `Box<dyn SlotValue>` containing this
31/// carrier; the engine installs it in the slot table.
32/// 3. On slot overwrite / eviction, the writer reads
33/// [`SlotValue::charged_bytes`] (returns `self.charged_bytes`)
34/// and releases the budget against the engine counter.
35///
36/// `clone_fn` + `wire_encode_fn` carry the per-`T::Tensor` clone /
37/// re-encode shape so the carrier supports the universal
38/// `SlotValue` contract without a `Clone` / `Serialize` bound on
39/// the type-erased `inner`. The `#[derive(bb::Backend)]` derive
40/// captures `T` at the call site and stores these fn pointers in
41/// the carrier; intra-Node clones (`clone_boxed`) and re-encodes
42/// (`to_wire_bytes`) route through them.
43pub struct BackendTensorCarrier {
44 /// Backend's native tensor, type-erased. Internally Arc-shared
45 /// (the backend's `Tensor` impl chooses the strategy — see
46 /// `CpuTensor(Arc<CpuBackendBuffer>)`); cloning the carrier
47 /// invokes `clone_fn` which calls the typed `Clone` impl, which
48 /// is an `Arc::clone` for pooling-friendly backends.
49 pub(crate) inner: Box<dyn Any + Send + Sync>,
50 /// Per-`T` clone bridge. Reads the erased `inner` as `&T` and
51 /// returns a `Box<dyn Any>` over a fresh `T::clone()`. Captured
52 /// at materialize-time so the carrier stays dyn-safe without a
53 /// `Clone` bound on `dyn Any`.
54 pub(crate) clone_fn: fn(&(dyn Any + Send + Sync)) -> Box<dyn Any + Send + Sync>,
55 /// Per-`T` wire-encode bridge. Reads the erased `inner` as `&T`
56 /// and returns the bincode payload bytes. Mirrors the blanket
57 /// `SlotValue::to_wire_bytes` impl for `T: Serialize` but lives
58 /// here so the carrier can re-encode through the same path the
59 /// sender used.
60 pub(crate) wire_encode_fn: fn(&(dyn Any + Send + Sync)) -> Result<Vec<u8>, SlotValueError>,
61 /// Wire-type hash this carrier originated from. Receivers
62 /// validate downcast targets and re-encode against this; senders
63 /// stamp it into outbound `SlotFill.type_hash`.
64 pub(crate) type_hash: u64,
65 /// Bytes admitted against `NodeConfig::ingress_byte_budget` at
66 /// receive time. The slot-table writer releases these on
67 /// overwrite / eviction via [`SlotValue::charged_bytes`].
68 pub(crate) charged_bytes: usize,
69 /// `ComponentRef` of the backend that produced this carrier.
70 /// `decode_typed_fill` stamps the source backend so future
71 /// re-encode / forwarding paths can route through the same
72 /// backend instance.
73 pub(crate) backend_ref: ComponentRef,
74}
75
76impl BackendTensorCarrier {
77 /// Construct a carrier from the backend's already-typed
78 /// `Self::Tensor`. The `#[derive(bb::Backend)]` materialize
79 /// bridge is the canonical caller; the constructor is `pub` so
80 /// derive expansions in downstream crates can call it, but the
81 /// engine-side fields (`charged_bytes`, `backend_ref`) get
82 /// stamped via [`Self::stamp_engine_fields`] immediately after
83 /// the bridge returns so authoring code never holds a carrier
84 /// with stale accounting.
85 pub fn from_typed<T>(
86 tensor: T,
87 type_hash: u64,
88 charged_bytes: usize,
89 backend_ref: ComponentRef,
90 ) -> Self
91 where
92 T: Any + Send + Sync + Clone + serde::Serialize + 'static,
93 {
94 Self {
95 inner: Box::new(tensor),
96 clone_fn: |any| {
97 let t: &T = any.downcast_ref::<T>().expect("inner is T by construction");
98 Box::new(t.clone())
99 },
100 wire_encode_fn: |any| {
101 let t: &T = any.downcast_ref::<T>().expect("inner is T by construction");
102 bincode::serialize(t).map_err(|e| SlotValueError::EncodeFailed(Box::new(e)))
103 },
104 type_hash,
105 charged_bytes,
106 backend_ref,
107 }
108 }
109
110 /// Borrow the carrier's wire-type hash. Used by the wire-encode
111 /// path and by tests that assert a fill's type discriminator
112 /// round-trips through the carrier.
113 pub fn type_hash(&self) -> u64 {
114 self.type_hash
115 }
116
117 /// Borrow the producing backend's `ComponentRef`. Used by
118 /// re-encode + introspection.
119 pub fn backend_ref(&self) -> ComponentRef {
120 self.backend_ref
121 }
122
123 /// Downcast the type-erased inner tensor to the backend's
124 /// concrete `Self::Tensor`. Engine consumers reach the tensor
125 /// through this accessor; the inner field stays
126 /// `pub(crate)` so external code can't dodge the downcast +
127 /// type-hash validation step.
128 pub fn downcast_inner<T: Any + Send + Sync + 'static>(&self) -> Option<&T> {
129 self.inner.downcast_ref::<T>()
130 }
131}
132
133impl std::fmt::Debug for BackendTensorCarrier {
134 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135 f.debug_struct("BackendTensorCarrier")
136 .field("type_hash", &format_args!("{:#018x}", self.type_hash))
137 .field("charged_bytes", &self.charged_bytes)
138 .field("backend_ref", &self.backend_ref)
139 .finish()
140 }
141}
142
143impl SlotValue for BackendTensorCarrier {
144 fn as_any(&self) -> &dyn Any {
145 self
146 }
147
148 fn into_any_boxed(self: Box<Self>) -> Box<dyn Any + Send + Sync> {
149 self
150 }
151
152 fn clone_boxed(&self) -> Box<dyn SlotValue> {
153 Box::new(Self {
154 inner: (self.clone_fn)(&*self.inner),
155 clone_fn: self.clone_fn,
156 wire_encode_fn: self.wire_encode_fn,
157 type_hash: self.type_hash,
158 charged_bytes: self.charged_bytes,
159 backend_ref: self.backend_ref,
160 })
161 }
162
163 fn to_wire_bytes(&self) -> Result<Vec<u8>, SlotValueError> {
164 (self.wire_encode_fn)(&*self.inner)
165 }
166
167 fn type_hash(&self) -> u64 {
168 self.type_hash
169 }
170
171 fn charged_bytes(&self) -> usize {
172 self.charged_bytes
173 }
174}
175
176/// Typed error surfaced by
177/// [`crate::roles::BackendRuntime::materialize_from_wire`]. The
178/// derive bridge converts the backend's typed
179/// `<T as crate::contracts::Backend>::Error` to this through
180/// `Display`; the engine maps it onto
181/// [`crate::bus::WireReceiveErrorKind::BackendMaterializeFailed`].
182#[derive(Debug, Clone)]
183pub struct BackendMaterializeError {
184 /// Short `Display` of the backend's typed error.
185 pub summary: String,
186}
187
188impl std::fmt::Display for BackendMaterializeError {
189 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190 write!(f, "Backend::materialize_from_wire: {}", self.summary)
191 }
192}
193
194impl std::error::Error for BackendMaterializeError {}
195