Skip to content
Open
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
57 changes: 54 additions & 3 deletions compiler/src/dotty/tools/dotc/typer/Deriving.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import util.Spans.*
import util.SrcPos
import collection.mutable.ListBuffer
import ErrorReporting.errorTree
import config.Feature

/** A typer mixin that implements type class derivation functionality */
trait Deriving {
Expand Down Expand Up @@ -68,8 +69,8 @@ trait Deriving {
*
* If it passes the checks, enter a type class instance for it in the current scope.
*
* See test run/typeclass-derivation2, run/poly-kinded-derives and pos/derive-eq
* for examples that spell out what would be generated.
* See test run/typeclass-derivation2, run/poly-kinded-derives, pos/derive-eq
* and run/self-type-class-derives for examples that spell out what would be generated.
*
* Note that the name of the derived method contains the name in the derives clause, not
* the underlying class name. This allows one to disambiguate derivations of type classes
Expand Down Expand Up @@ -103,6 +104,20 @@ trait Deriving {
addDerivedInstance(originalTypeClassTree, originalTypeClassType.typeSymbol.name, derivedInfo, derived.srcPos)
}

/** `tp is TC`, i.e. `TC { type Self = tp }`, for the self-based type class `typeClassType`. */
def selfRefined(tp: Type): Type = RefinedType(typeClassType, tpnme.Self, TypeAlias(tp))

def addSelfInstance(derivedParams: List[TypeSymbol], evidenceParamTypes: List[Type], instanceType: Type): Unit = {
val resultType = selfRefined(instanceType)
val monoInfo =
if evidenceParamTypes.isEmpty then resultType
else ImplicitMethodType(evidenceParamTypes.map(selfRefined), resultType)
val derivedInfo =
if derivedParams.isEmpty then monoInfo
else PolyType.fromParams(derivedParams, monoInfo)
addDerivedInstance(originalTypeClassTree, originalTypeClassType.typeSymbol.name, derivedInfo, derived.srcPos)
}

def deriveSingleParameter: Unit = {
// Single parameter type classes ... (a) and (b) above
//
Expand Down Expand Up @@ -186,6 +201,39 @@ trait Deriving {
cannotBeUnified
}

def deriveSelfParameter: Unit = {
// Self-based type classes, i.e. type classes of the form
//
// trait TC:
// type Self
// ...
//
// which are used via the `is` context-bound syntax (`X is TC`, aka `TC { type Self = X }`)
// rather than by being applied to a type argument (`TC[X]`). Since `TC` itself takes no
// type parameters, derivation instead pattern-matches on the abstract `Self` member.
//
// This is the `Self`-based analogue of case (b) in deriveSingleParameter above:
//
// Type class: TC (with abstract type member Self)
//
// ADT: C[A, B, C]
//
// given derived$TC[a, b, c] given (a is TC), (b is TC), (c is TC): (C[a, b, c] is TC)
//
// ADT: C (no type parameters)
//
// given derived$TC: (C is TC)
val clsType = cls.typeRef
val clsParams = cls.typeParams

if clsParams.exists(_.info.isLambdaSub) then
cannotBeUnified
else
val instanceType = clsType.appliedTo(clsParams.map(_.typeRef))
val evidenceParamTypes = clsParams.map(_.typeRef)
addSelfInstance(clsParams, evidenceParamTypes, instanceType)
}

def deriveCanEqual: Unit = {
// Specific derives rules for the CanEqual type class ... (c) above
//
Expand Down Expand Up @@ -252,7 +300,10 @@ trait Deriving {
if (typeClassArity == 1) deriveSingleParameter
else if (typeClass == defn.CanEqualClass) deriveCanEqual
else if (typeClassArity == 0)
report.error(em"type ${typeClass.name} in derives clause of ${cls.name} has no type parameters", derived.srcPos)
if Feature.enabled(Feature.modularity) && typeClassType.member(tpnme.Self).symbol.isAbstractOrParamType then
deriveSelfParameter
else
report.error(em"type ${typeClass.name} in derives clause of ${cls.name} has no type parameters", derived.srcPos)
else
cannotBeUnified
}
Expand Down
24 changes: 24 additions & 0 deletions tests/neg/self-type-class-derives.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
trait TC:
type Self
def apply(): Self

object TC:
def derived[X]: TC { type Self = X } = ???

// Without the `modularity` feature enabled, a self-based type class in a
// `derives` clause is rejected exactly as before this proposal.
case class NoModularity() derives TC // error

object WithModularity:
import scala.language.experimental.modularity

// Derivation succeeds here; both spellings of the resulting type refer
// to the same shape, `TC { type Self = Mono }`.
case class Mono() derives TC
val ok1: Mono is TC = summon[Mono is TC]
val ok2: TC { type Self = Mono } = summon[TC { type Self = Mono }]

// Higher-kinded ADT type parameters have no counterpart to unify against
// for a self-based type class (there is no case (a) here), so this fails
// exactly like an ordinary kind-* type class would.
case class HK[F[_]](fa: F[Int]) derives TC // error
27 changes: 27 additions & 0 deletions tests/run/self-type-class-derives.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//> using options -language:experimental.modularity

trait Named:
type Self
def name: String

object Named:
def derived[X]: Named { type Self = X } = new Named:
type Self = X
def name = "derived"

given intNamed: (Int is Named):
def name = "Int"

// Arity 0: no evidence needed.
case class Mono() derives Named

// Arity >= 1: one evidence instance per ADT type parameter, mirroring
// deriveSingleParameter's case (b) for ordinary kind-* type classes.
case class Poly[A](a: A) derives Named
case class Poly2[A, B](a: A, b: B) derives Named

@main def Test(): Unit =
assert(summon[Mono is Named].name == "derived")
assert(summon[Named { type Self = Int }].name == "Int")
assert(summon[Poly[Int] is Named].name == "derived")
assert(summon[Named { type Self = Poly2[Int, Int] }].name == "derived")
Loading