Skip to content

Commit

Permalink
Generate mirrors for named tuples (#22469)
Browse files Browse the repository at this point in the history
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`
  • Loading branch information
jchyb authored Jan 31, 2025
1 parent f88f92e commit b908d81
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 9 deletions.
41 changes: 32 additions & 9 deletions compiler/src/dotty/tools/dotc/typer/Synthesizer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
case ClassSymbol(pre: Type, cls: Symbol)
case Singleton(src: Symbol, tref: TermRef)
case GenericTuple(tps: List[Type])
case NamedTuple(nameTypePairs: List[(TermName, Type)])

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

private[Synthesizer] object MirrorSource:

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

def makeProductMirror(pre: Type, cls: Symbol, tps: Option[List[Type]]): TreeWithErrors =
def makeNamedTupleProductMirror(nameTypePairs: List[(TermName, Type)]): TreeWithErrors =
val (labels, typeElems) = nameTypePairs.unzip
val elemLabels = labels.map(label => ConstantType(Constant(label.toString)))
val mirrorRef: Type => Tree = _ => newTupleMirror(typeElems.size)
makeProductMirror(typeElems, elemLabels, tpnme.NamedTuple, mirrorRef)
end makeNamedTupleProductMirror

def makeClassProductMirror(pre: Type, cls: Symbol, tps: Option[List[Type]]) =
val accessors = cls.caseAccessors
val elemLabels = accessors.map(acc => ConstantType(Constant(acc.name.toString)))
val typeElems = tps.getOrElse(accessors.map(mirroredType.resultType.memberInfo(_).widenExpr))
val mirrorRef = (monoType: Type) =>
if cls.useCompanionAsProductMirror then companionPath(pre, cls, span)
else if defn.isTupleClass(cls) then newTupleMirror(typeElems.size) // TODO: cls == defn.PairClass when > 22
else anonymousMirror(monoType, MirrorImpl.OfProduct(pre), span)
makeProductMirror(typeElems, elemLabels, cls.name, mirrorRef)
end makeClassProductMirror

def makeProductMirror(typeElems: List[Type], elemLabels: List[Type], label: Name, mirrorRef: Type => Tree): TreeWithErrors =
val nestedPairs = TypeOps.nestedPairs(typeElems)
val (monoType, elemsType) = mirroredType match
case mirroredType: HKTypeLambda =>
Expand All @@ -442,15 +465,11 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
checkRefinement(formal, tpnme.MirroredElemTypes, elemsType, span)
checkRefinement(formal, tpnme.MirroredElemLabels, elemsLabels, span)
val mirrorType = formal.constrained_& {
mirrorCore(defn.Mirror_ProductClass, monoType, mirroredType, cls.name)
mirrorCore(defn.Mirror_ProductClass, monoType, mirroredType, label)
.refinedWith(tpnme.MirroredElemTypes, TypeAlias(elemsType))
.refinedWith(tpnme.MirroredElemLabels, TypeAlias(elemsLabels))
}
val mirrorRef =
if cls.useCompanionAsProductMirror then companionPath(pre, cls, span)
else if defn.isTupleClass(cls) then newTupleMirror(typeElems.size) // TODO: cls == defn.PairClass when > 22
else anonymousMirror(monoType, MirrorImpl.OfProduct(pre), span)
withNoErrors(mirrorRef.cast(mirrorType).withSpan(span))
withNoErrors(mirrorRef(monoType).cast(mirrorType).withSpan(span))
end makeProductMirror

MirrorSource.reduce(mirroredType) match
Expand All @@ -474,10 +493,12 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
val arity = tps.size
if tps.size <= maxArity then
val tupleCls = defn.TupleType(arity).nn.classSymbol
makeProductMirror(tupleCls.owner.reachableThisType, tupleCls, Some(tps))
makeClassProductMirror(tupleCls.owner.reachableThisType, tupleCls, Some(tps))
else
val reason = s"it reduces to a tuple with arity $arity, expected arity <= $maxArity"
withErrors(i"${defn.PairClass} is not a generic product because $reason")
case MirrorSource.NamedTuple(nameTypePairs) =>
makeNamedTupleProductMirror(nameTypePairs)
case MirrorSource.ClassSymbol(pre, cls) =>
if cls.isGenericProduct then
if ctx.runZincPhases then
Expand All @@ -486,7 +507,7 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
val rec = ctx.compilationUnit.depRecorder
rec.addClassDependency(cls, DependencyByMemberRef)
rec.addUsedName(cls.primaryConstructor)
makeProductMirror(pre, cls, None)
makeClassProductMirror(pre, cls, None)
else withErrors(i"$cls is not a generic product because ${cls.whyNotGenericProduct}")
case Left(msg) =>
withErrors(i"type `$mirroredType` is not a generic product because $msg")
Expand All @@ -501,6 +522,8 @@ class Synthesizer(typer: Typer)(using @constructorOnly c: Context):
val arity = tps.size
val cls = if arity <= Definitions.MaxTupleArity then defn.TupleType(arity).nn.classSymbol else defn.PairClass
("", NoType, cls)
case Right(MirrorSource.NamedTuple(_)) =>
("named tuples are not sealed classes", NoType, NoSymbol)
case Left(msg) => (msg, NoType, NoSymbol)

val clsIsGenericSum = cls.isGenericSum(pre)
Expand Down
8 changes: 8 additions & 0 deletions tests/neg/named-tuples-mirror.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- [E172] Type Error: tests/neg/named-tuples-mirror.scala:6:47 ---------------------------------------------------------
6 | summon[Mirror.SumOf[(foo: Int, bla: String)]] // error
| ^
|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
-- Error: tests/neg/named-tuples-mirror.scala:9:4 ----------------------------------------------------------------------
9 | }]// error
| ^
|MirroredElemLabels mismatch, expected: (("foo" : String), ("bla" : String)), found: (("foo" : String), ("ba" : String)).
10 changes: 10 additions & 0 deletions tests/neg/named-tuples-mirror.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import scala.language.experimental.namedTuples
import scala.deriving.*
import scala.compiletime.*

@main def Test =
summon[Mirror.SumOf[(foo: Int, bla: String)]] // error
val namedTuple = summon[Mirror.Of[(foo: Int, bla: String)]{
type MirroredElemLabels = ("foo", "ba")
}]// error

4 changes: 4 additions & 0 deletions tests/run/named-tuples-mirror.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
NamedTuple
List(foo: Int, bla: String)
15
test
29 changes: 29 additions & 0 deletions tests/run/named-tuples-mirror.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import scala.language.experimental.namedTuples
import scala.deriving.*
import scala.compiletime.*

type ToString[T] = T match
case Int => "Int"
case String => "String"

inline def showLabelsAndTypes[Types <: Tuple, Labels <: Tuple]: List[String] =
inline erasedValue[Types] match {
case _: (tpe *: types) =>
inline erasedValue[Labels] match {
case _: (label *: labels) =>
val labelStr = constValue[label]
val tpeStr = constValue[ToString[tpe]]
s"$labelStr: $tpeStr" :: showLabelsAndTypes[types, labels]
}
case _: EmptyTuple =>
Nil
}

@main def Test =
val mirror = summon[Mirror.Of[(foo: Int, bla: String)]]
println(constValue[mirror.MirroredLabel])
println(showLabelsAndTypes[mirror.MirroredElemTypes, mirror.MirroredElemLabels])

val namedTuple = summon[Mirror.Of[(foo: Int, bla: String)]].fromProduct((15, "test"))
println(namedTuple.foo)
println(namedTuple.bla)

0 comments on commit b908d81

Please sign in to comment.