rigless 0.0.4

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; needs the rigless-sidecar package from rigless-labs/qemu
Documentation
// Copyright (c) 2026 Rigless Labs.
// SPDX-License-Identifier: Apache-2.0
//! What joins a [`Bus`] to the models that give its peripherals behaviour.
//!
//! A [`Bus`] knows where an address lands and what that register holds. It owns no models. An MCU
//! owns them, as fields of its own type — `mcu.rcc`, `mcu.spi1` — and implements [`Mcu`] to say
//! which field answers for which slot. That one `match` is the whole of the mapping, and it is why
//! nothing after construction reaches a model by name or downcasts to its type.
//!
//! Everything the runtime needs of an MCU is here as a provided method: [`Mcu::read`],
//! [`Mcu::write`] and [`Mcu::advance_to`] are written once against [`Mcu::split`], so an SoC module
//! writes its fields and its dispatch and nothing else.

use crate::device::{ErasedDevice, InterruptChange, RegisterFile};
use crate::soc::{Access, Bus, Cpu, Error, Fault, Region, SAME_INSTANT_LIMIT, Width};
use crate::time::VirtualNanos;

/// One whole microcontroller: a bus, and a model for each of its slots that has one.
///
/// The three required methods are the only thing an SoC module writes by hand. [`Mcu::split`] borrows
/// the bus and one model apart, which is what lets a model be called with its own register file, and
/// [`Mcu::check`] holds the dispatch to the slots it claims when the MCU is built.
pub trait Mcu {
    /// The bus, for reading the map and what the registers hold.
    fn bus(&self) -> &Bus;

    /// The bus, and the model that answers for `slot` if this MCU has one, borrowed apart.
    ///
    /// The implementation is one `match` from the slot to a field:
    ///
    /// ```ignore
    /// fn split(&mut self, slot: usize) -> (&mut Bus, Option<&mut dyn ErasedDevice>) {
    ///     let Self { bus, rcc, spi1, wiring, .. } = self;
    ///     let model: Option<&mut dyn ErasedDevice> = match wiring[slot] {
    ///         Field::Rcc => Some(rcc),
    ///         Field::Spi1 => Some(spi1),
    ///         Field::None => None,
    ///     };
    ///     (bus, model)
    /// }
    /// ```
    fn split(&mut self, slot: usize) -> (&mut Bus, Option<&mut dyn ErasedDevice>);

    /// That every model is wired to a slot carrying the layout it was written for.
    ///
    /// The one mistake a hand-written dispatch can make is pointing a slot at the wrong field, and it
    /// is a mistake the type system cannot catch, because every model erases to the same trait object.
    /// This asks `split` itself what it hands over and compares that model's own layout to the slot's,
    /// so the answer comes from the dispatch under test rather than from a second table beside it.
    fn check(&mut self) -> Result<(), Error> {
        for index in 0..self.bus().slots().len() {
            let (bus, model) = self.split(index);
            let Some(model) = model else {
                continue;
            };
            let carried = bus.slots()[index].peripheral();
            if !std::ptr::eq(model.layout(), carried.registers) {
                return Err(Error::LayoutMismatch {
                    instance: carried.name,
                });
            }
        }
        Ok(())
    }

    /// The processor core this MCU carries.
    fn cpu(&self) -> Cpu {
        self.bus().cpu()
    }

    /// The memory regions, which is what a target's engine is given.
    fn memory(&self) -> &'static [Region] {
        self.bus().memory()
    }

    /// The instant of the earliest pending event, which is when the engine next has to stop.
    fn next_event(&self) -> Option<VirtualNanos> {
        self.bus().next_event()
    }

    /// A read from the firmware of `width` bits at `address`, which may be part of a wider register.
    ///
    /// The model answering for that slot sees it first; what it does not answer, the register file does.
    fn read(&mut self, at: VirtualNanos, address: u64, width: Width) -> Result<u64, Fault> {
        let located = self.bus().locate(address, width, Access::is_readable)?;
        let (bus, model) = self.split(located.slot);
        let (slot, mut context) = bus.call(at, located.slot);
        let answered =
            model.and_then(|model| model.read(located.row, slot.registers_mut(), &mut context));
        let value = answered.unwrap_or_else(|| slot.registers_mut().bus_read(located.row));
        Ok((value >> located.lane.shift) & width.mask())
    }

    /// A write over the bus at `at`: the register file applies the rules, then the model sees what landed.
    fn write(
        &mut self,
        at: VirtualNanos,
        address: u64,
        width: Width,
        value: u64,
    ) -> Result<(), Fault> {
        let located = self.bus().locate(address, width, Access::is_writable)?;
        let (bus, model) = self.split(located.slot);
        let (slot, mut context) = bus.call(at, located.slot);
        let register = &slot.peripheral().registers[located.row];
        // A write-only register stores nothing, so the lanes a narrow write leaves alone carry no action.
        let neutral = if register.access.is_readable() {
            let registers = slot.registers_mut();
            registers
                .write_rule(located.row)
                .neutral(registers.get_row(located.row))
        } else {
            0
        };
        let untouched = neutral & !(width.mask() << located.lane.shift);
        let written = untouched | ((value & width.mask()) << located.lane.shift);
        slot.registers_mut().bus_write(located.row, written);
        if let Some(model) = model {
            model.write(located.row, written, slot.registers_mut(), &mut context);
        }
        Ok(())
    }

    /// Fires every event due by `now`, earliest first, each to the model that scheduled it; an event
    /// scheduled meanwhile at `now` fires in the same pass.
    fn advance_to(&mut self, now: VirtualNanos) {
        // Each event runs at its own instant, not at the one being advanced to: a model that schedules a
        // second event from the first would otherwise anchor it to however far the caller happened to step.
        //
        // A model that asks for an instant already gone gets this one instead, and would then be called
        // again in this same pass, forever. That is a model bug rather than a state a run should reach, so
        // it is caught here where it can be named rather than left to hang.
        let mut fired_here = 0_u32;
        let mut last = None;
        while let Some((event, owner, at)) = self.bus_mut().pop_due(now) {
            if last == Some(at) {
                fired_here += 1;
                assert!(
                    fired_here <= SAME_INSTANT_LIMIT,
                    "a model scheduled {fired_here} events at {at:?} without advancing the clock"
                );
            } else {
                last = Some(at);
                fired_here = 0;
            }
            let (bus, model) = self.split(owner);
            let (slot, mut context) = bus.call(at, owner);
            if let Some(model) = model {
                model.on_event(event, slot.registers_mut(), &mut context);
            }
        }
    }

    /// Takes the interrupt levels the models set since the last drain, in the order they set them.
    fn drain_interrupts(&mut self) -> Vec<InterruptChange> {
        self.bus_mut().drain_interrupts().collect()
    }

    /// The bus, mutably; `split` gives it alongside a model, this gives it alone.
    fn bus_mut(&mut self) -> &mut Bus;

    /// What one instance holds, for a test reading a register back.
    fn registers(&self, instance: &str) -> Option<&RegisterFile> {
        self.bus().registers(instance)
    }
}

/// One model on a bus of its own: what a test wants when it puts a model under the microscope.
///
/// It is an [`Mcu`] with exactly one field, so everything the runtime does to a whole part — serve a
/// read, fire an event, drain an interrupt — works on it unchanged, and the model is `single.model`,
/// a field, with `single.registers()` beside it.
#[derive(Debug)]
pub struct Single<D> {
    /// The model, reachable as a field like any other.
    pub model: D,
    bus: Bus,
    slot: usize,
}

impl<D: crate::device::Device + 'static> Single<D> {
    /// Puts `model` on `instance` of the bus `builder` describes.
    ///
    /// Fails when the bus has no such instance, or when that instance's layout is not the one the
    /// model was written for.
    pub fn new(
        builder: crate::soc::Builder,
        instance: &'static str,
        model: D,
    ) -> Result<Self, Error> {
        let bus = builder.build()?;
        let slot = bus
            .slot_of(instance)
            .ok_or(Error::UnknownInstance { instance })?;
        let mut single = Self { model, bus, slot };
        single.check()?;
        Ok(single)
    }

    /// What the instance holds, which is where a model keeps half its state.
    pub fn registers(&mut self) -> &mut RegisterFile {
        self.bus.slot_mut(self.slot).registers_mut()
    }

    /// The model and its registers together, which is what a boundary method takes.
    pub fn pair(&mut self) -> (&mut D, &mut RegisterFile) {
        let Self { model, bus, slot } = self;
        (model, bus.slot_mut(*slot).registers_mut())
    }

    /// Drives an input the bus does not carry at `at`: the model gets its registers and a context as
    /// on a bus access, so it may schedule and raise.
    pub fn drive<T>(
        &mut self,
        at: VirtualNanos,
        input: impl FnOnce(&mut D, &mut RegisterFile, &mut crate::device::DeviceContext<'_>) -> T,
    ) -> T {
        let Self { model, bus, slot } = self;
        let (carried, mut context) = bus.call(at, *slot);
        input(model, carried.registers_mut(), &mut context)
    }
}

impl<D: crate::device::Device + 'static> Mcu for Single<D> {
    fn bus(&self) -> &Bus {
        &self.bus
    }

    fn split(&mut self, slot: usize) -> (&mut Bus, Option<&mut dyn ErasedDevice>) {
        let Self {
            model,
            bus,
            slot: mine,
        } = self;
        let answering: Option<&mut dyn ErasedDevice> = (slot == *mine).then_some(model);
        (bus, answering)
    }

    fn bus_mut(&mut self) -> &mut Bus {
        &mut self.bus
    }
}

/// A bus with no models at all: every instance answers from its register file alone.
///
/// It is what "register-only" means, made a type. A part is this plus its models; a test that wants
/// to see what the tables alone do wants this.
#[derive(Debug)]
pub struct Bare(Bus);

impl Bare {
    /// The bus `builder` describes, with nothing on it.
    pub fn new(builder: crate::soc::Builder) -> Result<Self, Error> {
        Ok(Self(builder.build()?))
    }
}

impl Mcu for Bare {
    fn bus(&self) -> &Bus {
        &self.0
    }

    fn split(&mut self, _slot: usize) -> (&mut Bus, Option<&mut dyn ErasedDevice>) {
        (&mut self.0, None)
    }

    fn bus_mut(&mut self) -> &mut Bus {
        &mut self.0
    }
}