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(ssa): Simple serialization of unoptimized SSA to file #5679

Merged
merged 20 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from 14 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
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.

24 changes: 18 additions & 6 deletions compiler/noirc_driver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use noirc_frontend::monomorphization::{
};
use noirc_frontend::node_interner::FuncId;
use noirc_frontend::token::SecondaryAttribute;
use std::path::Path;
use std::path::{Path, PathBuf};
use tracing::info;

mod abi_gen;
Expand Down Expand Up @@ -66,10 +66,16 @@ pub struct CompileOptions {
#[arg(long = "force")]
pub force_compile: bool,

/// Emit debug information for the intermediate SSA IR
/// Emit debug information for the intermediate SSA IR to stdout
#[arg(long, hide = true)]
pub show_ssa: bool,

/// Emit the unoptimized SSA IR to file.
/// The IR will be dumped into the workspace target directory,
/// under `[compiled-package].ssa.json`.
#[arg(long, hide = true)]
vezenovm marked this conversation as resolved.
Show resolved Hide resolved
pub emit_ssa: bool,

#[arg(long, hide = true)]
pub show_brillig: bool,

Expand Down Expand Up @@ -312,6 +318,7 @@ pub fn compile_main(
crate_id: CrateId,
options: &CompileOptions,
cached_program: Option<CompiledProgram>,
emit_ssa: Option<PathBuf>,
jfecher marked this conversation as resolved.
Show resolved Hide resolved
) -> CompilationResult<CompiledProgram> {
let (_, mut warnings) = check_crate(context, crate_id, options)?;

Expand All @@ -325,7 +332,7 @@ pub fn compile_main(
})?;

let compiled_program =
compile_no_check(context, options, main, cached_program, options.force_compile)
compile_no_check(context, options, main, cached_program, options.force_compile, emit_ssa)
.map_err(FileDiagnostic::from)?;

let compilation_warnings = vecmap(compiled_program.warnings.clone(), FileDiagnostic::from);
Expand Down Expand Up @@ -427,7 +434,7 @@ fn compile_contract_inner(
continue;
}

let function = match compile_no_check(context, options, function_id, None, true) {
let function = match compile_no_check(context, options, function_id, None, true, None) {
Ok(function) => function,
Err(new_error) => {
errors.push(FileDiagnostic::from(new_error));
Expand Down Expand Up @@ -533,6 +540,7 @@ pub fn compile_no_check(
main_function: FuncId,
cached_program: Option<CompiledProgram>,
force_compile: bool,
emit_ssa: Option<PathBuf>,
) -> Result<CompiledProgram, CompileError> {
let program = if options.instrument_debug {
monomorphize_debug(main_function, &mut context.def_interner, &context.debug_instrumenter)?
Expand All @@ -548,8 +556,11 @@ pub fn compile_no_check(

// If user has specified that they want to see intermediate steps printed then we should
// force compilation even if the program hasn't changed.
let force_compile =
force_compile || options.print_acir || options.show_brillig || options.show_ssa;
let force_compile = force_compile
|| options.print_acir
|| options.show_brillig
|| options.show_ssa
|| emit_ssa.is_some();

if !force_compile && hashes_match {
info!("Program matches existing artifact, returning early");
Expand All @@ -566,6 +577,7 @@ pub fn compile_no_check(
} else {
ExpressionWidth::default()
},
emit_ssa,
};

let SsaProgramArtifact { program, debug, warnings, names, error_types, .. } =
Expand Down
2 changes: 2 additions & 0 deletions compiler/noirc_evaluator/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ thiserror.workspace = true
num-bigint = "0.4"
im.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_with = "3.2.0"
tracing.workspace = true
chrono = "0.4.37"

Expand Down
41 changes: 40 additions & 1 deletion compiler/noirc_evaluator/src/ssa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
//! This module heavily borrows from Cranelift
#![allow(dead_code)]

use std::collections::{BTreeMap, BTreeSet};
use std::{
collections::{BTreeMap, BTreeSet},
fs::File,
io::Write,
path::{Path, PathBuf},
};

use crate::errors::{RuntimeError, SsaReport};
use acvm::{
Expand Down Expand Up @@ -56,6 +61,9 @@

/// Width of expressions to be used for ACIR
pub expression_width: ExpressionWidth,

/// Dump the unoptimized SSA to the supplied path if it exists
pub emit_ssa: Option<PathBuf>,
}

pub(crate) struct ArtifactsAndWarnings(Artifacts, Vec<SsaReport>);
Expand All @@ -76,6 +84,7 @@
options.enable_ssa_logging,
options.force_brillig_output,
options.print_codegen_timings,
&options.emit_ssa,
)?
.run_pass(Ssa::defunctionalize, "After Defunctionalization:")
.run_pass(Ssa::remove_paired_rc, "After Removing Paired rc_inc & rc_decs:")
Expand Down Expand Up @@ -108,7 +117,7 @@
.run_pass(Ssa::array_set_optimization, "After Array Set Optimizations:")
.finish();

let ssa_level_warnings = ssa.check_for_underconstrained_values();

Check warning on line 120 in compiler/noirc_evaluator/src/ssa.rs

View workflow job for this annotation

GitHub Actions / Code

Unknown word (underconstrained)
let brillig = time("SSA to Brillig", options.print_codegen_timings, || {
ssa.to_brillig(options.enable_brillig_logging)
});
Expand Down Expand Up @@ -346,8 +355,18 @@
print_ssa_passes: bool,
force_brillig_runtime: bool,
print_codegen_timings: bool,
emit_ssa: &Option<PathBuf>,
) -> Result<SsaBuilder, RuntimeError> {
let ssa = ssa_gen::generate_ssa(program, force_brillig_runtime)?;
if let Some(emit_ssa) = emit_ssa {
let mut emit_ssa_dir = emit_ssa.clone();
// We expect the full package artifact path to be passed in here,
// and attempt to create the target directory if it does not exist.
emit_ssa_dir.pop();
create_named_dir(emit_ssa_dir.as_ref(), "target");
let ssa_path = emit_ssa.with_extension("ssa.json");
write_to_file(&serde_json::to_vec(&ssa).unwrap(), &ssa_path);
}
Ok(SsaBuilder { print_ssa_passes, print_codegen_timings, ssa }.print("Initial SSA:"))
}

Expand Down Expand Up @@ -378,3 +397,23 @@
self
}
}

fn create_named_dir(named_dir: &Path, name: &str) -> PathBuf {
std::fs::create_dir_all(named_dir)
.unwrap_or_else(|_| panic!("could not create the `{name}` directory"));

PathBuf::from(named_dir)
}

fn write_to_file(bytes: &[u8], path: &Path) {
let display = path.display();

let mut file = match File::create(path) {
Err(why) => panic!("couldn't create {display}: {why}"),
Ok(file) => file,
};

if let Err(why) = file.write_all(bytes) {
panic!("couldn't write to {display}: {why}");
}
}
5 changes: 3 additions & 2 deletions compiler/noirc_evaluator/src/ssa/function_builder/data_bus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use acvm::FieldElement;
use fxhash::FxHashMap as HashMap;
use noirc_frontend::ast;
use noirc_frontend::hir_def::function::FunctionSignature;
use serde::{Deserialize, Serialize};

use super::FunctionBuilder;

Expand Down Expand Up @@ -52,13 +53,13 @@ impl DataBusBuilder {
}
}

#[derive(Clone, Debug)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct CallData {
pub(crate) array_id: ValueId,
pub(crate) index_map: HashMap<ValueId, usize>,
}

#[derive(Clone, Default, Debug)]
#[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub(crate) struct DataBus {
pub(crate) call_data: Vec<CallData>,
pub(crate) return_data: Option<ValueId>,
Expand Down
3 changes: 2 additions & 1 deletion compiler/noirc_evaluator/src/ssa/ir/basic_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ use super::{
map::Id,
value::ValueId,
};
use serde::{Deserialize, Serialize};

/// A Basic block is a maximal collection of instructions
/// such that there are only jumps at the end of block
/// and one can only enter the block from the beginning.
///
/// This means that if one instruction is executed in a basic
/// block, then all instructions are executed. ie single-entry single-exit.
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
pub(crate) struct BasicBlock {
/// Parameters to the basic block.
parameters: Vec<ValueId>,
Expand Down
14 changes: 13 additions & 1 deletion compiler/noirc_evaluator/src/ssa/ir/dfg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,16 @@ use acvm::{acir::AcirField, FieldElement};
use fxhash::FxHashMap as HashMap;
use iter_extended::vecmap;
use noirc_errors::Location;
use serde::{Deserialize, Serialize};
use serde_with::serde_as;
use serde_with::DisplayFromStr;

/// The DataFlowGraph contains most of the actual data in a function including
/// its blocks, instructions, and values. This struct is largely responsible for
/// owning most data in a function and handing out Ids to this data that can be
/// shared without worrying about ownership.
#[derive(Debug, Default, Clone)]
#[serde_as]
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub(crate) struct DataFlowGraph {
/// All of the instructions in a function
instructions: DenseMap<Instruction>,
Expand All @@ -36,6 +40,7 @@ pub(crate) struct DataFlowGraph {
/// Currently, we need to define them in a better way
/// Call instructions require the func signature, but
/// other instructions may need some more reading on my part
#[serde_as(as = "HashMap<DisplayFromStr, _>")]
results: HashMap<InstructionId, Vec<ValueId>>,

/// Storage for all of the values defined in this
Expand All @@ -44,21 +49,25 @@ pub(crate) struct DataFlowGraph {

/// Each constant is unique, attempting to insert the same constant
/// twice will return the same ValueId.
#[serde(skip)]
constants: HashMap<(FieldElement, Type), ValueId>,

/// Contains each function that has been imported into the current function.
/// A unique `ValueId` for each function's [`Value::Function`] is stored so any given FunctionId
/// will always have the same ValueId within this function.
#[serde(skip)]
functions: HashMap<FunctionId, ValueId>,

/// Contains each intrinsic that has been imported into the current function.
/// This map is used to ensure that the ValueId for any given intrinsic is always
/// represented by only 1 ValueId within this function.
#[serde(skip)]
intrinsics: HashMap<Intrinsic, ValueId>,

/// Contains each foreign function that has been imported into the current function.
/// This map is used to ensure that the ValueId for any given foreign function is always
/// represented by only 1 ValueId within this function.
#[serde(skip)]
foreign_functions: HashMap<String, ValueId>,

/// All blocks in a function
Expand All @@ -67,6 +76,7 @@ pub(crate) struct DataFlowGraph {
/// Debugging information about which `ValueId`s have had their underlying `Value` substituted
/// for that of another. This information is purely used for printing the SSA, and has no
/// material effect on the SSA itself.
#[serde(skip)]
replaced_value_ids: HashMap<ValueId, ValueId>,

/// Source location of each instruction for debugging and issuing errors.
Expand All @@ -79,8 +89,10 @@ pub(crate) struct DataFlowGraph {
///
/// Instructions inserted by internal SSA passes that don't correspond to user code
/// may not have a corresponding location.
#[serde(skip)]
locations: HashMap<InstructionId, CallStack>,

#[serde(skip)]
pub(crate) data_bus: DataBus,
}

Expand Down
5 changes: 3 additions & 2 deletions compiler/noirc_evaluator/src/ssa/ir/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::collections::BTreeSet;

use iter_extended::vecmap;
use noirc_frontend::monomorphization::ast::InlineType;
use serde::{Deserialize, Serialize};

use super::basic_block::BasicBlockId;
use super::dfg::DataFlowGraph;
Expand All @@ -10,7 +11,7 @@ use super::map::Id;
use super::types::Type;
use super::value::ValueId;

#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, Serialize, Deserialize)]
pub(crate) enum RuntimeType {
// A noir function, to be compiled in ACIR and executed by ACVM
Acir(InlineType),
Expand All @@ -37,7 +38,7 @@ impl RuntimeType {
/// All functions outside of the current function are seen as external.
/// To reference external functions its FunctionId can be used but this
/// cannot be checked for correctness until inlining is performed.
#[derive(Debug)]
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct Function {
/// The first basic block in the function
entry_block: BasicBlockId,
Expand Down
11 changes: 6 additions & 5 deletions compiler/noirc_evaluator/src/ssa/ir/instruction.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use serde::{Deserialize, Serialize};
use std::hash::{Hash, Hasher};

use acvm::{
Expand Down Expand Up @@ -47,7 +48,7 @@ pub(crate) type InstructionId = Id<Instruction>;
/// - Opcodes which have no function definition in the
/// source code and must be processed by the IR. An example
/// of this is println.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub(crate) enum Intrinsic {
ArrayLen,
AsSlice,
Expand Down Expand Up @@ -169,13 +170,13 @@ impl Intrinsic {
}

/// The endian-ness of bits when encoding values as bits in e.g. ToBits or ToRadix
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) enum Endian {
Big,
Little,
}

#[derive(Debug, PartialEq, Eq, Hash, Clone)]
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
/// Instructions are used to perform tasks.
/// The instructions that the IR is able to specify are listed below.
pub(crate) enum Instruction {
Expand Down Expand Up @@ -753,7 +754,7 @@ pub(crate) fn error_selector_from_type(typ: &ErrorType) -> ErrorSelector {
}
}

#[derive(Debug, PartialEq, Eq, Hash, Clone)]
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
pub(crate) enum ConstrainError {
// These are errors which have been hardcoded during SSA gen
Intrinsic(String),
Expand Down Expand Up @@ -795,7 +796,7 @@ pub(crate) enum InstructionResultType {
/// Since our IR needs to be in SSA form, it makes sense
/// to split up instructions like this, as we are sure that these instructions
/// will not be in the list of instructions for a basic block.
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
pub(crate) enum TerminatorInstruction {
/// Control flow
///
Expand Down
5 changes: 3 additions & 2 deletions compiler/noirc_evaluator/src/ssa/ir/instruction/binary.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use acvm::{acir::AcirField, FieldElement};
use serde::{Deserialize, Serialize};

use super::{
DataFlowGraph, Instruction, InstructionResultType, NumericType, SimplifyResult, Type, ValueId,
Expand All @@ -11,7 +12,7 @@
/// All binary operators are also only for numeric types. To implement
/// e.g. equality for a compound type like a struct, one must add a
/// separate Eq operation for each field and combine them later with And.
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, Serialize, Deserialize)]
pub(crate) enum BinaryOp {
/// Addition of lhs + rhs.
Add,
Expand All @@ -38,9 +39,9 @@
Or,
/// Bitwise xor (^)
Xor,
/// Bitshift left (<<)

Check warning on line 42 in compiler/noirc_evaluator/src/ssa/ir/instruction/binary.rs

View workflow job for this annotation

GitHub Actions / Code

Unknown word (Bitshift)
Shl,
/// Bitshift right (>>)

Check warning on line 44 in compiler/noirc_evaluator/src/ssa/ir/instruction/binary.rs

View workflow job for this annotation

GitHub Actions / Code

Unknown word (Bitshift)
Shr,
}

Expand All @@ -64,7 +65,7 @@
}

/// A binary instruction in the IR.
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
pub(crate) struct Binary {
/// Left hand side of the binary operation
pub(crate) lhs: ValueId,
Expand Down Expand Up @@ -281,7 +282,7 @@
return SimplifyResult::SimplifiedTo(zero);
}

// `two_pow_rhs` is limited to be at most `2 ^ {operand_bitsize - 1}` so it fits in `operand_type`.

Check warning on line 285 in compiler/noirc_evaluator/src/ssa/ir/instruction/binary.rs

View workflow job for this annotation

GitHub Actions / Code

Unknown word (bitsize)
let two_pow_rhs = FieldElement::from(2u128).pow(&rhs_const);
let two_pow_rhs = dfg.make_constant(two_pow_rhs, operand_type);
return SimplifyResult::SimplifiedToInstruction(Instruction::binary(
Expand Down Expand Up @@ -389,7 +390,7 @@
if int >= 0 {
FieldElement::from(int)
} else {
// We add an offset of `bit_size` bits to shift the negative values into the range [2^(bitsize-1), 2^bitsize)

Check warning on line 393 in compiler/noirc_evaluator/src/ssa/ir/instruction/binary.rs

View workflow job for this annotation

GitHub Actions / Code

Unknown word (bitsize)

Check warning on line 393 in compiler/noirc_evaluator/src/ssa/ir/instruction/binary.rs

View workflow job for this annotation

GitHub Actions / Code

Unknown word (bitsize)
let offset_int = (1i128 << bit_size) + int;
FieldElement::from(offset_int)
}
Expand Down
Loading
Loading