diff --git a/Cargo.lock b/Cargo.lock index a3ffc32a6b4..c2f77fb27a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3215,6 +3215,7 @@ version = "1.0.0-beta.3" dependencies = [ "acvm", "assert_cmd", + "bn254_blackbox_solver", "build-data", "codespan-reporting", "dap", diff --git a/compiler/noirc_frontend/src/debug/mod.rs b/compiler/noirc_frontend/src/debug/mod.rs index e5284eacea7..f169203b84c 100644 --- a/compiler/noirc_frontend/src/debug/mod.rs +++ b/compiler/noirc_frontend/src/debug/mod.rs @@ -1,12 +1,8 @@ use crate::ast::PathSegment; use crate::parse_program; -use crate::parser::ParsedModule; +use crate::parser::{ParsedModule, ParsedSubModule}; use crate::signed_field::SignedField; -use crate::{ - ast, - ast::Path, - parser::{Item, ItemKind}, -}; +use crate::{ast, ast::Path, parser::ItemKind}; use fm::FileId; use noirc_errors::debug_info::{DebugFnId, DebugFunction}; use noirc_errors::{Location, Span}; @@ -60,13 +56,24 @@ impl Default for DebugInstrumenter { impl DebugInstrumenter { pub fn instrument_module(&mut self, module: &mut ParsedModule, file: FileId) { module.items.iter_mut().for_each(|item| { - if let Item { kind: ItemKind::Function(f), .. } = item { - self.walk_fn(&mut f.def); + match &mut item.kind { + // Instrument top-level functions of a module + ItemKind::Function(f) => self.walk_fn(&mut f.def), + // Instrument contract module + ItemKind::Submodules(ParsedSubModule { + is_contract: true, + contents: contract_module, + .. + }) => { + self.instrument_module(contract_module, file); + } + _ => (), } }); + // this part absolutely must happen after ast traversal above // so that oracle functions don't get wrapped, resulting in infinite recursion: - self.insert_state_set_oracle(module, 8, file); + self.insert_state_set_oracle(module, file); } fn insert_var(&mut self, var_name: &str) -> Option { @@ -499,8 +506,8 @@ impl DebugInstrumenter { } } - fn insert_state_set_oracle(&self, module: &mut ParsedModule, n: u32, file: FileId) { - let member_assigns = (1..=n) + fn insert_state_set_oracle(&self, module: &mut ParsedModule, file: FileId) { + let member_assigns = (1..=MAX_MEMBER_ASSIGN_DEPTH) .map(|i| format!["__debug_member_assign_{i}"]) .collect::>() .join(",\n"); diff --git a/docs/docs/how_to/debugger/debugging_with_the_repl.md b/docs/docs/how_to/debugger/debugging_with_the_repl.md index 1d64dae3f37..aa662fa1a74 100644 --- a/docs/docs/how_to/debugger/debugging_with_the_repl.md +++ b/docs/docs/how_to/debugger/debugging_with_the_repl.md @@ -1,7 +1,7 @@ --- title: Using the REPL Debugger description: - Step-by-step guide on how to debug your Noir circuits with the REPL Debugger. + Step-by-step guide on how to debug your Noir circuits with the REPL Debugger. keywords: [ Nargo, @@ -14,7 +14,7 @@ sidebar_position: 1 #### Pre-requisites -In order to use the REPL debugger, first you need to install recent enough versions of Nargo and vscode-noir. +In order to use the REPL debugger, first you need to install recent enough versions of Nargo. ## Debugging a simple circuit @@ -38,7 +38,7 @@ At ~/noir-examples/recursion/circuits/main/src/main.nr:1:9 1 -> fn main(x : Field, y : pub Field) { 2 assert(x != y); 3 } -> +> ``` The debugger displays the current Noir code location, and it is now waiting for us to drive it. @@ -84,7 +84,7 @@ Some commands operate only for unconstrained functions, such as `memory` and `me ``` > memory Unconstrained VM memory not available -> +> ``` Before continuing, we can take a look at the initial witness map: @@ -115,7 +115,7 @@ _1 = 2 > ``` -Now we can inspect the current state of local variables. For that we use the `vars` command. +Now we can inspect the current state of local variables. For that we use the `vars` command. ``` > vars @@ -162,3 +162,40 @@ Finished execution Upon quitting the debugger after a solved circuit, the resulting circuit witness gets saved, equivalent to what would happen if we had run the same circuit with `nargo execute`. We just went through the basics of debugging using Noir REPL debugger. For a comprehensive reference, check out [the reference page](../../reference/debugger/debugger_repl.md). + +## Debugging a test function + +Let's debug a simple test: + +```rust +#[noir] +fn test_simple_equal() { + let x = 2; + let y = 1 + 1; + assert(x == y, "should be equal"); +} +``` + +To debug a test function using the REPL debugger, navigate to a Noir project directory inside a terminal, and run the `nargo debug` command passing the `--test-name your_test_name_here` argument. + +```bash +nargo debug --test-name test_simple_equal +``` + +After that, the debugger has started and works the same as debugging a main function, you can use any of the above explained commands to control the execution of the test function. + +### Test result + +The debugger does not end the session automatically. Once you finish debugging the execution of the test function you will notice that the debugger remains in the `Execution finished` state. When you are done debugging the test function you can exit the debugger by using the `quit` command. Once you finish the debugging session you should see the test result. + +```text +$ nargo debug --test-name test_simple_equal +[simple_noir_project] Starting debugger +At opcode 0:0 :: BRILLIG CALL func 0: inputs: [], outputs: [] +> continue +(Continuing execution...) +Finished execution +> quit +[simple_noir_project] Circuit witness successfully solved +[simple_noir_project] Testing test_simple_equal... ok +``` diff --git a/docs/docs/how_to/debugger/debugging_with_vs_code.md b/docs/docs/how_to/debugger/debugging_with_vs_code.md index ecd64fc2653..beb37170c8e 100644 --- a/docs/docs/how_to/debugger/debugging_with_vs_code.md +++ b/docs/docs/how_to/debugger/debugging_with_vs_code.md @@ -13,7 +13,7 @@ keywords: sidebar_position: 0 --- -This guide will show you how to use VS Code with the vscode-noir extension to debug a Noir project. +This guide will show you how to use VS Code with the vscode-noir extension to debug a Noir project. #### Pre-requisites @@ -23,7 +23,15 @@ This guide will show you how to use VS Code with the vscode-noir extension to de ## Running the debugger -The easiest way to start debugging is to open the file you want to debug, and press `F5`. This will cause the debugger to launch, using your `Prover.toml` file as input. +The easiest way to start debugging is to open the file you want to debug, and click on `Debug` codelens over main functions or `Debug test` over `#[test]` functions + +If you don't see the codelens options `Compile|Info|..|Debug` over the `main` function or `Run test| Debug test` over a test function then you probably have the codelens feature disabled. To enable it open the extension configuration page and check the `Enable Code Lens` setting. + +![Debugger codelens](@site/static/img/debugger/debugger-codelens.png) + +Another way of starting the debugger is to press `F5` on the file you want to debug. This will cause the debugger to launch, using your `Prover.toml` file as input. + +Once the debugger has started you should see something like this: You should see something like this: @@ -37,11 +45,11 @@ You will now see two categories of variables: Locals and Witness Map. ![Debug pane expanded](@site/static/img/debugger/3-debug-pane.png) -1. **Locals**: variables of your program. At this point in execution this section is empty, but as we step through the code it will get populated by `x`, `result`, `digest`, etc. +1. **Locals**: variables of your program. At this point in execution this section is empty, but as we step through the code it will get populated by `x`, `result`, `digest`, etc. 2. **Witness map**: these are initially populated from your project's `Prover.toml` file. In this example, they will be used to populate `x` and `result` at the beginning of the `main` function. -Most of the time you will probably be focusing mostly on locals, as they represent the high level state of your program. +Most of the time you will probably be focusing mostly on locals, as they represent the high level state of your program. You might be interested in inspecting the witness map in case you are trying to solve a really low level issue in the compiler or runtime itself, so this concerns mostly advanced or niche users. @@ -57,7 +65,7 @@ We can also inspect the values of variables by directly hovering on them on the ![Hover locals](@site/static/img/debugger/6-hover.png) -Let's set a break point at the `keccak256` function, so we can continue execution up to the point when it's first invoked without having to go one step at a time. +Let's set a break point at the `keccak256` function, so we can continue execution up to the point when it's first invoked without having to go one step at a time. We just need to click to the right of the line number 18. Once the breakpoint appears, we can click the `continue` button or use its corresponding keyboard shortcut (`F5` by default). diff --git a/docs/docs/reference/debugger/debugger_repl.md b/docs/docs/reference/debugger/debugger_repl.md index 46e2011304e..71a201213b8 100644 --- a/docs/docs/reference/debugger/debugger_repl.md +++ b/docs/docs/reference/debugger/debugger_repl.md @@ -1,7 +1,7 @@ --- title: REPL Debugger description: - Noir Debugger REPL options and commands. + Noir Debugger REPL options and commands. keywords: [ Nargo, @@ -20,14 +20,14 @@ Runs the Noir REPL debugger. If a `WITNESS_NAME` is provided the debugger writes ### Options -| Option | Description | -| --------------------- | ------------------------------------------------------------ | +| Option | Description | +| --------------------------------- | ----------------------------------------------------------------------------------- | | `-p, --prover-name ` | The name of the toml file which contains the inputs for the prover [default: Prover]| -| `--package ` | The name of the package to debug | -| `--print-acir` | Display the ACIR for compiled circuit | -| `--deny-warnings` | Treat all warnings as errors | -| `--silence-warnings` | Suppress warnings | -| `-h, --help` | Print help | +| `--package ` | The name of the package to debug | +| `--print-acir` | Display the ACIR for compiled circuit | +| `--test-name ` | The name (or substring) of the test function to debug | +| `--oracle-resolver `| JSON RPC url to solve oracle calls | +| `-h, --help` | Print help | None of these options are required. @@ -35,6 +35,15 @@ None of these options are required. Since the debugger starts by compiling the target package, all Noir compiler options are also available. Check out the [compiler reference](../nargo_commands.md#nargo-compile) to learn more about the compiler options. ::: +:::note +If the `--test-name` option is provided the debugger will debug the matching function instead of the package `main` function. +This argument must only match one function. If the given name matches with more than one test function the debugger will not start. +::: + +:::note +For debugging aztec-contract tests that interact with the TXE ([see further details here](https://docs.aztec.network/developers/guides/smart_contracts/testing)), a JSON RPC server URL must be provided by setting the `--oracle-resolver` option +::: + ## REPL commands Once the debugger is running, it accepts the following commands. @@ -53,6 +62,7 @@ Available commands: out step until a new source location is reached and the current stack frame is finished break LOCATION:OpcodeLocation add a breakpoint at an opcode location + break line:i64 add a breakpoint at an opcode associated to the given source code line over step until a new source location is reached without diving into function calls restart restart the debugging session @@ -94,7 +104,7 @@ Step until the next Noir source code location. While other commands, such as [`i ``` -Using `next` here would cause the debugger to jump to the definition of `deep_entry_point` (if available). +Using `next` here would cause the debugger to jump to the definition of `deep_entry_point` (if available). If you want to step over `deep_entry_point` and go straight to line 8, use [the `over` command](#over) instead. @@ -129,11 +139,11 @@ Step until the end of the current function call. For example: 7 -> assert(deep_entry_point(x) == 4); 8 multiple_values_entry_point(x); 9 } - 10 + 10 11 unconstrained fn returns_multiple_values(x: u32) -> (u32, u32, u32, u32) { 12 ... ... - 55 + 55 56 unconstrained fn deep_entry_point(x: u32) -> u32 { 57 -> level_1(x + 1) 58 } @@ -180,7 +190,7 @@ Steps into the next opcode. A compiled Noir program is a sequence of ACIR opcode ... 1.43 | Return 2 EXPR [ (1, _1) -2 ] -``` +``` The `->` here shows the debugger paused at an ACIR opcode: `BRILLIG`, at index 1, which denotes an unconstrained code block is about to start. @@ -249,6 +259,10 @@ In this example, issuing a `break 1.2` command adds break on opcode 1.2, as deno Running [the `continue` command](#continue-c) at this point would cause the debugger to execute the program until opcode 1.2. +#### `break [line]` (or shorthand `b [line]`) + +Similar to `break [opcode]`, but instead of selecting the opcode by index selects the opcode location by matching the source code location + #### `delete [Opcode]` (or shorthand `d [Opcode]`) Deletes a breakpoint at an opcode location. Usage is analogous to [the `break` command](#). @@ -260,7 +274,7 @@ Deletes a breakpoint at an opcode location. Usage is analogous to [the `break` c Show variable values available at this point in execution. :::note -The ability to inspect variable values from the debugger depends on compilation to be run in a special debug instrumentation mode. This instrumentation weaves variable tracing code with the original source code. +The ability to inspect variable values from the debugger depends on compilation to be run in a special debug instrumentation mode. This instrumentation weaves variable tracing code with the original source code. So variable value inspection comes at the expense of making the resulting ACIR bytecode bigger and harder to understand and optimize. @@ -357,4 +371,4 @@ Update a memory cell with the given value. For example: :::note This command is only functional while the debugger is executing unconstrained code. -::: \ No newline at end of file +::: diff --git a/docs/docs/reference/debugger/debugger_vscode.md b/docs/docs/reference/debugger/debugger_vscode.md index c027332b3b0..5f3baefdc17 100644 --- a/docs/docs/reference/debugger/debugger_vscode.md +++ b/docs/docs/reference/debugger/debugger_vscode.md @@ -17,16 +17,15 @@ sidebar_position: 0 The Noir debugger enabled by the vscode-noir extension ships with default settings such that the most common scenario should run without any additional configuration steps. -These defaults can nevertheless be overridden by defining a launch configuration file. This page provides a reference for the properties you can override via a launch configuration file, as well as documenting the Nargo `dap` command, which is a dependency of the VS Code Noir debugger. - +These defaults can nevertheless be overridden by defining a launch configuration file. This page provides a reference for the properties you can override via a launch configuration file, as well as documenting the Nargo `dap` command, which is a dependency of the VS Code Noir debugger. ## Creating and editing launch configuration files -To create a launch configuration file from VS Code, open the _debug pane_, and click on _create a launch.json file_. +To create a launch configuration file from VS Code, open the _debug pane_, and click on _create a launch.json file_. ![Creating a launch configuration file](@site/static/img/debugger/ref1-create-launch.png) -A `launch.json` file will be created, populated with basic defaults. +A `launch.json` file will be created, populated with basic defaults. ### Noir Debugger launch.json properties @@ -34,7 +33,7 @@ A `launch.json` file will be created, populated with basic defaults. _String, optional._ -Absolute path to the Nargo project to debug. By default, it is dynamically determined by looking for the nearest `Nargo.toml` file to the active file at the moment of launching the debugger. +Absolute path to the Nargo project to debug. By default, it is dynamically determined by looking for the nearest `Nargo.toml` file to the active file at the moment of launching the debugger. #### proverName @@ -47,7 +46,7 @@ Name of the prover input to use. Defaults to `Prover`, which looks for a file na _Boolean, optional._ If true, generate ACIR opcodes instead of unconstrained opcodes which will be closer to release binaries but less convenient for debugging. Defaults to `false`. - + #### skipInstrumentation _Boolean, optional._ @@ -58,11 +57,34 @@ Skips variables debugging instrumentation of code, making debugging less conveni Skipping instrumentation causes the debugger to be unable to inspect local variables. ::: +#### testName + +_String, optional._ + +Test name (or substring) of the test function to debug. The name is not required to match exactly, as long as it's non-ambiguous. +For the debugger to run, only one test function should match the name lookup. + +ie: if there are two test functions `test_simple_assert` and `test_increment`, setting `--test-name test_` will fail with `'test_' matches with more than one test function`. Instead, setting `--test_name test_simple` is not ambiguous, so the debugger will start debugging the `test_simple_assert` test function. + +:::note +When provided, the debugger will debug the matching function instead of the package `main` function. +::: + +#### oracleResolver + +_String, optional._ + +JSON RPC URL to solve oracle calls. + +:::note +When the debugger is run using the `Debug test` codelens, this option is set from the `TXE_TARGET` environment variable value. +::: + ## `nargo dap [OPTIONS]` -When run without any option flags, it starts the Nargo Debug Adapter Protocol server, which acts as the debugging backend for the VS Code Noir Debugger. +When run without any option flags, it starts the Nargo Debug Adapter Protocol server, which acts as the debugging backend for the VS Code Noir Debugger. -All option flags are related to preflight checks. The Debug Adapter Protocol specifies how errors are to be informed from a running DAP server, but it doesn't specify mechanisms to communicate server initialization errors between the DAP server and its client IDE. +All option flags are related to preflight checks. The Debug Adapter Protocol specifies how errors are to be informed from a running DAP server, but it doesn't specify mechanisms to communicate server initialization errors between the DAP server and its client IDE. Thus `nargo dap` ships with a _preflight check_ mode. If flag `--preflight-check` and the rest of the `--preflight-*` flags are provided, Nargo will run the same initialization routine except it will not start the DAP server. @@ -72,11 +94,11 @@ If the preflight check succeeds, `vscode-noir` proceeds to start the DAP server ### Options -| Option | Description | -| --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | -| `--preflight-check` | If present, dap runs in preflight check mode. | -| `--preflight-project-folder ` | Absolute path to the project to debug for preflight check. | -| `--preflight-prover-name ` | Name of prover file to use for preflight check | -| `--preflight-generate-acir` | Optional. If present, compile in ACIR mode while running preflight check. | -| `--preflight-skip-instrumentation` | Optional. If present, compile without introducing debug instrumentation while running preflight check. | -| `-h, --help` | Print help. | +| Option | Description | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | +| `--preflight-check` | If present, dap runs in preflight check mode. | +| `--preflight-project-folder ` | Absolute path to the project to debug for preflight check. | +| `--preflight-prover-name ` | Name of prover file to use for preflight check | +| `--preflight-generate-acir` | Optional. If present, compile in ACIR mode while running preflight check. | +| `--preflight-skip-instrumentation` | Optional. If present, compile without introducing debug instrumentation while running preflight check. | +| `-h, --help` | Print help. | diff --git a/docs/docs/tooling/debugger.md b/docs/docs/tooling/debugger.md index 200b5fc423a..e00a5e744e9 100644 --- a/docs/docs/tooling/debugger.md +++ b/docs/docs/tooling/debugger.md @@ -17,10 +17,6 @@ In order to use either version of the debugger, you will need to install recent - Noir & Nargo ≥0.28.0 - Noir's VS Code extension ≥0.0.11 -:::info -At the moment, the debugger supports debugging binary projects, but not contracts. -::: - We cover the VS Code Noir debugger more in depth in [its VS Code debugger how-to guide](../how_to/debugger/debugging_with_vs_code.md) and [the reference](../reference/debugger/debugger_vscode.md). The REPL debugger is discussed at length in [the REPL debugger how-to guide](../how_to/debugger/debugging_with_the_repl.md) and [the reference](../reference/debugger/debugger_repl.md). diff --git a/docs/static/img/debugger/debugger-codelens.png b/docs/static/img/debugger/debugger-codelens.png new file mode 100644 index 00000000000..a71f5f02a73 Binary files /dev/null and b/docs/static/img/debugger/debugger-codelens.png differ diff --git a/tooling/debugger/Cargo.toml b/tooling/debugger/Cargo.toml index b9b83d86836..6bebdd04d15 100644 --- a/tooling/debugger/Cargo.toml +++ b/tooling/debugger/Cargo.toml @@ -16,7 +16,7 @@ build-data.workspace = true [dependencies] acvm.workspace = true fm.workspace = true -nargo.workspace = true +nargo = { workspace = true, features = ["rpc"] } noirc_frontend = { workspace = true, features = ["bn254"] } noirc_printable_type.workspace = true noirc_errors.workspace = true @@ -28,6 +28,8 @@ dap.workspace = true easy-repl = "0.2.1" owo-colors = "3" serde_json.workspace = true +bn254_blackbox_solver.workspace = true + [dev-dependencies] assert_cmd = "2.0.12" diff --git a/tooling/debugger/build.rs b/tooling/debugger/build.rs index 4d75cdd4b57..204957ae987 100644 --- a/tooling/debugger/build.rs +++ b/tooling/debugger/build.rs @@ -1,8 +1,7 @@ use std::collections::HashSet; use std::fs::File; -use std::io::Write; +use std::io::{BufRead, BufReader, Write}; use std::path::{Path, PathBuf}; - const GIT_COMMIT: &&str = &"GIT_COMMIT"; fn main() { @@ -33,14 +32,17 @@ fn main() { println!("cargo:rerun-if-changed={}", test_dir.as_os_str().to_str().unwrap()); generate_debugger_tests(&mut test_file, &test_dir); + generate_test_runner_debugger_tests(&mut test_file, &test_dir); } fn generate_debugger_tests(test_file: &mut File, test_data_dir: &Path) { let test_sub_dir = "execution_success"; let test_data_dir = test_data_dir.join(test_sub_dir); - let test_case_dirs = - std::fs::read_dir(test_data_dir).unwrap().flatten().filter(|c| c.path().is_dir()); + let test_case_dirs = std::fs::read_dir(test_data_dir) + .unwrap() + .flatten() + .filter(|c| c.path().is_dir() && c.path().join("Nargo.toml").exists()); let ignored_tests_contents = std::fs::read_to_string("ignored-tests.txt").unwrap(); let ignored_tests = ignored_tests_contents.lines().collect::>(); @@ -70,3 +72,67 @@ fn debug_{test_name}() {{ .expect("Could not write templated test file."); } } + +fn generate_test_runner_debugger_tests(test_file: &mut File, test_data_dir: &Path) { + let test_sub_dir = "noir_test_success"; + let test_data_dir = test_data_dir.join(test_sub_dir); + + let test_case_dirs = std::fs::read_dir(test_data_dir) + .unwrap() + .flatten() + .filter(|c| c.path().is_dir() && c.path().join("Nargo.toml").exists()); + let ignored_tests_contents = std::fs::read_to_string("ignored-noir-tests.txt").unwrap(); + let ignored_tests = ignored_tests_contents.lines().collect::>(); + + for test_dir in test_case_dirs { + let test_file_name = + test_dir.file_name().into_string().expect("Directory can't be converted to string"); + if test_file_name.contains('-') { + panic!( + "Invalid test directory: {test_file_name}. Cannot include `-`, please convert to `_`" + ); + }; + let test_dir = &test_dir.path(); + + let file_name = test_dir.join("src").join("main.nr"); + let buf_reader = + BufReader::new(File::open(file_name.clone()).expect("Could not open file")); + let lines = buf_reader.lines(); + let test_names: Vec = lines + .filter_map(|line_res| { + line_res.ok().map(|line| if line.contains("fn test_") { Some(line) } else { None }) + }) + .flatten() + .collect(); + for test_name_line in test_names { + // TODO: get test name by regex perhaps? + let test_name = test_name_line + .split("fn ") + .collect::>() + .get(1) + .unwrap() + .split("<") + .next() + .unwrap() + .split("(") + .next() + .unwrap(); + + let ignored = ignored_tests.contains(test_name); + + write!( + test_file, + r#" + #[test] + {ignored} + fn debug_test_{test_file_name}_{test_name}() {{ + debugger_test_success("{test_dir}", "{test_name}"); + }} + "#, + test_dir = test_dir.display(), + ignored = if ignored { "#[ignore]" } else { "" }, + ) + .expect("Could not write templated test file."); + } + } +} diff --git a/tooling/debugger/ignored-noir-tests.txt b/tooling/debugger/ignored-noir-tests.txt new file mode 100644 index 00000000000..c32ecdc23f7 --- /dev/null +++ b/tooling/debugger/ignored-noir-tests.txt @@ -0,0 +1,12 @@ +test_assert_message_preserved_during_optimization +test_vec_new_bad +test_identity +test_identity_and_show +test_logic +test_logic_and_show +test_numeric +test_numeric_and_show +test_pow +test_pow_and_show +test_add +test_add_and_show diff --git a/tooling/debugger/src/context.rs b/tooling/debugger/src/context.rs index 79e03672e8d..01a9178c7d2 100644 --- a/tooling/debugger/src/context.rs +++ b/tooling/debugger/src/context.rs @@ -13,14 +13,16 @@ use acvm::{BlackBoxFunctionSolver, FieldElement}; use codespan_reporting::files::{Files, SimpleFile}; use fm::FileId; use nargo::NargoError; -use nargo::errors::{ExecutionError, Location}; +use nargo::errors::{ExecutionError, Location, ResolvedOpcodeLocation, execution_error_from}; use noirc_artifacts::debug::{DebugArtifact, StackFrame}; -use noirc_driver::DebugFile; +use noirc_driver::{CompiledProgram, DebugFile}; +use noirc_printable_type::{PrintableType, PrintableValue}; use thiserror::Error; use std::collections::BTreeMap; -use std::collections::{HashSet, hash_set::Iter}; +use std::collections::HashSet; +use std::path::PathBuf; /// A Noir program is composed by /// `n` ACIR circuits @@ -189,6 +191,15 @@ impl std::fmt::Display for DebugLocation { } } +impl From for ResolvedOpcodeLocation { + fn from(debug_loc: DebugLocation) -> Self { + ResolvedOpcodeLocation { + acir_function_index: usize::try_from(debug_loc.circuit_id).unwrap(), + opcode_location: debug_loc.opcode_location, + } + } +} + #[derive(Error, Debug)] pub enum DebugLocationFromStrError { #[error("Invalid debug location string: {0}")] @@ -227,11 +238,63 @@ pub(super) enum DebugCommandResult { Error(NargoError), } +#[derive(Debug)] +pub struct DebugStackFrame { + pub function_name: String, + pub function_params: Vec, + pub variables: Vec<(String, PrintableValue, PrintableType)>, +} + +impl From<&StackFrame<'_, F>> for DebugStackFrame { + fn from(value: &StackFrame) -> Self { + DebugStackFrame { + function_name: value.function_name.to_string(), + function_params: value.function_params.iter().map(|param| param.to_string()).collect(), + variables: value + .variables + .iter() + .map(|(name, value, var_type)| { + (name.to_string(), (**value).clone(), (*var_type).clone()) + }) + .collect(), + } + } +} + pub struct ExecutionFrame<'a, B: BlackBoxFunctionSolver> { circuit_id: u32, acvm: ACVM<'a, FieldElement, B>, } +#[derive(Debug)] +pub enum DebugExecutionResult { + Solved(WitnessStack), + Incomplete, + Error(NargoError), +} + +#[derive(Debug, Clone)] +pub struct DebugProject { + pub compiled_program: CompiledProgram, + pub initial_witness: WitnessMap, + pub root_dir: PathBuf, + pub package_name: String, +} + +#[derive(Debug, Clone)] + +pub struct RunParams { + /// Use pedantic ACVM solving + pub pedantic_solving: bool, + + /// Option for configuring the source_code_printer + /// This option only applies for the Repl interface + pub raw_source_printing: Option, + + /// JSON RPC url to solve oracle calls + pub oracle_resolver_url: Option, +} + pub(super) struct DebugContext<'a, B: BlackBoxFunctionSolver> { pub(crate) acvm: ACVM<'a, FieldElement, B>, current_circuit_id: u32, @@ -251,6 +314,25 @@ pub(super) struct DebugContext<'a, B: BlackBoxFunctionSolver> { unconstrained_functions: &'a [BrilligBytecode], acir_opcode_addresses: AddressMap, + initial_witness: WitnessMap, +} + +fn initialize_acvm<'a, B: BlackBoxFunctionSolver>( + backend: &'a B, + circuits: &'a [Circuit], + initial_witness: WitnessMap, + unconstrained_functions: &'a [BrilligBytecode], +) -> ACVM<'a, FieldElement, B> { + let current_circuit_id: u32 = 0; + let initial_circuit = &circuits[current_circuit_id as usize]; + + ACVM::new( + backend, + &initial_circuit.opcodes, + initial_witness, + unconstrained_functions, + &initial_circuit.assert_messages, + ) } impl<'a, B: BlackBoxFunctionSolver> DebugContext<'a, B> { @@ -264,16 +346,8 @@ impl<'a, B: BlackBoxFunctionSolver> DebugContext<'a, B> { ) -> Self { let source_to_opcodes = build_source_to_opcode_debug_mappings(debug_artifact); let current_circuit_id: u32 = 0; - let initial_circuit = &circuits[current_circuit_id as usize]; let acir_opcode_addresses = AddressMap::new(circuits, unconstrained_functions); Self { - acvm: ACVM::new( - blackbox_solver, - &initial_circuit.opcodes, - initial_witness, - unconstrained_functions, - &initial_circuit.assert_messages, - ), current_circuit_id, brillig_solver: None, witness_stack: WitnessStack::default(), @@ -286,6 +360,13 @@ impl<'a, B: BlackBoxFunctionSolver> DebugContext<'a, B> { circuits, unconstrained_functions, acir_opcode_addresses, + initial_witness: initial_witness.clone(), // we keep it so the context can restart itself + acvm: initialize_acvm( + blackbox_solver, + circuits, + initial_witness, + unconstrained_functions, + ), } } @@ -415,6 +496,12 @@ impl<'a, B: BlackBoxFunctionSolver> DebugContext<'a, B> { Some(found_location) } + pub(super) fn find_opcode_at_current_file_line(&self, line: i64) -> Option { + let file = self.get_current_file()?; + + self.find_opcode_for_source_location(&file, line) + } + /// 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 @@ -430,7 +517,7 @@ impl<'a, B: BlackBoxFunctionSolver> DebugContext<'a, B> { } /// Returns the `FileId` of the file associated with the innermost function on the call stack. - pub(super) fn get_current_file(&mut self) -> Option { + fn get_current_file(&self) -> Option { self.get_current_source_location() .and_then(|locations| locations.last().map(|location| location.file)) } @@ -539,9 +626,17 @@ impl<'a, B: BlackBoxFunctionSolver> DebugContext<'a, B> { self.brillig_solver = Some(solver); self.handle_foreign_call(foreign_call) } - Err(err) => DebugCommandResult::Error(NargoError::ExecutionError( - ExecutionError::SolvingError(err, None), - )), + Err(err) => { + let error = execution_error_from( + err, + &self + .get_call_stack() + .into_iter() + .map(|op| op.into()) + .collect::>(), + ); + DebugCommandResult::Error(NargoError::ExecutionError(error)) + } } } @@ -550,6 +645,7 @@ impl<'a, B: BlackBoxFunctionSolver> DebugContext<'a, B> { foreign_call: ForeignCallWaitInfo, ) -> DebugCommandResult { let foreign_call_result = self.foreign_call_executor.execute(&foreign_call); + match foreign_call_result { Ok(foreign_call_result) => { if let Some(mut solver) = self.brillig_solver.take() { @@ -844,10 +940,6 @@ impl<'a, B: BlackBoxFunctionSolver> DebugContext<'a, B> { self.breakpoints.remove(location) } - pub(super) fn iterate_breakpoints(&self) -> Iter<'_, DebugLocation> { - self.breakpoints.iter() - } - pub(super) fn clear_breakpoints(&mut self) { self.breakpoints.clear(); } @@ -861,6 +953,22 @@ impl<'a, B: BlackBoxFunctionSolver> DebugContext<'a, B> { self.witness_stack.push(0, last_witness_map); self.witness_stack } + + pub(super) fn restart(&mut self) { + // restart everything that's progress related + // by assigning the initial values + self.current_circuit_id = 0; + self.brillig_solver = None; + self.witness_stack = WitnessStack::default(); + self.acvm_stack = vec![]; + self.foreign_call_executor.restart(self.debug_artifact); + self.acvm = initialize_acvm( + self.backend, + self.circuits, + self.initial_witness.clone(), + self.unconstrained_functions, + ); + } } fn is_debug_file_in_debug_crate(debug_file: &DebugFile) -> bool { @@ -1035,9 +1143,12 @@ mod tests { let foreign_call_executor = Box::new(DefaultDebugForeignCallExecutor::from_artifact( PrintOutput::Stdout, + None, debug_artifact, + None, + String::new(), )); - let mut context = DebugContext::new( + let mut context = DebugContext::::new( &solver, circuits, debug_artifact, @@ -1203,10 +1314,13 @@ mod tests { let foreign_call_executor = Box::new(DefaultDebugForeignCallExecutor::from_artifact( PrintOutput::Stdout, + None, debug_artifact, + None, + String::new(), )); let brillig_funcs = &[brillig_bytecode]; - let mut context = DebugContext::new( + let mut context = DebugContext::::new( &solver, circuits, debug_artifact, @@ -1294,12 +1408,17 @@ mod tests { let debug_artifact = DebugArtifact { debug_symbols: vec![], file_map: BTreeMap::new() }; let brillig_funcs = &[brillig_one, brillig_two]; - let context = DebugContext::new( + let context = DebugContext::::new( &solver, &circuits, &debug_artifact, WitnessMap::new(), - Box::new(DefaultDebugForeignCallExecutor::new(PrintOutput::Stdout)), + Box::new(DefaultDebugForeignCallExecutor::new( + PrintOutput::Stdout, + None, + None, + String::new(), + )), brillig_funcs, ); diff --git a/tooling/debugger/src/dap.rs b/tooling/debugger/src/dap.rs index 1df27d8ea6f..213e97bdb88 100644 --- a/tooling/debugger/src/dap.rs +++ b/tooling/debugger/src/dap.rs @@ -1,14 +1,13 @@ use std::collections::BTreeMap; use std::io::{Read, Write}; -use acvm::acir::circuit::Circuit; -use acvm::acir::circuit::brillig::BrilligBytecode; -use acvm::acir::native_types::WitnessMap; use acvm::{BlackBoxFunctionSolver, FieldElement}; -use nargo::PrintOutput; +use bn254_blackbox_solver::Bn254BlackBoxSolver; +use nargo::{NargoError, PrintOutput}; -use crate::context::DebugContext; -use crate::context::{DebugCommandResult, DebugLocation}; +use crate::DebugProject; +use crate::context::{DebugCommandResult, DebugLocation, RunParams}; +use crate::context::{DebugContext, DebugExecutionResult}; use crate::foreign_calls::DefaultDebugForeignCallExecutor; use dap::errors::ServerError; @@ -28,18 +27,18 @@ use dap::types::{ use noirc_artifacts::debug::DebugArtifact; use fm::FileId; -use noirc_driver::CompiledProgram; type BreakpointId = i64; pub struct DapSession<'a, R: Read, W: Write, B: BlackBoxFunctionSolver> { - server: Server, + server: &'a mut Server, context: DebugContext<'a, B>, debug_artifact: &'a DebugArtifact, running: bool, next_breakpoint_id: BreakpointId, instruction_breakpoints: Vec<(DebugLocation, BreakpointId)>, source_breakpoints: BTreeMap>, + last_result: DebugCommandResult, } enum ScopeReferences { @@ -60,23 +59,25 @@ impl From for ScopeReferences { impl<'a, R: Read, W: Write, B: BlackBoxFunctionSolver> DapSession<'a, R, W, B> { pub fn new( - server: Server, + server: &'a mut Server, solver: &'a B, - circuits: &'a [Circuit], + project: &'a DebugProject, debug_artifact: &'a DebugArtifact, - initial_witness: WitnessMap, - unconstrained_functions: &'a [BrilligBytecode], + foreign_call_resolver_url: Option, ) -> Self { let context = DebugContext::new( solver, - circuits, + &project.compiled_program.program.functions, debug_artifact, - initial_witness, + project.initial_witness.clone(), Box::new(DefaultDebugForeignCallExecutor::from_artifact( PrintOutput::Stdout, + foreign_call_resolver_url, debug_artifact, + Some(project.root_dir.clone()), + project.package_name.clone(), )), - unconstrained_functions, + &project.compiled_program.program.unconstrained_functions, ); Self { server, @@ -86,6 +87,7 @@ impl<'a, R: Read, W: Write, B: BlackBoxFunctionSolver> DapSession< next_breakpoint_id: 1, instruction_breakpoints: vec![], source_breakpoints: BTreeMap::new(), + last_result: DebugCommandResult::Ok, } } @@ -125,7 +127,7 @@ impl<'a, R: Read, W: Write, B: BlackBoxFunctionSolver> DapSession< match req.command { Command::Disconnect(_) => { eprintln!("INFO: ending debugging session"); - self.server.respond(req.ack()?)?; + self.running = false; break; } Command::SetBreakpoints(_) => { @@ -342,7 +344,8 @@ impl<'a, R: Read, W: Write, B: BlackBoxFunctionSolver> DapSession< } fn handle_execution_result(&mut self, result: DebugCommandResult) -> Result<(), ServerError> { - match result { + self.last_result = result; + match &self.last_result { DebugCommandResult::Done => { self.running = false; } @@ -358,7 +361,7 @@ impl<'a, R: Read, W: Write, B: BlackBoxFunctionSolver> DapSession< }))?; } DebugCommandResult::BreakpointReached(location) => { - let breakpoint_ids = self.find_breakpoints_at_location(&location); + let breakpoint_ids = self.find_breakpoints_at_location(location); self.server.send_event(Event::Stopped(StoppedEventBody { reason: StoppedEventReason::Breakpoint, description: Some(String::from("Paused at breakpoint")), @@ -369,17 +372,7 @@ impl<'a, R: Read, W: Write, B: BlackBoxFunctionSolver> DapSession< hit_breakpoint_ids: Some(breakpoint_ids), }))?; } - DebugCommandResult::Error(err) => { - self.server.send_event(Event::Stopped(StoppedEventBody { - reason: StoppedEventReason::Exception, - description: Some(format!("{err:?}")), - thread_id: Some(0), - preserve_focus_hint: Some(false), - text: None, - all_threads_stopped: Some(false), - hit_breakpoint_ids: None, - }))?; - } + DebugCommandResult::Error(_) => self.server.send_event(Event::Terminated(None))?, } Ok(()) } @@ -604,23 +597,38 @@ impl<'a, R: Read, W: Write, B: BlackBoxFunctionSolver> DapSession< .respond(req.success(ResponseBody::Variables(VariablesResponse { variables })))?; Ok(()) } + + pub fn last_error(self) -> Option> { + match self.last_result { + DebugCommandResult::Error(error) => Some(error), + _ => None, + } + } } -pub fn run_session>( - server: Server, - solver: &B, - program: CompiledProgram, - initial_witness: WitnessMap, -) -> Result<(), ServerError> { - let debug_artifact = DebugArtifact { debug_symbols: program.debug, file_map: program.file_map }; - let mut session = DapSession::new( - server, - solver, - &program.program.functions, - &debug_artifact, - initial_witness, - &program.program.unconstrained_functions, - ); - - session.run_loop() +pub fn run_session( + server: &mut Server, + project: DebugProject, + run_params: RunParams, +) -> Result { + let debug_artifact = DebugArtifact { + debug_symbols: project.compiled_program.debug.clone(), + file_map: project.compiled_program.file_map.clone(), + }; + + let solver = Bn254BlackBoxSolver(run_params.pedantic_solving); + let mut session = + DapSession::new(server, &solver, &project, &debug_artifact, run_params.oracle_resolver_url); + + session.run_loop()?; + if session.context.is_solved() { + let solved_witness_stack = session.context.finalize(); + Ok(DebugExecutionResult::Solved(solved_witness_stack)) + } else { + match session.last_error() { + // Expose the last known error + Some(error) => Ok(DebugExecutionResult::Error(error)), + None => Ok(DebugExecutionResult::Incomplete), + } + } } diff --git a/tooling/debugger/src/foreign_calls.rs b/tooling/debugger/src/foreign_calls.rs index efae3df407a..ff29ca86e4f 100644 --- a/tooling/debugger/src/foreign_calls.rs +++ b/tooling/debugger/src/foreign_calls.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use acvm::{ AcirField, FieldElement, acir::brillig::{ForeignCallParam, ForeignCallResult}, @@ -43,6 +45,7 @@ impl DebugForeignCall { pub trait DebugForeignCallExecutor: ForeignCallExecutor { fn get_variables(&self) -> Vec>; fn current_stack_frame(&self) -> Option>; + fn restart(&mut self, artifact: &DebugArtifact); } #[derive(Default)] @@ -53,23 +56,42 @@ pub struct DefaultDebugForeignCallExecutor { impl DefaultDebugForeignCallExecutor { fn make( output: PrintOutput<'_>, + resolver_url: Option, ex: DefaultDebugForeignCallExecutor, + root_path: Option, + package_name: String, ) -> impl DebugForeignCallExecutor + '_ { - DefaultForeignCallBuilder::default().with_output(output).build().add_layer(ex) + DefaultForeignCallBuilder { + output, + enable_mocks: true, + resolver_url, + root_path: root_path.clone(), + package_name: Some(package_name), + } + .build() + .add_layer(ex) } #[allow(clippy::new_ret_no_self, dead_code)] - pub fn new(output: PrintOutput<'_>) -> impl DebugForeignCallExecutor + '_ { - Self::make(output, Self::default()) + pub fn new( + output: PrintOutput<'_>, + resolver_url: Option, + root_path: Option, + package_name: String, + ) -> impl DebugForeignCallExecutor + '_ { + Self::make(output, resolver_url, Self::default(), root_path, package_name) } pub fn from_artifact<'a>( output: PrintOutput<'a>, + resolver_url: Option, artifact: &DebugArtifact, + root_path: Option, + package_name: String, ) -> impl DebugForeignCallExecutor + use<'a> { let mut ex = Self::default(); ex.load_artifact(artifact); - Self::make(output, ex) + Self::make(output, resolver_url, ex, root_path, package_name) } pub fn load_artifact(&mut self, artifact: &DebugArtifact) { @@ -90,6 +112,11 @@ impl DebugForeignCallExecutor for DefaultDebugForeignCallExecutor { fn current_stack_frame(&self) -> Option> { self.debug_vars.current_stack_frame() } + + fn restart(&mut self, artifact: &DebugArtifact) { + self.debug_vars = DebugVars::default(); + self.load_artifact(artifact); + } } fn debug_var_id(value: &FieldElement) -> DebugVarId { @@ -192,4 +219,7 @@ where fn current_stack_frame(&self) -> Option> { self.handler().current_stack_frame() } + fn restart(&mut self, artifact: &DebugArtifact) { + self.handler.restart(artifact); + } } diff --git a/tooling/debugger/src/lib.rs b/tooling/debugger/src/lib.rs index f0dc859beb3..5e8384dfeac 100644 --- a/tooling/debugger/src/lib.rs +++ b/tooling/debugger/src/lib.rs @@ -9,26 +9,19 @@ use std::io::{Read, Write}; use ::dap::errors::ServerError; use ::dap::server::Server; -use acvm::acir::native_types::{WitnessMap, WitnessStack}; -use acvm::{BlackBoxFunctionSolver, FieldElement}; +// TODO: extract these pub structs to its own module +pub use context::DebugExecutionResult; +pub use context::DebugProject; +pub use context::RunParams; -use nargo::NargoError; -use noirc_driver::CompiledProgram; - -pub fn run_repl_session>( - solver: &B, - program: CompiledProgram, - initial_witness: WitnessMap, - raw_source_printing: bool, -) -> Result>, NargoError> { - repl::run(solver, program, initial_witness, raw_source_printing) +pub fn run_repl_session(project: DebugProject, run_params: RunParams) -> DebugExecutionResult { + repl::run(project, run_params) } -pub fn run_dap_loop>( - server: Server, - solver: &B, - program: CompiledProgram, - initial_witness: WitnessMap, -) -> Result<(), ServerError> { - dap::run_session(server, solver, program, initial_witness) +pub fn run_dap_loop( + server: &mut Server, + project: DebugProject, + run_params: RunParams, +) -> Result { + dap::run_session(server, project, run_params) } diff --git a/tooling/debugger/src/repl.rs b/tooling/debugger/src/repl.rs index 08156146985..e7456604c62 100644 --- a/tooling/debugger/src/repl.rs +++ b/tooling/debugger/src/repl.rs @@ -1,90 +1,220 @@ -use crate::context::{DebugCommandResult, DebugContext, DebugLocation}; - -use acvm::AcirField; -use acvm::acir::brillig::BitSize; -use acvm::acir::circuit::brillig::{BrilligBytecode, BrilligFunctionId}; -use acvm::acir::circuit::{Circuit, Opcode, OpcodeLocation}; -use acvm::acir::native_types::{Witness, WitnessMap, WitnessStack}; -use acvm::brillig_vm::MemoryValue; -use acvm::brillig_vm::brillig::Opcode as BrilligOpcode; -use acvm::{BlackBoxFunctionSolver, FieldElement}; -use nargo::{NargoError, PrintOutput}; +use crate::DebugProject; +use crate::context::{ + DebugCommandResult, DebugContext, DebugExecutionResult, DebugLocation, DebugStackFrame, + RunParams, +}; +use nargo::PrintOutput; use noirc_driver::CompiledProgram; use crate::foreign_calls::DefaultDebugForeignCallExecutor; use noirc_artifacts::debug::DebugArtifact; use easy_repl::{CommandStatus, Repl, command}; -use noirc_printable_type::PrintableValueDisplay; use std::cell::RefCell; +use std::sync::mpsc::{self, Receiver, Sender}; +use std::thread; + +use acvm::brillig_vm::brillig::Opcode as BrilligOpcode; +use acvm::{ + AcirField, FieldElement, + acir::{ + brillig::BitSize, + circuit::{ + Circuit, Opcode, OpcodeLocation, + brillig::{BrilligBytecode, BrilligFunctionId}, + }, + native_types::{Witness, WitnessMap}, + }, + brillig_vm::MemoryValue, +}; +use bn254_blackbox_solver::Bn254BlackBoxSolver; +use noirc_printable_type::PrintableValueDisplay; -use crate::source_code_printer::print_source_code_location; +use crate::{ + foreign_calls::DebugForeignCallExecutor, source_code_printer::print_source_code_location, +}; + +type Context<'a> = DebugContext<'a, Bn254BlackBoxSolver>; + +#[derive(Debug, Clone)] +pub(super) enum DebugCommandAPI { + AddBreakpoint(DebugLocation), + AddBreakpointAtLine(i64), + DeleteBreakpoint(DebugLocation), + Restart, + StepAcirOpcode, + StepIntoOpcode, + NextInto, + NextOver, + NextOut, + Cont, + UpdateWitness(u32, String), + WriteBrilligMemory(usize, String, u32), + ShowVariables, + ShowWitnessMap, + ShowWitness(u32), + ShowBrilligMemory, + ShowCurrentCallStack, + ShowCurrentVmStatus, + ShowOpcodes, + Terminate, +} -pub struct ReplDebugger<'a, B: BlackBoxFunctionSolver> { - context: DebugContext<'a, B>, - blackbox_solver: &'a B, +#[derive(Debug)] +pub(super) enum DebuggerStatus { + Idle, + Busy, + Final(DebugExecutionResult), +} + +pub struct AsyncReplDebugger<'a> { + circuits: Vec>, debug_artifact: &'a DebugArtifact, initial_witness: WitnessMap, + unconstrained_functions: Vec>, + command_receiver: Receiver, + status_sender: Sender, last_result: DebugCommandResult, - - // ACIR functions to debug - circuits: &'a [Circuit], - - // Brillig functions referenced from the ACIR circuits above - unconstrained_functions: &'a [BrilligBytecode], - - // whether to print the source without highlighting, pretty-printing, - // or line numbers + pedantic_solving: bool, raw_source_printing: bool, } -impl<'a, B: BlackBoxFunctionSolver> ReplDebugger<'a, B> { +impl<'a> AsyncReplDebugger<'a> { pub fn new( - blackbox_solver: &'a B, - circuits: &'a [Circuit], + compiled_program: &CompiledProgram, debug_artifact: &'a DebugArtifact, initial_witness: WitnessMap, - unconstrained_functions: &'a [BrilligBytecode], + status_sender: Sender, + command_receiver: Receiver, raw_source_printing: bool, + pedantic_solving: bool, ) -> Self { - let foreign_call_executor = Box::new(DefaultDebugForeignCallExecutor::from_artifact( - PrintOutput::Stdout, + let last_result = DebugCommandResult::Ok; + + Self { + command_receiver, + status_sender, + circuits: compiled_program.program.functions.clone(), debug_artifact, - )); - let context = DebugContext::new( + last_result, + unconstrained_functions: compiled_program.program.unconstrained_functions.clone(), + raw_source_printing, + initial_witness, + pedantic_solving, + } + } + + fn send_status(&mut self, status: DebuggerStatus) { + self.status_sender.send(status).expect("Downstream channel closed") + } + + pub(super) fn start_debugging( + mut self, + foreign_call_executor: Box, + ) { + let blackbox_solver = &Bn254BlackBoxSolver(self.pedantic_solving); + let circuits = &self.circuits.clone(); + let unconstrained_functions = &self.unconstrained_functions.clone(); + let mut context = DebugContext::new( blackbox_solver, circuits, - debug_artifact, - initial_witness.clone(), + self.debug_artifact, + self.initial_witness.clone(), foreign_call_executor, unconstrained_functions, ); - let last_result = if context.get_current_debug_location().is_none() { + + if context.get_current_debug_location().is_none() { // handle circuit with no opcodes - DebugCommandResult::Done - } else { - DebugCommandResult::Ok - }; - Self { - context, - blackbox_solver, - circuits, - debug_artifact, - initial_witness, - last_result, - unconstrained_functions, - raw_source_printing, + self.last_result = DebugCommandResult::Done + } + + println!("Debugger ready to receive messages.."); + loop { + self.send_status(DebuggerStatus::Idle); + // recv blocks until it receives message + if let Ok(received) = self.command_receiver.recv() { + self.send_status(DebuggerStatus::Busy); + match received { + DebugCommandAPI::AddBreakpoint(debug_location) => { + Self::add_breakpoint_at(&mut context, debug_location); + } + DebugCommandAPI::DeleteBreakpoint(debug_location) => { + Self::delete_breakpoint_at(&mut context, debug_location); + } + DebugCommandAPI::Restart => { + self.restart_session(&mut context); + } + DebugCommandAPI::WriteBrilligMemory(index, value, bit_size) => { + Self::write_brillig_memory(&mut context, index, value, bit_size); + } + DebugCommandAPI::UpdateWitness(index, value) => { + Self::update_witness(&mut context, index, value); + } + DebugCommandAPI::StepAcirOpcode => { + self.handle_step(&mut context, |context| context.step_acir_opcode()); + } + DebugCommandAPI::StepIntoOpcode => { + self.handle_step(&mut context, |context| context.step_into_opcode()) + } + DebugCommandAPI::NextInto => { + self.handle_step(&mut context, |context| context.next_into()) + } + DebugCommandAPI::NextOver => { + self.handle_step(&mut context, |context| context.next_over()) + } + DebugCommandAPI::NextOut => { + self.handle_step(&mut context, |context| context.next_out()) + } + DebugCommandAPI::Cont => self.handle_step(&mut context, |context| { + println!("(Continuing execution...)"); + context.cont() + }), + DebugCommandAPI::AddBreakpointAtLine(line_number) => { + Self::add_breakpoint_at_line(&mut context, line_number); + } + DebugCommandAPI::ShowVariables => { + Self::show_variables(&mut context); + } + DebugCommandAPI::ShowWitnessMap => { + Self::show_witness_map(&mut context); + } + DebugCommandAPI::ShowWitness(index) => { + Self::show_witness(&mut context, index); + } + DebugCommandAPI::ShowBrilligMemory => { + Self::show_brillig_memory(&mut context); + } + DebugCommandAPI::ShowCurrentCallStack => { + self.show_current_call_stack(&mut context); + } + DebugCommandAPI::ShowOpcodes => { + self.show_opcodes(&mut context); + } + DebugCommandAPI::ShowCurrentVmStatus => { + self.show_current_vm_status(&mut context); + } + DebugCommandAPI::Terminate => { + self.terminate(context); + break; + } + }; + } else { + println!("Upstream channel closed. Terminating debugger"); + break; + } } } - pub fn show_current_vm_status(&self) { - let location = self.context.get_current_debug_location(); + fn show_current_vm_status(&self, context: &mut Context<'_>) { + let location = context.get_current_debug_location(); match location { None => println!("Finished execution"), Some(location) => { let circuit_id = location.circuit_id; - let opcodes = self.context.get_opcodes_of_circuit(circuit_id); + let opcodes = context.get_opcodes_of_circuit(circuit_id); + match &location.opcode_location { OpcodeLocation::Acir(ip) => { println!("At opcode {} :: {}", location, opcodes[*ip]); @@ -102,7 +232,8 @@ impl<'a, B: BlackBoxFunctionSolver> ReplDebugger<'a, B> { ); } } - let locations = self.context.get_source_location_for_debug_location(&location); + let locations = context.get_source_location_for_debug_location(&location); + print_source_code_location( self.debug_artifact, &locations, @@ -112,8 +243,13 @@ impl<'a, B: BlackBoxFunctionSolver> ReplDebugger<'a, B> { } } - fn show_stack_frame(&self, index: usize, debug_location: &DebugLocation) { - let opcodes = self.context.get_opcodes(); + fn show_stack_frame( + &self, + context: &mut Context<'_>, + index: usize, + debug_location: &DebugLocation, + ) { + let opcodes = context.get_opcodes(); match &debug_location.opcode_location { OpcodeLocation::Acir(instruction_pointer) => { println!( @@ -134,38 +270,39 @@ impl<'a, B: BlackBoxFunctionSolver> ReplDebugger<'a, B> { ); } } - let locations = self.context.get_source_location_for_debug_location(debug_location); + let locations = context.get_source_location_for_debug_location(debug_location); print_source_code_location(self.debug_artifact, &locations, self.raw_source_printing); } - pub fn show_current_call_stack(&self) { - let call_stack = self.context.get_call_stack(); + fn show_current_call_stack(&mut self, context: &mut Context<'_>) { + let call_stack = context.get_call_stack(); + if call_stack.is_empty() { println!("Finished execution. Call stack empty."); return; } for (i, frame_location) in call_stack.iter().enumerate() { - self.show_stack_frame(i, frame_location); + self.show_stack_frame(context, i, frame_location); } } - fn display_opcodes(&self) { + fn show_opcodes(&mut self, context: &mut Context<'_>) { for i in 0..self.circuits.len() { - self.display_opcodes_of_circuit(i as u32); + self.show_opcodes_of_circuit(context, i as u32); } } - fn display_opcodes_of_circuit(&self, circuit_id: u32) { + fn show_opcodes_of_circuit(&mut self, context: &mut Context<'_>, circuit_id: u32) { let current_opcode_location = - self.context.get_current_debug_location().and_then(|debug_location| { + context.get_current_debug_location().and_then(|debug_location| { if debug_location.circuit_id == circuit_id { Some(debug_location.opcode_location) } else { None } }); - let opcodes = self.context.get_opcodes_of_circuit(circuit_id); + let opcodes = context.get_opcodes_of_circuit(circuit_id); let current_acir_index = match current_opcode_location { Some(OpcodeLocation::Acir(ip)) => Some(ip), Some(OpcodeLocation::Brillig { acir_index, .. }) => Some(acir_index), @@ -178,7 +315,7 @@ impl<'a, B: BlackBoxFunctionSolver> ReplDebugger<'a, B> { let outer_marker = |acir_index| { if current_acir_index == Some(acir_index) { "->" - } else if self.context.is_breakpoint_set(&DebugLocation { + } else if context.is_breakpoint_set(&DebugLocation { circuit_id, opcode_location: OpcodeLocation::Acir(acir_index), brillig_function_id: None, @@ -191,7 +328,7 @@ impl<'a, B: BlackBoxFunctionSolver> ReplDebugger<'a, B> { let brillig_marker = |acir_index, brillig_index, brillig_function_id| { if current_acir_index == Some(acir_index) && brillig_index == current_brillig_index { "->" - } else if self.context.is_breakpoint_set(&DebugLocation { + } else if context.is_breakpoint_set(&DebugLocation { circuit_id, opcode_location: OpcodeLocation::Brillig { acir_index, brillig_index }, brillig_function_id: Some(brillig_function_id), @@ -233,168 +370,113 @@ impl<'a, B: BlackBoxFunctionSolver> ReplDebugger<'a, B> { } } - fn add_breakpoint_at(&mut self, location: DebugLocation) { - if !self.context.is_valid_debug_location(&location) { + fn add_breakpoint_at(context: &mut Context<'_>, location: DebugLocation) { + if !context.is_valid_debug_location(&location) { println!("Invalid location {location}"); - } else if self.context.add_breakpoint(location) { + } else if context.add_breakpoint(location) { println!("Added breakpoint at {location}"); } else { println!("Breakpoint at {location} already set"); } } - fn add_breakpoint_at_line(&mut self, line_number: i64) { - let Some(current_file) = self.context.get_current_file() else { - println!("No current file."); - return; - }; - - let best_location = - self.context.find_opcode_for_source_location(¤t_file, line_number); - + fn add_breakpoint_at_line(context: &mut Context<'_>, line_number: i64) { + let best_location = context.find_opcode_at_current_file_line(line_number); match best_location { Some(location) => { println!("Added breakpoint at line {}", line_number); - self.add_breakpoint_at(location) + Self::add_breakpoint_at(context, location); } None => println!("No opcode at line {}", line_number), } } - fn delete_breakpoint_at(&mut self, location: DebugLocation) { - if self.context.delete_breakpoint(&location) { + fn delete_breakpoint_at(context: &mut Context<'_>, location: DebugLocation) { + if context.delete_breakpoint(&location) { println!("Breakpoint at {location} deleted"); } else { println!("Breakpoint at {location} not set"); } } - fn validate_in_progress(&self) -> bool { - match self.last_result { - DebugCommandResult::Ok | DebugCommandResult::BreakpointReached(..) => true, + fn handle_result(&mut self, result: DebugCommandResult) { + self.last_result = result; + match &self.last_result { 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) { - match &result { + DebugCommandResult::Ok => (), 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(); - } - - 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 next_into(&mut self) { - if self.validate_in_progress() { - let result = self.context.next_into(); - self.handle_debug_command_result(result); - } - } - - fn next_over(&mut self) { - if self.validate_in_progress() { - let result = self.context.next_over(); - self.handle_debug_command_result(result); - } - } - - fn next_out(&mut self) { - if self.validate_in_progress() { - let result = self.context.next_out(); - 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 handle_step(&mut self, context: &mut Context<'_>, step: F) + where + F: Fn(&mut Context) -> DebugCommandResult, + { + let should_execute = match self.last_result { + DebugCommandResult::Ok | DebugCommandResult::BreakpointReached(..) => true, + DebugCommandResult::Done => { + println!("Execution finished"); + false + } + DebugCommandResult::Error(ref error) => { + println!("ERROR: {}", error); + self.show_current_vm_status(context); + false + } + }; + if should_execute { + let result = step(context); + self.show_current_vm_status(context); + self.handle_result(result); } } - fn restart_session(&mut self) { - let breakpoints: Vec = self.context.iterate_breakpoints().copied().collect(); - let foreign_call_executor = Box::new(DefaultDebugForeignCallExecutor::from_artifact( - PrintOutput::Stdout, - self.debug_artifact, - )); - self.context = DebugContext::new( - self.blackbox_solver, - self.circuits, - self.debug_artifact, - self.initial_witness.clone(), - foreign_call_executor, - self.unconstrained_functions, - ); - for debug_location in breakpoints { - self.context.add_breakpoint(debug_location); - } + fn restart_session(&mut self, context: &mut Context<'_>) { + context.restart(); self.last_result = DebugCommandResult::Ok; println!("Restarted debugging session."); - self.show_current_vm_status(); + self.show_current_vm_status(context); } - pub fn show_witness_map(&self) { - let witness_map = self.context.get_witness_map(); + fn show_witness_map(context: &mut Context<'_>) { + let witness_map = context.get_witness_map(); // NOTE: we need to clone() here to get the iterator for (witness, value) in witness_map.clone().into_iter() { println!("_{} = {value}", witness.witness_index()); } } - pub fn show_witness(&self, index: u32) { - if let Some(value) = self.context.get_witness_map().get_index(index) { + fn show_witness(context: &mut Context<'_>, index: u32) { + if let Some(value) = context.get_witness_map().get_index(index) { println!("_{} = {value}", index); } } - pub fn update_witness(&mut self, index: u32, value: String) { + fn update_witness(context: &mut Context<'_>, index: u32, value: String) { let Some(field_value) = FieldElement::try_from_str(&value) else { println!("Invalid witness value: {value}"); return; }; let witness = Witness::from(index); - _ = self.context.overwrite_witness(witness, field_value); + _ = context.overwrite_witness(witness, field_value); println!("_{} = {value}", index); } - pub fn show_brillig_memory(&self) { - if !self.context.is_executing_brillig() { + fn show_brillig_memory(context: &mut Context<'_>) { + if !context.is_executing_brillig() { println!("Not executing a Brillig block"); return; } - let Some(memory) = self.context.get_brillig_memory() else { + let Some(memory) = context.get_brillig_memory() else { // this can happen when just entering the Brillig block since ACVM // would have not initialized the Brillig VM yet; in fact, the // Brillig code may be skipped altogether @@ -412,8 +494,7 @@ impl<'a, B: BlackBoxFunctionSolver> ReplDebugger<'a, B> { println!("{index} = {}", value); } } - - pub fn write_brillig_memory(&mut self, index: usize, value: String, bit_size: u32) { + fn write_brillig_memory(context: &mut Context<'_>, index: usize, value: String, bit_size: u32) { let Some(field_value) = FieldElement::try_from_str(&value) else { println!("Invalid value: {value}"); return; @@ -424,15 +505,17 @@ impl<'a, B: BlackBoxFunctionSolver> ReplDebugger<'a, B> { return; }; - if !self.context.is_executing_brillig() { + if !context.is_executing_brillig() { println!("Not executing a Brillig block"); return; } - self.context.write_brillig_memory(index, field_value, bit_size); + context.write_brillig_memory(index, field_value, bit_size); } - pub fn show_vars(&self) { - for frame in self.context.get_variables() { + fn show_variables(context: &mut Context<'_>) { + let variables: Vec> = + context.get_variables().iter().map(DebugStackFrame::from).collect(); + for frame in variables { println!("{}({})", frame.function_name, frame.function_params.join(", ")); for (var_name, value, var_type) in frame.variables.iter() { let printable_value = @@ -442,33 +525,146 @@ impl<'a, B: BlackBoxFunctionSolver> ReplDebugger<'a, B> { } } - fn is_solved(&self) -> bool { - self.context.is_solved() + fn terminate(self, context: Context<'_>) { + let result = if context.is_solved() { + let solved_witness_stack = context.finalize(); + DebugExecutionResult::Solved(solved_witness_stack) + } else { + match self.last_result { + // Expose the last known error + DebugCommandResult::Error(error) => DebugExecutionResult::Error(error), + _ => DebugExecutionResult::Incomplete, + } + }; + self.status_sender.send(DebuggerStatus::Final(result)).expect("Downstream channel closed") + } +} + +struct DebugController { + command_sender: Sender, + status_receiver: Receiver, +} +impl DebugController { + fn debugger_status(&self) -> DebuggerStatus { + self.status_receiver.recv().expect("Debugger closed connection unexpectedly") + } + + fn call_debugger(&self, command: DebugCommandAPI) { + self.command_sender.send(command).expect("Could not communicate with debugger"); + self.wait_for_idle(); + } + + fn get_final_result(&self) -> DebugExecutionResult { + loop { + let status = self.debugger_status(); + if let DebuggerStatus::Final(result) = status { + return result; + } + } + } + + fn wait_for_idle(&self) { + loop { + let status = self.debugger_status(); + if let DebuggerStatus::Idle = status { + break; + }; + } } - fn finalize(self) -> WitnessStack { - self.context.finalize() + pub fn step_acir_opcode(&self) { + self.call_debugger(DebugCommandAPI::StepAcirOpcode); + } + pub fn cont(&self) { + self.call_debugger(DebugCommandAPI::Cont); + } + pub fn step_into_opcode(&self) { + self.call_debugger(DebugCommandAPI::StepIntoOpcode); + } + pub fn next_into(&self) { + self.call_debugger(DebugCommandAPI::NextInto); + } + pub fn next_over(&self) { + self.call_debugger(DebugCommandAPI::NextOver); + } + pub fn next_out(&self) { + self.call_debugger(DebugCommandAPI::NextOut); + } + pub fn restart_session(&self) { + self.call_debugger(DebugCommandAPI::Restart); + } + pub fn add_breakpoint_at_line(&self, line_number: i64) { + self.call_debugger(DebugCommandAPI::AddBreakpointAtLine(line_number)); + } + pub fn add_breakpoint_at(&self, location: DebugLocation) { + self.call_debugger(DebugCommandAPI::AddBreakpoint(location)); + } + pub fn delete_breakpoint_at(&self, location: DebugLocation) { + self.call_debugger(DebugCommandAPI::DeleteBreakpoint(location)); + } + pub fn update_witness(&self, index: u32, value: String) { + self.call_debugger(DebugCommandAPI::UpdateWitness(index, value)); + } + pub fn write_brillig_memory(&self, index: usize, value: String, bit_size: u32) { + self.call_debugger(DebugCommandAPI::WriteBrilligMemory(index, value, bit_size)); + } + pub fn show_vars(&self) { + self.call_debugger(DebugCommandAPI::ShowVariables); + } + pub fn show_opcodes(&self) { + self.call_debugger(DebugCommandAPI::ShowOpcodes); + } + pub fn show_witness_map(&self) { + self.call_debugger(DebugCommandAPI::ShowWitnessMap); + } + pub fn show_witness(&self, index: u32) { + self.call_debugger(DebugCommandAPI::ShowWitness(index)); + } + pub fn show_brillig_memory(&self) { + self.call_debugger(DebugCommandAPI::ShowBrilligMemory); + } + pub fn show_current_call_stack(&self) { + self.call_debugger(DebugCommandAPI::ShowCurrentCallStack); + } + pub fn show_current_vm_status(&self) { + self.call_debugger(DebugCommandAPI::ShowCurrentVmStatus); + } + pub fn terminate(&self) { + self.call_debugger(DebugCommandAPI::Terminate); } } -pub fn run>( - blackbox_solver: &B, - program: CompiledProgram, - initial_witness: WitnessMap, - raw_source_printing: bool, -) -> Result>, NargoError> { - let circuits = &program.program.functions; - let debug_artifact = - &DebugArtifact { debug_symbols: program.debug, file_map: program.file_map }; - let unconstrained_functions = &program.program.unconstrained_functions; - let context = RefCell::new(ReplDebugger::new( - blackbox_solver, - circuits, - debug_artifact, - initial_witness, - unconstrained_functions, - raw_source_printing, +pub fn run(project: DebugProject, run_params: RunParams) -> DebugExecutionResult { + let debug_artifact = DebugArtifact { + debug_symbols: project.compiled_program.debug.clone(), + file_map: project.compiled_program.file_map.clone(), + }; + + let foreign_call_executor = Box::new(DefaultDebugForeignCallExecutor::from_artifact( + PrintOutput::Stdout, + run_params.oracle_resolver_url, + &debug_artifact, + Some(project.root_dir), + project.package_name, )); + + let (command_tx, command_rx) = mpsc::channel::(); + let (status_tx, status_rx) = mpsc::channel::(); + thread::spawn(move || { + let debugger = AsyncReplDebugger::new( + &project.compiled_program, + &debug_artifact, + project.initial_witness, + status_tx, + command_rx, + run_params.raw_source_printing.unwrap_or(false), + run_params.pedantic_solving, + ); + debugger.start_debugging(foreign_call_executor); + }); + + let context = + RefCell::new(DebugController { command_sender: command_tx, status_receiver: status_rx }); let ref_context = &context; ref_context.borrow().show_current_vm_status(); @@ -549,7 +745,7 @@ pub fn run>( command! { "display ACIR opcodes", () => || { - ref_context.borrow().display_opcodes(); + ref_context.borrow().show_opcodes(); Ok(CommandStatus::Done) } }, @@ -658,15 +854,10 @@ pub fn run>( .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_stack = context.into_inner().finalize(); - Ok(Some(solved_witness_stack)) - } else { - Ok(None) - } + context.borrow().terminate(); + context.borrow().get_final_result() } diff --git a/tooling/debugger/tests/debug.rs b/tooling/debugger/tests/debug.rs index 07985742085..9ed46003e9b 100644 --- a/tooling/debugger/tests/debug.rs +++ b/tooling/debugger/tests/debug.rs @@ -1,9 +1,11 @@ #[cfg(test)] mod tests { + use std::collections::VecDeque; + // Some of these imports are consumed by the injected tests use assert_cmd::cargo::cargo_bin; - use rexpect::spawn_bash; + use rexpect::{session::PtyReplSession, spawn_bash}; // include tests generated by `build.rs` include!(concat!(env!("OUT_DIR"), "/debug.rs")); @@ -12,42 +14,77 @@ mod tests { let nargo_bin = cargo_bin("nargo").into_os_string().into_string().expect("Cannot parse nargo path"); + let mut dbg_session = start_debug_session(&format!( + "{nargo_bin} debug --program-dir {test_program_dir} --force-brillig --expression-width 3" + )); + + // send continue which should run to the program to end + // given we haven't set any breakpoints. + send_continue_and_check_no_panic(&mut dbg_session); + + send_quit(&mut dbg_session); + dbg_session + .exp_regex(".*Circuit witness successfully solved.*") + .expect("Expected circuit witness to be successfully solved."); + + exit(dbg_session); + } + + pub fn debugger_test_success(test_program_dir: &str, test_name: &str) { + let nargo_bin = + cargo_bin("nargo").into_os_string().into_string().expect("Cannot parse nargo path"); + + let mut dbg_session = start_debug_session( + &(format!( + "{nargo_bin} debug --program-dir {test_program_dir} --test-name {test_name} --force-brillig --expression-width 3" + )), + ); + + // send continue which should run to the program to end + // given we haven't set any breakpoints. + send_continue_and_check_no_panic(&mut dbg_session); + + send_quit(&mut dbg_session); + dbg_session + .exp_regex(".*Testing .*\\.\\.\\. .*ok.*") + .expect("Expected test to be successful"); + + exit(dbg_session); + } + + fn start_debug_session(command: &str) -> PtyReplSession { let timeout_seconds = 30; let mut dbg_session = spawn_bash(Some(timeout_seconds * 1000)).expect("Could not start bash session"); // Start debugger and test that it loads for the given program. + dbg_session.execute(command, ".*\\Starting debugger.*").expect("Could not start debugger"); dbg_session - .execute( - &format!( - "{nargo_bin} debug --program-dir {test_program_dir} --force-brillig --expression-width 3" - ), - ".*\\Starting debugger.*", - ) - .expect("Could not start debugger"); + } - // While running the debugger, issue a "continue" cmd, - // which should run to the program to end given - // we haven't set any breakpoints. - // ">" is the debugger's prompt, so finding one - // after running "continue" indicates that the - // debugger has not panicked until the end of the program. + fn send_quit(dbg_session: &mut PtyReplSession) { + // Run the "quit" command, then check that the debugger confirms + // having successfully solved the circuit witness. + dbg_session.send_line("quit").expect("Failed to quit debugger"); + } + + /// Exit the bash session. + fn exit(mut dbg_session: PtyReplSession) { + dbg_session.send_line("exit").expect("Failed to quit bash session"); + } + + /// While running the debugger, issue a "continue" cmd, + /// which should run to the program to end or the next breakpoint + /// ">" is the debugger's prompt, so finding one + /// after running "continue" indicates that the + /// debugger has not panicked. + fn send_continue_and_check_no_panic(dbg_session: &mut PtyReplSession) { dbg_session .send_line("c") .expect("Debugger panicked while attempting to step through program."); dbg_session .exp_string(">") .expect("Failed while waiting for debugger to step through program."); - - // Run the "quit" command, then check that the debugger confirms - // having successfully solved the circuit witness. - dbg_session.send_line("quit").expect("Failed to quit debugger"); - dbg_session - .exp_regex(".*Circuit witness successfully solved.*") - .expect("Expected circuit witness to be successfully solved."); - - // Exit the bash session. - dbg_session.send_line("exit").expect("Failed to quit bash session"); } #[test] @@ -60,7 +97,9 @@ mod tests { spawn_bash(Some(timeout_seconds * 1000)).expect("Could not start bash session"); let test_program_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../test_programs/execution_success/regression_7195"); + .join("../../test_programs/execution_success/regression_7195") + .canonicalize() + .unwrap(); let test_program_dir = test_program_path.display(); // Start debugger and test that it loads for the given program. @@ -73,8 +112,39 @@ mod tests { ) .expect("Could not start debugger"); - let num_steps = 16; - for _ in 1..=num_steps { + let expected_lines_by_command: Vec> = vec![ + VecDeque::from(["fn main(x: Field, y: pub Field) {"]), + VecDeque::from(["fn main(x: Field, y: pub Field) {"]), + VecDeque::from(["fn main(x: Field, y: pub Field) {"]), + VecDeque::from([ + "let x = unsafe { baz(x) };", + "unconstrained fn baz(x: Field) -> Field {", + ]), + VecDeque::from([ + "let x = unsafe { baz(x) };", + "unconstrained fn baz(x: Field) -> Field {", + ]), + VecDeque::from(["let x = unsafe { baz(x) };", "}"]), + VecDeque::from(["let x = unsafe { baz(x) };"]), + VecDeque::from(["foo(x);", "fn foo(x: Field) {"]), + VecDeque::from(["foo(x);", "fn foo(x: Field) {"]), + VecDeque::from([ + "foo(x);", + "let y = unsafe { baz(x) };", + "unconstrained fn baz(x: Field) -> Field {", + ]), + VecDeque::from([ + "foo(x);", + "let y = unsafe { baz(x) };", + "unconstrained fn baz(x: Field) -> Field {", + ]), + VecDeque::from(["foo(x);", "let y = unsafe { baz(x) };", "}"]), + VecDeque::from(["foo(x);", "let y = unsafe { baz(x) };"]), + VecDeque::from(["foo(x);", "bar(y);", "fn bar(y: Field) {"]), + VecDeque::from(["foo(x);", "bar(y);", "fn bar(y: Field) {"]), + VecDeque::from(["foo(x);", "bar(y);", "assert(y != 0);"]), + ]; + for mut expected_lines in expected_lines_by_command { // While running the debugger, issue a "next" cmd, // which should run to the program to the next source line given // we haven't set any breakpoints. @@ -87,73 +157,30 @@ mod tests { dbg_session .exp_string(">") .expect("Failed while waiting for debugger to step through program."); - } - let mut lines = vec![]; - while let Ok(line) = dbg_session.read_line() { - if !(line.starts_with(">next") || line.starts_with("At ") || line.starts_with("...")) { - lines.push(line); + let at_filename = format!("At {test_program_dir}"); + while let Some(expected_line) = expected_lines.pop_front() { + let line = loop { + let read_line = dbg_session.read_line().unwrap(); + if !(read_line.contains("> next") + || read_line.contains("At opcode") + || read_line.contains(at_filename.as_str()) + || read_line.contains("...")) + { + break read_line; + } + }; + let ascii_line: String = line.chars().filter(char::is_ascii).collect(); + let line_expected_to_contain = expected_line.trim(); + assert!( + ascii_line.contains(line_expected_to_contain), + "{:?}\ndid not contain\n{:?}", + ascii_line, + line_expected_to_contain, + ); } } - let lines_expected_to_contain: Vec<&str> = vec![ - "> next", - " let x = unsafe { baz(x) };", - "unconstrained fn baz(x: Field) -> Field {", - "> next", - " let x = unsafe { baz(x) };", - "unconstrained fn baz(x: Field) -> Field {", - "> next", - " let x = unsafe { baz(x) };", - "}", - "> next", - " let x = unsafe { baz(x) };", - "> next", - " foo(x);", - "fn foo(x: Field) {", - "> next", - " foo(x);", - "fn foo(x: Field) {", - "> next", - " foo(x);", - " let y = unsafe { baz(x) };", - "unconstrained fn baz(x: Field) -> Field {", - "> next", - " foo(x);", - " let y = unsafe { baz(x) };", - "unconstrained fn baz(x: Field) -> Field {", - "> next", - " foo(x);", - " let y = unsafe { baz(x) };", - "}", - "> next", - " foo(x);", - " let y = unsafe { baz(x) };", - "> next", - " foo(x);", - " bar(y);", - "fn bar(y: Field) {", - "> next", - " foo(x);", - " bar(y);", - "fn bar(y: Field) {", - "> next", - " foo(x);", - " bar(y);", - " assert(y != 0);", - ]; - - for (line, line_expected_to_contain) in lines.into_iter().zip(lines_expected_to_contain) { - let ascii_line: String = line.chars().filter(char::is_ascii).collect(); - let line_expected_to_contain = line_expected_to_contain.trim_start(); - assert!( - ascii_line.contains(line_expected_to_contain), - "{:?}\ndid not contain\n{:?}", - ascii_line, - line_expected_to_contain, - ); - } - // Run the "quit" command dbg_session.send_line("quit").expect("Failed to quit debugger"); diff --git a/tooling/lsp/src/requests/code_lens_request.rs b/tooling/lsp/src/requests/code_lens_request.rs index 2417fa56fad..013f9e6970e 100644 --- a/tooling/lsp/src/requests/code_lens_request.rs +++ b/tooling/lsp/src/requests/code_lens_request.rs @@ -12,6 +12,7 @@ use crate::{ }; const ARROW: &str = "▶\u{fe0e}"; +const GEAR: &str = "⚙"; const TEST_COMMAND: &str = "nargo.test"; const TEST_CODELENS_TITLE: &str = "Run Test"; const COMPILE_COMMAND: &str = "nargo.compile"; @@ -22,6 +23,8 @@ const EXECUTE_COMMAND: &str = "nargo.execute"; const EXECUTE_CODELENS_TITLE: &str = "Execute"; const DEBUG_COMMAND: &str = "nargo.debug.dap"; const DEBUG_CODELENS_TITLE: &str = "Debug"; +const DEBUG_TEST_COMMAND: &str = "nargo.debug.test"; +const DEBUG_TEST_CODELENS_TITLE: &str = "Debug test"; fn with_arrow(title: &str) -> String { format!("{ARROW} {title}") @@ -116,7 +119,7 @@ pub(crate) fn collect_lenses_for_package( arguments: Some( [ package_selection_args(workspace, package), - vec!["--exact".into(), "--show-output".into(), func_name.into()], + vec!["--exact".into(), "--show-output".into(), func_name.clone().into()], ] .concat(), ), @@ -125,6 +128,22 @@ pub(crate) fn collect_lenses_for_package( let test_lens = CodeLens { range, command: Some(test_command), data: None }; lenses.push(test_lens); + + let debug_test_command = Command { + title: format!("{GEAR} {DEBUG_TEST_CODELENS_TITLE}"), + command: DEBUG_TEST_COMMAND.into(), + arguments: Some( + [ + package_selection_args(workspace, package), + vec!["--exact".into(), func_name.into()], + ] + .concat(), + ), + }; + + let debug_test_lens = CodeLens { range, command: Some(debug_test_command), data: None }; + + lenses.push(debug_test_lens); } if package.is_binary() { diff --git a/tooling/nargo/src/errors.rs b/tooling/nargo/src/errors.rs index b60b3ac8ad8..816d25230cf 100644 --- a/tooling/nargo/src/errors.rs +++ b/tooling/nargo/src/errors.rs @@ -249,3 +249,36 @@ pub fn try_to_diagnose_runtime_error( let error = CustomDiagnostic::simple_error(message, String::new(), location); Some(error.with_call_stack(source_locations)) } + +/// Map the given OpcodeResolutionError to the corresponding ExecutionError +/// In case of resulting in an ExecutionError::AssertionFailedThis it propagates the payload +pub fn execution_error_from( + error: OpcodeResolutionError, + call_stack: &[ResolvedOpcodeLocation], +) -> ExecutionError { + let (assertion_payload, brillig_function_id) = match &error { + OpcodeResolutionError::BrilligFunctionFailed { payload, function_id, .. } => { + (payload.clone(), Some(*function_id)) + } + OpcodeResolutionError::UnsatisfiedConstrain { payload, .. } => (payload.clone(), None), + _ => (None, None), + }; + + match assertion_payload { + Some(payload) => { + ExecutionError::AssertionFailed(payload, call_stack.to_owned(), brillig_function_id) + } + None => { + let call_stack = match &error { + OpcodeResolutionError::UnsatisfiedConstrain { .. } + | OpcodeResolutionError::IndexOutOfBounds { .. } + | OpcodeResolutionError::InvalidInputBitSize { .. } + | OpcodeResolutionError::BrilligFunctionFailed { .. } => { + Some(call_stack.to_owned()) + } + _ => None, + }; + ExecutionError::SolvingError(error, call_stack) + } + } +} diff --git a/tooling/nargo/src/foreign_calls/rpc.rs b/tooling/nargo/src/foreign_calls/rpc.rs index 6e485812885..d21389ec006 100644 --- a/tooling/nargo/src/foreign_calls/rpc.rs +++ b/tooling/nargo/src/foreign_calls/rpc.rs @@ -19,6 +19,9 @@ pub struct RPCForeignCallExecutor { id: u64, /// JSON RPC client to resolve foreign calls external_resolver: HttpClient, + /// External resolver target. We are keeping it to be able to restart httpClient if necessary + /// see noir-lang/noir#7463 + target_url: String, /// Root path to the program or workspace in execution. root_path: Option, /// Name of the package in execution @@ -59,17 +62,7 @@ impl RPCForeignCallExecutor { root_path: Option, package_name: Option, ) -> Self { - let mut client_builder = HttpClientBuilder::new(); - - if let Some(Ok(timeout)) = - std::env::var("NARGO_FOREIGN_CALL_TIMEOUT").ok().map(|timeout| timeout.parse()) - { - let timeout_duration = std::time::Duration::from_millis(timeout); - client_builder = client_builder.request_timeout(timeout_duration); - }; - - let oracle_resolver = - client_builder.build(resolver_url).expect("Invalid oracle resolver URL"); + let oracle_resolver = build_http_client(resolver_url); // Opcodes are executed in the `ProgramExecutor::execute_circuit` one by one in a loop, // we don't need a concurrent thread pool. @@ -81,12 +74,49 @@ impl RPCForeignCallExecutor { RPCForeignCallExecutor { external_resolver: oracle_resolver, + target_url: resolver_url.to_string(), id, root_path, package_name, runtime, } } + + fn send_foreign_call( + &mut self, + foreign_call: &ForeignCallWaitInfo, + ) -> Result, jsonrpsee::core::ClientError> + where + F: AcirField + Serialize + for<'a> Deserialize<'a>, + { + let params = ResolveForeignCallRequest { + session_id: self.id, + function_call: foreign_call.clone(), + root_path: self + .root_path + .clone() + .map(|path| path.to_str().unwrap().to_string()) + .or(Some(String::new())), + package_name: self.package_name.clone().or(Some(String::new())), + }; + let encoded_params = rpc_params!(params); + self.runtime.block_on(async { + self.external_resolver.request("resolve_foreign_call", encoded_params).await + }) + } +} + +fn build_http_client(target: &str) -> HttpClient { + let mut client_builder = HttpClientBuilder::new(); + + if let Some(Ok(timeout)) = + std::env::var("NARGO_FOREIGN_CALL_TIMEOUT").ok().map(|timeout| timeout.parse()) + { + let timeout_duration = std::time::Duration::from_millis(timeout); + client_builder = client_builder.request_timeout(timeout_duration); + }; + + client_builder.build(target).expect("Invalid oracle resolver URL") } impl ForeignCallExecutor for RPCForeignCallExecutor @@ -97,18 +127,20 @@ where /// This method cannot be called from inside a `tokio` runtime, for that to work /// we need to offload the execution into a different thread; see the tests. fn execute(&mut self, foreign_call: &ForeignCallWaitInfo) -> ResolveForeignCallResult { - let encoded_params = rpc_params!(ResolveForeignCallRequest { - session_id: self.id, - function_call: foreign_call.clone(), - root_path: self.root_path.clone().map(|path| path.to_str().unwrap().to_string()), - package_name: self.package_name.clone(), - }); - - let parsed_response = self.runtime.block_on(async { - self.external_resolver.request("resolve_foreign_call", encoded_params).await - })?; - - Ok(parsed_response) + let result = self.send_foreign_call(foreign_call); + + match result { + Ok(parsed_response) => Ok(parsed_response), + // TODO: This is a workaround for noir-lang/noir#7463 + // The client is losing connection with the server and it's not being able to manage it + // so we are re-creating the HttpClient when it happens + Err(jsonrpsee::core::ClientError::Transport(_)) => { + self.external_resolver = build_http_client(&self.target_url); + let parsed_response = self.send_foreign_call(foreign_call)?; + Ok(parsed_response) + } + Err(other) => Err(ForeignCallError::from(other)), + } } } diff --git a/tooling/nargo/src/ops/compile.rs b/tooling/nargo/src/ops/compile.rs index b1d5a79e263..94a9263701c 100644 --- a/tooling/nargo/src/ops/compile.rs +++ b/tooling/nargo/src/ops/compile.rs @@ -1,9 +1,10 @@ use fm::FileManager; use noirc_driver::{ - CompilationResult, CompileOptions, CompiledContract, CompiledProgram, link_to_debug_crate, + CompilationResult, CompileOptions, CompiledContract, CompiledProgram, CrateId, check_crate, + link_to_debug_crate, }; use noirc_frontend::debug::DebugInstrumenter; -use noirc_frontend::hir::ParsedFiles; +use noirc_frontend::hir::{Context, ParsedFiles}; use crate::errors::CompileError; use crate::prepare_package; @@ -151,3 +152,15 @@ pub fn report_errors( Ok(t) } + +/// Run the lexing, parsing, name resolution, and type checking passes and report any warnings +/// and errors found. +/// TODO: check if it's ok for this fn to be here. Should it be in ops::check instead? +pub fn check_crate_and_report_errors( + context: &mut Context, + crate_id: CrateId, + options: &CompileOptions, +) -> Result<(), CompileError> { + let result = check_crate(context, crate_id, options); + report_errors(result, &context.file_manager, options.deny_warnings, options.silence_warnings) +} diff --git a/tooling/nargo/src/ops/debug.rs b/tooling/nargo/src/ops/debug.rs new file mode 100644 index 00000000000..ac58b5cc2c5 --- /dev/null +++ b/tooling/nargo/src/ops/debug.rs @@ -0,0 +1,216 @@ +use std::path::Path; + +use acvm::acir::circuit::ExpressionWidth; +use fm::FileManager; +use noirc_driver::{ + CompileOptions, CompiledProgram, CrateId, DEFAULT_EXPRESSION_WIDTH, compile_no_check, + file_manager_with_stdlib, link_to_debug_crate, +}; +use noirc_frontend::{ + debug::DebugInstrumenter, + hir::{Context, FunctionNameMatch, ParsedFiles, def_map::TestFunction}, +}; + +use crate::{ + errors::CompileError, insert_all_files_for_workspace_into_file_manager, package::Package, + parse_all, prepare_package, workspace::Workspace, +}; + +use super::{ + compile_program, compile_program_with_debug_instrumenter, report_errors, transform_program, +}; + +pub struct TestDefinition { + pub name: String, + pub function: TestFunction, +} +pub fn get_test_function_for_debug( + crate_id: CrateId, + context: &Context, + test_name: &str, +) -> Result { + let test_pattern = FunctionNameMatch::Contains(vec![test_name.into()]); + + let test_functions = context.get_all_test_functions_in_crate_matching(&crate_id, &test_pattern); + + let (test_name, test_function) = match test_functions { + matchings if matchings.is_empty() => { + return Err(format!("`{}` does not match with any test function", test_name)); + } + matchings if matchings.len() == 1 => matchings.into_iter().next().unwrap(), + matchings => { + let exact_match_op = matchings + .into_iter() + .filter(|(name, _)| name.split("::").last() == Some(test_name)) + .collect::>(); + // There can be multiple matches but only one that matches exactly + // this would be the case of tests names that englobe others + // i.e.: + // - test_something + // - unconstrained_test_something + // in this case, looking up "test_something" throws two matchings + // but only one matches exact + if exact_match_op.len() == 1 { + exact_match_op.into_iter().next().unwrap() + } else { + return Err(format!("`{}` matches with more than one test function", test_name)); + } + } + }; + + let test_function_has_arguments = !context + .def_interner + .function_meta(&test_function.get_id()) + .function_signature() + .0 + .is_empty(); + + if test_function_has_arguments { + return Err(String::from("Cannot debug tests with arguments")); + } + Ok(TestDefinition { name: test_name, function: test_function }) +} + +pub fn compile_test_fn_for_debugging( + test_def: &TestDefinition, + context: &mut Context, + package: &Package, + compile_options: CompileOptions, +) -> Result { + let compiled_program = + compile_no_check(context, &compile_options, test_def.function.get_id(), None, false)?; + let expression_width = + get_target_width(package.expression_width, compile_options.expression_width); + let compiled_program = transform_program(compiled_program, expression_width); + Ok(compiled_program) +} + +pub fn compile_bin_package_for_debugging( + workspace: &Workspace, + package: &Package, + compile_options: &CompileOptions, +) -> Result { + let (workspace_file_manager, mut parsed_files) = load_workspace_files(workspace); + + let expression_width = + get_target_width(package.expression_width, compile_options.expression_width); + + let compilation_result = if compile_options.instrument_debug { + let debug_state = + instrument_package_files(&mut parsed_files, &workspace_file_manager, package); + + compile_program_with_debug_instrumenter( + &workspace_file_manager, + &parsed_files, + workspace, + package, + compile_options, + None, + debug_state, + ) + } else { + compile_program( + &workspace_file_manager, + &parsed_files, + workspace, + package, + compile_options, + None, + ) + }; + + report_errors( + compilation_result, + &workspace_file_manager, + compile_options.deny_warnings, + compile_options.silence_warnings, + ) + .map(|compiled_program| transform_program(compiled_program, expression_width)) +} + +pub fn compile_options_for_debugging( + acir_mode: bool, + skip_instrumentation: bool, + expression_width: Option, + compile_options: CompileOptions, +) -> CompileOptions { + CompileOptions { + // Compilation warnings are disabled when + // compiling for debugging + // + // For instrumenting the program the debugger + // will import functions that may not be used, + // which would generate compilation warnings + silence_warnings: true, + deny_warnings: false, + instrument_debug: !skip_instrumentation, + force_brillig: !acir_mode, + expression_width, + ..compile_options + } +} + +pub fn prepare_package_for_debug<'a>( + file_manager: &'a FileManager, + parsed_files: &'a mut ParsedFiles, + package: &'a Package, + workspace: &Workspace, +) -> (Context<'a, 'a>, CrateId) { + let debug_instrumenter = instrument_package_files(parsed_files, file_manager, package); + + // -- This :down: is from nargo::ops(compile).compile_program_with_debug_instrumenter + let (mut context, crate_id) = prepare_package(file_manager, parsed_files, package); + link_to_debug_crate(&mut context, crate_id); + context.debug_instrumenter = debug_instrumenter; + context.package_build_path = workspace.package_build_path(package); + (context, crate_id) +} + +pub fn load_workspace_files(workspace: &Workspace) -> (FileManager, ParsedFiles) { + let mut file_manager = file_manager_with_stdlib(Path::new("")); + insert_all_files_for_workspace_into_file_manager(workspace, &mut file_manager); + + let parsed_files = parse_all(&file_manager); + (file_manager, parsed_files) +} + +/// Add debugging instrumentation to all parsed files belonging to the package +/// being compiled +fn instrument_package_files( + parsed_files: &mut ParsedFiles, + file_manager: &FileManager, + package: &Package, +) -> DebugInstrumenter { + // Start off at the entry path and read all files in the parent directory. + let entry_path_parent = package + .entry_path + .parent() + .unwrap_or_else(|| panic!("The entry path is expected to be a single file within a directory and so should have a parent {:?}", package.entry_path)); + + let mut debug_instrumenter = DebugInstrumenter::default(); + + for (file_id, parsed_file) in parsed_files.iter_mut() { + let file_path = + file_manager.path(*file_id).expect("Parsed file ID not found in file manager"); + for ancestor in file_path.ancestors() { + if ancestor == entry_path_parent { + // file is in package + debug_instrumenter.instrument_module(&mut parsed_file.0, *file_id); + } + } + } + + debug_instrumenter +} + +// This is the same as in compile_cmd, perhaps we should move it to ops::compile? +fn get_target_width( + package_default_width: Option, + compile_options_width: Option, +) -> ExpressionWidth { + if let (Some(manifest_default_width), None) = (package_default_width, compile_options_width) { + manifest_default_width + } else { + compile_options_width.unwrap_or(DEFAULT_EXPRESSION_WIDTH) + } +} diff --git a/tooling/nargo/src/ops/execute.rs b/tooling/nargo/src/ops/execute.rs index bc10bf84fcd..5917c5d331d 100644 --- a/tooling/nargo/src/ops/execute.rs +++ b/tooling/nargo/src/ops/execute.rs @@ -3,13 +3,12 @@ use acvm::acir::circuit::{OpcodeLocation, Program}; use acvm::acir::native_types::WitnessStack; use acvm::pwg::{ ACVM, ACVMStatus, ErrorLocation, OpcodeNotSolvable, OpcodeResolutionError, ProfilingSamples, - ResolvedAssertionPayload, }; use acvm::{AcirField, BlackBoxFunctionSolver}; use acvm::{acir::circuit::Circuit, acir::native_types::WitnessMap}; use crate::NargoError; -use crate::errors::{ExecutionError, ResolvedOpcodeLocation}; +use crate::errors::{ExecutionError, ResolvedOpcodeLocation, execution_error_from}; use crate::foreign_calls::ForeignCallExecutor; struct ProgramExecutor<'a, F: AcirField, B: BlackBoxFunctionSolver, E: ForeignCallExecutor> { @@ -88,7 +87,7 @@ impl<'a, F: AcirField, B: BlackBoxFunctionSolver, E: ForeignCallExecutor> unreachable!("Execution should not stop while in `InProgress` state.") } ACVMStatus::Failure(error) => { - let call_stack = match &error { + match &error { OpcodeResolutionError::UnsatisfiedConstrain { opcode_location: ErrorLocation::Resolved(opcode_location), .. @@ -106,7 +105,6 @@ impl<'a, F: AcirField, B: BlackBoxFunctionSolver, E: ForeignCallExecutor> opcode_location: *opcode_location, }; self.call_stack.push(resolved_location); - Some(self.call_stack.clone()) } OpcodeResolutionError::BrilligFunctionFailed { call_stack, .. } => { let brillig_call_stack = @@ -115,34 +113,14 @@ impl<'a, F: AcirField, B: BlackBoxFunctionSolver, E: ForeignCallExecutor> opcode_location: *location, }); self.call_stack.extend(brillig_call_stack); - Some(self.call_stack.clone()) } - _ => None, + _ => (), }; - let assertion_payload: Option> = match &error { - OpcodeResolutionError::BrilligFunctionFailed { payload, .. } - | OpcodeResolutionError::UnsatisfiedConstrain { payload, .. } => { - payload.clone() - } - _ => None, - }; - - let brillig_function_id = match &error { - OpcodeResolutionError::BrilligFunctionFailed { function_id, .. } => { - Some(*function_id) - } - _ => None, - }; - - return Err(NargoError::ExecutionError(match assertion_payload { - Some(payload) => ExecutionError::AssertionFailed( - payload, - call_stack.expect("Should have call stack for an assertion failure"), - brillig_function_id, - ), - None => ExecutionError::SolvingError(error, call_stack), - })); + return Err(NargoError::ExecutionError(execution_error_from( + error, + &self.call_stack, + ))); } ACVMStatus::RequiresForeignCall(foreign_call) => { let foreign_call_result = self.foreign_call_executor.execute(&foreign_call)?; diff --git a/tooling/nargo/src/ops/mod.rs b/tooling/nargo/src/ops/mod.rs index 7ce34b1acd2..fcab1b57e86 100644 --- a/tooling/nargo/src/ops/mod.rs +++ b/tooling/nargo/src/ops/mod.rs @@ -1,16 +1,20 @@ pub use self::check::check_program; pub use self::compile::{ - collect_errors, compile_contract, compile_program, compile_program_with_debug_instrumenter, - compile_workspace, report_errors, + check_crate_and_report_errors, collect_errors, compile_contract, compile_program, + compile_program_with_debug_instrumenter, compile_workspace, report_errors, }; pub use self::optimize::{optimize_contract, optimize_program}; pub use self::transform::{transform_contract, transform_program}; pub use self::execute::{execute_program, execute_program_with_profiling}; -pub use self::test::{TestStatus, run_test}; +pub use self::test::{ + TestStatus, check_expected_failure_message, run_test, test_status_program_compile_fail, + test_status_program_compile_pass, +}; mod check; mod compile; +pub mod debug; mod execute; mod optimize; mod test; diff --git a/tooling/nargo/src/ops/test.rs b/tooling/nargo/src/ops/test.rs index c4adaa5cfaa..29e9fdfaa08 100644 --- a/tooling/nargo/src/ops/test.rs +++ b/tooling/nargo/src/ops/test.rs @@ -175,7 +175,10 @@ where /// that a constraint was never satisfiable. /// An example of this is the program `assert(false)` /// In that case, we check if the test function should fail, and if so, we return `TestStatus::Pass`. -fn test_status_program_compile_fail(err: CompileError, test_function: &TestFunction) -> TestStatus { +pub fn test_status_program_compile_fail( + err: CompileError, + test_function: &TestFunction, +) -> TestStatus { // The test has failed compilation, but it should never fail. Report error. if !test_function.should_fail() { return TestStatus::CompileError(err.into()); @@ -188,7 +191,7 @@ fn test_status_program_compile_fail(err: CompileError, test_function: &TestFunct /// /// We now check whether execution passed/failed and whether it should have /// passed/failed to determine the test status. -fn test_status_program_compile_pass( +pub fn test_status_program_compile_pass( test_function: &TestFunction, abi: &Abi, debug: &[DebugInfo], @@ -228,7 +231,7 @@ fn test_status_program_compile_pass( ) } -fn check_expected_failure_message( +pub fn check_expected_failure_message( test_function: &TestFunction, failed_assertion: Option, error_diagnostic: Option, diff --git a/tooling/nargo_cli/src/cli/check_cmd.rs b/tooling/nargo_cli/src/cli/check_cmd.rs index 2247f40f181..80d32cd52b6 100644 --- a/tooling/nargo_cli/src/cli/check_cmd.rs +++ b/tooling/nargo_cli/src/cli/check_cmd.rs @@ -4,17 +4,15 @@ use clap::Args; use fm::FileManager; use iter_extended::btree_map; use nargo::{ - errors::CompileError, insert_all_files_for_workspace_into_file_manager, ops::report_errors, - package::Package, parse_all, prepare_package, workspace::Workspace, + errors::CompileError, insert_all_files_for_workspace_into_file_manager, + ops::check_crate_and_report_errors, package::Package, parse_all, prepare_package, + workspace::Workspace, }; use nargo_toml::PackageSelection; use noir_artifact_cli::fs::artifact::write_to_file; use noirc_abi::{AbiParameter, AbiType, MAIN_RETURN_NAME}; -use noirc_driver::{CompileOptions, CrateId, check_crate, compute_function_abi}; -use noirc_frontend::{ - hir::{Context, ParsedFiles}, - monomorphization::monomorphize, -}; +use noirc_driver::{CompileOptions, check_crate, compute_function_abi}; +use noirc_frontend::{hir::ParsedFiles, monomorphization::monomorphize}; use super::{LockType, PackageOptions, WorkspaceCommand}; @@ -151,17 +149,6 @@ fn create_input_toml_template( toml::to_string(&map).unwrap() } -/// Run the lexing, parsing, name resolution, and type checking passes and report any warnings -/// and errors found. -pub(crate) fn check_crate_and_report_errors( - context: &mut Context, - crate_id: CrateId, - options: &CompileOptions, -) -> Result<(), CompileError> { - let result = check_crate(context, crate_id, options); - report_errors(result, &context.file_manager, options.deny_warnings, options.silence_warnings) -} - #[cfg(test)] mod tests { use noirc_abi::{AbiParameter, AbiType, AbiVisibility, Sign}; diff --git a/tooling/nargo_cli/src/cli/dap_cmd.rs b/tooling/nargo_cli/src/cli/dap_cmd.rs index 8987ed80d3e..8d892a1e759 100644 --- a/tooling/nargo_cli/src/cli/dap_cmd.rs +++ b/tooling/nargo_cli/src/cli/dap_cmd.rs @@ -1,25 +1,32 @@ -use acvm::FieldElement; use acvm::acir::circuit::ExpressionWidth; -use acvm::acir::native_types::WitnessMap; -use bn254_blackbox_solver::Bn254BlackBoxSolver; use clap::Args; +use dap::errors::ServerError; +use dap::events::OutputEventBody; +use dap::requests::Command; +use dap::responses::ResponseBody; +use dap::server::Server; +use dap::types::{Capabilities, OutputEventCategory}; use nargo::constants::PROVER_INPUT_FILE; +use nargo::ops::debug::{ + TestDefinition, compile_bin_package_for_debugging, compile_options_for_debugging, + compile_test_fn_for_debugging, get_test_function_for_debug, load_workspace_files, + prepare_package_for_debug, +}; +use nargo::ops::{TestStatus, check_crate_and_report_errors, test_status_program_compile_pass}; +use nargo::package::Package; use nargo::workspace::Workspace; use nargo_toml::{PackageSelection, get_package_manifest, resolve_workspace_from_toml}; use noir_artifact_cli::fs::inputs::read_inputs_from_file; +use noir_debugger::{DebugExecutionResult, DebugProject, RunParams}; +use noirc_abi::Abi; use noirc_driver::{CompileOptions, CompiledProgram, NOIR_ARTIFACT_VERSION_STRING}; +use noirc_errors::debug_info::DebugInfo; use noirc_frontend::graph::CrateName; - use std::io::{BufReader, BufWriter, Read, Write}; use std::path::Path; -use dap::requests::Command; -use dap::responses::ResponseBody; -use dap::server::Server; -use dap::types::Capabilities; use serde_json::Value; -use super::debug_cmd::compile_bin_package_for_debugging; use crate::errors::CliError; use noir_debugger::errors::{DapError, LoadError}; @@ -48,6 +55,9 @@ pub(crate) struct DapCommand { #[clap(long)] preflight_skip_instrumentation: bool, + #[clap(long)] + preflight_test_name: Option, + /// Use pedantic ACVM solving, i.e. double-check some black-box function /// assumptions when solving. /// This is disabled by default. @@ -98,31 +108,62 @@ fn workspace_not_found_error_msg(project_folder: &str, package: Option<&str>) -> } } +fn compile_main( + workspace: &Workspace, + package: &Package, + compile_options: &CompileOptions, +) -> Result { + compile_bin_package_for_debugging(workspace, package, compile_options) + .map_err(|_| LoadError::Generic("Failed to compile project".into())) +} + +fn compile_test( + workspace: &Workspace, + package: &Package, + compile_options: CompileOptions, + test_name: String, +) -> Result<(CompiledProgram, TestDefinition), LoadError> { + let (file_manager, mut parsed_files) = load_workspace_files(workspace); + + let (mut context, crate_id) = + prepare_package_for_debug(&file_manager, &mut parsed_files, package, workspace); + + check_crate_and_report_errors(&mut context, crate_id, &compile_options) + .map_err(|_| LoadError::Generic("Failed to compile project".into()))?; + + let test = get_test_function_for_debug(crate_id, &context, &test_name) + .map_err(|_| LoadError::Generic("Failed to compile project".into()))?; + + let program = compile_test_fn_for_debugging(&test, &mut context, package, compile_options) + .map_err(|_| LoadError::Generic("Failed to compile project".into()))?; + Ok((program, test)) +} + fn load_and_compile_project( project_folder: &str, package: Option<&str>, prover_name: &str, - expression_width: ExpressionWidth, - acir_mode: bool, - skip_instrumentation: bool, -) -> Result<(CompiledProgram, WitnessMap), LoadError> { + compile_options: CompileOptions, + test_name: Option, +) -> Result<(DebugProject, Option), LoadError> { let workspace = find_workspace(project_folder, package) .ok_or(LoadError::Generic(workspace_not_found_error_msg(project_folder, package)))?; let package = workspace .into_iter() - .find(|p| p.is_binary()) - .ok_or(LoadError::Generic("No matching binary packages found in workspace".into()))?; - - let compiled_program = compile_bin_package_for_debugging( - &workspace, - package, - acir_mode, - skip_instrumentation, - CompileOptions::default(), - ) - .map_err(|_| LoadError::Generic("Failed to compile project".into()))?; + .find(|p| p.is_binary() || p.is_contract()) + .ok_or(LoadError::Generic("No matching binary or contract packages found in workspace. Only these packages can be debugged.".into()))?; - let compiled_program = nargo::ops::transform_program(compiled_program, expression_width); + let (compiled_program, test_def) = match test_name { + None => { + let program = compile_main(&workspace, package, &compile_options)?; + Ok((program, None)) + } + Some(test_name) => { + let (program, test_def) = + compile_test(&workspace, package, compile_options, test_name)?; + Ok((program, Some(test_def))) + } + }?; let (inputs_map, _) = read_inputs_from_file( &package.root_dir.join(prover_name).with_extension("toml"), @@ -136,7 +177,13 @@ fn load_and_compile_project( .encode(&inputs_map, None) .map_err(|_| LoadError::Generic("Failed to encode inputs".into()))?; - Ok((compiled_program, initial_witness)) + let project = DebugProject { + compiled_program, + initial_witness, + root_dir: workspace.root_dir.clone(), + package_name: package.name.to_string(), + }; + Ok((project, test_def)) } fn loop_uninitialized_dap( @@ -180,28 +227,49 @@ fn loop_uninitialized_dap( .get("skipInstrumentation") .and_then(|v| v.as_bool()) .unwrap_or(generate_acir); + let test_name = + additional_data.get("testName").and_then(|v| v.as_str()).map(String::from); + let oracle_resolver_url = additional_data + .get("oracleResolver") + .and_then(|v| v.as_str()) + .map(String::from); eprintln!("Project folder: {}", project_folder); eprintln!("Package: {}", package.unwrap_or("(default)")); eprintln!("Prover name: {}", prover_name); + let compile_options = compile_options_for_debugging( + generate_acir, + skip_instrumentation, + Some(expression_width), + CompileOptions::default(), + ); + match load_and_compile_project( project_folder, package, prover_name, - expression_width, - generate_acir, - skip_instrumentation, + compile_options, + test_name, ) { - Ok((compiled_program, initial_witness)) => { + Ok((project, test)) => { server.respond(req.ack()?)?; - - noir_debugger::run_dap_loop( - server, - &Bn254BlackBoxSolver(pedantic_solving), - compiled_program, - initial_witness, + let abi = project.compiled_program.abi.clone(); + let debug = project.compiled_program.debug.clone(); + + let result = noir_debugger::run_dap_loop( + &mut server, + project, + RunParams { + oracle_resolver_url, + pedantic_solving, + raw_source_printing: None, + }, )?; + + if let Some(test) = test { + analyze_test_result(&mut server, result, test, abi, debug)?; + } break; } Err(LoadError::Generic(message)) => { @@ -224,6 +292,47 @@ fn loop_uninitialized_dap( Ok(()) } +fn analyze_test_result( + server: &mut Server, + result: DebugExecutionResult, + test: TestDefinition, + abi: Abi, + debug: Vec, +) -> Result<(), ServerError> { + let test_status = match result { + DebugExecutionResult::Solved(result) => { + test_status_program_compile_pass(&test.function, &abi, &debug, &Ok(result)) + } + // Test execution failed + DebugExecutionResult::Error(error) => { + test_status_program_compile_pass(&test.function, &abi, &debug, &Err(error)) + } + // Execution didn't complete + DebugExecutionResult::Incomplete => { + TestStatus::Fail { message: "Execution halted".into(), error_diagnostic: None } + } + }; + + let test_result_message = match test_status { + TestStatus::Pass => "✓ Test passed".into(), + TestStatus::Fail { message, error_diagnostic } => { + let basic_message = format!("x Test failed: {message}"); + match error_diagnostic { + Some(diagnostic) => format!("{basic_message}.\n{diagnostic:#?}"), + None => basic_message, + } + } + TestStatus::CompileError(diagnostic) => format!("x Test failed.\n{diagnostic:#?}"), + TestStatus::Skipped => "* Test skipped".into(), + }; + + server.send_event(dap::events::Event::Output(OutputEventBody { + category: Some(OutputEventCategory::Console), + output: test_result_message, + ..OutputEventBody::default() + })) +} + fn run_preflight_check( expression_width: ExpressionWidth, args: DapCommand, @@ -235,15 +344,22 @@ fn run_preflight_check( }; let package = args.preflight_package.as_deref(); + let test_name = args.preflight_test_name; let prover_name = args.preflight_prover_name.as_deref().unwrap_or(PROVER_INPUT_FILE); + let compile_options: CompileOptions = compile_options_for_debugging( + args.preflight_generate_acir, + args.preflight_skip_instrumentation, + Some(expression_width), + CompileOptions::default(), + ); + let _ = load_and_compile_project( project_folder.as_str(), package, prover_name, - expression_width, - args.preflight_generate_acir, - args.preflight_skip_instrumentation, + compile_options, + test_name, )?; Ok(()) diff --git a/tooling/nargo_cli/src/cli/debug_cmd.rs b/tooling/nargo_cli/src/cli/debug_cmd.rs index f9303180fc0..13cc4a0b78a 100644 --- a/tooling/nargo_cli/src/cli/debug_cmd.rs +++ b/tooling/nargo_cli/src/cli/debug_cmd.rs @@ -1,28 +1,34 @@ use std::path::Path; +use std::time::Duration; use acvm::FieldElement; -use acvm::acir::native_types::WitnessStack; -use bn254_blackbox_solver::Bn254BlackBoxSolver; +use acvm::acir::native_types::{WitnessMap, WitnessStack}; use clap::Args; - use fm::FileManager; use nargo::constants::PROVER_INPUT_FILE; -use nargo::errors::CompileError; -use nargo::ops::{compile_program, compile_program_with_debug_instrumenter, report_errors}; +use nargo::ops::debug::{ + TestDefinition, compile_bin_package_for_debugging, compile_options_for_debugging, + compile_test_fn_for_debugging, get_test_function_for_debug, load_workspace_files, + prepare_package_for_debug, +}; +use nargo::ops::{ + TestStatus, check_crate_and_report_errors, test_status_program_compile_fail, + test_status_program_compile_pass, +}; use nargo::package::{CrateName, Package}; use nargo::workspace::Workspace; -use nargo::{insert_all_files_for_workspace_into_file_manager, parse_all}; use nargo_toml::PackageSelection; use noir_artifact_cli::fs::inputs::read_inputs_from_file; use noir_artifact_cli::fs::witness::save_witness_to_dir; -use noirc_abi::InputMap; -use noirc_abi::input_parser::InputValue; -use noirc_driver::{CompileOptions, CompiledProgram, file_manager_with_stdlib}; -use noirc_frontend::debug::DebugInstrumenter; -use noirc_frontend::hir::ParsedFiles; +use noir_debugger::{DebugExecutionResult, DebugProject, RunParams}; +use noirc_abi::Abi; +use noirc_driver::{CompileOptions, CompiledProgram}; +use noirc_frontend::hir::Context; -use super::compile_cmd::get_target_width; +use super::test_cmd::TestResult; +use super::test_cmd::formatters::Formatter; use super::{LockType, WorkspaceCommand}; +use crate::cli::test_cmd::formatters::PrettyFormatter; use crate::errors::CliError; /// Executes a circuit in debug mode @@ -37,7 +43,7 @@ pub(crate) struct DebugCommand { /// The name of the package to execute #[clap(long)] - pub(super) package: Option, + package: Option, #[clap(flatten)] compile_options: CompileOptions, @@ -53,6 +59,21 @@ pub(crate) struct DebugCommand { /// Raw string printing of source for testing #[clap(long, hide = true)] raw_source_printing: Option, + + /// Name (or substring) of the test function to debug + #[clap(long)] + test_name: Option, + + /// JSON RPC url to solve oracle calls + #[clap(long)] + oracle_resolver: Option, +} + +// TODO: find a better name +struct PackageParams<'a> { + prover_name: String, + witness_name: Option, + target_dir: &'a Path, } impl WorkspaceCommand for DebugCommand { @@ -73,201 +94,207 @@ impl WorkspaceCommand for DebugCommand { pub(crate) fn run(args: DebugCommand, workspace: Workspace) -> Result<(), CliError> { let acir_mode = args.acir_mode; let skip_instrumentation = args.skip_instrumentation.unwrap_or(acir_mode); - let target_dir = &workspace.target_directory_path(); - let Some(package) = workspace.into_iter().find(|p| p.is_binary()) else { + let package_params = PackageParams { + prover_name: args.prover_name, + witness_name: args.witness_name, + target_dir: &workspace.target_directory_path(), + }; + let run_params = RunParams { + pedantic_solving: args.compile_options.pedantic_solving, + raw_source_printing: args.raw_source_printing, + oracle_resolver_url: args.oracle_resolver, + }; + let workspace_clone = workspace.clone(); + + let Some(package) = workspace_clone.into_iter().find(|p| p.is_binary() || p.is_contract()) + else { println!( - "No matching binary packages found in workspace. Only binary packages can be debugged." + "No matching binary or contract packages found in workspace. Only these packages can be debugged." ); return Ok(()); }; - let compiled_program = compile_bin_package_for_debugging( - &workspace, - package, - acir_mode, - skip_instrumentation, - args.compile_options.clone(), - )?; - - let target_width = - get_target_width(package.expression_width, args.compile_options.expression_width); + let compile_options = + compile_options_for_debugging(acir_mode, skip_instrumentation, None, args.compile_options); - let compiled_program = nargo::ops::transform_program(compiled_program, target_width); + if let Some(test_name) = args.test_name { + debug_test(test_name, package, workspace, compile_options, run_params, package_params) + } else { + debug_main(package, workspace, compile_options, run_params, package_params) + } +} - run_async( - package, - compiled_program, - &args.prover_name, - &args.witness_name, - target_dir, - args.compile_options.pedantic_solving, - args.raw_source_printing.unwrap_or(false), - ) +fn print_test_result(test_result: TestResult, file_manager: &FileManager) { + let formatter: Box = Box::new(PrettyFormatter); + formatter + .test_end_sync(&test_result, 1, 1, file_manager, true, false, false) + .expect("Could not display test result"); } -pub(crate) fn compile_bin_package_for_debugging( +fn debug_test_fn( + test: &TestDefinition, + context: &mut Context, workspace: &Workspace, package: &Package, - acir_mode: bool, - skip_instrumentation: bool, compile_options: CompileOptions, -) -> Result { - let mut workspace_file_manager = file_manager_with_stdlib(Path::new("")); - insert_all_files_for_workspace_into_file_manager(workspace, &mut workspace_file_manager); - let mut parsed_files = parse_all(&workspace_file_manager); - - let compile_options = CompileOptions { - instrument_debug: !skip_instrumentation, - force_brillig: !acir_mode, - ..compile_options - }; - - let compilation_result = if !skip_instrumentation { - let debug_state = - instrument_package_files(&mut parsed_files, &workspace_file_manager, package); - - compile_program_with_debug_instrumenter( - &workspace_file_manager, - &parsed_files, - workspace, - package, - &compile_options, - None, - debug_state, - ) - } else { - compile_program( - &workspace_file_manager, - &parsed_files, - workspace, - package, - &compile_options, - None, - ) + run_params: RunParams, + package_params: PackageParams, +) -> TestResult { + let compiled_program = compile_test_fn_for_debugging(test, context, package, compile_options); + + let test_status = match compiled_program { + Ok(compiled_program) => { + let abi = compiled_program.abi.clone(); + let debug = compiled_program.debug.clone(); + + // Run debugger + let debug_result = + run_async(package, compiled_program, workspace, run_params, package_params); + + match debug_result { + Ok(DebugExecutionResult::Solved(result)) => { + test_status_program_compile_pass(&test.function, &abi, &debug, &Ok(result)) + } + Ok(DebugExecutionResult::Error(error)) => { + test_status_program_compile_pass(&test.function, &abi, &debug, &Err(error)) + } + Ok(DebugExecutionResult::Incomplete) => TestStatus::Fail { + message: "Incomplete execution. Debugger halted".to_string(), + error_diagnostic: None, + }, + Err(error) => TestStatus::Fail { + message: format!("Debugger failed: {error}"), + error_diagnostic: None, + }, + } + } + Err(err) => test_status_program_compile_fail(err, &test.function), }; - report_errors( - compilation_result, - &workspace_file_manager, - compile_options.deny_warnings, - compile_options.silence_warnings, + TestResult::new( + test.name.clone(), + package.name.to_string(), + test_status, + String::new(), + Duration::from_secs(1), // FIXME: hardcoded value ) } -/// Add debugging instrumentation to all parsed files belonging to the package -/// being compiled -fn instrument_package_files( - parsed_files: &mut ParsedFiles, - file_manager: &FileManager, +fn debug_main( package: &Package, -) -> DebugInstrumenter { - // Start off at the entry path and read all files in the parent directory. - let entry_path_parent = package - .entry_path - .parent() - .unwrap_or_else(|| panic!("The entry path is expected to be a single file within a directory and so should have a parent {:?}", package.entry_path)); - - let mut debug_instrumenter = DebugInstrumenter::default(); - - for (file_id, parsed_file) in parsed_files.iter_mut() { - let file_path = - file_manager.path(*file_id).expect("Parsed file ID not found in file manager"); - for ancestor in file_path.ancestors() { - if ancestor == entry_path_parent { - // file is in package - debug_instrumenter.instrument_module(&mut parsed_file.0, *file_id); - } - } - } + workspace: Workspace, + compile_options: CompileOptions, + run_params: RunParams, + package_params: PackageParams, +) -> Result<(), CliError> { + let compiled_program = + compile_bin_package_for_debugging(&workspace, package, &compile_options)?; + + run_async(package, compiled_program, &workspace, run_params, package_params)?; - debug_instrumenter + Ok(()) +} + +fn debug_test( + test_name: String, + package: &Package, + workspace: Workspace, + compile_options: CompileOptions, + run_params: RunParams, + package_params: PackageParams, +) -> Result<(), CliError> { + let (file_manager, mut parsed_files) = load_workspace_files(&workspace); + + let (mut context, crate_id) = + prepare_package_for_debug(&file_manager, &mut parsed_files, package, &workspace); + + check_crate_and_report_errors(&mut context, crate_id, &compile_options)?; + + let test = + get_test_function_for_debug(crate_id, &context, &test_name).map_err(CliError::Generic)?; + + let test_result = debug_test_fn( + &test, + &mut context, + &workspace, + package, + compile_options, + run_params, + package_params, + ); + print_test_result(test_result, &file_manager); + + Ok(()) } fn run_async( package: &Package, program: CompiledProgram, - prover_name: &str, - witness_name: &Option, - target_dir: &Path, - pedantic_solving: bool, - raw_source_printing: bool, -) -> Result<(), CliError> { + workspace: &Workspace, + run_params: RunParams, + package_params: PackageParams, +) -> Result { use tokio::runtime::Builder; let runtime = Builder::new_current_thread().enable_all().build().unwrap(); + let abi = &program.abi.clone(); runtime.block_on(async { println!("[{}] Starting debugger", package.name); - let (return_value, witness_stack) = debug_program_and_decode( - program, - package, - prover_name, - pedantic_solving, - raw_source_printing, - )?; - - if let Some(solved_witness_stack) = witness_stack { - println!("[{}] Circuit witness successfully solved", package.name); - - if let Some(return_value) = return_value { - println!("[{}] Circuit output: {return_value:?}", package.name); - } + let initial_witness = parse_initial_witness(package, &package_params.prover_name, abi)?; - if let Some(witness_name) = witness_name { - let witness_path = - save_witness_to_dir(&solved_witness_stack, witness_name, target_dir)?; + let project = DebugProject { + compiled_program: program, + initial_witness, + root_dir: workspace.root_dir.clone(), + package_name: package.name.to_string(), + }; + let result = noir_debugger::run_repl_session(project, run_params); - println!("[{}] Witness saved to {}", package.name, witness_path.display()); - } - } else { - println!("Debugger execution halted."); + if let DebugExecutionResult::Solved(ref witness_stack) = result { + println!("[{}] Circuit witness successfully solved", package.name); + decode_and_save_program_witness( + &package.name, + witness_stack, + abi, + package_params.witness_name, + package_params.target_dir, + )?; } - Ok(()) + Ok(result) }) } -fn debug_program_and_decode( - program: CompiledProgram, - package: &Package, - prover_name: &str, - pedantic_solving: bool, - raw_source_printing: bool, -) -> Result<(Option, Option>), CliError> { - // Parse the initial witness values from Prover.toml - let (inputs_map, _) = read_inputs_from_file( - &package.root_dir.join(prover_name).with_extension("toml"), - &program.abi, - )?; - let program_abi = program.abi.clone(); - let witness_stack = debug_program(program, &inputs_map, pedantic_solving, raw_source_printing)?; - - match witness_stack { - Some(witness_stack) => { - let main_witness = &witness_stack - .peek() - .expect("Should have at least one witness on the stack") - .witness; - let (_, return_value) = program_abi.decode(main_witness)?; - Ok((return_value, Some(witness_stack))) - } - None => Ok((None, None)), +fn decode_and_save_program_witness( + package_name: &CrateName, + witness_stack: &WitnessStack, + abi: &Abi, + target_witness_name: Option, + target_dir: &Path, +) -> Result<(), CliError> { + let main_witness = + &witness_stack.peek().expect("Should have at least one witness on the stack").witness; + + if let (_, Some(return_value)) = abi.decode(main_witness)? { + println!("[{}] Circuit output: {return_value:?}", package_name); + } + + if let Some(witness_name) = target_witness_name { + let witness_path = save_witness_to_dir(witness_stack, &witness_name, target_dir)?; + println!("[{}] Witness saved to {}", package_name, witness_path.display()); } + Ok(()) } -pub(crate) fn debug_program( - compiled_program: CompiledProgram, - inputs_map: &InputMap, - pedantic_solving: bool, - raw_source_printing: bool, -) -> Result>, CliError> { - let initial_witness = compiled_program.abi.encode(inputs_map, None)?; - - noir_debugger::run_repl_session( - &Bn254BlackBoxSolver(pedantic_solving), - compiled_program, - initial_witness, - raw_source_printing, - ) - .map_err(CliError::from) +fn parse_initial_witness( + package: &Package, + prover_name: &str, + abi: &Abi, +) -> Result, CliError> { + // Parse the initial witness values from Prover.toml + let (inputs_map, _) = + read_inputs_from_file(&package.root_dir.join(prover_name).with_extension("toml"), abi)?; + let initial_witness = abi.encode(&inputs_map, None)?; + Ok(initial_witness) } diff --git a/tooling/nargo_cli/src/cli/export_cmd.rs b/tooling/nargo_cli/src/cli/export_cmd.rs index fd809c41b18..d4407a059a0 100644 --- a/tooling/nargo_cli/src/cli/export_cmd.rs +++ b/tooling/nargo_cli/src/cli/export_cmd.rs @@ -1,5 +1,5 @@ use nargo::errors::CompileError; -use nargo::ops::report_errors; +use nargo::ops::{check_crate_and_report_errors, report_errors}; use noir_artifact_cli::fs::artifact::save_program_to_file; use noirc_errors::CustomDiagnostic; use noirc_frontend::hir::ParsedFiles; @@ -18,8 +18,6 @@ use clap::Args; use crate::errors::CliError; -use super::check_cmd::check_crate_and_report_errors; - use super::{LockType, PackageOptions, WorkspaceCommand}; #[allow(rustdoc::broken_intra_doc_links)] diff --git a/tooling/nargo_cli/src/cli/test_cmd.rs b/tooling/nargo_cli/src/cli/test_cmd.rs index 13c6efe2aee..59543260f84 100644 --- a/tooling/nargo_cli/src/cli/test_cmd.rs +++ b/tooling/nargo_cli/src/cli/test_cmd.rs @@ -14,15 +14,19 @@ use clap::Args; use fm::FileManager; use formatters::{Formatter, JsonFormatter, PrettyFormatter, TerseFormatter}; use nargo::{ - PrintOutput, foreign_calls::DefaultForeignCallBuilder, - insert_all_files_for_workspace_into_file_manager, ops::TestStatus, package::Package, parse_all, - prepare_package, workspace::Workspace, + PrintOutput, + foreign_calls::DefaultForeignCallBuilder, + insert_all_files_for_workspace_into_file_manager, + ops::{TestStatus, check_crate_and_report_errors}, + package::Package, + parse_all, prepare_package, + workspace::Workspace, }; use nargo_toml::PackageSelection; use noirc_driver::{CompileOptions, check_crate}; use noirc_frontend::hir::{FunctionNameMatch, ParsedFiles}; -use crate::{cli::check_cmd::check_crate_and_report_errors, errors::CliError}; +use crate::errors::CliError; use super::{LockType, PackageOptions, WorkspaceCommand}; @@ -116,7 +120,7 @@ struct Test<'a> { runner: Box (TestStatus, String) + Send + UnwindSafe + 'a>, } -struct TestResult { +pub(crate) struct TestResult { name: String, package_name: String, status: TestStatus, @@ -124,6 +128,18 @@ struct TestResult { time_to_run: Duration, } +impl TestResult { + pub(crate) fn new( + name: String, + package_name: String, + status: TestStatus, + output: String, + time_to_run: Duration, + ) -> Self { + TestResult { name, package_name, status, output, time_to_run } + } +} + const STACK_SIZE: usize = 4 * 1024 * 1024; pub(crate) fn run(args: TestCommand, workspace: Workspace) -> Result<(), CliError> { diff --git a/tooling/nargo_cli/src/cli/test_cmd/formatters.rs b/tooling/nargo_cli/src/cli/test_cmd/formatters.rs index 68628129245..97622fc9097 100644 --- a/tooling/nargo_cli/src/cli/test_cmd/formatters.rs +++ b/tooling/nargo_cli/src/cli/test_cmd/formatters.rs @@ -24,7 +24,7 @@ use super::TestResult; /// to humans rely on the `sync` events to show a more predictable output (package by package), /// and formatters that output to a machine-readable format (like JSON) rely on the `async` /// events to show things as soon as they happen, regardless of a package ordering. -pub(super) trait Formatter: Send + Sync + RefUnwindSafe { +pub(crate) trait Formatter: Send + Sync + RefUnwindSafe { fn package_start_async(&self, package_name: &str, test_count: usize) -> std::io::Result<()>; fn package_start_sync(&self, package_name: &str, test_count: usize) -> std::io::Result<()>; @@ -64,7 +64,7 @@ pub(super) trait Formatter: Send + Sync + RefUnwindSafe { ) -> std::io::Result<()>; } -pub(super) struct PrettyFormatter; +pub(crate) struct PrettyFormatter; impl Formatter for PrettyFormatter { fn package_start_async(&self, _package_name: &str, _test_count: usize) -> std::io::Result<()> {