Slow-Auto, Inconvenient-Semi

escaping false dichotomy with sanely-automatic derivation


Mateusz Kubuszok

About me

Agenda

  • what is a type class

  • what is type class derivation

  • automatic and semi-automatic derivation a’la Circe

  • semi-automatic derivation a’la Jsoniter

  • sanely-automatic derivation a’la Chimney

  • does it matter to a library users how these approach differ

Examples

Link to examples

Type class

  • interface

  • with type paremeters

  • whose implementation can be automatically provided based on their type only

trait Encoder[A] {
  def apply(a: A): Json // <-- JSON as data
}
object Encoder {
  given encodeString: Encoder[String] = ...
  given encodeInt: Encoder[Int] = ...
  given encodeDouble: Encoder[Double] = ...
}
extension [A](value: A) {
  def asJson(using encoder: Encoder[A]): Json = encoder(value)
}
"value".asJson // using Encoder.encodeString
1024.asJson // using Encoder.encodeInt
3.13.asJson // using Encoder.encodeDouble

What if nobody wrote the implementation explicitly for my type?

case class Address(value: String)
case class User(name: String, address: Address)
Address("Paper St. 19").asJson // ???
User("John Smith", Address("Paper St. 19")).asJson // ???
No given instance of type Encoder[Address] was found for parameter encoder of
method asJson in object ...
No given instance of type Encoder[User] was found for parameter encoder of
method asJson in object ...

Type class derivation

Derivation
(If you don’t understand this diagram, you probably haven’t spend 600h on a topic that most sane people avoid.)

Derivation a’la Circe

trait Encoder[A] {
  def apply(a: A): Json // <-- JSON as data
}
extension [A](value: A) {
  def asJson(using encoder: Encoder[A]): Json = encoder(value)
}
case class Address(value: String)
case class User(name: String, address: Address)
Address("Paper St. 19")
// { "value": "Paper St. 19" }
User("John Smith", Address("Paper St. 19"))
// { "name": "John Smith", "address": { "value": "Paper St. 19" } }
import MagicImportOfSomethingThatCreatesEncoders.given

Address("Paper St. 19").asJson // generates Encoder[Address] on demand
User("John Smith", Address("Paper St. 19")).asJson // ditto but for User
import ImportOfSomethingThatLetsYouCreateEncoders.deriveEncoder

given addressEncoder: Encoder[Address] = deriveEncoder[Address]
given userEncoder: Encoder[User] = deriveEncoder[User]

Address("Paper St. 19").asJson // using addressEncoder
User("John Smith", Address("Paper St. 19")).asJson // using userEncoder

Automatic derivation of Address

implicitly[Encoder[Address]] // <-- using Encoder[Address]
Automatic Derivation of Address

Semi-automatic derivation of Address

deriveEncoder[Address] // <-- creates new Encoder[Address]
Semi-automatic Derivation of Address

Automatic derivation of User

implicitly[Encoder[User]] // <-- using Encoder[User]
Automatic Derivation of User

Semi-automatic derivation of User

deriveEncoder[User] // <-- creates new Encoder[User]
Semi-automatic Derivation of User

OK, but where is the code?

Wouldn’t it be easier to understand with some examples?

1. We are focusing on user-side of the derivation story

3. If you really need the derivation-internals-explanation-experience

3h later
3h later

Why people bother with semi-automatic derivation?

1. They want to make sure that they use the same implementation everywhere

2. "Speed"

// We're use Circe:
// trait Encoder[A] { ... } turns A -> Json
// trait Decoder[A] { ... } turns Json -> Either[Decoder.DecodingError, A]

case class Out(...) // <-- really big case class with nested case classes

// value -> Json -> value again
def roundTrip(out: Out): (Json, Either[Decoder.DecodingError, Out]) = {
  val json = out.asJson // <-- encode as Json using Encoder[Out]
  val parsed = json.as[Out] // <-- decode from Json using  Decoder[Out]
  json -> parsed
}
// Semi-automatic version will just have this:
implicit val in1Decoder: Decoder[In1] = deriveDecoder
implicit val in1Encoder: Encoder[In1] = deriveEncoder
implicit val in2Decoder: Decoder[In2] = deriveDecoder
implicit val in2Encoder: Encoder[In2] = deriveEncoder
implicit val in3Decoder: Decoder[In3] = deriveDecoder
implicit val in3Encoder: Encoder[In3] = deriveEncoder
implicit val in4Decoder: Decoder[In4] = deriveDecoder
implicit val in4Encoder: Encoder[In4] = deriveEncoder
implicit val in5Decoder: Decoder[In5] = deriveDecoder
implicit val in5Encoder: Encoder[In5] = deriveEncoder
implicit val outDecoder: Decoder[Out] = deriveDecoder
implicit val outEncoder: Encoder[Out] = deriveEncoder
// instead of automatic derivation import.

This shouldn’t be hard on compiler?

Json Compilation Times
(less is better)

Scala 2.13.14

[info] Benchmark                          Mode  Cnt   Score   Error   Units
[info] JsonRoundTrips.circeGenericAuto    thrpt  10   7.319 ± 0.011  ops/ms
[info] JsonRoundTrips.circeGenericSemi    thrpt  10   6.775 ± 0.013  ops/ms

Scala 3.3.3

[info] Benchmark                            Mode  Cnt   Score   Error   Units
[info] JsonRoundTrips.circeGenericAuto     thrpt   10   0.490 ± 0.432  ops/ms
[info] JsonRoundTrips.circeGenericSemi     thrpt   10   4.607 ± 0.014  ops/ms
(more is better)

Auto vs Semi on Scala 2

  • PR #5649 - Faster compilation of inductive implicits (closed)

  • PR #6481 - Topic/inductive implicits 2.13.x (closed)

  • PR #6580 - Prune polymorphic implicits more aggressively (merged)

  • PR #7012 - Speed up implicit resolution by avoiding allocations when traversing TypeRefs in core (merged)

  • and more

             1) baseline - scalac 2.13.x  2) scalac 2.13.x with matchesPtInst
 HList Size
  50          4                            3
 100          7                            3
 150         15                            4
 200         28                            4
 250         48                            5
 300         81                            6
 350        126                            8
 400        189                           11
 450        322                           13
 500        405                           16         Compile time in seconds

Could something else improve performance?

Magnolia

  • alternative to Shapeless/Mirrors

  • boasts about:

    • better API

    • better performance

    • better compilation times

    • better error messages when derivation fail

Error messages

Semi-automatic derivation

case class Street(name: Either[String, Nothing]) // <-- should not be able to derive name
case class Address(street: Street)
case class User(name: String, address: Address)
implicit val streetEncoder: Encoder[Street] = deriveEncoder
implicit val addressEncoder: Encoder[Address] = deriveEncoder
implicit val userEncoder: Encoder[User] = deriveEncoder

user.asJson

Shapeless' errors

could not find Lazy implicit value of type DerivedAsObjectEncoder[Street]
   implicit val streetEncoder: Encoder[Street] = deriveEncoder
                                                 ^

Mirrors' errors

  implicit val streetEncoder: Encoder[Street] = deriveEncoder
                                                ^^^^^^^^^^^^^
Failed to find an instance of Encoder[Either[String, Nothing]]

Magnolia’s errors

magnolia: could not find Encoder.Typeclass for type Either[String,Nothing]
     in parameter 'name' of product type Street
   implicit val streetEncoder: Encoder[Street] = EncoderSemi.derived
                                                             ^

Automatic derivation

case class Street(name: Either[String, Nothing])
case class Address(street: Street)
case class User(name: String, address: Address)

user.asJson

Shapeless/Mirrors/Magnolia

could not find implicit value for parameter encoder: Encoder[User]
     user.asJson
          ^

Round trip (reminder)

// Out - the outerermost of a deep nested, nasty case class structure
def roundTrip(out: Out): (Json, Result[Out]) = {
  val json = out.asJson // encode
  val parsed = json.as[Out] // decode
  json -> parsed
}
Json Compilation Times
(less is better)

Scala 2.13.14

[info] Benchmark                          Mode  Cnt   Score   Error   Units
[info] JsonRoundTrips.circeGenericAuto    thrpt  10   7.319 ± 0.011  ops/ms
[info] JsonRoundTrips.circeGenericSemi    thrpt  10   6.775 ± 0.013  ops/ms
[info] JsonRoundTrips.circeMagnoliaAuto   thrpt  10   7.689 ± 0.013  ops/ms
[info] JsonRoundTrips.circeMagnoliaSemi   thrpt  10   7.838 ± 0.013  ops/ms

Scala 3.3.3

[info] Benchmark                            Mode  Cnt   Score   Error   Units
[info] JsonRoundTrips.circeGenericAuto     thrpt   10   0.490 ± 0.432  ops/ms
[info] JsonRoundTrips.circeGenericSemi     thrpt   10   4.607 ± 0.014  ops/ms
[info] JsonRoundTrips.circeMagnoliaAuto    thrpt   10   0.077 ± 0.039  ops/ms
[info] JsonRoundTrips.circeMagnoliaSemi    thrpt   10   5.590 ± 0.013  ops/ms
(more is better)

Shapeless/Mirrors/Magnolia - different APIs, same approach.

Did anyone try something else?

Jsoniter Scala

  • prioritizes performance

  • no automatic derivation

  • no need to derive intermediate instances

How?

// Yes, only 1 codec, no need to manually derive implicits for nested cases
implicit val outCodec: JsonValueCodec[Out] =
  JsonCodecMaker.make(CodecMakerConfig.withAllowRecursiveTypes(true))

def roundTrip(out: Out): (String, Either[Throwable, Out]) = {
  val str = writeToString(out)
  val parsed = scala.util.Try(readFromString(str)).toEither
  str -> parsed
}

Recursive semi-automatic derivation

Recursive Macro Derivation

Recursive semi-automatic derivation

Derivation
  • delegates everything to implicit search

  • types supported OOTB are handled via implicits in companion object

Recursive Macro Derivation
  • use implicit search only for overrides

  • types supported OOTB are handled by macro, implicit scope is empty by default

OK, but what does this gibberish mean for users?

Json Compilation Times
(less is better)

Scala 2.13.14

[info] Benchmark                          Mode  Cnt   Score   Error   Units
[info] JsonRoundTrips.circeGenericAuto    thrpt  10   7.319 ± 0.011  ops/ms
[info] JsonRoundTrips.circeGenericSemi    thrpt  10   6.775 ± 0.013  ops/ms
[info] JsonRoundTrips.circeMagnoliaAuto   thrpt  10   7.689 ± 0.013  ops/ms
[info] JsonRoundTrips.circeMagnoliaSemi   thrpt  10   7.838 ± 0.013  ops/ms
[info] JsonRoundTrips.jsoniterScalaSemi   thrpt  10  20.081 ± 0.151  ops/ms

Scala 3.3.3

[info] Benchmark                            Mode  Cnt   Score   Error   Units
[info] JsonRoundTrips.circeGenericAuto     thrpt   10   0.490 ± 0.432  ops/ms
[info] JsonRoundTrips.circeGenericSemi     thrpt   10   4.607 ± 0.014  ops/ms
[info] JsonRoundTrips.circeMagnoliaAuto    thrpt   10   0.077 ± 0.039  ops/ms
[info] JsonRoundTrips.circeMagnoliaSemi    thrpt   10   5.590 ± 0.013  ops/ms
[info] JsonRoundTrips.jsoniterScalaSemi    thrpt   10  21.480 ± 0.070  ops/ms
(more is better)

But can it be automatic?

Automatic derivation a’la Chimney

Similar problem:

  • derivation should be recursive

  • macro should only use implicits for overrides

But:

  • automatic derivation should be available without breaking the 2 above

Solution

trait TypeClass[A] extends TypeClass.AutoDerived[A] { ... }
object TypeClass {

  // semi-automatic derivation of TypeClass[A]
  inline def derived[A]: TypeClass[A] = ${ derivedImpl[A] }

  trait AutoDerived[A] { ... }
  object AutoDerived extends AutoDerivedLowPriorityImplicits
  trait AutoDerivedLowPriorityImplicits {

    // automatic derivation of TypeClass.AutoDerived[A]
    inline given derived[A]: AutoDerived[A] = ${ derivedImpl[A] }
  }
}
extension [A](value: A)
  // uses TypeClass[A] defined by user manually or with TypeClass.derived,
  // falling back on automatic derivation
  def method(using TypeClass.AutoDerived[A]) = ...
// allowed to try summoning TypeClass[Sth].
// NOT allowed to try summoning TypeClass.AutoDerived[Sth]!
def derivedImpl[A: Type]: Expr[TypeClass[A]] = ...
(Disclaimer: understanding this code is not necessary to understand its implications on the next slides)
(Solutions for New Prioritization of Givens in Scala 3.7 in a moment)

Can we test it outside Chimney?

Yes.

Sanely-automatic derivation

I implemented wrapper around Jsoniter (on Scala 3-only) which works like this:

import jsonitersanely.* // <-- 1 import, like with std automatic derivation

def roundTrip(out: Out): (String, Either[Throwable, Out]) = {
  val str = write(out)
  val parsed = scala.util.Try(read[Out](str)).toEither
  str -> parsed
}

How does it compare to Circe or normal Jsoniter Scala?

Json Compilation Times
(less is better)

Scala 2.13.14

[info] Benchmark                          Mode  Cnt   Score   Error   Units
[info] JsonRoundTrips.circeGenericAuto    thrpt  10   7.319 ± 0.011  ops/ms
[info] JsonRoundTrips.circeGenericSemi    thrpt  10   6.775 ± 0.013  ops/ms
[info] JsonRoundTrips.circeMagnoliaAuto   thrpt  10   7.689 ± 0.013  ops/ms
[info] JsonRoundTrips.circeMagnoliaSemi   thrpt  10   7.838 ± 0.013  ops/ms
[info] JsonRoundTrips.jsoniterScalaSemi   thrpt  10  20.081 ± 0.151  ops/ms

Scala 3.3.3

[info] Benchmark                            Mode  Cnt   Score   Error   Units
[info] JsonRoundTrips.circeGenericAuto     thrpt   10   0.490 ± 0.432  ops/ms
[info] JsonRoundTrips.circeGenericSemi     thrpt   10   4.607 ± 0.014  ops/ms
[info] JsonRoundTrips.circeMagnoliaAuto    thrpt   10   0.077 ± 0.039  ops/ms
[info] JsonRoundTrips.circeMagnoliaSemi    thrpt   10   5.590 ± 0.013  ops/ms
[info] JsonRoundTrips.jsoniterScalaSemi    thrpt   10  21.480 ± 0.070  ops/ms
[info] JsonRoundTrips.jsoniterScalaSanely  thrpt   10  21.408 ± 0.070  ops/ms
(more is better)

But Jsoniter parsing String s vs Circe parsing Json might be apples vs oranges.

Can we have some more fair comparison?

More fair comparison

trait FastShowPretty[A] {

  def showPretty(
    value:   A,
    sb:      StringBuilder,
    indent:  String = "  ",
    nesting: Int = 0
  ): StringBuilder
}

implicit class FastShowPrettyOps[A](private val value: A) {

  def showPretty(indent: String = "  ", nesting: Int = 0)(
    implicit fsp: FastShowPretty[A]
  ): String =
    fsp.showPretty(value, new StringBuilder, indent, nesting).toString()
}
case class Street(name: String)
case class Address(street: Street)
case class User(name: String, address: Address)

println(User("John", Address(Street("Paper St"))).showPretty())
User(
  name = "John",
  address = Address(
    street = Street(
      name = "Paper St"
    )
  )
)
  • automatic and semi-automatic derivation using Shapeless (Scala 2)

  • automatic and semi-automatic derivation using Mirror s (Scala 3)

  • automatic and semi-automatic derivation using Magnolia (Scala 2 & 3)

  • sanely-automatic derivation with macros and Chimney macro commons (Scala 2 & 3)

Show Compilation Times
(less is better)

Scala 2.13.14

[info] Benchmark                                Mode  Cnt  Score   Error   Units
[info] ShowOutputs.showGenericProgrammingAuto  thrpt   10  2.651 ± 0.012  ops/ms
[info] ShowOutputs.showGenericProgrammingSemi  thrpt   10  2.829 ± 0.033  ops/ms
[info] ShowOutputs.showMagnoliaAuto            thrpt   10  3.621 ± 0.017  ops/ms
[info] ShowOutputs.showMagnoliaSemi            thrpt   10  3.745 ± 0.028  ops/ms
[info] ShowOutputs.showSanely                  thrpt   10  2.202 ± 0.359  ops/ms

Scala 3.3.3

[info] Benchmark                                Mode  Cnt  Score   Error   Units
[info] ShowOutputs.showGenericProgrammingAuto  thrpt   10  0.156 ± 0.013  ops/ms
[info] ShowOutputs.showGenericProgrammingSemi  thrpt   10  3.492 ± 0.013  ops/ms
[info] ShowOutputs.showMagnoliaAuto            thrpt   10  0.090 ± 0.023  ops/ms
[info] ShowOutputs.showMagnoliaSemi            thrpt   10  3.918 ± 0.012  ops/ms
[info] ShowOutputs.showSanely                  thrpt   10  2.204 ± 0.396  ops/ms
(more is better)

But wait.

Jsoniter had one more trick. It "caches" subroutines as def s.

Would that make a difference?

Show Compilation Times
(less is better)

Scala 2.13.14

[info] Benchmark                                Mode  Cnt  Score   Error   Units
[info] ShowOutputs.showGenericProgrammingAuto  thrpt   10  2.651 ± 0.012  ops/ms
[info] ShowOutputs.showGenericProgrammingSemi  thrpt   10  2.829 ± 0.033  ops/ms
[info] ShowOutputs.showMagnoliaAuto            thrpt   10  3.621 ± 0.017  ops/ms
[info] ShowOutputs.showMagnoliaSemi            thrpt   10  3.745 ± 0.028  ops/ms
[info] ShowOutputs.showSanely                  thrpt   10  4.811 ± 0.026  ops/ms

Scala 3.3.3

[info] Benchmark                                Mode  Cnt  Score   Error   Units
[info] ShowOutputs.showGenericProgrammingAuto  thrpt   10  0.156 ± 0.013  ops/ms
[info] ShowOutputs.showGenericProgrammingSemi  thrpt   10  3.492 ± 0.013  ops/ms
[info] ShowOutputs.showMagnoliaAuto            thrpt   10  0.090 ± 0.023  ops/ms
[info] ShowOutputs.showMagnoliaSemi            thrpt   10  3.918 ± 0.012  ops/ms
[info] ShowOutputs.showSanely                  thrpt   10  4.800 ± 0.042  ops/ms
(more is better)

Bonus: debugging

case class Street(name: Either[String, Nothing]) // <-- this should fail the derivation
case class Address(street: Street)
case class User(name: String, address: Address)

// scalacOptions += "-Xmacro-settings:fastshowpretty.logging=true"
def printObject(out: User): String = out.showPretty()
[error] .../ShowSanely.scala:12:54: Failed to derive showing for value : example.ShowSanely.User:
[error] No build-in support nor implicit for type scala.Nothing
[error]   def printObject(out: User): String = out.showPretty()
[error]                                                      ^
[info] .../ShowSanely.scala:12:54: Logs:
[info]  - Started derivation for value : example.ShowSanely.User
[info]  - Attempting rule ImplicitRule
[info]  - Skipped summoning example.showmacros.FastShowPretty[example.ShowSanely.User]
[info]  - Attempting rule CachedDefRule
[info]  - Attempting rule BuildInRule
[info]  - Attempting rule ProductRule
[info]  - Checking if def for example.ShowSanely.User exists
[info]  - Started deriving def for example.ShowSanely.User
[info]    - Started derivation for string : java.lang.String
[info]    - Attempting rule ImplicitRule
[info]    - Attempting rule CachedDefRule
[info]    - Attempting rule BuildInRule
[info]    - Successfully shown java.lang.String: sb.append("\"").append(string).append("\"")
[info]    - Started derivation for address : example.ShowSanely.Address
[info]    - Attempting rule ImplicitRule
[info]    - Attempting rule CachedDefRule
[info]    - Attempting rule BuildInRule
[info]    - Attempting rule ProductRule
[info]    - Checking if def for example.ShowSanely.Address exists
[info]    - Started deriving def for example.ShowSanely.Address
[info]      - Started derivation for street : example.ShowSanely.Street
[info]      - Attempting rule ImplicitRule
[info]      - Attempting rule CachedDefRule
[info]      - Attempting rule BuildInRule
[info]      - Attempting rule ProductRule
[info]      - Checking if def for example.ShowSanely.Street exists
[info]      - Started deriving def for example.ShowSanely.Street
[info]        - Started derivation for either : scala.util.Either[java.lang.String, scala.Nothing]
[info]        - Attempting rule ImplicitRule
[info]        - Attempting rule CachedDefRule
[info]        - Attempting rule BuildInRule
[info]        - Attempting rule ProductRule
[info]        - Attempting rule SumTypeRule
[info]        - Checking if def for scala.util.Either[java.lang.String, scala.Nothing] exists
[info]        - Started deriving def for scala.util.Either[java.lang.String, scala.Nothing]
[info]          - Started derivation for left : scala.util.Left[java.lang.String, scala.Nothing]
[info]          - Attempting rule ImplicitRule
[info]          - Attempting rule CachedDefRule
[info]          - Attempting rule BuildInRule
[info]          - Attempting rule ProductRule
[info]          - Checking if def for scala.util.Left[java.lang.String, scala.Nothing] exists
[info]          - Started deriving def for scala.util.Left[java.lang.String, scala.Nothing]
[info]            - Started derivation for string : java.lang.String
[info]            - Attempting rule ImplicitRule
[info]            - Attempting rule CachedDefRule
[info]            - Attempting rule BuildInRule
[info]            - Successfully shown java.lang.String: sb.append("\"").append(string).append("\"")
[info]          - Cached result of def for scala.util.Left[java.lang.String, scala.Nothing]
[info]          - Successfully shown scala.util.Left[java.lang.String, scala.Nothing]: show_nothing$u005D(left, nesting)
[info]          - Started derivation for right : scala.util.Right[java.lang.String, scala.Nothing]
[info]          - Attempting rule ImplicitRule
[info]          - Attempting rule CachedDefRule
[info]          - Attempting rule BuildInRule
[info]          - Attempting rule ProductRule
[info]          - Checking if def for scala.util.Right[java.lang.String, scala.Nothing] exists
[info]          - Started deriving def for scala.util.Right[java.lang.String, scala.Nothing]
[info]            - Started derivation for nothing : scala.Nothing
[info]            - Attempting rule ImplicitRule
[info]            - Attempting rule CachedDefRule
[info]            - Attempting rule BuildInRule
[info]          - Cached result of def for scala.util.Right[java.lang.String, scala.Nothing]
[info]        - Cached result of def for scala.util.Either[java.lang.String, scala.Nothing]
[info]      - Cached result of def for example.ShowSanely.Street
[info]    - Cached result of def for example.ShowSanely.Address
[info]  - Cached result of def for example.ShowSanely.User
[info]   def printObject(out: User): String = out.showPretty()
[info]                                                      ^

What changed?

Changes to givens (3.7.0)

// an example of mature, production-grade library using these stuffs:
import io.scalaland.chimney.dsl._
import io.scalaland.chimney.Transformer

case class Foo()
case class Bar(a: Int)
object Bar {
  given t: Transformer[Foo, Bar] = _ => Bar(0)
}

Foo().transformInto[Bar]
// before 3.7.0
Bar(0)
Now:
Ambiguous given instances: both given instance t in object Bar
and method deriveAutomatic in trait TransformerAutoDerivedCompanionPlatform
match type io.scalaland.chimney.Transformer.AutoDerived[Foo, Bar]
of parameter transformer of method transformInto
in package io.scalaland.chimney.dsl

However

// New utility!
Expr.summonIgnoring[A](symbolsVarArgs*)
// When looking for implicits
//   io.scalaland.chimney.Transformer.derive
// will be ignored - only user-provided implicits are considered!
Expr.summonIgnoring[Target](
  Symbol.classSymbol("io.scalaland.chimney.Transformer")
    .companionModule
    .methodMember("derive").head
)

So in general

trait TypeClass[A] {
  // methods
}
object TypeClass {

  given inline derive[A]: TypeClass[A] =
    ${ macros[A] } // Derive code recursively using Expr.summonignoring
                   // to avoid calling itself: users' implicits only!
}

Benchmarks for 2.13.20 vs 3.7.0-RC1

Show Compilation Times
(less is better)

Scala 2.13.20

[info] Benchmark                          Mode  Cnt   Score   Error   Units
[info] JsonRoundTrips.circeGenericAuto   thrpt   10   8.392 ± 0.137  ops/ms
[info] JsonRoundTrips.circeGenericSemi   thrpt   10   8.815 ± 0.121  ops/ms
[info] JsonRoundTrips.circeMagnoliaAuto  thrpt   10   9.469 ± 0.165  ops/ms
[info] JsonRoundTrips.circeMagnoliaSemi  thrpt   10   9.443 ± 0.143  ops/ms
[info] JsonRoundTrips.jsoniterScalaSemi  thrpt   10  24.615 ± 0.101  ops/ms

Scala 3.7.0-RC1

[info] Benchmark                            Mode  Cnt   Score   Error   Units
[info] JsonRoundTrips.circeGenericAuto     thrpt   10   3.394 ± 1.267  ops/ms
[info] JsonRoundTrips.circeGenericSemi     thrpt   10   8.047 ± 0.096  ops/ms
[info] JsonRoundTrips.circeMagnoliaAuto    thrpt   10   0.128 ± 0.062  ops/ms
[info] JsonRoundTrips.circeMagnoliaSemi    thrpt   10   6.176 ± 0.104  ops/ms
[info] JsonRoundTrips.jsoniterScalaSanely  thrpt   10  24.460 ± 0.149  ops/ms
[info] JsonRoundTrips.jsoniterScalaSemi    thrpt   10  24.396 ± 0.116  ops/ms
Show Compilation Times
(less is better)

Scala 2.13.20

[info] Benchmark                                Mode  Cnt  Score   Error   Units
[info] ShowOutputs.showGenericProgrammingAuto  thrpt   10  5.584 ± 0.067  ops/ms
[info] ShowOutputs.showGenericProgrammingSemi  thrpt   10  5.953 ± 0.057  ops/ms
[info] ShowOutputs.showMagnoliaAuto            thrpt   10  6.702 ± 0.106  ops/ms
[info] ShowOutputs.showMagnoliaSemi            thrpt   10  6.605 ± 0.289  ops/ms
[info] ShowOutputs.showSanely                  thrpt   10  8.201 ± 0.078  ops/ms

Scala 3.7.0-RC1

[info] Benchmark                                Mode  Cnt  Score   Error   Units
[info] ShowOutputs.showGenericProgrammingAuto  thrpt   10  3.687 ± 1.588  ops/ms
[info] ShowOutputs.showGenericProgrammingSemi  thrpt   10  6.291 ± 0.062  ops/ms
[info] ShowOutputs.showMagnoliaAuto            thrpt   10  0.265 ± 0.194  ops/ms
[info] ShowOutputs.showMagnoliaSemi            thrpt   10  8.316 ± 0.067  ops/ms
[info] ShowOutputs.showSanely                  thrpt   10  9.191 ± 0.081  ops/ms

Summary

Thank you!