Skip to content

Commit 3423338

Browse files
committed
Implement strict size-decreasing divergence check for Match Types
This is the first step towards implementing the full divergence check algorithm for Match Types. The logic enforces that recursive reduction steps must have a strictly smaller size than the sum of the type sizes of the scrutinee. This check applies to match types during reduction as well as those that do not reduce. When the algorithm detects a non-decreasing size, it returns an ErrorType to prevent infinite recursion. This work is part of the research project specified by Martin Odersky in issue #22587.
1 parent c6051b4 commit 3423338

File tree

8 files changed

+107
-1
lines changed

8 files changed

+107
-1
lines changed

compiler/src/dotty/tools/dotc/core/Types.scala

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,17 @@ import cc.*
4343
import CaptureSet.IdentityCaptRefMap
4444
import Capabilities.*
4545
import transform.Recheck.currentRechecker
46+
import scala.collection.immutable.HashMap
47+
import dotty.tools.dotc.util.Property
48+
import dotty.tools.dotc.reporting.NonDecreasingMatchReduction
4649

4750
import scala.annotation.internal.sharable
4851
import scala.annotation.threadUnsafe
4952

5053
object Types extends TypeUtils {
5154

55+
val Reduction = new Property.Key[HashMap[Type, List[Int]]]
56+
5257
@sharable private var nextId = 0
5358

5459
implicit def eqType: CanEqual[Type, Type] = CanEqual.derived
@@ -1628,7 +1633,25 @@ object Types extends TypeUtils {
16281633
* then the result after applying all toplevel normalizations, otherwise NoType.
16291634
*/
16301635
def tryNormalize(using Context): Type = underlyingNormalizable match
1631-
case mt: MatchType => mt.reduced.normalized
1636+
case mt: MatchType =>
1637+
this match
1638+
case self: AppliedType =>
1639+
report.log(i"AppliedType with underlying MatchType: ${self.tycon}${self.args}")
1640+
val currentListSize = self.args.map(_.typeSize)
1641+
val currentSize = currentListSize.sum
1642+
report.log(i"Arguments Size: ${currentListSize}")
1643+
val history = ctx.property(Reduction).getOrElse(Map.empty)
1644+
1645+
if history.contains(self.tycon) && currentSize >= history(self.tycon).sum then
1646+
val prevSize = history(self.tycon).sum
1647+
ErrorType(NonDecreasingMatchReduction(self, prevSize, currentSize))
1648+
else
1649+
val newHistory = history.updated(self.tycon, currentListSize)
1650+
val result =
1651+
given Context = ctx.fresh.setProperty(Reduction, newHistory)
1652+
mt.reduced.normalized
1653+
result
1654+
case _ => mt.reduced.normalized
16321655
case tp: AppliedType => tp.tryCompiletimeConstantFold
16331656
case _ => NoType
16341657

compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe
236236
case DefaultShadowsGivenID // errorNumber: 220
237237
case RecurseWithDefaultID // errorNumber: 221
238238
case EncodedPackageNameID // errorNumber: 222
239+
case NonDecreasingMatchReductionID // errorNumber: 223
239240

240241
def errorNumber = ordinal - 1
241242

compiler/src/dotty/tools/dotc/reporting/messages.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3321,6 +3321,11 @@ class MatchTypeLegacyPattern(errorText: String)(using Context) extends TypeMsg(M
33213321
def msg(using Context) = errorText
33223322
def explain(using Context) = ""
33233323

3324+
class NonDecreasingMatchReduction(tpe: Type, originalSize: Int, newSize: Int)(using Context)
3325+
extends TypeMsg(NonDecreasingMatchReductionID):
3326+
def msg(using Context) = i"Match type reduction failed: Non-decreasing argument size detected for $tpe. Size went from $originalSize to $newSize."
3327+
def explain(using Context) = ""
3328+
33243329
class ClosureCannotHaveInternalParameterDependencies(mt: Type)(using Context)
33253330
extends TypeMsg(ClosureCannotHaveInternalParameterDependenciesID):
33263331
def msg(using Context) =

tests/neg/i22587-neg-A.scala

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Test Case 2: Direct Self-Reference Without Reduction (NEGATIVE)
2+
// This SHOULD diverge because Loop[X] immediately expands to Loop[X]
3+
// with no progress made - infinite loop without termination
4+
5+
type Loop[X] = X match
6+
case Int => Loop[Int]
7+
case _ => String
8+
9+
@main def test02(): Unit =
10+
val e1: Loop[Int] = ??? // error
11+
println("Test 2 - Direct self-reference:")
12+
println(s"e1 value: $e1")

tests/neg/i22587-neg-B.scala

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Test Case 3: Wrapping Type Without Progress (NEGATIVE)
2+
// This SHOULD diverge because Wrap[Int] => Wrap[List[Int]] => Wrap[List[List[Int]]] => ...
3+
// The type grows infinitely without reaching a base case
4+
5+
type Wrap[X] = X match
6+
case AnyVal => Wrap[List[X]]
7+
case AnyRef => Wrap[List[X]]
8+
9+
@main def test03(): Unit =
10+
val e1: Wrap[Int] = ??? // error
11+
println("Test 3 - Wrapping without progress:")
12+
println(s"e1 value: $e1")

tests/neg/i22587-neg-C.scala

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Test Case 5: Two-Parameter Match Type with Swapping (Negative)
2+
// This should diverge because even though recursion should reduce,
3+
// the sum of parameters size does not decrease in one recursive step.
4+
5+
type Swap[X, Y] = (X, Y) match
6+
case (Int, String) => Swap[Y, X]
7+
case (String, Int) => X
8+
case (List[a], b) => Swap[a, b]
9+
case (a, List[b]) => Swap[a, b]
10+
case _ => X
11+
12+
@main def test05(): Unit =
13+
val e: Swap[List[List[Int]], List[String]] = ??? // error
14+
println(s"e value: $e")

tests/pos/i22587-pos-A.scala

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Test Case 1: Simple Recursive Deconstruction (POSITIVE)
2+
// This should NOT diverge because recursion always reduces the type structure
3+
// by unwrapping one layer (Option[t] -> t)
4+
5+
type Unwrap[X] = X match
6+
case Option[t] => Unwrap[t]
7+
case _ => X
8+
9+
@main def test01(): Unit =
10+
val e1: Unwrap[Option[Option[Int]]] = 42
11+
println("Test 1 - Simple recursive unwrapping:")
12+
println(s"e1 value: $e1")
13+
14+
val e2: Unwrap[Option[String]] = "hello"
15+
println(s"e2 value: $e2")
16+
17+
val e3: Unwrap[Int] = 99
18+
println(s"e3 value: $e3")

tests/pos/i22587-pos-B.scala

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Test Case 4: Two-Parameter Match Type with Swapping (POSITIVE)
2+
// This should NOT diverge because even though parameters swap positions,
3+
// the tuple structure reduces (Tuple2 -> single type)
4+
5+
type Swap[X, Y] = (X, Y) match
6+
case (Int, String) => Y
7+
case (String, Int) => X
8+
case (List[a], b) => Swap[a, b]
9+
case (a, List[b]) => Swap[a, b]
10+
case _ => X
11+
12+
@main def test04(): Unit =
13+
val e1: Swap[Int, String] = "result"
14+
println("Test 4 - Two-parameter swap:")
15+
println(s"e1 value: $e1")
16+
17+
val e2: Swap[String, Int] = "swapped"
18+
println(s"e2 value: $e2")
19+
20+
val e3: Swap[List[List[Int]], List[String]] = "42"
21+
println(s"e3 value: $e3")

0 commit comments

Comments
 (0)