Merge pull request #477 from aiken-lang/blueprint-json-deserializers

Blueprint schema validation
This commit is contained in:
Matthias Benkort 2023-04-08 10:26:11 +02:00 committed by GitHub
commit ee8509956d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 2148 additions and 454 deletions

620
Cargo.lock generated vendored

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,10 @@
use std::{cell::RefCell, collections::HashMap, ops::Deref, sync::Arc}; use self::{environment::Environment, pretty::Printer};
use uplc::{ast::Type as UplcType, builtins::DefaultFunction};
use crate::{ use crate::{
ast::{Constant, DefinitionLocation, ModuleKind, Span}, ast::{Constant, DefinitionLocation, ModuleKind, Span},
tipo::fields::FieldMap, tipo::fields::FieldMap,
}; };
use std::{cell::RefCell, collections::HashMap, ops::Deref, sync::Arc};
use self::{environment::Environment, pretty::Printer}; use uplc::{ast::Type as UplcType, builtins::DefaultFunction};
mod environment; mod environment;
pub mod error; pub mod error;

View File

@ -20,6 +20,7 @@ ignore = "0.4.20"
indexmap = "1.9.2" indexmap = "1.9.2"
itertools = "0.10.5" itertools = "0.10.5"
miette = { version = "5.5.0", features = ["fancy"] } miette = { version = "5.5.0", features = ["fancy"] }
minicbor = "0.19.1"
owo-colors = { version = "3.5.0", features = ["supports-colors"] } owo-colors = { version = "3.5.0", features = ["supports-colors"] }
pallas = "0.18.0" pallas = "0.18.0"
pallas-traverse = "0.18.0" pallas-traverse = "0.18.0"
@ -37,3 +38,6 @@ toml = "0.7.2"
uplc = { path = '../uplc', version = "0.0.29" } uplc = { path = '../uplc', version = "0.0.29" }
walkdir = "2.3.2" walkdir = "2.3.2"
zip = "0.6.4" zip = "0.6.4"
[dev-dependencies]
proptest = "1.1.0"

View File

@ -1,6 +1,7 @@
use aiken_lang::tipo::{Type, TypeVar}; use aiken_lang::tipo::{Type, TypeVar};
use serde::{ use serde::{
self, self,
de::{self, Deserialize, Deserializer, MapAccess, Visitor},
ser::{Serialize, SerializeStruct, Serializer}, ser::{Serialize, SerializeStruct, Serializer},
}; };
use std::{ use std::{
@ -52,6 +53,12 @@ impl<T> Definitions<T> {
self.inner.remove(reference.as_key()); self.inner.remove(reference.as_key());
} }
/// Insert a new definition
pub fn insert(&mut self, reference: &Reference, schema: T) {
self.inner
.insert(reference.as_key().to_string(), Some(schema));
}
/// Register a new definition only if it doesn't exist. This uses a strategy of /// 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. /// mark-and-insert such that recursive definitions are only built once.
pub fn register<F, E>( pub fn register<F, E>(
@ -94,14 +101,14 @@ impl Reference {
} }
/// Turn a reference into a key suitable for lookup. /// Turn a reference into a key suitable for lookup.
fn as_key(&self) -> &str { pub(crate) fn as_key(&self) -> &str {
self.inner.as_str() self.inner.as_str()
} }
/// Turn a reference into a valid JSON pointer. Note that the JSON pointer specification /// 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 /// indicates that '/' must be escaped as '~1' in pointer addresses (as they are otherwise
/// treated as path delimiter in pointers paths). /// treated as path delimiter in pointers paths).
fn as_json_pointer(&self) -> String { pub(crate) fn as_json_pointer(&self) -> String {
format!("#/definitions/{}", self.as_key().replace('/', "~1")) format!("#/definitions/{}", self.as_key().replace('/', "~1"))
} }
} }
@ -181,3 +188,55 @@ impl Serialize for Reference {
s.end() s.end()
} }
} }
impl<'a> Deserialize<'a> for Reference {
fn deserialize<D: Deserializer<'a>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(serde::Deserialize)]
enum Field {
#[serde(rename = "$ref")]
Ref,
}
const FIELDS: &[&str] = &["$ref"];
struct ReferenceVisitor;
impl<'a> Visitor<'a> for ReferenceVisitor {
type Value = Reference;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("Reference")
}
fn visit_map<V>(self, mut map: V) -> Result<Reference, V::Error>
where
V: MapAccess<'a>,
{
let mut inner = None;
while let Some(key) = map.next_key()? {
match key {
Field::Ref => {
if inner.is_some() {
return Err(de::Error::duplicate_field(FIELDS[0]));
}
inner = Some(map.next_value()?);
}
}
}
let inner: String = inner.ok_or_else(|| de::Error::missing_field(FIELDS[0]))?;
match inner.strip_prefix("#/definitions/") {
Some(suffix) => Ok(Reference {
inner: suffix.to_string(),
}),
None => Err(de::Error::custom(
"Invalid reference; only local JSON pointer to #/definitions are allowed.",
)),
}
}
}
deserializer.deserialize_struct("Reference", FIELDS, ReferenceVisitor)
}
}

View File

@ -1,8 +1,13 @@
use super::schema; use super::{
definitions::Reference,
schema::{self, Schema},
};
use aiken_lang::ast::Span; use aiken_lang::ast::Span;
use miette::{Diagnostic, NamedSource}; use miette::{Diagnostic, NamedSource};
use minicbor as cbor;
use owo_colors::{OwoColorize, Stream::Stdout}; use owo_colors::{OwoColorize, Stream::Stdout};
use std::fmt::Debug; use std::fmt::Debug;
use uplc::ast::Constant;
#[derive(Debug, thiserror::Error, Diagnostic)] #[derive(Debug, thiserror::Error, Diagnostic)]
pub enum Error { pub enum Error {
@ -37,9 +42,61 @@ pub enum Error {
)] )]
#[diagnostic(code("aiken::blueprint::address::parameterized"))] #[diagnostic(code("aiken::blueprint::address::parameterized"))]
#[diagnostic(help( #[diagnostic(help(
"I can only compute addresses of validators that are fully applied. For example, a {keyword_spend} validator must have exactly 3 arguments: a datum, a redeemer and a context. If it has more, they need to be provided beforehand and applied directly in the validator. Applying parameters change the validator's compiled code, and thus the address.\n\nThis is why I need you to apply parameters first.", "I can only compute addresses of validators that are fully applied. For example, a {keyword_spend} validator must have exactly {spend_arity} arguments: a datum, a redeemer and a context. If it has more, they need to be provided beforehand and applied directly to the validator.\n\nApplying parameters change the validator's compiled code, and thus the address. This is why I need you to apply parameters first using the {blueprint_apply_command} command.",
keyword_spend = "spend".if_supports_color(Stdout, |s| s.purple())))] keyword_spend = "spend".if_supports_color(Stdout, |s| s.yellow()),
spend_arity = "3".if_supports_color(Stdout, |s| s.yellow()),
blueprint_apply_command = "blueprint apply".if_supports_color(Stdout, |s| s.purple()),
))]
ParameterizedValidator { n: usize }, ParameterizedValidator { n: usize },
#[error("I stumble upon something else than a constant when I expected one.")]
#[diagnostic(code("aiken:blueprint::apply::malformed::argument"))]
#[diagnostic(help(
"Parameters applied to blueprints must be constant; they cannot be lambdas or delayed terms."
))]
NonConstantParameter,
#[error("I couldn't find a definition corresponding to a reference.")]
#[diagnostic(code("aiken::blueprint::apply::unknown::reference"))]
#[diagnostic(help(
"While resolving a schema definition, I stumbled upon an unknown reference:\n\n→ {reference}\n\nThis is unfortunate, but signals that either the reference is invalid or that the corresponding schema definition is missing. Double-check the blueprint for that reference or definition.",
reference = reference.as_json_pointer().if_supports_color(Stdout, |s| s.red())
))]
UnresolvedSchemaReference { reference: Reference },
#[error("I caught a parameter application that seems off.")]
#[diagnostic(code("aiken::blueprint::apply::mismatch"))]
#[diagnostic(help(
"When applying parameters to a validator, I control that the shape of the parameter you give me matches what is specified in the blueprint. Unfortunately, it didn't match in this case.\n\nI am looking at the following value:\n\n{term}\n\nbut failed to match it against the specified schema:\n\n{expected}\n\n\nNOTE: this may only represent part of a bigger whole as I am validating the parameter incrementally.",
expected = serde_json::to_string_pretty(&schema).unwrap().if_supports_color(Stdout, |s| s.green()),
term = {
let mut buf = vec![];
match term {
Constant::Data(data) => {
cbor::encode(data, &mut buf).unwrap();
cbor::display(&buf).to_string()
},
_ => term.to_pretty()
}
}.if_supports_color(Stdout, |s| s.red()),
))]
SchemaMismatch { schema: Schema, term: Constant },
#[error(
"I discovered a discrepancy of elements between a given tuple and its declared schema."
)]
#[diagnostic(code("aiken::blueprint::apply::tuple::mismatch"))]
#[diagnostic(help(
"When validating a list-like schema with multiple 'items' schemas, I try to match each element of the instance with each item schema (by their position). Hence, I expect to be as many items in the declared schema ({expected}) than there are items in the instance ({found}).",
expected = expected.if_supports_color(Stdout, |s| s.green()),
found = found.if_supports_color(Stdout, |s| s.red()),
))]
TupleItemsMismatch { expected: usize, found: usize },
#[error("I failed to convert some input into a valid parameter")]
#[diagnostic(code("aiken::blueprint::parse::parameter"))]
#[diagnostic(help("{hint}"))]
MalformedParameter { hint: String },
} }
unsafe impl Send for Error {} unsafe impl Send for Error {}

View File

@ -1,22 +1,23 @@
pub mod definitions; pub mod definitions;
pub mod error; pub mod error;
pub mod parameter;
pub mod schema; pub mod schema;
pub mod validator; pub mod validator;
use crate::{config::Config, module::CheckedModules}; use crate::{config::Config, module::CheckedModules};
use aiken_lang::gen_uplc::CodeGenerator; use aiken_lang::gen_uplc::CodeGenerator;
use definitions::{Definitions, Reference}; use definitions::Definitions;
use error::Error; use error::Error;
use schema::{Annotated, Schema}; use schema::{Annotated, Schema};
use std::fmt::Debug; use std::fmt::Debug;
use validator::Validator; use validator::Validator;
#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
pub struct Blueprint<R: Default, S: Default> { pub struct Blueprint {
pub preamble: Preamble, pub preamble: Preamble,
pub validators: Vec<Validator<R, S>>, pub validators: Vec<Validator>,
#[serde(skip_serializing_if = "Definitions::is_empty", default)] #[serde(skip_serializing_if = "Definitions::is_empty", default)]
pub definitions: Definitions<S>, pub definitions: Definitions<Annotated<Schema>>,
} }
#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
@ -48,7 +49,7 @@ pub enum LookupResult<'a, T> {
Many, Many,
} }
impl Blueprint<Reference, Annotated<Schema>> { impl Blueprint {
pub fn new( pub fn new(
config: &Config, config: &Config,
modules: &CheckedModules, modules: &CheckedModules,
@ -82,12 +83,8 @@ impl Blueprint<Reference, Annotated<Schema>> {
} }
} }
impl<R, S> Blueprint<R, S> impl Blueprint {
where pub fn lookup(&self, title: Option<&String>) -> Option<LookupResult<Validator>> {
R: Clone + Default,
S: Clone + Default,
{
pub fn lookup(&self, title: Option<&String>) -> Option<LookupResult<Validator<R, S>>> {
let mut validator = None; let mut validator = None;
for v in self.validators.iter() { for v in self.validators.iter() {
@ -112,7 +109,7 @@ where
action: F, action: F,
) -> Result<A, E> ) -> Result<A, E>
where where
F: Fn(Validator<R, S>) -> Result<A, E>, F: Fn(Validator) -> Result<A, E>,
{ {
match self.lookup(title) { match self.lookup(title) {
Some(LookupResult::One(validator)) => action(validator.to_owned()), Some(LookupResult::One(validator)) => action(validator.to_owned()),
@ -146,13 +143,13 @@ impl From<&Config> for Preamble {
mod test { mod test {
use super::*; use super::*;
use aiken_lang::builtins; use aiken_lang::builtins;
use schema::{Data, Items, Schema}; use schema::{Data, Declaration, Items, Schema};
use serde_json::{self, json}; use serde_json::{self, json};
use std::collections::HashMap; use std::collections::HashMap;
#[test] #[test]
fn serialize_no_description() { fn serialize_no_description() {
let blueprint: Blueprint<Reference, Annotated<Schema>> = Blueprint { let blueprint = Blueprint {
preamble: Preamble { preamble: Preamble {
title: "Foo".to_string(), title: "Foo".to_string(),
description: None, description: None,
@ -179,7 +176,7 @@ mod test {
#[test] #[test]
fn serialize_with_description() { fn serialize_with_description() {
let blueprint: Blueprint<Reference, Annotated<Schema>> = Blueprint { let blueprint = Blueprint {
preamble: Preamble { preamble: Preamble {
title: "Foo".to_string(), title: "Foo".to_string(),
description: Some("Lorem ipsum".to_string()), description: Some("Lorem ipsum".to_string()),
@ -222,12 +219,15 @@ mod test {
&HashMap::new(), &HashMap::new(),
|_| Ok(Schema::Data(Data::Bytes).into()), |_| Ok(Schema::Data(Data::Bytes).into()),
)?; )?;
Ok(Schema::Data(Data::List(Items::One(Box::new(ref_bytes)))).into()) Ok(
Schema::Data(Data::List(Items::One(Declaration::Referenced(ref_bytes))))
.into(),
)
}, },
) )
.unwrap(); .unwrap();
let blueprint: Blueprint<Reference, Annotated<Schema>> = Blueprint { let blueprint = Blueprint {
preamble: Preamble { preamble: Preamble {
title: "Foo".to_string(), title: "Foo".to_string(),
description: None, description: None,

View File

@ -0,0 +1,428 @@
use super::{
definitions::{Definitions, Reference},
error::Error,
schema::{Annotated, Constructor, Data, Declaration, Items, Schema},
};
use std::{iter, ops::Deref};
use uplc::{
ast::{Constant, Data as UplcData, DeBruijn, Term},
PlutusData,
};
#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)]
pub struct Parameter {
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
pub schema: Reference,
}
impl From<Reference> for Parameter {
fn from(schema: Reference) -> Parameter {
Parameter {
title: None,
schema,
}
}
}
impl Parameter {
pub fn validate(
&self,
definitions: &Definitions<Annotated<Schema>>,
term: &Term<DeBruijn>,
) -> Result<(), Error> {
let schema = &definitions
.lookup(&self.schema)
.map(Ok)
.unwrap_or_else(|| {
Err(Error::UnresolvedSchemaReference {
reference: self.schema.clone(),
})
})?
.annotated;
if let Term::Constant(constant) = term {
validate_schema(schema, definitions, constant)
} else {
Err(Error::NonConstantParameter)
}
}
}
fn mismatch(term: &Constant, schema: Schema) -> Error {
Error::SchemaMismatch {
schema,
term: term.clone(),
}
}
fn validate_schema(
schema: &Schema,
definitions: &Definitions<Annotated<Schema>>,
term: &Constant,
) -> Result<(), Error> {
match schema {
Schema::Data(data) => validate_data(data, definitions, term),
Schema::Unit => expect_unit(term),
Schema::Integer => expect_integer(term),
Schema::Bytes => expect_bytes(term),
Schema::String => expect_string(term),
Schema::Boolean => expect_boolean(term),
Schema::Pair(left, right) => {
let (term_left, term_right) = expect_pair(term)?;
let left =
left.schema(definitions)
.ok_or_else(|| Error::UnresolvedSchemaReference {
reference: left.reference().unwrap().clone(),
})?;
validate_schema(left, definitions, &term_left)?;
let right =
right
.schema(definitions)
.ok_or_else(|| Error::UnresolvedSchemaReference {
reference: right.reference().unwrap().clone(),
})?;
validate_schema(right, definitions, &term_right)?;
Ok(())
}
Schema::List(Items::One(item)) => {
let terms = expect_list(term)?;
let item =
item.schema(definitions)
.ok_or_else(|| Error::UnresolvedSchemaReference {
reference: item.reference().unwrap().clone(),
})?;
for ref term in terms {
validate_schema(item, definitions, term)?;
}
Ok(())
}
Schema::List(Items::Many(items)) => {
let terms = expect_list(term)?;
let items = items
.iter()
.map(|item| {
item.schema(definitions)
.ok_or_else(|| Error::UnresolvedSchemaReference {
reference: item.reference().unwrap().clone(),
})
})
.collect::<Result<Vec<_>, _>>()?;
if terms.len() != items.len() {
return Err(Error::TupleItemsMismatch {
expected: items.len(),
found: terms.len(),
});
}
for (item, ref term) in iter::zip(items, terms) {
validate_schema(item, definitions, term)?;
}
Ok(())
}
}
}
fn validate_data(
data: &Data,
definitions: &Definitions<Annotated<Schema>>,
term: &Constant,
) -> Result<(), Error> {
match data {
Data::Opaque => expect_data(term),
Data::Integer => expect_data_integer(term),
Data::Bytes => expect_data_bytes(term),
Data::List(Items::One(item)) => {
let terms = expect_data_list(term)?;
let item =
item.schema(definitions)
.ok_or_else(|| Error::UnresolvedSchemaReference {
reference: item.reference().unwrap().clone(),
})?;
for ref term in terms {
validate_data(item, definitions, term)?;
}
Ok(())
}
Data::List(Items::Many(items)) => {
let terms = expect_data_list(term)?;
let items = items
.iter()
.map(|item| {
item.schema(definitions)
.ok_or_else(|| Error::UnresolvedSchemaReference {
reference: item.reference().unwrap().clone(),
})
})
.collect::<Result<Vec<_>, _>>()?;
if terms.len() != items.len() {
return Err(Error::TupleItemsMismatch {
expected: items.len(),
found: terms.len(),
});
}
for (item, ref term) in iter::zip(items, terms) {
validate_data(item, definitions, term)?;
}
Ok(())
}
Data::Map(keys, values) => {
let terms = expect_data_map(term)?;
let keys =
keys.schema(definitions)
.ok_or_else(|| Error::UnresolvedSchemaReference {
reference: keys.reference().unwrap().clone(),
})?;
let values =
values
.schema(definitions)
.ok_or_else(|| Error::UnresolvedSchemaReference {
reference: values.reference().unwrap().clone(),
})?;
for (ref k, ref v) in terms {
validate_data(keys, definitions, k)?;
validate_data(values, definitions, v)?;
}
Ok(())
}
Data::AnyOf(constructors) => {
let constructors: Vec<(usize, Vec<&Data>)> = constructors
.iter()
.map(|constructor| {
constructor
.annotated
.fields
.iter()
.map(|field| {
field.annotated.schema(definitions).ok_or_else(|| {
Error::UnresolvedSchemaReference {
reference: field.annotated.reference().unwrap().clone(),
}
})
})
.collect::<Result<_, _>>()
.map(|fields| (constructor.annotated.index, fields))
})
.collect::<Result<_, _>>()?;
for (index, fields_schema) in constructors.iter() {
if let Ok(fields) = expect_data_constr(term, *index) {
if fields_schema.len() != fields.len() {
panic!("fields length different");
}
for (instance, schema) in iter::zip(fields, fields_schema) {
validate_data(schema, definitions, &instance)?;
}
return Ok(());
}
}
Err(mismatch(
term,
Schema::Data(Data::AnyOf(
constructors
.iter()
.map(|(index, fields)| {
Constructor {
index: *index,
fields: fields
.iter()
.map(|_| Declaration::Inline(Box::new(Data::Opaque)).into())
.collect(),
}
.into()
})
.collect(),
)),
))
}
}
}
fn expect_data(term: &Constant) -> Result<(), Error> {
if matches!(term, Constant::Data(..)) {
return Ok(());
}
Err(mismatch(term, Schema::Data(Data::Opaque)))
}
fn expect_data_integer(term: &Constant) -> Result<(), Error> {
if let Constant::Data(data) = term {
if matches!(data, PlutusData::BigInt(..)) {
return Ok(());
}
}
Err(mismatch(term, Schema::Data(Data::Integer)))
}
fn expect_data_bytes(term: &Constant) -> Result<(), Error> {
if let Constant::Data(data) = term {
if matches!(data, PlutusData::BoundedBytes(..)) {
return Ok(());
}
}
Err(mismatch(term, Schema::Data(Data::Bytes)))
}
fn expect_data_list(term: &Constant) -> Result<Vec<Constant>, Error> {
if let Constant::Data(PlutusData::Array(elems)) = term {
return Ok(elems
.iter()
.map(|elem| Constant::Data(elem.to_owned()))
.collect());
}
Err(mismatch(
term,
Schema::Data(Data::List(Items::One(Declaration::Inline(Box::new(
Data::Opaque,
))))),
))
}
fn expect_data_map(term: &Constant) -> Result<Vec<(Constant, Constant)>, Error> {
if let Constant::Data(PlutusData::Map(pairs)) = term {
return Ok(pairs
.iter()
.map(|(k, v)| (Constant::Data(k.to_owned()), Constant::Data(v.to_owned())))
.collect());
}
Err(mismatch(
term,
Schema::Data(Data::Map(
Declaration::Inline(Box::new(Data::Opaque)),
Declaration::Inline(Box::new(Data::Opaque)),
)),
))
}
fn expect_data_constr(term: &Constant, index: usize) -> Result<Vec<Constant>, Error> {
if let Constant::Data(PlutusData::Constr(constr)) = term {
if let PlutusData::Constr(expected) = UplcData::constr(index as u64, vec![]) {
if expected.tag == constr.tag && expected.any_constructor == constr.any_constructor {
return Ok(constr
.fields
.iter()
.map(|field| Constant::Data(field.to_owned()))
.collect());
}
}
}
Err(mismatch(
term,
Schema::Data(Data::AnyOf(vec![Constructor {
index,
fields: vec![],
}
.into()])),
))
}
fn expect_unit(term: &Constant) -> Result<(), Error> {
if matches!(term, Constant::Unit) {
return Ok(());
}
Err(mismatch(term, Schema::Unit))
}
fn expect_integer(term: &Constant) -> Result<(), Error> {
if matches!(term, Constant::Integer(..)) {
return Ok(());
}
Err(mismatch(term, Schema::Integer))
}
fn expect_bytes(term: &Constant) -> Result<(), Error> {
if matches!(term, Constant::ByteString(..)) {
return Ok(());
}
Err(mismatch(term, Schema::Bytes))
}
fn expect_string(term: &Constant) -> Result<(), Error> {
if matches!(term, Constant::String(..)) {
return Ok(());
}
Err(mismatch(term, Schema::String))
}
fn expect_boolean(term: &Constant) -> Result<(), Error> {
if matches!(term, Constant::Bool(..)) {
return Ok(());
}
Err(mismatch(term, Schema::Boolean))
}
fn expect_pair(term: &Constant) -> Result<(Constant, Constant), Error> {
if let Constant::ProtoPair(_, _, left, right) = term {
return Ok((left.deref().clone(), right.deref().clone()));
}
Err(mismatch(
term,
Schema::Pair(
Declaration::Inline(Box::new(Schema::Data(Data::Opaque))),
Declaration::Inline(Box::new(Schema::Data(Data::Opaque))),
),
))
}
fn expect_list(term: &Constant) -> Result<Vec<Constant>, Error> {
if let Constant::ProtoList(_, elems) = term {
return Ok(elems.to_owned());
}
Err(mismatch(
term,
Schema::List(Items::One(Declaration::Inline(Box::new(Schema::Data(
Data::Opaque,
))))),
))
}

View File

@ -8,10 +8,11 @@ use aiken_lang::{
use owo_colors::{OwoColorize, Stream::Stdout}; use owo_colors::{OwoColorize, Stream::Stdout};
use serde::{ use serde::{
self, self,
de::{self, Deserialize, Deserializer, MapAccess, Visitor},
ser::{Serialize, SerializeStruct, Serializer}, ser::{Serialize, SerializeStruct, Serializer},
}; };
use std::ops::Deref; use serde_json as json;
use std::{collections::HashMap, sync::Arc}; use std::{collections::HashMap, fmt, ops::Deref, sync::Arc};
// NOTE: Can be anything BUT 0 // NOTE: Can be anything BUT 0
pub const REDEEMER_DISCRIMINANT: usize = 1; pub const REDEEMER_DISCRIMINANT: usize = 1;
@ -26,6 +27,50 @@ pub struct Annotated<T> {
pub annotated: T, pub annotated: T,
} }
#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum Declaration<T> {
Referenced(Reference),
Inline(Box<T>),
}
impl<'a, T> Declaration<T> {
pub fn reference(&'a self) -> Option<&'a Reference> {
match self {
Declaration::Referenced(reference) => Some(reference),
Declaration::Inline(..) => None,
}
}
fn try_schema(
&'a self,
definitions: &'a Definitions<Annotated<Schema>>,
cast: fn(&'a Schema) -> Option<&'a T>,
) -> Option<&'a T> {
match self {
Declaration::Inline(inner) => Some(inner.deref()),
Declaration::Referenced(reference) => definitions
.lookup(reference)
.and_then(|s| cast(&s.annotated)),
}
}
}
impl<'a> Declaration<Data> {
pub fn schema(&'a self, definitions: &'a Definitions<Annotated<Schema>>) -> Option<&'a Data> {
self.try_schema(definitions, |s| match s {
Schema::Data(data) => Some(data),
_ => None,
})
}
}
impl<'a> Declaration<Schema> {
pub fn schema(&'a self, definitions: &'a Definitions<Annotated<Schema>>) -> Option<&'a Schema> {
self.try_schema(definitions, Some)
}
}
/// A schema for low-level UPLC primitives. /// A schema for low-level UPLC primitives.
#[derive(Debug, PartialEq, Eq, Clone)] #[derive(Debug, PartialEq, Eq, Clone)]
pub enum Schema { pub enum Schema {
@ -34,8 +79,8 @@ pub enum Schema {
Integer, Integer,
Bytes, Bytes,
String, String,
Pair(Data, Data), Pair(Declaration<Schema>, Declaration<Schema>),
List(Vec<Data>), List(Items<Schema>),
Data(Data), Data(Data),
} }
@ -44,24 +89,25 @@ pub enum Schema {
pub enum Data { pub enum Data {
Integer, Integer,
Bytes, Bytes,
List(Items<Reference>), List(Items<Data>),
Map(Box<Reference>, Box<Reference>), Map(Declaration<Data>, Declaration<Data>),
AnyOf(Vec<Annotated<Constructor>>), AnyOf(Vec<Annotated<Constructor>>),
Opaque, Opaque,
} }
/// A structure that represents either one or many elements. /// A structure that represents either one or many elements.
#[derive(Debug, PartialEq, Eq, Clone)] #[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum Items<T> { pub enum Items<T> {
One(Box<T>), One(Declaration<T>),
Many(Vec<T>), Many(Vec<Declaration<T>>),
} }
/// Captures a single UPLC constructor with its /// Captures a single UPLC constructor with its
#[derive(Debug, PartialEq, Eq, Clone)] #[derive(Debug, PartialEq, Eq, Clone)]
pub struct Constructor { pub struct Constructor {
pub index: usize, pub index: usize,
pub fields: Vec<Annotated<Reference>>, pub fields: Vec<Annotated<Declaration<Data>>>,
} }
impl<T> From<T> for Annotated<T> { impl<T> From<T> for Annotated<T> {
@ -90,7 +136,7 @@ impl Annotated<Schema> {
description: Some("A redeemer wrapped in an extra constructor to make multi-validator detection possible on-chain.".to_string()), description: Some("A redeemer wrapped in an extra constructor to make multi-validator detection possible on-chain.".to_string()),
annotated: Schema::Data(Data::AnyOf(vec![Constructor { annotated: Schema::Data(Data::AnyOf(vec![Constructor {
index: REDEEMER_DISCRIMINANT, index: REDEEMER_DISCRIMINANT,
fields: vec![schema.into()], fields: vec![Declaration::Referenced(schema).into()],
} }
.into()])), .into()])),
}) })
@ -219,7 +265,7 @@ impl Annotated<Schema> {
description: Some("An optional value.".to_string()), description: Some("An optional value.".to_string()),
annotated: Constructor { annotated: Constructor {
index: 0, index: 0,
fields: vec![generic.into()], fields: vec![Declaration::Referenced(generic).into()],
}, },
}, },
Annotated { Annotated {
@ -260,22 +306,15 @@ impl Annotated<Schema> {
} if xs.len() == 2 => { } if xs.len() == 2 => {
definitions.remove(&generic); definitions.remove(&generic);
Data::Map( Data::Map(
Box::new( xs.first()
xs.first() .expect("length (== 2) checked in pattern clause")
.expect("length (== 2) checked in pattern clause") .to_owned(),
.to_owned(), xs.last()
), .expect("length (== 2) checked in pattern clause")
Box::new( .to_owned(),
xs.last()
.expect("length (== 2) checked in pattern clause")
.to_owned(),
),
) )
} }
_ => { _ => Data::List(Items::One(Declaration::Referenced(generic))),
// let inner = schema.clone().into_data(type_info)?.annotated;
Data::List(Items::One(Box::new(generic)))
}
}; };
Ok(Schema::Data(data).into()) Ok(Schema::Data(data).into())
@ -320,6 +359,7 @@ impl Annotated<Schema> {
.iter() .iter()
.map(|elem| { .map(|elem| {
Annotated::do_from_type(elem, modules, type_parameters, definitions) Annotated::do_from_type(elem, modules, type_parameters, definitions)
.map(Declaration::Referenced)
}) })
.collect::<Result<Vec<_>, _>>() .collect::<Result<Vec<_>, _>>()
.map_err(|e| e.backtrack(type_info))?; .map_err(|e| e.backtrack(type_info))?;
@ -398,7 +438,7 @@ impl Data {
fields.push(Annotated { fields.push(Annotated {
title: field.label.clone(), title: field.label.clone(),
description: field.doc.clone().map(|s| s.trim().to_string()), description: field.doc.clone().map(|s| s.trim().to_string()),
annotated: reference, annotated: Declaration::Referenced(reference),
}); });
} }
@ -510,6 +550,245 @@ impl Serialize for Schema {
} }
} }
fn visit_schema<'a, V>(mut map: V) -> Result<Schema, V::Error>
where
V: MapAccess<'a>,
{
#[derive(serde::Deserialize)]
#[serde(field_identifier, rename_all = "camelCase")]
enum Field {
DataType,
Items,
Keys,
Values,
Left,
Right,
AnyOf,
OneOf,
}
let mut data_type: Option<String> = None;
let mut items: Option<json::Value> = None; // defer items deserialization to later
let mut keys = None;
let mut left = None;
let mut right = None;
let mut values = None;
let mut any_of = None;
while let Some(key) = map.next_key()? {
match key {
Field::DataType => {
if data_type.is_some() {
return Err(de::Error::duplicate_field("dataType"));
}
data_type = Some(map.next_value()?);
}
Field::Items => {
if items.is_some() {
return Err(de::Error::duplicate_field("items"));
}
items = Some(map.next_value()?);
}
Field::Keys => {
if keys.is_some() {
return Err(de::Error::duplicate_field("keys"));
}
keys = Some(map.next_value()?);
}
Field::Values => {
if values.is_some() {
return Err(de::Error::duplicate_field("values"));
}
values = Some(map.next_value()?);
}
Field::Left => {
if left.is_some() {
return Err(de::Error::duplicate_field("left"));
}
left = Some(map.next_value()?);
}
Field::Right => {
if right.is_some() {
return Err(de::Error::duplicate_field("right"));
}
right = Some(map.next_value()?);
}
Field::AnyOf => {
if any_of.is_some() {
return Err(de::Error::duplicate_field("anyOf/oneOf"));
}
any_of = Some(map.next_value()?);
}
Field::OneOf => {
if any_of.is_some() {
return Err(de::Error::duplicate_field("anyOf/oneOf"));
}
any_of = Some(map.next_value()?);
}
}
}
let expect_data_items = || match &items {
Some(items) => serde_json::from_value::<Items<Data>>(items.clone())
.map_err(|e| de::Error::custom(e.to_string())),
None => Err(de::Error::missing_field("items")),
};
let expect_schema_items = || match &items {
Some(items) => serde_json::from_value::<Items<Schema>>(items.clone())
.map_err(|e| de::Error::custom(e.to_string())),
None => Err(de::Error::missing_field("items")),
};
let expect_no_items = || {
if items.is_some() {
return Err(de::Error::custom(
"unexpected fields 'items' for non-list data-type",
));
}
Ok(())
};
let expect_no_keys = || {
if keys.is_some() {
return Err(de::Error::custom(
"unexpected fields 'keys' for non-map data-type",
));
}
Ok(())
};
let expect_no_values = || {
if values.is_some() {
return Err(de::Error::custom(
"unexpected fields 'values' for non-map data-type",
));
}
Ok(())
};
let expect_no_any_of = || {
if any_of.is_some() {
return Err(de::Error::custom(
"unexpected fields 'anyOf' or 'oneOf'; applicators must singletons",
));
}
Ok(())
};
let expect_no_left_or_right = || {
if left.is_some() || right.is_some() {
return Err(de::Error::custom(
"unexpected field(s) 'left' and/or 'right' for a non-pair data-type",
));
}
Ok(())
};
match data_type {
None => {
expect_no_items()?;
expect_no_keys()?;
expect_no_values()?;
expect_no_left_or_right()?;
match any_of {
None => Ok(Schema::Data(Data::Opaque)),
Some(constructors) => Ok(Schema::Data(Data::AnyOf(constructors))),
}
}
Some(data_type) if data_type == "list" => {
expect_no_keys()?;
expect_no_values()?;
expect_no_any_of()?;
expect_no_left_or_right()?;
let items = expect_data_items()?;
Ok(Schema::Data(Data::List(items)))
}
Some(data_type) if data_type == "#list" => {
expect_no_keys()?;
expect_no_values()?;
expect_no_any_of()?;
expect_no_left_or_right()?;
let items = expect_schema_items()?;
Ok(Schema::List(items))
}
Some(data_type) if data_type == "map" => {
expect_no_items()?;
expect_no_any_of()?;
expect_no_left_or_right()?;
match (keys, values) {
(Some(keys), Some(values)) => Ok(Schema::Data(Data::Map(keys, values))),
(None, _) => Err(de::Error::missing_field("keys")),
(Some(..), None) => Err(de::Error::missing_field("values")),
}
}
Some(data_type) if data_type == "#pair" => {
expect_no_items()?;
expect_no_keys()?;
expect_no_values()?;
expect_no_any_of()?;
match (left, right) {
(Some(left), Some(right)) => Ok(Schema::Pair(left, right)),
(None, _) => Err(de::Error::missing_field("left")),
(Some(..), None) => Err(de::Error::missing_field("right")),
}
}
Some(data_type) => {
expect_no_items()?;
expect_no_keys()?;
expect_no_values()?;
expect_no_any_of()?;
expect_no_left_or_right()?;
if data_type == "bytes" {
Ok(Schema::Data(Data::Bytes))
} else if data_type == "integer" {
Ok(Schema::Data(Data::Integer))
} else if data_type == "#unit" {
Ok(Schema::Unit)
} else if data_type == "#integer" {
Ok(Schema::Integer)
} else if data_type == "#bytes" {
Ok(Schema::Bytes)
} else if data_type == "#boolean" {
Ok(Schema::Boolean)
} else if data_type == "#string" {
Ok(Schema::String)
} else {
Err(de::Error::custom("unknown data-type"))
}
}
}
}
impl<'a> Deserialize<'a> for Schema {
fn deserialize<D: Deserializer<'a>>(deserializer: D) -> Result<Self, D::Error> {
struct SchemaVisitor;
impl<'a> Visitor<'a> for SchemaVisitor {
type Value = Schema;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("Schema")
}
fn visit_map<V>(self, mut map: V) -> Result<Schema, V::Error>
where
V: MapAccess<'a>,
{
visit_schema(&mut map)
}
}
deserializer.deserialize_struct(
"Schema",
&[
"dataType", "items", "keys", "values", "anyOf", "oneOf", "left", "right",
],
SchemaVisitor,
)
}
}
impl Serialize for Data { impl Serialize for Data {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self { match self {
@ -527,13 +806,7 @@ impl Serialize for Data {
s.serialize_field("dataType", "bytes")?; s.serialize_field("dataType", "bytes")?;
s.end() s.end()
} }
Data::List(Items::One(item)) => { Data::List(items) => {
let mut s = serializer.serialize_struct("List", 2)?;
s.serialize_field("dataType", "list")?;
s.serialize_field("items", &item)?;
s.end()
}
Data::List(Items::Many(items)) => {
let mut s = serializer.serialize_struct("List", 2)?; let mut s = serializer.serialize_struct("List", 2)?;
s.serialize_field("dataType", "list")?; s.serialize_field("dataType", "list")?;
s.serialize_field("items", &items)?; s.serialize_field("items", &items)?;
@ -554,6 +827,38 @@ impl Serialize for Data {
} }
} }
} }
impl<'a> Deserialize<'a> for Data {
fn deserialize<D: Deserializer<'a>>(deserializer: D) -> Result<Self, D::Error> {
struct DataVisitor;
impl<'a> Visitor<'a> for DataVisitor {
type Value = Data;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("Data")
}
fn visit_map<V>(self, mut map: V) -> Result<Data, V::Error>
where
V: MapAccess<'a>,
{
let schema = visit_schema(&mut map)?;
match schema {
Schema::Data(data) => Ok(data),
_ => Err(de::Error::custom("not a valid 'data'")),
}
}
}
deserializer.deserialize_struct(
"Data",
&["dataType", "items", "keys", "values", "anyOf", "oneOf"],
DataVisitor,
)
}
}
impl Serialize for Constructor { impl Serialize for Constructor {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut s = serializer.serialize_struct("Constructor", 3)?; let mut s = serializer.serialize_struct("Constructor", 3)?;
@ -564,6 +869,60 @@ impl Serialize for Constructor {
} }
} }
impl<'a> Deserialize<'a> for Constructor {
fn deserialize<D: Deserializer<'a>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(serde::Deserialize)]
#[serde(field_identifier, rename_all = "camelCase")]
enum Field {
Index,
Fields,
}
const FIELDS: &[&str] = &["index", "fields"];
struct ConstructorVisitor;
impl<'a> Visitor<'a> for ConstructorVisitor {
type Value = Constructor;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("Constructor")
}
fn visit_map<V>(self, mut map: V) -> Result<Constructor, V::Error>
where
V: MapAccess<'a>,
{
let mut index = None;
let mut fields = None;
while let Some(key) = map.next_key()? {
match key {
Field::Index => {
if index.is_some() {
return Err(de::Error::duplicate_field(FIELDS[0]));
}
index = Some(map.next_value()?);
}
Field::Fields => {
if fields.is_some() {
return Err(de::Error::duplicate_field(FIELDS[1]));
}
fields = Some(map.next_value()?);
}
}
}
Ok(Constructor {
index: index.ok_or_else(|| de::Error::missing_field(FIELDS[0]))?,
fields: fields.ok_or_else(|| de::Error::missing_field(FIELDS[1]))?,
})
}
}
deserializer.deserialize_struct("Constructor", FIELDS, ConstructorVisitor)
}
}
#[derive(Debug, PartialEq, Clone, thiserror::Error)] #[derive(Debug, PartialEq, Clone, thiserror::Error)]
#[error("{}", context)] #[error("{}", context)]
pub struct Error { pub struct Error {
@ -681,6 +1040,7 @@ Here's the types I followed and that led me to this problem:
#[cfg(test)] #[cfg(test)]
pub mod test { pub mod test {
use super::*; use super::*;
use proptest::prelude::*;
use serde_json::{self, json, Value}; use serde_json::{self, json, Value};
pub fn assert_json(schema: &impl Serialize, expected: Value) { pub fn assert_json(schema: &impl Serialize, expected: Value) {
@ -712,7 +1072,7 @@ pub mod test {
#[test] #[test]
fn serialize_data_list_1() { fn serialize_data_list_1() {
let ref_integer = Reference::new("Int"); let ref_integer = Reference::new("Int");
let schema = Schema::Data(Data::List(Items::One(Box::new(ref_integer)))); let schema = Schema::Data(Data::List(Items::One(Declaration::Referenced(ref_integer))));
assert_json( assert_json(
&schema, &schema,
json!({ json!({
@ -727,7 +1087,9 @@ pub mod test {
#[test] #[test]
fn serialize_data_list_2() { fn serialize_data_list_2() {
let ref_list_integer = Reference::new("List$Int"); let ref_list_integer = Reference::new("List$Int");
let schema = Schema::Data(Data::List(Items::One(Box::new(ref_list_integer)))); let schema = Schema::Data(Data::List(Items::One(Declaration::Referenced(
ref_list_integer,
))));
assert_json( assert_json(
&schema, &schema,
json!({ json!({
@ -741,9 +1103,9 @@ pub mod test {
#[test] #[test]
fn serialize_data_map_1() { fn serialize_data_map_1() {
let ref_integer = Reference::new("Int"); let ref_integer = Declaration::Referenced(Reference::new("Int"));
let ref_bytes = Reference::new("ByteArray"); let ref_bytes = Declaration::Referenced(Reference::new("ByteArray"));
let schema = Schema::Data(Data::Map(Box::new(ref_integer), Box::new(ref_bytes))); let schema = Schema::Data(Data::Map(ref_integer, ref_bytes));
assert_json( assert_json(
&schema, &schema,
json!({ json!({
@ -782,12 +1144,12 @@ pub mod test {
let schema = Schema::Data(Data::AnyOf(vec![ let schema = Schema::Data(Data::AnyOf(vec![
Constructor { Constructor {
index: 0, index: 0,
fields: vec![Reference::new("Int").into()], fields: vec![Declaration::Referenced(Reference::new("Int")).into()],
} }
.into(), .into(),
Constructor { Constructor {
index: 1, index: 1,
fields: vec![Reference::new("Bytes").into()], fields: vec![Declaration::Referenced(Reference::new("Bytes")).into()],
} }
.into(), .into(),
])); ]));
@ -848,4 +1210,224 @@ pub mod test {
}), }),
) )
} }
#[test]
fn deserialize_data_opaque() {
assert_eq!(Data::Opaque, serde_json::from_value(json!({})).unwrap())
}
#[test]
fn deserialize_data_integer() {
assert_eq!(
Data::Integer,
serde_json::from_value(json!({
"dataType": "integer",
}))
.unwrap()
)
}
#[test]
fn deserialize_data_bytes() {
assert_eq!(
Data::Bytes,
serde_json::from_value(json!({
"dataType": "bytes",
}))
.unwrap()
)
}
#[test]
fn deserialize_data_list_one() {
assert_eq!(
Data::List(Items::One(Declaration::Referenced(Reference::new("foo")))),
serde_json::from_value(json!({
"dataType": "list",
"items": { "$ref": "#/definitions/foo" }
}))
.unwrap()
)
}
#[test]
fn deserialize_data_list_many() {
assert_eq!(
Data::List(Items::Many(vec![
Declaration::Referenced(Reference::new("foo")),
Declaration::Referenced(Reference::new("bar"))
])),
serde_json::from_value(json!({
"dataType": "list",
"items": [
{ "$ref": "#/definitions/foo" },
{ "$ref": "#/definitions/bar" }
],
}))
.unwrap()
)
}
#[test]
fn deserialize_data_map() {
assert_eq!(
Data::Map(
Declaration::Referenced(Reference::new("foo")),
Declaration::Referenced(Reference::new("bar"))
),
serde_json::from_value(json!({
"dataType": "map",
"keys": { "$ref": "#/definitions/foo" },
"values": { "$ref": "#/definitions/bar" }
}))
.unwrap()
)
}
#[test]
fn deserialize_any_of() {
assert_eq!(
Data::AnyOf(vec![Constructor {
index: 0,
fields: vec![
Declaration::Referenced(Reference::new("foo")).into(),
Declaration::Referenced(Reference::new("bar")).into()
],
}
.into()]),
serde_json::from_value(json!({
"anyOf": [{
"index": 0,
"fields": [
{
"$ref": "#/definitions/foo",
},
{
"$ref": "#/definitions/bar",
}
]
}]
}))
.unwrap()
)
}
#[test]
fn deserialize_one_of() {
assert_eq!(
Data::AnyOf(vec![Constructor {
index: 0,
fields: vec![
Declaration::Referenced(Reference::new("foo")).into(),
Declaration::Referenced(Reference::new("bar")).into()
],
}
.into()]),
serde_json::from_value(json!({
"oneOf": [{
"index": 0,
"fields": [
{
"$ref": "#/definitions/foo",
},
{
"$ref": "#/definitions/bar",
}
]
}]
}))
.unwrap()
)
}
fn arbitrary_data() -> impl Strategy<Value = Data> {
let leaf = prop_oneof![Just(Data::Opaque), Just(Data::Bytes), Just(Data::Integer)];
leaf.prop_recursive(3, 8, 3, |inner| {
let r = prop_oneof![
".*".prop_map(|s| Declaration::Referenced(Reference::new(&s))),
inner.prop_map(|s| Declaration::Inline(Box::new(s)))
];
let constructor =
(0..3usize, prop::collection::vec(r.clone(), 0..3)).prop_map(|(index, fields)| {
Constructor {
index,
fields: fields.into_iter().map(|f| f.into()).collect(),
}
.into()
});
prop_oneof![
(r.clone(), r.clone()).prop_map(|(k, v)| Data::Map(k, v)),
r.clone().prop_map(|x| Data::List(Items::One(x))),
prop::collection::vec(r, 1..3).prop_map(|xs| Data::List(Items::Many(xs))),
prop::collection::vec(constructor, 1..3).prop_map(Data::AnyOf)
]
})
}
fn arbitrary_schema() -> impl Strategy<Value = Schema> {
prop_compose! {
fn data_strategy()(data in arbitrary_data()) -> Schema {
Schema::Data(data)
}
}
let leaf = prop_oneof![
Just(Schema::Unit),
Just(Schema::Boolean),
Just(Schema::Bytes),
Just(Schema::Integer),
Just(Schema::String),
data_strategy(),
];
leaf.prop_recursive(3, 8, 3, |inner| {
let r = prop_oneof![
".*".prop_map(|s| Declaration::Referenced(Reference::new(&s))),
inner.prop_map(|s| Declaration::Inline(Box::new(s)))
];
prop_oneof![
(r.clone(), r.clone()).prop_map(|(l, r)| Schema::Pair(l, r)),
r.clone().prop_map(|x| Schema::List(Items::One(x))),
prop::collection::vec(r, 1..3).prop_map(|xs| Schema::List(Items::Many(xs))),
]
})
}
proptest! {
#[test]
fn data_serialization_roundtrip(data in arbitrary_data()) {
let json = serde_json::to_value(data);
let pretty = json
.as_ref()
.map(|v| serde_json::to_string_pretty(v).unwrap())
.unwrap_or_else(|_| "invalid".to_string());
assert!(
matches!(
json.and_then(serde_json::from_value::<Data>),
Ok{..}
),
"\ncounterexample: {pretty}\n",
)
}
}
proptest! {
#[test]
fn schema_serialization_roundtrip(schema in arbitrary_schema()) {
let json = serde_json::to_value(schema);
let pretty = json
.as_ref()
.map(|v| serde_json::to_string_pretty(v).unwrap())
.unwrap_or_else(|_| "invalid".to_string());
assert!(
matches!(
json.and_then(serde_json::from_value::<Schema>),
Ok{..}
),
"\ncounterexample: {pretty}\n",
)
}
}
} }

View File

@ -1,6 +1,7 @@
use super::{ use super::{
definitions::{Definitions, Reference}, definitions::Definitions,
error::Error, error::Error,
parameter::Parameter,
schema::{Annotated, Schema}, schema::{Annotated, Schema},
}; };
use crate::module::{CheckedModule, CheckedModules}; use crate::module::{CheckedModule, CheckedModules};
@ -13,44 +14,36 @@ use serde;
use uplc::ast::{DeBruijn, Program, Term}; use uplc::ast::{DeBruijn, Program, Term};
#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
pub struct Validator<R, S> { pub struct Validator {
pub title: String, pub title: String,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>, pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub datum: Option<Argument<R>>, pub datum: Option<Parameter>,
pub redeemer: Argument<R>, pub redeemer: Parameter,
#[serde(skip_serializing_if = "Vec::is_empty")] #[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)] #[serde(default)]
pub parameters: Vec<Argument<R>>, pub parameters: Vec<Parameter>,
#[serde(flatten)] #[serde(flatten)]
pub program: Program<DeBruijn>, pub program: Program<DeBruijn>,
#[serde(skip_serializing_if = "Definitions::is_empty")] #[serde(skip_serializing_if = "Definitions::is_empty")]
#[serde(default)] #[serde(default)]
pub definitions: Definitions<S>, pub definitions: Definitions<Annotated<Schema>>,
} }
#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)] impl Validator {
pub struct Argument<T> {
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
pub schema: T,
}
impl Validator<Reference, Annotated<Schema>> {
pub fn from_checked_module( pub fn from_checked_module(
modules: &CheckedModules, modules: &CheckedModules,
generator: &mut CodeGenerator, generator: &mut CodeGenerator,
module: &CheckedModule, module: &CheckedModule,
def: &TypedValidator, def: &TypedValidator,
) -> Vec<Result<Validator<Reference, Annotated<Schema>>, Error>> { ) -> Vec<Result<Validator, Error>> {
let program = generator.generate(def).try_into().unwrap(); let program = generator.generate(def).try_into().unwrap();
let is_multi_validator = def.other_fun.is_some(); let is_multi_validator = def.other_fun.is_some();
@ -85,7 +78,7 @@ impl Validator<Reference, Annotated<Schema>> {
params: &[TypedArg], params: &[TypedArg],
func: &TypedFunction, func: &TypedFunction,
is_multi_validator: bool, is_multi_validator: bool,
) -> Result<Validator<Reference, Annotated<Schema>>, Error> { ) -> Result<Validator, Error> {
let mut args = func.arguments.iter().rev(); let mut args = func.arguments.iter().rev();
let (_, redeemer, datum) = (args.next(), args.next().unwrap(), args.next()); let (_, redeemer, datum) = (args.next(), args.next().unwrap(), args.next());
@ -102,7 +95,7 @@ impl Validator<Reference, Annotated<Schema>> {
.iter() .iter()
.map(|param| { .map(|param| {
Annotated::from_type(modules.into(), &param.tipo, &mut definitions) Annotated::from_type(modules.into(), &param.tipo, &mut definitions)
.map(|schema| Argument { .map(|schema| Parameter {
title: Some(param.arg_name.get_label()), title: Some(param.arg_name.get_label()),
schema, schema,
}) })
@ -130,7 +123,7 @@ impl Validator<Reference, Annotated<Schema>> {
) )
}) })
.transpose()? .transpose()?
.map(|schema| Argument { .map(|schema| Parameter {
title: datum.map(|datum| datum.arg_name.get_label()), title: datum.map(|datum| datum.arg_name.get_label()),
schema, schema,
}), }),
@ -143,7 +136,7 @@ impl Validator<Reference, Annotated<Schema>> {
module.code.clone(), module.code.clone(),
), ),
}) })
.map(|schema| Argument { .map(|schema| Parameter {
title: Some(redeemer.arg_name.get_label()), title: Some(redeemer.arg_name.get_label()),
schema: match datum { schema: match datum {
Some(..) if is_multi_validator => Annotated::as_wrapped_redeemer( Some(..) if is_multi_validator => Annotated::as_wrapped_redeemer(
@ -160,16 +153,16 @@ impl Validator<Reference, Annotated<Schema>> {
} }
} }
impl<R, S> Validator<R, S> impl Validator {
where pub fn apply(
S: Clone, self,
R: Clone, definitions: &Definitions<Annotated<Schema>>,
{ arg: &Term<DeBruijn>,
pub fn apply(self, arg: &Term<DeBruijn>) -> Result<Self, Error> { ) -> Result<Self, Error> {
match self.parameters.split_first() { match self.parameters.split_first() {
None => Err(Error::NoParametersToApply), None => Err(Error::NoParametersToApply),
Some((_, tail)) => { Some((head, tail)) => {
// TODO: Ideally, we should control that the applied term matches its schema. head.validate(definitions, arg)?;
Ok(Self { Ok(Self {
program: self.program.apply_term(arg), program: self.program.apply_term(arg),
parameters: tail.to_vec(), parameters: tail.to_vec(),
@ -182,7 +175,14 @@ where
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::{
super::{
definitions::{Definitions, Reference},
error::Error,
schema::{Annotated, Constructor, Data, Declaration, Items, Schema},
},
*,
};
use crate::{module::ParsedModule, PackageName}; use crate::{module::ParsedModule, PackageName};
use aiken_lang::{ use aiken_lang::{
self, self,
@ -197,6 +197,7 @@ mod test {
use indexmap::IndexMap; use indexmap::IndexMap;
use serde_json::{self, json}; use serde_json::{self, json};
use std::{collections::HashMap, path::PathBuf}; use std::{collections::HashMap, path::PathBuf};
use uplc::ast as uplc;
// TODO: Possible refactor this out of the module and have it used by `Project`. The idea would // TODO: Possible refactor this out of the module and have it used by `Project`. The idea would
// be to make this struct below the actual project, and wrap it in another metadata struct // be to make this struct below the actual project, and wrap it in another metadata struct
@ -318,6 +319,69 @@ mod test {
assert_json_eq!(serde_json::to_value(validator).unwrap(), expected); assert_json_eq!(serde_json::to_value(validator).unwrap(), expected);
} }
fn fixture_definitions() -> Definitions<Annotated<Schema>> {
let mut definitions = Definitions::new();
// #/definitions/Int
//
// {
// "dataType": "integer"
// }
definitions
.register::<_, Error>(&builtins::int(), &HashMap::new(), |_| {
Ok(Schema::Data(Data::Integer).into())
})
.unwrap();
// #/definitions/ByteArray
//
// {
// "dataType": "bytes"
// }
definitions
.register::<_, Error>(&builtins::byte_array(), &HashMap::new(), |_| {
Ok(Schema::Data(Data::Bytes).into())
})
.unwrap();
// #/definitions/Bool
//
// {
// "anyOf": [
// {
// "dataType": "constructor",
// "index": 0,
// "fields": []
// },
// {
// "dataType": "constructor",
// "index": 1,
// "fields": []
// },
// ]
// }
definitions.insert(
&Reference::new("Bool"),
Schema::Data(Data::AnyOf(vec![
// False
Constructor {
index: 0,
fields: vec![],
}
.into(),
// True
Constructor {
index: 1,
fields: vec![],
}
.into(),
]))
.into(),
);
definitions
}
#[test] #[test]
fn mint_basic() { fn mint_basic() {
assert_validator( assert_validator(
@ -1096,4 +1160,266 @@ mod test {
}), }),
) )
} }
#[test]
fn validate_arguments_integer() {
let definitions = fixture_definitions();
let term = Term::data(uplc::Data::integer(42.into()));
let param = Parameter {
title: None,
schema: Reference::new("Int"),
};
assert!(matches!(param.validate(&definitions, &term), Ok { .. }))
}
#[test]
fn validate_arguments_bytestring() {
let definitions = fixture_definitions();
let term = Term::data(uplc::Data::bytestring(vec![102, 111, 111]));
let param = Parameter {
title: None,
schema: Reference::new("ByteArray"),
};
assert!(matches!(param.validate(&definitions, &term), Ok { .. }))
}
#[test]
fn validate_arguments_list_inline() {
let schema = Reference::new("List$Int");
// #/definitions/List$Int
//
// {
// "dataType": "list",
// "items": { "dataType": "integer" }
// }
let mut definitions = fixture_definitions();
definitions.insert(
&schema,
Schema::Data(Data::List(Items::One(Declaration::Inline(Box::new(
Data::Integer,
)))))
.into(),
);
let term = Term::data(uplc::Data::list(vec![
uplc::Data::integer(42.into()),
uplc::Data::integer(14.into()),
]));
let param: Parameter = schema.into();
assert!(matches!(param.validate(&definitions, &term), Ok { .. }))
}
#[test]
fn validate_arguments_list_ref() {
let schema = Reference::new("List$ByteArray");
// #/definitions/List$ByteArray
//
// {
// "dataType": "list",
// "items": { "$ref": "#/definitions/ByteArray" }
// }
let mut definitions = fixture_definitions();
definitions.insert(
&schema,
Schema::Data(Data::List(Items::One(Declaration::Referenced(
Reference::new("ByteArray"),
))))
.into(),
);
let term = Term::data(uplc::Data::list(vec![uplc::Data::bytestring(vec![
102, 111, 111,
])]));
let param: Parameter = schema.into();
assert!(matches!(param.validate(&definitions, &term), Ok { .. }))
}
#[test]
fn validate_arguments_tuple() {
let schema = Reference::new("Tuple$Int_ByteArray");
// #/definitions/Tuple$Int_ByteArray
//
// {
// "dataType": "list",
// "items": [
// { "$ref": "#/definitions/Int" }
// { "$ref": "#/definitions/ByteArray" }
// ]
// }
let mut definitions = fixture_definitions();
definitions.insert(
&schema,
Schema::Data(Data::List(Items::Many(vec![
Declaration::Referenced(Reference::new("Int")),
Declaration::Referenced(Reference::new("ByteArray")),
])))
.into(),
);
let term = Term::data(uplc::Data::list(vec![
uplc::Data::integer(42.into()),
uplc::Data::bytestring(vec![102, 111, 111]),
]));
let param: Parameter = schema.into();
assert!(matches!(param.validate(&definitions, &term), Ok { .. }))
}
#[test]
fn validate_arguments_dict() {
let schema = Reference::new("Dict$ByteArray_Int");
// #/definitions/Dict$Int_ByteArray
//
// {
// "dataType": "map",
// "keys": { "dataType": "bytes" },
// "values": { "dataType": "integer" }
// }
let mut definitions = fixture_definitions();
definitions.insert(
&Reference::new("Dict$ByteArray_Int"),
Schema::Data(Data::Map(
Declaration::Inline(Box::new(Data::Bytes)),
Declaration::Inline(Box::new(Data::Integer)),
))
.into(),
);
let term = Term::data(uplc::Data::map(vec![(
uplc::Data::bytestring(vec![102, 111, 111]),
uplc::Data::integer(42.into()),
)]));
let param: Parameter = schema.into();
assert!(matches!(param.validate(&definitions, &term), Ok { .. }))
}
#[test]
fn validate_arguments_constr_nullary() {
let schema = Reference::new("Bool");
let definitions = fixture_definitions();
let term = Term::data(uplc::Data::constr(1, vec![]));
let param: Parameter = schema.into();
assert!(matches!(param.validate(&definitions, &term), Ok { .. }))
}
#[test]
fn validate_arguments_constr_n_ary() {
let schema = Reference::new("Foo");
// #/definitions/Foo
//
// {
// "anyOf": [
// {
// "dataType": "constructor",
// "index": 0,
// "fields": [{
// "$ref": "#/definitions/Bool
// }]
// },
// ]
// }
let mut definitions = fixture_definitions();
definitions.insert(
&schema,
Schema::Data(Data::AnyOf(vec![Constructor {
index: 0,
fields: vec![Declaration::Referenced(Reference::new("Bool")).into()],
}
.into()]))
.into(),
);
let term = Term::data(uplc::Data::constr(0, vec![uplc::Data::constr(0, vec![])]));
let param: Parameter = schema.into();
assert!(matches!(param.validate(&definitions, &term), Ok { .. }))
}
#[test]
fn validate_arguments_constr_recursive() {
let schema = Reference::new("LinkedList$Int");
// #/definitions/LinkedList$Int
//
// {
// "anyOf": [
// {
// "dataType": "constructor",
// "index": 0,
// "fields": []
// },
// {
// "dataType": "constructor",
// "index": 1,
// "fields": [{
// "$ref": "#/definitions/Int
// "$ref": "#/definitions/LinkedList$Int
// }]
// },
// ]
// }
let mut definitions = fixture_definitions();
definitions.insert(
&schema,
Schema::Data(Data::AnyOf(vec![
// Empty
Constructor {
index: 0,
fields: vec![],
}
.into(),
// Node
Constructor {
index: 1,
fields: vec![
Declaration::Referenced(Reference::new("Int")).into(),
Declaration::Referenced(Reference::new("LinkedList$Int")).into(),
],
}
.into(),
]))
.into(),
);
let term = Term::data(uplc::Data::constr(
1,
vec![
uplc::Data::integer(14.into()),
uplc::Data::constr(
1,
vec![
uplc::Data::integer(42.into()),
uplc::Data::constr(0, vec![]),
],
),
],
));
let param: Parameter = schema.into();
assert!(matches!(param.validate(&definitions, &term), Ok { .. }))
}
} }

View File

@ -12,11 +12,7 @@ pub mod pretty;
pub mod script; pub mod script;
pub mod telemetry; pub mod telemetry;
use crate::blueprint::{ use crate::blueprint::Blueprint;
definitions::Reference,
schema::{Annotated, Schema},
Blueprint,
};
use aiken_lang::{ use aiken_lang::{
ast::{Definition, Function, ModuleKind, Tracing, TypedDataType, TypedFunction}, ast::{Definition, Function, ModuleKind, Tracing, TypedDataType, TypedFunction},
builtins, builtins,
@ -218,10 +214,7 @@ where
self.compile(options) self.compile(options)
} }
pub fn dump_uplc( pub fn dump_uplc(&self, blueprint: &Blueprint) -> Result<(), Error> {
&self,
blueprint: &Blueprint<Reference, Annotated<Schema>>,
) -> Result<(), Error> {
let dir = self.root.join("artifacts"); let dir = self.root.join("artifacts");
self.event_listener self.event_listener
@ -362,8 +355,7 @@ where
// Read blueprint // Read blueprint
let blueprint = File::open(self.blueprint_path()) let blueprint = File::open(self.blueprint_path())
.map_err(|_| blueprint::error::Error::InvalidOrMissingFile)?; .map_err(|_| blueprint::error::Error::InvalidOrMissingFile)?;
let blueprint: Blueprint<serde_json::Value, serde_json::Value> = let blueprint: Blueprint = serde_json::from_reader(BufReader::new(blueprint))?;
serde_json::from_reader(BufReader::new(blueprint))?;
// Calculate the address // Calculate the address
let when_too_many = let when_too_many =
@ -386,12 +378,11 @@ where
&self, &self,
title: Option<&String>, title: Option<&String>,
param: &Term<DeBruijn>, param: &Term<DeBruijn>,
) -> Result<Blueprint<serde_json::Value, serde_json::Value>, Error> { ) -> Result<Blueprint, Error> {
// Read blueprint // Read blueprint
let blueprint = File::open(self.blueprint_path()) let blueprint = File::open(self.blueprint_path())
.map_err(|_| blueprint::error::Error::InvalidOrMissingFile)?; .map_err(|_| blueprint::error::Error::InvalidOrMissingFile)?;
let mut blueprint: Blueprint<serde_json::Value, serde_json::Value> = let mut blueprint: Blueprint = serde_json::from_reader(BufReader::new(blueprint))?;
serde_json::from_reader(BufReader::new(blueprint))?;
// Apply parameters // Apply parameters
let when_too_many = let when_too_many =
@ -400,7 +391,9 @@ where
let applied_validator = let applied_validator =
blueprint.with_validator(title, when_too_many, when_missing, |validator| { blueprint.with_validator(title, when_too_many, when_missing, |validator| {
validator.apply(param).map_err(|e| e.into()) validator
.apply(&blueprint.definitions, param)
.map_err(|e| e.into())
})?; })?;
// Overwrite validator // Overwrite validator

View File

@ -1,18 +1,22 @@
use crate::with_project; use crate::with_project;
use aiken_project::error::Error; use aiken_project::{blueprint, error::Error};
use miette::IntoDiagnostic; use owo_colors::{OwoColorize, Stream::Stderr};
use std::{fs, path::PathBuf}; use std::{fs, path::PathBuf, process, rc::Rc};
use uplc::{ use uplc::ast::{Constant, DeBruijn, Term};
ast::{DeBruijn, Term},
parser,
};
/// Apply a parameter to a parameterized validator. /// Apply a parameter to a parameterized validator.
#[derive(clap::Args)] #[derive(clap::Args)]
pub struct Args { pub struct Args {
/// The parameter, as a Plutus Data (CBOR, hex-encoded)
parameter: String,
/// Path to project /// Path to project
directory: Option<PathBuf>, directory: Option<PathBuf>,
/// Output file. Optional, print on stdout when omitted.
#[clap(short, long)]
out: Option<PathBuf>,
/// Name of the validator's module within the project. Optional if there's only one validator. /// Name of the validator's module within the project. Optional if there's only one validator.
#[clap(short, long)] #[clap(short, long)]
module: Option<String>, module: Option<String>,
@ -20,23 +24,58 @@ pub struct Args {
/// Name of the validator within the module. Optional if there's only one validator. /// Name of the validator within the module. Optional if there's only one validator.
#[clap(short, long)] #[clap(short, long)]
validator: Option<String>, validator: Option<String>,
/// The parameter, using high-level UPLC-syntax
parameter: String,
} }
pub fn exec( pub fn exec(
Args { Args {
parameter,
directory, directory,
out,
module, module,
validator, validator,
parameter,
}: Args, }: Args,
) -> miette::Result<()> { ) -> miette::Result<()> {
let term: Term<DeBruijn> = parser::term(&parameter) eprintln!(
.into_diagnostic()? "{} inputs",
.try_into() " Parsing"
.into_diagnostic()?; .if_supports_color(Stderr, |s| s.purple())
.if_supports_color(Stderr, |s| s.bold()),
);
let bytes = hex::decode(parameter)
.map_err::<Error, _>(|e| {
blueprint::error::Error::MalformedParameter {
hint: format!("Invalid hex-encoded string: {e}"),
}
.into()
})
.unwrap_or_else(|e| {
println!();
e.report();
process::exit(1)
});
let data = uplc::plutus_data(&bytes)
.map_err::<Error, _>(|e| {
blueprint::error::Error::MalformedParameter {
hint: format!("Invalid Plutus data; malformed CBOR encoding: {e}"),
}
.into()
})
.unwrap_or_else(|e| {
println!();
e.report();
process::exit(1)
});
let term: Term<DeBruijn> = Term::Constant(Rc::new(Constant::Data(data)));
eprintln!(
"{} blueprint",
" Analyzing"
.if_supports_color(Stderr, |s| s.purple())
.if_supports_color(Stderr, |s| s.bold()),
);
with_project(directory, |p| { with_project(directory, |p| {
let title = module.as_ref().map(|m| { let title = module.as_ref().map(|m| {
@ -51,16 +90,35 @@ pub fn exec(
let title = title.as_ref().or(validator.as_ref()); let title = title.as_ref().or(validator.as_ref());
eprintln!(
"{} parameter",
" Applying"
.if_supports_color(Stderr, |s| s.purple())
.if_supports_color(Stderr, |s| s.bold()),
);
let blueprint = p.apply_parameter(title, &term)?; let blueprint = p.apply_parameter(title, &term)?;
let json = serde_json::to_string_pretty(&blueprint).unwrap(); let json = serde_json::to_string_pretty(&blueprint).unwrap();
fs::write(p.blueprint_path(), json).map_err(|error| { match out {
Error::FileIo { None => {
println!("\n{}\n", json);
Ok(())
}
Some(ref path) => fs::write(path, json).map_err(|error| Error::FileIo {
error, error,
path: p.blueprint_path(), path: p.blueprint_path(),
} }),
.into() }?;
})
eprintln!(
"{}",
" Done"
.if_supports_color(Stderr, |s| s.purple())
.if_supports_color(Stderr, |s| s.bold()),
);
Ok(())
}) })
} }

View File

@ -65,7 +65,7 @@ pub fn exec(
.map_err(|_| BlueprintError::InvalidOrMissingFile) .map_err(|_| BlueprintError::InvalidOrMissingFile)
.into_diagnostic()?; .into_diagnostic()?;
let blueprint: Blueprint<serde_json::Value, serde_json::Value> = let blueprint: Blueprint =
serde_json::from_reader(BufReader::new(blueprint)).into_diagnostic()?; serde_json::from_reader(BufReader::new(blueprint)).into_diagnostic()?;
// Perform the conversion // Perform the conversion

View File

@ -1,23 +1,3 @@
use std::{
fmt::{self, Display},
hash::{self, Hash},
rc::Rc,
};
use num_bigint::BigInt;
use serde::{
self,
de::{self, Deserialize, Deserializer, MapAccess, Visitor},
ser::{Serialize, SerializeStruct, Serializer},
};
use pallas_addresses::{Network, ShelleyAddress, ShelleyDelegationPart, ShelleyPaymentPart};
use pallas_primitives::{
alonzo::PlutusData,
babbage::{self as cardano, Language},
};
use pallas_traverse::ComputeHash;
use crate::{ use crate::{
builtins::DefaultFunction, builtins::DefaultFunction,
debruijn::{self, Converter}, debruijn::{self, Converter},
@ -28,6 +8,24 @@ use crate::{
Machine, Machine,
}, },
}; };
use num_bigint::BigInt;
use num_traits::ToPrimitive;
use pallas_addresses::{Network, ShelleyAddress, ShelleyDelegationPart, ShelleyPaymentPart};
use pallas_primitives::{
alonzo::{self as pallas, Constr, PlutusData},
babbage::{self as cardano, Language},
};
use pallas_traverse::ComputeHash;
use serde::{
self,
de::{self, Deserialize, Deserializer, MapAccess, Visitor},
ser::{Serialize, SerializeStruct, Serializer},
};
use std::{
fmt::{self, Display},
hash::{self, Hash},
rc::Rc,
};
/// This represents a program in Untyped Plutus Core. /// This represents a program in Untyped Plutus Core.
/// A program contains a version tuple and a term. /// A program contains a version tuple and a term.
@ -239,6 +237,61 @@ pub enum Constant {
Data(PlutusData), Data(PlutusData),
} }
pub struct Data {}
// TODO: See about moving these builders upstream to Pallas?
impl Data {
pub fn integer(i: BigInt) -> PlutusData {
match i.to_i64() {
Some(i) => PlutusData::BigInt(pallas::BigInt::Int(i.into())),
None => {
let (sign, bytes) = i.to_bytes_be();
match sign {
num_bigint::Sign::Minus => {
PlutusData::BigInt(pallas::BigInt::BigNInt(bytes.into()))
}
_ => PlutusData::BigInt(pallas::BigInt::BigUInt(bytes.into())),
}
}
}
}
pub fn bytestring(bytes: Vec<u8>) -> PlutusData {
PlutusData::BoundedBytes(bytes.into())
}
pub fn map(kvs: Vec<(PlutusData, PlutusData)>) -> PlutusData {
PlutusData::Map(kvs.into())
}
pub fn list(xs: Vec<PlutusData>) -> PlutusData {
PlutusData::Array(xs)
}
pub fn constr(ix: u64, fields: Vec<PlutusData>) -> PlutusData {
// NOTE: see https://github.com/input-output-hk/plutus/blob/9538fc9829426b2ecb0628d352e2d7af96ec8204/plutus-core/plutus-core/src/PlutusCore/Data.hs#L139-L155
if ix < 7 {
PlutusData::Constr(Constr {
tag: 121 + ix,
any_constructor: None,
fields,
})
} else if ix < 128 {
PlutusData::Constr(Constr {
tag: 1280 + ix - 7,
any_constructor: None,
fields,
})
} else {
PlutusData::Constr(Constr {
tag: 102,
any_constructor: Some(ix),
fields,
})
}
}
}
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum Type { pub enum Type {
Bool, Bool,

View File

@ -2,13 +2,14 @@ use crate::{
ast::{Constant, Name, Term, Type}, ast::{Constant, Name, Term, Type},
builtins::DefaultFunction, builtins::DefaultFunction,
}; };
use pallas_primitives::alonzo::PlutusData;
pub const CONSTR_FIELDS_EXPOSER: &str = "__constr_fields_exposer"; pub const CONSTR_FIELDS_EXPOSER: &str = "__constr_fields_exposer";
pub const CONSTR_INDEX_EXPOSER: &str = "__constr_index_exposer"; pub const CONSTR_INDEX_EXPOSER: &str = "__constr_index_exposer";
pub const CONSTR_GET_FIELD: &str = "__constr_get_field"; pub const CONSTR_GET_FIELD: &str = "__constr_get_field";
pub const EXPECT_ON_LIST: &str = "__expect_on_list"; pub const EXPECT_ON_LIST: &str = "__expect_on_list";
impl Term<Name> { impl<T> Term<T> {
pub fn apply(self, arg: Self) -> Self { pub fn apply(self, arg: Self) -> Self {
Term::Apply { Term::Apply {
function: self.into(), function: self.into(),
@ -16,13 +17,6 @@ impl Term<Name> {
} }
} }
pub fn lambda(self, parameter_name: impl ToString) -> Self {
Term::Lambda {
parameter_name: Name::text(parameter_name).into(),
body: self.into(),
}
}
pub fn force(self) -> Self { pub fn force(self) -> Self {
Term::Force(self.into()) Term::Force(self.into())
} }
@ -31,10 +25,6 @@ impl Term<Name> {
Term::Delay(self.into()) Term::Delay(self.into())
} }
pub fn var(name: impl ToString) -> Self {
Term::Var(Name::text(name).into())
}
pub fn integer(i: num_bigint::BigInt) -> Self { pub fn integer(i: num_bigint::BigInt) -> Self {
Term::Constant(Constant::Integer(i).into()) Term::Constant(Constant::Integer(i).into())
} }
@ -55,6 +45,10 @@ impl Term<Name> {
Term::Constant(Constant::Unit.into()) Term::Constant(Constant::Unit.into())
} }
pub fn data(d: PlutusData) -> Self {
Term::Constant(Constant::Data(d).into())
}
pub fn empty_list() -> Self { pub fn empty_list() -> Self {
Term::Constant(Constant::ProtoList(Type::Data, vec![]).into()) Term::Constant(Constant::ProtoList(Type::Data, vec![]).into())
} }
@ -204,7 +198,7 @@ impl Term<Name> {
.force() .force()
} }
pub fn trace(self, msg_term: Term<Name>) -> Self { pub fn trace(self, msg_term: Self) -> Self {
Term::Builtin(DefaultFunction::Trace) Term::Builtin(DefaultFunction::Trace)
.force() .force()
.apply(msg_term) .apply(msg_term)
@ -212,41 +206,34 @@ impl Term<Name> {
.force() .force()
} }
pub fn assert_on_list(self) -> Term<Name> { pub fn final_wrapper(self) -> Self {
self.lambda(EXPECT_ON_LIST.to_string())
.apply(
Term::var(EXPECT_ON_LIST.to_string()).apply(Term::var(EXPECT_ON_LIST.to_string())),
)
.lambda(EXPECT_ON_LIST.to_string())
.apply(
Term::var("__list_to_check".to_string())
.delayed_choose_list(
Term::unit(),
Term::var("__check_with".to_string())
.apply(
Term::head_list().apply(Term::var("__list_to_check".to_string())),
)
.choose_unit(
Term::var(EXPECT_ON_LIST.to_string())
.apply(Term::var(EXPECT_ON_LIST.to_string()))
.apply(
Term::tail_list()
.apply(Term::var("__list_to_check".to_string())),
)
.apply(Term::var("__check_with".to_string())),
),
)
.lambda("__check_with".to_string())
.lambda("__list_to_check".to_string())
.lambda(EXPECT_ON_LIST),
)
}
pub fn final_wrapper(self: Term<Name>) -> Term<Name> {
self.delayed_if_else(Term::unit(), Term::Error) self.delayed_if_else(Term::unit(), Term::Error)
} }
pub fn constr_fields_exposer(self: Term<Name>) -> Term<Name> { pub fn repeat_tail_list(self, repeat: usize) -> Self {
let mut term = self;
for _ in 0..repeat {
term = Term::tail_list().apply(term);
}
term
}
}
impl Term<Name> {
pub fn lambda(self, parameter_name: impl ToString) -> Self {
Term::Lambda {
parameter_name: Name::text(parameter_name).into(),
body: self.into(),
}
}
pub fn var(name: impl ToString) -> Self {
Term::Var(Name::text(name).into())
}
pub fn constr_fields_exposer(self) -> Self {
self.lambda(CONSTR_FIELDS_EXPOSER.to_string()).apply( self.lambda(CONSTR_FIELDS_EXPOSER.to_string()).apply(
Term::snd_pair() Term::snd_pair()
.apply(Term::unconstr_data().apply(Term::var("__constr_var".to_string()))) .apply(Term::unconstr_data().apply(Term::var("__constr_var".to_string())))
@ -254,7 +241,7 @@ impl Term<Name> {
) )
} }
pub fn constr_index_exposer(self: Term<Name>) -> Term<Name> { pub fn constr_index_exposer(self) -> Self {
self.lambda(CONSTR_INDEX_EXPOSER.to_string()).apply( self.lambda(CONSTR_INDEX_EXPOSER.to_string()).apply(
Term::fst_pair() Term::fst_pair()
.apply(Term::unconstr_data().apply(Term::var("__constr_var".to_string()))) .apply(Term::unconstr_data().apply(Term::var("__constr_var".to_string())))
@ -262,7 +249,7 @@ impl Term<Name> {
) )
} }
pub fn constr_get_field(self: Term<Name>) -> Term<Name> { pub fn constr_get_field(self) -> Self {
self.lambda(CONSTR_GET_FIELD.to_string()) self.lambda(CONSTR_GET_FIELD.to_string())
.apply( .apply(
Term::var(CONSTR_GET_FIELD.to_string()) Term::var(CONSTR_GET_FIELD.to_string())
@ -298,13 +285,33 @@ impl Term<Name> {
) )
} }
pub fn repeat_tail_list(self: Term<Name>, repeat: usize) -> Term<Name> { pub fn assert_on_list(self) -> Self {
let mut term = self; self.lambda(EXPECT_ON_LIST.to_string())
.apply(
for _ in 0..repeat { Term::var(EXPECT_ON_LIST.to_string()).apply(Term::var(EXPECT_ON_LIST.to_string())),
term = Term::tail_list().apply(term); )
} .lambda(EXPECT_ON_LIST.to_string())
.apply(
term Term::var("__list_to_check".to_string())
.delayed_choose_list(
Term::unit(),
Term::var("__check_with".to_string())
.apply(
Term::head_list().apply(Term::var("__list_to_check".to_string())),
)
.choose_unit(
Term::var(EXPECT_ON_LIST.to_string())
.apply(Term::var(EXPECT_ON_LIST.to_string()))
.apply(
Term::tail_list()
.apply(Term::var("__list_to_check".to_string())),
)
.apply(Term::var("__check_with".to_string())),
),
)
.lambda("__check_with".to_string())
.lambda("__list_to_check".to_string())
.lambda(EXPECT_ON_LIST),
)
} }
} }