From b0feea938a03061dc785914b08390e700eda5ec7 Mon Sep 17 00:00:00 2001 From: fef Date: Wed, 4 Oct 2023 21:05:10 +0200 Subject: [PATCH] derive: another refactor, support generics The two derive macros share common code now. Furthermore, components derived from one of these macros may use arbitrary generics. Finally, the uwui crate name (as in, crate or ::uwui) is being resolved dynamically. --- Cargo.lock | 64 +++++++++++ dsl/Cargo.toml | 1 + dsl/src/derive/component.rs | 179 +++++++++++++++++++++++++++++ dsl/src/derive/custom_component.rs | 31 ----- dsl/src/derive/mod.rs | 3 +- dsl/src/derive/native_component.rs | 31 ----- dsl/src/lib.rs | 5 +- dsl/src/template/compile.rs | 42 ++++--- dsl/src/template/mod.rs | 11 +- dsl/src/util.rs | 98 +++++++++++++--- src/lib.rs | 3 +- 11 files changed, 365 insertions(+), 103 deletions(-) create mode 100644 dsl/src/derive/component.rs delete mode 100644 dsl/src/derive/custom_component.rs delete mode 100644 dsl/src/derive/native_component.rs diff --git a/Cargo.lock b/Cargo.lock index 5844762..3e2e6b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,43 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "hashbrown" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" + +[[package]] +name = "indexmap" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "proc-macro-crate" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.67" @@ -31,6 +68,23 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + [[package]] name = "unicode-ident" version = "1.0.12" @@ -48,7 +102,17 @@ dependencies = [ name = "uwui-dsl" version = "0.1.0" dependencies = [ + "proc-macro-crate", "proc-macro2", "quote", "syn", ] + +[[package]] +name = "winnow" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" +dependencies = [ + "memchr", +] diff --git a/dsl/Cargo.toml b/dsl/Cargo.toml index 5845f5a..e59d70d 100644 --- a/dsl/Cargo.toml +++ b/dsl/Cargo.toml @@ -8,6 +8,7 @@ repository.workspace = true proc-macro = true [dependencies] +proc-macro-crate = "2.0.0" proc-macro2 = "1.0.67" quote = "1.0.33" syn = "2.0.37" diff --git a/dsl/src/derive/component.rs b/dsl/src/derive/component.rs new file mode 100644 index 0000000..cd1258d --- /dev/null +++ b/dsl/src/derive/component.rs @@ -0,0 +1,179 @@ +use crate::util::{define_ident, use_absolute}; +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use syn::{parse_macro_input, DeriveInput, GenericParam}; + +pub fn custom(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = ComponentDef::new(parse_macro_input!(input as DeriveInput)); + let name = input.name_with_generics(); + let generics = input.generic_predicates(); + let where_clause_predicates = input.where_clause_predicates(); + + use_absolute! { + uwui::backend::Backend as Backend, + uwui::view::GetTemplate as GetTemplate, + uwui::view::__internal::CustomComponent as CustomComponent, + uwui::view::__internal::IntoNode as IntoNode, + uwui::view::__internal::CustomComponentWrapper as CustomComponentWrapper, + } + + define_ident! { + B + } + + let expanded = quote! { + #[automatically_derived] + impl<#B: #Backend, #generics> + #CustomComponent<#B> for #name + where + #name : #GetTemplate<#B>, + #where_clause_predicates + {} + + #[automatically_derived] + impl<#B: #Backend, #generics> + #IntoNode<#B> for #name + where + #name : #GetTemplate<#B>, + #where_clause_predicates + { + type Node = #CustomComponentWrapper<#B, Self>; + + fn into_node(self) -> Self::Node { + #CustomComponentWrapper::new(self) + } + } + }; + expanded.into() +} + +pub fn native(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = ComponentDef::new(parse_macro_input!(input as DeriveInput)); + let name = input.name_with_generics(); + let generics = input.generic_predicates(); + let where_clause_predicates = input.where_clause_predicates(); + + use_absolute! { + uwui::backend::Backend as Backend, + uwui::backend::Render as Render, + uwui::view::__internal::Node as Node, + uwui::view::__internal::IntoNode as IntoNode, + } + + define_ident! { + B + } + + let expanded = quote! { + #[automatically_derived] + impl<#B: #Backend, #generics> + #Node<#B> for #name + where + #name : #Render<#B>, + #where_clause_predicates + {} + + #[automatically_derived] + impl<#B: #Backend, #generics> + #IntoNode<#B> for #name + where + #name : #Render<#B>, + #where_clause_predicates + { + type Node = Self; + + fn into_node(self) -> Self::Node { + self + } + } + }; + expanded.into() +} + +struct ComponentDef { + derive_input: DeriveInput, +} + +impl ComponentDef { + pub fn new(derive_input: DeriveInput) -> Self { + Self { derive_input } + } + + /// `Name` + pub fn name_with_generics(&self) -> TokenStream { + let name = &self.derive_input.ident; + let generics = self.generic_names(); + if generics.is_empty() { + name.to_token_stream() + } else { + quote! { #name < #generics > } + } + } + + /// `T1, T2, T3` + pub fn generic_names(&self) -> TokenStream { + let mut out = Vec::new(); + let generics = &self.derive_input.generics; + for param in generics.params.iter() { + match param { + GenericParam::Lifetime(lft) => { + out.push(lft.lifetime.to_token_stream()); + } + GenericParam::Type(typ) => { + out.push(typ.ident.to_token_stream()); + } + GenericParam::Const(cnst) => { + out.push(cnst.ident.to_token_stream()); + } + } + } + + quote! { + #(#out),* + } + } + + /// `T1: Constraint1, T2: Constraint2 + Constraint1, T3` + pub fn generic_predicates(&self) -> TokenStream { + let mut out = TokenStream::new(); + let generics = &self.derive_input.generics; + for param in generics.params.iter() { + match param { + GenericParam::Lifetime(lft) => { + lft.lifetime.to_tokens(&mut out); + if let Some(colon) = lft.colon_token { + let bounds = &lft.bounds; + out.extend(quote! { #colon #bounds }); + } + out.extend(quote! { , }); + } + GenericParam::Type(typ) => { + typ.ident.to_tokens(&mut out); + if let Some(colon) = typ.colon_token { + let bounds = &typ.bounds; + out.extend(quote! { #colon #bounds }); + } + out.extend(quote! { , }); + } + GenericParam::Const(cnst) => { + let kw = &cnst.const_token; + let ident = &cnst.ident; + let colon = &cnst.colon_token; + let ty = &cnst.ty; + out.extend(quote! { #kw #ident #colon #ty , }); + } + } + } + + out + } + + pub fn where_clause_predicates(&self) -> TokenStream { + self.derive_input + .generics + .where_clause + .as_ref() + .map(|clause| &clause.predicates) + .to_token_stream() + } +} diff --git a/dsl/src/derive/custom_component.rs b/dsl/src/derive/custom_component.rs deleted file mode 100644 index b70503b..0000000 --- a/dsl/src/derive/custom_component.rs +++ /dev/null @@ -1,31 +0,0 @@ -use proc_macro::TokenStream; -use quote::quote; -use syn::{parse_macro_input, DeriveInput}; - -pub fn invoke(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - let name = input.ident; - - let expanded = quote! { - #[automatically_derived] - impl - crate::view::__internal::CustomComponent for #name - where - #name : crate::view::GetTemplate - {} - - #[automatically_derived] - impl - crate::view::__internal::IntoNode for #name - where - #name : crate::view::GetTemplate - { - type Node = crate::view::__internal::CustomComponentWrapper; - - fn into_node(self) -> Self::Node { - crate::view::__internal::CustomComponentWrapper::new(self) - } - } - }; - expanded.into() -} diff --git a/dsl/src/derive/mod.rs b/dsl/src/derive/mod.rs index 5afd19c..9cea807 100644 --- a/dsl/src/derive/mod.rs +++ b/dsl/src/derive/mod.rs @@ -1,2 +1 @@ -pub mod custom_component; -pub mod native_component; +pub mod component; diff --git a/dsl/src/derive/native_component.rs b/dsl/src/derive/native_component.rs deleted file mode 100644 index 19a69a4..0000000 --- a/dsl/src/derive/native_component.rs +++ /dev/null @@ -1,31 +0,0 @@ -use proc_macro::TokenStream; -use quote::quote; -use syn::{parse_macro_input, DeriveInput}; - -pub fn invoke(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - let name = input.ident; - - let expanded = quote! { - #[automatically_derived] - impl - crate::view::__internal::Node for #name - where - #name : crate::backend::Render, - {} - - #[automatically_derived] - impl - crate::view::__internal::IntoNode for #name - where - #name : crate::backend::Render, - { - type Node = Self; - - fn into_node(self) -> Self::Node { - self - } - } - }; - expanded.into() -} diff --git a/dsl/src/lib.rs b/dsl/src/lib.rs index 930b64a..ef8a18b 100644 --- a/dsl/src/lib.rs +++ b/dsl/src/lib.rs @@ -4,6 +4,7 @@ use proc_macro::TokenStream; mod derive; mod template; +mod util; #[proc_macro] pub fn template(input: TokenStream) -> TokenStream { @@ -12,10 +13,10 @@ pub fn template(input: TokenStream) -> TokenStream { #[proc_macro_derive(NativeComponent)] pub fn native_component(input: TokenStream) -> TokenStream { - derive::native_component::invoke(input) + derive::component::native(input) } #[proc_macro_derive(CustomComponent)] pub fn custom_component(input: TokenStream) -> TokenStream { - derive::custom_component::invoke(input) + derive::component::custom(input) } diff --git a/dsl/src/template/compile.rs b/dsl/src/template/compile.rs index 42ac01b..cec3a76 100644 --- a/dsl/src/template/compile.rs +++ b/dsl/src/template/compile.rs @@ -1,4 +1,5 @@ use crate::template::Params; +use crate::util::{define_ident, use_absolute}; use proc_macro2::TokenStream; use quote::{quote, ToTokens}; @@ -17,49 +18,62 @@ impl ToTokens for Body { impl ToTokens for Component { fn to_tokens(&self, tokens: &mut TokenStream) { let name = &self.name; + use_absolute! { + core::convert::Into as Into, + core::default::Default as Default, + core::iter::IntoIterator as IntoIterator, + uwui::backend::TargetBackend as TargetBackend, + uwui::view::Construct as Construct, + uwui::view::ConstructGroup as ConstructGroup, + uwui::view::View as View, + uwui::view::__internal::IntoNode as IntoNode, + } + define_ident! { + construct_params + } + let params = match self.params.as_ref() { Some(p) => match p { + #[allow(non_snake_case)] Params::Anon(params) => quote! { - ::core::convert::Into::<<#name as crate::view::Construct>::Params>::into( - (#(::core::convert::Into::<_>::into(#params)),*,) + #Into::<<#name as #Construct>::Params>::into( + (#(#Into::<_>::into(#params)),*,) ) }, Params::Named(params) => quote! { { - let mut __uwui_params = < - <#name as crate::view::Construct>::Params as ::core::default::Default + let mut #construct_params = < + <#name as #Construct>::Params as #Default >::default(); - #(__uwui_params.#params)* - __uwui_params + #(#construct_params.#params)* + #construct_params } }, }, None => quote! { - <<#name as crate::view::Construct>::Params as ::core::default::Default>::default() + <<#name as #Construct>::Params as #Default>::default() }, }; let initializer = if let Some(body) = self.body.as_ref() { let body = body.elements.as_slice(); quote! { - < - #name as crate::view::ConstructGroup:: - >::construct_group( + < #name as #ConstructGroup::<#TargetBackend> >::construct_group( #params, - ::core::iter::IntoIterator::into_iter([ + #IntoIterator::into_iter([ #(#body),* ]) ) } } else { quote! { - <#name as crate::view::Construct>::construct(#params) + <#name as #Construct>::construct(#params) } }; let initializer = quote! { - crate::view::View::::new( - crate::view::__internal::IntoNode::::into_node( + #View::<#TargetBackend>::new( + #IntoNode::<#TargetBackend>::into_node( #initializer ) ) diff --git a/dsl/src/template/mod.rs b/dsl/src/template/mod.rs index ccc6bd9..c947025 100644 --- a/dsl/src/template/mod.rs +++ b/dsl/src/template/mod.rs @@ -1,3 +1,4 @@ +use crate::util::use_absolute; use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, token, Expr, Ident, Path}; @@ -7,12 +8,16 @@ mod parse; pub fn invoke(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as Body); + use_absolute! { + core::iter::FromIterator as FromIterator, + uwui::view::Template as Template, + uwui::view::View as View, + } + let output = quote! { match (()) { (()) => { - < - crate::view::Template as ::core::iter::FromIterator> - >::from_iter([ + < #Template as #FromIterator<#View<_>> >::from_iter([ #input ]) } diff --git a/dsl/src/util.rs b/dsl/src/util.rs index d9b5a1a..dbef52f 100644 --- a/dsl/src/util.rs +++ b/dsl/src/util.rs @@ -1,30 +1,92 @@ -use proc_macro::{Diagnostic, Level, Span}; -use proc_macro2::TokenStream; +use proc_macro2::{Ident, Span, TokenStream}; +use proc_macro_crate::{crate_name, FoundCrate}; +use quote::quote; +use syn::{parse::ParseStream, Error}; -pub trait Compile: Sized { - fn compile(self) -> TokenStream; +pub fn __make_absolute_path(name: &str, input: TokenStream) -> TokenStream { + match name { + // core and std are special because they don't appear in Cargo.toml + "core" => quote! { ::core #input }, + "std" => quote! { ::std #input }, + name => { + let prefix = match crate_name(name) { + Ok(found) => match found { + FoundCrate::Itself => quote! { crate }, + FoundCrate::Name(name) => { + let name = Ident::new(&name, Span::call_site()); + quote! { :: #name } + } + }, + Err(e) => { + // proc_macro_crate failed to do its thing, so we try our best to + // not break other people's builds. Assume we're being called + // from another crate and the crate's author didn't rename us. + eprintln!("uwui_dsl: proc_macro_crate failed to resolve crate {name}, expect macro bugs!"); + eprintln!("uwui_dsl: proc_macro_crate has this to say: \"{e}\""); + let name = Ident::new(name, Span::call_site()); + quote! { :: #name } + } + }; + quote! { #prefix #input } + } + } } -pub fn emit_err(span: impl Into>, msg: impl Into) { - emit_diagnostic(span.into(), Level::Error, msg) +macro_rules! absolute_path { + ($crate_name:ident $(::$segment:ident)*) => { + $crate::util::__make_absolute_path( + stringify!($crate_name), + ::quote::quote!($(::$segment)*) + ) + }; } +pub(crate) use absolute_path; -pub fn emit_warning(span: impl Into>, msg: impl Into) { - emit_diagnostic(span.into(), Level::Warning, msg) +macro_rules! use_absolute { + ($($crate_name:ident $(::$path:ident)* as $name:ident),* $(,)?) => { + $( + #[allow(non_snake_case)] + let $name = $crate::util::absolute_path!($crate_name $(::$path)*); + )* + } } +pub(crate) use use_absolute; + +const RESERVED_IDENT_PREFIX: &str = "__uwui_"; -pub fn emit_note(span: impl Into>, msg: impl Into) { - emit_diagnostic(span.into(), Level::Note, msg) +pub fn make_reserved(name: &str, span: impl Into>) -> Ident { + let span = span.into().unwrap_or_else(Span::call_site); + if name.starts_with(RESERVED_IDENT_PREFIX) { + Ident::new(name, span) + } else { + Ident::new(&format!("{RESERVED_IDENT_PREFIX}{name}"), span) + } } -pub fn emit_help(span: impl Into>, msg: impl Into) { - emit_diagnostic(span.into(), Level::Help, msg) +macro_rules! define_ident { + ($($name:ident),* $(,)?) => { + $( + #[allow(non_snake_case)] + let $name = $crate::util::make_reserved(stringify!($name), None); + )* + } } +pub(crate) use define_ident; -fn emit_diagnostic(span: Option, level: Level, msg: impl Into) { - let diagnostic = match span { - Some(span) => Diagnostic::spanned(span, level, msg), - None => Diagnostic::new(level, msg), - }; - diagnostic.emit(); +/// Recursively walk through a `ParseStream` and examine all identifiers. +/// Emit an error for any identifiers that start with [`RESERVED_IDENT_PREFIX`] +/// (`__uwui_`). +/// +/// TODO: See if we can wrap ParseStream in a monad that does this check when +/// actually consuming the tokens +pub fn scan_for_reserved_idents(stream: ParseStream) -> syn::Result { + let fork = stream.fork(); + match scan_buf_for_ident_prefix(&fork, RESERVED_IDENT_PREFIX) { + Some(error) => Err(error), + None => Ok(stream), + } +} + +fn scan_buf_for_ident_prefix(stream: ParseStream, prefix: &str) -> Option { + todo!() } diff --git a/src/lib.rs b/src/lib.rs index 781de7c..d545bdb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,9 +5,8 @@ pub mod widget; pub use uwui_dsl::{template, CustomComponent, NativeComponent}; -use crate::view::Construct; use layout::Gravity; -use view::{GetTemplate, Template}; +use view::{Construct, GetTemplate, Template}; use widget::{Button, Text, VStack}; fn asdf() -> Template {