Skip to main content

byteflow/vm/
machine.rs

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