cranelift-isle: Rewrite error reporting (#5318)

There were several issues with ISLE's existing error reporting
implementation.

- When using Miette for more readable error reports, it would panic if
  errors were reported from multiple files in the same run.
- Miette is pretty heavy-weight for what we're doing, with a lot of
  dependencies.
- The `Error::Errors` enum variant led to normalization steps in many
  places, to avoid using that variant to represent a single error.

This commit:
- replaces Miette with codespan-reporting
- gets rid of a bunch of cargo-vet exemptions
- replaces the `Error::Errors` variant with a new `Errors` type
- removes source info from `Error` variants so they're easy to construct
- adds source info only when formatting `Errors`
- formats `Errors` with a custom `Debug` impl
- shares common code between ISLE's callers, islec and cranelift-codegen
- includes a source snippet even with fancy-errors disabled

I tried to make this a series of smaller commits but I couldn't find any
good split points; everything was too entangled with everything else.
This commit is contained in:
Jamey Sharp
2022-11-23 14:20:48 -08:00
committed by GitHub
parent 48ee42efc2
commit 044b57f334
18 changed files with 342 additions and 548 deletions

View File

@@ -40,7 +40,6 @@ criterion = "0.3"
[build-dependencies]
cranelift-codegen-meta = { path = "meta", version = "0.91.0" }
cranelift-isle = { path = "../isle/isle", version = "=0.91.0" }
miette = { version = "5.1.0", features = ["fancy"], optional = true }
[features]
default = ["std", "unwind"]
@@ -101,7 +100,7 @@ incremental-cache = [
souper-harvest = ["souper-ir", "souper-ir/stringify"]
# Provide fancy Miette-produced errors for ISLE.
isle-errors = ["miette", "cranelift-isle/miette-errors"]
isle-errors = ["cranelift-isle/fancy-errors"]
# Put ISLE generated files in isle_generated_code/, for easier
# inspection, rather than inside of target/.

View File

@@ -15,6 +15,7 @@
// current directory is used to find the sources.
use cranelift_codegen_meta as meta;
use cranelift_isle::error::Errors;
use std::env;
use std::io::Read;
@@ -288,13 +289,16 @@ fn build_isle(
}
if let Err(e) = run_compilation(compilation) {
eprintln!("Error building ISLE files: {:?}", e);
let mut source = e.source();
while let Some(e) = source {
eprintln!("{:?}", e);
source = e.source();
}
had_error = true;
eprintln!("Error building ISLE files:");
eprintln!("{:?}", e);
#[cfg(not(feature = "isle-errors"))]
{
eprintln!("To see a more detailed error report, run: ");
eprintln!();
eprintln!(" $ cargo check -p cranelift-codegen --features isle-errors");
eprintln!();
}
}
}
@@ -311,21 +315,16 @@ fn build_isle(
///
/// NB: This must happen *after* the `cranelift-codegen-meta` functions, since
/// it consumes files generated by them.
fn run_compilation(
compilation: &IsleCompilation,
) -> Result<(), Box<dyn std::error::Error + 'static>> {
fn run_compilation(compilation: &IsleCompilation) -> Result<(), Errors> {
use cranelift_isle as isle;
eprintln!("Rebuilding {}", compilation.output.display());
let code = (|| {
let lexer = isle::lexer::Lexer::from_files(
compilation
.inputs
.iter()
.chain(compilation.untracked_inputs.iter()),
)?;
let defs = isle::parser::parse(lexer)?;
let code = {
let file_paths = compilation
.inputs
.iter()
.chain(compilation.untracked_inputs.iter());
let mut options = isle::codegen::CodegenOptions::default();
// Because we include!() the generated ISLE source, we cannot
@@ -335,62 +334,8 @@ fn run_compilation(
// https://github.com/rust-lang/rust/issues/47995.)
options.exclude_global_allow_pragmas = true;
isle::compile::compile(&defs, &options)
})()
.map_err(|e| {
// Make sure to include the source snippets location info along with
// the error messages.
#[cfg(feature = "isle-errors")]
{
let report = miette::Report::new(e);
return DebugReport(report);
struct DebugReport(miette::Report);
impl std::fmt::Display for DebugReport {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
self.0.handler().debug(&*self.0, f)
}
}
impl std::fmt::Debug for DebugReport {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Display::fmt(self, f)
}
}
impl std::error::Error for DebugReport {}
}
#[cfg(not(feature = "isle-errors"))]
{
return DebugReport(format!("{}", e));
struct DebugReport(String);
impl std::fmt::Display for DebugReport {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
writeln!(f, "ISLE errors:\n\n{}\n", self.0)?;
writeln!(f, "To see a more detailed error report, run: ")?;
writeln!(f, "")?;
writeln!(
f,
" $ cargo check -p cranelift-codegen --features isle-errors"
)?;
writeln!(f, "")?;
Ok(())
}
}
impl std::fmt::Debug for DebugReport {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Display::fmt(self, f)
}
}
impl std::error::Error for DebugReport {}
}
})?;
isle::compile::from_files(file_paths, &options)?
};
let code = rustfmt(&code).unwrap_or_else(|e| {
println!(
@@ -404,7 +349,8 @@ fn run_compilation(
"Writing ISLE-generated Rust code to {}",
compilation.output.display()
);
std::fs::write(&compilation.output, code)?;
std::fs::write(&compilation.output, code)
.map_err(|e| Errors::from_io(e, "failed writing output"))?;
Ok(())
}

View File

@@ -9,8 +9,8 @@ repository = "https://github.com/bytecodealliance/wasmtime/tree/main/cranelift/i
version = "0.91.0"
[dependencies]
codespan-reporting = { version = "0.11.1", optional = true }
log = { workspace = true, optional = true }
miette = { version = "5.1.0", optional = true }
[dev-dependencies]
tempfile = "3"
@@ -19,4 +19,4 @@ tempfile = "3"
default = []
logging = ["log"]
miette-errors = ["miette"]
fancy-errors = ["codespan-reporting"]

View File

@@ -1,13 +1,25 @@
//! Compilation process, from AST to Sema to Sequences of Insts.
use crate::error::Result;
use std::path::Path;
use crate::error::Errors;
use crate::{ast, codegen, sema, trie};
/// Compile the given AST definitions into Rust source code.
pub fn compile(defs: &ast::Defs, options: &codegen::CodegenOptions) -> Result<String> {
pub fn compile(defs: &ast::Defs, options: &codegen::CodegenOptions) -> Result<String, Errors> {
let mut typeenv = sema::TypeEnv::from_ast(defs)?;
let termenv = sema::TermEnv::from_ast(&mut typeenv, defs)?;
crate::overlap::check(&mut typeenv, &termenv)?;
let tries = trie::build_tries(&termenv);
Ok(codegen::codegen(&typeenv, &termenv, &tries, options))
}
/// Compile the given files into Rust source code.
pub fn from_files<P: AsRef<Path>>(
inputs: impl IntoIterator<Item = P>,
options: &codegen::CodegenOptions,
) -> Result<String, Errors> {
let lexer = crate::lexer::Lexer::from_files(inputs)?;
let defs = crate::parser::parse(lexer)?;
compile(&defs, options)
}

View File

@@ -4,16 +4,75 @@ use std::sync::Arc;
use crate::lexer::Pos;
/// Either `Ok(T)` or `Err(isle::Error)`.
pub type Result<T> = std::result::Result<T, Error>;
/// A collection of errors from attempting to compile some ISLE source files.
pub struct Errors {
/// The individual errors.
pub errors: Vec<Error>,
pub(crate) filenames: Vec<Arc<str>>,
pub(crate) file_texts: Vec<Arc<str>>,
}
impl std::fmt::Debug for Errors {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
if self.errors.is_empty() {
return Ok(());
}
let diagnostics = Vec::from_iter(self.errors.iter().map(|e| {
let message = match e {
Error::IoError { context, .. } => format!("{}", context),
Error::ParseError { msg, .. } => format!("parse error: {}", msg),
Error::TypeError { msg, .. } => format!("type error: {}", msg),
Error::UnreachableError { msg, .. } => format!("unreachable rule: {}", msg),
Error::OverlapError { msg, .. } => format!("overlap error: {}", msg),
};
let labels = match e {
Error::IoError { .. } => vec![],
Error::ParseError { span, .. }
| Error::TypeError { span, .. }
| Error::UnreachableError { span, .. } => {
vec![Label::primary(span.from.file, span)]
}
Error::OverlapError { rules, .. } => {
let mut labels = vec![Label::primary(rules[0].from.file, &rules[0])];
labels.extend(
rules[1..]
.iter()
.map(|span| Label::secondary(span.from.file, span)),
);
labels
}
};
let mut sources = Vec::new();
let mut source = e.source();
while let Some(e) = source {
sources.push(format!("{:?}", e));
source = std::error::Error::source(e);
}
Diagnostic::error()
.with_message(message)
.with_labels(labels)
.with_notes(sources)
}));
self.emit(f, diagnostics)?;
if self.errors.len() > 1 {
writeln!(f, "found {} errors", self.errors.len())?;
}
Ok(())
}
}
/// Errors produced by ISLE.
#[derive(Clone, Debug)]
#[derive(Debug)]
pub enum Error {
/// An I/O error.
IoError {
/// The underlying I/O error.
error: Arc<std::io::Error>,
error: std::io::Error,
/// The context explaining what caused the I/O error.
context: String,
},
@@ -23,9 +82,6 @@ pub enum Error {
/// The error message.
msg: String,
/// The input ISLE source.
src: Source,
/// The location of the parse error.
span: Span,
},
@@ -35,9 +91,6 @@ pub enum Error {
/// The error message.
msg: String,
/// The input ISLE source.
src: Source,
/// The location of the type error.
span: Span,
},
@@ -47,9 +100,6 @@ pub enum Error {
/// The error message.
msg: String,
/// The input ISLE source.
src: Source,
/// The location of the unreachable rule.
span: Span,
},
@@ -62,153 +112,98 @@ pub enum Error {
/// The locations of all the rules that overlap. When there are more than two rules
/// present, the first rule is the one with the most overlaps (likely a fall-through
/// wildcard case).
rules: Vec<(Source, Span)>,
rules: Vec<Span>,
},
/// Multiple errors.
Errors(Vec<Error>),
}
impl Error {
/// Create a `isle::Error` from the given I/O error and context.
impl Errors {
/// Create `isle::Errors` from the given I/O error and context.
pub fn from_io(error: std::io::Error, context: impl Into<String>) -> Self {
Error::IoError {
error: Arc::new(error),
context: context.into(),
Errors {
errors: vec![Error::IoError {
error,
context: context.into(),
}],
filenames: Vec::new(),
file_texts: Vec::new(),
}
}
}
impl From<Vec<Error>> for Error {
fn from(es: Vec<Error>) -> Self {
Error::Errors(es)
#[cfg(feature = "fancy-errors")]
fn emit(
&self,
f: &mut std::fmt::Formatter,
diagnostics: Vec<Diagnostic<usize>>,
) -> std::fmt::Result {
use codespan_reporting::term::termcolor;
let w = termcolor::BufferWriter::stderr(termcolor::ColorChoice::Auto);
let mut b = w.buffer();
let mut files = codespan_reporting::files::SimpleFiles::new();
for (name, source) in self.filenames.iter().zip(self.file_texts.iter()) {
files.add(name, source);
}
for diagnostic in diagnostics {
codespan_reporting::term::emit(&mut b, &Default::default(), &files, &diagnostic)
.map_err(|_| std::fmt::Error)?;
}
let b = b.into_inner();
let b = std::str::from_utf8(&b).map_err(|_| std::fmt::Error)?;
f.write_str(b)
}
#[cfg(not(feature = "fancy-errors"))]
fn emit(
&self,
f: &mut std::fmt::Formatter,
diagnostics: Vec<Diagnostic<usize>>,
) -> std::fmt::Result {
let line_starts: Vec<Vec<_>> = self
.file_texts
.iter()
.map(|text| {
let mut end = 0;
text.split_inclusive('\n')
.map(|line| {
let start = end;
end += line.len();
start
})
.collect()
})
.collect();
let pos = |file_id: usize, offset| {
let starts = &line_starts[file_id];
let line = starts.partition_point(|&start| start <= offset);
let text = &self.file_texts[file_id];
let line_range = starts[line - 1]..starts.get(line).copied().unwrap_or(text.len());
let col = offset - line_range.start + 1;
format!(
"{}:{}:{}: {}",
self.filenames[file_id], line, col, &text[line_range]
)
};
for diagnostic in diagnostics {
writeln!(f, "{}", diagnostic.message)?;
for label in diagnostic.labels {
f.write_str(&pos(label.file_id, label.range.start))?;
}
for note in diagnostic.notes {
writeln!(f, "{}", note)?;
}
}
Ok(())
}
}
impl Error {
fn unwrap_errors(&self) -> &[Error] {
match self {
Error::Errors(e) => e,
_ => panic!("`isle::Error::unwrap_errors` on non-`isle::Error::Errors`"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::IoError { error, .. } => Some(&*error as &dyn std::error::Error),
Error::IoError { error, .. } => Some(error),
_ => None,
}
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Error::IoError { context, .. } => write!(f, "{}", context),
// Include locations directly in the `Display` output when
// we're not wrapping errors with miette (which provides
// its own way of showing locations and context).
#[cfg(not(feature = "miette-errors"))]
Error::ParseError { src, span, msg, .. } => write!(
f,
"{}: parse error: {}",
span.from.pretty_print_with_filename(&*src.name),
msg
),
#[cfg(not(feature = "miette-errors"))]
Error::TypeError { src, span, msg, .. } => write!(
f,
"{}: type error: {}",
span.from.pretty_print_with_filename(&*src.name),
msg
),
#[cfg(feature = "miette-errors")]
Error::ParseError { msg, .. } => write!(f, "parse error: {}", msg),
#[cfg(feature = "miette-errors")]
Error::TypeError { msg, .. } => write!(f, "type error: {}", msg),
Error::UnreachableError { src, span, msg } => {
write!(
f,
"{}: unreachable rule: {}",
span.from.pretty_print_with_filename(&*src.name),
msg
)
}
Error::OverlapError { msg, rules, .. } => {
writeln!(f, "overlap error: {}\n{}", msg, OverlappingRules(&rules))
}
Error::Errors(_) => write!(
f,
"{}found {} errors",
DisplayErrors(self.unwrap_errors()),
self.unwrap_errors().len(),
),
}
}
}
struct DisplayErrors<'a>(&'a [Error]);
impl std::fmt::Display for DisplayErrors<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for e in self.0 {
writeln!(f, "{}", e)?;
}
Ok(())
}
}
struct OverlappingRules<'a>(&'a [(Source, Span)]);
impl std::fmt::Display for OverlappingRules<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for (src, span) in self.0 {
writeln!(f, " {}", span.from.pretty_print_with_filename(&*src.name))?;
}
Ok(())
}
}
/// A source file and its contents.
#[derive(Clone)]
pub struct Source {
/// The name of this source file.
pub name: Arc<str>,
/// The text of this source file.
#[allow(unused)] // Used only when miette is enabled.
pub text: Arc<str>,
}
impl std::fmt::Debug for Source {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Source")
.field("name", &self.name)
.field("source", &"<redacted>");
Ok(())
}
}
impl Source {
pub(crate) fn new(name: Arc<str>, text: Arc<str>) -> Self {
Self { name, text }
}
/// Get this source's file name.
pub fn name(&self) -> &Arc<str> {
&self.name
}
/// Get this source's text contents.
pub fn text(&self) -> &Arc<str> {
&self.name
}
}
/// A span in a given source.
#[derive(Clone, Debug)]
pub struct Span {
@@ -237,3 +232,69 @@ impl Span {
}
}
}
impl From<&Span> for std::ops::Range<usize> {
fn from(span: &Span) -> Self {
span.from.offset..span.to.offset
}
}
use diagnostic::{Diagnostic, Label};
#[cfg(feature = "fancy-errors")]
use codespan_reporting::diagnostic;
#[cfg(not(feature = "fancy-errors"))]
/// Minimal versions of types from codespan-reporting.
mod diagnostic {
use std::ops::Range;
pub struct Diagnostic<FileId> {
pub message: String,
pub labels: Vec<Label<FileId>>,
pub notes: Vec<String>,
}
impl<FileId> Diagnostic<FileId> {
pub fn error() -> Self {
Self {
message: String::new(),
labels: Vec::new(),
notes: Vec::new(),
}
}
pub fn with_message(mut self, message: impl Into<String>) -> Self {
self.message = message.into();
self
}
pub fn with_labels(mut self, labels: Vec<Label<FileId>>) -> Self {
self.labels = labels;
self
}
pub fn with_notes(mut self, notes: Vec<String>) -> Self {
self.notes = notes;
self
}
}
pub struct Label<FileId> {
pub file_id: FileId,
pub range: Range<usize>,
}
impl<FileId> Label<FileId> {
pub fn primary(file_id: FileId, range: impl Into<Range<usize>>) -> Self {
Self {
file_id,
range: range.into(),
}
}
pub fn secondary(file_id: FileId, range: impl Into<Range<usize>>) -> Self {
Self::primary(file_id, range)
}
}
}

View File

@@ -1,65 +0,0 @@
//! miette-specific trait implementations. This is kept separate so
//! that we can have a very lightweight build of the ISLE compiler as
//! part of the Cranelift build process without pulling in any
//! dependencies.
use crate::error::{Error, Source, Span};
use miette::{SourceCode, SourceSpan};
impl From<Span> for SourceSpan {
fn from(span: Span) -> Self {
SourceSpan::new(span.from.offset.into(), span.to.offset.into())
}
}
impl SourceCode for Source {
fn read_span<'a>(
&'a self,
span: &SourceSpan,
context_lines_before: usize,
context_lines_after: usize,
) -> std::result::Result<Box<dyn miette::SpanContents<'a> + 'a>, miette::MietteError> {
let contents = self
.text
.read_span(span, context_lines_before, context_lines_after)?;
Ok(Box::new(miette::MietteSpanContents::new_named(
self.name.to_string(),
contents.data(),
contents.span().clone(),
contents.line(),
contents.column(),
contents.line_count(),
)))
}
}
impl miette::Diagnostic for Error {
fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
match self {
Self::ParseError { msg, span, .. } | Self::TypeError { msg, span, .. } => {
Some(Box::new(
vec![miette::LabeledSpan::new_with_span(
Some(msg.clone()),
span.clone(),
)]
.into_iter(),
))
}
_ => None,
}
}
fn source_code(&self) -> std::option::Option<&dyn miette::SourceCode> {
match self {
Self::ParseError { src, .. } | Self::TypeError { src, .. } => Some(src),
_ => None,
}
}
fn related(&self) -> Option<Box<dyn Iterator<Item = &dyn miette::Diagnostic> + '_>> {
match self {
Self::Errors(errors) => Some(Box::new(
errors.iter().map(|x| x as &dyn miette::Diagnostic),
)),
_ => None,
}
}
}

View File

@@ -1,10 +1,12 @@
//! Lexer for the ISLE language.
use crate::error::{Error, Result, Source, Span};
use crate::error::{Error, Errors, Span};
use std::borrow::Cow;
use std::path::Path;
use std::sync::Arc;
type Result<T> = std::result::Result<T, Errors>;
/// The lexer.
///
/// Breaks source text up into a sequence of tokens (with source positions).
@@ -43,19 +45,10 @@ pub struct Pos {
}
impl Pos {
/// Print this source position as `file.isle:12:34`.
pub fn pretty_print(&self, filenames: &[Arc<str>]) -> String {
self.pretty_print_with_filename(&filenames[self.file])
}
/// Print this source position as `file.isle line 12`.
pub fn pretty_print_line(&self, filenames: &[Arc<str>]) -> String {
format!("{} line {}", filenames[self.file], self.line)
}
/// As above for `pretty_print`, but with the specific filename
/// already provided.
pub fn pretty_print_with_filename(&self, filename: &str) -> String {
format!("{}:{}:{}", filename, self.line, self.col)
}
}
/// A token of ISLE source.
@@ -107,7 +100,7 @@ impl<'a> Lexer<'a> {
filenames.push(f.display().to_string().into());
let s = std::fs::read_to_string(f)
.map_err(|e| Error::from_io(e, format!("failed to read file: {}", f.display())))?;
.map_err(|e| Errors::from_io(e, format!("failed to read file: {}", f.display())))?;
file_texts.push(s.into());
}
@@ -165,14 +158,14 @@ impl<'a> Lexer<'a> {
}
}
fn error(&self, pos: Pos, msg: impl Into<String>) -> Error {
Error::ParseError {
msg: msg.into(),
src: Source::new(
self.filenames[pos.file].clone(),
self.file_texts[pos.file].clone(),
),
span: Span::new_single(self.pos()),
fn error(&self, pos: Pos, msg: impl Into<String>) -> Errors {
Errors {
errors: vec![Error::ParseError {
msg: msg.into(),
span: Span::new_single(pos),
}],
filenames: self.filenames.clone(),
file_texts: self.file_texts.clone(),
}
}

View File

@@ -211,6 +211,3 @@ pub mod parser;
pub mod sema;
pub mod trie;
pub mod trie_again;
#[cfg(feature = "miette-errors")]
mod error_miette;

View File

@@ -3,20 +3,24 @@
use std::collections::hash_map::Entry;
use std::collections::{HashMap, HashSet};
use crate::error::{Error, Result, Source, Span};
use crate::error::{self, Error, Span};
use crate::lexer::Pos;
use crate::sema::{TermEnv, TermId, TermKind, TypeEnv};
use crate::trie_again;
/// Check for overlap.
pub fn check(tyenv: &TypeEnv, termenv: &TermEnv) -> Result<()> {
let (terms, mut errors) = trie_again::build(termenv, tyenv);
errors.append(&mut check_overlaps(terms, termenv).report(tyenv));
pub fn check(tyenv: &TypeEnv, termenv: &TermEnv) -> Result<(), error::Errors> {
let (terms, mut errors) = trie_again::build(termenv);
errors.append(&mut check_overlaps(terms, termenv).report());
match errors.len() {
0 => Ok(()),
1 => Err(errors.pop().unwrap()),
_ => Err(Error::Errors(errors)),
if errors.is_empty() {
Ok(())
} else {
Err(error::Errors {
errors,
filenames: tyenv.filenames.clone(),
file_texts: tyenv.file_texts.clone(),
})
}
}
@@ -32,19 +36,9 @@ impl Errors {
/// nodes from the graph with the highest degree, reporting errors for them and their direct
/// connections. The goal with reporting errors this way is to prefer reporting rules that
/// overlap with many others first, and then report other more targeted overlaps later.
fn report(mut self, tyenv: &TypeEnv) -> Vec<Error> {
fn report(mut self) -> Vec<Error> {
let mut errors = Vec::new();
let get_info = |pos: Pos| {
let file = pos.file;
let src = Source::new(
tyenv.filenames[file].clone(),
tyenv.file_texts[file].clone(),
);
let span = Span::new_single(pos);
(src, span)
};
while let Some((&pos, _)) = self
.nodes
.iter()
@@ -62,9 +56,9 @@ impl Errors {
}
// build the real error
let mut rules = vec![get_info(pos)];
let mut rules = vec![Span::new_single(pos)];
rules.extend(node.into_iter().map(get_info));
rules.extend(node.into_iter().map(Span::new_single));
errors.push(Error::OverlapError {
msg: String::from("rules are overlapping"),
@@ -73,7 +67,7 @@ impl Errors {
}
errors.sort_by_key(|err| match err {
Error::OverlapError { rules, .. } => rules.first().unwrap().1.from,
Error::OverlapError { rules, .. } => rules.first().unwrap().from,
_ => Pos::default(),
});
errors

View File

@@ -1,9 +1,11 @@
//! Parser for ISLE language.
use crate::ast::*;
use crate::error::*;
use crate::error::{Error, Errors, Span};
use crate::lexer::{Lexer, Pos, Token};
type Result<T> = std::result::Result<T, Errors>;
/// Parse the top-level ISLE definitions and return their AST.
pub fn parse(lexer: Lexer) -> Result<Defs> {
let parser = Parser::new(lexer);
@@ -32,14 +34,14 @@ impl<'a> Parser<'a> {
Parser { lexer }
}
fn error(&self, pos: Pos, msg: String) -> Error {
Error::ParseError {
msg,
src: Source::new(
self.lexer.filenames[pos.file].clone(),
self.lexer.file_texts[pos.file].clone(),
),
span: Span::new_single(pos),
fn error(&self, pos: Pos, msg: String) -> Errors {
Errors {
errors: vec![Error::ParseError {
msg,
span: Span::new_single(pos),
}],
filenames: self.lexer.filenames.clone(),
file_texts: self.lexer.file_texts.clone(),
}
}

View File

@@ -56,7 +56,7 @@ declare_id!(
/// The type environment.
///
/// Keeps track of which symbols and rules have which types.
#[derive(Clone, Debug)]
#[derive(Debug)]
pub struct TypeEnv {
/// Arena of input ISLE source filenames.
///
@@ -887,7 +887,7 @@ macro_rules! unwrap_or_continue {
impl TypeEnv {
/// Construct the type environment from the AST.
pub fn from_ast(defs: &ast::Defs) -> Result<TypeEnv> {
pub fn from_ast(defs: &ast::Defs) -> Result<TypeEnv, Errors> {
let mut tyenv = TypeEnv {
filenames: defs.filenames.clone(),
file_texts: defs.file_texts.clone(),
@@ -968,11 +968,15 @@ impl TypeEnv {
Ok(tyenv)
}
fn return_errors(&mut self) -> Result<()> {
match self.errors.len() {
0 => Ok(()),
1 => Err(self.errors.pop().unwrap()),
_ => Err(Error::Errors(std::mem::take(&mut self.errors))),
fn return_errors(&mut self) -> Result<(), Errors> {
if self.errors.is_empty() {
Ok(())
} else {
Err(Errors {
errors: std::mem::take(&mut self.errors),
filenames: self.filenames.clone(),
file_texts: self.file_texts.clone(),
})
}
}
@@ -1062,16 +1066,10 @@ impl TypeEnv {
}
fn error(&self, pos: Pos, msg: impl Into<String>) -> Error {
let e = Error::TypeError {
Error::TypeError {
msg: msg.into(),
src: Source::new(
self.filenames[pos.file].clone(),
self.file_texts[pos.file].clone(),
),
span: Span::new_single(pos),
};
log!("{}", e);
e
}
}
fn report_error(&mut self, pos: Pos, msg: impl Into<String>) {
@@ -1121,7 +1119,7 @@ impl Bindings {
impl TermEnv {
/// Construct the term environment from the AST and the type environment.
pub fn from_ast(tyenv: &mut TypeEnv, defs: &ast::Defs) -> Result<TermEnv> {
pub fn from_ast(tyenv: &mut TypeEnv, defs: &ast::Defs) -> Result<TermEnv, Errors> {
let mut env = TermEnv {
terms: vec![],
term_map: StableMap::new(),

View File

@@ -1,6 +1,6 @@
//! A strongly-normalizing intermediate representation for ISLE rules. This representation is chosen
//! to closely reflect the operations we can implement in Rust, to make code generation easy.
use crate::error::{Error, Source, Span};
use crate::error::{Error, Span};
use crate::lexer::Pos;
use crate::sema;
use crate::DisjointSets;
@@ -167,16 +167,13 @@ pub struct RuleSet {
}
/// Construct a [RuleSet] for each term in `termenv` that has rules.
pub fn build(
termenv: &sema::TermEnv,
tyenv: &sema::TypeEnv,
) -> (Vec<(sema::TermId, RuleSet)>, Vec<Error>) {
pub fn build(termenv: &sema::TermEnv) -> (Vec<(sema::TermId, RuleSet)>, Vec<Error>) {
let mut errors = Vec::new();
let mut term = HashMap::new();
for rule in termenv.rules.iter() {
term.entry(rule.root_term)
.or_insert_with(RuleSetBuilder::default)
.add_rule(rule, termenv, tyenv, &mut errors);
.add_rule(rule, termenv, &mut errors);
}
// The `term` hash map may return terms in any order. Sort them to ensure that we produce the
@@ -282,13 +279,7 @@ struct RuleSetBuilder {
}
impl RuleSetBuilder {
fn add_rule(
&mut self,
rule: &sema::Rule,
termenv: &sema::TermEnv,
tyenv: &sema::TypeEnv,
errors: &mut Vec<Error>,
) {
fn add_rule(&mut self, rule: &sema::Rule, termenv: &sema::TermEnv, errors: &mut Vec<Error>) {
self.current_rule.pos = rule.pos;
self.current_rule.prio = rule.prio;
self.current_rule.result = rule.visit(self, termenv);
@@ -299,20 +290,17 @@ impl RuleSetBuilder {
self.rules.rules.push(rule);
} else {
// If this rule can never match, drop it so it doesn't affect overlap checking.
errors.extend(self.unreachable.drain(..).map(|err| {
let src = Source::new(
tyenv.filenames[err.pos.file].clone(),
tyenv.file_texts[err.pos.file].clone(),
);
Error::UnreachableError {
msg: format!(
"rule requires binding to match both {:?} and {:?}",
err.constraint_a, err.constraint_b
),
src,
span: Span::new_single(err.pos),
}
}))
errors.extend(
self.unreachable
.drain(..)
.map(|err| Error::UnreachableError {
msg: format!(
"rule requires binding to match both {:?} and {:?}",
err.constraint_a, err.constraint_b
),
span: Span::new_single(err.pos),
}),
)
}
}

View File

@@ -1,18 +1,16 @@
//! Helper for autogenerated unit tests.
use cranelift_isle::error::Result;
use cranelift_isle::{compile, lexer, parser};
use cranelift_isle::compile;
use cranelift_isle::error::Errors;
use std::default::Default;
fn build(filename: &str) -> Result<String> {
let lexer = lexer::Lexer::from_files(vec![filename])?;
let defs = parser::parse(lexer)?;
compile::compile(&defs, &Default::default())
fn build(filename: &str) -> Result<String, Errors> {
compile::from_files(&[filename], &Default::default())
}
pub fn run_pass(filename: &str) {
if let Err(err) = build(filename) {
panic!("pass test failed:\n{}", err);
panic!("pass test failed:\n{:?}", err);
}
}

View File

@@ -7,7 +7,6 @@ license = "Apache-2.0 WITH LLVM-exception"
publish = false
[dependencies]
cranelift-isle = { version = "*", path = "../isle/", features = ["miette-errors", "logging"] }
cranelift-isle = { version = "*", path = "../isle/", features = ["fancy-errors", "logging"] }
env_logger = { workspace = true }
miette = { version = "5.1.0", features = ["fancy"] }
clap = { workspace = true }

View File

@@ -1,6 +1,6 @@
use clap::Parser;
use cranelift_isle::{compile, lexer, parser};
use miette::{Context, IntoDiagnostic, Result};
use cranelift_isle::compile;
use cranelift_isle::error::Errors;
use std::{
default::Default,
fs,
@@ -20,33 +20,19 @@ struct Opts {
inputs: Vec<PathBuf>,
}
fn main() -> Result<()> {
fn main() -> Result<(), Errors> {
let _ = env_logger::try_init();
let _ = miette::set_hook(Box::new(|_| {
Box::new(
miette::MietteHandlerOpts::new()
// `miette` mistakenly uses braille-optimized output for emacs's
// `M-x shell`.
.force_graphical(true)
.build(),
)
}));
let opts = Opts::parse();
let lexer = lexer::Lexer::from_files(opts.inputs)?;
let defs = parser::parse(lexer)?;
let code = compile::compile(&defs, &Default::default())?;
let code = compile::from_files(opts.inputs, &Default::default())?;
let stdout = io::stdout();
let (mut output, output_name): (Box<dyn Write>, _) = match &opts.output {
Some(f) => {
let output = Box::new(
fs::File::create(f)
.into_diagnostic()
.with_context(|| format!("failed to create '{}'", f.display()))?,
);
let output =
Box::new(fs::File::create(f).map_err(|e| {
Errors::from_io(e, format!("failed to create '{}'", f.display()))
})?);
(output, f.display().to_string())
}
None => {
@@ -57,8 +43,7 @@ fn main() -> Result<()> {
output
.write_all(code.as_bytes())
.into_diagnostic()
.with_context(|| format!("failed to write to '{}'", output_name))?;
.map_err(|e| Errors::from_io(e, format!("failed to write to '{}'", output_name)))?;
Ok(())
}