├── 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 | [![Zymposium - ZIO Parser](https://i.ytimg.com/vi/DEPpL9LBiyA/maxresdefault.jpg)](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 | [![Development](https://img.shields.io/badge/Project%20Stage-Development-green.svg)](https://github.com/zio/zio/wiki/Project-Stages) ![CI Badge](https://github.com/zio/zio-parser/workflows/CI/badge.svg) [![Sonatype Releases](https://img.shields.io/nexus/r/https/oss.sonatype.org/dev.zio/zio-parser_3.svg?label=Sonatype%20Release)](https://oss.sonatype.org/content/repositories/releases/dev/zio/zio-parser_3/) [![Sonatype Snapshots](https://img.shields.io/nexus/s/https/oss.sonatype.org/dev.zio/zio-parser_3.svg?label=Sonatype%20Snapshot)](https://oss.sonatype.org/content/repositories/snapshots/dev/zio/zio-parser_3/) [![javadoc](https://javadoc.io/badge2/dev.zio/zio-parser-docs_3/javadoc.svg)](https://javadoc.io/doc/dev.zio/zio-parser-docs_3) [![ZIO Parser](https://img.shields.io/github/stars/zio/zio-parser?style=social)](https://github.com/zio/zio-parser) 10 | 11 | ## Introduction 12 | 13 | [![Zymposium - ZIO Parser](https://i.ytimg.com/vi/DEPpL9LBiyA/maxresdefault.jpg)](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"", ()) ?? 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 | --------------------------------------------------------------------------------