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

macros by example for derive and attribute macros #3

Open
programmerjake opened this issue Mar 15, 2024 · 5 comments
Open

macros by example for derive and attribute macros #3

programmerjake opened this issue Mar 15, 2024 · 5 comments

Comments

@programmerjake
Copy link
Member

programmerjake commented Mar 15, 2024

It would be useful to support macro_rules!/macro-style macros for derive and attribute macros. For derive macros, I think we'll need enough functionality to be able to write a Clone-syle macro so I think we'll want:

  • fragment rules for generics and where clauses
  • macro expansions for impl generics, type generics
  • macro expansions for only type parameters (so you can add bounds)
  • fragment rules for matching member names (matches ident or both matches nothing and expands to a thing that's a member index but also usable as a variable name; alternatively have ${ident{$my_member}} convert any numeric member names to valid identifiers (e.g. _1)). also a variant to match member names that matches following colon too -- so it matches nothing or <ident>:.
  • fragment rules for matching delimiters:
    • always must be immediately followed by {<contents>}, on expansion the {} are replaced with the matched delimiters.
    • e.g. ($delim:paren_or_brace{$($body:tt)*}) => { $delim{inner} ($($body)*) } would convert:
      • (a, b, c) to (inner) (a, b, c)
      • {foo} to {inner} (foo)

example Clone-style derive macro:

#[derive]
macro_rules! MyClone {
    (
        $(#[$struct_meta:meta])*
        $struct_vis:vis struct $struct_name:ident$(<$generics:generics>)?
        $(where $where:where)?
        $($struct_delim:paren_or_brace{
             $(
                 $(#[$field_meta:meta])*
                 $field_vis:vis $field:member_colon $field_ty:ty
             ),*
             $(,)?
        })?
        $($(where $where2:where)?;)?
    ) => {
        impl$(<${impl{$generics}}>)? $crate::MyClone for $struct_name $(<${type{$generics}}>)?
        where
            $(${type_params{$generics}}: $crate::MyClone,)*
            $($where)?
            $($($where2)?)?
        {
            fn my_clone(&self) -> Self {
                Self $({
                    $($field: $crate::MyClone::my_clone(&self.$field),)*
                })?
            }
        }
    };
    // TODO: enums
}

alternative struct body matching:

  • maybe it would be better to instead have a matcher for a struct body and have macro expansions for getting field names, field types, field attributes, any where clauses, and building a struct body

  • or maybe have a matcher for a struct body that pretends all struct bodies are of the { a: Ty, b: Ty2 } variety and transforms the expansion of that matcher to the actual input struct body variety:

e.g.:

#[derive]
macro_rules! MyClone {
    (
        $(#[$struct_meta:meta])*
        $struct_vis:vis struct $struct_name:ident$(<$generics:generics>)?
        $body:struct_body{ // automagically transforms tuple and unit structs
             $(where $where:where)?
             {
                 $(
                     $(#[$field_meta:meta])*
                     $field_vis:vis $field:member: $field_ty:ty,
                 )*
             }
        }
        $(;)?
    ) => {
        impl$(<${impl{$generics}}>)? $crate::MyClone for $struct_name $(<${type{$generics}}>)?
        where
            $(${type_params{$generics}}: $crate::MyClone,)*
            $($where)?
        {
            fn my_clone(&self) -> Self {
                Self $body{
                    $($field: $crate::MyClone::my_clone(&self.$field),)*
                }
            }
        }
    };
    (
        $(#[$enum_meta:meta])*
        $enum_vis:vis enum $enum_name:ident$(<$generics:generics>)?
        $(where $where:where)?
        {
             $($Variant:ident $body:struct_body{ // no where here
             { // automagically transforms tuple and unit variants
                 $(
                     $(#[$field_meta:meta])*
                     $field_vis:vis $field:member: $field_ty:ty,
                 )*
             }} $(= $discriminant:expr)?),*
             $(,)?
        }
    ) => {
        impl$(<${impl{$generics}}>)? $crate::MyClone for $enum_name $(<${type{$generics}}>)?
        where
            $(${type_params{$generics}}: $crate::MyClone,)*
            $($where)?
        {
            fn my_clone(&self) -> Self {
                match *self {
                    $(Self::$Variant $body{$(ref $field,)*} => Self::$Variant $body{
                        $($field: $crate::MyClone::my_clone($field),)*
                    },)*
                }
            }
        }
    };
}
@danielhenrymantilla
Copy link

danielhenrymantilla commented Mar 23, 2024

I like the suggestion; however, I worry that having to parse the whole item "manually" may yield a lot of scope creep and effort. A more future-proof, and tractable pattern, following on your idea of impl{} and type{}, would be to rely on macro_metavar_exprs to, in a way, implement "fields" in captures. Yielding something along the lines of:

macro_rules! Clone {(
    $struct:item_struct
) => (
    impl<${struct.generics}>
        ::core::clone::Clone
    for
        ${struct.name} <${struct.forward_generics}>
    where
        ${struct.where_clauses} // <- these things would expand to smth with trailing `,`
        ${add_clone_bound!( ${struct.forward_generics} )} // <- eager-expansion pattern
    {
        fn clone(&self) -> Self {
            Self {
                ${clone_fields!( self, ${struct.field_names} )}
            }
        }
    }
)}

Which would only require simple extra helpers for these eager expansions:

macro_rules! add_clone_bound {(
    $($lt:lifetime),* $(,)?
    $($T:ident),* $(,)?
    $(const $CONST:ident: $ConstTy:ty),* $(,)?
) => (
    $(
        $T : ::core::clone::Clone,
    )*
)}

macro_rules! clone_fields {(
    $self:tt $(, $field_name:ident)* $(,)?
) => (
    $(
        $field_name: ::core::Clone::clone(&self.$field_name),
    )*
)}

This reduces the needs for features required for this to work down to two:

  • :item_struct kind of matchers, alongside its ${_.member} getters;
  • ${macro_name!(...)}, for eager expansion (or at least: "arbitrary call-site" expansion)

@programmerjake
Copy link
Member Author

add_clone_bound is incorrect, const generics can be mixed with type generics. this is part of why I have ${type_params(...)}

@programmerjake
Copy link
Member Author

also, handling struct and field attributes is commonly needed, e.g. for #[skip] to skip using some field for derive(Debug)-style things.

@danielhenrymantilla
Copy link

add_clone_bound is incorrect, const generics can be mixed with type generics. this is part of why I have ${type_params(...)}

Good point; that's what I get for tring to do things manually 😅. Agree on the need for thus extra getters/operators (I'd still lean towards fields/methods for the sake of namespacing and whatnot).

also, handling struct and field attributes is commonly needed, e.g. for #[skip] to skip using some field for derive(Debug)-style things.

Yeah, I'm not advocating fully against manually doing things, just that it should not be mandatory 🙂

@programmerjake
Copy link
Member Author

also, handling struct and field attributes is commonly needed, e.g. for #[skip] to skip using some field for derive(Debug)-style things.

Yeah, I'm not advocating fully against manually doing things, just that it should not be mandatory 🙂

I like the idea of just having $v:struct_item and then a bunch of ${v.struct_meta} or ${v.field_meta} (which would have to be 2-deep repetition since there's multiple fields and multiple meta per field).

extending that to handle enums would probably need both $v:enum_item and $v:enum_variant since you'd want to pass single variants to an eager macro for some processing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants