Step 4: Runtime Behavior

Understand how parsers handle errors, consume input, and control caching for optimal performance.

Parsing Methods

parseAll(...).getOrThrow()

Requires the entire input to be consumed:

import io.github.mirrgieriana.xarpeg.*
import io.github.mirrgieriana.xarpeg.parsers.*

val number = +Regex("[0-9]+") map { it.value.toInt() } named "number"

fun main() {
    number.parseAll("123").getOrThrow()      // ✓ Returns 123
    // number.parseAll("123abc").getOrThrow() // ✗ ParseException
    // number.parseAll("abc").getOrThrow()    // ✗ ParseException
}

Exception Types

This exception provides a context property for detailed error information.

Error Context

ParseContext tracks parsing failures to help build user-friendly error messages:

import io.github.mirrgieriana.xarpeg.*
import io.github.mirrgieriana.xarpeg.parsers.*

val letter = +Regex("[a-z]") map { it.value } named "letter"
val digit = +Regex("[0-9]") map { it.value } named "digit"
val identifier = letter * (letter + digit).zeroOrMore

fun main() {
    val result = identifier.parseAll("1abc")
    val exception = result.exceptionOrNull() as? ParseException

    check(exception != null)  // Parsing fails
    check((exception.context.errorPosition ?: 0) == 0)  // Failed at position 0

    val expected = exception.context.suggestedParsers.orEmpty()
        .mapNotNull { it.name }
        .distinct()
        .sorted()
        .joinToString(", ")

    check(expected == "letter")  // Expected "letter"
}

Error Tracking Properties

As parsing proceeds:

  1. When a parser fails further than errorPosition, it updates and suggestedParsers clears
  2. Parsers failing at the current errorPosition are added to suggestedParsers
  3. Named parsers appear using their assigned names

Using Error Context with Exceptions

import io.github.mirrgieriana.xarpeg.*
import io.github.mirrgieriana.xarpeg.parsers.*

val number = +Regex("[0-9]+") map { it.value.toInt() } named "number"
val operator = (+'*' + +'+') named "operator"
val expr = number * operator * number

fun main() {
    val result = expr.parseAll("42 + 10")
    val exception = result.exceptionOrNull() as? ParseException

    check(exception != null)  // Parsing fails
    check((exception.context.errorPosition ?: 0) > 0)  // Error position tracked
    val suggestions = exception.context.suggestedParsers.orEmpty().mapNotNull { it.name }
    check(suggestions.isNotEmpty())  // Has suggestions
}

Rich Error Messages

Use the formatMessage extension function to generate user-friendly error messages that include error position, expected elements, the source line, and a caret indicator pointing to the error location:

import io.github.mirrgieriana.xarpeg.*
import io.github.mirrgieriana.xarpeg.parsers.*

val number = +Regex("[0-9]+") map { it.value.toInt() } named "number"
val operator = +'+' + +'-'
val expr = number * operator * number

fun main() {
    val input = "42*10"
    try {
        expr.parseAll(input).getOrThrow()
    } catch (exception: ParseException) {
        val message = exception.formatMessage()
        val lines = message.lines()
        check(lines[0] == "Syntax Error at 1:3")
        check(lines[1] == "Expect: \"+\", \"-\"")
        check(lines[2] == "Actual: \"*\"")
        check(lines[3] == "42*10")
        check(lines[4] == "  ^")
    }
}

The formatMessage function provides:

Memoization and Caching

Default Behavior

DefaultParseContext uses memoization by default to make backtracking predictable:

import io.github.mirrgieriana.xarpeg.*
import io.github.mirrgieriana.xarpeg.parsers.*

val parser = +Regex("[a-z]+") map { it.value } named "word"

fun main() {
    // Memoization enabled (default)
    parser.parseAll("hello").getOrThrow()
}

Each (parser, position) pair is memoized, so repeated attempts at the same position return memoized results.

Disabling Memoization

Disable memoization for lower memory usage when your grammar doesn’t backtrack heavily:

import io.github.mirrgieriana.xarpeg.*
import io.github.mirrgieriana.xarpeg.parsers.*

val parser = +Regex("[a-z]+") map { it.value } named "word"

fun main() {
    parser.parseAll("hello") { s -> DefaultParseContext(s).also { it.useMemoization = false } }.getOrThrow()
}

Trade-offs:

Error Propagation

If a map function throws an exception, it bubbles up and aborts parsing:

import io.github.mirrgieriana.xarpeg.*
import io.github.mirrgieriana.xarpeg.parsers.*

val divisionByZero = +Regex("[0-9]+") map { value ->
    val n = value.value.toInt()
    if (n == 0) error("Cannot divide by zero")
    100 / n
} named "number"

fun main() {
    divisionByZero.parseAll("10").getOrThrow()  // ✓ Returns 10
    // divisionByZero.parseAll("0").getOrThrow()  // ✗ IllegalStateException
}

Validate before mapping or catch and wrap errors when recovery is needed.

Debugging Tips

Inspect Error Details from Result

Access error context from parse result:

import io.github.mirrgieriana.xarpeg.*
import io.github.mirrgieriana.xarpeg.parsers.*

val parser = +Regex("[a-z]+") named "word"

fun main() {
    val result = parser.parseAll("123")
    val exception = result.exceptionOrNull() as? ParseException

    check(exception != null)  // Parsing fails
    check((exception.context.errorPosition ?: 0) == 0)  // Error at position 0
    check(exception.context.suggestedParsers?.any { it.name == "word" } == true)  // Suggests "word"
}

Check Rewind Behavior

Confirm how optional and zeroOrMore rewind on failure:

import io.github.mirrgieriana.xarpeg.*
import io.github.mirrgieriana.xarpeg.parsers.*

val parser = (+Regex("[a-z]+") named "letters").optional * +Regex("[0-9]+") named "digits"

fun main() {
    // optional fails but rewinds, allowing number parser to succeed
    val result = parser.parseAll("123").getOrThrow()
    check(result != null)  // Succeeds
}

Use Tests as Reference

Check the test suite for observed behavior:

Key Takeaways

Next Steps

Learn how to extract position information for error reporting and source mapping.

Step 5: Parsing Positions