Skip to content

Commit

Permalink
First cut at implementing a debug command for nargo
Browse files Browse the repository at this point in the history
Allows stepping through the ACIR opcodes, showing the current opcode and mapped
source location.
  • Loading branch information
ggiraldez committed Oct 2, 2023
1 parent 0a51b17 commit 4bac24d
Show file tree
Hide file tree
Showing 5 changed files with 283 additions and 0 deletions.
4 changes: 4 additions & 0 deletions tooling/nargo/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ impl NargoError {
OpcodeResolutionError::BrilligFunctionFailed { message, .. } => Some(message),
OpcodeResolutionError::BlackBoxFunctionFailed(_, reason) => Some(reason),
},
ExecutionError::Halted => None,
}
}
}
Expand All @@ -56,4 +57,7 @@ pub enum ExecutionError {

#[error(transparent)]
SolvingError(#[from] OpcodeResolutionError),

#[error("Execution halted")]
Halted,
}
157 changes: 157 additions & 0 deletions tooling/nargo/src/ops/debug.rs
Original file line number Diff line number Diff line change
@@ -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<B: BlackBoxFunctionSolver>(
blackbox_solver: &B,
circuit: Circuit,
debug_artifact: DebugArtifact,
initial_witness: WitnessMap,
show_output: bool,
) -> Result<WitnessMap, NargoError> {
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<B: BlackBoxFunctionSolver>(
acvm: &mut ACVM<B>,
assert_messages: &Vec<(OpcodeLocation, String)>,
show_output: bool,
) -> Result<SolveResult, NargoError> {
// 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<B: BlackBoxFunctionSolver> (acvm: &ACVM<B>, 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<Command, io::Error> {
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")
}
}
}
2 changes: 2 additions & 0 deletions tooling/nargo/src/ops/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
117 changes: 117 additions & 0 deletions tooling/nargo_cli/src/cli/debug_cmd.rs
Original file line number Diff line number Diff line change
@@ -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<String>,

/// 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<CrateName>,

#[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<InputValue>, 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<WitnessMap, CliError> {
#[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))
}
}
}
3 changes: 3 additions & 0 deletions tooling/nargo_cli/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down

0 comments on commit 4bac24d

Please sign in to comment.