Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
278 changes: 189 additions & 89 deletions Cargo.lock

Large diffs are not rendered by default.

17 changes: 16 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ name = "miden-debug"
path = "src/main.rs"
test = false
bench = false
required-features = ["tui"]
required-features = ["std"]

[[example]]
name = "compile-masm"
Expand All @@ -34,6 +34,7 @@ required-features = ["std"]
[features]
default = ["tui", "dap"]
tui = ["std", "dep:crossterm", "dep:env_logger", "dep:ratatui", "dep:tui-input", "dep:signal-hook", "dep:syntect", "miden-debug-engine/tui"]
repl = ["std", "dep:env_logger", "dep:rustyline", "miden-debug-engine/tui"]
dap = ["dep:dap", "dep:socket2", "miden-debug-engine/dap"]
std = ["dep:glob", "clap/std", "clap/env", "miden-assembly-syntax/std", "miden-debug-engine/std"]
proptest = ["dep:proptest", "miden-debug-engine/proptest"]
Expand Down Expand Up @@ -80,6 +81,7 @@ syntect = { version = "5.2.0", optional = true, default-features = false, featur
thiserror = { package = "miden-thiserror", version = "1.0" }
toml = { version = "0.8", features = ["preserve_order"] }
tui-input = { version = "0.11", optional = true }
rustyline = { version = "15.0", optional = true }

socket2 = { version = "0.5", optional = true }
tokio = { version = "1.39.2", features = ["rt", "time", "macros", "rt-multi-thread"] }
Expand All @@ -89,3 +91,16 @@ proptest = { version = "1.4", optional = true }

miden-crypto = { version = "=0.23.0", default-features = false }

[patch.crates-io]
miden-assembly = { git = "https://github.com/walnuthq/miden-vm", branch = "fix/debug-var-dedup-crash" }
miden-assembly-syntax = { git = "https://github.com/walnuthq/miden-vm", branch = "fix/debug-var-dedup-crash" }
miden-core = { git = "https://github.com/walnuthq/miden-vm", branch = "fix/debug-var-dedup-crash" }
miden-core-lib = { git = "https://github.com/walnuthq/miden-vm", branch = "fix/debug-var-dedup-crash" }
miden-debug-types = { git = "https://github.com/walnuthq/miden-vm", branch = "fix/debug-var-dedup-crash" }
miden-mast-package = { git = "https://github.com/walnuthq/miden-vm", branch = "fix/debug-var-dedup-crash" }
miden-package-registry = { git = "https://github.com/walnuthq/miden-vm", branch = "fix/debug-var-dedup-crash" }
miden-processor = { git = "https://github.com/walnuthq/miden-vm", branch = "fix/debug-var-dedup-crash" }
miden-project = { git = "https://github.com/walnuthq/miden-vm", branch = "fix/debug-var-dedup-crash" }
miden-utils-diagnostics = { git = "https://github.com/walnuthq/miden-vm", branch = "fix/debug-var-dedup-crash" }
miden-utils-indexing = { git = "https://github.com/walnuthq/miden-vm", branch = "fix/debug-var-dedup-crash" }
miden-utils-sync = { git = "https://github.com/walnuthq/miden-vm", branch = "fix/debug-var-dedup-crash" }
11 changes: 10 additions & 1 deletion crates/engine/src/debug/breakpoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ pub enum BreakpointType {
StepTo(usize),
/// Break at the first cycle of the next instruction
Next,
/// Break at the next source line, or the next instruction if no source location is available.
NextLine,
/// Break when we exit the current call frame
Finish,
/// Break when any cycle corresponds to a source location whose file matches PATTERN
Expand Down Expand Up @@ -100,14 +102,21 @@ impl BreakpointType {

/// Returns true if this breakpoint is internal to the debugger (i.e. not creatable via :b)
pub fn is_internal(&self) -> bool {
matches!(self, BreakpointType::Next | BreakpointType::Step | BreakpointType::Finish)
matches!(
self,
BreakpointType::Next
| BreakpointType::NextLine
| BreakpointType::Step
| BreakpointType::Finish
)
}

/// Returns true if this breakpoint is removed upon being hit
pub fn is_one_shot(&self) -> bool {
matches!(
self,
BreakpointType::Next
| BreakpointType::NextLine
| BreakpointType::Finish
| BreakpointType::Step
| BreakpointType::StepN(_)
Expand Down
2 changes: 2 additions & 0 deletions crates/engine/src/debug/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod breakpoint;
mod memory;
mod native_ptr;
mod stacktrace;
mod variables;

pub use self::{
breakpoint::{Breakpoint, BreakpointType},
Expand All @@ -11,4 +12,5 @@ pub use self::{
CallFrame, CallStack, ControlFlowOp, CurrentFrame, OpDetail, ResolvedLocation, StackTrace,
StepInfo,
},
variables::{DebugVarSnapshot, DebugVarTracker, resolve_variable_value},
};
187 changes: 187 additions & 0 deletions crates/engine/src/debug/variables.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
use std::{cell::RefCell, collections::BTreeMap, rc::Rc};

use miden_core::{
Felt,
operations::{DebugVarInfo, DebugVarLocation},
};
use miden_processor::trace::RowIndex;

const FRAME_BASE_LOCAL_MARKER: u32 = 1 << 31;

fn decode_frame_base_local_offset(encoded: u32) -> Option<i16> {
if encoded & FRAME_BASE_LOCAL_MARKER == 0 {
return None;
}

let low_bits = (encoded & 0xffff) as u16;
Some(i16::from_le_bytes(low_bits.to_le_bytes()))
}

/// A snapshot of a debug variable at a specific clock cycle.
#[derive(Debug, Clone)]
pub struct DebugVarSnapshot {
/// The clock cycle when this variable info was recorded.
pub clk: RowIndex,
/// The debug variable information.
pub info: DebugVarInfo,
}

/// Tracks debug variable snapshots, mapping variable names to their most recent location info.
pub struct DebugVarTracker {
/// All debug variable events recorded during execution, keyed by clock cycle.
events: Rc<RefCell<BTreeMap<RowIndex, Vec<DebugVarInfo>>>>,
/// Current view of variables - maps variable name to most recent info.
current_vars: BTreeMap<String, DebugVarSnapshot>,
/// The clock cycle up to which we've processed events.
processed_up_to: RowIndex,
}

impl DebugVarTracker {
/// Create a new tracker using the given shared event store.
pub fn new(events: Rc<RefCell<BTreeMap<RowIndex, Vec<DebugVarInfo>>>>) -> Self {
Self {
events,
current_vars: BTreeMap::new(),
processed_up_to: RowIndex::from(0),
}
}

/// Record debug variable events at the given clock cycle.
pub fn record_events(&self, clk: RowIndex, infos: Vec<DebugVarInfo>) {
if !infos.is_empty() {
self.events.borrow_mut().entry(clk).or_default().extend(infos);
}
}

/// Process all events up to and including `clk`, updating current variable state.
pub fn update_to_cycle(&mut self, clk: RowIndex) {
let events = self.events.borrow();

// Process events from processed_up_to to clk
for (event_clk, var_infos) in events.range(self.processed_up_to..=clk) {
for info in var_infos {
let snapshot = DebugVarSnapshot {
clk: *event_clk,
info: info.clone(),
};
self.current_vars.insert(info.name().to_string(), snapshot);
}
}

self.processed_up_to = clk;
}

/// Reset the tracker to the beginning of execution.
pub fn reset(&mut self) {
self.current_vars.clear();
self.processed_up_to = RowIndex::from(0);
}

/// Get all currently visible variables.
pub fn current_variables(&self) -> impl Iterator<Item = &DebugVarSnapshot> {
self.current_vars.values()
}

/// Get a specific variable by name.
pub fn get_variable(&self, name: &str) -> Option<&DebugVarSnapshot> {
self.current_vars.get(name)
}

/// Get the number of tracked variables.
pub fn variable_count(&self) -> usize {
self.current_vars.len()
}

/// Check if there are any tracked variables.
pub fn has_variables(&self) -> bool {
!self.current_vars.is_empty()
}
}

/// Resolve a debug variable's value given its location and the current VM state.
pub fn resolve_variable_value(
location: &DebugVarLocation,
stack: &[Felt],
get_memory: impl Fn(u32) -> Option<Felt>,
get_local: impl Fn(i16) -> Option<Felt>,
) -> Option<Felt> {
match location {
DebugVarLocation::Stack(pos) => stack.get(*pos as usize).copied(),
DebugVarLocation::Memory(addr) => get_memory(*addr),
DebugVarLocation::Const(felt) => Some(*felt),
DebugVarLocation::Local(offset) => get_local(*offset),
DebugVarLocation::FrameBase {
global_index,
byte_offset,
} => {
if let Some(local_offset) = decode_frame_base_local_offset(*global_index) {
let base = get_local(local_offset)?;
let byte_addr = base.as_canonical_u64() as i64 + byte_offset;
let elem_addr = u32::try_from(byte_addr / 4).ok()?;
return get_memory(elem_addr);
}

// global_index was resolved to a Miden byte address during compilation.
// Convert to element address (÷4) to read the stack pointer value.
let sp_elem_addr = *global_index / 4;
let base = get_memory(sp_elem_addr)?;
// The stack pointer value is also a byte address; apply byte_offset,
// then convert to element address to read the variable's value.
let byte_addr = base.as_canonical_u64() as i64 + byte_offset;
let elem_addr = (byte_addr / 4) as u32;
get_memory(elem_addr)
}
DebugVarLocation::Expression(_) => None,
}
}

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

#[test]
fn test_tracker_basic() {
let events: Rc<RefCell<BTreeMap<RowIndex, Vec<DebugVarInfo>>>> =
Rc::new(Default::default());

// Add some events
{
let mut events_mut = events.borrow_mut();
events_mut.insert(
RowIndex::from(1),
vec![DebugVarInfo::new("x", DebugVarLocation::Stack(0))],
);
events_mut.insert(
RowIndex::from(5),
vec![DebugVarInfo::new("y", DebugVarLocation::Stack(1))],
);
}

let mut tracker = DebugVarTracker::new(events);

// Initially no variables
assert_eq!(tracker.variable_count(), 0);

// Process up to cycle 3
tracker.update_to_cycle(RowIndex::from(3));
assert_eq!(tracker.variable_count(), 1);
assert!(tracker.get_variable("x").is_some());
assert!(tracker.get_variable("y").is_none());

// Process up to cycle 10
tracker.update_to_cycle(RowIndex::from(10));
assert_eq!(tracker.variable_count(), 2);
assert!(tracker.get_variable("x").is_some());
assert!(tracker.get_variable("y").is_some());

// Verify resolve_variable_value resolves stack values
let x_snapshot = tracker.get_variable("x").unwrap();
let value = resolve_variable_value(
x_snapshot.info.value_location(),
&[Felt::new(42)],
|_| None,
|_| None,
);
assert_eq!(value, Some(Felt::new(42)));
}
}
29 changes: 26 additions & 3 deletions crates/engine/src/exec/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ use std::{
};

use miden_assembly_syntax::{Library, diagnostics::Report};
use miden_core::Word;
use miden_core::program::{Program, StackInputs};
use miden_core::{
Word,
operations::DebugVarInfo,
program::{Program, StackInputs},
};
use miden_debug_types::{SourceManager, SourceManagerExt};
use miden_mast_package::Dependency;
use miden_processor::{
Expand All @@ -21,7 +24,10 @@ use miden_processor::{
};

use super::{DebugExecutor, DebuggerHost, ExecutionConfig, ExecutionTrace, TraceEvent};
use crate::{debug::CallStack, felt::FromMidenRepr};
use crate::{
debug::{CallStack, DebugVarTracker},
felt::FromMidenRepr,
};

/// The [Executor] is responsible for executing a program with the Miden VM.
///
Expand Down Expand Up @@ -163,6 +169,12 @@ impl Executor {
assertion_events.borrow_mut().insert(clk, event);
});

// Set up debug variable tracking
// Note: Currently no debug var events are emitted (requires new miden-core),
// but we set up the infrastructure for when they become available.
let debug_var_events: Rc<RefCell<BTreeMap<RowIndex, Vec<DebugVarInfo>>>> =
Rc::new(Default::default());

let mut processor = FastProcessor::new(self.stack)
.with_advice(self.advice)
.with_options(self.options)
Expand All @@ -175,6 +187,7 @@ impl Executor {
.expect("failed to get initial resume context");

let callstack = CallStack::new(trace_events);
let debug_vars = DebugVarTracker::new(debug_var_events);
DebugExecutor {
processor,
host,
Expand All @@ -187,6 +200,9 @@ impl Executor {
root_context,
current_context: root_context,
callstack,
current_proc: None,
debug_vars,
last_debug_var_count: 0,
recent: VecDeque::with_capacity(5),
cycle: 0,
stopped: false,
Expand Down Expand Up @@ -219,6 +235,9 @@ impl Executor {
}
host.set_event_replay(event_replay);

let debug_var_events: Rc<RefCell<BTreeMap<RowIndex, Vec<DebugVarInfo>>>> =
Rc::new(Default::default());

let trace_events: Rc<RefCell<BTreeMap<RowIndex, TraceEvent>>> = Rc::new(Default::default());
let frame_start_events = Rc::clone(&trace_events);
host.register_trace_handler(TraceEvent::FrameStart, move |clk, event| {
Expand All @@ -245,6 +264,7 @@ impl Executor {
.expect("failed to get initial resume context");

let callstack = CallStack::new(trace_events);
let debug_vars = DebugVarTracker::new(debug_var_events);
DebugExecutor {
processor,
host,
Expand All @@ -257,6 +277,9 @@ impl Executor {
root_context,
current_context: root_context,
callstack,
current_proc: None,
debug_vars,
last_debug_var_count: 0,
recent: VecDeque::with_capacity(5),
cycle: 0,
stopped: false,
Expand Down
Loading