Skip to content

Retry failed macro matching for diagnostics #103898

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Nov 11, 2022
Merged
Changes from 1 commit
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
118 changes: 109 additions & 9 deletions compiler/rustc_expand/src/mbe/macro_rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -316,16 +316,116 @@ fn expand_macro<'cx>(
is_local,
});
}
Err(()) => {
todo!("Retry macro invocation while tracking diagnostics info and emit error");

Err(CanRetry::No(_)) => {
debug!("Will not retry matching as an error was emitted already");
return DummyResult::any(sp);
}
Err(CanRetry::Yes) => {
// Retry and emit a better error below.
}
}

// An error occured, try the expansion again, tracking the expansion closely for better diagnostics
let mut tracker = CollectTrackerAndEmitter::new(cx, sp);

let try_success_result = try_match_macro(sess, name, &arg, lhses, &mut tracker);
assert!(try_success_result.is_err(), "Macro matching returned a success on the second try");

if let Some(result) = tracker.result {
// An irrecoverable error occured and has been emitted.
return result;
}

let Some((token, label)) = tracker.best_failure else {
return tracker.result.expect("must have encountered Error or ErrorReported");
};

let span = token.span.substitute_dummy(sp);

let mut err = cx.struct_span_err(span, &parse_failure_msg(&token));
err.span_label(span, label);
if !def_span.is_dummy() && !cx.source_map().is_imported(def_span) {
err.span_label(cx.source_map().guess_head_span(def_span), "when calling this macro");
}

annotate_doc_comment(&mut err, sess.source_map(), span);

// Check whether there's a missing comma in this macro call, like `println!("{}" a);`
if let Some((arg, comma_span)) = arg.add_comma() {
for lhs in lhses {
let parser = parser_from_cx(sess, arg.clone());
let mut tt_parser = TtParser::new(name);

if let Success(_) =
tt_parser.parse_tt(&mut Cow::Borrowed(&parser), lhs, &mut NoopTracker)
{
if comma_span.is_dummy() {
err.note("you might be missing a comma");
} else {
err.span_suggestion_short(
comma_span,
"missing comma here",
", ",
Applicability::MachineApplicable,
);
}
}
}
}
err.emit();
cx.trace_macros_diag();
DummyResult::any(sp)
}

/// The tracker used for the slow error path that collects useful info for diagnostics
struct CollectTrackerAndEmitter<'a, 'cx> {
cx: &'a mut ExtCtxt<'cx>,
/// Which arm's failure should we report? (the one furthest along)
best_failure: Option<(Token, &'static str)>,
root_span: Span,
result: Option<Box<dyn MacResult + 'cx>>,
}

impl<'a, 'cx, 'matcher> Tracker<'matcher> for CollectTrackerAndEmitter<'a, 'cx> {
fn before_match_loc(&mut self, _parser: &TtParser, _matcher: &'matcher MatcherLoc) {
// Empty for now.
}

fn after_arm(&mut self, result: &NamedParseResult) {
match result {
Success(_) => {
unreachable!("should not collect detailed info for successful macro match");
}
Failure(token, msg) => match self.best_failure {
Some((ref best_token, _)) if best_token.span.lo() >= token.span.lo() => {}
_ => self.best_failure = Some((token.clone(), msg)),
},
Error(err_sp, msg) => {
let span = err_sp.substitute_dummy(self.root_span);
self.cx.struct_span_err(span, msg).emit();
self.result = Some(DummyResult::any(span));
}
ErrorReported(_) => self.result = Some(DummyResult::any(self.root_span)),
}
}

fn description() -> &'static str {
"detailed"
}
}

impl<'a, 'cx> CollectTrackerAndEmitter<'a, 'cx> {
fn new(cx: &'a mut ExtCtxt<'cx>, root_span: Span) -> Self {
Self { cx, best_failure: None, root_span, result: None }
}
}

enum CanRetry {
Yes,
/// We are not allowed to retry macro expansion as a fatal error has been emitted already.
No(ErrorGuaranteed),
}

/// Try expanding the macro. Returns the index of the sucessful arm and its named_matches if it was successful,
/// and nothing if it failed. On failure, it's the callers job to use `track` accordingly to record all errors
/// correctly.
Expand All @@ -335,7 +435,7 @@ fn try_match_macro<'matcher, T: Tracker<'matcher>>(
arg: &TokenStream,
lhses: &'matcher [Vec<MatcherLoc>],
track: &mut T,
) -> Result<(usize, NamedMatches), ()> {
) -> Result<(usize, NamedMatches), CanRetry> {
// We create a base parser that can be used for the "black box" parts.
// Every iteration needs a fresh copy of that parser. However, the parser
// is not mutated on many of the iterations, particularly when dealing with
Expand Down Expand Up @@ -383,10 +483,10 @@ fn try_match_macro<'matcher, T: Tracker<'matcher>>(
}
Error(_, _) => {
// We haven't emitted an error yet
return Err(());
return Err(CanRetry::Yes);
}
ErrorReported(_) => {
return Err(());
ErrorReported(guarantee) => {
return Err(CanRetry::No(guarantee));
}
}

Expand All @@ -395,7 +495,7 @@ fn try_match_macro<'matcher, T: Tracker<'matcher>>(
mem::swap(&mut gated_spans_snapshot, &mut sess.gated_spans.spans.borrow_mut());
}

Err(())
Err(CanRetry::Yes)
}

// Note that macro-by-example's input is also matched against a token tree:
Expand Down Expand Up @@ -478,7 +578,7 @@ pub fn compile_declarative_macro(
let mut tt_parser =
TtParser::new(Ident::with_dummy_span(if macro_rules { kw::MacroRules } else { kw::Macro }));
let argument_map =
match tt_parser.parse_tt(&mut Cow::Borrowed(&parser), &argument_gram, &mut NoopTracker) {
match tt_parser.parse_tt(&mut Cow::Owned(parser), &argument_gram, &mut NoopTracker) {
Success(m) => m,
Failure(token, msg) => {
let s = parse_failure_msg(&token);
Expand Down