diff --git a/Cargo.lock b/Cargo.lock index 1c243db9..5ea5fdb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,6 +112,7 @@ version = "0.0.28" dependencies = [ "aiken-lang", "askama", + "assert-json-diff", "dirs", "fslock", "futures", @@ -197,6 +198,16 @@ dependencies = [ "toml", ] +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "atty" version = "0.2.14" diff --git a/crates/aiken-lang/src/builtins.rs b/crates/aiken-lang/src/builtins.rs index 9a094a6b..3d426d8b 100644 --- a/crates/aiken-lang/src/builtins.rs +++ b/crates/aiken-lang/src/builtins.rs @@ -5,8 +5,8 @@ use strum::IntoEnumIterator; use uplc::builtins::DefaultFunction; use crate::{ - ast::{Arg, ArgName, CallArg, Function, ModuleKind, Span, TypedFunction, UnOp}, - builder::FunctionAccessKey, + ast::{Arg, ArgName, CallArg, Function, ModuleKind, Span, TypedDataType, TypedFunction, UnOp}, + builder::{DataTypeKey, FunctionAccessKey}, expr::TypedExpr, tipo::{ fields::FieldMap, Type, TypeConstructor, TypeInfo, TypeVar, ValueConstructor, @@ -800,6 +800,22 @@ pub fn prelude_functions(id_gen: &IdGenerator) -> HashMap HashMap { + let mut data_types = HashMap::new(); + + // Option + 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, + ); + + data_types +} + pub fn int() -> Arc { Arc::new(Type::App { public: true, diff --git a/crates/aiken-project/Cargo.toml b/crates/aiken-project/Cargo.toml index 5bc164e9..b2dcd942 100644 --- a/crates/aiken-project/Cargo.toml +++ b/crates/aiken-project/Cargo.toml @@ -11,6 +11,7 @@ authors = ["Lucas Rosa ", "Kasey White "] [dependencies] aiken-lang = { path = "../aiken-lang", version = "0.0.28" } askama = "0.10.5" +assert-json-diff = "2.0.2" dirs = "4.0.0" fslock = "0.2.1" futures = "0.3.25" diff --git a/crates/aiken-project/src/blueprint/mod.rs b/crates/aiken-project/src/blueprint/mod.rs index 10434b88..1557edab 100644 --- a/crates/aiken-project/src/blueprint/mod.rs +++ b/crates/aiken-project/src/blueprint/mod.rs @@ -5,7 +5,7 @@ pub mod validator; use crate::{config::Config, module::CheckedModules}; use aiken_lang::uplc::CodeGenerator; use error::Error; -use std::fmt::Debug; +use std::fmt::{self, Debug, Display}; use validator::Validator; #[derive(Debug, PartialEq, Clone, serde::Serialize)] @@ -14,6 +14,16 @@ pub struct Blueprint { pub validators: Vec, } +#[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 Blueprint { pub fn new( config: &Config, @@ -36,14 +46,11 @@ impl Blueprint { } } -#[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 Display for Blueprint { + 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 Preamble { diff --git a/crates/aiken-project/src/blueprint/validator.rs b/crates/aiken-project/src/blueprint/validator.rs index 00ee99ca..6fc95ba8 100644 --- a/crates/aiken-project/src/blueprint/validator.rs +++ b/crates/aiken-project/src/blueprint/validator.rs @@ -60,6 +60,13 @@ impl Serialize for Validator { } } +impl Display for Validator { + 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 Validator { pub fn from_checked_module( modules: &CheckedModules, @@ -128,51 +135,141 @@ impl From for Purpose { #[cfg(test)] mod test { - use super::super::schema::{Constructor, Data, Schema}; use super::*; + use crate::{module::ParsedModule, PackageName}; + use aiken_lang::{ + self, + ast::{ModuleKind, TypedDataType, TypedFunction}, + builder::{DataTypeKey, FunctionAccessKey}, + builtins, parser, + tipo::TypeInfo, + IdGenerator, + }; + use assert_json_diff::assert_json_eq; use serde_json::{self, json}; - use uplc::parser; + use std::{collections::HashMap, path::PathBuf}; + + // TODO: Possible refactor this out of the module and have it used by `Project`. The idea would + // be to make this struct below the actual project, and wrap it in another metadata struct + // which contains all the config and I/O stuff regarding the project. + struct TestProject { + package: PackageName, + id_gen: IdGenerator, + module_types: HashMap, + functions: HashMap, + data_types: HashMap, + } + + impl TestProject { + fn new() -> Self { + let id_gen = IdGenerator::new(); + + let package = PackageName { + owner: "test".to_owned(), + repo: "project".to_owned(), + }; + + let mut module_types = HashMap::new(); + module_types.insert("aiken".to_string(), builtins::prelude(&id_gen)); + module_types.insert("aiken/builtin".to_string(), builtins::plutus(&id_gen)); + + let functions = builtins::prelude_functions(&id_gen); + let data_types = builtins::prelude_data_types(&id_gen); + + TestProject { + package, + id_gen, + module_types, + functions, + data_types, + } + } + + fn parse(&self, source_code: &str) -> ParsedModule { + let kind = ModuleKind::Validator; + let (ast, extra) = parser::module(source_code, kind).unwrap(); + let mut module = ParsedModule { + kind, + ast, + code: source_code.to_string(), + name: "test".to_owned(), + path: PathBuf::new(), + extra, + package: self.package.to_string(), + }; + module.attach_doc_and_module_comments(); + module + } + + fn check(&self, module: ParsedModule) -> CheckedModule { + let mut warnings = vec![]; + + let ast = module + .ast + .infer( + &self.id_gen, + module.kind, + &self.package.to_string(), + &self.module_types, + &mut warnings, + ) + .unwrap(); + + CheckedModule { + kind: module.kind, + extra: module.extra, + name: module.name, + code: module.code, + package: module.package, + input_path: module.path, + ast, + } + } + } + + fn assert_validator(source_code: &str, json: serde_json::Value) { + let project = TestProject::new(); + + let modules = CheckedModules::singleton(project.check(project.parse(source_code))); + let mut generator = modules.new_generator( + &project.functions, + &project.data_types, + &project.module_types, + ); + + let (validator, def) = modules + .validators() + .next() + .expect("source code did no yield any validator"); + + let validator = + Validator::from_checked_module(&modules, &mut generator, validator, def).unwrap(); + + println!("{}", validator); + assert_json_eq!(serde_json::to_value(&validator).unwrap(), json); + } #[test] - fn serialize() { - let program = parser::program("(program 1.0.0 (con integer 42))") - .unwrap() - .try_into() - .unwrap(); - let validator = Validator { - title: "foo".to_string(), - description: Some("Lorem ipsum".to_string()), - purpose: Purpose::Spend, - datum: None, - redeemer: Annotated { - title: Some("Bar".to_string()), - description: None, - annotated: Schema::Data(Some(Data::AnyOf(vec![Constructor { - index: 0, - fields: vec![Data::Bytes.into()], - } - .into()]))), - }, - program, - }; - assert_eq!( - serde_json::to_value(&validator).unwrap(), + fn validator_1() { + assert_validator( + r#" + fn spend(datum: Data, redeemer: Data, ctx: Data) { + True + } + "#, json!({ - "title": "foo", - "purpose": "spend", - "hash": "27dc8e44c17b4ae5f4b9286ab599fffe70e61b49dec61eaca1fc5898", - "description": "Lorem ipsum", - "redeemer": { - "title": "Bar", - "anyOf": [{ - "dataType": "constructor", - "index": 0, - "fields": [{ - "dataType": "bytes" - }] - }], - }, - "compiledCode": "46010000481501" + "title": "test", + "purpose": "spend", + "hash": "cf2cd3bed32615bfecbd280618c1c1bec2198fc0f72b04f323a8a0d2", + "datum": { + "title": "Data", + "description": "Any Plutus data." + }, + "redeemer": { + "title": "Data", + "description": "Any Plutus data." + }, + "compiledCode": "58250100002105646174756d00210872656465656d657200210363747800533357349445261601" }), ); } diff --git a/crates/aiken-project/src/lib.rs b/crates/aiken-project/src/lib.rs index 2741322b..6bd6cbc5 100644 --- a/crates/aiken-project/src/lib.rs +++ b/crates/aiken-project/src/lib.rs @@ -16,7 +16,7 @@ use crate::blueprint::Blueprint; use aiken_lang::{ ast::{Definition, Function, ModuleKind, TypedDataType, TypedFunction}, builder::{DataTypeKey, FunctionAccessKey}, - builtins::{self, generic_var}, + builtins, tipo::TypeInfo, IdGenerator, }; @@ -81,20 +81,9 @@ 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 functions = builtins::prelude_functions(&id_gen); - 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 data_types = builtins::prelude_data_types(&id_gen); let config = Config::load(&root)?; diff --git a/crates/aiken-project/src/module.rs b/crates/aiken-project/src/module.rs index ae1760d8..c083bc30 100644 --- a/crates/aiken-project/src/module.rs +++ b/crates/aiken-project/src/module.rs @@ -244,6 +244,12 @@ impl<'a> From<&'a CheckedModules> for &'a HashMap { } impl CheckedModules { + pub fn singleton(module: CheckedModule) -> Self { + let mut modules = Self::default(); + modules.insert(module.name.clone(), module); + modules + } + pub fn validators(&self) -> impl Iterator { let mut items = vec![]; for validator in self.0.values().filter(|module| module.kind.is_validator()) {