From bfdfdd0b1f9d74cef5f83d2e7592aa8fc6ea092c Mon Sep 17 00:00:00 2001 From: i10416 Date: Sun, 13 Feb 2022 17:59:27 +0900 Subject: [PATCH 01/23] wip:add native build --- .github/workflows/ci.yml | 4 +- build.sbt | 5 +- .../js/src/main/scala/cats/parse/BitSet.scala | 1 + .../src/main/scala/cats/parse/BitSet.scala | 2 +- .../src/main/scala/cats/parse/BitSet.scala | 69 +++++++++++++++++++ .../test/scala/cats/parse/BitSetTest.scala | 9 ++- project/plugins.sbt | 2 + 7 files changed, 82 insertions(+), 10 deletions(-) create mode 100644 core/native/src/main/scala/cats/parse/BitSet.scala diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6bb657d5..ae7c478a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,11 +67,11 @@ jobs: - name: Make target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') - run: mkdir -p target docs/target core/js/target core/jvm/target project/target + run: mkdir -p target core/native/target docs/target core/js/target core/jvm/target project/target - name: Compress target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') - run: tar cf targets.tar target docs/target core/js/target core/jvm/target project/target + run: tar cf targets.tar target core/native/target docs/target core/js/target core/jvm/target project/target - name: Upload target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') diff --git a/build.sbt b/build.sbt index 5696119a..d6f7b015 100644 --- a/build.sbt +++ b/build.sbt @@ -77,7 +77,7 @@ lazy val jvmVersionSettings = VersionNumber(sys.props("java.version")) match { lazy val root = project .in(file(".")) - .aggregate(core.jvm, core.js, bench) + .aggregate(core.jvm, core.js, core.native, bench) .enablePlugins(NoPublishPlugin) .settings(scalaVersion := "2.13.8") @@ -109,7 +109,7 @@ lazy val docs = project ) .dependsOn(coreJVM, bench) -lazy val core = crossProject(JSPlatform, JVMPlatform) +lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) .crossType(CrossType.Full) .settings( name := "cats-parse", @@ -156,6 +156,7 @@ lazy val core = crossProject(JSPlatform, JVMPlatform) lazy val coreJVM = core.jvm.settings(jvmVersionSettings) lazy val coreJS = core.js +lazy val coreNative = core.native lazy val bench = project .enablePlugins(JmhPlugin, NoPublishPlugin) diff --git a/core/js/src/main/scala/cats/parse/BitSet.scala b/core/js/src/main/scala/cats/parse/BitSet.scala index 3e14f75b..df2c9280 100644 --- a/core/js/src/main/scala/cats/parse/BitSet.scala +++ b/core/js/src/main/scala/cats/parse/BitSet.scala @@ -26,6 +26,7 @@ import scala.collection.mutable.BitSet object BitSetUtil { type Tpe = BitSet + @inline final val isScalaNative = false @inline final val isScalaJs = true @inline final val isScalaJvm = false diff --git a/core/jvm/src/main/scala/cats/parse/BitSet.scala b/core/jvm/src/main/scala/cats/parse/BitSet.scala index b84674cb..fae46c1b 100644 --- a/core/jvm/src/main/scala/cats/parse/BitSet.scala +++ b/core/jvm/src/main/scala/cats/parse/BitSet.scala @@ -25,7 +25,7 @@ import java.util.BitSet object BitSetUtil { type Tpe = BitSet - + @inline final val isScalaNative = false @inline final val isScalaJs = false @inline final val isScalaJvm = true diff --git a/core/native/src/main/scala/cats/parse/BitSet.scala b/core/native/src/main/scala/cats/parse/BitSet.scala new file mode 100644 index 00000000..471e08b6 --- /dev/null +++ b/core/native/src/main/scala/cats/parse/BitSet.scala @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2021 Typelevel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package cats.parse + +import scala.collection.mutable.BitSet + +object BitSetUtil { + type Tpe = BitSet + @inline final val isScalaNative = true + @inline final val isScalaJs = false + @inline final val isScalaJvm = false + + @inline final def isSet(b: BitSet, idx: Int): Boolean = + (idx >= 0) && b(idx) + + def bitSetFor(charArray: Array[Char]): BitSet = { + val min = charArray(0).toInt + val bs = new BitSet(charArray(charArray.length - 1).toInt + 1 - min) + var idx = 0 + while (idx < charArray.length) { + bs += charArray(idx).toInt - min + idx += 1 + } + + bs + } + + def isSingleton(t: Tpe): Boolean = t.size == 1 + + def union(bs: List[(Int, BitSet)]): Iterable[Char] = + union(bs.iterator) + + // what are all the Chars in these bitsets + def union(bs: Iterator[(Int, BitSet)]): Iterable[Char] = { + def toIter(m: Int, bs: BitSet): Iterator[Char] = + bs.iterator.map { i => (i + m).toChar } ++ Iterator.single(m.toChar) + + bs.flatMap { case (m, bs) => toIter(m, bs) }.toSet + } + + def bitSetForRange(count: Int): BitSet = { + val bs = new BitSet(count) + var cur = 0 + while (cur < count) { + bs += cur + cur += 1 + } + bs + } +} diff --git a/core/shared/src/test/scala/cats/parse/BitSetTest.scala b/core/shared/src/test/scala/cats/parse/BitSetTest.scala index d2c989fa..48b16969 100644 --- a/core/shared/src/test/scala/cats/parse/BitSetTest.scala +++ b/core/shared/src/test/scala/cats/parse/BitSetTest.scala @@ -24,11 +24,10 @@ package cats.parse import org.scalacheck.Prop.forAll class BitSetTest extends munit.ScalaCheckSuite { - test("isScalaJs/isScalaJvm is consistent") { - // This will need to be updated if we ever add scala-native - assert(!(BitSetUtil.isScalaJs && BitSetUtil.isScalaJvm)) - assert(BitSetUtil.isScalaJs || BitSetUtil.isScalaJvm) - assert(BitSetUtil.isScalaJs ^ BitSetUtil.isScalaJvm) + test("isScalaJs/isScalaJvm/isScalaNative is consistent") { + assert(!(BitSetUtil.isScalaJs && BitSetUtil.isScalaJvm && BitSetUtil.isScalaNative)) + assert(BitSetUtil.isScalaJs || BitSetUtil.isScalaJvm || BitSetUtil.isScalaNative) + assert(BitSetUtil.isScalaJs ^ BitSetUtil.isScalaJvm ^ BitSetUtil.isScalaNative) } property("BitSetUtil union works") { diff --git a/project/plugins.sbt b/project/plugins.sbt index 90fcde32..1910c4ae 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -2,7 +2,9 @@ addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.1.0") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.10.0") addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.9.2") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.1.0") +addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.1.0") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.8.0") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.1") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.9.3") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.3") addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.4.4") From f5f2cbfb21634cdc852da32325b8ba3890e4fcd0 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sun, 13 Feb 2022 11:05:17 +0100 Subject: [PATCH 02/23] Update sbt-assembly to 1.1.1 (#377) --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 1910c4ae..7fc7541f 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,4 +1,4 @@ -addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.1.0") +addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.1.1") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.10.0") addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.9.2") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.1.0") From e80ab780487cacf69f7443ed030d62fbdd4a182d Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sun, 13 Feb 2022 11:05:34 +0100 Subject: [PATCH 03/23] Update sbt to 1.6.2 (#366) --- project/build.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/build.properties b/project/build.properties index 3161d214..c8fcab54 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.6.1 +sbt.version=1.6.2 From cdc16a0fe542cb329c801998b88ff8f12ba9dd94 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sun, 13 Feb 2022 11:05:54 +0100 Subject: [PATCH 04/23] Update sbt-typelevel to 0.4.5 (#374) --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 7fc7541f..acd87735 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -7,7 +7,7 @@ addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.8.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.1") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.9.3") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.3") -addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.4.4") +addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.4.5") addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.0") addSbtPlugin("io.github.jonas" % "sbt-paradox-material-theme" % "0.6.0") addSbtPlugin("com.typesafe.sbt" % "sbt-site" % "1.4.1") From 366f147919b62de8b249f010de755397eb927ea6 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sun, 13 Feb 2022 14:27:38 +0100 Subject: [PATCH 05/23] Update sbt-assembly to 1.2.0 (#378) --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index acd87735..7ce94818 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,4 +1,4 @@ -addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.1.1") +addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.2.0") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.10.0") addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.9.2") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.1.0") From 4489359064389cbb225f2c7831c0bb08bc3a7152 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Fri, 18 Feb 2022 01:37:52 +0100 Subject: [PATCH 06/23] Update sbt-buildinfo to 0.11.0 (#383) --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 7ce94818..55a9f85f 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.2.0") -addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.10.0") +addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0") addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.9.2") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.1.0") addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.1.0") From 93180243d51931826d30c179f3df96819e80ec08 Mon Sep 17 00:00:00 2001 From: i10416 Date: Sun, 20 Mar 2022 17:55:29 +0900 Subject: [PATCH 07/23] update plugins --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 55a9f85f..325015b6 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -3,7 +3,7 @@ addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0") addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.9.2") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.1.0") addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.1.0") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.8.0") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.9.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.1") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.9.3") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.3") From 2cdeef2c151df92474be3bd814226fb49d2f4c3f Mon Sep 17 00:00:00 2001 From: "P. Oscar Boykin" Date: Sun, 20 Feb 2022 08:33:39 -1000 Subject: [PATCH 08/23] Remove allocations from StringIn when returning a string (#368) * checkpoint with compiling * refactor merging * use DefiniteString more * try to fix tests * get 3.0.2 passing * fix mima, improve oneOf * checkpoint * get tests passing * more tests, format, mima * remove unused code * simplify isVoided * cleanup * add isUnit * address review comments * add non-voided stringIn benchmarks --- .../cats/parse/bench/StringInBench.scala | 15 +- build.sbt | 27 +- .../src/main/scala/cats/parse/Parser.scala | 637 ++++++++++++------ .../test/scala/cats/parse/ParserTest.scala | 279 ++++++-- 4 files changed, 715 insertions(+), 243 deletions(-) diff --git a/bench/src/main/scala/cats/parse/bench/StringInBench.scala b/bench/src/main/scala/cats/parse/bench/StringInBench.scala index f4307a26..0c6a73ce 100644 --- a/bench/src/main/scala/cats/parse/bench/StringInBench.scala +++ b/bench/src/main/scala/cats/parse/bench/StringInBench.scala @@ -39,7 +39,9 @@ private[parse] class StringInBenchmarks { var radixNode: RadixNode = _ - var stringIn: Parser[Unit] = _ + var stringInV: Parser[Unit] = _ + + var stringInS: Parser[String] = _ var oneOf: Parser[Unit] = _ @@ -60,13 +62,18 @@ private[parse] class StringInBenchmarks { } radixNode = RadixNode.fromStrings(stringsToMatch) - stringIn = Parser.stringIn(stringsToMatch).void + stringInS = Parser.stringIn(stringsToMatch) + stringInV = stringInS.void oneOf = Parser.oneOf(stringsToMatch.map(Parser.string(_))) } @Benchmark - def stringInParse(): Unit = - inputs.foreach(stringIn.parseAll(_)) + def stringInVParse(): Unit = + inputs.foreach(stringInV.parseAll(_)) + + @Benchmark + def stringInSParse(): Unit = + inputs.foreach(stringInS.parseAll(_)) @Benchmark def oneOfParse(): Unit = diff --git a/build.sbt b/build.sbt index d6f7b015..67442c2f 100644 --- a/build.sbt +++ b/build.sbt @@ -135,18 +135,33 @@ lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) if (isScala211) Set.empty else mimaPreviousArtifacts.value }, mimaBinaryIssueFilters ++= { + /* + * It is okay to filter anything in Impl or RadixNode which are private + */ if (tlIsScala3.value) List( - ProblemFilters.exclude[IncompatibleResultTypeProblem]("cats.parse.Parser#State.error"), - ProblemFilters.exclude[IncompatibleMethTypeProblem]("cats.parse.Parser#State.error_="), - ProblemFilters.exclude[IncompatibleMethTypeProblem]("cats.parse.RadixNode.this"), + ProblemFilters.exclude[DirectMissingMethodProblem]("cats.parse.Parser#Impl.mergeCharIn"), + ProblemFilters.exclude[DirectMissingMethodProblem]("cats.parse.Parser#Impl.mergeStrIn"), + ProblemFilters.exclude[DirectMissingMethodProblem]("cats.parse.RadixNode.children"), ProblemFilters.exclude[DirectMissingMethodProblem]("cats.parse.RadixNode.fsts"), ProblemFilters.exclude[DirectMissingMethodProblem]("cats.parse.RadixNode.prefixes"), - ProblemFilters.exclude[DirectMissingMethodProblem]("cats.parse.RadixNode.children"), ProblemFilters.exclude[DirectMissingMethodProblem]("cats.parse.RadixNode.word"), - ProblemFilters.exclude[FinalClassProblem]("cats.parse.RadixNode") + ProblemFilters.exclude[FinalClassProblem]("cats.parse.RadixNode"), + ProblemFilters.exclude[IncompatibleMethTypeProblem]("cats.parse.Parser#State.error_="), + ProblemFilters.exclude[IncompatibleMethTypeProblem]("cats.parse.RadixNode.this"), + ProblemFilters + .exclude[IncompatibleResultTypeProblem]("cats.parse.Parser#Impl#StringIn.parseMut"), + ProblemFilters.exclude[IncompatibleResultTypeProblem]("cats.parse.Parser#Impl.stringIn"), + ProblemFilters.exclude[IncompatibleResultTypeProblem]("cats.parse.Parser#State.error") + ) + else + List( + ProblemFilters.exclude[DirectMissingMethodProblem]("cats.parse.Parser#Impl.mergeCharIn"), + ProblemFilters.exclude[DirectMissingMethodProblem]("cats.parse.Parser#Impl.mergeStrIn"), + ProblemFilters + .exclude[IncompatibleResultTypeProblem]("cats.parse.Parser#Impl#StringIn.parseMut"), + ProblemFilters.exclude[IncompatibleResultTypeProblem]("cats.parse.Parser#Impl.stringIn") ) - else Nil } ) .jsSettings( diff --git a/core/shared/src/main/scala/cats/parse/Parser.scala b/core/shared/src/main/scala/cats/parse/Parser.scala index 710bc155..7212d1d8 100644 --- a/core/shared/src/main/scala/cats/parse/Parser.scala +++ b/core/shared/src/main/scala/cats/parse/Parser.scala @@ -27,6 +27,7 @@ import cats.data.{AndThen, Chain, NonEmptyList} import cats.implicits._ import scala.collection.immutable.SortedSet import scala.collection.mutable.ListBuffer +import scala.annotation.tailrec import java.util.Arrays import scala.collection.immutable.NumericRange @@ -742,7 +743,7 @@ object Parser { } private def mergeInRange(irs: List[InRange]): List[InRange] = { - @annotation.tailrec + @tailrec def merge(rs: List[InRange], aux: Chain[InRange] = Chain.empty): Chain[InRange] = rs match { case x :: y :: rest => @@ -762,14 +763,14 @@ object Parser { Some(OneOfStr(ooss.head.offset, ssb.result().toList)) } - @annotation.tailrec + @tailrec private def stripContext(ex: Expectation): Expectation = ex match { case WithContext(_, inner) => stripContext(inner) case _ => ex } - @annotation.tailrec + @tailrec private def addContext(revCtx: List[String], ex: Expectation): Expectation = revCtx match { case Nil => ex @@ -1007,31 +1008,56 @@ object Parser { * Note: order matters here, since we don't backtrack by default. */ def oneOf[A](parsers: List[Parser[A]]): Parser[A] = { - @annotation.tailrec - def flatten(ls: List[Parser[A]], acc: ListBuffer[Parser[A]]): List[Parser[A]] = - ls match { - case Nil => acc.toList.distinct - case Impl.OneOf(ps) :: rest => - flatten(ps ::: rest, acc) - case Impl.Fail() :: rest => - flatten(rest, acc) - case notOneOf :: rest => - flatten(rest, acc += notOneOf) - } - - val flattened = flatten(parsers, new ListBuffer) - // we unmap if we can to make merging work better - val isStr = flattened.forall(Impl.matchesString) - val maybeUnmap = if (isStr) flattened.map(Impl.unmap) else flattened - - val cs = Impl.mergeCharIn[Any, Parser[Any]](maybeUnmap) - val res = Impl.mergeStrIn[Any, Parser[Any]](cs) match { - case Nil => fail - case p :: Nil => p - case two => Impl.OneOf(two) - } + @tailrec + def loop(ps: List[Parser[A]], acc: List[Parser[A]]): Parser[A] = + ps match { + case Nil => + /* + * we can still have inner oneof if the head items + * were not oneof and couldn't be merged + * but the last items did have oneof + */ + val flat = acc.reverse.flatMap { + case Impl.OneOf(ps) => ps + case one => one :: Nil + } - (if (isStr) string(res) else res).asInstanceOf[Parser[A]] + flat match { + case Nil => Impl.Fail() + case one :: Nil => one + case many => + many.traverse[Option, Parser[Any]] { + case Impl.StringP(p) => Some(p) + case _ => None + } match { + case Some(m0) => + Impl + .StringP(Impl.OneOf(m0)) + .asInstanceOf[Parser[A]] + case None => + many.traverse[Option, Parser[Any]] { + case Impl.Void(p) => Some(p) + case _ => None + } match { + case Some(m0) => + Impl + .Void(Impl.OneOf(m0)) + .asInstanceOf[Parser[A]] + case None => + Impl.OneOf(many) + } + } + } + case h :: Nil => loop(Nil, h :: acc) + case h1 :: (t1 @ (h2 :: tail2)) => + Impl.merge(h1, h2) match { + case Impl.OneOf(a :: b :: Nil) if (a eq h1) && (b eq h2) => + loop(t1, h1 :: acc) + case h => + loop(h :: tail2, acc) + } + } + loop(parsers, Nil) } /** go through the list of parsers trying each as long as they are epsilon failures (don't @@ -1043,40 +1069,42 @@ object Parser { * * Note: order matters here, since we don't backtrack by default. */ - def oneOf0[A](ps: List[Parser0[A]]): Parser0[A] = { - @annotation.tailrec - def flatten(ls: List[Parser0[A]], acc: ListBuffer[Parser0[A]]): List[Parser0[A]] = - ls match { - case Nil => acc.toList.distinct - case Impl.OneOf0(ps) :: rest => - flatten(ps ::: rest, acc) - case Impl.OneOf(ps) :: rest => - flatten(ps ::: rest, acc) - case Impl.Fail() :: rest => - flatten(rest, acc) - case notOneOf :: rest => - if (Impl.alwaysSucceeds(notOneOf)) { - (acc += notOneOf).toList.distinct - } else { - flatten(rest, acc += notOneOf) - } - } - - val flat0 = flatten(ps, new ListBuffer) - // we unmap if we can to make merging work better - val isStr = flat0.forall(Impl.matchesString) - val flat = if (isStr) flat0.map(Impl.unmap0) else flat0 - - val cs = Impl.mergeCharIn[Any, Parser0[Any]](flat) - val res = Impl.mergeStrIn[Any, Parser0[Any]](cs) match { - case Nil => fail - case p :: Nil => p - case two => Impl.OneOf0(two) + def oneOf0[A](ps: List[Parser0[A]]): Parser0[A] = + if (ps.forall(_.isInstanceOf[Parser[_]])) oneOf(ps.asInstanceOf[List[Parser[A]]]) + else { + @tailrec + def loop(ps: List[Parser0[A]], acc: List[Parser0[A]]): Parser0[A] = + ps match { + case Nil => + /* + * we can still have inner oneof if the head items + * were not oneof and couldn't be merged + * but the last items did have oneof + */ + val flat = acc.reverse.flatMap { + case Impl.OneOf(ps) => ps + case Impl.OneOf0(ps) => ps + case one => one :: Nil + } + flat match { + case Nil => Impl.Fail() + case one :: Nil => one + case many => Impl.OneOf0(many) + } + case h :: Nil => loop(Nil, h :: acc) + case h1 :: (t1 @ (h2 :: tail2)) => + Impl.merge0(h1, h2) match { + case Impl.OneOf0(a :: b :: Nil) if (a eq h1) && (b eq h2) => + loop(t1, h1 :: acc) + case Impl.OneOf(a :: b :: Nil) if (a eq h1) && (b eq h2) => + loop(t1, h1 :: acc) + case h => + loop(h :: tail2, acc) + } + } + loop(ps, Nil) } - (if (isStr) string0(res) else res).asInstanceOf[Parser0[A]] - } - /** Parse the longest matching string between alternatives. The order of the strings does not * matter. * @@ -1093,7 +1121,6 @@ object Parser { .StringIn( SortedSet(two: _*) ) // sadly scala 2.12 doesn't have the `SortedSet.from` constructor function - .string } /** Version of stringIn that allows the empty string @@ -1605,9 +1632,10 @@ object Parser { */ def charWhere(fn: Char => Boolean): Parser[Char] = fn match { - case s: Set[Char] => - // Set extends function, but it is also iterable - charIn(s) + case s: Set[_] => + // Set extends function, so if the fn is a Set it has to be a Set[Char] + // but it is also iterable + charIn(s.asInstanceOf[Set[Char]]) case _ => charIn(Impl.allChars.filter(fn)) } @@ -1663,17 +1691,13 @@ object Parser { */ def void0(pa: Parser0[Any]): Parser0[Unit] = pa match { - case v @ Impl.Void0(_) => v case p1: Parser[_] => void(p1) case s if Impl.alwaysSucceeds(s) => unit + case v @ Impl.Void0(_) => v case _ => - Impl.unmap0(pa) match { - case Impl.StartParser => Impl.StartParser - case Impl.EndParser => Impl.EndParser - case n @ Impl.Not(_) => n - case p @ Impl.Peek(_) => p - case other => Impl.Void0(other) - } + val unmapped = Impl.unmap0(pa) + if (Impl.isVoided(unmapped)) unmapped.asInstanceOf[Parser0[Unit]] + else Impl.Void0(unmapped) } /** discard the value in a Parser. This is an optimization because we remove trailing map @@ -1687,10 +1711,9 @@ object Parser { Impl.unmap(pa) match { case f @ Impl.Fail() => f.widen case f @ Impl.FailWith(_) => f.widen - case p: Impl.Str => p - case p: Impl.StringIn => p - case p: Impl.IgnoreCase => p - case notVoid => Impl.Void(notVoid) + case notVoid => + if (Impl.isVoided(notVoid)) notVoid.asInstanceOf[Parser[Unit]] + else Impl.Void(notVoid) } } @@ -1716,11 +1739,12 @@ object Parser { case str if Impl.matchesString(str) => str.asInstanceOf[Parser[String]] case _ => Impl.unmap(pa) match { + case si @ Impl.StringIn(_) => si case len @ Impl.Length(_) => len case strP @ Impl.Str(expect) => strP.as(expect) - case ci @ Impl.CharIn(min, bs, _) if BitSetUtil.isSingleton(bs) => + case ci @ Impl.SingleChar(c) => // we can allocate the returned string once here - val minStr = min.toChar.toString + val minStr = c.toString Impl.Map(ci, Impl.ConstFn(minStr)) case f @ Impl.Fail() => f.widen case f @ Impl.FailWith(_) => f.widen @@ -1775,6 +1799,7 @@ object Parser { pa match { case p1: Parser[A] => backtrack(p1) case pa if Impl.doesBacktrack(pa) => pa + case Impl.Void0(b) => Impl.Void0(Impl.Backtrack0(b)) case nbt => Impl.Backtrack0(nbt) } @@ -1784,6 +1809,7 @@ object Parser { def backtrack[A](pa: Parser[A]): Parser[A] = pa match { case pa if Impl.doesBacktrack(pa) => pa + case Impl.Void(b) => Impl.Void(Impl.Backtrack(b)) case nbt => Impl.Backtrack(nbt) } @@ -1797,7 +1823,7 @@ object Parser { // void cannot make a Parser0 a Parser // If b is (), such as foo.as(()) // we can just return v - if (b.equals(())) voided.asInstanceOf[Parser0[B]] + if (Impl.isUnit(b)) voided.asInstanceOf[Parser0[B]] else if (Impl.alwaysSucceeds(voided)) pure(b) else Impl.Map0(voided, Impl.ConstFn(b)) } @@ -1808,19 +1834,21 @@ object Parser { val v = pa.void // If b is (), such as foo.as(()) // we can just return v - if (b.equals(())) v.asInstanceOf[Parser[B]] + if (Impl.isUnit(b)) v.asInstanceOf[Parser[B]] else v match { - case Impl.Void(ci @ Impl.CharIn(min, bs, _)) => + case Impl.Void(ci @ Impl.SingleChar(c)) => // CharIn is common and cheap, no need to wrap // with Void since CharIn always returns the char // even when voided b match { - case bc: Char if BitSetUtil.isSingleton(bs) && (min.toChar == bc) => + case bc: Char if bc == c => ci.asInstanceOf[Parser[B]] case _ => Impl.Map(ci, Impl.ConstFn(b)) } + case f @ Impl.Fail() => f.widen + case f @ Impl.FailWith(_) => f.widen case voided => Impl.Map(voided, Impl.ConstFn(b)) } @@ -1829,12 +1857,19 @@ object Parser { /** Add a context string to Errors to aid debugging */ def withContext0[A](p0: Parser0[A], ctx: String): Parser0[A] = - Impl.WithContextP0(ctx, p0) + p0 match { + case Impl.Void0(p) => Impl.Void0(withContext0(p, ctx)).asInstanceOf[Parser0[A]] + case _ if Impl.alwaysSucceeds(p0) => p0 + case _ => Impl.WithContextP0(ctx, p0) + } /** Add a context string to Errors to aid debugging */ def withContext[A](p: Parser[A], ctx: String): Parser[A] = - Impl.WithContextP(ctx, p) + p match { + case Impl.Void(p) => Impl.Void(withContext(p, ctx)) + case _ => Impl.WithContextP(ctx, p) + } implicit val catsInstancesParser : FlatMap[Parser] with Defer[Parser] with MonoidK[Parser] with FunctorFilter[Parser] = @@ -1950,6 +1985,9 @@ object Parser { val allChars = Char.MinValue to Char.MaxValue + def isUnit(a: Any): Boolean = + a.equals(()) + case class ConstFn[A](result: A) extends Function[Any, A] { def apply(any: Any) = result @@ -1978,7 +2016,7 @@ object Parser { final def doesBacktrackCheat(p: Parser0[Any]): Boolean = doesBacktrack(p) - @annotation.tailrec + @tailrec final def doesBacktrack(p: Parser0[Any]): Boolean = p match { case Backtrack0(_) | Backtrack(_) | AnyChar | CharIn(_, _, _) | Str(_) | IgnoreCase(_) | @@ -1990,18 +2028,42 @@ object Parser { case Map(p, _) => doesBacktrack(p) case SoftProd0(a, b) => doesBacktrackCheat(a) && doesBacktrack(b) case SoftProd(a, b) => doesBacktrackCheat(a) && doesBacktrack(b) - case WithContextP(_, p) => doesBacktrack(p) case WithContextP0(_, p) => doesBacktrack(p) + case WithContextP(_, p) => doesBacktrack(p) + case OneOf0(ps) => ps.forall(doesBacktrackCheat(_)) + case OneOf(ps) => ps.forall(doesBacktrackCheat(_)) + case Void0(p) => doesBacktrack(p) + case Void(p) => doesBacktrack(p) case _ => false } + object SingleChar { + def unapply(p: Parser0[Any]): Option[Char] = + p match { + case CharIn(min, bs, _) if BitSetUtil.isSingleton(bs) => Some(min.toChar) + case _ => None + } + } + + object DefiniteString { + def unapply(p: Parser0[Any]): Option[String] = + p match { + case Pure("") => Some("") + case Map(left, ConstFn(res: String)) => + left match { + case Str(s0) if s0 == res => Some(s0) + case SingleChar(c) if (res.length == 1) && (res.charAt(0) == c) => Some(res) + case _ => None + } + case _ => None + } + } // does this parser return the string it matches def matchesString(p: Parser0[Any]): Boolean = p match { - case StringP0(_) | StringP(_) | Pure("") | Length(_) | Fail() | FailWith(_) => true - case Map(Str(e1), ConstFn(e2)) => e1 == e2 - case Map(CharIn(min, bs, _), ConstFn(e)) if BitSetUtil.isSingleton(bs) => - e == min.toChar.toString + case StringP0(_) | StringP(_) | StringIn(_) | Length(_) | Fail() | FailWith(_) | + DefiniteString(_) => + true case OneOf(ss) => ss.forall(matchesString) case OneOf0(ss) => ss.forall(matchesString) case WithContextP(_, p) => matchesString(p) @@ -2009,7 +2071,7 @@ object Parser { case _ => false } - // does this parser always succeed? + // does this parser always succeed without consuming input // note: a parser1 does not always succeed // and by construction, a oneOf0 never always succeeds final def alwaysSucceeds(p: Parser0[Any]): Boolean = @@ -2025,14 +2087,30 @@ object Parser { case _ => false } + // does this parser always eventually succeed (maybe consuming input) + // note, Parser1 has to consume, but may get an empty string, so can't + // always succeed + final def eventuallySucceeds(p: Parser0[Any]): Boolean = + p match { + case Index | GetCaret | Pure(_) => true + case Map0(p, _) => eventuallySucceeds(p) + case SoftProd0(a, b) => eventuallySucceeds(a) && eventuallySucceeds(b) + case Prod0(a, b) => eventuallySucceeds(a) && eventuallySucceeds(b) + case WithContextP0(_, p) => eventuallySucceeds(p) + case OneOf0(ps) => eventuallySucceeds(ps.last) + // by construction we never build a Not(Fail()) since + // it would just be the same as unit + // case Not(Fail() | FailWith(_)) => true + case _ => false + } + val someUnit: Some[Unit] = Some(()) // *if* the parser succeeds, do we know the result? // it may not always suceed final def hasKnownResult[A](p: Parser0[A]): Option[A] = p match { case Pure(a) => Some(a) - case Impl.CharIn(min, bs, _) if BitSetUtil.isSingleton(bs) => - Some(min.toChar.asInstanceOf[A]) + case SingleChar(c) => Some(c.asInstanceOf[A]) case Map0(_, fn) => // scala 3.0.2 seems to fail if we inline // this match above @@ -2087,22 +2165,49 @@ object Parser { case WithContextP0(_, p) => hasKnownResult(p) case Backtrack(p) => hasKnownResult(p) case Backtrack0(p) => hasKnownResult(p) - case Not(_) | Peek(_) | Void(_) | Void0(_) | StartParser | EndParser | Str(_) | StringIn( - _ - ) | IgnoreCase( + case Not(_) | Peek(_) | Void(_) | Void0(_) | StartParser | EndParser | Str(_) | IgnoreCase( _ ) => // these are always unit someUnit.asInstanceOf[Option[A]] case Rep0(_, _, _) | Rep(_, _, _, _) | FlatMap0(_, _) | FlatMap(_, _) | TailRecM(_, _) | - TailRecM0(_, _) | Defer(_) | Defer0(_) | GetCaret | Index | OneOf(_) | OneOf0(_) | - Length(_) | Fail() | FailWith(_) | CharIn(_, _, _) | AnyChar | StringP( + TailRecM0(_, _) | Defer(_) | Defer0(_) | GetCaret | Index | Length(_) | Fail() | + FailWith(_) | CharIn(_, _, _) | AnyChar | StringP( + _ + ) | OneOf(Nil) | OneOf0(Nil) | StringP0(_) | Select(_, _) | Select0(_, _) | StringIn( _ - ) | StringP0(_) | Select(_, _) | Select0(_, _) => + ) => // these we don't know the value fundamentally or by construction None } + /** return true if this is already the same as void + * + * @param p + * the Parser to check + * @return + * true if this parser does not capture + */ + def isVoided(p: Parser0[Any]): Boolean = + p match { + case Pure(a) => isUnit(a) + case StartParser | EndParser | Void(_) | Void0(_) | IgnoreCase(_) | Str(_) | Fail() | + FailWith(_) | Not(_) | Peek(_) => + true + case OneOf(ps) => ps.forall(isVoided(_)) + case OneOf0(ps) => ps.forall(isVoided(_)) + case WithContextP(_, p) => isVoided(p) + case WithContextP0(_, p) => isVoided(p) + case Backtrack(p) => isVoided(p) + case Backtrack0(p) => isVoided(p) + case Length(_) | StringP(_) | StringIn(_) | Prod(_, _) | SoftProd(_, _) | Map(_, _) | + Select(_, _) | FlatMap(_, _) | TailRecM(_, _) | Defer(_) | Rep(_, _, _, _) | AnyChar | + CharIn(_, _, _) | StringP0(_) | Index | GetCaret | Prod0(_, _) | SoftProd0(_, _) | + Map0(_, _) | Select0(_, _) | FlatMap0(_, _) | TailRecM0(_, _) | Defer0(_) | + Rep0(_, _, _) => + false + } + /** This removes any trailing map functions which can cause wasted allocations if we are later * going to void or return strings. This stops at StringP or VoidP since those are markers that * anything below has already been transformed @@ -2133,7 +2238,11 @@ object Parser { // unmap0 may simplify enough // to remove the backtrack wrapper Parser.backtrack0(unmap0(p)) - case OneOf0(ps) => Parser.oneOf0(ps.map(unmap0)) + case OneOf0(ps) => + // Find the fixed point here + val next = Parser.oneOf0(ps.map(unmap0)) + if (next == pa) pa + else unmap0(next) case Prod0(p1, p2) => unmap0(p1) match { case Prod0(p11, p12) => @@ -2206,7 +2315,10 @@ object Parser { // unmap may simplify enough // to remove the backtrack wrapper Parser.backtrack(unmap(p)) - case OneOf(ps) => Parser.oneOf(ps.map(unmap)) + case OneOf(ps) => + val next = Parser.oneOf(ps.map(unmap)) + if (next == pa) pa + else unmap(next) case Prod(p1, p2) => unmap0(p1) match { case Prod0(p11, p12) => @@ -2309,9 +2421,9 @@ object Parser { state.capture = false val init = state.offset pa.parseMut(state) - val str = state.str.substring(init, state.offset) state.capture = s0 - str + if (state.error eq null) state.str.substring(init, state.offset) + else null } case class StringP0[A](parser: Parser0[A]) extends Parser0[String] { @@ -2465,16 +2577,16 @@ object Parser { null.asInstanceOf[A] } - final def stringIn[A](radix: RadixNode, all: SortedSet[String], state: State): Unit = { - val str = state.str + final def stringIn(radix: RadixNode, all: SortedSet[String], state: State): String = { val startOffset = state.offset - val lastMatch = radix.matchAt(str, startOffset) + val matched = radix.matchAtOrNull(state.str, startOffset) - if (lastMatch < 0) { + if (matched eq null) { state.error = Eval.later(Chain.one(Expectation.OneOfStr(startOffset, all.toList))) - state.offset = startOffset + null } else { - state.offset = lastMatch + state.offset = startOffset + matched.length + matched } } @@ -2492,13 +2604,13 @@ object Parser { override def parseMut(state: State): A = oneOf(ary, state) } - case class StringIn(sorted: SortedSet[String]) extends Parser[Unit] { + case class StringIn(sorted: SortedSet[String]) extends Parser[String] { require(sorted.size >= 2, s"expected more than two items, found: ${sorted.size}") require(!sorted.contains(""), "empty string is not allowed in alternatives") private[this] val tree = RadixNode.fromSortedStrings(NonEmptyList.fromListUnsafe(sorted.toList)) - override def parseMut(state: State): Unit = stringIn(tree, sorted, state) + override def parseMut(state: State): String = stringIn(tree, sorted, state) } final def prod[A, B](pa: Parser0[A], pb: Parser0[B], state: State): (A, B) = { @@ -2656,14 +2768,14 @@ object Parser { override def parseMut(state: State): B = Impl.tailRecM(p1, fn, state) } - @annotation.tailrec + @tailrec final def compute0[A](fn: () => Parser0[A]): Parser0[A] = fn() match { case Defer(f) => compute(f) case Defer0(f) => compute0(f) case notDefer0 => notDefer0 } - @annotation.tailrec + @tailrec final def compute[A](fn: () => Parser[A]): Parser[A] = fn() match { case Defer(f) => compute(f) @@ -2806,93 +2918,224 @@ object Parser { } } - /* - * Merge CharIn bitsets - */ - def mergeCharIn[A, P0 <: Parser0[A]](ps: List[P0]): List[P0] = { - @annotation.tailrec - def loop(ps: List[P0], front: List[CharIn], result: Chain[P0]): Chain[P0] = { - @inline - def frontRes: Chain[P0] = - front match { - case Nil => Chain.nil - case one :: Nil => Chain.one(one.asInstanceOf[P0]) - case many => - // we need to union - val minBs = many.iterator.map { case CharIn(m, bs, _) => (m, bs) } - Chain.one(Parser.charIn(BitSetUtil.union(minBs)).asInstanceOf[P0]) - } + def allCharsIn(ci: CharIn): List[String] = + BitSetUtil + .union((ci.min, ci.bitSet) :: Nil) + .iterator + .map(_.toString) + .toList - ps match { - case Nil => result ++ frontRes - case AnyChar :: tail => - // AnyChar is bigger than all subsequent CharIn: - // and any direct prefix CharIns - val tail1 = tail.filterNot(_.isInstanceOf[CharIn]) - (result :+ AnyChar.asInstanceOf[P0]) ++ Chain.fromSeq(tail1) - case (ci: CharIn) :: tail => - loop(tail, ci :: front, result) - case h :: tail => - // h is not an AnyChar or CharIn - // we make our prefix frontRes - // and resume working on the tail - loop(tail, Nil, (result ++ frontRes) :+ h) - } + def merge0[A](left: Parser0[A], right: Parser0[A]): Parser0[A] = + (left, right) match { + case (l1: Parser[A], r1: Parser[A]) => merge(l1, r1) + case (_, _) if eventuallySucceeds(left) => left + case (Fail(), _) => right + case (_, Fail()) => left + case (OneOf0(_), OneOf(rs)) => + merge0(left, OneOf0(rs)) + case (OneOf(ls), OneOf0(_)) => + merge0(OneOf0(ls), right) + case (OneOf0(ls), OneOf0(rights @ (h :: t))) => + merge0(ls.last, h) match { + case OneOf(_) | OneOf0(_) => + // just concat + OneOf0(ls ::: rights) + case l1 => + val newLeft = OneOf0(ls.init :+ l1) + t match { + case rlast :: Nil => + merge0(newLeft, rlast) + case twoOrMore => + merge0(newLeft, OneOf0(twoOrMore)) + } + } + case (left, OneOf0(rs @ (h :: t))) => + merge0(left, h) match { + case OneOf(_) | OneOf0(_) => + OneOf0(left :: rs) + case h1 => + OneOf0(h1 :: t) + } + case (left, OneOf(rs @ (h :: t))) => + merge0(left, h) match { + case OneOf(_) | OneOf0(_) => + OneOf0(left :: rs) + case h1: Parser[A] => + OneOf(h1 :: t) + case h1 => + OneOf0(h1 :: t) + } + case (OneOf0(ls), right) => + merge0(ls.last, right) match { + case OneOf(_) | OneOf0(_) => + OneOf0(ls :+ right) + case l1 => + OneOf0(ls.init :+ l1) + } + case (OneOf(ls), right) => + merge0(ls.last, right) match { + case OneOf(_) | OneOf0(_) => + OneOf0(ls :+ right) + case l: Parser[A] => OneOf(ls.init :+ l) + case l => OneOf0(ls.init :+ l) + } + case (Void0(vl), Void0(vr)) => + merge0(vl, vr).void + case (Void0(vl), right) if isVoided(right) => + merge0(vl, right).void + case (Void(vl), right) if isVoided(right) => + merge0(vl, right).void + case (left, Void0(vr)) if isVoided(left) => + merge0(left, vr).void + case (left, Void(vr)) if isVoided(left) => + merge0(left, vr).void + case _ => OneOf0(left :: right :: Nil) } - loop(ps, Nil, Chain.nil).toList - } - - /* - * Merge Str and StringIn - * - * the semantic issue is this: - * oneOf matches the first, left to right - * StringIn matches the longest. - * we can only merge into the left if - * there are no prefixes of the right inside the left - */ - def mergeStrIn[A, P0 <: Parser0[A]](ps: List[P0]): List[P0] = { - @annotation.tailrec - def loop(ps: List[P0], front: SortedSet[String], result: Chain[P0]): Chain[P0] = { - @inline - def res(front: SortedSet[String]): Chain[P0] = - if (front.isEmpty) Chain.nil - else if (front.size == 1) Chain.one(Str(front.head).asInstanceOf[P0]) - else Chain.one(StringIn(front).asInstanceOf[P0]) - - // returns if there is a strict prefix (equality does not count) - def frontHasPrefixOf(s: String): Boolean = - front.exists { f => s.startsWith(f) && (f.length != s.length) } - - ps match { - case Nil => result ++ res(front) - case Str(s) :: tail => - if (frontHasPrefixOf(s)) { - // there is an overlap, so we need to match what we have first, then come here - loop(tail, SortedSet(s), result ++ res(front)) - } else { - // there is no overlap in the tree, just merge it in: - loop(tail, front + s, result) - } - case StringIn(ss) :: tail => - val (rights, lefts) = ss.partition(frontHasPrefixOf(_)) - val front1 = front | lefts - if (rights.nonEmpty) { - // there are some that can't be merged in - loop(tail, rights, result ++ res(front1)) - } else { - // everything can be merged in - loop(tail, front1, result) - } - case h :: tail => - loop(tail, SortedSet.empty, (result ++ res(front)) :+ h) - } + def merge[A](left: Parser[A], right: Parser[A]): Parser[A] = + (left, right) match { + case (Fail(), _) => right + case (_, Fail()) => left + case (OneOf(ls), OneOf(rights @ (h :: t))) => + merge(ls.last, h) match { + case OneOf(_) => + // just concat + OneOf(ls ::: rights) + case l1 => + val newLeft = OneOf(ls.init :+ l1) + t match { + case rlast :: Nil => + merge(newLeft, rlast) + case twoOrMore => + merge(newLeft, OneOf(twoOrMore)) + } + } + case (left, OneOf(rs @ (h :: t))) => + merge(left, h) match { + case OneOf(_) => + OneOf(left :: rs) + case h1 => + // maybe we can progess on t + if (t.lengthCompare(2) >= 0) + merge(h1, OneOf(t)) + else + merge(h1, t.head) + } + case (OneOf(ls), right) => + merge(ls.last, right) match { + case OneOf(_) => + OneOf(ls :+ right) + case l1 => + val li = ls.init + if (li.lengthCompare(2) >= 0) + merge(OneOf(li), l1) + else + merge(li.head, l1) + } + case (CharIn(_, _, _), AnyChar) => AnyChar + case (AnyChar, CharIn(_, _, _) | Str(_) | StringIn(_)) => AnyChar + case (CharIn(m1, b1, _), CharIn(m2, b2, _)) => + Parser.charIn(BitSetUtil.union((m1, b1) :: (m2, b2) :: Nil)) + case (Void(ci @ CharIn(_, _, _)), Str(_)) => + Parser.oneOf(allCharsIn(ci).map(Str(_)) ::: (right :: Nil)).asInstanceOf[Parser[A]] + case (StringP(ci @ CharIn(_, _, _)), DefiniteString(_) | StringIn(_)) => + // make sure we make progress... + val strs = StringIn(SortedSet(allCharsIn(ci): _*)) + merge(strs.asInstanceOf[Parser[A]], right) + case (Str(l), Void(ci @ CharIn(_, _, _))) => + Parser.oneOf(Str(l) :: allCharsIn(ci).map(Str(_))).asInstanceOf[Parser[A]] + case (DefiniteString(_) | StringIn(_), StringP(ci @ CharIn(_, _, _))) => + // make sure we make progress... + val strs = StringIn(SortedSet(allCharsIn(ci): _*)) + merge(left, strs.asInstanceOf[Parser[A]]) + case (Str(l), Str(r)) => + // if l is a prefix of r, it matches first + // if not, then we can make a StringIn(_).void + if (r.startsWith(l)) left + else Void(StringIn(SortedSet(l, r))) + case (DefiniteString(l), DefiniteString(r)) => + // if l is a prefix of r, it matches first + // if not, then we can make a StringIn(_).void + if (r.startsWith(l)) left + else { + val res = + if (l.length == 1 && r.length == 1) { + charIn(l.head :: r.head :: Nil).string + } else { + StringIn(SortedSet(l, r)) + } + + res.asInstanceOf[Parser[A]] + } + case (StringIn(ls), DefiniteString(s1)) => + if (ls.exists { l => s1.startsWith(l) && (l.length <= s1.length) }) { + // if left didn't match, then s1 can't match + left + } else StringIn(ls + s1) + case (Void(StringIn(ls)), Str(s1)) => + if (ls.exists { l => s1.startsWith(l) && (l.length <= s1.length) }) { + // if left didn't match, then s1 can't match + left + } else Void(StringIn(ls + s1)) + case (DefiniteString(l), StringIn(rs)) => + // We know if we go to rs that l did + // not match so nothing in rs can have l as a prefix + val good = rs.filterNot(_.startsWith(l)) + if (good.isEmpty) left + else { + StringIn(good + l) + } + case (Str(l), Void(StringIn(rs))) => + // We know if we go to rs that l did + // not match so nothing in rs can have l as a prefix + val good = rs.filterNot(_.startsWith(l)) + if (good.isEmpty) left + else { + Void(StringIn(good + l)) + } + case (StringIn(ls), StringIn(rs)) => + // any string in rs that doesn't have a substring in ls can be moved + // over, since substrings would match first in oneOf but not StringIn + val canMatch = rs.filterNot { s => + ls.exists { l => s.startsWith(l) && (l.length <= s.length) } + } + if (canMatch.isEmpty) left + else { + StringIn(ls | canMatch) + } + case (Void(StringIn(ls)), Void(ci @ CharIn(_, _, _))) => + val rs = SortedSet(allCharsIn(ci): _*) + // any string in rs that doesn't have a substring in ls can be moved + // over, since substrings would match first in oneOf but not StringIn + val canMatch = rs.filterNot { s => + ls.exists { l => s.startsWith(l) && (l.length <= s.length) } + } + if (canMatch.isEmpty) left + else { + Void(StringIn(ls | canMatch)) + } + case (Void(ci @ CharIn(_, _, _)), Void(StringIn(rs))) => + val ls = SortedSet(allCharsIn(ci): _*) + // any string in rs that doesn't have a substring in ls can be moved + // over, since substrings would match first in oneOf but not StringIn + val canMatch = rs.filterNot { s => + ls.exists { l => s.startsWith(l) && (l.length <= s.length) } + } + if (canMatch.isEmpty) left + else { + Void(StringIn(ls | canMatch)) + } + case (Void(vl), Void(vr)) => + merge(vl, vr).void + case (StringP(l1), StringP(r1)) => + merge(l1, r1).string + case (Void(vl), right) if isVoided(right) => + merge(vl, right).void + case (left, Void(vr)) if isVoided(left) => + merge(left, vr).void + case _ => OneOf(left :: right :: Nil) } - loop(ps, SortedSet.empty, Chain.nil).toList - } - case object AnyChar extends Parser[Char] { override def parseMut(state: State): Char = { val offset = state.offset @@ -2951,7 +3194,10 @@ object Parser { case class Not(under: Parser0[Unit]) extends Parser0[Unit] { override def parseMut(state: State): Unit = { val offset = state.offset + val cap = state.capture + state.capture = false under.parseMut(state) + state.capture = cap if (state.error ne null) { // under failed, so we succeed state.error = null @@ -2982,7 +3228,10 @@ object Parser { case class Peek(under: Parser0[Unit]) extends Parser0[Unit] { override def parseMut(state: State): Unit = { val offset = state.offset + val cap = state.capture + state.capture = false under.parseMut(state) + state.capture = cap if (state.error eq null) { // under passed, so we succeed state.offset = offset diff --git a/core/shared/src/test/scala/cats/parse/ParserTest.scala b/core/shared/src/test/scala/cats/parse/ParserTest.scala index 920e5ac6..c993b8b8 100644 --- a/core/shared/src/test/scala/cats/parse/ParserTest.scala +++ b/core/shared/src/test/scala/cats/parse/ParserTest.scala @@ -21,15 +21,15 @@ package cats.parse -import cats.{Eq, Id, FlatMap, Functor, Defer, MonoidK, Monad, Eval} import cats.arrow.FunctionK -import cats.data.NonEmptyList +import cats.data.{NonEmptyList, NonEmptyVector} +import cats.implicits._ +import cats.{Eq, Id, FlatMap, Functor, Defer, MonoidK, Monad, Eval} import org.scalacheck.Prop.forAll import org.scalacheck.{Arbitrary, Gen, Cogen} - -import cats.implicits._ import scala.util.Random -import cats.data.NonEmptyVector + +import Arbitrary.arbitrary sealed abstract class GenT[F[_]] { self => type A @@ -71,7 +71,7 @@ object ParserGen { } implicit val cogenCaret: Cogen[Caret] = - Cogen { caret: Caret => + Cogen { (caret: Caret) => (caret.offset.toLong << 32) | (caret.col.toLong << 16) | (caret.line.toLong) } @@ -612,6 +612,8 @@ class ParserTest extends munit.ScalaCheckSuite { // "bUQvEfxFZ73bxtVjauK8tJDrEKOFbxUfk6WrGiy3bkH=" // "PPsKExr4HRlyCXkMrC6Rki5u59V88vwSeVTiGWJFS3G=" // "Ova1uT18mkE4uTX4RdgQza6z70fxyv6micl4hIZvywP=" + // "YcGRsiTHa791rV5CIL4wYhWDofanqbYbvO418dbZnOK=" + // "6YoSspuNxqEoMosfi5J6wHgo4I4rD48Zg21XAnZtMcA=" def parseTest[A: Eq](p: Parser0[A], str: String, a: A) = p.parse(str) match { @@ -757,12 +759,23 @@ class ParserTest extends munit.ScalaCheckSuite { } } + test("string(x).void == string(x) and withContext") { + assertEquals(Parser.string("foo").void, Parser.string("foo")) + assertEquals( + Parser.string("foo").withContext("ctx").void, + Parser.string("foo").withContext("ctx") + ) + } + property("voided only changes the result") { forAll(ParserGen.gen0, Arbitrary.arbitrary[String]) { (genP, str) => - val r1 = genP.fa.parse(str) - val r2 = genP.fa.void.parse(str) - val r3 = FlatMap[Parser0].void(genP.fa).parse(str) - val r4 = genP.fa.as(()).parse(str) + def go[A](p: Parser0[A]) = + p.parse(str).leftMap(_.offsets) + + val r1 = go(genP.fa) + val r2 = go(genP.fa.void) + val r3 = go(FlatMap[Parser0].void(genP.fa)) + val r4 = go(genP.fa.as(())) assertEquals(r2, r1.map { case (off, _) => (off, ()) }) assertEquals(r2, r3) @@ -772,11 +785,14 @@ class ParserTest extends munit.ScalaCheckSuite { property("voided only changes the result Parser") { forAll(ParserGen.gen, Arbitrary.arbitrary[String]) { (genP, str) => - val r1 = genP.fa.parse(str) - val r2 = genP.fa.void.parse(str) - val r3 = FlatMap[Parser].void(genP.fa).parse(str) - val r4 = genP.fa.as(()).parse(str) - val r5 = ((genP.fa.void: Parser0[Unit]) <* Monad[Parser0].unit).parse(str) + def go[A](p: Parser0[A]) = + p.parse(str).leftMap(_.offsets) + + val r1 = go(genP.fa) + val r2 = go(genP.fa.void) + val r3 = go(FlatMap[Parser].void(genP.fa)) + val r4 = go(genP.fa.as(())) + val r5 = go((genP.fa.void: Parser0[Unit]) <* Monad[Parser0].unit) assertEquals(r2, r1.map { case (off, _) => (off, ()) }) assertEquals(r2, r3) @@ -804,7 +820,7 @@ class ParserTest extends munit.ScalaCheckSuite { val oneOf = Parser.oneOf0((genP1 ::: genP2).map(_.fa)) val oneOf2 = Parser.oneOf0(genP1.map(_.fa)).orElse(Parser.oneOf0(genP2.map(_.fa))) - assertEquals(oneOf.parse(str), oneOf2.parse(str)) + assertEquals(oneOf.parse(str).leftMap(_.offsets), oneOf2.parse(str).leftMap(_.offsets)) } } @@ -818,7 +834,7 @@ class ParserTest extends munit.ScalaCheckSuite { Parser.oneOf(genP2.map(_.fa)) ) - assertEquals(oneOf.parse(str), oneOf2.parse(str)) + assertEquals(oneOf.parse(str).leftMap(_.offsets), oneOf2.parse(str).leftMap(_.offsets)) } } @@ -845,15 +861,45 @@ class ParserTest extends munit.ScalaCheckSuite { case right => right } + def oneOfLaw(left: Parser0[Any], right: Parser0[Any], str: String) = { + assertEquals( + left.orElse(right).parse(str).leftMap(_.offsets), + orElse(left, right, str).leftMap(_.offsets) + ) + } + property("oneOf0 composes as expected") { forAll(ParserGen.gen0, ParserGen.gen0, Arbitrary.arbitrary[String]) { (genP1, genP2, str) => - assertEquals(genP1.fa.orElse(genP2.fa).parse(str), orElse(genP1.fa, genP2.fa, str)) + oneOfLaw(genP1.fa, genP2.fa, str) } } property("oneOf composes as expected") { forAll(ParserGen.gen, ParserGen.gen, Arbitrary.arbitrary[String]) { (genP1, genP2, str) => - assertEquals(genP1.fa.orElse(genP2.fa).parse(str), orElse(genP1.fa, genP2.fa, str)) + oneOfLaw(genP1.fa, genP2.fa, str) + } + } + + property("check some specific oneOf compositions") { + val pairs: List[(Parser0[Any], Parser0[Any])] = + (Parser.string("foo").string, Parser.stringIn("foo" :: "bar" :: "foobar" :: Nil)) :: + (Parser.stringIn("foo" :: "quux" :: Nil), Parser.string("foobar").string) :: + (Parser.stringIn("foo" :: "quux" :: Nil), Parser.char('f').string) :: + (Parser.stringIn("foo" :: "quux" :: Nil), Parser.stringIn("foo" :: "quux" :: Nil)) :: + ( + Parser.stringIn("foo" :: "quux" :: "bar" :: Nil), + Parser.stringIn("foo" :: "quux" :: Nil) + ) :: + ( + Parser.stringIn("foo" :: "quux" :: Nil), + Parser.stringIn("foo" :: "quux" :: "bar" :: Nil) + ) :: + Nil + + forAll { (str: String) => + pairs.foreach { case (p1, p2) => + oneOfLaw(p1, p2, str) + } } } @@ -863,7 +909,10 @@ class ParserTest extends munit.ScalaCheckSuite { leftp.orElse(p.fa) } - assertEquals(oneOfImpl.parse(str), Parser.oneOf0(genP1.map(_.fa)).parse(str)) + assertEquals( + oneOfImpl.parse(str).leftMap(_.offsets), + Parser.oneOf0(genP1.map(_.fa)).parse(str).leftMap(_.offsets) + ) } } @@ -871,7 +920,10 @@ class ParserTest extends munit.ScalaCheckSuite { forAll(Gen.listOf(ParserGen.gen), Arbitrary.arbitrary[String]) { (genP1, str) => val oneOfImpl = genP1.foldLeft(Parser.fail[Any]) { (leftp, p) => leftp.orElse(p.fa) } - assertEquals(oneOfImpl.parse(str), Parser.oneOf(genP1.map(_.fa)).parse(str)) + assertEquals( + oneOfImpl.parse(str).leftMap(_.offsets), + Parser.oneOf(genP1.map(_.fa)).parse(str).leftMap(_.offsets) + ) } } @@ -1364,35 +1416,40 @@ class ParserTest extends munit.ScalaCheckSuite { property("repSep0 with unit sep is the same as rep0") { + case class MinMax(min: Int, max: Int) val minMax = for { min <- biasSmall(0) max <- biasSmall(Integer.max(min, 1)) - } yield (min, max) + } yield MinMax(min, max) - forAll(ParserGen.gen, biasSmall(0), Arbitrary.arbitrary[String]) { (genP, min, str) => + forAll(ParserGen.gen, biasSmall(0), Arbitrary.arbitrary[String]) { (genP, min0, str) => + val min = min0 & Int.MaxValue // make sure it is positive + // repSep0 internally uses | which triggers rewriting optimizations val p1a = genP.fa.repSep0(min = min, sep = Parser.unit) val p1b = genP.fa.rep0(min = min) - assertEquals(p1a.parse(str), p1b.parse(str)) + assertEquals(p1a.parse(str).leftMap(_.offsets), p1b.parse(str).leftMap(_.offsets)) val min1 = if (min < 1) 1 else min val p2a = genP.fa.repSep(min = min1, sep = Parser.unit) val p2b = genP.fa.rep(min = min1) - assertEquals(p2a.parse(str), p2b.parse(str)) + assertEquals(p2a.parse(str).leftMap(_.offsets), p2b.parse(str).leftMap(_.offsets)) } && - forAll(ParserGen.gen, minMax, Arbitrary.arbitrary[String]) { case (genP, (min, max), str) => - val p1a = genP.fa.repSep0(min = min, max = max, sep = Parser.unit) - val p1b = genP.fa.rep0(min = min, max = max) + forAll(ParserGen.gen, minMax, Arbitrary.arbitrary[String]) { + case (genP, MinMax(min, max), str) => + // repSep0 internally uses | which triggers rewriting optimizations + val p1a = genP.fa.repSep0(min = min, max = max, sep = Parser.unit) + val p1b = genP.fa.rep0(min = min, max = max) - assertEquals(p1a.parse(str), p1b.parse(str)) + assertEquals(p1a.parse(str).leftMap(_.offsets), p1b.parse(str).leftMap(_.offsets)) - val min1 = if (min < 1) 1 else min - val p2a = genP.fa.repSep(min = min1, max = max, sep = Parser.unit) - val p2b = genP.fa.rep(min = min1, max = max) + val min1 = if (min < 1) 1 else min + val p2a = genP.fa.repSep(min = min1, max = max, sep = Parser.unit) + val p2b = genP.fa.rep(min = min1, max = max) - assertEquals(p2a.parse(str), p2b.parse(str)) + assertEquals(p2a.parse(str).leftMap(_.offsets), p2b.parse(str).leftMap(_.offsets)) } } @@ -1454,8 +1511,8 @@ class ParserTest extends munit.ScalaCheckSuite { } } - property("p orElse p == p") { - forAll(ParserGen.gen, Arbitrary.arbitrary[String]) { (genP, str) => + property("p orElse p == p (0)") { + forAll(ParserGen.gen0, Arbitrary.arbitrary[String]) { (genP, str) => val res0 = genP.fa.parse(str) val res1 = genP.fa.orElse(genP.fa).parse(str) assertEquals(res1, res0) @@ -2001,8 +2058,8 @@ class ParserTest extends munit.ScalaCheckSuite { val left = pa.repAs0[Unit](Accumulator0.unitAccumulator0) val right = pa.rep0.void - val leftRes = left.parse(str) - val rightRes = right.parse(str) + val leftRes = left.parse(str).leftMap(_.offsets) + val rightRes = right.parse(str).leftMap(_.offsets) assertEquals(leftRes, rightRes) } } @@ -2297,6 +2354,26 @@ class ParserTest extends munit.ScalaCheckSuite { } property("p.as(a).map(fn) == p.as(fn(a))") { + val regressions = + (Parser.defer(Parser.string("foo")).void.backtrack) :: + (Parser.defer(Parser.string("foo")).void.withContext("ctx").backtrack) :: + Nil + + regressions.foreach { p => + assertEquals(p.void.as(1), p.as(1)) + assertEquals(p.as(1).void, p.void) + assertEquals(p.as(1).as(1), p.as(1)) + + val a = 42 + val fn = { (x: Int) => x + 1 } + assertEquals(p.as(a).map(fn), p.as(fn(a))) + } + assertEquals(Parser.string("foo").void, Parser.string("foo")) + assertEquals( + Parser.string("foo").withContext("bar").void, + Parser.string("foo").withContext("bar") + ) + forAll(ParserGen.gen, Gen.choose(0, 128), Gen.function1[Int, Int](Gen.choose(0, 128))) { (p, a, fn) => assertEquals(p.fa.as(a).map(fn), p.fa.as(fn(a))) @@ -2442,7 +2519,10 @@ class ParserTest extends munit.ScalaCheckSuite { val left = Parser.oneOf0(as.map(_.fa.string)) val right = Parser.oneOf0[Any](as.map(_.fa)).string - assertEquals(left.parse(toParse), right.parse(toParse)) + assertEquals( + left.parse(toParse).leftMap(_.offsets), + right.parse(toParse).leftMap(_.offsets) + ) } } @@ -2452,7 +2532,10 @@ class ParserTest extends munit.ScalaCheckSuite { val left = Parser.oneOf(as.map(_.fa.string)) val right = Parser.oneOf[Any](as.map(_.fa)).string - assertEquals(left.parse(toParse), right.parse(toParse)) + assertEquals( + left.parse(toParse).leftMap(_.offsets), + right.parse(toParse).leftMap(_.offsets) + ) } } @@ -2586,6 +2669,17 @@ class ParserTest extends munit.ScalaCheckSuite { } property("P.void is idempotent") { + val regressions = + ((Parser.string("aa").map(_ => 1) | Parser.string("bb").map(_ => 2)).withContext("ctx")) :: + (Parser.defer(Parser.string("foo")).void.backtrack) :: + (Parser.defer(Parser.string("foo")).void.withContext("ctx").backtrack) :: + Nil + + regressions.foreach { p => + val v = p.void + assertEquals(v.void, v) + } + forAll(ParserGen.gen) { p => val v1 = p.fa.void assertEquals(v1.void, v1) @@ -2640,4 +2734,111 @@ class ParserTest extends munit.ScalaCheckSuite { } } } + + property("stringIn(s).void.string == stringIn(s)") { + forAll { (ss0: List[String]) => + val ss = ss0.filter(_.nonEmpty) + val si = Parser.stringIn(ss) + + assertEquals(si.void.string, si) + } + } + + property("test that some parsers unify with |") { + forAll { (s10: Set[String], s20: Set[String]) => + val s1 = s10.filter(_.length > 1) + val s2 = s20.filterNot { s => (s.length < 2) || s1.exists(s.startsWith(_)) } + assertEquals(Parser.stringIn(s1) | Parser.stringIn(s2), Parser.stringIn(s1 | s2)) + assertEquals( + Parser.stringIn(s1).void | Parser.stringIn(s2).void, + Parser.stringIn(s1 | s2).void + ) + } && + forAll { (s1: Set[Char], s2: Set[Char]) => + assertEquals(Parser.charIn(s1) | Parser.charIn(s2), Parser.charIn(s1 | s2)) + assertEquals(Parser.charIn(s1).void | Parser.charIn(s2).void, Parser.charIn(s1 | s2).void) + // TODO: make this law pass. Currently the left is StringIn, but the right is StringP(CharIn(_, _, _)) + // assertEquals(Parser.charIn(s1).string | Parser.charIn(s2).string, Parser.charIn(s1 | s2).string) + } && + forAll { (s1: String, s2: String) => + if (!s2.startsWith(s1) && (s1.nonEmpty && s2.nonEmpty)) { + if ((s1.length > 1) || (s2.length > 1)) { + assertEquals( + Parser.stringIn(s1 :: s2 :: Nil).void, + Parser.string(s1) | Parser.string(s2) + ) + + assertEquals( + Parser.stringIn(s1 :: s2 :: Nil), + (Parser.string(s1) | Parser.string(s2)).string + ) + + assertEquals( + Parser.stringIn(s1 :: s2 :: Nil), + Parser.string(s1).string | Parser.string(s2).string + ) + } else () + } else () + } + } + + property("a | b is associative") { + def strict(a: Parser0[Any], b: Parser0[Any], c: Parser0[Any]) = + assertEquals((a | b) | c, a | (b | c)) + + val regressions: List[(Parser0[Any], Parser0[Any], Parser0[Any])] = + (Parser.char('c'), Parser.unit, Parser.char('b')) :: + (Parser.string("foo"), Parser.string("bar"), Parser.string("cow")) :: + ( + Parser.stringIn("foo" :: "bar" :: Nil), + Parser.string("x").string, + Parser.string("y").string + ) :: + ( + Parser.stringIn("foo" :: "bar" :: Nil), + Parser.char('x').string, + Parser.char('y').string + ) :: + (Parser.stringIn("foo" :: "bar" :: Nil).void, Parser.string("x"), Parser.string("y")) :: + (Parser.stringIn("foo" :: "bar" :: Nil).void, Parser.char('x'), Parser.char('y')) :: + (Parser.string("foo"), Parser.string("bar"), Parser.string("cow").withContext("ctx")) :: + // (Parser.string("foo"), Parser.string("bar"), Parser.unit.withContext("ctx")) :: + (Parser.char('a'), Parser.char('b'), Parser.char('c')) :: + // (Parser.end, Parser.string("foo"), Parser.char('c')) :: + (Parser.char('c'), Parser.string("foo"), Parser.ignoreCase("select")) :: + (Parser.string("aa"), Parser.string("bb"), Parser.char('c')) :: + (Parser.string("aa").string, Parser.string("bb").string, Parser.char('c').string) :: + ( + Parser.string("aa").string, + Parser.string("bb").string, + Parser.charIn('c' :: 'd' :: Nil).string + ) :: + (Parser.ignoreCase("select").string, Parser.char('a').string, Parser.char('b').string) :: + (Parser.anyChar.void, Parser.char('a'), Parser.string("foo")) :: + (Parser.anyChar, Parser.string("foo"), Parser.stringIn("bar" :: "baz" :: Nil)) :: + ( + Parser.stringIn("foo" :: "bar" :: Nil).void, + Parser.anyChar.void, + Parser.charIn('a' :: 'b' :: Nil) + ) :: + Nil + + regressions.foreach { case (a, b, c) => strict(a, b, c) } + + def law(a: Parser0[Any], b: Parser0[Any], c: Parser0[Any], str: String) = + // We only compare the offsets of errors here because occassionally + // merges produce equivalent parsers, but different errors kinds + // since there is some overlap in how things can be expressed + assertEquals( + ((a | b) | c).parse(str).leftMap(_.offsets), + (a | (b | c)).parse(str).leftMap(_.offsets) + ) + + forAll(ParserGen.gen, ParserGen.gen, ParserGen.gen, arbitrary[String]) { (a, b, c, str) => + law(a.fa, b.fa, c.fa, str) + } && + forAll(ParserGen.gen0, ParserGen.gen0, ParserGen.gen0, arbitrary[String]) { (a, b, c, str) => + law(a.fa, b.fa, c.fa, str) + } + } } From fc26c5b9b8f906c5ab1b73b72e2391f980810881 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Thu, 24 Feb 2022 18:05:21 +0100 Subject: [PATCH 09/23] Update sbt-mdoc to 2.3.1 (#387) --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 325015b6..03997aa0 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -8,7 +8,7 @@ addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.1") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.9.3") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.3") addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.4.5") -addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.0") +addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.1") addSbtPlugin("io.github.jonas" % "sbt-paradox-material-theme" % "0.6.0") addSbtPlugin("com.typesafe.sbt" % "sbt-site" % "1.4.1") addSbtPlugin("com.typesafe.sbt" % "sbt-ghpages" % "0.6.3") From 34816ea8224eb4082f80b9e5bf46eb74a51f0949 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Thu, 24 Feb 2022 18:06:12 +0100 Subject: [PATCH 10/23] Update parboiled to 2.4.0 (#388) --- project/Dependencies.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 51069efc..bea9c227 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -9,6 +9,6 @@ object Dependencies { lazy val fastParse = "com.lihaoyi" %% "fastparse" % "2.3.3" lazy val parsley = "org.http4s" %% "parsley" % "1.5.0-M3" lazy val jawnAst = "org.typelevel" %% "jawn-ast" % "1.3.2" - lazy val parboiled = "org.parboiled" %% "parboiled" % "2.3.0" + lazy val parboiled = "org.parboiled" %% "parboiled" % "2.4.0" lazy val attoCore = "org.tpolecat" %% "atto-core" % "0.9.5" } From f9eaeac283a316f3acb04f5b3a74eeb086312188 Mon Sep 17 00:00:00 2001 From: "P. Oscar Boykin" Date: Fri, 25 Feb 2022 14:54:37 -1000 Subject: [PATCH 11/23] Improve String generation (#385) --- .../src/main/scala/cats/parse/Numbers.scala | 4 +- .../test/scala/cats/parse/ParserTest.scala | 426 +++++++++--------- 2 files changed, 212 insertions(+), 218 deletions(-) diff --git a/core/shared/src/main/scala/cats/parse/Numbers.scala b/core/shared/src/main/scala/cats/parse/Numbers.scala index af5e196d..09239b75 100644 --- a/core/shared/src/main/scala/cats/parse/Numbers.scala +++ b/core/shared/src/main/scala/cats/parse/Numbers.scala @@ -30,11 +30,11 @@ object Numbers { /** zero or more digit chars */ - val digits0: Parser0[String] = digit.rep0.string + val digits0: Parser0[String] = digit.repAs0 /** one or more digit chars */ - val digits: Parser[String] = digit.rep.string + val digits: Parser[String] = digit.repAs /** a single base 10 digit excluding 0 */ diff --git a/core/shared/src/test/scala/cats/parse/ParserTest.scala b/core/shared/src/test/scala/cats/parse/ParserTest.scala index c993b8b8..fe63f0e2 100644 --- a/core/shared/src/test/scala/cats/parse/ParserTest.scala +++ b/core/shared/src/test/scala/cats/parse/ParserTest.scala @@ -65,6 +65,16 @@ object GenT { } object ParserGen { + // a small space to increase the chance a random parser matches + val genSmallChar: Gen[Char] = Gen.oneOf('A', 'a', 'B', 'b', ' ') + val genSmallString: Gen[String] = + Gen.geometric(5.0).flatMap(Gen.stringOfN(_, genSmallChar)) + + val genString: Gen[String] = + Gen.oneOf(genSmallString, Gen.asciiPrintableStr, Arbitrary.arbString.arbitrary) + + implicit val arbString: Arbitrary[String] = Arbitrary(genString) + implicit val functorGen: Functor[Gen] = new Functor[Gen] { def map[A, B](ga: Gen[A])(fn: A => B) = ga.map(fn) @@ -82,12 +92,12 @@ object ParserGen { Gen.oneOf(arbGen[Int], arbGen[Boolean], arbGen[String], arbGen[(Int, Int)]) val expect0: Gen[GenT[Parser0]] = - Arbitrary.arbitrary[String].map { str => + genString.map { str => GenT(Parser.string0(str)) } val ignoreCase0: Gen[GenT[Parser0]] = - Arbitrary.arbitrary[String].map { str => + genString.map { str => GenT(Parser.ignoreCase0(str)) } @@ -129,13 +139,13 @@ object ParserGen { } val expect1: Gen[GenT[Parser]] = - Arbitrary.arbitrary[String].map { str => + genString.map { str => if (str.isEmpty) GenT(Parser.fail: Parser[Unit]) else GenT(Parser.string(str)) } val ignoreCase: Gen[GenT[Parser]] = - Arbitrary.arbitrary[String].map { str => + genString.map { str => if (str.isEmpty) GenT(Parser.fail: Parser[Unit]) else GenT(Parser.ignoreCase(str)) } @@ -144,7 +154,7 @@ object ParserGen { Gen.const(GenT(Parser.fail: Parser0[Unit])) val failWith: Gen[GenT[Parser0]] = - Arbitrary.arbitrary[String].map { str => + genString.map { str => GenT(Parser.failWith[Unit](str)) } @@ -183,12 +193,12 @@ object ParserGen { } def withContext0(g: GenT[Parser0]): Gen[GenT[Parser0]] = - Arbitrary.arbitrary[String].map { ctx => + genString.map { ctx => GenT(Parser.withContext0(g.fa, ctx))(g.cogen) } def withContext(g: GenT[Parser]): Gen[GenT[Parser]] = - Arbitrary.arbitrary[String].map { ctx => + genString.map { ctx => GenT(Parser.withContext(g.fa, ctx))(g.cogen) } @@ -598,7 +608,7 @@ object ParserGen { class ParserTest extends munit.ScalaCheckSuite { - import ParserGen.{arbParser0, arbParser, biasSmall} + import ParserGen.{arbParser0, arbParser, biasSmall, genString, arbString} val tests: Int = if (BitSetUtil.isScalaJs) 50 else 2000 @@ -694,7 +704,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("Parser0 on success replaces parsed value") { - forAll(ParserGen.gen0, Arbitrary.arbitrary[String]) { (genP, str) => + forAll(ParserGen.gen0, genString) { (genP, str) => val res0 = genP.fa.as("something").parse(str) res0 match { case Left(_) => () @@ -768,7 +778,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("voided only changes the result") { - forAll(ParserGen.gen0, Arbitrary.arbitrary[String]) { (genP, str) => + forAll(ParserGen.gen0, genString) { (genP, str) => def go[A](p: Parser0[A]) = p.parse(str).leftMap(_.offsets) @@ -784,7 +794,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("voided only changes the result Parser") { - forAll(ParserGen.gen, Arbitrary.arbitrary[String]) { (genP, str) => + forAll(ParserGen.gen, genString) { (genP, str) => def go[A](p: Parser0[A]) = p.parse(str).leftMap(_.offsets) @@ -802,7 +812,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("expected in errors gives valid offsets") { - forAll(ParserGen.gen0, Arbitrary.arbitrary[String]) { (genP, str) => + forAll(ParserGen.gen0, genString) { (genP, str) => genP.fa.parse(str) match { case Left(err) => err.offsets.forall { off => @@ -815,7 +825,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("oneOf0 nesting doesn't change results") { - forAll(Gen.listOf(ParserGen.gen0), Gen.listOf(ParserGen.gen0), Arbitrary.arbitrary[String]) { + forAll(Gen.listOf(ParserGen.gen0), Gen.listOf(ParserGen.gen0), genString) { (genP1, genP2, str) => val oneOf = Parser.oneOf0((genP1 ::: genP2).map(_.fa)) val oneOf2 = Parser.oneOf0(genP1.map(_.fa)).orElse(Parser.oneOf0(genP2.map(_.fa))) @@ -825,16 +835,15 @@ class ParserTest extends munit.ScalaCheckSuite { } property("oneOf nesting doesn't change results") { - forAll(Gen.listOf(ParserGen.gen), Gen.listOf(ParserGen.gen), Arbitrary.arbitrary[String]) { - (genP1, genP2, str) => - val oneOf = Parser.oneOf((genP1 ::: genP2).map(_.fa)) - val oneOf2 = Parser - .oneOf(genP1.map(_.fa)) - .orElse( - Parser.oneOf(genP2.map(_.fa)) - ) + forAll(Gen.listOf(ParserGen.gen), Gen.listOf(ParserGen.gen), genString) { (genP1, genP2, str) => + val oneOf = Parser.oneOf((genP1 ::: genP2).map(_.fa)) + val oneOf2 = Parser + .oneOf(genP1.map(_.fa)) + .orElse( + Parser.oneOf(genP2.map(_.fa)) + ) - assertEquals(oneOf.parse(str).leftMap(_.offsets), oneOf2.parse(str).leftMap(_.offsets)) + assertEquals(oneOf.parse(str).leftMap(_.offsets), oneOf2.parse(str).leftMap(_.offsets)) } } @@ -869,13 +878,13 @@ class ParserTest extends munit.ScalaCheckSuite { } property("oneOf0 composes as expected") { - forAll(ParserGen.gen0, ParserGen.gen0, Arbitrary.arbitrary[String]) { (genP1, genP2, str) => + forAll(ParserGen.gen0, ParserGen.gen0, genString) { (genP1, genP2, str) => oneOfLaw(genP1.fa, genP2.fa, str) } } property("oneOf composes as expected") { - forAll(ParserGen.gen, ParserGen.gen, Arbitrary.arbitrary[String]) { (genP1, genP2, str) => + forAll(ParserGen.gen, ParserGen.gen, genString) { (genP1, genP2, str) => oneOfLaw(genP1.fa, genP2.fa, str) } } @@ -904,7 +913,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("oneOf0 same as foldLeft(fail)(_.orElse(_))") { - forAll(Gen.listOf(ParserGen.gen0), Arbitrary.arbitrary[String]) { (genP1, str) => + forAll(Gen.listOf(ParserGen.gen0), genString) { (genP1, str) => val oneOfImpl = genP1.foldLeft(Parser.fail: Parser0[Any]) { (leftp, p) => leftp.orElse(p.fa) } @@ -917,7 +926,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("oneOf same as foldLeft(fail)(_.orElse(_))") { - forAll(Gen.listOf(ParserGen.gen), Arbitrary.arbitrary[String]) { (genP1, str) => + forAll(Gen.listOf(ParserGen.gen), genString) { (genP1, str) => val oneOfImpl = genP1.foldLeft(Parser.fail[Any]) { (leftp, p) => leftp.orElse(p.fa) } assertEquals( @@ -928,7 +937,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("string can be recovered with index") { - forAll(ParserGen.gen0, Arbitrary.arbitrary[String]) { (genP, str) => + forAll(ParserGen.gen0, genString) { (genP, str) => val r1 = genP.fa.string.parse(str) val r2 = (genP.fa ~ Parser.index).map { case (_, end) => str.substring(0, end) }.parse(str) @@ -937,7 +946,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("backtrack orElse pure always succeeds") { - forAll(ParserGen.gen0, Arbitrary.arbitrary[String]) { (genP, str) => + forAll(ParserGen.gen0, genString) { (genP, str) => val p1 = genP.fa.backtrack.orElse(Parser.pure(())): Parser0[Any] assert(p1.parse(str).isRight) @@ -945,7 +954,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("backtrack.? pure always succeeds") { - forAll(ParserGen.gen0, Arbitrary.arbitrary[String]) { (genP, str) => + forAll(ParserGen.gen0, genString) { (genP, str) => val p1 = genP.fa.backtrack.? assert(p1.parse(str).isRight) @@ -953,7 +962,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("a.backtrack either succeeds or fails at 0") { - forAll(ParserGen.gen0, Arbitrary.arbitrary[String]) { (a, str) => + forAll(ParserGen.gen0, genString) { (a, str) => a.fa.backtrack.parse(str) match { case Right(_) => () case Left(err) => assertEquals(err.failedAtOffset, 0) @@ -962,7 +971,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("a ~ b composes as expected") { - forAll(ParserGen.gen0, ParserGen.gen0, Arbitrary.arbitrary[String]) { (p1, p2, str) => + forAll(ParserGen.gen0, ParserGen.gen0, genString) { (p1, p2, str) => val composed = p1.fa ~ p2.fa val cres = composed.parse(str) @@ -986,7 +995,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("a ~ b composes as expected parser1") { - forAll(ParserGen.gen, ParserGen.gen, Arbitrary.arbitrary[String]) { (p1, p2, str) => + forAll(ParserGen.gen, ParserGen.gen, genString) { (p1, p2, str) => val composed = p1.fa ~ p2.fa val cres = composed.parse(str) @@ -1011,7 +1020,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("a.with1 ~ b composes as expected") { - forAll(ParserGen.gen0, ParserGen.gen, Arbitrary.arbitrary[String]) { (p1, p2, str) => + forAll(ParserGen.gen0, ParserGen.gen, genString) { (p1, p2, str) => val composed = p1.fa.with1 ~ p2.fa val cres = composed.parse(str) @@ -1032,7 +1041,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("a.soft ~ b composes as expected") { - forAll(ParserGen.gen0, ParserGen.gen0, Arbitrary.arbitrary[String]) { (p1, p2, str) => + forAll(ParserGen.gen0, ParserGen.gen0, genString) { (p1, p2, str) => val composed = p1.fa.soft ~ p2.fa val cres = composed.parse(str) @@ -1056,7 +1065,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("a1.soft ~ b composes as expected Parser") { - forAll(ParserGen.gen, ParserGen.gen0, Arbitrary.arbitrary[String]) { (p1, p2, str) => + forAll(ParserGen.gen, ParserGen.gen0, genString) { (p1, p2, str) => val composed = p1.fa.soft ~ p2.fa val cres = composed.parse(str) @@ -1080,7 +1089,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("a.with1.soft ~ b1 composes as expected") { - forAll(ParserGen.gen0, ParserGen.gen, Arbitrary.arbitrary[String]) { (p1, p2, str) => + forAll(ParserGen.gen0, ParserGen.gen, genString) { (p1, p2, str) => val composed = p1.fa.with1.soft ~ p2.fa val cres = composed.parse(str) @@ -1104,7 +1113,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("a.flatMap(b) composes as expected parser00") { - forAll(ParserGen.gen0, Arbitrary.arbitrary[String]) { (p1, str) => + forAll(ParserGen.gen0, genString) { (p1, str) => forAll(Gen.function1(ParserGen.gen0)(p1.cogen)) { fn => val flatMapped = p1.fa.flatMap { a => fn(a).fa } @@ -1125,7 +1134,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("a.flatMap(b) composes as expected parser10") { - forAll(ParserGen.gen, Arbitrary.arbitrary[String]) { (p1, str) => + forAll(ParserGen.gen, genString) { (p1, str) => forAll(Gen.function1(ParserGen.gen0)(p1.cogen)) { fn => val flatMapped = p1.fa.flatMap { a => fn(a).fa } @@ -1148,7 +1157,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("a.flatMap(b) composes as expected parser01") { - forAll(ParserGen.gen0, Arbitrary.arbitrary[String]) { (p1, str) => + forAll(ParserGen.gen0, genString) { (p1, str) => forAll(Gen.function1(ParserGen.gen)(p1.cogen)) { fn => val flatMapped = p1.fa.with1.flatMap { a => fn(a).fa } @@ -1265,7 +1274,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("rep0 can be reimplemented with oneOf0 and defer") { - forAll(ParserGen.gen, Arbitrary.arbitrary[String]) { (genP, str) => + forAll(ParserGen.gen, genString) { (genP, str) => def rep0[A](pa: Parser[A]): Parser0[List[A]] = Parser .recursive[List[A]] { tail => @@ -1285,7 +1294,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("rep0 is consistent with rep") { - forAll(ParserGen.gen, biasSmall(1), Arbitrary.arbitrary[String]) { (genP, min, str) => + forAll(ParserGen.gen, biasSmall(1), genString) { (genP, min, str) => val repA = genP.fa.rep0(min) val repB = genP.fa .rep(min) @@ -1296,7 +1305,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("repExactlyAs is consistent with repAs") { - forAll(ParserGen.gen, Gen.choose(1, 10000), Arbitrary.arbitrary[String]) { (genP, n, str) => + forAll(ParserGen.gen, Gen.choose(1, 10000), genString) { (genP, n, str) => val repA = genP.fa.repAs[NonEmptyVector[_]](n, n) val repB = genP.fa.repExactlyAs[NonEmptyVector[_]](n) assertEquals(repA.parse(str), repB.parse(str)) @@ -1308,7 +1317,7 @@ class ParserTest extends munit.ScalaCheckSuite { min <- Gen.choose(1, Int.MaxValue) max <- Gen.choose(min, Int.MaxValue) } yield (min, max) - forAll(ParserGen.gen, validMinMax, Arbitrary.arbitrary[String]) { (genP, minmax, str) => + forAll(ParserGen.gen, validMinMax, genString) { (genP, minmax, str) => { val (min, max) = minmax genP.fa.rep(min, max).parse(str).foreach { case (_, l) => @@ -1325,7 +1334,7 @@ class ParserTest extends munit.ScalaCheckSuite { max <- biasSmall(min) } yield (min, max) - forAll(ParserGen.gen, validMinMax, Arbitrary.arbitrary[String]) { (genP, minmax, str) => + forAll(ParserGen.gen, validMinMax, genString) { (genP, minmax, str) => { val (min, max) = minmax genP.fa.rep0(min, max).parse(str).foreach { case (_, l) => @@ -1337,7 +1346,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("rep0 parses at most max entries (min == 0)") { - forAll(ParserGen.gen, biasSmall(0), Arbitrary.arbitrary[String]) { (genP, max, str) => + forAll(ParserGen.gen, biasSmall(0), genString) { (genP, max, str) => genP.fa.rep0(0, max).parse(str).foreach { case (_, l) => assert(l.length <= max) } } } @@ -1423,7 +1432,7 @@ class ParserTest extends munit.ScalaCheckSuite { max <- biasSmall(Integer.max(min, 1)) } yield MinMax(min, max) - forAll(ParserGen.gen, biasSmall(0), Arbitrary.arbitrary[String]) { (genP, min0, str) => + forAll(ParserGen.gen, biasSmall(0), genString) { (genP, min0, str) => val min = min0 & Int.MaxValue // make sure it is positive // repSep0 internally uses | which triggers rewriting optimizations val p1a = genP.fa.repSep0(min = min, sep = Parser.unit) @@ -1437,24 +1446,23 @@ class ParserTest extends munit.ScalaCheckSuite { assertEquals(p2a.parse(str).leftMap(_.offsets), p2b.parse(str).leftMap(_.offsets)) } && - forAll(ParserGen.gen, minMax, Arbitrary.arbitrary[String]) { - case (genP, MinMax(min, max), str) => - // repSep0 internally uses | which triggers rewriting optimizations - val p1a = genP.fa.repSep0(min = min, max = max, sep = Parser.unit) - val p1b = genP.fa.rep0(min = min, max = max) + forAll(ParserGen.gen, minMax, genString) { case (genP, MinMax(min, max), str) => + // repSep0 internally uses | which triggers rewriting optimizations + val p1a = genP.fa.repSep0(min = min, max = max, sep = Parser.unit) + val p1b = genP.fa.rep0(min = min, max = max) - assertEquals(p1a.parse(str).leftMap(_.offsets), p1b.parse(str).leftMap(_.offsets)) + assertEquals(p1a.parse(str).leftMap(_.offsets), p1b.parse(str).leftMap(_.offsets)) - val min1 = if (min < 1) 1 else min - val p2a = genP.fa.repSep(min = min1, max = max, sep = Parser.unit) - val p2b = genP.fa.rep(min = min1, max = max) + val min1 = if (min < 1) 1 else min + val p2a = genP.fa.repSep(min = min1, max = max, sep = Parser.unit) + val p2b = genP.fa.rep(min = min1, max = max) - assertEquals(p2a.parse(str).leftMap(_.offsets), p2b.parse(str).leftMap(_.offsets)) + assertEquals(p2a.parse(str).leftMap(_.offsets), p2b.parse(str).leftMap(_.offsets)) } } property("repSep without min is the same as repSep with min = 1") { - forAll(ParserGen.gen, ParserGen.gen, Arbitrary.arbitrary[String]) { (genP, genPSep, str) => + forAll(ParserGen.gen, ParserGen.gen, genString) { (genP, genPSep, str) => { val p = genP.fa val psep = genPSep.fa @@ -1464,7 +1472,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("repSep with sep = fail is the same as parsing 1") { - forAll(ParserGen.gen, Arbitrary.arbitrary[String]) { (genP, str) => + forAll(ParserGen.gen, genString) { (genP, str) => assertEquals( genP.fa.parse(str), Parser.repSep(genP.fa, Parser.fail).parse(str).map { case (rest, nel) => @@ -1477,7 +1485,7 @@ class ParserTest extends munit.ScalaCheckSuite { property("charsWhile/charsWhere consistency") { forAll( Gen.choose(0, 100).flatMap(Gen.listOfN(_, Gen.choose(Char.MinValue, Char.MaxValue))), - Arbitrary.arbitrary[String] + genString ) { (chars, str) => val pred = chars.toSet val p1a = Parser.charsWhile0(pred) @@ -1504,7 +1512,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("p orElse p == p") { - forAll(ParserGen.gen, Arbitrary.arbitrary[String]) { (genP, str) => + forAll(ParserGen.gen, genString) { (genP, str) => val res0 = genP.fa.parse(str) val res1 = genP.fa.orElse(genP.fa).parse(str) assertEquals(res1, res0) @@ -1512,7 +1520,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("p orElse p == p (0)") { - forAll(ParserGen.gen0, Arbitrary.arbitrary[String]) { (genP, str) => + forAll(ParserGen.gen0, genString) { (genP, str) => val res0 = genP.fa.parse(str) val res1 = genP.fa.orElse(genP.fa).parse(str) assertEquals(res1, res0) @@ -1520,7 +1528,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("Parser fails or consumes 1 or more") { - forAll(ParserGen.gen, Arbitrary.arbitrary[String]) { (genP, str) => + forAll(ParserGen.gen, genString) { (genP, str) => val res0 = genP.fa.parse(str) res0 match { case Left(_) => assert(true) @@ -1530,7 +1538,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("p1.backtrack.orElse(p2) succeeds if either p1 or p2 do (Parser0)") { - forAll(ParserGen.gen0, ParserGen.gen0, Arbitrary.arbitrary[String]) { (p1, p2, str) => + forAll(ParserGen.gen0, ParserGen.gen0, genString) { (p1, p2, str) => val ores = p1.fa.backtrack.orElse(p2.fa).parse(str) val r1 = p1.fa.parse(str) val r = if (r1.isLeft) p2.fa.parse(str) else r1 @@ -1542,7 +1550,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("p1.backtrack.orElse(p2) succeeds if either p1 or p2 do") { - forAll(ParserGen.gen, ParserGen.gen, Arbitrary.arbitrary[String]) { (p1, p2, str) => + forAll(ParserGen.gen, ParserGen.gen, genString) { (p1, p2, str) => val ores = p1.fa.backtrack.orElse(p2.fa).parse(str) val r1 = p1.fa.parse(str) val r = if (r1.isLeft) p2.fa.parse(str) else r1 @@ -1581,7 +1589,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("with1 *> and with1 <* work as expected") { - forAll(ParserGen.gen0, ParserGen.gen, Arbitrary.arbitrary[String]) { (p1, p2, str) => + forAll(ParserGen.gen0, ParserGen.gen, genString) { (p1, p2, str) => val rp1 = p1.fa.with1 *> p2.fa val rp2 = (p1.fa.void.with1 ~ p2.fa).map(_._2) assertEquals(rp1.parse(str), rp2.parse(str)) @@ -1593,7 +1601,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("a1 *> b and a1 <* b") { - forAll(ParserGen.gen, ParserGen.gen0, Arbitrary.arbitrary[String]) { (p1, p2, str) => + forAll(ParserGen.gen, ParserGen.gen0, genString) { (p1, p2, str) => assertEquals( (p1.fa *> p2.fa).parse(str), Parser.product10(p1.fa.void, p2.fa).map(_._2).parse(str) @@ -1606,17 +1614,16 @@ class ParserTest extends munit.ScalaCheckSuite { } property("parse between open and close") { - forAll(ParserGen.gen0, ParserGen.gen0, ParserGen.gen0, Arbitrary.arbitrary[String]) { - (genP1, genP, genQ, str) => - val pa = genP1.fa.between(genP.fa, genQ.fa) - val pb = genP.fa *> genP1.fa <* genQ.fa + forAll(ParserGen.gen0, ParserGen.gen0, ParserGen.gen0, genString) { (genP1, genP, genQ, str) => + val pa = genP1.fa.between(genP.fa, genQ.fa) + val pb = genP.fa *> genP1.fa <* genQ.fa - assertEquals(pa.parse(str), pb.parse(str)) + assertEquals(pa.parse(str), pb.parse(str)) } } property("surroundedBy consistent with between") { - forAll(ParserGen.gen0, ParserGen.gen0, Arbitrary.arbitrary[String]) { (genP1, genP, str) => + forAll(ParserGen.gen0, ParserGen.gen0, genString) { (genP1, genP, str) => val pa = genP1.fa.between(genP.fa, genP.fa) val pb = genP1.fa.surroundedBy(genP.fa) @@ -1625,17 +1632,16 @@ class ParserTest extends munit.ScalaCheckSuite { } property("parse between open and close with Parser this") { - forAll(ParserGen.gen, ParserGen.gen0, ParserGen.gen0, Arbitrary.arbitrary[String]) { - (genP1, genP, genQ, str) => - val pa = genP1.fa.between(genP.fa, genQ.fa) - val pb = genP.fa *> genP1.fa <* genQ.fa + forAll(ParserGen.gen, ParserGen.gen0, ParserGen.gen0, genString) { (genP1, genP, genQ, str) => + val pa = genP1.fa.between(genP.fa, genQ.fa) + val pb = genP.fa *> genP1.fa <* genQ.fa - assertEquals(pa.parse(str), pb.parse(str)) + assertEquals(pa.parse(str), pb.parse(str)) } } property("surroundedBy consistent with between with Parser this") { - forAll(ParserGen.gen, ParserGen.gen0, Arbitrary.arbitrary[String]) { (genP1, genP, str) => + forAll(ParserGen.gen, ParserGen.gen0, genString) { (genP1, genP, str) => val pa = genP1.fa.between(genP.fa, genP.fa) val pb = genP1.fa.surroundedBy(genP.fa) @@ -1644,17 +1650,16 @@ class ParserTest extends munit.ScalaCheckSuite { } property("parse soft.between open and close") { - forAll(ParserGen.gen0, ParserGen.gen0, ParserGen.gen0, Arbitrary.arbitrary[String]) { - (genP1, genP, genQ, str) => - val pa = genP1.fa.soft.between(genP.fa, genQ.fa) - val pb = genP.fa.soft *> (genP1.fa.soft <* genQ.fa) + forAll(ParserGen.gen0, ParserGen.gen0, ParserGen.gen0, genString) { (genP1, genP, genQ, str) => + val pa = genP1.fa.soft.between(genP.fa, genQ.fa) + val pb = genP.fa.soft *> (genP1.fa.soft <* genQ.fa) - assertEquals(pa.parse(str), pb.parse(str)) + assertEquals(pa.parse(str), pb.parse(str)) } } property("soft.surroundedBy consistent with soft.between") { - forAll(ParserGen.gen0, ParserGen.gen0, Arbitrary.arbitrary[String]) { (genP1, genP, str) => + forAll(ParserGen.gen0, ParserGen.gen0, genString) { (genP1, genP, str) => val pa = genP1.fa.soft.between(genP.fa, genP.fa) val pb = genP1.fa.soft.surroundedBy(genP.fa) @@ -1663,17 +1668,16 @@ class ParserTest extends munit.ScalaCheckSuite { } property("parse soft.between open and close with Parser this") { - forAll(ParserGen.gen, ParserGen.gen0, ParserGen.gen0, Arbitrary.arbitrary[String]) { - (genP1, genP, genQ, str) => - val pa = genP1.fa.soft.between(genP.fa, genQ.fa) - val pb = genP.fa.soft *> (genP1.fa.soft <* genQ.fa) + forAll(ParserGen.gen, ParserGen.gen0, ParserGen.gen0, genString) { (genP1, genP, genQ, str) => + val pa = genP1.fa.soft.between(genP.fa, genQ.fa) + val pb = genP.fa.soft *> (genP1.fa.soft <* genQ.fa) - assertEquals(pa.parse(str), pb.parse(str)) + assertEquals(pa.parse(str), pb.parse(str)) } } property("soft.surroundedBy consistent with between with Parser this") { - forAll(ParserGen.gen, ParserGen.gen0, Arbitrary.arbitrary[String]) { (genP1, genP, str) => + forAll(ParserGen.gen, ParserGen.gen0, genString) { (genP1, genP, str) => val pa = genP1.fa.soft.between(genP.fa, genP.fa) val pb = genP1.fa.soft.surroundedBy(genP.fa) @@ -1682,17 +1686,16 @@ class ParserTest extends munit.ScalaCheckSuite { } property("parse with1.between open and close with Parser args") { - forAll(ParserGen.gen0, ParserGen.gen, ParserGen.gen, Arbitrary.arbitrary[String]) { - (genP1, genP, genQ, str) => - val pa = genP1.fa.with1.between(genP.fa, genQ.fa) - val pb = genP.fa *> genP1.fa <* genQ.fa + forAll(ParserGen.gen0, ParserGen.gen, ParserGen.gen, genString) { (genP1, genP, genQ, str) => + val pa = genP1.fa.with1.between(genP.fa, genQ.fa) + val pb = genP.fa *> genP1.fa <* genQ.fa - assertEquals(pa.parse(str), pb.parse(str)) + assertEquals(pa.parse(str), pb.parse(str)) } } property("with1.surroundedBy consistent with between with Parser this") { - forAll(ParserGen.gen0, ParserGen.gen, Arbitrary.arbitrary[String]) { (genP1, genP, str) => + forAll(ParserGen.gen0, ParserGen.gen, genString) { (genP1, genP, str) => val pa = genP1.fa.with1.between(genP.fa, genP.fa) val pb = genP1.fa.with1.surroundedBy(genP.fa) @@ -1701,17 +1704,16 @@ class ParserTest extends munit.ScalaCheckSuite { } property("parse soft.with1.between open and close with Parser args") { - forAll(ParserGen.gen0, ParserGen.gen, ParserGen.gen, Arbitrary.arbitrary[String]) { - (genP1, genP, genQ, str) => - val pa = genP1.fa.soft.with1.between(genP.fa, genQ.fa) - val pb = genP.fa.soft *> (genP1.fa.soft <* genQ.fa) + forAll(ParserGen.gen0, ParserGen.gen, ParserGen.gen, genString) { (genP1, genP, genQ, str) => + val pa = genP1.fa.soft.with1.between(genP.fa, genQ.fa) + val pb = genP.fa.soft *> (genP1.fa.soft <* genQ.fa) - assertEquals(pa.parse(str), pb.parse(str)) + assertEquals(pa.parse(str), pb.parse(str)) } } property("soft.with1.surroundedBy consistent with between with Parser this") { - forAll(ParserGen.gen0, ParserGen.gen, Arbitrary.arbitrary[String]) { (genP1, genP, str) => + forAll(ParserGen.gen0, ParserGen.gen, genString) { (genP1, genP, str) => val pa = genP1.fa.soft.with1.between(genP.fa, genP.fa) val pb = genP1.fa.soft.with1.surroundedBy(genP.fa) @@ -1720,7 +1722,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("exactly one of x or !x parse") { - forAll(ParserGen.gen0, Arbitrary.arbitrary[String]) { (p1, str) => + forAll(ParserGen.gen0, genString) { (p1, str) => val notx = !p1.fa val xor = p1.fa.parse(str).isRight ^ notx.parse(str).isRight @@ -1729,7 +1731,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("if x ~ y matches then x ~ y.peek match") { - forAll(ParserGen.gen0, ParserGen.gen0, Arbitrary.arbitrary[String]) { (x, y, str) => + forAll(ParserGen.gen0, ParserGen.gen0, genString) { (x, y, str) => val m1 = (x.fa ~ y.fa).parse(str) val m2 = ((x.fa ~ y.fa.peek).map(_._1)).parse(str) @@ -1741,7 +1743,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("if x matches then x.peek matches but returns the whole string and unit") { - forAll(ParserGen.gen0, Arbitrary.arbitrary[String]) { (x, str) => + forAll(ParserGen.gen0, genString) { (x, str) => if (x.fa.parse(str).isRight) { assertEquals(x.fa.peek.parse(str), Right((str, ()))) } @@ -1749,7 +1751,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("(a.soft ~ b) == a ~ b in success of expected (not partials)") { - forAll(ParserGen.gen0, ParserGen.gen0, Arbitrary.arbitrary[String]) { (a, b, str) => + forAll(ParserGen.gen0, ParserGen.gen0, genString) { (a, b, str) => val left = a.fa.soft ~ b.fa val right = a.fa ~ b.fa val leftRes = left.parse(str).leftMap(_.expected) @@ -1759,7 +1761,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("(a.soft ~ b) == softProduct(a, b)") { - forAll(ParserGen.gen0, ParserGen.gen0, Arbitrary.arbitrary[String]) { (a, b, str) => + forAll(ParserGen.gen0, ParserGen.gen0, genString) { (a, b, str) => val left = a.fa.soft ~ b.fa val right = Parser.softProduct0(a.fa, b.fa) assertEquals(left.parse(str), right.parse(str)) @@ -1775,7 +1777,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("(a1.soft ~ b) == softProduct(a, b)") { - forAll(ParserGen.gen, ParserGen.gen0, Arbitrary.arbitrary[String]) { (a, b, str) => + forAll(ParserGen.gen, ParserGen.gen0, genString) { (a, b, str) => val left1 = a.fa.soft ~ b.fa val right1 = Parser.softProduct10(a.fa, b.fa) assertEquals(left1.parse(str), right1.parse(str)) @@ -1800,7 +1802,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("Parser.until is like a search") { - forAll(ParserGen.gen0, Arbitrary.arbitrary[String]) { (a, str) => + forAll(ParserGen.gen0, genString) { (a, str) => val p = Parser.until0(a.fa) *> a.fa def loopMatch(cnt: Int): Option[(String, a.A)] = (Parser.length0(cnt) *> a.fa).parse(str) match { @@ -1814,7 +1816,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("Parser.repUntil end Parser succeeds works as expected") { - forAll(ParserGen.gen, ParserGen.gen0, Arbitrary.arbitrary[String]) { (p, end, str) => + forAll(ParserGen.gen, ParserGen.gen0, genString) { (p, end, str) => val left = Parser.repUntil(p.fa.string, end.fa).parse(str) val right = (Parser.not(end.fa).void.with1 ~ p.fa.string).rep.map(_.map(_._2)).parse(str) @@ -1830,7 +1832,7 @@ class ParserTest extends munit.ScalaCheckSuite { case Left(_) => () } } && - forAll(ParserGen.gen, ParserGen.gen0, Arbitrary.arbitrary[String]) { (p, end, str) => + forAll(ParserGen.gen, ParserGen.gen0, genString) { (p, end, str) => val left = Parser.repUntil0(p.fa.string, end.fa).parse(str) val right = (Parser.not(end.fa).void.with1 ~ p.fa.string).rep0.map(_.map(_._2)).parse(str) @@ -1849,7 +1851,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("a.repUntilAs(end) matches a.repUntil(end)") { - forAll(ParserGen.gen, ParserGen.gen0, Arbitrary.arbitrary[String]) { (p, end, str) => + forAll(ParserGen.gen, ParserGen.gen0, genString) { (p, end, str) => val left = p.fa.string.repUntilAs[NonEmptyList[String]](end.fa).parse(str) val right = p.fa.string.repUntil(end.fa).parse(str) @@ -1865,7 +1867,7 @@ class ParserTest extends munit.ScalaCheckSuite { case Left(_) => () } } && - forAll(ParserGen.gen, ParserGen.gen0, Arbitrary.arbitrary[String]) { (p, end, str) => + forAll(ParserGen.gen, ParserGen.gen0, genString) { (p, end, str) => val left = p.fa.string.repUntilAs0[List[String]](end.fa).parse(str) val right = p.fa.string.repUntil0(end.fa).parse(str) @@ -1884,7 +1886,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("parseAll law") { - forAll(ParserGen.gen0, Arbitrary.arbitrary[String]) { (a, str) => + forAll(ParserGen.gen0, genString) { (a, str) => val pall = (a.fa <* Parser.end).parse(str).map(_._2) assertEquals(a.fa.parseAll(str), pall) @@ -1904,73 +1906,69 @@ class ParserTest extends munit.ScalaCheckSuite { * Instead, we have some weakened versions of distributive laws */ property("b.orElse(c) ~ a == (b ~ a).orElse((!b) *> (c ~ a))") { - forAll(ParserGen.gen0, ParserGen.gen0, ParserGen.gen0, Arbitrary.arbitrary[String]) { - (a, b, c, str) => - val pa = a.fa - val pb = b.fa - val pc = c.fa + forAll(ParserGen.gen0, ParserGen.gen0, ParserGen.gen0, genString) { (a, b, c, str) => + val pa = a.fa + val pb = b.fa + val pc = c.fa - val left = pb.orElse(pc) ~ pa - val right = (pb ~ pa).orElse((!pb) *> (pc ~ pa)) + val left = pb.orElse(pc) ~ pa + val right = (pb ~ pa).orElse((!pb) *> (pc ~ pa)) - val leftRes = left.parse(str).toOption - val rightRes = right.parse(str).toOption - assertEquals(leftRes, rightRes) + val leftRes = left.parse(str).toOption + val rightRes = right.parse(str).toOption + assertEquals(leftRes, rightRes) } } property("b.orElse(c) ~ a == (b ~ a).orElse((!b) *> (c ~ a))") { - forAll(ParserGen.gen0, ParserGen.gen, ParserGen.gen, Arbitrary.arbitrary[String]) { - (a, b, c, str) => - val pa = a.fa - val pb = b.fa - val pc = c.fa - - val left = pb.orElse(pc) ~ pa - val right = (pb ~ pa).orElse((!pb).with1 *> (pc ~ pa)) - - val leftRes = left.parseAll(str).toOption - val rightRes = right.parseAll(str).toOption - if (leftRes.isDefined && rightRes.isDefined) { - assertEquals(leftRes, rightRes) - } else () + forAll(ParserGen.gen0, ParserGen.gen, ParserGen.gen, genString) { (a, b, c, str) => + val pa = a.fa + val pb = b.fa + val pc = c.fa + + val left = pb.orElse(pc) ~ pa + val right = (pb ~ pa).orElse((!pb).with1 *> (pc ~ pa)) + + val leftRes = left.parseAll(str).toOption + val rightRes = right.parseAll(str).toOption + if (leftRes.isDefined && rightRes.isDefined) { + assertEquals(leftRes, rightRes) + } else () } } property("a ~ b.orElse(c) == (a.soft ~ b).orElse(a ~ c)") { - forAll(ParserGen.gen0, ParserGen.gen0, ParserGen.gen0, Arbitrary.arbitrary[String]) { - (a, b, c, str) => - val pa = a.fa - val pb = b.fa - val pc = c.fa + forAll(ParserGen.gen0, ParserGen.gen0, ParserGen.gen0, genString) { (a, b, c, str) => + val pa = a.fa + val pb = b.fa + val pc = c.fa - val left = pa ~ pb.orElse(pc) - val right = (pa.soft ~ pb).orElse(pa ~ pc) + val left = pa ~ pb.orElse(pc) + val right = (pa.soft ~ pb).orElse(pa ~ pc) - val leftRes = left.parse(str).toOption - val rightRes = right.parse(str).toOption - assertEquals(leftRes, rightRes) + val leftRes = left.parse(str).toOption + val rightRes = right.parse(str).toOption + assertEquals(leftRes, rightRes) } } property("a ~ b.orElse(c) == (a.soft ~ b).orElse(a ~ c)") { - forAll(ParserGen.gen0, ParserGen.gen, ParserGen.gen, Arbitrary.arbitrary[String]) { - (a, b, c, str) => - val pa = a.fa - val pb = b.fa - val pc = c.fa + forAll(ParserGen.gen0, ParserGen.gen, ParserGen.gen, genString) { (a, b, c, str) => + val pa = a.fa + val pb = b.fa + val pc = c.fa - val left = pa ~ pb.orElse(pc) - val right = (pa.soft.with1 ~ pb).orElse(pa.with1 ~ pc) + val left = pa ~ pb.orElse(pc) + val right = (pa.soft.with1 ~ pb).orElse(pa.with1 ~ pc) - val leftRes = left.parse(str).toOption - val rightRes = right.parse(str).toOption - assertEquals(leftRes, rightRes) + val leftRes = left.parse(str).toOption + val rightRes = right.parse(str).toOption + assertEquals(leftRes, rightRes) } } property("a.backtrack.orElse(b) parses iff b.backtrack.orElse(a) (Parser0)") { - forAll(ParserGen.gen0, ParserGen.gen0, Arbitrary.arbitrary[String]) { (a, b, str) => + forAll(ParserGen.gen0, ParserGen.gen0, genString) { (a, b, str) => val pa = a.fa val pb = b.fa @@ -1984,7 +1982,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("a.backtrack.orElse(b) parses iff b.backtrack.orElse(a)") { - forAll(ParserGen.gen, ParserGen.gen, Arbitrary.arbitrary[String]) { (a, b, str) => + forAll(ParserGen.gen, ParserGen.gen, genString) { (a, b, str) => val pa = a.fa val pb = b.fa @@ -2013,7 +2011,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("a.repAs0[Vector[A]] matches a.rep0.map(_.toVector)") { - forAll(ParserGen.gen, Arbitrary.arbitrary[String]) { (a, str) => + forAll(ParserGen.gen, genString) { (a, str) => val pa: Parser[a.A] = a.fa val left = pa.repAs0[Vector[a.A]] @@ -2026,7 +2024,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("a.repAs[Vector[A]] matches a.rep.map(_.toList.toVector)") { - forAll(ParserGen.gen, Arbitrary.arbitrary[String]) { (a, str) => + forAll(ParserGen.gen, genString) { (a, str) => val pa: Parser[a.A] = a.fa val left = pa.repAs0[Vector[a.A]] @@ -2039,7 +2037,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("a.string.repAs0[String] matches a.string.rep0.map(_.mkString)") { - forAll(ParserGen.gen, Arbitrary.arbitrary[String]) { (a, str) => + forAll(ParserGen.gen, genString) { (a, str) => val pa: Parser[String] = a.fa.string val left = pa.repAs0[String] @@ -2052,7 +2050,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("a.repAs0[Unit] matches a.rep0.void") { - forAll(ParserGen.gen, Arbitrary.arbitrary[String]) { (a, str) => + forAll(ParserGen.gen, genString) { (a, str) => val pa: Parser[a.A] = a.fa val left = pa.repAs0[Unit](Accumulator0.unitAccumulator0) @@ -2065,7 +2063,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("a.peek == a.peek.peek") { - forAll(ParserGen.gen0, Arbitrary.arbitrary[String]) { (a, str) => + forAll(ParserGen.gen0, genString) { (a, str) => val pa = a.fa val left = pa.peek @@ -2078,7 +2076,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("a.backtrack.peek.orElse(b.peek) == (a.backtrack.orElse(b)).peek") { - forAll(ParserGen.gen0, ParserGen.gen0, Arbitrary.arbitrary[String]) { (a, b, str) => + forAll(ParserGen.gen0, ParserGen.gen0, genString) { (a, b, str) => val pa = a.fa.backtrack val pb = b.fa @@ -2092,7 +2090,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("a.peek == a.peek *> a.peek") { - forAll(ParserGen.gen0, Arbitrary.arbitrary[String]) { (a, str) => + forAll(ParserGen.gen0, genString) { (a, str) => val pa = a.fa.peek val left = pa @@ -2105,7 +2103,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("!a == (!a) *> (!a)") { - forAll(ParserGen.gen0, Arbitrary.arbitrary[String]) { (a, str) => + forAll(ParserGen.gen0, genString) { (a, str) => val pa = !a.fa val left = pa @@ -2118,7 +2116,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("!(!a) == a.peek") { - forAll(ParserGen.gen0, Arbitrary.arbitrary[String]) { (a, str) => + forAll(ParserGen.gen0, genString) { (a, str) => val pa = a.fa val left = (!(!pa)) @@ -2131,7 +2129,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("!(!(!a)) == !a") { - forAll(ParserGen.gen0, Arbitrary.arbitrary[String]) { (a, str) => + forAll(ParserGen.gen0, genString) { (a, str) => val pa = a.fa val left = !(!(!pa)) @@ -2242,7 +2240,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("p.filter(_ => true) == p") { - forAll(ParserGen.gen0, Arbitrary.arbitrary[String]) { (genP, str) => + forAll(ParserGen.gen0, genString) { (genP, str) => val res0 = genP.fa.filter(_ => true).parse(str) val res1 = genP.fa.parse(str) assertEquals(res0, res1) @@ -2250,14 +2248,14 @@ class ParserTest extends munit.ScalaCheckSuite { } property("p.filter(_ => false) fails") { - forAll(ParserGen.gen0, Arbitrary.arbitrary[String]) { (genP, str) => + forAll(ParserGen.gen0, genString) { (genP, str) => val res = genP.fa.filter(_ => false).parse(str) assert(res.isLeft) } } property("a Parser never succeeds and does not advance") { - forAll(ParserGen.gen, Arbitrary.arbitrary[String]) { (genP, str) => + forAll(ParserGen.gen, genString) { (genP, str) => genP.fa.parse(str) match { case Right((rest, _)) => assertNotEquals(rest, str) case Left(_) => () @@ -2490,7 +2488,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("a.string ~ b.string == (a ~ b).string") { - forAll(ParserGen.gen0, ParserGen.gen0, Arbitrary.arbitrary[String]) { (a, b, toParse) => + forAll(ParserGen.gen0, ParserGen.gen0, genString) { (a, b, toParse) => val pa = a.fa val pb = b.fa @@ -2502,7 +2500,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("a.string.soft ~ b.string == (a.soft ~ b).string") { - forAll(ParserGen.gen0, ParserGen.gen0, Arbitrary.arbitrary[String]) { (a, b, toParse) => + forAll(ParserGen.gen0, ParserGen.gen0, genString) { (a, b, toParse) => val pa = a.fa val pb = b.fa @@ -2514,28 +2512,26 @@ class ParserTest extends munit.ScalaCheckSuite { } property("oneOf0(a.map(_.string)) ~ oneOf0(a).string") { - forAll(Gen.choose(0, 5).flatMap(Gen.listOfN(_, ParserGen.gen0)), Arbitrary.arbitrary[String]) { - (as, toParse) => - val left = Parser.oneOf0(as.map(_.fa.string)) - val right = Parser.oneOf0[Any](as.map(_.fa)).string + forAll(Gen.choose(0, 5).flatMap(Gen.listOfN(_, ParserGen.gen0)), genString) { (as, toParse) => + val left = Parser.oneOf0(as.map(_.fa.string)) + val right = Parser.oneOf0[Any](as.map(_.fa)).string - assertEquals( - left.parse(toParse).leftMap(_.offsets), - right.parse(toParse).leftMap(_.offsets) - ) + assertEquals( + left.parse(toParse).leftMap(_.offsets), + right.parse(toParse).leftMap(_.offsets) + ) } } property("oneOf(a.map(_.string)) ~ oneOf(a).string") { - forAll(Gen.choose(0, 5).flatMap(Gen.listOfN(_, ParserGen.gen)), Arbitrary.arbitrary[String]) { - (as, toParse) => - val left = Parser.oneOf(as.map(_.fa.string)) - val right = Parser.oneOf[Any](as.map(_.fa)).string + forAll(Gen.choose(0, 5).flatMap(Gen.listOfN(_, ParserGen.gen)), genString) { (as, toParse) => + val left = Parser.oneOf(as.map(_.fa.string)) + val right = Parser.oneOf[Any](as.map(_.fa)).string - assertEquals( - left.parse(toParse).leftMap(_.offsets), - right.parse(toParse).leftMap(_.offsets) - ) + assertEquals( + left.parse(toParse).leftMap(_.offsets), + right.parse(toParse).leftMap(_.offsets) + ) } } @@ -2622,36 +2618,34 @@ class ParserTest extends munit.ScalaCheckSuite { } property("a context0 added is always at the top") { - forAll(ParserGen.gen0, Arbitrary.arbitrary[List[String]], Arbitrary.arbitrary[String]) { - (genP, ctx, str) => - ctx.foldLeft(genP.fa)(_.withContext(_)).parse(str) match { - case Left(err) => - err.expected.toList.foreach { exp => - val ectx = exp.context - assert(ectx.length >= ctx.length) - exp.context.zip(ctx.reverse).foreach { case (l, r) => - assertEquals(l, r) - } + forAll(ParserGen.gen0, Arbitrary.arbitrary[List[String]], genString) { (genP, ctx, str) => + ctx.foldLeft(genP.fa)(_.withContext(_)).parse(str) match { + case Left(err) => + err.expected.toList.foreach { exp => + val ectx = exp.context + assert(ectx.length >= ctx.length) + exp.context.zip(ctx.reverse).foreach { case (l, r) => + assertEquals(l, r) } - case _ => () - } + } + case _ => () + } } } property("a context added is always at the top") { - forAll(ParserGen.gen, Arbitrary.arbitrary[List[String]], Arbitrary.arbitrary[String]) { - (genP, ctx, str) => - ctx.foldLeft(genP.fa)(_.withContext(_)).parse(str) match { - case Left(err) => - err.expected.toList.foreach { exp => - val ectx = exp.context - assert(ectx.length >= ctx.length) - exp.context.zip(ctx.reverse).foreach { case (l, r) => - assertEquals(l, r) - } + forAll(ParserGen.gen, Arbitrary.arbitrary[List[String]], genString) { (genP, ctx, str) => + ctx.foldLeft(genP.fa)(_.withContext(_)).parse(str) match { + case Left(err) => + err.expected.toList.foreach { exp => + val ectx = exp.context + assert(ectx.length >= ctx.length) + exp.context.zip(ctx.reverse).foreach { case (l, r) => + assertEquals(l, r) } - case _ => () - } + } + case _ => () + } } } @@ -2687,7 +2681,7 @@ class ParserTest extends munit.ScalaCheckSuite { } property("P.caret is the same as index + toCaretUnsafe") { - forAll(ParserGen.gen, Arbitrary.arbitrary[String]) { (p, input) => + forAll(ParserGen.gen, genString) { (p, input) => val v1 = p.fa.void val lm = LocationMap(input) val left = (v1 *> Parser.index).map(lm.toCaretUnsafe(_)).parse(input) From b0d91b6ee931ff1b2df89d843c5abd5459f36722 Mon Sep 17 00:00:00 2001 From: Scala Steward <43047562+scala-steward@users.noreply.github.com> Date: Sat, 5 Mar 2022 19:07:33 +0100 Subject: [PATCH 12/23] Update sbt-typelevel to 0.4.6 (#390) * Update sbt-typelevel to 0.4.6 * Run prePR with sbt-typelevel --- .github/workflows/ci.yml | 44 ++++++++++++++++++++++++++++++++++++---- project/plugins.sbt | 2 +- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae7c478a..394c9612 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,12 +37,21 @@ jobs: with: fetch-depth: 0 + - name: Download Java (temurin@8) + id: download-java-temurin-8 + if: matrix.java == 'temurin@8' + uses: typelevel/download-java@v1 + with: + distribution: temurin + java-version: 8 + - name: Setup Java (temurin@8) if: matrix.java == 'temurin@8' uses: actions/setup-java@v2 with: - distribution: temurin + distribution: jdkfile java-version: 8 + jdkFile: ${{ steps.download-java-temurin-8.outputs.jdkFile }} - name: Cache sbt uses: actions/cache@v2 @@ -96,12 +105,21 @@ jobs: with: fetch-depth: 0 + - name: Download Java (temurin@8) + id: download-java-temurin-8 + if: matrix.java == 'temurin@8' + uses: typelevel/download-java@v1 + with: + distribution: temurin + java-version: 8 + - name: Setup Java (temurin@8) if: matrix.java == 'temurin@8' uses: actions/setup-java@v2 with: - distribution: temurin + distribution: jdkfile java-version: 8 + jdkFile: ${{ steps.download-java-temurin-8.outputs.jdkFile }} - name: Cache sbt uses: actions/cache@v2 @@ -189,12 +207,21 @@ jobs: - name: Checkout current branch (fast) uses: actions/checkout@v2 + - name: Download Java (temurin@8) + id: download-java-temurin-8 + if: matrix.java == 'temurin@8' + uses: typelevel/download-java@v1 + with: + distribution: temurin + java-version: 8 + - name: Setup Java (temurin@8) if: matrix.java == 'temurin@8' uses: actions/setup-java@v2 with: - distribution: temurin + distribution: jdkfile java-version: 8 + jdkFile: ${{ steps.download-java-temurin-8.outputs.jdkFile }} - name: Cache sbt uses: actions/cache@v2 @@ -222,12 +249,21 @@ jobs: - name: Checkout current branch (fast) uses: actions/checkout@v2 + - name: Download Java (temurin@8) + id: download-java-temurin-8 + if: matrix.java == 'temurin@8' + uses: typelevel/download-java@v1 + with: + distribution: temurin + java-version: 8 + - name: Setup Java (temurin@8) if: matrix.java == 'temurin@8' uses: actions/setup-java@v2 with: - distribution: temurin + distribution: jdkfile java-version: 8 + jdkFile: ${{ steps.download-java-temurin-8.outputs.jdkFile }} - name: Cache sbt uses: actions/cache@v2 diff --git a/project/plugins.sbt b/project/plugins.sbt index 03997aa0..b4989add 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -7,7 +7,7 @@ addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.9.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.1") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.9.3") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.3") -addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.4.5") +addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.4.6") addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.1") addSbtPlugin("io.github.jonas" % "sbt-paradox-material-theme" % "0.6.0") addSbtPlugin("com.typesafe.sbt" % "sbt-site" % "1.4.1") From 80ab15c78e3c9049650063cbb14d09ae4d72e36f Mon Sep 17 00:00:00 2001 From: i10416 Date: Sun, 20 Mar 2022 17:56:21 +0900 Subject: [PATCH 13/23] resolve conflicts --- build.sbt | 17 +---- .../js/src/main/scala/cats/parse/BitSet.scala | 48 +------------ .../src/main/scala/cats/parse/BitSet.scala | 48 +------------ .../main/scala/cats/parse/BitSetCompat.scala | 70 +++++++++++++++++++ .../test/scala/cats/parse/BitSetTest.scala | 10 +-- project/MimaExclusionRules.scala | 29 ++++++++ 6 files changed, 109 insertions(+), 113 deletions(-) create mode 100644 core/shared/src/main/scala/cats/parse/BitSetCompat.scala create mode 100644 project/MimaExclusionRules.scala diff --git a/build.sbt b/build.sbt index 67442c2f..0d93a980 100644 --- a/build.sbt +++ b/build.sbt @@ -1,7 +1,6 @@ import com.typesafe.tools.mima.core._ import sbtcrossproject.CrossPlugin.autoImport.{crossProject, CrossType} import Dependencies._ - addCommandAlias("fmt", "; scalafmtAll; scalafmtSbt") addCommandAlias("fmtCheck", "; scalafmtCheckAll; scalafmtSbtCheck") @@ -140,8 +139,6 @@ lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) */ if (tlIsScala3.value) List( - ProblemFilters.exclude[DirectMissingMethodProblem]("cats.parse.Parser#Impl.mergeCharIn"), - ProblemFilters.exclude[DirectMissingMethodProblem]("cats.parse.Parser#Impl.mergeStrIn"), ProblemFilters.exclude[DirectMissingMethodProblem]("cats.parse.RadixNode.children"), ProblemFilters.exclude[DirectMissingMethodProblem]("cats.parse.RadixNode.fsts"), ProblemFilters.exclude[DirectMissingMethodProblem]("cats.parse.RadixNode.prefixes"), @@ -149,20 +146,10 @@ lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) ProblemFilters.exclude[FinalClassProblem]("cats.parse.RadixNode"), ProblemFilters.exclude[IncompatibleMethTypeProblem]("cats.parse.Parser#State.error_="), ProblemFilters.exclude[IncompatibleMethTypeProblem]("cats.parse.RadixNode.this"), - ProblemFilters - .exclude[IncompatibleResultTypeProblem]("cats.parse.Parser#Impl#StringIn.parseMut"), - ProblemFilters.exclude[IncompatibleResultTypeProblem]("cats.parse.Parser#Impl.stringIn"), ProblemFilters.exclude[IncompatibleResultTypeProblem]("cats.parse.Parser#State.error") ) - else - List( - ProblemFilters.exclude[DirectMissingMethodProblem]("cats.parse.Parser#Impl.mergeCharIn"), - ProblemFilters.exclude[DirectMissingMethodProblem]("cats.parse.Parser#Impl.mergeStrIn"), - ProblemFilters - .exclude[IncompatibleResultTypeProblem]("cats.parse.Parser#Impl#StringIn.parseMut"), - ProblemFilters.exclude[IncompatibleResultTypeProblem]("cats.parse.Parser#Impl.stringIn") - ) - } + else Nil + } ++ MimaExclusionRules.parserImpl ++ MimaExclusionRules.bitSetUtil ) .jsSettings( crossScalaVersions := (ThisBuild / crossScalaVersions).value.filterNot(_.startsWith("2.11")), diff --git a/core/js/src/main/scala/cats/parse/BitSet.scala b/core/js/src/main/scala/cats/parse/BitSet.scala index df2c9280..b8d329b8 100644 --- a/core/js/src/main/scala/cats/parse/BitSet.scala +++ b/core/js/src/main/scala/cats/parse/BitSet.scala @@ -21,50 +21,4 @@ package cats.parse -import scala.collection.mutable.BitSet - -object BitSetUtil { - type Tpe = BitSet - - @inline final val isScalaNative = false - @inline final val isScalaJs = true - @inline final val isScalaJvm = false - - @inline final def isSet(b: BitSet, idx: Int): Boolean = - (idx >= 0) && b(idx) - - def bitSetFor(charArray: Array[Char]): BitSet = { - val min = charArray(0).toInt - val bs = new BitSet(charArray(charArray.length - 1).toInt + 1 - min) - var idx = 0 - while (idx < charArray.length) { - bs += charArray(idx).toInt - min - idx += 1 - } - - bs - } - - def isSingleton(t: Tpe): Boolean = t.size == 1 - - def union(bs: List[(Int, BitSet)]): Iterable[Char] = - union(bs.iterator) - - // what are all the Chars in these bitsets - def union(bs: Iterator[(Int, BitSet)]): Iterable[Char] = { - def toIter(m: Int, bs: BitSet): Iterator[Char] = - bs.iterator.map { i => (i + m).toChar } ++ Iterator.single(m.toChar) - - bs.flatMap { case (m, bs) => toIter(m, bs) }.toSet - } - - def bitSetForRange(count: Int): BitSet = { - val bs = new BitSet(count) - var cur = 0 - while (cur < count) { - bs += cur - cur += 1 - } - bs - } -} +object BitSetUtil extends BitSetUtilCompat(true, false) diff --git a/core/jvm/src/main/scala/cats/parse/BitSet.scala b/core/jvm/src/main/scala/cats/parse/BitSet.scala index fae46c1b..a542b234 100644 --- a/core/jvm/src/main/scala/cats/parse/BitSet.scala +++ b/core/jvm/src/main/scala/cats/parse/BitSet.scala @@ -21,50 +21,4 @@ package cats.parse -import java.util.BitSet - -object BitSetUtil { - type Tpe = BitSet - @inline final val isScalaNative = false - @inline final val isScalaJs = false - @inline final val isScalaJvm = true - - @inline final def isSet(b: BitSet, idx: Int): Boolean = - // BitSet can't deal with negatives, so mask those out - b.get(idx & Int.MaxValue) - - // we require a sorted, nonEmpty, charArray - def bitSetFor(charArray: Array[Char]): BitSet = { - val min = charArray(0).toInt - val bs = new BitSet(charArray(charArray.length - 1).toInt + 1 - min) - var idx = 0 - while (idx < charArray.length) { - bs.set(charArray(idx).toInt - min) - idx += 1 - } - - bs - } - - def isSingleton(t: Tpe): Boolean = t.cardinality() == 1 - - // what are all the Chars in these bitsets - def union(bs: List[(Int, BitSet)]): Iterable[Char] = - union(bs.iterator) - - def union(bs: Iterator[(Int, BitSet)]): Iterable[Char] = { - def toIter(m: Int, bs: BitSet): Iterator[Char] = - Iterator - .iterate(0) { m => bs.nextSetBit(m + 1) } - .takeWhile(_ >= 0) - .map { i => (m + i).toChar } - - bs.flatMap { case (m, bs) => toIter(m, bs) }.toSet - } - - def bitSetForRange(count: Int): BitSet = { - val bs = new BitSet(count) - bs.flip(0, count) - bs - } -} +object BitSetUtil extends BitSetUtilCompat(false, true) diff --git a/core/shared/src/main/scala/cats/parse/BitSetCompat.scala b/core/shared/src/main/scala/cats/parse/BitSetCompat.scala new file mode 100644 index 00000000..ebec94d8 --- /dev/null +++ b/core/shared/src/main/scala/cats/parse/BitSetCompat.scala @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2021 Typelevel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package cats.parse + +import java.util.BitSet +private[parse] abstract class BitSetUtilCompat( + // TODO: Remove isScalaJs/isScalaJvm in next minor version update. See https://github.com/typelevel/cats-parse/issues/391. + @inline final val isScalaJs: Boolean, + @inline final val isScalaJvm: Boolean +) { + type Tpe = BitSet + + @inline final def isSet(b: BitSet, idx: Int): Boolean = + // BitSet can't deal with negatives, so mask those out + b.get(idx & Int.MaxValue) + + // we require a sorted, nonEmpty, charArray + def bitSetFor(charArray: Array[Char]): BitSet = { + val min = charArray(0).toInt + val bs = new BitSet(charArray(charArray.length - 1).toInt + 1 - min) + var idx = 0 + while (idx < charArray.length) { + bs.set(charArray(idx).toInt - min) + idx += 1 + } + + bs + } + + def isSingleton(t: Tpe): Boolean = t.cardinality() == 1 + + // what are all the Chars in these bitsets + def union(bs: List[(Int, BitSet)]): Iterable[Char] = + union(bs.iterator) + + def union(bs: Iterator[(Int, BitSet)]): Iterable[Char] = { + def toIter(m: Int, bs: BitSet): Iterator[Char] = + Iterator + .iterate(0) { m => bs.nextSetBit(m + 1) } + .takeWhile(_ >= 0) + .map { i => (m + i).toChar } + + bs.flatMap { case (m, bs) => toIter(m, bs) }.toSet + } + + def bitSetForRange(count: Int): BitSet = { + val bs = new BitSet(count) + bs.flip(0, count) + bs + } +} diff --git a/core/shared/src/test/scala/cats/parse/BitSetTest.scala b/core/shared/src/test/scala/cats/parse/BitSetTest.scala index 48b16969..12eac98f 100644 --- a/core/shared/src/test/scala/cats/parse/BitSetTest.scala +++ b/core/shared/src/test/scala/cats/parse/BitSetTest.scala @@ -24,10 +24,12 @@ package cats.parse import org.scalacheck.Prop.forAll class BitSetTest extends munit.ScalaCheckSuite { - test("isScalaJs/isScalaJvm/isScalaNative is consistent") { - assert(!(BitSetUtil.isScalaJs && BitSetUtil.isScalaJvm && BitSetUtil.isScalaNative)) - assert(BitSetUtil.isScalaJs || BitSetUtil.isScalaJvm || BitSetUtil.isScalaNative) - assert(BitSetUtil.isScalaJs ^ BitSetUtil.isScalaJvm ^ BitSetUtil.isScalaNative) + // TODO: Remove isScalaJs/isScalaJvm in next minor version update. See https://github.com/typelevel/cats-parse/issues/391. + test("isScalaJs/isScalaJvm is consistent") { + // This will need to be updated if we ever add scala-native + assert(!(BitSetUtil.isScalaJs && BitSetUtil.isScalaJvm)) + assert(BitSetUtil.isScalaJs || BitSetUtil.isScalaJvm) + assert(BitSetUtil.isScalaJs ^ BitSetUtil.isScalaJvm) } property("BitSetUtil union works") { diff --git a/project/MimaExclusionRules.scala b/project/MimaExclusionRules.scala new file mode 100644 index 00000000..c20cf738 --- /dev/null +++ b/project/MimaExclusionRules.scala @@ -0,0 +1,29 @@ +import com.typesafe.tools.mima.core.ProblemFilters +import com.typesafe.tools.mima.core.IncompatibleMethTypeProblem +import com.typesafe.tools.mima.core.IncompatibleResultTypeProblem +import com.typesafe.tools.mima.core.DirectMissingMethodProblem +object MimaExclusionRules { + val parserImpl = Seq( + "cats.parse.Parser#Impl.mergeCharIn", + "cats.parse.Parser#Impl.mergeStrIn", + "cats.parse.Parser#Impl#CharIn.copy", + "cats.parse.Parser#Impl#CharIn.apply", + "cats.parse.Parser#Impl#CharIn.this" + ).map(ProblemFilters.exclude[IncompatibleMethTypeProblem](_)) ++ Seq( + "cats.parse.Parser#Impl#StringIn.parseMut", + "cats.parse.Parser#Impl.stringIn", + "cats.parse.Parser#Impl#CharIn.copy$default$2", + "cats.parse.Parser#Impl#CharIn.bitSet", + "cats.parse.Parser#Impl#CharIn._2" + ).map(ProblemFilters.exclude[IncompatibleResultTypeProblem](_)) ++ Seq( + "cats.parse.Parser#Impl.mergeCharIn", + "cats.parse.Parser#Impl.mergeStrIn" + ).map(ProblemFilters.exclude[DirectMissingMethodProblem](_)) + // TODO: Remove these rules in future release. + val bitSetUtil = Seq( + "cats.parse.BitSetUtil.isSingleton", + "cats.parse.BitSetUtil.isSet" + ).map(ProblemFilters.exclude[IncompatibleMethTypeProblem](_)) ++ Seq( + "cats.parse.BitSetUtil.bitSetFor" + ).map(ProblemFilters.exclude[IncompatibleResultTypeProblem](_)) +} From 2e82ec11e1816376e66db74ccec9cce785545efc Mon Sep 17 00:00:00 2001 From: i10416 Date: Tue, 22 Mar 2022 00:09:49 +0900 Subject: [PATCH 14/23] tweak --- build.sbt | 3 ++ .../src/main/scala/cats/parse/BitSet.scala | 47 +------------------ .../test/scala/cats/parse/BitSetTest.scala | 10 ++-- .../test/scala/cats/parse/ParserTest.scala | 2 +- .../test/scala/cats/parse/RadixNodeTest.scala | 2 +- project/plugins.sbt | 2 +- 6 files changed, 13 insertions(+), 53 deletions(-) diff --git a/build.sbt b/build.sbt index 0d93a980..0b4ab9fe 100644 --- a/build.sbt +++ b/build.sbt @@ -155,6 +155,9 @@ lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) crossScalaVersions := (ThisBuild / crossScalaVersions).value.filterNot(_.startsWith("2.11")), coverageEnabled := false ) + .nativeSettings( + crossScalaVersions := (ThisBuild / crossScalaVersions).value.filterNot(_.startsWith("2.11")) + ) lazy val coreJVM = core.jvm.settings(jvmVersionSettings) lazy val coreJS = core.js diff --git a/core/native/src/main/scala/cats/parse/BitSet.scala b/core/native/src/main/scala/cats/parse/BitSet.scala index 471e08b6..83bbe01f 100644 --- a/core/native/src/main/scala/cats/parse/BitSet.scala +++ b/core/native/src/main/scala/cats/parse/BitSet.scala @@ -21,49 +21,4 @@ package cats.parse -import scala.collection.mutable.BitSet - -object BitSetUtil { - type Tpe = BitSet - @inline final val isScalaNative = true - @inline final val isScalaJs = false - @inline final val isScalaJvm = false - - @inline final def isSet(b: BitSet, idx: Int): Boolean = - (idx >= 0) && b(idx) - - def bitSetFor(charArray: Array[Char]): BitSet = { - val min = charArray(0).toInt - val bs = new BitSet(charArray(charArray.length - 1).toInt + 1 - min) - var idx = 0 - while (idx < charArray.length) { - bs += charArray(idx).toInt - min - idx += 1 - } - - bs - } - - def isSingleton(t: Tpe): Boolean = t.size == 1 - - def union(bs: List[(Int, BitSet)]): Iterable[Char] = - union(bs.iterator) - - // what are all the Chars in these bitsets - def union(bs: Iterator[(Int, BitSet)]): Iterable[Char] = { - def toIter(m: Int, bs: BitSet): Iterator[Char] = - bs.iterator.map { i => (i + m).toChar } ++ Iterator.single(m.toChar) - - bs.flatMap { case (m, bs) => toIter(m, bs) }.toSet - } - - def bitSetForRange(count: Int): BitSet = { - val bs = new BitSet(count) - var cur = 0 - while (cur < count) { - bs += cur - cur += 1 - } - bs - } -} +object BitSetUtil extends BitSetUtilCompat(false, false) diff --git a/core/shared/src/test/scala/cats/parse/BitSetTest.scala b/core/shared/src/test/scala/cats/parse/BitSetTest.scala index 12eac98f..a0e38ff0 100644 --- a/core/shared/src/test/scala/cats/parse/BitSetTest.scala +++ b/core/shared/src/test/scala/cats/parse/BitSetTest.scala @@ -26,10 +26,12 @@ import org.scalacheck.Prop.forAll class BitSetTest extends munit.ScalaCheckSuite { // TODO: Remove isScalaJs/isScalaJvm in next minor version update. See https://github.com/typelevel/cats-parse/issues/391. test("isScalaJs/isScalaJvm is consistent") { - // This will need to be updated if we ever add scala-native - assert(!(BitSetUtil.isScalaJs && BitSetUtil.isScalaJvm)) - assert(BitSetUtil.isScalaJs || BitSetUtil.isScalaJvm) - assert(BitSetUtil.isScalaJs ^ BitSetUtil.isScalaJvm) + if (BitSetUtil.isScalaJs || BitSetUtil.isScalaJvm) { + // This will need to be updated if we ever add scala-native + assert(!(BitSetUtil.isScalaJs && BitSetUtil.isScalaJvm)) + assert(BitSetUtil.isScalaJs || BitSetUtil.isScalaJvm) + assert(BitSetUtil.isScalaJs ^ BitSetUtil.isScalaJvm) + } } property("BitSetUtil union works") { diff --git a/core/shared/src/test/scala/cats/parse/ParserTest.scala b/core/shared/src/test/scala/cats/parse/ParserTest.scala index fe63f0e2..90234a58 100644 --- a/core/shared/src/test/scala/cats/parse/ParserTest.scala +++ b/core/shared/src/test/scala/cats/parse/ParserTest.scala @@ -610,7 +610,7 @@ class ParserTest extends munit.ScalaCheckSuite { import ParserGen.{arbParser0, arbParser, biasSmall, genString, arbString} - val tests: Int = if (BitSetUtil.isScalaJs) 50 else 2000 + val tests: Int = if (BitSetUtil.isScalaJvm) 2000 else 50 override def scalaCheckTestParameters = super.scalaCheckTestParameters diff --git a/core/shared/src/test/scala/cats/parse/RadixNodeTest.scala b/core/shared/src/test/scala/cats/parse/RadixNodeTest.scala index 8b6669af..a7e78b30 100644 --- a/core/shared/src/test/scala/cats/parse/RadixNodeTest.scala +++ b/core/shared/src/test/scala/cats/parse/RadixNodeTest.scala @@ -25,7 +25,7 @@ import org.scalacheck.{Gen, Prop} import org.scalacheck.Prop.forAll class RadixNodeTest extends munit.ScalaCheckSuite { - val tests: Int = if (BitSetUtil.isScalaJs) 50 else 20000 + val tests: Int = if (BitSetUtil.isScalaJvm) 20000 else 50 override def scalaCheckTestParameters = super.scalaCheckTestParameters diff --git a/project/plugins.sbt b/project/plugins.sbt index b4989add..1c1a6c88 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -4,7 +4,7 @@ addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.9.2") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.1.0") addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.1.0") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.9.0") -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.1") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.4") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.9.3") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.3") addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.4.6") From 4131ef028e7fe2190796d8536f83f6fd1a94f80d Mon Sep 17 00:00:00 2001 From: i10416 Date: Tue, 22 Mar 2022 02:14:06 +0900 Subject: [PATCH 15/23] rm comment --- core/shared/src/test/scala/cats/parse/BitSetTest.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/core/shared/src/test/scala/cats/parse/BitSetTest.scala b/core/shared/src/test/scala/cats/parse/BitSetTest.scala index a0e38ff0..2a1f9027 100644 --- a/core/shared/src/test/scala/cats/parse/BitSetTest.scala +++ b/core/shared/src/test/scala/cats/parse/BitSetTest.scala @@ -27,7 +27,6 @@ class BitSetTest extends munit.ScalaCheckSuite { // TODO: Remove isScalaJs/isScalaJvm in next minor version update. See https://github.com/typelevel/cats-parse/issues/391. test("isScalaJs/isScalaJvm is consistent") { if (BitSetUtil.isScalaJs || BitSetUtil.isScalaJvm) { - // This will need to be updated if we ever add scala-native assert(!(BitSetUtil.isScalaJs && BitSetUtil.isScalaJvm)) assert(BitSetUtil.isScalaJs || BitSetUtil.isScalaJvm) assert(BitSetUtil.isScalaJs ^ BitSetUtil.isScalaJvm) From 1b65e386e87490ea83eb4285b470428c1e3977b6 Mon Sep 17 00:00:00 2001 From: i10416 Date: Tue, 22 Mar 2022 03:07:42 +0900 Subject: [PATCH 16/23] tweak build --- build.sbt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/build.sbt b/build.sbt index 0b4ab9fe..003a43c7 100644 --- a/build.sbt +++ b/build.sbt @@ -113,6 +113,9 @@ lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) .settings( name := "cats-parse", tlFatalWarningsInCi := !tlIsScala3.value, + libraryDependencies ++= { + if (tlIsScala3.value) Nil else Seq("org.scala-lang" % "scala-reflect" % scalaVersion.value) + }, libraryDependencies ++= { val isScala211 = CrossVersion.partialVersion(scalaVersion.value).contains((2, 11)) Seq( @@ -121,9 +124,6 @@ lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) munitScalacheck.value % Test ) }, - libraryDependencies ++= { - if (tlIsScala3.value) Nil else Seq("org.scala-lang" % "scala-reflect" % scalaVersion.value) - }, scalacOptions ++= { val isScala211 = CrossVersion.partialVersion(scalaVersion.value).contains((2, 11)) // this code seems to trigger a bug in 2.11 pattern analysis @@ -156,7 +156,8 @@ lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) coverageEnabled := false ) .nativeSettings( - crossScalaVersions := (ThisBuild / crossScalaVersions).value.filterNot(_.startsWith("2.11")) + crossScalaVersions := (ThisBuild / crossScalaVersions).value.filterNot(_.startsWith("2.11")), + coverageEnabled := false ) lazy val coreJVM = core.jvm.settings(jvmVersionSettings) From 3f74d14be126f79a4ac6142d5faf5bdf14773cde Mon Sep 17 00:00:00 2001 From: i10416 Date: Thu, 31 Mar 2022 02:18:18 +0900 Subject: [PATCH 17/23] tweak build --- build.sbt | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/build.sbt b/build.sbt index 003a43c7..11ce8502 100644 --- a/build.sbt +++ b/build.sbt @@ -1,6 +1,11 @@ import com.typesafe.tools.mima.core._ import sbtcrossproject.CrossPlugin.autoImport.{crossProject, CrossType} import Dependencies._ +val scala211 = "2.11.12" +val scala212 = "2.12.15" +val scala213 = "2.13.8" +val scala30 = "3.0.2" + addCommandAlias("fmt", "; scalafmtAll; scalafmtSbt") addCommandAlias("fmtCheck", "; scalafmtCheckAll; scalafmtSbtCheck") @@ -10,7 +15,7 @@ ThisBuild / tlBaseVersion := "0.3" ThisBuild / startYear := Some(2021) ThisBuild / developers += tlGitHubDev("johnynek", "P. Oscar Boykin") -ThisBuild / crossScalaVersions := List("3.0.2", "2.11.12", "2.12.15", "2.13.8") +ThisBuild / crossScalaVersions := List(scala30, scala211, scala212, scala213) ThisBuild / tlVersionIntroduced := Map("3" -> "0.3.4") ThisBuild / githubWorkflowBuild := Seq( @@ -28,7 +33,7 @@ ThisBuild / githubWorkflowAddedJobs ++= Seq( WorkflowJob( id = "build-docs", name = "Build docs", - scalas = List("2.13.8"), + scalas = List(scala213), steps = List(WorkflowStep.Checkout) ++ WorkflowStep.SetupJava( githubWorkflowJavaVersions.value.toList ) ++ githubWorkflowGeneratedCacheSteps.value ++ List(WorkflowStep.Sbt(List("docs/mdoc"))) @@ -36,7 +41,7 @@ ThisBuild / githubWorkflowAddedJobs ++= Seq( WorkflowJob( id = "coverage", name = "Generate coverage report", - scalas = List("2.13.8"), + scalas = List(scala213), steps = List(WorkflowStep.Checkout) ++ WorkflowStep.SetupJava( githubWorkflowJavaVersions.value.toList ) ++ githubWorkflowGeneratedCacheSteps.value ++ List( @@ -78,7 +83,7 @@ lazy val root = project .in(file(".")) .aggregate(core.jvm, core.js, core.native, bench) .enablePlugins(NoPublishPlugin) - .settings(scalaVersion := "2.13.8") + .settings(scalaVersion := scala213) lazy val docs = project .enablePlugins( From bf3fddefef465823feb1d0ae73053016c008dbb7 Mon Sep 17 00:00:00 2001 From: i10416 Date: Mon, 11 Apr 2022 21:58:31 +0900 Subject: [PATCH 18/23] Update deps --- project/Dependencies.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index bea9c227..44110d28 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -4,8 +4,8 @@ import org.portablescala.sbtplatformdeps.PlatformDepsPlugin.autoImport._ object Dependencies { lazy val cats = Def.setting("org.typelevel" %%% "cats-core" % "2.7.0") lazy val cats211 = Def.setting("org.typelevel" %%% "cats-core" % "2.0.0") - lazy val munit = Def.setting("org.scalameta" %%% "munit" % "0.7.29") - lazy val munitScalacheck = Def.setting("org.scalameta" %%% "munit-scalacheck" % "0.7.29") + lazy val munit = Def.setting("org.scalameta" %%% "munit" % "1.0.0-M3") + lazy val munitScalacheck = Def.setting("org.scalameta" %%% "munit-scalacheck" % "1.0.0-M3") lazy val fastParse = "com.lihaoyi" %% "fastparse" % "2.3.3" lazy val parsley = "org.http4s" %% "parsley" % "1.5.0-M3" lazy val jawnAst = "org.typelevel" %% "jawn-ast" % "1.3.2" From da84253223cb1ba9edd8270e9f8c50f251bbd255 Mon Sep 17 00:00:00 2001 From: i10416 Date: Mon, 11 Apr 2022 21:58:44 +0900 Subject: [PATCH 19/23] remove redundant assertion --- core/shared/src/test/scala/cats/parse/BitSetTest.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/core/shared/src/test/scala/cats/parse/BitSetTest.scala b/core/shared/src/test/scala/cats/parse/BitSetTest.scala index 2a1f9027..fdbf0cf4 100644 --- a/core/shared/src/test/scala/cats/parse/BitSetTest.scala +++ b/core/shared/src/test/scala/cats/parse/BitSetTest.scala @@ -28,7 +28,6 @@ class BitSetTest extends munit.ScalaCheckSuite { test("isScalaJs/isScalaJvm is consistent") { if (BitSetUtil.isScalaJs || BitSetUtil.isScalaJvm) { assert(!(BitSetUtil.isScalaJs && BitSetUtil.isScalaJvm)) - assert(BitSetUtil.isScalaJs || BitSetUtil.isScalaJvm) assert(BitSetUtil.isScalaJs ^ BitSetUtil.isScalaJvm) } } From bc79463ea57a2cc1e1f8a9532ad2a6c67cd0ad1f Mon Sep 17 00:00:00 2001 From: i10416 Date: Mon, 11 Apr 2022 22:03:19 +0900 Subject: [PATCH 20/23] bye-bye scala 3.0 --- build.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 11ce8502..1cf3ef95 100644 --- a/build.sbt +++ b/build.sbt @@ -4,7 +4,7 @@ import Dependencies._ val scala211 = "2.11.12" val scala212 = "2.12.15" val scala213 = "2.13.8" -val scala30 = "3.0.2" +val scala31 = "3.1.2" addCommandAlias("fmt", "; scalafmtAll; scalafmtSbt") addCommandAlias("fmtCheck", "; scalafmtCheckAll; scalafmtSbtCheck") @@ -15,7 +15,7 @@ ThisBuild / tlBaseVersion := "0.3" ThisBuild / startYear := Some(2021) ThisBuild / developers += tlGitHubDev("johnynek", "P. Oscar Boykin") -ThisBuild / crossScalaVersions := List(scala30, scala211, scala212, scala213) +ThisBuild / crossScalaVersions := List(scala31, scala211, scala212, scala213) ThisBuild / tlVersionIntroduced := Map("3" -> "0.3.4") ThisBuild / githubWorkflowBuild := Seq( From 9d2df6ce8871ccc29f36044c2a82e4cf5e76829d Mon Sep 17 00:00:00 2001 From: i10416 Date: Mon, 11 Apr 2022 22:09:22 +0900 Subject: [PATCH 21/23] fix workflow --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 394c9612..3900bd65 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [3.0.2, 2.11.12, 2.12.15, 2.13.8] + scala: [3.1.2, 2.11.12, 2.12.15, 2.13.8] java: [temurin@8] runs-on: ${{ matrix.os }} steps: @@ -133,12 +133,12 @@ jobs: ~/Library/Caches/Coursier/v1 key: ${{ runner.os }}-sbt-cache-v2-${{ hashFiles('**/*.sbt') }}-${{ hashFiles('project/build.properties') }} - - name: Download target directories (3.0.2) + - name: Download target directories (3.1.2) uses: actions/download-artifact@v2 with: - name: target-${{ matrix.os }}-${{ matrix.java }}-3.0.2 + name: target-${{ matrix.os }}-${{ matrix.java }}-3.1.2 - - name: Inflate target directories (3.0.2) + - name: Inflate target directories (3.1.2) run: | tar xf targets.tar rm targets.tar From e4a8aa3efd82c28f1971240152872fb8d2fe452b Mon Sep 17 00:00:00 2001 From: i10416 Date: Sat, 30 Apr 2022 02:46:53 +0900 Subject: [PATCH 22/23] exclude scala2.12 x scala native bincompat repo cats-parse for scala 2.12 scala native is not released --- build.sbt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build.sbt b/build.sbt index 1cf3ef95..7f52a95a 100644 --- a/build.sbt +++ b/build.sbt @@ -162,6 +162,10 @@ lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) ) .nativeSettings( crossScalaVersions := (ThisBuild / crossScalaVersions).value.filterNot(_.startsWith("2.11")), + mimaPreviousArtifacts := { + val isScala212 = CrossVersion.partialVersion(scalaVersion.value).contains((2, 12)) + if (isScala212) Set.empty else mimaPreviousArtifacts.value + }, coverageEnabled := false ) From 18065c81a351b9dceb2cc5c6c619a06d89e65dd8 Mon Sep 17 00:00:00 2001 From: 110416 Date: Sun, 12 Jun 2022 01:30:59 +0900 Subject: [PATCH 23/23] Update build.sbt Co-authored-by: Arman Bilge --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 7905b013..83ec0bba 100644 --- a/build.sbt +++ b/build.sbt @@ -92,7 +92,7 @@ lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) "Sonatype OSS Snapshots s01" at "https://s01.oss.sonatype.org/content/repositories/snapshots" ), crossScalaVersions := (ThisBuild / crossScalaVersions).value.filterNot(_.startsWith("2.11")), - mimaPreviousArtifacts := Set.empty, + tlVersionIntroduced := List("2.12", "2.13", "3").map(_ -> "0.3.8").toMap, libraryDependencies := { if (scalaVersion.value.startsWith("3.1")) { libraryDependencies.value.filterNot(_ == Dependencies.cats) ++ Seq(