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
11pub const MAX_CALL_DEPTH: usize = 4096;
17
18pub struct Vm {
26 chunk: Arc<Chunk>,
27 natives: Arc<NativeTable>,
28 native_gate: NativeGate,
29 frames: Vec<Frame>,
30 instructions_executed: u64,
33}
34
35impl Vm {
36 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 pub fn current_function(&self) -> u32 {
87 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 pub fn chunk_arc(&self) -> Arc<Chunk> {
103 self.chunk.clone()
104 }
105
106 pub fn natives_arc(&self) -> Arc<NativeTable> {
110 self.natives.clone()
111 }
112
113 #[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 pub fn current_pc(&self) -> Option<usize> {
126 self.frames.last().map(|f| f.pc)
127 }
128
129 pub fn current_num_registers(&self) -> Option<u8> {
131 self.frames.last().map(|f| f.registers.len() as u8)
132 }
133
134 pub fn top_registers(&self) -> Option<&[Value]> {
136 self.frames.last().map(|f| f.registers.as_slice())
137 }
138
139 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 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 pub fn set_register(&mut self, reg: u8, value: Value) -> Result<(), Fault> {
153 self.set_reg(reg, value)
154 }
155
156 pub fn return_value(&mut self, value: Value) -> Result<Option<VmResult>, Fault> {
158 self.pop_frame(value)
159 }
160
161 #[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 #[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 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 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 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 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.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 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 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#[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#[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 #[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 #[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 #[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 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}