ステップ4:実行時の動作
パーサがエラーを処理し、入力を消費し、最適なパフォーマンスのためにキャッシングを制御する方法を理解します。
解析メソッド
parseAll(...).getOrThrow()
入力全体が消費されることを要求します:
import io.github.mirrgieriana.xarpeg.*
import io.github.mirrgieriana.xarpeg.parsers.*
val number = (+Regex("[0-9]+")).value map { it.toInt() } named "number"
fun main() {
number.parseAll("123").getOrThrow() // ✓ 123を返す
// number.parseAll("123abc").getOrThrow() // ✗ ParseException
// number.parseAll("abc").getOrThrow() // ✗ ParseException
}
例外の種類
ParseException- 現在位置でパーサがマッチしなかった場合、または解析は成功したが末尾の入力が残っている場合
この例外は、詳細なエラー情報のためのcontextプロパティを提供します。
エラーコンテキスト
DefaultParseContextは、ユーザーフレンドリーなエラーメッセージを構築するために解析の失敗を追跡します:
import io.github.mirrgieriana.xarpeg.*
import io.github.mirrgieriana.xarpeg.parsers.*
val letter = (+Regex("[a-z]")).value named "letter"
val digit = (+Regex("[0-9]")).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) // 解析失敗
check((exception.context.errorPosition ?: 0) == 0) // 位置0で失敗
val expected = exception.context.suggestedParsers
.mapNotNull { it.name }
.distinct()
.sorted()
.joinToString(", ")
check(expected == "letter") // "letter"が期待される
}
エラー追跡プロパティ
errorPosition- 解析中に試行された最も遠い位置suggestedParsers-errorPositionで失敗したパーサのセット
解析が進むにつれて:
- パーサが
errorPositionより遠くで失敗した場合、更新され、suggestedParsersがクリアされます - 現在の
errorPositionで失敗したパーサがsuggestedParsersに追加されます - 名前付きパーサは割り当てられた名前を使用して表示されます
例外でのエラーコンテキストの使用
import io.github.mirrgieriana.xarpeg.*
import io.github.mirrgieriana.xarpeg.parsers.*
val number = (+Regex("[0-9]+")).value map { it.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) // 解析失敗
check((exception.context.errorPosition ?: 0) > 0) // エラー位置が追跡される
val suggestions = exception.context.suggestedParsers.orEmpty().mapNotNull { it.name }
check(suggestions.isNotEmpty()) // 提案がある
}
リッチなエラーメッセージ
formatMessage拡張関数を使用すると、エラー位置、期待される要素、該当行のソースコード、エラー箇所を示すキャレット表示を含むユーザーフレンドリーなエラーメッセージを生成できます:
import io.github.mirrgieriana.xarpeg.*
import io.github.mirrgieriana.xarpeg.parsers.*
val number = (+Regex("[0-9]+")).value map { it.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] == " ^")
}
}
formatMessage関数は以下を提供します:
- エラーの行番号と列番号
- 期待される名前付きパーサのリスト(利用可能な場合)
- 実際に見つかった文字(またはEOF)
- エラーが発生した行のソースコード
- エラー位置を示すキャレット(
^)記号
メモ化とキャッシング
デフォルトの動作
DefaultParseContextはデフォルトでメモ化を使用して、バックトラッキングを予測可能にします:
import io.github.mirrgieriana.xarpeg.*
import io.github.mirrgieriana.xarpeg.parsers.*
val parser = (+Regex("[a-z]+")).value named "word"
fun main() {
// メモ化有効(デフォルト)
parser.parseAll("hello").getOrThrow()
}
各(parser, position)ペアがメモ化されるため、同じ位置での繰り返しの試行はメモ化された結果を返します。
メモ化の無効化
文法が大量のバックトラックをしない場合、メモリ使用量を減らすためにメモ化を無効化します:
import io.github.mirrgieriana.xarpeg.*
import io.github.mirrgieriana.xarpeg.parsers.*
val parser = (+Regex("[a-z]+")).value named "word"
fun main() {
parser.parseAll("hello") { s -> DefaultParseContext(s).also { it.useMemoization = false } }.getOrThrow()
}
トレードオフ:
- メモ化有効 - 高メモリ、大量のバックトラックで予測可能なパフォーマンス
- メモ化無効 - 低メモリ、代替案で潜在的なパフォーマンス問題
状態依存メモ化
DefaultParseContextをサブクラス化して可変状態を持たせる場合、その状態が解析結果に影響するならgetState()をオーバーライドしてメモテーブルを状態ごとに分離します。これにより、ある状態でキャッシュされた結果が異なる状態で再利用されることを防ぎます:
import io.github.mirrgieriana.xarpeg.*
import io.github.mirrgieriana.xarpeg.parsers.*
class IndentAwareContext(src: String) : DefaultParseContext(src) {
var indentLevel: Int = 0
override fun getState(): Any = indentLevel
}
fun main() {
val parser = +"hello"
val context = IndentAwareContext("hello")
context.indentLevel = 0
check(context.parseOrNull(parser, 0) != null) // indentLevel=0でキャッシュ
context.indentLevel = 1
check(context.parseOrNull(parser, 0) != null) // indentLevel=1で再評価
}
デフォルトのgetState()はUnitを返すため、すべての結果が単一のメモテーブルを共有します。これは標準的なメモ化と同等です。戻り値はMapのキーとして使用されるため、equalsとhashCodeを適切に実装する必要があります。
エラー伝播
map関数が例外をスローした場合、それは伝播して解析を中止します:
import io.github.mirrgieriana.xarpeg.*
import io.github.mirrgieriana.xarpeg.parsers.*
val divisionByZero = (+Regex("[0-9]+")).value map { value ->
val n = value.toInt()
if (n == 0) error("Cannot divide by zero")
100 / n
} named "number"
fun main() {
divisionByZero.parseAll("10").getOrThrow() // ✓ 10を返す
// divisionByZero.parseAll("0").getOrThrow() // ✗ IllegalStateException
}
マッピング前に検証するか、回復が必要な場合はエラーをキャッチしてラップします。
デバッグのヒント
結果からエラー詳細を検査
解析結果からエラーコンテキストにアクセス:
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) // 解析失敗
check((exception.context.errorPosition ?: 0) == 0) // 位置0でエラー
check(exception.context.suggestedParsers?.any { it.name == "word" }) // "word"を提案
}
巻き戻し動作の確認
optionalとzeroOrMoreが失敗時にどのように巻き戻すかを確認:
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は失敗するが巻き戻し、数値パーサが成功できる
val result = parser.parseAll("123").getOrThrow()
check(result != null) // 成功
}
リファレンスとしてテストを使用
観測された動作についてはテストスイートを確認:
- ErrorContextTest.kt - エラー追跡の例
- ParserTest.kt - 包括的な動作テスト
- MemoizationStateTest.kt - 状態依存メモ化のテスト
ParseContextの拡張
ParseContextはインターフェースであり、その基本実装であるDefaultParseContextはopen classとして宣言されています。そのため、特殊な解析ニーズに応じてカスタム状態を持つ拡張(DefaultParseContextの継承やParseContextの直接実装)が可能です。
例:インデント方式言語のサポート
Python風の言語のインデントレベルを追跡するためにDefaultParseContextを拡張できます:
import io.github.mirrgieriana.xarpeg.*
import io.github.mirrgieriana.xarpeg.parsers.*
fun main() {
class IndentParseContext(
src: String,
) : DefaultParseContext(src) {
private val indentStack = mutableListOf(0)
val currentIndent: Int get() = indentStack.last()
val isInIndentBlock: Boolean get() = indentStack.size > 1
fun pushIndent(indent: Int) {
require(indent > currentIndent)
indentStack.add(indent)
}
fun popIndent() {
require(indentStack.size > 1)
indentStack.removeLast()
}
// 必須: 可変状態のスナップショットを返すことで、インデント状態ごとに
// 独立したメモ化テーブルが使われるようになります。
override fun getState(): Any = indentStack.toList()
}
val ctx = IndentParseContext("source")
check(ctx.currentIndent == 0)
check(!ctx.isInIndentBlock)
ctx.pushIndent(4)
check(ctx.currentIndent == 4)
check(ctx.isInIndentBlock)
ctx.popIndent()
check(ctx.currentIndent == 0)
check(!ctx.isInIndentBlock)
}
完全な実装についてはonline-parserサンプルのソースコードを参照してください。
重要なポイント
parseAll(...).getOrThrow()完全な消費を要求し、失敗時にスロー- エラーコンテキスト
errorPositionとsuggestedParsersを提供 - 名前付きパーサ 割り当てられた名前でエラーメッセージに表示
- メモ化 デフォルトで有効;
useMemoization = falseで無効化。サブクラスでgetState()をオーバーライドすることで状態依存メモ化が可能 mapでの例外 伝播して解析を中止parseOrNullDefaultParseContextとともに詳細なデバッグを可能にする
次のステップ
エラー報告とソースマッピングのための位置情報を抽出する方法を学びます。