diff --git a/crates/aiken-project/src/blueprint/schema.rs b/crates/aiken-project/src/blueprint/schema.rs index f07e02e7..dc40915c 100644 --- a/crates/aiken-project/src/blueprint/schema.rs +++ b/crates/aiken-project/src/blueprint/schema.rs @@ -1,5 +1,7 @@ -use crate::blueprint::definitions::{Definitions, Reference}; -use crate::CheckedModule; +use crate::{ + blueprint::definitions::{Definitions, Reference}, + CheckedModule, +}; use aiken_lang::{ ast::{Definition, TypedDataType, TypedDefinition}, builtins::wrapped_redeemer, @@ -12,8 +14,7 @@ use serde::{ ser::{Serialize, SerializeStruct, Serializer}, }; use serde_json as json; -use std::rc::Rc; -use std::{collections::HashMap, fmt, ops::Deref}; +use std::{collections::HashMap, fmt, ops::Deref, rc::Rc}; // NOTE: Can be anything BUT 0 pub const REDEEMER_DISCRIMINANT: usize = 1; @@ -385,21 +386,6 @@ impl Annotated { Type::Fn { .. } => unreachable!(), } } - - fn into_data(self, type_info: &Type) -> Result, Error> { - match self { - Annotated { - title, - description, - annotated: Schema::Data(data), - } => Ok(Annotated { - title, - description, - annotated: data, - }), - _ => Err(Error::new(ErrorContext::ExpectedData, type_info)), - } - } } impl Data { @@ -409,28 +395,24 @@ impl Data { type_parameters: &mut HashMap>, definitions: &mut Definitions>, ) -> Result { + if data_type.opaque { + // NOTE: No breadcrumbs here which is *okay*, as the caller will backtrack + // and add the necessary type information. + return Err(Error { + context: ErrorContext::IllegalOpaqueType, + breadcrumbs: vec![], + }); + } + 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 reference = Annotated::do_from_type(&field.tipo, modules, type_parameters, definitions)?; - // 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); - } - fields.push(Annotated { title: field.label.clone(), description: field.doc.clone().map(|s| s.trim().to_string()), @@ -479,7 +461,7 @@ fn find_data_type(name: &str, definitions: &[TypedDefinition]) -> Option { - return Some(data_type.clone()) + return Some(data_type.clone()); } Definition::Fn { .. } | Definition::Validator { .. } @@ -928,7 +910,9 @@ pub struct Error { #[derive(Debug, PartialEq, Clone, thiserror::Error)] pub enum ErrorContext { - #[error("I failed at my own job and couldn't figure out how to generate a specification for a type.")] + #[error( + "I failed at my own job and couldn't figure out how to generate a specification for a type." + )] UnsupportedType, #[error("I discovered a type hole where I would expect a concrete type.")] @@ -942,6 +926,9 @@ pub enum ErrorContext { #[error("I figured you tried to export a function in your contract's binary interface.")] UnexpectedFunction, + + #[error("I caught an opaque type trying to escape")] + IllegalOpaqueType, } impl Error { @@ -963,6 +950,26 @@ impl Error { pub fn help(&self) -> String { match self.context { + ErrorContext::IllegalOpaqueType => format!( + r#"Opaque types cannot figure anywhere in an outward-facing type like a validator's redeemer or datum. This is because an {opaque} type hides its implementation details, and likely enforce invariants that cannot be expressed only structurally. In particular, the {opaque} type {signature} cannot be safely constructed from any Plutus Data. + +Hence, {opaque} types are forbidden from interface points with the off-chain world. Instead, use an intermediate representation and construct the {opaque} type at runtime using constructors and methods provided for that type (e.g. {Dict}.{from_list}, {Rational}.{new}, ...)."#, + opaque = "opaque".if_supports_color(Stdout, |s| s.purple()), + signature = Error::fmt_breadcrumbs(&[self + .breadcrumbs + .last() + .expect("always at least one breadcrumb") + .to_owned()]), + Dict = "Dict" + .if_supports_color(Stdout, |s| s.bright_blue()) + .if_supports_color(Stdout, |s| s.bold()), + from_list = "from_list".if_supports_color(Stdout, |s| s.blue()), + Rational = "Rational" + .if_supports_color(Stdout, |s| s.bright_blue()) + .if_supports_color(Stdout, |s| s.bold()), + new = "new".if_supports_color(Stdout, |s| s.blue()), + ), + ErrorContext::UnsupportedType => format!( r#"I do not know how to generate a portable Plutus specification for the following type: diff --git a/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__opaque_singleton_multi_variants.snap b/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__opaque_singleton_multi_variants.snap new file mode 100644 index 00000000..e397e816 --- /dev/null +++ b/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__opaque_singleton_multi_variants.snap @@ -0,0 +1,22 @@ +--- +source: crates/aiken-project/src/blueprint/validator.rs +description: "Code:\n\npub opaque type Rational {\n numerator: Int,\n denominator: Int,\n}\n\nvalidator {\n fn opaque_singleton_multi_variants(redeemer: Rational, ctx: Void) {\n True\n }\n}\n" +--- +Schema { + error: Error { + context: IllegalOpaqueType, + breadcrumbs: [ + App { + public: true, + module: "test_module", + name: "Rational", + args: [], + }, + ], + }, + location: 117..135, + source_code: NamedSource { + name: "", + source: "", + , +} diff --git a/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__opaque_singleton_variants.snap b/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__opaque_singleton_variants.snap index 2485eb95..18a09e86 100644 --- a/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__opaque_singleton_variants.snap +++ b/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__opaque_singleton_variants.snap @@ -2,32 +2,46 @@ source: crates/aiken-project/src/blueprint/validator.rs description: "Code:\n\npub opaque type Dict {\n inner: List<(ByteArray, value)>\n}\n\ntype UUID { UUID }\n\nvalidator {\n fn opaque_singleton_variants(redeemer: Dict, ctx: Void) {\n True\n }\n}\n" --- -{ - "title": "test_module.opaque_singleton_variants", - "redeemer": { - "title": "redeemer", - "schema": { - "$ref": "#/definitions/test_module~1Dict$test_module~1UUID_Int" - } - }, - "compiledCode": "58f201000032323232323232323223232253330064a22930a99803a4811856616c696461746f722072657475726e65642066616c73650013656323300100100222533300a00114984c8cc00c00cc034008c8c8c94cccccc04000454cc0280205854cc0280205854cc028020584dd68008a998050040b18058011929999998078008a998048038b0a998048038b0a998048038b0a998048038b09bae0013009001300b001533333300a001153300400216137560022a660080042c2a660080042c2a660080042c9211972656465656d65723a20446963743c555549442c20496e743e005734ae7155ceaab9e5573eae855d12ba41", - "hash": "c3f68ad7fb4d6c26e1f19799fe0ded6c9904bf04b924835ddad2abf0", - "definitions": { - "ByteArray": { - "dataType": "bytes" +Schema { + error: Error { + context: IllegalOpaqueType, + breadcrumbs: [ + App { + public: true, + module: "test_module", + name: "Dict", + args: [ + Var { + tipo: RefCell { + value: Link { + tipo: App { + public: false, + module: "test_module", + name: "UUID", + args: [], + }, + }, + }, + }, + Var { + tipo: RefCell { + value: Link { + tipo: App { + public: true, + module: "", + name: "Int", + args: [], + }, + }, + }, + }, + ], + }, + ], }, - "Int": { - "dataType": "integer" - }, - "test_module/Dict$test_module/UUID_Int": { - "title": "Dict", - "dataType": "map", - "keys": { - "$ref": "#/definitions/ByteArray" - }, - "values": { - "$ref": "#/definitions/Int" - } - } - } + location: 137..162, + source_code: NamedSource { + name: "", + source: "", + , } diff --git a/crates/aiken-project/src/blueprint/validator.rs b/crates/aiken-project/src/blueprint/validator.rs index cd891418..990b7de9 100644 --- a/crates/aiken-project/src/blueprint/validator.rs +++ b/crates/aiken-project/src/blueprint/validator.rs @@ -12,8 +12,7 @@ use aiken_lang::{ }; use miette::NamedSource; use serde; -use std::borrow::Borrow; -use std::rc::Rc; +use std::{borrow::Borrow, rc::Rc}; use uplc::{ ast::{Constant, DeBruijn, Program, Term}, PlutusData, @@ -250,17 +249,6 @@ impl Validator { #[cfg(test)] mod tests { - use std::collections::HashMap; - - use aiken_lang::{ - self, - ast::{TraceLevel, Tracing}, - builtins, - }; - use uplc::ast as uplc_ast; - - use crate::tests::TestProject; - use super::{ super::{ definitions::{Definitions, Reference}, @@ -269,6 +257,14 @@ mod tests { }, *, }; + use crate::tests::TestProject; + use aiken_lang::{ + self, + ast::{TraceLevel, Tracing}, + builtins, + }; + use std::collections::HashMap; + use uplc::ast as uplc_ast; macro_rules! assert_validator { ($code:expr) => { @@ -296,15 +292,23 @@ mod tests { let validator = validators .get(0) .unwrap() - .as_ref() - .expect("Failed to create validator blueprint"); + .as_ref(); - insta::with_settings!({ - description => concat!("Code:\n\n", indoc::indoc! { $code }), - omit_expression => true - }, { - insta::assert_json_snapshot!(validator); - }); + match validator { + Err(e) => insta::with_settings!({ + description => concat!("Code:\n\n", indoc::indoc! { $code }), + omit_expression => true + }, { + insta::assert_debug_snapshot!(e); + }), + + Ok(validator) => insta::with_settings!({ + description => concat!("Code:\n\n", indoc::indoc! { $code }), + omit_expression => true + }, { + insta::assert_json_snapshot!(validator); + }), + }; }; } @@ -514,6 +518,24 @@ mod tests { ); } + #[test] + fn opaque_singleton_multi_variants() { + assert_validator!( + r#" + pub opaque type Rational { + numerator: Int, + denominator: Int, + } + + validator { + fn opaque_singleton_multi_variants(redeemer: Rational, ctx: Void) { + True + } + } + "# + ); + } + #[test] fn nested_data() { assert_validator!(