├── benchmarks
└── src
│ ├── main
│ ├── resources
│ │ └── bar.json
│ └── scala
│ │ └── zio
│ │ └── parser
│ │ └── benchmarks
│ │ ├── Parserz.scala
│ │ ├── json
│ │ ├── Json.scala
│ │ ├── JsonParsley.scala
│ │ ├── JsonFastParse.scala
│ │ ├── JsonAttoParse.scala
│ │ ├── JsonZioParser.scala
│ │ ├── JsonParserBenchmark.scala
│ │ ├── JsonParboiled.scala
│ │ ├── JsonParserz.scala
│ │ └── JsonCatsParse.scala
│ │ ├── basic
│ │ ├── RepeatSpecificChars.scala
│ │ ├── Zipping.scala
│ │ ├── RepeatAnyChar.scala
│ │ ├── Zipping16.scala
│ │ └── StringAlternatives.scala
│ │ ├── regex
│ │ └── RegexBenchmarks.scala
│ │ ├── ParserBenchmarkTestRunner.scala
│ │ ├── lucene
│ │ ├── model.scala
│ │ ├── LuceneQueryBenchmark.scala
│ │ ├── CatsLuceneQueryParser.scala
│ │ └── ZioLuceneQueryParser.scala
│ │ ├── ParserBenchmark.scala
│ │ ├── RegexBenchmark.scala
│ │ └── micro
│ │ ├── Experiments.scala
│ │ └── CharParserMicroBenchmarks.scala
│ └── test
│ └── scala
│ └── zio
│ └── parser
│ └── benchmarks
│ └── BenchmarksSpec.scala
├── project
├── build.properties
└── plugins.sbt
├── docs
├── package.json
├── sidebars.js
└── index.md
├── zio-parser
└── shared
│ └── src
│ ├── main
│ ├── scala-2
│ │ └── zio
│ │ │ └── parser
│ │ │ ├── VersionSpecificParser.scala
│ │ │ ├── VersionSpecificPrinter.scala
│ │ │ ├── VersionSpecificSyntax.scala
│ │ │ ├── SyntaxCompanionVersionSpecific.scala
│ │ │ └── TupleConversion.scala
│ ├── scala
│ │ └── zio
│ │ │ └── parser
│ │ │ ├── target
│ │ │ ├── Target.scala
│ │ │ ├── ChunkTarget.scala
│ │ │ └── ZStreamTarget.scala
│ │ │ ├── ParserImplementation.scala
│ │ │ ├── internal
│ │ │ ├── Stack.scala
│ │ │ ├── recursive
│ │ │ │ └── ParserState.scala
│ │ │ ├── Debug.scala
│ │ │ ├── PrinterImpl.scala
│ │ │ └── PUnzippable.scala
│ │ │ └── package.scala
│ └── scala-3
│ │ └── zio
│ │ └── parser
│ │ ├── SyntaxCompanionVersionSpecific.scala
│ │ ├── VersionSpecificPrinter.scala
│ │ ├── VersionSpecificParser.scala
│ │ ├── VersionSpecificSyntax.scala
│ │ ├── TupleConversion.scala
│ │ └── Macros.scala
│ └── test
│ └── scala
│ └── zio
│ └── parser
│ ├── examples
│ ├── ExpressionExample.scala
│ ├── ContextualExample.scala
│ └── JsonExample.scala
│ ├── SyntaxSpec.scala
│ ├── StringParserErrorSpec.scala
│ ├── PrinterSpec.scala
│ └── RegexSpec.scala
├── .git-blame-ignore-revs
├── .scalafmt.conf
├── .github
├── workflows
│ ├── release-drafter.yml
│ ├── auto-approve.yml
│ └── ci.yml
└── release-drafter.yml
├── README.md
├── .gitignore
└── LICENSE
/benchmarks/src/main/resources/bar.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=1.10.5
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@zio.dev/zio-parser",
3 | "description": "ZIO Parser Documentation",
4 | "license": "Apache-2.0"
5 | }
6 |
--------------------------------------------------------------------------------
/zio-parser/shared/src/main/scala-2/zio/parser/VersionSpecificParser.scala:
--------------------------------------------------------------------------------
1 | package zio.parser
2 |
3 | trait VersionSpecificParser[+Err, -In, +Result] {
4 | self: Parser[Err, In, Result] =>
5 | }
6 |
--------------------------------------------------------------------------------
/zio-parser/shared/src/main/scala-2/zio/parser/VersionSpecificPrinter.scala:
--------------------------------------------------------------------------------
1 | package zio.parser
2 |
3 | trait VersionSpecificPrinter[+Err, +Out, -Value] {
4 | self: Printer[Err, Out, Value] =>
5 | }
6 |
--------------------------------------------------------------------------------
/zio-parser/shared/src/main/scala-2/zio/parser/VersionSpecificSyntax.scala:
--------------------------------------------------------------------------------
1 | package zio.parser
2 |
3 | trait VersionSpecificSyntax[+Err, -In, +Out, Value] {
4 | self: Syntax[Err, In, Out, Value] =>
5 | }
6 |
--------------------------------------------------------------------------------
/benchmarks/src/main/scala/zio/parser/benchmarks/Parserz.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.benchmarks
2 |
3 | object Parserz extends org.spartanz.parserz.ParsersModule {
4 | override type Input = List[Char]
5 | }
6 |
--------------------------------------------------------------------------------
/.git-blame-ignore-revs:
--------------------------------------------------------------------------------
1 | # Scala Steward: Reformat with scalafmt 3.7.1
2 | 07f4c5c563c4fa850fa7d230d7367846a99b9669
3 |
4 | # Scala Steward: Reformat with scalafmt 3.8.1
5 | c782b48485ca4a5c1899d453732d5935d731bc72
6 |
--------------------------------------------------------------------------------
/.scalafmt.conf:
--------------------------------------------------------------------------------
1 | version = 3.8.1
2 | maxColumn = 120
3 | align.preset = most
4 | rewrite.rules = [RedundantBraces]
5 | lineEndings = preserve
6 | runner.dialect = scala213source3
7 |
8 | fileOverride {
9 | "glob:**/scala-3*/**" {
10 | runner.dialect = scala3
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/docs/sidebars.js:
--------------------------------------------------------------------------------
1 | const sidebars = {
2 | sidebar: [
3 | {
4 | type: "category",
5 | label: "ZIO Parser",
6 | collapsed: false,
7 | link: { type: "doc", id: "index" },
8 | items: [ ]
9 | }
10 | ]
11 | };
12 |
13 | module.exports = sidebars;
14 |
--------------------------------------------------------------------------------
/zio-parser/shared/src/main/scala/zio/parser/target/Target.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.target
2 |
3 | trait Target[Output] {
4 | type Capture
5 |
6 | def write(value: Output): Unit
7 |
8 | def capture(): Capture
9 | def emit(capture: Capture): Unit
10 | def drop(capture: Capture): Unit
11 | }
12 |
--------------------------------------------------------------------------------
/.github/workflows/release-drafter.yml:
--------------------------------------------------------------------------------
1 | name: Release Drafter
2 |
3 | on:
4 | push:
5 | branches: ['master']
6 |
7 | jobs:
8 | update_release_draft:
9 | runs-on: ubuntu-20.04
10 | steps:
11 | - uses: release-drafter/release-drafter@v5
12 | env:
13 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
--------------------------------------------------------------------------------
/.github/workflows/auto-approve.yml:
--------------------------------------------------------------------------------
1 | name: Auto approve
2 |
3 | on:
4 | pull_request_target
5 |
6 | jobs:
7 | auto-approve:
8 | runs-on: ubuntu-20.04
9 | steps:
10 | - uses: hmarr/auto-approve-action@v2.1.0
11 | if: github.actor == 'scala-steward' || github.actor == 'renovate[bot]'
12 | with:
13 | github-token: "${{ secrets.GITHUB_TOKEN }}"
--------------------------------------------------------------------------------
/zio-parser/shared/src/main/scala-2/zio/parser/SyntaxCompanionVersionSpecific.scala:
--------------------------------------------------------------------------------
1 | package zio.parser
2 |
3 | trait SyntaxCompanionVersionSpecific {
4 | def oneOf[A, I, O](
5 | syntax: Syntax[String, I, O, _ <: A],
6 | syntaxes: Syntax[String, I, O, _ <: A]*
7 | ): Syntax[String, I, O, A] =
8 | syntaxes.map(_.widen[A]).foldLeft(syntax.widen[A])(_ | _)
9 | }
10 |
--------------------------------------------------------------------------------
/zio-parser/shared/src/main/scala-3/zio/parser/SyntaxCompanionVersionSpecific.scala:
--------------------------------------------------------------------------------
1 | package zio.parser
2 |
3 | trait SyntaxCompanionVersionSpecific {
4 |
5 | /** Syntax that combines all the constructors of subclasses of a sum type */
6 | inline def oneOf[A, I, O](
7 | inline syntax: Syntax[String, I, O, _ <: A],
8 | inline syntaxes: Syntax[String, I, O, _ <: A]*
9 | ): Syntax[String, I, O, A] =
10 | ${ Macros.oneOfImpl[A, I, O]('syntax, 'syntaxes) }
11 | }
12 |
--------------------------------------------------------------------------------
/zio-parser/shared/src/main/scala/zio/parser/ParserImplementation.scala:
--------------------------------------------------------------------------------
1 | package zio.parser
2 |
3 | /** The available parser implementations
4 | *
5 | * The default parser implementation is Recursive. There is an alternative implementation available which is stack-safe
6 | * but slower.
7 | */
8 | sealed trait ParserImplementation
9 | object ParserImplementation {
10 | case object StackSafe extends ParserImplementation
11 | case object Recursive extends ParserImplementation
12 | }
13 |
--------------------------------------------------------------------------------
/benchmarks/src/main/scala/zio/parser/benchmarks/json/Json.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.benchmarks.json
2 |
3 | import zio.Chunk
4 |
5 | sealed abstract class Json
6 | object Json {
7 | final case class Obj(fields: Chunk[(String, Json)]) extends Json
8 | final case class Arr(elements: Chunk[Json]) extends Json
9 | final case class Bool(value: Boolean) extends Json
10 | final case class Str(value: String) extends Json
11 | final case class Num(value: BigDecimal) extends Json
12 | case object Null extends Json
13 | }
14 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.9.0")
2 | addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2")
3 | addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2")
4 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.17.0")
5 | addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.6")
6 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2")
7 | addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.7")
8 | addSbtPlugin("dev.zio" % "zio-sbt-website" % "0.3.4")
9 |
--------------------------------------------------------------------------------
/zio-parser/shared/src/main/scala-3/zio/parser/VersionSpecificPrinter.scala:
--------------------------------------------------------------------------------
1 | package zio.parser
2 |
3 | import scala.reflect.ClassTag
4 |
5 | trait VersionSpecificPrinter[+Err, +Out, -Value] { self: Printer[Err, Out, Value] =>
6 |
7 | /** Prints this and if it fails, ignore the printed output and print 'that' instead. */
8 | final def orElseU[Err2 >: Err, Out2 >: Out, Value1 <: Value, Value2: ClassTag](
9 | that: => Printer[Err2, Out2, Value2]
10 | )(implicit vtag: ClassTag[Value1]): Printer[Err2, Out2, Value1 | Value2] =
11 | orElseEither(that).contramap {
12 | case v2: Value2 => Right(v2)
13 | case v: Value1 => Left(v)
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/zio-parser/shared/src/main/scala-3/zio/parser/VersionSpecificParser.scala:
--------------------------------------------------------------------------------
1 | package zio.parser
2 |
3 | trait VersionSpecificParser[+Err, -In, +Result] {
4 | self: Parser[Err, In, Result] =>
5 |
6 | /** Assigns 'that' parser as a fallback of this. First this parser gets evaluated. In case it succeeds, the result is
7 | * this parser's result. In case it fails, the result is 'that' parser's result.
8 | *
9 | * If auto-backtracking is on, this parser will backtrack before trying 'that' parser.
10 | */
11 | final def orElseU[Err2 >: Err, In2 <: In, Result2](
12 | that: => Parser[Err2, In2, Result2]
13 | ): Parser[Err2, In2, Result | Result2] =
14 | Parser.OrElse(Parser.Lazy(() => self), Parser.Lazy(() => that))
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/zio-parser/shared/src/main/scala-3/zio/parser/VersionSpecificSyntax.scala:
--------------------------------------------------------------------------------
1 | package zio.parser
2 |
3 | import scala.reflect.ClassTag
4 |
5 | trait VersionSpecificSyntax[+Err, -In, +Out, Value] { self: Syntax[Err, In, Out, Value] =>
6 |
7 | /** Assigns 'that' syntax as a fallback of this. First this parser or printer gets evaluated. In case it succeeds, the
8 | * result is this syntax's result. In case it fails, the result is 'that' syntax's result.
9 | *
10 | * If auto-backtracking is on, this parser will backtrack before trying 'that' parser.
11 | */
12 | final def orElseU[Err2 >: Err, In2 <: In, Out2 >: Out, Value2: ClassTag](
13 | that: => Syntax[Err2, In2, Out2, Value2]
14 | )(implicit vtag: ClassTag[Value]): Syntax[Err2, In2, Out2, Value | Value2] =
15 | asParser.orElseU(that.asParser) <=> asPrinter.orElseU(that.asPrinter)
16 | }
17 |
--------------------------------------------------------------------------------
/.github/release-drafter.yml:
--------------------------------------------------------------------------------
1 | name-template: 'v$RESOLVED_VERSION'
2 | tag-template: 'v$RESOLVED_VERSION'
3 | template: |
4 | # What's Changed
5 | $CHANGES
6 | categories:
7 | - title: 'Breaking'
8 | label: 'type: breaking'
9 | - title: 'New'
10 | label: 'type: feature'
11 | - title: 'Bug Fixes'
12 | label: 'type: bug'
13 | - title: 'Maintenance'
14 | label: 'type: maintenance'
15 | - title: 'Documentation'
16 | label: 'type: docs'
17 | - title: 'Dependency Updates'
18 | label: 'type: dependencies'
19 |
20 | version-resolver:
21 | major:
22 | labels:
23 | - 'type: breaking'
24 | minor:
25 | labels:
26 | - 'type: feature'
27 | patch:
28 | labels:
29 | - 'type: bug'
30 | - 'type: maintenance'
31 | - 'type: docs'
32 | - 'type: dependencies'
33 | - 'type: security'
34 |
35 | exclude-labels:
36 | - 'skip-changelog'
37 |
--------------------------------------------------------------------------------
/zio-parser/shared/src/main/scala-3/zio/parser/TupleConversion.scala:
--------------------------------------------------------------------------------
1 | package zio.parser
2 |
3 | import scala.deriving._
4 |
5 | trait TupleConversion[A, B] {
6 | def to(a: A): B
7 | def from(b: B): A
8 | }
9 |
10 | class TupleConversionImpl[A, B](_to: A => B, _from: B => A) extends TupleConversion[A, B] {
11 | def to(a: A): B = _to(a)
12 | def from(b: B): A = _from(b)
13 | }
14 |
15 | object TupleConversion extends ImplicitTupleConversion
16 |
17 | trait ImplicitTupleConversion {
18 | inline given autoTupleConversion[Prod <: Product](using
19 | m: Mirror.ProductOf[Prod]
20 | ): TupleConversion[Prod, m.MirroredElemTypes] =
21 | TupleConversionImpl[Prod, m.MirroredElemTypes](Tuple.fromProductTyped(_), m.fromProduct(_))
22 |
23 | inline given autoTupleConversion1[Prod <: Product, A](using
24 | c: TupleConversion[Prod, Tuple1[A]]
25 | ): TupleConversion[Prod, A] =
26 | TupleConversionImpl[Prod, A](
27 | a =>
28 | val Tuple1(v) = c.to(a)
29 | v
30 | ,
31 | b => c.from(Tuple1(b))
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: index
3 | title: "Introduction to ZIO Parser"
4 | sidebar_label: "ZIO Parser"
5 | ---
6 |
7 | Library for constructing parsers and pretty printers based on invertible syntax descriptions
8 |
9 | @PROJECT_BADGES@
10 |
11 | ## Introduction
12 |
13 | [](https://www.youtube.com/watch?v=DEPpL9LBiyA)
14 |
15 | ## Installation
16 |
17 | Start by adding `zio-parser` as a dependency to your project:
18 |
19 | ```scala
20 | libraryDependencies += "dev.zio" %% "zio-parser" % "@VERSION@"
21 | ```
22 |
23 | ## Getting Started
24 |
25 | ```scala mdoc
26 | import zio.parser.*
27 | ```
28 |
29 | Declare your parsing syntax:
30 |
31 | ```scala mdoc:silent
32 | val digitSyntax: Syntax[String, Char, Char, Char] = Syntax.digit
33 | ```
34 |
35 | Parse your string:
36 |
37 | ```scala mdoc
38 | val result: Either[StringParserError[String], Char] = digitSyntax.parseString("1")
39 | ```
40 |
41 | Pretty print the parsing errors:
42 |
43 | ```scala mdoc
44 | println(digitSyntax.parseString("Hello").left.map(_.pretty).merge)
45 | ```
46 |
47 | [//]: # (TODO: Add example section)
48 | [//]: # (## Example)
49 |
--------------------------------------------------------------------------------
/benchmarks/src/main/scala/zio/parser/benchmarks/basic/RepeatSpecificChars.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.benchmarks
2 |
3 | import zio.parser.benchmarks.basic.RepeatAnyChar
4 |
5 | import scala.util.Try
6 |
7 | class RepeatSpecificChars extends ParserBenchmark[String] {
8 | override final def loadInput(): String = "abcdefghijklmnop" * 10000
9 |
10 | override final val zioSyntax: zio.parser.Syntax[String, Char, Char, String] = {
11 | import zio.parser._
12 | Syntax.charIn('a' to 'p': _*).atLeast(10000).string
13 | }
14 |
15 | override final val catParser: cats.parse.Parser[String] = {
16 | import cats.parse._
17 | (Parser.charIn('a' to 'p')).rep(10000).string
18 | }
19 |
20 | override final def fastParseP[P: fastparse.P]: fastparse.P[String] = null
21 |
22 | override final val attoParser: atto.Parser[String] = null
23 |
24 | override final def runParboiledParser(input: String): Try[String] = null
25 |
26 | override final val parsley: org.http4s.parsley.Parsley[String] = null
27 |
28 | override final val parserz: Parserz.Grammar[Any, Nothing, String, String] = null
29 | }
30 |
31 | case class Chars(chars: Seq[Char]) {
32 | override def toString: String = chars.mkString
33 | }
34 |
35 | object RepeatSpecificChars extends ParserBenchmarkTestRunner[String, RepeatSpecificChars] {
36 | override val self: ParserBenchmark[String] = new RepeatSpecificChars
37 | }
38 |
--------------------------------------------------------------------------------
/zio-parser/shared/src/main/scala-3/zio/parser/Macros.scala:
--------------------------------------------------------------------------------
1 | package zio.parser
2 |
3 | import scala.quoted._
4 | import scala.reflect.ClassTag
5 |
6 | object Macros {
7 | def oneOfImpl[A: Type, I: Type, O: Type](
8 | syntax: Expr[Syntax[String, I, O, _ <: A]],
9 | syntaxes: Expr[Seq[Syntax[String, I, O, _ <: A]]]
10 | )(using Quotes) = {
11 | import quotes.reflect._
12 | syntaxes match {
13 | case Varargs(exprs) =>
14 | val widenedSeq = Expr.ofSeq(exprs.prepended(syntax).map(widenImpl(_)))
15 | '{ $widenedSeq.reduce(_ | _) }
16 | }
17 | }
18 |
19 | private def widenImpl[A: Type, I: Type, O: Type](syntax: Expr[Syntax[String, I, O, _ <: A]])(using Quotes) = {
20 | import quotes.reflect._
21 | syntax.asExprOf[Any] match {
22 | case '{ $syntax: Syntax[String, I, O, a] } =>
23 | val classTag: Expr[ClassTag[a]] = summonClassTag[a]
24 | val ev: Expr[a <:< A] = Expr.summon[a <:< A].get
25 | '{ $syntax.widen[A]($ev, $classTag) }
26 | case other =>
27 | throw new Error(s"Unexpected syntax: $other")
28 | }
29 | }
30 |
31 | private def summonClassTag[A: Type](using Quotes): Expr[ClassTag[A]] = {
32 | import quotes.reflect._
33 | Expr.summon[ClassTag[A]] match {
34 | case Some(ct) => ct
35 | case None => report.errorAndAbort(s"Could not summon ClassTag[${Type.show[A]}]")
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/benchmarks/src/main/scala/zio/parser/benchmarks/json/JsonParsley.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.benchmarks.json
2 |
3 | import org.http4s.parsley._
4 | import org.http4s.parsley.Parsley._
5 | import org.http4s.parsley.Combinator._
6 | import zio.Chunk
7 |
8 | // Based on https://github.com/typelevel/cats-parse/blob/main/bench/src/main/scala/cats/parse/bench/parsley.scala
9 | object JsonParsley {
10 |
11 | def json: Parsley[Json] = {
12 | val jsontoks = LanguageDef(
13 | "",
14 | "",
15 | "",
16 | false,
17 | NotRequired,
18 | NotRequired,
19 | NotRequired,
20 | NotRequired,
21 | Set.empty,
22 | Set.empty,
23 | true,
24 | Predicate(Char.isWhitespace)
25 | )
26 | val tok = new TokenParser(jsontoks)
27 | lazy val obj: Parsley[Json] = tok.braces(
28 | tok.commaSep(+(tok.stringLiteral <~> tok.colon *> value)).map(pairs => Json.Obj(Chunk.fromIterable(pairs)))
29 | )
30 | lazy val array: Parsley[Json] =
31 | tok.brackets(tok.commaSep(value)).map(list => Json.Arr(Chunk.fromIterable(list)))
32 | lazy val value: Parsley[Json] =
33 | (tok.stringLiteral.map(Json.Str.apply)
34 | <|> tok.symbol("true") *> Parsley.pure(Json.Bool(true))
35 | <|> tok.symbol("false") *> Parsley.pure(Json.Bool(false))
36 | <|> tok.symbol("null") *> Parsley.pure(Json.Null)
37 | <|> array
38 | <|> attempt(tok.float).map(value => Json.Num(BigDecimal(value)))
39 | <|> tok.integer.map(i => Json.Num(BigDecimal(i)))
40 | <|> obj)
41 |
42 | tok.whiteSpace *> (obj <|> array) <* eof
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/benchmarks/src/main/scala/zio/parser/benchmarks/regex/RegexBenchmarks.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.benchmarks.regex
2 |
3 | import org.openjdk.jmh.annotations.{
4 | Benchmark,
5 | BenchmarkMode,
6 | Fork,
7 | Measurement,
8 | Mode,
9 | OutputTimeUnit,
10 | Scope,
11 | Setup,
12 | State,
13 | Warmup
14 | }
15 | import zio.Chunk
16 | import zio.parser.Regex
17 |
18 | import java.util.concurrent.TimeUnit
19 |
20 | @BenchmarkMode(Array(Mode.Throughput))
21 | @OutputTimeUnit(TimeUnit.MILLISECONDS)
22 | @State(Scope.Benchmark)
23 | @Warmup(iterations = 3, time = 3, timeUnit = TimeUnit.SECONDS)
24 | @Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
25 | @Fork(value = 2)
26 | class RegexBenchmarks {
27 |
28 | var value: String = _
29 | var valueChunk: Chunk[Char] = _
30 | var anyCharRep: Regex.Compiled = _
31 | var someCharRep: Regex.Compiled = _
32 | var literal: Regex.Compiled = _
33 | var literalRep: Regex.Compiled = _
34 |
35 | @Setup
36 | def setUp(): Unit = {
37 | value = "hello" * 1000
38 | valueChunk = Chunk.fromArray(value.toCharArray)
39 | anyCharRep = Regex.anyChar.atLeast(0).compile
40 | someCharRep = Regex.charIn('h', 'e', 'l', 'o').atLeast(0).compile
41 | literal = Regex.string("hello").compile
42 | literalRep = Regex.string("hello").atLeast(0).compile
43 | }
44 |
45 | @Benchmark
46 | def anyCharRepeated(): Int =
47 | anyCharRep.test(0, value) // TODO: test vs passthrough
48 |
49 | @Benchmark
50 | def someCharRepeated(): Int =
51 | someCharRep.test(0, value)
52 |
53 | @Benchmark
54 | def literalString(): Int =
55 | literal.test(0, value) // TODO: test vs regionMatches
56 |
57 | @Benchmark
58 | def literalStringRepeated(): Int =
59 | literalRep.test(0, value)
60 | }
61 |
--------------------------------------------------------------------------------
/benchmarks/src/main/scala/zio/parser/benchmarks/json/JsonFastParse.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.benchmarks.json
2 |
3 | import fastparse._, NoWhitespace._
4 | import zio.Chunk
5 |
6 | // Based on https://github.com/typelevel/cats-parse/blob/main/bench/src/main/scala/cats/parse/bench/fastparse.scala
7 | object JsonFastParse {
8 | def stringChars(c: Char) = c != '\"' && c != '\\'
9 |
10 | def space[P0: P] = P(CharsWhileIn(" \r\n", 0))
11 | def digits[P0: P] = P(CharsWhileIn("0-9"))
12 | def exponent[P0: P] = P(CharIn("eE") ~ CharIn("+\\-").? ~ digits)
13 | def fractional[P0: P] = P("." ~ digits)
14 | def integral[P0: P] = P("0" | CharIn("1-9") ~ digits.?)
15 |
16 | def number[P0: P] =
17 | P(CharIn("+\\-").? ~ integral ~ fractional.? ~ exponent.?).!.map(x => Json.Num(BigDecimal(x)))
18 |
19 | def `null`[P0: P] = P("null").map(_ => Json.Null)
20 | def `false`[P0: P] = P("false").map(_ => Json.Bool(false))
21 | def `true`[P0: P] = P("true").map(_ => Json.Bool(true))
22 |
23 | def hexDigit[P0: P] = P(CharIn("0-9a-fA-F"))
24 | def unicodeEscape[P0: P] = P("u" ~ hexDigit ~ hexDigit ~ hexDigit ~ hexDigit)
25 | def escape[P0: P] = P("\\" ~ (CharIn("\"/\\\\bfnrt") | unicodeEscape))
26 |
27 | def strChars[P0: P] = P(CharsWhile(stringChars))
28 | def string[P0: P] =
29 | P(space ~ "\"" ~/ (strChars | escape).rep.! ~ "\"").map(Json.Str.apply)
30 |
31 | def array[P0: P] =
32 | P("[" ~/ jsonExpr.rep(sep = ","./) ~ space ~ "]").map(values => Json.Arr(Chunk.fromIterable(values)))
33 |
34 | def pair[P0: P] = P(string.map(_.value) ~/ ":" ~/ jsonExpr)
35 |
36 | def obj[P0: P] =
37 | P("{" ~/ pair.rep(sep = ","./) ~ space ~ "}").map(pairs => Json.Obj(Chunk.fromIterable(pairs)))
38 |
39 | def jsonExpr[P0: P]: P[Json] = P(
40 | space ~ (obj | array | string | `true` | `false` | `null` | number) ~ space
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/zio-parser/shared/src/main/scala/zio/parser/target/ChunkTarget.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.target
2 |
3 | import zio.parser.internal.Stack
4 | import zio.{Chunk, ChunkBuilder}
5 |
6 | class ChunkTarget[Output] extends Target[Output] {
7 | private val builder: ChunkBuilder[Output] = ChunkBuilder.make()
8 | private val captureStack: Stack[Capture] = Stack()
9 | private var currentBuilder: ChunkBuilder[Output] = builder
10 |
11 | override type Capture = ChunkTarget.Capture[Output]
12 |
13 | def result: Chunk[Output] = builder.result()
14 |
15 | override def write(value: Output): Unit = {
16 | currentBuilder += value
17 | ()
18 | }
19 |
20 | override def capture(): Capture = {
21 | val capture = ChunkTarget.Capture(ChunkBuilder.make[Output]())
22 | captureStack.push(capture)
23 | currentBuilder = capture.subBuilder
24 | capture
25 | }
26 |
27 | override def emit(capture: Capture): Unit = {
28 | val popped = captureStack.pop()
29 | if (popped != capture) {
30 | throw new IllegalStateException(s"emit called on a capture group not on top of the stack")
31 | }
32 | if (!captureStack.isEmpty) {
33 | currentBuilder = captureStack.peek().subBuilder
34 | } else {
35 | currentBuilder = builder
36 | }
37 | currentBuilder ++= capture.subBuilder.result()
38 | ()
39 | }
40 |
41 | override def drop(capture: Capture): Unit = {
42 | val popped = captureStack.pop()
43 | if (popped != capture) {
44 | throw new IllegalStateException(s"emit called on a capture group not on top of the stack")
45 | }
46 | if (!captureStack.isEmpty) {
47 | currentBuilder = captureStack.peek().subBuilder
48 | } else {
49 | currentBuilder = builder
50 | }
51 | }
52 | }
53 |
54 | object ChunkTarget {
55 | case class Capture[Output](subBuilder: ChunkBuilder[Output])
56 | }
57 |
--------------------------------------------------------------------------------
/benchmarks/src/main/scala/zio/parser/benchmarks/json/JsonAttoParse.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.benchmarks.json
2 |
3 | import atto._, Atto._
4 | import cats.implicits._
5 | import zio.Chunk
6 |
7 | // Based on https://github.com/typelevel/cats-parse/blob/main/bench/src/main/scala/cats/parse/bench/atto.scala
8 | object JsonAttoParse extends Whitespace {
9 | // Bracketed, comma-separated sequence, internal whitespace allowed
10 | def seq[A](open: Char, p: Parser[A], close: Char): Parser[List[A]] =
11 | char(open).t ~> sepByT(p, char(',')) <~ char(close)
12 |
13 | // Colon-separated pair, internal whitespace allowed
14 | lazy val pair: Parser[(String, Json)] =
15 | pairByT(stringLiteral, char(':'), jexpr)
16 |
17 | // Json Expression
18 | lazy val jexpr: Parser[Json] = delay {
19 | stringLiteral -| Json.Str.apply |
20 | seq('{', pair, '}') -| (pairs => Json.Obj(Chunk.fromIterable(pairs))) |
21 | seq('[', jexpr, ']') -| (values => Json.Arr(Chunk.fromIterable(values))) |
22 | double -| (s => Json.Num(BigDecimal(s))) |
23 | string("null") >| Json.Null |
24 | string("true") >| Json.Bool(true) |
25 | string("false") >| Json.Bool(false)
26 | }
27 |
28 | }
29 |
30 | // Some extre combinators and syntax for coping with whitespace. Something like this might be
31 | // useful in core but it needs some thought.
32 | trait Whitespace {
33 |
34 | // Syntax for turning a parser into one that consumes trailing whitespace
35 | implicit class TokenOps[A](self: Parser[A]) {
36 | def t: Parser[A] =
37 | self <~ takeWhile(c => c.isSpaceChar || c === '\n')
38 | }
39 |
40 | // Delimited list
41 | def sepByT[A](a: Parser[A], b: Parser[_]): Parser[List[A]] =
42 | sepBy(a.t, b.t)
43 |
44 | // Delimited pair, internal whitespace allowed
45 | def pairByT[A, B](a: Parser[A], delim: Parser[_], b: Parser[B]): Parser[(A, B)] =
46 | pairBy(a.t, delim.t, b)
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/benchmarks/src/main/scala/zio/parser/benchmarks/ParserBenchmarkTestRunner.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.benchmarks
2 |
3 | import zio.parser.internal.Debug
4 |
5 | abstract class ParserBenchmarkTestRunner[T, B <: ParserBenchmark[T]] {
6 | val self: ParserBenchmark[T]
7 |
8 | def resultToString(value: T): String =
9 | value.toString
10 |
11 | def main(args: Array[String]): Unit = {
12 | self.setUp()
13 | println(s"input: ${self.value}")
14 | println()
15 | // Console.in.readLine()
16 | // while(true) self.zioParserOnString()
17 | // Debug.printSyntaxTree(self.zioSyntax)
18 | // println("---")
19 | Debug.printParserTree(self.zioSyntax.asParser.optimized)
20 | //
21 | // val builder = new VMBuilder
22 | // builder.add(self.zioSyntax.optimized.asInstanceOf[ErasedSyntax])
23 | // println(builder.result())
24 | //
25 | println(s"Cats Parser result: ${self.catsParse().map(resultToString)}")
26 | // println(s"ZIO Parser result: ${self.zioParserRecursiveOnChunk().map(resultToString)}")
27 | // println(s"ZIO Parser result: ${self.zioParserRecursiveUnitOnChunk().map(resultToString)}")
28 | println(s"ZIO Parser result: ${self.zioParserOpStack().map(resultToString)}")
29 | println(s"ZIO Parser result: ${self.zioParserRecursive().map(resultToString)}")
30 | // Console.in.readLine()
31 | // println(s"Cats Parse result: ${self.catsParse().map(resultToString)}")
32 | // println(
33 | // s"Fastparse result: ${self.fastParse().fold((s, n, e) => s"Left(${(s, n, e)})", (v, _) => s"Right(${resultToString(v)})")}"
34 | // )
35 | // println(s"Atto result: ${self.attoParse().either.map(resultToString)}")
36 | // println(s"Parboiled result: ${self.parboiledParse().toEither.map(resultToString)}")
37 | // println(s"Parsley result: ${self.parsleyParse().toEither.map(resultToString)}")
38 | // println(s"Parserz result: ${self.parserzParse()._2.map(t => resultToString(t._2))}")
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/benchmarks/src/main/scala/zio/parser/benchmarks/json/JsonZioParser.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.benchmarks.json
2 |
3 | import zio.Chunk
4 | import zio.parser._
5 |
6 | import scala.util.Try
7 |
8 | object JsonZioParser {
9 | // TODO: this is not a proper JSON parser yet, to be fixed (numbers, escaping)
10 |
11 | val whitespaces = Syntax.charIn(' ', '\t', '\r', '\n').*.unit(Chunk(' '))
12 |
13 | val quote = Syntax.char('\"')
14 | val escapedChar = Syntax.charNotIn('\"') // TODO: real escaping support
15 |
16 | val quotedString = (quote ~> escapedChar.*.string <~ quote)
17 |
18 | val nul = Syntax.string("null", Json.Null)
19 |
20 | val bool =
21 | Syntax.string("true", Json.Bool(true)) <>
22 | Syntax.string("false", Json.Bool(false))
23 |
24 | val str = quotedString
25 | .transform(Json.Str.apply, (s: Json.Str) => s.value)
26 |
27 | val digits = Syntax.digit.repeat
28 | val signedIntStr = Syntax.char('-').? ~ digits
29 | val frac = Syntax.char('.') ~> digits
30 | val exp = Syntax.charIn('e', 'E') ~ Syntax.charIn('+', '-') ~ digits
31 | val jsonNum = (signedIntStr ~ frac.? ~ exp.?).string
32 |
33 | val num = jsonNum
34 | .transform(
35 | s => Json.Num(BigDecimal(s)),
36 | (v: Json.Num) => v.value.toString()
37 | )
38 |
39 | val listSep = Syntax.char(',').surroundedBy(whitespaces)
40 | lazy val list = (Syntax.char('[') ~> json.repeatWithSep0(listSep) <~ Syntax.char(']'))
41 | .transform(Json.Arr, (arr: Json.Arr) => arr.elements)
42 |
43 | val keyValueSep = Syntax.char(':').surroundedBy(whitespaces)
44 | lazy val keyValue = (str ~ keyValueSep ~ json).transform[(String, Json)](
45 | { case (key, value) => (key.value, value) },
46 | { case (key, value) => (Json.Str(key), value) }
47 | )
48 | val obj = (Syntax.char('{') ~>
49 | keyValue.repeatWithSep0(listSep).surroundedBy(whitespaces) <~
50 | Syntax.char('}'))
51 | .transform(Json.Obj, (arr: Json.Obj) => arr.fields)
52 |
53 | lazy val json: Syntax[String, Char, Char, Json] =
54 | (nul.widen[Json] <>
55 | bool.widen[Json] <>
56 | str.widen[Json] <>
57 | num.widen[Json] <>
58 | list.widen[Json] <>
59 | obj.widen[Json]).manualBacktracking
60 | }
61 |
--------------------------------------------------------------------------------
/benchmarks/src/test/scala/zio/parser/benchmarks/BenchmarksSpec.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.benchmarks
2 |
3 | import zio._
4 | import zio.parser.ParserImplementation
5 | import zio.parser.benchmarks.basic.{RepeatAnyChar, StringAlternatives, Zipping, Zipping16}
6 | import zio.parser.benchmarks.json._
7 | import zio.parser.benchmarks.lucene.LuceneQueryBenchmark
8 | import zio.test.Assertion.{anything, isRight}
9 | import zio.test._
10 |
11 | /** Tests whether the benchmark examples run with success */
12 | object BenchmarksSpec extends ZIOSpecDefault {
13 | override def spec: Spec[TestEnvironment, Any] =
14 | suite("Benchmarks")(
15 | suite("basic")(
16 | testParserBenchmark("RepeatAnyChar", new RepeatAnyChar),
17 | testParserBenchmark("StringAlternatives", new StringAlternatives),
18 | testParserBenchmark("Zipping", new Zipping),
19 | testParserBenchmark("Zipping16", new Zipping16)
20 | ),
21 | suite("json")(
22 | testParserBenchmark("BarBench", new BarBench),
23 | testParserBenchmark("Qux2Bench", new Qux2Bench),
24 | testParserBenchmark("Bla25Bench", new Bla25Bench),
25 | testParserBenchmark(
26 | "CountriesBench",
27 | new CountriesBench
28 | ) @@ TestAspect.ignore, // TODO: figure out why parser fails
29 | testParserBenchmark("Ugh10kBench", new Ugh10kBench)
30 | ),
31 | suite("lucene")(
32 | test("stacksafe") {
33 | val benchmark = new LuceneQueryBenchmark
34 | benchmark.setUp()
35 | assert(benchmark.zioParseStrippedOpStack())(isRight(anything))
36 | },
37 | test("recursive") {
38 | val benchmark = new LuceneQueryBenchmark
39 | benchmark.setUp()
40 | assert(benchmark.zioParseStrippedRecursive())(isRight(anything))
41 | }
42 | )
43 | ) @@ TestAspect.timeout(90.seconds)
44 |
45 | private def testParserBenchmark[T](
46 | name: String,
47 | create: => ParserBenchmark[T]
48 | ): Spec[Any, Nothing] = {
49 | val benchmark = create
50 | benchmark.setUp()
51 |
52 | suite(name)(
53 | test("stacksafe") {
54 | assert(benchmark.zioSyntax.parseString(benchmark.value, ParserImplementation.StackSafe))(isRight(anything))
55 | },
56 | test("recursive") {
57 | assert(benchmark.zioSyntax.parseString(benchmark.value, ParserImplementation.Recursive))(isRight(anything))
58 | }
59 | )
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/zio-parser/shared/src/test/scala/zio/parser/examples/ExpressionExample.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.examples
2 |
3 | import zio.parser.{Syntax, _}
4 | import zio.test.Assertion._
5 | import zio.test._
6 |
7 | object ExpressionExample extends ZIOSpecDefault {
8 |
9 | sealed trait OpType
10 | case object Add extends OpType
11 | case object Sub extends OpType
12 |
13 | sealed trait Expr
14 | case class Const(value: Int) extends Expr
15 | case class Op(operator: OpType, a: Expr, b: Expr) extends Expr
16 |
17 | val const: Syntax[String, Char, Char, Expr] = Syntax.anyChar
18 | .filter[String, Char](_.isDigit, "not a digit")
19 | .repeat
20 | .string
21 | .transformTo[String, Expr](
22 | s => Const(s.toInt),
23 | { case (n: Const) => n.value.toString },
24 | "Not a constant"
25 | ) ?? "constant"
26 | val operator: Syntax[String, Char, Char, OpType] =
27 | (Syntax.charIn('+').transformTo[String, OpType](_ => Add, { case Add => '+' }, "Not +") <>
28 | Syntax.charIn('-').transformTo[String, OpType](_ => Sub, { case Sub => '-' }, "Not -")) ?? "operator"
29 | val lparen: Syntax[String, Char, Char, Unit] = Syntax.char('(')
30 | val rparen: Syntax[String, Char, Char, Unit] = Syntax.char(')')
31 |
32 | lazy val subExpr: Syntax[String, Char, Char, Expr] =
33 | (expr ~ operator ~ expr)
34 | .transformTo[String, Expr](
35 | { case (a, op, b) =>
36 | Op(op, a, b)
37 | },
38 | { case (op: Op) => (op.a, op.operator, op.b) },
39 | "Not valid sub expression"
40 | ) ?? "sub expression"
41 |
42 | lazy val subExprInParens: Syntax[String, Char, Char, Expr] = (lparen ~> subExpr <~ rparen)
43 |
44 | lazy val expr: Syntax[String, Char, Char, Expr] =
45 | (subExprInParens | const) ?? "expression"
46 |
47 | val exampleExpr: Expr = Op(
48 | Sub,
49 | Op(Add, Op(Sub, Op(Add, Const(123), Const(456)), Const(789)), Op(Add, Const(0), Op(Add, Const(1), Const(2)))),
50 | Const(3)
51 | )
52 |
53 | override def spec: Spec[Environment, Any] =
54 | suite("Expression example")(
55 | test("Parses expression correctly") {
56 | // Debug.printParserTree(expr.asParser.optimized)
57 | val ast = expr.parseString("((((123+456)-789)+(0+(1+2)))-3)")
58 | assert(ast)(isRight(equalTo(exampleExpr)))
59 | },
60 | test("Prints expression correctly") {
61 | assert(expr.printString(exampleExpr))(isRight(equalTo("((((123+456)-789)+(0+(1+2)))-3)")))
62 | }
63 | )
64 | }
65 |
--------------------------------------------------------------------------------
/zio-parser/shared/src/main/scala/zio/parser/target/ZStreamTarget.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.target
2 |
3 | import zio.parser.internal.Stack
4 | import zio.stream.ZStream
5 | import zio.{ChunkBuilder, Queue, Runtime, UIO, Unsafe, ZIO}
6 |
7 | class ZStreamTarget[R, Output](runtime: Runtime[R]) extends Target[Output] {
8 | private val queue: Queue[Output] = Unsafe.unsafe { implicit u =>
9 | runtime.unsafe.run(Queue.unbounded[Output]).getOrThrowFiberFailure()
10 | }
11 |
12 | val stream: ZStream[Any, Nothing, Output] = ZStream.fromQueue(queue)
13 |
14 | private val captureStack: Stack[Capture] = Stack()
15 | private var currentBuilder: ChunkBuilder[Output] = _
16 |
17 | override type Capture = ZStreamTarget.Capture[Output]
18 |
19 | override def write(value: Output): Unit =
20 | if (currentBuilder == null) {
21 | Unsafe.unsafe { implicit u =>
22 | runtime.unsafe.run(queue.offer(value).unit).getOrThrowFiberFailure()
23 | }
24 | } else {
25 | currentBuilder += value
26 | ()
27 | }
28 |
29 | override def capture(): Capture = {
30 | val capture = ZStreamTarget.Capture(ChunkBuilder.make[Output]())
31 | captureStack.push(capture)
32 | currentBuilder = capture.subBuilder
33 | capture
34 | }
35 |
36 | override def emit(capture: Capture): Unit = {
37 | val popped = captureStack.pop()
38 | if (popped != capture) {
39 | throw new IllegalStateException(s"emit called on a capture group not on top of the stack")
40 | }
41 | if (!captureStack.isEmpty) {
42 | currentBuilder = captureStack.peek().subBuilder
43 | currentBuilder ++= capture.subBuilder.result()
44 | ()
45 | } else {
46 | currentBuilder = null
47 | Unsafe.unsafe { implicit u =>
48 | runtime.unsafe.run(queue.offerAll(capture.subBuilder.result()).unit).getOrThrowFiberFailure()
49 | }
50 | }
51 | }
52 |
53 | override def drop(capture: Capture): Unit = {
54 | val popped = captureStack.pop()
55 | if (popped != capture) {
56 | throw new IllegalStateException(s"emit called on a capture group not on top of the stack")
57 | }
58 | if (!captureStack.isEmpty) {
59 | currentBuilder = captureStack.peek().subBuilder
60 | } else {
61 | currentBuilder = null
62 | }
63 | }
64 | }
65 |
66 | object ZStreamTarget {
67 | case class Capture[Output](subBuilder: ChunkBuilder[Output])
68 |
69 | def apply[Output]: UIO[ZStreamTarget[Any, Output]] =
70 | ZIO.runtime[Any].map { runtime =>
71 | new ZStreamTarget[Any, Output](runtime)
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/benchmarks/src/main/scala/zio/parser/benchmarks/basic/Zipping.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.benchmarks.basic
2 |
3 | import zio.Chunk
4 | import zio.parser.benchmarks.{ParserBenchmark, ParserBenchmarkTestRunner, Parserz}
5 | import zio.parser.internal.Debug
6 |
7 | import scala.util.{Random, Try}
8 |
9 | class Zipping extends ParserBenchmark[Zips] {
10 | override def loadInput(): String = {
11 | val N = 1000
12 | val sb = new StringBuilder
13 | for (_ <- 1 to N) {
14 | val a = "A" + Random.alphanumeric.take(5).mkString
15 | val b = "B" + Random.alphanumeric.take(5).mkString
16 | val c = "C" + Random.alphanumeric.take(5).mkString
17 | val d = "D" + Random.alphanumeric.take(5).mkString
18 | sb.append(s"$a,$b,$c,$d\n")
19 | }
20 | sb.toString()
21 | }
22 |
23 | override final val zioSyntax: zio.parser.Syntax[String, Char, Char, Zips] = {
24 | import zio.parser._
25 |
26 | val item = Syntax
27 | .charNotIn(',', '\n')
28 | .repeat
29 | .string
30 | val sep = Syntax.char(',')
31 | val tuple = (item <~ sep) ~ (item <~ sep) ~ (item <~ sep) ~ item
32 | val zip = tuple.transform(
33 | { case (as, bs, cs, ds) => Zip(as, bs, cs, ds) },
34 | (zip: Zip) => (zip.a, zip.b, zip.c, zip.d)
35 | )
36 | val line = zip <~ Syntax.char('\n')
37 | val lines = line.repeat0.transform[Zips](Zips.apply, z => Chunk.fromIterable(z.zips)).manualBacktracking
38 | lines
39 | }
40 |
41 | override final val catParser: cats.parse.Parser0[Zips] = {
42 | import cats.parse._
43 |
44 | val item = Parser.charsWhile(ch => ch != ',' && ch != '\n')
45 | val sep = Parser.char(',')
46 | val tuple = (item <* sep) ~ (item <* sep) ~ (item <* sep) ~ item
47 | val zip = tuple.map { case (((as, bs), cs), ds) => Zip(as, bs, cs, ds) }
48 | val line = zip <* Parser.char('\n')
49 | val lines = line.rep0.map(lst => Zips(lst))
50 | lines
51 | }
52 |
53 | // TODO: implement for all
54 |
55 | override def fastParseP[P: fastparse.P]: fastparse.P[Zips] = ???
56 |
57 | override val attoParser: atto.Parser[Zips] = null
58 |
59 | override def runParboiledParser(input: String): Try[Zips] = ???
60 |
61 | override val parsley: org.http4s.parsley.Parsley[Zips] = null
62 | override val parserz: Parserz.Grammar[Any, Nothing, String, Zips] = null
63 | }
64 |
65 | case class Zip(a: String, b: String, c: String, d: String)
66 | case class Zips(zips: Seq[Zip])
67 |
68 | object Zipping extends ParserBenchmarkTestRunner[Zips, Zipping] {
69 | override val self: ParserBenchmark[Zips] = new Zipping
70 | }
71 |
--------------------------------------------------------------------------------
/zio-parser/shared/src/main/scala/zio/parser/internal/Stack.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.internal
2 |
3 | /** A very fast, growable/shrinkable, mutable stack.
4 | *
5 | * Based on zio.internal.Stack
6 | */
7 | private[zio] final class Stack[A <: AnyRef]() {
8 | private[this] var array = new Array[AnyRef](13)
9 | private[this] var size = 0
10 | private[this] var nesting = 0
11 |
12 | /** Determines if the stack is empty.
13 | */
14 | def isEmpty: Boolean = size == 0
15 |
16 | /** Pushes an item onto the stack.
17 | */
18 | def push(a: A): Unit =
19 | if (size == 13) {
20 | array = Array(array, a, null, null, null, null, null, null, null, null, null, null, null)
21 | size = 2
22 | nesting += 1
23 | } else {
24 | array(size) = a
25 | size += 1
26 | }
27 |
28 | /** Pops an item off the stack, or returns `null` if the stack is empty.
29 | */
30 | def pop(): A =
31 | if (size <= 0) {
32 | null.asInstanceOf[A]
33 | } else {
34 | val idx = size - 1
35 | var a = array(idx)
36 | if (idx == 0 && nesting > 0) {
37 | array = a.asInstanceOf[Array[AnyRef]]
38 | a = array(12)
39 | array(12) = null // GC
40 | size = 12
41 | nesting -= 1
42 | } else {
43 | array(idx) = null // GC
44 | size = idx
45 | }
46 | a.asInstanceOf[A]
47 | }
48 |
49 | /** Peeks the item on the head of the stack, or returns `null` if empty.
50 | */
51 | def peek(): A =
52 | if (size <= 0) {
53 | null.asInstanceOf[A]
54 | } else {
55 | val idx = size - 1
56 | var a = array(idx)
57 | if (idx == 0 && nesting > 0) a = (a.asInstanceOf[Array[AnyRef]])(12)
58 | a.asInstanceOf[A]
59 | }
60 |
61 | def peekOrElse(a: A): A = if (size <= 0) a else peek()
62 |
63 | override def clone(): Stack[A] = {
64 | val cloned = new Stack[A]
65 | cloned.cloneFrom(array, size, nesting)
66 | cloned
67 | }
68 |
69 | private def cloneFrom(otherArray: Array[AnyRef], otherSize: Int, otherNesting: Int): Unit = {
70 | this.size = otherSize
71 | this.nesting = otherNesting
72 | this.array = otherArray.clone()
73 | var idx = 0
74 | var arr = this.array
75 | while (idx < nesting) {
76 | val c = arr(0).asInstanceOf[Array[AnyRef]].clone()
77 | arr(0) = c
78 | arr = c
79 | idx = idx + 1
80 | }
81 | }
82 | }
83 |
84 | private[zio] object Stack {
85 | def apply[A <: AnyRef](): Stack[A] = new Stack[A]
86 | def apply[A <: AnyRef](a: A): Stack[A] = {
87 | val stack = new Stack[A]
88 |
89 | stack.push(a)
90 |
91 | stack
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/benchmarks/src/main/scala/zio/parser/benchmarks/json/JsonParserBenchmark.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.benchmarks.json
2 |
3 | import atto.Parser
4 | import cats.parse.Parser0
5 | import fastparse.P
6 | import org.http4s.parsley.Parsley
7 | import zio.parser.Syntax
8 | import zio.parser.benchmarks.{ParserBenchmark, ParserBenchmarkTestRunner, Parserz}
9 |
10 | import scala.io.Source
11 | import scala.util.Try
12 |
13 | /** JSON benchmark suite from cats-parse
14 | */
15 | abstract class JsonParserBenchmark(fileName: String) extends ParserBenchmark[Json] {
16 | override def loadInput(): String =
17 | Source.fromResource(fileName).getLines().mkString("\n")
18 |
19 | override final val zioSyntax: Syntax[String, Char, Char, Json] = JsonZioParser.json
20 |
21 | override final val catParser: Parser0[Json] = JsonCatsParse.parser
22 |
23 | override final def fastParseP[P0: P]: P[Json] = JsonFastParse.jsonExpr
24 |
25 | override final val attoParser: Parser[Json] = JsonAttoParse.jexpr
26 |
27 | override final def runParboiledParser(input: String): Try[Json] = {
28 | val parser = new JsonParboiled(input)
29 | parser.JSON.run()
30 | }
31 |
32 | override final val parsley: Parsley[Json] = JsonParsley.json
33 |
34 | override final val parserz: Parserz.Grammar[Any, Nothing, String, Json] =
35 | JsonParserz.js
36 | }
37 |
38 | class BarBench extends JsonParserBenchmark("bar.json")
39 | object BarBench extends ParserBenchmarkTestRunner[Json, BarBench] {
40 | override val self: ParserBenchmark[Json] = new BarBench
41 |
42 | override def resultToString(value: Json): String = "..."
43 | }
44 |
45 | // TODO: fix atto failure
46 | class Qux2Bench extends JsonParserBenchmark("qux2.json")
47 | object Qux2Bench extends ParserBenchmarkTestRunner[Json, Qux2Bench] {
48 | override val self: ParserBenchmark[Json] = new Qux2Bench
49 |
50 | override def resultToString(value: Json): String = "..."
51 | }
52 |
53 | // TODO: fix parserz failure
54 | class Bla25Bench extends JsonParserBenchmark("bla25.json")
55 | object Bla25Bench extends ParserBenchmarkTestRunner[Json, Bla25Bench] {
56 | override val self: ParserBenchmark[Json] = new Bla25Bench
57 |
58 | override def resultToString(value: Json): String = "..."
59 | }
60 |
61 | // TODO: fix atto, zio and cats-parse failures
62 | class CountriesBench extends JsonParserBenchmark("countries.geo.json")
63 | object CountriesBench extends ParserBenchmarkTestRunner[Json, CountriesBench] {
64 | override val self: ParserBenchmark[Json] = new CountriesBench
65 |
66 | override def resultToString(value: Json): String = "..."
67 | }
68 |
69 | class Ugh10kBench extends JsonParserBenchmark("ugh10k.json")
70 | object Ugh10kBench extends ParserBenchmarkTestRunner[Json, Ugh10kBench] {
71 | override val self: ParserBenchmark[Json] = new Ugh10kBench
72 |
73 | override def resultToString(value: Json): String = "..."
74 | }
75 |
--------------------------------------------------------------------------------
/benchmarks/src/main/scala/zio/parser/benchmarks/basic/RepeatAnyChar.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.benchmarks.basic
2 | import zio.parser.benchmarks.{ParserBenchmark, ParserBenchmarkTestRunner, Parserz}
3 |
4 | import scala.util.Try
5 |
6 | /** Simple basic benchmark parsing (any.repeat1).map(case class)
7 | */
8 | class RepeatAnyChar extends ParserBenchmark[Chars] {
9 | import RepeatAnyChar._
10 |
11 | override final def loadInput(): String = "hello" * 1000
12 |
13 | override final val zioSyntax: zio.parser.Syntax[String, Char, Char, Chars] = {
14 | import zio.Chunk
15 | import zio.parser._
16 |
17 | Syntax.anyChar.repeat
18 | .transform[Chars](
19 | chunk => Chars(chunk.toSeq),
20 | chars => Chunk.fromIterable(chars.chars)
21 | )
22 | .manualBacktracking
23 | }
24 |
25 | override final val catParser: cats.parse.Parser[Chars] = {
26 | import cats.parse._
27 | Parser.anyChar.rep.map(nel => Chars(nel.toList))
28 | }
29 |
30 | override final def fastParseP[P: fastparse.P]: fastparse.P[Chars] = {
31 | import fastparse._
32 | import NoWhitespace._
33 |
34 | SingleChar.rep.map(Chars.apply)
35 | }
36 |
37 | override final val attoParser: atto.Parser[Chars] = {
38 | import atto._
39 | import Atto._
40 |
41 | many1(anyChar).map(nel => Chars(nel.toList))
42 | }
43 |
44 | override final def runParboiledParser(input: String): Try[Chars] = {
45 | import org.parboiled2._
46 | val parser = new ParboiledTest(input)
47 | parser.InputLine.run()
48 | }
49 |
50 | override final val parsley: org.http4s.parsley.Parsley[Chars] = {
51 | import org.http4s.parsley._
52 | import org.http4s.parsley.Parsley._
53 | import org.http4s.parsley.Combinator._
54 |
55 | many(Char.anyChar).map(Chars.apply)
56 | }
57 |
58 | override final val parserz: Parserz.Grammar[Any, Nothing, String, Chars] = {
59 | import Parserz._
60 | import Parserz.Grammar._
61 |
62 | val anyChar: Grammar[Any, Nothing, String, Char] = consume(
63 | {
64 | case c :: cs => Right(cs -> c)
65 | case Nil => Left("eoi")
66 | },
67 | { case (cs, c) =>
68 | Right(c :: cs)
69 | }
70 | )
71 | anyChar.rep1.map(
72 | Chars.apply,
73 | _.chars.toList.asInstanceOf[::[Char]]
74 | )
75 | }
76 | }
77 |
78 | case class Chars(chars: Seq[Char]) {
79 | override def toString: String = chars.mkString
80 | }
81 |
82 | object RepeatAnyChar extends ParserBenchmarkTestRunner[Chars, RepeatAnyChar] {
83 | override val self: ParserBenchmark[Chars] = new RepeatAnyChar
84 |
85 | class ParboiledTest(val input: org.parboiled2.ParserInput) extends org.parboiled2.Parser {
86 | import org.parboiled2._
87 |
88 | def InputLine = rule(Chs ~ EOI)
89 | def Chs = rule(capture(oneOrMore(ANY)) ~> ((s: String) => Chars(s)))
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [//]: # (This file was autogenerated using `zio-sbt-website` plugin via `sbt generateReadme` command.)
2 | [//]: # (So please do not edit it manually. Instead, change "docs/index.md" file or sbt setting keys)
3 | [//]: # (e.g. "readmeDocumentation" and "readmeSupport".)
4 |
5 | # ZIO Parser
6 |
7 | Library for constructing parsers and pretty printers based on invertible syntax descriptions
8 |
9 | [](https://github.com/zio/zio/wiki/Project-Stages)  [](https://oss.sonatype.org/content/repositories/releases/dev/zio/zio-parser_3/) [](https://oss.sonatype.org/content/repositories/snapshots/dev/zio/zio-parser_3/) [](https://javadoc.io/doc/dev.zio/zio-parser-docs_3) [](https://github.com/zio/zio-parser)
10 |
11 | ## Introduction
12 |
13 | [](https://www.youtube.com/watch?v=DEPpL9LBiyA)
14 |
15 | ## Installation
16 |
17 | Start by adding `zio-parser` as a dependency to your project:
18 |
19 | ```scala
20 | libraryDependencies += "dev.zio" %% "zio-parser" % "0.1.9"
21 | ```
22 |
23 | ## Getting Started
24 |
25 | ```scala
26 | import zio.parser.*
27 | ```
28 |
29 | Declare your parsing syntax:
30 |
31 | ```scala
32 | val digitSyntax: Syntax[String, Char, Char, Char] = Syntax.digit
33 | ```
34 |
35 | Parse your string:
36 |
37 | ```scala
38 | val result: Either[StringParserError[String], Char] = digitSyntax.parseString("1")
39 | // result: Either[StringParserError[String], Char] = Right(value = '1')
40 | ```
41 |
42 | Pretty print the parsing errors:
43 |
44 | ```scala
45 | println(digitSyntax.parseString("Hello").left.map(_.pretty).merge)
46 | // Hello
47 | // ^
48 | // error: Failure at position 0: not a digit
49 | //
50 | ```
51 |
52 | [//]: # (TODO: Add example section)
53 | [//]: # (## Example)
54 |
55 | ## Documentation
56 |
57 | Learn more on the [ZIO Parser homepage](https://zio.dev/zio-parser/)!
58 |
59 | ## Contributing
60 |
61 | For the general guidelines, see ZIO [contributor's guide](https://zio.dev/about/contributing).
62 |
63 | ## Code of Conduct
64 |
65 | See the [Code of Conduct](https://zio.dev/about/code-of-conduct)
66 |
67 | ## Support
68 |
69 | Come chat with us on [![Badge-Discord]][Link-Discord].
70 |
71 | [Badge-Discord]: https://img.shields.io/discord/629491597070827530?logo=discord "chat on discord"
72 | [Link-Discord]: https://discord.gg/2ccFBr4 "Discord"
73 |
74 | ## License
75 |
76 | [License](LICENSE)
77 |
--------------------------------------------------------------------------------
/benchmarks/src/main/scala/zio/parser/benchmarks/lucene/model.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.benchmarks.lucene
2 |
3 | sealed trait Query
4 | object Query {
5 | sealed trait TopLevelCombinatorQuery extends Query {
6 | def normalize: Query
7 | }
8 | final case class Or(queries: List[Query]) extends TopLevelCombinatorQuery {
9 | def normalize: Query =
10 | queries match {
11 | case Nil => this
12 | case head :: Nil => head
13 | case _ =>
14 | Or(queries.map {
15 | case Require(inner) => inner
16 | case Prohibit(inner) => Not(inner)
17 | case other: Query => other
18 | })
19 | }
20 | }
21 | final case class And(queries: List[Query]) extends TopLevelCombinatorQuery {
22 | def normalize: Query =
23 | queries match {
24 | case Nil => this
25 | case head :: Nil => head
26 | case _ =>
27 | And(queries.map {
28 | case Require(inner) => inner
29 | case Prohibit(inner) => Not(inner)
30 | case other: Query => other
31 | })
32 | }
33 | }
34 | final case class Not(query: Query) extends Query
35 | final case class Require(query: Query) extends Query
36 | final case class Prohibit(query: Query) extends Query
37 | final case class Boost(query: Query, score: Double) extends Query
38 | final case class Range(lower: Boundary, upper: Boundary) extends Query
39 | final case class Regex(value: String) extends Query
40 | final case class Term(value: String, maxEditDistances: Option[Int]) extends Query
41 | final case class Phrase(value: String, maxDistance: Option[Int]) extends Query
42 | final case class Field(fieldName: String, query: Query) extends Query {
43 | def normalize: Query =
44 | query match {
45 | case Boost(And(queries), boost) =>
46 | Boost(And(queries.map(Field(fieldName, _))), boost)
47 | case And(queries) =>
48 | And(queries.map(Field(fieldName, _)))
49 | case Boost(Or(queries), boost) =>
50 | Boost(Or(queries.map(Field(fieldName, _))), boost)
51 | case Or(queries) =>
52 | Or(queries.map(Field(fieldName, _)))
53 | case _ => this
54 | }
55 | }
56 | }
57 |
58 | sealed trait Boundary
59 | object Boundary {
60 | final case class Bound(value: String, boundType: BoundType) extends Boundary
61 | case object Unbound extends Boundary
62 | }
63 |
64 | sealed trait BoundType
65 | object BoundType {
66 | case object Inclusive extends BoundType
67 | case object Exclusive extends BoundType
68 | }
69 |
--------------------------------------------------------------------------------
/zio-parser/shared/src/test/scala/zio/parser/SyntaxSpec.scala:
--------------------------------------------------------------------------------
1 | package zio.parser
2 |
3 | import zio._
4 | import zio.parser.Parser.ParserError.AllBranchesFailed
5 | import zio.test.Assertion._
6 | import zio.test._
7 |
8 | sealed trait WeekDay {
9 | self =>
10 | override def toString: String =
11 | self match {
12 | case WeekDay.Monday => "Monday"
13 | case WeekDay.Tuesday => "Tuesday"
14 | case WeekDay.Wednesday => "Wednesday"
15 | case WeekDay.Thursday => "Thursday"
16 | case WeekDay.Friday => "Friday"
17 | case WeekDay.Saturday => "Saturday"
18 | case WeekDay.Sunday => "Sunday"
19 | }
20 | }
21 |
22 | object WeekDay {
23 | case object Monday extends WeekDay
24 |
25 | case object Tuesday extends WeekDay
26 |
27 | case object Wednesday extends WeekDay
28 |
29 | case object Thursday extends WeekDay
30 |
31 | case object Friday extends WeekDay
32 |
33 | case object Saturday extends WeekDay
34 |
35 | case object Sunday extends WeekDay
36 |
37 | val mondaySyntax: Syntax[String, Char, Char, Monday.type] = Syntax.string("Mon", Monday)
38 | val tuesdaySyntax: Syntax[String, Char, Char, Tuesday.type] = Syntax.string("Tue", Tuesday)
39 | val wednesdaySyntax: Syntax[String, Char, Char, Wednesday.type] = Syntax.string("Wed", Wednesday)
40 | val thursdaySyntax: Syntax[String, Char, Char, Thursday.type] = Syntax.string("Thu", Thursday)
41 | val fridaySyntax: Syntax[String, Char, Char, Friday.type] = Syntax.string("Fri", Friday)
42 | val saturdaySyntax: Syntax[String, Char, Char, Saturday.type] = Syntax.string("Sat", Saturday)
43 | val sundaySyntax: Syntax[String, Char, Char, Sunday.type] = Syntax.string("Sun", Sunday)
44 |
45 | val arbitrary: Gen[Any, WeekDay] =
46 | Gen.oneOf(
47 | Gen.const(Monday),
48 | Gen.const(Tuesday),
49 | Gen.const(Wednesday),
50 | Gen.const(Thursday),
51 | Gen.const(Friday),
52 | Gen.const(Saturday),
53 | Gen.const(Sunday)
54 | )
55 |
56 | val weekDaySyntax: Syntax[String, Char, Char, WeekDay] = Syntax.oneOf(
57 | mondaySyntax,
58 | tuesdaySyntax,
59 | wednesdaySyntax,
60 | thursdaySyntax,
61 | fridaySyntax,
62 | saturdaySyntax,
63 | sundaySyntax
64 | )
65 | }
66 |
67 | object SyntaxSpec extends ZIOSpecDefault {
68 | override def spec: Spec[TestEnvironment with Scope, Any] =
69 | suite("Syntax")(
70 | test("oneOf can parse sum types") {
71 | check(WeekDay.arbitrary) { day =>
72 | val parsed = WeekDay.weekDaySyntax.parseString(day.toString.take(3))
73 | assert(parsed)(isRight(equalTo(day)))
74 | }
75 | },
76 | test("oneOf can print sum types") {
77 | check(WeekDay.arbitrary) { day =>
78 | val printed = WeekDay.weekDaySyntax.printString(day)
79 | assert(printed)(isRight(equalTo(day.toString.take(3))))
80 | }
81 | },
82 | test("oneOf fails to parse garbage") {
83 | val parsed = WeekDay.weekDaySyntax.parseString("garbage")
84 | assert(parsed.left.map(_.error))(isLeft(isSubtype[AllBranchesFailed[String]](anything)))
85 | }
86 | )
87 | }
88 |
--------------------------------------------------------------------------------
/benchmarks/src/main/scala/zio/parser/benchmarks/ParserBenchmark.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.benchmarks
2 |
3 | import org.openjdk.jmh.annotations._
4 | import org.spartanz.parserz.\/
5 | import zio.Chunk
6 |
7 | import java.util.concurrent.TimeUnit
8 | import scala.util.Try
9 |
10 | @BenchmarkMode(Array(Mode.Throughput))
11 | @OutputTimeUnit(TimeUnit.MILLISECONDS)
12 | @State(Scope.Benchmark)
13 | @Warmup(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
14 | @Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
15 | @Fork(value = 2)
16 | abstract class ParserBenchmark[T] {
17 | def loadInput(): String
18 | val zioSyntax: zio.parser.Syntax[String, Char, Char, T]
19 | val catParser: cats.parse.Parser0[T]
20 | def fastParseP[P: fastparse.P]: fastparse.P[T]
21 | val attoParser: atto.Parser[T]
22 | def runParboiledParser(input: String): Try[T]
23 | val parsley: org.http4s.parsley.Parsley[T]
24 | val parserz: Parserz.Grammar[Any, Nothing, String, T]
25 |
26 | var value: String = _
27 | var valueAsChunk: Chunk[Char] = _
28 |
29 | @Setup
30 | def setUp(): Unit = {
31 | value = loadInput()
32 | valueAsChunk = Chunk.fromArray(value.toCharArray)
33 | zioSyntax.parseString("")
34 | }
35 |
36 | // @Benchmark
37 | // def zioParserExperiment(): Either[String, T] = {
38 | // zioCombinator.parse(0, chunk)
39 | // }
40 |
41 | // @Benchmark
42 | // def zioParserOnString(): Either[zio.parser.Parser.ParserError[String], T] = {
43 | // import zio.parser._
44 | // import zio.parser.source._
45 | // zioSyntax.parseString(value)
46 | // }
47 |
48 | @Benchmark
49 | def zioParserRecursive(): Either[zio.parser.StringParserError[String], T] = {
50 | import zio.parser._
51 | zioSyntax.parseString(value, ParserImplementation.Recursive)
52 | }
53 |
54 | @Benchmark
55 | def zioParserRecursiveOnChunk(): Either[zio.parser.Parser.ParserError[String], T] = {
56 | import zio.parser._
57 | zioSyntax.parseChunk(valueAsChunk)
58 | }
59 |
60 | @Benchmark
61 | def zioParserOpStack(): Either[zio.parser.StringParserError[String], T] = {
62 | import zio.parser._
63 | zioSyntax.parseString(value, ParserImplementation.StackSafe)
64 | }
65 |
66 | @Benchmark
67 | def catsParse(): Either[cats.parse.Parser.Error, T] = {
68 | import cats.parse._
69 | catParser.parseAll(value)
70 | }
71 |
72 | // @Benchmark
73 | // def fastParse(): fastparse.Parsed[T] = {
74 | // import fastparse._
75 | // parse[T](value, fastParseP(_), verboseFailures = true)
76 | // }
77 | //
78 | // @Benchmark
79 | // def attoParse(): atto.ParseResult[T] = {
80 | // import atto._
81 | // import Atto._
82 | // attoParser.parse(value).done
83 | // }
84 | //
85 | // @Benchmark
86 | // def parboiledParse(): Try[T] = {
87 | // runParboiledParser(value)
88 | // }
89 | //
90 | // @Benchmark
91 | // def parsleyParse(): org.http4s.parsley.Result[T] = {
92 | // import org.http4s.parsley._
93 | //
94 | // runParserThreadSafe(parsley, value)
95 | // }
96 | //
97 | // @Benchmark
98 | // def parserzParse(): (Any, \/[String, (List[Char], T)]) = {
99 | // Parserz.parser(parserz)((), value.toList)
100 | // }
101 | }
102 |
--------------------------------------------------------------------------------
/zio-parser/shared/src/main/scala-2/zio/parser/TupleConversion.scala:
--------------------------------------------------------------------------------
1 | package zio.parser
2 |
3 | import scala.annotation.nowarn
4 | import scala.reflect.macros.whitebox
5 |
6 | trait TupleConversion[A, B] {
7 | def to(a: A): B
8 | def from(b: B): A
9 | }
10 |
11 | object TupleConversion {
12 | def apply[P, T]: TupleConversion[P, T] = macro genTupleConversion[P, T]
13 |
14 | @nowarn def genTupleConversion[P: c.WeakTypeTag, T: c.WeakTypeTag](
15 | c: whitebox.Context
16 | ): c.Expr[TupleConversion[P, T]] = {
17 | import c.universe._
18 | val prodTpe = c.weakTypeOf[P]
19 | if (
20 | !prodTpe.typeSymbol.isClass ||
21 | !prodTpe.typeSymbol.asClass.isCaseClass
22 | ) {
23 | c.abort(c.enclosingPosition, s"Type ${prodTpe.typeSymbol} is not a case class")
24 | }
25 | val paramLists = prodTpe.typeSymbol.asClass.primaryConstructor.asMethod.typeSignatureIn(prodTpe).paramLists
26 | val result = paramLists match {
27 | case List(List(singleParam)) =>
28 | // Special case: single parameter products
29 |
30 | val typ = singleParam.typeSignatureIn(prodTpe).finalResultType
31 |
32 | q"""new _root_.zio.parser.TupleConversion[$prodTpe, $typ] {
33 | override def to(a: $prodTpe): $typ = a.${singleParam.name.toTermName}
34 | override def from(b: $typ): $prodTpe =
35 | new ${prodTpe}(b)
36 | }
37 | """
38 |
39 | case List(params) =>
40 | // Generic case: n > 1 parameters
41 |
42 | val tupleName = definitions.TupleClass(params.size).name.toTypeName
43 | val tupleParams = params.map { sym =>
44 | sym.typeSignatureIn(prodTpe).finalResultType
45 | }
46 | val tup = tq"$tupleName[..$tupleParams]"
47 | val packers =
48 | params.map { sym =>
49 | val symTerm = sym.name.toTermName
50 | q"a.$symTerm"
51 | }
52 | val unpackers =
53 | params.indices.map { idx =>
54 | val accessor = TermName(s"_${idx + 1}")
55 | q"b.$accessor"
56 | }
57 |
58 | q"""new _root_.zio.parser.TupleConversion[$prodTpe, $tup] {
59 | override def to(a: $prodTpe): $tup =
60 | (..$packers)
61 | override def from(b: $tup): $prodTpe =
62 | new ${prodTpe}(..$unpackers)
63 | }
64 | """
65 | case Nil =>
66 | // Special case: zero parameter products
67 | q"""new _root_.zio.parser.TupleConversion[$prodTpe, Unit] {
68 | override def to(a: $prodTpe): Unit = ()
69 | override def from(b: Unit): $prodTpe =
70 | new $prodTpe()
71 | }
72 | """
73 | case _ =>
74 | c.abort(
75 | c.enclosingPosition,
76 | s"Type ${prodTpe.typeSymbol} has multiple parameter lists which is currently not supported"
77 | )
78 | }
79 |
80 | c.Expr(result)
81 | }
82 | }
83 | trait ImplicitTupleConversion {
84 | implicit def autoTupleConversion[P <: Product, T]: TupleConversion[P, T] =
85 | macro TupleConversion.genTupleConversion[P, T]
86 |
87 | def autoTupleConversion1[P, A](implicit ev: TupleConversion[P, Tuple1[A]]): TupleConversion[P, A] =
88 | ??? // defined here so it can be used in explicit imports when cross-compiling
89 | }
90 |
--------------------------------------------------------------------------------
/benchmarks/src/main/scala/zio/parser/benchmarks/lucene/LuceneQueryBenchmark.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.benchmarks.lucene
2 |
3 | import cats.parse.Parser
4 | import org.openjdk.jmh.annotations.{
5 | Benchmark,
6 | BenchmarkMode,
7 | Fork,
8 | Measurement,
9 | Mode,
10 | OutputTimeUnit,
11 | Scope,
12 | Setup,
13 | State,
14 | Warmup
15 | }
16 | import zio.Chunk
17 | import zio.parser.{ParserImplementation, Syntax}
18 | import zio.parser.StringParserError
19 | import zio.parser.Parser.ParserError
20 | import zio.parser.internal.Debug
21 |
22 | import java.util.concurrent.TimeUnit
23 |
24 | @BenchmarkMode(Array(Mode.Throughput))
25 | @OutputTimeUnit(TimeUnit.MILLISECONDS)
26 | @State(Scope.Benchmark)
27 | @Warmup(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
28 | @Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
29 | @Fork(value = 1)
30 | class LuceneQueryBenchmark {
31 | var testQuery: String = _
32 | var testQueryChunk: Chunk[Char] = _
33 | var catsParser: CatsLuceneQueryParser = _
34 | var zioParserQuery: Syntax[String, Char, Char, Query] = _
35 | var zioParserStrippedQuery: Syntax[String, Char, Char, Query] = _
36 |
37 | @Setup
38 | def setUp(): Unit = {
39 | testQuery =
40 | "status:(active OR pending) AND title:(full text search)^2 AND date:[2012-01-01 TO 2012-12-31] AND (quikc~ brwn~ foks~)"
41 | testQueryChunk = Chunk.fromArray(testQuery.toCharArray)
42 |
43 | catsParser = new CatsLuceneQueryParser()
44 | val zioParser = new ZioLuceneQueryParser()
45 | zioParserQuery = zioParser.query
46 | zioParserStrippedQuery = zioParser.query.strip
47 | }
48 |
49 | @Benchmark
50 | def catsParse(): Either[Parser.Error, Query] =
51 | catsParser.query.parseAll(testQuery)
52 |
53 | @Benchmark
54 | def zioParse(): Either[StringParserError[String], Query] =
55 | zioParserQuery.parseString(testQuery)
56 |
57 | @Benchmark
58 | def zioParseStrippedRecursive(): Either[StringParserError[String], Query] =
59 | zioParserStrippedQuery.parseString(testQuery, ParserImplementation.Recursive)
60 |
61 | @Benchmark
62 | def zioParseStrippedRecursiveChunk(): Either[ParserError[String], Query] =
63 | zioParserStrippedQuery.parseChunk(testQueryChunk)
64 |
65 | @Benchmark
66 | def zioParseStrippedOpStack(): Either[StringParserError[String], Query] =
67 | zioParserStrippedQuery.parseString(testQuery, ParserImplementation.StackSafe)
68 |
69 | // @Benchmark
70 | // def zioParseStrippedVM(): Either[ParserError[String], Query] =
71 | // zioParserStrippedQuery.parseChars(testQueryChunk, ParserImplementation.VM)
72 | }
73 |
74 | //object LuceneQueryBenchmark extends LuceneQueryBenchmark {
75 | // def main(args: Array[String]): Unit = {
76 | // setUp()
77 | // Debug.printParserTree(zioParserQuery.asParser.optimized)
78 | // println("----")
79 | // Debug.printParserTree(zioParserStrippedQuery.asParser.optimized)
80 | // println(s"ZIO Parser result: ${zioParse()}")
81 | // println(s"ZIO Parser stripped result: ${zioParseStrippedRecursive()}")
82 | // println(s"ZIO Parser stripped op-stack result: ${zioParseStrippedOpStack()}")
83 | // println(s"Cats Parser result: ${catsParse()}")
84 | //
85 | //// val builder = new VMBuilder
86 | //// builder.compile(zioParserQuery.asParser.asInstanceOf[ErasedParser])
87 | //// println(builder.result())
88 | // }
89 | //}
90 |
--------------------------------------------------------------------------------
/benchmarks/src/main/scala/zio/parser/benchmarks/RegexBenchmark.scala:
--------------------------------------------------------------------------------
1 | //package zio.parser.benchmarks
2 | //
3 | //import org.openjdk.jmh.annotations._
4 | //import java.util.concurrent.TimeUnit
5 | //
6 | //import zio.parser.Regex
7 | //
8 | //@BenchmarkMode(Array(Mode.Throughput))
9 | //@OutputTimeUnit(TimeUnit.MILLISECONDS)
10 | //@State(Scope.Benchmark)
11 | //@Warmup(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
12 | //@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
13 | //@Fork(value = 1)
14 | //class RegexBenchmark {
15 | // val keywordStrings =
16 | // List(
17 | // "abstract",
18 | // "case",
19 | // "catch",
20 | // "class",
21 | // "def",
22 | // "do",
23 | // "else",
24 | // "extends",
25 | // "false",
26 | // "final",
27 | // "finally",
28 | // "for",
29 | // "forSome",
30 | // "if",
31 | // "implicit",
32 | // "import",
33 | // "lazy",
34 | // "match",
35 | // "new",
36 | // "null",
37 | // "object",
38 | // "override",
39 | // "package",
40 | // "private",
41 | // "protected",
42 | // "return",
43 | // "sealed",
44 | // "super",
45 | // "this",
46 | // "throw",
47 | // "trait",
48 | // "try",
49 | // "true",
50 | // "type",
51 | // "val",
52 | // "var",
53 | // "while",
54 | // "with",
55 | // "yield"
56 | // )
57 | //
58 | // val keywordsChars = keywordStrings
59 | //
60 | // val keywordsRegex = keywordStrings.map(Regex.string(_)).reduce(_ | _)
61 | //
62 | // val keywordsCompiled = keywordsRegex.compile
63 | //
64 | // val firstKeywordChars = keywordsChars.head
65 | // val lastKeywordChars = keywordsChars.last
66 | //
67 | // val shortLiteral = Regex.string("true").compile
68 | // val longLiteral = Regex.string("supercalifragilisticexpialidocious").compile
69 | //
70 | // val charsFalse = "false"
71 | // val charsTrue = "true"
72 | //
73 | // val charsUnsuper = "superman"
74 | // val charsSuper = "supercalifragilisticexpialidocious"
75 | //
76 | // val shortLiteral2 = Regex.string("true") | Regex.string("false")
77 | //
78 | // val anyChars = Regex.anyChar.atLeast(0).compile
79 | //
80 | // val anyCharsN = Regex.anyChar.atMost(charsSuper.length).compile
81 | //
82 | // @Benchmark
83 | // def shortLiteralNegativeBenchmark() = shortLiteral.matches(charsFalse)
84 | //
85 | // @Benchmark
86 | // def shortLiteralPositiveBenchmark() = shortLiteral.matches(charsTrue)
87 | //
88 | // @Benchmark
89 | // def longLiteralNegativeBenchmark() = longLiteral.matches(charsUnsuper)
90 | //
91 | // @Benchmark
92 | // def longLiteralPositiveBenchmark() = longLiteral.matches(charsSuper)
93 | //
94 | // @Benchmark
95 | // def shortLiteral2PositiveBenchmark() = shortLiteral.matches(charsFalse)
96 | //
97 | // @Benchmark
98 | // def shortLiteral2NegativeBenchmark() = shortLiteral.matches(charsUnsuper)
99 | //
100 | // @Benchmark
101 | // def keywordLastPositiveBenchmark() = keywordsCompiled.matches(lastKeywordChars)
102 | //
103 | // @Benchmark
104 | // def keywordNegativeBenchmark() = keywordsCompiled.matches(charsSuper)
105 | //
106 | // @Benchmark
107 | // def keywordFirstPositiveBenchmark() = keywordsCompiled.matches(firstKeywordChars)
108 | //
109 | // @Benchmark
110 | // def anyCharsBenchmark() = anyChars.matches(charsSuper)
111 | //
112 | // @Benchmark
113 | // def anyCharsNBenchmark() = anyCharsN.matches(charsSuper)
114 | //}
115 |
--------------------------------------------------------------------------------
/zio-parser/shared/src/test/scala/zio/parser/examples/ContextualExample.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.examples
2 |
3 | import zio.parser.Parser.ParserError
4 | import zio.parser.{Parser, Printer, Syntax, _}
5 | import zio.test.Assertion._
6 | import zio.test._
7 |
8 | object ContextualExample extends ZIOSpecDefault {
9 |
10 | // Context sensitive example
11 |
12 | // Model
13 | case class Node(name: String, child: Option[Node])
14 |
15 | // Invertible syntax fragments
16 | val openTag: Syntax[String, Char, Char, String] =
17 | (Syntax.char('<') ~> Syntax.anyChar
18 | .filter[String, Char](_.isLetterOrDigit, "not a letter/digit")
19 | .repeat
20 | .string <~ Syntax.char('>')) ?? "open tag"
21 |
22 | def closeTag(name: String): Syntax[String, Char, Char, Unit] =
23 | Syntax.string(s"$name>", ()) ?? s"close tag ($name)"
24 |
25 | // Separate definition of recursive parser
26 | lazy val nodeParser: Parser[String, Char, Node] =
27 | for {
28 | name <- openTag.asParser
29 | inner <- nodeParser.optional
30 | _ <- closeTag(name).asParser
31 | } yield Node(name, inner)
32 |
33 | // Separate definition of recursive printer
34 | lazy val nodePrinter: Printer[String, Char, Node] = Printer.byValue { (model: Node) =>
35 | (model.child match {
36 | case Some(child) => openTag.asPrinter(model.name) ~> nodePrinter(child) ~> closeTag(model.name).asPrinter(())
37 | case None => openTag.asPrinter(model.name) ~> closeTag(model.name).asPrinter(())
38 | })
39 | }
40 |
41 | // Plugging the two together to get an invertible syntax
42 | lazy val node: Syntax[String, Char, Char, Node] =
43 | nodeParser <=> nodePrinter
44 |
45 | override def spec: Spec[Environment, Any] =
46 | suite("Contextual example")(
47 | suite("Separate parser")(
48 | test("simple") {
49 | assert(nodeParser.parseString(""))(
50 | isRight(equalTo(Node("hello", None)))
51 | )
52 | },
53 | test("nested") {
54 | assert(nodeParser.parseString(""))(
55 | isRight(equalTo(Node("hello", Some(Node("world", None)))))
56 | )
57 | }
58 | ),
59 | suite("Separate printer")(
60 | test("nested") {
61 | assert(nodePrinter.printString(Node("hello", Some(Node("world", None)))))(
62 | isRight(equalTo(""))
63 | )
64 | }
65 | ),
66 | suite("Fused")(
67 | test("parse simple") {
68 | assert(node.parseString(""))(
69 | isRight(equalTo(Node("hello", None)))
70 | )
71 | },
72 | test("parse nested") {
73 | assert(node.parseString(""))(
74 | isRight(equalTo(Node("hello", Some(Node("world", None)))))
75 | )
76 | },
77 | test("parse wrong") {
78 | assert(node.parseString("").left.map(_.error))(
79 | isLeft(equalTo(ParserError.Failure(List("close tag (hello)"), 7, "Not ''")))
80 | )
81 | },
82 | test("print simple") {
83 | assert(node.printString(Node("hello", None)))(
84 | isRight(equalTo(""))
85 | )
86 | },
87 | test("print nested") {
88 | assert(node.printString(Node("hello", Some(Node("world", None)))))(
89 | isRight(equalTo(""))
90 | )
91 | }
92 | )
93 | )
94 | }
95 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | env:
4 | JDK_JAVA_OPTIONS: -XX:+PrintCommandLineFlags # JDK_JAVA_OPTIONS is _the_ env. variable to use for modern Java
5 | JVM_OPTS: -XX:+PrintCommandLineFlags # for Java 8 only (sadly, it is not modern enough for JDK_JAVA_OPTIONS)
6 |
7 | on:
8 | pull_request:
9 | push:
10 | branches: ['master']
11 | release:
12 | types:
13 | - published
14 |
15 | jobs:
16 | lint:
17 | runs-on: ubuntu-latest
18 | timeout-minutes: 30
19 | steps:
20 | - name: Checkout current branch
21 | uses: actions/checkout@v4.2.0
22 | with:
23 | fetch-depth: 0
24 | - name: Setup Scala and Java
25 | uses: actions/setup-java@v4.4.0
26 | with:
27 | distribution: zulu
28 | java-version: 17
29 | check-latest: true
30 | - name: Setup sbt
31 | uses: sbt/setup-sbt@v1
32 | - name: Cache scala dependencies
33 | uses: coursier/cache-action@v6
34 | - name: Lint code
35 | run: sbt check
36 |
37 | website:
38 | runs-on: ubuntu-latest
39 | timeout-minutes: 60
40 | steps:
41 | - name: Checkout current branch
42 | uses: actions/checkout@v4.2.0
43 | - name: Setup Scala and Java
44 | uses: actions/setup-java@v4.4.0
45 | with:
46 | distribution: zulu
47 | java-version: 17
48 | check-latest: true
49 | - name: Setup sbt
50 | uses: sbt/setup-sbt@v1
51 | - name: Cache scala dependencies
52 | uses: coursier/cache-action@v6
53 | - name: Check Document Generation
54 | run: sbt docs/compileDocs
55 |
56 | test:
57 | runs-on: ubuntu-latest
58 | timeout-minutes: 30
59 | strategy:
60 | fail-fast: false
61 | matrix:
62 | java: ['17']
63 | scala: ['212', '213', '3']
64 | platform: ['JVM', 'JS', 'Native']
65 | steps:
66 | - name: Checkout current branch
67 | uses: actions/checkout@v4.2.0
68 | with:
69 | fetch-depth: 0
70 | - name: Setup Scala and Java
71 | uses: actions/setup-java@v4.4.0
72 | with:
73 | distribution: zulu
74 | java-version: ${{ matrix.java }}
75 | check-latest: true
76 | - name: Setup sbt
77 | uses: sbt/setup-sbt@v1
78 | - name: Cache scala dependencies
79 | uses: coursier/cache-action@v6
80 | - name: Run tests
81 | run: sbt test${{ matrix.platform }}_${{ matrix.scala }}
82 |
83 | ci:
84 | runs-on: ubuntu-latest
85 | needs: [lint, website, test]
86 | steps:
87 | - name: Aggregate of lint, and all tests
88 | run: echo "ci passed"
89 |
90 | publish:
91 | runs-on: ubuntu-latest
92 | timeout-minutes: 30
93 | needs: [ci]
94 | if: github.event_name != 'pull_request'
95 | steps:
96 | - name: Checkout current branch
97 | uses: actions/checkout@v4.2.0
98 | with:
99 | fetch-depth: 0
100 | - name: Setup Scala and Java
101 | uses: actions/setup-java@v4.4.0
102 | with:
103 | distribution: zulu
104 | java-version: 17
105 | check-latest: true
106 | - name: Setup sbt
107 | uses: sbt/setup-sbt@v1
108 | - name: Cache scala dependencies
109 | uses: coursier/cache-action@v6
110 | - name: Release artifacts
111 | run: sbt ci-release
112 | env:
113 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }}
114 | PGP_SECRET: ${{ secrets.PGP_SECRET }}
115 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
116 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
--------------------------------------------------------------------------------
/benchmarks/src/main/scala/zio/parser/benchmarks/micro/Experiments.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.benchmarks.micro
2 |
3 | import org.openjdk.jmh.annotations._
4 | import zio.Chunk
5 | import zio.parser.Regex
6 |
7 | import java.util.concurrent.TimeUnit
8 |
9 | @BenchmarkMode(Array(Mode.Throughput))
10 | @OutputTimeUnit(TimeUnit.MILLISECONDS)
11 | @State(Scope.Benchmark)
12 | @Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
13 | @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
14 | @Fork(value = 2)
15 | class Experiments {
16 |
17 | val longString: String = ("hello" * 1000) + "world" + ("hello" * 1000) + "!!!"
18 | val longChunk: Chunk[Char] = Chunk.fromIterable(longString.toCharArray)
19 |
20 | val regexClass = Regex.charIn('h', 'e', 'l', 'o').atLeast(1).compile
21 | val builtInRegex = """\G[helo]+""".r
22 |
23 | val regex2Class = Regex.string("hello").compile
24 | val builtInRegex2 = """\Ghello""".r
25 |
26 | @Setup
27 | def setup(): Unit = {}
28 |
29 | @Benchmark
30 | def shortSliceString(): String =
31 | longString.slice(5000, 5006)
32 |
33 | @Benchmark
34 | def shortSliceChunk1(): Chunk[Char] =
35 | longChunk.drop(5000).take(5)
36 |
37 | @Benchmark
38 | def shortSliceChunk2(): Chunk[Char] =
39 | longChunk.slice(5000, 5006)
40 |
41 | @Benchmark
42 | def longSliceString(): String =
43 | longString.slice(2000, 5006)
44 |
45 | @Benchmark
46 | def longSliceChunk1(): Chunk[Char] =
47 | longChunk.drop(2000).take(3005)
48 |
49 | @Benchmark
50 | def longSliceChunk2(): Chunk[Char] =
51 | longChunk.slice(2000, 5006)
52 |
53 | @Benchmark
54 | def shortSliceStringS(): String =
55 | longString.slice(5000, 5006)
56 |
57 | @Benchmark
58 | def shortSliceChunk1S(): String =
59 | String.copyValueOf(longChunk.drop(5000).take(5).toArray)
60 |
61 | @Benchmark
62 | def shortSliceChunk2S(): String =
63 | new String(longChunk.slice(5000, 5006).toArray)
64 |
65 | @Benchmark
66 | def longSliceStringS(): String =
67 | longString.slice(2000, 5006)
68 |
69 | @Benchmark
70 | def longSliceChunk1S(): String =
71 | String.copyValueOf(longChunk.drop(2000).take(3005).toArray)
72 |
73 | @Benchmark
74 | def longSliceChunk2S(): String =
75 | new String(longChunk.drop(2000).take(3005).toArray)
76 |
77 | @Benchmark
78 | def getCharString: Char =
79 | longString(5000)
80 |
81 | @Benchmark
82 | def getCharChunk: Char =
83 | longChunk(5000)
84 |
85 | @Benchmark
86 | def regexOnString =
87 | regexClass.test(0, longString)
88 |
89 | @Benchmark
90 | def builtInRegexOnString =
91 | builtInRegex.findPrefixMatchOf(longString) match {
92 | case Some(m) => m.end
93 | case None => 0
94 | }
95 |
96 | @Benchmark
97 | def regexOnStringOffset =
98 | regexClass.test(2000, longString)
99 |
100 | @Benchmark
101 | def builtInRegexOnStringOffset =
102 | builtInRegex.findPrefixMatchOf(longString.slice(2000, longString.length)) match {
103 | case Some(m) => m.end
104 | case None => 0
105 | }
106 |
107 | @Benchmark
108 | def builtInRegexOnStringOffset2 = {
109 | val matcher = builtInRegex.pattern.matcher(longString)
110 | if (matcher.find(2000))
111 | matcher.end()
112 | else {
113 | Regex.NotMatched
114 | }
115 | }
116 |
117 | @Benchmark
118 | def regex2OnStringOffset =
119 | regex2Class.test(2000, longString)
120 |
121 | @Benchmark
122 | def builtInRegex2OnStringOffset2 = {
123 | val matcher = builtInRegex2.pattern.matcher(longString)
124 | if (matcher.find(2000))
125 | matcher.end()
126 | else {
127 | Regex.NotMatched
128 | }
129 | }
130 |
131 | }
132 |
--------------------------------------------------------------------------------
/benchmarks/src/main/scala/zio/parser/benchmarks/basic/Zipping16.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.benchmarks.basic
2 |
3 | import zio.Chunk
4 | import zio.parser.benchmarks.{ParserBenchmark, ParserBenchmarkTestRunner, Parserz}
5 | import zio.parser.internal.Debug
6 |
7 | import scala.util.{Random, Try}
8 |
9 | class Zipping16 extends ParserBenchmark[Zips16] {
10 | override def loadInput(): String = {
11 | val N = 250
12 | val sb = new StringBuilder
13 | for (_ <- 1 to N) {
14 | sb.append((1 to 16).map(_ => Random.alphanumeric.take(6).mkString).mkString(","))
15 | sb.append('\n')
16 | }
17 | sb.toString()
18 | }
19 |
20 | override final val zioSyntax: zio.parser.Syntax[String, Char, Char, Zips16] = {
21 | import zio.parser._
22 |
23 | val item = Syntax
24 | .charNotIn(',', '\n')
25 | .repeat
26 | .string
27 | val sep = Syntax.char(',')
28 | val tuple =
29 | (item <~ sep) ~ (item <~ sep) ~ (item <~ sep) ~ (item <~ sep) ~ (item <~ sep) ~ (item <~ sep) ~ (item <~ sep) ~ (item <~ sep) ~ (item <~ sep) ~ (item <~ sep) ~ (item <~ sep) ~ (item <~ sep) ~ (item <~ sep) ~ (item <~ sep) ~ (item <~ sep) ~ item
30 | val zip = tuple.transform(
31 | { case (as, bs, cs, ds, es, fs, gs, hs, is, js, ks, ls, ms, ns, os, ps) =>
32 | Zip16(as, bs, cs, ds, es, fs, gs, hs, is, js, ks, ls, ms, ns, os, ps)
33 | },
34 | (zip: Zip16) =>
35 | (
36 | zip.a,
37 | zip.b,
38 | zip.c,
39 | zip.d,
40 | zip.e,
41 | zip.f,
42 | zip.g,
43 | zip.h,
44 | zip.i,
45 | zip.j,
46 | zip.k,
47 | zip.l,
48 | zip.m,
49 | zip.n,
50 | zip.o,
51 | zip.p
52 | )
53 | )
54 | val line = zip <~ Syntax.char('\n')
55 | val lines = line.repeat0.transform[Zips16](Zips16.apply, z => Chunk.fromIterable(z.zips)).manualBacktracking
56 | lines
57 | }
58 |
59 | override final val catParser: cats.parse.Parser0[Zips16] = {
60 | import cats.parse._
61 |
62 | val item = Parser.charsWhile(ch => ch != ',' && ch != '\n')
63 | val sep = Parser.char(',')
64 | val tuple =
65 | (item <* sep) ~ (item <* sep) ~ (item <* sep) ~ (item <* sep) ~ (item <* sep) ~ (item <* sep) ~ (item <* sep) ~ (item <* sep) ~ (item <* sep) ~ (item <* sep) ~ (item <* sep) ~ (item <* sep) ~ (item <* sep) ~ (item <* sep) ~ (item <* sep) ~ item
66 | val zip = tuple.map {
67 | case (((((((((((((((as, bs), cs), ds), es), fs), gs), hs), is), js), ks), ls), ms), ns), os), ps) =>
68 | Zip16(as, bs, cs, ds, es, fs, gs, hs, is, js, ks, ls, ms, ns, os, ps)
69 | }
70 | val line = zip <* Parser.char('\n')
71 | val lines = line.rep0.map(lst => Zips16(lst))
72 | lines
73 | }
74 |
75 | // TODO: implement for all
76 |
77 | override def fastParseP[P: fastparse.P]: fastparse.P[Zips16] = ???
78 |
79 | override val attoParser: atto.Parser[Zips16] = null
80 |
81 | override def runParboiledParser(input: String): Try[Zips16] = ???
82 |
83 | override val parsley: org.http4s.parsley.Parsley[Zips16] = null
84 | override val parserz: Parserz.Grammar[Any, Nothing, String, Zips16] = null
85 | }
86 |
87 | case class Zip16(
88 | a: String,
89 | b: String,
90 | c: String,
91 | d: String,
92 | e: String,
93 | f: String,
94 | g: String,
95 | h: String,
96 | i: String,
97 | j: String,
98 | k: String,
99 | l: String,
100 | m: String,
101 | n: String,
102 | o: String,
103 | p: String
104 | )
105 | case class Zips16(zips: Seq[Zip16])
106 |
107 | object Zipping16 extends ParserBenchmarkTestRunner[Zips16, Zipping16] {
108 | override val self: ParserBenchmark[Zips16] = new Zipping16
109 | }
110 |
--------------------------------------------------------------------------------
/zio-parser/shared/src/test/scala/zio/parser/StringParserErrorSpec.scala:
--------------------------------------------------------------------------------
1 | package zio.parser
2 |
3 | import zio.test.Assertion._
4 | import zio.test._
5 |
6 | object StringParserErrorSpec extends ZIOSpecDefault {
7 | override def spec =
8 | suite("StringParserError")(
9 | failureTest(
10 | "should pretty print a Failure error",
11 | Syntax.char('y'),
12 | "x",
13 | """|x
14 | |^
15 | |error: Failure at position 0: not 'y'
16 | |""".stripMargin
17 | ),
18 | failureTest(
19 | "should pretty print an UnexpectedEndOfInput error",
20 | Syntax.char('y'),
21 | "",
22 | "Unexpected end of input"
23 | ),
24 | failureTest(
25 | "should pretty print a NotConsumedAll error",
26 | Syntax.char('y') <~ Syntax.end,
27 | "yyz",
28 | """|yyz
29 | | ^
30 | |error: Parser did not consume all input at position 1
31 | |""".stripMargin
32 | ),
33 | failureTest(
34 | "should pretty print an AllBranchesFailed error",
35 | Syntax.char('y').orElse(Syntax.char('z')),
36 | "x",
37 | "All branches failed: Failure at position 0: not 'y' and Failure at position 0: not 'z'"
38 | ),
39 | failureTest(
40 | "should pretty print an error with multiline input",
41 | Syntax.char('y'),
42 | "x\ny",
43 | """|x
44 | |^
45 | |error: Failure at position 0: not 'y'
46 | |y
47 | |""".stripMargin
48 | ),
49 | failureTest(
50 | "should point to the correct position in the input",
51 | Syntax.char('x').repeat <~ Syntax.end,
52 | "xxxyxx",
53 | """|xxxyxx
54 | | ^
55 | |error: Parser did not consume all input at position 3
56 | |""".stripMargin
57 | ),
58 | failureTest(
59 | "should replace the following lines with an ellipsis",
60 | Syntax.char('y'),
61 | "x\n" + "y\n" * 100,
62 | """|x
63 | |^
64 | |error: Failure at position 0: not 'y'
65 | |y
66 | |y
67 | |...
68 | |""".stripMargin
69 | ),
70 | failureTest(
71 | "should replace the preceding and following lines with an ellipsis",
72 | (Syntax.digit.orElse(Syntax.whitespace)).repeat <~ Syntax.end,
73 | "1\n1\n1\n" + "y\n" + "1\n1\n1\n",
74 | """|...
75 | |1
76 | |1
77 | |y
78 | |^
79 | |error: Parser did not consume all input at position 6
80 | |1
81 | |1
82 | |...
83 | |""".stripMargin
84 | ),
85 | failureTest(
86 | "should print the failed parser name",
87 | (Syntax.digit.named("digit").orElse(Syntax.whitespace.named("whitespace"))).repeat <~
88 | Syntax.end.named("end"),
89 | "1 1 1 1 y 1 1 1",
90 | """|1 1 1 1 y 1 1 1
91 | | ^
92 | |error: Parser did not consume all input at position 8 in end
93 | |""".stripMargin
94 | ),
95 | failureTest(
96 | "should print the parser name stack",
97 | ((Syntax.digit.named("digit") ~ Syntax.string("Keyword", ()).named("keyword"))
98 | .named("combined")),
99 | "5Keywor",
100 | "Unexpected end of input in keyword -> combined"
101 | )
102 | )
103 |
104 | private def failureTest[E, T](
105 | name: String,
106 | parser: Syntax[E, Char, Char, T],
107 | input: String,
108 | expectedErrorMessage: String
109 | ): Spec[Any, TestFailure[Nothing]] =
110 | test(name) {
111 | assert(parser.parseString(input).left.map(_.pretty))(isLeft(equalTo(expectedErrorMessage)))
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/benchmarks/src/main/scala/zio/parser/benchmarks/json/JsonParboiled.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.benchmarks.json
2 |
3 | import scala.annotation.switch
4 | import org.parboiled2._
5 | import zio.Chunk
6 |
7 | // Based on https://github.com/typelevel/cats-parse/blob/main/bench/src/main/scala/cats/parse/bench/parboiled2.scala
8 | class JsonParboiled(val input: ParserInput) extends Parser with StringBuilding {
9 | import CharPredicate.{Digit, Digit19, HexDigit}
10 | import JsonParboiled._
11 |
12 | // the root rule
13 | def JSON = rule(WhiteSpace ~ Value ~ EOI)
14 |
15 | def JsonObject: Rule1[Json.Obj] =
16 | rule {
17 | ws('{') ~ zeroOrMore(Pair).separatedBy(ws(',')) ~ ws('}') ~> ((fields: Seq[(String, Json)]) =>
18 | Json.Obj(Chunk.fromIterable(fields))
19 | )
20 | }
21 |
22 | def Pair = rule(JsonStringUnwrapped ~ ws(':') ~ Value ~> ((_, _)))
23 |
24 | def Value: Rule1[Json] =
25 | rule {
26 | // as an optimization of the equivalent rule:
27 | // JsonString | JsonNumber | JsonObject | JsonArray | JsonTrue | JsonFalse | JsonNull
28 | // we make use of the fact that one-char lookahead is enough to discriminate the cases
29 | run {
30 | (cursorChar: @switch) match {
31 | case '"' => JsonString
32 | case '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '-' => JsonNumber
33 | case '{' => JsonObject
34 | case '[' => JsonArray
35 | case 't' => JsonTrue
36 | case 'f' => JsonFalse
37 | case 'n' => JsonNull
38 | case _ => MISMATCH
39 | }
40 | }
41 | }
42 |
43 | def JsonString = rule(JsonStringUnwrapped ~> (Json.Str(_)))
44 |
45 | def JsonStringUnwrapped = rule('"' ~ clearSB() ~ Characters ~ ws('"') ~ push(sb.toString))
46 |
47 | def JsonNumber = rule(
48 | capture(Integer ~ optional(Frac) ~ optional(Exp)) ~> (s => Json.Num(BigDecimal(s))) ~ WhiteSpace
49 | )
50 |
51 | def JsonArray = rule(
52 | ws('[') ~ zeroOrMore(Value).separatedBy(ws(',')) ~ ws(']') ~> (values => Json.Arr(Chunk.fromIterable(values)))
53 | )
54 |
55 | def Characters = rule(zeroOrMore(NormalChar | '\\' ~ EscapedChar))
56 |
57 | def NormalChar = rule(!QuoteBackslash ~ ANY ~ appendSB())
58 |
59 | def EscapedChar =
60 | rule(
61 | QuoteSlashBackSlash ~ appendSB()
62 | | 'b' ~ appendSB('\b')
63 | | 'f' ~ appendSB('\f')
64 | | 'n' ~ appendSB('\n')
65 | | 'r' ~ appendSB('\r')
66 | | 't' ~ appendSB('\t')
67 | | Unicode ~> { code => sb.append(code.asInstanceOf[Char]); () }
68 | )
69 |
70 | def Unicode = rule(
71 | 'u' ~ capture(HexDigit ~ HexDigit ~ HexDigit ~ HexDigit) ~> (java.lang.Integer.parseInt(_, 16))
72 | )
73 |
74 | def Integer = rule(optional('-') ~ (Digit19 ~ Digits | Digit))
75 |
76 | def Digits = rule(oneOrMore(Digit))
77 |
78 | def Frac = rule("." ~ Digits)
79 |
80 | def Exp = rule(ignoreCase('e') ~ optional(anyOf("+-")) ~ Digits)
81 |
82 | def JsonTrue = rule("true" ~ WhiteSpace ~ push(Json.Bool(true)))
83 |
84 | def JsonFalse = rule("false" ~ WhiteSpace ~ push(Json.Bool(false)))
85 |
86 | def JsonNull = rule("null" ~ WhiteSpace ~ push(Json.Null))
87 |
88 | def WhiteSpace = rule(zeroOrMore(WhiteSpaceChar))
89 |
90 | def ws(c: Char) = rule(c ~ WhiteSpace)
91 | }
92 |
93 | object JsonParboiled {
94 | val WhiteSpaceChar = CharPredicate(" \n\r\t\f")
95 | val QuoteBackslash = CharPredicate("\"\\")
96 | val QuoteSlashBackSlash = QuoteBackslash ++ "/"
97 | }
98 |
--------------------------------------------------------------------------------
/zio-parser/shared/src/main/scala/zio/parser/internal/recursive/ParserState.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.internal.recursive
2 |
3 | import zio.Chunk
4 | import zio.parser.Parser.ParserError
5 | import zio.parser.Regex
6 |
7 | /** State of the recursive parser implementation
8 | *
9 | * The implementation itself is in Parser#parseRec.
10 | */
11 | sealed trait ParserState[+In] {
12 |
13 | def regex(compiledRegex: Regex.Compiled): Int
14 | def sliceToChunk(pos: Int, until: Int): Chunk[In]
15 | def sliceToString(pos: Int, until: Int): String
16 | def at(pos: Int): In
17 | val length: Int
18 |
19 | def char(index: Int)(implicit ev: In <:< Char): Char =
20 | ev(at(index))
21 |
22 | /** Position in the parsed string (source) */
23 | var position: Int = 0
24 |
25 | /** Stack of named parsers, used to enrich failures */
26 | var nameStack: List[String] = Nil
27 |
28 | /** Error emitted during parsing */
29 | var error: ParserError[Any] = _
30 |
31 | /** Flag indicating that parser results are not being used */
32 | var discard: Boolean = false
33 |
34 | /** Push a name to nameStack */
35 | def pushName(name: String): Unit =
36 | nameStack = name :: nameStack
37 |
38 | /** Pop the last pushed name from nameStack */
39 | def popName(): Unit =
40 | nameStack = nameStack.tail
41 | }
42 |
43 | object ParserState {
44 | private final class StringParserState(source: String) extends ParserState[Char] {
45 | override def regex(compiledRegex: Regex.Compiled): Int =
46 | compiledRegex.test(position, source)
47 |
48 | override def sliceToChunk(pos: Int, until: Int): Chunk[Char] =
49 | Chunk.fromArray(source.slice(pos, until).toCharArray)
50 |
51 | override def sliceToString(pos: Int, until: Int): String =
52 | source.slice(pos, until)
53 |
54 | override def at(pos: Int): Char = source(pos)
55 |
56 | override val length: Int = source.length
57 | }
58 |
59 | private final class CharChunkParserState(source: Chunk[Char]) extends ParserState[Char] {
60 | override def regex(compiledRegex: Regex.Compiled): Int =
61 | compiledRegex.test(position, source)
62 |
63 | override def sliceToChunk(pos: Int, until: Int): Chunk[Char] =
64 | source.slice(pos, until)
65 |
66 | override def sliceToString(pos: Int, until: Int): String =
67 | new String(source.slice(pos, until).toArray)
68 |
69 | override def at(pos: Int): Char = source(pos)
70 |
71 | override val length: Int = source.length
72 | }
73 |
74 | private final class ChunkParserState[In](source: Chunk[In]) extends ParserState[In] {
75 | override def regex(compiledRegex: Regex.Compiled): Int =
76 | throw new UnsupportedOperationException("regex not supported on non-char Chunks")
77 |
78 | override def sliceToChunk(pos: Int, until: Int): Chunk[In] =
79 | source.slice(pos, until)
80 |
81 | override def sliceToString(pos: Int, until: Int): String =
82 | throw new UnsupportedOperationException("sliceToString not supported on non-char Chunks")
83 |
84 | override def at(pos: Int): In = source(pos)
85 |
86 | override val length: Int = source.length
87 | }
88 |
89 | def fromString(source: String): ParserState[Char] =
90 | new StringParserState(source)
91 |
92 | def fromChunk[In](chunk: Chunk[In])(implicit stateSelector: StateSelector[In]): ParserState[In] =
93 | stateSelector.create(chunk)
94 |
95 | trait StateSelector[In] {
96 | def create(chunk: Chunk[In]): ParserState[In]
97 | }
98 |
99 | object StateSelector extends LowerPriorityStateSelector {
100 | implicit val charStateSelector: StateSelector[Char] = (chunk: Chunk[Char]) => new CharChunkParserState(chunk)
101 | }
102 |
103 | trait LowerPriorityStateSelector {
104 | implicit def otherStateSelector[In]: StateSelector[In] = (chunk: Chunk[In]) => new ChunkParserState(chunk)
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/benchmarks/src/main/scala/zio/parser/benchmarks/micro/CharParserMicroBenchmarks.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.benchmarks.micro
2 |
3 | import org.openjdk.jmh.annotations.{
4 | Benchmark,
5 | BenchmarkMode,
6 | Fork,
7 | Measurement,
8 | Mode,
9 | OutputTimeUnit,
10 | Scope,
11 | Setup,
12 | State,
13 | Warmup
14 | }
15 | import zio.Chunk
16 | import zio.parser.StringParserError
17 | import zio.parser.Parser.ParserError
18 | import zio.parser.{Regex, Syntax}
19 |
20 | import java.util.concurrent.TimeUnit
21 |
22 | @BenchmarkMode(Array(Mode.Throughput))
23 | @OutputTimeUnit(TimeUnit.MILLISECONDS)
24 | @State(Scope.Benchmark)
25 | @Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
26 | @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
27 | @Fork(value = 1)
28 | class CharParserMicroBenchmarks {
29 |
30 | type String10 = (String, String, String, String, String, String, String, String, String, String)
31 | var skipAndTransformSyntax: Syntax[Nothing, Char, Char, String] = _
32 | var skipAndTransformOrElseSyntax: Syntax[String, Char, Char, String] = _
33 | var skipAndTransformZipSyntax: Syntax[String, Char, Char, String10] = _
34 | var skipAndTransformRepeatSyntax: Syntax[String, Char, Char, Chunk[String]] = _
35 | var repeatWithSep0Syntax: Syntax[String, Char, Char, Chunk[String]] = _
36 | var hello: Chunk[Char] = _
37 | var hellos: Chunk[Char] = _
38 | var world: Chunk[Char] = _
39 | var hellosSep: String = _
40 |
41 | @Setup
42 | def setUp(): Unit = {
43 | hello = Chunk('h', 'e', 'l', 'l', 'o')
44 | world = Chunk('w', 'o', 'r', 'l', 'd')
45 | hellos = Chunk.fromArray(("hello" * 10).toCharArray)
46 | hellosSep = (1 to 10000).map(_ => "hello").mkString(",")
47 |
48 | skipAndTransformSyntax = Syntax
49 | .unsafeRegexDiscard(Regex.anyChar.atLeast(0), hello)
50 | .transform(_ => "hello", _ => ())
51 | val _ = skipAndTransformSyntax.asParser.optimized
52 |
53 | val literalHello =
54 | Syntax.regexDiscard(Regex.string("hello"), "not hello", hello).transform[String](_ => "hello", _ => ())
55 |
56 | skipAndTransformOrElseSyntax = literalHello |
57 | Syntax.regexDiscard(Regex.string("world"), "not world", world).transform[String](_ => "world", _ => ())
58 | val _ = skipAndTransformOrElseSyntax.asParser.optimized
59 |
60 | skipAndTransformRepeatSyntax = literalHello.repeat
61 | val _ = skipAndTransformRepeatSyntax.asParser.optimized
62 |
63 | skipAndTransformZipSyntax =
64 | literalHello ~ literalHello ~ literalHello ~ literalHello ~ literalHello ~ literalHello ~ literalHello ~ literalHello ~ literalHello ~ literalHello
65 | val _ = skipAndTransformZipSyntax.asParser.optimized
66 |
67 | repeatWithSep0Syntax = Syntax.string("hello", "hello").repeatWithSep0(Syntax.char(','))
68 | val _ = repeatWithSep0Syntax.asParser.optimized
69 | }
70 |
71 | @Benchmark
72 | def skipAndTransform(): Either[StringParserError[Nothing], String] =
73 | skipAndTransformSyntax.parseChars(hello)
74 |
75 | @Benchmark
76 | def skipAndTransformOrElse(): Either[StringParserError[String], String] =
77 | skipAndTransformOrElseSyntax.parseChars(world)
78 |
79 | @Benchmark
80 | def skipAndTransformRepeat(): Either[StringParserError[String], Chunk[String]] =
81 | skipAndTransformRepeatSyntax.parseChars(hellos)
82 |
83 | @Benchmark
84 | def skipAndTransformZip(): Either[StringParserError[String], String10] =
85 | skipAndTransformZipSyntax.parseChars(hellos)
86 |
87 | @Benchmark
88 | def repeatWithSep0(): Either[StringParserError[String], Chunk[String]] =
89 | repeatWithSep0Syntax.parseString(hellosSep)
90 | }
91 |
92 | //object CharParserMicroBenchmarks extends CharParserMicroBenchmarks {
93 | // def main(args: Array[String]): Unit = {
94 | // setUp()
95 | //// println(skipAndTransform())
96 | //// println(skipAndTransformOrElse())
97 | //// println(skipAndTransformRepeat())
98 | //// println(skipAndTransformZip())
99 | // println(repeatWithSep0().map(_.length))
100 | // }
101 | //}
102 |
--------------------------------------------------------------------------------
/benchmarks/src/main/scala/zio/parser/benchmarks/json/JsonParserz.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.benchmarks.json
2 |
3 | import zio.parser.benchmarks.Parserz
4 | import Parserz.Grammar._
5 | import Parserz.Expr._
6 | import Parserz._
7 | import zio.Chunk
8 |
9 | // Based on https://github.com/spartanz/parserz/blob/master/src/test/scala/org/spartanz/parserz/compare/ParserzJsonTest.scala
10 | object JsonParserz {
11 | type S = Unit
12 | type E = String
13 | type G[A] = Grammar[Any, Nothing, E, A]
14 |
15 | def char(c: Char): G[Char] = consume(
16 | cs => if (cs.nonEmpty && cs.head == c) Right((cs.tail, c)) else Left("expected: " + c),
17 | { case (cs, _) => Right(c :: cs) }
18 | )
19 | def token(t: List[Char]): G[List[Char]] = consume(
20 | cs => if (cs.startsWith(t)) Right((cs.drop(t.length), t)) else Left("expected: " + t),
21 | { case (cs, _) => Right(t reverse_::: cs) }
22 | )
23 |
24 | val dot: G[Char] = char('.')
25 | val comma: G[Char] = char(',')
26 | val colon: G[Char] = char(':')
27 | val quote: G[Char] = char('"')
28 | val bracket1: G[Char] = char('[')
29 | val bracket2: G[Char] = char(']')
30 | val brace1: G[Char] = char('{')
31 | val brace2: G[Char] = char('}')
32 |
33 | val spacing: G[Unit] = consumePure(
34 | cs => (cs.dropWhile(c => c == ' ' || c == '\n' || c == '\r'), ()),
35 | { case (cs, _) => ' ' :: cs }
36 | )
37 |
38 | val ch: G[Char] = consume(
39 | cs => if (cs.nonEmpty) Right((cs.tail, cs.head)) else Left("expected: char"),
40 | { case (cs, c) => Right(c :: cs) }
41 | )
42 |
43 | def chars(cond: Char => Boolean): G[List[Char]] = consumePure(
44 | { cs =>
45 | val out = cs.takeWhile(cond)
46 | (cs.drop(out.length), out)
47 | },
48 | { case (cs, cs1) =>
49 | cs1 reverse_::: cs
50 | }
51 | )
52 |
53 | val digits: G[List[Char]] = chars(c => '0' <= c && c <= '9')
54 | val sign: G[Option[Char]] = ch.filter("expected: +/-")(in('+', '-')).option
55 | val exponent: G[List[Char]] = (ch.filter("expected: E")(in('e', 'E')) ~ sign, ('E', Some('+'))) ~> digits
56 | val fractional: G[List[Char]] = (dot, '.') ~> digits
57 | val integral: G[List[Char]] = digits
58 |
59 | val num: G[Json.Num] = (sign ~ integral ~ fractional.orEmpty ~ exponent.orEmpty).map(
60 | { case (((s, l1), l2), l3) => Json.Num((s.mkString + l1.mkString + l2.mkString + l3.mkString).toDouble) },
61 | { case Json.Num(_) => ??? }
62 | )
63 |
64 | val `null`: G[Json.Null.type] = token("null".toList).map(_ => Json.Null, _ => "null".toList)
65 | val `false`: G[Json.Bool] = token("false".toList).map(_ => Json.Bool(false), _ => "false".toList)
66 | val `true`: G[Json.Bool] = token("true".toList).map(_ => Json.Bool(true), _ => "true".toList)
67 |
68 | val string: G[String] =
69 | ((spacing ~ quote, ((), '"')) ~> chars(c => c != '\"' && c != '\\') <~ ('"', quote)).map(_.mkString, _.toList)
70 | val str: G[Json.Str] = string.map(Json.Str, "\"" + _.value + "\"")
71 |
72 | val arr: G[Json.Arr] =
73 | ((bracket1, '[') ~> js.separated(comma).map(_.values, { _: List[Json] => ??? }) <~ (((), ']'), spacing ~ bracket2))
74 | .map(
75 | lst => Json.Arr(Chunk.fromIterable(lst)),
76 | arr => arr.elements.toList
77 | )
78 |
79 | val field: G[(String, Json)] = (string <~ (':', colon)) ~ js
80 |
81 | val obj: G[Json.Obj] =
82 | ((brace1, '{') ~> field.separated(comma).map(_.values, { _: List[(String, Json)] => ??? }) <~ ((
83 | (),
84 | '}'
85 | ), spacing ~ brace2)).map(
86 | lst => Json.Obj(Chunk.fromIterable(lst)),
87 | obj => obj.fields.toList
88 | )
89 |
90 | // todo: smash ?
91 | lazy val js: G[Json] = delay {
92 | ((spacing, ()) ~> (obj | arr | str | `true` | `false` | `null` | num) <~ ((), spacing)).map(
93 | {
94 | case Left(Left(Left(Left(Left(Left(v)))))) => v
95 | case Left(Left(Left(Left(Left(Right(v)))))) => v
96 | case Left(Left(Left(Left(Right(v))))) => v
97 | case Left(Left(Left(Right(v)))) => v
98 | case Left(Left(Right(v))) => v
99 | case Left(Right(v)) => v
100 | case Right(v) => v
101 | },
102 | {
103 | case j: Json.Obj => Left(Left(Left(Left(Left(Left(j))))))
104 | case j: Json.Arr => Left(Left(Left(Left(Left(Right(j))))))
105 | case j: Json.Str => Left(Left(Left(Left(Right(j)))))
106 | case Json.Bool(true) => Left(Left(Left(Right(Json.Bool(true)))))
107 | case Json.Bool(false) => Left(Left(Right(Json.Bool(false))))
108 | case Json.Null => Left(Right(Json.Null))
109 | case j: Json.Num => Right(j)
110 | }
111 | )
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/zio-parser/shared/src/test/scala/zio/parser/PrinterSpec.scala:
--------------------------------------------------------------------------------
1 | package zio.parser
2 |
3 | import zio.Chunk
4 | import zio.test.Assertion._
5 | import zio.test._
6 |
7 | object PrinterSpec extends ZIOSpecDefault {
8 | private val charA: Syntax[String, Char, Char, Char] =
9 | Syntax.charIn('a')
10 |
11 | private val charB: Syntax[String, Char, Char, Char] =
12 | Syntax.charIn('b')
13 |
14 | case class TestCaseClass(a: Char, b: Char)
15 |
16 | override def spec: Spec[Environment, Any] =
17 | suite("Printing")(
18 | suite("Invertible syntax")(
19 | printerTest("anyChar", Syntax.anyChar, 'x')(isRight(equalTo("x"))),
20 | printerTest("filtered char, passing", Syntax.anyChar.filter((ch: Char) => ch == 'h', "not h"), 'h')(
21 | isRight(equalTo("h"))
22 | ),
23 | printerTest("filtered char, failing", Syntax.anyChar.filter((ch: Char) => ch == 'h', "not h"), 'e')(
24 | isLeft(equalTo("not h"))
25 | ),
26 | printerTest("transform", Syntax.anyChar.transform(_.toInt, (v: Int) => v.toChar), 66)(isRight(equalTo("B"))),
27 | printerTest(
28 | "transformEither, failing",
29 | Syntax.anyChar.transformEither[String, Int](_ => Left("bad"), (_: Int) => Left("bad")),
30 | 100
31 | )(
32 | isLeft(equalTo("bad"))
33 | ),
34 | printerTest("s ~ s", Syntax.anyChar ~ Syntax.anyChar, ('x', 'y'))(isRight(equalTo("xy"))),
35 | printerTest("s ~ s ~ s", Syntax.anyChar ~ Syntax.anyChar ~ Syntax.anyChar, ('x', 'y', 'z'))(
36 | isRight(equalTo("xyz"))
37 | ),
38 | printerTest(
39 | "s ~ s, failing left",
40 | Syntax.anyChar.filter((ch: Char) => ch == 'a', "not a") ~ Syntax.anyChar,
41 | ('b', 'c')
42 | )(
43 | isLeft(equalTo("not a"))
44 | ),
45 | printerTest(
46 | "s ~ s, failing right",
47 | Syntax.anyChar ~ Syntax.anyChar.filter((ch: Char) => ch == 'a', "not a"),
48 | ('a', 'b')
49 | )(
50 | isLeft(equalTo("not a"))
51 | ),
52 | printerTest("s <* s", Syntax.anyChar <~ Syntax.anyChar.asPrinted((), '?'), 'x')(isRight(equalTo("x?"))),
53 | printerTest("s *> s", Syntax.anyChar.asPrinted((), '?') ~> Syntax.anyChar, 'x')(isRight(equalTo("?x"))),
54 | printerTest("s | s, left passing", charA | charB, 'a')(
55 | isRight(equalTo("a"))
56 | ),
57 | printerTest("s | s, right passing", charA | charB, 'b')(
58 | isRight(equalTo("b"))
59 | ),
60 | printerTest("s | s, failing", charA | charB, 'c')(
61 | isLeft(equalTo("Not the expected character (b)"))
62 | ), {
63 | val hello = Syntax.string("hello", 'h')
64 | val world = Syntax.string("world", 'w')
65 | val all = Syntax.string("all", 'a')
66 | val a = hello ~ world
67 | val b = hello ~ all
68 | printerTest(
69 | "s | s, left failing inside",
70 | a | b,
71 | ('h', 'a')
72 | )(isRight(equalTo("helloall")))
73 | },
74 | printerTest("s <+> s, left passing", charA <+> charB, Left('a'))(
75 | isRight(equalTo("a"))
76 | ),
77 | printerTest(
78 | "s <+> s, right passing",
79 | charA <+> charB,
80 | Right('b')
81 | )(isRight(equalTo("b"))),
82 | printerTest("s <+> s, failing", charA <+> charB, Right('c'))(
83 | isLeft(equalTo("Not the expected character (b)"))
84 | ),
85 | printerTest(
86 | "s?, passing",
87 | (charA ~ charB).?,
88 | Some(('a', 'b'))
89 | )(
90 | isRight(equalTo("ab"))
91 | ),
92 | printerTest(
93 | "s?, not passing",
94 | (charA ~ charB).?,
95 | None
96 | )(
97 | isRight(equalTo(""))
98 | ),
99 | suite("repeat")(
100 | printerTest("repeat empty", charA.repeat, Chunk.empty)(
101 | isRight(equalTo(""))
102 | ),
103 | printerTest("repeat 1", charA.repeat, Chunk('a'))(
104 | isRight(equalTo("a"))
105 | ),
106 | printerTest("repeat 3", charA.repeat, Chunk('a', 'a', 'a'))(
107 | isRight(equalTo("aaa"))
108 | )
109 | ),
110 | suite("repeat0")(
111 | printerTest("repeat0 empty", charA.repeat0, Chunk.empty)(
112 | isRight(equalTo(""))
113 | ),
114 | printerTest("repeat0 1", charA.repeat0, Chunk('a'))(
115 | isRight(equalTo("a"))
116 | ),
117 | printerTest("repeat0 3", charA.repeat0, Chunk('a', 'a', 'a'))(
118 | isRight(equalTo("aaa"))
119 | )
120 | ),
121 | suite("repeatWithSep")(
122 | printerTest("repeatWithSep", Syntax.anyChar.repeatWithSep(Syntax.char('-')), Chunk('a', 'b', 'c'))(
123 | isRight(equalTo("a-b-c"))
124 | )
125 | ),
126 | printerTest_("from", ((charA ~ charB).asPrinter).from[TestCaseClass], TestCaseClass('a', 'b'))(
127 | isRight(equalTo("ab"))
128 | )
129 | )
130 | )
131 |
132 | private def printerTest[E, T](name: String, syntax: Syntax[E, Char, Char, T], input: T)(
133 | assertion: Assertion[Either[E, String]]
134 | ): Spec[Any, Nothing] =
135 | test(name)(assert(syntax.printString(input))(assertion))
136 |
137 | private def printerTest_[E, T](name: String, printer: Printer[E, Char, T], input: T)(
138 | assertion: Assertion[Either[E, String]]
139 | ): Spec[Any, Nothing] =
140 | test(name)(assert(printer.printString(input))(assertion))
141 | }
142 |
--------------------------------------------------------------------------------
/zio-parser/shared/src/test/scala/zio/parser/examples/JsonExample.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.examples
2 |
3 | import zio.Chunk
4 | import zio.parser.{Syntax, _}
5 | import zio.test.Assertion.{equalTo, isRight}
6 | import zio.test._
7 |
8 | object JsonExample extends ZIOSpecDefault {
9 | sealed abstract class Json
10 | object Json {
11 | final case class Obj(fields: Chunk[(String, Json)]) extends Json
12 | final case class Arr(elements: Chunk[Json]) extends Json
13 | final case class Bool(value: Boolean) extends Json
14 | final case class Str(value: String) extends Json
15 | final case class Num(value: java.math.BigDecimal) extends Json
16 | case object Null extends Json
17 | }
18 |
19 | val whitespace: Syntax[String, Char, Char, Char] = Syntax.charIn(' ', '\t', '\r', '\n')
20 | val whitespaces: Syntax[String, Char, Char, Unit] = whitespace.*.asPrinted((), Chunk(' '))
21 |
22 | val quote: Syntax[String, Char, Char, Unit] = Syntax.char('\"')
23 | val escapedChar: Syntax[String, Char, Char, Char] = Syntax.charNotIn('\"') // TODO: real escaping support
24 | val quotedString: Syntax[String, Char, Char, String] = (quote ~> escapedChar.*.string <~ quote)
25 |
26 | val nul: Syntax[String, Char, Char, Json.Null.type] = Syntax.string("null", Json.Null)
27 |
28 | val bool: Syntax[String, Char, Char, Json.Bool] =
29 | Syntax.string("true", Json.Bool(true)) <>
30 | Syntax.string("false", Json.Bool(false))
31 |
32 | val str: Syntax[String, Char, Char, Json.Str] = quotedString
33 | .transform(Json.Str.apply, (s: Json.Str) => s.value)
34 |
35 | val digits = Syntax.digit.repeat
36 | val signedIntStr: Syntax[String, Char, Char, (Option[Unit], Chunk[Char])] =
37 | Syntax.char('-').? ~ digits
38 | val frac: Syntax[String, Char, Char, Chunk[Char]] = Syntax.char('.') ~> digits
39 | val exp: Syntax[String, Char, Char, (Char, Char, Chunk[Char])] =
40 | Syntax.charIn('e', 'E') ~ Syntax.charIn('+', '-') ~ digits
41 | val jsonNum: Syntax[String, Char, Char, String] = (signedIntStr ~ frac.? ~ exp.?).string
42 |
43 | val num: Syntax[String, Char, Char, Json.Num] = jsonNum
44 | .transform(
45 | s => Json.Num(BigDecimal(s).bigDecimal),
46 | (v: Json.Num) => v.value.toString()
47 | )
48 |
49 | val listSep: Syntax[String, Char, Char, Unit] = Syntax.char(',').surroundedBy(whitespaces)
50 | lazy val list: Syntax[String, Char, Char, Json.Arr] =
51 | (Syntax.char('[') ~> json.repeatWithSep(listSep) <~ Syntax.char(']'))
52 | .transform(Json.Arr.apply, (arr: Json.Arr) => arr.elements)
53 |
54 | val keyValueSep: Syntax[String, Char, Char, Unit] = Syntax.char(':').surroundedBy(whitespaces)
55 | lazy val keyValue: Syntax[String, Char, Char, (String, Json)] =
56 | (str ~ keyValueSep ~ json).transform[(String, Json)](
57 | { case (key, value) => (key.value, value) },
58 | { case (key, value) => (Json.Str(key), value) }
59 | )
60 | val obj: Syntax[String, Char, Char, Json.Obj] = (Syntax.char('{') ~>
61 | keyValue.repeatWithSep(listSep).surroundedBy(whitespaces) <~
62 | Syntax.char('}'))
63 | .transform(Json.Obj.apply, (arr: Json.Obj) => arr.fields)
64 |
65 | lazy val json: Syntax[String, Char, Char, Json] =
66 | nul.widen[Json] <> bool.widen[Json] <> str.widen[Json] <> num.widen[Json] <> list.widen[Json] <> obj.widen[Json]
67 |
68 | // Debug.printParserTree(json.asParser)
69 | // println("-----")
70 | // Debug.printParserTree(json.asParser.optimized)
71 |
72 | override def spec: Spec[Environment, Any] =
73 | suite("JSON example")(
74 | parsingTests("parsing with auto-backtrack", json.autoBacktracking),
75 | parsingTests("parsing with manual-backtrack", json.manualBacktracking)
76 | )
77 |
78 | def parsingTests(
79 | name: String,
80 | json: Syntax[String, Char, Char, Json]
81 | ): Spec[Any, Nothing] =
82 | suite(name)(
83 | test("null") {
84 | assert(json.parseString("null"))(
85 | isRight(equalTo(Json.Null))
86 | )
87 | },
88 | test("true") {
89 | assert(json.parseString("true"))(
90 | isRight(equalTo(Json.Bool(true)))
91 | )
92 | },
93 | test("123") {
94 | assert(json.parseString("123"))(
95 | isRight(equalTo(Json.Num(BigDecimal(123).bigDecimal)))
96 | )
97 | },
98 | test("string") {
99 | assert(json.parseString("\"hello world\""))(
100 | isRight(equalTo(Json.Str("hello world")))
101 | )
102 | },
103 | test("array") {
104 | assert(json.parseString("[1, null, 3]"))(
105 | isRight(
106 | equalTo(
107 | Json.Arr(Chunk(Json.Num(BigDecimal(1).bigDecimal), Json.Null, Json.Num(BigDecimal(3).bigDecimal)))
108 | )
109 | )
110 | )
111 | },
112 | test("obj") {
113 | assert(json.parseString("""{ "x": 0, "hello": "world", "y": true, "z": [1, 2, 3] }"""))(
114 | isRight(
115 | equalTo(
116 | Json.Obj(
117 | Chunk(
118 | "x" -> Json.Num(BigDecimal(0).bigDecimal),
119 | "hello" -> Json.Str("world"),
120 | "y" -> Json.Bool(true),
121 | "z" -> Json.Arr(
122 | Chunk(
123 | Json.Num(BigDecimal(1).bigDecimal),
124 | Json.Num(BigDecimal(2).bigDecimal),
125 | Json.Num(BigDecimal(3).bigDecimal)
126 | )
127 | )
128 | )
129 | )
130 | )
131 | )
132 | )
133 | }
134 | )
135 | }
136 |
--------------------------------------------------------------------------------
/benchmarks/src/main/scala/zio/parser/benchmarks/basic/StringAlternatives.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.benchmarks.basic
2 |
3 | import zio.Chunk
4 | import zio.parser.benchmarks.{ParserBenchmark, ParserBenchmarkTestRunner, Parserz}
5 | import zio.parser.internal.Debug
6 |
7 | import scala.util.{Random, Try}
8 |
9 | /** Parsing a sequence of variable length string tokens that cannot be distinguished by their first character
10 | */
11 | class StringAlternatives extends ParserBenchmark[Tokens] {
12 | import StringAlternatives._
13 |
14 | override def loadInput(): String = {
15 | val N = 1000
16 | val sb = new StringBuilder
17 | for (_ <- 1 to N)
18 | Random.between(0, 4) match {
19 | case 0 => sb.append("true")
20 | case 1 => sb.append("false")
21 | case 2 => sb.append("maybe")
22 | case 3 => sb.append("maybenot")
23 | }
24 | sb.toString()
25 | }
26 |
27 | override final val zioSyntax: zio.parser.Syntax[String, Char, Char, Tokens] = {
28 | import zio.parser._
29 |
30 | val t = Syntax.string("true", True)
31 | val f = Syntax.string("false", False)
32 | val m = Syntax.string("maybe", Maybe)
33 | val mn = Syntax.string("maybenot", MaybeNot).backtrack
34 | val token = t.widen[Token] | f.widen[Token] | mn.widen[Token] | m.widen[Token]
35 | token.repeat0
36 | .transform(
37 | (chunk: Chunk[Token]) => Tokens(chunk),
38 | (tks: Tokens) => Chunk.fromIterable(tks.tokens)
39 | )
40 | .manualBacktracking
41 | }
42 |
43 | override final val catParser: cats.parse.Parser0[Tokens] = {
44 | import cats.parse._
45 |
46 | val t = Parser.string("true").as(True)
47 | val f = Parser.string("false").as(False)
48 | val m = Parser.string("maybe").as(Maybe)
49 | val mn = Parser.string("maybenot").as(MaybeNot)
50 |
51 | val token = t | f | mn.backtrack | m
52 | token.rep0.map(Tokens.apply)
53 | }
54 |
55 | override final def fastParseP[P: fastparse.P]: fastparse.P[Tokens] = {
56 | import fastparse._
57 | import NoWhitespace._
58 |
59 | def t = P("true").map(_ => True)
60 | def f = P("false").map(_ => False)
61 | def m = P("maybe").map(_ => Maybe)
62 | def mn = P("maybenot").map(_ => MaybeNot)
63 |
64 | def token = t | f | mn | m
65 | token./.rep.map(Tokens.apply) ~ End
66 | }
67 |
68 | override final val attoParser: atto.Parser[Tokens] = {
69 | import atto._
70 | import Atto._
71 |
72 | val t = string("true").map(_ => True)
73 | val f = string("false").map(_ => False)
74 | val m = string("maybe").map(_ => Maybe)
75 | val mn = string("maybenot").map(_ => MaybeNot)
76 |
77 | val token = t | f | mn | m
78 | many(token).map(Tokens.apply)
79 | }
80 |
81 | override final def runParboiledParser(input: String): Try[Tokens] = {
82 | val parser = new ParboiledTest(input)
83 | parser.InputLine.run()
84 | }
85 |
86 | override final val parsley: org.http4s.parsley.Parsley[Tokens] = {
87 | import org.http4s.parsley._
88 | import org.http4s.parsley.Parsley._
89 | import org.http4s.parsley.Combinator._
90 |
91 | val t = Char.string("true").map(_ => True)
92 | val f = Char.string("false").map(_ => False)
93 | val m = Char.string("maybe").map(_ => Maybe)
94 | val mn = Char.string("maybenot").map(_ => MaybeNot)
95 |
96 | val token = t <|> f <|> attempt(mn) <|> m
97 | many(token).map(Tokens.apply)
98 | }
99 |
100 | override final val parserz: Parserz.Grammar[Any, Nothing, String, Tokens] = {
101 | import Parserz._
102 | import Parserz.Grammar._
103 |
104 | def token(t: List[Char]): Grammar[Any, Nothing, String, List[Char]] = consume(
105 | cs => if (cs.startsWith(t)) Right((cs.drop(t.length), t)) else Left("expected: " + t),
106 | { case (cs, _) => Right(t reverse_::: cs) }
107 | )
108 |
109 | val t = token("true".toList).map(_ => True, (_: True.type) => "true".toList)
110 | val f = token("false".toList).map(_ => False, (_: False.type) => "false".toList)
111 | val m = token("maybe".toList).map(_ => Maybe, (_: Maybe.type) => "maybe".toList)
112 | val mn = token("maybenot".toList).map(_ => MaybeNot, (_: MaybeNot.type) => "maybenot".toList)
113 |
114 | val tok: Grammar[Any, Nothing, String, Token] =
115 | (t | f | mn | m).map[Token](
116 | {
117 | case Left(Left(Left(v))) => v
118 | case Left(Left(Right(v))) => v
119 | case Left(Right(v)) => v
120 | case Right(v) => v
121 | },
122 | {
123 | case True => Left(Left(Left(True)))
124 | case False => Left(Left(Right(False)))
125 | case MaybeNot => Left(Right(MaybeNot))
126 | case Maybe => Right(Maybe)
127 | }
128 | )
129 | tok.rep.map(Tokens.apply, (tks: Tokens) => tks.tokens.toList)
130 | }
131 | }
132 |
133 | case class Tokens(tokens: Seq[Token]) {
134 | override def toString: String = tokens.mkString(", ")
135 | }
136 |
137 | sealed trait Token
138 | case object True extends Token
139 | case object False extends Token
140 | case object Maybe extends Token
141 | case object MaybeNot extends Token
142 |
143 | object StringAlternatives extends ParserBenchmarkTestRunner[Tokens, StringAlternatives] {
144 | override val self: ParserBenchmark[Tokens] = new StringAlternatives
145 |
146 | class ParboiledTest(val input: org.parboiled2.ParserInput) extends org.parboiled2.Parser {
147 | import org.parboiled2._
148 |
149 | def InputLine = rule(Toks ~ EOI)
150 | def Toks = rule(oneOrMore(Tok) ~> Tokens)
151 | def Tok = rule {
152 | capture(T | F | MN | M) ~> {
153 | case "true" => True
154 | case "false" => False
155 | case "maybe" => Maybe
156 | case "maybenot" => MaybeNot
157 | }
158 | }
159 | def T = rule("true")
160 | def F = rule("false")
161 | def M = rule("maybe")
162 | def MN = rule("maybenot")
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/benchmarks/src/main/scala/zio/parser/benchmarks/json/JsonCatsParse.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.benchmarks.json
2 |
3 | import cats.parse.{Numbers, Parser => P, Parser0 => P0}
4 | import zio.Chunk
5 |
6 | // Based on: https://github.com/typelevel/cats-parse/blob/main/bench/src/main/scala/cats/parse/bench/self.scala
7 | object JsonCatsParse {
8 | private[this] val whitespace: P[Unit] = P.charIn(" \t\r\n").void
9 | private[this] val whitespaces0: P0[Unit] = whitespace.rep0.void
10 |
11 | /** This doesn't have to be super fast (but is fairly fast) since we use it in places where speed won't matter:
12 | * feeding it into a program that will convert it to bosatsu structured data
13 | */
14 | val parser: P[Json] = P.recursive[Json] { recurse =>
15 | val pnull = P.string("null").as(Json.Null)
16 | val bool = P.string("true").as(Json.Bool(true)).orElse(P.string("false").as(Json.Bool(false)))
17 | val justStr = JsonStringUtil.escapedString('"')
18 | val str = justStr.map(Json.Str)
19 | val num = Numbers.jsonNumber.map(s => Json.Num(BigDecimal(s)))
20 |
21 | val listSep: P[Unit] =
22 | P.char(',').surroundedBy(whitespaces0).void
23 |
24 | def rep0[A](pa: P[A]): P0[List[A]] =
25 | pa.repSep0(listSep).surroundedBy(whitespaces0)
26 |
27 | val list = rep0(recurse).with1
28 | .between(P.char('['), P.char(']'))
29 | .map(vs => Json.Arr(Chunk.fromIterable(vs)))
30 |
31 | val kv: P[(String, Json)] =
32 | justStr ~ (P.char(':').surroundedBy(whitespaces0) *> recurse)
33 |
34 | val obj = rep0(kv).with1
35 | .between(P.char('{'), P.char('}'))
36 | .map(vs => Json.Obj(Chunk.fromIterable(vs)))
37 |
38 | P.oneOf(str :: num :: list :: obj :: bool :: pnull :: Nil)
39 | }
40 |
41 | // any whitespace followed by json followed by whitespace followed by end
42 | val parserFile: P[Json] = parser.between(whitespaces0, whitespaces0 ~ P.end)
43 | }
44 |
45 | object JsonStringUtil extends GenericStringUtil {
46 | // Here are the rules for escaping in json
47 | lazy val decodeTable: Map[Char, Char] =
48 | Map(
49 | ('\\', '\\'),
50 | ('\'', '\''),
51 | ('\"', '\"'),
52 | ('b', 8.toChar), // backspace
53 | ('f', 12.toChar), // form-feed
54 | ('n', '\n'),
55 | ('r', '\r'),
56 | ('t', '\t')
57 | )
58 | }
59 |
60 | abstract class GenericStringUtil {
61 | protected def decodeTable: Map[Char, Char]
62 |
63 | private val encodeTable = decodeTable.iterator.map { case (v, k) => (k, s"\\$v") }.toMap
64 |
65 | private val nonPrintEscape: Array[String] =
66 | (0 until 32).map { c =>
67 | val strHex = c.toHexString
68 | val strPad = List.fill(4 - strHex.length)('0').mkString
69 | s"\\u$strPad$strHex"
70 | }.toArray
71 |
72 | val escapedToken: P[Unit] = {
73 | val escapes = P.charIn(decodeTable.keys.toSeq)
74 |
75 | val oct = P.charIn('0' to '7')
76 | val octP = P.char('o') ~ oct ~ oct
77 |
78 | val hex = P.charIn(('0' to '9') ++ ('a' to 'f') ++ ('A' to 'F'))
79 | val hex2 = hex ~ hex
80 | val hexP = P.char('x') ~ hex2
81 |
82 | val hex4 = hex2 ~ hex2
83 | val u4 = P.char('u') ~ hex4
84 | val hex8 = hex4 ~ hex4
85 | val u8 = P.char('U') ~ hex8
86 |
87 | val after = P.oneOf[Any](escapes :: octP :: hexP :: u4 :: u8 :: Nil)
88 | (P.char('\\') ~ after).void
89 | }
90 |
91 | /** String content without the delimiter
92 | */
93 | def undelimitedString(endP: P[Unit]): P[String] =
94 | escapedToken.backtrack
95 | .orElse((!endP).with1 ~ P.anyChar)
96 | .rep
97 | .string
98 | .flatMap { str =>
99 | unescape(str) match {
100 | case Right(str1) => P.pure(str1)
101 | case Left(_) => P.fail
102 | }
103 | }
104 |
105 | private val simpleString: P0[String] =
106 | P.charsWhile0(c => c >= ' ' && c != '"' && c != '\\')
107 |
108 | def escapedString(q: Char): P[String] = {
109 | val end: P[Unit] = P.char(q)
110 | end *> ((simpleString <* end).backtrack
111 | .orElse(undelimitedString(end) <* end))
112 | }
113 |
114 | def escape(quoteChar: Char, str: String): String = {
115 | // We can ignore escaping the opposite character used for the string
116 | // x isn't escaped anyway and is kind of a hack here
117 | val ignoreEscape = if (quoteChar == '\'') '"' else if (quoteChar == '"') '\'' else 'x'
118 | str.flatMap { c =>
119 | if (c == ignoreEscape) c.toString
120 | else
121 | encodeTable.get(c) match {
122 | case None =>
123 | if (c < ' ') nonPrintEscape(c.toInt)
124 | else c.toString
125 | case Some(esc) => esc
126 | }
127 | }
128 | }
129 |
130 | def unescape(str: String): Either[Int, String] = {
131 | val sb = new java.lang.StringBuilder
132 | def decodeNum(idx: Int, size: Int, base: Int): Int = {
133 | val end = idx + size
134 | if (end <= str.length) {
135 | val intStr = str.substring(idx, end)
136 | val asInt =
137 | try Integer.parseInt(intStr, base)
138 | catch { case _: NumberFormatException => ~idx }
139 | sb.append(asInt.toChar)
140 | end
141 | } else ~(str.length)
142 | }
143 | @annotation.tailrec
144 | def loop(idx: Int): Int =
145 | if (idx >= str.length) {
146 | // done
147 | idx
148 | } else if (idx < 0) {
149 | // error from decodeNum
150 | idx
151 | } else {
152 | val c0 = str.charAt(idx)
153 | if (c0 != '\\') {
154 | sb.append(c0)
155 | loop(idx + 1)
156 | } else {
157 | // str(idx) == \
158 | val nextIdx = idx + 1
159 | if (nextIdx >= str.length) {
160 | // error we expect there to be a character after \
161 | ~idx
162 | } else {
163 | val c = str.charAt(nextIdx)
164 | decodeTable.get(c) match {
165 | case Some(d) =>
166 | sb.append(d)
167 | loop(idx + 2)
168 | case None =>
169 | c match {
170 | case 'o' => loop(decodeNum(idx + 2, 2, 8))
171 | case 'x' => loop(decodeNum(idx + 2, 2, 16))
172 | case 'u' => loop(decodeNum(idx + 2, 4, 16))
173 | case 'U' => loop(decodeNum(idx + 2, 8, 16))
174 | case other =>
175 | // \c is interpretted as just \c, if the character isn't escaped
176 | sb.append('\\')
177 | sb.append(other)
178 | loop(idx + 2)
179 | }
180 | }
181 | }
182 | }
183 | }
184 |
185 | val res = loop(0)
186 | if (res < 0) Left(~res)
187 | else Right(sb.toString)
188 | }
189 | }
190 |
--------------------------------------------------------------------------------
/zio-parser/shared/src/main/scala/zio/parser/internal/Debug.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.internal
2 |
3 | import zio.parser.Parser.ErasedParser
4 | import zio.parser.{Parser, Regex}
5 |
6 | object Debug {
7 |
8 | case class DebugPrinterState(indent: Int, visited: Map[ErasedParser, Int], lastId: Int) {
9 | def visit(node: ErasedParser): DebugPrinterState =
10 | copy(visited = this.visited + (node -> lastId), lastId = this.lastId + 1)
11 | def in: DebugPrinterState =
12 | copy(indent = this.indent + 1)
13 |
14 | def mergeVisited(other: DebugPrinterState): DebugPrinterState =
15 | copy(
16 | visited = this.visited ++ other.visited,
17 | lastId = Math.max(this.lastId, other.lastId)
18 | )
19 | }
20 |
21 | private val initialState: DebugPrinterState = DebugPrinterState(0, Map.empty, 0)
22 |
23 | /** Prints a parser tree
24 | *
25 | * Useful for debugging constructed parsers.
26 | */
27 | def printParserTree(syntax: ErasedParser, state: DebugPrinterState = initialState): DebugPrinterState = {
28 | implicit val st: DebugPrinterState = state
29 | state.visited.get(syntax) match {
30 | case Some(id) =>
31 | printIndented(s"#[$id]")
32 | state
33 | case None =>
34 | syntax match {
35 | case Parser.Lazy(inner) =>
36 | printIndented("Lazy")
37 | printParserTree(inner(), state.visit(syntax).in)
38 | case Parser.Succeed(value) =>
39 | printIndented(s"Succeed($value")
40 | state
41 | case Parser.Fail(failure) =>
42 | printIndented(s"Fail($failure")
43 | state
44 | case Parser.Failed(failure) =>
45 | printIndented(s"Failed($failure")
46 | state
47 | case Parser.Named(inner, name) =>
48 | printIndented(s"Named($name)")
49 | printParserTree(inner, state.visit(syntax).in)
50 | case Parser.SkipRegex(regex, onFailure) =>
51 | printIndented(s"SkipRegex($onFailure)")
52 | printRegexTree(regex, state.visit(syntax).in)
53 | state
54 | case Parser.ParseRegex(regex, onFailure) =>
55 | printIndented(s"ParseRegex($onFailure)")
56 | printRegexTree(regex, state.visit(syntax).in)
57 | state
58 | case Parser.ParseRegexLastChar(regex, onFailure) =>
59 | printIndented(s"ParseRegexLastChar($onFailure)")
60 | printRegexTree(regex, state.visit(syntax).in)
61 | state
62 | case Parser.TransformEither(inner, _) =>
63 | printIndented(s"TransformEither")
64 | printParserTree(inner, state.visit(syntax).in)
65 | case Parser.Transform(inner, _) =>
66 | printIndented(s"Transform")
67 | printParserTree(inner, state.visit(syntax).in)
68 | case Parser.Ignore(inner, to) =>
69 | printIndented(s"Ignore($to)")
70 | printParserTree(inner, state.visit(syntax).in)
71 | case Parser.CaptureString(inner) =>
72 | printIndented("CaptureString")
73 | printParserTree(inner, state.visit(syntax).in)
74 | case Parser.Zip(left, right, _) =>
75 | printIndented(s"Zip")
76 | val leftSt = printParserTree(left, state.visit(syntax).in)
77 | printParserTree(right, state.mergeVisited(leftSt).in)
78 | case Parser.ZipLeft(left, right) =>
79 | printIndented(s"ZipLeft")
80 | val leftSt = printParserTree(left, state.visit(syntax).in)
81 | printParserTree(right, state.mergeVisited(leftSt).in)
82 | case Parser.ZipRight(left, right) =>
83 | printIndented(s"ZipRight")
84 | val leftSt = printParserTree(left, state.visit(syntax).in)
85 | printParserTree(right, state.mergeVisited(leftSt).in)
86 | case Parser.FlatMap(inner, _) =>
87 | printIndented(s"FlatMap")
88 | printParserTree(inner, state.visit(syntax).in)
89 | case Parser.OrElseEither(left, right) =>
90 | printIndented(s"OrElseEither")
91 | val leftSt = printParserTree(left, state.visit(syntax).in)
92 | printParserTree(right, state.mergeVisited(leftSt).in)
93 | case Parser.OrElse(left, right) =>
94 | printIndented(s"OrElse")
95 | val leftSt = printParserTree(left, state.visit(syntax).in)
96 | printParserTree(right, state.mergeVisited(leftSt).in)
97 | case Parser.Optional(inner) =>
98 | printIndented(s"Optional")
99 | printParserTree(inner, state.visit(syntax).in)
100 | case Parser.Repeat(inner, min, max) =>
101 | printIndented(s"Repeat($min, $max)")
102 | printParserTree(inner, state.visit(syntax).in)
103 | case Parser.Backtrack(inner) =>
104 | printIndented(s"Backtrack")
105 | printParserTree(inner, state.visit(syntax).in)
106 | case Parser.SetAutoBacktrack(inner, enabled) =>
107 | printIndented(s"SetAutoBacktrack($enabled)")
108 | printParserTree(inner, state.visit(syntax).in)
109 | case Parser.MapError(inner, _) =>
110 | printIndented(s"MapError")
111 | printParserTree(inner, state.visit(syntax).in)
112 | case Parser.Not(inner, _) =>
113 | printIndented("Not")
114 | printParserTree(inner, state.visit(syntax).in)
115 | case Parser.Index =>
116 | printIndented("Index")
117 | state
118 | case Parser.End =>
119 | printIndented("End")
120 | state
121 | case Parser.Passthrough() =>
122 | printIndented("Passthrough")
123 | state
124 | }
125 | }
126 | }
127 |
128 | private def printRegexTree(regex: Regex, state: DebugPrinterState): DebugPrinterState = {
129 | implicit val st: DebugPrinterState = state
130 | regex match {
131 | case Regex.Succeed =>
132 | printIndented("")
133 | state
134 | case Regex.OneOf(_) =>
135 | printIndented("")
136 | state
137 | case Regex.Sequence(first, second) =>
138 | printIndented("")
139 | printRegexTree(first, state.in)
140 | printRegexTree(second, state.in)
141 | case Regex.Repeat(regex, min, max) =>
142 | printIndented(s"")
143 | printRegexTree(regex, state.in)
144 | case Regex.Or(left, right) =>
145 | printIndented("")
146 | printRegexTree(left, state.in)
147 | printRegexTree(right, state.in)
148 | case Regex.And(left, right) =>
149 | printIndented("")
150 | printRegexTree(left, state.in)
151 | printRegexTree(right, state.in)
152 | }
153 | }
154 |
155 | private def printIndented(str: String)(implicit state: DebugPrinterState): Unit =
156 | println(s"[${state.lastId}] " + (" " * state.indent) + str)
157 | }
158 |
--------------------------------------------------------------------------------
/zio-parser/shared/src/main/scala/zio/parser/package.scala:
--------------------------------------------------------------------------------
1 | package zio
2 |
3 | import zio.parser.internal.{PUnzippable, PZippable}
4 |
5 | import scala.reflect.ClassTag
6 | import scala.util.Try
7 |
8 | package object parser extends ImplicitTupleConversion {
9 |
10 | implicit final class AnyParserOps[Err, In](private val self: Parser[Err, In, Any]) extends AnyVal {
11 |
12 | /** Symbolic alias for zipRight
13 | */
14 | def ~>[Err2 >: Err, In2 <: In, Result](
15 | that: => Parser[Err2, In2, Result]
16 | ): Parser[Err2, In2, Result] =
17 | zipRight(that)
18 |
19 | /** Concatenates this parser with that parser, and if both succeeds, discard the first result and use the second.
20 | */
21 | def zipRight[Err2 >: Err, In2 <: In, Result](
22 | that: => Parser[Err2, In2, Result]
23 | ): Parser[Err2, In2, Result] =
24 | Parser.ZipRight(Parser.Lazy(() => self), Parser.Lazy(() => that))
25 | }
26 |
27 | implicit final class TupleParserOps[Err, In, Result](private val self: Parser[Err, In, Result]) extends AnyVal {
28 |
29 | /** Maps the parser's successful tuple result to the given case class */
30 | def to[Result2 <: Product](implicit conversion: TupleConversion[Result2, Result]): Parser[Err, In, Result2] =
31 | self.map(conversion.from)
32 | }
33 |
34 | implicit final class AnySyntaxOps[Err, In, Out](private val self: Syntax[Err, In, Out, Unit]) extends AnyVal {
35 |
36 | /** Symbolic alias for zipRight
37 | */
38 | def ~>[Err2 >: Err, In2 <: In, Out2 >: Out, Value2](
39 | that: => Syntax[Err2, In2, Out2, Value2]
40 | ): Syntax[Err2, In2, Out2, Value2] =
41 | zipRight(that)
42 |
43 | /** Concatenates this parser with that parser, and if both succeeds, discard the first result and use the second.
44 | */
45 | def zipRight[Err2 >: Err, In2 <: In, Out2 >: Out, Value2](
46 | that: => Syntax[Err2, In2, Out2, Value2]
47 | ): Syntax[Err2, In2, Out2, Value2] =
48 | Syntax.from(
49 | self.asParser ~> that.asParser,
50 | self.asPrinter ~> that.asPrinter
51 | )
52 |
53 | /** Ignores the result of the syntax and result in 'value' instead */
54 | def as[Value2](value: => Value2): Syntax[Err, In, Out, Value2] =
55 | Syntax.from(
56 | self.asParser.as(value),
57 | self.asPrinter.asPrinted(value, ())
58 | )
59 | }
60 |
61 | implicit class StringErrSyntaxOps[In, Out, Value](private val self: Syntax[String, In, Out, Value]) extends AnyVal {
62 |
63 | /** Widens the parser to a supertype of its result
64 | *
65 | * This is useful in combination with the orElse (<>) operator. For example a JSON parser can be expressed by a
66 | * combination of parsers for the individual json type widened to Json:
67 | * {{{
68 | * nul.widen[Json] <>
69 | * bool.widen[Json] <>
70 | * str.widen[Json] <>
71 | * num.widen[Json] <>
72 | * list.widen[Json] <>
73 | * obj.widen[Json]
74 | * }}}
75 | */
76 | def widen[D](implicit ev: Value <:< D, tag: ClassTag[Value]): Syntax[String, In, Out, D] =
77 | self.asParser.asInstanceOf[Parser[String, In, D]] <=> self.asPrinter.contramapEither((value: D) =>
78 | if (tag.runtimeClass.isAssignableFrom(value.getClass)) {
79 | Right(value.asInstanceOf[Value])
80 | } else {
81 | val name = Try(tag.runtimeClass.getSimpleName).fold(error => error.getMessage, identity[String])
82 | Left(s"Not a $name")
83 | }
84 | )
85 | }
86 |
87 | implicit class SyntaxOps[Err, In, Out, Value](private val self: Syntax[Err, In, Out, Value]) {
88 |
89 | /** Symbolic alias for zip */
90 | final def ~[Err2 >: Err, In2 <: In, Out2 >: Out, Value2, ZippedValue](
91 | that: => Syntax[Err2, In2, Out2, Value2]
92 | )(implicit
93 | unzippableValue: PUnzippable.In[Value, Value2, ZippedValue],
94 | zippableResult: PZippable.Out[Value, Value2, ZippedValue]
95 | ): Syntax[Err2, In2, Out2, ZippedValue] =
96 | zip(that)
97 |
98 | /** Concatenates this syntax with 'that' syntax. In case both parser succeeds, the result is a pair of the results.
99 | * The printer destructures a pair and prints the left value with this, the right value with 'that'.
100 | */
101 | final def zip[Err2 >: Err, In2 <: In, Out2 >: Out, Value2, ZippedValue](
102 | that: => Syntax[Err2, In2, Out2, Value2]
103 | )(implicit
104 | unzippableValue: PUnzippable.In[Value, Value2, ZippedValue],
105 | zippableResult: PZippable.Out[Value, Value2, ZippedValue]
106 | ): Syntax[Err2, In2, Out2, ZippedValue] =
107 | Syntax.from(
108 | self.asParser.zip(that.asParser),
109 | self.asPrinter.zip(that.asPrinter)
110 | )
111 |
112 | /** Transforms the syntax of a tuple to a syntax of a given case class */
113 | def of[Value2 <: Product](implicit
114 | conversion: TupleConversion[Value2, Value],
115 | ev: Value =:= Value
116 | ): Syntax[Err, In, Out, Value2] =
117 | self.transform(conversion.from, conversion.to)
118 |
119 | def widenWith[D](err: Err)(implicit ev: Value <:< D, tag: ClassTag[Value]): Syntax[Err, In, Out, D] =
120 | self.asParser.asInstanceOf[Parser[Err, In, D]] <=> self.asPrinter.contramapEither((value: D) =>
121 | if (tag.runtimeClass.isAssignableFrom(value.getClass)) {
122 | Right(value.asInstanceOf[Value])
123 | } else {
124 | Left(err)
125 | }
126 | )
127 | }
128 |
129 | implicit class ParserOps[Err, In, Value](private val self: Parser[Err, In, Value]) extends AnyVal {
130 |
131 | /** Combines this parser with that printer to get a syntax.
132 | *
133 | * This operation enables the use of parser or printer-specific operators to build up fragments of a syntax. The
134 | * resulting syntax can be used as a building block to create bigger syntaxes.
135 | */
136 | def <=>[Out](that: Printer[Err, Out, Value]): Syntax[Err, In, Out, Value] =
137 | Syntax.from[Err, In, Out, Value](self, that)
138 | }
139 |
140 | implicit class UnitPrinterOps[Err, Out](private val self: Printer[Err, Out, Unit]) extends AnyVal {
141 |
142 | /** Symbolic alias for zipRight
143 | */
144 | def ~>[Err2 >: Err, Out2 >: Out, Value](
145 | that: => Printer[Err2, Out2, Value]
146 | ): Printer[Err2, Out2, Value] =
147 | zipRight(that)
148 |
149 | /** Print Unit with this, then print that and use the second printer's result value
150 | */
151 | def zipRight[Err2 >: Err, Out2 >: Out, Value](
152 | that: => Printer[Err2, Out2, Value]
153 | ): Printer[Err2, Out2, Value] =
154 | Printer.ZipRight(Printer.Lazy(() => self), Printer.Lazy(() => that))
155 | }
156 |
157 | implicit class PrinterOps[Err, Out, Value](private val self: Printer[Err, Out, Value]) extends AnyVal {
158 |
159 | /** Symbolic alias for zipRight
160 | */
161 | def ~>(that: => Printer[Err, Out, Value]): Printer[Err, Out, Value] =
162 | zipRight(that)
163 |
164 | /** Print this, then print that and use the second printer's result value. Both printers get the same value to be
165 | * printed.
166 | */
167 | def zipRight(that: => Printer[Err, Out, Value]): Printer[Err, Out, Value] =
168 | Printer.ZipRight(Printer.Lazy(() => self), Printer.Lazy(() => that))
169 |
170 | /** Symbolic alias for zip */
171 | final def ~[Err2 >: Err, Out2 >: Out, Value2, ZippedValue](
172 | that: => Printer[Err2, Out2, Value2]
173 | )(implicit zippableValue: PUnzippable.In[Value, Value2, ZippedValue]): Printer[Err2, Out2, ZippedValue] =
174 | zip(that)
175 |
176 | /** Take a pair to be printed, print the left value with this, and the right value with 'that'. The result is a pair
177 | * of both printer's results.
178 | */
179 | final def zip[Err2 >: Err, Out2 >: Out, Value2, ZippedValue](
180 | that: => Printer[Err2, Out2, Value2]
181 | )(implicit unzippableValue: PUnzippable.In[Value, Value2, ZippedValue]): Printer[Err2, Out2, ZippedValue] =
182 | Printer.Zip(Printer.Lazy(() => self), Printer.Lazy(() => that), unzippableValue.unzip)
183 | }
184 |
185 | implicit class TuplePrinterOps[Err, Out, Value <: Product](
186 | private val self: Printer[Err, Out, Value]
187 | ) {
188 |
189 | /** Transforms the printer's input from a given case class for a tuple printer */
190 | def from[Value2](implicit
191 | conversion: TupleConversion[Value2, Value]
192 | ): Printer[Err, Out, Value2] =
193 | self.contramap(conversion.to)
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Dropbox settings and caches
2 | .dropbox
3 | .dropbox.attr
4 | .dropbox.cache
5 | # -*- mode: gitignore; -*-
6 | *~
7 | \#*\#
8 | /.emacs.desktop
9 | /.emacs.desktop.lock
10 | *.elc
11 | auto-save-list
12 | tramp
13 | .\#*
14 |
15 | # Org-mode
16 | .org-id-locations
17 | *_archive
18 |
19 | # flymake-mode
20 | *_flymake.*
21 |
22 | # eshell files
23 | /eshell/history
24 | /eshell/lastdir
25 |
26 | # elpa packages
27 | /elpa/
28 |
29 | # reftex files
30 | *.rel
31 |
32 | # AUCTeX auto folder
33 | /auto/
34 |
35 | # cask packages
36 | .cask/
37 | dist/
38 |
39 | # Flycheck
40 | flycheck_*.el
41 |
42 | # server auth directory
43 | /server/
44 |
45 | # projectiles files
46 | .projectile
47 |
48 | # directory configuration
49 | .dir-locals.el
50 | *~
51 |
52 | # temporary files which can be created if a process still has a handle open of a deleted file
53 | .fuse_hidden*
54 |
55 | # KDE directory preferences
56 | .directory
57 |
58 | # Linux trash folder which might appear on any partition or disk
59 | .Trash-*
60 |
61 | # .nfs files are created when an open file is removed but is still being accessed
62 | .nfs*
63 | # General
64 | .DS_Store
65 | .AppleDouble
66 | .LSOverride
67 |
68 | # Icon must end with two \r
69 | Icon
70 |
71 | # Thumbnails
72 | ._*
73 |
74 | # Files that might appear in the root of a volume
75 | .DocumentRevisions-V100
76 | .fseventsd
77 | .Spotlight-V100
78 | .TemporaryItems
79 | .Trashes
80 | .VolumeIcon.icns
81 | .com.apple.timemachine.donotpresent
82 |
83 | # Directories potentially created on remote AFP share
84 | .AppleDB
85 | .AppleDesktop
86 | Network Trash Folder
87 | Temporary Items
88 | .apdisk
89 | # Cache files for Sublime Text
90 | *.tmlanguage.cache
91 | *.tmPreferences.cache
92 | *.stTheme.cache
93 |
94 | # Workspace files are user-specific
95 | *.sublime-workspace
96 |
97 | # Project files should be checked into the repository, unless a significant
98 | # proportion of contributors will probably not be using Sublime Text
99 | # *.sublime-project
100 |
101 | # SFTP configuration file
102 | sftp-config.json
103 |
104 | # Package control specific files
105 | Package Control.last-run
106 | Package Control.ca-list
107 | Package Control.ca-bundle
108 | Package Control.system-ca-bundle
109 | Package Control.cache/
110 | Package Control.ca-certs/
111 | Package Control.merged-ca-bundle
112 | Package Control.user-ca-bundle
113 | oscrypto-ca-bundle.crt
114 | bh_unicode_properties.cache
115 |
116 | # Sublime-github package stores a github token in this file
117 | # https://packagecontrol.io/packages/sublime-github
118 | GitHub.sublime-settings
119 | # Ignore tags created by etags, ctags, gtags (GNU global) and cscope
120 | TAGS
121 | .TAGS
122 | !TAGS/
123 | tags
124 | .tags
125 | !tags/
126 | gtags.files
127 | GTAGS
128 | GRTAGS
129 | GPATH
130 | GSYMS
131 | cscope.files
132 | cscope.out
133 | cscope.in.out
134 | cscope.po.out
135 |
136 | *.tmproj
137 | *.tmproject
138 | tmtags
139 | # Swap
140 | [._]*.s[a-v][a-z]
141 | [._]*.sw[a-p]
142 | [._]s[a-rt-v][a-z]
143 | [._]ss[a-gi-z]
144 | [._]sw[a-p]
145 |
146 | # Session
147 | Session.vim
148 |
149 | # Temporary
150 | .netrwhist
151 | *~
152 | # Auto-generated tag files
153 | tags
154 | # Persistent undo
155 | [._]*.un~
156 | # Windows thumbnail cache files
157 | Thumbs.db
158 | ehthumbs.db
159 | ehthumbs_vista.db
160 |
161 | # Dump file
162 | *.stackdump
163 |
164 | # Folder config file
165 | [Dd]esktop.ini
166 |
167 | # Recycle Bin used on file shares
168 | $RECYCLE.BIN/
169 |
170 | # Windows Installer files
171 | *.cab
172 | *.msi
173 | *.msix
174 | *.msm
175 | *.msp
176 |
177 | # Windows shortcuts
178 | *.lnk
179 |
180 | .metadata
181 | bin/
182 | tmp/
183 | *.tmp
184 | *.bak
185 | *.swp
186 | *~.nib
187 | local.properties
188 | .settings/
189 | .loadpath
190 | .recommenders
191 |
192 | # External tool builders
193 | .externalToolBuilders/
194 |
195 | # Locally stored "Eclipse launch configurations"
196 | *.launch
197 |
198 | # PyDev specific (Python IDE for Eclipse)
199 | *.pydevproject
200 |
201 | # CDT-specific (C/C++ Development Tooling)
202 | .cproject
203 |
204 | # CDT- autotools
205 | .autotools
206 |
207 | # Java annotation processor (APT)
208 | .factorypath
209 |
210 | # PDT-specific (PHP Development Tools)
211 | .buildpath
212 |
213 | # sbteclipse plugin
214 | .target
215 |
216 | # Tern plugin
217 | .tern-project
218 |
219 | # TeXlipse plugin
220 | .texlipse
221 |
222 | # STS (Spring Tool Suite)
223 | .springBeans
224 |
225 | # Code Recommenders
226 | .recommenders/
227 |
228 | # Annotation Processing
229 | .apt_generated/
230 |
231 | # Scala IDE specific (Scala & Java development for Eclipse)
232 | .cache-main
233 | .scala_dependencies
234 | .worksheet
235 | # Ensime specific
236 | .ensime
237 | .ensime_cache/
238 | .ensime_lucene/
239 | # default application storage directory used by the IDE Performance Cache feature
240 | .data/
241 |
242 | # used for ADF styles caching
243 | temp/
244 |
245 | # default output directories
246 | classes/
247 | deploy/
248 | javadoc/
249 |
250 | # lock file, a part of Oracle Credential Store Framework
251 | cwallet.sso.lck# JEnv local Java version configuration file
252 | .java-version
253 |
254 | # Used by previous versions of JEnv
255 | .jenv-version
256 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
257 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
258 |
259 | # CMake
260 | cmake-build-*/
261 |
262 | # File-based project format
263 | *.iws
264 |
265 | # IntelliJ
266 | out/
267 |
268 | # mpeltonen/sbt-idea plugin
269 | .idea_modules/
270 |
271 | # JIRA plugin
272 | atlassian-ide-plugin.xml
273 |
274 | # Crashlytics plugin (for Android Studio and IntelliJ)
275 | com_crashlytics_export_strings.xml
276 | crashlytics.properties
277 | crashlytics-build.properties
278 | fabric.properties
279 |
280 | # Editor-based Rest Client
281 | nbproject/private/
282 | build/
283 | nbbuild/
284 | dist/
285 | nbdist/
286 | .nb-gradle/
287 | # Built application files
288 | *.apk
289 | *.ap_
290 |
291 | # Files for the ART/Dalvik VM
292 | *.dex
293 |
294 | # Java class files
295 | *.class
296 |
297 | # Generated files
298 | bin/
299 | gen/
300 | out/
301 |
302 | # Gradle files
303 | .gradle/
304 | build/
305 |
306 | # Local configuration file (sdk path, etc)
307 | local.properties
308 |
309 | # Proguard folder generated by Eclipse
310 | proguard/
311 |
312 | # Log Files
313 | *.log
314 |
315 | # Android Studio Navigation editor temp files
316 | .navigation/
317 |
318 | # Android Studio captures folder
319 | captures/
320 |
321 | # IntelliJ
322 | *.iml
323 | .idea
324 |
325 | # Keystore files
326 | # Uncomment the following line if you do not want to check your keystore files in.
327 | #*.jks
328 |
329 | # External native build folder generated in Android Studio 2.2 and later
330 | .externalNativeBuild
331 |
332 | # Google Services (e.g. APIs or Firebase)
333 | google-services.json
334 |
335 | # Freeline
336 | freeline.py
337 | freeline/
338 | freeline_project_description.json
339 |
340 | # fastlane
341 | fastlane/report.xml
342 | fastlane/Preview.html
343 | fastlane/screenshots
344 | fastlane/test_output
345 | fastlane/readme.md
346 | .gradle
347 | /build/
348 |
349 | # Ignore Gradle GUI config
350 | gradle-app.setting
351 |
352 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
353 | !gradle-wrapper.jar
354 |
355 | # Cache of project
356 | .gradletasknamecache
357 |
358 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
359 | # gradle/wrapper/gradle-wrapper.properties
360 | # Compiled class file
361 | *.class
362 |
363 | # Log file
364 | *.log
365 |
366 | # BlueJ files
367 | *.ctxt
368 |
369 | # Mobile Tools for Java (J2ME)
370 | .mtj.tmp/
371 |
372 | # Package Files #
373 | *.jar
374 | *.war
375 | *.nar
376 | *.ear
377 | *.zip
378 | *.tar.gz
379 | *.rar
380 |
381 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
382 | hs_err_pid*
383 | target/
384 | pom.xml.tag
385 | pom.xml.releaseBackup
386 | pom.xml.versionsBackup
387 | pom.xml.next
388 | release.properties
389 | dependency-reduced-pom.xml
390 | buildNumber.properties
391 | .mvn/timing.properties
392 | .mvn/wrapper/maven-wrapper.jar
393 | # Simple Build Tool
394 | # http://www.scala-sbt.org/release/docs/Getting-Started/Directories.html#configuring-version-control
395 |
396 | dist/*
397 | target/
398 | lib_managed/
399 | src_managed/
400 | project/boot/
401 | project/plugins/project/
402 | .history
403 | .cache
404 | .lib/
405 | *.class
406 | *.log
407 |
408 | .metals/
409 | metals.sbt
410 | .bloop/
411 | project/secret
412 | .bsp/
413 | .vscode/
414 |
415 | # mdoc
416 | website/node_modules
417 | website/build
418 | website/i18n/en.json
419 | website/static/api
420 |
--------------------------------------------------------------------------------
/zio-parser/shared/src/test/scala/zio/parser/RegexSpec.scala:
--------------------------------------------------------------------------------
1 | package zio.parser
2 |
3 | import zio._
4 | import zio.test.Assertion._
5 | import zio.test._
6 |
7 | object RegexSpec extends ZIOSpecDefault {
8 | val keywordStrings: List[String] =
9 | List(
10 | "abstract",
11 | "case",
12 | "catch",
13 | "class",
14 | "def",
15 | "do",
16 | "else",
17 | "extends",
18 | "false",
19 | "final",
20 | "finally",
21 | "for",
22 | "forSome",
23 | "if",
24 | "implicit",
25 | "import",
26 | "lazy",
27 | "match",
28 | "new",
29 | "null",
30 | "object",
31 | "override",
32 | "package",
33 | "private",
34 | "protected",
35 | "return",
36 | "sealed",
37 | "super",
38 | "this",
39 | "throw",
40 | "trait",
41 | "try",
42 | "true",
43 | "type",
44 | "val",
45 | "var",
46 | "while",
47 | "with",
48 | "yield"
49 | )
50 |
51 | val keywordsChars: List[Array[Char]] = keywordStrings.map(_.toCharArray())
52 |
53 | val keywordsRegex: Regex = keywordStrings.map(Regex.string(_)).reduce(_ | _)
54 |
55 | val fooRegex: Regex.Tabular.Tabular = Regex.Tabular(Regex.string("foo"))
56 |
57 | val fooOrBar: Regex.Tabular.Tabular = Regex.Tabular(Regex.string("foo") | Regex.string("bar"))
58 |
59 | val aOrB: Regex.Tabular.Tabular = Regex.Tabular(Regex.string("a") | Regex.string("b"))
60 |
61 | val keywords: Regex.Tabular.Tabular = Regex.Tabular(keywordsRegex)
62 |
63 | override def spec: Spec[TestEnvironment, Any] =
64 | suite("RegexSpec")(
65 | suite("string")(
66 | test("positive matches") {
67 | assertTrue(fooRegex.matches("foo"))
68 | },
69 | test("negative matches") {
70 | assertTrue(!(fooRegex.matches("bar")))
71 | },
72 | test("single-letter positive or") {
73 | assertTrue(aOrB.matches("a")) &&
74 | assertTrue(aOrB.matches("b"))
75 | },
76 | test("word positive or") {
77 | assertTrue(fooOrBar.matches("foo")) &&
78 | assertTrue(fooOrBar.matches("bar"))
79 | }
80 | ),
81 | suite("keywords") {
82 | test("matches all keywords") {
83 | assert(keywordStrings.map(keywords.matches(_)))(Assertion.forall(Assertion.isTrue))
84 | }
85 | },
86 | suite("digits")(
87 | test("matches all digits") {
88 | assert(Regex.digits.compile.matches("123"))(Assertion.isTrue)
89 | },
90 | test("matches the first few digits") {
91 | assert(Regex.digits.compile.matches("123ABC"))(Assertion.isTrue)
92 | }
93 | ),
94 | suite("single-char constructors")(
95 | singleChar("alphaNumeric", Regex.anyAlphaNumeric, _.isLetterOrDigit),
96 | singleChar("digit", Regex.anyDigit, _.isDigit),
97 | singleChar("letter", Regex.anyLetter, _.isLetter),
98 | singleChar("whitespace", Regex.whitespace, _.isWhitespace),
99 | singleChar(
100 | "filter",
101 | Regex.filter(ch => ch == 'a' || ch == 'b'),
102 | ch => ch == 'a' || ch == 'b',
103 | Gen.fromIterable(List('a', 'b', 'c', 'd'))
104 | ),
105 | singleChar(
106 | "oneOf",
107 | Regex.charIn('a', 'b'),
108 | ch => ch == 'a' || ch == 'b',
109 | Gen.fromIterable(List('a', 'b', 'c', 'd'))
110 | ),
111 | singleChar(
112 | "noneOf",
113 | Regex.charNotIn('a', 'b'),
114 | ch => ch != 'a' && ch != 'b',
115 | Gen.fromIterable(List('a', 'b', 'c', 'd'))
116 | ),
117 | singleChar("literal 1", Regex.string("X"), _ == 'X', Gen.asciiChar)
118 | ),
119 | suite("multi-char constructors passing")(
120 | multiCharPassing("alphaNumerics", Regex.alphaNumerics, Gen.alphaNumericChar),
121 | multiCharPassing("digits", Regex.digits, Gen.numericChar),
122 | multiCharPassing("letters", Regex.letters, Gen.alphaChar),
123 | multiCharPassing("whitespaces", Regex.whitespace, Gen.fromIterable(List(' ', '\n', '\t')))
124 | ),
125 | suite("multi-char constructors failing")(
126 | multiCharFailing("alphaNumerics", Regex.alphaNumerics, Gen.alphaNumericChar, '!'),
127 | multiCharFailing("digits", Regex.digits, Gen.numericChar, 'a'),
128 | multiCharFailing("letters", Regex.letters, Gen.alphaChar, '0')
129 | ),
130 | test("literal0") {
131 | val r = Regex.string("").compile
132 | check(Gen.string) { input =>
133 | assertTrue(r.test(0, input) == 0)
134 | }
135 | },
136 | test("literal+") {
137 | check(Gen.string1(Gen.unicodeChar)) { str =>
138 | val r = Regex.string(str).compile
139 |
140 | val bad = "not" + str
141 | assertTrue(r.test(0, str) == str.length) &&
142 | assertTrue(r.test(0, bad) < str.length)
143 | }
144 | },
145 | suite("combinators")(
146 | test("string ~ string") {
147 | check(Gen.string1(Gen.unicodeChar), Gen.string1(Gen.unicodeChar)) { (s1, s2) =>
148 | val r = (Regex.string(s1) ~ Regex.string(s2)).compile
149 | assertTrue(r.test(0, s1 ++ s2) == s1.length + s2.length)
150 | }
151 | },
152 | test("charIn & charIn") {
153 | val r = (Regex.charIn('a', 'b', 'c') & Regex.charIn('b')).compile
154 |
155 | assertTrue(r.test(0, "a") == Regex.NotMatched) &&
156 | assertTrue(r.test(0, "b") == 1)
157 | },
158 | test("string | string") {
159 | check(Gen.string1(Gen.unicodeChar), Gen.string1(Gen.unicodeChar)) { (s1, s2) =>
160 | val r = (Regex.string(s1) | Regex.string(s2)).compile
161 | (assertTrue(r.test(0, s1) == s1.length) &&
162 | assertTrue(r.test(0, s2) == s2.length))
163 | }
164 | },
165 | test("atLeast") {
166 | check(Gen.chunkOfBounded(1, 20)(Gen.numericChar), Gen.int(0, 20)) { (chunk, min) =>
167 | val r = Regex.anyDigit.atLeast(min).compile
168 | val expected = if (chunk.length >= min) chunk.length else Regex.NeedMoreInput
169 | assertTrue(r.test(0, new String(chunk.toArray)) == expected)
170 | }
171 | },
172 | test("atMost") {
173 | check(Gen.int(0, 20), Gen.int(0, 20)) { (len, max) =>
174 | val s = "abc" * len
175 | val r = Regex.string("abc").atMost(max).compile
176 |
177 | val expected = math.min(len, max) * 3
178 | assertTrue(r.test(0, s) == expected)
179 | }
180 | },
181 | test("between") {
182 | check(Gen.int(0, 20), Gen.int(0, 20), Gen.int(0, 20)) { (len, a, b) =>
183 | val max = Math.max(a, b)
184 | val min = Math.min(a, b)
185 | val s = "x" * len
186 | val r = Regex.string("x").between(min, max).compile
187 |
188 | val expected = if (len >= min) Math.min(len, max) else Regex.NeedMoreInput
189 | assertTrue(r.test(0, s) == expected)
190 | }
191 | }
192 | ),
193 | suite("end of stream")(
194 | test("oneOf(a, b)") {
195 | assertTrue(Regex.charIn('a', 'b').compile.test(0, "") == Regex.NeedMoreInput)
196 | },
197 | test("oneOf(a)") {
198 | assertTrue(Regex.char('a').compile.test(0, "") == Regex.NeedMoreInput)
199 | },
200 | test("anyChar.atLeast(0)") {
201 | assertTrue(Regex.anyChar.atLeast(0).compile.test(0, "") == 0)
202 | },
203 | test("char(x).atLeast(0)") {
204 | assertTrue(Regex.char('x').atLeast(0).compile.test(0, "") == 0)
205 | }
206 | ),
207 | test("test") {
208 | assert("""^\x00\x00\xff""".r.pattern.matcher("\u0000\u0000\u00ff").find(0))(isTrue)
209 | }
210 | )
211 |
212 | private def singleChar(
213 | name: String,
214 | r: Regex,
215 | f: Char => Boolean,
216 | gen: Gen[Any, Char] = Gen.char
217 | ): Spec[TestConfig, Any] =
218 | test(name) {
219 | val compiled = r.compile
220 | check(gen) { ch =>
221 | assertTrue(compiled.test(0, ch.toString) == 1 == f(ch))
222 | }
223 | }
224 |
225 | private def multiCharPassing(
226 | name: String,
227 | r: Regex,
228 | gen: Gen[Any, Char]
229 | ): Spec[Sized with TestConfig, Any] =
230 | test(name) {
231 | val compiled = r.compile
232 | check(Gen.chunkOf1(gen)) { input =>
233 | assertTrue(compiled.test(0, new String(input.toArray)) == input.length)
234 | }
235 | }
236 |
237 | private def multiCharFailing(
238 | name: String,
239 | r: Regex,
240 | gen: Gen[Any, Char],
241 | counterexample: Char
242 | ): Spec[Sized with TestConfig, Any] =
243 | test(name) {
244 | val compiled = r.compile
245 | check(Gen.chunkOf1(gen)) { input =>
246 | Random.nextIntBetween(0, input.length).map { injectionPoint =>
247 | val injected = (input.take(injectionPoint) :+ counterexample) ++ input.drop(injectionPoint)
248 | val expected =
249 | if (injectionPoint == 0)
250 | Regex.NeedMoreInput
251 | else
252 | injectionPoint
253 | val result = compiled.test(0, new String(injected.toArray))
254 | assertTrue(result == expected)
255 | }
256 | }
257 | }
258 | }
259 |
--------------------------------------------------------------------------------
/benchmarks/src/main/scala/zio/parser/benchmarks/lucene/CatsLuceneQueryParser.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.benchmarks.lucene
2 |
3 | import cats.parse.{Numbers, Parser0, Parser => P}
4 | import zio.parser.benchmarks.lucene.Query.TopLevelCombinatorQuery
5 |
6 | class CatsLuceneQueryParser(
7 | topLevelCombinator: List[Query] => TopLevelCombinatorQuery = Query.Or,
8 | esCompatible: Boolean = false
9 | ) {
10 | import CatsLuceneQueryParser._
11 |
12 | val numChar: P[Char] = Numbers.digit
13 |
14 | val escapedChar: P[String] = (P.char('\\') *> P.anyChar).map(ch => "\\" + ch)
15 | val termStartChar: P[String] =
16 | P.charWhere(ch => !invalidStartChars.contains(ch)).map(_.toString) | escapedChar
17 | val termChar: P[String] = termStartChar | P.charIn('-', '+').map(_.toString)
18 | val whitespace: P[Unit] = P.charIn(whitespaceChars).void
19 | val whitespaces: Parser0[Unit] = whitespace.rep0.void
20 | val whitespaces1: Parser0[Unit] = whitespace.rep.void
21 | val quotedChar: P[String] =
22 | P.charWhere(ch => ch != '\"' && ch != '\\').map(_.toString) | escapedChar
23 |
24 | val and: P[Unit] =
25 | (P.string("AND").between(whitespaces, whitespaces1 | P.peek(P.char('('))) | P
26 | .string("&&")
27 | .surroundedBy(whitespaces)).backtrack.void
28 | val or: P[Unit] =
29 | (P.string("OR").between(whitespaces, whitespaces1 | P.peek(P.char('('))) | P
30 | .string("||")
31 | .surroundedBy(whitespaces)).backtrack.void
32 | val not: P[Unit] =
33 | (P.string("NOT").between(whitespaces, whitespaces1 | P.peek(P.char('('))) | P
34 | .char('!')
35 | .surroundedBy(whitespaces)).backtrack.void
36 | val plus: P[Unit] = P.char('+').surroundedBy(whitespaces)
37 | val minus: P[Unit] = P.char('-').surroundedBy(whitespaces)
38 | val lparen: P[Unit] = P.char('(').surroundedBy(whitespaces)
39 | val rparen: P[Unit] = P.char(')').surroundedBy(whitespaces)
40 | val opColon: P[Unit] = P.char(':').surroundedBy(whitespaces)
41 | val opEqual: P[Unit] = P.char('=').surroundedBy(whitespaces)
42 | val opLessThan: P[RangeOp] = P.char('<').surroundedBy(whitespaces).as(RangeOp.LessThan)
43 | val opLessThanEq: P[RangeOp] =
44 | P.string("<=").surroundedBy(whitespaces).as(RangeOp.LessThanEq)
45 | val opMoreThan: P[RangeOp] = P.char('>').surroundedBy(whitespaces).as(RangeOp.MoreThan)
46 | val opMoreThanEq: P[RangeOp] =
47 | P.string(">=").surroundedBy(whitespaces).as(RangeOp.MoreThanEq)
48 | val carat: P[Unit] = P.char('^').surroundedBy(whitespaces)
49 | val tilde: P[Unit] = P.char('~').surroundedBy(whitespaces)
50 |
51 | val quoted: P[String] = P.char('\"') *> quotedChar.rep0.string <* P.char('\"')
52 |
53 | val integer: P[Int] = numChar.rep.string.map(_.toInt)
54 | val number: P[Double] = (numChar.rep ~ (P.char('.') *> numChar.rep).?).string.map(_.toDouble)
55 |
56 | val term: P[String] = (termStartChar ~ termChar.rep0)
57 | .map { case (head, tail) =>
58 | (head :: tail).mkString
59 | }
60 | .filter(s => !keywords.contains(s))
61 | .map(unescape)
62 |
63 | val regexpTerm: P[String] =
64 | P.char('/') *> (P.string("\\/").as('/') | P.charWhere(_ != '/')).rep0.string <* P.char('/')
65 |
66 | val rangeInStart: P[BoundType] =
67 | P.char('[').surroundedBy(whitespaces).as(BoundType.Inclusive)
68 | val rangeExStart: P[BoundType] =
69 | P.char('{').surroundedBy(whitespaces).as(BoundType.Exclusive)
70 | val rangeInEnd: P[BoundType] = P.char(']').surroundedBy(whitespaces).as(BoundType.Inclusive)
71 | val rangeExEnd: P[BoundType] = P.char('}').surroundedBy(whitespaces).as(BoundType.Exclusive)
72 |
73 | val rangeStart: P[BoundType] = rangeInStart | rangeExStart
74 | val rangeEnd: P[BoundType] = rangeInEnd | rangeExEnd
75 | val rangeTo: P[Unit] = P.string("TO").surroundedBy(whitespaces)
76 |
77 | val bound: P[String] =
78 | P.char('\"') *> quotedChar.rep0.string <* P.char('\"') |
79 | P.charWhere(ch => ch != ' ' && ch != ']' && ch != '}').rep.string
80 |
81 | val range: P[Query.Range] =
82 | for {
83 | lower <- (rangeStart ~ bound).map { case (t, s) => toBoundary(s, t) }
84 | _ <- rangeTo
85 | upper <- (bound ~ rangeEnd).map { case (s, t) => toBoundary(s, t) }
86 | } yield Query.Range(lower, upper)
87 |
88 | val modifier: P[Query => Query] =
89 | plus.as(Query.Require).backtrack | minus.as(Query.Prohibit).backtrack | not.as(Query.Not)
90 |
91 | val fieldName: P[String] = term.map(unescapeWildcards)
92 |
93 | val luceneFieldRange: P[Query.Range] =
94 | ((opLessThanEq.backtrack | opMoreThanEq.backtrack | opLessThan | opMoreThan) ~
95 | (quoted.backtrack | number.backtrack.string | term.backtrack)).map { case (op, term) =>
96 | op match {
97 | case RangeOp.LessThan =>
98 | Query.Range(Boundary.Unbound, Boundary.Bound(term, BoundType.Exclusive))
99 | case RangeOp.LessThanEq =>
100 | Query.Range(Boundary.Unbound, Boundary.Bound(term, BoundType.Inclusive))
101 | case RangeOp.MoreThan =>
102 | Query.Range(Boundary.Bound(term, BoundType.Exclusive), Boundary.Unbound)
103 | case RangeOp.MoreThanEq =>
104 | Query.Range(Boundary.Bound(term, BoundType.Inclusive), Boundary.Unbound)
105 | }
106 | }
107 |
108 | private def mergeRanges(r1: Query.Range, r2: Query.Range): Parser0[Query.Range] = {
109 | val lowers = (r1.lower, r2.lower)
110 | val uppers = (r2.lower, r2.upper)
111 |
112 | for {
113 | lower <- lowers match {
114 | case (Boundary.Unbound, b: Boundary.Bound) => P.pure(b)
115 | case (b: Boundary.Bound, Boundary.Unbound) => P.pure(b)
116 | case (Boundary.Unbound, Boundary.Unbound) => P.pure(Boundary.Unbound)
117 | case (Boundary.Bound(_, _), Boundary.Bound(_, _)) =>
118 | P.failWith("Two lower bounds provided with AND")
119 | }
120 | upper <- uppers match {
121 | case (Boundary.Unbound, b: Boundary.Bound) => P.pure(b)
122 | case (b: Boundary.Bound, Boundary.Unbound) => P.pure(b)
123 | case (Boundary.Unbound, Boundary.Unbound) => P.pure(Boundary.Unbound)
124 | case (Boundary.Bound(_, _), Boundary.Bound(_, _)) =>
125 | P.failWith("Two upper bounds provided with AND")
126 | }
127 | } yield Query.Range(lower, upper)
128 | }
129 |
130 | val esFieldRange: P[Query.Range] =
131 | luceneFieldRange.backtrack |
132 | ((luceneFieldRange <* and) ~ luceneFieldRange).between(lparen, rparen).backtrack.flatMap { case (r1, r2) =>
133 | mergeRanges(r1, r2)
134 | } |
135 | ((plus *> luceneFieldRange <* whitespaces) ~ (plus *> luceneFieldRange))
136 | .between(lparen, rparen)
137 | .backtrack
138 | .flatMap { case (r1, r2) =>
139 | mergeRanges(r1, r2)
140 | }
141 |
142 | val fieldRange: P[Query] =
143 | for {
144 | fieldName <- if (esCompatible) (fieldName <* opColon) else fieldName
145 | range <- if (esCompatible) esFieldRange else luceneFieldRange
146 | } yield Query.Field(fieldName, range)
147 |
148 | val regexpTermQuery: P[Query] =
149 | regexpTerm.map(Query.Regex)
150 |
151 | val fuzzyOp: P[Int] = tilde *> integer.?.map(_.getOrElse(2))
152 | val proximityOp: P[Int] = tilde *> integer
153 |
154 | val fieldTermQuery: P[Query] =
155 | ((term | number.map(_.toString)) ~ fuzzyOp.backtrack.?).map { case (term, maxEditDistance) =>
156 | Query.Term(term, maxEditDistance)
157 | }
158 |
159 | val quotedTermQuery: P[Query] =
160 | (quoted ~ proximityOp.backtrack.?).map { case (term, maxDistance) =>
161 | Query.Phrase(term, maxDistance)
162 | }
163 |
164 | val termQueryInner: P[Query] =
165 | regexpTermQuery | range | quotedTermQuery | fieldTermQuery
166 |
167 | val termQuery: P[Query] =
168 | (termQueryInner ~ (carat *> number).backtrack.?).map {
169 | case (query, Some(score)) => Query.Boost(query, score)
170 | case (query, None) => query
171 | }
172 |
173 | val fieldNamePrefix: P[String] = fieldName <* (opColon | opEqual).surroundedBy(whitespaces)
174 |
175 | val query: P[Query] =
176 | P.recursive[Query] { recurse =>
177 | val grouping: P[Query] =
178 | (recurse.between(lparen, rparen) ~ (carat *> number).backtrack.?).map {
179 | case (query, Some(score)) => Query.Boost(query, score)
180 | case (query, None) => query
181 | }
182 |
183 | val clause: P[Query] =
184 | fieldRange.backtrack |
185 | (fieldNamePrefix.backtrack.?.with1 ~ (grouping | termQuery))
186 | .map {
187 | case (Some(fieldName), query) => Query.Field(fieldName, query).normalize
188 | case (None, query) => query
189 | }
190 | .surroundedBy(whitespaces)
191 |
192 | val modClause: P[Query] =
193 | (modifier.?.with1 ~ clause).map {
194 | case (Some(modFn), q) => modFn(q)
195 | case (None, q) => q
196 | }
197 |
198 | val conjunctQuery: P[Query] =
199 | modClause.repSep(and).map { lst =>
200 | Query.And(lst.toList).normalize
201 | }
202 |
203 | val disjunctQuery: P[Query] =
204 | conjunctQuery.repSep(or).map { lst =>
205 | Query.Or(lst.toList).normalize
206 | }
207 |
208 | disjunctQuery.repSep(whitespaces).map { lst =>
209 | topLevelCombinator(lst.toList).normalize
210 | }
211 | }
212 | }
213 |
214 | object CatsLuceneQueryParser {
215 | val invalidStartChars: Set[Char] = Set(' ', '\t', '\n', '\r', '\u3000', '+', '-', '!', '(', ')', ':', '^', '<', '>',
216 | '=', '[', ']', '\"', '{', '}', '~', '\\', '/')
217 |
218 | val whitespaceChars: Set[Char] = Set(' ', '\t', '\n', '\r', '\u3000')
219 |
220 | val keywords: Set[String] = Set("AND", "OR", "NOT")
221 |
222 | sealed trait RangeOp
223 | object RangeOp {
224 | final object LessThan extends RangeOp
225 | final object LessThanEq extends RangeOp
226 | final object MoreThan extends RangeOp
227 | final object MoreThanEq extends RangeOp
228 | }
229 |
230 | private def unescape(s: String): String =
231 | s.replaceAll("""\\([^?*])""", "$1")
232 |
233 | private def unescapeWildcards(s: String): String =
234 | s.replaceAll("""\\([?*])""", "$1")
235 |
236 | private def toBoundary(term: String, boundType: BoundType): Boundary =
237 | if (term == "*") Boundary.Unbound else Boundary.Bound(term, boundType)
238 | }
239 |
--------------------------------------------------------------------------------
/zio-parser/shared/src/main/scala/zio/parser/internal/PrinterImpl.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.internal
2 |
3 | import zio.Chunk
4 | import zio.parser.Parser.ParserError
5 | import zio.parser.Printer
6 | import zio.parser.target.Target
7 |
8 | import scala.annotation.nowarn
9 |
10 | /** Interpreter for Printer
11 | */
12 | class PrinterImpl[Err, Out, Value](printer: Printer[Err, Out, Value]) {
13 | type ErasedPrinter = Printer[Any, Any, Any]
14 | case class Cont(f: Either[Any, Any] => (ErasedPrinter, Any, Option[Cont]))
15 |
16 | def run(value: Value, output: Target[Out]): Either[Err, Unit] = {
17 | var input: Any = value
18 | var current: Any = printer
19 | var result: Either[Any, Any] = Right(())
20 | val stack: Stack[Cont] = Stack()
21 |
22 | def finish(r: Either[Any, Unit]): Unit =
23 | if (stack.isEmpty) {
24 | result = r
25 | current = null
26 | } else {
27 | val k = stack.pop()
28 | val (next, nextI, nextK) = k.f(r)
29 | current = next
30 | input = nextI
31 | nextK.foreach(stack.push)
32 | }
33 |
34 | while (current != null) {
35 | (current: @nowarn) match {
36 | case l @ Printer.Lazy(_) =>
37 | current = l.memoized
38 |
39 | case Printer.Succeed(_) =>
40 | finish(Right(()))
41 |
42 | case Printer.Fail(failure) =>
43 | finish(Left(failure))
44 |
45 | case Printer.FlatMapValue(f) =>
46 | current = f.asInstanceOf[Any => ErasedPrinter](input)
47 |
48 | case Printer.ProvideValue(syntax, value) =>
49 | val oldInput = input
50 | input = value
51 | current = syntax
52 | stack.push(Cont {
53 | case Left(failure) => (Printer.Fail(failure), oldInput, None)
54 | case Right(_) => (Printer.Succeed(()), oldInput, None)
55 | })
56 |
57 | case Printer.Passthrough() =>
58 | output.write(input.asInstanceOf[Out])
59 | finish(Right(()))
60 |
61 | case parseRegex @ Printer.ParseRegex(_, Some(failure)) =>
62 | val chunk = input.asInstanceOf[Chunk[Char]]
63 | if (parseRegex.compiledRegex.test(0, new String(chunk.toArray)) >= 0) {
64 | for (out <- input.asInstanceOf[Chunk[Out]])
65 | output.write(out)
66 | finish(Right(()))
67 | } else {
68 | finish(Left(failure))
69 | }
70 | case Printer.ParseRegex(_, None) =>
71 | for (out <- input.asInstanceOf[Chunk[Out]])
72 | output.write(out)
73 | finish(Right(()))
74 | case Printer.SkipRegex(_, as) =>
75 | for (out <- as.asInstanceOf[Chunk[Out]])
76 | output.write(out)
77 | finish(Right(()))
78 |
79 | case parseRegex @ Printer.ParseRegexLastChar(_, Some(failure)) =>
80 | val char = input.asInstanceOf[Char]
81 | if (parseRegex.compiledRegex.test(0, char.toString) > 0) {
82 | output.write(input.asInstanceOf[Out])
83 | finish(Right(()))
84 | } else {
85 | finish(Left(failure))
86 | }
87 | case Printer.ParseRegexLastChar(_, None) =>
88 | output.write(input.asInstanceOf[Out])
89 | finish(Right(()))
90 |
91 | case Printer.MapError(syntax, f) =>
92 | current = syntax
93 | stack.push(Cont {
94 | case Left(failure) => (Printer.Fail(f.asInstanceOf[Any => Any](failure)), input, None)
95 | case Right(_) => (Printer.Succeed(()), input, None)
96 | })
97 |
98 | case Printer.Ignore(syntax, to, from) =>
99 | if (input == to) {
100 | val oldInput = input
101 | input = from
102 | current = syntax
103 | stack.push(Cont {
104 | case Left(failure) => (Printer.Fail(failure), oldInput, None)
105 | case Right(_) => (Printer.Succeed(()), oldInput, None)
106 | })
107 | } else {
108 | finish(Left(Printer.Failed(ParserError.UnknownFailure(Nil, 0)))) // TODO
109 | }
110 |
111 | case Printer.Contramap(syntax, from) =>
112 | val oldInput = input
113 | input = from.asInstanceOf[Any => Any](input)
114 | current = syntax
115 | stack.push(Cont {
116 | case Left(failure) => (Printer.Fail(failure), oldInput, None)
117 | case Right(_) => (Printer.Succeed(()), oldInput, None)
118 | })
119 |
120 | case Printer.ContramapEither(syntax, from) =>
121 | val oldInput = input
122 | from.asInstanceOf[Any => Either[Any, Any]](input) match {
123 | case Left(failure) =>
124 | finish(Left(failure))
125 | case Right(newInput) =>
126 | input = newInput
127 | current = syntax
128 | stack.push(Cont {
129 | case Left(failure) => (Printer.Fail(failure), oldInput, None)
130 | case Right(_) =>
131 | (Printer.Succeed(()), oldInput, None)
132 | })
133 | }
134 |
135 | case Printer.Zip(left, right, unzipValue) =>
136 | val oldInput = input
137 | val (valueA, valueB) = unzipValue.asInstanceOf[Any => (Any, Any)](input)
138 | current = left
139 | input = valueA
140 |
141 | val k1 = Cont {
142 | case Left(failure) =>
143 | (Printer.Fail(failure), oldInput, None)
144 | case Right(_) =>
145 | val k2 = Cont((rightResult: Either[Any, Any]) =>
146 | rightResult match {
147 | case Left(failure) => (Printer.Fail(failure), oldInput, None)
148 | case Right(_) =>
149 | (Printer.Succeed(()), oldInput, None)
150 | }
151 | )
152 | (right.asInstanceOf[ErasedPrinter], valueB, Some(k2))
153 | }
154 | stack.push(k1)
155 | case Printer.ZipLeft(left, right) =>
156 | val oldInput = input
157 | val valueA = input
158 | val valueB = ()
159 | current = left
160 | input = valueA
161 |
162 | val k1 = Cont {
163 | case Left(failure) =>
164 | (Printer.Fail(failure), oldInput, None)
165 | case Right(_) =>
166 | val k2 = Cont((rightResult: Either[Any, Any]) =>
167 | rightResult match {
168 | case Left(failure) => (Printer.Fail(failure), oldInput, None)
169 | case Right(_) => (Printer.Succeed(()), oldInput, None)
170 | }
171 | )
172 | (right.asInstanceOf[ErasedPrinter], valueB, Some(k2))
173 | }
174 | stack.push(k1)
175 |
176 | case Printer.ZipRight(left, right) =>
177 | val oldInput = input
178 | val valueA = ()
179 | val valueB = input
180 | current = left
181 | input = valueA
182 |
183 | val k1 = Cont {
184 | case Left(failure) =>
185 | (Printer.Fail(failure), oldInput, None)
186 | case Right(_) =>
187 | val k2 = Cont((rightResult: Either[Any, Any]) =>
188 | rightResult match {
189 | case Left(failure) => (Printer.Fail(failure), oldInput, None)
190 | case Right(_) => (Printer.Succeed(()), oldInput, None)
191 | }
192 | )
193 | (right.asInstanceOf[ErasedPrinter], valueB, Some(k2))
194 | }
195 | stack.push(k1)
196 |
197 | case Printer.OrElseEither(left, right) =>
198 | val oldInput = input
199 | val eitherInput = input.asInstanceOf[Either[Any, Any]]
200 | eitherInput match {
201 | case Left(leftInput) =>
202 | input = leftInput
203 | current = left
204 | stack.push(Cont {
205 | case Left(failure) => (Printer.Fail(failure), oldInput, None)
206 | case Right(_) => (Printer.Succeed(()), oldInput, None)
207 | })
208 |
209 | case Right(rightInput) =>
210 | input = rightInput
211 | current = right
212 | stack.push(Cont {
213 | case Left(failure) => (Printer.Fail(failure), oldInput, None)
214 | case Right(_) => (Printer.Succeed(()), oldInput, None)
215 | })
216 |
217 | }
218 |
219 | case Printer.OrElse(left, right) =>
220 | current = left
221 | val capture = output.capture()
222 |
223 | stack.push(Cont {
224 | case Left(_) =>
225 | output.drop(capture)
226 | (right.asInstanceOf[ErasedPrinter], input, None)
227 | case Right(_) =>
228 | output.emit(capture)
229 | (Printer.Succeed(()), input, None)
230 | })
231 |
232 | case Printer.Optional(inner) =>
233 | val oldInput = input
234 | val optInput = input.asInstanceOf[Option[Any]]
235 |
236 | optInput match {
237 | case Some(someInput) =>
238 | input = someInput
239 | current = inner
240 | stack.push(Cont {
241 | case Left(failure) => (Printer.Fail(failure), oldInput, None)
242 | case Right(_) => (Printer.Succeed(()), oldInput, None)
243 | })
244 |
245 | case None =>
246 | finish(Right(()))
247 | }
248 |
249 | case rep @ Printer.Repeat(inner, _, _) =>
250 | val inputChunk = input.asInstanceOf[Chunk[Any]]
251 | if (inputChunk.isEmpty) {
252 | finish(Right(()))
253 | } else {
254 | val head = inputChunk.head
255 | val tail = inputChunk.tail
256 |
257 | current = inner
258 | input = head
259 | stack.push(Cont {
260 | case Left(failure) =>
261 | (Printer.Fail(failure), inputChunk, None)
262 | case Right(_) =>
263 | (
264 | rep.asInstanceOf[ErasedPrinter],
265 | tail,
266 | Some(Cont {
267 | case Left(failure) =>
268 | (Printer.Fail(failure), inputChunk, None)
269 | case Right(_) =>
270 | (Printer.Succeed(()), inputChunk, None)
271 | })
272 | )
273 | })
274 | }
275 | }
276 | }
277 |
278 | result.left
279 | .map(_.asInstanceOf[Err])
280 | .map(_ => ())
281 | }
282 | }
283 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/zio-parser/shared/src/main/scala/zio/parser/internal/PUnzippable.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.internal
2 |
3 | // Based on zio.Unzippable, extended with UnzippableLeftIdentityAny, renamed to PUnzippable to avoid name collision
4 |
5 | trait PUnzippable[A, B] {
6 | type In
7 | def unzip(in: In): (A, B)
8 | }
9 |
10 | object PUnzippable extends PUnzippableLowPriority0 {
11 |
12 | type In[A, B, C] = PUnzippable[A, B] { type In = C }
13 |
14 | implicit def UnzippableLeftIdentityAny[A]: PUnzippable.In[Any, A, A] =
15 | new PUnzippable[Any, A] {
16 | type In = A
17 | def unzip(in: A): (Any, A) =
18 | ((), in)
19 | }
20 | }
21 |
22 | trait PUnzippableLowPriority0 extends PUnzippableLowPriority1 {
23 |
24 | implicit def UnzippableLeftIdentity[A]: PUnzippable.In[Unit, A, A] =
25 | new PUnzippable[Unit, A] {
26 | type In = A
27 | def unzip(in: A): (Unit, A) =
28 | ((), in)
29 | }
30 | }
31 |
32 | trait PUnzippableLowPriority1 extends PUnzippableLowPriority2 {
33 |
34 | implicit def UnzippableRightIdentity[A]: PUnzippable.In[A, Unit, A] =
35 | new PUnzippable[A, Unit] {
36 | type In = A
37 | def unzip(in: A): (A, Unit) =
38 | (in, ())
39 | }
40 | }
41 |
42 | trait PUnzippableLowPriority2 extends PUnzippableLowPriority3 {
43 |
44 | implicit def Unzippable3[A, B, Z]: PUnzippable.In[(A, B), Z, (A, B, Z)] =
45 | new PUnzippable[(A, B), Z] {
46 | type In = (A, B, Z)
47 | def unzip(in: (A, B, Z)): ((A, B), Z) =
48 | ((in._1, in._2), in._3)
49 | }
50 |
51 | implicit def Unzippable4[A, B, C, Z]: PUnzippable.In[(A, B, C), Z, (A, B, C, Z)] =
52 | new PUnzippable[(A, B, C), Z] {
53 | type In = (A, B, C, Z)
54 | def unzip(in: (A, B, C, Z)): ((A, B, C), Z) =
55 | ((in._1, in._2, in._3), in._4)
56 | }
57 |
58 | implicit def Unzippable5[A, B, C, D, Z]: PUnzippable.In[(A, B, C, D), Z, (A, B, C, D, Z)] =
59 | new PUnzippable[(A, B, C, D), Z] {
60 | type In = (A, B, C, D, Z)
61 | def unzip(in: (A, B, C, D, Z)): ((A, B, C, D), Z) =
62 | ((in._1, in._2, in._3, in._4), in._5)
63 | }
64 |
65 | implicit def Unzippable6[A, B, C, D, E, Z]: PUnzippable.In[(A, B, C, D, E), Z, (A, B, C, D, E, Z)] =
66 | new PUnzippable[(A, B, C, D, E), Z] {
67 | type In = (A, B, C, D, E, Z)
68 | def unzip(in: (A, B, C, D, E, Z)): ((A, B, C, D, E), Z) =
69 | ((in._1, in._2, in._3, in._4, in._5), in._6)
70 | }
71 |
72 | implicit def Unzippable7[A, B, C, D, E, F, Z]: PUnzippable.In[(A, B, C, D, E, F), Z, (A, B, C, D, E, F, Z)] =
73 | new PUnzippable[(A, B, C, D, E, F), Z] {
74 | type In = (A, B, C, D, E, F, Z)
75 | def unzip(in: (A, B, C, D, E, F, Z)): ((A, B, C, D, E, F), Z) =
76 | ((in._1, in._2, in._3, in._4, in._5, in._6), in._7)
77 | }
78 |
79 | implicit def Unzippable8[A, B, C, D, E, F, G, Z]: PUnzippable.In[(A, B, C, D, E, F, G), Z, (A, B, C, D, E, F, G, Z)] =
80 | new PUnzippable[(A, B, C, D, E, F, G), Z] {
81 | type In = (A, B, C, D, E, F, G, Z)
82 | def unzip(in: (A, B, C, D, E, F, G, Z)): ((A, B, C, D, E, F, G), Z) =
83 | ((in._1, in._2, in._3, in._4, in._5, in._6, in._7), in._8)
84 | }
85 |
86 | implicit def Unzippable9[A, B, C, D, E, F, G, H, Z]
87 | : PUnzippable.In[(A, B, C, D, E, F, G, H), Z, (A, B, C, D, E, F, G, H, Z)] =
88 | new PUnzippable[(A, B, C, D, E, F, G, H), Z] {
89 | type In = (A, B, C, D, E, F, G, H, Z)
90 | def unzip(in: (A, B, C, D, E, F, G, H, Z)): ((A, B, C, D, E, F, G, H), Z) =
91 | ((in._1, in._2, in._3, in._4, in._5, in._6, in._7, in._8), in._9)
92 | }
93 |
94 | implicit def Unzippable10[A, B, C, D, E, F, G, H, I, Z]
95 | : PUnzippable.In[(A, B, C, D, E, F, G, H, I), Z, (A, B, C, D, E, F, G, H, I, Z)] =
96 | new PUnzippable[(A, B, C, D, E, F, G, H, I), Z] {
97 | type In = (A, B, C, D, E, F, G, H, I, Z)
98 | def unzip(in: (A, B, C, D, E, F, G, H, I, Z)): ((A, B, C, D, E, F, G, H, I), Z) =
99 | ((in._1, in._2, in._3, in._4, in._5, in._6, in._7, in._8, in._9), in._10)
100 | }
101 |
102 | implicit def Unzippable11[A, B, C, D, E, F, G, H, I, J, Z]
103 | : PUnzippable.In[(A, B, C, D, E, F, G, H, I, J), Z, (A, B, C, D, E, F, G, H, I, J, Z)] =
104 | new PUnzippable[(A, B, C, D, E, F, G, H, I, J), Z] {
105 | type In = (A, B, C, D, E, F, G, H, I, J, Z)
106 | def unzip(in: (A, B, C, D, E, F, G, H, I, J, Z)): ((A, B, C, D, E, F, G, H, I, J), Z) =
107 | ((in._1, in._2, in._3, in._4, in._5, in._6, in._7, in._8, in._9, in._10), in._11)
108 | }
109 |
110 | implicit def Unzippable12[A, B, C, D, E, F, G, H, I, J, K, Z]
111 | : PUnzippable.In[(A, B, C, D, E, F, G, H, I, J, K), Z, (A, B, C, D, E, F, G, H, I, J, K, Z)] =
112 | new PUnzippable[(A, B, C, D, E, F, G, H, I, J, K), Z] {
113 | type In = (A, B, C, D, E, F, G, H, I, J, K, Z)
114 | def unzip(in: (A, B, C, D, E, F, G, H, I, J, K, Z)): ((A, B, C, D, E, F, G, H, I, J, K), Z) =
115 | ((in._1, in._2, in._3, in._4, in._5, in._6, in._7, in._8, in._9, in._10, in._11), in._12)
116 | }
117 |
118 | implicit def Unzippable13[A, B, C, D, E, F, G, H, I, J, K, L, Z]
119 | : PUnzippable.In[(A, B, C, D, E, F, G, H, I, J, K, L), Z, (A, B, C, D, E, F, G, H, I, J, K, L, Z)] =
120 | new PUnzippable[(A, B, C, D, E, F, G, H, I, J, K, L), Z] {
121 | type In = (A, B, C, D, E, F, G, H, I, J, K, L, Z)
122 | def unzip(in: (A, B, C, D, E, F, G, H, I, J, K, L, Z)): ((A, B, C, D, E, F, G, H, I, J, K, L), Z) =
123 | ((in._1, in._2, in._3, in._4, in._5, in._6, in._7, in._8, in._9, in._10, in._11, in._12), in._13)
124 | }
125 |
126 | implicit def Unzippable14[A, B, C, D, E, F, G, H, I, J, K, L, M, Z]
127 | : PUnzippable.In[(A, B, C, D, E, F, G, H, I, J, K, L, M), Z, (A, B, C, D, E, F, G, H, I, J, K, L, M, Z)] =
128 | new PUnzippable[(A, B, C, D, E, F, G, H, I, J, K, L, M), Z] {
129 | type In = (A, B, C, D, E, F, G, H, I, J, K, L, M, Z)
130 | def unzip(in: (A, B, C, D, E, F, G, H, I, J, K, L, M, Z)): ((A, B, C, D, E, F, G, H, I, J, K, L, M), Z) =
131 | ((in._1, in._2, in._3, in._4, in._5, in._6, in._7, in._8, in._9, in._10, in._11, in._12, in._13), in._14)
132 | }
133 |
134 | implicit def Unzippable15[A, B, C, D, E, F, G, H, I, J, K, L, M, N, Z]
135 | : PUnzippable.In[(A, B, C, D, E, F, G, H, I, J, K, L, M, N), Z, (A, B, C, D, E, F, G, H, I, J, K, L, M, N, Z)] =
136 | new PUnzippable[(A, B, C, D, E, F, G, H, I, J, K, L, M, N), Z] {
137 | type In = (A, B, C, D, E, F, G, H, I, J, K, L, M, N, Z)
138 | def unzip(in: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, Z)): ((A, B, C, D, E, F, G, H, I, J, K, L, M, N), Z) =
139 | (
140 | (in._1, in._2, in._3, in._4, in._5, in._6, in._7, in._8, in._9, in._10, in._11, in._12, in._13, in._14),
141 | in._15
142 | )
143 | }
144 |
145 | implicit def Unzippable16[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, Z]: PUnzippable.In[
146 | (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O),
147 | Z,
148 | (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, Z)
149 | ] =
150 | new PUnzippable[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O), Z] {
151 | type In = (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, Z)
152 | def unzip(
153 | in: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, Z)
154 | ): ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O), Z) =
155 | (
156 | (
157 | in._1,
158 | in._2,
159 | in._3,
160 | in._4,
161 | in._5,
162 | in._6,
163 | in._7,
164 | in._8,
165 | in._9,
166 | in._10,
167 | in._11,
168 | in._12,
169 | in._13,
170 | in._14,
171 | in._15
172 | ),
173 | in._16
174 | )
175 | }
176 |
177 | implicit def Unzippable17[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Z]: PUnzippable.In[
178 | (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P),
179 | Z,
180 | (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Z)
181 | ] =
182 | new PUnzippable[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P), Z] {
183 | type In = (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Z)
184 | def unzip(
185 | in: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Z)
186 | ): ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P), Z) =
187 | (
188 | (
189 | in._1,
190 | in._2,
191 | in._3,
192 | in._4,
193 | in._5,
194 | in._6,
195 | in._7,
196 | in._8,
197 | in._9,
198 | in._10,
199 | in._11,
200 | in._12,
201 | in._13,
202 | in._14,
203 | in._15,
204 | in._16
205 | ),
206 | in._17
207 | )
208 | }
209 |
210 | implicit def Unzippable18[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, Z]: PUnzippable.In[
211 | (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q),
212 | Z,
213 | (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, Z)
214 | ] =
215 | new PUnzippable[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q), Z] {
216 | type In = (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, Z)
217 | def unzip(
218 | in: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, Z)
219 | ): ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q), Z) =
220 | (
221 | (
222 | in._1,
223 | in._2,
224 | in._3,
225 | in._4,
226 | in._5,
227 | in._6,
228 | in._7,
229 | in._8,
230 | in._9,
231 | in._10,
232 | in._11,
233 | in._12,
234 | in._13,
235 | in._14,
236 | in._15,
237 | in._16,
238 | in._17
239 | ),
240 | in._18
241 | )
242 | }
243 |
244 | implicit def Unzippable19[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, Z]: PUnzippable.In[
245 | (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R),
246 | Z,
247 | (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, Z)
248 | ] =
249 | new PUnzippable[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R), Z] {
250 | type In = (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, Z)
251 | def unzip(
252 | in: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, Z)
253 | ): ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R), Z) =
254 | (
255 | (
256 | in._1,
257 | in._2,
258 | in._3,
259 | in._4,
260 | in._5,
261 | in._6,
262 | in._7,
263 | in._8,
264 | in._9,
265 | in._10,
266 | in._11,
267 | in._12,
268 | in._13,
269 | in._14,
270 | in._15,
271 | in._16,
272 | in._17,
273 | in._18
274 | ),
275 | in._19
276 | )
277 | }
278 |
279 | implicit def Unzippable20[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, Z]: PUnzippable.In[
280 | (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S),
281 | Z,
282 | (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, Z)
283 | ] =
284 | new PUnzippable[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S), Z] {
285 | type In = (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, Z)
286 | def unzip(
287 | in: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, Z)
288 | ): ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S), Z) =
289 | (
290 | (
291 | in._1,
292 | in._2,
293 | in._3,
294 | in._4,
295 | in._5,
296 | in._6,
297 | in._7,
298 | in._8,
299 | in._9,
300 | in._10,
301 | in._11,
302 | in._12,
303 | in._13,
304 | in._14,
305 | in._15,
306 | in._16,
307 | in._17,
308 | in._18,
309 | in._19
310 | ),
311 | in._20
312 | )
313 | }
314 |
315 | implicit def Unzippable21[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, Z]: PUnzippable.In[
316 | (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T),
317 | Z,
318 | (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, Z)
319 | ] =
320 | new PUnzippable[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T), Z] {
321 | type In = (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, Z)
322 | def unzip(
323 | in: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, Z)
324 | ): ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T), Z) =
325 | (
326 | (
327 | in._1,
328 | in._2,
329 | in._3,
330 | in._4,
331 | in._5,
332 | in._6,
333 | in._7,
334 | in._8,
335 | in._9,
336 | in._10,
337 | in._11,
338 | in._12,
339 | in._13,
340 | in._14,
341 | in._15,
342 | in._16,
343 | in._17,
344 | in._18,
345 | in._19,
346 | in._20
347 | ),
348 | in._21
349 | )
350 | }
351 |
352 | implicit def Unzippable22[A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, Z]: PUnzippable.In[
353 | (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U),
354 | Z,
355 | (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, Z)
356 | ] =
357 | new PUnzippable[(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U), Z] {
358 | type In = (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, Z)
359 | def unzip(
360 | in: (A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, Z)
361 | ): ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U), Z) =
362 | (
363 | (
364 | in._1,
365 | in._2,
366 | in._3,
367 | in._4,
368 | in._5,
369 | in._6,
370 | in._7,
371 | in._8,
372 | in._9,
373 | in._10,
374 | in._11,
375 | in._12,
376 | in._13,
377 | in._14,
378 | in._15,
379 | in._16,
380 | in._17,
381 | in._18,
382 | in._19,
383 | in._20,
384 | in._21
385 | ),
386 | in._22
387 | )
388 | }
389 | }
390 |
391 | trait PUnzippableLowPriority3 {
392 |
393 | implicit def Unzippable2[A, B]: PUnzippable.In[A, B, (A, B)] =
394 | new PUnzippable[A, B] {
395 | type In = (A, B)
396 | def unzip(in: (A, B)): (A, B) =
397 | (in._1, in._2)
398 | }
399 | }
400 |
--------------------------------------------------------------------------------
/benchmarks/src/main/scala/zio/parser/benchmarks/lucene/ZioLuceneQueryParser.scala:
--------------------------------------------------------------------------------
1 | package zio.parser.benchmarks.lucene
2 |
3 | import zio.Chunk
4 | import zio.parser._
5 | import zio.parser.benchmarks.lucene.Query.TopLevelCombinatorQuery
6 |
7 | class ZioLuceneQueryParser(
8 | topLevelCombinator: List[Query] => TopLevelCombinatorQuery = Query.Or,
9 | esCompatible: Boolean = false
10 | ) {
11 | import ZioLuceneQueryParser._
12 |
13 | val numChar = Syntax.digit ?? "digit"
14 | val escapedChar = (Syntax.char('\\') ~> Syntax.anyChar).transform(
15 | ch => "\\" + ch,
16 | (s: String) => s(1)
17 | ) ?? "escaped char"
18 | val termStartChar =
19 | (Syntax
20 | .charNotIn(invalidStartChars.toSeq: _*)
21 | .transform(_.toString, (s: String) => s(0)) | escapedChar) ?? "term start"
22 | val termChar = (termStartChar | Syntax.charIn('-', '+').transform(_.toString, (s: String) => s(0))) ?? "term char"
23 | val whitespace = Syntax.charIn(whitespaceChars.toSeq: _*).unit(' ') ?? "whitespace"
24 | val whitespaces = whitespace.repeat0.asPrinted((), Chunk(())) ?? "any whitespace"
25 | val whitespaces1 = whitespace.repeat.asPrinted((), Chunk(())) ?? "at least 1 whitespace"
26 | val quotedChar =
27 | (Syntax.charNotIn('\"', '\\').transform(_.toString, (s: String) => s(0)) | escapedChar) ?? "quoted char"
28 |
29 | val and =
30 | (Syntax.string("AND", ()).between(whitespaces, whitespaces1 | Syntax.char('(')) |
31 | Syntax.string("&&", ()).surroundedBy(whitespaces)).backtrack ?? "AND"
32 | val or =
33 | (Syntax.string("OR", ()).between(whitespaces, whitespaces1 | Syntax.char('(')) |
34 | Syntax.string("||", ()).surroundedBy(whitespaces)).backtrack ?? "OR"
35 | val not =
36 | (Syntax.string("NOT", ()).between(whitespaces, whitespaces1 | Syntax.char('(')) |
37 | Syntax.char('!').surroundedBy(whitespaces)).backtrack ?? "NOT"
38 |
39 | val plus = Syntax.char('+').surroundedBy(whitespaces) ?? "plus"
40 | val minus = Syntax.char('-').surroundedBy(whitespaces) ?? "minus"
41 | val lparen = Syntax.char('(').surroundedBy(whitespaces) ?? "lparen"
42 | val rparen = Syntax.char(')').surroundedBy(whitespaces) ?? "rparen"
43 | val opColon = Syntax.char(':').surroundedBy(whitespaces) ?? "opColon"
44 | val opEqual = Syntax.char('=').surroundedBy(whitespaces) ?? "opEqual"
45 | val carat = Syntax.char('^').surroundedBy(whitespaces) ?? "carat"
46 | val tilde = Syntax.char('~').surroundedBy(whitespaces) ?? "tilde"
47 |
48 | val opLessThan = Syntax.string("<", RangeOp.LessThan).surroundedBy(whitespaces)
49 | val opLessThanEq = Syntax.string("<=", RangeOp.LessThanEq).surroundedBy(whitespaces)
50 | val opMoreThan = Syntax.string(">", RangeOp.MoreThan).surroundedBy(whitespaces)
51 | val opMoreThanEq = Syntax.string(">=", RangeOp.MoreThanEq).surroundedBy(whitespaces)
52 |
53 | val quoted = quotedChar.repeat0.flatten.surroundedBy(Syntax.char('\"')) ?? "quoted"
54 | val integer = numChar.repeat.transform(
55 | chars => chars.mkString.toInt,
56 | (num: Int) => Chunk.fromArray(num.toString.toCharArray)
57 | ) ?? "integer"
58 |
59 | val number = (numChar.repeat ~ (Syntax.char('.') ~> numChar.repeat).?).string.transform(
60 | _.toDouble,
61 | (d: Double) => d.toString
62 | ) ?? "number"
63 |
64 | val term = (termStartChar ~ termChar.repeat0)
65 | .transform(
66 | { case (head, tail) => unescape(head + tail.mkString) },
67 | (s: String) => (s.head.toString, Chunk(s.tail)) // TODO: escape
68 | )
69 | .filter((s: String) => !keywords.contains(s), "reserved keyword") ?? "term"
70 |
71 | val regexpTerm =
72 | (Syntax.string("\\/", '/') | Syntax.charNotIn('/')).repeat0.string.surroundedBy(Syntax.char('/')) ?? "regexpTerm"
73 |
74 | val rangeInStart = Syntax.string("[", BoundType.Inclusive).surroundedBy(whitespaces) ?? "rangeInStart"
75 | val rangeExStart = Syntax.string("{", BoundType.Exclusive).surroundedBy(whitespaces) ?? "rangeExStart"
76 | val rangeInEnd = Syntax.string("]", BoundType.Inclusive).surroundedBy(whitespaces) ?? "rangeInEnd"
77 | val rangeExEnd = Syntax.string("}", BoundType.Exclusive).surroundedBy(whitespaces) ?? "rangeExEnd"
78 |
79 | val rangeStart = (rangeInStart.widen[BoundType] | rangeExStart.widen[BoundType]) ?? "rangeStart"
80 | val rangeEnd = (rangeInEnd.widen[BoundType] | rangeExEnd.widen[BoundType]) ?? "rangeEnd"
81 | val rangeTo = (Syntax.string("TO", ()).surroundedBy(whitespaces)) ?? "rangeTo"
82 |
83 | val bound =
84 | (quotedChar.repeat0.flatten
85 | .surroundedBy(Syntax.char('\"')) | Syntax.charNotIn(' ', ']', '}').repeat.string) ?? "bound"
86 |
87 | val range =
88 | (
89 | ((rangeStart ~ bound).transform[Boundary](
90 | { case (t, s) => toBoundary(s, t) },
91 | {
92 | case Boundary.Bound(value, boundType) => (boundType, value)
93 | case Boundary.Unbound => (BoundType.Exclusive, "*")
94 | }
95 | ) ~ rangeTo ~
96 | (bound ~ rangeEnd).transform[Boundary](
97 | { case (s, t) => toBoundary(s, t) },
98 | {
99 | case Boundary.Bound(value, boundType) => (value, boundType)
100 | case Boundary.Unbound => ("*", BoundType.Exclusive)
101 | }
102 | )).transform(
103 | { case (lower, upper) => Query.Range(lower, upper) },
104 | (r: Query.Range) => (r.lower, r.upper)
105 | )
106 | ) ?? "range"
107 |
108 | sealed trait Mod
109 | object Mod {
110 | case object Require extends Mod
111 | case object Prohibit extends Mod
112 | case object Not extends Mod
113 | }
114 |
115 | val modifier =
116 | (plus.as(Mod.Require).backtrack.widen[Mod] |
117 | minus.as(Mod.Prohibit).backtrack.widen[Mod] |
118 | not
119 | .as(Mod.Not)
120 | .widen[Mod]) ?? "modifier"
121 |
122 | val fieldName = term.transform(unescapeWildcards, identity[String]) ?? "fieldName" // TODO: escapeWildcards
123 |
124 | val luceneFieldRange =
125 | (
126 | ((opLessThanEq.backtrack.widen[RangeOp] | opMoreThanEq.backtrack.widen[RangeOp] | opLessThan
127 | .widen[RangeOp] | opMoreThan
128 | .widen[RangeOp]) ~
129 | (quoted.backtrack | number.backtrack.transform(_.toString, (s: String) => s.toDouble) | term.backtrack))
130 | .transform(
131 | { case (op, term) =>
132 | op match {
133 | case RangeOp.LessThan =>
134 | Query.Range(Boundary.Unbound, Boundary.Bound(term, BoundType.Exclusive))
135 | case RangeOp.LessThanEq =>
136 | Query.Range(Boundary.Unbound, Boundary.Bound(term, BoundType.Inclusive))
137 | case RangeOp.MoreThan =>
138 | Query.Range(Boundary.Bound(term, BoundType.Exclusive), Boundary.Unbound)
139 | case RangeOp.MoreThanEq =>
140 | Query.Range(Boundary.Bound(term, BoundType.Inclusive), Boundary.Unbound)
141 | }
142 | },
143 | (r: Query.Range) =>
144 | r.lower match {
145 | case Boundary.Bound(value, BoundType.Exclusive) =>
146 | (RangeOp.MoreThan, value)
147 | case Boundary.Bound(value, BoundType.Inclusive) =>
148 | (RangeOp.MoreThanEq, value)
149 | case Boundary.Unbound =>
150 | r.upper match {
151 | case Boundary.Bound(value, BoundType.Exclusive) =>
152 | (RangeOp.LessThan, value)
153 | case Boundary.Bound(value, BoundType.Inclusive) =>
154 | (RangeOp.LessThanEq, value)
155 | }
156 | }
157 | )
158 | ) ?? "luceneFieldRange"
159 |
160 | private def mergeRanges(r1: Query.Range, r2: Query.Range): Either[String, Query.Range] = {
161 | val lowers = (r1.lower, r2.lower)
162 | val uppers = (r2.lower, r2.upper)
163 |
164 | for {
165 | lower <- lowers match {
166 | case (Boundary.Unbound, b: Boundary.Bound) => Right(b)
167 | case (b: Boundary.Bound, Boundary.Unbound) => Right(b)
168 | case (Boundary.Unbound, Boundary.Unbound) => Right(Boundary.Unbound)
169 | case (Boundary.Bound(_, _), Boundary.Bound(_, _)) =>
170 | Left("Two lower bounds provided with AND")
171 | }
172 | upper <- uppers match {
173 | case (Boundary.Unbound, b: Boundary.Bound) => Right(b)
174 | case (b: Boundary.Bound, Boundary.Unbound) => Right(b)
175 | case (Boundary.Unbound, Boundary.Unbound) => Right(Boundary.Unbound)
176 | case (Boundary.Bound(_, _), Boundary.Bound(_, _)) =>
177 | Left("Two upper bounds provided with AND")
178 | }
179 | } yield Query.Range(lower, upper)
180 | }
181 |
182 | private def splitRanges(r: Query.Range): Either[String, (Query.Range, Query.Range)] =
183 | ??? // TODO
184 |
185 | val esFieldRange =
186 | (luceneFieldRange.backtrack |
187 | ((luceneFieldRange <~ and) ~ luceneFieldRange)
188 | .between(lparen, rparen)
189 | .backtrack
190 | .transformEither((mergeRanges _).tupled, splitRanges) |
191 | ((plus ~> luceneFieldRange <~ whitespaces) ~ (plus ~> luceneFieldRange))
192 | .between(lparen, rparen)
193 | .backtrack
194 | .transformEither((mergeRanges _).tupled, splitRanges)) ?? "esFieldRange"
195 |
196 | val fieldRange =
197 | ((if (esCompatible) (fieldName <~ opColon) else fieldName) ~
198 | (if (esCompatible) esFieldRange else luceneFieldRange)).transform(
199 | { case (fieldName, range) => Query.Field(fieldName, range) },
200 | (f: Query.Field) => (f.fieldName, f.query.asInstanceOf[Query.Range]) // TODO: transformEither instead of cast
201 | ) ?? "fieldRange"
202 |
203 | val regexpTermQuery = regexpTerm.transform(Query.Regex, (r: Query.Regex) => r.value) ?? "regexpTermQuery"
204 |
205 | val fuzzyOp = tilde ~> integer.?.transform[Int](
206 | _.getOrElse(2),
207 | {
208 | case 2 => None
209 | case n: Int => Some(n)
210 | }
211 | ) ?? "fuzzyOp"
212 | val proximityOp = (tilde ~> integer) ?? "proximityOp"
213 |
214 | val fieldTermQuery =
215 | ((term | number.transform(_.toString, (s: String) => s.toDouble)) ~ fuzzyOp.backtrack.?).transform(
216 | { case (term, maxEditDistance) => Query.Term(term, maxEditDistance) },
217 | (t: Query.Term) => (t.value, t.maxEditDistances)
218 | ) ?? "fieldTermQuery"
219 |
220 | val quotedTermQuery =
221 | (quoted ~ proximityOp.backtrack.?).transform(
222 | { case (term, maxDistance) => Query.Phrase(term, maxDistance) },
223 | (p: Query.Phrase) => (p.value, p.maxDistance)
224 | ) ?? "quotedTermQuery"
225 |
226 | val termQueryInner =
227 | (regexpTermQuery.widen[Query] | range.widen[Query] | quotedTermQuery.widen[Query] | fieldTermQuery
228 | .widen[Query]) ?? "termQueryInner"
229 |
230 | val termQuery = (termQueryInner ~ (carat ~> number).backtrack.?).transform[Query](
231 | {
232 | case (query, Some(score)) => Query.Boost(query, score)
233 | case (query, None) => query
234 | },
235 | {
236 | case Query.Boost(inner, score) => (inner, Some(score))
237 | case other: Query => (other, None)
238 | }
239 | ) ?? "termQuery"
240 |
241 | val fieldNamePrefix = (fieldName <~ (opColon | opEqual).surroundedBy(whitespaces)) ?? "fieldNamePrefix"
242 |
243 | lazy val grouping: Syntax[String, Char, Char, Query] =
244 | (query.between(lparen, rparen) ~ (carat ~> number).backtrack.?).transform[Query](
245 | {
246 | case (query, Some(score)) => Query.Boost(query, score)
247 | case (query, None) => query
248 | },
249 | {
250 | case Query.Boost(inner, score) => (inner, Some(score))
251 | case other: Query => (other, None)
252 | }
253 | ) ?? "grouping"
254 |
255 | lazy val clause: Syntax[String, Char, Char, Query] = fieldRange.backtrack.widen[Query] |
256 | (fieldNamePrefix.backtrack.? ~ (grouping | termQuery))
257 | .transform[Query](
258 | {
259 | case (Some(fieldName), query) => Query.Field(fieldName, query).normalize
260 | case (None, query) => query
261 | },
262 | {
263 | case Query.Field(fieldName, query) => (Some(fieldName), query)
264 | case other: Query => (None, other)
265 | }
266 | )
267 | .surroundedBy(whitespaces) ?? "clause"
268 |
269 | val modClause = (modifier.? ~ clause).transform[Query](
270 | {
271 | case (Some(Mod.Require), query) => Query.Require(query)
272 | case (Some(Mod.Prohibit), query) => Query.Prohibit(query)
273 | case (Some(Mod.Not), query) => Query.Not(query)
274 | case (None, query) => query
275 | },
276 | {
277 | case Query.Require(query) => (Some(Mod.Require), query)
278 | case Query.Prohibit(query) => (Some(Mod.Prohibit), query)
279 | case Query.Not(query) => (Some(Mod.Not), query)
280 | case other: Query => (None, other)
281 | }
282 | ) ?? "modClause"
283 |
284 | lazy val conjunctQuery = modClause
285 | .repeatWithSep(and)
286 | .transform[Query](
287 | chunk => Query.And(chunk.toList).normalize,
288 | { case (and: Query.And) => Chunk.fromIterable(and.queries) }
289 | )
290 | .widen[Query] ?? "conjunctQuery"
291 |
292 | lazy val disjunctQuery = conjunctQuery
293 | .repeatWithSep(or)
294 | .transform[Query](
295 | chunk => Query.Or(chunk.toList).normalize,
296 | { case (or: Query.Or) => Chunk.fromIterable(or.queries) }
297 | )
298 | .widen[Query] ?? "disjunctQuery"
299 |
300 | lazy val query: Syntax[String, Char, Char, Query] =
301 | disjunctQuery
302 | .repeatWithSep(whitespaces)
303 | .transform[Query](
304 | chunk => topLevelCombinator(chunk.toList).normalize,
305 | { case Query.Or(lst) =>
306 | Chunk.fromIterable(lst) // TODO: other cases
307 | }
308 | )
309 | .manualBacktracking ?? "query"
310 | }
311 |
312 | object ZioLuceneQueryParser {
313 | val invalidStartChars: Set[Char] = Set(' ', '\t', '\n', '\r', '\u3000', '+', '-', '!', '(', ')', ':', '^', '<', '>',
314 | '=', '[', ']', '\"', '{', '}', '~', '\\', '/')
315 |
316 | val whitespaceChars: Set[Char] = Set(' ', '\t', '\n', '\r', '\u3000')
317 |
318 | val keywords: Set[String] = Set("AND", "OR", "NOT")
319 |
320 | sealed trait RangeOp
321 | object RangeOp {
322 | final object LessThan extends RangeOp
323 | final object LessThanEq extends RangeOp
324 | final object MoreThan extends RangeOp
325 | final object MoreThanEq extends RangeOp
326 | }
327 |
328 | private def unescape(s: String): String =
329 | s.replaceAll("""\\([^?*])""", "$1")
330 |
331 | private def unescapeWildcards(s: String): String =
332 | s.replaceAll("""\\([?*])""", "$1")
333 |
334 | private def toBoundary(term: String, boundType: BoundType): Boundary =
335 | if (term == "*") Boundary.Unbound else Boundary.Bound(term, boundType)
336 | }
337 |
--------------------------------------------------------------------------------