Allow testing blueprint generation from Aiken programs

This is quite something, because now we have a testing pipeline that
  can also be used for testing other compiler-related stuff such as the
  type-checker or the code generator.
This commit is contained in:
KtorZ 2023-01-28 16:01:50 +01:00
parent b93e14659c
commit d2cc44e5f4
No known key found for this signature in database
GPG Key ID: 33173CB6F77F4277
7 changed files with 192 additions and 65 deletions

11
Cargo.lock generated
View File

@ -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"

View File

@ -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<FunctionAccessKey, Typ
functions
}
pub fn prelude_data_types(id_gen: &IdGenerator) -> HashMap<DataTypeKey, TypedDataType> {
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<Type> {
Arc::new(Type::App {
public: true,

View File

@ -11,6 +11,7 @@ authors = ["Lucas Rosa <x@rvcas.dev>", "Kasey White <kwhitemsg@gmail.com>"]
[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"

View File

@ -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<validator::Validator>,
}
#[derive(Debug, PartialEq, Clone, serde::Serialize)]
pub struct Preamble {
pub title: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub license: Option<String>,
}
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<String>,
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub license: Option<String>,
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 {

View File

@ -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<String> 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<String, TypeInfo>,
functions: HashMap<FunctionAccessKey, TypedFunction>,
data_types: HashMap<DataTypeKey, TypedDataType>,
}
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"
}),
);
}

View File

@ -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)?;

View File

@ -244,6 +244,12 @@ impl<'a> From<&'a CheckedModules> for &'a HashMap<String, CheckedModule> {
}
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<Item = (&CheckedModule, &TypedFunction)> {
let mut items = vec![];
for validator in self.0.values().filter(|module| module.kind.is_validator()) {