From d620f6367c96c90bccf0ef7029d9b2bd79b90dc9 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Thu, 6 Apr 2023 11:57:23 +0200 Subject: [PATCH] Bootstrap schema validation for simple constants. --- crates/aiken-lang/src/tipo.rs | 9 +- .../src/blueprint/definitions.rs | 4 +- crates/aiken-project/src/blueprint/error.rs | 29 ++++- crates/aiken-project/src/blueprint/schema.rs | 45 ++++++++ .../aiken-project/src/blueprint/validator.rs | 101 +++++++++++++++++- 5 files changed, 175 insertions(+), 13 deletions(-) diff --git a/crates/aiken-lang/src/tipo.rs b/crates/aiken-lang/src/tipo.rs index fabb31c5..a3b494ce 100644 --- a/crates/aiken-lang/src/tipo.rs +++ b/crates/aiken-lang/src/tipo.rs @@ -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; diff --git a/crates/aiken-project/src/blueprint/definitions.rs b/crates/aiken-project/src/blueprint/definitions.rs index 20b042c2..047e3f68 100644 --- a/crates/aiken-project/src/blueprint/definitions.rs +++ b/crates/aiken-project/src/blueprint/definitions.rs @@ -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")) } } diff --git a/crates/aiken-project/src/blueprint/error.rs b/crates/aiken-project/src/blueprint/error.rs index af173f26..25e4f331 100644 --- a/crates/aiken-project/src/blueprint/error.rs +++ b/crates/aiken-project/src/blueprint/error.rs @@ -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 {} diff --git a/crates/aiken-project/src/blueprint/schema.rs b/crates/aiken-project/src/blueprint/schema.rs index 2f0b7958..66c84e5c 100644 --- a/crates/aiken-project/src/blueprint/schema.rs +++ b/crates/aiken-project/src/blueprint/schema.rs @@ -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 From for Annotated { } } +impl<'a, T> TryFrom<&'a Term> for Schema { + type Error = &'a str; + + fn try_from(term: &'a Term) -> Result { + 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 { pub fn as_wrapped_redeemer( definitions: &mut Definitions>, diff --git a/crates/aiken-project/src/blueprint/validator.rs b/crates/aiken-project/src/blueprint/validator.rs index d500706e..9f241930 100644 --- a/crates/aiken-project/src/blueprint/validator.rs +++ b/crates/aiken-project/src/blueprint/validator.rs @@ -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 for Argument { + fn from(schema: Reference) -> Argument { + Argument { + title: None, + schema, + } + } +} + +impl Argument { + pub fn validate( + &self, + definitions: &Definitions>, + term: &Term, + ) -> 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) -> Result { 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> { + 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 { .. })) + } }