rigless 0.3.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
//! Which of an SoC's registers a run reached, taken from the accesses a session already reports.
//!
//! A run hands its driver every access, address and all. Resolving that address to an instance
//! and a register is the whole of this. Nothing new is measured and nothing is added to the path a
//! run takes, so a report costs what the run already cost.
//!
//! What it is for: a model whose registers a firmware never touches is unproved however many unit tests
//! stand behind it, and this is what names those registers instead of leaving somebody to notice.

use std::collections::BTreeSet;
use std::fmt;

use crate::emulator::{Access, AccessKind};
use crate::mcu::Mcu;
use crate::soc::Peripheral;

/// One instance's registers and which of them a run has reached.
#[derive(Debug)]
struct Instance {
    peripheral: Peripheral,
    modeled: bool,
    reached: Vec<bool>,
    /// How many accesses a window took, since it has no registers to mark.
    window: Option<u64>,
}

/// What a run reached, instance by instance.
#[derive(Debug)]
pub struct Coverage {
    instances: Vec<Instance>,
    reads: u64,
    writes: u64,
    outside: BTreeSet<u64>,
    /// The last address an access reached inside an instance, and that instance. A firmware
    /// polling one register reaches nothing new after the first read, so a repeat only counts.
    last: Option<(u64, usize)>,
}

impl Coverage {
    /// An empty report over the instances an MCU carries.
    ///
    /// Whether a slot is modeled is asked of the MCU's own dispatch rather than of a list of names,
    /// so an instance counts as modeled exactly when a field answers for it.
    #[must_use]
    pub fn of(mcu: &mut dyn Mcu) -> Self {
        let modeled: Vec<bool> = (0..mcu.bus().slots().len())
            .map(|slot| mcu.split(slot).1.is_some())
            .collect();
        let mut instances: Vec<Instance> = mcu
            .bus()
            .slots()
            .iter()
            .enumerate()
            .map(|(slot, carried)| Instance {
                peripheral: *carried.peripheral(),
                modeled: modeled[slot],
                reached: vec![false; carried.peripheral().registers.len()],
                window: carried.is_window().then_some(0),
            })
            .collect();
        // A window a board added comes after the part's own slots, wherever its address is.
        instances.sort_by_key(|instance| instance.peripheral.base);
        Self {
            instances,
            reads: 0,
            writes: 0,
            outside: BTreeSet::new(),
            last: None,
        }
    }

    /// Records one access. An address inside no instance is remembered once, and counts toward the
    /// totals but toward no instance.
    pub fn saw(&mut self, access: &Access) {
        match access.kind {
            AccessKind::Read => self.reads += 1,
            AccessKind::Write => self.writes += 1,
        }
        let repeated = self.last.filter(|(address, _)| *address == access.address);
        let index = if let Some((_, index)) = repeated {
            index
        } else {
            // The instances are the bus's slots, by ascending base and apart.
            let at = self
                .instances
                .partition_point(|instance| instance.peripheral.base <= access.address);
            let Some(index) = at
                .checked_sub(1)
                .filter(|&index| self.instances[index].peripheral.contains(access.address))
            else {
                self.outside.insert(access.address);
                return;
            };
            self.last = Some((access.address, index));
            index
        };
        let instance = &mut self.instances[index];
        if let Some(accesses) = &mut instance.window {
            *accesses += 1;
            return;
        }
        if repeated.is_some() {
            return;
        }
        let offset = access.address - instance.peripheral.base;
        // Two registers may share an offset, one register under two names. Both are reached at
        // once, and a narrow access lands inside the register whose lane it is. The rows are in
        // the order their layout indexes them, which needn't be by offset.
        for (row, register) in instance.peripheral.registers.iter().enumerate() {
            if (u64::from(register.offset)..u64::from(register.end())).contains(&offset) {
                instance.reached[row] = true;
            }
        }
    }

    /// Every register of a modeled instance the run never reached, as instance and register.
    pub fn untouched(&self) -> impl Iterator<Item = (&'static str, &'static str)> {
        self.instances
            .iter()
            .filter(|instance| instance.modeled)
            .flat_map(|instance| {
                instance
                    .peripheral
                    .registers
                    .iter()
                    .zip(&instance.reached)
                    .filter(|(_, reached)| !**reached)
                    .map(|(register, _)| (instance.peripheral.name, register.name))
            })
    }
}

impl fmt::Display for Coverage {
    /// The report: every modeled instance with how much of it the run reached, then what it missed,
    /// then what it reached that nothing answers for.
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        writeln!(
            formatter,
            "{} reads and {} writes over {} of {} instances",
            self.reads,
            self.writes,
            self.instances.iter().filter(|i| i.modeled).count(),
            self.instances.len()
        )?;
        for instance in self.instances.iter().filter(|instance| instance.modeled) {
            if let Some(accesses) = instance.window {
                writeln!(
                    formatter,
                    "  {:10} window, {accesses} accesses",
                    instance.peripheral.name
                )?;
                continue;
            }
            let reached = instance.reached.iter().filter(|&&hit| hit).count();
            writeln!(
                formatter,
                "  {:10} {reached:3} of {:3} registers",
                instance.peripheral.name,
                instance.peripheral.registers.len()
            )?;
        }
        for (instance, register) in self.untouched() {
            writeln!(formatter, "untouched {instance}.{register}")?;
        }
        // The inverse of untouched, and the more useful half: a register the run reached that
        // nothing answers for. The run's own report says which and with what, and this says the
        // bench found any.
        for instance in self
            .instances
            .iter()
            .filter(|instance| !instance.modeled && instance.reached.iter().any(|&hit| hit))
        {
            let reached = instance.reached.iter().filter(|&&hit| hit).count();
            writeln!(
                formatter,
                "unimplemented {} {reached} of {} registers reached, with no model behind them",
                instance.peripheral.name,
                instance.peripheral.registers.len()
            )?;
        }
        for address in &self.outside {
            writeln!(formatter, "outside every instance {address:#010x}")?;
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::soc::{Access as Rule, ReadAction, RegisterSpec, Width, WriteModify};
    use crate::time::VirtualNanos;

    const fn register(name: &'static str, offset: u32) -> RegisterSpec {
        RegisterSpec {
            name,
            offset,
            width: Width::W32,
            reset: 0,
            access: Rule::ReadWrite,
            read: ReadAction::None,
            write: WriteModify::None,
            fields: &[],
        }
    }

    /// Two registers at one offset, one register under two names, and a third past them.
    static REGISTERS: [RegisterSpec; 3] = [
        register("FIRST", 0x0),
        register("ALSO_FIRST", 0x0),
        register("SECOND", 0x4),
    ];

    static INSTANCE: Peripheral = Peripheral {
        name: "ONE",
        base: 0x4000_0000,
        size: 0x400,
        interrupts: &[],
        registers: &REGISTERS,
    };

    fn read_at(address: u64) -> Access {
        Access {
            kind: AccessKind::Read,
            address,
            width: Width::W8,
            value: 0,
            instruction_count: 0,
            virtual_time: VirtualNanos(0),
            program_counter: Some(0),
        }
    }

    fn coverage() -> Coverage {
        Coverage {
            instances: vec![Instance {
                peripheral: INSTANCE,
                modeled: true,
                reached: vec![false; REGISTERS.len()],
                window: None,
            }],
            reads: 0,
            writes: 0,
            outside: BTreeSet::new(),
            last: None,
        }
    }

    fn untouched(coverage: &Coverage) -> Vec<&'static str> {
        coverage.untouched().map(|(_, register)| register).collect()
    }

    #[test]
    fn a_register_nothing_addressed_is_untouched() {
        assert_eq!(
            untouched(&coverage()),
            ["FIRST", "ALSO_FIRST", "SECOND"],
            "the gate is worth nothing if it cannot report a miss"
        );
    }

    #[test]
    fn a_narrow_access_reaches_the_register_whose_lane_it_is() {
        let mut coverage = coverage();
        coverage.saw(&read_at(INSTANCE.base + 0x7));
        assert_eq!(untouched(&coverage), ["FIRST", "ALSO_FIRST"]);
    }

    #[test]
    fn two_registers_at_one_offset_are_reached_together() {
        let mut coverage = coverage();
        coverage.saw(&read_at(INSTANCE.base));
        assert_eq!(untouched(&coverage), ["SECOND"]);
    }

    #[test]
    fn a_register_is_reached_whatever_row_it_sits_in() {
        static BACKWARDS: [RegisterSpec; 2] = [register("SECOND", 0x4), register("FIRST", 0x0)];
        let mut coverage = coverage();
        coverage.instances[0].peripheral.registers = &BACKWARDS;
        coverage.instances[0].reached = vec![false; BACKWARDS.len()];
        coverage.saw(&read_at(INSTANCE.base));
        assert_eq!(untouched(&coverage), ["SECOND"]);
    }

    #[test]
    fn a_repeated_access_counts_and_reaches_what_the_first_did() {
        let mut coverage = coverage();
        coverage.saw(&read_at(INSTANCE.base));
        coverage.saw(&read_at(INSTANCE.base));
        assert_eq!(coverage.reads, 2);
        assert_eq!(untouched(&coverage), ["SECOND"]);
        coverage.saw(&read_at(INSTANCE.base + 0x4));
        assert!(untouched(&coverage).is_empty());
    }

    #[test]
    fn a_window_counts_every_access_repeated_or_not() {
        let mut coverage = coverage();
        coverage.instances[0].window = Some(0);
        coverage.saw(&read_at(INSTANCE.base));
        coverage.saw(&read_at(INSTANCE.base));
        coverage.saw(&read_at(INSTANCE.base + 0x4));
        assert_eq!(coverage.instances[0].window, Some(3));
    }

    #[test]
    fn an_address_inside_no_instance_reaches_nothing() {
        let mut coverage = coverage();
        coverage.saw(&read_at(0x9000_0100));
        assert_eq!(
            untouched(&coverage),
            ["FIRST", "ALSO_FIRST", "SECOND"],
            "an address the bus never decoded cannot have reached a register"
        );
        assert!(coverage.to_string().contains("outside every instance"));
    }

    #[test]
    fn an_instance_with_no_model_is_not_gated_on() {
        let mut coverage = coverage();
        coverage.instances[0].modeled = false;
        assert!(coverage.untouched().next().is_none());
    }
}