diff --git a/.noir-sync-commit b/.noir-sync-commit index f9331310741..d63cc1a5a5c 100644 --- a/.noir-sync-commit +++ b/.noir-sync-commit @@ -1 +1 @@ -f8fd813b09ce870364700659e3ea8499ab51105e +b473d99b2b70b595596b8392617256dbaf5d5642 diff --git a/noir/noir-repo/acvm-repo/acvm/src/compiler/mod.rs b/noir/noir-repo/acvm-repo/acvm/src/compiler/mod.rs index 92e03cc90c2..8829f77e50b 100644 --- a/noir/noir-repo/acvm-repo/acvm/src/compiler/mod.rs +++ b/noir/noir-repo/acvm-repo/acvm/src/compiler/mod.rs @@ -7,10 +7,12 @@ use acir::{ // The various passes that we can use over ACIR mod optimizers; +mod simulator; mod transformers; pub use optimizers::optimize; use optimizers::optimize_internal; +pub use simulator::CircuitSimulator; use transformers::transform_internal; pub use transformers::{transform, MIN_EXPRESSION_WIDTH}; diff --git a/noir/noir-repo/acvm-repo/acvm/src/compiler/optimizers/merge_expressions.rs b/noir/noir-repo/acvm-repo/acvm/src/compiler/optimizers/merge_expressions.rs index ddf86f60f77..7c0be0fc3fe 100644 --- a/noir/noir-repo/acvm-repo/acvm/src/compiler/optimizers/merge_expressions.rs +++ b/noir/noir-repo/acvm-repo/acvm/src/compiler/optimizers/merge_expressions.rs @@ -6,6 +6,8 @@ use acir::{ AcirField, }; +use crate::compiler::CircuitSimulator; + pub(crate) struct MergeExpressionsOptimizer { resolved_blocks: HashMap>, } @@ -76,7 +78,7 @@ impl MergeExpressionsOptimizer { modified_gates.insert(b, Opcode::AssertZero(expr)); to_keep = false; // Update the 'used_witness' map to account for the merge. - for w2 in Self::expr_wit(&expr_define) { + for w2 in CircuitSimulator::expr_wit(&expr_define) { if !circuit_inputs.contains(&w2) { let mut v = used_witness[&w2].clone(); v.insert(b); @@ -104,22 +106,15 @@ impl MergeExpressionsOptimizer { (new_circuit, new_acir_opcode_positions) } - fn expr_wit(expr: &Expression) -> BTreeSet { - let mut result = BTreeSet::new(); - result.extend(expr.mul_terms.iter().flat_map(|i| vec![i.1, i.2])); - result.extend(expr.linear_combinations.iter().map(|i| i.1)); - result - } - fn brillig_input_wit(&self, input: &BrilligInputs) -> BTreeSet { let mut result = BTreeSet::new(); match input { BrilligInputs::Single(expr) => { - result.extend(Self::expr_wit(expr)); + result.extend(CircuitSimulator::expr_wit(expr)); } BrilligInputs::Array(exprs) => { for expr in exprs { - result.extend(Self::expr_wit(expr)); + result.extend(CircuitSimulator::expr_wit(expr)); } } BrilligInputs::MemoryArray(block_id) => { @@ -134,16 +129,16 @@ impl MergeExpressionsOptimizer { fn witness_inputs(&self, opcode: &Opcode) -> BTreeSet { let mut witnesses = BTreeSet::new(); match opcode { - Opcode::AssertZero(expr) => Self::expr_wit(expr), + Opcode::AssertZero(expr) => CircuitSimulator::expr_wit(expr), Opcode::BlackBoxFuncCall(bb_func) => bb_func.get_input_witnesses(), - Opcode::Directive(Directive::ToLeRadix { a, .. }) => Self::expr_wit(a), + Opcode::Directive(Directive::ToLeRadix { a, .. }) => CircuitSimulator::expr_wit(a), Opcode::MemoryOp { block_id: _, op, predicate } => { //index et value, et predicate let mut witnesses = BTreeSet::new(); - witnesses.extend(Self::expr_wit(&op.index)); - witnesses.extend(Self::expr_wit(&op.value)); + witnesses.extend(CircuitSimulator::expr_wit(&op.index)); + witnesses.extend(CircuitSimulator::expr_wit(&op.value)); if let Some(p) = predicate { - witnesses.extend(Self::expr_wit(p)); + witnesses.extend(CircuitSimulator::expr_wit(p)); } witnesses } @@ -162,7 +157,7 @@ impl MergeExpressionsOptimizer { witnesses.insert(*i); } if let Some(p) = predicate { - witnesses.extend(Self::expr_wit(p)); + witnesses.extend(CircuitSimulator::expr_wit(p)); } witnesses } diff --git a/noir/noir-repo/acvm-repo/acvm/src/compiler/simulator.rs b/noir/noir-repo/acvm-repo/acvm/src/compiler/simulator.rs new file mode 100644 index 00000000000..d89b53aa564 --- /dev/null +++ b/noir/noir-repo/acvm-repo/acvm/src/compiler/simulator.rs @@ -0,0 +1,305 @@ +use acir::{ + circuit::{ + brillig::{BrilligInputs, BrilligOutputs}, + directives::Directive, + opcodes::{BlockId, FunctionInput}, + Circuit, Opcode, + }, + native_types::{Expression, Witness}, + AcirField, +}; +use std::collections::{BTreeSet, HashMap, HashSet}; + +#[derive(PartialEq)] +enum BlockStatus { + Initialized, + Used, +} + +/// Simulate a symbolic solve for a circuit +#[derive(Default)] +pub struct CircuitSimulator { + /// Track the witnesses that can be solved + solvable_witness: HashSet, + + /// Tells whether a Memory Block is: + /// - Not initialized if not in the map + /// - Initialized if its status is Initialized in the Map + /// - Used, indicating that the block cannot be written anymore. + resolved_blocks: HashMap, +} + +impl CircuitSimulator { + /// Simulate a symbolic solve for a circuit by keeping track of the witnesses that can be solved. + /// Returns false if the circuit cannot be solved + #[tracing::instrument(level = "trace", skip_all)] + pub fn check_circuit(&mut self, circuit: &Circuit) -> bool { + let circuit_inputs = circuit.circuit_arguments(); + self.solvable_witness.extend(circuit_inputs.iter()); + for op in &circuit.opcodes { + if !self.try_solve(op) { + return false; + } + } + true + } + + /// Check if the Opcode can be solved, and if yes, add the solved witness to set of solvable witness + fn try_solve(&mut self, opcode: &Opcode) -> bool { + let mut unresolved = HashSet::new(); + match opcode { + Opcode::AssertZero(expr) => { + for (_, w1, w2) in &expr.mul_terms { + if !self.solvable_witness.contains(w1) { + if !self.solvable_witness.contains(w2) { + return false; + } + unresolved.insert(*w1); + } + if !self.solvable_witness.contains(w2) && w1 != w2 { + unresolved.insert(*w2); + } + } + for (_, w) in &expr.linear_combinations { + if !self.solvable_witness.contains(w) { + unresolved.insert(*w); + } + } + if unresolved.len() == 1 { + self.mark_solvable(*unresolved.iter().next().unwrap()); + return true; + } + unresolved.is_empty() + } + Opcode::BlackBoxFuncCall(black_box_func_call) => { + let inputs = black_box_func_call.get_inputs_vec(); + for input in inputs { + if !self.can_solve_function_input(&input) { + return false; + } + } + let outputs = black_box_func_call.get_outputs_vec(); + for output in outputs { + self.mark_solvable(output); + } + true + } + Opcode::Directive(directive) => match directive { + Directive::ToLeRadix { a, b, .. } => { + if !self.can_solve_expression(a) { + return false; + } + for w in b { + self.mark_solvable(*w); + } + true + } + }, + Opcode::MemoryOp { block_id, op, predicate } => { + if !self.can_solve_expression(&op.index) { + return false; + } + if let Some(predicate) = predicate { + if !self.can_solve_expression(predicate) { + return false; + } + } + if op.operation.is_zero() { + let w = op.value.to_witness().unwrap(); + self.mark_solvable(w); + true + } else { + if let Some(BlockStatus::Used) = self.resolved_blocks.get(block_id) { + // Writing after having used the block should not be allowed + return false; + } + self.try_solve(&Opcode::AssertZero(op.value.clone())) + } + } + Opcode::MemoryInit { block_id, init, .. } => { + for w in init { + if !self.solvable_witness.contains(w) { + return false; + } + } + self.resolved_blocks.insert(*block_id, BlockStatus::Initialized); + true + } + Opcode::BrilligCall { id: _, inputs, outputs, predicate } => { + for input in inputs { + if !self.can_solve_brillig_input(input) { + return false; + } + } + if let Some(predicate) = predicate { + if !self.can_solve_expression(predicate) { + return false; + } + } + for output in outputs { + match output { + BrilligOutputs::Simple(w) => self.mark_solvable(*w), + BrilligOutputs::Array(arr) => { + for w in arr { + self.mark_solvable(*w); + } + } + } + } + true + } + Opcode::Call { id: _, inputs, outputs, predicate } => { + for w in inputs { + if !self.solvable_witness.contains(w) { + return false; + } + } + if let Some(predicate) = predicate { + if !self.can_solve_expression(predicate) { + return false; + } + } + for w in outputs { + self.mark_solvable(*w); + } + true + } + } + } + + /// Adds the witness to set of solvable witness + pub(crate) fn mark_solvable(&mut self, witness: Witness) { + self.solvable_witness.insert(witness); + } + + pub fn can_solve_function_input(&self, input: &FunctionInput) -> bool { + if !input.is_constant() { + return self.solvable_witness.contains(&input.to_witness()); + } + true + } + fn can_solve_expression(&self, expr: &Expression) -> bool { + for w in Self::expr_wit(expr) { + if !self.solvable_witness.contains(&w) { + return false; + } + } + true + } + fn can_solve_brillig_input(&mut self, input: &BrilligInputs) -> bool { + match input { + BrilligInputs::Single(expr) => self.can_solve_expression(expr), + BrilligInputs::Array(exprs) => { + for expr in exprs { + if !self.can_solve_expression(expr) { + return false; + } + } + true + } + + BrilligInputs::MemoryArray(block_id) => match self.resolved_blocks.entry(*block_id) { + std::collections::hash_map::Entry::Vacant(_) => false, + std::collections::hash_map::Entry::Occupied(entry) + if *entry.get() == BlockStatus::Used => + { + true + } + std::collections::hash_map::Entry::Occupied(mut entry) => { + entry.insert(BlockStatus::Used); + true + } + }, + } + } + + pub(crate) fn expr_wit(expr: &Expression) -> BTreeSet { + let mut result = BTreeSet::new(); + result.extend(expr.mul_terms.iter().flat_map(|i| vec![i.1, i.2])); + result.extend(expr.linear_combinations.iter().map(|i| i.1)); + result + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeSet; + + use crate::compiler::CircuitSimulator; + use acir::{ + acir_field::AcirField, + circuit::{Circuit, ExpressionWidth, Opcode, PublicInputs}, + native_types::{Expression, Witness}, + FieldElement, + }; + + fn test_circuit( + opcodes: Vec>, + private_parameters: BTreeSet, + public_parameters: PublicInputs, + ) -> Circuit { + Circuit { + current_witness_index: 1, + expression_width: ExpressionWidth::Bounded { width: 4 }, + opcodes, + private_parameters, + public_parameters, + return_values: PublicInputs::default(), + assert_messages: Default::default(), + recursive: false, + } + } + + #[test] + fn reports_true_for_empty_circuit() { + let empty_circuit = test_circuit(vec![], BTreeSet::default(), PublicInputs::default()); + + assert!(CircuitSimulator::default().check_circuit(&empty_circuit)); + } + + #[test] + fn reports_true_for_connected_circuit() { + let connected_circuit = test_circuit( + vec![Opcode::AssertZero(Expression { + mul_terms: Vec::new(), + linear_combinations: vec![ + (FieldElement::one(), Witness(1)), + (-FieldElement::one(), Witness(2)), + ], + q_c: FieldElement::zero(), + })], + BTreeSet::from([Witness(1)]), + PublicInputs::default(), + ); + + assert!(CircuitSimulator::default().check_circuit(&connected_circuit)); + } + + #[test] + fn reports_false_for_disconnected_circuit() { + let disconnected_circuit = test_circuit( + vec![ + Opcode::AssertZero(Expression { + mul_terms: Vec::new(), + linear_combinations: vec![ + (FieldElement::one(), Witness(1)), + (-FieldElement::one(), Witness(2)), + ], + q_c: FieldElement::zero(), + }), + Opcode::AssertZero(Expression { + mul_terms: Vec::new(), + linear_combinations: vec![ + (FieldElement::one(), Witness(3)), + (-FieldElement::one(), Witness(4)), + ], + q_c: FieldElement::zero(), + }), + ], + BTreeSet::from([Witness(1)]), + PublicInputs::default(), + ); + + assert!(!CircuitSimulator::default().check_circuit(&disconnected_circuit)); + } +} diff --git a/noir/noir-repo/compiler/noirc_frontend/src/elaborator/patterns.rs b/noir/noir-repo/compiler/noirc_frontend/src/elaborator/patterns.rs index 05e3bfac348..9e60adcbc6f 100644 --- a/noir/noir-repo/compiler/noirc_frontend/src/elaborator/patterns.rs +++ b/noir/noir-repo/compiler/noirc_frontend/src/elaborator/patterns.rs @@ -510,30 +510,6 @@ impl<'context> Elaborator<'context> { self.resolve_turbofish_generics(item_generic_kinds, turbofish_generics) } - pub(super) fn resolve_alias_turbofish_generics( - &mut self, - type_alias: &TypeAlias, - generics: Vec, - unresolved_turbofish: Option>, - span: Span, - ) -> Vec { - let Some(turbofish_generics) = unresolved_turbofish else { - return generics; - }; - - if turbofish_generics.len() != generics.len() { - self.push_err(TypeCheckError::GenericCountMismatch { - item: format!("alias {}", type_alias.name), - expected: generics.len(), - found: turbofish_generics.len(), - span, - }); - return generics; - } - - self.resolve_turbofish_generics(&type_alias.generics, turbofish_generics) - } - pub(super) fn resolve_turbofish_generics( &mut self, kinds: Vec, 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 3d4f3e77792..0eed79348e2 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 @@ -1358,23 +1358,24 @@ impl Type { TypeBinding::Unbound(_, ref type_var_kind) => type_var_kind.clone(), }, Type::InfixExpr(lhs, _op, rhs) => lhs.infix_kind(rhs), + Type::Alias(def, generics) => def.borrow().get_type(generics).kind(), + // This is a concrete FieldElement, not an IntegerOrField Type::FieldElement + | Type::Integer(..) | Type::Array(..) | Type::Slice(..) - | Type::Integer(..) | Type::Bool | Type::String(..) | Type::FmtString(..) | Type::Unit | Type::Tuple(..) | Type::Struct(..) - | Type::Alias(..) | Type::TraitAsType(..) | Type::Function(..) | Type::MutableReference(..) | Type::Forall(..) - | Type::Quoted(..) - | Type::Error => Kind::Normal, + | Type::Quoted(..) => Kind::Normal, + Type::Error => Kind::Any, } } diff --git a/noir/noir-repo/compiler/wasm/src/compile.rs b/noir/noir-repo/compiler/wasm/src/compile.rs index 05f42bc91a1..e823f90add5 100644 --- a/noir/noir-repo/compiler/wasm/src/compile.rs +++ b/noir/noir-repo/compiler/wasm/src/compile.rs @@ -180,6 +180,13 @@ pub fn compile_program( .0; let optimized_program = nargo::ops::transform_program(compiled_program, expression_width); + nargo::ops::check_program(&optimized_program).map_err(|errs| { + CompileError::with_file_diagnostics( + "Compiled program is not solvable", + errs, + &context.file_manager, + ) + })?; let warnings = optimized_program.warnings.clone(); Ok(JsCompileProgramResult::new(optimized_program.into(), warnings)) diff --git a/noir/noir-repo/compiler/wasm/src/compile_new.rs b/noir/noir-repo/compiler/wasm/src/compile_new.rs index ef2af1dd654..ac2f79147b3 100644 --- a/noir/noir-repo/compiler/wasm/src/compile_new.rs +++ b/noir/noir-repo/compiler/wasm/src/compile_new.rs @@ -118,6 +118,13 @@ impl CompilerContext { .0; let optimized_program = nargo::ops::transform_program(compiled_program, expression_width); + nargo::ops::check_program(&optimized_program).map_err(|errs| { + CompileError::with_file_diagnostics( + "Compiled program is not solvable", + errs, + &self.context.file_manager, + ) + })?; let warnings = optimized_program.warnings.clone(); Ok(JsCompileProgramResult::new(optimized_program.into(), warnings)) diff --git a/noir/noir-repo/test_programs/execution_success/bit_shifts_runtime/Prover.toml b/noir/noir-repo/test_programs/execution_success/bit_shifts_runtime/Prover.toml index 98d8630792e..67bf6a6a234 100644 --- a/noir/noir-repo/test_programs/execution_success/bit_shifts_runtime/Prover.toml +++ b/noir/noir-repo/test_programs/execution_success/bit_shifts_runtime/Prover.toml @@ -1,2 +1,2 @@ x = 64 -y = 1 \ No newline at end of file +y = 1 diff --git a/noir/noir-repo/tooling/nargo/src/ops/check.rs b/noir/noir-repo/tooling/nargo/src/ops/check.rs new file mode 100644 index 00000000000..14d629ab0f6 --- /dev/null +++ b/noir/noir-repo/tooling/nargo/src/ops/check.rs @@ -0,0 +1,21 @@ +use acvm::compiler::CircuitSimulator; +use noirc_driver::{CompiledProgram, ErrorsAndWarnings}; +use noirc_errors::{CustomDiagnostic, FileDiagnostic}; + +pub fn check_program(compiled_program: &CompiledProgram) -> Result<(), ErrorsAndWarnings> { + // Check if the program is solvable + for (i, circuit) in compiled_program.program.functions.iter().enumerate() { + let mut simulator = CircuitSimulator::default(); + if !simulator.check_circuit(circuit) { + let diag = FileDiagnostic { + file_id: fm::FileId::dummy(), + diagnostic: CustomDiagnostic::from_message(&format!( + "Circuit \"{}\" is not solvable", + compiled_program.names[i] + )), + }; + return Err(vec![diag]); + } + } + Ok(()) +} diff --git a/noir/noir-repo/tooling/nargo/src/ops/mod.rs b/noir/noir-repo/tooling/nargo/src/ops/mod.rs index 16680dab980..f70577a14f1 100644 --- a/noir/noir-repo/tooling/nargo/src/ops/mod.rs +++ b/noir/noir-repo/tooling/nargo/src/ops/mod.rs @@ -1,3 +1,4 @@ +pub use self::check::check_program; pub use self::compile::{ collect_errors, compile_contract, compile_program, compile_program_with_debug_instrumenter, compile_workspace, report_errors, @@ -9,6 +10,7 @@ pub use self::transform::{transform_contract, transform_program}; pub use self::test::{run_test, TestStatus}; +mod check; mod compile; mod execute; mod foreign_calls; diff --git a/noir/noir-repo/tooling/nargo_cli/src/cli/compile_cmd.rs b/noir/noir-repo/tooling/nargo_cli/src/cli/compile_cmd.rs index 18fb407d413..304988ed516 100644 --- a/noir/noir-repo/tooling/nargo_cli/src/cli/compile_cmd.rs +++ b/noir/noir-repo/tooling/nargo_cli/src/cli/compile_cmd.rs @@ -192,6 +192,7 @@ fn compile_programs( let target_width = get_target_width(package.expression_width, compile_options.expression_width); let program = nargo::ops::transform_program(program, target_width); + nargo::ops::check_program(&program)?; save_program_to_file(&program.into(), &package.name, workspace.target_directory_path()); Ok(((), warnings))