From 0c97e3e0ecbd495dee71dd7bc7f6c0414c0a4026 Mon Sep 17 00:00:00 2001 From: noti0na1 Date: Mon, 27 Oct 2025 15:39:37 +0100 Subject: [PATCH] Implement safe widening of singleton types for abstract type constructors --- .../src/dotty/tools/dotc/core/TypeComparer.scala | 2 +- compiler/src/dotty/tools/dotc/core/Types.scala | 12 ++++++++++++ tests/neg/i20453.check | 14 ++++++++++++++ tests/neg/i20453.scala | 7 +++++++ tests/pos/i20453.scala | 5 +++++ 5 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 tests/neg/i20453.check create mode 100644 tests/neg/i20453.scala create mode 100644 tests/pos/i20453.scala diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 1d1c497b2196..a9fc7c02742a 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -3716,7 +3716,7 @@ class MatchReducer(initctx: Context) extends TypeComparer(initctx) { false case MatchTypeCasePattern.AbstractTypeConstructor(tycon, argPatterns) => - scrut.dealias match + scrut.dealias.widenSingletonNonParamRefs match case scrutDealias @ AppliedType(scrutTycon, args) if scrutTycon =:= tycon => matchArgs(argPatterns, args, tycon.typeParams, scrutIsWidenedAbstract) case _ => diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 71699c992ab6..a84b45ba746e 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -1400,6 +1400,18 @@ object Types extends TypeUtils { case _ => this } + /** Widen singleton types that are safe to widen (not parameters) + * to allow matching against abstract type constructors. + * See issue #20453 + */ + final def widenSingletonNonParamRefs(using Context): Type = stripped match { + case tp: TermRef if !tp.symbol.is(TypeParam) => + val denot = tp.denot + if denot.isOverloaded then this else denot.info.widenSingletonNonParamRefs + case tp: SingletonType => tp.underlying.widenSingletonNonParamRefs + case _ => this + } + /** Widen from TermRef to its underlying non-termref * base type, while also skipping Expr types. */ diff --git a/tests/neg/i20453.check b/tests/neg/i20453.check new file mode 100644 index 000000000000..1439120eff63 --- /dev/null +++ b/tests/neg/i20453.check @@ -0,0 +1,14 @@ +-- [E007] Type Mismatch Error: tests/neg/i20453.scala:7:4 -------------------------------------------------------------- +7 | ("name", "age") // error + | ^^^^^^^^^^^^^^^ + | Found: (String, String) + | Required: NamedTupleDecomposition.Names[(f : (name : String, age : Int))] + | + | Note: a match type could not be fully reduced: + | + | trying to reduce NamedTupleDecomposition.Names[(f : (name : String, age : Int))] + | failed since selector (f : (name : String, age : Int)) + | does not match case NamedTuple.NamedTuple[n, _] => n + | and cannot be shown to be disjoint from it either. + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/i20453.scala b/tests/neg/i20453.scala new file mode 100644 index 000000000000..e23d9b43ea2f --- /dev/null +++ b/tests/neg/i20453.scala @@ -0,0 +1,7 @@ +import scala.language.experimental.namedTuples + +// Ensure we don't widen parameter singleton types +// which would cause unsoundness similar to #19746 +object Test: + def foo[T](f: (name: String, age: Int)): NamedTupleDecomposition.Names[f.type] = + ("name", "age") // error diff --git a/tests/pos/i20453.scala b/tests/pos/i20453.scala new file mode 100644 index 000000000000..a7f95130c47f --- /dev/null +++ b/tests/pos/i20453.scala @@ -0,0 +1,5 @@ +import scala.language.experimental.namedTuples + +object Test: + val f: (name: String, age: Int) = ??? + val x: NamedTupleDecomposition.Names[f.type] = ("name", "age")