From 17a1997d1864756bcaa22856265340fa4bdaab5b Mon Sep 17 00:00:00 2001 From: Peter Goodspeed-Niklaus <coriolinus@users.noreply.github.com> Date: Fri, 7 May 2021 10:18:09 +0200 Subject: [PATCH] `#[derive(MaxEncodedLen)]` (#8737) * impl #[derive(MaxEncodedLen)] for structs * impl #[derive(MaxEncodedLen)] for enums, unions * break long comments onto multiple lines * add doc for public item * add examples to macro documentation * move MaxEncodedLen macro docs, un-ignore doc-tests --- substrate/frame/support/procedural/src/lib.rs | 7 + .../support/procedural/src/max_encoded_len.rs | 133 ++++++++++++++++ substrate/frame/support/src/traits.rs | 28 ++++ .../support/test/tests/max_encoded_len.rs | 149 ++++++++++++++++++ .../support/test/tests/max_encoded_len_ui.rs | 26 +++ .../tests/max_encoded_len_ui/not_encode.rs | 6 + .../max_encoded_len_ui/not_encode.stderr | 13 ++ .../test/tests/max_encoded_len_ui/not_mel.rs | 14 ++ .../tests/max_encoded_len_ui/not_mel.stderr | 21 +++ .../test/tests/max_encoded_len_ui/union.rs | 10 ++ .../tests/max_encoded_len_ui/union.stderr | 11 ++ .../max_encoded_len_ui/unsupported_variant.rs | 12 ++ .../unsupported_variant.stderr | 12 ++ 13 files changed, 442 insertions(+) create mode 100644 substrate/frame/support/procedural/src/max_encoded_len.rs create mode 100644 substrate/frame/support/test/tests/max_encoded_len.rs create mode 100644 substrate/frame/support/test/tests/max_encoded_len_ui.rs create mode 100644 substrate/frame/support/test/tests/max_encoded_len_ui/not_encode.rs create mode 100644 substrate/frame/support/test/tests/max_encoded_len_ui/not_encode.stderr create mode 100644 substrate/frame/support/test/tests/max_encoded_len_ui/not_mel.rs create mode 100644 substrate/frame/support/test/tests/max_encoded_len_ui/not_mel.stderr create mode 100644 substrate/frame/support/test/tests/max_encoded_len_ui/union.rs create mode 100644 substrate/frame/support/test/tests/max_encoded_len_ui/union.stderr create mode 100644 substrate/frame/support/test/tests/max_encoded_len_ui/unsupported_variant.rs create mode 100644 substrate/frame/support/test/tests/max_encoded_len_ui/unsupported_variant.stderr diff --git a/substrate/frame/support/procedural/src/lib.rs b/substrate/frame/support/procedural/src/lib.rs index 4cedf798821..069339a9794 100644 --- a/substrate/frame/support/procedural/src/lib.rs +++ b/substrate/frame/support/procedural/src/lib.rs @@ -28,6 +28,7 @@ mod debug_no_bound; mod clone_no_bound; mod partial_eq_no_bound; mod default_no_bound; +mod max_encoded_len; pub(crate) use storage::INHERENT_INSTANCE_NAME; use proc_macro::TokenStream; @@ -432,3 +433,9 @@ pub fn crate_to_pallet_version(input: TokenStream) -> TokenStream { /// The number of module instances supported by the runtime, starting at index 1, /// and up to `NUMBER_OF_INSTANCE`. pub(crate) const NUMBER_OF_INSTANCE: u8 = 16; + +/// Derive `MaxEncodedLen`. +#[proc_macro_derive(MaxEncodedLen)] +pub fn derive_max_encoded_len(input: TokenStream) -> TokenStream { + max_encoded_len::derive_max_encoded_len(input) +} diff --git a/substrate/frame/support/procedural/src/max_encoded_len.rs b/substrate/frame/support/procedural/src/max_encoded_len.rs new file mode 100644 index 00000000000..72efa446b3f --- /dev/null +++ b/substrate/frame/support/procedural/src/max_encoded_len.rs @@ -0,0 +1,133 @@ +// This file is part of Substrate. + +// Copyright (C) 2021 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use frame_support_procedural_tools::generate_crate_access_2018; +use quote::{quote, quote_spanned}; +use syn::{ + Data, DeriveInput, Fields, GenericParam, Generics, TraitBound, Type, TypeParamBound, + parse_quote, spanned::Spanned, +}; + +/// impl for `#[derive(MaxEncodedLen)]` +pub fn derive_max_encoded_len(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input: DeriveInput = match syn::parse(input) { + Ok(input) => input, + Err(e) => return e.to_compile_error().into(), + }; + + let mel_trait = match max_encoded_len_trait() { + Ok(mel_trait) => mel_trait, + Err(e) => return e.to_compile_error().into(), + }; + + let name = &input.ident; + let generics = add_trait_bounds(input.generics, mel_trait.clone()); + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let data_expr = data_length_expr(&input.data); + + quote::quote!( + const _: () = { + impl #impl_generics #mel_trait for #name #ty_generics #where_clause { + fn max_encoded_len() -> usize { + #data_expr + } + } + }; + ) + .into() +} + +fn max_encoded_len_trait() -> syn::Result<TraitBound> { + let frame_support = generate_crate_access_2018("frame-support")?; + Ok(parse_quote!(#frame_support::traits::MaxEncodedLen)) +} + +// Add a bound `T: MaxEncodedLen` to every type parameter T. +fn add_trait_bounds(mut generics: Generics, mel_trait: TraitBound) -> Generics { + for param in &mut generics.params { + if let GenericParam::Type(ref mut type_param) = *param { + type_param.bounds.push(TypeParamBound::Trait(mel_trait.clone())); + } + } + generics +} + +/// generate an expression to sum up the max encoded length from several fields +fn fields_length_expr(fields: &Fields) -> proc_macro2::TokenStream { + let type_iter: Box<dyn Iterator<Item = &Type>> = match fields { + Fields::Named(ref fields) => Box::new(fields.named.iter().map(|field| &field.ty)), + Fields::Unnamed(ref fields) => Box::new(fields.unnamed.iter().map(|field| &field.ty)), + Fields::Unit => Box::new(std::iter::empty()), + }; + // expands to an expression like + // + // 0 + // .saturating_add(<type of first field>::max_encoded_len()) + // .saturating_add(<type of second field>::max_encoded_len()) + // + // We match the span of each field to the span of the corresponding + // `max_encoded_len` call. This way, if one field's type doesn't implement + // `MaxEncodedLen`, the compiler's error message will underline which field + // caused the issue. + let expansion = type_iter.map(|ty| { + quote_spanned! { + ty.span() => .saturating_add(<#ty>::max_encoded_len()) + } + }); + quote! { + 0_usize #( #expansion )* + } +} + +// generate an expression to sum up the max encoded length of each field +fn data_length_expr(data: &Data) -> proc_macro2::TokenStream { + match *data { + Data::Struct(ref data) => fields_length_expr(&data.fields), + Data::Enum(ref data) => { + // We need an expression expanded for each variant like + // + // 0 + // .max(<variant expression>) + // .max(<variant expression>) + // .saturating_add(1) + // + // The 1 derives from the discriminant; see + // https://github.com/paritytech/parity-scale-codec/ + // blob/f0341dabb01aa9ff0548558abb6dcc5c31c669a1/derive/src/encode.rs#L211-L216 + // + // Each variant expression's sum is computed the way an equivalent struct's would be. + + let expansion = data.variants.iter().map(|variant| { + let variant_expression = fields_length_expr(&variant.fields); + quote! { + .max(#variant_expression) + } + }); + + quote! { + 0_usize #( #expansion )* .saturating_add(1) + } + } + Data::Union(ref data) => { + // https://github.com/paritytech/parity-scale-codec/ + // blob/f0341dabb01aa9ff0548558abb6dcc5c31c669a1/derive/src/encode.rs#L290-L293 + syn::Error::new(data.union_token.span(), "Union types are not supported") + .to_compile_error() + } + } +} diff --git a/substrate/frame/support/src/traits.rs b/substrate/frame/support/src/traits.rs index d15356c1e1b..2d7fb3db736 100644 --- a/substrate/frame/support/src/traits.rs +++ b/substrate/frame/support/src/traits.rs @@ -82,4 +82,32 @@ mod voting; pub use voting::{CurrencyToVote, SaturatingCurrencyToVote, U128CurrencyToVote}; mod max_encoded_len; +// This looks like an overlapping import/export, but it isn't: +// macros and traits live in distinct namespaces. pub use max_encoded_len::MaxEncodedLen; +/// Derive [`MaxEncodedLen`][max_encoded_len::MaxEncodedLen]. +/// +/// # Examples +/// +/// ``` +/// # use codec::Encode; +/// # use frame_support::traits::MaxEncodedLen; +/// #[derive(Encode, MaxEncodedLen)] +/// struct TupleStruct(u8, u32); +/// +/// assert_eq!(TupleStruct::max_encoded_len(), u8::max_encoded_len() + u32::max_encoded_len()); +/// ``` +/// +/// ``` +/// # use codec::Encode; +/// # use frame_support::traits::MaxEncodedLen; +/// #[derive(Encode, MaxEncodedLen)] +/// enum GenericEnum<T> { +/// A, +/// B(T), +/// } +/// +/// assert_eq!(GenericEnum::<u8>::max_encoded_len(), u8::max_encoded_len() + u8::max_encoded_len()); +/// assert_eq!(GenericEnum::<u128>::max_encoded_len(), u8::max_encoded_len() + u128::max_encoded_len()); +/// ``` +pub use frame_support_procedural::MaxEncodedLen; diff --git a/substrate/frame/support/test/tests/max_encoded_len.rs b/substrate/frame/support/test/tests/max_encoded_len.rs new file mode 100644 index 00000000000..e9e74929108 --- /dev/null +++ b/substrate/frame/support/test/tests/max_encoded_len.rs @@ -0,0 +1,149 @@ +// This file is part of Substrate. + +// Copyright (C) 2020-2021 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Tests for MaxEncodedLen derive macro + +use frame_support::traits::MaxEncodedLen; +use codec::{Compact, Encode}; + +// These structs won't even compile if the macro isn't working right. + +#[derive(Encode, MaxEncodedLen)] +struct Primitives { + bool: bool, + eight: u8, +} + +#[test] +fn primitives_max_length() { + assert_eq!(Primitives::max_encoded_len(), 2); +} + +#[derive(Encode, MaxEncodedLen)] +struct Composites { + fixed_size_array: [u8; 128], + tuple: (u128, u128), +} + +#[test] +fn composites_max_length() { + assert_eq!(Composites::max_encoded_len(), 128 + 16 + 16); +} + +#[derive(Encode, MaxEncodedLen)] +struct Generic<T> { + one: T, + two: T, +} + +#[test] +fn generic_max_length() { + assert_eq!(Generic::<u8>::max_encoded_len(), u8::max_encoded_len() * 2); + assert_eq!(Generic::<u32>::max_encoded_len(), u32::max_encoded_len() * 2); +} + +#[derive(Encode, MaxEncodedLen)] +struct TwoGenerics<T, U> { + t: T, + u: U, +} + +#[test] +fn two_generics_max_length() { + assert_eq!( + TwoGenerics::<u8, u16>::max_encoded_len(), + u8::max_encoded_len() + u16::max_encoded_len() + ); + assert_eq!( + TwoGenerics::<Compact<u64>, [u16; 8]>::max_encoded_len(), + Compact::<u64>::max_encoded_len() + <[u16; 8]>::max_encoded_len() + ); +} + +#[derive(Encode, MaxEncodedLen)] +struct UnitStruct; + +#[test] +fn unit_struct_max_length() { + assert_eq!(UnitStruct::max_encoded_len(), 0); +} + +#[derive(Encode, MaxEncodedLen)] +struct TupleStruct(u8, u32); + +#[test] +fn tuple_struct_max_length() { + assert_eq!(TupleStruct::max_encoded_len(), u8::max_encoded_len() + u32::max_encoded_len()); +} + +#[derive(Encode, MaxEncodedLen)] +struct TupleGeneric<T>(T, T); + +#[test] +fn tuple_generic_max_length() { + assert_eq!(TupleGeneric::<u8>::max_encoded_len(), u8::max_encoded_len() * 2); + assert_eq!(TupleGeneric::<u32>::max_encoded_len(), u32::max_encoded_len() * 2); +} + +#[derive(Encode, MaxEncodedLen)] +#[allow(unused)] +enum UnitEnum { + A, + B, +} + +#[test] +fn unit_enum_max_length() { + assert_eq!(UnitEnum::max_encoded_len(), 1); +} + +#[derive(Encode, MaxEncodedLen)] +#[allow(unused)] +enum TupleEnum { + A(u32), + B, +} + +#[test] +fn tuple_enum_max_length() { + assert_eq!(TupleEnum::max_encoded_len(), 1 + u32::max_encoded_len()); +} + +#[derive(Encode, MaxEncodedLen)] +#[allow(unused)] +enum StructEnum { + A { sixty_four: u64, one_twenty_eight: u128 }, + B, +} + +#[test] +fn struct_enum_max_length() { + assert_eq!(StructEnum::max_encoded_len(), 1 + u64::max_encoded_len() + u128::max_encoded_len()); +} + +// ensure that enums take the max of variant length, not the sum +#[derive(Encode, MaxEncodedLen)] +#[allow(unused)] +enum EnumMaxNotSum { + A(u32), + B(u32), +} + +#[test] +fn enum_max_not_sum_max_length() { + assert_eq!(EnumMaxNotSum::max_encoded_len(), 1 + u32::max_encoded_len()); +} diff --git a/substrate/frame/support/test/tests/max_encoded_len_ui.rs b/substrate/frame/support/test/tests/max_encoded_len_ui.rs new file mode 100644 index 00000000000..c5c0489da92 --- /dev/null +++ b/substrate/frame/support/test/tests/max_encoded_len_ui.rs @@ -0,0 +1,26 @@ +// This file is part of Substrate. + +// Copyright (C) 2020-2021 Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#[rustversion::attr(not(stable), ignore)] +#[test] +fn derive_no_bound_ui() { + // As trybuild is using `cargo check`, we don't need the real WASM binaries. + std::env::set_var("SKIP_WASM_BUILD", "1"); + + let t = trybuild::TestCases::new(); + t.compile_fail("tests/max_encoded_len_ui/*.rs"); +} diff --git a/substrate/frame/support/test/tests/max_encoded_len_ui/not_encode.rs b/substrate/frame/support/test/tests/max_encoded_len_ui/not_encode.rs new file mode 100644 index 00000000000..ed6fe94471e --- /dev/null +++ b/substrate/frame/support/test/tests/max_encoded_len_ui/not_encode.rs @@ -0,0 +1,6 @@ +use frame_support::traits::MaxEncodedLen; + +#[derive(MaxEncodedLen)] +struct NotEncode; + +fn main() {} diff --git a/substrate/frame/support/test/tests/max_encoded_len_ui/not_encode.stderr b/substrate/frame/support/test/tests/max_encoded_len_ui/not_encode.stderr new file mode 100644 index 00000000000..f4dbeac0408 --- /dev/null +++ b/substrate/frame/support/test/tests/max_encoded_len_ui/not_encode.stderr @@ -0,0 +1,13 @@ +error[E0277]: the trait bound `NotEncode: WrapperTypeEncode` is not satisfied + --> $DIR/not_encode.rs:3:10 + | +3 | #[derive(MaxEncodedLen)] + | ^^^^^^^^^^^^^ the trait `WrapperTypeEncode` is not implemented for `NotEncode` + | + ::: $WORKSPACE/frame/support/src/traits/max_encoded_len.rs + | + | pub trait MaxEncodedLen: Encode { + | ------ required by this bound in `MaxEncodedLen` + | + = note: required because of the requirements on the impl of `frame_support::dispatch::Encode` for `NotEncode` + = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/substrate/frame/support/test/tests/max_encoded_len_ui/not_mel.rs b/substrate/frame/support/test/tests/max_encoded_len_ui/not_mel.rs new file mode 100644 index 00000000000..6116f30e527 --- /dev/null +++ b/substrate/frame/support/test/tests/max_encoded_len_ui/not_mel.rs @@ -0,0 +1,14 @@ +use codec::Encode; +use frame_support::traits::MaxEncodedLen; + +#[derive(Encode)] +struct NotMel; + +#[derive(Encode, MaxEncodedLen)] +struct Generic<T> { + t: T, +} + +fn main() { + let _ = Generic::<NotMel>::max_encoded_len(); +} diff --git a/substrate/frame/support/test/tests/max_encoded_len_ui/not_mel.stderr b/substrate/frame/support/test/tests/max_encoded_len_ui/not_mel.stderr new file mode 100644 index 00000000000..0aabd4b2a39 --- /dev/null +++ b/substrate/frame/support/test/tests/max_encoded_len_ui/not_mel.stderr @@ -0,0 +1,21 @@ +error[E0599]: the function or associated item `max_encoded_len` exists for struct `Generic<NotMel>`, but its trait bounds were not satisfied + --> $DIR/not_mel.rs:13:29 + | +5 | struct NotMel; + | -------------- doesn't satisfy `NotMel: MaxEncodedLen` +... +8 | struct Generic<T> { + | ----------------- + | | + | function or associated item `max_encoded_len` not found for this + | doesn't satisfy `Generic<NotMel>: MaxEncodedLen` +... +13 | let _ = Generic::<NotMel>::max_encoded_len(); + | ^^^^^^^^^^^^^^^ function or associated item cannot be called on `Generic<NotMel>` due to unsatisfied trait bounds + | + = note: the following trait bounds were not satisfied: + `NotMel: MaxEncodedLen` + which is required by `Generic<NotMel>: MaxEncodedLen` + = help: items from traits can only be used if the trait is implemented and in scope + = note: the following trait defines an item `max_encoded_len`, perhaps you need to implement it: + candidate #1: `MaxEncodedLen` diff --git a/substrate/frame/support/test/tests/max_encoded_len_ui/union.rs b/substrate/frame/support/test/tests/max_encoded_len_ui/union.rs new file mode 100644 index 00000000000..c685b6939e9 --- /dev/null +++ b/substrate/frame/support/test/tests/max_encoded_len_ui/union.rs @@ -0,0 +1,10 @@ +use codec::Encode; +use frame_support::traits::MaxEncodedLen; + +#[derive(Encode, MaxEncodedLen)] +union Union { + a: u8, + b: u16, +} + +fn main() {} diff --git a/substrate/frame/support/test/tests/max_encoded_len_ui/union.stderr b/substrate/frame/support/test/tests/max_encoded_len_ui/union.stderr new file mode 100644 index 00000000000..bc5519d674d --- /dev/null +++ b/substrate/frame/support/test/tests/max_encoded_len_ui/union.stderr @@ -0,0 +1,11 @@ +error: Union types are not supported + --> $DIR/union.rs:5:1 + | +5 | union Union { + | ^^^^^ + +error: Union types are not supported. + --> $DIR/union.rs:5:1 + | +5 | union Union { + | ^^^^^ diff --git a/substrate/frame/support/test/tests/max_encoded_len_ui/unsupported_variant.rs b/substrate/frame/support/test/tests/max_encoded_len_ui/unsupported_variant.rs new file mode 100644 index 00000000000..675f62c168a --- /dev/null +++ b/substrate/frame/support/test/tests/max_encoded_len_ui/unsupported_variant.rs @@ -0,0 +1,12 @@ +use codec::Encode; +use frame_support::traits::MaxEncodedLen; + +#[derive(Encode)] +struct NotMel; + +#[derive(Encode, MaxEncodedLen)] +enum UnsupportedVariant { + NotMel(NotMel), +} + +fn main() {} diff --git a/substrate/frame/support/test/tests/max_encoded_len_ui/unsupported_variant.stderr b/substrate/frame/support/test/tests/max_encoded_len_ui/unsupported_variant.stderr new file mode 100644 index 00000000000..aa10b5e4cc1 --- /dev/null +++ b/substrate/frame/support/test/tests/max_encoded_len_ui/unsupported_variant.stderr @@ -0,0 +1,12 @@ +error[E0599]: no function or associated item named `max_encoded_len` found for struct `NotMel` in the current scope + --> $DIR/unsupported_variant.rs:9:9 + | +5 | struct NotMel; + | -------------- function or associated item `max_encoded_len` not found for this +... +9 | NotMel(NotMel), + | ^^^^^^ function or associated item not found in `NotMel` + | + = help: items from traits can only be used if the trait is implemented and in scope + = note: the following trait defines an item `max_encoded_len`, perhaps you need to implement it: + candidate #1: `MaxEncodedLen` -- GitLab