From 778fb3f76164fc7b8199e8c6edd9f085ba73438d Mon Sep 17 00:00:00 2001 From: Soares Chen Date: Thu, 4 Jul 2024 12:49:55 +0800 Subject: [PATCH] Add new `cgp-field` crate for deriving `HasField` field accessors (#19) * Add boilerplate for new cgp-field crates * Add HasField trait * Add symbol! macro * Implement derive_fields macro * Add test for symbol * Test derive HasField * Re-export cgp-field in cgp-core * Reformat imports * Make derive_field a derive macro * Auto derive HasField for contexts that implement Deref * Fix clippy * Fix test --- Cargo.lock | 35 ++++++-- Cargo.toml | 6 ++ crates/cgp-component-macro/Cargo.toml | 7 +- crates/cgp-core/Cargo.toml | 1 + crates/cgp-core/src/lib.rs | 5 +- crates/cgp-core/src/prelude.rs | 1 + crates/cgp-field-macro-lib/Cargo.toml | 23 +++++ crates/cgp-field-macro-lib/src/field.rs | 58 +++++++++++++ crates/cgp-field-macro-lib/src/lib.rs | 8 ++ crates/cgp-field-macro-lib/src/symbol.rs | 23 +++++ crates/cgp-field-macro-lib/src/tests/field.rs | 87 +++++++++++++++++++ .../src/tests/helper/equal.rs | 7 ++ .../src/tests/helper/format.rs | 7 ++ .../src/tests/helper/mod.rs | 2 + crates/cgp-field-macro-lib/src/tests/mod.rs | 3 + .../cgp-field-macro-lib/src/tests/symbol.rs | 31 +++++++ crates/cgp-field-macro/Cargo.toml | 23 +++++ crates/cgp-field-macro/src/lib.rs | 13 +++ crates/cgp-field/Cargo.toml | 19 ++++ crates/cgp-field/src/lib.rs | 8 ++ crates/cgp-field/src/traits.rs | 20 +++++ crates/cgp-field/src/types.rs | 1 + 22 files changed, 375 insertions(+), 13 deletions(-) create mode 100644 crates/cgp-field-macro-lib/Cargo.toml create mode 100644 crates/cgp-field-macro-lib/src/field.rs create mode 100644 crates/cgp-field-macro-lib/src/lib.rs create mode 100644 crates/cgp-field-macro-lib/src/symbol.rs create mode 100644 crates/cgp-field-macro-lib/src/tests/field.rs create mode 100644 crates/cgp-field-macro-lib/src/tests/helper/equal.rs create mode 100644 crates/cgp-field-macro-lib/src/tests/helper/format.rs create mode 100644 crates/cgp-field-macro-lib/src/tests/helper/mod.rs create mode 100644 crates/cgp-field-macro-lib/src/tests/mod.rs create mode 100644 crates/cgp-field-macro-lib/src/tests/symbol.rs create mode 100644 crates/cgp-field-macro/Cargo.toml create mode 100644 crates/cgp-field-macro/src/lib.rs create mode 100644 crates/cgp-field/Cargo.toml create mode 100644 crates/cgp-field/src/lib.rs create mode 100644 crates/cgp-field/src/traits.rs create mode 100644 crates/cgp-field/src/types.rs diff --git a/Cargo.lock b/Cargo.lock index edb8e8e..5273a13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,11 +31,7 @@ name = "cgp-component-macro" version = "0.1.0" dependencies = [ "cgp-component-macro-lib", - "itertools", - "prettyplease", "proc-macro2", - "quote", - "syn", ] [[package]] @@ -56,6 +52,7 @@ dependencies = [ "cgp-async", "cgp-component", "cgp-error", + "cgp-field", "cgp-inner", "cgp-run", ] @@ -83,6 +80,32 @@ dependencies = [ "cgp-core", ] +[[package]] +name = "cgp-field" +version = "0.1.0" +dependencies = [ + "cgp-field-macro", +] + +[[package]] +name = "cgp-field-macro" +version = "0.1.0" +dependencies = [ + "cgp-field-macro-lib", + "proc-macro2", +] + +[[package]] +name = "cgp-field-macro-lib" +version = "0.1.0" +dependencies = [ + "itertools", + "prettyplease", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "cgp-inner" version = "0.1.0" @@ -109,9 +132,9 @@ dependencies = [ [[package]] name = "either" -version = "1.9.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "eyre" diff --git a/Cargo.toml b/Cargo.toml index 7800333..ec19578 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,9 @@ members = [ "crates/cgp-component", "crates/cgp-component-macro", "crates/cgp-component-macro-lib", + "crates/cgp-field", + "crates/cgp-field-macro", + "crates/cgp-field-macro-lib", "crates/cgp-error", "crates/cgp-error-eyre", "crates/cgp-error-std", @@ -32,6 +35,9 @@ cgp-sync = { path = "./crates/cgp-sync" } cgp-component = { path = "./crates/cgp-component" } cgp-component-macro = { path = "./crates/cgp-component-macro" } cgp-component-macro-lib = { path = "./crates/cgp-component-macro-lib" } +cgp-field = { path = "./crates/cgp-field" } +cgp-field-macro = { path = "./crates/cgp-field-macro" } +cgp-field-macro-lib = { path = "./crates/cgp-field-macro-lib" } cgp-error = { path = "./crates/cgp-error" } cgp-run = { path = "./crates/cgp-run" } cgp-inner = { path = "./crates/cgp-inner" } diff --git a/crates/cgp-component-macro/Cargo.toml b/crates/cgp-component-macro/Cargo.toml index f366d15..05694ab 100644 --- a/crates/cgp-component-macro/Cargo.toml +++ b/crates/cgp-component-macro/Cargo.toml @@ -20,9 +20,4 @@ proc-macro = true [dependencies] cgp-component-macro-lib = { version = "0.1.0" } - -syn = { version = "2.0.37", features = [ "full" ] } -quote = "1.0.33" -proc-macro2 = "1.0.67" -itertools = "0.11.0" -prettyplease = "0.2.20" +proc-macro2 = "1.0.67" \ No newline at end of file diff --git a/crates/cgp-core/Cargo.toml b/crates/cgp-core/Cargo.toml index 3ff9bce..7c36c52 100644 --- a/crates/cgp-core/Cargo.toml +++ b/crates/cgp-core/Cargo.toml @@ -19,5 +19,6 @@ all-features = true cgp-async = { version = "0.1.0" } cgp-component = { version = "0.1.0" } cgp-error = { version = "0.1.0" } +cgp-field = { version = "0.1.0" } cgp-run = { version = "0.1.0" } cgp-inner = { version = "0.1.0" } \ No newline at end of file diff --git a/crates/cgp-core/src/lib.rs b/crates/cgp-core/src/lib.rs index ff1478c..cd4fa23 100644 --- a/crates/cgp-core/src/lib.rs +++ b/crates/cgp-core/src/lib.rs @@ -3,4 +3,7 @@ pub mod prelude; pub use cgp_async::{async_trait, Async}; -pub use {cgp_component as component, cgp_error as error, cgp_inner as inner, cgp_run as run}; +pub use { + cgp_component as component, cgp_error as error, cgp_field as field, cgp_inner as inner, + cgp_run as run, +}; diff --git a/crates/cgp-core/src/prelude.rs b/crates/cgp-core/src/prelude.rs index a7b3620..f08cbd6 100644 --- a/crates/cgp-core/src/prelude.rs +++ b/crates/cgp-core/src/prelude.rs @@ -3,3 +3,4 @@ pub use cgp_component::{ define_components, delegate_components, derive_component, DelegateComponent, HasComponents, }; pub use cgp_error::{CanRaiseError, HasErrorType}; +pub use cgp_field::{symbol, Char, HasField}; diff --git a/crates/cgp-field-macro-lib/Cargo.toml b/crates/cgp-field-macro-lib/Cargo.toml new file mode 100644 index 0000000..e0fc317 --- /dev/null +++ b/crates/cgp-field-macro-lib/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "cgp-field-macro-lib" +version = "0.1.0" +edition = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +authors = { workspace = true } +rust-version = { workspace = true } +readme = "README.md" +keywords = ["context-generic programming"] +description = """ + Context-generic programming core macros +""" + +[package.metadata.docs.rs] +all-features = true + +[dependencies] +syn = { version = "2.0.37", features = [ "full" ] } +quote = "1.0.33" +proc-macro2 = "1.0.67" +itertools = "0.11.0" +prettyplease = "0.2.20" diff --git a/crates/cgp-field-macro-lib/src/field.rs b/crates/cgp-field-macro-lib/src/field.rs new file mode 100644 index 0000000..eac91b2 --- /dev/null +++ b/crates/cgp-field-macro-lib/src/field.rs @@ -0,0 +1,58 @@ +use proc_macro2::TokenStream; +use quote::ToTokens; +use syn::{parse_quote, Fields, ItemImpl, ItemStruct}; + +use crate::symbol::symbol_from_string; + +pub fn derive_has_field_impls(item_struct: &ItemStruct) -> Vec { + let struct_ident = &item_struct.ident; + + let (impl_generics, ty_generics, where_clause) = item_struct.generics.split_for_impl(); + + let mut item_impls = Vec::new(); + + if let Fields::Named(fields) = &item_struct.fields { + for field in fields.named.iter() { + let field_ident = field.ident.as_ref().unwrap(); + + let field_symbol = symbol_from_string(&field_ident.to_string()); + + let field_type = &field.ty; + + let item_impl: ItemImpl = parse_quote! { + impl #impl_generics HasField< #field_symbol > + for #struct_ident #ty_generics + #where_clause + { + type Field = #field_type; + + fn get_field( + &self, + key: ::core::marker::PhantomData< #field_symbol >, + ) -> &Self::Field + { + &self. #field_ident + } + } + }; + + item_impls.push(item_impl); + } + } + + item_impls +} + +pub fn derive_fields(input: TokenStream) -> TokenStream { + let item_struct: ItemStruct = syn::parse2(input).unwrap(); + + let item_impls = derive_has_field_impls(&item_struct); + + let mut output = TokenStream::new(); + + for item_impl in item_impls { + output.extend(item_impl.to_token_stream()); + } + + output +} diff --git a/crates/cgp-field-macro-lib/src/lib.rs b/crates/cgp-field-macro-lib/src/lib.rs new file mode 100644 index 0000000..1439e39 --- /dev/null +++ b/crates/cgp-field-macro-lib/src/lib.rs @@ -0,0 +1,8 @@ +pub mod field; +pub mod symbol; + +#[cfg(test)] +mod tests; + +pub use field::derive_fields; +pub use symbol::make_symbol; diff --git a/crates/cgp-field-macro-lib/src/symbol.rs b/crates/cgp-field-macro-lib/src/symbol.rs new file mode 100644 index 0000000..37fbb21 --- /dev/null +++ b/crates/cgp-field-macro-lib/src/symbol.rs @@ -0,0 +1,23 @@ +use proc_macro2::TokenStream; +use quote::ToTokens; +use syn::punctuated::Punctuated; +use syn::token::Comma; +use syn::{parse_quote, LitStr, Type}; + +pub fn symbol_from_string(value: &str) -> Type { + let char_types = >::from_iter( + value + .chars() + .map(|c: char| -> Type { parse_quote!( Char< #c > ) }), + ); + + parse_quote!( ( #char_types ) ) +} + +pub fn make_symbol(input: TokenStream) -> TokenStream { + let literal: LitStr = syn::parse2(input).unwrap(); + + let symbol = symbol_from_string(&literal.value()); + + symbol.to_token_stream() +} diff --git a/crates/cgp-field-macro-lib/src/tests/field.rs b/crates/cgp-field-macro-lib/src/tests/field.rs new file mode 100644 index 0000000..72ad46c --- /dev/null +++ b/crates/cgp-field-macro-lib/src/tests/field.rs @@ -0,0 +1,87 @@ +use quote::quote; + +use crate::field::derive_fields; +use crate::tests::helper::equal::equal_token_stream; + +#[test] +fn test_basic_derive_fields() { + let derived = derive_fields(quote! { + pub struct Foo { + pub bar: Bar, + pub baz: Baz, + } + }); + + let expected = quote! { + impl HasField<(Char<'b'>, Char<'a'>, Char<'r'>)> for Foo { + type Field = Bar; + + fn get_field( + &self, + key: ::core::marker::PhantomData<(Char<'b'>, Char<'a'>, Char<'r'>)>, + ) -> &Self::Field { + &self.bar + } + } + + impl HasField<(Char<'b'>, Char<'a'>, Char<'z'>)> for Foo { + type Field = Baz; + + fn get_field( + &self, + key: ::core::marker::PhantomData<(Char<'b'>, Char<'a'>, Char<'z'>)>, + ) -> &Self::Field { + &self.baz + } + } + }; + + assert!(equal_token_stream(&derived, &expected)); +} + +#[test] +fn test_generic_derive_fields() { + let derived = derive_fields(quote! { + pub struct Foo + where + FooParamA: Eq, + { + pub bar: Bar, + pub baz: Baz, + } + }); + + let expected = quote! { + impl HasField<(Char<'b'>, Char<'a'>, Char<'r'>)> + for Foo + where + FooParamA: Eq, + { + type Field = Bar; + + fn get_field( + &self, + key: ::core::marker::PhantomData<(Char<'b'>, Char<'a'>, Char<'r'>)>, + ) -> &Self::Field { + &self.bar + } + } + + impl HasField<(Char<'b'>, Char<'a'>, Char<'z'>)> + for Foo + where + FooParamA: Eq, + { + type Field = Baz; + + fn get_field( + &self, + key: ::core::marker::PhantomData<(Char<'b'>, Char<'a'>, Char<'z'>)>, + ) -> &Self::Field { + &self.baz + } + } + }; + + assert!(equal_token_stream(&derived, &expected)); +} diff --git a/crates/cgp-field-macro-lib/src/tests/helper/equal.rs b/crates/cgp-field-macro-lib/src/tests/helper/equal.rs new file mode 100644 index 0000000..0e54436 --- /dev/null +++ b/crates/cgp-field-macro-lib/src/tests/helper/equal.rs @@ -0,0 +1,7 @@ +use proc_macro2::TokenStream; + +use crate::tests::helper::format::format_token_stream; + +pub fn equal_token_stream(left: &TokenStream, right: &TokenStream) -> bool { + format_token_stream(left) == format_token_stream(right) +} diff --git a/crates/cgp-field-macro-lib/src/tests/helper/format.rs b/crates/cgp-field-macro-lib/src/tests/helper/format.rs new file mode 100644 index 0000000..2c96595 --- /dev/null +++ b/crates/cgp-field-macro-lib/src/tests/helper/format.rs @@ -0,0 +1,7 @@ +use prettyplease::unparse; +use proc_macro2::TokenStream; +use syn::parse_file; + +pub fn format_token_stream(stream: &TokenStream) -> String { + unparse(&parse_file(&stream.to_string()).unwrap()) +} diff --git a/crates/cgp-field-macro-lib/src/tests/helper/mod.rs b/crates/cgp-field-macro-lib/src/tests/helper/mod.rs new file mode 100644 index 0000000..e54acd6 --- /dev/null +++ b/crates/cgp-field-macro-lib/src/tests/helper/mod.rs @@ -0,0 +1,2 @@ +pub mod equal; +pub mod format; diff --git a/crates/cgp-field-macro-lib/src/tests/mod.rs b/crates/cgp-field-macro-lib/src/tests/mod.rs new file mode 100644 index 0000000..e7c3ff4 --- /dev/null +++ b/crates/cgp-field-macro-lib/src/tests/mod.rs @@ -0,0 +1,3 @@ +pub mod field; +pub mod helper; +pub mod symbol; diff --git a/crates/cgp-field-macro-lib/src/tests/symbol.rs b/crates/cgp-field-macro-lib/src/tests/symbol.rs new file mode 100644 index 0000000..4198073 --- /dev/null +++ b/crates/cgp-field-macro-lib/src/tests/symbol.rs @@ -0,0 +1,31 @@ +use quote::quote; + +use crate::symbol::make_symbol; +use crate::tests::helper::equal::equal_token_stream; + +#[test] +fn test_symbol_macro() { + let symbol = make_symbol(quote!("hello_world")); + + let derived = quote! { + type Symbol = #symbol; + }; + + let expected = quote! { + type Symbol = ( + Char<'h'>, + Char<'e'>, + Char<'l'>, + Char<'l'>, + Char<'o'>, + Char<'_'>, + Char<'w'>, + Char<'o'>, + Char<'r'>, + Char<'l'>, + Char<'d'>, + ); + }; + + assert!(equal_token_stream(&derived, &expected)); +} diff --git a/crates/cgp-field-macro/Cargo.toml b/crates/cgp-field-macro/Cargo.toml new file mode 100644 index 0000000..100b0c8 --- /dev/null +++ b/crates/cgp-field-macro/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "cgp-field-macro" +version = "0.1.0" +edition = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +authors = { workspace = true } +rust-version = { workspace = true } +readme = "README.md" +keywords = ["context-generic programming"] +description = """ + Context-generic programming core macros +""" + +[package.metadata.docs.rs] +all-features = true + +[lib] +proc-macro = true + +[dependencies] +cgp-field-macro-lib = { version = "0.1.0" } +proc-macro2 = "1.0.67" \ No newline at end of file diff --git a/crates/cgp-field-macro/src/lib.rs b/crates/cgp-field-macro/src/lib.rs new file mode 100644 index 0000000..d8446c4 --- /dev/null +++ b/crates/cgp-field-macro/src/lib.rs @@ -0,0 +1,13 @@ +extern crate proc_macro; + +use proc_macro::TokenStream; + +#[proc_macro_derive(HasField)] +pub fn derive_fields(item: TokenStream) -> TokenStream { + cgp_field_macro_lib::derive_fields(item.into()).into() +} + +#[proc_macro] +pub fn symbol(body: TokenStream) -> TokenStream { + cgp_field_macro_lib::make_symbol(body.into()).into() +} diff --git a/crates/cgp-field/Cargo.toml b/crates/cgp-field/Cargo.toml new file mode 100644 index 0000000..ff88a44 --- /dev/null +++ b/crates/cgp-field/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "cgp-field" +version = "0.1.0" +edition = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +authors = { workspace = true } +rust-version = { workspace = true } +readme = "README.md" +keywords = ["context-generic programming"] +description = """ + Context-generic programming core traits +""" + +[package.metadata.docs.rs] +all-features = true + +[dependencies] +cgp-field-macro = { version = "0.1.0" } diff --git a/crates/cgp-field/src/lib.rs b/crates/cgp-field/src/lib.rs new file mode 100644 index 0000000..4b9f3d0 --- /dev/null +++ b/crates/cgp-field/src/lib.rs @@ -0,0 +1,8 @@ +#![no_std] + +pub mod traits; +pub mod types; + +pub use cgp_field_macro::{symbol, HasField}; +pub use traits::HasField; +pub use types::Char; diff --git a/crates/cgp-field/src/traits.rs b/crates/cgp-field/src/traits.rs new file mode 100644 index 0000000..29b5bae --- /dev/null +++ b/crates/cgp-field/src/traits.rs @@ -0,0 +1,20 @@ +use core::marker::PhantomData; +use core::ops::Deref; + +pub trait HasField { + type Field; + + fn get_field(&self, key: PhantomData) -> &Self::Field; +} + +impl HasField for Context +where + Context: Deref, + Target: HasField + 'static, +{ + type Field = Field; + + fn get_field(&self, key: PhantomData) -> &Self::Field { + self.deref().get_field(key) + } +} diff --git a/crates/cgp-field/src/types.rs b/crates/cgp-field/src/types.rs new file mode 100644 index 0000000..ad63b5c --- /dev/null +++ b/crates/cgp-field/src/types.rs @@ -0,0 +1 @@ +pub struct Char;