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:
parent
acab26e5c1
commit
09d67de989
11 changed files with 612 additions and 40 deletions
|
|
@ -42,6 +42,7 @@ serde_arrays = "0.2.0"
|
||||||
sha2 = { version = "0.10.9" }
|
sha2 = { version = "0.10.9" }
|
||||||
rand_chacha = "0.3.1"
|
rand_chacha = "0.3.1"
|
||||||
good_lp = { version = "1.8", default-features = false, features = ["microlp"] }
|
good_lp = { version = "1.8", default-features = false, features = ["microlp"] }
|
||||||
|
annotate-snippets = "0.11"
|
||||||
|
|
||||||
# Uncomment for debugging with https://github.com/ed255/plonky2/ at branch `feat/debug`. The repo directory needs to be checked out next to the pod2 repo directory.
|
# Uncomment for debugging with https://github.com/ed255/plonky2/ at branch `feat/debug`. The repo directory needs to be checked out next to the pod2 repo directory.
|
||||||
# [patch."https://github.com/0xPARC/plonky2"]
|
# [patch."https://github.com/0xPARC/plonky2"]
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
game_pk = game_pk,
|
game_pk = game_pk,
|
||||||
);
|
);
|
||||||
println!("# custom predicate batch:{}", input);
|
println!("# custom predicate batch:{}", input);
|
||||||
let module = load_module(&input, "points_module", ¶ms, vec![])?;
|
let module = load_module(&input, "points_module", ¶ms, &[])?;
|
||||||
let batch = module.batch.clone();
|
let batch = module.batch.clone();
|
||||||
let points_pred = batch.predicate_ref_by_name("points").unwrap();
|
let points_pred = batch.predicate_ref_by_name("points").unwrap();
|
||||||
let over_9000_pred = batch.predicate_ref_by_name("over_9000").unwrap();
|
let over_9000_pred = batch.predicate_ref_by_name("over_9000").unwrap();
|
||||||
|
|
|
||||||
|
|
@ -1175,7 +1175,7 @@ pub mod tests {
|
||||||
"#,
|
"#,
|
||||||
"test",
|
"test",
|
||||||
¶ms,
|
¶ms,
|
||||||
vec![],
|
&[],
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let batch = module.batch.clone();
|
let batch = module.batch.clone();
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ pub fn eth_dos_batch(params: &Params) -> Result<Arc<CustomPredicateBatch>> {
|
||||||
eth_dos_ind(src, dst, distance)
|
eth_dos_ind(src, dst, distance)
|
||||||
)
|
)
|
||||||
"#;
|
"#;
|
||||||
let module = load_module(input, "eth_dos", params, vec![]).expect("lang parse");
|
let module = load_module(input, "eth_dos", params, &[]).expect("lang parse");
|
||||||
let batch = module.batch.clone();
|
let batch = module.batch.clone();
|
||||||
println!("a.0. {}", batch.predicates()[0]);
|
println!("a.0. {}", batch.predicates()[0]);
|
||||||
println!("a.1. {}", batch.predicates()[1]);
|
println!("a.1. {}", batch.predicates()[1]);
|
||||||
|
|
|
||||||
|
|
@ -1381,7 +1381,7 @@ pub mod tests {
|
||||||
Equal(b, 5)
|
Equal(b, 5)
|
||||||
)
|
)
|
||||||
"#;
|
"#;
|
||||||
let module = load_module(input, "test", ¶ms, vec![]).unwrap();
|
let module = load_module(input, "test", ¶ms, &[]).unwrap();
|
||||||
let batch = module.batch.clone();
|
let batch = module.batch.clone();
|
||||||
let pred_test = batch.predicate_ref_by_name("Test").unwrap();
|
let pred_test = batch.predicate_ref_by_name("Test").unwrap();
|
||||||
|
|
||||||
|
|
@ -1430,7 +1430,7 @@ pub mod tests {
|
||||||
c(6, 3)
|
c(6, 3)
|
||||||
)
|
)
|
||||||
"#;
|
"#;
|
||||||
let module = load_module(input, "test", ¶ms, vec![]).unwrap();
|
let module = load_module(input, "test", ¶ms, &[]).unwrap();
|
||||||
let batch = module.batch.clone();
|
let batch = module.batch.clone();
|
||||||
let pred_test = batch.predicate_ref_by_name("Test").unwrap();
|
let pred_test = batch.predicate_ref_by_name("Test").unwrap();
|
||||||
|
|
||||||
|
|
@ -1452,7 +1452,7 @@ pub mod tests {
|
||||||
c(6, 3)
|
c(6, 3)
|
||||||
)
|
)
|
||||||
"#;
|
"#;
|
||||||
let module = load_module(input, "test", ¶ms, vec![]).unwrap();
|
let module = load_module(input, "test", ¶ms, &[]).unwrap();
|
||||||
let batch = module.batch.clone();
|
let batch = module.batch.clone();
|
||||||
let pred_test = batch.predicate_ref_by_name("Test").unwrap();
|
let pred_test = batch.predicate_ref_by_name("Test").unwrap();
|
||||||
|
|
||||||
|
|
@ -1491,7 +1491,7 @@ pub mod tests {
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
// Parse and batch the predicate (this handles splitting internally)
|
// Parse and batch the predicate (this handles splitting internally)
|
||||||
let module = load_module(input, "test", ¶ms, vec![])?;
|
let module = load_module(input, "test", ¶ms, &[])?;
|
||||||
|
|
||||||
// Verify it was split
|
// Verify it was split
|
||||||
assert!(module.split_chains.contains_key("large_pred"));
|
assert!(module.split_chains.contains_key("large_pred"));
|
||||||
|
|
|
||||||
|
|
@ -763,7 +763,7 @@ mod tests {
|
||||||
"#,
|
"#,
|
||||||
"test",
|
"test",
|
||||||
¶ms,
|
¶ms,
|
||||||
vec![],
|
&[],
|
||||||
)
|
)
|
||||||
.expect("load module");
|
.expect("load module");
|
||||||
let batch = &module.batch;
|
let batch = &module.batch;
|
||||||
|
|
@ -1492,7 +1492,7 @@ mod tests {
|
||||||
"#,
|
"#,
|
||||||
"test",
|
"test",
|
||||||
¶ms,
|
¶ms,
|
||||||
vec![],
|
&[],
|
||||||
)
|
)
|
||||||
.expect("load module");
|
.expect("load module");
|
||||||
let batch = &module.batch;
|
let batch = &module.batch;
|
||||||
|
|
@ -1621,7 +1621,7 @@ mod tests {
|
||||||
"#,
|
"#,
|
||||||
"test",
|
"test",
|
||||||
¶ms,
|
¶ms,
|
||||||
vec![],
|
&[],
|
||||||
)
|
)
|
||||||
.expect("load module");
|
.expect("load module");
|
||||||
let batch = &module.batch;
|
let batch = &module.batch;
|
||||||
|
|
|
||||||
486
src/lang/diagnostics.rs
Normal file
486
src/lang/diagnostics.rs
Normal file
|
|
@ -0,0 +1,486 @@
|
||||||
|
//! Rich error rendering for Podlang diagnostics.
|
||||||
|
//!
|
||||||
|
//! Provides [`render_error`], which takes a source string and a [`LangError`] and produces
|
||||||
|
//! a human-readable, source-annotated error message (similar to `rustc` output).
|
||||||
|
|
||||||
|
use annotate_snippets::{Level, Renderer, Snippet};
|
||||||
|
|
||||||
|
use crate::lang::{
|
||||||
|
error::{LangError, LangErrorKind, ValidationError},
|
||||||
|
frontend_ast::Span,
|
||||||
|
parser::ParseError,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Render a [`LangError`] with source context into a human-readable diagnostic string.
|
||||||
|
///
|
||||||
|
/// Uses `Renderer::plain()` (no ANSI codes) so the output is stable for tests and log-safe.
|
||||||
|
///
|
||||||
|
/// - `source`: the full Podlang source text that was parsed.
|
||||||
|
/// - `path`: optional file path for the `-->` origin line.
|
||||||
|
/// - `error`: the error to render.
|
||||||
|
pub fn render_error(source: &str, path: Option<&str>, error: &LangError) -> String {
|
||||||
|
let renderer = Renderer::plain();
|
||||||
|
match &error.kind {
|
||||||
|
LangErrorKind::Validation(e) => render_validation_error(&renderer, source, path, e),
|
||||||
|
LangErrorKind::Parse(e) => render_parse_error(&renderer, source, path, e),
|
||||||
|
LangErrorKind::Lowering(_)
|
||||||
|
| LangErrorKind::Batching(_)
|
||||||
|
| LangErrorKind::Middleware(_)
|
||||||
|
| LangErrorKind::Frontend(_) => render_title_only(&renderer, &error.kind.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render an error with only a title line and no source snippet.
|
||||||
|
fn render_title_only(renderer: &Renderer, message: &str) -> String {
|
||||||
|
let msg = Level::Error.title(message);
|
||||||
|
renderer.render(msg).to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render an error with a single annotated span in the source.
|
||||||
|
fn render_with_span(
|
||||||
|
renderer: &Renderer,
|
||||||
|
source: &str,
|
||||||
|
path: Option<&str>,
|
||||||
|
title: &str,
|
||||||
|
span: &Span,
|
||||||
|
label: &str,
|
||||||
|
) -> String {
|
||||||
|
let annotation = Level::Error.span(span.start..span.end).label(label);
|
||||||
|
let snippet = build_snippet(source, path).annotation(annotation);
|
||||||
|
let msg = Level::Error.title(title).snippet(snippet);
|
||||||
|
renderer.render(msg).to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render an error with an optional span — delegates to `render_with_span` or `render_title_only`.
|
||||||
|
fn render_with_optional_span(
|
||||||
|
renderer: &Renderer,
|
||||||
|
source: &str,
|
||||||
|
path: Option<&str>,
|
||||||
|
title: &str,
|
||||||
|
span: Option<&Span>,
|
||||||
|
label: &str,
|
||||||
|
) -> String {
|
||||||
|
match span {
|
||||||
|
Some(span) => render_with_span(renderer, source, path, title, span, label),
|
||||||
|
None => render_title_only(renderer, title),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render an error with two annotated spans (e.g. first definition + duplicate).
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn render_dual_span(
|
||||||
|
renderer: &Renderer,
|
||||||
|
source: &str,
|
||||||
|
path: Option<&str>,
|
||||||
|
title: &str,
|
||||||
|
first_span: Option<&Span>,
|
||||||
|
first_label: &str,
|
||||||
|
second_span: Option<&Span>,
|
||||||
|
second_label: &str,
|
||||||
|
) -> String {
|
||||||
|
let mut snippet = build_snippet(source, path);
|
||||||
|
if let Some(s) = first_span {
|
||||||
|
snippet = snippet.annotation(Level::Info.span(s.start..s.end).label(first_label));
|
||||||
|
}
|
||||||
|
if let Some(s) = second_span {
|
||||||
|
snippet = snippet.annotation(Level::Error.span(s.start..s.end).label(second_label));
|
||||||
|
}
|
||||||
|
let msg = Level::Error.title(title).snippet(snippet);
|
||||||
|
renderer.render(msg).to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a `Snippet` with source and optional path, ready for annotations.
|
||||||
|
fn build_snippet<'a>(source: &'a str, path: Option<&'a str>) -> Snippet<'a> {
|
||||||
|
let mut snippet = Snippet::source(source).fold(true);
|
||||||
|
if let Some(p) = path {
|
||||||
|
snippet = snippet.origin(p);
|
||||||
|
}
|
||||||
|
snippet
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Validation errors
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn render_validation_error(
|
||||||
|
renderer: &Renderer,
|
||||||
|
source: &str,
|
||||||
|
path: Option<&str>,
|
||||||
|
error: &ValidationError,
|
||||||
|
) -> String {
|
||||||
|
match error {
|
||||||
|
ValidationError::UndefinedWildcard {
|
||||||
|
name,
|
||||||
|
pred_name,
|
||||||
|
span,
|
||||||
|
} => {
|
||||||
|
let title = format!("undefined wildcard `{}`", name);
|
||||||
|
let label = format!("not declared in predicate `{}`", pred_name);
|
||||||
|
render_with_optional_span(renderer, source, path, &title, span.as_ref(), &label)
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidationError::UndefinedPredicate { name, span } => {
|
||||||
|
let title = format!("undefined predicate: {}", name);
|
||||||
|
render_with_optional_span(
|
||||||
|
renderer,
|
||||||
|
source,
|
||||||
|
path,
|
||||||
|
&title,
|
||||||
|
span.as_ref(),
|
||||||
|
"not defined or imported",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidationError::DuplicatePredicate {
|
||||||
|
name,
|
||||||
|
first_span,
|
||||||
|
second_span,
|
||||||
|
} => {
|
||||||
|
let title = format!("duplicate predicate definition: {}", name);
|
||||||
|
render_dual_span(
|
||||||
|
renderer,
|
||||||
|
source,
|
||||||
|
path,
|
||||||
|
&title,
|
||||||
|
first_span.as_ref(),
|
||||||
|
"first definition here",
|
||||||
|
second_span.as_ref(),
|
||||||
|
"duplicate definition",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidationError::ArgumentCountMismatch {
|
||||||
|
predicate,
|
||||||
|
expected,
|
||||||
|
found,
|
||||||
|
span,
|
||||||
|
} => {
|
||||||
|
let title = format!("argument count mismatch for `{}`", predicate);
|
||||||
|
let label = format!("expected {} arguments, found {}", expected, found);
|
||||||
|
render_with_optional_span(renderer, source, path, &title, span.as_ref(), &label)
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidationError::MultipleRequestDefinitions {
|
||||||
|
first_span,
|
||||||
|
second_span,
|
||||||
|
} => render_dual_span(
|
||||||
|
renderer,
|
||||||
|
source,
|
||||||
|
path,
|
||||||
|
"multiple REQUEST definitions found",
|
||||||
|
first_span.as_ref(),
|
||||||
|
"first REQUEST here",
|
||||||
|
second_span.as_ref(),
|
||||||
|
"second REQUEST here",
|
||||||
|
),
|
||||||
|
|
||||||
|
ValidationError::InvalidArgumentType { predicate, span } => {
|
||||||
|
let title = format!("invalid argument type for `{}`", predicate);
|
||||||
|
render_with_optional_span(
|
||||||
|
renderer,
|
||||||
|
source,
|
||||||
|
path,
|
||||||
|
&title,
|
||||||
|
span.as_ref(),
|
||||||
|
"anchored keys not allowed here",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidationError::DuplicateWildcard { name, span } => {
|
||||||
|
let title = format!("duplicate wildcard: {}", name);
|
||||||
|
render_with_optional_span(
|
||||||
|
renderer,
|
||||||
|
source,
|
||||||
|
path,
|
||||||
|
&title,
|
||||||
|
span.as_ref(),
|
||||||
|
"already declared",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidationError::EmptyStatementList { context, span } => {
|
||||||
|
let title = format!("empty statement list in {}", context);
|
||||||
|
render_with_optional_span(
|
||||||
|
renderer,
|
||||||
|
source,
|
||||||
|
path,
|
||||||
|
&title,
|
||||||
|
span.as_ref(),
|
||||||
|
"no statements",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidationError::InvalidHash { hash, span } => {
|
||||||
|
let title = format!("invalid hash: {}", hash);
|
||||||
|
render_with_optional_span(
|
||||||
|
renderer,
|
||||||
|
source,
|
||||||
|
path,
|
||||||
|
&title,
|
||||||
|
span.as_ref(),
|
||||||
|
"invalid hash",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidationError::DuplicateImport { name, span } => {
|
||||||
|
let title = format!("duplicate import name: {}", name);
|
||||||
|
render_with_optional_span(
|
||||||
|
renderer,
|
||||||
|
source,
|
||||||
|
path,
|
||||||
|
&title,
|
||||||
|
span.as_ref(),
|
||||||
|
"already imported",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidationError::ImportArityMismatch {
|
||||||
|
expected,
|
||||||
|
found,
|
||||||
|
span,
|
||||||
|
} => {
|
||||||
|
let title = "import arity mismatch".to_string();
|
||||||
|
let label = format!("expected {}, found {}", expected, found);
|
||||||
|
render_with_optional_span(renderer, source, path, &title, span.as_ref(), &label)
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidationError::ModuleNotFound { name, span } => {
|
||||||
|
let title = format!("module not found: {}", name);
|
||||||
|
render_with_optional_span(
|
||||||
|
renderer,
|
||||||
|
source,
|
||||||
|
path,
|
||||||
|
&title,
|
||||||
|
span.as_ref(),
|
||||||
|
"no module with this hash was provided",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidationError::WildcardPredicateNameCollision { name } => {
|
||||||
|
let title = format!("wildcard '{}' collides with a predicate name", name);
|
||||||
|
render_title_only(renderer, &title)
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidationError::PredicatesNotAllowedInRequest { span } => render_with_optional_span(
|
||||||
|
renderer,
|
||||||
|
source,
|
||||||
|
path,
|
||||||
|
"predicate definitions are not allowed in requests",
|
||||||
|
span.as_ref(),
|
||||||
|
"not allowed here",
|
||||||
|
),
|
||||||
|
|
||||||
|
ValidationError::RequestNotAllowedInModule { span } => render_with_optional_span(
|
||||||
|
renderer,
|
||||||
|
source,
|
||||||
|
path,
|
||||||
|
"REQUEST block is not allowed in modules",
|
||||||
|
span.as_ref(),
|
||||||
|
"not allowed here",
|
||||||
|
),
|
||||||
|
|
||||||
|
ValidationError::NoPredicatesInModule => render_title_only(
|
||||||
|
renderer,
|
||||||
|
"modules must contain at least one predicate definition",
|
||||||
|
),
|
||||||
|
|
||||||
|
ValidationError::NoRequestBlock => {
|
||||||
|
render_title_only(renderer, "requests must contain a REQUEST block")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Parse errors
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn render_parse_error(
|
||||||
|
renderer: &Renderer,
|
||||||
|
source: &str,
|
||||||
|
path: Option<&str>,
|
||||||
|
error: &ParseError,
|
||||||
|
) -> String {
|
||||||
|
match error {
|
||||||
|
ParseError::Pest(pest_err) => {
|
||||||
|
let span = span_from_pest_location(&pest_err.location);
|
||||||
|
let label = format_pest_label(pest_err);
|
||||||
|
render_with_span(renderer, source, path, "syntax error", &span, &label)
|
||||||
|
}
|
||||||
|
_ => render_title_only(renderer, &error.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract a byte-offset [`Span`] from a pest `InputLocation`.
|
||||||
|
fn span_from_pest_location(location: &pest::error::InputLocation) -> Span {
|
||||||
|
match *location {
|
||||||
|
pest::error::InputLocation::Pos(pos) => Span {
|
||||||
|
start: pos,
|
||||||
|
end: pos + 1,
|
||||||
|
},
|
||||||
|
pest::error::InputLocation::Span((start, end)) => Span { start, end },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format the expectations from a pest error into a readable label.
|
||||||
|
fn format_pest_label<R: std::fmt::Debug>(error: &pest::error::Error<R>) -> String {
|
||||||
|
match &error.variant {
|
||||||
|
pest::error::ErrorVariant::ParsingError {
|
||||||
|
positives,
|
||||||
|
negatives,
|
||||||
|
} => {
|
||||||
|
let mut parts = Vec::new();
|
||||||
|
if !positives.is_empty() {
|
||||||
|
let names: Vec<String> = positives.iter().map(|r| format!("{:?}", r)).collect();
|
||||||
|
parts.push(format!("expected {}", names.join(", ")));
|
||||||
|
}
|
||||||
|
if !negatives.is_empty() {
|
||||||
|
let names: Vec<String> = negatives.iter().map(|r| format!("{:?}", r)).collect();
|
||||||
|
parts.push(format!("unexpected {}", names.join(", ")));
|
||||||
|
}
|
||||||
|
if parts.is_empty() {
|
||||||
|
"unexpected input".to_string()
|
||||||
|
} else {
|
||||||
|
parts.join("; ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pest::error::ErrorVariant::CustomError { message } => message.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::lang::error::BatchingError;
|
||||||
|
|
||||||
|
/// Load a module from source and extract the error, or panic.
|
||||||
|
fn module_err(source: &str) -> LangError {
|
||||||
|
crate::lang::load_module(source, "test", &crate::middleware::Params::default(), &[])
|
||||||
|
.unwrap_err()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a request from source and extract the error, or panic.
|
||||||
|
fn request_err(source: &str) -> LangError {
|
||||||
|
crate::lang::parse_request(source, &crate::middleware::Params::default(), &[]).unwrap_err()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_undefined_wildcard() {
|
||||||
|
let source = r#"
|
||||||
|
my_pred(A, private: B) = AND(
|
||||||
|
Equal(A["key"], C["other"])
|
||||||
|
)
|
||||||
|
"#;
|
||||||
|
let err = module_err(source);
|
||||||
|
let rendered = render_error(source, Some("test.podlang"), &err);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
rendered.contains("undefined wildcard"),
|
||||||
|
"rendered: {rendered}"
|
||||||
|
);
|
||||||
|
assert!(rendered.contains("C"), "rendered: {rendered}");
|
||||||
|
assert!(rendered.contains("my_pred"), "rendered: {rendered}");
|
||||||
|
assert!(rendered.contains("test.podlang"), "rendered: {rendered}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_undefined_predicate() {
|
||||||
|
let source = r#"
|
||||||
|
REQUEST(
|
||||||
|
NoSuchPred(A, B)
|
||||||
|
)
|
||||||
|
"#;
|
||||||
|
let err = request_err(source);
|
||||||
|
let rendered = render_error(source, Some("test.podlang"), &err);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
rendered.contains("undefined predicate"),
|
||||||
|
"rendered: {rendered}"
|
||||||
|
);
|
||||||
|
assert!(rendered.contains("NoSuchPred"), "rendered: {rendered}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_duplicate_predicate() {
|
||||||
|
let source = r#"
|
||||||
|
my_pred(A) = AND(Equal(A["x"], 1))
|
||||||
|
my_pred(B) = AND(Equal(B["y"], 2))
|
||||||
|
"#;
|
||||||
|
let err = module_err(source);
|
||||||
|
let rendered = render_error(source, Some("test.podlang"), &err);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
rendered.contains("duplicate predicate"),
|
||||||
|
"rendered: {rendered}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
rendered.contains("first definition"),
|
||||||
|
"rendered: {rendered}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
rendered.contains("duplicate definition"),
|
||||||
|
"rendered: {rendered}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_argument_count_mismatch() {
|
||||||
|
let source = r#"
|
||||||
|
REQUEST(
|
||||||
|
Equal(A["x"], B["y"], C["z"])
|
||||||
|
)
|
||||||
|
"#;
|
||||||
|
let err = request_err(source);
|
||||||
|
let rendered = render_error(source, Some("test.podlang"), &err);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
rendered.contains("argument count mismatch"),
|
||||||
|
"rendered: {rendered}"
|
||||||
|
);
|
||||||
|
assert!(rendered.contains("Equal"), "rendered: {rendered}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_pest_syntax_error() {
|
||||||
|
let source = "REQUEST(!!!invalid!!!";
|
||||||
|
let err = request_err(source);
|
||||||
|
let rendered = render_error(source, Some("test.podlang"), &err);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
rendered.contains("syntax error") || rendered.contains("error"),
|
||||||
|
"rendered: {rendered}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_no_path() {
|
||||||
|
let source = r#"
|
||||||
|
REQUEST(
|
||||||
|
NoSuchPred(A, B)
|
||||||
|
)
|
||||||
|
"#;
|
||||||
|
let err = request_err(source);
|
||||||
|
let rendered = render_error(source, None, &err);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
rendered.contains("undefined predicate"),
|
||||||
|
"rendered: {rendered}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!rendered.contains("-->"),
|
||||||
|
"should not have path line, rendered: {rendered}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_error_without_span() {
|
||||||
|
let error = LangError::from(BatchingError::Internal {
|
||||||
|
message: "something went wrong".to_string(),
|
||||||
|
});
|
||||||
|
let rendered = render_error("", None, &error);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
rendered.contains("something went wrong"),
|
||||||
|
"rendered: {rendered}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::{fmt, ops::Deref};
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -7,7 +9,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum LangError {
|
pub enum LangErrorKind {
|
||||||
#[error("Parsing failed: {0}")]
|
#[error("Parsing failed: {0}")]
|
||||||
Parse(Box<ParseError>),
|
Parse(Box<ParseError>),
|
||||||
|
|
||||||
|
|
@ -27,6 +29,68 @@ pub enum LangError {
|
||||||
Batching(Box<BatchingError>),
|
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
|
/// Validation errors from frontend AST validation
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum ValidationError {
|
pub enum ValidationError {
|
||||||
|
|
@ -295,30 +359,30 @@ pub enum SplittingError {
|
||||||
|
|
||||||
impl From<ParseError> for LangError {
|
impl From<ParseError> for LangError {
|
||||||
fn from(err: ParseError) -> Self {
|
fn from(err: ParseError) -> Self {
|
||||||
LangError::Parse(Box::new(err))
|
LangError::new(LangErrorKind::Parse(Box::new(err)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<middleware::Error> for LangError {
|
impl From<middleware::Error> for LangError {
|
||||||
fn from(err: middleware::Error) -> Self {
|
fn from(err: middleware::Error) -> Self {
|
||||||
LangError::Middleware(Box::new(err))
|
LangError::new(LangErrorKind::Middleware(Box::new(err)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ValidationError> for LangError {
|
impl From<ValidationError> for LangError {
|
||||||
fn from(err: ValidationError) -> Self {
|
fn from(err: ValidationError) -> Self {
|
||||||
LangError::Validation(Box::new(err))
|
LangError::new(LangErrorKind::Validation(Box::new(err)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<LoweringError> for LangError {
|
impl From<LoweringError> for LangError {
|
||||||
fn from(err: LoweringError) -> Self {
|
fn from(err: LoweringError) -> Self {
|
||||||
LangError::Lowering(Box::new(err))
|
LangError::new(LangErrorKind::Lowering(Box::new(err)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<BatchingError> for LangError {
|
impl From<BatchingError> for LangError {
|
||||||
fn from(err: BatchingError) -> Self {
|
fn from(err: BatchingError) -> Self {
|
||||||
LangError::Batching(Box::new(err))
|
LangError::new(LangErrorKind::Batching(Box::new(err)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@
|
||||||
//! Large predicates are automatically split into chains of smaller predicates;
|
//! Large predicates are automatically split into chains of smaller predicates;
|
||||||
//! `apply_predicate` handles this transparently.
|
//! `apply_predicate` handles this transparently.
|
||||||
//!
|
//!
|
||||||
|
pub mod diagnostics;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod frontend_ast;
|
pub mod frontend_ast;
|
||||||
pub mod frontend_ast_lower;
|
pub mod frontend_ast_lower;
|
||||||
|
|
@ -37,7 +38,8 @@ pub mod pretty_print;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub use error::LangError;
|
pub use diagnostics::render_error;
|
||||||
|
pub use error::{LangError, LangErrorKind};
|
||||||
pub use frontend_ast_split::{SplitChainInfo, SplitChainPiece, SplitResult};
|
pub use frontend_ast_split::{SplitChainInfo, SplitChainPiece, SplitResult};
|
||||||
pub use module::{Module, MultiOperationError};
|
pub use module::{Module, MultiOperationError};
|
||||||
pub use parser::{parse_podlang, Pairs, ParseError, Rule};
|
pub use parser::{parse_podlang, Pairs, ParseError, Rule};
|
||||||
|
|
@ -57,7 +59,17 @@ pub fn load_module(
|
||||||
source: &str,
|
source: &str,
|
||||||
name: &str,
|
name: &str,
|
||||||
params: &Params,
|
params: &Params,
|
||||||
available_modules: Vec<Arc<Module>>,
|
available_modules: &[Arc<Module>],
|
||||||
|
) -> Result<Module, LangError> {
|
||||||
|
load_module_inner(source, name, params, available_modules)
|
||||||
|
.map_err(|e| e.with_source(source.to_string(), None))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_module_inner(
|
||||||
|
source: &str,
|
||||||
|
name: &str,
|
||||||
|
params: &Params,
|
||||||
|
available_modules: &[Arc<Module>],
|
||||||
) -> Result<Module, LangError> {
|
) -> Result<Module, LangError> {
|
||||||
let pairs = parse_podlang(source)?;
|
let pairs = parse_podlang(source)?;
|
||||||
let document_pair = pairs
|
let document_pair = pairs
|
||||||
|
|
@ -89,6 +101,15 @@ pub fn parse_request(
|
||||||
source: &str,
|
source: &str,
|
||||||
params: &Params,
|
params: &Params,
|
||||||
available_modules: &[Arc<Module>],
|
available_modules: &[Arc<Module>],
|
||||||
|
) -> Result<PodRequest, LangError> {
|
||||||
|
parse_request_inner(source, params, available_modules)
|
||||||
|
.map_err(|e| e.with_source(source.to_string(), None))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_request_inner(
|
||||||
|
source: &str,
|
||||||
|
params: &Params,
|
||||||
|
available_modules: &[Arc<Module>],
|
||||||
) -> Result<PodRequest, LangError> {
|
) -> Result<PodRequest, LangError> {
|
||||||
let pairs = parse_podlang(source)?;
|
let pairs = parse_podlang(source)?;
|
||||||
let document_pair = pairs
|
let document_pair = pairs
|
||||||
|
|
@ -160,7 +181,7 @@ mod tests {
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
let params = Params::default();
|
let params = Params::default();
|
||||||
let module = load_module(input, "test_module", ¶ms, vec![])?;
|
let module = load_module(input, "test_module", ¶ms, &[])?;
|
||||||
|
|
||||||
assert_eq!(module.batch.predicates().len(), 1);
|
assert_eq!(module.batch.predicates().len(), 1);
|
||||||
|
|
||||||
|
|
@ -235,7 +256,7 @@ mod tests {
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
let params = Params::default();
|
let params = Params::default();
|
||||||
let module = load_module(input, "test_module", ¶ms, vec![])?;
|
let module = load_module(input, "test_module", ¶ms, &[])?;
|
||||||
|
|
||||||
assert_eq!(module.batch.predicates().len(), 1);
|
assert_eq!(module.batch.predicates().len(), 1);
|
||||||
|
|
||||||
|
|
@ -281,7 +302,7 @@ mod tests {
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
let params = Params::default();
|
let params = Params::default();
|
||||||
let module = Arc::new(load_module(module_input, "my_module", ¶ms, vec![])?);
|
let module = Arc::new(load_module(module_input, "my_module", ¶ms, &[])?);
|
||||||
|
|
||||||
assert_eq!(module.batch.predicates().len(), 1);
|
assert_eq!(module.batch.predicates().len(), 1);
|
||||||
|
|
||||||
|
|
@ -330,7 +351,7 @@ mod tests {
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
let params = Params::default();
|
let params = Params::default();
|
||||||
let module = Arc::new(load_module(module_input, "some_module", ¶ms, vec![])?);
|
let module = Arc::new(load_module(module_input, "some_module", ¶ms, &[])?);
|
||||||
|
|
||||||
let module_hash = module.id().encode_hex::<String>();
|
let module_hash = module.id().encode_hex::<String>();
|
||||||
|
|
||||||
|
|
@ -585,7 +606,7 @@ mod tests {
|
||||||
)
|
)
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
let module = load_module(input, "ethdos", ¶ms, vec![])?;
|
let module = load_module(input, "ethdos", ¶ms, &[])?;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
module.batch.predicates().len(),
|
module.batch.predicates().len(),
|
||||||
|
|
@ -855,7 +876,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
|
|
||||||
// 3. Load as module
|
// 3. Load as module
|
||||||
let module = load_module(&input, "test", ¶ms, vec![extmod])?;
|
let module = load_module(&input, "test", ¶ms, &[extmod])?;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
module.batch.predicates().len(),
|
module.batch.predicates().len(),
|
||||||
|
|
@ -1011,8 +1032,8 @@ mod tests {
|
||||||
|
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
|
|
||||||
match result.err().unwrap() {
|
match result.err().unwrap().kind {
|
||||||
LangError::Validation(e) => match *e {
|
LangErrorKind::Validation(e) => match *e {
|
||||||
frontend_ast_validate::ValidationError::ModuleNotFound { name, .. } => {
|
frontend_ast_validate::ValidationError::ModuleNotFound { name, .. } => {
|
||||||
// The error now carries the hex-formatted hash
|
// The error now carries the hex-formatted hash
|
||||||
assert_eq!(name, fake_hash);
|
assert_eq!(name, fake_hash);
|
||||||
|
|
@ -1034,12 +1055,12 @@ mod tests {
|
||||||
)
|
)
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
let result = load_module(input, "test", ¶ms, vec![]);
|
let result = load_module(input, "test", ¶ms, &[]);
|
||||||
|
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
|
|
||||||
match result.err().unwrap() {
|
match result.err().unwrap().kind {
|
||||||
LangError::Validation(e) => match *e {
|
LangErrorKind::Validation(e) => match *e {
|
||||||
frontend_ast_validate::ValidationError::UndefinedWildcard {
|
frontend_ast_validate::ValidationError::UndefinedWildcard {
|
||||||
name,
|
name,
|
||||||
pred_name,
|
pred_name,
|
||||||
|
|
|
||||||
|
|
@ -610,7 +610,7 @@ mod tests {
|
||||||
r#"is_equal(X, Y) = AND(Equal(X["val"], Y["val"]))"#,
|
r#"is_equal(X, Y) = AND(Equal(X["val"], Y["val"]))"#,
|
||||||
"checks",
|
"checks",
|
||||||
¶ms,
|
¶ms,
|
||||||
vec![],
|
&[],
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
);
|
);
|
||||||
|
|
@ -621,7 +621,7 @@ mod tests {
|
||||||
r#"is_less(X, Y) = AND(Lt(X["val"], Y["val"]))"#,
|
r#"is_less(X, Y) = AND(Lt(X["val"], Y["val"]))"#,
|
||||||
"ordering",
|
"ordering",
|
||||||
¶ms,
|
¶ms,
|
||||||
vec![],
|
&[],
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
);
|
);
|
||||||
|
|
@ -645,7 +645,7 @@ mod tests {
|
||||||
),
|
),
|
||||||
"combined",
|
"combined",
|
||||||
¶ms,
|
¶ms,
|
||||||
vec![checks.clone(), ordering.clone()],
|
&[checks.clone(), ordering.clone()],
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -391,15 +391,15 @@ mod tests {
|
||||||
|
|
||||||
// Step 1: Parse the input
|
// Step 1: Parse the input
|
||||||
let module =
|
let module =
|
||||||
load_module(input, "test", ¶ms, vec![]).expect("Initial parsing should succeed");
|
load_module(input, "test", ¶ms, &[]).expect("Initial parsing should succeed");
|
||||||
|
|
||||||
// Step 2: Pretty-print the parsed batch
|
// Step 2: Pretty-print the parsed batch
|
||||||
let batch = &module.batch;
|
let batch = &module.batch;
|
||||||
let pretty_printed = batch.to_podlang_string();
|
let pretty_printed = batch.to_podlang_string();
|
||||||
|
|
||||||
// Step 3: Parse the pretty-printed result
|
// Step 3: Parse the pretty-printed result
|
||||||
let reparsed_module = load_module(&pretty_printed, "test", ¶ms, vec![])
|
let reparsed_module =
|
||||||
.expect("Reparsing should succeed");
|
load_module(&pretty_printed, "test", ¶ms, &[]).expect("Reparsing should succeed");
|
||||||
let reparsed_batch = &reparsed_module.batch;
|
let reparsed_batch = &reparsed_module.batch;
|
||||||
|
|
||||||
// Step 4: Verify the ASTs are equivalent
|
// Step 4: Verify the ASTs are equivalent
|
||||||
|
|
@ -555,7 +555,7 @@ mod tests {
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
let params = Params::default();
|
let params = Params::default();
|
||||||
let module = load_module(input, "test", ¶ms, vec![]).expect("Parsing should succeed");
|
let module = load_module(input, "test", ¶ms, &[]).expect("Parsing should succeed");
|
||||||
let batch = &module.batch;
|
let batch = &module.batch;
|
||||||
|
|
||||||
let pretty_printed = batch.to_podlang_string();
|
let pretty_printed = batch.to_podlang_string();
|
||||||
|
|
@ -563,8 +563,8 @@ mod tests {
|
||||||
println!("Original input:\n{}", input);
|
println!("Original input:\n{}", input);
|
||||||
println!("\nPretty-printed output:\n{}", pretty_printed);
|
println!("\nPretty-printed output:\n{}", pretty_printed);
|
||||||
|
|
||||||
let reparsed = load_module(&pretty_printed, "test", ¶ms, vec![])
|
let reparsed =
|
||||||
.expect("Reparsing should succeed");
|
load_module(&pretty_printed, "test", ¶ms, &[]).expect("Reparsing should succeed");
|
||||||
let reparsed_batch = &reparsed.batch;
|
let reparsed_batch = &reparsed.batch;
|
||||||
|
|
||||||
assert_eq!(batch.predicates(), reparsed_batch.predicates());
|
assert_eq!(batch.predicates(), reparsed_batch.predicates());
|
||||||
|
|
@ -630,12 +630,12 @@ mod tests {
|
||||||
|
|
||||||
let params = Params::default();
|
let params = Params::default();
|
||||||
let module =
|
let module =
|
||||||
load_module(&input, "test", ¶ms, vec![]).expect("Should parse successfully");
|
load_module(&input, "test", ¶ms, &[]).expect("Should parse successfully");
|
||||||
let batch = &module.batch;
|
let batch = &module.batch;
|
||||||
|
|
||||||
let pretty_printed = batch.to_podlang_string();
|
let pretty_printed = batch.to_podlang_string();
|
||||||
|
|
||||||
let reparsed_module = load_module(&pretty_printed, "test", ¶ms, vec![])
|
let reparsed_module = load_module(&pretty_printed, "test", ¶ms, &[])
|
||||||
.expect("Should reparse successfully");
|
.expect("Should reparse successfully");
|
||||||
let reparsed_batch = &reparsed_module.batch;
|
let reparsed_batch = &reparsed_module.batch;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue