From 33ecb1c4143660bf8815c2bffe3cd48bde82832e Mon Sep 17 00:00:00 2001 From: Marton Moro Date: Thu, 13 Aug 2026 11:49:34 +0200 Subject: [PATCH 1/2] parser: suggest turbofish for multi-param generics in call args --- .../rustc_parse/src/parser/diagnostics.rs | 100 ++++++++++++------ compiler/rustc_parse/src/parser/expr.rs | 33 +++++- ...gest-turbofish-parsed-as-comparisons.fixed | 34 ++++++ ...suggest-turbofish-parsed-as-comparisons.rs | 34 ++++++ ...est-turbofish-parsed-as-comparisons.stderr | 24 +++++ ...suggest-turbofish-unrelated-parse-error.rs | 10 ++ ...est-turbofish-unrelated-parse-error.stderr | 8 ++ 7 files changed, 206 insertions(+), 37 deletions(-) create mode 100644 tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.fixed create mode 100644 tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.rs create mode 100644 tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.stderr create mode 100644 tests/ui/suggestions/suggest-turbofish-unrelated-parse-error.rs create mode 100644 tests/ui/suggestions/suggest-turbofish-unrelated-parse-error.stderr diff --git a/compiler/rustc_parse/src/parser/diagnostics.rs b/compiler/rustc_parse/src/parser/diagnostics.rs index 5a2540f05c53b..ff5e6f29170af 100644 --- a/compiler/rustc_parse/src/parser/diagnostics.rs +++ b/compiler/rustc_parse/src/parser/diagnostics.rs @@ -1243,43 +1243,19 @@ impl<'a> Parser<'a> { ) -> PResult<'a, ErrorGuaranteed> { if let ExprKind::Binary(binop, _, _) = &expr.kind && let ast::BinOpKind::Lt = binop.node - && self.eat(exp!(Comma)) + && self.parse_mistyped_turbofish_generic_args() { - let x = self.parse_seq_to_before_end( - exp!(Gt), - SeqSep::trailing_allowed(exp!(Comma)), - |p| match p.parse_generic_arg(None)? { - Some(arg) => Ok(arg), - // If we didn't eat a generic arg, then we should error. - None => p.unexpected_any(), - }, - ); - match x { - Ok((_, _, Recovered::No)) => { - if self.eat(exp!(Gt)) { - // We made sense of it. Improve the error message. - e.span_suggestion_verbose( - binop.span.shrink_to_lo(), - msg!("use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments"), - "::", - Applicability::MaybeIncorrect, - ); - match self.parse_expr() { - Ok(_) => { - // The subsequent expression is valid. Mark - // `expr` as erroneous and emit `e` now, but - // return `Ok` so parsing can continue. - let guar = e.emit(); - *expr = self.mk_expr_err(expr.span.to(self.prev_token.span), guar); - return Ok(guar); - } - Err(err) => { - err.cancel(); - } - } - } + // We made sense of it. Improve the error message. + sugg_missing_turbofish(&mut e, binop.span); + match self.parse_expr() { + Ok(_) => { + // The subsequent expression is valid. Mark + // `expr` as erroneous and emit `e` now, but + // return `Ok` so parsing can continue. + let guar = e.emit(); + *expr = self.mk_expr_err(expr.span.to(self.prev_token.span), guar); + return Ok(guar); } - Ok((_, _, Recovered::Yes(_))) => {} Err(err) => { err.cancel(); } @@ -1288,6 +1264,50 @@ impl<'a> Parser<'a> { Err(e) } + /// Parses the `, T, U>` tail of a `Foo` whose turbofish `::` is missing, so it parsed + /// as a comparison. On failure the parser is left mid-way, so callers must snapshot first. + fn parse_mistyped_turbofish_generic_args(&mut self) -> bool { + if !self.eat(exp!(Comma)) { + return false; + } + match self.parse_seq_to_before_end(exp!(Gt), SeqSep::trailing_allowed(exp!(Comma)), |p| { + match p.parse_generic_arg(None)? { + Some(arg) => Ok(arg), + None => p.unexpected_any(), + } + }) { + Ok((_, _, Recovered::No)) => self.eat(exp!(Gt)), + Ok((_, _, Recovered::Yes(_))) => false, + Err(err) => { + err.cancel(); + false + } + } + } + + /// Check whether a call argument that parsed as a `<` comparison is really a path missing its + /// turbofish, i.e. the generic args are followed by `::` or a call. Leaves the parser after + /// what it managed to read, so callers must snapshot first. + pub(super) fn probe_missing_turbofish(&mut self) -> bool { + self.with_recovery(super::Recovery::Forbidden, |this| { + this.parse_mistyped_turbofish_generic_args() + && match this.token.kind { + token::PathSep => { + this.bump(); + match this.parse_expr() { + Ok(_) => true, + Err(err) => { + err.cancel(); + false + } + } + } + token::OpenParen => this.consume_fn_args().is_ok(), + _ => false, + } + }) + } + /// Suggest add the missing `let` before the identifier in stmt /// `a: Ty = 1` -> `let a: Ty = 1` pub(super) fn suggest_add_missing_let_for_stmt(&mut self, err: &mut Diag<'a>) { @@ -3172,3 +3192,13 @@ impl<'a> Parser<'a> { }) } } + +/// Suggest inserting the `::` of a turbofish before the `<` that parsed as a comparison. +pub(super) fn sugg_missing_turbofish(err: &mut Diag<'_>, binop_span: Span) { + err.span_suggestion_verbose( + binop_span.shrink_to_lo(), + msg!("use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments"), + "::", + Applicability::MaybeIncorrect, + ); +} diff --git a/compiler/rustc_parse/src/parser/expr.rs b/compiler/rustc_parse/src/parser/expr.rs index 12de4957e99c2..91281f204a45f 100644 --- a/compiler/rustc_parse/src/parser/expr.rs +++ b/compiler/rustc_parse/src/parser/expr.rs @@ -27,7 +27,7 @@ use rustc_span::{BytePos, ErrorGuaranteed, Ident, Pos, Span, Spanned, Symbol, kw use thin_vec::{ThinVec, thin_vec}; use tracing::instrument; -use super::diagnostics::SnapshotParser; +use super::diagnostics::{SnapshotParser, sugg_missing_turbofish}; use super::pat::{CommaRecoveryMode, Expected, RecoverColon, RecoverComma}; use super::ty::{AllowPlus, RecoverQPath, RecoverReturnSign}; use super::{ @@ -87,7 +87,36 @@ impl<'a> Parser<'a> { /// Parses a sequence of expressions delimited by parentheses. fn parse_expr_paren_seq(&mut self) -> PResult<'a, ThinVec>> { - self.parse_paren_comma_seq(Self::parse_expr).map(|(r, _)| r) + let mut candidates = Vec::new(); + + self.parse_paren_comma_seq(|p| match p.parse_expr() { + Ok(expr) => { + if p.may_recover() + && let ExprKind::Binary(binop, _, _) = &expr.kind + && binop.node == BinOpKind::Lt + { + candidates.push((p.create_snapshot_for_diagnostic(), binop.span)); + } + Ok(expr) + } + Err(mut err) => { + if candidates.is_empty() { + return Err(err); + } + let failed = p.create_snapshot_for_diagnostic(); + let failed_pos = p.approx_token_stream_pos(); + while let Some((snapshot, binop_span)) = candidates.pop() { + p.restore_snapshot(snapshot); + if p.probe_missing_turbofish() && p.approx_token_stream_pos() > failed_pos { + sugg_missing_turbofish(&mut err, binop_span); + break; + } + } + p.restore_snapshot(failed); + Err(err) + } + }) + .map(|(r, _)| r) } /// Parses an expression, subject to the given restrictions. diff --git a/tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.fixed b/tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.fixed new file mode 100644 index 0000000000000..aedcf41a270a7 --- /dev/null +++ b/tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.fixed @@ -0,0 +1,34 @@ +//@ run-rustfix +#![allow(dead_code)] + +struct S; + +struct Many { + a: A, + b: B, + c: C, + d: D, +} +impl Many { + fn new() -> Self { + todo!() + } +} +fn bar(_: Many) {} + +fn take_two(_: bool, _: bool) {} +fn take_three(_: bool, _: bool, _: Many, i32, i32>) {} + +fn main() { + let _ = bar(Many::, i32, i32>::new()); + //~^ ERROR expected expression + + // These are unambiguously comparisons and must keep compiling. + let (a, b, c, d) = (1, 2, 3, 4); + take_two(a < b, c > (d)); + take_two(a < b, c > ::std::primitive::i32::MAX); + + // An argument preceded by genuine comparisons. + take_three(a < b, c > (d), Many::, i32, i32>::new()); + //~^ ERROR expected expression +} diff --git a/tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.rs b/tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.rs new file mode 100644 index 0000000000000..279008e8b743f --- /dev/null +++ b/tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.rs @@ -0,0 +1,34 @@ +//@ run-rustfix +#![allow(dead_code)] + +struct S; + +struct Many { + a: A, + b: B, + c: C, + d: D, +} +impl Many { + fn new() -> Self { + todo!() + } +} +fn bar(_: Many) {} + +fn take_two(_: bool, _: bool) {} +fn take_three(_: bool, _: bool, _: Many, i32, i32>) {} + +fn main() { + let _ = bar(Many, i32, i32>::new()); + //~^ ERROR expected expression + + // These are unambiguously comparisons and must keep compiling. + let (a, b, c, d) = (1, 2, 3, 4); + take_two(a < b, c > (d)); + take_two(a < b, c > ::std::primitive::i32::MAX); + + // An argument preceded by genuine comparisons. + take_three(a < b, c > (d), Many, i32, i32>::new()); + //~^ ERROR expected expression +} diff --git a/tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.stderr b/tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.stderr new file mode 100644 index 0000000000000..db529099f2e72 --- /dev/null +++ b/tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.stderr @@ -0,0 +1,24 @@ +error: expected expression, found `,` + --> $DIR/suggest-turbofish-parsed-as-comparisons.rs:23:46 + | +LL | let _ = bar(Many, i32, i32>::new()); + | ^ expected expression + | +help: use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + | +LL | let _ = bar(Many::, i32, i32>::new()); + | ++ + +error: expected expression, found `,` + --> $DIR/suggest-turbofish-parsed-as-comparisons.rs:32:61 + | +LL | take_three(a < b, c > (d), Many, i32, i32>::new()); + | ^ expected expression + | +help: use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + | +LL | take_three(a < b, c > (d), Many::, i32, i32>::new()); + | ++ + +error: aborting due to 2 previous errors + diff --git a/tests/ui/suggestions/suggest-turbofish-unrelated-parse-error.rs b/tests/ui/suggestions/suggest-turbofish-unrelated-parse-error.rs new file mode 100644 index 0000000000000..c8872a7a82925 --- /dev/null +++ b/tests/ui/suggestions/suggest-turbofish-unrelated-parse-error.rs @@ -0,0 +1,10 @@ +// An argument that fails to parse for an unrelated reason must not make us blame an earlier +// comparison for a missing turbofish. + +fn take_three(_: bool, _: bool, _: ()) {} + +fn main() { + let (a, b, c, d) = (1, 2, 3, 4); + take_three(a < b, c > (d), @); + //~^ ERROR expected expression, found `@` +} diff --git a/tests/ui/suggestions/suggest-turbofish-unrelated-parse-error.stderr b/tests/ui/suggestions/suggest-turbofish-unrelated-parse-error.stderr new file mode 100644 index 0000000000000..bc2a2846b0572 --- /dev/null +++ b/tests/ui/suggestions/suggest-turbofish-unrelated-parse-error.stderr @@ -0,0 +1,8 @@ +error: expected expression, found `@` + --> $DIR/suggest-turbofish-unrelated-parse-error.rs:8:32 + | +LL | take_three(a < b, c > (d), @); + | ^ expected expression + +error: aborting due to 1 previous error + From 6a61f38e54815f3a2f7bbf389191a099803ed58f Mon Sep 17 00:00:00 2001 From: Marton Moro Date: Tue, 15 Sep 2026 09:42:44 +0200 Subject: [PATCH 2/2] parser: avoid turbofish recovery for unrelated parse errors --- compiler/rustc_parse/src/parser/expr.rs | 3 +- compiler/rustc_parse/src/parser/mod.rs | 3 ++ compiler/rustc_parse/src/parser/path.rs | 4 ++- ...gest-turbofish-parsed-as-comparisons.fixed | 5 ++++ ...suggest-turbofish-parsed-as-comparisons.rs | 5 ++++ ...est-turbofish-parsed-as-comparisons.stderr | 17 +++++++++-- ...suggest-turbofish-unrelated-parse-error.rs | 28 +++++++++++++++++ ...est-turbofish-unrelated-parse-error.stderr | 30 +++++++++++++++++-- 8 files changed, 88 insertions(+), 7 deletions(-) diff --git a/compiler/rustc_parse/src/parser/expr.rs b/compiler/rustc_parse/src/parser/expr.rs index 91281f204a45f..249e1bde6cf43 100644 --- a/compiler/rustc_parse/src/parser/expr.rs +++ b/compiler/rustc_parse/src/parser/expr.rs @@ -92,8 +92,9 @@ impl<'a> Parser<'a> { self.parse_paren_comma_seq(|p| match p.parse_expr() { Ok(expr) => { if p.may_recover() - && let ExprKind::Binary(binop, _, _) = &expr.kind + && let ExprKind::Binary(binop, lhs, _) = &expr.kind && binop.node == BinOpKind::Lt + && matches!(lhs.kind, ExprKind::Path(..)) { candidates.push((p.create_snapshot_for_diagnostic(), binop.span)); } diff --git a/compiler/rustc_parse/src/parser/mod.rs b/compiler/rustc_parse/src/parser/mod.rs index 80c1eeb4ef041..5f9b9c7d1b0b7 100644 --- a/compiler/rustc_parse/src/parser/mod.rs +++ b/compiler/rustc_parse/src/parser/mod.rs @@ -866,6 +866,9 @@ impl<'a> Parser<'a> { break; } Err(mut expect_err) => { + if !self.may_recover() { + return Err(expect_err); + } let sp = self.prev_token.span.shrink_to_hi(); let token_str = pprust::token_kind_to_string(&exp.tok); diff --git a/compiler/rustc_parse/src/parser/path.rs b/compiler/rustc_parse/src/parser/path.rs index 6bc7195b8371c..4a02227fa6a77 100644 --- a/compiler/rustc_parse/src/parser/path.rs +++ b/compiler/rustc_parse/src/parser/path.rs @@ -948,7 +948,7 @@ impl<'a> Parser<'a> { } } else if self.token.is_keyword(kw::Const) { return self.recover_const_param_declaration(ty_generics); - } else { + } else if self.may_recover() { // Fall back by trying to parse a const-expr expression. If we successfully do so, // then we should report an error that it needs to be wrapped in braces. let snapshot = self.create_snapshot_for_diagnostic(); @@ -965,6 +965,8 @@ impl<'a> Parser<'a> { return Ok(None); } } + } else { + return Ok(None); }; Ok(Some(arg)) diff --git a/tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.fixed b/tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.fixed index aedcf41a270a7..ba30e150b9f85 100644 --- a/tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.fixed +++ b/tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.fixed @@ -16,12 +16,17 @@ impl Many { } fn bar(_: Many) {} +struct Tuple(A, B, C, D); +fn baz(_: Tuple) {} + fn take_two(_: bool, _: bool) {} fn take_three(_: bool, _: bool, _: Many, i32, i32>) {} fn main() { let _ = bar(Many::, i32, i32>::new()); //~^ ERROR expected expression + baz(Tuple::, i32, i32>(1, Many::new(), 2, 3)); + //~^ ERROR expected expression // These are unambiguously comparisons and must keep compiling. let (a, b, c, d) = (1, 2, 3, 4); diff --git a/tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.rs b/tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.rs index 279008e8b743f..f20e72cc97546 100644 --- a/tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.rs +++ b/tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.rs @@ -16,12 +16,17 @@ impl Many { } fn bar(_: Many) {} +struct Tuple(A, B, C, D); +fn baz(_: Tuple) {} + fn take_two(_: bool, _: bool) {} fn take_three(_: bool, _: bool, _: Many, i32, i32>) {} fn main() { let _ = bar(Many, i32, i32>::new()); //~^ ERROR expected expression + baz(Tuple, i32, i32>(1, Many::new(), 2, 3)); + //~^ ERROR expected expression // These are unambiguously comparisons and must keep compiling. let (a, b, c, d) = (1, 2, 3, 4); diff --git a/tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.stderr b/tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.stderr index db529099f2e72..d5649e6f3db35 100644 --- a/tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.stderr +++ b/tests/ui/suggestions/suggest-turbofish-parsed-as-comparisons.stderr @@ -1,5 +1,5 @@ error: expected expression, found `,` - --> $DIR/suggest-turbofish-parsed-as-comparisons.rs:23:46 + --> $DIR/suggest-turbofish-parsed-as-comparisons.rs:26:46 | LL | let _ = bar(Many, i32, i32>::new()); | ^ expected expression @@ -10,7 +10,18 @@ LL | let _ = bar(Many::, i32, i32>::new()); | ++ error: expected expression, found `,` - --> $DIR/suggest-turbofish-parsed-as-comparisons.rs:32:61 + --> $DIR/suggest-turbofish-parsed-as-comparisons.rs:28:39 + | +LL | baz(Tuple, i32, i32>(1, Many::new(), 2, 3)); + | ^ expected expression + | +help: use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments + | +LL | baz(Tuple::, i32, i32>(1, Many::new(), 2, 3)); + | ++ + +error: expected expression, found `,` + --> $DIR/suggest-turbofish-parsed-as-comparisons.rs:37:61 | LL | take_three(a < b, c > (d), Many, i32, i32>::new()); | ^ expected expression @@ -20,5 +31,5 @@ help: use `::<...>` instead of `<...>` to specify lifetime, type, or const argum LL | take_three(a < b, c > (d), Many::, i32, i32>::new()); | ++ -error: aborting due to 2 previous errors +error: aborting due to 3 previous errors diff --git a/tests/ui/suggestions/suggest-turbofish-unrelated-parse-error.rs b/tests/ui/suggestions/suggest-turbofish-unrelated-parse-error.rs index c8872a7a82925..adecbf17cbc6e 100644 --- a/tests/ui/suggestions/suggest-turbofish-unrelated-parse-error.rs +++ b/tests/ui/suggestions/suggest-turbofish-unrelated-parse-error.rs @@ -3,8 +3,36 @@ fn take_three(_: bool, _: bool, _: ()) {} +fn take_generic(_: bool, _: T, _: ()) {} + +struct S; +struct Many(A, B, C, D); +impl Many { + fn new() -> Self { + todo!() + } +} + fn main() { let (a, b, c, d) = (1, 2, 3, 4); take_three(a < b, c > (d), @); //~^ ERROR expected expression, found `@` } + +fn closure_argument() { + let (a, b) = (1, 2); + take_generic(a < b, || 0, @); + //~^ ERROR expected expression, found `@` +} + +fn missing_comma() { + let (a, b, c, d) = (1, 2, 3, 4); + take_three(a < b, c d, @); + //~^ ERROR expected one of `!`, `)`, `,`, `.`, `::`, `?`, `{`, or an operator, found `d` + //~| ERROR expected expression, found `@` +} + +fn non_path_lhs() { + take_generic(1 < 2, Many<(), i32, S, S>, i32, i32>::new(), ()); + //~^ ERROR expected expression, found `,` +} diff --git a/tests/ui/suggestions/suggest-turbofish-unrelated-parse-error.stderr b/tests/ui/suggestions/suggest-turbofish-unrelated-parse-error.stderr index bc2a2846b0572..1af584b5824c0 100644 --- a/tests/ui/suggestions/suggest-turbofish-unrelated-parse-error.stderr +++ b/tests/ui/suggestions/suggest-turbofish-unrelated-parse-error.stderr @@ -1,8 +1,34 @@ error: expected expression, found `@` - --> $DIR/suggest-turbofish-unrelated-parse-error.rs:8:32 + --> $DIR/suggest-turbofish-unrelated-parse-error.rs:18:32 | LL | take_three(a < b, c > (d), @); | ^ expected expression -error: aborting due to 1 previous error +error: expected expression, found `@` + --> $DIR/suggest-turbofish-unrelated-parse-error.rs:24:31 + | +LL | take_generic(a < b, || 0, @); + | ^ expected expression + +error: expected one of `!`, `)`, `,`, `.`, `::`, `?`, `{`, or an operator, found `d` + --> $DIR/suggest-turbofish-unrelated-parse-error.rs:30:25 + | +LL | take_three(a < b, c d, @); + | -^ expected one of 8 possible tokens + | | + | help: missing `,` + +error: expected expression, found `@` + --> $DIR/suggest-turbofish-unrelated-parse-error.rs:30:28 + | +LL | take_three(a < b, c d, @); + | ^ expected expression + +error: expected expression, found `,` + --> $DIR/suggest-turbofish-unrelated-parse-error.rs:36:44 + | +LL | take_generic(1 < 2, Many<(), i32, S, S>, i32, i32>::new(), ()); + | ^ expected expression + +error: aborting due to 5 previous errors