diff --git a/.github/critical_libraries_status/AztecProtocol/aztec-packages/noir-projects/aztec-nr.failures.jsonl b/.github/critical_libraries_status/AztecProtocol/aztec-packages/noir-projects/aztec-nr.failures.jsonl new file mode 100644 index 00000000000..e69de29bb2d diff --git a/.github/critical_libraries_status/AztecProtocol/aztec-packages/noir-projects/noir-contracts.failures.jsonl b/.github/critical_libraries_status/AztecProtocol/aztec-packages/noir-projects/noir-contracts.failures.jsonl new file mode 100644 index 00000000000..e69de29bb2d diff --git a/.github/critical_libraries_status/AztecProtocol/aztec-packages/noir-projects/noir-protocol-circuits/crates/blob.failures.jsonl b/.github/critical_libraries_status/AztecProtocol/aztec-packages/noir-projects/noir-protocol-circuits/crates/blob.failures.jsonl new file mode 100644 index 00000000000..e69de29bb2d diff --git a/.github/critical_libraries_status/AztecProtocol/aztec-packages/noir-projects/noir-protocol-circuits/crates/parity-lib.failures.jsonl b/.github/critical_libraries_status/AztecProtocol/aztec-packages/noir-projects/noir-protocol-circuits/crates/parity-lib.failures.jsonl new file mode 100644 index 00000000000..e69de29bb2d diff --git a/.github/critical_libraries_status/AztecProtocol/aztec-packages/noir-projects/noir-protocol-circuits/crates/private-kernel-lib.failures.jsonl b/.github/critical_libraries_status/AztecProtocol/aztec-packages/noir-projects/noir-protocol-circuits/crates/private-kernel-lib.failures.jsonl new file mode 100644 index 00000000000..e69de29bb2d diff --git a/.github/critical_libraries_status/AztecProtocol/aztec-packages/noir-projects/noir-protocol-circuits/crates/reset-kernel-lib.failures.jsonl b/.github/critical_libraries_status/AztecProtocol/aztec-packages/noir-projects/noir-protocol-circuits/crates/reset-kernel-lib.failures.jsonl new file mode 100644 index 00000000000..e69de29bb2d diff --git a/.github/critical_libraries_status/AztecProtocol/aztec-packages/noir-projects/noir-protocol-circuits/crates/rollup-lib.failures.jsonl b/.github/critical_libraries_status/AztecProtocol/aztec-packages/noir-projects/noir-protocol-circuits/crates/rollup-lib.failures.jsonl new file mode 100644 index 00000000000..e69de29bb2d diff --git a/.github/critical_libraries_status/AztecProtocol/aztec-packages/noir-projects/noir-protocol-circuits/crates/types.failures.jsonl b/.github/critical_libraries_status/AztecProtocol/aztec-packages/noir-projects/noir-protocol-circuits/crates/types.failures.jsonl new file mode 100644 index 00000000000..e69de29bb2d diff --git a/.github/critical_libraries_status/README.md b/.github/critical_libraries_status/README.md new file mode 100644 index 00000000000..47e9d7ad6ed --- /dev/null +++ b/.github/critical_libraries_status/README.md @@ -0,0 +1,20 @@ +# Critical Libraries Status + +This directory contains one `.failures.jsonl` file per external directory that is checked by CI. +CI will run the external repository tests and compare the test failures against those recorded +in these files. If there's a difference, CI will fail. + +This allows us to mark some tests as expected to fail if we introduce breaking changes. +When tests are fixed on the external repository, CI will let us know that we need to remove +the `.failures.jsonl` failures on our side. + +The format of the `.failures.jsonl` files is one JSON per line with a failure: + +```json +{"suite":"one","name":"foo"} +``` + +If it's expected that an external repository doesn't compile (because a PR introduces breaking changes +to, say, the type system) you can remove the `.failures.jsonl` file for that repository and CI +will pass again. Once the repository compiles again, CI will let us know and require us to put +back the `.failures.jsonl` file. \ No newline at end of file diff --git a/.github/critical_libraries_status/noir-lang/ec/.actual.jsonl b/.github/critical_libraries_status/noir-lang/ec/.actual.jsonl new file mode 100644 index 00000000000..cb56f792778 --- /dev/null +++ b/.github/critical_libraries_status/noir-lang/ec/.actual.jsonl @@ -0,0 +1,4 @@ +{"event":"started","name":"one","test_count":1,"type":"suite"} +{"event":"started","name":"foo","suite":"one","type":"test"} +{"event":"failed","exec_time":0.05356625,"name":"foo","suite":"one","type":"test"} +{"event":"ok","failed":0,"ignored":0,"passed":1,"type":"suite"} \ No newline at end of file diff --git a/.github/critical_libraries_status/noir-lang/ec/.actual.jsonl.jq b/.github/critical_libraries_status/noir-lang/ec/.actual.jsonl.jq new file mode 100644 index 00000000000..1123e7e68e4 --- /dev/null +++ b/.github/critical_libraries_status/noir-lang/ec/.actual.jsonl.jq @@ -0,0 +1 @@ +{"suite":"one","name":"foo"} diff --git a/.github/critical_libraries_status/noir-lang/ec/.failures.jsonl b/.github/critical_libraries_status/noir-lang/ec/.failures.jsonl new file mode 100644 index 00000000000..e69de29bb2d diff --git a/.github/critical_libraries_status/noir-lang/ec/.failures.jsonl.jq b/.github/critical_libraries_status/noir-lang/ec/.failures.jsonl.jq new file mode 100644 index 00000000000..e69de29bb2d diff --git a/.github/critical_libraries_status/noir-lang/eddsa/.failures.jsonl b/.github/critical_libraries_status/noir-lang/eddsa/.failures.jsonl new file mode 100644 index 00000000000..e69de29bb2d diff --git a/.github/critical_libraries_status/noir-lang/mimc/.failures.jsonl b/.github/critical_libraries_status/noir-lang/mimc/.failures.jsonl new file mode 100644 index 00000000000..e69de29bb2d diff --git a/.github/critical_libraries_status/noir-lang/noir-bignum/.failures.jsonl b/.github/critical_libraries_status/noir-lang/noir-bignum/.failures.jsonl new file mode 100644 index 00000000000..e69de29bb2d diff --git a/.github/critical_libraries_status/noir-lang/noir-edwards/.failures.jsonl b/.github/critical_libraries_status/noir-lang/noir-edwards/.failures.jsonl new file mode 100644 index 00000000000..e69de29bb2d diff --git a/.github/critical_libraries_status/noir-lang/noir_base64/.failures.jsonl b/.github/critical_libraries_status/noir-lang/noir_base64/.failures.jsonl new file mode 100644 index 00000000000..e69de29bb2d diff --git a/.github/critical_libraries_status/noir-lang/noir_bigcurve/.failures.jsonl b/.github/critical_libraries_status/noir-lang/noir_bigcurve/.failures.jsonl new file mode 100644 index 00000000000..e69de29bb2d diff --git a/.github/critical_libraries_status/noir-lang/noir_json_parser/.failures.jsonl b/.github/critical_libraries_status/noir-lang/noir_json_parser/.failures.jsonl new file mode 100644 index 00000000000..e69de29bb2d diff --git a/.github/critical_libraries_status/noir-lang/noir_rsa/.failures.jsonl b/.github/critical_libraries_status/noir-lang/noir_rsa/.failures.jsonl new file mode 100644 index 00000000000..e69de29bb2d diff --git a/.github/critical_libraries_status/noir-lang/noir_sort/.failures.jsonl b/.github/critical_libraries_status/noir-lang/noir_sort/.failures.jsonl new file mode 100644 index 00000000000..e69de29bb2d diff --git a/.github/critical_libraries_status/noir-lang/noir_string_search/.failures.jsonl b/.github/critical_libraries_status/noir-lang/noir_string_search/.failures.jsonl new file mode 100644 index 00000000000..e69de29bb2d diff --git a/.github/critical_libraries_status/noir-lang/schnorr/.failures.jsonl b/.github/critical_libraries_status/noir-lang/schnorr/.failures.jsonl new file mode 100644 index 00000000000..e69de29bb2d diff --git a/.github/critical_libraries_status/noir-lang/sparse_array/.failures.jsonl b/.github/critical_libraries_status/noir-lang/sparse_array/.failures.jsonl new file mode 100644 index 00000000000..e69de29bb2d diff --git a/.github/scripts/check_test_results.sh b/.github/scripts/check_test_results.sh new file mode 100755 index 00000000000..c844c025876 --- /dev/null +++ b/.github/scripts/check_test_results.sh @@ -0,0 +1,38 @@ +#!/bin/bash +set -eu + +# Usage: ./check_test_results.sh +# Compares the output of two test results of the same repository. +# If any of the files doesn't exist or is empty, the script will consider that the test suite +# couldn't be compiled. + +function process_json_lines() { + cat $1 | jq -c 'select(.type == "test" and .event == "failed") | {suite: .suite, name: .name}' | jq -s -c 'sort_by(.suite, .name) | .[]' > $1.jq +} + +if [ -f $1 ] && [ -f $2 ]; then + # Both files exist, let's compare them + $(process_json_lines $2) + if ! diff $1 $2.jq; then + echo "Error: test failures don't match expected failures" + echo "Lines prefixed with '>' are new test failures (you could add them to '$1')" + echo "Lines prefixed with '<' are tests that were expected to fail but passed (you could remove them from '$1')" + fi +elif [ -f $1 ]; then + # Only the expected file exists, which means the actual test couldn't be compiled. + echo "Error: external library tests couldn't be compiled." + echo "You could rename '$1' to '$1.does_not_compile' if it's expected that the external library can't be compiled." + exit -1 +elif [ -f $2 ]; then + # Only the actual file exists, which means we are expecting the external library + # not to compile but it did. + echo "Error: expected external library not to compile, but it did." + echo "You could create '$1' with these contents:" + $(process_json_lines $2) + cat $2.jq + exit -1 +else + # Both files don't exists, which means we are expecting the external library not + # to compile, and it didn't, so all is good. + exit 0 +fi \ No newline at end of file diff --git a/.github/workflows/reports.yml b/.github/workflows/reports.yml index 1e355dc9e6b..37864dd9b68 100644 --- a/.github/workflows/reports.yml +++ b/.github/workflows/reports.yml @@ -225,8 +225,8 @@ jobs: retention-days: 3 overwrite: true - generate_compilation_report: - name: Compilation time + generate_compilation_and_execution_report: + name: Compilation and execution time needs: [build-nargo] runs-on: ubuntu-22.04 permissions: @@ -253,9 +253,14 @@ jobs: working-directory: ./test_programs run: | ./compilation_report.sh - cat compilation_report.json mv compilation_report.json ../compilation_report.json + - name: Generate Execution report + working-directory: ./test_programs + run: | + ./execution_report.sh + mv execution_report.json ../execution_report.json + - name: Upload compilation report uses: actions/upload-artifact@v4 with: @@ -263,8 +268,16 @@ jobs: path: compilation_report.json retention-days: 3 overwrite: true + + - name: Upload execution report + uses: actions/upload-artifact@v4 + with: + name: in_progress_execution_report + path: execution_report.json + retention-days: 3 + overwrite: true - external_repo_compilation_report: + external_repo_compilation_and_execution_report: needs: [build-nargo] runs-on: ubuntu-latest timeout-minutes: 15 @@ -272,7 +285,7 @@ jobs: fail-fast: false matrix: include: - - project: { repo: AztecProtocol/aztec-packages, path: noir-projects/noir-contracts } + - project: { repo: AztecProtocol/aztec-packages, path: noir-projects/noir-contracts, is_library: true } - project: { repo: AztecProtocol/aztec-packages, path: noir-projects/noir-protocol-circuits/crates/parity-root } - project: { repo: AztecProtocol/aztec-packages, path: noir-projects/noir-protocol-circuits/crates/private-kernel-inner } - project: { repo: AztecProtocol/aztec-packages, path: noir-projects/noir-protocol-circuits/crates/private-kernel-tail } @@ -280,7 +293,7 @@ jobs: - project: { repo: AztecProtocol/aztec-packages, path: noir-projects/noir-protocol-circuits/crates/rollup-base-private } - project: { repo: AztecProtocol/aztec-packages, path: noir-projects/noir-protocol-circuits/crates/rollup-base-public } - name: External repo compilation report - ${{ matrix.project.repo }}/${{ matrix.project.path }} + name: External repo compilation and execution reports - ${{ matrix.project.repo }}/${{ matrix.project.path }} steps: - name: Download nargo binary uses: actions/download-artifact@v4 @@ -302,6 +315,13 @@ jobs: sparse-checkout: | test_programs/compilation_report.sh sparse-checkout-cone-mode: false + + - uses: actions/checkout@v4 + with: + path: scripts + sparse-checkout: | + test_programs/execution_report.sh + sparse-checkout-cone-mode: false - name: Checkout uses: actions/checkout@v4 @@ -316,9 +336,16 @@ jobs: mv /home/runner/work/noir/noir/scripts/test_programs/compilation_report.sh ./compilation_report.sh chmod +x ./compilation_report.sh ./compilation_report.sh 1 + + - name: Generate execution report + working-directory: ./test-repo/${{ matrix.project.path }} + if: ${{ !matrix.project.is_library }} + run: | + mv /home/runner/work/noir/noir/scripts/test_programs/execution_report.sh ./execution_report.sh + ./execution_report.sh 1 - name: Move compilation report - id: report + id: compilation_report shell: bash run: | PACKAGE_NAME=${{ matrix.project.path }} @@ -326,18 +353,36 @@ jobs: mv ./test-repo/${{ matrix.project.path }}/compilation_report.json ./compilation_report_$PACKAGE_NAME.json echo "compilation_report_name=$PACKAGE_NAME" >> $GITHUB_OUTPUT + - name: Move execution report + id: execution_report + shell: bash + if: ${{ !matrix.project.is_library }} + run: | + PACKAGE_NAME=${{ matrix.project.path }} + PACKAGE_NAME=$(basename $PACKAGE_NAME) + mv ./test-repo/${{ matrix.project.path }}/execution_report.json ./execution_report_$PACKAGE_NAME.json + echo "execution_report_name=$PACKAGE_NAME" >> $GITHUB_OUTPUT + - name: Upload compilation report uses: actions/upload-artifact@v4 with: - name: compilation_report_${{ steps.report.outputs.compilation_report_name }} - path: compilation_report_${{ steps.report.outputs.compilation_report_name }}.json + name: compilation_report_${{ steps.compilation_report.outputs.compilation_report_name }} + path: compilation_report_${{ steps.compilation_report.outputs.compilation_report_name }}.json + retention-days: 3 + overwrite: true + + - name: Upload execution report + uses: actions/upload-artifact@v4 + with: + name: execution_report_${{ steps.execution_report.outputs.execution_report_name }} + path: execution_report_${{ steps.execution_report.outputs.execution_report_name }}.json retention-days: 3 overwrite: true upload_compilation_report: name: Upload compilation report - needs: [generate_compilation_report, external_repo_compilation_report] - # We want this job to run even if one variation of the matrix in `external_repo_compilation_report` fails + needs: [generate_compilation_and_execution_report, external_repo_compilation_and_execution_report] + # We want this job to run even if one variation of the matrix in `external_repo_compilation_and_execution_report` fails if: always() runs-on: ubuntu-latest permissions: @@ -491,48 +536,47 @@ jobs: header: memory message: ${{ steps.memory_report.outputs.markdown }} - generate_compilation_report: - name: Compilation time - needs: [build-nargo] - runs-on: ubuntu-22.04 + upload_execution_report: + name: Upload execution report + needs: [generate_compilation_and_execution_report, external_repo_compilation_and_execution_report] + # We want this job to run even if one variation of the matrix in `external_repo_compilation_and_execution_report` fails + if: always() + runs-on: ubuntu-latest permissions: pull-requests: write steps: - uses: actions/checkout@v4 - - name: Download nargo binary + - name: Download initial execution report uses: actions/download-artifact@v4 with: - name: nargo - path: ./nargo + name: in_progress_execution_report - - name: Set nargo on PATH - run: | - nargo_binary="${{ github.workspace }}/nargo/nargo" - chmod +x $nargo_binary - echo "$(dirname $nargo_binary)" >> $GITHUB_PATH - export PATH="$PATH:$(dirname $nargo_binary)" - nargo -V + - name: Download matrix execution reports + uses: actions/download-artifact@v4 + with: + pattern: execution_report_* + path: ./reports - - name: Generate Compilation report - working-directory: ./test_programs + - name: Merge execution reports using jq run: | - ./compilation_report.sh - mv compilation_report.json ../compilation_report.json + mv ./.github/scripts/merge-bench-reports.sh merge-bench-reports.sh + ./merge-bench-reports.sh execution_report - - name: Parse compilation report - id: compilation_report - uses: noir-lang/noir-bench-report@0d7464a8c39170523932d7846b6e6b458a294aea + - name: Parse execution report + id: execution_report + uses: noir-lang/noir-bench-report@0954121203ee55dcda5c7397b9c669c439a20922 with: - report: compilation_report.json + report: execution_report.json header: | - # Compilation Report - memory_report: false + # Execution Report + execution_report: true - name: Add memory report to sticky comment if: github.event_name == 'pull_request' || github.event_name == 'pull_request_target' uses: marocchino/sticky-pull-request-comment@v2 with: - header: compilation - message: ${{ steps.compilation_report.outputs.markdown }} + header: execution_time + message: ${{ steps.execution_report.outputs.markdown }} + diff --git a/.github/workflows/test-js-packages.yml b/.github/workflows/test-js-packages.yml index d227b4eb00b..e593389a971 100644 --- a/.github/workflows/test-js-packages.yml +++ b/.github/workflows/test-js-packages.yml @@ -543,8 +543,6 @@ jobs: external-repo-checks: needs: [build-nargo, critical-library-list] runs-on: ubuntu-22.04 - # Only run when 'run-external-checks' label is present - if: contains(github.event.pull_request.labels.*.name, 'run-external-checks') timeout-minutes: 30 strategy: fail-fast: false @@ -557,11 +555,17 @@ jobs: - project: { repo: AztecProtocol/aztec-packages, path: noir-projects/noir-protocol-circuits/crates/parity-lib } - project: { repo: AztecProtocol/aztec-packages, path: noir-projects/noir-protocol-circuits/crates/private-kernel-lib } - project: { repo: AztecProtocol/aztec-packages, path: noir-projects/noir-protocol-circuits/crates/reset-kernel-lib } - - project: { repo: AztecProtocol/aztec-packages, path: noir-projects/noir-protocol-circuits/crates/rollup-lib } - project: { repo: AztecProtocol/aztec-packages, path: noir-projects/noir-protocol-circuits/crates/types } + # Use 1 test threads for rollup-lib because each test requires a lot of memory, and multiple ones in parallel exceed the maximum memory limit. + - project: { repo: AztecProtocol/aztec-packages, path: noir-projects/noir-protocol-circuits/crates/rollup-lib, nargo_args: "--test-threads 1" } name: Check external repo - ${{ matrix.project.repo }}/${{ matrix.project.path }} steps: + - name: Checkout + uses: actions/checkout@v4 + with: + path: noir-repo + - name: Checkout uses: actions/checkout@v4 with: @@ -592,9 +596,14 @@ jobs: - name: Run nargo test working-directory: ./test-repo/${{ matrix.project.path }} - run: nargo test -q --silence-warnings + run: | + out=$(nargo test --silence-warnings --skip-brillig-constraints-check --format json ${{ matrix.project.nargo_args }}) && echo "$out" > ${{ github.workspace }}/noir-repo/.github/critical_libraries_status/${{ matrix.project.repo }}/${{ matrix.project.path }}.actual.jsonl env: NARGO_IGNORE_TEST_FAILURES_FROM_FOREIGN_CALLS: true + + - name: Compare test results + working-directory: ./noir-repo + run: .github/scripts/check_test_results.sh .github/critical_libraries_status/${{ matrix.project.repo }}/${{ matrix.project.path }}.failures.jsonl .github/critical_libraries_status/${{ matrix.project.repo }}/${{ matrix.project.path }}.actual.jsonl # This is a job which depends on all test jobs and reports the overall status. # This allows us to add/remove test jobs without having to update the required workflows. diff --git a/.gitignore b/.gitignore index 8442d688fbf..9bfb8a89331 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ gates_report.json gates_report_brillig.json gates_report_brillig_execution.json compilation_report.json +execution_report.json # Github Actions scratch space # This gives a location to download artifacts into the repository in CI without making git dirty. diff --git a/compiler/noirc_driver/src/lib.rs b/compiler/noirc_driver/src/lib.rs index 9318e4d2b5c..1b311504b5c 100644 --- a/compiler/noirc_driver/src/lib.rs +++ b/compiler/noirc_driver/src/lib.rs @@ -150,6 +150,10 @@ pub struct CompileOptions { /// A lower value keeps the original program if it was smaller, even if it has more jumps. #[arg(long, hide = true, allow_hyphen_values = true)] pub max_bytecode_increase_percent: Option, + + /// Used internally to test for non-determinism in the compiler. + #[clap(long, hide = true)] + pub check_non_determinism: bool, } pub fn parse_expression_width(input: &str) -> Result { diff --git a/compiler/noirc_evaluator/src/acir/mod.rs b/compiler/noirc_evaluator/src/acir/mod.rs index e7b011b6d7b..43eb7df303f 100644 --- a/compiler/noirc_evaluator/src/acir/mod.rs +++ b/compiler/noirc_evaluator/src/acir/mod.rs @@ -2905,7 +2905,6 @@ mod test { ssa::{ function_builder::FunctionBuilder, ir::{ - call_stack::CallStack, function::FunctionId, instruction::BinaryOp, map::Id, @@ -2932,8 +2931,7 @@ mod test { builder.new_function("foo".into(), foo_id, inline_type); } // Set a call stack for testing whether `brillig_locations` in the `GeneratedAcir` was accurately set. - let mut stack = CallStack::unit(Location::dummy()); - stack.push_back(Location::dummy()); + let stack = vec![Location::dummy(), Location::dummy()]; let call_stack = builder.current_function.dfg.call_stack_data.get_or_insert_locations(stack); builder.set_call_stack(call_stack); diff --git a/compiler/noirc_evaluator/src/brillig/brillig_ir/artifact.rs b/compiler/noirc_evaluator/src/brillig/brillig_ir/artifact.rs index be4a6b84bc1..3654a95a03f 100644 --- a/compiler/noirc_evaluator/src/brillig/brillig_ir/artifact.rs +++ b/compiler/noirc_evaluator/src/brillig/brillig_ir/artifact.rs @@ -27,7 +27,7 @@ pub(crate) struct GeneratedBrillig { pub(crate) locations: BTreeMap, pub(crate) error_types: BTreeMap, pub(crate) name: String, - pub(crate) procedure_locations: HashMap, + pub(crate) procedure_locations: BTreeMap, } #[derive(Default, Debug, Clone)] @@ -61,7 +61,7 @@ pub(crate) struct BrilligArtifact { /// This is created as artifacts are linked together and allows us to determine /// which opcodes originate from reusable procedures.s /// The range is inclusive for both start and end opcode locations. - pub(crate) procedure_locations: HashMap, + pub(crate) procedure_locations: BTreeMap, } /// A pointer to a location in the opcode. diff --git a/compiler/noirc_evaluator/src/errors.rs b/compiler/noirc_evaluator/src/errors.rs index ee662c50b75..1e484f8af59 100644 --- a/compiler/noirc_evaluator/src/errors.rs +++ b/compiler/noirc_evaluator/src/errors.rs @@ -201,7 +201,7 @@ impl RuntimeError { RuntimeError::UnknownLoopBound { .. } => { let primary_message = self.to_string(); let location = - self.call_stack().back().expect("Expected RuntimeError to have a location"); + self.call_stack().last().expect("Expected RuntimeError to have a location"); Diagnostic::simple_error( primary_message, @@ -212,7 +212,7 @@ impl RuntimeError { _ => { let message = self.to_string(); let location = - self.call_stack().back().unwrap_or_else(|| panic!("Expected RuntimeError to have a location. Error message: {message}")); + self.call_stack().last().unwrap_or_else(|| panic!("Expected RuntimeError to have a location. Error message: {message}")); Diagnostic::simple_error(message, String::new(), location.span) } diff --git a/compiler/noirc_evaluator/src/ssa/ir/call_stack.rs b/compiler/noirc_evaluator/src/ssa/ir/call_stack.rs index 113609f4a11..e1df616bc66 100644 --- a/compiler/noirc_evaluator/src/ssa/ir/call_stack.rs +++ b/compiler/noirc_evaluator/src/ssa/ir/call_stack.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use noirc_errors::Location; -pub(crate) type CallStack = im::Vector; +pub(crate) type CallStack = Vec; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub(crate) struct CallStackId(u32); @@ -57,9 +57,9 @@ impl Default for CallStackHelper { impl CallStackHelper { /// Construct a CallStack from a CallStackId pub(crate) fn get_call_stack(&self, mut call_stack: CallStackId) -> CallStack { - let mut result = im::Vector::new(); + let mut result = Vec::new(); while let Some(parent) = self.locations[call_stack.index()].parent { - result.push_back(self.locations[call_stack.index()].value); + result.push(self.locations[call_stack.index()].value); call_stack = parent; } result diff --git a/compiler/noirc_evaluator/src/ssa/ir/dfg.rs b/compiler/noirc_evaluator/src/ssa/ir/dfg.rs index 72ba369f98c..27eeaa0e15b 100644 --- a/compiler/noirc_evaluator/src/ssa/ir/dfg.rs +++ b/compiler/noirc_evaluator/src/ssa/ir/dfg.rs @@ -165,6 +165,36 @@ impl DataFlowGraph { id } + fn insert_instruction_without_simplification( + &mut self, + instruction_data: Instruction, + block: BasicBlockId, + ctrl_typevars: Option>, + call_stack: CallStackId, + ) -> InstructionId { + let id = self.make_instruction(instruction_data, ctrl_typevars); + self.blocks[block].insert_instruction(id); + self.locations.insert(id, call_stack); + id + } + + pub(crate) fn insert_instruction_and_results_without_simplification( + &mut self, + instruction_data: Instruction, + block: BasicBlockId, + ctrl_typevars: Option>, + call_stack: CallStackId, + ) -> InsertInstructionResult { + let id = self.insert_instruction_without_simplification( + instruction_data, + block, + ctrl_typevars, + call_stack, + ); + + InsertInstructionResult::Results(id, self.instruction_results(id)) + } + /// Inserts a new instruction at the end of the given block and returns its results pub(crate) fn insert_instruction_and_results( &mut self, @@ -184,7 +214,8 @@ impl DataFlowGraph { result @ (SimplifyResult::SimplifiedToInstruction(_) | SimplifyResult::SimplifiedToInstructionMultiple(_) | SimplifyResult::None) => { - let instructions = result.instructions().unwrap_or(vec![instruction]); + let mut instructions = result.instructions().unwrap_or(vec![instruction]); + assert!(!instructions.is_empty(), "`SimplifyResult::SimplifiedToInstructionMultiple` must not return empty vector"); if instructions.len() > 1 { // There's currently no way to pass results from one instruction in `instructions` on to the next. @@ -196,17 +227,22 @@ impl DataFlowGraph { ); } - let mut last_id = None; - + // Pull off the last instruction as we want to return its results. + let last_instruction = instructions.pop().expect("`instructions` can't be empty"); for instruction in instructions { - let id = self.make_instruction(instruction, ctrl_typevars.clone()); - self.blocks[block].insert_instruction(id); - self.locations.insert(id, call_stack); - last_id = Some(id); + self.insert_instruction_without_simplification( + instruction, + block, + ctrl_typevars.clone(), + call_stack, + ); } - - let id = last_id.expect("There should be at least 1 simplified instruction"); - InsertInstructionResult::Results(id, self.instruction_results(id)) + self.insert_instruction_and_results_without_simplification( + last_instruction, + block, + ctrl_typevars, + call_stack, + ) } } } diff --git a/compiler/noirc_evaluator/src/ssa/opt/resolve_is_unconstrained.rs b/compiler/noirc_evaluator/src/ssa/opt/resolve_is_unconstrained.rs index 87e680932c6..eff6576b87f 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/resolve_is_unconstrained.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/resolve_is_unconstrained.rs @@ -39,6 +39,8 @@ impl Function { } } + let is_unconstrained = matches!(self.runtime(), RuntimeType::Brillig(_)).into(); + let is_within_unconstrained = self.dfg.make_constant(is_unconstrained, NumericType::bool()); for instruction_id in is_unconstrained_calls { let call_returns = self.dfg.instruction_results(instruction_id); let original_return_id = call_returns[0]; @@ -46,9 +48,6 @@ impl Function { // We replace the result with a fresh id. This will be unused, so the DIE pass will remove the leftover intrinsic call. self.dfg.replace_result(instruction_id, original_return_id); - let is_unconstrained = matches!(self.runtime(), RuntimeType::Brillig(_)).into(); - let is_within_unconstrained = - self.dfg.make_constant(is_unconstrained, NumericType::bool()); // Replace all uses of the original return value with the constant self.dfg.set_value_from_id(original_return_id, is_within_unconstrained); } diff --git a/compiler/noirc_evaluator/src/ssa/opt/unrolling.rs b/compiler/noirc_evaluator/src/ssa/opt/unrolling.rs index 2a272236195..ab4256197b9 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/unrolling.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/unrolling.rs @@ -18,6 +18,8 @@ //! //! When unrolling ACIR code, we remove reference count instructions because they are //! only used by Brillig bytecode. +use std::collections::BTreeSet; + use acvm::{acir::AcirField, FieldElement}; use im::HashSet; @@ -117,7 +119,7 @@ pub(super) struct Loop { back_edge_start: BasicBlockId, /// All the blocks contained within the loop, including `header` and `back_edge_start`. - pub(super) blocks: HashSet, + pub(super) blocks: BTreeSet, } pub(super) struct Loops { @@ -238,7 +240,7 @@ impl Loop { back_edge_start: BasicBlockId, cfg: &ControlFlowGraph, ) -> Self { - let mut blocks = HashSet::default(); + let mut blocks = BTreeSet::default(); blocks.insert(header); let mut insert = |block, stack: &mut Vec| { diff --git a/compiler/noirc_frontend/src/ast/statement.rs b/compiler/noirc_frontend/src/ast/statement.rs index c77fe7513a1..bd65fc33377 100644 --- a/compiler/noirc_frontend/src/ast/statement.rs +++ b/compiler/noirc_frontend/src/ast/statement.rs @@ -1,5 +1,4 @@ use std::fmt::Display; -use std::sync::atomic::{AtomicU32, Ordering}; use acvm::acir::AcirField; use acvm::FieldElement; @@ -154,6 +153,7 @@ impl StatementKind { r#type, expression, comptime: false, + is_global_let: false, attributes, }) } @@ -562,6 +562,7 @@ pub struct LetStatement { // True if this should only be run during compile-time pub comptime: bool, + pub is_global_let: bool, } #[derive(Debug, PartialEq, Eq, Clone)] @@ -839,10 +840,9 @@ impl ForRange { block: Expression, for_loop_span: Span, ) -> Statement { - /// Counter used to generate unique names when desugaring - /// code in the parser requires the creation of fresh variables. - /// The parser is stateless so this is a static global instead. - static UNIQUE_NAME_COUNTER: AtomicU32 = AtomicU32::new(0); + // Counter used to generate unique names when desugaring + // code in the parser requires the creation of fresh variables. + let mut unique_name_counter: u32 = 0; match self { ForRange::Range(..) => { @@ -853,7 +853,8 @@ impl ForRange { let start_range = ExpressionKind::integer(FieldElement::zero()); let start_range = Expression::new(start_range, array_span); - let next_unique_id = UNIQUE_NAME_COUNTER.fetch_add(1, Ordering::Relaxed); + let next_unique_id = unique_name_counter; + unique_name_counter += 1; let array_name = format!("$i{next_unique_id}"); let array_span = array.span; let array_ident = Ident::new(array_name, array_span); @@ -886,7 +887,7 @@ impl ForRange { })); let end_range = Expression::new(end_range, array_span); - let next_unique_id = UNIQUE_NAME_COUNTER.fetch_add(1, Ordering::Relaxed); + let next_unique_id = unique_name_counter; let index_name = format!("$i{next_unique_id}"); let fresh_identifier = Ident::new(index_name.clone(), array_span); diff --git a/compiler/noirc_frontend/src/elaborator/mod.rs b/compiler/noirc_frontend/src/elaborator/mod.rs index fe1d1e38e1a..55144f8944a 100644 --- a/compiler/noirc_frontend/src/elaborator/mod.rs +++ b/compiler/noirc_frontend/src/elaborator/mod.rs @@ -4,8 +4,8 @@ use std::{ }; use crate::{ - ast::ItemVisibility, hir_def::traits::ResolvedTraitBound, usage_tracker::UsageTracker, - StructField, StructType, TypeBindings, + ast::ItemVisibility, hir_def::traits::ResolvedTraitBound, node_interner::GlobalValue, + usage_tracker::UsageTracker, StructField, StructType, TypeBindings, }; use crate::{ ast::{ @@ -1652,20 +1652,13 @@ impl<'context> Elaborator<'context> { self.push_err(ResolverError::MutableGlobal { span }); } - let comptime = let_stmt.comptime; - - let (let_statement, _typ) = if comptime { - self.elaborate_in_comptime_context(|this| this.elaborate_let(let_stmt, Some(global_id))) - } else { - self.elaborate_let(let_stmt, Some(global_id)) - }; + let (let_statement, _typ) = self + .elaborate_in_comptime_context(|this| this.elaborate_let(let_stmt, Some(global_id))); let statement_id = self.interner.get_global(global_id).let_statement; self.interner.replace_statement(statement_id, let_statement); - if comptime { - self.elaborate_comptime_global(global_id); - } + self.elaborate_comptime_global(global_id); if let Some(name) = name { self.interner.register_global(global_id, name, global.visibility, self.module_id()); @@ -1696,7 +1689,17 @@ impl<'context> Elaborator<'context> { self.debug_comptime(location, |interner| value.display(interner).to_string()); - self.interner.get_global_mut(global_id).value = Some(value); + self.interner.get_global_mut(global_id).value = GlobalValue::Resolved(value); + } + } + + /// If the given global is unresolved, elaborate it and return true + fn elaborate_global_if_unresolved(&mut self, global_id: &GlobalId) -> bool { + if let Some(global) = self.unresolved_globals.remove(global_id) { + self.elaborate_global(global); + true + } else { + false } } diff --git a/compiler/noirc_frontend/src/elaborator/path_resolution.rs b/compiler/noirc_frontend/src/elaborator/path_resolution.rs index a68991becb7..51673342fef 100644 --- a/compiler/noirc_frontend/src/elaborator/path_resolution.rs +++ b/compiler/noirc_frontend/src/elaborator/path_resolution.rs @@ -9,7 +9,7 @@ use crate::hir::resolution::visibility::item_in_module_is_visible; use crate::locations::ReferencesTracker; use crate::node_interner::{FuncId, GlobalId, StructId, TraitId, TypeAliasId}; -use crate::Type; +use crate::{Shared, Type, TypeAlias}; use super::types::SELF_TYPE_NAME; use super::Elaborator; @@ -218,18 +218,8 @@ impl<'context> Elaborator<'context> { ), ModuleDefId::TypeAliasId(id) => { let type_alias = self.interner.get_type_alias(id); - let type_alias = type_alias.borrow(); - - let module_id = match &type_alias.typ { - Type::Struct(struct_id, _generics) => struct_id.borrow().id.module_id(), - Type::Error => { - return Err(PathResolutionError::Unresolved(last_ident.clone())); - } - _ => { - // For now we only allow type aliases that point to structs. - // The more general case is captured here: https://github.com/noir-lang/noir/issues/6398 - panic!("Type alias in path not pointing to struct not yet supported") - } + let Some(module_id) = get_type_alias_module_def_id(&type_alias) else { + return Err(PathResolutionError::Unresolved(last_ident.clone())); }; ( @@ -345,3 +335,18 @@ fn merge_intermediate_path_resolution_item_with_module_def_id( }, } } + +fn get_type_alias_module_def_id(type_alias: &Shared) -> Option { + let type_alias = type_alias.borrow(); + + match &type_alias.typ { + Type::Struct(struct_id, _generics) => Some(struct_id.borrow().id.module_id()), + Type::Alias(type_alias, _generics) => get_type_alias_module_def_id(type_alias), + Type::Error => None, + _ => { + // For now we only allow type aliases that point to structs. + // The more general case is captured here: https://github.com/noir-lang/noir/issues/6398 + panic!("Type alias in path not pointing to struct not yet supported") + } + } +} diff --git a/compiler/noirc_frontend/src/elaborator/patterns.rs b/compiler/noirc_frontend/src/elaborator/patterns.rs index 3fbdadbbee8..133473219f6 100644 --- a/compiler/noirc_frontend/src/elaborator/patterns.rs +++ b/compiler/noirc_frontend/src/elaborator/patterns.rs @@ -604,17 +604,11 @@ impl<'context> Elaborator<'context> { alias_generics }; - // Now instantiate the underlying struct with those generics, the struct might + // Now instantiate the underlying struct or alias with those generics, the struct might // have more generics than those in the alias, like in this example: // // type Alias = Struct; - let typ = type_alias.get_type(&generics); - let Type::Struct(_, generics) = typ else { - // See https://github.com/noir-lang/noir/issues/6398 - panic!("Expected type alias to point to struct") - }; - - generics + get_type_alias_generics(&type_alias, &generics) } PathResolutionItem::TraitFunction(trait_id, Some(generics), _func_id) => { let trait_ = self.interner.get_trait(trait_id); @@ -666,9 +660,7 @@ impl<'context> Elaborator<'context> { self.interner.add_function_reference(func_id, hir_ident.location); } DefinitionKind::Global(global_id) => { - if let Some(global) = self.unresolved_globals.remove(&global_id) { - self.elaborate_global(global); - } + self.elaborate_global_if_unresolved(&global_id); if let Some(current_item) = self.current_item { self.interner.add_global_dependency(current_item, global_id); } @@ -882,3 +874,14 @@ impl<'context> Elaborator<'context> { (id, typ) } } + +fn get_type_alias_generics(type_alias: &TypeAlias, generics: &[Type]) -> Vec { + let typ = type_alias.get_type(generics); + match typ { + Type::Struct(_, generics) => generics, + Type::Alias(type_alias, generics) => { + get_type_alias_generics(&type_alias.borrow(), &generics) + } + _ => panic!("Expected type alias to point to struct or alias"), + } +} diff --git a/compiler/noirc_frontend/src/elaborator/statements.rs b/compiler/noirc_frontend/src/elaborator/statements.rs index 93009f49071..8a46d85d563 100644 --- a/compiler/noirc_frontend/src/elaborator/statements.rs +++ b/compiler/noirc_frontend/src/elaborator/statements.rs @@ -76,6 +76,7 @@ impl<'context> Elaborator<'context> { ) -> (HirStatement, Type) { let expr_span = let_stmt.expression.span; let (expression, expr_type) = self.elaborate_expression(let_stmt.expression); + let type_contains_unspecified = let_stmt.r#type.contains_unspecified(); let annotated_type = self.resolve_inferred_type(let_stmt.r#type); @@ -123,7 +124,9 @@ impl<'context> Elaborator<'context> { let attributes = let_stmt.attributes; let comptime = let_stmt.comptime; - let let_ = HirLetStatement { pattern, r#type, expression, attributes, comptime }; + let is_global_let = let_stmt.is_global_let; + let let_ = + HirLetStatement::new(pattern, r#type, expression, attributes, comptime, is_global_let); (HirStatement::Let(let_), Type::Unit) } diff --git a/compiler/noirc_frontend/src/elaborator/types.rs b/compiler/noirc_frontend/src/elaborator/types.rs index 2e4809f3511..9aafd690bb6 100644 --- a/compiler/noirc_frontend/src/elaborator/types.rs +++ b/compiler/noirc_frontend/src/elaborator/types.rs @@ -1,6 +1,5 @@ use std::{borrow::Cow, rc::Rc}; -use acvm::acir::AcirField; use iter_extended::vecmap; use noirc_errors::{Location, Span}; use rustc_hash::FxHashMap as HashMap; @@ -12,7 +11,6 @@ use crate::{ UnresolvedTypeData, UnresolvedTypeExpression, WILDCARD_TYPE, }, hir::{ - comptime::{Interpreter, Value}, def_collector::dc_crate::CompilationError, resolution::{errors::ResolverError, import::PathResolutionError}, type_check::{ @@ -30,7 +28,7 @@ use crate::{ traits::{NamedType, ResolvedTraitBound, Trait, TraitConstraint}, }, node_interner::{ - DefinitionKind, DependencyId, ExprId, GlobalId, ImplSearchErrorKind, NodeInterner, TraitId, + DependencyId, ExprId, GlobalValue, ImplSearchErrorKind, NodeInterner, TraitId, TraitImplKind, TraitMethodId, }, token::SecondaryAttribute, @@ -415,17 +413,41 @@ impl<'context> Elaborator<'context> { .map(|let_statement| Kind::numeric(let_statement.r#type)) .unwrap_or(Kind::u32()); - // TODO(https://github.com/noir-lang/noir/issues/6238): - // support non-u32 generics here - if !kind.unifies(&Kind::u32()) { - let error = TypeCheckError::EvaluatedGlobalIsntU32 { - expected_kind: Kind::u32().to_string(), - expr_kind: kind.to_string(), - expr_span: path.span(), - }; - self.push_err(error); - } - Some(Type::Constant(self.eval_global_as_array_length(id, path).into(), kind)) + let Some(stmt) = self.interner.get_global_let_statement(id) else { + if self.elaborate_global_if_unresolved(&id) { + return self.lookup_generic_or_global_type(path); + } else { + let path = path.clone(); + self.push_err(ResolverError::NoSuchNumericTypeVariable { path }); + return None; + } + }; + + let rhs = stmt.expression; + let span = self.interner.expr_span(&rhs); + + let GlobalValue::Resolved(global_value) = &self.interner.get_global(id).value + else { + self.push_err(ResolverError::UnevaluatedGlobalType { span }); + return None; + }; + + let Some(global_value) = global_value.to_field_element() else { + let global_value = global_value.clone(); + if global_value.is_integral() { + self.push_err(ResolverError::NegativeGlobalType { span, global_value }); + } else { + self.push_err(ResolverError::NonIntegralGlobalType { span, global_value }); + } + return None; + }; + + let Ok(global_value) = kind.ensure_value_fits(global_value, span) else { + self.push_err(ResolverError::GlobalLargerThanKind { span, global_value, kind }); + return None; + }; + + Some(Type::Constant(global_value, kind)) } _ => None, } @@ -633,31 +655,6 @@ impl<'context> Elaborator<'context> { .or_else(|| self.resolve_trait_method_by_named_generic(path)) } - fn eval_global_as_array_length(&mut self, global_id: GlobalId, path: &Path) -> u32 { - let Some(stmt) = self.interner.get_global_let_statement(global_id) else { - if let Some(global) = self.unresolved_globals.remove(&global_id) { - self.elaborate_global(global); - return self.eval_global_as_array_length(global_id, path); - } else { - let path = path.clone(); - self.push_err(ResolverError::NoSuchNumericTypeVariable { path }); - return 0; - } - }; - - let length = stmt.expression; - let span = self.interner.expr_span(&length); - let result = try_eval_array_length_id(self.interner, length, span); - - match result.map(|length| length.try_into()) { - Ok(Ok(length_value)) => return length_value, - Ok(Err(_cast_err)) => self.push_err(ResolverError::IntegerTooLarge { span }), - Err(Some(error)) => self.push_err(error), - Err(None) => (), - } - 0 - } - pub(super) fn unify( &mut self, actual: &Type, @@ -1834,88 +1831,6 @@ fn bind_generic(param: &ResolvedGeneric, arg: &Type, bindings: &mut TypeBindings } } -pub fn try_eval_array_length_id( - interner: &NodeInterner, - rhs: ExprId, - span: Span, -) -> Result> { - // Arbitrary amount of recursive calls to try before giving up - let fuel = 100; - try_eval_array_length_id_with_fuel(interner, rhs, span, fuel) -} - -fn try_eval_array_length_id_with_fuel( - interner: &NodeInterner, - rhs: ExprId, - span: Span, - fuel: u32, -) -> Result> { - if fuel == 0 { - // If we reach here, it is likely from evaluating cyclic globals. We expect an error to - // be issued for them after name resolution so issue no error now. - return Err(None); - } - - match interner.expression(&rhs) { - HirExpression::Literal(HirLiteral::Integer(int, false)) => { - int.try_into_u128().ok_or(Some(ResolverError::IntegerTooLarge { span })) - } - HirExpression::Ident(ident, _) => { - if let Some(definition) = interner.try_definition(ident.id) { - match definition.kind { - DefinitionKind::Global(global_id) => { - let let_statement = interner.get_global_let_statement(global_id); - if let Some(let_statement) = let_statement { - let expression = let_statement.expression; - try_eval_array_length_id_with_fuel(interner, expression, span, fuel - 1) - } else { - Err(Some(ResolverError::InvalidArrayLengthExpr { span })) - } - } - _ => Err(Some(ResolverError::InvalidArrayLengthExpr { span })), - } - } else { - Err(Some(ResolverError::InvalidArrayLengthExpr { span })) - } - } - HirExpression::Infix(infix) => { - let lhs = try_eval_array_length_id_with_fuel(interner, infix.lhs, span, fuel - 1)?; - let rhs = try_eval_array_length_id_with_fuel(interner, infix.rhs, span, fuel - 1)?; - - match infix.operator.kind { - BinaryOpKind::Add => Ok(lhs + rhs), - BinaryOpKind::Subtract => Ok(lhs - rhs), - BinaryOpKind::Multiply => Ok(lhs * rhs), - BinaryOpKind::Divide => Ok(lhs / rhs), - BinaryOpKind::Equal => Ok((lhs == rhs) as u128), - BinaryOpKind::NotEqual => Ok((lhs != rhs) as u128), - BinaryOpKind::Less => Ok((lhs < rhs) as u128), - BinaryOpKind::LessEqual => Ok((lhs <= rhs) as u128), - BinaryOpKind::Greater => Ok((lhs > rhs) as u128), - BinaryOpKind::GreaterEqual => Ok((lhs >= rhs) as u128), - BinaryOpKind::And => Ok(lhs & rhs), - BinaryOpKind::Or => Ok(lhs | rhs), - BinaryOpKind::Xor => Ok(lhs ^ rhs), - BinaryOpKind::ShiftRight => Ok(lhs >> rhs), - BinaryOpKind::ShiftLeft => Ok(lhs << rhs), - BinaryOpKind::Modulo => Ok(lhs % rhs), - } - } - HirExpression::Cast(cast) => { - let lhs = try_eval_array_length_id_with_fuel(interner, cast.lhs, span, fuel - 1)?; - let lhs_value = Value::Field(lhs.into()); - let evaluated_value = - Interpreter::evaluate_cast_one_step(&cast, rhs, lhs_value, interner) - .map_err(|error| Some(ResolverError::ArrayLengthInterpreter { error }))?; - - evaluated_value - .to_u128() - .ok_or_else(|| Some(ResolverError::InvalidArrayLengthExpr { span })) - } - _other => Err(Some(ResolverError::InvalidArrayLengthExpr { span })), - } -} - /// Gives an error if a user tries to create a mutable reference /// to an immutable variable. fn verify_mutable_reference(interner: &NodeInterner, rhs: ExprId) -> Result<(), ResolverError> { diff --git a/compiler/noirc_frontend/src/hir/comptime/errors.rs b/compiler/noirc_frontend/src/hir/comptime/errors.rs index 3df20b39209..6ff918328a1 100644 --- a/compiler/noirc_frontend/src/hir/comptime/errors.rs +++ b/compiler/noirc_frontend/src/hir/comptime/errors.rs @@ -243,6 +243,9 @@ pub enum InterpreterError { CannotInterpretFormatStringWithErrors { location: Location, }, + GlobalsDependencyCycle { + location: Location, + }, // These cases are not errors, they are just used to prevent us from running more code // until the loop can be resumed properly. These cases will never be displayed to users. @@ -319,7 +322,8 @@ impl InterpreterError { | InterpreterError::CannotResolveExpression { location, .. } | InterpreterError::CannotSetFunctionBody { location, .. } | InterpreterError::UnknownArrayLength { location, .. } - | InterpreterError::CannotInterpretFormatStringWithErrors { location } => *location, + | InterpreterError::CannotInterpretFormatStringWithErrors { location } + | InterpreterError::GlobalsDependencyCycle { location } => *location, InterpreterError::FailedToParseMacro { error, file, .. } => { Location::new(error.span(), *file) @@ -674,6 +678,11 @@ impl<'a> From<&'a InterpreterError> for CustomDiagnostic { "Some of the variables to interpolate could not be evaluated".to_string(); CustomDiagnostic::simple_error(msg, secondary, location.span) } + InterpreterError::GlobalsDependencyCycle { location } => { + let msg = "This global recursively depends on itself".to_string(); + let secondary = String::new(); + CustomDiagnostic::simple_error(msg, secondary, location.span) + } } } } diff --git a/compiler/noirc_frontend/src/hir/comptime/interpreter.rs b/compiler/noirc_frontend/src/hir/comptime/interpreter.rs index dfa55a9d79b..e9e37243e07 100644 --- a/compiler/noirc_frontend/src/hir/comptime/interpreter.rs +++ b/compiler/noirc_frontend/src/hir/comptime/interpreter.rs @@ -20,6 +20,7 @@ use crate::monomorphization::{ perform_impl_bindings, perform_instantiation_bindings, resolve_trait_method, undo_instantiation_bindings, }; +use crate::node_interner::GlobalValue; use crate::token::{FmtStrFragment, Tokens}; use crate::TypeVariable; use crate::{ @@ -568,26 +569,39 @@ impl<'local, 'interner> Interpreter<'local, 'interner> { DefinitionKind::Local(_) => self.lookup(&ident), DefinitionKind::Global(global_id) => { // Avoid resetting the value if it is already known - if let Some(value) = &self.elaborator.interner.get_global(*global_id).value { - Ok(value.clone()) - } else { - let global_id = *global_id; - let crate_of_global = self.elaborator.interner.get_global(global_id).crate_id; - let let_ = - self.elaborator.interner.get_global_let_statement(global_id).ok_or_else( - || { + let global_id = *global_id; + let global_info = self.elaborator.interner.get_global(global_id); + let global_crate_id = global_info.crate_id; + match &global_info.value { + GlobalValue::Resolved(value) => Ok(value.clone()), + GlobalValue::Resolving => { + // Note that the error we issue here isn't very informative (it doesn't include the actual cycle) + // but the general dependency cycle detector will give a better error later on during compilation. + let location = self.elaborator.interner.expr_location(&id); + Err(InterpreterError::GlobalsDependencyCycle { location }) + } + GlobalValue::Unresolved => { + let let_ = self + .elaborator + .interner + .get_global_let_statement(global_id) + .ok_or_else(|| { let location = self.elaborator.interner.expr_location(&id); InterpreterError::VariableNotInScope { location } - }, - )?; + })?; - if let_.comptime || crate_of_global != self.crate_id { - self.evaluate_let(let_.clone())?; - } + self.elaborator.interner.get_global_mut(global_id).value = + GlobalValue::Resolving; + + if let_.runs_comptime() || global_crate_id != self.crate_id { + self.evaluate_let(let_.clone())?; + } - let value = self.lookup(&ident)?; - self.elaborator.interner.get_global_mut(global_id).value = Some(value.clone()); - Ok(value) + let value = self.lookup(&ident)?; + self.elaborator.interner.get_global_mut(global_id).value = + GlobalValue::Resolved(value.clone()); + Ok(value) + } } } DefinitionKind::NumericGeneric(type_variable, numeric_typ) => { @@ -1380,13 +1394,19 @@ impl<'local, 'interner> Interpreter<'local, 'interner> { fn evaluate_cast(&mut self, cast: &HirCastExpression, id: ExprId) -> IResult { let evaluated_lhs = self.evaluate(cast.lhs)?; - Self::evaluate_cast_one_step(cast, id, evaluated_lhs, self.elaborator.interner) + let location = self.elaborator.interner.expr_location(&id); + Self::evaluate_cast_one_step( + &cast.r#type, + location, + evaluated_lhs, + self.elaborator.interner, + ) } /// evaluate_cast without recursion pub fn evaluate_cast_one_step( - cast: &HirCastExpression, - id: ExprId, + typ: &Type, + location: Location, evaluated_lhs: Value, interner: &NodeInterner, ) -> IResult { @@ -1417,7 +1437,6 @@ impl<'local, 'interner> Interpreter<'local, 'interner> { (if value { FieldElement::one() } else { FieldElement::zero() }, false) } value => { - let location = interner.expr_location(&id); let typ = value.get_type().into_owned(); return Err(InterpreterError::NonNumericCasted { typ, location }); } @@ -1434,7 +1453,7 @@ impl<'local, 'interner> Interpreter<'local, 'interner> { } // Now actually cast the lhs, bit casting and wrapping as necessary - match cast.r#type.follow_bindings() { + match typ.follow_bindings() { Type::FieldElement => { if lhs_is_negative { lhs = FieldElement::zero() - lhs; @@ -1443,8 +1462,7 @@ impl<'local, 'interner> Interpreter<'local, 'interner> { } Type::Integer(sign, bit_size) => match (sign, bit_size) { (Signedness::Unsigned, IntegerBitSize::One) => { - let location = interner.expr_location(&id); - Err(InterpreterError::TypeUnsupported { typ: cast.r#type.clone(), location }) + Err(InterpreterError::TypeUnsupported { typ: typ.clone(), location }) } (Signedness::Unsigned, IntegerBitSize::Eight) => cast_to_int!(lhs, to_u128, u8, U8), (Signedness::Unsigned, IntegerBitSize::Sixteen) => { @@ -1457,8 +1475,7 @@ impl<'local, 'interner> Interpreter<'local, 'interner> { cast_to_int!(lhs, to_u128, u64, U64) } (Signedness::Signed, IntegerBitSize::One) => { - let location = interner.expr_location(&id); - Err(InterpreterError::TypeUnsupported { typ: cast.r#type.clone(), location }) + Err(InterpreterError::TypeUnsupported { typ: typ.clone(), location }) } (Signedness::Signed, IntegerBitSize::Eight) => cast_to_int!(lhs, to_i128, i8, I8), (Signedness::Signed, IntegerBitSize::Sixteen) => { @@ -1472,10 +1489,7 @@ impl<'local, 'interner> Interpreter<'local, 'interner> { } }, Type::Bool => Ok(Value::Bool(!lhs.is_zero() || lhs_is_negative)), - typ => { - let location = interner.expr_location(&id); - Err(InterpreterError::CastToNonNumericType { typ, location }) - } + typ => Err(InterpreterError::CastToNonNumericType { typ, location }), } } diff --git a/compiler/noirc_frontend/src/hir/comptime/interpreter/builtin.rs b/compiler/noirc_frontend/src/hir/comptime/interpreter/builtin.rs index 3d8ccf78926..2e672e3d0d5 100644 --- a/compiler/noirc_frontend/src/hir/comptime/interpreter/builtin.rs +++ b/compiler/noirc_frontend/src/hir/comptime/interpreter/builtin.rs @@ -64,6 +64,7 @@ impl<'local, 'context> Interpreter<'local, 'context> { "array_len" => array_len(interner, arguments, location), "array_refcount" => Ok(Value::U32(0)), "assert_constant" => Ok(Value::Bool(true)), + "as_field" => as_field(interner, arguments, location), "as_slice" => as_slice(interner, arguments, location), "ctstring_eq" => ctstring_eq(arguments, location), "ctstring_hash" => ctstring_hash(arguments, location), @@ -116,6 +117,7 @@ impl<'local, 'context> Interpreter<'local, 'context> { "field_less_than" => field_less_than(arguments, location), "fmtstr_as_ctstring" => fmtstr_as_ctstring(interner, arguments, location), "fmtstr_quoted_contents" => fmtstr_quoted_contents(interner, arguments, location), + "from_field" => from_field(interner, arguments, return_type, location), "fresh_type_variable" => fresh_type_variable(interner), "function_def_add_attribute" => function_def_add_attribute(self, arguments, location), "function_def_body" => function_def_body(interner, arguments, location), @@ -290,6 +292,16 @@ fn array_as_str_unchecked( Ok(Value::String(Rc::new(string))) } +// fn as_field(x: T) -> Field {} +fn as_field( + interner: &NodeInterner, + arguments: Vec<(Value, Location)>, + location: Location, +) -> IResult { + let (value, value_location) = check_one_argument(arguments, location)?; + Interpreter::evaluate_cast_one_step(&Type::FieldElement, value_location, value, interner) +} + fn as_slice( interner: &NodeInterner, arguments: Vec<(Value, Location)>, @@ -2224,6 +2236,17 @@ fn fmtstr_quoted_contents( Ok(Value::Quoted(Rc::new(tokens))) } +// fn from_field(x: Field) -> T {} +fn from_field( + interner: &NodeInterner, + arguments: Vec<(Value, Location)>, + return_type: Type, + location: Location, +) -> IResult { + let (value, value_location) = check_one_argument(arguments, location)?; + Interpreter::evaluate_cast_one_step(&return_type, value_location, value, interner) +} + // fn fresh_type_variable() -> Type fn fresh_type_variable(interner: &NodeInterner) -> IResult { Ok(Value::Type(interner.next_type_variable_with_kind(Kind::Any))) diff --git a/compiler/noirc_frontend/src/hir/comptime/value.rs b/compiler/noirc_frontend/src/hir/comptime/value.rs index 945fb45026d..e5c55e2b0be 100644 --- a/compiler/noirc_frontend/src/hir/comptime/value.rs +++ b/compiler/noirc_frontend/src/hir/comptime/value.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, rc::Rc, vec}; -use acvm::{AcirField, FieldElement}; +use acvm::FieldElement; use im::Vector; use iter_extended::{try_vecmap, vecmap}; use noirc_errors::{Location, Span}; @@ -409,6 +409,9 @@ impl Value { Value::Pointer(element, true) => { return element.unwrap_or_clone().into_hir_expression(interner, location); } + Value::Closure(hir_lambda, _args, _typ, _opt_func_id, _module_id) => { + HirExpression::Lambda(hir_lambda) + } Value::TypedExpr(TypedExpr::StmtId(..)) | Value::Expr(..) | Value::Pointer(..) @@ -420,7 +423,6 @@ impl Value { | Value::Zeroed(_) | Value::Type(_) | Value::UnresolvedType(_) - | Value::Closure(..) | Value::ModuleDefinition(_) => { let typ = self.get_type().into_owned(); let value = self.display(interner).to_string(); @@ -502,19 +504,28 @@ impl Value { Ok(vec![token]) } - /// Converts any unsigned `Value` into a `u128`. - /// Returns `None` for negative integers. - pub(crate) fn to_u128(&self) -> Option { + /// Returns false for non-integral `Value`s. + pub(crate) fn is_integral(&self) -> bool { + use Value::*; + matches!( + self, + Field(_) | I8(_) | I16(_) | I32(_) | I64(_) | U8(_) | U16(_) | U32(_) | U64(_) + ) + } + + /// Converts any non-negative `Value` into a `FieldElement`. + /// Returns `None` for negative integers and non-integral `Value`s. + pub(crate) fn to_field_element(&self) -> Option { match self { - Self::Field(value) => Some(value.to_u128()), - Self::I8(value) => (*value >= 0).then_some(*value as u128), - Self::I16(value) => (*value >= 0).then_some(*value as u128), - Self::I32(value) => (*value >= 0).then_some(*value as u128), - Self::I64(value) => (*value >= 0).then_some(*value as u128), - Self::U8(value) => Some(*value as u128), - Self::U16(value) => Some(*value as u128), - Self::U32(value) => Some(*value as u128), - Self::U64(value) => Some(*value as u128), + Self::Field(value) => Some(*value), + Self::I8(value) => (*value >= 0).then_some((*value as u128).into()), + Self::I16(value) => (*value >= 0).then_some((*value as u128).into()), + Self::I32(value) => (*value >= 0).then_some((*value as u128).into()), + Self::I64(value) => (*value >= 0).then_some((*value as u128).into()), + Self::U8(value) => Some((*value as u128).into()), + Self::U16(value) => Some((*value as u128).into()), + Self::U32(value) => Some((*value as u128).into()), + Self::U64(value) => Some((*value as u128).into()), _ => None, } } diff --git a/compiler/noirc_frontend/src/hir/resolution/errors.rs b/compiler/noirc_frontend/src/hir/resolution/errors.rs index 774836f8992..8817904c6f5 100644 --- a/compiler/noirc_frontend/src/hir/resolution/errors.rs +++ b/compiler/noirc_frontend/src/hir/resolution/errors.rs @@ -5,10 +5,13 @@ use thiserror::Error; use crate::{ ast::{Ident, UnsupportedNumericGenericType}, - hir::{comptime::InterpreterError, type_check::TypeCheckError}, + hir::{ + comptime::{InterpreterError, Value}, + type_check::TypeCheckError, + }, parser::ParserError, usage_tracker::UnusedItem, - Type, + Kind, Type, }; use super::import::PathResolutionError; @@ -101,6 +104,14 @@ pub enum ResolverError { MutableGlobal { span: Span }, #[error("Globals must have a specified type")] UnspecifiedGlobalType { span: Span, expected_type: Type }, + #[error("Global failed to evaluate")] + UnevaluatedGlobalType { span: Span }, + #[error("Globals used in a type position must be non-negative")] + NegativeGlobalType { span: Span, global_value: Value }, + #[error("Globals used in a type position must be integers")] + NonIntegralGlobalType { span: Span, global_value: Value }, + #[error("Global value `{global_value}` is larger than its kind's maximum value")] + GlobalLargerThanKind { span: Span, global_value: FieldElement, kind: Kind }, #[error("Self-referential structs are not supported")] SelfReferentialStruct { span: Span }, #[error("#[no_predicates] attribute is only allowed on constrained functions")] @@ -443,6 +454,34 @@ impl<'a> From<&'a ResolverError> for Diagnostic { *span, ) }, + ResolverError::UnevaluatedGlobalType { span } => { + Diagnostic::simple_error( + "Global failed to evaluate".to_string(), + String::new(), + *span, + ) + } + ResolverError::NegativeGlobalType { span, global_value } => { + Diagnostic::simple_error( + "Globals used in a type position must be non-negative".to_string(), + format!("But found value `{global_value:?}`"), + *span, + ) + } + ResolverError::NonIntegralGlobalType { span, global_value } => { + Diagnostic::simple_error( + "Globals used in a type position must be integers".to_string(), + format!("But found value `{global_value:?}`"), + *span, + ) + } + ResolverError::GlobalLargerThanKind { span, global_value, kind } => { + Diagnostic::simple_error( + format!("Global value `{global_value}` is larger than its kind's maximum value"), + format!("Global's kind inferred to be `{kind}`"), + *span, + ) + } ResolverError::SelfReferentialStruct { span } => { Diagnostic::simple_error( "Self-referential structs are not supported".into(), diff --git a/compiler/noirc_frontend/src/hir/type_check/errors.rs b/compiler/noirc_frontend/src/hir/type_check/errors.rs index 15b8d50c78b..08864b919e3 100644 --- a/compiler/noirc_frontend/src/hir/type_check/errors.rs +++ b/compiler/noirc_frontend/src/hir/type_check/errors.rs @@ -66,9 +66,6 @@ pub enum TypeCheckError { from_value: FieldElement, span: Span, }, - // TODO(https://github.com/noir-lang/noir/issues/6238): implement handling for larger types - #[error("Expected type {expected_kind} when evaluating globals, but found {expr_kind} (this warning may become an error in the future)")] - EvaluatedGlobalIsntU32 { expected_kind: String, expr_kind: String, expr_span: Span }, #[error("Expected {expected:?} found {found:?}")] ArityMisMatch { expected: usize, found: usize, span: Span }, #[error("Return type in a function cannot be public")] @@ -275,15 +272,6 @@ impl<'a> From<&'a TypeCheckError> for Diagnostic { *span, ) } - // TODO(https://github.com/noir-lang/noir/issues/6238): implement - // handling for larger types - TypeCheckError::EvaluatedGlobalIsntU32 { expected_kind, expr_kind, expr_span } => { - Diagnostic::simple_warning( - format!("Expected type {expected_kind} when evaluating globals, but found {expr_kind} (this warning may become an error in the future)"), - String::new(), - *expr_span, - ) - } TypeCheckError::TraitMethodParameterTypeMismatch { method_name, expected_typ, actual_typ, parameter_index, parameter_span } => { Diagnostic::simple_error( format!("Parameter #{parameter_index} of method `{method_name}` must be of type {expected_typ}, not {actual_typ}"), diff --git a/compiler/noirc_frontend/src/hir_def/stmt.rs b/compiler/noirc_frontend/src/hir_def/stmt.rs index 0258cfd8ddb..c42b8230290 100644 --- a/compiler/noirc_frontend/src/hir_def/stmt.rs +++ b/compiler/noirc_frontend/src/hir_def/stmt.rs @@ -31,15 +31,31 @@ pub struct HirLetStatement { pub expression: ExprId, pub attributes: Vec, pub comptime: bool, + pub is_global_let: bool, } impl HirLetStatement { + pub fn new( + pattern: HirPattern, + r#type: Type, + expression: ExprId, + attributes: Vec, + comptime: bool, + is_global_let: bool, + ) -> HirLetStatement { + Self { pattern, r#type, expression, attributes, comptime, is_global_let } + } + pub fn ident(&self) -> HirIdent { match &self.pattern { HirPattern::Identifier(ident) => ident.clone(), _ => panic!("can only fetch hir ident from HirPattern::Identifier"), } } + + pub fn runs_comptime(&self) -> bool { + self.comptime || self.is_global_let + } } #[derive(Debug, Clone)] diff --git a/compiler/noirc_frontend/src/hir_def/types.rs b/compiler/noirc_frontend/src/hir_def/types.rs index 2c9a44c079d..0ed6399296b 100644 --- a/compiler/noirc_frontend/src/hir_def/types.rs +++ b/compiler/noirc_frontend/src/hir_def/types.rs @@ -226,6 +226,10 @@ impl Kind { // Kind::Integer unifies with Kind::IntegerOrField (Kind::Integer | Kind::IntegerOrField, Kind::Integer | Kind::IntegerOrField) => true, + // Kind::IntegerOrField unifies with Kind::Numeric(_) + (Kind::IntegerOrField, Kind::Numeric(_typ)) + | (Kind::Numeric(_typ), Kind::IntegerOrField) => true, + // Kind::Numeric unifies along its Type argument (Kind::Numeric(lhs), Kind::Numeric(rhs)) => { let mut bindings = TypeBindings::new(); @@ -255,7 +259,8 @@ impl Kind { match self { Kind::IntegerOrField => Some(Type::default_int_or_field_type()), Kind::Integer => Some(Type::default_int_type()), - Kind::Any | Kind::Normal | Kind::Numeric(..) => None, + Kind::Numeric(typ) => Some(*typ.clone()), + Kind::Any | Kind::Normal => None, } } @@ -267,7 +272,7 @@ impl Kind { } /// Ensure the given value fits in self.integral_maximum_size() - fn ensure_value_fits( + pub(crate) fn ensure_value_fits( &self, value: FieldElement, span: Span, @@ -2210,6 +2215,7 @@ impl Type { Type::NamedGeneric(binding, _) | Type::TypeVariable(binding) => { substitute_binding(binding) } + // Do not substitute_helper fields, it can lead to infinite recursion // and we should not match fields when type checking anyway. Type::Struct(fields, args) => { diff --git a/compiler/noirc_frontend/src/monomorphization/mod.rs b/compiler/noirc_frontend/src/monomorphization/mod.rs index b31a5744d09..8c07d71de21 100644 --- a/compiler/noirc_frontend/src/monomorphization/mod.rs +++ b/compiler/noirc_frontend/src/monomorphization/mod.rs @@ -11,7 +11,7 @@ use crate::ast::{FunctionKind, IntegerBitSize, Signedness, UnaryOp, Visibility}; use crate::hir::comptime::InterpreterError; use crate::hir::type_check::{NoMatchingImplFoundError, TypeCheckError}; -use crate::node_interner::{ExprId, ImplSearchErrorKind}; +use crate::node_interner::{ExprId, GlobalValue, ImplSearchErrorKind}; use crate::token::FmtStrFragment; use crate::{ debug::DebugInstrumenter, @@ -895,7 +895,7 @@ impl<'interner> Monomorphizer<'interner> { DefinitionKind::Global(global_id) => { let global = self.interner.get_global(*global_id); - let expr = if let Some(value) = global.value.clone() { + let expr = if let GlobalValue::Resolved(value) = global.value.clone() { value .into_hir_expression(self.interner, global.location) .map_err(MonomorphizationError::InterpreterError)? @@ -1133,7 +1133,7 @@ impl<'interner> Monomorphizer<'interner> { | HirType::Error | HirType::Quoted(_) => Ok(()), HirType::Constant(_value, kind) => { - if kind.is_error() { + if kind.is_error() || kind.default_type().is_none() { Err(MonomorphizationError::UnknownConstant { location }) } else { Ok(()) @@ -1154,7 +1154,6 @@ impl<'interner> Monomorphizer<'interner> { Ok(()) } - HirType::TypeVariable(ref binding) => { let type_var_kind = match &*binding.borrow() { TypeBinding::Bound(binding) => { diff --git a/compiler/noirc_frontend/src/node_interner.rs b/compiler/noirc_frontend/src/node_interner.rs index 6d70ea2fd6d..bee703a4de8 100644 --- a/compiler/noirc_frontend/src/node_interner.rs +++ b/compiler/noirc_frontend/src/node_interner.rs @@ -609,7 +609,14 @@ pub struct GlobalInfo { pub crate_id: CrateId, pub location: Location, pub let_statement: StmtId, - pub value: Option, + pub value: GlobalValue, +} + +#[derive(Debug, Clone)] +pub enum GlobalValue { + Unresolved, + Resolving, + Resolved(comptime::Value), } #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -849,6 +856,7 @@ impl NodeInterner { let id = GlobalId(self.globals.len()); let location = Location::new(ident.span(), file); let name = ident.to_string(); + let definition_id = self.push_definition(name, mutable, comptime, DefinitionKind::Global(id), location); @@ -860,7 +868,7 @@ impl NodeInterner { crate_id, let_statement, location, - value: None, + value: GlobalValue::Unresolved, }); self.global_attributes.insert(id, attributes); id @@ -884,6 +892,7 @@ impl NodeInterner { ) -> GlobalId { let statement = self.push_stmt(HirStatement::Error); let span = name.span(); + let id = self .push_global(name, local_id, crate_id, statement, file, attributes, mutable, comptime); self.push_stmt_location(statement, span, file); diff --git a/compiler/noirc_frontend/src/parser/parser/global.rs b/compiler/noirc_frontend/src/parser/parser/global.rs index 5ece510022a..3a4f6d922a4 100644 --- a/compiler/noirc_frontend/src/parser/parser/global.rs +++ b/compiler/noirc_frontend/src/parser/parser/global.rs @@ -23,6 +23,7 @@ impl<'a> Parser<'a> { // and throw the error in name resolution. let attributes = self.validate_secondary_attributes(attributes); + let is_global_let = true; let Some(ident) = self.eat_ident() else { self.eat_semicolons(); @@ -35,6 +36,7 @@ impl<'a> Parser<'a> { expression: Expression { kind: ExpressionKind::Error, span: Span::default() }, attributes, comptime, + is_global_let, }; }; @@ -53,7 +55,7 @@ impl<'a> Parser<'a> { self.expected_token(Token::Semicolon); } - LetStatement { pattern, r#type: typ, expression, attributes, comptime } + LetStatement { pattern, r#type: typ, expression, attributes, comptime, is_global_let } } } @@ -105,6 +107,7 @@ mod tests { assert_eq!("foo", name.to_string()); assert!(matches!(let_statement.r#type.typ, UnresolvedTypeData::Unspecified)); assert!(!let_statement.comptime); + assert!(let_statement.is_global_let); assert_eq!(visibility, ItemVisibility::Private); } diff --git a/compiler/noirc_frontend/src/parser/parser/statement.rs b/compiler/noirc_frontend/src/parser/parser/statement.rs index ff6117c9348..ab86f7d6649 100644 --- a/compiler/noirc_frontend/src/parser/parser/statement.rs +++ b/compiler/noirc_frontend/src/parser/parser/statement.rs @@ -364,7 +364,14 @@ impl<'a> Parser<'a> { Expression { kind: ExpressionKind::Error, span: self.current_token_span } }; - Some(LetStatement { pattern, r#type, expression, attributes, comptime: false }) + Some(LetStatement { + pattern, + r#type, + expression, + attributes, + comptime: false, + is_global_let: false, + }) } /// ConstrainStatement diff --git a/compiler/noirc_frontend/src/tests.rs b/compiler/noirc_frontend/src/tests.rs index 8ddf1b571e6..2d7cf8acca6 100644 --- a/compiler/noirc_frontend/src/tests.rs +++ b/compiler/noirc_frontend/src/tests.rs @@ -1287,11 +1287,15 @@ fn lambda$f1(mut env$l1: (Field)) -> Field { check_rewrite(src, expected_rewrite); } +// TODO(https://github.com/noir-lang/noir/issues/6780): currently failing +// with a stack overflow #[test] +#[ignore] fn deny_cyclic_globals() { let src = r#" global A: u32 = B; global B: u32 = A; + fn main() {} "#; @@ -1991,9 +1995,6 @@ fn numeric_generic_u16_array_size() { )); } -// TODO(https://github.com/noir-lang/noir/issues/6238): -// The EvaluatedGlobalIsntU32 warning is a stopgap -// (originally from https://github.com/noir-lang/noir/issues/6125) #[test] fn numeric_generic_field_larger_than_u32() { let src = r#" @@ -2006,29 +2007,16 @@ fn numeric_generic_field_larger_than_u32() { } "#; let errors = get_program_errors(src); - assert_eq!(errors.len(), 2); - assert!(matches!( - errors[0].0, - CompilationError::TypeError(TypeCheckError::EvaluatedGlobalIsntU32 { .. }), - )); - assert!(matches!( - errors[1].0, - CompilationError::ResolverError(ResolverError::IntegerTooLarge { .. }) - )); + assert_eq!(errors.len(), 0); } -// TODO(https://github.com/noir-lang/noir/issues/6238): -// The EvaluatedGlobalIsntU32 warning is a stopgap -// (originally from https://github.com/noir-lang/noir/issues/6126) #[test] fn numeric_generic_field_arithmetic_larger_than_u32() { let src = r#" struct Foo {} - impl Foo { - fn size(self) -> Field { - F - } + fn size(_x: Foo) -> Field { + F } // 2^32 - 1 @@ -2039,21 +2027,11 @@ fn numeric_generic_field_arithmetic_larger_than_u32() { } fn main() { - let _ = foo::().size(); + let _ = size(foo::()); } "#; let errors = get_program_errors(src); - assert_eq!(errors.len(), 2); - - assert!(matches!( - errors[0].0, - CompilationError::TypeError(TypeCheckError::EvaluatedGlobalIsntU32 { .. }), - )); - - assert!(matches!( - errors[1].0, - CompilationError::ResolverError(ResolverError::UnusedVariable { .. }) - )); + assert_eq!(errors.len(), 0); } #[test] @@ -2180,25 +2158,11 @@ fn numeric_generics_type_kind_mismatch() { } "#; let errors = get_program_errors(src); - assert_eq!(errors.len(), 3); - - // TODO(https://github.com/noir-lang/noir/issues/6238): - // The EvaluatedGlobalIsntU32 warning is a stopgap + assert_eq!(errors.len(), 1); assert!(matches!( errors[0].0, - CompilationError::TypeError(TypeCheckError::EvaluatedGlobalIsntU32 { .. }), - )); - - assert!(matches!( - errors[1].0, CompilationError::TypeError(TypeCheckError::TypeKindMismatch { .. }), )); - - // TODO(https://github.com/noir-lang/noir/issues/6238): see above - assert!(matches!( - errors[2].0, - CompilationError::TypeError(TypeCheckError::EvaluatedGlobalIsntU32 { .. }), - )); } #[test] @@ -3219,20 +3183,20 @@ fn dont_infer_globals_to_u32_from_type_use() { } "#; - let errors = get_program_errors(src); - assert_eq!(errors.len(), 3); - assert!(matches!( - errors[0].0, - CompilationError::ResolverError(ResolverError::UnspecifiedGlobalType { .. }) - )); - assert!(matches!( - errors[1].0, - CompilationError::ResolverError(ResolverError::UnspecifiedGlobalType { .. }) - )); - assert!(matches!( - errors[2].0, - CompilationError::ResolverError(ResolverError::UnspecifiedGlobalType { .. }) - )); + let mut errors = get_program_errors(src); + assert_eq!(errors.len(), 6); + for (error, _file_id) in errors.drain(0..3) { + assert!(matches!( + error, + CompilationError::ResolverError(ResolverError::UnspecifiedGlobalType { .. }) + )); + } + for (error, _file_id) in errors { + assert!(matches!( + error, + CompilationError::TypeError(TypeCheckError::TypeKindMismatch { .. }) + )); + } } #[test] @@ -3242,7 +3206,8 @@ fn dont_infer_partial_global_types() { pub global NESTED_ARRAY: [[Field; _]; 3] = [[]; 3]; pub global STR: str<_> = "hi"; pub global NESTED_STR: [str<_>] = &["hi"]; - pub global FMT_STR: fmtstr<_, _> = f"hi {ARRAY}"; + pub global FORMATTED_VALUE: str<5> = "there"; + pub global FMT_STR: fmtstr<_, _> = f"hi {FORMATTED_VALUE}"; pub global TUPLE_WITH_MULTIPLE: ([str<_>], [[Field; _]; 3]) = (&["hi"], [[]; 3]); fn main() { } @@ -3318,13 +3283,9 @@ fn non_u32_as_array_length() { "#; let errors = get_program_errors(src); - assert_eq!(errors.len(), 2); + assert_eq!(errors.len(), 1); assert!(matches!( errors[0].0, - CompilationError::TypeError(TypeCheckError::EvaluatedGlobalIsntU32 { .. }) - )); - assert!(matches!( - errors[1].0, CompilationError::TypeError(TypeCheckError::TypeKindMismatch { .. }) )); } @@ -3894,3 +3855,23 @@ fn disallows_underscore_on_right_hand_side() { assert_eq!(name, "_"); } + +#[test] +fn errors_on_cyclic_globals() { + let src = r#" + pub comptime global A: u32 = B; + pub comptime global B: u32 = A; + + fn main() { } + "#; + let errors = get_program_errors(src); + + assert!(errors.iter().any(|(error, _)| matches!( + error, + CompilationError::InterpreterError(InterpreterError::GlobalsDependencyCycle { .. }) + ))); + assert!(errors.iter().any(|(error, _)| matches!( + error, + CompilationError::ResolverError(ResolverError::DependencyCycle { .. }) + ))); +} diff --git a/compiler/noirc_frontend/src/tests/aliases.rs b/compiler/noirc_frontend/src/tests/aliases.rs index 8d3433299f6..1a5621c8adb 100644 --- a/compiler/noirc_frontend/src/tests/aliases.rs +++ b/compiler/noirc_frontend/src/tests/aliases.rs @@ -47,3 +47,31 @@ fn alias_in_let_pattern() { "#; assert_no_errors(src); } + +#[test] +fn double_alias_in_path() { + let src = r#" + struct Foo {} + + impl Foo { + fn new() -> Self { + Self {} + } + } + + type FooAlias1 = Foo; + type FooAlias2 = FooAlias1; + + fn main() { + let _ = FooAlias2::new(); + } + "#; + assert_no_errors(src); +} + +#[test] +fn double_generic_alias_in_path() { + let src = r#" + "#; + assert_no_errors(src); +} diff --git a/compiler/noirc_frontend/src/tests/arithmetic_generics.rs b/compiler/noirc_frontend/src/tests/arithmetic_generics.rs index 3328fe439ae..83de9c077ab 100644 --- a/compiler/noirc_frontend/src/tests/arithmetic_generics.rs +++ b/compiler/noirc_frontend/src/tests/arithmetic_generics.rs @@ -153,3 +153,49 @@ fn arithmetic_generics_checked_cast_indirect_zeros() { panic!("unexpected error: {:?}", monomorphization_error); } } + +#[test] +fn global_numeric_generic_larger_than_u32() { + // Regression test for https://github.com/noir-lang/noir/issues/6125 + let source = r#" + global A: Field = 4294967297; + + fn foo() { } + + fn main() { + let _ = foo::(); + } + "#; + let errors = get_program_errors(source); + assert_eq!(errors.len(), 0); +} + +#[test] +fn global_arithmetic_generic_larger_than_u32() { + // Regression test for https://github.com/noir-lang/noir/issues/6126 + let source = r#" + struct Foo {} + + impl Foo { + fn size(self) -> Field { + let _ = self; + F + } + } + + // 2^32 - 1 + global A: Field = 4294967295; + + // Avoiding overflow succeeds: + // fn foo() -> Foo { + fn foo() -> Foo { + Foo {} + } + + fn main() { + let _ = foo::().size(); + } + "#; + let errors = get_program_errors(source); + assert_eq!(errors.len(), 0); +} diff --git a/cspell.json b/cspell.json index 5c707e92e21..9a4bca1e339 100644 --- a/cspell.json +++ b/cspell.json @@ -32,6 +32,7 @@ "boilerplates", "bridgekeeper", "brillig", + "bunx", "bytecount", "cachix", "callsite", @@ -242,9 +243,11 @@ "walkdir", "wasi", "wasmer", + "webapps", "Weierstraß", "whitespace", "whitespaces", + "YOLO", "zkhash", "zshell" ], diff --git a/docs/docs/how_to/how-to-solidity-verifier.md b/docs/docs/how_to/how-to-solidity-verifier.mdx similarity index 64% rename from docs/docs/how_to/how-to-solidity-verifier.md rename to docs/docs/how_to/how-to-solidity-verifier.mdx index 2cc0f8e57ce..da36b60920d 100644 --- a/docs/docs/how_to/how-to-solidity-verifier.md +++ b/docs/docs/how_to/how-to-solidity-verifier.mdx @@ -20,24 +20,37 @@ sidebar_position: 0 pagination_next: tutorials/noirjs_app --- -Noir has the ability to generate a verifier contract in Solidity, which can be deployed in many EVM-compatible blockchains such as Ethereum. +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +Noir is universal. The witness and the compiled program can be fed into a proving backend such as Aztec's [Barretenberg](https://github.com/AztecProtocol/aztec-packages/tree/master/barretenberg), which can then generate a verifier contract for deployment on blockchains. This allows for a powerful feature set, as one can make use of the conciseness and the privacy provided by Noir in an immutable ledger. Applications can range from simple P2P guessing games, to complex private DeFi interactions. -This guide shows you how to generate a Solidity Verifier and deploy it on the [Remix IDE](https://remix.ethereum.org/). It is assumed that: +Although not strictly in the domain of Noir itself, this guide shows how to generate a Solidity Verifier with Barretenberg and deploy it on the [Remix IDE](https://remix.ethereum.org/). It is assumed that: +- You will be using Barretenberg as your proving backend +- You will be using an EVM blockchain to verify your proof - You are comfortable with the Solidity programming language and understand how contracts are deployed on the Ethereum network - You have Noir installed and you have a Noir program. If you don't, [get started](../getting_started/quick_start.md) with Nargo and the example Hello Noir circuit - You are comfortable navigating RemixIDE. If you aren't or you need a refresher, you can find some video tutorials [here](https://www.youtube.com/channel/UCjTUPyFEr2xDGN6Cg8nKDaA) that could help you. ## Rundown -Generating a Solidity Verifier contract is actually a one-command process. However, compiling it and deploying it can have some caveats. Here's the rundown of this guide: +Generating a Solidity Verifier with Barretenberg contract is actually a one-command process. However, compiling it and deploying it can have some caveats. Here's the rundown of this guide: 1. How to generate a solidity smart contract 2. How to compile the smart contract in the RemixIDE 3. How to deploy it to a testnet +:::info[Which proving system to use?] + +Barretenberg currently provides two provers: `UltraPlonk` and `UltraHonk`. In a nutshell, `UltraHonk` is faster and uses less RAM, but its verifier contract is much more expensive. `UltraPlonk` is optimized for on-chain verification, but proving is more expensive. + +In any case, we provide instructions for both. Choose your poison ☠️ + +::: + ## Step 1 - Generate a contract This is by far the most straightforward step. Just run: @@ -46,25 +59,31 @@ This is by far the most straightforward step. Just run: nargo compile ``` -This will compile your source code into a Noir build artifact to be stored in the `./target` directory, you can then generate the smart contract using the commands: +This will compile your source code into a Noir build artifact to be stored in the `./target` directory. From here on, it's Barretenberg's work. You can generate the smart contract using the commands: + + + + +```sh +bb write_vk_ultra_keccak_honk -b ./target/.json +bb contract_ultra_honk +``` + + + ```sh -# Here we pass the path to the newly generated Noir artifact. bb write_vk -b ./target/.json bb contract ``` -replacing `` with the name of your Noir project. A new `contract` folder would then be generated in your project directory, containing the Solidity -file `contract.sol`. It can be deployed to any EVM blockchain acting as a verifier smart contract. + + -You can find more information about `bb` and the default Noir proving backend on [this page](../getting_started/quick_start.md#proving-backend). +replacing `` with the name of your Noir project. A `Verifier.sol` contract is now in the target folder and can be deployed to any EVM blockchain acting as a verifier smart contract. -:::info - -It is possible to generate verifier contracts of Noir programs for other smart contract platforms as long as the proving backend supplies an implementation. +You can find more information about `bb` and the default Noir proving backend on [this page](../getting_started/quick_start.md#proving-backend). -Barretenberg, the default proving backend for Nargo, supports generation of verifier contracts, for the time being these are only in Solidity. -::: ## Step 2 - Compiling @@ -85,17 +104,12 @@ To compile our the verifier, we can navigate to the compilation tab: ![Compilation Tab](@site/static/img/how-tos/solidity_verifier_2.png) -Remix should automatically match a suitable compiler version. However, hitting the "Compile" button will most likely generate a "Stack too deep" error: +Remix should automatically match a suitable compiler version. However, hitting the "Compile" button will most likely tell you the contract is too big to deploy on mainnet, or complain about a stack too deep: -![Stack too deep](@site/static/img/how-tos/solidity_verifier_3.png) +![Contract code too big](@site/static/img/how-tos/solidity_verifier_6.png) +![Stack too deep](@site/static/img/how-tos/solidity_verifier_8.png) -This is due to the verify function needing to put many variables on the stack, but enabling the optimizer resolves the issue. To do this, let's open the "Advanced Configurations" tab and enable optimization. The default 200 runs will suffice. - -:::info - -This time we will see a warning about an unused function parameter. This is expected, as the `verify` function doesn't use the `_proof` parameter inside a solidity block, it is loaded from calldata and used in assembly. - -::: +To avoid this, you can just use some optimization. Open the "Advanced Configurations" tab and enable optimization. The default 200 runs will suffice. ![Compilation success](@site/static/img/how-tos/solidity_verifier_4.png) @@ -103,54 +117,81 @@ This time we will see a warning about an unused function parameter. This is expe At this point we should have a compiled contract ready to deploy. If we navigate to the deploy section in Remix, we will see many different environments we can deploy to. The steps to deploy on each environment would be out-of-scope for this guide, so we will just use the default Remix VM. -Looking closely, we will notice that our "Solidity Verifier" is actually three contracts working together: - -- An `UltraVerificationKey` library which simply stores the verification key for our circuit. -- An abstract contract `BaseUltraVerifier` containing most of the verifying logic. -- A main `UltraVerifier` contract that inherits from the Base and uses the Key contract. - -Remix will take care of the dependencies for us so we can simply deploy the UltraVerifier contract by selecting it and hitting "deploy": +Looking closely, we will notice that our "Solidity Verifier" is composed on multiple contracts working together. Remix will take care of the dependencies for us so we can simply deploy the Verifier contract by selecting it and hitting "deploy": -![Deploying UltraVerifier](@site/static/img/how-tos/solidity_verifier_5.png) + + -A contract will show up in the "Deployed Contracts" section, where we can retrieve the Verification Key Hash. This is particularly useful for double-checking that the deployer contract is the correct one. +![Deploying HonkVerifier](@site/static/img/how-tos/solidity_verifier_7.png) -:::note + + -Why "UltraVerifier"? +![Deploying PlonkVerifier](@site/static/img/how-tos/solidity_verifier_9.png) -To be precise, the Noir compiler (`nargo`) doesn't generate the verifier contract directly. It compiles the Noir code into an intermediate language (ACIR), which is then executed by the backend. So it is the backend that returns the verifier smart contract, not Noir. + + -In this case, the Barretenberg Backend uses the UltraPlonk proving system, hence the "UltraVerifier" name. - -::: +A contract will show up in the "Deployed Contracts" section. ## Step 4 - Verifying -To verify a proof using the Solidity verifier contract, we call the `verify` function in this extended contract: +To verify a proof using the Solidity verifier contract, we call the `verify` function: ```solidity function verify(bytes calldata _proof, bytes32[] calldata _publicInputs) external view returns (bool) ``` -When using the default example in the [Hello Noir](../getting_started/quick_start.md) guide, the easiest way to confirm that the verifier contract is doing its job is by calling the `verify` function via remix with the required parameters. Note that the public inputs must be passed in separately to the rest of the proof so we must split the proof as returned from `bb`. +First generate a proof with `bb`. We need a `Prover.toml` file for our inputs. Run: -First generate a proof with `bb` at the location `./proof` using the steps in [get started](../getting_started/quick_start.md), this proof is in a binary format but we want to convert it into a hex string to pass into Remix, this can be done with the +```bash +nargo check +``` + +This will generate a `Prover.toml` you can fill with the values you want to prove. We can now execute the circuit with `nargo` and then use the proving backend to prove: + + + + +```bash +nargo execute +bb prove_ultra_keccak_honk -b ./target/.json -w ./target/ -o ./target/proof +``` + +:::tip[Public inputs] +Barretenberg attaches the public inputs to the proof, which in this case it's not very useful. If you're up for some JS, `bb.js` has [a method for it](https://github.com/AztecProtocol/aztec-packages/blob/master/barretenberg/ts/src/proof/index.ts), but in the CLI you can use this ugly snippet: + +```bash +cat ./target/proof | od -An -v -t x1 | tr -d $' \n' | sed 's/^.\{8\}//' | (read hex; echo "${hex:0:192}${hex:256}") +``` + +Beautiful. This assumes a circuit with one public input (32 bytes, for Barretenberg). For more inputs, you can just increment `hex:256` with 32 more bytes for each public input. + +::: + + + ```bash -# This value must be changed to match the number of public inputs (including return values!) in your program. -NUM_PUBLIC_INPUTS=1 -PUBLIC_INPUT_BYTES=32*NUM_PUBLIC_INPUTS -HEX_PUBLIC_INPUTS=$(head -c $PUBLIC_INPUT_BYTES ./proof | od -An -v -t x1 | tr -d $' \n') -HEX_PROOF=$(tail -c +$(($PUBLIC_INPUT_BYTES + 1)) ./proof | od -An -v -t x1 | tr -d $' \n') +nargo execute +bb prove -b ./target/.json -w ./target/ -o ./target/proof +``` + -echo "Public inputs:" -echo $HEX_PUBLIC_INPUTS +:::tip[Public inputs] +Barretenberg attaches the public inputs to the proof, which in this case it's not very useful. If you're up for some JS, `bb.js` has [a method for it](https://github.com/AztecProtocol/aztec-packages/blob/master/barretenberg/ts/src/proof/index.ts), but in the CLI you can use this ugly snippet: -echo "Proof:" -echo "0x$HEX_PROOF" +```bash +tail -c +33 ./target/proof | od -An -v -t x1 | tr -d $' \n' ``` +Beautiful. This assumes a circuit with one public input (32 bytes, for Barretenberg). For more inputs, you can just add 32 more bytes for each public input to the `tail` command. + +::: + + + + Remix expects that the public inputs will be split into an array of `bytes32` values so `HEX_PUBLIC_INPUTS` needs to be split up into 32 byte chunks which are prefixed with `0x` accordingly. A programmatic example of how the `verify` function is called can be seen in the example zk voting application [here](https://github.com/noir-lang/noir-examples/blob/33e598c257e2402ea3a6b68dd4c5ad492bce1b0a/foundry-voting/src/zkVote.sol#L35): @@ -188,7 +229,7 @@ the `verify` function will expect the public inputs array (second function param Passing only two inputs will result in an error such as `PUBLIC_INPUT_COUNT_INVALID(3, 2)`. -In this case, the inputs parameter to `verify` would be an array ordered as `[pubkey_x, pubkey_y, return`. +In this case, the inputs parameter to `verify` would be an array ordered as `[pubkey_x, pubkey_y, return]`. ::: diff --git a/docs/docs/tutorials/noirjs_app.md b/docs/docs/tutorials/noirjs_app.md index 6e69ea0bbed..8967ee005ce 100644 --- a/docs/docs/tutorials/noirjs_app.md +++ b/docs/docs/tutorials/noirjs_app.md @@ -1,186 +1,91 @@ --- -title: Building a web app with NoirJS +title: Building a web app with Noir and Barretenberg description: Learn how to setup a new app that uses Noir to generate and verify zero-knowledge SNARK proofs in a typescript or javascript environment. keywords: [how to, guide, javascript, typescript, noir, barretenberg, zero-knowledge, proofs, app] sidebar_position: 0 pagination_next: noir/concepts/data_types/index --- -NoirJS is a set of packages meant to work both in a browser and a server environment. In this tutorial, we will build a simple web app using them. From here, you should get an idea on how to proceed with your own Noir projects! +NoirJS is a Typescript package meant to work both in a browser and a server environment. -You can find the complete app code for this guide [here](https://github.com/noir-lang/tiny-noirjs-app). - -## Setup - -:::note - -Feel free to use whatever versions, just keep in mind that Nargo and the NoirJS packages are meant to be in sync. For example, Nargo 0.31.x matches `noir_js@0.31.x`, etc. - -In this guide, we will be pinned to 0.31.0. - -::: - -Before we start, we want to make sure we have Node, Nargo and the Barretenberg proving system (`bb`) installed. - -We start by opening a terminal and executing `node --version`. If we don't get an output like `v20.10.0`, that means node is not installed. Let's do that by following the handy [nvm guide](https://github.com/nvm-sh/nvm?tab=readme-ov-file#install--update-script). - -As for `Nargo`, we can follow the [Nargo guide](../getting_started/quick_start.md) to install it. If you're lazy, just paste this on a terminal and run `noirup`: - -```sh -curl -L https://raw.githubusercontent.com/noir-lang/noirup/main/install | bash -``` +In this tutorial, we will combine NoirJS with Aztec's Barretenberg backend to build a simple web app. From here, you should get an idea on how to proceed with your own Noir projects! -Follow the instructions on [this page](https://github.com/AztecProtocol/aztec-packages/tree/master/barretenberg/cpp/src/barretenberg/bb#installation) to install `bb`. -Version 0.41.0 is compatible with `nargo` version 0.31.0, which you can install with `bbup -v 0.41.0` once `bbup` is installed. - -Easy enough. Onwards! - -## Our project - -ZK is a powerful technology. An app that doesn't reveal one of the inputs to _anyone_ is almost unbelievable, yet Noir makes it as easy as a single line of code. - -In fact, it's so simple that it comes nicely packaged in `nargo`. Let's do that! +You can find the complete app code for this guide [here](https://github.com/noir-lang/tiny-noirjs-app). -### Nargo +## Dependencies -Run: +Before we start, we want to make sure we have Node installed. For convenience (and speed), we can just install [Bun](https://bun.sh) as our package manager, and Node will work out-of-the-box: ```bash -nargo new circuit +curl -fsSL https://bun.sh/install | bash ``` -And... That's about it. Your program is ready to be compiled and run. +Let's go barebones. Doing the bare minimum is not only simple, but also allows you to easily adapt it to almost any frontend framework. -To compile, let's `cd` into the `circuit` folder to enter our project, and call: +Barebones means we can immediately start with the dependencies even on an empty folder 😈: ```bash -nargo compile +bun i @noir-lang/noir_wasm@1.0.0-beta.0 @noir-lang/noir_js@1.0.0-beta.0 @aztec/bb.js@0.63.1 ``` -This compiles our circuit into `json` format and add it to a new `target` folder. +Wait, what are these dependencies? -:::info +- `noir_wasm` is the `wasm` version of the Noir compiler. Although most developers prefer to use `nargo` for compiling, there's nothing wrong with `noir_wasm`. We like `noir_wasm`. +- `noir_js` is the main Noir package. It will execute our program, and generate the witness that will be sent to the backend. +- `bb.js` is the Typescript interface for Aztec's Barretenberg proving backend. It also uses the `wasm` version in order to run on the browser. -At this point in the tutorial, your folder structure should look like this: +:::info -```tree -. -└── circuit <---- our working directory - β”œβ”€β”€ Nargo.toml - β”œβ”€β”€ src - β”‚ └── main.nr - └── target - └── circuit.json -``` +In this guide, we will install versions pinned to 1.0.0-beta.0. These work with Barretenberg version 0.63.1, so we are using that one version too. Feel free to try with older or later versions, though! ::: -### Node and Vite - -If you want to explore Nargo, feel free to go on a side-quest now and follow the steps in the -[getting started](../getting_started/quick_start.md) guide. However, we want our app to run on the browser, so we need Vite. +## Setting up our Noir program -Vite is a powerful tool to generate static websites. While it provides all kinds of features, let's just go barebones with some good old vanilla JS. +ZK is a powerful technology. An app that reveals computational correctness but doesn't reveal some of its inputs is almost unbelievable, yet Noir makes it as easy as a single line of code. -To do this this, go back to the previous folder (`cd ..`) and create a new vite project by running `npm create vite` and choosing "Vanilla" and "Javascript". +:::tip -A wild `vite-project` directory should now appear in your root folder! Let's not waste any time and dive right in: +It's not just you. We also enjoy syntax highlighting. [Check out the Language Server](../tooling/language_server.md) -```bash -cd vite-project -``` +::: -### Setting Up Vite and Configuring the Project - -Before we proceed with any coding, let's get our environment tailored for Noir. We'll start by laying down the foundations with a `vite.config.js` file. This little piece of configuration is our secret sauce for making sure everything meshes well with the NoirJS libraries and other special setups we might need, like handling WebAssembly modules. Here’s how you get that going: - -#### Creating the vite.config.js - -In your freshly minted `vite-project` folder, create a new file named `vite.config.js` and open it in your code editor. Paste the following to set the stage: - -```javascript -import { defineConfig } from 'vite'; -import copy from 'rollup-plugin-copy'; -import fs from 'fs'; -import path from 'path'; - -const wasmContentTypePlugin = { - name: 'wasm-content-type-plugin', - configureServer(server) { - server.middlewares.use(async (req, res, next) => { - if (req.url.endsWith('.wasm')) { - res.setHeader('Content-Type', 'application/wasm'); - const newPath = req.url.replace('deps', 'dist'); - const targetPath = path.join(__dirname, newPath); - const wasmContent = fs.readFileSync(targetPath); - return res.end(wasmContent); - } - next(); - }); - }, -}; +All you need is a `main.nr` and a `Nargo.toml` file. You can follow the [noirup](../getting_started/noir_installation.md) installation and just run `noirup -v 1.0.0-beta.0`, or just create them by hand: -export default defineConfig(({ command }) => { - if (command === 'serve') { - return { - build: { - target: 'esnext', - rollupOptions: { - external: ['@aztec/bb.js'] - } - }, - optimizeDeps: { - esbuildOptions: { - target: 'esnext' - } - }, - plugins: [ - copy({ - targets: [{ src: 'node_modules/**/*.wasm', dest: 'node_modules/.vite/dist' }], - copySync: true, - hook: 'buildStart', - }), - command === 'serve' ? wasmContentTypePlugin : [], - ], - }; - } - - return {}; -}); +```bash +mkdir -p circuit/src +touch circuit/src/main.nr circuit/Nargo.toml ``` -#### Install Dependencies - -Now that our stage is set, install the necessary NoirJS packages along with our other dependencies: +To make our program interesting, let's give it a real use-case scenario: Bob wants to prove he is older than 18, without disclosing his age. Open `main.nr` and write: -```bash -npm install && npm install @noir-lang/backend_barretenberg@0.31.0 @noir-lang/noir_js@0.31.0 -npm install rollup-plugin-copy --save-dev +```rust +fn main(age: u8) { + assert(age >= 18); +} ``` -:::info - -At this point in the tutorial, your folder structure should look like this: +This program accepts a private input called age, and simply proves this number is higher than 18. But to run this code, we need to give the compiler a `Nargo.toml` with at least a name and a type: -```tree -. -└── circuit - └── ...etc... -└── vite-project <---- our working directory - └── ...etc... +```toml +[package] +name = "circuit" +type = "bin" ``` -::: +This is all that we need to get started with Noir. -#### Some cleanup +![my heart is ready for you, noir.js](@site/static/img/memes/titanic.jpeg) -`npx create vite` is amazing but it creates a bunch of files we don't really need for our simple example. Actually, let's just delete everything except for `vite.config.js`, `index.html`, `main.js` and `package.json`. I feel lighter already. +## Setting up our app -![my heart is ready for you, noir.js](@site/static/img/memes/titanic.jpeg) +Remember when apps only had one `html` and one `js` file? Well, that's enough for Noir webapps. Let's create them: -## HTML +```bash +touch index.html index.js +``` -Our app won't run like this, of course. We need some working HTML, at least. Let's open our broken-hearted `index.html` and replace everything with this code snippet: +And add something useful to our HTML file: ```html @@ -200,11 +105,11 @@ Our app won't run like this, of course. We need some working HTML, at least. Let - +

Noir app

- - + +

Logs

@@ -214,32 +119,26 @@ Our app won't run like this, of course. We need some working HTML, at least. Let ``` -It _could_ be a beautiful UI... Depending on which universe you live in. - -## Some good old vanilla Javascript - -Our love for Noir needs undivided attention, so let's just open `main.js` and delete everything (this is where the romantic scenery becomes a bit creepy). +It _could_ be a beautiful UI... Depending on which universe you live in. In any case, we're using some scary CSS to make two boxes that will show cool things on the screen. -Start by pasting in this boilerplate code: +As for the JS, real madmen could just `console.log` everything, but let's say we want to see things happening (the true initial purpose of JS... right?). Here's some boilerplate for that. Just paste it in `index.js`: ```js -function display(container, msg) { - const c = document.getElementById(container); - const p = document.createElement('p'); - p.textContent = msg; - c.appendChild(p); -} +const show = (id, content) => { + const container = document.getElementById(id); + container.appendChild(document.createTextNode(content)); + container.appendChild(document.createElement("br")); +}; -document.getElementById('submitGuess').addEventListener('click', async () => { - try { - // here's where love happens - } catch (err) { - display('logs', 'Oh πŸ’” Wrong guess'); - } +document.getElementById("submit").addEventListener("click", async () => { + try { + // noir goes here + } catch { + show("logs", "Oh πŸ’”"); + } }); -``` -The display function doesn't do much. We're simply manipulating our website to see stuff happening. For example, if the proof fails, it will simply log a broken heart 😒 +``` :::info @@ -248,30 +147,56 @@ At this point in the tutorial, your folder structure should look like this: ```tree . └── circuit - └── ...same as above -└── vite-project - β”œβ”€β”€ vite.config.js - β”œβ”€β”€ main.js - β”œβ”€β”€ package.json - └── index.html + └── src + └── main.nr + Nargo.toml + index.js + package.json + index.html + ...etc ``` -You'll see other files and folders showing up (like `package-lock.json`, `node_modules`) but you shouldn't have to care about those. - ::: -## Some NoirJS +## Compile compile compile -We're starting with the good stuff now. If you've compiled the circuit as described above, you should have a `json` file we want to import at the very top of our `main.js` file: +Finally we're up for something cool. But before we can execute a Noir program, we need to compile it into ACIR: an abstract representation. Here's where `noir_wasm` comes in. -```ts -import circuit from '../circuit/target/circuit.json'; +`noir_wasm` expects a filesystem so it can resolve dependencies. While we could use the `public` folder, let's just import those using the nice `?url` syntax provided by vite. At the top of the file: + +```js +import { compile, createFileManager } from "@noir-lang/noir_wasm" + +import main from "./circuit/src/main.nr?url"; +import nargoToml from "./circuit/Nargo.toml?url"; ``` -[Noir is backend-agnostic](../index.mdx#whats-new-about-noir). We write Noir, but we also need a proving backend. That's why we need to import and instantiate the two dependencies we installed above: `BarretenbergBackend` and `Noir`. Let's import them right below: +Compiling on the browser is common enough that `createFileManager` already gives us a nice in-memory filesystem we can use. So all we need to compile is fetching these files, writing them to our filesystem, and compile. Add this function: ```js -import { BarretenbergBackend, BarretenbergVerifier as Verifier } from '@noir-lang/backend_barretenberg'; +export async function getCircuit() { + const fm = createFileManager("/"); + const { body } = await fetch(main); + const { body: nargoTomlBody } = await fetch(nargoToml); + + fm.writeFile("./src/main.nr", body); + fm.writeFile("./Nargo.toml", nargoTomlBody); + return await compile(fm); +} +``` + +:::tip + +As you can imagine, with `node` it's all conveniently easier since you get native access to `fs`... + +::: + +## Some more JS + +We're starting with the good stuff now. We want to execute our circuit to get the witness, and then feed that witness to Barretenberg. Luckily, both packages are quite easy to work with. Let's import them at the top of the file: + +```js +import { UltraHonkBackend } from '@aztec/bb.js'; import { Noir } from '@noir-lang/noir_js'; ``` @@ -279,88 +204,103 @@ And instantiate them inside our try-catch block: ```ts // try { -const backend = new BarretenbergBackend(circuit); -const noir = new Noir(circuit); +const { program } = await getCircuit(); +const noir = new Noir(program); +const backend = new UltraHonkBackend(program.bytecode); // } ``` -:::note +:::warning -For the remainder of the tutorial, everything will be happening inside the `try` block +WASMs are not always easy to work with. In our case, `vite` likes serving them with the wrong MIME type. There are different fixes but we found the easiest one is just YOLO instantiating the WASMs manually. Paste this at the top of the file, just below the other imports, and it will work just fine: + +```js +import initNoirC from "@noir-lang/noirc_abi"; +import initACVM from "@noir-lang/acvm_js"; +import acvm from "@noir-lang/acvm_js/web/acvm_js_bg.wasm?url"; +import noirc from "@noir-lang/noirc_abi/web/noirc_abi_wasm_bg.wasm?url"; +await Promise.all([initACVM(fetch(acvm)), initNoirC(fetch(noirc))]); +``` ::: -## Our app +## Executing and proving -Now for the app itself. We're capturing whatever is in the input when people press the submit button. Just add this: +Now for the app itself. We're capturing whatever is in the input when people press the submit button. Inside our `try` block, let's just grab that input and get its value. Noir will gladly execute it, and give us a witness: ```js -const x = parseInt(document.getElementById('guessInput').value); -const input = { x, y: 2 }; +const age = document.getElementById("age").value; +show("logs", "Generating witness... ⏳"); +const { witness } = await noir.execute({ age }); +show("logs", "Generated witness... βœ…"); + ``` +:::note + +For the remainder of the tutorial, everything will be happening inside the `try` block + +::: + Now we're ready to prove stuff! Let's feed some inputs to our circuit and calculate the proof: ```js -await setup(); // let's squeeze our wasm inits here - -display('logs', 'Generating proof... βŒ›'); -const { witness } = await noir.execute(input); +show("logs", "Generating proof... ⏳"); const proof = await backend.generateProof(witness); -display('logs', 'Generating proof... βœ…'); -display('results', proof.proof); +show("logs", "Generated proof... βœ…"); +show("results", proof.proof); ``` -You're probably eager to see stuff happening, so go and run your app now! +Our program is technically **done** . You're probably eager to see stuff happening! To serve this in a convenient way, we can use a bundler like `vite` by creating a `vite.config.js` file: + +```bash +touch vite.config.js +``` -From your terminal, run `npm run dev`. If it doesn't open a browser for you, just visit `localhost:5173`. You should now see the worst UI ever, with an ugly input. +`vite` helps us with a little catch: `bb.js` in particular uses top-level awaits which aren't supported everywhere. So we can add this to the `vite.config.js` to make the bundler optimize them: -![Getting Started 0](@site/static/img/noir_getting_started_1.png) +```js +export default { optimizeDeps: { esbuildOptions: { target: "esnext" } } }; +``` + +This should be enough for vite. We don't even need to install it, just run: + +```bash +bunx vite +``` -Now, our circuit says `fn main(x: Field, y: pub Field)`. This means only the `y` value is public, and it's hardcoded above: `input = { x, y: 2 }`. In other words, you won't need to send your secret`x` to the verifier! +If it doesn't open a browser for you, just visit `localhost:5173`. You should now see the worst UI ever, with an ugly input. -By inputting any number other than 2 in the input box and clicking "submit", you should get a valid proof. Otherwise the proof won't even generate correctly. By the way, if you're human, you shouldn't be able to understand anything on the "proof" box. That's OK. We like you, human ❀️. +![Noir Webapp UI](@site/static/img/tutorials/noirjs_webapp/webapp1.png) + +Now, our circuit requires a private input `fn main(age: u8)`, and fails if it is less than 18. Let's see if it works. Submit any number above 18 (as long as it fits in 8 bits) and you should get a valid proof. Otherwise the proof won't even generate correctly. + +By the way, if you're human, you shouldn't be able to understand anything on the "proof" box. That's OK. We like you, human ❀️. ## Verifying Time to celebrate, yes! But we shouldn't trust machines so blindly. Let's add these lines to see our proof being verified: ```js -display('logs', 'Verifying proof... βŒ›'); +show('logs', 'Verifying proof... βŒ›'); const isValid = await backend.verifyProof(proof); - -// or to cache and use the verification key: -// const verificationKey = await backend.getVerificationKey(); -// const verifier = new Verifier(); -// const isValid = await verifier.verifyProof(proof, verificationKey); - -if (isValid) display('logs', 'Verifying proof... βœ…'); +show("logs", `Proof is ${isValid ? "valid" : "invalid"}... βœ…`); ``` You have successfully generated a client-side Noir web app! ![coded app without math knowledge](@site/static/img/memes/flextape.jpeg) -## Further Reading - -You can see how noirjs is used in a full stack Next.js hardhat application in the [noir-starter repo here](https://github.com/noir-lang/noir-starter/tree/main/vite-hardhat). The example shows how to calculate a proof in the browser and verify it with a deployed Solidity verifier contract from noirjs. - -You should also check out the more advanced examples in the [noir-examples repo](https://github.com/noir-lang/noir-examples), where you'll find reference usage for some cool apps. +## Next steps -## UltraHonk Backend +At this point, you have a working ZK app that works on the browser. Actually, it works on a mobile phone too! -Barretenberg has recently exposed a new UltraHonk backend. We can use UltraHonk in NoirJS after version 0.33.0. Everything will be the same as the tutorial above, except that the class we need to import will change: +If you want to continue learning by doing, here are some challenges for you: -```js -import { UltraHonkBackend, UltraHonkVerifier as Verifier } from '@noir-lang/backend_barretenberg'; -``` - -The backend will then be instantiated as such: - -```js -const backend = new UltraHonkBackend(circuit); -``` +- Install [nargo](https://noir-lang.org/docs/getting_started/noir_installation) and write [Noir tests](../tooling/testing) +- Change the circuit to accept a [public input](../noir/concepts/data_types/#private--public-types) as the cutoff age. It could be different depending on the purpose, for example! +- Enjoy Noir's Rust-like syntax and write a struct `Country` that implements a trait `MinAge` with a method `get_min_age`. Then, make a struct `Person` have an `u8` as its age and a country of type `Country`. You can pass a `person` in JS just like a JSON object `person: { age, country: { min_age: 18 }}` -Then all the commands to prove and verify your circuit will be same. +The world is your stage, just have fun with ZK! You can see how noirjs is used in a full stack Next.js hardhat application in the [noir-starter repo here](https://github.com/noir-lang/noir-starter/tree/main/vite-hardhat). The example shows how to calculate a proof in the browser and verify it with a deployed Solidity verifier contract from noirjs. -The only feature currently unsupported with UltraHonk are [recursive proofs](../explainers/explainer-recursion.md). +Check out other starters, tools, or just cool projects in the [awesome noir repository](https://github.com/noir-lang/awesome-noir). diff --git a/docs/static/img/how-tos/solidity_verifier_6.png b/docs/static/img/how-tos/solidity_verifier_6.png new file mode 100644 index 00000000000..c92e874740a Binary files /dev/null and b/docs/static/img/how-tos/solidity_verifier_6.png differ diff --git a/docs/static/img/how-tos/solidity_verifier_7.png b/docs/static/img/how-tos/solidity_verifier_7.png new file mode 100644 index 00000000000..c8541fa544c Binary files /dev/null and b/docs/static/img/how-tos/solidity_verifier_7.png differ diff --git a/docs/static/img/how-tos/solidity_verifier_8.png b/docs/static/img/how-tos/solidity_verifier_8.png new file mode 100644 index 00000000000..125c140acec Binary files /dev/null and b/docs/static/img/how-tos/solidity_verifier_8.png differ diff --git a/docs/static/img/how-tos/solidity_verifier_9.png b/docs/static/img/how-tos/solidity_verifier_9.png new file mode 100644 index 00000000000..150812e3320 Binary files /dev/null and b/docs/static/img/how-tos/solidity_verifier_9.png differ diff --git a/docs/static/img/tutorials/noirjs_webapp/webapp1.png b/docs/static/img/tutorials/noirjs_webapp/webapp1.png new file mode 100644 index 00000000000..7591cd827e3 Binary files /dev/null and b/docs/static/img/tutorials/noirjs_webapp/webapp1.png differ diff --git a/test_programs/compile_success_empty/comptime_as_field/Nargo.toml b/test_programs/compile_success_empty/comptime_as_field/Nargo.toml new file mode 100644 index 00000000000..294832ba329 --- /dev/null +++ b/test_programs/compile_success_empty/comptime_as_field/Nargo.toml @@ -0,0 +1,6 @@ +[package] +name = "comptime_as_field" +type = "bin" +authors = [""] + +[dependencies] diff --git a/test_programs/compile_success_empty/comptime_as_field/src/main.nr b/test_programs/compile_success_empty/comptime_as_field/src/main.nr new file mode 100644 index 00000000000..e13db54b80b --- /dev/null +++ b/test_programs/compile_success_empty/comptime_as_field/src/main.nr @@ -0,0 +1,6 @@ +fn main() { + comptime { + let _: U128 = U128::from_integer(1); + } +} + diff --git a/test_programs/compile_success_empty/comptime_from_field/Nargo.toml b/test_programs/compile_success_empty/comptime_from_field/Nargo.toml new file mode 100644 index 00000000000..38a46ba0dbe --- /dev/null +++ b/test_programs/compile_success_empty/comptime_from_field/Nargo.toml @@ -0,0 +1,6 @@ +[package] +name = "comptime_from_field" +type = "bin" +authors = [""] + +[dependencies] diff --git a/test_programs/compile_success_empty/comptime_from_field/src/main.nr b/test_programs/compile_success_empty/comptime_from_field/src/main.nr new file mode 100644 index 00000000000..722c5c7eb32 --- /dev/null +++ b/test_programs/compile_success_empty/comptime_from_field/src/main.nr @@ -0,0 +1,6 @@ +fn main() { + comptime { + let _: Field = U128::from_hex("0x0").to_integer(); + } +} + diff --git a/test_programs/compile_success_empty/double_generic_alias_in_path/Nargo.toml b/test_programs/compile_success_empty/double_generic_alias_in_path/Nargo.toml new file mode 100644 index 00000000000..aaebee8d6ef --- /dev/null +++ b/test_programs/compile_success_empty/double_generic_alias_in_path/Nargo.toml @@ -0,0 +1,7 @@ +[package] +name = "double_generic_alias_in_path" +type = "bin" +authors = [""] +compiler_version = ">=0.32.0" + +[dependencies] diff --git a/test_programs/compile_success_empty/double_generic_alias_in_path/src/main.nr b/test_programs/compile_success_empty/double_generic_alias_in_path/src/main.nr new file mode 100644 index 00000000000..09f2e5c4b43 --- /dev/null +++ b/test_programs/compile_success_empty/double_generic_alias_in_path/src/main.nr @@ -0,0 +1,14 @@ +struct Foo {} + +impl Foo { + fn new() -> Self { + Self {} + } +} + +type FooAlias1 = Foo; +type FooAlias2 = FooAlias1; + +fn main() { + let _ = FooAlias2::new(); +} diff --git a/test_programs/execution_report.sh b/test_programs/execution_report.sh new file mode 100755 index 00000000000..bdb1500d19e --- /dev/null +++ b/test_programs/execution_report.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +set -e + +current_dir=$(pwd) +base_path="$current_dir/execution_success" + +# Tests to be profiled for execution report +tests_to_profile=("sha256_regression" "regression_4709" "ram_blowup_regression") + +echo "{\"execution_reports\": [ " > $current_dir/execution_report.json + +# If there is an argument that means we want to generate a report for only the current directory +if [ "$#" -ne 0 ]; then + base_path="$current_dir" + tests_to_profile=(".") +fi + +ITER="1" +NUM_ARTIFACTS=${#tests_to_profile[@]} + +for dir in ${tests_to_profile[@]}; do + if [[ " ${excluded_dirs[@]} " =~ " ${dir} " ]]; then + continue + fi + + if [[ ${CI-false} = "true" ]] && [[ " ${ci_excluded_dirs[@]} " =~ " ${dir} " ]]; then + continue + fi + + cd $base_path/$dir + + # The default package to run is the supplied list hardcoded at the top of the script + PACKAGE_NAME=$dir + # Otherwise default to the current directory as the package we want to run + if [ "$#" -ne 0 ]; then + PACKAGE_NAME=$(basename $current_dir) + fi + + # Check whether a compilation artifact exists. + # Any programs part of this benchmark should already be compiled. + # We want to make sure that compilation time is not included in the execution time. + if [ ! -e ./target/*.json ]; then + echo "Missing compilation artifact for $PACKAGE_NAME" + exit 1 + fi + + COMPILE_TIME=$((time nargo execute --silence-warnings) 2>&1 | grep real | grep -oE '[0-9]+m[0-9]+.[0-9]+s') + echo -e " {\n \"artifact_name\":\"$PACKAGE_NAME\",\n \"time\":\"$COMPILE_TIME\"" >> $current_dir/execution_report.json + + if (($ITER == $NUM_ARTIFACTS)); then + echo "}" >> $current_dir/execution_report.json + else + echo "}, " >> $current_dir/execution_report.json + fi + + ITER=$(( $ITER + 1 )) +done + +echo "]}" >> $current_dir/execution_report.json diff --git a/tooling/lsp/src/requests/hover.rs b/tooling/lsp/src/requests/hover.rs index bb1ea661719..42b9705f3b1 100644 --- a/tooling/lsp/src/requests/hover.rs +++ b/tooling/lsp/src/requests/hover.rs @@ -5,7 +5,6 @@ use fm::{FileMap, PathString}; use lsp_types::{Hover, HoverContents, HoverParams, MarkupContent, MarkupKind}; use noirc_frontend::{ ast::{ItemVisibility, Visibility}, - elaborator::types::try_eval_array_length_id, hir::def_map::ModuleId, hir_def::{ expr::{HirArrayLiteral, HirExpression, HirLiteral}, @@ -205,8 +204,17 @@ fn format_global(id: GlobalId, args: &ProcessRequestCallbackArgs) -> String { string.push('\n'); } + let mut print_comptime = definition.comptime; + let mut opt_value = None; + + // See if we can figure out what's the global's value + if let Some(stmt) = args.interner.get_global_let_statement(id) { + print_comptime = stmt.comptime; + opt_value = get_global_value(args.interner, stmt.expression); + } + string.push_str(" "); - if definition.comptime { + if print_comptime { string.push_str("comptime "); } if definition.mutable { @@ -217,12 +225,9 @@ fn format_global(id: GlobalId, args: &ProcessRequestCallbackArgs) -> String { string.push_str(": "); string.push_str(&format!("{}", typ)); - // See if we can figure out what's the global's value - if let Some(stmt) = args.interner.get_global_let_statement(id) { - if let Some(value) = get_global_value(args.interner, stmt.expression) { - string.push_str(" = "); - string.push_str(&value); - } + if let Some(value) = opt_value { + string.push_str(" = "); + string.push_str(&value); } string.push_str(&go_to_type_links(&typ, args.interner, args.files)); @@ -233,13 +238,6 @@ fn format_global(id: GlobalId, args: &ProcessRequestCallbackArgs) -> String { } fn get_global_value(interner: &NodeInterner, expr: ExprId) -> Option { - let span = interner.expr_span(&expr); - - // Globals as array lengths are extremely common, so we try that first. - if let Ok(result) = try_eval_array_length_id(interner, expr, span) { - return Some(result.to_string()); - } - match interner.expression(&expr) { HirExpression::Literal(literal) => match literal { HirLiteral::Array(hir_array_literal) => { diff --git a/tooling/nargo_cli/build.rs b/tooling/nargo_cli/build.rs index 8db2c1786d8..003897489c4 100644 --- a/tooling/nargo_cli/build.rs +++ b/tooling/nargo_cli/build.rs @@ -215,6 +215,9 @@ fn test_{test_name}(force_brillig: ForceBrillig, inliner_aggressiveness: Inliner // Set the maximum increase so that part of the optimization is exercised (it might fail). nargo.arg("--max-bytecode-increase-percent"); nargo.arg("50"); + + // Check whether the test case is non-deterministic + nargo.arg("--check-non-determinism"); }} {test_content} diff --git a/tooling/nargo_cli/src/cli/compile_cmd.rs b/tooling/nargo_cli/src/cli/compile_cmd.rs index 2ecf6959a94..f718021c351 100644 --- a/tooling/nargo_cli/src/cli/compile_cmd.rs +++ b/tooling/nargo_cli/src/cli/compile_cmd.rs @@ -216,6 +216,20 @@ fn compile_programs( cached_program, )?; + if compile_options.check_non_determinism { + let (program_two, _) = compile_program( + file_manager, + parsed_files, + workspace, + package, + compile_options, + load_cached_program(package), + )?; + if fxhash::hash64(&program) != fxhash::hash64(&program_two) { + panic!("Non deterministic result compiling {}", package.name); + } + } + // Choose the target width for the final, backend specific transformation. let target_width = get_target_width(package.expression_width, compile_options.expression_width); diff --git a/tooling/nargo_cli/src/cli/test_cmd/formatters.rs b/tooling/nargo_cli/src/cli/test_cmd/formatters.rs index 1b9b2d50378..75cf14ba120 100644 --- a/tooling/nargo_cli/src/cli/test_cmd/formatters.rs +++ b/tooling/nargo_cli/src/cli/test_cmd/formatters.rs @@ -417,6 +417,7 @@ impl Formatter for JsonFormatter { let mut json = Map::new(); json.insert("type".to_string(), json!("test")); json.insert("name".to_string(), json!(&test_result.name)); + json.insert("suite".to_string(), json!(&test_result.package_name)); json.insert("exec_time".to_string(), json!(test_result.time_to_run.as_secs_f64())); let mut stdout = String::new();