Skip to content

Commit

Permalink
feat: SSA parser (#6489)
Browse files Browse the repository at this point in the history
Co-authored-by: Tom French <[email protected]>
  • Loading branch information
asterite and TomAFrench authored Nov 12, 2024
1 parent 82046be commit 21c9db5
Show file tree
Hide file tree
Showing 20 changed files with 2,910 additions and 1,029 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion compiler/noirc_evaluator/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ fxhash.workspace = true
iter-extended.workspace = true
thiserror.workspace = true
num-bigint = "0.4"
num-traits.workspace = true
im.workspace = true
serde.workspace = true
serde_json.workspace = true
Expand All @@ -31,7 +32,8 @@ cfg-if.workspace = true

[dev-dependencies]
proptest.workspace = true
similar-asserts.workspace = true

[features]
bn254 = ["noirc_frontend/bn254"]
bls12_381= []
bls12_381 = []
18 changes: 18 additions & 0 deletions compiler/noirc_evaluator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,21 @@ pub mod ssa;
pub mod brillig;

pub use ssa::create_program;

/// Trims leading whitespace from each line of the input string, according to
/// how much leading whitespace there is on the first non-empty line.
#[cfg(test)]
pub(crate) fn trim_leading_whitespace_from_lines(src: &str) -> String {
let mut lines = src.trim_end().lines();
let mut first_line = lines.next().unwrap();
while first_line.is_empty() {
first_line = lines.next().unwrap();
}
let indent = first_line.len() - first_line.trim_start().len();
let mut result = first_line.trim_start().to_string();
for line in lines {
result.push('\n');
result.push_str(&line[indent..]);
}
result
}
1 change: 1 addition & 0 deletions compiler/noirc_evaluator/src/ssa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ mod checks;
pub(super) mod function_builder;
pub mod ir;
mod opt;
mod parser;
pub mod ssa_gen;

pub struct SsaEvaluatorOptions {
Expand Down
10 changes: 10 additions & 0 deletions compiler/noirc_evaluator/src/ssa/function_builder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,16 @@ impl FunctionBuilder {
.first()
}

pub(crate) fn insert_mutable_array_set(
&mut self,
array: ValueId,
index: ValueId,
value: ValueId,
) -> ValueId {
self.insert_instruction(Instruction::ArraySet { array, index, value, mutable: true }, None)
.first()
}

/// Insert an instruction to increment an array's reference count. This only has an effect
/// in unconstrained code where arrays are reference counted and copy on write.
pub(crate) fn insert_inc_rc(&mut self, value: ValueId) {
Expand Down
50 changes: 42 additions & 8 deletions compiler/noirc_evaluator/src/ssa/ir/printer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,16 @@ fn value(function: &Function, id: ValueId) -> String {
}
Value::Function(id) => id.to_string(),
Value::Intrinsic(intrinsic) => intrinsic.to_string(),
Value::Array { array, .. } => {
Value::Array { array, typ } => {
let elements = vecmap(array, |element| value(function, *element));
format!("[{}]", elements.join(", "))
let element_types = &typ.clone().element_types();
let element_types_str =
element_types.iter().map(|typ| typ.to_string()).collect::<Vec<String>>().join(", ");
if element_types.len() == 1 {
format!("[{}] of {}", elements.join(", "), element_types_str)
} else {
format!("[{}] of ({})", elements.join(", "), element_types_str)
}
}
Value::Param { .. } | Value::Instruction { .. } | Value::ForeignFunction(_) => {
id.to_string()
Expand Down Expand Up @@ -120,7 +127,11 @@ pub(crate) fn display_terminator(
)
}
Some(TerminatorInstruction::Return { return_values, .. }) => {
writeln!(f, " return {}", value_list(function, return_values))
if return_values.is_empty() {
writeln!(f, " return")
} else {
writeln!(f, " return {}", value_list(function, return_values))
}
}
None => writeln!(f, " (no terminator instruction)"),
}
Expand All @@ -140,12 +151,13 @@ pub(crate) fn display_instruction(
write!(f, "{} = ", value_list(function, results))?;
}

display_instruction_inner(function, &function.dfg[instruction], f)
display_instruction_inner(function, &function.dfg[instruction], results, f)
}

fn display_instruction_inner(
function: &Function,
instruction: &Instruction,
results: &[ValueId],
f: &mut Formatter,
) -> Result {
let show = |id| value(function, id);
Expand All @@ -169,18 +181,29 @@ fn display_instruction_inner(
}
}
Instruction::Call { func, arguments } => {
writeln!(f, "call {}({})", show(*func), value_list(function, arguments))
let arguments = value_list(function, arguments);
writeln!(f, "call {}({}){}", show(*func), arguments, result_types(function, results))
}
Instruction::Allocate => {
writeln!(f, "allocate{}", result_types(function, results))
}
Instruction::Load { address } => {
writeln!(f, "load {}{}", show(*address), result_types(function, results))
}
Instruction::Allocate => writeln!(f, "allocate"),
Instruction::Load { address } => writeln!(f, "load {}", show(*address)),
Instruction::Store { address, value } => {
writeln!(f, "store {} at {}", show(*value), show(*address))
}
Instruction::EnableSideEffectsIf { condition } => {
writeln!(f, "enable_side_effects {}", show(*condition))
}
Instruction::ArrayGet { array, index } => {
writeln!(f, "array_get {}, index {}", show(*array), show(*index))
writeln!(
f,
"array_get {}, index {}{}",
show(*array),
show(*index),
result_types(function, results)
)
}
Instruction::ArraySet { array, index, value, mutable } => {
let array = show(*array);
Expand Down Expand Up @@ -211,6 +234,17 @@ fn display_instruction_inner(
}
}

fn result_types(function: &Function, results: &[ValueId]) -> String {
let types = vecmap(results, |result| function.dfg.type_of_value(*result).to_string());
if types.is_empty() {
String::new()
} else if types.len() == 1 {
format!(" -> {}", types[0])
} else {
format!(" -> ({})", types.join(", "))
}
}

/// Tries to extract a constant string from an error payload.
pub(crate) fn try_to_extract_string_from_error_payload(
error_selector: ErrorSelector,
Expand Down
6 changes: 5 additions & 1 deletion compiler/noirc_evaluator/src/ssa/ir/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,11 @@ impl std::fmt::Display for Type {
Type::Reference(element) => write!(f, "&mut {element}"),
Type::Array(element, length) => {
let elements = vecmap(element.iter(), |element| element.to_string());
write!(f, "[{}; {length}]", elements.join(", "))
if elements.len() == 1 {
write!(f, "[{}; {length}]", elements.join(", "))
} else {
write!(f, "[({}); {length}]", elements.join(", "))
}
}
Type::Slice(element) => {
let elements = vecmap(element.iter(), |element| element.to_string());
Expand Down
154 changes: 33 additions & 121 deletions compiler/noirc_evaluator/src/ssa/opt/array_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,135 +206,47 @@ fn make_mutable(

#[cfg(test)]
mod tests {
use std::sync::Arc;

use im::vector;
use noirc_frontend::monomorphization::ast::InlineType;

use crate::ssa::{
function_builder::FunctionBuilder,
ir::{
function::RuntimeType,
instruction::{BinaryOp, Instruction},
map::Id,
types::Type,
},
};
use crate::ssa::{opt::assert_normalized_ssa_equals, Ssa};

#[test]
fn array_set_in_loop_with_conditional_clone() {
// We want to make sure that we do not mark a single array set mutable which is loaded
// from and cloned in a loop. If the array is inadvertently marked mutable, and is cloned in a previous iteration
// of the loop, its clone will also be altered.
//
// brillig fn main f0 {
// b0():
// v3 = allocate
// store [[Field 0, Field 0, Field 0, Field 0, Field 0], [Field 0, Field 0, Field 0, Field 0, Field 0]] at v3
// v4 = allocate
// store [[Field 0, Field 0, Field 0, Field 0, Field 0], [Field 0, Field 0, Field 0, Field 0, Field 0]] at v4
// jmp b1(u32 0)
// b1(v6: u32):
// v8 = lt v6, u32 5
// jmpif v8 then: b3, else: b2
// b3():
// v9 = eq v6, u32 5
// jmpif v9 then: b4, else: b5
// b4():
// v10 = load v3
// store v10 at v4
// jmp b5()
// b5():
// v11 = load v3
// v13 = array_get v11, index Field 0
// v14 = array_set v13, index v6, value Field 20
// v15 = array_set v11, index v6, value v14
// store v15 at v3
// v17 = add v6, u32 1
// jmp b1(v17)
// b2():
// return
// }
let main_id = Id::test_new(0);
let mut builder = FunctionBuilder::new("main".into(), main_id);
builder.set_runtime(RuntimeType::Brillig(InlineType::default()));

let array_type = Type::Array(Arc::new(vec![Type::field()]), 5);
let zero = builder.field_constant(0u128);
let array_constant =
builder.array_constant(vector![zero, zero, zero, zero, zero], array_type.clone());
let nested_array_type = Type::Array(Arc::new(vec![array_type.clone()]), 2);
let nested_array_constant = builder
.array_constant(vector![array_constant, array_constant], nested_array_type.clone());

let v3 = builder.insert_allocate(array_type.clone());

builder.insert_store(v3, nested_array_constant);

let v4 = builder.insert_allocate(array_type.clone());
builder.insert_store(v4, nested_array_constant);

let b1 = builder.insert_block();
let zero_u32 = builder.numeric_constant(0u128, Type::unsigned(32));
builder.terminate_with_jmp(b1, vec![zero_u32]);

// Loop header
builder.switch_to_block(b1);
let v5 = builder.add_block_parameter(b1, Type::unsigned(32));
let five = builder.numeric_constant(5u128, Type::unsigned(32));
let v8 = builder.insert_binary(v5, BinaryOp::Lt, five);

let b2 = builder.insert_block();
let b3 = builder.insert_block();
let b4 = builder.insert_block();
let b5 = builder.insert_block();
builder.terminate_with_jmpif(v8, b3, b2);

// Loop body
// b3 is the if statement conditional
builder.switch_to_block(b3);
let two = builder.numeric_constant(5u128, Type::unsigned(32));
let v9 = builder.insert_binary(v5, BinaryOp::Eq, two);
builder.terminate_with_jmpif(v9, b4, b5);

// b4 is the rest of the loop after the if statement
builder.switch_to_block(b4);
let v10 = builder.insert_load(v3, nested_array_type.clone());
builder.insert_store(v4, v10);
builder.terminate_with_jmp(b5, vec![]);

builder.switch_to_block(b5);
let v11 = builder.insert_load(v3, nested_array_type.clone());
let twenty = builder.field_constant(20u128);
let v13 = builder.insert_array_get(v11, zero, array_type.clone());
let v14 = builder.insert_array_set(v13, v5, twenty);
let v15 = builder.insert_array_set(v11, v5, v14);

builder.insert_store(v3, v15);
let one = builder.numeric_constant(1u128, Type::unsigned(32));
let v17 = builder.insert_binary(v5, BinaryOp::Add, one);
builder.terminate_with_jmp(b1, vec![v17]);

builder.switch_to_block(b2);
builder.terminate_with_return(vec![]);
let src = "
brillig(inline) fn main f0 {
b0():
v1 = allocate -> &mut [Field; 5]
store [[Field 0, Field 0, Field 0, Field 0, Field 0] of Field, [Field 0, Field 0, Field 0, Field 0, Field 0] of Field] of [Field; 5] at v1
v6 = allocate -> &mut [Field; 5]
store [[Field 0, Field 0, Field 0, Field 0, Field 0] of Field, [Field 0, Field 0, Field 0, Field 0, Field 0] of Field] of [Field; 5] at v6
jmp b1(u32 0)
b1(v0: u32):
v12 = lt v0, u32 5
jmpif v12 then: b3, else: b2
b3():
v13 = eq v0, u32 5
jmpif v13 then: b4, else: b5
b4():
v14 = load v1 -> [[Field; 5]; 2]
store v14 at v6
jmp b5()
b5():
v15 = load v1 -> [[Field; 5]; 2]
v16 = array_get v15, index Field 0 -> [Field; 5]
v18 = array_set v16, index v0, value Field 20
v19 = array_set v15, index v0, value v18
store v19 at v1
v21 = add v0, u32 1
jmp b1(v21)
b2():
return
}
";
let ssa = Ssa::from_str(src).unwrap();

let ssa = builder.finish();
// We expect the same result as above
let ssa = ssa.array_set_optimization();

let main = ssa.main();
assert_eq!(main.reachable_blocks().len(), 6);

let array_set_instructions = main.dfg[b5]
.instructions()
.iter()
.filter(|instruction| matches!(&main.dfg[**instruction], Instruction::ArraySet { .. }))
.collect::<Vec<_>>();

assert_eq!(array_set_instructions.len(), 2);
if let Instruction::ArraySet { mutable, .. } = &main.dfg[*array_set_instructions[0]] {
// The single array set should not be marked mutable
assert!(!mutable);
}
assert_normalized_ssa_equals(ssa, src);
}
}
Loading

0 comments on commit 21c9db5

Please sign in to comment.