aiken/crates/aiken-project/src/blueprint/schema.rs

757 lines
25 KiB
Rust

use crate::CheckedModule;
use aiken_lang::{
ast::{DataType, Definition, TypedDefinition},
tipo::{pretty, Type, TypeVar},
};
use owo_colors::OwoColorize;
use serde::{
self,
ser::{Serialize, SerializeStruct, Serializer},
};
use serde_json;
use std::ops::Deref;
use std::{
collections::HashMap,
fmt::{self, Display},
sync::Arc,
};
#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)]
pub struct Annotated<T> {
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(flatten)]
pub annotated: T,
}
/// A schema for low-level UPLC primitives.
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum Schema {
Unit,
Boolean,
Integer,
Bytes,
String,
Pair(Data, Data),
List(Vec<Data>),
Data(Option<Data>),
}
/// A schema for Plutus' Data.
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum Data {
Integer,
Bytes,
List(Box<Data>),
Map(Box<Data>, Box<Data>),
AnyOf(Vec<Annotated<Constructor>>),
}
/// Captures a single UPLC constructor with its
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Constructor {
pub index: usize,
pub fields: Vec<Annotated<Data>>,
}
impl<T> From<T> for Annotated<T> {
fn from(annotated: T) -> Self {
Annotated {
title: None,
description: None,
annotated,
}
}
}
impl Annotated<Schema> {
pub fn from_type(
modules: &HashMap<String, CheckedModule>,
type_info: &Type,
type_parameters: &HashMap<u64, &Arc<Type>>,
) -> Result<Self, Error> {
match type_info {
Type::App {
module: module_name,
name: type_name,
args,
..
} if module_name.is_empty() => match &type_name[..] {
"Data" => Ok(Annotated {
title: Some("Data".to_string()),
description: Some("Any Plutus data.".to_string()),
annotated: Schema::Data(None),
}),
"ByteArray" => Ok(Schema::Data(Some(Data::Bytes)).into()),
"Int" => Ok(Schema::Data(Some(Data::Integer)).into()),
"String" => Ok(Schema::String.into()),
"Void" => Ok(Annotated {
title: Some("Unit".to_string()),
description: Some("The nullary constructor.".to_string()),
annotated: Schema::Data(Some(Data::AnyOf(vec![Annotated {
title: None,
description: None,
annotated: Constructor {
index: 0,
fields: vec![],
},
}]))),
}),
"Bool" => Ok(Annotated {
title: Some("Bool".to_string()),
description: None,
annotated: Schema::Data(Some(Data::AnyOf(vec![
Annotated {
title: Some("False".to_string()),
description: None,
annotated: Constructor {
index: 0,
fields: vec![],
},
},
Annotated {
title: Some("True".to_string()),
description: None,
annotated: Constructor {
index: 1,
fields: vec![],
},
},
]))),
}),
"Option" => {
let generic =
Annotated::from_type(modules, args.get(0).unwrap(), type_parameters)
.and_then(|s| s.into_data(type_info))?;
Ok(Annotated {
title: Some("Optional".to_string()),
description: None,
annotated: Schema::Data(Some(Data::AnyOf(vec![
Annotated {
title: Some("Some".to_string()),
description: Some("An optional value.".to_string()),
annotated: Constructor {
index: 0,
fields: vec![generic],
},
},
Annotated {
title: Some("None".to_string()),
description: Some("Nothing.".to_string()),
annotated: Constructor {
index: 1,
fields: vec![],
},
},
]))),
})
}
"List" => {
let generic =
Annotated::from_type(modules, args.get(0).unwrap(), type_parameters)?;
// NOTE: Lists of 2-tuples are treated as Maps. This is an oddity we inherit
// from the PlutusTx / LedgerApi Haskell codebase, which encodes some elements
// as such. We don't have a concept of language maps in Aiken, so we simply
// make all types abide by this convention.
let data = match generic.annotated {
Schema::Pair(left, right) => Data::Map(Box::new(left), Box::new(right)),
_ => {
let inner = generic.into_data(type_info)?.annotated;
Data::List(Box::new(inner))
}
};
Ok(Schema::Data(Some(data)).into())
}
_ => Err(Error::new(ErrorContext::UnsupportedType, type_info)),
},
Type::App {
module: module_name,
name: type_name,
args,
..
} => {
let module = modules.get(module_name).unwrap();
let constructor = find_definition(type_name, &module.ast.definitions).unwrap();
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.backtrack(type_info))?,
));
Ok(Annotated {
title: Some(constructor.name.clone()),
description: constructor.doc.clone().map(|s| s.trim().to_string()),
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_else(|| Error::new(ErrorContext::FreeTypeVariable, type_info))?;
Annotated::from_type(modules, tipo, type_parameters)
}
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)
.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 = elems
.iter()
.map(|e| {
Annotated::from_type(modules, e, type_parameters)
.and_then(|s| s.into_data(e).map(|s| s.annotated))
})
.collect::<Result<Vec<_>, _>>()
.map_err(|e| e.backtrack(type_info))?;
Ok(Annotated {
title: Some("Tuple".to_owned()),
description: None,
annotated: Schema::List(elems),
})
}
},
Type::Fn { .. } => Err(Error::new(ErrorContext::UnexpectedFunction, type_info)),
}
}
fn into_data(self, type_info: &Type) -> Result<Annotated<Data>, Error> {
match self {
Annotated {
title,
description,
annotated: Schema::Data(Some(data)),
} => Ok(Annotated {
title,
description,
annotated: data,
}),
_ => Err(Error::new(ErrorContext::ExpectedData, type_info)),
}
}
}
impl Data {
pub fn from_data_type(
modules: &HashMap<String, CheckedModule>,
data_type: &DataType<Arc<Type>>,
type_parameters: &HashMap<u64, &Arc<Type>>,
) -> Result<Self, Error> {
let mut variants = vec![];
for (index, constructor) in data_type.constructors.iter().enumerate() {
let mut fields = vec![];
for field in constructor.arguments.iter() {
let mut schema = Annotated::from_type(modules, &field.tipo, type_parameters)
.and_then(|t| t.into_data(&field.tipo))?;
if field.label.is_some() {
schema.title = field.label.clone();
}
if field.doc.is_some() {
schema.description = field.doc.clone().map(|s| s.trim().to_string());
}
fields.push(schema);
}
let variant = Annotated {
title: Some(constructor.name.clone()),
description: constructor.doc.clone().map(|s| s.trim().to_string()),
annotated: Constructor { index, fields },
};
variants.push(variant);
}
// NOTE: Opaque data-types with a single variant and a single field are transparent, they
// are erased completely at compilation time.
if data_type.opaque {
if let [variant] = &variants[..] {
if let [field] = &variant.annotated.fields[..] {
return Ok(field.annotated.clone());
}
}
}
Ok(Data::AnyOf(variants))
}
}
impl Default for Schema {
fn default() -> Self {
Schema::Unit
}
}
impl Display for Schema {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = serde_json::to_string_pretty(self).map_err(|_| fmt::Error)?;
f.write_str(&s)
}
}
impl Serialize for Schema {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
Schema::Unit => {
let mut s = serializer.serialize_struct("Unit", 1)?;
s.serialize_field("dataType", "#unit")?;
s.end()
}
Schema::Boolean => {
let mut s = serializer.serialize_struct("Boolean", 1)?;
s.serialize_field("dataType", "#boolean")?;
s.end()
}
Schema::Integer => {
let mut s = serializer.serialize_struct("Integer", 1)?;
s.serialize_field("dataType", "#integer")?;
s.end()
}
Schema::Bytes => {
let mut s = serializer.serialize_struct("Bytes", 1)?;
s.serialize_field("dataType", "#bytes")?;
s.end()
}
Schema::String => {
let mut s = serializer.serialize_struct("String", 1)?;
s.serialize_field("dataType", "#string")?;
s.end()
}
Schema::Pair(left, right) => {
let mut s = serializer.serialize_struct("Pair", 3)?;
s.serialize_field("dataType", "#pair")?;
s.serialize_field("left", &left)?;
s.serialize_field("right", &right)?;
s.end()
}
Schema::List(items) => {
let mut s = serializer.serialize_struct("List", 2)?;
s.serialize_field("dataType", "#list")?;
s.serialize_field("items", &items)?;
s.end()
}
Schema::Data(None) => {
let s = serializer.serialize_struct("Data", 0)?;
s.end()
}
Schema::Data(Some(data)) => data.serialize(serializer),
}
}
}
impl Serialize for Data {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
Data::Integer => {
let mut s = serializer.serialize_struct("Integer", 1)?;
s.serialize_field("dataType", "integer")?;
s.end()
}
Data::Bytes => {
let mut s = serializer.serialize_struct("Bytes", 1)?;
s.serialize_field("dataType", "bytes")?;
s.end()
}
Data::List(items) => {
let mut s = serializer.serialize_struct("List", 2)?;
s.serialize_field("dataType", "list")?;
s.serialize_field("items", &items)?;
s.end()
}
Data::Map(keys, values) => {
let mut s = serializer.serialize_struct("Map", 3)?;
s.serialize_field("dataType", "map")?;
s.serialize_field("keys", &keys)?;
s.serialize_field("values", &values)?;
s.end()
}
Data::AnyOf(constructors) => {
// TODO: Avoid 'anyOf' applicator when there's only one constructor
//
// match &constructors[..] {
// [constructor] => constructor.serialize(serializer),
// _ => {
let mut s = serializer.serialize_struct("AnyOf", 1)?;
s.serialize_field("anyOf", &constructors)?;
s.end()
}
}
}
}
impl Serialize for Constructor {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut s = serializer.serialize_struct("Constructor", 3)?;
s.serialize_field("dataType", "constructor")?;
s.serialize_field("index", &self.index)?;
s.serialize_field("fields", &self.fields)?;
s.end()
}
}
#[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,
#[error("I had the misfortune to find an invalid type in an interface boundary.")]
ExpectedData,
#[error("I figured you tried to export a function in your contract's binary interface.")]
UnexpectedFunction,
}
impl Error {
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>(
generics: &'a [Arc<Type>],
applications: &'a [Arc<Type>],
) -> HashMap<u64, &'a Arc<Type>> {
let mut type_parameters = HashMap::new();
for (index, generic) in generics.iter().enumerate() {
match &**generic {
Type::Var { tipo } => match *tipo.borrow() {
TypeVar::Generic { id } => {
type_parameters.insert(id, applications.get(index).unwrap());
}
_ => unreachable!(),
},
_ => unreachable!(),
}
}
type_parameters
}
fn find_definition<'a>(
name: &str,
definitions: &'a Vec<TypedDefinition>,
) -> Option<&'a DataType<Arc<Type>>> {
for def in definitions {
match def {
Definition::DataType(data_type) if name == data_type.name => return Some(data_type),
Definition::Fn { .. }
| Definition::DataType { .. }
| Definition::TypeAlias { .. }
| Definition::Use { .. }
| Definition::ModuleConstant { .. }
| Definition::Test { .. } => continue,
}
}
None
}
#[cfg(test)]
pub mod test {
use super::*;
use serde_json::{self, json, Value};
pub fn assert_json(schema: &impl Serialize, expected: Value) {
assert_eq!(serde_json::to_value(schema).unwrap(), expected);
}
#[test]
fn serialize_data_integer() {
let schema = Schema::Data(Some(Data::Integer));
assert_json(
&schema,
json!({
"dataType": "integer"
}),
);
}
#[test]
fn serialize_data_bytes() {
let schema = Schema::Data(Some(Data::Bytes));
assert_json(
&schema,
json!({
"dataType": "bytes"
}),
);
}
#[test]
fn serialize_data_list_1() {
let schema = Schema::Data(Some(Data::List(Box::new(Data::Integer))));
assert_json(
&schema,
json!({
"dataType": "list",
"items": {
"dataType": "integer"
}
}),
);
}
#[test]
fn serialize_data_list_2() {
let schema = Schema::Data(Some(Data::List(Box::new(Data::List(Box::new(
Data::Integer,
))))));
assert_json(
&schema,
json!({
"dataType": "list",
"items":
{
"dataType": "list",
"items": { "dataType": "integer" }
}
}),
);
}
#[test]
fn serialize_data_map_1() {
let schema = Schema::Data(Some(Data::Map(
Box::new(Data::Integer),
Box::new(Data::Bytes),
)));
assert_json(
&schema,
json!({
"dataType": "map",
"keys": {
"dataType": "integer"
},
"values": {
"dataType": "bytes"
}
}),
)
}
#[test]
fn serialize_data_map_2() {
let schema = Schema::Data(Some(Data::Map(
Box::new(Data::Bytes),
Box::new(Data::List(Box::new(Data::Integer))),
)));
assert_json(
&schema,
json!({
"dataType": "map",
"keys": {
"dataType": "bytes"
},
"values": {
"dataType": "list",
"items": { "dataType": "integer" }
}
}),
)
}
#[test]
fn serialize_data_constr_1() {
let schema = Schema::Data(Some(Data::AnyOf(vec![Constructor {
index: 0,
fields: vec![],
}
.into()])));
assert_json(
&schema,
json!({
"anyOf": [{
"dataType": "constructor",
"index": 0,
"fields": []
}]
}),
)
}
#[test]
fn serialize_data_constr_2() {
let schema = Schema::Data(Some(Data::AnyOf(vec![
Constructor {
index: 0,
fields: vec![Data::Integer.into()],
}
.into(),
Constructor {
index: 1,
fields: vec![Data::Bytes.into()],
}
.into(),
])));
assert_json(
&schema,
json!({
"anyOf": [
{
"dataType": "constructor",
"index": 0,
"fields": [{ "dataType": "integer" }]
},
{
"dataType": "constructor",
"index": 1,
"fields": [{ "dataType": "bytes" }]
}
]
}),
)
}
#[test]
fn serialize_empty_data() {
let schema = Schema::Data(None);
assert_json(&schema, json!({}))
}
#[test]
fn serialize_annotated_1() {
let schema = Annotated {
title: Some("foo".to_string()),
description: None,
annotated: Schema::Integer,
};
assert_json(
&schema,
json!({
"title": "foo",
"dataType": "#integer"
}),
)
}
#[test]
fn serialize_annotated_2() {
let schema = Annotated {
title: Some("foo".to_string()),
description: Some("Lorem Ipsum".to_string()),
annotated: Schema::String,
};
assert_json(
&schema,
json!({
"title": "foo",
"description": "Lorem Ipsum",
"dataType": "#string"
}),
)
}
}