From 0bf43b2fbb1ad0faa64cd1da97a9682b02724c49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 25 Mar 2024 17:15:15 +0100 Subject: [PATCH] Fix #19746: Do not follow param term refs in `isConcrete`. Term refs that reference term parameters can be substituted later by more precise ones, which can lead to different instantiations of type captures. They must therefore be considered as non concrete when following `baseType`s to captures in variant positions, like we do for type param refs and other substitutable references. We actually rewrite `isConcrete` in the process to be more based on an "allow list" of things we know to be concrete, rather than an "exclusion list" of things we know to be non-concrete. That should make it more straightforward to evaluate the validity of the algorithm. --- .../dotty/tools/dotc/core/TypeComparer.scala | 57 +++++++++++-------- tests/neg/i19746.check | 7 +++ tests/neg/i19746.scala | 15 +++++ tests/pos/TupleReverse.scala | 5 +- tests/pos/TupleReverseOnto.scala | 7 ++- 5 files changed, 61 insertions(+), 30 deletions(-) create mode 100644 tests/neg/i19746.check create mode 100644 tests/neg/i19746.scala diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 302ad7987889..cb58ebf8c025 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -3360,37 +3360,44 @@ class MatchReducer(initctx: Context) extends TypeComparer(initctx) { * * See notably neg/wildcard-match.scala for examples of this. * - * See neg/i13780.scala and neg/i13780-1.scala for ClassCastException - * reproducers if we disable this check. + * See neg/i13780.scala, neg/i13780-1.scala and neg/i19746.scala for + * ClassCastException reproducers if we disable this check. */ - def followEverythingConcrete(tp: Type): Type = - val widenedTp = tp.widenDealias - val tp1 = widenedTp.normalized - - def followTp1: Type = - // If both widenDealias and normalized did something, start again - if (tp1 ne widenedTp) && (widenedTp ne tp) then followEverythingConcrete(tp1) - else tp1 + def isConcrete(tp: Type): Boolean = + val tp1 = tp.normalized tp1 match case tp1: TypeRef => - tp1.info match - case TypeAlias(tl: HKTypeLambda) => tl - case MatchAlias(tl: HKTypeLambda) => tl - case _ => followTp1 - case tp1 @ AppliedType(tycon, args) => - val concreteTycon = followEverythingConcrete(tycon) - if concreteTycon eq tycon then followTp1 - else followEverythingConcrete(concreteTycon.applyIfParameterized(args)) + if tp1.symbol.isClass then true + else + tp1.info match + case info: AliasingBounds => isConcrete(info.alias) + case _ => false + case tp1: AppliedType => + isConcrete(tp1.tycon) && isConcrete(tp1.superType) + case tp1: HKTypeLambda => + true + case tp1: TermRef => + !tp1.symbol.is(Param) && isConcrete(tp1.underlying) + case tp1: TermParamRef => + false + case tp1: SingletonType => + isConcrete(tp1.underlying) + case tp1: ExprType => + isConcrete(tp1.underlying) + case tp1: AnnotatedType => + isConcrete(tp1.parent) + case tp1: RefinedType => + isConcrete(tp1.underlying) + case tp1: RecType => + isConcrete(tp1.underlying) + case tp1: AndOrType => + isConcrete(tp1.tp1) && isConcrete(tp1.tp2) case _ => - followTp1 - end followEverythingConcrete - - def isConcrete(tp: Type): Boolean = - followEverythingConcrete(tp) match - case tp1: AndOrType => isConcrete(tp1.tp1) && isConcrete(tp1.tp2) - case tp1 => tp1.underlyingClassRef(refinementOK = true).exists + val tp2 = tp1.stripped.stripLazyRef + (tp2 ne tp) && isConcrete(tp2) + end isConcrete // Actual matching logic diff --git a/tests/neg/i19746.check b/tests/neg/i19746.check new file mode 100644 index 000000000000..6be8700bb550 --- /dev/null +++ b/tests/neg/i19746.check @@ -0,0 +1,7 @@ +-- [E007] Type Mismatch Error: tests/neg/i19746.scala:9:30 ------------------------------------------------------------- +9 | def asX(w: W[Any]): w.X = self // error: Type Mismatch + | ^^^^ + | Found: (self : Any) + | Required: w.X + | + | longer explanation available when compiling with `-explain` diff --git a/tests/neg/i19746.scala b/tests/neg/i19746.scala new file mode 100644 index 000000000000..b2969d4d3fef --- /dev/null +++ b/tests/neg/i19746.scala @@ -0,0 +1,15 @@ +trait V: + type X = this.type match + case W[x] => x + +trait W[+Y] extends V + +object Test: + extension (self: Any) def as[T]: T = + def asX(w: W[Any]): w.X = self // error: Type Mismatch + asX(new W[T] {}) + + def main(args: Array[String]): Unit = + val b = 0.as[Boolean] // java.lang.ClassCastException if the code is allowed to compile + println(b) +end Test diff --git a/tests/pos/TupleReverse.scala b/tests/pos/TupleReverse.scala index 9b83280afcf1..a5ef2ed69f0c 100644 --- a/tests/pos/TupleReverse.scala +++ b/tests/pos/TupleReverse.scala @@ -12,5 +12,6 @@ def test[T1, T2, T3, T4] = def test2[Tup <: Tuple] = summon[Reverse[Tup] =:= Reverse[Tup]] -def test3[T1, T2, T3, T4](tup1: (T1, T2, T3, T4)) = - summon[Reverse[tup1.type] =:= (T4, T3, T2, T1)] +def test3[T1, T2, T3, T4](tup1: (T1, T2, T3, T4)): Unit = + val tup11: (T1, T2, T3, T4) = tup1 + summon[Reverse[tup11.type] =:= (T4, T3, T2, T1)] diff --git a/tests/pos/TupleReverseOnto.scala b/tests/pos/TupleReverseOnto.scala index 09d5a323cb29..eca8a3e3033c 100644 --- a/tests/pos/TupleReverseOnto.scala +++ b/tests/pos/TupleReverseOnto.scala @@ -13,6 +13,7 @@ def test2[Tup1 <: Tuple, Tup2 <: Tuple] = summon[ReverseOnto[EmptyTuple, Tup1] =:= Tup1] summon[ReverseOnto[Tup1, EmptyTuple] =:= Reverse[Tup1]] -def test3[T1, T2, T3, T4](tup1: (T1, T2), tup2: (T3, T4)) = - summon[ReverseOnto[tup1.type, tup2.type] <:< (T2, T1, T3, T4)] - summon[ReverseOnto[tup1.type, tup2.type] =:= T2 *: T1 *: tup2.type] +def test3[T1, T2, T3, T4](tup1: (T1, T2), tup2: (T3, T4)): Unit = + val tup11: (T1, T2) = tup1 + summon[ReverseOnto[tup11.type, tup2.type] <:< (T2, T1, T3, T4)] + summon[ReverseOnto[tup11.type, tup2.type] =:= T2 *: T1 *: tup2.type]