diff --git a/.github/workflows/docs-pr.yml b/.github/workflows/docs-pr.yml index 02044c82224..2b304b72b6f 100644 --- a/.github/workflows/docs-pr.yml +++ b/.github/workflows/docs-pr.yml @@ -55,7 +55,7 @@ jobs: if: needs.add_label.outputs.has_label == 'true' steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v2 @@ -71,24 +71,8 @@ jobs: run: | npm i wasm-opt -g - - name: Install dependencies - run: yarn - - - name: Build acvm_js - run: yarn workspace @noir-lang/acvm_js build - - - name: Build noirc_abi - run: yarn workspace @noir-lang/noirc_abi build - - - name: Build noir_js_types - run: yarn workspace @noir-lang/types build - - - name: Build barretenberg wrapper - run: yarn workspace @noir-lang/backend_barretenberg build - - - name: Run noir_js - run: | - yarn workspace @noir-lang/noir_js build + - name: Install Yarn dependencies + uses: ./.github/actions/setup - name: Remove pre-releases working-directory: docs @@ -97,8 +81,9 @@ jobs: run: yarn setStable - name: Build docs + working-directory: docs run: - yarn workspace docs build + yarn workspaces foreach -Rt run build - name: Deploy to Netlify uses: nwtgck/actions-netlify@v2.1 diff --git a/.github/workflows/test-noir_codegen.yml b/.github/workflows/test-noir_codegen.yml new file mode 100644 index 00000000000..d5a04a9dcd0 --- /dev/null +++ b/.github/workflows/test-noir_codegen.yml @@ -0,0 +1,97 @@ +name: noir_codegen + +on: + pull_request: + merge_group: + push: + branches: + - master + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref || github.run_id }} + cancel-in-progress: true + +jobs: + build-nargo: + runs-on: ubuntu-22.04 + strategy: + matrix: + target: [x86_64-unknown-linux-gnu] + + steps: + - name: Checkout Noir repo + uses: actions/checkout@v4 + + - name: Setup toolchain + uses: dtolnay/rust-toolchain@1.71.1 + + - uses: Swatinem/rust-cache@v2 + with: + key: ${{ matrix.target }} + cache-on-failure: true + save-if: ${{ github.event_name != 'merge_group' }} + + - name: Build Nargo + run: cargo build --package nargo_cli --release + + - name: Package artifacts + run: | + mkdir dist + cp ./target/release/nargo ./dist/nargo + 7z a -ttar -so -an ./dist/* | 7z a -si ./nargo-x86_64-unknown-linux-gnu.tar.gz + + - name: Upload artifact + uses: actions/upload-artifact@v3 + with: + name: nargo + path: ./dist/* + retention-days: 3 + + test: + needs: [build-nargo] + name: Test noir_codegen + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Yarn dependencies + uses: ./.github/actions/setup + + - name: Setup toolchain + uses: dtolnay/rust-toolchain@1.71.1 + with: + targets: wasm32-unknown-unknown + + - uses: Swatinem/rust-cache@v2 + with: + key: wasm32-unknown-unknown-noir-js + cache-on-failure: true + save-if: ${{ github.event_name != 'merge_group' }} + + - name: Install jq + run: sudo apt-get install jq + + - name: Install wasm-bindgen-cli + uses: taiki-e/install-action@v2 + with: + tool: wasm-bindgen-cli@0.2.86 + + - name: Install wasm-opt + run: | + npm i wasm-opt -g + + - name: Build acvm_js + run: yarn workspace @noir-lang/acvm_js build + + - name: Build noirc_abi + run: yarn workspace @noir-lang/noirc_abi build + + - name: Build noir_js_types + run: yarn workspace @noir-lang/types build + + - name: Build noir_js + run: yarn workspace @noir-lang/noir_js build + + - name: Run noir_codegen tests + run: yarn workspace @noir-lang/noir_codegen test diff --git a/compiler/noirc_evaluator/src/ssa/function_builder/mod.rs b/compiler/noirc_evaluator/src/ssa/function_builder/mod.rs index 732664bf757..47423841a3b 100644 --- a/compiler/noirc_evaluator/src/ssa/function_builder/mod.rs +++ b/compiler/noirc_evaluator/src/ssa/function_builder/mod.rs @@ -282,32 +282,34 @@ impl FunctionBuilder { ) -> ValueId { let base = self.field_constant(FieldElement::from(2_u128)); let typ = self.current_function.dfg.type_of_value(lhs); - let (max_bit, pow) = if let Some(rhs_constant) = - self.current_function.dfg.get_numeric_constant(rhs) - { - // Happy case is that we know precisely by how many bits the the integer will - // increase: lhs_bit_size + rhs - let (rhs_bit_size_pow_2, overflows) = - 2_u32.overflowing_pow(rhs_constant.to_u128() as u32); - if overflows { - let zero = self.numeric_constant(FieldElement::zero(), typ); - return InsertInstructionResult::SimplifiedTo(zero).first(); - } - let pow = self.numeric_constant(FieldElement::from(rhs_bit_size_pow_2 as u128), typ); - (bit_size + (rhs_constant.to_u128() as u32), pow) - } else { - // we use a predicate to nullify the result in case of overflow - let bit_size_var = - self.numeric_constant(FieldElement::from(bit_size as u128), typ.clone()); - let overflow = self.insert_binary(rhs, BinaryOp::Lt, bit_size_var); - let one = self.numeric_constant(FieldElement::one(), Type::unsigned(1)); - let predicate = self.insert_binary(overflow, BinaryOp::Eq, one); - let predicate = self.insert_cast(predicate, typ.clone()); - - let pow = self.pow(base, rhs); - let pow = self.insert_cast(pow, typ); - (FieldElement::max_num_bits(), self.insert_binary(predicate, BinaryOp::Mul, pow)) - }; + let (max_bit, pow) = + if let Some(rhs_constant) = self.current_function.dfg.get_numeric_constant(rhs) { + // Happy case is that we know precisely by how many bits the the integer will + // increase: lhs_bit_size + rhs + let (rhs_bit_size_pow_2, overflows) = + 2_u128.overflowing_pow(rhs_constant.to_u128() as u32); + if overflows { + assert!(bit_size < 128, "ICE - shift left with big integers are not supported"); + if bit_size < 128 { + let zero = self.numeric_constant(FieldElement::zero(), typ); + return InsertInstructionResult::SimplifiedTo(zero).first(); + } + } + let pow = self.numeric_constant(FieldElement::from(rhs_bit_size_pow_2), typ); + (bit_size + (rhs_constant.to_u128() as u32), pow) + } else { + // we use a predicate to nullify the result in case of overflow + let bit_size_var = + self.numeric_constant(FieldElement::from(bit_size as u128), typ.clone()); + let overflow = self.insert_binary(rhs, BinaryOp::Lt, bit_size_var); + let one = self.numeric_constant(FieldElement::one(), Type::unsigned(1)); + let predicate = self.insert_binary(overflow, BinaryOp::Eq, one); + let predicate = self.insert_cast(predicate, typ.clone()); + + let pow = self.pow(base, rhs); + let pow = self.insert_cast(pow, typ); + (FieldElement::max_num_bits(), self.insert_binary(predicate, BinaryOp::Mul, pow)) + }; let instruction = Instruction::Binary(Binary { lhs, rhs: pow, operator: BinaryOp::Mul }); if max_bit <= bit_size { diff --git a/compiler/noirc_evaluator/src/ssa/opt/mem2reg/block.rs b/compiler/noirc_evaluator/src/ssa/opt/mem2reg/block.rs index 22c5705b723..532785d2928 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/mem2reg/block.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/mem2reg/block.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, collections::BTreeMap}; +use std::borrow::Cow; use crate::ssa::ir::{ function::Function, @@ -19,27 +19,27 @@ pub(super) struct Block { /// Maps a ValueId to the Expression it represents. /// Multiple ValueIds can map to the same Expression, e.g. /// dereferences to the same allocation. - pub(super) expressions: BTreeMap, + pub(super) expressions: im::OrdMap, /// Each expression is tracked as to how many aliases it /// may have. If there is only 1, we can attempt to optimize /// out any known loads to that alias. Note that "alias" here /// includes the original reference as well. - pub(super) aliases: BTreeMap, + pub(super) aliases: im::OrdMap, /// Each allocate instruction result (and some reference block parameters) /// will map to a Reference value which tracks whether the last value stored /// to the reference is known. - pub(super) references: BTreeMap, + pub(super) references: im::OrdMap, /// The last instance of a `Store` instruction to each address in this block - pub(super) last_stores: BTreeMap, + pub(super) last_stores: im::OrdMap, } /// An `Expression` here is used to represent a canonical key /// into the aliases map since otherwise two dereferences of the /// same address will be given different ValueIds. -#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq)] +#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)] pub(super) enum Expression { Dereference(Box), ArrayElement(Box), @@ -111,10 +111,7 @@ impl Block { } fn invalidate_all_references(&mut self) { - for reference_value in self.references.values_mut() { - *reference_value = ReferenceValue::Unknown; - } - + self.references.clear(); self.last_stores.clear(); } @@ -137,7 +134,7 @@ impl Block { } // Keep only the references present in both maps. - let mut intersection = BTreeMap::new(); + let mut intersection = im::OrdMap::new(); for (value_id, reference) in &other.references { if let Some(existing) = self.references.get(value_id) { intersection.insert(*value_id, existing.unify(*reference)); diff --git a/compiler/noirc_frontend/src/hir/def_collector/dc_crate.rs b/compiler/noirc_frontend/src/hir/def_collector/dc_crate.rs index ce1cf675a07..f798dcb64e7 100644 --- a/compiler/noirc_frontend/src/hir/def_collector/dc_crate.rs +++ b/compiler/noirc_frontend/src/hir/def_collector/dc_crate.rs @@ -646,7 +646,6 @@ fn resolve_globals( let globals = vecmap(globals, |global| { let module_id = ModuleId { local_id: global.module_id, krate: crate_id }; let path_resolver = StandardPathResolver::new(module_id); - let storage_slot = context.next_storage_slot(module_id); let mut resolver = Resolver::new( &mut context.def_interner, @@ -662,7 +661,7 @@ fn resolve_globals( context.def_interner.update_global(global.stmt_id, hir_stmt); - context.def_interner.push_global(global.stmt_id, name, global.module_id, storage_slot); + context.def_interner.push_global(global.stmt_id, name, global.module_id); (global.file_id, global.stmt_id) }); diff --git a/compiler/noirc_frontend/src/hir/def_collector/dc_mod.rs b/compiler/noirc_frontend/src/hir/def_collector/dc_mod.rs index 17aa5e9951f..2f79333620e 100644 --- a/compiler/noirc_frontend/src/hir/def_collector/dc_mod.rs +++ b/compiler/noirc_frontend/src/hir/def_collector/dc_mod.rs @@ -528,11 +528,11 @@ impl<'a> ModCollector<'a> { }; errors.push((error.into(), location.file)); - let error2 = DefCollectorErrorKind::ModuleOriginallyDefined { + let error = DefCollectorErrorKind::ModuleOriginallyDefined { mod_name: mod_name.clone(), span: old_location.span, }; - errors.push((error2.into(), old_location.file)); + errors.push((error.into(), old_location.file)); return errors; } diff --git a/compiler/noirc_frontend/src/hir/mod.rs b/compiler/noirc_frontend/src/hir/mod.rs index 5a28d7b779a..789a59e276f 100644 --- a/compiler/noirc_frontend/src/hir/mod.rs +++ b/compiler/noirc_frontend/src/hir/mod.rs @@ -29,10 +29,6 @@ pub struct Context { /// A map of each file that already has been visited from a prior `mod foo;` declaration. /// This is used to issue an error if a second `mod foo;` is declared to the same file. pub visited_files: BTreeMap, - - /// Maps a given (contract) module id to the next available storage slot - /// for that contract. - pub storage_slots: BTreeMap, } #[derive(Debug, Copy, Clone)] @@ -42,8 +38,6 @@ pub enum FunctionNameMatch<'a> { Contains(&'a str), } -pub type StorageSlot = u32; - impl Context { pub fn new(file_manager: FileManager, crate_graph: CrateGraph) -> Context { Context { @@ -52,7 +46,6 @@ impl Context { visited_files: BTreeMap::new(), crate_graph, file_manager, - storage_slots: BTreeMap::new(), } } @@ -200,16 +193,4 @@ impl Context { fn module(&self, module_id: def_map::ModuleId) -> &def_map::ModuleData { module_id.module(&self.def_maps) } - - /// Returns the next available storage slot in the given module. - /// Returns None if the given module is not a contract module. - fn next_storage_slot(&mut self, module_id: def_map::ModuleId) -> Option { - let module = self.module(module_id); - - module.is_contract.then(|| { - let next_slot = self.storage_slots.entry(module_id).or_insert(0); - *next_slot += 1; - *next_slot - }) - } } diff --git a/compiler/noirc_frontend/src/node_interner.rs b/compiler/noirc_frontend/src/node_interner.rs index 300a95f819c..e66a6d57605 100644 --- a/compiler/noirc_frontend/src/node_interner.rs +++ b/compiler/noirc_frontend/src/node_interner.rs @@ -9,7 +9,6 @@ use crate::ast::Ident; use crate::graph::CrateId; use crate::hir::def_collector::dc_crate::{UnresolvedStruct, UnresolvedTrait, UnresolvedTypeAlias}; use crate::hir::def_map::{LocalModuleId, ModuleId}; -use crate::hir::StorageSlot; use crate::hir_def::stmt::HirLetStatement; use crate::hir_def::traits::TraitImpl; use crate::hir_def::traits::{Trait, TraitConstraint}; @@ -399,10 +398,6 @@ impl DefinitionKind { pub struct GlobalInfo { pub ident: Ident, pub local_id: LocalModuleId, - - /// Global definitions have an associated storage slot if they are defined within - /// a contract. If they're defined elsewhere, this value is None. - pub storage_slot: Option, } impl Default for NodeInterner { @@ -578,14 +573,8 @@ impl NodeInterner { self.id_to_type.insert(definition_id.into(), typ); } - pub fn push_global( - &mut self, - stmt_id: StmtId, - ident: Ident, - local_id: LocalModuleId, - storage_slot: Option, - ) { - self.globals.insert(stmt_id, GlobalInfo { ident, local_id, storage_slot }); + pub fn push_global(&mut self, stmt_id: StmtId, ident: Ident, local_id: LocalModuleId) { + self.globals.insert(stmt_id, GlobalInfo { ident, local_id }); } /// Intern an empty global stmt. Used for collecting globals diff --git a/compiler/noirc_printable_type/src/lib.rs b/compiler/noirc_printable_type/src/lib.rs index 195e7277bd2..e10e400b0db 100644 --- a/compiler/noirc_printable_type/src/lib.rs +++ b/compiler/noirc_printable_type/src/lib.rs @@ -209,7 +209,7 @@ fn to_string(value: &PrintableValue, typ: &PrintableType) -> Option { } (PrintableValue::String(s), PrintableType::String { .. }) => { - output.push_str(&format!(r#""{s}""#)); + output.push_str(s); } (PrintableValue::Struct(map), PrintableType::Struct { name, fields, .. }) => { diff --git a/docs/package.json b/docs/package.json index 09f8d718b56..db0efbe7543 100644 --- a/docs/package.json +++ b/docs/package.json @@ -13,6 +13,7 @@ "@docusaurus/preset-classic": "^2.4.0", "@easyops-cn/docusaurus-search-local": "^0.35.0", "@mdx-js/react": "^1.6.22", + "@noir-lang/noir_js": "workspace:*", "axios": "^1.4.0", "clsx": "^1.2.1", "docusaurus-plugin-typedoc": "1.0.0-next.18", diff --git a/tooling/debugger/src/context.rs b/tooling/debugger/src/context.rs index 35cfba497f0..4c429ca2a67 100644 --- a/tooling/debugger/src/context.rs +++ b/tooling/debugger/src/context.rs @@ -522,7 +522,7 @@ fn test_break_brillig_block_while_stepping_acir_opcodes() { // set breakpoint let breakpoint_location = OpcodeLocation::Brillig { acir_index: 0, brillig_index: 1 }; - assert!(context.add_breakpoint(breakpoint_location.clone())); + assert!(context.add_breakpoint(breakpoint_location)); // execute the first ACIR opcode (Brillig block) -> should reach the breakpoint instead let result = context.step_acir_opcode(); diff --git a/tooling/nargo_cli/tests/acir_artifacts/bit_shifts_comptime/target/acir.gz b/tooling/nargo_cli/tests/acir_artifacts/bit_shifts_comptime/target/acir.gz index 27e71fce0a7..d6c006fd102 100644 Binary files a/tooling/nargo_cli/tests/acir_artifacts/bit_shifts_comptime/target/acir.gz and b/tooling/nargo_cli/tests/acir_artifacts/bit_shifts_comptime/target/acir.gz differ diff --git a/tooling/nargo_cli/tests/acir_artifacts/bit_shifts_comptime/target/witness.gz b/tooling/nargo_cli/tests/acir_artifacts/bit_shifts_comptime/target/witness.gz index 3fadfc3d5be..81bae695da1 100644 Binary files a/tooling/nargo_cli/tests/acir_artifacts/bit_shifts_comptime/target/witness.gz and b/tooling/nargo_cli/tests/acir_artifacts/bit_shifts_comptime/target/witness.gz differ diff --git a/tooling/nargo_cli/tests/execution_success/array_dynamic/src/main.nr b/tooling/nargo_cli/tests/execution_success/array_dynamic/src/main.nr index 76e7f70f5d4..dde7bacc455 100644 --- a/tooling/nargo_cli/tests/execution_success/array_dynamic/src/main.nr +++ b/tooling/nargo_cli/tests/execution_success/array_dynamic/src/main.nr @@ -1,5 +1,11 @@ fn main( - x: [u32; 5], mut z: u32, t: u32, index: [Field;5], index2: [Field;5], offset: Field, sublen: Field + x: [u32; 5], + mut z: u32, + t: u32, + index: [Field;5], + index2: [Field;5], + offset: Field, + sublen: Field ) { let idx = (z - 5 * t - 5) as Field; //dynamic array test diff --git a/tooling/nargo_cli/tests/execution_success/bit_shifts_comptime/src/main.nr b/tooling/nargo_cli/tests/execution_success/bit_shifts_comptime/src/main.nr index a03376d062a..9bb1028173d 100644 --- a/tooling/nargo_cli/tests/execution_success/bit_shifts_comptime/src/main.nr +++ b/tooling/nargo_cli/tests/execution_success/bit_shifts_comptime/src/main.nr @@ -13,6 +13,8 @@ fn main(x: u64) { //regression for 3481 assert(x << 63 == 0); + + assert_eq((1 as u56) << (32 as u56), 0x0100000000); } fn regression_2250() { diff --git a/tooling/nargo_cli/tests/execution_success/brillig_scalar_mul/src/main.nr b/tooling/nargo_cli/tests/execution_success/brillig_scalar_mul/src/main.nr index 850800f44d7..ab2f79eb815 100644 --- a/tooling/nargo_cli/tests/execution_success/brillig_scalar_mul/src/main.nr +++ b/tooling/nargo_cli/tests/execution_success/brillig_scalar_mul/src/main.nr @@ -1,7 +1,12 @@ use dep::std; unconstrained fn main( - a: Field, a_pub_x: pub Field, a_pub_y: pub Field, b: Field, b_pub_x: pub Field, b_pub_y: pub Field + a: Field, + a_pub_x: pub Field, + a_pub_y: pub Field, + b: Field, + b_pub_x: pub Field, + b_pub_y: pub Field ) { let mut priv_key = a; let mut pub_x: Field = a_pub_x; diff --git a/tooling/nargo_cli/tests/execution_success/brillig_schnorr/src/main.nr b/tooling/nargo_cli/tests/execution_success/brillig_schnorr/src/main.nr index 8acdb792007..4cc79ae7e07 100644 --- a/tooling/nargo_cli/tests/execution_success/brillig_schnorr/src/main.nr +++ b/tooling/nargo_cli/tests/execution_success/brillig_schnorr/src/main.nr @@ -2,7 +2,11 @@ use dep::std; // Note: If main has any unsized types, then the verifier will never be able // to figure out the circuit instance unconstrained fn main( - message: [u8; 10], message_field: Field, pub_key_x: Field, pub_key_y: Field, signature: [u8; 64] + message: [u8; 10], + message_field: Field, + pub_key_x: Field, + pub_key_y: Field, + signature: [u8; 64] ) { // Regression for issue #2421 // We want to make sure that we can accurately verify a signature whose message is a slice vs. an array diff --git a/tooling/nargo_cli/tests/execution_success/schnorr/src/main.nr b/tooling/nargo_cli/tests/execution_success/schnorr/src/main.nr index 299bbdc07e2..107af152625 100644 --- a/tooling/nargo_cli/tests/execution_success/schnorr/src/main.nr +++ b/tooling/nargo_cli/tests/execution_success/schnorr/src/main.nr @@ -2,7 +2,11 @@ use dep::std; // Note: If main has any unsized types, then the verifier will never be able // to figure out the circuit instance fn main( - message: [u8; 10], message_field: Field, pub_key_x: Field, pub_key_y: Field, signature: [u8; 64] + message: [u8; 10], + message_field: Field, + pub_key_x: Field, + pub_key_y: Field, + signature: [u8; 64] ) { // Regression for issue #2421 // We want to make sure that we can accurately verify a signature whose message is a slice vs. an array diff --git a/tooling/nargo_fmt/src/visitor/stmt.rs b/tooling/nargo_fmt/src/visitor/stmt.rs index c27b7911d03..eabdb1a150a 100644 --- a/tooling/nargo_fmt/src/visitor/stmt.rs +++ b/tooling/nargo_fmt/src/visitor/stmt.rs @@ -4,7 +4,7 @@ use noirc_frontend::{ ConstrainKind, ConstrainStatement, ExpressionKind, ForRange, Statement, StatementKind, }; -use crate::rewrite; +use crate::{rewrite, visitor::expr::wrap_exprs}; use super::ExpressionType; @@ -33,30 +33,44 @@ impl super::FmtVisitor<'_> { self.push_rewrite(format!("{let_str} {expr_str};"), span); } StatementKind::Constrain(ConstrainStatement(expr, message, kind)) => { + let mut nested_shape = self.shape(); + let shape = nested_shape; + + nested_shape.indent.block_indent(self.config); + let message = message.map_or(String::new(), |message| format!(", \"{message}\"")); - let constrain = match kind { + + let (callee, args) = match kind { ConstrainKind::Assert => { - let assertion = rewrite::sub_expr(self, self.shape(), expr); + let assertion = rewrite::sub_expr(self, nested_shape, expr); + let args = format!("{assertion}{message}"); - format!("assert({assertion}{message});") + ("assert", args) } ConstrainKind::AssertEq => { if let ExpressionKind::Infix(infix) = expr.kind { - let lhs = rewrite::sub_expr(self, self.shape(), infix.lhs); - let rhs = rewrite::sub_expr(self, self.shape(), infix.rhs); + let lhs = rewrite::sub_expr(self, nested_shape, infix.lhs); + let rhs = rewrite::sub_expr(self, nested_shape, infix.rhs); - format!("assert_eq({lhs}, {rhs}{message});") + let args = format!("{lhs}, {rhs}{message}"); + + ("assert_eq", args) } else { unreachable!() } } ConstrainKind::Constrain => { let expr = rewrite::sub_expr(self, self.shape(), expr); - format!("constrain {expr};") + let constrain = format!("constrain {expr};"); + self.push_rewrite(constrain, span); + return; } }; + let args = wrap_exprs("(", ")", args, nested_shape, shape, true); + let constrain = format!("{callee}{args};"); + self.push_rewrite(constrain, span); } StatementKind::For(for_stmt) => { diff --git a/tooling/nargo_fmt/tests/expected/call.nr b/tooling/nargo_fmt/tests/expected/call.nr index 7824ba37089..de78d7c4edb 100644 --- a/tooling/nargo_fmt/tests/expected/call.nr +++ b/tooling/nargo_fmt/tests/expected/call.nr @@ -39,10 +39,12 @@ fn foo() { assert(x == y); - assert(p4_affine.eq( - Gaffine::new( - 6890855772600357754907169075114257697580319025794532037257385534741338397365, - 4338620300185947561074059802482547481416142213883829469920100239455078257889 + assert( + p4_affine.eq( + Gaffine::new( + 6890855772600357754907169075114257697580319025794532037257385534741338397365, + 4338620300185947561074059802482547481416142213883829469920100239455078257889 + ) ) - )); + ); } diff --git a/tooling/nargo_fmt/tests/expected/infix.nr b/tooling/nargo_fmt/tests/expected/infix.nr index cbc73045fe3..228dfdf68c4 100644 --- a/tooling/nargo_fmt/tests/expected/infix.nr +++ b/tooling/nargo_fmt/tests/expected/infix.nr @@ -9,10 +9,12 @@ fn foo() { } fn big() { - assert(bjj_affine.contains(bjj_affine.gen) + assert( + bjj_affine.contains(bjj_affine.gen) & bjj_affine.contains(p1_affine) & bjj_affine.contains(p2_affine) & bjj_affine.contains(p3_affine) & bjj_affine.contains(p4_affine) - & bjj_affine.contains(p5_affine)); + & bjj_affine.contains(p5_affine) + ); } diff --git a/tooling/noir_codegen/.gitignore b/tooling/noir_codegen/.gitignore index 721d05448d6..15ea344d453 100644 --- a/tooling/noir_codegen/.gitignore +++ b/tooling/noir_codegen/.gitignore @@ -2,3 +2,4 @@ crs lib !test/*/target +test/codegen diff --git a/tooling/noir_codegen/package.json b/tooling/noir_codegen/package.json index 14f9bad6df6..f2523654aab 100644 --- a/tooling/noir_codegen/package.json +++ b/tooling/noir_codegen/package.json @@ -26,8 +26,10 @@ "scripts": { "dev": "tsc-multi --watch", "build": "tsc", - "test": "ts-node --esm src/main.ts ./test/assert_lt/target/** --out-dir ./test/codegen && yarn test:node && rm -rf ./test/codegen", + "test": "yarn test:codegen && yarn test:node && yarn test:clean", + "test:codegen": "ts-node --esm src/main.ts ./test/assert_lt/target/** --out-dir ./test/codegen", "test:node": "mocha --timeout 25000 --exit --config ./.mocharc.json", + "test:clean": "rm -rf ./test/codegen", "prettier": "prettier 'src/**/*.ts'", "prettier:fix": "prettier --write 'src/**/*.ts' 'test/**/*.ts'", "lint": "NODE_NO_WARNINGS=1 eslint . --ext .ts --ignore-path ./.eslintignore --max-warnings 0", diff --git a/tooling/noir_codegen/src/index.ts b/tooling/noir_codegen/src/index.ts index 1f8d2d183d4..8d45b76bd7d 100644 --- a/tooling/noir_codegen/src/index.ts +++ b/tooling/noir_codegen/src/index.ts @@ -1,24 +1,56 @@ import { CompiledCircuit } from '@noir-lang/types'; +import { PrimitiveTypesUsed, generateTsInterface } from './noir_types.js'; -const codegenImports = `import { InputMap, InputValue } from "@noir-lang/noirc_abi" -import { Noir } from "@noir-lang/noir_js"`; +// TODO: reenable this. See `abiTypeToTs` for reasoning. +// export type FixedLengthArray = L extends 0 ? never[]: T[] & { length: L }; + +const codegenPrelude = `/* Autogenerated file, do not edit! */ + +/* eslint-disable */ + +import { Noir, InputMap, CompiledCircuit } from "@noir-lang/noir_js" +`; const codegenFunction = ( name: string, compiled_program: CompiledCircuit, -) => `export async function ${name}(args: InputMap): Promise { - const program = new Noir(${JSON.stringify(compiled_program)}); + function_signature: { inputs: [string, string][]; returnValue: string | null }, +) => { + const args = function_signature.inputs.map(([name]) => `${name}`).join(', '); + const args_with_types = function_signature.inputs.map(([name, type]) => `${name}: ${type}`).join(', '); + + return ` +export const ${name}_circuit: CompiledCircuit = ${JSON.stringify(compiled_program)}; + +export async function ${name}(${args_with_types}): Promise<${function_signature.returnValue}> { + const program = new Noir(${name}_circuit); + const args: InputMap = { ${args} }; const { returnValue } = await program.execute(args); - return returnValue; + return returnValue as ${function_signature.returnValue}; }`; +}; export const codegen = (programs: [string, CompiledCircuit][]): string => { - const results = [codegenImports]; + let results = [codegenPrelude]; + const primitiveTypeMap = new Map(); + + const functions: string[] = []; for (const [name, program] of programs) { - results.push(codegenFunction(name, stripUnwantedFields(program))); + const [types_string, function_sig] = generateTsInterface(program.abi, primitiveTypeMap); + functions.push(types_string); + functions.push('\n'); + functions.push(codegenFunction(name, stripUnwantedFields(program), function_sig)); } - return results.join('\n\n'); + // Add the primitive Noir types that do not have a 1-1 mapping to TypeScript. + const primitiveTypeAliases: string[] = []; + for (const value of primitiveTypeMap.values()) { + primitiveTypeAliases.push(`export type ${value.aliasName} = ${value.tsType};`); + } + + results = results.concat(...primitiveTypeAliases, ...functions); + + return results.filter((val) => val !== '').join('\n'); }; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/tooling/noir_codegen/src/noir_types.ts b/tooling/noir_codegen/src/noir_types.ts new file mode 100644 index 00000000000..d1a22a3e2da --- /dev/null +++ b/tooling/noir_codegen/src/noir_types.ts @@ -0,0 +1,189 @@ +import { AbiType, Abi } from '@noir-lang/noirc_abi'; + +/** + * Keep track off all of the Noir primitive types that were used. + * Most of these will not have a 1-1 definition in TypeScript, + * so we will need to generate type aliases for them. + * + * We want to generate type aliases + * for specific types that are used in the ABI. + * + * For example: + * - If `Field` is used we want to alias that + * with `number`. + * - If `u32` is used we want to alias that with `number` too. + */ +export type PrimitiveTypesUsed = { + /** + * The name of the type alias that we will generate. + */ + aliasName: string; + /** + * The TypeScript type that we will alias to. + */ + tsType: string; +}; + +/** + * Typescript does not allow us to check for equality of non-primitive types + * easily, so we create a addIfUnique function that will only add an item + * to the map if it is not already there by using JSON.stringify. + * @param item - The item to add to the map. + */ +function addIfUnique(primitiveTypeMap: Map, item: PrimitiveTypesUsed) { + const key = JSON.stringify(item); + if (!primitiveTypeMap.has(key)) { + primitiveTypeMap.set(key, item); + } +} + +/** + * Converts an ABI type to a TypeScript type. + * @param type - The ABI type to convert. + * @returns The typescript code to define the type. + */ +function abiTypeToTs(type: AbiType, primitiveTypeMap: Map): string { + switch (type.kind) { + case 'field': + addIfUnique(primitiveTypeMap, { aliasName: 'Field', tsType: 'string' }); + return 'Field'; + case 'integer': { + const typeName = type.sign === 'signed' ? `i${type.width}` : `u${type.width}`; + // Javascript cannot safely represent the full range of Noir's integer types as numbers. + // `Number.MAX_SAFE_INTEGER == 2**53 - 1` so we disallow passing numbers to types which may exceed this. + // 52 has been chosen as the cutoff rather than 53 for safety. + const tsType = type.width <= 52 ? `string | number` : `string`; + + addIfUnique(primitiveTypeMap, { aliasName: typeName, tsType }); + return typeName; + } + case 'boolean': + return `boolean`; + case 'array': + // We can't force the usage of fixed length arrays as this currently throws errors in TS. + // The array would need to be `as const` to support this whereas that's unlikely to happen in user code. + // return `FixedLengthArray<${abiTypeToTs(type.type, primitiveTypeMap)}, ${type.length}>`; + return `${abiTypeToTs(type.type, primitiveTypeMap)}[]`; + case 'string': + // We could enforce that literals are the correct length but not generally. + // This would run into similar problems to above. + return `string`; + case 'struct': + return getLastComponentOfPath(type.path); + case 'tuple': { + const field_types = type.fields.map((field) => abiTypeToTs(field, primitiveTypeMap)); + return `[${field_types.join(', ')}]`; + } + default: + throw new Error(`Unknown ABI type ${JSON.stringify(type)}`); + } +} + +/** + * Returns the last component of a path, e.g. "foo::bar::baz" -\> "baz" + * Note: that if we have a path such as "Baz", we will return "Baz". + * + * Since these paths corresponds to structs, we can assume that we + * cannot have "foo::bar::". + * + * We also make the assumption that since these paths are coming from + * Noir, then we will not have two paths that look like this: + * - foo::bar::Baz + * - cat::dog::Baz + * ie the last component of the path (struct name) is enough to uniquely identify + * the whole path. + * + * TODO: We should double check this assumption when we use type aliases, + * I expect that `foo::bar::Baz as Dog` would effectively give `foo::bar::Dog` + * @param str - The path to get the last component of. + * @returns The last component of the path. + */ +function getLastComponentOfPath(str: string): string { + const parts = str.split('::'); + const lastPart = parts[parts.length - 1]; + return lastPart; +} + +/** + * Generates TypeScript interfaces for the structs used in the ABI. + * @param type - The ABI type to generate the interface for. + * @param output - The set of structs that we have already generated bindings for. + * @returns The TypeScript code to define the struct. + */ +function generateStructInterfaces( + type: AbiType, + output: Set, + primitiveTypeMap: Map, +): string { + let result = ''; + + // Edge case to handle the array of structs case. + if (type.kind === 'array' && type.type.kind === 'struct' && !output.has(getLastComponentOfPath(type.type.path))) { + result += generateStructInterfaces(type.type, output, primitiveTypeMap); + } + if (type.kind !== 'struct') return result; + + // List of structs encountered while viewing this type that we need to generate + // bindings for. + const typesEncountered = new Set(); + + // Codegen the struct and then its fields, so that the structs fields + // are defined before the struct itself. + let codeGeneratedStruct = ''; + let codeGeneratedStructFields = ''; + + const structName = getLastComponentOfPath(type.path); + if (!output.has(structName)) { + codeGeneratedStruct += `export type ${structName} = {\n`; + for (const field of type.fields) { + codeGeneratedStruct += ` ${field.name}: ${abiTypeToTs(field.type, primitiveTypeMap)};\n`; + typesEncountered.add(field.type); + } + codeGeneratedStruct += `};`; + output.add(structName); + + // Generate code for the encountered structs in the field above + for (const type of typesEncountered) { + codeGeneratedStructFields += generateStructInterfaces(type, output, primitiveTypeMap); + } + } + + return codeGeneratedStructFields + '\n' + codeGeneratedStruct; +} + +/** + * Generates a TypeScript interface for the ABI. + * @param abiObj - The ABI to generate the interface for. + * @returns The TypeScript code to define the interface. + */ +export function generateTsInterface( + abiObj: Abi, + primitiveTypeMap: Map, +): [string, { inputs: [string, string][]; returnValue: string | null }] { + let result = ``; + const outputStructs = new Set(); + + // Define structs for composite types + for (const param of abiObj.parameters) { + result += generateStructInterfaces(param.type, outputStructs, primitiveTypeMap); + } + + // Generating Return type, if it exists + if (abiObj.return_type != null) { + result += generateStructInterfaces(abiObj.return_type, outputStructs, primitiveTypeMap); + } + + return [result, getTsFunctionSignature(abiObj, primitiveTypeMap)]; +} + +function getTsFunctionSignature( + abi: Abi, + primitiveTypeMap: Map, +): { inputs: [string, string][]; returnValue: string | null } { + const inputs: [string, string][] = abi.parameters.map((param) => [ + param.name, + abiTypeToTs(param.type, primitiveTypeMap), + ]); + const returnValue = abi.return_type ? abiTypeToTs(abi.return_type, primitiveTypeMap) : null; + return { inputs, returnValue }; +} diff --git a/tooling/noir_codegen/test/assert_lt/src/main.nr b/tooling/noir_codegen/test/assert_lt/src/main.nr index 0698cbce4a8..3b3e04ddece 100644 --- a/tooling/noir_codegen/test/assert_lt/src/main.nr +++ b/tooling/noir_codegen/test/assert_lt/src/main.nr @@ -1,4 +1,19 @@ -fn main(x: u64, y: pub u64) -> pub u64 { +struct MyStruct { + foo: bool, + bar: [str<5>; 3], +} + +fn main( + x: u64, + y: pub u64, + array: [u8; 5], + my_struct: MyStruct, + string: str<5> +) -> pub (u64, u64, MyStruct) { + assert(array.len() == 5); + assert(my_struct.foo); + assert(string == "12345"); + assert(x < y); - x + y + (x + y, 3, my_struct) } diff --git a/tooling/noir_codegen/test/assert_lt/target/assert_lt.json b/tooling/noir_codegen/test/assert_lt/target/assert_lt.json index 3b2b1b2c5a1..6d928a26d43 100644 --- a/tooling/noir_codegen/test/assert_lt/target/assert_lt.json +++ b/tooling/noir_codegen/test/assert_lt/target/assert_lt.json @@ -1 +1 @@ -{"hash":13834844072603749544,"backend":"acvm-backend-barretenberg","abi":{"parameters":[{"name":"x","type":{"kind":"integer","sign":"unsigned","width":64},"visibility":"private"},{"name":"y","type":{"kind":"integer","sign":"unsigned","width":64},"visibility":"public"}],"param_witnesses":{"x":[1],"y":[2]},"return_type":{"kind":"integer","sign":"unsigned","width":64},"return_witnesses":[12]},"bytecode":"H4sIAAAAAAAA/+1WUW6DMAx1QksZoGr72jUcAiX8VbvJ0Oj9j7ChJpKbtXw0NpvUWkImUXixn53w3gDgHc6mfh7t/ZGMtR9TU96HeYuHtp36ZjLWfGIzjK7DthsPzjjTue6rcdZOrnX9MA49Dqa1kzl1gz3h2bL7sTDCMhmJbylmTDOT8WEhjXfjH/DcB8u8zwVygWifmL/9lTnWzSWKsxHA3QJf00vlveWvERJIUU4x0eb86aEJppljVox9oO+Py8QTV1Jnw6a85t7vSL8pwvN89j7gd88o8q79Gr2wRt3AeSFz4XvRSyokl5MAtSfgGO2ZCewdsDibLRVrDzIXTMxfqiLIGXPeMdY1gb/Fg8+tznJY50eSGmfB2DNrqciCD+tCRc4X5FNFJmIWnkhu3BL+t4qc8y75aySqIkvGOP9CRWKaGQ0ydUrsgUUVWXlfw4OpyAouVWQN66pITDPDqSJfQaZxuVVkxZhzzVgLTv5uHbDwXhN+vwGywklHPBQAAA=="} \ No newline at end of file +{"noir_version":"0.19.3+e9322d14070fa444d77ee5c43c905dd86a67c6e3","hash":9449934793688855780,"backend":"acvm-backend-barretenberg","abi":{"parameters":[{"name":"x","type":{"kind":"integer","sign":"unsigned","width":64},"visibility":"private"},{"name":"y","type":{"kind":"integer","sign":"unsigned","width":64},"visibility":"public"},{"name":"array","type":{"kind":"array","length":5,"type":{"kind":"integer","sign":"unsigned","width":8}},"visibility":"private"},{"name":"my_struct","type":{"kind":"struct","path":"MyStruct","fields":[{"name":"foo","type":{"kind":"boolean"}},{"name":"bar","type":{"kind":"array","length":3,"type":{"kind":"string","length":5}}}]},"visibility":"private"},{"name":"string","type":{"kind":"string","length":5},"visibility":"private"}],"param_witnesses":{"array":[{"start":3,"end":8}],"my_struct":[{"start":8,"end":24}],"string":[{"start":24,"end":29}],"x":[{"start":1,"end":2}],"y":[{"start":2,"end":3}]},"return_type":{"kind":"tuple","fields":[{"kind":"integer","sign":"unsigned","width":64},{"kind":"integer","sign":"unsigned","width":64},{"kind":"struct","path":"MyStruct","fields":[{"name":"foo","type":{"kind":"boolean"}},{"name":"bar","type":{"kind":"array","length":3,"type":{"kind":"string","length":5}}}]}]},"return_witnesses":[31,32,33,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23]},"bytecode":"H4sIAAAAAAAA/81XbU/CMBDu5hv4gopvvGw49JOJH1q2wfaN+E+AddFEgzGL/H250Go5dInumnhJ0z2jXJ9er7s+t4yxe7YyZ9lc1Y8N7CK8tWw1A28jvIPwLsJ7Cus5mfIPxquZqBlzmX5DPowiORpIEYoJH6TTJOZRPB0mIhFxEmeDJAxlEiWjdJqOeCqiUIo8TsNcOa7RceQ6DnUUl32EDxA+RPgI4QbCxwifIHyKcBPhM4TPEb5A+BLhK4RbCLcR7iDcRdhjX3mjzUb+jIlyxibPFgFPmYNlVnm2yXjOcps8O3Q8pU2eXTqemU2eHh3PGdQbl22aS8zZYXRn3/07L4FffLN0Mt9mXH3V99iqhuu80GOgzj+wzZxxjGdXjXFLxjg/+Kkb7/T/G8bvVRe/EQxzciqfvgok9QXEp+P4eQHpGT61bRHHw9ahqurrhjCeZfH7JU+OeAqfcM09wn2tEL/SD9x/Pjdl+8yr2do54dVMUJ6Ta0b/3TF92tr3gI53aJNnn3DfuwZHyE8o2FDIQYBr0Q1FFoQmiEsQlCAiociCWASBCKIQhCCIPxB8IPJA2IGYA9EBF3q4LMNcHlsv/E31XGUOyI1g2fpsvfDfqd5T/aQo5MtrERTzYJJlweKpeAzm7/Itf54vPgBYg2KL1RAAAA=="} \ No newline at end of file diff --git a/tooling/noir_codegen/test/index.test.ts b/tooling/noir_codegen/test/index.test.ts index 702ba1f9cbb..48199c13a67 100644 --- a/tooling/noir_codegen/test/index.test.ts +++ b/tooling/noir_codegen/test/index.test.ts @@ -1,11 +1,16 @@ import { expect } from 'chai'; -import { assert_lt } from './codegen/index.js'; +import { assert_lt, MyStruct, u64 } from './codegen/index.js'; it('codegens a callable function', async () => { - const result = await assert_lt({ - x: '2', - y: '3', - }); + const [sum, constant, struct]: [u64, u64, MyStruct] = await assert_lt( + '2', + '3', + [0, 0, 0, 0, 0], + { foo: true, bar: ['12345', '12345', '12345'] }, + '12345', + ); - expect(result).to.be.eq('0x05'); + expect(sum).to.be.eq('0x05'); + expect(constant).to.be.eq('0x03'); + expect(struct).to.be.deep.eq({ foo: true, bar: ['12345', '12345', '12345'] }); }); diff --git a/tooling/noir_js/src/index.ts b/tooling/noir_js/src/index.ts index 5e700a5b583..bacb391a464 100644 --- a/tooling/noir_js/src/index.ts +++ b/tooling/noir_js/src/index.ts @@ -12,6 +12,7 @@ export { and, } from '@noir-lang/acvm_js'; +export { InputMap } from '@noir-lang/noirc_abi'; export { WitnessMap, ForeignCallHandler, ForeignCallInput, ForeignCallOutput } from '@noir-lang/acvm_js'; export { Noir } from './program.js'; diff --git a/tooling/noir_js_types/package.json b/tooling/noir_js_types/package.json index 57bb2f050b7..4232358ddff 100644 --- a/tooling/noir_js_types/package.json +++ b/tooling/noir_js_types/package.json @@ -19,7 +19,8 @@ "build": "yarn run build:cjs && yarn run build:esm", "nightly:version": "jq --arg new_version \"-$(git rev-parse --short HEAD)$1\" '.version = .version + $new_version' package.json > package-tmp.json && mv package-tmp.json package.json", "publish": "echo 📡 publishing `$npm_package_name` && yarn npm publish", - "lint": "NODE_NO_WARNINGS=1 eslint . --ext .ts --ignore-path ./.eslintignore --max-warnings 0" + "lint": "NODE_NO_WARNINGS=1 eslint . --ext .ts --ignore-path ./.eslintignore --max-warnings 0", + "clean": "rm -rf ./lib" }, "exports": { ".": { diff --git a/tooling/noirc_abi_wasm/src/lib.rs b/tooling/noirc_abi_wasm/src/lib.rs index 734ca1ff40e..a3d829dd40f 100644 --- a/tooling/noirc_abi_wasm/src/lib.rs +++ b/tooling/noirc_abi_wasm/src/lib.rs @@ -56,7 +56,7 @@ export type AbiType = { kind: "integer", sign: Sign, width: number } | { kind: "array", length: number, type: AbiType } | { kind: "tuple", fields: AbiType[] } | - { kind: "struct", path: string, fields: [string, AbiType][] }; + { kind: "struct", path: string, fields: { name: string, type: AbiType }[] }; export type AbiParameter = { name: string, diff --git a/yarn.lock b/yarn.lock index 9c6a447d718..6ae655f8ce2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8290,6 +8290,7 @@ __metadata: "@docusaurus/preset-classic": ^2.4.0 "@easyops-cn/docusaurus-search-local": ^0.35.0 "@mdx-js/react": ^1.6.22 + "@noir-lang/noir_js": "workspace:*" axios: ^1.4.0 clsx: ^1.2.1 docusaurus-plugin-typedoc: 1.0.0-next.18