Skip to content

Commit

Permalink
Error pattern combinators (#148)
Browse files Browse the repository at this point in the history
* Added the VerifiedErrors extension class in patterns object, added correct partial amending semantics

* satisfied MiMA

* Changed names of new combinators, no need to jump through so many hoops now!

* verify -> verified

* Added intrinsic, needs testing

* Added correct partial amend behaviour onto deprecated deoptimised combinators
  • Loading branch information
j-mie6 authored Jan 21, 2023
1 parent 764ba17 commit 367d758
Show file tree
Hide file tree
Showing 10 changed files with 144 additions and 115 deletions.
3 changes: 3 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ inThisBuild(List(
ProblemFilters.exclude[MissingClassProblem]("parsley.token.predicate$_CharSet$"),
ProblemFilters.exclude[MissingFieldProblem]("parsley.token.predicate._CharSet"),
ProblemFilters.exclude[MissingClassProblem]("parsley.token.errors.ErrorConfig$"),
ProblemFilters.exclude[DirectMissingMethodProblem]("parsley.errors.combinator#ErrorMethods.unexpected"),
ProblemFilters.exclude[MissingClassProblem]("parsley.token.errors.FilterOps"),
ProblemFilters.exclude[MissingClassProblem]("parsley.token.errors.FilterOps$"),
),
tlVersionIntroduced := Map(
"2.13" -> "1.5.0",
Expand Down
62 changes: 11 additions & 51 deletions parsley/shared/src/main/scala/parsley/errors/combinator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/
package parsley.errors

import parsley.Parsley, Parsley.attempt
import parsley.Parsley

import parsley.internal.deepembedding.{frontend, singletons}

Expand Down Expand Up @@ -443,20 +443,6 @@ object combinator {
*/
def hide: Parsley[A] = this.label("")

// TODO: move all of these to a `VerifiedErrorWidgets` class?
// TODO: it should have the partial amend semantics, because `amendAndDislodge` can restore the other semantics anyway
// Document that `attempt` may be used when this is an informative but not terminal error.
private [parsley] def fail(msggen: A => Seq[String]): Parsley[Nothing] = {
// holy hell, the hoops I jump through to be able to implement things
val r = parsley.registers.Reg.make[(Int, A, Int)]
val fails = Parsley.notFollowedBy(r.put(parsley.position.internalOffsetSpan(this.hide)))
(fails <|> r.get.flatMap { case (os, x, oe) =>
val msg0 +: msgs = msggen(x)
combinator.fail(oe - os, msg0, msgs: _*)
}) *> Parsley.empty
}
private [parsley] def fail(msg: String, msgs: String*): Parsley[Nothing] = attempt(this.hide).fail(_ => msg +: msgs)

// $COVERAGE-OFF$
/** This combinator parses this parser and then fails, using the result of this parser to customise the error message.
*
Expand All @@ -469,28 +455,14 @@ object combinator {
* @note $partialAmend
* @group fail
* @deprecated this combinator has not proven to be particularly useful, and will be replaced by a more appropriate,
* not exactly the same, `fail` combinator.
* not exactly the same, `verifiedFail` combinator.
*/
@deprecated("This combinator will be removed in 5.0.0, without direct replacement", "4.2.0")
def !(msggen: A => String): Parsley[Nothing] = //new Parsley(new frontend.FastFail(con(p).internal, msggen))
parsley.position.internalOffsetSpan(p).flatMap { case (os, x, oe) =>
def !(msggen: A => String): Parsley[Nothing] = partialAmendThenDislodge {
parsley.position.internalOffsetSpan(entrench(con(p))).flatMap { case (os, x, oe) =>
combinator.fail(oe - os, msggen(x))
}

// TODO: I think this can probably be deprecated for future removal soon...
// It will be replaced by one that generates reasons too!
/** This combinator parses this parser and then fails, using the result of this parser to customise the unexpected component
* of the error message.
*
* @group fail
* @see [[unexpectedLegacy `unexpectedLegacy`]]
* @deprecated this combinator has not proven to be particularly useful in its current state, and will be replaced by a more
* appropriate, not exactly the same, `unexpected` combinator in 4.4.0. This will be removed from the source API in
* 4.3.0 to reduce risk of conflation with the new combinator, and legitimate uses of this combinator should switch
* to `unexpectedLegacy` instead, which will be removed in 5.0.0.
*/
@deprecated("This combinator will be binary removed in 5.0.0 and source removed in 4.3.0, use unexpectedLegacy until 5.0.0", "4.2.0")
def unexpected(msggen: A => String): Parsley[Nothing] = this.unexpectedLegacy(msggen)
}

/** This combinator parses this parser and then fails, using the result of this parser to customise the unexpected component
* of the error message.
Expand All @@ -503,28 +475,16 @@ object combinator {
* @return a parser that always fails, with the given generator used to produce an unexpected message if this parser succeeded.
* @note $partialAmend
* @group fail
* @deprecated this combinator has not proven to be particularly useful and will be removed in 5.0.0.
* @deprecated this combinator has not proven to be particularly useful and will be removed in 5.0.0. There is a similar, but not
* exact replacement called `verifiedUnexpected`.
* @since 4.2.0
*/
@deprecated("This combinator will be removed in 5.0.0", "4.2.0")
def unexpectedLegacy(msggen: A => String): Parsley[Nothing] =
parsley.position.internalOffsetSpan(p).flatMap { case (os, x, oe) =>
@deprecated("This combinator will be removed in 5.0.0, without direct replacement", "4.2.0")
def unexpected(msggen: A => String): Parsley[Nothing] = partialAmendThenDislodge {
parsley.position.internalOffsetSpan(entrench(con(p))).flatMap { case (os, x, oe) =>
combinator.unexpected(oe - os, msggen(x))
}
// $COVERAGE-ON$

// TODO: Documentation and testing ahead of future release
// like notFollowedBy, but does consume input on "success" and always fails (FIXME: this needs intrinsic support to get right)
// it should also have the partial amend semantics, because `amendAndDislodge` can restore the other semantics anyway
// Document that `attempt` may be used when this is an informative but not terminal error.
private def unexpected(reason: Option[A => String]) = {
// holy hell, the hoops I jump through to be able to implement things
val r = parsley.registers.Reg.make[A]
val fails = Parsley.notFollowedBy(r.put(this.hide))
reason.fold(fails)(rgen => fails <|> r.get.flatMap(x => Parsley.empty.explain(rgen(x)))) *> Parsley.empty
}
private [parsley] def unexpected: Parsley[Nothing] = this.unexpected(None)
private [parsley] def unexpected(reason: String): Parsley[Nothing] = this._unexpected(_ => reason)
private [parsley] def _unexpected(reason: A => String): Parsley[Nothing] = this.unexpected(Some(reason))
// $COVERAGE-ON$
}
}
26 changes: 26 additions & 0 deletions parsley/shared/src/main/scala/parsley/errors/patterns.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package parsley.errors

import parsley.Parsley

import parsley.internal.deepembedding.frontend

// TODO: document
object patterns {
implicit final class VerifiedErrors[P, A](p: P)(implicit con: P => Parsley[A]) {
private def verified(msggen: Either[A => Seq[String], Option[A => String]]) = new Parsley(new frontend.VerifiedError(con(p).internal, msggen))

// TODO: it should have the partial amend semantics, because `amendAndDislodge` can restore the other semantics anyway
// Document that `attempt` may be used when this is an informative but not terminal error.
def verifiedFail(msggen: A => Seq[String]): Parsley[Nothing] = verified(Left(msggen))
def verifiedFail(msg: String, msgs: String*): Parsley[Nothing] = this.verifiedFail(_ => msg +: msgs)

// TODO: Documentation and testing ahead of future release
// like notFollowedBy, but does consume input on "success" and always fails
// it should also have the partial amend semantics, because `amendAndDislodge` can restore the other semantics anyway
// Document that `attempt` may be used when this is an informative but not terminal error.
private def verifiedUnexpected(reason: Option[A => String]) = verified(Right(reason))
def verifiedUnexpected: Parsley[Nothing] = this.verifiedUnexpected(None)
def verifiedUnexpected(reason: String): Parsley[Nothing] = this.verifiedUnexpected(_ => reason)
def verifiedUnexpected(reason: A => String): Parsley[Nothing] = this.verifiedUnexpected(Some(reason))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import parsley.token.errors.{Hidden, Label}

import parsley.internal.deepembedding.singletons._
import parsley.internal.machine.instructions

private [deepembedding] final class ErrorLabel[A](val p: StrictParsley[A], private [ErrorLabel] val label: String) extends ScopedUnary[A, A] {
// This needs to save the hints because error label will relabel the first hint, which because the list is ordered would be the hints that came _before_
// entering labels context. Instead label should relabel the first hint generated _within_ its context, then merge with the originals after
Expand Down Expand Up @@ -77,6 +78,18 @@ private [deepembedding] final class ErrorLexical[A](val p: StrictParsley[A]) ext
// $COVERAGE-ON$
}

private [deepembedding] final class VerifiedError[A](val p: StrictParsley[A], msggen: Either[A => scala.Seq[String], Option[A => String]])
extends ScopedUnary[A, Nothing] {
override def setup(label: Int): instructions.Instr = new instructions.PushHandlerAndState(label, saveHints = true, hideHints = true)
override def instr: instructions.Instr = instructions.MakeVerifiedError(msggen)
override def instrNeedsLabel: Boolean = false
override def handlerLabel(state: CodeGenState): Int = state.getLabel(instructions.NoVerifiedError)

// $COVERAGE-OFF$
final override def pretty(p: String): String = s"verifiedError($p)"
// $COVERAGE-ON$
}

private [backend] object ErrorLabel {
def apply[A](p: StrictParsley[A], label: String): ErrorLabel[A] = new ErrorLabel(p, label)
def unapply[A](self: ErrorLabel[A]): Some[(StrictParsley[A], String)] = Some((self.p, self.label))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ private [deepembedding] final class Look[A](val p: StrictParsley[A]) extends Sco
// $COVERAGE-ON$
}
private [deepembedding] final class NotFollowedBy[A](val p: StrictParsley[A]) extends Unary[A, Unit] {
override def optimise: StrictParsley[Unit] = p match {
/*override def optimise: StrictParsley[Unit] = p match {
case _: MZero => new Pure(())
case _ => this
}
}*/
final override def codeGen[Cont[_, +_]: ContOps, R](implicit instrs: InstrBuffer, state: CodeGenState): Cont[R, Unit] = {
val handler = state.freshLabel()
instrs += new instructions.PushHandlerAndState(handler, saveHints = true, hideHints = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@ private [parsley] final class ErrorDislodge[A](p: LazyParsley[A]) extends Unary[
private [parsley] final class ErrorLexical[A](p: LazyParsley[A]) extends Unary[A, A](p) {
override def make(p: StrictParsley[A]): StrictParsley[A] = new backend.ErrorLexical(p)
}

private [parsley] final class VerifiedError[A](p: LazyParsley[A], msggen: Either[A => Seq[String], Option[A => String]]) extends Unary[A, Nothing](p) {
override def make(p: StrictParsley[A]): StrictParsley[Nothing] = new backend.VerifiedError(p, msggen)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ package parsley.internal.machine.instructions
import parsley.internal.errors.UnexpectDesc
import parsley.internal.machine.Context
import parsley.internal.machine.XAssert._
import parsley.internal.machine.errors.EmptyError
import parsley.internal.machine.errors.{ClassicExpectedError, ClassicExpectedErrorWithReason, ClassicFancyError}

private [internal] final class RelabelHints(label: String) extends Instr {
private [this] val isHide: Boolean = label.isEmpty
Expand Down Expand Up @@ -160,3 +162,45 @@ private [internal] final class Unexpected(msg: String, width: Int) extends Instr
override def toString: String = s"Unexpected($msg)"
// $COVERAGE-ON$
}

private [internal] class MakeVerifiedError private (msggen: Either[Any => Seq[String], Option[Any => String]]) extends Instr {
override def apply(ctx: Context): Unit = {
ensureRegularInstruction(ctx)
val state = ctx.states
//ctx.restoreState()
ctx.states = ctx.states.tail
ctx.restoreHints()
// A previous success is a failure
ctx.handlers = ctx.handlers.tail
val caretWidth = ctx.offset - state.offset
val x = ctx.stack.upeek
val err = msggen match {
case Left(f) => new ClassicFancyError(ctx.offset, state.line, state.col, caretWidth, f(x): _*)
case Right(Some(f)) => new ClassicExpectedErrorWithReason(ctx.offset, state.line, state.col, None, f(x), caretWidth)
case Right(None) => new ClassicExpectedError(ctx.offset, state.line, state.col, None, caretWidth)
}
ctx.fail(err)
}
// $COVERAGE-OFF$
override def toString: String = "VerifiedErrorHandler"
// $COVERAGE-ON$
}
private [internal] object MakeVerifiedError {
def apply[A](msggen: Either[A => Seq[String], Option[A => String]]): MakeVerifiedError = {
new MakeVerifiedError(msggen.asInstanceOf[Either[Any => Seq[String], Option[Any => String]]])
}
}

private [internal] object NoVerifiedError extends Instr {
override def apply(ctx: Context): Unit = {
ensureHandlerInstruction(ctx)
// If a verified error goes wrong, then it should appear like nothing happened
ctx.restoreState()
ctx.restoreHints()
ctx.errs.error = new EmptyError(ctx.offset, ctx.line, ctx.col, unexpectedWidth = 0)
ctx.fail()
}
// $COVERAGE-OFF$
override def toString: String = "VerifiedErrorHandler"
// $COVERAGE-ON$
}
Loading

0 comments on commit 367d758

Please sign in to comment.