rigless 0.0.5

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; a fork of QEMU is built in and executes the instructions
Documentation
// Copyright (c) 2026 Rigless Labs.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! The emulator, and the seam between it and the models.
//!
//! The emulator executes instructions; the models answer the peripheral accesses those instructions
//! make. The emulator is the fork built into this crate and [`run`] starts it in this process: every access is a call from the emulated core's own thread into [`Session::access`],
//! and every instant the models asked to be told about is a call into [`Session::stop`]. Nothing crosses
//! a process boundary, and nothing waits on another thread.
//!
//! What the emulator lends the models while it calls them is a [`Host`]: guest memory, which a model
//! moving a block reads and writes, and the interrupt lines, which a model raises and lowers. What the
//! process lends the run is a [`Driver`]: it sees every access as it happens, and at every instant the
//! run was asked to reach it says where to reach next, or that the run is over. `rigless run`'s driver
//! prints the accesses and reads the outputs; `rigless run --link`'s answers the engine on the far side
//! of the link with the outputs and takes the next instant from it.
//!
//! A machine module says what its target's emulator is started as in an [`EmulatorCommand`]: the
//! emulator's machine, the core, the properties that machine declares, the instruction rate, and the
//! regions a second address also reaches. The runtime adds the firmware, the memory and the debugger,
//! and never reads or writes another property.
//!
//! [`Session::access`]: crate::session::Session::access
//! [`Session::stop`]: crate::session::Session::stop

use std::fmt;
use std::path::Path;

use crate::device::Level;
use crate::machine::Board;
use crate::session::Session;
use crate::soc::Width;
use crate::time::VirtualNanos;

#[cfg(not(emulator_absent))]
mod qemu;
#[cfg(not(emulator_absent))]
pub use qemu::run;

/// Runs a firmware on a session's target, and answers [`Error::NoEmulator`] because this build has
/// no emulator to run it on.
///
/// Only docs.rs and Miri reach this: neither can have an emulator, and both still have to produce a
/// crate. Removing `run` from those builds instead would leave the published API documentation
/// without the one function the rest of this module is about.
#[cfg(emulator_absent)]
pub fn run<D, R>(
    _session: Session<D>,
    _start: &Start<'_>,
    _driver: R,
) -> Result<Finished<D, R>, Error>
where
    D: Board + ?Sized + 'static,
    R: Driver<D> + 'static,
{
    Err(Error::NoEmulator)
}

/// ceiling: an interrupt number a machine routes is below this; upgrade when a core family has more lines.
pub const INTERRUPT_LINES: u16 = 1024;

/// An interrupt line the emulator drives, so an index below [`INTERRUPT_LINES`].
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct InterruptLine(u16);

impl InterruptLine {
    /// The line at `index`, or `None` when no core family has one there.
    #[must_use]
    pub const fn new(index: u16) -> Option<Self> {
        if index < INTERRUPT_LINES {
            Some(Self(index))
        } else {
            None
        }
    }

    /// The index this line is.
    #[must_use]
    pub const fn index(self) -> u16 {
        self.0
    }
}

/// Whether an access reads or writes.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum AccessKind {
    /// The core reads a peripheral.
    Read,
    /// The core writes a peripheral.
    Write,
}

/// One peripheral access as the emulated core made it.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Access {
    /// Read or write.
    pub kind: AccessKind,
    /// The address on the bus.
    pub address: u64,
    /// The width the core asked for.
    pub width: Width,
    /// The value written, or zero for a read.
    pub value: u64,
    /// The emulator's instruction count at the access.
    pub instruction_count: u64,
    /// The virtual clock at the access.
    pub virtual_time: VirtualNanos,
    /// The instruction making the access, or zero when none is: a debugger's access.
    pub program_counter: u64,
}

/// Why the emulator stopped.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum StopReason {
    /// The instant the models asked for came.
    Deadline,
    /// The core was halted when that instant came, so the clock warped to it.
    Idle,
    /// The firmware requested a system reset.
    Reset,
}

impl StopReason {
    /// The reason the emulator's own code names, or `None` for one it has no name for.
    #[must_use]
    pub const fn from_code(code: u32) -> Option<Self> {
        match code {
            0 => Some(Self::Deadline),
            1 => Some(Self::Idle),
            2 => Some(Self::Reset),
            _ => None,
        }
    }
}

/// Where and why the emulator stopped.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Stop {
    /// The virtual clock at the stop.
    pub at: VirtualNanos,
    /// The emulator's instruction count at the stop.
    pub instruction_count: u64,
    /// Why.
    pub reason: StopReason,
}

/// Guest memory, which a model reaches only through the emulator.
///
/// A model that must move a word to or from RAM, a DMA controller, asks for it here, at the instant of
/// the access or the event that caused it. The emulator serves it from its own address space; a test
/// serves it from a map.
pub trait Memory {
    /// The `width` at `address`.
    fn read(&mut self, address: u64, width: Width) -> Result<u64, Error>;

    /// Writes `value` as `width` at `address`.
    fn write(&mut self, address: u64, width: Width, value: u64) -> Result<(), Error>;
}

/// What the emulator lends the models while it calls them: its memory, and its interrupt lines.
pub trait Host: Memory {
    /// Drives `line` to `level` before the next instruction executes.
    fn set_interrupt(&mut self, line: InterruptLine, level: Level);
}

/// What one target's emulator is started as, as its machine module renders it.
///
/// The properties are the machine's own, in the order the machine module lists them; the runtime
/// never reads or writes another.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EmulatorCommand {
    /// The emulator's machine, which the machine module mirrors field for field.
    pub machine: &'static str,
    /// The core the machine runs.
    pub cpu: &'static str,
    /// The machine's properties, as it spells them.
    pub properties: Vec<Property>,
    /// Every instruction takes `2 ^ icount_shift` nanoseconds; the machine module owns the rate.
    pub icount_shift: u32,
    /// The regions a second address also reaches, which is the board's wiring and not the core's business.
    pub aliases: Vec<Alias>,
}

/// One property of the emulator's machine, with the value the machine module gives it.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Property {
    /// The name the machine declares.
    pub name: &'static str,
    /// The value, rendered as the machine's own parser takes it.
    pub value: String,
}

impl Property {
    /// The property `name` set to `value`.
    pub fn new(name: &'static str, value: impl fmt::Display) -> Self {
        Self {
            name,
            value: value.to_string(),
        }
    }
}

impl fmt::Display for Property {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "{}={}", self.name, self.value)
    }
}

/// A second address the whole of a memory region also answers at.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Alias {
    /// The region's name in the target's memory map.
    pub region: &'static str,
    /// Where the same bytes appear again.
    pub at: u64,
}

/// How one run starts: the firmware, and whether a debugger is served.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Start<'a> {
    /// The firmware image the emulator loads.
    pub firmware: &'a Path,
    /// A TCP port to serve the debugger on, if any.
    pub debugger_port: Option<u16>,
    /// Hold the core before its first instruction until the debugger continues it.
    pub wait_for_debugger: bool,
}

/// What the process does with a run while the emulator drives it.
///
/// Both methods run on the emulated core's thread, inside the emulator's call, before the next
/// instruction executes; nothing else touches the session while they do.
pub trait Driver<D: Board + ?Sized>: Send {
    /// Every access, with the value read or written, as it happens.
    fn on_access(&mut self, access: &Access, value: u64) {
        let _ = (access, value);
    }

    /// The run reached the instant it was asked to: the next instant to reach, or `None` to end the run.
    fn on_stop(&mut self, session: &mut Session<D>, stop: Stop) -> Option<VirtualNanos>;
}

/// What a run leaves behind: the session with its reports, the driver, and the stop that ended it.
#[derive(Debug)]
pub struct Finished<D: Board + ?Sized, R> {
    /// The session, its board and its reports as the run left them.
    pub session: Session<D>,
    /// The driver, with whatever it kept.
    pub driver: R,
    /// The stop the driver ended the run at, or `None` when something else ended it.
    pub stop: Option<Stop>,
}

/// What can go wrong between the emulator and the models.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
    /// The board has no target at the index asked for.
    #[error("{machine} has no target {target}; it has {count}")]
    NoTarget {
        /// The board.
        machine: &'static str,
        /// The index asked for.
        target: usize,
        /// How many the board has.
        count: usize,
    },
    /// Guest memory refused an access a model asked for.
    #[error("guest memory has nothing at {address:#x} for a {} bit access", width.bits())]
    Memory {
        /// The address.
        address: u64,
        /// The width.
        width: Width,
    },
    /// The emulator runs once per process, and it already has.
    #[error("the emulator has already run in this process; it runs once")]
    AlreadyRan,
    /// This build has no emulator.
    #[error("this build has no emulator; only docs.rs and Miri build the crate without one")]
    NoEmulator,
    /// An argument for the emulator holds a byte it cannot take.
    #[error("the emulator's argument {0:?} is not a C string")]
    Argument(String),
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn a_property_renders_as_the_machine_spells_it() {
        assert_eq!(Property::new("num-irq", 91u16).to_string(), "num-irq=91");
    }

    #[test]
    fn a_line_exists_below_the_ceiling_and_not_at_it() {
        assert_eq!(InterruptLine::new(0).map(InterruptLine::index), Some(0));
        assert!(InterruptLine::new(INTERRUPT_LINES - 1).is_some());
        assert!(InterruptLine::new(INTERRUPT_LINES).is_none());
    }

    #[test]
    fn every_reason_the_emulator_names_decodes_and_nothing_else_does() {
        assert_eq!(StopReason::from_code(0), Some(StopReason::Deadline));
        assert_eq!(StopReason::from_code(1), Some(StopReason::Idle));
        assert_eq!(StopReason::from_code(2), Some(StopReason::Reset));
        assert_eq!(StopReason::from_code(3), None);
    }
}