Bootstrap schema validation for simple constants.
This commit is contained in:
parent
9033b44044
commit
d620f6367c
|
@ -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;
|
||||||
|
|
|
@ -95,14 +95,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"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
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 owo_colors::{OwoColorize, Stream::Stdout};
|
use owo_colors::{OwoColorize, Stream::Stdout};
|
||||||
|
@ -43,6 +46,30 @@ pub enum Error {
|
||||||
blueprint_apply_command = "blueprint apply".if_supports_color(Stdout, |s| s.purple()),
|
blueprint_apply_command = "blueprint apply".if_supports_color(Stdout, |s| s.purple()),
|
||||||
))]
|
))]
|
||||||
ParameterizedValidator { n: usize },
|
ParameterizedValidator { n: usize },
|
||||||
|
|
||||||
|
#[error("I failed to infer what should be the schema of a given parameter to apply.")]
|
||||||
|
#[diagnostic(code("aiken:blueprint::apply::malformed::argument"))]
|
||||||
|
#[diagnostic(help(
|
||||||
|
"I couldn't figure out the schema corresponding to a term you've given. Here's a possible hint about why I failed: {hint}"
|
||||||
|
))]
|
||||||
|
UnableToInferArgumentSchema { hint: String },
|
||||||
|
|
||||||
|
#[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 stumble upon an unknown reference:\n\n {reference}\n\nThis is unfortunate, but signals that either the reference is invalid or that the correspond schema definition is missing.",
|
||||||
|
reference = reference.as_json_pointer()
|
||||||
|
))]
|
||||||
|
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, schemas didn't match in this case.\n\nI am expecting the following:\n\n{}But I've inferred the following schema from your input:\n\n{}",
|
||||||
|
serde_json::to_string_pretty(&expected).unwrap().if_supports_color(Stdout, |s| s.green()),
|
||||||
|
serde_json::to_string_pretty(&inferred).unwrap().if_supports_color(Stdout, |s| s.red()),
|
||||||
|
))]
|
||||||
|
SchemaMismatch { expected: Schema, inferred: Schema },
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe impl Send for Error {}
|
unsafe impl Send for Error {}
|
||||||
|
|
|
@ -12,6 +12,7 @@ use serde::{
|
||||||
ser::{Serialize, SerializeStruct, Serializer},
|
ser::{Serialize, SerializeStruct, Serializer},
|
||||||
};
|
};
|
||||||
use std::{collections::HashMap, fmt, ops::Deref, sync::Arc};
|
use std::{collections::HashMap, fmt, ops::Deref, sync::Arc};
|
||||||
|
use uplc::ast::Term;
|
||||||
|
|
||||||
// NOTE: Can be anything BUT 0
|
// NOTE: Can be anything BUT 0
|
||||||
pub const REDEEMER_DISCRIMINANT: usize = 1;
|
pub const REDEEMER_DISCRIMINANT: usize = 1;
|
||||||
|
@ -80,6 +81,50 @@ impl<T> From<T> for Annotated<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a, T> TryFrom<&'a Term<T>> for Schema {
|
||||||
|
type Error = &'a str;
|
||||||
|
|
||||||
|
fn try_from(term: &'a Term<T>) -> Result<Schema, Self::Error> {
|
||||||
|
use uplc::{ast::Constant, Constr, PlutusData};
|
||||||
|
|
||||||
|
match term {
|
||||||
|
Term::Constant(constant) => match constant.deref() {
|
||||||
|
Constant::Integer(..) => Ok(Schema::Integer),
|
||||||
|
Constant::Bool(..) => Ok(Schema::Boolean),
|
||||||
|
Constant::ByteString(..) => Ok(Schema::Bytes),
|
||||||
|
Constant::String(..) => Ok(Schema::String),
|
||||||
|
Constant::Unit => Ok(Schema::Unit),
|
||||||
|
Constant::ProtoList{..} => todo!("can't convert from ProtoList to Schema; note that you probably want to use a Data's list instead anyway."),
|
||||||
|
Constant::ProtoPair{..} => todo!("can't convert from ProtoPair to Schema; note that you probably want to use a Data's list instead anyway."),
|
||||||
|
Constant::Data(data) => Ok(Schema::Data(match data {
|
||||||
|
PlutusData::BigInt(..) => {
|
||||||
|
Data::Integer
|
||||||
|
}
|
||||||
|
PlutusData::BoundedBytes(..) => {
|
||||||
|
Data::Bytes
|
||||||
|
}
|
||||||
|
PlutusData::Map(keyValuePair) => {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
PlutusData::Array(elems) => {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
PlutusData::Constr(Constr{ tag, fields, any_constructor }) => {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
Term::Delay(..)
|
||||||
|
| Term::Lambda { .. }
|
||||||
|
| Term::Var(..)
|
||||||
|
| Term::Apply { .. }
|
||||||
|
| Term::Force(..)
|
||||||
|
| Term::Error
|
||||||
|
| Term::Builtin(..) => Err("not a UPLC constant"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Annotated<Schema> {
|
impl Annotated<Schema> {
|
||||||
pub fn as_wrapped_redeemer(
|
pub fn as_wrapped_redeemer(
|
||||||
definitions: &mut Definitions<Annotated<Schema>>,
|
definitions: &mut Definitions<Annotated<Schema>>,
|
||||||
|
|
|
@ -6,11 +6,13 @@ use super::{
|
||||||
use crate::module::{CheckedModule, CheckedModules};
|
use crate::module::{CheckedModule, CheckedModules};
|
||||||
use aiken_lang::{
|
use aiken_lang::{
|
||||||
ast::{TypedArg, TypedFunction, TypedValidator},
|
ast::{TypedArg, TypedFunction, TypedValidator},
|
||||||
|
builtins,
|
||||||
gen_uplc::CodeGenerator,
|
gen_uplc::CodeGenerator,
|
||||||
};
|
};
|
||||||
use miette::NamedSource;
|
use miette::NamedSource;
|
||||||
use serde;
|
use serde;
|
||||||
use uplc::ast::{DeBruijn, Program, Term};
|
use std::{borrow::Borrow, collections::HashMap};
|
||||||
|
use uplc::ast::{Constant, DeBruijn, Program, Term};
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct Validator {
|
pub struct Validator {
|
||||||
|
@ -44,6 +46,48 @@ pub struct Argument {
|
||||||
pub schema: Reference,
|
pub schema: Reference,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Reference> for Argument {
|
||||||
|
fn from(schema: Reference) -> Argument {
|
||||||
|
Argument {
|
||||||
|
title: None,
|
||||||
|
schema,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Argument {
|
||||||
|
pub fn validate(
|
||||||
|
&self,
|
||||||
|
definitions: &Definitions<Annotated<Schema>>,
|
||||||
|
term: &Term<DeBruijn>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let expected_schema = &definitions
|
||||||
|
.lookup(&self.schema)
|
||||||
|
.map(Ok)
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
Err(Error::UnresolvedSchemaReference {
|
||||||
|
reference: self.schema.clone(),
|
||||||
|
})
|
||||||
|
})?
|
||||||
|
.annotated;
|
||||||
|
|
||||||
|
let inferred_schema: Schema =
|
||||||
|
term.try_into()
|
||||||
|
.map_err(|hint: &str| Error::UnableToInferArgumentSchema {
|
||||||
|
hint: hint.to_owned(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if expected_schema != &inferred_schema {
|
||||||
|
Err(Error::SchemaMismatch {
|
||||||
|
expected: expected_schema.to_owned(),
|
||||||
|
inferred: inferred_schema,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Validator {
|
impl Validator {
|
||||||
pub fn from_checked_module(
|
pub fn from_checked_module(
|
||||||
modules: &CheckedModules,
|
modules: &CheckedModules,
|
||||||
|
@ -164,8 +208,8 @@ impl Validator {
|
||||||
pub fn apply(self, arg: &Term<DeBruijn>) -> Result<Self, Error> {
|
pub fn apply(self, arg: &Term<DeBruijn>) -> 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(&self.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(),
|
||||||
|
@ -178,7 +222,14 @@ impl Validator {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::{
|
||||||
|
super::{
|
||||||
|
definitions::Definitions,
|
||||||
|
error::Error,
|
||||||
|
schema::{Annotated, Data, Schema},
|
||||||
|
},
|
||||||
|
*,
|
||||||
|
};
|
||||||
use crate::{module::ParsedModule, PackageName};
|
use crate::{module::ParsedModule, PackageName};
|
||||||
use aiken_lang::{
|
use aiken_lang::{
|
||||||
self,
|
self,
|
||||||
|
@ -193,6 +244,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
|
||||||
|
@ -314,6 +366,24 @@ 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
|
||||||
|
.register::<_, Error>(&builtins::int(), &HashMap::new(), |_| {
|
||||||
|
Ok(Schema::Data(Data::Integer).into())
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
definitions
|
||||||
|
.register::<_, Error>(&builtins::byte_array(), &HashMap::new(), |_| {
|
||||||
|
Ok(Schema::Data(Data::Bytes).into())
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
definitions
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn mint_basic() {
|
fn mint_basic() {
|
||||||
assert_validator(
|
assert_validator(
|
||||||
|
@ -1092,4 +1162,27 @@ mod test {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validate_arguments_integer() {
|
||||||
|
let term = Term::data(uplc::Data::integer(42.into()));
|
||||||
|
let definitions = fixture_definitions();
|
||||||
|
let arg = Argument {
|
||||||
|
title: None,
|
||||||
|
schema: Reference::new("Int"),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(matches!(arg.validate(&definitions, &term), Ok { .. }))
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn validate_arguments_bytestring() {
|
||||||
|
let term = Term::data(uplc::Data::bytestring(vec![102, 111, 111]));
|
||||||
|
let definitions = fixture_definitions();
|
||||||
|
let arg = Argument {
|
||||||
|
title: None,
|
||||||
|
schema: Reference::new("ByteArray"),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(matches!(arg.validate(&definitions, &term), Ok { .. }))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue