Skip to main content

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