diff --git a/Cargo.lock b/Cargo.lock index 4f9f964181c..d807535d04d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2535,6 +2535,7 @@ dependencies = [ "codespan-reporting", "easy-repl", "nargo", + "noirc_errors", "noirc_printable_type", "owo-colors", "thiserror", diff --git a/acvm-repo/acvm/src/pwg/mod.rs b/acvm-repo/acvm/src/pwg/mod.rs index 1022ad13800..6016d0d1f3e 100644 --- a/acvm-repo/acvm/src/pwg/mod.rs +++ b/acvm-repo/acvm/src/pwg/mod.rs @@ -193,6 +193,10 @@ impl<'a, B: BlackBoxFunctionSolver> ACVM<'a, B> { status } + pub fn get_status(&self) -> &ACVMStatus { + &self.status + } + /// Sets the VM status to [ACVMStatus::Failure] using the provided `error`. /// Returns the new status. fn fail(&mut self, error: OpcodeResolutionError) -> ACVMStatus { diff --git a/tooling/debugger/Cargo.toml b/tooling/debugger/Cargo.toml index 1f9a54b4fe8..53c71754da4 100644 --- a/tooling/debugger/Cargo.toml +++ b/tooling/debugger/Cargo.toml @@ -12,6 +12,7 @@ license.workspace = true acvm.workspace = true nargo.workspace = true noirc_printable_type.workspace = true +noirc_errors.workspace = true thiserror.workspace = true codespan-reporting.workspace = true easy-repl = "0.2.1" diff --git a/tooling/debugger/src/context.rs b/tooling/debugger/src/context.rs new file mode 100644 index 00000000000..025e6404bcb --- /dev/null +++ b/tooling/debugger/src/context.rs @@ -0,0 +1,152 @@ +use acvm::acir::circuit::{Opcode, OpcodeLocation}; +use acvm::pwg::{ + ACVMStatus, BrilligSolver, BrilligSolverStatus, ForeignCallWaitInfo, StepResult, ACVM, +}; +use acvm::BlackBoxFunctionSolver; +use acvm::{acir::circuit::Circuit, acir::native_types::WitnessMap}; + +use nargo::errors::ExecutionError; +use nargo::ops::ForeignCallExecutor; +use nargo::NargoError; + +#[derive(Debug)] +pub(super) enum DebugCommandResult { + Done, + Ok, + Error(NargoError), +} + +pub(super) struct DebugContext<'a, B: BlackBoxFunctionSolver> { + acvm: ACVM<'a, B>, + brillig_solver: Option>, + foreign_call_executor: ForeignCallExecutor, + show_output: bool, +} + +impl<'a, B: BlackBoxFunctionSolver> DebugContext<'a, B> { + pub(super) fn new( + blackbox_solver: &'a B, + circuit: &'a Circuit, + initial_witness: WitnessMap, + ) -> Self { + Self { + acvm: ACVM::new(blackbox_solver, &circuit.opcodes, initial_witness), + brillig_solver: None, + foreign_call_executor: ForeignCallExecutor::default(), + show_output: true, + } + } + + pub(super) fn get_opcodes(&self) -> &[Opcode] { + self.acvm.opcodes() + } + + pub(super) fn get_current_opcode_location(&self) -> Option { + let ip = self.acvm.instruction_pointer(); + if ip >= self.get_opcodes().len() { + None + } else if let Some(ref solver) = self.brillig_solver { + Some(OpcodeLocation::Brillig { + acir_index: ip, + brillig_index: solver.program_counter(), + }) + } else { + Some(OpcodeLocation::Acir(ip)) + } + } + + 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); + 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) + } + }, + Err(err) => DebugCommandResult::Error(NargoError::ExecutionError( + ExecutionError::SolvingError(err), + )), + } + } + + fn handle_foreign_call(&mut self, foreign_call: ForeignCallWaitInfo) -> DebugCommandResult { + let foreign_call_result = + self.foreign_call_executor.execute(&foreign_call, self.show_output); + match foreign_call_result { + Ok(foreign_call_result) => { + self.acvm.resolve_pending_foreign_call(foreign_call_result); + // TODO: should we retry executing the opcode somehow in this case? + DebugCommandResult::Ok + } + Err(error) => DebugCommandResult::Error(error), + } + } + + 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"); + } + } + } + } + + pub(super) fn step_into_opcode(&mut self) -> DebugCommandResult { + if matches!(self.brillig_solver, 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), + } + } + } + + pub(super) fn step_acir_opcode(&mut self) -> DebugCommandResult { + let status = if let Some(solver) = self.brillig_solver.take() { + self.acvm.finish_brillig_with_solver(solver) + } else { + self.acvm.solve_opcode() + }; + self.handle_acvm_status(status) + } + + pub(super) fn cont(&mut self) -> DebugCommandResult { + loop { + let result = self.step_into_opcode(); + if !matches!(result, DebugCommandResult::Ok) { + return result; + } + } + } + + pub(super) fn is_solved(&self) -> bool { + matches!(self.acvm.get_status(), ACVMStatus::Solved) + } + + pub fn finalize(self) -> WitnessMap { + self.acvm.finalize() + } +} diff --git a/tooling/debugger/src/lib.rs b/tooling/debugger/src/lib.rs index 42f447ac857..42ae79fe411 100644 --- a/tooling/debugger/src/lib.rs +++ b/tooling/debugger/src/lib.rs @@ -1,293 +1,17 @@ -use acvm::acir::circuit::OpcodeLocation; -use acvm::pwg::{ - ACVMStatus, BrilligSolver, BrilligSolverStatus, ErrorLocation, OpcodeResolutionError, - StepResult, ACVM, -}; +mod context; +mod repl; + use acvm::BlackBoxFunctionSolver; use acvm::{acir::circuit::Circuit, acir::native_types::WitnessMap}; use nargo::artifacts::debug::DebugArtifact; -use nargo::errors::{ExecutionError, Location}; use nargo::NargoError; -use nargo::ops::ForeignCallExecutor; - -use easy_repl::{command, CommandStatus, Critical, Repl}; -use std::{ - cell::{Cell, RefCell}, - ops::Range, -}; - -use owo_colors::OwoColorize; - -use codespan_reporting::files::Files; - -enum SolveResult { - Done, - Ok, -} - -struct DebugContext<'backend, B: BlackBoxFunctionSolver> { - acvm: ACVM<'backend, B>, - brillig_solver: Option>, - debug_artifact: DebugArtifact, - foreign_call_executor: ForeignCallExecutor, - circuit: &'backend Circuit, - show_output: bool, -} - -impl<'backend, B: BlackBoxFunctionSolver> DebugContext<'backend, B> { - fn step_brillig_opcode(&mut self) -> Result { - let Some(mut solver) = self.brillig_solver.take() else { - unreachable!("Missing Brillig solver"); - }; - match solver.step() { - Ok(status) => { - println!("Brillig step result: {:?}", status); - println!("Brillig program counter: {:?}", solver.program_counter()); - match status { - BrilligSolverStatus::InProgress => { - self.brillig_solver = Some(solver); - Ok(SolveResult::Ok) - } - BrilligSolverStatus::Finished => { - let status = self.acvm.finish_brillig_with_solver(solver); - self.handle_acvm_status(status) - } - BrilligSolverStatus::ForeignCallWait(foreign_call) => { - let foreign_call_result = - self.foreign_call_executor.execute(&foreign_call, self.show_output)?; - solver.resolve_pending_foreign_call(foreign_call_result); - self.brillig_solver = Some(solver); - Ok(SolveResult::Ok) - } - } - } - Err(err) => self.handle_acvm_status(ACVMStatus::Failure(err)), - } - } - - fn step_opcode(&mut self) -> Result { - if matches!(self.brillig_solver, 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), - } - } - } - - fn handle_acvm_status(&mut self, status: ACVMStatus) -> Result { - match status { - ACVMStatus::Solved => Ok(SolveResult::Done), - ACVMStatus::InProgress => Ok(SolveResult::Ok), - ACVMStatus::Failure(error) => { - let call_stack = match &error { - OpcodeResolutionError::UnsatisfiedConstrain { - opcode_location: ErrorLocation::Resolved(opcode_location), - } => Some(vec![*opcode_location]), - OpcodeResolutionError::BrilligFunctionFailed { call_stack, .. } => { - Some(call_stack.clone()) - } - _ => None, - }; - - Err(NargoError::ExecutionError(match call_stack { - Some(call_stack) => { - if let Some(assert_message) = self.circuit.get_assert_message( - *call_stack.last().expect("Call stacks should not be empty"), - ) { - ExecutionError::AssertionFailed(assert_message.to_owned(), call_stack) - } else { - ExecutionError::SolvingError(error) - } - } - None => ExecutionError::SolvingError(error), - })) - } - ACVMStatus::RequiresForeignCall(foreign_call) => { - let foreign_call_result = - self.foreign_call_executor.execute(&foreign_call, self.show_output)?; - self.acvm.resolve_pending_foreign_call(foreign_call_result); - Ok(SolveResult::Ok) - } - } - } - - fn show_current_vm_status(&self) { - let ip = self.acvm.instruction_pointer(); - let opcodes = self.acvm.opcodes(); - if ip >= opcodes.len() { - println!("Finished execution"); - } else { - println!("Stopped at opcode {}: {}", ip, opcodes[ip]); - self.show_source_code_location(&OpcodeLocation::Acir(ip), &self.debug_artifact); - } - } - - fn print_location_path(&self, loc: Location) { - let line_number = self.debug_artifact.location_line_number(loc).unwrap(); - let column_number = self.debug_artifact.location_column_number(loc).unwrap(); - - println!( - "At {}:{line_number}:{column_number}", - self.debug_artifact.name(loc.file).unwrap() - ); - } - - fn show_source_code_location(&self, location: &OpcodeLocation, debug_artifact: &DebugArtifact) { - let locations = debug_artifact.debug_symbols[0].opcode_location(location); - let Some(locations) = locations else { return }; - for loc in locations { - self.print_location_path(loc); - - let loc_line_index = debug_artifact.location_line_index(loc).unwrap(); - - // How many lines before or after the location's line we - // print - let context_lines = 5; - - let first_line_to_print = - if loc_line_index < context_lines { 0 } else { loc_line_index - context_lines }; - - let last_line_index = debug_artifact.last_line_index(loc).unwrap(); - let last_line_to_print = std::cmp::min(loc_line_index + context_lines, last_line_index); - - let source = debug_artifact.location_source_code(loc).unwrap(); - for (current_line_index, line) in source.lines().enumerate() { - let current_line_number = current_line_index + 1; - - if current_line_index < first_line_to_print { - // Ignore lines before range starts - continue; - } else if current_line_index == first_line_to_print && current_line_index > 0 { - // Denote that there's more lines before but we're not showing them - print_line_of_ellipsis(current_line_index); - } - - if current_line_index > last_line_to_print { - // Denote that there's more lines after but we're not showing them, - // and stop printing - print_line_of_ellipsis(current_line_number); - break; - } - - if current_line_index == loc_line_index { - // Highlight current location - let Range { start: loc_start, end: loc_end } = - debug_artifact.location_in_line(loc).unwrap(); - println!( - "{:>3} {:2} {}{}{}", - current_line_number, - "->", - &line[0..loc_start].to_string().dimmed(), - &line[loc_start..loc_end], - &line[loc_end..].to_string().dimmed() - ); - } else { - print_dimmed_line(current_line_number, line); - } - } - } - } - - fn cont(&mut self) -> Result { - loop { - match self.step_opcode()? { - SolveResult::Done => break, - SolveResult::Ok => {} - } - } - Ok(SolveResult::Done) - } - - fn finalize(self) -> WitnessMap { - self.acvm.finalize() - } -} - -fn print_line_of_ellipsis(line_number: usize) { - println!("{}", format!("{:>3} {}", line_number, "...").dimmed()); -} - -fn print_dimmed_line(line_number: usize, line: &str) { - println!("{}", format!("{:>3} {:2} {}", line_number, "", line).dimmed()); -} - -fn map_command_status(result: SolveResult) -> CommandStatus { - match result { - SolveResult::Ok => CommandStatus::Done, - SolveResult::Done => CommandStatus::Quit, - } -} - pub fn debug_circuit( blackbox_solver: &B, circuit: &Circuit, debug_artifact: DebugArtifact, initial_witness: WitnessMap, - show_output: bool, ) -> Result, NargoError> { - let context = RefCell::new(DebugContext { - acvm: ACVM::new(blackbox_solver, &circuit.opcodes, initial_witness), - foreign_call_executor: ForeignCallExecutor::default(), - brillig_solver: None, - circuit, - debug_artifact, - show_output, - }); - let ref_step = &context; - let ref_cont = &context; - - let solved = Cell::new(false); - - context.borrow().show_current_vm_status(); - - let handle_result = |result| { - solved.set(matches!(result, SolveResult::Done)); - Ok(map_command_status(result)) - }; - - let mut repl = Repl::builder() - .add( - "s", - command! { - "step to the next opcode", - () => || { - let result = ref_step.borrow_mut().step_opcode().into_critical()?; - ref_step.borrow().show_current_vm_status(); - handle_result(result) - } - }, - ) - .add( - "c", - command! { - "continue execution until the end of the program", - () => || { - println!("(Continuing execution...)"); - let result = ref_cont.borrow_mut().cont().into_critical()?; - handle_result(result) - } - }, - ) - .build() - .expect("Failed to initialize debugger repl"); - - repl.run().expect("Debugger error"); - - // REPL execution has finished. - // Drop it so that we can move fields out from `context` again. - drop(repl); - - if solved.get() { - let solved_witness = context.into_inner().finalize(); - Ok(Some(solved_witness)) - } else { - Ok(None) - } + repl::run(blackbox_solver, circuit, &debug_artifact, initial_witness) } diff --git a/tooling/debugger/src/repl.rs b/tooling/debugger/src/repl.rs new file mode 100644 index 00000000000..028d5120b07 --- /dev/null +++ b/tooling/debugger/src/repl.rs @@ -0,0 +1,241 @@ +use crate::context::{DebugCommandResult, DebugContext}; + +use acvm::acir::circuit::OpcodeLocation; +use acvm::BlackBoxFunctionSolver; +use acvm::{acir::circuit::Circuit, acir::native_types::WitnessMap}; + +use nargo::artifacts::debug::DebugArtifact; +use nargo::NargoError; + +use easy_repl::{command, CommandStatus, Repl}; +use std::cell::RefCell; + +use codespan_reporting::files::Files; +use noirc_errors::Location; + +use owo_colors::OwoColorize; + +use std::ops::Range; + +pub struct ReplDebugger<'a, B: BlackBoxFunctionSolver> { + context: DebugContext<'a, B>, + debug_artifact: &'a DebugArtifact, + last_result: DebugCommandResult, +} + +impl<'a, B: BlackBoxFunctionSolver> ReplDebugger<'a, B> { + pub fn new( + blackbox_solver: &'a B, + circuit: &'a Circuit, + 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 } + } + + pub fn show_current_vm_status(&self) { + let location = self.context.get_current_opcode_location(); + let opcodes = self.context.get_opcodes(); + + match location { + None => println!("Finished execution"), + Some(location) => { + match location { + 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] + ), + } + self.show_source_code_location(&location); + } + } + } + + fn print_location_path(&self, loc: Location) { + let line_number = self.debug_artifact.location_line_number(loc).unwrap(); + let column_number = self.debug_artifact.location_column_number(loc).unwrap(); + + println!( + "At {}:{line_number}:{column_number}", + self.debug_artifact.name(loc.file).unwrap() + ); + } + + fn show_source_code_location(&self, location: &OpcodeLocation) { + let locations = self.debug_artifact.debug_symbols[0].opcode_location(location); + let Some(locations) = locations else { return }; + for loc in locations { + self.print_location_path(loc); + + let loc_line_index = self.debug_artifact.location_line_index(loc).unwrap(); + + // How many lines before or after the location's line we + // print + let context_lines = 5; + + let first_line_to_print = + if loc_line_index < context_lines { 0 } else { loc_line_index - context_lines }; + + let last_line_index = self.debug_artifact.last_line_index(loc).unwrap(); + let last_line_to_print = std::cmp::min(loc_line_index + context_lines, last_line_index); + + let source = self.debug_artifact.location_source_code(loc).unwrap(); + for (current_line_index, line) in source.lines().enumerate() { + let current_line_number = current_line_index + 1; + + if current_line_index < first_line_to_print { + // Ignore lines before range starts + continue; + } else if current_line_index == first_line_to_print && current_line_index > 0 { + // Denote that there's more lines before but we're not showing them + print_line_of_ellipsis(current_line_index); + } + + if current_line_index > last_line_to_print { + // Denote that there's more lines after but we're not showing them, + // and stop printing + print_line_of_ellipsis(current_line_number); + break; + } + + if current_line_index == loc_line_index { + // Highlight current location + let Range { start: loc_start, end: loc_end } = + self.debug_artifact.location_in_line(loc).unwrap(); + println!( + "{:>3} {:2} {}{}{}", + current_line_number, + "->", + &line[0..loc_start].to_string().dimmed(), + &line[loc_start..loc_end], + &line[loc_end..].to_string().dimmed() + ); + } else { + print_dimmed_line(current_line_number, line); + } + } + } + } + + fn validate_in_progress(&self) -> bool { + match self.last_result { + DebugCommandResult::Ok => true, + DebugCommandResult::Done => { + println!("Execution finished"); + false + } + DebugCommandResult::Error(ref error) => { + println!("ERROR: {}", error); + self.show_current_vm_status(); + false + } + } + } + + fn handle_debug_command_result(&mut self, result: DebugCommandResult) { + self.last_result = result; + self.show_current_vm_status(); + } + + fn step_acir_opcode(&mut self) { + if self.validate_in_progress() { + let result = self.context.step_acir_opcode(); + self.handle_debug_command_result(result); + } + } + + fn step_into_opcode(&mut self) { + if self.validate_in_progress() { + let result = self.context.step_into_opcode(); + self.handle_debug_command_result(result); + } + } + + fn cont(&mut self) { + if self.validate_in_progress() { + println!("(Continuing execution...)"); + let result = self.context.cont(); + self.handle_debug_command_result(result); + } + } + + fn is_solved(&self) -> bool { + self.context.is_solved() + } + + fn finalize(self) -> WitnessMap { + self.context.finalize() + } +} + +fn print_line_of_ellipsis(line_number: usize) { + println!("{}", format!("{:>3} {}", line_number, "...").dimmed()); +} + +fn print_dimmed_line(line_number: usize, line: &str) { + println!("{}", format!("{:>3} {:2} {}", line_number, "", line).dimmed()); +} + +pub fn run( + blackbox_solver: &B, + circuit: &Circuit, + debug_artifact: &DebugArtifact, + initial_witness: WitnessMap, +) -> Result, NargoError> { + let context = + RefCell::new(ReplDebugger::new(blackbox_solver, circuit, debug_artifact, initial_witness)); + let ref_context = &context; + + ref_context.borrow().show_current_vm_status(); + + let mut repl = Repl::builder() + .add( + "step", + command! { + "step to the next ACIR opcode", + () => || { + ref_context.borrow_mut().step_acir_opcode(); + Ok(CommandStatus::Done) + } + }, + ) + .add( + "into", + command! { + "step into to the next opcode", + () => || { + ref_context.borrow_mut().step_into_opcode(); + Ok(CommandStatus::Done) + } + }, + ) + .add( + "continue", + command! { + "continue execution until the end of the program", + () => || { + ref_context.borrow_mut().cont(); + Ok(CommandStatus::Done) + } + }, + ) + .build() + .expect("Failed to initialize debugger repl"); + + repl.run().expect("Debugger error"); + + // REPL execution has finished. + // Drop it so that we can move fields out from `context` again. + drop(repl); + + if context.borrow().is_solved() { + let solved_witness = context.into_inner().finalize(); + Ok(Some(solved_witness)) + } else { + Ok(None) + } +} diff --git a/tooling/nargo_cli/src/cli/debug_cmd.rs b/tooling/nargo_cli/src/cli/debug_cmd.rs index 82cd3349ec4..1f2daa6caae 100644 --- a/tooling/nargo_cli/src/cli/debug_cmd.rs +++ b/tooling/nargo_cli/src/cli/debug_cmd.rs @@ -119,7 +119,6 @@ pub(crate) fn debug_program( &compiled_program.circuit, debug_artifact, initial_witness, - true, ) .map_err(CliError::from) }