Skip to main content

myriad/
lib.rs

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    // Bit i (LSB of word i/64) = 1 iff registers[i] is a handle.
47    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    // Constants resolved per-fn at module load. Value bits + parallel mask.
60    pub(crate) resolved_constants: Vec<Vec<u64>>,
61    pub(crate) resolved_const_mask: Vec<Vec<u64>>,
62    // Permanent heap handles for string constants; rc=1 module-lifetime.
63    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    // Text-diagnostic sink for frame/static traces. None = silent (no_std default).
90    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    // Bits set = the slot holds a handle when written by do_yield.
135    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    // N of instructions executed. Monotonic; a profiler reads the per-frame delta.
205    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    // Configure heap RC tracing (TRACE_SLOT). slot=None+all=true traces every
212    // cell; out is the text sink the host provides (e.g. an eprintln wrapper).
213    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    // Text sink for frame/static diagnostics (TRACE_STATIC, trace_frames).
219    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    // fn_id-indexed bitset; out-of-range fn ids are silenced. None = trace all.
240    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    // Opcode + per-fn execution histogram (PROFILE=1). Op counts are
262    // pacing-independent: work per run, not wall time. Returns "" when profiling
263    // is off; the host prints the report. Kept here so the VM owns the format.
264    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    // Refcount of the module static-table root. heap_live_count() hides table
340    // leaks (the cell is module-reachable, so always subtracted); this exposes
341    // the raw rc so tests can assert per-call Dei/Drop balance on it.
342    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    // User-visible live count. Excludes module-lifetime cells owned by the
352    // loader/runtime (not by user code): string constants and the module table.
353    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    // Debug: render every live heap cell. Host prints the returned report.
387    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    // Structural heap dump for debuggers/hosts. No runtime types: cells render
547    // as [v0, v1, …], scalars as decimal, HANDLE_NONE as "none". Read-only;
548    // stale handles render as an error string (gen check), never panic.
549    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    // Test helper: release one rc on a handle (e.g. a value returned by main).
571    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}