Skip to content

Commit

Permalink
Merge branch 'master' into 6742-optional-mock
Browse files Browse the repository at this point in the history
  • Loading branch information
aakoshh authored Jan 10, 2025
2 parents 7b53dae + 8c92b70 commit 72e2ebd
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 33 deletions.
6 changes: 3 additions & 3 deletions compiler/noirc_evaluator/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -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, .. }
Expand Down
18 changes: 18 additions & 0 deletions compiler/noirc_evaluator/src/ssa/ir/dfg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
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)]
Expand Down
21 changes: 5 additions & 16 deletions compiler/noirc_evaluator/src/ssa/ir/printer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,22 +274,11 @@ pub(crate) fn try_to_extract_string_from_error_payload(
values: &[ValueId],
dfg: &DataFlowGraph,
) -> Option<String> {
(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::<Option<Vec<_>>>()
})
.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(
Expand Down
6 changes: 5 additions & 1 deletion compiler/noirc_evaluator/src/ssa/opt/assert_constant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
}
Expand Down
45 changes: 32 additions & 13 deletions docs/docs/explainers/explainer-writing-noir.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions noir_stdlib/src/lib.nr
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}

0 comments on commit 72e2ebd

Please sign in to comment.