Skip to content
28 changes: 25 additions & 3 deletions tooling/ast_fuzzer/src/program/func.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ impl FunctionDeclaration {

(param_types, return_type)
}

fn is_acir(&self) -> bool {
!self.unconstrained
}

fn is_brillig(&self) -> bool {
self.unconstrained
}
}

/// HIR representation of a function parameter.
Expand Down Expand Up @@ -153,19 +161,33 @@ impl<'a> FunctionContext<'a> {
let call_targets = ctx
.function_declarations
.iter()
.filter_map(|(callee_id, decl)| {
.filter_map(|(callee_id, callee_decl)| {
// We can't call `main`.
if *callee_id == Program::main_id() {
return None;
}

// From an ACIR function we can call any Brillig function,
// but we avoid creating infinite recursive ACIR calls by
// only calling functions with higher IDs than ours,
// otherwise the inliner could get stuck.
if !decl.unconstrained && *callee_id <= id {
if decl.is_acir() && callee_decl.is_acir() && *callee_id <= id {
return None;
}
Some((*callee_id, types::types_produced(&decl.return_type)))

// From a Brillig function we restrict ourselves to only call
// other Brillig functions. That's because the `Monomorphizer`
// would make an unconstrained copy of any ACIR function called
// from Brillig, and this is expected by the inliner for example,
// but if we did similarly in the generator after we know who
// calls who, we would incur two drawbacks:
// 1) it would make programs bigger for little benefit
// 2) it would skew calibration frequencies as ACIR freqs would overlay Brillig ones
if decl.is_brillig() && !callee_decl.is_brillig() {
return None;
}

Some((*callee_id, types::types_produced(&callee_decl.return_type)))
})
.collect();

Expand Down
99 changes: 99 additions & 0 deletions tooling/ast_fuzzer/tests/smoke.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//! Smoke test for the AST fuzzer, which generates a bunch of
//! random programs and executes them, without asserting anything
//! about the outcome. The only criteria it needs to pass is not
//! to crash the compiler, which could indicate invalid input.
//!
//! ```shell
//! cargo test -p noir_ast_fuzzer --test smoke
//! ```
use std::time::Duration;

use acir::circuit::ExpressionWidth;
use arbtest::arbtest;
use bn254_blackbox_solver::Bn254BlackBoxSolver;
use nargo::{NargoError, foreign_calls::DefaultForeignCallBuilder};
use noir_ast_fuzzer::{Config, DisplayAstAsNoir, arb_inputs, arb_program, program_abi};
use noirc_evaluator::{brillig::BrilligOptions, ssa};

fn seed_from_env() -> Option<u64> {
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_program_can_be_executed() {
let maybe_seed = seed_from_env();

let mut prop = arbtest(|u| {
let program = arb_program(u, Config::default())?;
let abi = program_abi(&program);

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,
};

// 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() {
// It could be useful to also show the input, but in the smoke test we're mostly interested in compiler crashes,
// not the execution. For that we have the actual fuzz targets.
eprintln!("{}", DisplayAstAsNoir(&program));
}

let ssa = ssa::create_program(program.clone(), &options)
.unwrap_or_else(|e| print_ast_and_panic(&format!("Failed to compile program: {e}")));

let inputs = arb_inputs(u, &ssa.program, &abi)?;

let blackbox_solver = Bn254BlackBoxSolver(false);
let initial_witness = abi.encode(&inputs, None).unwrap();

let mut foreign_call_executor =
DefaultForeignCallBuilder::default().with_mocks(false).build();

let res = nargo::ops::execute_program(
&ssa.program,
initial_witness,
&blackbox_solver,
&mut foreign_call_executor,
);

match res {
Err(NargoError::CompilationError) => {
print_ast_and_panic("Failed to compile program into ACIR.")
}
Err(NargoError::ForeignCallError(e)) => {
print_ast_and_panic(&format!("Failed to call foreign function: {e}"))
}
Err(NargoError::ExecutionError(_)) | Ok(_) => {
// If some assertion failed, it's okay, we can't tell if it should or shouldn't.
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();
}
Loading