From 192725239e7257b2f8707e48f85ca6fc2f05c41e Mon Sep 17 00:00:00 2001 From: Jarred Allen Date: Thu, 1 Jun 2023 13:41:15 -0700 Subject: [PATCH 1/4] Fragment Specifiers for Generic Arguments --- ...agment-specifiers-for-generic-arguments.md | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 text/0000-fragment-specifiers-for-generic-arguments.md diff --git a/text/0000-fragment-specifiers-for-generic-arguments.md b/text/0000-fragment-specifiers-for-generic-arguments.md new file mode 100644 index 00000000000..c1e53f231c6 --- /dev/null +++ b/text/0000-fragment-specifiers-for-generic-arguments.md @@ -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: + 'a + SomeTrait, const N: usize>`) +* `:generic_param`: A generic parameter (e.g. `'a`, `T`, or `const N`) +* `: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). +* `: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. From d77ac900befbfb6aeda4d9eea35ecadc57a392bd Mon Sep 17 00:00:00 2001 From: Jarred Allen Date: Thu, 1 Jun 2023 14:27:19 -0700 Subject: [PATCH 2/4] Reflow to fix inline code span --- text/0000-fragment-specifiers-for-generic-arguments.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/text/0000-fragment-specifiers-for-generic-arguments.md b/text/0000-fragment-specifiers-for-generic-arguments.md index c1e53f231c6..a99f3806709 100644 --- a/text/0000-fragment-specifiers-for-generic-arguments.md +++ b/text/0000-fragment-specifiers-for-generic-arguments.md @@ -142,8 +142,8 @@ 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 +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. From d666f15f16fcf7e98eb7fa54f1941ba5170adf12 Mon Sep 17 00:00:00 2001 From: Jarred Allen Date: Mon, 5 Jun 2023 16:18:25 -0700 Subject: [PATCH 3/4] Generic bounds may be empty --- ...0000-fragment-specifiers-for-generic-arguments.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/text/0000-fragment-specifiers-for-generic-arguments.md b/text/0000-fragment-specifiers-for-generic-arguments.md index a99f3806709..835c3d1fbf9 100644 --- a/text/0000-fragment-specifiers-for-generic-arguments.md +++ b/text/0000-fragment-specifiers-for-generic-arguments.md @@ -44,7 +44,7 @@ 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 } => { + { < $( $param:generic_param $( : $bound:generic_bounds )? ),+ > $type:ty } => { impl < $( $param $( : $bound )? ),+ > Debug for $ty { /* .. implementation goes here .. */ } @@ -68,7 +68,7 @@ fragment specifiers and their descriptions: * `:generic`: A full set of generic parameters and their bounds (e.g. `<'a, T: 'a + SomeTrait, const N: usize>`) * `:generic_param`: A generic parameter (e.g. `'a`, `T`, or `const N`) -* `:generic_bound`: Bounds on a generic parameter (e.g. `'lifetime + SomeTrait` +* `: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 @@ -85,12 +85,12 @@ Exact parsing behavior: grammar item. * `:generic_param` matches any of a lifetime, an identifier, or `const` followed by an identifier. -* `:generic_bound` matches the +* `: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). + 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). @@ -102,7 +102,7 @@ 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`, +* `: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 @@ -142,7 +142,7 @@ 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. +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. From 8db5d8debe2e2ae54831d9b08bf834111512f646 Mon Sep 17 00:00:00 2001 From: Jarred Allen Date: Mon, 5 Jun 2023 16:43:57 -0700 Subject: [PATCH 4/4] Add a fragment specifier for `where`-clauses --- ...agment-specifiers-for-generic-arguments.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/text/0000-fragment-specifiers-for-generic-arguments.md b/text/0000-fragment-specifiers-for-generic-arguments.md index 835c3d1fbf9..959cbd14221 100644 --- a/text/0000-fragment-specifiers-for-generic-arguments.md +++ b/text/0000-fragment-specifiers-for-generic-arguments.md @@ -21,16 +21,18 @@ toy example: ```rust macro_rules! implement_debug { - { $params:generic $type:ty } => { - impl $params Debug for $ty { + { $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 @@ -71,8 +73,9 @@ fragment specifiers and their descriptions: * `: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 four parameters are designed to make it easier to write declarative macros +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. @@ -94,6 +97,9 @@ Exact parsing behavior: * `: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. @@ -107,6 +113,11 @@ Following behavior: 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