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