Skip to content

Commit

Permalink
synthesize mirrors for small generic tuples, redux
Browse files Browse the repository at this point in the history
- handles generic tuples of different arity
  • Loading branch information
bishabosha committed May 31, 2022
1 parent 9d2d194 commit f82f8cb
Show file tree
Hide file tree
Showing 12 changed files with 121 additions and 14 deletions.
4 changes: 3 additions & 1 deletion compiler/src/dotty/tools/dotc/core/TypeErasure.scala
Original file line number Diff line number Diff line change
Expand Up @@ -689,7 +689,9 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst
}

private def erasePair(tp: Type)(using Context): Type = {
val arity = tp.tupleArity
// NOTE: `tupleArity` does not consider TypeRef(EmptyTuple$) equivalent to EmptyTuple.type,
// we fix this for printers, but type erasure should be preserved.
val arity = tp.tupleArity()
if (arity < 0) defn.ProductClass.typeRef
else if (arity <= Definitions.MaxTupleArity) defn.TupleType(arity).nn
else defn.TupleXXLClass.typeRef
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
val cls = tycon.typeSymbol
if tycon.isRepeatedParam then toTextLocal(args.head) ~ "*"
else if defn.isFunctionClass(cls) then toTextFunction(args, cls.name.isContextFunction, cls.name.isErasedFunction)
else if tp.tupleArity >= 2 && !printDebug then toTextTuple(tp.tupleElementTypes)
else if tp.tupleArity(relaxEmptyTuple = true) >= 2 && !printDebug then toTextTuple(tp.tupleElementTypes)
else if isInfixType(tp) then
val l :: r :: Nil = args: @unchecked
val opName = tyconName(tycon)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ object GenericSignatures {
case _ => jsig(elemtp)

case RefOrAppliedType(sym, pre, args) =>
if (sym == defn.PairClass && tp.tupleArity > Definitions.MaxTupleArity)
if (sym == defn.PairClass && tp.tupleArity() > Definitions.MaxTupleArity)
jsig(defn.TupleXXLClass.typeRef)
else if (isTypeParameterInSig(sym, sym0)) {
assert(!sym.isAliasType, "Unexpected alias type: " + sym)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ object SyntheticMembers {

/** Attachment recording that an anonymous class should extend Mirror.Sum */
val ExtendsSumMirror: Property.StickyKey[Unit] = new Property.StickyKey

/** Attachment recording that an anonymous class should extend Mirror.Sum */
val GenericTupleArity: Property.StickyKey[Int] = new Property.StickyKey
}

/** Synthetic method implementations for case classes, case objects,
Expand Down Expand Up @@ -601,7 +604,11 @@ class SyntheticMembers(thisPhase: DenotTransformer) {
else if (impl.removeAttachment(ExtendsSingletonMirror).isDefined)
makeSingletonMirror()
else if (impl.removeAttachment(ExtendsProductMirror).isDefined)
makeProductMirror(monoType.typeRef.dealias.classSymbol)
val tupleArity = impl.removeAttachment(GenericTupleArity)
val cls = tupleArity match
case Some(n) => defn.TupleType(n).nn.classSymbol
case _ => monoType.typeRef.dealias.classSymbol
makeProductMirror(cls)
else if (impl.removeAttachment(ExtendsSumMirror).isDefined)
makeSumMirror(monoType.typeRef.dealias.classSymbol)

Expand Down
10 changes: 8 additions & 2 deletions compiler/src/dotty/tools/dotc/transform/TypeUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,17 @@ object TypeUtils {

/** The arity of this tuple type, which can be made up of EmptyTuple, TupleX and `*:` pairs,
* or -1 if this is not a tuple type.
*
* @param relaxEmptyTuple if true then TypeRef(EmptyTuple$) =:= EmptyTuple.type
*/
def tupleArity(using Context): Int = self match {
def tupleArity(relaxEmptyTuple: Boolean = false)(using Context): Int = self match {
case AppliedType(tycon, _ :: tl :: Nil) if tycon.isRef(defn.PairClass) =>
val arity = tl.tupleArity
val arity = tl.tupleArity(relaxEmptyTuple)
if (arity < 0) arity else arity + 1
case self: SingletonType =>
if self.termSymbol == defn.EmptyTupleModule then 0 else -1
case self: TypeRef if relaxEmptyTuple && self.classSymbol == defn.EmptyTupleModule.moduleClass =>
0
case self if defn.isTupleClass(self.classSymbol) =>
self.dealias.argInfos.length
case _ =>
Expand All @@ -71,6 +75,8 @@ object TypeUtils {
case self: SingletonType =>
assert(self.termSymbol == defn.EmptyTupleModule, "not a tuple")
Nil
case self: TypeRef if self.classSymbol == defn.EmptyTupleModule.moduleClass =>
Nil
case self if defn.isTupleClass(self.classSymbol) =>
self.dealias.argInfos
case _ =>
Expand Down
51 changes: 44 additions & 7 deletions compiler/src/dotty/tools/dotc/typer/Synthesizer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -223,16 +223,19 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
/** Create an anonymous class `new Object { type MirroredMonoType = ... }`
* and mark it with given attachment so that it is made into a mirror at PostTyper.
*/
private def anonymousMirror(monoType: Type, attachment: Property.StickyKey[Unit], span: Span)(using Context) =
private def anonymousMirror(monoType: Type, attachment: Property.StickyKey[Unit], tupleArity: Option[Int], span: Span)(using Context) =
if ctx.isAfterTyper then ctx.compilationUnit.needsMirrorSupport = true
val monoTypeDef = untpd.TypeDef(tpnme.MirroredMonoType, untpd.TypeTree(monoType))
val newImpl = untpd.Template(
var newImpl = untpd.Template(
constr = untpd.emptyConstructor,
parents = untpd.TypeTree(defn.ObjectType) :: Nil,
derived = Nil,
self = EmptyValDef,
body = monoTypeDef :: Nil
).withAttachment(attachment, ())
tupleArity.foreach { n =>
newImpl = newImpl.withAttachment(GenericTupleArity, n)
}
typer.typed(untpd.New(newImpl).withSpan(span))

/** The mirror type
Expand Down Expand Up @@ -279,6 +282,17 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
private[Synthesizer] enum MirrorSource:
case ClassSymbol(cls: Symbol)
case Singleton(src: Symbol, tref: TermRef)
case GenericTuple(tps: List[Type])

/** Tests that both sides are tuples of the same arity */
infix def sameTuple(that: MirrorSource)(using Context): Boolean =
def arity(msrc: MirrorSource): Int = msrc match
case GenericTuple(tps) => tps.size
case ClassSymbol(cls) if defn.isTupleClass(cls) => cls.typeParams.size // tested in tests/pos/i13859.scala
case _ => -1
def equivalent(n: Int, m: Int) =
n == m && n > 0
equivalent(arity(this), arity(that))

/** A comparison that chooses the most specific MirrorSource, this is guided by what is necessary for
* `Mirror.Product.fromProduct`. i.e. its result type should be compatible with the erasure of `mirroredType`.
Expand All @@ -289,10 +303,15 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
case (ClassSymbol(cls1), ClassSymbol(cls2)) => cls1.isSubClass(cls2)
case (Singleton(src1, _), Singleton(src2, _)) => src1 eq src2
case (_: ClassSymbol, _: Singleton) => false
case _ => this sameTuple that

def show(using Context): String = this match
case ClassSymbol(cls) => i"$cls"
case Singleton(src, _) => i"$src"
case GenericTuple(tps) =>
val arity = tps.size
if arity <= Definitions.MaxTupleArity then s"class Tuple$arity"
else s"trait Tuple { def size: $arity }"

private[Synthesizer] object MirrorSource:

Expand Down Expand Up @@ -332,6 +351,11 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
reduce(tp.underlying)
case tp: HKTypeLambda if tp.resultType.isInstanceOf[HKTypeLambda] =>
Left(i"its subpart `$tp` is not a supported kind (either `*` or `* -> *`)")
case tp @ AppliedType(tref: TypeRef, _)
if tref.symbol == defn.PairClass
&& tp.tupleArity(relaxEmptyTuple = true) > 0 =>
// avoid type aliases for tuples
Right(MirrorSource.GenericTuple(tp.tupleElementTypes))
case tp: TypeProxy =>
reduce(tp.underlying)
case tp @ AndType(l, r) =>
Expand All @@ -354,10 +378,12 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):

private def productMirror(mirroredType: Type, formal: Type, span: Span)(using Context): TreeWithErrors =

def makeProductMirror(cls: Symbol): TreeWithErrors =
def makeProductMirror(cls: Symbol, tps: Option[List[Type]]): TreeWithErrors =
val accessors = cls.caseAccessors.filterNot(_.isAllOf(PrivateLocal))
val elemLabels = accessors.map(acc => ConstantType(Constant(acc.name.toString)))
val nestedPairs = TypeOps.nestedPairs(accessors.map(mirroredType.resultType.memberInfo(_).widenExpr))
val nestedPairs =
val elems = tps.getOrElse(accessors.map(mirroredType.resultType.memberInfo(_).widenExpr))
TypeOps.nestedPairs(elems)
val (monoType, elemsType) = mirroredType match
case mirroredType: HKTypeLambda =>
(mkMirroredMonoType(mirroredType), mirroredType.derivedLambdaType(resType = nestedPairs))
Expand All @@ -372,7 +398,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
.refinedWith(tpnme.MirroredElemLabels, TypeAlias(elemsLabels))
val mirrorRef =
if cls.useCompanionAsProductMirror then companionPath(mirroredType, span)
else anonymousMirror(monoType, ExtendsProductMirror, span)
else anonymousMirror(monoType, ExtendsProductMirror, tps.map(_.size), span)
withNoErrors(mirrorRef.cast(mirrorType))
end makeProductMirror

Expand All @@ -389,8 +415,15 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
else
val mirrorType = mirrorCore(defn.Mirror_SingletonClass, mirroredType, mirroredType, singleton.name, formal)
withNoErrors(singletonPath.cast(mirrorType))
case MirrorSource.GenericTuple(tps) =>
val maxArity = Definitions.MaxTupleArity
val arity = tps.size
if tps.size <= maxArity then makeProductMirror(defn.TupleType(arity).nn.classSymbol, Some(tps))
else
val reason = s"it reduces to tuple with arity $arity, expected arity <= $maxArity"
withErrors(i"${defn.PairClass} is not a generic product because $reason")
case MirrorSource.ClassSymbol(cls) =>
if cls.isGenericProduct then makeProductMirror(cls)
if cls.isGenericProduct then makeProductMirror(cls, None)
else withErrors(i"$cls is not a generic product because ${cls.whyNotGenericProduct}")
case Left(msg) =>
withErrors(i"type `$mirroredType` is not a generic product because $msg")
Expand All @@ -401,6 +434,10 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
val (acceptableMsg, cls) = MirrorSource.reduce(mirroredType) match
case Right(MirrorSource.Singleton(_, tp)) => (i"its subpart `$tp` is a term reference", NoSymbol)
case Right(MirrorSource.ClassSymbol(cls)) => ("", cls)
case Right(MirrorSource.GenericTuple(tps)) =>
val arity = tps.size
val cls = if arity <= Definitions.MaxTupleArity then defn.TupleType(arity).nn.classSymbol else defn.PairClass
("", cls)
case Left(msg) => (msg, NoSymbol)

val clsIsGenericSum = cls.isGenericSum
Expand Down Expand Up @@ -457,7 +494,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
.refinedWith(tpnme.MirroredElemLabels, TypeAlias(TypeOps.nestedPairs(elemLabels)))
val mirrorRef =
if cls.useCompanionAsSumMirror then companionPath(mirroredType, span)
else anonymousMirror(monoType, ExtendsSumMirror, span)
else anonymousMirror(monoType, ExtendsSumMirror, None, span)
withNoErrors(mirrorRef.cast(mirrorType))
else if acceptableMsg.nonEmpty then
withErrors(i"type `$mirroredType` is not a generic sum because $acceptableMsg")
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2767,7 +2767,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
typed(desugar.smallTuple(tree).withSpan(tree.span), pt)
else {
val pts =
if (arity == pt.tupleArity) pt.tupleElementTypes
if (arity == pt.tupleArity()) pt.tupleElementTypes
else List.fill(arity)(defn.AnyType)
val elems = tree.trees.lazyZip(pts).map(
if ctx.mode.is(Mode.Type) then typedType(_, _, mapPatternBounds = true)
Expand Down
10 changes: 10 additions & 0 deletions tests/neg/i14127.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- Error: tests/neg/i14127.scala:6:55 ----------------------------------------------------------------------------------
6 | *: Int *: Int *: Int *: Int *: Int *: EmptyTuple)]] // error
| ^
|No given instance of type deriving.Mirror.Of[(Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int,
| Int
|, Int, Int)] was found for parameter x of method summon in object Predef. Failed to synthesize an instance of type deriving.Mirror.Of[(Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int,
| Int
|, Int, Int)]:
| * class *: is not a generic product because it reduces to tuple with arity 23, expected arity <= 22
| * class *: is not a generic sum because it does not have subclasses
6 changes: 6 additions & 0 deletions tests/neg/i14127.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import scala.deriving.Mirror

val mT23 = summon[Mirror.Of[(
Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int
*: Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int
*: Int *: Int *: Int *: Int *: Int *: EmptyTuple)]] // error
7 changes: 7 additions & 0 deletions tests/pos/i13859.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,10 @@ object Test:
type MirroredElemTypes[X, Y] = (Y, X)
}
}

locally {
val x = summon[Kind2[Mirror.Product, [X, Y] =>> (Y, X) & (Y *: X *: EmptyTuple)]]
x: Mirror.Product {
type MirroredElemTypes[X, Y] = (Y, X)
}
}
14 changes: 14 additions & 0 deletions tests/run/i14127.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import scala.deriving.Mirror

@main def Test =
val mISB = summon[Mirror.Of[Int *: String *: Boolean *: EmptyTuple]]
assert(mISB.fromProduct((1, "foo", true)) == (1, "foo", true))

val mT22 = summon[Mirror.Of[(
Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int
*: Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int
*: Int *: Int *: Int *: Int *: EmptyTuple)]]

// tuple of 22 elements
val t22 = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22)
assert(mT22.fromProduct(t22) == t22)
18 changes: 18 additions & 0 deletions tests/run/i7049.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import scala.deriving._

case class Foo(x: Int, y: String)

def toTuple[T <: Product](x: T)(using m: Mirror.ProductOf[T], mt: Mirror.ProductOf[m.MirroredElemTypes]) =
mt.fromProduct(x)

@main def Test = {
val m = summon[Mirror.ProductOf[Foo]]
val mt1 = summon[Mirror.ProductOf[(Int, String)]]
type R = (Int, String)
val mt2 = summon[Mirror.ProductOf[R]]
val mt3 = summon[Mirror.ProductOf[m.MirroredElemTypes]]

val f = Foo(1, "foo")
val g: (Int, String) = toTuple(f)// (using m, mt1)
assert(g == (1, "foo"))
}

0 comments on commit f82f8cb

Please sign in to comment.