Add slightly more readable error messages in the compiler

Previously, the CompileError error messages were just `Debug::fmt`
written to stdout and there wasn't really a backtrace in the code
included. Now, when there is an error in an imported file, it will
display a backtrace of the files included that caused this error.

These are not perfect error messages and are a bit rough around the
edges but they are good enough for now.

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
This commit is contained in:
2024-10-06 20:26:14 -07:00
parent b21a02f12f
commit 8179611c23
2 changed files with 95 additions and 43 deletions

View File

@@ -70,17 +70,44 @@ impl Scope {
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub struct CompileError { pub enum CompileError {
pub line: Option<LineRange>, Error {
pub message: String, line: Option<LineRange>,
source_path: String,
message: String,
},
Import {
error: Box<dyn std::error::Error>,
line: LineRange,
source_path: String,
dest_path: String,
},
} }
impl Display for CompileError { impl Display for CompileError {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
if let Some(line) = &self.line { match self {
write!(fmt, "line {:?}: {}", line, self.message) CompileError::Error {
} else { line: Some((line, _)),
write!(fmt, "{}", self.message) source_path,
message,
} => write!(fmt, "{source_path}: line {line}: {message}"),
CompileError::Error {
line: None,
source_path,
message,
} => write!(fmt, "{source_path}: {}", message),
CompileError::Import {
error,
line: (line, _),
source_path,
dest_path,
} => {
write!(
fmt,
"error in {dest_path} (included from {source_path} on line {line}:\n\t{error}",
)
}
} }
} }
} }
@@ -138,8 +165,9 @@ impl<'c> Compiler<'c> {
pub fn compile_path(self, path: impl AsRef<Path>) -> Result<Ptr<Module>> { pub fn compile_path(self, path: impl AsRef<Path>) -> Result<Ptr<Module>> {
let path_str = &path.as_ref().as_os_str().to_str().unwrap(); let path_str = &path.as_ref().as_os_str().to_str().unwrap();
let mut file = File::open(path.as_ref()).map_err(|e| CompileError { let mut file = File::open(path.as_ref()).map_err(|e| CompileError::Error {
line: None, line: None,
source_path: self.path.display().to_string(),
message: format!("could not open {}: {}", path.as_ref().display(), e), message: format!("could not open {}: {}", path.as_ref().display(), e),
})?; })?;
let mut contents = String::new(); let mut contents = String::new();
@@ -149,8 +177,9 @@ impl<'c> Compiler<'c> {
let ast = parser.parse_all()?; let ast = parser.parse_all()?;
if parser.was_error() { if parser.was_error() {
return Err(CompileError { return Err(CompileError::Error {
line: None, line: None,
source_path: self.path.display().to_string(),
message: format!("error in '{}'", path.as_ref().display()), message: format!("error in '{}'", path.as_ref().display()),
} }
.into()); .into());
@@ -217,8 +246,9 @@ impl<'c> Compiler<'c> {
let index = self.constants.len(); let index = self.constants.len();
if index > (ConstantId::MAX as usize) { if index > (ConstantId::MAX as usize) {
return Err(CompileError { return Err(CompileError::Error {
line: None, line: None,
source_path: self.path.display().to_string(),
message: format!("too many constants (maximum {})", ConstantId::MAX), message: format!("too many constants (maximum {})", ConstantId::MAX),
} }
.into()); .into());
@@ -243,8 +273,9 @@ impl<'c> Compiler<'c> {
let index = self.globals.len(); let index = self.globals.len();
if index > (GlobalId::MAX as usize) { if index > (GlobalId::MAX as usize) {
return Err(CompileError { return Err(CompileError::Error {
line: None, line: None,
source_path: self.path.display().to_string(),
message: format!("too many globals (maximum {})", GlobalId::MAX), message: format!("too many globals (maximum {})", GlobalId::MAX),
} }
.into()); .into());
@@ -302,8 +333,9 @@ impl<'c> Compiler<'c> {
fn insert_local(&mut self, name: String) -> Result<&Local> { fn insert_local(&mut self, name: String) -> Result<&Local> {
let index = self.chunk().locals.len(); let index = self.chunk().locals.len();
if index > (LocalIndex::MAX as usize) { if index > (LocalIndex::MAX as usize) {
return Err(CompileError { return Err(CompileError::Error {
line: None, line: None,
source_path: self.path.display().to_string(),
message: format!("too many locals (maximum: {})", LocalIndex::MAX), message: format!("too many locals (maximum: {})", LocalIndex::MAX),
} }
.into()); .into());
@@ -327,8 +359,9 @@ impl<'c> Compiler<'c> {
// get the last allocated slot and increment by one // get the last allocated slot and increment by one
let last = &scope.scope.last().unwrap(); let last = &scope.scope.last().unwrap();
if last.slot == LocalSlot::MAX { if last.slot == LocalSlot::MAX {
return Err(CompileError { return Err(CompileError::Error {
line: None, line: None,
source_path: self.path.display().to_string(),
message: format!( message: format!(
"too many stack slots used by locals(maximum: {})", "too many stack slots used by locals(maximum: {})",
LocalSlot::MAX LocalSlot::MAX
@@ -444,9 +477,11 @@ impl StmtVisitor for Compiler<'_> {
// also into the modules cache // also into the modules cache
let module = Compiler::new(path.clone(), self.constants, self.imported) let module = Compiler::new(path.clone(), self.constants, self.imported)
.compile_path(&path) .compile_path(&path)
.map_err(|e| CompileError { .map_err(|error| CompileError::Import {
line: Some(line), error,
message: format!("while importing module '{}': {}", stmt.module.text, e), line,
source_path: self.path.display().to_string(),
dest_path: path_str.to_string(),
})?; })?;
self.imported.insert(path_str.to_string(), module.clone()); self.imported.insert(path_str.to_string(), module.clone());
module module
@@ -684,8 +719,9 @@ impl ExprVisitor for Compiler<'_> {
self.compile_expr(arg)?; self.compile_expr(arg)?;
} }
if expr.args.len() > (Argc::MAX as usize) { if expr.args.len() > (Argc::MAX as usize) {
return Err(CompileError { return Err(CompileError::Error {
line: Some(expr_line_number(expr)), line: Some(expr_line_number(expr)),
source_path: self.path.display().to_string(),
message: format!("too many function arguments (maximum: {})", Argc::MAX), message: format!("too many function arguments (maximum: {})", Argc::MAX),
} }
.into()); .into());
@@ -718,8 +754,9 @@ impl ExprVisitor for Compiler<'_> {
if let Some(local) = self.get_local(name) { if let Some(local) = self.get_local(name) {
self.emit(expr_line_number(expr), Op::GetLocal(local.index)); self.emit(expr_line_number(expr), Op::GetLocal(local.index));
} else { } else {
let global = self.get_global(name).ok_or_else(|| CompileError { let global = self.get_global(name).ok_or_else(|| CompileError::Error {
line: Some(expr_line_number(expr)), line: Some(expr_line_number(expr)),
source_path: self.path.display().to_string(),
message: if self.is_global_scope() { message: if self.is_global_scope() {
format!("unknown global {}", name) format!("unknown global {}", name)
} else { } else {

View File

@@ -22,7 +22,11 @@ struct Args {
path: PathBuf, path: PathBuf,
} }
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() {
// While main() is allowed to return an error, it will always do `Debug::fmt` on the output wrather
// than `Display::fmt`. We want to display pretty errors, and the easiest way to do this (in my
// experience) is to just wrap it and output the error.
fn main_wrapper() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse(); let args = Args::parse();
// initialize type system // initialize type system
@@ -56,3 +60,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(()) Ok(())
} }
let result = main_wrapper();
match result {
Ok(()) => { /* return 0 OK */ }
Err(e) => {
println!("{}", e);
std::process::exit(1)
}
}
}