Skip to content

Commit 6b11dfe

Browse files
committed
synthesize mirrors for small generic tuples
1 parent ee9cc8f commit 6b11dfe

File tree

6 files changed

+91
-26
lines changed

6 files changed

+91
-26
lines changed

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: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ object TypeUtils {
5858
if (arity < 0) arity else arity + 1
5959
case self: SingletonType =>
6060
if self.termSymbol == defn.EmptyTupleModule then 0 else -1
61+
case self: TypeRef if self.classSymbol == defn.EmptyTupleModule.moduleClass =>
62+
0
6163
case self if defn.isTupleClass(self.classSymbol) =>
6264
self.dealias.argInfos.length
6365
case _ =>
@@ -69,12 +71,14 @@ object TypeUtils {
6971
case AppliedType(tycon, hd :: tl :: Nil) if tycon.isRef(defn.PairClass) =>
7072
hd :: tl.tupleElementTypes
7173
case self: SingletonType =>
72-
assert(self.termSymbol == defn.EmptyTupleModule, "not a tuple")
74+
assert(self.termSymbol == defn.EmptyTupleModule, i"not a tuple `$self`")
75+
Nil
76+
case self: TypeRef if self.classSymbol == defn.EmptyTupleModule.moduleClass =>
7377
Nil
7478
case self if defn.isTupleClass(self.classSymbol) =>
7579
self.dealias.argInfos
76-
case _ =>
77-
throw new AssertionError("not a tuple")
80+
case tp =>
81+
throw new AssertionError(i"not a tuple `$tp`")
7882
}
7983

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

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

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
2626
/** Handlers to synthesize implicits for special types */
2727
type SpecialHandler = (Type, Span) => Context ?=> TreeWithErrors
2828
private type SpecialHandlers = List[(ClassSymbol, SpecialHandler)]
29-
29+
3030
val synthesizedClassTag: SpecialHandler = (formal, span) =>
3131
formal.argInfos match
3232
case arg :: Nil =>
@@ -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
@@ -278,9 +281,18 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
278281

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

284+
var isSafeGenericTuple = Option.empty[List[Type]]
285+
286+
def illegalGenericTuple(tp: AppliedType): Boolean =
287+
val tupleArgs = tp.tupleElementTypes
288+
val isTooLarge = tupleArgs.length > Definitions.MaxTupleArity
289+
isSafeGenericTuple = Option.when(!isTooLarge)(tupleArgs)
290+
isTooLarge
291+
281292
/** do all parts match the class symbol? */
282293
def acceptable(tp: Type, cls: Symbol): Boolean = tp match
283294
case tp: HKTypeLambda if tp.resultType.isInstanceOf[HKTypeLambda] => false
295+
case tp @ AppliedType(cons: TypeRef, _) if cons.isRef(defn.PairClass) && illegalGenericTuple(tp) => false
284296
case tp: TypeProxy => acceptable(tp.underlying, cls)
285297
case OrType(tp1, tp2) => acceptable(tp1, cls) && acceptable(tp2, cls)
286298
case _ => tp.classSymbol eq cls
@@ -299,12 +311,15 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
299311
}
300312

301313
def genAnonyousMirror(cls: Symbol): Boolean =
302-
cls.is(Scala2x) || cls.linkedClass.is(Case)
314+
isSafeGenericTuple.isDefined || cls.is(Scala2x) || cls.linkedClass.is(Case)
303315

304316
def makeProductMirror(cls: Symbol): TreeWithErrors =
305-
val accessors = cls.caseAccessors.filterNot(_.isAllOf(PrivateLocal))
317+
val mirroredClass = isSafeGenericTuple.fold(cls)(tps => defn.TupleType(tps.size).nn.classSymbol)
318+
val accessors = mirroredClass.caseAccessors.filterNot(_.isAllOf(PrivateLocal))
306319
val elemLabels = accessors.map(acc => ConstantType(Constant(acc.name.toString)))
307-
val nestedPairs = TypeOps.nestedPairs(accessors.map(mirroredType.resultType.memberInfo(_).widenExpr))
320+
val nestedPairs = isSafeGenericTuple.map(TypeOps.nestedPairs).getOrElse {
321+
TypeOps.nestedPairs(accessors.map(mirroredType.resultType.memberInfo(_).widenExpr))
322+
}
308323
val (monoType, elemsType) = mirroredType match
309324
case mirroredType: HKTypeLambda =>
310325
(mkMirroredMonoType(mirroredType), mirroredType.derivedLambdaType(resType = nestedPairs))
@@ -318,18 +333,20 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
318333
.refinedWith(tpnme.MirroredElemTypes, TypeAlias(elemsType))
319334
.refinedWith(tpnme.MirroredElemLabels, TypeAlias(elemsLabels))
320335
val mirrorRef =
321-
if (genAnonyousMirror(cls)) anonymousMirror(monoType, ExtendsProductMirror, span)
336+
if genAnonyousMirror(cls) then
337+
anonymousMirror(monoType, ExtendsProductMirror, isSafeGenericTuple.map(_.size), span)
322338
else companionPath(mirroredType, span)
323339
withNoErrors(mirrorRef.cast(mirrorType))
324340
end makeProductMirror
325341

326-
def getError(cls: Symbol): String =
327-
val reason = if !cls.isGenericProduct then
328-
i"because ${cls.whyNotGenericProduct}"
329-
else if !canAccessCtor(cls) then
330-
i"because the constructor of $cls is innaccessible from the calling scope."
331-
else
332-
""
342+
def getError(cls: Symbol): String =
343+
val reason =
344+
if !cls.isGenericProduct then
345+
i"because ${cls.whyNotGenericProduct}"
346+
else if !canAccessCtor(cls) then
347+
i"because the constructor of $cls is innaccessible from the calling scope."
348+
else
349+
""
333350
i"$cls is not a generic product $reason"
334351
end getError
335352

@@ -350,8 +367,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
350367
else
351368
val cls = mirroredType.classSymbol
352369
if acceptable(mirroredType, cls)
353-
&& cls.isGenericProduct
354-
&& canAccessCtor(cls)
370+
&& isSafeGenericTuple.isDefined || (cls.isGenericProduct && canAccessCtor(cls))
355371
then
356372
makeProductMirror(cls)
357373
else
@@ -424,11 +440,11 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
424440
.refinedWith(tpnme.MirroredElemLabels, TypeAlias(TypeOps.nestedPairs(elemLabels)))
425441
val mirrorRef =
426442
if useCompanion then companionPath(mirroredType, span)
427-
else anonymousMirror(monoType, ExtendsSumMirror, span)
443+
else anonymousMirror(monoType, ExtendsSumMirror, tupleArity = None, span)
428444
withNoErrors(mirrorRef.cast(mirrorType))
429445
else if !clsIsGenericSum then
430446
(EmptyTree, List(i"$cls is not a generic sum because ${cls.whyNotGenericSum(declScope)}"))
431-
else
447+
else
432448
EmptyTreeNoError
433449
end sumMirror
434450

@@ -595,7 +611,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
595611
tp.baseType(cls)
596612
val base = baseWithRefinements(formal)
597613
val result =
598-
if (base <:< formal.widenExpr)
614+
if (base <:< formal.widenExpr)
599615
// With the subtype test we enforce that the searched type `formal` is of the right form
600616
handler(base, span)
601617
else EmptyTreeNoError
@@ -609,19 +625,19 @@ end Synthesizer
609625

610626
object Synthesizer:
611627

612-
/** Tuple used to store the synthesis result with a list of errors. */
628+
/** Tuple used to store the synthesis result with a list of errors. */
613629
type TreeWithErrors = (Tree, List[String])
614630
private def withNoErrors(tree: Tree): TreeWithErrors = (tree, List.empty)
615631

616632
private val EmptyTreeNoError: TreeWithErrors = withNoErrors(EmptyTree)
617633

618634
private def orElse(treeWithErrors1: TreeWithErrors, treeWithErrors2: => TreeWithErrors): TreeWithErrors = treeWithErrors1 match
619-
case (tree, errors) if tree eq genericEmptyTree =>
635+
case (tree, errors) if tree eq genericEmptyTree =>
620636
val (tree2, errors2) = treeWithErrors2
621637
(tree2, errors ::: errors2)
622638
case _ => treeWithErrors1
623639

624-
private def clearErrorsIfNotEmpty(treeWithErrors: TreeWithErrors) = treeWithErrors match
640+
private def clearErrorsIfNotEmpty(treeWithErrors: TreeWithErrors) = treeWithErrors match
625641
case (tree, _) if tree eq genericEmptyTree => treeWithErrors
626642
case (tree, _) => withNoErrors(tree)
627643

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)