miden-debug-engine 0.19.0

Core debugger engine for miden-debug
Documentation
use miden_assembly::{
    Assembler, DefaultSourceManager, Linkage,
    ast::{DebugInlineCallInfo, Instruction, Module, ModuleKind, Op},
};
use miden_debug_types::Span;
use miden_mast_package::Package;

use super::*;

fn run(
    package: Arc<Package>,
    source_manager: Arc<DefaultSourceManager>,
    libraries: &[Arc<Package>],
    mut check: impl FnMut(&AssemblyOp, &[LogicalStackFrame]),
) {
    let mut executor = crate::Executor::new(Vec::new());
    for library in libraries {
        executor.with_package(library.clone()).unwrap();
    }
    let mut executor = executor.into_debug(package, source_manager);
    while !executor.stopped {
        executor.step().unwrap();
        if !executor.stopped
            && let Some(asmop) = executor.current_asmop.as_ref()
        {
            check(asmop, &executor.callstack.logical_frames(""));
        }
    }
}

#[test]
fn repeated_calls_at_one_site_have_distinct_invocation_identities() {
    for invocation in ["call.inner", "exec.inner"] {
        let source_manager = Arc::new(DefaultSourceManager::default());
        let source = format!(
            "proc inner push.7 drop end begin push.2 dup neq.0 while.true {invocation} sub.1 dup \
             neq.0 end drop end"
        );
        let package = Arc::from(
            Assembler::new(source_manager.clone())
                .assemble_program("program", source)
                .unwrap(),
        );
        let mut executor = crate::Executor::new(Vec::new()).into_debug(package, source_manager);
        let mut activations = Vec::new();
        while !executor.stopped {
            executor.step().unwrap();
            if executor.stopped
                || executor.current_op != Some(Operation::Push(miden_core::Felt::from(7u32)))
            {
                continue;
            }
            let frames = executor.callstack.logical_frames("");
            let current = frames.last().unwrap();
            assert!(current.name().ends_with("::inner"));
            activations.push(current.invocation_id().expect("physical invocation"));
        }
        assert_eq!(activations.len(), 2, "{invocation}");
        assert_ne!(activations[0], activations[1], "{invocation}");
    }
}

#[test]
fn caller_source_inline_frames_survive_calls_and_clear_on_return() {
    for invocation in ["call.target", "dynexec"] {
        let source_manager = Arc::new(DefaultSourceManager::default());
        let setup = if invocation == "dynexec" {
            "procref.target mem_storew_le.100 dropw push.100"
        } else {
            ""
        };
        let source = format!(
            "proc target push.7 drop end begin {setup} nop nop push.1 drop {invocation} nop \
             push.9 drop end"
        );
        let mut module = Module::parser(Some(ModuleKind::Executable))
            .parse_str(None, source, source_manager.clone())
            .unwrap();
        let entrypoint = module.procedures_mut().find(|proc| proc.is_entrypoint()).unwrap();
        let mut markers = [None, Some("source::caller"), None].into_iter();
        let mut call_site = None;
        for op in entrypoint.body_mut().iter_mut() {
            let Op::Inst(instruction) = op else { continue };
            if !matches!(instruction.inner(), Instruction::Nop) {
                continue;
            }
            let span = instruction.span();
            let replacement = match markers.next().unwrap() {
                Some(name) => {
                    call_site = Some(source_manager.location(span).unwrap());
                    let location = source_manager.file_line_col(span).unwrap();
                    Instruction::DebugInlineCall(DebugInlineCallInfo::new(
                        name,
                        location.clone(),
                        location,
                    ))
                }
                None => Instruction::DebugInlineCallClear,
            };
            *op = Op::Inst(Span::new(span, replacement));
        }
        assert!(markers.next().is_none());
        let package = Arc::from(
            Assembler::new(source_manager.clone())
                .assemble_program("program", module)
                .unwrap(),
        );
        let call_site = call_site.unwrap();
        let mut suspended_locations = None;
        let mut suspended_frames = None;
        let mut target_cycles = 0;
        let mut returned = false;
        run(package, source_manager, &[], |asmop, frames| {
            let names = frames.iter().map(LogicalStackFrame::name).collect::<Vec<_>>();
            if names == ["$exec::$main", "source::caller"] {
                let location = frames[0].assembly_location().unwrap();
                assert_eq!(location.uri, call_site.uri);
                assert_eq!(location.start, call_site.start);
                assert_eq!(frames[1].kind(), LogicalFrameKind::Inline);
                suspended_locations = Some(
                    frames
                        .iter()
                        .map(|frame| frame.assembly_location().cloned())
                        .collect::<Vec<_>>(),
                );
                suspended_frames = Some(frames.to_vec());
            }
            if asmop.context_name().ends_with("::target") {
                assert_eq!(names, ["$exec::$main", "source::caller", "$exec::target"]);
                assert_eq!(
                    frames[..2]
                        .iter()
                        .map(|frame| frame.assembly_location().cloned())
                        .collect::<Vec<_>>(),
                    *suspended_locations.as_ref().expect("caller must be observed before target"),
                    "suspended caller locations must survive {invocation}"
                );
                assert_eq!(frames[2].assembly_location(), asmop.location());
                for (caller, suspended) in frames.iter().zip(suspended_frames.as_ref().unwrap()) {
                    assert!(caller.is_same_source_frame(suspended));
                }
                for frame in frames {
                    let info = frame.debug_info().expect("recovered frame retains its package");
                    let function = frame.function().expect("recovered frame retains its function");
                    assert_eq!(info[function.name_idx].trim_start_matches("::"), frame.name());
                }
                target_cycles += 1;
            }
            if asmop.op().as_ref() == "push.9" {
                assert_eq!(names, ["$exec::$main"], "returned frames must be removed");
                assert_eq!(frames[0].assembly_location(), asmop.location());
                returned = true;
            }
        });
        assert!(target_cycles >= 2, "target must span multiple operations: {invocation}");
        assert!(returned, "caller must resume after {invocation}");
    }
}

#[test]
fn external_exec_has_one_loaded_target_and_restores_the_caller() {
    let source_manager = Arc::new(DefaultSourceManager::default());
    let library: Arc<Package> = Arc::from(
        Assembler::new(source_manager.clone())
            .assemble_library(
                "dep",
                "namespace dep::math pub proc target push.7 drop end",
                None::<String>,
            )
            .unwrap(),
    );
    let package = Arc::from(
        Assembler::new(source_manager.clone())
            .with_package(library.clone(), Linkage::Dynamic)
            .unwrap()
            .assemble_program("program", "use dep::math begin exec.math::target push.9 drop end")
            .unwrap(),
    );
    let mut target_cycles = 0;
    let mut returned = false;
    run(package, source_manager, &[library], |asmop, frames| {
        let names = frames.iter().map(LogicalStackFrame::name).collect::<Vec<_>>();
        if asmop.context_name().ends_with("::target") {
            assert_eq!(names, ["$exec::$main", "dep::math::target"]);
            assert_eq!(frames[1].kind(), LogicalFrameKind::Physical);
            assert_eq!(frames[1].assembly_location(), asmop.location());
            assert_eq!(
                frames[0].assembly_location().unwrap().start.to_usize(),
                20,
                "caller must point to exec.math::target"
            );
            target_cycles += 1;
        }
        if asmop.op().as_ref() == "push.9" {
            assert_eq!(names, ["$exec::$main"]);
            assert_eq!(frames[0].assembly_location(), asmop.location());
            returned = true;
        }
    });
    assert!(target_cycles >= 2);
    assert!(returned);
}

#[test]
fn changing_source_inline_depth_stays_inside_the_callee() {
    let source_manager = Arc::new(DefaultSourceManager::default());
    let mut module = Module::parser(Some(ModuleKind::Executable))
        .parse_str(
            None,
            "proc inner push.7 drop nop push.6 drop nop push.5 drop end proc outer exec.inner \
             push.8 drop end begin exec.outer push.9 drop end",
            source_manager.clone(),
        )
        .unwrap();
    let mut entering = true;
    for procedure in module.procedures_mut() {
        for op in procedure.body_mut().iter_mut() {
            let Op::Inst(instruction) = op else { continue };
            if !matches!(instruction.inner(), Instruction::Nop) {
                continue;
            }
            let span = instruction.span();
            let replacement = if entering {
                let location = source_manager.file_line_col(span).unwrap();
                Instruction::DebugInlineCall(DebugInlineCallInfo::new(
                    "source::inline",
                    location.clone(),
                    location,
                ))
            } else {
                Instruction::DebugInlineCallClear
            };
            *op = Op::Inst(Span::new(span, replacement));
            entering = false;
        }
    }
    let package = Arc::from(
        Assembler::new(source_manager.clone())
            .assemble_program("program", module)
            .unwrap(),
    );
    let mut phases = [false; 3];
    run(package, source_manager, &[], |asmop, frames| {
        let phase = match asmop.op().as_ref() {
            "push.7" => 0,
            "push.6" => 1,
            "push.5" => 2,
            _ => return,
        };
        let names = frames.iter().map(LogicalStackFrame::name).collect::<Vec<_>>();
        let expected = if phase == 1 {
            vec!["$exec::$main", "$exec::outer", "$exec::inner", "source::inline"]
        } else {
            vec!["$exec::$main", "$exec::outer", "$exec::inner"]
        };
        assert_eq!(names, expected);
        assert_eq!(frames.last().unwrap().assembly_location(), asmop.location());
        phases[phase] = true;
    });
    assert_eq!(phases, [true; 3]);
}