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())
|
||||
.map_with_span(|(arg_name, annotation), location| (arg_name, annotation, location))
|
||||
.then_ignore(just(Token::Via))
|
||||
.then(fuzzer())
|
||||
.map_with_span(|((arg_name, annotation), via), location| ast::ArgVia {
|
||||
.map(|((arg_name, annotation, location), via)| ast::ArgVia {
|
||||
arg_name,
|
||||
via,
|
||||
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]
|
||||
fn utf8_hex_literal_warning() {
|
||||
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")]
|
||||
#[diagnostic(code("illegal::type_in_data"))]
|
||||
#[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())
|
||||
))]
|
||||
IllegalTypeInData {
|
||||
|
@ -957,6 +957,15 @@ The best thing to do from here is to remove it."#))]
|
|||
#[label("too many arguments")]
|
||||
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 {
|
||||
|
@ -1009,6 +1018,7 @@ impl ExtraData for Error {
|
|||
| Error::UpdateMultiConstructorType { .. }
|
||||
| Error::ValidatorImported { .. }
|
||||
| Error::IncorrectTestArity { .. }
|
||||
| Error::GenericLeftAtBoundary { .. }
|
||||
| Error::ValidatorMustReturnBool { .. } => None,
|
||||
|
||||
Error::UnknownType { name, .. }
|
||||
|
@ -1206,14 +1216,14 @@ fn suggest_unify(
|
|||
|
||||
(
|
||||
format!(
|
||||
"{} - {}",
|
||||
"{}.{{{}}}",
|
||||
expected_module.if_supports_color(Stdout, |s| s.bright_blue()),
|
||||
expected_str.if_supports_color(Stdout, |s| s.green()),
|
||||
expected_module.if_supports_color(Stdout, |s| s.bright_blue())
|
||||
),
|
||||
format!(
|
||||
"{} - {}",
|
||||
"{}.{{{}}}",
|
||||
given_module.if_supports_color(Stdout, |s| s.bright_blue()),
|
||||
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,
|
||||
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! {
|
||||
r#"I am inferring the following type:
|
||||
|
||||
|
@ -1677,6 +1702,8 @@ pub enum UnifyErrorSituation {
|
|||
|
||||
/// The operands of a binary operator were incorrect.
|
||||
Operator(BinOp),
|
||||
|
||||
FuzzerAnnotationMismatch,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
|
|
@ -8,7 +8,7 @@ use crate::{
|
|||
Use, Validator,
|
||||
},
|
||||
builtins,
|
||||
builtins::function,
|
||||
builtins::{function, fuzzer, generic_var},
|
||||
expr::{TypedExpr, UntypedExpr},
|
||||
line_numbers::LineNumbers,
|
||||
tipo::{Span, Type, TypeVar},
|
||||
|
@ -17,7 +17,7 @@ use crate::{
|
|||
|
||||
use super::{
|
||||
environment::{generalise, EntityKind, Environment},
|
||||
error::{Error, Warning},
|
||||
error::{Error, UnifyErrorSituation, Warning},
|
||||
expr::ExprTyper,
|
||||
hydrator::Hydrator,
|
||||
TypeInfo, ValueConstructor, ValueConstructorVariant,
|
||||
|
@ -337,7 +337,7 @@ fn infer_definition(
|
|||
if f.arguments.len() > 1 {
|
||||
return Err(Error::IncorrectTestArity {
|
||||
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())?;
|
||||
|
||||
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 {
|
||||
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) {
|
||||
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> {
|
||||
match tipo {
|
||||
// TODO: Ensure args & first returned element is a Prelude's PRNG.
|
||||
Type::Fn { ret, .. } => match &ret.borrow() {
|
||||
fn infer_fuzzer(
|
||||
environment: &mut Environment<'_>,
|
||||
tipo: &Rc<Type>,
|
||||
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 {
|
||||
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 => {
|
||||
match args.first().expect("args.len() == 1").borrow() {
|
||||
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")
|
||||
|
||||
// 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 an Option<a>"),
|
||||
_ => Err(could_not_unify()),
|
||||
},
|
||||
|
||||
Type::Var { .. } | Type::App { .. } | Type::Tuple { .. } => {
|
||||
todo!("Fuzzer type isn't a function?");
|
||||
}
|
||||
Type::Var { tipo } => match &*tipo.deref().borrow() {
|
||||
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 {
|
||||
Type::App {
|
||||
name, module, args, ..
|
||||
} => {
|
||||
let arguments = args
|
||||
.iter()
|
||||
.map(|arg| annotation_from_type(arg, location))
|
||||
.map(|arg| annotate_fuzzer(arg, location))
|
||||
.collect::<Result<Vec<Annotation>, _>>()?;
|
||||
Ok(Annotation::Constructor {
|
||||
name: name.to_owned(),
|
||||
module: Some(module.to_owned()),
|
||||
module: if module.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(module.to_owned())
|
||||
},
|
||||
arguments,
|
||||
location: *location,
|
||||
})
|
||||
|
@ -783,7 +829,7 @@ fn annotation_from_type(tipo: &Type, location: &Span) -> Result<Annotation, Erro
|
|||
Type::Tuple { elems } => {
|
||||
let elems = elems
|
||||
.iter()
|
||||
.map(|arg| annotation_from_type(arg, location))
|
||||
.map(|arg| annotate_fuzzer(arg, location))
|
||||
.collect::<Result<Vec<Annotation>, _>>()?;
|
||||
Ok(Annotation::Tuple {
|
||||
elems,
|
||||
|
@ -792,11 +838,14 @@ fn annotation_from_type(tipo: &Type, location: &Span) -> Result<Annotation, Erro
|
|||
}
|
||||
|
||||
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:#?}"),
|
||||
TypeVar::Link { tipo } => annotate_fuzzer(tipo, location),
|
||||
_ => Err(Error::GenericLeftAtBoundary {
|
||||
location: *location,
|
||||
}),
|
||||
},
|
||||
Type::Fn { .. } => {
|
||||
todo!("Fuzzer contains functions and/or non-concrete data-types? {tipo:#?}");
|
||||
}
|
||||
Type::Fn { .. } => Err(Error::IllegalTypeInData {
|
||||
location: *location,
|
||||
tipo: Rc::new(tipo.clone()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue