Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion compiler/rustc_parse/src/parser/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2844,7 +2844,11 @@ impl<'a> Parser<'a> {
{
return false;
}
let label = self.eat_label().expect("just checked if a label exists");
let (label, err) = self.eat_label();
if let Some(e) = err {
e.emit();
}
let label = label.expect("just checked if a label exists");
self.bump(); // eat `:`
let span = label.ident.span.to(self.prev_token.span);
let mut diag = self
Expand Down
155 changes: 139 additions & 16 deletions compiler/rustc_parse/src/parser/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,9 +274,70 @@ impl<'a> Parser<'a> {
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 mut rhs_restrictions = restrictions - Restrictions::STMT_EXPR;
if matches!(op, AssocOp::Binary(BinOpKind::Lt))
&& matches!(lhs.kind, ExprKind::Path(..))
{
rhs_restrictions |= Restrictions::IS_RHS_OF_LT_AFTER_PATH;
}
let rhs = if op == AssocOp::Binary(BinOpKind::Lt) {
// Make the path ident available to `parse_expr_labeled` so the turbofish
// suggestion can name the actual path (e.g. "for `Struct`").
if let ExprKind::Path(_, ref path) = lhs.kind {
self.expected_turbofish_context =
path.segments.last().map(|seg| (op_span, seg.ident));
}
let res = self.with_res(rhs_restrictions, |this| this.parse_expr_assoc(min_prec));
// Clear regardless of whether recovery fired; the context is only valid for
// this one `<` operator.
self.expected_turbofish_context = None;
res?
} else {
self.with_res(rhs_restrictions, |this| this.parse_expr_assoc(min_prec))?
};

// Recover `Struct<'_>` missing its turbofish `::`.
//
// `parse_expr_labeled` set `turbofish_missing_lifetime_recovery` and returned
// an `ExprKind::Err` sentinel after emitting the diagnostic. We now have all
// the information needed to rebuild a well-formed path with the lifetime grafted
// onto the last segment, and then resume normal postfix parsing.
if let Some((ident, span)) = self.turbofish_missing_lifetime_recovery.take() {
if let ExprKind::Err(_) = rhs.kind {
let mut lhs_expr = *lhs;
if let ExprKind::Path(qself, mut path) = lhs_expr.kind {
// Graft the lifetime onto the `lhs` path.
if let Some(last_segment) = path.segments.last_mut() {
let arg = ast::GenericArg::Lifetime(ast::Lifetime {
id: ast::DUMMY_NODE_ID,
ident,
});
let args = ast::AngleBracketedArgs {
span: op_span.to(span),
args: thin_vec::thin_vec![ast::AngleBracketedArg::Arg(arg)],
};
last_segment.args =
Some(Box::new(ast::GenericArgs::AngleBracketed(args)));
}

// Resume parsing as a struct literal.
if self.token == token::OpenBrace {
if let Some(expr) = self.maybe_parse_struct_expr(&qself, &path) {
lhs = expr?;
continue;
}
}

// Otherwise, resume parsing as a postfix expression (e.g. function call).
lhs_expr.kind = ExprKind::Path(qself, path);
let span = lhs_expr.span;
lhs = Box::new(lhs_expr);
lhs = self.parse_expr_dot_or_call_with(ast::AttrVec::new(), lhs, span)?;
continue;
}
lhs = Box::new(lhs_expr);
}
}

let span = self.mk_expr_sp(&lhs, lhs_span, op_span, rhs.span);
lhs = match op {
Expand Down Expand Up @@ -680,7 +741,7 @@ impl<'a> Parser<'a> {
segment.ident.span,
),
};
match self.parse_expr_labeled(label, false) {
match self.parse_expr_labeled(label, false, None) {
Ok(expr) => {
type_err.cancel();
self.dcx().emit_err(diagnostics::MalformedLoopLabel {
Expand Down Expand Up @@ -874,6 +935,14 @@ impl<'a> Parser<'a> {
mut e: Box<Expr>,
lo: Span,
) -> PResult<'a, Box<Expr>> {
// When recovering a missing turbofish lifetime, `e` is an `ExprKind::Err` sentinel
// that `parse_expr_assoc_rest` will replace with the correctly-reconstructed node.
// Skip postfix parsing here so we don't attach method/call/index suffixes to the
// error node before the replacement happens.
if self.turbofish_missing_lifetime_recovery.is_some() {
return Ok(e);
}

let mut res = loop {
let has_question = if self.prev_token == TokenKind::Ident(kw::Return, IdentIsRaw::No) {
// We are using noexpect here because we don't expect a `?` directly after
Expand Down Expand Up @@ -1518,8 +1587,8 @@ impl<'a> Parser<'a> {
}
} else if this.eat_keyword(exp!(While)) {
this.parse_expr_while(None, lo)
} else if let Some(label) = this.eat_label() {
this.parse_expr_labeled(label, true)
} else if let (Some(label), err) = this.eat_label() {
this.parse_expr_labeled(label, true, err)
} else if this.eat_keyword(exp!(Loop)) {
this.parse_expr_loop(None, lo).map_err(|mut err| {
err.span_label(lo, "while parsing this `loop` expression");
Expand Down Expand Up @@ -1714,10 +1783,57 @@ impl<'a> Parser<'a> {
&mut self,
label_: Label,
mut consume_colon: bool,
mut label_err: Option<Diag<'a>>,
) -> PResult<'a, Box<Expr>> {
let lo = label_.ident.span;
let label = Some(label_);
let ate_colon = self.eat(exp!(Colon));

// Intercept `Struct<'_>` / `Struct<'a>` / `Struct<'abc>` missing their turbofish `::`.
// Fires when the lifetime is on the RHS of `<` after a path, has no following `:`, and
// the next token is `>` — i.e. this was never a real label, just a mistyped type arg.
// Reserved-keyword lifetimes (`'_`) already carry a diag; named ones need a fresh one.
if !ate_colon
&& self.restrictions.contains(Restrictions::IS_RHS_OF_LT_AFTER_PATH)
&& self.check_noexpect(&token::Gt)
{
let mut err = if let Some(e) = label_err.take() {
e
} else {
self.dcx().create_err(diagnostics::UnexpectedTokenAfterLabel {
span: self.token.span,
remove_label: None,
enclose_in_block: None,
})
};
let (op_span, ident) = self.expected_turbofish_context.take().expect(
"IS_RHS_OF_LT_AFTER_PATH is only ever set alongside expected_turbofish_context \
in parse_expr_assoc_rest, so this must be Some here",
Comment on lines +1809 to +1811

@raushan728 raushan728 Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

code handled three cases when building the turbofish suggestion a path aware span_suggestion, a fallback span_suggestion using just a bare lifetime span, and a final bare .help() with no span at all. In practice only the first one is ever reachable the restriction flag that gates this whole recovery branch (IS_RHS_OF_LT_AFTER_PATH) and the path context used to build the message are always set together, in the same code path in parse_expr_assoc_rest, and a Path is guaranteed to have at least one segment. So the other two branches could never actually fire with the current call graph.

I'd originally kept them as a defensive fallback in case some future change set the restriction flag from a different call site without also populating the context - but on reflection that's the wrong kind of defensiveness for a compiler internal it would silently degrade to a less informative message instead of surfacing the broken invariant. I replaced it with a single .expect() carrying an explicit message about the invariant, so if this assumption is ever violated by future changes, it fails loudly and immediately at the exact point of violation.

This also drops the now-unused lt_span parameter from parse_expr_labeled entirely, which was only ever feeding the dead fallback branch.

View changes since the review

);
err.span_suggestion(
op_span.shrink_to_lo(),
format!(
"use `::<...>` instead of `<...>` to specify lifetime arguments for `{ident}`"
),
"::",
Applicability::MachineApplicable,
);
err.emit();
self.bump(); // consume `>`

// Return an `Err` sentinel so `parse_expr_assoc_rest` can detect the recovery and
// graft the lifetime onto the lhs path. We cannot build the final node here because
// `lhs` (the path being parameterized) is owned by the caller above us.
self.turbofish_missing_lifetime_recovery = Some((label_.ident, label_.ident.span));
return Ok(
self.mk_expr_err(lo, self.dcx().delayed_bug("turbofish_missing_lifetime_recovery"))
);
}

if let Some(e) = label_err.take() {
e.emit();
}

let tok_sp = self.token.span;
let expr = if self.eat_keyword(exp!(While)) {
self.parse_expr_while(label, lo)
Expand Down Expand Up @@ -1909,13 +2025,16 @@ impl<'a> Parser<'a> {
/// with a labeled loop does not even get a warning because there is no ambiguity.
fn parse_expr_break(&mut self) -> PResult<'a, Box<Expr>> {
let lo = self.prev_token.span;
let mut label = self.eat_label();
let (mut label, err) = self.eat_label();
if let Some(e) = err {
e.emit();
}
let kind = if self.token == token::Colon
&& let Some(label) = label.take()
{
// The value expression can be a labeled loop, see issue #86948, e.g.:
// `loop { break 'label: loop { break 'label 42; }; }`
let lexpr = self.parse_expr_labeled(label, true)?;
let lexpr = self.parse_expr_labeled(label, true, None)?;
self.dcx().emit_err(diagnostics::LabeledLoopInBreak {
span: lexpr.span,
sub: diagnostics::WrapInParentheses::Expression {
Expand Down Expand Up @@ -1976,7 +2095,10 @@ impl<'a> Parser<'a> {

/// Parse `"continue" label?`.
fn parse_expr_continue(&mut self, lo: Span) -> PResult<'a, Box<Expr>> {
let mut label = self.eat_label();
let (mut label, err) = self.eat_label();
if let Some(e) = err {
e.emit();
}

// Recover `continue label` -> `continue 'label`
if self.may_recover()
Expand Down Expand Up @@ -3162,17 +3284,18 @@ impl<'a> Parser<'a> {
))
}

pub(crate) fn eat_label(&mut self) -> Option<Label> {
pub(crate) fn eat_label(&mut self) -> (Option<Label>, Option<Diag<'a>>) {
if let Some((ident, is_raw)) = self.token.lifetime() {
// Disallow `'fn`, but with a better error message than `expect_lifetime`.
if is_raw == IdentIsRaw::No && ident.without_first_quote().is_reserved() {
self.dcx().emit_err(diagnostics::KeywordLabel { span: ident.span });
}

let err = if is_raw == IdentIsRaw::No && ident.without_first_quote().is_reserved() {
Some(self.dcx().create_err(diagnostics::KeywordLabel { span: ident.span }))
} else {
None
};
self.bump();
Some(Label { ident })
(Some(Label { ident }), err)
} else {
None
(None, None)
}
}

Expand Down
11 changes: 9 additions & 2 deletions compiler/rustc_parse/src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ bitflags::bitflags! {
/// expression, but halts parsing the expression when reaching certain
/// tokens like `=`.
const IS_PAT = 1 << 5;
/// Used to detect missing turbofish `path < 'a >` inside the RHS of `<`.
const IS_RHS_OF_LT_AFTER_PATH = 1 << 6;
}
}

Expand Down Expand Up @@ -241,13 +243,18 @@ pub struct Parser<'a> {
in_fn_body: bool = false,
/// Whether we have detected a missing semicolon in function body.
pub fn_body_missing_semi_guar: Option<ErrorGuaranteed> = None,
/// Context passed down to `parse_expr_labeled` for turbofish recovery.
pub expected_turbofish_context: Option<(Span, Ident)> = None,
/// Recovery state when encountering a missing turbofish for a lifetime argument.
pub turbofish_missing_lifetime_recovery: Option<(Ident, Span)> = None,
}

// This type is used a lot, e.g. it's cloned when matching many declarative macro rules with
// nonterminals. Make sure it doesn't unintentionally get bigger. We only check a few arches
// `Parser::try_parse`. Ensure the `Parser` doesn't get excessively large, as it's passed around a lot.
// We're excluding `i686` and `powerpc` here, as the assert fails due to alignment differences,
// though, because `TokenTypeSet(u128)` alignment varies on others, changing the total size.
#[cfg(all(target_pointer_width = "64", any(target_arch = "aarch64", target_arch = "x86_64")))]
rustc_data_structures::static_assert_size!(Parser<'_>, 288);
rustc_data_structures::static_assert_size!(Parser<'_>, 336);

/// Stores span information about a closure.
#[derive(Clone, Debug)]
Expand Down
2 changes: 1 addition & 1 deletion compiler/rustc_parse/src/parser/stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1032,7 +1032,7 @@ impl<'a> Parser<'a> {
segment.ident.span,
),
};
match self.parse_expr_labeled(label, false) {
match self.parse_expr_labeled(label, false, None) {
Ok(labeled_expr) => {
e.cancel();
self.dcx().emit_err(MalformedLoopLabel {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//@ run-rustfix
struct Struct<'a>(&'a str);

fn main() {
let s = Struct::<'_>("hi");
//~^ ERROR labels cannot use keyword names
//~| HELP use `::<...>` instead of `<...>` to specify lifetime arguments for `Struct`
println!("{}", s.0);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//@ run-rustfix
struct Struct<'a>(&'a str);

fn main() {
let s = Struct<'_>("hi");
//~^ ERROR labels cannot use keyword names
//~| HELP use `::<...>` instead of `<...>` to specify lifetime arguments for `Struct`
println!("{}", s.0);
}
13 changes: 13 additions & 0 deletions tests/ui/parser/recover/missing-turbofish-lifetime-run-pass.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
error: labels cannot use keyword names
--> $DIR/missing-turbofish-lifetime-run-pass.rs:5:20
|
LL | let s = Struct<'_>("hi");
| ^^
|
help: use `::<...>` instead of `<...>` to specify lifetime arguments for `Struct`
|
LL | let s = Struct::<'_>("hi");
| ++

error: aborting due to 1 previous error

26 changes: 26 additions & 0 deletions tests/ui/parser/recover/missing-turbofish-lifetime.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
struct Struct<'a> {
string: &'a str,
}

fn struct_with_reserved_lifetime() {
let _ = Struct<'_> {
//~^ ERROR labels cannot use keyword names
string: "",
};
}

fn struct_with_named_lifetime<'a>() {
let _ = Struct<'a> {
//~^ ERROR expected `while`, `for`, `loop` or `{` after a label
string: "",
};
}

fn struct_with_multichar_lifetime<'abc>() {
let _ = Struct<'abc> {
//~^ ERROR expected `while`, `for`, `loop` or `{` after a label
string: "",
};
}

fn main() {}
35 changes: 35 additions & 0 deletions tests/ui/parser/recover/missing-turbofish-lifetime.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
error: labels cannot use keyword names
--> $DIR/missing-turbofish-lifetime.rs:6:20
|
LL | let _ = Struct<'_> {
| ^^
|
help: use `::<...>` instead of `<...>` to specify lifetime arguments for `Struct`
|
LL | let _ = Struct::<'_> {
| ++

error: expected `while`, `for`, `loop` or `{` after a label
--> $DIR/missing-turbofish-lifetime.rs:13:22
|
LL | let _ = Struct<'a> {
| ^ expected `while`, `for`, `loop` or `{` after a label
|
help: use `::<...>` instead of `<...>` to specify lifetime arguments for `Struct`
|
LL | let _ = Struct::<'a> {
| ++

error: expected `while`, `for`, `loop` or `{` after a label
--> $DIR/missing-turbofish-lifetime.rs:20:24
|
LL | let _ = Struct<'abc> {
| ^ expected `while`, `for`, `loop` or `{` after a label
|
help: use `::<...>` instead of `<...>` to specify lifetime arguments for `Struct`
|
LL | let _ = Struct::<'abc> {
| ++

error: aborting due to 3 previous errors

6 changes: 2 additions & 4 deletions tests/ui/parser/require-parens-for-chained-comparison.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,9 @@ fn main() {
//~| ERROR labels cannot use keyword names

f<'_>();
//~^ ERROR comparison operators cannot be chained
//~| HELP use `::<...>` instead of `<...>` to specify lifetime, type, or const arguments
//~| ERROR expected
//~| HELP add `'` to close the char literal
//~^ ERROR cannot find function `f` in this scope
//~| ERROR labels cannot use keyword names
//~| HELP use `::<...>` instead of `<...>` to specify lifetime arguments for `f`

let _ = f<u8>;
//~^ ERROR comparison operators cannot be chained
Expand Down
Loading
Loading