diff --git a/text/3697-declarative-attribute-macros.md b/text/3697-declarative-attribute-macros.md new file mode 100644 index 00000000000..8f9d9cb855d --- /dev/null +++ b/text/3697-declarative-attribute-macros.md @@ -0,0 +1,222 @@ +- Feature Name: `declarative_attribute_macros` +- Start Date: 2024-09-20 +- RFC PR: [rust-lang/rfcs#3697](https://github.com/rust-lang/rfcs/pull/3697) +- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) + +# Summary +[summary]: #summary + +Support defining `macro_rules!` macros that work as attribute macros. + +# Motivation +[motivation]: #motivation + +Many crates provide attribute macros. Today, this requires defining proc +macros, in a separate crate, typically with several additional dependencies +adding substantial compilation time, and typically guarded by a feature that +users need to remember to enable. + +However, many common cases of attribute macros don't require any more power +than an ordinary `macro_rules!` macro. Supporting these common cases would +allow many crates to avoid defining proc macros, reduce dependencies and +compilation time, and provide these macros unconditionally without requiring +the user to enable a feature. + +# Guide-level explanation +[guide-level-explanation]: #guide-level-explanation + +When defining a `macro_rules!` macro, you can prefix some of the macro's rules +with `attr(...)` to allow using the macro as an attribute. The +arguments to the attribute, if any, are parsed by the *MacroMatcher* in the +first set of parentheses; the second *MacroMatcher* parses the entire construct +the attribute was applied to. The resulting macro will work anywhere an +attribute currently works. + +```rust +macro_rules! main { + attr() ($func:item) => { make_async_main!($func) }; + attr(threads = $threads:literal) ($func:item) => { make_async_main!($threads, $func) }; +} + +#[main] +async fn main() { ... } + +#[main(threads = 42)] +async fn main() { ... } +``` + +Attribute macros defined using `macro_rules!` follow the same scoping rules as +any other macro, and may be invoked by any path that resolves to them. + +An attribute macro must not require itself for resolution, either directly or +indirectly (e.g. applied to a containing module or item). + +Note that a single macro can have both `attr` and non-`attr` rules. Attribute +invocations can only match the `attr` rules, and non-attribute invocations can +only match the non-`attr` rules. This allows adding `attr` rules to an existing +macro without breaking backwards compatibility. + +An attribute macro may emit code containing another attribute, including one +provided by an attribute macro. An attribute macro may use this to recursively +invoke itself. + +An `attr` rule may be prefixed with `unsafe`. Invoking an attribute macro in a +way that makes use of a rule declared with `unsafe attr` requires the unsafe +attribute syntax `#[unsafe(attribute_name)]`. + +# Reference-level explanation +[reference-level-explanation]: #reference-level-explanation + +The grammar for macros is extended as follows: + +> _MacroRule_ :\ +>    ( `unsafe`? `attr` _MacroMatcher_ )? _MacroMatcher_ `=>` _MacroTranscriber_ + +The first _MacroMatcher_ matches the attribute's arguments, which will be an +empty token tree if either not present (`#[myattr]`) or empty (`#[myattr()]`). +The second _MacroMatcher_ matches the entire construct the attribute was +applied to, receiving precisely what a proc-macro-based attribute would in the +same place. + +Only a rule matching both the arguments to the attribute and the construct the +attribute was applied to will apply. Note that the captures in both +`MacroMatcher`s share the same namespace; attempting to use the same name for +two captures will give a "duplicate matcher binding" error. + +An attribute macro invocation that uses an `unsafe attr` rule will produce an +error if invoked without using the `unsafe` attribute syntax. An attribute +macro invocation that uses an `attr` rule will trigger the "unused unsafe" lint +if invoked using the `unsafe` attribute syntax. A single attribute macro may +have both `attr` and `unsafe attr` rules, such as if only some invocations are +unsafe. + +This grammar addition is backwards compatible: previously, a _MacroRule_ could +only start with `(`, `[`, or `{`, so the parser can easily distinguish rules +that start with `attr` or `unsafe`. + +Attribute macros declared using `macro_rules!` are +[active](https://doc.rust-lang.org/reference/attributes.html#active-and-inert-attributes), +just like those declared using proc macros. + +Adding `attr` rules to an existing macro is a semver-compatible change. + +# Drawbacks +[drawbacks]: #drawbacks + +This feature will not be sufficient for *all* uses of proc macros in the +ecosystem, and its existence may create social pressure for crate maintainers +to switch even if the result is harder to maintain. + +Before stabilizing this feature, we should receive feedback from crate +maintainers, and potentially make further improvements to `macro_rules` to make +it easier to use for their use cases. This feature will provide motivation to +evaluate many new use cases that previously weren't written using +`macro_rules`, and we should consider quality-of-life improvements to better +support those use cases. + +# Rationale and alternatives +[rationale-and-alternatives]: #rationale-and-alternatives + +Adding this feature will allow many crates in the ecosystem to drop their proc +macro crates and corresponding dependencies, and decrease their build times. + +This will also give attribute macros access to the `$crate` mechanism to refer +to the defining crate, which is simpler than mechanisms currently used in proc +macros to achieve the same goal. + +Macros defined this way can more easily support caching, as they cannot depend +on arbitrary unspecified inputs. + +Crates could instead define `macro_rules!` macros and encourage users to invoke +them using existing syntax like `macroname! { ... }`. This would provide the +same functionality, but would not support the same syntax people are accustomed +to, and could not maintain semver compatibility with an existing +proc-macro-based attribute. + +We could require the `!` in attribute macros (`#[myattr!]` or similar). +However, proc-macro-based attribute macros do not require this, and this would +not allow declarative attribute macros to be fully compatible with +proc-macro-based attribute macros. + +Many macros will want to parse their arguments and separately parse the +construct they're applied to, rather than a combinatorial explosion of both. +This problem is not unique to attribute macros. In both cases, the standard +solution is to parse one while carrying along the other. + +We could leave out support for writing a function-like macro and an attribute +macro with the same name. However, this would prevent crates from preserving +backwards compatibility when adding attribute support to an existing +function-like macro. + +Instead of or in addition to marking the individual rules, we could mark the +whole macro with `#[attribute_macro]` or similar, and allow having an attribute +macro and a non-attribute macro with the same name. + +We could include another `=>` or other syntax between the first and second +macro matchers. + +We could use `attribute` rather than `attr`. Rust usually avoids abbreviating +except for the most common constructs; however, `cfg_attr` provides precedent +for this abbreviation, and `attr` appears repeatedly in multiple rules which +motivates abbreviating it. + +# Prior art +[prior-art]: #prior-art + +We have had proc-macro-based attribute macros for a long time, and the +ecosystem makes extensive use of them. + +The [`macro_rules_attribute`](https://crates.io/crates/macro_rules_attribute) +crate defines proc macros that allow invoking declarative macros as attributes, +demonstrating a demand for this. This feature would allow defining such +attributes without requiring proc macros at all, and would support the same +invocation syntax as a proc macro. + +Some macros in the ecosystem already implement the equivalent of attribute +using declarative macros; for instance, see +[smol-macros](https://crates.io/crates/smol-macros), which provides a `main!` +macro and recommends using it with `macro_rules_attribute::apply`. + +# Unresolved questions +[unresolved-questions]: #unresolved-questions + +Is an attribute macro allowed to recursively invoke itself by emitting the +attriute in its output? If there is no technical issue with allowing this, then +we should do so, to allow simple recursion (e.g. handling defaults by invoking +the same rule as if they were explicitly specified). + +Before stabilizing this feature, we should make sure it doesn't produce wildly +worse error messages in common cases. + +Before stabilizing this feature, we should receive feedback from crate +maintainers, and potentially make further improvements to `macro_rules` to make +it easier to use for their use cases. This feature will provide motivation to +evaluate many new use cases that previously weren't written using +`macro_rules`, and we should consider quality-of-life improvements to better +support those use cases. + +# Future possibilities +[future-possibilities]: #future-possibilities + +We should provide a way to define `derive` macros declaratively, as well. + +We should provide a way for `macro_rules!` macros to provide better error +reporting, with spans, rather than just pointing to the macro. + +We may want to provide more fine-grained control over the requirement for +`unsafe`, to make it easier for attribute macros to be safe in some +circumstances and unsafe in others (e.g. unsafe only if a given parameter is +provided). + +As people test this feature and run into limitations of `macro_rules!` parsing, +we should consider additional features to make this easier to use for various +use cases. + +Some use cases involve multiple attribute macros that users expect to be able +to apply in any order. For instance, `#[test]` and `#[should_panic]` can appear +on the same function in any order. Implementing that via this mechanism for +attribute macros would require making both of those attributes into macros that +both do all the parsing regardless of which got invoked first, likely by +invoking a common helper. We should consider if we consider that mechanism +sufficient, or if we should provide another mechanism for a set of related +attribute macros to appear in any order.