midenc_session/
diagnostics.rs

1use alloc::{
2    boxed::Box,
3    collections::BTreeMap,
4    fmt::{self, Display},
5    format,
6    string::{String, ToString},
7    sync::Arc,
8    vec::Vec,
9};
10use core::sync::atomic::{AtomicUsize, Ordering};
11
12pub use miden_assembly::diagnostics::{
13    miette,
14    miette::MietteDiagnostic as AdHocDiagnostic,
15    reporting,
16    reporting::{PrintDiagnostic, ReportHandlerOpts},
17    Diagnostic, IntoDiagnostic, Label, LabeledSpan, RelatedError, RelatedLabel, Report, Severity,
18    WrapErr,
19};
20pub use miden_core::debuginfo::*;
21pub use midenc_hir_macros::Spanned;
22
23#[cfg(feature = "std")]
24pub use crate::emitter::CaptureEmitter;
25pub use crate::emitter::{Buffer, DefaultEmitter, Emitter, NullEmitter};
26use crate::{ColorChoice, Verbosity, Warnings};
27
28#[derive(Default, Debug, Copy, Clone)]
29pub struct DiagnosticsConfig {
30    pub verbosity: Verbosity,
31    pub warnings: Warnings,
32}
33
34pub struct DiagnosticsHandler {
35    emitter: Arc<dyn Emitter>,
36    source_manager: Arc<dyn SourceManager>,
37    err_count: AtomicUsize,
38    verbosity: Verbosity,
39    warnings: Warnings,
40    silent: bool,
41}
42
43impl Default for DiagnosticsHandler {
44    fn default() -> Self {
45        let emitter = Arc::new(DefaultEmitter::new(ColorChoice::Auto));
46        let source_manager = Arc::new(DefaultSourceManager::default());
47        Self::new(Default::default(), source_manager, emitter)
48    }
49}
50
51// We can safely implement these traits for DiagnosticsHandler,
52// as the only two non-atomic fields are read-only after creation
53unsafe impl Send for DiagnosticsHandler {}
54unsafe impl Sync for DiagnosticsHandler {}
55
56impl DiagnosticsHandler {
57    /// Create a new [DiagnosticsHandler] from the given [DiagnosticsConfig],
58    /// [CodeMap], and [Emitter] implementation.
59    pub fn new(
60        config: DiagnosticsConfig,
61        source_manager: Arc<dyn SourceManager>,
62        emitter: Arc<dyn Emitter>,
63    ) -> Self {
64        let warnings = match config.warnings {
65            Warnings::Error => Warnings::Error,
66            _ if config.verbosity > Verbosity::Warning => Warnings::None,
67            warnings => warnings,
68        };
69        Self {
70            emitter,
71            source_manager,
72            err_count: AtomicUsize::new(0),
73            verbosity: config.verbosity,
74            warnings,
75            silent: config.verbosity == Verbosity::Silent,
76        }
77    }
78
79    #[inline]
80    pub fn source_manager(&self) -> Arc<dyn SourceManager> {
81        self.source_manager.clone()
82    }
83
84    #[inline]
85    pub fn source_manager_ref(&self) -> &dyn SourceManager {
86        self.source_manager.as_ref()
87    }
88
89    /// Returns true if the [DiagnosticsHandler] has emitted any error diagnostics
90    pub fn has_errors(&self) -> bool {
91        self.err_count.load(Ordering::Relaxed) > 0
92    }
93
94    /// Triggers a panic if the [DiagnosticsHandler] has emitted any error diagnostics
95    #[track_caller]
96    pub fn abort_if_errors(&self) {
97        if self.has_errors() {
98            panic!("Compiler has encountered unexpected errors. See diagnostics for details.")
99        }
100    }
101
102    /// Emit a diagnostic [Report]
103    pub fn report(&self, report: impl Into<Report>) {
104        self.emit(report.into())
105    }
106
107    /// Report an error diagnostic
108    pub fn error(&self, error: impl ToString) {
109        self.emit(Report::msg(error.to_string()));
110    }
111
112    /// Report a warning diagnostic
113    ///
114    /// If `warnings_as_errors` is set, it produces an error diagnostic instead.
115    pub fn warn(&self, warning: impl ToString) {
116        if matches!(self.warnings, Warnings::Error) {
117            return self.error(warning);
118        }
119        let diagnostic = AdHocDiagnostic::new(warning.to_string()).with_severity(Severity::Warning);
120        self.emit(diagnostic);
121    }
122
123    /// Emits an informational diagnostic
124    pub fn info(&self, message: impl ToString) {
125        if self.verbosity > Verbosity::Info {
126            return;
127        }
128        let diagnostic = AdHocDiagnostic::new(message.to_string()).with_severity(Severity::Advice);
129        self.emit(diagnostic);
130    }
131
132    /// Starts building an [InFlightDiagnostic] for rich compiler diagnostics.
133    ///
134    /// The caller is responsible for dropping/emitting the diagnostic using the
135    /// [InFlightDiagnostic] API.
136    pub fn diagnostic(&self, severity: Severity) -> InFlightDiagnosticBuilder<'_> {
137        InFlightDiagnosticBuilder::new(self, severity)
138    }
139
140    /// Emits the given diagnostic
141    #[inline(never)]
142    pub fn emit(&self, diagnostic: impl Into<Report>) {
143        let diagnostic: Report = diagnostic.into();
144        let diagnostic = match diagnostic.severity() {
145            Some(Severity::Advice) if self.verbosity > Verbosity::Info => return,
146            Some(Severity::Warning) => match self.warnings {
147                Warnings::None => return,
148                Warnings::All => diagnostic,
149                Warnings::Error => {
150                    self.err_count.fetch_add(1, Ordering::Relaxed);
151                    Report::from(WarningAsError::from(diagnostic))
152                }
153            },
154            Some(Severity::Error) => {
155                self.err_count.fetch_add(1, Ordering::Relaxed);
156                diagnostic
157            }
158            _ => diagnostic,
159        };
160
161        if self.silent {
162            return;
163        }
164
165        self.write_report(diagnostic);
166    }
167
168    #[cfg(feature = "std")]
169    fn write_report(&self, diagnostic: Report) {
170        use std::io::Write;
171
172        let mut buffer = self.emitter.buffer();
173        let printer = PrintDiagnostic::new(diagnostic);
174        write!(&mut buffer, "{printer}").expect("failed to write diagnostic to buffer");
175        self.emitter.print(buffer).unwrap();
176    }
177
178    #[cfg(not(feature = "std"))]
179    fn write_report(&self, diagnostic: Report) {
180        let out = PrintDiagnostic::new(diagnostic).to_string();
181        self.emitter.print(out).unwrap();
182    }
183}
184
185#[derive(thiserror::Error, Diagnostic, Debug)]
186#[error("{}", .report)]
187#[diagnostic(
188    severity(Error),
189    help("this warning was promoted to an error via --warnings-as-errors")
190)]
191struct WarningAsError {
192    #[diagnostic_source]
193    report: Report,
194}
195impl From<Report> for WarningAsError {
196    fn from(report: Report) -> Self {
197        Self { report }
198    }
199}
200
201/// Constructs an in-flight diagnostic using the builder pattern
202pub struct InFlightDiagnosticBuilder<'h> {
203    handler: &'h DiagnosticsHandler,
204    diagnostic: InFlightDiagnostic,
205    /// The source id of the primary diagnostic being constructed, if known
206    primary_source_id: Option<SourceId>,
207    /// The set of secondary labels which reference code in other source files than the primary
208    references: BTreeMap<SourceId, RelatedLabel>,
209}
210impl<'h> InFlightDiagnosticBuilder<'h> {
211    pub(crate) fn new(handler: &'h DiagnosticsHandler, severity: Severity) -> Self {
212        Self {
213            handler,
214            diagnostic: InFlightDiagnostic::new(severity),
215            primary_source_id: None,
216            references: BTreeMap::default(),
217        }
218    }
219
220    /// Sets the primary diagnostic message to `message`
221    pub fn with_message(mut self, message: impl ToString) -> Self {
222        self.diagnostic.message = message.to_string();
223        self
224    }
225
226    /// Sets the error code for this diagnostic
227    pub fn with_code(mut self, code: impl ToString) -> Self {
228        self.diagnostic.code = Some(code.to_string());
229        self
230    }
231
232    /// Sets the error url for this diagnostic
233    pub fn with_url(mut self, url: impl ToString) -> Self {
234        self.diagnostic.url = Some(url.to_string());
235        self
236    }
237
238    /// Adds a primary label for `span` to this diagnostic, with no label message.
239    pub fn with_primary_span(mut self, span: SourceSpan) -> Self {
240        use miden_assembly::diagnostics::LabeledSpan;
241
242        assert!(self.diagnostic.labels.is_empty(), "cannot set the primary span more than once");
243        let source_id = span.source_id();
244        let source_file = self.handler.source_manager.get(source_id).ok();
245        self.primary_source_id = Some(source_id);
246        self.diagnostic.source_code = source_file;
247        self.diagnostic.labels.push(LabeledSpan::new_primary_with_span(None, span));
248        self
249    }
250
251    /// Adds a primary label for `span` to this diagnostic, with the given message
252    ///
253    /// A primary label is one which should be rendered as the relevant source code
254    /// at which a diagnostic originates. Secondary labels are used for related items
255    /// involved in the diagnostic.
256    pub fn with_primary_label(mut self, span: SourceSpan, message: impl ToString) -> Self {
257        use miden_assembly::diagnostics::LabeledSpan;
258
259        assert!(self.diagnostic.labels.is_empty(), "cannot set the primary span more than once");
260        let source_id = span.source_id();
261        let source_file = self.handler.source_manager.get(source_id).ok();
262        self.primary_source_id = Some(source_id);
263        self.diagnostic.source_code = source_file;
264        self.diagnostic
265            .labels
266            .push(LabeledSpan::new_primary_with_span(Some(message.to_string()), span));
267        self
268    }
269
270    /// Adds a secondary label for `span` to this diagnostic, with the given message
271    ///
272    /// A secondary label is used to point out related items in the source code which
273    /// are relevant to the diagnostic, but which are not themselves the point at which
274    /// the diagnostic originates.
275    pub fn with_secondary_label(mut self, span: SourceSpan, message: impl ToString) -> Self {
276        use miden_assembly::diagnostics::LabeledSpan;
277
278        assert!(
279            !self.diagnostic.labels.is_empty(),
280            "must set a primary label before any secondary labels"
281        );
282        let source_id = span.source_id();
283        if source_id != self.primary_source_id.unwrap_or_default() {
284            let related = self.references.entry(source_id).or_insert_with(|| {
285                let source_file = self.handler.source_manager.get(source_id).ok();
286                RelatedLabel::advice("see diagnostics for more information")
287                    .with_source_file(source_file)
288            });
289            related.labels.push(Label::new(span, message.to_string()));
290        } else {
291            self.diagnostic
292                .labels
293                .push(LabeledSpan::new_with_span(Some(message.to_string()), span));
294        }
295        self
296    }
297
298    /// Adds a note to the diagnostic
299    ///
300    /// Notes are used for explaining general concepts or suggestions
301    /// related to a diagnostic, and are not associated with any particular
302    /// source location. They are always rendered after the other diagnostic
303    /// content.
304    pub fn with_help(mut self, note: impl ToString) -> Self {
305        self.diagnostic.help = Some(note.to_string());
306        self
307    }
308
309    /// Consume this [InFlightDiagnostic] and create a [Report]
310    pub fn into_report(mut self) -> Report {
311        if self.diagnostic.message.is_empty() {
312            self.diagnostic.message = "reported".into();
313        }
314        self.diagnostic.related.extend(self.references.into_values());
315        Report::from(self.diagnostic)
316    }
317
318    /// Emit the underlying [Diagnostic] via the [DiagnosticHandler]
319    pub fn emit(self) {
320        let handler = self.handler;
321        handler.emit(self.into_report());
322    }
323}
324
325#[derive(Default)]
326struct InFlightDiagnostic {
327    source_code: Option<Arc<SourceFile>>,
328    severity: Option<Severity>,
329    message: String,
330    code: Option<String>,
331    help: Option<String>,
332    url: Option<String>,
333    labels: Vec<LabeledSpan>,
334    related: Vec<RelatedLabel>,
335}
336
337impl InFlightDiagnostic {
338    fn new(severity: Severity) -> Self {
339        Self {
340            severity: Some(severity),
341            ..Default::default()
342        }
343    }
344}
345
346impl fmt::Display for InFlightDiagnostic {
347    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
348        write!(f, "{}", &self.message)
349    }
350}
351
352impl fmt::Debug for InFlightDiagnostic {
353    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
354        write!(f, "{}", &self.message)
355    }
356}
357
358impl core::error::Error for InFlightDiagnostic {}
359
360impl Diagnostic for InFlightDiagnostic {
361    fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
362        self.code.as_ref().map(Box::new).map(|c| c as Box<dyn Display>)
363    }
364
365    fn severity(&self) -> Option<Severity> {
366        self.severity
367    }
368
369    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
370        self.help.as_ref().map(Box::new).map(|c| c as Box<dyn Display>)
371    }
372
373    fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
374        self.url.as_ref().map(Box::new).map(|c| c as Box<dyn Display>)
375    }
376
377    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
378        if self.labels.is_empty() {
379            return None;
380        }
381        let iter = self.labels.iter().cloned();
382        Some(Box::new(iter) as Box<dyn Iterator<Item = LabeledSpan>>)
383    }
384
385    fn related(&self) -> Option<Box<dyn Iterator<Item = &dyn Diagnostic> + '_>> {
386        if self.related.is_empty() {
387            return None;
388        }
389
390        let iter = self.related.iter().map(|r| r as &dyn Diagnostic);
391        Some(Box::new(iter) as Box<dyn Iterator<Item = &dyn Diagnostic>>)
392    }
393
394    fn diagnostic_source(&self) -> Option<&(dyn Diagnostic + '_)> {
395        None
396    }
397}