diff --git a/tooling/nargo/src/errors.rs b/tooling/nargo/src/errors.rs index 466909db24d..d7e2641acea 100644 --- a/tooling/nargo/src/errors.rs +++ b/tooling/nargo/src/errors.rs @@ -45,6 +45,7 @@ impl NargoError { OpcodeResolutionError::BrilligFunctionFailed { message, .. } => Some(message), OpcodeResolutionError::BlackBoxFunctionFailed(_, reason) => Some(reason), }, + ExecutionError::Halted => None, } } } @@ -56,4 +57,7 @@ pub enum ExecutionError { #[error(transparent)] SolvingError(#[from] OpcodeResolutionError), + + #[error("Execution halted")] + Halted, } diff --git a/tooling/nargo/src/ops/debug.rs b/tooling/nargo/src/ops/debug.rs new file mode 100644 index 00000000000..e289b55430e --- /dev/null +++ b/tooling/nargo/src/ops/debug.rs @@ -0,0 +1,157 @@ +use acvm::pwg::{ACVMStatus, ErrorLocation, OpcodeResolutionError, ACVM}; +use acvm::BlackBoxFunctionSolver; +use acvm::{acir::circuit::Circuit, acir::native_types::WitnessMap}; +use acvm::acir::circuit::OpcodeLocation; + +use crate::artifacts::debug::DebugArtifact; +use crate::errors::ExecutionError; +use crate::NargoError; + +use super::foreign_calls::ForeignCall; + +use std::io::{self, Write}; + +enum SolveResult { + Done, + Ok, +} + +enum Command { + Step, + Continue, + Stop, +} + +pub fn debug_circuit( + blackbox_solver: &B, + circuit: Circuit, + debug_artifact: DebugArtifact, + initial_witness: WitnessMap, + show_output: bool, +) -> Result { + let mut acvm = ACVM::new(blackbox_solver, circuit.opcodes, initial_witness); + + 'outer: loop { + show_current_vm_status(&acvm, &debug_artifact); + let command = match read_command() { + Ok(cmd) => cmd, + Err(err) => { + eprintln!("Error reading command: {}", err); + return Err(NargoError::ExecutionError(ExecutionError::Halted)) + } + }; + match command { + Command::Stop => return Err(NargoError::ExecutionError(ExecutionError::Halted)), + Command::Step => { + match step_opcode(&mut acvm, &circuit.assert_messages, show_output)? { + SolveResult::Done => break, + SolveResult::Ok => {}, + } + } + Command::Continue => { + println!("(Continuing execution...)"); + loop { + match step_opcode(&mut acvm, &circuit.assert_messages, show_output)? { + SolveResult::Done => break 'outer, + SolveResult::Ok => {}, + } + } + }, + } + } + + let solved_witness = acvm.finalize(); + Ok(solved_witness) +} + +fn step_opcode( + acvm: &mut ACVM, + assert_messages: &Vec<(OpcodeLocation, String)>, + show_output: bool, +) -> Result { + // Assert messages are not a map due to https://github.com/noir-lang/acvm/issues/522 + let get_assert_message = |opcode_location| { + assert_messages + .iter() + .find(|(loc, _)| loc == opcode_location) + .map(|(_, message)| message.clone()) + }; + + let solver_status = acvm.solve_opcode(); + + match solver_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) = get_assert_message( + call_stack.last().expect("Call stacks should not be empty"), + ) { + ExecutionError::AssertionFailed(assert_message, call_stack) + } else { + ExecutionError::SolvingError(error) + } + } + None => ExecutionError::SolvingError(error), + })) + } + ACVMStatus::RequiresForeignCall(foreign_call) => { + let foreign_call_result = ForeignCall::execute(&foreign_call, show_output)?; + acvm.resolve_pending_foreign_call(foreign_call_result); + Ok(SolveResult::Ok) + } + } +} + +fn show_source_code_location(location: &OpcodeLocation, debug_artifact: &DebugArtifact) { + let locations = debug_artifact.debug_symbols[0].opcode_location(&location); + match locations { + Some(locations) => { + for loc in locations { + let file = &debug_artifact.file_map[&loc.file]; + let source = &file.source.as_str(); + let start = loc.span.start() as usize; + let end = loc.span.end() as usize; + println!("At {}:{start}-{end}", file.path.as_path().display()); + println!("\n{}\n", &source[start..end]); + } + }, + None => {} + } +} + +fn show_current_vm_status (acvm: &ACVM, debug_artifact: &DebugArtifact) { + let ip = acvm.instruction_pointer(); + println!("Stopped at opcode {}: {}", ip, acvm.opcodes()[ip]); + show_source_code_location(&OpcodeLocation::Acir(ip), &debug_artifact); +} + +fn read_command() -> Result { + loop { + let mut line = String::new(); + print!(">>> "); + io::stdout().flush().unwrap(); + io::stdin().read_line(&mut line)?; + if line.is_empty() { + return Ok(Command::Stop); + } + match line.trim() { + "s" => return Ok(Command::Step), + "c" => return Ok(Command::Continue), + "q" => return Ok(Command::Stop), + "" => continue, + _ => println!("ERROR: unknown command") + } + } +} diff --git a/tooling/nargo/src/ops/mod.rs b/tooling/nargo/src/ops/mod.rs index f789455577c..16933a8217c 100644 --- a/tooling/nargo/src/ops/mod.rs +++ b/tooling/nargo/src/ops/mod.rs @@ -1,8 +1,10 @@ pub use self::execute::execute_circuit; +pub use self::debug::debug_circuit; pub use self::optimize::{optimize_contract, optimize_program}; pub use self::test::{run_test, TestStatus}; mod execute; +mod debug; mod foreign_calls; mod optimize; mod test; diff --git a/tooling/nargo_cli/src/cli/debug_cmd.rs b/tooling/nargo_cli/src/cli/debug_cmd.rs new file mode 100644 index 00000000000..024037d06b6 --- /dev/null +++ b/tooling/nargo_cli/src/cli/debug_cmd.rs @@ -0,0 +1,117 @@ +use acvm::acir::native_types::WitnessMap; +use clap::Args; + +use nargo::constants::PROVER_INPUT_FILE; +use nargo::package::Package; +use nargo::artifacts::debug::DebugArtifact; +use nargo_toml::{get_package_manifest, resolve_workspace_from_toml, PackageSelection}; +use noirc_abi::input_parser::{Format, InputValue}; +use noirc_abi::InputMap; +use noirc_driver::{CompileOptions, CompiledProgram}; +use noirc_frontend::graph::CrateName; + +use super::compile_cmd::compile_bin_package; +use super::fs::{inputs::read_inputs_from_file, witness::save_witness_to_dir}; +use super::NargoConfig; +use crate::backends::Backend; +use crate::errors::CliError; + +/// Executes a circuit in debug mode +#[derive(Debug, Clone, Args)] +pub(crate) struct DebugCommand { + /// Write the execution witness to named file + witness_name: Option, + + /// The name of the toml file which contains the inputs for the prover + #[clap(long, short, default_value = PROVER_INPUT_FILE)] + prover_name: String, + + /// The name of the package to execute + #[clap(long)] + package: Option, + + #[clap(flatten)] + compile_options: CompileOptions, +} + +pub(crate) fn run( + backend: &Backend, + args: DebugCommand, + config: NargoConfig, +) -> Result<(), CliError> { + let toml_path = get_package_manifest(&config.program_dir)?; + let selection = args.package.map_or(PackageSelection::DefaultOrAll, PackageSelection::Selected); + let workspace = resolve_workspace_from_toml(&toml_path, selection)?; + let target_dir = &workspace.target_directory_path(); + + let (np_language, opcode_support) = backend.get_backend_info()?; + for package in &workspace { + let compiled_program = compile_bin_package( + &workspace, + package, + &args.compile_options, + true, + np_language, + &|opcode| opcode_support.is_opcode_supported(opcode), + )?; + + println!("[{}] Starting debugger", package.name); + let (return_value, solved_witness) = + debug_program_and_decode(compiled_program, package, &args.prover_name)?; + + println!("[{}] Circuit witness successfully solved", package.name); + if let Some(return_value) = return_value { + println!("[{}] Circuit output: {return_value:?}", package.name); + } + if let Some(witness_name) = &args.witness_name { + let witness_path = save_witness_to_dir(solved_witness, witness_name, target_dir)?; + + println!("[{}] Witness saved to {}", package.name, witness_path.display()); + } + } + Ok(()) +} + +fn debug_program_and_decode( + program: CompiledProgram, + package: &Package, + prover_name: &str, +) -> Result<(Option, WitnessMap), CliError> { + // Parse the initial witness values from Prover.toml + let (inputs_map, _) = + read_inputs_from_file(&package.root_dir, prover_name, Format::Toml, &program.abi)?; + let solved_witness = debug_program(&program, &inputs_map)?; + let public_abi = program.abi.public_abi(); + let (_, return_value) = public_abi.decode(&solved_witness)?; + + Ok((return_value, solved_witness)) +} + +pub(crate) fn debug_program( + compiled_program: &CompiledProgram, + inputs_map: &InputMap, +) -> Result { + #[allow(deprecated)] + let blackbox_solver = barretenberg_blackbox_solver::BarretenbergSolver::new(); + + let initial_witness = compiled_program.abi.encode(inputs_map, None)?; + + let debug_artifact = DebugArtifact { + debug_symbols: vec![compiled_program.debug.clone()], + file_map: compiled_program.file_map.clone(), + }; + + let solved_witness_err = nargo::ops::debug_circuit( + &blackbox_solver, + compiled_program.circuit.clone(), + debug_artifact, + initial_witness, + true, + ); + match solved_witness_err { + Ok(solved_witness) => Ok(solved_witness), + Err(err) => { + Err(crate::errors::CliError::NargoError(err)) + } + } +} diff --git a/tooling/nargo_cli/src/cli/mod.rs b/tooling/nargo_cli/src/cli/mod.rs index 56d36095518..657973a92d0 100644 --- a/tooling/nargo_cli/src/cli/mod.rs +++ b/tooling/nargo_cli/src/cli/mod.rs @@ -21,6 +21,7 @@ mod new_cmd; mod prove_cmd; mod test_cmd; mod verify_cmd; +mod debug_cmd; const GIT_HASH: &str = env!("GIT_COMMIT"); const IS_DIRTY: &str = env!("GIT_DIRTY"); @@ -58,6 +59,7 @@ enum NargoCommand { New(new_cmd::NewCommand), Init(init_cmd::InitCommand), Execute(execute_cmd::ExecuteCommand), + Debug(debug_cmd::DebugCommand), Prove(prove_cmd::ProveCommand), Verify(verify_cmd::VerifyCommand), Test(test_cmd::TestCommand), @@ -93,6 +95,7 @@ pub(crate) fn start_cli() -> eyre::Result<()> { NargoCommand::Check(args) => check_cmd::run(&backend, args, config), NargoCommand::Compile(args) => compile_cmd::run(&backend, args, config), NargoCommand::Execute(args) => execute_cmd::run(&backend, args, config), + NargoCommand::Debug(args) => debug_cmd::run(&backend, args, config), NargoCommand::Prove(args) => prove_cmd::run(&backend, args, config), NargoCommand::Verify(args) => verify_cmd::run(&backend, args, config), NargoCommand::Test(args) => test_cmd::run(&backend, args, config),