Skip to content

Commit

Permalink
Add a few new laws and optimizations (#58)
Browse files Browse the repository at this point in the history
* Add a few new laws and optimizations

* minor cleanup

* improve alwaysSucceeds a bit, use in oneOf

* respond to ross's suggestion
  • Loading branch information
johnynek committed Nov 11, 2020
1 parent 02f2501 commit 0bc2f47
Show file tree
Hide file tree
Showing 2 changed files with 121 additions and 10 deletions.
49 changes: 39 additions & 10 deletions core/shared/src/main/scala/cats/parse/Parser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -705,7 +705,11 @@ object Parser extends ParserInstances {
case Impl.Fail() :: rest =>
flatten(rest, acc)
case notOneOf :: rest =>
flatten(rest, notOneOf :: acc)
if (Impl.alwaysSucceeds(notOneOf)) {
(notOneOf :: acc).reverse.distinct
} else {
flatten(rest, notOneOf :: acc)
}
}

val flat = flatten(ps, Nil)
Expand Down Expand Up @@ -821,10 +825,9 @@ object Parser extends ParserInstances {
*/
def map[A, B](p: Parser[A])(fn: A => B): Parser[B] =
p match {
case p1: Parser1[A] => map1(p1)(fn)
case Impl.Map(p0, f0) =>
Impl.Map(p0, AndThen(f0).andThen(fn))
case Impl.Map1(p0, f0) =>
Impl.Map1(p0, AndThen(f0).andThen(fn))
case _ => Impl.Map(p, fn)
}

Expand All @@ -834,6 +837,10 @@ object Parser extends ParserInstances {
p match {
case Impl.Map1(p0, f0) =>
Impl.Map1(p0, AndThen(f0).andThen(fn))
case Impl.Fail() | Impl.FailWith(_) =>
// these are really Parser1[Nothing{
// but scala can't see that, so we cast
p.asInstanceOf[Parser1[B]]
case _ => Impl.Map1(p, fn)
}

Expand Down Expand Up @@ -1006,6 +1013,7 @@ object Parser extends ParserInstances {
*/
def void(pa: Parser[Any]): Parser[Unit] =
pa match {
case s if Impl.alwaysSucceeds(s) => unit
case v @ Impl.Void(_) => v
case Impl.StartParser => Impl.StartParser
case Impl.EndParser => Impl.EndParser
Expand Down Expand Up @@ -1052,16 +1060,24 @@ object Parser extends ParserInstances {
* current parser fails.
*/
def not(pa: Parser[Any]): Parser[Unit] =
Impl.Not(void(pa))
pa match {
case Impl.Fail() | Impl.FailWith(_) => unit
case _ => Impl.Not(void(pa))
}

/** a parser that consumes nothing when
* it succeeds, basically rewind on success
*/
def peek(pa: Parser[Any]): Parser[Unit] =
// TODO: we can adjust Rep/Rep1 to do minimal
// work since we rewind after we are sure there is
// a match
Impl.Peek(void(pa))
pa match {
case peek @ Impl.Peek(_) => peek
case s if Impl.alwaysSucceeds(s) => unit
case notPeek =>
// TODO: we can adjust Rep/Rep1 to do minimal
// work since we rewind after we are sure there is
// a match
Impl.Peek(void(notPeek))
}

/** return the current position in the string
* we are parsing. This lets you record position information
Expand Down Expand Up @@ -1168,6 +1184,18 @@ object Parser extends ParserInstances {
case _ => false
}

// does this parser always succeed?
// note: a parser1 does not always succeed
// and by construction, a oneOf never always succeeds
final def alwaysSucceeds(p: Parser[Any]): Boolean =
p match {
case Index | Pure(_) => true
case Map(p, _) => alwaysSucceeds(p)
case SoftProd(a, b) => alwaysSucceeds(a) && alwaysSucceeds(b)
case Prod(a, b) => alwaysSucceeds(a) && alwaysSucceeds(b)
case _ => false
}

/** This removes any trailing map functions which
* can cause wasted allocations if we are later going
* to void or return strings. This stops
Expand All @@ -1177,6 +1205,8 @@ object Parser extends ParserInstances {
def unmap(pa: Parser[Any]): Parser[Any] =
pa match {
case p1: Parser1[Any] => unmap1(p1)
case Pure(_) | Index => Parser.unit
case s if alwaysSucceeds(s) => Parser.unit
case Map(p, _) =>
// we discard any allocations done by fn
unmap(p)
Expand Down Expand Up @@ -1212,8 +1242,7 @@ object Parser extends ParserInstances {
case Defer(fn) =>
Defer(() => unmap(compute(fn)))
case Rep(p, _) => Rep(unmap1(p), Accumulator.unitAccumulator)
case Pure(_) => Parser.unit
case Index | StartParser | EndParser | TailRecM(_, _) | FlatMap(_, _) =>
case StartParser | EndParser | TailRecM(_, _) | FlatMap(_, _) =>
// we can't transform this significantly
pa
}
Expand Down
82 changes: 82 additions & 0 deletions core/shared/src/test/scala/cats/parse/ParserTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1387,4 +1387,86 @@ class ParserTest extends munit.ScalaCheckSuite {
assertEquals(leftRes, rightRes)
}
}

property("a.peek == a.peek.peek") {
forAll(ParserGen.gen, Arbitrary.arbitrary[String]) { (a, str) =>
val pa = a.fa

val left = pa.peek
val right = pa.peek.peek

val leftRes = left.parse(str)
val rightRes = right.parse(str)
assertEquals(leftRes, rightRes)
}
}

property("a.peek == a.peek *> a.peek") {
forAll(ParserGen.gen, Arbitrary.arbitrary[String]) { (a, str) =>
val pa = a.fa.peek

val left = pa
val right = pa *> pa

val leftRes = left.parse(str)
val rightRes = right.parse(str)
assertEquals(leftRes, rightRes)
}
}

property("!a == (!a) *> (!a)") {
forAll(ParserGen.gen, Arbitrary.arbitrary[String]) { (a, str) =>
val pa = !a.fa

val left = pa
val right = pa *> pa

val leftRes = left.parse(str)
val rightRes = right.parse(str)
assertEquals(leftRes, rightRes)
}
}

property("!(!a) == a.peek") {
forAll(ParserGen.gen, Arbitrary.arbitrary[String]) { (a, str) =>
val pa = a.fa

val left = (!(!pa))
val right = pa.peek

val leftRes = left.parse(str).toOption
val rightRes = right.parse(str).toOption
assertEquals(leftRes, rightRes)
}
}

property("!(!(!a)) == !a") {
forAll(ParserGen.gen, Arbitrary.arbitrary[String]) { (a, str) =>
val pa = a.fa

val left = !(!(!pa))
val right = !pa

val leftRes = left.parse(str).toOption
val rightRes = right.parse(str).toOption
assertEquals(leftRes, rightRes)
}
}

property("!anyChar == end") {
forAll { (str: String) =>
val left = !Parser.anyChar
val right = Parser.end

val leftRes = left.parse(str).toOption
val rightRes = right.parse(str).toOption
assertEquals(leftRes, rightRes)
}
}

property("anyChar.repAs[String] parses the whole string") {
forAll { (str: String) =>
assertEquals(Parser.anyChar.repAs[String].parse(str), Right(("", str)))
}
}
}

0 comments on commit 0bc2f47

Please sign in to comment.