Skip to content

Commit

Permalink
Defer codePointWhere and charWhere description
Browse files Browse the repository at this point in the history
Improving performance when those descriptions are not needed
  • Loading branch information
rayrobdod committed Nov 26, 2024
1 parent d3db69f commit b32ca70
Show file tree
Hide file tree
Showing 11 changed files with 112 additions and 80 deletions.
14 changes: 0 additions & 14 deletions Base/src/main/scala-2/Expecting.scala

This file was deleted.

6 changes: 2 additions & 4 deletions Base/src/main/scala-2/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,8 @@ package object stringContextParserCombinator {
case ExpectingSet.Empty() => {
c.abort(c.enclosingPosition, "Parsing failed")
}
case ExpectingSet.NonEmpty(position, descriptions) => {
// `sorted` to make result deterministic
val descriptions2 = descriptions.toList.sortBy(_.toString).mkString("Expected ", " or ", "")
c.abort(position, descriptions2)
case set @ ExpectingSet.NonEmpty(position, _) => {
c.abort(position, set.renderDescriptions)
}
}
}
Expand Down
17 changes: 0 additions & 17 deletions Base/src/main/scala-3/Expecting.scala

This file was deleted.

6 changes: 2 additions & 4 deletions Base/src/main/scala-3/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,8 @@ package object stringContextParserCombinator {
case ExpectingSet.Empty() => {
scala.quoted.quotes.reflect.report.errorAndAbort("Parsing failed")
}
case ExpectingSet.NonEmpty(position, descriptions) => {
// `sorted` to make result deterministic
val descriptions2 = descriptions.toList.sortBy(_.toString).mkString("Expected ", " or ", "")
q.reflect.report.errorAndAbort(descriptions2, position + 1)
case set @ ExpectingSet.NonEmpty(position, _) => {
q.reflect.report.errorAndAbort(set.renderDescriptions, position + 1)
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions Base/src/main/scala/Expecting.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package name.rayrobdod.stringContextParserCombinator

private[stringContextParserCombinator]
final case class Expecting[Pos](
val description:ExpectingDescription,
val position:Pos,
) {
def where(condition:ExpectingDescription) =
new Expecting(description.where(condition), position)
}
42 changes: 42 additions & 0 deletions Base/src/main/scala/ExpectingDescription.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package name.rayrobdod.stringContextParserCombinator

/** Represents a textual description of under what conditions a parser would return success */
private[stringContextParserCombinator]
sealed abstract class ExpectingDescription {
private[stringContextParserCombinator]
def where(condition:ExpectingDescription):ExpectingDescription
private[stringContextParserCombinator]
def render:String
}

private[stringContextParserCombinator]
object ExpectingDescription {
def apply(backing: String): ExpectingDescription = {
new ExpectingDescription {
override def where(condition:ExpectingDescription):ExpectingDescription = {
ExpectingDescription(s"${this.render} where ${condition.render}")
}
override def render: String = {
backing
}
override def toString: String = {
s"ExpectingDescription.eager($backing)"
}
}
}

def delayed(backingFn: => String): ExpectingDescription = {
new ExpectingDescription {
private lazy val backingValue = backingFn
override def where(condition:ExpectingDescription):ExpectingDescription = {
ExpectingDescription.delayed(s"${this.render} where ${condition.render}")
}
override def render: String = {
backingValue
}
override def toString: String = {
s"ExpectingDescription.delayed(<fn>)"
}
}
}
}
19 changes: 16 additions & 3 deletions Base/src/main/scala/ExpectingSet.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ sealed trait ExpectingSet[Pos] {

private[stringContextParserCombinator]
object ExpectingSet {
final case class NonEmpty[Pos : Ordering](val maxPosition:Pos, val descriptionsAtMaxPosition:Set[ExpectingDescription]) extends ExpectingSet[Pos] {
final case class NonEmpty[Pos : Ordering](
val maxPosition:Pos,
val descriptionsAtMaxPosition:Seq[ExpectingDescription]
) extends ExpectingSet[Pos] {
override def ++(other: ExpectingSet[Pos]): ExpectingSet[Pos] = {
import Ordering.Implicits.infixOrderingOps
other match {
Expand All @@ -22,19 +25,29 @@ object ExpectingSet {
override def mapDescriptions(fn: ExpectingDescription => ExpectingDescription):ExpectingSet[Pos] = {
new NonEmpty[Pos](maxPosition, descriptionsAtMaxPosition.map(fn))
}

def renderDescriptions: String =
// `sorted` and `distinct` to make result deterministic
descriptionsAtMaxPosition
.view
.map(_.render)
.distinct
.toList
.sorted
.mkString("Expected ", " or ", "")
}

final case class Empty[Pos]() extends ExpectingSet[Pos] {
override def ++(other: ExpectingSet[Pos]):ExpectingSet[Pos] = other
override def mapDescriptions(fn: ExpectingDescription => ExpectingDescription):ExpectingSet[Pos] = this
}

def apply[Pos : Ordering](a:Expecting[Pos]):ExpectingSet[Pos] = new NonEmpty(a.position, Set(a.description))
def apply[Pos : Ordering](a:Expecting[Pos]):ExpectingSet[Pos] = new NonEmpty(a.position, Seq(a.description))

def fromSpecific[Pos : Ordering](as:Iterable[Expecting[Pos]]):ExpectingSet[Pos] = {
if (as.nonEmpty) {
val maxPosition = as.map(_.position).reduce({(a, b) => if (Ordering[Pos].gt(a, b)) a else b})
new NonEmpty(maxPosition, as.collect({case ex if ex.position == maxPosition => ex.description}).toSet)
new NonEmpty(maxPosition, as.collect({case ex if ex.position == maxPosition => ex.description}).toSeq)
} else {
new Empty
}
Expand Down
5 changes: 2 additions & 3 deletions Base/src/main/scala/Extractor.scala
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,8 @@ final class Extractor[Expr[_], Type[_], -A] private[stringContextParserCombinato
case f:Failure[Int] => {
val msg = f.expecting match {
case ExpectingSet.Empty() => "Parsing Failed"
case ExpectingSet.NonEmpty(position, descriptions) => {
// `sorted` to make result deterministic
val exp = descriptions.toList.sortBy(_.toString).mkString("Expected ", " or ", "")
case set @ ExpectingSet.NonEmpty(position, _) => {
val exp = set.renderDescriptions
val instr = sc.parts.mkString(argString)
val pointer = (" " * position) + "^"

Expand Down
5 changes: 2 additions & 3 deletions Base/src/main/scala/Interpolator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,8 @@ final class Interpolator[-Expr, +A] private[stringContextParserCombinator] (
case f:Failure[Int] => {
val msg = f.expecting match {
case ExpectingSet.Empty() => "Parsing Failed"
case ExpectingSet.NonEmpty(position, descriptions) => {
// `sorted` to make result deterministic
val exp = descriptions.toList.sortBy(_.toString).mkString("Expected ", " or ", "")
case set @ ExpectingSet.NonEmpty(position, _) => {
val exp = set.renderDescriptions
val instr = sc.parts.mkString(argString)
val pointer = (" " * position) + "^"

Expand Down
65 changes: 33 additions & 32 deletions Base/src/main/scala/internal/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -51,47 +51,48 @@ package object internal {
* Returns a string that describes which codepoints match the predicate
*/
private def describeCodepointPredicate(predicate:Int => Boolean, domainMax: Int):ExpectingDescription = {
val builder = new StringBuilder()
var inMatchingBlock:Boolean = false
var firstCharOfBlock:Int = 0

(0 to domainMax).foreach({c =>
if (predicate(c)) {
if (inMatchingBlock) {
// continue
} else {
inMatchingBlock = true
firstCharOfBlock = c
}
} else {
if (inMatchingBlock) {
builder.++=("'")
builder.++=(escape(firstCharOfBlock))
if (firstCharOfBlock != c - 1) {
builder.++=("'<=c<='")
builder.++=(escape((c - 1)))
ExpectingDescription.delayed({
val builder = new StringBuilder()
var inMatchingBlock:Boolean = false
var firstCharOfBlock:Int = 0

(0 to domainMax).foreach({c =>
if (predicate(c)) {
if (inMatchingBlock) {
// continue
} else {
inMatchingBlock = true
firstCharOfBlock = c
}
builder.++=("' or ")
inMatchingBlock = false
} else {
// continue
if (inMatchingBlock) {
builder.++=("'")
builder.++=(escape(firstCharOfBlock))
if (firstCharOfBlock != c - 1) {
builder.++=("'<=c<='")
builder.++=(escape((c - 1)))
}
builder.++=("' or ")
inMatchingBlock = false
} else {
// continue
}
}
})
if (inMatchingBlock) {
builder.++=("'")
builder.++=(escape(firstCharOfBlock))
builder.++=("'<=c<='")
builder.++=(escape(domainMax))
builder.++=("' or ")
}
})
if (inMatchingBlock) {
builder.++=("'")
builder.++=(escape(firstCharOfBlock))
builder.++=("'<=c<='")
builder.++=(escape(domainMax))
builder.++=("' or ")
}
ExpectingDescription(

if (builder.length > 4) {
builder.substring(0, builder.length - 4)
} else {
"nothing"
}
)
})
}

/* * * Leaf parsers * * */
Expand Down
3 changes: 3 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

## [Unreleased-Major]
* Improve performance of `codePointWhere` and `charWhere` if default error message is not needed

## [Unreleased]
* Add symbolic operators to Parser, Extractor and Interpolator
* `<~>` and `<|>` as aliases for `andThen` and `orElse`, respectively
Expand Down

0 comments on commit b32ca70

Please sign in to comment.