Skip to content

Commit 1e1eb8c

Browse files
committed
synthesize mirrors for small generic tuples
- handles generic tuples of different arity
1 parent 77bcd0d commit 1e1eb8c

File tree

11 files changed

+187
-35
lines changed

11 files changed

+187
-35
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/core/Types.scala

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,23 @@ import scala.annotation.internal.sharable
4040
import scala.annotation.threadUnsafe
4141

4242
import dotty.tools.dotc.transform.SymUtils._
43+
import dotty.tools.dotc.transform.TypeUtils.*
4344

4445
object Types {
4546

4647
@sharable private var nextId = 0
4748

4849
implicit def eqType: CanEqual[Type, Type] = CanEqual.derived
4950

51+
object GenericTupleType:
52+
def unapply(tp: Type)(using Context): Option[List[Type]] = tp match
53+
case tp @ AppliedType(r: TypeRef, _) if r.symbol == defn.PairClass && tp.tupleArity(relaxEmptyTuple = true) > 0 =>
54+
Some(tp.tupleElementTypes)
55+
case AppliedType(r: TypeRef, args) if defn.isTupleClass(r.symbol) =>
56+
Some(args)
57+
case _ =>
58+
None
59+
5060
/** Main class representing types.
5161
*
5262
* The principal subclasses and sub-objects are as follows:

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: 11 additions & 5 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 _ =>
@@ -69,12 +73,14 @@ object TypeUtils {
6973
case AppliedType(tycon, hd :: tl :: Nil) if tycon.isRef(defn.PairClass) =>
7074
hd :: tl.tupleElementTypes
7175
case self: SingletonType =>
72-
assert(self.termSymbol == defn.EmptyTupleModule, "not a tuple")
76+
assert(self.termSymbol == defn.EmptyTupleModule, i"not a tuple `$self`")
77+
Nil
78+
case self: TypeRef if self.classSymbol == defn.EmptyTupleModule.moduleClass =>
7379
Nil
7480
case self if defn.isTupleClass(self.classSymbol) =>
7581
self.dealias.argInfos
76-
case _ =>
77-
throw new AssertionError("not a tuple")
82+
case tp =>
83+
throw new AssertionError(i"not a tuple `$tp`")
7884
}
7985

8086
/** The `*:` equivalent of an instance of a Tuple class */

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

Lines changed: 114 additions & 25 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
@@ -276,21 +279,104 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
276279
case t => mapOver(t)
277280
monoMap(mirroredType.resultType)
278281

282+
private[Synthesizer] enum MirrorSource:
283+
case ClassSymbol(cls: Symbol)
284+
case GenericTuple(tuplArity: Int, tpArgs: List[Type])
285+
286+
def isGenericTuple: Boolean = this.isInstanceOf[GenericTuple]
287+
288+
/** tuple arity, works for TupleN classes and generic tuples */
289+
final def arity(using Context): Int = this match
290+
case GenericTuple(arity, _) => arity
291+
case ClassSymbol(cls) if defn.isTupleClass(cls) => cls.typeParams.length
292+
case _ => -1
293+
294+
def equiv(that: MirrorSource)(using Context): Boolean = (this.arity, that.arity) match
295+
case (n, m) if n > 0 || m > 0 =>
296+
// we shortcut when at least one was a tuple.
297+
// This protects us from comparing classes for two TupleXXL with different arities.
298+
n == m
299+
case _ => this.asClass eq that.asClass // class equality otherwise
300+
301+
def isSub(that: MirrorSource)(using Context): Boolean = (this.arity, that.arity) match
302+
case (n, m) if n > 0 || m > 0 =>
303+
// we shortcut when at least one was a tuple.
304+
// This protects us from comparing classes for two TupleXXL with different arities.
305+
n == m
306+
case _ => this.asClass isSubClass that.asClass
307+
308+
def asClass(using Context): Symbol = this match
309+
case ClassSymbol(cls) => cls
310+
case GenericTuple(arity, _) =>
311+
if arity <= Definitions.MaxTupleArity then defn.TupleType(arity).nn.classSymbol
312+
else defn.TupleXXLClass
313+
314+
object MirrorSource:
315+
def tuple(tps: List[Type]): MirrorSource.GenericTuple = MirrorSource.GenericTuple(tps.size, tps)
316+
317+
end MirrorSource
318+
279319
private def productMirror(mirroredType: Type, formal: Type, span: Span)(using Context): TreeWithErrors =
280320

281-
def whyNotAcceptableType(tp: Type, cls: Symbol): String = tp match
321+
extension (msrc: MirrorSource) def isGenericProd(using Context) =
322+
msrc.isGenericTuple || msrc.asClass.isGenericProduct
323+
324+
/** Follows `classSymbol`, but instead reduces to a proxy of a generic tuple (or a scala.TupleN class).
325+
*
326+
* Does not need to consider AndType, as that is already stripped.
327+
*/
328+
def tupleProxy(tp: Type)(using Context): Option[MirrorSource] = tp match
329+
case tp: TypeRef => if tp.symbol.isClass then None else tupleProxy(tp.superType)
330+
case GenericTupleType(args) => Some(MirrorSource.tuple(args))
331+
case tp: TypeProxy =>
332+
tupleProxy(tp.underlying)
333+
case tp: OrType =>
334+
if tp.tp1.hasClassSymbol(defn.NothingClass) then
335+
tupleProxy(tp.tp2)
336+
else if tp.tp2.hasClassSymbol(defn.NothingClass) then
337+
tupleProxy(tp.tp1)
338+
else tupleProxy(tp.join)
339+
case _ =>
340+
None
341+
342+
def mirrorSource(tp: Type)(using Context): Option[MirrorSource] =
343+
val fromClass = tp.classSymbol
344+
if fromClass.exists then // test if it could be reduced to a generic tuple
345+
if fromClass.isSubClass(defn.TupleClass) && !defn.isTupleClass(fromClass) then tupleProxy(tp)
346+
else Some(MirrorSource.ClassSymbol(fromClass))
347+
else None
348+
349+
/** do all parts match the class symbol? */
350+
def whyNotAcceptableType(tp: Type, msrc: MirrorSource): String = tp match
282351
case tp: HKTypeLambda if tp.resultType.isInstanceOf[HKTypeLambda] =>
283352
i"its subpart `$tp` is not a supported kind (either `*` or `* -> *`)"
284-
case tp: TypeProxy => whyNotAcceptableType(tp.underlying, cls)
285353
case OrType(tp1, tp2) => i"its subpart `$tp` is a top-level union type."
354+
case GenericTupleType(args) if args.size <= Definitions.MaxTupleArity =>
355+
val tup = MirrorSource.tuple(args)
356+
if tup.equiv(msrc) then ""
357+
else i"a subpart reduces to the unrelated tuple ${tup.asClass}, expected ${msrc.asClass}"
358+
case tp: TypeProxy => whyNotAcceptableType(tp.underlying, msrc)
286359
case _ =>
287-
if tp.classSymbol eq cls then ""
288-
else i"a subpart reduces to the more precise ${tp.classSymbol}, expected $cls"
360+
mirrorSource(tp) match
361+
case Some(msrc2) =>
362+
if msrc2.equiv(msrc) then ""
363+
else i"a subpart reduces to the more precise ${msrc2.asClass}, expected ${msrc.asClass}"
364+
case _ => "???" // caught early by initial `mirrorSource` that made `msrc`
289365

290-
def makeProductMirror(cls: Symbol): TreeWithErrors =
291-
val accessors = cls.caseAccessors.filterNot(_.isAllOf(PrivateLocal))
366+
/** widen TermRef to see if they are an alias to an enum singleton */
367+
def isEnumSingletonRef(tp: Type)(using Context): Boolean = tp match
368+
case tp: TermRef =>
369+
val sym = tp.termSymbol
370+
sym.isEnumCase || (!tp.isOverloaded && isEnumSingletonRef(tp.underlying.widenExpr))
371+
case _ => false
372+
373+
def makeProductMirror(msrc: MirrorSource): TreeWithErrors =
374+
val mirroredClass = msrc.asClass
375+
val accessors = mirroredClass.caseAccessors.filterNot(_.isAllOf(PrivateLocal))
292376
val elemLabels = accessors.map(acc => ConstantType(Constant(acc.name.toString)))
293-
val nestedPairs = TypeOps.nestedPairs(accessors.map(mirroredType.resultType.memberInfo(_).widenExpr))
377+
val nestedPairs = msrc match
378+
case MirrorSource.GenericTuple(_, args) => TypeOps.nestedPairs(args)
379+
case _ => TypeOps.nestedPairs(accessors.map(mirroredType.resultType.memberInfo(_).widenExpr))
294380
val (monoType, elemsType) = mirroredType match
295381
case mirroredType: HKTypeLambda =>
296382
(mkMirroredMonoType(mirroredType), mirroredType.derivedLambdaType(resType = nestedPairs))
@@ -300,22 +386,19 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
300386
checkRefinement(formal, tpnme.MirroredElemTypes, elemsType, span)
301387
checkRefinement(formal, tpnme.MirroredElemLabels, elemsLabels, span)
302388
val mirrorType =
303-
mirrorCore(defn.Mirror_ProductClass, monoType, mirroredType, cls.name, formal)
389+
mirrorCore(defn.Mirror_ProductClass, monoType, mirroredType, mirroredClass.name, formal)
304390
.refinedWith(tpnme.MirroredElemTypes, TypeAlias(elemsType))
305391
.refinedWith(tpnme.MirroredElemLabels, TypeAlias(elemsLabels))
306392
val mirrorRef =
307-
if cls.useCompanionAsProductMirror then companionPath(mirroredType, span)
308-
else anonymousMirror(monoType, ExtendsProductMirror, span)
393+
if mirroredClass.useCompanionAsProductMirror then companionPath(mirroredType, span)
394+
else
395+
val arity = msrc match
396+
case MirrorSource.GenericTuple(arity, _) => Some(arity)
397+
case _ => None
398+
anonymousMirror(monoType, ExtendsProductMirror, arity, span)
309399
withNoErrors(mirrorRef.cast(mirrorType))
310400
end makeProductMirror
311401

312-
/** widen TermRef to see if they are an alias to an enum singleton */
313-
def isEnumSingletonRef(tp: Type)(using Context): Boolean = tp match
314-
case tp: TermRef =>
315-
val sym = tp.termSymbol
316-
sym.isEnumCase || (!tp.isOverloaded && isEnumSingletonRef(tp.underlying.widenExpr))
317-
case _ => false
318-
319402
mirroredType match
320403
case AndType(tp1, tp2) =>
321404
orElse(productMirror(tp1, formal, span), productMirror(tp2, formal, span))
@@ -334,11 +417,17 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
334417
val mirrorType = mirrorCore(defn.Mirror_SingletonClass, mirroredType, mirroredType, singleton.name, formal)
335418
withNoErrors(singletonPath.cast(mirrorType))
336419
else
337-
val acceptableMsg = whyNotAcceptableType(mirroredType, cls)
338-
if acceptableMsg.isEmpty then
339-
if cls.isGenericProduct then makeProductMirror(cls)
340-
else withErrors(i"$cls is not a generic product because ${cls.whyNotGenericProduct}")
341-
else withErrors(i"type `$mirroredType` is not a generic product because $acceptableMsg")
420+
mirrorSource(mirroredType) match
421+
case Some(msrc) =>
422+
val acceptableMsg = whyNotAcceptableType(mirroredType, msrc)
423+
if acceptableMsg.isEmpty then
424+
if msrc.isGenericProd then
425+
makeProductMirror(msrc)
426+
else
427+
withErrors(i"${msrc.asClass} is not a generic product because ${msrc.asClass.whyNotGenericProduct}")
428+
else withErrors(i"type `$mirroredType` is not a generic product because $acceptableMsg")
429+
case None =>
430+
withErrors(i"type `$mirroredType` does not reduce to a class or generic tuple type")
342431
end productMirror
343432

344433
private def sumMirror(mirroredType: Type, formal: Type, span: Span)(using Context): TreeWithErrors =
@@ -411,7 +500,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
411500
.refinedWith(tpnme.MirroredElemLabels, TypeAlias(TypeOps.nestedPairs(elemLabels)))
412501
val mirrorRef =
413502
if cls.useCompanionAsSumMirror then companionPath(mirroredType, span)
414-
else anonymousMirror(monoType, ExtendsSumMirror, span)
503+
else anonymousMirror(monoType, ExtendsSumMirror, tupleArity = None, span)
415504
withNoErrors(mirrorRef.cast(mirrorType))
416505
else if acceptableMsg.nonEmpty then
417506
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.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/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/i7079.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)