Bootstrap schema validation for simple constants.

This commit is contained in:
KtorZ 2023-04-06 11:57:23 +02:00
parent 9033b44044
commit d620f6367c
No known key found for this signature in database
GPG Key ID: 33173CB6F77F4277
5 changed files with 175 additions and 13 deletions

View File

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

View File

@ -95,14 +95,14 @@ impl Reference {
}
/// 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()
}
/// 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
/// 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"))
}
}

View File

@ -1,4 +1,7 @@
use super::schema;
use super::{
definitions::Reference,
schema::{self, Schema},
};
use aiken_lang::ast::Span;
use miette::{Diagnostic, NamedSource};
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()),
))]
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 {}

View File

@ -12,6 +12,7 @@ use serde::{
ser::{Serialize, SerializeStruct, Serializer},
};
use std::{collections::HashMap, fmt, ops::Deref, sync::Arc};
use uplc::ast::Term;
// NOTE: Can be anything BUT 0
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> {
pub fn as_wrapped_redeemer(
definitions: &mut Definitions<Annotated<Schema>>,

View File

@ -6,11 +6,13 @@ use super::{
use crate::module::{CheckedModule, CheckedModules};
use aiken_lang::{
ast::{TypedArg, TypedFunction, TypedValidator},
builtins,
gen_uplc::CodeGenerator,
};
use miette::NamedSource;
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)]
pub struct Validator {
@ -44,6 +46,48 @@ pub struct Argument {
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 {
pub fn from_checked_module(
modules: &CheckedModules,
@ -164,8 +208,8 @@ impl Validator {
pub fn apply(self, arg: &Term<DeBruijn>) -> Result<Self, Error> {
match self.parameters.split_first() {
None => Err(Error::NoParametersToApply),
Some((_, tail)) => {
// TODO: Ideally, we should control that the applied term matches its schema.
Some((head, tail)) => {
head.validate(&self.definitions, arg)?;
Ok(Self {
program: self.program.apply_term(arg),
parameters: tail.to_vec(),
@ -178,7 +222,14 @@ impl Validator {
#[cfg(test)]
mod test {
use super::*;
use super::{
super::{
definitions::Definitions,
error::Error,
schema::{Annotated, Data, Schema},
},
*,
};
use crate::{module::ParsedModule, PackageName};
use aiken_lang::{
self,
@ -193,6 +244,7 @@ mod test {
use indexmap::IndexMap;
use serde_json::{self, json};
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
// 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);
}
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]
fn mint_basic() {
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 { .. }))
}
}