diff --git a/benchmark/README.md b/benchmark/README.md index f28eaf79..7e8c96c1 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -15,15 +15,15 @@ sbt "jmh:run -i 10 -wi 10 -f 2 -t 1 co.blocke.WritingBenchmark" ## Writing Performance: -| Benchmark | Mode | Count | Score | Error | Units | -|------------------|-------|----:|------------:|-------------:|-------| -| Hand-Tooled | thrpt | 20 | 2,575,393.513 | ± 178731.952 | ops/s | -| Circe | thrpt | 20 | 1,939,339.085 | ± 6279.547 | ops/s | -|**ScalaJack 8** | thrpt | 20 | **1,703,256.521** | ± 12260.518 | ops/s | -| ZIO JSON | thrpt | 20 | 818,228.736 | ± 3070.298 | ops/s | -| Argonaut | thrpt | 20 | 716,228.404 | ± 6241.145 | ops/s | -| Play JSON | thrpt | 20 | 438,538.475 | ± 16319.198 | ops/s | -| ScalaJack 7 | thrpt | 20 | 106,292.338 | ± 330.111 | ops/s | +| Benchmark | Mode | Count | Score | Error | Units | +|------------------|-------|-------:|----------------:|-------------:|-------| +| Hand-Tooled | thrpt | 20 | 2,575,393.513 | ± 178731.952 | ops/s | +| Circe | thrpt | 20 | 1,939,339.085 | ± 6279.547 | ops/s | +|**ScalaJack 8** | thrpt | 20 | **176,867,514.557** | ± 12260.518 | ops/s | +| ZIO JSON | thrpt | 20 | 818,228.736 | ± 3070.298 | ops/s | +| Argonaut | thrpt | 20 | 716,228.404 | ± 6241.145 | ops/s | +| Play JSON | thrpt | 20 | 438,538.475 | ± 16319.198 | ops/s | +| ScalaJack 7 | thrpt | 20 | 106,292.338 | ± 330.111 | ops/s | ### Interpretation diff --git a/benchmark/build.sbt b/benchmark/build.sbt index 2b1ad05b..761843db 100644 --- a/benchmark/build.sbt +++ b/benchmark/build.sbt @@ -7,10 +7,7 @@ val compilerOptions = Seq( "-feature", "-language:existentials", "-language:higherKinds", - "-unchecked", - "-Ywarn-dead-code", - "-Ywarn-numeric-widen", - "-Xfuture" + "-unchecked" ) val circeVersion = "0.15.0-M1" @@ -39,9 +36,14 @@ lazy val benchmark = project libraryDependencies ++= Seq( "org.playframework" %% "play-json" % "3.0.1", "io.argonaut" %% "argonaut" % "6.3.9", - "co.blocke" %% "scalajack" % "826a30_unknown", + // "co.blocke" %% "scalajack" % "826a30_unknown", // Old-New + "co.blocke" %% "scalajack" % "e48b35_unknown", // New-New "co.blocke" %% "scala-reflection" % "sj_fixes_edbef8", "dev.zio" %% "zio-json" % "0.6.1", + "org.typelevel" %% "fabric-core" % "1.12.6", + "org.typelevel" %% "fabric-io" % "1.12.6", + "org.typelevel" %% "jawn-parser" % "1.3.2", + "org.typelevel" %% "jawn-ast" % "1.3.2", // "io.circe" %% "circe-derivation" % "0.15.0-M1", // "io.circe" %% "circe-jackson29" % "0.14.0", // "org.json4s" %% "json4s-jackson" % "4.0.4", diff --git a/benchmark/src/main/scala/co.blocke/Argonaut.scala b/benchmark/src/main/scala/co.blocke/Argonaut.scala index 99424827..7b614f0e 100644 --- a/benchmark/src/main/scala/co.blocke/Argonaut.scala +++ b/benchmark/src/main/scala/co.blocke/Argonaut.scala @@ -1,26 +1,27 @@ package co.blocke -import argonaut._, Argonaut._ import org.openjdk.jmh.annotations._ -implicit val CodecPet: CodecJson[Pet] = - casecodec3(Pet.apply, (a: Pet) => Option((a.name, a.species, a.age)))("name","species","age") +object ArgonautZ: + import argonaut._, Argonaut._ -implicit val CodecFriend: CodecJson[Friend] = - casecodec3(Friend.apply, (a: Friend) => Option((a.name, a.age, a.email)))("name","age","email") + implicit val CodecPet: CodecJson[Pet] = + casecodec3(Pet.apply, (a: Pet) => Option((a.name, a.species, a.age)))("name","species","age") -implicit val CodecAddress: CodecJson[Address] = - casecodec4(Address.apply, (a: Address) => Option((a.street, a.city, a.state, a.postal_code)))("street","city","state","postal_code") + implicit val CodecFriend: CodecJson[Friend] = + casecodec3(Friend.apply, (a: Friend) => Option((a.name, a.age, a.email)))("name","age","email") -implicit val CodecPerson: CodecJson[Person] = - casecodec6(Person.apply, (a: Person) => Option((a.name, a.age, a.address, a.email, a.phone_numbers, a.is_employed)))("name", "age","address","email","phone_numbers","is_employed") + implicit val CodecAddress: CodecJson[Address] = + casecodec4(Address.apply, (a: Address) => Option((a.street, a.city, a.state, a.postal_code)))("street","city","state","postal_code") -implicit val CodecRecord: CodecJson[Record] = - casecodec4(Record.apply, (a: Record) => Option((a.person, a.hobbies, a.friends, a.pets)))("person", "hobbies", "friends", "pets") + implicit val CodecPerson: CodecJson[Person] = + casecodec6(Person.apply, (a: Person) => Option((a.name, a.age, a.address, a.email, a.phone_numbers, a.is_employed)))("name", "age","address","email","phone_numbers","is_employed") + implicit val CodecRecord: CodecJson[Record] = + casecodec4(Record.apply, (a: Record) => Option((a.person, a.hobbies, a.friends, a.pets)))("person", "hobbies", "friends", "pets") -trait ArgonautWritingBenchmark { - @Benchmark - def writeRecordArgonaut = record.asJson -} + trait ArgonautWritingBenchmark { + @Benchmark + def writeRecordArgonaut = record.asJson + } diff --git a/benchmark/src/main/scala/co.blocke/Benchmark.scala b/benchmark/src/main/scala/co.blocke/Benchmark.scala index a5210215..4f6a2110 100644 --- a/benchmark/src/main/scala/co.blocke/Benchmark.scala +++ b/benchmark/src/main/scala/co.blocke/Benchmark.scala @@ -1,52 +1,14 @@ package co.blocke import org.openjdk.jmh.annotations._ - import java.util.concurrent.TimeUnit -import co.blocke.scalajack.* - -import io.circe.syntax.* -import io.circe.* -import io.circe.generic.semiauto.* - -val record = ScalaJack.read[Record](jsData) - -implicit val recordDecoder: Decoder[Record] = deriveDecoder[Record] -implicit val recordEncoder: Encoder[Record] = deriveEncoder[Record] - -implicit val personDecoder: Decoder[Person] = deriveDecoder[Person] -implicit val personEncoder: Encoder[Person] = deriveEncoder[Person] - -implicit val addressDecoder: Decoder[Address] = deriveDecoder[Address] -implicit val addressEncoder: Encoder[Address] = deriveEncoder[Address] - -implicit val friendDecoder: Decoder[Friend] = deriveDecoder[Friend] -implicit val friendEncoder: Encoder[Friend] = deriveEncoder[Friend] - -implicit val petDecoder: Decoder[Pet] = deriveDecoder[Pet] -implicit val petEncoder: Encoder[Pet] = deriveEncoder[Pet] - - - -trait CirceReadingBenchmark{ - val circeJson = record.asJson - def readRecordCirce = circeJson.as[Record] -} - -// trait ScalaJackReadingBenchmark{ -// def readRecordScalaJack = ScalaJack.read[Record](jsData) -// } - -trait CirceWritingBenchmark { - @Benchmark - def writeRecordCirce = record.asJson -} +import ZIOZ.* +import zio.json._ +val record = jsData.fromJson[Record] match + case Right(r) => r + case Left(_) => null.asInstanceOf[Record] -trait ScalaJackWritingBenchmark { - @Benchmark - def writeRecordScalaJack = ScalaJack.write(record) -} trait HandTooledWritingBenchmark { @Benchmark @@ -77,20 +39,45 @@ trait HandTooledWritingBenchmark { sb.toString } -// @State(Scope.Thread) -// @BenchmarkMode(Array(Mode.Throughput)) -// @OutputTimeUnit(TimeUnit.SECONDS) -// class ReadingBenchmark -// extends CirceReadingBenchmark -// with ScalaJackReadingBenchmark +@State(Scope.Thread) +@BenchmarkMode(Array(Mode.Throughput)) +@OutputTimeUnit(TimeUnit.SECONDS) +class ReadingBenchmark + // extends CirceZ.CirceReadingBenchmark + extends ScalaJackZ.ScalaJackReadingBenchmark + // extends ZIOZ.ZIOJsonReadingBenchmark + // extends PlayZ.PlayReadingBenchmark + // extends FabricZ.FabricReadingBenchmark + // extends JawnZ.JawnReadingBenchmark @State(Scope.Thread) @BenchmarkMode(Array(Mode.Throughput)) @OutputTimeUnit(TimeUnit.SECONDS) class WritingBenchmark - extends CirceWritingBenchmark - with ScalaJackWritingBenchmark - with HandTooledWritingBenchmark - with ArgonautWritingBenchmark - with PlayWritingBenchmark - with ZIOJsonWritingBenchmark + // extends CirceZ.CirceWritingBenchmark + extends ScalaJackZ.ScalaJackWritingBenchmark + // with HandTooledWritingBenchmark + // with ArgonautZ.ArgonautWritingBenchmark + // with PlayZ.PlayWritingBenchmark + // with ZIOZ.ZIOJsonWritingBenchmark + + +// "Old-New" ScalaJack +// [info] Benchmark Mode Cnt Score Error Units +// [info] ReadingBenchmark.readRecordScalaJack thrpt 20 30113.982 ± 97.701 ops/s +// [info] New-New ScalaJack thrpt 20 50908.982 ± 97.701 ops/s + +// ScalaJack w/ZIO-based parser 635977.008 +// ZIO (Fast!!) 568123.000 <-- How do they do this?! More than 2x faster than everyone else! +// Circe 279231.646 +// Play 209756.408 + +// Jawn (parse only + AST) 336384.617 +// ScalaJack JsonParser3 (parse only + AST) 279456.523 +// Fabric (new!) (parse only + AST) 270706.567 + + + + +// SJ StringBuffer : 1740040.225 +// SJ FastStringBuffer : \ No newline at end of file diff --git a/benchmark/src/main/scala/co.blocke/Circe.scala b/benchmark/src/main/scala/co.blocke/Circe.scala new file mode 100644 index 00000000..ebb88705 --- /dev/null +++ b/benchmark/src/main/scala/co.blocke/Circe.scala @@ -0,0 +1,34 @@ +package co.blocke + +import org.openjdk.jmh.annotations._ + +object CirceZ: + import io.circe.syntax.* + import io.circe.* + import io.circe.generic.semiauto.* + import io.circe.parser.* + + implicit val recordDecoder: Decoder[Record] = deriveDecoder[Record] + implicit val recordEncoder: Encoder[Record] = deriveEncoder[Record] + + implicit val personDecoder: Decoder[Person] = deriveDecoder[Person] + implicit val personEncoder: Encoder[Person] = deriveEncoder[Person] + + implicit val addressDecoder: Decoder[Address] = deriveDecoder[Address] + implicit val addressEncoder: Encoder[Address] = deriveEncoder[Address] + + implicit val friendDecoder: Decoder[Friend] = deriveDecoder[Friend] + implicit val friendEncoder: Encoder[Friend] = deriveEncoder[Friend] + + implicit val petDecoder: Decoder[Pet] = deriveDecoder[Pet] + implicit val petEncoder: Encoder[Pet] = deriveEncoder[Pet] + + trait CirceReadingBenchmark{ + @Benchmark + def readRecordCirce = parse(jsData).flatMap(_.as[Record]) + } + + trait CirceWritingBenchmark { + @Benchmark + def writeRecordCirce = record.asJson + } diff --git a/benchmark/src/main/scala/co.blocke/Fabric.scala b/benchmark/src/main/scala/co.blocke/Fabric.scala new file mode 100644 index 00000000..18c3fb81 --- /dev/null +++ b/benchmark/src/main/scala/co.blocke/Fabric.scala @@ -0,0 +1,17 @@ +package co.blocke + +import org.openjdk.jmh.annotations._ + +object FabricZ: + import fabric.* + import fabric.io.* + + trait FabricReadingBenchmark{ + @Benchmark + def readRecordFabric = JsonParser(jsData, Format.Json) + } + + // trait CirceWritingBenchmark { + // @Benchmark + // def writeRecordCirce = record.asJson + // } diff --git a/benchmark/src/main/scala/co.blocke/Jawn.scala b/benchmark/src/main/scala/co.blocke/Jawn.scala new file mode 100644 index 00000000..68b7ad8a --- /dev/null +++ b/benchmark/src/main/scala/co.blocke/Jawn.scala @@ -0,0 +1,17 @@ +package co.blocke + +import org.openjdk.jmh.annotations._ + +object JawnZ: + + import org.typelevel.jawn.ast.* + + trait JawnReadingBenchmark{ + @Benchmark + def readRecordFabric = JParser.parseFromString(jsData) + } + + // trait CirceWritingBenchmark { + // @Benchmark + // def writeRecordCirce = record.asJson + // } diff --git a/benchmark/src/main/scala/co.blocke/PlayJson.scala b/benchmark/src/main/scala/co.blocke/PlayJson.scala index 9e1ccd27..0eb58a60 100644 --- a/benchmark/src/main/scala/co.blocke/PlayJson.scala +++ b/benchmark/src/main/scala/co.blocke/PlayJson.scala @@ -1,46 +1,60 @@ package co.blocke -import play.api.libs.json._ -import play.api.libs.json.Reads._ -import play.api.libs.functional.syntax._ import org.openjdk.jmh.annotations._ -implicit val friendWrites: Writes[Friend] = ( - (JsPath \ "name").write[String] and - (JsPath \ "age").write[Int] and - (JsPath \ "email").write[String] -)(unlift((a: Friend) => Option((a.name, a.age, a.email)))) - -implicit val petWrites: Writes[Pet] = ( - (JsPath \ "name").write[String] and - (JsPath \ "species").write[String] and - (JsPath \ "age").write[Int] -)(unlift((a: Pet) => Option((a.name, a.species, a.age)))) - -implicit val addressWrites: Writes[Address] = ( - (JsPath \ "street").write[String] and - (JsPath \ "city").write[String] and - (JsPath \ "state").write[String] and - (JsPath \ "postal_code").write[String] -)(unlift((a: Address) => Option((a.street, a.city, a.state, a.postal_code)))) - -implicit val personWrites: Writes[Person] = ( - (JsPath \ "namet").write[String] and - (JsPath \ "age").write[Int] and - (JsPath \ "address").write[Address] and - (JsPath \ "email").write[String] and - (JsPath \ "phone_numbers").write[List[String]] and - (JsPath \ "is_employed").write[Boolean] -)(unlift((a: Person) => Option((a.name, a.age, a.address, a.email, a.phone_numbers, a.is_employed)))) - -implicit val recordWrites: Writes[Record] = ( - (JsPath \ "person").write[Person] and - (JsPath \ "hobbies").write[List[String]] and - (JsPath \ "friends").write[List[Friend]] and - (JsPath \ "pets").write[List[Pet]] -)(unlift((a: Record) => Option((a.person, a.hobbies, a.friends, a.pets)))) - -trait PlayWritingBenchmark { - @Benchmark - def writeRecordPlay = Json.toJson(record) -} +object PlayZ: + import play.api.libs.json._ + import play.api.libs.json.Reads._ + import play.api.libs.functional.syntax._ + + implicit val friendWrites: Writes[Friend] = ( + (JsPath \ "name").write[String] and + (JsPath \ "age").write[Int] and + (JsPath \ "email").write[String] + )(unlift((a: Friend) => Option((a.name, a.age, a.email)))) + + implicit val petWrites: Writes[Pet] = ( + (JsPath \ "name").write[String] and + (JsPath \ "species").write[String] and + (JsPath \ "age").write[Int] + )(unlift((a: Pet) => Option((a.name, a.species, a.age)))) + + implicit val addressWrites: Writes[Address] = ( + (JsPath \ "street").write[String] and + (JsPath \ "city").write[String] and + (JsPath \ "state").write[String] and + (JsPath \ "postal_code").write[String] + )(unlift((a: Address) => Option((a.street, a.city, a.state, a.postal_code)))) + + implicit val personWrites: Writes[Person] = ( + (JsPath \ "namet").write[String] and + (JsPath \ "age").write[Int] and + (JsPath \ "address").write[Address] and + (JsPath \ "email").write[String] and + (JsPath \ "phone_numbers").write[List[String]] and + (JsPath \ "is_employed").write[Boolean] + )(unlift((a: Person) => Option((a.name, a.age, a.address, a.email, a.phone_numbers, a.is_employed)))) + + implicit val recordWrites: Writes[Record] = ( + (JsPath \ "person").write[Person] and + (JsPath \ "hobbies").write[List[String]] and + (JsPath \ "friends").write[List[Friend]] and + (JsPath \ "pets").write[List[Pet]] + )(unlift((a: Record) => Option((a.person, a.hobbies, a.friends, a.pets)))) + + implicit val friendReads: play.api.libs.json.Reads[co.blocke.Friend] = Json.reads[Friend] + implicit val petReads: play.api.libs.json.Reads[co.blocke.Pet] = Json.reads[Pet] + implicit val addressReads: play.api.libs.json.Reads[co.blocke.Address] = Json.reads[Address] + implicit val personReads: play.api.libs.json.Reads[co.blocke.Person] = Json.reads[Person] + implicit val recordReads: play.api.libs.json.Reads[co.blocke.Record] = Json.reads[Record] + + trait PlayWritingBenchmark { + @Benchmark + def writeRecordPlay = Json.toJson(record) + } + + // val playJS = Json.toJson(record) + trait PlayReadingBenchmark { + @Benchmark + def readRecordPlay = Json.fromJson[Record](Json.parse(jsData)) //Json.fromJson[Record](playJS) + } diff --git a/benchmark/src/main/scala/co.blocke/Run.scala b/benchmark/src/main/scala/co.blocke/Run.scala index 5a1d8a31..dc945391 100644 --- a/benchmark/src/main/scala/co.blocke/Run.scala +++ b/benchmark/src/main/scala/co.blocke/Run.scala @@ -1,8 +1,16 @@ package co.blocke -case class Foo() extends ZIOJsonWritingBenchmark object RunMe extends App: - val f = Foo() - println(f.writeRecordZIOJson) \ No newline at end of file + import ZIOZ.* + import zio.json._ + import co.blocke.scalajack.* + + val f = jsData.fromJson[Record] + println(f) + + println("\n---------") + println(ScalaJack.write(f)) + + println("ZIO Decoder (Address): "+DeriveJsonDecoder.gen[Address]) \ No newline at end of file diff --git a/benchmark/src/main/scala/co.blocke/ScalaJack.scala b/benchmark/src/main/scala/co.blocke/ScalaJack.scala new file mode 100644 index 00000000..0701cbe6 --- /dev/null +++ b/benchmark/src/main/scala/co.blocke/ScalaJack.scala @@ -0,0 +1,43 @@ +package co.blocke + +import org.openjdk.jmh.annotations._ + +object ScalaJackZ: + import co.blocke.scalajack.* + + import json2.* + + implicit val addrDecoder: JsonDecoder[Address] = ClassDecoder( + Array("street", "city", "state", "postal_code"), + Array(JsonDecoder[String], JsonDecoder[String], JsonDecoder[String], JsonDecoder[String]) + ) + implicit val friendDecoder: JsonDecoder[Friend] = ClassDecoder( + Array("name", "age", "email"), + Array(JsonDecoder[String], JsonDecoder[Int], JsonDecoder[String]) + ) + implicit val petDecoder: JsonDecoder[Pet] = ClassDecoder( + Array("name", "species", "age"), + Array(JsonDecoder[String], JsonDecoder[String], JsonDecoder[Int]) + ) + implicit val PersonDecoder: JsonDecoder[Person] = ClassDecoder( + Array("name", "age", "address", "email", "phone_numbers", "is_employed"), + Array(JsonDecoder[String], JsonDecoder[Int], JsonDecoder[Address], JsonDecoder[String], JsonDecoder[List[String]], JsonDecoder[Boolean]) + ) + implicit val RecordDecoder: JsonDecoder[Record] = ClassDecoder( + Array("person", "hobbies", "friends", "pets"), + Array(JsonDecoder[Person], JsonDecoder[List[String]], JsonDecoder[List[Friend]], JsonDecoder[List[Pet]]) + ) + + // val jp = co.blocke.scalajack.json.JsonParser3(jsData) + + trait ScalaJackReadingBenchmark{ + @Benchmark + // def readRecordScalaJack = { jp.reset(); jp.parse() } + // def readRecordScalaJack = ScalaJack.read[Record](jsData) + def readRecordScalaJack = JsonDecoder[Record].decodeJson(jsData) + } + + trait ScalaJackWritingBenchmark { + @Benchmark + def writeRecordScalaJack = ScalaJack.write(record) + } \ No newline at end of file diff --git a/benchmark/src/main/scala/co.blocke/ZIOJson.scala b/benchmark/src/main/scala/co.blocke/ZIOJson.scala index d0462744..9a5b2d97 100644 --- a/benchmark/src/main/scala/co.blocke/ZIOJson.scala +++ b/benchmark/src/main/scala/co.blocke/ZIOJson.scala @@ -1,20 +1,27 @@ package co.blocke -import zio.json._ import org.openjdk.jmh.annotations._ -implicit val decoder1: JsonDecoder[Address] = DeriveJsonDecoder.gen[Address] -implicit val decoder2: JsonDecoder[Pet] = DeriveJsonDecoder.gen[Pet] -implicit val decoder3: JsonDecoder[Friend] = DeriveJsonDecoder.gen[Friend] -implicit val decoder4: JsonDecoder[Person] = DeriveJsonDecoder.gen[Person] -implicit val decoder5: JsonDecoder[Record] = DeriveJsonDecoder.gen[Record] -implicit val encoder1: JsonEncoder[Address] = DeriveJsonEncoder.gen[Address] -implicit val encoder2: JsonEncoder[Pet] = DeriveJsonEncoder.gen[Pet] -implicit val encoder3: JsonEncoder[Friend] = DeriveJsonEncoder.gen[Friend] -implicit val encoder4: JsonEncoder[Person] = DeriveJsonEncoder.gen[Person] -implicit val encoder5: JsonEncoder[Record] = DeriveJsonEncoder.gen[Record] +object ZIOZ: + import zio.json._ -trait ZIOJsonWritingBenchmark { - @Benchmark - def writeRecordZIOJson = record.toJson -} \ No newline at end of file + implicit val decoder1: JsonDecoder[Address] = DeriveJsonDecoder.gen[Address] + implicit val decoder2: JsonDecoder[Pet] = DeriveJsonDecoder.gen[Pet] + implicit val decoder3: JsonDecoder[Friend] = DeriveJsonDecoder.gen[Friend] + implicit val decoder4: JsonDecoder[Person] = DeriveJsonDecoder.gen[Person] + implicit val decoder5: JsonDecoder[Record] = DeriveJsonDecoder.gen[Record] + implicit val encoder1: JsonEncoder[Address] = DeriveJsonEncoder.gen[Address] + implicit val encoder2: JsonEncoder[Pet] = DeriveJsonEncoder.gen[Pet] + implicit val encoder3: JsonEncoder[Friend] = DeriveJsonEncoder.gen[Friend] + implicit val encoder4: JsonEncoder[Person] = DeriveJsonEncoder.gen[Person] + implicit val encoder5: JsonEncoder[Record] = DeriveJsonEncoder.gen[Record] + + trait ZIOJsonWritingBenchmark { + @Benchmark + def writeRecordZIOJson = record.toJson + } + + trait ZIOJsonReadingBenchmark { + @Benchmark + def readRecordZIOJson = jsData.fromJson[Record] + } \ No newline at end of file diff --git a/src/main/scala/co.blocke.scalajack/ScalaJack.scala b/src/main/scala/co.blocke.scalajack/ScalaJack.scala index 7974fb9a..73bac0f8 100644 --- a/src/main/scala/co.blocke.scalajack/ScalaJack.scala +++ b/src/main/scala/co.blocke.scalajack/ScalaJack.scala @@ -3,6 +3,7 @@ package co.blocke.scalajack import co.blocke.scala_reflection.{RTypeRef, TypedName} import co.blocke.scala_reflection.reflect.ReflectOnType import co.blocke.scala_reflection.reflect.rtypeRefs.ClassRef +import parser.ParseError import scala.collection.mutable.{HashMap, Map} import scala.quoted.* import quoted.Quotes @@ -21,22 +22,43 @@ object ScalaJack: // --------------------------------------------------------------------- - inline def read[T](js: String)(using cfg: JsonConfig = JsonConfig()): T = ${ readImpl[T]('js, 'cfg) } + inline def read[T](js: String)(using cfg: JsonConfig = JsonConfig()): Either[ParseError, T] = ${ readImpl[T]('js, 'cfg) } - def readImpl[T: Type](js: Expr[String], cfg: Expr[JsonConfig])(using q: Quotes): Expr[T] = + def readImpl[T: Type](js: Expr[String], cfg: Expr[JsonConfig])(using q: Quotes): Expr[Either[ParseError, T]] = import quotes.reflect.* val classRef = ReflectOnType[T](quotes)(TypeRepr.of[T], true)(using scala.collection.mutable.Map.empty[TypedName, Boolean]) - // Used to trap SelfRef's from going into endless loops and causing Stack Overflow. - val seenBeforeFnCache = HashMap.empty[Expr[TypedName], Expr[(JsonConfig, JsonParser) => Either[ParseError, ?]]] + val instruction = JsonReader.refRead[T](classRef) - val fn = JsonReader().readerFn[T](classRef)(using quotes, Type.of[T])(using seenBeforeFnCache) - val listifiedCache = Expr.ofList(seenBeforeFnCache.toList.map(t => Expr.ofTuple(t))) - - '{ // run-time - val parser = JsonParser($js, $listifiedCache.toMap) - $fn($cfg, parser) match - case Right(v) => v - case Left(t) => throw t + '{ + val foo = json2.JsonReader($js) + var c = 0 + while c != json2.BUFFER_EXCEEDED do c = foo.read() + Right(null.asInstanceOf[T]) } + + // '{ + // val parser = JsonParser2($js, $cfg) + // parser.parse($instruction).asInstanceOf[Either[ParseError, T]] + // } + + // -------------------->>>> OLD <<---------------------- + + // // Used to trap SelfRef's from going into endless loops and causing Stack Overflow. + // val seenBeforeFnCache = HashMap.empty[Expr[TypedName], Expr[(JsonConfig, JsonParser) => Either[ParseError, ?]]] + + // val parserE = '{ new JsonParser($js, $cfg) } + // JsonReader().refRead(classRef, parserE, cfg)(using quotes, Type.of[T])(using seenBeforeFnCache) + + // --------------- + + // val fn = JsonReader().readerFn[T](classRef)(using quotes, Type.of[T])(using seenBeforeFnCache) + // val listifiedCache = Expr.ofList(seenBeforeFnCache.toList.map(t => Expr.ofTuple(t))) + + // '{ // run-time + // val parser = JsonParser($js, $listifiedCache.toMap) + // $fn($cfg, parser) match + // case Right(v) => v + // case Left(t) => throw t + // } diff --git a/src/main/scala/co.blocke.scalajack/json/JsonError.scala b/src/main/scala/co.blocke.scalajack/json/JsonError.scala index 076a7388..848c12c9 100644 --- a/src/main/scala/co.blocke.scalajack/json/JsonError.scala +++ b/src/main/scala/co.blocke.scalajack/json/JsonError.scala @@ -3,6 +3,5 @@ package json class JsonError(msg: String) extends Throwable -abstract class ParseError(message: String) extends Throwable(message) -case class JsonParseError(message: String) extends ParseError(message) -case class CommaExpected(message: String = "") extends ParseError(message) +case class JsonParseError(message: String) extends parser.ParseError(message) +case class CommaExpected(message: String = "") extends parser.ParseError(message) diff --git a/src/main/scala/co.blocke.scalajack/json/JsonParser.scala b/src/main/scala/co.blocke.scalajack/json/JsonParser.scala index 4b558da6..cfbdb80f 100644 --- a/src/main/scala/co.blocke.scalajack/json/JsonParser.scala +++ b/src/main/scala/co.blocke.scalajack/json/JsonParser.scala @@ -1,11 +1,12 @@ package co.blocke.scalajack package json +import parser.* import scala.util.* import co.blocke.scala_reflection.TypedName import scala.collection.mutable.ListBuffer -case class JsonParser(js: String, cache: Map[TypedName, (JsonConfig, JsonParser) => Either[ParseError, ?]]): +case class JsonParser(js: String, cfg: JsonConfig): // , cache: Map[TypedName, (JsonConfig, JsonParser) => Either[ParseError, ?]]): private val jsChars: Array[Char] = js.toCharArray private var i = 0 @@ -61,7 +62,7 @@ case class JsonParser(js: String, cache: Map[TypedName, (JsonConfig, JsonParser) // Data Types // ------------------------------ - def expectBoolean(cfg: JsonConfig, p: JsonParser): Either[ParseError, Boolean] = + def expectBoolean(isMapKey: Boolean = false): Either[ParseError, Boolean] = jsChars(i) match case 't' if i + 3 < max && jsChars(i + 1) == 'r' && jsChars(i + 2) == 'u' && jsChars(i + 3) == 'e' => i += 4 @@ -70,15 +71,15 @@ case class JsonParser(js: String, cache: Map[TypedName, (JsonConfig, JsonParser) i += 5 Right(false) case x => - if cfg.permissivePrimitives && jsChars(i) == '"' then + if (isMapKey || cfg.permissivePrimitives) && jsChars(i) == '"' then for { _ <- expectQuote - result <- expectBoolean(cfg, p) + result <- expectBoolean() _ <- expectQuote } yield result else Left(JsonParseError(showError(s"Unexpected character '$x' where beginning of boolean value expected at position [$i]"))) - def expectLong(cfg: JsonConfig, p: JsonParser): Either[ParseError, Long] = + def expectLong(isMapKey: Boolean = false): Either[ParseError, Long] = val mark = i var done = false while !done do @@ -88,10 +89,10 @@ case class JsonParser(js: String, cache: Map[TypedName, (JsonConfig, JsonParser) Try(js.substring(mark, i).toLong) match case Success(g) => Right(g) case Failure(f) => - if cfg.permissivePrimitives && jsChars(i) == '"' then + if (isMapKey || cfg.permissivePrimitives) && jsChars(i) == '"' then for { _ <- expectQuote - result <- expectLong(cfg, p) + result <- expectLong() _ <- expectQuote } yield result else @@ -101,7 +102,7 @@ case class JsonParser(js: String, cache: Map[TypedName, (JsonConfig, JsonParser) i = mark Left(JsonParseError(showError(msg))) - def expectBigLong(cfg: JsonConfig, p: JsonParser): Either[ParseError, BigInt] = + def expectBigLong(isMapKey: Boolean = false): Either[ParseError, BigInt] = nullCheck match case true if cfg.forbidNullsInInput => Left(JsonParseError(showError(s"Forbidden 'null' value received at position [$i]"))) case true => Right(null.asInstanceOf[BigInt]) @@ -115,10 +116,10 @@ case class JsonParser(js: String, cache: Map[TypedName, (JsonConfig, JsonParser) Try(BigInt(js.substring(mark, i))) match case Success(g) => Right(g) case Failure(f) => - if cfg.permissivePrimitives && jsChars(i) == '"' then + if (isMapKey || cfg.permissivePrimitives) && jsChars(i) == '"' then for { _ <- expectQuote - result <- expectBigLong(cfg, p) + result <- expectBigLong() _ <- expectQuote } yield result else @@ -128,7 +129,7 @@ case class JsonParser(js: String, cache: Map[TypedName, (JsonConfig, JsonParser) i = mark Left(JsonParseError(showError(msg))) - def expectDouble(cfg: JsonConfig, p: JsonParser): Either[ParseError, Double] = + def expectDouble(isMapKey: Boolean = false): Either[ParseError, Double] = val mark = i var done = false while !done do @@ -138,10 +139,10 @@ case class JsonParser(js: String, cache: Map[TypedName, (JsonConfig, JsonParser) Try(js.substring(mark, i).toDouble) match case Success(g) => Right(g) case Failure(_) => - if cfg.permissivePrimitives && jsChars(i) == '"' then + if (isMapKey || cfg.permissivePrimitives) && jsChars(i) == '"' then for { _ <- expectQuote - result <- expectDouble(cfg, p) + result <- expectDouble() _ <- expectQuote } yield result else @@ -151,7 +152,7 @@ case class JsonParser(js: String, cache: Map[TypedName, (JsonConfig, JsonParser) i = mark Left(JsonParseError(showError(msg))) - def expectBigDouble(cfg: JsonConfig, p: JsonParser): Either[ParseError, BigDecimal] = + def expectBigDouble(isMapKey: Boolean = false): Either[ParseError, BigDecimal] = nullCheck match case true if cfg.forbidNullsInInput => Left(JsonParseError(showError(s"Forbidden 'null' value received at position [$i]"))) case true => Right(null.asInstanceOf[BigDecimal]) @@ -165,10 +166,10 @@ case class JsonParser(js: String, cache: Map[TypedName, (JsonConfig, JsonParser) Try(BigDecimal(js.substring(mark, i))) match case Success(g) => Right(g) case Failure(_) => - if cfg.permissivePrimitives && jsChars(i) == '"' then + if (isMapKey || cfg.permissivePrimitives) && jsChars(i) == '"' then for { _ <- expectQuote - result <- expectBigDouble(cfg, p) + result <- expectBigDouble() _ <- expectQuote } yield result else @@ -178,7 +179,7 @@ case class JsonParser(js: String, cache: Map[TypedName, (JsonConfig, JsonParser) i = mark Left(JsonParseError(showError(msg))) - def expectString(cfg: JsonConfig, p: JsonParser): Either[ParseError, String] = + def expectString(): Either[ParseError, String] = nullCheck match case true if cfg.forbidNullsInInput => Left(JsonParseError(showError(s"Forbidden 'null' value received at position [$i]"))) case true => Right(null.asInstanceOf[String]) @@ -198,7 +199,7 @@ case class JsonParser(js: String, cache: Map[TypedName, (JsonConfig, JsonParser) Right(captured.getOrElse(js.substring(mark, i - 1))) case x => Left(JsonParseError(showError(s"Unexpected character '$x' where beginning of a string expected at position [$i]"))) - def expectList[T](cfg: JsonConfig, expectElement: (JsonConfig, JsonParser) => Either[ParseError, T]): Either[ParseError, List[T]] = + def expectList[T](expectElement: () => Either[ParseError, T]): Either[ParseError, List[T]] = nullCheck match case true if cfg.forbidNullsInInput => Left(JsonParseError(showError(s"Forbidden 'null' value received at position [$i]"))) case true => Right(null.asInstanceOf[List[T]]) @@ -211,7 +212,7 @@ case class JsonParser(js: String, cache: Map[TypedName, (JsonConfig, JsonParser) var done: Option[Either[ParseError, List[T]]] = None while done.isEmpty do (for { - el <- expectElement(cfg, this) + el <- expectElement() _ = acc.addOne(el) _ <- expectComma } yield el) match @@ -225,7 +226,6 @@ case class JsonParser(js: String, cache: Map[TypedName, (JsonConfig, JsonParser) done.get def expectTuple( - cfg: JsonConfig, tupleFns: List[(JsonConfig, JsonParser) => Either[ParseError, ?]] ): Either[ParseError, List[?]] = if jsChars(i) != '[' then Left(JsonParseError(showError(s"Beginning of tuple expected at position [$i]"))) @@ -252,7 +252,6 @@ case class JsonParser(js: String, cache: Map[TypedName, (JsonConfig, JsonParser) case Right(_) => Left(JsonParseError(showError(s"Extra/unexpected tuple fields at position [$i]"))) def expectObject[K, V]( - cfg: JsonConfig, keyElement: (JsonConfig, JsonParser) => Either[ParseError, K], valueElement: (JsonConfig, JsonParser) => Either[ParseError, V] ): Either[ParseError, Map[K, V]] = @@ -281,7 +280,6 @@ case class JsonParser(js: String, cache: Map[TypedName, (JsonConfig, JsonParser) // Special case of JSON object where each entry is a field of a class def expectClass( - cfg: JsonConfig, fieldMap: Map[String, (JsonConfig, JsonParser) => Either[ParseError, ?]], fieldValues: scala.collection.mutable.HashMap[String, Any] // pre-set values (Option:None, default values) ): Either[ParseError, Map[String, ?]] = @@ -308,7 +306,7 @@ case class JsonParser(js: String, cache: Map[TypedName, (JsonConfig, JsonParser) done.get // Slower String parsing that handles special escaped chars - def _expectString = + private def _expectString = val builder = new java.lang.StringBuilder() while i < max && jsChars(i) != '"' do if jsChars(i) == '\\' then { @@ -365,5 +363,5 @@ case class JsonParser(js: String, cache: Map[TypedName, (JsonConfig, JsonParser) ("..." + js.substring(i - 49), 52) case ep => ("..." + js.substring(ep - 49, ep + 27) + "...", 52) } - msg + "\n" + clip.replaceAll("[\n\t]", "~") + "\n" + ("-" * dashes) + "^" + msg + s" at positio [$i]" + "\n" + clip.replaceAll("[\n\t]", "~") + "\n" + ("-" * dashes) + "^" } diff --git a/src/main/scala/co.blocke.scalajack/json/JsonParser2.scala b/src/main/scala/co.blocke.scalajack/json/JsonParser2.scala new file mode 100644 index 00000000..2922ab5d --- /dev/null +++ b/src/main/scala/co.blocke.scalajack/json/JsonParser2.scala @@ -0,0 +1,219 @@ +package co.blocke.scalajack +package json + +import parser.* + +import scala.util.* +import co.blocke.scala_reflection.TypedName +import scala.collection.mutable.{ListBuffer, HashMap} + +case class JsonParser2(js: String, cfg: JsonConfig) extends Parser: + + //-------------------------------------- + //-------------------------------------- + // JSON Housekeeping + //-------------------------------------- + //-------------------------------------- + + private val jsChars: Array[Char] = js.toCharArray + private var i = 0 + private val max: Int = jsChars.length + + inline def eatWhitespace = + while i < max && jsChars(i).isWhitespace do i += 1 + + inline def nullCheck: Boolean = + if i + 3 < max && jsChars(i) == 'n' && jsChars(i + 1) == 'u' && jsChars(i + 2) == 'l' && jsChars(i + 3) == 'l' then + i += 4 + true + else + false + + inline def expectComma: Either[ParseError, Unit] = // Note: this consumes whitespace before/after the ',' + eatWhitespace + if jsChars(i) == ',' then + i += 1 + eatWhitespace + Right(()) + else + Left(CommaExpected(showError(s"Comma expected at position [$i]"))) + + inline def expectColon: Either[ParseError, Unit] = // Note: this consumes whitespace before/after the ':' + eatWhitespace + if jsChars(i) == ':' then + i += 1 + eatWhitespace + Right(()) + else + Left(JsonParseError(showError(s"Expected colon at position [$i]"))) + + // JSON label (i.e. object key, which is always a simple string, ie no escaped/special chars) + inline def expectLabel: Either[ParseError, String] = + jsChars(i) match + case '"' => + i += 1 + val mark = i + while i < max && jsChars(i) != '"' do i += 1 + i += 1 + Right(js.substring(mark, i - 1)) + case x => Left(JsonParseError(showError(s"Unexpected character '$x' where beginning of label expected at position [$i]"))) + + //-------------------------------------- + //-------------------------------------- + // Parser Implementation + //-------------------------------------- + //-------------------------------------- + inline def parseBoolean(): Either[ParseError, Boolean] = + jsChars(i) match + case 't' if i + 3 < max && jsChars(i + 1) == 'r' && jsChars(i + 2) == 'u' && jsChars(i + 3) == 'e' => + i += 4 + Right(true) + case 'f' if i + 4 < max && jsChars(i + 1) == 'a' && jsChars(i + 2) == 'l' && jsChars(i + 3) == 's' && jsChars(i + 4) == 'e' => + i += 5 + Right(false) + case x => + // if (isMapKey || cfg.permissivePrimitives) && jsChars(i) == '"' then + // for { + // _ <- expectQuote + // result <- expectBoolean() + // _ <- expectQuote + // } yield result + // else + Left(JsonParseError(showError(s"Unexpected character '$x' where beginning of boolean value expected at position [$i]"))) + + + inline def parseString(): Either[ParseError, String] = + nullCheck match + case true if cfg.forbidNullsInInput => Left(JsonParseError(showError(s"Forbidden 'null' value received at position [$i]"))) + case true => Right(null.asInstanceOf[String]) + case false => + jsChars(i) match + case '"' => + i += 1 + val mark = i // save position in case we need complex string parse + var captured: Option[String] = None + while i < max && jsChars(i) != '"' do + jsChars(i) match + case '\\' => // oops! special char found--do slow string parse + i = mark + captured = Some(_expectString) + case _ => i += 1 + i += 1 + Right(captured.getOrElse(js.substring(mark, i - 1))) + case x => Left(JsonParseError(showError(s"Unexpected character '$x' where beginning of a string expected at position [$i]"))) + + inline def parseLong(): Either[ParseError, Long] = + val mark = i + var done = false + while !done do + jsChars(i) match + case c if (c >= '0' && c <= '9') || c == '-' => i += 1 + case _ => done = true + Try(js.substring(mark, i).toLong) match + case Success(g) => Right(g) + case Failure(f) => + // if (isMapKey || cfg.permissivePrimitives) && jsChars(i) == '"' then + // for { + // _ <- expectQuote + // result <- expectLong() + // _ <- expectQuote + // } yield result + // else + val msg = + if mark == i then s"""Int/Long expected but couldn't parse from "${jsChars(i)}" at position [$i]""" + else s"""Int/Long expected but couldn't parse from "${js.substring(mark, i)}" at position [$i]""" + i = mark + Left(JsonParseError(showError(msg))) + + def parseList(inst: Instruction): Either[ParseError, List[inst.Z]] = + nullCheck match + case true if cfg.forbidNullsInInput => Left(JsonParseError(showError(s"Forbidden 'null' value received at position [$i]"))) + case true => Right(null) + case false => + if jsChars(i) != '[' then Left(JsonParseError(showError(s"Beginning of list expected at position [$i]"))) + else + i += 1 + eatWhitespace + val acc = ListBuffer.empty[inst.Z] + var done: Option[Either[ParseError, List[inst.Z]]] = None + while done.isEmpty do + (for { + el <- parse(inst) + _ = acc.addOne(el) + _ <- expectComma + } yield el) match + case Left(CommaExpected(_)) if jsChars(i) == ']' => + i += 1 + eatWhitespace + done = Some(Right(acc.toList)) + case Left(e) => + done = Some(Left(e)) + case Right(_) => + done.get + + // Special case of JSON object where each entry is a field of a class + def parseClass(inst: Map[String,Instruction], fieldValues: HashMap[String,Any]): Either[ParseError, Map[String,Any]] = + if jsChars(i) != '{' then Left(JsonParseError(showError(s"Beginning of class object expected at position [$i]"))) + else + i += 1 + eatWhitespace + var done: Option[Either[ParseError, Map[String, ?]]] = None + while done.isEmpty do + (for { + fieldLabel <- expectLabel + _ <- expectColon + fieldValue <- inst.get(fieldLabel).map(parse(_)).getOrElse(null) + _ = fieldValues.put(fieldLabel, fieldValue) + _ <- expectComma + } yield fieldValue) match + case Left(CommaExpected(_)) if jsChars(i) == '}' => + i += 1 + eatWhitespace + done = Some(Right(fieldValues.toMap)) + case Left(e) => + done = Some(Left(e)) + case Right(_) => + done.get + + //-------------------------------------- + //-------------------------------------- + // Utility Functions + //-------------------------------------- + //-------------------------------------- + + // Slower String parsing that handles special escaped chars + private def _expectString = + val builder = new java.lang.StringBuilder() + while i < max && jsChars(i) != '"' do + if jsChars(i) == '\\' then { + jsChars(i + 1) match { + case c @ ('"' | '\\' | 'b' | 'f' | 'n' | 'r' | 't') => + builder.append(c) + i += 2 + + case 'u' => + val hexEncoded = js.substring(i + 2, i + 6) + val unicodeChar = Integer.parseInt(hexEncoded, 16).toChar + builder.append(unicodeChar.toString) + i += 6 + + case c => + builder.append(c) + i += 2 + } + } else { + builder.append(jsChars(i)) + i += 1 + } + builder.toString + + def showError(msg: String): String = { + val (clip, dashes) = i match { + case ep if ep <= 50 && max < 80 => (js, ep) + case ep if ep <= 50 => (js.substring(0, 77) + "...", ep) + case ep if ep > 50 && ep + 30 >= max => + ("..." + js.substring(i - 49), 52) + case ep => ("..." + js.substring(ep - 49, ep + 27) + "...", 52) + } + msg + s" at position [$i]" + "\n" + clip.replaceAll("[\n\t]", "~") + "\n" + ("-" * dashes) + "^" + } diff --git a/src/main/scala/co.blocke.scalajack/json/JsonParser3.scala b/src/main/scala/co.blocke.scalajack/json/JsonParser3.scala new file mode 100644 index 00000000..260f800b --- /dev/null +++ b/src/main/scala/co.blocke.scalajack/json/JsonParser3.scala @@ -0,0 +1,188 @@ +package co.blocke.scalajack +package json + +import scala.util.{Try, Success} +import scala.annotation._ + +case class JsonParser3( js: String ): + + trait Terminal + sealed trait JSON + + case class StringJson( b: Int, e: Int ) extends JSON: + def get: String = js.substring(b,e) + case class BooleanJson( v: Boolean ) extends JSON + case class IntJson( v: Long ) extends JSON + case class FloatJson( v: Double ) extends JSON + case class ObjectJson( v: Map[String, JSON] ) extends JSON + case class ArrayJson( v: List[JSON] ) extends JSON + case class NullJson() extends JSON + + case class TerminateArrayJson() extends JSON with Terminal // marker for end of Array/Object + case class TerminateObjectJson() extends JSON with Terminal // marker for end of Array/Object + + private val jsChars: Array[Char] = js.toCharArray + private var i = 0 + private val max: Int = jsChars.length + + def reset() = i = 0 + + def parse(expectComma: Boolean = false): JSON = + var result: Option[JSON] = None + val commaFound: Option[JSON] = + if expectComma then + var found = if i == max && (jsChars(i-1)==']' || jsChars(i-1)=='}') then true else false + var done = false + while i < max && !done do + (jsChars(i): @switch) match + case ' ' => + i += 1 + case '\n' => + i += 1 + case '\t' => + i += 1 + case '\r' => + i += 1 + case ',' => + i += 1 + found = true + done = true + case ':' => // cheat! Treat ',' and ':' the same, so {"foo","bar","a","b"} == {"foo":"bar","a":"b"} + i += 1 + found = true + done = true + case ']' => + i += 1 + found = true + done = true + case '}' => + i += 1 + found = true + done = true + case c => + done = true + found match + case true if jsChars(i-1) == '}' => Some(TerminateObjectJson()) + case true if jsChars(i-1) == ']' => Some(TerminateArrayJson()) + case true => None // comma found... continue parsing + case _ => throw new JsonParseError("Unterminated array or object") + else + None // no comma involvement--continue parsing + + commaFound.getOrElse{ + while i < max && result.isEmpty do { + (jsChars(i): @switch) match + case '"' => + i += 1 + result = Some(parseString()) + case 't' | 'f' => + result = Some(parseBoolean()) + case 'n' => + result = Some(parseNull()) + case ' ' => + i += 1 + case '\n' => + i += 1 + case '\t' => + i += 1 + case '\r' => + i += 1 + case ']' => + i += 1 + result = Some(TerminateArrayJson()) + case '}' => + i += 1 + result = Some(TerminateObjectJson()) + case '[' => + val acc = scala.collection.mutable.ListBuffer.empty[JSON] + i += 1 + var last: JSON = parse() + while !last.isInstanceOf[TerminateArrayJson] do + acc.addOne(last) + last = parse(true) + result = Some(ArrayJson(acc.toList)) + case '{' => + val acc = scala.collection.mutable.Map.empty[String,JSON] + i += 1 + var done = false + var key: JSON = parse() + while !done do + key match + case k if k.isInstanceOf[StringJson] => + // while i < max && !key.isInstanceOf[TerminateObjectJson] do + val value: JSON = parse(true) + if value.isInstanceOf[Terminal] then + throw new JsonParseError("Malformed object") + acc.put(k.asInstanceOf[StringJson].get, value) + key = parse(true) + case k if k.isInstanceOf[TerminateObjectJson] => + result = Some(ObjectJson(acc.toMap)) + done = true + case _ => + throw new JsonParseError("Malformed object (invalid key value)") + if i == max && key.isInstanceOf[TerminateObjectJson] then result = Some(ObjectJson(acc.toMap)) + case c if (c >= '0' && c <= '9') || c == '-' || c == '.' || c == '+' => + result = Some(parseNumber()) + } + result.getOrElse(throw new JsonParseError("Unterminated array or object")) + } + + inline def parseNull() = + if i + 3 < max && jsChars(i) == 'n' && jsChars(i + 1) == 'u' && jsChars(i + 2) == 'l' && jsChars(i + 3) == 'l' then + i += 4 + NullJson() + else + throw new JsonParseError(s"Unexpected character '${jsChars(i)}'") + + inline def parseBoolean() = + jsChars(i) match + case 't' if i + 3 < max && jsChars(i + 1) == 'r' && jsChars(i + 2) == 'u' && jsChars(i + 3) == 'e' => + i += 4 + BooleanJson(true) + case 'f' if i + 4 < max && jsChars(i + 1) == 'a' && jsChars(i + 2) == 'l' && jsChars(i + 3) == 's' && jsChars(i + 4) == 'e' => + i += 5 + BooleanJson(false) + case _ => + throw new JsonParseError(s"Unexpected character '${jsChars(i)}'") + + inline def parseString() = + val mark = i + while i < max && jsChars(i) != '"' do + if jsChars(i) == '\\' then // skip escaped special characters + i += 1 + i += 1 + if i == max then + i = mark + throw new JsonParseError(s"Unterminated string starting") + i += 1 + StringJson(mark, i-1) + + inline def parseNumber() = + var isFloat = false + var done = false + val mark = i + while i < max && !done do + jsChars(i) match + case c if (c >= '0' && c <= '9') => + i += 1 + case '.' => + isFloat = true + i += 1 + case 'e' | 'E' | '-' | '+' => + i += 1 + case _ => + done = true + if isFloat then + Try(js.substring(mark,i).toDouble) match + case Success(v) => + FloatJson(v) + case _ => + i = mark + throw new JsonParseError(s"Cannot parse number starting") + else + Try(js.substring(mark,i).toLong) match + case Success(v) => + IntJson(v) + case _ => + i = mark + throw new JsonParseError(s"Cannot parse number starting") diff --git a/src/main/scala/co.blocke.scalajack/json/JsonReader.scala b/src/main/scala/co.blocke.scalajack/json/JsonReader.scala index 2b392b1b..cf569c9e 100644 --- a/src/main/scala/co.blocke.scalajack/json/JsonReader.scala +++ b/src/main/scala/co.blocke.scalajack/json/JsonReader.scala @@ -2,45 +2,44 @@ package co.blocke.scalajack package json import co.blocke.scala_reflection.{RTypeRef, TypedName} +import co.blocke.scala_reflection.reflect.rtypeRefs.* +import parser.* import scala.quoted.* import scala.collection.mutable.HashMap import scala.util.{Failure, Success, Try} - -case class JsonReader() extends ReaderModule: - - val root: ReaderModule = null // Should never be accessed--we're the root! - - // Did the user supply an extension module? - val extension = Try(Class.forName("co.blocke.scalajack.json.ReaderExtension")) match - case Success(c) => Some(c.getDeclaredConstructor().newInstance.asInstanceOf[ReaderModule]) - case Failure(_) => None - - val modules = readers.PrimitiveReader( - readers.ColletionReader( - readers.ClassReader( - readers.EnumReader( - readers.MiscReader( - TerminusReaderModule(extension, root), - root - ), - root - ), - this - ), - this - ), - this - ) - - def readerFn[T](ref: RTypeRef[T], isMapKey: Boolean = false)(using q: Quotes, tt: Type[T])(using cache: HashMap[Expr[TypedName], Expr[(JsonConfig, JsonParser) => Either[ParseError, ?]]]): Expr[(JsonConfig, JsonParser) => Either[ParseError, T]] = - modules.readerFn[T](ref) - - // TODO: - // * Java Enums - // * Java Classes - // * Non-case Scala classes - // * SealedTraitRef - // * TraitRef - // * Primitive: Any - - // ----------------------------------- +import scala.collection.Factory + +object JsonReader: + + def refRead[T]( + ref: RTypeRef[T] + )(using q: Quotes, tt: Type[T]): Expr[Instruction] = + import quotes.reflect.* + + ref match + case r: PrimitiveRef[?] if r.family == PrimFamily.Boolish => '{ BooleanInstruction() } + case r: PrimitiveRef[?] if r.family == PrimFamily.Stringish => '{ StringInstruction() } + case r: PrimitiveRef[?] if r.family == PrimFamily.Longish => '{ IntInstruction() } + + case r: SeqRef[?] => + r.elementRef.refType match + case '[e] => + r.refType match + case '[t] => + val elementInstruction = refRead[e](r.elementRef.asInstanceOf[RTypeRef[e]]) + '{ SeqInstruction[e, t]($elementInstruction)(using ${ Expr.summon[Factory[e, t]].get }) } + + case r: ScalaClassRef[?] => + r.refType match + case '[t] => + val fieldInstructions = Expr.ofList( + r.fields + .map { f => + f.fieldRef.refType match + case '[e] => + (Expr(f.name), refRead[e](f.fieldRef.asInstanceOf[RTypeRef[e]])) + } + .map(u => Expr.ofTuple(u)) + ) + val instantiator = JsonReaderUtil.classInstantiator[t](r.asInstanceOf[ClassRef[t]]) + '{ ScalaClassInstruction[t]($fieldInstructions.toMap, $instantiator) } diff --git a/src/main/scala/co.blocke.scalajack/json/JsonReader.scalax b/src/main/scala/co.blocke.scalajack/json/JsonReader.scalax new file mode 100644 index 00000000..f9c395c8 --- /dev/null +++ b/src/main/scala/co.blocke.scalajack/json/JsonReader.scalax @@ -0,0 +1,49 @@ +package co.blocke.scalajack +package json + +import co.blocke.scala_reflection.{RTypeRef, TypedName} +import scala.quoted.* +import scala.collection.mutable.HashMap +import scala.util.{Failure, Success, Try} + +trait JsonReader[B, T]: + def apply(in: B): T + +/* +case class JsonReader() extends ReaderModule: + + val root: ReaderModule = this // Should never be accessed--we're the root! + + // Did the user supply an extension module? + val extension = Try(Class.forName("co.blocke.scalajack.json.ReaderExtension")) match + case Success(c) => Some(c.getDeclaredConstructor().newInstance.asInstanceOf[ReaderModule]) + case Failure(_) => None + + val next: Option[ReaderModule] = Some( + readers.PrimitiveReader( + Some(readers.CollectionReader(Some(TerminusReaderModule(extension, root)), root)), + // readers.ColletionReader( + // readers.ClassReader( + // readers.EnumReader( + // readers.MiscReader( + // TerminusReaderModule(extension, root), + // root + // ), + // root + // ), + // this + // ), + // this + // ), + this + ) + ) + + def refRead[T]( + ref: RTypeRef[T], + parserE: Expr[JsonParser], + cfgE: Expr[JsonConfig], + isMapKey: Boolean = false + )(using q: Quotes, tt: Type[T])(using cache: HashMap[Expr[TypedName], Expr[(JsonConfig, JsonParser) => Either[ParseError, ?]]]): Expr[Either[ParseError, T]] = + next.get.refRead(ref, parserE, cfgE, isMapKey) + */ diff --git a/src/main/scala/co.blocke.scalajack/json/JsonReaderUtil.scala b/src/main/scala/co.blocke.scalajack/json/JsonReaderUtil.scala index 195fd384..1b52e355 100644 --- a/src/main/scala/co.blocke.scalajack/json/JsonReaderUtil.scala +++ b/src/main/scala/co.blocke.scalajack/json/JsonReaderUtil.scala @@ -24,6 +24,7 @@ object JsonReaderUtil: } } + /* def tupleInstantiator[T: Type](ref: TupleRef[T])(using Quotes): Expr[List[?] => T] = import quotes.reflect.* val sym = TypeRepr.of[T].classSymbol.get @@ -49,22 +50,23 @@ object JsonReaderUtil: tree.asExpr.asExprOf[T] } } + */ - def classParseMap[T: Type](ref: ClassRef[T], root: ReaderModule)(using q: Quotes)(using - cache: scala.collection.mutable.HashMap[Expr[TypedName], Expr[(JsonConfig, JsonParser) => Either[ParseError, ?]]] - ): Expr[JsonParser => Map[String, (JsonConfig, JsonParser) => Either[ParseError, ?]]] = - import Clazzes.* - '{ (parser: JsonParser) => - val daList = ${ - val fieldList = ref.fields.map(f => - f.fieldRef.refType match - case '[m] => - val fn = root.readerFn[m](f.fieldRef.asInstanceOf[RTypeRef[m]]) - '{ - ${ Expr(f.name) } -> $fn - } - ) - Expr.ofList(fieldList) - } - daList.toMap - } + // def classParseMap[T: Type](ref: ClassRef[T], root: ReaderModule)(using q: Quotes)(using + // cache: scala.collection.mutable.HashMap[Expr[TypedName], Expr[(JsonConfig, JsonParser) => Either[ParseError, ?]]] + // ): Expr[JsonParser => Map[String, (JsonConfig, JsonParser) => Either[ParseError, ?]]] = + // import Clazzes.* + // '{ (parser: JsonParser) => + // val daList = ${ + // val fieldList = ref.fields.map(f => + // f.fieldRef.refType match + // case '[m] => + // val fn = root.readerFn[m](f.fieldRef.asInstanceOf[RTypeRef[m]]) + // '{ + // ${ Expr(f.name) } -> $fn + // } + // ) + // Expr.ofList(fieldList) + // } + // daList.toMap + // } diff --git a/src/main/scala/co.blocke.scalajack/json/ParseInstructions.scalax b/src/main/scala/co.blocke.scalajack/json/ParseInstructions.scalax new file mode 100644 index 00000000..3051b812 --- /dev/null +++ b/src/main/scala/co.blocke.scalajack/json/ParseInstructions.scalax @@ -0,0 +1,11 @@ +package co.blocke.scalajack +package json + + +sealed trait ParseInstruction + +// A JsonReader converts the JSON type to final Scala/Java type +case class ParseLong[T]( reader: JsonReader[Long,T] ) extends ParseInstruction +case class ParseString[T]( reader: JsonReader[String,T] ) extends ParseInstruction +case class ParseList[T]( reader: JsonReader[List[T],Seq[T]] ) extends ParseInstruction +case class ParseObject[T]( fields: Map[String, JsonReader[Map[String,?],T]] ) extends ParseInstruction \ No newline at end of file diff --git a/src/main/scala/co.blocke.scalajack/json/ReaderModule.scala b/src/main/scala/co.blocke.scalajack/json/ReaderModule.scalax similarity index 100% rename from src/main/scala/co.blocke.scalajack/json/ReaderModule.scala rename to src/main/scala/co.blocke.scalajack/json/ReaderModule.scalax diff --git a/src/main/scala/co.blocke.scalajack/json/ReaderModule2.scalax b/src/main/scala/co.blocke.scalajack/json/ReaderModule2.scalax new file mode 100644 index 00000000..172f6e9e --- /dev/null +++ b/src/main/scala/co.blocke.scalajack/json/ReaderModule2.scalax @@ -0,0 +1,35 @@ +package co.blocke.scalajack +package json + +import co.blocke.scala_reflection.{RTypeRef, TypedName} +import scala.quoted.* +import scala.collection.mutable.HashMap +import scala.util.{Failure, Success, Try} + +trait ReaderModule: + val next: Option[ReaderModule] + val root: ReaderModule + + def refRead[T]( + ref: RTypeRef[T], + parserE: Expr[JsonParser], + cfgE: Expr[JsonConfig], + isMapKey: Boolean = false + )(using q: Quotes, tt: Type[T])(using cache: HashMap[Expr[TypedName], Expr[(JsonConfig, JsonParser) => Either[ParseError, ?]]]): Expr[Either[ParseError, T]] + +case class TerminusReaderModule(next: Option[ReaderModule], root: ReaderModule) extends ReaderModule: + + def refRead[T]( + ref: RTypeRef[T], + parserE: Expr[JsonParser], + cfgE: Expr[JsonConfig], + isMapKey: Boolean = false + )(using q: Quotes, tt: Type[T])(using cache: HashMap[Expr[TypedName], Expr[(JsonConfig, JsonParser) => Either[ParseError, ?]]]): Expr[Either[ParseError, T]] = + + val name = Expr(ref.name) + next match + case None => '{ Left(JsonParseError("Unknown (or unsupported) type " + $name)) } + case Some(ext) => + Try(ext.refRead[T](ref, parserE, cfgE, isMapKey)) match + case Success(v) => v + case Failure(_) => '{ Left(JsonParseError("Unknown (or unsupported) type " + $name)) } diff --git a/src/main/scala/co.blocke.scalajack/json/readers/ClassReader.scala b/src/main/scala/co.blocke.scalajack/json/readers/ClassReader.scalax similarity index 100% rename from src/main/scala/co.blocke.scalajack/json/readers/ClassReader.scala rename to src/main/scala/co.blocke.scalajack/json/readers/ClassReader.scalax diff --git a/src/main/scala/co.blocke.scalajack/json/readers/CollectionReader.scala b/src/main/scala/co.blocke.scalajack/json/readers/CollectionReader.scalax similarity index 74% rename from src/main/scala/co.blocke.scalajack/json/readers/CollectionReader.scala rename to src/main/scala/co.blocke.scalajack/json/readers/CollectionReader.scalax index 3831e4fe..ef5322b3 100644 --- a/src/main/scala/co.blocke.scalajack/json/readers/CollectionReader.scala +++ b/src/main/scala/co.blocke.scalajack/json/readers/CollectionReader.scalax @@ -14,23 +14,29 @@ import scala.jdk.CollectionConverters.* import scala.collection.mutable.HashMap import scala.util.{Failure, Success, Try} -case class ColletionReader(next: ReaderModule, root: ReaderModule) extends ReaderModule: +case class CollectionReader(next: Option[ReaderModule], root: ReaderModule) extends ReaderModule: - def readerFn[T](ref: RTypeRef[T], isMapKey: Boolean = false)(using q: Quotes, tt: Type[T])(using cache: HashMap[Expr[TypedName], Expr[(JsonConfig, JsonParser) => Either[ParseError, ?]]]): Expr[(JsonConfig, JsonParser) => Either[ParseError, T]] = + def refRead[T]( + ref: RTypeRef[T], + parserE: Expr[JsonParser], + cfgE: Expr[JsonConfig], + isMapKey: Boolean = false + )(using q: Quotes, tt: Type[T])(using cache: HashMap[Expr[TypedName], Expr[(JsonConfig, JsonParser) => Either[ParseError, ?]]]): Expr[Either[ParseError, T]] = import quotes.reflect.* ref match case t: SeqRef[T] => - if isMapKey then throw new JsonError("Seq types cannot be map keys.") - t.refType match - case '[s] => - t.elementRef.refType match - case '[e] => - val subFn = root.readerFn[e](t.elementRef.asInstanceOf[RTypeRef[e]]).asInstanceOf[Expr[(JsonConfig, JsonParser) => Either[ParseError, e]]] - '{ (j: JsonConfig, p: JsonParser) => - p.expectList[e](j, $subFn).map(_.to(${ Expr.summon[Factory[e, T]].get })) // Convert List to whatever the target type should be - } + if isMapKey then '{ Left(new JsonError("Seq types cannot be map keys.")) } + t.elementRef.refType match + case '[e] => + val fn = () => root.refRead[e](t.elementRef.asInstanceOf[RTypeRef[e]], parserE, cfgE) + '{ + $parserE + .expectList[e]($fn) + .map(_.to(${ Expr.summon[Factory[e, T]].get })) // Convert List to whatever the target type should be + } + /* case t: MapRef[T] => if isMapKey then throw new JsonError("Map types cannot be map keys.") t.refType match @@ -99,6 +105,26 @@ case class ColletionReader(next: ReaderModule, root: ReaderModule) extends Reade case Failure(e) => Left(JsonParseError(p.showError(s"Could not instantiate a $cname, with error: " + e))) ) } + */ case t => - next.readerFn[T](t) + next.get.refRead(ref, parserE, cfgE, isMapKey) + + + + /* + + 1. At compile-time, use reflected RTypes to build a set of pre-baked instructions for the parser... an entire set of commands to parse the given object + 2. Pass this command set to the parser at runtime for linear execution + + + Parson(name: String, age: Int, stuff: List[String]) + + ParseObject + fields: + name: ParseString + age: ParseInt + stuff: ParseList + ParseString + + */ \ No newline at end of file diff --git a/src/main/scala/co.blocke.scalajack/json/readers/EnumReader.scala b/src/main/scala/co.blocke.scalajack/json/readers/EnumReader.scalax similarity index 100% rename from src/main/scala/co.blocke.scalajack/json/readers/EnumReader.scala rename to src/main/scala/co.blocke.scalajack/json/readers/EnumReader.scalax diff --git a/src/main/scala/co.blocke.scalajack/json/readers/MiscReader.scala b/src/main/scala/co.blocke.scalajack/json/readers/MiscReader.scalax similarity index 100% rename from src/main/scala/co.blocke.scalajack/json/readers/MiscReader.scala rename to src/main/scala/co.blocke.scalajack/json/readers/MiscReader.scalax diff --git a/src/main/scala/co.blocke.scalajack/json/readers/PrimitiveReader.scala b/src/main/scala/co.blocke.scalajack/json/readers/PrimitiveReader.scalax similarity index 61% rename from src/main/scala/co.blocke.scalajack/json/readers/PrimitiveReader.scala rename to src/main/scala/co.blocke.scalajack/json/readers/PrimitiveReader.scalax index d290ac89..40f733ad 100644 --- a/src/main/scala/co.blocke.scalajack/json/readers/PrimitiveReader.scala +++ b/src/main/scala/co.blocke.scalajack/json/readers/PrimitiveReader.scalax @@ -14,130 +14,104 @@ import scala.jdk.CollectionConverters.* import scala.collection.mutable.HashMap import scala.util.{Failure, Success, Try} -case class PrimitiveReader(next: ReaderModule, root: ReaderModule) extends ReaderModule: - - def readerFn[T](ref: RTypeRef[T], isMapKey: Boolean = false)(using q: Quotes, tt: Type[T])(using cache: HashMap[Expr[TypedName], Expr[(JsonConfig, JsonParser) => Either[ParseError, ?]]]): Expr[(JsonConfig, JsonParser) => Either[ParseError, T]] = - import Clazzes.* +case class IntReader() extends JsonReader[Long, Int]: + def apply(in: Long) = in.toInt + +case class StringReader() extends JsonReader[String, String]: + def apply(in: String) = in + +// Magic line to convert a List[T] to some U <: Seq[?], eg zconvert[Int,Set[?]] +// can use this if I have element type T, and target collection U, which the SeqRef has! +// This line should be executed at runtime. +// +// inline def zconvert[T,U](in:List[T])(using Factory[T, U]) = in.to( summon[Factory[T,U]] ) +// +// case class SeqReader[B, T](toObj: B) extends JsonReader[List[T], Seq[T]]: +// def apply(in: List[T]) = in.to(toObj) +// def apply[s](in: List[T]) = in.to(${ Expr.summon[Factory[T, s]].get }) + +/* +case class PrimitiveReader(next: Option[ReaderModule], root: ReaderModule) extends ReaderModule: + + def refRead[T]( + ref: RTypeRef[T], + parserE: Expr[JsonParser], + cfgE: Expr[JsonConfig], + isMapKey: Boolean = false + )(using q: Quotes, tt: Type[T])(using cache: HashMap[Expr[TypedName], Expr[(JsonConfig, JsonParser) => Either[ParseError, ?]]]): Expr[Either[ParseError, T]] = import quotes.reflect.* + import Clazzes.* ref match - // // Scala Primitives // case t: PrimitiveRef[T] if t.name == BIG_DECIMAL_CLASS => - if isMapKey then - '{ (j: JsonConfig, p: JsonParser) => - (for { - _ <- p.expectQuote - v <- p.expectBigDouble(j, p).map(_.asInstanceOf[T]) - _ <- p.expectQuote - } yield v) - } - else '{ (j: JsonConfig, p: JsonParser) => p.expectBigDouble(j, p).map(_.asInstanceOf[T]) } + val isMapKeyE = Expr(isMapKey) + '{ $parserE.expectBigDouble($isMapKeyE).map(_.asInstanceOf[T]) } case t: PrimitiveRef[T] if t.name == BIG_INT_CLASS => - if isMapKey then - '{ (j: JsonConfig, p: JsonParser) => - (for { - _ <- p.expectQuote - v <- p.expectBigLong(j, p).map(_.asInstanceOf[T]) - _ <- p.expectQuote - } yield v) - } - else '{ (j: JsonConfig, p: JsonParser) => p.expectBigLong(j, p).map(_.asInstanceOf[T]) } + val isMapKeyE = Expr(isMapKey) + '{ $parserE.expectBigLong($isMapKeyE).map(_.asInstanceOf[T]) } case t: PrimitiveRef[T] if t.name == BOOLEAN_CLASS => - if isMapKey then - '{ (j: JsonConfig, p: JsonParser) => - (for { - _ <- p.expectQuote - v <- p.expectBoolean(j, p).map(_.asInstanceOf[T]) - _ <- p.expectQuote - } yield v) - } - else '{ (j: JsonConfig, p: JsonParser) => p.expectBoolean(j, p).map(_.asInstanceOf[T]) } + val isMapKeyE = Expr(isMapKey) + '{ $parserE.expectBoolean($isMapKeyE).map(_.asInstanceOf[T]) } case t: PrimitiveRef[T] if t.name == BYTE_CLASS => - if isMapKey then - '{ (j: JsonConfig, p: JsonParser) => - (for { - _ <- p.expectQuote - v <- p.expectLong(j, p).map(_.toByte.asInstanceOf[T]) - _ <- p.expectQuote - } yield v) - } - else '{ (j: JsonConfig, p: JsonParser) => p.expectLong(j, p).map(_.toByte.asInstanceOf[T]) } + val isMapKeyE = Expr(isMapKey) + '{ $parserE.expectLong($isMapKeyE).map(_.toByte.asInstanceOf[T]) } case t: PrimitiveRef[T] if t.name == CHAR_CLASS => - '{ (j: JsonConfig, p: JsonParser) => - p.expectString(j, p) + '{ + $parserE + .expectString() .flatMap(s => - if s == null then Left(JsonParseError(p.showError(s"Char typed values cannot be null at position [${p.getPos}]"))) + if s == null then Left(JsonParseError($parserE.showError(s"Char typed values cannot be null"))) else s.toArray.headOption match case Some(c) => Right(c.asInstanceOf[T]) - case None => Left(JsonParseError(p.showError(s"Cannot convert value '$s' into a Char at position [${p.getPos}]"))) + case None => Left(JsonParseError($parserE.showError(s"Cannot convert value '$s' into a Char"))) ) } case t: PrimitiveRef[T] if t.name == DOUBLE_CLASS => - if isMapKey then - '{ (j: JsonConfig, p: JsonParser) => - (for { - _ <- p.expectQuote - v <- p.expectDouble(j, p).map(_.asInstanceOf[T]) - _ <- p.expectQuote - } yield v) - } - else '{ (j: JsonConfig, p: JsonParser) => p.expectDouble(j, p).map(_.asInstanceOf[T]) } + val isMapKeyE = Expr(isMapKey) + '{ $parserE.expectDouble($isMapKeyE).map(_.asInstanceOf[T]) } case t: PrimitiveRef[T] if t.name == FLOAT_CLASS => - if isMapKey then - '{ (j: JsonConfig, p: JsonParser) => - (for { - _ <- p.expectQuote - v <- p.expectDouble(j, p).map(_.toFloat.asInstanceOf[T]) - _ <- p.expectQuote - } yield v) - } - else '{ (j: JsonConfig, p: JsonParser) => p.expectDouble(j, p).map(_.toFloat.asInstanceOf[T]) } + val isMapKeyE = Expr(isMapKey) + '{ $parserE.expectDouble($isMapKeyE).map(_.toFloat.asInstanceOf[T]) } case t: PrimitiveRef[T] if t.name == INT_CLASS => - if isMapKey then - '{ (j: JsonConfig, p: JsonParser) => - (for { - _ <- p.expectQuote - v <- p.expectLong(j, p).map(_.toInt.asInstanceOf[T]) - _ <- p.expectQuote - } yield v) - } - else '{ (j: JsonConfig, p: JsonParser) => p.expectLong(j, p).map(_.toInt.asInstanceOf[T]) } + val isMapKeyE = Expr(isMapKey) + '{ $parserE.expectLong($isMapKeyE).map(_.toInt.asInstanceOf[T]) } case t: PrimitiveRef[T] if t.name == LONG_CLASS => - if isMapKey then - '{ (j: JsonConfig, p: JsonParser) => - (for { - _ <- p.expectQuote - v <- p.expectLong(j, p).map(_.asInstanceOf[T]) - _ <- p.expectQuote - } yield v) - } - else '{ (j: JsonConfig, p: JsonParser) => p.expectLong(j, p).map(_.asInstanceOf[T]) } + val isMapKeyE = Expr(isMapKey) + '{ $parserE.expectLong($isMapKeyE).map(_.asInstanceOf[T]) } case t: PrimitiveRef[T] if t.name == SHORT_CLASS => - if isMapKey then - '{ (j: JsonConfig, p: JsonParser) => - (for { - _ <- p.expectQuote - v <- p.expectLong(j, p).map(_.toShort.asInstanceOf[T]) - _ <- p.expectQuote - } yield v) - } - else '{ (j: JsonConfig, p: JsonParser) => p.expectLong(j, p).map(_.toShort.asInstanceOf[T]) } + val isMapKeyE = Expr(isMapKey) + '{ $parserE.expectLong($isMapKeyE).map(_.toShort.asInstanceOf[T]) } case t: PrimitiveRef[T] if t.name == STRING_CLASS => - '{ (j: JsonConfig, p: JsonParser) => p.expectString(j, p).map(_.asInstanceOf[T]) } + '{ $parserE.expectString().map(_.asInstanceOf[T]) } + + case t => + next.get.refRead(ref, parserE, cfgE, isMapKey) + */ + +/* + def readerFn[T](ref: RTypeRef[T], isMapKey: Boolean = false)(using q: Quotes, tt: Type[T])(using cache: HashMap[Expr[TypedName], Expr[(JsonConfig, JsonParser) => Either[ParseError, ?]]]): Expr[(JsonConfig, JsonParser) => Either[ParseError, T]] = + import Clazzes.* + import quotes.reflect.* + + ref match + + // + // Scala Primitives + // // // Java Primitives @@ -243,3 +217,4 @@ case class PrimitiveReader(next: ReaderModule, root: ReaderModule) extends Reade case t => next.readerFn[T](t) + */ diff --git a/src/main/scala/co.blocke.scalajack/json2/ClassDecoder.scala b/src/main/scala/co.blocke.scalajack/json2/ClassDecoder.scala new file mode 100644 index 00000000..8b8d1618 --- /dev/null +++ b/src/main/scala/co.blocke.scalajack/json2/ClassDecoder.scala @@ -0,0 +1,31 @@ +package co.blocke.scalajack +package json2 + +import json.JsonParseError +import scala.annotation._ + +trait ClassDecoder[A] extends JsonDecoder[A]: + self => + def unsafeDecodeField(in: JsonReader): A + +object ClassDecoder: + def apply[A: scala.reflect.ClassTag](fields: Array[String], fieldDecoders: Array[JsonDecoder[_]]) = new JsonDecoder[A] { + val fieldMatrix = new StringMatrix(fields) + // not ideal--use Scala macros, but for now... + val constructor = summon[scala.reflect.ClassTag[A]].runtimeClass.getConstructors().head + + def unsafeDecode(in: JsonReader): A = + val fieldValues = new Array[Any](fields.length) + JsonParser.charWithWS(in, '{') + if JsonParser.firstField(in) then + var done = false + while !done do + val fieldIdx = JsonParser.parseField(in, fieldMatrix) + if fieldIdx < 0 then JsonParser.skipValue(in) + else + val dec = fieldDecoders(fieldIdx) + fieldValues(fieldIdx) = dec.unsafeDecode(in) + if !JsonParser.nextField(in) then done = true + else throw new JsonParseError("Expected fields!") + constructor.newInstance(fieldValues*).asInstanceOf[A] + } \ No newline at end of file diff --git a/src/main/scala/co.blocke.scalajack/json2/FastStringBuilder.scala b/src/main/scala/co.blocke.scalajack/json2/FastStringBuilder.scala new file mode 100644 index 00000000..7dacfb01 --- /dev/null +++ b/src/main/scala/co.blocke.scalajack/json2/FastStringBuilder.scala @@ -0,0 +1,21 @@ +package co.blocke.scalajack +package json2 + +import java.nio.CharBuffer +import java.util.Arrays + +// like StringBuilder but doesn't have any encoding or range checks +final class FastStringBuilder(initial: Int = 16) { + private[this] var chars: Array[Char] = new Array[Char](initial) + private[this] var i: Int = 0 + + def append(c: Char): FastStringBuilder = { + if (i == chars.length) + chars = Arrays.copyOf(chars, chars.length * 2) + chars(i) = c + i += 1 + this + } + + def buffer: CharSequence = CharBuffer.wrap(chars, 0, i) +} \ No newline at end of file diff --git a/src/main/scala/co.blocke.scalajack/json2/FieldKeyDecoder.scala b/src/main/scala/co.blocke.scalajack/json2/FieldKeyDecoder.scala new file mode 100644 index 00000000..a174b686 --- /dev/null +++ b/src/main/scala/co.blocke.scalajack/json2/FieldKeyDecoder.scala @@ -0,0 +1,50 @@ +package co.blocke.scalajack +package json2 + +import json.JsonParseError + +trait FieldKeyDecoder[+A] { + self => + + final def map[B](f: A => B): FieldKeyDecoder[B] = + new FieldKeyDecoder[B] { + def unsafeDecodeField(in: String): B = f(self.unsafeDecodeField(in)) + } + + final def mapOrFail[B](f: A => Either[String, B]): FieldKeyDecoder[B] = + new FieldKeyDecoder[B] { + def unsafeDecodeField(in: String): B = + f(self.unsafeDecodeField(in)) match { + case Left(err) => throw JsonParseError(err) + case Right(b) => b + } + } + + def unsafeDecodeField(in: String): A +} + +object FieldKeyDecoder { + def apply[A](implicit a: FieldKeyDecoder[A]): FieldKeyDecoder[A] = a + + implicit val string: FieldKeyDecoder[String] = new FieldKeyDecoder[String] { + def unsafeDecodeField(in: String): String = in + } + + implicit val int: FieldKeyDecoder[Int] = + FieldKeyDecoder[String].mapOrFail { str => + try { + Right(str.toInt) + } catch { + case n: NumberFormatException => Left(s"Invalid Int: '$str': $n") + } + } + + implicit val long: FieldKeyDecoder[Long] = + FieldKeyDecoder[String].mapOrFail { str => + try { + Right(str.toLong) + } catch { + case n: NumberFormatException => Left(s"Invalid Long: '$str': $n") + } + } +} \ No newline at end of file diff --git a/src/main/scala/co.blocke.scalajack/json2/JsonDecoder.scala b/src/main/scala/co.blocke.scalajack/json2/JsonDecoder.scala new file mode 100644 index 00000000..05d638e3 --- /dev/null +++ b/src/main/scala/co.blocke.scalajack/json2/JsonDecoder.scala @@ -0,0 +1,81 @@ +package co.blocke.scalajack +package json2 + +import json.JsonParseError +import scala.annotation._ + +trait JsonDecoder[A]: + final def decodeJson(str: CharSequence): Either[JsonParseError, A] = + try Right(unsafeDecode(new JsonReader(str))) + catch { + case jpe: JsonParseError => Left(jpe) + } + + def unsafeDecode(in: JsonReader): A + +//------------------------------------------------------------ + +object JsonDecoder extends DecoderLowPriority1: + + def apply[A](implicit a: JsonDecoder[A]): JsonDecoder[A] = a + + // Primitive support... + implicit val string: JsonDecoder[String] = new JsonDecoder[String] { + def unsafeDecode(in: JsonReader): String = + JsonParser.parseString(in).toString + } + + implicit val boolean: JsonDecoder[Boolean] = new JsonDecoder[Boolean] { + def unsafeDecode(in: JsonReader): Boolean = + JsonParser.parseBoolean(in) + } + + implicit val int: JsonDecoder[Int] = number(JsonParser.parseInt, _.intValueExact()) + private def number[A]( + f: (JsonReader) => A, + fromBigDecimal: java.math.BigDecimal => A + ): JsonDecoder[A] = + new JsonDecoder[A] { + def unsafeDecode(in: JsonReader): A = + (in.readSkipWhitespace(): @switch) match { + case '"' => + val i = f(in) + JsonParser.char(in, '"') + i + case _ => + in.retract() + f(in) + } + } + + private[json2] def builder[A, T[_]]( + in: JsonReader, + builder: scala.collection.mutable.Builder[A, T[A]] + )(implicit A: JsonDecoder[A]): T[A] = { + JsonParser.charWithWS(in, '[') + var i: Int = 0 + if (JsonParser.firstArrayElement(in)) while ({ + { + builder += A.unsafeDecode(in) + i += 1 + }; JsonParser.nextArrayElement(in) + }) () + builder.result() + } + +//------------------------------------------------------------ + +private trait DecoderLowPriority1: + this: JsonDecoder.type => + + // TODO: Experiment with other Seq *NOT* explicitly provided as separate "implicit def"s. See if this will + // convert them to the correct subtype. + implicit def seq[A: JsonDecoder]: JsonDecoder[Seq[A]] = new JsonDecoder[Seq[A]] { + def unsafeDecode(in: JsonReader): Seq[A] = + builder(in, scala.collection.immutable.Seq.newBuilder[A]) + } + + implicit def list[A: JsonDecoder]: JsonDecoder[List[A]] = new JsonDecoder[List[A]] { + def unsafeDecode(in: JsonReader): List[A] = + builder(in, new scala.collection.mutable.ListBuffer[A]) + } diff --git a/src/main/scala/co.blocke.scalajack/json2/JsonParser.scala b/src/main/scala/co.blocke.scalajack/json2/JsonParser.scala new file mode 100644 index 00000000..671b6e4b --- /dev/null +++ b/src/main/scala/co.blocke.scalajack/json2/JsonParser.scala @@ -0,0 +1,164 @@ +package co.blocke.scalajack +package json2 + +import json.JsonParseError +import scala.annotation._ + +object JsonParser: + + private val ull: Array[Char] = "ull".toCharArray + private val alse: Array[Char] = "alse".toCharArray + private val rue: Array[Char] = "rue".toCharArray + + def parseBoolean(gen: JsonReader): Boolean = + (gen.readSkipWhitespace(): @switch) match { + case 't' => + readChars(gen, rue, "true") + true + case 'f' => + readChars(gen, alse, "false") + false + case c => + throw JsonParseError(s"Expected true or false value") + } + + def parseInt(gen: JsonReader): Int = { + checkNumber(gen) + try { + val i = UnsafeNumbers.int_(gen, false) + gen.retract() + i + } catch { + case UnsafeNumbers.UnsafeNumber => throw JsonParseError("Expected an Int") + } + } + + def parseString(gen: JsonReader): CharSequence = + charWithWS(gen, '"') + val sb = new FastStringBuilder(64) + while true do + val c = gen.readEscapedString() + if (c == END_OF_STRING) + return sb.buffer // mutable thing escapes, but cannot be changed + sb.append(c.toChar) + throw JsonParseError("Invalid string value detected") + + // Returns index of field name read in, or -1 if not found + def parseField(gen: JsonReader, fieldMatrix: StringMatrix): Int = + val f = enumeration(gen, fieldMatrix) + charWithWS(gen, ':') + f + + // True if we got anything besides a ], False for ] + def firstArrayElement(in: JsonReader): Boolean = + (in.readSkipWhitespace(): @switch) match + case ']' => false + case _ => + in.retract() + true + + def nextArrayElement(in: JsonReader): Boolean = + (in.readSkipWhitespace(): @switch) match + case ',' => true + case ']' => false + case c => throw JsonParseError(s"expected ',' or ']' got '$c'") + + // True if we got a string (implies a retraction), False for } + def firstField(in: JsonReader): Boolean = + (in.readSkipWhitespace(): @switch) match { + case '"' => true + case '}' => false + case c => + throw JsonParseError(s"expected string or '}' got '$c'") + } + + // True if we got a comma, and False for } + def nextField(in: JsonReader): Boolean = + (in.readSkipWhitespace(): @switch) match { + case ',' => + charWithWS(in, '"') + true + case '}' => false + case c => + throw JsonParseError(s"expected ',' or '}' got '$c'") + } + + def skipValue(in: JsonReader): Unit = + (in.readSkipWhitespace(): @switch) match { + case 'n' => readChars(in, ull, "null") + case 'f' => readChars(in, alse, "false") + case 't' => readChars(in, rue, "true") + case '{' => + if (firstField(in)) { + while ({ + { + char(in, '"') + skipString(in) + char(in, ':') + skipValue(in) + }; nextField(in) + }) () + } + case '[' => + if (firstArrayElement(in)) { + while ({ skipValue(in); nextArrayElement(in) }) () + } + case '"' => + skipString(in) + case '-' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '.' => + skipNumber(in) + case c => throw JsonParseError(s"Unexpected '$c'") + } + + def skipNumber(in: JsonReader): Unit = { + while (isNumber(in.read())) {} + in.retract() + } + + def skipString(in: JsonReader): Unit = + var i: Int = 0 + while ({ i = in.readEscapedString(); i != -1 }) () + + private def checkNumber(gen: JsonReader): Unit = + (gen.readSkipWhitespace(): @switch) match + case '-' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '.' => () + case c => throw JsonParseError(s"Expected a number, got $c") + gen.retract() + + @inline private[this] def isNumber(c: Char): Boolean = + (c: @switch) match + case '+' | '-' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '.' | 'e' | 'E' => true + case _ => false + + private inline def readChars( + gen: JsonReader, + expect: Array[Char], + errMsg: String + ): Unit = + var i: Int = 0 + while i < expect.length do + if gen.read() != expect(i) then throw JsonParseError(s"Expected ${errMsg}") + i += 1 + + @inline def charWithWS(gen: JsonReader, c: Char): Unit = + val got = gen.readSkipWhitespace() + if got != c then throw JsonParseError(s"Expected '$c' got '$got'") + + @inline def char(gen: JsonReader, c: Char): Unit = + val got = gen.read() + if got != c then throw JsonParseError(s"Expected '$c' got '$got'") + + def enumeration( + gen: JsonReader, + matrix: StringMatrix + ): Int = { + var i: Int = 0 + var bs: Long = matrix.initial + var c: Int = -1 + while ({ c = gen.readEscapedString(); c != END_OF_STRING }) { + bs = matrix.update(bs, i, c) + i += 1 + } + bs = matrix.exact(bs, i) + matrix.first(bs) + } \ No newline at end of file diff --git a/src/main/scala/co.blocke.scalajack/json2/JsonReader.scala b/src/main/scala/co.blocke.scalajack/json2/JsonReader.scala new file mode 100644 index 00000000..c0f57bde --- /dev/null +++ b/src/main/scala/co.blocke.scalajack/json2/JsonReader.scala @@ -0,0 +1,86 @@ +package co.blocke.scalajack +package json2 + +import scala.annotation._ +import json.JsonParseError + +case class JsonReader( js: CharSequence ): + private var i = 0 + private val max = js.length + + inline def read(): Char = + if i < max then + i += 1 + history(i - 1) + else BUFFER_EXCEEDED + + inline def readSkipWhitespace(): Char = + var c: Char = 0 + while { c = read(); isWhitespace(c) } do () + c + + inline private def history(p: Int): Char = js.charAt(p) + + inline def retract() = i -= 1 + + inline private def isWhitespace(c: Char): Boolean = + (c: @switch) match { + case ' ' => true + case '\r' => true + case '\n' => true + case '\t' => true + case _ => false + } + + // Read, transforming escaped chars and stopping when we hit '"' + inline def readEscapedString(): Char = + read() match + case '\\' => + val c2 = read() + (c2: @switch) match + case '"' | '\\' | '/' => c2 + case 'b' => '\b' + case 'f' => '\f' + case 'n' => '\n' + case 'r' => '\r' + case 't' => '\t' + case 'u' => nextHex4() + case _ => throw JsonParseError(s"Invalid '\\${c2.toChar}' in string") + case '"' => END_OF_STRING + case BUFFER_EXCEEDED => throw new JsonParseError("Unexpected end of buffer") + case c => c + + inline def nextHex4(): Char = + var i: Int = 0 + var accum: Int = 0 + while i < 4 do + var c = read().toInt + if (c == BUFFER_EXCEEDED) + throw JsonParseError("unexpected EOB in string") + c = + if ('0' <= c && c <= '9') c - '0' + else if ('A' <= c && c <= 'F') c - 'A' + 10 + else if ('a' <= c && c <= 'f') c - 'a' + 10 + else + throw JsonParseError("Invalid hex character in string") + accum = accum * 16 + c + i += 1 + accum.toChar + + /* + Learnings: + + 1) Group implicit decoders into prioritized groups, favoring common/simple things like primitive types. Eg. esoteric things like date/time would be lowest. + 2) Spell out specific collection types BEFORE a general "built" colleciton type--saves cycles doing the conversion + 3) Use @switch macro to tell Scala to use a lookup table, not if-then-else in match-case statements + + Unknown--as of yet unknown how classes are handled.... + + TODO: + [ ] Learn how this StringMatrix works + [ ] Classes are *derived* decoders--magic! Clearly some mix of reading fields (set in StringMatrix), using FieldKeyDecoder and map decoder, then + constructed into a class.... + 1. In JsonDecoder, implement the [K,V] decoder. + 2. Manually construct a class decoder using StringMatrix and supply field-specific decoders for each field + 3. Try it! + */ \ No newline at end of file diff --git a/src/main/scala/co.blocke.scalajack/json2/Numbers.scala b/src/main/scala/co.blocke.scalajack/json2/Numbers.scala new file mode 100644 index 00000000..40cc69f6 --- /dev/null +++ b/src/main/scala/co.blocke.scalajack/json2/Numbers.scala @@ -0,0 +1,990 @@ +/* + * Copyright 2019-2022 John A. De Goes and the ZIO Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package co.blocke.scalajack +package json2 + +import java.io._ +import scala.util.control.NoStackTrace + +/** + * Total, fast, number parsing. + * + * The Java and Scala standard libraries throw exceptions when we attempt to + * parse an invalid number. Unfortunately, exceptions are very expensive, and + * untrusted data can be maliciously constructed to DOS a server. + * + * This suite of functions mitigates against such attacks by building up the + * numbers one character at a time, which has been shown through extensive + * benchmarking to be orders of magnitude faster than exception-throwing stdlib + * parsers, for valid and invalid inputs. This approach, proposed by alexknvl, + * was also benchmarked against regexp-based pre-validation. + * + * Note that although the behaviour is identical to the Java stdlib when given + * the canonical form of a primitive (i.e. the .toString) of a number there may + * be differences in behaviour for non-canonical forms. e.g. the Java stdlib + * may reject "1.0" when parsed as an `BigInteger` but we may parse it as a + * `1`, although "1.1" would be rejected. Parsing of `BigDecimal` preserves the + * trailing zeros on the right but not on the left, e.g. "000.00001000" will be + * "1.000e-5", which is useful in cases where the trailing zeros denote + * measurement accuracy. + * + * `BigInteger`, `BigDecimal`, `Float` and `Double` have a configurable bit + * limit on the size of the significand, to avoid OOM style attacks, which is + * 128 bits by default. + * + * Results are contained in a specialisation of Option that avoids boxing. + */ +// TODO hex radix +// TODO octal radix +object SafeNumbers { + import UnsafeNumbers.UnsafeNumber + + def byte(num: String): ByteOption = + try ByteSome(UnsafeNumbers.byte(num)) + catch { case UnsafeNumber => ByteNone } + + def short(num: String): ShortOption = + try ShortSome(UnsafeNumbers.short(num)) + catch { case UnsafeNumber => ShortNone } + + def int(num: String): IntOption = + try IntSome(UnsafeNumbers.int(num)) + catch { case UnsafeNumber => IntNone } + + def long(num: String): LongOption = + try LongSome(UnsafeNumbers.long(num)) + catch { case UnsafeNumber => LongNone } + + def bigInteger( + num: String, + max_bits: Int = 128 + ): Option[java.math.BigInteger] = + try Some(UnsafeNumbers.bigInteger(num, max_bits)) + catch { case UnsafeNumber => None } + + def float(num: String, max_bits: Int = 128): FloatOption = + try FloatSome(UnsafeNumbers.float(num, max_bits)) + catch { case UnsafeNumber => FloatNone } + + def double(num: String, max_bits: Int = 128): DoubleOption = + try DoubleSome(UnsafeNumbers.double(num, max_bits)) + catch { case UnsafeNumber => DoubleNone } + + def bigDecimal( + num: String, + max_bits: Int = 128 + ): Option[java.math.BigDecimal] = + try Some(UnsafeNumbers.bigDecimal(num, max_bits)) + catch { case UnsafeNumber => None } + + // Based on the amazing work of Raffaello Giulietti + // "The Schubfach way to render doubles": https://drive.google.com/file/d/1luHhyQF9zKlM8yJ1nebU0OgVYhfC6CBN/view + // Sources with the license are here: https://github.com/c4f7fcce9cb06515/Schubfach/blob/3c92d3c9b1fead540616c918cdfef432bca53dfa/todec/src/math/DoubleToDecimal.java + def toString(x: Double): String = { + val bits = java.lang.Double.doubleToLongBits(x) + val ieeeExponent = (bits >> 52).toInt & 0x7ff + val ieeeMantissa = bits & 0xfffffffffffffL + if (ieeeExponent == 2047) { + if (x != x) """"NaN"""" + else if (bits < 0) """"-Infinity"""" + else """"Infinity"""" + } else { + val s = new java.lang.StringBuilder(24) + if (bits < 0) s.append('-') + if (x == 0.0f) s.append('0').append('.').append('0') + else { + var e = ieeeExponent - 1075 + var m = ieeeMantissa | 0x10000000000000L + var dv = 0L + var exp = 0 + if (e == 0) dv = m + else if (e >= -52 && e < 0 && m << e == 0) dv = m >> -e + else { + var expShift, expCorr = 0 + var cblShift = 2 + if (ieeeExponent == 0) { + e = -1074 + m = ieeeMantissa + if (ieeeMantissa < 3) { + m *= 10 + expShift = 1 + } + } else if (ieeeMantissa == 0 && ieeeExponent > 1) { + expCorr = 131007 + cblShift = 1 + } + exp = e * 315653 - expCorr >> 20 + val i = exp + 324 << 1 + val g1 = gs(i) + val g0 = gs(i + 1) + val h = (-exp * 108853 >> 15) + e + 2 + val cb = m << 2 + val outm1 = (m.toInt & 0x1) - 1 + val vb = rop(g1, g0, cb << h) + val vbls = rop(g1, g0, cb - cblShift << h) + outm1 + val vbrd = outm1 - rop(g1, g0, cb + 2 << h) + val s = vb >> 2 + if ( + s < 100 || { + dv = s / 10 // FIXME: Use Math.multiplyHigh(s, 1844674407370955168L) instead after dropping JDK 8 support + val sp40 = dv * 40 + val upin = (vbls - sp40).toInt + (((sp40 + vbrd).toInt + 40) ^ upin) >= 0 || { + dv += ~upin >>> 31 + exp += 1 + false + } + } + ) { + val s4 = s << 2 + val uin = (vbls - s4).toInt + dv = (~ { + if ((((s4 + vbrd).toInt + 4) ^ uin) < 0) uin + else (vb.toInt & 0x3) + (s.toInt & 0x1) - 3 + } >>> 31) + s + exp -= expShift + } + } + val len = digitCount(dv) + exp += len - 1 + if (exp < -3 || exp >= 7) { + val dotOff = s.length + 1 + s.append(dv) + var i = s.length - 1 + while (i > dotOff && s.charAt(i) == '0') i -= 1 + s.setLength(i + 1) + s.insert(dotOff, '.').append('E').append(exp) + } else if (exp < 0) { + s.append('0').append('.') + while ({ + exp += 1 + exp != 0 + }) s.append('0') + s.append(dv) + var i = s.length - 1 + while (s.charAt(i) == '0') i -= 1 + s.setLength(i + 1) + s + } else if (exp + 1 < len) { + val dotOff = s.length + exp + 1 + s.append(dv) + var i = s.length - 1 + while (s.charAt(i) == '0') i -= 1 + s.setLength(i + 1) + s.insert(dotOff, '.') + } else s.append(dv).append('.').append('0') + } + }.toString + } + + def toString(x: Float): String = { + val bits = java.lang.Float.floatToIntBits(x) + val ieeeExponent = (bits >> 23) & 0xff + val ieeeMantissa = bits & 0x7fffff + if (ieeeExponent == 255) { + if (x != x) """"NaN"""" + else if (bits < 0) """"-Infinity"""" + else """"Infinity"""" + } else { + val s = new java.lang.StringBuilder(16) + if (bits < 0) s.append('-') + if (x == 0.0f) s.append('0').append('.').append('0') + else { + var e = ieeeExponent - 150 + var m = ieeeMantissa | 0x800000 + var dv, exp = 0 + if (e == 0) dv = m + else if (e >= -23 && e < 0 && m << e == 0) dv = m >> -e + else { + var expShift, expCorr = 0 + var cblShift = 2 + if (ieeeExponent == 0) { + e = -149 + m = ieeeMantissa + if (ieeeMantissa < 8) { + m *= 10 + expShift = 1 + } + } else if (ieeeMantissa == 0 && ieeeExponent > 1) { + expCorr = 131007 + cblShift = 1 + } + exp = e * 315653 - expCorr >> 20 + val g1 = gs(exp + 324 << 1) + 1 + val h = (-exp * 108853 >> 15) + e + 1 + val cb = m << 2 + val outm1 = (m & 0x1) - 1 + val vb = rop(g1, cb << h) + val vbls = rop(g1, cb - cblShift << h) + outm1 + val vbrd = outm1 - rop(g1, cb + 2 << h) + val s = vb >> 2 + if ( + s < 100 || { + dv = (s * 3435973837L >>> 35).toInt // divide a positive int by 10 + val sp40 = dv * 40 + val upin = vbls - sp40 + ((sp40 + vbrd + 40) ^ upin) >= 0 || { + dv += ~upin >>> 31 + exp += 1 + false + } + } + ) { + val s4 = s << 2 + val uin = vbls - s4 + dv = (~ { + if (((s4 + vbrd + 4) ^ uin) < 0) uin + else (vb & 0x3) + (s & 0x1) - 3 + } >>> 31) + s + exp -= expShift + } + } + val len = digitCount(dv.toLong) + exp += len - 1 + if (exp < -3 || exp >= 7) { + val dotOff = s.length + 1 + s.append(dv) + var i = s.length - 1 + while (i > dotOff && s.charAt(i) == '0') i -= 1 + s.setLength(i + 1) + s.insert(dotOff, '.').append('E').append(exp) + } else if (exp < 0) { + s.append('0').append('.') + while ({ + exp += 1 + exp != 0 + }) s.append('0') + s.append(dv) + var i = s.length - 1 + while (s.charAt(i) == '0') i -= 1 + s.setLength(i + 1) + s + } else if (exp + 1 < len) { + val dotOff = s.length + exp + 1 + s.append(dv) + var i = s.length - 1 + while (s.charAt(i) == '0') i -= 1 + s.setLength(i + 1) + s.insert(dotOff, '.') + } else s.append(dv).append('.').append('0') + } + }.toString + } + + private[this] def rop(g1: Long, g0: Long, cp: Long): Long = { + val x1 = multiplyHigh(g0, cp) // FIXME: Use Math.multiplyHigh after dropping JDK 8 support + val z = (g1 * cp >>> 1) + x1 + val y1 = multiplyHigh(g1, cp) // FIXME: Use Math.multiplyHigh after dropping JDK 8 support + (z >>> 63) + y1 | -(z & 0x7fffffffffffffffL) >>> 63 + } + + private[this] def rop(g: Long, cp: Int): Int = { + val x1 = + ((g & 0xffffffffL) * cp >>> 32) + (g >>> 32) * cp // FIXME: Use Math.multiplyHigh after dropping JDK 8 support + (x1 >>> 31).toInt | -x1.toInt >>> 31 + } + + private[this] def multiplyHigh(x: Long, y: Long): Long = { + val x2 = x & 0xffffffffL + val y2 = y & 0xffffffffL + val b = x2 * y2 + val x1 = x >>> 32 + val y1 = y >>> 32 + val a = x1 * y1 + (((b >>> 32) + (x1 + x2) * (y1 + y2) - b - a) >>> 32) + a + } + + // Adoption of a nice trick form Daniel Lemire's blog that works for numbers up to 10^18: + // https://lemire.me/blog/2021/06/03/computing-the-number-of-digits-of-an-integer-even-faster/ + private[this] def digitCount(x: Long): Int = (offsets(java.lang.Long.numberOfLeadingZeros(x)) + x >> 58).toInt + + private final val offsets = Array( + 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, + 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, 4889916394579099648L, 4889916394579099648L, + 4889916394579099648L, 4610686018427387904L, 4610686018427387904L, 4610686018427387904L, 4610686018427387904L, + 4323355642275676160L, 4323355642275676160L, 4323355642275676160L, 4035215266123964416L, 4035215266123964416L, + 4035215266123964416L, 3746993889972252672L, 3746993889972252672L, 3746993889972252672L, 3746993889972252672L, + 3458764413820540928L, 3458764413820540928L, 3458764413820540928L, 3170534127668829184L, 3170534127668829184L, + 3170534127668829184L, 2882303760517117440L, 2882303760517117440L, 2882303760517117440L, 2882303760517117440L, + 2594073385265405696L, 2594073385265405696L, 2594073385265405696L, 2305843009203693952L, 2305843009203693952L, + 2305843009203693952L, 2017612633060982208L, 2017612633060982208L, 2017612633060982208L, 2017612633060982208L, + 1729382256910170464L, 1729382256910170464L, 1729382256910170464L, 1441151880758548720L, 1441151880758548720L, + 1441151880758548720L, 1152921504606845976L, 1152921504606845976L, 1152921504606845976L, 1152921504606845976L, + 864691128455135132L, 864691128455135132L, 864691128455135132L, 576460752303423478L, 576460752303423478L, + 576460752303423478L, 576460752303423478L, 576460752303423478L, 576460752303423478L, 576460752303423478L + ) + + private[this] val gs: Array[Long] = Array( + 5696189077778435540L, 6557778377634271669L, 9113902524445496865L, 1269073367360058862L, 7291122019556397492L, + 1015258693888047090L, 5832897615645117993L, 6346230177223303157L, 4666318092516094394L, 8766332956520552849L, + 7466108948025751031L, 8492109508320019073L, 5972887158420600825L, 4949013199285060097L, 4778309726736480660L, + 3959210559428048077L, 7645295562778369056L, 6334736895084876923L, 6116236450222695245L, 3223115108696946377L, + 4892989160178156196L, 2578492086957557102L, 7828782656285049914L, 436238524390181040L, 6263026125028039931L, + 2193665226883099993L, 5010420900022431944L, 9133629810990300641L, 8016673440035891111L, 9079784475471615541L, + 6413338752028712889L, 5419153173006337271L, 5130671001622970311L, 6179996945776024979L, 8209073602596752498L, + 6198646298499729642L, 6567258882077401998L, 8648265853541694037L, 5253807105661921599L, 1384589460720489745L, + 8406091369059074558L, 5904691951894693915L, 6724873095247259646L, 8413102376257665455L, 5379898476197807717L, + 4885807493635177203L, 8607837561916492348L, 438594360332462878L, 6886270049533193878L, 4040224303007880625L, + 5509016039626555102L, 6921528257148214824L, 8814425663402488164L, 3695747581953323071L, 7051540530721990531L, + 4801272472933613619L, 5641232424577592425L, 1996343570975935733L, 9025971879324147880L, 3194149713561497173L, + 7220777503459318304L, 2555319770849197738L, 5776622002767454643L, 3888930224050313352L, 4621297602213963714L, + 6800492993982161005L, 7394076163542341943L, 5346765568258592123L, 5915260930833873554L, 7966761269348784022L, + 4732208744667098843L, 8218083422849982379L, 7571533991467358150L, 2080887032334240837L, 6057227193173886520L, + 1664709625867392670L, 4845781754539109216L, 1331767700693914136L, 7753250807262574745L, 7664851543223128102L, + 6202600645810059796L, 6131881234578502482L, 4962080516648047837L, 3060830580291846824L, 7939328826636876539L, + 6742003335837910079L, 6351463061309501231L, 7238277076041283225L, 5081170449047600985L, 3945947253462071419L, + 8129872718476161576L, 6313515605539314269L, 6503898174780929261L, 3206138077060496254L, 5203118539824743409L, + 720236054277441842L, 8324989663719589454L, 4841726501585817270L, 6659991730975671563L, 5718055608639608977L, + 5327993384780537250L, 8263793301653597505L, 8524789415648859601L, 3998697245790980200L, 6819831532519087681L, + 1354283389261828999L, 5455865226015270144L, 8462124340893283845L, 8729384361624432231L, 8005375723316388668L, + 6983507489299545785L, 4559626171282155773L, 5586805991439636628L, 3647700937025724618L, 8938889586303418605L, + 3991647091870204227L, 7151111669042734884L, 3193317673496163382L, 5720889335234187907L, 4399328546167885867L, + 9153422936374700651L, 8883600081239572549L, 7322738349099760521L, 5262205657620702877L, 5858190679279808417L, + 2365090118725607140L, 4686552543423846733L, 7426095317093351197L, 7498484069478154774L, 813706063123630946L, + 5998787255582523819L, 2495639257869859918L, 4799029804466019055L, 3841185813666843096L, 7678447687145630488L, + 6145897301866948954L, 6142758149716504390L, 8606066656235469486L, 4914206519773203512L, 6884853324988375589L, + 7862730431637125620L, 3637067690497580296L, 6290184345309700496L, 2909654152398064237L, 5032147476247760397L, + 483048914547496228L, 8051435961996416635L, 2617552670646949126L, 6441148769597133308L, 2094042136517559301L, + 5152919015677706646L, 5364582523955957764L, 8244670425084330634L, 4893983223587622099L, 6595736340067464507L, + 5759860986241052841L, 5276589072053971606L, 918539974250931950L, 8442542515286354569L, 7003687180914356604L, + 6754034012229083655L, 7447624152102440445L, 5403227209783266924L, 5958099321681952356L, 8645163535653227079L, + 3998935692578258285L, 6916130828522581663L, 5043822961433561789L, 5532904662818065330L, 7724407183888759755L, + 8852647460508904529L, 3135679457367239799L, 7082117968407123623L, 4353217973264747001L, 5665694374725698898L, + 7171923193353707924L, 9065110999561118238L, 407030665140201709L, 7252088799648894590L, 4014973346854071690L, + 5801671039719115672L, 3211978677483257352L, 4641336831775292537L, 8103606164099471367L, 7426138930840468060L, + 5587072233075333540L, 5940911144672374448L, 4469657786460266832L, 4752728915737899558L, 7265075043910123789L, + 7604366265180639294L, 556073626030467093L, 6083493012144511435L, 2289533308195328836L, 4866794409715609148L, + 1831626646556263069L, 7786871055544974637L, 1085928227119065748L, 6229496844435979709L, 6402765803808118083L, + 4983597475548783767L, 6966887050417449628L, 7973755960878054028L, 3768321651184098759L, 6379004768702443222L, + 6704006135689189330L, 5103203814961954578L, 1673856093809441141L, 8165126103939127325L, 833495342724150664L, + 6532100883151301860L, 666796274179320531L, 5225680706521041488L, 533437019343456425L, 8361089130433666380L, + 8232196860433350926L, 6688871304346933104L, 6585757488346680741L, 5351097043477546483L, 7113280398048299755L, + 8561755269564074374L, 313202192651548637L, 6849404215651259499L, 2095236161492194072L, 5479523372521007599L, + 3520863336564710419L, 8767237396033612159L, 99358116390671185L, 7013789916826889727L, 1924160900483492110L, + 5611031933461511781L, 7073351942499659173L, 8977651093538418850L, 7628014293257544353L, 7182120874830735080L, + 6102411434606035483L, 5745696699864588064L, 4881929147684828386L, 9193114719783340903L, 2277063414182859933L, + 7354491775826672722L, 5510999546088198270L, 5883593420661338178L, 719450822128648293L, 4706874736529070542L, + 4264909472444828957L, 7530999578446512867L, 8668529563282681493L, 6024799662757210294L, 3245474835884234871L, + 4819839730205768235L, 4441054276078343059L, 7711743568329229176L, 7105686841725348894L, 6169394854663383341L, + 3839875066009323953L, 4935515883730706673L, 1227225645436504001L, 7896825413969130677L, 118886625327451240L, + 6317460331175304541L, 5629132522374826477L, 5053968264940243633L, 2658631610528906020L, 8086349223904389813L, + 2409136169475294470L, 6469079379123511850L, 5616657750322145900L, 5175263503298809480L, 4493326200257716720L, + 8280421605278095168L, 7189321920412346751L, 6624337284222476135L, 217434314217011916L, 5299469827377980908L, + 173947451373609533L, 8479151723804769452L, 7657013551681595899L, 6783321379043815562L, 2436262026603366396L, + 5426657103235052449L, 7483032843395558602L, 8682651365176083919L, 6438829327320028278L, 6946121092140867135L, + 6995737869226977784L, 5556896873712693708L, 5596590295381582227L, 8891034997940309933L, 7109870065239576402L, + 7112827998352247947L, 153872830078795637L, 5690262398681798357L, 5657121486175901994L, 9104419837890877372L, + 1672696748397622544L, 7283535870312701897L, 6872180620830963520L, 5826828696250161518L, 1808395681922860493L, + 4661462957000129214L, 5136065360280198718L, 7458340731200206743L, 2683681354335452463L, 5966672584960165394L, + 5836293898210272294L, 4773338067968132315L, 6513709525939172997L, 7637340908749011705L, 1198563204647900987L, + 6109872726999209364L, 958850563718320789L, 4887898181599367491L, 2611754858345611793L, 7820637090558987986L, + 489458958611068546L, 6256509672447190388L, 7770264796372675483L, 5005207737957752311L, 682188614985274902L, + 8008332380732403697L, 6625525006089305327L, 6406665904585922958L, 1611071190129533939L, 5125332723668738366L, + 4978205766845537474L, 8200532357869981386L, 4275780412210949635L, 6560425886295985109L, 1575949922397804547L, + 5248340709036788087L, 3105434345289198799L, 8397345134458860939L, 6813369359833673240L, 6717876107567088751L, + 7295369895237893754L, 5374300886053671001L, 3991621508819359841L, 8598881417685873602L, 2697245599369065423L, + 6879105134148698881L, 7691819701608117823L, 5503284107318959105L, 4308781353915539097L, 8805254571710334568L, + 6894050166264862555L, 7044203657368267654L, 9204588947753800367L, 5635362925894614123L, 9208345565573995455L, + 9016580681431382598L, 3665306460692661759L, 7213264545145106078L, 6621593983296039730L, 5770611636116084862L, + 8986624001378742108L, 4616489308892867890L, 3499950386361083363L, 7386382894228588624L, 5599920618177733380L, + 5909106315382870899L, 6324610901913141866L, 4727285052306296719L, 6904363128901468655L, 7563656083690074751L, + 5512957784129484362L, 6050924866952059801L, 2565691819932632328L, 4840739893561647841L, 207879048575150701L, + 7745183829698636545L, 5866629699833106606L, 6196147063758909236L, 4693303759866485285L, 4956917651007127389L, + 1909968600522233067L, 7931068241611403822L, 6745298575577483229L, 6344854593289123058L, 1706890045720076260L, + 5075883674631298446L, 5054860851317971332L, 8121413879410077514L, 4398428547366843807L, 6497131103528062011L, + 5363417245264430207L, 5197704882822449609L, 2446059388840589004L, 8316327812515919374L, 7603043836886852730L, + 6653062250012735499L, 7927109476880437346L, 5322449800010188399L, 8186361988875305038L, 8515919680016301439L, + 7564155960087622576L, 6812735744013041151L, 7895999175441053223L, 5450188595210432921L, 4472124932981887417L, + 8720301752336692674L, 3466051078029109543L, 6976241401869354139L, 4617515269794242796L, 5580993121495483311L, + 5538686623206349399L, 8929588994392773298L, 5172549782388248714L, 7143671195514218638L, 7827388640652509295L, + 5714936956411374911L, 727887690409141951L, 9143899130258199857L, 6698643526767492606L, 7315119304206559886L, + 1669566006672083762L, 5852095443365247908L, 8714350434821487656L, 4681676354692198327L, 1437457125744324640L, + 7490682167507517323L, 4144605808561874585L, 5992545734006013858L, 7005033461591409992L, 4794036587204811087L, + 70003547160262509L, 7670458539527697739L, 1956680082827375175L, 6136366831622158191L, 3410018473632855302L, + 4909093465297726553L, 883340371535329080L, 7854549544476362484L, 8792042223940347174L, 6283639635581089987L, + 8878308186523232901L, 5026911708464871990L, 3413297734476675998L, 8043058733543795184L, 5461276375162681596L, + 6434446986835036147L, 6213695507501100438L, 5147557589468028918L, 1281607591258970028L, 8236092143148846269L, + 205897738643396882L, 6588873714519077015L, 2009392598285672668L, 5271098971615261612L, 1607514078628538134L, + 8433758354584418579L, 4416696933176616176L, 6747006683667534863L, 5378031953912248102L, 5397605346934027890L, + 7991774377871708805L, 8636168555094444625L, 3563466967739958280L, 6908934844075555700L, 2850773574191966624L, + 5527147875260444560L, 2280618859353573299L, 8843436600416711296L, 3648990174965717279L, 7074749280333369037L, + 1074517732601618662L, 5659799424266695229L, 6393637408194160414L, 9055679078826712367L, 4695796630997791177L, + 7244543263061369894L, 67288490056322619L, 5795634610449095915L, 1898505199416013257L, 4636507688359276732L, + 1518804159532810606L, 7418412301374842771L, 4274761062623452130L, 5934729841099874217L, 1575134442727806543L, + 4747783872879899373L, 6794130776295110719L, 7596454196607838997L, 9025934834701221989L, 6077163357286271198L, + 3531399053019067268L, 4861730685829016958L, 6514468057157164137L, 7778769097326427133L, 8578474484080507458L, + 6223015277861141707L, 1328756365151540482L, 4978412222288913365L, 6597028314234097870L, 7965459555662261385L, + 1331873265919780784L, 6372367644529809108L, 1065498612735824627L, 5097894115623847286L, 4541747704930570025L, + 8156630584998155658L, 3577447513147001717L, 6525304467998524526L, 6551306825259511697L, 5220243574398819621L, + 3396371052836654196L, 8352389719038111394L, 1744844869796736390L, 6681911775230489115L, 3240550303208344274L, + 5345529420184391292L, 2592440242566675419L, 8552847072295026067L, 5992578795477635832L, 6842277657836020854L, + 1104714221640198342L, 5473822126268816683L, 2728445784683113836L, 8758115402030106693L, 2520838848122026975L, + 7006492321624085354L, 5706019893239531903L, 5605193857299268283L, 6409490321962580684L, 8968310171678829253L, + 8410510107769173933L, 7174648137343063403L, 1194384864102473662L, 5739718509874450722L, 4644856706023889253L, + 9183549615799121156L, 53073100154402158L, 7346839692639296924L, 7421156109607342373L, 5877471754111437539L, + 7781599295056829060L, 4701977403289150031L, 8069953843416418410L, 7523163845262640050L, 9222577334724359132L, + 6018531076210112040L, 7378061867779487306L, 4814824860968089632L, 5902449494223589845L, 7703719777548943412L, + 2065221561273923105L, 6162975822039154729L, 7186200471132003969L, 4930380657631323783L, 7593634784276558337L, + 7888609052210118054L, 1081769210616762369L, 6310887241768094443L, 2710089775864365057L, 5048709793414475554L, + 5857420635433402369L, 8077935669463160887L, 3837849794580578305L, 6462348535570528709L, 8604303057777328129L, + 5169878828456422967L, 8728116853592817665L, 8271806125530276748L, 6586289336264687617L, 6617444900424221398L, + 8958380283753660417L, 5293955920339377119L, 1632681004890062849L, 8470329472543003390L, 6301638422566010881L, + 6776263578034402712L, 5041310738052808705L, 5421010862427522170L, 343699775700336641L, 8673617379884035472L, + 549919641120538625L, 6938893903907228377L, 5973958935009296385L, 5551115123125782702L, 1089818333265526785L, + 8881784197001252323L, 3588383740595798017L, 7105427357601001858L, 6560055807218548737L, 5684341886080801486L, + 8937393460516749313L, 9094947017729282379L, 1387108685230112769L, 7275957614183425903L, 2954361355555045377L, + 5820766091346740722L, 6052837899185946625L, 4656612873077392578L, 1152921504606846977L, 7450580596923828125L, 1L, + 5960464477539062500L, 1L, 4768371582031250000L, 1L, 7629394531250000000L, 1L, 6103515625000000000L, 1L, + 4882812500000000000L, 1L, 7812500000000000000L, 1L, 6250000000000000000L, 1L, 5000000000000000000L, 1L, + 8000000000000000000L, 1L, 6400000000000000000L, 1L, 5120000000000000000L, 1L, 8192000000000000000L, 1L, + 6553600000000000000L, 1L, 5242880000000000000L, 1L, 8388608000000000000L, 1L, 6710886400000000000L, 1L, + 5368709120000000000L, 1L, 8589934592000000000L, 1L, 6871947673600000000L, 1L, 5497558138880000000L, 1L, + 8796093022208000000L, 1L, 7036874417766400000L, 1L, 5629499534213120000L, 1L, 9007199254740992000L, 1L, + 7205759403792793600L, 1L, 5764607523034234880L, 1L, 4611686018427387904L, 1L, 7378697629483820646L, + 3689348814741910324L, 5902958103587056517L, 1106804644422573097L, 4722366482869645213L, 6419466937650923963L, + 7555786372591432341L, 8426472692870523179L, 6044629098073145873L, 4896503746925463381L, 4835703278458516698L, + 7606551812282281028L, 7737125245533626718L, 1102436455425918676L, 6189700196426901374L, 4571297979082645264L, + 4951760157141521099L, 5501712790637071373L, 7922816251426433759L, 3268717242906448711L, 6338253001141147007L, + 4459648201696114131L, 5070602400912917605L, 9101741783469756789L, 8112963841460668169L, 5339414816696835055L, + 6490371073168534535L, 6116206260728423206L, 5192296858534827628L, 4892965008582738565L, 8307674973655724205L, + 5984069606361426541L, 6646139978924579364L, 4787255685089141233L, 5316911983139663491L, 5674478955442268148L, + 8507059173023461586L, 5389817513965718714L, 6805647338418769269L, 2467179603801619810L, 5444517870735015415L, + 3818418090412251009L, 8711228593176024664L, 6109468944659601615L, 6968982874540819731L, 6732249563098636453L, + 5575186299632655785L, 3541125243107954001L, 8920298079412249256L, 5665800388972726402L, 7136238463529799405L, + 2687965903807225960L, 5708990770823839524L, 2150372723045780768L, 9134385233318143238L, 7129945171615159552L, + 7307508186654514591L, 169932915179262157L, 5846006549323611672L, 7514643961627230372L, 4676805239458889338L, + 2322366354559873974L, 7482888383134222941L, 1871111759924843197L, 5986310706507378352L, 8875587037423695204L, + 4789048565205902682L, 3411120815197045840L, 7662477704329444291L, 7302467711686228506L, 6129982163463555433L, + 3997299761978027643L, 4903985730770844346L, 6887188624324332438L, 7846377169233350954L, 7330152984177021577L, + 6277101735386680763L, 7708796794712572423L, 5021681388309344611L, 633014213657192454L, 8034690221294951377L, + 6546845963964373411L, 6427752177035961102L, 1548127956429588405L, 5142201741628768881L, 6772525587256536209L, + 8227522786606030210L, 7146692124868547611L, 6582018229284824168L, 5717353699894838089L, 5265614583427859334L, + 8263231774657780795L, 8424983333484574935L, 7687147617339583786L, 6739986666787659948L, 6149718093871667029L, + 5391989333430127958L, 8609123289839243947L, 8627182933488204734L, 2706550819517059345L, 6901746346790563787L, + 4009915062984602637L, 5521397077432451029L, 8741955272500547595L, 8834235323891921647L, 8453105213888010667L, + 7067388259113537318L, 3073135356368498210L, 5653910607290829854L, 6147857099836708891L, 9046256971665327767L, + 4302548137625868741L, 7237005577332262213L, 8976061732213560478L, 5789604461865809771L, 1646826163657982898L, + 4631683569492647816L, 8696158560410206965L, 7410693711188236507L, 1001132845059645012L, 5928554968950589205L, + 6334929498160581494L, 4742843975160471364L, 5067943598528465196L, 7588550360256754183L, 2574686535532678828L, + 6070840288205403346L, 5749098043168053386L, 4856672230564322677L, 2754604027163487547L, 7770675568902916283L, + 6252040850832535236L, 6216540455122333026L, 8690981495407938512L, 4973232364097866421L, 5108110788955395648L, + 7957171782556586274L, 4483628447586722714L, 6365737426045269019L, 5431577165440333333L, 5092589940836215215L, + 6189936139723221828L, 8148143905337944345L, 680525786702379117L, 6518515124270355476L, 544420629361903293L, + 5214812099416284380L, 7814234132973343281L, 8343699359066055009L, 3279402575902573442L, 6674959487252844007L, + 4468196468093013915L, 5339967589802275205L, 9108580396587276617L, 8543948143683640329L, 5350356597684866779L, + 6835158514946912263L, 6124959685518848585L, 5468126811957529810L, 8589316563156989191L, 8749002899132047697L, + 4519534464196406897L, 6999202319305638157L, 9149650793469991003L, 5599361855444510526L, 3630371820034082479L, + 8958978968711216842L, 2119246097312621643L, 7167183174968973473L, 7229420099962962799L, 5733746539975178779L, + 249512857857504755L, 9173994463960286046L, 4088569387313917931L, 7339195571168228837L, 1426181102480179183L, + 5871356456934583069L, 6674968104097008831L, 4697085165547666455L, 7184648890648562227L, 7515336264876266329L, + 2272066188182923754L, 6012269011901013063L, 3662327357917294165L, 4809815209520810450L, 6619210701075745655L, + 7695704335233296721L, 1367365084866417240L, 6156563468186637376L, 8472589697376954439L, 4925250774549309901L, + 4933397350530608390L, 7880401239278895842L, 4204086946107063100L, 6304320991423116673L, 8897292778998515965L, + 5043456793138493339L, 1583811001085947287L, 8069530869021589342L, 6223446416479425982L, 6455624695217271474L, + 1289408318441630463L, 5164499756173817179L, 2876201062124259532L, 8263199609878107486L, 8291270514140725574L, + 6610559687902485989L, 4788342003941625298L, 5288447750321988791L, 5675348010524255400L, 8461516400515182066L, + 5391208002096898316L, 6769213120412145653L, 2468291994306563491L, 5415370496329716522L, 5663982410187161116L, + 8664592794127546436L, 1683674226815637140L, 6931674235302037148L, 8725637010936330358L, 5545339388241629719L, + 1446486386636198802L, 8872543021186607550L, 6003727033359828406L, 7098034416949286040L, 4802981626687862725L, + 5678427533559428832L, 3842385301350290180L, 9085484053695086131L, 7992490889531419449L, 7268387242956068905L, + 4549318304254180398L, 5814709794364855124L, 3639454643403344318L, 4651767835491884099L, 4756238122093630616L, + 7442828536787014559L, 2075957773236943501L, 5954262829429611647L, 3505440625960509963L, 4763410263543689317L, + 8338375722881273455L, 7621456421669902908L, 5962703527126216881L, 6097165137335922326L, 8459511636442883828L, + 4877732109868737861L, 4922934901783351901L, 7804371375789980578L, 4187347028111452718L, 6243497100631984462L, + 7039226437231072498L, 4994797680505587570L, 1942032335042947675L, 7991676288808940112L, 3107251736068716280L, + 6393341031047152089L, 8019824610967838509L, 5114672824837721671L, 8260534096145225969L, 8183476519740354675L, + 304133702235675419L, 6546781215792283740L, 243306961788540335L, 5237424972633826992L, 194645569430832268L, + 8379879956214123187L, 2156107318460286790L, 6703903964971298549L, 7258909076881094917L, 5363123171977038839L, + 7651801668875831096L, 8580997075163262143L, 6708859448088464268L, 6864797660130609714L, 9056436373212681737L, + 5491838128104487771L, 9089823505941100552L, 8786941004967180435L, 1630996757909074751L, 7029552803973744348L, + 1304797406327259801L, 5623642243178995478L, 4733186739803718164L, 8997827589086392765L, 5728424376314993901L, + 7198262071269114212L, 4582739501051995121L, 5758609657015291369L, 9200214822954461581L, 9213775451224466191L, + 9186320494614273045L, 7371020360979572953L, 5504381988320463275L, 5896816288783658362L, 8092854405398280943L, + 4717453031026926690L, 2784934709576714431L, 7547924849643082704L, 4455895535322743090L, 6038339879714466163L, + 5409390835629149634L, 4830671903771572930L, 8016861483245230030L, 7729075046034516689L, 3603606336337592240L, + 6183260036827613351L, 4727559476441028954L, 4946608029462090681L, 1937373173781868001L, 7914572847139345089L, + 8633820300163854287L, 6331658277711476071L, 8751730647502038591L, 5065326622169180857L, 5156710110630675711L, + 8104522595470689372L, 872038547525260492L, 6483618076376551497L, 6231654060133073878L, 5186894461101241198L, + 1295974433364548779L, 8299031137761985917L, 228884686012322885L, 6639224910209588733L, 5717130970922723793L, + 5311379928167670986L, 8263053591480089358L, 8498207885068273579L, 308164894771456841L, 6798566308054618863L, + 2091206323188120634L, 5438853046443695090L, 5362313873292406831L, 8702164874309912144L, 8579702197267850929L, + 6961731899447929715L, 8708436165185235905L, 5569385519558343772L, 6966748932148188724L, 8911016831293350036L, + 3768100661953281312L, 7128813465034680029L, 1169806122191669888L, 5703050772027744023L, 2780519305124291072L, + 9124881235244390437L, 2604156480827910553L, 7299904988195512349L, 7617348406775193928L, 5839923990556409879L, + 7938553132791110304L, 4671939192445127903L, 8195516913603843405L, 7475102707912204646L, 2044780617540418478L, + 5980082166329763716L, 9014522123516155429L, 4784065733063810973L, 5366943291441969181L, 7654505172902097557L, + 6742434858936195528L, 6123604138321678046L, 1704599072407046100L, 4898883310657342436L, 8742376887409457526L, + 7838213297051747899L, 1075082168258445910L, 6270570637641398319L, 2704740141977711890L, 5016456510113118655L, + 4008466520953124674L, 8026330416180989848L, 6413546433524999478L, 6421064332944791878L, 8820185961561909905L, + 5136851466355833503L, 1522125547136662440L, 8218962346169333605L, 590726468047704741L, 6575169876935466884L, + 472581174438163793L, 5260135901548373507L, 2222739346921486196L, 8416217442477397611L, 5401057362445333075L, + 6732973953981918089L, 2476171482585311299L, 5386379163185534471L, 3825611593439204201L, 8618206661096855154L, + 2431629734760816398L, 6894565328877484123L, 3789978195179608280L, 5515652263101987298L, 6721331370885596947L, + 8825043620963179677L, 8909455786045999954L, 7060034896770543742L, 3438215814094889640L, 5648027917416434993L, + 8284595873388777197L, 9036844667866295990L, 2187306953196312545L, 7229475734293036792L, 1749845562557050036L, + 5783580587434429433L, 6933899672158505514L, 4626864469947543547L, 13096515613938926L, 7402983151916069675L, + 1865628832353257443L, 5922386521532855740L, 1492503065882605955L, 4737909217226284592L, 1194002452706084764L, + 7580654747562055347L, 3755078331700690783L, 6064523798049644277L, 8538085887473418112L, 4851619038439715422L, + 3141119895236824166L, 7762590461503544675L, 6870466239749873827L, 6210072369202835740L, 5496372991799899062L, + 4968057895362268592L, 4397098393439919250L, 7948892632579629747L, 8880031836874825961L, 6359114106063703798L, + 3414676654757950445L, 5087291284850963038L, 6421090138548270680L, 8139666055761540861L, 8429069814306277926L, + 6511732844609232689L, 4898581444074067179L, 5209386275687386151L, 5763539562630208905L, 8335018041099817842L, + 5532314485466423924L, 6668014432879854274L, 736502773631228816L, 5334411546303883419L, 2433876626275938215L, + 8535058474086213470L, 7583551416783411467L, 6828046779268970776L, 6066841133426729173L, 5462437423415176621L, + 3008798499370428177L, 8739899877464282594L, 1124728784250774760L, 6991919901971426075L, 2744457434771574970L, + 5593535921577140860L, 2195565947817259976L, 8949657474523425376L, 3512905516507615961L, 7159725979618740301L, + 965650005835137607L, 5727780783694992240L, 8151217634151930732L, 9164449253911987585L, 3818576177788313364L, + 7331559403129590068L, 3054860942230650691L, 5865247522503672054L, 6133237568526430876L, 4692198018002937643L, + 6751264462192099863L, 7507516828804700229L, 8957348732136404618L, 6006013463043760183L, 9010553393080078856L, + 4804810770435008147L, 1674419492351197600L, 7687697232696013035L, 4523745595132871322L, 6150157786156810428L, + 3618996476106297057L, 4920126228925448342L, 6584545995626947969L, 7872201966280717348L, 3156575963519296104L, + 6297761573024573878L, 6214609585557347207L, 5038209258419659102L, 8661036483187788089L, 8061134813471454564L, + 6478960743616640295L, 6448907850777163651L, 7027843002264267398L, 5159126280621730921L, 3777599994440458757L, + 8254602048994769474L, 2354811176362823687L, 6603681639195815579L, 3728523348461214111L, 5282945311356652463L, + 4827493086139926451L, 8452712498170643941L, 5879314530452927160L, 6762169998536515153L, 2858777216991386566L, + 5409735998829212122L, 5976370588335019576L, 8655577598126739396L, 2183495311852210675L, 6924462078501391516L, + 9125493878965589187L, 5539569662801113213L, 5455720695801516188L, 8863311460481781141L, 6884478705911470739L, + 7090649168385424913L, 3662908557358221429L, 5672519334708339930L, 6619675660628487467L, 9076030935533343889L, + 1368109020150804139L, 7260824748426675111L, 2939161623491598473L, 5808659798741340089L, 506654891422323617L, + 4646927838993072071L, 2249998320508814055L, 7435084542388915313L, 9134020534926967972L, 5948067633911132251L, + 1773193205828708893L, 4758454107128905800L, 8797252194146787761L, 7613526571406249281L, 4852231473780084609L, + 6090821257124999425L, 2037110771653112526L, 4872657005699999540L, 1629688617322490021L, 7796251209119999264L, + 2607501787715984033L, 6237000967295999411L, 3930675837543742388L, 4989600773836799529L, 1299866262664038749L, + 7983361238138879246L, 5769134835004372321L, 6386688990511103397L, 2770633460632542696L, 5109351192408882717L, + 7750529990618899641L, 8174961907854212348L, 5022150355506418780L, 6539969526283369878L, 7707069099147045347L, + 5231975621026695903L, 631632057204770793L, 8371160993642713444L, 8389308921011453915L, 6696928794914170755L, + 8556121544180118293L, 5357543035931336604L, 6844897235344094635L, 8572068857490138567L, 5417812354437685931L, + 6857655085992110854L, 644901068808238421L, 5486124068793688683L, 2360595262417545899L, 8777798510069901893L, + 1932278012497118276L, 7022238808055921514L, 5235171224739604944L, 5617791046444737211L, 6032811387162639117L, + 8988465674311579538L, 5963149404718312264L, 7190772539449263630L, 8459868338516560134L, 5752618031559410904L, + 6767894670813248108L, 9204188850495057447L, 5294608251188331487L + ) +} + +// specialised Options to avoid boxing. Prefer .isEmpty guarded access to .value +// for higher performance: pattern matching is slightly slower. + +sealed abstract class ByteOption { + def isEmpty: Boolean + def value: Byte +} +case object ByteNone extends ByteOption { + def isEmpty = true + def value: Byte = throw new java.util.NoSuchElementException +} +case class ByteSome(value: Byte) extends ByteOption { + def isEmpty = false +} + +sealed abstract class ShortOption { + def isEmpty: Boolean + def value: Short +} +case object ShortNone extends ShortOption { + def isEmpty = true + def value: Short = throw new java.util.NoSuchElementException +} +case class ShortSome(value: Short) extends ShortOption { + def isEmpty = false +} + +sealed abstract class IntOption { + def isEmpty: Boolean + def value: Int +} +case object IntNone extends IntOption { + def isEmpty = true + def value: Int = throw new java.util.NoSuchElementException +} +case class IntSome(value: Int) extends IntOption { + def isEmpty = false +} + +sealed abstract class LongOption { + def isEmpty: Boolean + def value: Long +} +case object LongNone extends LongOption { + def isEmpty = true + def value: Long = throw new java.util.NoSuchElementException +} +case class LongSome(value: Long) extends LongOption { + def isEmpty = false +} + +sealed abstract class FloatOption { + def isEmpty: Boolean + def value: Float +} +case object FloatNone extends FloatOption { + def isEmpty = true + def value: Float = throw new java.util.NoSuchElementException +} +case class FloatSome(value: Float) extends FloatOption { + def isEmpty = false +} + +sealed abstract class DoubleOption { + def isEmpty: Boolean + def value: Double +} +case object DoubleNone extends DoubleOption { + def isEmpty = true + def value: Double = throw new java.util.NoSuchElementException +} +case class DoubleSome(value: Double) extends DoubleOption { + def isEmpty = false +} + +// The underlying implementation uses an exception that has no stack trace for +// the failure case, which is 20x faster than retaining stack traces. Therefore, +// we require no boxing of the results on the happy path. This slows down the +// unhappy path a little bit, but it's still on the same order of magnitude as +// the happy path. +// +// This API should only be used by people who know what they are doing. Note +// that JsonReader implementations consume one character beyond the number that is +// parsed, because there is no terminator character. +object UnsafeNumbers { + + // should never escape into user code + case object UnsafeNumber + extends Exception( + "if you see this a dev made a mistake using UnsafeNumbers" + ) + with NoStackTrace + + def byte(num: String): Byte = + byte_(new JsonReader(num), true) + def byte_(in: JsonReader, consume: Boolean): Byte = + long__(in, Byte.MinValue, Byte.MaxValue, consume).toByte + + def short(num: String): Short = + short_(new JsonReader(num), true) + def short_(in: JsonReader, consume: Boolean): Short = + long__(in, Short.MinValue, Short.MaxValue, consume).toShort + + def int(num: String): Int = + int_(new JsonReader(num), true) + def int_(in: JsonReader, consume: Boolean): Int = + long__(in, Int.MinValue, Int.MaxValue, consume).toInt + + def long(num: String): Long = + long_(new JsonReader(num), true) + def long_(in: JsonReader, consume: Boolean): Long = + long__(in, Long.MinValue, Long.MaxValue, consume) + + def bigInteger(num: String, max_bits: Int): java.math.BigInteger = + bigInteger_(new JsonReader(num), true, max_bits) + def bigInteger_( + in: JsonReader, + consume: Boolean, + max_bits: Int + ): java.math.BigInteger = { + var current: Int = in.read() + var negative = false + + if (current == '-') { + negative = true + current = in.read() + } else if (current == '+') + current = in.read() + if (current == -1) throw UnsafeNumber + + bigDecimal__(in, consume, negative, current, true, max_bits).unscaledValue + } + + // measured faster than Character.isDigit + @inline private[this] def isDigit(i: Int): Boolean = + '0' <= i && i <= '9' + + // is it worth keeping this custom long__ instead of using bigInteger since it + // is approximately double the performance. + def long__(in: JsonReader, lower: Long, upper: Long, consume: Boolean): Long = { + var current: Int = 0 + + current = in.read() + if (current == -1) throw UnsafeNumber + var negative = false + if (current == '-') { + negative = true + current = in.read() + if (current == -1) throw UnsafeNumber + } else if (current == '+') { + current = in.read() + if (current == -1) throw UnsafeNumber + } + + if (!isDigit(current)) + throw UnsafeNumber + + var accum: Long = 0L + while ({ + { + val c = current - '0' + if (accum <= longunderflow) + if (accum < longunderflow) + throw UnsafeNumber + else if (accum == longunderflow && c == 9) + throw UnsafeNumber + // count down, not up, because it is larger + accum = accum * 10 - c // should never underflow + current = in.read() + }; current != -1 && isDigit(current) + }) () + + if (consume && current != -1) throw UnsafeNumber + + if (negative) + if (accum < lower || upper < accum) throw UnsafeNumber + else accum + else if (accum == Long.MinValue) + throw UnsafeNumber + else { + accum = -accum + if (accum < lower || upper < accum) throw UnsafeNumber + else accum + } + } + + def float(num: String, max_bits: Int): Float = + float_(new JsonReader(num), true, max_bits) + + def float_(in: JsonReader, consume: Boolean, max_bits: Int): Float = { + var current: Int = in.read() + var negative = false + + def readAll(s: String): Unit = { + var i = 0 + val len = s.length + + while (i < len) { + current = in.read() + if (current != s(i)) throw UnsafeNumber + i += 1 + } + + current = in.read() // to be consistent read the terminator + + if (consume && current != -1) + throw UnsafeNumber + } + + if (current == 'N') { + readAll("aN") + return Float.NaN + } + + if (current == '-') { + negative = true + current = in.read() + } else if (current == '+') { + current = in.read() + } + + if (current == 'I') { + readAll("nfinity") + + if (negative) return Float.NegativeInfinity + else return Float.PositiveInfinity + } + + if (current == -1) + throw UnsafeNumber + + val res = bigDecimal__(in, consume, negative = negative, initial = current, int_only = false, max_bits = max_bits) + + if (negative && res.unscaledValue == java.math.BigInteger.ZERO) -0.0f + else res.floatValue + } + + def double(num: String, max_bits: Int): Double = + double_(new JsonReader(num), true, max_bits) + + def double_(in: JsonReader, consume: Boolean, max_bits: Int): Double = { + var current: Int = in.read() + var negative = false + + def readall(s: String): Unit = { + var i = 0 + val len = s.length + while (i < len) { + current = in.read() + if (current != s(i)) throw UnsafeNumber + i += 1 + } + current = in.read() // to be consistent read the terminator + if (consume && current != -1) throw UnsafeNumber + } + + if (current == 'N') { + readall("aN") + return Double.NaN + } + + if (current == '-') { + negative = true + current = in.read() + } else if (current == '+') + current = in.read() + + if (current == 'I') { + readall("nfinity") + if (negative) return Double.NegativeInfinity + else return Double.PositiveInfinity + } + + if (current == -1) throw UnsafeNumber + + // we could avoid going via BigDecimal if we wanted to do something like + // https://github.com/plokhotnyuk/jsoniter-scala/blob/56ff2a60e28aa27bd4788caf3b1557a558c00fa1/jsoniter-scala-core/jvm/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonJsonReader.scala#L1395-L1425 + // based on + // https://www.reddit.com/r/rust/comments/a6j5j1/making_rust_float_parsing_fast_and_correct + // + // the fallback of .doubleValue tends to call out to parseDouble which + // ultimately uses strtod from the system libraries and they may loop until + // the answer converges + // https://github.com/rust-lang/rust/pull/27307/files#diff-fe6c36003393c49bf7e5c413458d6d9cR43-R84 + val res = bigDecimal__(in, consume, negative, current, false, max_bits) + // BigDecimal doesn't have a negative zero, so we need to apply manually + if (negative && res.unscaledValue == java.math.BigInteger.ZERO) -0.0 + // TODO implement Algorithm M or Bigcomp and avoid going via BigDecimal + else res.doubleValue + } + + def bigDecimal(num: String, max_bits: Int): java.math.BigDecimal = + bigDecimal_(new JsonReader(num), true, max_bits) + def bigDecimal_( + in: JsonReader, + consume: Boolean, + max_bits: Int + ): java.math.BigDecimal = { + var current: Int = in.read() + var negative = false + + if (current == '-') { + negative = true + current = in.read() + } else if (current == '+') + current = in.read() + if (current == -1) throw UnsafeNumber + + bigDecimal__(in, consume, negative, current, false, max_bits) + } + + def bigDecimal__( + in: JsonReader, + consume: Boolean, + negative: Boolean, + initial: Int, + int_only: Boolean, + max_bits: Int + ): java.math.BigDecimal = { + var current: Int = initial + // record the significand as Long until it overflows, then swap to BigInteger + var sig: Long = -1 // -1 means it hasn't been seen yet + var sig_ : java.math.BigInteger = null // non-null wins over sig + var dot: Int = 0 // counts from the right + var exp: Int = 0 // implied + + def advance(): Boolean = { + current = in.read() + current != -1 + } + + // skip trailing zero on the left + while (current == '0') { + sig = 0 + if (!advance()) + return java.math.BigDecimal.ZERO + } + + def push_sig(): Unit = { + val c = current - '0' + // would be nice if there was a fused instruction... + if (sig_ != null) { + sig_ = sig_ + .multiply(java.math.BigInteger.TEN) + .add(bigIntegers(c)) + // arbitrary limit on BigInteger size to avoid OOM attacks + if (sig_.bitLength >= max_bits) + throw UnsafeNumber + } else if (sig >= longoverflow) + sig_ = java.math.BigInteger + .valueOf(sig) + .multiply(java.math.BigInteger.TEN) + .add(bigIntegers(c)) + else if (sig < 0) sig = c.toLong + else sig = sig * 10 + c + } + + def significand() = + if (sig <= 0) java.math.BigDecimal.ZERO + else { + val res = + if (sig_ != null) + new java.math.BigDecimal(sig_) + else + new java.math.BigDecimal(sig) + if (negative) res.negate else res + } + + while (isDigit(current)) { + push_sig() + if (!advance()) + return significand() + } + + if (int_only) { + if (consume && current != -1) + throw UnsafeNumber + return significand() + } + + if (current == '.') { + if (sig < 0) sig = 0 // e.g. ".1" is shorthand for "0.1" + if (!advance()) + return significand() + while (isDigit(current)) { + dot += 1 + if (sig > 0 || current != '0') + push_sig() + // overflowed... + if (dot < 0) throw UnsafeNumber + advance() + } + } + + if (sig < 0) throw UnsafeNumber // no significand + + if (current == 'E' || current == 'e') + exp = int_(in, consume) + else if (consume && current != -1) + throw UnsafeNumber + + val scale = if (dot < 1) exp else exp - dot + val res = significand() + if (scale != 0) + res.scaleByPowerOfTen(scale) + else + res + } + // note that bigDecimal does not have a negative zero + private[this] val bigIntegers: Array[java.math.BigInteger] = + (0L to 9L).map(java.math.BigInteger.valueOf).toArray + private[this] val longunderflow: Long = Long.MinValue / 10L + private[this] val longoverflow: Long = Long.MaxValue / 10L +} \ No newline at end of file diff --git a/src/main/scala/co.blocke.scalajack/json2/StringMatrix.scala b/src/main/scala/co.blocke.scalajack/json2/StringMatrix.scala new file mode 100644 index 00000000..320d826a --- /dev/null +++ b/src/main/scala/co.blocke.scalajack/json2/StringMatrix.scala @@ -0,0 +1,92 @@ +package co.blocke.scalajack +package json2 + +// A data structure encoding a simple algorithm for Trie pruning: Given a list +// of strings, and a sequence of incoming characters, find the strings that +// match, by manually maintaining a bitset. Empty strings are not allowed. +// +// +final class StringMatrix(val xs: Array[String], aliases: Array[(String, Int)] = Array.empty) { + require(xs.forall(_.nonEmpty)) + require(xs.nonEmpty) + require(xs.length + aliases.length < 64) + require(aliases.forall(_._1.nonEmpty)) + require(aliases.forall(p => p._2 >= 0 && p._2 < xs.length)) + + val width = xs.length + aliases.length + val height: Int = xs.map(_.length).max max (if (aliases.isEmpty) 0 else aliases.map(_._1.length).max) + val lengths: Array[Int] = xs.map(_.length) ++ aliases.map(_._1.length) + val initial: Long = (0 until width).foldLeft(0L)((bs, r) => bs | (1L << r)) + + private val matrix: Array[Int] = { + val m = Array.fill[Int](width * height)(-1) + var string: Int = 0 + while (string < width) { + val s = if (string < xs.length) xs(string) else aliases(string - xs.length)._1 + val len = s.length + var char: Int = 0 + while (char < len) { + m(width * char + string) = s.codePointAt(char) + char += 1 + } + string += 1 + } + m + } + + private val resolve: Array[Int] = { + val r = Array.tabulate[Int](xs.length + aliases.length)(identity) + aliases.zipWithIndex.foreach { case ((_, pi), i) => r(xs.length + i) = pi } + r + } + + // must be called with increasing `char` (starting with bitset obtained from a + // call to 'initial', char = 0) + def update(bitset: Long, char: Int, c: Int): Long = + if (char >= height) 0L // too long + else if (bitset == 0L) 0L // everybody lost + else { + var latest: Long = bitset + val base: Int = width * char + + if (bitset == initial) { // special case when it is dense since it is simple + var string: Int = 0 + while (string < width) { + if (matrix(base + string) != c) + latest = latest ^ (1L << string) + string += 1 + } + } else { + var remaining: Long = bitset + while (remaining != 0L) { + val string: Int = java.lang.Long.numberOfTrailingZeros(remaining) + val bit: Long = 1L << string + if (matrix(base + string) != c) + latest = latest ^ bit + remaining = remaining ^ bit + } + } + + latest + } + + // excludes entries that are not the given exact length + def exact(bitset: Long, length: Int): Long = + if (length > height) 0L // too long + else { + var latest: Long = bitset + var remaining: Long = bitset + while (remaining != 0L) { + val string: Int = java.lang.Long.numberOfTrailingZeros(remaining) + val bit: Long = 1L << string + if (lengths(string) != length) + latest = latest ^ bit + remaining = remaining ^ bit + } + latest + } + + def first(bitset: Long): Int = + if (bitset == 0L) -1 + else resolve(java.lang.Long.numberOfTrailingZeros(bitset)) // never returns 64 +} \ No newline at end of file diff --git a/src/main/scala/co.blocke.scalajack/json2/package.scala b/src/main/scala/co.blocke.scalajack/json2/package.scala new file mode 100644 index 00000000..3bb815c2 --- /dev/null +++ b/src/main/scala/co.blocke.scalajack/json2/package.scala @@ -0,0 +1,5 @@ +package co.blocke.scalajack +package json2 + +val BUFFER_EXCEEDED: Char = 7 // Old "BELL" ASCII value, used as a marker when we've run off the end of the known world +val END_OF_STRING: Char = 3 \ No newline at end of file diff --git a/src/main/scala/co.blocke.scalajack/parser/Instruction.scala b/src/main/scala/co.blocke.scalajack/parser/Instruction.scala new file mode 100644 index 00000000..479eab24 --- /dev/null +++ b/src/main/scala/co.blocke.scalajack/parser/Instruction.scala @@ -0,0 +1,60 @@ +package co.blocke.scalajack +package parser + +import scala.collection.Factory + +enum Expects{ + case ExpectBoolean, ExpectString, ExpectLong, ExpectBigLong, ExpectBigDouble, ExpectDouble, ExpectList, ExpectClass, ExpectObject, ExpectTuple +} + +// Don't need Expects for: +// expectList: ListInstruction is the only acceptable instruction with arity-1 (1 paramter) +// expectObject: ObjectInstruction is the only taker here: 2 params [K,V] +// expectClass: ClassInstrunction is the only applicable value + + +sealed trait Instruction: + val expect: Expects + type Z + +// Expects function requires no parameters +abstract class SimpleInstruction[I,O]() extends Instruction: + type Z = O + val expect: Expects + def transform(in: Either[ParseError, I]): Either[ParseError, O] + +// Expects function requires a single parameter +abstract class ListInstruction[E,T](val elementInstruction: Instruction) extends Instruction: + type Z = T + def transform(in: Either[ParseError, List[elementInstruction.Z]]): Either[ParseError, T] + +abstract class ClassInstruction[T](val fieldInstructions: Map[String,Instruction]) extends Instruction: + type Z = T + def transform(in: Either[ParseError, Map[String,Any]]): Either[ParseError, T] + +//------------------------------------------------------------- + +case class BooleanInstruction() extends SimpleInstruction[Boolean,Boolean]: + val expect: Expects = Expects.ExpectBoolean + def transform(in: Either[ParseError, Boolean]): Either[ParseError, Boolean] = in + +case class CharInstruction() extends SimpleInstruction[String,Char]: + val expect: Expects = Expects.ExpectString + def transform(in: Either[ParseError, String]): Either[ParseError, Char] = in.map(_.head) + +case class IntInstruction() extends SimpleInstruction[Long,Int]: + val expect: Expects = Expects.ExpectLong + def transform(in: Either[ParseError, Long]): Either[ParseError, Int] = in.map(_.intValue) + +case class StringInstruction() extends SimpleInstruction[String,String]: + val expect: Expects = Expects.ExpectString + def transform(in: Either[ParseError, String]): Either[ParseError, String] = in + +case class SeqInstruction[E,T](override val elementInstruction: Instruction)(using Factory[E,T]) extends ListInstruction[E,T](elementInstruction): + val expect: Expects = Expects.ExpectList + inline def collectionConvert[X,Y](in:List[X])(using Factory[X,Y]) = in.to( summon[Factory[X,Y]] ) + def transform(in: Either[ParseError, List[elementInstruction.Z]]): Either[ParseError, T] = in.map(e => collectionConvert[E,T](e.asInstanceOf[List[E]])) + +case class ScalaClassInstruction[T](override val fieldInstructions: Map[String,Instruction], instantiator: Map[String,?]=>T) extends ClassInstruction[T](fieldInstructions): + val expect: Expects = Expects.ExpectClass + def transform(in: Either[ParseError, Map[String,Any]]): Either[ParseError,T] = in.map(instantiator(_)) diff --git a/src/main/scala/co.blocke.scalajack/parser/Parser.scala b/src/main/scala/co.blocke.scalajack/parser/Parser.scala new file mode 100644 index 00000000..97e9c8fd --- /dev/null +++ b/src/main/scala/co.blocke.scalajack/parser/Parser.scala @@ -0,0 +1,40 @@ +package co.blocke.scalajack +package parser + +import scala.collection.mutable.HashMap + +abstract class ParseError(message: String) extends Throwable(message) + + +trait Parser: + + inline def parse(inst: Instruction): Either[ParseError, inst.Z] = + (inst.expect match + case Expects.ExpectString => inst.asInstanceOf[SimpleInstruction[String,?]].transform(parseString()) + case Expects.ExpectLong => inst.asInstanceOf[SimpleInstruction[Long,?]].transform(parseLong()) + case Expects.ExpectBoolean => parseBoolean() + case Expects.ExpectList => + val z = inst.asInstanceOf[ListInstruction[?,inst.Z]] + z.transform(parseList(z.elementInstruction)) + case Expects.ExpectClass => + val z = inst.asInstanceOf[ScalaClassInstruction[inst.Z]] + z.transform(parseClass(z.fieldInstructions, HashMap.empty[String,Any])) // TODO: HashMap of default values, option:None, etc + case Expects.ExpectObject => Left(new json.JsonParseError("Unupported")) + case Expects.ExpectTuple => Left(new json.JsonParseError("Unupported")) + case Expects.ExpectDouble => Left(new json.JsonParseError("Unupported")) + case Expects.ExpectBigLong => Left(new json.JsonParseError("Unupported")) + case Expects.ExpectBigDouble => Left(new json.JsonParseError("Unupported")) + ).asInstanceOf[Either[ParseError,inst.Z]] + + + //----------------------------------------------------- + + // Parse the raw primitives + inline def parseBoolean(): Either[ParseError, Boolean] + inline def parseString(): Either[ParseError, String] + inline def parseLong(): Either[ParseError, Long] + def parseList(inst: Instruction): Either[ParseError, List[inst.Z]] + def parseClass(inst: Map[String,Instruction], fieldValues: HashMap[String,Any]): Either[ParseError, Map[String,Any]] + + //----------------------------------------------------- + diff --git a/src/main/scala/co.blocke.scalajack/parser/Reader.scalax b/src/main/scala/co.blocke.scalajack/parser/Reader.scalax new file mode 100644 index 00000000..ef89c4bf --- /dev/null +++ b/src/main/scala/co.blocke.scalajack/parser/Reader.scalax @@ -0,0 +1,33 @@ +package co.blocke.scalajack +package parser + +import scala.collection.Factory + +trait Parser + +/** + * A Reader's job is simply this: to accept as input raw data from a Parser in one of the basic parser data types + * and produce a well-typed result fit for consumption. Raw Parser types are: + * - String + * - Long + * - BigLong + * - Double + * - BigDouble + * - Object (differs from map in that the values, representing fields, will likely have different types) + * - List[T] + * - Map[K,V] + */ +trait Reader + +class CharReader() extends Reader: + inline def read(p: Parser, data: String): Char = data.head + +class StringReader() extends Reader: + inline def read(p: Parser, data: String): String = data + +case class IntReader() extends Reader: + inline def read(p: Parser, data: Long): Int = data.intValue + +case class ListReader() extends Reader: + inline def collectionConvert[T,U](in:List[T])(using Factory[T, U]) = in.to( summon[Factory[T,U]] ) + inline def read[E,T](p: Parser, data: List[E])(using Factory[E,T]): T = collectionConvert[E,T](data) diff --git a/src/main/scala/co.blocke.scalajack/run/Play.scala b/src/main/scala/co.blocke.scalajack/run/Play.scala index c5ef4a4a..04d069b7 100644 --- a/src/main/scala/co.blocke.scalajack/run/Play.scala +++ b/src/main/scala/co.blocke.scalajack/run/Play.scala @@ -6,14 +6,6 @@ import scala.jdk.CollectionConverters.* object RunMe extends App: - /* - val f = () => parser.expectLong - val f2 = () => parser.expectList[Long](() => parser.expectLong) - val r = parser.expectList[List[Long]](f2) - - println("R: " + r) - */ - given json.JsonConfig = json .JsonConfig() .copy(noneAsNull = true) @@ -21,37 +13,73 @@ object RunMe extends App: // .copy(enumsAsIds = '*') try - val v = Person("Greg", DIAMOND, Command(15), new Wrapper(-10)) - println("HERE: " + ScalaJack.write(v)) - println(RType.of[Wrapper].pretty) + import json2.* + import json.JsonParseError + + /* StringMatrix experiments.... + */ + + /* + val matrix = new StringMatrix(List("foo", "bar", "baz").toArray, Array(("boom", 0))) + val r = JsonReader("boom\" asdf ") + var i: Int = 0 + var bs: Long = matrix.initial + var c: Int = -1 + while { c = r.readEscapedString(); c != END_OF_STRING } do { + bs = matrix.update(bs, i, c) + i += 1 + } + bs = matrix.exact(bs, i) + println("HERE: " + matrix.first(bs)) + */ - // println(RType.of[Person[Int]]) + // val numList = """[["1","2,3"],["4","5"]]""" + // val dec = JsonDecoder[List[List[String]]] + // println(dec.decodeJson(numList)) - // println("HERE: " + ScalaJack.write(Person("Greg", Foom('z'), Some(Person("Lili", Foom('x'), None))))) + implicit val addrDecoder: JsonDecoder[Address] = ClassDecoder( + Array("street", "city", "state", "postal_code"), + Array(JsonDecoder[String], JsonDecoder[String], JsonDecoder[String], JsonDecoder[String]) + ) + implicit val friendDecoder: JsonDecoder[Friend] = ClassDecoder( + Array("name", "age", "email"), + Array(JsonDecoder[String], JsonDecoder[Int], JsonDecoder[String]) + ) + implicit val petDecoder: JsonDecoder[Pet] = ClassDecoder( + Array("name", "species", "age"), + Array(JsonDecoder[String], JsonDecoder[String], JsonDecoder[Int]) + ) + implicit val PersonDecoder: JsonDecoder[Person] = ClassDecoder( + Array("name", "age", "address", "email", "phone_numbers", "is_employed"), + Array(JsonDecoder[String], JsonDecoder[Int], JsonDecoder[Address], JsonDecoder[String], JsonDecoder[List[String]], JsonDecoder[Boolean]) + ) + implicit val RecordDecoder: JsonDecoder[Record] = ClassDecoder( + Array("person", "hobbies", "friends", "pets"), + Array(JsonDecoder[Person], JsonDecoder[List[String]], JsonDecoder[List[Friend]], JsonDecoder[List[Pet]]) + ) - // val now = System.currentTimeMillis() - // for i <- 1 to 10000000 do ScalaJack.write(List(List(1, 2, 3), List(4, 5, 6), List(7, 8, 9))) - // val now2 = System.currentTimeMillis() - // println("Macro-based: " + (now2 - now)) + val addr = """{"street":"319 Hampton Ct","city":"Coppell","state":"TX","postal_code":"75019"}""" + val dec2 = JsonDecoder[Address] + println(JsonDecoder[Record].decodeJson(jsData)) - // val person = Person("David", 16) - // val minor = MinorPerson.make(person) match - // case Right(r) => r - // case _ => throw new Exception("boom") + /* + import parser.* + import json.* - // val p = SampleNeo(NonEmptyString("Greg"), "MVP", minor) - // val js = ScalaJack.write(x) - // println(js) + // val inst = SeqInstruction[Int, List[Int]](IntInstruction()) + // val inst2 = SeqInstruction[List[Int], Set[Seq[Int]]](inst) + val js = """{"hey":"wowowow","age":57,"other":[1,2,3]}""" + // 0123456789012345678901234567890123456789012234567890 - /* - val x = Blah("foo", WeekDay.Fri) - val js = ScalaJack.write(x) - println(js) + println(ScalaJack.read[Record](jsData)) - val inst = ScalaJack.read[Blah](js) - println(inst) - */ + val jp = json.JsonParser3(jsData) + println(">>> " + jp.parse()) + + // val p = JsonParser2(js, JsonConfig()) + // println("Result: " + p.parse(inst2)) + */ catch { case t: Throwable => diff --git a/src/main/scala/co.blocke.scalajack/run/Record.scala b/src/main/scala/co.blocke.scalajack/run/Record.scala new file mode 100644 index 00000000..76b3f79f --- /dev/null +++ b/src/main/scala/co.blocke.scalajack/run/Record.scala @@ -0,0 +1,88 @@ +package co.blocke.scalajack +package run + +case class Person( + name: String, + age: Int, + address: Address, + email: String, + phone_numbers: List[String], + is_employed: Boolean +) + +case class Address( + street: String, + city: String, + state: String, + postal_code: String +) + +case class Friend( + name: String, + age: Int, + email: String +) + +case class Pet( + name: String, + species: String, + age: Int +) + +case class Record( + person: Person, + hobbies: List[String], + friends: List[Friend], + pets: List[Pet] +) + +case class Foo(name: String, age:Int) + +val jsData = + """{ + "person": { + "name": "John Doe", + "age": 30, + "address": { + "street": "123 Main Street", + "city": "Anytown", + "state": "CA", + "postal_code": "12345" + }, + "email": "john.doe@example.com", + "phone_numbers": [ + "555-555-5555", + "555-123-4567" + ], + "is_employed": true + }, + "hobbies": [ + "reading", + "swimming", + "traveling" + ], + "friends": [ + { + "name": "Jane Smith", + "age": 28, + "email": "jane.smith@example.com" + }, + { + "name": "Bob Johnson", + "age": 32, + "email": "bob.johnson@example.com" + } + ], + "pets": [ + { + "name": "Fido", + "species": "Dog", + "age": 5 + }, + { + "name": "Whiskers", + "species": "Cat", + "age": 3 + } + ] + }""" \ No newline at end of file diff --git a/src/main/scala/co.blocke.scalajack/run/Sample.scala b/src/main/scala/co.blocke.scalajack/run/Sample.scalax similarity index 96% rename from src/main/scala/co.blocke.scalajack/run/Sample.scala rename to src/main/scala/co.blocke.scalajack/run/Sample.scalax index dd7a650f..1a67dd21 100644 --- a/src/main/scala/co.blocke.scalajack/run/Sample.scala +++ b/src/main/scala/co.blocke.scalajack/run/Sample.scalax @@ -25,6 +25,8 @@ case class Foom[X](x: X) extends Miss[X] // case class Person[Y](name: String, age: Miss[Y], again: Option[Person[Y]]) +case class Greg(hey: String, age: Int) + case class Person[T](val name: String, val card: card, val msg: msg[T], meh: Wrapper): var thingy: String = "wow"