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 13, 2023
1 parent 0ec30b5 commit 1ad81c8
Show file tree
Hide file tree
Showing 13 changed files with 365 additions and 142 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,
)
}
}
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 1ad81c8

Please sign in to comment.