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

Error pattern combinators #148

Merged
merged 6 commits into from
Jan 21, 2023
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
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