rigless 0.2.2

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
//! Breakpoints and watchpoints, `Z` and `z`, and what GDB has the server do at a breakpoint itself:
//! test its conditions, and run its commands, "Debugging with GDB" for GDB 18.1, section E.3's
//! `Z0`.

use std::io::Write as _;

use super::super::Core;
use super::Debugger;
use super::agent::{self, Failed, Inspect};
use super::memory::peek;
use super::packet::{from_hex, hex};
use super::registers::value;
use crate::emulator::{AccessKind, DataModel};
use crate::soc::Width;

/// What GDB asks the server to do at a breakpoint, as agent expressions: conditions, any one of
/// which holding has a hit stand, and commands, which run at a hit that stands and leave the core
/// running, as `dprintf` does with `set dprintf-style agent`.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(super) struct Agent {
    conditions: Vec<Vec<u8>>,
    commands: Vec<Vec<u8>>,
}

impl Agent {
    /// The conditions and commands after a `Z0` or `Z1`'s kind: `;` then `X<len>,<bytecode>` for
    /// each condition, then `;cmds:<persist>,` and the same for each command, or `None` for ones
    /// that don't parse.
    // ceiling: a persistent command ends with the debugger that set it; upgrade when a run keeps
    // printing with no debugger attached
    fn parse(rest: &str) -> Option<Self> {
        let (conditions, commands) = match rest.split_once("cmds:") {
            Some((conditions, commands)) => {
                let (_persist, commands) = commands.split_once(',')?;
                (conditions.strip_suffix(';').unwrap_or(conditions), commands)
            }
            None => (rest, ""),
        };
        Some(Self {
            conditions: expressions(conditions)?,
            commands: expressions(commands)?,
        })
    }
}

/// Each `X<len>,<bytecode>`, written one after another.
fn expressions(mut text: &str) -> Option<Vec<Vec<u8>>> {
    let mut each = Vec::new();
    while !text.is_empty() {
        let (length, rest) = text.strip_prefix('X')?.split_once(',')?;
        let digits = usize::try_from(hex(length)?).ok()?.checked_mul(2)?;
        let code = rest.get(..digits)?;
        each.push(from_hex(code)?);
        text = rest.get(digits..)?;
    }
    Some(each)
}

/// The core as an agent expression reads it: its registers as `g` reads them, and memory peeked.
struct Reading<'a>(&'a mut Core);

impl Inspect for Reading<'_> {
    fn register(&mut self, number: u16) -> Option<u64> {
        let endian = self.0.machine.memory.endian();
        value(self.0.machine.cpu(), usize::from(number), endian)
    }

    fn read(&mut self, address: u64, width: Width) -> Option<u64> {
        peek(self.0, address, width)
    }

    fn data_model(&mut self) -> DataModel {
        self.0.machine.cpu().data_model()
    }
}

/// The most breakpoints a debugger may set at once, each one more address the core looks out for.
const BREAKPOINTS: usize = 1 << 12;

/// A debugger's breakpoint as the packet that set it names it, `Z0` or `Z1`. Both are the same
/// thing to the core, and a stop at one says which it was.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub(super) enum Breakpoint {
    Software,
    Hardware,
}

/// What a debugger's watchpoint stops the core on, `Z2` to `Z4`.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(in super::super) enum Watchpoint {
    Write,
    Read,
    Access,
}

impl Watchpoint {
    /// The watchpoint a `Z` packet's `kind` names.
    fn of(kind: &str) -> Option<Self> {
        match kind {
            "2" => Some(Self::Write),
            "3" => Some(Self::Read),
            "4" => Some(Self::Access),
            _ => None,
        }
    }

    /// Whether an access of `kind` hits it.
    pub(in super::super) fn watches(self, kind: AccessKind) -> bool {
        match self {
            Self::Write => kind == AccessKind::Write,
            Self::Read => kind == AccessKind::Read,
            Self::Access => true,
        }
    }

    /// What a stop reply calls the hit.
    pub(super) fn reason(self) -> &'static str {
        match self {
            Self::Write => "watch",
            Self::Read => "rwatch",
            Self::Access => "awatch",
        }
    }
}

impl Debugger<'_> {
    /// The kind of the debugger's breakpoint at `address`, if it set one there.
    pub(super) fn breakpoint_at(&self, address: u64) -> Option<Breakpoint> {
        self.breakpoints
            .range((address, Breakpoint::Software)..=(address, Breakpoint::Hardware))
            .next()
            .map(|(&(_, kind), _)| kind)
    }

    /// Whether the debugger's breakpoints at `address` stop the core there: one with no commands
    /// and either no conditions or one that holds. Each that holds and has commands runs
    /// them, and what they print goes to GDB. An expression that fails stops the core, and says so.
    pub(super) fn stops_at(&mut self, core: &mut Core, address: u64) -> bool {
        let points: Vec<Agent> = self
            .breakpoints
            .range((address, Breakpoint::Software)..=(address, Breakpoint::Hardware))
            .map(|(_, agent)| agent.clone())
            .collect();
        let mut printed = Vec::new();
        let mut stops = false;
        for point in points {
            let mut reading = Reading(core);
            let holds = if point.conditions.is_empty() {
                Ok(true)
            } else {
                point.conditions.iter().try_fold(false, |holds, condition| {
                    Ok::<_, Failed>(
                        holds || agent::evaluate(condition, &mut reading, &mut printed)? != 0,
                    )
                })
            };
            let ran = match holds {
                Ok(true) if point.commands.is_empty() => {
                    stops = true;
                    Ok(())
                }
                Ok(true) => point.commands.iter().try_for_each(|command| {
                    agent::evaluate(command, &mut reading, &mut printed).map(drop)
                }),
                Ok(false) => Ok(()),
                Err(failed) => Err(failed),
            };
            if let Err(failed) = ran {
                stops = true;
                writeln!(
                    printed,
                    "the breakpoint at {address:#x} stopped the core: {failed}"
                )
                .expect("a Vec takes any bytes");
            }
        }
        // A run going forward over what it did printed this the first time. In non-stop mode GDB
        // takes no `O` packet, so it goes to the run's output.
        if !printed.is_empty() && self.history.first_print(core.machine.position()) {
            if self.non_stop() {
                let (at, count) = (core.machine.now(), core.machine.retired());
                core.driven.debugger_print(at, count, &printed);
            } else {
                self.console(core, &printed);
            }
        }
        stops
    }

    /// Sets or clears a breakpoint or a watchpoint. A `Z0` and a `Z1` at one address are two asks
    /// of the core, each taken back by its own `z`. A breakpoint set again, as GDB does when its
    /// conditions or commands change, keeps its ask and takes the new ones.
    pub(super) fn point(&mut self, core: &mut Core, set: bool, rest: &str) -> &'static str {
        let (rest, agent) = rest.split_once(';').unwrap_or((rest, ""));
        let Some(agent) = Agent::parse(agent) else {
            return "E01";
        };
        let mut fields = rest.split(',');
        let (Some(kind), Some(address), Some(length)) = (
            fields.next(),
            fields.next().and_then(hex),
            fields.next().and_then(hex),
        ) else {
            return "E01";
        };
        match kind {
            "0" | "1" => {
                let each = if kind == "0" {
                    (address, Breakpoint::Software)
                } else {
                    (address, Breakpoint::Hardware)
                };
                if set {
                    if !self.breakpoints.contains_key(&each)
                        && (self.breakpoints.len() >= BREAKPOINTS
                            || !core.machine.cpu().add_breakpoint(address))
                    {
                        return "E01";
                    }
                    self.breakpoints.insert(each, agent);
                } else if self.breakpoints.remove(&each).is_some() {
                    core.machine.cpu().remove_breakpoint(address);
                }
                "OK"
            }
            _ => {
                let Some(watchpoint) = Watchpoint::of(kind) else {
                    return "";
                };
                let Some(last) = length
                    .checked_sub(1)
                    .and_then(|extent| address.checked_add(extent))
                else {
                    return "E01";
                };
                if core
                    .machine
                    .debugger_watch((address, last), watchpoint, set)
                {
                    "OK"
                } else {
                    "E01"
                }
            }
        }
    }
}

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

    /// Each `X` expression after a `Z0`'s kind is a condition, one after another with nothing
    /// between them, and each after `cmds:` is a command. One that's short or that isn't hex
    /// doesn't parse.
    #[test]
    fn conditions_and_commands_parse_one_after_another() {
        let never = vec![0x22, 0x00, 0x27];
        let always = vec![0x22, 0x01, 0x27];
        let both = Agent::parse("X3,220027X3,220127").unwrap();
        assert_eq!(both.conditions, [never.clone(), always.clone()]);
        assert!(both.commands.is_empty());
        let printing = Agent::parse("X3,220027;cmds:1,X3,220127X3,220027").unwrap();
        assert_eq!(printing.conditions, std::slice::from_ref(&never));
        assert_eq!(printing.commands, [always, never]);
        assert_eq!(Agent::parse(""), Some(Agent::default()));
        for malformed in [
            "X4,220027",
            "X3,22zz27",
            "Y3,220027",
            "X3,220027;cmds:X3,220027",
        ] {
            assert_eq!(Agent::parse(malformed), None, "{malformed}");
        }
    }
}