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