Skip to main content

byteflow/bytecode/
value.rs

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