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::{BasicBlock, BlockId},
22};
23use qcode_emulator::{EmulatorErrorKind, EmulatorMemory, StandaloneEmulator};
24use rustc_hash::FxHashSet;
25
26use crate::{memory::VmMemory, mmu::MemFault, stats::Stats};
27
28/// Why a lifting attempt failed.
29#[derive(Debug, Clone)]
30pub enum CodeError {
31    /// The instruction bytes could not be fetched.
32    Fault(MemFault),
33    /// The bytes were fetched but did not decode, or did not lift.
34    Decode(Box<str>),
35}
36
37/// Supplies code the machine has not seen yet.
38///
39/// Kept as a trait so the VM does not depend on SLEIGH: a decoder is a policy
40/// choice (which specification, which variant), and a test wants to hand over
41/// blocks without compiling one. An implementation reads instruction bytes from
42/// the [`Mmu`](crate::Mmu) — via [`read_code`](crate::Mmu::read_code), so that
43/// executing a non-executable page faults at the fetch — and lowers them into
44/// `ctx`.
45pub trait CodeSource {
46    /// Lifts the code at `addr` into `ctx`.
47    ///
48    /// Returning `Ok(())` asserts that a block starting at `addr` now exists;
49    /// the VM re-resolves the address itself rather than trusting a returned id,
50    /// so a source is free to lift a whole run of instructions at once.
51    ///
52    /// `index` is the machine's live address lookup, and the implementation must
53    /// keep it current as it adds blocks — every lifting entry point takes one
54    /// for exactly this reason. Rebuilding it per instruction instead is
55    /// quadratic in the size of the module discovered so far.
56    ///
57    /// `stats` is the machine's own counters: an implementation records the
58    /// time it spends fetching and decoding there, so a benchmark can separate
59    /// translation cost from interpretation cost.
60    fn lift(
61        &mut self,
62        ctx: &mut Context<'static>,
63        memory: &VmMemory,
64        index: &mut AddressIndex,
65        addr: u64,
66        stats: &mut Stats,
67    ) -> Result<(), CodeError>;
68}
69
70/// An alternative way to execute a block's body.
71///
72/// The interpreter is always present and always correct; an executor is an
73/// *optimisation* that may decline any block for any reason, in which case the
74/// interpreter runs it unchanged. That is what lets a backend be partial: a JIT
75/// need only handle the shapes it handles well.
76///
77/// An executor runs the block's body, not its terminator. Control flow, block
78/// parameters and call semantics stay in one implementation.
79///
80/// It is handed the whole emulator rather than just its memory because the
81/// interpreter still has to run the terminator, and a terminator reads
82/// operands — a `cbranch` condition, a branch's block arguments. Those are
83/// values the body produced, so an executor that keeps them somewhere other
84/// than the interpreter's value table must put them back before returning.
85pub trait BlockExecutor {
86    /// Runs everything in `block` except its terminator.
87    ///
88    /// `Ok(None)` means "not mine" and is not an error — the caller falls back
89    /// to the interpreter.
90    ///
91    /// On `Ok(Some(_))` every value the terminator of [`Executed::block`] reads
92    /// must be readable from `emu.insn_values`, exactly as if the interpreter
93    /// had run that body.
94    ///
95    /// `chain` lets the executor run on past `block` into successors it also
96    /// handles, instead of handing control back after one. Deciding a branch
97    /// itself is how an executor keeps control inside its own code rather than
98    /// paying a round trip per block. The caller withholds it when something
99    /// needs to observe every block — a breakpoint is set, say — because blocks
100    /// crossed this way are never offered to the interpreter.
101    fn run_block(
102        &mut self,
103        ctx: &Context<'_>,
104        emu: &mut StandaloneEmulator<VmMemory>,
105        block: BlockId,
106        chain: bool,
107    ) -> Result<Option<Executed>, EmulatorErrorKind>;
108}
109
110/// Where an executor left the machine.
111#[derive(Debug, Clone, Copy)]
112pub struct Executed {
113    /// The block whose terminator the interpreter still has to run. With
114    /// chaining this is the last of several, not the one that was asked for.
115    pub block: BlockId,
116    /// How many instructions of that block were retired: its body.
117    pub body: usize,
118    /// Operations retired across every block run, for accounting.
119    pub retired: u64,
120}
121
122/// Why the machine stopped.
123#[derive(Debug, Clone)]
124pub enum VmExit {
125    /// The step budget ran out. The machine is resumable.
126    InstructionLimit,
127    /// Execution reached an address with a breakpoint on it. The breakpoint
128    /// instruction has *not* been executed.
129    Breakpoint(u64),
130    /// A memory access failed.
131    Fault(MemFault),
132    /// Code at this address could not be lifted.
133    Unlifted { addr: u64, error: CodeError },
134    /// The interpreter reported something the VM does not model as a guest
135    /// event — an unsupported p-code op, a malformed block.
136    Error(Box<str>),
137}
138
139/// A machine: an owned module, a memory, and a position in the code.
140pub struct Vm<S> {
141    ctx: Context<'static>,
142    emu: StandaloneEmulator<VmMemory>,
143    source: S,
144    /// Counters and phase timings for this run.
145    pub stats: Stats,
146    /// An optional faster path for block bodies. `None` means the interpreter
147    /// executes everything, which is always a valid way to run.
148    executor: Option<Box<dyn BlockExecutor>>,
149    /// Set by a lift that folded the block it filled into a predecessor, which
150    /// leaves the machine already positioned. Taken by the step that asked for
151    /// the lift.
152    absorbed_into: Option<BlockId>,
153    /// Whether freshly lifted blocks get a cleanup round.
154    ///
155    /// Lifting one machine instruction emits every side effect the
156    /// specification describes, including flag computations the surrounding
157    /// code never reads. Removing the ones with no users at all is sound
158    /// block-locally — an instruction with no users cannot be observed — and is
159    /// paid once per block instead of on every execution of it.
160    pub optimize: bool,
161    /// The block that has grown by absorption and not been cleaned since.
162    ///
163    /// Absorption folds a straight-line run one guest instruction at a time,
164    /// and cleaning the whole enlarged block after each one is quadratic in the
165    /// length of the run — which on unrolled code is the dominant cost of
166    /// translation. The cleanup is deferred to the point the block is next
167    /// entered at its first instruction, by which time the run has stopped
168    /// growing and one pass does the work of all of them.
169    ///
170    /// At most one: absorption extends one run at a time, so a *different*
171    /// block being absorbed into means the previous run has stopped growing
172    /// and can be cleaned right there. Waiting for it to be entered again
173    /// instead would let compiled code be built from the uncleaned form — and
174    /// worse, would clean it in an interpreted run but not in a chained
175    /// compiled one, leaving the two strategies running different QCode.
176    dirty: Option<BlockId>,
177    breakpoints: FxHashSet<u64>,
178}
179
180impl<S: CodeSource> Vm<S> {
181    /// Builds a machine positioned at `entry`.
182    pub fn new(ctx: Context<'static>, entry: BlockId, source: S) -> Self {
183        let mut emu = StandaloneEmulator::<VmMemory>::new_in(entry);
184        emu.memory.configure_spaces(&ctx);
185        Self {
186            ctx,
187            emu,
188            source,
189            optimize: true,
190            stats: Stats::default(),
191            executor: None,
192            absorbed_into: None,
193            dirty: None,
194            breakpoints: FxHashSet::default(),
195        }
196    }
197
198    /// Builds a machine positioned at a guest address, lifting the entry block
199    /// if the module does not already contain it.
200    pub fn at_address(
201        mut ctx: Context<'static>,
202        addr: u64,
203        mut source: S,
204        memory: VmMemory,
205    ) -> Result<Self, CodeError> {
206        // Built once here and handed to the machine, which keeps it current
207        // from then on.
208        let mut index = AddressIndex::analyze(&ctx);
209        let mut stats = Stats::default();
210        if resolve(&ctx, &index, addr).is_none() {
211            stats.lifts += 1;
212            source.lift(&mut ctx, &memory, &mut index, addr, &mut stats)?;
213        }
214        let entry = resolve(&ctx, &index, addr).ok_or_else(|| {
215            CodeError::Decode(format!("no block at {addr:#x} after lifting").into())
216        })?;
217        let mut vm = Self::new(ctx, entry, source);
218        vm.emu.memory = memory;
219        vm.emu.memory.configure_spaces(&vm.ctx);
220        vm.emu.set_address_index(index);
221        vm.stats = stats;
222        Ok(vm)
223    }
224
225    /// Installs an alternative executor for block bodies, replacing any
226    /// previous one. Purely an optimisation: removing it changes speed, not
227    /// behaviour.
228    pub fn set_block_executor(&mut self, executor: Box<dyn BlockExecutor>) {
229        self.executor = Some(executor);
230    }
231
232    pub fn clear_block_executor(&mut self) {
233        self.executor = None;
234    }
235
236    pub fn context(&self) -> &Context<'static> {
237        &self.ctx
238    }
239
240    pub fn memory(&self) -> &VmMemory {
241        &self.emu.memory
242    }
243
244    pub fn memory_mut(&mut self) -> &mut VmMemory {
245        &mut self.emu.memory
246    }
247
248    /// The emulator underneath, for register access and harness seeding.
249    pub fn emulator(&mut self) -> &mut StandaloneEmulator<VmMemory> {
250        &mut self.emu
251    }
252
253    /// The guest address of the block about to execute, if it has one.
254    pub fn pc(&self) -> Option<u64> {
255        BasicBlock::from_id(&self.ctx, self.emu.block).address()
256    }
257
258    pub fn add_breakpoint(&mut self, addr: u64) -> bool {
259        self.breakpoints.insert(addr)
260    }
261
262    pub fn remove_breakpoint(&mut self, addr: u64) -> bool {
263        self.breakpoints.remove(&addr)
264    }
265
266    /// Executes one instruction, lifting code on demand if control leaves the
267    /// part of the module already known.
268    ///
269    /// Returns `None` when the step was ordinary, and `Some(exit)` when the
270    /// machine stopped for a reason worth reporting.
271    pub fn step(&mut self) -> Option<VmExit> {
272        // A branch to unlifted code fails *before* the emulator moves, so the
273        // address can be lifted and the same step retried. One retry is enough:
274        // the second failure means the source did not produce the block it
275        // claimed to, which is a source bug rather than a discovery step.
276        for attempt in 0..2 {
277            // At its first instruction a block is between runs, which is the
278            // one moment a deferred cleanup can be taken without disturbing a
279            // position inside it — and it has to happen before the executor
280            // looks, or compiled code gets built from uncleaned QCode.
281            if self.emu.idx == 0 {
282                let block = self.emu.block;
283                self.clean_before_entering(block);
284            }
285
286            // At a block's first instruction, an installed executor may run the
287            // whole body at once, leaving the interpreter only the terminator.
288            if self.emu.idx == 0
289                && let Some(executor) = self.executor.as_mut()
290            {
291                let block = self.emu.block;
292                // Blocks the executor runs are never offered to the interpreter, so
293                // it may only run past the first when nothing needs to see them.
294                let chain = self.breakpoints.is_empty();
295                match executor.run_block(&self.ctx, &mut self.emu, block, chain) {
296                    Ok(Some(run)) => {
297                        // The operations were retired by the executor; they are
298                        // counted so throughput stays comparable between strategies.
299                        self.stats.steps += run.retired;
300                        self.stats.native_bodies += 1;
301                        // Positioning inside a block the interpreter has not walked
302                        // into invalidates its cached instruction list.
303                        self.emu.invalidate_block_cache();
304                        self.emu.block = run.block;
305                        self.emu.idx = run.body;
306                    }
307                    Ok(None) => {}
308                    Err(kind) => {
309                        let fault = self.emu.memory.take_fault();
310                        return Some(match fault {
311                            Some(fault) => VmExit::Fault(fault),
312                            None => VmExit::Error(kind.to_string().into()),
313                        });
314                    }
315                }
316            }
317
318            match self.emu.step(&self.ctx) {
319                Ok(()) => {
320                    self.stats.steps += 1;
321                    return None;
322                }
323                Err(error) => match error.kind {
324                    EmulatorErrorKind::InvalidBlockAddress(addr)
325                    | EmulatorErrorKind::UnknownAddress(addr)
326                        if attempt == 0 =>
327                    {
328                        if let Some(exit) = self.discover(addr) {
329                            return Some(exit);
330                        }
331                    }
332                    // A direct branch to code that has not been lifted does not
333                    // fail to resolve: the lifter materializes the target as an
334                    // *empty* block at that address, and execution walks into
335                    // it. So an empty block carrying an address is a request to
336                    // discover it, not a malformed-IR error.
337                    EmulatorErrorKind::EmptyBlock(block) if attempt == 0 => {
338                        let Some(addr) = BasicBlock::from_id(&self.ctx, block).address() else {
339                            return Some(VmExit::Error(
340                                EmulatorErrorKind::EmptyBlock(block).to_string().into(),
341                            ));
342                        };
343                        // The address may already be lifted: a branch back into
344                        // known code still gets a fresh placeholder block in the
345                        // branching instruction's own function, and lifting it
346                        // again would collide with the function that owns it.
347                        // Resolving first is what makes loops work.
348                        let before = self.emu.block;
349                        self.reposition(addr);
350                        if self.emu.block != before {
351                            // Already lifted: a translation-cache hit.
352                            self.stats.resolves += 1;
353                            continue;
354                        }
355                        if let Some(exit) = self.discover(addr) {
356                            return Some(exit);
357                        }
358                        // Lifting may place the instruction in a *new* block
359                        // rather than filling the placeholder the emulator is
360                        // sitting in, so the machine has to be moved onto
361                        // whatever now covers the address — unless the lift
362                        // folded that block into its predecessor, which
363                        // positions the machine itself. `addr` is interior to
364                        // the absorbing block then, and resolving it by address
365                        // would land at that block's *start*.
366                        if self.absorbed_into.take().is_none() {
367                            self.reposition(addr);
368                        }
369                    }
370                    EmulatorErrorKind::MemoryReadError(addr)
371                    | EmulatorErrorKind::MemoryWriteError(addr) => {
372                        // The backend records the precise cause; the error alone
373                        // could only say that an access at this address failed.
374                        let fault = self.emu.memory.take_fault().unwrap_or(MemFault {
375                            kind: crate::mmu::FaultKind::ReadUnmapped,
376                            addr,
377                        });
378                        return Some(VmExit::Fault(fault));
379                    }
380                    kind => return Some(VmExit::Error(kind.to_string().into())),
381                },
382            }
383        }
384        None
385    }
386
387    /// Points the emulator at whatever block now covers `addr`, reusing the
388    /// emulator's own cached index rather than building one.
389    fn reposition(&mut self, addr: u64) {
390        if let Some(block) = self.emu.block_at_address(&self.ctx, addr)
391            && block != self.emu.block
392        {
393            self.emu.block = block;
394            self.emu.idx = 0;
395        }
396    }
397
398    /// Lifts `addr` and makes it visible to the emulator. Returns an exit only
399    /// if the address could not be supplied.
400    fn discover(&mut self, addr: u64) -> Option<VmExit> {
401        // Moved out, updated in place by the lift, and moved back: the index
402        // stays current without ever being rebuilt, and the borrow checker is
403        // satisfied because nothing borrows the emulator across the lift.
404        let mut index = self
405            .emu
406            .take_address_index()
407            .unwrap_or_else(|| AddressIndex::analyze(&self.ctx));
408        self.stats.lifts += 1;
409        let result = self.source.lift(
410            &mut self.ctx,
411            &self.emu.memory,
412            &mut index,
413            addr,
414            &mut self.stats,
415        );
416        self.emu.set_address_index(index);
417        if let Err(error) = result {
418            return Some(VmExit::Unlifted { addr, error });
419        }
420        if self.optimize
421            && let Some(block) = self.emu.block_at_address(&self.ctx, addr)
422        {
423            // Forwarding first: it turns the temp round trips into direct value
424            // uses, which is what leaves the surrounding computation dead.
425            let started = std::time::Instant::now();
426            let cleanup = crate::optimize::forward_temp_stores(&mut self.ctx, block);
427            qcode_passes::remove_dead_insns(&mut self.ctx, block);
428            self.stats.optimize += started.elapsed();
429            self.stats.forwarded_loads += cleanup.forwarded_loads as u64;
430            self.stats.removed_stores += cleanup.removed_stores as u64;
431        }
432        self.absorbed_into = self.absorb_into_basic_block(addr);
433        None
434    }
435
436    /// Points every address `block` has absorbed back at `block`.
437    ///
438    /// Absorption deletes the blocks it takes in, and each of them was what the
439    /// index named for its address. Left alone the index hands out ids of
440    /// deleted blocks — and a run may absorb a whole chain, not just the block
441    /// that was being discovered, so every address the absorber now covers has
442    /// to be repointed, not only the one that prompted this.
443    fn reindex_absorbed(&mut self, block: BlockId) {
444        let covered = self.ctx.block(block).extra_addresses.clone();
445        if covered.is_empty() {
446            return;
447        }
448        let mut index = self
449            .emu
450            .take_address_index()
451            .unwrap_or_else(|| AddressIndex::analyze(&self.ctx));
452        for addr in covered {
453            index.set_block(addr, block);
454        }
455        self.emu.set_address_index(index);
456    }
457
458    /// Re-runs the block cleanup over a block that has just grown.
459    ///
460    /// The cleanup at discovery saw a single guest instruction, where every
461    /// register it writes is still live at the block's edge. Absorption puts a
462    /// whole run in one block, and that is the first point at which a write
463    /// nothing goes on to read is visible as dead: the flags an arithmetic
464    /// instruction sets, when the next instruction overwrites all of them
465    /// before the branch reads any.
466    ///
467    /// Deliberately block-local (no alias result): at discovery the rest of the
468    /// CFG is still unknown, so only a store this block itself overwrites can
469    /// be proven dead. Anything live at the exit stays.
470    /// Records that `block` has grown and owes a cleanup, cleaning whatever
471    /// run was growing before it.
472    fn mark_dirty(&mut self, block: BlockId) {
473        if !self.optimize {
474            return;
475        }
476        let previous = self.dirty.replace(block);
477        if let Some(previous) = previous
478            && previous != block
479            // Discovery retires blocks — splitting an absorbed run empties
480            // both halves — so the one that was growing may be gone.
481            && self.ctx.contains_block(previous)
482        {
483            self.reoptimize(previous);
484        }
485    }
486
487    /// Cleans the block the machine is about to run, if it has grown since it
488    /// was last cleaned.
489    ///
490    /// Only ever the block being entered, which is why a stale id cannot be
491    /// reached here: absorption and splitting retire blocks that may still be
492    /// listed, but the machine can only be about to run a live one. A leftover
493    /// entry for a retired id is harmless — at worst it cleans a block whose id
494    /// was reused, which is always safe.
495    fn clean_before_entering(&mut self, block: BlockId) {
496        if self.dirty == Some(block) {
497            self.dirty = None;
498            self.reoptimize(block);
499        }
500    }
501
502    fn reoptimize(&mut self, block: BlockId) {
503        if !self.optimize {
504            return;
505        }
506        let started = std::time::Instant::now();
507        let cleanup = crate::optimize::forward_temp_stores(&mut self.ctx, block);
508        qcode_passes::remove_dead_insns(&mut self.ctx, block);
509        self.stats.optimize += started.elapsed();
510        self.stats.forwarded_loads += cleanup.forwarded_loads as u64;
511        self.stats.removed_stores += cleanup.removed_stores as u64;
512        // The interpreter may hold this block's instruction list, and some of
513        // those instructions are gone.
514        self.emu.invalidate_block_cache();
515    }
516
517    /// Folds the block just lifted at `addr` into its predecessor, when the two
518    /// are a straight-line pair.
519    ///
520    /// Lifting is per guest instruction, so a run of straight-line guest code
521    /// arrives as a chain of one-instruction blocks joined by unconditional
522    /// branches. Left that way, every guest instruction is its own unit of
523    /// execution: a separate compilation, a separate entry into compiled code
524    /// and a separate return to the interpreter for its terminator. Folding the
525    /// chain as it is discovered rebuilds the guest's *basic block*, which is
526    /// the unit worth compiling.
527    ///
528    /// Absorbing an address does not settle that it belongs here — code
529    /// discovered later may branch into the middle of the run, and
530    /// [`Context::split_block_at_address`] breaks it apart again when it does.
531    fn absorb_into_basic_block(&mut self, addr: u64) -> Option<BlockId> {
532        let filled = self.emu.block_at_address(&self.ctx, addr)?;
533
534        // Forward: the rest of this run may already be known. That is the shape
535        // a split leaves behind — it re-establishes a block's *start* while
536        // everything after it is still lifted — and the shape a back-edge into
537        // the middle of a run creates generally.
538        let forward = qcode_passes::absorb_straight_line(&mut self.ctx, filled);
539        self.stats.absorbed += forward as u64;
540        if forward > 0 {
541            self.reindex_absorbed(filled);
542            self.mark_dirty(filled);
543        }
544
545        // Backward: the straight-line predecessor that branched here, for the
546        // ordinary case of a run discovered one guest instruction at a time.
547        //
548        // Not if something branches to this address: it has to keep *starting*
549        // a block, or the split that established that would be undone here.
550        // Extending it forward, above, stays fine — that moves its end, not its
551        // start.
552        if self
553            .emu
554            .address_index()
555            .is_some_and(|index| index.is_boundary(addr))
556        {
557            return (forward > 0).then_some(filled);
558        }
559        // Exactly one predecessor, or absorbing would strand the others.
560        // Collected eagerly so the module is free to be mutated below.
561        let preds: Vec<BlockId> = BasicBlock::from_id(&self.ctx, filled)
562            .predecessors()
563            .map(|(_, block)| block)
564            .take(2)
565            .collect();
566        let [head] = preds[..] else {
567            return None;
568        };
569        if head == filled {
570            return None;
571        }
572        // Only into a block that starts at a machine address. Absorbing makes
573        // the head responsible for the absorbed addresses, and a later branch
574        // to one of them splits the head apart again — which works by emptying
575        // it and lifting it afresh. A block with no address of its own (the
576        // fallthrough arm of a branch *inside* one instruction's p-code) has
577        // nowhere to be lifted from, so emptying it leaves a hole nothing can
578        // fill.
579        if self.ctx.block(head).address.is_none() {
580            return (forward > 0).then_some(filled);
581        }
582        // Where `filled`'s instructions land: the head's own, less the
583        // terminator that absorption drops.
584        let offset = self
585            .ctx
586            .block(head)
587            .instruction_ids()
588            .len()
589            .saturating_sub(1);
590        if qcode_passes::absorb_straight_line(&mut self.ctx, head) == 0 {
591            return (forward > 0).then_some(filled);
592        }
593        self.stats.absorbed += 1;
594
595        // Where the machine has to resume, named by instruction rather than by
596        // index: cleaning the enlarged block deletes instructions ahead of that
597        // point, and every index after a deletion shifts. The first of these
598        // still standing afterwards is the one to resume at.
599        let resume: Vec<LocalInsnId> = self.ctx.block(head).instruction_ids()[offset..].to_vec();
600        self.mark_dirty(head);
601
602        self.reindex_absorbed(head);
603
604        // The machine stopped at the empty placeholder this lift filled, which
605        // absorption has just deleted; its instructions are in the head now.
606        if self.emu.block == filled {
607            let now = self.ctx.block(head).instruction_ids();
608            let resumed = resume
609                .iter()
610                .find_map(|wanted| now.iter().position(|have| have == wanted))
611                .unwrap_or(now.len().saturating_sub(1));
612            self.emu.block = head;
613            self.emu.idx = resumed;
614            self.emu.invalidate_block_cache();
615        }
616        Some(head)
617    }
618
619    /// Runs until the machine stops, or until `budget` p-code operations have
620    /// been retired.
621    pub fn run(&mut self, budget: u64) -> VmExit {
622        let deadline = self.stats.steps + budget;
623        while self.stats.steps < deadline {
624            // Checked before the step so a breakpoint reports the instruction
625            // about to run, not the one after it, and so resuming from a
626            // breakpoint is possible without immediately re-triggering it.
627            //
628            // Guarded on there being any breakpoint at all: `pc()` resolves the
629            // block through the module arena, and paying that on every step to
630            // consult an empty set cost about 6% of run time.
631            if !self.breakpoints.is_empty()
632                && let Some(pc) = self.pc()
633                && self.breakpoints.contains(&pc)
634                && self.stats.steps > 0
635            {
636                return VmExit::Breakpoint(pc);
637            }
638            if let Some(exit) = self.step() {
639                return exit;
640            }
641        }
642        VmExit::InstructionLimit
643    }
644}
645
646/// Resolves a guest address to a block against an existing index.
647fn resolve(ctx: &Context<'_>, index: &AddressIndex, addr: u64) -> Option<BlockId> {
648    match index.get(addr) {
649        Some(AddressTarget::Block(block)) => Some(block),
650        Some(AddressTarget::Function(function)) => {
651            qcode::value::FunctionBody::from_id(ctx, function)
652                .root()
653                .map(|root| root.id)
654        }
655        None => None,
656    }
657}
658
659#[cfg(test)]
660mod tests {
661    use super::*;
662    use crate::mmu::{PAGE_SIZE, perm};
663    use qcode::value::FunctionBody;
664
665    /// A source that hands over one pre-planned block per address, so the
666    /// discovery path can be exercised without a decoder.
667    #[derive(Default)]
668    struct Planned {
669        /// Addresses this source is willing to supply, and how many times it was
670        /// actually asked.
671        available: Vec<u64>,
672        pub calls: Vec<u64>,
673    }
674
675    impl CodeSource for Planned {
676        fn lift(
677            &mut self,
678            ctx: &mut Context<'static>,
679            memory: &VmMemory,
680            index: &mut AddressIndex,
681            addr: u64,
682            _stats: &mut Stats,
683        ) -> Result<(), CodeError> {
684            self.calls.push(addr);
685            // A real source fetches through the MMU, so executing unmapped or
686            // non-executable memory faults at the fetch. Mirrored here.
687            let mut byte = [0u8; 1];
688            memory
689                .mmu
690                .read_code(addr, &mut byte)
691                .map_err(CodeError::Fault)?;
692            if !self.available.contains(&addr) {
693                return Err(CodeError::Decode("no plan for this address".into()));
694            }
695            let function = FunctionBody::make_at_addr(ctx, addr, None).id;
696            let block = BasicBlock::make(ctx, function).with_address(addr).id;
697            index
698                .register(ctx, addr, AddressTarget::Block(block))
699                .map_err(|error| CodeError::Decode(format!("{error:?}").into()))?;
700            Ok(())
701        }
702    }
703
704    /// A module with a single empty block at `addr`.
705    fn module(addr: u64) -> (Context<'static>, BlockId) {
706        let mut ctx = Context::new();
707        let function = FunctionBody::make_at_addr(&mut ctx, addr, None).id;
708        let block = BasicBlock::make(&mut ctx, function).with_address(addr).id;
709        (ctx, block)
710    }
711
712    fn executable_memory() -> VmMemory {
713        let mut memory = VmMemory::new();
714        memory.mmu.map(0x1000, PAGE_SIZE, perm::RX_INIT).unwrap();
715        memory
716    }
717
718    #[test]
719    fn an_empty_addressed_block_asks_the_source_for_code() {
720        let (ctx, block) = module(0x1000);
721        let mut vm = Vm::new(ctx, block, Planned::default());
722        // An empty block carrying an address is how the lifter represents a
723        // branch target it has not reached yet, so it is a discovery request.
724        // This source cannot supply it, which is what makes the exit reportable.
725        let exit = vm.run(16);
726        assert!(
727            matches!(exit, VmExit::Unlifted { addr: 0x1000, .. }),
728            "expected a discovery attempt, got {exit:?}"
729        );
730        assert_eq!(vm.source.calls, vec![0x1000]);
731    }
732
733    #[test]
734    fn an_empty_block_with_no_address_is_an_error() {
735        // Nothing to discover: without an address there is no code to fetch.
736        let mut ctx = Context::new();
737        let function = FunctionBody::make_at_addr(&mut ctx, 0x1000, None).id;
738        let block = BasicBlock::make(&mut ctx, function).id;
739        let mut vm = Vm::new(ctx, block, Planned::default());
740        assert!(matches!(vm.run(16), VmExit::Error(_)));
741    }
742
743    #[test]
744    fn pc_reports_the_block_about_to_run() {
745        let (ctx, block) = module(0x1000);
746        let vm = Vm::new(ctx, block, Planned::default());
747        assert_eq!(vm.pc(), Some(0x1000));
748    }
749
750    #[test]
751    fn at_address_lifts_an_entry_the_module_lacks() {
752        let ctx = Context::new();
753        let source = Planned {
754            available: vec![0x1000],
755            calls: Vec::new(),
756        };
757        let vm = Vm::at_address(ctx, 0x1000, source, executable_memory())
758            .expect("the source can supply this address");
759        assert_eq!(vm.pc(), Some(0x1000));
760    }
761
762    #[test]
763    fn at_address_reuses_a_block_the_module_already_has() {
764        let (ctx, _) = module(0x1000);
765        let vm = Vm::at_address(ctx, 0x1000, Planned::default(), executable_memory())
766            .expect("no lifting is needed");
767        assert_eq!(vm.pc(), Some(0x1000));
768        // The source was never consulted.
769        assert!(vm.source.calls.is_empty());
770    }
771
772    #[test]
773    fn fetching_from_non_executable_memory_reports_the_fault() {
774        let ctx = Context::new();
775        let mut memory = VmMemory::new();
776        memory.mmu.map(0x1000, PAGE_SIZE, perm::RW_INIT).unwrap();
777        let source = Planned {
778            available: vec![0x1000],
779            calls: Vec::new(),
780        };
781        let error = Vm::at_address(ctx, 0x1000, source, memory)
782            .err()
783            .expect("the page is not executable");
784        assert!(matches!(
785            error,
786            CodeError::Fault(MemFault {
787                kind: crate::mmu::FaultKind::ExecViolation,
788                addr: 0x1000
789            })
790        ));
791    }
792
793    #[test]
794    fn an_unsuppliable_address_reports_where_it_stopped() {
795        let ctx = Context::new();
796        let error = Vm::at_address(ctx, 0x2000, Planned::default(), executable_memory())
797            .err()
798            .expect("nothing is mapped or planned at 0x2000");
799        assert!(matches!(error, CodeError::Fault(_)));
800    }
801
802    #[test]
803    fn breakpoints_are_recorded_and_removable() {
804        let (ctx, block) = module(0x1000);
805        let mut vm = Vm::new(ctx, block, Planned::default());
806        assert!(vm.add_breakpoint(0x2000));
807        assert!(!vm.add_breakpoint(0x2000));
808        assert!(vm.remove_breakpoint(0x2000));
809        assert!(!vm.remove_breakpoint(0x2000));
810    }
811
812    #[test]
813    fn memory_is_reachable_and_backed_by_the_mmu() {
814        let (ctx, block) = module(0x1000);
815        let mut vm = Vm::new(ctx, block, Planned::default());
816        vm.memory_mut()
817            .mmu
818            .map(0x4000, PAGE_SIZE, perm::RW_INIT)
819            .unwrap();
820        vm.memory_mut().mmu.write(0x4000, &[1, 2, 3]).unwrap();
821        let mut out = [0; 3];
822        vm.memory().mmu.read(0x4000, &mut out).unwrap();
823        assert_eq!(out, [1, 2, 3]);
824    }
825}