From ae5e42ac5570cb73d08aa178eaab8a3d0c4e22b6 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Thu, 5 May 2022 18:05:32 +0100 Subject: [PATCH 1/8] Local classes are uncheckable (type tests) --- .../tools/dotc/transform/TypeTestsCasts.scala | 14 +++++ tests/neg/i4812.check | 20 +++++++ tests/neg/i4812.scala | 58 +++++++++++++++++++ 3 files changed, 92 insertions(+) create mode 100644 tests/neg/i4812.check create mode 100644 tests/neg/i4812.scala diff --git a/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala index fe06e90340a7..2b5e6303b789 100644 --- a/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala +++ b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala @@ -152,6 +152,20 @@ object TypeTestsCasts { case AnnotatedType(t, _) => recur(X, t) case tp2: RefinedType => recur(X, tp2.parent) && TypeComparer.hasMatchingMember(tp2.refinedName, X, tp2) case tp2: RecType => recur(X, tp2.parent) + case tp2 + if tp2.typeSymbol.isLocal => + val sym = tp2.typeSymbol + val methodSymbol = sym.owner + val tpSyms = typer.ErrorReporting.substitutableTypeSymbolsInScope(sym).toSet + def isAccessible(sym: Symbol): Boolean = sym == methodSymbol || sym.isType && isAccessible(sym.owner) + def hasPoison(tp: Type): Boolean = + tp.baseClasses.filter(isAccessible).exists { sym => + sym.info.decls.exists { sym => + sym.info.existsPart(tp => tpSyms.contains(tp.typeSymbol)) + || isAccessible(sym.info.typeSymbol.maybeOwner) && hasPoison(sym.info) + } + } + !hasPoison(tp2) case _ => true }) diff --git a/tests/neg/i4812.check b/tests/neg/i4812.check new file mode 100644 index 000000000000..5ae40177360a --- /dev/null +++ b/tests/neg/i4812.check @@ -0,0 +1,20 @@ +-- Error: tests/neg/i4812.scala:8:11 ----------------------------------------------------------------------------------- +8 | case prev: A => // error: the type test for A cannot be checked at runtime + | ^ + | the type test for A cannot be checked at runtime +-- Error: tests/neg/i4812.scala:18:11 ---------------------------------------------------------------------------------- +18 | case prev: A => // error: the type test for A cannot be checked at runtime + | ^ + | the type test for A cannot be checked at runtime +-- Error: tests/neg/i4812.scala:28:11 ---------------------------------------------------------------------------------- +28 | case prev: A => // error: the type test for A cannot be checked at runtime + | ^ + | the type test for A cannot be checked at runtime +-- Error: tests/neg/i4812.scala:38:11 ---------------------------------------------------------------------------------- +38 | case prev: A => // error: the type test for A cannot be checked at runtime + | ^ + | the type test for A cannot be checked at runtime +-- Error: tests/neg/i4812.scala:50:13 ---------------------------------------------------------------------------------- +50 | case prev: A => // error: the type test for A cannot be checked at runtime + | ^ + | the type test for A cannot be checked at runtime diff --git a/tests/neg/i4812.scala b/tests/neg/i4812.scala new file mode 100644 index 000000000000..4bf0848aa5d9 --- /dev/null +++ b/tests/neg/i4812.scala @@ -0,0 +1,58 @@ +// scalac: -Werror +object Test: + var prev: Any = _ + + def test[T](x: T): T = + class A(val elem: (T, Boolean)) + prev match + case prev: A => // error: the type test for A cannot be checked at runtime + prev.elem._1 + case _ => + prev = new A((x, true)) + x + + def test2[T](x: T): T = + abstract class Parent(_elem: T) { def elem: T = _elem } + class A extends Parent(x) + prev match + case prev: A => // error: the type test for A cannot be checked at runtime + prev.elem + case _ => + prev = new A + x + + def test3[T](x: T): T = + class Holder(val elem: T) + class A(val holder: Holder) + prev match + case prev: A => // error: the type test for A cannot be checked at runtime + prev.holder.elem + case _ => + prev = new A(new Holder(x)) + x + + def test4[T](x: T): T = + class Holder(val elem: (Int, (Unit, (T, Boolean)))) + class A { var holder: Holder = null } + prev match + case prev: A => // error: the type test for A cannot be checked at runtime + prev.holder.elem._2._2._1 + case _ => + val a = new A + a.holder = new Holder((42, ((), (x, true)))) + prev = a + x + + class Foo[U]: + def test5(x: U): U = + class A(val elem: U) + prev match + case prev: A => // error: the type test for A cannot be checked at runtime + prev.elem + case _ => + prev = new A(x) + x + + def main(args: Array[String]): Unit = + test(1) + val x: String = test("") // was: ClassCastException: java.lang.Integer cannot be cast to java.lang.String From e455fd7b0e1041a4c2d748043ad8ff06ad0251b5 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Tue, 10 May 2022 12:30:06 +0100 Subject: [PATCH 2/8] Break looping in checkable/hasPoison --- .../tools/dotc/transform/TypeTestsCasts.scala | 4 +++- tests/neg/i4812.check | 4 ++++ tests/neg/i4812.scala | 14 ++++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala index 2b5e6303b789..8eb53c8ef739 100644 --- a/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala +++ b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala @@ -158,11 +158,13 @@ object TypeTestsCasts { val methodSymbol = sym.owner val tpSyms = typer.ErrorReporting.substitutableTypeSymbolsInScope(sym).toSet def isAccessible(sym: Symbol): Boolean = sym == methodSymbol || sym.isType && isAccessible(sym.owner) + val seen = scala.collection.mutable.Set.empty[Type] def hasPoison(tp: Type): Boolean = + seen += tp tp.baseClasses.filter(isAccessible).exists { sym => sym.info.decls.exists { sym => sym.info.existsPart(tp => tpSyms.contains(tp.typeSymbol)) - || isAccessible(sym.info.typeSymbol.maybeOwner) && hasPoison(sym.info) + || !seen.contains(sym.info) && isAccessible(sym.info.typeSymbol.maybeOwner) && hasPoison(sym.info) } } !hasPoison(tp2) diff --git a/tests/neg/i4812.check b/tests/neg/i4812.check index 5ae40177360a..8f9b2e7f120e 100644 --- a/tests/neg/i4812.check +++ b/tests/neg/i4812.check @@ -18,3 +18,7 @@ 50 | case prev: A => // error: the type test for A cannot be checked at runtime | ^ | the type test for A cannot be checked at runtime +-- Error: tests/neg/i4812.scala:60:11 ---------------------------------------------------------------------------------- +60 | case prev: Foo => // error: the type test for A cannot be checked at runtime + | ^ + | the type test for Foo cannot be checked at runtime diff --git a/tests/neg/i4812.scala b/tests/neg/i4812.scala index 4bf0848aa5d9..445291fbaf69 100644 --- a/tests/neg/i4812.scala +++ b/tests/neg/i4812.scala @@ -53,6 +53,20 @@ object Test: prev = new A(x) x + def test6[T](x: T): T = + class Foo { var bar: Bar = null } + class Bar { var foo: Foo = null; var elem: T = _ } + prev match + case prev: Foo => // error: the type test for A cannot be checked at runtime + prev.bar.elem + case _ => + val foo = new Foo + val bar = new Bar + bar.elem = x + foo.bar = bar + prev = foo + x + def main(args: Array[String]): Unit = test(1) val x: String = test("") // was: ClassCastException: java.lang.Integer cannot be cast to java.lang.String From 2283d6f80e151241df3452c72843554ed1e6bc80 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Wed, 11 May 2022 11:52:35 +0100 Subject: [PATCH 3/8] Handle unchecked scrutinee --- .../tools/dotc/transform/TypeTestsCasts.scala | 7 ++++- tests/neg/i4812.check | 4 +-- tests/neg/i4812.scala | 31 +++++++++++++------ 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala index 8eb53c8ef739..a59079238201 100644 --- a/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala +++ b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala @@ -123,6 +123,11 @@ object TypeTestsCasts { } + lazy val scrutineeIsUnchecked = X.widenTermRefExpr.existsPart { + case AnnotatedType(_, annot) if annot.symbol == defn.UncheckedAnnot => true + case _ => false + } + def recur(X: Type, P: Type): Boolean = (X <:< P) || (P.dealias match { case _: SingletonType => true case _: TypeProxy @@ -153,7 +158,7 @@ object TypeTestsCasts { case tp2: RefinedType => recur(X, tp2.parent) && TypeComparer.hasMatchingMember(tp2.refinedName, X, tp2) case tp2: RecType => recur(X, tp2.parent) case tp2 - if tp2.typeSymbol.isLocal => + if tp2.typeSymbol.isLocal && !scrutineeIsUnchecked => val sym = tp2.typeSymbol val methodSymbol = sym.owner val tpSyms = typer.ErrorReporting.substitutableTypeSymbolsInScope(sym).toSet diff --git a/tests/neg/i4812.check b/tests/neg/i4812.check index 8f9b2e7f120e..5333e6ad5cf2 100644 --- a/tests/neg/i4812.check +++ b/tests/neg/i4812.check @@ -19,6 +19,6 @@ | ^ | the type test for A cannot be checked at runtime -- Error: tests/neg/i4812.scala:60:11 ---------------------------------------------------------------------------------- -60 | case prev: Foo => // error: the type test for A cannot be checked at runtime +60 | case prev: A => // error: the type test for A cannot be checked at runtime | ^ - | the type test for Foo cannot be checked at runtime + | the type test for A cannot be checked at runtime diff --git a/tests/neg/i4812.scala b/tests/neg/i4812.scala index 445291fbaf69..7392f6b56c2c 100644 --- a/tests/neg/i4812.scala +++ b/tests/neg/i4812.scala @@ -54,19 +54,32 @@ object Test: x def test6[T](x: T): T = - class Foo { var bar: Bar = null } - class Bar { var foo: Foo = null; var elem: T = _ } + class A { var b: B = null } + class B { var a: A = null; var elem: T = _ } prev match - case prev: Foo => // error: the type test for A cannot be checked at runtime - prev.bar.elem + case prev: A => // error: the type test for A cannot be checked at runtime + prev.b.elem case _ => - val foo = new Foo - val bar = new Bar - bar.elem = x - foo.bar = bar - prev = foo + val a = new A + val b = new B + b.elem = x + a.b = b + prev = a x + def test7[T](x: T): T = + class A(val elem: T) + prev match + case prev: A @unchecked => prev.elem + case _ => prev = new A(x); x + + def test8[T](x: T): T = + class A(val elem: T) + val p = prev + (p: @unchecked) match + case prev: A => prev.elem + case _ => prev = new A(x); x + def main(args: Array[String]): Unit = test(1) val x: String = test("") // was: ClassCastException: java.lang.Integer cannot be cast to java.lang.String From 4b2684c76a9dc1be4a97975c7ffb481b8d932b3c Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Wed, 11 May 2022 12:08:31 +0100 Subject: [PATCH 4/8] Update fs2, with an added @unchecked --- community-build/community-projects/fs2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/community-build/community-projects/fs2 b/community-build/community-projects/fs2 index 457c025ef506..1ba97a221819 160000 --- a/community-build/community-projects/fs2 +++ b/community-build/community-projects/fs2 @@ -1 +1 @@ -Subproject commit 457c025ef506457fc8a7a8ba3abbf8b607decd4a +Subproject commit 1ba97a221819aa3d44f3e2e3a3266e1947bce8dc From 62fdf0e3fe620f3785100919cab47d6eb901dc3f Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Mon, 6 Jun 2022 09:52:23 +0100 Subject: [PATCH 5/8] Simplify uncheckable local classes check --- .../tools/dotc/transform/TypeTestsCasts.scala | 33 +++++-------------- tests/neg/i4812.check | 4 +++ tests/neg/i4812.scala | 28 ++++++++++++++++ 3 files changed, 40 insertions(+), 25 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala index a59079238201..0831e8db8803 100644 --- a/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala +++ b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala @@ -31,7 +31,7 @@ object TypeTestsCasts { import typer.Inferencing.maximizeType import typer.ProtoTypes.constrained - /** Whether `(x:X).isInstanceOf[P]` can be checked at runtime? + /** Whether `(x: X).isInstanceOf[P]` can be checked at runtime? * * First do the following substitution: * (a) replace `T @unchecked` and pattern binder types (e.g., `_$1`) in P with WildcardType @@ -48,7 +48,8 @@ object TypeTestsCasts { * (c) maximize `pre.F[Xs]` and check `pre.F[Xs] <:< P` * 6. if `P = T1 | T2` or `P = T1 & T2`, checkable(X, T1) && checkable(X, T2). * 7. if `P` is a refinement type, FALSE - * 8. otherwise, TRUE + * 8. if `P` is a local class which is not statically reachable from the scope where `X` is defined, FALSE + * 9. otherwise, TRUE */ def checkable(X: Type, P: Type, span: Span)(using Context): Boolean = atPhase(Phases.refchecksPhase.next) { // Run just before ElimOpaque transform (which follows RefChecks) @@ -58,7 +59,7 @@ object TypeTestsCasts { def apply(tp: Type) = tp match { case tref: TypeRef if tref.typeSymbol.isPatternBound => WildcardType - case AnnotatedType(_, annot) if annot.symbol == defn.UncheckedAnnot => + case tp if tp.hasAnnotation(defn.UncheckedAnnot) => WildcardType case _ => mapOver(tp) } @@ -123,11 +124,6 @@ object TypeTestsCasts { } - lazy val scrutineeIsUnchecked = X.widenTermRefExpr.existsPart { - case AnnotatedType(_, annot) if annot.symbol == defn.UncheckedAnnot => true - case _ => false - } - def recur(X: Type, P: Type): Boolean = (X <:< P) || (P.dealias match { case _: SingletonType => true case _: TypeProxy @@ -157,26 +153,13 @@ object TypeTestsCasts { case AnnotatedType(t, _) => recur(X, t) case tp2: RefinedType => recur(X, tp2.parent) && TypeComparer.hasMatchingMember(tp2.refinedName, X, tp2) case tp2: RecType => recur(X, tp2.parent) - case tp2 - if tp2.typeSymbol.isLocal && !scrutineeIsUnchecked => - val sym = tp2.typeSymbol - val methodSymbol = sym.owner - val tpSyms = typer.ErrorReporting.substitutableTypeSymbolsInScope(sym).toSet - def isAccessible(sym: Symbol): Boolean = sym == methodSymbol || sym.isType && isAccessible(sym.owner) - val seen = scala.collection.mutable.Set.empty[Type] - def hasPoison(tp: Type): Boolean = - seen += tp - tp.baseClasses.filter(isAccessible).exists { sym => - sym.info.decls.exists { sym => - sym.info.existsPart(tp => tpSyms.contains(tp.typeSymbol)) - || !seen.contains(sym.info) && isAccessible(sym.info.typeSymbol.maybeOwner) && hasPoison(sym.info) - } - } - !hasPoison(tp2) + case _ + if P.classSymbol.isLocal && P.classSymbol.isInaccessibleChildOf(X.classSymbol) => // 8 + false case _ => true }) - val res = recur(X.widen, replaceP(P)) + val res = X.widenTermRefExpr.hasAnnotation(defn.UncheckedAnnot) || recur(X.widen, replaceP(P)) debug.println(i"checking ${X.show} isInstanceOf ${P} = $res") diff --git a/tests/neg/i4812.check b/tests/neg/i4812.check index 5333e6ad5cf2..6914a2f515de 100644 --- a/tests/neg/i4812.check +++ b/tests/neg/i4812.check @@ -22,3 +22,7 @@ 60 | case prev: A => // error: the type test for A cannot be checked at runtime | ^ | the type test for A cannot be checked at runtime +-- Error: tests/neg/i4812.scala:96:11 ---------------------------------------------------------------------------------- +96 | case x: B => // error: the type test for B cannot be checked at runtime + | ^ + | the type test for B cannot be checked at runtime diff --git a/tests/neg/i4812.scala b/tests/neg/i4812.scala index 7392f6b56c2c..2032dd5e8f09 100644 --- a/tests/neg/i4812.scala +++ b/tests/neg/i4812.scala @@ -80,6 +80,34 @@ object Test: case prev: A => prev.elem case _ => prev = new A(x); x + def test9 = + trait A + class B extends A + val x: A = new B + x match + case x: B => x + + sealed class A + var prevA: A = _ + def test10: A = + val methodCallId = System.nanoTime() + class B(val id: Long) extends A + prevA match + case x: B => // error: the type test for B cannot be checked at runtime + x.ensuring(x.id == methodCallId, s"Method call id $methodCallId != ${x.id}") + case _ => + val x = new B(methodCallId) + prevA = x + x + + def test11 = + trait A + trait B + class C extends A with B + val x: A = new C + x match + case x: B => x + def main(args: Array[String]): Unit = test(1) val x: String = test("") // was: ClassCastException: java.lang.Integer cannot be cast to java.lang.String From 7f51a86b8b7d8d45b09a3898c494c3ee026f8c4a Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Mon, 6 Jun 2022 14:45:20 +0100 Subject: [PATCH 6/8] Handle OrType in local class checking --- .../tools/dotc/transform/TypeTestsCasts.scala | 20 +++++++++---------- tests/neg/i4812.scala | 6 ++++++ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala index 0831e8db8803..a8024d603bb8 100644 --- a/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala +++ b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala @@ -154,7 +154,7 @@ object TypeTestsCasts { case tp2: RefinedType => recur(X, tp2.parent) && TypeComparer.hasMatchingMember(tp2.refinedName, X, tp2) case tp2: RecType => recur(X, tp2.parent) case _ - if P.classSymbol.isLocal && P.classSymbol.isInaccessibleChildOf(X.classSymbol) => // 8 + if P.classSymbol.isLocal && foundClasses(X, Nil).exists(P.classSymbol.isInaccessibleChildOf) => // 8 false case _ => true }) @@ -178,15 +178,6 @@ object TypeTestsCasts { def derivedTree(expr1: Tree, sym: Symbol, tp: Type) = cpy.TypeApply(tree)(expr1.select(sym).withSpan(expr.span), List(TypeTree(tp))) - def effectiveClass(tp: Type): Symbol = - if tp.isRef(defn.PairClass) then effectiveClass(erasure(tp)) - else if tp.isRef(defn.AnyValClass) then defn.AnyClass - else tp.classSymbol - - def foundClasses(tp: Type, acc: List[Symbol]): List[Symbol] = tp.dealias match - case OrType(tp1, tp2) => foundClasses(tp2, foundClasses(tp1, acc)) - case _ => effectiveClass(tp) :: acc - def inMatch = tree.fun.symbol == defn.Any_typeTest || // new scheme expr.symbol.is(Case) // old scheme @@ -376,4 +367,13 @@ object TypeTestsCasts { } interceptWith(expr) } + + private def effectiveClass(tp: Type)(using Context): Symbol = + if tp.isRef(defn.PairClass) then effectiveClass(erasure(tp)) + else if tp.isRef(defn.AnyValClass) then defn.AnyClass + else tp.classSymbol + + private def foundClasses(tp: Type, acc: List[Symbol])(using Context): List[Symbol] = tp.dealias match + case OrType(tp1, tp2) => foundClasses(tp2, foundClasses(tp1, acc)) + case _ => effectiveClass(tp) :: acc } diff --git a/tests/neg/i4812.scala b/tests/neg/i4812.scala index 2032dd5e8f09..fa38980f78d1 100644 --- a/tests/neg/i4812.scala +++ b/tests/neg/i4812.scala @@ -108,6 +108,12 @@ object Test: x match case x: B => x + def test12 = + class Foo + class Bar + val x: Foo | Bar = new Foo + x.isInstanceOf[Foo] + def main(args: Array[String]): Unit = test(1) val x: String = test("") // was: ClassCastException: java.lang.Integer cannot be cast to java.lang.String From ef6102a567e6daf67742abf00bf29029c879c8e5 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Mon, 13 Jun 2022 10:49:19 +0100 Subject: [PATCH 7/8] Test TypeTestsCasts.foundClasses & handle nesting in AndTypes --- .../tools/dotc/transform/TypeTestsCasts.scala | 15 ++++--- .../dotc/transform/TypeTestsCastsTest.scala | 45 +++++++++++++++++++ 2 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 compiler/test/dotty/tools/dotc/transform/TypeTestsCastsTest.scala diff --git a/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala index a8024d603bb8..c92fd5abe07a 100644 --- a/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala +++ b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala @@ -154,7 +154,7 @@ object TypeTestsCasts { case tp2: RefinedType => recur(X, tp2.parent) && TypeComparer.hasMatchingMember(tp2.refinedName, X, tp2) case tp2: RecType => recur(X, tp2.parent) case _ - if P.classSymbol.isLocal && foundClasses(X, Nil).exists(P.classSymbol.isInaccessibleChildOf) => // 8 + if P.classSymbol.isLocal && foundClasses(X).exists(P.classSymbol.isInaccessibleChildOf) => // 8 false case _ => true }) @@ -246,7 +246,7 @@ object TypeTestsCasts { if expr.tpe.isBottomType then report.warning(TypeTestAlwaysDiverges(expr.tpe, testType), tree.srcPos) val nestedCtx = ctx.fresh.setNewTyperState() - val foundClsSyms = foundClasses(expr.tpe.widen, Nil) + val foundClsSyms = foundClasses(expr.tpe.widen) val sensical = checkSensical(foundClsSyms)(using nestedCtx) if (!sensical) { nestedCtx.typerState.commit() @@ -267,7 +267,7 @@ object TypeTestsCasts { def transformAsInstanceOf(testType: Type): Tree = { def testCls = effectiveClass(testType.widen) def foundClsSymPrimitive = { - val foundClsSyms = foundClasses(expr.tpe.widen, Nil) + val foundClsSyms = foundClasses(expr.tpe.widen) foundClsSyms.size == 1 && foundClsSyms.head.isPrimitiveValueClass } if (erasure(expr.tpe) <:< testType) @@ -373,7 +373,10 @@ object TypeTestsCasts { else if tp.isRef(defn.AnyValClass) then defn.AnyClass else tp.classSymbol - private def foundClasses(tp: Type, acc: List[Symbol])(using Context): List[Symbol] = tp.dealias match - case OrType(tp1, tp2) => foundClasses(tp2, foundClasses(tp1, acc)) - case _ => effectiveClass(tp) :: acc + private[transform] def foundClasses(tp: Type)(using Context): List[Symbol] = + def go(tp: Type, acc: List[Type])(using Context): List[Type] = tp.dealias match + case OrType(tp1, tp2) => go(tp2, go(tp1, acc)) + case AndType(tp1, tp2) => (for t1 <- go(tp1, Nil); t2 <- go(tp2, Nil); yield AndType(t1, t2)) ::: acc + case _ => tp :: acc + go(tp, Nil).map(effectiveClass) } diff --git a/compiler/test/dotty/tools/dotc/transform/TypeTestsCastsTest.scala b/compiler/test/dotty/tools/dotc/transform/TypeTestsCastsTest.scala new file mode 100644 index 000000000000..0db7a6072579 --- /dev/null +++ b/compiler/test/dotty/tools/dotc/transform/TypeTestsCastsTest.scala @@ -0,0 +1,45 @@ +package dotty.tools +package dotc +package transform + +import core.* +import Contexts.*, Decorators.*, Denotations.*, SymDenotations.*, Symbols.*, Types.* +import Annotations.* + +import org.junit.Test +import org.junit.Assert.* + +class TypeTestsCastsTest extends DottyTest: + val defn = ctx.definitions; import defn.* + + @Test def orL = checkFound(List(StringType, LongType), OrType(LongType, StringType, false)) + @Test def orR = checkFound(List(LongType, StringType), OrType(StringType, LongType, false)) + + @Test def annot = checkFound(List(StringType, LongType), AnnotatedType(OrType(LongType, StringType, false), Annotation(defn.UncheckedAnnot))) + + @Test def andL = checkFound(List(StringType), AndType(StringType, AnyType)) + @Test def andR = checkFound(List(StringType), AndType(AnyType, StringType)) + @Test def andX = checkFound(List(NoType), AndType(StringType, BooleanType)) + + // (A | B) & C => {(A & B), (A & C)} + // A & (B | C) => {(A & B), (A & C)} + // (A | B) & (C | D) => {(A & C), (A & D), (B & C), (B & D)} + @Test def orInAndL = checkFound(List(StringType, LongType), AndType(OrType(LongType, StringType, false), AnyType)) + @Test def orInAndR = checkFound(List(StringType, LongType), AndType(AnyType, OrType(LongType, StringType, false))) + @Test def orInAndZ = + // (Throwable | Exception) & (RuntimeException | Any) = + // Throwable & RuntimeException = RuntimeException + // Throwable & Any = Throwable + // Exception & RuntimeException = RuntimeException + // Exception & Any = Exception + val ExceptionType = defn.ExceptionClass.typeRef + val RuntimeExceptionType = defn.RuntimeExceptionClass.typeRef + val tp = AndType(OrType(ThrowableType, ExceptionType, false), OrType(RuntimeExceptionType, AnyType, false)) + val exp = List(ExceptionType, RuntimeExceptionType, ThrowableType, RuntimeExceptionType) + checkFound(exp, tp) + + def checkFound(found: List[Type], tp: Type) = + val expected = found.map(_.classSymbol) + val obtained = TypeTestsCasts.foundClasses(tp) + assertEquals(expected, obtained) +end TypeTestsCastsTest From 3ca5b70737312ea33854ab7e70fafe317fd33fbf Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Tue, 5 Jul 2022 18:35:59 +0100 Subject: [PATCH 8/8] Revert quadratic blowup on mapping AnnotatedType --- compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala index c92fd5abe07a..88f06122310e 100644 --- a/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala +++ b/compiler/src/dotty/tools/dotc/transform/TypeTestsCasts.scala @@ -59,7 +59,7 @@ object TypeTestsCasts { def apply(tp: Type) = tp match { case tref: TypeRef if tref.typeSymbol.isPatternBound => WildcardType - case tp if tp.hasAnnotation(defn.UncheckedAnnot) => + case AnnotatedType(_, annot) if annot.symbol == defn.UncheckedAnnot => WildcardType case _ => mapOver(tp) }