tame/tamer/src/diagnose.rs

327 lines
11 KiB
Rust
Raw Normal View History

// Diagnostic system
//
// Copyright (C) 2014-2023 Ryan Specialty, LLC.
//
// This file is part of TAME.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! Diagnostic system for error reporting.
//!
//! This system is heavily motivated by Rust's.
//! While the data structures and organization may differ,
//! the diagnostic output is visually similar.
//!
//! Visual Report
//! -------------
//! The primary output of the system is a [`Report`](report::Report).
//! A report consists not only of the diagnostic information provided by the
//! system
//! (such as error messages),
//! but also annotated source code.
//! Reports attempt to create a narrative that walks the user through why
//! and how problems arose,
//! and hopefully provides information on how to resolve the problem.
//!
//! Here is an example report:
//!
//! ```text
//! error: expected closing tag for `classify`
//! --> /home/user/program/foo.xml:16:5
//! |
//! 16 | <classify as="foo" desc="Example classification">
//! | --------- note: element `classify` is opened here
//!
//! --> /home/user/program/foo.xml:24:5
//! |
//! 24 | </wrong>
//! | ^^^^^^^^ error: expected `</classify>`
//! ```
//!
//! A single report is produced for each error
//! (or other suitable diagnostic event).
//! The system may produce multiple reports at a time if it discovers
//! multiple issues and is able to recover enough to continue to discover
//! others.
//!
//! Each report is separated into a number of sections,
//! with each section delimited by a header.
//! Each section describes one or more spans related to a range of related
//! source lines.
//! Those source lines are annotated using the [`Span`]s associated with the
//! emitted diagnostic data,
//! such as an error.
//!
//! Each section has a _gutter_ containing the line number of source lines.
//! The area below the section header and to the right of the gutter is
//! called the _body_ of the section.
//! The gutter will expand as needed to fit the line number.
//!
//! _Warning: Reports do not yet strip terminal escape sequences,
//! which may interfere with the diagnostic output.
//! This may represent a security risk depending on your threat model,
//! but does require access to the source code being compiled._
//!
//! See the [`report`] module for more information.
#[macro_use]
pub mod panic;
mod report;
mod resolve;
pub use report::{Reporter, VisualReporter};
pub use resolve::FsSpanResolver;
use core::fmt;
use std::{borrow::Cow, convert::Infallible, error::Error, fmt::Display};
use crate::span::Span;
/// No annotated description is applicable for the diagnostic message.
///
/// The system should strive to give the user as much relevant information
/// as is useful to resolve the problem.
/// Whether or not the absence of this description represents a deficiency
/// depends on the error context.
pub const NO_DESC: Vec<AnnotatedSpan> = vec![];
/// Diagnostic report.
///
/// This describes an error condition or other special event using a series
/// of [`Span`]s to describe the source, cause, and circumstances around
/// an event.
pub trait Diagnostic: Error + Sized {
/// Produce a series of [`AnnotatedSpan`]s describing the source and
/// circumstances of the diagnostic event.
fn describe(&self) -> Vec<AnnotatedSpan>;
}
impl Diagnostic for Infallible {
fn describe(&self) -> Vec<AnnotatedSpan> {
// This should never actually happen unless someone is explicitly
// invoking this method on `Infallible`.
unreachable!("Infallible is not supposed to fail")
}
}
/// Diagnostic severity level.
///
/// Levels are used both for entire reports and for styling of individual
/// [`AnnotatedSpan`]s.
///
/// Higher severity levels are represented by lower integer values
/// (e.g. level 1 is the worst),
/// like DEFCON levels.
/// The rationale here is that,
/// provided that you remember that these are 1-indexed,
/// you do not need to know how many levels exist to know how severe it
/// is.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Default)]
#[repr(u8)]
pub enum Level {
/// An error internal to TAMER that the user cannot resolve,
/// but may be able to work around.
InternalError = 1,
/// A user-resolvable error.
///
/// These represent errors resulting from the user's input.
Error,
/// Useful information that supplements other messages.
///
/// This is most often used when multiple spans are in play for a given
/// diagnostic report.
Note,
/// Additional advice to the user that may help in debugging or fixing a
/// problem.
///
/// These messages may suggest concrete fixes and are intended to
/// hopefully replace having to request advice from a human.
/// Unlike other severity levels which provide concrete factual
/// information,
/// help messages may be more speculative.
#[default]
Help,
}
impl Level {
/// Whether this error level represents an error.
pub fn is_error(self) -> bool {
self <= Self::Error
}
}
impl Display for Level {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Level::InternalError => write!(f, "internal error"),
Level::Error => write!(f, "error"),
Level::Note => write!(f, "note"),
Level::Help => write!(f, "help"),
}
}
}
/// A label associated with a report or [`Span`].
///
/// See [`AnnotatedSpan`].
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Label<'a>(Cow<'a, str>);
impl<'a> Display for Label<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(&self.0, f)
}
}
impl<'a> From<String> for Label<'a> {
fn from(s: String) -> Self {
Self(Cow::Owned(s))
}
}
impl<'a> From<&'a String> for Label<'a> {
fn from(s: &'a String) -> Self {
Self(Cow::Borrowed(s))
}
}
impl<'a> From<&'a str> for Label<'a> {
fn from(s: &'a str) -> Self {
Self(Cow::Borrowed(s))
}
}
/// A span with an associated severity level and optional label.
///
/// Annotated spans are intended to guide users through debugging a
/// diagnostic message by describing important source locations that
/// contribute to a given diagnostic event.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct AnnotatedSpan<'l>(Span, Level, Option<Label<'l>>);
impl<'l> AnnotatedSpan<'l> {
pub fn with_help<L: Into<Label<'l>>>(
self,
label: L,
) -> [AnnotatedSpan<'l>; 2] {
let span = self.0;
[self, span.help(label)]
}
tamer: xir::parse: Attribute parser generator This is the first parser generator for the parsing framework. I've been waiting quite a while to do this because I wanted to be sure that I understood how I intended to write the attribute parsers manually. Now that I'm about to start parsing source XML files, it is necessary to have a parser generator. Typically one thinks of a parser generator as a separate program that generates code for some language, but that is not always the case---that represents a lack of expressiveness in the language itself (e.g. C). Here, I simply use Rust's macro system, which should be a concept familiar to someone coming from a language like Lisp. This also resolves where I stand on parser combinators with respect to this abstraction: they both accomplish the exact same thing (composition of smaller parsers), but this abstraction doesn't do so in the typical functional way. But the end result is the same. The parser generated by this abstraction will be optimized an inlined in the same manner as the hand-written parsers. Since they'll be tightly coupled with an element parser (which too will have a parser generator), I expect that most attribute parsers will simply be inlined; they exist as separate parsers conceptually, for the same reason that you'd use parser combinators. It's worth mentioning that this awkward reliance on dead state for a lookahead token to determine when aggregation is complete rubs me the wrong way, but resolving it would involve reintroducing the XIR AttrEnd that I had previously removed. I'll keep fighting with myself on this, but I want to get a bit further before I determine if it's worth the tradeoff of reintroducing (more complex IR but simplified parsing). DEV-7145
2022-06-13 11:17:21 -04:00
/// The [`Span`] with which the annotation is associated.
pub fn span(&self) -> Span {
match self {
AnnotatedSpan(span, ..) => *span,
}
}
/// A reference to the label of the annotation,
/// if available.
pub fn label(&self) -> Option<&Label<'l>> {
match self {
AnnotatedSpan(.., label) => label.as_ref(),
}
}
}
impl<'l> From<AnnotatedSpan<'l>> for Vec<AnnotatedSpan<'l>> {
fn from(x: AnnotatedSpan<'l>) -> Self {
vec![x]
}
}
pub trait Annotate: Sized {
/// Annotate a [`Span`] with a severity [`Level`] and an optional
/// [`Label`] to display alongside of it.
///
/// You may wish to use one of the more specific methods that provide a
/// more pleasent interface.
fn annotate(self, level: Level, label: Option<Label>) -> AnnotatedSpan;
/// Annotate a span as an internal error that the user is not expected
/// to be able to resolve,
/// but may be able to work around.
///
/// Since internal errors are one of the work things that can happen to
/// a user of a programming language,
/// given that they can only work around it,
/// this method mandates a help label that provides additional
/// context and a possible workaround.
fn internal_error<'l, L: Into<Label<'l>>>(
self,
label: L,
) -> AnnotatedSpan<'l> {
self.annotate(Level::InternalError, Some(label.into()))
}
/// Annotate a span with a clarifying label styled as an error.
///
/// This label is intended to augment the error message to help guide
/// the user to a resolution.
/// If the label does not include additional _useful_ information over
/// the generic message,
/// then it may be omitted in favor of `Annotate::mark_error` to
/// simply mark the location of the error.
///
/// (This is not named `err` since it does not return an [`Err`].)
fn error<'l, L: Into<Label<'l>>>(self, label: L) -> AnnotatedSpan<'l> {
self.annotate(Level::Error, Some(label.into()))
}
/// Like [`Annotate::error`],
/// but only styles the span as a [`Level::Error`] without attaching a
/// label.
///
/// This may be appropriate when a label would provide no more useful
/// information and would simply repeat the generic error text.
/// With that said,
/// if the repeat message seems psychologically beneficial in context,
/// you may wish to use [`Annotate::error`] anyway.
fn mark_error(self) -> AnnotatedSpan<'static> {
self.annotate(Level::Error, None)
}
/// Supplemental annotated span providing additional context for another
/// span.
///
/// For example,
/// if an error is related to a conflict with how an identifier is
/// defined,
/// then a note span may indicate the location of the identifier
/// definition.
fn note<'l, L: Into<Label<'l>>>(self, label: L) -> AnnotatedSpan<'l> {
self.annotate(Level::Note, Some(label.into()))
}
/// Provide additional information that may be used to help the user in
/// debugging or fixing a diagnostic.
///
/// While the other severity levels denote factual information,
/// this provides more loose guidance.
/// It may also include concrete suggested fixes.
fn help<'l, L: Into<Label<'l>>>(self, label: L) -> AnnotatedSpan<'l> {
self.annotate(Level::Help, Some(label.into()))
}
}
impl<S: Into<Span>> Annotate for S {
fn annotate(self, level: Level, label: Option<Label>) -> AnnotatedSpan {
AnnotatedSpan(self.into(), level, label)
}
}