use crate::{
black_box::BlackBoxOp,
lengths::{ElementsFlattenedLength, FlattenedLength, SemanticLength, SemiFlattenedLength},
};
use acir_field::AcirField;
use itertools::Itertools;
use msgpack_tagged::MsgpackTagged;
use serde::{Deserialize, Serialize};
pub type Label = usize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[derive(Serialize, Deserialize, MsgpackTagged)]
#[cfg_attr(feature = "arb", derive(proptest_derive::Arbitrary))]
pub enum MemoryAddress {
#[tag(0)]
Direct(u32),
#[tag(1)]
Relative(u32),
}
impl MemoryAddress {
pub fn direct(address: u32) -> Self {
MemoryAddress::Direct(address)
}
pub fn relative(offset: u32) -> Self {
MemoryAddress::Relative(offset)
}
pub fn unwrap_direct(self) -> u32 {
match self {
MemoryAddress::Direct(address) => address,
MemoryAddress::Relative(_) => panic!("Expected direct memory address"),
}
}
pub fn unwrap_relative(self) -> u32 {
match self {
MemoryAddress::Direct(_) => panic!("Expected relative memory address"),
MemoryAddress::Relative(offset) => offset,
}
}
pub fn to_u32(self) -> u32 {
match self {
MemoryAddress::Direct(address) => address,
MemoryAddress::Relative(offset) => offset,
}
}
pub fn is_relative(&self) -> bool {
match self {
MemoryAddress::Relative(_) => true,
MemoryAddress::Direct(_) => false,
}
}
pub fn is_direct(&self) -> bool {
!self.is_relative()
}
pub fn offset(&self, amount: u32) -> Self {
let address = self.unwrap_direct();
MemoryAddress::direct(address.checked_add(amount).expect("memory offset overflow"))
}
}
impl std::fmt::Display for MemoryAddress {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MemoryAddress::Direct(address) => write!(f, "@{address}"),
MemoryAddress::Relative(offset) => write!(f, "sp[{offset}]"),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[derive(Serialize, Deserialize, MsgpackTagged)]
pub enum HeapValueType {
#[tag(0)]
Simple(BitSize),
#[tag(1)]
Array {
#[tag(0)]
value_types: Vec<HeapValueType>,
#[tag(1)]
size: SemanticLength,
},
#[tag(2)]
Vector {
#[tag(0)]
value_types: Vec<HeapValueType>,
},
}
impl HeapValueType {
pub fn all_simple(types: &[HeapValueType]) -> bool {
types.iter().all(|typ| matches!(typ, HeapValueType::Simple(_)))
}
pub fn field() -> HeapValueType {
HeapValueType::Simple(BitSize::Field)
}
pub fn flattened_size(&self) -> Option<FlattenedLength> {
match self {
HeapValueType::Simple(_) => Some(FlattenedLength(1)),
HeapValueType::Array { value_types, size } => {
let elements_flattened_size =
value_types.iter().map(|t| t.flattened_size()).sum::<Option<FlattenedLength>>();
elements_flattened_size.map(|elements_flattened_size| {
ElementsFlattenedLength::from(elements_flattened_size) * *size
})
}
HeapValueType::Vector { .. } => {
None
}
}
}
}
impl std::fmt::Display for HeapValueType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let write_types =
|f: &mut std::fmt::Formatter<'_>, value_types: &[HeapValueType]| -> std::fmt::Result {
if value_types.len() == 1 {
write!(f, "{}", value_types[0])?;
} else {
write!(f, "(")?;
for (index, value_type) in value_types.iter().enumerate() {
if index > 0 {
write!(f, ", ")?;
}
write!(f, "{value_type}")?;
}
write!(f, ")")?;
}
Ok(())
};
match self {
HeapValueType::Simple(bit_size) => {
write!(f, "{bit_size}")
}
HeapValueType::Array { value_types, size } => {
write!(f, "[")?;
write_types(f, value_types)?;
write!(f, "; {size}")?;
write!(f, "]")
}
HeapValueType::Vector { value_types } => {
write!(f, "@[")?;
write_types(f, value_types)?;
write!(f, "]")
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash)]
#[derive(Serialize, Deserialize, MsgpackTagged)]
#[cfg_attr(feature = "arb", derive(proptest_derive::Arbitrary))]
pub struct HeapArray {
#[tag(0)]
pub pointer: MemoryAddress,
#[tag(1)]
pub size: SemiFlattenedLength,
}
impl Default for HeapArray {
fn default() -> Self {
Self { pointer: MemoryAddress::direct(0), size: SemiFlattenedLength(0) }
}
}
impl std::fmt::Display for HeapArray {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}; {}]", self.pointer, self.size)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash)]
#[derive(Serialize, Deserialize, MsgpackTagged)]
#[cfg_attr(feature = "arb", derive(proptest_derive::Arbitrary))]
pub struct HeapVector {
#[tag(0)]
pub pointer: MemoryAddress,
#[tag(1)]
pub size: MemoryAddress,
}
impl std::fmt::Display for HeapVector {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "@[{}; {}]", self.pointer, self.size)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, Hash)]
#[derive(Serialize, Deserialize, MsgpackTagged)]
#[cfg_attr(feature = "arb", derive(proptest_derive::Arbitrary))]
pub enum IntegerBitSize {
#[tag(0)]
U1,
#[tag(1)]
U8,
#[tag(2)]
U16,
#[tag(3)]
U32,
#[tag(4)]
U64,
#[tag(5)]
U128,
}
impl IntegerBitSize {
pub const fn to_u32(self) -> u32 {
match self {
IntegerBitSize::U1 => 1,
IntegerBitSize::U8 => 8,
IntegerBitSize::U16 => 16,
IntegerBitSize::U32 => 32,
IntegerBitSize::U64 => 64,
IntegerBitSize::U128 => 128,
}
}
}
impl From<IntegerBitSize> for u32 {
fn from(bit_size: IntegerBitSize) -> u32 {
bit_size.to_u32()
}
}
impl TryFrom<u32> for IntegerBitSize {
type Error = &'static str;
fn try_from(value: u32) -> Result<Self, Self::Error> {
match value {
1 => Ok(IntegerBitSize::U1),
8 => Ok(IntegerBitSize::U8),
16 => Ok(IntegerBitSize::U16),
32 => Ok(IntegerBitSize::U32),
64 => Ok(IntegerBitSize::U64),
128 => Ok(IntegerBitSize::U128),
_ => Err("Invalid bit size"),
}
}
}
impl std::fmt::Display for IntegerBitSize {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
IntegerBitSize::U1 => write!(f, "bool"),
IntegerBitSize::U8 => write!(f, "u8"),
IntegerBitSize::U16 => write!(f, "u16"),
IntegerBitSize::U32 => write!(f, "u32"),
IntegerBitSize::U64 => write!(f, "u64"),
IntegerBitSize::U128 => write!(f, "u128"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, Hash)]
#[derive(Serialize, Deserialize, MsgpackTagged)]
#[cfg_attr(feature = "arb", derive(proptest_derive::Arbitrary))]
pub enum BitSize {
#[tag(0)]
Field,
#[tag(1)]
Integer(IntegerBitSize),
}
impl BitSize {
pub fn to_u32<F: AcirField>(self) -> u32 {
match self {
BitSize::Field => F::max_num_bits(),
BitSize::Integer(bit_size) => bit_size.into(),
}
}
pub fn try_from_u32<F: AcirField>(value: u32) -> Result<Self, &'static str> {
if value == F::max_num_bits() {
Ok(BitSize::Field)
} else {
Ok(BitSize::Integer(IntegerBitSize::try_from(value)?))
}
}
}
impl std::fmt::Display for BitSize {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
BitSize::Field => write!(f, "field"),
BitSize::Integer(bit_size) => write!(f, "{bit_size}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash)]
#[derive(Serialize, Deserialize, MsgpackTagged)]
#[cfg_attr(feature = "arb", derive(proptest_derive::Arbitrary))]
pub enum ValueOrArray {
#[tag(0)]
MemoryAddress(MemoryAddress),
#[tag(1)]
HeapArray(HeapArray),
#[tag(2)]
HeapVector(HeapVector),
}
impl std::fmt::Display for ValueOrArray {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ValueOrArray::MemoryAddress(memory_address) => {
write!(f, "{memory_address}")
}
ValueOrArray::HeapArray(heap_array) => {
write!(f, "{heap_array}")
}
ValueOrArray::HeapVector(heap_vector) => {
write!(f, "{heap_vector}")
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[derive(Serialize, Deserialize, MsgpackTagged)]
#[cfg_attr(feature = "arb", derive(proptest_derive::Arbitrary))]
pub enum BrilligOpcode<F> {
#[tag(0)]
BinaryFieldOp {
#[tag(0)]
destination: MemoryAddress,
#[tag(1)]
op: BinaryFieldOp,
#[tag(2)]
lhs: MemoryAddress,
#[tag(3)]
rhs: MemoryAddress,
},
#[tag(1)]
BinaryIntOp {
#[tag(0)]
destination: MemoryAddress,
#[tag(1)]
op: BinaryIntOp,
#[tag(2)]
bit_size: IntegerBitSize,
#[tag(3)]
lhs: MemoryAddress,
#[tag(4)]
rhs: MemoryAddress,
},
#[tag(2)]
Not {
#[tag(0)]
destination: MemoryAddress,
#[tag(1)]
source: MemoryAddress,
#[tag(2)]
bit_size: IntegerBitSize,
},
#[tag(3)]
Cast {
#[tag(0)]
destination: MemoryAddress,
#[tag(1)]
source: MemoryAddress,
#[tag(2)]
bit_size: BitSize,
},
#[tag(4)]
JumpIf {
#[tag(0)]
condition: MemoryAddress,
#[tag(1)]
location: Label,
},
#[tag(5)]
Jump {
#[tag(0)]
location: Label,
},
#[tag(6)]
CalldataCopy {
#[tag(0)]
destination_address: MemoryAddress,
#[tag(1)]
size_address: MemoryAddress,
#[tag(2)]
offset_address: MemoryAddress,
},
#[tag(7)]
Call {
#[tag(0)]
location: Label,
},
#[tag(8)]
Const {
#[tag(0)]
destination: MemoryAddress,
#[tag(1)]
bit_size: BitSize,
#[tag(2)]
value: F,
},
#[tag(9)]
IndirectConst {
#[tag(0)]
destination_pointer: MemoryAddress,
#[tag(1)]
bit_size: BitSize,
#[tag(2)]
value: F,
},
#[tag(10)]
Return,
#[tag(11)]
ForeignCall {
#[tag(0)]
function: String,
#[tag(1)]
destinations: Vec<ValueOrArray>,
#[tag(2)]
destination_value_types: Vec<HeapValueType>,
#[tag(3)]
inputs: Vec<ValueOrArray>,
#[tag(4)]
input_value_types: Vec<HeapValueType>,
},
#[tag(12)]
Mov {
#[tag(0)]
destination: MemoryAddress,
#[tag(1)]
source: MemoryAddress,
},
#[tag(13)]
ConditionalMov {
#[tag(0)]
destination: MemoryAddress,
#[tag(1)]
source_a: MemoryAddress,
#[tag(2)]
source_b: MemoryAddress,
#[tag(3)]
condition: MemoryAddress,
},
#[tag(14)]
Load {
#[tag(0)]
destination: MemoryAddress,
#[tag(1)]
source_pointer: MemoryAddress,
},
#[tag(15)]
Store {
#[tag(0)]
destination_pointer: MemoryAddress,
#[tag(1)]
source: MemoryAddress,
},
#[tag(16)]
BlackBox(BlackBoxOp),
#[tag(17)]
Trap {
#[tag(0)]
revert_data: HeapVector,
},
#[tag(18)]
Stop {
#[tag(0)]
return_data: HeapVector,
},
}
impl<F: std::fmt::Display> std::fmt::Display for BrilligOpcode<F> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BrilligOpcode::BinaryFieldOp { destination, op, lhs, rhs } => {
write!(f, "{destination} = field {op} {lhs}, {rhs}")
}
BrilligOpcode::BinaryIntOp { destination, op, bit_size, lhs, rhs } => {
write!(f, "{destination} = {bit_size} {op} {lhs}, {rhs}")
}
BrilligOpcode::Not { destination, source, bit_size } => {
write!(f, "{destination} = {bit_size} not {source}")
}
BrilligOpcode::Cast { destination, source, bit_size } => {
write!(f, "{destination} = cast {source} to {bit_size}")
}
BrilligOpcode::JumpIf { condition, location } => {
write!(f, "jump if {condition} to {location}")
}
BrilligOpcode::Jump { location } => {
write!(f, "jump to {location}")
}
BrilligOpcode::CalldataCopy { destination_address, size_address, offset_address } => {
write!(
f,
"{destination_address} = calldata copy [{offset_address}; {size_address}]"
)
}
BrilligOpcode::Call { location } => {
write!(f, "call {location}")
}
BrilligOpcode::Const { destination, bit_size, value } => {
write!(f, "{destination} = const {bit_size} {value}")
}
BrilligOpcode::IndirectConst { destination_pointer, bit_size, value } => {
write!(f, "{destination_pointer} = indirect const {bit_size} {value}")
}
BrilligOpcode::Return => {
write!(f, "return")
}
BrilligOpcode::ForeignCall {
function,
destinations,
destination_value_types,
inputs,
input_value_types,
} => {
if !destinations.is_empty() {
for (index, (destination, destination_value_type)) in
destinations.iter().zip_eq(destination_value_types).enumerate()
{
if index > 0 {
write!(f, ", ")?;
}
write!(f, "{destination}: {destination_value_type}")?;
}
write!(f, " = ")?;
}
write!(f, "foreign call {function}(")?;
for (index, (input, input_value_type)) in
inputs.iter().zip_eq(input_value_types).enumerate()
{
if index > 0 {
write!(f, ", ")?;
}
write!(f, "{input}: {input_value_type}")?;
}
write!(f, ")")?;
Ok(())
}
BrilligOpcode::Mov { destination, source } => {
write!(f, "{destination} = {source}")
}
BrilligOpcode::ConditionalMov { destination, source_a, source_b, condition } => {
write!(f, "{destination} = if {condition} then {source_a} else {source_b}")
}
BrilligOpcode::Load { destination, source_pointer } => {
write!(f, "{destination} = load {source_pointer}")
}
BrilligOpcode::Store { destination_pointer, source } => {
write!(f, "store {source} at {destination_pointer}")
}
BrilligOpcode::BlackBox(black_box_op) => {
write!(f, "{black_box_op}")
}
BrilligOpcode::Trap { revert_data } => {
write!(f, "trap {revert_data}")
}
BrilligOpcode::Stop { return_data } => {
write!(f, "stop {return_data}")
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Serialize, Deserialize, MsgpackTagged)]
#[cfg_attr(feature = "arb", derive(proptest_derive::Arbitrary))]
pub enum BinaryFieldOp {
#[tag(0)]
Add,
#[tag(1)]
Sub,
#[tag(2)]
Mul,
#[tag(3)]
Div,
#[tag(4)]
IntegerDiv,
#[tag(5)]
Equals,
#[tag(6)]
LessThan,
#[tag(7)]
LessThanEquals,
}
impl std::fmt::Display for BinaryFieldOp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BinaryFieldOp::Add => write!(f, "add"),
BinaryFieldOp::Sub => write!(f, "sub"),
BinaryFieldOp::Mul => write!(f, "mul"),
BinaryFieldOp::Div => write!(f, "field_div"),
BinaryFieldOp::IntegerDiv => write!(f, "int_div"),
BinaryFieldOp::Equals => write!(f, "eq"),
BinaryFieldOp::LessThan => write!(f, "lt"),
BinaryFieldOp::LessThanEquals => write!(f, "lt_eq"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Serialize, Deserialize, MsgpackTagged)]
#[cfg_attr(feature = "arb", derive(proptest_derive::Arbitrary))]
pub enum BinaryIntOp {
#[tag(0)]
Add,
#[tag(1)]
Sub,
#[tag(2)]
Mul,
#[tag(3)]
Div,
#[tag(4)]
Equals,
#[tag(5)]
LessThan,
#[tag(6)]
LessThanEquals,
#[tag(7)]
And,
#[tag(8)]
Or,
#[tag(9)]
Xor,
#[tag(10)]
Shl,
#[tag(11)]
Shr,
}
impl std::fmt::Display for BinaryIntOp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BinaryIntOp::Add => write!(f, "add"),
BinaryIntOp::Sub => write!(f, "sub"),
BinaryIntOp::Mul => write!(f, "mul"),
BinaryIntOp::Div => write!(f, "div"),
BinaryIntOp::Equals => write!(f, "eq"),
BinaryIntOp::LessThan => write!(f, "lt"),
BinaryIntOp::LessThanEquals => write!(f, "lt_eq"),
BinaryIntOp::And => write!(f, "and"),
BinaryIntOp::Or => write!(f, "or"),
BinaryIntOp::Xor => write!(f, "xor"),
BinaryIntOp::Shl => write!(f, "shl"),
BinaryIntOp::Shr => write!(f, "shr"),
}
}
}
#[cfg(test)]
mod tests {
use crate::MemoryAddress;
use super::{BitSize, IntegerBitSize};
use acir_field::FieldElement;
#[test]
fn test_integer_bitsize_roundtrip() {
let integer_sizes = [
IntegerBitSize::U1,
IntegerBitSize::U8,
IntegerBitSize::U16,
IntegerBitSize::U32,
IntegerBitSize::U64,
IntegerBitSize::U128,
];
for int_size in integer_sizes {
let as_u32: u32 = int_size.into();
let roundtrip = IntegerBitSize::try_from(as_u32)
.expect("Should successfully convert back from u32");
assert_eq!(
int_size, roundtrip,
"IntegerBitSize::{int_size} should roundtrip through From<IntegerBitSize> for u32 and TryFrom<u32>"
);
}
}
#[test]
fn test_integer_bitsize_values() {
assert_eq!(u32::from(IntegerBitSize::U1), 1);
assert_eq!(u32::from(IntegerBitSize::U8), 8);
assert_eq!(u32::from(IntegerBitSize::U16), 16);
assert_eq!(u32::from(IntegerBitSize::U32), 32);
assert_eq!(u32::from(IntegerBitSize::U64), 64);
assert_eq!(u32::from(IntegerBitSize::U128), 128);
}
#[test]
fn test_integer_bitsize_try_from_invalid() {
assert!(IntegerBitSize::try_from(0).is_err());
assert!(IntegerBitSize::try_from(2).is_err());
assert!(IntegerBitSize::try_from(7).is_err());
assert!(IntegerBitSize::try_from(15).is_err());
assert!(IntegerBitSize::try_from(31).is_err());
assert!(IntegerBitSize::try_from(63).is_err());
assert!(IntegerBitSize::try_from(127).is_err());
assert!(IntegerBitSize::try_from(129).is_err());
assert!(IntegerBitSize::try_from(256).is_err());
}
#[test]
fn test_bitsize_roundtrip() {
let integer_sizes = [
IntegerBitSize::U1,
IntegerBitSize::U8,
IntegerBitSize::U16,
IntegerBitSize::U32,
IntegerBitSize::U64,
IntegerBitSize::U128,
];
for int_size in integer_sizes {
let bit_size = BitSize::Integer(int_size);
let as_u32 = bit_size.to_u32::<FieldElement>();
let roundtrip = BitSize::try_from_u32::<FieldElement>(as_u32)
.expect("Should successfully convert back from u32");
assert_eq!(
bit_size, roundtrip,
"BitSize::Integer({int_size}) should roundtrip through to_u32/try_from_u32"
);
}
let field_bit_size = BitSize::Field;
let as_u32 = field_bit_size.to_u32::<FieldElement>();
let roundtrip = BitSize::try_from_u32::<FieldElement>(as_u32)
.expect("Should successfully convert Field back from u32");
assert_eq!(
field_bit_size, roundtrip,
"BitSize::Field should roundtrip through to_u32/try_from_u32"
);
}
#[test]
fn test_bitsize_to_u32_values_integers() {
assert_eq!(BitSize::Integer(IntegerBitSize::U1).to_u32::<FieldElement>(), 1);
assert_eq!(BitSize::Integer(IntegerBitSize::U8).to_u32::<FieldElement>(), 8);
assert_eq!(BitSize::Integer(IntegerBitSize::U16).to_u32::<FieldElement>(), 16);
assert_eq!(BitSize::Integer(IntegerBitSize::U32).to_u32::<FieldElement>(), 32);
assert_eq!(BitSize::Integer(IntegerBitSize::U64).to_u32::<FieldElement>(), 64);
assert_eq!(BitSize::Integer(IntegerBitSize::U128).to_u32::<FieldElement>(), 128);
}
#[test]
#[cfg(feature = "bn254")]
fn test_bitsize_to_u32_field_bn254() {
assert_eq!(BitSize::Field.to_u32::<FieldElement>(), 254);
}
#[test]
#[cfg(feature = "bls12_381")]
fn test_bitsize_to_u32_field_bls12_381() {
assert_eq!(BitSize::Field.to_u32::<FieldElement>(), 255);
}
#[test]
fn test_bitsize_try_from_u32_invalid() {
assert!(BitSize::try_from_u32::<FieldElement>(2).is_err());
assert!(BitSize::try_from_u32::<FieldElement>(7).is_err());
assert!(BitSize::try_from_u32::<FieldElement>(0).is_err());
assert!(BitSize::try_from_u32::<FieldElement>(256).is_err());
}
#[test]
#[should_panic = "memory offset overflow"]
fn memory_offset_overflow() {
let addr = MemoryAddress::direct(u32::MAX);
let _ = addr.offset(1);
}
}
#[cfg(feature = "arb")]
mod prop_tests {
use proptest::arbitrary::Arbitrary;
use proptest::prelude::*;
use crate::lengths::SemanticLength;
use super::{BitSize, HeapValueType};
impl Arbitrary for HeapValueType {
type Parameters = ();
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
let leaf = any::<BitSize>().prop_map(HeapValueType::Simple);
leaf.prop_recursive(2, 3, 2, |inner| {
prop_oneof![
(prop::collection::vec(inner.clone(), 1..3), any::<u32>()).prop_map(
|(value_types, size)| {
HeapValueType::Array { value_types, size: SemanticLength(size) }
}
),
(prop::collection::vec(inner, 1..3))
.prop_map(|value_types| { HeapValueType::Vector { value_types } }),
]
})
.boxed()
}
}
}