Skip to content

Commit e583685

Browse files
authored
Merge pull request #15250 from dotty-staging/mirrors-generic-tuple
synthesize mirrors for small generic tuples
2 parents 55b47f6 + a9c1ef0 commit e583685

File tree

12 files changed

+124
-14
lines changed

12 files changed

+124
-14
lines changed

compiler/src/dotty/tools/dotc/core/TypeErasure.scala

+3-1
Original file line numberDiff line numberDiff line change
@@ -689,7 +689,9 @@ class TypeErasure(sourceLanguage: SourceLanguage, semiEraseVCs: Boolean, isConst
689689
}
690690

691691
private def erasePair(tp: Type)(using Context): Type = {
692-
val arity = tp.tupleArity
692+
// NOTE: `tupleArity` does not consider TypeRef(EmptyTuple$) equivalent to EmptyTuple.type,
693+
// we fix this for printers, but type erasure should be preserved.
694+
val arity = tp.tupleArity()
693695
if (arity < 0) defn.ProductClass.typeRef
694696
else if (arity <= Definitions.MaxTupleArity) defn.TupleType(arity).nn
695697
else defn.TupleXXLClass.typeRef

compiler/src/dotty/tools/dotc/printing/RefinedPrinter.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ class RefinedPrinter(_ctx: Context) extends PlainPrinter(_ctx) {
218218
val cls = tycon.typeSymbol
219219
if tycon.isRepeatedParam then toTextLocal(args.head) ~ "*"
220220
else if defn.isFunctionClass(cls) then toTextFunction(args, cls.name.isContextFunction, cls.name.isErasedFunction)
221-
else if tp.tupleArity >= 2 && !printDebug then toTextTuple(tp.tupleElementTypes)
221+
else if tp.tupleArity(relaxEmptyTuple = true) >= 2 && !printDebug then toTextTuple(tp.tupleElementTypes)
222222
else if isInfixType(tp) then
223223
val l :: r :: Nil = args: @unchecked
224224
val opName = tyconName(tycon)

compiler/src/dotty/tools/dotc/transform/GenericSignatures.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ object GenericSignatures {
248248
case _ => jsig(elemtp)
249249

250250
case RefOrAppliedType(sym, pre, args) =>
251-
if (sym == defn.PairClass && tp.tupleArity > Definitions.MaxTupleArity)
251+
if (sym == defn.PairClass && tp.tupleArity() > Definitions.MaxTupleArity)
252252
jsig(defn.TupleXXLClass.typeRef)
253253
else if (isTypeParameterInSig(sym, sym0)) {
254254
assert(!sym.isAliasType, "Unexpected alias type: " + sym)

compiler/src/dotty/tools/dotc/transform/SyntheticMembers.scala

+11-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ object SyntheticMembers {
2626

2727
/** Attachment recording that an anonymous class should extend Mirror.Sum */
2828
val ExtendsSumMirror: Property.StickyKey[Unit] = new Property.StickyKey
29+
30+
/** Attachment recording that an anonymous class (with the ExtendsProductMirror attachment)
31+
* should implement its `fromProduct` method in terms of the runtime class corresponding
32+
* to a tuple with that arity.
33+
*/
34+
val GenericTupleArity: Property.StickyKey[Int] = new Property.StickyKey
2935
}
3036

3137
/** Synthetic method implementations for case classes, case objects,
@@ -601,7 +607,11 @@ class SyntheticMembers(thisPhase: DenotTransformer) {
601607
else if (impl.removeAttachment(ExtendsSingletonMirror).isDefined)
602608
makeSingletonMirror()
603609
else if (impl.removeAttachment(ExtendsProductMirror).isDefined)
604-
makeProductMirror(monoType.typeRef.dealias.classSymbol)
610+
val tupleArity = impl.removeAttachment(GenericTupleArity)
611+
val cls = tupleArity match
612+
case Some(n) => defn.TupleType(n).nn.classSymbol
613+
case _ => monoType.typeRef.dealias.classSymbol
614+
makeProductMirror(cls)
605615
else if (impl.removeAttachment(ExtendsSumMirror).isDefined)
606616
makeSumMirror(monoType.typeRef.dealias.classSymbol)
607617

compiler/src/dotty/tools/dotc/transform/TypeUtils.scala

+8-2
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,17 @@ object TypeUtils {
5151

5252
/** The arity of this tuple type, which can be made up of EmptyTuple, TupleX and `*:` pairs,
5353
* or -1 if this is not a tuple type.
54+
*
55+
* @param relaxEmptyTuple if true then TypeRef(EmptyTuple$) =:= EmptyTuple.type
5456
*/
55-
def tupleArity(using Context): Int = self match {
57+
def tupleArity(relaxEmptyTuple: Boolean = false)(using Context): Int = self match {
5658
case AppliedType(tycon, _ :: tl :: Nil) if tycon.isRef(defn.PairClass) =>
57-
val arity = tl.tupleArity
59+
val arity = tl.tupleArity(relaxEmptyTuple)
5860
if (arity < 0) arity else arity + 1
5961
case self: SingletonType =>
6062
if self.termSymbol == defn.EmptyTupleModule then 0 else -1
63+
case self: TypeRef if relaxEmptyTuple && self.classSymbol == defn.EmptyTupleModule.moduleClass =>
64+
0
6165
case self if defn.isTupleClass(self.classSymbol) =>
6266
self.dealias.argInfos.length
6367
case _ =>
@@ -71,6 +75,8 @@ object TypeUtils {
7175
case self: SingletonType =>
7276
assert(self.termSymbol == defn.EmptyTupleModule, "not a tuple")
7377
Nil
78+
case self: TypeRef if self.classSymbol == defn.EmptyTupleModule.moduleClass =>
79+
Nil
7480
case self if defn.isTupleClass(self.classSymbol) =>
7581
self.dealias.argInfos
7682
case _ =>

compiler/src/dotty/tools/dotc/typer/Synthesizer.scala

+44-7
Original file line numberDiff line numberDiff line change
@@ -223,16 +223,19 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
223223
/** Create an anonymous class `new Object { type MirroredMonoType = ... }`
224224
* and mark it with given attachment so that it is made into a mirror at PostTyper.
225225
*/
226-
private def anonymousMirror(monoType: Type, attachment: Property.StickyKey[Unit], span: Span)(using Context) =
226+
private def anonymousMirror(monoType: Type, attachment: Property.StickyKey[Unit], tupleArity: Option[Int], span: Span)(using Context) =
227227
if ctx.isAfterTyper then ctx.compilationUnit.needsMirrorSupport = true
228228
val monoTypeDef = untpd.TypeDef(tpnme.MirroredMonoType, untpd.TypeTree(monoType))
229-
val newImpl = untpd.Template(
229+
var newImpl = untpd.Template(
230230
constr = untpd.emptyConstructor,
231231
parents = untpd.TypeTree(defn.ObjectType) :: Nil,
232232
derived = Nil,
233233
self = EmptyValDef,
234234
body = monoTypeDef :: Nil
235235
).withAttachment(attachment, ())
236+
tupleArity.foreach { n =>
237+
newImpl = newImpl.withAttachment(GenericTupleArity, n)
238+
}
236239
typer.typed(untpd.New(newImpl).withSpan(span))
237240

238241
/** The mirror type
@@ -286,6 +289,17 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
286289
private[Synthesizer] enum MirrorSource:
287290
case ClassSymbol(cls: Symbol)
288291
case Singleton(src: Symbol, tref: TermRef)
292+
case GenericTuple(tps: List[Type])
293+
294+
/** Tests that both sides are tuples of the same arity */
295+
infix def sameTuple(that: MirrorSource)(using Context): Boolean =
296+
def arity(msrc: MirrorSource): Int = msrc match
297+
case GenericTuple(tps) => tps.size
298+
case ClassSymbol(cls) if defn.isTupleClass(cls) => cls.typeParams.size // tested in tests/pos/i13859.scala
299+
case _ => -1
300+
def equivalent(n: Int, m: Int) =
301+
n == m && n > 0
302+
equivalent(arity(this), arity(that))
289303

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

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

304323
private[Synthesizer] object MirrorSource:
305324

@@ -339,6 +358,11 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
339358
reduce(tp.underlying)
340359
case tp: HKTypeLambda if tp.resultType.isInstanceOf[HKTypeLambda] =>
341360
Left(i"its subpart `$tp` is not a supported kind (either `*` or `* -> *`)")
361+
case tp @ AppliedType(tref: TypeRef, _)
362+
if tref.symbol == defn.PairClass
363+
&& tp.tupleArity(relaxEmptyTuple = true) > 0 =>
364+
// avoid type aliases for tuples
365+
Right(MirrorSource.GenericTuple(tp.tupleElementTypes))
342366
case tp: TypeProxy =>
343367
reduce(tp.underlying)
344368
case tp @ AndType(l, r) =>
@@ -361,10 +385,12 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
361385

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

364-
def makeProductMirror(cls: Symbol): TreeWithErrors =
388+
def makeProductMirror(cls: Symbol, tps: Option[List[Type]]): TreeWithErrors =
365389
val accessors = cls.caseAccessors.filterNot(_.isAllOf(PrivateLocal))
366390
val elemLabels = accessors.map(acc => ConstantType(Constant(acc.name.toString)))
367-
val nestedPairs = TypeOps.nestedPairs(accessors.map(mirroredType.resultType.memberInfo(_).widenExpr))
391+
val nestedPairs =
392+
val elems = tps.getOrElse(accessors.map(mirroredType.resultType.memberInfo(_).widenExpr))
393+
TypeOps.nestedPairs(elems)
368394
val (monoType, elemsType) = mirroredType match
369395
case mirroredType: HKTypeLambda =>
370396
(mkMirroredMonoType(mirroredType), mirroredType.derivedLambdaType(resType = nestedPairs))
@@ -380,7 +406,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
380406
}
381407
val mirrorRef =
382408
if cls.useCompanionAsProductMirror then companionPath(mirroredType, span)
383-
else anonymousMirror(monoType, ExtendsProductMirror, span)
409+
else anonymousMirror(monoType, ExtendsProductMirror, tps.map(_.size), span)
384410
withNoErrors(mirrorRef.cast(mirrorType))
385411
end makeProductMirror
386412

@@ -400,8 +426,15 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
400426
mirrorCore(defn.Mirror_SingletonClass, mirroredType, mirroredType, singleton.name)
401427
}
402428
withNoErrors(singletonPath.cast(mirrorType))
429+
case MirrorSource.GenericTuple(tps) =>
430+
val maxArity = Definitions.MaxTupleArity
431+
val arity = tps.size
432+
if tps.size <= maxArity then makeProductMirror(defn.TupleType(arity).nn.classSymbol, Some(tps))
433+
else
434+
val reason = s"it reduces to a tuple with arity $arity, expected arity <= $maxArity"
435+
withErrors(i"${defn.PairClass} is not a generic product because $reason")
403436
case MirrorSource.ClassSymbol(cls) =>
404-
if cls.isGenericProduct then makeProductMirror(cls)
437+
if cls.isGenericProduct then makeProductMirror(cls, None)
405438
else withErrors(i"$cls is not a generic product because ${cls.whyNotGenericProduct}")
406439
case Left(msg) =>
407440
withErrors(i"type `$mirroredType` is not a generic product because $msg")
@@ -412,6 +445,10 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
412445
val (acceptableMsg, cls) = MirrorSource.reduce(mirroredType) match
413446
case Right(MirrorSource.Singleton(_, tp)) => (i"its subpart `$tp` is a term reference", NoSymbol)
414447
case Right(MirrorSource.ClassSymbol(cls)) => ("", cls)
448+
case Right(MirrorSource.GenericTuple(tps)) =>
449+
val arity = tps.size
450+
val cls = if arity <= Definitions.MaxTupleArity then defn.TupleType(arity).nn.classSymbol else defn.PairClass
451+
("", cls)
415452
case Left(msg) => (msg, NoSymbol)
416453

417454
val clsIsGenericSum = cls.isGenericSum
@@ -471,7 +508,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
471508
}
472509
val mirrorRef =
473510
if cls.useCompanionAsSumMirror then companionPath(mirroredType, span)
474-
else anonymousMirror(monoType, ExtendsSumMirror, span)
511+
else anonymousMirror(monoType, ExtendsSumMirror, None, span)
475512
withNoErrors(mirrorRef.cast(mirrorType))
476513
else if acceptableMsg.nonEmpty then
477514
withErrors(i"type `$mirroredType` is not a generic sum because $acceptableMsg")

compiler/src/dotty/tools/dotc/typer/Typer.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -2767,7 +2767,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
27672767
typed(desugar.smallTuple(tree).withSpan(tree.span), pt)
27682768
else {
27692769
val pts =
2770-
if (arity == pt.tupleArity) pt.tupleElementTypes
2770+
if (arity == pt.tupleArity()) pt.tupleElementTypes
27712771
else List.fill(arity)(defn.AnyType)
27722772
val elems = tree.trees.lazyZip(pts).map(
27732773
if ctx.mode.is(Mode.Type) then typedType(_, _, mapPatternBounds = true)

tests/neg/i14127.check

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
-- Error: tests/neg/i14127.scala:6:55 ----------------------------------------------------------------------------------
2+
6 | *: Int *: Int *: Int *: Int *: Int *: EmptyTuple)]] // error
3+
| ^
4+
|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,
5+
| Int
6+
|, 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,
7+
| Int
8+
|, Int, Int)]:
9+
| * class *: is not a generic product because it reduces to a tuple with arity 23, expected arity <= 22
10+
| * class *: is not a generic sum because it does not have subclasses

tests/neg/i14127.scala

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import scala.deriving.Mirror
2+
3+
val mT23 = summon[Mirror.Of[(
4+
Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int
5+
*: Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int
6+
*: Int *: Int *: Int *: Int *: Int *: EmptyTuple)]] // error

tests/pos/i13859.scala

+7
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,10 @@ object Test:
2929
type MirroredElemTypes[X, Y] = (Y, X)
3030
}
3131
}
32+
33+
locally {
34+
val x = summon[Kind2[Mirror.Product, [X, Y] =>> (Y, X) & (Y *: X *: EmptyTuple)]]
35+
x: Mirror.Product {
36+
type MirroredElemTypes[X, Y] = (Y, X)
37+
}
38+
}

tests/run/i14127.scala

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import scala.deriving.Mirror
2+
3+
@main def Test =
4+
val mISB = summon[Mirror.Of[Int *: String *: Boolean *: EmptyTuple]]
5+
assert(mISB.fromProduct((1, "foo", true)) == (1, "foo", true))
6+
7+
val mT22 = summon[Mirror.Of[(
8+
Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int
9+
*: Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int *: Int
10+
*: Int *: Int *: Int *: Int *: EmptyTuple)]]
11+
12+
// tuple of 22 elements
13+
val t22 = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22)
14+
assert(mT22.fromProduct(t22) == t22)

tests/run/i7049.scala

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import scala.deriving._
2+
3+
case class Foo(x: Int, y: String)
4+
5+
def toTuple[T <: Product](x: T)(using m: Mirror.ProductOf[T], mt: Mirror.ProductOf[m.MirroredElemTypes]) =
6+
mt.fromProduct(x)
7+
8+
@main def Test = {
9+
val m = summon[Mirror.ProductOf[Foo]]
10+
val mt1 = summon[Mirror.ProductOf[(Int, String)]]
11+
type R = (Int, String)
12+
val mt2 = summon[Mirror.ProductOf[R]]
13+
val mt3 = summon[Mirror.ProductOf[m.MirroredElemTypes]]
14+
15+
val f = Foo(1, "foo")
16+
val g: (Int, String) = toTuple(f)// (using m, mt1)
17+
assert(g == (1, "foo"))
18+
}

0 commit comments

Comments
 (0)