Allow Fuzzer with type parameter
Also fix shrinker first reduction, as well as passing of List/Tuples to fuzzer.
This commit is contained in:
parent
c29d163900
commit
c766f44601
|
@ -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() {
|
||||||
|
|
|
@ -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:#?}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
Loading…
Reference in New Issue