Skip to content

Implement asymmetrical precedence for closures and jumps #134847

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

Merged
merged 1 commit into from
Jun 13, 2025

Conversation

dtolnay
Copy link
Member

@dtolnay dtolnay commented Dec 28, 2024

I have been through a series of asymmetrical precedence designs in Syn, and finally have one that I like and is worth backporting into rustc. It is based on just 2 bits of state: next_operator_can_begin_expr and next_operator_can_continue_expr.

Asymmetrical precedence is the thing that enables (return 1) + 1 to require parentheses while 1 + return 1 does not, despite + always having stronger precedence than return according to the Rust Reference. This is facilitated by next_operator_can_continue_expr.

Relatedly, it is the thing that enables (return) - 1 to require parentheses while return + 1 does not, despite + and - having exactly the same precedence. This is facilitated by next_operator_can_begin_expr.

Example:

macro_rules! repro {
    ($e:expr) => {
        $e - $e;
        $e + $e;
    };
}

fn main() {
    repro!{return}
    repro!{return 1}
}

-Zunpretty=expanded Before:

fn main() {
    (return) - (return);
    (return) + (return);
    (return 1) - (return 1);
    (return 1) + (return 1);
}

After:

fn main() {
    (return) - return;
    return + return;
    (return 1) - return 1;
    (return 1) + return 1;
}

@dtolnay dtolnay added the A-pretty Area: Pretty printing (including `-Z unpretty`) label Dec 28, 2024
@rustbot
Copy link
Collaborator

rustbot commented Dec 28, 2024

r? @fmease

rustbot has assigned @fmease.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Dec 28, 2024
@fmease
Copy link
Member

fmease commented Dec 28, 2024

I'm going to look at this in a few hours.

@dtolnay
Copy link
Member Author

dtolnay commented Dec 28, 2024

One note on testing: the best approach I've found in Syn for thoroughly testing the pretty-printing algorithm and parenthesis insertion is the following pair of tests:

  • The test from Add pretty-printer parenthesis insertion test #133730. This test is able to assert that for any expression parsed from source code, when re-printing it, the pretty-printer's parenthesis insertion never kicks in. We know this because we just parsed that expression; extra parenthesis cannot be necessary to disambiguate it. In Syn we run this test by scraping every expression from every Rust file in the rust-lang/rust repo. In rustc, for now it just runs over the short list of expression edge cases in that test file. This test is designed to rule out false positives (parenthesis inserted where it shouldn't need to be). The implementation in rustc is augmented with a step to also check for false negatives to make the same edge cases list more useful for both kinds of fixes.

  • A test similar to pprust-expr-roundtrip.rs, except actually rigorous. In Syn this is test_permutations. This test programmatically generates all possible improperly parenthesized expression trees, and asserts that they can be parsed back after printing. This test is designed to rule out false negatives (missing parenthesis).

For the second test, a major difference between Syn's and rustc's is this:

// Ignore expressions with chained comparisons that fail to parse
if let Some(mut parsed) = parse_expr(&psess, &printed) {

Rustc's test just throws up its hands and says "sometimes the printer will just generate invalid syntax". (It is not just the one case mentioned in the comment.)

Once the pretty-printer is in better shape, the goal will be to turn this into an unwrap, effectively asserting that the pretty-printer always produces syntactically valid Rust output for any arbitrary syntax tree input that a macro could have generated. That will provide exhaustive coverage of the edge cases. Syn's printer is tested this way.

In rustc, for now there are still too many false positives and false negatives to easily make an exhaustive test of the set of expressions that currently happen to work. Fixing false positives (like this PR) is an important prerequisite to being able to fix certain false negatives without indiscriminately inserting way too many parentheses. Meanwhile, PRs like #134661 and #134600 have been fixing false negatives.

@bors
Copy link
Collaborator

bors commented Feb 11, 2025

☔ The latest upstream changes (presumably #127541) made this pull request unmergeable. Please resolve the merge conflicts.

@dtolnay
Copy link
Member Author

dtolnay commented Feb 18, 2025

Rebased to resolve conflict with #127541 in tests/ui/invalid/invalid-rustc_legacy_const_generics-issue-123077.stderr.

@wesleywiser
Copy link
Member

Hi @fmease, could you take another look at this PR when you get a chance? Thanks!

@dtolnay
Copy link
Member Author

dtolnay commented Apr 5, 2025

@dtolnay dtolnay linked an issue Apr 5, 2025 that may be closed by this pull request
@bors
Copy link
Collaborator

bors commented May 2, 2025

☔ The latest upstream changes (presumably #140581) made this pull request unmergeable. Please resolve the merge conflicts.

@dtolnay
Copy link
Member Author

dtolnay commented May 4, 2025

@jieyouxu jieyouxu added the A-parser Area: The lexing & parsing of Rust source code to an AST label Jun 12, 2025
@wesleywiser
Copy link
Member

r? rust-lang/compiler

@rustbot rustbot assigned petrochenkov and unassigned fmease Jun 12, 2025
@fmease fmease assigned fmease and unassigned petrochenkov Jun 12, 2025
Copy link
Member

@fmease fmease left a comment

Choose a reason for hiding this comment

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

I'm very sorry for the long wait. The impl looks good, thanks!

I've also performed a lot of testing locally and everything seems to check out.

| ExprKind::Yield(YieldKind::Prefix(..))
| ExprKind::Range(None, ..) = expr.kind
{
return ExprPrecedence::Prefix;
Copy link
Member

@fmease fmease Jun 12, 2025

Choose a reason for hiding this comment

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

I keep wondering if this could be relaxed to ExprPrecedence::Unambiguous. I wasn't able to find any counterexamples for doing that so far but I'm sure I'm missing some very obvious cases

Copy link
Member Author

Choose a reason for hiding this comment

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

Prefix and Unambiguous are interchangeable on this line. The only difference between Prefix and Unambiguous is for subexpressions that are not a rightmost subexpression:

$a.method()
// needs parens if $a is `&x` (Prefix)
// no parens if $a is `loop {}` (Unambiguous)

and if something is not a rightmost subexpression, then by definition next_operator_can_continue_expr would be true, because the outer expression literally continues after that subexpression, so we would not be in this code.

But I still think Prefix is the more natural value to return here compared to Unambiguous if you consider it against the $a.method() example — a subexpression like return false behaves more like &a than like loop {} in that (return false).method() needs the parenthesization, even if that behavior is ultimately not controlled by this return value (because next_operator_can_continue_expr would be true).

@fmease
Copy link
Member

fmease commented Jun 12, 2025

@bors r+ rollup

@bors
Copy link
Collaborator

bors commented Jun 12, 2025

📌 Commit 6cca4ca has been approved by fmease

It is now in the queue for this repository.

@bors bors added S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jun 12, 2025
bors added a commit that referenced this pull request Jun 13, 2025
Rollup of 10 pull requests

Successful merges:

 - #134847 (Implement asymmetrical precedence for closures and jumps)
 - #141491 (Delegate `<CStr as Debug>` to `ByteStr`)
 - #141770 (Merge `Cfg::render_long_html` and `Cfg::render_long_plain` methods common code)
 - #142069 (Introduce `-Zmacro-stats`)
 - #142158 (Tracking the old name of renamed unstable library features)
 - #142221 ([AIX] strip underlying xcoff object)
 - #142340 (miri: we can use apfloat's mul_add now)
 - #142379 (Add bootstrap option to compile a tool with features)
 - #142410 (intrinsics: rename min_align_of to align_of)
 - #142413 (rustc-dev-guide subtree update)

r? `@ghost`
`@rustbot` modify labels: rollup
@bors bors merged commit b12bb25 into rust-lang:master Jun 13, 2025
6 checks passed
@rustbot rustbot added this to the 1.89.0 milestone Jun 13, 2025
rust-timer added a commit that referenced this pull request Jun 13, 2025
Rollup merge of #134847 - dtolnay:asymmetrical, r=fmease

Implement asymmetrical precedence for closures and jumps

I have been through a series of asymmetrical precedence designs in Syn, and finally have one that I like and is worth backporting into rustc. It is based on just 2 bits of state: `next_operator_can_begin_expr` and `next_operator_can_continue_expr`.

Asymmetrical precedence is the thing that enables `(return 1) + 1` to require parentheses while `1 + return 1` does not, despite `+` always having stronger precedence than `return` [according to the Rust Reference](https://doc.rust-lang.org/1.83.0/reference/expressions.html#expression-precedence). This is facilitated by `next_operator_can_continue_expr`.

Relatedly, it is the thing that enables `(return) - 1` to require parentheses while `return + 1` does not, despite `+` and `-` having exactly the same precedence. This is facilitated by `next_operator_can_begin_expr`.

**Example:**

```rust
macro_rules! repro {
    ($e:expr) => {
        $e - $e;
        $e + $e;
    };
}

fn main() {
    repro!{return}
    repro!{return 1}
}
```

`-Zunpretty=expanded` **Before:**

```console
fn main() {
    (return) - (return);
    (return) + (return);
    (return 1) - (return 1);
    (return 1) + (return 1);
}
```

**After:**

```console
fn main() {
    (return) - return;
    return + return;
    (return 1) - return 1;
    (return 1) + return 1;
}
```
@dtolnay dtolnay deleted the asymmetrical branch June 13, 2025 16:30
github-actions bot pushed a commit to rust-lang/miri that referenced this pull request Jun 14, 2025
Rollup of 10 pull requests

Successful merges:

 - rust-lang/rust#134847 (Implement asymmetrical precedence for closures and jumps)
 - rust-lang/rust#141491 (Delegate `<CStr as Debug>` to `ByteStr`)
 - rust-lang/rust#141770 (Merge `Cfg::render_long_html` and `Cfg::render_long_plain` methods common code)
 - rust-lang/rust#142069 (Introduce `-Zmacro-stats`)
 - rust-lang/rust#142158 (Tracking the old name of renamed unstable library features)
 - rust-lang/rust#142221 ([AIX] strip underlying xcoff object)
 - rust-lang/rust#142340 (miri: we can use apfloat's mul_add now)
 - rust-lang/rust#142379 (Add bootstrap option to compile a tool with features)
 - rust-lang/rust#142410 (intrinsics: rename min_align_of to align_of)
 - rust-lang/rust#142413 (rustc-dev-guide subtree update)

r? `@ghost`
`@rustbot` modify labels: rollup
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-parser Area: The lexing & parsing of Rust source code to an AST A-pretty Area: Pretty printing (including `-Z unpretty`) S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Postfix yield is pretty-printed with too many parentheses
7 participants