Skip to content

Commit

Permalink
Add pretty errors
Browse files Browse the repository at this point in the history
  • Loading branch information
johnspade committed Apr 1, 2024
1 parent 639ebf6 commit 125eed1
Show file tree
Hide file tree
Showing 11 changed files with 226 additions and 38 deletions.
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))

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

0 comments on commit 125eed1

Please sign in to comment.