diff --git a/README.md b/README.md index d50eb86..78042d9 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ This repository contains the following libraries: - [`parsers-common`](modules/parsers-common): Common utilities for parser combinator. + - [`parsers-contparse`](modules/parsers-contparse): A parser combinator library with continuation passing style. - [`parsers-funcparse`](modules/parsers-funcparse): A basic functional parser combinator library. - [`parsers-inlineparse`](modules/parsers-inlineparse): A faster parser combinator library with mutable state & inline expansion. - [`parsers-stackparse`](modules/parsers-stackparse): A stackless parser combinator library. diff --git a/build.sbt b/build.sbt index c962357..f73979f 100644 --- a/build.sbt +++ b/build.sbt @@ -22,7 +22,7 @@ ThisBuild / scalafixDependencies += "com.github.vovapolu" %% "scaluzzi" % "0.1.1 lazy val root = project .in(file(".")) .settings(publish / skip := true) - .aggregate(bench, common, funcparse, inlineparse, stackparse) + .aggregate(bench, common, contparse, funcparse, inlineparse, stackparse) lazy val bench = project .in(file("modules/bench")) @@ -45,7 +45,7 @@ lazy val bench = project libraryDependencies += "io.monix" %% "minitest" % "2.8.2" % Test, testFrameworks += new TestFramework("minitest.runner.Framework") ) - .dependsOn(funcparse, inlineparse, stackparse) + .dependsOn(contparse, funcparse, inlineparse, stackparse) .enablePlugins(JmhPlugin) def moduleSettings(moduleName: String) = @@ -66,6 +66,15 @@ lazy val common = project .in(file("modules/parsers-common")) .settings(moduleSettings("common")) +lazy val contparse = project + .in(file("modules/parsers-contparse")) + .settings( + moduleSettings("contparse"), + // Dependencies: + libraryDependencies += "com.lihaoyi" %% "sourcecode" % "0.2.1" + ) + .dependsOn(common) + lazy val funcparse = project .in(file("modules/parsers-funcparse")) .settings( diff --git a/modules/bench/src/main/scala/codes/quine/labo/parsers/bench/Bench.scala b/modules/bench/src/main/scala/codes/quine/labo/parsers/bench/Bench.scala index 40f908a..0999ef2 100644 --- a/modules/bench/src/main/scala/codes/quine/labo/parsers/bench/Bench.scala +++ b/modules/bench/src/main/scala/codes/quine/labo/parsers/bench/Bench.scala @@ -8,6 +8,10 @@ class Bench { def measureAtto(): atto.ParseResult[JSON] = AttoJSONParser.parse(Bench.source) + @Benchmark + def measureContparse(): contparse.Parsed[JSON] = + ContparseJSONParser.parse(Bench.source) + @Benchmark def measureFastparse(): fastparse.Parsed[JSON] = FastparseJSONParser.parse(Bench.source) diff --git a/modules/bench/src/main/scala/codes/quine/labo/parsers/bench/ContparseJSONParser.scala b/modules/bench/src/main/scala/codes/quine/labo/parsers/bench/ContparseJSONParser.scala new file mode 100644 index 0000000..74a5ec2 --- /dev/null +++ b/modules/bench/src/main/scala/codes/quine/labo/parsers/bench/ContparseJSONParser.scala @@ -0,0 +1,64 @@ +package codes.quine.labo.parsers +package bench + +import scala.annotation.switch + +import contparse._ +import JSON._ + +object ContparseJSONParser { + val space: P[Unit] = CharsWhileIn(" \r\n", 0) + + val json: P[JSON] = + P((number | string | `null` | `true` | `false` | array | `object`) ~ space) + + val entry: P[JSON] = space ~ json ~ End + + val digits: P[Unit] = CharsWhileIn("0123456789") + val exponent: P[Unit] = CharIn("eE") ~ CharIn("+-").? ~ digits + val fractional: P[Unit] = "." ~ digits + val integral: P[Unit] = "0" | CharIn("123456789") ~ digits.? + + val number: P[JSON] = + (CharIn("+-").? ~ integral ~ fractional.? ~ exponent.?).!.map(s => JSONNumber(s.toDouble)).named("") + + val `null`: P[JSON] = "null" ~ Pass(JSONNull) + val `true`: P[JSON] = "true" ~ Pass(JSONBoolean(true)) + val `false`: P[JSON] = "false" ~ Pass(JSONBoolean(false)) + + val hexDigit: P[Unit] = CharIn("0123456789abcdefABCDEF") + val unicodeEscape: P[Char] = "u" ~ hexDigit.count(4).!.map(s => Integer.parseInt(s, 16).toChar) + val simpleEscape: P[Char] = + CharIn("\"\\/bfnrt").!.map(s => + (s.charAt(0): @switch) match { + case 'b' => '\b' + case 'f' => '\f' + case 'n' => '\n' + case 'r' => '\r' + case 't' => '\t' + case c => c + } + ) + val escape: P[Char] = "\\" ~/ (simpleEscape | unicodeEscape) + val stringContent: P[String] = CharsWhile(c => c != '"' && c != '\\').! | escape.map(String.valueOf(_)) + val key: P[String] = ("\"" ~/ stringContent.rep.map(_.mkString) ~ "\"").named("") + val string: P[JSON] = key.map(JSONString(_)) + + val arrayContent: P[Seq[JSON]] = + (json ~ ("," ~ space ~/ json).rep).?.map { + case None => Seq.empty + case Some((x, xs)) => x +: xs + } + val array: P[JSON] = "[" ~ space ~/ arrayContent.map(JSONArray(_)) ~ "]" + + val pair: P[(String, JSON)] = key ~ space ~ ":" ~ space ~ json + val objectContent: P[Seq[(String, JSON)]] = + (pair ~ ("," ~ space ~/ pair).rep).?.map { + case None => Seq.empty + case Some((k, v, xs)) => (k, v) +: xs + } + val `object`: P[JSON] = "{" ~ space ~/ objectContent.map(JSONObject(_)) ~ "}" + + def parse(input: String): Parsed[JSON] = + contparse.parse(input, entry) +} diff --git a/modules/parsers-contparse/README.md b/modules/parsers-contparse/README.md new file mode 100644 index 0000000..53d476f --- /dev/null +++ b/modules/parsers-contparse/README.md @@ -0,0 +1,3 @@ +# parsers-contparse + +> A parser combinator library with continuation passing style. diff --git a/modules/parsers-contparse/src/main/scala/codes/quine/labo/parsers/contparse/Parser.scala b/modules/parsers-contparse/src/main/scala/codes/quine/labo/parsers/contparse/Parser.scala new file mode 100644 index 0000000..b036749 --- /dev/null +++ b/modules/parsers-contparse/src/main/scala/codes/quine/labo/parsers/contparse/Parser.scala @@ -0,0 +1,497 @@ +package codes.quine.labo.parsers +package contparse + +import scala.annotation.tailrec +import scala.util.control.TailCalls.TailRec +import scala.util.control.TailCalls.tailcall + +import common.{Sequencer, Repeater, Optioner, Util} + +sealed trait Parser[+T] { + def run[R]( + p: Parsing, + onSuccess: (T, Parsing, Boolean) => TailRec[R], + onFailure: (Parsing, Boolean) => TailRec[R] + ): TailRec[R] + + def ! : Parser[String] = Parser.Capture(this) + + def ~[U, V](parser2: Parser[U])(implicit seq: Sequencer.Aux[T, U, V]): Parser[V] = + Parser.Sequence(this, parser2, false, seq) + + def ~/[U, V](parser2: Parser[U])(implicit seq: Sequencer.Aux[T, U, V]): Parser[V] = + Parser.Sequence(this, parser2, true, seq) + + def / : Parser[T] = Parser.Cut(this) + + def rep[V](implicit rep: Repeater.Aux[T, V]): Parser[V] = + Parser.Repeat(this, 0, Int.MaxValue, rep) + + def rep[V](min: Int, max: Int = Int.MaxValue)(implicit rep: Repeater.Aux[T, V]): Parser[V] = + Parser.Repeat(this, min, max, rep) + + def count[V](times: Int)(implicit rep: Repeater.Aux[T, V]): Parser[V] = + Parser.Count(this, times, rep) + + def ?[V](implicit opt: Optioner.Aux[T, V]): Parser[V] = + Parser.Optional(this, opt) + + def |[U >: T](parser2: Parser[U]): Parser[U] = Parser.Alternative(this, parser2) + + def map[U](f: T => U): Parser[U] = Parser.Map(this, f) + + def flatMap[U](f: T => Parser[U]): Parser[U] = Parser.FlatMap(this, f) + + def filter(f: T => Boolean): Parser[T] = Parser.Filter(this, f) + + def named(name: String): Parser[T] = Parser.Named(this, name) +} + +object Parser { + final case class Literal(string: String) extends Parser[Unit] { + def run[R]( + p: Parsing, + onSuccess: (Unit, Parsing, Boolean) => TailRec[R], + onFailure: (Parsing, Boolean) => TailRec[R] + ): TailRec[R] = + if (p.input.startsWith(string, p.pos)) onSuccess((), p.advance(string.length), false) + else onFailure(p.unexpected(name), false) + + lazy val name: String = "\"" + Util.escape(string) + "\"" + override def toString: String = name + } + + final case class CharIn(string: String) extends Parser[Unit] { + def run[R]( + p: Parsing, + onSuccess: (Unit, Parsing, Boolean) => TailRec[R], + onFailure: (Parsing, Boolean) => TailRec[R] + ): TailRec[R] = + if (p.pos < p.input.length && string.contains(p.input.charAt(p.pos))) onSuccess((), p.advance(1), false) + else onFailure(p.unexpected(name), false) + + lazy val name: String = "CharIn(\"" + Util.escape(string) + "\")" + override def toString: String = name + } + + final case class CharPred(f: Char => Boolean) extends Parser[Unit] { + def run[R]( + p: Parsing, + onSuccess: (Unit, Parsing, Boolean) => TailRec[R], + onFailure: (Parsing, Boolean) => TailRec[R] + ): TailRec[R] = + if (p.pos < p.input.length && f(p.input.charAt(p.pos))) onSuccess((), p.advance(1), false) + else onFailure(p.unexpected(toString), false) + + override def toString: String = "CharPred(...)" + } + + final case class CharsWhileIn(string: String, min: Int) extends Parser[Unit] { + def run[R]( + p: Parsing, + onSuccess: (Unit, Parsing, Boolean) => TailRec[R], + onFailure: (Parsing, Boolean) => TailRec[R] + ): TailRec[R] = { + var pos = p.pos + while (pos < p.input.length && string.contains(p.input.charAt(pos))) { + pos += 1 + } + if (min <= pos - p.pos) onSuccess((), p.reset(pos), false) + else onFailure(p.unexpected(toString), false) + } + + lazy val name: String = "CharsWhileIn(\"" + Util.escape(string) + "\")" + override def toString: String = name + } + + final case class CharsWhile(f: Char => Boolean, min: Int) extends Parser[Unit] { + def run[R]( + p: Parsing, + onSuccess: (Unit, Parsing, Boolean) => TailRec[R], + onFailure: (Parsing, Boolean) => TailRec[R] + ): TailRec[R] = { + var pos = p.pos + while (pos < p.input.length && f(p.input.charAt(pos))) { + pos += 1 + } + if (min <= pos - p.pos) onSuccess((), p.reset(pos), false) + else onFailure(p.unexpected(toString), false) + } + + override def toString: String = "CharsWhile(...)" + } + case object AnyChar extends Parser[Unit] { + def run[R]( + p: Parsing, + onSuccess: (Unit, Parsing, Boolean) => TailRec[R], + onFailure: (Parsing, Boolean) => TailRec[R] + ): TailRec[R] = + if (p.pos < p.input.length) onSuccess((), p.advance(1), false) + else onFailure(p.unexpected(toString), false) + + override def toString: String = "AnyChar" + } + + case object Start extends Parser[Unit] { + def run[R]( + p: Parsing, + onSuccess: (Unit, Parsing, Boolean) => TailRec[R], + onFailure: (Parsing, Boolean) => TailRec[R] + ): TailRec[R] = + if (p.pos == 0) onSuccess((), p, false) + else onFailure(p.unexpected(toString), false) + + override def toString: String = "Start" + } + + case object End extends Parser[Unit] { + def run[R]( + p: Parsing, + onSuccess: (Unit, Parsing, Boolean) => TailRec[R], + onFailure: (Parsing, Boolean) => TailRec[R] + ): TailRec[R] = + if (p.pos == p.input.length) onSuccess((), p, false) + else onFailure(p.unexpected(toString), false) + + override def toString: String = "End" + } + + final case class Capture(parser: Parser[Any]) extends Parser[String] { + def run[R]( + p0: Parsing, + onSuccess: (String, Parsing, Boolean) => TailRec[R], + onFailure: (Parsing, Boolean) => TailRec[R] + ): TailRec[R] = + tailcall(parser.run(p0, (_, p1, cut) => onSuccess(p0.input.slice(p0.pos, p1.pos), p1, cut), onFailure)) + + override def toString: String = + parser match { + case _: Sequence[_, _, _] | _: Alternative[_] => s"($parser).!" + case _ => s"$parser.!" + } + } + + final case class Cut[T](parser: Parser[T]) extends Parser[T] { + def run[R]( + p0: Parsing, + onSuccess: (T, Parsing, Boolean) => TailRec[R], + onFailure: (Parsing, Boolean) => TailRec[R] + ): TailRec[R] = + tailcall(parser.run(p0, (v, p1, _) => onSuccess(v, p1, true), onFailure)) + + override def toString: String = + parser match { + case _: Sequence[_, _, _] | _: Alternative[_] => s"($parser)./" + case _ => s"$parser./" + } + } + + final case class NoCut[T](parser: Parser[T]) extends Parser[T] { + def run[R]( + p0: Parsing, + onSuccess: (T, Parsing, Boolean) => TailRec[R], + onFailure: (Parsing, Boolean) => TailRec[R] + ): TailRec[R] = + tailcall(parser.run(p0, (v, p1, _) => onSuccess(v, p1, false), (p1, _) => tailcall(onFailure(p1, false)))) + + override def toString: String = s"NoCut($parser)" + } + + final case class Sequence[T, U, V](parser1: Parser[T], parser2: Parser[U], cut: Boolean, seq: Sequencer.Aux[T, U, V]) + extends Parser[V] { + def run[R]( + p0: Parsing, + onSuccess: (V, Parsing, Boolean) => TailRec[R], + onFailure: (Parsing, Boolean) => TailRec[R] + ): TailRec[R] = + tailcall( + parser1.run( + p0, + (x, p1, cut1) => + tailcall( + parser2.run( + p1, + (y, p2, cut2) => onSuccess(seq(x, y), p2, cut1 || cut || cut2), + (p2, cut2) => onFailure(p2, cut1 || cut || cut2) + ) + ), + onFailure + ) + ) + + override def toString: String = { + def paren(p: Parser[Any]): String = + p match { + case _: Sequence[_, _, _] | _: Alternative[_] => "(" + p + ")" + case _ => p.toString + } + @tailrec def loop(p1: Parser[Any], s: String, cut: Boolean): String = + p1 match { + case Sequence(p1, p2, _, _) => loop(p1, s"${paren(p2)} ${if (cut) "~/" else "~"} $s", cut) + case _ => s"${paren(p1)} ${if (cut) "~/" else "~"} $s" + } + loop(parser1, paren(parser2), cut) + } + } + + private def parseRepeat[T, V, R]( + parser: Parser[T], + min: Int, + max: Int, + rep: Repeater.Aux[T, V], + p: Parsing, + onSuccess: (V, Parsing, Boolean) => TailRec[R], + onFailure: (Parsing, Boolean) => TailRec[R] + ): TailRec[R] = { + def loop(p0: Parsing, as: V, cut: Boolean, n: Int): TailRec[R] = + if (n == max) onSuccess(as, p0, cut) + else if (n < min) + tailcall( + parser.run( + p0, + (a, p1, cut1) => loop(p1, rep.append(as, a), cut || cut1, n + 1), + (p1, cut1) => onFailure(p1, cut || cut1) + ) + ) + else + tailcall( + parser.run( + p0, + { + case (_, p1, true) if p0.pos == p1.pos => onFailure(p1.fail("null repeat"), true) + case (_, p1, false) if p0.pos == p1.pos => onSuccess(as, p1, cut) + case (a, p1, cut1) => loop(p1, rep.append(as, a), cut || cut1, n + 1) + }, + { + case (p1, true) => onFailure(p1, true) + case (p1, false) => onSuccess(as, p1.reset(p0.pos), cut) + } + ) + ) + loop(p, rep.empty, false, 0) + } + + final case class Repeat[T, V](parser: Parser[T], min: Int, max: Int, rep: Repeater.Aux[T, V]) extends Parser[V] { + def run[R]( + p: Parsing, + onSuccess: (V, Parsing, Boolean) => TailRec[R], + onFailure: (Parsing, Boolean) => TailRec[R] + ): TailRec[R] = + parseRepeat(parser, min, max, rep, p, onSuccess, onFailure) + + override def toString: String = { + val method = (min, max) match { + case (0, Int.MaxValue) => "rep" + case (m, Int.MaxValue) => s"rep($m)" + case (m, n) => s"rep($m, $n)" + } + parser match { + case _: Sequence[_, _, _] | _: Alternative[_] => s"($parser).$method" + case _ => s"$parser.$method" + } + } + } + + final case class Count[T, V](parser: Parser[T], times: Int, rep: Repeater.Aux[T, V]) extends Parser[V] { + def run[R]( + p: Parsing, + onSuccess: (V, Parsing, Boolean) => TailRec[R], + onFailure: (Parsing, Boolean) => TailRec[R] + ): TailRec[R] = + parseRepeat(parser, times, times, rep, p, onSuccess, onFailure) + + override def toString: String = + parser match { + case _: Sequence[_, _, _] | _: Alternative[_] => s"($parser).count($times)" + case _ => s"$parser.count($times)" + } + } + + final case class Optional[T, V](parser: Parser[T], opt: Optioner.Aux[T, V]) extends Parser[V] { + def run[R]( + p0: Parsing, + onSuccess: (V, Parsing, Boolean) => TailRec[R], + onFailure: (Parsing, Boolean) => TailRec[R] + ): TailRec[R] = + tailcall( + parser.run( + p0, + (v, p1, cut) => onSuccess(opt.some(v), p1, cut), + (p1, cut) => if (cut) onFailure(p1, true) else onSuccess(opt.none, p1.reset(p0.pos), false) + ) + ) + + override def toString: String = + parser match { + case _: Sequence[_, _, _] | _: Alternative[_] => s"($parser).?" + case _ => s"$parser.?" + } + } + + final case class Alternative[T](parser1: Parser[T], parser2: Parser[T]) extends Parser[T] { + def run[R]( + p0: Parsing, + onSuccess: (T, Parsing, Boolean) => TailRec[R], + onFailure: (Parsing, Boolean) => TailRec[R] + ): TailRec[R] = + tailcall( + parser1.run( + p0, + onSuccess, + (p1, cut) => if (cut) onFailure(p1, cut) else tailcall(parser2.run(p1.reset(p0.pos), onSuccess, onFailure)) + ) + ) + + override def toString: String = s"$parser1 | $parser2" + } + + final case class LookAhead[T](parser: Parser[T]) extends Parser[T] { + def run[R]( + p0: Parsing, + onSuccess: (T, Parsing, Boolean) => TailRec[R], + onFailure: (Parsing, Boolean) => TailRec[R] + ): TailRec[R] = + tailcall(parser.run(p0, (a, p1, cut1) => onSuccess(a, p1.reset(p0.pos), cut1), onFailure)) + + override def toString: String = s"&?($parser)" + } + + final case class NegativeLookAhead(parser: Parser[Any]) extends Parser[Unit] { + def run[R]( + p0: Parsing, + onSuccess: (Unit, Parsing, Boolean) => TailRec[R], + onFailure: (Parsing, Boolean) => TailRec[R] + ): TailRec[R] = + // Set `p0.errorPos` as `Int.MaxValue` for preventing to override an error message. + // Generally an error message in negative look-ahead is not useful. + tailcall( + parser.run( + p0.copy(errorPos = Int.MaxValue), + (_, p1, cut1) => onFailure(p1.copy(errorPos = p0.errorPos).fail(message, p0.pos), cut1), + (p1, cut1) => onSuccess((), p1.copy(errorPos = p0.errorPos).reset(p0.pos), cut1) + ) + ) + + lazy val message: String = s"unexpected: $parser" + override def toString: String = s"&!($parser)" + } + + final case class Map[T, U](parser: Parser[T], f: T => U) extends Parser[U] { + def run[R]( + p0: Parsing, + onSuccess: (U, Parsing, Boolean) => TailRec[R], + onFailure: (Parsing, Boolean) => TailRec[R] + ): TailRec[R] = + tailcall(parser.run(p0, (x, p1, cut) => onSuccess(f(x), p1, cut), onFailure)) + + override def toString: String = + parser match { + case _: Sequence[_, _, _] | _: Alternative[_] => s"($parser).map(...)" + case _ => s"$parser.map(...)" + } + } + + final case class FlatMap[T, U](parser: Parser[T], f: T => Parser[U]) extends Parser[U] { + def run[R]( + p0: Parsing, + onSuccess: (U, Parsing, Boolean) => TailRec[R], + onFailure: (Parsing, Boolean) => TailRec[R] + ): TailRec[R] = + tailcall( + parser.run( + p0, + (x, p1, cut1) => + f(x).run(p1, (y, p2, cut2) => onSuccess(y, p2, cut1 || cut2), (p2, cut2) => onFailure(p2, cut1 || cut2)), + onFailure + ) + ) + + override def toString: String = + parser match { + case _: Sequence[_, _, _] | _: Alternative[_] => s"($parser).flatMap(...)" + case _ => s"$parser.flatMap(...)" + } + } + + final case class Filter[T](parser: Parser[T], f: T => Boolean) extends Parser[T] { + def run[R]( + p0: Parsing, + onSuccess: (T, Parsing, Boolean) => TailRec[R], + onFailure: (Parsing, Boolean) => TailRec[R] + ): TailRec[R] = + tailcall( + parser.run( + p0, + (x, p1, cut) => if (f(x)) onSuccess(x, p1, cut) else onFailure(p1.fail("filter", p0.pos), cut), + onFailure + ) + ) + + override def toString: String = { + parser match { + case _: Sequence[_, _, _] | _: Alternative[_] => s"($parser).filter(...)" + case _ => s"$parser.filter(...)" + } + } + } + + final case class Delay[T](f: () => Parser[T], name: String) extends Parser[T] { + lazy val parser: Parser[T] = f() + + def run[R]( + p: Parsing, + onSuccess: (T, Parsing, Boolean) => TailRec[R], + onFailure: (Parsing, Boolean) => TailRec[R] + ): TailRec[R] = + tailcall(parser.run(p, onSuccess, onFailure)) + + override def toString: String = name + } + + final case class Named[T](parser: Parser[T], name: String) extends Parser[T] { + def run[R]( + p0: Parsing, + onSuccess: (T, Parsing, Boolean) => TailRec[R], + onFailure: (Parsing, Boolean) => TailRec[R] + ): TailRec[R] = + tailcall( + parser.run( + p0.named(name), + (a, p1, cut1) => onSuccess(a, p1.named(p0.name, p0.namePos), cut1), + (p1, cut1) => onFailure(p1.named(p0.name, p0.namePos), cut1) + ) + ) + + override def toString: String = { + val args = "\"" + Util.escape(name) + "\"" + parser match { + case _: Sequence[_, _, _] | _: Alternative[_] => s"($parser).named($args)" + case _ => s"$parser.named($args)" + } + } + } + + final case class Pass[T](value: T) extends Parser[T] { + def run[R]( + p: Parsing, + onSuccess: (T, Parsing, Boolean) => TailRec[R], + onFailure: (Parsing, Boolean) => TailRec[R] + ): TailRec[R] = + onSuccess(value, p, false) + + override def toString: String = if (value == ()) "Pass" else s"Pass($value)" + } + + final case class Fail(message: String) extends Parser[Nothing] { + def run[R]( + p: Parsing, + onSuccess: (Nothing, Parsing, Boolean) => TailRec[R], + onFailure: (Parsing, Boolean) => TailRec[R] + ): TailRec[R] = + onFailure(p.fail(message), false) + + override def toString: String = + if (message == "fail") "Fail" else "Fail(\"" + Util.escape(message) + "\")" + } + +} diff --git a/modules/parsers-contparse/src/main/scala/codes/quine/labo/parsers/contparse/Parsing.scala b/modules/parsers-contparse/src/main/scala/codes/quine/labo/parsers/contparse/Parsing.scala new file mode 100644 index 0000000..a1ed148 --- /dev/null +++ b/modules/parsers-contparse/src/main/scala/codes/quine/labo/parsers/contparse/Parsing.scala @@ -0,0 +1,33 @@ +package codes.quine.labo.parsers +package contparse + +final case class Parsing( + input: String, + pos: Int = 0, + message: Option[String] = None, + expected: Seq[String] = Seq.empty, + errorPos: Int = -1, + name: String = "", + namePos: Int = Int.MaxValue +) { + def reset(pos: Int): Parsing = copy(pos = pos) + + def advance(n: Int): Parsing = copy(pos = pos + n) + + def named(name: String, pos: Int = this.pos): Parsing = copy(name = name, namePos = pos) + + def unexpected(name: String, pos: Int = this.pos): Parsing = + if (namePos <= pos && name != this.name) unexpected(this.name, namePos) + else if (errorPos < pos) copy(errorPos = pos, expected = Seq(name), message = None) + else if (errorPos == pos) copy(expected = expected :+ name, message = None) + else this + + def fail(message: String, pos: Int = this.pos): Parsing = + if (namePos <= pos) unexpected(this.name, namePos) + else if (errorPos < pos) copy(errorPos = pos, expected = Seq.empty, message = Some(message)) + else if (errorPos == pos && expected.isEmpty) copy(expected = Seq.empty, message = Some(message)) + else this + + def errorMessage: String = + message.getOrElse(s"expected: ${expected.sorted.distinct.mkString(", ")}") +} diff --git a/modules/parsers-contparse/src/main/scala/codes/quine/labo/parsers/contparse/package.scala b/modules/parsers-contparse/src/main/scala/codes/quine/labo/parsers/contparse/package.scala new file mode 100644 index 0000000..ad70644 --- /dev/null +++ b/modules/parsers-contparse/src/main/scala/codes/quine/labo/parsers/contparse/package.scala @@ -0,0 +1,58 @@ +package codes.quine.labo.parsers + +import scala.language.implicitConversions +import scala.util.control.TailCalls.done + +package object contparse { + type Optioner[-A] = common.Optioner[A] + val Optioner = common.Optioner + type Repeater[-A] = common.Repeater[A] + val Repeater = common.Repeater + type Sequencer[-A, -B] = common.Sequencer[A, B] + val Sequencer = common.Sequencer + type Parsed[+A] = common.Parsed[A] + val Parsed = common.Parsed + + type P[+T] = Parser[T] + + def parse[T](input: String, parser: P[T]): Parsed[T] = + parser + .run( + Parsing(input), + (x, p, _) => done(Parsed.Success(x, p.pos)), + (p, _) => done(Parsed.Failure(p.errorMessage, p.errorPos)) + ) + .result + + implicit def Literal(s: String): P[Unit] = Parser.Literal(s) + + def CharIn(s: String): P[Unit] = Parser.CharIn(s) + + def CharPred(f: Char => Boolean): P[Unit] = Parser.CharPred(f) + + def CharsWhileIn(s: String, min: Int = 1): P[Unit] = Parser.CharsWhileIn(s, min) + + def CharsWhile(f: Char => Boolean, min: Int = 1): P[Unit] = Parser.CharsWhile(f, min) + + val AnyChar: P[Unit] = Parser.AnyChar + + val Start: P[Unit] = Parser.Start + + val End: P[Unit] = Parser.End + + def NoCut[T](parser: P[T]): P[T] = Parser.NoCut(parser) + + def &?[T](parser: P[T]): P[T] = Parser.LookAhead(parser) + + def &!(parser: P[Any]): P[Unit] = Parser.NegativeLookAhead(parser) + + def P[T](parser: => P[T])(implicit name: sourcecode.Name): P[T] = Parser.Delay(() => parser, name.value) + + def Pass: P[Unit] = Parser.Pass(()) + + def Pass[T](value: T): P[T] = Parser.Pass(value) + + def Fail: P[Nothing] = Parser.Fail("fail") + + def Fail(message: String): P[Nothing] = Parser.Fail(message) +} diff --git a/modules/parsers-contparse/src/test/scala/codes/quine/labo/parsers/contparse/ParserSuite.scala b/modules/parsers-contparse/src/test/scala/codes/quine/labo/parsers/contparse/ParserSuite.scala new file mode 100644 index 0000000..aa4e2f5 --- /dev/null +++ b/modules/parsers-contparse/src/test/scala/codes/quine/labo/parsers/contparse/ParserSuite.scala @@ -0,0 +1,246 @@ +package codes.quine.labo.parsers.contparse + +import minitest.SimpleTestSuite + +object ParserSuite extends SimpleTestSuite { + def assertSuccess[T](s: String, parser: P[T], expected: T, pos: Int): Unit = + assertEquals(parse(s, parser), Parsed.Success(expected, pos)) + + def assertFailure(s: String, parser: P[Any], message: String, pos: Int): Unit = + assertEquals(parse(s, parser), Parsed.Failure(message, pos)) + + test("Literal") { + assertSuccess("", "", (), 0) + assertSuccess("a", "a", (), 1) + assertFailure("b", "a", "expected: \"a\"", 0) + assertSuccess("aa", "a", (), 1) + assertSuccess("abc", "abc", (), 3) + assertFailure("ab", "abc", "expected: \"abc\"", 0) + assertSuccess("あい", "あい", (), 2) + assertFailure("うえ", "あい", "expected: \"あい\"", 0) + assertSuccess("あいうえお", "あい", (), 2) + } + + test("CharIn") { + assertSuccess("a", CharIn("ab"), (), 1) + assertSuccess("aa", CharIn("ab"), (), 1) + assertSuccess("b", CharIn("ab"), (), 1) + assertFailure("", CharIn("ab"), "expected: CharIn(\"ab\")", 0) + assertFailure("c", CharIn("ab"), "expected: CharIn(\"ab\")", 0) + } + + test("CharPred") { + assertSuccess("a", CharPred(_.isLower), (), 1) + assertSuccess("aa", CharPred(_.isLower), (), 1) + assertSuccess("b", CharPred(_.isLower), (), 1) + assertFailure("", CharPred(_.isLower), "expected: CharPred(...)", 0) + assertFailure("A", CharPred(_.isLower), "expected: CharPred(...)", 0) + } + + test("CharsWhileIn") { + assertSuccess("a", CharsWhileIn("abc"), (), 1) + assertSuccess("abcba", CharsWhileIn("abc"), (), 5) + assertSuccess("abcdcba", CharsWhileIn("abc"), (), 3) + assertSuccess("", CharsWhileIn("abc", 0), (), 0) + assertFailure("", CharsWhileIn("abc"), "expected: CharsWhileIn(\"abc\")", 0) + assertFailure("def", CharsWhileIn("abc"), "expected: CharsWhileIn(\"abc\")", 0) + } + + test("CharsWhile") { + assertSuccess("a", CharsWhile(_.isLower), (), 1) + assertSuccess("abcba", CharsWhile(_.isLower), (), 5) + assertSuccess("abcAcba", CharsWhile(_.isLower), (), 3) + assertSuccess("", CharsWhile(_.isLower, 0), (), 0) + assertFailure("", CharsWhile(_.isLower), "expected: CharsWhile(...)", 0) + assertFailure("ABC", CharsWhile(_.isLower), "expected: CharsWhile(...)", 0) + } + + test("AnyChar") { + assertSuccess("a", AnyChar, (), 1) + assertSuccess("aa", AnyChar, (), 1) + assertSuccess("A", AnyChar, (), 1) + assertFailure("", AnyChar, "expected: AnyChar", 0) + } + + test("Start") { + assertSuccess("", Start, (), 0) + assertSuccess("a", Start, (), 0) + assertFailure("a", "a" ~ Start, "expected: Start", 1) + } + + test("End") { + assertSuccess("", End, (), 0) + assertSuccess("a", "a" ~ End, (), 1) + assertFailure("a", End, "expected: End", 0) + } + + test("NoCut") { + assertSuccess("a", NoCut(AnyChar), (), 1) + assertSuccess("aa", NoCut("a" ~/ "b") | "aa", (), 2) + } + + test("&?") { + assertEquals(&?(AnyChar).toString, "&?(AnyChar)") + assertSuccess("a", &?("a"), (), 0) + assertSuccess("a", &?("a".!), "a", 0) + assertFailure("a", &?(AnyChar./) ~ "b" | "a", "expected: \"b\"", 0) + } + + test("&!") { + assertEquals(&!(AnyChar).toString, "&!(AnyChar)") + assertSuccess("", &!(AnyChar), (), 0) + assertFailure("a", &!(AnyChar), "unexpected: AnyChar", 0) + assertFailure("ab", &!(AnyChar ~ "a" | "ab"), "unexpected: AnyChar ~ \"a\" | \"ab\"", 0) + assertFailure("ab", &!(AnyChar ~/ "a") ~ "aa" | "ab", "expected: \"aa\"", 0) + } + + test("Parser#!") { + assertEquals(AnyChar.!.toString, "AnyChar.!") + assertSuccess("abc", "abc".!, "abc", 3) + assertSuccess("abc", "abc".!.!, "abc", 3) + assertSuccess("aaa", CharsWhileIn("a").!, "aaa", 3) + assertSuccess("aaab", CharsWhileIn("a").!, "aaa", 3) + assertFailure("", "abc".!, "expected: \"abc\"", 0) + } + + test("Parser#~") { + assertEquals((AnyChar ~ AnyChar).toString, "AnyChar ~ AnyChar") + assertEquals((AnyChar ~ AnyChar ~ AnyChar).toString, "AnyChar ~ AnyChar ~ AnyChar") + assertEquals((AnyChar ~ (AnyChar | AnyChar)).toString, "AnyChar ~ (AnyChar | AnyChar)") + assertSuccess("ab", "a" ~ "b", (), 2) + assertSuccess("ab", "a".! ~ "b", "a", 2) + assertSuccess("ab", "a" ~ "b".!, "b", 2) + assertSuccess("ab", "a".! ~ "b".!, ("a", "b"), 2) + assertSuccess("abc", "a".! ~ "b".! ~ "c".!, ("a", "b", "c"), 3) + assertSuccess("abcd", "a".! ~ "b".! ~ "c".! ~ "d".!, ("a", "b", "c", "d"), 4) + assertSuccess("abcd", "" ~ "a".! ~ "b".! ~ "" ~ "c".! ~ "d".! ~ "", ("a", "b", "c", "d"), 4) + assertSuccess("ab", ("a" ~ "b").!, "ab", 2) + assertFailure("bc", "a" ~ "b", "expected: \"a\"", 0) + assertFailure("ac", "a" ~ "b", "expected: \"b\"", 1) + } + + test("Parser#rep") { + assertEquals(AnyChar.rep.toString, "AnyChar.rep") + assertEquals(AnyChar.rep(1).toString, "AnyChar.rep(1)") + assertEquals(AnyChar.rep(1, 2).toString, "AnyChar.rep(1, 2)") + assertEquals((AnyChar ~ AnyChar).rep.toString, "(AnyChar ~ AnyChar).rep") + assertSuccess("", "a".rep, (), 0) + assertSuccess("aaa", "a".rep, (), 3) + assertSuccess("aaa", "a".rep.!, "aaa", 3) + assertSuccess("aaa", "a".!.rep, Seq("a", "a", "a"), 3) + assertSuccess("aab", "a".rep, (), 2) + assertSuccess("aaab", ("a" ~ "a").rep, (), 2) + assertFailure("aaab", ("a" ~/ "a").rep, "expected: \"a\"", 3) + assertSuccess("", ("a" | "").rep, (), 0) + assertSuccess("a", ("a" | "").rep, (), 1) + assertFailure("", ("a" | ""./).rep, "expected: \"a\"", 0) + assertFailure("a", ("a" | ""./).rep, "expected: \"a\"", 1) + assertSuccess("ba", ("a" | "b"./).rep, (), 2) + assertFailure("ba", ("a" | "b"./).rep ~ "c" | "ba", "expected: \"a\", \"b\", \"c\"", 2) + assertSuccess("aa", "a".rep(2), (), 2) + assertFailure("", "a".rep(2), "expected: \"a\"", 0) + assertFailure("a", "a".rep(2), "expected: \"a\"", 1) + assertSuccess("a", "a".rep(0, 1), (), 1) + assertSuccess("aa", "a".rep(0, 1), (), 1) + } + + test("Parser#count") { + assertEquals(AnyChar.count(3).toString, "AnyChar.count(3)") + assertEquals((AnyChar ~ AnyChar).count(3).toString, "(AnyChar ~ AnyChar).count(3)") + assertSuccess("aa", "a".count(2), (), 2) + assertSuccess("aa", "a".count(2).!, "aa", 2) + assertSuccess("aa", "a".!.count(2), Seq("a", "a"), 2) + assertFailure("", "a".count(2), "expected: \"a\"", 0) + assertFailure("a", "a".count(2), "expected: \"a\"", 1) + } + + test("Parser#?") { + assertEquals(AnyChar.?.toString, "AnyChar.?") + assertEquals((AnyChar ~ AnyChar).?.toString, "(AnyChar ~ AnyChar).?") + assertSuccess("", "a".?, (), 0) + assertSuccess("a", "a".?, (), 1) + assertSuccess("aa", "a".?, (), 1) + assertSuccess("", "a".!.?, None, 0) + assertSuccess("a", "a".!.?, Some("a"), 1) + assertSuccess("a", ("a" ~ "b").?, (), 0) + assertFailure("a", ("a" ~/ "b").?, "expected: \"b\"", 1) + } + + test("Parser#|") { + assertEquals((AnyChar | AnyChar).toString, "AnyChar | AnyChar") + assertEquals((AnyChar | AnyChar | AnyChar).toString, "AnyChar | AnyChar | AnyChar") + assertEquals((AnyChar | AnyChar ~ AnyChar).toString, "AnyChar | AnyChar ~ AnyChar") + assertSuccess("a", "a" | "b", (), 1) + assertSuccess("b", "a" | "b", (), 1) + assertSuccess("a", "a".! | "b".!, "a", 1) + assertSuccess("b", "a".! | "b".!, "b", 1) + assertFailure("c", "a" | "b", "expected: \"a\", \"b\"", 0) + assertFailure("az", "a" ~ "b" | "b" ~ "c", "expected: \"b\"", 1) + assertFailure("az", "b" ~ "c" | "a" ~ "b", "expected: \"b\"", 1) + assertSuccess("ac", "a" ~ "b" | "ac", (), 2) + assertFailure("ac", "a" ~/ "b" | "ac", "expected: \"b\"", 1) + assertFailure("abc", ("a" ~/ "b" | "ac") ~ "d" | "abc", "expected: \"d\"", 2) + assertFailure("abc", ("ac" | "a" ~/ "b") ~ "d" | "abc", "expected: \"d\"", 2) + } + + test("Parser#map") { + assertEquals(AnyChar.map(_ => 1).toString, "AnyChar.map(...)") + assertEquals((AnyChar ~ AnyChar).map(_ => 1).toString, "(AnyChar ~ AnyChar).map(...)") + assertSuccess("a", AnyChar.map(_ => 1), 1, 1) + assertSuccess("a", AnyChar.!.map(_.length), 1, 1) + assertFailure("b", "a".!.map(_.length), "expected: \"a\"", 0) + } + + test("Parser#flatMap") { + assertEquals(AnyChar.flatMap(_ => Pass(1)).toString, "AnyChar.flatMap(...)") + assertEquals((AnyChar ~ AnyChar).flatMap(_ => Pass(1)).toString, "(AnyChar ~ AnyChar).flatMap(...)") + assertSuccess("a", AnyChar.flatMap(_ => Pass(1)), 1, 1) + assertSuccess("ab", AnyChar.flatMap(_ => AnyChar), (), 2) + assertSuccess("aa", AnyChar.!.flatMap(s => s), (), 2) + assertFailure("ab", AnyChar.!.flatMap(s => s), "expected: \"a\"", 1) + assertFailure("ab", AnyChar./.flatMap(_ => "a" | "c") | "ab", "expected: \"a\", \"c\"", 1) + } + + test("Parser#filter") { + assertEquals(AnyChar.filter(_ => true).toString, "AnyChar.filter(...)") + assertEquals((AnyChar ~ AnyChar).filter(_ => true).toString, "(AnyChar ~ AnyChar).filter(...)") + assertSuccess("a", AnyChar.!.filter(_ == "a"), "a", 1) + assertFailure("b", AnyChar.!.filter(_ == "a"), "filter", 0) + } + + test("P") { + lazy val p: P[Unit] = P("a" ~ "b" | "c" ~ p) + assertEquals(p.toString, "p") + assertSuccess("ab", p, (), 2) + assertSuccess("cab", p, (), 3) + assertFailure("ac", p, "expected: \"b\"", 1) + } + + test("Parser#named") { + assertEquals(AnyChar.named("foo").toString, "AnyChar.named(\"foo\")") + assertSuccess("ab", ("a" ~ "b").named("foo"), (), 2) + assertFailure("aa", ("a" ~ "b").named("foo"), "expected: foo", 0) + assertFailure("abc", ("a" ~ "b").named("foo") ~ "a", "expected: \"a\"", 2) + } + + test("Parser#/") { + assertEquals(AnyChar./.toString, "AnyChar./") + assertEquals((AnyChar ~ AnyChar)./.toString, "(AnyChar ~ AnyChar)./") + assertFailure("aa", AnyChar./ ~ "b" | "aa", "expected: \"b\"", 1) + } + + test("Pass") { + assertEquals(Pass.toString, "Pass") + assertEquals(Pass(0).toString, "Pass(0)") + assertSuccess("", Pass(0), 0, 0) + assertSuccess("a", Pass(0), 0, 0) + assertSuccess("", Pass, (), 0) + } + + test("Fail") { + assertEquals(Fail.toString, "Fail") + assertEquals(Fail("foo").toString, "Fail(\"foo\")") + assertFailure("", Fail, "fail", 0) + assertFailure("", Fail("foo"), "foo", 0) + } +} diff --git a/modules/parsers-funcparse/src/main/scala/codes/quine/labo/parsers/funcparse/Parser.scala b/modules/parsers-funcparse/src/main/scala/codes/quine/labo/parsers/funcparse/Parser.scala index c9cf482..22af933 100644 --- a/modules/parsers-funcparse/src/main/scala/codes/quine/labo/parsers/funcparse/Parser.scala +++ b/modules/parsers-funcparse/src/main/scala/codes/quine/labo/parsers/funcparse/Parser.scala @@ -286,7 +286,6 @@ object Parser { } lazy val message: String = s"unexpected: $parser" - override def toString: String = s"&!($parser)" }