Deepest Error Reporting
Find the deepest parse failure point for more helpful error messages.
Key Concepts
- deepest failure point
- error position tracking
- meaningful error messages
- parse failure analysis
Why Parse This Format?
PEG parsers can fail at many points. Finding the deepest failure provides the most useful error location for users.
Use Cases
- Compiler error messages
- Configuration file validation
- Query language debugging
- DSL error reporting
Input Format
Any input that might fail parsing. The example uses a block-structured DSL.
Examples
define f()
@res.name(
endINVALIDUnclosed function call - error at '('
define f()
@res.name()
INVALIDMissing 'end' - error at EOF
Output Format
Position and context of the deepest parse failure.
Structure
positionByte offset where parsing failed
lineLine number of failure
columnColumn number of failure
contextSurrounding input for display
Visual Overview
How the Code Works
This section explains how the parsing rules map to the format semantics.
The Deepest Failure Concept
Input: "abc123xyz"
Rule tries: letter -> succeeds for "abc"
digit -> fails at position 3
letter -> fails at position 3
Deepest: position 6 (after "123") where digit succeeded longestPEG parsers try alternatives in order. The deepest failure is where parsing progressed furthest before failing: This gives better errors than "expected something at position 0".
Error Position Extraction
fn find_deepest_error(input: &str, grammar: &Grammar) -> (usize, String) {
let mut arena = AstArena::for_input(input.len());
let mut parser = PortableParser::new(grammar, input, &mut arena);
match parser.parse() {
Ok(_) => (0, "Success".to_string()),
Err(e) => {
let pos = e.to_string().find("at position")
.map(|p| /* extract position */)
.unwrap_or(0);
(pos, e.to_string())
}
}
}Error messages include position information that can be extracted.
Position Formatting
fn format_error(input: &str, position: usize) -> String {
let lines: Vec<&str> = input.lines().collect();
let mut current_pos = 0;
for (i, line) in lines.iter().enumerate() {
result.push_str(&format!("{:02} {}\n", i + 1, line));
let line_end = current_pos + line.len();
if position >= current_pos && position <= line_end {
let col = position - current_pos;
result.push_str(&format!(" {}^\n", " ".repeat(col)));
}
current_pos = line_end + 1;
}
result
}Show the error location with context: Convert byte position to line/column for display.
Grammar for Testing
builder = builder.rule("define_block", seq(vec![
dynamic(str("define")),
dynamic(ref_("space")),
dynamic(ref_("identifier")),
dynamic(str("()")),
dynamic(ref_("body_full")),
dynamic(str("end")),
]));The example uses a realistic block-structured grammar: This has enough complexity to demonstrate error scenarios.
Test Cases
let test_cases = [
("define f()\n @res.name()\nend", true), // valid
("define f()\n @res.name(\nend", false), // unclosed paren
("define f()\n @res.name()\n", false), // missing end
];The example tests both valid and invalid inputs: Each demonstrates a different error location.
Test Cases
let test_cases = [
("define f()\n @res.name()\nend", true), // valid
("define f()\n @res.name(\nend", false), // unclosed paren
("define f()\n @res.name()\n", false), // missing end
];The example tests both valid and invalid inputs: Each demonstrates a different error location.
Source Code
//! Deepest Error Reporting Example
//!
//! Demonstrates finding the deepest parse failure point for better error messages.
//!
//! Run with: cargo run --example deepest-errors --no-default-features
#![allow(clippy::print_literal)]
use parsanol::portable::{
parser_dsl::{choice, dynamic, re, ref_, seq, str, GrammarBuilder},
AstArena, Grammar, PortableParser,
};
/// Build a simple grammar for error demonstration
fn build_grammar() -> Grammar {
GrammarBuilder::new()
// Basic tokens
.rule("identifier", re(r"[a-zA-Z_][a-zA-Z0-9_]*"))
.rule("number", re(r"[0-9]+"))
// Expression
.rule(
"expr",
choice(vec![
dynamic(re(r"[a-zA-Z_][a-zA-Z0-9_]*")),
dynamic(re(r"[0-9]+")),
]),
)
// Statement: identifier = expr
.rule(
"statement",
seq(vec![
dynamic(ref_("identifier")),
dynamic(str("=")),
dynamic(ref_("expr")),
]),
)
.build()
}
/// Find the deepest error position
fn find_deepest_error(input: &str, grammar: &Grammar) -> (usize, String) {
let mut arena = AstArena::for_input(input.len());
let mut parser = PortableParser::new(grammar, input, &mut arena);
match parser.parse() {
Ok(_) => (0, "Success".to_string()),
Err(e) => {
// Find deepest position from error
let error_str = e.to_string();
let pos = error_str
.find("at position")
.map(|p| {
let rest = &error_str[p..];
rest.split_whitespace()
.nth(2)
.and_then(|s| s.trim_end_matches(':').parse::<usize>().ok())
.unwrap_or(0)
})
.unwrap_or(0);
(pos, error_str)
}
}
}
/// Format input with position marker
fn format_error(input: &str, position: usize) -> String {
let lines: Vec<&str> = input.lines().collect();
let mut current_pos = 0;
let mut result = String::new();
for (i, line) in lines.iter().enumerate() {
result.push_str(&format!("{:02} {}\n", i + 1, line));
let line_end = current_pos + line.len();
if position >= current_pos && position <= line_end {
let col = position - current_pos;
result.push_str(&format!(" {}^\n", " ".repeat(col)));
}
current_pos = line_end + 1; // +1 for newline
}
result
}
fn main() {
println!("Deepest Error Reporting Example");
println!("================================");
println!();
let grammar = build_grammar();
// Test cases
let test_cases = [
// Valid
("x = 42", true),
("name = hello", true),
// Invalid
("= 42", false),
("x =", false),
("x = ", false),
];
for (input, should_succeed) in test_cases {
println!("---");
println!("Input: {:?}", input);
let (pos, error) = find_deepest_error(input, &grammar);
if should_succeed {
if pos == 0 && error == "Success" {
println!("OK Parsed successfully");
} else {
println!("FAIL Unexpected error: {}", error);
}
} else {
println!("Error at position {}", pos);
println!("{}", format_error(input, pos));
}
println!();
}
println!("---");
println!("Deepest error reporting finds the point where parsing");
println!("progressed furthest before failing, providing more");
println!("useful error messages than just 'expected something'.");
}
# frozen_string_literal: true
$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../lib"
# This example demonstrates how to do deepest error reporting, as invented
# by John Mettraux (issue #64).
require "parsanol/parslet"
require "parsanol/convenience"
def prettify(str)
puts "#{' ' * 3}#{' ' * 4}.#{' ' * 4}10#{' ' * 3}.#{' ' * 4}20"
str.lines.each_with_index do |line, index|
printf "%02d %s\n",
index + 1,
line.chomp
end
end
class MyParser < Parsanol::Parser
# commons
rule(:space) { match('[ \t]').repeat(1) }
rule(:space?) { space.maybe }
rule(:newline) { match('[\r\n]') }
rule(:comment) { str("#") >> match('[^\r\n]').repeat }
rule(:line_separator) do
(space? >> ((comment.maybe >> newline) | str(";")) >> space?).repeat(1)
end
rule(:blank) { line_separator | space }
rule(:blank?) { blank.maybe }
rule(:identifier) { match("[a-zA-Z0-9_]").repeat(1) }
# res_statement
rule(:reference) do
(str("@").repeat(1, 2) >> identifier).as(:reference)
end
rule(:res_action_or_link) do
str(".").as(:dot) >> (identifier >> str("?").maybe).as(:name) >> str("()")
end
rule(:res_actions) do
reference.as(:resources) >>
res_action_or_link.as(:res_action).repeat(0).as(:res_actions)
end
rule(:res_statement) do
res_actions >>
(str(":") >> identifier.as(:name)).maybe.as(:res_field)
end
# expression
rule(:expression) do
res_statement
end
# body
rule(:body) do
(line_separator >> (block | expression)).repeat(1).as(:body) >>
line_separator
end
# blocks
rule(:begin_block) do
(str("concurrent").as(:type) >> space).maybe.as(:pre) >>
str("begin").as(:begin) >>
body >>
str("end")
end
rule(:define_block) do
str("define").as(:define) >> space >>
identifier.as(:name) >> str("()") >>
body >>
str("end")
end
rule(:block) do
define_block | begin_block
end
# root
rule(:radix) do
line_separator.maybe >> block >> line_separator.maybe
end
root(:radix)
end
ds = [
%{
define f()
@res.name
end
},
%{
define f()
begin
@res.name
end
end
},
]
ds.each do |d|
puts "-" * 80
prettify(d)
parser = MyParser.new
parser.parse_with_debug(d,
reporter: Parsanol::ErrorReporter::Deepest.new)
end
puts "-" * 80