Skip to main content

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