use crate::source::{FileRegistry, SourceLocation};
use crate::token::TokenKind;
use std::fmt;
use std::path::{Path, PathBuf};
pub struct DisplayLocation<'a> {
pub loc: &'a SourceLocation,
pub files: &'a FileRegistry,
}
impl<'a> fmt::Display for DisplayLocation<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let path = self.files.get_path(self.loc.file_id);
write!(f, "{}:{}:{}", path.display(), self.loc.line, self.loc.column)
}
}
#[derive(Debug)]
pub enum LexError {
UnterminatedComment,
UnterminatedString,
UnterminatedChar,
EmptyCharLit,
InvalidChar(char),
InvalidEscape(char),
InvalidNumber(String),
InvalidSuffix(String),
UnknownIdentifier(String),
}
impl fmt::Display for LexError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LexError::UnterminatedComment => write!(f, "unterminated block comment"),
LexError::UnterminatedString => write!(f, "unterminated string literal"),
LexError::UnterminatedChar => write!(f, "unterminated character literal"),
LexError::EmptyCharLit => write!(f, "empty character literal"),
LexError::InvalidChar(c) => write!(f, "invalid character: {:?}", c),
LexError::InvalidEscape(c) => write!(f, "invalid escape sequence: \\{}", c),
LexError::InvalidNumber(s) => write!(f, "invalid number: {}", s),
LexError::InvalidSuffix(s) => write!(f, "invalid suffix: {}", s),
LexError::UnknownIdentifier(s) => write!(f, "unknown identifier: {}", s),
}
}
}
#[derive(Debug)]
pub enum PPError {
InvalidDirective(String),
MacroRedefinition(String),
IncludeNotFound(PathBuf),
UnmatchedEndif,
MissingEndif,
InvalidMacroArgs(String),
IoError(PathBuf, String),
InvalidCondition(String),
InvalidStringize,
InvalidTokenPaste,
RecursiveMacro(String),
UnmatchedElse,
ElifAfterElse,
}
impl fmt::Display for PPError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PPError::InvalidDirective(s) => write!(f, "invalid directive: #{}", s),
PPError::MacroRedefinition(s) => write!(f, "macro redefinition: {}", s),
PPError::IncludeNotFound(p) => write!(f, "include file not found: {}", p.display()),
PPError::UnmatchedEndif => write!(f, "#endif without matching #if"),
PPError::MissingEndif => write!(f, "missing #endif"),
PPError::InvalidMacroArgs(s) => write!(f, "invalid macro arguments: {}", s),
PPError::IoError(p, e) => write!(f, "I/O error reading {}: {}", p.display(), e),
PPError::InvalidCondition(s) => write!(f, "invalid preprocessor condition: {}", s),
PPError::InvalidStringize => write!(f, "'#' is not followed by a macro parameter"),
PPError::InvalidTokenPaste => write!(f, "'##' cannot appear at boundary of macro expansion"),
PPError::RecursiveMacro(s) => write!(f, "recursive macro expansion: {}", s),
PPError::UnmatchedElse => write!(f, "#else without matching #if"),
PPError::ElifAfterElse => write!(f, "#elif after #else"),
}
}
}
#[derive(Debug)]
pub enum ParseError {
UnexpectedToken { expected: String, found: TokenKind },
UnexpectedEof,
InvalidDeclaration(String),
InvalidType(String),
InvalidAssertArgs { macro_name: String, arg_count: usize },
AssertNotFunctionMacro { macro_name: String },
NestedAssertNotSupported,
MacroEndNotFound,
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ParseError::UnexpectedToken { expected, found } => {
write!(f, "expected {}, found {:?}", expected, found)
}
ParseError::UnexpectedEof => write!(f, "unexpected end of file"),
ParseError::InvalidDeclaration(s) => write!(f, "invalid declaration: {}", s),
ParseError::InvalidType(s) => write!(f, "invalid type: {}", s),
ParseError::InvalidAssertArgs { macro_name, arg_count } => {
write!(f, "assert macro '{}' expects 1 argument, got {}", macro_name, arg_count)
}
ParseError::AssertNotFunctionMacro { macro_name } => {
write!(f, "assert macro '{}' must be called as function macro", macro_name)
}
ParseError::NestedAssertNotSupported => {
write!(f, "nested assert macros are not supported")
}
ParseError::MacroEndNotFound => {
write!(f, "matching MacroEnd not found")
}
}
}
}
#[derive(Debug)]
pub enum CompileError {
Lex { loc: SourceLocation, kind: LexError },
Preprocess { loc: SourceLocation, kind: PPError },
Parse { loc: SourceLocation, kind: ParseError },
}
impl fmt::Display for CompileError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CompileError::Lex { loc, kind } => {
write!(f, "{}:{}:{}: lexer error: {}", loc.file_id.as_u32(), loc.line, loc.column, kind)
}
CompileError::Preprocess { loc, kind } => {
write!(f, "{}:{}:{}: preprocessor error: {}", loc.file_id.as_u32(), loc.line, loc.column, kind)
}
CompileError::Parse { loc, kind } => {
write!(f, "{}:{}:{}: parse error: {}", loc.file_id.as_u32(), loc.line, loc.column, kind)
}
}
}
}
impl std::error::Error for CompileError {}
impl CompileError {
pub fn loc(&self) -> &SourceLocation {
match self {
CompileError::Lex { loc, .. } => loc,
CompileError::Preprocess { loc, .. } => loc,
CompileError::Parse { loc, .. } => loc,
}
}
pub fn format_with_files(&self, files: &FileRegistry) -> String {
match self {
CompileError::Lex { loc, kind } => {
let disp = DisplayLocation { loc, files };
format!("{}: lexer error: {}", disp, kind)
}
CompileError::Preprocess { loc, kind } => {
let disp = DisplayLocation { loc, files };
format!("{}: preprocessor error: {}", disp, kind)
}
CompileError::Parse { loc, kind } => {
let disp = DisplayLocation { loc, files };
format!("{}: parse error: {}", disp, kind)
}
}
}
pub fn with_files(self, files: &FileRegistry) -> EnrichedCompileError {
let loc = self.loc().clone();
let file_path = files.try_get_path(loc.file_id).map(|p| p.to_path_buf());
let source_line = file_path.as_deref().and_then(|p| read_source_line(p, loc.line));
EnrichedCompileError {
error: self,
file_path,
source_line,
}
}
}
fn read_source_line(path: &Path, line: u32) -> Option<String> {
use std::io::{BufRead, BufReader};
if line == 0 {
return None;
}
let f = std::fs::File::open(path).ok()?;
let reader = BufReader::new(f);
let mut iter = reader.lines();
let target = (line as usize).saturating_sub(1);
iter.nth(target).and_then(|r| r.ok())
}
#[derive(Debug)]
pub struct EnrichedCompileError {
pub error: CompileError,
pub file_path: Option<PathBuf>,
pub source_line: Option<String>,
}
impl EnrichedCompileError {
pub fn inner(&self) -> &CompileError {
&self.error
}
pub fn loc(&self) -> &SourceLocation {
self.error.loc()
}
}
impl fmt::Display for EnrichedCompileError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let loc = self.error.loc();
let (kind_label, kind_msg) = match &self.error {
CompileError::Lex { kind, .. } => ("lexer error", format!("{}", kind)),
CompileError::Preprocess { kind, .. } => ("preprocessor error", format!("{}", kind)),
CompileError::Parse { kind, .. } => ("parse error", format!("{}", kind)),
};
match &self.file_path {
Some(p) => write!(
f,
"{}:{}:{}: {}: {}",
p.display(), loc.line, loc.column, kind_label, kind_msg
)?,
None => write!(
f,
"<file_id={}>:{}:{}: {}: {}",
loc.file_id.as_u32(), loc.line, loc.column, kind_label, kind_msg
)?,
}
if let Some(line_text) = &self.source_line {
write!(f, "\n {:>4} | {}", loc.line, line_text)?;
let col = (loc.column as usize).saturating_sub(1);
write!(f, "\n | {}^", " ".repeat(col))?;
}
Ok(())
}
}
impl std::error::Error for EnrichedCompileError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.error)
}
}
pub type Result<T> = std::result::Result<T, CompileError>;
#[cfg(test)]
mod tests {
use super::*;
use crate::source::FileId;
#[test]
fn test_lex_error_display() {
let err = LexError::UnterminatedComment;
assert_eq!(format!("{}", err), "unterminated block comment");
}
#[test]
fn test_compile_error_display() {
let loc = SourceLocation::new(FileId::default(), 10, 5);
let err = CompileError::Lex {
loc,
kind: LexError::InvalidChar('$'),
};
assert!(format!("{}", err).contains("invalid character"));
}
}