Skip to content

Commit b908d81

Browse files
authored
Generate mirrors for named tuples (#22469)
For `summon[Mirror.Of[(foo: Int, bla: String)]]` we generate: ```scala new scala.runtime.TupleMirror(2).$asInstanceOf[ scala.deriving.Mirror.Product{ type MirroredMonoType = (foo : Int, bla : String); type MirroredType = (foo : Int, bla : String); type MirroredLabel = ("NamedTuple" : String); type MirroredElemTypes = (Int, String); type MirroredElemLabels = (("foo" : String), ("bla" : String)) } ] ``` We reuse scala.runtime.TupleMirror, because it pretty much does everything we want it to, and fromProduct (with supplied Product types) call on that mirror still works there. Since NamedTuple is not technically a `Product` type, I imagine users might be a little confused why they can't put a named tuple into a `fromProduct` argument, but this is easily worked around with `.toTuple`
1 parent f88f92e commit b908d81

File tree

5 files changed

+83
-9
lines changed

5 files changed

+83
-9
lines changed

Diff for: compiler/src/dotty/tools/dotc/typer/Synthesizer.scala

+32-9
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
322322
case ClassSymbol(pre: Type, cls: Symbol)
323323
case Singleton(src: Symbol, tref: TermRef)
324324
case GenericTuple(tps: List[Type])
325+
case NamedTuple(nameTypePairs: List[(TermName, Type)])
325326

326327
/** Tests that both sides are tuples of the same arity */
327328
infix def sameTuple(that: MirrorSource)(using Context): Boolean =
@@ -351,6 +352,11 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
351352
val arity = tps.size
352353
if arity <= Definitions.MaxTupleArity then s"class Tuple$arity"
353354
else s"trait Tuple { def size: $arity }"
355+
case NamedTuple(nameTypePairs) =>
356+
val (names, types) = nameTypePairs.unzip
357+
val namesStr = names.map(_.show).mkString("(\"", "\", \"", "\")")
358+
val typesStr = types.map(_.show).mkString("(", ", ", ")")
359+
s"NamedTuple.NamedTuple[${namesStr}, ${typesStr}]"
354360

355361
private[Synthesizer] object MirrorSource:
356362

@@ -398,6 +404,8 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
398404
// avoid type aliases for tuples
399405
Right(MirrorSource.GenericTuple(types))
400406
case _ => reduce(tp.underlying)
407+
case defn.NamedTupleDirect(_, _) =>
408+
Right(MirrorSource.NamedTuple(tp.namedTupleElementTypes(derived = false)))
401409
case tp: MatchType =>
402410
val n = tp.tryNormalize
403411
if n.exists then reduce(n) else Left(i"its subpart `$tp` is an unreducible match type.")
@@ -428,10 +436,25 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
428436
def newTupleMirror(arity: Int): Tree =
429437
New(defn.RuntimeTupleMirrorTypeRef, Literal(Constant(arity)) :: Nil)
430438

431-
def makeProductMirror(pre: Type, cls: Symbol, tps: Option[List[Type]]): TreeWithErrors =
439+
def makeNamedTupleProductMirror(nameTypePairs: List[(TermName, Type)]): TreeWithErrors =
440+
val (labels, typeElems) = nameTypePairs.unzip
441+
val elemLabels = labels.map(label => ConstantType(Constant(label.toString)))
442+
val mirrorRef: Type => Tree = _ => newTupleMirror(typeElems.size)
443+
makeProductMirror(typeElems, elemLabels, tpnme.NamedTuple, mirrorRef)
444+
end makeNamedTupleProductMirror
445+
446+
def makeClassProductMirror(pre: Type, cls: Symbol, tps: Option[List[Type]]) =
432447
val accessors = cls.caseAccessors
433448
val elemLabels = accessors.map(acc => ConstantType(Constant(acc.name.toString)))
434449
val typeElems = tps.getOrElse(accessors.map(mirroredType.resultType.memberInfo(_).widenExpr))
450+
val mirrorRef = (monoType: Type) =>
451+
if cls.useCompanionAsProductMirror then companionPath(pre, cls, span)
452+
else if defn.isTupleClass(cls) then newTupleMirror(typeElems.size) // TODO: cls == defn.PairClass when > 22
453+
else anonymousMirror(monoType, MirrorImpl.OfProduct(pre), span)
454+
makeProductMirror(typeElems, elemLabels, cls.name, mirrorRef)
455+
end makeClassProductMirror
456+
457+
def makeProductMirror(typeElems: List[Type], elemLabels: List[Type], label: Name, mirrorRef: Type => Tree): TreeWithErrors =
435458
val nestedPairs = TypeOps.nestedPairs(typeElems)
436459
val (monoType, elemsType) = mirroredType match
437460
case mirroredType: HKTypeLambda =>
@@ -442,15 +465,11 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
442465
checkRefinement(formal, tpnme.MirroredElemTypes, elemsType, span)
443466
checkRefinement(formal, tpnme.MirroredElemLabels, elemsLabels, span)
444467
val mirrorType = formal.constrained_& {
445-
mirrorCore(defn.Mirror_ProductClass, monoType, mirroredType, cls.name)
468+
mirrorCore(defn.Mirror_ProductClass, monoType, mirroredType, label)
446469
.refinedWith(tpnme.MirroredElemTypes, TypeAlias(elemsType))
447470
.refinedWith(tpnme.MirroredElemLabels, TypeAlias(elemsLabels))
448471
}
449-
val mirrorRef =
450-
if cls.useCompanionAsProductMirror then companionPath(pre, cls, span)
451-
else if defn.isTupleClass(cls) then newTupleMirror(typeElems.size) // TODO: cls == defn.PairClass when > 22
452-
else anonymousMirror(monoType, MirrorImpl.OfProduct(pre), span)
453-
withNoErrors(mirrorRef.cast(mirrorType).withSpan(span))
472+
withNoErrors(mirrorRef(monoType).cast(mirrorType).withSpan(span))
454473
end makeProductMirror
455474

456475
MirrorSource.reduce(mirroredType) match
@@ -474,10 +493,12 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
474493
val arity = tps.size
475494
if tps.size <= maxArity then
476495
val tupleCls = defn.TupleType(arity).nn.classSymbol
477-
makeProductMirror(tupleCls.owner.reachableThisType, tupleCls, Some(tps))
496+
makeClassProductMirror(tupleCls.owner.reachableThisType, tupleCls, Some(tps))
478497
else
479498
val reason = s"it reduces to a tuple with arity $arity, expected arity <= $maxArity"
480499
withErrors(i"${defn.PairClass} is not a generic product because $reason")
500+
case MirrorSource.NamedTuple(nameTypePairs) =>
501+
makeNamedTupleProductMirror(nameTypePairs)
481502
case MirrorSource.ClassSymbol(pre, cls) =>
482503
if cls.isGenericProduct then
483504
if ctx.runZincPhases then
@@ -486,7 +507,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
486507
val rec = ctx.compilationUnit.depRecorder
487508
rec.addClassDependency(cls, DependencyByMemberRef)
488509
rec.addUsedName(cls.primaryConstructor)
489-
makeProductMirror(pre, cls, None)
510+
makeClassProductMirror(pre, cls, None)
490511
else withErrors(i"$cls is not a generic product because ${cls.whyNotGenericProduct}")
491512
case Left(msg) =>
492513
withErrors(i"type `$mirroredType` is not a generic product because $msg")
@@ -501,6 +522,8 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
501522
val arity = tps.size
502523
val cls = if arity <= Definitions.MaxTupleArity then defn.TupleType(arity).nn.classSymbol else defn.PairClass
503524
("", NoType, cls)
525+
case Right(MirrorSource.NamedTuple(_)) =>
526+
("named tuples are not sealed classes", NoType, NoSymbol)
504527
case Left(msg) => (msg, NoType, NoSymbol)
505528

506529
val clsIsGenericSum = cls.isGenericSum(pre)

Diff for: tests/neg/named-tuples-mirror.check

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-- [E172] Type Error: tests/neg/named-tuples-mirror.scala:6:47 ---------------------------------------------------------
2+
6 | summon[Mirror.SumOf[(foo: Int, bla: String)]] // error
3+
| ^
4+
|No given instance of type scala.deriving.Mirror.SumOf[(foo : Int, bla : String)] was found for parameter x of method summon in object Predef. Failed to synthesize an instance of type scala.deriving.Mirror.SumOf[(foo : Int, bla : String)]: type `(foo : Int, bla : String)` is not a generic sum because named tuples are not sealed classes
5+
-- Error: tests/neg/named-tuples-mirror.scala:9:4 ----------------------------------------------------------------------
6+
9 | }]// error
7+
| ^
8+
|MirroredElemLabels mismatch, expected: (("foo" : String), ("bla" : String)), found: (("foo" : String), ("ba" : String)).

Diff for: tests/neg/named-tuples-mirror.scala

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import scala.language.experimental.namedTuples
2+
import scala.deriving.*
3+
import scala.compiletime.*
4+
5+
@main def Test =
6+
summon[Mirror.SumOf[(foo: Int, bla: String)]] // error
7+
val namedTuple = summon[Mirror.Of[(foo: Int, bla: String)]{
8+
type MirroredElemLabels = ("foo", "ba")
9+
}]// error
10+

Diff for: tests/run/named-tuples-mirror.check

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
NamedTuple
2+
List(foo: Int, bla: String)
3+
15
4+
test

Diff for: tests/run/named-tuples-mirror.scala

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import scala.language.experimental.namedTuples
2+
import scala.deriving.*
3+
import scala.compiletime.*
4+
5+
type ToString[T] = T match
6+
case Int => "Int"
7+
case String => "String"
8+
9+
inline def showLabelsAndTypes[Types <: Tuple, Labels <: Tuple]: List[String] =
10+
inline erasedValue[Types] match {
11+
case _: (tpe *: types) =>
12+
inline erasedValue[Labels] match {
13+
case _: (label *: labels) =>
14+
val labelStr = constValue[label]
15+
val tpeStr = constValue[ToString[tpe]]
16+
s"$labelStr: $tpeStr" :: showLabelsAndTypes[types, labels]
17+
}
18+
case _: EmptyTuple =>
19+
Nil
20+
}
21+
22+
@main def Test =
23+
val mirror = summon[Mirror.Of[(foo: Int, bla: String)]]
24+
println(constValue[mirror.MirroredLabel])
25+
println(showLabelsAndTypes[mirror.MirroredElemTypes, mirror.MirroredElemLabels])
26+
27+
val namedTuple = summon[Mirror.Of[(foo: Int, bla: String)]].fromProduct((15, "test"))
28+
println(namedTuple.foo)
29+
println(namedTuple.bla)

0 commit comments

Comments
 (0)