From 874dfa914f9af52eaecf596c1935fc63f271f0d0 Mon Sep 17 00:00:00 2001 From: Eugene Flesselle Date: Thu, 28 Aug 2025 20:15:16 +0200 Subject: [PATCH 1/4] Fix `derivesFrom` false negative in `provablyDisjointClasses` Before the addition of `SymDenotation#mayDeriveFrom`, tests/neg/i17132.min.scala was unsoundly accepted without a type error because `R[T]` is reduced to `Any` where `T <: P[Any]` by testing `provablyDisjointClasses` before `P` is added as a baseClass of `Q`. Now, a recursion overflows occurs because: - reducing `R[T] := T match case Q[t] => R[t]; case ...` requires - proving `T` disjoint from `Q[t]` where `T <: P[Any]`, which asks - whether existsCommonBaseTypeWithDisjointArguments, which requires - normalizing the type arg to the base class P for the class Q, i.e. R[t] - ... In short, despite the use of the pending set in provablyDisjoint, diverging is still possible when there is "cycle" in the type arguments in the base classes, (and where the pending set is thus reset for a separate normalization problem). One could attempt to add some more logic to detect these loops s.t. they are considered "stuck", as opposed to reporting an error. But it isn't clear that there are any concrete motivations for this. [Cherry-picked 5975a0692b29e06f9905b382e5e175dcd88ce2dc] --- .../tools/dotc/core/SymDenotations.scala | 11 +++++++ .../dotty/tools/dotc/core/TypeComparer.scala | 2 +- tests/neg/i17132.min.check | 31 +++++++++++++++++++ tests/neg/i17132.min.scala | 9 ++++++ 4 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 tests/neg/i17132.min.check create mode 100644 tests/neg/i17132.min.scala diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index e17f127b7714..268f1d621815 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -865,9 +865,20 @@ object SymDenotations { * and is the denoting symbol also different from `Null` or `Nothing`? * @note erroneous classes are assumed to derive from all other classes * and all classes derive from them. + * @note may return a false negative when `this.info.isInstanceOf[TempClassInfo]`. */ def derivesFrom(base: Symbol)(using Context): Boolean = false + /** Could `this` derive from `base` now or in the future. + * For concistency with derivesFrom, The info is only forced when this is a ClassDenotation. + * If the info is a TempClassInfo then the baseClassSet may be temporarily approximated as empty. + * This is problematic when stability of `!derivesFrom(base)` is assumed for soundness, + * e.g., in `TypeComparer#provablyDisjointClasses`. + * @note may return a false positive when `this.info.isInstanceOf[TempClassInfo]`. + */ + final def mayDeriveFrom(base: Symbol)(using Context): Boolean = + this.isInstanceOf[ClassDenotation] && (info.isInstanceOf[TempClassInfo] || derivesFrom(base)) + /** Is this a Scala or Java annotation ? */ def isAnnotation(using Context): Boolean = isClass && (derivesFrom(defn.AnnotationClass) || is(JavaAnnotation)) diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 7f8f8a34c171..cc9ba4396fc5 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -3230,7 +3230,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling .filter(child => child.exists && child != cls) def eitherDerivesFromOther(cls1: Symbol, cls2: Symbol): Boolean = - cls1.derivesFrom(cls2) || cls2.derivesFrom(cls1) + cls1.mayDeriveFrom(cls2) || cls2.mayDeriveFrom(cls1) def smallestNonTraitBase(cls: Symbol): Symbol = val classes = if cls.isClass then cls.asClass.baseClasses else cls.info.classSymbols diff --git a/tests/neg/i17132.min.check b/tests/neg/i17132.min.check new file mode 100644 index 000000000000..f23d3e91549a --- /dev/null +++ b/tests/neg/i17132.min.check @@ -0,0 +1,31 @@ +-- Error: tests/neg/i17132.min.scala:4:7 ------------------------------------------------------------------------------- +4 |class Q[T <: P[Any]] extends P[R[T]] // error + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | Recursion limit exceeded. + | Maybe there is an illegal cyclic reference? + | If that's not the case, you could also try to increase the stacksize using the -Xss JVM option. + | For the unprocessed stack trace, compile with -Xno-enrich-error-messages. + | A recurring operation is (inner to outer): + | + | reduce type t match ... + | reduce type t match ... + | reduce type t match ... + | reduce type t match ... + | reduce type t match ... + | reduce type t match ... + | reduce type t match ... + | reduce type t match ... + | reduce type t match ... + | reduce type t match ... + | ... + | + | reduce type t match ... + | reduce type t match ... + | reduce type t match ... + | reduce type t match ... + | reduce type t match ... + | reduce type t match ... + | reduce type t match ... + | reduce type t match ... + | reduce type t match ... + | reduce type T match ... diff --git a/tests/neg/i17132.min.scala b/tests/neg/i17132.min.scala new file mode 100644 index 000000000000..903b19e5cfed --- /dev/null +++ b/tests/neg/i17132.min.scala @@ -0,0 +1,9 @@ + +class P[T] +//class Q[T] extends P[R[T]] // ok +class Q[T <: P[Any]] extends P[R[T]] // error +//type Q[T <: P[Any]] <: P[R[T]] // ok + +type R[U] = U match + case Q[t] => R[t] + case P[t] => t From e3c9bb1e379cb5b51b929ab70e4cd241e6ef60ef Mon Sep 17 00:00:00 2001 From: Eugene Flesselle Date: Thu, 28 Aug 2025 20:16:21 +0200 Subject: [PATCH 2/4] Close #17132 as neg test For some reason, the derivesFrom issue was only observable in the minimization. The original test case diverges similarly to the previous description, but only when attempting normalizing during CodeGen... [Cherry-picked 43af5114bdc969a6f5fb77d3357928cfb24e8355] --- compiler/src/dotty/tools/backend/jvm/CodeGen.scala | 3 +-- tests/neg/i17132.scala | 11 +++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 tests/neg/i17132.scala diff --git a/compiler/src/dotty/tools/backend/jvm/CodeGen.scala b/compiler/src/dotty/tools/backend/jvm/CodeGen.scala index be86f704fa41..71d25d6f0cf2 100644 --- a/compiler/src/dotty/tools/backend/jvm/CodeGen.scala +++ b/compiler/src/dotty/tools/backend/jvm/CodeGen.scala @@ -85,8 +85,7 @@ class CodeGen(val int: DottyBackendInterface, val primitives: DottyPrimitives)( case ex: InterruptedException => throw ex case ex: CompilationUnit.SuspendException => throw ex case ex: Throwable => - ex.printStackTrace() - report.error(s"Error while emitting ${unit.source}\n${ex.getMessage}", NoSourcePosition) + report.error(s"Error while emitting ${unit.source}\n${ex.getMessage}", cd.sourcePos) def genTastyAndSetAttributes(claszSymbol: Symbol, store: ClassNode): Unit = diff --git a/tests/neg/i17132.scala b/tests/neg/i17132.scala new file mode 100644 index 000000000000..d4b97e54293a --- /dev/null +++ b/tests/neg/i17132.scala @@ -0,0 +1,11 @@ + +sealed trait Transformation[T] + +case object Count extends Transformation[Int] +case class MultiTransformation[T1 <: Transformation[?], T2 <: Transformation[?]](t1: T1, t2: T2) // error cyclic + extends Transformation[MultiTransformationResult[T1, T2]] + +type MultiTransformationResult[T1 <: Transformation[?], T2 <: Transformation[?]] <: Tuple = (T1, T2) match { + case (Transformation[t], MultiTransformation[t1, t2]) => t *: MultiTransformationResult[t1, t2] + case (Transformation[t1], Transformation[t2]) => (t1, t2) +} From e8f8c2be73d33e42018254470bbac817e2369e69 Mon Sep 17 00:00:00 2001 From: Eugene Flesselle Date: Thu, 28 Aug 2025 20:43:11 +0200 Subject: [PATCH 3/4] Use more `mayDeriveFrom` where appropriate in `provablyDisjoint` [Cherry-picked 470be4705071e8a3b146f6f8aa259eb1bcd6581c] --- compiler/src/dotty/tools/dotc/core/SymDenotations.scala | 3 +++ compiler/src/dotty/tools/dotc/core/TypeComparer.scala | 6 +++--- compiler/src/dotty/tools/dotc/core/Types.scala | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 268f1d621815..2561aee03842 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -879,6 +879,9 @@ object SymDenotations { final def mayDeriveFrom(base: Symbol)(using Context): Boolean = this.isInstanceOf[ClassDenotation] && (info.isInstanceOf[TempClassInfo] || derivesFrom(base)) + final def derivesFrom(base: Symbol, defaultIfUnknown: Boolean)(using Context): Boolean = + if defaultIfUnknown/*== true*/ then mayDeriveFrom(base) else derivesFrom(base) + /** Is this a Scala or Java annotation ? */ def isAnnotation(using Context): Boolean = isClass && (derivesFrom(defn.AnnotationClass) || is(JavaAnnotation)) diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index cc9ba4396fc5..d420590d0c40 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -3131,9 +3131,9 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling * unique value derives from the class. */ case (tp1: SingletonType, tp2) => - !tp1.derivesFrom(tp2.classSymbol) + !tp1.derivesFrom(tp2.classSymbol, defaultIfUnknown = true) case (tp1, tp2: SingletonType) => - !tp2.derivesFrom(tp1.classSymbol) + !tp2.derivesFrom(tp1.classSymbol, defaultIfUnknown = true) /* Now both sides are possibly-parameterized class types `p.C[Ts]` and `q.D[Us]`. * @@ -3189,7 +3189,7 @@ class TypeComparer(@constructorOnly initctx: Context) extends ConstraintHandling val cls2BaseClassSet = SymDenotations.BaseClassSet(cls2.classDenot.baseClasses) val commonBaseClasses = cls1.classDenot.baseClasses.filter(cls2BaseClassSet.contains(_)) def isAncestorOfOtherBaseClass(cls: ClassSymbol): Boolean = - commonBaseClasses.exists(other => (other ne cls) && other.derivesFrom(cls)) + commonBaseClasses.exists(other => (other ne cls) && other.mayDeriveFrom(cls)) val result = commonBaseClasses.exists { baseClass => !isAncestorOfOtherBaseClass(baseClass) && isBaseTypeWithDisjointArguments(baseClass, innerPending) } diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 7fac8c818a1a..5b195802f969 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -270,7 +270,7 @@ object Types extends TypeUtils { /** True if this type is an instance of the given `cls` or an instance of * a non-bottom subclass of `cls`. */ - final def derivesFrom(cls: Symbol)(using Context): Boolean = { + final def derivesFrom(cls: Symbol, defaultIfUnknown: Boolean = false)(using Context): Boolean = { def isLowerBottomType(tp: Type) = tp.isBottomType && (tp.hasClassSymbol(defn.NothingClass) @@ -278,7 +278,7 @@ object Types extends TypeUtils { def loop(tp: Type): Boolean = try tp match case tp: TypeRef => val sym = tp.symbol - if (sym.isClass) sym.derivesFrom(cls) else loop(tp.superType) + if (sym.isClass) sym.derivesFrom(cls, defaultIfUnknown) else loop(tp.superType) case tp: AppliedType => tp.superType.derivesFrom(cls) case tp: MatchType => From 13d0a613c611fee47e8421cdb8271e73a279f5df Mon Sep 17 00:00:00 2001 From: Eugene Flesselle Date: Fri, 29 Aug 2025 14:42:47 +0200 Subject: [PATCH 4/4] Address review comments [Cherry-picked 546fbd1d18a813bab024aa8fb4a6d9253a229ec9] --- compiler/src/dotty/tools/backend/jvm/CodeGen.scala | 1 + compiler/src/dotty/tools/dotc/core/SymDenotations.scala | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/backend/jvm/CodeGen.scala b/compiler/src/dotty/tools/backend/jvm/CodeGen.scala index 71d25d6f0cf2..1e2abbcff866 100644 --- a/compiler/src/dotty/tools/backend/jvm/CodeGen.scala +++ b/compiler/src/dotty/tools/backend/jvm/CodeGen.scala @@ -85,6 +85,7 @@ class CodeGen(val int: DottyBackendInterface, val primitives: DottyPrimitives)( case ex: InterruptedException => throw ex case ex: CompilationUnit.SuspendException => throw ex case ex: Throwable => + if !ex.isInstanceOf[TypeError] then ex.printStackTrace() report.error(s"Error while emitting ${unit.source}\n${ex.getMessage}", cd.sourcePos) diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 2561aee03842..9fe08c27a159 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -870,7 +870,7 @@ object SymDenotations { def derivesFrom(base: Symbol)(using Context): Boolean = false /** Could `this` derive from `base` now or in the future. - * For concistency with derivesFrom, The info is only forced when this is a ClassDenotation. + * For concistency with derivesFrom, the info is only forced when this is a ClassDenotation. * If the info is a TempClassInfo then the baseClassSet may be temporarily approximated as empty. * This is problematic when stability of `!derivesFrom(base)` is assumed for soundness, * e.g., in `TypeComparer#provablyDisjointClasses`.