Repository: zhiburt/tabled
Pull request #39 (2021-11-27 23:45:47)

tabled/ Add `LENGTH` constant to `Tabled` trait
The constant will increase the performance of `enum` table building.


Patch:
diff --git a/src/lib.rs b/src/lib.rs
index 03f0824..4f29e77 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -173,6 +173,10 @@ pub use tabled_derive::Tabled;
 /// Self::headers().len() == self.fields().len()
 /// ```
 pub trait Tabled {
+    /// A length of fields and headers,
+    /// which must be the same.
+    const LENGTH: usize;
+
     /// Fields method must return a list of cells.
     ///
     /// The cells will be placed in the same row, preserving the order.
@@ -185,6 +189,8 @@ impl<T> Tabled for &T
 where
     T: Tabled,
 {
+    const LENGTH: usize = T::LENGTH;
+
     fn fields(&self) -> Vec<String> {
         T::fields(self)
     }
@@ -357,6 +363,8 @@ fn build_grid<T: Tabled>(iter: impl IntoIterator<Item = T>) -> Grid {
 macro_rules! tuple_table {
     ( $($name:ident)+ ) => {
         impl<$($name: Tabled),+> Tabled for ($($name,)+){
+            const LENGTH: usize = $($name::LENGTH+)+ 0;
+
             fn fields(&self) -> Vec<String> {
                 #![allow(non_snake_case)]
                 let ($($name,)+) = self;
@@ -384,6 +392,8 @@ tuple_table! { A B C D E F }
 macro_rules! default_table {
     ( $t:ty ) => {
         impl Tabled for $t {
+            const LENGTH: usize = 1;
+
             fn fields(&self) -> Vec<String> {
                 vec![format!("{}", self)]
             }
@@ -420,6 +430,8 @@ default_table!(f32);
 default_table!(f64);
 
 impl<T: fmt::Display, const N: usize> Tabled for [T; N] {
+    const LENGTH: usize = N;
+
     fn fields(&self) -> Vec<String> {
         self.iter().map(|e| e.to_string()).collect()
     }
diff --git a/tabled_derive/src/lib.rs b/tabled_derive/src/lib.rs
index 8bcbe51..55ac58e 100644
--- a/tabled_derive/src/lib.rs
+++ b/tabled_derive/src/lib.rs
@@ -16,6 +16,7 @@ pub fn tabled(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
 }
 
 fn impl_tabled(ast: &DeriveInput) -> TokenStream {
+    let length = get_tabled_length(ast).unwrap();
     let info = collect_info(ast).unwrap();
     let fields = info.values;
     let headers = info.headers;
@@ -24,6 +25,8 @@ fn impl_tabled(ast: &DeriveInput) -> TokenStream {
     let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
     let expanded = quote! {
         impl #impl_generics Tabled for #name #ty_generics #where_clause {
+            const LENGTH: usize = #length;
+
             fn fields(&self) -> Vec<String> {
                 #fields
             }
@@ -37,6 +40,67 @@ fn impl_tabled(ast: &DeriveInput) -> TokenStream {
     expanded
 }
 
+fn get_tabled_length(ast: &DeriveInput) -> Result<TokenStream, String> {
+    match &ast.data {
+        Data::Struct(data) => Ok(get_fields_length(&data.fields)),
+        Data::Enum(data) => Ok(get_enum_length(data)),
+        Data::Union(_) => Err("Union type isn't supported".to_owned()),
+    }
+}
+
+fn get_fields_length(fields: &Fields) -> TokenStream {
+    let size_compontents = fields
+        .iter()
+        .map(|field| {
+            let attributes = Attributes::parse(&field.attrs);
+            (field, attributes)
+        })
+        .filter(|(_, attr)| !attr.is_ignored())
+        .map(|(field, attr)| {
+            if !attr.inline {
+                quote!({ 1 })
+            } else {
+                let field_type = &field.ty;
+                quote!({<#field_type as Tabled>::LENGTH})
+            }
+        });
+
+    let size_compontents = std::iter::once(quote!(0)).chain(size_compontents);
+
+    let mut stream = TokenStream::new();
+    stream.append_separated(size_compontents, syn::token::Add::default());
+
+    stream
+}
+
+fn get_enum_length(enum_ast: &DataEnum) -> TokenStream {
+    let variant_sizes = get_enum_variant_length(enum_ast);
+    let variant_sizes = std::iter::once(quote!(0)).chain(variant_sizes);
+
+    let mut stream = TokenStream::new();
+    stream.append_separated(variant_sizes, syn::token::Add::default());
+
+    stream
+}
+
+fn get_enum_variant_length(enum_ast: &DataEnum) -> impl Iterator<Item = TokenStream> + '_ {
+    enum_ast
+        .variants
+        .iter()
+        .map(|variant| {
+            let attributes = Attributes::parse(&variant.attrs);
+            (variant, attributes)
+        })
+        .filter(|(_, attr)| !attr.is_ignored())
+        .map(|(variant, attr)| {
+            if !attr.inline {
+                quote!(1)
+            } else {
+                get_fields_length(&variant.fields)
+            }
+        })
+}
+
 fn collect_info(ast: &DeriveInput) -> Result<Impl, String> {
     match &ast.data {
         Data::Struct(data) => collect_info_struct(data),
@@ -114,6 +178,7 @@ fn field_headers(
 }
 
 fn collect_info_enum(ast: &DataEnum) -> Result<Impl, String> {
+    let mut headers_list = Vec::new();
     let mut variants = Vec::new();
     for variant in &ast.variants {
         let attributes = Attributes::parse(&variant.attrs);
@@ -122,10 +187,13 @@ fn collect_info_enum(ast: &DataEnum) -> Result<Impl, String> {
         }
 
         let info = info_from_variant(variant, &attributes)?;
-        variants.push((variant, info));
+        variants.push((variant, info.values));
+        headers_list.push(info.headers);
     }
 
-    let headers_list = variants.iter().map(|(_, i)| &i.headers).collect::<Vec<_>>();
+    let variant_sizes = get_enum_variant_length(ast);
+    let values = values_for_enum(variant_sizes, variants);
+
     let headers = quote! {
         vec![
             #(#headers_list,)*
@@ -133,8 +201,6 @@ fn collect_info_enum(ast: &DataEnum) -> Result<Impl, String> {
         .concat()
     };
 
-    let values = values_for_enum(variants);
-
     Ok(Impl { headers, values })
 }
 
@@ -216,17 +282,15 @@ fn variant_var_name(index: usize, field: &Field) -> TokenStream {
     }
 }
 
-fn values_for_enum(variants: Vec<(&Variant, Impl)>) -> TokenStream {
+fn values_for_enum(
+    variant_sizes: impl Iterator<Item = TokenStream>,
+    variants: Vec<(&Variant, TokenStream)>,
+) -> TokenStream {
     let branches = variants.iter().map(|(variant, _)| match_variant(variant));
 
-    let headers = variants
-        .iter()
-        .map(|(_, info)| &info.headers)
-        .collect::<Vec<_>>();
-
     let fields = variants
         .iter()
-        .map(|(_, info)| &info.values)
+        .map(|(_, values)| values)
         .collect::<Vec<_>>();
 
     let mut stream = TokenStream::new();
@@ -253,21 +317,13 @@ fn values_for_enum(variants: Vec<(&Variant, Impl)>) -> TokenStream {
         //
         // It's a bit strange trick but I haven't found any better
         // how to calculate a size and offset.
-        let headers: Vec<Vec<String>> = vec![#(#headers,)*];
-        let lengths = headers.iter().map(|values| values.len()).collect::<Vec<_>>();
-        let size = lengths.iter().sum::<usize>();
-        let offsets: Vec<usize> = lengths.iter().fold(Vec::new(), |mut acc, len| {
-            // offset of 1 element is 0
-            if acc.is_empty() {
-                acc.push(0);
-            }
-
-            let privious_len: usize = acc.last().map(|l| *l).unwrap_or(0);
-            acc.push(privious_len + len);
-            acc
-        });
+        let mut offsets: &mut [usize] = &mut [0, #(#variant_sizes,)*];
+        for i in 1 .. offsets.len() {
+            offsets[i] += offsets[i-1]
+        }
 
-        let mut out_vec: Vec<String> = std::iter::repeat(String::new()).take(size).collect();
+        let size = <Self as Tabled>::LENGTH;
+        let mut out_vec: Vec<String> = vec![String::new(); size];
 
         #[allow(unused_variables)]
         match &self {


Test patch:
diff --git a/tests/derive_test.rs b/tests/derive_test.rs
index 37d4926..9ca9688 100644
--- a/tests/derive_test.rs
+++ b/tests/derive_test.rs
@@ -27,6 +27,7 @@ mod tupple_structure {
 
         assert_eq!(vec!["123".to_owned()], st.fields());
         assert_eq!(vec!["2".to_owned()], St::headers());
+        assert_eq!(St::LENGTH, 1);
     }
 
     #[allow(dead_code)]
@@ -83,6 +84,7 @@ mod tupple_structure {
 
         assert_eq!(vec!["0".to_owned(), "v2".to_owned()], st.fields());
         assert_eq!(vec!["0".to_owned(), "1".to_owned()], St::headers());
+        assert_eq!(St::LENGTH, 2);
     }
 
     #[test]
@@ -93,6 +95,7 @@ mod tupple_structure {
 
         assert!(st.fields().is_empty());
         assert!(St::headers().is_empty());
+        assert_eq!(St::LENGTH, 0);
     }
 
     #[test]
@@ -103,6 +106,7 @@ mod tupple_structure {
 
         assert_eq!(vec!["0".to_owned()], St::headers());
         assert_eq!(vec!["1".to_owned()], st.fields());
+        assert_eq!(St::LENGTH, 1);
     }
 
     #[test]
@@ -119,6 +123,8 @@ mod tupple_structure {
 
         assert_eq!(vec!["0".to_owned()], headers);
         assert_eq!(vec!["1".to_owned()], fields);
+
+        assert_eq!(St::<String>::LENGTH, 1);
     }
 }
 
@@ -150,6 +156,7 @@ mod enum_ {
             vec!["+".to_owned(), "".to_owned(), "".to_owned(), "".to_owned(),],
             Domain::Security.fields()
         );
+        assert_eq!(Domain::LENGTH, 4);
     }
 
     #[test]
@@ -178,6 +185,7 @@ mod enum_ {
             vec!["".to_owned(), "+".to_owned(), "".to_owned()],
             E::B(String::new()).fields()
         );
+        assert_eq!(E::LENGTH, 3);
     }
 
     #[test]
@@ -226,6 +234,7 @@ mod enum_ {
         );
         assert_eq!(vec!["".to_owned(), "+".to_owned()], E::K.fields());
         assert!(E::B(String::new()).fields().is_empty());
+        assert_eq!(E::LENGTH, 2);
     }
 
     #[test]
@@ -306,6 +315,8 @@ mod enum_ {
             )
             .fields()
         );
+
+        assert_eq!(Vehicle::LENGTH, 6);
     }
 
     #[test]
@@ -374,6 +385,7 @@ mod unit {
 
         assert!(st.fields().is_empty());
         assert!(St::headers().is_empty());
+        assert_eq!(St::LENGTH, 0);
     }
 }
 
@@ -391,6 +403,7 @@ mod structure {
         let st = St { f1: 0, f2: "v2" };
         assert_eq!(vec!["0".to_owned(), "v2".to_owned()], st.fields());
         assert_eq!(vec!["f1".to_owned(), "f2".to_owned()], St::headers());
+        assert_eq!(St::LENGTH, 2);
     }
 
     #[test]
@@ -401,6 +414,7 @@ mod structure {
         let st = St {};
         assert!(st.fields().is_empty());
         assert!(St::headers().is_empty());
+        assert_eq!(St::LENGTH, 0);
     }
 
     #[test]
@@ -441,6 +455,7 @@ mod structure {
 
         assert_eq!(vec!["123".to_owned()], st.fields());
         assert_eq!(vec!["f3".to_owned()], St::headers());
+        assert_eq!(St::LENGTH, 1);
     }
 
     #[test]
@@ -487,6 +502,8 @@ mod structure {
             ],
             Person::headers()
         );
+
+        assert_eq!(Person::LENGTH, 4);
     }
 
     #[test]
diff --git a/tests/expanded_display_test.rs b/tests/expanded_display_test.rs
index d993906..99a2b28 100644
--- a/tests/expanded_display_test.rs
+++ b/tests/expanded_display_test.rs
@@ -74,6 +74,8 @@ fn display_empty() {
     struct Type;
 
     impl Tabled for Type {
+        const LENGTH: usize = 0;
+
         fn fields(&self) -> Vec<String> {
             Vec::new()
         }
@@ -94,6 +96,8 @@ fn display_dynamic_header_template() {
         struct Type;
 
         impl Tabled for Type {
+            const LENGTH: usize = 3;
+
             fn fields(&self) -> Vec<String> {
                 vec!["He".to_string(), "123".to_string(), "asd".to_string()]
             }
@@ -113,6 +117,8 @@ fn display_dynamic_header_template() {
         struct Type;
 
         impl Tabled for Type {
+            const LENGTH: usize = 3;
+
             fn fields(&self) -> Vec<String> {
                 vec!["He".to_string(), "123".to_string(), "asd".to_string()]
             }
@@ -137,6 +143,8 @@ fn display_dynamic_header_template() {
         struct Type;
 
         impl Tabled for Type {
+            const LENGTH: usize = 3;
+
             fn fields(&self) -> Vec<String> {
                 vec!["HeheHehe".to_string(), "123".to_string(), "asd".to_string()]
             }
@@ -161,6 +169,8 @@ fn display_dynamic_header_template() {
         struct Type;
 
         impl Tabled for Type {
+            const LENGTH: usize = 3;
+
             fn fields(&self) -> Vec<String> {
                 vec!["He".to_string(), "123".to_string(), "asd".to_string()]
             }
@@ -185,6 +195,8 @@ fn display_dynamic_header_template() {
         struct Type;
 
         impl Tabled for Type {
+            const LENGTH: usize = 3;
+
             fn fields(&self) -> Vec<String> {
                 vec!["He".to_string(), "123".to_string(), "asd".to_string()]
             }
@@ -213,6 +225,8 @@ fn display_dynamic_header_template() {
         struct Type;
 
         impl Tabled for Type {
+            const LENGTH: usize = 3;
+
             fn fields(&self) -> Vec<String> {
                 vec!["He".to_string(), "123".to_string(), "asd".to_string()]
             }
@@ -242,6 +256,8 @@ fn display_dynamic_header_template() {
         struct Type;
 
         impl Tabled for Type {
+            const LENGTH: usize = 1;
+
             fn fields(&self) -> Vec<String> {
                 vec!["22".to_string()]
             }
@@ -287,6 +303,8 @@ fn display_multiline_field() {
     struct St;
 
     impl Tabled for St {
+        const LENGTH: usize = 3;
+
         fn fields(&self) -> Vec<String> {
             vec!["1".to_string(), "2".to_string(), "3".to_string()]
         }
diff --git a/tests/util.rs b/tests/util.rs
index 3487f42..9da1e4e 100644
--- a/tests/util.rs
+++ b/tests/util.rs
@@ -30,6 +30,8 @@ impl<const N: usize> IndexMut<usize> for Obj<N> {
 }
 
 impl<const N: usize> Tabled for Obj<N> {
+    const LENGTH: usize = N;
+
     fn fields(&self) -> Vec<String> {
         self.data.clone()
     }