miden-debug-engine 0.17.0

Core debugger engine for miden-debug
Documentation
use std::{
    io::{BufRead, Read, Write},
    net::Shutdown,
    thread,
    time::Duration,
};

use miden_assembly::{Assembler, DefaultSourceManager, ast::Module};
use miden_core::Felt;
use serde_json::{Value, json};

use super::*;
use crate::exec::DebuggerHost;

struct Session {
    reader: BufReader<std::net::TcpStream>,
    writer: BufWriter<std::net::TcpStream>,
    server: Option<thread::JoinHandle<Result<ExecutionOutput, ExecutionError>>>,
    sequence: i64,
}

impl Session {
    fn start(source: &str, config: DapConfig) -> Self {
        let source_manager = Arc::new(DefaultSourceManager::default());
        let package: Arc<Package> = Assembler::new(source_manager.clone())
            .assemble_program("protocol-test", source)
            .unwrap()
            .into();
        Self::start_package(package, source_manager, config)
    }

    fn start_module(module: Module, source_manager: Arc<DefaultSourceManager>) -> Self {
        let package: Arc<Package> = Assembler::new(source_manager.clone())
            .assemble_program("protocol-test", module)
            .unwrap()
            .into();
        Self::start_package(package, source_manager, DapConfig::new("127.0.0.1:0"))
    }

    fn start_package(
        package: Arc<Package>,
        source_manager: Arc<DefaultSourceManager>,
        config: DapConfig,
    ) -> Self {
        let listener = DapExecutor::bind_listener("127.0.0.1:0").unwrap();
        let address = listener.local_addr().unwrap();
        let mut executor = DapExecutor::new(
            StackInputs::default(),
            AdviceInputs::default(),
            ExecutionOptions::default(),
        );
        executor.config = config;
        executor.event_recorder = Some(EventMutationRecorder::new());
        executor.forest_recorder = Some(MastForestRecorder::new());
        let server = thread::spawn(move || {
            let mut host = DebuggerHost::new(source_manager);
            executor.execute_on_listener(package, &mut host, listener)
        });
        let stream = std::net::TcpStream::connect(address).unwrap();
        stream.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
        stream.set_write_timeout(Some(Duration::from_secs(5))).unwrap();
        Self {
            reader: BufReader::new(stream.try_clone().unwrap()),
            writer: BufWriter::new(stream),
            server: Some(server),
            sequence: 0,
        }
    }

    fn read(&mut self) -> Value {
        let mut length = None;
        loop {
            let mut line = String::new();
            assert_ne!(
                self.reader.read_line(&mut line).unwrap(),
                0,
                "server closed before sending a message"
            );
            if let Some(value) = line.trim().strip_prefix("Content-Length:") {
                length = Some(value.trim().parse::<usize>().unwrap());
            } else if line.trim().is_empty() && length.is_some() {
                break;
            }
        }
        let mut bytes = vec![0; length.unwrap()];
        self.reader.read_exact(&mut bytes).unwrap();
        serde_json::from_slice(&bytes).unwrap()
    }

    fn request(&mut self, command: &str, arguments: Value) -> Value {
        self.sequence += 1;
        let payload = json!({"seq": self.sequence, "type": "request", "command": command, "arguments": arguments}).to_string();
        write!(self.writer, "Content-Length: {}\r\n\r\n{payload}", payload.len()).unwrap();
        self.writer.flush().unwrap();
        loop {
            let message = self.read();
            if message["type"] == "response" {
                assert_eq!(message["request_seq"], self.sequence);
                return message;
            }
        }
    }

    fn stop(&mut self) -> (Value, Option<Value>) {
        let mut snapshot = None;
        loop {
            let message = self.read();
            match message["event"].as_str() {
                Some("miden/uiState") => snapshot = Some(message["body"].clone()),
                Some("stopped" | "terminated") => return (message, snapshot),
                _ => {}
            }
        }
    }

    fn handshake(&mut self, command: &str) {
        let response = self.request("initialize", json!({"adapterID": "test-client"}));
        assert_eq!(response["body"]["supportsRestartRequest"], true);
        assert_eq!(self.request(command, json!({}))["success"], true);
        let (event, snapshot) = self.stop();
        assert_eq!(event["body"]["reason"], "entry");
        assert_eq!(snapshot.unwrap()["cycle"], 0);
        assert_eq!(self.request("configurationDone", json!({}))["success"], true);
    }

    fn join(&mut self) -> Result<ExecutionOutput, ExecutionError> {
        self.server.take().unwrap().join().unwrap()
    }

    fn disconnect(mut self) -> ExecutionOutput {
        assert_eq!(self.request("disconnect", json!({}))["success"], true);
        self.join().unwrap()
    }
}

impl Drop for Session {
    fn drop(&mut self) {
        let _ = self.writer.get_ref().shutdown(Shutdown::Both);
    }
}

#[test]
fn dap_session_inspects_initial_state_and_evaluations() {
    let mut session = Session::start(
        "proc increment push.1 add end\nbegin push.7 dup.0 mem_store.0 exec.increment add end",
        DapConfig::new("127.0.0.1:0"),
    );
    session.handshake("attach");
    let threads = session.request("threads", json!({}));
    assert_eq!(threads["body"]["threads"][0]["name"], "main");
    let scopes = session.request("scopes", json!({"frameId": 0}));
    assert_eq!(scopes["body"]["scopes"].as_array().unwrap().len(), 3);
    assert_eq!(scopes["body"]["scopes"][0]["name"], "Local Variables");
    for reference in [SCOPE_STACK, SCOPE_MEMORY, SCOPE_LOCALS, 99] {
        let variables = session.request("variables", json!({"variablesReference": reference}));
        let values = variables["body"]["variables"].as_array().unwrap();
        if reference == SCOPE_STACK {
            assert_eq!(values.len(), 16);
            assert_eq!(values[0]["value"], "0");
        } else {
            assert!(values.is_empty());
        }
    }
    for (expression, expected) in [
        ("__miden_read_memory 0 -t u32", "0"),
        ("vars all", "No debug variables tracked"),
        ("vars", "No source-level variables (use 'vars all' to show compiler locals)"),
    ] {
        let response = session.request("evaluate", json!({"expression": expression}));
        assert_eq!(response["body"]["result"], expected);
    }
    let state = session.request("evaluate", json!({"expression": "__miden_ui_state"}));
    let state: Value = serde_json::from_str(state["body"]["result"].as_str().unwrap()).unwrap();
    assert_eq!(state["cycle"], 0);
    for expression in
        ["missing_variable", "__miden_read_memory invalid", "__miden_read_memory 0 -c 2"]
    {
        assert_eq!(
            session.request("evaluate", json!({"expression": expression}))["success"],
            false
        );
    }
    assert_eq!(
        session.request("pause", json!({"threadId": 1}))["message"],
        "Unsupported command"
    );
    session.disconnect();
}

#[test]
fn dap_session_steps_breaks_restarts_and_records_a_snapshot() {
    let directory = tempfile::tempdir().unwrap();
    let path = directory.path().join("replay.bin");
    let mut config = DapConfig::new("127.0.0.1:0");
    let recorder = config.record_snapshot(path.clone());
    let mut session = Session::start(
        "proc increment push.1 add end\nbegin push.7 dup.0 mem_store.0 exec.increment add end",
        config,
    );
    session.handshake("attach");
    let breakpoints = session.request(
        "setBreakpoints",
        json!({"source": {"path": "missing.rs"}, "breakpoints": [{"line": 10}]}),
    );
    assert_eq!(breakpoints["body"]["breakpoints"][0]["verified"], false);
    let functions = session.request(
        "setFunctionBreakpoints",
        json!({"breakpoints": [{"name": "increment"}, {"name": "[invalid"}]}),
    );
    assert_eq!(functions["body"]["breakpoints"][0]["verified"], true);
    assert_eq!(functions["body"]["breakpoints"][1]["verified"], false);
    assert_eq!(session.request("continue", json!({"threadId": 1}))["success"], true);
    let (event, snapshot) = session.stop();
    assert_eq!(event["body"]["reason"], "breakpoint");
    assert!(snapshot.unwrap()["cycle"].as_u64().unwrap() > 0);
    let trace = session.request("stackTrace", json!({"threadId": 1}));
    assert!(trace["body"]["totalFrames"].as_u64().unwrap() > 0);
    let memory = session.request("variables", json!({"variablesReference": SCOPE_MEMORY}));
    assert!(
        memory["body"]["variables"]
            .as_array()
            .unwrap()
            .iter()
            .any(|value| value["value"] == "7")
    );
    assert_eq!(
        session.request("setFunctionBreakpoints", json!({"breakpoints": []}))["success"],
        true
    );
    for command in ["stepIn", "next", "stepOut"] {
        assert_eq!(
            session.request(command, json!({"threadId": 1, "granularity": "instruction"}))
                ["success"],
            true
        );
        let (event, _) = session.stop();
        assert!(matches!(event["event"].as_str(), Some("stopped" | "terminated")));
    }
    assert_eq!(session.request("restart", json!({}))["success"], true);
    let (_, snapshot) = session.stop();
    assert_eq!(snapshot.unwrap()["cycle"], 0);
    assert_eq!(
        session.request("next", json!({"threadId": 1, "granularity": "statement"}))["success"],
        true
    );
    session.stop();
    let output = session.disconnect();
    assert_eq!(output.stack[0], Felt::from_u32(8));
    let snapshot = crate::exec::ReplaySnapshot::read_from_file(&path).unwrap();
    assert!(snapshot.event_log.is_empty());
    assert_eq!(recorder.take().unwrap().unwrap().path, Uri::from(path.as_path()));
}

#[test]
fn dap_locals_scope_reports_a_live_debug_variable() {
    use miden_assembly_syntax::{
        Parse,
        ast::{Instruction, Op},
        debuginfo::{SourceSpan, Span},
    };

    let source_manager = Arc::new(DefaultSourceManager::default());
    let mut module =
        Parse::parse("begin push.5 nop push.1 add add end", false, source_manager.clone()).unwrap();
    let entrypoint = module.procedures_mut().find(|procedure| procedure.is_entrypoint()).unwrap();
    for operation in entrypoint.body_mut().iter_mut() {
        if let Op::Inst(instruction) = operation
            && matches!(instruction.inner(), Instruction::Nop)
        {
            *instruction = Span::new(
                SourceSpan::default(),
                Instruction::DebugVar(DebugVarInfo::new("answer", DebugVarLocation::Stack(0))),
            );
        }
    }

    let mut session = Session::start_module(*module, source_manager);
    session.handshake("launch");
    let mut variable = None;
    for _ in 0..20 {
        assert_eq!(
            session.request("stepIn", json!({"threadId": 1, "granularity": "instruction"}))
                ["success"],
            true
        );
        let (event, _) = session.stop();
        assert_eq!(event["event"], "stopped");
        let response = session.request("variables", json!({"variablesReference": SCOPE_LOCALS}));
        variable = response["body"]["variables"]
            .as_array()
            .unwrap()
            .iter()
            .find(|variable| variable["name"] == "answer")
            .cloned();
        if variable.is_some() {
            break;
        }
    }
    let variable = variable.expect("live DAP local was not reported");
    assert_eq!(variable["value"], "5");
    assert_eq!(variable["type"], "Felt");
    session.disconnect();
}

#[test]
fn dap_phase_two_restart_sets_and_resets_the_restart_flag() {
    let config = DapConfig::new("127.0.0.1:0");
    let handle = config.clone();
    let mut session = Session::start("begin push.1 end", config);
    session.handshake("launch");
    assert_eq!(session.request("restart", json!({"arguments": {}}))["success"], true);
    let (event, _) = session.stop();
    assert_eq!(event["event"], "terminated");
    assert_eq!(event["body"]["restart"], true);
    session.join().unwrap();
    assert!(handle.restart_requested());
    handle.reset_restart();
    assert!(!handle.restart_requested());
}

#[test]
fn dap_reports_execution_failure_and_writes_the_failed_snapshot() {
    let directory = tempfile::tempdir().unwrap();
    let path = directory.path().join("failed.bin");
    let mut config = DapConfig::new("127.0.0.1:0");
    config.record_snapshot(path.clone());
    let mut session = Session::start("begin push.0 assert end", config);
    session.handshake("launch");
    assert_eq!(session.request("continue", json!({"threadId": 1}))["success"], true);
    assert_eq!(session.stop().0["event"], "terminated");
    assert!(session.join().is_err());
    assert!(crate::exec::ReplaySnapshot::read_from_file(&path).is_ok());
}