From 2183e537abb94f04636cf5c70b3d52af0a1b273c Mon Sep 17 00:00:00 2001 From: aherlihy Date: Fri, 25 Jul 2025 20:24:54 +0200 Subject: [PATCH 1/5] Skip bypassing unapply for scala 2 case classes to allow for single-element named tuple in unapply --- .../src/dotty/tools/dotc/transform/PatternMatcher.scala | 2 +- tests/run/i23131.scala | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 tests/run/i23131.scala diff --git a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala index e2505144abda..6a13de14450e 100644 --- a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala +++ b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala @@ -343,7 +343,7 @@ object PatternMatcher { receiver.ensureConforms(defn.NonEmptyTupleTypeRef), // If scrutinee is a named tuple, cast to underlying tuple Literal(Constant(i))) - if (isSyntheticScala2Unapply(unapp.symbol) && caseAccessors.length == args.length) + if (isSyntheticScala2Unapply(unapp.symbol) && caseAccessors.length == args.length && args.length != 1) def tupleSel(sym: Symbol) = // If scrutinee is a named tuple, cast to underlying tuple, so that we can // continue to select with _1, _2, ... diff --git a/tests/run/i23131.scala b/tests/run/i23131.scala new file mode 100644 index 000000000000..8d22cd2a4b94 --- /dev/null +++ b/tests/run/i23131.scala @@ -0,0 +1,9 @@ +import scala.NamedTuple +@main +def Test = + Some((name = "Bob")) match { + case Some(name = a) => println(a) + } +// (name = "Bob") match { // works fine +// case (name = a) => println (a) +// } \ No newline at end of file From 4e684dc176967e5fd2841637a62f2c167fceab25 Mon Sep 17 00:00:00 2001 From: aherlihy Date: Fri, 25 Jul 2025 20:53:04 +0200 Subject: [PATCH 2/5] more precise check --- compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala index 6a13de14450e..4cfcba44ee9c 100644 --- a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala +++ b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala @@ -343,7 +343,8 @@ object PatternMatcher { receiver.ensureConforms(defn.NonEmptyTupleTypeRef), // If scrutinee is a named tuple, cast to underlying tuple Literal(Constant(i))) - if (isSyntheticScala2Unapply(unapp.symbol) && caseAccessors.length == args.length && args.length != 1) + val wasNamedArg = args.length == 1 && args.head.removeAttachment(FirstTransform.WasNamedArg).isDefined + if (isSyntheticScala2Unapply(unapp.symbol) && caseAccessors.length == args.length && !wasNamedArg) def tupleSel(sym: Symbol) = // If scrutinee is a named tuple, cast to underlying tuple, so that we can // continue to select with _1, _2, ... @@ -388,7 +389,7 @@ object PatternMatcher { letAbstract(get) { getResult => def isUnaryNamedTupleSelectArg(arg: Tree) = get.tpe.widenDealias.isNamedTupleType - && arg.removeAttachment(FirstTransform.WasNamedArg).isDefined + && wasNamedArg // Special case: Normally, we pull out the argument wholesale if // there is only one. But if the argument is a named argument for // a single-element named tuple, we have to select the field instead. From 7f95284aa2d608054faae0751e75875e8abb022e Mon Sep 17 00:00:00 2001 From: aherlihy Date: Tue, 29 Jul 2025 16:30:26 +0200 Subject: [PATCH 3/5] Make check more precise for single named tuple selector in pattern match --- .../tools/dotc/transform/PatternMatcher.scala | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala index 4cfcba44ee9c..b9c93ce1b36f 100644 --- a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala +++ b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala @@ -343,8 +343,12 @@ object PatternMatcher { receiver.ensureConforms(defn.NonEmptyTupleTypeRef), // If scrutinee is a named tuple, cast to underlying tuple Literal(Constant(i))) - val wasNamedArg = args.length == 1 && args.head.removeAttachment(FirstTransform.WasNamedArg).isDefined - if (isSyntheticScala2Unapply(unapp.symbol) && caseAccessors.length == args.length && !wasNamedArg) + // Disable Scala2Unapply optimization if the argument is a named argument for a single-element named tuple to + // enable selecting the field. See i23131.scala for test cases. + val wasSingleNamedArgForNamedTuple = + args.length == 1 && args.head.removeAttachment(FirstTransform.WasNamedArg).isDefined && + isGetMatch(unappType) && unapp.select(nme.get, _.info.isParameterless).tpe.widenDealias.isNamedTupleType + if (isSyntheticScala2Unapply(unapp.symbol) && caseAccessors.length == args.length && !wasSingleNamedArgForNamedTuple) def tupleSel(sym: Symbol) = // If scrutinee is a named tuple, cast to underlying tuple, so that we can // continue to select with _1, _2, ... @@ -387,20 +391,16 @@ object PatternMatcher { } else letAbstract(get) { getResult => - def isUnaryNamedTupleSelectArg(arg: Tree) = - get.tpe.widenDealias.isNamedTupleType - && wasNamedArg // Special case: Normally, we pull out the argument wholesale if // there is only one. But if the argument is a named argument for // a single-element named tuple, we have to select the field instead. // NamedArg trees are eliminated in FirstTransform but for named arguments // of patterns we add a WasNamedArg attachment, which is used to guide the // logic here. See i22900.scala for test cases. - val selectors = args match - case arg :: Nil if !isUnaryNamedTupleSelectArg(arg) => - ref(getResult) :: Nil - case _ => - productSelectors(getResult.info).map(ref(getResult).select(_)) + val selectors = if args.length == 1 && !wasSingleNamedArgForNamedTuple then + ref(getResult) :: Nil + else + productSelectors(getResult.info).map(ref(getResult).select(_)) matchArgsPlan(selectors, args, onSuccess) } } From 3c5e4a086a6f061672e015136c8878067ae7cf70 Mon Sep 17 00:00:00 2001 From: aherlihy Date: Tue, 29 Jul 2025 16:49:52 +0200 Subject: [PATCH 4/5] minimize diff --- .../dotty/tools/dotc/transform/PatternMatcher.scala | 13 +++++++------ tests/run/i23131.scala | 5 +---- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala index b9c93ce1b36f..e111cf845dfc 100644 --- a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala +++ b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala @@ -345,10 +345,10 @@ object PatternMatcher { // Disable Scala2Unapply optimization if the argument is a named argument for a single-element named tuple to // enable selecting the field. See i23131.scala for test cases. - val wasSingleNamedArgForNamedTuple = + val wasUnaryNamedTupleSelectArgForNamedTuple = args.length == 1 && args.head.removeAttachment(FirstTransform.WasNamedArg).isDefined && isGetMatch(unappType) && unapp.select(nme.get, _.info.isParameterless).tpe.widenDealias.isNamedTupleType - if (isSyntheticScala2Unapply(unapp.symbol) && caseAccessors.length == args.length && !wasSingleNamedArgForNamedTuple) + if (isSyntheticScala2Unapply(unapp.symbol) && caseAccessors.length == args.length && !wasUnaryNamedTupleSelectArgForNamedTuple) def tupleSel(sym: Symbol) = // If scrutinee is a named tuple, cast to underlying tuple, so that we can // continue to select with _1, _2, ... @@ -397,10 +397,11 @@ object PatternMatcher { // NamedArg trees are eliminated in FirstTransform but for named arguments // of patterns we add a WasNamedArg attachment, which is used to guide the // logic here. See i22900.scala for test cases. - val selectors = if args.length == 1 && !wasSingleNamedArgForNamedTuple then - ref(getResult) :: Nil - else - productSelectors(getResult.info).map(ref(getResult).select(_)) + val selectors = args match + case arg :: Nil if !wasUnaryNamedTupleSelectArgForNamedTuple => + ref(getResult) :: Nil + case _ => + productSelectors(getResult.info).map(ref(getResult).select(_)) matchArgsPlan(selectors, args, onSuccess) } } diff --git a/tests/run/i23131.scala b/tests/run/i23131.scala index 8d22cd2a4b94..2fdee0d9a618 100644 --- a/tests/run/i23131.scala +++ b/tests/run/i23131.scala @@ -3,7 +3,4 @@ import scala.NamedTuple def Test = Some((name = "Bob")) match { case Some(name = a) => println(a) - } -// (name = "Bob") match { // works fine -// case (name = a) => println (a) -// } \ No newline at end of file + } \ No newline at end of file From 73da315cb25cf1e39df5f76165752512bda48d41 Mon Sep 17 00:00:00 2001 From: aherlihy Date: Wed, 30 Jul 2025 10:46:32 +0200 Subject: [PATCH 5/5] Refactor out getOfGetMatch --- compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala index e111cf845dfc..778a97f6c38b 100644 --- a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala +++ b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala @@ -343,11 +343,12 @@ object PatternMatcher { receiver.ensureConforms(defn.NonEmptyTupleTypeRef), // If scrutinee is a named tuple, cast to underlying tuple Literal(Constant(i))) + def getOfGetMatch(gm: Tree) = gm.select(nme.get, _.info.isParameterless) // Disable Scala2Unapply optimization if the argument is a named argument for a single-element named tuple to // enable selecting the field. See i23131.scala for test cases. val wasUnaryNamedTupleSelectArgForNamedTuple = args.length == 1 && args.head.removeAttachment(FirstTransform.WasNamedArg).isDefined && - isGetMatch(unappType) && unapp.select(nme.get, _.info.isParameterless).tpe.widenDealias.isNamedTupleType + isGetMatch(unappType) && getOfGetMatch(unapp).tpe.widenDealias.isNamedTupleType if (isSyntheticScala2Unapply(unapp.symbol) && caseAccessors.length == args.length && !wasUnaryNamedTupleSelectArgForNamedTuple) def tupleSel(sym: Symbol) = // If scrutinee is a named tuple, cast to underlying tuple, so that we can @@ -381,7 +382,7 @@ object PatternMatcher { else { assert(isGetMatch(unappType)) val argsPlan = { - val get = ref(unappResult).select(nme.get, _.info.isParameterless) + val get = getOfGetMatch(ref(unappResult)) val arity = productArity(get.tpe.stripNamedTuple, unapp.srcPos) if (isUnapplySeq) letAbstract(get) { getResult =>