Skip to content

Commit f0565f5

Browse files
committed
Overhaul how stashed diagnostics work, again.
Stashed errors used to be counted as errors, but could then be cancelled, leading to `ErrorGuaranteed` soundness holes. #120828 changed that, closing the soundness hole. But it introduced other difficulties because you sometimes have to account for pending stashed errors when making decisions about whether errors have occured/will occur and it's easy to overlook these. This commit aims for a middle ground. - Stashed errors (not warnings) are counted immediately as emitted errors, avoiding the possibility of forgetting to consider them. - The ability to cancel (or downgrade) stashed errors is eliminated, by disallowing the use of `steal_diagnostic` with errors, and introducing the more restrictive methods `try_steal_{modify,replace}_and_emit_err` that can be used instead. Other things: - `DiagnosticBuilder::stash` and `DiagCtxt::stash_diagnostic` now both return `Option<ErrorGuaranteed>`, which enables the removal of two `delayed_bug` calls and one `Ty::new_error_with_message` call. This is possible because we store error guarantees in `DiagCtxt::stashed_diagnostics`. - Storing the guarantees also saves us having to maintain a counter. - Calls to the `stashed_err_count` method are no longer necessary alongside calls to `has_errors`, which is a nice simplification, and eliminates two more `span_delayed_bug` calls and one FIXME comment. - Tests are added for three of the four fixed PRs mentioned below. - `issue-121108.rs`'s output improved slightly, omitting a non-useful error message. Fixes #121451. Fixes #121477. Fixes #121504. Fixes #121508.
1 parent 99a4015 commit f0565f5

File tree

29 files changed

+405
-295
lines changed

29 files changed

+405
-295
lines changed

Diff for: compiler/rustc_errors/src/diagnostic.rs

+3-5
Original file line numberDiff line numberDiff line change
@@ -1295,11 +1295,9 @@ impl<'a, G: EmissionGuarantee> DiagnosticBuilder<'a, G> {
12951295
drop(self);
12961296
}
12971297

1298-
/// Stashes diagnostic for possible later improvement in a different,
1299-
/// later stage of the compiler. The diagnostic can be accessed with
1300-
/// the provided `span` and `key` through [`DiagCtxt::steal_diagnostic()`].
1301-
pub fn stash(mut self, span: Span, key: StashKey) {
1302-
self.dcx.stash_diagnostic(span, key, self.take_diag());
1298+
/// See `DiagCtxt::stash_diagnostic` for details.
1299+
pub fn stash(mut self, span: Span, key: StashKey) -> Option<ErrorGuaranteed> {
1300+
self.dcx.stash_diagnostic(span, key, self.take_diag())
13031301
}
13041302

13051303
/// Delay emission of this diagnostic as a bug.

Diff for: compiler/rustc_errors/src/lib.rs

+149-62
Original file line numberDiff line numberDiff line change
@@ -435,10 +435,6 @@ struct DiagCtxtInner {
435435
/// The delayed bugs and their error guarantees.
436436
delayed_bugs: Vec<(DelayedDiagnostic, ErrorGuaranteed)>,
437437

438-
/// The number of stashed errors. Unlike the other counts, this can go up
439-
/// and down, so it doesn't guarantee anything.
440-
stashed_err_count: usize,
441-
442438
/// The error count shown to the user at the end.
443439
deduplicated_err_count: usize,
444440
/// The warning count shown to the user at the end.
@@ -476,7 +472,7 @@ struct DiagCtxtInner {
476472
/// add more information). All stashed diagnostics must be emitted with
477473
/// `emit_stashed_diagnostics` by the time the `DiagCtxtInner` is dropped,
478474
/// otherwise an assertion failure will occur.
479-
stashed_diagnostics: FxIndexMap<(Span, StashKey), Diagnostic>,
475+
stashed_diagnostics: FxIndexMap<(Span, StashKey), (Diagnostic, Option<ErrorGuaranteed>)>,
480476

481477
future_breakage_diagnostics: Vec<Diagnostic>,
482478

@@ -562,8 +558,14 @@ impl Drop for DiagCtxtInner {
562558
fn drop(&mut self) {
563559
// Any stashed diagnostics should have been handled by
564560
// `emit_stashed_diagnostics` by now.
561+
//
562+
// Important: it is sound to produce an `ErrorGuaranteed` when stashing
563+
// errors because they are guaranteed to have been emitted by here.
565564
assert!(self.stashed_diagnostics.is_empty());
566565

566+
// Important: it is sound to produce an `ErrorGuaranteed` when emitting
567+
// delayed bugs because they are guaranteed to be emitted here if
568+
// necessary.
567569
if self.err_guars.is_empty() {
568570
self.flush_delayed()
569571
}
@@ -616,7 +618,6 @@ impl DiagCtxt {
616618
err_guars: Vec::new(),
617619
lint_err_guars: Vec::new(),
618620
delayed_bugs: Vec::new(),
619-
stashed_err_count: 0,
620621
deduplicated_err_count: 0,
621622
deduplicated_warn_count: 0,
622623
emitter,
@@ -677,7 +678,6 @@ impl DiagCtxt {
677678
err_guars,
678679
lint_err_guars,
679680
delayed_bugs,
680-
stashed_err_count,
681681
deduplicated_err_count,
682682
deduplicated_warn_count,
683683
emitter: _,
@@ -700,7 +700,6 @@ impl DiagCtxt {
700700
*err_guars = Default::default();
701701
*lint_err_guars = Default::default();
702702
*delayed_bugs = Default::default();
703-
*stashed_err_count = 0;
704703
*deduplicated_err_count = 0;
705704
*deduplicated_warn_count = 0;
706705
*must_produce_diag = false;
@@ -716,39 +715,111 @@ impl DiagCtxt {
716715
*fulfilled_expectations = Default::default();
717716
}
718717

719-
/// Stash a given diagnostic with the given `Span` and [`StashKey`] as the key.
720-
/// Retrieve a stashed diagnostic with `steal_diagnostic`.
721-
pub fn stash_diagnostic(&self, span: Span, key: StashKey, diag: Diagnostic) {
722-
let mut inner = self.inner.borrow_mut();
723-
724-
let key = (span.with_parent(None), key);
725-
726-
if diag.is_error() {
727-
if diag.is_lint.is_none() {
728-
inner.stashed_err_count += 1;
729-
}
730-
}
718+
/// Stashes a diagnostic for possible later improvement in a different,
719+
/// later stage of the compiler. Possible actions depend on the diagnostic
720+
/// level:
721+
/// - Level::Error: immediately counted as an error that has occurred, because it
722+
/// is guaranteed to be emitted eventually. Can be later accessed with the
723+
/// provided `span` and `key` through
724+
/// [`DiagCtxt::try_steal_modify_and_emit_err`] or
725+
/// [`DiagCtxt::try_steal_replace_and_emit_err`]. These do not allow
726+
/// cancellation or downgrading of the error. Returns
727+
/// `Some(ErrorGuaranteed)`.
728+
/// - Level::Warning and lower (i.e. !is_error()): can be accessed with the
729+
/// provided `span` and `key` through [`DiagCtxt::steal_non_err()`]. This
730+
/// allows cancelling and downgrading of the diagnostic. Returns `None`.
731+
/// - Others: not allowed, will trigger a panic.
732+
pub fn stash_diagnostic(
733+
&self,
734+
span: Span,
735+
key: StashKey,
736+
diag: Diagnostic,
737+
) -> Option<ErrorGuaranteed> {
738+
let guar = if diag.level() == Level::Error {
739+
// This `unchecked_error_guaranteed` is valid. It is where the
740+
// `ErrorGuaranteed` for stashed errors originates. See
741+
// `DiagCtxtInner::drop`.
742+
#[allow(deprecated)]
743+
Some(ErrorGuaranteed::unchecked_error_guaranteed())
744+
} else if !diag.is_error() {
745+
None
746+
} else {
747+
self.span_bug(span, format!("invalid level in `stash_diagnostic`: {}", diag.level));
748+
};
731749

732750
// FIXME(Centril, #69537): Consider reintroducing panic on overwriting a stashed diagnostic
733751
// if/when we have a more robust macro-friendly replacement for `(span, key)` as a key.
734752
// See the PR for a discussion.
735-
inner.stashed_diagnostics.insert(key, diag);
753+
let key = (span.with_parent(None), key);
754+
self.inner.borrow_mut().stashed_diagnostics.insert(key, (diag, guar));
755+
756+
guar
736757
}
737758

738-
/// Steal a previously stashed diagnostic with the given `Span` and [`StashKey`] as the key.
739-
pub fn steal_diagnostic(&self, span: Span, key: StashKey) -> Option<DiagnosticBuilder<'_, ()>> {
740-
let mut inner = self.inner.borrow_mut();
759+
/// Steal a previously stashed non-error diagnostic with the given `Span`
760+
/// and [`StashKey`] as the key. Panics if the found diagnostic is an
761+
/// error.
762+
pub fn steal_non_err(&self, span: Span, key: StashKey) -> Option<DiagnosticBuilder<'_, ()>> {
741763
let key = (span.with_parent(None), key);
742764
// FIXME(#120456) - is `swap_remove` correct?
743-
let diag = inner.stashed_diagnostics.swap_remove(&key)?;
744-
if diag.is_error() {
745-
if diag.is_lint.is_none() {
746-
inner.stashed_err_count -= 1;
747-
}
748-
}
765+
let (diag, guar) = self.inner.borrow_mut().stashed_diagnostics.swap_remove(&key)?;
766+
assert!(!diag.is_error());
767+
assert!(guar.is_none());
749768
Some(DiagnosticBuilder::new_diagnostic(self, diag))
750769
}
751770

771+
/// Steals a previously stashed error with the given `Span` and
772+
/// [`StashKey`] as the key, modifies it, and emits it. Returns `None` if
773+
/// no matching diagnostic is found. Panics if the found diagnostic's level
774+
/// isn't `Level::Error`.
775+
pub fn try_steal_modify_and_emit_err<F>(
776+
&self,
777+
span: Span,
778+
key: StashKey,
779+
mut modify_err: F,
780+
) -> Option<ErrorGuaranteed>
781+
where
782+
F: FnMut(&mut DiagnosticBuilder<'_>),
783+
{
784+
let key = (span.with_parent(None), key);
785+
// FIXME(#120456) - is `swap_remove` correct?
786+
let err = self.inner.borrow_mut().stashed_diagnostics.swap_remove(&key);
787+
err.map(|(err, guar)| {
788+
// The use of `::<ErrorGuaranteed>` is safe because level is `Level::Error`.
789+
assert_eq!(err.level, Level::Error);
790+
assert!(guar.is_some());
791+
let mut err = DiagnosticBuilder::<ErrorGuaranteed>::new_diagnostic(self, err);
792+
modify_err(&mut err);
793+
assert_eq!(err.level, Level::Error);
794+
err.emit()
795+
})
796+
}
797+
798+
/// Steals a previously stashed error with the given `Span` and
799+
/// [`StashKey`] as the key, cancels it if found, and emits `new_err`.
800+
/// Panics if the found diagnostic's level isn't `Level::Error`.
801+
pub fn try_steal_replace_and_emit_err(
802+
&self,
803+
span: Span,
804+
key: StashKey,
805+
new_err: DiagnosticBuilder<'_>,
806+
) -> ErrorGuaranteed {
807+
let key = (span.with_parent(None), key);
808+
// FIXME(#120456) - is `swap_remove` correct?
809+
let old_err = self.inner.borrow_mut().stashed_diagnostics.swap_remove(&key);
810+
match old_err {
811+
Some((old_err, guar)) => {
812+
assert_eq!(old_err.level, Level::Error);
813+
assert!(guar.is_some());
814+
// Because `old_err` has already been counted, it can only be
815+
// safely cancelled because the `new_err` supplants it.
816+
DiagnosticBuilder::<ErrorGuaranteed>::new_diagnostic(self, old_err).cancel();
817+
}
818+
None => {}
819+
};
820+
new_err.emit()
821+
}
822+
752823
pub fn has_stashed_diagnostic(&self, span: Span, key: StashKey) -> bool {
753824
self.inner.borrow().stashed_diagnostics.get(&(span.with_parent(None), key)).is_some()
754825
}
@@ -758,41 +829,40 @@ impl DiagCtxt {
758829
self.inner.borrow_mut().emit_stashed_diagnostics()
759830
}
760831

761-
/// This excludes lint errors, delayed bugs and stashed errors.
832+
/// This excludes lint errors, and delayed bugs.
762833
#[inline]
763834
pub fn err_count_excluding_lint_errs(&self) -> usize {
764-
self.inner.borrow().err_guars.len()
835+
let inner = self.inner.borrow();
836+
inner.err_guars.len()
837+
+ inner
838+
.stashed_diagnostics
839+
.values()
840+
.filter(|(diag, guar)| guar.is_some() && diag.is_lint.is_none())
841+
.count()
765842
}
766843

767-
/// This excludes delayed bugs and stashed errors.
844+
/// This excludes delayed bugs.
768845
#[inline]
769846
pub fn err_count(&self) -> usize {
770847
let inner = self.inner.borrow();
771-
inner.err_guars.len() + inner.lint_err_guars.len()
848+
inner.err_guars.len()
849+
+ inner.lint_err_guars.len()
850+
+ inner.stashed_diagnostics.values().filter(|(_diag, guar)| guar.is_some()).count()
772851
}
773852

774-
/// This excludes normal errors, lint errors, and delayed bugs. Unless
775-
/// absolutely necessary, avoid using this. It's dubious because stashed
776-
/// errors can later be cancelled, so the presence of a stashed error at
777-
/// some point of time doesn't guarantee anything -- there are no
778-
/// `ErrorGuaranteed`s here.
779-
pub fn stashed_err_count(&self) -> usize {
780-
self.inner.borrow().stashed_err_count
781-
}
782-
783-
/// This excludes lint errors, delayed bugs, and stashed errors. Unless
784-
/// absolutely necessary, prefer `has_errors` to this method.
853+
/// This excludes lint errors and delayed bugs. Unless absolutely
854+
/// necessary, prefer `has_errors` to this method.
785855
pub fn has_errors_excluding_lint_errors(&self) -> Option<ErrorGuaranteed> {
786856
self.inner.borrow().has_errors_excluding_lint_errors()
787857
}
788858

789-
/// This excludes delayed bugs and stashed errors.
859+
/// This excludes delayed bugs.
790860
pub fn has_errors(&self) -> Option<ErrorGuaranteed> {
791861
self.inner.borrow().has_errors()
792862
}
793863

794-
/// This excludes stashed errors. Unless absolutely necessary, prefer
795-
/// `has_errors` to this method.
864+
/// This excludes nothing. Unless absolutely necessary, prefer `has_errors`
865+
/// to this method.
796866
pub fn has_errors_or_delayed_bugs(&self) -> Option<ErrorGuaranteed> {
797867
self.inner.borrow().has_errors_or_delayed_bugs()
798868
}
@@ -877,10 +947,10 @@ impl DiagCtxt {
877947
}
878948
}
879949

880-
/// This excludes delayed bugs and stashed errors. Used for early aborts
881-
/// after errors occurred -- e.g. because continuing in the face of errors is
882-
/// likely to lead to bad results, such as spurious/uninteresting
883-
/// additional errors -- when returning an error `Result` is difficult.
950+
/// This excludes delayed bugs. Used for early aborts after errors occurred
951+
/// -- e.g. because continuing in the face of errors is likely to lead to
952+
/// bad results, such as spurious/uninteresting additional errors -- when
953+
/// returning an error `Result` is difficult.
884954
pub fn abort_if_errors(&self) {
885955
if self.has_errors().is_some() {
886956
FatalError.raise();
@@ -964,7 +1034,7 @@ impl DiagCtxt {
9641034
inner
9651035
.stashed_diagnostics
9661036
.values_mut()
967-
.for_each(|diag| diag.update_unstable_expectation_id(unstable_to_stable));
1037+
.for_each(|(diag, _guar)| diag.update_unstable_expectation_id(unstable_to_stable));
9681038
inner
9691039
.future_breakage_diagnostics
9701040
.iter_mut()
@@ -1286,12 +1356,8 @@ impl DiagCtxtInner {
12861356
fn emit_stashed_diagnostics(&mut self) -> Option<ErrorGuaranteed> {
12871357
let mut guar = None;
12881358
let has_errors = !self.err_guars.is_empty();
1289-
for (_, diag) in std::mem::take(&mut self.stashed_diagnostics).into_iter() {
1290-
if diag.is_error() {
1291-
if diag.is_lint.is_none() {
1292-
self.stashed_err_count -= 1;
1293-
}
1294-
} else {
1359+
for (_, (diag, _guar)) in std::mem::take(&mut self.stashed_diagnostics).into_iter() {
1360+
if !diag.is_error() {
12951361
// Unless they're forced, don't flush stashed warnings when
12961362
// there are errors, to avoid causing warning overload. The
12971363
// stash would've been stolen already if it were important.
@@ -1350,7 +1416,8 @@ impl DiagCtxtInner {
13501416
} else {
13511417
let backtrace = std::backtrace::Backtrace::capture();
13521418
// This `unchecked_error_guaranteed` is valid. It is where the
1353-
// `ErrorGuaranteed` for delayed bugs originates.
1419+
// `ErrorGuaranteed` for delayed bugs originates. See
1420+
// `DiagCtxtInner::drop`.
13541421
#[allow(deprecated)]
13551422
let guar = ErrorGuaranteed::unchecked_error_guaranteed();
13561423
self.delayed_bugs
@@ -1462,11 +1529,31 @@ impl DiagCtxtInner {
14621529
}
14631530

14641531
fn has_errors_excluding_lint_errors(&self) -> Option<ErrorGuaranteed> {
1465-
self.err_guars.get(0).copied()
1532+
self.err_guars.get(0).copied().or_else(|| {
1533+
if let Some((_diag, guar)) = self
1534+
.stashed_diagnostics
1535+
.values()
1536+
.find(|(diag, guar)| guar.is_some() && diag.is_lint.is_none())
1537+
{
1538+
*guar
1539+
} else {
1540+
None
1541+
}
1542+
})
14661543
}
14671544

14681545
fn has_errors(&self) -> Option<ErrorGuaranteed> {
1469-
self.has_errors_excluding_lint_errors().or_else(|| self.lint_err_guars.get(0).copied())
1546+
self.err_guars.get(0).copied().or_else(|| self.lint_err_guars.get(0).copied()).or_else(
1547+
|| {
1548+
if let Some((_diag, guar)) =
1549+
self.stashed_diagnostics.values().find(|(_diag, guar)| guar.is_some())
1550+
{
1551+
*guar
1552+
} else {
1553+
None
1554+
}
1555+
},
1556+
)
14701557
}
14711558

14721559
fn has_errors_or_delayed_bugs(&self) -> Option<ErrorGuaranteed> {

Diff for: compiler/rustc_hir_analysis/src/astconv/lint.rs

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use rustc_ast::TraitObjectSyntax;
2-
use rustc_errors::{codes::*, DiagnosticBuilder, EmissionGuarantee, StashKey};
2+
use rustc_errors::{codes::*, DiagnosticBuilder, EmissionGuarantee, Level, StashKey};
33
use rustc_hir as hir;
44
use rustc_hir::def::{DefKind, Res};
55
use rustc_lint_defs::{builtin::BARE_TRAIT_OBJECTS, Applicability};
@@ -241,7 +241,15 @@ impl<'o, 'tcx> dyn AstConv<'tcx> + 'o {
241241
}
242242
// check if the impl trait that we are considering is a impl of a local trait
243243
self.maybe_lint_blanket_trait_impl(self_ty, &mut diag);
244-
diag.stash(self_ty.span, StashKey::TraitMissingMethod);
244+
match diag.level() {
245+
Level::Error => {
246+
diag.stash(self_ty.span, StashKey::TraitMissingMethod);
247+
}
248+
Level::DelayedBug => {
249+
diag.emit();
250+
}
251+
_ => unreachable!(),
252+
}
245253
} else {
246254
let msg = "trait objects without an explicit `dyn` are deprecated";
247255
tcx.node_span_lint(BARE_TRAIT_OBJECTS, self_ty.hir_id, self_ty.span, msg, |lint| {

Diff for: compiler/rustc_hir_analysis/src/check/check.rs

+1-2
Original file line numberDiff line numberDiff line change
@@ -1350,8 +1350,7 @@ fn check_type_alias_type_params_are_used<'tcx>(tcx: TyCtxt<'tcx>, def_id: LocalD
13501350
let ty = tcx.type_of(def_id).instantiate_identity();
13511351
if ty.references_error() {
13521352
// If there is already another error, do not emit an error for not using a type parameter.
1353-
// Without the `stashed_err_count` part this can fail (#120856).
1354-
assert!(tcx.dcx().has_errors().is_some() || tcx.dcx().stashed_err_count() > 0);
1353+
assert!(tcx.dcx().has_errors().is_some());
13551354
return;
13561355
}
13571356

0 commit comments

Comments
 (0)