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.
This commit is contained in:
KtorZ 2023-01-29 12:58:19 +01:00
parent b3fc2d51cf
commit 90ee86d14e
No known key found for this signature in database
GPG Key ID: 33173CB6F77F4277
3 changed files with 173 additions and 86 deletions

View File

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

View File

@ -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<Schema> {
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<Schema> {
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<Schema> {
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<Vec<Data>, _> = 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::<Result<Vec<_>, _>>()
.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<Schema> {
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<Type>,
}
#[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::<Vec<String>>()
.join("")
.bright_blue()
))]
FreeParameter { breadcrumbs: Vec<Type> },
#[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 <https://github.com/aiken-lang/aiken>."#,
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::<Vec<_>>()
.join("")
}
}
fn collect_type_parameters<'a>(

View File

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