Allow annotating Data for blueprint

This commit allows Data to be optionally annotated with a
  phantom-type. This doesn't change anything in codegen but we can now
  leverage this information to generate better blueprint schemas.
This commit is contained in:
KtorZ 2024-02-07 16:01:35 +01:00 committed by Kasey
parent 20ce19dfb1
commit 3c8460e6af
8 changed files with 174 additions and 51 deletions

View File

@ -684,7 +684,7 @@ impl UnqualifiedImport {
} }
// TypeAst // TypeAst
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Annotation { pub enum Annotation {
Constructor { Constructor {
location: Span, location: Span,
@ -1411,7 +1411,7 @@ impl Display for TraceLevel {
} }
} }
#[derive(Copy, Clone, PartialEq, Eq)] #[derive(Copy, Clone, PartialEq, Eq, Hash)]
pub struct Span { pub struct Span {
pub start: usize, pub start: usize,
pub end: usize, pub end: usize,

View File

@ -38,6 +38,7 @@ pub fn prelude(id_gen: &IdGenerator) -> TypeInfo {
types_constructors: HashMap::new(), types_constructors: HashMap::new(),
values: HashMap::new(), values: HashMap::new(),
accessors: HashMap::new(), accessors: HashMap::new(),
annotations: HashMap::new(),
}; };
// Int // Int
@ -423,6 +424,7 @@ pub fn plutus(id_gen: &IdGenerator) -> TypeInfo {
types_constructors: HashMap::new(), types_constructors: HashMap::new(),
values: HashMap::new(), values: HashMap::new(),
accessors: HashMap::new(), accessors: HashMap::new(),
annotations: HashMap::new(),
}; };
for builtin in DefaultFunction::iter() { for builtin in DefaultFunction::iter() {

View File

@ -1,6 +1,6 @@
use self::{environment::Environment, pretty::Printer}; use self::{environment::Environment, pretty::Printer};
use crate::{ use crate::{
ast::{Constant, DefinitionLocation, ModuleKind, Span}, ast::{Annotation, Constant, DefinitionLocation, ModuleKind, Span},
builtins::{G1_ELEMENT, G2_ELEMENT, MILLER_LOOP_RESULT}, builtins::{G1_ELEMENT, G2_ELEMENT, MILLER_LOOP_RESULT},
tipo::fields::FieldMap, tipo::fields::FieldMap,
}; };
@ -755,6 +755,7 @@ pub struct TypeInfo {
pub types_constructors: HashMap<String, Vec<String>>, pub types_constructors: HashMap<String, Vec<String>>,
pub values: HashMap<String, ValueConstructor>, pub values: HashMap<String, ValueConstructor>,
pub accessors: HashMap<String, AccessorsMap>, pub accessors: HashMap<String, AccessorsMap>,
pub annotations: HashMap<Annotation, Rc<Type>>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View File

@ -72,6 +72,9 @@ pub struct Environment<'a> {
pub unused_modules: HashMap<String, Span>, pub unused_modules: HashMap<String, Span>,
/// A mapping from known annotations to their resolved type.
pub annotations: HashMap<Annotation, Rc<Type>>,
/// Warnings /// Warnings
pub warnings: &'a mut Vec<Warning>, pub warnings: &'a mut Vec<Warning>,
} }
@ -88,6 +91,12 @@ impl<'a> Environment<'a> {
self.scope = data.local_values; self.scope = data.local_values;
} }
pub fn annotate(&mut self, return_type: Rc<Type>, annotation: &Annotation) -> Rc<Type> {
self.annotations
.insert(annotation.clone(), return_type.clone());
return_type
}
/// Converts entities with a usage count of 0 to warnings /// Converts entities with a usage count of 0 to warnings
pub fn convert_unused_to_warnings(&mut self) { pub fn convert_unused_to_warnings(&mut self) {
let unused = self let unused = self
@ -657,6 +666,7 @@ impl<'a> Environment<'a> {
imported_types: HashSet::new(), imported_types: HashSet::new(),
current_module, current_module,
current_kind, current_kind,
annotations: HashMap::new(),
warnings, warnings,
entity_usages: vec![HashMap::new()], entity_usages: vec![HashMap::new()],
} }

View File

@ -123,7 +123,7 @@ impl Hydrator {
environment: &mut Environment, environment: &mut Environment,
unbounds: &mut Vec<&'a Span>, unbounds: &mut Vec<&'a Span>,
) -> Result<Rc<Type>, Error> { ) -> Result<Rc<Type>, Error> {
match annotation { let return_type = match annotation {
Annotation::Constructor { Annotation::Constructor {
location, location,
module, module,
@ -153,8 +153,16 @@ impl Hydrator {
environment.increment_usage(name); environment.increment_usage(name);
} }
// Ensure that the correct number of arguments have been given to the constructor // Ensure that the correct number of arguments have been given to the constructor.
if args.len() != parameters.len() { //
// NOTE:
// We do consider a special case for 'Data', where we allow them to optionally
// carry a phantom type. That type has no effect whatsoever on the semantic (since
// anything can be cast to `Data` anyway) but it does provide some nice context for
// blueprint schema generation.
if args.len() != parameters.len() && !return_type.is_data()
|| args.len() > 1 && return_type.is_data()
{
return Err(Error::IncorrectTypeArity { return Err(Error::IncorrectTypeArity {
location: *location, location: *location,
name: name.to_string(), name: name.to_string(),
@ -240,6 +248,8 @@ impl Hydrator {
Ok(tuple(typed_elems)) Ok(tuple(typed_elems))
} }
} }?;
Ok(environment.annotate(return_type, annotation))
} }
} }

View File

@ -126,6 +126,7 @@ impl UntypedModule {
module_types_constructors: types_constructors, module_types_constructors: types_constructors,
module_values: values, module_values: values,
accessors, accessors,
annotations,
.. ..
} = environment; } = environment;
@ -141,6 +142,7 @@ impl UntypedModule {
types_constructors, types_constructors,
values, values,
accessors, accessors,
annotations,
kind, kind,
package: package.to_string(), package: package.to_string(),
}, },

View File

@ -0,0 +1,46 @@
---
source: crates/aiken-project/src/blueprint/validator.rs
description: "Code:\n\npub type Foo {\n foo: Int\n}\n\nvalidator {\n fn annotated_data(datum: Data<Foo>, redeemer: Data, ctx: Void) {\n True\n }\n}\n"
---
{
"title": "test_module.annotated_data",
"datum": {
"title": "datum",
"schema": {
"$ref": "#/definitions/test_module~1Foo"
}
},
"redeemer": {
"title": "redeemer",
"schema": {
"$ref": "#/definitions/Data"
}
},
"compiledCode": "5833010000323222253330044a22930a99802a491856616c696461746f722072657475726e65642066616c736500136565734ae701",
"hash": "52a21f2b4f282074cb6c5aefef20d18c25f3657ca348c73875810c37",
"definitions": {
"Data": {
"title": "Data",
"description": "Any Plutus data."
},
"Int": {
"dataType": "integer"
},
"test_module/Foo": {
"title": "Foo",
"anyOf": [
{
"title": "Foo",
"dataType": "constructor",
"index": 0,
"fields": [
{
"title": "foo",
"$ref": "#/definitions/Int"
}
]
}
]
}
}
}

View File

@ -5,14 +5,15 @@ use super::{
schema::{Annotated, Schema}, schema::{Annotated, Schema},
}; };
use crate::module::{CheckedModule, CheckedModules}; use crate::module::{CheckedModule, CheckedModules};
use std::rc::Rc;
use aiken_lang::{ use aiken_lang::{
ast::{TypedArg, TypedFunction, TypedValidator}, ast::{Annotation, TypedArg, TypedFunction, TypedValidator},
gen_uplc::CodeGenerator, gen_uplc::CodeGenerator,
tipo::Type,
}; };
use miette::NamedSource; use miette::NamedSource;
use serde; use serde;
use std::borrow::Borrow;
use std::rc::Rc;
use uplc::{ use uplc::{
ast::{Constant, DeBruijn, Program, Term}, ast::{Constant, DeBruijn, Program, Term},
PlutusData, PlutusData,
@ -96,7 +97,11 @@ impl Validator {
let parameters = params let parameters = params
.iter() .iter()
.map(|param| { .map(|param| {
Annotated::from_type(modules.into(), &param.tipo, &mut definitions) Annotated::from_type(
modules.into(),
tipo_or_annotation(module, param),
&mut definitions,
)
.map(|schema| Parameter { .map(|schema| Parameter {
title: Some(param.arg_name.get_label()), title: Some(param.arg_name.get_label()),
schema, schema,
@ -118,23 +123,30 @@ impl Validator {
parameters, parameters,
datum: datum datum: datum
.map(|datum| { .map(|datum| {
Annotated::from_type(modules.into(), &datum.tipo, &mut definitions).map_err( Annotated::from_type(
|error| Error::Schema { modules.into(),
tipo_or_annotation(module, datum),
&mut definitions,
)
.map_err(|error| Error::Schema {
error, error,
location: datum.location, location: datum.location,
source_code: NamedSource::new( source_code: NamedSource::new(
module.input_path.display().to_string(), module.input_path.display().to_string(),
module.code.clone(), module.code.clone(),
), ),
}, })
)
}) })
.transpose()? .transpose()?
.map(|schema| Parameter { .map(|schema| Parameter {
title: datum.map(|datum| datum.arg_name.get_label()), title: datum.map(|datum| datum.arg_name.get_label()),
schema, schema,
}), }),
redeemer: Annotated::from_type(modules.into(), &redeemer.tipo, &mut definitions) redeemer: Annotated::from_type(
modules.into(),
tipo_or_annotation(module, redeemer),
&mut definitions,
)
.map_err(|error| Error::Schema { .map_err(|error| Error::Schema {
error, error,
location: redeemer.location, location: redeemer.location,
@ -160,6 +172,29 @@ impl Validator {
} }
} }
fn tipo_or_annotation<'a>(module: &'a CheckedModule, arg: &'a TypedArg) -> &'a Type {
match *arg.tipo.borrow() {
Type::App {
module: ref module_name,
name: ref type_name,
..
} if module_name.is_empty() && &type_name[..] == "Data" => match arg.annotation {
Some(Annotation::Constructor { ref arguments, .. }) if !arguments.is_empty() => module
.ast
.type_info
.annotations
.get(
arguments
.first()
.expect("guard ensures at least one element"),
)
.unwrap_or(&arg.tipo),
_ => &arg.tipo,
},
_ => &arg.tipo,
}
}
impl Validator { impl Validator {
pub fn apply( pub fn apply(
self, self,
@ -543,6 +578,23 @@ mod tests {
); );
} }
#[test]
fn annotated_data() {
assert_validator!(
r#"
pub type Foo {
foo: Int
}
validator {
fn annotated_data(datum: Data<Foo>, redeemer: Data, ctx: Void) {
True
}
}
"#
);
}
#[test] #[test]
fn validate_arguments_integer() { fn validate_arguments_integer() {
let definitions = fixture_definitions(); let definitions = fixture_definitions();