diff --git a/compiler/src/dotty/tools/dotc/typer/Deriving.scala b/compiler/src/dotty/tools/dotc/typer/Deriving.scala index ef77adf18626..202b4cb548e0 100644 --- a/compiler/src/dotty/tools/dotc/typer/Deriving.scala +++ b/compiler/src/dotty/tools/dotc/typer/Deriving.scala @@ -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 { @@ -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 @@ -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 // @@ -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 // @@ -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 } diff --git a/tests/neg/self-type-class-derives.scala b/tests/neg/self-type-class-derives.scala new file mode 100644 index 000000000000..f1e350416ed8 --- /dev/null +++ b/tests/neg/self-type-class-derives.scala @@ -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 diff --git a/tests/run/self-type-class-derives.scala b/tests/run/self-type-class-derives.scala new file mode 100644 index 000000000000..01ef704cd15e --- /dev/null +++ b/tests/run/self-type-class-derives.scala @@ -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")