Skip to content

Commit

Permalink
Minimal support for dependent case classes
Browse files Browse the repository at this point in the history
This lets us write:

    trait A:
      type B

    case class CC(a: A, b: a.B)

Pattern matching works but isn't dependent yet:

    x match
      case CC(a, b) =>
        val a1: A = a
        // Dependent pattern matching is not currently supported
        // val b1: a1.B = b
        val b1 = b // Type is CC#a.B

(for my usecase this isn't a problem, I'm working on a type constraint API
which lets me write things like `case class CC(a: Int, b: Int
GreaterThan[a.type])`)

Because case class pattern matching relies on the product selectors `_N`, making
it dependent is a bit tricky, currently we generate:

    case class CC(a: A, b: a.B):
      def _1: A = a
      def _2: a.B = b

So the type of `_2` is not obviously related to the type of `_1`, we probably
need to change what we generate into:

    case class CC(a: A, b: a.B):
      @uncheckedStable def _1: a.type = a
      def _2: _1.B = b

But this can be done in a separate PR.

Fixes #8073.
  • Loading branch information
smarter committed Oct 3, 2024
1 parent afee285 commit 171aafe
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 44 deletions.
76 changes: 45 additions & 31 deletions compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -498,53 +498,67 @@ class SyntheticMembers(thisPhase: DenotTransformer) {
/** The class
*
* ```
* case class C[T <: U](x: T, y: String*)
* trait U:
* type Elem
*
* case class C[T <: U](a: T, b: a.Elem, c: String*)
* ```
*
* gets the `fromProduct` method:
*
* ```
* def fromProduct(x$0: Product): MirroredMonoType =
* new C[U](
* x$0.productElement(0).asInstanceOf[U],
* x$0.productElement(1).asInstanceOf[Seq[String]]: _*)
* val a$1 = x$0.productElement(0).asInstanceOf[U]
* val b$1 = x$0.productElement(1).asInstanceOf[a$1.Elem]
* val c$1 = x$0.productElement(2).asInstanceOf[Seq[String]]
* new C[U](a$1, b$1, c$1*)
* ```
* where
* ```
* type MirroredMonoType = C[?]
* ```
*/
def fromProductBody(caseClass: Symbol, param: Tree, optInfo: Option[MirrorImpl.OfProduct])(using Context): Tree =
def extractParams(tpe: Type): List[Type] =
tpe.asInstanceOf[MethodType].paramInfos

def computeFromCaseClass: (Type, List[Type]) =
val (baseRef, baseInfo) =
val rawRef = caseClass.typeRef
val rawInfo = caseClass.primaryConstructor.info
optInfo match
case Some(info) =>
(rawRef.asSeenFrom(info.pre, caseClass.owner), rawInfo.asSeenFrom(info.pre, caseClass.owner))
case _ =>
(rawRef, rawInfo)
baseInfo match
def fromProductBody(caseClass: Symbol, productParam: Tree, optInfo: Option[MirrorImpl.OfProduct])(using Context): Tree =
val classRef = optInfo match
case Some(info) => TypeRef(info.pre, caseClass)
case _ => caseClass.typeRef
val newPrefix =
val constr = TermRef(classRef, caseClass.primaryConstructor)
(constr.info: @unchecked) match
case tl: PolyType =>
val tvars = constrained(tl)
val targs = for tvar <- tvars yield
tvar.instantiate(fromBelow = false)
(baseRef.appliedTo(targs), extractParams(tl.instantiate(targs)))
case methTpe =>
(baseRef, extractParams(methTpe))
end computeFromCaseClass

val (classRefApplied, paramInfos) = computeFromCaseClass
val elems =
for ((formal, idx) <- paramInfos.zipWithIndex) yield
val elem =
param.select(defn.Product_productElement).appliedTo(Literal(Constant(idx)))
.ensureConforms(formal.translateFromRepeated(toArray = false))
if (formal.isRepeatedParam) ctx.typer.seqToRepeated(elem) else elem
New(classRefApplied, elems)
AppliedType(classRef, targs)
case mt: MethodType =>
classRef

// The MethodType of the constructor after applying it to the widest type arguments.
val constrMeth = TermRef(newPrefix, caseClass.primaryConstructor).info.asInstanceOf[MethodType]

// Create symbols for the vals corresponding to each parameter
// If there are dependent parameters, the infos won't be correct yet.
val bindingSyms = constrMeth.paramRefs.map: pref =>
newSymbol(ctx.owner, pref.paramName.freshened, Synthetic,
pref.underlying.translateFromRepeated(toArray = false), coord = productParam.span.toSynthetic)
val bindingRefs = bindingSyms.map(TermRef(NoPrefix, _))
// Fix the info for dependent parameters
if constrMeth.isParamDependent then
bindingSyms.foreach: bindingSym =>
bindingSym.info = bindingSym.info.substParams(constrMeth, bindingRefs)

val bindingDefs = bindingSyms.zipWithIndex.map: (bindingSym, idx) =>
ValDef(bindingSym,
productParam.select(defn.Product_productElement).appliedTo(Literal(Constant(idx)))
.ensureConforms(bindingSym.info))

val newArgs = bindingRefs.lazyZip(constrMeth.paramInfos).map: (bindingRef, paramInfo) =>
val refTree = ref(bindingRef)
if paramInfo.isRepeatedParam then ctx.typer.seqToRepeated(refTree) else refTree
Block(
bindingDefs,
New(newPrefix, newArgs)
)
end fromProductBody

/** For an enum T:
Expand Down
14 changes: 1 addition & 13 deletions compiler/src/dotty/tools/dotc/typer/Namer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1925,9 +1925,7 @@ class Namer { typer: Typer =>
if isConstructor then
// set result type tree to unit, but take the current class as result type of the symbol
typedAheadType(ddef.tpt, defn.UnitType)
val mt = wrapMethType(effectiveResultType(sym, paramSymss))
if sym.isPrimaryConstructor then checkCaseClassParamDependencies(mt, sym.owner)
mt
wrapMethType(effectiveResultType(sym, paramSymss))
else if sym.isAllOf(Given | Method) && Feature.enabled(modularity) then
// set every context bound evidence parameter of a given companion method
// to be tracked, provided it has a type that has an abstract type member.
Expand Down Expand Up @@ -1976,16 +1974,6 @@ class Namer { typer: Typer =>
ddef.trailingParamss.foreach(completeParams)
end completeTrailingParamss

/** Checks an implementation restriction on case classes. */
def checkCaseClassParamDependencies(mt: Type, cls: Symbol)(using Context): Unit =
mt.stripPoly match
case mt: MethodType if cls.is(Case) && mt.isParamDependent =>
// See issue #8073 for background
report.error(
em"""Implementation restriction: case classes cannot have dependencies between parameters""",
cls.srcPos)
case _ =>

/** Under x.modularity, we add `tracked` to context bound witnesses
* that have abstract type members
*/
Expand Down
59 changes: 59 additions & 0 deletions tests/pos/i8073.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import scala.deriving.Mirror

trait A:
type B

case class CC(a: A, b: a.B)

enum Enum:
case EC(a: A, b: a.B)

def test1(): Unit =
val generic = summon[Mirror.Of[CC]]
// No language syntax for type projection of a singleton type
// summon[generic.MirroredElemTypes =:= (A, CC#a.B)]

val dependent = summon[Mirror.Of[x.type]]
summon[dependent.MirroredElemTypes =:= (A, x.a.B)]

val aa: A { type B = Int } = new A { type B = Int }
val x: CC { val a: aa.type } = CC(aa, 1).asInstanceOf[CC { val a: aa.type }] // manual `tracked`

assert(CC(aa, 1) == generic.fromProduct((aa, 1)))
assert(CC(aa, 1) == dependent.fromProduct((aa, 1)))

x match
case CC(a, b) =>
val a1: A = a
// Dependent pattern matching is not currently supported
// val b1: a1.B = b
val b1 = b // Type is CC#a.B

end test1

def test2(): Unit =
val generic = summon[Mirror.Of[Enum.EC]]
// No language syntax for type projection of a singleton type
// summon[generic.MirroredElemTypes =:= (A, Enum.EC#a.B)]

val dependent = summon[Mirror.Of[x.type]]
summon[dependent.MirroredElemTypes =:= (A, x.a.B)]

val aa: A { type B = Int } = new A { type B = Int }
val x: Enum.EC { val a: aa.type } = Enum.EC(aa, 1).asInstanceOf[Enum.EC { val a: aa.type }] // manual `tracked`

assert(Enum.EC(aa, 1) == generic.fromProduct((aa, 1)))
assert(Enum.EC(aa, 1) == dependent.fromProduct((aa, 1)))

x match
case Enum.EC(a, b) =>
val a1: A = a
// Dependent pattern matching is not currently supported
// val b1: a1.B = b
val b1 = b // Type is Enum.EC#a.B

end test2

@main def Test =
test1()
test2()
60 changes: 60 additions & 0 deletions tests/pos/i8073b.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import scala.deriving.Mirror

trait A:
type B

// Test local mirrors
@main def Test =
case class CC(a: A, b: a.B)

enum Enum:
case EC(a: A, b: a.B)

def test1(): Unit =
val generic = summon[Mirror.Of[CC]]
// No language syntax for type projection of a singleton type
// summon[generic.MirroredElemTypes =:= (A, CC#a.B)]

val dependent = summon[Mirror.Of[x.type]]
summon[dependent.MirroredElemTypes =:= (A, x.a.B)]

val aa: A { type B = Int } = new A { type B = Int }
val x: CC { val a: aa.type } = CC(aa, 1).asInstanceOf[CC { val a: aa.type }] // manual `tracked`

assert(CC(aa, 1) == generic.fromProduct((aa, 1)))
assert(CC(aa, 1) == dependent.fromProduct((aa, 1)))

x match
case CC(a, b) =>
val a1: A = a
// Dependent pattern matching is not currently supported
// val b1: a1.B = b
val b1 = b // Type is CC#a.B

end test1

def test2(): Unit =
val generic = summon[Mirror.Of[Enum.EC]]
// No language syntax for type projection of a singleton type
// summon[generic.MirroredElemTypes =:= (A, Enum.EC#a.B)]

val dependent = summon[Mirror.Of[x.type]]
summon[dependent.MirroredElemTypes =:= (A, x.a.B)]

val aa: A { type B = Int } = new A { type B = Int }
val x: Enum.EC { val a: aa.type } = Enum.EC(aa, 1).asInstanceOf[Enum.EC { val a: aa.type }] // manual `tracked`

assert(Enum.EC(aa, 1) == generic.fromProduct((aa, 1)))
assert(Enum.EC(aa, 1) == dependent.fromProduct((aa, 1)))

x match
case Enum.EC(a, b) =>
val a1: A = a
// Dependent pattern matching is not currently supported
// val b1: a1.B = b
val b1 = b // Type is Enum.EC#a.B

end test2

test1()
test2()

0 comments on commit 171aafe

Please sign in to comment.