Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add overflow and underflow checks for unsigned integers in brillig #4445

Merged
merged 6 commits into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 94 additions & 17 deletions compiler/noirc_evaluator/src/brillig/brillig_gen/brillig_block.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
use crate::brillig::brillig_ir::brillig_variable::{
type_to_heap_value_type, BrilligArray, BrilligVariable, BrilligVector, SingleAddrVariable,
};
use crate::brillig::brillig_ir::{
BrilligBinaryOp, BrilligContext, BRILLIG_INTEGER_ARITHMETIC_BIT_SIZE,
};
use crate::brillig::brillig_ir::{BrilligBinaryOp, BrilligContext};
use crate::ssa::ir::dfg::CallStack;
use crate::ssa::ir::instruction::ConstrainError;
use crate::ssa::ir::{
Expand Down Expand Up @@ -1197,7 +1195,7 @@ impl<'block> BrilligBlock<'block> {
let left = self.convert_ssa_single_addr_value(binary.lhs, dfg);
let right = self.convert_ssa_single_addr_value(binary.rhs, dfg);

let brillig_binary_op =
let (brillig_binary_op, is_signed) =
convert_ssa_binary_op_to_brillig_binary_op(binary.operator, &binary_type);

self.brillig_context.binary_instruction(
Expand All @@ -1206,6 +1204,93 @@ impl<'block> BrilligBlock<'block> {
result_variable.address,
brillig_binary_op,
);

self.add_overflow_check(brillig_binary_op, left, right, result_variable, is_signed);
}

fn add_overflow_check(
&mut self,
binary_operation: BrilligBinaryOp,
left: SingleAddrVariable,
right: SingleAddrVariable,
result: SingleAddrVariable,
is_signed: bool,
) {
let (op, bit_size) = if let BrilligBinaryOp::Integer { op, bit_size } = binary_operation {
(op, bit_size)
} else {
return;
};

match (op, is_signed) {
(BinaryIntOp::Add, false) => {
jfecher marked this conversation as resolved.
Show resolved Hide resolved
let condition = self.brillig_context.allocate_register();
// Check that lhs <= result
self.brillig_context.binary_instruction(
left.address,
result.address,
condition,
BrilligBinaryOp::Integer { op: BinaryIntOp::LessThanEquals, bit_size },
);
self.brillig_context.constrain_instruction(
condition,
Some("attempt to add with overflow".to_string()),
);
self.brillig_context.deallocate_register(condition);
}
(BinaryIntOp::Sub, false) => {
let condition = self.brillig_context.allocate_register();
// Check that rhs <= lhs
self.brillig_context.binary_instruction(
right.address,
left.address,
condition,
BrilligBinaryOp::Integer { op: BinaryIntOp::LessThanEquals, bit_size },
);
self.brillig_context.constrain_instruction(
condition,
Some("attempt to subtract with overflow".to_string()),
);
self.brillig_context.deallocate_register(condition);
}
(BinaryIntOp::Mul, false) => {
// Multiplication overflow is only possible for bit sizes > 1
if bit_size > 1 {
let is_right_zero = self.brillig_context.allocate_register();
let zero = self.brillig_context.make_constant(0_usize.into(), bit_size);
self.brillig_context.binary_instruction(
zero,
right.address,
is_right_zero,
BrilligBinaryOp::Integer { op: BinaryIntOp::Equals, bit_size },
);
self.brillig_context.if_not_instruction(is_right_zero, |ctx| {
let condition = ctx.allocate_register();
// Check that result / rhs == lhs
ctx.binary_instruction(
result.address,
right.address,
condition,
BrilligBinaryOp::Integer { op: BinaryIntOp::UnsignedDiv, bit_size },
);
ctx.binary_instruction(
condition,
left.address,
condition,
BrilligBinaryOp::Integer { op: BinaryIntOp::Equals, bit_size },
);
ctx.constrain_instruction(
condition,
Some("attempt to multiply with overflow".to_string()),
);
ctx.deallocate_register(condition);
});
self.brillig_context.deallocate_register(is_right_zero);
self.brillig_context.deallocate_register(zero);
}
}
_ => {}
}
}

/// Converts an SSA `ValueId` into a `RegisterOrMemory`. Initializes if necessary.
Expand Down Expand Up @@ -1403,8 +1488,6 @@ impl<'block> BrilligBlock<'block> {
}

/// Returns the type of the operation considering the types of the operands
/// TODO: SSA issues binary operations between fields and integers.
/// This probably should be explicitly casted in SSA to avoid having to coerce at this level.
pub(crate) fn type_of_binary_operation(lhs_type: &Type, rhs_type: &Type) -> Type {
match (lhs_type, rhs_type) {
(_, Type::Function) | (Type::Function, _) => {
Expand All @@ -1419,10 +1502,6 @@ pub(crate) fn type_of_binary_operation(lhs_type: &Type, rhs_type: &Type) -> Type
(_, Type::Slice(..)) | (Type::Slice(..), _) => {
unreachable!("Arrays are invalid in binary operations")
}
// If either side is a Field constant then, we coerce into the type
// of the other operand
(Type::Numeric(NumericType::NativeField), typ)
| (typ, Type::Numeric(NumericType::NativeField)) => typ.clone(),
// If both sides are numeric type, then we expect their types to be
// the same.
(Type::Numeric(lhs_type), Type::Numeric(rhs_type)) => {
Expand All @@ -1441,7 +1520,7 @@ pub(crate) fn type_of_binary_operation(lhs_type: &Type, rhs_type: &Type) -> Type
pub(crate) fn convert_ssa_binary_op_to_brillig_binary_op(
ssa_op: BinaryOp,
typ: &Type,
) -> BrilligBinaryOp {
) -> (BrilligBinaryOp, bool) {
// First get the bit size and whether its a signed integer, if it is a numeric type
// if it is not,then we return None, indicating that
// it is a Field.
Expand All @@ -1461,10 +1540,6 @@ pub(crate) fn convert_ssa_binary_op_to_brillig_binary_op(
BinaryOp::Mul => BrilligBinaryOp::Field { op: BinaryFieldOp::Mul },
BinaryOp::Div => BrilligBinaryOp::Field { op: BinaryFieldOp::Div },
BinaryOp::Eq => BrilligBinaryOp::Field { op: BinaryFieldOp::Equals },
BinaryOp::Lt => BrilligBinaryOp::Integer {
op: BinaryIntOp::LessThan,
bit_size: BRILLIG_INTEGER_ARITHMETIC_BIT_SIZE,
},
_ => unreachable!(
"Field type cannot be used with {op}. This should have been caught by the frontend"
),
Expand Down Expand Up @@ -1500,7 +1575,9 @@ pub(crate) fn convert_ssa_binary_op_to_brillig_binary_op(

// If bit size is available then it is a binary integer operation
match bit_size_signedness {
Some((bit_size, is_signed)) => binary_op_to_int_op(ssa_op, *bit_size, is_signed),
None => binary_op_to_field_op(ssa_op),
Some((bit_size, is_signed)) => {
(binary_op_to_int_op(ssa_op, *bit_size, is_signed), is_signed)
}
None => (binary_op_to_field_op(ssa_op), false),
}
}
30 changes: 17 additions & 13 deletions compiler/noirc_evaluator/src/brillig/brillig_ir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,6 @@ use acvm::{
use debug_show::DebugShow;
use num_bigint::BigUint;

/// Integer arithmetic in Brillig is limited to 127 bit
/// integers.
///
/// We could lift this in the future and have Brillig
/// do big integer arithmetic when it exceeds the field size
/// or we could have users re-implement big integer arithmetic
/// in Brillig.
/// Since constrained functions do not have this property, it
/// would mean that unconstrained functions will differ from
/// constrained functions in terms of syntax compatibility.
pub(crate) const BRILLIG_INTEGER_ARITHMETIC_BIT_SIZE: u32 = 127;
/// The Brillig VM does not apply a limit to the memory address space,
/// As a convention, we take use 64 bits. This means that we assume that
/// memory has 2^64 memory slots.
Expand Down Expand Up @@ -356,6 +345,21 @@ impl BrilligContext {
self.enter_section(end_section);
}

/// This instruction issues a branch that jumps over the code generated by the given function if the condition is truthy
pub(crate) fn if_not_instruction(
&mut self,
condition: MemoryAddress,
mut f: impl FnMut(&mut BrilligContext),
sirasistant marked this conversation as resolved.
Show resolved Hide resolved
) {
let (end_section, end_label) = self.reserve_next_section_label();

self.jump_if_instruction(condition, end_label.clone());

f(self);

self.enter_section(end_section);
}

/// Adds a label to the next opcode
pub(crate) fn enter_context<T: ToString>(&mut self, label: T) {
self.debug_show.enter_context(label.to_string());
Expand Down Expand Up @@ -533,7 +537,7 @@ impl BrilligContext {
result: MemoryAddress,
operation: BrilligBinaryOp,
) {
self.debug_show.binary_instruction(lhs, rhs, result, operation.clone());
self.debug_show.binary_instruction(lhs, rhs, result, operation);
match operation {
BrilligBinaryOp::Field { op } => {
let opcode = BrilligOpcode::BinaryFieldOp { op, destination: result, lhs, rhs };
Expand Down Expand Up @@ -1082,7 +1086,7 @@ impl BrilligContext {
}

/// Type to encapsulate the binary operation types in Brillig
#[derive(Clone)]
#[derive(Clone, Copy)]
pub(crate) enum BrilligBinaryOp {
Field { op: BinaryFieldOp },
Integer { op: BinaryIntOp, bit_size: u32 },
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
x = "0"
x = "1"
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ struct MyStruct {
fn main(x: u32) {
assert(wrapper(increment, x) == x + 1);
assert(wrapper(increment_acir, x) == x + 1);
assert(wrapper(decrement, x) == std::wrapping_sub(x, 1));
assert(wrapper(decrement, x) == x - 1);
assert(wrapper_with_struct(MyStruct { operation: increment }, x) == x + 1);
assert(wrapper_with_struct(MyStruct { operation: decrement }, x) == std::wrapping_sub(x, 1));
assert(wrapper_with_struct(MyStruct { operation: decrement }, x) == x - 1);
jfecher marked this conversation as resolved.
Show resolved Hide resolved
// https://github.com/noir-lang/noir/issues/1975
assert(increment(x) == x + 1);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[package]
name = "brillig_overflow_checks"
type = "bin"
authors = [""]
[dependencies]
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use dep::std::field::bn254::{TWO_POW_128, assert_gt};

#[test(should_fail_with = "attempt to add with overflow")]
unconstrained fn test_overflow_add() {
let a: u8 = 255;
let b: u8 = 1;
assert_eq(a + b, 0);
}

#[test(should_fail_with = "attempt to subtract with overflow")]
unconstrained fn test_overflow_sub() {
let a: u8 = 0;
let b: u8 = 1;
assert_eq(a - b, 255);
}

#[test(should_fail_with = "attempt to multiply with overflow")]
unconstrained fn test_overflow_mul() {
let a: u8 = 128;
let b: u8 = 2;
assert_eq(a * b, 0);
}

Loading