Old Fuzzer, new Sampler

This commit is contained in:
Riley-Kilgore 2024-12-17 06:01:19 -08:00 committed by Riley
parent c0fabcd26a
commit 84a0abeb0f
14 changed files with 164 additions and 127 deletions

View File

@ -258,7 +258,7 @@ fn str_to_keyword(word: &str) -> Option<Token> {
"or" => Some(Token::Or),
"validator" => Some(Token::Validator),
"via" => Some(Token::Via),
"benchmark" => Some(Token::Benchmark),
"bench" => Some(Token::Benchmark),
_ => None,
}
}

View File

@ -8,7 +8,8 @@ pub const BOOL: &str = "Bool";
pub const BOOL_CONSTRUCTORS: &[&str] = &["False", "True"];
pub const BYTE_ARRAY: &str = "ByteArray";
pub const DATA: &str = "Data";
pub const GENERATOR: &str = "Generator";
pub const FUZZER: &str = "Fuzzer";
pub const SAMPLER: &str = "Sampler";
pub const G1_ELEMENT: &str = "G1Element";
pub const G2_ELEMENT: &str = "G2Element";
pub const INT: &str = "Int";
@ -179,7 +180,7 @@ impl Type {
})
}
pub fn generator(c: Rc<Type>, a: Rc<Type>) -> Rc<Type> {
pub fn fuzzer(a: Rc<Type>) -> Rc<Type> {
let prng_annotation = Annotation::Constructor {
location: Span::empty(),
module: None,
@ -188,15 +189,15 @@ impl Type {
};
Rc::new(Type::Fn {
args: vec![c, Type::prng()],
args: vec![Type::prng()],
ret: Type::option(Type::tuple(vec![Type::prng(), a])),
alias: Some(
TypeAliasAnnotation {
alias: GENERATOR.to_string(),
parameters: vec!["c".to_string(), "a".to_string()],
alias: FUZZER.to_string(),
parameters: vec!["a".to_string()],
annotation: Annotation::Fn {
location: Span::empty(),
arguments: vec![Annotation::data(Span::empty()), prng_annotation.clone()],
arguments: vec![prng_annotation.clone()],
ret: Annotation::Constructor {
location: Span::empty(),
module: None,
@ -220,6 +221,53 @@ impl Type {
})
}
pub fn sampler(a: Rc<Type>) -> Rc<Type> {
let prng_annotation = Annotation::Constructor {
location: Span::empty(),
module: None,
name: PRNG.to_string(),
arguments: vec![],
};
Rc::new(Type::Fn {
args: vec![Type::int()],
ret: Type::fuzzer(a),
alias: Some(
TypeAliasAnnotation {
alias: SAMPLER.to_string(),
parameters: vec!["a".to_string()],
annotation: Annotation::Fn {
location: Span::empty(),
arguments: vec![Annotation::int(Span::empty())],
ret: Annotation::Fn {
location: Span::empty(),
arguments: vec![prng_annotation.clone()],
ret: Annotation::Constructor {
location: Span::empty(),
module: None,
name: OPTION.to_string(),
arguments: vec![Annotation::Tuple {
location: Span::empty(),
elems: vec![
prng_annotation,
Annotation::Var {
location: Span::empty(),
name: "a".to_string(),
},
],
}],
}
.into(),
}
.into(),
}
.into(),
}
.into(),
),
})
}
pub fn map(k: Rc<Type>, v: Rc<Type>) -> Rc<Type> {
Rc::new(Type::App {
public: true,

View File

@ -477,23 +477,38 @@ pub fn prelude(id_gen: &IdGenerator) -> TypeInfo {
),
);
// Generator
// Fuzzer
//
// pub type Generator<c, a> =
// fn(Data, PRNG) -> Option<(PRNG, a)>
let generator_context = Type::generic_var(id_gen.next());
let generator_value = Type::generic_var(id_gen.next());
// pub type Fuzzer<a> =
// fn(PRNG) -> Option<(PRNG, a)>
let fuzzer_generic = Type::generic_var(id_gen.next());
prelude.types.insert(
well_known::GENERATOR.to_string(),
well_known::FUZZER.to_string(),
TypeConstructor {
location: Span::empty(),
parameters: vec![generator_context.clone(), generator_value.clone()],
tipo: Type::generator(generator_context, generator_value),
parameters: vec![fuzzer_generic.clone()],
tipo: Type::fuzzer(fuzzer_generic),
module: "".to_string(),
public: true,
},
);
// Sampler
//
// pub type Sampler<a> =
// fn(Int) -> Fuzzer<a>
let sampler_generic = Type::generic_var(id_gen.next());
prelude.types.insert(
well_known::SAMPLER.to_string(),
TypeConstructor {
location: Span::empty(),
parameters: vec![sampler_generic.clone()],
tipo: Type::sampler(sampler_generic),
module: "".to_string(),
public: true,
}
);
prelude
}

View File

@ -135,7 +135,7 @@ mod tests {
fn def_benchmark() {
assert_definition!(
r#"
benchmark foo(x via fuzz.any_int) {
bench foo(x via fuzz.any_int) {
True
}
"#
@ -146,7 +146,7 @@ mod tests {
fn def_invalid_benchmark() {
assert_definition!(
r#"
benchmark foo(x via f, y via g) {
bench foo(x via f, y via g) {
True
}
"#
@ -157,7 +157,7 @@ mod tests {
fn def_benchmark_annotated_fuzzer() {
assert_definition!(
r#"
benchmark foo(x: Int via foo()) {
bench foo(x: Int via foo()) {
True
}
"#

View File

@ -1,7 +1,7 @@
---
source: crates/aiken-lang/src/parser/definition/benchmark.rs
assertion_line: 136
description: "Code:\n\nbenchmark foo(x via fuzz.any_int) {\n True\n}\n"
description: "Code:\n\nbench foo(x via fuzz.any_int) {\n True\n}\n"
snapshot_kind: text
---
Benchmark(
@ -13,35 +13,35 @@ Benchmark(
Named {
name: "x",
label: "x",
location: 14..15,
location: 10..11,
},
),
location: 14..15,
location: 10..11,
annotation: None,
doc: None,
is_validator_param: false,
},
via: FieldAccess {
location: 20..32,
location: 16..28,
label: "any_int",
container: Var {
location: 20..24,
location: 16..20,
name: "fuzz",
},
},
},
],
body: Var {
location: 40..44,
location: 36..40,
name: "True",
},
doc: None,
location: 0..33,
location: 0..29,
name: "foo",
public: false,
return_annotation: None,
return_type: (),
end_position: 45,
end_position: 41,
on_test_failure: FailImmediately,
},
)

View File

@ -1,7 +1,7 @@
---
source: crates/aiken-lang/src/parser/definition/benchmark.rs
assertion_line: 158
description: "Code:\n\nbenchmark foo(x: Int via foo()) {\n True\n}\n"
description: "Code:\n\nbench foo(x: Int via foo()) {\n True\n}\n"
snapshot_kind: text
---
Benchmark(
@ -13,13 +13,13 @@ Benchmark(
Named {
name: "x",
label: "x",
location: 14..15,
location: 10..11,
},
),
location: 14..20,
location: 10..16,
annotation: Some(
Constructor {
location: 17..20,
location: 13..16,
module: None,
name: "Int",
arguments: [],
@ -31,24 +31,24 @@ Benchmark(
via: Call {
arguments: [],
fun: Var {
location: 25..28,
location: 21..24,
name: "foo",
},
location: 25..30,
location: 21..26,
},
},
],
body: Var {
location: 38..42,
location: 34..38,
name: "True",
},
doc: None,
location: 0..31,
location: 0..27,
name: "foo",
public: false,
return_annotation: None,
return_type: (),
end_position: 43,
end_position: 39,
on_test_failure: FailImmediately,
},
)

View File

@ -1,7 +1,7 @@
---
source: crates/aiken-lang/src/parser/definition/benchmark.rs
assertion_line: 147
description: "Code:\n\nbenchmark foo(x via f, y via g) {\n True\n}\n"
description: "Code:\n\nbench foo(x via f, y via g) {\n True\n}\n"
snapshot_kind: text
---
Benchmark(
@ -13,16 +13,16 @@ Benchmark(
Named {
name: "x",
label: "x",
location: 14..15,
location: 10..11,
},
),
location: 14..15,
location: 10..11,
annotation: None,
doc: None,
is_validator_param: false,
},
via: Var {
location: 20..21,
location: 16..17,
name: "f",
},
},
@ -32,31 +32,31 @@ Benchmark(
Named {
name: "y",
label: "y",
location: 23..24,
location: 19..20,
},
),
location: 23..24,
location: 19..20,
annotation: None,
doc: None,
is_validator_param: false,
},
via: Var {
location: 29..30,
location: 25..26,
name: "g",
},
},
],
body: Var {
location: 38..42,
location: 34..38,
name: "True",
},
doc: None,
location: 0..31,
location: 0..27,
name: "foo",
public: false,
return_annotation: None,
return_type: (),
end_position: 43,
end_position: 39,
on_test_failure: FailImmediately,
},
)

View File

@ -243,7 +243,7 @@ pub fn lexer() -> impl Parser<char, Vec<(Token, Span)>, Error = ParseError> {
"when" => Token::When,
"validator" => Token::Validator,
"via" => Token::Via,
"benchmark" => Token::Benchmark,
"bench" => Token::Benchmark,
_ => {
if s.chars().next().map_or(false, |c| c.is_uppercase()) {
Token::UpName {

View File

@ -183,7 +183,7 @@ impl fmt::Display for Token {
Token::Once => "once",
Token::Validator => "validator",
Token::Via => "via",
Token::Benchmark => "benchmark",
Token::Benchmark => "bench",
};
write!(f, "{s}")
}

View File

@ -332,14 +332,12 @@ impl PropertyTest {
) -> Result<Option<Counterexample<'a>>, FuzzerError> {
let mut prng = initial_prng;
let mut counterexample = None;
let mut iteration = 0;
while *remaining > 0 && counterexample.is_none() {
let (next_prng, cex) = self.run_once(prng, labels, plutus_version, iteration)?;
let (next_prng, cex) = self.run_once(prng, labels, plutus_version)?;
prng = next_prng;
counterexample = cex;
*remaining -= 1;
iteration += 1;
}
Ok(counterexample)
@ -350,12 +348,11 @@ impl PropertyTest {
prng: Prng,
labels: &mut BTreeMap<String, usize>,
plutus_version: &'a PlutusVersion,
iteration: usize,
) -> Result<(Prng, Option<Counterexample<'a>>), FuzzerError> {
use OnTestFailure::*;
let (next_prng, value) = prng
.sample(&self.fuzzer.program, iteration)?
.sample(&self.fuzzer.program)?
.expect("A seeded PRNG returned 'None' which indicates a fuzzer is ill-formed and implemented wrongly; please contact library's authors.");
let mut result = self.eval(&value, plutus_version);
@ -386,8 +383,8 @@ impl PropertyTest {
value,
choices: next_prng.choices(),
cache: Cache::new(move |choices| {
match Prng::from_choices(choices, iteration)
.sample(&self.fuzzer.program, iteration)
match Prng::from_choices(choices)
.sample(&self.fuzzer.program)
{
Err(..) => Status::Invalid,
Ok(None) => Status::Invalid,
@ -454,7 +451,7 @@ pub struct Benchmark {
pub name: String,
pub on_test_failure: OnTestFailure,
pub program: Program<Name>,
pub fuzzer: Fuzzer<Name>,
pub sampler: Fuzzer<Name>,
}
unsafe impl Send for Benchmark {}
@ -467,11 +464,12 @@ impl Benchmark {
plutus_version: &PlutusVersion,
) -> Vec<BenchmarkResult> {
let mut results = Vec::with_capacity(n);
let mut remaining = n;
let mut iteration = 0;
let mut prng = Prng::from_seed(seed);
while remaining > 0 {
match prng.sample(&self.fuzzer.program, n - remaining) {
while n > iteration {
let fuzzer = self.sampler.program.apply_data(Data::integer(num_bigint::BigInt::from(iteration as i64)));
match prng.sample(&fuzzer) {
Ok(Some((new_prng, value))) => {
prng = new_prng;
let mut eval_result = self.eval(&value, plutus_version);
@ -496,7 +494,7 @@ impl Benchmark {
break;
}
}
remaining -= 1;
iteration += 1;
}
results
@ -534,12 +532,10 @@ pub enum Prng {
Seeded {
choices: Vec<u8>,
uplc: PlutusData,
iteration: usize,
},
Replayed {
choices: Vec<u8>,
uplc: PlutusData,
iteration: usize,
},
}
@ -588,12 +584,11 @@ impl Prng {
Data::bytestring(vec![]), // Random choices
],
),
iteration: 0,
}
}
/// Construct a Pseudo-random number generator from a pre-defined list of choices.
pub fn from_choices(choices: &[u8], iteration: usize) -> Prng {
pub fn from_choices(choices: &[u8]) -> Prng {
Prng::Replayed {
uplc: Data::constr(
Prng::REPLAYED,
@ -603,7 +598,6 @@ impl Prng {
],
),
choices: choices.to_vec(),
iteration,
}
}
@ -611,11 +605,10 @@ impl Prng {
pub fn sample(
&self,
fuzzer: &Program<Name>,
iteration: usize,
// iteration: usize,
) -> Result<Option<(Prng, PlutusData)>, FuzzerError> {
let program = Program::<NamedDeBruijn>::try_from(
fuzzer
.apply_data(Data::integer(num_bigint::BigInt::from(iteration as i64)))
.apply_data(self.uplc())).unwrap();
let mut result = program.eval(ExBudget::max());
result
@ -624,7 +617,7 @@ impl Prng {
traces: result.logs(),
uplc_error,
})
.map(|term| Prng::from_result(term, iteration))
.map(|term| Prng::from_result(term))
}
/// Obtain a Prng back from a fuzzer execution. As a reminder, fuzzers have the following
@ -639,10 +632,9 @@ impl Prng {
/// aborted altogether with 'None'.
pub fn from_result(
result: Term<NamedDeBruijn>,
iteration: usize,
) -> Option<(Self, PlutusData)> {
/// Interpret the given 'PlutusData' as one of two Prng constructors.
fn as_prng(cst: &PlutusData, iteration: usize) -> Prng {
fn as_prng(cst: &PlutusData) -> Prng {
if let PlutusData::Constr(Constr { tag, fields, .. }) = cst {
if *tag == 121 + Prng::SEEDED {
if let [PlutusData::BoundedBytes(bytes), PlutusData::BoundedBytes(choices)] =
@ -659,7 +651,6 @@ impl Prng {
PlutusData::BoundedBytes(vec![].into()),
],
),
iteration,
};
}
}
@ -670,7 +661,6 @@ impl Prng {
return Prng::Replayed {
choices: choices.to_vec(),
uplc: cst.clone(),
iteration,
};
}
}
@ -684,7 +674,7 @@ impl Prng {
if *tag == 121 + Prng::SOME {
if let [PlutusData::Array(elems)] = &fields[..] {
if let [new_seed, value] = &elems[..] {
return Some((as_prng(new_seed, iteration), value.clone()));
return Some((as_prng(new_seed), value.clone()));
}
}
}

View File

@ -1773,17 +1773,27 @@ fn pipe_wrong_arity_fully_saturated_return_fn() {
#[test]
fn fuzzer_ok_basic() {
let source_code = r#"
fn int() -> Generator<Void, Int> { todo }
fn int() -> Fuzzer<Int> { todo }
test prop(n via int()) { True }
"#;
assert!(check(parse(source_code)).is_ok());
}
#[test]
fn sampler_ok_basic() {
let source_code = r#"
fn int() -> Sampler<Int> { todo }
bench prop(n via int()) { True }
"#;
assert!(check(parse(source_code)).is_ok());
}
#[test]
fn fuzzer_ok_explicit() {
let source_code = r#"
fn int(void: Void, prng: PRNG) -> Option<(PRNG, Int)> { todo }
fn int(prng: PRNG) -> Option<(PRNG, Int)> { todo }
test prop(n via int) { Void }
"#;
@ -1793,8 +1803,8 @@ fn fuzzer_ok_explicit() {
#[test]
fn fuzzer_ok_list() {
let source_code = r#"
fn int() -> Generator<Void, Int> { todo }
fn list(a: Generator<Void, a>) -> Generator<Void, List<a>> { todo }
fn int() -> Fuzzer<Int> { todo }
fn list(a: Fuzzer<a>) -> Fuzzer<List<a>> { todo }
test prop(xs via list(int())) { True }
"#;
@ -1805,8 +1815,8 @@ fn fuzzer_ok_list() {
#[test]
fn fuzzer_err_unbound() {
let source_code = r#"
fn any() -> Generator<Void, a> { todo }
fn list(a: Generator<Void, a>) -> Generator<Void, List<a>> { todo }
fn any() -> Fuzzer<a> { todo }
fn list(a: Fuzzer<a>) -> Fuzzer<List<a>> { todo }
test prop(xs via list(any())) { todo }
"#;
@ -1838,7 +1848,7 @@ fn fuzzer_err_unify_1() {
#[test]
fn fuzzer_err_unify_2() {
let source_code = r#"
fn any() -> Generator<Void, a> { todo }
fn any() -> Fuzzer<a> { todo }
test prop(xs via any) { todo }
"#;
@ -1857,8 +1867,8 @@ fn fuzzer_err_unify_2() {
#[test]
fn fuzzer_err_unify_3() {
let source_code = r#"
fn list(a: Generator<Void, a>) -> Generator<Void, List<a>> { todo }
fn int() -> Generator<Void, Int> { todo }
fn list(a: Fuzzer<a>) -> Fuzzer<List<a>> { todo }
fn int() -> Fuzzer<Int> { todo }
test prop(xs: Int via list(int())) { todo }
"#;

View File

@ -838,8 +838,7 @@ fn infer_fuzzer(
) -> Result<(Annotation, Rc<Type>), Error> {
let could_not_unify = || Error::CouldNotUnify {
location: *location,
expected: Type::generator(
Type::void(),
expected: Type::fuzzer(
expected_inner_type
.clone()
.unwrap_or_else(|| Type::generic_var(0)),
@ -863,7 +862,7 @@ fn infer_fuzzer(
contains_opaque: _,
alias: _,
} if module.is_empty() && name == "Option" && args.len() == 1 => {
match args.first().expect("args.len() == 2 && args[0].is_void()").borrow() {
match args.first().expect("args.len() == 1").borrow() {
Type::Tuple { elems, .. } if elems.len() == 2 => {
let wrapped = elems.get(1).expect("Tuple has two elements");
@ -878,7 +877,7 @@ fn infer_fuzzer(
// `unify` now that we have figured out the type carried by the fuzzer.
environment.unify(
tipo.clone(),
Type::generator(Type::void(), wrapped.clone()),
Type::fuzzer(wrapped.clone()),
*location,
false,
)?;
@ -916,8 +915,7 @@ fn infer_sampler(
) -> Result<(Annotation, Rc<Type>), Error> {
let could_not_unify = || Error::CouldNotUnify {
location: *location,
expected: Type::generator(
Type::int(),
expected: Type::sampler(
expected_inner_type
.clone()
.unwrap_or_else(|| Type::generic_var(0)),
@ -930,34 +928,12 @@ fn infer_sampler(
match tipo.borrow() {
Type::Fn {
ret,
args: _,
args,
alias: _,
} => match ret.borrow() {
Type::App {
module,
name,
args,
public: _,
contains_opaque: _,
alias: _,
} if module.is_empty() && name == "Option" && args.len() == 1 => {
match args.first().expect("args.len() == 2 && args[0].is_int()").borrow() {
Type::Tuple { elems, .. } if elems.len() == 2 => {
let wrapped = elems.get(1).expect("Tuple has two elements");
environment.unify(
tipo.clone(),
Type::generator(Type::int(), wrapped.clone()),
*location,
false,
)?;
Ok((annotate_fuzzer(wrapped, location)?, wrapped.clone()))
}
_ => Err(could_not_unify()),
}
}
_ => Err(could_not_unify()),
} => if args.len() == 1 && args[0].is_int() {
infer_fuzzer(environment, expected_inner_type, ret, &Span::empty())
} else {
Err(could_not_unify())
},
Type::Var { tipo, alias } => match &*tipo.deref().borrow() {

View File

@ -193,7 +193,7 @@ impl Error {
test.input_path.to_path_buf(),
test.program.to_pretty(),
),
TestResult::Benchmark(_) => ("benchmark".to_string(), PathBuf::new(), String::new()), // todo
TestResult::Benchmark(_) => ("bench".to_string(), PathBuf::new(), String::new()), // todo
};
Error::TestFailure {

View File

@ -112,10 +112,8 @@ mod test {
const max_int: Int = 255
pub type Fuzzer<a> = Generator<Void, a>
pub fn int() -> Fuzzer<Int> {
fn(v: Void, prng: PRNG) -> Option<(PRNG, Int)> {
fn(prng: PRNG) -> Option<(PRNG, Int)> {
when prng is {
Seeded { seed, choices } -> {
let choice =
@ -163,21 +161,21 @@ mod test {
}
pub fn constant(a: a) -> Fuzzer<a> {
fn(v, s0) { Some((s0, a)) }
fn(s0) { Some((s0, a)) }
}
pub fn and_then(fuzz_a: Fuzzer<a>, f: fn(a) -> Fuzzer<b>) -> Fuzzer<b> {
fn(v, s0) {
when fuzz_a(v, s0) is {
Some((s1, a)) -> f(a)(v, s1)
fn(s0) {
when fuzz_a(s0) is {
Some((s1, a)) -> f(a)(s1)
None -> None
}
}
}
pub fn map(fuzz_a: Fuzzer<a>, f: fn(a) -> b) -> Fuzzer<b> {
fn(v, s0) {
when fuzz_a(v, s0) is {
fn(s0) {
when fuzz_a(s0) is {
Some((s1, a)) -> Some((s1, f(a)))
None -> None
}
@ -185,10 +183,10 @@ mod test {
}
pub fn map2(fuzz_a: Fuzzer<a>, fuzz_b: Fuzzer<b>, f: fn(a, b) -> c) -> Fuzzer<c> {
fn(v, s0) {
when fuzz_a(v, s0) is {
fn(s0) {
when fuzz_a(s0) is {
Some((s1, a)) ->
when fuzz_b(Void, s1) is {
when fuzz_b(s1) is {
Some((s2, b)) -> Some((s2, f(a, b)))
None -> None
}