Skip to content

Commit

Permalink
feat(brillig): Added cast instruction (#1649)
Browse files Browse the repository at this point in the history
* feat: added cast instruction

* docs: add comment on cast

* docs: added comment on cast instruction

* simplify convert_cast

* Alvaro to review

* feat: do casts as no ops instead

---------

Co-authored-by: kevaundray <kevtheappdev@gmail.com>
  • Loading branch information
sirasistant and kevaundray authored Jun 14, 2023
1 parent 2893855 commit 3050b44
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[package]
authors = [""]
compiler_version = "0.1"

[dependencies]
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Tests a very simple Brillig function.
//
// The features being tested are cast operations on brillig
fn main() {
bool_casts();
field_casts();
uint_casts();
int_casts();
mixed_casts();
}

unconstrained fn bool_casts() {
assert(false == 0 as bool);
assert(true == 1 as bool);
assert(true == 3 as bool);
}

unconstrained fn field_casts() {
assert(5 as u8 as Field == 5);
assert(16 as u4 as Field == 0);
}

unconstrained fn uint_casts() {
let x: u32 = 100;
assert(x as u2 == 0);
assert(x as u4 == 4);
assert(x as u6 == 36);
assert(x as u8 == 100);
assert(x as u64 == 100);
assert(x as u126 == 100);
}

unconstrained fn int_casts() {
let x: i32 = 100;
assert(x as i2 == 0);
assert(x as i4 == 4);
assert(x as i6 == -28 as i6);
assert(x as i8 == 100);
assert(x as i8 == 100);
assert(x as i8 == 100);
}


unconstrained fn mixed_casts() {
assert(100 as u32 as i32 as u32 == 100);
assert(13 as u4 as i2 as u32 == 1);
assert(15 as u4 as i2 as u32 == 3);
assert(1 as u8 as bool == true);
assert(true as i8 == 1);
}
55 changes: 54 additions & 1 deletion crates/noirc_evaluator/src/brillig/brillig_gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ use crate::ssa_refactor::ir::{
types::{NumericType, Type},
value::{Value, ValueId},
};
use acvm::acir::brillig_vm::{BinaryFieldOp, BinaryIntOp, RegisterIndex, RegisterValueOrArray};
use acvm::{
acir::brillig_vm::{BinaryFieldOp, BinaryIntOp, RegisterIndex, RegisterValueOrArray},
FieldElement,
};
use iter_extended::vecmap;
use std::collections::HashMap;

Expand Down Expand Up @@ -190,10 +193,60 @@ impl BrilligGen {
let source = self.convert_ssa_value(*value, dfg);
self.context.truncate_instruction(destination, source);
}
Instruction::Cast(value, target_type) => {
let result_ids = dfg.instruction_results(instruction_id);
let destination = self.get_or_create_register(result_ids[0]);
let source = self.convert_ssa_value(*value, dfg);
self.convert_cast(destination, source, target_type, &dfg.type_of_value(*value));
}
_ => todo!("ICE: Instruction not supported {instruction:?}"),
};
}

/// Converts an SSA cast to a sequence of Brillig opcodes.
/// Casting is only necessary when shrinking the bit size of a numeric value.
fn convert_cast(
&mut self,
destination: RegisterIndex,
source: RegisterIndex,
target_type: &Type,
source_type: &Type,
) {
fn numeric_to_bit_size(typ: &NumericType) -> u32 {
match typ {
NumericType::Signed { bit_size } | NumericType::Unsigned { bit_size } => *bit_size,
NumericType::NativeField => FieldElement::max_num_bits(),
}
}

// Casting is only valid for numeric types
// This should be checked by the frontend, so we panic if this is the case
let (source_numeric_type, target_numeric_type) = match (source_type, target_type) {
(Type::Numeric(source_numeric_type), Type::Numeric(target_numeric_type)) => {
(source_numeric_type, target_numeric_type)
}
_ => unimplemented!("The cast operation is only valid for integers."),
};

let source_bit_size = numeric_to_bit_size(source_numeric_type);
let target_bit_size = numeric_to_bit_size(target_numeric_type);

// Casting from a larger bit size to a smaller bit size (narrowing cast)
// requires a cast instruction.
// If its a widening cast, ie casting from a smaller bit size to a larger bit size
// we simply put a mov instruction as a no-op
//
// Field elements by construction always have the largest bit size
// This means that casting to a Field element, will always be a widening cast
// and therefore a no-op. Conversely, casting from a Field element
// will always be a narrowing cast and therefore a cast instruction
if source_bit_size > target_bit_size {
self.context.cast_instruction(destination, source, target_bit_size);
} else {
self.context.mov_instruction(destination, source);
}
}

/// Converts the Binary instruction into a sequence of Brillig opcodes.
fn convert_ssa_binary(
&mut self,
Expand Down
40 changes: 40 additions & 0 deletions crates/noirc_evaluator/src/brillig/brillig_ir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ use acvm::{
FieldElement,
};

/// 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.
const BRILLIG_INTEGER_ARITHMETIC_BIT_SIZE: u32 = 127;

/// Brillig context object that is used while constructing the
/// Brillig bytecode.
#[derive(Default)]
Expand Down Expand Up @@ -302,6 +314,34 @@ impl BrilligContext {
rhs: scratch_register_j,
});
}

/// Emits a modulo instruction against 2**target_bit_size
///
/// Integer arithmetic in Brillig is currently constrained to 127 bit integers.
/// We restrict the cast operation, so that integer types over 127 bits
/// cannot be created.
pub(crate) fn cast_instruction(
&mut self,
destination: RegisterIndex,
source: RegisterIndex,
target_bit_size: u32,
) {
assert!(
target_bit_size <= BRILLIG_INTEGER_ARITHMETIC_BIT_SIZE,
"tried to cast to a bit size greater than allowed {target_bit_size}"
);

// The brillig VM performs all arithmetic operations modulo 2**bit_size
// So to cast any value to a target bit size we can just issue a no-op arithmetic operation
// With bit size equal to target_bit_size
let zero = self.make_constant(Value::from(FieldElement::zero()));
self.binary_instruction(
source,
zero,
destination,
BrilligBinaryOp::Integer { op: BinaryIntOp::Add, bit_size: target_bit_size },
);
}
}

/// Type to encapsulate the binary operation types in Brillig
Expand Down
2 changes: 1 addition & 1 deletion crates/noirc_evaluator/src/ssa_refactor/acir_gen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ impl Context {
// Generate the brillig code of the function
let code = BrilligArtifact::default().link(&brillig[*id]);

let outputs: Vec<AcirType> = vecmap(result_ids, |result_id| dfg.type_of_value(*result_id).into());
let outputs: Vec<AcirType> = vecmap(result_ids, |result_id| dfg.type_of_value(*result_id).into());

let output_values = self.acir_context.brillig(code, inputs, outputs);
// Compiler sanity check
Expand Down

0 comments on commit 3050b44

Please sign in to comment.