From 3cefbd00afa3ec4553fa8362e981f44b6b604779 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Thu, 26 Jan 2023 09:28:45 +0100 Subject: [PATCH 01/20] Draft basic Blueprint schema type definition. This doesn't include validations yet. Let's start simple and try to get some basic schema generated already. --- crates/aiken-project/src/blueprint.rs | 191 ++++++++++++++++++++++++++ crates/aiken-project/src/lib.rs | 1 + 2 files changed, 192 insertions(+) create mode 100644 crates/aiken-project/src/blueprint.rs diff --git a/crates/aiken-project/src/blueprint.rs b/crates/aiken-project/src/blueprint.rs new file mode 100644 index 00000000..9a4c35ef --- /dev/null +++ b/crates/aiken-project/src/blueprint.rs @@ -0,0 +1,191 @@ +use serde::ser::{Serialize, SerializeStruct, Serializer}; +use serde_json; +use std::fmt::{self, Display}; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum Schema { + Integer, + Bytes, + List(Item>), + Map((Box, Box)), + Constructor(usize, Vec), +} + +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(&self, serializer: S) -> Result { + match self { + 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::List(items) => { + let mut s = serializer.serialize_struct("List", 2)?; + s.serialize_field("dataType", "list")?; + s.serialize_field("items", &items)?; + s.end() + } + Schema::Map(elements) => { + let mut s = serializer.serialize_struct("Map", 2)?; + s.serialize_field("dataType", "map")?; + s.serialize_field("elements", &elements)?; + s.end() + } + _ => { + todo!() + } + } + } +} + +// Represent a items list in a JSON schema. Can be either a singleton (i.e. a single schema) when +// all elements in the list are uniform or a list of schemas. +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum Item { + Singleton(T), + Many(Vec), +} + +impl Serialize for Item { + fn serialize(&self, serializer: S) -> Result { + match self { + Item::Singleton(elem) => Serialize::serialize(elem, serializer), + Item::Many(elems) => Serialize::serialize(elems, serializer), + } + } +} + +pub mod test { + use super::*; + #[allow(unused_imports)] + use serde_json::{self, json, Value}; + + pub fn assert_json(schema: &Schema, expected: Value) { + assert_eq!(serde_json::to_value(schema).unwrap(), expected); + } + + #[test] + fn serialize_integer() { + let schema = Schema::Integer; + assert_json( + &schema, + json!({ + "dataType": "integer" + }), + ); + } + + #[test] + fn serialize_bytes() { + let schema = Schema::Bytes; + assert_json( + &schema, + json!({ + "dataType": "bytes" + }), + ); + } + + #[test] + fn serialize_list_1() { + let schema = Schema::List(Item::Many(vec![])); + assert_json( + &schema, + json!({ + "dataType": "list", + "items": [] + }), + ); + } + + #[test] + fn serialize_list_2() { + let schema = Schema::List(Item::Singleton(Box::new(Schema::Integer))); + assert_json( + &schema, + json!({ + "dataType": "list", + "items": { + "dataType": "integer" + } + }), + ); + } + + #[test] + fn serialize_list_3() { + let schema = Schema::List(Item::Many(vec![ + Box::new(Schema::Bytes), + Box::new(Schema::List(Item::Singleton(Box::new(Schema::Integer)))), + ])); + assert_json( + &schema, + json!({ + "dataType": "list", + "items": [ + { + "dataType": "bytes" + }, + { + "dataType": "list", + "items": { "dataType": "integer" } + } + ] + }), + ); + } + + #[test] + fn serialize_map_1() { + let schema = Schema::Map((Box::new(Schema::Integer), Box::new(Schema::Bytes))); + assert_json( + &schema, + json!({ + "dataType": "map", + "elements": [ + { + "dataType": "integer" + }, + { + "dataType": "bytes" + } + ] + }), + ) + } + + #[test] + fn serialize_map_2() { + let schema = Schema::Map(( + Box::new(Schema::Bytes), + Box::new(Schema::List(Item::Singleton(Box::new(Schema::Integer)))), + )); + assert_json( + &schema, + json!({ + "dataType": "map", + "elements": [ + { + "dataType": "bytes" + }, + { + "dataType": "list", + "items": { "dataType": "integer" } + } + ] + }), + ) + } +} diff --git a/crates/aiken-project/src/lib.rs b/crates/aiken-project/src/lib.rs index 86c3ca7c..fd23d11e 100644 --- a/crates/aiken-project/src/lib.rs +++ b/crates/aiken-project/src/lib.rs @@ -1,3 +1,4 @@ +pub mod blueprint; pub mod config; pub mod deps; pub mod docs; From 5683d19a4c4f719b1c8b74bd926caecd001868e0 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Fri, 27 Jan 2023 09:49:05 +0100 Subject: [PATCH 02/20] Refactor build steps to generate blueprints instead The blueprint is generated at the root of the repository and is intended to be versioned with the rest. It acts as a business card that contains many practical information. There's a variety of tools we can then build on top of open-source contracts. And, quite importantly, the blueprint is language-agnostic; it isn't specific to Aiken. So it is really meant as an interop format within the ecosystem. --- crates/aiken-lang/src/builder.rs | 2 +- crates/aiken-lang/src/uplc.rs | 26 +- crates/aiken-project/src/blueprint.rs | 191 ---------- crates/aiken-project/src/blueprint/error.rs | 59 +++ crates/aiken-project/src/blueprint/mod.rs | 141 +++++++ crates/aiken-project/src/blueprint/schema.rs | 353 ++++++++++++++++++ .../aiken-project/src/blueprint/validator.rs | 121 ++++++ crates/aiken-project/src/error.rs | 16 +- crates/aiken-project/src/lib.rs | 341 +++-------------- crates/aiken-project/src/module.rs | 89 ++++- crates/aiken-project/src/telemetry.rs | 5 +- crates/aiken/src/lib.rs | 8 +- 12 files changed, 849 insertions(+), 503 deletions(-) delete mode 100644 crates/aiken-project/src/blueprint.rs create mode 100644 crates/aiken-project/src/blueprint/error.rs create mode 100644 crates/aiken-project/src/blueprint/mod.rs create mode 100644 crates/aiken-project/src/blueprint/schema.rs create mode 100644 crates/aiken-project/src/blueprint/validator.rs diff --git a/crates/aiken-lang/src/builder.rs b/crates/aiken-lang/src/builder.rs index 4f185a14..39c2385b 100644 --- a/crates/aiken-lang/src/builder.rs +++ b/crates/aiken-lang/src/builder.rs @@ -988,7 +988,7 @@ pub fn convert_constants_to_data(constants: Vec) -> Vec, arguments: Vec) -> Term { +pub fn wrap_validator_args(term: Term, arguments: &[TypedArg]) -> Term { let mut term = term; for arg in arguments.iter().rev() { if !matches!(arg.tipo.get_uplc_type(), UplcType::Data) { diff --git a/crates/aiken-lang/src/uplc.rs b/crates/aiken-lang/src/uplc.rs index 8ec8895e..806dee1e 100644 --- a/crates/aiken-lang/src/uplc.rs +++ b/crates/aiken-lang/src/uplc.rs @@ -39,11 +39,11 @@ use crate::{ IdGenerator, }; +#[derive(Clone)] pub struct CodeGenerator<'a> { defined_functions: IndexMap, - functions: &'a IndexMap, - // type_aliases: &'a IndexMap<(String, String), &'a TypeAlias>>, - data_types: &'a IndexMap, + functions: IndexMap, + data_types: IndexMap, module_types: &'a IndexMap, id_gen: IdGenerator, needs_field_access: bool, @@ -53,15 +53,13 @@ pub struct CodeGenerator<'a> { impl<'a> CodeGenerator<'a> { pub fn new( - functions: &'a IndexMap, - // type_aliases: &'a IndexMap<(String, String), &'a TypeAlias>>, - data_types: &'a IndexMap, + functions: IndexMap, + data_types: IndexMap, module_types: &'a IndexMap, ) -> Self { CodeGenerator { defined_functions: IndexMap::new(), functions, - // type_aliases, data_types, module_types, id_gen: IdGenerator::new(), @@ -73,14 +71,14 @@ impl<'a> CodeGenerator<'a> { pub fn generate( &mut self, - body: TypedExpr, - arguments: Vec, + body: &TypedExpr, + arguments: &[TypedArg], wrap_as_validator: bool, ) -> Program { let mut ir_stack = vec![]; let scope = vec![self.id_gen.next()]; - self.build_ir(&body, &mut ir_stack, scope); + self.build_ir(body, &mut ir_stack, scope); self.define_ir(&mut ir_stack); @@ -2865,7 +2863,7 @@ impl<'a> CodeGenerator<'a> { variant_name: String::new(), }; - let function = self.functions.get(&non_variant_function_key).unwrap(); + let function = *self.functions.get(&non_variant_function_key).unwrap(); let mut func_ir = vec![]; @@ -3355,7 +3353,7 @@ impl<'a> CodeGenerator<'a> { count, scope, } => { - if check_replaceable_opaque_type(&tipo, self.data_types) { + if check_replaceable_opaque_type(&tipo, &self.data_types) { indices_to_remove.push(index); } else { let mut replaced_type = tipo.clone(); @@ -3377,7 +3375,7 @@ impl<'a> CodeGenerator<'a> { let record = ir_stack[index + 1].clone(); let record_type = record.tipo(); if let Some(record_type) = record_type { - if check_replaceable_opaque_type(&record_type, self.data_types) { + if check_replaceable_opaque_type(&record_type, &self.data_types) { indices_to_remove.push(index); } else { let mut replaced_type = tipo.clone(); @@ -3408,7 +3406,7 @@ impl<'a> CodeGenerator<'a> { let record = ir_stack[index + 1].clone(); let record_type = record.tipo(); if let Some(record_type) = record_type { - if check_replaceable_opaque_type(&record_type, self.data_types) { + if check_replaceable_opaque_type(&record_type, &self.data_types) { ir_stack[index] = Air::Let { scope, name: indices[0].1.clone(), diff --git a/crates/aiken-project/src/blueprint.rs b/crates/aiken-project/src/blueprint.rs deleted file mode 100644 index 9a4c35ef..00000000 --- a/crates/aiken-project/src/blueprint.rs +++ /dev/null @@ -1,191 +0,0 @@ -use serde::ser::{Serialize, SerializeStruct, Serializer}; -use serde_json; -use std::fmt::{self, Display}; - -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum Schema { - Integer, - Bytes, - List(Item>), - Map((Box, Box)), - Constructor(usize, Vec), -} - -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(&self, serializer: S) -> Result { - match self { - 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::List(items) => { - let mut s = serializer.serialize_struct("List", 2)?; - s.serialize_field("dataType", "list")?; - s.serialize_field("items", &items)?; - s.end() - } - Schema::Map(elements) => { - let mut s = serializer.serialize_struct("Map", 2)?; - s.serialize_field("dataType", "map")?; - s.serialize_field("elements", &elements)?; - s.end() - } - _ => { - todo!() - } - } - } -} - -// Represent a items list in a JSON schema. Can be either a singleton (i.e. a single schema) when -// all elements in the list are uniform or a list of schemas. -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum Item { - Singleton(T), - Many(Vec), -} - -impl Serialize for Item { - fn serialize(&self, serializer: S) -> Result { - match self { - Item::Singleton(elem) => Serialize::serialize(elem, serializer), - Item::Many(elems) => Serialize::serialize(elems, serializer), - } - } -} - -pub mod test { - use super::*; - #[allow(unused_imports)] - use serde_json::{self, json, Value}; - - pub fn assert_json(schema: &Schema, expected: Value) { - assert_eq!(serde_json::to_value(schema).unwrap(), expected); - } - - #[test] - fn serialize_integer() { - let schema = Schema::Integer; - assert_json( - &schema, - json!({ - "dataType": "integer" - }), - ); - } - - #[test] - fn serialize_bytes() { - let schema = Schema::Bytes; - assert_json( - &schema, - json!({ - "dataType": "bytes" - }), - ); - } - - #[test] - fn serialize_list_1() { - let schema = Schema::List(Item::Many(vec![])); - assert_json( - &schema, - json!({ - "dataType": "list", - "items": [] - }), - ); - } - - #[test] - fn serialize_list_2() { - let schema = Schema::List(Item::Singleton(Box::new(Schema::Integer))); - assert_json( - &schema, - json!({ - "dataType": "list", - "items": { - "dataType": "integer" - } - }), - ); - } - - #[test] - fn serialize_list_3() { - let schema = Schema::List(Item::Many(vec![ - Box::new(Schema::Bytes), - Box::new(Schema::List(Item::Singleton(Box::new(Schema::Integer)))), - ])); - assert_json( - &schema, - json!({ - "dataType": "list", - "items": [ - { - "dataType": "bytes" - }, - { - "dataType": "list", - "items": { "dataType": "integer" } - } - ] - }), - ); - } - - #[test] - fn serialize_map_1() { - let schema = Schema::Map((Box::new(Schema::Integer), Box::new(Schema::Bytes))); - assert_json( - &schema, - json!({ - "dataType": "map", - "elements": [ - { - "dataType": "integer" - }, - { - "dataType": "bytes" - } - ] - }), - ) - } - - #[test] - fn serialize_map_2() { - let schema = Schema::Map(( - Box::new(Schema::Bytes), - Box::new(Schema::List(Item::Singleton(Box::new(Schema::Integer)))), - )); - assert_json( - &schema, - json!({ - "dataType": "map", - "elements": [ - { - "dataType": "bytes" - }, - { - "dataType": "list", - "items": { "dataType": "integer" } - } - ] - }), - ) - } -} diff --git a/crates/aiken-project/src/blueprint/error.rs b/crates/aiken-project/src/blueprint/error.rs new file mode 100644 index 00000000..16af5caa --- /dev/null +++ b/crates/aiken-project/src/blueprint/error.rs @@ -0,0 +1,59 @@ +use super::schema; +use crate::module::CheckedModule; +use aiken_lang::ast::{Span, TypedFunction}; +use miette::{Diagnostic, NamedSource}; +use std::{fmt::Debug, path::PathBuf}; + +#[derive(Debug, thiserror::Error, Diagnostic)] +pub enum Error { + #[error("Validator functions must return Bool")] + ValidatorMustReturnBool { + path: PathBuf, + src: String, + named: NamedSource, + location: Span, + }, + #[error("Validator\n\n{name}\n\nrequires at least {at_least} arguments")] + WrongValidatorArity { + name: String, + at_least: u8, + location: Span, + path: PathBuf, + src: String, + named: NamedSource, + }, + #[error(transparent)] + Schema(schema::Error), +} + +pub fn assert_return_bool(module: &CheckedModule, def: &TypedFunction) -> Result<(), Error> { + if !def.return_type.is_bool() { + Err(Error::ValidatorMustReturnBool { + location: def.location, + src: module.code.clone(), + path: module.input_path.clone(), + named: NamedSource::new(module.input_path.display().to_string(), module.code.clone()), + }) + } else { + Ok(()) + } +} + +pub fn assert_min_arity( + module: &CheckedModule, + def: &TypedFunction, + at_least: u8, +) -> 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, + }) + } else { + Ok(()) + } +} diff --git a/crates/aiken-project/src/blueprint/mod.rs b/crates/aiken-project/src/blueprint/mod.rs new file mode 100644 index 00000000..6347ebb9 --- /dev/null +++ b/crates/aiken-project/src/blueprint/mod.rs @@ -0,0 +1,141 @@ +pub mod error; +pub mod schema; +pub mod validator; + +use crate::{config::Config, module::CheckedModules}; +use aiken_lang::uplc::CodeGenerator; +use error::*; +use schema::Schema; +use std::fmt::Debug; +use validator::{Purpose, Validator}; + +#[derive(Debug, PartialEq, Clone, serde::Serialize)] +pub struct Blueprint { + pub preamble: Preamble, + pub validators: Vec, +} + +impl Blueprint { + pub fn new( + config: &Config, + modules: &CheckedModules, + generator: &mut CodeGenerator, + ) -> Result { + let mut validators = Vec::new(); + + for (validator, def) in modules.validators() { + let purpose: Purpose = def.name.clone().into(); + + assert_return_bool(validator, def)?; + assert_min_arity(validator, def, purpose.min_arity())?; + + let mut args = def.arguments.iter().rev(); + let (_, redeemer, datum) = (args.next(), args.next().unwrap(), args.next()); + + validators.push(Validator { + description: None, + purpose, + datum: datum + .map(|datum| { + Schema::from_type(modules.into(), &datum.arg_name.get_label(), &datum.tipo) + .map_err(Error::Schema) + }) + .transpose()?, + redeemer: Schema::from_type( + modules.into(), + &redeemer.arg_name.get_label(), + &redeemer.tipo, + ) + .map_err(Error::Schema)?, + program: generator + .generate(&def.body, &def.arguments, true) + .try_into() + .unwrap(), + }); + } + + Ok(Blueprint { + preamble: Preamble::from_config(config), + validators, + }) + } +} + +#[derive(Debug, PartialEq, Clone, serde::Serialize)] +pub struct Preamble { + pub title: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + pub version: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub license: Option, +} + +impl Preamble { + pub fn from_config(config: &Config) -> Self { + Preamble { + title: config.name.to_string(), + description: if config.description.is_empty() { + None + } else { + Some(config.description.clone()) + }, + version: config.version.clone(), + license: config.license.clone(), + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use serde_json::{self, json}; + + #[test] + fn serialize_no_description() { + let blueprint = Blueprint { + preamble: Preamble { + title: "Foo".to_string(), + description: None, + version: "1.0.0".to_string(), + license: Some("Apache-2.0".to_string()), + }, + validators: vec![], + }; + assert_eq!( + serde_json::to_value(&blueprint).unwrap(), + json!({ + "preamble": { + "title": "Foo", + "version": "1.0.0", + "license": "Apache-2.0" + }, + "validators": [] + }), + ); + } + + #[test] + fn serialize_with_description() { + let blueprint = Blueprint { + preamble: Preamble { + title: "Foo".to_string(), + description: Some("Lorem ipsum".to_string()), + version: "1.0.0".to_string(), + license: None, + }, + validators: vec![], + }; + assert_eq!( + serde_json::to_value(&blueprint).unwrap(), + json!({ + "preamble": { + "title": "Foo", + "description": "Lorem ipsum", + "version": "1.0.0", + }, + "validators": [] + }), + ); + } +} diff --git a/crates/aiken-project/src/blueprint/schema.rs b/crates/aiken-project/src/blueprint/schema.rs new file mode 100644 index 00000000..54a3578f --- /dev/null +++ b/crates/aiken-project/src/blueprint/schema.rs @@ -0,0 +1,353 @@ +use crate::CheckedModule; +use aiken_lang::{ + ast::{DataType, Definition, TypedDefinition}, + tipo::Type, +}; +use miette::Diagnostic; +use serde::ser::{Serialize, SerializeStruct, Serializer}; +use serde_json; +use std::{ + collections::HashMap, + fmt::{self, Display}, + sync::Arc, +}; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum Schema { + Integer, + Bytes, + List(Item>), + Map((Box, Box)), + AnyOf(Vec), +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Constructor { + pub index: usize, + pub fields: Vec, +} + +impl Schema { + pub fn from_type( + modules: &HashMap, + name: &str, + type_info: &Type, + ) -> Result { + match type_info { + Type::App { + module: module_name, + name: type_name, + .. + } if module_name.is_empty() => match &type_name[..] { + "ByteArray" => Ok(Schema::Bytes), + "Integer" => Ok(Schema::Integer), + _ => Err(Error::UnsupportedPrimitiveType { + type_name: type_name.clone(), + }), + }, + Type::App { + module: module_name, + name: type_name, + .. + } => { + let module = modules.get(module_name).unwrap(); + let constructor = find_definition(type_name, &module.ast.definitions).unwrap(); + Self::from_data_type(modules, constructor) + } + Type::Fn { .. } | Type::Var { .. } | Type::Tuple { .. } => { + Err(Error::UnsupportedKind { + arg_or_field_name: name.to_string(), + type_info: type_info.clone(), + }) + } + } + } + + pub fn from_data_type( + modules: &HashMap, + data_type: &DataType>, + ) -> Result { + let mut variants = vec![]; + for (index, constructor) in data_type.constructors.iter().enumerate() { + let mut fields = vec![]; + for field in constructor.arguments.iter() { + fields.push(Schema::from_type( + modules, + &field.label.clone().unwrap_or_default(), + &field.tipo, + )?); + } + variants.push(Constructor { index, fields }); + } + Ok(Schema::AnyOf(variants)) + } +} + +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(&self, serializer: S) -> Result { + match self { + 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::List(items) => { + let mut s = serializer.serialize_struct("List", 2)?; + s.serialize_field("dataType", "list")?; + s.serialize_field("items", &items)?; + s.end() + } + Schema::Map(elements) => { + let mut s = serializer.serialize_struct("Map", 2)?; + s.serialize_field("dataType", "map")?; + s.serialize_field("elements", &elements)?; + s.end() + } + Schema::AnyOf(constructors) => 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(&self, serializer: S) -> Result { + 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() + } +} + +// Represent a items list in a JSON schema. Can be either a singleton (i.e. a single schema) when +// all elements in the list are uniform or a list of schemas. +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum Item { + Singleton(T), + Many(Vec), +} + +impl Serialize for Item { + fn serialize(&self, serializer: S) -> Result { + match self { + Item::Singleton(elem) => Serialize::serialize(elem, serializer), + Item::Many(elems) => Serialize::serialize(elems, serializer), + } + } +} + +#[derive(Debug, PartialEq, Clone, thiserror::Error, Diagnostic)] +pub enum Error { + #[error("I stumble upon an unsupported kind in a datum or redeemer definition.\n")] + UnsupportedKind { + arg_or_field_name: String, + type_info: Type, + }, + #[error("I discovered an unexpected primitive in a datum or redeemer definition.\n")] + UnsupportedPrimitiveType { type_name: String }, +} + +fn find_definition<'a>( + name: &str, + definitions: &'a Vec, +) -> Option<&'a DataType>> { + 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: &Schema, expected: Value) { + assert_eq!(serde_json::to_value(schema).unwrap(), expected); + } + + #[test] + fn serialize_integer() { + let schema = Schema::Integer; + assert_json( + &schema, + json!({ + "dataType": "integer" + }), + ); + } + + #[test] + fn serialize_bytes() { + let schema = Schema::Bytes; + assert_json( + &schema, + json!({ + "dataType": "bytes" + }), + ); + } + + #[test] + fn serialize_list_1() { + let schema = Schema::List(Item::Many(vec![])); + assert_json( + &schema, + json!({ + "dataType": "list", + "items": [] + }), + ); + } + + #[test] + fn serialize_list_2() { + let schema = Schema::List(Item::Singleton(Box::new(Schema::Integer))); + assert_json( + &schema, + json!({ + "dataType": "list", + "items": { + "dataType": "integer" + } + }), + ); + } + + #[test] + fn serialize_list_3() { + let schema = Schema::List(Item::Many(vec![ + Box::new(Schema::Bytes), + Box::new(Schema::List(Item::Singleton(Box::new(Schema::Integer)))), + ])); + assert_json( + &schema, + json!({ + "dataType": "list", + "items": [ + { + "dataType": "bytes" + }, + { + "dataType": "list", + "items": { "dataType": "integer" } + } + ] + }), + ); + } + + #[test] + fn serialize_map_1() { + let schema = Schema::Map((Box::new(Schema::Integer), Box::new(Schema::Bytes))); + assert_json( + &schema, + json!({ + "dataType": "map", + "elements": [ + { + "dataType": "integer" + }, + { + "dataType": "bytes" + } + ] + }), + ) + } + + #[test] + fn serialize_map_2() { + let schema = Schema::Map(( + Box::new(Schema::Bytes), + Box::new(Schema::List(Item::Singleton(Box::new(Schema::Integer)))), + )); + assert_json( + &schema, + json!({ + "dataType": "map", + "elements": [ + { + "dataType": "bytes" + }, + { + "dataType": "list", + "items": { "dataType": "integer" } + } + ] + }), + ) + } + + #[test] + fn serialize_constr_1() { + let schema = Schema::AnyOf(vec![Constructor { + index: 0, + fields: vec![], + }]); + assert_json( + &schema, + json!({ + "dataType": "constructor", + "index": 0, + "fields": [] + }), + ) + } + + #[test] + fn serialize_constr_2() { + let schema = Schema::AnyOf(vec![ + Constructor { + index: 0, + fields: vec![Schema::Integer], + }, + Constructor { + index: 1, + fields: vec![Schema::Bytes], + }, + ]); + assert_json( + &schema, + json!({ + "anyOf": [ + { + "dataType": "constructor", + "index": 0, + "fields": [{ "dataType": "integer" }] + }, + { + "dataType": "constructor", + "index": 1, + "fields": [{ "dataType": "bytes" }] + } + ] + }), + ) + } +} diff --git a/crates/aiken-project/src/blueprint/validator.rs b/crates/aiken-project/src/blueprint/validator.rs new file mode 100644 index 00000000..f3dc0b30 --- /dev/null +++ b/crates/aiken-project/src/blueprint/validator.rs @@ -0,0 +1,121 @@ +use super::schema::Schema; +use pallas::ledger::primitives::babbage as cardano; +use pallas_traverse::ComputeHash; +use serde::{ + self, + ser::{Serialize, SerializeStruct, Serializer}, +}; +use std::fmt::{self, Display}; +use uplc::ast::{NamedDeBruijn, Program}; + +#[derive(Debug, PartialEq, Clone)] +pub struct Validator { + pub purpose: Purpose, + pub description: Option, + pub datum: Option, + pub redeemer: Schema, + pub program: Program, +} + +impl Serialize for Validator { + fn serialize(&self, serializer: S) -> Result { + let cbor = self.program.to_cbor().unwrap(); + let source_code = hex::encode(&cbor); + let mut s = serializer.serialize_struct("Validator", 5)?; + s.serialize_field("purpose", &self.purpose)?; + let hash = cardano::PlutusV2Script(cbor.into()).compute_hash(); + s.serialize_field("hash", &hash)?; + if let Some { .. } = self.description { + s.serialize_field("description", &self.description)?; + } + if let Some { .. } = self.datum { + s.serialize_field("datum", &self.datum)?; + } + s.serialize_field("redeemer", &self.redeemer)?; + s.serialize_field("compiledCode", &source_code)?; + s.end() + } +} + +#[derive(Debug, PartialEq, Clone, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub enum Purpose { + Spend, + Mint, + Withdraw, + Publish, +} + +impl Purpose { + pub fn min_arity(&self) -> u8 { + match self { + Purpose::Spend => 3, + Purpose::Mint | Purpose::Withdraw | Purpose::Publish => 2, + } + } +} + +impl From for Purpose { + fn from(purpose: String) -> Purpose { + match &purpose[..] { + "spend" => Purpose::Spend, + "mint" => Purpose::Mint, + "withdraw" => Purpose::Withdraw, + "publish" => Purpose::Publish, + unexpected => panic!("Can't turn '{}' into any Purpose", unexpected), + } + } +} + +impl Display for Purpose { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(match self { + Purpose::Spend => "spend", + Purpose::Mint => "mint", + Purpose::Withdraw => "withdraw", + Purpose::Publish => "publish", + }) + } +} + +#[cfg(test)] +mod test { + use super::super::schema::Constructor; + use super::*; + use serde_json::{self, json}; + use uplc::parser; + + #[test] + fn serialize() { + let program = parser::program("(program 1.0.0 (con integer 42))") + .unwrap() + .try_into() + .unwrap(); + let validator = Validator { + description: Some("Lorem ipsum".to_string()), + purpose: Purpose::Spend, + datum: None, + redeemer: Schema::AnyOf(vec![Constructor { + index: 0, + fields: vec![Schema::Bytes], + }]), + program, + }; + assert_eq!( + serde_json::to_value(&validator).unwrap(), + json!({ + "description": "Lorem ipsum", + "purpose": "spend", + "redeemer": { + "dataType": "constructor", + "index": 0, + "fields": [{ + "dataType": "bytes" + }] + }, + "compiledCode": "46010000481501", + "hash": "27dc8e44c17b4ae5f4b9286ab599fffe70e61b49dec61eaca1fc5898" + }), + ); + } +} diff --git a/crates/aiken-project/src/error.rs b/crates/aiken-project/src/error.rs index 988d275a..94678712 100644 --- a/crates/aiken-project/src/error.rs +++ b/crates/aiken-project/src/error.rs @@ -1,4 +1,7 @@ -use crate::{deps::manifest::Package, package_name::PackageName, pretty, script::EvalHint}; +use crate::{ + blueprint::error as blueprint, deps::manifest::Package, package_name::PackageName, pretty, + script::EvalHint, +}; use aiken_lang::{ ast::{BinOp, Span}, parser::error::ParseError, @@ -31,6 +34,9 @@ pub enum Error { #[error("I found some files with incorrectly formatted source code.")] Format { problem_files: Vec }, + #[error(transparent)] + Blueprint(#[from] blueprint::Error), + #[error(transparent)] StandardIo(#[from] io::Error), @@ -183,6 +189,7 @@ impl Error { Error::FileIo { .. } => None, Error::Format { .. } => None, Error::StandardIo(_) => None, + Error::Blueprint(_) => None, Error::MissingManifest { path } => Some(path.to_path_buf()), Error::TomlLoading { path, .. } => Some(path.to_path_buf()), Error::ImportCycle { .. } => None, @@ -205,6 +212,7 @@ impl Error { Error::FileIo { .. } => None, Error::Format { .. } => None, Error::StandardIo(_) => None, + Error::Blueprint(_) => None, Error::MissingManifest { .. } => None, Error::TomlLoading { src, .. } => Some(src.to_string()), Error::ImportCycle { .. } => None, @@ -250,6 +258,7 @@ impl Diagnostic for Error { match self { Error::DuplicateModule { .. } => Some(Box::new("aiken::module::duplicate")), Error::FileIo { .. } => None, + Error::Blueprint(e) => e.code(), Error::ImportCycle { .. } => Some(Box::new("aiken::module::cyclical")), Error::List(_) => None, Error::Parse { .. } => Some(Box::new("aiken::parser")), @@ -279,6 +288,7 @@ impl Diagnostic for Error { second.display() ))), Error::FileIo { .. } => None, + Error::Blueprint(e) => e.help(), Error::ImportCycle { modules } => Some(Box::new(format!( "Try moving the shared code to a separate module that the others can depend on\n- {}", modules.join("\n- ") @@ -332,6 +342,7 @@ impl Diagnostic for Error { Error::DuplicateModule { .. } => None, Error::FileIo { .. } => None, Error::ImportCycle { .. } => None, + Error::Blueprint(e) => e.labels(), Error::List(_) => None, Error::Parse { error, .. } => error.labels(), Error::MissingManifest { .. } => None, @@ -366,6 +377,7 @@ impl Diagnostic for Error { Error::DuplicateModule { .. } => None, Error::FileIo { .. } => None, Error::ImportCycle { .. } => None, + Error::Blueprint(e) => e.source_code(), Error::List(_) => None, Error::Parse { named, .. } => Some(named), Error::Type { named, .. } => Some(named), @@ -388,6 +400,7 @@ impl Diagnostic for Error { Error::DuplicateModule { .. } => None, Error::FileIo { .. } => None, Error::ImportCycle { .. } => None, + Error::Blueprint(e) => e.url(), Error::List { .. } => None, Error::Parse { .. } => None, Error::Type { error, .. } => error.url(), @@ -409,6 +422,7 @@ impl Diagnostic for Error { match self { Error::DuplicateModule { .. } => None, Error::FileIo { .. } => None, + Error::Blueprint(e) => e.related(), Error::ImportCycle { .. } => None, Error::List { .. } => None, Error::Parse { .. } => None, diff --git a/crates/aiken-project/src/lib.rs b/crates/aiken-project/src/lib.rs index fd23d11e..0cd67678 100644 --- a/crates/aiken-project/src/lib.rs +++ b/crates/aiken-project/src/lib.rs @@ -12,26 +12,21 @@ pub mod pretty; pub mod script; pub mod telemetry; +use crate::blueprint::Blueprint; use aiken_lang::{ ast::{Definition, Function, ModuleKind, TypedDataType, TypedDefinition, TypedFunction}, builder::{DataTypeKey, FunctionAccessKey}, builtins::{self, generic_var}, tipo::TypeInfo, uplc::CodeGenerator, - IdGenerator, MINT, PUBLISH, SPEND, VALIDATOR_NAMES, WITHDRAW, + IdGenerator, }; use deps::UseManifest; use indexmap::IndexMap; use miette::NamedSource; use options::{CodeGenMode, Options}; use package_name::PackageName; -use pallas::{ - codec::minicbor, - ledger::{addresses::Address, primitives::babbage}, -}; -use pallas_traverse::ComputeHash; use script::{EvalHint, EvalInfo, Script}; -use serde_json::json; use std::{ collections::HashMap, fs, @@ -39,7 +34,7 @@ use std::{ }; use telemetry::EventListener; use uplc::{ - ast::{Constant, DeBruijn, Program, Term}, + ast::{Constant, Term}, machine::cost_model::ExBudget, }; @@ -71,6 +66,8 @@ where sources: Vec, pub warnings: Vec, event_listener: T, + functions: HashMap, + data_types: HashMap, } impl Project @@ -85,6 +82,21 @@ where module_types.insert("aiken".to_string(), builtins::prelude(&id_gen)); module_types.insert("aiken/builtin".to_string(), builtins::plutus(&id_gen)); + let mut functions = HashMap::new(); + for (access_key, func) in builtins::prelude_functions(&id_gen).into_iter() { + functions.insert(access_key.to_owned(), func); + } + + let mut data_types = HashMap::new(); + let option_data_type = TypedDataType::option(generic_var(id_gen.next())); + data_types.insert( + DataTypeKey { + module_name: "".to_string(), + defined_type: "Option".to_string(), + }, + option_data_type, + ); + let config = Config::load(&root)?; Ok(Project { @@ -97,6 +109,8 @@ where sources: vec![], warnings: vec![], event_listener, + functions, + data_types, }) } @@ -187,19 +201,43 @@ where self.type_check(parsed_modules)?; - let validators = self.validate_validators()?; - match options.code_gen_mode { CodeGenMode::Build(uplc_dump) => { - if validators.is_empty() { + let blueprint_path = self.root.join("plutus.json"); + self.event_listener + .handle_event(Event::GeneratingBlueprint { + path: blueprint_path.clone(), + }); + + let mut generator = self.checked_modules.new_generator( + &self.functions, + &self.data_types, + &self.module_types, + ); + + let blueprint = Blueprint::new(&self.config, &self.checked_modules, &mut generator) + .map_err(Error::Blueprint)?; + + if blueprint.validators.is_empty() { self.warnings.push(Warning::NoValidators); } - let programs = self.code_gen(validators)?; + if uplc_dump { + let dir = self.root.join("artifacts"); + fs::create_dir_all(&dir)?; + for validator in &blueprint.validators { + // TODO: Also include validator name. + let path = dir.clone().join(format!("{}.uplc", validator.purpose)); + fs::write(&path, validator.program.to_pretty()) + .map_err(|error| Error::FileIo { error, path })?; + } + } - self.write_build_outputs(programs, uplc_dump)?; - - Ok(()) + let json = serde_json::to_string_pretty(&blueprint).unwrap(); + fs::write(&blueprint_path, json).map_err(|error| Error::FileIo { + error, + path: blueprint_path, + }) } CodeGenMode::Test { match_tests, @@ -409,170 +447,6 @@ where Ok(()) } - fn validate_validators(&self) -> Result, Error> { - let mut errors = Vec::new(); - let mut validators = Vec::new(); - - for module in self.checked_modules.validators() { - for def in module.ast.definitions() { - if let Definition::Fn(func_def) = def { - if VALIDATOR_NAMES.contains(&func_def.name.as_str()) { - // validators must return a Bool - if !func_def.return_type.is_bool() { - errors.push(Error::ValidatorMustReturnBool { - location: func_def.location, - src: module.code.clone(), - path: module.input_path.clone(), - named: NamedSource::new( - module.input_path.display().to_string(), - module.code.clone(), - ), - }) - } - - // depending on name, validate the minimum number of arguments - // if too low, push a new error on to errors - if [MINT, WITHDRAW, PUBLISH].contains(&func_def.name.as_str()) - && func_def.arguments.len() < 2 - { - errors.push(Error::WrongValidatorArity { - location: func_def.location, - src: module.code.clone(), - path: module.input_path.clone(), - named: NamedSource::new( - module.input_path.display().to_string(), - module.code.clone(), - ), - name: func_def.name.clone(), - at_least: 2, - }) - } - - if SPEND == func_def.name && func_def.arguments.len() < 3 { - errors.push(Error::WrongValidatorArity { - location: func_def.location, - src: module.code.clone(), - path: module.input_path.clone(), - named: NamedSource::new( - module.input_path.display().to_string(), - module.code.clone(), - ), - name: func_def.name.clone(), - at_least: 3, - }) - } - - validators.push(( - module.input_path.clone(), - module.name.clone(), - func_def.clone(), - )); - } - } - } - } - - if errors.is_empty() { - Ok(validators) - } else { - Err(Error::List(errors)) - } - } - - fn code_gen( - &mut self, - validators: Vec<(PathBuf, String, TypedFunction)>, - ) -> Result, Error> { - let mut programs = Vec::new(); - let mut functions = IndexMap::new(); - let mut type_aliases = IndexMap::new(); - let mut data_types = IndexMap::new(); - - let prelude_functions = builtins::prelude_functions(&self.id_gen); - for (access_key, func) in prelude_functions.iter() { - functions.insert(access_key.clone(), func); - } - - let option_data_type = TypedDataType::option(generic_var(self.id_gen.next())); - data_types.insert( - DataTypeKey { - module_name: "".to_string(), - defined_type: "Option".to_string(), - }, - &option_data_type, - ); - - for module in self.checked_modules.values() { - for def in module.ast.definitions() { - match def { - Definition::Fn(func) => { - functions.insert( - FunctionAccessKey { - module_name: module.name.clone(), - function_name: func.name.clone(), - variant_name: String::new(), - }, - func, - ); - } - Definition::TypeAlias(ta) => { - type_aliases.insert((module.name.clone(), ta.alias.clone()), ta); - } - Definition::DataType(dt) => { - data_types.insert( - DataTypeKey { - module_name: module.name.clone(), - defined_type: dt.name.clone(), - }, - dt, - ); - } - - Definition::ModuleConstant(_) | Definition::Test(_) | Definition::Use(_) => {} - } - } - } - - for (input_path, module_name, func_def) in validators { - let Function { - arguments, - name, - body, - .. - } = func_def; - - let mut modules_map = IndexMap::new(); - - modules_map.extend(self.module_types.clone()); - - let mut generator = CodeGenerator::new( - &functions, - // &type_aliases, - &data_types, - &modules_map, - ); - - self.event_listener.handle_event(Event::GeneratingUPLC { - output_path: self.output_path().join(&module_name).join(&name), - name: format!("{}.{}", module_name, name), - }); - - let program = generator.generate(body, arguments, true); - - let script = Script::new( - input_path, - module_name, - name, - program.try_into().unwrap(), - None, - ); - - programs.push(script); - } - - Ok(programs) - } - fn collect_scripts( &mut self, verbose: bool, @@ -654,26 +528,20 @@ where }) } - let mut modules_map = IndexMap::new(); - - modules_map.extend(self.module_types.clone()); - - let mut generator = CodeGenerator::new( - &functions, - // &type_aliases, - &data_types, - &modules_map, - ); + let mut generator = + CodeGenerator::new(functions.clone(), data_types.clone(), &self.module_types); let evaluation_hint = if let Some((bin_op, left_src, right_src)) = func_def.test_hint() { - let left = CodeGenerator::new(&functions, &data_types, &modules_map) - .generate(*left_src, vec![], false) + let left = generator + .clone() + .generate(&left_src, &[], false) .try_into() .unwrap(); - let right = CodeGenerator::new(&functions, &data_types, &modules_map) - .generate(*right_src, vec![], false) + let right = generator + .clone() + .generate(&right_src, &[], false) .try_into() .unwrap(); @@ -686,7 +554,7 @@ where None }; - let program = generator.generate(body.clone(), arguments.clone(), false); + let program = generator.generate(body, arguments, false); let script = Script::new( input_path, @@ -786,91 +654,6 @@ where .collect() } - fn output_path(&self) -> PathBuf { - self.root.join("assets") - } - - fn write_build_outputs(&self, programs: Vec