miden-debug 0.17.0

An interactive debugger for Miden VM programs
Documentation
use std::{
    io::Write,
    path::PathBuf,
    string::{String, ToString},
    vec::Vec,
};

use super::engine::Outcome;
use crate::{
    config::DebuggerConfig,
    script::{PythonScriptSession, ScriptDebugger},
};

pub(crate) fn project_init_file(config: &DebuggerConfig) -> Option<PathBuf> {
    if config.no_user_python_init {
        return None;
    }

    let path = config.working_dir().join(".miden-debug.py");
    path.try_exists().ok().is_some_and(|exists| exists).then_some(path)
}

pub(crate) fn execute_line(
    debugger: &ScriptDebugger,
    python: &mut PythonScriptSession,
    line: &str,
    out: &mut dyn Write,
) -> Result<Outcome, String> {
    match parse_script_command(line) {
        Some(ScriptCommand::Snippet(code)) => {
            let output = python.execute_snippet(code)?;
            write!(out, "{output}").map_err(|err| format!("failed to write output: {err}"))?;
            Ok(Outcome::Continue)
        }
        Some(ScriptCommand::InteractiveConsole) => {
            python.interact()?;
            Ok(Outcome::Continue)
        }
        Some(ScriptCommand::Import(path)) => {
            python.import_file(path)?;
            writeln!(out, "Imported Python script: {path}")
                .map_err(|err| format!("failed to write output: {err}"))?;
            Ok(Outcome::Continue)
        }
        Some(ScriptCommand::AddCommand { name, function }) => {
            python.add_custom_command(name, function)?;
            writeln!(out, "Added Python command: {name}")
                .map_err(|err| format!("failed to write output: {err}"))?;
            Ok(Outcome::Continue)
        }
        Some(ScriptCommand::ListCommands) => {
            let commands = python.custom_commands();
            if commands.is_empty() {
                writeln!(out, "No Python commands registered")
                    .map_err(|err| format!("failed to write output: {err}"))?;
            } else {
                writeln!(out, "Python commands:")
                    .map_err(|err| format!("failed to write output: {err}"))?;
                for command in commands {
                    writeln!(out, "  {command}")
                        .map_err(|err| format!("failed to write output: {err}"))?;
                }
            }
            Ok(Outcome::Continue)
        }
        Some(ScriptCommand::DeleteCommand(name)) => {
            python.delete_custom_command(name)?;
            writeln!(out, "Deleted Python command: {name}")
                .map_err(|err| format!("failed to write output: {err}"))?;
            Ok(Outcome::Continue)
        }
        Some(ScriptCommand::AddBreakpointCallback { id, function }) => {
            python.add_breakpoint_callback(id, function)?;
            writeln!(out, "Added Python callback for breakpoint {id}")
                .map_err(|err| format!("failed to write output: {err}"))?;
            Ok(Outcome::Continue)
        }
        Some(ScriptCommand::ListBreakpointCallbacks) => {
            let callbacks = python.breakpoint_callbacks();
            if callbacks.is_empty() {
                writeln!(out, "No Python breakpoint callbacks registered")
                    .map_err(|err| format!("failed to write output: {err}"))?;
            } else {
                writeln!(out, "Python breakpoint callbacks:")
                    .map_err(|err| format!("failed to write output: {err}"))?;
                for id in callbacks {
                    writeln!(out, "  breakpoint {id}")
                        .map_err(|err| format!("failed to write output: {err}"))?;
                }
            }
            Ok(Outcome::Continue)
        }
        Some(ScriptCommand::DeleteBreakpointCallback(id)) => {
            python.delete_breakpoint_callback(id)?;
            match id {
                Some(id) => writeln!(out, "Deleted Python callback for breakpoint {id}"),
                None => writeln!(out, "Deleted all Python breakpoint callbacks"),
            }
            .map_err(|err| format!("failed to write output: {err}"))?;
            Ok(Outcome::Continue)
        }
        None if is_resume_command(line) => execute_resume_command(debugger, python, line, out),
        None => match debugger.execute_repl_line(line, out) {
            Err(err) if is_unknown_command_error(line, &err) => {
                let (name, args) = split_command(line);
                match python.execute_custom_command(name, args)? {
                    Some(output) => {
                        write!(out, "{output}")
                            .map_err(|err| format!("failed to write output: {err}"))?;
                        Ok(Outcome::Continue)
                    }
                    None => Err(err),
                }
            }
            outcome => outcome,
        },
    }
}

enum ScriptCommand<'a> {
    Snippet(&'a str),
    InteractiveConsole,
    Import(&'a str),
    AddCommand { name: &'a str, function: &'a str },
    ListCommands,
    DeleteCommand(&'a str),
    AddBreakpointCallback { id: u8, function: &'a str },
    ListBreakpointCallbacks,
    DeleteBreakpointCallback(Option<u8>),
}

fn parse_script_command(line: &str) -> Option<ScriptCommand<'_>> {
    let line = line.trim();
    if let Some(command) = parse_command_script_command(line) {
        return Some(command);
    }
    if let Some(command) = parse_breakpoint_command(line) {
        return Some(command);
    }

    if line == "script" {
        return Some(ScriptCommand::InteractiveConsole);
    }

    let code = line.strip_prefix("script")?;
    if code.chars().next().is_some_and(char::is_whitespace) {
        let code = code.trim_start();
        if code.is_empty() {
            Some(ScriptCommand::InteractiveConsole)
        } else {
            Some(ScriptCommand::Snippet(code))
        }
    } else {
        None
    }
}

fn execute_resume_command(
    debugger: &ScriptDebugger,
    python: &mut PythonScriptSession,
    line: &str,
    out: &mut dyn Write,
) -> Result<Outcome, String> {
    let mut command = line.to_string();
    loop {
        let mut buffered_output = Vec::new();
        let outcome = debugger.execute_repl_line(&command, &mut buffered_output)?;
        if outcome == Outcome::Quit || debugger.terminated() {
            out.write_all(&buffered_output)
                .map_err(|err| format!("failed to write output: {err}"))?;
            return Ok(outcome);
        }

        if python.should_continue_after_breakpoint_callbacks()? {
            debugger.clear_hit_breakpoints();
            command = "continue".into();
            continue;
        }

        out.write_all(&buffered_output)
            .map_err(|err| format!("failed to write output: {err}"))?;
        return Ok(outcome);
    }
}

fn parse_breakpoint_command(line: &str) -> Option<ScriptCommand<'_>> {
    if let Some(rest) = line.strip_prefix("breakpoint command add")
        && rest.chars().next().is_some_and(char::is_whitespace)
        && let Some((id, function)) = parse_breakpoint_command_add_args(rest.trim_start())
    {
        return Some(ScriptCommand::AddBreakpointCallback { id, function });
    }

    if let Some(rest) = line.strip_prefix("breakpoint command list")
        && rest.trim().is_empty()
    {
        return Some(ScriptCommand::ListBreakpointCallbacks);
    }

    if let Some(rest) = line.strip_prefix("breakpoint command delete") {
        let rest = rest.trim();
        if rest.is_empty() {
            return Some(ScriptCommand::DeleteBreakpointCallback(None));
        }
        if let Ok(id) = rest.parse::<u8>() {
            return Some(ScriptCommand::DeleteBreakpointCallback(Some(id)));
        }
    }

    None
}

fn parse_breakpoint_command_add_args(args: &str) -> Option<(u8, &str)> {
    let (id, rest) = split_command(args);
    let id = id.parse::<u8>().ok()?;
    let rest = rest.trim();
    let function = rest.strip_prefix("-f")?.trim_start();
    if function.is_empty() {
        None
    } else {
        Some((id, function))
    }
}

fn parse_command_script_command(line: &str) -> Option<ScriptCommand<'_>> {
    if let Some(path) = line.strip_prefix("command script import")
        && path.chars().next().is_some_and(char::is_whitespace)
    {
        let path = path.trim_start();
        if !path.is_empty() {
            return Some(ScriptCommand::Import(path));
        }
    }

    if let Some(rest) = line.strip_prefix("command script list")
        && rest.trim().is_empty()
    {
        return Some(ScriptCommand::ListCommands);
    }

    if let Some(name) = line.strip_prefix("command script delete")
        && name.chars().next().is_some_and(char::is_whitespace)
    {
        let name = name.trim_start();
        if !name.is_empty() {
            return Some(ScriptCommand::DeleteCommand(name));
        }
    }

    if let Some(rest) = line.strip_prefix("command script add")
        && rest.chars().next().is_some_and(char::is_whitespace)
        && let Some((name, function)) = parse_command_script_add_args(rest.trim_start())
    {
        return Some(ScriptCommand::AddCommand { name, function });
    }

    None
}

fn parse_command_script_add_args(args: &str) -> Option<(&str, &str)> {
    let (name, rest) = split_command(args);
    let rest = rest.trim();
    let function = rest.strip_prefix("-f")?.trim_start();
    if name.is_empty() || function.is_empty() {
        None
    } else {
        Some((name, function))
    }
}

fn split_command(line: &str) -> (&str, &str) {
    match line.trim().split_once(char::is_whitespace) {
        Some((name, args)) => (name, args.trim_start()),
        None => (line.trim(), ""),
    }
}

fn is_unknown_command_error(line: &str, err: &str) -> bool {
    let (name, _) = split_command(line);
    err == format!("unknown command: {name}")
}

fn is_resume_command(line: &str) -> bool {
    let (name, _) = split_command(line);
    matches!(
        name,
        "c" | "continue" | "n" | "next" | "nl" | "next-line" | "nextline" | "e" | "finish"
    )
}

#[cfg(test)]
mod tests;