Skip to content

Commit

Permalink
feat: add breakpoint instruction
Browse files Browse the repository at this point in the history
This commit introduces `breakpoint`, a transparent instruction that will
cause a debug execution to break when reached.

This instruction will not be serialized into libraries, and will not
have an opcode or be part of the code block tree.

For debug executions, it will produce a `NOOP`, so the internal clock of
the VM will be affected.

The decision of not including this as part of the code block is to avoid
further consuming variants of opcodes as they are already scarce.

related issue: #580
  • Loading branch information
vlopes11 committed Mar 14, 2023
1 parent 0ec30b5 commit 34da914
Show file tree
Hide file tree
Showing 14 changed files with 419 additions and 159 deletions.
16 changes: 15 additions & 1 deletion assembly/src/assembler/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,11 +250,18 @@ impl AssemblyContext {
// HELPER METHODS
// --------------------------------------------------------------------------------------------

/// Returns the context of the procedure currently being complied, or None if module or
/// Returns the context of the procedure currently being compiled, or None if module or
/// procedure stacks are empty.
fn current_proc_context(&self) -> Option<&ProcedureContext> {
self.module_stack.last().and_then(|m| m.proc_stack.last())
}

/// Returns the name of the current procedure, or the reserved name for the main block.
pub(crate) fn current_context_name(&self) -> &str {
self.current_proc_context()
.map(|p| p.name().as_ref())
.unwrap_or(ProcedureName::MAIN_PROC_NAME)
}
}

// MODULE CONTEXT
Expand Down Expand Up @@ -478,6 +485,13 @@ impl ProcedureContext {
self.name.is_main()
}

/// Returns the current context name.
///
/// Check [AssemblyContext::current_context_name] for reference.
pub const fn name(&self) -> &ProcedureName {
&self.name
}

pub fn into_procedure(self, id: ProcedureId, code_root: CodeBlock) -> Procedure {
let Self {
name,
Expand Down
11 changes: 10 additions & 1 deletion assembly/src/assembler/instruction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ impl Assembler {
// this will allow us to map the instruction to the sequence of operations which were
// executed as a part of this instruction.
if self.in_debug_mode() {
span.track_instruction(instruction);
span.track_instruction(instruction, ctx);
}

let result = match instruction {
Expand Down Expand Up @@ -306,6 +306,15 @@ impl Assembler {
Instruction::CallLocal(idx) => self.call_local(*idx, ctx),
Instruction::CallImported(id) => self.call_imported(id, ctx),
Instruction::SysCall(id) => self.syscall(id, ctx),

// ----- debug decorators -------------------------------------------------------------
Instruction::Breakpoint => {
if self.in_debug_mode() {
span.add_op(Noop)?;
span.track_instruction(instruction, ctx);
}
Ok(None)
}
};

// compute and update the cycle count of the instruction which just finished executing
Expand Down
12 changes: 8 additions & 4 deletions assembly/src/assembler/span_builder.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::{
AssemblyError, BodyWrapper, Borrow, CodeBlock, Decorator, DecoratorList, Instruction,
Operation, ToString, Vec,
AssemblyContext, AssemblyError, BodyWrapper, Borrow, CodeBlock, Decorator, DecoratorList,
Instruction, Operation, ToString, Vec,
};
use vm_core::AssemblyOp;

Expand Down Expand Up @@ -102,8 +102,12 @@ impl SpanBuilder {
///
/// This indicates that the provided instruction should be tracked and the cycle count for
/// this instruction will be computed when the call to set_instruction_cycle_count() is made.
pub fn track_instruction(&mut self, instruction: &Instruction) {
let op = AssemblyOp::new(instruction.to_string(), 0);
pub fn track_instruction(&mut self, instruction: &Instruction, ctx: &mut AssemblyContext) {
let context_name = ctx.current_context_name().to_string();
let num_cycles = 0;
let op = instruction.to_string();
let should_break = instruction.should_break();
let op = AssemblyOp::new(context_name, num_cycles, op, should_break);
self.push_decorator(Decorator::AsmOp(op));
self.last_asmop_pos = self.decorators.len() - 1;
}
Expand Down
3 changes: 3 additions & 0 deletions assembly/src/parsers/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,9 @@ impl ParserContext {
// ----- constant statements ----------------------------------------------------------
"const" => Err(ParsingError::const_invalid_scope(op)),

// ----- debug decorators -------------------------------------------------------------
"breakpoint" => simple_instruction(op, Breakpoint),

// ----- catch all --------------------------------------------------------------------
_ => Err(ParsingError::invalid_op(op)),
}
Expand Down
13 changes: 13 additions & 0 deletions assembly/src/parsers/nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,16 @@ pub enum Instruction {
CallLocal(u16),
CallImported(ProcedureId),
SysCall(ProcedureId),

// ----- debug decorators ---------------------------------------------------------------------
Breakpoint,
}

impl Instruction {
/// Returns true if the instruction should yield a breakpoint.
pub const fn should_break(&self) -> bool {
matches!(self, Self::Breakpoint)
}
}

impl fmt::Display for Instruction {
Expand Down Expand Up @@ -543,6 +553,9 @@ impl fmt::Display for Instruction {
Self::CallLocal(index) => write!(f, "call.{index}"),
Self::CallImported(proc_id) => write!(f, "call.{proc_id}"),
Self::SysCall(proc_id) => write!(f, "syscall.{proc_id}"),

// ----- debug decorators -------------------------------------------------------------
Self::Breakpoint => write!(f, "breakpoint"),
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions assembly/src/parsers/serde/serialization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,11 @@ impl Serializable for Instruction {
OpCode::SysCall.write_into(target)?;
imported.write_into(target)?
}

// ----- debug decorators -------------------------------------------------------------
Self::Breakpoint => {
// this is a transparent instruction and will not be encoded into the library
}
}
Ok(())
}
Expand Down
42 changes: 35 additions & 7 deletions core/src/operations/decorators/assembly_op.rs
Original file line number Diff line number Diff line change
@@ -1,32 +1,50 @@
use crate::utils::string::String;
use core::fmt;

// ASSEMBLY OP
// ================================================================================================

/// Contains information corresponding to an assembly instruction (only applicable in debug mode).
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AssemblyOp {
op: String,
context_name: String,
num_cycles: u8,
op: String,
should_break: bool,
}

impl AssemblyOp {
/// Returns [AssemblyOp] instantiated with the specified assembly instruction string and number
/// of cycles it takes to execute the assembly instruction.
pub fn new(op: String, num_cycles: u8) -> Self {
Self { op, num_cycles }
pub fn new(context_name: String, num_cycles: u8, op: String, should_break: bool) -> Self {
Self {
context_name,
num_cycles,
op,
should_break,
}
}

/// Returns the assembly instruction corresponding to this decorator.
pub fn op(&self) -> &String {
&self.op
/// Returns the context name for this operation.
pub fn context_name(&self) -> &str {
&self.context_name
}

/// Returns the number of VM cycles taken to execute the assembly instruction of this decorator.
pub fn num_cycles(&self) -> u8 {
pub const fn num_cycles(&self) -> u8 {
self.num_cycles
}

/// Returns the assembly instruction corresponding to this decorator.
pub fn op(&self) -> &str {
&self.op
}

/// Returns `true` if there is a breakpoint for the current operation.
pub const fn should_break(&self) -> bool {
self.should_break
}

// STATE MUTATORS
// --------------------------------------------------------------------------------------------

Expand All @@ -35,3 +53,13 @@ impl AssemblyOp {
self.num_cycles = num_cycles;
}
}

impl fmt::Display for AssemblyOp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"context={}, operation={}, cost={}",
self.context_name, self.op, self.num_cycles,
)
}
}
71 changes: 54 additions & 17 deletions docs/src/intro/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,23 +65,60 @@ This will dump the output of the program into the `fib.out` file. The output fil

### 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
The Miden debugger is a command-line interface (CLI) application, inspired on [GNU gdb](https://sourceware.org/gdb/), that allows debugging of Miden assembly (MASM) programs. The debugger allows the user to step through the execution of the program, both forward and backward, either per clock cycle tick, or via breakpoints.

The Miden debugger supports the following commands:

| Command | Shortcut | Arguments | Description |
| --- | --- | --- | --- |
| next | n | count? | Steps `count` clock cycles. Will step `1` cycle of `count` is ommitted. |
| continue | c | - | Executes the program until completion, failure or a breakpoint. |
| back | b | count? | Backward step `count` clock cycles. Will back-step `1` cycle of `count` is ommitted. |
| rewind | r | - | Executes the program backwards until the beginning, failure or a breakpoint. |
| print | p | - | Displays the complete state of the virtual machine. |
| print mem | p m | address? | Displays the memory value at `address`. If `address` is ommitted, didisplays all the memory values. |
| print stack | p s | index? | Displays the stack value at `index`. If `index` is ommitted, displays all the stack values. |
| clock | c | - | Displays the current clock cycle. |
| quit | q | - | Quits the debugger. |
| help | h | - | Displays the help message. |

In order to start debugging, the user should provide a `MASM` program:

```shell
cargo run --features executable -- debug --assembly miden/examples/nprime/nprime.masm
```

The expected output is:

```
============================================================
Debug program
============================================================
Reading program file `miden/examples/nprime/nprime.masm`
Compiling program... done (16 ms)
Debugging program with hash 11dbbddff27e26e48be3198133df8cbed6c5875d0fb
606c9f037c7893fde4118...
Reading input file `miden/examples/nprime/nprime.inputs`
Welcome! Enter `h` for help.
>>
```

In order to add a breakpoint, the user should insert a `breakpoint` instruction into the MASM file. This will generate a `Noop` operation that will be decorated with the debug break configuration. This is a provisory solution until the source mapping is implemented.

The following example will halt on the third instruction of `foo`:

```
proc.foo
dup
dup.2
breakpoint
swap
add.1
end
begin
exec.foo
end
```

### REPL
Expand Down
10 changes: 5 additions & 5 deletions miden/src/cli/debug/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
pub enum DebugCommand {
Continue,
Next(usize),
RewindAll,
Rewind(usize),
Rewind,
Back(usize),
PrintState,
PrintStack,
PrintStackItem(usize),
Expand Down Expand Up @@ -36,7 +36,7 @@ impl DebugCommand {
"n" | "next" => Self::parse_next(tokens.by_ref())?,
"c" | "continue" => Self::Continue,
"b" | "back" => Self::parse_back(tokens.by_ref())?,
"r" | "rewind" => Self::RewindAll,
"r" | "rewind" => Self::Rewind,
"p" | "print" => Self::parse_print(tokens.by_ref())?,
"l" | "clock" => Self::Clock,
"h" | "?" | "help" => Self::Help,
Expand Down Expand Up @@ -89,9 +89,9 @@ impl DebugCommand {
n, err
)
})?,
None => return Ok(Self::Rewind(1)),
None => return Ok(Self::Back(1)),
};
Ok(Self::Rewind(num_cycles))
Ok(Self::Back(num_cycles))
}

/// parse print command - p [m|s] [addr]
Expand Down
33 changes: 18 additions & 15 deletions miden/src/cli/debug/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ impl DebugExecutor {
DebugCommand::Continue => {
while let Some(new_vm_state) = self.next_vm_state() {
self.vm_state = new_vm_state;
if self.should_break() {
break;
}
}
self.print_vm_state();
}
Expand All @@ -53,23 +56,29 @@ impl DebugExecutor {
match self.next_vm_state() {
Some(next_vm_state) => {
self.vm_state = next_vm_state;
if self.should_break() {
break;
}
}
None => break,
}
}
self.print_vm_state();
}
DebugCommand::RewindAll => {
while let Some(new_vm_state) = self.prev_vm_state() {
DebugCommand::Rewind => {
while let Some(new_vm_state) = self.vm_state_iter.back() {
self.vm_state = new_vm_state;
}
self.print_vm_state();
}
DebugCommand::Rewind(cycles) => {
DebugCommand::Back(cycles) => {
for _cycle in 0..cycles {
match self.prev_vm_state() {
match self.vm_state_iter.back() {
Some(new_vm_state) => {
self.vm_state = new_vm_state;
if self.should_break() {
break;
}
}
None => break,
}
Expand Down Expand Up @@ -105,17 +114,6 @@ impl DebugExecutor {
}
}

/// iterates to the previous clock cycle.
fn prev_vm_state(&mut self) -> Option<VmState> {
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
// --------------------------------------------------------------------------------------------

Expand Down Expand Up @@ -213,4 +211,9 @@ impl DebugExecutor {

println!("{}", message);
}

/// Returns `true` if the current state should break.
fn should_break(&self) -> bool {
self.vm_state.asmop.as_ref().map(|asm| asm.should_break()).unwrap_or(false)
}
}
Loading

0 comments on commit 34da914

Please sign in to comment.