diff --git a/Cargo.lock b/Cargo.lock index 4c197dc148b..1ba72276367 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3424,6 +3424,7 @@ dependencies = [ "proptest", "rand 0.8.5", "regex", + "serde_json", "similar-asserts", "strum", ] diff --git a/compiler/noirc_evaluator/src/ssa/ir/function.rs b/compiler/noirc_evaluator/src/ssa/ir/function.rs index feeb34bc036..a6cd515bc15 100644 --- a/compiler/noirc_evaluator/src/ssa/ir/function.rs +++ b/compiler/noirc_evaluator/src/ssa/ir/function.rs @@ -270,6 +270,12 @@ impl std::fmt::Display for RuntimeType { } } +/// Iterate over every Value in this DFG in no particular order, including unused Values, +/// for testing purposes. +pub fn function_values_iter(func: &Function) -> impl DoubleEndedIterator { + func.dfg.values_iter() +} + /// FunctionId is a reference for a function /// /// This Id is how each function refers to other functions diff --git a/tooling/ast_fuzzer/Cargo.toml b/tooling/ast_fuzzer/Cargo.toml index 56dcbd80169..2ad1f740d06 100644 --- a/tooling/ast_fuzzer/Cargo.toml +++ b/tooling/ast_fuzzer/Cargo.toml @@ -39,3 +39,4 @@ noir_greybox_fuzzer.workspace = true arbtest.workspace = true insta.workspace = true similar-asserts.workspace = true +serde_json.workspace = true diff --git a/tooling/ast_fuzzer/tests/parser.rs b/tooling/ast_fuzzer/tests/parser.rs new file mode 100644 index 00000000000..20c114f0ab4 --- /dev/null +++ b/tooling/ast_fuzzer/tests/parser.rs @@ -0,0 +1,105 @@ +//! Test that the SSA of an arbitrary program can be printed and parsed back. +//! +//! ```shell +//! cargo test -p noir_ast_fuzzer --test parser +//! ``` +use std::time::Duration; + +use acir::circuit::ExpressionWidth; +use arbtest::arbtest; +use noir_ast_fuzzer::{Config, DisplayAstAsNoir, arb_program}; +use noirc_evaluator::{ + brillig::BrilligOptions, + ssa::{ + self, + ir::function::function_values_iter, + primary_passes, + ssa_gen::{self, Ssa}, + }, +}; + +fn seed_from_env() -> Option { + let Ok(seed) = std::env::var("NOIR_ARBTEST_SEED") else { return None }; + let seed = u64::from_str_radix(seed.trim_start_matches("0x"), 16) + .unwrap_or_else(|e| panic!("failed to parse seed '{seed}': {e}")); + Some(seed) +} + +#[test] +fn arb_ssa_roundtrip() { + let maybe_seed = seed_from_env(); + + let mut prop = arbtest(|u| { + let config = Config::default(); + let program = arb_program(u, config)?; + + let options = ssa::SsaEvaluatorOptions { + ssa_logging: ssa::SsaLogging::None, + brillig_options: BrilligOptions::default(), + print_codegen_timings: false, + expression_width: ExpressionWidth::default(), + emit_ssa: None, + skip_underconstrained_check: true, + skip_brillig_constraints_check: true, + enable_brillig_constraints_check_lookback: false, + inliner_aggressiveness: 0, + max_bytecode_increase_percent: None, + skip_passes: Default::default(), + }; + let pipeline = primary_passes(&options); + let last_pass = u.choose_index(pipeline.len())?; + let passes = &pipeline[0..last_pass]; + + // Print the AST if something goes wrong, then panic. + let print_ast_and_panic = |msg: &str| -> ! { + eprintln!("{}", DisplayAstAsNoir(&program)); + panic!("{msg}") + }; + + // If we have a seed to debug and we know it's going to crash, print the AST. + if maybe_seed.is_some() { + eprintln!("{}", DisplayAstAsNoir(&program)); + } + + // Generate the initial SSA; + let ssa = ssa_gen::generate_ssa(program.clone()).unwrap_or_else(|e| { + print_ast_and_panic(&format!("Failed to generate initial SSA: {e}")) + }); + + let mut ssa1 = passes.iter().fold(ssa, |ssa, pass| { + pass.run(ssa).unwrap_or_else(|e| { + print_ast_and_panic(&format!("Failed to run pass {}: {e}", pass.msg())) + }) + }); + + // Normalize before printing so IDs don't change. + ssa1.normalize_ids(); + + // Print to str and parse back. + let ssa2 = Ssa::from_str(&ssa1.to_string()).unwrap_or_else(|e| { + let msg = passes.last().map(|p| p.msg()).unwrap_or("Initial SSA"); + print_ast_and_panic(&format!( + "Could not parse SSA after step {last_pass} ({msg}): \n{e:?}" + )) + }); + + // Not everything is populated by the parser, and unfortunately serializing to JSON doesn't work either. + for (func_id, func1) in ssa1.functions { + let func2 = &ssa2.functions[&func_id]; + let values1 = function_values_iter(&func1).collect::>(); + let values2 = function_values_iter(func2).collect::>(); + similar_asserts::assert_eq!(values1, values2); + } + + Ok(()) + }) + .budget(Duration::from_secs(10)) + .size_min(1 << 12) + .size_max(1 << 20); + + if let Some(seed) = maybe_seed { + prop = prop.seed(seed); + } + + prop.run(); +}