diff --git a/Cargo.lock b/Cargo.lock index e97a58dc..89a9b33b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,6 +88,7 @@ dependencies = [ "num-bigint", "ordinal", "owo-colors", + "pallas", "petgraph", "pretty_assertions", "strum", diff --git a/crates/aiken-lang/Cargo.toml b/crates/aiken-lang/Cargo.toml index 8ff5bc89..4f5f6d76 100644 --- a/crates/aiken-lang/Cargo.toml +++ b/crates/aiken-lang/Cargo.toml @@ -21,6 +21,7 @@ itertools = "0.10.5" miette = "5.9.0" ordinal = "0.3.2" owo-colors = { version = "3.5.0", features = ["supports-colors"] } +pallas.workspace = true strum = "0.24.1" thiserror = "1.0.39" vec1 = "1.10.1" diff --git a/crates/aiken-lang/src/ast.rs b/crates/aiken-lang/src/ast.rs index faf7ff98..58751258 100644 --- a/crates/aiken-lang/src/ast.rs +++ b/crates/aiken-lang/src/ast.rs @@ -242,6 +242,18 @@ pub struct TypeAlias { pub tipo: T, } +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub struct DataTypeKey { + pub module_name: String, + pub defined_type: String, +} + +#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub struct FunctionAccessKey { + pub module_name: String, + pub function_name: String, +} + pub type TypedDataType = DataType>; impl TypedDataType { diff --git a/crates/aiken-lang/src/builtins.rs b/crates/aiken-lang/src/builtins.rs index c3b0d662..45da5a00 100644 --- a/crates/aiken-lang/src/builtins.rs +++ b/crates/aiken-lang/src/builtins.rs @@ -1,7 +1,9 @@ use crate::{ - ast::{Arg, ArgName, CallArg, Function, ModuleKind, Span, TypedDataType, TypedFunction, UnOp}, + ast::{ + Arg, ArgName, CallArg, DataTypeKey, Function, FunctionAccessKey, ModuleKind, Span, + TypedDataType, TypedFunction, UnOp, + }, expr::TypedExpr, - gen_uplc::builder::{DataTypeKey, FunctionAccessKey}, tipo::{ fields::FieldMap, Type, TypeConstructor, TypeInfo, TypeVar, ValueConstructor, ValueConstructorVariant, diff --git a/crates/aiken-lang/src/expr.rs b/crates/aiken-lang/src/expr.rs index dccc9aaa..976c47df 100644 --- a/crates/aiken-lang/src/expr.rs +++ b/crates/aiken-lang/src/expr.rs @@ -1,7 +1,10 @@ -use std::rc::Rc; +use std::{collections::HashMap, rc::Rc}; use vec1::Vec1; +use pallas::ledger::primitives::alonzo::{Constr, PlutusData}; +use uplc::machine::value::from_pallas_bigint; + use crate::{ ast::{ self, Annotation, Arg, AssignmentKind, BinOp, ByteArrayFormatPreference, CallArg, Curve, @@ -11,7 +14,7 @@ use crate::{ }, builtins::void, parser::token::Base, - tipo::{ModuleValueConstructor, PatternConstructor, Type, ValueConstructor}, + tipo::{ModuleValueConstructor, PatternConstructor, Type, TypeInfo, TypeVar, ValueConstructor}, }; #[derive(Debug, Clone, PartialEq)] @@ -573,6 +576,150 @@ pub const DEFAULT_TODO_STR: &str = "aiken::todo"; pub const DEFAULT_ERROR_STR: &str = "aiken::error"; impl UntypedExpr { + // Reify some opaque 'PlutusData' into an 'UntypedExpr', using a Type annotation. We also need + // an extra map to lookup record & enum constructor's names as they're completely erased when + // in their PlutusData form, and the Type annotation only contains type name. + // + // The function performs some sanity check to ensure that the type does indeed somewhat + // correspond to the data being given. + pub fn reify( + data_types: &HashMap, + data: PlutusData, + tipo: &Type, + ) -> Result { + if let Type::Var { tipo } = tipo { + if let TypeVar::Link { tipo } = &*tipo.borrow() { + return UntypedExpr::reify(data_types, data, tipo); + } + } + + match data { + PlutusData::BigInt(ref i) => Ok(UntypedExpr::UInt { + location: Span::empty(), + base: Base::Decimal { + numeric_underscore: false, + }, + value: from_pallas_bigint(i).to_string(), + }), + PlutusData::BoundedBytes(bytes) => Ok(UntypedExpr::ByteArray { + location: Span::empty(), + bytes: bytes.into(), + preferred_format: ByteArrayFormatPreference::HexadecimalString, + }), + PlutusData::Array(args) => { + let inner; + match tipo { + Type::App { + module, name, args, .. + } if module.is_empty() && name.as_str() == "List" => { + if let [arg] = &args[..] { + inner = arg.clone() + } else { + return Err("invalid List type annotation: the list has multiple type-parameters.".to_string()); + }; + } + _ => { + return Err(format!( + "invalid type annotation. expected List but got: {tipo:?}" + )) + } + } + + Ok(UntypedExpr::List { + location: Span::empty(), + elements: args + .into_iter() + .map(|arg| UntypedExpr::reify(data_types, arg, &inner)) + .collect::, _>>()?, + tail: None, + }) + } + + PlutusData::Constr(Constr { + tag, + any_constructor, + fields, + }) => { + let ix = if tag == 102 { + any_constructor.unwrap() as usize + } else if tag < 128 { + tag as usize - 121 + } else { + tag as usize - 1280 + 7 + }; + + if let Type::App { module, name, .. } = tipo { + let module = if module.is_empty() { "aiken" } else { module }; + if let Some(type_info) = data_types.get(module) { + if let Some(constructors) = type_info.types_constructors.get(name) { + let constructor = &constructors[ix]; + return if fields.is_empty() { + Ok(UntypedExpr::Var { + location: Span::empty(), + name: constructor.to_string(), + }) + } else { + // NOTE: When the type is _private_, we cannot see the + // value constructor and so we default to showing a + // placeholder. + let arguments = match type_info.values.get(constructor) { + None => Ok(fields + .iter() + .map(|_| CallArg { + label: None, + location: Span::empty(), + value: UntypedExpr::Var { + name: "".to_string(), + location: Span::empty(), + }, + }) + .collect()), + Some(value) => { + let types = + if let Type::Fn { args, .. } = value.tipo.as_ref() { + &args[..] + } else { + &[] + }; + + fields + .into_iter() + .zip(types) + .map(|(field, tipo)| { + UntypedExpr::reify(data_types, field, tipo).map( + |value| CallArg { + label: None, + location: Span::empty(), + value, + }, + ) + }) + .collect::, _>>() + } + }?; + + Ok(UntypedExpr::Call { + location: Span::empty(), + arguments, + fun: Box::new(UntypedExpr::Var { + name: constructor.to_string(), + location: Span::empty(), + }), + }) + }; + } + } + } + + Err(format!( + "invalid type annotation {tipo:?} for constructor: {tag:?} with {fields:?}" + )) + } + + PlutusData::Map(..) => todo!("reify Map"), + } + } + pub fn todo(reason: Option, location: Span) -> Self { UntypedExpr::Trace { location, diff --git a/crates/aiken-lang/src/gen_uplc.rs b/crates/aiken-lang/src/gen_uplc.rs index 63907f47..59182f88 100644 --- a/crates/aiken-lang/src/gen_uplc.rs +++ b/crates/aiken-lang/src/gen_uplc.rs @@ -19,8 +19,9 @@ use uplc::{ use crate::{ ast::{ - AssignmentKind, BinOp, Bls12_381Point, Curve, Pattern, Span, TraceLevel, TypedArg, - TypedClause, TypedDataType, TypedFunction, TypedPattern, TypedValidator, UnOp, + AssignmentKind, BinOp, Bls12_381Point, Curve, DataTypeKey, FunctionAccessKey, Pattern, + Span, TraceLevel, TypedArg, TypedClause, TypedDataType, TypedFunction, TypedPattern, + TypedValidator, UnOp, }, builtins::{bool, data, int, list, string, void}, expr::TypedExpr, @@ -48,7 +49,7 @@ use self::{ air_holds_msg, cast_validator_args, constants_ir, convert_type_to_data, extract_constant, lookup_data_type_by_tipo, modify_cyclic_calls, modify_self_calls, rearrange_list_clauses, AssignmentProperties, ClauseProperties, CodeGenSpecialFuncs, CycleFunctionNames, - DataTypeKey, FunctionAccessKey, HoistableFunction, Variant, + HoistableFunction, Variant, }, tree::{AirMsg, AirTree, TreePath}, }; diff --git a/crates/aiken-lang/src/gen_uplc/builder.rs b/crates/aiken-lang/src/gen_uplc/builder.rs index 3bf84e00..9a4da382 100644 --- a/crates/aiken-lang/src/gen_uplc/builder.rs +++ b/crates/aiken-lang/src/gen_uplc/builder.rs @@ -15,8 +15,8 @@ use uplc::{ use crate::{ ast::{ - AssignmentKind, DataType, Pattern, Span, TraceLevel, TypedArg, TypedClause, - TypedClauseGuard, TypedDataType, TypedPattern, + AssignmentKind, DataType, DataTypeKey, FunctionAccessKey, Pattern, Span, TraceLevel, + TypedArg, TypedClause, TypedClauseGuard, TypedDataType, TypedPattern, }, builtins::{bool, data, function, int, list, string, void}, expr::TypedExpr, @@ -68,18 +68,6 @@ pub enum HoistableFunction { CyclicLink(FunctionAccessKey), } -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub struct DataTypeKey { - pub module_name: String, - pub defined_type: String, -} - -#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] -pub struct FunctionAccessKey { - pub module_name: String, - pub function_name: String, -} - #[derive(Clone, Debug)] pub struct AssignmentProperties { pub value_type: Rc, diff --git a/crates/aiken-lang/src/parser/definition/snapshots/def_invalid_property_test.snap b/crates/aiken-lang/src/parser/definition/snapshots/def_invalid_property_test.snap index 7cd61a30..66cabfe5 100644 --- a/crates/aiken-lang/src/parser/definition/snapshots/def_invalid_property_test.snap +++ b/crates/aiken-lang/src/parser/definition/snapshots/def_invalid_property_test.snap @@ -13,8 +13,8 @@ Test( is_validator_param: false, }, location: 9..16, - via: DefinitionIdentifier { - module: None, + via: Var { + location: 15..16, name: "f", }, tipo: (), @@ -27,8 +27,8 @@ Test( is_validator_param: false, }, location: 18..25, - via: DefinitionIdentifier { - module: None, + via: Var { + location: 24..25, name: "g", }, tipo: (), @@ -42,7 +42,14 @@ Test( location: 0..26, name: "foo", public: false, - return_annotation: None, + return_annotation: Some( + Constructor { + location: 0..39, + module: None, + name: "Bool", + arguments: [], + }, + ), return_type: (), end_position: 38, can_error: false, diff --git a/crates/aiken-lang/src/parser/definition/snapshots/def_property_test.snap b/crates/aiken-lang/src/parser/definition/snapshots/def_property_test.snap index 8047f21e..b159b1f5 100644 --- a/crates/aiken-lang/src/parser/definition/snapshots/def_property_test.snap +++ b/crates/aiken-lang/src/parser/definition/snapshots/def_property_test.snap @@ -13,11 +13,13 @@ Test( is_validator_param: false, }, location: 9..27, - via: DefinitionIdentifier { - module: Some( - "fuzz", - ), - name: "any_int", + via: FieldAccess { + location: 15..27, + label: "any_int", + container: Var { + location: 15..19, + name: "fuzz", + }, }, tipo: (), }, @@ -30,7 +32,14 @@ Test( location: 0..28, name: "foo", public: false, - return_annotation: None, + return_annotation: Some( + Constructor { + location: 0..41, + module: None, + name: "Bool", + arguments: [], + }, + ), return_type: (), end_position: 40, can_error: false, diff --git a/crates/aiken-lang/src/parser/definition/snapshots/def_test.snap b/crates/aiken-lang/src/parser/definition/snapshots/def_test.snap index de24dc47..3ddb5121 100644 --- a/crates/aiken-lang/src/parser/definition/snapshots/def_test.snap +++ b/crates/aiken-lang/src/parser/definition/snapshots/def_test.snap @@ -13,7 +13,14 @@ Test( location: 0..10, name: "foo", public: false, - return_annotation: None, + return_annotation: Some( + Constructor { + location: 0..23, + module: None, + name: "Bool", + arguments: [], + }, + ), return_type: (), end_position: 22, can_error: false, diff --git a/crates/aiken-lang/src/parser/definition/snapshots/def_test_fail.snap b/crates/aiken-lang/src/parser/definition/snapshots/def_test_fail.snap index a6e3e07b..a2da8f18 100644 --- a/crates/aiken-lang/src/parser/definition/snapshots/def_test_fail.snap +++ b/crates/aiken-lang/src/parser/definition/snapshots/def_test_fail.snap @@ -37,7 +37,14 @@ Test( location: 0..26, name: "invalid_inputs", public: false, - return_annotation: None, + return_annotation: Some( + Constructor { + location: 0..61, + module: None, + name: "Bool", + arguments: [], + }, + ), return_type: (), end_position: 60, can_error: true, diff --git a/crates/aiken-lang/src/parser/expr/record.rs b/crates/aiken-lang/src/parser/expr/record.rs index 5cc4cc25..fe12ad74 100644 --- a/crates/aiken-lang/src/parser/expr/record.rs +++ b/crates/aiken-lang/src/parser/expr/record.rs @@ -187,6 +187,11 @@ pub fn parser( mod tests { use crate::assert_expr; + #[test] + fn record_enum() { + assert_expr!(r#"Winter"#); + } + #[test] fn record_create_labeled() { assert_expr!(r#"User { name: "Aiken", age, thing: 2 }"#); diff --git a/crates/aiken-lang/src/parser/expr/snapshots/record_enum.snap b/crates/aiken-lang/src/parser/expr/snapshots/record_enum.snap new file mode 100644 index 00000000..8885f178 --- /dev/null +++ b/crates/aiken-lang/src/parser/expr/snapshots/record_enum.snap @@ -0,0 +1,8 @@ +--- +source: crates/aiken-lang/src/parser/expr/record.rs +description: "Code:\n\nWinter" +--- +Var { + location: 0..6, + name: "Winter", +} diff --git a/crates/aiken-lang/src/tipo/infer.rs b/crates/aiken-lang/src/tipo/infer.rs index aaf20645..10027676 100644 --- a/crates/aiken-lang/src/tipo/infer.rs +++ b/crates/aiken-lang/src/tipo/infer.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, ops::Deref, rc::Rc}; +use std::{borrow::Borrow, collections::HashMap, ops::Deref, rc::Rc}; use crate::{ ast::{ @@ -332,78 +332,6 @@ fn infer_definition( } Definition::Test(f) => { - fn annotate_fuzzer(tipo: &Type, location: &Span) -> Result { - match tipo { - // TODO: Ensure args & first returned element is a Prelude's PRNG. - Type::Fn { ret, .. } => { - let ann = tipo_to_annotation(ret, location)?; - match ann { - Annotation::Constructor { - module, - name, - arguments, - .. - } if module.as_ref().unwrap_or(&String::new()).is_empty() - && name == "Option" => - { - match &arguments[..] { - [Annotation::Tuple { elems, .. }] if elems.len() == 2 => { - Ok(elems.get(1).expect("Tuple has two elements").to_owned()) - } - _ => { - todo!("expected a single generic argument unifying as 2-tuple") - } - } - } - _ => todo!("expected an Option"), - } - } - Type::Var { .. } | Type::App { .. } | Type::Tuple { .. } => { - todo!("Fuzzer type isn't a function?"); - } - } - } - - fn tipo_to_annotation(tipo: &Type, location: &Span) -> Result { - match tipo { - Type::App { - name, module, args, .. - } => { - let arguments = args - .iter() - .map(|arg| tipo_to_annotation(arg, location)) - .collect::, _>>()?; - Ok(Annotation::Constructor { - name: name.to_owned(), - module: Some(module.to_owned()), - arguments, - location: *location, - }) - } - Type::Tuple { elems } => { - let elems = elems - .iter() - .map(|arg| tipo_to_annotation(arg, location)) - .collect::, _>>()?; - Ok(Annotation::Tuple { - elems, - location: *location, - }) - } - Type::Var { tipo } => match tipo.borrow().deref() { - 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:#?}" - ); - } - } - } - let (typed_via, annotation) = match f.arguments.first() { Some(arg) => { if f.arguments.len() > 1 { @@ -416,12 +344,9 @@ fn infer_definition( let typed_via = ExprTyper::new(environment, lines, tracing).infer(arg.via.clone())?; - let tipo = typed_via.tipo(); + let (annotation, inner_type) = infer_fuzzer(&typed_via.tipo(), &arg.location)?; - Ok(( - Some(typed_via), - Some(annotate_fuzzer(&tipo, &arg.location)?), - )) + Ok((Some((typed_via, inner_type)), Some(annotation))) } None => Ok((None, None)), }?; @@ -466,17 +391,15 @@ fn infer_definition( name: typed_f.name, public: typed_f.public, arguments: match typed_via { - Some(via) => { + Some((via, tipo)) => { let Arg { - arg_name, - location, - tipo, - .. + arg_name, location, .. } = typed_f .arguments .first() .expect("has exactly one argument") .to_owned(); + vec![ArgVia { arg_name, location, @@ -805,3 +728,67 @@ fn infer_function( end_position, }) } + +fn infer_fuzzer(tipo: &Type, location: &Span) -> Result<(Annotation, Rc), Error> { + match tipo { + // TODO: Ensure args & first returned element is a Prelude's PRNG. + Type::Fn { ret, .. } => match &ret.borrow() { + Type::App { + module, name, args, .. + } if module.is_empty() && name == "Option" && args.len() == 1 => { + match &args.first().map(|x| x.borrow()) { + Some(Type::Tuple { elems }) if elems.len() == 2 => { + let wrapped = elems.get(1).expect("Tuple has two elements"); + Ok((annotation_from_type(wrapped, location)?, wrapped.clone())) + } + _ => { + todo!("expected a single generic argument unifying as 2-tuple") + } + } + } + _ => todo!("expected an Option"), + }, + + Type::Var { .. } | Type::App { .. } | Type::Tuple { .. } => { + todo!("Fuzzer type isn't a function?"); + } + } +} + +fn annotation_from_type(tipo: &Type, location: &Span) -> Result { + match tipo { + Type::App { + name, module, args, .. + } => { + let arguments = args + .iter() + .map(|arg| annotation_from_type(arg, location)) + .collect::, _>>()?; + Ok(Annotation::Constructor { + name: name.to_owned(), + module: Some(module.to_owned()), + arguments, + location: *location, + }) + } + + Type::Tuple { elems } => { + let elems = elems + .iter() + .map(|arg| annotation_from_type(arg, location)) + .collect::, _>>()?; + Ok(Annotation::Tuple { + elems, + location: *location, + }) + } + + Type::Var { tipo } => match &*tipo.deref().borrow() { + TypeVar::Link { tipo } => annotation_from_type(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:#?}"); + } + } +} diff --git a/crates/aiken-project/src/lib.rs b/crates/aiken-project/src/lib.rs index 145fcc11..349ff7d6 100644 --- a/crates/aiken-project/src/lib.rs +++ b/crates/aiken-project/src/lib.rs @@ -29,11 +29,11 @@ use crate::{ }; use aiken_lang::{ ast::{ - Definition, Function, ModuleKind, Span, Tracing, TypedDataType, TypedFunction, Validator, + DataTypeKey, Definition, Function, FunctionAccessKey, ModuleKind, Span, Tracing, + TypedDataType, TypedFunction, Validator, }, builtins, - expr::TypedExpr, - gen_uplc::builder::{DataTypeKey, FunctionAccessKey}, + expr::{TypedExpr, UntypedExpr}, tipo::{Type, TypeInfo}, IdGenerator, }; @@ -321,9 +321,9 @@ where self.event_listener.handle_event(Event::RunningTests); } - let results = self.run_tests(tests); + let tests = self.run_tests(tests); - let errors: Vec = results + let errors: Vec = tests .iter() .filter_map(|e| { if e.is_success() { @@ -335,7 +335,7 @@ where .collect(); self.event_listener - .handle_event(Event::FinishedTests { tests: results }); + .handle_event(Event::FinishedTests { tests }); if !errors.is_empty() { Err(errors) @@ -886,7 +886,7 @@ where Ok(programs) } - fn run_tests(&self, tests: Vec) -> Vec { + fn run_tests(&self, tests: Vec) -> Vec> { use rayon::prelude::*; tests @@ -897,6 +897,9 @@ where // provided. Test::PropertyTest(property_test) => property_test.run(42), }) + .collect::>>() + .into_iter() + .map(|test| test.reify(&self.module_types)) .collect() } diff --git a/crates/aiken-project/src/module.rs b/crates/aiken-project/src/module.rs index f6157d66..19b79fe2 100644 --- a/crates/aiken-project/src/module.rs +++ b/crates/aiken-project/src/module.rs @@ -1,13 +1,11 @@ use crate::error::Error; use aiken_lang::{ ast::{ - DataType, Definition, Function, Located, ModuleKind, Tracing, TypedDataType, TypedFunction, - TypedModule, TypedValidator, UntypedModule, Validator, - }, - gen_uplc::{ - builder::{DataTypeKey, FunctionAccessKey}, - CodeGenerator, + DataType, DataTypeKey, Definition, Function, FunctionAccessKey, Located, ModuleKind, + Tracing, TypedDataType, TypedFunction, TypedModule, TypedValidator, UntypedModule, + Validator, }, + gen_uplc::CodeGenerator, line_numbers::LineNumbers, parser::extra::{comments_before, Comment, ModuleExtra}, tipo::TypeInfo, diff --git a/crates/aiken-project/src/script.rs b/crates/aiken-project/src/script.rs index 386ab1b1..256929a3 100644 --- a/crates/aiken-project/src/script.rs +++ b/crates/aiken-project/src/script.rs @@ -1,10 +1,17 @@ use crate::{pretty, ExBudget}; -use aiken_lang::gen_uplc::builder::convert_data_to_type; -use aiken_lang::{ast::BinOp, tipo::Type}; -use pallas::codec::utils::Int; -use pallas::ledger::primitives::alonzo::{BigInt, Constr, PlutusData}; +use aiken_lang::{ + ast::BinOp, + expr::UntypedExpr, + gen_uplc::builder::convert_data_to_type, + tipo::{Type, TypeInfo}, +}; +use pallas::{ + codec::utils::Int, + ledger::primitives::alonzo::{BigInt, Constr, PlutusData}, +}; use std::{ borrow::Borrow, + collections::HashMap, fmt::{self, Display}, path::PathBuf, rc::Rc, @@ -14,26 +21,25 @@ use uplc::{ machine::eval_result::EvalResult, }; -// ---------------------------------------------------------------------------- -// -// Test -// -// Aiken supports two kinds of tests: unit and property. A unit test is a simply -// UPLC program which returns must be a lambda that returns a boolean. -// -// A property on the other-hand is a template for generating tests, which is also -// a lambda but that takes an extra argument. The argument is generated from a -// fuzzer which is meant to yield random values in a pseudo-random (albeit seeded) -// sequence. On failures, the value that caused a failure is simplified using an -// approach similar to what's described in MiniThesis, -// which is a simplified version of Hypothesis, a property-based testing framework -// with integrated shrinking. -// -// Our approach could perhaps be called "microthesis", as it implements a subset of -// minithesis. More specifically, we do not currently support pre-conditions, nor -// targets. -// ---------------------------------------------------------------------------- - +/// ---------------------------------------------------------------------------- +/// +/// Test +/// +/// Aiken supports two kinds of tests: unit and property. A unit test is a simply +/// UPLC program which returns must be a lambda that returns a boolean. +/// +/// A property on the other-hand is a template for generating tests, which is also +/// a lambda but that takes an extra argument. The argument is generated from a +/// fuzzer which is meant to yield random values in a pseudo-random (albeit seeded) +/// sequence. On failures, the value that caused a failure is simplified using an +/// approach similar to what's described in MiniThesis, +/// which is a simplified version of Hypothesis, a property-based testing framework +/// with integrated shrinking. +/// +/// Our approach could perhaps be called "microthesis", as it implements a subset of +/// minithesis. More specifically, we do not currently support pre-conditions, nor +/// targets. +/// ---------------------------------------------------------------------------- #[derive(Debug, Clone)] pub enum Test { UnitTest(UnitTest), @@ -99,7 +105,7 @@ pub struct UnitTest { unsafe impl Send for UnitTest {} impl UnitTest { - pub fn run(self) -> TestResult { + pub fn run(self) -> TestResult { let mut eval_result = self.program.clone().eval(ExBudget::max()); TestResult::UnitTestResult(UnitTestResult { test: self.to_owned(), @@ -134,7 +140,7 @@ impl PropertyTest { /// Run a property test from a given seed. The property is run at most MAX_TEST_RUN times. It /// may stops earlier on failure; in which case a 'counterexample' is returned. - pub fn run(self, seed: u32) -> TestResult { + pub fn run(self, seed: u32) -> TestResult { let n = PropertyTest::MAX_TEST_RUN; let (counterexample, iterations) = match self.run_n_times(n, seed, None) { @@ -153,8 +159,8 @@ impl PropertyTest { &self, remaining: usize, seed: u32, - counterexample: Option<(usize, Term)>, - ) -> Option<(usize, Term)> { + counterexample: Option<(usize, PlutusData)>, + ) -> Option<(usize, PlutusData)> { // We short-circuit failures in case we have any. The counterexample is already simplified // at this point. if remaining > 0 && counterexample.is_none() { @@ -169,12 +175,12 @@ impl PropertyTest { } } - fn run_once(&self, seed: u32) -> (u32, Option>) { + fn run_once(&self, seed: u32) -> (u32, Option) { let (next_prng, value) = Prng::from_seed(seed) - .sample(&self.fuzzer.0, &self.fuzzer.1) + .sample(&self.fuzzer.0) .expect("running seeded Prng cannot fail."); - let result = self.program.apply_term(&value).eval(ExBudget::max()); + let result = self.eval(&value); if let Prng::Seeded { seed: next_seed, .. @@ -182,12 +188,9 @@ impl PropertyTest { { if result.failed(self.can_error) { let mut counterexample = Counterexample { - result, value, choices: next_prng.choices(), - can_error: self.can_error, - program: &self.program, - fuzzer: (&self.fuzzer.0, &self.fuzzer.1), + property: self, }; if !counterexample.choices.is_empty() { @@ -202,6 +205,13 @@ impl PropertyTest { unreachable!("Prng constructed from a seed necessarily yield a seed."); } } + + pub fn eval(&self, value: &PlutusData) -> EvalResult { + let term = convert_data_to_type(Term::data(value.clone()), &self.fuzzer.1) + .try_into() + .expect("safe conversion from Name -> NamedDeBruijn"); + self.program.apply_term(&term).eval(ExBudget::max()) + } } // ---------------------------------------------------------------------------- @@ -230,9 +240,9 @@ impl Prng { const REPLAYED: u64 = 1; /// Constructor tag for Option's 'Some' - const OK: u64 = 0; + const SOME: u64 = 0; /// Constructor tag for Option's 'None' - const ERR: u64 = 1; + const NONE: u64 = 1; pub fn uplc(&self) -> PlutusData { match self { @@ -281,18 +291,14 @@ impl Prng { } /// Generate a pseudo-random value from a fuzzer using the given PRNG. - pub fn sample( - &self, - fuzzer: &Program, - return_type: &Type, - ) -> Option<(Prng, Term)> { + pub fn sample(&self, fuzzer: &Program) -> Option<(Prng, PlutusData)> { let result = fuzzer .apply_data(self.uplc()) .eval(ExBudget::max()) .result() .expect("Fuzzer crashed?"); - Prng::from_result(result, return_type) + Prng::from_result(result) } /// Obtain a Prng back from a fuzzer execution. As a reminder, fuzzers have the following @@ -305,10 +311,7 @@ impl Prng { /// 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 /// aborted altogether with 'None'. - pub fn from_result( - result: Term, - type_info: &Type, - ) -> Option<(Self, Term)> { + pub fn from_result(result: Term) -> Option<(Self, PlutusData)> { /// Interpret the given 'PlutusData' as one of two Prng constructors. fn as_prng(cst: &PlutusData) -> Prng { if let PlutusData::Constr(Constr { tag, fields, .. }) = cst { @@ -345,15 +348,10 @@ impl Prng { if let Term::Constant(rc) = &result { if let Constant::Data(PlutusData::Constr(Constr { tag, fields, .. })) = &rc.borrow() { - if *tag == 121 + Prng::OK { + if *tag == 121 + Prng::SOME { if let [PlutusData::Array(elems)] = &fields[..] { if let [new_seed, value] = &elems[..] { - return Some(( - as_prng(new_seed), - convert_data_to_type(Term::data(value.clone()), type_info) - .try_into() - .expect("safe conversion from Name -> NamedDeBruijn"), - )); + return Some((as_prng(new_seed), value.clone())); } } } @@ -362,7 +360,7 @@ impl Prng { // choices. If we run out of choices, or a choice end up being // invalid as per the expectation, the fuzzer can't go further and // fail. - if *tag == 121 + Prng::ERR { + if *tag == 121 + Prng::NONE { return None; } } @@ -385,12 +383,9 @@ impl Prng { #[derive(Debug)] pub struct Counterexample<'a> { - pub value: Term, + pub value: PlutusData, pub choices: Vec, - pub result: EvalResult, - pub can_error: bool, - pub program: &'a Program, - pub fuzzer: (&'a Program, &'a Type), + pub property: &'a PropertyTest, } impl<'a> Counterexample<'a> { @@ -404,17 +399,17 @@ impl<'a> Counterexample<'a> { // test cases many times. Given that tests are fully deterministic, we can // memoize the already seen choices to avoid re-running the generators and // the test (which can be quite expensive). - match Prng::from_choices(choices).sample(self.fuzzer.0, self.fuzzer.1) { + match Prng::from_choices(choices).sample(&self.property.fuzzer.0) { // Shrinked choices led to an impossible generation. None => false, // Shrinked choices let to a new valid generated value, now, is it better? Some((_, value)) => { - let result = self.program.apply_term(&value).eval(ExBudget::max()); + let result = self.property.eval(&value); // If the test no longer fails, it isn't better as we're only // interested in counterexamples. - if !result.failed(self.can_error) { + if !result.failed(self.property.can_error) { return false; } @@ -546,14 +541,25 @@ impl<'a> Counterexample<'a> { // ---------------------------------------------------------------------------- #[derive(Debug)] -pub enum TestResult { +pub enum TestResult { UnitTestResult(UnitTestResult), - PropertyTestResult(PropertyTestResult), + PropertyTestResult(PropertyTestResult), } -unsafe impl Send for TestResult {} +unsafe impl Send for TestResult {} -impl TestResult { +impl TestResult { + pub fn reify(self, data_types: &HashMap) -> TestResult { + match self { + TestResult::UnitTestResult(test) => TestResult::UnitTestResult(test), + TestResult::PropertyTestResult(test) => { + TestResult::PropertyTestResult(test.reify(data_types)) + } + } + } +} + +impl TestResult { pub fn is_success(&self) -> bool { match self { TestResult::UnitTestResult(UnitTestResult { success, .. }) => *success, @@ -633,13 +639,29 @@ pub struct UnitTestResult { unsafe impl Send for UnitTestResult {} #[derive(Debug)] -pub struct PropertyTestResult { +pub struct PropertyTestResult { pub test: PropertyTest, - pub counterexample: Option>, + pub counterexample: Option, pub iterations: usize, } -unsafe impl Send for PropertyTestResult {} +unsafe impl Send for PropertyTestResult {} + +impl PropertyTestResult { + pub fn reify(self, data_types: &HashMap) -> PropertyTestResult { + PropertyTestResult { + counterexample: match self.counterexample { + None => None, + Some(counterexample) => Some( + UntypedExpr::reify(data_types, counterexample, &self.test.fuzzer.1) + .expect("Failed to reify counterexample?"), + ), + }, + iterations: self.iterations, + test: self.test, + } + } +} #[derive(Debug, Clone)] pub struct Assertion { diff --git a/crates/aiken-project/src/telemetry.rs b/crates/aiken-project/src/telemetry.rs index 812e9b94..e17bd7d4 100644 --- a/crates/aiken-project/src/telemetry.rs +++ b/crates/aiken-project/src/telemetry.rs @@ -1,5 +1,6 @@ use crate::pretty; use crate::script::{PropertyTestResult, TestResult, UnitTestResult}; +use aiken_lang::{expr::UntypedExpr, format::Formatter}; use owo_colors::{OwoColorize, Stream::Stderr}; use std::{collections::BTreeMap, fmt::Display, path::PathBuf}; use uplc::machine::cost_model::ExBudget; @@ -34,7 +35,7 @@ pub enum Event { }, RunningTests, FinishedTests { - tests: Vec, + tests: Vec>, }, WaitingForBuildDirLock, ResolvingPackages { @@ -249,7 +250,12 @@ impl EventListener for Terminal { } } -fn fmt_test(result: &TestResult, max_mem: usize, max_cpu: usize, styled: bool) -> String { +fn fmt_test( + result: &TestResult, + max_mem: usize, + max_cpu: usize, + styled: bool, +) -> String { // Status let mut test = if result.is_success() { pretty::style_if(styled, "PASS".to_string(), |s| { @@ -316,7 +322,9 @@ fn fmt_test(result: &TestResult, max_mem: usize, max_cpu: usize, styled: bool) - .if_supports_color(Stderr, |s| s.red()) .if_supports_color(Stderr, |s| s.bold()) .to_string()), - &counterexample.to_pretty(), + &Formatter::new() + .expr(counterexample, false) + .to_pretty_string(70), |s| s.red().to_string() ) ) @@ -351,7 +359,7 @@ fn fmt_test(result: &TestResult, max_mem: usize, max_cpu: usize, styled: bool) - test } -fn fmt_test_summary(tests: &[&TestResult], styled: bool) -> String { +fn fmt_test_summary(tests: &[&TestResult], styled: bool) -> String { let (n_passed, n_failed) = tests.iter().fold((0, 0), |(n_passed, n_failed), result| { if result.is_success() { (n_passed + 1, n_failed) @@ -375,16 +383,16 @@ fn fmt_test_summary(tests: &[&TestResult], styled: bool) -> String { ) } -fn group_by_module(results: &Vec) -> BTreeMap> { +fn group_by_module(results: &Vec>) -> BTreeMap>> { let mut modules = BTreeMap::new(); for r in results { - let xs: &mut Vec<&TestResult> = modules.entry(r.module().to_string()).or_default(); + let xs: &mut Vec<&TestResult<_>> = modules.entry(r.module().to_string()).or_default(); xs.push(r); } modules } -fn find_max_execution_units(xs: &[TestResult]) -> (usize, usize) { +fn find_max_execution_units(xs: &[TestResult]) -> (usize, usize) { let (max_mem, max_cpu) = xs .iter() .fold((0, 0), |(max_mem, max_cpu), test| match test { diff --git a/crates/aiken-project/src/tests/mod.rs b/crates/aiken-project/src/tests/mod.rs index bd5eb1e7..e319a908 100644 --- a/crates/aiken-project/src/tests/mod.rs +++ b/crates/aiken-project/src/tests/mod.rs @@ -2,8 +2,10 @@ use std::collections::HashMap; use std::path::PathBuf; use aiken_lang::{ - ast::{ModuleKind, TraceLevel, Tracing, TypedDataType, TypedFunction}, - gen_uplc::builder::{DataTypeKey, FunctionAccessKey}, + ast::{ + DataTypeKey, FunctionAccessKey, ModuleKind, TraceLevel, Tracing, TypedDataType, + TypedFunction, + }, parser, tipo::TypeInfo, IdGenerator, diff --git a/crates/uplc/src/machine/value.rs b/crates/uplc/src/machine/value.rs index 4b8a3228..8de10a4f 100644 --- a/crates/uplc/src/machine/value.rs +++ b/crates/uplc/src/machine/value.rs @@ -1,13 +1,12 @@ use std::{collections::VecDeque, mem::size_of, ops::Deref, rc::Rc}; -use num_bigint::BigInt; -use num_traits::{Signed, ToPrimitive, Zero}; -use pallas::ledger::primitives::babbage::{self, PlutusData}; - use crate::{ ast::{Constant, NamedDeBruijn, Term, Type}, builtins::DefaultFunction, }; +use num_bigint::BigInt; +use num_traits::{Signed, ToPrimitive, Zero}; +use pallas::ledger::primitives::babbage::{self, PlutusData}; use super::{runtime::BuiltinRuntime, Error}; diff --git a/examples/acceptance_tests/095/lib/foo.ak b/examples/acceptance_tests/095/lib/foo.ak index 775f6ec2..0225a2d0 100644 --- a/examples/acceptance_tests/095/lib/foo.ak +++ b/examples/acceptance_tests/095/lib/foo.ak @@ -2,11 +2,16 @@ use aiken/builtin const max_int: Int = 255 -type PRNG { +pub type PRNG { Seeded { seed: Int, choices: List } Replayed { choices: List } } +type Fuzzer = + fn(PRNG) -> Option<(PRNG, a)> + +// Primitives + fn any_int(prng: PRNG) -> Option<(PRNG, Int)> { when prng is { Seeded { seed, choices } -> { @@ -40,10 +45,94 @@ fn any_int(prng: PRNG) -> Option<(PRNG, Int)> { } } -test prop_foo_1(n via any_int) { - n >= 0 && n <= 255 +pub fn constant(a: a) -> Fuzzer { + fn(s0) { Some((s0, a)) } } -test prop_foo_2(n via any_int) fail { - n < 100 +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) + None -> None + } + } +} + +pub fn map(fuzz_a: Fuzzer, f: fn(a) -> b) -> Fuzzer { + fn(s0) { + when fuzz_a(s0) is { + Some((s1, a)) -> Some((s1, f(a))) + None -> None + } + } +} + +pub fn map2(fuzz_a: Fuzzer, fuzz_b: Fuzzer, f: fn(a, b) -> c) -> Fuzzer { + fn(s0) { + when fuzz_a(s0) is { + Some((s1, a)) -> + when fuzz_b(s1) is { + Some((s2, b)) -> Some((s2, f(a, b))) + None -> None + } + None -> None + } + } +} + +// Builders + +fn any_bool() -> Fuzzer { + any_int |> map(fn(n) { n <= 127 }) +} + +fn any_list(fuzz_a: Fuzzer) -> Fuzzer> { + any_bool() + |> and_then( + fn(continue) { + if continue { + map2(fuzz_a, any_list(fuzz_a), fn(head, tail) { [head, ..tail] }) + } else { + constant([]) + } + }, + ) +} + +fn any_season() -> Fuzzer { + any_int + |> map( + fn(i) { + let n = i % 3 + if n == 0 { + Winter { cold: True } + } else if n == 1 { + Spring(i) + } else if n == 2 { + Summer + } else { + Fall + } + }, + ) +} + +// Properties + +pub type Season { + Winter { cold: Bool } + Spring(Int) + Summer + Fall +} + +test prop_list(xs via any_list(any_season())) { + xs != [Winter(True)] || xs != [Winter(False)] +} + +fn length(xs: List) -> Int { + when xs is { + [] -> 0 + [_, ..tail] -> 1 + length(tail) + } }