diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index e987d8037c98..a1fe581fbef5 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -1489,10 +1489,11 @@ class Definitions { * @return true if the dealiased type of `tp` is `TupleN[T1, T2, ..., Tn]` */ def isTupleNType(tp: Type)(using Context): Boolean = { - val arity = tp.dealias.argInfos.length + val tp1 = tp.dealias + val arity = tp1.argInfos.length arity <= MaxTupleArity && { val tupletp = TupleType(arity) - tupletp != null && tp.isRef(tupletp.symbol) + tupletp != null && tp1.isRef(tupletp.symbol) } } diff --git a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala index 79ea4d4c2121..14e2b674446c 100644 --- a/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala +++ b/compiler/src/dotty/tools/dotc/transform/PatternMatcher.scala @@ -9,6 +9,7 @@ import Symbols._, Contexts._, Types._, StdNames._, NameOps._ import util.Spans._ import typer.Applications.* import SymUtils._ +import TypeUtils.* import Flags._, Constants._ import Decorators._ import NameKinds.{PatMatStdBinderName, PatMatAltsName, PatMatResultName} @@ -332,11 +333,11 @@ object PatternMatcher { ref(defn.RuntimeTuplesModule) .select(defn.RuntimeTuples_apply) .appliedTo(receiver, Literal(Constant(i))) - .cast(args(i).tpe.widen) if (isSyntheticScala2Unapply(unapp.symbol) && caseAccessors.length == args.length) def tupleSel(sym: Symbol) = ref(scrutinee).select(sym) - val isGenericTuple = defn.isTupleClass(caseClass) && !defn.isTupleNType(tree.tpe) + val isGenericTuple = defn.isTupleClass(caseClass) && + !defn.isTupleNType(tree.tpe match { case tp: OrType => tp.join case tp => tp }) // widen even hard unions, to see if it's a union of tuples val components = if isGenericTuple then caseAccessors.indices.toList.map(tupleApp(_, ref(scrutinee))) else caseAccessors.map(tupleSel) matchArgsPlan(components, args, onSuccess) else if (unapp.tpe <:< (defn.BooleanType)) diff --git a/tests/pos/i14587.hard-union-tuples.scala b/tests/pos/i14587.hard-union-tuples.scala new file mode 100644 index 000000000000..2d4ebcf93cc0 --- /dev/null +++ b/tests/pos/i14587.hard-union-tuples.scala @@ -0,0 +1,10 @@ +// scalac: -Werror +class Test: + type Foo = Option[String] | Option[Int] + + def test(foo: Foo) = + val (_, foo2: Foo) = // was: the type test for Test.this.Foo cannot be checked at runtime + foo match + case Some(s: String) => ((), s) + case _ => ((), 0) + foo2 diff --git a/tests/run/i14587.min.scala b/tests/run/i14587.min.scala new file mode 100644 index 000000000000..0f5ee94950aa --- /dev/null +++ b/tests/run/i14587.min.scala @@ -0,0 +1,10 @@ +object Test: + def test(cond: Boolean) = + val tup: (String, Unit) | (Int, Unit) = if cond then ("", ()) else (1, ()) + tup match + case (s: String, _) => s + case _ => "n/a" + + def main(args: Array[String]): Unit = + test(true) // works + test(false) // was: ClassCastException: class scala.None$ cannot be cast to class scala.Some diff --git a/tests/run/i14587.opaques.scala b/tests/run/i14587.opaques.scala new file mode 100644 index 000000000000..a0804990ce5c --- /dev/null +++ b/tests/run/i14587.opaques.scala @@ -0,0 +1,8 @@ +object Test: + opaque type Tup = (Int, String) + + def test(tup: Tup) = + val (n, s) = tup + assert(n == 1 && s == "") + + def main(args: Array[String]): Unit = test((1, "")) diff --git a/tests/run/i14587.scala b/tests/run/i14587.scala new file mode 100644 index 000000000000..c2746b4fdc11 --- /dev/null +++ b/tests/run/i14587.scala @@ -0,0 +1,16 @@ +def test(foo: String): Unit = { + // Does not crash if the type is written explicitly as: Option[(Option[Int], String)] + val bar = { + if (foo.isEmpty) Some((Some(1), "")) + else Some((None, "")) + } + + bar.foreach { + case (Some(_), "") => + case _ => + } +} + +@main def Test() = + test("") // works + test("a")