diff --git a/tooling/debugger/src/context.rs b/tooling/debugger/src/context.rs index f8f573930c0..a97bc9e8b86 100644 --- a/tooling/debugger/src/context.rs +++ b/tooling/debugger/src/context.rs @@ -5,14 +5,18 @@ use acvm::pwg::{ use acvm::BlackBoxFunctionSolver; use acvm::{acir::circuit::Circuit, acir::native_types::WitnessMap}; -use nargo::errors::ExecutionError; +use nargo::artifacts::debug::DebugArtifact; +use nargo::errors::{ExecutionError, Location}; use nargo::ops::ForeignCallExecutor; use nargo::NargoError; +use std::collections::{hash_set::Iter, HashSet}; + #[derive(Debug)] pub(super) enum DebugCommandResult { Done, Ok, + BreakpointReached(OpcodeLocation), Error(NargoError), } @@ -20,20 +24,25 @@ pub(super) struct DebugContext<'a, B: BlackBoxFunctionSolver> { acvm: ACVM<'a, B>, brillig_solver: Option>, foreign_call_executor: ForeignCallExecutor, + debug_artifact: &'a DebugArtifact, show_output: bool, + breakpoints: HashSet, } impl<'a, B: BlackBoxFunctionSolver> DebugContext<'a, B> { pub(super) fn new( blackbox_solver: &'a B, circuit: &'a Circuit, + debug_artifact: &'a DebugArtifact, initial_witness: WitnessMap, ) -> Self { Self { acvm: ACVM::new(blackbox_solver, &circuit.opcodes, initial_witness), brillig_solver: None, foreign_call_executor: ForeignCallExecutor::default(), + debug_artifact, show_output: true, + breakpoints: HashSet::new(), } } @@ -55,25 +64,41 @@ impl<'a, B: BlackBoxFunctionSolver> DebugContext<'a, B> { } } + // Returns the callstack in source code locations for the currently + // executing opcode. This can be None if the execution finished (and + // get_current_opcode_location() returns None) or if the opcode is not + // mapped to a specific source location in the debug artifact (which can + // happen for certain opcodes inserted synthetically by the compiler) + pub(super) fn get_current_source_location(&self) -> Option> { + self.get_current_opcode_location() + .as_ref() + .and_then(|location| self.debug_artifact.debug_symbols[0].opcode_location(location)) + } + fn step_brillig_opcode(&mut self) -> DebugCommandResult { let Some(mut solver) = self.brillig_solver.take() else { unreachable!("Missing Brillig solver"); }; match solver.step() { - Ok(status) => match status { - BrilligSolverStatus::InProgress => { - self.brillig_solver = Some(solver); + Ok(BrilligSolverStatus::InProgress) => { + self.brillig_solver = Some(solver); + if self.breakpoint_reached() { + DebugCommandResult::BreakpointReached( + self.get_current_opcode_location() + .expect("Breakpoint reached but we have no location"), + ) + } else { DebugCommandResult::Ok } - BrilligSolverStatus::Finished => { - let status = self.acvm.finish_brillig_with_solver(solver); - self.handle_acvm_status(status) - } - BrilligSolverStatus::ForeignCallWait(foreign_call) => { - self.brillig_solver = Some(solver); - self.handle_foreign_call(foreign_call) - } - }, + } + Ok(BrilligSolverStatus::Finished) => { + let status = self.acvm.finish_brillig_with_solver(solver); + self.handle_acvm_status(status) + } + Ok(BrilligSolverStatus::ForeignCallWait(foreign_call)) => { + self.brillig_solver = Some(solver); + self.handle_foreign_call(foreign_call) + } Err(err) => DebugCommandResult::Error(NargoError::ExecutionError( ExecutionError::SolvingError(err), )), @@ -95,32 +120,41 @@ impl<'a, B: BlackBoxFunctionSolver> DebugContext<'a, B> { fn handle_acvm_status(&mut self, status: ACVMStatus) -> DebugCommandResult { if let ACVMStatus::RequiresForeignCall(foreign_call) = status { - self.handle_foreign_call(foreign_call) - } else { - match status { - ACVMStatus::Solved => DebugCommandResult::Done, - ACVMStatus::InProgress => DebugCommandResult::Ok, - ACVMStatus::Failure(error) => DebugCommandResult::Error( - NargoError::ExecutionError(ExecutionError::SolvingError(error)), - ), - ACVMStatus::RequiresForeignCall(_) => { - unreachable!("Unexpected pending foreign call resolution"); + return self.handle_foreign_call(foreign_call); + } + + match status { + ACVMStatus::Solved => DebugCommandResult::Done, + ACVMStatus::InProgress => { + if self.breakpoint_reached() { + DebugCommandResult::BreakpointReached( + self.get_current_opcode_location() + .expect("Breakpoint reached but we have no location"), + ) + } else { + DebugCommandResult::Ok } } + ACVMStatus::Failure(error) => DebugCommandResult::Error(NargoError::ExecutionError( + ExecutionError::SolvingError(error), + )), + ACVMStatus::RequiresForeignCall(_) => { + unreachable!("Unexpected pending foreign call resolution"); + } } } pub(super) fn step_into_opcode(&mut self) -> DebugCommandResult { if self.brillig_solver.is_some() { - self.step_brillig_opcode() - } else { - match self.acvm.step_into_brillig_opcode() { - StepResult::IntoBrillig(solver) => { - self.brillig_solver = Some(solver); - self.step_brillig_opcode() - } - StepResult::Status(status) => self.handle_acvm_status(status), + return self.step_brillig_opcode(); + } + + match self.acvm.step_into_brillig_opcode() { + StepResult::IntoBrillig(solver) => { + self.brillig_solver = Some(solver); + self.step_brillig_opcode() } + StepResult::Status(status) => self.handle_acvm_status(status), } } @@ -133,6 +167,20 @@ impl<'a, B: BlackBoxFunctionSolver> DebugContext<'a, B> { self.handle_acvm_status(status) } + pub(super) fn next(&mut self) -> DebugCommandResult { + let start_location = self.get_current_source_location(); + loop { + let result = self.step_into_opcode(); + if !matches!(result, DebugCommandResult::Ok) { + return result; + } + let new_location = self.get_current_source_location(); + if new_location.is_some() && new_location != start_location { + return DebugCommandResult::Ok; + } + } + } + pub(super) fn cont(&mut self) -> DebugCommandResult { loop { let result = self.step_into_opcode(); @@ -142,6 +190,48 @@ impl<'a, B: BlackBoxFunctionSolver> DebugContext<'a, B> { } } + fn breakpoint_reached(&self) -> bool { + if let Some(location) = self.get_current_opcode_location() { + self.breakpoints.contains(&location) + } else { + false + } + } + + pub(super) fn is_valid_opcode_location(&self, location: &OpcodeLocation) -> bool { + let opcodes = self.get_opcodes(); + match *location { + OpcodeLocation::Acir(acir_index) => acir_index < opcodes.len(), + OpcodeLocation::Brillig { acir_index, brillig_index } => { + acir_index < opcodes.len() + && matches!(opcodes[acir_index], Opcode::Brillig(..)) + && { + if let Opcode::Brillig(ref brillig) = opcodes[acir_index] { + brillig_index < brillig.bytecode.len() + } else { + false + } + } + } + } + } + + pub(super) fn is_breakpoint_set(&self, location: &OpcodeLocation) -> bool { + self.breakpoints.contains(location) + } + + pub(super) fn add_breakpoint(&mut self, location: OpcodeLocation) -> bool { + self.breakpoints.insert(location) + } + + pub(super) fn delete_breakpoint(&mut self, location: &OpcodeLocation) -> bool { + self.breakpoints.remove(location) + } + + pub(super) fn iterate_breakpoints(&self) -> Iter<'_, OpcodeLocation> { + self.breakpoints.iter() + } + pub(super) fn is_solved(&self) -> bool { matches!(self.acvm.get_status(), ACVMStatus::Solved) } diff --git a/tooling/debugger/src/repl.rs b/tooling/debugger/src/repl.rs index 028d5120b07..320d8edf63a 100644 --- a/tooling/debugger/src/repl.rs +++ b/tooling/debugger/src/repl.rs @@ -1,6 +1,6 @@ use crate::context::{DebugCommandResult, DebugContext}; -use acvm::acir::circuit::OpcodeLocation; +use acvm::acir::circuit::{Opcode, OpcodeLocation}; use acvm::BlackBoxFunctionSolver; use acvm::{acir::circuit::Circuit, acir::native_types::WitnessMap}; @@ -19,7 +19,10 @@ use std::ops::Range; pub struct ReplDebugger<'a, B: BlackBoxFunctionSolver> { context: DebugContext<'a, B>, + blackbox_solver: &'a B, + circuit: &'a Circuit, debug_artifact: &'a DebugArtifact, + initial_witness: WitnessMap, last_result: DebugCommandResult, } @@ -30,8 +33,16 @@ impl<'a, B: BlackBoxFunctionSolver> ReplDebugger<'a, B> { debug_artifact: &'a DebugArtifact, initial_witness: WitnessMap, ) -> Self { - let context = DebugContext::new(blackbox_solver, circuit, initial_witness); - Self { context, debug_artifact, last_result: DebugCommandResult::Ok } + let context = + DebugContext::new(blackbox_solver, circuit, debug_artifact, initial_witness.clone()); + Self { + context, + blackbox_solver, + circuit, + debug_artifact, + initial_witness, + last_result: DebugCommandResult::Ok, + } } pub fn show_current_vm_status(&self) { @@ -45,10 +56,15 @@ impl<'a, B: BlackBoxFunctionSolver> ReplDebugger<'a, B> { OpcodeLocation::Acir(ip) => { println!("At opcode {}: {}", ip, opcodes[ip]) } - OpcodeLocation::Brillig { acir_index: ip, brillig_index } => println!( - "At opcode {} in Brillig block {}: {}", - brillig_index, ip, opcodes[ip] - ), + OpcodeLocation::Brillig { acir_index, brillig_index } => { + let Opcode::Brillig(ref brillig) = opcodes[acir_index] else { + unreachable!("Brillig location does not contain a Brillig block"); + }; + println!( + "At opcode {}.{}: {:?}", + acir_index, brillig_index, brillig.bytecode[brillig_index] + ); + } } self.show_source_code_location(&location); } @@ -121,9 +137,80 @@ impl<'a, B: BlackBoxFunctionSolver> ReplDebugger<'a, B> { } } + fn display_opcodes(&self) { + let opcodes = self.context.get_opcodes(); + let current_opcode_location = self.context.get_current_opcode_location(); + let current_acir_index = match current_opcode_location { + Some(OpcodeLocation::Acir(ip)) => Some(ip), + Some(OpcodeLocation::Brillig { acir_index, .. }) => Some(acir_index), + None => None, + }; + let current_brillig_index = match current_opcode_location { + Some(OpcodeLocation::Brillig { brillig_index, .. }) => brillig_index, + _ => 0, + }; + let outer_marker = |acir_index| { + if current_acir_index == Some(acir_index) { + "->" + } else if self.context.is_breakpoint_set(&OpcodeLocation::Acir(acir_index)) { + " *" + } else { + "" + } + }; + let brillig_marker = |acir_index, brillig_index| { + if current_acir_index == Some(acir_index) && brillig_index == current_brillig_index { + "->" + } else if self + .context + .is_breakpoint_set(&OpcodeLocation::Brillig { acir_index, brillig_index }) + { + " *" + } else { + "" + } + }; + for (acir_index, opcode) in opcodes.iter().enumerate() { + let marker = outer_marker(acir_index); + if let Opcode::Brillig(brillig) = opcode { + println!("{:>3} {:2} BRILLIG inputs={:?}", acir_index, marker, brillig.inputs); + println!(" | outputs={:?}", brillig.outputs); + for (brillig_index, brillig_opcode) in brillig.bytecode.iter().enumerate() { + println!( + "{:>3}.{:<2} |{:2} {:?}", + acir_index, + brillig_index, + brillig_marker(acir_index, brillig_index), + brillig_opcode + ); + } + } else { + println!("{:>3} {:2} {:?}", acir_index, marker, opcode); + } + } + } + + fn add_breakpoint_at(&mut self, location: OpcodeLocation) { + if !self.context.is_valid_opcode_location(&location) { + println!("Invalid opcode location {location}"); + } else if self.context.add_breakpoint(location) { + println!("Added breakpoint at opcode {location}"); + } else { + println!("Breakpoint at opcode {location} already set"); + } + } + + fn delete_breakpoint_at(&mut self, location: OpcodeLocation) { + if self.context.delete_breakpoint(&location) { + println!("Breakpoint at opcode {location} deleted"); + } else { + println!("Breakpoint at opcode {location} not set"); + } + } + fn validate_in_progress(&self) -> bool { match self.last_result { - DebugCommandResult::Ok => true, + DebugCommandResult::Ok | DebugCommandResult::BreakpointReached(..) => true, DebugCommandResult::Done => { println!("Execution finished"); false @@ -137,6 +224,15 @@ impl<'a, B: BlackBoxFunctionSolver> ReplDebugger<'a, B> { } fn handle_debug_command_result(&mut self, result: DebugCommandResult) { + match &result { + DebugCommandResult::BreakpointReached(location) => { + println!("Stopped at breakpoint in opcode {}", location); + } + DebugCommandResult::Error(error) => { + println!("ERROR: {}", error); + } + _ => (), + } self.last_result = result; self.show_current_vm_status(); } @@ -155,6 +251,13 @@ impl<'a, B: BlackBoxFunctionSolver> ReplDebugger<'a, B> { } } + fn next(&mut self) { + if self.validate_in_progress() { + let result = self.context.next(); + self.handle_debug_command_result(result); + } + } + fn cont(&mut self) { if self.validate_in_progress() { println!("(Continuing execution...)"); @@ -163,6 +266,23 @@ impl<'a, B: BlackBoxFunctionSolver> ReplDebugger<'a, B> { } } + fn restart_session(&mut self) { + let breakpoints: Vec = + self.context.iterate_breakpoints().copied().collect(); + self.context = DebugContext::new( + self.blackbox_solver, + self.circuit, + self.debug_artifact, + self.initial_witness.clone(), + ); + for opcode_location in breakpoints { + self.context.add_breakpoint(opcode_location); + } + self.last_result = DebugCommandResult::Ok; + println!("Restarted debugging session."); + self.show_current_vm_status(); + } + fn is_solved(&self) -> bool { self.context.is_solved() } @@ -213,6 +333,16 @@ pub fn run( } }, ) + .add( + "next", + command! { + "step until a new source location is reached", + () => || { + ref_context.borrow_mut().next(); + Ok(CommandStatus::Done) + } + }, + ) .add( "continue", command! { @@ -223,6 +353,46 @@ pub fn run( } }, ) + .add( + "restart", + command! { + "restart the debugging session", + () => || { + ref_context.borrow_mut().restart_session(); + Ok(CommandStatus::Done) + } + }, + ) + .add( + "opcodes", + command! { + "display ACIR opcodes", + () => || { + ref_context.borrow().display_opcodes(); + Ok(CommandStatus::Done) + } + }, + ) + .add( + "break", + command! { + "add a breakpoint at an opcode location", + (LOCATION:OpcodeLocation) => |location| { + ref_context.borrow_mut().add_breakpoint_at(location); + Ok(CommandStatus::Done) + } + }, + ) + .add( + "delete", + command! { + "delete breakpoint at an opcode location", + (LOCATION:OpcodeLocation) => |location| { + ref_context.borrow_mut().delete_breakpoint_at(location); + Ok(CommandStatus::Done) + } + }, + ) .build() .expect("Failed to initialize debugger repl");