diff --git a/crates/aiken-project/src/blueprint/definitions.rs b/crates/aiken-project/src/blueprint/definitions.rs new file mode 100644 index 00000000..91d5e582 --- /dev/null +++ b/crates/aiken-project/src/blueprint/definitions.rs @@ -0,0 +1,183 @@ +use aiken_lang::tipo::{Type, TypeVar}; +use serde::{ + self, + ser::{Serialize, SerializeStruct, Serializer}, +}; +use std::{ + collections::{BTreeMap, HashMap}, + fmt::{self, Display}, + ops::Deref, + sync::Arc, +}; + +// ---------- Definitions + +/// A map of definitions meant to be optionally registered and looked up. +#[derive(Debug, PartialEq, Eq, Clone, Default, serde::Serialize, serde::Deserialize)] +pub struct Definitions { + #[serde(flatten, default)] + inner: BTreeMap>, +} + +impl Definitions { + /// Constructs a new empty definitions set. + pub fn new() -> Self { + Definitions { + inner: BTreeMap::new(), + } + } + + /// True when there's no known definitions. + pub fn is_empty(&self) -> bool { + self.inner.is_empty() + } + + /// Retrieve a definition, if it exists. + pub fn lookup(&self, reference: &Reference) -> Option<&T> { + self.inner + .get(reference.as_key()) + .map(|v| v + .as_ref() + .expect("All registered definitions are 'Some'. 'None' state is only transient during registration") + ) + } + + /// Merge two set of definitions together. Prioritize callee. + pub fn merge(&mut self, other: &mut Definitions) { + self.inner.append(&mut other.inner); + } + + /// Erase a known definition. Does nothing if the reference is unknown. + pub fn remove(&mut self, reference: &Reference) { + self.inner.remove(reference.as_key()); + } + + /// Register a new definition only if it doesn't exist. This uses a strategy of + /// mark-and-insert such that recursive definitions are only built once. + pub fn register( + &mut self, + type_info: &Type, + type_parameters: &HashMap>, + build_schema: F, + ) -> Result + where + F: FnOnce(&mut Self) -> Result, + { + let reference = Reference::from_type(type_info, type_parameters); + let key = reference.as_key(); + + if !self.inner.contains_key(key) { + self.inner.insert(key.to_string(), None); + let schema = build_schema(self)?; + self.inner.insert(key.to_string(), Some(schema)); + } + + Ok(reference) + } +} + +// ---------- Reference + +/// A URI pointer to an underlying data-type. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Default)] +pub struct Reference { + inner: String, +} + +impl Reference { + /// Create a (possibly unsound) Reference from a string. This isn't the preferred way to create + /// a reference. One should use: `into()` on a 'Type' instead. + pub fn new(path: &str) -> Reference { + Reference { + inner: path.to_string(), + } + } + + /// Turn a reference into a key suitable for lookup. + fn as_key(&self) -> &str { + self.inner.as_str() + } + + /// Turn a reference into a valid JSON pointer. Note that the JSON pointer specification + /// indicates that '/' must be escaped as '~1' in pointer addresses (as they are otherwise + /// treated as path delimiter in pointers paths). + fn as_json_pointer(&self) -> String { + format!("#/definitions/{}", self.as_key().replace('/', "~1")) + } +} + +impl Display for Reference { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(&self.inner) + } +} + +impl Reference { + pub fn from_type(type_info: &Type, type_parameters: &HashMap>) -> Self { + match type_info { + Type::App { + module, name, args, .. + } => { + let args: Self = Self::from_types(args, type_parameters); + Self { + inner: if module.is_empty() { + format!("{name}{args}") + } else { + format!("{module}/{name}{args}") + }, + } + } + + Type::Tuple { elems } => Self { + inner: format!( + "Tuple{elems}", + elems = Self::from_types(elems, type_parameters) + ), + }, + + // NOTE: + // + // Implementations below are only there for completeness. In practice, we should never + // end up creating references for 'Var' or 'Fn' in the context of blueprints. + Type::Var { tipo } => match tipo.borrow().deref() { + TypeVar::Link { tipo } => Self::from_type(tipo.as_ref(), type_parameters), + TypeVar::Generic { id } | TypeVar::Unbound { id } => { + let tipo = type_parameters.get(id).unwrap(); + Self::from_type(tipo, type_parameters) + } + }, + + Type::Fn { args, ret } => Self { + inner: format!( + "Fn{args}_{ret}", + args = Self::from_types(args, type_parameters), + ret = Self::from_type(ret, type_parameters) + ), + }, + } + } + + fn from_types(args: &Vec>, type_parameters: &HashMap>) -> Self { + if args.is_empty() { + Reference::new("") + } else { + Reference { + inner: format!( + "${}", + args.iter() + .map(|s| Self::from_type(s.as_ref(), type_parameters).inner) + .collect::>() + .join("_") + ), + } + } + } +} + +impl Serialize for Reference { + fn serialize(&self, serializer: S) -> Result { + let mut s = serializer.serialize_struct("$ref", 1)?; + s.serialize_field("$ref", &self.as_json_pointer())?; + s.end() + } +} diff --git a/crates/aiken-project/src/blueprint/mod.rs b/crates/aiken-project/src/blueprint/mod.rs index 190489ae..2863d5f1 100644 --- a/crates/aiken-project/src/blueprint/mod.rs +++ b/crates/aiken-project/src/blueprint/mod.rs @@ -1,18 +1,22 @@ +pub mod definitions; pub mod error; pub mod schema; pub mod validator; use crate::{config::Config, module::CheckedModules}; use aiken_lang::uplc::CodeGenerator; +use definitions::{Definitions, Reference}; use error::Error; -use schema::Schema; -use std::fmt::{self, Debug, Display}; +use schema::{Annotated, Schema}; +use std::fmt::Debug; use validator::Validator; #[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)] -pub struct Blueprint { +pub struct Blueprint { pub preamble: Preamble, - pub validators: Vec>, + pub validators: Vec>, + #[serde(skip_serializing_if = "Definitions::is_empty", default)] + pub definitions: Definitions, } #[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)] @@ -44,7 +48,7 @@ pub enum LookupResult<'a, T> { Many, } -impl Blueprint { +impl Blueprint> { pub fn new( config: &Config, modules: &CheckedModules, @@ -52,25 +56,35 @@ impl Blueprint { ) -> Result { let preamble = config.into(); + let mut definitions = Definitions::new(); + let validators: Result, Error> = modules .validators() .map(|(validator, def)| { - Validator::from_checked_module(modules, generator, validator, def) + Validator::from_checked_module(modules, generator, validator, def).map( + |mut schema| { + definitions.merge(&mut schema.definitions); + schema.definitions = Definitions::new(); + schema + }, + ) }) .collect(); Ok(Blueprint { preamble, validators: validators?, + definitions, }) } } -impl Blueprint +impl Blueprint where - T: Clone + Default, + R: Clone + Default, + S: Clone + Default, { - pub fn lookup(&self, title: Option<&String>) -> Option>> { + pub fn lookup(&self, title: Option<&String>) -> Option>> { let mut validator = None; for v in self.validators.iter() { @@ -95,7 +109,7 @@ where action: F, ) -> Result where - F: Fn(Validator) -> Result, + F: Fn(Validator) -> Result, { match self.lookup(title) { Some(LookupResult::One(validator)) => action(validator.to_owned()), @@ -109,13 +123,6 @@ where } } -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 From<&Config> for Preamble { fn from(config: &Config) -> Self { Preamble { @@ -135,11 +142,14 @@ impl From<&Config> for Preamble { #[cfg(test)] mod test { use super::*; + use aiken_lang::builtins; + use schema::{Data, Items, Schema}; use serde_json::{self, json}; + use std::collections::HashMap; #[test] fn serialize_no_description() { - let blueprint: Blueprint = Blueprint { + let blueprint: Blueprint> = Blueprint { preamble: Preamble { title: "Foo".to_string(), description: None, @@ -148,6 +158,7 @@ mod test { license: Some("Apache-2.0".to_string()), }, validators: vec![], + definitions: Definitions::new(), }; assert_eq!( serde_json::to_value(&blueprint).unwrap(), @@ -165,7 +176,7 @@ mod test { #[test] fn serialize_with_description() { - let blueprint: Blueprint = Blueprint { + let blueprint: Blueprint> = Blueprint { preamble: Preamble { title: "Foo".to_string(), description: Some("Lorem ipsum".to_string()), @@ -174,6 +185,7 @@ mod test { license: None, }, validators: vec![], + definitions: Definitions::new(), }; assert_eq!( serde_json::to_value(&blueprint).unwrap(), @@ -188,4 +200,65 @@ mod test { }), ); } + + #[test] + fn serialize_with_definitions() { + let mut definitions = Definitions::new(); + definitions + .register::<_, Error>(&builtins::int(), &HashMap::new(), |_| { + Ok(Schema::Data(Data::Integer).into()) + }) + .unwrap(); + definitions + .register::<_, Error>( + &builtins::list(builtins::byte_array()), + &HashMap::new(), + |definitions| { + let ref_bytes = definitions.register::<_, Error>( + &builtins::byte_array(), + &HashMap::new(), + |_| Ok(Schema::Data(Data::Bytes).into()), + )?; + Ok(Schema::Data(Data::List(Items::One(Box::new(ref_bytes)))).into()) + }, + ) + .unwrap(); + + let blueprint: Blueprint> = Blueprint { + preamble: Preamble { + title: "Foo".to_string(), + description: None, + version: "1.0.0".to_string(), + plutus_version: PlutusVersion::V2, + license: None, + }, + validators: vec![], + definitions, + }; + assert_eq!( + serde_json::to_value(&blueprint).unwrap(), + json!({ + "preamble": { + "title": "Foo", + "version": "1.0.0", + "plutusVersion": "v2" + }, + "validators": [], + "definitions": { + "ByteArray": { + "dataType": "bytes" + }, + "Int": { + "dataType": "integer" + }, + "List$ByteArray": { + "dataType": "list", + "items": { + "$ref": "#/definitions/ByteArray" + } + } + } + }), + ); + } } diff --git a/crates/aiken-project/src/blueprint/schema.rs b/crates/aiken-project/src/blueprint/schema.rs index d5499003..b8cbd4b9 100644 --- a/crates/aiken-project/src/blueprint/schema.rs +++ b/crates/aiken-project/src/blueprint/schema.rs @@ -1,3 +1,4 @@ +use crate::blueprint::definitions::{Definitions, Reference}; use crate::CheckedModule; use aiken_lang::{ ast::{DataType, Definition, TypedDefinition}, @@ -8,13 +9,8 @@ use serde::{ self, ser::{Serialize, SerializeStruct, Serializer}, }; -use serde_json; use std::ops::Deref; -use std::{ - collections::HashMap, - fmt::{self, Display}, - sync::Arc, -}; +use std::{collections::HashMap, sync::Arc}; #[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)] pub struct Annotated { @@ -44,8 +40,8 @@ pub enum Schema { pub enum Data { Integer, Bytes, - List(Items), - Map(Box, Box), + List(Items), + Map(Box, Box), AnyOf(Vec>), Opaque, } @@ -61,15 +57,7 @@ pub enum Items { #[derive(Debug, PartialEq, Eq, Clone)] pub struct Constructor { pub index: usize, - pub fields: Vec, -} - -/// A field of a constructor. Can be either an inlined Data schema or a reference to another type. -/// References are mostly only used for recursive types. -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum Field { - Inline(Annotated), - Reference { path: String }, + pub fields: Vec, } impl From for Annotated { @@ -86,195 +74,251 @@ impl Annotated { pub fn from_type( modules: &HashMap, type_info: &Type, - type_parameters: &HashMap>, - ) -> Result { + definitions: &mut Definitions, + ) -> Result { + Annotated::do_from_type(type_info, modules, &mut HashMap::new(), definitions) + } + + fn do_from_type( + type_info: &Type, + modules: &HashMap, + type_parameters: &mut HashMap>, + definitions: &mut Definitions, + ) -> Result { 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(Data::Opaque), - }), + } if module_name.is_empty() => { + definitions.register(type_info, &type_parameters.clone(), |definitions| { + match &type_name[..] { + "Data" => Ok(Annotated { + title: Some("Data".to_string()), + description: Some("Any Plutus data.".to_string()), + annotated: Schema::Data(Data::Opaque), + }), - "ByteArray" => Ok(Schema::Data(Data::Bytes).into()), + "ByteArray" => Ok(Schema::Data(Data::Bytes).into()), - "Int" => Ok(Schema::Data(Data::Integer).into()), + "Int" => Ok(Schema::Data(Data::Integer).into()), - "String" => Ok(Schema::String.into()), + "String" => Ok(Schema::String.into()), - "Void" => Ok(Annotated { - title: Some("Unit".to_string()), - description: Some("The nullary constructor.".to_string()), - annotated: Schema::Data(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(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![], - }, - }, - ])), - }), - - "Ordering" => Ok(Annotated { - title: Some("Ordering".to_string()), - description: None, - annotated: Schema::Data(Data::AnyOf(vec![ - Annotated { - title: Some("Less".to_string()), - description: None, - annotated: Constructor { - index: 0, - fields: vec![], - }, - }, - Annotated { - title: Some("Equal".to_string()), - description: None, - annotated: Constructor { - index: 1, - fields: vec![], - }, - }, - Annotated { - title: Some("Greater".to_string()), - description: None, - annotated: Constructor { - index: 2, - 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(Data::AnyOf(vec![ - Annotated { - title: Some("Some".to_string()), - description: Some("An optional value.".to_string()), + "Void" => Ok(Annotated { + title: Some("Unit".to_string()), + description: Some("The nullary constructor.".to_string()), + annotated: Schema::Data(Data::AnyOf(vec![Annotated { + title: None, + description: None, annotated: Constructor { index: 0, - fields: vec![Field::Inline(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)?; + "Bool" => Ok(Annotated { + title: Some("Bool".to_string()), + description: None, + annotated: Schema::Data(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![], + }, + }, + ])), + }), - // 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::Data(Data::List(Items::Many(xs))) if xs.len() == 2 => Data::Map( - Box::new(xs.first().unwrap().to_owned()), - Box::new(xs.last().unwrap().to_owned()), - ), - _ => { - let inner = generic.into_data(type_info)?.annotated; - Data::List(Items::One(Box::new(inner))) + "Ordering" => Ok(Annotated { + title: Some("Ordering".to_string()), + description: None, + annotated: Schema::Data(Data::AnyOf(vec![ + Annotated { + title: Some("Less".to_string()), + description: None, + annotated: Constructor { + index: 0, + fields: vec![], + }, + }, + Annotated { + title: Some("Equal".to_string()), + description: None, + annotated: Constructor { + index: 1, + fields: vec![], + }, + }, + Annotated { + title: Some("Greater".to_string()), + description: None, + annotated: Constructor { + index: 2, + fields: vec![], + }, + }, + ])), + }), + + "Option" => { + let generic = Annotated::do_from_type( + args.get(0) + .expect("Option types have always one generic argument"), + modules, + type_parameters, + definitions, + )?; + + Ok(Annotated { + title: Some("Optional".to_string()), + description: None, + annotated: Schema::Data(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![], + }, + }, + ])), + }) } - }; - Ok(Schema::Data(data).into()) - } + "List" => { + let generic = Annotated::do_from_type( + args.get(0) + .expect("List types have always one generic argument"), + modules, + type_parameters, + definitions, + )?; + + // 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 definitions + .lookup(&generic) + .expect( + "Generic type argument definition was registered just above.", + ) + .clone() + { + Annotated { + annotated: Schema::Data(Data::List(Items::Many(xs))), + .. + } if xs.len() == 2 => { + definitions.remove(&generic); + Data::Map( + Box::new( + xs.first() + .expect("length (== 2) checked in pattern clause") + .to_owned(), + ), + Box::new( + xs.last() + .expect("length (== 2) checked in pattern clause") + .to_owned(), + ), + ) + } + _ => { + // let inner = schema.clone().into_data(type_info)?.annotated; + Data::List(Items::One(Box::new(generic))) + } + }; + + Ok(Schema::Data(data).into()) + } + + _ => Err(Error::new(ErrorContext::UnsupportedType, type_info)), + } + }) + } - _ => 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); + name, module, args, .. + } => definitions.register(type_info, &type_parameters.clone(), |definitions| { + let module = modules + .get(module) + .unwrap_or_else(|| panic!("unknown module '{module}'\n\n{modules:?}")); + + let data_type = + find_data_type(name, &module.ast.definitions).unwrap_or_else(|| { + panic!( + "unknown data-type for '{name:?}' \n\n{definitions:?}", + definitions = module.ast.definitions + ) + }); + + collect_type_parameters(type_parameters, &data_type.typed_parameters, args); + let annotated = Schema::Data( - Data::from_data_type(modules, constructor, &type_parameters) + Data::from_data_type(&data_type, modules, type_parameters, definitions) .map_err(|e| e.backtrack(type_info))?, ); Ok(Annotated { - title: Some(constructor.name.clone()), - description: constructor.doc.clone().map(|s| s.trim().to_string()), + title: Some(data_type.name.clone()), + description: data_type.doc.clone().map(|s| s.trim().to_string()), annotated, }) + }), + Type::Tuple { elems } => { + definitions.register(type_info, &type_parameters.clone(), |definitions| { + let elems = elems + .iter() + .map(|elem| { + Annotated::do_from_type(elem, modules, type_parameters, definitions) + }) + .collect::, _>>() + .map_err(|e| e.backtrack(type_info))?; + + Ok(Annotated { + title: Some("Tuple".to_owned()), + description: None, + annotated: Schema::Data(Data::List(Items::Many(elems))), + }) + }) } Type::Var { tipo } => match tipo.borrow().deref() { - TypeVar::Link { tipo } => Annotated::from_type(modules, tipo, type_parameters), + TypeVar::Link { tipo } => { + Annotated::do_from_type(tipo, modules, type_parameters, definitions) + } 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) + .ok_or_else(|| Error::new(ErrorContext::FreeTypeVariable, type_info))? + .clone(); + Annotated::do_from_type(&tipo, modules, type_parameters, definitions) } TypeVar::Unbound { .. } => { Err(Error::new(ErrorContext::UnboundTypeVariable, type_info)) } }, - Type::Tuple { 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::, _>>() - .map_err(|e| e.backtrack(type_info))?; - - Ok(Annotated { - title: Some("Tuple".to_owned()), - description: None, - annotated: Schema::Data(Data::List(Items::Many(elems))), - }) - } - Type::Fn { .. } => Err(Error::new(ErrorContext::UnexpectedFunction, type_info)), + Type::Fn { .. } => unreachable!(), } } @@ -295,28 +339,35 @@ impl Annotated { } impl Data { - pub fn from_data_type( - modules: &HashMap, + fn from_data_type( data_type: &DataType>, - type_parameters: &HashMap>, + modules: &HashMap, + type_parameters: &mut HashMap>, + definitions: &mut Definitions>, ) -> Result { let mut variants = vec![]; + let len_constructors = data_type.constructors.len(); for (index, constructor) in data_type.constructors.iter().enumerate() { let mut fields = vec![]; + + let len_arguments = data_type.constructors.len(); 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))?; + let reference = + Annotated::do_from_type(&field.tipo, modules, type_parameters, definitions)?; - if field.label.is_some() { - schema.title = field.label.clone(); + // 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 && len_constructors == 1 && len_arguments == 1 { + let schema = definitions + .lookup(&reference) + .expect("Schema definition registered just above") + .clone(); + definitions.remove(&reference); + return Ok(schema.into_data(&field.tipo)?.annotated); } - if field.doc.is_some() { - schema.description = field.doc.clone().map(|s| s.trim().to_string()); - } - - fields.push(Field::Inline(schema)); + fields.push(reference); } let variant = Annotated { @@ -328,31 +379,56 @@ impl Data { 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::Inline(field)] = &variant.annotated.fields[..] { - return Ok(field.annotated.clone()); - } - } - } - Ok(Data::AnyOf(variants)) } } -// Needed because of Blueprint's default, but actually never used. -impl Default for Schema { - fn default() -> Self { - Schema::Data(Data::Opaque) +fn collect_type_parameters<'a>( + type_parameters: &'a mut HashMap>, + generics: &'a [Arc], + applications: &'a [Arc], +) { + 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_or_else(|| panic!("Couldn't find generic identifier ({id}) in applied types: {applications:?}")) + .to_owned() + ); + } + _ => unreachable!(), + }, + _ => unreachable!(), + } } } -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) +fn find_data_type(name: &str, definitions: &[TypedDefinition]) -> Option>> { + for def in definitions { + match def { + Definition::DataType(data_type) if name == data_type.name => { + return Some(data_type.clone()) + } + Definition::Fn { .. } + | Definition::Validator { .. } + | Definition::DataType { .. } + | Definition::TypeAlias { .. } + | Definition::Use { .. } + | Definition::ModuleConstant { .. } + | Definition::Test { .. } => continue, + } + } + None +} + +// Needed because of Blueprint's default, but actually never used. +impl Default for Annotated { + fn default() -> Self { + Schema::Data(Data::Opaque).into() } } @@ -439,11 +515,6 @@ impl Serialize for Data { 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() @@ -461,19 +532,6 @@ impl Serialize for Constructor { } } -impl Serialize for Field { - fn serialize(&self, serializer: S) -> Result { - match self { - Field::Inline(schema) => schema.serialize(serializer), - Field::Reference { path } => { - let mut s = serializer.serialize_struct("$ref", 1)?; - s.serialize_field("$ref", path)?; - s.end() - } - } - } -} - #[derive(Debug, PartialEq, Clone, thiserror::Error)] #[error("{}", context)] pub struct Error { @@ -524,7 +582,11 @@ impl Error { ╰─▶ {signature} This is likely a bug. I should know. May you be kind enough and report this on ."#, - signature = Error::fmt_breadcrumbs(&[self.breadcrumbs.last().unwrap().to_owned()]), + signature = Error::fmt_breadcrumbs(&[self + .breadcrumbs + .last() + .expect("always at least one breadcrumb") + .to_owned()]), ), ErrorContext::FreeTypeVariable => format!( @@ -584,46 +646,6 @@ Here's the types I followed and that led me to this problem: } } -fn collect_type_parameters<'a>( - generics: &'a [Arc], - applications: &'a [Arc], -) -> HashMap> { - 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, -) -> 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::Validator { .. } - | Definition::DataType { .. } - | Definition::TypeAlias { .. } - | Definition::Use { .. } - | Definition::ModuleConstant { .. } - | Definition::Test { .. } => continue, - } - } - None -} - #[cfg(test)] pub mod test { use super::*; @@ -657,13 +679,14 @@ pub mod test { #[test] fn serialize_data_list_1() { - let schema = Schema::Data(Data::List(Items::One(Box::new(Data::Integer)))); + let ref_integer = Reference::new("Int"); + let schema = Schema::Data(Data::List(Items::One(Box::new(ref_integer)))); assert_json( &schema, json!({ "dataType": "list", "items": { - "dataType": "integer" + "$ref": "#/definitions/Int" } }), ); @@ -671,70 +694,33 @@ pub mod test { #[test] fn serialize_data_list_2() { - let schema = Schema::Data(Data::List(Items::One(Box::new(Data::List(Items::One( - Box::new(Data::Integer), - )))))); + let ref_list_integer = Reference::new("List$Int"); + let schema = Schema::Data(Data::List(Items::One(Box::new(ref_list_integer)))); assert_json( &schema, json!({ "dataType": "list", - "items": - { - "dataType": "list", - "items": { "dataType": "integer" } - } - }), - ); - } - - #[test] - fn serialize_data_list_3() { - let schema = Schema::Data(Data::List(Items::Many(vec![Data::Integer, Data::Bytes]))); - assert_json( - &schema, - json!({ - "dataType": "list", - "items": [ - { "dataType": "integer" }, - { "dataType": "bytes" }, - ] + "items": { + "$ref": "#/definitions/List$Int" + } }), ); } #[test] fn serialize_data_map_1() { - let schema = Schema::Data(Data::Map(Box::new(Data::Integer), Box::new(Data::Bytes))); + let ref_integer = Reference::new("Int"); + let ref_bytes = Reference::new("ByteArray"); + let schema = Schema::Data(Data::Map(Box::new(ref_integer), Box::new(ref_bytes))); assert_json( &schema, json!({ "dataType": "map", "keys": { - "dataType": "integer" + "$ref": "#/definitions/Int" }, "values": { - "dataType": "bytes" - } - }), - ) - } - - #[test] - fn serialize_data_map_2() { - let schema = Schema::Data(Data::Map( - Box::new(Data::Bytes), - Box::new(Data::List(Items::One(Box::new(Data::Integer)))), - )); - assert_json( - &schema, - json!({ - "dataType": "map", - "keys": { - "dataType": "bytes" - }, - "values": { - "dataType": "list", - "items": { "dataType": "integer" } + "$ref": "#/definitions/ByteArray" } }), ) @@ -764,12 +750,12 @@ pub mod test { let schema = Schema::Data(Data::AnyOf(vec![ Constructor { index: 0, - fields: vec![Field::Inline(Data::Integer.into())], + fields: vec![Reference::new("Int")], } .into(), Constructor { index: 1, - fields: vec![Field::Inline(Data::Bytes.into())], + fields: vec![Reference::new("Bytes")], } .into(), ])); @@ -780,12 +766,12 @@ pub mod test { { "dataType": "constructor", "index": 0, - "fields": [{ "dataType": "integer" }] + "fields": [{ "$ref": "#/definitions/Int" }] }, { "dataType": "constructor", "index": 1, - "fields": [{ "dataType": "bytes" }] + "fields": [{ "$ref": "#/definitions/Bytes" }] } ] }), diff --git a/crates/aiken-project/src/blueprint/validator.rs b/crates/aiken-project/src/blueprint/validator.rs index 070854f9..b31085d5 100644 --- a/crates/aiken-project/src/blueprint/validator.rs +++ b/crates/aiken-project/src/blueprint/validator.rs @@ -1,4 +1,5 @@ use super::{ + definitions::{Definitions, Reference}, error::Error, schema::{Annotated, Schema}, }; @@ -6,30 +7,30 @@ use crate::module::{CheckedModule, CheckedModules}; use aiken_lang::{ast::TypedValidator, uplc::CodeGenerator}; use miette::NamedSource; use serde; -use std::{ - collections::HashMap, - fmt::{self, Display}, -}; use uplc::ast::{DeBruijn, Program, Term}; #[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)] -pub struct Validator { +pub struct Validator { pub title: String, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub datum: Option>, + pub datum: Option>, - pub redeemer: Argument, + pub redeemer: Argument, #[serde(skip_serializing_if = "Vec::is_empty")] #[serde(default)] - pub parameters: Vec>, + pub parameters: Vec>, #[serde(flatten)] pub program: Program, + + #[serde(skip_serializing_if = "Definitions::is_empty")] + #[serde(default)] + pub definitions: Definitions, } #[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)] @@ -37,43 +38,16 @@ pub struct Argument { #[serde(skip_serializing_if = "Option::is_none")] pub title: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - pub schema: T, } -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 Display for Argument { - 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 From> for Argument { - fn from(annotated: Annotated) -> Self { - Argument { - title: annotated.title, - description: annotated.description, - schema: annotated.annotated, - } - } -} - -impl Validator { +impl Validator> { pub fn from_checked_module( modules: &CheckedModules, generator: &mut CodeGenerator, module: &CheckedModule, def: &TypedValidator, - ) -> Result, Error> { + ) -> Result>, Error> { let mut args = def.fun.arguments.iter().rev(); let (_, redeemer, datum) = (args.next(), args.next().unwrap(), args.next()); @@ -82,6 +56,8 @@ impl Validator { arguments.extend(def.params.clone()); arguments.extend(def.fun.arguments.clone()); + let mut definitions = Definitions::new(); + Ok(Validator { title: format!("{}.{}", &module.name, &def.fun.name), description: None, @@ -89,28 +65,24 @@ impl Validator { .params .iter() .map(|param| { - let annotation = - Annotated::from_type(modules.into(), ¶m.tipo, &HashMap::new()).map_err( - |error| Error::Schema { - error, - location: param.location, - source_code: NamedSource::new( - module.input_path.display().to_string(), - module.code.clone(), - ), - }, - ); - annotation.map(|mut annotation| { - annotation.title = annotation - .title - .or_else(|| Some(param.arg_name.get_label())); - annotation.into() - }) + Annotated::from_type(modules.into(), ¶m.tipo, &mut definitions) + .map(|schema| Argument { + title: Some(param.arg_name.get_label()), + schema, + }) + .map_err(|error| Error::Schema { + error, + location: param.location, + source_code: NamedSource::new( + module.input_path.display().to_string(), + module.code.clone(), + ), + }) }) .collect::>()?, datum: datum .map(|datum| { - Annotated::from_type(modules.into(), &datum.tipo, &HashMap::new()).map_err( + Annotated::from_type(modules.into(), &datum.tipo, &mut definitions).map_err( |error| Error::Schema { error, location: datum.location, @@ -122,8 +94,11 @@ impl Validator { ) }) .transpose()? - .map(|annotated| annotated.into()), - redeemer: Annotated::from_type(modules.into(), &redeemer.tipo, &HashMap::new()) + .map(|schema| Argument { + title: datum.map(|datum| datum.arg_name.get_label()), + schema, + }), + redeemer: Annotated::from_type(modules.into(), &redeemer.tipo, &mut definitions) .map_err(|error| Error::Schema { error, location: redeemer.location, @@ -131,19 +106,24 @@ impl Validator { module.input_path.display().to_string(), module.code.clone(), ), - })? - .into(), + }) + .map(|schema| Argument { + title: Some(redeemer.arg_name.get_label()), + schema, + })?, program: generator .generate(&def.fun.body, &arguments, true) .try_into() .unwrap(), + definitions, }) } } -impl Validator +impl Validator where - T: Clone, + S: Clone, + R: Clone, { pub fn apply(self, arg: &Term) -> Result { match self.parameters.split_first() { @@ -289,7 +269,7 @@ mod test { } #[test] - fn validator_mint_basic() { + fn mint_basic() { assert_validator( r#" validator mint { @@ -300,19 +280,26 @@ mod test { "#, json!({ "title": "test_module.mint", - "hash": "afddc16c18e7d8de379fb9aad39b3d1b5afd27603e5ebac818432a72", "redeemer": { - "title": "Data", - "description": "Any Plutus data.", - "schema": {} + "title": "redeemer", + "schema": { + "$ref": "#/definitions/Data" + } }, - "compiledCode": "583b010000323232323232322253330054a22930b180080091129998030010a4c26600a6002600e0046660060066010004002ae695cdaab9f5742ae881" + "compiledCode": "583b010000323232323232322253330054a22930b180080091129998030010a4c26600a6002600e0046660060066010004002ae695cdaab9f5742ae881", + "hash": "afddc16c18e7d8de379fb9aad39b3d1b5afd27603e5ebac818432a72", + "definitions": { + "Data": { + "title": "Data", + "description": "Any Plutus data." + } + } }), ); } #[test] - fn validator_mint_parameterized() { + fn mint_parameterized() { assert_validator( r#" validator mint(utxo_ref: Int) { @@ -323,25 +310,37 @@ mod test { "#, json!({ "title": "test_module.mint", - "hash": "a82df717fd39f5b273c4eb89ae5252e11cc272ac59d815419bf2e4c3", - "parameters": [{ - "title": "utxo_ref", + "redeemer": { + "title": "redeemer", "schema": { + "$ref": "#/definitions/Data" + } + }, + "parameters": [ + { + "title": "utxo_ref", + "schema": { + "$ref": "#/definitions/Int" + } + } + ], + "compiledCode": "5840010000323232323232322322253330074a22930b1bad0013001001222533300600214984cc014c004c01c008ccc00c00cc0200080055cd2b9b5573eae855d101", + "hash": "a82df717fd39f5b273c4eb89ae5252e11cc272ac59d815419bf2e4c3", + "definitions": { + "Data": { + "title": "Data", + "description": "Any Plutus data." + }, + "Int": { "dataType": "integer" } - }], - "redeemer": { - "title": "Data", - "description": "Any Plutus data.", - "schema": {} - }, - "compiledCode": "5840010000323232323232322322253330074a22930b1bad0013001001222533300600214984cc014c004c01c008ccc00c00cc0200080055cd2b9b5573eae855d101" + } }), ); } #[test] - fn validator_spend() { + fn simplified_hydra() { assert_validator( r#" /// On-chain state @@ -374,62 +373,60 @@ mod test { Abort } - validator spend { + validator simplified_hydra { fn(datum: State, redeemer: Input, ctx: Data) { True } } "#, json!({ - "title": "test_module.spend", - "hash": "e37db487fbd58c45d059bcbf5cd6b1604d3bec16cf888f1395a4ebc4", + "title": "test_module.simplified_hydra", "datum": { - "title": "State", - "description": "On-chain state", + "title": "datum", "schema": { + "$ref": "#/definitions/test_module~1State" + } + }, + "redeemer": { + "title": "redeemer", + "schema": { + "$ref": "#/definitions/test_module~1Input" + } + }, + "compiledCode": "583b0100003232323232323222253330064a22930b180080091129998030010a4c26600a6002600e0046660060066010004002ae695cdaab9f5742ae89", + "hash": "e37db487fbd58c45d059bcbf5cd6b1604d3bec16cf888f1395a4ebc4", + "definitions": { + "ByteArray": { + "dataType": "bytes" + }, + "Int": { + "dataType": "integer" + }, + "List$ByteArray": { + "dataType": "list", + "items": { + "$ref": "#/definitions/ByteArray" + } + }, + "test_module/ContestationPeriod": { + "title": "ContestationPeriod", + "description": "Whatever", "anyOf": [ { - "title": "State", + "title": "ContestationPeriod", + "description": "A positive, non-zero number of seconds.", "dataType": "constructor", "index": 0, "fields": [ { - "title": "contestationPeriod", - "description": "The contestation period as a number of seconds", - "anyOf": [ - { - "title": "ContestationPeriod", - "description": "A positive, non-zero number of seconds.", - "dataType": "constructor", - "index": 0, - "fields": [ - { - "dataType": "integer" - } - ] - } - ] - }, - { - "title": "parties", - "description": "List of public key hashes of all participants", - "dataType": "list", - "items": { - "dataType": "bytes" - } - }, - { - "title": "utxoHash", - "dataType": "bytes" + "$ref": "#/definitions/Int" } ] } ] - } - }, - "redeemer": { - "title": "Input", - "schema": { + }, + "test_module/Input": { + "title": "Input", "anyOf": [ { "title": "CollectCom", @@ -451,88 +448,101 @@ mod test { "fields": [] } ] + }, + "test_module/State": { + "title": "State", + "description": "On-chain state", + "anyOf": [ + { + "title": "State", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "$ref": "#/definitions/test_module~1ContestationPeriod" + }, + { + "$ref": "#/definitions/List$ByteArray" + }, + { + "$ref": "#/definitions/ByteArray" + } + ] + } + ] } - }, - "compiledCode": "583b0100003232323232323222253330064a22930b180080091129998030010a4c26600a6002600e0046660060066010004002ae695cdaab9f5742ae89" + } }), ); } #[test] - fn validator_spend_2tuple() { + fn tuples() { assert_validator( r#" - validator spend { - fn(datum: (Int, ByteArray), redeemer: String, ctx: Void) { + validator tuples { + fn(datum: (Int, ByteArray), redeemer: (Int, Int, Int), ctx: Void) { True } } "#, json!({ - "title": "test_module.spend", - "hash": "3c6766e7a36df2aa13c0e9e6e071317ed39d05f405771c4f1a81c6cc", + "title": "test_module.tuples", "datum": { - "title": "Tuple", + "title": "datum", "schema": { - "dataType": "list", - "items": [ - { "dataType": "integer" }, - { "dataType": "bytes" } - ] + "$ref": "#/definitions/Tuple$Int_ByteArray" } }, "redeemer": { + "title": "redeemer", "schema": { - "dataType": "#string" + "$ref": "#/definitions/Tuple$Int_Int_Int" } }, - "compiledCode": "585501000032323232323232232232253330084a22930b1b99375c002646466ec0c024008c024004c024004dd6000980080091129998030010a4c26600a6002600e0046660060066010004002ae695cdaab9f5742ae881" - }), - ) - } - - #[test] - fn validator_spend_tuples() { - assert_validator( - r#" - validator spend { - fn(datum: (Int, Int, Int), redeemer: Data, ctx: Void) { - True - } - } - "#, - json!({ - "title": "test_module.spend", - "hash": "f335ce0436fd7df56e727a66ada7298534a27b98f887bc3b7947ee48", - "datum": { - "title": "Tuple", - "schema": { + "compiledCode": "585301000032323232323232232232253330084a22930b1bac0013232337606012004601200260120026eb0004c0040048894ccc0180085261330053001300700233300300330080020015734ae6d55cfaba157441", + "hash": "500b9b576c11ad73dee3b9d5202496a7df78e8de4097c57f0acfcc3a", + "definitions": { + "ByteArray": { + "dataType": "bytes" + }, + "Int": { + "dataType": "integer" + }, + "Tuple$Int_ByteArray": { + "title": "Tuple", "dataType": "list", "items": [ { - "dataType": "integer" + "$ref": "#/definitions/Int" }, { - "dataType": "integer" + "$ref": "#/definitions/ByteArray" + } + ] + }, + "Tuple$Int_Int_Int": { + "title": "Tuple", + "dataType": "list", + "items": [ + { + "$ref": "#/definitions/Int" }, { - "dataType": "integer" + "$ref": "#/definitions/Int" + }, + { + "$ref": "#/definitions/Int" } ] } - }, - "redeemer": { - "title": "Data", - "description": "Any Plutus data.", - "schema": {} - }, - "compiledCode": "5840010000323232323232322322253330074a22930b1bac0013001001222533300600214984cc014c004c01c008ccc00c00cc0200080055cd2b9b5573eae855d101" + } }), ) } #[test] - fn validator_generics() { + fn generics() { assert_validator( r#" type Either { @@ -545,69 +555,82 @@ mod test { Infinite } - validator withdraw { + validator generics { fn(redeemer: Either>, ctx: Void) { True } } "#, - json!( - { - "title": "test_module.withdraw", - "hash": "afddc16c18e7d8de379fb9aad39b3d1b5afd27603e5ebac818432a72", - "redeemer": { - "title": "Either", - "schema": { - "anyOf": [ + json!({ + "title": "test_module.generics", + "redeemer": { + "title": "redeemer", + "schema": { + "$ref": "#/definitions/test_module~1Either$ByteArray_test_module~1Interval$Int" + } + }, + "compiledCode": "583b010000323232323232322253330054a22930b180080091129998030010a4c26600a6002600e0046660060066010004002ae695cdaab9f5742ae881", + "hash": "afddc16c18e7d8de379fb9aad39b3d1b5afd27603e5ebac818432a72", + "definitions": { + "ByteArray": { + "dataType": "bytes" + }, + "Int": { + "dataType": "integer" + }, + "test_module/Either$ByteArray_test_module/Interval$Int": { + "title": "Either", + "anyOf": [ + { + "title": "Left", + "dataType": "constructor", + "index": 0, + "fields": [ { - "title": "Left", - "dataType": "constructor", - "index": 0, - "fields": [ - { - "dataType": "bytes" - } - ] - }, + "$ref": "#/definitions/ByteArray" + } + ] + }, + { + "title": "Right", + "dataType": "constructor", + "index": 1, + "fields": [ { - "title": "Right", - "dataType": "constructor", - "index": 1, - "fields": [ - { - "title": "Interval", - "anyOf": [ - { - "title": "Finite", - "dataType": "constructor", - "index": 0, - "fields": [ - { - "dataType": "integer" - } - ] - }, - { - "title": "Infinite", - "dataType": "constructor", - "index": 1, - "fields": [] - } - ] - } - ] + "$ref": "#/definitions/test_module~1Interval$Int" } ] } - }, - "compiledCode": "583b010000323232323232322253330054a22930b180080091129998030010a4c26600a6002600e0046660060066010004002ae695cdaab9f5742ae881" + ] + }, + "test_module/Interval$Int": { + "title": "Interval", + "anyOf": [ + { + "title": "Finite", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "$ref": "#/definitions/Int" + } + ] + }, + { + "title": "Infinite", + "dataType": "constructor", + "index": 1, + "fields": [] + } + ] } - ), + } + }), ) } #[test] - fn validator_phantom_types() { + fn list_2_tuples_as_map() { assert_validator( r#" type Dict { @@ -616,48 +639,60 @@ mod test { type UUID { UUID } - validator mint { + validator list_2_tuples_as_map { fn(redeemer: Dict, ctx: Void) { True } } "#, - json!( - { - "title": "test_module.mint", - "hash": "afddc16c18e7d8de379fb9aad39b3d1b5afd27603e5ebac818432a72", - "redeemer": { - "title": "Dict", - "schema": { - "anyOf": [ + json!({ + "title": "test_module.list_2_tuples_as_map", + "redeemer": { + "title": "redeemer", + "schema": { + "$ref": "#/definitions/test_module~1Dict$test_module~1UUID_Int" + } + }, + "compiledCode": "583b010000323232323232322253330054a22930b180080091129998030010a4c26600a6002600e0046660060066010004002ae695cdaab9f5742ae881", + "hash": "afddc16c18e7d8de379fb9aad39b3d1b5afd27603e5ebac818432a72", + "definitions": { + "ByteArray": { + "dataType": "bytes" + }, + "Int": { + "dataType": "integer" + }, + "List$Tuple$ByteArray_Int": { + "dataType": "map", + "keys": { + "$ref": "#/definitions/ByteArray" + }, + "values": { + "$ref": "#/definitions/Int" + } + }, + "test_module/Dict$test_module/UUID_Int": { + "title": "Dict", + "anyOf": [ + { + "title": "Dict", + "dataType": "constructor", + "index": 0, + "fields": [ { - "title": "Dict", - "dataType": "constructor", - "index": 0, - "fields": [ - { - "title": "inner", - "dataType": "map", - "keys": { - "dataType": "bytes" - }, - "values": { - "dataType": "integer" - } - } - ] + "$ref": "#/definitions/List$Tuple$ByteArray_Int" } ] } - }, - "compiledCode": "583b010000323232323232322253330054a22930b180080091129998030010a4c26600a6002600e0046660060066010004002ae695cdaab9f5742ae881" + ] } - ), + } + }), ); } #[test] - fn validator_opaque_types() { + fn opaque_singleton_variants() { assert_validator( r#" pub opaque type Dict { @@ -666,84 +701,185 @@ mod test { type UUID { UUID } - validator mint { + validator opaque_singleton_variants { fn(redeemer: Dict, ctx: Void) { True } } "#, - json!( - { - "title": "test_module.mint", - "hash": "afddc16c18e7d8de379fb9aad39b3d1b5afd27603e5ebac818432a72", - "redeemer": { - "title": "Dict", - "schema": { - "dataType": "map", - "keys": { - "dataType": "bytes" - }, - "values": { - "dataType": "integer" - } - } - }, - "compiledCode": "583b010000323232323232322253330054a22930b180080091129998030010a4c26600a6002600e0046660060066010004002ae695cdaab9f5742ae881" + json!({ + "title": "test_module.opaque_singleton_variants", + "redeemer": { + "title": "redeemer", + "schema": { + "$ref": "#/definitions/test_module~1Dict$test_module~1UUID_Int" } - ), + }, + "compiledCode": "583b010000323232323232322253330054a22930b180080091129998030010a4c26600a6002600e0046660060066010004002ae695cdaab9f5742ae881", + "hash": "afddc16c18e7d8de379fb9aad39b3d1b5afd27603e5ebac818432a72", + "definitions": { + "ByteArray": { + "dataType": "bytes" + }, + "Int": { + "dataType": "integer" + }, + "test_module/Dict$test_module/UUID_Int": { + "title": "Dict", + "dataType": "map", + "keys": { + "$ref": "#/definitions/ByteArray" + }, + "values": { + "$ref": "#/definitions/Int" + } + } + } + }), ); } #[test] - fn exported_data() { + fn nested_data() { assert_validator( r#" pub type Foo { foo: Data } - validator spend { + validator nested_data { fn(datum: Foo, redeemer: Int, ctx: Void) { True } } "#, - json!( - { - "title": "test_module.spend", - "hash": "a3dbab684d90d19e6bab3a0b00a7290ff59fe637d14428859bf74376", - "datum": { - "title": "Foo", - "schema": { - "anyOf": [{ - "title": "Foo", - "index": 0, - "fields": [{ - "title": "foo", - "description": "Any Plutus data.", - }], - "dataType": "constructor", - }] - }, - }, - "redeemer": { - "schema": { - "dataType": "integer", - } - }, - "compiledCode": "5840010000323232323232322232253330074a22930b1bad0013001001222533300600214984cc014c004c01c008ccc00c00cc0200080055cd2b9b5573eae855d101" + json!({ + "title": "test_module.nested_data", + "datum": { + "title": "datum", + "schema": { + "$ref": "#/definitions/test_module~1Foo" } - ), + }, + "redeemer": { + "title": "redeemer", + "schema": { + "$ref": "#/definitions/Int" + } + }, + "compiledCode": "5840010000323232323232322232253330074a22930b1bad0013001001222533300600214984cc014c004c01c008ccc00c00cc0200080055cd2b9b5573eae855d101", + "hash": "a3dbab684d90d19e6bab3a0b00a7290ff59fe637d14428859bf74376", + "definitions": { + "Data": { + "title": "Data", + "description": "Any Plutus data." + }, + "Int": { + "dataType": "integer" + }, + "test_module/Foo": { + "title": "Foo", + "anyOf": [ + { + "title": "Foo", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "$ref": "#/definitions/Data" + } + ] + } + ] + } + } + }), ); } #[test] - fn recursive_type() { + fn recursive_types() { + assert_validator( + r#" + pub type Expr { + Val(Int) + Sum(Expr, Expr) + Mul(Expr, Expr) + } + + validator recursive_types { + fn(redeemer: Expr, ctx: Void) { + True + } + } + "#, + json!({ + "title": "test_module.recursive_types", + "redeemer": { + "title": "redeemer", + "schema": { + "$ref": "#/definitions/test_module~1Expr" + } + }, + "compiledCode": "583b010000323232323232322253330054a22930b180080091129998030010a4c26600a6002600e0046660060066010004002ae695cdaab9f5742ae881", + "hash": "afddc16c18e7d8de379fb9aad39b3d1b5afd27603e5ebac818432a72", + "definitions": { + "Int": { + "dataType": "integer" + }, + "test_module/Expr": { + "title": "Expr", + "anyOf": [ + { + "title": "Val", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "$ref": "#/definitions/Int" + } + ] + }, + { + "title": "Sum", + "dataType": "constructor", + "index": 1, + "fields": [ + { + "$ref": "#/definitions/test_module~1Expr" + }, + { + "$ref": "#/definitions/test_module~1Expr" + } + ] + }, + { + "title": "Mul", + "dataType": "constructor", + "index": 2, + "fields": [ + { + "$ref": "#/definitions/test_module~1Expr" + }, + { + "$ref": "#/definitions/test_module~1Expr" + } + ] + } + ] + } + } + }), + ) + } + + #[test] + fn recursive_generic_types() { assert_validator( r#" pub type LinkedList { - Nil Cons(a, LinkedList) + Nil } pub type Foo { @@ -756,43 +892,147 @@ mod test { } } - validator spend { + validator recursive_generic_types { fn(datum: Foo, redeemer: LinkedList, ctx: Void) { True } } "#, json!({ - "redeemer": { - "schema": { - "$ref": "#/$defs/LinkedList_Int" + "title": "test_module.recursive_generic_types", + "datum": { + "title": "datum", + "schema": { + "$ref": "#/definitions/test_module~1Foo" + } + }, + "redeemer": { + "title": "redeemer", + "schema": { + "$ref": "#/definitions/test_module~1LinkedList$Int" + } + }, + "compiledCode": "583b0100003232323232323222253330064a22930b180080091129998030010a4c26600a6002600e0046660060066010004002ae695cdaab9f5742ae89", + "hash": "e37db487fbd58c45d059bcbf5cd6b1604d3bec16cf888f1395a4ebc4", + "definitions": { + "Bool": { + "title": "Bool", + "anyOf": [ + { + "title": "False", + "dataType": "constructor", + "index": 0, + "fields": [] + }, + { + "title": "True", + "dataType": "constructor", + "index": 1, + "fields": [] + } + ] + }, + "ByteArray": { + "dataType": "bytes" + }, + "Int": { + "dataType": "integer" + }, + "List$test_module/LinkedList$Int": { + "dataType": "list", + "items": { + "$ref": "#/definitions/test_module~1LinkedList$Int" } }, - "$defs": { - "LinkedList_Int": { - "anyOf": [ - { - "title": "Nil", - "dataType": "constructor", - "index": 0, - "fields": [] - }, - { - "title": "Cons", - "dataType": "constructor", - "index": 1, - "fields": [ - { - "dataType": "integer" - }, - { - "$ref": "#/$defs/LinkedList_Int" - }, - ] - } - ], - }, + "Tuple$ByteArray_List$test_module/LinkedList$Int": { + "title": "Tuple", + "dataType": "list", + "items": [ + { + "$ref": "#/definitions/ByteArray" + }, + { + "$ref": "#/definitions/List$test_module~1LinkedList$Int" + } + ] + }, + "test_module/Foo": { + "title": "Foo", + "anyOf": [ + { + "title": "Foo", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "$ref": "#/definitions/test_module~1LinkedList$Bool" + } + ] + }, + { + "title": "Bar", + "dataType": "constructor", + "index": 1, + "fields": [ + { + "$ref": "#/definitions/Int" + }, + { + "$ref": "#/definitions/Tuple$ByteArray_List$test_module~1LinkedList$Int" + } + ] + } + ] + }, + "test_module/LinkedList$Bool": { + "title": "LinkedList", + "anyOf": [ + { + "title": "Cons", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "$ref": "#/definitions/Bool" + }, + { + "$ref": "#/definitions/test_module~1LinkedList$Bool" + } + ] + }, + { + "title": "Nil", + "dataType": "constructor", + "index": 1, + "fields": [] + } + ] + }, + "test_module/LinkedList$Int": { + "title": "LinkedList", + "anyOf": [ + { + "title": "Cons", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "$ref": "#/definitions/Int" + }, + { + "$ref": "#/definitions/test_module~1LinkedList$Int" + } + ] + }, + { + "title": "Nil", + "dataType": "constructor", + "index": 1, + "fields": [] + } + ] } + } }), ) } diff --git a/crates/aiken-project/src/lib.rs b/crates/aiken-project/src/lib.rs index 199e1e7e..7f9abdab 100644 --- a/crates/aiken-project/src/lib.rs +++ b/crates/aiken-project/src/lib.rs @@ -12,7 +12,11 @@ pub mod pretty; pub mod script; pub mod telemetry; -use crate::blueprint::{schema::Schema, Blueprint}; +use crate::blueprint::{ + definitions::Reference, + schema::{Annotated, Schema}, + Blueprint, +}; use aiken_lang::{ ast::{Definition, Function, ModuleKind, Tracing, TypedDataType, TypedFunction}, builder::{DataTypeKey, FunctionAccessKey}, @@ -214,7 +218,10 @@ where self.compile(options) } - pub fn dump_uplc(&self, blueprint: &Blueprint) -> Result<(), Error> { + pub fn dump_uplc( + &self, + blueprint: &Blueprint>, + ) -> Result<(), Error> { let dir = self.root.join("artifacts"); self.event_listener @@ -355,7 +362,7 @@ where // Read blueprint let blueprint = File::open(self.blueprint_path()) .map_err(|_| blueprint::error::Error::InvalidOrMissingFile)?; - let blueprint: Blueprint = + let blueprint: Blueprint = serde_json::from_reader(BufReader::new(blueprint))?; // Calculate the address @@ -379,11 +386,11 @@ where &self, title: Option<&String>, param: &Term, - ) -> Result, Error> { + ) -> Result, Error> { // Read blueprint let blueprint = File::open(self.blueprint_path()) .map_err(|_| blueprint::error::Error::InvalidOrMissingFile)?; - let mut blueprint: Blueprint = + let mut blueprint: Blueprint = serde_json::from_reader(BufReader::new(blueprint))?; // Apply parameters diff --git a/crates/aiken/src/cmd/blueprint/convert.rs b/crates/aiken/src/cmd/blueprint/convert.rs index f92b8647..2adeb3c5 100644 --- a/crates/aiken/src/cmd/blueprint/convert.rs +++ b/crates/aiken/src/cmd/blueprint/convert.rs @@ -65,7 +65,7 @@ pub fn exec( .map_err(|_| BlueprintError::InvalidOrMissingFile) .into_diagnostic()?; - let blueprint: Blueprint = + let blueprint: Blueprint = serde_json::from_reader(BufReader::new(blueprint)).into_diagnostic()?; // Perform the conversion