From 2b3e5fbbd2ae2cf4721de7a816814126b0766a1d Mon Sep 17 00:00:00 2001 From: Vadim Petrochenkov Date: Sat, 16 Dec 2017 19:23:53 +0300 Subject: [PATCH 1/5] Finalize syntax of `impl Trait` and `dyn Trait` with multiple bounds --- text/0000-finalize-impl-dyn-syntax.md | 159 ++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 text/0000-finalize-impl-dyn-syntax.md diff --git a/text/0000-finalize-impl-dyn-syntax.md b/text/0000-finalize-impl-dyn-syntax.md new file mode 100644 index 00000000000..a7f3408630e --- /dev/null +++ b/text/0000-finalize-impl-dyn-syntax.md @@ -0,0 +1,159 @@ +- Feature Name: N/A +- Start Date: 2017-12-16 +- RFC PR: (leave this empty) +- Rust Issue: (leave this empty) + +# Summary +[summary]: #summary + +Finalize syntax of `impl Trait` and `dyn Trait` with multiple bounds before +stabilization of these features. + +# Motivation +[motivation]: #motivation + +Current priority of `+` in `impl Trait1 + Trait2` / `dyn Trait1 + Trait2` brings +inconsistency in the type grammar. +This RFC outlines possible syntactic +alternatives and suggests one of them for stabilization. + +# Guide-level explanation +[guide-level-explanation]: #guide-level-explanation + +"Alternative 3" (see reference-level explanation) is selected for stabilization. + +`impl Trait1 + Trait2` / `dyn Trait1 + Trait2` now require parentheses in all +contexts - `impl(Trait1 + Trait2)` / `dyn(Trait1 + Trait2)`, similarly to other +unary operators, e.g. `&(Trait1 + Trait2)`. + +# Reference-level explanation +[reference-level-explanation]: #reference-level-explanation + +## Current situation + +In the current implementation when we see `impl` or `dyn` we start parsing +following bounds separated by `+`s greedily regardless of context, so `+` +effectively gets the strongest priority. + +So, for example: +- `&dyn A + B` is parsed as `&(dyn A + B)` +- `Fn() -> impl A + B` is parsed as `Fn() -> (impl A + B)` +- `x as &dyn A + y` is parsed as `x as &(dyn A + y)`. + +Compare this with parsing of trait object types without prefixes +([RFC 438](https://github.com/rust-lang/rfcs/pull/438)): +- `&A + B` is parsed as `(&A) + B` and is an error +- `Fn() -> A + B` is parsed as `(Fn() -> A) + B` +- `x as &A + y` is parsed as `(x as &A) + y` + +In general, binary operations like `+` have lower priority than unary operations +in all contexts - expressions, patterns, types. So the priorities as implemented +bring inconsistency and may break intuition. + +## Alternative 1: high priority `+` (status quo) + +Pros: +- The greedy parsing with high priority of `+` after `impl` / `dyn` +has one benefit - it requires the least amout of parentheses from all the +alternatives. +Parentheses are needed only when the greedy behaviour needs to be prevented, +e.g. `Fn() -> &(dyn Write) + Send`, this doesn't happen often. + +Cons: +- Inconsistent and possibly surprising operator priorities. +- `impl` / `dyn` is a somewhat weird syntactic construction, it's not an usual +unary operator, its a prefix describing how to interpret the following tokens. +In particular, if the `impl A + B` needs to be parenthesized for some reason, +it needs to be done like this `(impl A + B)`, and not `impl (A + B)`. The second +variant is a parsing error, but some people find it surprising and expect it to +work, as if `impl` were an unary operator. + +## Alternative 2: low priority `+` + +Basically, `impl A + B` is parsed using same rules as `A + B`. + +If `impl A + B` is located inside a higher priority operator like `&` it has +to be parenthesized. +If it is located at intersection of type and expressions +grammars like `expr1 as Type + expr2`, it has to be parenthesized as well. + +`&dyn A + B` / `Fn() -> impl A + B` / `x as &dyn A + y` has to be rewritten as +`&(dyn A + B)` / `Fn() -> (impl A + B)` / `x as &(dyn A + y)` respectively. + +One location must be mentioned specially, the location in a function return +type: +```rust +fn f() -> impl A + B +{ + // Do things +} +``` +This is probably the most common location for `impl Trait` types. +In theory, it doesn't require parentheses in any way - it's not inside of an +unary operator and it doesn't cross expression boundaries. +However, it creates a bit of percieved inconsistency with function-like traits +and function pointers that do require parentheses for `impl Trait` in return +types (`Fn() -> (impl A + B)` / `fn() -> (impl A + B)`) because they, in their +turn, can appear inside of unary operators and casts. +So, if avoiding this is considered more important than ergonomics, then +we can require parentheses in function definitions as well. +```rust +fn f() -> (impl A + B) +{ + // Do things +} +``` + +Pros: +- Consistent priorities of binary and unary operators. +- Parentheses are required relatively rarely (unless we require them in +function definitions as well). + +Cons: +- More parentheses than in the "Alternative 1". +- `impl` / `dyn` is still a somewhat weird prefix construction and `dyn (A + B)` +is not a valid syntax. + +## Alternative 3: Unary operator + +`impl` and `dyn` can become usual unary operators in type grammar like `&` or +`*const`. +Their application to any other types except for (possibly parenthesized) paths +(single `A`) or "legacy trait objects" (`A + B`) becomes an error, but this +could be changed in the future if some other use is found. + +`&dyn A + B` / `Fn() -> impl A + B` / `x as &dyn A + y` has to be rewritten as +`&dyn(A + B)` / `Fn() -> impl(A + B)` / `x as &dyn(A + y)` respectively. + +Function definitions with `impl A + B` in return type have to be rewritten too. +```rust +fn f() -> impl(A + B) +{ + // Do things +} +``` + +Pros: +- Consistent priorities of binary and unary operators. +- `impl` / `dyn` are usual unary operators, `dyn (A + B)` is a valid syntax. + +Cons: +- The largest amount of parentheses, parentheses are always required. +Parentheses are noise, there may be even less desire to use `dyn` in trait +objects now, if something like `Box` turns into +`Box`. + +# Drawbacks +[drawbacks]: #drawbacks + +See above. + +# Rationale and alternatives +[alternatives]: #alternatives + +See above. + +# Unresolved questions +[unresolved]: #unresolved-questions + +None. From a4934a2ee1a2a8c70663e9848b93bc0de850df8f Mon Sep 17 00:00:00 2001 From: Vadim Petrochenkov Date: Mon, 1 Jan 2018 20:09:16 +0300 Subject: [PATCH 2/5] Incorporate feedback, use Alternative 2 as the primary alternative --- text/0000-finalize-impl-dyn-syntax.md | 49 +++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/text/0000-finalize-impl-dyn-syntax.md b/text/0000-finalize-impl-dyn-syntax.md index a7f3408630e..d5c8edeabc6 100644 --- a/text/0000-finalize-impl-dyn-syntax.md +++ b/text/0000-finalize-impl-dyn-syntax.md @@ -20,11 +20,12 @@ alternatives and suggests one of them for stabilization. # Guide-level explanation [guide-level-explanation]: #guide-level-explanation -"Alternative 3" (see reference-level explanation) is selected for stabilization. +"Alternative 2" (see reference-level explanation) is selected for stabilization. `impl Trait1 + Trait2` / `dyn Trait1 + Trait2` now require parentheses in all -contexts - `impl(Trait1 + Trait2)` / `dyn(Trait1 + Trait2)`, similarly to other -unary operators, e.g. `&(Trait1 + Trait2)`. +contexts where they are used inside of unary operators `&(impl Trait1 + Trait2)` +/ `&(dyn Trait1 + Trait2)`, similarly to trait object types without +prefix, e.g. `&(Trait1 + Trait2)`. # Reference-level explanation [reference-level-explanation]: #reference-level-explanation @@ -46,6 +47,11 @@ Compare this with parsing of trait object types without prefixes - `Fn() -> A + B` is parsed as `(Fn() -> A) + B` - `x as &A + y` is parsed as `(x as &A) + y` +Also compare with unary operators in bounds themselves: +- `for<'a> A<'a> + B` is parsed as `(for<'a> A<'a>) + B`, +not `for<'a> (A<'a> + B)` +- `?A + B` is parsed as `(?A) + B`, not `?(A + B)` + In general, binary operations like `+` have lower priority than unary operations in all contexts - expressions, patterns, types. So the priorities as implemented bring inconsistency and may break intuition. @@ -143,6 +149,43 @@ Parentheses are noise, there may be even less desire to use `dyn` in trait objects now, if something like `Box` turns into `Box`. +## Other alternatives + +Two separate grammars can be used depending on context +(https://github.com/rust-lang/rfcs/pull/2250#issuecomment-352435687) - +Alternative 1/2 in lists of arguments like `Box` or +`Fn(impl A + B, impl A + B)`, and Alternative 3 otherwise (`&dyn (A + B)`). + +## Compatibility + +The alternatives are ordered by strictness from the most relaxed Alternative 1 +to the strictest Alternative 3, but switching from more strict alternatives to +less strict is not exactly backward-compatible. + +Switching from 2/3 to 1 can change meaning of legal code in rare cases. +Switching from 3 to 2/1 requires keeping around the syntax with parentheses +after `impl` / `dyn`. + +Alternative 2 can be backward-compatibly extended to "relaxed 3" in which +parentheses like `dyn (A + B)` are permitted, but technically unnecessary. +Such parens may keep people expecting `dyn (A + B)` to work happy, but +complicate parsing by introducing more ambiguities to the grammar. + +## Experimental check + +An application of all the alternatives to rustc and libstd codebase can be found +in [this branch](https://github.com/petrochenkov/rust/commits/impldyntest). +The first commit is the baseline (Alternative 1) and the next commits show +changes required to move to Alternatives 2 and 3. Alternative 2 requires fewer +changes compared to Alternative 3. + +As the RFC author interprets it, the Alternative 3 turns out to be impractical +due to common use of `Box`es and other contexts where the parens are technically +unnecessary, but required by Alternative 3. +The number of parens required by Alternative 2 is limited and they seem +appropriate because they follow "normal" priorities for unary and binary +operators. + # Drawbacks [drawbacks]: #drawbacks From 36da3780dc22d03018dca8fab29884465ce432aa Mon Sep 17 00:00:00 2001 From: Vadim Petrochenkov Date: Wed, 31 Jan 2018 02:50:42 +0300 Subject: [PATCH 3/5] Add a paragraph about non-associativity --- text/0000-finalize-impl-dyn-syntax.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/text/0000-finalize-impl-dyn-syntax.md b/text/0000-finalize-impl-dyn-syntax.md index d5c8edeabc6..2f5c02a4166 100644 --- a/text/0000-finalize-impl-dyn-syntax.md +++ b/text/0000-finalize-impl-dyn-syntax.md @@ -27,6 +27,13 @@ contexts where they are used inside of unary operators `&(impl Trait1 + Trait2)` / `&(dyn Trait1 + Trait2)`, similarly to trait object types without prefix, e.g. `&(Trait1 + Trait2)`. +Additionally, parentheses are required in all cases where `+` in `impl` or `dyn` +is ambiguous. +For example, `Fn() -> impl A + B` can be interpreted as both +`(Fn() -> impl A) + B` (low priority plus) or `Fn() -> (impl A + B)` (high +priority plus), so we are refusing to disambiguate and require explicit +parentheses. + # Reference-level explanation [reference-level-explanation]: #reference-level-explanation @@ -171,6 +178,18 @@ parentheses like `dyn (A + B)` are permitted, but technically unnecessary. Such parens may keep people expecting `dyn (A + B)` to work happy, but complicate parsing by introducing more ambiguities to the grammar. +While unary operators like `&` "obviously" have higher priority than `+`, +cases like `Fn() -> impl A + B` are not so obvious. +The Alternative 2 considers "low priority plus" to have lower priority than `Fn` +, so `Fn() -> impl A + B` can be treated as `(Fn() -> impl A) + B`, however +it may be more intuitive and consistent with `fn` items to make `+` have higher +priority than `Fn` (but still lower priority than `&`). +As an immediate solution we refuse to disambiguate this case and treat +`Fn() -> impl A + B` as an error, so we can change the rules in the future and +interpret `Fn() -> impl A + B` (and maybe even `Fn() -> A + B` after long +deprecation period) as `Fn() -> (impl A + B)` (and `Fn() -> (A + B)`, +respectively). + ## Experimental check An application of all the alternatives to rustc and libstd codebase can be found From f393ff90414177ffa05cb24fc3a733f9c70d9b81 Mon Sep 17 00:00:00 2001 From: Mazdak Farrokhzad Date: Tue, 27 Feb 2018 23:26:58 +0100 Subject: [PATCH 4/5] RFC 2250 --- text/0000-finalize-impl-dyn-syntax.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/text/0000-finalize-impl-dyn-syntax.md b/text/0000-finalize-impl-dyn-syntax.md index 2f5c02a4166..51069b12bf5 100644 --- a/text/0000-finalize-impl-dyn-syntax.md +++ b/text/0000-finalize-impl-dyn-syntax.md @@ -1,7 +1,7 @@ - Feature Name: N/A - Start Date: 2017-12-16 -- RFC PR: (leave this empty) -- Rust Issue: (leave this empty) +- RFC PR: [rust-lang/rfcs#2250](https://github.com/rust-lang/rfcs/pull/2250) +- Rust Issue: [rust-lang/rust#34511](https://github.com/rust-lang/rust/issues/34511) # Summary [summary]: #summary From 8a889530e27b9625f89a6d869734f595b618b584 Mon Sep 17 00:00:00 2001 From: Mazdak Farrokhzad Date: Tue, 27 Feb 2018 23:29:08 +0100 Subject: [PATCH 5/5] Rename 0000-finalize-impl-dyn-syntax.md to 2250-finalize-impl-dyn-syntax.md --- ...nalize-impl-dyn-syntax.md => 2250-finalize-impl-dyn-syntax.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename text/{0000-finalize-impl-dyn-syntax.md => 2250-finalize-impl-dyn-syntax.md} (100%) diff --git a/text/0000-finalize-impl-dyn-syntax.md b/text/2250-finalize-impl-dyn-syntax.md similarity index 100% rename from text/0000-finalize-impl-dyn-syntax.md rename to text/2250-finalize-impl-dyn-syntax.md