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 all 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
166 changes: 166 additions & 0 deletions text/0000-fragment-specifiers-for-generic-arguments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
- 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 $( where $where:where_clause )? } => {
impl $params Debug for $ty $( where $where )? {
/* .. implementation goes here .. */
}
};
}

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

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

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_bounds )? ),+ > $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_bounds`: 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
* `:where_clause`: A where clause providing constraints on generic parameters

These five 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_bounds` 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). It may also match nothing.
* `: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).
* `:where_clause` matches a
[`WhereClause`](https://doc.rust-lang.org/reference/items/generics.html#where-clauses)
excluding the initial `where` token.

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_bounds` 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.
* `:where_clause` can be followed by anything that follows `:generic_bounds`
except not a `,`, as it contains a comma-separated repetition whose terms end
in a generic bound, so we need to tell that the generic bound is ending, and
we can't have a following comma (because then we don't know if there's another
repetition).

# 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_bounds` 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.