rigless 0.0.6

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 fork of QEMU built in to execute the instructions
Documentation
// Copyright (c) 2026 Rigless Labs.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! A run that prints what `rigless run` prints, for a board the command line doesn't carry.

use std::io::{self, BufWriter, Write};

use super::{Access, AccessKind, Driver, Error, Finished, Start, Stop, StopReason, run};
use crate::coverage::Coverage;
use crate::link::values_of;
use crate::machine::Board;
use crate::session::Session;
use crate::time::VirtualNanos;

/// The deadline of a run given none: 292 years of virtual time, which no run reaches.
///
/// The emulator's deadline is signed on its side, and a value past this one is read as no deadline
/// at all, so nothing longer can be asked for.
pub const NO_DEADLINE: VirtualNanos = VirtualNanos(i64::MAX as u64);

/// What a printed run hands back: the session with its board and its reports, what the firmware
/// reached, and the output it printed to.
#[derive(Debug)]
pub struct Printed<D: Board + ?Sized, W> {
    /// The session, its board and its reports as the run left them.
    pub session: Session<D>,
    /// Every instance the MCU carries and which of its registers the firmware reached, and any
    /// address outside all of them. Its `Display` is the file `rigless run --coverage` writes.
    pub coverage: Coverage,
    /// Where the run printed, flushed.
    pub output: W,
}

/// Runs a firmware on a session's target like [`run`] does, and prints what `rigless run` prints: a
/// line per access as it happens, each of the board's outputs that changed, read every `outputs`
/// period, then the stop, the resets, the unmapped, refused, unimplemented and unrouted gaps, and
/// what a model told. A refusal by a shut gate is the part saying no, so it isn't one.
///
/// The session's `until` is how far to run. [`NO_DEADLINE`] runs until the process is interrupted.
/// The debugger is `start`'s. A host on one of the board's wires is attached before the session is
/// made, with [`Board::attach_host`].
///
/// # Errors
///
/// What [`run`] answers, what a model's access failed with, and a write to `output` that failed. An
/// `outputs` period of zero is [`Error::ZeroPeriod`], before anything runs.
pub fn run_printed<D, W>(
    mut session: Session<D>,
    start: &Start<'_>,
    outputs: Option<VirtualNanos>,
    output: W,
) -> Result<Printed<D, W>, Error>
where
    D: Board + ?Sized + 'static,
    W: Write + Send + 'static,
{
    if outputs.is_some_and(|period| period.0 == 0) {
        return Err(Error::ZeroPeriod);
    }
    let until = session.until();
    // With outputs to read, the first stop is the first period, and each stop asks for the next.
    session.run_until(outputs.map_or(until, |period| period.min(until)));
    let target = session.target();
    let coverage = Coverage::of(
        session
            .board_mut()
            .mcu(target)
            .expect("the session checked the target"),
    );
    let driver = Printing {
        output: BufWriter::new(output),
        coverage,
        until,
        period: outputs,
        outputs: Vec::new(),
        failed: None,
    };
    let Finished {
        mut session,
        driver,
        stop,
    } = run(session, start, driver)?;
    let Printing {
        mut output,
        coverage,
        failed,
        ..
    } = driver;
    if let Some(error) = failed {
        return Err(error.into());
    }
    if let Some(error) = session.take_error() {
        return Err(error);
    }
    if let Some(stop) = stop {
        writeln!(
            output,
            "stop t={} n={} reason={}",
            stop.at.0,
            stop.instruction_count,
            stop_reason(stop.reason)
        )?;
    }
    for reset in session.resets() {
        let cause = match reset.cause {
            crate::device::ResetCause::Core => "core".to_owned(),
            crate::device::ResetCause::Model(instance) => instance.to_owned(),
        };
        writeln!(
            output,
            "reset t={} n={} cause={cause}",
            reset.at.0, reset.instructions
        )?;
    }
    for unmapped in session.unmapped() {
        writeln!(output, "{unmapped}")?;
    }
    for (address, fault) in session.refused() {
        if !fault.is_shut_gate() {
            writeln!(output, "refused at={address:#010x} why={fault}")?;
        }
    }
    for (address, seen) in session.unimplemented() {
        let wrote = seen
            .written
            .map_or_else(String::new, |value| format!(" wrote={value:#010x}"));
        writeln!(
            output,
            "unimplemented instance={} register={} at={address:#010x} pc={:#010x} reads={} \
             writes={}{wrote}",
            seen.instance, seen.register, seen.program_counter, seen.reads, seen.writes
        )?;
    }
    for (instance, interrupt) in session.unrouted() {
        writeln!(output, "unrouted instance={instance} interrupt={interrupt}")?;
    }
    for told in session.told() {
        writeln!(
            output,
            "told instance={} register={} t={} reason={}",
            told.instance, told.register, told.at.0, told.reason
        )?;
    }
    let output = output
        .into_inner()
        .map_err(io::IntoInnerError::into_error)?;
    Ok(Printed {
        session,
        coverage,
        output,
    })
}

/// Prints every access as it happens and the outputs as they change.
struct Printing<W: Write + Send> {
    output: BufWriter<W>,
    coverage: Coverage,
    until: VirtualNanos,
    period: Option<VirtualNanos>,
    outputs: Vec<i64>,
    failed: Option<io::Error>,
}

impl<D: Board + ?Sized, W: Write + Send> Driver<D> for Printing<W> {
    fn on_access(&mut self, access: &Access, value: u64) {
        self.coverage.saw(access);
        if self.failed.is_none() {
            self.failed = write_access(&mut self.output, access, value).err();
        }
    }

    fn on_stop(&mut self, session: &mut Session<D>, stop: Stop) -> Option<VirtualNanos> {
        let period = self.period?;
        if let Some(edge) = session.board_mut().edge() {
            let fields = edge.output_fields();
            match values_of(fields, &edge.drive_wire()) {
                Ok(read) => {
                    for (index, (field, value)) in fields.iter().zip(&read).enumerate() {
                        if self.outputs.get(index) != Some(value) && self.failed.is_none() {
                            self.failed = writeln!(
                                self.output,
                                "output t={} {}={value}",
                                stop.at.0, field.name
                            )
                            .err();
                        }
                    }
                    self.outputs = read;
                }
                Err(error) => {
                    self.failed.get_or_insert_with(|| io::Error::other(error));
                }
            }
        }
        if stop.at >= self.until {
            return None;
        }
        Some(VirtualNanos(
            stop.at.0.saturating_add(period.0).min(self.until.0),
        ))
    }
}

fn write_access(output: &mut impl Write, access: &Access, value: u64) -> io::Result<()> {
    let kind = match access.kind {
        AccessKind::Read => "read ",
        AccessKind::Write => "write",
    };
    writeln!(
        output,
        "{kind} t={} n={} pc={:#010x} at={:#010x} w={} v={value:#010x}",
        access.virtual_time.0,
        access.instruction_count,
        access.program_counter,
        access.address,
        access.width.bits(),
    )
}

fn stop_reason(reason: StopReason) -> &'static str {
    match reason {
        StopReason::Deadline => "deadline",
        StopReason::Idle => "idle",
        StopReason::Reset => "reset",
    }
}