From 104be7248b92840b93836e73147bfb3276e9733e Mon Sep 17 00:00:00 2001 From: Ana Perez Ghiglia Date: Tue, 25 Mar 2025 11:33:17 -0300 Subject: [PATCH] feat(debugger): debug tests functions (#5208) (#8) - Add the possibility to compile and debug a test function - Add a new config parameter to configure an external oracle resolver when debugging - REPL: execute the debugger in a separate thread than the repl cli - Add new `Debug test` codelens request. Related vscode-noir PR (noir-lang/vscode-noir#100) --------- Co-authored-by: Martin Verzilli --- Cargo.lock | 1 + compiler/noirc_frontend/src/debug/mod.rs | 29 +- .../debugger/debugging_with_the_repl.md | 47 +- .../how_to/debugger/debugging_with_vs_code.md | 18 +- docs/docs/reference/debugger/debugger_repl.md | 42 +- .../reference/debugger/debugger_vscode.md | 54 +- docs/docs/tooling/debugger.md | 4 - .../static/img/debugger/debugger-codelens.png | Bin 0 -> 93022 bytes tooling/debugger/Cargo.toml | 4 +- tooling/debugger/build.rs | 74 ++- tooling/debugger/ignored-noir-tests.txt | 12 + tooling/debugger/src/context.rs | 165 ++++- tooling/debugger/src/dap.rs | 100 +-- tooling/debugger/src/foreign_calls.rs | 38 +- tooling/debugger/src/lib.rs | 31 +- tooling/debugger/src/repl.rs | 591 ++++++++++++------ tooling/debugger/tests/debug.rs | 207 +++--- tooling/lsp/src/requests/code_lens_request.rs | 21 +- tooling/nargo/src/errors.rs | 33 + tooling/nargo/src/foreign_calls/rpc.rs | 78 ++- tooling/nargo/src/ops/compile.rs | 17 +- tooling/nargo/src/ops/debug.rs | 216 +++++++ tooling/nargo/src/ops/execute.rs | 36 +- tooling/nargo/src/ops/mod.rs | 10 +- tooling/nargo/src/ops/test.rs | 9 +- tooling/nargo_cli/src/cli/check_cmd.rs | 23 +- tooling/nargo_cli/src/cli/dap_cmd.rs | 194 ++++-- tooling/nargo_cli/src/cli/debug_cmd.rs | 367 ++++++----- tooling/nargo_cli/src/cli/export_cmd.rs | 4 +- tooling/nargo_cli/src/cli/test_cmd.rs | 26 +- .../nargo_cli/src/cli/test_cmd/formatters.rs | 4 +- 31 files changed, 1715 insertions(+), 740 deletions(-) create mode 100644 docs/static/img/debugger/debugger-codelens.png create mode 100644 tooling/debugger/ignored-noir-tests.txt create mode 100644 tooling/nargo/src/ops/debug.rs 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 0000000000000000000000000000000000000000..a71f5f02a73d9442e102e8f40c693da7c07e2aa4 GIT binary patch literal 93022 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 PLx7iui?6f5DLV5nUT1m9 literal 0 HcmV?d00001 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<()> {