Error Reporting
Demonstrate rich error reporting techniques including position-to-line/column conversion, context extraction, and formatted error messages.
Key Concepts
- position tracking
- line/column conversion
- context extraction
- error formatting
- failure point detection
Why Parse This Format?
Good error messages are critical for developer experience. Parser errors should clearly indicate what went wrong and where, with helpful context for debugging.
Use Cases
- Compiler error messages
- Linter diagnostics
- IDE error highlighting
- Configuration file validation
- Debugging parser failures
Input Format
Any parseable input that demonstrates error conditions. Focus is on the error output quality, not the input format.
various - errors are the focusExamples
valid_identifierVALIDValid input - no error
42VALIDValid number
hello world!INVALIDInvalid: unexpected character
# comment\nidentifier\n42\nbad@symbolINVALIDMulti-line with error
Output Format
The parser produces detailed ParseErrorInfo with position, line/column, and surrounding context.
Structure
ParseErrorInfoStruct with message, position, line, column, context
contextString showing surrounding lines with error highlighted
Visual Overview
How the Code Works
This section explains how the parsing rules map to the format semantics.
Error Grammar Definition
.rule("identifier", re("[a-zA-Z_][a-zA-Z0-9_]*"))
.rule("number", re("[0-9]+(\\.[0-9]+)?"))
.rule(
"expr",
choice(vec![
dynamic(re("[0-9]+(\\.[0-9]+)?")),
dynamic(re("[a-zA-Z_][a-zA-Z0-9_]*")),
]),
)The grammar defines what valid input looks like: When parsing fails, we need to explain why.
Position to Line/Column Conversion
pub fn position_to_line_col(input: &str, pos: usize) -> (usize, usize) {
let mut line = 1;
let mut col = 1;
for (i, c) in input.chars().enumerate() {
if i >= pos {
break;
}
if c == '\n' {
line += 1;
col = 1;
} else {
col += 1;
}
}
(line, col)
}Byte positions are converted to human-readable locations: This enables "Error at line 3, column 5" messages.
Context Extraction
pub fn get_context(input: &str, pos: usize, radius: usize) -> String {
let lines: Vec<&str> = input.lines().collect();
let (line, _col) = position_to_line_col(input, pos);
let start_line = line.saturating_sub(radius);
let end_line = (line + radius).min(lines.len());
let mut context = String::new();
for i in start_line..end_line {
let prefix = if i + 1 == line { ">>> " } else { " " };
context.push_str(&format!("{}{:3}: {}\n", prefix, i + 1, lines[i]));
}
context
}Surrounding lines provide context: The error line is marked with `>>>` for visibility.
Failure Point Detection
fn find_failure_point(input: &str) -> usize {
// Try parsing progressively longer prefixes
for i in 1..=input.len() {
let prefix = &input[..i];
let mut arena = AstArena::for_input(prefix.len());
let mut parser = PortableParser::new(&grammar, prefix, &mut arena);
if parser.parse().is_err() {
// Check if adding more characters helps
if i < input.len() {
let next_prefix = &input[..i + 1];
// If next prefix parses, failure is transient
if parser2.parse().is_ok() {
continue;
}
}
return i.saturating_sub(1);
}
}
input.len()
}Finding where parsing failed: This finds the deepest point where parsing could succeed before failing.
Error Information Structure
pub struct ParseErrorInfo {
pub message: String,
pub position: usize,
pub line: usize,
pub column: usize,
pub context: String,
}Error Information Structure
pub struct ParseErrorInfo {
pub message: String,
pub position: usize,
pub line: usize,
pub column: usize,
pub context: String,
}Source Code
//! Error Reporting Example
//!
//! This example demonstrates rich error reporting for parsers.
//! Shows how to provide helpful error messages with position info.
//! Based on the Parslet deepest_errors.rb and nested_errors.rb examples.
//!
//! Run with: cargo run --example error_reporting --no-default-features
#![allow(clippy::print_literal)]
#![allow(clippy::get_first)]
use parsanol::portable::{
parser_dsl::{choice, dynamic, re, seq, str, GrammarBuilder},
AstArena, Grammar, PortableParser,
};
/// Build a grammar that demonstrates error reporting
fn build_grammar() -> Grammar {
GrammarBuilder::new()
// Whitespace
.rule("space", re("[ \\t]+"))
// Newline
.rule("newline", re("[\\r\\n]"))
// Comment: # to end of line
.rule(
"comment",
seq(vec![dynamic(str("#")), dynamic(re("[^\\r\\n]*"))]),
)
// Identifier
.rule("identifier", re("[a-zA-Z_][a-zA-Z0-9_]*"))
// Number
.rule("number", re("[0-9]+(\\.[0-9]+)?"))
// Expression
.rule(
"expr",
choice(vec![
dynamic(re("[0-9]+(\\.[0-9]+)?")),
dynamic(re("[a-zA-Z_][a-zA-Z0-9_]*")),
]),
)
.build()
}
/// Error information
#[derive(Debug, Clone)]
pub struct ParseErrorInfo {
pub message: String,
pub position: usize,
pub line: usize,
pub column: usize,
pub context: String,
}
impl std::fmt::Display for ParseErrorInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Error at line {}, column {} (position {}): {}\nContext: {}",
self.line, self.column, self.position, self.message, self.context
)
}
}
/// Convert position to line/column
pub fn position_to_line_col(input: &str, pos: usize) -> (usize, usize) {
let mut line = 1;
let mut col = 1;
for (i, c) in input.chars().enumerate() {
if i >= pos {
break;
}
if c == '\n' {
line += 1;
col = 1;
} else {
col += 1;
}
}
(line, col)
}
/// Get context around a position
pub fn get_context(input: &str, pos: usize, radius: usize) -> String {
let lines: Vec<&str> = input.lines().collect();
let (line, _col) = position_to_line_col(input, pos);
let start_line = line.saturating_sub(radius);
let end_line = (line + radius).min(lines.len());
let mut context = String::new();
for i in start_line..end_line {
let prefix = if i + 1 == line { ">>> " } else { " " };
if let Some(l) = lines.get(i) {
context.push_str(&format!("{}{:3}: {}\n", prefix, i + 1, l));
}
}
context
}
/// Create detailed error info
pub fn create_error_info(input: &str, pos: usize, message: &str) -> ParseErrorInfo {
let (line, col) = position_to_line_col(input, pos);
let context = get_context(input, pos, 2);
ParseErrorInfo {
message: message.to_string(),
position: pos,
line,
column: col,
context,
}
}
/// Prettify input with line numbers
pub fn prettify(input: &str) -> String {
let mut result = String::new();
for (i, line) in input.lines().enumerate() {
result.push_str(&format!("{:02} {}\n", i + 1, line));
}
result
}
/// Try parsing with detailed error reporting
pub fn parse_with_errors(input: &str) -> Result<(), ParseErrorInfo> {
let grammar = build_grammar();
let mut arena = AstArena::for_input(input.len());
let mut parser = PortableParser::new(&grammar, input, &mut arena);
match parser.parse() {
Ok(_) => Ok(()),
Err(_) => {
// Find the deepest failure point
let pos = find_failure_point(input);
Err(create_error_info(input, pos, "Failed to parse input"))
}
}
}
/// Find the deepest failure point in the input
fn find_failure_point(input: &str) -> usize {
let grammar = build_grammar();
// Try parsing progressively longer prefixes
for i in 1..=input.len() {
let prefix = &input[..i];
let mut arena = AstArena::for_input(prefix.len());
let mut parser = PortableParser::new(&grammar, prefix, &mut arena);
if parser.parse().is_err() {
// Check if adding more characters helps
if i < input.len() {
let next_prefix = &input[..i + 1];
let mut arena2 = AstArena::for_input(next_prefix.len());
let mut parser2 = PortableParser::new(&grammar, next_prefix, &mut arena2);
if parser2.parse().is_ok() {
continue;
}
}
return i.saturating_sub(1);
}
}
input.len()
}
fn main() {
println!("Error Reporting Example");
println!("=======================\n");
// Test valid input
println!("=== Valid Input ===");
let valid = "hello";
println!("Input: {:?}", valid);
match parse_with_errors(valid) {
Ok(_) => println!("Result: Parsed successfully"),
Err(e) => println!("Error: {}", e),
}
println!();
// Test invalid input with error
println!("=== Invalid Input (Single Line) ===");
let invalid = "hello world!";
println!("Input: {:?}", invalid);
match parse_with_errors(invalid) {
Ok(_) => println!("Result: Parsed successfully"),
Err(e) => {
println!("Error:\n{}", e);
}
}
println!();
// Test multi-line input
println!("=== Multi-line Input ===");
let multiline = r#"# Comment
identifier
42
bad@symbol
another
"#;
println!("Input:");
println!("{}", prettify(multiline));
println!("Parsing:");
match parse_with_errors(multiline) {
Ok(_) => println!("Result: Parsed successfully"),
Err(e) => {
println!("Error at position {}:", e.position);
println!("{}", e.context);
}
}
println!();
// Demonstrate position conversion
println!("=== Position to Line/Column Conversion ===");
let test = "line1\nline2\nline3\n";
println!("Input: {:?}", test);
for pos in [0, 3, 5, 6, 10, 12, 17] {
let (line, col) = position_to_line_col(test, pos);
println!(" Position {} => Line {}, Column {}", pos, line, col);
}
println!();
// Context extraction
println!("=== Context Extraction ===");
let long_input = r#"first line
second line
third line
fourth line
fifth line
sixth line
"#;
println!("Input:");
println!("{}", long_input);
println!("Context around line 4:");
println!("{}", get_context(long_input, 30, 2));
// Summary
println!("=== Summary ===");
println!("Error reporting features:");
println!(" - Position to line/column conversion");
println!(" - Context extraction around errors");
println!(" - Formatted output with line numbers");
println!(" - Deeper failure point detection");
}
# 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