Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 0 additions & 19 deletions crates/wit-parser/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1845,25 +1845,6 @@ impl SourceMap {
Ok((resolver.resolve()?, nested))
}

/// Runs `f` and, on error, attempts to add source highlighting to resolver
/// error types that still use `anyhow`. Only needed until the resolver is
/// migrated to structured errors.
pub(crate) fn rewrite_error<F, T>(&self, f: F) -> anyhow::Result<T>
where
F: FnOnce() -> anyhow::Result<T>,
{
let mut err = match f() {
Ok(t) => return Ok(t),
Err(e) => e,
};
if let Some(e) = err.downcast_mut::<crate::Error>() {
e.highlight(self);
} else if let Some(e) = err.downcast_mut::<crate::PackageNotFoundError>() {
e.highlight(self);
}
Err(err)
}

pub(crate) fn highlight_span(&self, span: Span, err: impl fmt::Display) -> Option<String> {
if !span.is_known() {
return None;
Expand Down
8 changes: 8 additions & 0 deletions crates/wit-parser/src/ast/error.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
//! Error types for WIT parsing.

use alloc::boxed::Box;
use alloc::string::{String, ToString};
use core::fmt;

use crate::{SourceMap, Span, ast::lex};

/// Convenience alias for a `Result` whose error type is [`ParseError`].
pub type ParseResult<T, E = ParseError> = Result<T, E>;

/// The category of error that occurred while parsing a WIT package.
#[non_exhaustive]
#[derive(Debug, PartialEq, Eq)]
pub enum ParseErrorKind {
Expand All @@ -31,6 +35,7 @@ pub enum ParseErrorKind {
}

impl ParseErrorKind {
/// Returns the source span associated with this error.
pub fn span(&self) -> Span {
match self {
ParseErrorKind::Lex(e) => Span::new(e.position(), e.position() + 1),
Expand Down Expand Up @@ -62,6 +67,7 @@ impl fmt::Display for ParseErrorKind {
}
}

/// A single structured error from parsing a WIT package.
#[derive(Debug, PartialEq, Eq)]
pub struct ParseError(Box<ParseErrorKind>);

Expand All @@ -74,10 +80,12 @@ impl ParseError {
.into()
}

/// Returns the underlying error kind
pub fn kind(&self) -> &ParseErrorKind {
&self.0
}

/// Returns the underlying error kind (mutable).
pub fn kind_mut(&mut self) -> &mut ParseErrorKind {
&mut self.0
}
Expand Down
1 change: 1 addition & 0 deletions crates/wit-parser/src/decoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::*;
use alloc::string::{String, ToString};
use alloc::vec;
use alloc::vec::Vec;
use anyhow::Result;
use anyhow::{Context, anyhow, bail};
use core::mem;
use std::io::Read;
Expand Down
99 changes: 7 additions & 92 deletions crates/wit-parser/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
#[cfg(feature = "std")]
use anyhow::Context;
use anyhow::{Result, bail};
use anyhow::Context as _;
use id_arena::{Arena, Id};
use semver::Version;

Expand Down Expand Up @@ -52,6 +51,7 @@ pub use ast::{ParsedUsePath, parse_use_path};
mod sizealign;
pub use sizealign::*;
mod resolve;
pub use resolve::error::*;
pub use resolve::*;
mod live;
pub use live::{LiveTypes, TypeIdVisitor};
Expand All @@ -64,7 +64,7 @@ mod serde_;
use serde_::*;

/// Checks if the given string is a legal identifier in wit.
pub fn validate_id(s: &str) -> Result<()> {
pub fn validate_id(s: &str) -> anyhow::Result<()> {
ast::validate_id(0, s)?;
Ok(())
}
Expand Down Expand Up @@ -294,99 +294,14 @@ impl fmt::Display for PackageName {
}
}

#[derive(Debug)]
struct Error {
span: Span,
msg: String,
highlighted: Option<String>,
}

impl Error {
fn new(span: Span, msg: impl Into<String>) -> Error {
Error {
span,
msg: msg.into(),
highlighted: None,
}
}

/// Highlights this error using the given source map, if the span is known.
fn highlight(&mut self, source_map: &ast::SourceMap) {
if self.highlighted.is_none() {
self.highlighted = source_map.highlight_span(self.span, &self.msg);
}
}
}

impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.highlighted.as_ref().unwrap_or(&self.msg).fmt(f)
}
}

impl core::error::Error for Error {}

#[derive(Debug)]
struct PackageNotFoundError {
span: Span,
requested: PackageName,
known: Vec<PackageName>,
highlighted: Option<String>,
}

impl PackageNotFoundError {
pub fn new(span: Span, requested: PackageName, known: Vec<PackageName>) -> Self {
Self {
span,
requested,
known,
highlighted: None,
}
}

/// Highlights this error using the given source map, if the span is known.
fn highlight(&mut self, source_map: &ast::SourceMap) {
if self.highlighted.is_none() {
self.highlighted = source_map.highlight_span(self.span, &format!("{self}"));
}
}
}

impl fmt::Display for PackageNotFoundError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(highlighted) = &self.highlighted {
return highlighted.fmt(f);
}
if self.known.is_empty() {
write!(
f,
"package '{}' not found. no known packages.",
self.requested
)?;
} else {
write!(
f,
"package '{}' not found. known packages:\n",
self.requested
)?;
for known in self.known.iter() {
write!(f, " {known}\n")?;
}
}
Ok(())
}
}

impl core::error::Error for PackageNotFoundError {}

impl UnresolvedPackageGroup {
/// Parses the given string as a wit document.
///
/// The `path` argument is used for error reporting. The `contents` provided
/// are considered to be the contents of `path`. This function does not read
/// the filesystem.
#[cfg(feature = "std")]
pub fn parse(path: impl AsRef<Path>, contents: &str) -> Result<UnresolvedPackageGroup> {
pub fn parse(path: impl AsRef<Path>, contents: &str) -> anyhow::Result<UnresolvedPackageGroup> {
let path = path
.as_ref()
.to_str()
Expand All @@ -404,7 +319,7 @@ impl UnresolvedPackageGroup {
/// grouping. This is useful when a WIT package is split across multiple
/// files.
#[cfg(feature = "std")]
pub fn parse_dir(path: impl AsRef<Path>) -> Result<UnresolvedPackageGroup> {
pub fn parse_dir(path: impl AsRef<Path>) -> anyhow::Result<UnresolvedPackageGroup> {
let path = path.as_ref();
let mut map = SourceMap::default();
let cx = || format!("failed to read directory {path:?}");
Expand Down Expand Up @@ -1162,12 +1077,12 @@ pub enum Mangling {
impl core::str::FromStr for Mangling {
type Err = anyhow::Error;

fn from_str(s: &str) -> Result<Mangling> {
fn from_str(s: &str) -> anyhow::Result<Mangling> {
match s {
"legacy" => Ok(Mangling::Legacy),
"standard32" => Ok(Mangling::Standard32),
_ => {
bail!(
anyhow::bail!(
"unknown name mangling `{s}`, \
supported values are `legacy` or `standard32`"
)
Expand Down
136 changes: 136 additions & 0 deletions crates/wit-parser/src/resolve/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
//! Error types for WIT package resolution.

use alloc::boxed::Box;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::fmt;

use crate::{PackageName, SourceMap, Span};

/// Convenience alias for a `Result` whose error type is [`ResolveError`].
pub type ResolveResult<T, E = ResolveError> = Result<T, E>;

/// The category of error that occurred while resolving a WIT package.
#[non_exhaustive]
#[derive(Debug, PartialEq, Eq)]
pub enum ResolveErrorKind {
/// A referenced package could not be found among the known packages.
PackageNotFound {
span: Span,
requested: PackageName,
known: Vec<PackageName>,
},
/// An interface has a transitive dependency that creates an incompatible
/// import relationship.
InvalidTransitiveDependency { span: Span, name: String },
/// The same package is defined in two different locations.
DuplicatePackage {
name: PackageName,
span1: Span,
span2: Span,
},
/// Packages form a dependency cycle.
PackageCycle { package: PackageName, span: Span },
/// A semantic error during resolution (type mismatch, invalid use, etc.)
Semantic { span: Span, message: String },
}

impl ResolveErrorKind {
/// Returns the source span associated with this error.
pub fn span(&self) -> Span {
match self {
ResolveErrorKind::PackageNotFound { span, .. }
| ResolveErrorKind::InvalidTransitiveDependency { span, .. }
| ResolveErrorKind::PackageCycle { span, .. }
| ResolveErrorKind::Semantic { span, .. } => *span,
ResolveErrorKind::DuplicatePackage { span1, .. } => *span1,
}
}
}

impl fmt::Display for ResolveErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ResolveErrorKind::PackageNotFound {
requested, known, ..
} => {
if known.is_empty() {
write!(f, "package '{requested}' not found")
} else {
write!(f, "package '{requested}' not found. known packages:")?;
for k in known {
write!(f, "\n {k}")?;
}
Ok(())
}
}
ResolveErrorKind::InvalidTransitiveDependency { name, .. } => write!(
f,
"interface `{name}` transitively depends on an interface in incompatible ways",
),
ResolveErrorKind::DuplicatePackage { name, .. } => {
write!(f, "package `{name}` is defined in two different locations",)
}
ResolveErrorKind::PackageCycle { package, .. } => {
write!(f, "package `{package}` creates a dependency cycle")
}
ResolveErrorKind::Semantic { message, .. } => message.fmt(f),
}
}
}

/// A single structured error from resolving a WIT package.
#[derive(Debug, PartialEq, Eq)]
pub struct ResolveError(Box<ResolveErrorKind>);

impl ResolveError {
/// Creates a [`ResolveError`] with the [`ResolveErrorKind::Semantic`] variant.
pub fn new_semantic(span: Span, message: impl Into<String>) -> Self {
ResolveErrorKind::Semantic {
span,
message: message.into(),
}
.into()
}

/// Returns the underlying error kind.
pub fn kind(&self) -> &ResolveErrorKind {
&self.0
}

/// Returns the underlying error kind (mutable).
pub fn kind_mut(&mut self) -> &mut ResolveErrorKind {
&mut self.0
}

/// Format this error with source context (file:line:col + snippet).
pub fn highlight(&self, source_map: &SourceMap) -> String {
let e = self.kind();
let msg = e.to_string();
match e {
ResolveErrorKind::DuplicatePackage { name, span1, span2 } => {
let loc1 = source_map.render_location(*span1);
let loc2 = source_map.render_location(*span2);
format!(
"package `{name}` is defined in two different locations:\n * {loc1}\n * {loc2}"
)
}
_ => source_map.highlight_span(e.span(), &msg).unwrap_or(msg),
}
}
}

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

impl core::error::Error for ResolveError {}

impl From<ResolveErrorKind> for ResolveError {
fn from(kind: ResolveErrorKind) -> Self {
ResolveError(Box::new(kind))
}
}
8 changes: 6 additions & 2 deletions crates/wit-parser/src/resolve/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,9 @@ impl Resolve {
.parse_deps_dir(&deps)
.with_context(|| format!("failed to parse dependency directory: {}", deps.display()))?;

let (pkg_id, inner) = self.sort_unresolved_packages(top_pkg, deps)?;
let sort_result = self.sort_unresolved_packages(top_pkg, deps);
let (pkg_id, inner) =
sort_result.map_err(|e| anyhow::anyhow!("{}", e.highlight(&self.source_map)))?;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like I might have asked this already on the previous PR of yours, so I apologize if so, but here this feels kind of bad to stringify the error and forget about the original error. The main downside here is that the returned value of anyhow::Result doesn't have the ability to downcast the error to a concrete error type and inspect it.

I forget, though, was this something where the anyhow::anyhow!("{}", ...) is temporary? Or is this intended to be more permanent? For example should ResolveError have a "from I/O error" variant and this function returns a ResolveError?


Or, alternatively, one thing I thought of is that it's useful to have a decoupled error from the SourceMap where the returned error has all the info it needs to be rendered without pairing to something else, which this call to highlight is doing. I was thinking, though, that highlight would mutate the error-in-place (e.g. filling in some fields) as oppose to returning just a string

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like I might have asked this already on the previous PR of yours, so I apologize if so, but here this feels kind of bad to stringify the error and forget about the original error. The main downside here is that the returned value of anyhow::Result doesn't have the ability to downcast the error to a concrete error type and inspect it.

You're right, I was a bit sloppy here. I think I can fix this by using anyhow::Error::from instead of anyhow!, something like anyhow::Error::from(e).context(msg). I'll play around with it a bit.

I forget, though, was this something where the anyhow::anyhow!("{}", ...) is temporary? Or is this intended to be more permanent? For example should ResolveError have a "from I/O error" variant and this function returns a ResolveError?

In this place it's not temporary. I personally don't think we should introduce any I/O error variants to ResolveError - the resolver's main job is not to interact with the filesystem, but to deal with already loaded files. Adding I/O variants would mix the concerns too much. I/O matters only at the boundary of the resolver.

Or, alternatively, one thing I thought of is that it's useful to have a decoupled error from the SourceMap where the returned error has all the info it needs to be rendered without pairing to something else, which this call to highlight is doing. I was thinking, though, that highlight would mutate the error-in-place (e.g. filling in some fields) as oppose to returning just a string

I generally prefer the errors to stay very lightweight - ie only contain the span info and the necessary metadata. Adding rendered strings to errors means that we lose a clean separation between what went wrong (spans/metadata), and how to display it (source map rendering). And if we mutate the error, the error now becomes dependent on when and whether you call that mutation (which increases the chance of bugs).

I also plan to revisit the highlight naming and the overall error presentation in a follow-up - now that errors are structured, it should be much easier to reason about where and how rendering happens, and to improve individual error messages.

Ok((pkg_id, PackageSourceMap::from_inner(inner)))
}

Expand Down Expand Up @@ -195,7 +197,9 @@ impl Resolve {
match self._push_file(path.as_ref())? {
#[cfg(feature = "decoding")]
ParsedFile::Package(id) => Ok(id),
ParsedFile::Unresolved(pkg) => self.push_group(pkg),
ParsedFile::Unresolved(pkg) => self
.push_group(pkg)
.map_err(|e| anyhow::anyhow!("{}", e.highlight(&self.source_map))),
}
}

Expand Down
Loading
Loading