diff --git a/text/3698-declarative-derive-macros.md b/text/3698-declarative-derive-macros.md new file mode 100644 index 00000000000..648557d969b --- /dev/null +++ b/text/3698-declarative-derive-macros.md @@ -0,0 +1,288 @@ +- Feature Name: `declarative_derive_macros` +- Start Date: 2024-09-20 +- RFC PR: [rust-lang/rfcs#3698](https://github.com/rust-lang/rfcs/pull/3698) +- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) + +# Summary +[summary]: #summary + +Support implementing `derive(Trait)` via a `macro_rules!` macro. + +# Motivation +[motivation]: #motivation + +Many crates support deriving their traits with `derive(Trait)`. 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 derives 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 + +You can define a macro to implement `derive(MyTrait)` by defining a +`macro_rules!` macro with one or more `derive()` rules. Such a macro can create +new items based on a struct, enum, or union. Note that the macro can only +append new items; it cannot modify the item it was applied to. + +For example: + +```rust +trait Answer { fn answer(&self) -> u32; } + +macro_rules! Answer { + // Simplified for this example + derive() (struct $n:ident $_:tt) => { + impl Answer for $n { + fn answer(&self) -> u32 { 42 } + } + }; +} + +#[derive(Answer)] +struct Struct; + +fn main() { + let s = Struct; + assert_eq!(42, s.answer()); +} +``` + +Derive 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. + +A derive macro may share the same path as a trait of the same name. For +instance, the name `mycrate::MyTrait` can refer to both the `MyTrait` trait and +the macro for `derive(MyTrait)`. This is consistent with existing derive +macros. + +If a derive macro emits a trait impl for the type, it may want to add the +[`#[automatically_derived]`](https://doc.rust-lang.org/reference/attributes/derive.html#the-automatically_derived-attribute) +attribute, for the benefit of diagnostics. + +If a derive macro mistakenly emits the token stream it was applied to +(resulting in a duplicate item definition), the error the compiler emits for +the duplicate item should hint to the user that the macro was defined +incorrectly, and remind the user that derive macros only append new items. + +A `derive()` rule can be marked as `unsafe`: `unsafe derive() (...) +=> { ... }`. Invoking such a derive using a rule marked as unsafe +requires unsafe derive syntax: either +`#[unsafe(derive(DangerousTrait))]` or +`#[derive(unsafe(DangerousTrait))]`. (The latter syntax allows +isolating the `unsafe` to a single derive within a list of +derives.) Invoking an unsafe derive rule without the unsafe derive +syntax will produce a compiler error. Using the unsafe derive +syntax without an unsafe derive will trigger an "unused unsafe" +lint. (RFC 3715 defines the equivalent mechanism for proc macro +derives.) + +# Reference-level explanation +[reference-level-explanation]: #reference-level-explanation + +The grammar for macros is extended as follows: + +> _MacroRule_ :\ +>    ( `unsafe`? `derive` `(` `)` )? _MacroMatcher_ `=>` _MacroTranscriber_ + +The _MacroMatcher_ matches the entire construct the attribute was +applied to, receiving precisely what a proc-macro-based attribute +would in the same place. + +(The empty parentheses after `derive` reserve future syntax space +for derives accepting arguments, at which time they'll be replaced +by a second _MacroMatcher_ that matches the arguments.) + +A derive invocation that uses an `unsafe derive` rule will produce +an error if invoked without using the `unsafe` derive syntax. A +derive invocation that uses an `derive` rule (without `unsafe`) +will trigger the "unused unsafe" lint if invoked using the `unsafe` +derive syntax. A single derive macro may have both `derive` and +`unsafe derive` 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 `derive` or `unsafe`. + +Adding `derive` rules to an existing macro is a semver-compatible change, +though in practice, it will likely be uncommon. + +If a user invokes a macro as a derive and that macro does not have any `derive` +rules, the compiler should give a clear error stating that the macro is not +usable as a derive because it does not have any `derive` rules. + +# 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. We can and should attempt +to avert and such pressure, such as by providing a post with guidance that +crate maintainers can link to when responding to such requests. + +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 derive 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! { ... }`, rather than using +derives. 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 derive. In addition, this would +not preserve the property derive macros normally have that they cannot change +the item they are applied to. + +A mechanism to define attribute macros would let people write attributes like +`#[derive_mytrait]`, but that would not provide compatibility with existing +derive syntax. + +We could allow `macro_rules!` derive macros to emit a replacement token stream. +That would be inconsistent with the restriction preventing proc macros from +doing the same, but it would give macros more capabilities, and simplify some +use cases. Notably, that would make it easy for derive macros to re-emit a +structure with another `derive` attached to it. + +We could allow directly invoking a `macro_rules!` derive macro as a +function-like macro. This has the potential for confusion, given the +append-only nature of derive macros versus the behavior of normal function-like +macros. It might potentially be useful for code reuse, however. + +## Syntax alternatives + +Rather than using `derive()` rules, we could have `macro_rules!` macros use a +`#[macro_derive]` attribute, similar to the `#[proc_macro_derive]` attribute +used for proc macros. + +However, this would be inconsistent with `attr()` rules as defined in RFC 3697. +This would also make it harder to add parameterized derives in the future (e.g. +`derive(MyTrait(params))`). + +# Prior art +[prior-art]: #prior-art + +We have had proc-macro-based derive 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 derives, +demonstrating a demand for this. This feature would allow defining such derives +without requiring proc macros at all, and would support the same invocation +syntax as a proc macro. + +Some crates in the ecosystem already implement the equivalent of derives using +declarative macros; for instance, see +[merde](https://github.com/bearcove/merde). + +# Unresolved questions +[unresolved-questions]: #unresolved-questions + +Before stabilizing this feature, we should ensure there's a mechanism macros +can use to ensure that an error when producing an impl does not result in a +cascade of additional errors caused by a missing impl. This may take the form +of a fallback impl, for instance. + +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. + +Before stabilizing this feature, we should have clear public guidance +recommending against pressuring crate maintainers to adopt this feature +rapidly, and encourage crate maintainers to link to that guidance if such +requests arise. + +# Future possibilities +[future-possibilities]: #future-possibilities + +We should provide a way for derive macros to declare themselves `unsafe` to +invoke, requiring an unsafe attribute syntax to invoke. + +We should provide a way for derive macros to invoke other derive macros. + +We should provide a means to perform a `derive` on a struct without being +directly attached to that struct. (This would also potentially require +something like a compile-time reflection mechanism.) + +We could support passing parameters to derive macros (e.g. +`#[derive(Trait(params), OtherTrait(other, params))]`). This may benefit from +having `derive(...)` rules inside the `macro_rules!` macro declaration, similar +to the `attr(...)` rules proposed in RFC 3697. + +In the future, if we support something like `const Trait` or similar trait +modifiers, we'll want to support `derive(const Trait)`, and define how a +`macro_rules!` derive handles that. + +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 support error recovery, so that a derive can produce an error +but still provide enough for the remainder of the compilation to proceed far +enough to usefully report further errors. + +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. + +We could provide a macro matcher to match an entire struct field, along with +syntax (based on macro metavariable expressions) to extract the field name or +type (e.g. `${f.name}`). This would simplify many common cases by leveraging +the compiler's own parser. + +We could do the same for various other high-level constructs. + +We may want to provide simple helpers for generating/propagating `where` +bounds, which would otherwise be complex to do in a `macro_rules!` macro. + +We may want to add a lint for macro names, encouraging macros with derive rules +to use `CamelCase` names, and encouraging macros without derive rules to use +`snake_case` names. + +## Helper attribute namespacing and hygiene + +We should provide a way for derive macros to define helper attributes ([inert +attributes](https://doc.rust-lang.org/reference/attributes.html#active-and-inert-attributes) +that exist for the derive macro to parse and act upon). Such attributes are +supported by proc macro derives; however, such attributes have no namespacing, +and thus currently represent compatibility hazards because they can conflict. +We should provide a namespaced, hygienic mechanism for defining and using +helper attributes. + +For instance, could we have `pub macro_helper_attr! skip` in the standard +library, namespaced under `core::derives` or similar? Could we let macros parse +that in a way that matches it in a namespaced fashion, so that: +- If you write `#[core::derives::skip]`, the macro matches it +- If you `use core::derives::skip;` and `write #[skip]`, the macro matches it +- If you `use elsewhere::skip` (or no import at all) and write `#[skip]`, the + macro *doesn't* match it. + +We already have *some* interaction between macros and name resolution, in order +to have namespaced `macro_rules!` macros. Would something like this be feasible? + +(We would still need to specify the exact mechanism by which macros match these +helper attributes.)