diff --git a/project.scala b/project.scala index 7aa42da..14a6f7f 100644 --- a/project.scala +++ b/project.scala @@ -17,12 +17,16 @@ //> using options -Ycheck:all //> using options -Yexplain-lowlevel -Yexplicit-nulls //> using options -Yshow-suppressed-errors -Yshow-var-bounds -//> using options -Wsafe-init -Werror -Wunused:all +// -Wsafe-init is disabled: the checker never terminates on this codebase's +// inline-derivation-heavy code (confirmed via thread dump stuck for 10+ min +// recursing through dotty.tools.dotc.transform.init.Semantic$$anon$1.traverse +// on TypeAccumulator.foldOver). Re-enable once upstream fixes this, or once +// derivation is scoped down enough for the checker to terminate. +//> using options -Werror -Wunused:all //> using options -Xmax-inlines 100 ////> using options -Xprint-suspension - //> using options -Yprofile-enabled -Yprofile-trace:debug.json //> using publish.organization io.github.halotukozak //> using publish.name mrpc diff --git a/src/mrpc/Fallback.scala b/src/mrpc/Fallback.scala new file mode 100644 index 0000000..314537d --- /dev/null +++ b/src/mrpc/Fallback.scala @@ -0,0 +1,10 @@ +package mrpc + +/** + * Wraps a value to lower its implicit priority below normal (unwrapped) instances — mirrors commons + * `Fallback[T]`. A typeclass companion that wants to ship a default instance without it out-competing + * a user-supplied one exposes a low-priority `fromFallback` given that unwraps a `Fallback[T]` instead + * of offering `T` directly at normal priority: a `given AsRaw[Raw, Real]` elsewhere in scope is always + * preferred, and `Fallback`'s own instance is only picked when nothing else resolves. + */ +final case class Fallback[+T](value: T) extends AnyVal diff --git a/src/mrpc/conv/AsRaw.scala b/src/mrpc/conv/AsRaw.scala index da6e3af..3afc8be 100644 --- a/src/mrpc/conv/AsRaw.scala +++ b/src/mrpc/conv/AsRaw.scala @@ -7,7 +7,7 @@ import scala.util.Try trait AsRaw[Raw, Real]: def asRaw(real: Real): Raw -object AsRaw: +object AsRaw extends AsRawLowPrio: def apply[Raw, Real](using instance: AsRaw[Raw, Real]): AsRaw[Raw, Real] = instance given identity[A]: AsRaw[A, A] = (a: A) => a @@ -32,3 +32,7 @@ object AsRaw: def makeLazy[Raw, Real](conv: => AsRaw[Raw, Real]): AsRaw[Raw, Real] = new AsRaw[Raw, Real]: private lazy val c = conv def asRaw(real: Real): Raw = c.asRaw(real) + +/** Lowest-priority instances, so a `Fallback[AsRaw[Raw, Real]]` never out-competes a normal given. */ +private[conv] trait AsRawLowPrio: + given fromFallback[Raw, Real](using fallback: Fallback[AsRaw[Raw, Real]]): AsRaw[Raw, Real] = fallback.value diff --git a/src/mrpc/conv/AsRawReal.scala b/src/mrpc/conv/AsRawReal.scala index 1bf0d91..1a7f293 100644 --- a/src/mrpc/conv/AsRawReal.scala +++ b/src/mrpc/conv/AsRawReal.scala @@ -18,9 +18,14 @@ object AsRawReal extends AsRawRealLowPrio: def asReal(raw: A): A = raw /** Low-priority instances so `fromSeparate` does not collide with `identity` when `Raw = Real`. */ -private[conv] trait AsRawRealLowPrio: +private[conv] trait AsRawRealLowPrio extends AsRawRealLowestPrio: given fromSeparate[Raw, Real]( using asRaw: AsRaw[Raw, Real], asReal: AsReal[Raw, Real], ): AsRawReal[Raw, Real] = AsRawReal.create(asRaw.asRaw, asReal.asReal) + +/** Lowest-priority instance, below `fromSeparate` too — `Fallback` is meant as a last resort. */ +private[conv] trait AsRawRealLowestPrio: + given fromFallback[Raw, Real](using fallback: mrpc.Fallback[AsRawReal[Raw, Real]]): AsRawReal[Raw, Real] = + fallback.value diff --git a/src/mrpc/conv/AsReal.scala b/src/mrpc/conv/AsReal.scala index c7b9329..ac18951 100644 --- a/src/mrpc/conv/AsReal.scala +++ b/src/mrpc/conv/AsReal.scala @@ -7,7 +7,7 @@ import scala.util.Try trait AsReal[Raw, Real]: def asReal(raw: Raw): Real -object AsReal: +object AsReal extends AsRealLowPrio: def apply[Raw, Real](using instance: AsReal[Raw, Real]): AsReal[Raw, Real] = instance given identity[A]: AsReal[A, A] = (a: A) => a @@ -32,3 +32,7 @@ object AsReal: def makeLazy[Raw, Real](conv: => AsReal[Raw, Real]): AsReal[Raw, Real] = new AsReal[Raw, Real]: private lazy val c = conv def asReal(raw: Raw): Real = c.asReal(raw) + +/** Lowest-priority instances, so a `Fallback[AsReal[Raw, Real]]` never out-competes a normal given. */ +private[conv] trait AsRealLowPrio: + given fromFallback[Raw, Real](using fallback: Fallback[AsReal[Raw, Real]]): AsReal[Raw, Real] = fallback.value diff --git a/src/mrpc/meta/RpcMetadataCompanion.scala b/src/mrpc/meta/RpcMetadataCompanion.scala index 3b31228..480984e 100644 --- a/src/mrpc/meta/RpcMetadataCompanion.scala +++ b/src/mrpc/meta/RpcMetadataCompanion.scala @@ -28,3 +28,6 @@ trait RpcMetadataCompanion[M[_]]: inline def materialize[Real: {Done.Of as done}](using made: Made.Of[M[Real]]): M[Real] = mrpc.derive.MetadataDerivation .impl[M, Real, done.Operations](done.operations, RpcNames.materialize[Real])(using summon, containsOnly.refl) + + /** Lowest priority so a `Fallback[M[Real]]` never out-competes a normal `given M[Real]`. Mirrors commons `MetadataCompanion.fromFallback`. */ + given fromFallback[Real](using fallback: mrpc.Fallback[M[Real]]): M[Real] = fallback.value diff --git a/test/mrpc/conv/FallbackSuite.scala b/test/mrpc/conv/FallbackSuite.scala new file mode 100644 index 0000000..6a44d67 --- /dev/null +++ b/test/mrpc/conv/FallbackSuite.scala @@ -0,0 +1,46 @@ +package mrpc.conv + +import mrpc.Fallback + +/** + * DIVERGENCES.md D17: `Fallback[T]` lowers an implicit's priority below normal givens. Proves both + * halves — a `Fallback`-wrapped instance is picked up when nothing else resolves, AND a normal given + * in scope wins over a `Fallback`-wrapped competitor instead of causing an ambiguity error. + */ +class FallbackSuite extends munit.FunSuite: + + // Distinct Raw/Real so `identity` can never apply and only `fromFallback` is a candidate. + final case class Wrapped(s: String) + + test("AsRaw: a Fallback-wrapped instance resolves when no normal given is in scope"): + given Fallback[AsRaw[String, Wrapped]] = Fallback((w: Wrapped) => w.s) + assertEquals(summon[AsRaw[String, Wrapped]].asRaw(Wrapped("x")), "x") + + test("AsRaw: a normal given wins over a Fallback-wrapped competitor, no ambiguity"): + given Fallback[AsRaw[String, Wrapped]] = Fallback((_: Wrapped) => "from-fallback") + given AsRaw[String, Wrapped] = (w: Wrapped) => "from-normal:" + w.s + assertEquals(summon[AsRaw[String, Wrapped]].asRaw(Wrapped("x")), "from-normal:x") + + test("AsReal: a Fallback-wrapped instance resolves when no normal given is in scope"): + given Fallback[AsReal[String, Wrapped]] = Fallback((s: String) => Wrapped(s)) + assertEquals(summon[AsReal[String, Wrapped]].asReal("x"), Wrapped("x")) + + test("AsReal: a normal given wins over a Fallback-wrapped competitor, no ambiguity"): + given Fallback[AsReal[String, Wrapped]] = Fallback((_: String) => Wrapped("from-fallback")) + given AsReal[String, Wrapped] = (s: String) => Wrapped("from-normal:" + s) + assertEquals(summon[AsReal[String, Wrapped]].asReal("x"), Wrapped("from-normal:x")) + + test("AsRawReal: a Fallback-wrapped instance resolves when no normal given/fromSeparate applies"): + given Fallback[AsRawReal[String, Wrapped]] = Fallback(AsRawReal.create[String, Wrapped](_.s, Wrapped(_))) + val instance = summon[AsRawReal[String, Wrapped]] + assertEquals(instance.asRaw(Wrapped("x")), "x") + assertEquals(instance.asReal("x"), Wrapped("x")) + + test("AsRawReal: fromSeparate (built from AsRaw+AsReal givens) wins over a Fallback competitor"): + given Fallback[AsRawReal[String, Wrapped]] = + Fallback(AsRawReal.create[String, Wrapped](_ => "from-fallback", _ => Wrapped("from-fallback"))) + given AsRaw[String, Wrapped] = (w: Wrapped) => "from-separate:" + w.s + given AsReal[String, Wrapped] = (s: String) => Wrapped("from-separate:" + s) + val instance = summon[AsRawReal[String, Wrapped]] + assertEquals(instance.asRaw(Wrapped("x")), "from-separate:x") + assertEquals(instance.asReal("x"), Wrapped("from-separate:x")) diff --git a/test/mrpc/meta/MetadataFallbackSuite.scala b/test/mrpc/meta/MetadataFallbackSuite.scala new file mode 100644 index 0000000..aa353c9 --- /dev/null +++ b/test/mrpc/meta/MetadataFallbackSuite.scala @@ -0,0 +1,24 @@ +package mrpc.meta + +import mrpc.Fallback +import mrpc.derive.SampleApi.SampleApi + +/** + * DIVERGENCES.md D17: `RpcMetadataCompanion.fromFallback` mirrors commons `MetadataCompanion.fromFallback` — + * a `Fallback[M[Real]]` resolves via implicit search when no normal `given M[Real]` is in scope, but a + * normal given still wins over it (no ambiguity). This class needs no steering annotations: neither test + * calls `.materialize`, so the constructor param is never read by the metadata macro. + */ +final case class SimpleMeta[T](tag: String) +object SimpleMeta extends RpcMetadataCompanion[SimpleMeta] + +class MetadataFallbackSuite extends munit.FunSuite: + + test("a Fallback-wrapped metadata instance resolves via implicit search when nothing else provides one"): + given Fallback[SimpleMeta[SampleApi]] = Fallback(SimpleMeta("from-fallback")) + assertEquals(summon[SimpleMeta[SampleApi]].tag, "from-fallback") + + test("a normal given wins over a Fallback-wrapped competitor, no ambiguity"): + given Fallback[SimpleMeta[SampleApi]] = Fallback(SimpleMeta("from-fallback")) + given SimpleMeta[SampleApi] = SimpleMeta("from-normal") + assertEquals(summon[SimpleMeta[SampleApi]].tag, "from-normal")