From 03066912d3b136074ffd6802860b55997d50182f Mon Sep 17 00:00:00 2001 From: frisitano Date: Fri, 10 Feb 2023 00:03:54 +0400 Subject: [PATCH] feat(miden): add primitve miden assembly debug interface --- docs/src/intro/usage.md | 40 +++++-- miden/README.md | 1 + miden/src/cli/debug/command.rs | 88 ++++++++++++++ miden/src/cli/debug/executor.rs | 203 ++++++++++++++++++++++++++++++++ miden/src/cli/debug/mod.rs | 76 ++++++++++++ miden/src/cli/mod.rs | 2 + miden/src/main.rs | 2 + processor/src/debug.rs | 53 ++++++++- 8 files changed, 456 insertions(+), 9 deletions(-) create mode 100644 miden/src/cli/debug/command.rs create mode 100644 miden/src/cli/debug/executor.rs create mode 100644 miden/src/cli/debug/mod.rs diff --git a/docs/src/intro/usage.md b/docs/src/intro/usage.md index 5124e5f6b1..5307fe7f1c 100644 --- a/docs/src/intro/usage.md +++ b/docs/src/intro/usage.md @@ -35,6 +35,7 @@ Currently, Miden VM can be executed with the following subcommands: * `prove` - this will execute a Miden assembly program, and will also generate a STARK proof of execution. * `verify` - this will verify a previously generated proof of execution for a given program. * `compile` - this will compile a Miden assembly program (i.e., build a program [MAST](../design/programs.md)) and outputs stats about the compilation process. +* `debug` - this will instantiate a CLI debugger against the specified Miden assembly program and inputs. * `analyze` - this will run a Miden assembly program against specific inputs and will output stats about its execution. * `repl` - this will initiate the [Miden REPL](usage.md#repl) tool. @@ -54,11 +55,34 @@ In the `miden/examples/fib` directory, we provide a very simple Fibonacci calcul ``` This will run the example code to completion and will output the top element remaining on the stack. -## REPL +## Miden Development Tooling + +### Miden Debugger + +The Miden debugger is a shell that allow for efficient debugging of miden assembly programs that are sourced from file. A Miden assembly program file and inputs are specified when the debugger is instantiated. The debugger allows the user to step through the execution of the program with clock cycle granularity and provides the ability for virtual machine state inspection at each clock cycle. The Miden debugger supports the following commands: + +``` +!next steps to the next clock cycle +!play executes program until completion or failure +!play.n executes n clock cycles +!prev steps to the previous clock cycle +!rewind rewinds program until beginning +!rewind.n rewinds n clock cycles +!print displays the complete state of the virtual machine +!stack displays the complete state of the stack +!stack[i] displays the stack element at index i +!mem displays the complete state of memory +!mem[i] displays memory at address i +!clock displays the current clock cycle +!quit quits the debugger +!help displays this message +``` + +### REPL The Miden Read–eval–print loop (REPL) is a Miden shell that allows for quick and easy debugging of Miden assembly. After the REPL gets initialized, you can execute any Miden instruction, undo executed instructions, check the state of the stack and memory at a given point, and do many other useful things! When the REPL is exited, a `history.txt` file is saved. One thing to note is that all the REPL native commands start with an `!` to differentiate them from regular assembly instructions. The REPL currently supports the following commands: -### Miden assembly instruction +#### Miden assembly instruction All Miden instructions mentioned in the [Miden Assembly sections](../user_docs/assembly/main.md) are valid. One can either input instructions one by one or multiple instructions in one input. @@ -87,11 +111,11 @@ The above example should be written as follows in the REPL tool: repeat.20 pow2 end ``` -### !help +#### !help The `!help` command prints out all the available commands in the REPL tool. -### !program +#### !program The `!program` command prints out the entire Miden program being executed. E.g., in the below scenario: @@ -108,7 +132,7 @@ begin end ``` -### !stack +#### !stack The `!stack` command prints out the state of the stack at the last executed instruction. Since the stack always contains at least 16 elements, 16 or more elements will be printed out (even if all of them are zeros). @@ -128,7 +152,7 @@ The `!stack` command will print out the following state of the stack: 3072 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ``` -### !mem +#### !mem The `!mem` command prints out the contents of all initialized memory locations. For each such location, the address, along with its memory values, is printed. Recall that four elements are stored at each memory address. @@ -148,7 +172,7 @@ If the memory is not yet been initialized: The memory has not been initialized yet ``` -### !mem[addr] +#### !mem[addr] The `!mem[addr]` command prints out memory contents at the address specified by `addr`. @@ -166,7 +190,7 @@ If the `addr` has not been initialized: Memory at address 87 is empty ``` -### !undo +#### !undo The `!undo` command reverts to the previous state of the stack and memory by dropping off the last executed assembly instruction from the program. One could use `!undo` as often as they want to restore the state of a stack and memory $n$ instructions ago (provided there are $n$ instructions in the program). The `!undo` command will result in an error if no remaining instructions are left in the Miden program. diff --git a/miden/README.md b/miden/README.md index 14da9b34bc..fb7a5db037 100644 --- a/miden/README.md +++ b/miden/README.md @@ -222,6 +222,7 @@ Currently, Miden VM can be executed with the following subcommands: * `prove` - this will execute a Miden assembly program, and will also generate a STARK proof of execution. * `verify` - this will verify a previously generated proof of execution for a given program. * `compile` - this will compile a Miden assembly program and outputs stats about the compilation process. +* `debug` - this will instantiate a CLI debugger against the specified Miden assembly program and inputs. * `analyze` - this will run a Miden assembly program against specific inputs and will output stats about its execution. All of the above subcommands require various parameters to be provided. To get more detailed help on what is needed for a given subcommand, you can run the following: diff --git a/miden/src/cli/debug/command.rs b/miden/src/cli/debug/command.rs new file mode 100644 index 0000000000..634f0bd1e6 --- /dev/null +++ b/miden/src/cli/debug/command.rs @@ -0,0 +1,88 @@ +/// debug commands supported by the debugger +pub enum DebugCommand { + PlayAll, + Play(usize), + RewindAll, + Rewind(usize), + PrintState, + PrintStack, + PrintStackItem(usize), + PrintMem, + PrintMemAddress(u64), + Clock, + Quit, + Help, +} + +impl DebugCommand { + // CONSTRUCTOR + // -------------------------------------------------------------------------------------------- + /// Returns a new DebugCommand created specified command string. + /// + /// # Errors + /// Returns an error if the command cannot be parsed. + pub fn parse(command: &str) -> Result { + match command { + "!next" => Ok(Self::Play(1)), + "!play" => Ok(Self::PlayAll), + "!prev" => Ok(Self::Rewind(1)), + "!rewind" => Ok(Self::RewindAll), + "!print" => Ok(Self::PrintState), + "!mem" => Ok(Self::PrintMem), + "!stack" => Ok(Self::PrintStack), + "!clock" => Ok(Self::Clock), + "!quit" => Ok(Self::Quit), + "!help" => Ok(Self::Help), + x if x.starts_with("!rewind.") => Self::parse_rewind(x), + x if x.starts_with("!play.") => Self::parse_play(command), + x if x.starts_with("!stack[") && x.ends_with("]") => Self::parse_print_stack(x), + x if x.starts_with("!mem[") && x.ends_with(']') => Self::parse_print_memory(x), + _ => { + Err(format!("malformed command - does not match any known command: `{}`", command)) + } + } + } + + // HELPERS + // -------------------------------------------------------------------------------------------- + + /// parse play command - !play.num_cycles + fn parse_play(command: &str) -> Result { + // parse number of cycles + let num_cycles = command[6..].parse::().map_err(|err| { + format!("malformed command - failed to parse number of cycles: `{}` {}", command, err) + })?; + + Ok(Self::Play(num_cycles)) + } + + /// parse rewind command - !rewind.num_cycles + fn parse_rewind(command: &str) -> Result { + // parse number of cycles + let num_cycles = command[8..].parse::().map_err(|err| { + format!("malformed command - failed to parse number of cycles: `{}` {}", command, err) + })?; + + Ok(Self::Rewind(num_cycles)) + } + + /// parse print memory command - !mem[address] + fn parse_print_memory(command: &str) -> Result { + // parse address + let address = command[5..command.len() - 1].parse::().map_err(|err| { + format!("malformed command - failed to parse address parameter: `{}` {}", command, err) + })?; + + Ok(Self::PrintMemAddress(address)) + } + + /// parse print stack command - !stack[index] + fn parse_print_stack(command: &str) -> Result { + // parse stack index + let index = command[7..command.len() - 1].parse::().map_err(|err| { + format!("malformed command - failed to parse stack index: `{}` {}", command, err) + })?; + + Ok(Self::PrintStackItem(index)) + } +} diff --git a/miden/src/cli/debug/executor.rs b/miden/src/cli/debug/executor.rs new file mode 100644 index 0000000000..26dc91a170 --- /dev/null +++ b/miden/src/cli/debug/executor.rs @@ -0,0 +1,203 @@ +use super::DebugCommand; +use miden::{ + math::{Felt, StarkField}, + MemAdviceProvider, Program, StackInputs, VmState, VmStateIterator, +}; + +/// Holds debugger state and iterator used for debugging. +pub struct DebugExecutor { + vm_state_iter: VmStateIterator, + vm_state: VmState, +} + +impl DebugExecutor { + // CONSTRUCTOR + // -------------------------------------------------------------------------------------------- + /// Returns a new DebugExecutor for the specified program, inputs and advice provider. + /// + /// # Errors + /// Returns an error if the command cannot be parsed. + pub fn new( + program: Program, + stack_inputs: StackInputs, + advice_provider: MemAdviceProvider, + ) -> Result { + let mut vm_state_iter = processor::execute_iter(&program, stack_inputs, advice_provider); + let vm_state = vm_state_iter + .next() + .ok_or(format!( + "Failed to instantiate DebugExecutor - `VmStateIterator` is not yielding!" + ))? + .expect("initial state of vm must be healthy!"); + + Ok(Self { + vm_state_iter, + vm_state, + }) + } + + // MODIFIERS + // -------------------------------------------------------------------------------------------- + + /// executes a debug command against the vm in it's current state. + pub fn execute(&mut self, command: DebugCommand) -> bool { + match command { + DebugCommand::PlayAll => { + while let Some(new_vm_state) = self.next_vm_state() { + self.vm_state = new_vm_state; + } + self.print_vm_state(); + } + DebugCommand::Play(cycles) => { + for _cycle in 0..cycles { + match self.next_vm_state() { + Some(next_vm_state) => { + self.vm_state = next_vm_state; + } + None => break, + } + } + self.print_vm_state(); + } + DebugCommand::RewindAll => { + while let Some(new_vm_state) = self.prev_vm_state() { + self.vm_state = new_vm_state; + } + self.print_vm_state(); + } + DebugCommand::Rewind(cycles) => { + for _cycle in 0..cycles { + match self.prev_vm_state() { + Some(new_vm_state) => { + self.vm_state = new_vm_state; + } + None => break, + } + } + self.print_vm_state() + } + DebugCommand::PrintState => self.print_vm_state(), + DebugCommand::PrintStack => self.print_stack(), + DebugCommand::PrintStackItem(index) => self.print_stack_item(index), + DebugCommand::PrintMem => self.print_memory(), + DebugCommand::PrintMemAddress(address) => self.print_memory_entry(address), + DebugCommand::Clock => println!("{}", self.vm_state.clk), + DebugCommand::Help => Self::print_help(), + DebugCommand::Quit => return false, + } + true + } + + /// iterates to the next clock cycle. + fn next_vm_state(&mut self) -> Option { + match self.vm_state_iter.next() { + Some(next_vm_state_result) => match next_vm_state_result { + Ok(vm_state) => Some(vm_state), + Err(err) => { + println!("Execution error: {err:?}"); + None + } + }, + None => { + println!("Program execution complete."); + None + } + } + } + + /// iterates to the previous clock cycle. + fn prev_vm_state(&mut self) -> Option { + match self.vm_state_iter.next_back() { + Some(prev_vm_state_result) => prev_vm_state_result.ok(), + None => { + println!("At start of program execution."); + None + } + } + } + + // ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// print general VM state information. + fn print_vm_state(&self) { + println!("{}", self.vm_state) + } + + /// print all stack items. + pub fn print_stack(&self) { + println!( + "{}", + self.vm_state + .stack + .iter() + .enumerate() + .map(|(i, f)| format!("[{i}] {f}")) + .collect::>() + .join("\n"), + ) + } + + /// print specified stack item. + pub fn print_stack_item(&self, index: usize) { + let len = self.vm_state.stack.len(); + println!("stack len {}", len); + if index >= len { + println!("stack index must be < {len}") + } else { + println!("[{index}] = {}", self.vm_state.stack[index]) + } + } + + /// print all memory entries. + pub fn print_memory(&self) { + for (address, mem) in self.vm_state.memory.iter() { + Self::print_memory_data(address, mem) + } + } + + /// print specified memory entry. + pub fn print_memory_entry(&self, address: u64) { + let entry = self.vm_state.memory.iter().find_map(|(addr, mem)| match address == *addr { + true => Some(mem), + false => None, + }); + + match entry { + Some(mem) => Self::print_memory_data(&address, mem), + None => println!("memory at address '{address}' not found"), + } + } + + // HELPERS + // -------------------------------------------------------------------------------------------- + + /// print memory data. + fn print_memory_data(address: &u64, memory: &[Felt]) { + let mem_int = memory.iter().map(|&x| x.as_int()).collect::>(); + println!("{address} {mem_int:?}"); + } + + /// print help message + fn print_help() { + let message = "---------------------------------------------------------\n\ + Miden Assembly Debug CLI\n\ + ---------------------------------------------------------\n\ + !next steps to the next clock cycle\n\ + !play executes program until completion or failure\n\ + !play.n executes n clock cycles\n\ + !prev steps to the previous clock cycle\n\ + !rewind rewinds program until beginning\n\ + !rewind.n rewinds n clock cycles\n\ + !print displays the complete state of the virtual machine\n\ + !stack displays the complete state of the stack\n\ + !stack[i] displays the stack element at index i\n\ + !mem displays the complete state of memory\n\ + !mem[i] displays memory at address i\n\ + !clock displays the current clock cycle\n\ + !quit quits the debugger\n\ + !help displays this message"; + + println!("{}", message); + } +} diff --git a/miden/src/cli/debug/mod.rs b/miden/src/cli/debug/mod.rs new file mode 100644 index 0000000000..ab70cd5090 --- /dev/null +++ b/miden/src/cli/debug/mod.rs @@ -0,0 +1,76 @@ +use super::data::{InputFile, ProgramFile}; +use rustyline::{Config, EditMode, Editor}; +use std::path::PathBuf; +use structopt::StructOpt; + +mod command; +use command::DebugCommand; + +mod executor; +use executor::DebugExecutor; + +#[derive(StructOpt, Debug)] +#[structopt(name = "Debug", about = "Debug a miden program")] +pub struct DebugCmd { + /// Path to .masm assembly file + #[structopt(short = "a", long = "assembly", parse(from_os_str))] + assembly_file: PathBuf, + /// Path to input file + #[structopt(short = "i", long = "input", parse(from_os_str))] + input_file: Option, + /// Enable vi edit mode + #[structopt(short = "vi", long = "vim_edit_mode")] + vim_edit_mode: Option, +} + +impl DebugCmd { + pub fn execute(&self) -> Result<(), String> { + println!("============================================================"); + println!("Debug program"); + println!("============================================================"); + + // load program from file and compile + let program = ProgramFile::read(&self.assembly_file)?; + + let program_hash: [u8; 32] = program.hash().into(); + println!("Debugging program with hash {}... ", hex::encode(program_hash)); + + // load input data from file + let input_data = InputFile::read(&self.input_file, &self.assembly_file)?; + + // fetch the stack and program inputs from the arguments + let stack_inputs = input_data.parse_stack_inputs()?; + let advice_provider = input_data.parse_advice_provider()?; + + // Instantiate DebugExecutor + let mut debug_executor = DebugExecutor::new(program, stack_inputs, advice_provider)?; + + // build readline config + let mut rl_config = Config::builder().auto_add_history(true); + if self.vim_edit_mode.is_some() { + rl_config = rl_config.edit_mode(EditMode::Vi); + } + let rl_config = rl_config.build(); + + // initialize readline + let mut rl = + Editor::<()>::with_config(rl_config).expect("Readline couldn't be initialized"); + + loop { + match rl.readline(">> ") { + Ok(command) => match DebugCommand::parse(&command) { + Ok(command) => { + if !debug_executor.execute(command) { + println!("Debugging complete"); + break; + } + } + Err(err) => println!("{err}"), + }, + Err(err) => println!("malformed command - failed to read user input: {}", err), + } + } + + Ok(()) + } +} diff --git a/miden/src/cli/mod.rs b/miden/src/cli/mod.rs index 66c3d5a5db..f285d3a73c 100644 --- a/miden/src/cli/mod.rs +++ b/miden/src/cli/mod.rs @@ -1,5 +1,6 @@ mod compile; mod data; +mod debug; mod prove; mod repl; mod run; @@ -7,6 +8,7 @@ mod verify; pub use compile::CompileCmd; pub use data::InputFile; +pub use debug::DebugCmd; pub use prove::ProveCmd; pub use repl::ReplCmd; pub use run::RunCmd; diff --git a/miden/src/main.rs b/miden/src/main.rs index 91ef119b95..eed714b1f9 100644 --- a/miden/src/main.rs +++ b/miden/src/main.rs @@ -20,6 +20,7 @@ pub struct Cli { pub enum Actions { Analyze(tools::Analyze), Compile(cli::CompileCmd), + Debug(cli::DebugCmd), Example(examples::ExampleOptions), Prove(cli::ProveCmd), Run(cli::RunCmd), @@ -34,6 +35,7 @@ impl Cli { match &self.action { Actions::Analyze(analyze) => analyze.execute(), Actions::Compile(compile) => compile.execute(), + Actions::Debug(debug) => debug.execute(), Actions::Example(example) => example.execute(), Actions::Prove(prove) => prove.execute(), Actions::Run(run) => run.execute(), diff --git a/processor/src/debug.rs b/processor/src/debug.rs index 42168772fd..a6475eb821 100644 --- a/processor/src/debug.rs +++ b/processor/src/debug.rs @@ -22,7 +22,11 @@ impl fmt::Display for VmState { let stack: Vec = self.stack.iter().map(|x| x.as_int()).collect(); let memory: Vec<(u64, [u64; 4])> = self.memory.iter().map(|x| (x.0, word_to_ints(&x.1))).collect(); - write!(f, "clk={}, fmp={}, stack={stack:?}, memory={memory:?}", self.clk, self.fmp) + write!( + f, + "clk={}, op={:?}, asmop={:?}, fmp={}, stack={stack:?}, memory={memory:?}", + self.clk, self.op, self.asmop, self.fmp + ) } } @@ -39,6 +43,7 @@ pub struct VmStateIterator { error: Option, clk: u32, asmop_idx: usize, + forward: bool, } impl VmStateIterator { @@ -55,6 +60,7 @@ impl VmStateIterator { error: result.err(), clk: 0, asmop_idx: 0, + forward: true, } } @@ -130,6 +136,12 @@ impl Iterator for VmStateIterator { } } + // if we are changing iteration directions we must increment the clk counter + if !self.forward && self.clk < self.system.clk() { + self.clk += 1; + self.forward = true; + } + let ctx = self.system.get_ctx_at(self.clk); let op = if self.clk == 0 { @@ -159,6 +171,45 @@ impl Iterator for VmStateIterator { } } +impl DoubleEndedIterator for VmStateIterator { + fn next_back(&mut self) -> Option { + if self.clk == 0 { + return None; + } + + self.clk -= 1; + + // if we are changing directions we must decrement the clk counter. + if self.forward && self.clk > 0 { + self.clk -= 1; + self.forward = false; + } + + let ctx = self.system.get_ctx_at(self.clk); + + let op = if self.clk == 0 { + None + } else { + Some(self.decoder.debug_info().operations()[self.clk as usize - 1]) + }; + + let (asmop, is_start) = self.get_asmop(); + if is_start { + self.asmop_idx -= 1; + } + + Some(Ok(VmState { + clk: self.clk, + ctx, + op, + asmop, + fmp: self.system.get_fmp_at(self.clk), + stack: self.stack.get_state_at(self.clk), + memory: self.chiplets.get_mem_state_at(ctx, self.clk), + })) + } +} + // HELPER FUNCTIONS // ================================================================================================ fn word_to_ints(word: &Word) -> [u64; 4] {