Skip to content

Add a derive macro for Prism #101

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ license = "MIT OR Apache-2.0"

[features]
async = ["tokio/rt", "futures", "flume"]
derive = ["druid-widget-nursery-derive"]
hot-reload = ["libloading", "notify5", "rand"]

[dependencies.druid]
Expand All @@ -27,6 +28,9 @@ tokio = { version = "1.0", features = ["rt", "time"], optional = true }
futures = { version = "0.3", optional = true }
flume = { version = "0.10", optional = true }

# derive
druid-widget-nursery-derive = { version = "=0.1.0", path = "druid-widget-nursery-derive", optional = true }

# hot reload
notify5 = { version = "5.0.0-pre.11", optional = true, package = "notify" }
libloading = { version = "0.6.6", optional = true }
Expand All @@ -40,11 +44,18 @@ required-features = ["async", "tokio/rt-multi-thread", "tokio/macros", "tokio/ti
[[example]]
name = "animator"

[[example]]
name = "prisms"
required-features = ["derive"]

[[example]]
name = "splits"

[workspace]
members = ["examples/hot-reload"]
members = [
"druid-widget-nursery-derive",
"examples/hot-reload",
]

[dev-dependencies]
qu = "0.3.1"
Expand Down
12 changes: 12 additions & 0 deletions druid-widget-nursery-derive/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "druid-widget-nursery-derive"
version = "0.1.0"
edition = "2021"

[lib]
proc-macro = true

[dependencies]
proc-macro2 = "1.0.36"
quote = "1.0.14"
syn = "1.0.85"
13 changes: 13 additions & 0 deletions druid-widget-nursery-derive/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput};

mod prism;
use prism::expand_prism;

#[proc_macro_derive(Prism)]
pub fn prism(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
expand_prism(input)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
104 changes: 104 additions & 0 deletions druid-widget-nursery-derive/src/prism.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{parse_quote, spanned::Spanned, Data, DeriveInput, Fields, GenericParam, WherePredicate};

pub fn expand_prism(input: DeriveInput) -> syn::Result<TokenStream> {
let variants = match input.data {
Data::Enum(e) => e.variants,
_ => panic!("this derive macro only works on enums"),
};

let enum_name = input.ident;
let enum_vis = input.vis;

let mut generics = input.generics;

let mut prism_where_clause = generics.make_where_clause().clone();
prism_where_clause
.predicates
.extend(generics.params.iter().filter_map(|param| match param {
GenericParam::Type(ty) => {
let name = &ty.ident;
let pred: WherePredicate = parse_quote! { #name: ::std::clone::Clone };

Some(pred)
}
GenericParam::Lifetime(_) | GenericParam::Const(_) => None,
}));

let (impl_generics, enum_generics, _enum_where_clause) = generics.split_for_impl();

variants
.iter()
.map(|v| {
let variant_name = &v.ident;
let name = format_ident!("{}{}", enum_name, variant_name, span = v.span());

let inner_type;
let inner_expr;
let cloned_inner;
let variant_expr;

match &v.fields {
Fields::Named(_) => {
return Err(syn::Error::new_spanned(
&v,
"variants with named fields are not supported for deriving `Prism`",
));
}
Fields::Unnamed(f) => {
let fields = f.unnamed.iter();

// By having the comma outside instead of inside the #(),
// it is only added between items, not after the last one.
// For `Variant()` the inner type is `()`, for `Variant(A)`
// it is `(A)` (equal to just `A`), for `Variant(A, B)` it
// is the tuple `(A, B)`.
inner_type = quote! { (#(#fields),*) };

let fields = (0..f.unnamed.len()).map(|n| format_ident!("_v{}", n + 1));
let cloned = fields
.clone()
.map(|f| quote! { ::std::clone::Clone::clone(#f) });

inner_expr = quote! { (#(#fields),*) };
cloned_inner = quote! { (#(#cloned),*) };
variant_expr = inner_expr.clone();
}
Fields::Unit => {
inner_type = quote! { () };
inner_expr = quote! { () };
cloned_inner = quote! { () };
variant_expr = quote! {};
}
}

Ok(quote! {
#[derive(Clone)]
#enum_vis struct #name;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we make it mod $enum_name { pub struct $variant_name; } instead of struct $enum_name$variant_name?
this is done in derive Lens

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that! I thought this kind of thing would require inherent associated types, great to hear it doesn't.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately this doesn't work because enum variants already exist in the value namespace (Option::Some in the value namespace is a function from T to Option<T>).

Because of rust-lang/rust#76347, I only got errors after fully implementing this idea 🙄

Copy link
Member Author

@jplatte jplatte Jan 11, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh by the way the way druid does it / what almost worked wasn't mod $enum_name, it was mod $mod_name {} + impl #enum_name { pub const Some: $mod_name = $mod_name::Some; }.

The first approach immediately resulted in amibiguity errors between the enum and module.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's the broken code that uses associated constants: jplatte@7a62a9b


#[automatically_derived]
impl #impl_generics ::druid_widget_nursery::prism::Prism<
#enum_name #enum_generics,
#inner_type,
> for #name #prism_where_clause {
fn get(
&self,
data: &#enum_name #enum_generics,
) -> ::std::option::Option<#inner_type> {
match data {
#enum_name::#variant_name #variant_expr => {
::std::option::Option::Some(#cloned_inner)
}
_ => ::std::option::Option::None,
}
}

fn put(&self, data: &mut #enum_name #enum_generics, #inner_expr: #inner_type) {
*data = #enum_name::#variant_name #variant_expr;
}
}
})
})
.collect()
}
50 changes: 1 addition & 49 deletions examples/prisms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,61 +3,13 @@ use druid::{AppLauncher, Data, UnitPoint, Widget, WidgetExt, WindowDesc};
use druid_widget_nursery::prism::{Closures, Prism};
use druid_widget_nursery::{MultiCheckbox, MultiRadio};

#[derive(Data, Clone, PartialEq)]
#[derive(Data, Clone, PartialEq, Prism)]
enum TestData {
A(f64),
B(String),
C(Option<String>),
}

struct TestDataA;

impl Prism<TestData, f64> for TestDataA {
fn get(&self, data: &TestData) -> Option<f64> {
if let TestData::A(value) = data {
Some(*value)
} else {
None
}
}

fn put(&self, data: &mut TestData, inner: f64) {
*data = TestData::A(inner);
}
}

struct TestDataB;

impl Prism<TestData, String> for TestDataB {
fn get(&self, data: &TestData) -> Option<String> {
if let TestData::B(str) = data {
Some(str.to_string())
} else {
None
}
}

fn put(&self, data: &mut TestData, inner: String) {
*data = TestData::B(inner);
}
}

struct TestDataC;

impl Prism<TestData, Option<String>> for TestDataC {
fn get(&self, data: &TestData) -> Option<Option<String>> {
if let TestData::C(value) = data {
Some((*value).clone())
} else {
None
}
}

fn put(&self, data: &mut TestData, inner: Option<String>) {
*data = TestData::C(inner);
}
}

fn main_widget() -> impl Widget<TestData> {
let a = MultiRadio::new(
"Variant A",
Expand Down
4 changes: 3 additions & 1 deletion src/prism.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ use druid::{
Point, Size, UpdateCtx, Widget, WidgetPod,
};

//TODO: Maybe write a derive macro
#[cfg(feature = "derive")]
pub use druid_widget_nursery_derive::Prism;

/// A trait similar to druid::Lens that represents data which is not always present
///
/// This is just a simple prototype for me to work with until [`#1136`] is merged.
Expand Down
37 changes: 37 additions & 0 deletions tests/prism.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#![cfg(feature = "derive")]

use std::{fmt::Debug, marker::PhantomData};

use druid_widget_nursery::prism::Prism;

#[derive(Clone, Prism)]
enum MyOption<T> {
Some(T),
None,
}

#[derive(Clone, Prism)]
enum CLike {
A,
B,
C,
}

#[derive(Clone, Prism)]
enum Complex {
First,
Second(),
Third(u32),
Fourth(String, Box<Complex>),
}

#[derive(Clone, Prism)]
enum LotsOfGenerics<T, U: Debug>
where
T: Clone,
(T, U): Clone,
{
V1,
V2(T),
V3(PhantomData<T>, Box<(U, U)>),
}