Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add pretty errors #226

Merged
merged 1 commit into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ abstract class ParserBenchmark[T] {
// }

@Benchmark
def zioParserRecursive(): Either[zio.parser.Parser.ParserError[String], T] = {
def zioParserRecursive(): Either[zio.parser.StringParserError[String], T] = {
import zio.parser._
zioSyntax.parseString(value, ParserImplementation.Recursive)
}
Expand All @@ -58,7 +58,7 @@ abstract class ParserBenchmark[T] {
}

@Benchmark
def zioParserOpStack(): Either[zio.parser.Parser.ParserError[String], T] = {
def zioParserOpStack(): Either[zio.parser.StringParserError[String], T] = {
import zio.parser._
zioSyntax.parseString(value, ParserImplementation.StackSafe)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import org.openjdk.jmh.annotations.{
}
import zio.Chunk
import zio.parser.{ParserImplementation, Syntax}
import zio.parser.StringParserError
import zio.parser.Parser.ParserError
import zio.parser.internal.Debug

Expand Down Expand Up @@ -50,19 +51,19 @@ class LuceneQueryBenchmark {
catsParser.query.parseAll(testQuery)

@Benchmark
def zioParse(): Either[ParserError[String], Query] =
def zioParse(): Either[StringParserError[String], Query] =
zioParserQuery.parseString(testQuery)

@Benchmark
def zioParseStrippedRecursive(): Either[ParserError[String], Query] =
def zioParseStrippedRecursive(): Either[StringParserError[String], Query] =
zioParserStrippedQuery.parseString(testQuery, ParserImplementation.Recursive)

@Benchmark
def zioParseStrippedRecursiveChunk(): Either[ParserError[String], Query] =
zioParserStrippedQuery.parseChunk(testQueryChunk)

@Benchmark
def zioParseStrippedOpStack(): Either[ParserError[String], Query] =
def zioParseStrippedOpStack(): Either[StringParserError[String], Query] =
zioParserStrippedQuery.parseString(testQuery, ParserImplementation.StackSafe)

// @Benchmark
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import org.openjdk.jmh.annotations.{
Warmup
}
import zio.Chunk
import zio.parser.StringParserError
import zio.parser.Parser.ParserError
import zio.parser.{Regex, Syntax}

Expand Down Expand Up @@ -68,23 +69,23 @@ class CharParserMicroBenchmarks {
}

@Benchmark
def skipAndTransform(): Either[ParserError[Nothing], String] =
def skipAndTransform(): Either[StringParserError[Nothing], String] =
skipAndTransformSyntax.parseChars(hello)

@Benchmark
def skipAndTransformOrElse(): Either[ParserError[String], String] =
def skipAndTransformOrElse(): Either[StringParserError[String], String] =
skipAndTransformOrElseSyntax.parseChars(world)

@Benchmark
def skipAndTransformRepeat(): Either[ParserError[String], Chunk[String]] =
def skipAndTransformRepeat(): Either[StringParserError[String], Chunk[String]] =
skipAndTransformRepeatSyntax.parseChars(hellos)

@Benchmark
def skipAndTransformZip(): Either[ParserError[String], String10] =
def skipAndTransformZip(): Either[StringParserError[String], String10] =
skipAndTransformZipSyntax.parseChars(hellos)

@Benchmark
def repeatWithSep0(): Either[ParserError[String], Chunk[String]] =
def repeatWithSep0(): Either[StringParserError[String], Chunk[String]] =
repeatWithSep0Syntax.parseString(hellosSep)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -911,7 +911,7 @@ object CalibanDemo extends ZIOAppDefault {
(id: "1000", int: 3, float: 3.14, bool: true, nope: null, enum: YES, list: [1,2,3])
""".trim

val parsed: ZIO[Any, Nothing, Either[Parser.ParserError[String], StringValue]] =
val parsed: ZIO[Any, Nothing, Either[StringParserError[String], StringValue]] =
ZIO
.succeed(CalibanParser.stringValue.parseString(query))
// val parsed = UIO(CalibanSyntax.stringValue.parse("\"\"\"hello\"\"\""))
Expand Down
99 changes: 86 additions & 13 deletions zio-parser/shared/src/main/scala/zio/parser/Parser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -272,38 +272,38 @@ sealed trait Parser[+Err, -In, +Result] extends VersionSpecificParser[Err, In, R

// Execution
/** Run this parser on the given 'input' string */
final def parseString(input: String)(implicit ev: Char <:< In): Either[ParserError[Err], Result] =
final def parseString(input: String)(implicit ev: Char <:< In): Either[StringParserError[Err], Result] =
parseString(input, self.defaultImplementation)

/** Run this parser on the given 'input' string using a specific parser implementation */
final def parseString(input: String, parserImplementation: ParserImplementation)(implicit
ev: Char <:< In
): Either[ParserError[Err], Result] =
): Either[StringParserError[Err], Result] =
parserImplementation match {
case ParserImplementation.StackSafe =>
val parser = new CharParserImpl[Err, Result](
self.compiledOpStack,
input
)
parser.run()
parser.run().left.map(err => StringParserError(input, err))
case ParserImplementation.Recursive =>
val state = recursive.ParserState.fromString(input)
val result = self.optimized.parseRec(state.asInstanceOf[ParserState[In]])

if (state.error != null)
Left(state.error.asInstanceOf[ParserError[Err]])
Left(StringParserError(input, state.error.asInstanceOf[ParserError[Err]]))
else
Right(result)
}

/** Run this parser on the given 'input' chunk of characters */
final def parseChars(input: Chunk[Char])(implicit ev: Char <:< In): Either[ParserError[Err], Result] =
final def parseChars(input: Chunk[Char])(implicit ev: Char <:< In): Either[StringParserError[Err], Result] =
parseChars(input, self.defaultImplementation)

/** Run this parser on the given 'input' chunk of characters using a specific parser implementation */
final def parseChars(input: Chunk[Char], parserImplementation: ParserImplementation)(implicit
ev: Char <:< In
): Either[ParserError[Err], Result] =
): Either[StringParserError[Err], Result] =
parseString(new String(input.toArray), parserImplementation)

/** Run this parser on the given 'input' chunk */
Expand Down Expand Up @@ -505,7 +505,7 @@ object Parser {
state.position += 1
result.asInstanceOf[D2]
} else {
state.error = ParserError.UnexpectedEndOfInput
state.error = ParserError.UnexpectedEndOfInput(state.nameStack)
null.asInstanceOf[D2]
}

Expand All @@ -527,7 +527,7 @@ object Parser {
val position = state.position
val result = state.regex(compiledRegex)
if (result == Regex.NeedMoreInput) {
state.error = ParserError.UnexpectedEndOfInput
state.error = ParserError.UnexpectedEndOfInput(state.nameStack)
} else if (result == Regex.NotMatched) {
onFailure match {
case Some(failure) =>
Expand Down Expand Up @@ -568,7 +568,7 @@ object Parser {
val position = state.position
val result = state.regex(compiledRegex)
if (result == Regex.NeedMoreInput) {
state.error = ParserError.UnexpectedEndOfInput
state.error = ParserError.UnexpectedEndOfInput(state.nameStack)
null.asInstanceOf[Chunk[Char]]
} else if (result == Regex.NotMatched) {
state.error = getFailure(position, state.nameStack)
Expand Down Expand Up @@ -609,7 +609,7 @@ object Parser {
val position = state.position
val result = state.regex(compiledRegex)
if (result == Regex.NeedMoreInput) {
state.error = ParserError.UnexpectedEndOfInput
state.error = ParserError.UnexpectedEndOfInput(state.nameStack)
null.asInstanceOf[Char]
} else if (result == Regex.NotMatched) {
state.error = getFailure(position, state.nameStack)
Expand Down Expand Up @@ -1145,7 +1145,7 @@ object Parser {
}

if (count < min && state.error == null) {
state.error = ParserError.UnexpectedEndOfInput
state.error = ParserError.UnexpectedEndOfInput(state.nameStack)
} else {
if (count >= min) {
state.error = null
Expand Down Expand Up @@ -1391,7 +1391,7 @@ object Parser {
def map[Err2](f: Err => Err2): ParserError[Err2] = self match {
case ParserError.Failure(nameStack, position, failure) => ParserError.Failure(nameStack, position, f(failure))
case ParserError.UnknownFailure(nameStack, position) => ParserError.UnknownFailure(nameStack, position)
case ParserError.UnexpectedEndOfInput => ParserError.UnexpectedEndOfInput
case ParserError.UnexpectedEndOfInput(nameStack) => ParserError.UnexpectedEndOfInput(nameStack)
case ParserError.NotConsumedAll(nameStack, position) => ParserError.NotConsumedAll(nameStack, position)
case ParserError.AllBranchesFailed(left, right) => ParserError.AllBranchesFailed(left.map(f), right.map(f))
}
Expand Down Expand Up @@ -1423,7 +1423,7 @@ object Parser {
final case class UnknownFailure(nameStack: List[String], position: Int) extends ParserError[Nothing]

/** The input stream ended before the parser finished */
case object UnexpectedEndOfInput extends ParserError[Nothing]
final case class UnexpectedEndOfInput(nameStack: List[String]) extends ParserError[Nothing]

/** The parser was supposed to consume the full input but it did not.
*
Expand All @@ -1439,3 +1439,76 @@ object Parser {
final case class AllBranchesFailed[Err](left: ParserError[Err], right: ParserError[Err]) extends ParserError[Err]
}
}

final case class StringParserError[+Err](input: String, error: ParserError[Err]) {
def pretty: String = {
val sb = new StringBuilder

def errorBuilder(error: ParserError[Err]): Unit =
error match {
case ParserError.Failure(nameStack, position, failure) =>
sb.append(s"Failure at position $position: $failure")
if (nameStack.nonEmpty) {
sb.append(" in ")
sb.append(nameStack.mkString(" -> "))
}
case ParserError.UnknownFailure(nameStack, position) =>
sb.append(s"Unknown failure at position $position")
if (nameStack.nonEmpty) {
sb.append(" in ")
sb.append(nameStack.mkString(" -> "))
}
case ParserError.UnexpectedEndOfInput(nameStack) =>
sb.append("Unexpected end of input")
if (nameStack.nonEmpty) {
sb.append(" in ")
sb.append(nameStack.mkString(" -> "))
}
case ParserError.NotConsumedAll(nameStack, position) =>
sb.append(s"Parser did not consume all input at position $position")
if (nameStack.nonEmpty) {
sb.append(" in ")
sb.append(nameStack.mkString(" -> "))
}
case ParserError.AllBranchesFailed(left, right) =>
sb.append("All branches failed: ")
errorBuilder(left)
sb.append(" and ")
errorBuilder(right)
}

def printError(pos: Int) = {
val lines = input.split("\n")
val positions = lines.scanLeft(0)(_ + _.length + 1) // +1 for newline character
val errorLineIndex = positions.indexWhere(_ > pos) - 1

val contextRange = (math.max(errorLineIndex - 2, 0), math.min(errorLineIndex + 2, lines.length - 1))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For multiline input, output the previous two lines and the next two lines, replace the rest with an ellipsis


if (contextRange._1 > 0) sb.append("...\n")

lines.slice(contextRange._1, contextRange._2 + 1).zipWithIndex.foreach { case (line, index) =>
val actualIndex = contextRange._1 + index
sb.append(line).append("\n")
if (actualIndex == errorLineIndex) {
val columnPos = pos - positions(errorLineIndex)
sb.append(" " * columnPos)
sb.append("^\n")
sb.append("error: ")
errorBuilder(error)
sb.append("\n")
}
}

if (contextRange._2 < lines.length - 1) sb.append("...\n")
}

error match {
case ParserError.Failure(_, position, _) => printError(position)
case ParserError.UnknownFailure(_, position) => printError(position)
case ParserError.NotConsumedAll(_, position) => printError(position)
case _ => errorBuilder(error)
}

sb.toString()
}
}
8 changes: 4 additions & 4 deletions zio-parser/shared/src/main/scala/zio/parser/Syntax.scala
Original file line number Diff line number Diff line change
Expand Up @@ -384,23 +384,23 @@ class Syntax[+Err, -In, +Out, Value] private (
)

/** Run this parser on the given 'input' string */
final def parseString(input: String)(implicit ev: Char <:< In): Either[ParserError[Err], Value] =
final def parseString(input: String)(implicit ev: Char <:< In): Either[StringParserError[Err], Value] =
asParser.parseString(input)

/** Run this parser on the given 'input' string using a specific parser implementation */
final def parseString(input: String, parserImplementation: ParserImplementation)(implicit
ev: Char <:< In
): Either[ParserError[Err], Value] =
): Either[StringParserError[Err], Value] =
asParser.parseString(input, parserImplementation)

/** Run this parser on the given 'input' chunk of characters */
final def parseChars(input: Chunk[Char])(implicit ev: Char <:< In): Either[ParserError[Err], Value] =
final def parseChars(input: Chunk[Char])(implicit ev: Char <:< In): Either[StringParserError[Err], Value] =
asParser.parseChars(input)

/** Run this parser on the given 'input' chunk of characters using a specific parser implementation */
final def parseChars(input: Chunk[Char], parserImplementation: ParserImplementation)(implicit
ev: Char <:< In
): Either[ParserError[Err], Value] = asParser.parseChars(input, parserImplementation)
): Either[StringParserError[Err], Value] = asParser.parseChars(input, parserImplementation)

/** Run this parser on the given 'input' chunk */
final def parseChunk[In0 <: In](input: Chunk[In0])(implicit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ final class CharParserImpl[Err, Result](parser: InitialParser, source: String) {
position = position + 1
lastSuccess1 = source(position - 1).asInstanceOf[AnyRef]
} else {
lastFailure1 = ParserError.UnexpectedEndOfInput
lastFailure1 = ParserError.UnexpectedEndOfInput(nameStack)
}
opStack.pop()

Expand All @@ -157,7 +157,7 @@ final class CharParserImpl[Err, Result](parser: InitialParser, source: String) {
)
}
} else {
failure = ParserError.UnexpectedEndOfInput
failure = ParserError.UnexpectedEndOfInput(nameStack)
}
pos = pos + 1
}
Expand All @@ -174,7 +174,7 @@ final class CharParserImpl[Err, Result](parser: InitialParser, source: String) {
case MatchRegex(regex, pushAs, failAs) =>
val result = regex.test(position, source)
if (result == Regex.NeedMoreInput) {
lastFailure1 = ParserError.UnexpectedEndOfInput
lastFailure1 = ParserError.UnexpectedEndOfInput(nameStack)
} else if (result == Regex.NotMatched) {
failAs match {
case Some(failure) =>
Expand Down Expand Up @@ -355,7 +355,7 @@ final class CharParserImpl[Err, Result](parser: InitialParser, source: String) {
val result = builder.result()
if (result.length < min) {
// not enough elements
lastFailure1 = ParserError.UnexpectedEndOfInput
lastFailure1 = ParserError.UnexpectedEndOfInput(nameStack)
opStack.pop()
} else {
lastIgnoredError = lastFailure1
Expand Down
8 changes: 4 additions & 4 deletions zio-parser/shared/src/test/scala/zio/parser/ParserSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ object ParserSpec extends ZIOSpecDefault {
isLeft(equalTo(ParserError.Failure(Nil, 0, "not a")))
) @@ TestAspect.ignore, // With compiling to Regex it fails with UnexpectedEndOfInput - to be discussed
parserTest("repeat immediate end of stream", Syntax.char('a', "not a").repeat, "")(
isLeft(equalTo(ParserError.UnexpectedEndOfInput))
isLeft(equalTo(ParserError.UnexpectedEndOfInput(Nil)))
),
parserTest("repeat 1", charA.repeat, "abc")(
isRight(equalTo(Chunk('a')))
Expand Down Expand Up @@ -412,7 +412,7 @@ object ParserSpec extends ZIOSpecDefault {
isRight(equalTo("123"))
),
parserTest("digits, failing", Syntax.digit.+.string, "abc123")(
isLeft(equalTo(ParserError.UnexpectedEndOfInput))
isLeft(equalTo(ParserError.UnexpectedEndOfInput(Nil)))
)
),
parserTest("Not, inner failing", Syntax.string("hello", ()).not("it was hello"), "world")(
Expand Down Expand Up @@ -461,7 +461,7 @@ object ParserSpec extends ZIOSpecDefault {
): Spec[Any, Nothing] =
test(name) {
// Debug.printParserTree(syntax.asParser.optimized)
assert(syntax.parseString(input, implementation))(assertion)
assert(syntax.parseString(input, implementation).left.map(_.error))(assertion)
}

private def createParserTest_[E, T](
Expand All @@ -470,7 +470,7 @@ object ParserSpec extends ZIOSpecDefault {
assertion: Assertion[Either[ParserError[E], T]]
): Spec[Any, Nothing] =
test(name)(
assert(parser.parseString(input, implementation))(assertion)
assert(parser.parseString(input, implementation).left.map(_.error))(assertion)
)
// test(name)(assert(syntax.parseString(input))(assertion))
}
Loading
Loading