Skip to content

Commit 8b266ef

Browse files
committed
Support for named tuples with new representation
1 parent 37b706a commit 8b266ef

40 files changed

+1197
-210
lines changed

compiler/src/dotty/tools/dotc/ast/Desugar.scala

Lines changed: 93 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import Decorators.*
99
import Annotations.Annotation
1010
import NameKinds.{UniqueName, ContextBoundParamName, ContextFunctionParamName, DefaultGetterName, WildcardParamName}
1111
import typer.{Namer, Checking}
12-
import util.{Property, SourceFile, SourcePosition, Chars}
12+
import util.{Property, SourceFile, SourcePosition, SrcPos, Chars}
1313
import config.Feature.{sourceVersion, migrateTo3, enabled}
1414
import config.SourceVersion.*
15-
import collection.mutable.ListBuffer
15+
import collection.mutable
1616
import reporting.*
1717
import annotation.constructorOnly
1818
import printing.Formatting.hl
@@ -242,7 +242,7 @@ object desugar {
242242

243243
private def elimContextBounds(meth: DefDef, isPrimaryConstructor: Boolean)(using Context): DefDef =
244244
val DefDef(_, paramss, tpt, rhs) = meth
245-
val evidenceParamBuf = ListBuffer[ValDef]()
245+
val evidenceParamBuf = mutable.ListBuffer[ValDef]()
246246

247247
var seenContextBounds: Int = 0
248248
def desugarContextBounds(rhs: Tree): Tree = rhs match
@@ -1445,22 +1445,101 @@ object desugar {
14451445
AppliedTypeTree(
14461446
TypeTree(defn.throwsAlias.typeRef).withSpan(op.span), tpt :: excepts :: Nil)
14471447

1448+
private def checkWellFormedTupleElems(elems: List[Tree])(using Context): List[Tree] =
1449+
val seen = mutable.Set[Name]()
1450+
for case arg @ NamedArg(name, _) <- elems do
1451+
if seen.contains(name) then
1452+
report.error(em"Duplicate tuple element name", arg.srcPos)
1453+
seen += name
1454+
if name.startsWith("_") && name.toString.tail.toIntOption.isDefined then
1455+
report.error(
1456+
em"$name cannot be used as the name of a tuple element because it is a regular tuple selector",
1457+
arg.srcPos)
1458+
1459+
elems match
1460+
case elem :: elems1 =>
1461+
val mismatchOpt =
1462+
if elem.isInstanceOf[NamedArg]
1463+
then elems1.find(!_.isInstanceOf[NamedArg])
1464+
else elems1.find(_.isInstanceOf[NamedArg])
1465+
mismatchOpt match
1466+
case Some(misMatch) =>
1467+
report.error(em"Illegal combination of named and unnamed tuple elements", misMatch.srcPos)
1468+
elems.mapConserve(dropNamedArg)
1469+
case None => elems
1470+
case _ => elems
1471+
end checkWellFormedTupleElems
1472+
14481473
/** Translate tuple expressions of arity <= 22
14491474
*
14501475
* () ==> ()
14511476
* (t) ==> t
14521477
* (t1, ..., tN) ==> TupleN(t1, ..., tN)
14531478
*/
1454-
def smallTuple(tree: Tuple)(using Context): Tree = {
1455-
val ts = tree.trees
1456-
val arity = ts.length
1457-
assert(arity <= Definitions.MaxTupleArity)
1458-
def tupleTypeRef = defn.TupleType(arity).nn
1459-
if (arity == 0)
1460-
if (ctx.mode is Mode.Type) TypeTree(defn.UnitType) else unitLiteral
1461-
else if (ctx.mode is Mode.Type) AppliedTypeTree(ref(tupleTypeRef), ts)
1462-
else Apply(ref(tupleTypeRef.classSymbol.companionModule.termRef), ts)
1463-
}
1479+
def tuple(tree: Tuple, pt: Type)(using Context): Tree =
1480+
var elems = checkWellFormedTupleElems(tree.trees)
1481+
if ctx.mode.is(Mode.Pattern) then elems = adaptPatternArgs(elems, pt)
1482+
val elemValues = elems.mapConserve(dropNamedArg)
1483+
val tup =
1484+
val arity = elems.length
1485+
if arity <= Definitions.MaxTupleArity then
1486+
def tupleTypeRef = defn.TupleType(arity).nn
1487+
val tree1 =
1488+
if arity == 0 then
1489+
if ctx.mode is Mode.Type then TypeTree(defn.UnitType) else unitLiteral
1490+
else if ctx.mode is Mode.Type then AppliedTypeTree(ref(tupleTypeRef), elemValues)
1491+
else Apply(ref(tupleTypeRef.classSymbol.companionModule.termRef), elemValues)
1492+
tree1.withSpan(tree.span)
1493+
else
1494+
cpy.Tuple(tree)(elemValues)
1495+
val names = elems.collect:
1496+
case NamedArg(name, arg) => name
1497+
if names.isEmpty || ctx.mode.is(Mode.Pattern) then
1498+
tup
1499+
else
1500+
def namesTuple = inMode(ctx.mode &~ Mode.Pattern | Mode.Type):
1501+
tuple(Tuple(
1502+
names.map: name =>
1503+
SingletonTypeTree(Literal(Constant(name.toString))).withSpan(tree.span)),
1504+
WildcardType)
1505+
if ctx.mode.is(Mode.Type) then
1506+
AppliedTypeTree(ref(defn.NamedTupleTypeRef), namesTuple :: tup :: Nil)
1507+
else
1508+
TypeApply(
1509+
Apply(Select(ref(defn.NamedTupleModule), nme.withNames), tup),
1510+
namesTuple :: Nil)
1511+
1512+
/** When desugaring a list pattern arguments `elems` adapt them and the
1513+
* expected type `pt` to each other. This means:
1514+
* - If `elems` are named pattern elements, rearrange them to match `pt`.
1515+
* This requires all names in `elems` to be also present in `pt`.
1516+
* - If `elems` are unnamed elements, and `pt` is a named tuple, drop all
1517+
* tuple element names from `pt`.
1518+
*/
1519+
def adaptPatternArgs(elems: List[Tree], pt: Type)(using Context): List[Tree] =
1520+
1521+
def reorderedNamedArgs(wildcardSpan: Span): List[untpd.Tree] =
1522+
var selNames = pt.namedTupleElementTypes.map(_(0))
1523+
if selNames.isEmpty && pt.classSymbol.is(CaseClass) then
1524+
selNames = pt.classSymbol.caseAccessors.map(_.name.asTermName)
1525+
val nameToIdx = selNames.zipWithIndex.toMap
1526+
val reordered = Array.fill[untpd.Tree](selNames.length):
1527+
untpd.Ident(nme.WILDCARD).withSpan(wildcardSpan)
1528+
for case arg @ NamedArg(name: TermName, _) <- elems do
1529+
nameToIdx.get(name) match
1530+
case Some(idx) =>
1531+
if reordered(idx).isInstanceOf[Ident] then
1532+
reordered(idx) = arg
1533+
else
1534+
report.error(em"Duplicate named pattern", arg.srcPos)
1535+
case _ =>
1536+
report.error(em"No element named `$name` is defined in selector type $pt", arg.srcPos)
1537+
reordered.toList
1538+
1539+
elems match
1540+
case (first @ NamedArg(_, _)) :: _ => reorderedNamedArgs(first.span.startPos)
1541+
case _ => elems
1542+
end adaptPatternArgs
14641543

14651544
private def isTopLevelDef(stat: Tree)(using Context): Boolean = stat match
14661545
case _: ValDef | _: PatDef | _: DefDef | _: Export | _: ExtMethods => true
@@ -1977,7 +2056,7 @@ object desugar {
19772056
* without duplicates
19782057
*/
19792058
private def getVariables(tree: Tree, shouldAddGiven: Context ?=> Bind => Boolean)(using Context): List[VarInfo] = {
1980-
val buf = ListBuffer[VarInfo]()
2059+
val buf = mutable.ListBuffer[VarInfo]()
19812060
def seenName(name: Name) = buf exists (_._1.name == name)
19822061
def add(named: NameTree, t: Tree): Unit =
19832062
if (!seenName(named.name) && named.name.isTermName) buf += ((named, t))

compiler/src/dotty/tools/dotc/ast/TreeInfo.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,10 @@ trait TreeInfo[T <: Untyped] { self: Trees.Instance[T] =>
244244
def hasNamedArg(args: List[Any]): Boolean = args exists isNamedArg
245245
val isNamedArg: Any => Boolean = (arg: Any) => arg.isInstanceOf[Trees.NamedArg[?]]
246246

247+
def dropNamedArg(arg: Tree) = arg match
248+
case NamedArg(_, arg1) => arg1
249+
case arg => arg
250+
247251
/** Is this pattern node a catch-all (wildcard or variable) pattern? */
248252
def isDefaultCase(cdef: CaseDef): Boolean = cdef match {
249253
case CaseDef(pat, EmptyTree, _) => isWildcardArg(pat)

compiler/src/dotty/tools/dotc/ast/untpd.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -530,15 +530,15 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo {
530530
def makeSelfDef(name: TermName, tpt: Tree)(using Context): ValDef =
531531
ValDef(name, tpt, EmptyTree).withFlags(PrivateLocal)
532532

533-
def makeTupleOrParens(ts: List[Tree])(using Context): Tree = ts match {
533+
def makeTupleOrParens(ts: List[Tree])(using Context): Tree = ts match
534+
case (t: NamedArg) :: Nil => Tuple(t :: Nil)
534535
case t :: Nil => Parens(t)
535536
case _ => Tuple(ts)
536-
}
537537

538-
def makeTuple(ts: List[Tree])(using Context): Tree = ts match {
538+
def makeTuple(ts: List[Tree])(using Context): Tree = ts match
539+
case (t: NamedArg) :: Nil => Tuple(t :: Nil)
539540
case t :: Nil => t
540541
case _ => Tuple(ts)
541-
}
542542

543543
def makeAndType(left: Tree, right: Tree)(using Context): AppliedTypeTree =
544544
AppliedTypeTree(ref(defn.andType.typeRef), left :: right :: Nil)

compiler/src/dotty/tools/dotc/config/Feature.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ object Feature:
3333
val pureFunctions = experimental("pureFunctions")
3434
val captureChecking = experimental("captureChecking")
3535
val into = experimental("into")
36+
val namedTuples = experimental("namedTuples")
3637

3738
val globalOnlyImports: Set[TermName] = Set(pureFunctions, captureChecking)
3839

compiler/src/dotty/tools/dotc/core/Definitions.scala

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -950,6 +950,9 @@ class Definitions {
950950
def TupleXXL_fromIterator(using Context): Symbol = TupleXXLModule.requiredMethod("fromIterator")
951951
def TupleXXL_unapplySeq(using Context): Symbol = TupleXXLModule.requiredMethod(nme.unapplySeq)
952952

953+
@tu lazy val NamedTupleModule = requiredModule("scala.NamedTuple")
954+
@tu lazy val NamedTupleTypeRef: TypeRef = NamedTupleModule.termRef.select(tpnme.NamedTuple).asInstanceOf
955+
953956
@tu lazy val RuntimeTupleMirrorTypeRef: TypeRef = requiredClassRef("scala.runtime.TupleMirror")
954957

955958
@tu lazy val RuntimeTuplesModule: Symbol = requiredModule("scala.runtime.Tuples")
@@ -1302,6 +1305,14 @@ class Definitions {
13021305
case ByNameFunction(_) => true
13031306
case _ => false
13041307

1308+
object NamedTuple:
1309+
def apply(nmes: Type, vals: Type)(using Context): Type =
1310+
AppliedType(NamedTupleTypeRef, nmes :: vals :: Nil)
1311+
def unapply(t: Type)(using Context): Option[(Type, Type)] = t match
1312+
case AppliedType(tycon, nmes :: vals :: Nil) if tycon.typeSymbol == NamedTupleTypeRef.symbol =>
1313+
Some((nmes, vals))
1314+
case _ => None
1315+
13051316
final def isCompiletime_S(sym: Symbol)(using Context): Boolean =
13061317
sym.name == tpnme.S && sym.owner == CompiletimeOpsIntModuleClass
13071318

compiler/src/dotty/tools/dotc/core/StdNames.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,7 @@ object StdNames {
374374
val MirroredMonoType: N = "MirroredMonoType"
375375
val MirroredType: N = "MirroredType"
376376
val Modifiers: N = "Modifiers"
377+
val NamedTuple: N = "NamedTuple"
377378
val NestedAnnotArg: N = "NestedAnnotArg"
378379
val NoFlags: N = "NoFlags"
379380
val NoPrefix: N = "NoPrefix"
@@ -647,6 +648,7 @@ object StdNames {
647648
val wildcardType: N = "wildcardType"
648649
val withFilter: N = "withFilter"
649650
val withFilterIfRefutable: N = "withFilterIfRefutable$"
651+
val withNames: N = "withNames"
650652
val WorksheetWrapper: N = "WorksheetWrapper"
651653
val wrap: N = "wrap"
652654
val writeReplace: N = "writeReplace"

compiler/src/dotty/tools/dotc/core/TypeUtils.scala

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ package core
44

55
import TypeErasure.ErasedValueType
66
import Types.*, Contexts.*, Symbols.*, Flags.*, Decorators.*
7-
import Names.Name
7+
import Names.{Name, TermName}
8+
import Constants.Constant
89

910
class TypeUtils {
1011
/** A decorator that provides methods on types
@@ -65,13 +66,34 @@ class TypeUtils {
6566
case tp: AppliedType if defn.isTupleNType(tp) && normalize =>
6667
Some(tp.args) // if normalize is set, use the dealiased tuple
6768
// otherwise rely on the default case below to print unaliased tuples.
69+
case tp: SkolemType =>
70+
recur(tp.underlying, bound)
6871
case tp: SingletonType =>
69-
if tp.termSymbol == defn.EmptyTupleModule then Some(Nil) else None
72+
if tp.termSymbol == defn.EmptyTupleModule then Some(Nil)
73+
else if normalize then recur(tp.widen, bound)
74+
else None
7075
case _ =>
7176
if defn.isTupleClass(tp.typeSymbol) && !normalize then Some(tp.dealias.argInfos)
7277
else None
7378
recur(self.stripTypeVar, bound)
7479

80+
def namedTupleElementTypesUpTo(bound: Int, normalize: Boolean = true)(using Context): List[(TermName, Type)] =
81+
(if normalize then self.normalized else self).dealias match
82+
case defn.NamedTuple(nmes, vals) =>
83+
val names = nmes.tupleElementTypesUpTo(bound, normalize).getOrElse(Nil).map:
84+
case ConstantType(Constant(str: String)) => str.toTermName
85+
val values = vals.tupleElementTypesUpTo(bound, normalize).getOrElse(Nil)
86+
names.zip(values)
87+
case t =>
88+
Nil
89+
90+
def namedTupleElementTypes(using Context): List[(TermName, Type)] =
91+
namedTupleElementTypesUpTo(Int.MaxValue)
92+
93+
def isNamedTupleType(using Context): Boolean = self match
94+
case defn.NamedTuple(_, _) => true
95+
case _ => false
96+
7597
/** Is this a generic tuple that would fit into the range 1..22,
7698
* but is not already an instance of one of Tuple1..22?
7799
* In this case we need to cast it to make the TupleN/ members accessible.
@@ -84,6 +106,16 @@ class TypeUtils {
84106
case Some(elems) if elems.length <= Definitions.MaxTupleArity => true
85107
case _ => false
86108

109+
/** Drop all named elements in tuple type */
110+
def stripNamedTuple(using Context): Type = self.normalized.dealias match
111+
case defn.NamedTuple(_, vals) =>
112+
vals
113+
case self @ AnnotatedType(tp, annot) =>
114+
val tp1 = tp.stripNamedTuple
115+
if tp1 ne tp then AnnotatedType(tp1, annot) else self
116+
case _ =>
117+
self
118+
87119
/** The `*:` equivalent of an instance of a Tuple class */
88120
def toNestedPairs(using Context): Type =
89121
tupleElementTypes match

0 commit comments

Comments
 (0)