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

Fragment Specifiers for Generic Arguments #3442

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Changes from 2 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
155 changes: 155 additions & 0 deletions text/0000-fragment-specifiers-for-generic-arguments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
- Feature Name: fragment-specifiers-for-generic-arguments
- Start Date: 2023-05-31
- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000)
- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000)

# Summary
[summary]: #summary

Right now, there is no support for parsing the syntax of generic
parameters/arguments in declarative macros. This makes it difficult to
impossible to write a declarative macro that handles arbitrary generics easily.
I propose adding fragment specifiers to parse a generic parameter definition as
a whole and also parts of the definition.

# Motivation
[motivation]: #motivation

I personally encountered this issue when attempting to write a declarative macro
to implement a trait on a type. I wanted to write something like this minimal
toy example:

```rust
macro_rules! implement_debug {
{ $params:generic $type:ty } => {
impl $params Debug for $ty {
/* .. implementation goes here .. */
}
};
}

struct Container<'a, T>(&'a T);

implement_debug!(<'a, T: Debug + 'a> Container<'a, T>);
```

However, with the current state of declarative macros, there's no way to parse
arbitrary generic parameters in the body of the macro, forcing this macro of
mine to be a procedural macro. However, declarative macros are easier to read
and write, and this could be a declarative macro if there was only a way to
parse generic parameters.

Additionally, more complicated macros want to be able to parse each parameters
and its bounds for use in various places, so I'd like this as well:

```rust
macro_rules! implement_debug {
{ < $( $param:generic_param $( : $bound:generic_bound )? ),+ > $type:ty } => {
impl < $( $param $( : $bound )? ),+ > Debug for $ty {
/* .. implementation goes here .. */
}
};
}

struct Container<'a, T>(&'a T);

implement_debug!(<'a, T: Debug + 'a> Container<'a, T>);
```

Any toy example will be obviously redundant with `:generic`, but more involved
macros sometimes want it.

# Guide-level explanation
[guide-level-explanation]: #guide-level-explanation

When explaining fragment specifiers, this can be explained by adding the new
fragment specifiers and their descriptions:

* `:generic`: A full set of generic parameters and their bounds (e.g. `<'a, T:

Choose a reason for hiding this comment

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

maybe a nit but generic being singular confuses me. I think it would make more sense for it to be named generics or generic_set, or anything to add plurality to the concept, especially since it would be responsible for parsing the opening and closing angle brackets

'a + SomeTrait, const N: usize>`)
* `:generic_param`: A generic parameter (e.g. `'a`, `T`, or `const N`)
Copy link

@oxalica oxalica Jun 2, 2023

Choose a reason for hiding this comment

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

const N does not work for the macro example above, $param:generic_param $( : $bound:generic_bound )?. Const generics have the syntax const $param:ident : $ty:ty $(= $default:expr)?. Note that it is a type instead of generic bounds after the colon, so syntactically it should also accept non-trait-like types like const N: (i32, i32), though it's forbidden currently.

Copy link
Author

Choose a reason for hiding this comment

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

This was a deliberate deviation from the default syntax to make it easier to parse an arbitrary set of generic parameters. If we use the fragment specifiers as I defined them in the example macro defined in the RFC (< $( $param:generic_param $( : $bound:generic_bound )? ),+ > , then this successfully matches <const N: usize> ($param being const N and $bound being usize.

If it matches as you're requesting that it match, then the same pattern will match this trait, but it will also allow in nonsense like <const N: usize: 'a + SomeTrait> which my approach avoids. I'm not sure how one could change the pattern to avoid allowing for nonsense like that.

Though also, my approach would match <const N>, which is also nonsense, so idk which one would be better to reject while matching the macro. I'm open to changing it if you have arguments for your way of splitting the definition between the two fragment specifiers being better.

To your last point, the definition that I give for :generic_bounds does explicitly allow for a type, so that syntax would be accepted.

Copy link

@Aloso Aloso Jun 5, 2023

Choose a reason for hiding this comment

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

Your approach also matches const N: 'a and 'a: usize (see my comment below)

* `:generic_bound`: Bounds on a generic parameter (e.g. `'lifetime + SomeTrait`
on a type or `usize` on a const parameter).
* `:generic_default`: A default value for a generic type or lifetime

These four parameters are designed to make it easier to write declarative macros
that take in generic arguments (e.g. to use with a type or function), and then
use them to be generic on e.g. type definitions or `impl` blocks.

# Reference-level explanation
[reference-level-explanation]: #reference-level-explanation

Exact parsing behavior:
* `:generic` matches the
[`GenericParams`](https://doc.rust-lang.org/reference/items/generics.html)
grammar item.
* `:generic_param` matches any of a lifetime, an identifier, or `const` followed
by an identifier.
* `:generic_bound` matches the
[`TypeParamBounds`](https://doc.rust-lang.org/reference/trait-bounds.html)
(can be the bounds on a type parameter) or
[`LifetimeBounds`](https://doc.rust-lang.org/reference/trait-bounds.html) (can
be the bounds on a lifetime parameter) grammar items, or a type (can be the
bounds on a const parameter).
Copy link

Choose a reason for hiding this comment

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

Should it be named "generic_bound" or "generic_bounds"?
Also that TypeParamBounds does not allow empty, while the generic syntax do allow <T: /*nothing*/> (except for const generics). If we want it to match multiple bounds, it should also match no bounds.

Copy link
Author

Choose a reason for hiding this comment

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

Neat, I never knew that Rust allowed <T:> as a generic parameter list. That doesn't seem very useful to me, but I'll change it to allow that since I agree that it should be matched if Rust allows it.

* `:generic_default` matches a type (can be the default for a type parameter) or
anything that can be default for a const parameter (a block, an identifier, or
a literal).

All of these can potentially pick up on multiple tokens, so the result of any of
these parses is undestructible in the declarative macro.

Following behavior:
* `:generic` can be followed by anything, as it unambiguously ends when the
closing `>` appears.
* `:generic_param` is similarly bounded and so anything can follow it, as well.
* `:generic_bound` can be followed by anything that follows `:path` and `:ty`,
as it contains some repetition of lifetimes and paths separated by `+`, or a
type, and `+` is already illegal following a path or type.
* `:generic_default` can be followed by anything that follows `:ty`, since the
other options all have an unambiguous end.

# Drawbacks
[drawbacks]: #drawbacks

This provides more features which will need to be supported going forward. This
also provides another features which "macros 2.0" will need to implement for
parity with existing declarative macros.

As far as I can tell, this additional cost to implementing and maintaining extra
code is the only drawback associated with this feature.

# Rationale and alternatives
[rationale-and-alternatives]: #rationale-and-alternatives

We can do nothing, which provides no additional features and avoids the time and
effort cost of implementing and maintaining this feature.

# Prior art
[prior-art]: #prior-art

I'm not personally aware of any other languages that have similar declarative
macros to Rust with an equivalent to fragment specifiers, nor any prior effort
to add fragment specifiers covering this usage into Rust, so I don't know of any
prior art on this topic.

# Unresolved questions
[unresolved-questions]: #unresolved-questions

Are these the best fragment specifiers to use for parsing macros? I'd like
opinions from other people who write macros that would want something like this
about if breaking up generics into some other form might be more useful. I think
this set of fragment specifiers is the best for my use cases, but other people
might be interested in macros that parse differently and they might want other
things instead.

Also, should `:generic_bound` include the preceding `:` in the match (e.g.
`: 'a + SomeTrait` in the example above)? And likewise with the `=` before
`:generic_default`? I personally think it looks nicer without, but other people
may disagree with my aesthetic preferences.

# Future possibilities
[future-possibilities]: #future-possibilities

This could be combined with metavariable expressions for doing something with
them. I don't know what expressions would be useful for this, but other people
might have ideas.