Skip to content
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

Support pointwise addition for arrays and tuples #343

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions impl/src/add_assign_like.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub fn expand(input: &DeriveInput, trait_name: &str) -> TokenStream {
impl #impl_generics ::derive_more::#trait_ident for #input_type #ty_generics #where_clause {
#[inline]
fn #method_ident(&mut self, rhs: #input_type #ty_generics) {
let lhs: &mut Self = self;
#( #exprs; )*
}
}
Expand Down
72 changes: 54 additions & 18 deletions impl/src/add_helpers.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,64 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Field, Ident, Index};
use syn::{Field, Ident, Index, Type, TypeArray, TypeTuple};

pub fn tuple_exprs(fields: &[&Field], method_ident: &Ident) -> Vec<TokenStream> {
let mut exprs = vec![];

for i in 0..fields.len() {
let i = Index::from(i);
// generates `self.0.add(rhs.0)`
let expr = quote! { self.#i.#method_ident(rhs.#i) };
exprs.push(expr);
}
exprs
let fields: Vec<&Type> = fields.iter().map(|field| &field.ty).collect::<Vec<_>>();
inner_tuple_exprs(&quote! {}, &fields, method_ident)
}

pub fn struct_exprs(fields: &[&Field], method_ident: &Ident) -> Vec<TokenStream> {
let mut exprs = vec![];
fields
.iter()
.map(|field| {
// It's safe to unwrap because struct fields always have an identifier
let field_path = field.ident.as_ref().unwrap();
elem_content(&quote! { .#field_path }, &field.ty, method_ident)
})
.collect()
}

pub fn inner_tuple_exprs(
field_path: &TokenStream,
fields: &[&Type],
method_ident: &Ident,
) -> Vec<TokenStream> {
fields
.iter()
.enumerate()
.map(|(i, ty)| {
let i = Index::from(i);
elem_content(&quote! { #field_path.#i }, ty, method_ident)
})
.collect()
}

pub fn elem_content(
field_path: &TokenStream,
ty: &Type,
method_ident: &Ident,
) -> TokenStream {
match ty {
Type::Array(TypeArray { elem, .. }) => {
let fn_body = elem_content(&quote! {}, elem.as_ref(), method_ident);

for field in fields {
// It's safe to unwrap because struct fields always have an identifier
let field_id = field.ident.as_ref().unwrap();
// generates `x: self.x.add(rhs.x)`
let expr = quote! { self.#field_id.#method_ident(rhs.#field_id) };
exprs.push(expr)
quote! {
lhs #field_path.into_iter().zip(rhs #field_path.into_iter())
.map(|(lhs, rhs)| #fn_body)
.collect::<Vec<_>>()
Copy link
Collaborator

@tyranron tyranron Mar 26, 2024

Choose a reason for hiding this comment

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

@matthiasgoergens

The latest commit on this branch has an alternative that uses Vec and iterators, but does not require Copy. I'm not sure if that's better or worse?

Nah... this is not good, as is not zero-cost and forbids no_std usages.

I wonder whether it could be solved with some additional add-hoc type machinery, which can be put into the add module of the derive_more crate. We could provide either our custom Addable trait, or a #[repr(transparent)] wrapper type implementing the Add trait from core, and fill it with all the necessary blanket implementations for tuples and arrays. While in macro expansion, it would be only enough to call these implementations, which will do the job recursively and automatically.

Copy link
Collaborator

@tyranron tyranron Mar 26, 2024

Choose a reason for hiding this comment

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

Expanding this machinery to all types, we may support also the following case:

type MyArr = [i32; 2];
struct StructRecursive {
    a: i32,
    b: MyArr,
}

Which won't work if we try detecting the tuple/array type via macro.

However, I do think that the coherence won't allow this, as we will quickly hit the "upstream crates may add a new impl of trait core::ops::Add" error. Which, in turn, could be tried to overcome with autoref-based specialization.

Copy link
Collaborator

@tyranron tyranron Mar 26, 2024

Choose a reason for hiding this comment

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

Yeah... it seems that something like the following should work in general case, allowing us to consider type aliases naturally:

#[repr(transparent)]
struct Wrap<T>(T);

impl<T, const N: usize> Add for &Wrap<[T; N]> {
   // custom implementation for arrays
}
impl<T> Add for &Wrap<(T,)> {
   // and so on for other tuples...
}

impl<T: Add> Add for Wrap<T> {
    // piggy back implemetation to `core::ops::Add`
}

// and something like this when expanding the macro:
(&&Wrap::from(self)).add(&Wrap::from(rhs));
// where `Wrap::from` is `#[repr(transparent)]` transmuting for references

This way autoref-based specialization will try first to resolve our custom implementations for tuple and arrays, and fallback to core::ops::Add otherwise.

.try_into()
.unwrap_or_else(|_| unreachable!("Lengths should always match."))
}
}
Type::Tuple(TypeTuple { elems, .. }) => {
let exprs = inner_tuple_exprs(
field_path,
&elems.iter().collect::<Vec<_>>(),
method_ident,
);
quote! { (#(#exprs),*) }
}
// generates `lhs.x.add(rhs.x)`
_ => quote! { lhs #field_path.#method_ident(rhs #field_path) },
}
exprs
}
6 changes: 4 additions & 2 deletions impl/src/add_like.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub fn expand(input: &DeriveInput, trait_name: &str) -> TokenStream {
let generics = add_extra_type_param_bound_op_output(&input.generics, &trait_ident);
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();

// TODO(Matthias): do we add support for arrays here?
let (output_type, block) = match input.data {
Data::Struct(ref data_struct) => match data_struct.fields {
Fields::Unnamed(ref fields) => (
Expand Down Expand Up @@ -47,13 +48,14 @@ pub fn expand(input: &DeriveInput, trait_name: &str) -> TokenStream {

#[inline]
fn #method_ident(self, rhs: #input_type #ty_generics) -> #output_type {
let lhs: Self = self;
#block
}
}
}
}

fn tuple_content<T: ToTokens>(
pub(crate) fn tuple_content<T: ToTokens>(
input_type: &T,
fields: &[&Field],
method_ident: &Ident,
Expand Down Expand Up @@ -148,7 +150,7 @@ fn enum_content(
});
}
quote! {
match (self, rhs) {
match (lhs, rhs) {
#(#matches),*
}
}
Expand Down
38 changes: 19 additions & 19 deletions impl/src/into.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,30 +237,30 @@ impl Parse for FieldAttribute {
}
}

type Untyped = Either<attr::Skip, Either<attr::Empty, ConversionsAttribute>>;
impl From<Untyped> for FieldAttribute {
fn from(v: Untyped) -> Self {
match v {
Untyped::Left(skip) => Self {
skip: Some(skip),
convs: None,
},
Untyped::Right(c) => Self {
skip: None,
convs: Some(match c {
Either::Left(_empty) => ConversionsAttribute::default(),
Either::Right(convs) => convs,
}),
},
}
}
}

impl attr::ParseMultiple for FieldAttribute {
fn parse_attr_with<P: attr::Parser>(
attr: &syn::Attribute,
parser: &P,
) -> syn::Result<Self> {
type Untyped = Either<attr::Skip, Either<attr::Empty, ConversionsAttribute>>;
impl From<Untyped> for FieldAttribute {
fn from(v: Untyped) -> Self {
match v {
Untyped::Left(skip) => Self {
skip: Some(skip),
convs: None,
},
Untyped::Right(c) => Self {
skip: None,
convs: Some(match c {
Either::Left(_empty) => ConversionsAttribute::default(),
Either::Right(convs) => convs,
}),
},
}
}
}

Untyped::parse_attr_with(attr, parser).map(Self::from)
}

Expand Down
30 changes: 30 additions & 0 deletions tests/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,33 @@ enum MixedInts {
UnsignedTwo(u32),
Unit,
}

#[derive(Add)]
#[derive(Default)]
struct StructRecursive {
a: i32,
b: [i32; 2],
c: [[i32; 2]; 3],
d: (i32, i32),
e: ((u8, [i32; 3]), i32),
f: ((u8, i32), (u8, ((i32, u64, ((u8, u8), u16)), u8))),
g: i32,
}

#[test]
fn test_sanity() {
let mut a: StructRecursive = Default::default();
let mut b: StructRecursive = Default::default();
a.c[0][1] = 1;
b.c[0][1] = 2;
let c = a + b;
assert_eq!(c.c[0][1], 3);
}

#[derive(Add)]
struct TupleRecursive((i32, u8), [(i32, u8); 10]);

#[derive(Add)]
pub struct GenericArrayStruct<T> {
pub a: [T; 2],
}
Loading