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

Fix parenthesization of subexprs containing statement boundary #119105

Merged
merged 2 commits into from
Dec 27, 2023

Conversation

dtolnay
Copy link
Member

@dtolnay dtolnay commented Dec 19, 2023

This PR fixes a multitude of false negatives and false positives in the AST pretty printer's parenthesis insertion related to statement boundaries — statements which terminate unexpectedly early if there aren't parentheses.

Without this fix, the AST pretty printer (including both stringify! and rustc -Zunpretty=expanded) is prone to producing output which is not syntactically valid Rust. Invalid output is problematic because it means Rustfmt is unable to parse the output of cargo expand, for example, causing friction by forcing someone trying to debug a macro into reading poorly formatted code.

I believe the set of bugs fixed in this PR account for the most prevalent reason that cargo expand produces invalid output in real-world usage.

Fixes #98790.

False negatives

The following is a correct program — cargo check succeeds.

macro_rules! m {
    ($e:expr) => {
        match () { _ => $e }
    };
}

fn main() {
    m!({ 1 } - 1);
}

But rustc -Zunpretty=expanded main.rs produces output that is invalid Rust syntax, because parenthesization is needed and not being done by the pretty printer.

fn main() { match () { _ => { 1 } - 1, }; }

Piping this expanded code to rustfmt, it fails to parse.

error: unexpected `,` in pattern
 --> <stdin>:1:38
  |
1 | fn main() { match () { _ => { 1 } - 1, }; }
  |                                      ^
  |
help: try adding parentheses to match on a tuple...
  |
1 | fn main() { match () { _ => { 1 } (- 1,) }; }
  |                                   +    +
help: ...or a vertical bar to match on multiple alternatives
  |
1 | fn main() { match () { _ => { 1 } - 1 | }; }
  |                                   ~~~~~

Fixed output after this PR:

fn main() { match () { _ => ({ 1 }) - 1, }; }

False positives

Less problematic, but worth fixing (just like #118726).

fn main() {
    let _ = match () { _ => 1 } - 1;
}

Output of rustc -Zunpretty=expanded lib.rs before this PR. There is no reason parentheses would need to be inserted there.

fn main() { let _ = (match () { _ => 1, }) - 1; }

After this PR:

fn main() { let _ = match () { _ => 1, } - 1; }

Alternatives considered

In this PR I opted to parenthesize only the leading subexpression causing the statement boundary, rather than the entire statement. Example:

macro_rules! m {
    ($e:expr) => {
        $e
    };
}

fn main() {
    m!(loop { break [1]; }[0] - 1);
}

This PR produces the following pretty-printed contents for fn main:

(loop { break [1]; })[0] - 1;

A different equally correct output would be:

(loop { break [1]; }[0] - 1);

I chose the one I did because it is the only approach used by handwritten code in the standard library and compiler. There are 4 places where parenthesization is being used to prevent a statement boundary, and in all 4, the developer has chosen to parenthesize the smallest subexpression rather than the whole statement:

(if ptr.is_null() { ptr } else { align_ptr(ptr, layout.align()) }) as *mut u8

(match token_descr {
Some(TokenDescription::ReservedIdentifier) => {
ExpectedIdentifierFound::ReservedIdentifier
}
Some(TokenDescription::Keyword) => ExpectedIdentifierFound::Keyword,
Some(TokenDescription::ReservedKeyword) => ExpectedIdentifierFound::ReservedKeyword,
Some(TokenDescription::DocComment) => ExpectedIdentifierFound::DocComment,
None => ExpectedIdentifierFound::Other,
})(span)

(unsafe { &mut self.get_unchecked_mut().f })(cx)

(match self.start_bound() {
Included(start) => start <= item,
Excluded(start) => start < item,
Unbounded => true,
}) && (match self.end_bound() {

@rustbot
Copy link
Collaborator

rustbot commented Dec 19, 2023

r? @WaffleLapkin

(rustbot has picked a reviewer for you, use r? to override)

@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 19, 2023
@WaffleLapkin
Copy link
Member

WaffleLapkin commented Dec 27, 2023

I'm surprised that stringify is expected to produce valid rust, as opposed to "what's literally written in the file", I but given it can observe macro expansion and there is precedence for this, I'm inclined to say that it's fine.

Will review the code a bit later.

@dtolnay
Copy link
Member Author

dtolnay commented Dec 27, 2023

For stringify!, I agree it is not obvious that we need it to produce valid Rust syntax. I do not know of any use cases that involve parsing the output of stringify! other than visually by a human (for which valid syntax is nice but not a hard requirement).

But there is only one AST pretty-printer, and stringify! and rustc -Zunpretty=expanded use the same one. I care 100× more about the latter producing valid syntax, because cargo expand is based on it and relies on parsing the output not just for piping to rustfmt / prettyplease, but also for cargo expand path::to::module where it filters the output down to just a single item by path, which I use commonly.

@WaffleLapkin
Copy link
Member

@bors r+

@bors
Copy link
Contributor

bors commented Dec 27, 2023

📌 Commit 17239d9 has been approved by WaffleLapkin

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 Dec 27, 2023
@bors
Copy link
Contributor

bors commented Dec 27, 2023

⌛ Testing commit 17239d9 with merge 89e2160...

@bors
Copy link
Contributor

bors commented Dec 27, 2023

☀️ Test successful - checks-actions
Approved by: WaffleLapkin
Pushing 89e2160 to master...

@bors bors added the merged-by-bors This PR was explicitly merged by bors. label Dec 27, 2023
@bors bors merged commit 89e2160 into rust-lang:master Dec 27, 2023
12 checks passed
@rustbot rustbot added this to the 1.77.0 milestone Dec 27, 2023
@dtolnay dtolnay deleted the paren branch December 28, 2023 00:16
@rust-timer
Copy link
Collaborator

Finished benchmarking commit (89e2160): comparison URL.

Overall result: no relevant changes - no action needed

@rustbot label: -perf-regression

Instruction count

This benchmark run did not return any relevant results for this metric.

Max RSS (memory usage)

Results

This is a less reliable metric that may be of interest but was not used to determine the overall result at the top of this comment.

mean range count
Regressions ❌
(primary)
1.0% [1.0%, 1.0%] 1
Regressions ❌
(secondary)
- - 0
Improvements ✅
(primary)
-0.1% [-0.1%, -0.1%] 1
Improvements ✅
(secondary)
- - 0
All ❌✅ (primary) 0.4% [-0.1%, 1.0%] 2

Cycles

Results

This is a less reliable metric that may be of interest but was not used to determine the overall result at the top of this comment.

mean range count
Regressions ❌
(primary)
0.6% [0.6%, 0.6%] 1
Regressions ❌
(secondary)
- - 0
Improvements ✅
(primary)
- - 0
Improvements ✅
(secondary)
- - 0
All ❌✅ (primary) 0.6% [0.6%, 0.6%] 1

Binary size

This benchmark run did not return any relevant results for this metric.

Bootstrap: 672.366s -> 670.933s (-0.21%)
Artifact size: 312.33 MiB -> 312.35 MiB (0.00%)

@dtolnay dtolnay added the A-pretty Area: Pretty printing (including `-Z unpretty`) label Dec 28, 2023
bors added a commit to rust-lang-ci/rust that referenced this pull request May 12, 2024
Fix, document, and test parser and pretty-printer edge cases related to braced macro calls

_Review note: this is a deceptively small PR because it comes with 145 lines of docs and 196 lines of tests, and only 25 lines of compiler code changed. However, I recommend reviewing it 1 commit at a time because much of the effect of the code changes is non-local i.e. affecting code that is not visible in the final state of the PR. I have paid attention that reviewing the PR one commit at a time is as easy as I can make it. All of the code you need to know about is touched in those commits, even if some of those changes disappear by the end of the stack._

This is a follow-up to rust-lang#119105. One case that is not relevant to `-Zunpretty=expanded`, but which came up as I'm porting rust-lang#119105 and rust-lang#118726 into `syn`'s printer and `prettyplease`'s printer where it **is** relevant, and is also relevant to rustc's `stringify!`, is statement boundaries in the vicinity of braced macro calls.

Rustc's AST pretty-printer produces invalid syntax for statements that begin with a braced macro call:

```rust
macro_rules! stringify_item {
    ($i:item) => {
        stringify!($i)
    };
}

macro_rules! repro {
    ($e:expr) => {
        stringify_item!(fn main() { $e + 1; })
    };
}

fn main() {
    println!("{}", repro!(m! {}));
}
```

**Before this PR:** output is not valid Rust syntax.

```console
fn main() { m! {} + 1; }
```

```console
error: leading `+` is not supported
 --> <anon>:1:19
  |
1 | fn main() { m! {} + 1; }
  |                   ^ unexpected `+`
  |
help: try removing the `+`
  |
1 - fn main() { m! {} + 1; }
1 + fn main() { m! {}  1; }
  |
```

**After this PR:** valid syntax.

```console
fn main() { (m! {}) + 1; }
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-pretty Area: Pretty printing (including `-Z unpretty`) merged-by-bors This PR was explicitly merged by bors. 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.

Rustc pretty printer generates syntactically invalid output for some binary operators
5 participants