Skip to main content

byteflow/bytecode/
value.rs

1use std::fmt;
2use std::sync::Arc;
3
4/// Fixed-size envelope carried in mailboxes and registers (**Atomic Hop**).
5///
6/// # Why this exists (request-reply / typed protocols)
7///
8/// `Receive` delivers a single [`Value`] — not `{sender, Value}`. Without an
9/// envelope, a server flow cannot learn who sent a request, and two clients
10/// cannot safely share a `request_id` space.
11///
12/// # Security
13///
14/// - **`sender`**: FlowId stamped by the scheduler on bytecode `Send` / `Ask`
15///   (invariant **S1**). Not a capability.
16/// - **`reply_cap`**: CapId minted at the same boundary with **SEND**-only
17///   rights so the recipient can answer without ambient Pid addressing
18///   (phase 2). Zero means “no reply grant” (host-injected hops may omit it).
19///
20/// See `docs/security.md`.
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
22pub struct Message {
23    /// Authenticated origin FlowId (`0` = trusted host / non-flow).
24    pub sender: u64,
25    /// Capability granting **SEND** back to [`Self::sender`], or `0`.
26    pub reply_cap: u64,
27    /// Client correlation token; echoed on replies (`Ask` / **S2**).
28    pub request_id: u64,
29    /// Protocol discriminator (opaque to the VM).
30    pub tag: u16,
31    /// Small protocol payload.
32    pub payload: u64,
33}
34
35impl Message {
36    /// Build an envelope. `sender` / `reply_cap` are placeholders until a
37    /// bytecode hop is authenticated by the scheduler (`reply_cap` typically
38    /// `0` here).
39    pub const fn new(sender: u64, request_id: u64, tag: u16, payload: u64) -> Self {
40        Self {
41            sender,
42            reply_cap: 0,
43            request_id,
44            tag,
45            payload,
46        }
47    }
48
49    /// Stamp origin FlowId and attach a reply capability (scheduler only).
50    #[inline]
51    pub(crate) fn authenticate(mut self, sender: u64, reply_cap: u64) -> Self {
52        self.sender = sender;
53        self.reply_cap = reply_cap;
54        self
55    }
56}
57
58impl fmt::Display for Message {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        write!(
61            f,
62            "msg{{from=flow#{}, reply=cap#{}, id={}, tag={}, payload={}}}",
63            self.sender, self.reply_cap, self.request_id, self.tag, self.payload
64        )
65    }
66}
67
68/// A dynamically-tagged runtime value.
69///
70/// [`Value::Cap`] is an unforgeable address for `Send` / `Ask` (phase 2).
71/// [`Value::Pid`] remains for **identity** inside authenticated messages
72/// (`Message.sender` / `msg_sender`), not for ambient addressing.
73///
74/// [`Value::Str`] / [`Value::Bytes`] are heap payloads shared via [`Arc`] so
75/// register moves and mailbox hops clone the handle, not the buffer.
76#[derive(Clone, Debug, PartialEq)]
77pub enum Value {
78    Unit,
79    Bool(bool),
80    Int(i64),
81    Float(f64),
82    /// Internal / message identity (FlowId as `u64`). **Not** a Send target.
83    Pid(u64),
84    /// Atomic Hop envelope. See [`Message`].
85    Message(Message),
86    /// Unforgeable capability (`CapId` as `u64`). Required for `Send` / `Ask`.
87    Cap(u64),
88    /// UTF-8 text (constant pool, natives, host).
89    Str(Arc<str>),
90    /// Opaque byte buffer (constant pool, natives, host).
91    Bytes(Arc<[u8]>),
92}
93
94impl Value {
95    /// Build a [`Value::Str`] from anything string-like.
96    #[inline]
97    pub fn str(s: impl AsRef<str>) -> Self {
98        Value::Str(Arc::from(s.as_ref()))
99    }
100
101    /// Build a [`Value::Bytes`] from a byte slice.
102    #[inline]
103    pub fn bytes(b: impl AsRef<[u8]>) -> Self {
104        Value::Bytes(Arc::from(b.as_ref()))
105    }
106
107    /// Truthiness used by `Opcode::Branch`: falsy are `Unit`, `Bool(false)`,
108    /// `Int(0)`, empty [`Value::Str`], and empty [`Value::Bytes`].
109    #[inline]
110    pub fn is_truthy(&self) -> bool {
111        match self {
112            Value::Unit | Value::Bool(false) | Value::Int(0) => false,
113            Value::Str(s) if s.is_empty() => false,
114            Value::Bytes(b) if b.is_empty() => false,
115            _ => true,
116        }
117    }
118
119    #[inline]
120    pub fn as_int(&self) -> Option<i64> {
121        match self {
122            Value::Int(i) => Some(*i),
123            Value::Bool(b) => Some(*b as i64),
124            _ => None,
125        }
126    }
127
128    #[inline]
129    pub fn as_pid(&self) -> Option<u64> {
130        match self {
131            Value::Pid(p) => Some(*p),
132            _ => None,
133        }
134    }
135
136    #[inline]
137    pub fn as_cap(&self) -> Option<u64> {
138        match self {
139            Value::Cap(c) => Some(*c),
140            _ => None,
141        }
142    }
143
144    #[inline]
145    pub fn as_message(&self) -> Option<Message> {
146        match self {
147            Value::Message(m) => Some(*m),
148            _ => None,
149        }
150    }
151
152    #[inline]
153    pub fn as_str(&self) -> Option<&str> {
154        match self {
155            Value::Str(s) => Some(s.as_ref()),
156            _ => None,
157        }
158    }
159
160    #[inline]
161    pub fn as_bytes(&self) -> Option<&[u8]> {
162        match self {
163            Value::Bytes(b) => Some(b.as_ref()),
164            Value::Str(s) => Some(s.as_bytes()),
165            _ => None,
166        }
167    }
168
169    /// Bytes this value is **charged** for against a mailbox byte budget
170    /// (see [`crate::MailboxBytes`]).
171    ///
172    /// # This is a charge model, not an RSS measurement
173    ///
174    /// [`Value::Str`] / [`Value::Bytes`] are `Arc`-shared: the same buffer
175    /// cloned into N mailboxes exists once in memory, but each mailbox is
176    /// charged the full length. That over-counts on purpose — a budget that
177    /// under-counts shared payloads is not a bound at all, since a single
178    /// producer could fan one large `Arc` out to every inbox and stay
179    /// "within budget" everywhere while the host pays once per distinct
180    /// buffer it keeps alive.
181    ///
182    /// The inline `size_of::<Value>()` term is included so a flood of
183    /// scalar hops is also bounded, not just blob hops.
184    #[inline]
185    pub fn memory_size(&self) -> usize {
186        std::mem::size_of::<Self>() + self.heap_size()
187    }
188
189    /// Heap bytes owned (transitively) by this value, excluding the enum
190    /// itself. Zero for every scalar variant.
191    #[inline]
192    pub fn heap_size(&self) -> usize {
193        match self {
194            Value::Str(s) => s.len(),
195            Value::Bytes(b) => b.len(),
196            Value::Unit
197            | Value::Bool(_)
198            | Value::Int(_)
199            | Value::Float(_)
200            | Value::Pid(_)
201            | Value::Message(_)
202            | Value::Cap(_) => 0,
203        }
204    }
205
206    pub fn type_name(&self) -> &'static str {
207        match self {
208            Value::Unit => "unit",
209            Value::Bool(_) => "bool",
210            Value::Int(_) => "int",
211            Value::Float(_) => "float",
212            Value::Pid(_) => "pid",
213            Value::Message(_) => "message",
214            Value::Cap(_) => "cap",
215            Value::Str(_) => "str",
216            Value::Bytes(_) => "bytes",
217        }
218    }
219}
220
221impl fmt::Display for Value {
222    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223        match self {
224            Value::Unit => write!(f, "()"),
225            Value::Bool(b) => write!(f, "{b}"),
226            Value::Int(i) => write!(f, "{i}"),
227            Value::Float(x) => write!(f, "{x}"),
228            Value::Pid(p) => write!(f, "flow#{p}"),
229            Value::Message(m) => write!(f, "{m}"),
230            Value::Cap(c) => write!(f, "cap#{c}"),
231            Value::Str(s) => write!(f, "{s}"),
232            Value::Bytes(b) => write!(f, "bytes[{}]", b.len()),
233        }
234    }
235}
236
237impl From<i64> for Value {
238    fn from(v: i64) -> Self {
239        Value::Int(v)
240    }
241}
242impl From<bool> for Value {
243    fn from(v: bool) -> Self {
244        Value::Bool(v)
245    }
246}
247impl From<f64> for Value {
248    fn from(v: f64) -> Self {
249        Value::Float(v)
250    }
251}
252impl From<Message> for Value {
253    fn from(m: Message) -> Self {
254        Value::Message(m)
255    }
256}
257impl From<&str> for Value {
258    fn from(s: &str) -> Self {
259        Value::str(s)
260    }
261}
262impl From<String> for Value {
263    fn from(s: String) -> Self {
264        Value::Str(Arc::from(s))
265    }
266}
267impl From<&[u8]> for Value {
268    fn from(b: &[u8]) -> Self {
269        Value::bytes(b)
270    }
271}
272impl From<Vec<u8>> for Value {
273    fn from(b: Vec<u8>) -> Self {
274        Value::Bytes(Arc::from(b))
275    }
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281
282    #[test]
283    fn message_is_truthy_and_round_trips_helpers() {
284        let m = Message::new(7, 99, 10, 1);
285        let v = Value::Message(m);
286        assert!(v.is_truthy());
287        assert_eq!(v.as_message(), Some(m));
288        assert_eq!(v.type_name(), "message");
289    }
290
291    #[test]
292    fn authenticate_stamps_sender_and_reply_cap() {
293        let m = Message::new(999, 1, 2, 3).authenticate(42, 7);
294        assert_eq!(m.sender, 42);
295        assert_eq!(m.reply_cap, 7);
296        assert_eq!(m.request_id, 1);
297    }
298
299    #[test]
300    fn cap_is_truthy() {
301        assert!(Value::Cap(1).is_truthy());
302        assert_eq!(Value::Cap(3).as_cap(), Some(3));
303    }
304
305    #[test]
306    fn str_and_bytes_helpers() {
307        let s = Value::str("hi");
308        assert_eq!(s.as_str(), Some("hi"));
309        assert_eq!(s.type_name(), "str");
310        assert!(s.is_truthy());
311        assert!(!Value::str("").is_truthy());
312
313        let b = Value::bytes([1u8, 2, 3]);
314        assert_eq!(b.as_bytes(), Some(&[1, 2, 3][..]));
315        assert_eq!(b.type_name(), "bytes");
316        assert!(b.is_truthy());
317        assert!(!Value::bytes([]).is_truthy());
318
319        // Str also exposes UTF-8 bytes via as_bytes.
320        assert_eq!(s.as_bytes(), Some(b"hi".as_slice()));
321    }
322
323    #[test]
324    fn str_eq_compares_content() {
325        assert_eq!(Value::str("a"), Value::from("a".to_owned()));
326        assert_ne!(Value::str("a"), Value::str("b"));
327        assert_eq!(Value::bytes([9]), Value::from(vec![9u8]));
328    }
329}