From bbc9fc57621cf72f36aa0d04014b1209d048ac13 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sat, 2 Mar 2024 12:55:56 +0100 Subject: [PATCH] Yield proper user-facing error when inferring Fuzzer usage --- .../aiken-lang/src/parser/definition/test.rs | 3 +- crates/aiken-lang/src/tests/check.rs | 107 ++++++++++++++++++ crates/aiken-lang/src/tipo/error.rs | 37 +++++- crates/aiken-lang/src/tipo/infer.rs | 105 ++++++++++++----- 4 files changed, 218 insertions(+), 34 deletions(-) diff --git a/crates/aiken-lang/src/parser/definition/test.rs b/crates/aiken-lang/src/parser/definition/test.rs index cbd57a23..726d68f8 100644 --- a/crates/aiken-lang/src/parser/definition/test.rs +++ b/crates/aiken-lang/src/parser/definition/test.rs @@ -69,9 +69,10 @@ pub fn via() -> impl Parser { }), )) .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, diff --git a/crates/aiken-lang/src/tests/check.rs b/crates/aiken-lang/src/tests/check.rs index 1c978c6d..3f743156 100644 --- a/crates/aiken-lang/src/tests/check.rs +++ b/crates/aiken-lang/src/tests/check.rs @@ -1202,6 +1202,113 @@ fn pipe_with_wrong_type_and_full_args() { )) } +#[test] +fn fuzzer_ok_basic() { + let source_code = r#" + fn int() -> Fuzzer { 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 { todo } + fn list(a: Fuzzer) -> Fuzzer> { 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 { todo } + fn list(a: Fuzzer) -> Fuzzer> { 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 { 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) -> Fuzzer> { todo } + fn int() -> Fuzzer { 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#" diff --git a/crates/aiken-lang/src/tipo/error.rs b/crates/aiken-lang/src/tipo/error.rs index 1ed18458..1dacbf9e 100644 --- a/crates/aiken-lang/src/tipo/error.rs +++ b/crates/aiken-lang/src/tipo/error.rs @@ -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)] diff --git a/crates/aiken-lang/src/tipo/infer.rs b/crates/aiken-lang/src/tipo/infer.rs index 5c02eb94..0386c668 100644 --- a/crates/aiken-lang/src/tipo/infer.rs +++ b/crates/aiken-lang/src/tipo/infer.rs @@ -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), 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, + location: &Span, +) -> Result<(Annotation, Rc), 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"), + _ => 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 { +fn annotate_fuzzer(tipo: &Type, location: &Span) -> Result { 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::, _>>()?; 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 { let elems = elems .iter() - .map(|arg| annotation_from_type(arg, location)) + .map(|arg| annotate_fuzzer(arg, location)) .collect::, _>>()?; Ok(Annotation::Tuple { elems, @@ -792,11 +838,14 @@ fn annotation_from_type(tipo: &Type, location: &Span) -> Result 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()), + }), } }