From ee1793886ad677b7af9ed86c90e54b29892b5edc Mon Sep 17 00:00:00 2001 From: Robin Freyler Date: Thu, 7 Mar 2019 17:07:42 +0100 Subject: [PATCH] [lang] Initial commit for the actual eDSL --- lang/Cargo.toml | 43 +++++++ lang/LICENSE | 1 + lang/README.md | 1 + lang/src/ast.rs | 110 ++++++++++++++++++ lang/src/errors.rs | 61 ++++++++++ lang/src/hir.rs | 245 +++++++++++++++++++++++++++++++++++++++ lang/src/ident_ext.rs | 17 +++ lang/src/lib.rs | 35 ++++++ lang/src/parser.rs | 213 ++++++++++++++++++++++++++++++++++ lang/tests/simple.rs | 37 ++++++ lang/tests/simple.rs.gen | 51 ++++++++ 11 files changed, 814 insertions(+) create mode 100644 lang/Cargo.toml create mode 120000 lang/LICENSE create mode 120000 lang/README.md create mode 100644 lang/src/ast.rs create mode 100644 lang/src/errors.rs create mode 100644 lang/src/hir.rs create mode 100644 lang/src/ident_ext.rs create mode 100644 lang/src/lib.rs create mode 100644 lang/src/parser.rs create mode 100644 lang/tests/simple.rs create mode 100644 lang/tests/simple.rs.gen diff --git a/lang/Cargo.toml b/lang/Cargo.toml new file mode 100644 index 0000000000..c6ca0f3993 --- /dev/null +++ b/lang/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "pdsl_lang" +version = "0.1.0" +authors = ["Herobird "] +edition = "2018" + +license = "MIT/Apache-2.0" +readme = "README.md" + +# repository = "https://github.com/robbepop/substrate-contract" +# homepage = "https://github.com/robbepop/substrate-contract" +# documentation = "https://robbepop.github.io/pwasm-abi/substrate-contract/" + +description = "[pDSL: Parity eDSL] Rust based eDSL for writing smart contracts for Substrate" +keywords = ["wasm", "parity", "webassembly", "blockchain", "edsl"] +categories = ["no-std", "embedded"] + +include = ["Cargo.toml", "src/**/*.rs", "README.md", "LICENSE"] + +[dependencies] +pdsl_core = { path = "../core/" } +parity-codec = { version = "3.1", default-features = false, features = ["derive"] } + +quote = "0.6" +syn = { version = "0.15", features = ["parsing", "full", "extra-traits"] } +proc-macro2 = "0.4" +heck = "0.3" +itertools = "0.7" +either = "1.5" + +[lib] +name = "pdsl_lang" +proc-macro = true + +[features] +default = ["std"] +std = [ + "pdsl_core/std", + "parity-codec/std", +] +test-env = [ + "pdsl_core/test-env", +] diff --git a/lang/LICENSE b/lang/LICENSE new file mode 120000 index 0000000000..ea5b60640b --- /dev/null +++ b/lang/LICENSE @@ -0,0 +1 @@ +../LICENSE \ No newline at end of file diff --git a/lang/README.md b/lang/README.md new file mode 120000 index 0000000000..32d46ee883 --- /dev/null +++ b/lang/README.md @@ -0,0 +1 @@ +../README.md \ No newline at end of file diff --git a/lang/src/ast.rs b/lang/src/ast.rs new file mode 100644 index 0000000000..21b3d1fbe3 --- /dev/null +++ b/lang/src/ast.rs @@ -0,0 +1,110 @@ +use crate::parser::keywords; + +use proc_macro2::Ident; +// use quote::ToTokens; +use syn::{ + token, + punctuated::Punctuated, + ReturnType, + Type, +}; + +#[derive(Debug)] +pub struct Contract { + pub items: Vec, +} + +impl Contract { + pub fn states<'a>(&'a self) -> impl Iterator + 'a { + self.items.iter().filter_map(|item| match *item { + Item::State(ref c) => Some(c), + _ => None, + }) + } + + pub fn impl_blocks<'a>(&'a self) -> impl Iterator + 'a { + self.items.iter().filter_map(|item| match *item { + Item::Impl(ref i) => Some(i), + _ => None, + }) + } +} + +#[derive(Debug)] +pub enum Item { + State(ItemState), + Impl(ItemImpl), +} + +#[derive(Debug)] +pub struct ItemState { + pub attrs: Vec, + pub struct_tok: token::Struct, + pub ident: Ident, + pub fields: syn::FieldsNamed, +} + +#[derive(Debug)] +pub struct ItemImpl { + pub attrs: Vec, + pub impl_tok: token::Impl, + pub self_ty: Ident, + pub brace_tok: token::Brace, + pub items: Vec, +} + +#[derive(Debug)] +pub struct ItemImplMethod { + pub attrs: Vec, + pub vis: MethodVisibility, + pub sig: MethodSig, + pub block: syn::Block, +} + +#[derive(Debug, Clone)] +pub enum MethodVisibility { + External(ExternalVisibility), + Inherited, +} + +impl MethodVisibility { + /// Returns `true` if this is an external visibility. + /// + /// # Note + /// + /// The `pub(external)` visibility is only used for contract messages. + pub fn is_external(&self) -> bool { + match self { + MethodVisibility::External(_) => true, + _ => false, + } + } +} + +#[derive(Debug, Clone)] +pub struct ExternalVisibility { + pub pub_tok: token::Pub, + pub paren_tok: token::Paren, + pub external_tok: keywords::external, +} + +#[derive(Debug, Clone)] +pub struct MethodSig { + pub ident: Ident, + pub decl: FnDecl, +} + +#[derive(Debug, Clone)] +pub struct FnDecl { + pub fn_tok: token::Fn, + pub paren_tok: token::Paren, + pub inputs: Punctuated, + pub output: ReturnType, +} + +#[derive(Debug, Clone)] +pub enum FnArg { + SelfRef(syn::ArgSelfRef), + SelfValue(syn::ArgSelf), + Captured(syn::ArgCaptured), +} diff --git a/lang/src/errors.rs b/lang/src/errors.rs new file mode 100644 index 0000000000..fa5e6409e5 --- /dev/null +++ b/lang/src/errors.rs @@ -0,0 +1,61 @@ +use proc_macro2::TokenStream; +use quote::ToTokens; +use std::result::Result as StdResult; +pub use syn::parse::Error as SynError; + +macro_rules! bail { + ($($args:tt)*) => { + return Err(format_err!($($args)*).into()) + } +} + +macro_rules! format_err { + ($tokens:expr, $($msg:tt)*) => { + match &$tokens { + t => { + syn::parse::Error::new_spanned(t, format_args!($($msg)*)) + } + } + } +} + +/// A collection of errors. +/// +/// # Note +/// +/// This is used to allow for reporting multiple errors at the same time. +#[derive(Debug)] +pub struct Errors { + errors: Vec, +} + +impl From for Errors { + fn from(err: SynError) -> Errors { + Errors{ + errors: vec![err], + } + } +} + +impl From> for Errors { + fn from(err: Vec) -> Errors { + let result = err + .into_iter() + .flat_map(|v| v.errors) + .collect::>(); + assert!(result.len() > 0); + Errors{ errors: result } + } +} + +/// Used to create a TokenStream from a list of errors +impl ToTokens for Errors { + fn to_tokens(&self, tokens: &mut TokenStream) { + for item in self.errors.iter() { + item.to_compile_error().to_tokens(tokens); + } + } +} + +/// Result type alias for an error type which allows for accumulating errors. +pub type Result = StdResult; diff --git a/lang/src/hir.rs b/lang/src/hir.rs new file mode 100644 index 0000000000..20b1406b76 --- /dev/null +++ b/lang/src/hir.rs @@ -0,0 +1,245 @@ +use crate::{ + ast, + errors::{Result, SynError} +}; +use syn::token; +use proc_macro2::{Ident, Span}; + +/// A smart contract. +#[derive(Debug)] +pub struct Contract { + /// The name of the smart contract. + name: Ident, + /// The storage state fields. + state: State, + /// The deploy handler of the smart contract. + on_deploy: OnDeployHandler, + /// The messages of the smart contract. + messages: Vec, + /// Methods of the smart contract. + methods: Vec, +} + +impl Contract { + /// Extracts the contract state from the contract items + /// and performs some integrity checks on it. + fn extract_state(contract: &ast::Contract) -> Result<(&Ident, State)> { + let states = contract.states().collect::>(); + if states.is_empty() { + return Err( + SynError::new( + Span::call_site(), + "requires exactly one contract state `struct`; found none" + ).into() + ) + } + if states.len() > 1 { + return Err( + SynError::new( + Span::call_site(), + format!( + "requires exactly one contract state `struct`; found {:?}", + states.len() + ) + ).into() + ) + } + let state = states[0]; + Ok((&state.ident, State::from(state))) + } + + fn unpack_impl_blocks(contract_ident: &Ident, contract: &ast::Contract) + -> Result<(OnDeployHandler, Vec, Vec)> + { + let impl_blocks = contract.impl_blocks().collect::>(); + if impl_blocks.is_empty() { + return Err( + SynError::new( + Span::call_site(), + "requires at least one contract impl block `struct`; found none" + ).into() + ) + } + if impl_blocks.iter().any(|impl_block| impl_block.self_ty != *contract_ident) { + return Err( + SynError::new( + Span::call_site(), + format!( + "contract impl blocks must implement for the contract type: {}", + contract_ident + ) + ).into() + ) + } + use itertools::Itertools as _; + let (mut messages, methods): (Vec<_>, Vec<_>) = impl_blocks + .into_iter() + .flat_map(|impl_block| impl_block.items.iter()) + .partition_map(|msg_or_method| { + use either::Either; + if msg_or_method.vis.is_external() { + Either::Left(Message::from(msg_or_method)) + } else { + Either::Right(Method::from(msg_or_method)) + } + }); + let deploy_handler_idx = messages + .iter() + .position(|msg| msg.sig.ident == "on_deploy"); + let deploy_handler = match deploy_handler_idx { + Some(idx) => { + messages.swap_remove(idx).into() + } + None => { + return Err( + SynError::new( + Span::call_site(), + "could not find deploy handler (`on_deploy` message)" + ).into() + ) + } + }; + Ok((deploy_handler, messages, methods)) + } + + pub fn from_ast(contract: &ast::Contract) -> Result { + let (ident, state) = Self::extract_state(contract)?; + let (deploy_handler, messages, methods) = Self::unpack_impl_blocks(ident, contract)?; + Ok( + Self { + name: ident.clone(), + state, + on_deploy: deploy_handler, + messages, + methods, + } + ) + } +} + +#[derive(Debug)] +pub struct State { + /// The attributes. + /// + /// # Note + /// + /// Also used for documentation. + pub attrs: Vec, + /// The state fields. + /// + /// # Note + /// + /// These are the fields that are going to + /// be stored in the contract storage. + pub fields: syn::FieldsNamed, +} + +impl From<&ast::ItemState> for State { + fn from(state: &ast::ItemState) -> Self { + Self { + attrs: state.attrs.clone(), + fields: state.fields.clone(), + } + } +} + +/// The deploy handler of a smart contract. +/// +/// # Note +/// +/// This is what is getting called upon deploying a smart contract. +/// Normally this is used to initialize storage values. +#[derive(Debug)] +pub struct OnDeployHandler { + /// The attributes. + /// + /// # Note + /// + /// Also used for documentation. + pub attrs: Vec, + /// The function declaration. + pub decl: ast::FnDecl, + /// The actual implementation. + pub block: syn::Block, +} + +impl From for OnDeployHandler { + fn from(msg: Message) -> Self { + Self { + attrs: msg.attrs, + decl: msg.sig.decl, + block: msg.block, + } + } +} + +/// A message that is handled by the smart contract. +/// +/// # Note +/// +/// Messages of a smart contract are only callable externally. +/// They are used to communicate with other smart contracts. +#[derive(Debug)] +pub struct Message { + /// The attributes. + /// + /// # Note + /// + /// Also used for documentation. + pub attrs: Vec, + /// The message signature. + /// + /// # Note + /// + /// This also holds the name of the message. + pub sig: ast::MethodSig, + /// The actual implementation. + pub block: syn::Block, +} + +impl From<&ast::ItemImplMethod> for Message { + fn from(impl_method: &ast::ItemImplMethod) -> Self { + Self { + attrs: impl_method.attrs.clone(), + sig: impl_method.sig.clone(), + block: impl_method.block.clone(), + } + } +} + +/// A method defined on the smart contract. +#[derive(Debug)] +pub struct Method { + /// The attributes. + /// + /// # Note + /// + /// Also used for documentation. + pub attrs: Vec, + /// The method visibility. + /// + /// # Note + /// + /// Currently only inherent visibility (private) is + /// available for methods. + pub vis: ast::MethodVisibility, + /// The method signature. + /// + /// # Note + /// + /// This also holds the name of the method. + pub sig: ast::MethodSig, + /// The actual implementation. + pub block: syn::Block, +} + +impl From<&ast::ItemImplMethod> for Method { + fn from(impl_method: &ast::ItemImplMethod) -> Self { + Self { + attrs: impl_method.attrs.clone(), + sig: impl_method.sig.clone(), + vis: impl_method.vis.clone(), + block: impl_method.block.clone(), + } + } +} diff --git a/lang/src/ident_ext.rs b/lang/src/ident_ext.rs new file mode 100644 index 0000000000..f36a2da061 --- /dev/null +++ b/lang/src/ident_ext.rs @@ -0,0 +1,17 @@ +use proc_macro2::{Ident, Span}; +use std::fmt::Display; + +/// Utilities for operating on `Ident` instances. +pub trait IdentExt: Display { + /// Creates a string out of the ident's name. + fn to_owned_string(&self) -> String { + format!("{}", self) + } + + /// Creates a new Ident from the given `str`. + fn from_str>(s: T) -> Ident { + Ident::new(s.as_ref(), Span::call_site()) + } +} + +impl IdentExt for Ident {} diff --git a/lang/src/lib.rs b/lang/src/lib.rs new file mode 100644 index 0000000000..29083a1209 --- /dev/null +++ b/lang/src/lib.rs @@ -0,0 +1,35 @@ +#![recursion_limit = "128"] + +extern crate proc_macro; + +use quote::{ + // quote, + ToTokens, +}; + +#[proc_macro] +pub fn contract(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + match contract_gen_inner(input) { + Ok(tokens) => tokens, + Err(err) => { err.into_token_stream().into() }, + } +} + +#[macro_use] +mod errors; + +mod ast; +mod hir; +mod parser; +mod ident_ext; + +use errors::Result; + +fn contract_gen_inner(input: proc_macro::TokenStream) -> Result { + let ast_contract = parser::parse_contract(input.clone())?; + let _hir_contract = hir::Contract::from_ast(&ast_contract)?; + // gen::gir::generate(&hir_program)?; + // let tokens = gen::codegen(&hir_program); + // Ok(tokens.into()) + Ok(proc_macro::TokenStream::new()) +} diff --git a/lang/src/parser.rs b/lang/src/parser.rs new file mode 100644 index 0000000000..5c3736628a --- /dev/null +++ b/lang/src/parser.rs @@ -0,0 +1,213 @@ +use crate::proc_macro; + +// use proc_macro2::TokenStream; +use syn::{ + self, + Token, + parse::{ + Parse, + ParseStream, + Result, + }, +}; +use crate::{ + ast, +}; + +pub mod keywords { + use syn::custom_keyword; + + custom_keyword!(external); + custom_keyword!(env); +} + +pub fn parse_contract(token_stream: proc_macro::TokenStream) -> Result { + syn::parse(token_stream).map_err(|e| e.into()) +} + +impl Parse for ast::Contract { + fn parse(input: ParseStream<'_>) -> Result { + Ok(ast::Contract { + items: ast::Item::parse_outer(input)?, + }) + } +} + +impl ast::Item { + fn parse_outer(input: ParseStream<'_>) -> Result> { + let mut res = Vec::new(); + while !input.is_empty() { + res.push(input.parse()?); + } + Ok(res) + } +} + +impl Parse for ast::Item { + fn parse(input: ParseStream<'_>) -> Result { + let attrs = syn::Attribute::parse_outer(input)?; + let lookahead = input.lookahead1(); + if lookahead.peek(Token![struct]) { + input.parse() + .map(|mut state: ast::ItemState| { + state.attrs = attrs; + ast::Item::State(state) + }) + } else if lookahead.peek(Token![impl]) { + input.parse() + .map(|mut block: ast::ItemImpl| { + block.attrs = attrs; + ast::Item::Impl(block) + }) + } else { + Err(lookahead.error()) + } + } +} + +impl Parse for ast::ItemState { + fn parse(input: ParseStream<'_>) -> Result { + let struct_tok = input.parse()?; + let ident = input.parse()?; + let fields = input.parse()?; + Ok(Self { + attrs: vec![], + struct_tok, + ident, + fields, + }) + } +} + +impl Parse for ast::ItemImpl { + fn parse(input: ParseStream<'_>) -> Result { + let impl_tok = input.parse()?; + let self_ty = input.parse()?; + let (brace_tok, inner_attrs, items) = { + let content; + let brace_tok = syn::braced!(content in input); + let inner_attrs = content.call(syn::Attribute::parse_inner)?; + + let mut items = Vec::new(); + while !content.is_empty() { + items.push(content.parse()?); + } + (brace_tok, inner_attrs, items) + }; + Ok(Self { + attrs: inner_attrs, + impl_tok, + self_ty, + brace_tok, + items, + }) + } +} + +impl Parse for ast::ItemImplMethod { + fn parse(input: ParseStream<'_>) -> Result { + let attrs = syn::Attribute::parse_outer(input)?; + let vis = input.parse()?; + let fn_tok = input.parse()?; + let ident = input.parse()?; + let (paren_tok, inputs) = { + let content; + let paren_tok = syn::parenthesized!(content in input); + let inputs = content.parse_terminated(ast::FnArg::parse)?; + (paren_tok, inputs) + }; + let output = input.parse()?; + let block = input.parse()?; + + Ok(Self { + attrs, + vis, + sig: ast::MethodSig{ + ident, + decl: ast::FnDecl{ + fn_tok, + paren_tok, + inputs, + output, + } + }, + block, + }) + } +} + +impl Parse for ast::MethodVisibility { + fn parse(input: ParseStream<'_>) -> Result { + if input.peek(Token![pub]) { + Ok(ast::MethodVisibility::External(input.parse()?)) + } else { + Ok(ast::MethodVisibility::Inherited) + } + } +} + +impl Parse for ast::ExternalVisibility { + fn parse(input: ParseStream<'_>) -> Result { + let pub_tok = input.parse::()?; + let mut content; + let paren_tok = syn::parenthesized!(content in input); + let external_tok = content.parse()?; + Ok(ast::ExternalVisibility { + pub_tok, + paren_tok, + external_tok, + }) + } +} + +impl Parse for ast::FnArg { + fn parse(input: ParseStream<'_>) -> Result { + if input.peek(Token![&]) { + let ahead = input.fork(); + if ahead.call(ast::FnArg::arg_self_ref).is_ok() && !ahead.peek(Token![:]) { + return input.call(ast::FnArg::arg_self_ref).map(ast::FnArg::SelfRef); + } + } + + if input.peek(Token![mut]) || input.peek(Token![self]) { + let ahead = input.fork(); + if ahead.call(ast::FnArg::arg_self).is_ok() && !ahead.peek(Token![:]) { + return input.call(ast::FnArg::arg_self).map(ast::FnArg::SelfValue); + } + } + + let ahead = input.fork(); + let err = match ahead.call(ast::FnArg::arg_captured) { + Ok(_) => return input.call(ast::FnArg::arg_captured).map(ast::FnArg::Captured), + Err(err) => err, + }; + + Err(err) + } +} + +impl ast::FnArg { + fn arg_self_ref(input: ParseStream) -> Result { + Ok(syn::ArgSelfRef { + and_token: input.parse()?, + lifetime: input.parse()?, + mutability: input.parse()?, + self_token: input.parse()?, + }) + } + + fn arg_self(input: ParseStream) -> Result { + Ok(syn::ArgSelf { + mutability: input.parse()?, + self_token: input.parse()?, + }) + } + + fn arg_captured(input: ParseStream) -> Result { + Ok(syn::ArgCaptured { + pat: input.parse()?, + colon_token: input.parse()?, + ty: input.parse()?, + }) + } +} diff --git a/lang/tests/simple.rs b/lang/tests/simple.rs new file mode 100644 index 0000000000..951b64daf9 --- /dev/null +++ b/lang/tests/simple.rs @@ -0,0 +1,37 @@ +#[macro_use] +extern crate pdsl_lang; + +use pdsl_core::{ + // storage, // Not in use since `contract!` currently generates no code. +}; + +contract! { + /// A simple contract that has a value that can be + /// incremented, returned and compared. + struct Incrementer { + /// The internal value. + value: storage::Value, + } + + impl Incrementer { + /// Automatically called when the contract is deployed. + pub(external) fn on_deploy(&mut self, init_value: u32) { + self.value = init_value; + } + + /// Increments the internal counter. + pub(external) fn inc(&mut self, by: u32) { + self.value += by + } + + /// Returns the internal counter. + pub(external) fn get(&self) -> Balance { + *self.value + } + + /// Returns `true` if `x` is greater than the internal value. + pub(external) fn compare(&self, x: u32) -> bool { + x > *self.value + } + } +} diff --git a/lang/tests/simple.rs.gen b/lang/tests/simple.rs.gen new file mode 100644 index 0000000000..666545d13c --- /dev/null +++ b/lang/tests/simple.rs.gen @@ -0,0 +1,51 @@ +use pdsl_core::storage; +use pdsl_model::{ + messages, + state, + Contract, + ContractDecl, + EnvHandler, +}; + +state! { + /// A simple contract that has a value that can be + /// incremented, returned and compared. + struct Incrementer { + /// The internal value. + value: storage::Value + } +} + +messages! { + 0 => Inc(by: u32); + 1 => Get() -> u32; + 2 => Compare(with: u32) -> bool; +} + +#[rustfmt::skip] +fn instantiate() -> impl Contract { + ContractDecl::using::() + .on_deploy(|env, init_val| { + self.val.set(init_val) + }) + .on_msg_mut::(|env, by| { + self.value += by + }) + .on_msg::(|env, _| { + *self.value + }) + .on_msg::(|env, x| { + x > *self.value + }) + .instantiate() +} + +#[no_mangle] +fn deploy() { + instantiate().deploy() +} + +#[no_mangle] +fn call() { + instantiate().dispatch() +}