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: acir_formal_proofs #6947

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
287 changes: 287 additions & 0 deletions compiler/noirc_evaluator/src/acir_instruction_builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
use std::collections::BTreeSet;
use acvm::{
acir::{
circuit::{
Circuit, ExpressionWidth,
Program as AcirProgram
},
native_types::Witness,
},
FieldElement,
};

use crate::ssa::ssa_gen::Ssa;
use crate::ssa::ir::map::Id;
use crate::ssa::ir::function::Function;

use crate::ssa::{
function_builder::FunctionBuilder,
ir::{instruction::BinaryOp, types::Type},
};
use crate::brillig::Brillig;
use serde::{Deserialize, Serialize};

/// Represents artifacts generated from compiling an instruction
#[derive(Serialize, Deserialize)]
pub struct InstructionArtifacts {
/// Name of the instruction
pub instruction_name: String,

/// SSA representation formatted as "acir(inline) {...}"
pub formatted_ssa: String,

/// JSON serialized SSA
pub serialized_ssa: String,

/// Gzipped bytes of ACIR program
pub serialized_acir: Vec<u8>,
}
Comment on lines +26 to +38
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks quite specific to the use case you have. Could it live in the new crate you created?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks quite specific to the use case you have. Could it live in the new crate you created?

No, InstructionArtifacts needs to stay in noirc_evaluator since its implementation uses internal methods and structs of this crate. I don't know how to move this struct to the new crate without breaking the current architecture, if I understand you correctly

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I see even FunctionBuilder is only pub(crate). Maybe there is a case for making that public, as a more general purpose SSA related utility, than making InstructionArtifacts part of the public interface, given its limited scope. I could be wrong, just seeing different serialised SSA formats in it seems overly specific to how you are generating the inputs for the verification, not something anyone else will use.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can try to move the entire ssa_verification crate into the noirc_evaluator crate -- this way we wouldn't need to expose anything. However, I'm not sure if this is the right approach since ssa_verification is very specific.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, I don’t think that’s the right approach either. Maybe @TomAFrench can weigh in on how important it is to keep things private to the crate?


/// Represents the type of a variable in the instruction
#[derive(Debug)]
pub enum VariableType {
/// Field element type
Field,
/// Unsigned integer type
Unsigned,
/// Signed integer type
Signed
}

/// Represents a variable with its type and size information
pub struct Variable {
/// Type of the variable (Field, Unsigned, or Signed)
pub variable_type: VariableType,
/// Bit size of the variable (ignored for Field type)
pub variable_size: u32,
}

impl Variable {
/// Gets a string representation of the variable's type and size
pub fn get_name(&self) -> String {
return format!("{:?}_{}", self.variable_type, self.variable_size)
}
}

impl InstructionArtifacts {
/// Converts a Variable into its corresponding SSA Type
fn get_type(variable: &Variable) -> Type {
match variable.variable_type {
VariableType::Field => Type::field(),
VariableType::Signed => Type::signed(variable.variable_size),
VariableType::Unsigned => Type::unsigned(variable.variable_size)
}
}

/// Creates a new binary operation instruction artifact
fn new_binary(op: BinaryOp, instruction_name: String, first_variable: &Variable, second_variable: &Variable) -> Self {
let first_variable_type = Self::get_type(first_variable);
let second_variable_type = Self::get_type(second_variable);
let ssa = binary_function(op, first_variable_type, second_variable_type);
let serialized_ssa = &serde_json::to_string(&ssa).unwrap();
let formatted_ssa = format!("{}", ssa);

let program = ssa_to_acir_program(ssa);
let serialized_program = AcirProgram::serialize_program(&program);
let name = format!("{}_{}_{}", instruction_name, first_variable.get_name(), second_variable.get_name());

Self {
instruction_name: name,
formatted_ssa: formatted_ssa,
serialized_ssa: serialized_ssa.to_string(),
serialized_acir: serialized_program
}
}

/// Creates a new instruction artifact using a provided SSA generation function
fn new_by_func(ssa_generate_function: fn(Type) -> Ssa, instruction_name: String, variable: &Variable) -> Self {
let variable_type = Self::get_type(variable);
let ssa = ssa_generate_function(variable_type);
let serialized_ssa = &serde_json::to_string(&ssa).unwrap();
let formatted_ssa = format!("{}", ssa);

let program = ssa_to_acir_program(ssa);
let serialized_program = AcirProgram::serialize_program(&program);
let name = format!("{}_{}", instruction_name, variable.get_name());

Self {
instruction_name: name,
formatted_ssa: formatted_ssa,
serialized_ssa: serialized_ssa.to_string(),
serialized_acir: serialized_program
}
}

/// Creates a new constrain instruction artifact
pub fn new_constrain(variable: &Variable) -> Self {
return Self::new_by_func(constrain_function, "Constrain".into(), variable)
}

/// Creates a new NOT operation instruction artifact
pub fn new_not(variable: &Variable) -> Self {
return Self::new_by_func(not_function, "Not".into(), variable)
}

/// Creates a new range check instruction artifact
pub fn new_range_check(variable: &Variable) -> Self {
return Self::new_by_func(range_check_function, "RangeCheck".into(), variable)
}

/// Creates a new truncate instruction artifact
pub fn new_truncate(variable: &Variable) -> Self {
return Self::new_by_func(truncate_function, "Truncate".into(), variable)
}

/// Creates a new ADD operation instruction artifact
pub fn new_add(first_variable: &Variable, second_variable: &Variable) -> Self {
return Self::new_binary(BinaryOp::Add, "Binary::Add".into(), first_variable, second_variable);
}

/// Creates a new SUB operation instruction artifact
pub fn new_sub(first_variable: &Variable, second_variable: &Variable) -> Self {
return Self::new_binary(BinaryOp::Sub, "Binary::Sub".into(), first_variable, second_variable);
}

/// Creates a new XOR operation instruction artifact
pub fn new_xor(first_variable: &Variable, second_variable: &Variable) -> Self {
return Self::new_binary(BinaryOp::Xor, "Binary::Xor".into(), first_variable, second_variable);
}

/// Creates a new AND operation instruction artifact
pub fn new_and(first_variable: &Variable, second_variable: &Variable) -> Self {
return Self::new_binary(BinaryOp::And, "Binary::And".into(), first_variable, second_variable);
}

/// Creates a new OR operation instruction artifact
pub fn new_or(first_variable: &Variable, second_variable: &Variable) -> Self {
return Self::new_binary(BinaryOp::Or, "Binary::Or".into(), first_variable, second_variable);
}

/// Creates a new less than operation instruction artifact
pub fn new_lt(first_variable: &Variable, second_variable: &Variable) -> Self {
return Self::new_binary(BinaryOp::Lt, "Binary::Lt".into(), first_variable, second_variable);
}

/// Creates a new equals operation instruction artifact
pub fn new_eq(first_variable: &Variable, second_variable: &Variable) -> Self {
return Self::new_binary(BinaryOp::Eq, "Binary::Eq".into(), first_variable, second_variable);
}

/// Creates a new modulo operation instruction artifact
pub fn new_mod(first_variable: &Variable, second_variable: &Variable) -> Self {
return Self::new_binary(BinaryOp::Mod, "Binary::Mod".into(), first_variable, second_variable);
}

/// Creates a new multiply operation instruction artifact
pub fn new_mul(first_variable: &Variable, second_variable: &Variable) -> Self {
return Self::new_binary(BinaryOp::Mul, "Binary::Mul".into(), first_variable, second_variable);
}

/// Creates a new divide operation instruction artifact
pub fn new_div(first_variable: &Variable, second_variable: &Variable) -> Self {
return Self::new_binary(BinaryOp::Div, "Binary::Div".into(), first_variable, second_variable);
}

/// Creates a new shift left operation instruction artifact
pub fn new_shl(first_variable: &Variable, second_variable: &Variable) -> Self {
return Self::new_binary(BinaryOp::Shl, "Binary::Shl".into(), first_variable, second_variable);
}

/// Creates a new shift right operation instruction artifact
pub fn new_shr(first_variable: &Variable, second_variable: &Variable) -> Self {
return Self::new_binary(BinaryOp::Shr, "Binary::Shr".into(), first_variable, second_variable);
}
}

/// Converts SSA to ACIR program
fn ssa_to_acir_program(ssa: Ssa) -> AcirProgram<FieldElement> {
// third brillig names, fourth errors
let (acir_functions, brillig, _, _) = ssa
.into_acir(&Brillig::default(), ExpressionWidth::default())
.expect("Should compile manually written SSA into ACIR");

let mut functions: Vec<Circuit<FieldElement>> = Vec::new();

for acir_func in acir_functions.iter() {
let mut private_params: BTreeSet<Witness> = acir_func.input_witnesses.clone().into_iter().collect();
let ret_values: BTreeSet<Witness> = acir_func.return_witnesses.clone().into_iter().collect();
let circuit: Circuit<FieldElement>;
private_params.extend(ret_values.iter().cloned());
circuit = Circuit {
current_witness_index: acir_func.current_witness_index().witness_index(),
opcodes: acir_func.opcodes().to_vec(),
private_parameters: private_params.clone(),
..Circuit::<FieldElement>::default()
};
functions.push(circuit);
}
return AcirProgram { functions: functions, unconstrained_functions: brillig };
}

/// Creates an SSA function for binary operations
fn binary_function(op: BinaryOp, first_variable_type: Type, second_variable_type: Type) -> Ssa {
// returns v0 op v1
let main_id: Id<Function> = Id::new(0);
let mut builder = FunctionBuilder::new("main".into(), main_id);
let v0 = builder.add_parameter(first_variable_type);
let v1 = builder.add_parameter(second_variable_type);
let v2 = builder.insert_binary(v0, op, v1);
builder.terminate_with_return(vec![v2]);

let func = builder.finish();
// remove_bit_shifts replaces bit shifts with equivalent arithmetic operations
let cleared_func = func.remove_bit_shifts();
return cleared_func;
}

/// Creates an SSA function for constraint operations
fn constrain_function(variable_type: Type) -> Ssa {
// constrains v0 == v1, returns v1
let main_id: Id<Function> = Id::new(0);
let mut builder = FunctionBuilder::new("main".into(), main_id);

let v0 = builder.add_parameter(variable_type.clone());
let v1 = builder.add_parameter(variable_type);
builder.insert_constrain(v0, v1, None);
builder.terminate_with_return(vec![v1]);

return builder.finish();
}

/// Creates an SSA function for range check operations
fn range_check_function(variable_type: Type) -> Ssa {
let main_id: Id<Function> = Id::new(0);
let mut builder = FunctionBuilder::new("main".into(), main_id);

let v0 = builder.add_parameter(variable_type);
builder.insert_range_check(v0, 64, Some("Range Check failed".to_string()));
builder.terminate_with_return(vec![v0]);

return builder.finish()
}

/// Creates an SSA function for truncate operations
fn truncate_function(variable_type: Type) -> Ssa {
// truncate v0: field 10, 20?..
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the question mark?

Copy link
Author

@jewelofchaos9 jewelofchaos9 Jan 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the question mark?

I wasn't sure what the third argument (max_bit_size) of the Truncate opcode does. Fixed.

let main_id: Id<Function> = Id::new(0);
let mut builder = FunctionBuilder::new("main".into(), main_id);

let v0 = builder.add_parameter(variable_type);
let v1 = builder.insert_truncate(v0, 10, 20);
builder.terminate_with_return(vec![v1]);

return builder.finish();
}

/// Creates an SSA function for NOT operations
fn not_function(variable_type: Type) -> Ssa {
// returns not v0
let main_id: Id<Function> = Id::new(0);
let mut builder = FunctionBuilder::new("main".into(), main_id);

let v0 = builder.add_parameter(variable_type);
let v1 = builder.insert_not(v0);
builder.terminate_with_return(vec![v1]);

return builder.finish()
}
4 changes: 4 additions & 0 deletions compiler/noirc_evaluator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ pub mod ssa;

pub use ssa::create_program;
pub use ssa::ir::instruction::ErrorType;
pub mod acir_instruction_builder;
pub use acir_instruction_builder::{
InstructionArtifacts, VariableType, Variable
};

/// Trims leading whitespace from each line of the input string
#[cfg(test)]
Expand Down
2 changes: 1 addition & 1 deletion compiler/noirc_evaluator/src/ssa/ir/map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ impl<T> Id<T> {
///
/// This is private so that we can guarantee ids created from this function
/// point to valid T values in their external maps.
fn new(index: u32) -> Self {
pub(crate) fn new(index: u32) -> Self {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

It was changed in #6807 from pub(crate) to private. I can not use test_new instead, because of #[cfg(test)] attribute. I need Id for function building.

Self { index, _marker: std::marker::PhantomData }
}

Expand Down
1 change: 1 addition & 0 deletions compiler/noirc_evaluator/ssa_test/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Cargo.lock
12 changes: 12 additions & 0 deletions compiler/noirc_evaluator/ssa_test/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "ssa_test"
aakoshh marked this conversation as resolved.
Show resolved Hide resolved
version = "0.1.0"
edition = "2021"

[dependencies]
jewelofchaos9 marked this conversation as resolved.
Show resolved Hide resolved
noirc_evaluator = {path = "../"}
serde = "1.0.214"
serde_json = "1.0.120"
flate2 = "1.0.35"
clap = { version = "4.4.11", features = ["derive"] }
[workspace]
27 changes: 27 additions & 0 deletions compiler/noirc_evaluator/ssa_test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# SSA Test Generator

This tool generates test artifacts for formally verifying SSA instructions and their conversion to ACIR.

## Purpose

The test generator creates test cases for:

- Bitvector operations (up to 127 bits): add, sub, mul, mod, xor, and, div, eq, lt, not, etc.

Check warning on line 9 in compiler/noirc_evaluator/ssa_test/README.md

View workflow job for this annotation

GitHub Actions / Code

Unknown word (Bitvector)
- Shift operations (tested with smaller bit sizes 32 and 64): shl, shr
- Binary operations (32-bit): xor, and, or
- Field operations: add, mul, div
- Signed integer operations: div (126-bit)

Each test case generates:
- Formatted SSA representation
- Serialized ACIR output

## Usage

Run the generator with the desired output directory. The directory can be specified using the `-d` or `--dir` flag:

```bash
cargo run -- -d /path/to/output/directory/
```

DON'T FORGET TO CHANGE ARTIFACTS_PATH IN barretenberg/cpp/src/barretenberg/acir_formal_proofs/acir_loader.test.cpp TO THE NEW OUTPUT DIRECTORY.
Loading
Loading