Skip to content

Commit

Permalink
feat(ssa): Simple serialization of unoptimized SSA to file (#5679)
Browse files Browse the repository at this point in the history
# Description

## Problem\*

Resolves <!-- Link to GitHub Issue -->

No issue as this essentially extends our ability to emit the SSA from
just stdout to also a file.

## Summary\*

I enabled using serde on our `Ssa` object. This initial serialization
only serializes fields that are deemed necessary for reading the SSA
instructions as we expect for something like `show-ssa`. If we deem that
more information from the `DataFlowGraph` would be useful we can always
add those in follow-ups.

A basic serialization roundtrip test under
`compiler/noirc_evaluator/src/ssa/ssa_gen/program.rs` has been added as
well.

## Additional Context



## Documentation\*

Check one:
- [ ] No documentation needed.
- [ ] Documentation included in this PR.
- [x] **[For Experimental Features]** Documentation to be submitted in a
separate PR.

# PR Checklist\*

- [X] I have tested the changes locally.
- [X] I have formatted the changes with [Prettier](https://prettier.io/)
and/or `cargo fmt` on default settings.

---------

Co-authored-by: jfecher <jake@aztecprotocol.com>
  • Loading branch information
vezenovm and jfecher authored Aug 6, 2024
1 parent 2b18151 commit 07ea107
Show file tree
Hide file tree
Showing 20 changed files with 226 additions and 27 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.

16 changes: 13 additions & 3 deletions compiler/noirc_driver/src/lib.rs
Original file line number Diff line number Diff line change
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)]
pub emit_ssa: bool,

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

Expand Down Expand Up @@ -548,8 +554,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
|| options.emit_ssa;

if !force_compile && hashes_match {
info!("Program matches existing artifact, returning early");
Expand All @@ -566,6 +575,7 @@ pub fn compile_no_check(
} else {
ExpressionWidth::default()
},
emit_ssa: if options.emit_ssa { Some(context.package_build_path.clone()) } else { None },
};

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 @@ pub struct SsaEvaluatorOptions {

/// 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 @@ pub(crate) fn optimize_into_acir(
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 @@ -346,8 +355,18 @@ impl SsaBuilder {
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 @@ impl SsaBuilder {
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 @@ use super::{
/// 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 Down Expand Up @@ -64,7 +65,7 @@ impl std::fmt::Display for BinaryOp {
}

/// 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
Loading

0 comments on commit 07ea107

Please sign in to comment.