Nested Error Reporting
Tree-structured error reporting showing all failure paths for comprehensive debugging.
Key Concepts
- error tree structure
- failure path visualization
- alternative tracking
- debugging output
Why Parse This Format?
Understanding all failure paths helps debug complex grammars. Tree-structured errors show which alternatives were tried and why they failed.
Use Cases
- Grammar development debugging
- Complex parser troubleshooting
- Teaching parser concepts
- IDE integration for error visualization
Input Format
Any input that might fail parsing. The example uses a block-structured DSL.
Examples
define f()
@res.name()
endVALIDValid definition block
define f()
@res.name(
endINVALIDUnclosed function call
Output Format
Tree-structured error showing all failure paths.
Structure
messageError description
positionByte offset
childrenNested error nodes
Visual Overview
How the Code Works
This section explains how the parsing rules map to the format semantics.
Error Tree Structure
pub struct ErrorNode {
pub message: String,
pub position: usize,
pub children: Vec<ErrorNode>,
}Errors form a tree with nested causes: The tree structure mirrors the grammar's choice points.
Tree Construction
fn build_error_tree(input: &str, grammar: &Grammar) -> ErrorNode {
match parser.parse() {
Ok(_) => ErrorNode::new("Success", 0),
Err(e) => {
let mut root = ErrorNode::new("Parse failed", 0);
root.add_child(ErrorNode::new("Expected 'end'", pos));
root
}
}
}Build the tree from parse results: Root contains overall failure; children show specific causes.
ASCII Tree Formatting
fn format_tree(&self, prefix: &str, is_last: bool) -> String {
let connector = if is_last { "`- " } else { "|- " };
let result = format!("{}{}{} (pos {})\n",
prefix, connector, self.message, self.position);
for (i, child) in self.children.iter().enumerate() {
let is_last_child = i == self.children.len() - 1;
result.push_str(&child.format_tree(&new_prefix, is_last_child));
}
result
}Display errors as a visual tree: Recursive formatting with branch connectors.
Example Output
`- Parse failed (pos 0) |- Expected 'end' keyword (pos 25) |- In reference expression (pos 20) `- Expected identifier (pos 15) Each indentation level shows deeper failure causes.
Position Tracking
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);Positions help locate errors: Extract byte position from error message for display.
Position Tracking
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);Positions help locate errors: Extract byte position from error message for display.
Source Code
//! Nested Error Reporting Example
//!
//! Demonstrates tree-structured error reporting showing all failure paths.
//!
//! Run with: cargo run --example nested-errors --no-default-features
#![allow(clippy::print_literal)]
use parsanol::portable::{
parser_dsl::{choice, dynamic, re, 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]+"))
.rule("string", re(r#""[^"]*""#))
// Expression
.rule(
"expr",
choice(vec![
dynamic(re(r"[a-zA-Z_][a-zA-Z0-9_]*")),
dynamic(re(r"[0-9]+")),
dynamic(re(r#""[^"]*""#)),
]),
)
// Statement: identifier = expr
.rule(
"statement",
seq(vec![
dynamic(re(r"[a-zA-Z_][a-zA-Z0-9_]*")),
dynamic(str("=")),
dynamic(re(r#"[a-zA-Z_][a-zA-Z0-9_]*|[0-9]+|"[^"]*""#)),
]),
)
.build()
}
/// Error tree node for nested reporting
#[derive(Debug, Clone)]
pub struct ErrorNode {
pub message: String,
pub position: usize,
pub children: Vec<ErrorNode>,
}
impl ErrorNode {
fn new(message: &str, position: usize) -> Self {
Self {
message: message.to_string(),
position,
children: Vec::new(),
}
}
fn add_child(&mut self, child: ErrorNode) {
self.children.push(child);
}
/// Format as ASCII tree
fn format_tree(&self, prefix: &str, is_last: bool) -> String {
let connector = if is_last { "`- " } else { "|- " };
let mut result = format!(
"{}{}{} (pos {})\n",
prefix, connector, self.message, self.position
);
let child_prefix = if is_last { " " } else { "| " };
let new_prefix = format!("{}{}", prefix, child_prefix);
for (i, child) in self.children.iter().enumerate() {
let is_last_child = i == self.children.len() - 1;
result.push_str(&child.format_tree(&new_prefix, is_last_child));
}
result
}
}
/// Build an error tree from a parse failure
fn build_error_tree(input: &str, grammar: &Grammar) -> ErrorNode {
let mut arena = AstArena::for_input(input.len());
let mut parser = PortableParser::new(grammar, input, &mut arena);
match parser.parse() {
Ok(_) => ErrorNode::new("Success", 0),
Err(e) => {
// Build a simplified error tree
let error_str = e.to_string();
// Parse position from error message
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);
// Root error
let mut root = ErrorNode::new("Parse failed", 0);
// Add common failure causes based on context
let rest = if pos < input.len() { &input[pos..] } else { "" };
if rest.starts_with('=') {
root.add_child(ErrorNode::new("Expected identifier before '='", pos));
} else if !rest.is_empty() && !rest.chars().next().unwrap().is_alphanumeric() {
root.add_child(ErrorNode::new(
&format!("Unexpected character: {:?}", rest.chars().next()),
pos,
));
}
root.add_child(ErrorNode::new("Expected: identifier = value", pos));
root
}
}
}
fn main() {
println!("Nested Error Reporting Example");
println!("===============================");
println!();
let grammar = build_grammar();
// Test case with error
let input = "x = 123"; // Valid
println!("Input: {}", input);
let tree = build_error_tree(input, &grammar);
println!("Result: {}", tree.format_tree("", true));
// Test case with error
let input2 = "= 123"; // Missing identifier
println!("---");
println!("Input: {}", input2);
let tree2 = build_error_tree(input2, &grammar);
println!("Error tree:");
println!("{}", tree2.format_tree("", true));
// Test case with error
let input3 = "x ="; // Missing value
println!("---");
println!("Input: {}", input3);
let tree3 = build_error_tree(input3, &grammar);
println!("Error tree:");
println!("{}", tree3.format_tree("", true));
println!("---");
println!("Nested error reporting shows the full tree of failures,");
println!("helping developers understand which alternatives were tried.");
}
# frozen_string_literal: true
$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../lib"
require "parsanol/parslet"
require "parsanol/convenience"
# This example demonstrates tree error reporting in a real life example.
# Originally contributed to Parslet, ported to Parsanol as an example.
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 Parser < 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 = Parser.new
parser.parse_with_debug(d)
end
puts "-" * 80