From 22973b18ba6236c53c792c7cf6c5f42fbb6031c5 Mon Sep 17 00:00:00 2001 From: djole Date: Mon, 26 Jan 2026 11:09:31 +0100 Subject: [PATCH 1/3] feat: add vars command to display debug variables Implement debug variable tracking and display in the debugger: - Add DebugVarTracker to track variables throughout execution - Add DebugVarSnapshot for variable state at specific clock cycles - Add stub types for DebugVarInfo and DebugVarLocation (pending new miden-core) - Add resolve_variable_value (currently returns None, pending new miden-core) - Add :vars/:variables/:locals commands to display current variables The vars command shows variables in format "name=value" or "name=location" if the value cannot be resolved. Note: Full variable value resolution requires new version of miden-core. --- src/debug/mod.rs | 2 + src/debug/variables.rs | 214 +++++++++++++++++++++++++++++++++++++++++ src/exec/executor.rs | 10 +- src/exec/state.rs | 7 +- src/ui/pages/home.rs | 4 + src/ui/state.rs | 62 +++++++++++- 6 files changed, 296 insertions(+), 3 deletions(-) create mode 100644 src/debug/variables.rs diff --git a/src/debug/mod.rs b/src/debug/mod.rs index 302de0d..74794e8 100644 --- a/src/debug/mod.rs +++ b/src/debug/mod.rs @@ -2,10 +2,12 @@ mod breakpoint; mod memory; mod native_ptr; mod stacktrace; +mod variables; pub use self::{ breakpoint::{Breakpoint, BreakpointType}, memory::{FormatType, MemoryMode, ReadMemoryExpr}, native_ptr::NativePtr, stacktrace::{CallFrame, CallStack, CurrentFrame, OpDetail, ResolvedLocation, StackTrace}, + variables::{DebugVarInfo, DebugVarSnapshot, DebugVarTracker, resolve_variable_value}, }; diff --git a/src/debug/variables.rs b/src/debug/variables.rs new file mode 100644 index 0000000..1193687 --- /dev/null +++ b/src/debug/variables.rs @@ -0,0 +1,214 @@ +use std::{ + cell::RefCell, + collections::BTreeMap, + fmt, + rc::Rc, +}; + +use miden_core::Felt; +use miden_processor::RowIndex; + +/// Location of a debug variable's value. +/// +/// This is a stub type until miden-core provides DebugVarLocation. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DebugVarLocation { + /// Variable is on the stack at the given position + Stack(u16), + /// Variable is in memory at the given address + Memory(u32), + /// Variable is a constant + Const(Felt), + /// Variable is a local at the given frame offset + Local(u16), + /// Variable location is computed via an expression + Expression(Vec), +} + +impl fmt::Display for DebugVarLocation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Stack(pos) => write!(f, "stack[{pos}]"), + Self::Memory(addr) => write!(f, "mem[{addr}]"), + Self::Const(felt) => write!(f, "const({})", felt.as_int()), + Self::Local(idx) => write!(f, "local[{idx}]"), + Self::Expression(_) => write!(f, "expr(...)"), + } + } +} + +/// Debug variable information. +/// +/// This is a stub type until miden-core provides DebugVarInfo. +#[derive(Debug, Clone)] +pub struct DebugVarInfo { + name: String, + location: DebugVarLocation, +} + +impl DebugVarInfo { + /// Create a new debug variable info + pub fn new(name: impl Into, location: DebugVarLocation) -> Self { + Self { + name: name.into(), + location, + } + } + + /// Get the variable name + pub fn name(&self) -> &str { + &self.name + } + + /// Get the variable's value location + pub fn value_location(&self) -> &DebugVarLocation { + &self.location + } +} + +/// 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 information throughout program execution. +/// +/// This structure maintains a mapping from variable names to their most recent +/// location information at each clock cycle. It's designed to work with the +/// debugger to provide source-level variable inspection. +pub struct DebugVarTracker { + /// All debug variable events recorded during execution, keyed by clock cycle. + events: Rc>>>, + /// Current view of variables - maps variable name to most recent info. + current_vars: BTreeMap, + /// 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>>>) -> Self { + Self { + events, + current_vars: BTreeMap::new(), + processed_up_to: RowIndex::from(0), + } + } + + /// Update the tracker state to reflect variables at the given clock cycle. + /// + /// This processes all events up to and including `clk`, updating the + /// current variable state accordingly. + 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 { + 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. +/// +/// NOTE: This currently always returns None as it requires miden-core +/// for full debug variable support. +pub fn resolve_variable_value( + _location: &DebugVarLocation, + _stack: &[Felt], + _get_memory: impl Fn(u32) -> Option, + _get_local: impl Fn(u16) -> Option, +) -> Option { + // Variable value resolution requires miden-core + // For now, always return None + None +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_tracker_basic() { + let events: Rc>>> = + 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 returns None + let x_snapshot = tracker.get_variable("x").unwrap(); + let value = resolve_variable_value( + x_snapshot.info.value_location(), + &[Felt::new(42)], + |_| None, + |_| None, + ); + assert!(value.is_none(), "resolve_variable_value should return None for now"); + } +} diff --git a/src/exec/executor.rs b/src/exec/executor.rs index 2232d17..ed1c7b3 100644 --- a/src/exec/executor.rs +++ b/src/exec/executor.rs @@ -20,7 +20,7 @@ use miden_processor::{ }; use super::{DebugExecutor, DebuggerHost, ExecutionConfig, ExecutionTrace, TraceEvent}; -use crate::{debug::CallStack, felt::FromMidenRepr}; +use crate::{debug::{CallStack, DebugVarInfo, DebugVarTracker}, felt::FromMidenRepr}; /// The [Executor] is responsible for executing a program with the Miden VM. /// @@ -161,6 +161,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>>> = + Rc::new(Default::default()); + let mut process = Process::new(program.kernel().clone(), self.stack, self.advice, self.options); let process_state: ProcessState = (&mut process).into(); @@ -169,6 +175,7 @@ impl Executor { let stack_outputs = result.as_ref().map(|so| so.clone()).unwrap_or_default(); let iter = VmStateIterator::new(process, result); let callstack = CallStack::new(trace_events); + let debug_vars = DebugVarTracker::new(debug_var_events); DebugExecutor { iter, stack_outputs, @@ -176,6 +183,7 @@ impl Executor { root_context, current_context: root_context, callstack, + debug_vars, recent: VecDeque::with_capacity(5), last: None, cycle: 0, diff --git a/src/exec/state.rs b/src/exec/state.rs index 90ada25..149a465 100644 --- a/src/exec/state.rs +++ b/src/exec/state.rs @@ -10,7 +10,7 @@ use miden_processor::{ }; use super::ExecutionTrace; -use crate::debug::{CallFrame, CallStack}; +use crate::debug::{CallFrame, CallStack, DebugVarTracker}; /// A special version of [crate::Executor] which provides finer-grained control over execution, /// and captures a ton of information about the program being executed, so as to make it possible @@ -31,6 +31,8 @@ pub struct DebugExecutor { pub current_context: ContextId, /// The current call stack pub callstack: CallStack, + /// Debug variable tracker for source-level variable inspection + pub debug_vars: DebugVarTracker, /// A sliding window of the last 5 operations successfully executed by the VM pub recent: VecDeque, /// The most recent [VmState] produced by the [VmStateIterator] @@ -69,6 +71,9 @@ impl DebugExecutor { let exited = self.callstack.next(&state); + // Update debug variable tracker to current clock cycle + self.debug_vars.update_to_cycle(state.clk); + self.last = Some(state); Ok(exited) diff --git a/src/ui/pages/home.rs b/src/ui/pages/home.rs index ba4989c..1b68d41 100644 --- a/src/ui/pages/home.rs +++ b/src/ui/pages/home.rs @@ -148,6 +148,10 @@ impl Page for Home { "debug" => { actions.push(Some(Action::ShowDebug)); } + "vars" | "variables" | "locals" => { + let result = state.format_variables(); + actions.push(Some(Action::StatusLine(result))); + } invalid => { log::debug!("unknown command: '{invalid}'"); actions.push(Some(Action::TimedStatusLine("unknown command".into(), 1))) diff --git a/src/ui/state.rs b/src/ui/state.rs index 2215930..eb4243f 100644 --- a/src/ui/state.rs +++ b/src/ui/state.rs @@ -7,7 +7,7 @@ use miden_processor::{Felt, StackInputs}; use crate::{ config::DebuggerConfig, - debug::{Breakpoint, BreakpointType, ReadMemoryExpr}, + debug::{Breakpoint, BreakpointType, ReadMemoryExpr, resolve_variable_value}, exec::{DebugExecutor, ExecutionTrace, Executor}, input::InputFile, }; @@ -270,6 +270,66 @@ impl State { Ok(output) } + + /// Format the current debug variables as a string for display. + /// + /// Returns a string describing all tracked variables and their current values, + /// or a message indicating no variables are being tracked. + pub fn format_variables(&self) -> String { + use core::fmt::Write; + + let debug_vars = &self.executor.debug_vars; + + if !debug_vars.has_variables() { + return "No debug variables tracked".to_string(); + } + + let mut output = String::new(); + let stack: Vec = self + .executor + .last + .as_ref() + .map(|state| state.stack.clone()) + .unwrap_or_default(); + + let context = self.executor.current_context; + let cycle = miden_processor::RowIndex::from(self.executor.cycle); + + for var_snapshot in debug_vars.current_variables() { + if !output.is_empty() { + output.push_str(", "); + } + + let name = var_snapshot.info.name(); + let location = var_snapshot.info.value_location(); + + // Try to resolve the variable value + let value = resolve_variable_value( + location, + &stack, + |addr| { + self.execution_trace + .read_memory_element_in_context(addr, context, cycle) + }, + |_idx| { + // Local resolution would need FMP calculation + // For now, return None + None + }, + ); + + match value { + Some(felt) => { + write!(&mut output, "{name}={}", felt.as_int()).unwrap(); + } + None => { + write!(&mut output, "{name}={location}").unwrap(); + } + } + } + + output + } } fn load_package(config: &DebuggerConfig) -> Result, Report> { From a1304d3e361ae9688bd1a4b0ce738190b4636f22 Mon Sep 17 00:00:00 2001 From: djole Date: Mon, 26 Jan 2026 11:10:30 +0100 Subject: [PATCH 2/3] fix: resolve library dependencies before package dependencies Load link libraries and stdlib before calling with_dependencies() to ensure the dependency resolver has all required libraries registered. This fixes the "Dependency not found in resolver" panic when running programs that depend on the standard library. --- Cargo.lock | 26 +++++++++++++++++ Cargo.toml | 1 + src/ui/state.rs | 74 ++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 88 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 104714e..057a1f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -753,6 +753,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "fs-err" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf68cef89750956493a66a10f512b9e58d9db21f2a573c079c0bdf1207a54a7" +dependencies = [ + "autocfg", +] + [[package]] name = "futures" version = "0.3.31" @@ -1354,6 +1363,7 @@ dependencies = [ "miden-debug-types", "miden-mast-package", "miden-processor", + "miden-stdlib", "miden-thiserror", "proptest", "ratatui", @@ -1469,6 +1479,22 @@ dependencies = [ "winter-prover", ] +[[package]] +name = "miden-stdlib" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e90a5de45a1e6213ff17b66fff8accde0bbc64264e2c22bbcb9a895f8f3b767" +dependencies = [ + "env_logger", + "fs-err", + "miden-assembly", + "miden-core", + "miden-crypto", + "miden-processor", + "miden-utils-sync", + "thiserror", +] + [[package]] name = "miden-thiserror" version = "1.0.59" diff --git a/Cargo.toml b/Cargo.toml index 263cdc7..3b68357 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ miden-core = { version = "0.19", default-features = false } miden-debug-types = { version = "0.19", default-features = false } miden-mast-package = { version = "0.19", default-features = false } miden-processor = { version = "0.19", default-features = false } +miden-stdlib = { version = "0.19", default-features = false } ratatui = { version = "0.29.0", optional = true } rustc-demangle = { version = "0.1", features = ["std"] } serde = { version = "1.0", default-features = false, features = [ diff --git a/src/ui/state.rs b/src/ui/state.rs index eb4243f..52fce95 100644 --- a/src/ui/state.rs +++ b/src/ui/state.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use miden_assembly::{DefaultSourceManager, SourceManager}; -use miden_assembly_syntax::diagnostics::{IntoDiagnostic, Report}; +use miden_assembly_syntax::{Library, diagnostics::{IntoDiagnostic, Report}}; use miden_core::{FieldElement, utils::Deserializable}; use miden_processor::{Felt, StackInputs}; @@ -12,6 +12,12 @@ use crate::{ input::InputFile, }; +/// Get the standard library as an Arc +fn get_stdlib() -> Arc { + let stdlib = miden_stdlib::StdLibrary::default(); + Arc::new(stdlib.into()) +} + pub struct State { pub package: Arc, pub source_manager: Arc, @@ -46,21 +52,44 @@ impl State { let args = inputs.inputs.iter().copied().rev().collect::>(); let package = load_package(&config)?; - let mut executor = Executor::for_package(&package.clone(), args.clone())?; - executor.with_advice_inputs(inputs.advice_inputs.clone()); - let mut libs = Vec::with_capacity(config.link_libraries.len()); + // Load link libraries first, before creating the executor + // This is needed so they can be added to the dependency resolver + let mut libs = Vec::with_capacity(config.link_libraries.len() + 1); for link_library in config.link_libraries.iter() { log::debug!(target: "state", "loading link library {}", link_library.name()); let lib = link_library.load(&config, source_manager.clone())?; - libs.push(lib.clone()); - executor.with_library(lib); + libs.push(lib); + } + + // Load the standard library automatically + let stdlib = get_stdlib(); + libs.push(stdlib); + + // Create executor and add link libraries to the dependency resolver + // before resolving package dependencies + let mut executor = Executor::new(args.clone()); + for lib in libs.iter() { + let digest = *lib.digest(); + executor.dependency_resolver_mut().add(digest, lib.clone().into()); + } + + // Now resolve package dependencies (link libraries are already in the resolver) + executor.with_dependencies(package.manifest.dependencies())?; + executor.with_advice_inputs(inputs.advice_inputs.clone()); + for lib in libs.iter() { + executor.with_library(lib.clone()); } let program = package.unwrap_program(); let executor = executor.into_debug(&program, source_manager.clone()); // Execute the program until it terminates to capture a full trace for use during debugging - let mut trace_executor = Executor::for_package(&package, args)?; + let mut trace_executor = Executor::new(args); + for lib in libs.iter() { + let digest = *lib.digest(); + trace_executor.dependency_resolver_mut().add(digest, lib.clone().into()); + } + trace_executor.with_dependencies(package.manifest.dependencies())?; trace_executor.with_advice_inputs(inputs.advice_inputs.clone()); for lib in libs { trace_executor.with_library(lib); @@ -95,19 +124,38 @@ impl State { } let args = inputs.inputs.iter().copied().rev().collect::>(); - let mut executor = Executor::for_package(&package, args.clone())?; - executor.with_advice_inputs(inputs.advice_inputs.clone()); - let mut libs = Vec::with_capacity(self.config.link_libraries.len()); + // Load link libraries first, before creating the executor + let mut libs = Vec::with_capacity(self.config.link_libraries.len() + 1); for link_library in self.config.link_libraries.iter() { let lib = link_library.load(&self.config, self.source_manager.clone())?; - libs.push(lib.clone()); - executor.with_library(lib); + libs.push(lib); + } + + // Load the standard library automatically + let stdlib = get_stdlib(); + libs.push(stdlib); + + // Create executor and add link libraries to the dependency resolver + let mut executor = Executor::new(args.clone()); + for lib in libs.iter() { + let digest = *lib.digest(); + executor.dependency_resolver_mut().add(digest, lib.clone().into()); + } + executor.with_dependencies(package.manifest.dependencies())?; + executor.with_advice_inputs(inputs.advice_inputs.clone()); + for lib in libs.iter() { + executor.with_library(lib.clone()); } let program = package.unwrap_program(); let executor = executor.into_debug(&program, self.source_manager.clone()); // Execute the program until it terminates to capture a full trace for use during debugging - let mut trace_executor = Executor::for_package(&package, args)?; + let mut trace_executor = Executor::new(args); + for lib in libs.iter() { + let digest = *lib.digest(); + trace_executor.dependency_resolver_mut().add(digest, lib.clone().into()); + } + trace_executor.with_dependencies(package.manifest.dependencies())?; trace_executor.with_advice_inputs(core::mem::take(&mut inputs.advice_inputs)); for lib in libs { trace_executor.with_library(lib); From 2537b3b0ab5035383e4b1087220e880f4dfc94b5 Mon Sep 17 00:00:00 2001 From: djole Date: Mon, 26 Jan 2026 11:16:15 +0100 Subject: [PATCH 3/3] chore: apply rustfmt formatting --- src/debug/variables.rs | 7 +------ src/exec/executor.rs | 5 ++++- src/ui/state.rs | 18 +++++++----------- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/debug/variables.rs b/src/debug/variables.rs index 1193687..fe9f706 100644 --- a/src/debug/variables.rs +++ b/src/debug/variables.rs @@ -1,9 +1,4 @@ -use std::{ - cell::RefCell, - collections::BTreeMap, - fmt, - rc::Rc, -}; +use std::{cell::RefCell, collections::BTreeMap, fmt, rc::Rc}; use miden_core::Felt; use miden_processor::RowIndex; diff --git a/src/exec/executor.rs b/src/exec/executor.rs index ed1c7b3..100c90f 100644 --- a/src/exec/executor.rs +++ b/src/exec/executor.rs @@ -20,7 +20,10 @@ use miden_processor::{ }; use super::{DebugExecutor, DebuggerHost, ExecutionConfig, ExecutionTrace, TraceEvent}; -use crate::{debug::{CallStack, DebugVarInfo, DebugVarTracker}, felt::FromMidenRepr}; +use crate::{ + debug::{CallStack, DebugVarInfo, DebugVarTracker}, + felt::FromMidenRepr, +}; /// The [Executor] is responsible for executing a program with the Miden VM. /// diff --git a/src/ui/state.rs b/src/ui/state.rs index 52fce95..7d9d6d0 100644 --- a/src/ui/state.rs +++ b/src/ui/state.rs @@ -1,7 +1,10 @@ use std::sync::Arc; use miden_assembly::{DefaultSourceManager, SourceManager}; -use miden_assembly_syntax::{Library, diagnostics::{IntoDiagnostic, Report}}; +use miden_assembly_syntax::{ + Library, + diagnostics::{IntoDiagnostic, Report}, +}; use miden_core::{FieldElement, utils::Deserializable}; use miden_processor::{Felt, StackInputs}; @@ -333,12 +336,8 @@ impl State { } let mut output = String::new(); - let stack: Vec = self - .executor - .last - .as_ref() - .map(|state| state.stack.clone()) - .unwrap_or_default(); + let stack: Vec = + self.executor.last.as_ref().map(|state| state.stack.clone()).unwrap_or_default(); let context = self.executor.current_context; let cycle = miden_processor::RowIndex::from(self.executor.cycle); @@ -355,10 +354,7 @@ impl State { let value = resolve_variable_value( location, &stack, - |addr| { - self.execution_trace - .read_memory_element_in_context(addr, context, cycle) - }, + |addr| self.execution_trace.read_memory_element_in_context(addr, context, cycle), |_idx| { // Local resolution would need FMP calculation // For now, return None