Yield proper user-facing error when inferring Fuzzer usage

This commit is contained in:
KtorZ 2024-03-02 12:55:56 +01:00
parent cf61387a41
commit bbc9fc5762
No known key found for this signature in database
GPG Key ID: 33173CB6F77F4277
4 changed files with 218 additions and 34 deletions

View File

@ -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,

View File

@ -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#"

View File

@ -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)]

View File

@ -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()),
}),
} }
} }