Skip to content

Commit

Permalink
prelude: revert pending type encoding to an opaque type (#563)
Browse files Browse the repository at this point in the history
I'm porting the other modules to the new design in `kyo-prelude` but
there's a performance regression that I can't see how to avoid with the
new encoding in the current version of the Scala 3 compiler:

- Given the lack of specialization, the generated bytecode contains a
significant amount of unexpected `<` boxing. Fortunately, the JIT is
able to avoid most of these allocations but profiling sessions show `<`
allocations in several benchmarks. It isn't a bottleneck in the
benchmarks I analyzed but introduces some overhead.

- The compiler doesn't allow implementing interfaces with `AnyVal`
generic type parameters. As a workaround, I was planning to use a
wrapper class like in `MonadLawsTest` but the approach is too expensive
in integrations like in `kyo-sttp`. The wrapper class essentially
introduces a new allocation to all methods in
[KyoSttpMonad](https://github.com/getkyo/kyo/blob/0279f30163759d834b1793cbba5bcf7d08f9f0c3/kyo-sttp/shared/src/main/scala/kyo/internal/KyoSttpMonad.scala#L9),
which are heavily used by sttp's transformations. Given how critical the
integrations with sttp and tapir are, this regression doesn't seem
ideal.

As described in #531, the purpose of
the new `AnyVal` encoding was to allow arbitrary nesting of Kyo
computations. Although it's a nice usability improvement, the current
limitations of the Scala 3 compiler doesn't seem to make it worth it and
finalizing the migration to the new design seems a much higher priority.
I can create a ticket so we can try again later.

Related compiler tickets: scala/scala3#11264
scala/scala3#15532
  • Loading branch information
fwbrasil authored Aug 8, 2024
1 parent 0279f30 commit e92c44f
Show file tree
Hide file tree
Showing 23 changed files with 301 additions and 129 deletions.
20 changes: 12 additions & 8 deletions kyo-prelude/jvm/src/test/scala/kyo2/MonadLawsTest.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package kyo2

import Flat.unsafe.bypass
import kyo2.Result
import zio.Trace
import zio.prelude.Equal
Expand All @@ -11,7 +12,7 @@ import zio.test.laws.*

object MonadLawsTest extends ZIOSpecDefault:

case class Myo[+A](v: A < (Env[String] & Abort[String] & Emit[Int] & Var[Boolean]))
type Myo[+A] = A < (Env[String] & Abort[String] & Emit[Int] & Var[Boolean])

val listGenF: GenF[Any, Myo] =
new GenF[Any, Myo]:
Expand Down Expand Up @@ -48,30 +49,33 @@ object MonadLawsTest extends ZIOSpecDefault:
_ <- Abort.when(s.length() > 10)("length exceeded")
yield v
)
).map(Myo(_))
)
end apply

given CovariantDeriveEqualIdentityFlatten[Myo] =
new CovariantDeriveEqualIdentityFlatten[Myo]:
override def flatten[A](ffa: Myo[Myo[A]]): Myo[A] =
Myo(ffa.v.flatMap(_.v))
ffa.flatten
override def any: Myo[Any] =
Myo(())
()
override def map[A, B](f: A => B): Myo[A] => Myo[B] =
m => Myo[B](m.v.map(f(_)))
_.map(f(_))
override def derive[A: Equal]: Equal[Myo[A]] =
new Equal[Myo[A]]:
protected def checkEqual(l: Myo[A], r: Myo[A]): Boolean =
def run(m: Myo[A]): Result[String, A] =
Var.run(true)(
Emit.run(
Abort.run(
Env.run("test")(m.v)
Env.run("test")(m)
)
)
).eval._2

run(l).equals(run(r))
(run(l), run(r)) match
case (Result.Success(l), Result.Success(r)) => summon[Equal[A]].equal(l, r)
case (Result.Fail(l), Result.Fail(r)) => l == r
case _ => false
end match
end checkEqual

def spec = suite("MonadLawsTest")(
Expand Down
8 changes: 4 additions & 4 deletions kyo-prelude/jvm/src/test/scala/kyo2/kernel/BytecodeTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,22 @@ class BytecodeTest extends Test:

"suspend" in {
val map = methodBytecodeSize[TestSuspend]
assert(map == Map("test" -> 20))
assert(map == Map("test" -> 16))
}

"suspendMap" in {
val map = methodBytecodeSize[TestSuspendMap]
assert(map == Map("test" -> 20))
assert(map == Map("test" -> 16))
}

"map" in {
val map = methodBytecodeSize[TestMap]
assert(map == Map("test" -> 24, "anonfun" -> 14, "mapLoop" -> 161))
assert(map == Map("test" -> 22, "anonfun" -> 10, "mapLoop" -> 138))
}

"handle" in {
val map = methodBytecodeSize[TestHandle]
assert(map == Map("test" -> 30, "anonfun" -> 15, "handleLoop" -> 267))
assert(map == Map("test" -> 20, "anonfun" -> 8, "handleLoop" -> 240))
}

def methodBytecodeSize[A](using ct: ClassTag[A]): Map[String, Int] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import scala.concurrent.Future

class SafepointTest extends Test:

def fork[A](f: => A < Any): A < Any =
def fork[A: Flat](f: => A < Any): A < Any =
Effect.defer {
val future = Future(f.eval)
Await.result(future, timeout.toScala): A
Expand Down
3 changes: 1 addition & 2 deletions kyo-prelude/shared/src/main/scala/kyo2/Abort.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package kyo2

import kernel.<
import kernel.Const
import kernel.Effect
import kernel.Frame
Expand Down Expand Up @@ -58,7 +57,7 @@ object Abort:
inline def get[E >: Nothing]: GetOps[E] = GetOps(())

final class RunOps[E >: Nothing](dummy: Unit) extends AnyVal:
def apply[A, S, ES, ER](v: => A < (Abort[E | ER] & S))(
def apply[A: Flat, S, ES, ER](v: => A < (Abort[E | ER] & S))(
using
ct: ClassTag[E],
tag: Tag[E], // TODO Used only to ensure E isn't a type union. There should be a more lightweight solution for this.
Expand Down
9 changes: 5 additions & 4 deletions kyo-prelude/shared/src/main/scala/kyo2/Emit.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ object Emit:
opaque type Ack = Int
object Ack:
given CanEqual[Ack, Ack] = CanEqual.derived
inline given Flat[Ack] = Flat.unsafe.bypass

extension (ack: Ack)
def maxItems(n: Int): Ack =
Expand Down Expand Up @@ -40,7 +41,7 @@ object Emit:
Effect.suspendMap[Any](tag, value)(f(_))

final class RunOps[V](dummy: Unit) extends AnyVal:
def apply[A, S](v: A < (Emit[V] & S))(using tag: Tag[Emit[V]], frame: Frame): (Chunk[V], A) < S =
def apply[A: Flat, S](v: A < (Emit[V] & S))(using tag: Tag[Emit[V]], frame: Frame): (Chunk[V], A) < S =
Effect.handle.state(tag, Chunk.empty[V], v)(
handle = [C] => (input, state, cont) => (state.append(input), cont(Ack.Continue())),
done = (state, res) => (state, res)
Expand All @@ -50,7 +51,7 @@ object Emit:
inline def run[V >: Nothing]: RunOps[V] = RunOps(())

final class RunFoldOps[V](dummy: Unit) extends AnyVal:
def apply[A, S, B, S2](acc: A)(f: (A, V) => A < S)(v: B < (Emit[V] & S2))(
def apply[A, S, B: Flat, S2](acc: A)(f: (A, V) => A < S)(v: B < (Emit[V] & S2))(
using
tag: Tag[Emit[V]],
frame: Frame
Expand All @@ -66,7 +67,7 @@ object Emit:
inline def runFold[V >: Nothing]: RunFoldOps[V] = RunFoldOps(())

final class RunDiscardOps[V](dummy: Unit) extends AnyVal:
def apply[A, S](v: A < (Emit[V] & S))(using tag: Tag[Emit[V]], frame: Frame): A < S =
def apply[A: Flat, S](v: A < (Emit[V] & S))(using tag: Tag[Emit[V]], frame: Frame): A < S =
Effect.handle(tag, v)(
handle = [C] => (input, cont) => cont(Ack.Stop)
)
Expand All @@ -75,7 +76,7 @@ object Emit:
inline def runDiscard[V >: Nothing]: RunDiscardOps[V] = RunDiscardOps(())

final class RunAckOps[V](dummy: Unit) extends AnyVal:
def apply[A, S, S2](v: A < (Emit[V] & S))(f: V => Ack < S2)(using tag: Tag[Emit[V]], frame: Frame): A < (S & S2) =
def apply[A: Flat, S, S2](v: A < (Emit[V] & S))(f: V => Ack < S2)(using tag: Tag[Emit[V]], frame: Frame): A < (S & S2) =
Effect.handle(tag, v)(
[C] => (input, cont) => f(input).map(cont)
)
Expand Down
4 changes: 2 additions & 2 deletions kyo-prelude/shared/src/main/scala/kyo2/Env.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ object Env:
inline def get[R](using inline tag: Tag[R])(using inline frame: Frame): R < Env[R] =
use[R](identity)

def run[R >: Nothing: Tag, A, S, VR](env: R)(v: A < (Env[R & VR] & S))(
def run[R >: Nothing: Tag, A: Flat, S, VR](env: R)(v: A < (Env[R & VR] & S))(
using
reduce: Reducible[Env[VR]],
frame: Frame
): A < (S & reduce.SReduced) =
runTypeMap(TypeMap(env))(v)

def runTypeMap[R >: Nothing, A, S, VR](env: TypeMap[R])(v: A < (Env[R & VR] & S))(
def runTypeMap[R >: Nothing, A: Flat, S, VR](env: TypeMap[R])(v: A < (Env[R & VR] & S))(
using
reduce: Reducible[Env[VR]],
frame: Frame
Expand Down
1 change: 0 additions & 1 deletion kyo-prelude/shared/src/main/scala/kyo2/Kyo.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package kyo2

import kernel.<
import kernel.Frame
import kernel.Loop
import scala.annotation.tailrec
Expand Down
1 change: 1 addition & 0 deletions kyo-prelude/shared/src/main/scala/kyo2/Maybe.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ opaque type Maybe[+A] >: (Empty | Defined[A]) = Empty | Defined[A]
object Maybe:
inline given [A, B](using inline ce: CanEqual[A, B]): CanEqual[Maybe[A], Maybe[B]] = CanEqual.derived
given [A]: Conversion[Maybe[A], IterableOnce[A]] = _.iterator
inline given [A]: Flat[Maybe[A]] = Flat.unsafe.bypass

def apply[A](v: A): Maybe[A] =
if isNull(v) then Empty
Expand Down
2 changes: 1 addition & 1 deletion kyo-prelude/shared/src/main/scala/kyo2/Memo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ object Memo:
}
end apply

def run[A, S](v: A < (Memo & S)): A < S =
def run[A: Flat, S](v: A < (Memo & S)): A < S =
Var.run(empty)(v)

end Memo
1 change: 1 addition & 0 deletions kyo-prelude/shared/src/main/scala/kyo2/Result.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ object Result:

inline given [E, A](using inline ce: CanEqual[A, A]): CanEqual[Result[E, A], Result[E, A]] = CanEqual.derived
given [E, A]: CanEqual[Result[E, A], Panic] = CanEqual.derived
inline given [E, A]: Flat[Result[E, A]] = Flat.unsafe.bypass

inline def apply[A](expr: => A): Result[Nothing, A] =
try
Expand Down
2 changes: 2 additions & 0 deletions kyo-prelude/shared/src/main/scala/kyo2/TypeMap.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import scala.collection.immutable.HashMap
opaque type TypeMap[+A] = HashMap[Tag[Any], Any]

object TypeMap:
inline given [A]: Flat[TypeMap[A]] = Flat.unsafe.bypass

extension [A](self: TypeMap[A])

private inline def fatal[B](using t: Tag[B]): Nothing =
Expand Down
6 changes: 3 additions & 3 deletions kyo-prelude/shared/src/main/scala/kyo2/Var.scala
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ object Var:
inline def updateDiscard[V](inline f: V => V)(using inline tag: Tag[Var[V]], inline frame: Frame): Unit < Var[V] =
Effect.suspendMap[Unit](tag, (v => f(v)): Update[V])(_ => ())

private inline def runWith[V, A, S, B, S2](state: V)(v: A < (Var[V] & S))(
private inline def runWith[V, A: Flat, S, B, S2](state: V)(v: A < (Var[V] & S))(
inline f: (V, A) => B < S2
)(using inline tag: Tag[Var[V]], inline frame: Frame): B < (S & S2) =
Effect.handle.state(tag, state, v)(
Expand All @@ -58,11 +58,11 @@ object Var:
)

/** Handles the effect and discards the 'Var' state. */
def run[V, A, S](state: V)(v: A < (Var[V] & S))(using Tag[Var[V]], Frame): A < S =
def run[V, A: Flat, S](state: V)(v: A < (Var[V] & S))(using Tag[Var[V]], Frame): A < S =
runWith(state)(v)((_, result) => result)

/** Handles the effect and returns a tuple with the final `Var` state and the computation's result. */
def runTuple[V, A, S](state: V)(v: A < (Var[V] & S))(using Tag[Var[V]], Frame): (V, A) < S =
def runTuple[V, A: Flat, S](state: V)(v: A < (Var[V] & S))(using Tag[Var[V]], Frame): (V, A) < S =
runWith(state)(v)((state, result) => (state, result))

object internal:
Expand Down
2 changes: 2 additions & 0 deletions kyo-prelude/shared/src/main/scala/kyo2/kernel/Context.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import kyo2.bug
opaque type Context = Map[Tag[Any], AnyRef]

object Context:
inline given Flat[Context] = Flat.unsafe.bypass

val empty: Context = Map.empty

extension (context: Context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,13 @@ object ContextEffect:
inline ifUndefined: A,
inline ifDefined: A => A
)(v: B < (E & S))(
using inline _frame: Frame
using
inline _frame: Frame,
inline flat: Flat[A]
): B < S =
def handleLoop(v: B < (E & S))(using Safepoint): B < S =
v match
case <(kyo: KyoSuspend[IX, OX, EX, Any, B, S] @unchecked) =>
case kyo: KyoSuspend[IX, OX, EX, Any, B, S] @unchecked =>
new KyoContinue[IX, OX, EX, Any, B, S](kyo):
def frame = _frame
def apply(v: OX[Any], context: Context)(using Safepoint) =
Expand All @@ -58,7 +60,7 @@ object ContextEffect:
else context.set(tag, ifDefined(context.get(tag)))
handleLoop(kyo(v, updated))
end apply
case <(kyo) =>
case kyo =>
kyo.asInstanceOf[B]
handleLoop(v)
end handle
Expand Down
Loading

0 comments on commit e92c44f

Please sign in to comment.