Skip to content

Commit

Permalink
Merge pull request #22 from j-mie6/refactor
Browse files Browse the repository at this point in the history
Refactor
  • Loading branch information
j-mie6 authored Jan 5, 2021
2 parents 3934d87 + 6d79f01 commit 1c64f5f
Show file tree
Hide file tree
Showing 23 changed files with 896 additions and 967 deletions.
26 changes: 25 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ on: [push, pull_request]
env:
CI: true
CI_SNAPSHOT_RELEASE: +publishSigned
SCALA_VERSION: 2.12.12
SCALA_VERSION: 2.13.4
jobs:
validate:
name: Scala ${{ matrix.scala }}, Java ${{ matrix.java }}
Expand Down Expand Up @@ -34,3 +34,27 @@ jobs:
run: sbt ++$SCALA_VERSION test
- name: Scaladoc
run: sbt ++$SCALA_VERSION doc
coverage:
needs: [validate]
name: Test Coverage
runs-on: ubuntu-20.04
steps:
- uses: actions/[email protected]
- uses: olafurpg/setup-scala@v10
with:
java-version: [email protected]
- uses: actions/cache@v1
with:
path: ~/.cache/coursier
key: sbt-coursier-cache
- uses: actions/cache@v1
with:
path: ~/.sbt
key: sbt-${{ hashFiles('**/build.sbt') }}
- run: sbt clean coverage test
- uses: paambaati/[email protected]
env:
CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
with:
coverageCommand: sbt coverageReport
coverageLocations: ${{github.workspace}}/target/scala-2.13/coverage-report/cobertura.xml:cobertura
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ target/
.cache-main
.classpath
.project
.jvmopts
*.class
*.log
parsley*.jar
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ its competitive performance, but for best effect a parser should be compiled onc
To make recursive parsers work in this AST format, you must ensure that recursion is done by knot-tying: you should define all
recursive parsers with `val` and introduce `lazy val` where necessary for the compiler to accept the definition.

## Bug Reports [![Percentage of issues still open](https://isitmaintained.com/badge/open/j-mie6/parsley.svg)](https://isitmaintained.com/project/j-mie6/parsley "Percentage of issues still open") [![Maintainability](https://api.codeclimate.com/v1/badges/337556ceb02f4d6dc599/maintainability)](https://codeclimate.com/github/j-mie6/parsley/maintainability)
## Bug Reports [![Percentage of issues still open](https://isitmaintained.com/badge/open/j-mie6/Parsley.svg)](https://isitmaintained.com/project/j-mie6/Parsley "Percentage of issues still open") [![Maintainability](https://img.shields.io/codeclimate/maintainability/j-mie6/Parsley)](https://codeclimate.com/github/j-mie6/Parsley) [![Test Coverage](https://img.shields.io/codeclimate/coverage-letter/j-mie6/Parsley)](https://codeclimate.com/github/j-mie6/Parsley)

If you encounter a bug when using Parsley, try and minimise the example of the parser (and the input) that triggers the bug.
If possible, make a self contained example: this will help me to identify the issue without too much issue.

Expand Down
2 changes: 2 additions & 0 deletions src/main/scala/parsley/Implicits.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import scala.language.implicitConversions
*/
object Implicits
{
// $COVERAGE-OFF$
@inline implicit def voidImplicitly[P](p: P)(implicit con: P => Parsley[_]): Parsley[Unit] = void(p)
@inline implicit def stringLift(str: String): Parsley[String] = string(str)
@inline implicit def charLift(c: Char): Parsley[Char] = char(c)
// $COVERAGE-ON$
}
67 changes: 40 additions & 27 deletions src/main/scala/parsley/Token.scala
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,17 @@ final case class LanguageDef(commentStart: String,
keywords: Set[String],
operators: Set[String],
caseSensitive: Boolean,
space: Impl)
space: Impl) {
private [parsley] lazy val supportsComments = {
val on = (commentStart.nonEmpty && commentEnd.nonEmpty) || commentLine.nonEmpty
if (on && commentStart.nonEmpty && commentLine.startsWith(commentStart)) {
throw new IllegalArgumentException(
"multi-line comments which are a valid prefix of a single-line comment are not allowed as this causes ambiguity in the parser"
)
}
on
}
}
object LanguageDef
{
val plain = LanguageDef("", "", "", false, NotRequired, NotRequired, NotRequired, NotRequired, Set.empty, Set.empty, true, NotRequired)
Expand Down Expand Up @@ -104,30 +114,33 @@ object BitGen
*/
final class TokenParser(lang: LanguageDef)
{
private def keyOrOp(startImpl: Impl, letterImpl: Impl, parser: Parsley[String], predicate: String => Boolean, name: String,
builder: (TokenSet, TokenSet) => deepembedding.Parsley[String]) = (startImpl, letterImpl) match
{
case (BitSetImpl(start), BitSetImpl(letter)) => lexeme(new Parsley(builder(start, letter)))
case (BitSetImpl(start), Predicate(letter)) => lexeme(new Parsley(builder(start, letter)))
case (Predicate(start), BitSetImpl(letter)) => lexeme(new Parsley(builder(start, letter)))
case (Predicate(start), Predicate(letter)) => lexeme(new Parsley(builder(start, letter)))
case _ => lexeme(attempt(parser.guard(predicate, s"unexpected $name " + _)))
private def keyOrOp(startImpl: Impl, letterImpl: Impl, parser: Parsley[String], predicate: String => Boolean,
combinatorName: String, name: String, illegalName: String) = {
val builder = (start: TokenSet, letter: TokenSet) =>
new Parsley(new deepembedding.NonSpecific(combinatorName, name, illegalName, start, letter, !predicate(_)))
lexeme((startImpl, letterImpl) match
{
case (BitSetImpl(start), BitSetImpl(letter)) => builder(start, letter)
case (BitSetImpl(start), Predicate(letter)) => builder(start, letter)
case (Predicate(start), BitSetImpl(letter)) => builder(start, letter)
case (Predicate(start), Predicate(letter)) => builder(start, letter)
case _ => attempt((parser ? name).guard(predicate, s"unexpected $illegalName " + _))
})
}

// Identifiers & Reserved words
/**This lexeme parser parses a legal identifier. Returns the identifier string. This parser will
* fail on identifiers that are reserved words (i.e. keywords). Legal identifier characters and
* keywords are defined in the `LanguageDef` provided to the token parser. An identifier is treated
* as a single token using `attempt`.*/
lazy val identifier: Parsley[String] = keyOrOp(lang.identStart, lang.identLetter, ident, !isReservedName(_), "keyword",
(start, letter) => new deepembedding.Identifier(start, letter, theReservedNames))
lazy val identifier: Parsley[String] = keyOrOp(lang.identStart, lang.identLetter, ident, !isReservedName(_), "identifier", "identifier", "keyword")

/**The lexeme parser `keyword(name)` parses the symbol `name`, but it also checks that the `name`
* is not a prefix of a valid identifier. A `keyword` is treated as a single token using `attempt`.*/
def keyword(name: String): Parsley[Unit] = lang.identLetter match
{
case BitSetImpl(letter) => lexeme(new Parsley(new deepembedding.Keyword(name, letter, lang.caseSensitive)))
case Predicate(letter) => lexeme(new Parsley(new deepembedding.Keyword(name, letter, lang.caseSensitive)))
case BitSetImpl(letter) => lexeme(new Parsley(new deepembedding.Specific("keyword", name, letter, lang.caseSensitive)))
case Predicate(letter) => lexeme(new Parsley(new deepembedding.Specific("keyword", name, letter, lang.caseSensitive)))
case _ => lexeme(attempt(caseString(name) *> notFollowedBy(identLetter) ? ("end of " + name)))
}

Expand All @@ -141,21 +154,19 @@ final class TokenParser(lang: LanguageDef)
private val theReservedNames = if (lang.caseSensitive) lang.keywords else lang.keywords.map(_.toLowerCase)
private lazy val identStart = toParser(lang.identStart)
private lazy val identLetter = toParser(lang.identLetter)
private lazy val ident = lift2((c: Char, cs: List[Char]) => (c::cs).mkString, identStart, many(identLetter)) ? "identifier"
private lazy val ident = lift2((c: Char, cs: List[Char]) => (c::cs).mkString, identStart, many(identLetter))

// Operators & Reserved ops
/**This lexeme parser parses a legal operator. Returns the name of the operator. This parser
* will fail on any operators that are reserved operators. Legal operator characters and
* reserved operators are defined in the `LanguageDef` provided to the token parser. A
* `userOp` is treated as a single token using `attempt`.*/
lazy val userOp: Parsley[String] = keyOrOp(lang.opStart, lang.opLetter, oper, !isReservedOp(_), "reserved operator",
(start, letter) => new deepembedding.UserOp(start, letter, lang.operators))
lazy val userOp: Parsley[String] = keyOrOp(lang.opStart, lang.opLetter, oper, !isReservedOp(_), "userOp", "operator", "reserved operator")

/**This non-lexeme parser parses a reserved operator. Returns the name of the operator.
* Legal operator characters and reserved operators are defined in the `LanguageDef`
* provided to the token parser. A `reservedOp_` is treated as a single token using `attempt`.*/
lazy val reservedOp_ : Parsley[String] = keyOrOp(lang.opStart, lang.opLetter, oper, isReservedOp(_), "non-reserved operator",
(start, letter) => new deepembedding.ReservedOp(start, letter, lang.operators))
lazy val reservedOp_ : Parsley[String] = keyOrOp(lang.opStart, lang.opLetter, oper, isReservedOp(_), "reservedOp", "operator", "non-reserved operator")

/**This lexeme parser parses a reserved operator. Returns the name of the operator. Legal
* operator characters and reserved operators are defined in the `LanguageDef` provided
Expand All @@ -172,8 +183,8 @@ final class TokenParser(lang: LanguageDef)
* `attempt`.*/
def operator_(name: String): Parsley[Unit] = lang.opLetter match
{
case BitSetImpl(letter) => new Parsley(new deepembedding.Operator(name, letter))
case Predicate(letter) => new Parsley(new deepembedding.Operator(name, letter))
case BitSetImpl(letter) => new Parsley(new deepembedding.Specific("operator", name, letter, true))
case Predicate(letter) => new Parsley(new deepembedding.Specific("operator", name, letter, true))
case _ => attempt(name *> notFollowedBy(opLetter) ? ("end of " + name))
}

Expand All @@ -190,7 +201,7 @@ final class TokenParser(lang: LanguageDef)
private def isReservedOp(op: String): Boolean = lang.operators.contains(op)
private lazy val opStart = toParser(lang.opStart)
private lazy val opLetter = toParser(lang.opLetter)
private lazy val oper = lift2((c: Char, cs: List[Char]) => (c::cs).mkString, opStart, many(opLetter)) ? "operator"
private lazy val oper = lift2((c: Char, cs: List[Char]) => (c::cs).mkString, opStart, many(opLetter))

// Chars & Strings
/**This lexeme parser parses a single literal character. Returns the literal character value.
Expand All @@ -213,6 +224,7 @@ final class TokenParser(lang: LanguageDef)
{
case BitSetImpl(ws) => new Parsley(new deepembedding.StringLiteral(ws))
case Predicate(ws) => new Parsley(new deepembedding.StringLiteral(ws))
case NotRequired => new Parsley(new deepembedding.StringLiteral(_ => false))
case _ => between('"' ? "string", '"' ? "end of string", many(stringChar)) <#> (_.flatten.mkString)
}

Expand Down Expand Up @@ -301,10 +313,7 @@ final class TokenParser(lang: LanguageDef)
* or "0O". Returns the value of the number.*/
lazy val octal: Parsley[Int] = lexeme('0' *> octal_)

private def number(base: Int, baseDigit: Parsley[Char]): Parsley[Int] =
{
for (digits <- some(baseDigit)) yield digits.foldLeft(0)((x, d) => base*x + d.asDigit)
}
private def number(base: Int, baseDigit: Parsley[Char]): Parsley[Int] = baseDigit.foldLeft(0)((x, d) => base*x + d.asDigit)

// White space & symbols
/**Lexeme parser `symbol(s)` parses `string(s)` and skips trailing white space.*/
Expand Down Expand Up @@ -340,14 +349,18 @@ final class TokenParser(lang: LanguageDef)
new Parsley(new deepembedding.WhiteSpace(ws, lang.commentStart, lang.commentEnd, lang.commentLine, lang.nestedComments))
case Predicate(ws) =>
new Parsley(new deepembedding.WhiteSpace(ws, lang.commentStart, lang.commentEnd, lang.commentLine, lang.nestedComments))
case Parser(space_) =>
case Parser(space_) if lang.supportsComments =>
skipMany(new Parsley(new deepembedding.Comment(lang.commentStart, lang.commentEnd, lang.commentLine, lang.nestedComments)) <\> space_)
case Parser(space_) => skipMany(space_)
case NotRequired => skipComments
}

/**Parses any comments and skips them, this includes both line comments and block comments.*/
lazy val skipComments: Parsley[Unit] = {
new Parsley(new deepembedding.SkipComments(lang.commentStart, lang.commentEnd, lang.commentLine, lang.nestedComments))
if (!lang.supportsComments) unit
else {
new Parsley(new deepembedding.SkipComments(lang.commentStart, lang.commentEnd, lang.commentLine, lang.nestedComments))
}
}

private def enclosing[A](p: =>Parsley[A], open: Char, close: Char, singular: String, plural: String) =
Expand Down
71 changes: 71 additions & 0 deletions src/main/scala/parsley/internal/Radix.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package parsley.internal

import Radix.Entry
import scala.collection.mutable
import scala.language.implicitConversions

class Radix[A] {
private var x = Option.empty[A]
private val m = mutable.Map.empty[Char, Entry[A]]

def get(key: String): Option[A] = {
if (key.isEmpty) x
else for
{
e <- m.get(key.head)
if key.startsWith(e.prefix)
v <- e.radix.get(key.drop(e.prefix.length))
} yield v
}

def isEmpty: Boolean = x.isEmpty && m.isEmpty
def nonEmpty: Boolean = !isEmpty

def suffixes(c: Char): Radix[A] = m.get(c) match {
case Some(e) =>
// We have to form a new root
if (e.prefix.length > 1) Radix(new Entry(e.prefix.tail, e.radix))
else e.radix
case None => Radix.empty
}

def contains(key: String): Boolean = get(key).nonEmpty
def apply(key: String): A = get(key).getOrElse(throw new NoSuchElementException(key))

def update(key: String, value: A): Unit =
if (key.isEmpty) x = Some(value)
else {
val e = m.getOrElseUpdate(key.head, new Entry(key, Radix.empty[A]))
if (key.startsWith(e.prefix)) e.radix(key.drop(e.prefix.length)) = value
else {
// Need to split the tree: find their common prefix first
val common = key.view.zip(e.prefix).takeWhile(Function.tupled(_ == _)).map(_._1).mkString
e.dropInPlace(common.length)
val radix = Radix(e)
// Continue inserting the key
radix(key.drop(common.length)) = value
// Insert our new entry
m(common.head) = new Entry(common, radix)
}
}
}

object Radix {
def empty[A]: Radix[A] = new Radix

private def apply[A](e: Entry[A]): Radix[A] = {
val radix = empty[A]
radix.m(e.prefix.head) = e
radix
}

def apply[A](xs: Iterable[String]): Radix[Unit] = {
val r = Radix.empty[Unit]
for (x <- xs) r(x) = ()
r
}

private class Entry[A](var prefix: String, val radix: Radix[A]) {
def dropInPlace(n: Int): Unit = prefix = prefix.drop(n)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,7 @@ private [parsley] final class <|>[A, B](_p: =>Parsley[A], _q: =>Parsley[B]) exte
val (c, expected) = lead match {
case ct@CharTok(d) => (d, ct.expected)
case st@StringTok(s) => (s.head, if (st.expected == null) "\"" + s + "\"" else st.expected)
case kw@Keyword(k) => (k.head, if (kw.expected == null) k else kw.expected)
case op@Operator(o) => (o.head, if (op.expected == null) o else op.expected)
case st@Specific(s) => (s.head, if (st.expected == null) s else st.expected)
case op@MaxOp(o) => (o.head, if (op.expected == null) o else op.expected)
case sl: StringLiteral => ('"', if (sl.expected == null) "string" else sl.expected)
case rs: RawStringLiteral => ('"', if (rs.expected == null) "string" else rs.expected)
Expand All @@ -155,7 +154,7 @@ private [parsley] final class <|>[A, B](_p: =>Parsley[A], _q: =>Parsley[B]) exte
}
@tailrec private def tablable(p: Parsley[_]): Option[Parsley[_]] = p match {
// CODO: Numeric parsers by leading digit (This one would require changing the foldTablified function a bit)
case t@(_: CharTok | _: StringTok | _: Keyword | _: StringLiteral | _: RawStringLiteral | _: Operator | _: MaxOp) => Some(t)
case t@(_: CharTok | _: StringTok | _: Specific | _: StringLiteral | _: RawStringLiteral | _: MaxOp) => Some(t)
case Attempt(t) => tablable(t)
case (_: Pure[_]) <*> t => tablable(t)
case Lift2(_, t, _) => tablable(t)
Expand Down
20 changes: 5 additions & 15 deletions src/main/scala/parsley/internal/deepembedding/Cont.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ private [deepembedding] abstract class ContOps[Cont[_, +_]]
def unwrap[R](wrapped: Cont[R, R]): R
def map[R, A, B](c: =>Cont[R, A], f: A => B): Cont[R, B]
def flatMap[R, A, B](c: =>Cont[R, A], f: A => Cont[R, B]): Cont[R, B]
// $COVERAGE-OFF$
def >>[R, A, B](c: =>Cont[R, A], k: =>Cont[R, B]): Cont[R, B] = flatMap[R, A, B](c, _ => k)
def |>[R, A, B](c: =>Cont[R, A], x: =>B): Cont[R, B] = map[R, A, B](c, _ => x)
// $COVERAGE-ON$
}
private [deepembedding] object ContOps
{
Expand All @@ -36,9 +38,10 @@ private [deepembedding] object ContOps
def result[R, A, Cont[_, +_]](x: A)(implicit canWrap: ContOps[Cont]): Cont[R, A] = canWrap.wrap(x)
def perform[R, Cont[_, +_]](wrapped: Cont[R, R])(implicit canUnwrap: ContOps[Cont]): R = canUnwrap.unwrap(wrapped)
type GenOps = ContOps[({type C[_, +_]})#C]
def safeCall[A](task: GenOps => A): A =
def safeCall[A](task: GenOps => A): A = {
try task(Id.ops.asInstanceOf[GenOps])
catch { case _: StackOverflowError => task(Cont.ops.asInstanceOf[GenOps]) }
}
}

private [deepembedding] class Cont[R, +A](val cont: (A => Bounce[R]) => Bounce[R]) extends AnyVal
Expand All @@ -60,15 +63,6 @@ private [deepembedding] object Cont
{
new Cont(k => new Thunk(() => mx.cont(_ => my.cont(k))))
}
override def |>[R, A, B](mx: => Cont[R, A], y: => B): Cont[R, B] =
{
new Cont(k => new Thunk(() => mx.cont(_ => k(y))))
}
}

def callCC[R, A, B](f: (A => Cont[R, B]) => Cont[R, A]): Cont[R, A] =
{
new Cont[R, A](k => f(x => new Cont[R, B](_ => k(x))).cont(k))
}
}

Expand All @@ -82,10 +76,6 @@ private [deepembedding] object Id
override def map[R, A, B](c: =>Id[R, A], f: A => B): Id[R, B] = new Id(f(c.x))
override def flatMap[R, A, B](c: =>Id[R, A], f: A => Id[R, B]): Id[R, B] = f(c.x)
override def >>[R, A, B](c: => Id[R, A], k: => Id[R, B]): Id[R, B] = {c; k}
override def |>[R, A, B](c: => Id[R, A], x: => B): Id[R, B] =
{
c.x
new Id(x)
}
override def |>[R, A, B](c: => Id[R, A], x: => B): Id[R, B] = {c; new Id(x)}
}
}
Loading

0 comments on commit 1c64f5f

Please sign in to comment.