Added benchmark keyword and unified Samplers and Fuzzers as Generator

This commit is contained in:
Riley-Kilgore 2024-12-16 10:33:01 -08:00 committed by Riley
parent d353e07ea1
commit c0fabcd26a
21 changed files with 828 additions and 262 deletions

View File

@ -118,6 +118,7 @@ impl TypedModule {
Definition::Use(_) => false,
Definition::Test(_) => false,
Definition::Validator(_) => false,
Definition::Benchmark(_) => false,
})
}
@ -134,6 +135,7 @@ impl TypedModule {
Definition::Use(_) => false,
Definition::Test(_) => false,
Definition::Validator(_) => false,
Definition::Benchmark(_) => false,
})
}
@ -186,6 +188,16 @@ impl TypedModule {
);
}
Definition::Benchmark(benchmark) => {
functions.insert(
FunctionAccessKey {
module_name: self.name.clone(),
function_name: benchmark.name.clone(),
},
benchmark.clone().into(),
);
}
Definition::DataType(dt) => {
data_types.insert(
DataTypeKey {
@ -246,6 +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),
_ => None,
}
}
@ -832,6 +845,8 @@ pub enum Definition<T, Arg, Expr, PackageName> {
Test(Function<T, Expr, ArgVia<Arg, Expr>>),
Benchmark(Function<T, Expr, ArgVia<Arg, Expr>>),
Validator(Validator<T, Arg, Expr>),
}
@ -844,6 +859,7 @@ impl<A, B, C, D> Definition<A, B, C, D> {
| Definition::DataType(DataType { location, .. })
| Definition::ModuleConstant(ModuleConstant { location, .. })
| Definition::Validator(Validator { location, .. })
| Definition::Benchmark(Function { location, .. })
| Definition::Test(Function { location, .. }) => *location,
}
}
@ -856,6 +872,7 @@ impl<A, B, C, D> Definition<A, B, C, D> {
| Definition::DataType(DataType { doc, .. })
| Definition::ModuleConstant(ModuleConstant { doc, .. })
| Definition::Validator(Validator { doc, .. })
| Definition::Benchmark(Function { doc, .. })
| Definition::Test(Function { doc, .. }) => {
let _ = std::mem::replace(doc, Some(new_doc));
}
@ -870,6 +887,7 @@ impl<A, B, C, D> Definition<A, B, C, D> {
| Definition::DataType(DataType { doc, .. })
| Definition::ModuleConstant(ModuleConstant { doc, .. })
| Definition::Validator(Validator { doc, .. })
| Definition::Benchmark(Function { doc, .. })
| Definition::Test(Function { doc, .. }) => doc.clone(),
}
}

View File

@ -8,8 +8,7 @@ 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 FUZZER: &str = "Fuzzer";
pub const SCALED_FUZZER: &str = "ScaledFuzzer";
pub const GENERATOR: &str = "Generator";
pub const G1_ELEMENT: &str = "G1Element";
pub const G2_ELEMENT: &str = "G2Element";
pub const INT: &str = "Int";
@ -180,7 +179,7 @@ impl Type {
})
}
pub fn fuzzer(a: Rc<Type>) -> Rc<Type> {
pub fn generator(c: Rc<Type>, a: Rc<Type>) -> Rc<Type> {
let prng_annotation = Annotation::Constructor {
location: Span::empty(),
module: None,
@ -189,64 +188,15 @@ impl Type {
};
Rc::new(Type::Fn {
args: vec![Type::prng()],
args: vec![c, Type::prng()],
ret: Type::option(Type::tuple(vec![Type::prng(), a])),
alias: Some(
TypeAliasAnnotation {
alias: FUZZER.to_string(),
parameters: vec!["a".to_string()],
alias: GENERATOR.to_string(),
parameters: vec!["c".to_string(), "a".to_string()],
annotation: 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(),
),
})
}
pub fn scaled_fuzzer(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::prng(), Type::int()],
ret: Type::option(Type::tuple(vec![Type::prng(), a])),
alias: Some(
TypeAliasAnnotation {
alias: SCALED_FUZZER.to_string(),
parameters: vec!["a".to_string()],
annotation: Annotation::Fn {
location: Span::empty(),
arguments: vec![
prng_annotation.clone(),
Annotation::Constructor {
location: Span::empty(),
module: None,
name: INT.to_string(),
arguments: vec![],
},
],
arguments: vec![Annotation::data(Span::empty()), prng_annotation.clone()],
ret: Annotation::Constructor {
location: Span::empty(),
module: None,

View File

@ -477,33 +477,18 @@ pub fn prelude(id_gen: &IdGenerator) -> TypeInfo {
),
);
// Fuzzer
// Generator
//
// pub type Fuzzer<a> =
// fn(PRNG) -> Option<(PRNG, a)>
let fuzzer_value = Type::generic_var(id_gen.next());
// 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());
prelude.types.insert(
well_known::FUZZER.to_string(),
well_known::GENERATOR.to_string(),
TypeConstructor {
location: Span::empty(),
parameters: vec![fuzzer_value.clone()],
tipo: Type::fuzzer(fuzzer_value),
module: "".to_string(),
public: true,
},
);
// ScaledFuzzer
//
// pub type ScaledFuzzer<a> =
// fn(PRNG, Int) -> Option<(PRNG, a)>
let scaled_fuzzer_value = Type::generic_var(id_gen.next());
prelude.types.insert(
well_known::SCALED_FUZZER.to_string(),
TypeConstructor {
location: Span::empty(),
parameters: vec![scaled_fuzzer_value.clone()],
tipo: Type::scaled_fuzzer(scaled_fuzzer_value),
parameters: vec![generator_context.clone(), generator_value.clone()],
tipo: Type::generator(generator_context, generator_value),
module: "".to_string(),
public: true,
},

View File

@ -258,6 +258,15 @@ impl<'comments> Formatter<'comments> {
..
}) => self.definition_test(name, args, body, *end_position, on_test_failure),
Definition::Benchmark(Function {
name,
arguments: args,
body,
end_position,
on_test_failure,
..
}) => self.definition_benchmark(name, args, body, *end_position, on_test_failure),
Definition::TypeAlias(TypeAlias {
alias,
parameters: args,
@ -631,6 +640,43 @@ impl<'comments> Formatter<'comments> {
.append("}")
}
#[allow(clippy::too_many_arguments)]
fn definition_benchmark<'a>(
&mut self,
name: &'a str,
args: &'a [UntypedArgVia],
body: &'a UntypedExpr,
end_location: usize,
on_test_failure: &'a OnTestFailure,
) -> Document<'a> {
// Fn name and args
let head = "benchmark "
.to_doc()
.append(name)
.append(wrap_args(args.iter().map(|e| (self.fn_arg_via(e), false))))
.append(match on_test_failure {
OnTestFailure::FailImmediately => "",
OnTestFailure::SucceedEventually => "",
OnTestFailure::SucceedImmediately => "",
})
.group();
// Format body
let body = self.expr(body, true);
// Add any trailing comments
let body = match printed_comments(self.pop_comments(end_location), false) {
Some(comments) => body.append(line()).append(comments),
None => body,
};
// Stick it all together
head.append(" {")
.append(line().append(body).nest(INDENT).group())
.append(line())
.append("}")
}
fn definition_validator<'a>(
&mut self,
name: &'a str,

View File

@ -0,0 +1,166 @@
use crate::{
ast,
ast::OnTestFailure,
expr::UntypedExpr,
parser::{
annotation,
chain::{call::parser as call, field_access, tuple_index::parser as tuple_index, Chain},
error::ParseError,
expr::{self, bytearray, int as uint, list, string, tuple, var},
pattern,
token::Token,
},
};
use chumsky::prelude::*;
pub fn parser() -> impl Parser<Token, ast::UntypedDefinition, Error = ParseError> {
just(Token::Benchmark)
.ignore_then(select! {Token::Name {name} => name})
.then(
via()
.separated_by(just(Token::Comma))
.allow_trailing()
.delimited_by(just(Token::LeftParen), just(Token::RightParen)),
)
.then(
just(Token::Fail)
.ignore_then(just(Token::Once).ignored().or_not().map(|once| {
once.map(|_| OnTestFailure::SucceedImmediately)
.unwrap_or(OnTestFailure::SucceedEventually)
}))
.or_not(),
)
.map_with_span(|name, span| (name, span))
.then(
expr::sequence()
.or_not()
.delimited_by(just(Token::LeftBrace), just(Token::RightBrace)),
)
.map_with_span(|((((name, arguments), fail), span_end), body), span| {
ast::UntypedDefinition::Benchmark(ast::Function {
arguments,
body: body.unwrap_or_else(|| UntypedExpr::todo(None, span)),
doc: None,
location: span_end,
end_position: span.end - 1,
name,
public: false,
return_annotation: None,
return_type: (),
on_test_failure: fail.unwrap_or(OnTestFailure::FailImmediately),
})
})
}
pub fn via() -> impl Parser<Token, ast::UntypedArgVia, Error = ParseError> {
choice((
select! {Token::DiscardName {name} => name}.map_with_span(|name, span| {
ast::ArgBy::ByName(ast::ArgName::Discarded {
label: name.clone(),
name,
location: span,
})
}),
select! {Token::Name {name} => name}.map_with_span(|name, location| {
ast::ArgBy::ByName(ast::ArgName::Named {
label: name.clone(),
name,
location,
})
}),
pattern().map(ast::ArgBy::ByPattern),
))
.then(just(Token::Colon).ignore_then(annotation()).or_not())
.map_with_span(|(arg_name, annotation), location| (arg_name, annotation, location))
.then_ignore(just(Token::Via))
.then(fuzzer())
.map(|((by, annotation, location), via)| ast::ArgVia {
arg: ast::UntypedArg {
by,
annotation,
location,
doc: None,
is_validator_param: false,
},
via,
})
}
pub fn fuzzer<'a>() -> impl Parser<Token, UntypedExpr, Error = ParseError> + 'a {
recursive(|expression| {
let chain = choice((
tuple_index(),
field_access::parser(),
call(expression.clone()),
));
let int = || {
just(Token::Minus)
.to(ast::UnOp::Negate)
.map_with_span(|op, span| (op, span))
.or_not()
.then(uint())
.map(|(op, value)| match op {
None => value,
Some((op, location)) => UntypedExpr::UnOp {
op,
location,
value: Box::new(value),
},
})
};
choice((
int(),
string(),
bytearray(),
tuple(expression.clone()),
list(expression.clone()),
var(),
))
.then(chain.repeated())
.foldl(|expr, chain| match chain {
Chain::Call(args, span) => expr.call(args, span),
Chain::FieldAccess(label, span) => expr.field_access(label, span),
Chain::TupleIndex(index, span) => expr.tuple_index(index, span),
})
})
}
#[cfg(test)]
mod tests {
use crate::assert_definition;
#[test]
fn def_benchmark() {
assert_definition!(
r#"
benchmark foo(x via fuzz.any_int) {
True
}
"#
);
}
#[test]
fn def_invalid_benchmark() {
assert_definition!(
r#"
benchmark foo(x via f, y via g) {
True
}
"#
);
}
#[test]
fn def_benchmark_annotated_fuzzer() {
assert_definition!(
r#"
benchmark foo(x: Int via foo()) {
True
}
"#
);
}
}

View File

@ -1,5 +1,6 @@
use chumsky::prelude::*;
pub mod benchmark;
pub mod constant;
mod data_type;
mod function;
@ -10,6 +11,7 @@ mod validator;
use super::{error::ParseError, token::Token};
use crate::ast;
pub use benchmark::parser as benchmark;
pub use constant::parser as constant;
pub use data_type::parser as data_type;
pub use function::parser as function;
@ -24,6 +26,7 @@ pub fn parser() -> impl Parser<Token, ast::UntypedDefinition, Error = ParseError
validator(),
function(),
test(),
benchmark(),
constant(),
))
}

View File

@ -0,0 +1,47 @@
---
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"
snapshot_kind: text
---
Benchmark(
Function {
arguments: [
ArgVia {
arg: UntypedArg {
by: ByName(
Named {
name: "x",
label: "x",
location: 14..15,
},
),
location: 14..15,
annotation: None,
doc: None,
is_validator_param: false,
},
via: FieldAccess {
location: 20..32,
label: "any_int",
container: Var {
location: 20..24,
name: "fuzz",
},
},
},
],
body: Var {
location: 40..44,
name: "True",
},
doc: None,
location: 0..33,
name: "foo",
public: false,
return_annotation: None,
return_type: (),
end_position: 45,
on_test_failure: FailImmediately,
},
)

View File

@ -0,0 +1,54 @@
---
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"
snapshot_kind: text
---
Benchmark(
Function {
arguments: [
ArgVia {
arg: UntypedArg {
by: ByName(
Named {
name: "x",
label: "x",
location: 14..15,
},
),
location: 14..20,
annotation: Some(
Constructor {
location: 17..20,
module: None,
name: "Int",
arguments: [],
},
),
doc: None,
is_validator_param: false,
},
via: Call {
arguments: [],
fun: Var {
location: 25..28,
name: "foo",
},
location: 25..30,
},
},
],
body: Var {
location: 38..42,
name: "True",
},
doc: None,
location: 0..31,
name: "foo",
public: false,
return_annotation: None,
return_type: (),
end_position: 43,
on_test_failure: FailImmediately,
},
)

View File

@ -0,0 +1,62 @@
---
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"
snapshot_kind: text
---
Benchmark(
Function {
arguments: [
ArgVia {
arg: UntypedArg {
by: ByName(
Named {
name: "x",
label: "x",
location: 14..15,
},
),
location: 14..15,
annotation: None,
doc: None,
is_validator_param: false,
},
via: Var {
location: 20..21,
name: "f",
},
},
ArgVia {
arg: UntypedArg {
by: ByName(
Named {
name: "y",
label: "y",
location: 23..24,
},
),
location: 23..24,
annotation: None,
doc: None,
is_validator_param: false,
},
via: Var {
location: 29..30,
name: "g",
},
},
],
body: Var {
location: 38..42,
name: "True",
},
doc: None,
location: 0..31,
name: "foo",
public: false,
return_annotation: None,
return_type: (),
end_position: 43,
on_test_failure: FailImmediately,
},
)

View File

@ -243,6 +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,
_ => {
if s.chars().next().map_or(false, |c| c.is_uppercase()) {
Token::UpName {

View File

@ -73,6 +73,7 @@ pub enum Token {
NewLine,
// Keywords (alphabetically):
As,
Benchmark,
Const,
Fn,
If,
@ -182,6 +183,7 @@ impl fmt::Display for Token {
Token::Once => "once",
Token::Validator => "validator",
Token::Via => "via",
Token::Benchmark => "benchmark",
};
write!(f, "{s}")
}

View File

@ -49,6 +49,7 @@ use vec1::{vec1, Vec1};
pub enum Test {
UnitTest(UnitTest),
PropertyTest(PropertyTest),
Benchmark(Benchmark)
}
unsafe impl Send for Test {}
@ -442,7 +443,23 @@ impl PropertyTest {
None
}
}
}
/// ----- Benchmark -----------------------------------------------------------------
#[derive(Debug, Clone)]
pub struct Benchmark {
pub input_path: PathBuf,
pub module: String,
pub name: String,
pub on_test_failure: OnTestFailure,
pub program: Program<Name>,
pub fuzzer: Fuzzer<Name>,
}
unsafe impl Send for Benchmark {}
impl Benchmark {
pub fn benchmark(
self,
seed: u32,
@ -484,6 +501,14 @@ impl PropertyTest {
results
}
pub fn eval(&self, value: &PlutusData, plutus_version: &PlutusVersion) -> EvalResult {
let program = self.program.apply_data(value.clone());
Program::<NamedDeBruijn>::try_from(program)
.unwrap()
.eval_version(ExBudget::max(), &plutus_version.into())
}
}
/// ----- PRNG -----------------------------------------------------------------
@ -558,7 +583,10 @@ impl Prng {
choices: vec![],
uplc: Data::constr(
Prng::SEEDED,
vec![Data::bytestring(digest.to_vec()), Data::bytestring(vec![])],
vec![
Data::bytestring(digest.to_vec()), // Prng's seed
Data::bytestring(vec![]), // Random choices
],
),
iteration: 0,
}
@ -585,35 +613,18 @@ impl Prng {
fuzzer: &Program<Name>,
iteration: usize,
) -> Result<Option<(Prng, PlutusData)>, FuzzerError> {
// First try evaluating as a regular fuzzer
let program = Program::<NamedDeBruijn>::try_from(fuzzer.apply_data(self.uplc())).unwrap();
let program_clone = program.clone();
let result = program.eval(ExBudget::max());
match result.result() {
Ok(term) if matches!(term, Term::Constant(_)) => {
// If we got a valid constant result, process it
Ok(Prng::from_result(term, iteration))
}
_ => {
// Use the cloned program for the second attempt
let program_with_iteration = program_clone
.apply_data(Data::integer(num_bigint::BigInt::from(iteration as i64)));
let mut result = program_with_iteration.eval(ExBudget::max());
match result.result() {
Ok(term) if matches!(term, Term::Constant(_)) => {
Ok(Prng::from_result(term, iteration))
}
Err(uplc_error) => Err(FuzzerError {
traces: result.logs(),
uplc_error,
}),
_ => unreachable!("Fuzzer returned a malformed result? {result:#?}"),
}
}
}
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
.result()
.map_err(|uplc_error| FuzzerError {
traces: result.logs(),
uplc_error,
})
.map(|term| Prng::from_result(term, iteration))
}
/// Obtain a Prng back from a fuzzer execution. As a reminder, fuzzers have the following
@ -1431,7 +1442,7 @@ impl Assertion<UntypedExpr> {
#[derive(Debug, Clone)]
pub struct BenchmarkResult {
pub test: PropertyTest,
pub test: Benchmark,
pub cost: ExBudget,
pub success: bool,
pub traces: Vec<String>,

View File

@ -1773,7 +1773,7 @@ fn pipe_wrong_arity_fully_saturated_return_fn() {
#[test]
fn fuzzer_ok_basic() {
let source_code = r#"
fn int() -> Fuzzer<Int> { todo }
fn int() -> Generator<Void, Int> { todo }
test prop(n via int()) { True }
"#;
@ -1783,7 +1783,7 @@ fn fuzzer_ok_basic() {
#[test]
fn fuzzer_ok_explicit() {
let source_code = r#"
fn int(prng: PRNG) -> Option<(PRNG, Int)> { todo }
fn int(void: Void, prng: PRNG) -> Option<(PRNG, Int)> { todo }
test prop(n via int) { Void }
"#;
@ -1793,8 +1793,8 @@ fn fuzzer_ok_explicit() {
#[test]
fn fuzzer_ok_list() {
let source_code = r#"
fn int() -> Fuzzer<Int> { todo }
fn list(a: Fuzzer<a>) -> Fuzzer<List<a>> { todo }
fn int() -> Generator<Void, Int> { todo }
fn list(a: Generator<Void, a>) -> Generator<Void, List<a>> { todo }
test prop(xs via list(int())) { True }
"#;
@ -1805,8 +1805,8 @@ fn fuzzer_ok_list() {
#[test]
fn fuzzer_err_unbound() {
let source_code = r#"
fn any() -> Fuzzer<a> { todo }
fn list(a: Fuzzer<a>) -> Fuzzer<List<a>> { todo }
fn any() -> Generator<Void, a> { todo }
fn list(a: Generator<Void, a>) -> Generator<Void, List<a>> { todo }
test prop(xs via list(any())) { todo }
"#;
@ -1838,7 +1838,7 @@ fn fuzzer_err_unify_1() {
#[test]
fn fuzzer_err_unify_2() {
let source_code = r#"
fn any() -> Fuzzer<a> { todo }
fn any() -> Generator<Void, a> { todo }
test prop(xs via any) { todo }
"#;
@ -1857,8 +1857,8 @@ fn fuzzer_err_unify_2() {
#[test]
fn fuzzer_err_unify_3() {
let source_code = r#"
fn list(a: Fuzzer<a>) -> Fuzzer<List<a>> { todo }
fn int() -> Fuzzer<Int> { todo }
fn list(a: Generator<Void, a>) -> Generator<Void, List<a>> { todo }
fn int() -> Generator<Void, Int> { todo }
test prop(xs: Int via list(int())) { todo }
"#;
@ -1875,45 +1875,6 @@ fn fuzzer_err_unify_3() {
))
}
#[test]
fn scaled_fuzzer_ok_basic() {
let source_code = r#"
fn int() -> ScaledFuzzer<Int> { todo }
test prop(n via int()) { True }
"#;
assert!(check(parse(source_code)).is_ok());
}
#[test]
fn scaled_fuzzer_ok_explicit() {
let source_code = r#"
fn int(prng: PRNG, complexity: Int) -> Option<(PRNG, Int)> { todo }
test prop(n via int) { True }
"#;
assert!(check(parse(source_code)).is_ok());
}
#[test]
fn scaled_fuzzer_err_unify() {
let source_code = r#"
fn int() -> ScaledFuzzer<Int> { todo }
test prop(n: Bool via int()) { True }
"#;
assert!(matches!(
check(parse(source_code)),
Err((
_,
Error::CouldNotUnify {
situation: Some(UnifyErrorSituation::FuzzerAnnotationMismatch),
..
}
))
));
}
#[test]
fn utf8_hex_literal_warning() {
let source_code = r#"

View File

@ -367,6 +367,7 @@ impl<'a> Environment<'a> {
| Definition::DataType { .. }
| Definition::Use { .. }
| Definition::Test { .. }
| Definition::Benchmark { .. }
| Definition::ModuleConstant { .. }) => definition,
}
}
@ -1061,6 +1062,7 @@ impl<'a> Environment<'a> {
| Definition::Validator { .. }
| Definition::Use { .. }
| Definition::ModuleConstant { .. }
| Definition::Benchmark { .. }
| Definition::Test { .. } => None,
})
.collect::<Vec<Span>>();
@ -1184,6 +1186,7 @@ impl<'a> Environment<'a> {
Definition::Fn { .. }
| Definition::Validator { .. }
| Definition::Test { .. }
| Definition::Benchmark { .. }
| Definition::Use { .. }
| Definition::ModuleConstant { .. } => {}
}
@ -1387,6 +1390,24 @@ impl<'a> Environment<'a> {
})
}
Definition::Benchmark(benchmark) => {
let arguments = benchmark
.arguments
.iter()
.map(|arg| arg.clone().into())
.collect::<Vec<_>>();
self.register_function(
&benchmark.name,
&arguments,
&benchmark.return_annotation,
module_name,
hydrators,
names,
&benchmark.location,
)?;
}
Definition::Test(test) => {
let arguments = test
.arguments

View File

@ -81,6 +81,7 @@ impl UntypedModule {
Definition::Validator { .. } => (),
Definition::Fn { .. }
| Definition::Test { .. }
| Definition::Benchmark { .. }
| Definition::TypeAlias { .. }
| Definition::DataType { .. }
| Definition::Use { .. } => not_consts.push(def),
@ -364,15 +365,7 @@ fn infer_definition(
&arg.via.location(),
) {
Ok(result) => Ok(result),
Err(err) => match err {
Error::CouldNotUnify { .. } => infer_scaled_fuzzer(
environment,
provided_inner_type.clone(),
&typed_via.tipo(),
&arg.via.location(),
),
_ => Err(err),
},
Err(err) => Err(err)
}?;
// Ensure that the annotation, if any, matches the type inferred from the
@ -475,6 +468,142 @@ fn infer_definition(
}))
}
Definition::Benchmark(f) => {
let (typed_via, annotation) = match f.arguments.first() {
Some(arg) => {
if f.arguments.len() > 1 {
return Err(Error::IncorrectTestArity {
count: f.arguments.len(),
location: f
.arguments
.get(1)
.expect("arguments.len() > 1")
.arg
.location,
});
}
let typed_via = ExprTyper::new(environment, tracing).infer(arg.via.clone())?;
let hydrator: &mut Hydrator = hydrators.get_mut(&f.name).unwrap();
let provided_inner_type = arg
.arg
.annotation
.as_ref()
.map(|ann| hydrator.type_from_annotation(ann, environment))
.transpose()?;
let (inferred_annotation, inferred_inner_type) = match infer_sampler(
environment,
provided_inner_type.clone(),
&typed_via.tipo(),
&arg.via.location(),
) {
Ok(result) => Ok(result),
Err(err) => Err(err)
}?;
// Ensure that the annotation, if any, matches the type inferred from the
// Fuzzer.
if let Some(provided_inner_type) = provided_inner_type {
if !arg
.arg
.annotation
.as_ref()
.unwrap()
.is_logically_equal(&inferred_annotation)
{
return Err(Error::CouldNotUnify {
location: arg.arg.location,
expected: inferred_inner_type.clone(),
given: provided_inner_type.clone(),
situation: Some(UnifyErrorSituation::FuzzerAnnotationMismatch),
rigid_type_names: hydrator.rigid_names(),
});
}
}
// Replace the pre-registered type for the test function, to allow inferring
// the function body with the right type arguments.
let scope = environment
.scope
.get_mut(&f.name)
.expect("Could not find preregistered type for benchmark");
if let Type::Fn {
ref ret,
ref alias,
args: _,
} = scope.tipo.as_ref()
{
scope.tipo = Rc::new(Type::Fn {
ret: ret.clone(),
args: vec![inferred_inner_type.clone()],
alias: alias.clone(),
})
}
Ok((
Some((typed_via, inferred_inner_type)),
Some(inferred_annotation),
))
}
None => Ok((None, None)),
}?;
let typed_f = infer_function(&f.into(), module_name, hydrators, environment, tracing)?;
let is_bool = environment.unify(
typed_f.return_type.clone(),
Type::bool(),
typed_f.location,
false,
);
let is_void = environment.unify(
typed_f.return_type.clone(),
Type::void(),
typed_f.location,
false,
);
if is_bool.or(is_void).is_err() {
return Err(Error::IllegalTestType {
location: typed_f.location,
});
}
Ok(Definition::Test(Function {
doc: typed_f.doc,
location: typed_f.location,
name: typed_f.name,
public: typed_f.public,
arguments: match typed_via {
Some((via, tipo)) => {
let arg = typed_f
.arguments
.first()
.expect("has exactly one argument")
.to_owned();
vec![ArgVia {
arg: TypedArg {
tipo,
annotation,
..arg
},
via,
}]
}
None => vec![],
},
return_annotation: typed_f.return_annotation,
return_type: typed_f.return_type,
body: typed_f.body,
on_test_failure: typed_f.on_test_failure,
end_position: typed_f.end_position,
}))
}
Definition::TypeAlias(TypeAlias {
doc,
location,
@ -709,7 +838,8 @@ fn infer_fuzzer(
) -> Result<(Annotation, Rc<Type>), Error> {
let could_not_unify = || Error::CouldNotUnify {
location: *location,
expected: Type::fuzzer(
expected: Type::generator(
Type::void(),
expected_inner_type
.clone()
.unwrap_or_else(|| Type::generic_var(0)),
@ -733,7 +863,7 @@ fn infer_fuzzer(
contains_opaque: _,
alias: _,
} if module.is_empty() && name == "Option" && args.len() == 1 => {
match args.first().expect("args.len() == 1").borrow() {
match args.first().expect("args.len() == 2 && args[0].is_void()").borrow() {
Type::Tuple { elems, .. } if elems.len() == 2 => {
let wrapped = elems.get(1).expect("Tuple has two elements");
@ -748,7 +878,7 @@ fn infer_fuzzer(
// `unify` now that we have figured out the type carried by the fuzzer.
environment.unify(
tipo.clone(),
Type::fuzzer(wrapped.clone()),
Type::generator(Type::void(), wrapped.clone()),
*location,
false,
)?;
@ -777,6 +907,75 @@ fn infer_fuzzer(
}
}
#[allow(clippy::result_large_err)]
fn infer_sampler(
environment: &mut Environment<'_>,
expected_inner_type: Option<Rc<Type>>,
tipo: &Rc<Type>,
location: &Span,
) -> Result<(Annotation, Rc<Type>), Error> {
let could_not_unify = || Error::CouldNotUnify {
location: *location,
expected: Type::generator(
Type::int(),
expected_inner_type
.clone()
.unwrap_or_else(|| Type::generic_var(0)),
),
given: tipo.clone(),
situation: None,
rigid_type_names: HashMap::new(),
};
match tipo.borrow() {
Type::Fn {
ret,
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()),
},
Type::Var { tipo, alias } => match &*tipo.deref().borrow() {
TypeVar::Link { tipo } => infer_sampler(
environment,
expected_inner_type,
&Type::with_alias(tipo.clone(), alias.clone()),
location,
),
_ => Err(Error::GenericLeftAtBoundary {
location: *location,
}),
},
Type::App { .. } | Type::Tuple { .. } | Type::Pair { .. } => Err(could_not_unify()),
}
}
#[allow(clippy::result_large_err)]
fn annotate_fuzzer(tipo: &Type, location: &Span) -> Result<Annotation, Error> {
match tipo {
@ -837,75 +1036,6 @@ fn annotate_fuzzer(tipo: &Type, location: &Span) -> Result<Annotation, Error> {
}
}
#[allow(clippy::result_large_err)]
fn infer_scaled_fuzzer(
environment: &mut Environment<'_>,
expected_inner_type: Option<Rc<Type>>,
tipo: &Rc<Type>,
location: &Span,
) -> Result<(Annotation, Rc<Type>), Error> {
let could_not_unify = || Error::CouldNotUnify {
location: *location,
expected: Type::scaled_fuzzer(
expected_inner_type
.clone()
.unwrap_or_else(|| Type::generic_var(0)),
),
given: tipo.clone(),
situation: None,
rigid_type_names: Default::default(),
};
match tipo.borrow() {
Type::Fn { ret, args, .. } => {
// Check if this is a ScaledFuzzer (fn(PRNG, Int) -> Option<(PRNG, a)>)
if args.len() == 2 {
match ret.borrow() {
Type::App {
module,
name,
args: ret_args,
..
} if module.is_empty() && name == "Option" && ret_args.len() == 1 => {
if let Type::Tuple { elems, .. } = ret_args[0].borrow() {
if elems.len() == 2 {
let wrapped = &elems[1];
// Unify with expected ScaledFuzzer type
environment.unify(
tipo.clone(),
Type::scaled_fuzzer(wrapped.clone()),
*location,
false,
)?;
return Ok((annotate_fuzzer(wrapped, location)?, wrapped.clone()));
}
}
}
_ => (),
}
}
Err(could_not_unify())
}
Type::Var { tipo, alias } => match &*tipo.deref().borrow() {
TypeVar::Link { tipo } => infer_scaled_fuzzer(
environment,
expected_inner_type,
&Type::with_alias(tipo.clone(), alias.clone()),
location,
),
_ => Err(Error::GenericLeftAtBoundary {
location: *location,
}),
},
Type::App { .. } | Type::Tuple { .. } | Type::Pair { .. } => Err(could_not_unify()),
}
}
fn put_params_in_scope<'a>(
name: &'_ str,
environment: &'a mut Environment,

View File

@ -540,6 +540,7 @@ fn find_data_type(name: &str, definitions: &[TypedDefinition]) -> Option<TypedDa
| Definition::TypeAlias { .. }
| Definition::Use { .. }
| Definition::ModuleConstant { .. }
| Definition::Benchmark { .. }
| Definition::Test { .. } => continue,
}
}

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()),
TestResult::Benchmark(_) => ("benchmark".to_string(), PathBuf::new(), String::new()), // todo
};
Error::TestFailure {

View File

@ -302,7 +302,7 @@ where
match_tests: Option<Vec<String>>,
exact_match: bool,
seed: u32,
property_max_success: usize,
times_to_run: usize,
env: Option<String>,
output: PathBuf,
) -> Result<(), Vec<Error>> {
@ -313,7 +313,7 @@ where
match_tests,
exact_match,
seed,
property_max_success,
times_to_run,
output,
},
blueprint_path: self.blueprint_path(None),
@ -470,16 +470,17 @@ where
match_tests,
exact_match,
seed,
property_max_success,
times_to_run,
output,
} => {
let tests = self.collect_tests(false, match_tests, exact_match, options.tracing)?;
// todo - collect benchmarks
let tests = self.collect_benchmarks(false, match_tests, exact_match, options.tracing)?;
if !tests.is_empty() {
self.event_listener.handle_event(Event::RunningBenchmarks);
}
let tests = self.run_benchmarks(tests, seed, property_max_success);
let tests = self.run_benchmarks(tests, seed, times_to_run);
let errors: Vec<Error> = tests
.iter()
@ -994,6 +995,107 @@ where
Ok(())
}
fn collect_benchmarks(
&mut self,
verbose: bool,
match_tests: Option<Vec<String>>,
exact_match: bool,
tracing: Tracing,
) -> Result<Vec<Test>, Error> {
let mut scripts = Vec::new();
let match_tests = match_tests.map(|mt| {
mt.into_iter()
.map(|match_test| {
let mut match_split_dot = match_test.split('.');
let match_module = if match_test.contains('.') || match_test.contains('/') {
match_split_dot.next().unwrap_or("")
} else {
""
};
let match_names = match_split_dot.next().map(|names| {
let names = names.replace(&['{', '}'][..], "");
let names_split_comma = names.split(',');
names_split_comma.map(str::to_string).collect()
});
(match_module.to_string(), match_names)
})
.collect::<Vec<(String, Option<Vec<String>>)>>()
});
for checked_module in self.checked_modules.values() {
if checked_module.package != self.config.name.to_string() {
continue;
}
for def in checked_module.ast.definitions() {
if let Definition::Benchmark(func) = def {
if let Some(match_tests) = &match_tests {
let is_match = match_tests.iter().any(|(module, names)| {
let matched_module =
module.is_empty() || checked_module.name.contains(module);
let matched_name = match names {
None => true,
Some(names) => names.iter().any(|name| {
if exact_match {
name == &func.name
} else {
func.name.contains(name)
}
}),
};
matched_module && matched_name
});
if is_match {
scripts.push((
checked_module.input_path.clone(),
checked_module.name.clone(),
func,
))
}
} else {
scripts.push((
checked_module.input_path.clone(),
checked_module.name.clone(),
func,
))
}
}
}
}
let mut generator = self.new_generator(tracing);
let mut tests = Vec::new();
for (input_path, module_name, test) in scripts.into_iter() {
if verbose {
// TODO: We may want to handle the event listener differently for benchmarks
self.event_listener.handle_event(Event::GeneratingUPLCFor {
name: test.name.clone(),
path: input_path.clone(),
})
}
tests.push(Test::from_function_definition(
&mut generator,
test.to_owned(),
module_name,
input_path,
));
}
Ok(tests)
}
fn collect_tests(
&mut self,
verbose: bool,
@ -1113,6 +1215,7 @@ where
Test::PropertyTest(property_test) => {
property_test.run(seed, property_max_success, plutus_version)
}
Test::Benchmark(_) => unreachable!("Benchmarks cannot be run in PBT.")
})
.collect::<Vec<TestResult<(Constant, Rc<Type>), PlutusData>>>()
.into_iter()
@ -1134,8 +1237,8 @@ where
tests
.into_par_iter()
.flat_map(|test| match test {
Test::UnitTest(_) => Vec::new(),
Test::PropertyTest(property_test) => property_test
Test::UnitTest(_) | Test::PropertyTest(_) => unreachable!("Tests cannot be ran during benchmarking."),
Test::Benchmark(benchmark) => benchmark
.benchmark(seed, property_max_success, plutus_version)
.into_iter()
.map(TestResult::Benchmark)

View File

@ -33,7 +33,7 @@ pub enum CodeGenMode {
match_tests: Option<Vec<String>>,
exact_match: bool,
seed: u32,
property_max_success: usize,
times_to_run: usize,
output: PathBuf,
},
NoOp,

View File

@ -112,8 +112,10 @@ mod test {
const max_int: Int = 255
pub type Fuzzer<a> = Generator<Void, a>
pub fn int() -> Fuzzer<Int> {
fn(prng: PRNG) -> Option<(PRNG, Int)> {
fn(v: Void, prng: PRNG) -> Option<(PRNG, Int)> {
when prng is {
Seeded { seed, choices } -> {
let choice =
@ -161,21 +163,21 @@ mod test {
}
pub fn constant(a: a) -> Fuzzer<a> {
fn(s0) { Some((s0, a)) }
fn(v, s0) { Some((s0, a)) }
}
pub fn and_then(fuzz_a: Fuzzer<a>, f: fn(a) -> Fuzzer<b>) -> Fuzzer<b> {
fn(s0) {
when fuzz_a(s0) is {
Some((s1, a)) -> f(a)(s1)
fn(v, s0) {
when fuzz_a(v, s0) is {
Some((s1, a)) -> f(a)(v, s1)
None -> None
}
}
}
pub fn map(fuzz_a: Fuzzer<a>, f: fn(a) -> b) -> Fuzzer<b> {
fn(s0) {
when fuzz_a(s0) is {
fn(v, s0) {
when fuzz_a(v, s0) is {
Some((s1, a)) -> Some((s1, f(a)))
None -> None
}
@ -183,10 +185,10 @@ mod test {
}
pub fn map2(fuzz_a: Fuzzer<a>, fuzz_b: Fuzzer<b>, f: fn(a, b) -> c) -> Fuzzer<c> {
fn(s0) {
when fuzz_a(s0) is {
fn(v, s0) {
when fuzz_a(v, s0) is {
Some((s1, a)) ->
when fuzz_b(s1) is {
when fuzz_b(Void, s1) is {
Some((s2, b)) -> Some((s2, f(a, b)))
None -> None
}
@ -214,6 +216,9 @@ mod test {
(Test::UnitTest(..), _) => {
panic!("Expected to yield a PropertyTest but found a UnitTest")
}
(Test::Benchmark(..), _) => {
panic!("Expected to yield a PropertyTest but found a Benchmark")
}
}
}

View File

@ -17,9 +17,9 @@ pub struct Args {
#[clap(long)]
seed: Option<u32>,
/// Maximum number of successful test run for considering a property-based test valid.
/// How many times we will run each benchmark in the relevant project.
#[clap(long, default_value_t = PropertyTest::DEFAULT_MAX_SUCCESS)]
max_success: usize,
times_to_run: usize,
/// Only run tests if they match any of these strings.
/// You can match a module with `-m aiken/list` or `-m list`.
@ -46,7 +46,7 @@ pub fn exec(
match_tests,
exact_match,
seed,
max_success,
times_to_run,
env,
output,
}: Args,
@ -65,7 +65,7 @@ pub fn exec(
match_tests.clone(),
exact_match,
seed,
max_success,
times_to_run,
env.clone(),
output.clone(),
)