Display counterexamples as Aiken values instead of raw UPLC.
This commit is contained in:
parent
c766f44601
commit
14f1025f0b
|
@ -88,6 +88,7 @@ dependencies = [
|
||||||
"num-bigint",
|
"num-bigint",
|
||||||
"ordinal",
|
"ordinal",
|
||||||
"owo-colors",
|
"owo-colors",
|
||||||
|
"pallas",
|
||||||
"petgraph",
|
"petgraph",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"strum",
|
"strum",
|
||||||
|
|
|
@ -21,6 +21,7 @@ itertools = "0.10.5"
|
||||||
miette = "5.9.0"
|
miette = "5.9.0"
|
||||||
ordinal = "0.3.2"
|
ordinal = "0.3.2"
|
||||||
owo-colors = { version = "3.5.0", features = ["supports-colors"] }
|
owo-colors = { version = "3.5.0", features = ["supports-colors"] }
|
||||||
|
pallas.workspace = true
|
||||||
strum = "0.24.1"
|
strum = "0.24.1"
|
||||||
thiserror = "1.0.39"
|
thiserror = "1.0.39"
|
||||||
vec1 = "1.10.1"
|
vec1 = "1.10.1"
|
||||||
|
|
|
@ -242,6 +242,18 @@ pub struct TypeAlias<T> {
|
||||||
pub tipo: T,
|
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<Rc<Type>>;
|
pub type TypedDataType = DataType<Rc<Type>>;
|
||||||
|
|
||||||
impl TypedDataType {
|
impl TypedDataType {
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
use crate::{
|
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,
|
expr::TypedExpr,
|
||||||
gen_uplc::builder::{DataTypeKey, FunctionAccessKey},
|
|
||||||
tipo::{
|
tipo::{
|
||||||
fields::FieldMap, Type, TypeConstructor, TypeInfo, TypeVar, ValueConstructor,
|
fields::FieldMap, Type, TypeConstructor, TypeInfo, TypeVar, ValueConstructor,
|
||||||
ValueConstructorVariant,
|
ValueConstructorVariant,
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
use std::rc::Rc;
|
use std::{collections::HashMap, rc::Rc};
|
||||||
|
|
||||||
use vec1::Vec1;
|
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,
|
||||||
|
@ -11,7 +14,7 @@ use crate::{
|
||||||
},
|
},
|
||||||
builtins::void,
|
builtins::void,
|
||||||
parser::token::Base,
|
parser::token::Base,
|
||||||
tipo::{ModuleValueConstructor, PatternConstructor, Type, ValueConstructor},
|
tipo::{ModuleValueConstructor, PatternConstructor, Type, TypeInfo, TypeVar, ValueConstructor},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
@ -573,6 +576,150 @@ pub const DEFAULT_TODO_STR: &str = "aiken::todo";
|
||||||
pub const DEFAULT_ERROR_STR: &str = "aiken::error";
|
pub const DEFAULT_ERROR_STR: &str = "aiken::error";
|
||||||
|
|
||||||
impl UntypedExpr {
|
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<String, TypeInfo>,
|
||||||
|
data: PlutusData,
|
||||||
|
tipo: &Type,
|
||||||
|
) -> Result<Self, String> {
|
||||||
|
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::<Result<Vec<_>, _>>()?,
|
||||||
|
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: "<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()
|
||||||
|
.zip(types)
|
||||||
|
.map(|(field, tipo)| {
|
||||||
|
UntypedExpr::reify(data_types, field, tipo).map(
|
||||||
|
|value| CallArg {
|
||||||
|
label: None,
|
||||||
|
location: Span::empty(),
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
}
|
||||||
|
}?;
|
||||||
|
|
||||||
|
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<Self>, location: Span) -> Self {
|
pub fn todo(reason: Option<Self>, location: Span) -> Self {
|
||||||
UntypedExpr::Trace {
|
UntypedExpr::Trace {
|
||||||
location,
|
location,
|
||||||
|
|
|
@ -19,8 +19,9 @@ use uplc::{
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ast::{
|
ast::{
|
||||||
AssignmentKind, BinOp, Bls12_381Point, Curve, Pattern, Span, TraceLevel, TypedArg,
|
AssignmentKind, BinOp, Bls12_381Point, Curve, DataTypeKey, FunctionAccessKey, Pattern,
|
||||||
TypedClause, TypedDataType, TypedFunction, TypedPattern, TypedValidator, UnOp,
|
Span, TraceLevel, TypedArg, TypedClause, TypedDataType, TypedFunction, TypedPattern,
|
||||||
|
TypedValidator, UnOp,
|
||||||
},
|
},
|
||||||
builtins::{bool, data, int, list, string, void},
|
builtins::{bool, data, int, list, string, void},
|
||||||
expr::TypedExpr,
|
expr::TypedExpr,
|
||||||
|
@ -48,7 +49,7 @@ use self::{
|
||||||
air_holds_msg, cast_validator_args, constants_ir, convert_type_to_data, extract_constant,
|
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,
|
lookup_data_type_by_tipo, modify_cyclic_calls, modify_self_calls, rearrange_list_clauses,
|
||||||
AssignmentProperties, ClauseProperties, CodeGenSpecialFuncs, CycleFunctionNames,
|
AssignmentProperties, ClauseProperties, CodeGenSpecialFuncs, CycleFunctionNames,
|
||||||
DataTypeKey, FunctionAccessKey, HoistableFunction, Variant,
|
HoistableFunction, Variant,
|
||||||
},
|
},
|
||||||
tree::{AirMsg, AirTree, TreePath},
|
tree::{AirMsg, AirTree, TreePath},
|
||||||
};
|
};
|
||||||
|
|
|
@ -15,8 +15,8 @@ use uplc::{
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ast::{
|
ast::{
|
||||||
AssignmentKind, DataType, Pattern, Span, TraceLevel, TypedArg, TypedClause,
|
AssignmentKind, DataType, DataTypeKey, FunctionAccessKey, Pattern, Span, TraceLevel,
|
||||||
TypedClauseGuard, TypedDataType, TypedPattern,
|
TypedArg, TypedClause, TypedClauseGuard, TypedDataType, TypedPattern,
|
||||||
},
|
},
|
||||||
builtins::{bool, data, function, int, list, string, void},
|
builtins::{bool, data, function, int, list, string, void},
|
||||||
expr::TypedExpr,
|
expr::TypedExpr,
|
||||||
|
@ -68,18 +68,6 @@ pub enum HoistableFunction {
|
||||||
CyclicLink(FunctionAccessKey),
|
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)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct AssignmentProperties {
|
pub struct AssignmentProperties {
|
||||||
pub value_type: Rc<Type>,
|
pub value_type: Rc<Type>,
|
||||||
|
|
|
@ -13,8 +13,8 @@ Test(
|
||||||
is_validator_param: false,
|
is_validator_param: false,
|
||||||
},
|
},
|
||||||
location: 9..16,
|
location: 9..16,
|
||||||
via: DefinitionIdentifier {
|
via: Var {
|
||||||
module: None,
|
location: 15..16,
|
||||||
name: "f",
|
name: "f",
|
||||||
},
|
},
|
||||||
tipo: (),
|
tipo: (),
|
||||||
|
@ -27,8 +27,8 @@ Test(
|
||||||
is_validator_param: false,
|
is_validator_param: false,
|
||||||
},
|
},
|
||||||
location: 18..25,
|
location: 18..25,
|
||||||
via: DefinitionIdentifier {
|
via: Var {
|
||||||
module: None,
|
location: 24..25,
|
||||||
name: "g",
|
name: "g",
|
||||||
},
|
},
|
||||||
tipo: (),
|
tipo: (),
|
||||||
|
@ -42,7 +42,14 @@ Test(
|
||||||
location: 0..26,
|
location: 0..26,
|
||||||
name: "foo",
|
name: "foo",
|
||||||
public: false,
|
public: false,
|
||||||
return_annotation: None,
|
return_annotation: Some(
|
||||||
|
Constructor {
|
||||||
|
location: 0..39,
|
||||||
|
module: None,
|
||||||
|
name: "Bool",
|
||||||
|
arguments: [],
|
||||||
|
},
|
||||||
|
),
|
||||||
return_type: (),
|
return_type: (),
|
||||||
end_position: 38,
|
end_position: 38,
|
||||||
can_error: false,
|
can_error: false,
|
||||||
|
|
|
@ -13,11 +13,13 @@ Test(
|
||||||
is_validator_param: false,
|
is_validator_param: false,
|
||||||
},
|
},
|
||||||
location: 9..27,
|
location: 9..27,
|
||||||
via: DefinitionIdentifier {
|
via: FieldAccess {
|
||||||
module: Some(
|
location: 15..27,
|
||||||
"fuzz",
|
label: "any_int",
|
||||||
),
|
container: Var {
|
||||||
name: "any_int",
|
location: 15..19,
|
||||||
|
name: "fuzz",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
tipo: (),
|
tipo: (),
|
||||||
},
|
},
|
||||||
|
@ -30,7 +32,14 @@ Test(
|
||||||
location: 0..28,
|
location: 0..28,
|
||||||
name: "foo",
|
name: "foo",
|
||||||
public: false,
|
public: false,
|
||||||
return_annotation: None,
|
return_annotation: Some(
|
||||||
|
Constructor {
|
||||||
|
location: 0..41,
|
||||||
|
module: None,
|
||||||
|
name: "Bool",
|
||||||
|
arguments: [],
|
||||||
|
},
|
||||||
|
),
|
||||||
return_type: (),
|
return_type: (),
|
||||||
end_position: 40,
|
end_position: 40,
|
||||||
can_error: false,
|
can_error: false,
|
||||||
|
|
|
@ -13,7 +13,14 @@ Test(
|
||||||
location: 0..10,
|
location: 0..10,
|
||||||
name: "foo",
|
name: "foo",
|
||||||
public: false,
|
public: false,
|
||||||
return_annotation: None,
|
return_annotation: Some(
|
||||||
|
Constructor {
|
||||||
|
location: 0..23,
|
||||||
|
module: None,
|
||||||
|
name: "Bool",
|
||||||
|
arguments: [],
|
||||||
|
},
|
||||||
|
),
|
||||||
return_type: (),
|
return_type: (),
|
||||||
end_position: 22,
|
end_position: 22,
|
||||||
can_error: false,
|
can_error: false,
|
||||||
|
|
|
@ -37,7 +37,14 @@ Test(
|
||||||
location: 0..26,
|
location: 0..26,
|
||||||
name: "invalid_inputs",
|
name: "invalid_inputs",
|
||||||
public: false,
|
public: false,
|
||||||
return_annotation: None,
|
return_annotation: Some(
|
||||||
|
Constructor {
|
||||||
|
location: 0..61,
|
||||||
|
module: None,
|
||||||
|
name: "Bool",
|
||||||
|
arguments: [],
|
||||||
|
},
|
||||||
|
),
|
||||||
return_type: (),
|
return_type: (),
|
||||||
end_position: 60,
|
end_position: 60,
|
||||||
can_error: true,
|
can_error: true,
|
||||||
|
|
|
@ -187,6 +187,11 @@ pub fn parser(
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::assert_expr;
|
use crate::assert_expr;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn record_enum() {
|
||||||
|
assert_expr!(r#"Winter"#);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn record_create_labeled() {
|
fn record_create_labeled() {
|
||||||
assert_expr!(r#"User { name: "Aiken", age, thing: 2 }"#);
|
assert_expr!(r#"User { name: "Aiken", age, thing: 2 }"#);
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
source: crates/aiken-lang/src/parser/expr/record.rs
|
||||||
|
description: "Code:\n\nWinter"
|
||||||
|
---
|
||||||
|
Var {
|
||||||
|
location: 0..6,
|
||||||
|
name: "Winter",
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{collections::HashMap, ops::Deref, rc::Rc};
|
use std::{borrow::Borrow, collections::HashMap, ops::Deref, rc::Rc};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ast::{
|
ast::{
|
||||||
|
@ -332,78 +332,6 @@ fn infer_definition(
|
||||||
}
|
}
|
||||||
|
|
||||||
Definition::Test(f) => {
|
Definition::Test(f) => {
|
||||||
fn annotate_fuzzer(tipo: &Type, location: &Span) -> Result<Annotation, Error> {
|
|
||||||
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<a>"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Type::Var { .. } | Type::App { .. } | Type::Tuple { .. } => {
|
|
||||||
todo!("Fuzzer type isn't a function?");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn tipo_to_annotation(tipo: &Type, location: &Span) -> Result<Annotation, Error> {
|
|
||||||
match tipo {
|
|
||||||
Type::App {
|
|
||||||
name, module, args, ..
|
|
||||||
} => {
|
|
||||||
let arguments = args
|
|
||||||
.iter()
|
|
||||||
.map(|arg| tipo_to_annotation(arg, location))
|
|
||||||
.collect::<Result<Vec<Annotation>, _>>()?;
|
|
||||||
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::<Result<Vec<Annotation>, _>>()?;
|
|
||||||
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() {
|
let (typed_via, annotation) = match f.arguments.first() {
|
||||||
Some(arg) => {
|
Some(arg) => {
|
||||||
if f.arguments.len() > 1 {
|
if f.arguments.len() > 1 {
|
||||||
|
@ -416,12 +344,9 @@ fn infer_definition(
|
||||||
let typed_via =
|
let typed_via =
|
||||||
ExprTyper::new(environment, lines, tracing).infer(arg.via.clone())?;
|
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((
|
Ok((Some((typed_via, inner_type)), Some(annotation)))
|
||||||
Some(typed_via),
|
|
||||||
Some(annotate_fuzzer(&tipo, &arg.location)?),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
None => Ok((None, None)),
|
None => Ok((None, None)),
|
||||||
}?;
|
}?;
|
||||||
|
@ -466,17 +391,15 @@ fn infer_definition(
|
||||||
name: typed_f.name,
|
name: typed_f.name,
|
||||||
public: typed_f.public,
|
public: typed_f.public,
|
||||||
arguments: match typed_via {
|
arguments: match typed_via {
|
||||||
Some(via) => {
|
Some((via, tipo)) => {
|
||||||
let Arg {
|
let Arg {
|
||||||
arg_name,
|
arg_name, location, ..
|
||||||
location,
|
|
||||||
tipo,
|
|
||||||
..
|
|
||||||
} = typed_f
|
} = typed_f
|
||||||
.arguments
|
.arguments
|
||||||
.first()
|
.first()
|
||||||
.expect("has exactly one argument")
|
.expect("has exactly one argument")
|
||||||
.to_owned();
|
.to_owned();
|
||||||
|
|
||||||
vec![ArgVia {
|
vec![ArgVia {
|
||||||
arg_name,
|
arg_name,
|
||||||
location,
|
location,
|
||||||
|
@ -805,3 +728,67 @@ fn infer_function(
|
||||||
end_position,
|
end_position,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn infer_fuzzer(tipo: &Type, location: &Span) -> Result<(Annotation, Rc<Type>), 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<a>"),
|
||||||
|
},
|
||||||
|
|
||||||
|
Type::Var { .. } | Type::App { .. } | Type::Tuple { .. } => {
|
||||||
|
todo!("Fuzzer type isn't a function?");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn annotation_from_type(tipo: &Type, location: &Span) -> Result<Annotation, Error> {
|
||||||
|
match tipo {
|
||||||
|
Type::App {
|
||||||
|
name, module, args, ..
|
||||||
|
} => {
|
||||||
|
let arguments = args
|
||||||
|
.iter()
|
||||||
|
.map(|arg| annotation_from_type(arg, location))
|
||||||
|
.collect::<Result<Vec<Annotation>, _>>()?;
|
||||||
|
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::<Result<Vec<Annotation>, _>>()?;
|
||||||
|
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:#?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -29,11 +29,11 @@ use crate::{
|
||||||
};
|
};
|
||||||
use aiken_lang::{
|
use aiken_lang::{
|
||||||
ast::{
|
ast::{
|
||||||
Definition, Function, ModuleKind, Span, Tracing, TypedDataType, TypedFunction, Validator,
|
DataTypeKey, Definition, Function, FunctionAccessKey, ModuleKind, Span, Tracing,
|
||||||
|
TypedDataType, TypedFunction, Validator,
|
||||||
},
|
},
|
||||||
builtins,
|
builtins,
|
||||||
expr::TypedExpr,
|
expr::{TypedExpr, UntypedExpr},
|
||||||
gen_uplc::builder::{DataTypeKey, FunctionAccessKey},
|
|
||||||
tipo::{Type, TypeInfo},
|
tipo::{Type, TypeInfo},
|
||||||
IdGenerator,
|
IdGenerator,
|
||||||
};
|
};
|
||||||
|
@ -321,9 +321,9 @@ where
|
||||||
self.event_listener.handle_event(Event::RunningTests);
|
self.event_listener.handle_event(Event::RunningTests);
|
||||||
}
|
}
|
||||||
|
|
||||||
let results = self.run_tests(tests);
|
let tests = self.run_tests(tests);
|
||||||
|
|
||||||
let errors: Vec<Error> = results
|
let errors: Vec<Error> = tests
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|e| {
|
.filter_map(|e| {
|
||||||
if e.is_success() {
|
if e.is_success() {
|
||||||
|
@ -335,7 +335,7 @@ where
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
self.event_listener
|
self.event_listener
|
||||||
.handle_event(Event::FinishedTests { tests: results });
|
.handle_event(Event::FinishedTests { tests });
|
||||||
|
|
||||||
if !errors.is_empty() {
|
if !errors.is_empty() {
|
||||||
Err(errors)
|
Err(errors)
|
||||||
|
@ -886,7 +886,7 @@ where
|
||||||
Ok(programs)
|
Ok(programs)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_tests(&self, tests: Vec<Test>) -> Vec<TestResult> {
|
fn run_tests(&self, tests: Vec<Test>) -> Vec<TestResult<UntypedExpr>> {
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
|
|
||||||
tests
|
tests
|
||||||
|
@ -897,6 +897,9 @@ where
|
||||||
// provided.
|
// provided.
|
||||||
Test::PropertyTest(property_test) => property_test.run(42),
|
Test::PropertyTest(property_test) => property_test.run(42),
|
||||||
})
|
})
|
||||||
|
.collect::<Vec<TestResult<PlutusData>>>()
|
||||||
|
.into_iter()
|
||||||
|
.map(|test| test.reify(&self.module_types))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use aiken_lang::{
|
use aiken_lang::{
|
||||||
ast::{
|
ast::{
|
||||||
DataType, Definition, Function, Located, ModuleKind, Tracing, TypedDataType, TypedFunction,
|
DataType, DataTypeKey, Definition, Function, FunctionAccessKey, Located, ModuleKind,
|
||||||
TypedModule, TypedValidator, UntypedModule, Validator,
|
Tracing, TypedDataType, TypedFunction, TypedModule, TypedValidator, UntypedModule,
|
||||||
},
|
Validator,
|
||||||
gen_uplc::{
|
|
||||||
builder::{DataTypeKey, FunctionAccessKey},
|
|
||||||
CodeGenerator,
|
|
||||||
},
|
},
|
||||||
|
gen_uplc::CodeGenerator,
|
||||||
line_numbers::LineNumbers,
|
line_numbers::LineNumbers,
|
||||||
parser::extra::{comments_before, Comment, ModuleExtra},
|
parser::extra::{comments_before, Comment, ModuleExtra},
|
||||||
tipo::TypeInfo,
|
tipo::TypeInfo,
|
||||||
|
|
|
@ -1,10 +1,17 @@
|
||||||
use crate::{pretty, ExBudget};
|
use crate::{pretty, ExBudget};
|
||||||
use aiken_lang::gen_uplc::builder::convert_data_to_type;
|
use aiken_lang::{
|
||||||
use aiken_lang::{ast::BinOp, tipo::Type};
|
ast::BinOp,
|
||||||
use pallas::codec::utils::Int;
|
expr::UntypedExpr,
|
||||||
use pallas::ledger::primitives::alonzo::{BigInt, Constr, PlutusData};
|
gen_uplc::builder::convert_data_to_type,
|
||||||
|
tipo::{Type, TypeInfo},
|
||||||
|
};
|
||||||
|
use pallas::{
|
||||||
|
codec::utils::Int,
|
||||||
|
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,
|
||||||
|
@ -14,26 +21,25 @@ use uplc::{
|
||||||
machine::eval_result::EvalResult,
|
machine::eval_result::EvalResult,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
/// ----------------------------------------------------------------------------
|
||||||
//
|
///
|
||||||
// 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.
|
||||||
//
|
///
|
||||||
// A property on the other-hand is a template for generating tests, which is also
|
/// 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
|
/// 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)
|
/// 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
|
/// sequence. On failures, the value that caused a failure is simplified using an
|
||||||
// approach similar to what's described in MiniThesis<https://github.com/DRMacIver/minithesis>,
|
/// approach similar to what's described in MiniThesis<https://github.com/DRMacIver/minithesis>,
|
||||||
// which is a simplified version of Hypothesis, a property-based testing framework
|
/// which is a simplified version of Hypothesis, a property-based testing framework
|
||||||
// with integrated shrinking.
|
/// with integrated shrinking.
|
||||||
//
|
///
|
||||||
// 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),
|
||||||
|
@ -99,7 +105,7 @@ pub struct UnitTest {
|
||||||
unsafe impl Send for UnitTest {}
|
unsafe impl Send for UnitTest {}
|
||||||
|
|
||||||
impl UnitTest {
|
impl UnitTest {
|
||||||
pub fn run(self) -> TestResult {
|
pub fn run<T>(self) -> TestResult<T> {
|
||||||
let mut eval_result = self.program.clone().eval(ExBudget::max());
|
let mut eval_result = self.program.clone().eval(ExBudget::max());
|
||||||
TestResult::UnitTestResult(UnitTestResult {
|
TestResult::UnitTestResult(UnitTestResult {
|
||||||
test: self.to_owned(),
|
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
|
/// 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.
|
/// 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<PlutusData> {
|
||||||
let n = PropertyTest::MAX_TEST_RUN;
|
let n = PropertyTest::MAX_TEST_RUN;
|
||||||
|
|
||||||
let (counterexample, iterations) = match self.run_n_times(n, seed, None) {
|
let (counterexample, iterations) = match self.run_n_times(n, seed, None) {
|
||||||
|
@ -153,8 +159,8 @@ impl PropertyTest {
|
||||||
&self,
|
&self,
|
||||||
remaining: usize,
|
remaining: usize,
|
||||||
seed: u32,
|
seed: u32,
|
||||||
counterexample: Option<(usize, Term<NamedDeBruijn>)>,
|
counterexample: Option<(usize, PlutusData)>,
|
||||||
) -> Option<(usize, Term<NamedDeBruijn>)> {
|
) -> Option<(usize, PlutusData)> {
|
||||||
// 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() {
|
||||||
|
@ -169,12 +175,12 @@ impl PropertyTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_once(&self, seed: u32) -> (u32, Option<Term<NamedDeBruijn>>) {
|
fn run_once(&self, seed: u32) -> (u32, Option<PlutusData>) {
|
||||||
let (next_prng, value) = Prng::from_seed(seed)
|
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.");
|
.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 {
|
if let Prng::Seeded {
|
||||||
seed: next_seed, ..
|
seed: next_seed, ..
|
||||||
|
@ -182,12 +188,9 @@ impl PropertyTest {
|
||||||
{
|
{
|
||||||
if result.failed(self.can_error) {
|
if result.failed(self.can_error) {
|
||||||
let mut counterexample = Counterexample {
|
let mut counterexample = Counterexample {
|
||||||
result,
|
|
||||||
value,
|
value,
|
||||||
choices: next_prng.choices(),
|
choices: next_prng.choices(),
|
||||||
can_error: self.can_error,
|
property: self,
|
||||||
program: &self.program,
|
|
||||||
fuzzer: (&self.fuzzer.0, &self.fuzzer.1),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if !counterexample.choices.is_empty() {
|
if !counterexample.choices.is_empty() {
|
||||||
|
@ -202,6 +205,13 @@ impl PropertyTest {
|
||||||
unreachable!("Prng constructed from a seed necessarily yield a seed.");
|
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;
|
const REPLAYED: u64 = 1;
|
||||||
|
|
||||||
/// Constructor tag for Option's 'Some'
|
/// Constructor tag for Option's 'Some'
|
||||||
const OK: u64 = 0;
|
const SOME: u64 = 0;
|
||||||
/// Constructor tag for Option's 'None'
|
/// Constructor tag for Option's 'None'
|
||||||
const ERR: u64 = 1;
|
const NONE: u64 = 1;
|
||||||
|
|
||||||
pub fn uplc(&self) -> PlutusData {
|
pub fn uplc(&self) -> PlutusData {
|
||||||
match self {
|
match self {
|
||||||
|
@ -281,18 +291,14 @@ 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(
|
pub fn sample(&self, fuzzer: &Program<NamedDeBruijn>) -> Option<(Prng, PlutusData)> {
|
||||||
&self,
|
|
||||||
fuzzer: &Program<NamedDeBruijn>,
|
|
||||||
return_type: &Type,
|
|
||||||
) -> Option<(Prng, Term<NamedDeBruijn>)> {
|
|
||||||
let result = fuzzer
|
let result = fuzzer
|
||||||
.apply_data(self.uplc())
|
.apply_data(self.uplc())
|
||||||
.eval(ExBudget::max())
|
.eval(ExBudget::max())
|
||||||
.result()
|
.result()
|
||||||
.expect("Fuzzer crashed?");
|
.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
|
/// 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
|
/// 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
|
/// values to replay). In such case, the replayed sequence is simply invalid and the fuzzer
|
||||||
/// aborted altogether with 'None'.
|
/// aborted altogether with 'None'.
|
||||||
pub fn from_result(
|
pub fn from_result(result: Term<NamedDeBruijn>) -> Option<(Self, PlutusData)> {
|
||||||
result: Term<NamedDeBruijn>,
|
|
||||||
type_info: &Type,
|
|
||||||
) -> Option<(Self, Term<NamedDeBruijn>)> {
|
|
||||||
/// Interpret the given 'PlutusData' as one of two Prng constructors.
|
/// Interpret the given 'PlutusData' as one of two Prng constructors.
|
||||||
fn as_prng(cst: &PlutusData) -> Prng {
|
fn as_prng(cst: &PlutusData) -> Prng {
|
||||||
if let PlutusData::Constr(Constr { tag, fields, .. }) = cst {
|
if let PlutusData::Constr(Constr { tag, fields, .. }) = cst {
|
||||||
|
@ -345,15 +348,10 @@ impl Prng {
|
||||||
|
|
||||||
if let Term::Constant(rc) = &result {
|
if let Term::Constant(rc) = &result {
|
||||||
if let Constant::Data(PlutusData::Constr(Constr { tag, fields, .. })) = &rc.borrow() {
|
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 [PlutusData::Array(elems)] = &fields[..] {
|
||||||
if let [new_seed, value] = &elems[..] {
|
if let [new_seed, value] = &elems[..] {
|
||||||
return Some((
|
return Some((as_prng(new_seed), value.clone()));
|
||||||
as_prng(new_seed),
|
|
||||||
convert_data_to_type(Term::data(value.clone()), type_info)
|
|
||||||
.try_into()
|
|
||||||
.expect("safe conversion from Name -> NamedDeBruijn"),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -362,7 +360,7 @@ impl Prng {
|
||||||
// choices. If we run out of choices, or a choice end up being
|
// 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
|
// invalid as per the expectation, the fuzzer can't go further and
|
||||||
// fail.
|
// fail.
|
||||||
if *tag == 121 + Prng::ERR {
|
if *tag == 121 + Prng::NONE {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -385,12 +383,9 @@ impl Prng {
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Counterexample<'a> {
|
pub struct Counterexample<'a> {
|
||||||
pub value: Term<NamedDeBruijn>,
|
pub value: PlutusData,
|
||||||
pub choices: Vec<u32>,
|
pub choices: Vec<u32>,
|
||||||
pub result: EvalResult,
|
pub property: &'a PropertyTest,
|
||||||
pub can_error: bool,
|
|
||||||
pub program: &'a Program<NamedDeBruijn>,
|
|
||||||
pub fuzzer: (&'a Program<NamedDeBruijn>, &'a Type),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Counterexample<'a> {
|
impl<'a> Counterexample<'a> {
|
||||||
|
@ -404,17 +399,17 @@ impl<'a> Counterexample<'a> {
|
||||||
// test cases many times. Given that tests are fully deterministic, we can
|
// test cases many times. Given that tests are fully deterministic, we can
|
||||||
// memoize the already seen choices to avoid re-running the generators and
|
// memoize the already seen choices to avoid re-running the generators and
|
||||||
// the test (which can be quite expensive).
|
// 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.
|
// Shrinked choices led to an impossible generation.
|
||||||
None => false,
|
None => false,
|
||||||
|
|
||||||
// Shrinked choices let to a new valid generated value, now, is it better?
|
// Shrinked choices let to a new valid generated value, now, is it better?
|
||||||
Some((_, value)) => {
|
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
|
// If the test no longer fails, it isn't better as we're only
|
||||||
// interested in counterexamples.
|
// interested in counterexamples.
|
||||||
if !result.failed(self.can_error) {
|
if !result.failed(self.property.can_error) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -546,14 +541,25 @@ impl<'a> Counterexample<'a> {
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum TestResult {
|
pub enum TestResult<T> {
|
||||||
UnitTestResult(UnitTestResult),
|
UnitTestResult(UnitTestResult),
|
||||||
PropertyTestResult(PropertyTestResult),
|
PropertyTestResult(PropertyTestResult<T>),
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe impl Send for TestResult {}
|
unsafe impl<T> Send for TestResult<T> {}
|
||||||
|
|
||||||
impl TestResult {
|
impl TestResult<PlutusData> {
|
||||||
|
pub fn reify(self, data_types: &HashMap<String, TypeInfo>) -> TestResult<UntypedExpr> {
|
||||||
|
match self {
|
||||||
|
TestResult::UnitTestResult(test) => TestResult::UnitTestResult(test),
|
||||||
|
TestResult::PropertyTestResult(test) => {
|
||||||
|
TestResult::PropertyTestResult(test.reify(data_types))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> TestResult<T> {
|
||||||
pub fn is_success(&self) -> bool {
|
pub fn is_success(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
TestResult::UnitTestResult(UnitTestResult { success, .. }) => *success,
|
TestResult::UnitTestResult(UnitTestResult { success, .. }) => *success,
|
||||||
|
@ -633,13 +639,29 @@ pub struct UnitTestResult {
|
||||||
unsafe impl Send for UnitTestResult {}
|
unsafe impl Send for UnitTestResult {}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct PropertyTestResult {
|
pub struct PropertyTestResult<T> {
|
||||||
pub test: PropertyTest,
|
pub test: PropertyTest,
|
||||||
pub counterexample: Option<Term<NamedDeBruijn>>,
|
pub counterexample: Option<T>,
|
||||||
pub iterations: usize,
|
pub iterations: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe impl Send for PropertyTestResult {}
|
unsafe impl<T> Send for PropertyTestResult<T> {}
|
||||||
|
|
||||||
|
impl PropertyTestResult<PlutusData> {
|
||||||
|
pub fn reify(self, data_types: &HashMap<String, TypeInfo>) -> PropertyTestResult<UntypedExpr> {
|
||||||
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Assertion {
|
pub struct Assertion {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::pretty;
|
use crate::pretty;
|
||||||
use crate::script::{PropertyTestResult, TestResult, UnitTestResult};
|
use crate::script::{PropertyTestResult, TestResult, UnitTestResult};
|
||||||
|
use aiken_lang::{expr::UntypedExpr, format::Formatter};
|
||||||
use owo_colors::{OwoColorize, Stream::Stderr};
|
use owo_colors::{OwoColorize, Stream::Stderr};
|
||||||
use std::{collections::BTreeMap, fmt::Display, path::PathBuf};
|
use std::{collections::BTreeMap, fmt::Display, path::PathBuf};
|
||||||
use uplc::machine::cost_model::ExBudget;
|
use uplc::machine::cost_model::ExBudget;
|
||||||
|
@ -34,7 +35,7 @@ pub enum Event {
|
||||||
},
|
},
|
||||||
RunningTests,
|
RunningTests,
|
||||||
FinishedTests {
|
FinishedTests {
|
||||||
tests: Vec<TestResult>,
|
tests: Vec<TestResult<UntypedExpr>>,
|
||||||
},
|
},
|
||||||
WaitingForBuildDirLock,
|
WaitingForBuildDirLock,
|
||||||
ResolvingPackages {
|
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<UntypedExpr>,
|
||||||
|
max_mem: usize,
|
||||||
|
max_cpu: usize,
|
||||||
|
styled: bool,
|
||||||
|
) -> String {
|
||||||
// Status
|
// Status
|
||||||
let mut test = if result.is_success() {
|
let mut test = if result.is_success() {
|
||||||
pretty::style_if(styled, "PASS".to_string(), |s| {
|
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.red())
|
||||||
.if_supports_color(Stderr, |s| s.bold())
|
.if_supports_color(Stderr, |s| s.bold())
|
||||||
.to_string()),
|
.to_string()),
|
||||||
&counterexample.to_pretty(),
|
&Formatter::new()
|
||||||
|
.expr(counterexample, false)
|
||||||
|
.to_pretty_string(70),
|
||||||
|s| s.red().to_string()
|
|s| s.red().to_string()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -351,7 +359,7 @@ fn fmt_test(result: &TestResult, max_mem: usize, max_cpu: usize, styled: bool) -
|
||||||
test
|
test
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fmt_test_summary(tests: &[&TestResult], styled: bool) -> String {
|
fn fmt_test_summary<T>(tests: &[&TestResult<T>], styled: bool) -> String {
|
||||||
let (n_passed, n_failed) = tests.iter().fold((0, 0), |(n_passed, n_failed), result| {
|
let (n_passed, n_failed) = tests.iter().fold((0, 0), |(n_passed, n_failed), result| {
|
||||||
if result.is_success() {
|
if result.is_success() {
|
||||||
(n_passed + 1, n_failed)
|
(n_passed + 1, n_failed)
|
||||||
|
@ -375,16 +383,16 @@ fn fmt_test_summary(tests: &[&TestResult], styled: bool) -> String {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn group_by_module(results: &Vec<TestResult>) -> BTreeMap<String, Vec<&TestResult>> {
|
fn group_by_module<T>(results: &Vec<TestResult<T>>) -> BTreeMap<String, Vec<&TestResult<T>>> {
|
||||||
let mut modules = BTreeMap::new();
|
let mut modules = BTreeMap::new();
|
||||||
for r in results {
|
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);
|
xs.push(r);
|
||||||
}
|
}
|
||||||
modules
|
modules
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_max_execution_units(xs: &[TestResult]) -> (usize, usize) {
|
fn find_max_execution_units<T>(xs: &[TestResult<T>]) -> (usize, usize) {
|
||||||
let (max_mem, max_cpu) = xs
|
let (max_mem, max_cpu) = xs
|
||||||
.iter()
|
.iter()
|
||||||
.fold((0, 0), |(max_mem, max_cpu), test| match test {
|
.fold((0, 0), |(max_mem, max_cpu), test| match test {
|
||||||
|
|
|
@ -2,8 +2,10 @@ use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use aiken_lang::{
|
use aiken_lang::{
|
||||||
ast::{ModuleKind, TraceLevel, Tracing, TypedDataType, TypedFunction},
|
ast::{
|
||||||
gen_uplc::builder::{DataTypeKey, FunctionAccessKey},
|
DataTypeKey, FunctionAccessKey, ModuleKind, TraceLevel, Tracing, TypedDataType,
|
||||||
|
TypedFunction,
|
||||||
|
},
|
||||||
parser,
|
parser,
|
||||||
tipo::TypeInfo,
|
tipo::TypeInfo,
|
||||||
IdGenerator,
|
IdGenerator,
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
use std::{collections::VecDeque, mem::size_of, ops::Deref, rc::Rc};
|
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::{
|
use crate::{
|
||||||
ast::{Constant, NamedDeBruijn, Term, Type},
|
ast::{Constant, NamedDeBruijn, Term, Type},
|
||||||
builtins::DefaultFunction,
|
builtins::DefaultFunction,
|
||||||
};
|
};
|
||||||
|
use num_bigint::BigInt;
|
||||||
|
use num_traits::{Signed, ToPrimitive, Zero};
|
||||||
|
use pallas::ledger::primitives::babbage::{self, PlutusData};
|
||||||
|
|
||||||
use super::{runtime::BuiltinRuntime, Error};
|
use super::{runtime::BuiltinRuntime, Error};
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,16 @@ use aiken/builtin
|
||||||
|
|
||||||
const max_int: Int = 255
|
const max_int: Int = 255
|
||||||
|
|
||||||
type PRNG {
|
pub type PRNG {
|
||||||
Seeded { seed: Int, choices: List<Int> }
|
Seeded { seed: Int, choices: List<Int> }
|
||||||
Replayed { choices: List<Int> }
|
Replayed { choices: List<Int> }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Fuzzer<a> =
|
||||||
|
fn(PRNG) -> Option<(PRNG, a)>
|
||||||
|
|
||||||
|
// Primitives
|
||||||
|
|
||||||
fn any_int(prng: PRNG) -> Option<(PRNG, Int)> {
|
fn any_int(prng: PRNG) -> Option<(PRNG, Int)> {
|
||||||
when prng is {
|
when prng is {
|
||||||
Seeded { seed, choices } -> {
|
Seeded { seed, choices } -> {
|
||||||
|
@ -40,10 +45,94 @@ fn any_int(prng: PRNG) -> Option<(PRNG, Int)> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test prop_foo_1(n via any_int) {
|
pub fn constant(a: a) -> Fuzzer<a> {
|
||||||
n >= 0 && n <= 255
|
fn(s0) { Some((s0, a)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
test prop_foo_2(n via any_int) fail {
|
pub fn and_then(fuzz_a: Fuzzer<a>, f: fn(a) -> Fuzzer<b>) -> Fuzzer<b> {
|
||||||
n < 100
|
fn(s0) {
|
||||||
|
when fuzz_a(s0) is {
|
||||||
|
Some((s1, a)) -> f(a)(s1)
|
||||||
|
None -> None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn map(fuzz_a: Fuzzer<a>, f: fn(a) -> b) -> Fuzzer<b> {
|
||||||
|
fn(s0) {
|
||||||
|
when fuzz_a(s0) is {
|
||||||
|
Some((s1, a)) -> Some((s1, f(a)))
|
||||||
|
None -> None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builders
|
||||||
|
|
||||||
|
fn any_bool() -> Fuzzer<Bool> {
|
||||||
|
any_int |> map(fn(n) { n <= 127 })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn any_list(fuzz_a: Fuzzer<a>) -> Fuzzer<List<a>> {
|
||||||
|
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<Season> {
|
||||||
|
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<a>) -> Int {
|
||||||
|
when xs is {
|
||||||
|
[] -> 0
|
||||||
|
[_, ..tail] -> 1 + length(tail)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue