[Pre-RFC} Declarative attribute aliases

》 Edit: following from the discussion in this post, I published the portrait framework for writing procedural multi-input derives like trait impls.

Goals

  • Provide a way to conveniently encapsulate attribute macro parameters
  • Provide a way to derive an attribute macro, i.e. to declare an attribute macro in the output of another procedural macro

Non-goals

  • This is not to provide a generic syntax to manipulate the input or output body that the attribute macro is applied on. There are some attempts for this, e.g. macro-attr and macro_rules_attribute both of which not related to the goals of this RFC.

Motivating examples

Reusing derive lists

It is common to reuse the same set of derive macros in many similar types. For example, if we have a trait bound that requires Debug + Copy + Eq + Ord, it would be very useful to have a single attribute that expands to derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord).

In this case, I want to be able to write

pub macro [my_derive] = #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)];
#[my_derive] struct Foo;
#[my_derive] struct Bar;

Macro output

Procedural macros are unable to read the contents of other referenced types. For example, if I write an attribute macro on an impl block, it is not possible to get information about the trait it implements. An alternative method would be to define the attribute macro from the trait itself, so that impl blocks can use this attribute macro:

#[foo]
pub trait MyTrait { fn x(&self) -> &str; }
// Generates:
pub macro [impl_my_trait] = #[foo_impl({fn x(&self) -> &str;})];
#[impl_my_trait]
impl MyTrait for MyType {}
// Equivalent to
#[foo_impl({fn x(&self) -> &str;})]
impl MyTrait for MyType {}
// now `#[foo_impl]` knows that it has to fill this impl block with fn x

Guide-level explanation

Attribute aliases are similar to type aliases, in the sense they also re-export an existing attribute (existing type) but manipulates the attribute input (type/lifetime/const generic parameters).

Attribute aliases can be defined to receive no parameters:

macro [new_identifier] = #[existing_attribute_macro(parameters)];

The (parameters) part can be any token stream valid for an attribute macro. For example this can be used with the = syntax:

macro [simple_doc] = #[doc = "Boilerplate documentation goes here"];

Trivia: this also supports the /// syntax

macro [simple_doc] =
    /// Boilerplate documentation goes here
    /// Can be multiline, which expands to a consecutive #[] #[] stream
;

although the trailing ; is a bit ugly

Attribute aliases can also receive declarative parameters, defined using the same syntax as declarative parameters:

/// This macro elides #[derive(Serialize, Deserialize)] if #[serde] is passed anyway
macro [serde($($params:tt)*)] =
    #[derive(::serde::Serialize, ::serde::Deserialize)]
    #[serde($($params)*)];
1 Like

See also

6 Likes

I don't think I understand the motivation for this proposal. The "give a name to a set of derives" part is clear, but also feels too ad hoc to base a feature around it. The "macro output" part, I just don't understand. How would this proposal help there?

So you can't make an alias which expands to a sequence of attributes? I wanted this more often than derives (which may be split over separate individual attributes). The "manipulates input" part also isn't clear. How does your syntax support manipulating the proc macro input, and how does it avoid writing a proc macro?

Overall, I think this proposal isn't specific enough on what it proposes, and how it would help to solve the motivating problems.

I've explained it with the second motivating example. The derive macro generates an alias that contains information on its own and triggers an actual proc macro. In the example, information about a trait is copied into the attribute, so every time users apply that attribute, they actually send the contents of both the trait (through the alias) and the impl block (through the normal input TokenStream) into the procedural attribute.

Perhaps the confusion here is that the alias does not implement logic on its own. It just carries some extra data (i.e. the trait body) into the procedural macro that implements the similar logic depending on the data.

Sorry, my typo there. I actually meant attribute parameters, i.e. the part behind the attribute path (e.g. (Debug) in #[derive(Debug)]).

You can; see the serde example. Type alias is just an illustrative analogy. (If you insist, type aliases can resolve as a tuple too)

Thanks, added it to the non-goals part.

It is indeed true that the goals in this RFC can be achieved by a similar approach (through generating a decl macro that mutates the params TokenStream), where the main trick is that a decl macro can be used to deliver arbitrary TokenStream received by another procedural macro.

Based on macro_rules_attribute, a general framework to achieve the second goal in this RFC without compiler changes would be:

  1. An attribute macro on the trait derives a decl macro foo that expands into ($tt:tt) => another_proc_macro!( {#trait_body} $tt)
  2. User calls #[dynamic_attr(foo)] impl Input {}
  3. another_proc_macro implements the real logic that takes in trait body and user input (impl Input{})

I suppose the main reason why this RFC can't be too useful is that we still can't resolve the problem of crate-level custom attributes, which makes the most common use case (crate-level #[deny]s) infeasible.

I just don't feel that this is the right approach. It introduces an entirely new item, with entirely new syntax, using the currently unused (but reserved) keyword, with a lot of affordances that don't seem to be relevant to the proposal. Why do you want to allow passing arbitrary $($params:tt)* inside attribute aliases? What is the explicit use case where it makes the code much simpler? If you introduce tt matchers, does that mean that other macro matchers should be usable, now or in the future? What would be the semantics and use cases for that (given that attribute syntax doesn't deal with expressions, blocks, visibility etc)? If complex matchers aren't useful, why introduce the semantic and syntactic complexity of $($params:tt)* in the first place? Also, is there a use case where $($params:tt)* would be spliced in several different parts of an attribute list?

It's a very heavyweight solution for a relatively minor problem, with no clear reason to believe that it's the right solution, or that it can be extended in the future with more desired features (and there is always pressure to extend the language). It's also something which can be trivially accomplished with a proc macro. Now, proc macros are currently a bit of a pain to use, but it's not impossible that they become much more ergonomic in the future, and thus usable for such small ad hoc attributes.

On the other hand, perhaps we could extend macro_rules! macros to work in attribute position? I'm not aware why it wasn't done, other than nobody cared to do it. That alone would solve the issue at hand, you would just declare a macro, which accepts an item and spits it out with extra attributes attached.

2 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.