pod2/src/lang/error.rs
2026-05-06 14:24:00 +01:00

311 lines
8.9 KiB
Rust

use std::{fmt, ops::Deref};
use thiserror::Error;
use crate::{
frontend,
lang::{frontend_ast::Span, parser::ParseError},
middleware,
};
#[derive(Error, Debug)]
pub enum LangErrorKind {
#[error("Parsing failed: {0}")]
Parse(Box<ParseError>),
#[error("Middleware error during processing: {0}")]
Middleware(Box<middleware::Error>),
#[error("Frontend error: {0}")]
Frontend(Box<frontend::Error>),
#[error("Validation error: {0}")]
Validation(Box<ValidationError>),
#[error("Lowering error: {0}")]
Lowering(Box<LoweringError>),
#[error("Batching error: {0}")]
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 {
#[error("Invalid hash: {hash}")]
InvalidHash { hash: String, span: Option<Span> },
#[error("Duplicate predicate definition: {name}")]
DuplicatePredicate {
name: String,
first_span: Option<Span>,
second_span: Option<Span>,
},
#[error("Duplicate import name: {name}")]
DuplicateImport { name: String, span: Option<Span> },
#[error("Import arity mismatch: expected {expected} predicates, found {found}")]
ImportArityMismatch {
expected: usize,
found: usize,
span: Option<Span>,
},
#[error("Module not found: {name}")]
ModuleNotFound { name: String, span: Option<Span> },
#[error("Undefined predicate: {name}")]
UndefinedPredicate { name: String, span: Option<Span> },
#[error("Undefined wildcard: {name} in predicate {pred_name}")]
UndefinedWildcard {
name: String,
pred_name: String,
span: Option<Span>,
},
#[error("Argument count mismatch for {predicate}: expected {expected}, found {found}")]
ArgumentCountMismatch {
predicate: String,
expected: usize,
found: usize,
span: Option<Span>,
},
#[error("Duplicate wildcard in predicate arguments: {name}")]
DuplicateWildcard { name: String, span: Option<Span> },
#[error("Empty statement list in {context}")]
EmptyStatementList { context: String, span: Option<Span> },
#[error("Multiple REQUEST definitions found. Only one is allowed.")]
MultipleRequestDefinitions {
first_span: Option<Span>,
second_span: Option<Span>,
},
#[error("Wildcard '{name}' collides with a predicate name")]
WildcardPredicateNameCollision { name: String },
#[error("Predicate definitions are not allowed in requests")]
PredicatesNotAllowedInRequest { span: Option<Span> },
#[error("REQUEST block is not allowed in modules")]
RequestNotAllowedInModule { span: Option<Span> },
#[error("Modules must contain at least one predicate definition")]
NoPredicatesInModule,
#[error("Self-referential predicate literal not allowed in requests")]
SelfReferentialPredicateLiteralNotAllowedInRequests { span: Option<Span> },
#[error("Requests must contain a REQUEST block")]
NoRequestBlock,
#[error("Duplicate record definition: {name}")]
DuplicateRecord {
name: String,
first_span: Option<Span>,
second_span: Option<Span>,
},
#[error("Record '{name}' has {count} entries, exceeding the limit of {max}")]
RecordTooManyEntries {
name: String,
count: usize,
max: usize,
span: Option<Span>,
},
#[error("Duplicate entry name '{entry}' in record '{record}'")]
DuplicateRecordEntry {
record: String,
entry: String,
span: Option<Span>,
},
#[error("Unknown record type: {name}")]
UnknownRecord { name: String, span: Option<Span> },
#[error("Record '{record}' has no entry '{entry}'")]
UnknownRecordEntry {
record: String,
entry: String,
span: Option<Span>,
},
#[error("Duplicate entry '{entry}' in record literal '{record}'")]
DuplicateLiteralRecordEntry {
record: String,
entry: String,
span: Option<Span>,
},
#[error("Bracket access '{wildcard}[...]' is not allowed on a wildcard typed as record '{record}'; use `{wildcard}.entry` instead")]
BracketAccessOnTypedWildcard {
wildcard: String,
record: String,
span: Option<Span>,
},
}
/// Lowering errors from frontend AST lowering to middleware
#[derive(Debug, thiserror::Error)]
pub enum LoweringError {
#[error("Too many statements in predicate '{predicate}': {count} exceeds limit of {max}")]
TooManyStatements {
predicate: String,
count: usize,
max: usize,
},
#[error("Too many wildcards in predicate '{predicate}': {count} exceeds limit of {max}")]
TooManyWildcards {
predicate: String,
count: usize,
max: usize,
},
#[error("Too many arguments in statement template: {count} exceeds limit of {max}")]
TooManyStatementArgs { count: usize, max: usize },
#[error("Predicate '{name}' not found in symbol table")]
PredicateNotFound { name: String },
#[error("Invalid argument type in statement template")]
InvalidArgumentType,
#[error("Middleware error: {0}")]
Middleware(#[from] middleware::Error),
#[error("Splitting error: {0}")]
Splitting(#[from] SplittingError),
#[error("Batching error: {0}")]
Batching(#[from] BatchingError),
#[error("Cannot lower document with validation errors")]
ValidationErrors,
}
/// Batching errors from multi-batch packing
#[derive(Debug, thiserror::Error)]
pub enum BatchingError {
#[error("Internal batching error: {message}")]
Internal { message: String },
}
/// Splitting errors from predicate splitting
#[derive(Debug, thiserror::Error)]
pub enum SplittingError {
#[error("Too many public arguments in predicate '{predicate}': {count} exceeds max of {max_allowed}. {message}")]
TooManyPublicArgs {
predicate: String,
count: usize,
max_allowed: usize,
message: String,
},
#[error("Could not split predicate '{predicate}' into a chain: no feasible partition exists with up to {max_links} links. \
The predicate's wildcard structure may be too dense for any chain to fit within max_statement_args ({max_statement_args}) \
and max_custom_predicate_wildcards ({max_wildcards}) per link.")]
Infeasible {
predicate: String,
max_links: usize,
max_statement_args: usize,
max_wildcards: usize,
},
}
impl From<ParseError> for LangError {
fn from(err: ParseError) -> Self {
LangError::new(LangErrorKind::Parse(Box::new(err)))
}
}
impl From<middleware::Error> for LangError {
fn from(err: middleware::Error) -> Self {
LangError::new(LangErrorKind::Middleware(Box::new(err)))
}
}
impl From<ValidationError> for LangError {
fn from(err: ValidationError) -> Self {
LangError::new(LangErrorKind::Validation(Box::new(err)))
}
}
impl From<LoweringError> for LangError {
fn from(err: LoweringError) -> Self {
LangError::new(LangErrorKind::Lowering(Box::new(err)))
}
}
impl From<BatchingError> for LangError {
fn from(err: BatchingError) -> Self {
LangError::new(LangErrorKind::Batching(Box::new(err)))
}
}