Skip to main content

qcode_vm/
vm.rs

1//! The run loop: a machine that owns its code, discovers more of it as the
2//! guest reaches it, and stops with a reason instead of an error.
3//!
4//! Two things separate this from driving [`StandaloneEmulator`] directly.
5//!
6//! **The context is owned, not borrowed.** Lifting new code mutates the module,
7//! which a `&Context` cannot allow. This is why the VM is built on
8//! [`StandaloneEmulator`] — the lifetime-free emulator that takes `&Context` per
9//! call — rather than on `Emulator<'ctx>`, which would pin the module for as
10//! long as the machine exists.
11//!
12//! **Stopping is a value.** A guest that reads unmapped memory has not broken
13//! the emulator; it has taken a fault, which a harness may want to report,
14//! resume from, or count as a crash. So the loop returns [`VmExit`] and leaves
15//! the machine intact and inspectable.
16
17use qcode::value::LocalInsnId;
18use qcode::{
19    address_index::{AddressIndex, AddressTarget},
20    context::Context,
21    value::{
22        BasicBlock, BlockId, InstructionId, ValueId,
23        insn::{Mnemonic, PCodeOpId, VM_INTERRUPT},
24    },
25};
26use qcode_emulator::{EmulatorErrorKind, EmulatorMemory, SizedValue, StandaloneEmulator};
27use rustc_hash::{FxHashMap, FxHashSet};
28
29use crate::{
30    hook::{AddressHook, BlockEntryHook, WriteWatch},
31    inject::CodeInjector,
32    memory::VmMemory,
33    mmu::MemFault,
34    stats::Stats,
35    table::{
36        Callback, CodeRangeHook, HookAction, HookId, HookTable, InsnAction, MemAccess, ReadWatch,
37    },
38};
39
40/// Why a lifting attempt failed.
41#[derive(Debug, Clone)]
42pub enum CodeError {
43    /// The instruction bytes could not be fetched.
44    Fault(MemFault),
45    /// The bytes were fetched but did not decode, or did not lift.
46    Decode(Box<str>),
47}
48
49/// Supplies code the machine has not seen yet.
50///
51/// Kept as a trait so the VM does not depend on SLEIGH: a decoder is a policy
52/// choice (which specification, which variant), and a test wants to hand over
53/// blocks without compiling one. An implementation reads instruction bytes from
54/// the [`Mmu`](crate::Mmu) — via [`read_code`](crate::Mmu::read_code), so that
55/// executing a non-executable page faults at the fetch — and lowers them into
56/// `ctx`.
57pub trait CodeSource {
58    /// Lifts the code at `addr` into `ctx`.
59    ///
60    /// Returning `Ok(())` asserts that a block starting at `addr` now exists;
61    /// the VM re-resolves the address itself rather than trusting a returned id,
62    /// so a source is free to lift a whole run of instructions at once.
63    ///
64    /// `index` is the machine's live address lookup, and the implementation must
65    /// keep it current as it adds blocks — every lifting entry point takes one
66    /// for exactly this reason. Rebuilding it per instruction instead is
67    /// quadratic in the size of the module discovered so far.
68    ///
69    /// `stats` is the machine's own counters: an implementation records the
70    /// time it spends fetching and decoding there, so a benchmark can separate
71    /// translation cost from interpretation cost.
72    fn lift(
73        &mut self,
74        ctx: &mut Context<'static>,
75        memory: &VmMemory,
76        index: &mut AddressIndex,
77        addr: u64,
78        stats: &mut Stats,
79    ) -> Result<(), CodeError>;
80}
81
82/// An alternative way to execute a block's body.
83///
84/// The interpreter is always present and always correct; an executor is an
85/// *optimisation* that may decline any block for any reason, in which case the
86/// interpreter runs it unchanged. That is what lets a backend be partial: a JIT
87/// need only handle the shapes it handles well.
88///
89/// An executor runs the block's body, not its terminator. Control flow, block
90/// parameters and call semantics stay in one implementation.
91///
92/// It is handed the whole emulator rather than just its memory because the
93/// interpreter still has to run the terminator, and a terminator reads
94/// operands — a `cbranch` condition, a branch's block arguments. Those are
95/// values the body produced, so an executor that keeps them somewhere other
96/// than the interpreter's value table must put them back before returning.
97pub trait BlockExecutor {
98    /// Runs everything in `block` except its terminator.
99    ///
100    /// `Ok(None)` means "not mine" and is not an error — the caller falls back
101    /// to the interpreter.
102    ///
103    /// On `Ok(Some(_))` every value the terminator of [`Executed::block`] reads
104    /// must be readable from `emu.insn_values`, exactly as if the interpreter
105    /// had run that body.
106    ///
107    /// `chain` lets the executor run on past `block` into successors it also
108    /// handles, instead of handing control back after one. Deciding a branch
109    /// itself is how an executor keeps control inside its own code rather than
110    /// paying a round trip per block. The caller withholds it when something
111    /// needs to observe every block — a breakpoint is set, say — because blocks
112    /// crossed this way are never offered to the interpreter.
113    ///
114    /// `start` is the body index to begin at. It is 0 when a block is
115    /// entered, and the instruction after an interrupting op when the
116    /// interpreter has run the block up to there and hands the rest over.
117    /// Results the interpreter already computed for the block are in
118    /// `emu.insn_values`; an executor may read them or decline.
119    fn run_block(
120        &mut self,
121        ctx: &Context<'_>,
122        emu: &mut StandaloneEmulator<VmMemory>,
123        block: BlockId,
124        start: usize,
125        chain: bool,
126    ) -> Result<Option<Executed>, EmulatorErrorKind>;
127}
128
129/// Where an executor left the machine.
130#[derive(Debug, Clone, Copy)]
131pub struct Executed {
132    /// The block the interpreter continues in. With chaining this is the
133    /// last of several, not the one that was asked for.
134    pub block: BlockId,
135    /// The body index of that block the interpreter continues from: its
136    /// terminator, or an interrupting op the executor stopped short of.
137    pub body: usize,
138    /// Operations retired across every block run, for accounting.
139    pub retired: u64,
140}
141
142/// Why the machine stopped.
143#[derive(Debug, Clone)]
144pub enum VmExit {
145    /// The step budget ran out. The machine is resumable.
146    InstructionLimit,
147    /// Execution reached an address with a breakpoint on it. The breakpoint
148    /// instruction has *not* been executed.
149    Breakpoint(u64),
150    /// A memory access failed.
151    Fault(MemFault),
152    /// Code at this address could not be lifted.
153    Unlifted { addr: u64, error: CodeError },
154    /// The machine stopped *at* a user p-code operation for the host to act:
155    /// an explicit `vm.interrupt`, or an op the interpreter has no semantics
156    /// for (`syscall`, `cpuid`, `rdtsc`, ...). Everything before the op in
157    /// its block has retired and nothing after it has run. The machine stays
158    /// there until [`Vm::resume`] supplies the op's effect; running again
159    /// without resuming reports the same interrupt.
160    Interrupt(Interrupt),
161    /// A hook registered through the [table](crate::table) asked the run to
162    /// stop. For a code or memory hook the machine is before the hooked
163    /// guest instruction, and running again executes it without notifying
164    /// the hook again. For an instruction hook the user op is still pending,
165    /// as [`Interrupt`] describes, until the caller resumes it.
166    HookStop(HookId),
167    /// The interpreter reported something the VM does not model as a guest
168    /// event — a malformed block, an internal failure.
169    Error(Box<str>),
170}
171
172/// What kind of operation stopped the machine.
173#[derive(Debug, Clone, PartialEq, Eq)]
174pub enum InterruptKind {
175    /// An explicit [`VM_INTERRUPT`] placed in the IR by a hook or an injector.
176    /// `code` is its first operand.
177    Explicit { code: u64 },
178    /// An architecture user op with no interpreter semantics, named as in the
179    /// SLEIGH specification.
180    Intrinsic { op: PCodeOpId, name: Box<str> },
181}
182
183/// A stop at a user p-code operation, with what the host needs to act on it.
184#[derive(Debug, Clone, PartialEq, Eq)]
185pub struct Interrupt {
186    pub kind: InterruptKind,
187    /// The operation's instruction: where [`Vm::resume`] files its result.
188    pub insn: InstructionId,
189    /// The width in bytes of the result the operation declares, or 0 when it
190    /// produces nothing.
191    pub size: usize,
192    /// The operation's operands as read at the stop — after `code` for an
193    /// explicit interrupt. `None` where an operand is wider than 64 bits or
194    /// could not be read.
195    pub args: Vec<Option<u64>>,
196    /// The guest address of the instruction that lifted to the operation.
197    pub pc: Option<u64>,
198}
199
200/// Why [`Vm::resume`] refused.
201#[derive(Debug, Clone, PartialEq, Eq)]
202pub enum ResumeError {
203    /// The machine is not stopped at an interrupt.
204    NotInterrupted,
205    /// The operation declares a result of this many bytes and none was given.
206    ResultRequired { size: usize },
207}
208
209impl std::fmt::Display for ResumeError {
210    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211        match self {
212            Self::NotInterrupted => write!(f, "the machine is not stopped at an interrupt"),
213            Self::ResultRequired { size } => {
214                write!(f, "the interrupted operation needs a {size}-byte result")
215            }
216        }
217    }
218}
219
220impl std::error::Error for ResumeError {}
221
222/// A machine: an owned module, a memory, and a position in the code.
223pub struct Vm<S> {
224    ctx: Context<'static>,
225    emu: StandaloneEmulator<VmMemory>,
226    source: S,
227    /// Counters and phase timings for this run.
228    pub stats: Stats,
229    /// An optional faster path for block bodies. `None` means the interpreter
230    /// executes everything, which is always a valid way to run.
231    executor: Option<Box<dyn BlockExecutor>>,
232    /// Set by a lift that folded the block it filled into a predecessor, which
233    /// leaves the machine already positioned. Taken by the step that asked for
234    /// the lift.
235    absorbed_into: Option<BlockId>,
236    /// Whether freshly lifted blocks get a cleanup round.
237    ///
238    /// Lifting one machine instruction emits every side effect the
239    /// specification describes, including flag computations the surrounding
240    /// code never reads. Removing the ones with no users at all is sound
241    /// block-locally — an instruction with no users cannot be observed — and is
242    /// paid once per block instead of on every execution of it.
243    pub optimize: bool,
244    /// The block that has grown by absorption and not been cleaned since.
245    ///
246    /// Absorption folds a straight-line run one guest instruction at a time,
247    /// and cleaning the whole enlarged block after each one is quadratic in the
248    /// length of the run — which on unrolled code is the dominant cost of
249    /// translation. The cleanup is deferred to the point the block is next
250    /// entered at its first instruction, by which time the run has stopped
251    /// growing and one pass does the work of all of them.
252    ///
253    /// At most one: absorption extends one run at a time, so a *different*
254    /// block being absorbed into means the previous run has stopped growing
255    /// and can be cleaned right there. Waiting for it to be entered again
256    /// instead would let compiled code be built from the uncleaned form — and
257    /// worse, would clean it in an interpreted run but not in a chained
258    /// compiled one, leaving the two strategies running different QCode.
259    dirty: Option<BlockId>,
260    breakpoints: FxHashSet<u64>,
261    /// The interrupt the machine is stopped at, until it is resumed.
262    pending: Option<Interrupt>,
263    /// Set by a resume: the machine is part-way into a block, and the
264    /// executor gets one chance to take the rest of it.
265    offer_rest: bool,
266    /// Code rewriters, run over each block before it is first entered and
267    /// again when it grows. See [`crate::inject`].
268    injectors: Vec<Box<dyn CodeInjector>>,
269    /// The injector set each block was last rewritten by, so registering an
270    /// injector reaches blocks already lifted the next time they are entered.
271    injected: FxHashMap<BlockId, u64>,
272    /// Bumped by every registration.
273    generation: u64,
274    /// Callbacks the run calls itself. See [`crate::table`].
275    table: HookTable<S>,
276}
277
278impl<S: CodeSource> Vm<S> {
279    /// Builds a machine positioned at `entry`.
280    pub fn new(ctx: Context<'static>, entry: BlockId, source: S) -> Self {
281        let mut emu = StandaloneEmulator::<VmMemory>::new_in(entry);
282        emu.memory.configure_spaces(&ctx);
283        Self {
284            ctx,
285            emu,
286            source,
287            optimize: true,
288            stats: Stats::default(),
289            executor: None,
290            absorbed_into: None,
291            dirty: None,
292            breakpoints: FxHashSet::default(),
293            pending: None,
294            offer_rest: false,
295            injectors: Vec::new(),
296            injected: FxHashMap::default(),
297            generation: 0,
298            table: HookTable::default(),
299        }
300    }
301
302    /// Builds a machine positioned at a guest address, lifting the entry block
303    /// if the module does not already contain it.
304    pub fn at_address(
305        mut ctx: Context<'static>,
306        addr: u64,
307        mut source: S,
308        memory: VmMemory,
309    ) -> Result<Self, CodeError> {
310        // Built once here and handed to the machine, which keeps it current
311        // from then on.
312        let mut index = AddressIndex::analyze(&ctx);
313        let mut stats = Stats::default();
314        if resolve(&ctx, &index, addr).is_none() {
315            stats.lifts += 1;
316            source.lift(&mut ctx, &memory, &mut index, addr, &mut stats)?;
317        }
318        let entry = resolve(&ctx, &index, addr).ok_or_else(|| {
319            CodeError::Decode(format!("no block at {addr:#x} after lifting").into())
320        })?;
321        let mut vm = Self::new(ctx, entry, source);
322        vm.emu.memory = memory;
323        vm.emu.memory.configure_spaces(&vm.ctx);
324        vm.emu.set_address_index(index);
325        vm.stats = stats;
326        Ok(vm)
327    }
328
329    /// Installs an alternative executor for block bodies, replacing any
330    /// previous one. Purely an optimisation: removing it changes speed, not
331    /// behaviour.
332    pub fn set_block_executor(&mut self, executor: Box<dyn BlockExecutor>) {
333        self.executor = Some(executor);
334    }
335
336    pub fn clear_block_executor(&mut self) {
337        self.executor = None;
338    }
339
340    pub fn context(&self) -> &Context<'static> {
341        &self.ctx
342    }
343
344    pub fn memory(&self) -> &VmMemory {
345        &self.emu.memory
346    }
347
348    pub fn memory_mut(&mut self) -> &mut VmMemory {
349        &mut self.emu.memory
350    }
351
352    /// The emulator underneath, for register access and harness seeding.
353    pub fn emulator(&mut self) -> &mut StandaloneEmulator<VmMemory> {
354        &mut self.emu
355    }
356
357    /// The guest address of the block about to execute, if it has one.
358    pub fn pc(&self) -> Option<u64> {
359        BasicBlock::from_id(&self.ctx, self.emu.block).address()
360    }
361
362    pub fn add_breakpoint(&mut self, addr: u64) -> bool {
363        self.breakpoints.insert(addr)
364    }
365
366    pub fn remove_breakpoint(&mut self, addr: u64) -> bool {
367        self.breakpoints.remove(&addr)
368    }
369
370    /// Registers a code rewriter. It runs over every block the machine enters
371    /// from now on, including blocks lifted before this call, before the
372    /// block executes or is compiled.
373    pub fn add_injector(&mut self, injector: Box<dyn CodeInjector>) {
374        self.injectors.push(injector);
375        self.generation += 1;
376    }
377
378    /// Registers a [`Hook`](crate::hook::Hook): a code rewriter that picks
379    /// its sites and emits through an [`Emitter`](crate::hook::Emitter).
380    pub fn add_hook(&mut self, hook: impl crate::hook::Hook + 'static) {
381        self.add_injector(Box::new(crate::hook::HookInjector::new(hook)));
382    }
383
384    // ---- Unicorn-shaped hooks. See [`crate::table`].
385
386    /// Calls `callback` with the address at the entry of every block in
387    /// `begin..=end` (anywhere when `begin > end`).
388    pub fn hook_block(
389        &mut self,
390        begin: u64,
391        end: u64,
392        callback: impl FnMut(&mut Self, u64) -> HookAction + 'static,
393    ) -> HookId {
394        let id = self.table.register(Callback::Code(Box::new(callback)));
395        self.add_hook(BlockEntryHook {
396            begin,
397            end,
398            code: HookTable::<S>::code(id),
399        });
400        id
401    }
402
403    /// Calls `callback` before every guest instruction in `begin..=end`.
404    pub fn hook_code(
405        &mut self,
406        begin: u64,
407        end: u64,
408        callback: impl FnMut(&mut Self, u64) -> HookAction + 'static,
409    ) -> HookId {
410        let id = self.table.register(Callback::Code(Box::new(callback)));
411        self.add_hook(CodeRangeHook {
412            begin,
413            end,
414            code: HookTable::<S>::code(id),
415        });
416        id
417    }
418
419    /// Calls `callback` before the guest instruction at `addr`.
420    pub fn hook_address(
421        &mut self,
422        addr: u64,
423        callback: impl FnMut(&mut Self, u64) -> HookAction + 'static,
424    ) -> HookId {
425        let id = self.table.register(Callback::Code(Box::new(callback)));
426        self.add_hook(AddressHook::new([addr], HookTable::<S>::code(id)));
427        id
428    }
429
430    /// Calls `callback` before every store to guest memory in `begin..=end`,
431    /// with the address, width and value.
432    pub fn hook_mem_write(
433        &mut self,
434        begin: u64,
435        end: u64,
436        callback: impl FnMut(&mut Self, &MemAccess) -> HookAction + 'static,
437    ) -> HookId {
438        let id = self.table.register(Callback::Mem(Box::new(callback)));
439        self.add_hook(WriteWatch {
440            begin,
441            end,
442            code: HookTable::<S>::code(id),
443        });
444        id
445    }
446
447    /// Calls `callback` before every load from guest memory in `begin..=end`,
448    /// with the address and width.
449    pub fn hook_mem_read(
450        &mut self,
451        begin: u64,
452        end: u64,
453        callback: impl FnMut(&mut Self, &MemAccess) -> HookAction + 'static,
454    ) -> HookId {
455        let id = self.table.register(Callback::Mem(Box::new(callback)));
456        self.add_hook(ReadWatch {
457            begin,
458            end,
459            code: HookTable::<S>::code(id),
460        });
461        id
462    }
463
464    /// Calls `callback` when the guest reaches the user op called `name` —
465    /// `syscall`, `rdtsc`, `cpuid_basic`, ... — to supply its effect.
466    pub fn hook_insn(
467        &mut self,
468        name: &str,
469        callback: impl FnMut(&mut Self, &Interrupt) -> InsnAction + 'static,
470    ) -> HookId {
471        self.table.register(Callback::Insn {
472            name: Some(Box::from(name)),
473            callback: Box::new(callback),
474        })
475    }
476
477    /// Calls `callback` for every user op the interpreter cannot run, whatever
478    /// its name.
479    pub fn hook_intr(
480        &mut self,
481        callback: impl FnMut(&mut Self, &Interrupt) -> InsnAction + 'static,
482    ) -> HookId {
483        self.table.register(Callback::Insn {
484            name: None,
485            callback: Box::new(callback),
486        })
487    }
488
489    /// Removes a hook. Interrupts it injected stay in the code and resume
490    /// silently; they cost a stop and nothing more.
491    pub fn hook_del(&mut self, id: HookId) -> bool {
492        self.table.remove(id)
493    }
494
495    /// Handles an interrupt the table owns, or an intrinsic a hook answers.
496    ///
497    /// Returns `None` when the run should carry on, and the exit to return
498    /// otherwise. The callbacks are moved out for the duration so they can be
499    /// handed the machine; ones registered meanwhile are kept.
500    fn dispatch(&mut self, interrupt: &Interrupt) -> Option<VmExit> {
501        let owner = HookTable::<S>::owner(interrupt);
502        let intrinsic = match &interrupt.kind {
503            InterruptKind::Intrinsic { name, .. } => Some(name.clone()),
504            InterruptKind::Explicit { .. } => None,
505        };
506        if owner.is_none() && intrinsic.is_none() {
507            return Some(VmExit::Interrupt(interrupt.clone()));
508        }
509        let mut callbacks = std::mem::take(&mut self.table.callbacks);
510        let mut outcome: Option<VmExit> = None;
511        let mut handled = false;
512        for (id, callback) in &mut callbacks {
513            match (callback, owner, &intrinsic) {
514                (Callback::Code(callback), Some(owner), _) if *id == owner => {
515                    let pc = interrupt
516                        .args
517                        .first()
518                        .copied()
519                        .flatten()
520                        .unwrap_or_default();
521                    if callback(self, pc) == HookAction::Stop {
522                        outcome = Some(VmExit::HookStop(*id));
523                    }
524                    handled = true;
525                }
526                (Callback::Mem(callback), Some(owner), _) if *id == owner => {
527                    let arg = |index: usize| interrupt.args.get(index).copied().flatten();
528                    let access = MemAccess {
529                        pc: interrupt.pc,
530                        addr: arg(0).unwrap_or_default(),
531                        size: arg(1).unwrap_or_default(),
532                        value: arg(2),
533                    };
534                    if callback(self, &access) == HookAction::Stop {
535                        outcome = Some(VmExit::HookStop(*id));
536                    }
537                    handled = true;
538                }
539                (Callback::Insn { name, callback }, None, Some(op))
540                    if name.as_ref().is_none_or(|name| name == op) =>
541                {
542                    match callback(self, interrupt) {
543                        InsnAction::Handled(value) => {
544                            if let Err(error) = self.resume(value) {
545                                outcome = Some(VmExit::Error(error.to_string().into()));
546                            }
547                            handled = true;
548                        }
549                        InsnAction::Stop => {
550                            outcome = Some(VmExit::HookStop(*id));
551                            handled = true;
552                        }
553                        InsnAction::Unhandled => continue,
554                    }
555                }
556                _ => continue,
557            }
558            if outcome.is_some() {
559                break;
560            }
561            if intrinsic.is_some() && handled {
562                // One answer per op.
563                break;
564            }
565        }
566        // Hooks registered by a callback were pushed onto the empty table.
567        callbacks.append(&mut self.table.callbacks);
568        self.table.callbacks = callbacks;
569
570        if owner.is_some() {
571            // The table's interrupt is a notification, delivered now (or
572            // owed to a hook since deleted). Whether the run goes on or
573            // stops, the machine is left before the guest instruction, not
574            // at the notification: a later run must not deliver it again.
575            if let Err(error) = self.resume(None) {
576                return Some(VmExit::Error(error.to_string().into()));
577            }
578            return outcome;
579        }
580        if let Some(exit) = outcome {
581            return Some(exit);
582        }
583        if !handled {
584            // An intrinsic no hook answered is the caller's.
585            return Some(VmExit::Interrupt(interrupt.clone()));
586        }
587        None
588    }
589
590    /// Runs the injectors over `block` unless the current set already has.
591    ///
592    /// Only over lifted code: an empty block carrying an address is a request
593    /// to lift it, and its instructions arrive — and get rewritten — once it
594    /// is discovered.
595    fn inject(&mut self, block: BlockId) {
596        if self.injectors.is_empty()
597            || self.injected.get(&block) == Some(&self.generation)
598            || !BasicBlock::from_id(&self.ctx, block).is_terminated()
599        {
600            return;
601        }
602        // Moved out for the duration, so the injectors can be handed the
603        // module without borrowing the machine twice.
604        let mut injectors = std::mem::take(&mut self.injectors);
605        for injector in &mut injectors {
606            injector.inject(&mut self.ctx, block);
607        }
608        self.injectors = injectors;
609        self.injected.insert(block, self.generation);
610        // The interpreter may hold this block's instruction list, and it has
611        // changed.
612        self.emu.invalidate_block_cache();
613    }
614
615    /// Executes one instruction, lifting code on demand if control leaves the
616    /// part of the module already known.
617    ///
618    /// Returns `None` when the step was ordinary, and `Some(exit)` when the
619    /// machine stopped for a reason worth reporting.
620    pub fn step(&mut self) -> Option<VmExit> {
621        // Stopped at an operation nobody has resumed: the machine has not
622        // moved, and stepping it would run the op again without its effect.
623        if let Some(interrupt) = &self.pending {
624            return Some(VmExit::Interrupt(interrupt.clone()));
625        }
626        // A branch to unlifted code fails *before* the emulator moves, so the
627        // address can be lifted and the same step retried. One retry is enough:
628        // the second failure means the source did not produce the block it
629        // claimed to, which is a source bug rather than a discovery step.
630        for attempt in 0..2 {
631            // At its first instruction a block is between runs, which is the
632            // one moment a deferred cleanup can be taken without disturbing a
633            // position inside it — and it has to happen before the executor
634            // looks, or compiled code gets built from uncleaned QCode.
635            if self.emu.idx == 0 {
636                let block = self.emu.block;
637                self.clean_before_entering(block);
638                self.inject(block);
639            }
640
641            // At a block's first instruction — or just past an interrupt it
642            // resumed from — an installed executor may run the rest of the
643            // body at once, leaving the interpreter only the terminator.
644            if (self.emu.idx == 0 || self.offer_rest)
645                && let Some(executor) = self.executor.as_mut()
646            {
647                self.offer_rest = false;
648                let block = self.emu.block;
649                let start = self.emu.idx;
650                // Blocks the executor runs are never offered to the interpreter, so
651                // it may only run past the first when nothing needs to see them.
652                let chain = self.breakpoints.is_empty();
653                match executor.run_block(&self.ctx, &mut self.emu, block, start, chain) {
654                    Ok(Some(run)) => {
655                        // The operations were retired by the executor; they are
656                        // counted so throughput stays comparable between strategies.
657                        self.stats.steps += run.retired;
658                        self.stats.native_bodies += 1;
659                        // Positioning inside a block the interpreter has not walked
660                        // into invalidates its cached instruction list.
661                        self.emu.invalidate_block_cache();
662                        self.emu.block = run.block;
663                        self.emu.idx = run.body;
664                    }
665                    Ok(None) => {}
666                    Err(kind) => {
667                        let fault = self.emu.memory.take_fault();
668                        return Some(match fault {
669                            Some(fault) => VmExit::Fault(fault),
670                            None => self.exit_for(kind),
671                        });
672                    }
673                }
674            }
675
676            match self.emu.step(&self.ctx) {
677                Ok(()) => {
678                    self.stats.steps += 1;
679                    return None;
680                }
681                Err(error) => match error.kind {
682                    EmulatorErrorKind::InvalidBlockAddress(addr)
683                    | EmulatorErrorKind::UnknownAddress(addr)
684                        if attempt == 0 =>
685                    {
686                        if let Some(exit) = self.discover(addr) {
687                            return Some(exit);
688                        }
689                    }
690                    // A direct branch to code that has not been lifted does not
691                    // fail to resolve: the lifter materializes the target as an
692                    // *empty* block at that address, and execution walks into
693                    // it. So an empty block carrying an address is a request to
694                    // discover it, not a malformed-IR error.
695                    EmulatorErrorKind::EmptyBlock(block) if attempt == 0 => {
696                        let Some(addr) = BasicBlock::from_id(&self.ctx, block).address() else {
697                            return Some(VmExit::Error(
698                                EmulatorErrorKind::EmptyBlock(block).to_string().into(),
699                            ));
700                        };
701                        // The address may already be lifted: a branch back into
702                        // known code still gets a fresh placeholder block in the
703                        // branching instruction's own function, and lifting it
704                        // again would collide with the function that owns it.
705                        // Resolving first is what makes loops work.
706                        let before = self.emu.block;
707                        self.reposition(addr);
708                        if self.emu.block != before {
709                            // Already lifted: a translation-cache hit.
710                            self.stats.resolves += 1;
711                            continue;
712                        }
713                        if let Some(exit) = self.discover(addr) {
714                            return Some(exit);
715                        }
716                        // Lifting may place the instruction in a *new* block
717                        // rather than filling the placeholder the emulator is
718                        // sitting in, so the machine has to be moved onto
719                        // whatever now covers the address — unless the lift
720                        // folded that block into its predecessor, which
721                        // positions the machine itself. `addr` is interior to
722                        // the absorbing block then, and resolving it by address
723                        // would land at that block's *start*.
724                        if self.absorbed_into.take().is_none() {
725                            self.reposition(addr);
726                        }
727                    }
728                    EmulatorErrorKind::MemoryReadError(addr)
729                    | EmulatorErrorKind::MemoryWriteError(addr) => {
730                        // The backend records the precise cause; the error alone
731                        // could only say that an access at this address failed.
732                        let fault = self.emu.memory.take_fault().unwrap_or(MemFault {
733                            kind: crate::mmu::FaultKind::ReadUnmapped,
734                            addr,
735                        });
736                        return Some(VmExit::Fault(fault));
737                    }
738                    kind => return Some(self.exit_for(kind)),
739                },
740            }
741        }
742        None
743    }
744
745    /// The exit for an interpreter error that is not a memory fault or a
746    /// discovery request.
747    ///
748    /// A stop at a user operation is a guest event with a typed exit; anything
749    /// else is reported as the error it is.
750    fn exit_for(&mut self, kind: EmulatorErrorKind) -> VmExit {
751        match kind {
752            EmulatorErrorKind::Interrupt | EmulatorErrorKind::UnsupportedPCodeOp(_) => {
753                match self.interrupt_at_position() {
754                    Some(interrupt) => {
755                        self.pending = Some(interrupt.clone());
756                        VmExit::Interrupt(interrupt)
757                    }
758                    // The error named an op, but the machine is not at one:
759                    // an executor stopped somewhere it should not have.
760                    None => VmExit::Error(kind.to_string().into()),
761                }
762            }
763            other => VmExit::Error(other.to_string().into()),
764        }
765    }
766
767    /// Describes the user operation at the machine's position, if that is
768    /// what it is stopped at.
769    fn interrupt_at_position(&mut self) -> Option<Interrupt> {
770        let block = self.emu.block;
771        let idx = self.emu.idx;
772        if !self.ctx.contains_block(block) {
773            return None;
774        }
775        let (insn, size, pc, op, operands) = {
776            let insn = BasicBlock::from_id(&self.ctx, block)
777                .instructions()
778                .nth(idx)?;
779            let Mnemonic::PCodeOp(op) = insn.mnemonic() else {
780                return None;
781            };
782            let operands: Vec<ValueId> =
783                op.args.iter().map(|arg| arg.qualify(block.func)).collect();
784            // An instruction carries the address of the guest instruction it
785            // was lifted from; one built by hand may not, in which case the
786            // block's own address is the best that can be said.
787            let pc = insn
788                .address()
789                .or_else(|| BasicBlock::from_id(&self.ctx, block).address());
790            (insn.id, insn.size(), pc, op.id, operands)
791        };
792        let name = self.ctx.shared.pcode_ops[op].clone();
793        let mut args: Vec<Option<u64>> = operands
794            .into_iter()
795            .map(|value| self.emu.get_value(&self.ctx, value))
796            .collect();
797        let kind = if name.as_ref() == VM_INTERRUPT {
798            let code = if args.is_empty() {
799                0
800            } else {
801                args.remove(0).unwrap_or(0)
802            };
803            InterruptKind::Explicit { code }
804        } else {
805            InterruptKind::Intrinsic { op, name }
806        };
807        Some(Interrupt {
808            kind,
809            insn,
810            size,
811            args,
812            pc,
813        })
814    }
815
816    /// The interrupt the machine is stopped at, if any.
817    pub fn pending_interrupt(&self) -> Option<&Interrupt> {
818        self.pending.as_ref()
819    }
820
821    /// Supplies the effect of the operation the machine is stopped at and
822    /// steps past it.
823    ///
824    /// `value` is the operation's result, required when it declares one
825    /// ([`Interrupt::size`] is non-zero) and ignored otherwise. Any other
826    /// effect — a register written by a system call, memory filled by a host
827    /// service — the caller applies through [`Vm::emulator`] and
828    /// [`Vm::memory_mut`] before resuming. The operation's own instruction is
829    /// not run; the one after it is next.
830    pub fn resume(&mut self, value: Option<u128>) -> Result<(), ResumeError> {
831        let Some(interrupt) = self.pending.as_ref() else {
832            return Err(ResumeError::NotInterrupted);
833        };
834        if interrupt.size > 0 {
835            let Some(value) = value else {
836                return Err(ResumeError::ResultRequired {
837                    size: interrupt.size,
838                });
839            };
840            self.emu
841                .insn_values
842                .insert(interrupt.insn, SizedValue::from_bits(value, interrupt.size));
843        }
844        self.pending = None;
845        self.stats.steps += 1;
846        self.emu.idx += 1;
847        self.offer_rest = true;
848        Ok(())
849    }
850
851    /// Points the emulator at whatever block now covers `addr`, reusing the
852    /// emulator's own cached index rather than building one.
853    fn reposition(&mut self, addr: u64) {
854        if let Some(block) = self.emu.block_at_address(&self.ctx, addr)
855            && block != self.emu.block
856        {
857            self.emu.block = block;
858            self.emu.idx = 0;
859        }
860    }
861
862    /// Lifts `addr` and makes it visible to the emulator. Returns an exit only
863    /// if the address could not be supplied.
864    fn discover(&mut self, addr: u64) -> Option<VmExit> {
865        // Moved out, updated in place by the lift, and moved back: the index
866        // stays current without ever being rebuilt, and the borrow checker is
867        // satisfied because nothing borrows the emulator across the lift.
868        let mut index = self
869            .emu
870            .take_address_index()
871            .unwrap_or_else(|| AddressIndex::analyze(&self.ctx));
872        self.stats.lifts += 1;
873        let result = self.source.lift(
874            &mut self.ctx,
875            &self.emu.memory,
876            &mut index,
877            addr,
878            &mut self.stats,
879        );
880        self.emu.set_address_index(index);
881        if let Err(error) = result {
882            return Some(VmExit::Unlifted { addr, error });
883        }
884        if self.optimize
885            && let Some(block) = self.emu.block_at_address(&self.ctx, addr)
886        {
887            // Forwarding first: it turns the temp round trips into direct value
888            // uses, which is what leaves the surrounding computation dead.
889            let started = std::time::Instant::now();
890            let cleanup = crate::optimize::forward_temp_stores(&mut self.ctx, block);
891            qcode_passes::remove_dead_insns(&mut self.ctx, block);
892            self.stats.optimize += started.elapsed();
893            self.stats.forwarded_loads += cleanup.forwarded_loads as u64;
894            self.stats.removed_stores += cleanup.removed_stores as u64;
895        }
896        self.absorbed_into = self.absorb_into_basic_block(addr);
897        None
898    }
899
900    /// Points every address `block` has absorbed back at `block`.
901    ///
902    /// Absorption deletes the blocks it takes in, and each of them was what the
903    /// index named for its address. Left alone the index hands out ids of
904    /// deleted blocks — and a run may absorb a whole chain, not just the block
905    /// that was being discovered, so every address the absorber now covers has
906    /// to be repointed, not only the one that prompted this.
907    fn reindex_absorbed(&mut self, block: BlockId) {
908        let covered = self.ctx.block(block).extra_addresses.clone();
909        if covered.is_empty() {
910            return;
911        }
912        let mut index = self
913            .emu
914            .take_address_index()
915            .unwrap_or_else(|| AddressIndex::analyze(&self.ctx));
916        for addr in covered {
917            index.set_block(addr, block);
918        }
919        self.emu.set_address_index(index);
920    }
921
922    /// Re-runs the block cleanup over a block that has just grown.
923    ///
924    /// The cleanup at discovery saw a single guest instruction, where every
925    /// register it writes is still live at the block's edge. Absorption puts a
926    /// whole run in one block, and that is the first point at which a write
927    /// nothing goes on to read is visible as dead: the flags an arithmetic
928    /// instruction sets, when the next instruction overwrites all of them
929    /// before the branch reads any.
930    ///
931    /// Deliberately block-local (no alias result): at discovery the rest of the
932    /// CFG is still unknown, so only a store this block itself overwrites can
933    /// be proven dead. Anything live at the exit stays.
934    /// Records that `block` has grown and owes a cleanup, cleaning whatever
935    /// run was growing before it.
936    fn mark_dirty(&mut self, block: BlockId) {
937        // Grown, so it holds guest instructions the injectors have not seen.
938        self.injected.remove(&block);
939        if !self.optimize {
940            return;
941        }
942        let previous = self.dirty.replace(block);
943        if let Some(previous) = previous
944            && previous != block
945            // Discovery retires blocks — splitting an absorbed run empties
946            // both halves — so the one that was growing may be gone.
947            && self.ctx.contains_block(previous)
948        {
949            self.reoptimize(previous);
950        }
951    }
952
953    /// Cleans the block the machine is about to run, if it has grown since it
954    /// was last cleaned.
955    ///
956    /// Only ever the block being entered, which is why a stale id cannot be
957    /// reached here: absorption and splitting retire blocks that may still be
958    /// listed, but the machine can only be about to run a live one. A leftover
959    /// entry for a retired id is harmless — at worst it cleans a block whose id
960    /// was reused, which is always safe.
961    fn clean_before_entering(&mut self, block: BlockId) {
962        if self.dirty == Some(block) {
963            self.dirty = None;
964            self.reoptimize(block);
965        }
966    }
967
968    fn reoptimize(&mut self, block: BlockId) {
969        if !self.optimize {
970            return;
971        }
972        let started = std::time::Instant::now();
973        let cleanup = crate::optimize::forward_temp_stores(&mut self.ctx, block);
974        qcode_passes::remove_dead_insns(&mut self.ctx, block);
975        self.stats.optimize += started.elapsed();
976        self.stats.forwarded_loads += cleanup.forwarded_loads as u64;
977        self.stats.removed_stores += cleanup.removed_stores as u64;
978        // The interpreter may hold this block's instruction list, and some of
979        // those instructions are gone.
980        self.emu.invalidate_block_cache();
981    }
982
983    /// Folds the block just lifted at `addr` into its predecessor, when the two
984    /// are a straight-line pair.
985    ///
986    /// Lifting is per guest instruction, so a run of straight-line guest code
987    /// arrives as a chain of one-instruction blocks joined by unconditional
988    /// branches. Left that way, every guest instruction is its own unit of
989    /// execution: a separate compilation, a separate entry into compiled code
990    /// and a separate return to the interpreter for its terminator. Folding the
991    /// chain as it is discovered rebuilds the guest's *basic block*, which is
992    /// the unit worth compiling.
993    ///
994    /// Absorbing an address does not settle that it belongs here — code
995    /// discovered later may branch into the middle of the run, and
996    /// [`Context::split_block_at_address`] breaks it apart again when it does.
997    fn absorb_into_basic_block(&mut self, addr: u64) -> Option<BlockId> {
998        let filled = self.emu.block_at_address(&self.ctx, addr)?;
999
1000        // Forward: the rest of this run may already be known. That is the shape
1001        // a split leaves behind — it re-establishes a block's *start* while
1002        // everything after it is still lifted — and the shape a back-edge into
1003        // the middle of a run creates generally.
1004        let forward = qcode_passes::absorb_straight_line(&mut self.ctx, filled);
1005        self.stats.absorbed += forward as u64;
1006        if forward > 0 {
1007            self.reindex_absorbed(filled);
1008            self.mark_dirty(filled);
1009        }
1010
1011        // Backward: the straight-line predecessor that branched here, for the
1012        // ordinary case of a run discovered one guest instruction at a time.
1013        //
1014        // Not if something branches to this address: it has to keep *starting*
1015        // a block, or the split that established that would be undone here.
1016        // Extending it forward, above, stays fine — that moves its end, not its
1017        // start.
1018        if self
1019            .emu
1020            .address_index()
1021            .is_some_and(|index| index.is_boundary(addr))
1022        {
1023            return (forward > 0).then_some(filled);
1024        }
1025        // Exactly one predecessor, or absorbing would strand the others.
1026        // Collected eagerly so the module is free to be mutated below.
1027        let preds: Vec<BlockId> = BasicBlock::from_id(&self.ctx, filled)
1028            .predecessors()
1029            .map(|(_, block)| block)
1030            .take(2)
1031            .collect();
1032        let [head] = preds[..] else {
1033            return None;
1034        };
1035        if head == filled {
1036            return None;
1037        }
1038        // Only into a block that starts at a machine address. Absorbing makes
1039        // the head responsible for the absorbed addresses, and a later branch
1040        // to one of them splits the head apart again — which works by emptying
1041        // it and lifting it afresh. A block with no address of its own (the
1042        // fallthrough arm of a branch *inside* one instruction's p-code) has
1043        // nowhere to be lifted from, so emptying it leaves a hole nothing can
1044        // fill.
1045        if self.ctx.block(head).address.is_none() {
1046            return (forward > 0).then_some(filled);
1047        }
1048        // Where `filled`'s instructions land: the head's own, less the
1049        // terminator that absorption drops.
1050        let offset = self
1051            .ctx
1052            .block(head)
1053            .instruction_ids()
1054            .len()
1055            .saturating_sub(1);
1056        if qcode_passes::absorb_straight_line(&mut self.ctx, head) == 0 {
1057            return (forward > 0).then_some(filled);
1058        }
1059        self.stats.absorbed += 1;
1060
1061        // What the head has already run: everything it held before the
1062        // absorbed instructions were appended. The machine resumes right
1063        // after the last of these still standing, whatever cleanup deletes
1064        // ahead of that point or injection inserts after it — an interrupt an
1065        // injector places before the first absorbed instruction, say, which
1066        // has to run before it.
1067        let executed: FxHashSet<LocalInsnId> = self.ctx.block(head).instruction_ids()[..offset]
1068            .iter()
1069            .copied()
1070            .collect();
1071        self.mark_dirty(head);
1072
1073        self.reindex_absorbed(head);
1074
1075        // The machine stopped at the empty placeholder this lift filled, which
1076        // absorption has just deleted; its instructions are in the head now.
1077        if self.emu.block == filled {
1078            // The absorbed instructions have not run, and the injectors have
1079            // not seen them: rewrite now, so a hook on the instruction about
1080            // to execute is not missed the first time.
1081            self.inject(head);
1082            let now = self.ctx.block(head).instruction_ids();
1083            let resumed = now
1084                .iter()
1085                .rposition(|local| executed.contains(local))
1086                .map_or(0, |last| last + 1);
1087            self.emu.block = head;
1088            self.emu.idx = resumed;
1089            self.emu.invalidate_block_cache();
1090        }
1091        Some(head)
1092    }
1093
1094    /// Runs until the machine stops, or until `budget` p-code operations have
1095    /// been retired.
1096    ///
1097    /// Hooks registered through the [table](crate::table) are called from
1098    /// here and the machine resumes past them, so the run only returns for
1099    /// an exit the caller has to see.
1100    pub fn run(&mut self, budget: u64) -> VmExit {
1101        let deadline = self.stats.steps + budget;
1102        while self.stats.steps < deadline {
1103            // Checked before the step so a breakpoint reports the instruction
1104            // about to run, not the one after it, and so resuming from a
1105            // breakpoint is possible without immediately re-triggering it.
1106            //
1107            // Guarded on there being any breakpoint at all: `pc()` resolves the
1108            // block through the module arena, and paying that on every step to
1109            // consult an empty set cost about 6% of run time.
1110            if !self.breakpoints.is_empty()
1111                && let Some(pc) = self.pc()
1112                && self.breakpoints.contains(&pc)
1113                && self.stats.steps > 0
1114            {
1115                return VmExit::Breakpoint(pc);
1116            }
1117            match self.step() {
1118                None => {}
1119                Some(VmExit::Interrupt(interrupt)) => {
1120                    if let Some(exit) = self.dispatch(&interrupt) {
1121                        return exit;
1122                    }
1123                }
1124                Some(exit) => return exit,
1125            }
1126        }
1127        VmExit::InstructionLimit
1128    }
1129}
1130
1131/// Resolves a guest address to a block against an existing index.
1132fn resolve(ctx: &Context<'_>, index: &AddressIndex, addr: u64) -> Option<BlockId> {
1133    match index.get(addr) {
1134        Some(AddressTarget::Block(block)) => Some(block),
1135        Some(AddressTarget::Function(function)) => {
1136            qcode::value::FunctionBody::from_id(ctx, function)
1137                .root()
1138                .map(|root| root.id)
1139        }
1140        None => None,
1141    }
1142}
1143
1144#[cfg(test)]
1145mod tests {
1146    use super::*;
1147    use crate::mmu::{PAGE_SIZE, perm};
1148    use qcode::value::FunctionBody;
1149
1150    /// A source that hands over one pre-planned block per address, so the
1151    /// discovery path can be exercised without a decoder.
1152    #[derive(Default)]
1153    struct Planned {
1154        /// Addresses this source is willing to supply, and how many times it was
1155        /// actually asked.
1156        available: Vec<u64>,
1157        pub calls: Vec<u64>,
1158    }
1159
1160    impl CodeSource for Planned {
1161        fn lift(
1162            &mut self,
1163            ctx: &mut Context<'static>,
1164            memory: &VmMemory,
1165            index: &mut AddressIndex,
1166            addr: u64,
1167            _stats: &mut Stats,
1168        ) -> Result<(), CodeError> {
1169            self.calls.push(addr);
1170            // A real source fetches through the MMU, so executing unmapped or
1171            // non-executable memory faults at the fetch. Mirrored here.
1172            let mut byte = [0u8; 1];
1173            memory
1174                .mmu
1175                .read_code(addr, &mut byte)
1176                .map_err(CodeError::Fault)?;
1177            if !self.available.contains(&addr) {
1178                return Err(CodeError::Decode("no plan for this address".into()));
1179            }
1180            let function = FunctionBody::make_at_addr(ctx, addr, None).id;
1181            let block = BasicBlock::make(ctx, function).with_address(addr).id;
1182            index
1183                .register(ctx, addr, AddressTarget::Block(block))
1184                .map_err(|error| CodeError::Decode(format!("{error:?}").into()))?;
1185            Ok(())
1186        }
1187    }
1188
1189    /// A module with a single empty block at `addr`.
1190    fn module(addr: u64) -> (Context<'static>, BlockId) {
1191        let mut ctx = Context::new();
1192        let function = FunctionBody::make_at_addr(&mut ctx, addr, None).id;
1193        let block = BasicBlock::make(&mut ctx, function).with_address(addr).id;
1194        (ctx, block)
1195    }
1196
1197    fn executable_memory() -> VmMemory {
1198        let mut memory = VmMemory::new();
1199        memory.mmu.map(0x1000, PAGE_SIZE, perm::RX_INIT).unwrap();
1200        memory
1201    }
1202
1203    #[test]
1204    fn an_empty_addressed_block_asks_the_source_for_code() {
1205        let (ctx, block) = module(0x1000);
1206        let mut vm = Vm::new(ctx, block, Planned::default());
1207        // An empty block carrying an address is how the lifter represents a
1208        // branch target it has not reached yet, so it is a discovery request.
1209        // This source cannot supply it, which is what makes the exit reportable.
1210        let exit = vm.run(16);
1211        assert!(
1212            matches!(exit, VmExit::Unlifted { addr: 0x1000, .. }),
1213            "expected a discovery attempt, got {exit:?}"
1214        );
1215        assert_eq!(vm.source.calls, vec![0x1000]);
1216    }
1217
1218    #[test]
1219    fn an_empty_block_with_no_address_is_an_error() {
1220        // Nothing to discover: without an address there is no code to fetch.
1221        let mut ctx = Context::new();
1222        let function = FunctionBody::make_at_addr(&mut ctx, 0x1000, None).id;
1223        let block = BasicBlock::make(&mut ctx, function).id;
1224        let mut vm = Vm::new(ctx, block, Planned::default());
1225        assert!(matches!(vm.run(16), VmExit::Error(_)));
1226    }
1227
1228    #[test]
1229    fn pc_reports_the_block_about_to_run() {
1230        let (ctx, block) = module(0x1000);
1231        let vm = Vm::new(ctx, block, Planned::default());
1232        assert_eq!(vm.pc(), Some(0x1000));
1233    }
1234
1235    #[test]
1236    fn at_address_lifts_an_entry_the_module_lacks() {
1237        let ctx = Context::new();
1238        let source = Planned {
1239            available: vec![0x1000],
1240            calls: Vec::new(),
1241        };
1242        let vm = Vm::at_address(ctx, 0x1000, source, executable_memory())
1243            .expect("the source can supply this address");
1244        assert_eq!(vm.pc(), Some(0x1000));
1245    }
1246
1247    #[test]
1248    fn at_address_reuses_a_block_the_module_already_has() {
1249        let (ctx, _) = module(0x1000);
1250        let vm = Vm::at_address(ctx, 0x1000, Planned::default(), executable_memory())
1251            .expect("no lifting is needed");
1252        assert_eq!(vm.pc(), Some(0x1000));
1253        // The source was never consulted.
1254        assert!(vm.source.calls.is_empty());
1255    }
1256
1257    #[test]
1258    fn fetching_from_non_executable_memory_reports_the_fault() {
1259        let ctx = Context::new();
1260        let mut memory = VmMemory::new();
1261        memory.mmu.map(0x1000, PAGE_SIZE, perm::RW_INIT).unwrap();
1262        let source = Planned {
1263            available: vec![0x1000],
1264            calls: Vec::new(),
1265        };
1266        let error = Vm::at_address(ctx, 0x1000, source, memory)
1267            .err()
1268            .expect("the page is not executable");
1269        assert!(matches!(
1270            error,
1271            CodeError::Fault(MemFault {
1272                kind: crate::mmu::FaultKind::ExecViolation,
1273                addr: 0x1000
1274            })
1275        ));
1276    }
1277
1278    #[test]
1279    fn an_unsuppliable_address_reports_where_it_stopped() {
1280        let ctx = Context::new();
1281        let error = Vm::at_address(ctx, 0x2000, Planned::default(), executable_memory())
1282            .err()
1283            .expect("nothing is mapped or planned at 0x2000");
1284        assert!(matches!(error, CodeError::Fault(_)));
1285    }
1286
1287    #[test]
1288    fn breakpoints_are_recorded_and_removable() {
1289        let (ctx, block) = module(0x1000);
1290        let mut vm = Vm::new(ctx, block, Planned::default());
1291        assert!(vm.add_breakpoint(0x2000));
1292        assert!(!vm.add_breakpoint(0x2000));
1293        assert!(vm.remove_breakpoint(0x2000));
1294        assert!(!vm.remove_breakpoint(0x2000));
1295    }
1296
1297    #[test]
1298    fn memory_is_reachable_and_backed_by_the_mmu() {
1299        let (ctx, block) = module(0x1000);
1300        let mut vm = Vm::new(ctx, block, Planned::default());
1301        vm.memory_mut()
1302            .mmu
1303            .map(0x4000, PAGE_SIZE, perm::RW_INIT)
1304            .unwrap();
1305        vm.memory_mut().mmu.write(0x4000, &[1, 2, 3]).unwrap();
1306        let mut out = [0; 3];
1307        vm.memory().mmu.read(0x4000, &mut out).unwrap();
1308        assert_eq!(out, [1, 2, 3]);
1309    }
1310
1311    /// A block whose body is one user op, followed by a branch to an empty
1312    /// block at the next address, so continuing past the op is observable as
1313    /// a discovery request for that address.
1314    fn module_with_op(
1315        op_name: &str,
1316        args: Vec<u64>,
1317        size: usize,
1318    ) -> (Context<'static>, BlockId, InstructionId) {
1319        let (mut ctx, block) = module(0x1000);
1320        let op = ctx.shared.pcode_op(op_name);
1321        let target = BasicBlock::make(&mut ctx, block.func)
1322            .with_address(0x1001)
1323            .id;
1324        let args = args
1325            .into_iter()
1326            .map(|value| ctx.shared.get_const(value, 8))
1327            .collect();
1328        let insn = {
1329            let mut builder = ctx.builder(block);
1330            let insn = builder.push_pcode_op(op, args, None, size).id;
1331            builder.finalize(target);
1332            insn
1333        };
1334        (ctx, block, insn)
1335    }
1336
1337    #[test]
1338    fn an_explicit_interrupt_stops_at_the_op_and_reports_its_operands() {
1339        let (ctx, block, insn) = module_with_op(VM_INTERRUPT, vec![7, 99], 8);
1340        let mut vm = Vm::new(ctx, block, Planned::default());
1341        let exit = vm.run(16);
1342        let VmExit::Interrupt(interrupt) = exit else {
1343            panic!("expected an interrupt, got {exit:?}");
1344        };
1345        assert_eq!(interrupt.kind, InterruptKind::Explicit { code: 7 });
1346        assert_eq!(interrupt.args, vec![Some(99)]);
1347        assert_eq!(interrupt.insn, insn);
1348        assert_eq!(interrupt.size, 8);
1349        assert_eq!(interrupt.pc, Some(0x1000));
1350        // The machine is at the op, not past it.
1351        assert_eq!(vm.emulator().idx, 0);
1352        assert_eq!(vm.pending_interrupt(), Some(&interrupt));
1353    }
1354
1355    #[test]
1356    fn running_again_without_resuming_reports_the_same_interrupt() {
1357        let (ctx, block, _) = module_with_op(VM_INTERRUPT, vec![1], 0);
1358        let mut vm = Vm::new(ctx, block, Planned::default());
1359        let first = vm.run(16);
1360        let again = vm.run(16);
1361        assert!(
1362            matches!(&first, VmExit::Interrupt(i) if i.kind == InterruptKind::Explicit { code: 1 })
1363        );
1364        assert!(
1365            matches!(&again, VmExit::Interrupt(i) if i.kind == InterruptKind::Explicit { code: 1 })
1366        );
1367        assert_eq!(vm.emulator().idx, 0);
1368    }
1369
1370    #[test]
1371    fn resume_files_the_result_and_continues_after_the_op() {
1372        let (ctx, block, insn) = module_with_op(VM_INTERRUPT, vec![7], 8);
1373        let mut vm = Vm::new(ctx, block, Planned::default());
1374        assert!(matches!(vm.run(16), VmExit::Interrupt(_)));
1375        // The op declares an 8-byte result, so resuming needs one.
1376        assert_eq!(
1377            vm.resume(None),
1378            Err(ResumeError::ResultRequired { size: 8 })
1379        );
1380        vm.resume(Some(42)).unwrap();
1381        assert_eq!(vm.pending_interrupt(), None);
1382        assert_eq!(
1383            vm.emulator().insn_values.get(&insn).map(|v| v.as_bits()),
1384            Some(42)
1385        );
1386        // Past the op, the branch runs and reaches the next address.
1387        assert!(matches!(vm.run(16), VmExit::Unlifted { addr: 0x1001, .. }));
1388    }
1389
1390    #[test]
1391    fn resume_needs_an_interrupt() {
1392        let (ctx, block) = module(0x1000);
1393        let mut vm = Vm::new(ctx, block, Planned::default());
1394        assert_eq!(vm.resume(None), Err(ResumeError::NotInterrupted));
1395    }
1396
1397    #[test]
1398    fn an_unmodelled_user_op_is_an_intrinsic_interrupt() {
1399        let (ctx, block, _) = module_with_op("rdpmc", vec![], 0);
1400        let mut vm = Vm::new(ctx, block, Planned::default());
1401        let exit = vm.run(16);
1402        let VmExit::Interrupt(interrupt) = exit else {
1403            panic!("expected an interrupt, got {exit:?}");
1404        };
1405        assert!(
1406            matches!(&interrupt.kind, InterruptKind::Intrinsic { name, .. } if name.as_ref() == "rdpmc")
1407        );
1408        assert_eq!(interrupt.size, 0);
1409        // Nothing to supply for an op with no result.
1410        vm.resume(None).unwrap();
1411        assert!(matches!(vm.run(16), VmExit::Unlifted { addr: 0x1001, .. }));
1412    }
1413}