From f60df16bc28aa142c9040c658c28561a5a05fc58 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Tue, 3 Sep 2024 16:39:50 +0200 Subject: [PATCH] Preserve type-aliases during blueprint generation. --- CHANGELOG.md | 2 + crates/aiken-lang/src/tipo/pretty.rs | 2 +- .../src/blueprint/definitions.rs | 54 ++++++- crates/aiken-project/src/blueprint/schema.rs | 61 ++++++-- ...t__validator__tests__simplified_hydra.snap | 11 +- ...print__validator__tests__type_aliases.snap | 145 ++++++++++++++++++ .../aiken-project/src/blueprint/validator.rs | 29 ++++ 7 files changed, 286 insertions(+), 18 deletions(-) create mode 100644 crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__type_aliases.snap diff --git a/CHANGELOG.md b/CHANGELOG.md index ec8b59d5..d7d87370 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ - **aiken-project**: modules starting with `@hidden` in their docs will be skipped from docs generation. @KtorZ +- **aiken-project**: preserve type-aliases as titles in blueprint generated schemas. @KtorZ + - **uplc**: support evaluation of Plutus V3 transactions, including new purposes introduced in Conway. @KtorZ ### Changed diff --git a/crates/aiken-lang/src/tipo/pretty.rs b/crates/aiken-lang/src/tipo/pretty.rs index a38b899f..d0b437ec 100644 --- a/crates/aiken-lang/src/tipo/pretty.rs +++ b/crates/aiken-lang/src/tipo/pretty.rs @@ -214,7 +214,7 @@ fn qualify_type_name(module: &String, typ_name: &str) -> Document<'static> { } } -fn resolve_alias( +pub fn resolve_alias( parameters: &[String], annotation: &Annotation, typ: &Type, diff --git a/crates/aiken-project/src/blueprint/definitions.rs b/crates/aiken-project/src/blueprint/definitions.rs index 2cc70a0d..efcd8569 100644 --- a/crates/aiken-project/src/blueprint/definitions.rs +++ b/crates/aiken-project/src/blueprint/definitions.rs @@ -1,4 +1,5 @@ -use aiken_lang::tipo::{Type, TypeVar}; +use aiken_lang::tipo::{pretty::resolve_alias, Type, TypeAliasAnnotation, TypeVar}; +use itertools::Itertools; use serde::{ self, de::{self, Deserialize, Deserializer, MapAccess, Visitor}, @@ -125,6 +126,22 @@ impl Display for Reference { impl Reference { pub fn from_type(type_info: &Type, type_parameters: &HashMap>) -> Self { + if let Some(TypeAliasAnnotation { + alias, + parameters, + annotation, + }) = type_info.alias().as_deref() + { + if let Some(resolved_parameters) = resolve_alias(parameters, annotation, type_info) { + return Self::from_type_alias( + type_info, + alias.to_string(), + resolved_parameters, + type_parameters, + ); + } + } + match type_info { Type::App { module, name, args, .. @@ -190,6 +207,41 @@ impl Reference { } } } + + fn from_type_alias( + type_info: &Type, + alias: String, + parameters: Vec>, + type_parameters: &HashMap>, + ) -> Self { + if !parameters.is_empty() { + Reference { + inner: format!( + "{alias}${}", + parameters + .iter() + .map(|param| { + // Avoid infinite recursion for recursive types instantiated to + // themselves. For example: type Identity = t + if param.as_ref() == type_info { + Self::from_type( + type_info.clone().set_alias(None).as_ref(), + type_parameters, + ) + .inner + } else { + Self::from_type(param, type_parameters).inner + } + }) + .join("_"), + ), + } + } else { + Reference { + inner: alias.clone(), + } + } + } } impl Serialize for Reference { diff --git a/crates/aiken-project/src/blueprint/schema.rs b/crates/aiken-project/src/blueprint/schema.rs index 88f391ce..0cb5fe9b 100644 --- a/crates/aiken-project/src/blueprint/schema.rs +++ b/crates/aiken-project/src/blueprint/schema.rs @@ -172,6 +172,24 @@ impl Annotated { type_parameters: &mut HashMap>, definitions: &mut Definitions, ) -> Result { + let title = if type_info.alias().is_some() { + Some(type_info.to_pretty(0)) + } else { + None + }; + + fn with_title(title: Option<&String>, annotated: Schema) -> Annotated { + if title.is_some() { + Annotated { + title: title.cloned(), + description: None, + annotated, + } + } else { + annotated.into() + } + } + match type_info { Type::App { module: module_name, @@ -182,20 +200,20 @@ impl Annotated { definitions.register(type_info, &type_parameters.clone(), |definitions| { match &type_name[..] { "Data" => Ok(Annotated { - title: Some("Data".to_string()), + title: title.or(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(with_title(title.as_ref(), Schema::Data(Data::Bytes))), - "Int" => Ok(Schema::Data(Data::Integer).into()), + "Int" => Ok(with_title(title.as_ref(), Schema::Data(Data::Integer))), - "String" => Ok(Schema::String.into()), + "String" => Ok(with_title(title.as_ref(), Schema::String)), "Void" => Ok(Annotated { - title: Some("Unit".to_string()), - description: Some("The nullary constructor.".to_string()), + title: title.or(Some("Unit".to_string())), + description: None, annotated: Schema::Data(Data::AnyOf(vec![Annotated { title: None, description: None, @@ -207,7 +225,7 @@ impl Annotated { }), "Bool" => Ok(Annotated { - title: Some("Bool".to_string()), + title: title.or(Some("Bool".to_string())), description: None, annotated: Schema::Data(Data::AnyOf(vec![ Annotated { @@ -230,7 +248,7 @@ impl Annotated { }), "Ordering" => Ok(Annotated { - title: Some("Ordering".to_string()), + title: title.or(Some("Ordering".to_string())), description: None, annotated: Schema::Data(Data::AnyOf(vec![ Annotated { @@ -260,6 +278,23 @@ impl Annotated { ])), }), + "Never" => { + Ok(Annotated { + title: title.or(Some("Never".to_string())), + description: None, + annotated: Schema::Data(Data::AnyOf(vec![ + Annotated { + title: Some("Never".to_string()), + description: Some("Nothing.".to_string()), + annotated: Constructor { + index: 1, + fields: vec![], + }, + }, + ])), + }) + } + "Option" => { let generic = Annotated::do_from_type( args.first() @@ -270,7 +305,7 @@ impl Annotated { )?; Ok(Annotated { - title: Some("Optional".to_string()), + title: title.or(Some("Option".to_string())), description: None, annotated: Schema::Data(Data::AnyOf(vec![ Annotated { @@ -329,7 +364,7 @@ impl Annotated { _ => Data::List(Items::One(Declaration::Referenced(generic))), }; - Ok(Schema::Data(data).into()) + Ok(with_title(title.as_ref(), Schema::Data(data))) } _ => Err(Error::new(ErrorContext::UnsupportedType, type_info)), @@ -360,7 +395,7 @@ impl Annotated { ); Ok(Annotated { - title: Some(data_type.name.clone()), + title: title.or(Some(data_type.name.clone())), description: data_type.doc.clone().map(|s| s.trim().to_string()), annotated, }) @@ -377,7 +412,7 @@ impl Annotated { .map_err(|e| e.backtrack(type_info))?; Ok(Annotated { - title: Some("Pair".to_owned()), + title: title.or(Some("Pair".to_owned())), description: None, annotated: Schema::Pair(left, right), }) @@ -396,7 +431,7 @@ impl Annotated { .map_err(|e| e.backtrack(type_info))?; Ok(Annotated { - title: Some("Tuple".to_owned()), + title: title.or(Some("Tuple".to_owned())), description: None, annotated: Schema::Data(Data::List(Items::Many(elems))), }) diff --git a/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__simplified_hydra.snap b/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__simplified_hydra.snap index 144c582d..3a54c1ba 100644 --- a/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__simplified_hydra.snap +++ b/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__simplified_hydra.snap @@ -20,17 +20,22 @@ description: "Code:\n\n/// On-chain state\npub type State {\n /// The contest "hash": "", "definitions": { "ByteArray": { + "title": "ByteArray", "dataType": "bytes" }, "Int": { "dataType": "integer" }, - "List$ByteArray": { + "List$Party": { "dataType": "list", "items": { - "$ref": "#/definitions/ByteArray" + "$ref": "#/definitions/Party" } }, + "Party": { + "title": "Party", + "dataType": "bytes" + }, "test_module/ContestationPeriod": { "title": "ContestationPeriod", "description": "Whatever", @@ -89,7 +94,7 @@ description: "Code:\n\n/// On-chain state\npub type State {\n /// The contest { "title": "parties", "description": "List of public key hashes of all participants", - "$ref": "#/definitions/List$ByteArray" + "$ref": "#/definitions/List$Party" }, { "title": "utxoHash", diff --git a/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__type_aliases.snap b/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__type_aliases.snap new file mode 100644 index 00000000..16b93cc6 --- /dev/null +++ b/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__type_aliases.snap @@ -0,0 +1,145 @@ +--- +source: crates/aiken-project/src/blueprint/validator.rs +description: "Code:\n\npub type Asset = (ByteArray, Int)\n\npub type POSIXTime = Int\n\npub type AlwaysNone = Never\n\npub type MyDatum {\n Either(AlwaysNone)\n OrElse(Pair)\n}\n\npub type MyRedeemer {\n fst_field: List,\n snd_field: Pairs\n}\n\nvalidator recursive_types {\n spend(datum: Option, redeemer: MyRedeemer, output_reference: Data, transaction: Data) {\n True\n }\n}\n" +--- +{ + "title": "test_module.recursive_types.spend", + "datum": { + "title": "datum", + "schema": { + "$ref": "#/definitions/test_module~1MyDatum" + } + }, + "redeemer": { + "title": "redeemer", + "schema": { + "$ref": "#/definitions/test_module~1MyRedeemer$Asset" + } + }, + "compiledCode": "", + "hash": "", + "definitions": { + "AlwaysNone": { + "title": "AlwaysNone", + "anyOf": [ + { + "title": "Never", + "description": "Nothing.", + "dataType": "constructor", + "index": 1, + "fields": [] + } + ] + }, + "Asset": { + "title": "Asset", + "dataType": "list", + "items": [ + { + "$ref": "#/definitions/ByteArray" + }, + { + "$ref": "#/definitions/Int" + } + ] + }, + "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$Asset": { + "dataType": "list", + "items": { + "$ref": "#/definitions/Asset" + } + }, + "POSIXTime": { + "title": "POSIXTime", + "dataType": "integer" + }, + "Pair$POSIXTime_Bool": { + "title": "Pair", + "dataType": "#pair", + "left": { + "$ref": "#/definitions/POSIXTime" + }, + "right": { + "$ref": "#/definitions/Bool" + } + }, + "Pairs$Asset_POSIXTime": { + "title": "Pairs", + "dataType": "map", + "keys": { + "$ref": "#/definitions/Asset" + }, + "values": { + "$ref": "#/definitions/POSIXTime" + } + }, + "test_module/MyDatum": { + "title": "MyDatum", + "anyOf": [ + { + "title": "Either", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "$ref": "#/definitions/AlwaysNone" + } + ] + }, + { + "title": "OrElse", + "dataType": "constructor", + "index": 1, + "fields": [ + { + "$ref": "#/definitions/Pair$POSIXTime_Bool" + } + ] + } + ] + }, + "test_module/MyRedeemer$Asset": { + "title": "MyRedeemer", + "anyOf": [ + { + "title": "MyRedeemer", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "title": "fst_field", + "$ref": "#/definitions/List$Asset" + }, + { + "title": "snd_field", + "$ref": "#/definitions/Pairs$Asset_POSIXTime" + } + ] + } + ] + } + } +} diff --git a/crates/aiken-project/src/blueprint/validator.rs b/crates/aiken-project/src/blueprint/validator.rs index 1be58a9b..0c36f8ae 100644 --- a/crates/aiken-project/src/blueprint/validator.rs +++ b/crates/aiken-project/src/blueprint/validator.rs @@ -695,6 +695,35 @@ mod tests { ); } + #[test] + fn type_aliases() { + assert_validator!( + r#" + pub type Asset = (ByteArray, Int) + + pub type POSIXTime = Int + + pub type AlwaysNone = Never + + pub type MyDatum { + Either(AlwaysNone) + OrElse(Pair) + } + + pub type MyRedeemer { + fst_field: List, + snd_field: Pairs + } + + validator recursive_types { + spend(datum: Option, redeemer: MyRedeemer, output_reference: Data, transaction: Data) { + True + } + } + "# + ); + } + #[test] fn validate_arguments_integer() { let definitions = fixture_definitions();