Skip to main content

bb_ir/
slot_value.rs

1//! Universal slot value trait. The blanket impl is the only path,
2//! so anything `Any + Send + Sync + Clone + Serialize +
3//! DeserializeOwned` is a `SlotValue` by construction. Type
4//! identity per slot rides on `ValueInfoProto.type_node`; consumers
5//! downcast to the graph-guaranteed concrete type.
6
7use std::any::{Any, TypeId};
8use std::collections::HashMap;
9use std::sync::OnceLock;
10
11use serde::{de::DeserializeOwned, Serialize};
12
13use crate::types::{TypeNode, TYPE_ANY};
14
15/// Wire-coding failure for a `SlotValue`. The default bincode path
16/// is infallible for vanilla serde derives; custom `Serialize` impls
17/// and missing receiver-side decoders surface here.
18#[derive(Debug)]
19pub enum SlotValueError {
20    /// Encoder returned an error; boxed inner is the serde
21    /// diagnostic.
22    EncodeFailed(Box<dyn std::error::Error + Send + Sync>),
23    /// Decoder returned an error; boxed inner is the serde
24    /// diagnostic.
25    DecodeFailed(Box<dyn std::error::Error + Send + Sync>),
26    /// Receiver has no registered decoder for the stamped
27    /// `type_hash` (older / divergent build).
28    UnknownTypeHash(u64),
29}
30
31impl std::fmt::Display for SlotValueError {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        match self {
34            Self::EncodeFailed(err) => write!(f, "SlotValue::to_wire_bytes failed: {err}"),
35            Self::DecodeFailed(err) => write!(f, "SlotValue decode failed: {err}"),
36            Self::UnknownTypeHash(hash) => write!(
37                f,
38                "SlotValue decode: no registered decoder for type_hash {hash:#018x}",
39            ),
40        }
41    }
42}
43
44impl std::error::Error for SlotValueError {
45    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
46        match self {
47            Self::EncodeFailed(err) => Some(err.as_ref()),
48            Self::DecodeFailed(err) => Some(err.as_ref()),
49            Self::UnknownTypeHash(_) => None,
50        }
51    }
52}
53
54/// Universal slot value. Slot-table values, op outputs, and
55/// `dispatch_atomic` inputs are all `Box<dyn SlotValue>` /
56/// `&dyn SlotValue`. Local forwarding uses `clone_boxed`; wire +
57/// snapshot paths use `to_wire_bytes`.
58pub trait SlotValue: Any + Send + Sync {
59    /// Downcast surface — recover the concrete type.
60    fn as_any(&self) -> &dyn Any;
61
62    /// Repackage `Box<dyn SlotValue>` as `Box<dyn Any>` for
63    /// [`Box::downcast`]. Required because the `SlotValue` and
64    /// `Any` vtables are distinct even though `SlotValue: Any`.
65    fn into_any_boxed(self: Box<Self>) -> Box<dyn Any + Send + Sync>;
66
67    /// Polymorphic clone preserving the concrete type.
68    fn clone_boxed(&self) -> Box<dyn SlotValue>;
69
70    /// Wire-boundary encode (bincode + serde). Local forwarding
71    /// uses `clone_boxed` instead.
72    fn to_wire_bytes(&self) -> Result<Vec<u8>, SlotValueError>;
73
74    /// Stable cross-Node type discriminator. FNV-1a of
75    /// `std::any::type_name::<T>()`; receiver decodes only on a
76    /// matching hash.
77    fn type_hash(&self) -> u64;
78
79    /// Runtime [`TypeNode`] for this value. Returns the leaf
80    /// registered via [`register_type_node!`] or [`TYPE_ANY`].
81    /// Consulted at wire boundaries + TypeSolver seeding; the
82    /// atomic-dispatch hot path uses compile-time-stamped closures.
83    fn runtime_type(&self) -> &'static TypeNode {
84        let tid = self.as_any().type_id();
85        runtime_type_registry()
86            .get(&tid)
87            .copied()
88            .unwrap_or(&TYPE_ANY)
89    }
90
91    /// Bytes the carrier owes against
92    /// `NodeConfig::ingress_byte_budget`. Slot-table eviction calls
93    /// this to release the charge. Default `0` — only
94    /// ingress-derived carriers register a non-zero resolver via
95    /// [`register_charged_bytes!`].
96    fn charged_bytes(&self) -> usize {
97        let any = self.as_any();
98        let tid = any.type_id();
99        match charged_bytes_registry().get(&tid) {
100            Some(f) => f(any),
101            None => 0,
102        }
103    }
104}
105
106/// Inventory entry mapping `TypeId → &'static TypeNode`. Submitted
107/// by [`register_type_node!`].
108pub struct RuntimeTypeBinding {
109    /// `TypeId::of::<T>` is non-const; use a closure.
110    pub type_id_fn: fn() -> TypeId,
111    /// Lattice node this concrete type resolves to.
112    pub type_node: &'static TypeNode,
113}
114
115inventory::collect!(RuntimeTypeBinding);
116
117/// Startup-built `TypeId → &TypeNode` map driving
118/// [`SlotValue::runtime_type`].
119pub fn runtime_type_registry() -> &'static HashMap<TypeId, &'static TypeNode> {
120    static REG: OnceLock<HashMap<TypeId, &'static TypeNode>> = OnceLock::new();
121    REG.get_or_init(|| {
122        let mut m: HashMap<TypeId, &'static TypeNode> = HashMap::new();
123        for binding in inventory::iter::<RuntimeTypeBinding> {
124            m.insert((binding.type_id_fn)(), binding.type_node);
125        }
126        m
127    })
128}
129
130/// Per-type charged-bytes resolver. Takes the carrier's erased
131/// `&dyn Any` and returns the byte count.
132pub type ChargedBytesFn = fn(&dyn Any) -> usize;
133
134/// Inventory entry mapping `TypeId → ChargedBytesFn`. Submitted by
135/// [`register_charged_bytes!`].
136pub struct ChargedBytesBinding {
137    /// `TypeId::of::<T>` is non-const; use a closure.
138    pub type_id_fn: fn() -> TypeId,
139    /// Downcasts and returns the wire-byte count.
140    pub resolve_fn: ChargedBytesFn,
141}
142
143inventory::collect!(ChargedBytesBinding);
144
145/// Startup-built `TypeId → ChargedBytesFn` map driving
146/// [`SlotValue::charged_bytes`].
147pub fn charged_bytes_registry() -> &'static HashMap<TypeId, ChargedBytesFn> {
148    static REG: OnceLock<HashMap<TypeId, ChargedBytesFn>> = OnceLock::new();
149    REG.get_or_init(|| {
150        let mut m: HashMap<TypeId, ChargedBytesFn> = HashMap::new();
151        for binding in inventory::iter::<ChargedBytesBinding> {
152            m.insert((binding.type_id_fn)(), binding.resolve_fn);
153        }
154        m
155    })
156}
157
158/// Register a carrier's wire-byte resolver.
159///
160/// ```ignore
161/// register_charged_bytes!(BytesValue, |b: &BytesValue| b.0.len());
162/// ```
163#[macro_export]
164macro_rules! register_charged_bytes {
165    ($t:ty, $resolve:expr) => {
166        $crate::inventory::submit! {
167            $crate::slot_value::ChargedBytesBinding {
168                type_id_fn: || ::std::any::TypeId::of::<$t>(),
169                resolve_fn: |any| {
170                    let resolve: fn(&$t) -> usize = $resolve;
171                    match any.downcast_ref::<$t>() {
172                        Some(v) => resolve(v),
173                        None => 0,
174                    }
175                },
176            }
177        }
178    };
179}
180
181/// Wire-decode fn for a known concrete type.
182pub type WireDecodeFn = fn(&[u8]) -> Result<Box<dyn SlotValue>, SlotValueError>;
183
184/// Inventory entry mapping `type_hash → WireDecodeFn`. Emitted by
185/// [`register_type_node!`].
186pub struct WireDecoderBinding {
187    /// Concrete type's stable `type_hash`.
188    pub type_hash_fn: fn() -> u64,
189    /// Bincode decoder.
190    pub decode_fn: WireDecodeFn,
191}
192
193inventory::collect!(WireDecoderBinding);
194
195/// Startup-built `type_hash → WireDecodeFn` map used by
196/// `CompositeValue`'s wire codec to materialise typed children.
197pub fn wire_decoder_registry() -> &'static HashMap<u64, WireDecodeFn> {
198    static REG: OnceLock<HashMap<u64, WireDecodeFn>> = OnceLock::new();
199    REG.get_or_init(|| {
200        let mut m: HashMap<u64, WireDecodeFn> = HashMap::new();
201        for binding in inventory::iter::<WireDecoderBinding> {
202            m.insert((binding.type_hash_fn)(), binding.decode_fn);
203        }
204        m
205    })
206}
207
208/// Register a concrete type's lattice [`TypeNode`] + wire decoder.
209/// Emits both a [`RuntimeTypeBinding`] and a [`WireDecoderBinding`].
210/// Unregistered types resolve to [`crate::types::TYPE_ANY`] and
211/// their wire payloads cannot be decoded.
212///
213/// ```ignore
214/// use bb_ir::slot_value::register_type_node;
215/// use bb_ir::types::TYPE_PEER_ID;
216/// register_type_node!(PeerIdValue, &TYPE_PEER_ID);
217/// ```
218#[macro_export]
219macro_rules! register_type_node {
220    ($t:ty, $node:expr) => {
221        $crate::inventory::submit! {
222            $crate::slot_value::RuntimeTypeBinding {
223                type_id_fn: || ::std::any::TypeId::of::<$t>(),
224                type_node: $node,
225            }
226        }
227        $crate::inventory::submit! {
228            $crate::slot_value::WireDecoderBinding {
229                type_hash_fn: || $crate::slot_value::type_hash_of::<$t>(),
230                decode_fn: |bytes| {
231                    $crate::bincode::deserialize::<$t>(bytes)
232                        .map(|v| Box::new(v) as Box<dyn $crate::slot_value::SlotValue>)
233                        .map_err(|e| $crate::slot_value::SlotValueError::DecodeFailed(Box::new(e)))
234                },
235            }
236        }
237    };
238}
239
240/// The only `SlotValue` impl path. Manual impls are not supported —
241/// "derive serde + Clone" gets you a wire-eligible carrier.
242impl<T> SlotValue for T
243where
244    T: Any + Send + Sync + Clone + Serialize + DeserializeOwned,
245{
246    fn as_any(&self) -> &dyn Any {
247        self
248    }
249
250    fn into_any_boxed(self: Box<Self>) -> Box<dyn Any + Send + Sync> {
251        self
252    }
253
254    fn clone_boxed(&self) -> Box<dyn SlotValue> {
255        Box::new(self.clone())
256    }
257
258    fn to_wire_bytes(&self) -> Result<Vec<u8>, SlotValueError> {
259        bincode::serialize(self).map_err(|e| SlotValueError::EncodeFailed(Box::new(e)))
260    }
261
262    fn type_hash(&self) -> u64 {
263        type_hash_of::<T>()
264    }
265}
266
267/// FNV-1a 64-bit hash of `std::any::type_name::<T>()`. Deterministic
268/// across runs of the same Rust toolchain; stamped onto
269/// `SlotFill.type_hash` and matched on the receive side.
270#[inline]
271pub fn type_hash_of<T: ?Sized + 'static>() -> u64 {
272    fnv1a_64(std::any::type_name::<T>().as_bytes())
273}
274
275/// `const`-callable FNV-1a 64-bit. Used by [`type_hash_of`] and
276/// compile-time `TY_*` constant builders.
277#[inline]
278pub const fn fnv1a_64(bytes: &[u8]) -> u64 {
279    let mut h: u64 = 0xcbf2_9ce4_8422_2325;
280    let mut i = 0;
281    while i < bytes.len() {
282        h ^= bytes[i] as u64;
283        h = h.wrapping_mul(0x0000_0100_0000_01b3);
284        i += 1;
285    }
286    h
287}