Step 5: Parsing Positions
Extract location information from parsed results for error reporting, source mapping, and debugging.
Parse Results and Positions
Every successful parse returns a ParseResult<T> containing:
value: T- The parsed valuestart: Int- Starting position in inputend: Int- Ending position in input
Position information is always available, but you typically work with simple types like Parser<Int> until you need the positions.
Simple Transformations with map
The map combinator works with just the value, keeping types simple:
import io.github.mirrgieriana.xarpeg.*
import io.github.mirrgieriana.xarpeg.parsers.*
val number = +Regex("[0-9]+") map { it.value.toInt() } named "number"
fun main() {
val result = number.parseAll("42").getOrThrow()
check(result == 42) // Just the value, no position info
}
Accessing Positions with mapEx
Use mapEx when you need position information. It receives the ParseContext and full ParseResult:
import io.github.mirrgieriana.xarpeg.*
import io.github.mirrgieriana.xarpeg.parsers.*
val identifier = +Regex("[a-zA-Z][a-zA-Z0-9_]*") named "identifier"
val identifierWithPosition = identifier mapEx { ctx, result ->
"${result.value.value}@${result.start}-${result.end}"
}
fun main() {
val result = identifierWithPosition.parseAll("hello").getOrThrow()
check(result == "hello@0-5") // Includes position info
}
Note: +Regex(...) returns Parser<MatchResult>, so access the string with result.value.value.
Getting the Full ParseResult
When you need the complete ParseResult object (including value, start, and end positions), use the .result extension:
import io.github.mirrgieriana.xarpeg.*
import io.github.mirrgieriana.xarpeg.parsers.*
val word = +"hello"
val wordWithResult = word.result
fun main() {
val result = wordWithResult.parseAll("hello").getOrThrow()
check(result.value == "hello")
check(result.start == 0)
check(result.end == 5)
}
The .result extension transforms Parser<T> into Parser<ParseResult<T>>, giving you direct access to all position information without needing to use mapEx.
Extracting Matched Text
Get the original matched substring using the text() extension:
import io.github.mirrgieriana.xarpeg.*
import io.github.mirrgieriana.xarpeg.parsers.*
val number = +Regex("[0-9]+") named "number"
val numberWithText = number mapEx { ctx, result ->
val matched = result.text(ctx)
val value = matched.toInt()
"Parsed '$matched' as $value"
}
fun main() {
val result = numberWithText.parseAll("123").getOrThrow()
check(result == "Parsed '123' as 123") // Matched text extracted
}
Calculating Line and Column Numbers
Build enhanced error reporting with line/column information:
import io.github.mirrgieriana.xarpeg.*
import io.github.mirrgieriana.xarpeg.parsers.*
data class Located<T>(val value: T, val line: Int, val column: Int)
fun <T : Any> Parser<T>.withLocation(): Parser<Located<T>> = this mapEx { ctx, result ->
val text = ctx.src.substring(0, result.start)
val line = text.count { it == '\n' } + 1
val column = text.length - (text.lastIndexOf('\n') + 1) + 1
Located(result.value, line, column)
}
val keyword = +Regex("[a-z]+") map { it.value } named "keyword"
val keywordWithLocation = keyword.withLocation()
fun main() {
val result = keywordWithLocation.parseAll("hello").getOrThrow()
check(result.value == "hello" && result.line == 1 && result.column == 1)
}
Multi-line Position Tracking
Track positions across multiple lines:
import io.github.mirrgieriana.xarpeg.*
import io.github.mirrgieriana.xarpeg.parsers.*
data class Token(val value: String, val line: Int, val col: Int)
fun <T : Any> Parser<T>.withPos(): Parser<Token> = this mapEx { ctx, result ->
val prefix = ctx.src.substring(0, result.start)
val line = prefix.count { it == '\n' } + 1
val col = prefix.length - (prefix.lastIndexOf('\n') + 1) + 1
Token(result.text(ctx), line, col)
}
fun main() {
val word = +Regex("[a-z]+") map { it.value } named "word"
val wordWithPos = word.withPos()
// Parse tracks position in input
val result = wordWithPos.parseAll("hello").getOrThrow()
check(result == Token("hello", 1, 1))
}
Practical Example: Error Messages
Combine position tracking with error context for helpful messages:
import io.github.mirrgieriana.xarpeg.*
import io.github.mirrgieriana.xarpeg.parsers.*
fun main() {
val parser = +Regex("[0-9]+") map { it.value.toInt() } named "number"
fun parseWithErrors(input: String): Result<Int> {
val result = parser.parseAll(input)
val exception = result.exceptionOrNull() as? ParseException
return if (exception != null) {
val pos = exception.context.errorPosition ?: 0
val prefix = input.substring(0, pos)
val line = prefix.count { it == '\n' } + 1
val column = prefix.length - (prefix.lastIndexOf('\n') + 1) + 1
val expected = exception.context.suggestedParsers.orEmpty().mapNotNull { it.name }
Result.failure(Exception(
"Syntax error at line $line, column $column. Expected: ${expected.joinToString()}"
))
} else {
result
}
}
val result = parseWithErrors("abc")
check(result.isFailure) // Parsing fails as expected
}
Best Practices
Use map by default - Keep types simple when positions aren’t needed (example: val simple = +Regex("[0-9]+") map { it.value.toInt() } named "number").
Use mapEx when needed - Extract positions only where required.
Isolate position logic - Create reusable helpers like fun <T : Any> Parser<T>.withLocation(): Parser<Located<T>> for position tracking.
Remember: positions are always there - You don’t need to change your parser’s return type throughout your grammar. Extract position information at boundaries where you need it.
Key Takeaways
ParseResultincludesvalue,start, andendmaptransforms values, keeping types simplemapExaccesses context and position information.resultreturns fullParseResult<T>for direct access to all position data.text(ctx)extracts the matched substring- Line/column calculation requires counting newlines
- Position helpers keep grammar code clean
Next Steps
Discover how PEG parsers naturally handle template strings with embedded expressions.