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
//! A processor with code and RAM mapped and a bus that records what reaches it, for the tests of
//! every encoding group.
//!
//! A fuzz build runs code on the bench too, and uses no more of it than its core.
#![cfg_attr(
    not(test),
    allow(
        dead_code,
        reason = "a fuzz build uses the bench's core and nothing else of it"
    )
)]

use super::super::family::Family;
use crate::emulator::Property;
use crate::emulator::memory::{Mapped, Memory};
use crate::emulator::{Access, AccessKind};
use crate::emulator::{Alias, CoreSpec};
use crate::emulator::{CoreBus, Exit, Processor, Queued};
use crate::machine::armv7m::{
    CLOCK_HZ, FPU, Fpu, INTERRUPT_LINES, MPU_REGIONS, MpuRegions, PPB_WINDOWS, PRIORITY_BITS,
    REFCLK_DIVIDER, Variant,
};
use crate::soc::{Endian, Region, Width};
use crate::time::VirtualNanos;

/// Where [`Bench`] maps its code.
pub(crate) const CODE: u32 = 0x0000_0000;
/// Where it maps its RAM.
pub(crate) const RAM: u32 = 0x2000_0000;
/// The bytes of each.
pub(crate) const SIZE: usize = 0x1_0000;
/// An address nothing maps, so every access there reaches the bus.
pub(crate) const DEVICE: u32 = 0x4000_0000;

/// The Cortex-M4F with an eight-region MPU.
pub(crate) fn spec() -> CoreSpec {
    spec_of(Variant::M4, Fpu::FpV4Sp, MpuRegions::Eight)
}

/// A target on `variant` with `fpu` and an MPU of `regions`.
pub(crate) fn spec_of(variant: Variant, fpu: Fpu, regions: MpuRegions) -> CoreSpec {
    CoreSpec {
        processor: crate::cpu::armv7m::processor,
        cpu: variant.name(),
        properties: vec![
            Property::new(CLOCK_HZ, 168_000_000u64),
            Property::new(INTERRUPT_LINES, 91u32),
            Property::new(PRIORITY_BITS, 4u8),
            Property::new(REFCLK_DIVIDER, 8u32),
            Property::new(PPB_WINDOWS, "0xe0001000:0x8"),
            Property::new(FPU, fpu),
            Property::new(MPU_REGIONS, regions.count()),
        ],
        instructions_per_second: std::num::NonZeroU64::new(250_000_000).expect("a running core"),
        aliases: Vec::new(),
        boot: crate::emulator::Boot {
            reset: 0,
            alignment: std::num::NonZeroU64::new(512).expect("not zero"),
        },
    }
}

/// A bus that answers every read with `answer` and records every access.
#[derive(Debug, Default)]
pub(crate) struct Recorder {
    pub(crate) accesses: Vec<Access>,
    pub(crate) answer: u64,
    /// Fails every access from this address up.
    pub(crate) fails_from: Option<u64>,
    pub(crate) queued: Vec<Queued>,
    /// What the next access to reach the bus asks of the processor, as a model that raises a line
    /// when it's accessed does.
    pub(crate) on_access: Vec<Queued>,
    /// Stops the core before an instruction's access of this kind at this address, as a debugger's
    /// watchpoint does.
    pub(crate) watched: Option<(u64, AccessKind)>,
    /// What the processor's own trace sent.
    pub(crate) traced: Vec<u8>,
}

impl CoreBus for Recorder {
    fn access(&mut self, access: Access, _from_code: bool) -> Option<u64> {
        self.accesses.push(access);
        self.queued.append(&mut self.on_access);
        if self.fails_from.is_some_and(|from| access.address >= from) {
            return None;
        }
        Some(match access.kind {
            AccessKind::Read => self.answer,
            AccessKind::Write => access.value,
        })
    }

    fn time(&self, instructions: u64) -> VirtualNanos {
        VirtualNanos(instructions * 4)
    }

    fn queued(&mut self) -> Vec<Queued> {
        std::mem::take(&mut self.queued)
    }

    fn send_trace(&mut self, bytes: &[u8]) {
        self.traced.extend_from_slice(bytes);
    }

    fn stops_before(&mut self, access: &Access) -> bool {
        self.watched == Some((access.address, access.kind))
    }
}

/// A processor, the Cortex-M4F unless a test says otherwise, with [`SIZE`] bytes of code at [`CODE`] and of RAM at [`RAM`], the stack at the top
/// of RAM, and Thumb code placed at the start of code.
pub(crate) struct Bench {
    pub(crate) core: Box<Family>,
    pub(crate) bus: Recorder,
    memory: Mapped,
}

impl Bench {
    /// A processor about to run `code`, halfwords in the order they're fetched.
    pub(crate) fn new(code: &[u16]) -> Self {
        Self::of(&spec(), code)
    }

    /// [`Bench::new`] on the core `spec` describes.
    pub(crate) fn of(spec: &CoreSpec, code: &[u16]) -> Self {
        Self::ordered(spec, code, Endian::Little, &[])
    }

    /// [`Bench::new`] on a part whose data is big-endian.
    pub(crate) fn big_endian(code: &[u16]) -> Self {
        Self::ordered(&spec(), code, Endian::Big, &[])
    }

    /// [`Bench::new`] with RAM at `at` too, as a machine's alias has it, so both addresses are the
    /// same bytes.
    pub(crate) fn aliased(code: &[u16], at: u32) -> Self {
        let alias = Alias {
            region: "ram",
            at: u64::from(at),
        };
        Self::ordered(&spec(), code, Endian::Little, &[alias])
    }

    fn ordered(spec: &CoreSpec, code: &[u16], endian: Endian, aliases: &[Alias]) -> Self {
        let mut core = Family::new(spec, endian).expect("a processor");
        let memory = Memory::new(
            &[
                Region::flash("code", u64::from(CODE), SIZE as u64),
                Region::ram("ram", u64::from(RAM), SIZE as u64),
            ],
            aliases,
            endian,
        );
        #[expect(
            unsafe_code,
            reason = "the core keeps pointers into the bench's memory"
        )]
        // SAFETY: the bench declares its core before its memory, so it drops it first.
        unsafe {
            memory.map_into(&mut *core);
        }
        let mut bench = Self {
            core,
            bus: Recorder::default(),
            memory: Mapped::new(memory),
        };
        bench.place(CODE, code);
        bench.core.state.r[13] = RAM + u32::try_from(SIZE).expect("a bench-sized region");
        bench.core.state.r[15] = CODE;
        bench
    }

    /// Starts the core over on `code`, reset, with code and RAM cleared first, which drops what was
    /// translated from them, and the code generator kept.
    #[cfg(fuzzing)]
    pub(crate) fn restart(&mut self, code: &[u16]) {
        use crate::emulator::Processor as _;
        self.core.reset();
        // That's a Local reset, which keeps `DEMCR.TRCENA`, and no input may see another's.
        self.core.scs.write(0xDFC, 0, VirtualNanos(0));
        let zeros = vec![0; SIZE];
        self.write(CODE, &zeros);
        self.write(RAM, &zeros);
        self.place(CODE, code);
        self.core.state.r[13] = RAM + u32::try_from(SIZE).expect("a bench-sized region");
        self.core.state.r[15] = CODE;
        self.bus = Recorder::default();
    }

    /// Writes halfwords at `address`, in code or RAM, as a loader would.
    pub(crate) fn place(&mut self, address: u32, halfwords: &[u16]) {
        let bytes: Vec<u8> = halfwords
            .iter()
            .flat_map(|halfword| halfword.to_le_bytes())
            .collect();
        self.write(address, &bytes);
    }

    /// Writes bytes at `address`, in code or RAM, as a loader would.
    pub(crate) fn write(&mut self, address: u32, bytes: &[u8]) {
        assert!(
            self.memory.place(u64::from(address), bytes),
            "inside a region"
        );
        self.core
            .invalidate(u64::from(address), u64::from(address) + bytes.len() as u64);
    }

    /// Reads the word at `address`, in code or RAM.
    pub(crate) fn word(&self, address: u32) -> u32 {
        let word = self
            .memory
            .read(u64::from(address), Width::W32)
            .expect("inside a region");
        u32::try_from(word).expect("a word")
    }

    /// Runs at most `instructions`, and answers why it stopped and how many it ran.
    pub(crate) fn run(&mut self, instructions: u32) -> (Exit, u64) {
        self.core.run(instructions, &mut self.bus)
    }
}