diff --git a/.vscode/settings.json b/.vscode/settings.json index ab8056e8a7c3..96c620bae1df 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -166,6 +166,4 @@ "**/l1-contracts/lib/**": true, "**/barretenberg/cpp/build*/**": true }, - "cmake.sourceDirectory": "${workspaceFolder}/barretenberg/cpp", - "noir.nargoPath": "./noir/noir-repo/target/release/nargo" } diff --git a/noir/noir-repo/aztec_macros/src/transforms/compute_note_hash_and_nullifier.rs b/noir/noir-repo/aztec_macros/src/transforms/compute_note_hash_and_nullifier.rs index f624cde99699..0e25b6ac3ea6 100644 --- a/noir/noir-repo/aztec_macros/src/transforms/compute_note_hash_and_nullifier.rs +++ b/noir/noir-repo/aztec_macros/src/transforms/compute_note_hash_and_nullifier.rs @@ -41,7 +41,10 @@ fn check_for_compute_note_hash_and_nullifier_definition( FunctionReturnType::Default(_) => false, FunctionReturnType::Ty(unresolved_type) => { match &unresolved_type.typ { - UnresolvedTypeData::Array(_, inner_type) => matches!(inner_type.typ, UnresolvedTypeData::FieldElement), + UnresolvedTypeData::Unconstrained(inner_type) => match inner_type.to_owned().typ { + UnresolvedTypeData::Array(_, elements) => matches!(elements.typ.to_owned(), UnresolvedTypeData::FieldElement), + _ => false + }, _ => false, } } @@ -179,7 +182,7 @@ fn generate_compute_note_hash_and_nullifier_source( storage_slot: Field, note_type_id: Field, serialized_note: [Field; {}] - ) -> pub [Field; 4] {{ + ) -> pub Unconstrained<[Field; 4]> {{ assert(false, \"This contract does not use private notes\"); [0, 0, 0, 0] }}", @@ -210,7 +213,7 @@ fn generate_compute_note_hash_and_nullifier_source( storage_slot: Field, note_type_id: Field, serialized_note: [Field; {}] - ) -> pub [Field; 4] {{ + ) -> pub Unconstrained<[Field; 4]> {{ let note_header = dep::aztec::prelude::NoteHeader::new(contract_address, nonce, storage_slot); {} diff --git a/noir/noir-repo/aztec_macros/src/transforms/contract_interface.rs b/noir/noir-repo/aztec_macros/src/transforms/contract_interface.rs index 90f9ce6164a7..18e1ab230a1c 100644 --- a/noir/noir-repo/aztec_macros/src/transforms/contract_interface.rs +++ b/noir/noir-repo/aztec_macros/src/transforms/contract_interface.rs @@ -94,7 +94,9 @@ pub fn stub_function(aztec_visibility: &str, func: &NoirFunction, is_static_call "let mut args_acc: [Field] = &[]; {} let args_hash = dep::aztec::hash::hash_args(args_acc); - assert(args_hash == dep::aztec::oracle::arguments::pack_arguments(args_acc));", + dep::aztec::oracle::arguments::pack_arguments(args_acc).make_constrained(| packed_hash | {{ + assert(args_hash == packed_hash); + }});", call_args ) } else { diff --git a/noir/noir-repo/compiler/noirc_evaluator/src/errors.rs b/noir/noir-repo/compiler/noirc_evaluator/src/errors.rs index 1e9220601009..adff63229abe 100644 --- a/noir/noir-repo/compiler/noirc_evaluator/src/errors.rs +++ b/noir/noir-repo/compiler/noirc_evaluator/src/errors.rs @@ -43,6 +43,8 @@ pub enum RuntimeError { UnconstrainedSliceReturnToConstrained { call_stack: CallStack }, #[error("All `oracle` methods should be wrapped in an unconstrained fn")] UnconstrainedOracleReturnToConstrained { call_stack: CallStack }, + #[error("Called from a constrained runtime")] + OnlyWithinUnconstrained { call_stack: CallStack }, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -123,7 +125,8 @@ impl RuntimeError { | RuntimeError::NestedSlice { call_stack, .. } | RuntimeError::BigIntModulus { call_stack, .. } | RuntimeError::UnconstrainedSliceReturnToConstrained { call_stack } - | RuntimeError::UnconstrainedOracleReturnToConstrained { call_stack } => call_stack, + | RuntimeError::UnconstrainedOracleReturnToConstrained { call_stack } + | RuntimeError::OnlyWithinUnconstrained { call_stack } => call_stack, } } } diff --git a/noir/noir-repo/compiler/noirc_evaluator/src/ssa.rs b/noir/noir-repo/compiler/noirc_evaluator/src/ssa.rs index d38601bfc1b4..b346394c6d8a 100644 --- a/noir/noir-repo/compiler/noirc_evaluator/src/ssa.rs +++ b/noir/noir-repo/compiler/noirc_evaluator/src/ssa.rs @@ -59,7 +59,10 @@ pub(crate) fn optimize_into_acir( .run_pass(Ssa::defunctionalize, "After Defunctionalization:") .run_pass(Ssa::remove_paired_rc, "After Removing Paired rc_inc & rc_decs:") .run_pass(Ssa::inline_functions, "After Inlining:") - .run_pass(Ssa::resolve_is_unconstrained, "After Resolving IsUnconstrained:") + .try_run_pass( + Ssa::resolve_runtime_checks, + "After Resolving runtime checks (is_unconstrained, assert_unconstrained):", + )? // Run mem2reg with the CFG separated into blocks .run_pass(Ssa::mem2reg, "After Mem2Reg:") .run_pass(Ssa::as_slice_optimization, "After `as_slice` optimization") diff --git a/noir/noir-repo/compiler/noirc_evaluator/src/ssa/ir/instruction.rs b/noir/noir-repo/compiler/noirc_evaluator/src/ssa/ir/instruction.rs index 5110140bfcc8..8950e96fdbca 100644 --- a/noir/noir-repo/compiler/noirc_evaluator/src/ssa/ir/instruction.rs +++ b/noir/noir-repo/compiler/noirc_evaluator/src/ssa/ir/instruction.rs @@ -67,6 +67,7 @@ pub(crate) enum Intrinsic { AsField, AsWitness, IsUnconstrained, + AssertUnconstrained, } impl std::fmt::Display for Intrinsic { @@ -92,6 +93,7 @@ impl std::fmt::Display for Intrinsic { Intrinsic::AsField => write!(f, "as_field"), Intrinsic::AsWitness => write!(f, "as_witness"), Intrinsic::IsUnconstrained => write!(f, "is_unconstrained"), + Intrinsic::AssertUnconstrained => write!(f, "assert_unconstrained"), } } } @@ -102,9 +104,10 @@ impl Intrinsic { /// If there are no side effects then the `Intrinsic` can be removed if the result is unused. pub(crate) fn has_side_effects(&self) -> bool { match self { - Intrinsic::AssertConstant | Intrinsic::ApplyRangeConstraint | Intrinsic::AsWitness => { - true - } + Intrinsic::AssertConstant + | Intrinsic::ApplyRangeConstraint + | Intrinsic::AsWitness + | Intrinsic::AssertUnconstrained => true, // These apply a constraint that the input must fit into a specified number of limbs. Intrinsic::ToBits(_) | Intrinsic::ToRadix(_) => true, @@ -150,6 +153,7 @@ impl Intrinsic { "as_field" => Some(Intrinsic::AsField), "as_witness" => Some(Intrinsic::AsWitness), "is_unconstrained" => Some(Intrinsic::IsUnconstrained), + "assert_unconstrained" => Some(Intrinsic::AssertUnconstrained), other => BlackBoxFunc::lookup(other).map(Intrinsic::BlackBox), } } diff --git a/noir/noir-repo/compiler/noirc_evaluator/src/ssa/ir/instruction/call.rs b/noir/noir-repo/compiler/noirc_evaluator/src/ssa/ir/instruction/call.rs index 74e5653c7bab..96ef78d474b2 100644 --- a/noir/noir-repo/compiler/noirc_evaluator/src/ssa/ir/instruction/call.rs +++ b/noir/noir-repo/compiler/noirc_evaluator/src/ssa/ir/instruction/call.rs @@ -295,6 +295,7 @@ pub(super) fn simplify_call( } Intrinsic::AsWitness => SimplifyResult::None, Intrinsic::IsUnconstrained => SimplifyResult::None, + Intrinsic::AssertUnconstrained => SimplifyResult::None, } } diff --git a/noir/noir-repo/compiler/noirc_evaluator/src/ssa/opt/mod.rs b/noir/noir-repo/compiler/noirc_evaluator/src/ssa/opt/mod.rs index f6c3f022bfc0..9329956b127a 100644 --- a/noir/noir-repo/compiler/noirc_evaluator/src/ssa/opt/mod.rs +++ b/noir/noir-repo/compiler/noirc_evaluator/src/ssa/opt/mod.rs @@ -17,6 +17,6 @@ mod rc; mod remove_bit_shifts; mod remove_enable_side_effects; mod remove_if_else; -mod resolve_is_unconstrained; +mod resolve_runtime_checks; mod simplify_cfg; mod unrolling; diff --git a/noir/noir-repo/compiler/noirc_evaluator/src/ssa/opt/remove_enable_side_effects.rs b/noir/noir-repo/compiler/noirc_evaluator/src/ssa/opt/remove_enable_side_effects.rs index 6db769967474..4a388a545a1d 100644 --- a/noir/noir-repo/compiler/noirc_evaluator/src/ssa/opt/remove_enable_side_effects.rs +++ b/noir/noir-repo/compiler/noirc_evaluator/src/ssa/opt/remove_enable_side_effects.rs @@ -159,7 +159,8 @@ impl Context { | Intrinsic::AsField | Intrinsic::AsSlice | Intrinsic::AsWitness - | Intrinsic::IsUnconstrained => false, + | Intrinsic::IsUnconstrained + | Intrinsic::AssertUnconstrained => false, }, // We must assume that functions contain a side effect as we cannot inspect more deeply. diff --git a/noir/noir-repo/compiler/noirc_evaluator/src/ssa/opt/remove_if_else.rs b/noir/noir-repo/compiler/noirc_evaluator/src/ssa/opt/remove_if_else.rs index 6ca7eb74e9d7..dd497645cac2 100644 --- a/noir/noir-repo/compiler/noirc_evaluator/src/ssa/opt/remove_if_else.rs +++ b/noir/noir-repo/compiler/noirc_evaluator/src/ssa/opt/remove_if_else.rs @@ -233,6 +233,7 @@ fn slice_capacity_change( | Intrinsic::FromField | Intrinsic::AsField | Intrinsic::AsWitness - | Intrinsic::IsUnconstrained => SizeChange::None, + | Intrinsic::IsUnconstrained + | Intrinsic::AssertUnconstrained => SizeChange::None, } } diff --git a/noir/noir-repo/compiler/noirc_evaluator/src/ssa/opt/resolve_is_unconstrained.rs b/noir/noir-repo/compiler/noirc_evaluator/src/ssa/opt/resolve_runtime_checks.rs similarity index 54% rename from noir/noir-repo/compiler/noirc_evaluator/src/ssa/opt/resolve_is_unconstrained.rs rename to noir/noir-repo/compiler/noirc_evaluator/src/ssa/opt/resolve_runtime_checks.rs index 2c9e33ae528a..ce8676ee5fd4 100644 --- a/noir/noir-repo/compiler/noirc_evaluator/src/ssa/opt/resolve_is_unconstrained.rs +++ b/noir/noir-repo/compiler/noirc_evaluator/src/ssa/opt/resolve_runtime_checks.rs @@ -1,11 +1,14 @@ -use crate::ssa::{ - ir::{ - function::{Function, RuntimeType}, - instruction::{Instruction, Intrinsic}, - types::Type, - value::Value, +use crate::{ + errors::RuntimeError, + ssa::{ + ir::{ + function::{Function, RuntimeType}, + instruction::{Instruction, Intrinsic}, + types::Type, + value::Value, + }, + ssa_gen::Ssa, }, - ssa_gen::Ssa, }; use acvm::FieldElement; use fxhash::FxHashSet as HashSet; @@ -15,11 +18,12 @@ impl Ssa { /// with the resolved boolean value. /// Note that this pass must run after the pass that does runtime separation, since in SSA generation an ACIR function can end up targeting brillig. #[tracing::instrument(level = "trace", skip(self))] - pub(crate) fn resolve_is_unconstrained(mut self) -> Self { + pub(crate) fn resolve_runtime_checks(mut self) -> Result { for func in self.functions.values_mut() { replace_is_unconstrained_result(func); + assert_unconstrained_calls(func)?; } - self + Ok(self) } } @@ -54,3 +58,33 @@ fn replace_is_unconstrained_result(func: &mut Function) { func.dfg.set_value_from_id(original_return_id, is_within_unconstrained); } } + +fn assert_unconstrained_calls(func: &mut Function) -> Result<(), RuntimeError> { + let is_within_unconstrained = matches!(func.runtime(), RuntimeType::Brillig); + for block_id in func.reachable_blocks() { + let instructions = func.dfg[block_id].take_instructions(); + let mut filtered_instructions = Vec::with_capacity(instructions.len()); + + for instruction_id in instructions { + let target_func = match &func.dfg[instruction_id] { + Instruction::Call { func, .. } => *func, + _ => { + filtered_instructions.push(instruction_id); + continue; + } + }; + if let Value::Intrinsic(Intrinsic::AssertUnconstrained) = &func.dfg[target_func] { + if !is_within_unconstrained { + return Err(RuntimeError::OnlyWithinUnconstrained { + call_stack: func.dfg.get_call_stack(instruction_id), + }); + } + } else { + filtered_instructions.push(instruction_id); + } + } + + *func.dfg[block_id].instructions_mut() = filtered_instructions; + } + Ok(()) +} diff --git a/noir/noir-repo/compiler/noirc_frontend/src/ast/mod.rs b/noir/noir-repo/compiler/noirc_frontend/src/ast/mod.rs index 090a41fa7d95..a22223c4289a 100644 --- a/noir/noir-repo/compiler/noirc_frontend/src/ast/mod.rs +++ b/noir/noir-repo/compiler/noirc_frontend/src/ast/mod.rs @@ -119,7 +119,9 @@ pub enum UnresolvedTypeData { // The type of quoted code for metaprogramming Code, - Unspecified, // This is for when the user declares a variable without specifying it's type + Unspecified, // This is for when the user declares a variable without specifying it's type, + + Unconstrained(Box), Error, } @@ -216,6 +218,9 @@ impl std::fmt::Display for UnresolvedTypeData { Error => write!(f, "error"), Unspecified => write!(f, "unspecified"), Parenthesized(typ) => write!(f, "({typ})"), + Unconstrained(arg) => { + write!(f, "Unconstrained<{}>", arg) + } } } } diff --git a/noir/noir-repo/compiler/noirc_frontend/src/elaborator/types.rs b/noir/noir-repo/compiler/noirc_frontend/src/elaborator/types.rs index 059ff857df81..7c0b2ea734b8 100644 --- a/noir/noir-repo/compiler/noirc_frontend/src/elaborator/types.rs +++ b/noir/noir-repo/compiler/noirc_frontend/src/elaborator/types.rs @@ -26,10 +26,11 @@ use crate::{ traits::TraitConstraint, }, macros_api::{ - HirExpression, HirLiteral, HirStatement, Path, PathKind, SecondaryAttribute, Signedness, - UnaryOp, UnresolvedType, UnresolvedTypeData, + HirExpression, HirLiteral, HirStatement, Ident, Path, PathKind, SecondaryAttribute, + Signedness, UnaryOp, UnresolvedType, UnresolvedTypeData, }, node_interner::{DefinitionKind, ExprId, GlobalId, TraitId, TraitImplKind, TraitMethodId}, + token::{Keyword, Token}, Generics, Type, TypeBinding, TypeVariable, TypeVariableKind, }; @@ -118,6 +119,14 @@ impl<'context> Elaborator<'context> { Type::MutableReference(Box::new(self.resolve_type_inner(*element, new_variables))) } Parenthesized(typ) => self.resolve_type_inner(*typ, new_variables), + Unconstrained(arg) => self.resolve_named_type( + Path::from_ident(Ident::from_token( + Token::Ident(Keyword::UnconstrainedType.to_string()), + arg.span.unwrap_or_default(), + )), + vec![*arg], + new_variables, + ), }; if let Type::Struct(_, _) = resolved_type { diff --git a/noir/noir-repo/compiler/noirc_frontend/src/hir/resolution/resolver.rs b/noir/noir-repo/compiler/noirc_frontend/src/hir/resolution/resolver.rs index fd77312e4f2a..0a96286cc714 100644 --- a/noir/noir-repo/compiler/noirc_frontend/src/hir/resolution/resolver.rs +++ b/noir/noir-repo/compiler/noirc_frontend/src/hir/resolution/resolver.rs @@ -22,7 +22,7 @@ use crate::hir_def::expr::{ use crate::hir_def::traits::{Trait, TraitConstraint}; use crate::macros_api::SecondaryAttribute; -use crate::token::{Attributes, FunctionAttribute}; +use crate::token::{Attributes, FunctionAttribute, Keyword, Token}; use regex::Regex; use std::collections::{BTreeMap, BTreeSet, HashSet}; use std::rc::Rc; @@ -603,6 +603,14 @@ impl<'a> Resolver<'a> { Type::MutableReference(Box::new(self.resolve_type_inner(*element, new_variables))) } Parenthesized(typ) => self.resolve_type_inner(*typ, new_variables), + Unconstrained(arg) => self.resolve_named_type( + Path::from_ident(Ident::from_token( + Token::Ident(Keyword::UnconstrainedType.to_string()), + arg.span.unwrap_or_default(), + )), + vec![*arg], + new_variables, + ), }; if let Type::Struct(_, _) = resolved_type { diff --git a/noir/noir-repo/compiler/noirc_frontend/src/hir_def/types.rs b/noir/noir-repo/compiler/noirc_frontend/src/hir_def/types.rs index cf9aafbb308c..09ec786fbb8a 100644 --- a/noir/noir-repo/compiler/noirc_frontend/src/hir_def/types.rs +++ b/noir/noir-repo/compiler/noirc_frontend/src/hir_def/types.rs @@ -9,6 +9,7 @@ use crate::{ ast::IntegerBitSize, hir::type_check::TypeCheckError, node_interner::{ExprId, NodeInterner, TraitId, TypeAliasId}, + token::Keyword, }; use iter_extended::vecmap; use noirc_errors::{Location, Span}; @@ -19,7 +20,10 @@ use crate::{ node_interner::StructId, }; -use super::expr::{HirCallExpression, HirExpression, HirIdent}; +use super::{ + expr::{HirBlockExpression, HirCallExpression, HirExpression, HirIdent}, + stmt::HirStatement, +}; #[derive(PartialEq, Eq, Clone, Hash)] pub enum Type { @@ -1342,7 +1346,9 @@ impl Type { let mut bindings = TypeBindings::new(); if let Err(UnificationError) = self.try_unify(expected, &mut bindings) { - if !self.try_array_to_slice_coercion(expected, expression, interner) { + if !self.try_array_to_slice_coercion(expected, expression, interner) + && !self.try_unconstrained_wrapper_coercion(expected, expression, interner) + { errors.push(make_error()); } } else { @@ -1350,6 +1356,32 @@ impl Type { } } + /// Try to apply the unconstrained wrapper coercion to this given type pair and expression. + /// If self can be converted to target this way, do so and return true to indicate success. + fn try_unconstrained_wrapper_coercion( + &self, + target: &Type, + expression: ExprId, + interner: &mut NodeInterner, + ) -> bool { + let this = self.follow_bindings(); + let target = target.follow_bindings(); + + if let Type::Struct(typ, generics) = &target { + // Still have to ensure the element types match. + // Don't need to issue an error here if not, it will be done in unify_with_coercions + let mut bindings = TypeBindings::new(); + if typ.borrow().name.0.contents == Keyword::UnconstrainedType.to_string() + && this.try_unify(&generics[0], &mut bindings).is_ok() + { + wrap_unconstrained_return(expression, this, typ.borrow().id, &target, interner); + Self::apply_type_bindings(bindings.clone()); + return true; + } + } + false + } + /// Try to apply the array to slice coercion to this given type pair and expression. /// If self can be converted to target this way, do so and return true to indicate success. fn try_array_to_slice_coercion( @@ -1768,7 +1800,7 @@ fn convert_array_expression_to_slice( let func = interner.push_expr(as_slice); // Copy the expression and give it a new ExprId. The old one - // will be mutated in place into a Call expression. + // will be mutated in place into a Block with a statement pointing to a Call expression. let argument = interner.expression(&expression); let argument = interner.push_expr(argument); interner.push_expr_type(argument, array_type.clone()); @@ -1776,7 +1808,14 @@ fn convert_array_expression_to_slice( let arguments = vec![argument]; let call = HirExpression::Call(HirCallExpression { func, arguments, location }); - interner.replace_expr(&expression, call); + let call_id = interner.push_expr(call); + interner.push_expr_location(call_id, location.span, location.file); + interner.push_expr_type(call_id, target_type.clone()); + let call_stmt_id = interner.push_stmt(HirStatement::Expression(call_id)); + interner.replace_expr( + &expression, + HirExpression::Block(HirBlockExpression { statements: vec![call_stmt_id] }), + ); interner.push_expr_location(func, location.span, location.file); interner.push_expr_type(expression, target_type.clone()); @@ -1785,6 +1824,68 @@ fn convert_array_expression_to_slice( interner.push_expr_type(func, func_type); } +/// Wraps a given `expression` in `Unconstrained::new()` +fn wrap_unconstrained_return( + expression: ExprId, + original_type: Type, + unconstrained_wrapper_id: StructId, + unconstrained_wrapper_type: &Type, + interner: &mut NodeInterner, +) { + let new_method_id = interner + .lookup_method(unconstrained_wrapper_type, unconstrained_wrapper_id, "new", true) + .expect("Expected 'Unconstrained::new' method to be present in Noir's stdlib"); + + let modifiers = interner.function_modifiers(&new_method_id).clone(); + let module_id = interner.function_module(new_method_id); + let location = interner.expr_location(&expression); + + let new_function_def_id = + interner.push_function_definition(new_method_id, modifiers, module_id, location); + let new_unconstrained_wrapper = + HirExpression::Ident(HirIdent::non_trait_method(new_function_def_id, location), None); + let func_id = interner.push_expr(new_unconstrained_wrapper); + + let meta = interner.function_meta(&new_method_id).clone(); + let (func_type, bindings) = meta.typ.instantiate_with(vec![original_type.clone()], interner, 0); + let instantiated_func_type = func_type.follow_bindings(); + interner.push_expr_type(func_id, instantiated_func_type); + match meta.typ { + Type::Forall(_, _) => { + for (id, (var, _)) in bindings.clone() { + var.unbind(id); + } + } + _ => { + unreachable!("Unconstrained::new function must be a forall type"); + } + } + interner.store_instantiation_bindings(func_id, bindings.clone()); + + // Copy the expression and give it a new ExprId. The old one + // will be mutated in place into a Block with a statement pointing to a Call expression. + let location = interner.expr_location(&expression); + let argument = interner.expression(&expression); + let argument = interner.push_expr(argument); + interner.push_expr_type(argument, original_type.clone()); + interner.push_expr_location(argument, location.span, location.file); + + let arguments = vec![argument]; + let call = HirExpression::Call(HirCallExpression { func: func_id, arguments, location }); + + let call_id = interner.push_expr(call); + interner.push_expr_location(call_id, location.span, location.file); + interner.push_expr_type(call_id, unconstrained_wrapper_type.clone()); + interner.store_instantiation_bindings(call_id, bindings.clone()); + let call_stmt_id = interner.push_stmt(HirStatement::Expression(call_id)); + interner.replace_expr( + &expression, + HirExpression::Block(HirBlockExpression { statements: vec![call_stmt_id] }), + ); + interner.push_expr_type(expression, unconstrained_wrapper_type.clone()); + interner.store_instantiation_bindings(expression, bindings.clone()); +} + impl BinaryTypeOperator { /// Return the actual rust numeric function associated with this operator pub fn function(self) -> fn(u64, u64) -> u64 { diff --git a/noir/noir-repo/compiler/noirc_frontend/src/lexer/token.rs b/noir/noir-repo/compiler/noirc_frontend/src/lexer/token.rs index fdda271e79ca..ec06304456d7 100644 --- a/noir/noir-repo/compiler/noirc_frontend/src/lexer/token.rs +++ b/noir/noir-repo/compiler/noirc_frontend/src/lexer/token.rs @@ -856,6 +856,7 @@ pub enum Keyword { Type, Unchecked, Unconstrained, + UnconstrainedType, Use, Where, While, @@ -901,6 +902,7 @@ impl fmt::Display for Keyword { Keyword::Type => write!(f, "type"), Keyword::Unchecked => write!(f, "unchecked"), Keyword::Unconstrained => write!(f, "unconstrained"), + Keyword::UnconstrainedType => write!(f, "Unconstrained"), Keyword::Use => write!(f, "use"), Keyword::Where => write!(f, "where"), Keyword::While => write!(f, "while"), @@ -949,6 +951,7 @@ impl Keyword { "type" => Keyword::Type, "unchecked" => Keyword::Unchecked, "unconstrained" => Keyword::Unconstrained, + "Unconstrained" => Keyword::UnconstrainedType, "use" => Keyword::Use, "where" => Keyword::Where, "while" => Keyword::While, diff --git a/noir/noir-repo/compiler/noirc_frontend/src/parser/errors.rs b/noir/noir-repo/compiler/noirc_frontend/src/parser/errors.rs index 9f9a82009541..262f1bb4120e 100644 --- a/noir/noir-repo/compiler/noirc_frontend/src/parser/errors.rs +++ b/noir/noir-repo/compiler/noirc_frontend/src/parser/errors.rs @@ -46,6 +46,8 @@ pub enum ParserErrorReason { InvalidBitSize(u32), #[error("{0}")] Lexer(LexerErrorKind), + #[error("Unconstrained functions return values must be wrapped in Unconstrained")] + UnconstrainedReturnType, } /// Represents a parsing error, or a parsing error in the making. diff --git a/noir/noir-repo/compiler/noirc_frontend/src/parser/parser.rs b/noir/noir-repo/compiler/noirc_frontend/src/parser/parser.rs index 890ab795e00f..bb811b35f835 100644 --- a/noir/noir-repo/compiler/noirc_frontend/src/parser/parser.rs +++ b/noir/noir-repo/compiler/noirc_frontend/src/parser/parser.rs @@ -685,6 +685,7 @@ fn parse_type_inner<'a>( bool_type(), string_type(), format_string_type(recursive_type_parser.clone()), + unconstrained_type(recursive_type_parser.clone()), named_type(recursive_type_parser.clone()), named_trait(recursive_type_parser.clone()), slice_type(recursive_type_parser.clone()), @@ -791,6 +792,19 @@ fn named_type<'a>( }) } +fn unconstrained_type<'a>( + type_parser: impl NoirParser + 'a, +) -> impl NoirParser + 'a { + keyword(Keyword::UnconstrainedType).then(generic_type_args(type_parser)).validate( + |(_, args), span, emit| { + if args.len() != 1 { + emit(ParserError::with_reason(ParserErrorReason::UnconstrainedReturnType, span)); + } + UnresolvedTypeData::Unconstrained(Box::new(args[0].clone())).with_span(span) + }, + ) +} + fn named_trait<'a>( type_parser: impl NoirParser + 'a, ) -> impl NoirParser + 'a { diff --git a/noir/noir-repo/compiler/noirc_frontend/src/parser/parser/function.rs b/noir/noir-repo/compiler/noirc_frontend/src/parser/parser/function.rs index 40180a9f9ac5..199ad85bb8c6 100644 --- a/noir/noir-repo/compiler/noirc_frontend/src/parser/parser/function.rs +++ b/noir/noir-repo/compiler/noirc_frontend/src/parser/parser/function.rs @@ -4,6 +4,7 @@ use super::{ parameter_name_recovery, parameter_recovery, parenthesized, parse_type, pattern, self_parameter, where_clause, NoirParser, }; +use crate::macros_api::UnresolvedTypeData; use crate::parser::labels::ParsingRuleLabel; use crate::parser::spanned; use crate::token::{Keyword, Token}; @@ -34,6 +35,19 @@ pub(super) fn function_definition(allow_self: bool) -> impl NoirParser impl NoirParser { } pub(super) fn ident() -> impl NoirParser { - token_kind(TokenKind::Ident).map_with_span(Ident::from_token) + keyword(Keyword::UnconstrainedType) + .map(|_| Token::Ident(Keyword::UnconstrainedType.to_string())) + .or(token_kind(TokenKind::Ident)) + .map_with_span(Ident::from_token) } // Right-shift (>>) is issued as two separate > tokens by the lexer as this makes it easier diff --git a/noir/noir-repo/noir_stdlib/src/array.nr b/noir/noir-repo/noir_stdlib/src/array.nr index 8a8a1fad01c5..e4bcd312209d 100644 --- a/noir/noir-repo/noir_stdlib/src/array.nr +++ b/noir/noir-repo/noir_stdlib/src/array.nr @@ -1,4 +1,5 @@ use crate::cmp::Ord; +use crate::runtime::Unconstrained; // TODO: Once we fully move to the new SSA pass this module can be removed and replaced // by the methods in the `slice` module @@ -11,13 +12,20 @@ impl [T; N] { } pub fn sort_via(self, ordering: fn[Env](T, T) -> bool) -> Self { - let sorted_index = self.get_sorting_index(ordering); - let mut result = self; - // Ensure the indexes are correct - for i in 0..N { - let pos = find_index(sorted_index, i); - assert(sorted_index[pos] == i); + let sorted_index = self.get_sorting_index(ordering).make_constrained( + | sorted_index |{ + + // Ensure the indexes are correct + for i in 0..N { + find_index(sorted_index, i).make_constrained(| pos | { + assert(sorted_index[pos] == i); + }); + } + sorted_index } + ); + let mut result = self; + // Sort the array using the indexes for i in 0..N { result[i] = self[sorted_index[i]]; @@ -31,7 +39,7 @@ impl [T; N] { } /// Returns the index of the elements in the array that would sort it, using the provided custom sorting function. - unconstrained fn get_sorting_index(self, ordering: fn[Env](T, T) -> bool) -> [u64; N] { + unconstrained fn get_sorting_index(self, ordering: fn[Env](T, T) -> bool) -> Unconstrained<[u64; N]> { let mut result = [0; N]; let mut a = self; for i in 0..N { @@ -110,7 +118,7 @@ impl [T; N] { // helper function used to look up the position of a value in an array of Field // Note that function returns 0 if the value is not found -unconstrained fn find_index(a: [u64; N], find: u64) -> u64 { +unconstrained fn find_index(a: [u64; N], find: u64) -> Unconstrained { let mut result = 0; for i in 0..a.len() { if a[i] == find { diff --git a/noir/noir-repo/noir_stdlib/src/field/bn254.nr b/noir/noir-repo/noir_stdlib/src/field/bn254.nr index bcdc23f80dc4..86307b521e9a 100644 --- a/noir/noir-repo/noir_stdlib/src/field/bn254.nr +++ b/noir/noir-repo/noir_stdlib/src/field/bn254.nr @@ -1,4 +1,4 @@ -use crate::runtime::is_unconstrained; +use crate::runtime::{Unconstrained, is_unconstrained}; // The low and high decomposition of the field modulus global PLO: Field = 53438638232309528389504892708671455233; @@ -23,7 +23,7 @@ fn compute_decomposition(x: Field) -> (Field, Field) { (low, high) } -unconstrained fn decompose_hint(x: Field) -> (Field, Field) { +unconstrained fn decompose_hint(x: Field) -> Unconstrained<(Field, Field)> { compute_decomposition(x) } @@ -54,11 +54,11 @@ fn compute_lte(x: Field, y: Field, num_bytes: u32) -> bool { } } -unconstrained fn lt_32_hint(x: Field, y: Field) -> bool { +unconstrained fn lt_32_hint(x: Field, y: Field) -> Unconstrained { compute_lt(x, y, 32) } -unconstrained fn lte_16_hint(x: Field, y: Field) -> bool { +unconstrained fn lte_16_hint(x: Field, y: Field) -> Unconstrained { compute_lte(x, y, 16) } @@ -66,13 +66,15 @@ unconstrained fn lte_16_hint(x: Field, y: Field) -> bool { fn assert_gt_limbs(a: (Field, Field), b: (Field, Field)) { let (alo, ahi) = a; let (blo, bhi) = b; - let borrow = lte_16_hint(alo, blo); + lte_16_hint(alo, blo).make_constrained( + | borrow: bool | { + let rlo = alo - blo - 1 + (borrow as Field) * TWO_POW_128; + let rhi = ahi - bhi - (borrow as Field); - let rlo = alo - blo - 1 + (borrow as Field) * TWO_POW_128; - let rhi = ahi - bhi - (borrow as Field); - - rlo.assert_max_bit_size(128); - rhi.assert_max_bit_size(128); + rlo.assert_max_bit_size(128); + rhi.assert_max_bit_size(128); + } + ) } /// Decompose a single field into two 16 byte fields. @@ -81,18 +83,20 @@ pub fn decompose(x: Field) -> (Field, Field) { compute_decomposition(x) } else { // Take hints of the decomposition - let (xlo, xhi) = decompose_hint(x); - - // Range check the limbs - xlo.assert_max_bit_size(128); - xhi.assert_max_bit_size(128); - - // Check that the decomposition is correct - assert_eq(x, xlo + TWO_POW_128 * xhi); - - // Assert that the decomposition of P is greater than the decomposition of x - assert_gt_limbs((PLO, PHI), (xlo, xhi)); - (xlo, xhi) + decompose_hint(x).make_constrained( + | (xlo, xhi): (Field, Field) | { + // Range check the limbs + xlo.assert_max_bit_size(128); + xhi.assert_max_bit_size(128); + + // Check that the decomposition is correct + assert_eq(x, xlo + TWO_POW_128 * xhi); + + // Assert that the decomposition of P is greater than the decomposition of x + assert_gt_limbs((PLO, PHI), (xlo, xhi)); + (xlo, xhi) + } + ) } } @@ -118,15 +122,19 @@ pub fn gt(a: Field, b: Field) -> bool { compute_lt(b, a, 32) } else if a == b { false - } else { + } else { // Take a hint of the comparison and verify it - if lt_32_hint(a, b) { - assert_gt(b, a); - false - } else { - assert_gt(a, b); - true - } + lt_32_hint(a, b).make_constrained( + | lt | { + if lt { + assert_gt(b, a); + false + } else { + assert_gt(a, b); + true + } + } + ) } } diff --git a/noir/noir-repo/noir_stdlib/src/prelude.nr b/noir/noir-repo/noir_stdlib/src/prelude.nr index 3244329aa4b4..b459f543fe6f 100644 --- a/noir/noir-repo/noir_stdlib/src/prelude.nr +++ b/noir/noir-repo/noir_stdlib/src/prelude.nr @@ -6,3 +6,4 @@ use crate::uint128::U128; use crate::cmp::{Eq, Ord}; use crate::default::Default; use crate::convert::{From, Into}; +use crate::runtime::Unconstrained; diff --git a/noir/noir-repo/noir_stdlib/src/runtime.nr b/noir/noir-repo/noir_stdlib/src/runtime.nr index c075107cd52b..7c67b045aa5d 100644 --- a/noir/noir-repo/noir_stdlib/src/runtime.nr +++ b/noir/noir-repo/noir_stdlib/src/runtime.nr @@ -1,2 +1,24 @@ #[builtin(is_unconstrained)] pub fn is_unconstrained() -> bool {} + +#[builtin(assert_unconstrained)] +pub fn assert_unconstrained() {} + +struct Unconstrained { + _value: T, +} + +impl Unconstrained { + fn new(value: T) -> Self { + Self { _value: value } + } + + fn make_constrained(self, constrainer: fn[Env](T) -> P) -> P { + constrainer(self._value) + } + + fn unwrap(self) -> T { + assert_unconstrained(); + self._value + } +} diff --git a/noir/noir-repo/noir_stdlib/src/test.nr b/noir/noir-repo/noir_stdlib/src/test.nr index e6a7e03fefcf..2285a91f6b0a 100644 --- a/noir/noir-repo/noir_stdlib/src/test.nr +++ b/noir/noir-repo/noir_stdlib/src/test.nr @@ -1,50 +1,76 @@ +use crate::runtime::Unconstrained; + #[oracle(create_mock)] -unconstrained fn create_mock_oracle(name: str) -> Field {} +fn create_mock_oracle(name: str) -> P {} #[oracle(set_mock_params)] -unconstrained fn set_mock_params_oracle

(id: Field, params: P) {} +fn set_mock_params_oracle

(id: Field, params: P) {} #[oracle(get_mock_last_params)] -unconstrained fn get_mock_last_params_oracle

(id: Field) -> P {} +fn get_mock_last_params_oracle

(id: Field) -> P {} #[oracle(set_mock_returns)] -unconstrained fn set_mock_returns_oracle(id: Field, returns: R) {} +fn set_mock_returns_oracle(id: Field, returns: R) {} #[oracle(set_mock_times)] -unconstrained fn set_mock_times_oracle(id: Field, times: u64) {} +fn set_mock_times_oracle(id: Field, times: u64) {} #[oracle(clear_mock)] -unconstrained fn clear_mock_oracle(id: Field) {} +fn clear_mock_oracle(id: Field) {} + +unconstrained fn create_mock_oracle_inner(name: str) -> Unconstrained

{ + create_mock_oracle(name) +} + +unconstrained fn set_mock_params_oracle_inner

(id: Field, params: P) { + set_mock_params_oracle(id, params) +} + +unconstrained fn get_mock_last_params_oracle_inner

(id: Field) -> Unconstrained

{ + get_mock_last_params_oracle(id) +} + +unconstrained fn set_mock_returns_oracle_inner(id: Field, returns: R) { + set_mock_returns_oracle(id, returns) +} + +unconstrained fn set_mock_times_oracle_inner(id: Field, times: u64) { + set_mock_times_oracle(id, times) +} + +unconstrained fn clear_mock_oracle_inner(id: Field) { + clear_mock_oracle(id) +} struct OracleMock { id: Field, } impl OracleMock { - unconstrained pub fn mock(name: str) -> Self { - Self { id: create_mock_oracle(name) } + pub fn mock(name: str) -> Self { + Self { id: create_mock_oracle_inner(name).make_constrained(| x | x) } } - unconstrained pub fn with_params

(self, params: P) -> Self { - set_mock_params_oracle(self.id, params); + pub fn with_params

(self, params: P) -> Self { + set_mock_params_oracle_inner(self.id, params); self } - unconstrained pub fn get_last_params

(self) -> P { - get_mock_last_params_oracle(self.id) + pub fn get_last_params

(self) -> P { + get_mock_last_params_oracle_inner(self.id).make_constrained(| x | x) } - unconstrained pub fn returns(self, returns: R) -> Self { - set_mock_returns_oracle(self.id, returns); + pub fn returns(self, returns: R) -> Self { + set_mock_returns_oracle_inner(self.id, returns); self } - unconstrained pub fn times(self, times: u64) -> Self { - set_mock_times_oracle(self.id, times); + pub fn times(self, times: u64) -> Self { + set_mock_times_oracle_inner(self.id, times); self } - unconstrained pub fn clear(self) { - clear_mock_oracle(self.id); + pub fn clear(self) { + clear_mock_oracle_inner(self.id); } } diff --git a/noir/noir-repo/noir_stdlib/src/uint128.nr b/noir/noir-repo/noir_stdlib/src/uint128.nr index 173fa54863aa..334bee65339d 100644 --- a/noir/noir-repo/noir_stdlib/src/uint128.nr +++ b/noir/noir-repo/noir_stdlib/src/uint128.nr @@ -1,6 +1,7 @@ use crate::ops::{Add, Sub, Mul, Div, Rem, Not, BitOr, BitAnd, BitXor, Shl, Shr}; use crate::cmp::{Eq, Ord, Ordering}; use crate::println; +use crate::runtime::Unconstrained; global pow64 : Field = 18446744073709551616; //2^64; global pow63 : Field = 9223372036854775808; // 2^63; @@ -95,7 +96,7 @@ impl U128 { U128 { lo: lo as Field, hi: hi as Field } } - unconstrained fn uconstrained_check_is_upper_ascii(ascii: u8) -> bool { + unconstrained fn uconstrained_check_is_upper_ascii(ascii: u8) -> Unconstrained { ((ascii >= 65) & (ascii <= 90)) // Between 'A' and 'Z' } @@ -103,17 +104,21 @@ impl U128 { if ascii < 58 { ascii - 48 } else { - let ascii = ascii + 32 * (U128::uconstrained_check_is_upper_ascii(ascii) as u8); - assert(ascii >= 97); // enforce >= 'a' - assert(ascii <= 102); // enforce <= 'f' - ascii - 87 + U128::uconstrained_check_is_upper_ascii(ascii).make_constrained( + | check: bool | { + let ascii = ascii + 32 * check as u8; + assert(ascii >= 97); // enforce >= 'a' + assert(ascii <= 102); // enforce <= 'f' + ascii - 87 + } + ) } as Field } // TODO: Replace with a faster version. // A circuit that uses this function can be slow to compute // (we're doing up to 127 calls to compute the quotient) - unconstrained fn unconstrained_div(self: Self, b: U128) -> (U128, U128) { + unconstrained fn unconstrained_div(self: Self, b: U128) -> Unconstrained<(U128, U128)> { if b == U128::zero() { // Return 0,0 to avoid eternal loop (U128::zero(), U128::zero()) @@ -126,7 +131,7 @@ impl U128 { // The result of multiplication by 2 would overflow (U128::zero(), self) } else { - self.unconstrained_div(b * U128::from_u64s_le(2, 0)) + self.unconstrained_div(b * U128::from_u64s_le(2, 0)).unwrap() }; let q_mul_2 = q * U128::from_u64s_le(2, 0); if r < b { @@ -212,21 +217,23 @@ impl Mul for U128 { impl Div for U128 { fn div(self: Self, b: U128) -> U128 { - let (q,r) = self.unconstrained_div(b); - let a = b * q + r; - assert_eq(self, a); - assert(r < b); - q + self.unconstrained_div(b).make_constrained(|(q,r): (U128, U128)| { + let a = b * q + r; + assert_eq(self, a); + assert(r < b); + q + }) } } impl Rem for U128 { fn rem(self: Self, b: U128) -> U128 { - let (q,r) = self.unconstrained_div(b); - let a = b * q + r; - assert_eq(self, a); - assert(r < b); - r + self.unconstrained_div(b).make_constrained(|(q,r): (U128, U128)| { + let a = b * q + r; + assert_eq(self, a); + assert(r < b); + r + }) } } @@ -440,31 +447,45 @@ mod tests { let b= U128::from_u64s_le(0x0, 0xfffffffffffffffe); let c= U128::one(); let d= U128::from_u64s_le(0x0, 0x1); - let (q,r) = a.unconstrained_div(b); - assert_eq(q, c); - assert_eq(r, d); + a.unconstrained_div(b).make_constrained( + | (q, r): (U128, U128) | { + assert_eq(q, c); + assert_eq(r, d); + } + ); let a = U128::from_u64s_le(2, 0); let b = U128::one(); // Check the case where a is a multiple of b - let (c,d ) = a.unconstrained_div(b); - assert_eq((c, d), (a, U128::zero())); + a.unconstrained_div(b).make_constrained( + | (c, d): (U128, U128) | { + assert_eq((c, d), (a, U128::zero())); + } + ); // Check where b is a multiple of a - let (c,d) = b.unconstrained_div(a); - assert_eq((c, d), (U128::zero(), b)); + b.unconstrained_div(a).make_constrained( + | (c, d): (U128, U128) | { + assert_eq((c, d), (U128::zero(), b)); + } + ); // Dividing by zero returns 0,0 let a = U128::from_u64s_le(0x1, 0x0); let b = U128::zero(); - let (c,d)= a.unconstrained_div(b); - assert_eq((c, d), (U128::zero(), U128::zero())); - + a.unconstrained_div(b).make_constrained( + |(c,d): (U128, U128)| { + assert_eq((c, d), (U128::zero(), U128::zero())); + } + ); // Dividing 1<<127 by 1<<127 (special case) let a = U128::from_u64s_le(0x0, pow63 as u64); let b = U128::from_u64s_le(0x0, pow63 as u64); - let (c,d )= a.unconstrained_div(b); - assert_eq((c, d), (U128::one(), U128::zero())); + a.unconstrained_div(b).make_constrained( + |(c,d): (U128, U128)| { + assert_eq((c, d), (U128::one(), U128::zero())); + } + ); } #[test] diff --git a/noir/noir-repo/tooling/nargo_fmt/src/rewrite/typ.rs b/noir/noir-repo/tooling/nargo_fmt/src/rewrite/typ.rs index 278457f82d16..2cb36d422f8e 100644 --- a/noir/noir-repo/tooling/nargo_fmt/src/rewrite/typ.rs +++ b/noir/noir-repo/tooling/nargo_fmt/src/rewrite/typ.rs @@ -65,6 +65,7 @@ pub(crate) fn rewrite(visitor: &FmtVisitor, _shape: Shape, typ: UnresolvedType) | UnresolvedTypeData::String(_) | UnresolvedTypeData::FormatString(_, _) | UnresolvedTypeData::Code + | UnresolvedTypeData::Unconstrained(_) | UnresolvedTypeData::TraitAsType(_, _) => visitor.slice(typ.span.unwrap()).into(), UnresolvedTypeData::Error => unreachable!(), }