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