rucc-diag 0.2.5

Diagnostics, spans and source maps for the rucc C compiler.
Documentation
//! Diagnostics: spans, severities, and the structured form every message is built as.
//!
//! Design: `spec/03-architecture.md`. Layer rank 1, see `spec/18-package-layout.md`.
//!
//! A diagnostic is a value, not a string. It is rendered to a terminal, to JSON for editors,
//! or not at all when a caller is only counting errors, and building it as a value is what
//! makes those three the same code path. `spec/03-architecture.md` makes the JSON form a
//! tier 2 stability promise because editors consume it.
//!
//! # Status
//!
//! The severity, span and diagnostic types are real, and so is the source map that turns a
//! span back into a file, a line and a column. Rendering and the `-fdiagnostics-format=`
//! plumbing are the remaining piece.
//!
//! This crate is tier 3 in `spec/18-package-layout.md` section 18.5: its Rust API is
//! explicitly unstable and will change without a major version bump.

#![doc(html_root_url = "https://docs.rs/rucc-diag/0.2.5")]

mod source;

pub use crate::source::{FileId, Loc, SourceBytes, SourceFile, SourceMap, SourceMapFull};

use std::fmt;

/// A byte offset into the concatenated source map.
///
/// One flat coordinate space across every file in the translation unit, so a span is eight
/// bytes and comparing two spans does not need to know which file they came from. The map
/// from offset to file, line and column is built once and queried only when a diagnostic is
/// actually rendered, which keeps the cost off the hot path.
pub type BytePos = u32;

/// A half-open range of source, `lo .. hi`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Span {
    /// First byte of the range.
    pub lo: BytePos,
    /// One past the last byte of the range.
    pub hi: BytePos,
}

impl Span {
    /// The span covering `lo .. hi`.
    ///
    /// # Panics
    ///
    /// Panics if `hi` is before `lo`.
    #[inline]
    pub const fn new(lo: BytePos, hi: BytePos) -> Self {
        assert!(lo <= hi, "reversed span");
        Self { lo, hi }
    }

    /// The empty span at `at`, used for things the source does not contain: an implicit
    /// conversion, a compiler-generated temporary, a builtin declaration.
    #[inline]
    pub const fn empty_at(at: BytePos) -> Self {
        Self { lo: at, hi: at }
    }

    /// A span with no position at all.
    ///
    /// Distinct from an empty span, because "generated by the compiler" and "zero width at
    /// offset zero" are different things and only one of them should be rendered with a
    /// caret.
    pub const DUMMY: Self = Self { lo: BytePos::MAX, hi: BytePos::MAX };

    /// Whether this is [`Span::DUMMY`].
    #[inline]
    pub const fn is_dummy(self) -> bool {
        self.lo == BytePos::MAX
    }

    /// Width in bytes.
    #[inline]
    pub const fn len(self) -> u32 {
        self.hi - self.lo
    }

    /// Whether the span covers no bytes.
    #[inline]
    pub const fn is_empty(self) -> bool {
        self.lo == self.hi
    }

    /// The smallest span covering both, ignoring dummies.
    ///
    /// Macro expansion and error recovery both need this constantly: the span of a binary
    /// expression is the join of its operands, and the span of a recovered declaration is
    /// the join of everything the parser skipped.
    #[inline]
    pub fn to(self, other: Self) -> Self {
        if self.is_dummy() {
            return other;
        }
        if other.is_dummy() {
            return self;
        }
        Self { lo: self.lo.min(other.lo), hi: self.hi.max(other.hi) }
    }

    /// Whether `pos` falls inside the span.
    #[inline]
    pub const fn contains(self, pos: BytePos) -> bool {
        !self.is_dummy() && self.lo <= pos && pos < self.hi
    }
}

/// How bad a diagnostic is.
///
/// The ordering is by severity, so `max` over a run of diagnostics gives the worst one and
/// the exit status falls out of that.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Severity {
    /// Extra context attached to another diagnostic, never emitted alone.
    Note,
    /// A suggested fix, attached to another diagnostic.
    Help,
    /// Accepted, compiled, and worth telling the user about. Becomes an error under
    /// `-Werror`.
    Warning,
    /// Rejected. Compilation continues so that more than one error is reported, but no
    /// output is produced.
    Error,
    /// A bug in the compiler. Distinguished from `Error` because the two need completely
    /// different text: an error is the user's problem to fix, an internal compiler error is
    /// ours, and telling a user to fix an ICE wastes their afternoon.
    Ice,
}

impl Severity {
    /// Whether a diagnostic at this severity means no output is produced.
    #[inline]
    pub const fn is_fatal(self) -> bool {
        matches!(self, Severity::Error | Severity::Ice)
    }

    /// The lowercase word used when rendering.
    pub const fn as_str(self) -> &'static str {
        match self {
            Severity::Note => "note",
            Severity::Help => "help",
            Severity::Warning => "warning",
            Severity::Error => "error",
            Severity::Ice => "internal compiler error",
        }
    }
}

impl fmt::Display for Severity {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.as_str())
    }
}

/// One message, with the place it is about and any attached sub-diagnostics.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Diagnostic {
    /// How bad it is.
    pub severity: Severity,
    /// The stable identifier, for example `E0102`. Every diagnostic has one so that it can
    /// be suppressed, documented and searched for. `None` only during construction.
    pub code: Option<&'static str>,
    /// The one-line summary. Lowercase, no trailing period, no formatting: this is the line
    /// a user greps for.
    pub message: String,
    /// Where in the source.
    pub span: Span,
    /// Notes and helps hanging off this diagnostic.
    pub children: Vec<Diagnostic>,
}

impl Diagnostic {
    /// A new diagnostic at `severity`.
    pub fn new(severity: Severity, message: impl Into<String>, span: Span) -> Self {
        Self { severity, code: None, message: message.into(), span, children: Vec::new() }
    }

    /// A new error.
    pub fn error(message: impl Into<String>, span: Span) -> Self {
        Self::new(Severity::Error, message, span)
    }

    /// A new warning.
    pub fn warning(message: impl Into<String>, span: Span) -> Self {
        Self::new(Severity::Warning, message, span)
    }

    /// Attaches the stable diagnostic code.
    #[must_use]
    pub fn with_code(mut self, code: &'static str) -> Self {
        self.code = Some(code);
        self
    }

    /// Attaches a note.
    #[must_use]
    pub fn note(mut self, message: impl Into<String>, span: Span) -> Self {
        self.children.push(Self::new(Severity::Note, message, span));
        self
    }

    /// Attaches a suggestion.
    #[must_use]
    pub fn help(mut self, message: impl Into<String>, span: Span) -> Self {
        self.children.push(Self::new(Severity::Help, message, span));
        self
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn joining_spans_covers_both() {
        let a = Span::new(4, 9);
        let b = Span::new(20, 22);
        assert_eq!(a.to(b), Span::new(4, 22));
        assert_eq!(b.to(a), Span::new(4, 22));
    }

    #[test]
    fn joining_with_a_dummy_keeps_the_real_one() {
        let a = Span::new(4, 9);
        assert_eq!(a.to(Span::DUMMY), a);
        assert_eq!(Span::DUMMY.to(a), a);
    }

    #[test]
    fn a_dummy_span_contains_nothing() {
        assert!(!Span::DUMMY.contains(0));
        assert!(!Span::DUMMY.contains(BytePos::MAX));
    }

    #[test]
    fn an_empty_span_is_not_a_dummy_span() {
        let e = Span::empty_at(0);
        assert!(e.is_empty());
        assert!(!e.is_dummy());
    }

    #[test]
    fn severity_orders_by_how_bad_it_is() {
        assert!(Severity::Error > Severity::Warning);
        assert!(Severity::Ice > Severity::Error);
        assert!(Severity::Warning > Severity::Note);
    }

    #[test]
    fn only_errors_and_ices_suppress_output() {
        assert!(Severity::Error.is_fatal());
        assert!(Severity::Ice.is_fatal());
        assert!(!Severity::Warning.is_fatal());
    }

    #[test]
    fn a_diagnostic_carries_its_children() {
        let d = Diagnostic::error("expected an expression", Span::new(1, 2))
            .with_code("E0001")
            .note("in this macro expansion", Span::new(0, 8))
            .help("did you mean a compound literal", Span::DUMMY);
        assert_eq!(d.code, Some("E0001"));
        assert_eq!(d.children.len(), 2);
        assert_eq!(d.children[0].severity, Severity::Note);
        assert_eq!(d.children[1].severity, Severity::Help);
    }

    #[test]
    #[should_panic(expected = "reversed span")]
    fn a_reversed_span_is_rejected() {
        let _ = Span::new(9, 4);
    }
}