Skip to content

Commit f82f8cb

Browse files
committed
synthesize mirrors for small generic tuples, redux
- handles generic tuples of different arity
1 parent 9d2d194 commit f82f8cb

File tree

12 files changed

+121
-14
lines changed

12 files changed

+121
-14
lines changed

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

Lines changed: 3 additions & 1 deletion
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ 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 should extend Mirror.Sum */
31+
val GenericTupleArity: Property.StickyKey[Int] = new Property.StickyKey
2932
}
3033

3134
/** Synthetic method implementations for case classes, case objects,
@@ -601,7 +604,11 @@ class SyntheticMembers(thisPhase: DenotTransformer) {
601604
else if (impl.removeAttachment(ExtendsSingletonMirror).isDefined)
602605
makeSingletonMirror()
603606
else if (impl.removeAttachment(ExtendsProductMirror).isDefined)
604-
makeProductMirror(monoType.typeRef.dealias.classSymbol)
607+
val tupleArity = impl.removeAttachment(GenericTupleArity)
608+
val cls = tupleArity match
609+
case Some(n) => defn.TupleType(n).nn.classSymbol
610+
case _ => monoType.typeRef.dealias.classSymbol
611+
makeProductMirror(cls)
605612
else if (impl.removeAttachment(ExtendsSumMirror).isDefined)
606613
makeSumMirror(monoType.typeRef.dealias.classSymbol)
607614

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

Lines changed: 8 additions & 2 deletions
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

Lines changed: 44 additions & 7 deletions
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
@@ -279,6 +282,17 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
279282
private[Synthesizer] enum MirrorSource:
280283
case ClassSymbol(cls: Symbol)
281284
case Singleton(src: Symbol, tref: TermRef)
285+
case GenericTuple(tps: List[Type])
286+
287+
/** Tests that both sides are tuples of the same arity */
288+
infix def sameTuple(that: MirrorSource)(using Context): Boolean =
289+
def arity(msrc: MirrorSource): Int = msrc match
290+
case GenericTuple(tps) => tps.size
291+
case ClassSymbol(cls) if defn.isTupleClass(cls) => cls.typeParams.size // tested in tests/pos/i13859.scala
292+
case _ => -1
293+
def equivalent(n: Int, m: Int) =
294+
n == m && n > 0
295+
equivalent(arity(this), arity(that))
282296

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

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

297316
private[Synthesizer] object MirrorSource:
298317

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

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

357-
def makeProductMirror(cls: Symbol): TreeWithErrors =
381+
def makeProductMirror(cls: Symbol, tps: Option[List[Type]]): TreeWithErrors =
358382
val accessors = cls.caseAccessors.filterNot(_.isAllOf(PrivateLocal))
359383
val elemLabels = accessors.map(acc => ConstantType(Constant(acc.name.toString)))
360-
val nestedPairs = TypeOps.nestedPairs(accessors.map(mirroredType.resultType.memberInfo(_).widenExpr))
384+
val nestedPairs =
385+
val elems = tps.getOrElse(accessors.map(mirroredType.resultType.memberInfo(_).widenExpr))
386+
TypeOps.nestedPairs(elems)
361387
val (monoType, elemsType) = mirroredType match
362388
case mirroredType: HKTypeLambda =>
363389
(mkMirroredMonoType(mirroredType), mirroredType.derivedLambdaType(resType = nestedPairs))
@@ -372,7 +398,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
372398
.refinedWith(tpnme.MirroredElemLabels, TypeAlias(elemsLabels))
373399
val mirrorRef =
374400
if cls.useCompanionAsProductMirror then companionPath(mirroredType, span)
375-
else anonymousMirror(monoType, ExtendsProductMirror, span)
401+
else anonymousMirror(monoType, ExtendsProductMirror, tps.map(_.size), span)
376402
withNoErrors(mirrorRef.cast(mirrorType))
377403
end makeProductMirror
378404

@@ -389,8 +415,15 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
389415
else
390416
val mirrorType = mirrorCore(defn.Mirror_SingletonClass, mirroredType, mirroredType, singleton.name, formal)
391417
withNoErrors(singletonPath.cast(mirrorType))
418+
case MirrorSource.GenericTuple(tps) =>
419+
val maxArity = Definitions.MaxTupleArity
420+
val arity = tps.size
421+
if tps.size <= maxArity then makeProductMirror(defn.TupleType(arity).nn.classSymbol, Some(tps))
422+
else
423+
val reason = s"it reduces to tuple with arity $arity, expected arity <= $maxArity"
424+
withErrors(i"${defn.PairClass} is not a generic product because $reason")
392425
case MirrorSource.ClassSymbol(cls) =>
393-
if cls.isGenericProduct then makeProductMirror(cls)
426+
if cls.isGenericProduct then makeProductMirror(cls, None)
394427
else withErrors(i"$cls is not a generic product because ${cls.whyNotGenericProduct}")
395428
case Left(msg) =>
396429
withErrors(i"type `$mirroredType` is not a generic product because $msg")
@@ -401,6 +434,10 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
401434
val (acceptableMsg, cls) = MirrorSource.reduce(mirroredType) match
402435
case Right(MirrorSource.Singleton(_, tp)) => (i"its subpart `$tp` is a term reference", NoSymbol)
403436
case Right(MirrorSource.ClassSymbol(cls)) => ("", cls)
437+
case Right(MirrorSource.GenericTuple(tps)) =>
438+
val arity = tps.size
439+
val cls = if arity <= Definitions.MaxTupleArity then defn.TupleType(arity).nn.classSymbol else defn.PairClass
440+
("", cls)
404441
case Left(msg) => (msg, NoSymbol)
405442

406443
val clsIsGenericSum = cls.isGenericSum
@@ -457,7 +494,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
457494
.refinedWith(tpnme.MirroredElemLabels, TypeAlias(TypeOps.nestedPairs(elemLabels)))
458495
val mirrorRef =
459496
if cls.useCompanionAsSumMirror then companionPath(mirroredType, span)
460-
else anonymousMirror(monoType, ExtendsSumMirror, span)
497+
else anonymousMirror(monoType, ExtendsSumMirror, None, span)
461498
withNoErrors(mirrorRef.cast(mirrorType))
462499
else if acceptableMsg.nonEmpty then
463500
withErrors(i"type `$mirroredType` is not a generic sum because $acceptableMsg")

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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 10 additions & 0 deletions
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 tuple with arity 23, expected arity <= 22
10+
| * class *: is not a generic sum because it does not have subclasses

tests/neg/i14127.scala

Lines changed: 6 additions & 0 deletions
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

Lines changed: 7 additions & 0 deletions
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

Lines changed: 14 additions & 0 deletions
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

Lines changed: 18 additions & 0 deletions
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)