Hardened property-based testing framework. More tests, less bugs.

Those end-to-end tests are useful. Both for controlling the behavior of the shrinker, but also to double check the reification of Plutus Data back into untyped expressions.
  I had to work-around a few things to get opaque type and private types play nice. Also found a weird bug due to how we apply parameters after unique debruijn indexes have been also applied. A work-around is to re-intern the program.
This commit is contained in:
KtorZ 2024-03-03 03:16:36 +01:00
parent 775a34bc47
commit 26e563a9be
No known key found for this signature in database
GPG Key ID: 33173CB6F77F4277
9 changed files with 589 additions and 199 deletions

View File

@ -258,6 +258,34 @@ pub struct FunctionAccessKey {
pub type TypedDataType = DataType<Rc<Type>>; pub type TypedDataType = DataType<Rc<Type>>;
impl TypedDataType { impl TypedDataType {
pub fn bool() -> Self {
DataType {
constructors: vec![
RecordConstructor {
location: Span::empty(),
name: "False".to_string(),
arguments: vec![],
doc: None,
sugar: false,
},
RecordConstructor {
location: Span::empty(),
name: "True".to_string(),
arguments: vec![],
doc: None,
sugar: false,
},
],
doc: None,
location: Span::empty(),
name: "Bool".to_string(),
opaque: false,
parameters: vec![],
public: true,
typed_parameters: vec![],
}
}
pub fn prng() -> Self { pub fn prng() -> Self {
DataType { DataType {
constructors: vec![ constructors: vec![

View File

@ -1220,6 +1220,16 @@ pub fn prelude_data_types(id_gen: &IdGenerator) -> IndexMap<DataTypeKey, TypedDa
ordering_data_type, ordering_data_type,
); );
// Bool
let bool_data_type = TypedDataType::bool();
data_types.insert(
DataTypeKey {
module_name: "".to_string(),
defined_type: "Bool".to_string(),
},
bool_data_type,
);
// Option // Option
let option_data_type = TypedDataType::option(generic_var(id_gen.next())); let option_data_type = TypedDataType::option(generic_var(id_gen.next()));
data_types.insert( data_types.insert(

View File

@ -1,22 +1,23 @@
use std::{collections::HashMap, rc::Rc};
use uplc::KeyValuePairs;
use vec1::Vec1;
use pallas::ledger::primitives::alonzo::{Constr, PlutusData};
use uplc::machine::value::from_pallas_bigint;
use crate::{ use crate::{
ast::{ ast::{
self, Annotation, Arg, AssignmentKind, BinOp, ByteArrayFormatPreference, CallArg, Curve, self, Annotation, Arg, AssignmentKind, BinOp, ByteArrayFormatPreference, CallArg, Curve,
DefinitionLocation, IfBranch, Located, LogicalOpChainKind, ParsedCallArg, Pattern, DataType, DataTypeKey, DefinitionLocation, IfBranch, Located, LogicalOpChainKind,
RecordUpdateSpread, Span, TraceKind, TypedClause, TypedRecordUpdateArg, UnOp, ParsedCallArg, Pattern, RecordConstructorArg, RecordUpdateSpread, Span, TraceKind,
UntypedClause, UntypedRecordUpdateArg, TypedClause, TypedDataType, TypedRecordUpdateArg, UnOp, UntypedClause,
UntypedRecordUpdateArg,
}, },
builtins::void, builtins::void,
gen_uplc::builder::{
check_replaceable_opaque_type, convert_opaque_type, lookup_data_type_by_tipo,
},
parser::token::Base, parser::token::Base,
tipo::{ModuleValueConstructor, PatternConstructor, Type, TypeInfo, TypeVar, ValueConstructor}, tipo::{ModuleValueConstructor, PatternConstructor, Type, TypeVar, ValueConstructor},
}; };
use indexmap::IndexMap;
use pallas::ledger::primitives::alonzo::{Constr, PlutusData};
use std::rc::Rc;
use uplc::{machine::value::from_pallas_bigint, KeyValuePairs};
use vec1::Vec1;
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum TypedExpr { pub enum TypedExpr {
@ -584,7 +585,7 @@ impl UntypedExpr {
// The function performs some sanity check to ensure that the type does indeed somewhat // The function performs some sanity check to ensure that the type does indeed somewhat
// correspond to the data being given. // correspond to the data being given.
pub fn reify( pub fn reify(
data_types: &HashMap<String, TypeInfo>, data_types: &IndexMap<DataTypeKey, &TypedDataType>,
data: PlutusData, data: PlutusData,
tipo: &Type, tipo: &Type,
) -> Result<Self, String> { ) -> Result<Self, String> {
@ -594,6 +595,36 @@ impl UntypedExpr {
} }
} }
// NOTE: Opaque types are tricky. We can't tell from a type only if it is
// opaque or not. We have to lookup its datatype definition.
//
// Also, we can't -- in theory -- peak into an opaque type. More so, if it
// has a single constructor with a single argument, it is an zero-cost
// wrapper. That means the underlying PlutusData has no residue of that
// wrapper. So we have to manually reconstruct it before crawling further
// down the type tree.
if check_replaceable_opaque_type(tipo, data_types) {
let DataType { name, .. } = lookup_data_type_by_tipo(data_types, tipo)
.expect("Type just disappeared from known types? {tipo:?}");
let inner_type = convert_opaque_type(&tipo.clone().into(), data_types, false);
let value = UntypedExpr::reify(data_types, data, &inner_type)?;
return Ok(UntypedExpr::Call {
location: Span::empty(),
arguments: vec![CallArg {
label: None,
location: Span::empty(),
value,
}],
fun: Box::new(UntypedExpr::Var {
name,
location: Span::empty(),
}),
});
}
match data { match data {
PlutusData::BigInt(ref i) => Ok(UntypedExpr::UInt { PlutusData::BigInt(ref i) => Ok(UntypedExpr::UInt {
location: Span::empty(), location: Span::empty(),
@ -655,68 +686,52 @@ impl UntypedExpr {
tag as usize - 1280 + 7 tag as usize - 1280 + 7
}; };
if let Type::App { module, name, .. } = tipo { if let Type::App { .. } = tipo {
let module = if module.is_empty() { "aiken" } else { module }; if let Some(DataType { constructors, .. }) =
if let Some(type_info) = data_types.get(module) { lookup_data_type_by_tipo(data_types, tipo)
if let Some(constructors) = type_info.types_constructors.get(name) { {
let constructor = &constructors[ix]; let constructor = &constructors[ix];
return if fields.is_empty() { return if fields.is_empty() {
Ok(UntypedExpr::Var { Ok(UntypedExpr::Var {
location: Span::empty(), location: Span::empty(),
name: constructor.to_string(), name: constructor.name.to_string(),
}) })
} else { } else {
// NOTE: When the type is _private_, we cannot see the let arguments = fields
// 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: "<hidden>".to_string(),
location: Span::empty(),
},
})
.collect()),
Some(value) => {
let types =
if let Type::Fn { args, .. } = value.tipo.as_ref() {
&args[..]
} else {
&[]
};
fields
.into_iter() .into_iter()
.zip(types) .zip(constructor.arguments.iter())
.map(|(field, tipo)| { .map(
UntypedExpr::reify(data_types, field, tipo).map( |(
|value| CallArg { field,
label: None, RecordConstructorArg {
ref label,
ref tipo,
..
},
)| {
UntypedExpr::reify(data_types, field, tipo).map(|value| {
CallArg {
label: label.clone(),
location: Span::empty(), location: Span::empty(),
value, value,
}
})
}, },
) )
}) .collect::<Result<Vec<_>, _>>()?;
.collect::<Result<Vec<_>, _>>()
}
}?;
Ok(UntypedExpr::Call { Ok(UntypedExpr::Call {
location: Span::empty(), location: Span::empty(),
arguments, arguments,
fun: Box::new(UntypedExpr::Var { fun: Box::new(UntypedExpr::Var {
name: constructor.to_string(), name: constructor.name.to_string(),
location: Span::empty(), location: Span::empty(),
}), }),
}) })
}; };
} }
} }
}
Err(format!( Err(format!(
"invalid type annotation {tipo:?} for constructor: {tag:?} with {fields:?}" "invalid type annotation {tipo:?} for constructor: {tag:?} with {fields:?}"

View File

@ -53,7 +53,7 @@ use uplc::{
#[derive(Clone)] #[derive(Clone)]
pub struct CodeGenerator<'a> { pub struct CodeGenerator<'a> {
/// immutable index maps /// immutable index maps
pub functions: IndexMap<FunctionAccessKey, &'a TypedFunction>, functions: IndexMap<FunctionAccessKey, &'a TypedFunction>,
data_types: IndexMap<DataTypeKey, &'a TypedDataType>, data_types: IndexMap<DataTypeKey, &'a TypedDataType>,
module_types: IndexMap<&'a String, &'a TypeInfo>, module_types: IndexMap<&'a String, &'a TypeInfo>,
module_src: IndexMap<String, (String, LineNumbers)>, module_src: IndexMap<String, (String, LineNumbers)>,
@ -255,7 +255,7 @@ impl<'a> CodeGenerator<'a> {
panic!("Dangling expressions without an assignment") panic!("Dangling expressions without an assignment")
}; };
let replaced_type = convert_opaque_type(tipo, &self.data_types); let replaced_type = convert_opaque_type(tipo, &self.data_types, true);
let air_value = self.build(value, module_build_name, &[]); let air_value = self.build(value, module_build_name, &[]);
@ -895,7 +895,7 @@ impl<'a> CodeGenerator<'a> {
if props.full_check { if props.full_check {
let mut index_map = IndexMap::new(); let mut index_map = IndexMap::new();
let non_opaque_tipo = convert_opaque_type(tipo, &self.data_types); let non_opaque_tipo = convert_opaque_type(tipo, &self.data_types, true);
let val = AirTree::local_var(name, tipo.clone()); let val = AirTree::local_var(name, tipo.clone());
@ -933,7 +933,7 @@ impl<'a> CodeGenerator<'a> {
let name = &format!("__discard_expect_{}", name); let name = &format!("__discard_expect_{}", name);
let mut index_map = IndexMap::new(); let mut index_map = IndexMap::new();
let non_opaque_tipo = convert_opaque_type(tipo, &self.data_types); let non_opaque_tipo = convert_opaque_type(tipo, &self.data_types, true);
let val = AirTree::local_var(name, tipo.clone()); let val = AirTree::local_var(name, tipo.clone());
@ -1326,7 +1326,7 @@ impl<'a> CodeGenerator<'a> {
msg_func: Option<AirMsg>, msg_func: Option<AirMsg>,
) -> AirTree { ) -> AirTree {
assert!(tipo.get_generic().is_none()); assert!(tipo.get_generic().is_none());
let tipo = &convert_opaque_type(tipo, &self.data_types); let tipo = &convert_opaque_type(tipo, &self.data_types, true);
if tipo.is_primitive() { if tipo.is_primitive() {
// Since we would return void anyway and ignore then we can just return value here and ignore // Since we would return void anyway and ignore then we can just return value here and ignore
@ -2775,7 +2775,7 @@ impl<'a> CodeGenerator<'a> {
let param = AirTree::local_var(&arg_name, data()); let param = AirTree::local_var(&arg_name, data());
let actual_type = convert_opaque_type(&arg.tipo, &self.data_types); let actual_type = convert_opaque_type(&arg.tipo, &self.data_types, true);
let msg_func = match self.tracing { let msg_func = match self.tracing {
TraceLevel::Silent => None, TraceLevel::Silent => None,
@ -3632,12 +3632,13 @@ impl<'a> CodeGenerator<'a> {
let mut function_def_types = function_def let mut function_def_types = function_def
.arguments .arguments
.iter() .iter()
.map(|arg| convert_opaque_type(&arg.tipo, &self.data_types)) .map(|arg| convert_opaque_type(&arg.tipo, &self.data_types, true))
.collect_vec(); .collect_vec();
function_def_types.push(convert_opaque_type( function_def_types.push(convert_opaque_type(
&function_def.return_type, &function_def.return_type,
&self.data_types, &self.data_types,
true,
)); ));
let mono_types: IndexMap<u64, Rc<Type>> = if !function_def_types.is_empty() { let mono_types: IndexMap<u64, Rc<Type>> = if !function_def_types.is_empty() {

View File

@ -347,6 +347,7 @@ pub fn get_arg_type_name(tipo: &Type) -> String {
pub fn convert_opaque_type( pub fn convert_opaque_type(
t: &Rc<Type>, t: &Rc<Type>,
data_types: &IndexMap<DataTypeKey, &TypedDataType>, data_types: &IndexMap<DataTypeKey, &TypedDataType>,
deep: bool,
) -> Rc<Type> { ) -> Rc<Type> {
if check_replaceable_opaque_type(t, data_types) && matches!(t.as_ref(), Type::App { .. }) { if check_replaceable_opaque_type(t, data_types) && matches!(t.as_ref(), Type::App { .. }) {
let data_type = lookup_data_type_by_tipo(data_types, t).unwrap(); let data_type = lookup_data_type_by_tipo(data_types, t).unwrap();
@ -363,7 +364,11 @@ pub fn convert_opaque_type(
let mono_type = find_and_replace_generics(generic_type, &mono_types); let mono_type = find_and_replace_generics(generic_type, &mono_types);
convert_opaque_type(&mono_type, data_types) if deep {
convert_opaque_type(&mono_type, data_types, deep)
} else {
mono_type
}
} else { } else {
match t.as_ref() { match t.as_ref() {
Type::App { Type::App {
@ -374,7 +379,7 @@ pub fn convert_opaque_type(
} => { } => {
let mut new_args = vec![]; let mut new_args = vec![];
for arg in args { for arg in args {
let arg = convert_opaque_type(arg, data_types); let arg = convert_opaque_type(arg, data_types, deep);
new_args.push(arg); new_args.push(arg);
} }
Type::App { Type::App {
@ -388,11 +393,11 @@ pub fn convert_opaque_type(
Type::Fn { args, ret } => { Type::Fn { args, ret } => {
let mut new_args = vec![]; let mut new_args = vec![];
for arg in args { for arg in args {
let arg = convert_opaque_type(arg, data_types); let arg = convert_opaque_type(arg, data_types, deep);
new_args.push(arg); new_args.push(arg);
} }
let ret = convert_opaque_type(ret, data_types); let ret = convert_opaque_type(ret, data_types, deep);
Type::Fn { Type::Fn {
args: new_args, args: new_args,
@ -402,7 +407,7 @@ pub fn convert_opaque_type(
} }
Type::Var { tipo: var_tipo } => { Type::Var { tipo: var_tipo } => {
if let TypeVar::Link { tipo } = &var_tipo.borrow().clone() { if let TypeVar::Link { tipo } = &var_tipo.borrow().clone() {
convert_opaque_type(tipo, data_types) convert_opaque_type(tipo, data_types, deep)
} else { } else {
t.clone() t.clone()
} }
@ -410,7 +415,7 @@ pub fn convert_opaque_type(
Type::Tuple { elems } => { Type::Tuple { elems } => {
let mut new_elems = vec![]; let mut new_elems = vec![];
for arg in elems { for arg in elems {
let arg = convert_opaque_type(arg, data_types); let arg = convert_opaque_type(arg, data_types, deep);
new_elems.push(arg); new_elems.push(arg);
} }
Type::Tuple { elems: new_elems }.into() Type::Tuple { elems: new_elems }.into()
@ -420,7 +425,7 @@ pub fn convert_opaque_type(
} }
pub fn check_replaceable_opaque_type( pub fn check_replaceable_opaque_type(
t: &Rc<Type>, t: &Type,
data_types: &IndexMap<DataTypeKey, &TypedDataType>, data_types: &IndexMap<DataTypeKey, &TypedDataType>,
) -> bool { ) -> bool {
let data_type = lookup_data_type_by_tipo(data_types, t); let data_type = lookup_data_type_by_tipo(data_types, t);
@ -633,7 +638,7 @@ pub fn erase_opaque_type_operations(
let mut held_types = air_tree.mut_held_types(); let mut held_types = air_tree.mut_held_types();
while let Some(tipo) = held_types.pop() { while let Some(tipo) = held_types.pop() {
*tipo = convert_opaque_type(tipo, data_types); *tipo = convert_opaque_type(tipo, data_types, true);
} }
} }

View File

@ -1,7 +1,7 @@
use super::Type; use super::Type;
use crate::error::ExtraData;
use crate::{ use crate::{
ast::{Annotation, BinOp, CallArg, LogicalOpChainKind, Span, UntypedPattern}, ast::{Annotation, BinOp, CallArg, LogicalOpChainKind, Span, UntypedPattern},
error::ExtraData,
expr::{self, UntypedExpr}, expr::{self, UntypedExpr},
format::Formatter, format::Formatter,
levenshtein, levenshtein,
@ -261,7 +261,9 @@ You can use '{discard}' and numbers to distinguish between similar names.
#[error("I found a type definition that has a function type in it. This is not allowed.\n")] #[error("I found a type definition that has a function type in it. This is not allowed.\n")]
#[diagnostic(code("illegal::function_in_type"))] #[diagnostic(code("illegal::function_in_type"))]
#[diagnostic(help("Data-types can't hold functions. If you want to define method-like functions, group the type definition and the methods under a common namespace in a standalone module."))] #[diagnostic(help(
"Data-types can't hold functions. If you want to define method-like functions, group the type definition and the methods under a common namespace in a standalone module."
))]
FunctionTypeInData { FunctionTypeInData {
#[label] #[label]
location: Span, location: Span,
@ -477,9 +479,13 @@ If you really meant to return that last expression, try to replace it with the f
"I stumbled upon an invalid (non-local) clause guard '{}'.\n", "I stumbled upon an invalid (non-local) clause guard '{}'.\n",
name.if_supports_color(Stdout, |s| s.purple()) name.if_supports_color(Stdout, |s| s.purple())
)] )]
#[diagnostic(url("https://aiken-lang.org/language-tour/control-flow#checking-equality-and-ordering-in-patterns"))] #[diagnostic(url(
"https://aiken-lang.org/language-tour/control-flow#checking-equality-and-ordering-in-patterns"
))]
#[diagnostic(code("illegal::clause_guard"))] #[diagnostic(code("illegal::clause_guard"))]
#[diagnostic(help("There are some conditions regarding what can be used in a guard. Values must be either local to the function, or defined as module constants. You can't use functions or records in there."))] #[diagnostic(help(
"There are some conditions regarding what can be used in a guard. Values must be either local to the function, or defined as module constants. You can't use functions or records in there."
))]
NonLocalClauseGuardVariable { NonLocalClauseGuardVariable {
#[label] #[label]
location: Span, location: Span,
@ -492,7 +498,7 @@ If you really meant to return that last expression, try to replace it with the f
#[diagnostic(url("https://aiken-lang.org/language-tour/primitive-types#tuples"))] #[diagnostic(url("https://aiken-lang.org/language-tour/primitive-types#tuples"))]
#[diagnostic(code("illegal::tuple_index"))] #[diagnostic(code("illegal::tuple_index"))]
#[diagnostic(help( #[diagnostic(help(
r#"Because you used a tuple-index on an element, I assumed it had to be a tuple or some kind, but instead I found: r#"Because you used a tuple-index on an element, I assumed it had to be a tuple but instead I found something of type:
{type_info}"#, {type_info}"#,
type_info = tipo.to_pretty(0).if_supports_color(Stdout, |s| s.red()) type_info = tipo.to_pretty(0).if_supports_color(Stdout, |s| s.red())
@ -637,7 +643,9 @@ You can help me by providing a type-annotation for 'x', as such:
#[error("I almost got caught in an endless loop while inferring a recursive type.\n")] #[error("I almost got caught in an endless loop while inferring a recursive type.\n")]
#[diagnostic(url("https://aiken-lang.org/language-tour/custom-types#type-annotations"))] #[diagnostic(url("https://aiken-lang.org/language-tour/custom-types#type-annotations"))]
#[diagnostic(code("missing::type_annotation"))] #[diagnostic(code("missing::type_annotation"))]
#[diagnostic(help("I have several aptitudes, but inferring recursive types isn't one them. It is still possible to define recursive types just fine, but I will need a little help in the form of type annotation to infer their types should they show up."))] #[diagnostic(help(
"I have several aptitudes, but inferring recursive types isn't one them. It is still possible to define recursive types just fine, but I will need a little help in the form of type annotation to infer their types should they show up."
))]
RecursiveType { RecursiveType {
#[label] #[label]
location: Span, location: Span,
@ -961,7 +969,8 @@ The best thing to do from here is to remove it."#))]
#[error("I choked on a generic type left in an outward-facing interface.\n")] #[error("I choked on a generic type left in an outward-facing interface.\n")]
#[diagnostic(code("illegal::generic_in_abi"))] #[diagnostic(code("illegal::generic_in_abi"))]
#[diagnostic(help( #[diagnostic(help(
"Functions of the outer-most parts of a project, such as a validator or a property-based test, must be fully instantiated. That means they can no longer carry unbound generic variables. The type must be fully-known at this point since many structural validation must occur to ensure a safe boundary between the on-chain and off-chain worlds."))] "Functions of the outer-most parts of a project, such as a validator or a property-based test, must be fully instantiated. That means they can no longer carry unbound generic variables. The type must be fully-known at this point since many structural validation must occur to ensure a safe boundary between the on-chain and off-chain worlds."
))]
GenericLeftAtBoundary { GenericLeftAtBoundary {
#[label("unbound generic at boundary")] #[label("unbound generic at boundary")]
location: Span, location: Span,

View File

@ -1,5 +1,10 @@
use std::{borrow::Borrow, collections::HashMap, ops::Deref, rc::Rc}; use super::{
environment::{generalise, EntityKind, Environment},
error::{Error, UnifyErrorSituation, Warning},
expr::ExprTyper,
hydrator::Hydrator,
TypeInfo, ValueConstructor, ValueConstructorVariant,
};
use crate::{ use crate::{
ast::{ ast::{
Annotation, Arg, ArgName, ArgVia, DataType, Definition, Function, Layer, ModuleConstant, Annotation, Arg, ArgName, ArgVia, DataType, Definition, Function, Layer, ModuleConstant,
@ -14,14 +19,7 @@ use crate::{
tipo::{Span, Type, TypeVar}, tipo::{Span, Type, TypeVar},
IdGenerator, IdGenerator,
}; };
use std::{borrow::Borrow, collections::HashMap, ops::Deref, rc::Rc};
use super::{
environment::{generalise, EntityKind, Environment},
error::{Error, UnifyErrorSituation, Warning},
expr::ExprTyper,
hydrator::Hydrator,
TypeInfo, ValueConstructor, ValueConstructorVariant,
};
impl UntypedModule { impl UntypedModule {
pub fn infer( pub fn infer(
@ -347,6 +345,21 @@ fn infer_definition(
let (inferred_annotation, inner_type) = let (inferred_annotation, inner_type) =
infer_fuzzer(environment, &typed_via.tipo(), &arg.via.location())?; infer_fuzzer(environment, &typed_via.tipo(), &arg.via.location())?;
// 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 test");
if let Type::Fn { ref ret, .. } = scope.tipo.as_ref() {
scope.tipo = Rc::new(Type::Fn {
ret: ret.clone(),
args: vec![inner_type.clone()],
})
}
// Ensure that the annotation, if any, matches the type inferred from the
// Fuzzer.
if let Some(ref provided_annotation) = arg.annotation { if let Some(ref provided_annotation) = arg.annotation {
let hydrator: &mut Hydrator = hydrators.get_mut(&f.name).unwrap(); let hydrator: &mut Hydrator = hydrators.get_mut(&f.name).unwrap();

View File

@ -30,7 +30,7 @@ use crate::{
}; };
use aiken_lang::{ use aiken_lang::{
ast::{ ast::{
DataTypeKey, Definition, FunctionAccessKey, ModuleKind, Tracing, TypedDataType, DataTypeKey, Definition, FunctionAccessKey, ModuleKind, TraceLevel, Tracing, TypedDataType,
TypedFunction, Validator, TypedFunction, Validator,
}, },
builtins, builtins,
@ -810,6 +810,13 @@ where
fn run_tests(&self, tests: Vec<Test>) -> Vec<TestResult<UntypedExpr>> { fn run_tests(&self, tests: Vec<Test>) -> Vec<TestResult<UntypedExpr>> {
use rayon::prelude::*; use rayon::prelude::*;
let generator = self.checked_modules.new_generator(
&self.functions,
&self.data_types,
&self.module_types,
Tracing::All(TraceLevel::Silent),
);
tests tests
.into_par_iter() .into_par_iter()
.map(|test| match test { .map(|test| match test {
@ -820,7 +827,7 @@ where
}) })
.collect::<Vec<TestResult<PlutusData>>>() .collect::<Vec<TestResult<PlutusData>>>()
.into_iter() .into_iter()
.map(|test| test.reify(&self.module_types)) .map(|test| test.reify(generator.data_types()))
.collect() .collect()
} }

View File

@ -1,32 +1,31 @@
use crate::pretty; use crate::pretty;
use aiken_lang::{ use aiken_lang::{
ast::{BinOp, Span, TypedTest}, ast::{BinOp, DataTypeKey, Span, TypedDataType, TypedTest},
expr::{TypedExpr, UntypedExpr}, expr::{TypedExpr, UntypedExpr},
gen_uplc::{ gen_uplc::{
builder::{convert_data_to_type, convert_opaque_type}, builder::{convert_data_to_type, convert_opaque_type},
CodeGenerator, CodeGenerator,
}, },
tipo::{Type, TypeInfo}, tipo::Type,
}; };
use indexmap::IndexMap;
use pallas::{ use pallas::{
codec::utils::Int, codec::utils::Int,
ledger::primitives::alonzo::{BigInt, Constr, PlutusData}, ledger::primitives::alonzo::{BigInt, Constr, PlutusData},
}; };
use std::{ use std::{
borrow::Borrow, borrow::Borrow,
collections::HashMap,
fmt::{self, Display}, fmt::{self, Display},
path::PathBuf, path::PathBuf,
rc::Rc, rc::Rc,
}; };
use uplc::{ use uplc::{
ast::{Constant, Data, NamedDeBruijn, Program, Term}, ast::{Constant, Data, Name, NamedDeBruijn, Program, Term},
machine::{cost_model::ExBudget, eval_result::EvalResult}, machine::{cost_model::ExBudget, eval_result::EvalResult},
parser::interner::Interner,
}; };
/// ---------------------------------------------------------------------------- /// ----- Test -----------------------------------------------------------------
///
/// Test
/// ///
/// Aiken supports two kinds of tests: unit and property. A unit test is a simply /// 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. /// UPLC program which returns must be a lambda that returns a boolean.
@ -42,7 +41,7 @@ use uplc::{
/// Our approach could perhaps be called "microthesis", as it implements a subset of /// Our approach could perhaps be called "microthesis", as it implements a subset of
/// minithesis. More specifically, we do not currently support pre-conditions, nor /// minithesis. More specifically, we do not currently support pre-conditions, nor
/// targets. /// targets.
/// ---------------------------------------------------------------------------- ///
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Test { pub enum Test {
UnitTest(UnitTest), UnitTest(UnitTest),
@ -75,8 +74,8 @@ impl Test {
module: String, module: String,
name: String, name: String,
can_error: bool, can_error: bool,
program: Program<NamedDeBruijn>, program: Program<Name>,
fuzzer: Fuzzer<NamedDeBruijn>, fuzzer: Fuzzer<Name>,
) -> Test { ) -> Test {
Test::PropertyTest(PropertyTest { Test::PropertyTest(PropertyTest {
input_path, input_path,
@ -131,7 +130,7 @@ impl Test {
let via = parameter.via.clone(); let via = parameter.via.clone();
let type_info = convert_opaque_type(&parameter.tipo, generator.data_types()); let type_info = parameter.tipo.clone();
// TODO: Possibly refactor 'generate_raw' to accept arguments and do this wrapping // TODO: Possibly refactor 'generate_raw' to accept arguments and do this wrapping
// itself. // itself.
@ -147,17 +146,9 @@ impl Test {
return_annotation: None, return_annotation: None,
}; };
let program = generator let program = generator.clone().generate_raw(&body, &module_name);
.clone()
.generate_raw(&body, &module_name)
.try_into()
.unwrap();
let fuzzer: Program<NamedDeBruijn> = generator let fuzzer = generator.clone().generate_raw(&via, &module_name);
.clone()
.generate_raw(&via, &module_name)
.try_into()
.unwrap();
Self::property_test( Self::property_test(
input_path, input_path,
@ -167,6 +158,11 @@ impl Test {
program, program,
Fuzzer { Fuzzer {
program: fuzzer, program: fuzzer,
stripped_type_info: convert_opaque_type(
&type_info,
generator.data_types(),
true,
),
type_info, type_info,
}, },
) )
@ -174,12 +170,8 @@ impl Test {
} }
} }
// ---------------------------------------------------------------------------- /// ----- UnitTest -----------------------------------------------------------------
// ///
// UnitTest
//
// ----------------------------------------------------------------------------
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct UnitTest { pub struct UnitTest {
pub input_path: PathBuf, pub input_path: PathBuf,
@ -205,20 +197,16 @@ impl UnitTest {
} }
} }
// ---------------------------------------------------------------------------- /// ----- PropertyTest -----------------------------------------------------------------
// ///
// PropertyTest
//
// ----------------------------------------------------------------------------
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PropertyTest { pub struct PropertyTest {
pub input_path: PathBuf, pub input_path: PathBuf,
pub module: String, pub module: String,
pub name: String, pub name: String,
pub can_error: bool, pub can_error: bool,
pub program: Program<NamedDeBruijn>, pub program: Program<Name>,
pub fuzzer: Fuzzer<NamedDeBruijn>, pub fuzzer: Fuzzer<Name>,
} }
unsafe impl Send for PropertyTest {} unsafe impl Send for PropertyTest {}
@ -226,7 +214,13 @@ unsafe impl Send for PropertyTest {}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Fuzzer<T> { pub struct Fuzzer<T> {
pub program: Program<T>, pub program: Program<T>,
pub type_info: Rc<Type>, pub type_info: Rc<Type>,
/// A version of the Fuzzer's type that has gotten rid of
/// all erasable opaque type. This is needed in order to
/// generate Plutus data with the appropriate shape.
pub stripped_type_info: Rc<Type>,
} }
impl PropertyTest { impl PropertyTest {
@ -239,7 +233,7 @@ impl PropertyTest {
let (counterexample, iterations) = match self.run_n_times(n, seed, None) { let (counterexample, iterations) = match self.run_n_times(n, seed, None) {
None => (None, n), None => (None, n),
Some((remaining, counterexample)) => (Some(counterexample), n - remaining + 1), Some((remaining, counterexample)) => (Some(counterexample.value), n - remaining + 1),
}; };
TestResult::PropertyTestResult(PropertyTestResult { TestResult::PropertyTestResult(PropertyTestResult {
@ -249,12 +243,12 @@ impl PropertyTest {
}) })
} }
fn run_n_times( fn run_n_times<'a>(
&self, &'a self,
remaining: usize, remaining: usize,
seed: u32, seed: u32,
counterexample: Option<(usize, PlutusData)>, counterexample: Option<(usize, Counterexample<'a>)>,
) -> Option<(usize, PlutusData)> { ) -> Option<(usize, Counterexample<'a>)> {
// We short-circuit failures in case we have any. The counterexample is already simplified // We short-circuit failures in case we have any. The counterexample is already simplified
// at this point. // at this point.
if remaining > 0 && counterexample.is_none() { if remaining > 0 && counterexample.is_none() {
@ -269,7 +263,7 @@ impl PropertyTest {
} }
} }
fn run_once(&self, seed: u32) -> (u32, Option<PlutusData>) { fn run_once(&self, seed: u32) -> (u32, Option<Counterexample<'_>>) {
let (next_prng, value) = Prng::from_seed(seed) let (next_prng, value) = Prng::from_seed(seed)
.sample(&self.fuzzer.program) .sample(&self.fuzzer.program)
.expect("running seeded Prng cannot fail."); .expect("running seeded Prng cannot fail.");
@ -291,7 +285,7 @@ impl PropertyTest {
counterexample.simplify(); counterexample.simplify();
} }
(next_seed, Some(counterexample.value)) (next_seed, Some(counterexample))
} else { } else {
(next_seed, None) (next_seed, None)
} }
@ -301,19 +295,37 @@ impl PropertyTest {
} }
pub fn eval(&self, value: &PlutusData) -> EvalResult { pub fn eval(&self, value: &PlutusData) -> EvalResult {
let term = convert_data_to_type(Term::data(value.clone()), &self.fuzzer.type_info) let term: Term<Name> =
.try_into() convert_data_to_type(Term::data(value.clone()), &self.fuzzer.stripped_type_info);
.expect("safe conversion from Name -> NamedDeBruijn");
self.program.apply_term(&term).eval(ExBudget::max()) let mut program = self.program.apply_term(&term);
Interner::new().program(&mut program);
Program::<NamedDeBruijn>::try_from(program)
.unwrap()
.eval(ExBudget::max())
} }
} }
// ---------------------------------------------------------------------------- /// ----- PRNG -----------------------------------------------------------------
// ///
// Prng /// A Pseudo-random generator (PRNG) used to produce random values for fuzzers.
// /// Note that the randomness isn't actually managed by the Rust framework, it
// ---------------------------------------------------------------------------- /// entirely relies on properties of hashing algorithm on-chain (e.g. blake2b).
///
/// The PRNG can have two forms:
///
/// 1. Seeded: which occurs during the initial run of a property. Each time a
/// number is drawn from the PRNG, a new seed is created. We retain all the
/// choices drawn in a _choices_ vector.
///
/// 2. Replayed: which is used to replay a Prng sequenced from a list of known
/// choices. This happens when shrinking an example. Instead of trying to
/// shrink the value directly, we shrink the PRNG sequence with the hope that
/// it will generate a smaller value. This implies that generators tend to
/// generate smaller values when drawing smaller numbers.
///
#[derive(Debug)] #[derive(Debug)]
pub enum Prng { pub enum Prng {
Seeded { Seeded {
@ -385,9 +397,9 @@ impl Prng {
} }
/// Generate a pseudo-random value from a fuzzer using the given PRNG. /// Generate a pseudo-random value from a fuzzer using the given PRNG.
pub fn sample(&self, fuzzer: &Program<NamedDeBruijn>) -> Option<(Prng, PlutusData)> { pub fn sample(&self, fuzzer: &Program<Name>) -> Option<(Prng, PlutusData)> {
let result = fuzzer let result = Program::<NamedDeBruijn>::try_from(fuzzer.apply_data(self.uplc()))
.apply_data(self.uplc()) .unwrap()
.eval(ExBudget::max()) .eval(ExBudget::max())
.result() .result()
.expect("Fuzzer crashed?"); .expect("Fuzzer crashed?");
@ -464,14 +476,12 @@ impl Prng {
} }
} }
// ---------------------------------------------------------------------------- /// ----- Counterexample -----------------------------------------------------------------
// ///
// Counterexample /// A counterexample is constructed from a test failure. It holds a value, and a sequence
// /// of random choices that led to this value. It holds a reference to the underlying
// A counterexample is constructed on test failures. /// property and fuzzer. In many cases, a counterexample can be simplified (a.k.a "shrinked")
// /// into a smaller counterexample.
// ----------------------------------------------------------------------------
#[derive(Debug)] #[derive(Debug)]
pub struct Counterexample<'a> { pub struct Counterexample<'a> {
pub value: PlutusData, pub value: PlutusData,
@ -690,7 +700,10 @@ pub enum TestResult<T> {
unsafe impl<T> Send for TestResult<T> {} unsafe impl<T> Send for TestResult<T> {}
impl TestResult<PlutusData> { impl TestResult<PlutusData> {
pub fn reify(self, data_types: &HashMap<String, TypeInfo>) -> TestResult<UntypedExpr> { pub fn reify(
self,
data_types: &IndexMap<DataTypeKey, &TypedDataType>,
) -> TestResult<UntypedExpr> {
match self { match self {
TestResult::UnitTestResult(test) => TestResult::UnitTestResult(test), TestResult::UnitTestResult(test) => TestResult::UnitTestResult(test),
TestResult::PropertyTestResult(test) => { TestResult::PropertyTestResult(test) => {
@ -789,7 +802,10 @@ pub struct PropertyTestResult<T> {
unsafe impl<T> Send for PropertyTestResult<T> {} unsafe impl<T> Send for PropertyTestResult<T> {}
impl PropertyTestResult<PlutusData> { impl PropertyTestResult<PlutusData> {
pub fn reify(self, data_types: &HashMap<String, TypeInfo>) -> PropertyTestResult<UntypedExpr> { pub fn reify(
self,
data_types: &IndexMap<DataTypeKey, &TypedDataType>,
) -> PropertyTestResult<UntypedExpr> {
PropertyTestResult { PropertyTestResult {
counterexample: match self.counterexample { counterexample: match self.counterexample {
None => None, None => None,
@ -882,16 +898,19 @@ mod test {
use crate::module::{CheckedModule, CheckedModules}; use crate::module::{CheckedModule, CheckedModules};
use aiken_lang::{ use aiken_lang::{
ast::{Definition, ModuleKind, TraceLevel, Tracing}, ast::{Definition, ModuleKind, TraceLevel, Tracing},
builtins, parser, builtins,
format::Formatter,
parser,
parser::extra::ModuleExtra, parser::extra::ModuleExtra,
IdGenerator, IdGenerator,
}; };
use indoc::indoc; use indoc::indoc;
use std::collections::HashMap;
const TEST_KIND: ModuleKind = ModuleKind::Lib; const TEST_KIND: ModuleKind = ModuleKind::Lib;
impl Test { impl Test {
pub fn from_source(src: &str) -> Self { pub fn from_source(src: &str) -> (Self, IndexMap<DataTypeKey, TypedDataType>) {
let id_gen = IdGenerator::new(); let id_gen = IdGenerator::new();
let module_name = ""; let module_name = "";
@ -924,6 +943,22 @@ mod test {
.last() .last()
.expect("No test found in declared src?"); .expect("No test found in declared src?");
let functions = builtins::prelude_functions(&id_gen);
let mut data_types = builtins::prelude_data_types(&id_gen);
for def in ast.definitions() {
if let Definition::DataType(dt) = def {
data_types.insert(
DataTypeKey {
module_name: module_name.to_string(),
defined_type: dt.name.clone(),
},
dt.clone(),
);
}
}
let mut modules = CheckedModules::default(); let mut modules = CheckedModules::default();
modules.insert( modules.insert(
module_name.to_string(), module_name.to_string(),
@ -938,10 +973,6 @@ mod test {
}, },
); );
let functions = builtins::prelude_functions(&id_gen);
let data_types = builtins::prelude_data_types(&id_gen);
let mut generator = modules.new_generator( let mut generator = modules.new_generator(
&functions, &functions,
&data_types, &data_types,
@ -949,16 +980,19 @@ mod test {
Tracing::All(TraceLevel::Verbose), Tracing::All(TraceLevel::Verbose),
); );
(
Self::from_function_definition( Self::from_function_definition(
&mut generator, &mut generator,
test.to_owned(), test.to_owned(),
module_name.to_string(), module_name.to_string(),
PathBuf::new(), PathBuf::new(),
),
data_types,
) )
} }
} }
fn property(src: &str) -> PropertyTest { fn property(src: &str) -> (PropertyTest, impl Fn(PlutusData) -> String) {
let prelude = indoc! { r#" let prelude = indoc! { r#"
use aiken/builtin use aiken/builtin
@ -999,6 +1033,22 @@ mod test {
} }
} }
fn bool() -> Fuzzer<Bool> {
int() |> map(fn(n) { n % 2 == 0 })
}
fn bytearray() -> Fuzzer<ByteArray> {
int()
|> map(
fn(n) {
n
|> builtin.integer_to_bytearray(True, 32, _)
|> builtin.blake2b_256()
|> builtin.slice_bytearray(8, 4, _)
},
)
}
pub fn constant(a: a) -> Fuzzer<a> { pub fn constant(a: a) -> Fuzzer<a> {
fn(s0) { Some((s0, a)) } fn(s0) { Some((s0, a)) }
} }
@ -1020,39 +1070,58 @@ 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 {
Some((s1, a)) ->
when fuzz_b(s1) is {
Some((s2, b)) -> Some((s2, f(a, b)))
None -> None
}
None -> None
}
}
}
"#}; "#};
let src = format!("{prelude}\n{src}"); let src = format!("{prelude}\n{src}");
match Test::from_source(&src) { match Test::from_source(&src) {
Test::PropertyTest(test) => test, (Test::PropertyTest(test), data_types) => {
Test::UnitTest(..) => panic!("Expected to yield a PropertyTest but found a UnitTest"), let type_info = test.fuzzer.type_info.clone();
let reify = move |counterexample| {
let mut data_type_refs = IndexMap::new();
for (k, v) in &data_types {
data_type_refs.insert(k.clone(), v);
}
let expr = UntypedExpr::reify(&data_type_refs, counterexample, &type_info)
.expect("Failed to reify value.");
Formatter::new().expr(&expr, false).to_pretty_string(70)
};
(test, reify)
}
(Test::UnitTest(..), _) => {
panic!("Expected to yield a PropertyTest but found a UnitTest")
}
} }
} }
impl PropertyTest { impl PropertyTest {
fn expect_failure(&self, seed: u32) -> Counterexample { fn expect_failure(&self, seed: u32) -> Counterexample {
let (next_prng, value) = Prng::from_seed(seed) match self.run_n_times(PropertyTest::MAX_TEST_RUN, seed, None) {
.sample(&self.fuzzer.program) Some((_, counterexample)) => counterexample,
.expect("running seeded Prng cannot fail."); _ => panic!("expected property to fail but it didn't."),
let result = self.eval(&value);
if result.failed(self.can_error) {
return Counterexample {
value,
choices: next_prng.choices(),
property: self,
};
} }
unreachable!("Prng constructed from a seed necessarily yield a seed.");
} }
} }
#[test] #[test]
fn test_prop_basic() { fn test_prop_basic() {
let prop = property(indoc! { r#" let (prop, _) = property(indoc! { r#"
test foo(n: Int via int()) { test foo(n: Int via int()) {
n >= 0 n >= 0
} }
@ -1063,16 +1132,249 @@ mod test {
#[test] #[test]
fn test_prop_always_odd() { fn test_prop_always_odd() {
let prop = property(indoc! { r#" let (prop, reify) = property(indoc! { r#"
test foo(n: Int via int()) { test foo(n: Int via int()) {
n % 2 == 0 n % 2 == 0
} }
"#}); "#});
let mut counterexample = prop.expect_failure(12); let mut counterexample = prop.expect_failure(42);
counterexample.simplify(); counterexample.simplify();
assert_eq!(counterexample.choices, vec![1]); assert_eq!(counterexample.choices, vec![1]);
assert_eq!(reify(counterexample.value), "1");
}
#[test]
fn test_prop_combine() {
let (prop, reify) = property(indoc! { r#"
fn pair(fuzz_a: Fuzzer<a>, fuzz_b: Fuzzer<b>) -> Fuzzer<(a, b)> {
fuzz_a
|> and_then(fn(a) {
fuzz_b
|> map(fn(b) {
(a, b)
})
})
}
test foo(t: (Int, Int) via pair(int(), int())) {
t.1st + t.2nd <= 400
}
"#});
let mut counterexample = prop.expect_failure(42);
counterexample.simplify();
assert_eq!(counterexample.choices, vec![201, 200]);
assert_eq!(reify(counterexample.value), "(201, 200)");
}
#[test]
fn test_prop_enum_bool() {
let (prop, reify) = property(indoc! { r#"
test foo(predicate via bool()) {
predicate
}
"#});
let mut counterexample = prop.expect_failure(42);
counterexample.simplify();
assert_eq!(counterexample.choices, vec![1]);
assert_eq!(reify(counterexample.value), "False");
}
#[test]
fn test_prop_enum_custom() {
let (prop, reify) = property(indoc! { r#"
type Temperature {
Hot
Cold
}
fn temperature() -> Fuzzer<Temperature> {
bool() |> map(fn(is_cold) {
if is_cold { Cold } else { Hot }
})
}
test foo(t via temperature()) {
t == Hot
}
"#});
let mut counterexample = prop.expect_failure(42);
counterexample.simplify();
assert_eq!(counterexample.choices, vec![0]);
assert_eq!(reify(counterexample.value), "Cold");
}
#[test]
fn test_prop_opaque() {
let (prop, reify) = property(indoc! { r#"
opaque type Temperature {
Hot
Cold
}
fn temperature() -> Fuzzer<Temperature> {
bool() |> map(fn(is_cold) {
if is_cold { Cold } else { Hot }
})
}
test foo(t via temperature()) {
t == Hot
}
"#});
let mut counterexample = prop.expect_failure(42);
counterexample.simplify();
assert_eq!(counterexample.choices, vec![0]);
assert_eq!(reify(counterexample.value), "Cold");
}
#[test]
fn test_prop_private_enum() {
let (prop, reify) = property(indoc! { r#"
type Vehicle {
Car { wheels: Int }
Bike { wheels: Int }
}
fn vehicle() -> Fuzzer<Vehicle> {
bool() |> map(fn(is_car) {
if is_car { Car(4) } else { Bike(2) }
})
}
test foo(v via vehicle()) {
when v is {
Car { wheels } -> wheels
Bike { wheels } -> wheels
} == 4
}
"#});
let mut counterexample = prop.expect_failure(42);
counterexample.simplify();
assert_eq!(counterexample.choices, vec![1]);
assert_eq!(reify(counterexample.value), "Bike { wheels: 2 }");
}
#[test]
fn test_prop_list() {
let (prop, reify) = property(indoc! { r#"
fn list(elem: Fuzzer<a>) -> Fuzzer<List<a>> {
bool()
|> and_then(fn(continue) {
if continue {
map2(elem, list(elem), fn(head, tail) { [head, ..tail] })
} else {
constant([])
}
})
}
fn length(es: List<a>) -> Int {
when es is {
[] -> 0
[_, ..tail] -> 1 + length(tail)
}
}
test foo(es: List<Int> via list(int())) {
length(es) < 3
}
"#});
let mut counterexample = prop.expect_failure(42);
counterexample.simplify();
assert_eq!(counterexample.choices, vec![0, 0, 0, 0, 0, 0, 1]);
assert_eq!(reify(counterexample.value), "[0, 0, 0]");
}
#[test]
fn test_prop_opaque_dict() {
let (prop, reify) = property(indoc! { r#"
pub opaque type Dict<a> {
inner: List<(ByteArray, a)>,
}
fn dict(elem: Fuzzer<a>) -> Fuzzer<Dict<a>> {
bool()
|> and_then(
fn(continue) {
if continue {
let kv = map2(bytearray(), elem, fn(k, v) { (k, v) })
map2(kv, dict(elem), fn(head, tail) { Dict([head, ..tail.inner]) })
} else {
constant(Dict([]))
}
},
)
}
test foo(d via dict(bool())) {
d == Dict([])
}
"#});
let mut counterexample = prop.expect_failure(42);
counterexample.simplify();
assert_eq!(counterexample.choices, vec![0, 0, 0, 1]);
assert_eq!(reify(counterexample.value), "Dict([(#\"2cd15ed0\", True)])");
}
#[test]
fn test_prop_opaque_nested_dict() {
let (prop, reify) = property(indoc! { r#"
pub opaque type Dict<a> {
inner: List<(ByteArray, a)>,
}
fn dict(elem: Fuzzer<a>) -> Fuzzer<Dict<a>> {
bool()
|> and_then(
fn(continue) {
if continue {
let kv = map2(bytearray(), elem, fn(k, v) { (k, v) })
map2(kv, dict(elem), fn(head, tail) { Dict([head, ..tail.inner]) })
} else {
constant(Dict([]))
}
},
)
}
test foo(d via dict(dict(int()))) {
d == Dict([])
}
"#});
let mut counterexample = prop.expect_failure(42);
counterexample.simplify();
assert_eq!(counterexample.choices, vec![0, 0, 1, 1]);
assert_eq!(
reify(counterexample.value),
"Dict([(#\"2cd15ed0\", Dict([]))])"
);
} }
} }