rigless 0.1.0

Virtual hardware-in-the-loop runtime: run unmodified firmware on an emulated MCU with every peripheral, chip model and unit of virtual time in Rust, and a CPU core of its own that translates the firmware's instructions to host code
Documentation
// Copyright (c) 2026 Rigless Labs.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! The seam between the runner and a processor family's core: the [`Processor`] a family
//! implements, the [`CoreBus`] it's lent while it runs, and what passes between them.
#![expect(
    unsafe_code,
    reason = "`Processor::map` hands the processor host memory by raw pointer, which only its caller can keep in place"
)]

use super::{Access, Error, HostCall};
use crate::soc::{Endian, Width};
use crate::time::VirtualNanos;

/// What a processor reaches while it runs a stretch: the run's memory and models, its clock, and
/// what the run asked of it since.
///
/// [`run`](super::run) lends one to [`Processor::run`]. To run a core on your own, lend it one of
/// your own.
pub trait CoreBus {
    /// Answers an access the processor's own space and its plain memory don't, or `None` when the
    /// bus fails it. `from_code` is whether an instruction made it, rather than the processor on its
    /// own, entering an exception say.
    fn access(&mut self, access: Access, from_code: bool) -> Option<u64>;

    /// The virtual time with `instructions` retired.
    fn time(&self, instructions: u64) -> VirtualNanos;

    /// What the run asked of the processor since this last answered, oldest first.
    fn queued(&mut self) -> Vec<Queued>;

    /// After an access, with `instructions` retired counting its own, and the processor's own next
    /// event at `event`: how many more instructions the processor may run before the run needs it,
    /// which is what the run would give it next, or `None` when the run needs it back now.
    fn carry_on(&mut self, instructions: u64, event: Option<VirtualNanos>) -> Option<u32> {
        let _ = (instructions, event);
        None
    }

    /// Whether a debugger's watchpoint stops the core before `access` happens. The processor then
    /// abandons the instruction, as it would on a precise data abort, and returns [`Exit::Halted`]
    /// with it unretired.
    fn stops_before(&mut self, access: &Access) -> bool {
        let _ = access;
        false
    }

    /// Bytes the processor's own trace sends out, in its trace protocol's packets, which go to
    /// the run's trace port when it has one.
    fn send_trace(&mut self, bytes: &[u8]) {
        let _ = bytes;
    }
}

/// A processor family's core, as the runner drives it: a stretch of instructions at a time, and
/// between stretches whatever the runner, the models or a debugger ask of it.
///
/// A family's module implements it and puts a function that makes one in the
/// [`CoreSpec::processor`](super::CoreSpec::processor) its machine module renders.
/// That's how the runner finds the family, so a family can live in a crate of its own.
///
/// # What the runner relies on
///
/// - **Counting.** [`Processor::run`] answers exactly how many instructions it retired, and
///   [`Processor::retired`] grows by the same number, and by one more for each host call
///   [`Processor::answer_host_call`] answers, which retires the instruction that made it. Virtual
///   time is that count at the target's rate, so an instruction you don't count takes no time.
/// - **Accesses.** A data access that neither the processor's own space nor the plain memory it
///   mapped answers goes to [`CoreBus::access`]. An instruction's access counts the instructions
///   retired with that instruction among them, and its time is [`CoreBus::time`] of that count. One the
///   processor makes on its own counts only the instructions before it, and isn't `from_code`.
/// - **Routed pages.** From [`Queued::Route`] until [`Queued::Unroute`], every data access to that
///   page goes to the bus, mapped or not. A store to memory mapped read-only goes to the bus too,
///   which drops it.
/// - **Returning.** Once an instruction's access went to the bus, return [`Exit::Returned`] when
///   that instruction is done, unless [`CoreBus::carry_on`], asked with the access's count, answers a
///   budget to go on with in place of what's left. Return the same way after [`Queued::Return`],
///   and once the firmware asks for a reset, so nothing runs before the runner makes it.
/// - **Draining.** Take [`CoreBus::queued`] and do each with [`Processor::ask`], oldest first, before
///   the first instruction of a stretch and again after every [`CoreBus::access`]. That's how an
///   interrupt a model raised during an access is taken before the next instruction. Whatever you
///   leave queued, the runner asks for once the stretch ends.
/// - **Waiting.** A processor that waits for an interrupt or an event returns [`Exit::Waiting`],
///   and [`Processor::waiting`] answers so until something wakes it. A stretch that runs nothing
///   then has the runner warp the clock to the next instant anything happens.
/// - **Events.** Once [`Processor::next_event`] has come, the runner calls
///   [`Processor::on_event`], which moves it on.
/// - **Watches.** Before an instruction's access goes to the bus, ask [`CoreBus::stops_before`]. When
///   it answers true, the access doesn't happen: leave the instruction as if it never ran, its
///   earlier stores aside, and return [`Exit::Halted`]. It runs again from its start the next time.
///   A processor that never asks still stops for a debugger's watchpoint, after the instruction
///   that made the access, as it returns once that's done.
/// - **Host calls.** Return [`Exit::HostCall`] at an instruction that calls the host, unretired,
///   and implement [`Processor::host_call`] and [`Processor::answer_host_call`] with it. The runner
///   answers every such stop before the next stretch: with an answer, which retires the
///   instruction, or with `None` when nothing serves host calls, which has the instruction do what
///   it does then. The defaults decode nothing and do nothing, which is right only for a family
///   that never returns it. Left at the instruction, the core would stop there again for ever.
pub trait Processor: Send + std::fmt::Debug {
    /// Runs at most `budget` instructions on `bus`, plus whatever [`CoreBus::carry_on`] gives it on top,
    /// and answers why it stopped and how many it retired.
    fn run(&mut self, budget: u32, bus: &mut dyn CoreBus) -> (Exit, u64);

    /// Every instruction retired in the run, across resets.
    fn retired(&self) -> u64;

    /// Does what the run asks of the processor.
    fn ask(&mut self, queued: Queued);

    /// Whether this processor can hold memory from `start` for `length` bytes: inside the
    /// addresses it has, and clear of the ones it answers itself. The run refuses a machine with
    /// memory anywhere it can't, before the first instruction. One that can hold it anywhere
    /// leaves this as it is.
    fn maps(&self, start: u64, length: u64) -> bool {
        let _ = (start, length);
        true
    }

    /// The bytes in one of this processor's pages, a power of two: the least it maps, and the
    /// stretch of memory [`Queued::Route`] sends to the bus. The run refuses a memory map whose
    /// regions and aliases don't start and end on one, or a page past 4 KiB, the alignment the run
    /// allocates memory at. A processor that maps nothing leaves this at one byte. The run counts
    /// how much a watch may route in bytes, so a page of one byte costs it nothing.
    fn page_size(&self) -> u64 {
        1
    }

    /// Maps `length` bytes of plain memory at `start` to the host's bytes at `host`. The runner
    /// maps every region and alias of the target this way, each a whole number of
    /// [`Processor::page_size`] pages, before it boots the processor. A processor that maps
    /// nothing leaves this as it is, and every access then goes to the [`CoreBus`], which answers
    /// memory too.
    ///
    /// # Safety
    ///
    /// `start` and `length` are whole pages of [`Processor::page_size`], and `host` is aligned to
    /// one. From this call until the
    /// processor is dropped, its `Drop` included, `host` is valid for reads of `length` bytes, and
    /// for writes when `writable`, and doesn't move. No `&` or `&mut` to those bytes is live while
    /// a method of the processor runs, and every access to them from another thread happens before
    /// the processor's next call. The processor isn't their only writer: the caller and the [`CoreBus`]
    /// change them between calls and during any call to the bus, so the processor never holds a
    /// reference to them across either.
    unsafe fn map(&mut self, start: u64, length: u64, host: *mut u8, writable: bool) {
        let _ = (start, length, host, writable);
    }

    /// Starts the core where its boot table at `table` says, reading it with `read`.
    fn boot(&mut self, table: u64, read: &dyn Fn(u64, Width) -> Option<u64>);

    /// Puts the processor back as a reset leaves it, with what it maps kept and everything it
    /// translated dropped.
    fn reset(&mut self);

    /// Whether the firmware asked for a reset since this last answered.
    fn take_reset_request(&mut self) -> bool;

    /// The next instant the processor's own timers need the run stopped at.
    fn next_event(&self) -> Option<VirtualNanos>;

    /// The run reached `now`, which a timer of the processor's own was waiting for.
    fn on_event(&mut self, now: VirtualNanos);

    /// Whether the processor waits for something to wake it.
    fn waiting(&self) -> bool;

    /// The address of the next instruction.
    fn pc(&self) -> u64;

    /// Asks for a breakpoint at `address`, which stays while anything asks for it, and answers
    /// whether the processor can stop there. One it can't keeps nothing, and the run refuses the
    /// debugger's breakpoint or the watch that asked. The processor returns [`Exit::Breakpoint`]
    /// before it runs the instruction there. One that never stops at an instruction leaves this
    /// and the two below as they are.
    fn add_breakpoint(&mut self, address: u64) -> bool {
        let _ = address;
        false
    }

    /// Takes back one ask for a breakpoint at `address`.
    fn remove_breakpoint(&mut self, address: u64) {
        let _ = address;
    }

    /// Runs the instruction at `address` the next time the processor starts there, past its
    /// breakpoint, whose arrival has been reported.
    fn pass_breakpoint(&mut self, address: u64) {
        let _ = address;
    }

    /// A debugger is attached, or it went: while one is, the core's debug events halt it as the
    /// architecture's halting debug does, and the firmware can see one is there.
    fn attach_debugger(&mut self, attached: bool);

    /// Holds interrupts off for a debugger's step, or lets them go.
    fn hold_interrupts(&mut self, held: bool);

    /// The register GDB numbers `number`, as its value and its width in bytes, if there's one.
    fn gdb_register(&self, number: usize) -> Option<(u64, usize)>;

    /// Writes the register GDB numbers `number`, and answers whether there's one.
    fn set_gdb_register(&mut self, number: usize, value: u64) -> bool;

    /// A debugger's access of `width` at `address` at `now`, `write` for a write, when the
    /// processor's own space answers it, or `None` for one the run's memory and models do. A read
    /// changes nothing.
    fn debugger_access(
        &mut self,
        write: Option<u64>,
        address: u64,
        width: Width,
        now: VirtualNanos,
    ) -> Option<u64>;

    /// What the processor's own trace sent for a debugger's access between stretches, which the
    /// runner sends to its trace port at once. A stretch's own goes to [`CoreBus::send_trace`] as it
    /// ends.
    fn take_debugger_trace(&mut self) -> Vec<u8> {
        Vec::new()
    }

    /// The target description GDB reads, which names the registers [`Processor::gdb_register`]
    /// numbers.
    fn target_xml(&self) -> &'static str;

    /// The order of the bytes at `address` as a debugger reads them: the part's, or the one the
    /// architecture fixes for that range whatever the part's.
    fn byte_order(&self, address: u64) -> Endian;

    /// The processor's state, for a snapshot: the same bytes however the run was split into
    /// stretches on the way there. It includes [`Processor::retired`].
    fn save(&self) -> Vec<u8>;

    /// Puts back what [`Processor::save`] wrote, refusing a state from another machine with
    /// [`Error::State`]. Afterwards [`Processor::retired`] answers what it did when the state was
    /// saved, since a resume checks it against the stop it resumes from. What the processor
    /// translated stays: the runner loads into a processor that may have run, a run a debugger
    /// takes back or one resumed from a snapshot, and asks with [`Queued::Invalidate`] for every
    /// byte of memory it puts back with it.
    fn load(&mut self, bytes: &[u8]) -> Result<(), Error>;

    /// Answers a monitor command of the family's own, the whole of what follows `monitor`, or
    /// `None` for one it doesn't have. `help` answers the lines `monitor help` adds for the family.
    /// A debugger sends one only while it has the core.
    fn monitor_command(&mut self, command: &str) -> Option<String> {
        let _ = command;
        None
    }

    /// The debugger stopped the core between instructions, for `why`, rather than a debug event of
    /// the core's own stopping it with [`Exit::Halted`]. Answers whether the halt changed what the
    /// firmware sees once the core runs again, a mark it leaves say, so a run that goes back over
    /// it makes the halt there again.
    fn debugger_halted(&mut self, why: HaltReason) -> bool {
        let _ = why;
        false
    }

    /// The unit of `width` that a debugger's access at `address` stands for one bit of, and which
    /// bit of its value, when `address` is a bit alias the processor answers itself. A debugger's
    /// read answers that bit, and its write sets or clears it in the unit.
    fn bit_alias(&self, address: u64, width: Width) -> Option<BitAlias> {
        let _ = (address, width);
        None
    }

    /// The ranges of addresses the processor answers itself, which a debugger's memory map lists as
    /// RAM, since a debugger refuses an address its map doesn't list.
    fn own_ranges(&self) -> Vec<std::ops::Range<u64>> {
        Vec::new()
    }

    /// The host call the processor stopped at with [`Exit::HostCall`], decoded, reading the
    /// guest's memory with `read`, or `None` for one this family doesn't serve.
    fn host_call(&self, _read: &mut dyn FnMut(u64, Width) -> Option<u64>) -> Option<HostCall> {
        None
    }

    /// Finishes the host call the processor stopped at. `Some(answer)` puts `answer` where the
    /// call's answer goes and moves past the call, counting it retired. `None` does what the
    /// instruction does when nothing serves host calls, so a run that serves none is the run it
    /// would be if the processor never stopped for them. The default does nothing, for a family
    /// that never returns [`Exit::HostCall`].
    fn answer_host_call(&mut self, _answer: Option<u64>) {}
}

/// Something the run asked of a processor while it was executing, which the processor does before
/// its next instruction.
///
/// It's exhaustive on purpose. Every family has to do every request, so a new one breaks the build
/// of a family that doesn't, wherever that family lives.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Queued {
    /// Drive external interrupt line `0` to `1`.
    Line(u16, bool),
    /// Drop what was translated from the bytes in `0..1`.
    Invalidate(u64, u64),
    /// Send every data access to the page holding this address to the bus, a page of
    /// [`Processor::page_size`].
    Route(u64),
    /// Stop sending the page holding this address to the bus.
    Unroute(u64),
    /// Return to the runner before the next instruction.
    Return,
}

/// What a debugger's access at a bit alias stands for: one bit of a unit, which its read answers and
/// its write sets or clears.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BitAlias {
    /// Where the unit is, as wide as the access.
    pub unit: u64,
    /// Which bit of the unit's value.
    pub bit: u32,
}

/// Why a debugger stopped the core between instructions.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum HaltReason {
    /// It asked: a halt request, a step's end or a halt point.
    Requested,
    /// One of its breakpoints, which the processor stopped before with [`Exit::Breakpoint`].
    Breakpoint,
    /// One of its watchpoints, hit by an access of the instruction the core stopped after.
    Watchpoint,
}

/// Why a processor stopped running a stretch.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Exit {
    /// It ran every instruction it was given.
    Budget,
    /// It's about to run an instruction with a breakpoint on it.
    Breakpoint,
    /// It's waiting for an interrupt or an event.
    Waiting,
    /// It locked up, and runs nothing until it's reset.
    Lockup,
    /// A debug event halted it while a debugger is attached, before the instruction that raised
    /// it retired.
    Halted,
    /// It stopped at a call the firmware makes of the host it runs on, before the instruction that
    /// made it retired. [`Processor::host_call`] says what it is, and the runner answers it with
    /// [`Processor::answer_host_call`] before the processor runs again.
    HostCall,
    /// An access went to the bus, or the run asked it to return.
    Returned,
}