Yield proper user-facing error when inferring Fuzzer usage
This commit is contained in:
parent
cf61387a41
commit
bbc9fc5762
|
@ -69,9 +69,10 @@ pub fn via() -> impl Parser<Token, ast::UntypedArgVia, Error = ParseError> {
|
||||||
}),
|
}),
|
||||||
))
|
))
|
||||||
.then(just(Token::Colon).ignore_then(annotation()).or_not())
|
.then(just(Token::Colon).ignore_then(annotation()).or_not())
|
||||||
|
.map_with_span(|(arg_name, annotation), location| (arg_name, annotation, location))
|
||||||
.then_ignore(just(Token::Via))
|
.then_ignore(just(Token::Via))
|
||||||
.then(fuzzer())
|
.then(fuzzer())
|
||||||
.map_with_span(|((arg_name, annotation), via), location| ast::ArgVia {
|
.map(|((arg_name, annotation, location), via)| ast::ArgVia {
|
||||||
arg_name,
|
arg_name,
|
||||||
via,
|
via,
|
||||||
annotation,
|
annotation,
|
||||||
|
|
|
@ -1202,6 +1202,113 @@ fn pipe_with_wrong_type_and_full_args() {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fuzzer_ok_basic() {
|
||||||
|
let source_code = r#"
|
||||||
|
fn int() -> Fuzzer<Int> { todo }
|
||||||
|
|
||||||
|
test prop(n via int()) { todo }
|
||||||
|
"#;
|
||||||
|
|
||||||
|
assert!(check(parse(source_code)).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fuzzer_ok_explicit() {
|
||||||
|
let source_code = r#"
|
||||||
|
fn int(prng: PRNG) -> Option<(PRNG, Int)> { todo }
|
||||||
|
|
||||||
|
test prop(n via int) { todo }
|
||||||
|
"#;
|
||||||
|
|
||||||
|
assert!(check(parse(source_code)).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fuzzer_ok_list() {
|
||||||
|
let source_code = r#"
|
||||||
|
fn int() -> Fuzzer<Int> { todo }
|
||||||
|
fn list(a: Fuzzer<a>) -> Fuzzer<List<a>> { todo }
|
||||||
|
|
||||||
|
test prop(xs via list(int())) { todo }
|
||||||
|
"#;
|
||||||
|
|
||||||
|
assert!(check(parse(source_code)).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fuzzer_err_unbound() {
|
||||||
|
let source_code = r#"
|
||||||
|
fn any() -> Fuzzer<a> { todo }
|
||||||
|
fn list(a: Fuzzer<a>) -> Fuzzer<List<a>> { todo }
|
||||||
|
|
||||||
|
test prop(xs via list(any())) { todo }
|
||||||
|
"#;
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
check(parse(source_code)),
|
||||||
|
Err((_, Error::GenericLeftAtBoundary { .. }))
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fuzzer_err_unify_1() {
|
||||||
|
let source_code = r#"
|
||||||
|
test prop(xs via Void) { todo }
|
||||||
|
"#;
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
check(parse(source_code)),
|
||||||
|
Err((
|
||||||
|
_,
|
||||||
|
Error::CouldNotUnify {
|
||||||
|
situation: None,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
))
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fuzzer_err_unify_2() {
|
||||||
|
let source_code = r#"
|
||||||
|
fn any() -> Fuzzer<a> { todo }
|
||||||
|
test prop(xs via any) { todo }
|
||||||
|
"#;
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
check(parse(source_code)),
|
||||||
|
Err((
|
||||||
|
_,
|
||||||
|
Error::CouldNotUnify {
|
||||||
|
situation: None,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
))
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fuzzer_err_unify_3() {
|
||||||
|
let source_code = r#"
|
||||||
|
fn list(a: Fuzzer<a>) -> Fuzzer<List<a>> { todo }
|
||||||
|
fn int() -> Fuzzer<Int> { todo }
|
||||||
|
|
||||||
|
test prop(xs: Int via list(int())) { todo }
|
||||||
|
"#;
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
check(parse(source_code)),
|
||||||
|
Err((
|
||||||
|
_,
|
||||||
|
Error::CouldNotUnify {
|
||||||
|
situation: Some(UnifyErrorSituation::FuzzerAnnotationMismatch),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
))
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn utf8_hex_literal_warning() {
|
fn utf8_hex_literal_warning() {
|
||||||
let source_code = r#"
|
let source_code = r#"
|
||||||
|
|
|
@ -270,7 +270,7 @@ You can use '{discard}' and numbers to distinguish between similar names.
|
||||||
#[error("I found a type definition that has an unsupported type in it.\n")]
|
#[error("I found a type definition that has an unsupported type in it.\n")]
|
||||||
#[diagnostic(code("illegal::type_in_data"))]
|
#[diagnostic(code("illegal::type_in_data"))]
|
||||||
#[diagnostic(help(
|
#[diagnostic(help(
|
||||||
r#"Data-types can't contain type {type_info} because it can't be represented as PlutusData."#,
|
r#"Data-types can't contain type {type_info} because it isn't serializable into a Plutus Data. Yet, this is a strong requirement for types found in compound structures such as List or Tuples."#,
|
||||||
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())
|
||||||
))]
|
))]
|
||||||
IllegalTypeInData {
|
IllegalTypeInData {
|
||||||
|
@ -957,6 +957,15 @@ The best thing to do from here is to remove it."#))]
|
||||||
#[label("too many arguments")]
|
#[label("too many arguments")]
|
||||||
location: Span,
|
location: Span,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#[error("I choked on a generic type left in an outward-facing interface.\n")]
|
||||||
|
#[diagnostic(code("illegal::generic_in_abi"))]
|
||||||
|
#[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."))]
|
||||||
|
GenericLeftAtBoundary {
|
||||||
|
#[label("unbound generic at boundary")]
|
||||||
|
location: Span,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExtraData for Error {
|
impl ExtraData for Error {
|
||||||
|
@ -1009,6 +1018,7 @@ impl ExtraData for Error {
|
||||||
| Error::UpdateMultiConstructorType { .. }
|
| Error::UpdateMultiConstructorType { .. }
|
||||||
| Error::ValidatorImported { .. }
|
| Error::ValidatorImported { .. }
|
||||||
| Error::IncorrectTestArity { .. }
|
| Error::IncorrectTestArity { .. }
|
||||||
|
| Error::GenericLeftAtBoundary { .. }
|
||||||
| Error::ValidatorMustReturnBool { .. } => None,
|
| Error::ValidatorMustReturnBool { .. } => None,
|
||||||
|
|
||||||
Error::UnknownType { name, .. }
|
Error::UnknownType { name, .. }
|
||||||
|
@ -1206,14 +1216,14 @@ fn suggest_unify(
|
||||||
|
|
||||||
(
|
(
|
||||||
format!(
|
format!(
|
||||||
"{} - {}",
|
"{}.{{{}}}",
|
||||||
|
expected_module.if_supports_color(Stdout, |s| s.bright_blue()),
|
||||||
expected_str.if_supports_color(Stdout, |s| s.green()),
|
expected_str.if_supports_color(Stdout, |s| s.green()),
|
||||||
expected_module.if_supports_color(Stdout, |s| s.bright_blue())
|
|
||||||
),
|
),
|
||||||
format!(
|
format!(
|
||||||
"{} - {}",
|
"{}.{{{}}}",
|
||||||
|
given_module.if_supports_color(Stdout, |s| s.bright_blue()),
|
||||||
given_str.if_supports_color(Stdout, |s| s.red()),
|
given_str.if_supports_color(Stdout, |s| s.red()),
|
||||||
given_module.if_supports_color(Stdout, |s| s.bright_blue())
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1288,6 +1298,21 @@ fn suggest_unify(
|
||||||
expected,
|
expected,
|
||||||
given
|
given
|
||||||
},
|
},
|
||||||
|
Some(UnifyErrorSituation::FuzzerAnnotationMismatch) => formatdoc! {
|
||||||
|
r#"While comparing the return annotation of a Fuzzer with its actual return type, I realized that both don't match.
|
||||||
|
|
||||||
|
I am inferring the Fuzzer should return:
|
||||||
|
|
||||||
|
{}
|
||||||
|
|
||||||
|
but I found a conflicting annotation saying it returns:
|
||||||
|
|
||||||
|
{}
|
||||||
|
|
||||||
|
Either, fix (or remove) the annotation or adjust the Fuzzer to return the expected type."#,
|
||||||
|
expected,
|
||||||
|
given
|
||||||
|
},
|
||||||
None => formatdoc! {
|
None => formatdoc! {
|
||||||
r#"I am inferring the following type:
|
r#"I am inferring the following type:
|
||||||
|
|
||||||
|
@ -1677,6 +1702,8 @@ pub enum UnifyErrorSituation {
|
||||||
|
|
||||||
/// The operands of a binary operator were incorrect.
|
/// The operands of a binary operator were incorrect.
|
||||||
Operator(BinOp),
|
Operator(BinOp),
|
||||||
|
|
||||||
|
FuzzerAnnotationMismatch,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
|
|
@ -8,7 +8,7 @@ use crate::{
|
||||||
Use, Validator,
|
Use, Validator,
|
||||||
},
|
},
|
||||||
builtins,
|
builtins,
|
||||||
builtins::function,
|
builtins::{function, fuzzer, generic_var},
|
||||||
expr::{TypedExpr, UntypedExpr},
|
expr::{TypedExpr, UntypedExpr},
|
||||||
line_numbers::LineNumbers,
|
line_numbers::LineNumbers,
|
||||||
tipo::{Span, Type, TypeVar},
|
tipo::{Span, Type, TypeVar},
|
||||||
|
@ -17,7 +17,7 @@ use crate::{
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
environment::{generalise, EntityKind, Environment},
|
environment::{generalise, EntityKind, Environment},
|
||||||
error::{Error, Warning},
|
error::{Error, UnifyErrorSituation, Warning},
|
||||||
expr::ExprTyper,
|
expr::ExprTyper,
|
||||||
hydrator::Hydrator,
|
hydrator::Hydrator,
|
||||||
TypeInfo, ValueConstructor, ValueConstructorVariant,
|
TypeInfo, ValueConstructor, ValueConstructorVariant,
|
||||||
|
@ -337,7 +337,7 @@ fn infer_definition(
|
||||||
if f.arguments.len() > 1 {
|
if f.arguments.len() > 1 {
|
||||||
return Err(Error::IncorrectTestArity {
|
return Err(Error::IncorrectTestArity {
|
||||||
count: f.arguments.len(),
|
count: f.arguments.len(),
|
||||||
location: f.arguments.get(1).unwrap().location,
|
location: f.arguments.get(1).expect("arguments.len() > 1").location,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -345,11 +345,22 @@ fn infer_definition(
|
||||||
ExprTyper::new(environment, lines, tracing).infer(arg.via.clone())?;
|
ExprTyper::new(environment, lines, tracing).infer(arg.via.clone())?;
|
||||||
|
|
||||||
let (inferred_annotation, inner_type) =
|
let (inferred_annotation, inner_type) =
|
||||||
infer_fuzzer(&typed_via.tipo(), &arg.location)?;
|
infer_fuzzer(environment, &typed_via.tipo(), &arg.via.location())?;
|
||||||
|
|
||||||
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 given =
|
||||||
|
hydrator.type_from_annotation(provided_annotation, environment)?;
|
||||||
|
|
||||||
if !provided_annotation.is_logically_equal(&inferred_annotation) {
|
if !provided_annotation.is_logically_equal(&inferred_annotation) {
|
||||||
todo!("Inferred annotation doesn't match actual annotation.")
|
return Err(Error::CouldNotUnify {
|
||||||
|
location: arg.location,
|
||||||
|
expected: inner_type.clone(),
|
||||||
|
given,
|
||||||
|
situation: Some(UnifyErrorSituation::FuzzerAnnotationMismatch),
|
||||||
|
rigid_type_names: hydrator.rigid_names(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -737,44 +748,79 @@ fn infer_function(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn infer_fuzzer(tipo: &Type, location: &Span) -> Result<(Annotation, Rc<Type>), Error> {
|
fn infer_fuzzer(
|
||||||
match tipo {
|
environment: &mut Environment<'_>,
|
||||||
// TODO: Ensure args & first returned element is a Prelude's PRNG.
|
tipo: &Rc<Type>,
|
||||||
Type::Fn { ret, .. } => match &ret.borrow() {
|
location: &Span,
|
||||||
|
) -> Result<(Annotation, Rc<Type>), Error> {
|
||||||
|
let could_not_unify = || Error::CouldNotUnify {
|
||||||
|
location: *location,
|
||||||
|
expected: fuzzer(generic_var(0)),
|
||||||
|
given: tipo.clone(),
|
||||||
|
situation: None,
|
||||||
|
rigid_type_names: HashMap::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
match tipo.borrow() {
|
||||||
|
Type::Fn { ret, .. } => match ret.borrow() {
|
||||||
Type::App {
|
Type::App {
|
||||||
module, name, args, ..
|
module, name, args, ..
|
||||||
} if module.is_empty() && name == "Option" && args.len() == 1 => {
|
} if module.is_empty() && name == "Option" && args.len() == 1 => {
|
||||||
match &args.first().map(|x| x.borrow()) {
|
match args.first().expect("args.len() == 1").borrow() {
|
||||||
Some(Type::Tuple { elems }) if elems.len() == 2 => {
|
Type::Tuple { elems } if elems.len() == 2 => {
|
||||||
let wrapped = elems.get(1).expect("Tuple has two elements");
|
let wrapped = elems.get(1).expect("Tuple has two elements");
|
||||||
Ok((annotation_from_type(wrapped, location)?, wrapped.clone()))
|
|
||||||
|
// NOTE: Although we've drilled through the Fuzzer structure to get here,
|
||||||
|
// we still need to enforce that:
|
||||||
|
//
|
||||||
|
// 1. The Fuzzer is a function with a single argument of type PRNG
|
||||||
|
// 2. It returns not only a wrapped type, but also a new PRNG
|
||||||
|
//
|
||||||
|
// All-in-all, we could bundle those verification through the
|
||||||
|
// `infer_fuzzer` function, but instead, we can also just piggyback on
|
||||||
|
// `unify` now that we have figured out the type carried by the fuzzer.
|
||||||
|
environment.unify(
|
||||||
|
tipo.clone(),
|
||||||
|
fuzzer(wrapped.clone()),
|
||||||
|
*location,
|
||||||
|
false,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok((annotate_fuzzer(wrapped, location)?, wrapped.clone()))
|
||||||
}
|
}
|
||||||
_ => {
|
_ => Err(could_not_unify()),
|
||||||
todo!("expected a single generic argument unifying as 2-tuple")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
_ => Err(could_not_unify()),
|
||||||
_ => todo!("expected an Option<a>"),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
Type::Var { .. } | Type::App { .. } | Type::Tuple { .. } => {
|
Type::Var { tipo } => match &*tipo.deref().borrow() {
|
||||||
todo!("Fuzzer type isn't a function?");
|
TypeVar::Link { tipo } => infer_fuzzer(environment, tipo, location),
|
||||||
}
|
_ => Err(Error::GenericLeftAtBoundary {
|
||||||
|
location: *location,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
Type::App { .. } | Type::Tuple { .. } => Err(could_not_unify()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn annotation_from_type(tipo: &Type, location: &Span) -> Result<Annotation, Error> {
|
fn annotate_fuzzer(tipo: &Type, location: &Span) -> Result<Annotation, Error> {
|
||||||
match tipo {
|
match tipo {
|
||||||
Type::App {
|
Type::App {
|
||||||
name, module, args, ..
|
name, module, args, ..
|
||||||
} => {
|
} => {
|
||||||
let arguments = args
|
let arguments = args
|
||||||
.iter()
|
.iter()
|
||||||
.map(|arg| annotation_from_type(arg, location))
|
.map(|arg| annotate_fuzzer(arg, location))
|
||||||
.collect::<Result<Vec<Annotation>, _>>()?;
|
.collect::<Result<Vec<Annotation>, _>>()?;
|
||||||
Ok(Annotation::Constructor {
|
Ok(Annotation::Constructor {
|
||||||
name: name.to_owned(),
|
name: name.to_owned(),
|
||||||
module: Some(module.to_owned()),
|
module: if module.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(module.to_owned())
|
||||||
|
},
|
||||||
arguments,
|
arguments,
|
||||||
location: *location,
|
location: *location,
|
||||||
})
|
})
|
||||||
|
@ -783,7 +829,7 @@ fn annotation_from_type(tipo: &Type, location: &Span) -> Result<Annotation, Erro
|
||||||
Type::Tuple { elems } => {
|
Type::Tuple { elems } => {
|
||||||
let elems = elems
|
let elems = elems
|
||||||
.iter()
|
.iter()
|
||||||
.map(|arg| annotation_from_type(arg, location))
|
.map(|arg| annotate_fuzzer(arg, location))
|
||||||
.collect::<Result<Vec<Annotation>, _>>()?;
|
.collect::<Result<Vec<Annotation>, _>>()?;
|
||||||
Ok(Annotation::Tuple {
|
Ok(Annotation::Tuple {
|
||||||
elems,
|
elems,
|
||||||
|
@ -792,11 +838,14 @@ fn annotation_from_type(tipo: &Type, location: &Span) -> Result<Annotation, Erro
|
||||||
}
|
}
|
||||||
|
|
||||||
Type::Var { tipo } => match &*tipo.deref().borrow() {
|
Type::Var { tipo } => match &*tipo.deref().borrow() {
|
||||||
TypeVar::Link { tipo } => annotation_from_type(tipo, location),
|
TypeVar::Link { tipo } => annotate_fuzzer(tipo, location),
|
||||||
_ => todo!("Fuzzer contains functions and/or non-concrete data-types? {tipo:#?}"),
|
_ => Err(Error::GenericLeftAtBoundary {
|
||||||
|
location: *location,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
Type::Fn { .. } => {
|
Type::Fn { .. } => Err(Error::IllegalTypeInData {
|
||||||
todo!("Fuzzer contains functions and/or non-concrete data-types? {tipo:#?}");
|
location: *location,
|
||||||
}
|
tipo: Rc::new(tipo.clone()),
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue