tamer: diagnose: Introduction of diagnostic system

This is a working concept that will continue to evolve.  I wanted to start
with some basic output before getting too carried away, since there's a lot
of potential here.

This is heavily influenced by Rust's helpful diagnostic messages, but will
take some time to realize a lot of the things that Rust does.  The next step
will be to resolve line and column numbers, and then possibly include
snippets and underline spans, placing the labels alongside them.  I need to
balance this work with everything else I have going on.

This is a large commit, but it converts the existing Error Display impls
into Diagnostic.  This separation is a bit verbose, so I'll see how this
ends up evolving.

Diagnostics are tied to Error at the moment, but I imagine in the future
that any object would be able to describe itself, error or not, which would
be useful in the future both for the Summary Page and for query
functionality, to help developers understand the systems they are writing
using TAME.

Output is integrated into tameld only in this commit; I'll add tamec
next.  Examples of what this outputs are available in the test cases in this
commit.

DEV-10935
main
Mike Gerwitz 2022-04-13 14:41:54 -04:00
parent 702b5ebb23
commit eaa8133d21
18 changed files with 979 additions and 159 deletions

View File

@ -28,7 +28,10 @@ extern crate tamer;
use getopts::{Fail, Options};
use std::env;
use tamer::ld::poc::{self, TameldError};
use tamer::{
diagnose::{Reporter, VisualReporter},
ld::poc::{self, TameldError},
};
/// Types of commands
enum Command {
@ -56,14 +59,20 @@ pub fn main() -> Result<(), TameldError> {
let usage =
opts.usage(&format!("Usage: {} [OPTIONS] -o OUTPUT FILE", program));
let mut reporter = VisualReporter;
match parse_options(opts, args) {
Ok(Command::Link(input, output, emit)) => match emit {
Emit::Xmle => poc::xmle(&input, &output),
Emit::Graphml => poc::graphml(&input, &output),
}
.or_else(|e| {
eprintln!("error: {}", e);
eprintln!("fatal: failed to link `{}`", output);
// POC: Rendering to a string ensures buffering so that we don't
// interleave output between processes,
// but we ought to reuse a buffer when we support multiple
// errors.
let report = reporter.render_to_string(&e)?;
println!("{report}\nfatal: failed to link `{}`", output);
std::process::exit(1);
}),

View File

@ -0,0 +1,208 @@
// Diagnostic system
//
// Copyright (C) 2014-2021 Ryan Specialty Group, 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.
mod report;
pub use report::{Reporter, VisualReporter};
use core::fmt;
use std::{error::Error, fmt::Display};
use crate::span::Span;
/// 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>;
}
/// Diagnostic severity level.
///
/// Levels are used both for entire reports and for styling of individual
/// [`AnnotatedSpan`]s.
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum Level {
/// An error internal to TAMER that the user cannot resolve,
/// but may be able to work around.
InternalError,
/// 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.
Help,
}
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(String);
impl Display for Label {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(&self.0, f)
}
}
impl From<String> for Label {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for Label {
fn from(s: &str) -> Self {
String::from(s).into()
}
}
/// 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(Span, Level, Option<Label>);
impl AnnotatedSpan {
pub fn with_help<L: Into<Label>>(self, label: L) -> [AnnotatedSpan; 2] {
let span = self.0;
[self, span.help(label)]
}
}
impl From<AnnotatedSpan> for Vec<AnnotatedSpan> {
fn from(x: AnnotatedSpan) -> 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: Into<Label>>(self, label: L) -> AnnotatedSpan {
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: Into<Label>>(self, label: L) -> AnnotatedSpan {
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 {
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: Into<Label>>(self, label: L) -> AnnotatedSpan {
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: Into<Label>>(self, label: L) -> AnnotatedSpan {
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)
}
}

View File

@ -0,0 +1,358 @@
// Diagnostic system rendering
//
// Copyright (C) 2014-2021 Ryan Specialty Group, 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/>.
//! Rendering of diagnostic information.
use super::{AnnotatedSpan, Diagnostic};
use crate::span::UNKNOWN_SPAN;
use std::fmt::{self, Write};
pub trait Reporter {
/// Render diagnostic report.
///
/// Please be mindful of where this report is being rendered `to`.
/// For example,
/// if rendering to standard out,
/// it is a good idea to buffer the entire report before flushing to
/// stdout,
/// otherwise the report may become interleaved with other
/// concurrent processes
/// (e.g. if TAMER is being invoked using `make -jN`).
fn render(
&mut self,
diagnostic: &impl Diagnostic,
to: &mut impl Write,
) -> Result<(), fmt::Error>;
/// Render a diagnostic report into an owned [`String`].
///
/// This invokes [`Reporter::render`] on a newly allocated [`String`].
fn render_to_string(
&mut self,
diagnostic: &impl Diagnostic,
) -> Result<String, fmt::Error> {
let mut str = String::new();
self.render(diagnostic, &mut str)?;
Ok(str)
}
}
/// Render diagnostic report in a highly visual way.
///
/// This report is modeled after Rust's default error reporting,
/// most notable for including sections of source code associated with
/// spans,
/// underlining spans,
/// and including helpful information that walks the user through
/// understanding why the error occurred and how to approach resolving
/// it.
pub struct VisualReporter;
impl VisualReporter {
pub fn new() -> Self {
Self
}
}
impl Reporter for VisualReporter {
// _TODO: This is a proof-of-concept._
fn render(
&mut self,
diagnostic: &impl Diagnostic,
to: &mut impl Write,
) -> Result<(), fmt::Error> {
// TODO: not only errors; get the max level from the annotated spans
writeln!(to, "error: {}", diagnostic)?;
let mut prev_span = UNKNOWN_SPAN;
for AnnotatedSpan(span, level, olabel) in diagnostic.describe() {
if span != prev_span {
writeln!(
to,
" --> {} offset {}--{}",
span.ctx(),
span.offset(),
span.offset() + span.len() as u32
)?;
}
if let Some(label) = olabel {
writeln!(to, " {level}: {label}")?;
}
prev_span = span;
}
Ok(())
}
}
#[cfg(test)]
mod test {
//! Integration tests for the diagnostic system.
//!
//! These tests have the potential to be rather rigid,
//! but the proper rendering of the diagnostic system is very
//! important,
//! given that the slightest misrendering could cause significant
//! confusion.
//!
//! While it may be tempting to use format strings to reduce the
//! maintenance burden,
//! this should be avoided most cases,
//! since writing out the entire expected value will allow the
//! developer to visualize what output will be produced and
//! whether it is appropriate for the user.
//!
//! In essence:
//! this diagnostic report is effectively another compiler target,
//! and it must be byte-for-byte identical to what is expected.
//! With that said,
//! _do not interpret these tests as providing a stable,
//! unchanging diagnostic report output_.
//! This can and will change over time,
//! and separate reporters will be provided for machine-readable
//! formats if that is what is needed.
use crate::{diagnose::Annotate, span::Context};
use super::*;
use std::{error::Error, fmt::Display};
#[derive(Debug)]
struct StubError(String, Vec<AnnotatedSpan>);
impl Display for StubError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl Error for StubError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None
}
}
impl Diagnostic for StubError {
fn describe(&self) -> Vec<AnnotatedSpan> {
self.1.clone()
}
}
macro_rules! assert_report {
($msg:expr, $aspans:expr, $expected:expr) => {
let mut sut = VisualReporter::new();
assert_eq!(
sut.render_to_string(&StubError($msg.into(), $aspans)),
Ok($expected.into()),
);
};
}
#[test]
fn no_spans() {
assert_report!(
"test with no spans",
vec![],
// No spans will result in the `Display` of the error only.
"error: test with no spans\n"
);
}
#[test]
fn span_error_no_label() {
let span = Context::from("foo/bar").span(50, 5);
assert_report!(
"single span no label",
vec![span.mark_error()],
// Context and span are rendered without a label.
"\
error: single span no label
--> foo/bar offset 50--55
"
);
}
#[test]
fn span_error_with_label() {
let span = Context::from("bar/baz").span(60, 2);
assert_report!(
"single span with label",
vec![span.error("span label here")],
// Context and span are rendered without a label.
"\
error: single span with label
--> bar/baz offset 60--62
error: span label here
"
);
}
#[test]
fn adjacent_eq_span_no_labels_collapsed() {
let ctx = Context::from("foo/bar");
let span = ctx.span(50, 5);
assert_report!(
"multiple adjacent same span no label",
vec![span.mark_error(), span.mark_error()],
// Collapsed into one `-->` line since the spans are the same.
// This is unlikely to happen,
// given that there is not much use in having multiple
// duplicate spans without some additional context.
"\
error: multiple adjacent same span no label
--> foo/bar offset 50--55
"
);
}
#[test]
fn adjacent_eq_span_labels_collapsed() {
let ctx = Context::from("baz/quux");
let span = ctx.span(50, 5);
assert_report!(
"multiple adjacent same span with labels",
vec![
span.error("A label"),
span.mark_error(), // no label
span.error("C label"),
],
// Labels are collapsed under the same `-->` line since the
// spans are the same.
"\
error: multiple adjacent same span with labels
--> baz/quux offset 50--55
error: A label
error: C label
"
);
}
#[test]
fn adjacent_eq_context_neq_offset_len_spans_not_collapsed() {
let ctx = Context::from("quux/quuux");
assert_report!(
"multiple adjacent different context",
vec![
// -->
ctx.span(10, 5).mark_error(),
ctx.span(10, 5).error("A, first label"), // collapse
// -->
ctx.span(10, 6).error("B, different length"),
ctx.span(10, 6).mark_error(), // collapse
ctx.span(10, 6).error("B, collapse"),
// -->
ctx.span(15, 6).error("C, different offset"),
// -->
// Back to (10, 6), but not adjacent to previous
ctx.span(10, 6).error("B', not adjacent"),
],
"\
error: multiple adjacent different context
--> quux/quuux offset 10--15
error: A, first label
--> quux/quuux offset 10--16
error: B, different length
error: B, collapse
--> quux/quuux offset 15--21
error: C, different offset
--> quux/quuux offset 10--16
error: B', not adjacent
"
);
}
#[test]
fn adjacent_neq_context_spans_not_collapsed() {
// Note that the offsets and lengths are purposefully the same to
// ensure that the differentiator is exclusively the context.
let span_a = Context::from("foo/bar").span(10, 3);
let span_b = Context::from("bar/baz").span(10, 3);
assert_report!(
"multiple adjacent different context",
vec![
// -->
span_a.mark_error(),
span_a.error("A, first"),
span_a.error("A, collapsed"),
span_a.mark_error(), // collapsed, same
// -->
span_b.error("B, first"),
span_b.error("B, collapsed"),
span_b.mark_error(), // collapsed, same
// -->
// Back to 'a' again, but we can't collapse now since we're
// adjacent to 'b'
span_a.error("A, not collapsed"),
// Back to 'b' again, but we can't collapse now since we're
// adjacent to 'a'
// (same as prev but without label this time)
span_b.mark_error(),
// And just so we have two adjacent label-less spans
span_a.mark_error(),
],
"\
error: multiple adjacent different context
--> foo/bar offset 10--13
error: A, first
error: A, collapsed
--> bar/baz offset 10--13
error: B, first
error: B, collapsed
--> foo/bar offset 10--13
error: A, not collapsed
--> bar/baz offset 10--13
--> foo/bar offset 10--13
"
);
}
#[test]
fn severity_levels_reflected() {
let ctx = Context::from("foo/bar");
let span = ctx.span(50, 5);
assert_report!(
"multiple spans with labels of different severity level",
vec![
span.internal_error("an internal error"),
span.error("an error"),
span.note("a note"),
span.help("a help message"),
],
"\
error: multiple spans with labels of different severity level
--> foo/bar offset 50--55
internal error: an internal error
error: an error
note: a note
help: a help message
"
);
}
}

View File

@ -27,6 +27,7 @@ use super::xmle::{
};
use crate::{
asg::{Asg, DefaultAsg, IdentObject},
diagnose::{AnnotatedSpan, Diagnostic},
fs::{
Filesystem, FsCanonicalizer, PathFile, VisitOnceFile,
VisitOnceFilesystem,
@ -49,7 +50,8 @@ use crate::{
use fxhash::FxBuildHasher;
use petgraph_graphml::GraphMl;
use std::{
fmt::Display,
error::Error,
fmt::{self, Display},
fs,
io::{self, BufReader, BufWriter, Write},
path::{Path, PathBuf},
@ -257,8 +259,8 @@ pub enum TameldError {
XmloParseError(ParseError<XirfToken, XmloError>),
AsgBuilderError(AsgBuilderError),
XirWriterError(XirWriterError),
CycleError(Vec<Vec<SymbolId>>),
Fmt(fmt::Error),
}
impl From<io::Error> for TameldError {
@ -303,6 +305,12 @@ impl From<XirWriterError> for TameldError {
}
}
impl From<fmt::Error> for TameldError {
fn from(e: fmt::Error) -> Self {
Self::Fmt(e)
}
}
impl Display for TameldError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
@ -313,8 +321,7 @@ impl Display for TameldError {
Self::XmloParseError(e) => Display::fmt(e, f),
Self::AsgBuilderError(e) => Display::fmt(e, f),
Self::XirWriterError(e) => Display::fmt(e, f),
TameldError::CycleError(cycles) => {
Self::CycleError(cycles) => {
for cycle in cycles {
writeln!(
f,
@ -329,6 +336,36 @@ impl Display for TameldError {
Ok(())
}
Self::Fmt(e) => Display::fmt(e, f),
}
}
}
impl Error for TameldError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Io(e) => Some(e),
Self::SortError(e) => Some(e),
Self::XirError(e) => Some(e),
Self::XirfParseError(e) => Some(e),
Self::XmloParseError(e) => Some(e),
Self::AsgBuilderError(e) => Some(e),
Self::XirWriterError(e) => Some(e),
Self::CycleError(..) => None,
Self::Fmt(e) => Some(e),
}
}
}
impl Diagnostic for TameldError {
fn describe(&self) -> Vec<AnnotatedSpan> {
match self {
Self::XirError(e) => e.describe(),
Self::XirfParseError(e) => e.describe(),
Self::XmloParseError(e) => e.describe(),
// TODO (will fall back to rendering just the error `Display`)
_ => vec![],
}
}
}

View File

@ -81,6 +81,7 @@ pub mod xir;
pub mod asg;
pub mod convert;
pub mod diagnose;
pub mod fs;
pub mod iter;
pub mod ld;

View File

@ -403,11 +403,11 @@ mod test {
fn xmlo_error_returned() {
let mut sut = Sut::new();
let evs = vec![Err(XmloError::UnexpectedRoot)];
let evs = vec![Err(XmloError::UnassociatedSym(DUMMY_SPAN))];
let result = sut.import_xmlo(evs.into_iter(), SutState::new());
assert_eq!(
AsgBuilderError::XmloError(XmloError::UnexpectedRoot),
AsgBuilderError::XmloError(XmloError::UnassociatedSym(DUMMY_SPAN)),
result.expect_err("expected error to be proxied"),
);
}

View File

@ -19,8 +19,11 @@
//! Errors while processing `xmlo` object files.
use crate::diagnose::{Annotate, AnnotatedSpan, Diagnostic};
use crate::parse::Token;
use crate::span::Span;
use crate::sym::SymbolId;
use crate::xir::flat::Object as XirfToken;
use std::fmt::Display;
/// Error during `xmlo` processing.
@ -35,7 +38,7 @@ use std::fmt::Display;
#[derive(Debug, PartialEq, Eq)]
pub enum XmloError {
/// The root node was not an `lv:package`.
UnexpectedRoot,
UnexpectedRoot(XirfToken),
/// A `preproc:sym` node was found, but is missing `@name`.
UnassociatedSym(Span),
/// The provided `preproc:sym/@type` is unknown or invalid.
@ -57,65 +60,54 @@ pub enum XmloError {
UnassociatedFragment(Span),
/// A `preproc:fragment` element was found, but is missing `text()`.
MissingFragmentText(SymbolId, Span),
/// Token stream ended unexpectedly.
UnexpectedEof,
}
impl Display for XmloError {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
use XmloError::*;
match self {
Self::UnexpectedRoot => {
write!(fmt, "unexpected package root (is this a package?)")
UnexpectedRoot(_tok) => {
write!(fmt, "expected `package` root element")
}
Self::UnassociatedSym(span) => write!(
fmt,
"unassociated symbol table entry: \
preproc:sym/@name missing at {span}"
),
Self::InvalidType(ty, span) => {
write!(fmt, "invalid preproc:sym/@type `{ty}` at {span}")
UnassociatedSym(_) => {
write!(fmt, "unassociated symbol table entry")
}
Self::InvalidDtype(dtype, span) => {
write!(fmt, "invalid preproc:sym/@dtype `{dtype}` at {span}")
InvalidType(ty, _) => {
write!(fmt, "invalid symbol type `{ty}`")
}
Self::InvalidDim(dim, span) => {
write!(fmt, "invalid preproc:sym/@dim `{dim}` at {span}")
InvalidDtype(dtype, _) => {
write!(fmt, "invalid symbol dtype `{dtype}`")
}
Self::MapFromNameMissing(sym, span) => {
write!(
fmt,
"preproc:sym[@type=\"map\"]/preproc:from/@name missing \
for symbol `{sym}` at {span}"
)
InvalidDim(dim, _) => {
write!(fmt, "invalid dimensionality `{dim}`")
}
Self::MapFromMultiple(sym, span) => {
write!(
fmt,
"preproc:sym[@type=\"map\"]/preproc:from must appear \
only once for symbol `{sym}` at {span}"
)
MapFromNameMissing(sym, _) => {
write!(fmt, "map `from` name missing for symbol `{sym}`")
}
Self::UnassociatedSymDep(span) => write!(
fmt,
"unassociated dependency list: preproc:sym-dep/@name \
missing at {span}"
),
Self::MalformedSymRef(name, span) => {
write!(
fmt,
"malformed dependency ref for symbol \
{name} at {span}"
)
MapFromMultiple(sym, _) => {
write!(fmt, "multiple map `from` for `{sym}`")
}
UnassociatedSymDep(_) => {
write!(fmt, "unassociated dependency list")
}
MalformedSymRef(name, _) => {
write!(fmt, "malformed dependency ref for symbol {name}")
}
UnassociatedFragment(_) => write!(fmt, "unassociated fragment"),
MissingFragmentText(sym, _) => {
write!(fmt, "missing fragment text for symbol `{sym}`",)
}
Self::UnassociatedFragment(span) => write!(
fmt,
"unassociated fragment: preproc:fragment/@id missing at {span}"
),
Self::MissingFragmentText(sym, span) => write!(
fmt,
"fragment found, but missing text for symbol `{sym}` at {span}",
),
Self::UnexpectedEof => write!(fmt, "unexpected EOF"),
}
}
}
@ -125,3 +117,86 @@ impl std::error::Error for XmloError {
None
}
}
impl Diagnostic for XmloError {
fn describe(&self) -> Vec<AnnotatedSpan> {
use XmloError::*;
let malformed = "this `xmlo` file is malformed or corrupt; \
try removing it to force it to be rebuilt,
and please report this error";
// Note that these errors _could_ potentially be more descriptive
// and contain more spans,
// but they are internal compiler errors and so it's not yet
// deemed to be worth the effort.
match self {
UnexpectedRoot(tok) => {
// TODO: If we recommend `<package`,
// we ought to have a span that guarantees that `<` will
// be included.
tok.span()
.error("`<package` expected here")
.with_help(
"an `xmlo` file was expected, \
but this appears to be something else",
)
.into()
}
UnassociatedSym(span) => span
.internal_error("`@name` is missing")
.with_help(malformed)
.into(),
InvalidType(_ty, span) => span
.internal_error("the type `{ty}` is unknown")
.with_help(malformed)
.into(),
InvalidDtype(_dtype, span) => span
.internal_error("the dtype `{dtype}` is unknown")
.with_help(malformed)
.into(),
InvalidDim(_dim, span) => span
.internal_error(
"the number of dimensions must be `0`, `1`, or `2`",
)
.with_help(malformed)
.into(),
MapFromNameMissing(_sym, span) => span
.internal_error("`@name` is missing")
.with_help(malformed)
.into(),
MapFromMultiple(_sym, span) => span
.internal_error(
"this is an unexpected extra `preproc:from` for `{sym}`",
)
.with_help(malformed)
.into(),
UnassociatedSymDep(span) => span
.internal_error("`@name` is missing")
.with_help(malformed)
.into(),
MalformedSymRef(_name, span) => span
.internal_error("`@name` is missing")
.with_help(malformed)
.into(),
UnassociatedFragment(span) => span
.internal_error("@id is missing")
.with_help(malformed)
.into(),
MissingFragmentText(_sym, span) => span
.internal_error("missing fragment text")
.with_help(malformed)
.into(),
}
}
}

View File

@ -170,7 +170,9 @@ impl<SS: XmloState, SD: XmloState, SF: XmloState> ParseState
Transition(Package).incomplete()
}
(Ready, _) => Transition(Ready).err(XmloError::UnexpectedRoot),
(Ready, tok) => {
Transition(Ready).err(XmloError::UnexpectedRoot(tok))
}
(Package, Xirf::Attr(Attr(name, value, _))) => {
Transition(Package).ok(match name {

View File

@ -43,18 +43,14 @@ type Sut = XmloReader;
#[test]
fn fails_on_invalid_root() {
let mut sut = Sut::parse(
[Xirf::Open(
"not-a-valid-package-node".unwrap_into(),
S1,
Depth(0),
)]
.into_iter(),
);
let tok =
Xirf::Open("not-a-valid-package-node".unwrap_into(), S1, Depth(0));
let mut sut = Sut::parse([tok.clone()].into_iter());
assert_matches!(
sut.next(),
Some(Err(ParseError::StateError(XmloError::UnexpectedRoot)))
Some(Err(ParseError::StateError(XmloError::UnexpectedRoot(etok)))) if etok == tok
);
}

View File

@ -21,6 +21,7 @@
//!
//! _TODO: Some proper docs and examples!_
use crate::diagnose::{Annotate, AnnotatedSpan, Diagnostic};
use crate::iter::{TripIter, TrippableIterator};
use crate::span::{Span, UNKNOWN_SPAN};
use std::fmt::Debug;
@ -120,7 +121,7 @@ pub trait ParseState: Default + PartialEq + Eq + Debug {
type Object: Object;
/// Errors specific to this set of states.
type Error: Debug + Error + PartialEq + Eq;
type Error: Debug + Diagnostic + PartialEq + Eq;
type Context: Default + Debug = EmptyContext;
@ -777,7 +778,7 @@ impl<S: ParseState, I: TokenStream<S::Token>> Iterator for Parser<S, I> {
/// Parsers may return their own unique errors via the
/// [`StateError`][ParseError::StateError] variant.
#[derive(Debug, PartialEq, Eq)]
pub enum ParseError<T: Token, E: Error + PartialEq + Eq> {
pub enum ParseError<T: Token, E: Diagnostic + PartialEq + Eq> {
/// Token stream ended unexpectedly.
///
/// This error means that the parser was expecting more input before
@ -812,8 +813,10 @@ pub enum ParseError<T: Token, E: Error + PartialEq + Eq> {
StateError(E),
}
impl<T: Token, EA: Error + PartialEq + Eq> ParseError<T, EA> {
pub fn inner_into<EB: Error + PartialEq + Eq>(self) -> ParseError<T, EB>
impl<T: Token, EA: Diagnostic + PartialEq + Eq> ParseError<T, EA> {
pub fn inner_into<EB: Diagnostic + PartialEq + Eq>(
self,
) -> ParseError<T, EB>
where
EA: Into<EB>,
{
@ -826,27 +829,29 @@ impl<T: Token, EA: Error + PartialEq + Eq> ParseError<T, EA> {
}
}
impl<T: Token, E: Error + PartialEq + Eq> From<E> for ParseError<T, E> {
impl<T: Token, E: Diagnostic + PartialEq + Eq> From<E> for ParseError<T, E> {
fn from(e: E) -> Self {
Self::StateError(e)
}
}
impl<T: Token, E: Error + PartialEq + Eq> Display for ParseError<T, E> {
impl<T: Token, E: Diagnostic + PartialEq + Eq> Display for ParseError<T, E> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnexpectedEof(span) => {
write!(f, "unexpected end of input at {span}")
Self::UnexpectedEof(_) => {
write!(f, "unexpected end of input")
}
Self::UnexpectedToken(tok) => {
write!(f, "unexpected {}", tok)
Self::UnexpectedToken(_tok) => {
write!(f, "unexpected input")
}
Self::StateError(e) => Display::fmt(e, f),
}
}
}
impl<T: Token, E: Error + PartialEq + Eq + 'static> Error for ParseError<T, E> {
impl<T: Token, E: Diagnostic + PartialEq + Eq + 'static> Error
for ParseError<T, E>
{
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::StateError(e) => Some(e),
@ -855,6 +860,29 @@ impl<T: Token, E: Error + PartialEq + Eq + 'static> Error for ParseError<T, E> {
}
}
impl<T: Token, E: Diagnostic + PartialEq + Eq + 'static> Diagnostic
for ParseError<T, E>
{
fn describe(&self) -> Vec<AnnotatedSpan> {
use ParseError::*;
match self {
// TODO: More information from the underlying parser on what was expected.
UnexpectedEof(span) => {
span.error("unexpected end of input here").into()
}
UnexpectedToken(tok) => {
tok.span().error("this was unexpected").into()
}
// TODO: Is there any additional useful context we can augment
// this with?
StateError(e) => e.describe(),
}
}
}
impl<S: ParseState, I: TokenStream<S::Token>> From<I> for Parser<S, I> {
fn from(toks: I) -> Self {
Self {
@ -1023,6 +1051,12 @@ pub mod test {
}
}
impl Diagnostic for EchoStateError {
fn describe(&self) -> Vec<AnnotatedSpan> {
unimplemented!()
}
}
type Sut<I> = Parser<EchoState, I>;
#[test]

View File

@ -517,9 +517,9 @@ pub const UNKNOWN_CONTEXT: Context = Context(st16::raw::CTX_UNKNOWN);
/// See also [`UNKNOWN_CONTEXT`].
pub const DUMMY_CONTEXT: Context = Context(st16::raw::CTX_DUMMY);
impl From<PathSymbolId> for Context {
fn from(sym: PathSymbolId) -> Self {
Self(sym)
impl<P: Into<PathSymbolId>> From<P> for Context {
fn from(sym: P) -> Self {
Self(sym.into())
}
}

View File

@ -263,7 +263,7 @@ impl Deref for Whitespace {
}
impl TryFrom<&str> for Whitespace {
type Error = Error;
type Error = SpanlessError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
// We do not expect this to ever be a large value based on how we
@ -271,7 +271,7 @@ impl TryFrom<&str> for Whitespace {
// If it is, well, someone's doing something they ought not to be
// and we're not going to optimize for it.
if !value.as_bytes().iter().all(u8::is_ascii_whitespace) {
return Err(Error::NotWhitespace(value.into()));
return Err(SpanlessError::NotWhitespace(value.into()));
}
Ok(Self(value.intern()))
@ -656,7 +656,7 @@ mod test {
assert_eq!(
Whitespace::try_from("not ws!"),
Err(Error::NotWhitespace("not ws!".into()))
Err(SpanlessError::NotWhitespace("not ws!".into(),))
);
Ok(())

View File

@ -20,7 +20,8 @@
//! Parse XIR attribute [`TokenStream`][super::super::TokenStream]s.
use crate::{
parse::{NoContext, ParseState, Transition, TransitionResult},
diagnose::{Annotate, AnnotatedSpan, Diagnostic},
parse::{NoContext, ParseState, Token, Transition, TransitionResult},
span::Span,
xir::{QName, Token as XirToken},
};
@ -99,16 +100,12 @@ pub enum AttrParseError {
impl Display for AttrParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::AttrNameExpected(tok) => {
write!(f, "attribute name expected, found {}", tok)
Self::AttrNameExpected(_) => {
write!(f, "attribute name expected")
}
Self::AttrValueExpected(name, span, tok) => {
write!(
f,
"expected value for `@{}` at {}, found {}",
name, span, tok
)
Self::AttrValueExpected(name, _span, _tok) => {
write!(f, "expected value for `@{name}`",)
}
}
}
@ -120,6 +117,18 @@ impl Error for AttrParseError {
}
}
impl Diagnostic for AttrParseError {
fn describe(&self) -> Vec<AnnotatedSpan> {
match self {
Self::AttrNameExpected(tok) => tok.span().mark_error().into(),
Self::AttrValueExpected(_name, span, _tok) => {
span.mark_error().into()
}
}
}
}
#[cfg(test)]
mod test {
use super::*;

View File

@ -20,7 +20,11 @@
//! XIR error information.
use super::QName;
use crate::{span::Span, sym::SymbolId};
use crate::{
diagnose::{Annotate, AnnotatedSpan, Diagnostic},
span::Span,
sym::SymbolId,
};
use std::{fmt::Display, str::Utf8Error};
/// Error attempting to produce a XIR object.
@ -29,7 +33,7 @@ pub enum Error {
/// Provided name contains a `':'`.
NCColon(SymbolId, Span),
/// Provided string contains non-ASCII-whitespace characters.
NotWhitespace(String),
NotWhitespace(SymbolId, Span),
/// Provided QName is not valid.
InvalidQName(SymbolId, Span),
/// A UTF-8 error together with the byte slice that caused it.
@ -79,59 +83,52 @@ impl Display for Error {
use Error::*;
match self {
NCColon(sym, span) => {
write!(f, "NCName `{sym}` cannot contain ':' at {span}",)
NCColon(sym, _) => {
write!(f, "NCName `{sym}` cannot contain `:`",)
}
NotWhitespace(s) => {
write!(f, "string contains non-ASCII-whitespace: `{}`", s)
NotWhitespace(_s, _) => {
write!(f, "whitespace expected")
}
InvalidQName(qname, span) => {
write!(f, "invalid QName `{qname}` at {span}")
InvalidQName(qname, _) => {
write!(f, "invalid QName `{qname}`")
}
InvalidUtf8(inner, bytes, span) => {
write!(
f,
"{inner} for string `{}` with bytes `{bytes:?}` at {span}",
String::from_utf8_lossy(bytes)
)
InvalidUtf8(inner, _bytes, _) => Display::fmt(inner, f),
UnsupportedXmlVersion(ver, _) => {
write!(f, "unsupported XML version `{ver}`")
}
UnsupportedXmlVersion(ver, span) => {
write!(
f,
"expected XML version `1.0` at {span}, \
but found unsupported version `{ver}`"
)
}
UnsupportedEncoding(enc, span) => {
UnsupportedEncoding(enc, _) => {
// TODO: when we have hints,
// indicate that they can also entirely remove this
// attribute to resolve the error
write!(
f,
"expected `utf-8` or `UTF-8` encoding at {span}, \
but found unsupported encoding `{enc}`"
)
write!(f, "unsupported encoding `{enc}`")
}
AttrValueExpected(Some(name), span) => {
write!(f, "value expected for attribute `{name}` at {span}")
AttrValueExpected(Some(name), _) => {
write!(f, "value expected for attribute `@{name}`")
}
// TODO: Parsers should provide the name.
AttrValueExpected(None, span) => {
write!(f, "value expected for attribute at {span}")
AttrValueExpected(None, _) => {
write!(f, "value expected for attribute")
}
AttrValueUnquoted(Some(name), span) => {
write!(
f,
"value for attribute `{name}` is missing quotes at {span}"
)
AttrValueUnquoted(Some(name), _) => {
write!(f, "attribute `@{name}` missing quotes")
}
// TODO: Parsers should provide the name.
AttrValueUnquoted(None, span) => {
write!(f, "value for attribute is missing quotes at {span}")
AttrValueUnquoted(None, _) => {
write!(f, "value for attribute is missing quotes")
}
// TODO: Translate error messages
QuickXmlError(inner, span) => {
write!(f, "internal parser error: {inner} at {span}")
QuickXmlError(inner, _) => {
write!(f, "internal parser error: {inner}")
}
}
}
@ -147,6 +144,54 @@ impl std::error::Error for Error {
}
}
impl Diagnostic for Error {
fn describe(&self) -> Vec<AnnotatedSpan> {
use Error::*;
match self {
// NB: This is often constructed from a QName and so we may not
// have as much context as we would like;
// don't be too specific.
NCColon(_, span) => span.error("unexpected `:` here").into(),
NotWhitespace(_, span) => {
span.error("whitespace expected here").into()
}
InvalidQName(_, span) => span.mark_error().into(),
InvalidUtf8(_, bytes, span) => {
span.error(format!("has byte sequence `{bytes:?}`",)).into()
}
UnsupportedXmlVersion(_, span) => {
// TODO: suggested fix: replacement of span with `1.0`
span.error("expected version `1.0`").into()
}
UnsupportedEncoding(_, span) => {
// TODO: suggested fix: remove attribute and whitespace
span.error("expected `utf-8` or `UTF-8`").into()
}
AttrValueExpected(_, span) => {
span.error("attribute value expected").into()
}
AttrValueUnquoted(_, span) => {
// TODO: suggested fix: wrap in quotes
span.error("quotes expected around this value").into()
}
QuickXmlError(_, span) => {
// TODO: note saying that this should probably be reported
// to provide a better error
span.mark_error().into()
}
}
}
}
/// An [`Error`] that requires its [`Span`] to be filled in by the caller.
///
/// These errors should not be converted automatically,
@ -162,6 +207,7 @@ impl std::error::Error for Error {
#[derive(Debug, PartialEq)]
pub enum SpanlessError {
NCColon(SymbolId),
NotWhitespace(SymbolId),
InvalidQName(SymbolId),
InvalidUtf8(Utf8Error, Vec<u8>),
QuickXmlError(QuickXmlError),
@ -171,6 +217,7 @@ impl SpanlessError {
pub fn with_span(self, span: Span) -> Error {
match self {
Self::NCColon(sym) => Error::NCColon(sym, span),
Self::NotWhitespace(sym) => Error::NotWhitespace(sym, span),
Self::InvalidQName(qname) => Error::InvalidQName(qname, span),
Self::InvalidUtf8(inner, bytes) => {
Error::InvalidUtf8(inner, bytes, span)

View File

@ -43,6 +43,7 @@ use super::{
QName, Token as XirToken, TokenStream, Whitespace,
};
use crate::{
diagnose::{Annotate, AnnotatedSpan, Diagnostic},
parse::{
self, Context, ParseState, ParsedResult, Token, Transition,
TransitionResult,
@ -377,35 +378,25 @@ impl Display for StateError {
use StateError::*;
match self {
RootOpenExpected(tok) => {
write!(
f,
"opening root element tag expected, \
but found {tok}"
)
RootOpenExpected(_tok) => {
write!(f, "missing opening root element",)
}
MaxDepthExceeded {
open: (name, span),
open: (_name, _),
max,
} => {
write!(
f,
"maximum element nesting depth of {max} exceeded \
by `{name}` at {span}"
"maximum XML element nesting depth of `{max}` exceeded"
)
}
UnbalancedTag {
open: (open_name, open_span),
close: (close_name, close_span),
open: (open_name, _),
close: (_close_name, _),
} => {
write!(
f,
"expected closing tag `{open_name}`, \
but found `{close_name}` at {close_span} \
(opening tag at {open_span})",
)
write!(f, "expected closing tag for `{open_name}`")
}
AttrError(e) => Display::fmt(e, f),
@ -415,7 +406,55 @@ impl Display for StateError {
impl Error for StateError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
todo!()
match self {
Self::AttrError(e) => Some(e),
_ => None,
}
}
}
impl Diagnostic for StateError {
fn describe(&self) -> Vec<AnnotatedSpan> {
use StateError::*;
match self {
RootOpenExpected(tok) => {
// TODO: Should the span be the first byte,
// or should we delegate that question to an e.g. `SpanLike`?
tok.span()
.error("an opening root node was expected here")
.into()
}
MaxDepthExceeded {
open: (_, span),
max,
} => span
.error(format!(
"this opening tag increases the level of nesting \
past the limit of {max}"
))
.into(),
UnbalancedTag {
open: (open_name, open_span),
close: (close_name, close_span),
} => {
// TODO: hint saying that the nesting could be wrong, etc;
// we can't just suggest a replacement,
// since that's not necessarily the problem
vec![
open_span
.note(format!("element `{open_name}` is opened here")),
close_span.error(format!(
"expected `</{open_name}>`, \
but found closing tag for `{close_name}`"
)),
]
}
AttrError(e) => e.describe(),
}
}
}

View File

@ -360,7 +360,7 @@ impl<'s, B: BufRead, S: Escaper> XmlXirReader<'s, B, S> {
// Given this input, quick-xml ignores the bytes entirely:
// <foo bar>
// ^^^| missing `="value"`
// |--| missing `="value"`
//
// The whitespace check is to handle input like this:
// <foo />

View File

@ -179,6 +179,7 @@ use super::{
};
use crate::{
diagnose::{AnnotatedSpan, Diagnostic},
parse::{
self, EmptyContext, NoContext, ParseError, ParseResult, ParseState,
ParseStatus, ParsedResult, Transition, TransitionResult,
@ -698,6 +699,13 @@ impl Error for StackError {
}
}
impl Diagnostic for StackError {
fn describe(&self) -> Vec<AnnotatedSpan> {
// TODO: At the time of writing, XIRT isn't used outside of tests.
vec![]
}
}
/// Produce a streaming parser for the given [`TokenStream`].
///
/// If you do not require a single-step [`Iterator::next`] and simply want

View File

@ -316,17 +316,14 @@ impl<I: TokenStream, S: Escaper> XmlWriter<S> for I {
#[cfg(test)]
mod test {
use std::{
borrow::Cow,
convert::{TryFrom, TryInto},
};
use std::borrow::Cow;
use super::*;
use crate::{
convert::ExpectInto,
span::Span,
sym::GlobalSymbolIntern,
xir::{error::SpanlessError, QName, Whitespace},
xir::{error::SpanlessError, QName},
};
type TestResult = std::result::Result<(), Error>;
@ -433,7 +430,7 @@ mod test {
// Intended for alignment of attributes, primarily.
#[test]
fn whitespace_within_open_node() -> TestResult {
let result = Token::Whitespace(Whitespace::try_from(" \t ")?, *S)
let result = Token::Whitespace(" \t ".unwrap_into(), *S)
.write_new(WriterState::NodeOpen, &MockEscaper::default())?;
assert_eq!(result.0, b" \t ");
@ -563,7 +560,7 @@ mod test {
Token::AttrValue("value".intern(), *S),
Token::Text("text".intern(), *S),
Token::Open(("c", "child").unwrap_into(), *S),
Token::Whitespace(" ".try_into()?, *S),
Token::Whitespace(" ".unwrap_into(), *S),
Token::Close(None, *S),
Token::Close(Some(root), *S),
]