diff --git a/src/commonMain/kotlin/me/alllex/parsus/parser/ParseResult.kt b/src/commonMain/kotlin/me/alllex/parsus/parser/ParseResult.kt index 3a0a7ce..7b1e184 100644 --- a/src/commonMain/kotlin/me/alllex/parsus/parser/ParseResult.kt +++ b/src/commonMain/kotlin/me/alllex/parsus/parser/ParseResult.kt @@ -3,6 +3,7 @@ package me.alllex.parsus.parser import me.alllex.parsus.token.Token import me.alllex.parsus.token.TokenMatch import me.alllex.parsus.util.replaceNonPrintable +import me.alllex.parsus.util.toPrintableString /** * Result of a parse that is either a [parsed value][ParsedValue] @@ -36,11 +37,11 @@ abstract class ParseError : ParseResult() { appendLine() append(" ".repeat(lookBehind)).append(messageAtOffset) appendLine() - append(" ".repeat(lookBehind)).append("| offset=$offset (or after ignored tokens)") + append(" ".repeat(lookBehind)).append("$arrowDown offset=$offset (or after ignored tokens)") appendLine() appendLine(replaceNonPrintable(inputSection)) if (previousTokenMatch != null) { - append("^".repeat(previousTokenMatch.length.coerceAtLeast(1))) + append(arrowUp.repeat(previousTokenMatch.length.coerceAtLeast(1))) append(" Previous token: ${previousTokenMatch.token} at offset=${previousTokenMatch.offset}") appendLine() } @@ -48,6 +49,9 @@ abstract class ParseError : ParseResult() { } } +private const val arrowDown = "\u2193" +private const val arrowUp = "\u2191" + data class ParseErrorContext( val inputSection: String, val lookBehind: Int, @@ -68,8 +72,8 @@ data class UnmatchedToken( override fun toString(): String = describe() override fun describe(): String = format( - message = "Unmatched token at offset=$offset, when expected: $expected", - messageAtOffset = "Expected token: $expected" + message = "Unmatched token at offset=$offset, when expected: ${expected.toPrintableString()}", + messageAtOffset = "Expected token: ${expected.toPrintableString()}" ) } @@ -81,8 +85,8 @@ data class MismatchedToken( override val offset: Int get() = found.offset override fun toString(): String = describe() override fun describe(): String = format( - message = "Mismatched token at offset=$offset, when expected: $expected, got: ${found.token}", - messageAtOffset = "Expected token: $expected at offset=$offset, got: ${found.token}" + message = "Mismatched token at offset=$offset, when expected: ${expected.toPrintableString()}, got: ${found.token.toPrintableString()}", + messageAtOffset = "Expected token: ${expected.toPrintableString()} at offset=$offset, got: ${found.token.toPrintableString()}" ) } @@ -107,7 +111,11 @@ data class NoViableAlternative( ) } -data class NotEnoughRepetition(override val offset: Int, val expectedAtLeast: Int, val actualCount: Int) : ParseError() { +data class NotEnoughRepetition( + override val offset: Int, + val expectedAtLeast: Int, + val actualCount: Int +) : ParseError() { override fun toString(): String = describe() override fun describe(): String = "Expected at least $expectedAtLeast, found $actualCount" } diff --git a/src/commonMain/kotlin/me/alllex/parsus/util/text.kt b/src/commonMain/kotlin/me/alllex/parsus/util/text.kt index 5fa94a7..9239ef6 100644 --- a/src/commonMain/kotlin/me/alllex/parsus/util/text.kt +++ b/src/commonMain/kotlin/me/alllex/parsus/util/text.kt @@ -1,5 +1,7 @@ package me.alllex.parsus.util +internal fun Any.toPrintableString() = replaceNonPrintable(toString()) + internal fun replaceNonPrintable(string: String): String { return buildString { for (char in string) { diff --git a/src/commonTest/kotlin/me/alllex/parsus/ParseErrorTest.kt b/src/commonTest/kotlin/me/alllex/parsus/ParseErrorTest.kt index 2712c4e..ade665f 100644 --- a/src/commonTest/kotlin/me/alllex/parsus/ParseErrorTest.kt +++ b/src/commonTest/kotlin/me/alllex/parsus/ParseErrorTest.kt @@ -25,16 +25,16 @@ class ParseErrorTest { assertNotParsed("abab").prop(ParseError::describe).isEqualTo( "Unmatched token at offset=2, when expected: LiteralToken('cd')\n" + """ Expected token: LiteralToken('cd') - | offset=2 (or after ignored tokens) + ↓ offset=2 (or after ignored tokens) abab - ^^ Previous token: LiteralToken('ab') at offset=0 + ↑↑ Previous token: LiteralToken('ab') at offset=0 """.trimIndent() + "\n" ) assertNotParsed("cd").prop(ParseError::describe).isEqualTo( "Unmatched token at offset=0, when expected: LiteralToken('ab')\n" + """ Expected token: LiteralToken('ab') - | offset=0 (or after ignored tokens) + ↓ offset=0 (or after ignored tokens) cd """.trimIndent() + "\n" ) @@ -42,9 +42,9 @@ class ParseErrorTest { assertNotParsed("abcdab").prop(ParseError::describe).isEqualTo( "Unmatched token at offset=4, when expected: Token(EOF)\n" + """ Expected token: Token(EOF) - | offset=4 (or after ignored tokens) + ↓ offset=4 (or after ignored tokens) cdab - ^^ Previous token: LiteralToken('cd') at offset=2 + ↑↑ Previous token: LiteralToken('cd') at offset=2 """.trimIndent() + "\n" ) } @@ -63,9 +63,9 @@ class ParseErrorTest { assertNotParsed("ab ab").prop(ParseError::describe).isEqualTo( "Unmatched token at offset=2, when expected: LiteralToken('cd')\n" + """ Expected token: LiteralToken('cd') - | offset=2 (or after ignored tokens) + ↓ offset=2 (or after ignored tokens) ab␣ab - ^^ Previous token: LiteralToken('ab') at offset=0 + ↑↑ Previous token: LiteralToken('ab') at offset=0 """.trimIndent() + "\n" ) } @@ -85,9 +85,9 @@ class ParseErrorTest { assertNotParsed(" \t\r\ncd").prop(ParseError::describe).isEqualTo( "Unmatched token at offset=4, when expected: LiteralToken('ab')\n" + """ Expected token: LiteralToken('ab') - | offset=4 (or after ignored tokens) + ↓ offset=4 (or after ignored tokens) ␣␉␍␤cd - ^^^^ Previous token: RegexToken(ws [\s+]) at offset=0 + ↑↑↑↑ Previous token: RegexToken(ws [\s+]) at offset=0 """.trimIndent() + "\n" ) }