From b6f414fb480e392cde6e7534d72902ba284a5957 Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Tue, 12 Jul 2022 08:57:01 +0000 Subject: [PATCH] refactor(evm-coder)!: manual macro parsing We have plans to allow evm-coder to generate Callables for externally defined contracts, however our current attribute parsing was limiting code maintanability, rework was needed before starting to implement new features BREAKING CHANGE: solidity_interface definitions may now look slightly different, for example interface names now should be identifiers, not strings --- --- a/crates/evm-coder-macros/Cargo.toml +++ b/crates/evm-coder-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "evm-coder-macros" -version = "0.1.0" +version = "0.2.0" license = "GPLv3" edition = "2021" @@ -8,10 +8,9 @@ proc-macro = true [dependencies] -sha3 = "0.9.1" +sha3 = "0.10.1" quote = "1.0" proc-macro2 = "1.0" syn = { version = "1.0", features = ["full"] } hex = "0.4.3" Inflector = "0.11.4" -darling = "0.13.0" --- a/crates/evm-coder-macros/src/lib.rs +++ b/crates/evm-coder-macros/src/lib.rs @@ -16,14 +16,13 @@ #![allow(dead_code)] -use darling::FromMeta; use inflector::cases; use proc_macro::TokenStream; use quote::quote; use sha3::{Digest, Keccak256}; use syn::{ - AttributeArgs, DeriveInput, GenericArgument, Ident, ItemImpl, Pat, Path, PathArguments, - PathSegment, Type, parse_macro_input, spanned::Spanned, + DeriveInput, GenericArgument, Ident, ItemImpl, Pat, Path, PathArguments, + PathSegment, Type, parse_macro_input, spanned::Spanned, Attribute, parse::Parse, }; mod solidity_interface; @@ -254,8 +253,7 @@ /// ``` #[proc_macro_attribute] pub fn solidity_interface(args: TokenStream, stream: TokenStream) -> TokenStream { - let args = parse_macro_input!(args as AttributeArgs); - let args = solidity_interface::InterfaceInfo::from_list(&args).unwrap(); + let args = parse_macro_input!(args as solidity_interface::InterfaceInfo); let input: ItemImpl = match syn::parse(stream) { Ok(t) => t, --- a/crates/evm-coder-macros/src/solidity_interface.rs +++ b/crates/evm-coder-macros/src/solidity_interface.rs @@ -16,14 +16,15 @@ #![allow(dead_code)] -use quote::quote; -use darling::{FromMeta, ToTokens}; +use quote::{quote, ToTokens}; use inflector::cases; use std::fmt::Write; use syn::{ Expr, FnArg, GenericArgument, Generics, Ident, ImplItem, ImplItemMethod, ItemImpl, Lit, Meta, - MetaNameValue, NestedMeta, PatType, Path, PathArguments, ReturnType, Type, spanned::Spanned, - parse_str, + MetaNameValue, PatType, PathArguments, ReturnType, Type, + spanned::Spanned, + parse::{Parse, ParseStream}, + parenthesized, Token, LitInt, LitStr, }; use crate::{ @@ -39,19 +40,6 @@ via: Option<(Type, Ident)>, } impl Is { - fn new_via(path: &Path, via: Option<(Type, Ident)>) -> syn::Result { - let name = parse_ident_from_path(path, false)?.clone(); - Ok(Self { - pascal_call_name: pascal_ident_to_call(&name), - snake_call_name: pascal_ident_to_snake_call(&name), - name, - via, - }) - } - fn new(path: &Path) -> syn::Result { - Self::new_via(path, None) - } - fn expand_call_def(&self, gen_ref: &proc_macro2::TokenStream) -> proc_macro2::TokenStream { let name = &self.name; let pascal_call_name = &self.pascal_call_name; @@ -137,73 +125,141 @@ #[derive(Default)] struct IsList(Vec); -impl FromMeta for IsList { - fn from_list(items: &[NestedMeta]) -> darling::Result { - let mut out = Vec::new(); - for item in items { - match item { - NestedMeta::Meta(Meta::Path(path)) => out.push(Is::new(path)?), - // TODO: replace meta parsing with manual - NestedMeta::Meta(Meta::List(list)) - if list.path.is_ident("via") && list.nested.len() == 3 => - { - let mut data = list.nested.iter(); - let typ = match data.next().expect("len == 3") { - NestedMeta::Lit(Lit::Str(s)) => { - let v = s.value(); - let typ: Type = parse_str(&v)?; - typ - } - _ => { - return Err(syn::Error::new( - item.span(), - "via typ should be type in string", - ) - .into()) - } - }; - let via = match data.next().expect("len == 3") { - NestedMeta::Meta(Meta::Path(path)) => path - .get_ident() - .ok_or_else(|| syn::Error::new(item.span(), "via should be ident"))?, - _ => return Err(syn::Error::new(item.span(), "via should be ident").into()), - }; - let path = match data.next().expect("len == 3") { - NestedMeta::Meta(Meta::Path(path)) => path, - _ => return Err(syn::Error::new(item.span(), "path should be path").into()), - }; - - out.push(Is::new_via(path, Some((typ, via.clone())))?) - } - _ => { - return Err(syn::Error::new( - item.span(), - "expected either Name or via(\"Type\", getter, Name)", - ) - .into()) - } +impl Parse for IsList { + fn parse(input: ParseStream) -> syn::Result { + let mut out = vec![]; + loop { + if input.is_empty() { + break; + } + let name = input.parse::()?; + let lookahead = input.lookahead1(); + let via = if lookahead.peek(syn::token::Paren) { + let contents; + parenthesized!(contents in input); + let method = contents.parse::()?; + contents.parse::()?; + let ty = contents.parse::()?; + Some((ty, method)) + } else if lookahead.peek(Token![,]) { + None + } else if input.is_empty() { + None + } else { + return Err(lookahead.error()); + }; + out.push(Is { + pascal_call_name: pascal_ident_to_call(&name), + snake_call_name: pascal_ident_to_snake_call(&name), + name, + via, + }); + if input.peek(Token![,]) { + input.parse::()?; + continue; + } else { + break; } } Ok(Self(out)) } } -#[derive(FromMeta)] pub struct InterfaceInfo { name: Ident, - #[darling(default)] is: IsList, - #[darling(default)] inline_is: IsList, - #[darling(default)] events: IsList, + expect_selector: Option, } +impl Parse for InterfaceInfo { + fn parse(input: ParseStream) -> syn::Result { + let mut name = None; + let mut is = None; + let mut inline_is = None; + let mut events = None; + let mut expect_selector = None; + // TODO: create proc-macro to optimize proc-macro boilerplate? :D + loop { + let lookahead = input.lookahead1(); + if lookahead.peek(kw::name) { + let k = input.parse::()?; + input.parse::()?; + if name.replace(input.parse::()?).is_some() { + return Err(syn::Error::new(k.span(), "name is already set")); + } + } else if lookahead.peek(kw::is) { + let k = input.parse::()?; + let contents; + parenthesized!(contents in input); + if is.replace(contents.parse::()?).is_some() { + return Err(syn::Error::new(k.span(), "is is already set")); + } + } else if lookahead.peek(kw::inline_is) { + let k = input.parse::()?; + let contents; + parenthesized!(contents in input); + if inline_is.replace(contents.parse::()?).is_some() { + return Err(syn::Error::new(k.span(), "inline_is is already set")); + } + } else if lookahead.peek(kw::events) { + let k = input.parse::()?; + let contents; + parenthesized!(contents in input); + if events.replace(contents.parse::()?).is_some() { + return Err(syn::Error::new(k.span(), "events is already set")); + } + } else if lookahead.peek(kw::expect_selector) { + let k = input.parse::()?; + input.parse::()?; + let value = input.parse::()?; + if expect_selector + .replace(value.base10_parse::()?) + .is_some() + { + return Err(syn::Error::new(k.span(), "expect_selector is already set")); + } + } else if input.is_empty() { + break; + } else { + return Err(lookahead.error()); + } + if input.peek(Token![,]) { + input.parse::()?; + } else { + break; + } + } + Ok(Self { + name: name.ok_or_else(|| syn::Error::new(input.span(), "missing name"))?, + is: is.unwrap_or_default(), + inline_is: inline_is.unwrap_or_default(), + events: events.unwrap_or_default(), + expect_selector, + }) + } +} -#[derive(FromMeta)] struct MethodInfo { - #[darling(default)] rename_selector: Option, } +impl Parse for MethodInfo { + fn parse(input: ParseStream) -> syn::Result { + let mut rename_selector = None; + let lookahead = input.lookahead1(); + if lookahead.peek(kw::rename_selector) { + let k = input.parse::()?; + input.parse::()?; + if rename_selector + .replace(input.parse::()?.value()) + .is_some() + { + return Err(syn::Error::new(k.span(), "rename_selector is already set")); + } + } + Ok(Self { rename_selector }) + } +} enum AbiType { // type @@ -258,7 +314,7 @@ "expected only one generic for vec", )); } - let arg = args.first().unwrap(); + let arg = args.first().expect("first arg"); let ty = match arg { GenericArgument::Type(ty) => ty, @@ -429,23 +485,17 @@ Pure, } -pub struct WeightAttr(syn::Expr); - -mod keyword { +mod kw { syn::custom_keyword!(weight); -} -impl syn::parse::Parse for WeightAttr { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - input.parse::()?; - let content; - syn::bracketed!(content in input); - content.parse::()?; + syn::custom_keyword!(via); + syn::custom_keyword!(name); + syn::custom_keyword!(is); + syn::custom_keyword!(inline_is); + syn::custom_keyword!(events); + syn::custom_keyword!(expect_selector); - let weight_content; - syn::parenthesized!(weight_content in content); - Ok(WeightAttr(weight_content.parse::()?)) - } + syn::custom_keyword!(rename_selector); } struct Method { @@ -472,8 +522,7 @@ for attr in &value.attrs { let ident = parse_ident_from_path(&attr.path, false)?; if ident == "solidity" { - let args = attr.parse_meta().unwrap(); - info = MethodInfo::from_meta(&args).unwrap(); + info = attr.parse_args::()?; } else if ident == "doc" { let args = attr.parse_meta().unwrap(); let value = match args { @@ -484,7 +533,7 @@ }; docs.push(value); } else if ident == "weight" { - weight = Some(syn::parse2::(attr.to_token_stream())?.0); + weight = Some(attr.parse_args::()?); } } let ident = &value.sig.ident; @@ -869,6 +918,28 @@ .map(|is| Is::expand_generator(is, &gen_ref)); let solidity_event_generators = self.info.events.0.iter().map(Is::expand_event_generator); + if let Some(expect_selector) = &self.info.expect_selector { + if !self.info.inline_is.0.is_empty() { + return syn::Error::new( + name.span(), + "expect_selector is not compatible with inline_is", + ) + .to_compile_error(); + } + let selector = self + .methods + .iter() + .map(|m| m.selector) + .fold(0, |a, b| a ^ b); + + if *expect_selector != selector { + let mut methods = String::new(); + for meth in self.methods.iter() { + write!(methods, "\n- {}", meth.selector_str).expect("write to string"); + } + return syn::Error::new(name.span(), format!("expected selector mismatch, expected {expect_selector:0>8x}, but implementation has {selector:0>8x}{methods}")).to_compile_error(); + } + } // let methods = self.methods.iter().map(Method::solidity_def); quote! { @@ -917,9 +988,9 @@ )*), }; if is_impl { - tc.collect("// Common stubs holder\ncontract Dummy {\n\tuint8 dummy;\n\tstring stub_error = \"this contract is implemented in native\";\n}\ncontract ERC165 is Dummy {\n\tfunction supportsInterface(bytes4 interfaceID) external view returns (bool) {\n\t\trequire(false, stub_error);\n\t\tinterfaceID;\n\t\treturn true;\n\t}\n}\n".into()); + tc.collect("/// @dev common stubs holder\ncontract Dummy {\n\tuint8 dummy;\n\tstring stub_error = \"this contract is implemented in native\";\n}\ncontract ERC165 is Dummy {\n\tfunction supportsInterface(bytes4 interfaceID) external view returns (bool) {\n\t\trequire(false, stub_error);\n\t\tinterfaceID;\n\t\treturn true;\n\t}\n}\n".into()); } else { - tc.collect("// Common stubs holder\ninterface Dummy {\n}\ninterface ERC165 is Dummy {\n\tfunction supportsInterface(bytes4 interfaceID) external view returns (bool);\n}\n".into()); + tc.collect("/// @dev common stubs holder\ninterface Dummy {\n}\ninterface ERC165 is Dummy {\n\tfunction supportsInterface(bytes4 interfaceID) external view returns (bool);\n}\n".into()); } #( #solidity_generators @@ -930,9 +1001,9 @@ let mut out = string::new(); // In solidity interface usage (is) should be preceeded by interface definition - // This comment helps to sort it in a set + // HACK: this comment helps to sort it in a set if #solidity_name.starts_with("Inline") { - out.push_str("// Inline\n"); + out.push_str("/// @dev inlined interface\n"); } let _ = interface.format(is_impl, &mut out, tc); tc.collect(out); --- a/crates/evm-coder-macros/src/to_log.rs +++ b/crates/evm-coder-macros/src/to_log.rs @@ -207,7 +207,7 @@ )*), }; let mut out = string::new(); - out.push_str("// Inline\n"); + out.push_str("/// @dev inlined interface\n"); let _ = interface.format(is_impl, &mut out, tc); tc.collect(out); } --- a/crates/evm-coder/src/solidity.rs +++ b/crates/evm-coder/src/solidity.rs @@ -56,7 +56,7 @@ } let id = self.next_id(); let mut str = String::new(); - writeln!(str, "// Anonymous struct").unwrap(); + writeln!(str, "/// @dev anonymous struct").unwrap(); writeln!(str, "struct Tuple{} {{", id).unwrap(); for (i, name) in names.iter().enumerate() { writeln!(str, "\t{} field_{};", name, i).unwrap(); @@ -416,12 +416,12 @@ tc: &TypeCollector, ) -> fmt::Result { for doc in self.docs { - writeln!(writer, "\t//{}", doc)?; + writeln!(writer, "\t///{}", doc)?; } if !self.docs.is_empty() { - writeln!(writer, "\t//")?; + writeln!(writer, "\t///")?; } - writeln!(writer, "\t// Selector: {}", self.selector)?; + writeln!(writer, "\t/// Selector: {}", self.selector)?; write!(writer, "\tfunction {}(", self.name)?; self.args.solidity_name(writer, tc)?; write!(writer, ")")?; @@ -498,7 +498,7 @@ if self.selector != ZERO_BYTES { writeln!( out, - "// Selector: {:0>8x}", + "/// @dev the ERC-165 identifier for this interface is 0x{:0>8x}", u32::from_be_bytes(self.selector) )?; } --- a/crates/evm-coder/tests/generics.rs +++ b/crates/evm-coder/tests/generics.rs @@ -19,14 +19,14 @@ struct Generic(PhantomData); -#[solidity_interface(name = "GenericIs")] +#[solidity_interface(name = GenericIs)] impl Generic { fn test_1(&self) -> Result { unreachable!() } } -#[solidity_interface(name = "Generic", is(GenericIs))] +#[solidity_interface(name = Generic, is(GenericIs))] impl> Generic { fn test_2(&self) -> Result { unreachable!() @@ -35,7 +35,7 @@ generate_stubgen!(gen_iface, GenericCall<()>, false); -#[solidity_interface(name = "GenericWhere")] +#[solidity_interface(name = GenericWhere)] impl Generic where T: core::fmt::Debug, --- a/crates/evm-coder/tests/random.rs +++ b/crates/evm-coder/tests/random.rs @@ -21,14 +21,14 @@ struct Impls; -#[solidity_interface(name = "OurInterface")] +#[solidity_interface(name = OurInterface)] impl Impls { fn fn_a(&self, _input: uint256) -> Result { unreachable!() } } -#[solidity_interface(name = "OurInterface1")] +#[solidity_interface(name = OurInterface1)] impl Impls { fn fn_b(&self, _input: uint128) -> Result { unreachable!() @@ -48,7 +48,7 @@ } #[solidity_interface( - name = "OurInterface2", + name = OurInterface2, is(OurInterface), inline_is(OurInterface1), events(OurEvents) @@ -79,3 +79,9 @@ unreachable!() } } + +#[solidity_interface( + name = ValidSelector, + expect_selector = 0x00000000, +)] +impl Impls {} --- a/crates/evm-coder/tests/solidity_generation.rs +++ b/crates/evm-coder/tests/solidity_generation.rs @@ -18,7 +18,7 @@ struct ERC20; -#[solidity_interface(name = "ERC20")] +#[solidity_interface(name = ERC20)] impl ERC20 { fn decimals(&self) -> Result { unreachable!() -- gitstuff