Skip to main content

byteflow/vm/
machine.rs

1use std::sync::Arc;
2use std::time::Duration;
3
4use crate::bytecode::{Chunk, Instruction, Opcode, Value};
5use crate::scheduler::FlowQuota;
6
7use super::fault::Fault;
8use super::frame::Frame;
9use super::native::{check_native_gate, NativeGate, NativeTable};
10use super::result::VmResult;
11
12/// Hard limit on call nesting. Frames are heap-allocated, so
13/// unbounded recursion would grow the Flow's memory instead of crashing
14/// the worker thread's native stack — which is worse, not better, without a
15/// limit. `4096` comfortably covers real recursive algorithms while keeping
16/// a runaway `fn f() { f() }` a `Fault`, not an OOM.
17pub const MAX_CALL_DEPTH: usize = 4096;
18
19/// One virtual Flow's execution state: call stack + registers. Cheap
20/// enough to construct that spawning a Flow is a handful of small heap
21/// allocations, not a native thread/stack (contrast: a `std::thread` reserves
22/// megabytes of stack whether it uses them or not).
23///
24/// `Vm` owns no scheduler, mailbox, or thread handle — see [`VmResult`] for
25/// why that separation is the whole point.
26pub struct Vm {
27    chunk: Arc<Chunk>,
28    natives: Arc<NativeTable>,
29    native_gate: NativeGate,
30    frames: Vec<Frame>,
31    /// Lifetime instruction counter, exposed for `FlowMetrics` (design
32    /// notes §26).
33    instructions_executed: u64,
34    /// Next `Message.request_id` for [`Opcode::FreshRequestId`] and for
35    /// hops that still carry `0` (“unset”) at the Send/Ask boundary.
36    next_request_id: u64,
37    /// Interim heap charge for `Str`/`Bytes` written into registers.
38    quota: Option<Arc<FlowQuota>>,
39}
40
41impl Vm {
42    /// Construct a `Vm` ready to run `function` (an index into
43    /// `chunk.functions`) with the given arguments loaded into `r0..argc`.
44    /// `natives` is the FFI table `Opcode::CallNative` dispatches through —
45    /// pass [`NativeTable::empty`] if the chunk never calls out to Rust.
46    ///
47    /// Native calls are **denied** until [`Self::with_native_gate`] installs
48    /// an allowlist (the runtime does this from the flow's attenuated Cap).
49    pub fn new(chunk: Arc<Chunk>, natives: Arc<NativeTable>, function: u32, args: &[Value]) -> Result<Self, Fault> {
50        let gate = NativeGate::deny(natives.len());
51        Self::with_native_gate(chunk, natives, gate, function, args)
52    }
53
54    pub fn with_native_gate(
55        chunk: Arc<Chunk>,
56        natives: Arc<NativeTable>,
57        native_gate: NativeGate,
58        function: u32,
59        args: &[Value],
60    ) -> Result<Self, Fault> {
61        let def = chunk
62            .function(function)
63            .ok_or(Fault::BadFunction { index: function, table_size: chunk.functions.len() as u32 })?;
64        let mut frame = Frame::new(function, def.num_registers, None);
65        frame.pc = def.entry as usize;
66        for (i, arg) in args.iter().enumerate().take(def.arity as usize) {
67            match frame.registers.get_mut(i) {
68                Some(slot) => *slot = arg.clone(),
69                None => {
70                    return Err(Fault::RegisterOutOfRange {
71                        reg: i as u8,
72                        frame_size: def.num_registers,
73                    })
74                }
75            }
76        }
77        Ok(Vm {
78            chunk,
79            natives,
80            native_gate,
81            frames: vec![frame],
82            instructions_executed: 0,
83            next_request_id: 1,
84            quota: None,
85        })
86    }
87
88    /// Attach the flow's quota so register stores of `Str`/`Bytes` charge heap.
89    ///
90    /// Interim: charge on write, never release until the flow exits. Same
91    /// `Arc` rewritten into the same slot is not charged twice.
92    pub fn set_quota(&mut self, quota: Arc<FlowQuota>) -> Result<(), Fault> {
93        for frame in &self.frames {
94            for slot in &frame.registers {
95                charge_heap_value(&quota, slot)?;
96            }
97        }
98        self.quota = Some(quota);
99        Ok(())
100    }
101
102    /// Mint a per-flow correlation id. Never returns `0`.
103    pub fn fresh_request_id(&mut self) -> u64 {
104        let id = self.next_request_id;
105        self.next_request_id = self.next_request_id.saturating_add(1);
106        if id == 0 {
107            return self.fresh_request_id();
108        }
109        id
110    }
111
112    pub fn instructions_executed(&self) -> u64 {
113        self.instructions_executed
114    }
115
116    /// Index into `chunk.functions` for the active (top) call frame.
117    /// Useful for diagnostics / supervisor logs when a Flow traps.
118    pub fn current_function(&self) -> u32 {
119        // Category D: frames must be non-empty while the VM is runnable.
120        // We still avoid `.expect` — return 0 as a diagnostic fallback so a
121        // broken invariant cannot panic a worker; the next `run` will trap.
122        debug_assert!(!self.frames.is_empty(), "frames empty while running");
123        match self.frames.last() {
124            Some(frame) => frame.function,
125            None => 0,
126        }
127    }
128
129    /// A cheap `Arc` clone of the chunk this VM is executing. Used by the
130    /// scheduler to construct a child `Vm` for `Opcode::Spawn` without
131    /// needing to know anything about `Chunk`'s internals — every Flow
132    /// spawned (transitively) from the same top-level `spawn()` call shares
133    /// one immutable chunk in memory, never copies it.
134    pub fn chunk_arc(&self) -> Arc<Chunk> {
135        self.chunk.clone()
136    }
137
138    /// A cheap `Arc` clone of this VM's native function table, for the same
139    /// reason as [`Vm::chunk_arc`]: a `Spawn`-created child must dispatch
140    /// `CallNative` through the identical table its parent uses.
141    pub fn natives_arc(&self) -> Arc<NativeTable> {
142        self.natives.clone()
143    }
144
145    /// Deliver a value the scheduler produced on our behalf (a **Cap** from
146    /// `Spawn` / `SelfPid`, or a dequeued mailbox message from a `Receive`)
147    /// into the register the instruction that suspended us was targeting,
148    /// ahead of the next [`Vm::run`] call. A no-op is never valid to skip:
149    /// calling `run` without this after a `Spawn`/`Receive` result leaves the
150    /// destination register holding its previous (stale) value.
151    #[inline]
152    pub fn resume_with(&mut self, dest_reg: u8, value: Value) -> Result<(), Fault> {
153        self.set_reg(dest_reg, value)
154    }
155
156    /// Program counter of the active frame — used by the optional JIT hook.
157    pub fn current_pc(&self) -> Option<usize> {
158        self.frames.last().map(|f| f.pc)
159    }
160
161    /// Number of registers in the active frame.
162    pub fn current_num_registers(&self) -> Option<u8> {
163        self.frames.last().map(|f| f.registers.len() as u8)
164    }
165
166    /// Read-only view of the active register file.
167    pub fn top_registers(&self) -> Option<&[Value]> {
168        self.frames.last().map(|f| f.registers.as_slice())
169    }
170
171    /// Mutable view of the active register file (JIT sync path).
172    pub fn top_registers_mut(&mut self) -> Option<&mut [Value]> {
173        self.frames.last_mut().map(|f| f.registers.as_mut_slice())
174    }
175
176    /// Set the active frame's program counter.
177    pub fn set_pc(&mut self, pc: usize) {
178        if let Some(frame) = self.frames.last_mut() {
179            frame.pc = pc;
180        }
181    }
182
183    /// Write one register in the active frame (JIT sync path).
184    pub fn set_register(&mut self, reg: u8, value: Value) -> Result<(), Fault> {
185        self.set_reg(reg, value)
186    }
187
188    /// Deliver a return value through the call stack.
189    pub fn return_value(&mut self, value: Value) -> Result<Option<VmResult>, Fault> {
190        self.pop_frame(value)
191    }
192
193    /// Top call frame. Empty stack is a broken invariant (category D) —
194    /// returned as [`Fault::Invariant`], never as `unwrap`/`expect`.
195    #[inline]
196    fn current(&mut self) -> Result<&mut Frame, Fault> {
197        debug_assert!(!self.frames.is_empty(), "frames empty while running");
198        self.frames
199            .last_mut()
200            .ok_or(Fault::Invariant("empty frame stack while running"))
201    }
202
203    #[inline]
204    fn get_reg(&self, reg: u8) -> Result<Value, Fault> {
205        let frame = self
206            .frames
207            .last()
208            .ok_or(Fault::Invariant("empty frame stack while running"))?;
209        frame
210            .registers
211            .get(reg as usize)
212            .cloned()
213            .ok_or(Fault::RegisterOutOfRange {
214                reg,
215                frame_size: frame.registers.len() as u8,
216            })
217    }
218
219    #[inline]
220    fn set_reg(&mut self, reg: u8, value: Value) -> Result<(), Fault> {
221        self.charge_register_store(reg, &value)?;
222        let frame = self.current()?;
223        let len = frame.registers.len() as u8;
224        match frame.registers.get_mut(reg as usize) {
225            Some(slot) => {
226                *slot = value;
227                Ok(())
228            }
229            None => Err(Fault::RegisterOutOfRange { reg, frame_size: len }),
230        }
231    }
232
233    #[inline]
234    fn peek_reg(&self, reg: u8) -> Option<&Value> {
235        self.frames.last()?.registers.get(reg as usize)
236    }
237
238    /// Interim heap charge: `Str` / `Bytes` only. Hop payloads are charged
239    /// at `Send` / `Ask`. Overwrites of the same `Arc` in the same slot
240    /// are skipped; other copies super-count until the flow exits.
241    fn charge_register_store(&self, reg: u8, value: &Value) -> Result<(), Fault> {
242        let Some(quota) = self.quota.as_ref() else {
243            return Ok(());
244        };
245        match (value, self.peek_reg(reg)) {
246            (Value::Str(s), Some(Value::Str(old))) if Arc::ptr_eq(s, old) => Ok(()),
247            (Value::Bytes(b), Some(Value::Bytes(old))) if Arc::ptr_eq(b, old) => Ok(()),
248            (Value::Str(_) | Value::Bytes(_), _) => charge_heap_value(quota, value),
249            _ => Ok(()),
250        }
251    }
252
253    /// Fetch the next instruction and advance `pc`.
254    ///
255    /// `Ok(None)` if control fell off the end of the function body without an
256    /// explicit `Return`/`Halt` — treated as an implicit `Return Unit`
257    /// (friendlier to hand-written bytecode that omits a trailing return).
258    /// `Err` if the frame stack is empty (invariant break).
259    ///
260    /// Borrows are split on purpose: hold `pc`, look up `chunk.code`, then
261    /// write `pc+1` — a single `&mut Frame` across `self.chunk` would not
262    /// compile.
263    #[inline]
264    fn fetch(&mut self) -> Result<Option<Instruction>, Fault> {
265        let pc = self.current()?.pc;
266        let instr = self.chunk.code.get(pc).copied();
267        if instr.is_some() {
268            self.current()?.pc = pc + 1;
269        }
270        Ok(instr)
271    }
272
273    fn numeric_binop(&mut self, op: Opcode, dst: u8, lhs: u8, rhs: u8) -> Result<(), Fault> {
274        let a = self.get_reg(lhs)?;
275        let b = self.get_reg(rhs)?;
276        let result = match (op, &a, &b) {
277            (Opcode::Add, Value::Int(x), Value::Int(y)) => Value::Int(x.wrapping_add(*y)),
278            (Opcode::Add, _, _) => Value::Float(as_f64(&a)? + as_f64(&b)?),
279            (Opcode::Sub, Value::Int(x), Value::Int(y)) => Value::Int(x.wrapping_sub(*y)),
280            (Opcode::Sub, _, _) => Value::Float(as_f64(&a)? - as_f64(&b)?),
281            (Opcode::Mul, Value::Int(x), Value::Int(y)) => Value::Int(x.wrapping_mul(*y)),
282            (Opcode::Mul, _, _) => Value::Float(as_f64(&a)? * as_f64(&b)?),
283            (Opcode::Div, Value::Int(x), Value::Int(y)) => {
284                if *y == 0 {
285                    return Err(Fault::DivideByZero);
286                }
287                Value::Int(x.wrapping_div(*y))
288            }
289            (Opcode::Div, _, _) => {
290                let denom = as_f64(&b)?;
291                Value::Float(as_f64(&a)? / denom)
292            }
293            (Opcode::Mod, Value::Int(x), Value::Int(y)) => {
294                if *y == 0 {
295                    return Err(Fault::DivideByZero);
296                }
297                Value::Int(x.wrapping_rem(*y))
298            }
299            (Opcode::Eq, _, _) => Value::Bool(a == b),
300            (Opcode::Lt, Value::Int(x), Value::Int(y)) => Value::Bool(x < y),
301            (Opcode::Lt, _, _) => Value::Bool(as_f64(&a)? < as_f64(&b)?),
302            (Opcode::Le, Value::Int(x), Value::Int(y)) => Value::Bool(x <= y),
303            (Opcode::Le, _, _) => Value::Bool(as_f64(&a)? <= as_f64(&b)?),
304            _ => {
305                return Err(Fault::Invariant(
306                    "numeric_binop called with a non-arithmetic opcode",
307                ))
308            }
309        };
310        self.set_reg(dst, result)
311    }
312
313    /// Run at most `budget` instructions (cooperative-preemption quantum,
314    /// design notes §10-11), or until the Flow completes / needs an
315    /// effect the scheduler must perform / faults.
316    ///
317    /// Every exit path is captured by [`VmResult`] — this function itself
318    /// never panics on malformed *verified* bytecode; faults are returned,
319    /// not thrown, so a buggy Flow can't take a worker thread down.
320    pub fn run(&mut self, budget: u32) -> VmResult {
321        for _ in 0..budget {
322            self.instructions_executed += 1;
323            let instr = match self.fetch() {
324                Ok(Some(i)) => i,
325                Ok(None) => {
326                    // Fell off the end of a function: implicit `return Unit`.
327                    match self.pop_frame(Value::Unit) {
328                        Ok(Some(result)) => return result,
329                        Ok(None) => continue,
330                        Err(fault) => return VmResult::Trap(fault),
331                    }
332                }
333                Err(fault) => return VmResult::Trap(fault),
334            };
335
336            macro_rules! trap {
337                ($e:expr) => {
338                    match $e {
339                        Ok(v) => v,
340                        Err(fault) => return VmResult::Trap(fault),
341                    }
342                };
343            }
344
345            match instr.op {
346                Opcode::Halt => {
347                    let v = trap!(self.get_reg(0));
348                    return VmResult::Complete(v);
349                }
350                Opcode::Nop => {}
351                Opcode::LoadConst => {
352                    let idx = instr.imm as u32;
353                    let konst = match self.chunk.constant(idx) {
354                        Some(v) => v.clone(),
355                        None => {
356                            return VmResult::Trap(Fault::BadConstant {
357                                index: idx,
358                                pool_size: self.chunk.constants.len() as u32,
359                            })
360                        }
361                    };
362                    trap!(self.set_reg(instr.a, konst));
363                }
364                Opcode::LoadImm => {
365                    trap!(self.set_reg(instr.a, Value::Int(instr.imm as i64)));
366                }
367                Opcode::Move => {
368                    let v = trap!(self.get_reg(instr.b));
369                    trap!(self.set_reg(instr.a, v));
370                }
371                Opcode::Add | Opcode::Sub | Opcode::Mul | Opcode::Div | Opcode::Mod
372                | Opcode::Eq | Opcode::Lt | Opcode::Le => {
373                    trap!(self.numeric_binop(instr.op, instr.a, instr.b, instr.c));
374                }
375                Opcode::Neg => {
376                    let v = trap!(self.get_reg(instr.b));
377                    let negated = match v {
378                        Value::Int(x) => Value::Int(-x),
379                        Value::Float(x) => Value::Float(-x),
380                        other => {
381                            return VmResult::Trap(Fault::TypeMismatch {
382                                expected: "int or float",
383                                got: other.type_name(),
384                            })
385                        }
386                    };
387                    trap!(self.set_reg(instr.a, negated));
388                }
389                Opcode::Jump => {
390                    let frame = trap!(self.current());
391                    let target = frame.pc as i64 + instr.imm as i64;
392                    frame.pc = target as usize;
393                }
394                Opcode::Branch => {
395                    let cond = trap!(self.get_reg(instr.a));
396                    if !cond.is_truthy() {
397                        let frame = trap!(self.current());
398                        let target = frame.pc as i64 + instr.imm as i64;
399                        frame.pc = target as usize;
400                    }
401                }
402                Opcode::Call => {
403                    let function = instr.imm as u32;
404                    let argc = instr.b;
405                    let dst = instr.a;
406                    if self.frames.len() >= MAX_CALL_DEPTH {
407                        return VmResult::Trap(Fault::CallStackOverflow { depth: self.frames.len() });
408                    }
409                    let def = match self.chunk.function(function) {
410                        Some(d) => d.clone(),
411                        None => {
412                            return VmResult::Trap(Fault::BadFunction {
413                                index: function,
414                                table_size: self.chunk.functions.len() as u32,
415                            })
416                        }
417                    };
418                    let mut args = Vec::with_capacity(argc as usize);
419                    for i in 0..argc {
420                        args.push(trap!(self.get_reg(trap!(reg_at(dst, u16::from(i))))));
421                    }
422                    let mut new_frame = Frame::new(function, def.num_registers, Some(dst));
423                    new_frame.pc = def.entry as usize;
424                    // See `Vm::new` on why this is a checked write.
425                    for (i, a) in args.into_iter().enumerate().take(def.arity as usize) {
426                        match new_frame.registers.get_mut(i) {
427                            Some(slot) => *slot = a,
428                            None => {
429                                return VmResult::Trap(Fault::RegisterOutOfRange {
430                                    reg: i as u8,
431                                    frame_size: def.num_registers,
432                                })
433                            }
434                        }
435                    }
436                    self.frames.push(new_frame);
437                }
438                Opcode::CallNative => {
439                    let native_index = instr.imm as u32;
440                    let argc = instr.b;
441                    let dst = instr.a;
442                    if let Err(err) = check_native_gate(&self.native_gate, &self.natives, native_index)
443                    {
444                        return VmResult::Trap(match err {
445                            crate::vm::native::NativeCallError::IndexOutOfRange(index) => {
446                                Fault::BadNative {
447                                    index,
448                                    table_size: self.natives.len() as u32,
449                                }
450                            }
451                            other => Fault::NativeDenied(other.to_string()),
452                        });
453                    }
454                    let native_fn = match self.natives.get(native_index) {
455                        Some(f) => f.clone(),
456                        None => {
457                            return VmResult::Trap(Fault::BadNative {
458                                index: native_index,
459                                table_size: self.natives.len() as u32,
460                            })
461                        }
462                    };
463                    let mut args = Vec::with_capacity(argc as usize);
464                    for i in 0..argc {
465                        args.push(trap!(self.get_reg(trap!(reg_at(dst, u16::from(i))))));
466                    }
467                    // Runs inline on this worker thread — see
468                    // `NativeFn`'s doc comment on why natives must not
469                    // block. This is the actual FFI boundary (design
470                    // notes §30-31): plain Rust on one side, bytecode
471                    // registers on the other, with `Fault::NativeError`
472                    // as the only channel for a native-side failure to
473                    // become a Flow fault instead of a host panic.
474                    match native_fn(&args) {
475                        Ok(value) => trap!(self.set_reg(dst, value)),
476                        Err(fault) => return VmResult::Trap(fault),
477                    }
478                }
479                Opcode::Return => {
480                    let v = trap!(self.get_reg(instr.a));
481                    match self.pop_frame(v) {
482                        Ok(Some(result)) => return result,
483                        Ok(None) => {}
484                        Err(fault) => return VmResult::Trap(fault),
485                    }
486                }
487                Opcode::Spawn => {
488                    let argc = instr.b;
489                    let mut args = Vec::with_capacity(argc as usize);
490                    for i in 0..argc {
491                        // Args live at r[a+1 .. a+1+argc] — deliberately
492                        // offset from `a` itself, which the scheduler will
493                        // overwrite with a Cap to the child once it exists
494                        // (see Opcode::Spawn / FlowCap).
495                        //
496                        // The `+1` is why this one needs `reg_at` most: with
497                        // `a = 255` the very first index already leaves the
498                        // register space, before any bounds check gets a say.
499                        args.push(trap!(
500                            self.get_reg(trap!(reg_at(instr.a, u16::from(i) + 1)))
501                        ));
502                    }
503                    return VmResult::Spawn {
504                        function: instr.imm as u32,
505                        args,
506                        dest_reg: instr.a,
507                        requested_rights: crate::bytecode::CapRights::from_u8(instr.c),
508                    };
509                }
510                Opcode::Yield => return VmResult::Yield,
511                Opcode::Sleep => {
512                    let millis = trap!(self.get_reg(instr.a));
513                    let ms = match millis.as_int() {
514                        Some(ms) if ms >= 0 => ms as u64,
515                        _ => {
516                            return VmResult::Trap(Fault::TypeMismatch {
517                                expected: "non-negative int",
518                                got: millis.type_name(),
519                            })
520                        }
521                    };
522                    return VmResult::Sleep(Duration::from_millis(ms));
523                }
524                Opcode::Exit => {
525                    let v = trap!(self.get_reg(instr.a));
526                    return VmResult::Complete(v);
527                }
528                Opcode::SelfPid => {
529                    return VmResult::SelfPid { dest_reg: instr.a };
530                }
531                Opcode::Send => {
532                    let target = trap!(self.get_reg(instr.a));
533                    let message = trap!(self.get_reg(instr.b));
534                    let cap = match target.as_cap() {
535                        Some(c) => c,
536                        None => {
537                            return VmResult::Trap(Fault::TypeMismatch {
538                                expected: "cap",
539                                got: target.type_name(),
540                            })
541                        }
542                    };
543                    if message.as_message().is_none() {
544                        return VmResult::Trap(Fault::TypeMismatch {
545                            expected: "message",
546                            got: message.type_name(),
547                        });
548                    }
549                    return VmResult::Send {
550                        target_cap: cap,
551                        message,
552                    };
553                }
554                Opcode::Receive => {
555                    return VmResult::Receive {
556                        dest_reg: instr.a,
557                        timeout: None,
558                        match_tag: None,
559                        match_request_id: None,
560                    };
561                }
562                Opcode::ReceiveTimeout => {
563                    let millis = trap!(self.get_reg(instr.b));
564                    let ms = match millis.as_int() {
565                        Some(n) if n >= 0 => n as u64,
566                        _ => 0,
567                    };
568                    return VmResult::Receive {
569                        dest_reg: instr.a,
570                        timeout: Some(Duration::from_millis(ms)),
571                        match_tag: None,
572                        match_request_id: None,
573                    };
574                }
575                Opcode::ReceiveMatch => {
576                    let tag_v = trap!(self.get_reg(instr.b));
577                    let tag = match tag_from_value(&tag_v) {
578                        Ok(t) => t,
579                        Err(f) => return VmResult::Trap(f),
580                    };
581                    return VmResult::Receive {
582                        dest_reg: instr.a,
583                        timeout: None,
584                        match_tag: Some(tag),
585                        match_request_id: None,
586                    };
587                }
588                Opcode::ReceiveMatchImm => {
589                    let tag = match u16::try_from(instr.imm) {
590                        Ok(t) if instr.imm >= 0 => t,
591                        _ => {
592                            return VmResult::Trap(Fault::TypeMismatch {
593                                expected: "tag u16",
594                                got: "imm-out-of-range",
595                            })
596                        }
597                    };
598                    return VmResult::Receive {
599                        dest_reg: instr.a,
600                        timeout: None,
601                        match_tag: Some(tag),
602                        match_request_id: None,
603                    };
604                }
605                Opcode::FreshRequestId => {
606                    let id = self.fresh_request_id();
607                    trap!(self.set_reg(instr.a, Value::Int(id as i64)));
608                }
609                Opcode::ReceiveMatchCorr => {
610                    let tag_v = trap!(self.get_reg(instr.b));
611                    let tag = match tag_from_value(&tag_v) {
612                        Ok(t) => t,
613                        Err(f) => return VmResult::Trap(f),
614                    };
615                    let id_v = trap!(self.get_reg(instr.c));
616                    let rid = match request_id_from_value(&id_v) {
617                        Ok(id) => id,
618                        Err(f) => return VmResult::Trap(f),
619                    };
620                    return VmResult::Receive {
621                        dest_reg: instr.a,
622                        timeout: None,
623                        match_tag: Some(tag),
624                        match_request_id: Some(rid),
625                    };
626                }
627                Opcode::ReceiveMatchCorrImm => {
628                    let tag = match u16::try_from(instr.imm) {
629                        Ok(t) if instr.imm >= 0 => t,
630                        _ => {
631                            return VmResult::Trap(Fault::TypeMismatch {
632                                expected: "tag u16",
633                                got: "imm-out-of-range",
634                            })
635                        }
636                    };
637                    let id_v = trap!(self.get_reg(instr.b));
638                    let rid = match request_id_from_value(&id_v) {
639                        Ok(id) => id,
640                        Err(f) => return VmResult::Trap(f),
641                    };
642                    return VmResult::Receive {
643                        dest_reg: instr.a,
644                        timeout: None,
645                        match_tag: Some(tag),
646                        match_request_id: Some(rid),
647                    };
648                }
649                Opcode::Ask => {
650                    let target = trap!(self.get_reg(instr.b));
651                    let request = trap!(self.get_reg(instr.c));
652                    let cap = match target.as_cap() {
653                        Some(c) => c,
654                        None => {
655                            return VmResult::Trap(Fault::TypeMismatch {
656                                expected: "cap",
657                                got: target.type_name(),
658                            })
659                        }
660                    };
661                    if request.as_message().is_none() {
662                        return VmResult::Trap(Fault::TypeMismatch {
663                            expected: "message",
664                            got: request.type_name(),
665                        });
666                    }
667                    return VmResult::Ask {
668                        dest_reg: instr.a,
669                        target_cap: cap,
670                        request,
671                        timeout: None,
672                    };
673                }
674                Opcode::AskTimeout => {
675                    let target = trap!(self.get_reg(instr.b));
676                    let request = trap!(self.get_reg(instr.c));
677                    let millis_reg = match u8::try_from(instr.imm) {
678                        Ok(r) => r,
679                        Err(_) => {
680                            return VmResult::Trap(Fault::TypeMismatch {
681                                expected: "millis register",
682                                got: "imm-out-of-range",
683                            })
684                        }
685                    };
686                    let millis = trap!(self.get_reg(millis_reg));
687                    let ms = match millis.as_int() {
688                        Some(n) if n >= 0 => n as u64,
689                        _ => 0,
690                    };
691                    let cap = match target.as_cap() {
692                        Some(c) => c,
693                        None => {
694                            return VmResult::Trap(Fault::TypeMismatch {
695                                expected: "cap",
696                                got: target.type_name(),
697                            })
698                        }
699                    };
700                    if request.as_message().is_none() {
701                        return VmResult::Trap(Fault::TypeMismatch {
702                            expected: "message",
703                            got: request.type_name(),
704                        });
705                    }
706                    return VmResult::Ask {
707                        dest_reg: instr.a,
708                        target_cap: cap,
709                        request,
710                        timeout: Some(Duration::from_millis(ms)),
711                    };
712                }
713                Opcode::Monitor => {
714                    let target = trap!(self.get_reg(instr.b));
715                    let cap = match target.as_cap() {
716                        Some(c) => c,
717                        None => {
718                            return VmResult::Trap(Fault::TypeMismatch {
719                                expected: "cap",
720                                got: target.type_name(),
721                            })
722                        }
723                    };
724                    return VmResult::Monitor {
725                        dest_reg: instr.a,
726                        target_cap: cap,
727                    };
728                }
729                Opcode::Demonitor => {
730                    return VmResult::Demonitor {
731                        monitor_reg: instr.a,
732                    };
733                }
734                Opcode::Link => {
735                    let target = trap!(self.get_reg(instr.b));
736                    let cap = match target.as_cap() {
737                        Some(c) => c,
738                        None => {
739                            return VmResult::Trap(Fault::TypeMismatch {
740                                expected: "cap",
741                                got: target.type_name(),
742                            })
743                        }
744                    };
745                    return VmResult::Link {
746                        dest_reg: instr.a,
747                        target_cap: cap,
748                    };
749                }
750                Opcode::Unlink => {
751                    return VmResult::Unlink {
752                        link_reg: instr.a,
753                    };
754                }
755                Opcode::RegisterName => {
756                    let name = trap!(self.get_reg(instr.a));
757                    match name {
758                        Value::Str(s) => {
759                            return VmResult::RegisterName { name: s };
760                        }
761                        other => {
762                            return VmResult::Trap(Fault::TypeMismatch {
763                                expected: "str",
764                                got: other.type_name(),
765                            })
766                        }
767                    }
768                }
769                Opcode::Whereis => {
770                    let name = trap!(self.get_reg(instr.b));
771                    match name {
772                        Value::Str(s) => {
773                            return VmResult::Whereis {
774                                dest_reg: instr.a,
775                                name: s,
776                            };
777                        }
778                        other => {
779                            return VmResult::Trap(Fault::TypeMismatch {
780                                expected: "str",
781                                got: other.type_name(),
782                            })
783                        }
784                    }
785                }
786                Opcode::Delegate => {
787                    let src = trap!(self.get_reg(instr.b));
788                    let src_cap = match src.as_cap() {
789                        Some(c) => c,
790                        None => {
791                            return VmResult::Trap(Fault::TypeMismatch {
792                                expected: "cap",
793                                got: src.type_name(),
794                            })
795                        }
796                    };
797                    let want_native_cap = if instr.c == 255 {
798                        None
799                    } else {
800                        let v = trap!(self.get_reg(instr.c));
801                        match v.as_cap() {
802                            Some(c) => Some(c),
803                            None => {
804                                return VmResult::Trap(Fault::TypeMismatch {
805                                    expected: "cap",
806                                    got: v.type_name(),
807                                })
808                            }
809                        }
810                    };
811                    return VmResult::Delegate {
812                        dest_reg: instr.a,
813                        src_cap,
814                        want_rights: crate::bytecode::CapRights::from_bits(instr.imm as u32),
815                        want_native_cap,
816                    };
817                }
818                Opcode::Trap => return VmResult::Trap(Fault::Explicit(instr.imm)),
819            }
820        }
821        VmResult::Yield
822    }
823
824    /// Pop the current frame, delivering `value` to the caller (or
825    /// finishing the Flow if this was the outermost frame). Returns
826    /// `Ok(Some(VmResult::Complete(_)))` only in the latter case.
827    fn pop_frame(&mut self, value: Value) -> Result<Option<VmResult>, Fault> {
828        let finished = self
829            .frames
830            .pop()
831            .ok_or(Fault::Invariant("pop_frame on empty stack"))?;
832        match finished.dest_reg {
833            Some(dest) => {
834                // Caller frame still on the stack; ignore an out-of-range
835                // dest (shouldn't happen for verified bytecode emitted by
836                // this crate's own builder, but we don't want to panic on
837                // foreign bytecode) by trapping instead.
838                if self.set_reg(dest, value).is_err() {
839                    let frame_size = match self.frames.last() {
840                        Some(f) => f.registers.len() as u8,
841                        None => 0,
842                    };
843                    return Ok(Some(VmResult::Trap(Fault::RegisterOutOfRange {
844                        reg: dest,
845                        frame_size,
846                    })));
847                }
848                Ok(None)
849            }
850            None => Ok(Some(VmResult::Complete(value))),
851        }
852    }
853}
854
855/// The register index `base + offset`, or [`Fault::RegisterIndexOverflow`]
856/// if that sum leaves the register index space.
857///
858/// Every multi-operand opcode gathers its arguments from consecutive
859/// registers. Written as a plain `base + offset` on `u8`, that addition
860/// panics in debug and **wraps** in release — so a release build silently
861/// reads the wrong register instead of failing, which is the worst of the
862/// two outcomes and the one a debug-mode test suite never sees. The sum is
863/// therefore computed in a wider type and narrowed explicitly.
864#[inline]
865fn reg_at(base: u8, offset: u16) -> Result<u8, Fault> {
866    match u8::try_from(u32::from(base) + u32::from(offset)) {
867        Ok(reg) => Ok(reg),
868        Err(_) => Err(Fault::RegisterIndexOverflow {
869            base,
870            offset: match u8::try_from(offset) {
871                Ok(o) => o,
872                Err(_) => u8::MAX,
873            },
874        }),
875    }
876}
877
878/// Charge `Str` / `Bytes` length against the flow heap quota.
879/// Empty buffers are free. Fail-closed: the store does not happen on error.
880fn charge_heap_value(quota: &FlowQuota, value: &Value) -> Result<(), Fault> {
881    let bytes = match value {
882        Value::Str(s) => s.len(),
883        Value::Bytes(b) => b.len(),
884        _ => return Ok(()),
885    };
886    if bytes == 0 {
887        return Ok(());
888    }
889    quota
890        .alloc(bytes)
891        .map_err(|e| Fault::QuotaExceeded(e.to_string()))
892}
893
894fn as_f64(v: &Value) -> Result<f64, Fault> {
895    match v {
896        Value::Int(i) => Ok(*i as f64),
897        Value::Float(f) => Ok(*f),
898        other => Err(Fault::TypeMismatch { expected: "int or float", got: other.type_name() }),
899    }
900}
901
902/// Decode a Message tag from a register value (`Int` in `0..=u16::MAX`).
903#[inline]
904fn tag_from_value(v: &Value) -> Result<u16, Fault> {
905    match v.as_int() {
906        Some(i) if (0..=i64::from(u16::MAX)).contains(&i) => Ok(i as u16),
907        Some(_) => Err(Fault::TypeMismatch {
908            expected: "tag u16",
909            got: "int-out-of-range",
910        }),
911        None => Err(Fault::TypeMismatch {
912            expected: "int",
913            got: v.type_name(),
914        }),
915    }
916}
917
918/// Decode a `request_id` from a register (`Int` in `0..=i64::MAX`).
919#[inline]
920fn request_id_from_value(v: &Value) -> Result<u64, Fault> {
921    match v.as_int() {
922        Some(i) if i >= 0 => Ok(i as u64),
923        Some(_) => Err(Fault::TypeMismatch {
924            expected: "request_id u64",
925            got: "negative-int",
926        }),
927        None => Err(Fault::TypeMismatch {
928            expected: "int",
929            got: v.type_name(),
930        }),
931    }
932}
933
934#[cfg(test)]
935mod tests {
936    use super::*;
937    use crate::bytecode::builder::ChunkBuilder;
938
939    type TestResult = Result<(), Box<dyn std::error::Error>>;
940
941    /// `Spawn a=255` reads its arguments from `a+1`, so the first index
942    /// already leaves the register space. This used to panic in debug and —
943    /// far worse — wrap around to `r0` in release, silently spawning with the
944    /// wrong argument.
945    #[test]
946    fn spawn_from_the_last_register_traps_instead_of_wrapping() -> TestResult {
947        let mut b = ChunkBuilder::new("t");
948        b.begin_function("main", 0, 2);
949        b.emit_spawn(255, 0, 1);
950        b.emit_return(0);
951        let mut vm = Vm::new(Arc::new(b.finish()), NativeTable::empty(), 0, &[])?;
952        match vm.run(10) {
953            VmResult::Trap(Fault::RegisterIndexOverflow {
954                base: 255,
955                offset: 1,
956            }) => Ok(()),
957            other => Err(format!("expected RegisterIndexOverflow, got {other:?}").into()),
958        }
959    }
960
961    /// `Vm::new` is public and reachable without `verify`, and its panic
962    /// landed on the caller's thread — the embedder's on `Runtime::spawn`, or
963    /// a worker's on bytecode `Spawn`, outside the `catch_unwind`.
964    #[test]
965    fn entering_a_function_with_too_few_registers_faults() -> TestResult {
966        let mut b = ChunkBuilder::new("t");
967        b.begin_function("main", 3, 1);
968        b.emit_return(0);
969        let args = [Value::Int(1), Value::Int(2), Value::Int(3)];
970        match Vm::new(Arc::new(b.finish()), NativeTable::empty(), 0, &args) {
971            Err(Fault::RegisterOutOfRange {
972                reg: 1,
973                frame_size: 1,
974            }) => Ok(()),
975            Err(e) => Err(format!("unexpected fault: {e}").into()),
976            Ok(_) => Err("three arguments cannot be loaded into one register".into()),
977        }
978    }
979
980    #[test]
981    fn calling_a_function_with_too_few_registers_traps() -> TestResult {
982        let mut b = ChunkBuilder::new("t");
983        let callee = b.begin_function("callee", 3, 1);
984        b.emit_return(0);
985        let main = b.begin_function("main", 0, 4);
986        b.emit_load_imm(0, 7);
987        b.emit_load_imm(1, 8);
988        b.emit_load_imm(2, 9);
989        b.emit_call(0, callee, 3);
990        b.emit_return(0);
991        let mut vm = Vm::new(Arc::new(b.finish()), NativeTable::empty(), main, &[])?;
992        match vm.run(50) {
993            VmResult::Trap(Fault::RegisterOutOfRange {
994                reg: 1,
995                frame_size: 1,
996            }) => Ok(()),
997            other => Err(format!("expected RegisterOutOfRange, got {other:?}").into()),
998        }
999    }
1000
1001    /// The boundary case that must keep working: gathering right up to the
1002    /// last register is legal, it is only going *past* it that faults.
1003    #[test]
1004    fn gathering_up_to_the_last_register_still_works() -> TestResult {
1005        let mut b = ChunkBuilder::new("t");
1006        let callee = b.begin_function("callee", 1, 1);
1007        b.emit_return(0);
1008        let main = b.begin_function("main", 0, 255);
1009        b.emit_load_imm(254, 5);
1010        // Argument at r254 — the highest index a 255-register frame has.
1011        b.emit_call(254, callee, 1);
1012        b.emit_return(254);
1013        let mut vm = Vm::new(Arc::new(b.finish()), NativeTable::empty(), main, &[])?;
1014        match vm.run(100) {
1015            VmResult::Complete(_) => Ok(()),
1016            other => Err(format!("expected completion, got {other:?}").into()),
1017        }
1018    }
1019}