Skip to content

Commit

Permalink
Optimize TotalWrapper
Browse files Browse the repository at this point in the history
Because of the use of evidence, the types for TotalWrapper
are not being elided, leading to 10-15 JVM instruction methods
for OpaqueInt::atMost.  This is especially problematic because these
methods are tagged 'inline', so they are significantly increasing
size of the methods which use OpaqueInts.

I've verified these changes now correctly elide types entirely,
but this implementation is extremely fragile. Because scala sees
evidence for conversion, it will implicitly use the evidence given
a chance, which we don't want.

The best I could come up with is a scary warning. :-\
  • Loading branch information
isaacl committed Oct 14, 2024
1 parent acc5262 commit ec0466a
Showing 1 changed file with 47 additions and 42 deletions.
89 changes: 47 additions & 42 deletions core/src/main/scala/newtypes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,23 @@ import alleycats.Zero
// thanks Anton!
// https://github.com/indoorvivants/opaque-newtypes/blob/main/modules/core/src/main/scala/OpaqueNewtypes.scala

// WARNING: This implementation below is fragile and seemingly small changes can significantly
// degrade performance. In particular, do not add any methods which implicitly use the witness to perform
// type conversions. Only use `raw` and `apply` (or the extension methods) to convert types.

// Details: The classes use witnesses to ensure type compatibility. However, in order to completely inline
// simple methods like `<` (OpaqueInt), we cannot use the witness, and instead "cast" using asInstanceOf.
// scala will remove these redudant casts at compile time, leaving the types completely elided.
// The issue for new code is that scala will happily use a witness implicitly to convert types, so it's
// up to you to ensure the witness is not used (by using `raw`, `apply`, or the extension methods).

// Why we can't test it:
// - The scala class `=:=` is sealed and impossible to subclass, so we cannot create a mock which raises
// exceptions on use.
// - It's possible to compile code, decompile it, and inspect bytecode but this requires a hell of a lot of
// machinery. See github.com/scala/scala3/blob/main/compiler/test/dotty/tools/backend/jvm/ArrayApplyOptTest.scala
// as an example of what would be required. (it's a lot -- compiling to disk, finding the right class files,
// interpreting bytecode, ignoring irrelevant differences, etc.)
object newtypes:

@FunctionalInterface
Expand All @@ -23,36 +40,36 @@ object newtypes:
type IntRuntime[A] = SameRuntime[A, Int]
type DoubleRuntime[A] = SameRuntime[A, Double]

trait TotalWrapper[Newtype, Impl](using ev: Newtype =:= Impl):
inline def raw(inline a: Newtype): Impl = a
inline def apply(inline s: Impl): Newtype = s.asInstanceOf[Newtype]
inline def from[M[_]](inline f: M[Impl]): M[Newtype] = f.asInstanceOf[M[Newtype]]
inline def from[M[_], B](using sr: SameRuntime[B, Impl])(inline f: M[B]): M[Newtype] =
abstract class TotalWrapper[Newtype, Impl](using Newtype =:= Impl):
inline final def raw(inline a: Newtype): Impl = a.asInstanceOf[Impl]
inline final def apply(inline s: Impl): Newtype = s.asInstanceOf[Newtype]
inline final def from[M[_]](inline f: M[Impl]): M[Newtype] = f.asInstanceOf[M[Newtype]]
inline final def from[M[_], B](using sr: SameRuntime[B, Impl])(inline f: M[B]): M[Newtype] =
f.asInstanceOf[M[Newtype]]
inline def from[M[_], B](inline other: TotalWrapper[B, Impl])(inline f: M[B]): M[Newtype] =
inline final def from[M[_], B](inline other: TotalWrapper[B, Impl])(inline f: M[B]): M[Newtype] =
f.asInstanceOf[M[Newtype]]
inline def raw[M[_]](inline f: M[Newtype]): M[Impl] = f.asInstanceOf[M[Impl]]
inline final def raw[M[_]](inline f: M[Newtype]): M[Impl] = f.asInstanceOf[M[Impl]]

given SameRuntime[Newtype, Impl] = identity
given SameRuntime[Impl, Newtype] = _.asInstanceOf[Newtype]
given (using Eq[Impl]): Eq[Newtype] = Eq.by(_.value)
given SameRuntime[Newtype, Impl] = raw(_)
given SameRuntime[Impl, Newtype] = apply(_)
given (using e: Eq[Impl]): Eq[Newtype] = new Eq[Newtype]:
override def eqv(x: Newtype, y: Newtype) = e.eqv(raw(x), raw(y))

extension (a: Newtype)
inline def value: Impl = a
inline def into[X](inline other: TotalWrapper[X, Impl]): X = other.apply(a)
inline def map(inline f: Impl => Impl): Newtype = apply(f(a))
extension (inline a: Newtype)
inline def value: Impl = raw(a)
inline def into[X](inline other: TotalWrapper[X, Impl]): X = other.apply(raw(a))
inline def map(inline f: Impl => Impl): Newtype = apply(f(raw(a)))
end TotalWrapper

trait FunctionWrapper[Newtype, Impl](using ev: Newtype =:= Impl) extends TotalWrapper[Newtype, Impl]:
extension (a: Newtype) inline def apply: Impl = a
abstract class FunctionWrapper[Newtype, Impl](using Newtype =:= Impl) extends TotalWrapper[Newtype, Impl]:
extension (a: Newtype) inline def apply: Impl = a.asInstanceOf[Impl]

trait OpaqueString[A](using A =:= String) extends TotalWrapper[A, String]:
abstract class OpaqueString[A](using A =:= String) extends TotalWrapper[A, String]:
given Show[A] = _.value
given Render[A] = _.value

trait OpaqueInt[A](using A =:= Int) extends TotalWrapper[A, Int]:
abstract class OpaqueInt[A](using A =:= Int) extends TotalWrapper[A, Int]:
extension (inline a: A)
inline def unary_- : A = apply(-raw(a))
inline infix def >(inline o: Int): Boolean = raw(a) > o
inline infix def <(inline o: Int): Boolean = raw(a) < o
inline infix def >=(inline o: Int): Boolean = raw(a) >= o
Expand All @@ -70,7 +87,7 @@ object newtypes:
inline def atLeast(inline bot: A): A = atLeast(raw(bot))
inline def atMost(inline top: A): A = atMost(raw(top))

trait OpaqueIntSafer[A](using A =:= Int) extends TotalWrapper[A, Int]:
abstract class OpaqueIntSafer[A](using A =:= Int) extends TotalWrapper[A, Int]:
extension (inline a: A)
inline def unary_- : A = apply(-raw(a))
inline infix def >(inline o: A): Boolean = raw(a) > raw(o)
Expand All @@ -82,33 +99,21 @@ object newtypes:
inline def atLeast(inline bot: A): A = apply(Math.max(raw(a), raw(bot)))
inline def atMost(inline top: A): A = apply(Math.min(raw(a), raw(top)))

trait OpaqueLong[A](using A =:= Long) extends TotalWrapper[A, Long]
trait OpaqueDouble[A](using A =:= Double) extends TotalWrapper[A, Double]:
abstract class OpaqueLong[A](using A =:= Long) extends TotalWrapper[A, Long]
abstract class OpaqueDouble[A](using A =:= Double) extends TotalWrapper[A, Double]:
extension (inline a: A) inline def +(inline o: Int): A = apply(raw(a) + o)
trait OpaqueFloat[A](using A =:= Float) extends TotalWrapper[A, Float]
abstract class OpaqueFloat[A](using A =:= Float) extends TotalWrapper[A, Float]

import scala.concurrent.duration.FiniteDuration
trait OpaqueDuration[A](using A =:= FiniteDuration) extends TotalWrapper[A, FiniteDuration]

abstract class YesNo[A](using ev: Boolean =:= A):
val Yes: A = true
val No: A = false

inline def from[M[_]](inline a: M[Boolean]): M[A] = a.asInstanceOf[M[A]]

given SameRuntime[A, Boolean] = _ == Yes
given SameRuntime[Boolean, A] = if _ then Yes else No
given Eq[A] = Eq.by(_.value)

inline def apply(inline b: Boolean): A = b
abstract class OpaqueDuration[A](using A =:= FiniteDuration) extends TotalWrapper[A, FiniteDuration]

abstract class YesNo[A](using A =:= Boolean) extends TotalWrapper[A, Boolean]:
extension (inline a: A)
inline def value: Boolean = a == Yes
inline def flip: A = if value then No else Yes
inline def yes: Boolean = value
inline def no: Boolean = !value
inline def &&(inline other: A) = a.value && other.value
inline def `||`(inline other: A) = a.value || other.value
inline def flip: A = apply(!raw(a))
inline def yes: Boolean = raw(a)
inline def no: Boolean = !raw(a)
inline def &&(inline other: A): A = apply(raw(a) && raw(other))
inline def `||`(inline other: A): A = apply(raw(a) || raw(other))
end YesNo

inline def sameOrdering[A, T](using bts: SameRuntime[T, A], ord: Ordering[A]): Ordering[T] =
Expand Down

0 comments on commit ec0466a

Please sign in to comment.