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:
parent
b3fc2d51cf
commit
90ee86d14e
|
@ -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(())
|
||||
|
|
|
@ -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>(
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue