rigless 0.2.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
//! `monitor`, `qRcmd`, "Debugging with GDB" for GDB 18.1, section E.5: a table of commands, each
//! matched on the longest run of words its text starts with, the rest of the text being its
//! argument, which a command that takes none refuses.
//!
//! Nothing here writes a file on the host or starts a server, whatever another emulator's monitor
//! does, since anyone who reaches the debugger's port would then reach the host.

use std::fmt::Write as _;

use super::super::Core;
use super::history::{Halted, Input};
use super::memory::Reads;
use super::packet::{from_hex, to_hex};
use super::threads::Reason;
use super::{Asked, Debugger, HaltAt, Held};
use crate::emulator::HaltReason;
use crate::time::parse_virtual_time;

/// One monitor command: the words it's typed as, what `monitor help` says about it, whether it
/// needs a debugger to have the core, whether it takes an argument after its words, and what it
/// does with that argument.
struct Command {
    words: &'static str,
    help: &'static str,
    needs_core: bool,
    takes_argument: bool,
    run: fn(&mut Debugger<'_>, &mut Core, &str) -> String,
}

/// Every monitor command. IDEs' launch templates for an external GDB server send `reset halt` and
/// `reset init`, and other emulators' scripts send `system_reset`, so each is `reset`, which holds
/// the core already.
const COMMANDS: &[Command] = &[
    Command {
        words: "reset",
        help: "resets the machine as the firmware's own request does, and holds the core at its first \
               instruction. GDB keeps the registers it read, so ask again with `maintenance flush \
               register-cache`, or restart with `run` under `target extended-remote`.",
        needs_core: true,
        takes_argument: false,
        run: reset,
    },
    Command {
        words: "reset halt",
        help: "`monitor reset`, which holds the core already.",
        needs_core: true,
        takes_argument: false,
        run: reset,
    },
    Command {
        words: "reset init",
        help: "`monitor reset`.",
        needs_core: true,
        takes_argument: false,
        run: reset,
    },
    Command {
        words: "system_reset",
        help: "`monitor reset`.",
        needs_core: true,
        takes_argument: false,
        run: reset,
    },
    Command {
        words: "halt",
        help: "says whether the core is held, which it is whenever a debugger has it.",
        needs_core: false,
        takes_argument: false,
        run: halt,
    },
    Command {
        words: "help",
        help: "this.",
        needs_core: false,
        takes_argument: false,
        run: help,
    },
    Command {
        words: "icount",
        help: "prints how many instructions the core has retired, which `$icount` reads too, and \
               the virtual time it's at.",
        needs_core: true,
        takes_argument: false,
        run: icount,
    },
    Command {
        words: "until-icount",
        help: "`until-icount N` stops the core once it has retired N instructions, when you next \
               continue, as a breakpoint stops it. An N the run has passed is a place \
               `reverse-continue` stops at.",
        needs_core: true,
        takes_argument: true,
        run: until_icount,
    },
    Command {
        words: "until-time",
        help: "`until-time 5ms` stops the core at that instant of virtual time, waiting or not, when \
               you next continue. It takes ns, us, ms and s.",
        needs_core: true,
        takes_argument: true,
        run: until_time,
    },
    Command {
        words: "peek on",
        help: "makes your reads of memory peeks, as they are when the run starts: a read of a \
               peripheral answers what the firmware's would, and changes nothing in the run.",
        needs_core: false,
        takes_argument: false,
        run: peek_on,
    },
    Command {
        words: "peek off",
        help: "makes your reads of memory the firmware's for the rest of the run, side effects \
               and all, and the run prints each with `pc=debugger`.",
        needs_core: false,
        takes_argument: false,
        run: peek_off,
    },
    Command {
        words: "save",
        help: "`save NAME` keeps the run where it stands under NAME for this session, in memory, to \
               go back or forward to with `monitor restore NAME`.",
        needs_core: true,
        takes_argument: true,
        run: save,
    },
    Command {
        words: "restore",
        help: "`restore NAME` takes the run to where `monitor save NAME` kept it, back or forward, \
               and holds the core there. GDB keeps what it read, so ask again with `maintenance \
               flush register-cache` and `maintenance flush dcache`. Going on from there is the \
               same run: the driver hears nothing twice, and what you wrote or stepped comes in \
               again where it came, until you do something it didn't.",
        needs_core: true,
        takes_argument: true,
        run: restore,
    },
    Command {
        words: "snapshots",
        help: "lists what `monitor save` kept, each with its icount and virtual time.",
        needs_core: false,
        takes_argument: false,
        run: snapshots,
    },
];

impl Debugger<'_> {
    /// `qRcmd`, whose command comes in hex and whose answer goes back as hex for GDB to print.
    pub(super) fn monitor(&mut self, core: &mut Core, command: &str) -> Asked {
        let Some(command) = from_hex(command).and_then(|bytes| String::from_utf8(bytes).ok())
        else {
            return Asked::Reply("E01".to_owned());
        };
        let text = command.trim();
        let said = match command_in(text) {
            Some((command, argument)) if !command.takes_argument && !argument.is_empty() => {
                format!("no monitor command \"{text}\", try \"monitor help\"\n")
            }
            Some((command, _)) if command.needs_core && !self.attached() => {
                "no debugger has the core, so attach first, with `attach 1`\n".to_owned()
            }
            Some((command, argument)) => (command.run)(self, core, argument),
            // The family's own commands ask something of the core.
            None if !self.attached() => {
                "no debugger has the core, so attach first, with `attach 1`\n".to_owned()
            }
            // A family's command moves nothing and the run hears nothing of it, so it's noted
            // once it's known to be one.
            None => match core.machine.cpu().monitor_command(text) {
                Some(said) => {
                    self.note(core, Input::Monitor(text.to_owned()), |_| ());
                    said
                }
                None => format!("no monitor command \"{text}\", try \"monitor help\"\n"),
            },
        };
        Asked::Reply(to_hex(said.as_bytes()))
    }
}

/// The command with the longest words `text` starts with, as whole words, and the rest of `text`.
fn command_in(text: &str) -> Option<(&'static Command, &str)> {
    COMMANDS
        .iter()
        .filter_map(|command| {
            let rest = text.strip_prefix(command.words)?;
            (rest.is_empty() || rest.starts_with(char::is_whitespace))
                .then(|| (command, rest.trim_start()))
        })
        .max_by_key(|(command, _)| command.words.len())
}

/// Resets the machine, and the core it holds is the debugger's halt, a reset's own vector catch
/// included.
fn reset(debugger: &mut Debugger<'_>, core: &mut Core, _argument: &str) -> String {
    debugger.note(core, Input::Reset, super::history::reset);
    debugger.disarm(core);
    debugger.threads.forget();
    debugger.stopped = debugger.threads.stopped(core, Reason::Trap);
    "reset, and held at the first instruction\n".to_owned()
}

fn save(debugger: &mut Debugger<'_>, core: &mut Core, argument: &str) -> String {
    debugger.save(core, argument)
}

fn restore(debugger: &mut Debugger<'_>, core: &mut Core, argument: &str) -> String {
    debugger.restore(core, argument)
}

fn snapshots(debugger: &mut Debugger<'_>, _core: &mut Core, _argument: &str) -> String {
    debugger.snapshots()
}

fn halt(debugger: &mut Debugger<'_>, core: &mut Core, _argument: &str) -> String {
    if debugger.core != Held::Halted {
        return "running\n".to_owned();
    }
    if debugger.attached() {
        let reason = debugger.stopped.reason;
        debugger.hold(core, reason, Halted::By(HaltReason::Requested));
    }
    "held\n".to_owned()
}

fn peek_on(debugger: &mut Debugger<'_>, _core: &mut Core, _argument: &str) -> String {
    debugger.reads = Reads::Peeks;
    "your reads are peeks\n".to_owned()
}

fn peek_off(debugger: &mut Debugger<'_>, _core: &mut Core, _argument: &str) -> String {
    debugger.reads = Reads::Firmware;
    "your reads are the firmware's\n".to_owned()
}

fn help(_debugger: &mut Debugger<'_>, core: &mut Core, _argument: &str) -> String {
    let mut said = COMMANDS.iter().fold(String::new(), |mut said, command| {
        writeln!(said, "monitor {}: {}", command.words, command.help).expect("a String takes it");
        said
    });
    said.push_str(
        &core
            .machine
            .cpu()
            .monitor_command("help")
            .unwrap_or_default(),
    );
    said.push_str(WATCHES);
    said
}

fn icount(_debugger: &mut Debugger<'_>, core: &mut Core, _argument: &str) -> String {
    let retired = core.machine.retired();
    format!("icount {retired} at {} ns\n", core.machine.time(retired).0)
}

/// Arms a halt point at a count ahead of the run. One behind it is where `bc` stops in a run that
/// can go back, and refused in one that can't, one where it stands is refused, and so is any while
/// the debugger has detached.
fn until_icount(debugger: &mut Debugger<'_>, core: &mut Core, argument: &str) -> String {
    let Ok(count) = argument.parse::<u64>() else {
        return "until-icount takes an instruction count, like until-icount 123456\n".to_owned();
    };
    let retired = core.machine.retired();
    if count < retired && Debugger::reverses(core) {
        debugger.history.behind = Some(count);
        return format!("the core stops at icount {count} when you reverse-continue\n");
    }
    if count <= retired {
        return format!("the run is at icount {retired} already\n");
    }
    debugger.arm(core, HaltAt::Count(count));
    format!("the core stops at icount {count} when you continue\n")
}

/// Arms a halt point at an instant ahead of the run. One behind it, or where it stands, is refused,
/// and so is any while the debugger has detached.
fn until_time(debugger: &mut Debugger<'_>, core: &mut Core, argument: &str) -> String {
    let instant = match parse_virtual_time(argument) {
        Ok(instant) => instant,
        Err(error) => {
            return format!("until-time takes an instant, like until-time 5ms: {error}\n");
        }
    };
    let now = core.machine.now();
    if instant <= now {
        return format!("the run is at {} ns already\n", now.0);
    }
    debugger.arm(core, HaltAt::Instant(instant));
    format!("the core stops at {} ns when you continue\n", instant.0)
}

/// What `monitor help` says about watchpoints after the commands, since they have no command of
/// their own and a big one costs the run its speed.
const WATCHES: &str = "A watchpoint has every access to each page of memory it touches checked, so \
                       one over a lot of memory slows the run while it's in.\n";

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

    /// The longest words win, whole words only, and what's after them is the argument.
    #[test]
    fn a_command_is_the_longest_words_its_text_starts_with() {
        let words = |text| command_in(text).map(|(command, argument)| (command.words, argument));
        assert_eq!(words("reset"), Some(("reset", "")));
        assert_eq!(words("reset halt"), Some(("reset halt", "")));
        assert_eq!(words("reset  run now"), Some(("reset", "run now")));
        assert_eq!(words("reset halted"), Some(("reset", "halted")));
        assert_eq!(words("resets"), None);
        assert_eq!(words("info registers"), None);
    }
}