Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions project.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions src/mrpc/Fallback.scala
Original file line number Diff line number Diff line change
@@ -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
6 changes: 5 additions & 1 deletion src/mrpc/conv/AsRaw.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
7 changes: 6 additions & 1 deletion src/mrpc/conv/AsRawReal.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 5 additions & 1 deletion src/mrpc/conv/AsReal.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
3 changes: 3 additions & 0 deletions src/mrpc/meta/RpcMetadataCompanion.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
46 changes: 46 additions & 0 deletions test/mrpc/conv/FallbackSuite.scala
Original file line number Diff line number Diff line change
@@ -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"))
24 changes: 24 additions & 0 deletions test/mrpc/meta/MetadataFallbackSuite.scala
Original file line number Diff line number Diff line change
@@ -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")