Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions compiler/src/dotty/tools/dotc/core/TypeComparer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import NameKinds.WildcardParamName
import MatchTypes.isConcrete
import reporting.Message.Note
import scala.util.boundary, boundary.break
import dotty.tools.dotc.util.Property
import scala.collection.mutable.Stack

/** Provides methods to compare types.
*/
Expand Down Expand Up @@ -3594,6 +3596,7 @@ object MatchReducer:
case Stuck => "Stuck"
case NoInstance(fails) => "NoInstance(" ~ Text(fails.map(p.toText(_) ~ p.toText(_)), ", ") ~ ")"


/** A type comparer for reducing match types.
* TODO: Not sure this needs to be a type comparer. Can we make it a
* separate class?
Expand Down Expand Up @@ -3941,6 +3944,7 @@ class MatchReducer(initctx: Context) extends TypeComparer(initctx) {
NoType
case MatchResult.Reduced(tp) =>
tp.simplified

case MatchResult.ReducedAndDisjoint =>
// Empty types break the basic assumption that if a scrutinee and a
// pattern are disjoint it's OK to reduce passed that pattern. Indeed,
Expand Down
45 changes: 38 additions & 7 deletions compiler/src/dotty/tools/dotc/core/Types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,17 @@ import cc.*
import CaptureSet.IdentityCaptRefMap
import Capabilities.*
import transform.Recheck.currentRechecker
import scala.collection.immutable.HashMap
import dotty.tools.dotc.util.Property
import dotty.tools.dotc.reporting.NonDecreasingMatchReduction

import scala.annotation.internal.sharable
import scala.annotation.threadUnsafe

object Types extends TypeUtils {

val Reduction = new Property.Key[HashMap[Type, List[Int]]]

@sharable private var nextId = 0

implicit def eqType: CanEqual[Type, Type] = CanEqual.derived
Expand Down Expand Up @@ -1628,7 +1633,27 @@ object Types extends TypeUtils {
* then the result after applying all toplevel normalizations, otherwise NoType.
*/
def tryNormalize(using Context): Type = underlyingNormalizable match
case mt: MatchType => mt.reduced.normalized
case mt: MatchType =>
this match
case self: AppliedType =>
println(i"Applied match type alias: ${self.tycon} with args ${self.args}")
val currentListSize = self.args.map(_.typeSize())
val currentSize = currentListSize.sum
println(i"Arguments Size: ${currentSize} (${currentListSize})")
val argumentsId = self.args.map(_.hashCode().toHexString)
println(i"Arguments Ids: ${argumentsId}")
val history = ctx.property(Reduction).getOrElse(Map.empty)

if history.contains(self.tycon) && currentSize >= history(self.tycon).sum then
val prevSize = history(self.tycon).sum
ErrorType(NonDecreasingMatchReduction(self, prevSize, currentSize))
else
val newHistory = history.updated(self.tycon, currentListSize)
val result =
given Context = ctx.fresh.setProperty(Reduction, newHistory)
mt.reduced.normalized
result
case _ => mt.reduced.normalized
case tp: AppliedType => tp.tryCompiletimeConstantFold
case _ => NoType

Expand Down Expand Up @@ -2095,8 +2120,8 @@ object Types extends TypeUtils {
(new CoveringSetAccumulator).apply(Set.empty[Symbol], this)

/** The number of applications and refinements in this type, after all aliases are expanded */
def typeSize(using Context): Int =
(new TypeSizeAccumulator).apply(0, this)
def typeSize(normalizeMatchTypes: Boolean = true)(using Context): Int =
(new TypeSizeAccumulator).apply(0, this, normalizeMatchTypes)

/** Convert to text */
def toText(printer: Printer): Text = printer.toText(this)
Expand Down Expand Up @@ -7123,12 +7148,16 @@ object Types extends TypeUtils {

class TypeSizeAccumulator(using Context) extends TypeAccumulator[Int] {
var seen = util.HashSet[Type](initialCapacity = 8)
def apply(n: Int, tp: Type): Int =
def apply(n: Int, tp: Type, normalizeMatchTypes: Boolean): Int =
tp match {
case tp: AppliedType =>
val tpNorm = tp.tryNormalize
if tpNorm.exists then apply(n, tpNorm)
else foldOver(n + 1, tp)
if !normalizeMatchTypes && tp.isMatchAlias then
println(s"TypeSizeAccumulator: skipping match alias ${tp.show}")
foldOver(n + 1, tp)
else
val tpNorm = tp.tryNormalize
if tpNorm.exists then apply(n, tpNorm)
else foldOver(n + 1, tp)
case tp: RefinedType =>
foldOver(n + 1, tp)
case tp: TypeRef if tp.info.isTypeAlias =>
Expand All @@ -7146,6 +7175,8 @@ object Types extends TypeUtils {
case _ =>
foldOver(n, tp)
}

def apply(n: Int, tp: Type): Int = apply(n, tp, normalizeMatchTypes = false)
}

class CoveringSetAccumulator(using Context) extends TypeAccumulator[Set[Symbol]] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe
case DefaultShadowsGivenID // errorNumber: 220
case RecurseWithDefaultID // errorNumber: 221
case EncodedPackageNameID // errorNumber: 222
case NonDecreasingMatchReductionID // errorNumber: 223

def errorNumber = ordinal - 1

Expand Down
5 changes: 5 additions & 0 deletions compiler/src/dotty/tools/dotc/reporting/messages.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3321,6 +3321,11 @@ class MatchTypeLegacyPattern(errorText: String)(using Context) extends TypeMsg(M
def msg(using Context) = errorText
def explain(using Context) = ""

class NonDecreasingMatchReduction(tpe: Type, originalSize: Int, newSize: Int)(using Context)
extends TypeMsg(NonDecreasingMatchReductionID):
def msg(using Context) = i"Match type reduction failed: Non-decreasing argument size detected for $tpe. Size went from $originalSize to $newSize."
def explain(using Context) = ""

class ClosureCannotHaveInternalParameterDependencies(mt: Type)(using Context)
extends TypeMsg(ClosureCannotHaveInternalParameterDependenciesID):
def msg(using Context) =
Expand Down
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/typer/Implicits.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1827,7 +1827,7 @@ trait Implicits:

/** Fields needed for divergence checking */
@threadUnsafe lazy val ptCoveringSet = wideProto.coveringSet
@threadUnsafe lazy val ptSize = wideProto.typeSize
@threadUnsafe lazy val ptSize = wideProto.typeSize()
@threadUnsafe lazy val wildPt = wildApprox(wideProto)

/**
Expand Down Expand Up @@ -1967,7 +1967,7 @@ case class OpenSearch(cand: Candidate, pt: Type, outer: SearchHistory)(using Con
// by nested implicit searches, thus leading to types in search histories
// that grow larger the deeper the search gets. This can mask divergence.
// An example is in neg/9504.scala
lazy val typeSize = pt.typeSize
lazy val typeSize = pt.typeSize()
lazy val coveringSet = pt.coveringSet
end OpenSearch

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class DivergenceCheckerTests extends DottyTest {

tpes.lazyZip(expectedSizes).lazyZip(expectedCoveringSets).foreach {
case (tpe, expectedSize, expectedCoveringSet) =>
val size = tpe.typeSize
val size = tpe.typeSize()
val cs = tpe.coveringSet

assertTrue(size == expectedSize)
Expand Down
12 changes: 12 additions & 0 deletions tests/neg/i22587/Test02.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Test Case 2: Direct Self-Reference Without Reduction (NEGATIVE)
// This SHOULD diverge because Loop[X] immediately expands to Loop[X]
// with no progress made - infinite loop without termination

type Loop[X] = X match
case Int => Loop[Int]
case _ => String

@main def test02(): Unit =
val e1: Loop[Int] = ??? // error
println("Test 2 - Direct self-reference:")
println(s"e1 value: $e1")
11 changes: 11 additions & 0 deletions tests/neg/i22587/Test03.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Test Case 3: Wrapping Type Without Progress (NEGATIVE)
// This SHOULD diverge because Wrap[Int] => Wrap[List[Int]] => Wrap[List[List[Int]]] => ...
// The type grows infinitely without reaching a base case

type Wrap[X] = X match
case AnyVal => Wrap[List[Int]]

@main def test03(): Unit =
val e1: Wrap[Int] = ??? // error
Copy link
Member

@bishabosha bishabosha Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

List[Int] is not AnyVal , so then it stops there right?

println("Test 3 - Wrapping without progress:")
println(s"e1 value: $e1")
14 changes: 14 additions & 0 deletions tests/neg/i22587/Test05.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Test Case 5: Two-Parameter Match Type with Swapping (Negative)
// This should diverge because even though recursion should reduce,
// the sum of parameters size does not decrease in one recursive step.

type Swap[X, Y] = (X, Y) match
case (Int, String) => Swap[Y, X]
case (String, Int) => X
case (List[a], b) => Swap[a, b]
case (a, List[b]) => Swap[a, b]
case _ => X

@main def test05(): Unit =
val e: Swap[List[List[Int]], List[String]] = ??? // error
println(s"e value: $e")
18 changes: 18 additions & 0 deletions tests/pos/i22587/Test01.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Test Case 1: Simple Recursive Deconstruction (POSITIVE)
// This should NOT diverge because recursion always reduces the type structure
// by unwrapping one layer (Option[t] -> t)

type Unwrap[X] = X match
case Option[t] => Unwrap[t]
case _ => X

@main def test01(): Unit =
val e1: Unwrap[Option[Option[Int]]] = 42
println("Test 1 - Simple recursive unwrapping:")
println(s"e1 value: $e1")

val e2: Unwrap[Option[String]] = "hello"
println(s"e2 value: $e2")

val e3: Unwrap[Int] = 99
println(s"e3 value: $e3")
21 changes: 21 additions & 0 deletions tests/pos/i22587/Test04.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Test Case 4: Two-Parameter Match Type with Swapping (POSITIVE)
// This should NOT diverge because even though parameters swap positions,
// the tuple structure reduces (Tuple2 -> single type)

type Swap[X, Y] = (X, Y) match
case (Int, String) => Y
case (String, Int) => X
case (List[a], b) => Swap[a, b]
case (a, List[b]) => Swap[a, b]
case _ => X

@main def test04(): Unit =
val e1: Swap[Int, String] = "result"
println("Test 4 - Two-parameter swap:")
println(s"e1 value: $e1")

val e2: Swap[String, Int] = "swapped"
println(s"e2 value: $e2")

val e3: Swap[List[List[Int]], List[String]] = "42"
println(s"e3 value: $e3")
Loading