Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

synthesize mirrors for small generic tuples #15250

Merged
merged 2 commits into from
May 31, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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 */
bishabosha marked this conversation as resolved.
Show resolved Hide resolved
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 a 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 a 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"))
}