From 0eab566946395100fdfb4bb083009a05444fe155 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Le=C3=B3n=20Orell=20Valerian=20Liehr?= Date: Thu, 10 Sep 2026 14:11:22 +0200 Subject: [PATCH 1/9] Move parse error recovery from some invalid expr ops out of line --- compiler/rustc_parse/src/diagnostics.rs | 4 +- compiler/rustc_parse/src/parser/expr.rs | 83 +++---------------- .../rustc_parse/src/parser/expr/errors.rs | 80 ++++++++++++++++++ 3 files changed, 93 insertions(+), 74 deletions(-) create mode 100644 compiler/rustc_parse/src/parser/expr/errors.rs diff --git a/compiler/rustc_parse/src/diagnostics.rs b/compiler/rustc_parse/src/diagnostics.rs index 1dc2d625fe0e0..fc84591d8813f 100644 --- a/compiler/rustc_parse/src/diagnostics.rs +++ b/compiler/rustc_parse/src/diagnostics.rs @@ -797,7 +797,7 @@ pub(crate) struct EqFieldInit { #[derive(Diagnostic)] #[diag("unexpected token: `...`")] -pub(crate) struct DotDotDot { +pub(crate) struct DotDotDotExprOp { #[primary_span] #[suggestion( "use `..` for an exclusive range", @@ -816,7 +816,7 @@ pub(crate) struct DotDotDot { #[derive(Diagnostic)] #[diag("unexpected token: `<-`")] -pub(crate) struct LeftArrowOperator { +pub(crate) struct LArrowExprOp { #[primary_span] #[suggestion( "if you meant to write a comparison against a negative value, add a space in between `<` and `-`", diff --git a/compiler/rustc_parse/src/parser/expr.rs b/compiler/rustc_parse/src/parser/expr.rs index 12de4957e99c2..e268beb305d4a 100644 --- a/compiler/rustc_parse/src/parser/expr.rs +++ b/compiler/rustc_parse/src/parser/expr.rs @@ -37,6 +37,8 @@ use super::{ use crate::diagnostics::ExprParenthesesNeeded; use crate::{diagnostics, exp, maybe_recover_from_interpolated_ty_qpath}; +mod errors; + #[derive(Debug)] pub(super) enum DestructuredFloat { /// 1e2 @@ -165,74 +167,22 @@ impl<'a> Parser<'a> { } { break; } - // Check for deprecated `...` syntax - if self.token == token::DotDotDot && op.node == AssocOp::Range(RangeLimits::Closed) { - self.err_dotdotdot_syntax(self.token.span); - } - if self.token == token::LArrow { - self.err_larrow_operator(self.token.span); - } + self.reject_dotdotdot_expr_op(); + self.reject_larrow_expr_op(); parsed_something = true; self.bump(); - if op.node.is_comparison() { - if let Some(expr) = self.check_no_chained_comparison(&lhs, &op)? { - return Ok((expr, parsed_something)); - } - } - // Look for JS' `===` and `!==` and recover - if let AssocOp::Binary(bop @ BinOpKind::Eq | bop @ BinOpKind::Ne) = op.node - && self.token == token::Eq - && self.prev_token.span.hi() == self.token.span.lo() + if op.node.is_comparison() + && let Some(expr) = self.check_no_chained_comparison(&lhs, &op)? { - let sp = op.span.to(self.token.span); - let sugg = bop.as_str().into(); - let invalid = format!("{sugg}="); - self.dcx().emit_err(diagnostics::InvalidComparisonOperator { - span: sp, - invalid: invalid.clone(), - sub: diagnostics::InvalidComparisonOperatorSub::Correctable { - span: sp, - invalid, - correct: sugg, - }, - }); - self.bump(); + return Ok((expr, parsed_something)); } - // Look for PHP's `<>` and recover - if op.node == AssocOp::Binary(BinOpKind::Lt) - && self.token == token::Gt - && self.prev_token.span.hi() == self.token.span.lo() - { - let sp = op.span.to(self.token.span); - self.dcx().emit_err(diagnostics::InvalidComparisonOperator { - span: sp, - invalid: "<>".into(), - sub: diagnostics::InvalidComparisonOperatorSub::Correctable { - span: sp, - invalid: "<>".into(), - correct: "!=".into(), - }, - }); - self.bump(); - } - - // Look for C++'s `<=>` and recover - if op.node == AssocOp::Binary(BinOpKind::Le) - && self.token == token::Gt - && self.prev_token.span.hi() == self.token.span.lo() - { - let sp = op.span.to(self.token.span); - self.dcx().emit_err(diagnostics::InvalidComparisonOperator { - span: sp, - invalid: "<=>".into(), - sub: diagnostics::InvalidComparisonOperatorSub::Spaceship(sp), - }); - self.bump(); - } + self.recover_from_strict_eq_op(op); + self.recover_from_diamond_ne_op(op); + self.recover_from_spaceship_cmp_op(op); if self.prev_token == token::Plus && self.token == token::Plus @@ -445,10 +395,7 @@ impl<'a> Parser<'a> { self.dcx().emit_err(err); } - // Check for deprecated `...` syntax. - if self.token == token::DotDotDot { - self.err_dotdotdot_syntax(self.token.span); - } + self.reject_dotdotdot_expr_op(); debug_assert!( self.token.is_range_separator(), @@ -4133,14 +4080,6 @@ impl<'a> Parser<'a> { }); } - fn err_dotdotdot_syntax(&self, span: Span) { - self.dcx().emit_err(diagnostics::DotDotDot { span }); - } - - fn err_larrow_operator(&self, span: Span) { - self.dcx().emit_err(diagnostics::LeftArrowOperator { span }); - } - fn mk_assign_op(&self, assign_op: AssignOp, lhs: Box, rhs: Box) -> ExprKind { ExprKind::AssignOp(assign_op, lhs, rhs) } diff --git a/compiler/rustc_parse/src/parser/expr/errors.rs b/compiler/rustc_parse/src/parser/expr/errors.rs new file mode 100644 index 0000000000000..edbf844d5bbb0 --- /dev/null +++ b/compiler/rustc_parse/src/parser/expr/errors.rs @@ -0,0 +1,80 @@ +use rustc_ast::util::parser::AssocOp; +use rustc_ast::{BinOpKind, token}; +use rustc_span::Spanned; + +use crate::diagnostics; +use crate::parser::Parser; + +impl<'a> Parser<'a> { + /// Reject `...` being used as an expression operator. + pub(super) fn reject_dotdotdot_expr_op(&self) { + if self.token == token::DotDotDot { + self.dcx().emit_err(diagnostics::DotDotDotExprOp { span: self.token.span }); + } + } + + /// Reject `<-` being used as an expression operator. + pub(super) fn reject_larrow_expr_op(&self) { + if self.token == token::LArrow { + self.dcx().emit_err(diagnostics::LArrowExprOp { span: self.token.span }); + } + } + + /// Recover from strict equality operators `===` and `!==` as found in e.g., JS and PHP. + pub(super) fn recover_from_strict_eq_op(&mut self, op: Spanned) { + if let AssocOp::Binary(bop @ BinOpKind::Eq | bop @ BinOpKind::Ne) = op.node + && self.token == token::Eq + && self.prev_token.span.hi() == self.token.span.lo() + { + let sp = op.span.to(self.token.span); + let sugg = bop.as_str().into(); + let invalid = format!("{sugg}="); + self.dcx().emit_err(diagnostics::InvalidComparisonOperator { + span: sp, + invalid: invalid.clone(), + sub: diagnostics::InvalidComparisonOperatorSub::Correctable { + span: sp, + invalid, + correct: sugg, + }, + }); + self.bump(); + } + } + + /// Recover from inequality operator `<>` ("diamond") as found in e.g., PHP. + pub(super) fn recover_from_diamond_ne_op(&mut self, op: Spanned) { + if op.node == AssocOp::Binary(BinOpKind::Lt) + && self.token == token::Gt + && self.prev_token.span.hi() == self.token.span.lo() + { + let sp = op.span.to(self.token.span); + self.dcx().emit_err(diagnostics::InvalidComparisonOperator { + span: sp, + invalid: "<>".into(), + sub: diagnostics::InvalidComparisonOperatorSub::Correctable { + span: sp, + invalid: "<>".into(), + correct: "!=".into(), + }, + }); + self.bump(); + } + } + + /// Recover from comparison operator `<=>` ("spaceship") as found in e.g., C++. + pub(super) fn recover_from_spaceship_cmp_op(&mut self, op: Spanned) { + if op.node == AssocOp::Binary(BinOpKind::Le) + && self.token == token::Gt + && self.prev_token.span.hi() == self.token.span.lo() + { + let sp = op.span.to(self.token.span); + self.dcx().emit_err(diagnostics::InvalidComparisonOperator { + span: sp, + invalid: "<=>".into(), + sub: diagnostics::InvalidComparisonOperatorSub::Spaceship(sp), + }); + self.bump(); + } + } +} From 28af0940b3bbf361f4745e9d9ff45db1abd86963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Le=C3=B3n=20Orell=20Valerian=20Liehr?= Date: Thu, 10 Sep 2026 13:25:49 +0200 Subject: [PATCH 2/9] Don't needlessly pass the operand through some recovery functions by value These functions didn't actually modifiy the operand or return a new or different expression. So essentially the "`fn(Box) -> Box` part" was an identity function. Just change it to "fn(&Expr)". --- .../rustc_parse/src/parser/diagnostics.rs | 26 ++++++++----------- compiler/rustc_parse/src/parser/expr.rs | 7 ++--- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/compiler/rustc_parse/src/parser/diagnostics.rs b/compiler/rustc_parse/src/parser/diagnostics.rs index 6d4a0215eb7b3..0dcb382970c3d 100644 --- a/compiler/rustc_parse/src/parser/diagnostics.rs +++ b/compiler/rustc_parse/src/parser/diagnostics.rs @@ -1671,10 +1671,10 @@ impl<'a> Parser<'a> { pub(super) fn recover_from_prefix_increment( &mut self, - operand_expr: Box, + operand_expr: &Expr, op_span: Span, start_stmt: bool, - ) -> PResult<'a, Box> { + ) -> PResult<'a, ()> { let standalone = if start_stmt { IsStandalone::Standalone } else { IsStandalone::Subexpr }; let kind = IncDecRecovery { standalone, op: IncOrDec::Inc, fixity: UnaryFixity::Pre }; self.recover_from_inc_dec(operand_expr, kind, op_span) @@ -1682,10 +1682,10 @@ impl<'a> Parser<'a> { pub(super) fn recover_from_postfix_increment( &mut self, - operand_expr: Box, + operand_expr: &Expr, op_span: Span, start_stmt: bool, - ) -> PResult<'a, Box> { + ) -> PResult<'a, ()> { let kind = IncDecRecovery { standalone: if start_stmt { IsStandalone::Standalone } else { IsStandalone::Subexpr }, op: IncOrDec::Inc, @@ -1696,10 +1696,10 @@ impl<'a> Parser<'a> { pub(super) fn recover_from_postfix_decrement( &mut self, - operand_expr: Box, + operand_expr: &Expr, op_span: Span, start_stmt: bool, - ) -> PResult<'a, Box> { + ) -> PResult<'a, ()> { let kind = IncDecRecovery { standalone: if start_stmt { IsStandalone::Standalone } else { IsStandalone::Subexpr }, op: IncOrDec::Dec, @@ -1710,22 +1710,16 @@ impl<'a> Parser<'a> { fn recover_from_inc_dec( &mut self, - base: Box, + base: &Expr, kind: IncDecRecovery, op_span: Span, - ) -> PResult<'a, Box> { + ) -> PResult<'a, ()> { let mut err = self.dcx().struct_span_err( op_span, format!("Rust has no {} {} operator", kind.fixity, kind.op.name()), ); err.span_label(op_span, format!("not a valid {} operator", kind.fixity)); - let help_base_case = |mut err: Diag<'_, _>, base| { - err.help(format!("use `{}= 1` instead", kind.op.chr())); - err.emit(); - Ok(base) - }; - // (pre, post) let spans = match kind.fixity { UnaryFixity::Pre => (op_span, base.span.shrink_to_hi()), @@ -1738,7 +1732,9 @@ impl<'a> Parser<'a> { } IsStandalone::Subexpr => { let Ok(base_src) = self.span_to_snippet(base.span) else { - return help_base_case(err, base); + err.help(format!("use `{}= 1` instead", kind.op.chr())); + err.emit(); + return Ok(()); }; match kind.fixity { UnaryFixity::Pre => { diff --git a/compiler/rustc_parse/src/parser/expr.rs b/compiler/rustc_parse/src/parser/expr.rs index e268beb305d4a..7c83035345f72 100644 --- a/compiler/rustc_parse/src/parser/expr.rs +++ b/compiler/rustc_parse/src/parser/expr.rs @@ -191,7 +191,7 @@ impl<'a> Parser<'a> { let op_span = self.prev_token.span.to(self.token.span); // Eat the second `+` self.bump(); - lhs = self.recover_from_postfix_increment(lhs, op_span, starts_stmt)?; + self.recover_from_postfix_increment(&lhs, op_span, starts_stmt)?; continue; } @@ -203,7 +203,7 @@ impl<'a> Parser<'a> { let op_span = self.prev_token.span.to(self.token.span); // Eat the second `-` self.bump(); - lhs = self.recover_from_postfix_decrement(lhs, op_span, starts_stmt)?; + self.recover_from_postfix_decrement(&lhs, op_span, starts_stmt)?; continue; } @@ -487,7 +487,8 @@ impl<'a> Parser<'a> { this.bump(); let operand_expr = this.parse_expr_dot_or_call(attrs)?; - this.recover_from_prefix_increment(operand_expr, pre_span, starts_stmt) + this.recover_from_prefix_increment(&operand_expr, pre_span, starts_stmt)?; + Ok(operand_expr) } token::Ident(..) if this.token.is_keyword(kw::Move) From 8122a6c0ef602df5d70d40a95667a9ee3f10bab1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Le=C3=B3n=20Orell=20Valerian=20Liehr?= Date: Sun, 6 Sep 2026 22:13:38 +0200 Subject: [PATCH 3/9] Remove odd special case of some parse error recovery functions `recover_from_inc_dec` *always* returns a (fatal) `Err(_)` *except* if the increment/decrement operator is a subexpression *and* the source of the operand is not available in which case it emits the diagnostic and returns `Ok(_)` (rendering it non-fatal). This makes no sense whatsoever. For illustration purposes, listed below are steps that would make us reach this case: 1. `rustc a.rs --crate-type=lib` where `a.rs` contains: `#[macro_export] macro_rules! m { () => { i++ } }`. 2. Move or remove `a.rs` 3. `rustc b.rs --edition 2018 --extern a -L.` where `b.rs` contains: `fn main() { (a::m!()); }`. Just make the error unconditionally fatal and add a FIXME to make it non fatal in the future which would allow us to report name resolution errors and what not. However, since that would be slightly more involved and represent a behavior change (in the error path), this is out of scope for a mere cleanup commit like this one. --- .../rustc_parse/src/parser/diagnostics.rs | 19 ++++++++++++------- compiler/rustc_parse/src/parser/expr.rs | 13 +++++-------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/compiler/rustc_parse/src/parser/diagnostics.rs b/compiler/rustc_parse/src/parser/diagnostics.rs index 0dcb382970c3d..cc0f164576c0b 100644 --- a/compiler/rustc_parse/src/parser/diagnostics.rs +++ b/compiler/rustc_parse/src/parser/diagnostics.rs @@ -1674,7 +1674,7 @@ impl<'a> Parser<'a> { operand_expr: &Expr, op_span: Span, start_stmt: bool, - ) -> PResult<'a, ()> { + ) -> Diag<'a> { let standalone = if start_stmt { IsStandalone::Standalone } else { IsStandalone::Subexpr }; let kind = IncDecRecovery { standalone, op: IncOrDec::Inc, fixity: UnaryFixity::Pre }; self.recover_from_inc_dec(operand_expr, kind, op_span) @@ -1685,7 +1685,7 @@ impl<'a> Parser<'a> { operand_expr: &Expr, op_span: Span, start_stmt: bool, - ) -> PResult<'a, ()> { + ) -> Diag<'a> { let kind = IncDecRecovery { standalone: if start_stmt { IsStandalone::Standalone } else { IsStandalone::Subexpr }, op: IncOrDec::Inc, @@ -1699,7 +1699,7 @@ impl<'a> Parser<'a> { operand_expr: &Expr, op_span: Span, start_stmt: bool, - ) -> PResult<'a, ()> { + ) -> Diag<'a> { let kind = IncDecRecovery { standalone: if start_stmt { IsStandalone::Standalone } else { IsStandalone::Subexpr }, op: IncOrDec::Dec, @@ -1713,7 +1713,13 @@ impl<'a> Parser<'a> { base: &Expr, kind: IncDecRecovery, op_span: Span, - ) -> PResult<'a, ()> { + ) -> Diag<'a> { + // FIXME: Don't return an error diag, emit the diag here *and* return a new expr of the form + // `$base += 1` / `$base -= 1` (taking `base: Expr` by value) for *proper* recovery. + // (Just emitting the diag would be insufficient since callers would most likely just + // use `$base` as the recovered AST node which would lead to annoying follow-up diags + // like "variable doesn't need to be mutable" getting emitted in some cases.) + let mut err = self.dcx().struct_span_err( op_span, format!("Rust has no {} {} operator", kind.fixity, kind.op.name()), @@ -1733,8 +1739,7 @@ impl<'a> Parser<'a> { IsStandalone::Subexpr => { let Ok(base_src) = self.span_to_snippet(base.span) else { err.help(format!("use `{}= 1` instead", kind.op.chr())); - err.emit(); - return Ok(()); + return err; }; match kind.fixity { UnaryFixity::Pre => { @@ -1750,7 +1755,7 @@ impl<'a> Parser<'a> { } } } - Err(err) + err } fn prefix_inc_dec_suggest( diff --git a/compiler/rustc_parse/src/parser/expr.rs b/compiler/rustc_parse/src/parser/expr.rs index 7c83035345f72..a1aebfead8244 100644 --- a/compiler/rustc_parse/src/parser/expr.rs +++ b/compiler/rustc_parse/src/parser/expr.rs @@ -191,8 +191,7 @@ impl<'a> Parser<'a> { let op_span = self.prev_token.span.to(self.token.span); // Eat the second `+` self.bump(); - self.recover_from_postfix_increment(&lhs, op_span, starts_stmt)?; - continue; + return Err(self.recover_from_postfix_increment(&lhs, op_span, starts_stmt)); } if self.prev_token == token::Minus @@ -203,8 +202,7 @@ impl<'a> Parser<'a> { let op_span = self.prev_token.span.to(self.token.span); // Eat the second `-` self.bump(); - self.recover_from_postfix_decrement(&lhs, op_span, starts_stmt)?; - continue; + return Err(self.recover_from_postfix_decrement(&lhs, op_span, starts_stmt)); } let op_span = op.span; @@ -486,9 +484,8 @@ impl<'a> Parser<'a> { this.bump(); this.bump(); - let operand_expr = this.parse_expr_dot_or_call(attrs)?; - this.recover_from_prefix_increment(&operand_expr, pre_span, starts_stmt)?; - Ok(operand_expr) + let operand = this.parse_expr_dot_or_call(attrs)?; + return Err(this.recover_from_prefix_increment(&operand, pre_span, starts_stmt)); } token::Ident(..) if this.token.is_keyword(kw::Move) @@ -499,7 +496,7 @@ impl<'a> Parser<'a> { token::Ident(..) if this.may_recover() && this.is_mistaken_not_ident_negation() => { make_it!(this, attrs, |this, _| this.recover_not_expr(lo)) } - _ => return this.parse_expr_dot_or_call(attrs), + _ => this.parse_expr_dot_or_call(attrs), } } From 853d899dfda7cd19e3d0a57845bcd82e348d7768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Le=C3=B3n=20Orell=20Valerian=20Liehr?= Date: Sun, 6 Sep 2026 22:45:43 +0200 Subject: [PATCH 4/9] Dismantle bespoke diagnostic suggestion wrapper API There's literally no upside to use it and only downsides: It's not more concise, only adds code and obfuscates. Its `MultiSugg::emit{,_verbose}` didn't even *emit* the diagnostic, they merely *decorated* it! --- .../rustc_parse/src/parser/diagnostics.rs | 109 ++++++------------ 1 file changed, 36 insertions(+), 73 deletions(-) diff --git a/compiler/rustc_parse/src/parser/diagnostics.rs b/compiler/rustc_parse/src/parser/diagnostics.rs index cc0f164576c0b..a74525e8c664a 100644 --- a/compiler/rustc_parse/src/parser/diagnostics.rs +++ b/compiler/rustc_parse/src/parser/diagnostics.rs @@ -211,22 +211,6 @@ fn find_similar_kw(lookup: Ident, candidates: &[Symbol]) -> Option }) } -struct MultiSugg { - msg: String, - patches: Vec<(Span, String)>, - applicability: Applicability, -} - -impl MultiSugg { - fn emit(self, err: &mut Diag<'_>) { - err.multipart_suggestion(self.msg, self.patches, self.applicability); - } - - fn emit_verbose(self, err: &mut Diag<'_>) { - err.multipart_suggestion(self.msg, self.patches, self.applicability); - } -} - /// SnapshotParser is used to create a snapshot of the parser /// without causing duplicate errors being emitted when the `Parser` /// is dropped. @@ -1726,15 +1710,23 @@ impl<'a> Parser<'a> { ); err.span_label(op_span, format!("not a valid {} operator", kind.fixity)); - // (pre, post) - let spans = match kind.fixity { + let (pre_span, post_span) = match kind.fixity { UnaryFixity::Pre => (op_span, base.span.shrink_to_hi()), UnaryFixity::Post => (base.span.shrink_to_lo(), op_span), }; match kind.standalone { IsStandalone::Standalone => { - self.inc_dec_standalone_suggest(kind, spans).emit_verbose(&mut err) + let mut patches = Vec::new(); + if !pre_span.is_empty() { + patches.push((pre_span, String::new())); + } + patches.push((post_span, format!(" {}= 1", kind.op.chr()))); + err.multipart_suggestion( + format!("use `{}= 1` instead", kind.op.chr()), + patches, + Applicability::MachineApplicable, + ); } IsStandalone::Subexpr => { let Ok(base_src) = self.span_to_snippet(base.span) else { @@ -1743,13 +1735,36 @@ impl<'a> Parser<'a> { }; match kind.fixity { UnaryFixity::Pre => { - self.prefix_inc_dec_suggest(base_src, kind, spans).emit(&mut err) + err.multipart_suggestion( + format!("use `{}= 1` instead", kind.op.chr()), + vec![ + (pre_span, "{ ".to_string()), + (post_span, format!(" {}= 1; {} }}", kind.op.chr(), base_src)), + ], + Applicability::MachineApplicable, + ); } UnaryFixity::Post => { // won't suggest since we can not handle the precedences // for example: `a + b++` has been parsed (a + b)++ and we can not suggest here if !matches!(base.kind, ExprKind::Binary(_, _, _)) { - self.postfix_inc_dec_suggest(base_src, kind, spans).emit(&mut err) + let tmp_var = if base_src.trim() == "tmp" { "tmp_" } else { "tmp" }; + err.multipart_suggestion( + format!("use `{}= 1` instead", kind.op.chr()), + vec![ + (pre_span, format!("{{ let {tmp_var} = ")), + ( + post_span, + format!( + "; {} {}= 1; {} }}", + base_src, + kind.op.chr(), + tmp_var + ), + ), + ], + Applicability::HasPlaceholders, + ); } } } @@ -1758,58 +1773,6 @@ impl<'a> Parser<'a> { err } - fn prefix_inc_dec_suggest( - &mut self, - base_src: String, - kind: IncDecRecovery, - (pre_span, post_span): (Span, Span), - ) -> MultiSugg { - MultiSugg { - msg: format!("use `{}= 1` instead", kind.op.chr()), - patches: vec![ - (pre_span, "{ ".to_string()), - (post_span, format!(" {}= 1; {} }}", kind.op.chr(), base_src)), - ], - applicability: Applicability::MachineApplicable, - } - } - - fn postfix_inc_dec_suggest( - &mut self, - base_src: String, - kind: IncDecRecovery, - (pre_span, post_span): (Span, Span), - ) -> MultiSugg { - let tmp_var = if base_src.trim() == "tmp" { "tmp_" } else { "tmp" }; - MultiSugg { - msg: format!("use `{}= 1` instead", kind.op.chr()), - patches: vec![ - (pre_span, format!("{{ let {tmp_var} = ")), - (post_span, format!("; {} {}= 1; {} }}", base_src, kind.op.chr(), tmp_var)), - ], - applicability: Applicability::HasPlaceholders, - } - } - - fn inc_dec_standalone_suggest( - &mut self, - kind: IncDecRecovery, - (pre_span, post_span): (Span, Span), - ) -> MultiSugg { - let mut patches = Vec::new(); - - if !pre_span.is_empty() { - patches.push((pre_span, String::new())); - } - - patches.push((post_span, format!(" {}= 1", kind.op.chr()))); - MultiSugg { - msg: format!("use `{}= 1` instead", kind.op.chr()), - patches, - applicability: Applicability::MachineApplicable, - } - } - /// Tries to recover from associated item paths like `[T]::AssocItem` / `(T, U)::AssocItem`. /// Attempts to convert the base expression/pattern/type into a type, parses the `::AssocItem` /// tail, and combines them into a `::AssocItem` expression/pattern/type. From 83dc29ebcb284882f46e860391e9fca9bb822b70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Le=C3=B3n=20Orell=20Valerian=20Liehr?= Date: Thu, 10 Sep 2026 10:37:14 +0200 Subject: [PATCH 5/9] Move parse error recovery from C-style inc/dec ops out of line --- compiler/rustc_parse/src/parser/expr.rs | 23 +---------- .../rustc_parse/src/parser/expr/errors.rs | 38 ++++++++++++++++++- 2 files changed, 39 insertions(+), 22 deletions(-) diff --git a/compiler/rustc_parse/src/parser/expr.rs b/compiler/rustc_parse/src/parser/expr.rs index a1aebfead8244..be44b1b503331 100644 --- a/compiler/rustc_parse/src/parser/expr.rs +++ b/compiler/rustc_parse/src/parser/expr.rs @@ -183,27 +183,8 @@ impl<'a> Parser<'a> { self.recover_from_strict_eq_op(op); self.recover_from_diamond_ne_op(op); self.recover_from_spaceship_cmp_op(op); - - if self.prev_token == token::Plus - && self.token == token::Plus - && self.prev_token.span.between(self.token.span).is_empty() - { - let op_span = self.prev_token.span.to(self.token.span); - // Eat the second `+` - self.bump(); - return Err(self.recover_from_postfix_increment(&lhs, op_span, starts_stmt)); - } - - if self.prev_token == token::Minus - && self.token == token::Minus - && self.prev_token.span.between(self.token.span).is_empty() - && !self.look_ahead(1, |tok| tok.can_begin_expr()) - { - let op_span = self.prev_token.span.to(self.token.span); - // Eat the second `-` - self.bump(); - return Err(self.recover_from_postfix_decrement(&lhs, op_span, starts_stmt)); - } + self.recover_from_postfix_inc_op(&lhs, starts_stmt)?; + self.recover_from_postfix_dec_op(&lhs, starts_stmt)?; let op_span = op.span; let op = op.node; diff --git a/compiler/rustc_parse/src/parser/expr/errors.rs b/compiler/rustc_parse/src/parser/expr/errors.rs index edbf844d5bbb0..0008b576fdb4b 100644 --- a/compiler/rustc_parse/src/parser/expr/errors.rs +++ b/compiler/rustc_parse/src/parser/expr/errors.rs @@ -1,5 +1,6 @@ use rustc_ast::util::parser::AssocOp; -use rustc_ast::{BinOpKind, token}; +use rustc_ast::{BinOpKind, Expr, token}; +use rustc_errors::PResult; use rustc_span::Spanned; use crate::diagnostics; @@ -77,4 +78,39 @@ impl<'a> Parser<'a> { self.bump(); } } + + /// Recover from postfix increment operator `++` as found in many C-style languages. + pub(super) fn recover_from_postfix_inc_op( + &mut self, + lhs: &Expr, + starts_stmt: bool, + ) -> PResult<'a, ()> { + if let (token::Plus, token::Plus) = (self.prev_token.kind, self.token.kind) + && self.prev_token.span.hi() == self.token.span.lo() + { + let op_span = self.prev_token.span.to(self.token.span); + self.bump(); // eat the second `+` + Err(self.recover_from_postfix_increment(lhs, op_span, starts_stmt)) + } else { + Ok(()) + } + } + + /// Recover from postfix decrement operator `--` as found in many C-style languages. + pub(super) fn recover_from_postfix_dec_op( + &mut self, + lhs: &Expr, + starts_stmt: bool, + ) -> PResult<'a, ()> { + if let (token::Minus, token::Minus) = (self.prev_token.kind, self.token.kind) + && self.prev_token.span.hi() == self.token.span.lo() + && !self.look_ahead(1, |tok| tok.can_begin_expr()) + { + let op_span = self.prev_token.span.to(self.token.span); + self.bump(); // eat the second `-` + Err(self.recover_from_postfix_decrement(lhs, op_span, starts_stmt)) + } else { + Ok(()) + } + } } From d7bb7ca20583566a35f1d085b5bc6f78e47a3e09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Le=C3=B3n=20Orell=20Valerian=20Liehr?= Date: Thu, 10 Sep 2026 10:59:21 +0200 Subject: [PATCH 6/9] Inline fns & data types related to parse error recovery from C-style inc/dec ops --- .../rustc_parse/src/parser/diagnostics.rs | 178 ------------------ compiler/rustc_parse/src/parser/expr.rs | 8 +- .../rustc_parse/src/parser/expr/errors.rs | 104 +++++++++- 3 files changed, 106 insertions(+), 184 deletions(-) diff --git a/compiler/rustc_parse/src/parser/diagnostics.rs b/compiler/rustc_parse/src/parser/diagnostics.rs index a74525e8c664a..03bac5a23fccf 100644 --- a/compiler/rustc_parse/src/parser/diagnostics.rs +++ b/compiler/rustc_parse/src/parser/diagnostics.rs @@ -141,64 +141,6 @@ impl AttemptLocalParseRecovery { } } -/// Information for emitting suggestions and recovering from -/// C-style `i++`, `--i`, etc. -#[derive(Debug, Copy, Clone)] -struct IncDecRecovery { - /// Is this increment/decrement its own statement? - standalone: IsStandalone, - /// Is this an increment or decrement? - op: IncOrDec, - /// Is this pre- or postfix? - fixity: UnaryFixity, -} - -/// Is an increment or decrement expression its own statement? -#[derive(Debug, Copy, Clone)] -enum IsStandalone { - /// It's standalone, i.e., its own statement. - Standalone, - /// It's a subexpression, i.e., *not* standalone. - Subexpr, -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -enum IncOrDec { - Inc, - Dec, -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -enum UnaryFixity { - Pre, - Post, -} - -impl IncOrDec { - fn chr(&self) -> char { - match self { - Self::Inc => '+', - Self::Dec => '-', - } - } - - fn name(&self) -> &'static str { - match self { - Self::Inc => "increment", - Self::Dec => "decrement", - } - } -} - -impl std::fmt::Display for UnaryFixity { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Pre => write!(f, "prefix"), - Self::Post => write!(f, "postfix"), - } - } -} - /// Checks if the given `lookup` identifier is similar to any keyword symbol in `candidates`. /// /// This is a specialized version of [`Symbol::find_similar`] that constructs an error when a @@ -1653,126 +1595,6 @@ impl<'a> Parser<'a> { Ok(()) } - pub(super) fn recover_from_prefix_increment( - &mut self, - operand_expr: &Expr, - op_span: Span, - start_stmt: bool, - ) -> Diag<'a> { - let standalone = if start_stmt { IsStandalone::Standalone } else { IsStandalone::Subexpr }; - let kind = IncDecRecovery { standalone, op: IncOrDec::Inc, fixity: UnaryFixity::Pre }; - self.recover_from_inc_dec(operand_expr, kind, op_span) - } - - pub(super) fn recover_from_postfix_increment( - &mut self, - operand_expr: &Expr, - op_span: Span, - start_stmt: bool, - ) -> Diag<'a> { - let kind = IncDecRecovery { - standalone: if start_stmt { IsStandalone::Standalone } else { IsStandalone::Subexpr }, - op: IncOrDec::Inc, - fixity: UnaryFixity::Post, - }; - self.recover_from_inc_dec(operand_expr, kind, op_span) - } - - pub(super) fn recover_from_postfix_decrement( - &mut self, - operand_expr: &Expr, - op_span: Span, - start_stmt: bool, - ) -> Diag<'a> { - let kind = IncDecRecovery { - standalone: if start_stmt { IsStandalone::Standalone } else { IsStandalone::Subexpr }, - op: IncOrDec::Dec, - fixity: UnaryFixity::Post, - }; - self.recover_from_inc_dec(operand_expr, kind, op_span) - } - - fn recover_from_inc_dec( - &mut self, - base: &Expr, - kind: IncDecRecovery, - op_span: Span, - ) -> Diag<'a> { - // FIXME: Don't return an error diag, emit the diag here *and* return a new expr of the form - // `$base += 1` / `$base -= 1` (taking `base: Expr` by value) for *proper* recovery. - // (Just emitting the diag would be insufficient since callers would most likely just - // use `$base` as the recovered AST node which would lead to annoying follow-up diags - // like "variable doesn't need to be mutable" getting emitted in some cases.) - - let mut err = self.dcx().struct_span_err( - op_span, - format!("Rust has no {} {} operator", kind.fixity, kind.op.name()), - ); - err.span_label(op_span, format!("not a valid {} operator", kind.fixity)); - - let (pre_span, post_span) = match kind.fixity { - UnaryFixity::Pre => (op_span, base.span.shrink_to_hi()), - UnaryFixity::Post => (base.span.shrink_to_lo(), op_span), - }; - - match kind.standalone { - IsStandalone::Standalone => { - let mut patches = Vec::new(); - if !pre_span.is_empty() { - patches.push((pre_span, String::new())); - } - patches.push((post_span, format!(" {}= 1", kind.op.chr()))); - err.multipart_suggestion( - format!("use `{}= 1` instead", kind.op.chr()), - patches, - Applicability::MachineApplicable, - ); - } - IsStandalone::Subexpr => { - let Ok(base_src) = self.span_to_snippet(base.span) else { - err.help(format!("use `{}= 1` instead", kind.op.chr())); - return err; - }; - match kind.fixity { - UnaryFixity::Pre => { - err.multipart_suggestion( - format!("use `{}= 1` instead", kind.op.chr()), - vec![ - (pre_span, "{ ".to_string()), - (post_span, format!(" {}= 1; {} }}", kind.op.chr(), base_src)), - ], - Applicability::MachineApplicable, - ); - } - UnaryFixity::Post => { - // won't suggest since we can not handle the precedences - // for example: `a + b++` has been parsed (a + b)++ and we can not suggest here - if !matches!(base.kind, ExprKind::Binary(_, _, _)) { - let tmp_var = if base_src.trim() == "tmp" { "tmp_" } else { "tmp" }; - err.multipart_suggestion( - format!("use `{}= 1` instead", kind.op.chr()), - vec![ - (pre_span, format!("{{ let {tmp_var} = ")), - ( - post_span, - format!( - "; {} {}= 1; {} }}", - base_src, - kind.op.chr(), - tmp_var - ), - ), - ], - Applicability::HasPlaceholders, - ); - } - } - } - } - } - err - } - /// Tries to recover from associated item paths like `[T]::AssocItem` / `(T, U)::AssocItem`. /// Attempts to convert the base expression/pattern/type into a type, parses the `::AssocItem` /// tail, and combines them into a `::AssocItem` expression/pattern/type. diff --git a/compiler/rustc_parse/src/parser/expr.rs b/compiler/rustc_parse/src/parser/expr.rs index be44b1b503331..578605a66bd35 100644 --- a/compiler/rustc_parse/src/parser/expr.rs +++ b/compiler/rustc_parse/src/parser/expr.rs @@ -466,7 +466,13 @@ impl<'a> Parser<'a> { this.bump(); let operand = this.parse_expr_dot_or_call(attrs)?; - return Err(this.recover_from_prefix_increment(&operand, pre_span, starts_stmt)); + return Err(this.report_inc_dec_op( + &operand, + starts_stmt, + errors::IncOrDec::Inc, + errors::UnaryFixity::Pre, + pre_span, + )); } token::Ident(..) if this.token.is_keyword(kw::Move) diff --git a/compiler/rustc_parse/src/parser/expr/errors.rs b/compiler/rustc_parse/src/parser/expr/errors.rs index 0008b576fdb4b..18dd6dba13eb5 100644 --- a/compiler/rustc_parse/src/parser/expr/errors.rs +++ b/compiler/rustc_parse/src/parser/expr/errors.rs @@ -1,7 +1,7 @@ use rustc_ast::util::parser::AssocOp; -use rustc_ast::{BinOpKind, Expr, token}; -use rustc_errors::PResult; -use rustc_span::Spanned; +use rustc_ast::{BinOpKind, Expr, ExprKind, token}; +use rustc_errors::{Applicability, Diag, PResult}; +use rustc_span::{Span, Spanned}; use crate::diagnostics; use crate::parser::Parser; @@ -90,7 +90,7 @@ impl<'a> Parser<'a> { { let op_span = self.prev_token.span.to(self.token.span); self.bump(); // eat the second `+` - Err(self.recover_from_postfix_increment(lhs, op_span, starts_stmt)) + Err(self.report_inc_dec_op(lhs, starts_stmt, IncOrDec::Inc, UnaryFixity::Post, op_span)) } else { Ok(()) } @@ -108,9 +108,103 @@ impl<'a> Parser<'a> { { let op_span = self.prev_token.span.to(self.token.span); self.bump(); // eat the second `-` - Err(self.recover_from_postfix_decrement(lhs, op_span, starts_stmt)) + Err(self.report_inc_dec_op(lhs, starts_stmt, IncOrDec::Dec, UnaryFixity::Post, op_span)) } else { Ok(()) } } + + /// Report increment operator `++` & decrement operator `--` as found in many C-style languages. + pub(super) fn report_inc_dec_op( + &mut self, + base: &Expr, + starts_stmt: bool, + op: IncOrDec, + fixity: UnaryFixity, + op_span: Span, + ) -> Diag<'a> { + // FIXME: Don't return an error diag, emit the diag here *and* return a new expr of the form + // `$base += 1` / `$base -= 1` (taking `base: Expr` by value) for *proper* recovery. + // (Just emitting the diag would be insufficient since callers would most likely just + // use `$base` as the recovered AST node which would lead to annoying follow-up diags + // like "variable doesn't need to be mutable" getting emitted in some cases.) + + let mut err = { + let fixity = match fixity { + UnaryFixity::Pre => "prefix", + UnaryFixity::Post => "postfix", + }; + let op = match op { + IncOrDec::Inc => "increment", + IncOrDec::Dec => "decrement", + }; + self.dcx() + .struct_span_err(op_span, format!("Rust has no {fixity} {op} operator")) + .with_span_label(op_span, format!("not a valid {fixity} operator")) + }; + + let op = match op { + IncOrDec::Inc => "+= 1", + IncOrDec::Dec => "-= 1", + }; + let (pre_span, post_span) = match fixity { + UnaryFixity::Pre => (op_span, base.span.shrink_to_hi()), + UnaryFixity::Post => (base.span.shrink_to_lo(), op_span), + }; + + if starts_stmt { + let mut patches = Vec::new(); + if !pre_span.is_empty() { + patches.push((pre_span, String::new())); + } + patches.push((post_span, format!(" {op}"))); + err.multipart_suggestion( + format!("use `{op}` instead"), + patches, + Applicability::MachineApplicable, + ); + } else { + let Ok(base_src) = self.span_to_snippet(base.span) else { + err.help(format!("use `{op}` instead")); + return err; + }; + match fixity { + UnaryFixity::Pre => { + err.multipart_suggestion( + format!("use `{op}` instead"), + vec![(pre_span, "{ ".into()), (post_span, format!(" {op}; {base_src} }}"))], + Applicability::MachineApplicable, + ); + } + UnaryFixity::Post => { + // won't suggest since we can not handle the precedences + // for example: `a + b++` has been parsed (a + b)++ and we can not suggest here + if !matches!(base.kind, ExprKind::Binary(..)) { + let tmp_var = if base_src.trim() == "tmp" { "tmp_" } else { "tmp" }; + err.multipart_suggestion( + format!("use `{op}` instead"), + vec![ + (pre_span, format!("{{ let {tmp_var} = ")), + (post_span, format!("; {base_src} {op}; {tmp_var} }}")), + ], + Applicability::HasPlaceholders, + ); + } + } + } + } + err + } +} + +#[derive(Copy, Clone)] +pub(super) enum IncOrDec { + Inc, + Dec, +} + +#[derive(Copy, Clone)] +pub(super) enum UnaryFixity { + Pre, + Post, } From 9a07848f1bbe70fefbd1b0f5666eafca9e8ca328 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Le=C3=B3n=20Orell=20Valerian=20Liehr?= Date: Thu, 10 Sep 2026 12:05:49 +0200 Subject: [PATCH 7/9] Refactor the way we finish parsing expr ops 1. Remove unnecessary rebindings (`op_span` and `op = op.node`) 2. Remove binding `cur_op_span` as it's equal to `op.span` 3. Merge two `match`es on `op.node` into one to make the control flow more obvious and to render everything more legible. Moreover, it allows us to drop an ungly `unreachable!()` --- compiler/rustc_parse/src/parser/expr.rs | 56 ++++++++++++------------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/compiler/rustc_parse/src/parser/expr.rs b/compiler/rustc_parse/src/parser/expr.rs index 578605a66bd35..00cb7085f3f53 100644 --- a/compiler/rustc_parse/src/parser/expr.rs +++ b/compiler/rustc_parse/src/parser/expr.rs @@ -153,7 +153,6 @@ impl<'a> Parser<'a> { self.expected_token_types.insert(TokenType::Operator); while let Some(op) = self.check_assoc_op() { let lhs_span = self.interpolated_or_expr_span(&lhs); - let cur_op_span = self.token.span; let restrictions = if op.node.is_assign_like() { self.restrictions & Restrictions::NO_STRUCT_LITERAL } else { @@ -186,42 +185,41 @@ impl<'a> Parser<'a> { self.recover_from_postfix_inc_op(&lhs, starts_stmt)?; self.recover_from_postfix_dec_op(&lhs, starts_stmt)?; - let op_span = op.span; - let op = op.node; - // Special cases: - if op == AssocOp::Cast { - lhs = self.parse_assoc_op_cast(lhs, lhs_span, op_span, ExprKind::Cast)?; - continue; - } else if let AssocOp::Range(limits) = op { - // If we didn't have to handle `x..`/`x..=`, it would be pretty easy to - // generalise it to the Fixity::None code. - lhs = self.parse_expr_range(prec, lhs, limits, cur_op_span)?; - break; - } - - let min_prec = match op.fixity() { + let min_prec = match op.node.fixity() { Fixity::Right => Bound::Included(prec), Fixity::Left | Fixity::None => Bound::Excluded(prec), }; - let rhs = self.with_res(restrictions - Restrictions::STMT_EXPR, |this| { - this.parse_expr_assoc(min_prec) - })?; - let span = self.mk_expr_sp(&lhs, lhs_span, op_span, rhs.span); - lhs = match op { + let finish_parsing_bin_op = |this: &mut Self| { + let rhs = this.with_res(restrictions - Restrictions::STMT_EXPR, |this| { + this.parse_expr_assoc(min_prec) + })?; + let span = this.mk_expr_sp(&lhs, lhs_span, op.span, rhs.span); + Ok((rhs, span)) + }; + + lhs = match op.node { AssocOp::Binary(ast_op) => { - let binary = self.mk_binary(respan(cur_op_span, ast_op), lhs, rhs); - self.mk_expr(span, binary) + let (rhs, span) = finish_parsing_bin_op(self)?; + self.mk_expr(span, self.mk_binary(respan(op.span, ast_op), lhs, rhs)) } - AssocOp::Assign => self.mk_expr(span, ExprKind::Assign(lhs, rhs, cur_op_span)), AssocOp::AssignOp(aop) => { - let aopexpr = self.mk_assign_op(respan(cur_op_span, aop), lhs, rhs); - self.mk_expr(span, aopexpr) + let (rhs, span) = finish_parsing_bin_op(self)?; + self.mk_expr(span, self.mk_assign_op(respan(op.span, aop), lhs, rhs)) + } + AssocOp::Assign => { + let (rhs, span) = finish_parsing_bin_op(self)?; + self.mk_expr(span, ExprKind::Assign(lhs, rhs, op.span)) } - AssocOp::Cast | AssocOp::Range(_) => { - self.dcx().span_bug(span, "AssocOp should have been handled by special case") + AssocOp::Cast => { + self.parse_assoc_op_cast(lhs, lhs_span, op.span, ExprKind::Cast)? } + AssocOp::Range(limits) => self.parse_expr_range(min_prec, lhs, limits, op.span)?, }; + + if let AssocOp::Range(_) = op.node { + break; + } } Ok((lhs, parsed_something)) @@ -335,7 +333,7 @@ impl<'a> Parser<'a> { /// The other two variants are handled in `parse_prefix_range_expr` below. fn parse_expr_range( &mut self, - prec: ExprPrecedence, + min_prec: Bound, lhs: Box, limits: RangeLimits, cur_op_span: Span, @@ -343,7 +341,7 @@ impl<'a> Parser<'a> { let rhs = if self.is_at_start_of_range_notation_rhs() { let maybe_lt = self.token; Some( - self.parse_expr_assoc(Bound::Excluded(prec)) + self.parse_expr_assoc(min_prec) .map_err(|err| self.maybe_err_dotdotlt_syntax(maybe_lt, err))?, ) } else { From 29aebbb619d45ba7e28bc67ee911728cb2742d7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Le=C3=B3n=20Orell=20Valerian=20Liehr?= Date: Tue, 25 Aug 2026 16:30:11 +0200 Subject: [PATCH 8/9] Refactor `check_assoc_op` to make it more legible --- compiler/rustc_parse/src/diagnostics.rs | 2 +- compiler/rustc_parse/src/parser/expr.rs | 76 +++++++------------ .../rustc_parse/src/parser/expr/errors.rs | 25 +++++- 3 files changed, 53 insertions(+), 50 deletions(-) diff --git a/compiler/rustc_parse/src/diagnostics.rs b/compiler/rustc_parse/src/diagnostics.rs index fc84591d8813f..29e0aa80786ce 100644 --- a/compiler/rustc_parse/src/diagnostics.rs +++ b/compiler/rustc_parse/src/diagnostics.rs @@ -257,7 +257,7 @@ pub(crate) enum InvalidComparisonOperatorSub { pub(crate) struct InvalidLogicalOperator { #[primary_span] pub span: Span, - pub incorrect: String, + pub incorrect: Symbol, #[subdiagnostic] pub sub: InvalidLogicalOperatorSub, } diff --git a/compiler/rustc_parse/src/parser/expr.rs b/compiler/rustc_parse/src/parser/expr.rs index 00cb7085f3f53..aa9e52026fb27 100644 --- a/compiler/rustc_parse/src/parser/expr.rs +++ b/compiler/rustc_parse/src/parser/expr.rs @@ -273,55 +273,35 @@ impl<'a> Parser<'a> { /// Possibly translate the current token to an associative operator. /// The method does not advance the current token. - /// - /// Also performs recovery for `and` / `or` which are mistaken for `&&` and `||` respectively. pub(super) fn check_assoc_op(&self) -> Option> { - let (op, span) = match (AssocOp::from_token(&self.token), self.token.ident()) { - // When parsing const expressions, stop parsing when encountering `>`. - ( - Some( - AssocOp::Binary(BinOpKind::Shr | BinOpKind::Gt | BinOpKind::Ge) - | AssocOp::AssignOp(AssignOpKind::ShrAssign), - ), - _, - ) if self.restrictions.contains(Restrictions::CONST_EXPR) => { - return None; - } - // When recovering patterns as expressions, stop parsing when encountering an - // assignment `=`, an alternative `|`, or a range `..`. - ( - Some( - AssocOp::Assign - | AssocOp::AssignOp(_) - | AssocOp::Binary(BinOpKind::BitOr) - | AssocOp::Range(_), - ), - _, - ) if self.restrictions.contains(Restrictions::IS_PAT) => { - return None; - } - (Some(op), _) => (op, self.token.span), - (None, Some((Ident { name: sym::and, span }, IdentIsRaw::No))) - if self.may_recover() => - { - self.dcx().emit_err(diagnostics::InvalidLogicalOperator { - span: self.token.span, - incorrect: "and".into(), - sub: diagnostics::InvalidLogicalOperatorSub::Conjunction(self.token.span), - }); - (AssocOp::Binary(BinOpKind::And), span) - } - (None, Some((Ident { name: sym::or, span }, IdentIsRaw::No))) if self.may_recover() => { - self.dcx().emit_err(diagnostics::InvalidLogicalOperator { - span: self.token.span, - incorrect: "or".into(), - sub: diagnostics::InvalidLogicalOperatorSub::Disjunction(self.token.span), - }); - (AssocOp::Binary(BinOpKind::Or), span) - } - _ => return None, - }; - Some(respan(span, op)) + let op = AssocOp::from_token(&self.token); + + // When parsing const expressions, stop parsing when encountering `>`. + if self.restrictions.contains(Restrictions::CONST_EXPR) + && let Some(op) = op + && let AssocOp::Binary(BinOpKind::Shr | BinOpKind::Gt | BinOpKind::Ge) + | AssocOp::AssignOp(AssignOpKind::ShrAssign) = op + { + return None; + } + + // When recovering patterns as expressions, stop parsing when encountering an + // assignment `=`, an alternative `|`, or a range `..`. + if self.restrictions.contains(Restrictions::IS_PAT) + && let Some(op) = op + && let AssocOp::Assign + | AssocOp::AssignOp(_) + | AssocOp::Binary(BinOpKind::BitOr) + | AssocOp::Range(_) = op + { + return None; + } + + if let Some(op) = op { + return Some(respan(self.token.span, op)); + } + + self.recover_from_alpha_logic_op() } /// Checks if this expression is a successfully parsed statement. diff --git a/compiler/rustc_parse/src/parser/expr/errors.rs b/compiler/rustc_parse/src/parser/expr/errors.rs index 18dd6dba13eb5..4b9e288f3ec1b 100644 --- a/compiler/rustc_parse/src/parser/expr/errors.rs +++ b/compiler/rustc_parse/src/parser/expr/errors.rs @@ -1,12 +1,35 @@ use rustc_ast::util::parser::AssocOp; use rustc_ast::{BinOpKind, Expr, ExprKind, token}; use rustc_errors::{Applicability, Diag, PResult}; -use rustc_span::{Span, Spanned}; +use rustc_span::{Span, Spanned, respan, sym}; use crate::diagnostics; use crate::parser::Parser; impl<'a> Parser<'a> { + /// Recover from alphabetic logic operators `and` and `or` as found in e.g., Python and PHP. + pub(super) fn recover_from_alpha_logic_op(&self) -> Option> { + if self.may_recover() + && let Some((ident, token::IdentIsRaw::No)) = self.token.ident() + { + let (op, sub): (_, fn(_) -> _) = match ident.name { + sym::and => (BinOpKind::And, diagnostics::InvalidLogicalOperatorSub::Conjunction), + sym::or => (BinOpKind::Or, diagnostics::InvalidLogicalOperatorSub::Disjunction), + _ => return None, + }; + + self.dcx().emit_err(diagnostics::InvalidLogicalOperator { + span: self.token.span, + incorrect: ident.name, + sub: sub(self.token.span), + }); + + Some(respan(self.token.span, AssocOp::Binary(op))) + } else { + None + } + } + /// Reject `...` being used as an expression operator. pub(super) fn reject_dotdotdot_expr_op(&self) { if self.token == token::DotDotDot { From 129a2b9364362c8b8e9bc753f9cd71bdfe3cdc2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Le=C3=B3n=20Orell=20Valerian=20Liehr?= Date: Fri, 11 Sep 2026 10:24:51 +0200 Subject: [PATCH 9/9] Don't mistake `<->` for `<>` Previously we would check if the current operator was `Binary(Lt)` and the current token was `>` to determine if we're looking at `<>`. However, since `AssocOp::from_token` also treats `<-` as `Binary(Lt)` for better error recovery, the condition would also hold for `<->` (`<-`, `>`) which is not what we want. E.g., given `1 <-> 2` we would previously emit diagnostic "invalid comparison operator `<>`". --- Also update `recover_from_spaceship_cmp_op` to do something similar -- not to fix anything but simply to eliminate param `op: Spanned`. --- compiler/rustc_parse/src/parser/expr.rs | 4 ++-- compiler/rustc_parse/src/parser/expr/errors.rs | 14 ++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/compiler/rustc_parse/src/parser/expr.rs b/compiler/rustc_parse/src/parser/expr.rs index aa9e52026fb27..eaef230af7e1f 100644 --- a/compiler/rustc_parse/src/parser/expr.rs +++ b/compiler/rustc_parse/src/parser/expr.rs @@ -180,8 +180,8 @@ impl<'a> Parser<'a> { } self.recover_from_strict_eq_op(op); - self.recover_from_diamond_ne_op(op); - self.recover_from_spaceship_cmp_op(op); + self.recover_from_diamond_ne_op(); + self.recover_from_spaceship_cmp_op(); self.recover_from_postfix_inc_op(&lhs, starts_stmt)?; self.recover_from_postfix_dec_op(&lhs, starts_stmt)?; diff --git a/compiler/rustc_parse/src/parser/expr/errors.rs b/compiler/rustc_parse/src/parser/expr/errors.rs index 4b9e288f3ec1b..707ae5d34bc75 100644 --- a/compiler/rustc_parse/src/parser/expr/errors.rs +++ b/compiler/rustc_parse/src/parser/expr/errors.rs @@ -67,12 +67,11 @@ impl<'a> Parser<'a> { } /// Recover from inequality operator `<>` ("diamond") as found in e.g., PHP. - pub(super) fn recover_from_diamond_ne_op(&mut self, op: Spanned) { - if op.node == AssocOp::Binary(BinOpKind::Lt) - && self.token == token::Gt + pub(super) fn recover_from_diamond_ne_op(&mut self) { + if let (token::Lt, token::Gt) = (self.prev_token.kind, self.token.kind) && self.prev_token.span.hi() == self.token.span.lo() { - let sp = op.span.to(self.token.span); + let sp = self.prev_token.span.to(self.token.span); self.dcx().emit_err(diagnostics::InvalidComparisonOperator { span: sp, invalid: "<>".into(), @@ -87,12 +86,11 @@ impl<'a> Parser<'a> { } /// Recover from comparison operator `<=>` ("spaceship") as found in e.g., C++. - pub(super) fn recover_from_spaceship_cmp_op(&mut self, op: Spanned) { - if op.node == AssocOp::Binary(BinOpKind::Le) - && self.token == token::Gt + pub(super) fn recover_from_spaceship_cmp_op(&mut self) { + if let (token::Le, token::Gt) = (self.prev_token.kind, self.token.kind) && self.prev_token.span.hi() == self.token.span.lo() { - let sp = op.span.to(self.token.span); + let sp = self.prev_token.span.to(self.token.span); self.dcx().emit_err(diagnostics::InvalidComparisonOperator { span: sp, invalid: "<=>".into(),