1#![no_std]
2
3#[macro_use]
4extern crate alloc;
5
6use alloc::{boxed::Box, string::{String, ToString}, vec::Vec};
7
8pub mod value;
9pub mod frame;
10pub mod memory;
11pub mod devices;
12pub mod interpreter;
13pub mod loader;
14pub mod region;
15pub mod builtins;
16pub mod debug;
17pub mod host;
18pub mod snapshot;
19pub mod aot;
20
21pub use polka::{Value, HANDLE_NONE};
22pub use polka::cartridge::read_pk;
23pub use value::{alloc_string, read_string};
24pub use devices::{Device, DeviceTable};
25pub use memory::Heap;
26pub use region::RegionTable;
27pub use builtins::{NativeCtx, NativeFn, NativeRegistry};
28pub use debug::{render_fn_label, DebugEvent, DebugSink};
29pub use host::Host;
30pub use aot::{AotHost, AotNatives, reachable_live_count};
31
32use frame::Frame;
33
34pub type AotFn = alloc::rc::Rc<dyn for<'a> Fn(&mut NativeCtx<'a>, &[Value], &[bool]) -> Result<(Value, bool), String>>;
35
36pub fn run(module: polka::Module, host: Host) -> Result<i64, String> {
37 let loaded = loader::load(module)?;
38 let mut vm = VirtualMachine::new();
39 host.install_into(&mut vm);
40 let v = vm.run_module(&loaded.module)?;
41 Ok(v.as_int())
42}
43
44pub struct VirtualMachine {
45 pub(crate) registers: Vec<u64>,
46 pub(crate) register_mask: Vec<u64>,
48 pub(crate) frames: Vec<Frame>,
49 pub(crate) pc: usize,
50 pub(crate) base_reg: usize,
51 pub(crate) current_func: usize,
52 pub(crate) heap: Heap,
53 pub(crate) handlers: Vec<HandlerFrame>,
54 pub(crate) halted: bool,
55 pub(crate) exit_code: Option<i64>,
56 pub(crate) dispatch_last_result: Option<u16>,
57 pub(crate) dispatch_last_env: Option<(u64, bool)>,
58 pub(crate) devices: DeviceTable,
59 pub(crate) resolved_constants: Vec<Vec<u64>>,
61 pub(crate) resolved_const_mask: Vec<Vec<u64>>,
62 pub(crate) string_const_handles: Vec<(u32, u32)>,
64 pub(crate) resolved_natives: Vec<Option<NativeFn>>,
65 pub(crate) aot_fns: alloc::collections::BTreeMap<alloc::string::String, AotFn>,
66 pub(crate) resolved_aot: Vec<Option<AotFn>>,
67 pub(crate) region_table: RegionTable,
68 pub(crate) natives: NativeRegistry,
69 pub(crate) debug_sink: Option<DebugSink>,
70 pub(crate) trace_filter: Option<Vec<bool>>,
71 pub(crate) trace_frames: bool,
72 pub(crate) fn_names: Vec<String>,
73 pub(crate) failing_pc: usize,
74 pub(crate) last_result_is_handle: bool,
75 pub(crate) int32_safe: bool,
76 pub(crate) module_table_raw: u64,
77 pub(crate) module_table_is_handle: bool,
78 pub(crate) steps: u64,
79 pub(crate) step_cap: u64,
80 pub(crate) static_names: Vec<String>,
81 pub(crate) trace_static_filter: Option<String>,
82 pub(crate) heap_check: bool,
83 pub(crate) profile: bool,
84 pub(crate) prof_ops: hashbrown::HashMap<&'static str, u64>,
85 pub(crate) prof_fns: hashbrown::HashMap<usize, u64>,
86 pub(crate) prof_fn_ops: hashbrown::HashMap<usize, hashbrown::HashMap<&'static str, u64>>,
87 pub(crate) yielded: bool,
88 pub(crate) yield_dest_abs: usize,
89 pub(crate) trace_out: Option<fn(&str)>,
91}
92
93pub struct HandlerFrame {
94 pub effect_id: u16,
95 pub dispatch_table_slot: Option<u32>,
96 pub dispatch_table_gen: u32,
97 pub cell_slot: u32,
98 pub cell_gen: u32,
99 pub cells_allocated: Vec<(u32, u32)>,
100 pub body_frame_index: Option<usize>,
101 pub pending_return_arm_fn: Option<usize>,
102 pub pending_return_arm_env: u64,
103 pub pending_return_arm_env_is_handle: bool,
104}
105
106impl HandlerFrame {
107 pub fn release_cells(
108 &self,
109 heap: &mut crate::memory::Heap,
110 regions: &mut crate::region::RegionTable,
111 ) -> Result<(), String> {
112 for (slot, generation) in &self.cells_allocated {
113 regions.forget(*slot, *generation);
114 if heap.is_live(*slot, *generation) {
115 heap.rc_dec(*slot, *generation)?;
116 }
117 }
118 Ok(())
119 }
120}
121
122pub mod cont_slot {
123 pub const SUSPEND_PC: usize = 0;
124 pub const SUSPEND_BASE: usize = 1;
125 pub const DEST_REG: usize = 2;
126 pub const ALIVE: usize = 3;
127 pub const SUSPEND_FUNC: usize = 4;
128 pub const DISPATCH_FN_ID: usize = 5;
129 pub const DISPATCH_ENV: usize = 6;
130 pub const REGS_SNAPSHOT_SLOT: usize = 7;
131 pub const REGS_COUNT: usize = 8;
132 pub const SIZE: usize = 9;
133
134 pub const INIT_MASK_WORD0: u64 =
136 (1u64 << DISPATCH_ENV) | (1u64 << REGS_SNAPSHOT_SLOT);
137}
138
139impl VirtualMachine {
140 pub fn new() -> Self {
141 let mut natives = NativeRegistry::new();
142 builtins::register_default_builtins(&mut natives);
143 Self {
144 registers: Vec::new(),
145 register_mask: Vec::new(),
146 frames: Vec::new(),
147 pc: 0,
148 base_reg: 0,
149 current_func: 0,
150 heap: Heap::new(),
151 handlers: Vec::new(),
152 halted: false,
153 exit_code: None,
154 dispatch_last_result: None,
155 dispatch_last_env: None,
156 devices: DeviceTable::new(),
157 resolved_constants: Vec::new(),
158 resolved_const_mask: Vec::new(),
159 string_const_handles: Vec::new(),
160 resolved_natives: Vec::new(),
161 aot_fns: alloc::collections::BTreeMap::new(),
162 resolved_aot: Vec::new(),
163 region_table: RegionTable::new(),
164 natives,
165 debug_sink: None,
166 trace_filter: None,
167 trace_frames: false,
168 fn_names: Vec::new(),
169 failing_pc: 0,
170 last_result_is_handle: false,
171 int32_safe: false,
172 module_table_raw: polka::HANDLE_NONE,
173 module_table_is_handle: false,
174 steps: 0,
175 step_cap: u64::MAX,
176 static_names: Vec::new(),
177 trace_static_filter: None,
178 heap_check: false,
179 profile: false,
180 trace_out: None,
181 prof_ops: hashbrown::HashMap::new(),
182 prof_fns: hashbrown::HashMap::new(),
183 prof_fn_ops: hashbrown::HashMap::new(),
184 yielded: false,
185 yield_dest_abs: 0,
186 }
187 }
188
189 pub fn with_static_names(mut self, names: Vec<String>) -> Self {
190 self.static_names = names;
191 self
192 }
193
194 pub fn with_step_cap(mut self, cap: u64) -> Self {
195 self.step_cap = cap;
196 self
197 }
198
199 pub fn with_heap_check(mut self, on: bool) -> Self {
200 self.heap_check = on;
201 self
202 }
203
204 pub fn steps(&self) -> u64 { self.steps }
206
207 pub fn halted(&self) -> bool { self.exit_code.is_some() }
208
209 pub fn exit_code(&self) -> Option<i64> { self.exit_code }
210
211 pub fn with_heap_trace(mut self, slot: Option<u32>, all: bool, out: fn(&str)) -> Self {
214 self.heap.set_trace(slot, all, out);
215 self
216 }
217
218 pub fn with_trace_out(mut self, out: fn(&str)) -> Self {
220 self.trace_out = Some(out);
221 self
222 }
223
224 pub fn with_profile(mut self, on: bool) -> Self {
225 self.profile = on;
226 self
227 }
228
229 pub fn with_trace_static(mut self, filter: Option<String>) -> Self {
230 self.trace_static_filter = filter;
231 self
232 }
233
234 pub fn with_trace_frames(mut self, on: bool) -> Self {
235 self.trace_frames = on;
236 self
237 }
238
239 pub fn with_trace_filter(mut self, bits: Vec<bool>) -> Self {
241 self.trace_filter = Some(bits);
242 self
243 }
244
245 pub fn with_debug_sink(mut self, sink: DebugSink) -> Self {
246 self.debug_sink = Some(sink);
247 self
248 }
249
250 pub fn with_fn_names(mut self, names: Vec<String>) -> Self {
251 self.fn_names = names;
252 self
253 }
254
255 pub(crate) fn emit_debug(&mut self, event: &DebugEvent) {
256 if let Some(sink) = &mut self.debug_sink {
257 sink(event, &self.fn_names);
258 }
259 }
260
261 pub fn profile_report(&self) -> String {
265 use core::fmt::Write;
266 let mut s = String::new();
267 if !self.profile { return s; }
268 let mut ops: Vec<_> = self.prof_ops.iter().collect();
269 ops.sort_by(|a, b| b.1.cmp(a.1));
270 let total: u64 = self.prof_ops.values().sum();
271 let _ = writeln!(s, "[profile] {} ops executed", total);
272 for (name, n) in ops {
273 let _ = writeln!(s, " {:>12} {:>6.1}% {}", n, *n as f64 * 100.0 / total.max(1) as f64, name);
274 }
275 let mut fns: Vec<_> = self.prof_fns.iter().collect();
276 fns.sort_by(|a, b| b.1.cmp(a.1));
277 let _ = writeln!(s, "[profile] per-fn opcode breakdown (top 15 fns):");
278 for (fid, n) in fns.into_iter().take(15) {
279 let _ = writeln!(s, " {:>12} {} ({:.1}%)", n,
280 debug::render_fn_label(*fid, &self.fn_names),
281 *n as f64 * 100.0 / total.max(1) as f64);
282 if let Some(ops) = self.prof_fn_ops.get(fid) {
283 let mut fo: Vec<_> = ops.iter().collect();
284 fo.sort_by(|a, b| b.1.cmp(a.1));
285 let line: Vec<String> = fo.into_iter().take(8)
286 .map(|(name, c)| format!("{} {:.0}%", name, *c as f64 * 100.0 / (*n).max(1) as f64))
287 .collect();
288 let _ = writeln!(s, " {}", line.join(" "));
289 }
290 }
291 s
292 }
293
294 pub(crate) fn op_name(op: &polka::OpCode) -> &'static str {
295 use polka::OpCode::*;
296 match op {
297 Add(..) => "Add", Sub(..) => "Sub", Mul(..) => "Mul", Div(..) => "Div", Mod(..) => "Mod",
298 Neg(..) => "Neg", FAdd(..) => "FAdd", FSub(..) => "FSub", FMul(..) => "FMul", FDiv(..) => "FDiv",
299 FNeg(..) => "FNeg", FLt(..) => "FLt", FEq(..) => "FEq",
300 Eq(..) => "Eq", Neq(..) => "Neq", Lt(..) => "Lt", Gt(..) => "Gt", Lte(..) => "Lte", Gte(..) => "Gte",
301 And(..) => "And", Or(..) => "Or", Xor(..) => "Xor", Shl(..) => "Shl", Shr(..) => "Shr",
302 Jmp(..) => "Jmp", Jz(..) => "Jz", Jnz(..) => "Jnz", Call(..) => "Call", CallReg(..) => "CallReg",
303 Ret(..) => "Ret", PushConst(..) => "PushConst", Copy(..) => "Copy", Move(..) => "Move",
304 Ld(..) => "Ld", St(..) => "St", LdIdx(..) => "LdIdx", StIdx(..) => "StIdx",
305 AddImm(..) => "AddImm", SubImm(..) => "SubImm", Alloc(..) => "Alloc", Drop(..) => "Drop",
306 Dei(..) => "Dei", Deo(..) => "Deo", Handle(..) => "Handle", Resume(..) => "Resume", Raise(..) => "Raise",
307 }
308 }
309
310 #[inline]
311 pub(crate) fn trace_frame_event(&self, kind: &str, detail: core::fmt::Arguments<'_>) {
312 if !self.trace_frames { return; }
313 if let Some(f) = self.trace_out {
314 let bfi = self.handlers.last().and_then(|h| h.body_frame_index);
315 f(&format!("[{}] {} | frames={} handlers={} bfi={:?}",
316 kind, detail, self.frames.len(), self.handlers.len(), bfi));
317 }
318 }
319
320 pub fn region_push(&mut self) {
321 self.region_table.push();
322 }
323
324 pub fn region_pop(&mut self) -> Result<(), String> {
325 self.region_table.pop_and_release(&mut self.heap)
326 }
327
328 pub fn region_depth(&self) -> usize {
329 self.region_table.depth()
330 }
331
332 #[inline]
333 pub fn region_record_alloc(&mut self, slot: u32, generation: u32) {
334 if self.region_table.is_active() {
335 self.region_table.record_alloc(slot, generation);
336 }
337 }
338
339 pub fn module_table_rc(&self) -> Option<u32> {
343 if self.module_table_is_handle && self.module_table_raw != polka::HANDLE_NONE {
344 let (s, g) = crate::memory::handle_parts(self.module_table_raw);
345 self.heap.rc(s, g)
346 } else {
347 None
348 }
349 }
350
351 pub fn heap_live_count(&self) -> usize {
354 let total = self.heap.live_count();
355 let const_live = self.string_const_handles.iter()
356 .filter(|(s, g)| self.heap.is_live(*s, *g))
357 .count();
358 let mut rt_owned: hashbrown::HashSet<(u32, u32)> = hashbrown::HashSet::new();
359 if self.module_table_is_handle && self.module_table_raw != polka::HANDLE_NONE {
360 let root = crate::memory::handle_parts(self.module_table_raw);
361 self.collect_reachable(root.0, root.1, &mut rt_owned);
362 }
363 let module_live = rt_owned.iter().filter(|(s, g)| self.heap.is_live(*s, *g)).count();
364 total.saturating_sub(const_live).saturating_sub(module_live)
365 }
366
367 fn collect_reachable(&self, slot: u32, generation: u32, visited: &mut hashbrown::HashSet<(u32, u32)>) {
368 if !visited.insert((slot, generation)) { return; }
369 if !self.heap.is_live(slot, generation) { return; }
370 let Ok(data) = self.heap.cell_data(slot, generation) else { return; };
371 let Ok(mask) = self.heap.cell_mask(slot, generation) else { return; };
372 let n = data.len();
373 let data: Vec<u64> = data.to_vec();
374 for i in 0..n {
375 if crate::memory::mask_bit(mask, i) {
376 let child_raw = data[i];
377 if child_raw != polka::HANDLE_NONE {
378 let (cs, cg) = crate::memory::handle_parts(child_raw);
379 self.collect_reachable(cs, cg, visited);
380 }
381 }
382 }
383 }
384
385
386 pub fn live_slots_report(&self) -> String {
388 use core::fmt::Write;
389 let owned: hashbrown::HashSet<(u32, u32)> = {
390 let mut s: hashbrown::HashSet<(u32, u32)> =
391 self.string_const_handles.iter().copied().collect();
392 if self.module_table_is_handle && self.module_table_raw != polka::HANDLE_NONE {
393 s.insert(crate::memory::handle_parts(self.module_table_raw));
394 }
395 s
396 };
397 let cells = self.heap.live_cells();
398 let mut out = String::new();
399 let _ = writeln!(out, "[heap] {} live cell(s), {} user:", cells.len(), self.heap_live_count());
400 for (slot, gen_, rc, data, handles) in &cells {
401 let tag = if owned.contains(&(*slot, *gen_)) { "rt " } else { "USER" };
402 let slots: Vec<String> = data.iter().zip(handles.iter()).map(|(v, h)| {
403 if *h { format!("h:{:#x}", v) } else { format!("{}", *v as i64) }
404 }).collect();
405 let note = self.closure_cell_label(data, handles);
406 let _ = writeln!(out, " [{}] slot={} gen={} rc={} [{}]{}", tag, slot, gen_, rc, slots.join(", "), note);
407 }
408 out
409 }
410
411 fn closure_cell_label(&self, data: &[u64], handles: &[bool]) -> String {
412 if data.len() != 2 || handles.first() != Some(&false) { return String::new(); }
413 let fid = data[0] as usize;
414 match self.fn_names.get(fid) {
415 Some(n) if n.starts_with("__closure_") || n.starts_with("__fnval_") =>
416 format!(" ; closure({})", n),
417 _ => String::new(),
418 }
419 }
420
421 pub fn heap_ref(&self) -> &Heap { &self.heap }
422 pub fn heap_mut(&mut self) -> &mut Heap { &mut self.heap }
423
424 pub fn last_result_is_handle(&self) -> bool { self.last_result_is_handle }
425
426 pub fn install_device(&mut self, id: u8, dev: Box<dyn Device>) {
427 self.devices.install(id, dev);
428 }
429
430 pub fn register_native<S: Into<String>>(&mut self, name: S, func: NativeFn) {
431 self.natives.register(name, func);
432 }
433
434 pub fn register_aot_fn<S: Into<String>>(&mut self, name: S, func: AotFn) {
435 self.aot_fns.insert(name.into(), func);
436 }
437
438 pub fn take_device(&mut self, id: u8) -> Option<Box<dyn Device>> {
439 self.devices.take(id)
440 }
441
442 pub fn heap_alloc(&mut self, size: usize) -> (u32, u32) {
443 self.heap.alloc(size)
444 }
445
446 pub fn heap_st(
447 &mut self, slot: u32, gen_: u32, offset: usize, val: u64, is_handle: bool,
448 ) -> Result<(u64, bool), String> {
449 self.heap.st(slot, gen_, offset, val, is_handle)
450 }
451
452 pub fn push_handler(&mut self, h: HandlerFrame) {
453 self.handlers.push(h);
454 }
455}
456
457#[cfg(test)]
458mod region_tests {
459 use super::*;
460
461 fn vm() -> VirtualMachine { VirtualMachine::new() }
462
463 #[test]
464 fn region_force_frees_even_with_rc_greater_than_one() {
465 let mut v = vm();
466 v.region_push();
467 let (slot, gen_) = v.heap_alloc(1);
468 v.region_record_alloc(slot, gen_);
469 v.heap.rc_inc(slot, gen_).unwrap();
470 v.heap.rc_inc(slot, gen_).unwrap();
471 v.region_pop().expect("pop ok");
472 assert_eq!(v.heap_live_count(), 0, "force_free ignores rc");
473 }
474
475 #[test]
476 fn region_cascade_frees_handles_inside_cell() {
477 let mut v = vm();
478 let (child_slot, child_gen) = v.heap_alloc(1);
479 v.region_push();
480 let (parent_slot, parent_gen) = v.heap_alloc(1);
481 v.region_record_alloc(parent_slot, parent_gen);
482 v.heap.rc_inc(child_slot, child_gen).unwrap();
483 let child_handle = Value::from_handle(child_slot, child_gen).raw();
484 v.heap_st(parent_slot, parent_gen, 0, child_handle, true).unwrap();
485 assert_eq!(v.heap_live_count(), 2);
486 v.region_pop().expect("pop ok");
487 assert_eq!(v.heap_live_count(), 1, "child survives at rc=1; parent freed");
488 }
489
490 #[test]
491 fn region_pop_force_frees_recorded_alloc() {
492 let mut v = vm();
493 v.region_push();
494 let (slot, gen_) = v.heap_alloc(4);
495 v.region_record_alloc(slot, gen_);
496 assert_eq!(v.heap_live_count(), 1);
497 v.region_pop().expect("pop ok");
498 assert_eq!(v.heap_live_count(), 0, "alloc recorded in region must be force-freed");
499 }
500
501 #[test]
502 fn nested_region_pop_frees_only_inner() {
503 let mut v = vm();
504 v.region_push();
505 let (outer_slot, outer_gen) = v.heap_alloc(1);
506 v.region_record_alloc(outer_slot, outer_gen);
507
508 v.region_push();
509 let (inner_slot, inner_gen) = v.heap_alloc(1);
510 v.region_record_alloc(inner_slot, inner_gen);
511 assert_eq!(v.heap_live_count(), 2);
512
513 v.region_pop().expect("inner pop");
514 assert_eq!(v.region_depth(), 1, "outer region still active");
515 assert_eq!(v.heap_live_count(), 1, "outer alloc survives inner pop");
516
517 v.region_pop().expect("outer pop");
518 assert_eq!(v.region_depth(), 0);
519 assert_eq!(v.heap_live_count(), 0);
520 }
521
522 #[test]
523 fn region_records_only_to_topmost_region() {
524 let mut v = vm();
525 v.region_push();
526 v.region_push();
527 let (slot, gen_) = v.heap_alloc(1);
528 v.region_record_alloc(slot, gen_);
529
530 v.region_pop().expect("inner pop");
531 assert_eq!(v.heap_live_count(), 0);
532 v.region_pop().expect("outer pop");
533 }
534
535 #[test]
536 fn record_alloc_outside_region_is_noop() {
537 let mut v = vm();
538 let (slot, gen_) = v.heap_alloc(1);
539 v.region_record_alloc(slot, gen_);
540 assert_eq!(v.heap_live_count(), 1);
541 assert!(v.region_pop().is_err());
542 }
543}
544
545impl VirtualMachine {
546 pub fn render_value(&self, raw: u64, is_handle: bool, depth: usize) -> String {
550 if !is_handle { return (raw as i64).to_string(); }
551 if raw == polka::HANDLE_NONE { return "none".into(); }
552 if depth == 0 { return "…".into(); }
553 let (slot, g) = Self::decode_handle(raw);
554 let len = match self.heap.size(slot, g) {
555 Ok(n) => n,
556 Err(_) => return format!("<stale {:#x}>", raw),
557 };
558 let mut out = String::from("[");
559 for off in 0..len {
560 if off > 0 { out.push_str(", "); }
561 match self.heap.ld(slot, g, off) {
562 Ok((v, h)) => out.push_str(&self.render_value(v, h, depth - 1)),
563 Err(_) => { out.push_str("<err>"); }
564 }
565 }
566 out.push(']');
567 out
568 }
569
570 pub fn drop_result_for_test(&mut self, raw: u64) {
572 let _ = self.heap.rc_dec_handle(raw);
573 }
574}
575
576#[cfg(test)]
577mod vm_api_tests {
578 use super::*;
579 use polka::{Module, Chunk, BytecodeChunk, OpCode, Register};
580
581 fn const_module(val: u64) -> Module {
582 Module {
583 functions: vec![Chunk::Bytecode(BytecodeChunk {
584 code: vec![OpCode::PushConst(Register(0), 0), OpCode::Ret(Register(0))],
585 constants: vec![val],
586 const_mask: Vec::new(),
587 string_constants: Vec::new(),
588 reg_count: 1,
589 param_count: 0,
590 lines: Vec::new(),
591 src_file: String::new(),
592 })],
593 entry: 0,
594 flags: 0,
595 exports: Vec::new(),
596 }
597 }
598
599 #[test]
600 fn run_returns_entry_value_as_int() {
601 assert_eq!(run(const_module(42), Host::default()).unwrap(), 42);
602 }
603
604 #[test]
605 fn profile_and_step_counters_populate_after_run() {
606 let mut vm = VirtualMachine::new().with_profile(true);
607 let loaded = loader::load(const_module(7)).unwrap();
608 let v = vm.run_module(&loaded.module).unwrap();
609 assert_eq!(v.as_int(), 7);
610 assert!(vm.steps() > 0);
611 assert!(!vm.profile_report().is_empty());
612 }
613
614 #[test]
615 fn region_depth_tracks_push_pop() {
616 let mut vm = VirtualMachine::new();
617 assert_eq!(vm.region_depth(), 0);
618 vm.region_push();
619 assert_eq!(vm.region_depth(), 1);
620 vm.region_pop().unwrap();
621 assert_eq!(vm.region_depth(), 0);
622 }
623
624 #[test]
625 fn fresh_vm_not_halted_no_exit_code() {
626 let vm = VirtualMachine::new();
627 assert!(!vm.halted());
628 assert_eq!(vm.exit_code(), None);
629 assert_eq!(vm.module_table_rc(), None);
630 }
631
632 #[test]
633 fn render_value_formats_int_and_opaque_handle() {
634 let vm = VirtualMachine::new();
635 assert_eq!(vm.render_value(42, false, 0), "42");
636 assert_eq!(vm.render_value(polka::HANDLE_NONE, true, 4), "none");
637 assert_eq!(vm.render_value(0, true, 0), "…");
638 }
639
640 #[test]
641 fn take_device_absent_is_none() {
642 let mut vm = VirtualMachine::new();
643 assert!(vm.take_device(0x7e).is_none());
644 }
645}