Skip to content

Commit

Permalink
fix(ssa refactor): more comprehensive instruction simplification (#1735)
Browse files Browse the repository at this point in the history
* chore(ssa refactor): more comprehensive instruction simplification

* chore(ssa refactor): cp working test

* chore(ssa refactor): allow HO functions; tidy

* chore(ssa refactor): also now works

* chore(ssa refactor): PR feedback

* Update crates/noirc_evaluator/src/ssa_refactor/ir/instruction.rs

---------

Co-authored-by: jfecher <[email protected]>
  • Loading branch information
joss-aztec and jfecher authored Jun 16, 2023
1 parent c53bfc8 commit 97d6747
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 28 deletions.
5 changes: 5 additions & 0 deletions crates/nargo_cli/tests/test_data_ssa_refactor/6/Nargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[package]
authors = [""]
compiler_version = "0.1"

[dependencies]
39 changes: 39 additions & 0 deletions crates/nargo_cli/tests/test_data_ssa_refactor/6/Prover.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@

# hello as bytes
# used : https://emn178.github.io/online-tools/sha256.html
x = [104, 101, 108, 108, 111]

result = [
0x2c,
0xf2,
0x4d,
0xba,
0x5f,
0xb0,
0xa3,
0x0e,
0x26,
0xe8,
0x3b,
0x2a,
0xc5,
0xb9,
0xe2,
0x9e,
0x1b,
0x16,
0x1e,
0x5c,
0x1f,
0xa7,
0x42,
0x5e,
0x73,
0x04,
0x33,
0x62,
0x93,
0x8b,
0x98,
0x24,
]
20 changes: 20 additions & 0 deletions crates/nargo_cli/tests/test_data_ssa_refactor/6/src/main.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Sha256 circuit where the input is 5 bytes
// not five field elements since sha256 operates over
// bytes.
//
// If you do not cast, it will take all the bytes from the field element!

// Mimc input is an array of field elements
// The function is called mimc_bn254 to emphasize its parameters are chosen for bn254 curve, it should be used only with a proving system using the same curve (e.g Plonk from Aztec)
use dep::std;

fn main(x: [u8; 5], result: pub [u8; 32]) {
let mut digest = std::hash::sha256(x);
digest[0] = 5 as u8;
digest = std::hash::sha256(x);
assert(digest == result);

let y = [12,45,78,41];
let h = std::hash::mimc_bn254(y);
assert(h == 18226366069841799622585958305961373004333097209608110160936134895615261821931);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

[package]
authors = [""]
compiler_version = "0.1"

[dependencies]

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
old_root = "0x285785b10eca49cf456b935f1c9787ff571f306c1bc62549c31a9199a633f9f8"
old_leaf = "0x1cdcf02431ba623767fe389337d011df1048dcc24b98ed81cec97627bab454a0"
old_hash_path = [
"0x1cdcf02431ba623767fe389337d011df1048dcc24b98ed81cec97627bab454a0",
"0x0b5e9666e7323ce925c28201a97ddf4144ac9d148448ed6f49f9008719c1b85b",
"0x22ec636f8ad30ef78c42b7fe2be4a4cacf5a445cfb5948224539f59a11d70775",
]
new_root = "0x2d05c2650e6c2ef02c6dc7fae7f517b8ac191386666c0b5a68130a8c11092f5f"
leaf = "0x085ca53be9c9d95b57e6e5fc91c5d531ad9e63e85dd71af7e35562991774b435"
index = "0"
mimc_input = [12,45,78,41]
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use dep::std;

fn main(
old_root: Field,
old_leaf: Field,
old_hash_path: [Field; 3],
new_root: pub Field,
leaf: Field,
index: Field,
mimc_input: [Field; 4],
) {
assert(old_root == std::merkle::compute_merkle_root(old_leaf, index, old_hash_path));

let calculated_root = std::merkle::compute_merkle_root(leaf, index, old_hash_path);
assert(new_root == calculated_root);

let h = std::hash::mimc_bn254(mimc_input);
// Regression test for PR #891
std::println(h);
assert(h == 18226366069841799622585958305961373004333097209608110160936134895615261821931);
}
136 changes: 108 additions & 28 deletions crates/noirc_evaluator/src/ssa_refactor/ir/instruction.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::rc::Rc;

use acvm::{acir::BlackBoxFunc, FieldElement};
use iter_extended::vecmap;
use num_bigint::BigUint;
Expand Down Expand Up @@ -239,33 +241,7 @@ impl Instruction {
use SimplifyResult::*;
match self {
Instruction::Binary(binary) => binary.simplify(dfg),
Instruction::Cast(value, typ) => {
if let Some(constant) = dfg.get_numeric_constant(*value) {
let src_typ = dfg.type_of_value(*value);
match (typ, src_typ) {
(
Type::Numeric(NumericType::Unsigned { bit_size }),
Type::Numeric(NumericType::Unsigned { .. }),
)
| (
Type::Numeric(NumericType::Unsigned { bit_size }),
Type::Numeric(NumericType::NativeField),
) => {
let integer_modulus = BigUint::from(2u128).pow(*bit_size);
let constant: BigUint = BigUint::from_bytes_be(&constant.to_be_bytes());
let truncated = constant % integer_modulus;
let truncated =
FieldElement::from_be_bytes_reduce(&truncated.to_bytes_be());
SimplifiedTo(dfg.make_constant(truncated, typ.clone()))
}
_ => None,
}
} else if let Some(value) = (*typ == dfg.type_of_value(*value)).then_some(*value) {
SimplifiedTo(value)
} else {
None
}
}
Instruction::Cast(value, typ) => simplify_cast(*value, typ, dfg),
Instruction::Not(value) => {
match &dfg[dfg.resolve(*value)] {
// Limit optimizing ! on constants to only booleans. If we tried it on fields,
Expand Down Expand Up @@ -329,7 +305,7 @@ impl Instruction {
None
}
}
Instruction::Call { .. } => None,
Instruction::Call { func, arguments } => simplify_call(*func, arguments, dfg),
Instruction::Allocate { .. } => None,
Instruction::Load { .. } => None,
Instruction::Store { .. } => None,
Expand All @@ -338,6 +314,110 @@ impl Instruction {
}
}

/// Try to simplify this cast instruction. If the instruction can be simplified to a known value,
/// that value is returned. Otherwise None is returned.
fn simplify_cast(value: ValueId, dst_typ: &Type, dfg: &mut DataFlowGraph) -> SimplifyResult {
use SimplifyResult::*;
if let Some(constant) = dfg.get_numeric_constant(value) {
let src_typ = dfg.type_of_value(value);
match (src_typ, dst_typ) {
(Type::Numeric(NumericType::NativeField), Type::Numeric(NumericType::NativeField)) => {
// Field -> Field: use src value
SimplifiedTo(value)
}
(
Type::Numeric(NumericType::Unsigned { .. }),
Type::Numeric(NumericType::NativeField),
) => {
// Unsigned -> Field: redefine same constant as Field
SimplifiedTo(dfg.make_constant(constant, dst_typ.clone()))
}
(
Type::Numeric(NumericType::NativeField | NumericType::Unsigned { .. }),
Type::Numeric(NumericType::Unsigned { bit_size }),
) => {
// Field/Unsigned -> unsigned: truncate
let integer_modulus = BigUint::from(2u128).pow(*bit_size);
let constant: BigUint = BigUint::from_bytes_be(&constant.to_be_bytes());
let truncated = constant % integer_modulus;
let truncated = FieldElement::from_be_bytes_reduce(&truncated.to_bytes_be());
SimplifiedTo(dfg.make_constant(truncated, dst_typ.clone()))
}
_ => None,
}
} else if *dst_typ == dfg.type_of_value(value) {
SimplifiedTo(value)
} else {
None
}
}

/// Try to simplify this call instruction. If the instruction can be simplified to a known value,
/// that value is returned. Otherwise None is returned.
fn simplify_call(func: ValueId, arguments: &[ValueId], dfg: &mut DataFlowGraph) -> SimplifyResult {
use SimplifyResult::*;
let intrinsic = match &dfg[func] {
Value::Intrinsic(intrinsic) => *intrinsic,
_ => return None,
};
let constant_args: Option<Vec<_>> =
arguments.iter().map(|value_id| dfg.get_numeric_constant(*value_id)).collect();
let constant_args = match constant_args {
Some(constant_args) => constant_args,
Option::None => return None,
};
match intrinsic {
Intrinsic::ToBits(endian) => {
let field = constant_args[0];
let limb_count = constant_args[1].to_u128() as u32;
SimplifiedTo(constant_to_radix(endian, field, 2, limb_count, dfg))
}
Intrinsic::ToRadix(endian) => {
let field = constant_args[0];
let radix = constant_args[1].to_u128() as u32;
let limb_count = constant_args[1].to_u128() as u32;
SimplifiedTo(constant_to_radix(endian, field, radix, limb_count, dfg))
}
Intrinsic::BlackBox(_) | Intrinsic::Println | Intrinsic::Sort => None,
}
}

/// Returns a Value::Array of constants corresponding to the limbs of the radix decomposition.
fn constant_to_radix(
endian: Endian,
field: FieldElement,
radix: u32,
limb_count: u32,
dfg: &mut DataFlowGraph,
) -> ValueId {
let bit_size = u32::BITS - (radix - 1).leading_zeros();
let radix_big = BigUint::from(radix);
assert_eq!(BigUint::from(2u128).pow(bit_size), radix_big, "ICE: Radix must be a power of 2");
let big_integer = BigUint::from_bytes_be(&field.to_be_bytes());

// Decompose the integer into its radix digits in little endian form.
let decomposed_integer = big_integer.to_radix_le(radix);
let mut limbs = vecmap(0..limb_count, |i| match decomposed_integer.get(i as usize) {
Some(digit) => FieldElement::from_be_bytes_reduce(&[*digit]),
None => FieldElement::zero(),
});
if endian == Endian::Big {
limbs.reverse();
}

// For legacy reasons (see #617) the to_radix interface supports 256 bits even though
// FieldElement::max_num_bits() is only 254 bits. Any limbs beyond the specified count
// become zero padding.
let max_decomposable_bits: u32 = 256;
let limb_count_with_padding = max_decomposable_bits / bit_size;
while limbs.len() < limb_count_with_padding as usize {
limbs.push(FieldElement::zero());
}
let result_constants =
limbs.into_iter().map(|limb| dfg.make_constant(limb, Type::unsigned(bit_size))).collect();
dfg.make_array(result_constants, Rc::new(vec![Type::unsigned(bit_size)]))
}

/// The possible return values for Instruction::return_types
pub(crate) enum InstructionResultType {
/// The result type of this instruction matches that of this operand
Expand Down

0 comments on commit 97d6747

Please sign in to comment.