Allow Fuzzer with type parameter

Also fix shrinker first reduction, as well as passing of List/Tuples to fuzzer.
This commit is contained in:
KtorZ 2024-02-27 09:50:33 +01:00
parent c29d163900
commit c766f44601
No known key found for this signature in database
GPG Key ID: 33173CB6F77F4277
5 changed files with 101 additions and 79 deletions

View File

@ -1181,7 +1181,7 @@ pub fn find_list_clause_or_default_first(clauses: &[TypedClause]) -> &TypedClaus
.unwrap_or(&clauses[0]) .unwrap_or(&clauses[0])
} }
pub fn convert_data_to_type(term: Term<Name>, field_type: &Rc<Type>) -> Term<Name> { pub fn convert_data_to_type(term: Term<Name>, field_type: &Type) -> Term<Name> {
if field_type.is_int() { if field_type.is_int() {
Term::un_i_data().apply(term) Term::un_i_data().apply(term)
} else if field_type.is_bytearray() { } else if field_type.is_bytearray() {
@ -1222,7 +1222,7 @@ pub fn convert_data_to_type(term: Term<Name>, field_type: &Rc<Type>) -> Term<Nam
pub fn convert_data_to_type_debug( pub fn convert_data_to_type_debug(
term: Term<Name>, term: Term<Name>,
field_type: &Rc<Type>, field_type: &Type,
error_term: Term<Name>, error_term: Term<Name>,
) -> Term<Name> { ) -> Term<Name> {
if field_type.is_int() { if field_type.is_int() {

View File

@ -1,4 +1,4 @@
use std::collections::HashMap; use std::{collections::HashMap, ops::Deref, rc::Rc};
use crate::{ use crate::{
ast::{ ast::{
@ -11,10 +11,9 @@ use crate::{
builtins::function, builtins::function,
expr::{TypedExpr, UntypedExpr}, expr::{TypedExpr, UntypedExpr},
line_numbers::LineNumbers, line_numbers::LineNumbers,
tipo::{Span, Type}, tipo::{Span, Type, TypeVar},
IdGenerator, IdGenerator,
}; };
use std::rc::Rc;
use super::{ use super::{
environment::{generalise, EntityKind, Environment}, environment::{generalise, EntityKind, Environment},
@ -391,8 +390,16 @@ fn infer_definition(
location: *location, location: *location,
}) })
} }
Type::Fn { .. } | Type::Var { .. } => { Type::Var { tipo } => match tipo.borrow().deref() {
todo!("Fuzzer contains functions and/or non-concrete data-types?"); TypeVar::Link { tipo } => tipo_to_annotation(tipo, location),
_ => todo!(
"Fuzzer contains functions and/or non-concrete data-types? {tipo:#?}"
),
},
Type::Fn { .. } => {
todo!(
"Fuzzer contains functions and/or non-concrete data-types? {tipo:#?}"
);
} }
} }
} }

View File

@ -844,10 +844,12 @@ where
let via = parameter.via.clone(); let via = parameter.via.clone();
let type_info = parameter.tipo.clone();
let body = TypedExpr::Fn { let body = TypedExpr::Fn {
location: Span::empty(), location: Span::empty(),
tipo: Rc::new(Type::Fn { tipo: Rc::new(Type::Fn {
args: vec![parameter.tipo.clone()], args: vec![type_info.clone()],
ret: body.tipo(), ret: body.tipo(),
}), }),
is_capture: false, is_capture: false,
@ -874,7 +876,7 @@ where
name.to_string(), name.to_string(),
*can_error, *can_error,
program, program,
fuzzer, (fuzzer, type_info),
); );
programs.push(prop); programs.push(prop);

View File

@ -1,5 +1,6 @@
use crate::{pretty, ExBudget}; use crate::{pretty, ExBudget};
use aiken_lang::ast::BinOp; use aiken_lang::gen_uplc::builder::convert_data_to_type;
use aiken_lang::{ast::BinOp, tipo::Type};
use pallas::codec::utils::Int; use pallas::codec::utils::Int;
use pallas::ledger::primitives::alonzo::{BigInt, Constr, PlutusData}; use pallas::ledger::primitives::alonzo::{BigInt, Constr, PlutusData};
use std::{ use std::{
@ -10,7 +11,7 @@ use std::{
}; };
use uplc::{ use uplc::{
ast::{Constant, Data, NamedDeBruijn, Program, Term}, ast::{Constant, Data, NamedDeBruijn, Program, Term},
machine::{eval_result::EvalResult, value::from_pallas_bigint}, machine::eval_result::EvalResult,
}; };
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -66,7 +67,7 @@ impl Test {
name: String, name: String,
can_error: bool, can_error: bool,
program: Program<NamedDeBruijn>, program: Program<NamedDeBruijn>,
fuzzer: Program<NamedDeBruijn>, fuzzer: (Program<NamedDeBruijn>, Rc<Type>),
) -> Test { ) -> Test {
Test::PropertyTest(PropertyTest { Test::PropertyTest(PropertyTest {
input_path, input_path,
@ -123,7 +124,7 @@ pub struct PropertyTest {
pub name: String, pub name: String,
pub can_error: bool, pub can_error: bool,
pub program: Program<NamedDeBruijn>, pub program: Program<NamedDeBruijn>,
pub fuzzer: Program<NamedDeBruijn>, pub fuzzer: (Program<NamedDeBruijn>, Rc<Type>),
} }
unsafe impl Send for PropertyTest {} unsafe impl Send for PropertyTest {}
@ -170,7 +171,7 @@ impl PropertyTest {
fn run_once(&self, seed: u32) -> (u32, Option<Term<NamedDeBruijn>>) { fn run_once(&self, seed: u32) -> (u32, Option<Term<NamedDeBruijn>>) {
let (next_prng, value) = Prng::from_seed(seed) let (next_prng, value) = Prng::from_seed(seed)
.sample(&self.fuzzer) .sample(&self.fuzzer.0, &self.fuzzer.1)
.expect("running seeded Prng cannot fail."); .expect("running seeded Prng cannot fail.");
let result = self.program.apply_term(&value).eval(ExBudget::max()); let result = self.program.apply_term(&value).eval(ExBudget::max());
@ -186,7 +187,7 @@ impl PropertyTest {
choices: next_prng.choices(), choices: next_prng.choices(),
can_error: self.can_error, can_error: self.can_error,
program: &self.program, program: &self.program,
fuzzer: &self.fuzzer, fuzzer: (&self.fuzzer.0, &self.fuzzer.1),
}; };
if !counterexample.choices.is_empty() { if !counterexample.choices.is_empty() {
@ -280,14 +281,18 @@ impl Prng {
} }
/// Generate a pseudo-random value from a fuzzer using the given PRNG. /// Generate a pseudo-random value from a fuzzer using the given PRNG.
pub fn sample(&self, fuzzer: &Program<NamedDeBruijn>) -> Option<(Prng, Term<NamedDeBruijn>)> { pub fn sample(
&self,
fuzzer: &Program<NamedDeBruijn>,
return_type: &Type,
) -> Option<(Prng, Term<NamedDeBruijn>)> {
let result = fuzzer let result = fuzzer
.apply_data(self.uplc()) .apply_data(self.uplc())
.eval(ExBudget::max()) .eval(ExBudget::max())
.result() .result()
.expect("Fuzzer crashed?"); .expect("Fuzzer crashed?");
Prng::from_result(result) Prng::from_result(result, return_type)
} }
/// Obtain a Prng back from a fuzzer execution. As a reminder, fuzzers have the following /// Obtain a Prng back from a fuzzer execution. As a reminder, fuzzers have the following
@ -300,7 +305,10 @@ impl Prng {
/// made during shrinking aren't breaking underlying invariants (if only, because we run out of /// made during shrinking aren't breaking underlying invariants (if only, because we run out of
/// values to replay). In such case, the replayed sequence is simply invalid and the fuzzer /// values to replay). In such case, the replayed sequence is simply invalid and the fuzzer
/// aborted altogether with 'None'. /// aborted altogether with 'None'.
pub fn from_result(result: Term<NamedDeBruijn>) -> Option<(Self, Term<NamedDeBruijn>)> { pub fn from_result(
result: Term<NamedDeBruijn>,
type_info: &Type,
) -> Option<(Self, Term<NamedDeBruijn>)> {
/// Interpret the given 'PlutusData' as one of two Prng constructors. /// Interpret the given 'PlutusData' as one of two Prng constructors.
fn as_prng(cst: &PlutusData) -> Prng { fn as_prng(cst: &PlutusData) -> Prng {
if let PlutusData::Constr(Constr { tag, fields, .. }) = cst { if let PlutusData::Constr(Constr { tag, fields, .. }) = cst {
@ -315,12 +323,14 @@ impl Prng {
} }
if *tag == 121 + Prng::REPLAYED { if *tag == 121 + Prng::REPLAYED {
if let [PlutusData::Array(choices)] = &fields[..] {
return Prng::Replayed { return Prng::Replayed {
choices: fields.iter().map(as_u32).collect(), choices: choices.iter().map(as_u32).collect(),
uplc: cst.clone(), uplc: cst.clone(),
}; };
} }
} }
}
panic!("Malformed Prng: {cst:#?}") panic!("Malformed Prng: {cst:#?}")
} }
@ -333,25 +343,17 @@ impl Prng {
panic!("Malformed choice's value: {field:#?}") panic!("Malformed choice's value: {field:#?}")
} }
/// Convert wrapped integer & bytearrays as raw constant terms. Because fuzzer
/// return a pair, those values end up being wrapped in 'Data', but test
/// functions will expect them in their raw constant form.
///
/// Anything else is Data, so we're good.
fn as_value(data: &PlutusData) -> Term<NamedDeBruijn> {
Term::Constant(Rc::new(match data {
PlutusData::BigInt(n) => Constant::Integer(from_pallas_bigint(n)),
PlutusData::BoundedBytes(bytes) => Constant::ByteString(bytes.clone().into()),
_ => Constant::Data(data.clone()),
}))
}
if let Term::Constant(rc) = &result { if let Term::Constant(rc) = &result {
if let Constant::Data(PlutusData::Constr(Constr { tag, fields, .. })) = &rc.borrow() { if let Constant::Data(PlutusData::Constr(Constr { tag, fields, .. })) = &rc.borrow() {
if *tag == 121 + Prng::OK { if *tag == 121 + Prng::OK {
if let [PlutusData::Array(elems)] = &fields[..] { if let [PlutusData::Array(elems)] = &fields[..] {
if let [new_seed, value] = &elems[..] { if let [new_seed, value] = &elems[..] {
return Some((as_prng(new_seed), as_value(value))); return Some((
as_prng(new_seed),
convert_data_to_type(Term::data(value.clone()), type_info)
.try_into()
.expect("safe conversion from Name -> NamedDeBruijn"),
));
} }
} }
} }
@ -388,7 +390,7 @@ pub struct Counterexample<'a> {
pub result: EvalResult, pub result: EvalResult,
pub can_error: bool, pub can_error: bool,
pub program: &'a Program<NamedDeBruijn>, pub program: &'a Program<NamedDeBruijn>,
pub fuzzer: &'a Program<NamedDeBruijn>, pub fuzzer: (&'a Program<NamedDeBruijn>, &'a Type),
} }
impl<'a> Counterexample<'a> { impl<'a> Counterexample<'a> {
@ -402,7 +404,7 @@ impl<'a> Counterexample<'a> {
// test cases many times. Given that tests are fully deterministic, we can // test cases many times. Given that tests are fully deterministic, we can
// memoize the already seen choices to avoid re-running the generators and // memoize the already seen choices to avoid re-running the generators and
// the test (which can be quite expensive). // the test (which can be quite expensive).
match Prng::from_choices(choices).sample(self.fuzzer) { match Prng::from_choices(choices).sample(self.fuzzer.0, self.fuzzer.1) {
// Shrinked choices led to an impossible generation. // Shrinked choices led to an impossible generation.
None => false, None => false,
@ -455,32 +457,45 @@ impl<'a> Counterexample<'a> {
loop { loop {
prev = self.choices.clone(); prev = self.choices.clone();
// Delete choices by chunks of size 8, 4, 2, 1. // First try deleting each choice we made in chunks. We try longer chunks because this
let mut k: isize = 8; // allows us to delete whole composite elements: e.g. deleting an element from a
// generated list requires us to delete both the choice of whether to include it and
// also the element itself, which may involve more than one choice.
let mut k = 8;
while k > 0 { while k > 0 {
let mut i: isize = (self.choices.len() as isize) - k - 1; if k > self.choices.len() {
while i >= 0 { break;
if i >= self.choices.len() as isize {
i -= 1;
continue;
} }
let mut choices = self.choices[0..(i + k) as usize].to_vec();
if !self.consider(&choices) { for (i, j) in (0..=self.choices.len() - k).map(|i| (i, i + k)).rev() {
let mut choices = [
&self.choices[..i],
if j < self.choices.len() {
&self.choices[j..]
} else {
&[]
},
]
.concat();
if self.consider(&choices) {
break;
}
// Perform an extra reduction step that decrease the size of choices near // Perform an extra reduction step that decrease the size of choices near
// the end, to cope with dependencies between choices, e.g. drawing a // the end, to cope with dependencies between choices, e.g. drawing a
// number as a list length, and then drawing that many elements. // number as a list length, and then drawing that many elements.
// //
// This isn't perfect, but allows to make progresses in many cases. // This isn't perfect, but allows to make progresses in many cases.
if i > 0 && *choices.get((i - 1) as usize).unwrap_or(&0) > 0 { if i > 0 && choices[i - 1] > 0 {
choices[(i - 1) as usize] -= 1; choices[i - 1] -= 1;
if self.consider(&choices) { if self.consider(&choices) {
i += 1; break;
};
} }
} }
i -= 1;
} k /= 2
}
k /= 2;
} }
// Now we try replacing region of choices with zeroes. Note that unlike the above we // Now we try replacing region of choices with zeroes. Note that unlike the above we
@ -489,11 +504,9 @@ impl<'a> Counterexample<'a> {
let mut k: isize = 8; let mut k: isize = 8;
while k > 1 { while k > 1 {
let mut i: isize = self.choices.len() as isize - k; let mut i: isize = self.choices.len() as isize - k;
while i >= 0 { while i >= 0 {
i -= if self.zeroes(i, k) { k } else { 1 } i -= if self.zeroes(i, k) { k } else { 1 }
} }
k /= 2 k /= 2
} }

View File

@ -304,23 +304,23 @@ fn fmt_test(result: &TestResult, max_mem: usize, max_cpu: usize, styled: bool) -
); );
// CounterExample // CounterExample
// if let TestResult::PropertyTestResult(PropertyTestResult { if let TestResult::PropertyTestResult(PropertyTestResult {
// counterexample: Some(counterexample), counterexample: Some(counterexample),
// .. ..
// }) = result }) = result
// { {
// test = format!( test = format!(
// "{test}\n{}", "{test}\n{}",
// pretty::boxed_with( pretty::boxed_with(
// &pretty::style_if(styled, "counterexample".to_string(), |s| s &pretty::style_if(styled, "counterexample".to_string(), |s| s
// .if_supports_color(Stderr, |s| s.red()) .if_supports_color(Stderr, |s| s.red())
// .if_supports_color(Stderr, |s| s.bold()) .if_supports_color(Stderr, |s| s.bold())
// .to_string()), .to_string()),
// &counterexample.to_pretty(), &counterexample.to_pretty(),
// |s| s.red().to_string() |s| s.red().to_string()
// ) )
// ) )
// } }
// Traces // Traces
if !result.logs().is_empty() { if !result.logs().is_empty() {