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 + Send + Sync>,
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 =
46            Arc::new(DefaultSourceManager::default()) as Arc<dyn SourceManager + Send + Sync>;
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 + Send + Sync>,
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 + Send + Sync> {
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        use core::fmt::Write;
181
182        let mut buffer = self.emitter.buffer();
183        let printer = PrintDiagnostic::new(diagnostic);
184        write!(&mut buffer, "{printer}").expect("failed to write diagnostic to buffer");
185        self.emitter.print(buffer).unwrap();
186    }
187}
188
189#[derive(thiserror::Error, Diagnostic, Debug)]
190#[error("{}", .report)]
191#[diagnostic(
192    severity(Error),
193    help("this warning was promoted to an error via --warnings-as-errors")
194)]
195struct WarningAsError {
196    #[diagnostic_source]
197    report: Report,
198}
199impl From<Report> for WarningAsError {
200    fn from(report: Report) -> Self {
201        Self { report }
202    }
203}
204
205/// Constructs an in-flight diagnostic using the builder pattern
206pub struct InFlightDiagnosticBuilder<'h> {
207    handler: &'h DiagnosticsHandler,
208    diagnostic: InFlightDiagnostic,
209    /// The source id of the primary diagnostic being constructed, if known
210    primary_source_id: Option<SourceId>,
211    /// The set of secondary labels which reference code in other source files than the primary
212    references: BTreeMap<SourceId, RelatedLabel>,
213}
214impl<'h> InFlightDiagnosticBuilder<'h> {
215    pub(crate) fn new(handler: &'h DiagnosticsHandler, severity: Severity) -> Self {
216        Self {
217            handler,
218            diagnostic: InFlightDiagnostic::new(severity),
219            primary_source_id: None,
220            references: BTreeMap::default(),
221        }
222    }
223
224    /// Sets the primary diagnostic message to `message`
225    pub fn with_message(mut self, message: impl ToString) -> Self {
226        self.diagnostic.message = message.to_string();
227        self
228    }
229
230    /// Sets the error code for this diagnostic
231    pub fn with_code(mut self, code: impl ToString) -> Self {
232        self.diagnostic.code = Some(code.to_string());
233        self
234    }
235
236    /// Sets the error url for this diagnostic
237    pub fn with_url(mut self, url: impl ToString) -> Self {
238        self.diagnostic.url = Some(url.to_string());
239        self
240    }
241
242    /// Adds a primary label for `span` to this diagnostic, with no label message.
243    pub fn with_primary_span(mut self, span: SourceSpan) -> Self {
244        use miden_assembly::diagnostics::LabeledSpan;
245
246        assert!(self.diagnostic.labels.is_empty(), "cannot set the primary span more than once");
247        let source_id = span.source_id();
248        let source_file = self.handler.source_manager.get(source_id).ok();
249        self.primary_source_id = Some(source_id);
250        self.diagnostic.source_code = source_file;
251        self.diagnostic.labels.push(LabeledSpan::new_primary_with_span(None, span));
252        self
253    }
254
255    /// Adds a primary label for `span` to this diagnostic, with the given message
256    ///
257    /// A primary label is one which should be rendered as the relevant source code
258    /// at which a diagnostic originates. Secondary labels are used for related items
259    /// involved in the diagnostic.
260    pub fn with_primary_label(mut self, span: SourceSpan, message: impl ToString) -> Self {
261        use miden_assembly::diagnostics::LabeledSpan;
262
263        assert!(self.diagnostic.labels.is_empty(), "cannot set the primary span more than once");
264        let source_id = span.source_id();
265        let source_file = self.handler.source_manager.get(source_id).ok();
266        self.primary_source_id = Some(source_id);
267        self.diagnostic.source_code = source_file;
268        self.diagnostic
269            .labels
270            .push(LabeledSpan::new_primary_with_span(Some(message.to_string()), span));
271        self
272    }
273
274    /// Adds a secondary label for `span` to this diagnostic, with the given message
275    ///
276    /// A secondary label is used to point out related items in the source code which
277    /// are relevant to the diagnostic, but which are not themselves the point at which
278    /// the diagnostic originates.
279    pub fn with_secondary_label(mut self, span: SourceSpan, message: impl ToString) -> Self {
280        use miden_assembly::diagnostics::LabeledSpan;
281
282        assert!(
283            !self.diagnostic.labels.is_empty(),
284            "must set a primary label before any secondary labels"
285        );
286        let source_id = span.source_id();
287        if source_id != self.primary_source_id.unwrap_or_default() {
288            let related = self.references.entry(source_id).or_insert_with(|| {
289                let source_file = self.handler.source_manager.get(source_id).ok();
290                RelatedLabel::advice("see diagnostics for more information")
291                    .with_source_file(source_file)
292            });
293            related.labels.push(Label::new(span, message.to_string()));
294        } else {
295            self.diagnostic
296                .labels
297                .push(LabeledSpan::new_with_span(Some(message.to_string()), span));
298        }
299        self
300    }
301
302    /// Adds a note to the diagnostic
303    ///
304    /// Notes are used for explaining general concepts or suggestions
305    /// related to a diagnostic, and are not associated with any particular
306    /// source location. They are always rendered after the other diagnostic
307    /// content.
308    pub fn with_help(mut self, note: impl ToString) -> Self {
309        self.diagnostic.help = Some(note.to_string());
310        self
311    }
312
313    /// Consume this [InFlightDiagnostic] and create a [Report]
314    pub fn into_report(mut self) -> Report {
315        if self.diagnostic.message.is_empty() {
316            self.diagnostic.message = "reported".into();
317        }
318        self.diagnostic.related.extend(self.references.into_values());
319        Report::from(self.diagnostic)
320    }
321
322    /// Emit the underlying [Diagnostic] via the [DiagnosticHandler]
323    pub fn emit(self) {
324        let handler = self.handler;
325        handler.emit(self.into_report());
326    }
327}
328
329#[derive(Default)]
330struct InFlightDiagnostic {
331    source_code: Option<Arc<SourceFile>>,
332    severity: Option<Severity>,
333    message: String,
334    code: Option<String>,
335    help: Option<String>,
336    url: Option<String>,
337    labels: Vec<LabeledSpan>,
338    related: Vec<RelatedLabel>,
339}
340
341impl InFlightDiagnostic {
342    fn new(severity: Severity) -> Self {
343        Self {
344            severity: Some(severity),
345            ..Default::default()
346        }
347    }
348}
349
350impl fmt::Display for InFlightDiagnostic {
351    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
352        write!(f, "{}", &self.message)
353    }
354}
355
356impl fmt::Debug for InFlightDiagnostic {
357    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
358        write!(f, "{}", &self.message)
359    }
360}
361
362impl core::error::Error for InFlightDiagnostic {}
363
364impl Diagnostic for InFlightDiagnostic {
365    fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
366        self.code.as_ref().map(Box::new).map(|c| c as Box<dyn Display>)
367    }
368
369    fn severity(&self) -> Option<Severity> {
370        self.severity
371    }
372
373    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
374        self.help.as_ref().map(Box::new).map(|c| c as Box<dyn Display>)
375    }
376
377    fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
378        self.url.as_ref().map(Box::new).map(|c| c as Box<dyn Display>)
379    }
380
381    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
382        if self.labels.is_empty() {
383            return None;
384        }
385        let iter = self.labels.iter().cloned();
386        Some(Box::new(iter) as Box<dyn Iterator<Item = LabeledSpan>>)
387    }
388
389    fn related(&self) -> Option<Box<dyn Iterator<Item = &dyn Diagnostic> + '_>> {
390        if self.related.is_empty() {
391            return None;
392        }
393
394        let iter = self.related.iter().map(|r| r as &dyn Diagnostic);
395        Some(Box::new(iter) as Box<dyn Iterator<Item = &dyn Diagnostic>>)
396    }
397
398    fn diagnostic_source(&self) -> Option<&(dyn Diagnostic + '_)> {
399        None
400    }
401}
402
403pub use self::into_diagnostic::{DiagnosticError, IntoDiagnostic};
404
405mod into_diagnostic {
406    use alloc::boxed::Box;
407
408    /// Convenience [`Diagnostic`] that can be used as an "anonymous" wrapper for
409    /// Errors. This is intended to be paired with [`IntoDiagnostic`].
410    #[derive(Debug)]
411    pub struct DiagnosticError<E>(Box<E>);
412    impl<E> DiagnosticError<E> {
413        pub fn new(error: E) -> Self {
414            Self(Box::new(error))
415        }
416    }
417    impl<E: core::fmt::Debug + core::fmt::Display + 'static> miden_assembly::diagnostics::Diagnostic
418        for DiagnosticError<E>
419    {
420    }
421    impl<E: core::fmt::Display> core::fmt::Display for DiagnosticError<E> {
422        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
423            core::fmt::Display::fmt(self.0.as_ref(), f)
424        }
425    }
426    impl<E: core::fmt::Debug + core::fmt::Display + 'static> core::error::Error for DiagnosticError<E> {
427        default fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
428            None
429        }
430
431        default fn cause(&self) -> Option<&dyn core::error::Error> {
432            self.source()
433        }
434    }
435    impl<E: core::error::Error + 'static> core::error::Error for DiagnosticError<E> {
436        fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
437            self.0.source()
438        }
439    }
440    unsafe impl<E: Send> Send for DiagnosticError<E> {}
441    unsafe impl<E: Sync> Sync for DiagnosticError<E> {}
442
443    /**
444    Convenience trait that adds a [`.into_diagnostic()`](IntoDiagnostic::into_diagnostic) method that converts a type implementing
445    [`std::error::Error`] to a [`Result<T, Report>`].
446
447    ## Warning
448
449    Calling this on a type implementing [`Diagnostic`] will reduce it to the common denominator of
450    [`std::error::Error`]. Meaning all extra information provided by [`Diagnostic`] will be
451    inaccessible. If you have a type implementing [`Diagnostic`] consider simply returning it or using
452    [`Into`] or the [`Try`](std::ops::Try) operator (`?`).
453    */
454    pub trait IntoDiagnostic<T, E> {
455        /// Converts [`Result`] types that return regular [`std::error::Error`]s
456        /// into a [`Result`] that returns a [`Diagnostic`].
457        fn into_diagnostic(self) -> Result<T, super::Report>;
458    }
459
460    impl<T, E: core::fmt::Debug + core::fmt::Display + Sync + Send + 'static> IntoDiagnostic<T, E>
461        for Result<T, E>
462    {
463        fn into_diagnostic(self) -> Result<T, super::Report> {
464            self.map_err(|e| DiagnosticError::new(e).into())
465        }
466    }
467}