From f4f164c345d4e7a47080bd89fab40efbcad0ceb1 Mon Sep 17 00:00:00 2001 From: Jamie Willis Date: Sun, 22 Jan 2023 22:36:44 +0000 Subject: [PATCH] Version bump to 4.2 (#145) * Version bump to 4.2 * Fixed badges * Improved `position` API (#146) * Added positions object * deprecated old position stuff * Stopped coverage run unless it's actually needed (pr to master, or on master) * Fixed CI test coverage skipping * Apparently base_ref is a different style to ref... * Changed deprecation for Lexer inheritance to now, backport/4.1 needs to undo its deprecation * Deprecated old error combinators, added unexpectedLegacy (#147) * disabled test coverage for deprecated fastfail and fastunexpected * Replaced deprecated implementations by slow (but stable versions) * Error pattern combinators (#148) * 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 * Fixed bug where string terminal escapes relabelling * removed partial amend semantics on verifiedX, because it's evil: we won't be exposing it! * Added comment about the future improved partial amend * Fix name of instruction * Doc stubs * Add `dislodge` and `amendThenDislodge` combinators (#149) * Exposed amendThenDislodge and dislodge * Added tests * documentation * Introduce `unexpectedWhenWithReason` (#151) * Added combinator and generalised machinery * Removed defunct combinators from lexer * documentation * tests written * Added comment about spanWith, I'm not going to include it for now * Renamed combinator, it's a bit better with the When at the end * Documented the 1 indexing * Added position tests * position module description * Added positions to rootdoc * patterns documentation * sbt and README updated * update workflow * Fixed style issues * Added tests for verifiedFail * Full testing of new verified errors combinators --- .github/workflows/ci.yml | 1 + README.md | 15 +- build.sbt | 10 +- parsley/js/rootdoc.md | 1 + parsley/rootdoc.md | 1 + .../src/main/scala/parsley/Parsley.scala | 65 ++------- .../scala/parsley/errors/combinator.scala | 128 ++++++++++++------ .../main/scala/parsley/errors/patterns.scala | 109 +++++++++++++++ .../errors/tokenextractors/LexToken.scala | 3 +- .../backend/ErrorEmbedding.scala | 13 ++ .../backend/PrimitiveEmbedding.scala | 4 +- .../backend/SelectiveEmbedding.scala | 49 +------ .../frontend/ErrorEmbedding.scala | 4 + .../frontend/SelectiveEmbedding.scala | 9 +- .../machine/instructions/ErrorInstrs.scala | 47 ++++--- .../instructions/IntrinsicInstrs.scala | 12 +- .../src/main/scala/parsley/position.scala | 53 ++++++-- .../src/main/scala/parsley/token/Lexer.scala | 2 +- .../token/errors/ConfigImplTyped.scala | 81 ++++------- .../parsley/token/errors/ErrorConfig.scala | 24 ++-- .../VerifiedAndPreventativeErrors.scala | 17 +-- .../parsley/token/text/ConcreteString.scala | 3 +- .../src/test/scala/parsley/ErrorTests.scala | 89 +++++++++++- .../scala/parsley/ExpressionParserTests.scala | 2 +- .../test/scala/parsley/PositionTests.scala | 44 ++++++ .../src/test/scala/parsley/StringTests.scala | 3 +- .../machine/errors/DefuncErrorTests.scala | 21 +++ 27 files changed, 534 insertions(+), 276 deletions(-) create mode 100644 parsley/shared/src/main/scala/parsley/errors/patterns.scala create mode 100644 parsley/shared/src/test/scala/parsley/PositionTests.scala diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2515e4214..5fd14e909 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -221,6 +221,7 @@ jobs: coverage: name: Run Test Coverage and Upload + if: github.ref == 'refs/heads/master' || (github.event_name == 'pull_request' && github.base_ref == 'master') strategy: matrix: os: [ubuntu-latest] diff --git a/README.md b/README.md index 680da2ef9..889d34e5a 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Parsley is a fast and modern parser combinator library for Scala based loosely o Parsley is distributed on Maven Central, and can be added to your project via: ```scala -libraryDependencies += "com.github.j-mie6" %% "parsley" % "4.1.1" +libraryDependencies += "com.github.j-mie6" %% "parsley" % "4.2.0" ``` Documentation can be found [**here**](https://javadoc.io/doc/com.github.j-mie6/parsley_2.13/latest/index.html) @@ -151,13 +151,14 @@ make porting feasible. _An exception to this policy is made for any version `3.x.y`, which reaches EoL effective immediately (December 2022) excluding exceptional circumstances._ -| Version | Released On | EoL Status | -|:-------:|:-------------------|:----------------------------| -| `3.3.0` | January 7th 2022 | EoL reached | -| `4.0.0` | November 30th 2022 | EoL reached | -| `4.1.0` | January 18th 2023 | Enjoying indefinite support | +| Version | Released On | EoL Status | +|:-------:|:-------------------|:-------------------------------| +| `3.3.0` | January 7th 2022 | EoL reached | +| `4.0.0` | November 30th 2022 | EoL reached | +| `4.1.0` | January 18th 2023 | Supported until 22nd July 2023 | +| `4.2.0` | January 22th 2023 | Enjoying indefinite support | -## Bug Reports [![Percentage of issues still open](https://isitmaintained.com/badge/open/j-mie6/Parsley.svg)](https://isitmaintained.com/project/j-mie6/Parsley "Percentage of issues still open") [![Maintainability](https://img.shields.io/codeclimate/maintainability/j-mie6/parsley)](https://codeclimate.com/github/j-mie6/Parsley) [![Test Coverage](https://img.shields.io/codeclimate/coverage-letter/j-mie6/parsley)](https://codeclimate.com/github/j-mie6/Parsley) +## Bug Reports [![Percentage of issues still open](https://isitmaintained.com/badge/open/j-mie6/Parsley.svg)](https://isitmaintained.com/project/j-mie6/Parsley "Percentage of issues still open") [![Maintainability](https://img.shields.io/codeclimate/maintainability/j-mie6/parsley)](https://codeclimate.com/github/j-mie6/parsley) [![Test Coverage](https://img.shields.io/codeclimate/coverage-letter/j-mie6/parsley)](https://codeclimate.com/github/j-mie6/parsley) If you encounter a bug when using Parsley, try and minimise the example of the parser (and the input) that triggers the bug. If possible, make a self contained example: this will help to identify the issue without too much issue. diff --git a/build.sbt b/build.sbt index 7070170a2..3b3d34cae 100644 --- a/build.sbt +++ b/build.sbt @@ -9,13 +9,15 @@ val Java8 = JavaSpec.temurin("8") val JavaLTS = JavaSpec.temurin("11") val JavaLatest = JavaSpec.temurin("17") +val mainBranch = "master" + Global / onChangedBuildSource := ReloadOnSourceChanges val isInPublish = Option(System.getenv("GITHUB_JOB")).contains("publish") val releaseFlags = Seq("-Xdisable-assertions", "-opt:l:method,inline", "-opt-inline-from", "parsley.**", "-opt-warnings:at-inline-failed") inThisBuild(List( - tlBaseVersion := "4.1", + tlBaseVersion := "4.2", organization := "com.github.j-mie6", startYear := Some(2018), homepage := Some(url("https://github.com/j-mie6/parsley")), @@ -38,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", @@ -45,7 +50,7 @@ inThisBuild(List( "3" -> "3.1.2", ), // CI Configuration - tlCiReleaseBranches := Seq("master"), + tlCiReleaseBranches := Seq(mainBranch), tlSonatypeUseLegacyHost := false, githubWorkflowJavaVersions := Seq(Java8, JavaLTS, JavaLatest), // We need this because our release uses different flags @@ -91,6 +96,7 @@ def testCoverageJob(cacheSteps: List[WorkflowStep]) = WorkflowJob( id = "coverage", name = "Run Test Coverage and Upload", scalas = List(Scala213), + cond = Some(s"github.ref == 'refs/heads/$mainBranch' || (github.event_name == 'pull_request' && github.base_ref == '$mainBranch')"), steps = WorkflowStep.Checkout :: WorkflowStep.SetupJava(List(JavaLTS)) ::: diff --git a/parsley/js/rootdoc.md b/parsley/js/rootdoc.md index 3987768ea..49ec7381a 100644 --- a/parsley/js/rootdoc.md +++ b/parsley/js/rootdoc.md @@ -54,6 +54,7 @@ is defined as being an object which mocks a package): context-sensitive functionality in the form of registers. - [[parsley.token `parsley.token`]] contains the [[parsley.token.Lexer `Lexer`]] class that provides a host of helpful lexing combinators when provided with the description of a language. + - [[parsley.position `parsley.position`]] contains parsers for extracting position information. - [[parsley.genericbridges$ `parsley.genericbridges`]] contains some basic implementations of the ''Parser Bridge'' pattern (see [[https://dl.acm.org/doi/10.1145/3550198.3550427 Design Patterns for Parser Combinators in Scala]], diff --git a/parsley/rootdoc.md b/parsley/rootdoc.md index cef7052cc..ea2e97a40 100644 --- a/parsley/rootdoc.md +++ b/parsley/rootdoc.md @@ -56,6 +56,7 @@ is defined as being an object which mocks a package): context-sensitive functionality in the form of registers. - [[parsley.token `parsley.token`]] contains the [[parsley.token.Lexer `Lexer`]] class that provides a host of helpful lexing combinators when provided with the description of a language. + - [[parsley.position `parsley.position`]] contains parsers for extracting position information. - [[parsley.genericbridges$ `parsley.genericbridges`]] contains some basic implementations of the ''Parser Bridge'' pattern (see [[https://dl.acm.org/doi/10.1145/3550198.3550427 Design Patterns for Parser Combinators in Scala]], diff --git a/parsley/shared/src/main/scala/parsley/Parsley.scala b/parsley/shared/src/main/scala/parsley/Parsley.scala index 7a373fa70..0e18fb4e7 100644 --- a/parsley/shared/src/main/scala/parsley/Parsley.scala +++ b/parsley/shared/src/main/scala/parsley/Parsley.scala @@ -944,13 +944,6 @@ final class Parsley[+A] private [parsley] (private [parsley] val internal: front * useful; in particular, `pure` and `unit` can be put to good use in injecting results into a parser * without needing to consume anything, or mapping another parser. * - * @groupprio pos 10 - * @groupname pos Position-Tracking Parsers - * @groupdesc pos - * These parsers provide a way to extract position information during a parse. This can be important - * for when the final result of the parser needs to encode position information for later consumption: - * this is particularly useful for abstract syntax trees. - * * @groupprio monad 100 * @groupname monad Expensive Sequencing Combinators * @groupdesc monad @@ -1218,66 +1211,26 @@ object Parsley { * @group basic */ val unit: Parsley[Unit] = pure(()) + // $COVERAGE-OFF$ /** This parser returns the current line number of the input without having any other effect. * - * When this combinator is ran, no input is required, nor consumed, and - * the current line number will always be successfully returned. It has no other - * effect on the state of the parser. - * - * @example {{{ - * scala> import parsley.Parsley.line, parsley.character.char - * scala> line.parse("") - * val res0 = Success(1) - * scala> (char('a') *> line).parse("a") - * val res0 = Success(1) - * scala> (char('\n') *> line).parse("\n") - * val res0 = Success(2) - * }}} - * - * @return a parser that returns the line number the parser is currently at. - * @group pos + * @deprecated Moved to [[position.line `position.line`]], due for removal in 5.0.0 */ + @deprecated("Position parsing functionality was moved to `parsley.position`; use `position.line` instead as this will be removed in 5.0.0", "4.2.0") def line: Parsley[Int] = position.line /** This parser returns the current column number of the input without having any other effect. * - * When this combinator is ran, no input is required, nor consumed, and - * the current column number will always be successfully returned. It has no other - * effect on the state of the parser. - * - * @example {{{ - * scala> import parsley.Parsley.col, parsley.character.char - * scala> col.parse("") - * val res0 = Success(1) - * scala> (char('a') *> col).parse("a") - * val res0 = Success(2) - * scala> (char('\n') *> col).parse("\n") - * val res0 = Success(1) - * }}} - * - * @return a parser that returns the column number the parser is currently at. - * @note in the presence of wide unicode characters, the value returned may be inaccurate. - * @group pos + * @note in the presence of wide unicode characters, the column value returned may be inaccurate. + * @deprecated Moved to [[position.col `position.col`]], due for removal in 5.0.0 */ + @deprecated("Position parsing functionality was moved to `parsley.position`; use `position.line` instead as this will be removed in 5.0.0", "4.2.0") def col: Parsley[Int] = position.col /** This parser returns the current line and column numbers of the input without having any other effect. * - * When this combinator is ran, no input is required, nor consumed, and - * the current line and column number will always be successfully returned. It has no other - * effect on the state of the parser. - * - * @example {{{ - * scala> import parsley.Parsley.pos, parsley.character.char - * scala> pos.parse("") - * val res0 = Success((1, 1)) - * scala> (char('a') *> pos).parse("a") - * val res0 = Success((1, 2)) - * scala> (char('\n') *> pos).parse("\n") - * val res0 = Success((2, 1)) - * }}} - * - * @return a parser that returns the line and column number the parser is currently at. * @note in the presence of wide unicode characters, the column value returned may be inaccurate. - * @group pos + * @deprecated Moved to [[position.pos `position.pos`]], due for removal in 5.0.0 */ + @deprecated("Position parsing functionality was moved to `parsley.position`; use `position.line` instead as this will be removed in 5.0.0", "4.2.0") def pos: Parsley[(Int, Int)] = position.pos + // $COVERAGE-ON$ } diff --git a/parsley/shared/src/main/scala/parsley/errors/combinator.scala b/parsley/shared/src/main/scala/parsley/errors/combinator.scala index 1357299a8..1a78ad59e 100644 --- a/parsley/shared/src/main/scala/parsley/errors/combinator.scala +++ b/parsley/shared/src/main/scala/parsley/errors/combinator.scala @@ -3,7 +3,7 @@ */ package parsley.errors -import parsley.Parsley, Parsley.attempt +import parsley.Parsley import parsley.internal.deepembedding.{frontend, singletons} @@ -141,10 +141,33 @@ object combinator { */ def entrench[A](p: Parsley[A]): Parsley[A] = new Parsley(new frontend.ErrorEntrench(p.internal)) - // TODO: Documentation and testing ahead of future release - private [parsley] def dislodge[A](p: Parsley[A]): Parsley[A] = new Parsley(new frontend.ErrorDislodge(p.internal)) - private [parsley] def amendThenDislodge[A](p: Parsley[A]): Parsley[A] = dislodge(amend(p)) + /** This combinator undoes the action of the `entrench` combinator on the given parser. + * + * Entrenchment is important for preventing the incorrect amendment of certain parts of sub-errors + * for a parser, but it may be then undesireable to block further amendments from elsewhere in the + * parser. This combinator can be used to cancel and entrenchment after the critical section has + * passed. + * + * @param p a parser that should no longer be under the affect of an `entrench` combinator + * @return a parser that parses `p` and allows its error messages to be amended. + * @since 4.2.0 + */ + def dislodge[A](p: Parsley[A]): Parsley[A] = new Parsley(new frontend.ErrorDislodge(p.internal)) + + /** This combinator first tries to amend the position of any error generated by the given parser, + * and if the error was entrenched will dislodge it instead. + * + * @param p a parser whose error messages should be amended unless its been entrenched. + * @return a parser that parses `p` but ensures any errors generated occur as if no input were consumed. + * @since 4.2.0 + * @see [[amend `amend`]] and `[[dislodge `dislodge`]] + */ + def amendThenDislodge[A](p: Parsley[A]): Parsley[A] = dislodge(amend(p)) + + // These aren't going to be exposed and should be removed in 5.0.0 as well! + @deprecated("this combinator is evil, because it renders the error at the wrong place unless it is amended!", "4.2.0") private [parsley] def partialAmend[A](p: Parsley[A]): Parsley[A] = new Parsley(new frontend.ErrorAmend(p.internal, partial = true)) + @deprecated("this combinator is evil, because it renders the error at the wrong place unless it is amended!", "4.2.0") private [parsley] def partialAmendThenDislodge[A](p: Parsley[A]): Parsley[A] = dislodge(partialAmend(p)) /** This combinator marks any errors within the given parser as being ''lexical errors''. @@ -353,6 +376,10 @@ object combinator { this.guardAgainst{case x if !pf.isDefinedAt(x) => msggen(x)}.map(pf) } + private def _unexpectedWhen(pred: PartialFunction[A, (String, Option[String])]): Parsley[A] = { + new Parsley(new frontend.UnexpectedWhen(con(p).internal, pred)) + } + /** This combinator filters the result of this parser using the given partial-predicate, succeeding only when the predicate is undefined. * * First, parse this parser. If it succeeds then take its result `x` and test if `pred.isDefinedAt(x)` is true. If it is @@ -380,11 +407,50 @@ object combinator { * @return a parser that returns the result of this parser if it fails the predicate. * @see [[parsley.Parsley.filterNot `filterNot`]], which is a basic version of this same combinator with no unexpected message. * @see [[filterOut `filterOut`]], which is a variant that produces a reason for failure as opposed to an unexpected message. - * @see [[guardAgainst `guardAgainst`]], which is similar to `unexpectedWhen`, except it generates a ''specialised'' error as opposed to just a reason. + * @see [[guardAgainst `guardAgainst`]], which is similar to `unexpectedWhen`, except it generates a ''specialised'' error instead. + * @see [[unexpectedWithReasonWhen `unexpectedWithReasonWhen`]], which is similar, but also has a reason associated. * @note $autoAmend * @group filter */ - def unexpectedWhen(pred: PartialFunction[A, String]): Parsley[A] = new Parsley(new frontend.UnexpectedWhen(con(p).internal, pred)) + def unexpectedWhen(pred: PartialFunction[A, String]): Parsley[A] = this._unexpectedWhen { + case x if pred.isDefinedAt(x) => (pred(x), None) + } + + /** This combinator filters the result of this parser using the given partial-predicate, succeeding only when the predicate is undefined. + * + * First, parse this parser. If it succeeds then take its result `x` and test if `pred.isDefinedAt(x)` is true. If it is + * false, the parser succeeds, returning `x`. Otherwise, `pred(x)` will yield a unexpected label and the parser will + * fail using [[combinator.unexpected(caretWidth:Int,item:String)* `unexpected`]] and that label as well as a reason. + * + * This is useful for performing data validation, but where a the failure results in the entire token being unexpected. In this instance, + * the rest of the error message is generated as normal, with the expected components still given, along with + * any generated reasons. + * + * @example {{{ + * scala> import parsley.character.letter + * scala> val keywords = Set("if", "then", "else") + * scala> val ident = stringOfSome(letter).unexpectedWhenWithReason { + * case v if keywords.contains(v) => (s"keyword $v", "keywords cannot be identifiers") + * } + * scala> ident.parse("hello") + * val res0 = Success("hello") + * scala> ident.parse("if") + * val res1 = Failure(..) + * }}} + * + * @param pred the predicate that is tested against the parser result, which also generates errors. + * @return a parser that returns the result of this parser if it fails the predicate. + * @see [[parsley.Parsley.filterNot `filterNot`]], which is a basic version of this same combinator with no unexpected message or reason. + * @see [[filterOut `filterOut`]], which is a variant that just produces a reason for failure with no unexpected message. + * @see [[guardAgainst `guardAgainst`]], which is similar to `unexpectedWhen`, except it generates a ''specialised'' error instead. + * @see [[unexpectedWhen `unexpectedWhen`]], which is similar, but with no associated reason. + @since 4.2.0 + */ + def unexpectedWithReasonWhen(pred: PartialFunction[A, (String, String)]): Parsley[A] = this._unexpectedWhen { + case x if pred.isDefinedAt(x) => + val (unex, reason) = pred(x) + (unex, Some(reason)) + } /** This combinator changes the expected component of any errors generated by this parser. * @@ -443,21 +509,7 @@ 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) - - // TODO: deprecate, and stress there is no _direct_ equivalent available moving forward + // $COVERAGE-OFF$ /** This combinator parses this parser and then fails, using the result of this parser to customise the error message. * * Similar to `fail`, but first parses this parser: if it succeeded, then its result `x` is used to form the error @@ -468,11 +520,16 @@ object combinator { * @return a parser that always fails, with the given generator used to produce the error message if this parser succeeded. * @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, `verifiedFail` combinator. */ - def !(msggen: A => String): Parsley[Nothing] = new Parsley(new frontend.FastFail(con(p).internal, msggen)) + @deprecated("This combinator will be removed in 5.0.0, without direct replacement", "4.2.0") + 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. * @@ -484,21 +541,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. There is a similar, but not + * exact replacement called `verifiedUnexpected`. + * @since 4.2.0 */ - def unexpected(msggen: A => String): Parsley[Nothing] = new Parsley(new frontend.FastUnexpected(con(p).internal, msggen)) - - // 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 + @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)) + } } - 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$ } } diff --git a/parsley/shared/src/main/scala/parsley/errors/patterns.scala b/parsley/shared/src/main/scala/parsley/errors/patterns.scala new file mode 100644 index 000000000..cd43a29da --- /dev/null +++ b/parsley/shared/src/main/scala/parsley/errors/patterns.scala @@ -0,0 +1,109 @@ +/* SPDX-FileCopyrightText: © 2023 Parsley Contributors + * SPDX-License-Identifier: BSD-3-Clause + */ +package parsley.errors + +import parsley.Parsley + +import parsley.internal.deepembedding.frontend + +/** This module contains combinators that help facilitate the error message generational patterns ''Verified Errors'' and ''Preventative Errors''. + * + * In particular, exposes an extension class `VerifiedErrors` that facilitates creating verified errors in many different formats. + * + * @since 4.2.0 + */ +object patterns { + /** This class exposes combinators related to the ''Verified Errors'' parser design pattern. + * + * This extension class operates on values that are convertible to parsers. The combinators it enables + * allow for the parsing of known illegal values, providing richer error messages in case they succeed. + * + * @constructor This constructor should not be called manually, it is designed to be used via Scala's implicit resolution. + * @param p the value that this class is enabling methods on. + * @param con a conversion that allows values convertible to parsers to be used. + * @tparam P the type of base value that this class is used on (the conversion to `Parsley`) is summoned automatically. + * @since 4.2.0 + * + * @define autoAmend + * when this combinator fails (and not this parser itself), it will generate errors rooted at the start of the + * parse (as if [[parsley.errors.combinator$.amend `amend`]] had been used) and the caret will span the entire + * successful parse of this parser. + * + * @define attemptNonTerminal + * when this parser is not to be considered as a terminal error, use `attempt` around the ''entire'' combinator to + * allow for backtracking if this parser succeeds (and therefore fails). + * + * @define Ensures this parser does not succeed, failing with a + */ + implicit final class VerifiedErrors[P, A](p: P)(implicit con: P => Parsley[A]) { + /** Ensures this parser does not succeed, failing with a specialised error based on this parsers result if it does. + * + * If this parser succeeds, input is consumed and this combinator will fail, producing an error message + * based on the parsed result. However, if this parser fails, no input is consumed and an empty error is generated. + * This parser will produce no labels if it fails. + * + * @param msggen the function that generates the error messages from the parsed value. + * @since 4.2.0 + * @note $autoAmend + * @note $attemptNonTerminal + */ + def verifiedFail(msggen: A => Seq[String]): Parsley[Nothing] = verified(Left(msggen)) + + /** Ensures this parser does not succeed, failing with a specialised error if it does. + * + * If this parser succeeds, input is consumed and this combinator will fail, producing an error message + * based on the given messages. However, if this parser fails, no input is consumed and an empty error is generated. + * This parser will produce no labels if it fails. + * + * @param msg0 the first message in the error message. + * @param msgs the remaining messages that will make up the error message. + * @since 4.2.0 + * @note $autoAmend + * @note $attemptNonTerminal + */ + def verifiedFail(msg0: String, msgs: String*): Parsley[Nothing] = this.verifiedFail(_ => msg0 +: msgs) + + /** Ensures this parser does not succeed, failing with a vanilla error with an unexpected message and caret spanning the parse. + * + * If this parser succeeds, input is consumed and this combinator will fail, producing an unexpected message the same width as + * the parse. However, if this parser fails, no input is consumed and an empty error is generated. + * This parser will produce no labels if it fails. + * + * @since 4.2.0 + * @note $autoAmend + * @note $attemptNonTerminal + */ + def verifiedUnexpected: Parsley[Nothing] = this.verifiedUnexpected(None) + + /** Ensures this parser does not succeed, failing with a vanilla error with an unexpected message and caret spanning the parse and a given reason. + * + * If this parser succeeds, input is consumed and this combinator will fail, producing an unexpected message the same width as + * the parse along with the given reason. However, if this parser fails, no input is consumed and an empty error is generated. + * This parser will produce no labels if it fails. + * + * @param reason the reason that this parser is illegal. + * @since 4.2.0 + * @note $autoAmend + * @note $attemptNonTerminal + */ + def verifiedUnexpected(reason: String): Parsley[Nothing] = this.verifiedUnexpected(_ => reason) + + /** Ensures this parser does not succeed, failing with a vanilla error with an unexpected message and caret spanning the parse and a reason generated + * from this parser's result. + * + * If this parser succeeds, input is consumed and this combinator will fail, producing an unexpected message the same width as + * the parse along with a reason generated from the successful parse. However, if this parser fails, no input is consumed and an empty error + * is generated. This parser will produce no labels if it fails. + * + * @param reason a function that produces a reason for the error given the parsed result. + * @since 4.2.0 + * @note $autoAmend + * @note $attemptNonTerminal + */ + def verifiedUnexpected(reason: A => String): Parsley[Nothing] = this.verifiedUnexpected(Some(reason)) + + private def verified(msggen: Either[A => Seq[String], Option[A => String]]) = new Parsley(new frontend.VerifiedError(con(p).internal, msggen)) + private def verifiedUnexpected(reason: Option[A => String]) = verified(Right(reason)) + } +} diff --git a/parsley/shared/src/main/scala/parsley/errors/tokenextractors/LexToken.scala b/parsley/shared/src/main/scala/parsley/errors/tokenextractors/LexToken.scala index 7a1b7caac..ded842879 100644 --- a/parsley/shared/src/main/scala/parsley/errors/tokenextractors/LexToken.scala +++ b/parsley/shared/src/main/scala/parsley/errors/tokenextractors/LexToken.scala @@ -12,6 +12,7 @@ import parsley.XCompat.unused import parsley.character.item import parsley.combinator.{choice, eof, option, sequence, someUntil} import parsley.errors.{ErrorBuilder, Token, TokenSpan} +import parsley.position // Turn coverage off, because the tests have their own error builder // TODO: We might want to test this on its own though @@ -58,7 +59,7 @@ trait LexToken { this: ErrorBuilder[_] => // this parser cannot fail private lazy val makeParser: Parsley[Either[String, List[(String, (Int, Int))]]] = { - val toks = tokens.map(p => attempt(p <~> Parsley.pos)) + val toks = tokens.map(p => attempt(p <~> position.pos)) // TODO: I think this can be improved to delay raw token till after we have established // no valid tokens: this would be slightly more efficient. val rawTok = lookAhead(someUntil(item, eof <|> choice(toks: _*))).map(_.mkString) diff --git a/parsley/shared/src/main/scala/parsley/internal/deepembedding/backend/ErrorEmbedding.scala b/parsley/shared/src/main/scala/parsley/internal/deepembedding/backend/ErrorEmbedding.scala index f41a86256..4174ea2e8 100644 --- a/parsley/shared/src/main/scala/parsley/internal/deepembedding/backend/ErrorEmbedding.scala +++ b/parsley/shared/src/main/scala/parsley/internal/deepembedding/backend/ErrorEmbedding.scala @@ -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 @@ -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)) diff --git a/parsley/shared/src/main/scala/parsley/internal/deepembedding/backend/PrimitiveEmbedding.scala b/parsley/shared/src/main/scala/parsley/internal/deepembedding/backend/PrimitiveEmbedding.scala index bfc29841c..d70065b87 100644 --- a/parsley/shared/src/main/scala/parsley/internal/deepembedding/backend/PrimitiveEmbedding.scala +++ b/parsley/shared/src/main/scala/parsley/internal/deepembedding/backend/PrimitiveEmbedding.scala @@ -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) diff --git a/parsley/shared/src/main/scala/parsley/internal/deepembedding/backend/SelectiveEmbedding.scala b/parsley/shared/src/main/scala/parsley/internal/deepembedding/backend/SelectiveEmbedding.scala index 5bb6d64f7..8b147ed1e 100644 --- a/parsley/shared/src/main/scala/parsley/internal/deepembedding/backend/SelectiveEmbedding.scala +++ b/parsley/shared/src/main/scala/parsley/internal/deepembedding/backend/SelectiveEmbedding.scala @@ -74,43 +74,7 @@ private [deepembedding] final class If[A](val b: StrictParsley[Boolean], val p: // $COVERAGE-ON$ } -// TODO: Code generation for FastZero and FilterLike is shared -private [backend] sealed abstract class FastZero[A](fail: A => StrictParsley[Nothing], instr: instructions.Instr) extends Unary[A, Nothing] { - - final override def optimise: StrictParsley[Nothing] = p match { - case Pure(x) => fail(x) - case z: MZero => z - case _ => this - } - final override def codeGen[Cont[_, +_], R](implicit ops: ContOps[Cont], instrs: InstrBuffer, state: CodeGenState): Cont[R, Unit] = { - val handler = state.getLabel(instructions.PopStateAndFail) - instrs += new instructions.PushHandlerAndState(handler, saveHints = false, hideHints = false) - suspend(p.codeGen[Cont, R]) |> { - instrs += instr - } - } -} -private [deepembedding] final class FastFail[A](val p: StrictParsley[A], msggen: A => String) - extends FastZero[A](x => new Fail(0, msggen(x)), instructions.FastFail(msggen)) with MZero { - // $COVERAGE-OFF$ - final override def pretty(p: String): String = s"$p.fail(?)" - // $COVERAGE-ON$ -} -private [deepembedding] final class FastUnexpected[A](val p: StrictParsley[A], msggen: A => String) - extends FastZero[A](x => new Unexpected(msggen(x), 0), new instructions.FastUnexpected(msggen)) with MZero { - // $COVERAGE-OFF$ - final override def pretty(p: String): String = s"$p.unexpected(?)" - // $COVERAGE-ON$ -} - -private [backend] sealed abstract class FilterLike[A](fail: A => StrictParsley[Nothing], instr: instructions.Instr, pred: A => Boolean) - extends Unary[A, A] { - final override def optimise: StrictParsley[A] = p match { - case Pure(x) if pred(x) => fail(x) - case px: Pure[_] => px - case z: MZero => z - case _ => this - } +private [backend] sealed abstract class FilterLike[A](instr: instructions.Instr) extends Unary[A, A] { final override def codeGen[Cont[_, +_]: ContOps, R](implicit instrs: InstrBuffer, state: CodeGenState): Cont[R, Unit] = { val handler = state.getLabel(instructions.PopStateAndFail) instrs += new instructions.PushHandlerAndState(handler, saveHints = false, hideHints = false) @@ -119,8 +83,7 @@ private [backend] sealed abstract class FilterLike[A](fail: A => StrictParsley[N } } } -private [deepembedding] final class Filter[A](val p: StrictParsley[A], pred: A => Boolean) - extends FilterLike[A](_ => Empty, new instructions.Filter(pred), !pred(_)) { +private [deepembedding] final class Filter[A](val p: StrictParsley[A], pred: A => Boolean) extends FilterLike[A](new instructions.Filter(pred)) { // $COVERAGE-OFF$ final override def pretty(p: String): String = s"$p.filter(?)" // $COVERAGE-ON$ @@ -146,20 +109,20 @@ private [deepembedding] final class MapFilter[A, B](val p: StrictParsley[A], f: } private [deepembedding] final class FilterOut[A](val p: StrictParsley[A], pred: PartialFunction[A, String]) - extends FilterLike[A](x => ErrorExplain(Empty, pred(x)), new instructions.FilterOut(pred), pred.isDefinedAt(_)) { + extends FilterLike[A](new instructions.FilterOut(pred)) { // $COVERAGE-OFF$ final override def pretty(p: String): String = s"$p.filterOut(?)" // $COVERAGE-ON$ } private [deepembedding] final class GuardAgainst[A](val p: StrictParsley[A], pred: PartialFunction[A, scala.Seq[String]]) - extends FilterLike[A](x => new Fail(0, pred(x): _*), instructions.GuardAgainst(pred), pred.isDefinedAt(_)) { + extends FilterLike[A](instructions.GuardAgainst(pred)) { // $COVERAGE-OFF$ final override def pretty(p: String): String = s"$p.guardAgainst(?)" // $COVERAGE-ON$ } -private [deepembedding] final class UnexpectedWhen[A](val p: StrictParsley[A], pred: PartialFunction[A, String]) - extends FilterLike[A](x => new Unexpected(pred(x), 0), instructions.UnexpectedWhen(pred), pred.isDefinedAt(_)) { +private [deepembedding] final class UnexpectedWhen[A](val p: StrictParsley[A], pred: PartialFunction[A, (String, Option[String])]) + extends FilterLike[A](instructions.UnexpectedWhen(pred)) { // $COVERAGE-OFF$ final override def pretty(p: String): String = s"$p.unexpectedWhen(?)" // $COVERAGE-ON$ diff --git a/parsley/shared/src/main/scala/parsley/internal/deepembedding/frontend/ErrorEmbedding.scala b/parsley/shared/src/main/scala/parsley/internal/deepembedding/frontend/ErrorEmbedding.scala index 517c10369..372a826d6 100644 --- a/parsley/shared/src/main/scala/parsley/internal/deepembedding/frontend/ErrorEmbedding.scala +++ b/parsley/shared/src/main/scala/parsley/internal/deepembedding/frontend/ErrorEmbedding.scala @@ -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) +} diff --git a/parsley/shared/src/main/scala/parsley/internal/deepembedding/frontend/SelectiveEmbedding.scala b/parsley/shared/src/main/scala/parsley/internal/deepembedding/frontend/SelectiveEmbedding.scala index 40af54e2c..eb14da6b4 100644 --- a/parsley/shared/src/main/scala/parsley/internal/deepembedding/frontend/SelectiveEmbedding.scala +++ b/parsley/shared/src/main/scala/parsley/internal/deepembedding/frontend/SelectiveEmbedding.scala @@ -14,13 +14,6 @@ private [parsley] final class If[A](b: LazyParsley[Boolean], p: =>LazyParsley[A] override def make(b: StrictParsley[Boolean], p: StrictParsley[A], q: StrictParsley[A]): StrictParsley[A] = new backend.If(b, p, q) } -private [parsley] final class FastFail[A](p: LazyParsley[A], msggen: A => String) extends Unary[A, Nothing](p) { - override def make(p: StrictParsley[A]): StrictParsley[Nothing] = new backend.FastFail(p, msggen) -} -private [parsley] final class FastUnexpected[A](p: LazyParsley[A], msggen: A => String) extends Unary[A, Nothing](p) { - override def make(p: StrictParsley[A]): StrictParsley[Nothing] = new backend.FastUnexpected(p, msggen) -} - private [parsley] final class Filter[A](p: LazyParsley[A], pred: A => Boolean) extends Unary[A, A](p) { override def make(p: StrictParsley[A]): StrictParsley[A] = new backend.Filter(p, pred) } @@ -33,6 +26,6 @@ private [parsley] final class FilterOut[A](p: LazyParsley[A], pred: PartialFunct private [parsley] final class GuardAgainst[A](p: LazyParsley[A], pred: PartialFunction[A, Seq[String]]) extends Unary[A, A](p) { override def make(p: StrictParsley[A]): StrictParsley[A] = new backend.GuardAgainst(p, pred) } -private [parsley] final class UnexpectedWhen[A](p: LazyParsley[A], pred: PartialFunction[A, String]) extends Unary[A, A](p) { +private [parsley] final class UnexpectedWhen[A](p: LazyParsley[A], pred: PartialFunction[A, (String, Option[String])]) extends Unary[A, A](p) { override def make(p: StrictParsley[A]): StrictParsley[A] = new backend.UnexpectedWhen(p, pred) } diff --git a/parsley/shared/src/main/scala/parsley/internal/machine/instructions/ErrorInstrs.scala b/parsley/shared/src/main/scala/parsley/internal/machine/instructions/ErrorInstrs.scala index bae8c05ae..f490f4913 100644 --- a/parsley/shared/src/main/scala/parsley/internal/machine/instructions/ErrorInstrs.scala +++ b/parsley/shared/src/main/scala/parsley/internal/machine/instructions/ErrorInstrs.scala @@ -6,7 +6,7 @@ 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.{ClassicFancyError, ClassicUnexpectedError} +import parsley.internal.machine.errors.{ClassicExpectedError, ClassicExpectedErrorWithReason, ClassicFancyError, EmptyError} private [internal] final class RelabelHints(label: String) extends Instr { private [this] val isHide: Boolean = label.isEmpty @@ -162,34 +162,47 @@ private [internal] final class Unexpected(msg: String, width: Int) extends Instr // $COVERAGE-ON$ } -private [internal] final class FastFail(msggen: Any => String) extends Instr { +// partial amend semantics are BAD: they render the error in the wrong position unless amended anyway +// But it would make sense for an error that occured physically deeper to be stronger: a distinction is +// needed between occuredOffset and presentedOffset in the errors to make this work properly... +// If we did it, I'm not sure how we'd change this over: either 5.0.0 or we make new methods and `amend` the old ones +private [internal] class MakeVerifiedError private (msggen: Either[Any => Seq[String], Option[Any => String]]) extends Instr { override def apply(ctx: Context): Unit = { ensureRegularInstruction(ctx) - val x = ctx.stack.upop() - ctx.handlers = ctx.handlers.tail val state = ctx.states ctx.states = ctx.states.tail - ctx.fail(new ClassicFancyError(ctx.offset, state.line, state.col, ctx.offset - state.offset, msggen(x))) + 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(state.offset, state.line, state.col, caretWidth, f(x): _*) + case Right(Some(f)) => new ClassicExpectedErrorWithReason(state.offset, state.line, state.col, None, f(x), caretWidth) + case Right(None) => new ClassicExpectedError(state.offset, state.line, state.col, None, caretWidth) + } + ctx.fail(err) } // $COVERAGE-OFF$ - override def toString: String = "FastFail(?)" + override def toString: String = "MakeVerifiedError" // $COVERAGE-ON$ } -private [internal] object FastFail { - def apply[A](msggen: A => String): FastFail = new FastFail(msggen.asInstanceOf[Any => String]) +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] final class FastUnexpected[A](_namegen: A=>String) extends Instr { - private [this] def namegen(x: Any, width: Int) = new UnexpectDesc(_namegen(x.asInstanceOf[A]), width) +private [internal] object NoVerifiedError extends Instr { override def apply(ctx: Context): Unit = { - ensureRegularInstruction(ctx) - val x = ctx.stack.upop() - ctx.handlers = ctx.handlers.tail - val state = ctx.states - ctx.states = ctx.states.tail - ctx.fail(new ClassicUnexpectedError(ctx.offset, state.line, state.col, None, namegen(x, ctx.offset - state.offset))) + 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 = "FastUnexpected(?)" + override def toString: String = "VerifiedErrorHandler" // $COVERAGE-ON$ } diff --git a/parsley/shared/src/main/scala/parsley/internal/machine/instructions/IntrinsicInstrs.scala b/parsley/shared/src/main/scala/parsley/internal/machine/instructions/IntrinsicInstrs.scala index 98ac804cc..7caf84159 100644 --- a/parsley/shared/src/main/scala/parsley/internal/machine/instructions/IntrinsicInstrs.scala +++ b/parsley/shared/src/main/scala/parsley/internal/machine/instructions/IntrinsicInstrs.scala @@ -10,7 +10,7 @@ import parsley.token.errors.LabelConfig import parsley.internal.errors.{EndOfInput, ExpectItem, UnexpectDesc} import parsley.internal.machine.Context import parsley.internal.machine.XAssert._ -import parsley.internal.machine.errors.{ClassicFancyError, ClassicUnexpectedError, EmptyError, EmptyErrorWithReason} +import parsley.internal.machine.errors.{ClassicFancyError, ClassicUnexpectedError, DefuncError, EmptyError, EmptyErrorWithReason} private [internal] final class Lift2(f: (Any, Any) => Any) extends Instr { override def apply(ctx: Context): Unit = { @@ -210,14 +210,16 @@ private [internal] object GuardAgainst { def apply[A](pred: PartialFunction[A, Seq[String]]): GuardAgainst = new GuardAgainst(pred.asInstanceOf[PartialFunction[Any, Seq[String]]]) } -private [internal] final class UnexpectedWhen(pred: PartialFunction[Any, String]) extends Instr { +private [internal] final class UnexpectedWhen(pred: PartialFunction[Any, (String, Option[String])]) extends Instr { override def apply(ctx: Context): Unit = { ensureRegularInstruction(ctx) ctx.handlers = ctx.handlers.tail if (pred.isDefinedAt(ctx.stack.upeek)) { val state = ctx.states val caretWidth = ctx.offset - state.offset - ctx.fail(new ClassicUnexpectedError(state.offset, state.line, state.col, None, new UnexpectDesc(pred(ctx.stack.upop()), caretWidth))) + val (unex, reason) = pred(ctx.stack.upop()) + val err = new ClassicUnexpectedError(state.offset, state.line, state.col, None, new UnexpectDesc(unex, caretWidth)) + ctx.fail(reason.fold[DefuncError](err)(err.withReason(_))) } else ctx.inc() ctx.states = ctx.states.tail @@ -228,7 +230,9 @@ private [internal] final class UnexpectedWhen(pred: PartialFunction[Any, String] // $COVERAGE-ON$ } private [internal] object UnexpectedWhen { - def apply[A](pred: PartialFunction[A, String]): UnexpectedWhen = new UnexpectedWhen(pred.asInstanceOf[PartialFunction[Any, String]]) + def apply[A](pred: PartialFunction[A, (String, Option[String])]): UnexpectedWhen = { + new UnexpectedWhen(pred.asInstanceOf[PartialFunction[Any, (String, Option[String])]]) + } } private [internal] object NegLookFail extends Instr { diff --git a/parsley/shared/src/main/scala/parsley/position.scala b/parsley/shared/src/main/scala/parsley/position.scala index b5a6c2032..33c94a250 100644 --- a/parsley/shared/src/main/scala/parsley/position.scala +++ b/parsley/shared/src/main/scala/parsley/position.scala @@ -7,17 +7,24 @@ import parsley.implicits.zipped.Zipped3 import parsley.internal.deepembedding.singletons -// TODO: In future, the contents of this object will be made public, and the old versions -// will be deprecated for removal in 5.0.0 -private [parsley] object position { - /** This parser returns the current line number of the input without having any other effect. +/** This module contains parsers that provide a way to extract position information during a parse. + * + * Position parsers can be important + * for when the final result of the parser needs to encode position information for later consumption: + * this is particularly useful for abstract syntax trees. Offset is also exposed by this interface, which + * may be useful for establishing a caret size in specialised error messages. + * + * @since 4.2.0 + */ +object position { + /** This parser returns the current line number (starting at 1) of the input without having any other effect. * * When this combinator is ran, no input is required, nor consumed, and * the current line number will always be successfully returned. It has no other * effect on the state of the parser. * * @example {{{ - * scala> import parsley.Parsley.line, parsley.character.char + * scala> import parsley.position.line, parsley.character.char * scala> line.parse("") * val res0 = Success(1) * scala> (char('a') *> line).parse("a") @@ -30,14 +37,14 @@ private [parsley] object position { * @group pos */ val line: Parsley[Int] = new Parsley(singletons.Line) - /** This parser returns the current column number of the input without having any other effect. + /** This parser returns the current column number (starting at 1) of the input without having any other effect. * * When this combinator is ran, no input is required, nor consumed, and * the current column number will always be successfully returned. It has no other * effect on the state of the parser. * * @example {{{ - * scala> import parsley.Parsley.col, parsley.character.char + * scala> import parsley.position.col, parsley.character.char * scala> col.parse("") * val res0 = Success(1) * scala> (char('a') *> col).parse("a") @@ -51,14 +58,14 @@ private [parsley] object position { * @group pos */ val col: Parsley[Int] = new Parsley(singletons.Col) - /** This parser returns the current line and column numbers of the input without having any other effect. + /** This parser returns the current line and column numbers (starting at 1) of the input without having any other effect. * * When this combinator is ran, no input is required, nor consumed, and * the current line and column number will always be successfully returned. It has no other * effect on the state of the parser. * * @example {{{ - * scala> import parsley.Parsley.pos, parsley.character.char + * scala> import parsley.position.pos, parsley.character.char * scala> pos.parse("") * val res0 = Success((1, 1)) * scala> (char('a') *> pos).parse("a") @@ -75,12 +82,30 @@ private [parsley] object position { // this is subject to change at the slightest notice, do NOT expose private [parsley] val internalOffset: Parsley[Int] = new Parsley(singletons.Offset) - // IMPORTANT: this is NOT to be released until a Int/Long stance has been taken - // for offset in the deepest internals - // We could use `BigInt` as an arbiter here, and just declare it's expensive? - private [parsley] val offset: Parsley[BigInt] = internalOffset.map(BigInt(_)) - def spanWith[A, S](end: Parsley[S])(p: Parsley[A]): Parsley[(S, A, S)] = (end, p, end).zipped + /** This parser returns the current offset into the input (starting at 0) without having any other effect. + * + * When this combinator is ran, no input is required, nor consumed, and + * the current offset into the input will always be successfully returned. It has no other + * effect on the state of the parser. + * + * @example {{{ + * scala> import parsley.position.offset, parsley.character.char + * scala> offset.parse("") + * val res0 = Success(0) + * scala> (char('a') *> offset).parse("a") + * val res0 = Success(1) + * scala> (char('\n') *> offset).parse("\n") + * val res0 = Success(1) + * }}} + * + * @return a parser that returns the offset the parser is currently at. + * @note offset does not take wide unicode codepoints into account. + */ + val offset: Parsley[Int] = internalOffset + + // These are useless at 5.0.0 I think + private [parsley] def spanWith[A, S](end: Parsley[S])(p: Parsley[A]): Parsley[(S, A, S)] = (end, p, end).zipped // this is subject to change at the slightest notice, do NOT expose private [parsley] def internalOffsetSpan[A](p: Parsley[A]): Parsley[(Int, A, Int)] = spanWith(internalOffset)(p) } diff --git a/parsley/shared/src/main/scala/parsley/token/Lexer.scala b/parsley/shared/src/main/scala/parsley/token/Lexer.scala index 4706a5407..8fc66fb0e 100644 --- a/parsley/shared/src/main/scala/parsley/token/Lexer.scala +++ b/parsley/shared/src/main/scala/parsley/token/Lexer.scala @@ -224,7 +224,7 @@ private [token] abstract class Lexeme { * the lexer. * @since 4.0.0 */ -@deprecatedInheritance("this class will be made final in 5.0.0", since = "4.1.0") +@deprecatedInheritance("this class will be made final in 5.0.0", since = "4.2.0") class Lexer(desc: descriptions.LexicalDesc, errConfig: errors.ErrorConfig) { /** Builds a new lexer with a given description for the lexical structure of the language. * diff --git a/parsley/shared/src/main/scala/parsley/token/errors/ConfigImplTyped.scala b/parsley/shared/src/main/scala/parsley/token/errors/ConfigImplTyped.scala index 82b4f7b90..7c0a3d0a2 100644 --- a/parsley/shared/src/main/scala/parsley/token/errors/ConfigImplTyped.scala +++ b/parsley/shared/src/main/scala/parsley/token/errors/ConfigImplTyped.scala @@ -3,24 +3,9 @@ */ package parsley.token.errors -import parsley.Parsley, Parsley.pure +import parsley.Parsley +import parsley.XCompat.unused import parsley.errors.combinator, combinator.ErrorMethods -import parsley.position - -private [parsley] object FilterOps { - def amendThenDislodge[A](full: Boolean)(p: Parsley[A]): Parsley[A] = { - if (full) combinator.amendThenDislodge(p) - else p - } - def amendThenDislodgeOrPartial[A](full: Boolean)(p: Parsley[A]): Parsley[A] = { - if (full) combinator.amendThenDislodge(p) - else combinator.partialAmendThenDislodge(p) - } - def entrench[A](full: Boolean)(p: Parsley[A]): Parsley[A] = { - if (full) combinator.entrench(p) - else p - } -} /** This trait, and its subclasses, can be used to configure how filters should be used within the `Lexer`. * @since 4.1.0 @@ -48,32 +33,29 @@ trait VanillaFilterConfig[A] extends FilterConfig[A] /** This class ensures that the filter will generate ''specialised'' messages for the given failing parse. * @since 4.1.0 - * @param fullAmend filters usually have partial amend semantics: should this instead do a full amend? * @group filters */ -abstract class SpecialisedMessage[A](fullAmend: Boolean) extends SpecialisedFilterConfig[A] { self => +abstract class SpecialisedMessage[A] extends SpecialisedFilterConfig[A] { self => + @deprecated("filters do not have partial amend semantics, so this does nothing", "4.1.0") def this(@unused fullAmend: Boolean) = this() /** This method produces the messages for the given value. * @since 4.1.0 * @group badchar */ def message(x: A): Seq[String] - private [parsley] final override def filter(p: Parsley[A])(f: A => Boolean) = FilterOps.amendThenDislodge(fullAmend) { - FilterOps.entrench(fullAmend)(p).guardAgainst { - case x if !f(x) => message(x) - } - } - private [parsley] final override def collect[B](p: Parsley[A])(f: PartialFunction[A, B]) = FilterOps.amendThenDislodge(fullAmend) { - FilterOps.entrench(fullAmend)(p).collectMsg(message(_))(f) + private [parsley] final override def filter(p: Parsley[A])(f: A => Boolean) = p.guardAgainst { + case x if !f(x) => message(x) } + + private [parsley] final override def collect[B](p: Parsley[A])(f: PartialFunction[A, B]) = p.collectMsg(message(_))(f) // $COVERAGE-OFF$ - private [parsley] final override def injectLeft[B] = new SpecialisedMessage[Either[A, B]](fullAmend) { + private [parsley] final override def injectLeft[B] = new SpecialisedMessage[Either[A, B]] { def message(xy: Either[A, B]) = { val Left(x) = xy self.message(x) } } - private [parsley] final override def injectRight[B] = new SpecialisedMessage[Either[B, A]](fullAmend) { + private [parsley] final override def injectRight[B] = new SpecialisedMessage[Either[B, A]] { def message(xy: Either[B, A]) = { val Right(y) = xy self.message(y) @@ -84,29 +66,27 @@ abstract class SpecialisedMessage[A](fullAmend: Boolean) extends SpecialisedFilt /** This class ensures that the filter will generate a ''vanilla'' unexpected item for the given failing parse. * @since 4.1.0 - * @param fullAmend filters usually have partial amend semantics: should this instead do a full amend? * @group filters */ -abstract class Unexpected[A](fullAmend: Boolean) extends VanillaFilterConfig[A] { self => +abstract class Unexpected[A] extends VanillaFilterConfig[A] { self => + @deprecated("filters do not have partial amend semantics, so this does nothing", "4.1.0") def this(@unused fullAmend: Boolean) = this() /** This method produces the unexpected label for the given value. * @since 4.1.0 * @group badchar */ def unexpected(x: A): String - private [parsley] final override def filter(p: Parsley[A])(f: A => Boolean) = FilterOps.amendThenDislodge(fullAmend) { - FilterOps.entrench(fullAmend)(p).unexpectedWhen { - case x if !f(x) => unexpected(x) - } + private [parsley] final override def filter(p: Parsley[A])(f: A => Boolean) = p.unexpectedWhen { + case x if !f(x) => unexpected(x) } // $COVERAGE-OFF$ - private [parsley] final override def injectLeft[B] = new Unexpected[Either[A, B]](fullAmend) { + private [parsley] final override def injectLeft[B] = new Unexpected[Either[A, B]] { def unexpected(xy: Either[A, B]) = { val Left(x) = xy self.unexpected(x) } } - private [parsley] final override def injectRight[B] = new Unexpected[Either[B, A]](fullAmend) { + private [parsley] final override def injectRight[B] = new Unexpected[Either[B, A]] { def unexpected(xy: Either[B, A]) = { val Right(y) = xy self.unexpected(y) @@ -117,29 +97,27 @@ abstract class Unexpected[A](fullAmend: Boolean) extends VanillaFilterConfig[A] /** This class ensures that the filter will generate a ''vanilla'' reason for the given failing parse. * @since 4.1.0 - * @param fullAmend filters usually have partial amend semantics: should this instead do a full amend? * @group filters */ -abstract class Because[A](fullAmend: Boolean) extends VanillaFilterConfig[A] { self => +abstract class Because[A] extends VanillaFilterConfig[A] { self => + @deprecated("filters do not have partial amend semantics, so this does nothing", "4.1.0") def this(@unused fullAmend: Boolean) = this() /** This method produces the reason for the given value. * @since 4.1.0 * @group badchar */ def reason(x: A): String - private [parsley] final override def filter(p: Parsley[A])(f: A => Boolean) = FilterOps.amendThenDislodge(fullAmend) { - FilterOps.entrench(fullAmend)(p).filterOut { - case x if !f(x) => reason(x) - } + private [parsley] final override def filter(p: Parsley[A])(f: A => Boolean) = p.filterOut { + case x if !f(x) => reason(x) } // $COVERAGE-OFF$ - private [parsley] final override def injectLeft[B] = new Because[Either[A, B]](fullAmend) { + private [parsley] final override def injectLeft[B] = new Because[Either[A, B]] { def reason(xy: Either[A, B]) = { val Left(x) = xy self.reason(x) } } - private [parsley] final override def injectRight[B] = new Because[Either[B, A]](fullAmend) { + private [parsley] final override def injectRight[B] = new Because[Either[B, A]] { def reason(xy: Either[B, A]) = { val Right(y) = xy self.reason(y) @@ -150,10 +128,10 @@ abstract class Because[A](fullAmend: Boolean) extends VanillaFilterConfig[A] { s /** This class ensures that the filter will generate a ''vanilla'' unexpected item and a reason for the given failing parse. * @since 4.1.0 - * @param fullAmend filters usually have partial amend semantics: should this instead do a full amend? * @group filters */ -abstract class UnexpectedBecause[A](fullAmend: Boolean) extends VanillaFilterConfig[A] { self => +abstract class UnexpectedBecause[A] extends VanillaFilterConfig[A] { self => + @deprecated("filters do not have partial amend semantics, so this does nothing", "4.1.0") def this(@unused fullAmend: Boolean) = this() /** This method produces the unexpected label for the given value. * @since 4.1.0 * @group badchar @@ -166,14 +144,11 @@ abstract class UnexpectedBecause[A](fullAmend: Boolean) extends VanillaFilterCon def reason(x: A): String // TODO: factor this combinator out with the "Great Move" in 4.2 - private [parsley] final override def filter(p: Parsley[A])(f: A => Boolean) = FilterOps.amendThenDislodgeOrPartial(fullAmend) { - position.internalOffsetSpan(combinator.entrench(p)).flatMap { case (os, x, oe) => - if (f(x)) combinator.unexpected(oe - os, this.unexpected(x)).explain(reason(x)) - else pure(x) - } + private [parsley] final override def filter(p: Parsley[A])(f: A => Boolean) = p.unexpectedWithReasonWhen { + case x if !f(x) => (unexpected(x), reason(x)) } // $COVERAGE-OFF$ - private [parsley] final override def injectLeft[B] = new UnexpectedBecause[Either[A, B]](fullAmend) { + private [parsley] final override def injectLeft[B] = new UnexpectedBecause[Either[A, B]] { def unexpected(xy: Either[A, B]) = { val Left(x) = xy self.unexpected(x) @@ -183,7 +158,7 @@ abstract class UnexpectedBecause[A](fullAmend: Boolean) extends VanillaFilterCon self.reason(x) } } - private [parsley] final override def injectRight[B] = new UnexpectedBecause[Either[B, A]](fullAmend) { + private [parsley] final override def injectRight[B] = new UnexpectedBecause[Either[B, A]] { def unexpected(xy: Either[B, A]) = { val Right(y) = xy self.unexpected(y) diff --git a/parsley/shared/src/main/scala/parsley/token/errors/ErrorConfig.scala b/parsley/shared/src/main/scala/parsley/token/errors/ErrorConfig.scala index 1fee4e5cd..53a72a82a 100644 --- a/parsley/shared/src/main/scala/parsley/token/errors/ErrorConfig.scala +++ b/parsley/shared/src/main/scala/parsley/token/errors/ErrorConfig.scala @@ -361,7 +361,7 @@ class ErrorConfig { * @note defaults to a ''specialised'' error describing what the min and max bounds are. * @group numeric */ - def filterIntegerOutOfBounds(min: BigInt, max: BigInt, nativeRadix: Int): FilterConfig[BigInt] = new SpecialisedMessage[BigInt](fullAmend = false) { + def filterIntegerOutOfBounds(min: BigInt, max: BigInt, nativeRadix: Int): FilterConfig[BigInt] = new SpecialisedMessage[BigInt] { def message(n: BigInt) = Seq(s"literal is not within the range ${min.toString(nativeRadix)} to ${max.toString(nativeRadix)}") } @@ -371,7 +371,7 @@ class ErrorConfig { * @note defaults to a ''specialised'' error stating that the literal is not exactly representable. * @group numeric */ - def filterRealNotExact(name: String): FilterConfig[BigDecimal] = new SpecialisedMessage[BigDecimal](fullAmend = false) { + def filterRealNotExact(name: String): FilterConfig[BigDecimal] = new SpecialisedMessage[BigDecimal] { def message(n: BigDecimal) = Seq(s"literal cannot be represented exactly as an $name") } @@ -384,7 +384,7 @@ class ErrorConfig { * @group numeric */ def filterRealOutOfBounds(name: String, min: BigDecimal, max: BigDecimal): FilterConfig[BigDecimal] = - new SpecialisedMessage[BigDecimal](fullAmend = false) { + new SpecialisedMessage[BigDecimal] { def message(n: BigDecimal) = Seq(s"literal is not within the range $min to $max and is not an $name") } @@ -453,7 +453,7 @@ class ErrorConfig { * @note defaults to unexpected "identifier v" * @group names */ - def filterNameIllFormedIdentifier: FilterConfig[String] = new Unexpected[String](fullAmend = false) { + def filterNameIllFormedIdentifier: FilterConfig[String] = new Unexpected[String] { def unexpected(v: String) = s"identifer $v" } /** When parsing operators that are required to have specific start/end characters, how bad operators should be reported. @@ -461,7 +461,7 @@ class ErrorConfig { * @note defaults to unexpected "operator v" * @group names */ - def filterNameIllFormedOperator: FilterConfig[String] = new Unexpected[String](fullAmend = false) { + def filterNameIllFormedOperator: FilterConfig[String] = new Unexpected[String] { def unexpected(v: String) = s"operator $v" } @@ -632,7 +632,7 @@ class ErrorConfig { * @note defaults to a filter generating the reason "non-BMP character" * @group text */ - def filterCharNonBasicMultilingualPlane: VanillaFilterConfig[Int] = new Because[Int](fullAmend = false) { + def filterCharNonBasicMultilingualPlane: VanillaFilterConfig[Int] = new Because[Int] { def reason(@unused x: Int) = "non-BMP character" } /** When a non-ASCII character is found in a ASCII-only character literal, specifies how this should be reported. @@ -640,7 +640,7 @@ class ErrorConfig { * @note defaults to a filter generating the reason "non-ascii character" * @group text */ - def filterCharNonAscii: VanillaFilterConfig[Int] = new Because[Int](fullAmend = false) { + def filterCharNonAscii: VanillaFilterConfig[Int] = new Because[Int] { def reason(@unused x: Int) = "non-ascii character" } /** When a non-Latin1 character is found in a Latin1-only character literal, specifies how this should be reported. @@ -648,7 +648,7 @@ class ErrorConfig { * @note defaults to a filter generating the reason "non-latin1 character" * @group text */ - def filterCharNonLatin1: VanillaFilterConfig[Int] = new Because[Int](fullAmend = false) { + def filterCharNonLatin1: VanillaFilterConfig[Int] = new Because[Int] { def reason(@unused x: Int) = "non-latin1 character" } @@ -657,7 +657,7 @@ class ErrorConfig { * @note defaults to a filter generating a ''specialised'' message of "non-ascii characters in string literal, this is not allowed" * @group text */ - def filterStringNonAscii: SpecialisedFilterConfig[StringBuilder] = new SpecialisedMessage[StringBuilder](fullAmend = false) { + def filterStringNonAscii: SpecialisedFilterConfig[StringBuilder] = new SpecialisedMessage[StringBuilder] { def message(@unused s: StringBuilder) = Seq("non-ascii characters in string literal, this is not allowed") } @@ -666,7 +666,7 @@ class ErrorConfig { * @note defaults to a filter generating a ''specialised'' message of "non-latin1 characters in string literal, this is not allowed" * @group text */ - def filterStringNonLatin1: SpecialisedFilterConfig[StringBuilder] = new SpecialisedMessage[StringBuilder](fullAmend = false) { + def filterStringNonLatin1: SpecialisedFilterConfig[StringBuilder] = new SpecialisedMessage[StringBuilder] { def message(@unused s: StringBuilder) = Seq("non-latin1 characters in string literal, this is not allowed") } @@ -679,7 +679,7 @@ class ErrorConfig { * @group text */ def filterEscapeCharRequiresExactDigits(@unused radix: Int, needed: Seq[Int]): SpecialisedFilterConfig[Int] = - new SpecialisedMessage[Int](fullAmend = false) { + new SpecialisedMessage[Int] { def message(got: Int) = Seq( s"numeric escape requires ${parsley.errors.helpers.combineAsList(needed.toList.map(_.toString))} digits, but only got $got" ) @@ -693,7 +693,7 @@ class ErrorConfig { * @group text */ def filterEscapeCharNumericSequenceIllegal(maxEscape: Int, radix: Int): SpecialisedFilterConfig[BigInt] = - new SpecialisedMessage[BigInt](fullAmend = false) { + new SpecialisedMessage[BigInt] { def message(escapeChar: BigInt) = Seq( if (escapeChar > BigInt(maxEscape)) { s"${escapeChar.toString(radix)} is greater than the maximum character value of ${BigInt(maxEscape).toString(radix)}" diff --git a/parsley/shared/src/main/scala/parsley/token/errors/VerifiedAndPreventativeErrors.scala b/parsley/shared/src/main/scala/parsley/token/errors/VerifiedAndPreventativeErrors.scala index ba2bb1083..4b67a5e6a 100644 --- a/parsley/shared/src/main/scala/parsley/token/errors/VerifiedAndPreventativeErrors.scala +++ b/parsley/shared/src/main/scala/parsley/token/errors/VerifiedAndPreventativeErrors.scala @@ -3,10 +3,9 @@ */ package parsley.token.errors -import parsley.Parsley, Parsley.{pure, empty} +import parsley.Parsley, Parsley.empty import parsley.character.satisfyUtf16 -import parsley.errors.combinator, combinator.ErrorMethods -import parsley.position +import parsley.errors.{combinator, patterns}, combinator.ErrorMethods, patterns.VerifiedErrors /** This class is used to configure what error is generated when `.` is parsed as a real number. * @since 4.1.0 @@ -26,13 +25,9 @@ object UnexpectedZeroDot { def apply(unexpected: String): PreventDotIsZeroConfig = new UnexpectedZeroDot(unexpected) } -// TODO: factor this combinator out with the "Great Move" in 4.2 private final class UnexpectedZeroDotWithReason private (unexpected: String, reason: String) extends PreventDotIsZeroConfig { - private [token] override def apply(p: Parsley[Boolean]): Parsley[Boolean] = combinator.partialAmendThenDislodge { - position.internalOffsetSpan(combinator.entrench(p)).flatMap { case (os, x, oe) => - if (x) combinator.unexpected(oe - os, unexpected).explain(reason) - else pure(x) - } + private [token] override def apply(p: Parsley[Boolean]): Parsley[Boolean] = p.unexpectedWithReasonWhen { + case true => (unexpected, reason) } } /** This object makes "dot is zero" generate a given unexpected message with a given reason in a ''vanilla'' error. @@ -73,7 +68,7 @@ sealed abstract class VerifiedBadChars { private [token] def checkBadChar: Parsley[Nothing] } private final class BadCharsFail private (cs: Map[Int, Seq[String]]) extends VerifiedBadChars { - private [token] def checkBadChar: Parsley[Nothing] = satisfyUtf16(cs.contains).fail(cs.apply(_)) + private [token] def checkBadChar: Parsley[Nothing] = satisfyUtf16(cs.contains).verifiedFail(cs.apply(_)) } /** This object makes "bad literal chars" generate a bunch of given messages in a ''specialised'' error. Requires a map from bad characters to their messages. * @since 4.1.0 @@ -84,7 +79,7 @@ object BadCharsFail { } private final class BadCharsReason private (cs: Map[Int, String]) extends VerifiedBadChars { - private [token] def checkBadChar: Parsley[Nothing] = satisfyUtf16(cs.contains)._unexpected(cs.apply) + private [token] def checkBadChar: Parsley[Nothing] = satisfyUtf16(cs.contains).verifiedUnexpected(cs.apply(_)) } /** This object makes "bad literal chars" generate a reason in a ''vanilla'' error. Requires a map from bad characters to their reasons. * @since 4.1.0 diff --git a/parsley/shared/src/main/scala/parsley/token/text/ConcreteString.scala b/parsley/shared/src/main/scala/parsley/token/text/ConcreteString.scala index b883927a4..91d2ea85f 100644 --- a/parsley/shared/src/main/scala/parsley/token/text/ConcreteString.scala +++ b/parsley/shared/src/main/scala/parsley/token/text/ConcreteString.scala @@ -8,6 +8,7 @@ import scala.Predef.{String => ScalaString, _} import parsley.Parsley, Parsley.{attempt, fresh, pure} import parsley.character.{char, string} import parsley.combinator.{choice, skipManyUntil} +import parsley.errors.combinator.ErrorMethods import parsley.implicits.zipped.Zipped2 import parsley.token.errors.{ErrorConfig, LabelConfig, LabelWithExplainConfig} import parsley.token.predicate.CharPredicate @@ -40,7 +41,7 @@ private [token] final class ConcreteString(ends: Set[ScalaString], stringChar: S openLabel(allowsAllSpace, stringChar.isRaw)(terminal) *> // then only one string builder needs allocation sbReg.put(fresh(new StringBuilder)) *> - skipManyUntil(sbReg.modify(char(terminalInit) #> ((sb: StringBuilder) => sb += terminalInit)) <|> content, + skipManyUntil(sbReg.modify(char(terminalInit).hide #> ((sb: StringBuilder) => sb += terminalInit)) <|> content, closeLabel(allowsAllSpace, stringChar.isRaw)(attempt(terminal))) //is the attempt needed here? not sure } } diff --git a/parsley/shared/src/test/scala/parsley/ErrorTests.scala b/parsley/shared/src/test/scala/parsley/ErrorTests.scala index 190f9318f..22e1c300a 100644 --- a/parsley/shared/src/test/scala/parsley/ErrorTests.scala +++ b/parsley/shared/src/test/scala/parsley/ErrorTests.scala @@ -7,15 +7,14 @@ import parsley.combinator.{eof, optional} import parsley.Parsley._ import parsley.implicits.character.{charLift, stringLift} import parsley.character.{item, digit} -import parsley.errors.combinator.{fail => pfail, unexpected, amend, entrench, ErrorMethods} +import parsley.errors.combinator.{fail => pfail, unexpected, amend, entrench, dislodge, amendThenDislodge, ErrorMethods} +import parsley.errors.patterns._ class ErrorTests extends ParsleyTest { "mzero parsers" should "always fail" in { (Parsley.empty ~> 'a').parse("a") shouldBe a [Failure[_]] (pfail("") ~> 'a').parse("a") shouldBe a [Failure[_]] (unexpected("x") *> 'a').parse("a") shouldBe a [Failure[_]] - (('a' ! (_ => "")) *> 'b').parse("ab") shouldBe a [Failure[_]] - ('a'.unexpected(_ => "x") *> 'b').parse("ab") shouldBe a [Failure[_]] } "filtering parsers" should "function correctly" in { @@ -35,6 +34,24 @@ class ErrorTests extends ParsleyTest { } inside(q.parse("a")) { case Failure(TestError((1, 1), SpecialisedError(msgs))) => msgs should contain only ("'a' is not uppercase") } q.parse("A") shouldBe Success('A') + + val r = item.unexpectedWithReasonWhen { + case c if c.isLower => ("lowercase letter", s"'$c' should have been uppercase") + } + inside(r.parse("a")) { case Failure(TestError((1, 1), VanillaError(unex, exs, reasons))) => + unex should contain (Named("lowercase letter")) + exs shouldBe empty + reasons should contain only ("'a' should have been uppercase") + } + + val s = item.unexpectedWhen { + case c if c.isLower => "lowercase letter" + } + inside(s.parse("a")) { case Failure(TestError((1, 1), VanillaError(unex, exs, reasons))) => + unex should contain (Named("lowercase letter")) + exs shouldBe empty + reasons shouldBe empty + } } "the collectMsg combinator" should "act like a filter then a map" in { @@ -310,7 +327,7 @@ class ErrorTests extends ParsleyTest { (amend('a' *> 'b') <|> 'a').parse("a") shouldBe a [Failure[_]] } - "amend" should "prevent the change of error messages under it" in { + "entrench" should "prevent the change of error messages under it" in { val p = 'a' *> amend('b' *> entrench('c') *> 'd') inside(p.parse("ab")) { case Failure(TestError((1, 3), _)) => } inside(p.parse("abc")) { case Failure(TestError((1, 2), _)) => } @@ -326,6 +343,21 @@ class ErrorTests extends ParsleyTest { inside(p.parse("abcde")) { case Failure(TestError((1, 2), _)) => } } + "dislodge" should "undo an entrench so that amend works again" in { + val p = 'a' *> amend('b' *> dislodge(entrench('c')) *> 'd') + inside(p.parse("ab")) { case Failure(TestError((1, 2), _)) => } + inside(p.parse("abc")) { case Failure(TestError((1, 2), _)) => } + } + it should "not prevent another entrench from occurring" in { + val p = 'a' *> amend('b' *> entrench(dislodge(entrench('c')))) + inside(p.parse("ab")) { case Failure(TestError((1, 3), _)) => } + } + + "amendThenDislodge" should "amend only non-entrenched messages and dislodge those that are" in { + val p = amend('a' *> amendThenDislodge('b' *> entrench('c'))) + inside(p.parse("ab")) { case Failure(TestError((1, 1), _)) => } + } + "oneOf" should "incorporate range notation into the error" in { inside(character.oneOf('0' to '9').parse("a")) { case Failure(TestError(_, VanillaError(_, expecteds, _))) => @@ -354,6 +386,55 @@ class ErrorTests extends ParsleyTest { } } + // Verified Errors + "verifiedFail" should "fail having consumed input on the parser success" in { + inside(optional("abc".verifiedFail(x => Seq("no, no", s"absolutely not $x"))).parse("abc")) { + case Failure(TestError((1, 1), SpecialisedError(msgs))) => + msgs should contain only ("no, no", "absolutely not abc") + } + } + it should "not consume input if the parser did not succeed" in { + optional("abc".verifiedFail("no, no", "absolutely not")).parse("ab") shouldBe Success(()) + } + it should "not produce any labels" in { + inside("abc".verifiedFail("hi").parse("ab")) { + case Failure(TestError((1, 1), VanillaError(None, expecteds, _))) => + expecteds shouldBe empty + } + } + + "verifiedUnexpected" should "fail having consumed input on the parser success" in { + inside(optional("abc".verifiedUnexpected(x => s"$x is not allowed")).parse("abc")) { + case Failure(TestError((1, 1), VanillaError(unex, expecteds, reasons))) => + expecteds shouldBe empty + unex should contain (Raw("abc")) + reasons should contain only ("abc is not allowed") + } + inside(optional("abc".verifiedUnexpected(s"abc is not allowed")).parse("abc")) { + case Failure(TestError((1, 1), VanillaError(unex, expecteds, reasons))) => + expecteds shouldBe empty + unex should contain (Raw("abc")) + reasons should contain only ("abc is not allowed") + } + inside(optional("abc".verifiedUnexpected).parse("abc")) { + case Failure(TestError((1, 1), VanillaError(unex, expecteds, reasons))) => + expecteds shouldBe empty + unex should contain (Raw("abc")) + reasons shouldBe empty + } + } + it should "not consume input if the parser did not succeed" in { + optional("abc".verifiedUnexpected(x => s"$x is not allowed")).parse("ab") shouldBe Success(()) + optional("abc".verifiedUnexpected(s"abc is not allowed")).parse("ab") shouldBe Success(()) + optional("abc".verifiedUnexpected).parse("ab") shouldBe Success(()) + } + it should "not produce any labels" in { + inside("abc".verifiedUnexpected.parse("ab")) { + case Failure(TestError((1, 1), VanillaError(None, expecteds, _))) => + expecteds shouldBe empty + } + } + // Issue 107 "hints" should "incorporate only with errors at the same offset depth" in { val p = attempt('a' ~> digit) diff --git a/parsley/shared/src/test/scala/parsley/ExpressionParserTests.scala b/parsley/shared/src/test/scala/parsley/ExpressionParserTests.scala index 270e372cd..ce6a39dfd 100644 --- a/parsley/shared/src/test/scala/parsley/ExpressionParserTests.scala +++ b/parsley/shared/src/test/scala/parsley/ExpressionParserTests.scala @@ -11,7 +11,7 @@ import parsley.character.digit import parsley.implicits.character.{charLift, stringLift} import parsley.expr.{chain, infix, mixed} import parsley.expr.{precedence, Ops, GOps, SOps, InfixL, InfixR, Prefix, Postfix, InfixN, Atoms} -import parsley.Parsley._ +import parsley.position._ import parsley.genericbridges._ class ExpressionParserTests extends ParsleyTest { diff --git a/parsley/shared/src/test/scala/parsley/PositionTests.scala b/parsley/shared/src/test/scala/parsley/PositionTests.scala new file mode 100644 index 000000000..322ddbc60 --- /dev/null +++ b/parsley/shared/src/test/scala/parsley/PositionTests.scala @@ -0,0 +1,44 @@ +/* SPDX-FileCopyrightText: © 2023 Parsley Contributors + * SPDX-License-Identifier: BSD-3-Clause + */ +package parsley + +import parsley.position._ +import parsley.character.char + +class PositionTests extends ParsleyTest { + "line" should "start at 1" in { + line.parse("") shouldBe Success(1) + } + it should "increment on newline" in { + (char('\n') ~> line).parse("\n") shouldBe Success(2) + } + it should "not increment on tabs or other chars" in { + (char('\t') ~> line).parse("\t") shouldBe Success(1) + (char('a') ~> line).parse("a") shouldBe Success(1) + } + + "col" should "start at 1" in { + col.parse("") shouldBe Success(1) + } + it should "reset on newline" in { + (char('a') ~> char('\n') ~> col).parse("a\n") shouldBe Success(1) + } + it should "go to tab boundaries" in { + (char('\t') ~> col).parse("\t") shouldBe Success(5) + (char('a') ~> char('\t') ~> col).parse("a\t") shouldBe Success(5) + (char('a') ~> char('a') ~> char('a') ~> char('a') ~> char('\t') ~> col).parse("aaaa\t") shouldBe Success(9) + } + it should "increment on other chars" in { + (char('a') ~> col).parse("a") shouldBe Success(2) + } + + "offset" should "start at 0" in { + offset.parse("") shouldBe Success(0) + } + it should "only increase by one regardless of character" in { + (char('\n') ~> offset).parse("\n") shouldBe Success(1) + (char('\t') ~> offset).parse("\t") shouldBe Success(1) + (char('a') ~> offset).parse("a") shouldBe Success(1) + } +} diff --git a/parsley/shared/src/test/scala/parsley/StringTests.scala b/parsley/shared/src/test/scala/parsley/StringTests.scala index 9b2a06db0..f42fab05d 100644 --- a/parsley/shared/src/test/scala/parsley/StringTests.scala +++ b/parsley/shared/src/test/scala/parsley/StringTests.scala @@ -7,7 +7,8 @@ import Predef.{ArrowAssoc => _, _} import parsley.character.{letter, string, strings, stringOfMany, stringOfSome} import parsley.implicits.character.{charLift, stringLift} -import parsley.Parsley._ +import parsley.Parsley.{pos => _, _} +import parsley.position.pos class StringTests extends ParsleyTest { private def stringPositionCheck(initialCol: Int, str: String) = { diff --git a/parsley/shared/src/test/scala/parsley/internal/machine/errors/DefuncErrorTests.scala b/parsley/shared/src/test/scala/parsley/internal/machine/errors/DefuncErrorTests.scala index 3f0b4e5f8..fc0243ae3 100644 --- a/parsley/shared/src/test/scala/parsley/internal/machine/errors/DefuncErrorTests.scala +++ b/parsley/shared/src/test/scala/parsley/internal/machine/errors/DefuncErrorTests.scala @@ -192,6 +192,7 @@ class DefuncErrorTests extends ParsleyTest { "Entrenched" should "guard against amendment" in { val err = new EmptyError(0, 0, 0, 0).entrench.amend(10, 10, 10) val errOut = err.asParseError + err.entrenched shouldBe true errOut.col shouldBe 0 errOut.line shouldBe 0 errOut.offset shouldBe 0 @@ -199,8 +200,28 @@ class DefuncErrorTests extends ParsleyTest { it should "work for fancy errors too" in { val err = new ClassicFancyError(0, 0, 0, 1, "").entrench.amend(10, 10, 10) val errOut = err.asParseError + err.entrenched shouldBe true errOut.col shouldBe 0 errOut.line shouldBe 0 errOut.offset shouldBe 0 } + + "Dislodged" should "remove an entrenchment" in { + val err = new EmptyError(0, 0, 0, 0).entrench + require(err.entrenched) + err.dislodge.entrenched shouldBe false + val err2 = err.dislodge.amend(10, 10, 10).asParseError + err2.col shouldBe 10 + err2.line shouldBe 10 + err2.offset shouldBe 10 + } + it should "work for fancy errors too" in { + val err = new ClassicFancyError(0, 0, 0, 1, "").entrench + require(err.entrenched) + err.dislodge.entrenched shouldBe false + val err2 = err.dislodge.amend(10, 10, 10).asParseError + err2.col shouldBe 10 + err2.line shouldBe 10 + err2.offset shouldBe 10 + } }