Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions Cargo.lock

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

20 changes: 16 additions & 4 deletions acvm-repo/acir/src/circuit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ use msgpack_tagged::MsgpackTagged;
pub use opcodes::Opcode;
use thiserror::Error;

use std::{collections::HashMap, io::prelude::*, num::ParseIntError, str::FromStr};
use std::{
collections::{BTreeMap, HashMap},
io::prelude::*,
num::ParseIntError,
str::FromStr,
};

use base64::Engine;
use flate2::Compression;
Expand Down Expand Up @@ -341,13 +346,14 @@ impl<F: AcirField + for<'a> Deserialize<'a> + MsgpackTagged> Program<F> {

impl<F: AcirField> std::fmt::Display for Circuit<F> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
display_circuit(self, None, f)
display_circuit(self, None, None, f)
}
}

pub fn display_circuit<F: AcirField>(
circuit: &Circuit<F>,
error_types: Option<&HashMap<ErrorSelector, String>>,
annotations: Option<&BTreeMap<usize, String>>,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
let write_witness_indices =
Expand Down Expand Up @@ -382,6 +388,10 @@ pub fn display_circuit<F: AcirField>(
circuit.assert_messages.iter().cloned().collect::<HashMap<_, _>>();

for (index, opcode) in circuit.opcodes.iter().enumerate() {
if let Some(annotation) = annotations.and_then(|annotations| annotations.get(&index)) {
writeln!(f, "// {annotation}")?;
}

display_opcode(opcode, Some(&circuit.return_values), f)?;

if let Some(error_types) = error_types {
Expand All @@ -405,18 +415,20 @@ impl<F: AcirField> std::fmt::Debug for Circuit<F> {

impl<F: AcirField> std::fmt::Display for Program<F> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
display_program(self, None, f)
display_program(self, None, None, f)
}
}

pub fn display_program<F: AcirField>(
program: &Program<F>,
error_types: Option<&HashMap<ErrorSelector, String>>,
annotations: Option<&[BTreeMap<usize, String>]>,
f: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
for (func_index, function) in program.functions.iter().enumerate() {
writeln!(f, "func {func_index}")?;
display_circuit(function, error_types, f)?;
let annotations = annotations.and_then(|annotations: &[_]| annotations.get(func_index));
display_circuit(function, error_types, annotations, f)?;
writeln!(f)?;
}
for (func_index, function) in program.unconstrained_functions.iter().enumerate() {
Expand Down
58 changes: 53 additions & 5 deletions compiler/noirc_driver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use clap::Args;
use fm::{FileId, FileManager};
use iter_extended::vecmap;
use noirc_abi::{AbiErrorType, AbiNamedValue, AbiParameter, AbiType};
use noirc_artifacts::annotations::program_opcode_annotations;
use noirc_artifacts::contract::{CompiledContract, CompiledContractOutputs, ContractFunction};
use noirc_artifacts::debug::{DebugFile, DebugInfo, FunctionLocation};
use noirc_artifacts::program::CompiledProgram;
Expand Down Expand Up @@ -155,6 +156,11 @@ pub struct CompileOptions {
#[arg(long)]
pub print_acir: bool,

/// When displaying the ACIR (see `--print-acir`), annotate each opcode with the
/// Noir source location and code snippet it was compiled from.
#[arg(long)]
pub with_acir_locations: bool,

/// Pretty print benchmark times of each code generation pass
#[arg(long, hide = true)]
pub benchmark_codegen: bool,
Expand Down Expand Up @@ -316,6 +322,7 @@ impl Default for CompileOptions {
show_brillig: false,
show_brillig_opcode_advisories: false,
print_acir: false,
with_acir_locations: false,
benchmark_codegen: false,
deny_warnings: false,
silence_warnings: false,
Expand Down Expand Up @@ -525,7 +532,10 @@ pub fn compile_main(

if options.print_acir {
noirc_errors::println_to_stdout!("Compiled ACIR for main:");
noirc_errors::println_to_stdout!("{}", display_compiled_program(&compiled_program));
noirc_errors::println_to_stdout!(
"{}",
display_compiled_program(&compiled_program, options.with_acir_locations)
);
}

Ok((compiled_program, warnings))
Expand Down Expand Up @@ -590,7 +600,14 @@ pub fn compile_contract(
"Compiled ACIR for {}::{} (non-transformed):",
compiled_contract.name, contract_function.name
);
println!("{}", contract_function.bytecode);
println!(
"{}",
display_contract_function(
contract_function,
&compiled_contract.file_map,
options.with_acir_locations
)
);
}
}
Ok((compiled_contract, warnings))
Expand Down Expand Up @@ -1035,17 +1052,48 @@ fn ssa_report_to_custom_diagnostic(error: SsaReport) -> CustomDiagnostic {
}
}

pub fn display_compiled_program(program: &CompiledProgram) -> String {
ProgramDisplay { program: &program.program, error_types: &program.abi.error_types }.to_string()
pub fn display_compiled_program(program: &CompiledProgram, with_locations: bool) -> String {
let annotations =
program_annotations(&program.program, &program.debug, &program.file_map, with_locations);
ProgramDisplay { program: &program.program, error_types: &program.abi.error_types, annotations }
.to_string()
}

fn display_contract_function(
function: &ContractFunction,
file_map: &BTreeMap<FileId, DebugFile>,
with_locations: bool,
) -> String {
let annotations =
program_annotations(&function.bytecode, &function.debug, file_map, with_locations);
let error_types = BTreeMap::new();
ProgramDisplay { program: &function.bytecode, error_types: &error_types, annotations }
.to_string()
}

/// Builds per-circuit opcode annotations for every ACIR function of a program, or
/// `None` when source locations were not requested.
fn program_annotations<F: AcirField>(
program: &Program<F>,
debug: &[DebugInfo],
file_map: &BTreeMap<FileId, DebugFile>,
with_locations: bool,
) -> Option<Vec<BTreeMap<usize, String>>> {
with_locations.then(|| program_opcode_annotations(program, debug, file_map))
}

/// Formats an ACIR [Program] together with its ABI error types so that any static
/// assertion payloads embedded in the program are rendered as a `// message` comment
/// next to the relevant ACIR/Brillig opcode. This is the same display used by
/// `nargo compile --print-acir`.
///
/// When `annotations` are present (see [program_opcode_annotations]), each annotated
/// ACIR opcode is additionally preceded by a `// file:line:col: snippet` comment
/// describing the Noir source it was compiled from.
struct ProgramDisplay<'a, F: AcirField> {
program: &'a Program<F>,
error_types: &'a BTreeMap<ErrorSelector, AbiErrorType>,
annotations: Option<Vec<BTreeMap<usize, String>>>,
}

impl<F: AcirField> std::fmt::Display for ProgramDisplay<'_, F> {
Expand All @@ -1061,6 +1109,6 @@ impl<F: AcirField> std::fmt::Display for ProgramDisplay<'_, F> {
}
})
.collect::<HashMap<_, _>>();
display_program(self.program, Some(&error_types), f)
display_program(self.program, Some(&error_types), self.annotations.as_deref(), f)
}
}
139 changes: 137 additions & 2 deletions compiler/noirc_driver/tests/print_acir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ fn print_acir_renders_static_assertion_payload() {
"#;

let program = compile(source, false);
let displayed = display_compiled_program(&program);
let displayed = display_compiled_program(&program, false);

insta::assert_snapshot!(displayed, @r"
func 0
Expand Down Expand Up @@ -119,6 +119,141 @@ fn dynamic_custom_error_type_is_preserved() {
);
}

#[test]
fn print_acir_with_locations_annotates_opcode_runs() {
let source = r#"
fn main(x: u32, y: u32) -> pub u32 {
let sum = x * y;
assert(sum != 10);
sum
}
"#;

let program = compile(source, false);
let displayed = display_compiled_program(&program, true);

// Each run of opcodes compiled from the same source span gets a single
// `// file:line:col: snippet` comment above it.
insta::assert_snapshot!(displayed, @r"
func 0
private parameters: [w0, w1]
public parameters: []
return values: [w2]
BLACKBOX::RANGE input: w0, bits: 32
BLACKBOX::RANGE input: w1, bits: 32
// main.nr:3:19: x * y
ASSERT w3 = w0*w1
BLACKBOX::RANGE input: w3, bits: 32 // attempt to multiply with overflow
// main.nr:4:9: assert(sum != 10)
BRILLIG CALL func: 0, predicate: 1, inputs: [w3 - 10], outputs: [w4]
ASSERT 0 = w3*w4 - 10*w4 - 1
// no source location
ASSERT w2 = w3

unconstrained func 0: directive_invert
0: @21 = const u32 1
1: @20 = const u32 0
2: @0 = calldata copy [@20; @21]
3: @2 = const field 0
4: @3 = field eq @0, @2
5: jump if @3 to 8
6: @1 = const field 1
7: @0 = field field_div @1, @0
8: stop @[@20; @21]
");
}

#[test]
fn print_acir_with_locations_shows_inlined_caller_chain() {
let source = r#"
fn square(v: u32) -> u32 {
v * v
}

fn main(x: u32) -> pub u32 {
square(x)
}
"#;

let program = compile(source, false);
let displayed = display_compiled_program(&program, true);

// Opcodes from the inlined `square` body are annotated with the location
// inside `square` plus a `(via ...)` trail back to the call site in `main`.
insta::assert_snapshot!(displayed, @r"
func 0
private parameters: [w0]
public parameters: []
return values: [w1]
BLACKBOX::RANGE input: w0, bits: 32
// main.nr:3:9: v * v (via main.nr:7:9)
ASSERT w2 = w0*w0
BLACKBOX::RANGE input: w2, bits: 32 // attempt to multiply with overflow
// no source location
ASSERT w1 = w2
");
}

#[test]
fn print_acir_with_locations_collapses_multi_line_snippets() {
let source = r#"
fn main(x: u32) -> pub u32 {
let y = x
* 3
* x;
y
}
"#;

let program = compile(source, false);
let displayed = display_compiled_program(&program, true);

// A span covering several source lines is collapsed to a single line,
// so the two nested multiplication spans remain distinguishable.
insta::assert_snapshot!(displayed, @r"
func 0
private parameters: [w0]
public parameters: []
return values: [w1]
BLACKBOX::RANGE input: w0, bits: 32
// main.nr:3:17: x * 3
ASSERT w2 = 3*w0
BLACKBOX::RANGE input: w2, bits: 32 // attempt to multiply with overflow
// main.nr:3:17: x * 3 * x
ASSERT w3 = w0*w2
BLACKBOX::RANGE input: w3, bits: 32 // attempt to multiply with overflow
// no source location
ASSERT w1 = w3
");
}

#[test]
fn print_acir_with_locations_round_trips_through_parser() {
let source = r#"
fn main(x: u32, y: u32) -> pub u32 {
let sum = x * y;
assert(sum != 10);
sum
}
"#;

let program = compile(source, false);

// Location annotations are `//` comments, which the ACIR parser skips, so
// the annotated display must parse to exactly the same circuit as the
// unannotated one.
let parse = |displayed: &str| {
let circuit_text = displayed
.strip_prefix("func 0\n")
.expect("displayed program should start with the `func 0` header");
Circuit::from_str(circuit_text).expect("displayed ACIR should be parseable")
};

let annotated = parse(&display_compiled_program(&program, true));
let unannotated = parse(&display_compiled_program(&program, false));
assert_eq!(annotated, unannotated);
}

#[test]
fn print_acir_renders_brillig_assertion_payload() {
let source = r#"
Expand All @@ -128,7 +263,7 @@ fn print_acir_renders_brillig_assertion_payload() {
"#;

let program = compile(source, true);
let displayed = display_compiled_program(&program);
let displayed = display_compiled_program(&program, false);

insta::assert_snapshot!(displayed, @r#"
func 0
Expand Down
1 change: 1 addition & 0 deletions tooling/inspector/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ serde_json.workspace = true
color-eyre.workspace = true
const_format.workspace = true
acir.workspace = true
fm.workspace = true
noirc_artifacts.workspace = true
noirc_artifacts_info.workspace = true
noir_artifact_cli.workspace = true
Expand Down
Loading
Loading