diff --git a/crates/aiken-lang/src/ast.rs b/crates/aiken-lang/src/ast.rs index 6eb3162d..0569595c 100644 --- a/crates/aiken-lang/src/ast.rs +++ b/crates/aiken-lang/src/ast.rs @@ -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 { "or" => Some(Token::Or), "validator" => Some(Token::Validator), "via" => Some(Token::Via), + "benchmark" => Some(Token::Benchmark), _ => None, } } @@ -832,6 +845,8 @@ pub enum Definition { Test(Function>), + Benchmark(Function>), + Validator(Validator), } @@ -844,6 +859,7 @@ impl Definition { | 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 Definition { | 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 Definition { | Definition::DataType(DataType { doc, .. }) | Definition::ModuleConstant(ModuleConstant { doc, .. }) | Definition::Validator(Validator { doc, .. }) + | Definition::Benchmark(Function { doc, .. }) | Definition::Test(Function { doc, .. }) => doc.clone(), } } diff --git a/crates/aiken-lang/src/ast/well_known.rs b/crates/aiken-lang/src/ast/well_known.rs index 45248ef0..04624571 100644 --- a/crates/aiken-lang/src/ast/well_known.rs +++ b/crates/aiken-lang/src/ast/well_known.rs @@ -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) -> Rc { + pub fn generator(c: Rc, a: Rc) -> Rc { 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) -> Rc { - 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, diff --git a/crates/aiken-lang/src/builtins.rs b/crates/aiken-lang/src/builtins.rs index 69e8bb61..ff7891a1 100644 --- a/crates/aiken-lang/src/builtins.rs +++ b/crates/aiken-lang/src/builtins.rs @@ -477,33 +477,18 @@ pub fn prelude(id_gen: &IdGenerator) -> TypeInfo { ), ); - // Fuzzer + // Generator // - // pub type Fuzzer = - // fn(PRNG) -> Option<(PRNG, a)> - let fuzzer_value = Type::generic_var(id_gen.next()); + // pub type Generator = + // 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 = - // 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, }, diff --git a/crates/aiken-lang/src/format.rs b/crates/aiken-lang/src/format.rs index fc7c3213..93d8eb1d 100644 --- a/crates/aiken-lang/src/format.rs +++ b/crates/aiken-lang/src/format.rs @@ -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, diff --git a/crates/aiken-lang/src/parser/definition/benchmark.rs b/crates/aiken-lang/src/parser/definition/benchmark.rs new file mode 100644 index 00000000..85c31acc --- /dev/null +++ b/crates/aiken-lang/src/parser/definition/benchmark.rs @@ -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 { + 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 { + 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 + '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 + } + "# + ); + } +} diff --git a/crates/aiken-lang/src/parser/definition/mod.rs b/crates/aiken-lang/src/parser/definition/mod.rs index 871c54f8..843d2cb6 100644 --- a/crates/aiken-lang/src/parser/definition/mod.rs +++ b/crates/aiken-lang/src/parser/definition/mod.rs @@ -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 impl Parser, 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 { diff --git a/crates/aiken-lang/src/parser/token.rs b/crates/aiken-lang/src/parser/token.rs index 78c754ae..f4d2e7e6 100644 --- a/crates/aiken-lang/src/parser/token.rs +++ b/crates/aiken-lang/src/parser/token.rs @@ -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}") } diff --git a/crates/aiken-lang/src/test_framework.rs b/crates/aiken-lang/src/test_framework.rs index c8129cd4..31e226ed 100644 --- a/crates/aiken-lang/src/test_framework.rs +++ b/crates/aiken-lang/src/test_framework.rs @@ -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, + pub fuzzer: Fuzzer, +} + +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::::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, iteration: usize, ) -> Result, FuzzerError> { - // First try evaluating as a regular fuzzer - let program = Program::::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::::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 { #[derive(Debug, Clone)] pub struct BenchmarkResult { - pub test: PropertyTest, + pub test: Benchmark, pub cost: ExBudget, pub success: bool, pub traces: Vec, diff --git a/crates/aiken-lang/src/tests/check.rs b/crates/aiken-lang/src/tests/check.rs index a1820afd..5028e3e9 100644 --- a/crates/aiken-lang/src/tests/check.rs +++ b/crates/aiken-lang/src/tests/check.rs @@ -1773,7 +1773,7 @@ fn pipe_wrong_arity_fully_saturated_return_fn() { #[test] fn fuzzer_ok_basic() { let source_code = r#" - fn int() -> Fuzzer { todo } + fn int() -> Generator { 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 { todo } - fn list(a: Fuzzer) -> Fuzzer> { todo } + fn int() -> Generator { todo } + fn list(a: Generator) -> Generator> { 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 { todo } - fn list(a: Fuzzer) -> Fuzzer> { todo } + fn any() -> Generator { todo } + fn list(a: Generator) -> Generator> { 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 { todo } + fn any() -> Generator { 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) -> Fuzzer> { todo } - fn int() -> Fuzzer { todo } + fn list(a: Generator) -> Generator> { todo } + fn int() -> Generator { 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 { 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 { 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#" diff --git a/crates/aiken-lang/src/tipo/environment.rs b/crates/aiken-lang/src/tipo/environment.rs index 076808e1..67e23d73 100644 --- a/crates/aiken-lang/src/tipo/environment.rs +++ b/crates/aiken-lang/src/tipo/environment.rs @@ -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::>(); @@ -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::>(); + + self.register_function( + &benchmark.name, + &arguments, + &benchmark.return_annotation, + module_name, + hydrators, + names, + &benchmark.location, + )?; + } + Definition::Test(test) => { let arguments = test .arguments diff --git a/crates/aiken-lang/src/tipo/infer.rs b/crates/aiken-lang/src/tipo/infer.rs index f7acc510..042fdd3b 100644 --- a/crates/aiken-lang/src/tipo/infer.rs +++ b/crates/aiken-lang/src/tipo/infer.rs @@ -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), 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>, + tipo: &Rc, + location: &Span, +) -> Result<(Annotation, Rc), 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 { match tipo { @@ -837,75 +1036,6 @@ fn annotate_fuzzer(tipo: &Type, location: &Span) -> Result { } } -#[allow(clippy::result_large_err)] -fn infer_scaled_fuzzer( - environment: &mut Environment<'_>, - expected_inner_type: Option>, - tipo: &Rc, - location: &Span, -) -> Result<(Annotation, Rc), 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, diff --git a/crates/aiken-project/src/blueprint/schema.rs b/crates/aiken-project/src/blueprint/schema.rs index 0cb5fe9b..f8b68c2c 100644 --- a/crates/aiken-project/src/blueprint/schema.rs +++ b/crates/aiken-project/src/blueprint/schema.rs @@ -540,6 +540,7 @@ fn find_data_type(name: &str, definitions: &[TypedDefinition]) -> Option continue, } } diff --git a/crates/aiken-project/src/error.rs b/crates/aiken-project/src/error.rs index cb1d1d69..85a8c4e5 100644 --- a/crates/aiken-project/src/error.rs +++ b/crates/aiken-project/src/error.rs @@ -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 { diff --git a/crates/aiken-project/src/lib.rs b/crates/aiken-project/src/lib.rs index a8eff857..204e7509 100644 --- a/crates/aiken-project/src/lib.rs +++ b/crates/aiken-project/src/lib.rs @@ -302,7 +302,7 @@ where match_tests: Option>, exact_match: bool, seed: u32, - property_max_success: usize, + times_to_run: usize, env: Option, output: PathBuf, ) -> Result<(), Vec> { @@ -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 = tests .iter() @@ -994,6 +995,107 @@ where Ok(()) } + fn collect_benchmarks( + &mut self, + verbose: bool, + match_tests: Option>, + exact_match: bool, + tracing: Tracing, + ) -> Result, 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::>)>>() + }); + + 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::), 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) diff --git a/crates/aiken-project/src/options.rs b/crates/aiken-project/src/options.rs index e409d1c2..2ccf5fb2 100644 --- a/crates/aiken-project/src/options.rs +++ b/crates/aiken-project/src/options.rs @@ -33,7 +33,7 @@ pub enum CodeGenMode { match_tests: Option>, exact_match: bool, seed: u32, - property_max_success: usize, + times_to_run: usize, output: PathBuf, }, NoOp, diff --git a/crates/aiken-project/src/test_framework.rs b/crates/aiken-project/src/test_framework.rs index c2cc6eb1..e12072bc 100644 --- a/crates/aiken-project/src/test_framework.rs +++ b/crates/aiken-project/src/test_framework.rs @@ -112,8 +112,10 @@ mod test { const max_int: Int = 255 + pub type Fuzzer = Generator + pub fn int() -> Fuzzer { - 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 { - fn(s0) { Some((s0, a)) } + fn(v, s0) { Some((s0, a)) } } pub fn and_then(fuzz_a: Fuzzer, f: fn(a) -> Fuzzer) -> Fuzzer { - 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, f: fn(a) -> b) -> Fuzzer { - 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, fuzz_b: Fuzzer, f: fn(a, b) -> c) -> Fuzzer { - 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") + } } } diff --git a/crates/aiken/src/cmd/benchmark.rs b/crates/aiken/src/cmd/benchmark.rs index b471fc4b..1ba3506a 100644 --- a/crates/aiken/src/cmd/benchmark.rs +++ b/crates/aiken/src/cmd/benchmark.rs @@ -17,9 +17,9 @@ pub struct Args { #[clap(long)] seed: Option, - /// 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(), )