From 5975a0692b29e06f9905b382e5e175dcd88ce2dc 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. --- .../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 b6e8dc9a2c76..64a79e195ca7 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 ce56bc976ee0..f8a7a4b371ee 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 43af5114bdc969a6f5fb77d3357928cfb24e8355 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... --- 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 470be4705071e8a3b146f6f8aa259eb1bcd6581c 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` --- 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 64a79e195ca7..b0e58b2f6696 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 f8a7a4b371ee..ca6fff87ee14 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 0ababfb03a36..8070dcbeda30 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 546fbd1d18a813bab024aa8fb4a6d9253a229ec9 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 --- 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 b0e58b2f6696..d534db7df1ce 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`.