Skip to content

Commit 640c671

Browse files
committed
Complete implementation of pattern matching for named tuples
1 parent 3d31d20 commit 640c671

File tree

10 files changed

+153
-30
lines changed

10 files changed

+153
-30
lines changed

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

Lines changed: 74 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1445,14 +1445,26 @@ object desugar {
14451445
AppliedTypeTree(
14461446
TypeTree(defn.throwsAlias.typeRef).withSpan(op.span), tpt :: excepts :: Nil)
14471447

1448+
private def checkMismatched(elems: List[Tree])(using Context) = elems match
1449+
case elem :: elems1 =>
1450+
val misMatchOpt =
1451+
if elem.isInstanceOf[NamedArg]
1452+
then elems1.find(!_.isInstanceOf[NamedArg])
1453+
else elems1.find(_.isInstanceOf[NamedArg])
1454+
for misMatch <- misMatchOpt do
1455+
report.error(em"Illegal combination of named and unnamed tuple elements", misMatch.srcPos)
1456+
case _ =>
1457+
14481458
/** Translate tuple expressions of arity <= 22
14491459
*
14501460
* () ==> ()
14511461
* (t) ==> t
14521462
* (t1, ..., tN) ==> TupleN(t1, ..., tN)
14531463
*/
1454-
def tuple(tree: Tuple)(using Context): Tree =
1455-
val elems = tree.trees.mapConserve(desugarTupleElem)
1464+
def tuple(tree: Tuple, pt: Type)(using Context): Tree =
1465+
checkMismatched(tree.trees)
1466+
val adapted = adaptTupleElems(tree.trees, pt)
1467+
val elems = adapted.mapConserve(desugarTupleElem)
14561468
val arity = elems.length
14571469
if arity <= Definitions.MaxTupleArity then
14581470
def tupleTypeRef = defn.TupleType(arity).nn
@@ -1465,20 +1477,67 @@ object desugar {
14651477
else
14661478
cpy.Tuple(tree)(elems)
14671479

1468-
private def desugarTupleElem(elem: untpd.Tree)(using Context): untpd.Tree = elem match
1480+
def adaptTupleElems(elems: List[Tree], pt: Type)(using Context): List[Tree] =
1481+
1482+
def reorderedNamedArgs(selElems: List[Type], wildcardSpan: Span): List[untpd.Tree] =
1483+
val nameIdx =
1484+
for case (defn.NamedTupleElem(name, _), idx) <- selElems.zipWithIndex yield
1485+
(name, idx)
1486+
val nameToIdx = nameIdx.toMap[Name, Int]
1487+
val reordered = Array.fill[untpd.Tree](selElems.length):
1488+
untpd.Ident(nme.WILDCARD).withSpan(wildcardSpan)
1489+
for case arg @ NamedArg(name, _) <- elems do
1490+
nameToIdx.get(name) match
1491+
case Some(idx) =>
1492+
if reordered(idx).isInstanceOf[Ident] then
1493+
reordered(idx) = arg
1494+
else
1495+
report.error(em"Duplicate named pattern", arg.srcPos)
1496+
case _ =>
1497+
report.error(em"No element named `$name` is defined", arg.srcPos)
1498+
reordered.toList
1499+
1500+
def convertedToNamedArgs(selElems: List[Type]): List[untpd.Tree] =
1501+
elems.lazyZip(selElems).map:
1502+
case (arg, defn.NamedTupleElem(name, _)) =>
1503+
arg match
1504+
case NamedArg(_, _) => arg // can arise for malformed elements
1505+
case _ => NamedArg(name, arg)
1506+
case (arg, _) =>
1507+
arg
1508+
1509+
pt.tupleElementTypes match
1510+
case Some(selElems @ (firstSelElem :: _)) if ctx.mode.is(Mode.Pattern) =>
1511+
elems match
1512+
case (first @ NamedArg(_, _)) :: _ =>
1513+
reorderedNamedArgs(selElems, first.span.startPos)
1514+
case _ =>
1515+
if firstSelElem.isNamedTupleElem
1516+
then convertedToNamedArgs(selElems)
1517+
else elems
1518+
case _ =>
1519+
elems
1520+
end adaptTupleElems
1521+
1522+
private def desugarTupleElem(elem: Tree)(using Context): Tree = elem match
14691523
case NamedArg(name, arg) =>
1470-
val nameLit = untpd.Literal(Constant(name.toString))
1471-
if ctx.mode.is(Mode.Type) then
1472-
untpd.AppliedTypeTree(untpd.ref(defn.Tuple_NamedValueType),
1473-
untpd.SingletonTypeTree(nameLit) :: arg :: Nil)
1474-
else if ctx.mode.is(Mode.Pattern) then
1475-
untpd.Apply(
1476-
untpd.Block(Nil,
1477-
untpd.TypeApply(untpd.ref(defn.Tuple_NamedValue_extract),
1478-
untpd.SingletonTypeTree(nameLit) :: Nil)),
1479-
arg :: Nil)
1480-
else
1481-
untpd.Apply(untpd.ref(defn.Tuple_NamedValue_apply), nameLit :: arg :: Nil)
1524+
locally:
1525+
val nameLit = Literal(Constant(name.toString))
1526+
if ctx.mode.is(Mode.Type) then
1527+
AppliedTypeTree(ref(defn.Tuple_NamedValueTypeRef),
1528+
SingletonTypeTree(nameLit) :: arg :: Nil)
1529+
else if ctx.mode.is(Mode.Pattern) then
1530+
Apply(
1531+
Block(Nil,
1532+
TypeApply(
1533+
untpd.Select(untpd.ref(defn.Tuple_NamedValueModuleRef), nme.extract),
1534+
SingletonTypeTree(nameLit) :: Nil)),
1535+
arg :: Nil)
1536+
else
1537+
Apply(
1538+
Select(ref(defn.Tuple_NamedValueModuleRef), nme.apply),
1539+
nameLit :: arg :: Nil)
1540+
.withSpan(elem.span)
14821541
case _ =>
14831542
elem
14841543

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

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -938,13 +938,11 @@ class Definitions {
938938
def TupleClass(using Context): ClassSymbol = TupleTypeRef.symbol.asClass
939939
@tu lazy val Tuple_cons: Symbol = TupleClass.requiredMethod("*:")
940940
@tu lazy val TupleModule: Symbol = requiredModule("scala.Tuple")
941-
@tu lazy val TupleNamedValueModule: Symbol = requiredModule("scala.Tuple.NamedValue")
942-
@tu lazy val Tuple_NamedValue_apply: Symbol = TupleNamedValueModule.requiredMethod("apply")
943-
@tu lazy val Tuple_NamedValue_extract: Symbol = TupleNamedValueModule.requiredMethod("extract")
944941

945-
def Tuple_NamedValueType: TypeRef = TupleModule.termRef.select("NamedValue".toTypeName).asInstanceOf
942+
def Tuple_NamedValueTypeRef: TypeRef = TupleModule.termRef.select("NamedValue".toTypeName).asInstanceOf
943+
def Tuple_NamedValueModuleRef: TermRef = TupleModule.termRef.select("NamedValue".toTermName).asInstanceOf
946944
// Note: It would be dangerous to expose NamedValue as a symbol, since
947-
// NamedValue.typeRef gives the internal view of NamedValue inside Tuple
945+
// NamedValue.{typeRef/termRef} give the internal view of NamedValue inside Tuple
948946
// which reveals the opaque alias. To see it externally, we need the construction
949947
// above. Without this tweak, named-tuples.scala fails -Ycheck after typer.
950948

@@ -1315,10 +1313,10 @@ class Definitions {
13151313

13161314
object NamedTupleElem:
13171315
def apply(name: Name, tp: Type)(using Context): Type =
1318-
AppliedType(Tuple_NamedValueType, ConstantType(Constant(name.toString)) :: tp :: Nil)
1316+
AppliedType(Tuple_NamedValueTypeRef, ConstantType(Constant(name.toString)) :: tp :: Nil)
13191317
def unapply(t: Type)(using Context): Option[(TermName, Type)] = t match
13201318
case AppliedType(tycon, ConstantType(Constant(s: String)) :: tp :: Nil)
1321-
if tycon.typeSymbol == Tuple_NamedValueType.typeSymbol => Some((s.toTermName, tp))
1319+
if tycon.typeSymbol == Tuple_NamedValueTypeRef.typeSymbol => Some((s.toTermName, tp))
13221320
case _ => None
13231321

13241322
final def isCompiletime_S(sym: Symbol)(using Context): Boolean =

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,7 @@ object StdNames {
479479
val eqlAny: N = "eqlAny"
480480
val ex: N = "ex"
481481
val extension: N = "extension"
482+
val extract: N = "extract"
482483
val experimental: N = "experimental"
483484
val f: N = "f"
484485
val false_ : N = "false"

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1447,6 +1447,7 @@ trait Applications extends Compatibility {
14471447

14481448
val dummyArg = dummyTreeOfType(ownType)
14491449
val unapplyApp = typedExpr(untpd.TypedSplice(Apply(unapplyFn, dummyArg :: Nil)))
1450+
14501451
def unapplyImplicits(unapp: Tree): List[Tree] = {
14511452
val res = List.newBuilder[Tree]
14521453
def loop(unapp: Tree): Unit = unapp match {
@@ -1481,7 +1482,7 @@ trait Applications extends Compatibility {
14811482
else tryWithTypeTest(Typed(result, TypeTree(ownType)), selType)
14821483
case tp =>
14831484
val unapplyErr = if (tp.isError) unapplyFn else notAnExtractor(unapplyFn)
1484-
val typedArgsErr = args mapconserve (typed(_, defn.AnyType))
1485+
val typedArgsErr = args.mapconserve(typed(_, defn.AnyType))
14851486
cpy.UnApply(tree)(unapplyErr, Nil, typedArgsErr) withType unapplyErr.tpe
14861487
}
14871488
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -851,6 +851,15 @@ object Checking {
851851
templ.parents.find(_.tpe.derivesFrom(defn.PolyFunctionClass)) match
852852
case Some(parent) => report.error(s"`PolyFunction` marker trait is reserved for compiler generated refinements", parent.srcPos)
853853
case None =>
854+
855+
/** Check that `tp` is not a tuple containing a mixture of named and unnamed elements */
856+
def checkTupleWF(tp: Type, pos: SrcPos, kind: String = "")(using Context): Unit =
857+
tp.tupleElementTypes match
858+
case Some(elem :: elems1)
859+
if elem.isNamedTupleElem && elems1.exists(!_.isNamedTupleElem)
860+
|| !elem.isNamedTupleElem && elems1.exists(_.isNamedTupleElem) =>
861+
report.error(em"Illegal combination of named and unnamed tuple elements in$kind type $tp", pos)
862+
case _ =>
854863
}
855864

856865
trait Checking {

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -712,9 +712,11 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
712712
case _ => false
713713
if nameIdx >= 0 && Feature.enabled(Feature.namedTuples) then
714714
typed(
715-
untpd.Apply(
716-
untpd.Select(untpd.TypedSplice(qual), nme.apply),
717-
untpd.Literal(Constant(nameIdx))),
715+
untpd.Select(
716+
untpd.Apply(
717+
untpd.Select(untpd.TypedSplice(qual), nme.apply),
718+
untpd.Literal(Constant(nameIdx))),
719+
nme.value),
718720
pt)
719721
else if qual.tpe.isSmallGenericTuple then
720722
typedSelect(tree, pt, qual.cast(defn.tupleType(tupleElems)))
@@ -1801,6 +1803,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
18011803
val selType = rawSelectorTpe match
18021804
case c: ConstantType if tree.isInline => c
18031805
case otherTpe => otherTpe.widen
1806+
checkTupleWF(selType, tree.selector.srcPos, " expression's")
18041807

18051808
/** Does `tree` has the same shape as the given match type?
18061809
* We only support typed patterns with empty guards, but
@@ -3068,7 +3071,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
30683071

30693072
/** Translate tuples of all arities */
30703073
def typedTuple(tree: untpd.Tuple, pt: Type)(using Context): Tree =
3071-
val tree1 = desugar.tuple(tree)
3074+
val tree1 = desugar.tuple(tree, pt)
30723075
if tree1 ne tree then typed(tree1, pt)
30733076
else
30743077
val arity = tree.trees.length

tests/neg/i7751.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
import language.`3.3`
2-
val a = Some(a=a,)=> // error // error
2+
val a = Some(a=a,)=> // error // error // error
33
val a = Some(x=y,)=>

tests/neg/named-tuples.check

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,27 @@
2626
| Required: (age: Int, name: String)
2727
|
2828
| longer explanation available when compiling with `-explain`
29+
-- Error: tests/neg/named-tuples.scala:20:10 ---------------------------------------------------------------------------
30+
20 | case (name = n, age = a) => () // error // error
31+
| ^^^^^^^^
32+
| No element named `name` is defined
33+
-- Error: tests/neg/named-tuples.scala:20:20 ---------------------------------------------------------------------------
34+
20 | case (name = n, age = a) => () // error // error
35+
| ^^^^^^^
36+
| No element named `age` is defined
37+
-- Error: tests/neg/named-tuples.scala:25:9 ----------------------------------------------------------------------------
38+
25 | person ++ (1, 2) match // error
39+
| ^^^^^^^^^^^^^^^^
40+
| Illegal combination of named and unnamed tuple elements in expression's type (name: String, age: Int, Int, Int)
41+
-- Error: tests/neg/named-tuples.scala:28:17 ---------------------------------------------------------------------------
42+
28 | val bad = ("", age = 10) // error
43+
| ^^^^^^^^
44+
| Illegal combination of named and unnamed tuple elements
45+
-- Error: tests/neg/named-tuples.scala:31:20 ---------------------------------------------------------------------------
46+
31 | case (name = n, age) => () // error
47+
| ^^^
48+
| Illegal combination of named and unnamed tuple elements
49+
-- Error: tests/neg/named-tuples.scala:32:16 ---------------------------------------------------------------------------
50+
32 | case (name, age = a) => () // error
51+
| ^^^^^^^
52+
| Illegal combination of named and unnamed tuple elements

tests/neg/named-tuples.scala

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,20 @@ import language.experimental.namedTuples
1515
val _: Person = nameOnly // error
1616

1717
val _: (age: Int, name: String) = person // error
18+
19+
("Ives", 2) match
20+
case (name = n, age = a) => () // error // error
21+
22+
val pp = person ++ (1, 2) // ok, but should also be error
23+
val qq = ("a", true) ++ (1, 2)
24+
25+
person ++ (1, 2) match // error
26+
case _ =>
27+
28+
val bad = ("", age = 10) // error
29+
30+
person match
31+
case (name = n, age) => () // error
32+
case (name, age = a) => () // error
33+
34+

tests/run/named-tuples.scala

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import annotation.experimental
21
import language.experimental.namedTuples
32
import NamedTuple.dropNames
43

@@ -76,8 +75,20 @@ val _: CombinedInfo = bob ++ addr
7675
// But then we could not do this:
7776

7877
def swap[A, B](x: (A, B)): (B, A) = (x(1), x(0))
79-
8078
val bobS = swap(bob)
8179
val _: (age: Int, name: String) = bobS
8280

81+
val silly = bob match
82+
case (name, age) => name.length + age
83+
84+
assert(silly == 36)
85+
86+
val minors = persons.filter:
87+
case (age = a) => a < 18
88+
case _ => false
8389

90+
assert(minors.isEmpty)
91+
92+
bob match
93+
case (age = 33, name = "Bob") => ()
94+
case _ => assert(false)

0 commit comments

Comments
 (0)