Skip to content
← Back to Examples

Error Reporting

RustRubyerror handlingintermediate

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 focus

Examples

valid_identifierVALID

Valid input - no error

42VALID

Valid number

hello world!INVALID

Invalid: unexpected character

# comment\nidentifier\n42\nbad@symbolINVALID

Multi-line with error

Output Format

The parser produces detailed ParseErrorInfo with position, line/column, and surrounding context.

Structure

ParseErrorInfo

Struct with message, position, line, column, context

context

String showing surrounding lines with error highlighted

Visual Overview

Error Reporting diagram

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

Rust
//! 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");
}
Ruby
# 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