From bb7105387d8ce07180f8e183eb315363189c5ae7 Mon Sep 17 00:00:00 2001 From: Joshua Groves Date: Sun, 28 Feb 2021 00:52:34 -0330 Subject: [PATCH] [wgsl-in] Use codespan to report errors --- Cargo.toml | 3 +- bin/convert.rs | 9 ++- src/front/wgsl/lexer.rs | 6 +- src/front/wgsl/mod.rs | 164 ++++++++++++++++++++++++++++++---------- tests/errors.rs | 16 +++- 5 files changed, 148 insertions(+), 50 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6c85ab552d..43e16fde92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ license = "MIT OR Apache-2.0" [dependencies] bitflags = "1" bit-set = "0.5" +codespan-reporting = { version = "0.11.0", optional = true } fxhash = "0.2" log = "0.4" num-traits = "0.2" @@ -32,7 +33,7 @@ serialize = ["serde"] deserialize = ["serde"] spv-in = ["petgraph", "spirv"] spv-out = ["spirv"] -wgsl-in = [] +wgsl-in = ["codespan-reporting"] [[bin]] name = "convert" diff --git a/bin/convert.rs b/bin/convert.rs index f3cf33e508..2726ae44b8 100644 --- a/bin/convert.rs +++ b/bin/convert.rs @@ -100,7 +100,14 @@ fn main() { #[cfg(feature = "wgsl-in")] "wgsl" => { let input = fs::read_to_string(input_path).unwrap(); - naga::front::wgsl::parse_str(&input).unwrap_pretty() + let result = naga::front::wgsl::parse_str(&input); + match result { + Ok(v) => v, + Err(ref e) => { + e.emit_to_stderr(); + panic!("unable to parse WGSL"); + } + } } #[cfg(feature = "glsl-in")] "vert" => { diff --git a/src/front/wgsl/lexer.rs b/src/front/wgsl/lexer.rs index cd2aa04d0a..bc6e9fe05c 100644 --- a/src/front/wgsl/lexer.rs +++ b/src/front/wgsl/lexer.rs @@ -258,7 +258,7 @@ impl<'a> Lexer<'a> { pub(super) fn next_ident(&mut self) -> Result<&'a str, Error<'a>> { match self.next() { (Token::Word(word), _) => Ok(word), - other => Err(Error::Unexpected(other, "ident")), + other => Err(Error::Unexpected(other, "identifier")), } } @@ -305,10 +305,6 @@ impl<'a> Lexer<'a> { self.expect(Token::Paren('>'))?; Ok(format) } - - pub(super) fn offset_from(&self, source: &'a str) -> usize { - source.len() - self.input.len() - } } #[cfg(test)] diff --git a/src/front/wgsl/mod.rs b/src/front/wgsl/mod.rs index 5e369f4e86..e313bc5535 100644 --- a/src/front/wgsl/mod.rs +++ b/src/front/wgsl/mod.rs @@ -14,11 +14,21 @@ use crate::{ }; use self::lexer::Lexer; -use std::{num::NonZeroU32, ops::Range}; +use codespan_reporting::{ + diagnostic::{Diagnostic, Label}, + files::SimpleFile, + term::{ + self, + termcolor::{ColorChoice, ColorSpec, StandardStream, WriteColor}, + }, +}; +use std::{ + io::{self, Write}, + num::NonZeroU32, + ops::Range, +}; use thiserror::Error; -const SPACE: &str = " "; - type TokenSpan<'a> = (Token<'a>, Range); #[derive(Copy, Clone, Debug, PartialEq)] @@ -44,6 +54,27 @@ pub enum Token<'a> { End, } +impl<'a> Error<'a> { + fn as_parse_error(&self, source: &'a str) -> ParseError<'a> { + match self { + Error::Unexpected((_, unexpected_span), expected) => ParseError { + message: format!( + "expected {}, found '{}'", + expected, + &source[unexpected_span.clone()] + ), + labels: vec![(unexpected_span.clone(), format!("expected {}", expected))], + source, + }, + error => ParseError { + message: error.to_string(), + labels: Vec::new(), + source, + }, + } + } +} + #[derive(Clone, Debug, Error)] pub enum Error<'a> { #[error("unexpected token {:?}, expected {1}", (.0).0)] @@ -332,13 +363,55 @@ struct ParsedVariable<'a> { init: Option>, } -#[derive(Clone, Debug, Error)] -#[error("error while parsing WGSL in scopes {scopes:?} at line {line} pos {pos}: {error}")] +#[derive(Clone, Debug)] pub struct ParseError<'a> { - pub error: Error<'a>, - pub scopes: Vec, - pub line: usize, - pub pos: usize, + message: String, + labels: Vec<(Range, String)>, + source: &'a str, +} + +impl<'a> ParseError<'a> { + fn diagnostic(&self) -> Diagnostic<()> { + let diagnostic = Diagnostic::error() + .with_message(self.message.to_string()) + .with_labels( + self.labels + .iter() + .map(|label| { + Label::primary((), label.0.clone()).with_message(label.1.to_string()) + }) + .collect(), + ); + diagnostic + } + + pub fn emit_to_stderr(&self) { + let files = SimpleFile::new("wgsl", self.source); + let config = codespan_reporting::term::Config::default(); + let writer = StandardStream::stderr(ColorChoice::Always); + term::emit(&mut writer.lock(), &config, &files, &self.diagnostic()) + .expect("cannot write error"); + } + + pub fn emit_to_string(&self) -> String { + let files = SimpleFile::new("wgsl", self.source); + let config = codespan_reporting::term::Config::default(); + let mut writer = StringErrorBuffer::new(); + term::emit(&mut writer, &config, &files, &self.diagnostic()).expect("cannot write error"); + writer.into_string() + } +} + +impl<'a> std::fmt::Display for ParseError<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.message) + } +} + +impl<'a> std::error::Error for ParseError<'a> { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + None + } } pub struct Parser { @@ -2324,42 +2397,12 @@ impl Parser { let mut lookup_global_expression = FastHashMap::default(); loop { match self.parse_global_decl(&mut lexer, &mut module, &mut lookup_global_expression) { - Err(error) => { - let pos = lexer.offset_from(source); - let (mut rows, mut cols) = (0, 1); - let (mut prev_line, mut cur_line) = ("", ""); - for line in source[..pos].lines() { - rows += 1; - cols = line.len(); - prev_line = cur_line; - cur_line = line; - } - log::error!("|\t{}", prev_line); - log::error!( - ">\t{}{}", - cur_line, - source[pos..].lines().next().unwrap_or_default() - ); - if cols <= SPACE.len() { - log::error!("|\t{}^", &SPACE[..cols]); - } - return Err(ParseError { - error, - scopes: std::mem::replace(&mut self.scopes, Vec::new()), - line: rows, - pos: cols, - }); - } + Err(error) => return Err(error.as_parse_error(lexer.source)), Ok(true) => {} Ok(false) => { if !self.scopes.is_empty() { log::error!("Reached the end of file, but scopes are not closed"); - return Err(ParseError { - error: Error::Other, - scopes: std::mem::replace(&mut self.scopes, Vec::new()), - line: 0, - pos: 0, - }); + return Err(Error::Other.as_parse_error(lexer.source)); }; return Ok(module); } @@ -2371,3 +2414,42 @@ impl Parser { pub fn parse_str(source: &str) -> Result { Parser::new().parse(source) } + +pub struct StringErrorBuffer { + buf: Vec, +} + +impl StringErrorBuffer { + pub fn new() -> Self { + Self { buf: Vec::new() } + } + + pub fn into_string(self) -> String { + String::from_utf8(self.buf).unwrap() + } +} + +impl Write for StringErrorBuffer { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.buf.extend(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +impl WriteColor for StringErrorBuffer { + fn supports_color(&self) -> bool { + false + } + + fn set_color(&mut self, _spec: &ColorSpec) -> io::Result<()> { + Ok(()) + } + + fn reset(&mut self) -> io::Result<()> { + Ok(()) + } +} diff --git a/tests/errors.rs b/tests/errors.rs index 81841a82ed..d8d355e3e7 100644 --- a/tests/errors.rs +++ b/tests/errors.rs @@ -1,7 +1,12 @@ #[cfg(feature = "wgsl-in")] macro_rules! err { ($value:expr, @$snapshot:literal) => { - ::insta::assert_snapshot!(naga::front::wgsl::parse_str($value).expect_err("expected parser error").to_string(), @$snapshot); + ::insta::assert_snapshot!( + naga::front::wgsl::parse_str($value) + .expect_err("expected parser error") + .emit_to_string(), + @$snapshot + ); }; } @@ -10,6 +15,13 @@ macro_rules! err { fn function_without_identifier() { err!( "fn () {}", - @"error while parsing WGSL in scopes [FunctionDecl] at line 1 pos 4: unexpected token Paren('('), expected ident" + @r###" + error: expected identifier, found '(' + ┌─ wgsl:1:4 + │ + 1 │ fn () {} + │ ^ expected identifier + + "### ); }