Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate mirrors for named tuples #22469

Merged
merged 1 commit into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Loading