Skip to main content

byteflow/bytecode/
value.rs

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