From 90ee86d14ec6afdda6e0e602d031c442c5db2f72 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sun, 29 Jan 2023 12:58:19 +0100 Subject: [PATCH] Improve error reporting for the blueprint generation. Actually link schema error to source code with a span and a label. This is easily done and provides some extra context. --- crates/aiken-project/src/blueprint/error.rs | 58 ++++-- crates/aiken-project/src/blueprint/schema.rs | 179 ++++++++++++------ .../aiken-project/src/blueprint/validator.rs | 22 ++- 3 files changed, 173 insertions(+), 86 deletions(-) diff --git a/crates/aiken-project/src/blueprint/error.rs b/crates/aiken-project/src/blueprint/error.rs index 8219da9f..d7a612d9 100644 --- a/crates/aiken-project/src/blueprint/error.rs +++ b/crates/aiken-project/src/blueprint/error.rs @@ -1,21 +1,31 @@ use super::schema; use crate::module::CheckedModule; -use aiken_lang::ast::{Span, TypedFunction}; +use aiken_lang::{ + ast::{Span, TypedFunction}, + tipo::Type, +}; use miette::{Diagnostic, NamedSource}; use owo_colors::OwoColorize; -use std::{fmt::Debug, path::PathBuf}; +use std::{fmt::Debug, sync::Arc}; #[derive(Debug, thiserror::Error, Diagnostic)] pub enum Error { - #[error("A validator functions must return Bool")] + #[error("A validator must return {}", "Bool".bright_blue().bold())] #[diagnostic(code("aiken::blueprint::invalid::return_type"))] + #[diagnostic(help(r#"While analyzing the return type of your validator, I found it to be: + +╰─▶ {signature} + +...but I expected this to be a {type_Bool}. If I am inferring the wrong type, you may want to add a type annotation to the function."# + , type_Bool = "Bool".bright_blue().bold() + , signature = return_type.to_pretty(0).red() + ))] ValidatorMustReturnBool { - path: PathBuf, - src: String, - #[source_code] - named: NamedSource, #[label("invalid return type")] location: Span, + #[source_code] + source_code: NamedSource, + return_type: Arc, }, #[error("A {} validator requires at least {at_least} arguments", name.purple().bold())] #[diagnostic(code("aiken::blueprint::invalid::arity"))] @@ -24,23 +34,30 @@ pub enum Error { at_least: u8, #[label("not enough arguments")] location: Span, - path: PathBuf, - src: String, #[source_code] - named: NamedSource, + source_code: NamedSource, + }, + #[error("{}", error)] + #[diagnostic(help("{}", error.help()))] + #[diagnostic(code("aiken::blueprint::interface"))] + Schema { + error: schema::Error, + #[label("invalid contract's boundary")] + location: Span, + #[source_code] + source_code: NamedSource, }, - #[error(transparent)] - #[diagnostic(transparent)] - Schema(#[diagnostic_source] schema::Error), } pub fn assert_return_bool(module: &CheckedModule, def: &TypedFunction) -> Result<(), Error> { if !def.return_type.is_bool() { Err(Error::ValidatorMustReturnBool { + return_type: def.return_type.clone(), location: def.location, - src: module.code.clone(), - path: module.input_path.clone(), - named: NamedSource::new(module.input_path.display().to_string(), module.code.clone()), + source_code: NamedSource::new( + module.input_path.display().to_string(), + module.code.clone(), + ), }) } else { Ok(()) @@ -54,12 +71,13 @@ pub fn assert_min_arity( ) -> Result<(), Error> { if def.arguments.len() < at_least as usize { Err(Error::WrongValidatorArity { - location: def.location, - src: module.code.clone(), - path: module.input_path.clone(), - named: NamedSource::new(module.input_path.display().to_string(), module.code.clone()), name: def.name.clone(), at_least, + location: def.location, + source_code: NamedSource::new( + module.input_path.display().to_string(), + module.code.clone(), + ), }) } else { Ok(()) diff --git a/crates/aiken-project/src/blueprint/schema.rs b/crates/aiken-project/src/blueprint/schema.rs index d4a6508b..223172bc 100644 --- a/crates/aiken-project/src/blueprint/schema.rs +++ b/crates/aiken-project/src/blueprint/schema.rs @@ -3,7 +3,6 @@ use aiken_lang::{ ast::{DataType, Definition, TypedDefinition}, tipo::{pretty, Type, TypeVar}, }; -use miette::Diagnostic; use owo_colors::OwoColorize; use serde::{ self, @@ -175,9 +174,7 @@ impl Annotated { Ok(Schema::Data(Some(data)).into()) } - _ => Err(Error::UnsupportedType { - type_info: type_info.clone(), - }), + _ => Err(Error::new(ErrorContext::UnsupportedType, type_info)), }, Type::App { module: module_name, @@ -190,7 +187,7 @@ impl Annotated { let type_parameters = collect_type_parameters(&constructor.typed_parameters, args); let annotated = Schema::Data(Some( Data::from_data_type(modules, constructor, &type_parameters) - .map_err(|e| e.add_crumb(type_info))?, + .map_err(|e| e.backtrack(type_info))?, )); Ok(Annotated { @@ -202,41 +199,42 @@ impl Annotated { Type::Var { tipo } => match tipo.borrow().deref() { TypeVar::Link { tipo } => Annotated::from_type(modules, tipo, type_parameters), TypeVar::Generic { id } => { - let tipo = type_parameters.get(id).ok_or(Error::FreeParameter { - breadcrumbs: vec![type_info.clone()], - })?; + let tipo = type_parameters + .get(id) + .ok_or_else(|| Error::new(ErrorContext::FreeTypeVariable, type_info))?; Annotated::from_type(modules, tipo, type_parameters) } - TypeVar::Unbound { .. } => Err(Error::UnsupportedType { - type_info: type_info.clone(), - }), + TypeVar::Unbound { .. } => { + Err(Error::new(ErrorContext::UnboundTypeVariable, type_info)) + } }, Type::Tuple { elems } => match &elems[..] { [left, right] => { - let left = - Annotated::from_type(modules, left, type_parameters)?.into_data(left)?; - let right = - Annotated::from_type(modules, right, type_parameters)?.into_data(right)?; + let left = Annotated::from_type(modules, left, type_parameters)? + .into_data(left) + .map_err(|e| e.backtrack(type_info))?; + let right = Annotated::from_type(modules, right, type_parameters)? + .into_data(right) + .map_err(|e| e.backtrack(type_info))?; Ok(Schema::Pair(left.annotated, right.annotated).into()) } _ => { - let elems: Result, _> = elems + let elems = elems .iter() .map(|e| { Annotated::from_type(modules, e, type_parameters) .and_then(|s| s.into_data(e).map(|s| s.annotated)) }) - .collect(); + .collect::, _>>() + .map_err(|e| e.backtrack(type_info))?; Ok(Annotated { title: Some("Tuple".to_owned()), description: None, - annotated: Schema::List(elems?), + annotated: Schema::List(elems), }) } }, - Type::Fn { .. } => Err(Error::UnsupportedType { - type_info: type_info.clone(), - }), + Type::Fn { .. } => Err(Error::new(ErrorContext::UnexpectedFunction, type_info)), } } @@ -251,9 +249,7 @@ impl Annotated { description, annotated: data, }), - _ => Err(Error::ExpectedData { - got: type_info.to_owned(), - }), + _ => Err(Error::new(ErrorContext::ExpectedData, type_info)), } } } @@ -412,53 +408,110 @@ impl Serialize for Constructor { } } -#[derive(Debug, PartialEq, Clone, thiserror::Error, Diagnostic)] -pub enum Error { - #[error("I stumbled upon an unsupported type in a datum or redeemer definition.")] - #[diagnostic(help( - r#"I do not know how to generate a portable Plutus specification for the following type: +#[derive(Debug, PartialEq, Clone, thiserror::Error)] +#[error("{}", context)] +pub struct Error { + context: ErrorContext, + breadcrumbs: Vec, +} + +#[derive(Debug, PartialEq, Clone, thiserror::Error)] +pub enum ErrorContext { + #[error("I failed at my own job and couldn't figure out how to generate a specification for a type.")] + UnsupportedType, + + #[error("I discovered a type hole where I would expect a concrete type.")] + UnboundTypeVariable, + + #[error("I caught a free variable in the contract's interface boundary.")] + FreeTypeVariable, -╰─▶ {type_signature} - "# - , type_signature = pretty::Printer::new().print(type_info).to_pretty_string(70).bright_blue() - ))] - UnsupportedType { type_info: Type }, #[error("I had the misfortune to find an invalid type in an interface boundary.")] - ExpectedData { got: Type }, + ExpectedData, - #[error("I caught a free type-parameter in an interface boundary.")] - #[diagnostic(help( - r#"There can't be any free type-parameter at the contract boundary (i.e. in types used as datum and/or redeemer). -Indeed, the validator can only be invoked with (very) concrete types. Since there's no reflexion possible inside a validator, -it simply isn't possible to have any remaining free type variable in any of the datum or redeemer. - -I got there when trying to generate a blueprint specification of the following type: - -╰─▶ {type_signature} - "# - , type_signature = - breadcrumbs - .iter() - .map(|type_info| pretty::Printer::new().print(type_info).to_pretty_string(70)) - .collect::>() - .join(" → ") - .bright_blue() - - ))] - FreeParameter { breadcrumbs: Vec }, + #[error("I figured you tried to export a function in your contract's binary interface.")] + UnexpectedFunction, } impl Error { - fn add_crumb(self, type_info: &Type) -> Self { - match self { - Error::FreeParameter { breadcrumbs: tail } => { - let mut breadcrumbs = vec![type_info.clone()]; - breadcrumbs.extend(tail); - Error::FreeParameter { breadcrumbs } - } - _ => self, + pub fn new(context: ErrorContext, type_info: &Type) -> Self { + Error { + context, + breadcrumbs: vec![type_info.clone()], } } + + pub fn backtrack(self, type_info: &Type) -> Self { + let mut breadcrumbs = vec![type_info.clone()]; + breadcrumbs.extend(self.breadcrumbs); + Error { + context: self.context, + breadcrumbs, + } + } + + pub fn help(&self) -> String { + match self.context { + ErrorContext::UnsupportedType => format!( + r#"I do not know how to generate a portable Plutus specification for the following type: + +╰─▶ {signature} + +This is likely a bug. I should know. May you be kind enough and report this on ."#, + signature = Error::fmt_breadcrumbs(&[self.breadcrumbs.last().unwrap().to_owned()]), + ), + + ErrorContext::FreeTypeVariable => format!( + r#"There can't be any free type variable at the contract's boundary (i.e. in types used as datum and/or redeemer). Indeed, the validator can only be invoked with (very) concrete types. Since there's no reflexion possible inside a validator, it simply isn't possible to have any remaining free type variable in any of the datum or redeemer. + +I got there when trying to generate a blueprint specification of the following type: + +╰─▶ {breadcrumbs}"#, + breadcrumbs = Error::fmt_breadcrumbs(&self.breadcrumbs) + ), + + ErrorContext::UnboundTypeVariable => format!( + r#"There cannot be any unbound type variable at the contract's boundary (i.e. in types used as datum and/or redeemer). Indeed, in order to generate an outward-facing specification of the contract's interface, I need to know what concrete representations will the datum and/or the redeemer have. + +If your contract doesn't need datum or redeemer, you can always give them the type {type_Void} to indicate this. It is very concrete and will help me progress forward."#, + type_Void = "Void".bright_blue().bold() + ), + + ErrorContext::ExpectedData => format!( + r#"While figuring out the outward-facing specification for your contract, I found a type that cannot actually be represented as valid Untyped Plutus Core (the low-level language Cardano uses to execute smart-contracts. For example, it isn't possible to have a list or a tuple of {type_String} because the underlying execution engine doesn't allow it. + +There are few restrictions like this one. In this instance, here's the types I followed and that led me to this problem: + +╰─▶ {breadcrumbs}"#, + type_String = "String".bright_blue().bold(), + breadcrumbs = Error::fmt_breadcrumbs(&self.breadcrumbs) + ), + + ErrorContext::UnexpectedFunction => format!( + r#"I can't allow that. Functions aren't serializable as data on-chain and thus cannot be used within your datum and/or redeemer types. + +Here's the types I followed and that led me to this problem: + +╰─▶ {breadcrumbs}"#, + breadcrumbs = Error::fmt_breadcrumbs(&self.breadcrumbs) + ), + } + } + + fn fmt_breadcrumbs(breadcrumbs: &[Type]) -> String { + breadcrumbs + .iter() + .map(|type_info| { + pretty::Printer::new() + .print(type_info) + .to_pretty_string(70) + .bright_blue() + .bold() + .to_string() + }) + .collect::>() + .join(" → ") + } } fn collect_type_parameters<'a>( diff --git a/crates/aiken-project/src/blueprint/validator.rs b/crates/aiken-project/src/blueprint/validator.rs index ff6374af..814d4202 100644 --- a/crates/aiken-project/src/blueprint/validator.rs +++ b/crates/aiken-project/src/blueprint/validator.rs @@ -4,6 +4,7 @@ use super::{ }; use crate::module::{CheckedModule, CheckedModules}; use aiken_lang::{ast::TypedFunction, uplc::CodeGenerator}; +use miette::NamedSource; use pallas::ledger::primitives::babbage as cardano; use pallas_traverse::ComputeHash; use serde::{ @@ -91,12 +92,27 @@ impl Validator { purpose, datum: datum .map(|datum| { - Annotated::from_type(modules.into(), &datum.tipo, &HashMap::new()) - .map_err(Error::Schema) + Annotated::from_type(modules.into(), &datum.tipo, &HashMap::new()).map_err( + |error| Error::Schema { + error, + location: datum.location, + source_code: NamedSource::new( + validator.input_path.display().to_string(), + validator.code.clone(), + ), + }, + ) }) .transpose()?, redeemer: Annotated::from_type(modules.into(), &redeemer.tipo, &HashMap::new()) - .map_err(Error::Schema)?, + .map_err(|error| Error::Schema { + error, + location: redeemer.location, + source_code: NamedSource::new( + validator.input_path.display().to_string(), + validator.code.clone(), + ), + })?, program: generator .generate(&def.body, &def.arguments, true) .try_into()