rucc_diag/errors.rs
1//! Collecting diagnostics, and the limit on how many one run will produce.
2//!
3//! Design: `spec/06-lexer-and-parser.md` section 6.8.
4//!
5//! Every pass that reports needs the same three things: somewhere to put the diagnostics, a
6//! count of the ones that are errors, and a point at which it stops. It is here rather than in
7//! the parser that first needed it because semantic analysis needs exactly the same sink and the
8//! limit is one number for the whole compiler, not one per pass that happens to reach it.
9//!
10//! # Why counting errors is not what stops a cascade
11//!
12//! It is not. Every recovery leaves a poisoned node behind and a diagnostic about a poisoned
13//! node is not reported, which is what [`Errors::push_unless`] is for, and the limit here is the
14//! backstop for the file that is broken in more ways than one. A flag saying that something
15//! already went wrong is close enough to work on small inputs and wrong on real ones, because it
16//! either suppresses errors in code that was fine or fails to suppress the third message about
17//! the same broken subexpression.
18
19use crate::{Diagnostic, Severity};
20
21/// How many errors are reported before a pass gives up.
22///
23/// The number is clang's, measured rather than assumed: clang 23.1 stops after twenty with
24/// `too many errors emitted, stopping now`, and gcc 13.3 has no default limit at all and will
25/// print every error a file produces. Twenty is the better default of the two, because the
26/// errors after the twentieth in a file that is this broken are almost always consequences of
27/// the ones before them, and the accepted flag for changing it is gcc's `-fmax-errors=N`, with
28/// zero meaning no limit.
29pub const DEFAULT_ERROR_LIMIT: usize = 20;
30
31/// The diagnostics a pass produced, and the limit on how many it will produce.
32#[derive(Debug)]
33pub struct Errors {
34 diagnostics: Vec<Diagnostic>,
35 errors: usize,
36 limit: usize,
37 stopped: bool,
38}
39
40impl Default for Errors {
41 fn default() -> Self {
42 Errors::new(DEFAULT_ERROR_LIMIT)
43 }
44}
45
46impl Errors {
47 /// A sink that stops after `limit` errors, or that never stops when `limit` is zero.
48 #[must_use]
49 pub fn new(limit: usize) -> Self {
50 Errors { diagnostics: Vec::new(), errors: 0, limit, stopped: false }
51 }
52
53 /// Records a diagnostic.
54 ///
55 /// Once the limit is reached nothing more is recorded, warnings included. The pass is about
56 /// to stop and a warning arriving after the note that says so reads as though the compiler
57 /// carried on regardless.
58 pub fn push(&mut self, diagnostic: Diagnostic) {
59 if self.stopped {
60 return;
61 }
62 let fatal = diagnostic.severity.is_fatal();
63 let span = diagnostic.span;
64 self.diagnostics.push(diagnostic);
65 if fatal {
66 self.errors += 1;
67 if self.limit != 0 && self.errors >= self.limit {
68 self.diagnostics.push(Diagnostic::new(
69 Severity::Note,
70 "too many errors emitted, stopping now",
71 span,
72 ));
73 self.stopped = true;
74 }
75 }
76 }
77
78 /// Records a diagnostic unless `suppressed` says the node it is about is already poisoned.
79 ///
80 /// Every pass that recovers leaves a poisoned node behind, and a message about such a node
81 /// is not reported, which is what actually stops one error becoming twenty. What counts as
82 /// poisoned is a fact about a tree rather than about a diagnostic, so the caller answers the
83 /// question and this only honours the answer.
84 ///
85 /// The suppression is deliberately shallow: the question is whether the node the message is
86 /// about is itself poisoned, not whether anything underneath it is. A poisoned operand makes
87 /// its parent poisoned at the point the parent is built, so the answer propagates through
88 /// the tree rather than through a walk of it, and a walk would make reporting an error cost
89 /// the size of the subtree.
90 pub fn push_unless(&mut self, suppressed: bool, diagnostic: Diagnostic) {
91 if !suppressed {
92 self.push(diagnostic);
93 }
94 }
95
96 /// Whether the pass should stop, because it has reported as many errors as it will.
97 #[inline]
98 #[must_use]
99 pub fn stopped(&self) -> bool {
100 self.stopped
101 }
102
103 /// How many errors have been reported. Warnings and notes are not counted.
104 #[inline]
105 #[must_use]
106 pub fn errors(&self) -> usize {
107 self.errors
108 }
109
110 /// Whether anything was reported at all.
111 #[inline]
112 #[must_use]
113 pub fn is_empty(&self) -> bool {
114 self.diagnostics.is_empty()
115 }
116
117 /// How many diagnostics were reported, of every severity.
118 #[inline]
119 #[must_use]
120 pub fn len(&self) -> usize {
121 self.diagnostics.len()
122 }
123
124 /// What has been reported so far, in the order it was reported.
125 ///
126 /// For a caller that wants to look at the diagnostics and carry on, which is what a test
127 /// does and what a pass that reports at the end of each function will do.
128 #[inline]
129 #[must_use]
130 pub fn diagnostics(&self) -> &[Diagnostic] {
131 &self.diagnostics
132 }
133
134 /// What was reported, in the order it was reported.
135 #[must_use]
136 pub fn finish(self) -> Vec<Diagnostic> {
137 self.diagnostics
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use crate::Span;
144
145 use super::*;
146
147 #[test]
148 fn the_limit_stops_the_run_and_says_so() {
149 let mut errors = Errors::new(3);
150 for _ in 0..10 {
151 errors.push(Diagnostic::error("no", Span::empty_at(0)));
152 }
153 assert!(errors.stopped());
154 assert_eq!(errors.errors(), 3);
155 let diagnostics = errors.finish();
156 assert_eq!(diagnostics.len(), 4);
157 assert_eq!(diagnostics[3].severity, Severity::Note);
158 assert_eq!(diagnostics[3].message, "too many errors emitted, stopping now");
159 }
160
161 #[test]
162 fn a_limit_of_zero_never_stops() {
163 let mut errors = Errors::new(0);
164 for _ in 0..64 {
165 errors.push(Diagnostic::error("no", Span::empty_at(0)));
166 }
167 assert!(!errors.stopped());
168 assert_eq!(errors.len(), 64);
169 }
170
171 #[test]
172 fn warnings_do_not_count_against_the_limit() {
173 let mut errors = Errors::default();
174 assert!(errors.is_empty());
175 for _ in 0..64 {
176 errors.push(Diagnostic::warning("hmm", Span::empty_at(0)));
177 }
178 assert_eq!(errors.errors(), 0);
179 assert!(!errors.stopped());
180 }
181
182 #[test]
183 fn a_message_about_a_poisoned_node_is_held_back() {
184 let mut errors = Errors::default();
185 let at = Span::empty_at(0);
186 errors.push_unless(true, Diagnostic::error("about the broken one", at));
187 assert!(errors.is_empty());
188 errors.push_unless(false, Diagnostic::error("about the good one", at));
189 assert_eq!(errors.len(), 1);
190 }
191}