Skip to content

Commit

Permalink
Error rework (#61)
Browse files Browse the repository at this point in the history
* Added since labelling, and added _unsafe_ ErrorLabel API, as well as label method and deprecated old label function

* Switched over to the unsafe API for the non-terminals in token and character

* Added error instruction, and introduced new state into the machine

* Removed bad test

* Made some significant progress, I think anyway

* Added assertions to ensure stack validity, also token instrs don't push errors, giving NPEs

* Correctly ensured the consistency of the error stacks

* Fixed bug with expected messages from JumpTable

* Made string errors better

* I hate 2.12.13 honestly, when can I stop supporting it...

* Added in hints mechanism, the iterative combinators need to interact with it, and we need to thoroughly test everything

* Fixed up a few more things

* Fixed hinting so that ors and jumptables cannot feel their inbound hints until the end stage: this corrects label application to those hints

* The lookahead combinators no longer produce hints, but they can consume them

* Switched over to new mechanism fully, and ignored some of the tests for the time being (they aren't accurate at the moment, but are passing fine)

* Temporarily enabled release on this branch, to get a snapshot published for testing

* explicit type signature for 0.27

* changed a max into a min... oops!

* Added a notch before the caret, hopefully making it easier to see

* Off by one on the newline finding

* Fixed bugs in labelling mechanism, as well as some bugs in the hinting

* Ensured that unexpected prompts don't cross over a newline

* Sorted alternatives in lexographical order, not that it really changes anything...

* Fixed sorting, and ensured they are done in reverse order

* Removed indent in error lines
  • Loading branch information
j-mie6 authored Feb 7, 2021
1 parent e1d497b commit 30e5c72
Show file tree
Hide file tree
Showing 40 changed files with 911 additions and 350 deletions.
1 change: 1 addition & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
branches:
- master
- error-rework
tags:
- '*'
workflow_dispatch:
Expand Down
2 changes: 2 additions & 0 deletions src/main/deprecated/parsley/Registers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ package parsley
* independent parsers. You should be careful to parameterise the
* registers in shared parsers and allocate fresh ones for each "top-level"
* parser you will run.
* @since 2.0.0
*/
@deprecated("This class will be removed in Parsley 3.0, use `parsley.registers.Reg` instead", "v2.2.0")
final class Reg[A] private [Reg] extends registers.Reg[A]
Expand All @@ -22,6 +23,7 @@ object Reg {
/**
* @tparam A The type to be contained in this register during runtime
* @return A new register which can contain the given type
* @since 2.0.0
*/
@deprecated("This method will be removed in Parsley 3.0, use `parsley.registers.Reg.make` instead", "v2.2.0")
def make[A]: Reg[A] = new Reg
Expand Down
33 changes: 25 additions & 8 deletions src/main/scala/parsley/Parsley.scala
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ final class Parsley[+A] private [parsley] (private [parsley] val internal: deepe
* The file name is used to annotate any error messages.
* @param file The file to load and run against
* @return Either a success with a value of type `A` or a failure with error message
* @since 2.3.0
*/
def parseFromFile(file: File): Result[A] = new Context(internal.threadSafeInstrs, Source.fromFile(file).toArray, Some(file.getName)).runParser()
}
Expand Down Expand Up @@ -158,13 +159,15 @@ object Parsley
* the parse action of both parsers, in order, but discards the result of the invokee.
* @param q The parser whose result should be returned
* @return A new parser which first parses `p`, then `q` and returns the result of `q`
* @since 2.4.0
*/
def ~>[B](q: Parsley[B]): Parsley[B] = this *> q
/**
* This is the parser that corresponds to a more optimal version of `(p <~> q).map(_._1)`. It performs
* the parse action of both parsers, in order, but discards the result of the second parser.
* @param q The parser who should be executed but then discarded
* @return A new parser which first parses `p`, then `q` and returns the result of the `p`
* @since 2.4.0
*/
def <~[B](q: Parsley[B]): Parsley[A] = this <* q
/**This parser corresponds to `lift2(_+:_, p, ps)`.*/
Expand All @@ -173,7 +176,9 @@ object Parsley
def <::>[B >: A](ps: =>Parsley[List[B]]): Parsley[List[B]] = lift.lift2[A, List[B], List[B]](_ :: _, p, ps)
/**This parser corresponds to `lift2((_, _), p, q)`. For now it is sugar, but in future may be more optimal*/
def <~>[A_ >: A, B](q: =>Parsley[B]): Parsley[(A_, B)] = lift.lift2[A_, B, (A_, B)]((_, _), p, q)
/**This combinator is an alias for `<~>`*/
/** This combinator is an alias for `<~>`
* @since 2.3.0
*/
def zip[A_ >: A, B](q: =>Parsley[B]): Parsley[(A_, B)] = this <~> q
/** Filter the value of a parser; if the value returned by the parser matches the predicate `pred` then the
* filter succeeded, otherwise the parser fails with an empty error
Expand All @@ -192,23 +197,23 @@ object Parsley
* is mapped over its result. Roughly the same as a `filter` then a `map`.
* @param pf The partial function
* @return The result of applying `pf` to this parsers value (if possible), or fails
* @since 1.7
* @since 2.0.0
*/
def collect[B](pf: PartialFunction[A, B]): Parsley[B] = this.filter(pf.isDefinedAt).map(pf)
/** Attempts to first filter the parser to ensure that `pf` is defined over it. If it is, then the function `pf`
* is mapped over its result. Roughly the same as a `guard` then a `map`.
* @param pf The partial function
* @param msg The message used for the error if the input failed the check
* @return The result of applying `pf` to this parsers value (if possible), or fails
* @since 1.7
* @since 2.4.0
*/
def collectMsg[B](msg: String)(pf: PartialFunction[A, B]): Parsley[B] = this.guard(pf.isDefinedAt(_), msg).map(pf)
/** Attempts to first filter the parser to ensure that `pf` is defined over it. If it is, then the function `pf`
* is mapped over its result. Roughly the same as a `guard` then a `map`.
* @param pf The partial function
* @param msggen Generator function for error message, generating a message based on the result of the parser
* @return The result of applying `pf` to this parsers value (if possible), or fails
* @since 1.7
* @since 2.4.0
*/
def collectMsg[B](msggen: A => String)(pf: PartialFunction[A, B]): Parsley[B] = this.guard(pf.isDefinedAt(_), msggen).map(pf)
/** Similar to `filter`, except the error message desired is also provided. This allows you to name the message
Expand Down Expand Up @@ -245,10 +250,13 @@ object Parsley
def >?>(pred: A => Boolean, msg: String): Parsley[A] = this.guard(pred, msg)
/**Alias for guard combinator, taking a dynamic message generator.*/
def >?>(pred: A => Boolean, msggen: A => String): Parsley[A] = this.guard(pred, msggen)
/**Sets the expected message for a parser. If the parser fails then `expected msg` will added to the error*/
def ?(msg: String): Parsley[A] = new Parsley(new deepembedding.ErrorRelabel(p.internal, msg))
/**Alias for `label`*/
def ?(msg: String): Parsley[A] = /*new Parsley(new deepembedding.UnsafeErrorRelabel(p.internal, msg))*/this.label(msg)
/**Sets the expected message for a parser. If the parser fails then `expected msg` will added to the error
* @since 2.6.0 */
def label(msg: String): Parsley[A] = new Parsley(new deepembedding.ErrorLabel(p.internal, msg))
/**Hides the "expected" error message for a parser.*/
def hide: Parsley[A] = ?("")
def hide: Parsley[A] = this.label("") //THIS MUST BE LABEL
/** Same as `fail`, except allows for a message generated from the result of the failed parser. In essence, this
* is equivalent to `p >>= (x => fail(msggen(x))` but requires no expensive computations from the use of `>>=`.
* @param msggen The generator function for error message, creating a message based on the result of invokee
Expand Down Expand Up @@ -294,6 +302,7 @@ object Parsley
* @param k base case for iteration
* @param f combining function
* @return the result of folding the results of `p` with `f` and `k`
* @since 2.1.0
*/
def foldRight1[B](k: B)(f: (A, B) => B): Parsley[B] = {
lazy val q: Parsley[A] = p
Expand All @@ -309,6 +318,7 @@ object Parsley
* @param k base case for iteration
* @param f combining function
* @return the result of folding the results of `p` with `f` and `k`
* @since 2.1.0
*/
def foldLeft1[B](k: B)(f: (B, A) => B): Parsley[B] = {
lazy val q: Parsley[A] = p
Expand All @@ -320,6 +330,7 @@ object Parsley
*
* @param op combining function
* @return the result of reducing the results of `p` with `op`
* @since 2.3.0
*/
def reduceRight[B >: A](op: (A, B) => B): Parsley[B] = some(p).map(_.reduceRight(op))
/**
Expand All @@ -329,6 +340,7 @@ object Parsley
*
* @param op combining function
* @return the result of reducing the results of `p` with `op` wrapped in `Some` or `None` otherwise
* @since 2.3.0
*/
def reduceRightOption[B >: A](op: (A, B) => B): Parsley[Option[B]] = option(this.reduceRight(op))
/**
Expand All @@ -337,6 +349,7 @@ object Parsley
*
* @param op combining function
* @return the result of reducing the results of `p` with `op`
* @since 2.3.0
*/
def reduceLeft[B >: A](op: (B, A) => B): Parsley[B] = chain.left1(p, pure(op))
/**
Expand All @@ -346,13 +359,14 @@ object Parsley
*
* @param op combining function
* @return the result of reducing the results of `p` with `op` wrapped in `Some` or `None` otherwise
* @since 2.3.0
*/
def reduceLeftOption[B >: A](op: (B, A) => B): Parsley[Option[B]] = option(this.reduceLeft(op))
/**
* This casts the result of the parser into a new type `B`. If the value returned by the parser
* is castable to type `B`, then this cast is performed. Otherwise the parser fails.
* @tparam B The type to attempt to cast into
* @since 1.7
* @since 2.0.0
*/
def cast[B: ClassTag]: Parsley[B] = this.collect {
case x: B => x
Expand Down Expand Up @@ -455,8 +469,11 @@ object Parsley
* in which case the keyword is actually an identifier. We can program this behaviour as follows:
* {{{attempt(kw *> notFollowedBy(alphaNum))}}}*/
def notFollowedBy(p: Parsley[_]): Parsley[Unit] = new Parsley(new deepembedding.NotFollowedBy(p.internal))
// $COVERAGE-OFF$
/**Alias for `p ? msg`.*/
@deprecated("This method will be removed in Parsley 3.0, use `.label` or `?` instead", "v2.6.0")
def label[A](p: Parsley[A], msg: String): Parsley[A] = p ? msg
// $COVERAGE-ON$
/** The `fail(msg)` parser consumes no input and fails with `msg` as the error message */
def fail(msg: String): Parsley[Nothing] = new Parsley(new deepembedding.Fail(msg))
/** The `empty` parser consumes no input and fails softly (that is to say, no error message) */
Expand Down
124 changes: 75 additions & 49 deletions src/main/scala/parsley/Result.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,136 +9,162 @@ import scala.util.{Try, Success => TSuccess, Failure => TFailure}
sealed abstract class Result[+A]
{
/** Applies `fa` if this is a `Failure` or `fb` if this is a `Success`.
*
* @param ferr the function to apply if this is a `Failure`
* @param fa the function to apply if this is a `Success`
* @return the results of applying the function
*/
*
* @param ferr the function to apply if this is a `Failure`
* @param fa the function to apply if this is a `Success`
* @return the results of applying the function
* @since 1.7.0
*/
def fold[B](ferr: String => B, fa: A => B): B = this match {
case Success(x) => fa(x)
case Failure(msg) => ferr(msg)
}

/** Executes the given side-effecting function if this is a `Success`.
*
* @param f The side-effecting function to execute.
*/
*
* @param f The side-effecting function to execute.
* @since 1.7.0
*/
def foreach[U](f: A => U): Unit = this match {
case Success(x) => f(x)
case _ =>
}

/** Returns the results's value.
*
* @note The result must not be a failure.
* @throws java.util.NoSuchElementException if the result is a failure.
*/
*
* @note The result must not be a failure.
* @throws java.util.NoSuchElementException if the result is a failure.
* @since 1.7.0
*/
def get: A

/** Returns the value from this `Success` or the given argument if this is a `Failure`. */
/** Returns the value from this `Success` or the given argument if this is a `Failure`.
* @since 1.7.0
*/
def getOrElse[B >: A](or: =>B): B = orElse(Success(or)).get

/** Returns this `Success` or the given argument if this is a `Failure`. */
/** Returns this `Success` or the given argument if this is a `Failure`.
* @since 1.7.0
*/
def orElse[B >: A](or: =>Result[B]): Result[B] = this match {
case Success(_) => this
case _ => or
}

/** Returns `true` if this is a `Success` and its value is equal to `elem` (as determined by `==`),
* returns `false` otherwise.
*
* @param elem the element to test.
* @return `true` if this is a `Success` value equal to `elem`.
*/
* returns `false` otherwise.
*
* @param elem the element to test.
* @return `true` if this is a `Success` value equal to `elem`.
* @since 1.7.0
*/
final def contains[B >: A](elem: B): Boolean = exists(_ == elem)

/** Returns `true` if `Failure` or returns the result of the application of
* the given predicate to the `Success` value.
*/
* the given predicate to the `Success` value.
* @since 1.7.0
*/
def forall(f: A => Boolean): Boolean = this match {
case Success(x) => f(x)
case _ => true
}

/** Returns `false` if `Failure` or returns the result of the application of
* the given predicate to the `Success` value.
*/
* the given predicate to the `Success` value.
* @since 1.7.0
*/
def exists(p: A => Boolean): Boolean = this match {
case Success(x) => p(x)
case _ => false
}

/** Binds the given function across `Success`.
*
* @param f The function to bind across `Success`.
*/
*
* @param f The function to bind across `Success`.
* @since 1.7.0
*/
def flatMap[B](f: A => Result[B]): Result[B] = this match {
case Success(x) => f(x)
case _ => this.asInstanceOf[Result[B]]
}

/** Returns the right value if this is right
* or this value if this is left
*
* Equivalent to `flatMap(id => id)`
*/
* or this value if this is left
*
* Equivalent to `flatMap(id => id)`
* @since 1.7.0
*/
def flatten[B](implicit ev: A <:< Result[B]): Result[B] = flatMap(ev)

/** The given function is applied if this is a `Success`. */
/** The given function is applied if this is a `Success`.
* @since 1.7.0
*/
def map[B](f: A => B): Result[B] = this match {
case Success(x) => Success(f(x))
case _ => this.asInstanceOf[Result[B]]
}

/** Returns `Success` with the existing value of `Success` if this is a `Success`
* and the given predicate `p` holds for the right value,
* or `Failure(msg)` if this is a `Success` and the given predicate `p` does not hold for the right value,
* or `Failure` with the existing value of `Failure` if this is a `Failure`.
*/
* and the given predicate `p` holds for the right value,
* or `Failure(msg)` if this is a `Success` and the given predicate `p` does not hold for the right value,
* or `Failure` with the existing value of `Failure` if this is a `Failure`.
* @since 1.7.0
*/
def filterOrElse(p: A => Boolean, msg: =>String): Result[A] = this match {
case Success(x) if !p(x) => Failure(msg)
case _ => this
}

/** Returns a `Seq` containing the `Success` value if
* it exists or an empty `Seq` if this is a `Failure`.
*/
* it exists or an empty `Seq` if this is a `Failure`.
* @since 1.7.0
*/
def toSeq: collection.immutable.Seq[A] = this match {
case Success(x) => collection.immutable.Seq(x)
case _ => collection.immutable.Seq.empty
}

/** Returns a `Some` containing the `Success` value
* if it exists or a `None` if this is a `Failure`.
*/
* if it exists or a `None` if this is a `Failure`.
* @since 1.7.0
*/
def toOption: Option[A] = this match {
case Success(x) => Some(x)
case _ => None
}

/** Converts the `Result` into a `Try` where `Failure` maps to a plain `Exception` */
/** Converts the `Result` into a `Try` where `Failure` maps to a plain `Exception`
* @since 1.7.0
*/
def toTry: Try[A] = this match {
case Success(x) => TSuccess(x)
case Failure(msg) => TFailure(new Exception(s"ParseError: $msg"))
}

/** Converts the `Result` into a `Either` where `Failure` maps to a `Left[String]`
* @since 1.7.0
*/
def toEither: Either[String, A] = this match {
case Success(x) => Right(x)
case Failure(msg) => Left(msg)
}

/** Returns `true` if this is a `Success`, `false` otherwise. */
/** Returns `true` if this is a `Success`, `false` otherwise.
* @since 1.7.0
*/
def isSuccess: Boolean

/** Returns `true` if this is a `Failure`, `false` otherwise. */
/** Returns `true` if this is a `Failure`, `false` otherwise.
* @since 1.7.0
*/
def isFailure: Boolean
}

/**
* Returned when a parser succeeded.
* @param x The result value of the successful parse
* @tparam A The type of expected success result
*/
* Returned when a parser succeeded.
* @param x The result value of the successful parse
* @tparam A The type of expected success result
*/
case class Success[A] private [parsley] (x: A) extends Result[A]
{
override def isSuccess: Boolean = true
Expand All @@ -147,9 +173,9 @@ case class Success[A] private [parsley] (x: A) extends Result[A]
}

/**
* Returned on parsing failure
* @param msg The error message reported by the parser
*/
* Returned on parsing failure
* @param msg The error message reported by the parser
*/
case class Failure private [parsley] (msg: String) extends Result[Nothing]
{
override def isSuccess: Boolean = false
Expand Down
Loading

0 comments on commit 30e5c72

Please sign in to comment.