Add annotate_snippets for better parsing errors (#477)

Adds nicer errors for Podlang code, using the `annotate_snippets` crate, the same crate used by the Rust compiler to generate contextual errors. This prints a short snippet of the code containing the error within the error message, highlighting the part that needs to be fixed.

It also includes a change to the `load_module` function, changing a `Vec` function argument to a slice.
This commit is contained in:
Rob Knight 2026-02-11 11:14:23 +01:00 committed by GitHub
parent acab26e5c1
commit 09d67de989
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 612 additions and 40 deletions

View file

@ -1,3 +1,5 @@
use std::{fmt, ops::Deref};
use thiserror::Error;
use crate::{
@ -7,7 +9,7 @@ use crate::{
};
#[derive(Error, Debug)]
pub enum LangError {
pub enum LangErrorKind {
#[error("Parsing failed: {0}")]
Parse(Box<ParseError>),
@ -27,6 +29,68 @@ pub enum LangError {
Batching(Box<BatchingError>),
}
/// Top-level error type for the Podlang pipeline.
///
/// When source text is attached (e.g. from [`crate::lang::parse`]), `Display` produces a rich,
/// source-annotated diagnostic. Otherwise it falls back to a plain message.
///
/// The inner [`LangErrorKind`] variants are accessible via `Deref`, so existing match patterns
/// like `LangError::Validation(e)` continue to work.
pub struct LangError {
pub kind: LangErrorKind,
source_text: Option<String>,
path: Option<String>,
}
impl LangError {
pub fn new(kind: LangErrorKind) -> Self {
Self {
kind,
source_text: None,
path: None,
}
}
/// Attach source text (and optional path) so that `Display` produces a rich diagnostic.
pub fn with_source(mut self, source: impl Into<String>, path: Option<String>) -> Self {
self.source_text = Some(source.into());
self.path = path;
self
}
}
impl fmt::Debug for LangError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
impl Deref for LangError {
type Target = LangErrorKind;
fn deref(&self) -> &LangErrorKind {
&self.kind
}
}
impl fmt::Display for LangError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.source_text {
Some(source) => {
let rendered =
crate::lang::diagnostics::render_error(source, self.path.as_deref(), self);
write!(f, "{}", rendered)
}
None => write!(f, "{}", self.kind),
}
}
}
impl std::error::Error for LangError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.kind.source()
}
}
/// Validation errors from frontend AST validation
#[derive(Debug, thiserror::Error)]
pub enum ValidationError {
@ -295,30 +359,30 @@ pub enum SplittingError {
impl From<ParseError> for LangError {
fn from(err: ParseError) -> Self {
LangError::Parse(Box::new(err))
LangError::new(LangErrorKind::Parse(Box::new(err)))
}
}
impl From<middleware::Error> for LangError {
fn from(err: middleware::Error) -> Self {
LangError::Middleware(Box::new(err))
LangError::new(LangErrorKind::Middleware(Box::new(err)))
}
}
impl From<ValidationError> for LangError {
fn from(err: ValidationError) -> Self {
LangError::Validation(Box::new(err))
LangError::new(LangErrorKind::Validation(Box::new(err)))
}
}
impl From<LoweringError> for LangError {
fn from(err: LoweringError) -> Self {
LangError::Lowering(Box::new(err))
LangError::new(LangErrorKind::Lowering(Box::new(err)))
}
}
impl From<BatchingError> for LangError {
fn from(err: BatchingError) -> Self {
LangError::Batching(Box::new(err))
LangError::new(LangErrorKind::Batching(Box::new(err)))
}
}