diff --git a/CHANGELOG.md b/CHANGELOG.md index ba4c268c..202d09c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - **aiken**: support for `bench` keyword to define benchmarks. @Riley-Kilgore - **aiken-lang**: The compiler now raises a warning when attempting to destructure a record constructor without using named fields. See [#1084](https://github.com/aiken-lang/aiken/issues/1084). @KtorZ +- **aiken-lang**: Fix blueprint schema definitions related to pairs (no longer omit (sometimes) Pairs definitions, and generate them as data List). See [#1086](https://github.com/aiken-lang/aiken/issues/1086) and [#970](https://github.com/aiken-lang/aiken/issues/970). @KtorZ ## v1.1.10 - 2025-01-21 diff --git a/crates/aiken-lang/src/parser/expr/bytearray.rs b/crates/aiken-lang/src/parser/expr/bytearray.rs index 376f3eeb..d5f5ce29 100644 --- a/crates/aiken-lang/src/parser/expr/bytearray.rs +++ b/crates/aiken-lang/src/parser/expr/bytearray.rs @@ -66,11 +66,15 @@ mod tests { #[test] fn g1_element() { - assert_expr!("#\"950dfd33da2682260c76038dfb8bad6e84ae9d599a3c151815945ac1e6ef6b1027cd917f3907479d20d636ce437a41f5\""); + assert_expr!( + "#\"950dfd33da2682260c76038dfb8bad6e84ae9d599a3c151815945ac1e6ef6b1027cd917f3907479d20d636ce437a41f5\"" + ); } #[test] fn g2_element() { - assert_expr!("#\"b0629fa1158c2d23a10413fe91d381a84d25e31d041cd0377d25828498fd02011b35893938ced97535395e4815201e67108bcd4665e0db25d602d76fa791fab706c54abf5e1a9e44b4ac1e6badf3d2ac0328f5e30be341677c8bac5dda7682f1\""); + assert_expr!( + "#\"b0629fa1158c2d23a10413fe91d381a84d25e31d041cd0377d25828498fd02011b35893938ced97535395e4815201e67108bcd4665e0db25d602d76fa791fab706c54abf5e1a9e44b4ac1e6badf3d2ac0328f5e30be341677c8bac5dda7682f1\"" + ); } } diff --git a/crates/aiken-project/src/blueprint/definitions.rs b/crates/aiken-project/src/blueprint/definitions.rs index efcd8569..360a11dd 100644 --- a/crates/aiken-project/src/blueprint/definitions.rs +++ b/crates/aiken-project/src/blueprint/definitions.rs @@ -1,3 +1,10 @@ +use crate::{ + blueprint::{ + parameter::Parameter, + schema::{Data, Declaration, Items}, + }, + Annotated, Schema, +}; use aiken_lang::tipo::{pretty::resolve_alias, Type, TypeAliasAnnotation, TypeVar}; use itertools::Itertools; use serde::{ @@ -6,7 +13,7 @@ use serde::{ ser::{Serialize, SerializeStruct, Serializer}, }; use std::{ - collections::{BTreeMap, HashMap}, + collections::{BTreeMap, BTreeSet, HashMap}, fmt::{self, Display}, ops::Deref, rc::Rc, @@ -88,6 +95,218 @@ impl Definitions { } } +impl Definitions> { + /// Remove orphan definitions. Such definitions can exist due to List of pairs being + /// transformed to Maps. As a consequence, we may generate temporary Pair definitions + /// which needs to get cleaned up later. + /// + /// Initially, we would clean those Pair definitions right-away, but this would cause + /// Pair definitions to be missing in some legit cases when the Pair is also used as a + /// standalone type. + pub fn prune_orphan_pairs(&mut self, parameters: Vec<&Parameter>) -> &mut Self { + fn traverse_schema( + src: Reference, + schema: &Schema, + usage: &mut BTreeMap>, + ) { + match schema { + Schema::Unit + | Schema::Boolean + | Schema::Integer + | Schema::Bytes + | Schema::String => (), + Schema::Pair(left, right) => { + mark(src.clone(), left, usage, traverse_schema); + mark(src, right, usage, traverse_schema); + } + Schema::List(Items::One(item)) => { + mark(src, item, usage, traverse_schema); + } + Schema::List(Items::Many(items)) => { + items.iter().for_each(|item| { + mark(src.clone(), item, usage, traverse_schema); + }); + } + Schema::Data(data) => traverse_data(src, data, usage), + } + } + + fn traverse_data( + src: Reference, + data: &Data, + usage: &mut BTreeMap>, + ) { + match data { + Data::Opaque | Data::Integer | Data::Bytes => (), + Data::List(Items::One(item)) => { + mark(src, item, usage, traverse_data); + } + Data::List(Items::Many(items)) => { + items.iter().for_each(|item| { + mark(src.clone(), item, usage, traverse_data); + }); + } + Data::Map(keys, values) => { + mark(src.clone(), keys, usage, traverse_data); + mark(src, values, usage, traverse_data); + } + Data::AnyOf(items) => { + items.iter().for_each(|item| { + item.annotated.fields.iter().for_each(|field| { + mark(src.clone(), &field.annotated, usage, traverse_data); + }) + }); + } + } + } + + /// A mutually recursive function which works with either traverse_data or traverse_schema; + /// it is meant to peel the 'Declaration' and keep traversing if needed (when inline). + fn mark( + src: Reference, + declaration: &Declaration, + usage: &mut BTreeMap>, + mut traverse: F, + ) where + F: FnMut(Reference, &T, &mut BTreeMap>), + { + match declaration { + Declaration::Referenced(reference) => { + if let Some(dependencies) = usage.get_mut(reference) { + dependencies.insert(src); + } + } + Declaration::Inline(ref schema) => traverse(src, schema, usage), + } + } + + let mut usage: BTreeMap> = BTreeMap::new(); + + // 1. List all Pairs definitions + for (src, annotated) in self.inner.iter() { + if let Some(schema) = annotated.as_ref().map(|entry| &entry.annotated) { + if matches!(schema, Schema::Pair(_, _)) { + usage.insert(Reference::new(src), BTreeSet::new()); + } + } + } + + // 2. Mark those used in other definitions + for (src, annotated) in self.inner.iter() { + if let Some(schema) = annotated.as_ref().map(|entry| &entry.annotated) { + traverse_schema(Reference::new(src), schema, &mut usage) + } + } + + // 3. Mark also pairs definitions used in parameters / datums / redeemers + for (ix, param) in parameters.iter().enumerate() { + mark( + // NOTE: The name isn't important, so long as it doesn't clash with other typical + // references. If a definition is used in either of the parameter, it'll appear in + // its dependencies and won't ever be removed because parameters are considered + // always necessary. + Reference::new(&format!("__param^{ix}")), + ¶m.schema, + &mut usage, + traverse_schema, + ); + } + + // 4. Repeatedly remove pairs definitions that aren't used. We need doing this repeatedly + // because a Pair definition may only be used by an unused one; so as we prune the first + // unused definitions, new ones may become unused. + let mut last_len = usage.len(); + loop { + let mut unused = None; + for (k, v) in usage.iter() { + if v.is_empty() { + unused = Some(k.clone()); + } + } + + if let Some(k) = unused { + usage.remove(&k); + self.inner.remove(k.as_key().as_str()); + for (_, v) in usage.iter_mut() { + v.remove(&k); + } + } + + if usage.len() == last_len { + break; + } else { + last_len = usage.len(); + } + } + + self + } + + pub fn replace_pairs_with_data_lists(&mut self) { + fn swap_declaration(declaration: &mut Declaration) -> Declaration { + match std::mem::replace(declaration, Declaration::Inline(Schema::Unit.into())) { + Declaration::Referenced(reference) => Declaration::Referenced(reference), + Declaration::Inline(mut inline) => { + schema_to_data(&mut inline); + if let Schema::Data(data) = *inline { + Declaration::Inline(data.into()) + } else { + unreachable!() + } + } + } + } + + fn schema_to_data(schema: &mut Schema) { + let items = match schema { + Schema::Data(_) => None, + Schema::Pair(ref mut left, ref mut right) => { + let left = swap_declaration(left); + let right = swap_declaration(right); + Some(Items::Many(vec![left, right])) + } + Schema::List(Items::One(ref mut item)) => { + let item = swap_declaration(item); + Some(Items::One(item)) + } + Schema::List(Items::Many(ref mut items)) => Some(Items::Many( + items.iter_mut().map(swap_declaration).collect(), + )), + Schema::Integer => { + *schema = Schema::Data(Data::Integer); + None + } + Schema::Bytes => { + *schema = Schema::Data(Data::Bytes); + None + } + Schema::String => { + *schema = Schema::Data(Data::Bytes); + None + } + Schema::Unit => { + *schema = Schema::void(); + None + } + Schema::Boolean => { + *schema = Schema::bool(); + None + } + }; + + if let Some(items) = items { + *schema = Schema::Data(Data::List(items)); + } + } + + for (_, entry) in self.inner.iter_mut() { + if let Some(ref mut annotated) = entry { + schema_to_data(&mut annotated.annotated); + } + } + } +} + // ---------- Reference /// A URI pointer to an underlying data-type. diff --git a/crates/aiken-project/src/blueprint/schema.rs b/crates/aiken-project/src/blueprint/schema.rs index f8b68c2c..ea9f7471 100644 --- a/crates/aiken-project/src/blueprint/schema.rs +++ b/crates/aiken-project/src/blueprint/schema.rs @@ -97,6 +97,48 @@ pub enum Schema { Data(Data), } +impl Schema { + pub fn void() -> Self { + Schema::Data(Data::AnyOf(vec![Annotated { + title: None, + description: None, + annotated: Constructor { + index: 0, + fields: vec![], + }, + }])) + } + + pub fn int() -> Self { + Schema::Data(Data::Integer) + } + + pub fn bytes() -> Self { + Schema::Data(Data::Bytes) + } + + pub fn bool() -> Self { + 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![], + }, + }, + ])) + } +} + /// A schema for Plutus' Data. #[derive(Debug, PartialEq, Eq, Clone)] pub enum Data { @@ -205,46 +247,22 @@ impl Annotated { annotated: Schema::Data(Data::Opaque), }), - "ByteArray" => Ok(with_title(title.as_ref(), Schema::Data(Data::Bytes))), + "ByteArray" => Ok(with_title(title.as_ref(), Schema::bytes())), - "Int" => Ok(with_title(title.as_ref(), Schema::Data(Data::Integer))), + "Int" => Ok(with_title(title.as_ref(), Schema::int())), "String" => Ok(with_title(title.as_ref(), Schema::String)), "Void" => Ok(Annotated { title: title.or(Some("Unit".to_string())), description: None, - annotated: Schema::Data(Data::AnyOf(vec![Annotated { - title: None, - description: None, - annotated: Constructor { - index: 0, - fields: vec![], - }, - }])), + annotated: Schema::void(), }), "Bool" => Ok(Annotated { title: title.or(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![], - }, - }, - ])), + annotated: Schema::bool(), }), "Ordering" => Ok(Annotated { @@ -346,8 +364,6 @@ impl Annotated { annotated: Schema::Pair(left, right), .. }) => { - definitions.remove(&generic); - let left = left.map(|inner| match inner { Schema::Data(data) => data, _ => panic!("impossible: left inhabitant of pair isn't Data but: {inner:#?}"), diff --git a/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__map_in_map.snap b/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__map_in_map.snap new file mode 100644 index 00000000..c87f4655 --- /dev/null +++ b/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__map_in_map.snap @@ -0,0 +1,73 @@ +--- +source: crates/aiken-project/src/blueprint/validator.rs +description: "Code:\n\npub type OuterMap =\n List>\n\npub type InnerMap =\n List>\n\nvalidator placeholder {\n spend(_datum: Option, _redeemer: OuterMap, _utxo: Data, _self: Data,) {\n True\n }\n}\n" +--- +{ + "title": "test_module.placeholder.spend", + "datum": { + "title": "_datum", + "schema": { + "$ref": "#/definitions/Void" + } + }, + "redeemer": { + "title": "_redeemer", + "schema": { + "$ref": "#/definitions/OuterMap" + } + }, + "compiledCode": "", + "hash": "", + "definitions": { + "Bool": { + "title": "Bool", + "anyOf": [ + { + "title": "False", + "dataType": "constructor", + "index": 0, + "fields": [] + }, + { + "title": "True", + "dataType": "constructor", + "index": 1, + "fields": [] + } + ] + }, + "InnerMap": { + "title": "InnerMap", + "dataType": "map", + "keys": { + "$ref": "#/definitions/Int" + }, + "values": { + "$ref": "#/definitions/Bool" + } + }, + "Int": { + "dataType": "integer" + }, + "OuterMap": { + "title": "OuterMap", + "dataType": "map", + "keys": { + "$ref": "#/definitions/Int" + }, + "values": { + "$ref": "#/definitions/InnerMap" + } + }, + "Void": { + "title": "Unit", + "anyOf": [ + { + "dataType": "constructor", + "index": 0, + "fields": [] + } + ] + } + } +} diff --git a/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__pair_in_pair.snap b/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__pair_in_pair.snap new file mode 100644 index 00000000..e5cb9c86 --- /dev/null +++ b/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__pair_in_pair.snap @@ -0,0 +1,72 @@ +--- +source: crates/aiken-project/src/blueprint/validator.rs +description: "Code:\n\npub type MyPair =\n Pair\n\npub type InnerPair =\n Pair\n\nvalidator placeholder {\n spend(_datum: Option, _redeemer: List, _utxo: Data, _self: Data,) {\n True\n }\n}\n" +--- +{ + "title": "test_module.placeholder.spend", + "datum": { + "title": "_datum", + "schema": { + "$ref": "#/definitions/Void" + } + }, + "redeemer": { + "title": "_redeemer", + "schema": { + "$ref": "#/definitions/List$MyPair" + } + }, + "compiledCode": "", + "hash": "", + "definitions": { + "Bool": { + "title": "Bool", + "anyOf": [ + { + "title": "False", + "dataType": "constructor", + "index": 0, + "fields": [] + }, + { + "title": "True", + "dataType": "constructor", + "index": 1, + "fields": [] + } + ] + }, + "InnerPair": { + "title": "InnerPair", + "dataType": "#pair", + "left": { + "$ref": "#/definitions/Int" + }, + "right": { + "$ref": "#/definitions/Bool" + } + }, + "Int": { + "dataType": "integer" + }, + "List$MyPair": { + "dataType": "map", + "keys": { + "$ref": "#/definitions/Int" + }, + "values": { + "$ref": "#/definitions/InnerPair" + } + }, + "Void": { + "title": "Unit", + "anyOf": [ + { + "dataType": "constructor", + "index": 0, + "fields": [] + } + ] + } + } +} diff --git a/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__pair_of_lists.snap b/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__pair_of_lists.snap new file mode 100644 index 00000000..6f492bd9 --- /dev/null +++ b/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__pair_of_lists.snap @@ -0,0 +1,65 @@ +--- +source: crates/aiken-project/src/blueprint/validator.rs +description: "Code:\n\npub type MyPair =\n Pair, Bool>\n\nvalidator placeholder {\n spend(_datum: Option, _redeemer: MyPair, _utxo: Data, _self: Data,) {\n True\n }\n}\n" +--- +{ + "title": "test_module.placeholder.spend", + "datum": { + "title": "_datum", + "schema": { + "$ref": "#/definitions/Data" + } + }, + "redeemer": { + "title": "_redeemer", + "schema": { + "$ref": "#/definitions/MyPair" + } + }, + "compiledCode": "", + "hash": "", + "definitions": { + "Bool": { + "title": "Bool", + "anyOf": [ + { + "title": "False", + "dataType": "constructor", + "index": 0, + "fields": [] + }, + { + "title": "True", + "dataType": "constructor", + "index": 1, + "fields": [] + } + ] + }, + "Data": { + "title": "Data", + "description": "Any Plutus data." + }, + "Int": { + "dataType": "integer" + }, + "List$Int": { + "dataType": "list", + "items": { + "$ref": "#/definitions/Int" + } + }, + "MyPair": { + "title": "MyPair", + "dataType": "list", + "items": [ + { + "$ref": "#/definitions/List$Int" + }, + { + "$ref": "#/definitions/Bool" + } + ] + } + } +} diff --git a/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__pair_used_after_map.snap b/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__pair_used_after_map.snap new file mode 100644 index 00000000..c1f9ac28 --- /dev/null +++ b/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__pair_used_after_map.snap @@ -0,0 +1,77 @@ +--- +source: crates/aiken-project/src/blueprint/validator.rs +description: "Code:\n\npub type MyPair =\n Pair\n\npub type MyDatum {\n pairs: List,\n pair: MyPair,\n}\n\nvalidator placeholder {\n spend(_datum: Option, _redeemer: Void, _utxo: Data, _self: Data,) {\n True\n }\n}\n" +--- +{ + "title": "test_module.placeholder.spend", + "datum": { + "title": "_datum", + "schema": { + "$ref": "#/definitions/test_module~1MyDatum" + } + }, + "redeemer": { + "title": "_redeemer", + "schema": { + "$ref": "#/definitions/Void" + } + }, + "compiledCode": "", + "hash": "", + "definitions": { + "Int": { + "dataType": "integer" + }, + "List$MyPair": { + "dataType": "map", + "keys": { + "$ref": "#/definitions/Int" + }, + "values": { + "$ref": "#/definitions/Int" + } + }, + "MyPair": { + "title": "MyPair", + "dataType": "list", + "items": [ + { + "$ref": "#/definitions/Int" + }, + { + "$ref": "#/definitions/Int" + } + ] + }, + "Void": { + "title": "Unit", + "anyOf": [ + { + "dataType": "constructor", + "index": 0, + "fields": [] + } + ] + }, + "test_module/MyDatum": { + "title": "MyDatum", + "anyOf": [ + { + "title": "MyDatum", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "title": "pairs", + "$ref": "#/definitions/List$MyPair" + }, + { + "title": "pair", + "$ref": "#/definitions/MyPair" + } + ] + } + ] + } + } +} diff --git a/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__pair_used_before_map.snap b/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__pair_used_before_map.snap new file mode 100644 index 00000000..1ecdb0d7 --- /dev/null +++ b/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__pair_used_before_map.snap @@ -0,0 +1,77 @@ +--- +source: crates/aiken-project/src/blueprint/validator.rs +description: "Code:\n\npub type MyPair =\n Pair\n\npub type MyDatum {\n pair: MyPair,\n pairs: List,\n}\n\nvalidator placeholder {\n spend(_datum: Option, _redeemer: Void, _utxo: Data, _self: Data,) {\n True\n }\n}\n" +--- +{ + "title": "test_module.placeholder.spend", + "datum": { + "title": "_datum", + "schema": { + "$ref": "#/definitions/test_module~1MyDatum" + } + }, + "redeemer": { + "title": "_redeemer", + "schema": { + "$ref": "#/definitions/Void" + } + }, + "compiledCode": "", + "hash": "", + "definitions": { + "Int": { + "dataType": "integer" + }, + "List$MyPair": { + "dataType": "map", + "keys": { + "$ref": "#/definitions/Int" + }, + "values": { + "$ref": "#/definitions/Int" + } + }, + "MyPair": { + "title": "MyPair", + "dataType": "list", + "items": [ + { + "$ref": "#/definitions/Int" + }, + { + "$ref": "#/definitions/Int" + } + ] + }, + "Void": { + "title": "Unit", + "anyOf": [ + { + "dataType": "constructor", + "index": 0, + "fields": [] + } + ] + }, + "test_module/MyDatum": { + "title": "MyDatum", + "anyOf": [ + { + "title": "MyDatum", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "title": "pair", + "$ref": "#/definitions/MyPair" + }, + { + "title": "pairs", + "$ref": "#/definitions/List$MyPair" + } + ] + } + ] + } + } +} 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 index 16b93cc6..25351dd4 100644 --- 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 @@ -78,13 +78,15 @@ description: "Code:\n\npub type Asset = (ByteArray, Int)\n\npub type POSIXTime = }, "Pair$POSIXTime_Bool": { "title": "Pair", - "dataType": "#pair", - "left": { - "$ref": "#/definitions/POSIXTime" - }, - "right": { - "$ref": "#/definitions/Bool" - } + "dataType": "list", + "items": [ + { + "$ref": "#/definitions/POSIXTime" + }, + { + "$ref": "#/definitions/Bool" + } + ] }, "Pairs$Asset_POSIXTime": { "title": "Pairs", diff --git a/crates/aiken-project/src/blueprint/validator.rs b/crates/aiken-project/src/blueprint/validator.rs index e433df7e..36b476b8 100644 --- a/crates/aiken-project/src/blueprint/validator.rs +++ b/crates/aiken-project/src/blueprint/validator.rs @@ -122,7 +122,7 @@ impl Validator { ), }) }) - .collect::>()?; + .collect::, _>>()?; let (datum, redeemer) = if func.name == well_known::VALIDATOR_ELSE { (None, None) @@ -202,6 +202,16 @@ impl Validator { schema: Declaration::Inline(Box::new(Schema::Data(Data::Opaque))), })); + definitions + .prune_orphan_pairs( + parameters + .iter() + .chain(redeemer.as_ref().map(|x| vec![x]).unwrap_or_default()) + .chain(datum.as_ref().map(|x| vec![x]).unwrap_or_default()) + .collect::>(), + ) + .replace_pairs_with_data_lists(); + Ok(Validator { title: format!("{}.{}.{}", &module.name, &def.name, &func.name,), description: func.doc.clone(), @@ -736,6 +746,83 @@ mod tests { ); } + #[test] + fn pair_used_after_map() { + assert_validator!( + r#" + pub type MyPair = + Pair + + pub type MyDatum { + pairs: List, + pair: MyPair, + } + + validator placeholder { + spend(_datum: Option, _redeemer: Void, _utxo: Data, _self: Data,) { + True + } + } + "# + ); + } + + #[test] + fn pair_used_before_map() { + assert_validator!( + r#" + pub type MyPair = + Pair + + pub type MyDatum { + pair: MyPair, + pairs: List, + } + + validator placeholder { + spend(_datum: Option, _redeemer: Void, _utxo: Data, _self: Data,) { + True + } + } + "# + ); + } + + #[test] + fn map_in_map() { + assert_validator!( + r#" + pub type OuterMap = + List> + + pub type InnerMap = + List> + + validator placeholder { + spend(_datum: Option, _redeemer: OuterMap, _utxo: Data, _self: Data,) { + True + } + } + "# + ); + } + + #[test] + fn pair_of_lists() { + assert_validator!( + r#" + pub type MyPair = + Pair, Bool> + + validator placeholder { + spend(_datum: Option, _redeemer: MyPair, _utxo: Data, _self: Data,) { + True + } + } + "# + ); + } + #[test] fn else_redeemer() { assert_validator!(