Skip to content
← Back to Examples

Deepest Error Reporting

RustRubyerror handlingintermediate

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( endINVALID

Unclosed function call - error at '('

define f() @res.name() INVALID

Missing 'end' - error at EOF

Output Format

Position and context of the deepest parse failure.

Structure

position

Byte offset where parsing failed

line

Line number of failure

column

Column number of failure

context

Surrounding input for display

Visual Overview

Deepest Error Reporting diagram

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 longest

PEG 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

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