From 268229e8e1497472dc514baeb792985677963735 Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Fri, 10 Jan 2025 10:33:17 -0300 Subject: [PATCH 1/2] fix: let static_assert fail with the provided message (#7005) Co-authored-by: jfecher Co-authored-by: Tom French <15848336+TomAFrench@users.noreply.github.com> --- compiler/noirc_evaluator/src/errors.rs | 6 +++--- compiler/noirc_evaluator/src/ssa/ir/dfg.rs | 18 ++++++++++++++++ .../noirc_evaluator/src/ssa/ir/printer.rs | 21 +++++-------------- .../src/ssa/opt/assert_constant.rs | 6 +++++- noir_stdlib/src/lib.nr | 7 +++++++ 5 files changed, 38 insertions(+), 20 deletions(-) diff --git a/compiler/noirc_evaluator/src/errors.rs b/compiler/noirc_evaluator/src/errors.rs index 1e484f8af59..94c0e0554a4 100644 --- a/compiler/noirc_evaluator/src/errors.rs +++ b/compiler/noirc_evaluator/src/errors.rs @@ -49,8 +49,8 @@ pub enum RuntimeError { StaticAssertDynamicMessage { call_stack: CallStack }, #[error("Argument is dynamic")] StaticAssertDynamicPredicate { call_stack: CallStack }, - #[error("Argument is false")] - StaticAssertFailed { call_stack: CallStack }, + #[error("{message}")] + StaticAssertFailed { message: String, call_stack: CallStack }, #[error("Nested slices, i.e. slices within an array or slice, are not supported")] NestedSlice { call_stack: CallStack }, #[error("Big Integer modulus do no match")] @@ -165,7 +165,7 @@ impl RuntimeError { | RuntimeError::AssertConstantFailed { call_stack } | RuntimeError::StaticAssertDynamicMessage { call_stack } | RuntimeError::StaticAssertDynamicPredicate { call_stack } - | RuntimeError::StaticAssertFailed { call_stack } + | RuntimeError::StaticAssertFailed { call_stack, .. } | RuntimeError::IntegerOutOfBounds { call_stack, .. } | RuntimeError::UnsupportedIntegerSize { call_stack, .. } | RuntimeError::InvalidBlackBoxInputBitSize { call_stack, .. } diff --git a/compiler/noirc_evaluator/src/ssa/ir/dfg.rs b/compiler/noirc_evaluator/src/ssa/ir/dfg.rs index 8425e4d5e56..57e833af7eb 100644 --- a/compiler/noirc_evaluator/src/ssa/ir/dfg.rs +++ b/compiler/noirc_evaluator/src/ssa/ir/dfg.rs @@ -525,6 +525,24 @@ impl DataFlowGraph { } } + /// If this value points to an array of constant bytes, returns a string + /// consisting of those bytes if they form a valid UTF-8 string. + pub(crate) fn get_string(&self, value: ValueId) -> Option { + let (value_ids, _typ) = self.get_array_constant(value)?; + + let mut bytes = Vec::new(); + for value_id in value_ids { + let field_value = self.get_numeric_constant(value_id)?; + let u64_value = field_value.try_to_u64()?; + if u64_value > 255 { + return None; + }; + let byte = u64_value as u8; + bytes.push(byte); + } + String::from_utf8(bytes).ok() + } + /// A constant index less than the array length is safe pub(crate) fn is_safe_index(&self, index: ValueId, array: ValueId) -> bool { #[allow(clippy::match_like_matches_macro)] diff --git a/compiler/noirc_evaluator/src/ssa/ir/printer.rs b/compiler/noirc_evaluator/src/ssa/ir/printer.rs index 9bf26b8414d..d30fce170dc 100644 --- a/compiler/noirc_evaluator/src/ssa/ir/printer.rs +++ b/compiler/noirc_evaluator/src/ssa/ir/printer.rs @@ -274,22 +274,11 @@ pub(crate) fn try_to_extract_string_from_error_payload( values: &[ValueId], dfg: &DataFlowGraph, ) -> Option { - (is_string_type && (values.len() == 1)) - .then_some(()) - .and_then(|()| { - let (values, _) = &dfg.get_array_constant(values[0])?; - let values = values.iter().map(|value_id| dfg.get_numeric_constant(*value_id)); - values.collect::>>() - }) - .map(|fields| { - fields - .iter() - .map(|field| { - let as_u8 = field.try_to_u64().unwrap_or_default() as u8; - as_u8 as char - }) - .collect() - }) + if is_string_type && values.len() == 1 { + dfg.get_string(values[0]) + } else { + None + } } fn display_constrain_error( diff --git a/compiler/noirc_evaluator/src/ssa/opt/assert_constant.rs b/compiler/noirc_evaluator/src/ssa/opt/assert_constant.rs index 6936c7ad542..192c0f59344 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/assert_constant.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/assert_constant.rs @@ -122,7 +122,11 @@ fn evaluate_static_assert( } else { let call_stack = function.dfg.get_instruction_call_stack(instruction); if function.dfg.is_constant(arguments[0]) { - Err(RuntimeError::StaticAssertFailed { call_stack }) + let message = function + .dfg + .get_string(arguments[1]) + .expect("Expected second argument to be a string"); + Err(RuntimeError::StaticAssertFailed { message, call_stack }) } else { Err(RuntimeError::StaticAssertDynamicPredicate { call_stack }) } diff --git a/noir_stdlib/src/lib.nr b/noir_stdlib/src/lib.nr index 2e743822ffb..fb073516d29 100644 --- a/noir_stdlib/src/lib.nr +++ b/noir_stdlib/src/lib.nr @@ -120,3 +120,10 @@ where #[builtin(as_witness)] pub fn as_witness(x: Field) {} + +mod tests { + #[test(should_fail_with = "custom message")] + fn test_static_assert_custom_message() { + super::static_assert(1 == 2, "custom message"); + } +} From 8c92b70d1605325fb43017eab401744cb57587e9 Mon Sep 17 00:00:00 2001 From: James Zaki Date: Fri, 10 Jan 2025 15:14:09 +0000 Subject: [PATCH 2/2] chore: Add more Field use info (#7019) --- .../docs/explainers/explainer-writing-noir.md | 45 +++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/docs/docs/explainers/explainer-writing-noir.md b/docs/docs/explainers/explainer-writing-noir.md index 202bf76a827..b4265a14dbf 100644 --- a/docs/docs/explainers/explainer-writing-noir.md +++ b/docs/docs/explainers/explainer-writing-noir.md @@ -26,9 +26,9 @@ The equivalent optimization task when writing zk circuits is affectionately refe ### Coding for circuits - a paradigm shift -In zero knowledge cryptography, code is compiled to "circuits" consisting of arithmetic gates, and gate count is the significant cost. Depending on the proving system this is linearly proportionate to proving time, and so from a product point this should be kept as low as possible. +In zero knowledge cryptography, code is compiled to "circuits" consisting of arithmetic gates, and gate count is the significant cost. Depending on the proving system this is linearly proportionate to proof size and proving time, so from a product point of view this should be kept as low as possible. -Whilst writing efficient code for web apps and Solidity has a few key differences, writing efficient circuits have a different set of considerations. It is a bit of a paradigm shift, like writing code for GPUs for the first time... +Whilst writing efficient code for web apps and Solidity have some differences, writing efficient circuits have a different set of considerations. It is a bit of a paradigm shift, like writing code for GPUs for the first time... For example, drawing a circle at (0, 0) of radius `r`: - For a single CPU thread, @@ -57,7 +57,7 @@ For those coming from a primarily web app background, this article will explain ## Translating from Rust -For some applications using Noir, existing code might be a convenient starting point to then proceed to optimize the gate count of. +Programs written in anything from pseudo code to C, can be translated into Noir. A Rust program written for execution can be readily ported to Noir thanks to the similarities in syntax. :::note Many valuable functions and algorithms have been written in more established languages (C/C++), and converted to modern ones (like Rust). @@ -93,23 +93,42 @@ A Noir program compiles to an Abstract Circuit Intermediate Representation which :::tip The command `nargo info` shows the programs circuit size, and is useful to compare the value of changes made. -You can dig deeper and use the `--print-acir` param to take a closer look at individual ACIR opcodes, and the proving backend to see its gate count (eg for barretenberg, `bb gates -b ./target/program.json`). +You can dig deeper and use the `--print-acir` param to take a closer look at individual ACIR opcodes, and the proving backend to see its gate count (eg for barretenberg, the `bb` binary has a `gates` option). ::: -### Use the `Field` type +### Numerical types -Since the native type of values in circuits are `Field`s, using them for variables in Noir means less gates converting them under the hood. -Some things to be mindful of when using a Field type for a regular integer value: -- A variable of type `Field` can be cast `as` an integer type (eg `u8`, `u64`) - - Note: this retains only the bits of the integer type. Eg a Field value of 260 as a `u8` becomes 4 -- For Field types arithmetic operations meaningfully overflow/underflow, yet for integer types they are checked according to their size -- Comparisons and bitwise operations do not exist for `Field`s, cast to an appropriately sized integer type when you need to +As mentioned earlier Noir has many familiar integer types (eg `i8`, `u64`). Ideally after bringing a program into Noir, proving/verifying of its execution just works where needed: client/server side, on an evm, or on the Aztec network. + +A program optimized for execution may leverage the binary representations of integers, reducing the number of clock cycles, and thus time of execution. +The cryptography in a proving backend makes use of a `Field` type, and leveraging this lower level type correctly can reduce gate count, and thus proof size and proving time. + +In some instances simply replacing the integer type with a `Field` could save on some range checks (and hence gates). +Note: when casting a `Field` to an integer type, the value is converted based on the integer binary representation. Eg a Field variable with a value of 260 `as u8` becomes 4 + +### `Field`s for efficiency + +`Field` types have their own underlying representation that is efficient for cryptography, which is different to binary representations efficient for CPUs. So, mathematically speaking, things like bitwise operations do not directly translate to fields. That said, the same outcome can be achieved if wanting to use the Field type as a number with lower overhead. + +For instance shift (`<<`) and or (`|`) work seamlessly with integer types (bit-packing `u8`'s into a `u16`): +``` + high as u16 << 8 | low as u16 +``` + +More efficiently with `Field` types, the equivalent is: +``` + low.assert_max_bit_size::<8>(); // ensure Field values could be represented as 8 bit numbers + high.assert_max_bit_size::<8>(); + (high * 2.pow_32(8) + low) +``` +(Note, the power of two can instead be a constant (256) or global evaluated at compile time) + +The first snippet is good for compatibility when using existing code, converting to the latter can help optimize frequently used functions. :::tip -Where possible, use `Field` type for values. Using smaller value types, and bit-packing strategies, will result in MORE gates +Where possible, use the `Field` type for values. Writing code with smaller value types and bit-packing strategies will result in MORE gates ::: - ### Use Arithmetic over non-arithmetic operations Since circuits are made of arithmetic gates, the cost of arithmetic operations tends to be one gate. Whereas for procedural code, they represent several clock cycles.