miden-debug 0.17.0

An interactive debugger for Miden VM programs
Documentation
use super::*;

#[test]
fn memory_inspection_decodes_integer_widths_formats_and_reports_invalid_reads() {
    let mut state = state_with_variables(
        "begin push.4294967295 mem_store.0 push.1 mem_store.1 push.2 mem_store.2 push.3 \
         mem_store.3 end",
    );
    state.run_until_stopped();
    for (expression, expected) in [
        ("0 -t i1", "true"),
        ("0 -t i1 -f hex", "0x1"),
        ("0 -t i1 -f binary", "0b1"),
        ("0 -t i8", "-1"),
        ("0 -t u8", "255"),
        ("0 -t i16", "-1"),
        ("0 -t u16", "65535"),
        ("0 -t i32", "-1"),
        ("0 -t u32", "4294967295"),
        ("0 -t i64", "8589934591"),
        ("0 -t u64", "8589934591"),
        ("0 -t felt", "4294967295"),
        ("0 -t word", "[4294967295, 1, 2, 3]"),
        ("0 -t u8 -f hex", "0xff"),
        ("0 -t u8 -f binary", "0b11111111"),
        ("100 -t u32", "0"),
    ] {
        assert_eq!(
            state.read_memory(&expression.parse().unwrap()).unwrap(),
            expected,
            "{expression}"
        );
    }
    for (expression, expected) in [
        ("0 -t u32 -c 2", "-count"),
        ("1 -t felt -m byte", "element boundary"),
        ("1 -t word", "word boundary"),
        ("1 -t u16 -m byte", "unaligned reads"),
        ("0 -t i128", "not implemented"),
        ("4294967295 -t u64", "beyond end"),
    ] {
        assert!(
            state.read_memory(&expression.parse().unwrap()).unwrap_err().contains(expected),
            "{expression}"
        );
    }
    assert_eq!(state.selected_stack_frame(), 0);
    state.select_older_stack_frame();
    state.select_newer_stack_frame();
    assert_eq!(state.selected_stack_frame(), 0);
    assert!(state.execution_failed().is_none());
}

#[test]
fn source_candidates_keep_absolute_paths_and_apply_only_declared_prefixes() {
    assert_eq!(
        source_path_candidates("src/lib.rs", &["/workspace".into()]),
        vec![PathBuf::from("/workspace/src/lib.rs")]
    );
    assert!(source_path_candidates("/workspace/src/lib.rs", &["/workspace".into()]).is_empty());
    assert!(source_paths_match(
        "file:///workspace/src/lib.rs",
        "src/lib.rs",
        &["/workspace".into()]
    ));
    assert!(!source_paths_match(
        "/workspace2/src/lib.rs",
        "src/lib.rs",
        &["/workspace".into()]
    ));
    assert_eq!(
        strip_source_prefix("/workspace/src/lib.rs", "/workspace/"),
        Some("src/lib.rs".into())
    );
    assert!(is_compiler_generated_name("local12"));
    assert!(!is_compiler_generated_name("local_count"));
}

fn state_with_entry_variables(source: &str) -> State {
    use miden_assembly_syntax::{
        Parse,
        ast::{Block, DebugVarInfo, DebugVarLocation, Instruction, Op},
        debuginfo::Span,
    };

    fn inject_variables(block: &mut Block) {
        for operation in block.iter_mut() {
            if let Op::If {
                then_blk, else_blk, ..
            } = operation
            {
                inject_variables(then_blk);
                inject_variables(else_blk);
            }
            let Op::Inst(instruction) = operation else {
                continue;
            };
            let location = match instruction.inner() {
                Instruction::Nop => DebugVarLocation::Stack(0),
                Instruction::Not => DebugVarLocation::Unavailable,
                _ => continue,
            };
            *instruction = Span::new(
                SourceSpan::default(),
                Instruction::DebugVar(DebugVarInfo::new("n", location)),
            );
        }
    }
    let source_manager = Arc::new(DefaultSourceManager::default());
    let mut module = Parse::parse(source, false, source_manager.clone()).unwrap();
    for procedure in module.procedures_mut() {
        inject_variables(procedure.body_mut());
    }
    let package = miden_assembly::Assembler::new(source_manager.clone())
        .assemble_program("program", module)
        .unwrap();
    let executor = Executor::new(Vec::new()).into_debug(package.into(), source_manager.clone());
    let mut state = State::new_local(
        source_manager,
        Box::<DebuggerConfig>::default(),
        DebugMode::Program,
        LocalState {
            executor,
            execution_failed: None,
            typed_procedure: None,
        },
    );
    state.create_breakpoint("in *entrypoint".parse().unwrap());
    state
}

fn state_with_variables(source: &str) -> State {
    use miden_assembly_syntax::{
        Parse,
        ast::{DebugVarInfo, DebugVarLocation},
    };
    use miden_processor::trace::RowIndex;

    let source_manager = Arc::new(DefaultSourceManager::default());
    let module = Parse::parse(source, false, source_manager.clone()).unwrap();
    let package = miden_assembly::Assembler::new(source_manager.clone())
        .assemble_program("program", module)
        .unwrap();
    let executor = Executor::new(Vec::new()).into_debug(package.into(), source_manager.clone());
    let mut state = State::new_local(
        source_manager,
        Box::<DebuggerConfig>::default(),
        DebugMode::Program,
        LocalState {
            executor,
            execution_failed: None,
            typed_procedure: None,
        },
    );

    let tracker = &mut state.executor_mut().debug_vars;
    tracker.record_events_with_stack(
        RowIndex::from(0),
        vec![
            DebugVarInfo::new("answer", DebugVarLocation::Stack(0)),
            DebugVarInfo::new("local0", DebugVarLocation::Const(Felt::from(9u32))),
        ],
        &[Felt::from(7u32)],
    );
    tracker.update_to_cycle(RowIndex::from(0));
    state
}

#[test]
fn completed_execution_has_no_live_variables() {
    let mut state = state_with_variables("begin push.1 drop end");

    assert_eq!(state.format_variables(false), "answer=7");
    assert_eq!(state.format_variables(true), "answer=7, local0=9");

    state.run_until_stopped();

    assert!(state.executor().stopped);
    assert!(state.execution_failed().is_none());
    assert!(state.executor().debug_vars.has_variables());
    for show_all in [false, true] {
        assert!(state.current_variables(show_all).is_empty());
        assert_eq!(state.format_variables(show_all), "Program has terminated; no live variables");
    }
}

#[test]
fn failed_execution_preserves_variables_for_inspection() {
    let mut state = state_with_variables("begin push.0 assert end");

    state.run_until_stopped();

    assert!(state.executor().stopped);
    assert!(state.execution_failed().is_some());
    assert_eq!(state.current_variables(false).len(), 1);
    assert_eq!(state.current_variables(true).len(), 2);
    assert_eq!(state.format_variables(false), "answer=7");
    assert_eq!(state.format_variables(true), "answer=7, local0=9");
}

#[test]
fn function_breakpoint_waits_for_entry_variables() {
    let mut state = state_with_entry_variables(
        "proc entrypoint push.2 push.3 add nop drop push.1 if.true push.1 drop end end begin \
         exec.entrypoint end",
    );
    state.run_until_stopped();
    assert!(!state.executor().stopped);
    assert_eq!(state.breakpoints_hit.len(), 1);
    assert_eq!(state.format_variables(true), "n=5");
}

#[test]
fn function_breakpoint_does_not_wait_for_missing_variables() {
    let mut state = state_with_entry_variables(
        "proc entrypoint push.2 push.3 add drop end begin exec.entrypoint end",
    );
    state.run_until_stopped();
    assert!(!state.executor().stopped);
    assert_eq!(state.breakpoints_hit.len(), 1);
    assert!(state.current_variables(true).is_empty());
}

#[test]
fn function_breakpoint_accepts_variables_without_resolved_source() {
    let mut state = state_with_entry_variables(
        "proc entrypoint push.2 push.3 add nop drop end begin exec.entrypoint end",
    );
    state.source_manager = Arc::new(DefaultSourceManager::default());
    state.run_until_stopped();
    assert!(!state.executor().stopped);
    assert_eq!(state.breakpoints_hit.len(), 1);
    assert!(state.current_display_location().is_none());
    assert_eq!(state.format_variables(true), "n=5");
}

#[test]
fn function_breakpoint_ignores_caller_variables_and_kills() {
    let mut state = state_with_entry_variables(
        "proc entrypoint push.2 not push.3 add nop drop end begin push.91 nop drop \
         exec.entrypoint end",
    );
    state.run_until_stopped();
    assert!(!state.executor().stopped);
    assert_eq!(state.breakpoints_hit.len(), 1);
    assert_eq!(state.format_variables(true), "n=5");
}

#[test]
fn function_breakpoint_does_not_enter_branches_to_find_variables() {
    let mut state = state_with_entry_variables(
        "proc entrypoint push.0 if.true push.5 nop drop end push.1 drop end begin exec.entrypoint \
         end",
    );
    state.run_until_stopped();
    assert!(!state.executor().stopped);
    assert_eq!(state.breakpoints_hit.len(), 1);
    assert!(state.current_variables(true).is_empty());
    assert_eq!(state.executor().current_op, Some(miden_processor::operation::Operation::Pad));
}

#[test]
fn successful_reload_epilogue_resets_stack_selection() {
    let config = DebuggerConfig {
        input: Some(crate::program_loader::test_package_input()),
        ..Default::default()
    };
    let mut state = State::new(Box::new(config)).expect("state should build");
    state.selected_stack_frame = 3;
    state.breakpoints_hit.push(Breakpoint::default());
    state.stopped = false;
    state.executor_mut().stopped = true;

    state.finish_reload();

    assert!(!state.executor().stopped);
    assert_eq!(state.selected_stack_frame, 0);
    assert!(state.breakpoints_hit.is_empty());
    assert!(state.stopped);
}