Skip to content

Commit 5f55a9a

Browse files
committed
Refine divergence check: lexicographical order and same-size steps
This commit relaxes the previous strict size-decreasing constraint for Match Type reduction, allowing for more expressive recursive types while maintaining termination guarantees. Key changes: 1. Lexicographical Comparison: Instead of comparing the sum of argument sizes, the algorithm now compares the list of argument sizes lexicographically. This provides a more precise metric for progress. 2. Bounded Variation: Reductions are no longer required to strictly decrease in size at every step. Same-size reductions are now allowed, provided the sequence of arguments has not been seen before in the current reduction chain (cycle detection). 3. History Stack: A stack of previously seen argument lists is maintained for the current size baseline. - If size strictly decreases: The stack is cleared (progress made). - If size increases: An ErrorType is returned (divergence). - If size is equal: The arguments are checked against the stack. Repeating arguments trigger a cycle error; new arguments are added to the stack. Since the number of type combinations is finite, disallowing growth while preventing cycles guarantees the algorithm will eventually terminate.
1 parent 3423338 commit 5f55a9a

File tree

8 files changed

+79
-61
lines changed

8 files changed

+79
-61
lines changed

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

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,15 @@ import Capabilities.*
4545
import transform.Recheck.currentRechecker
4646
import scala.collection.immutable.HashMap
4747
import dotty.tools.dotc.util.Property
48-
import dotty.tools.dotc.reporting.NonDecreasingMatchReduction
48+
import dotty.tools.dotc.reporting.IncreasingMatchReduction
49+
import dotty.tools.dotc.reporting.CyclicMatchTypeReduction
4950

5051
import scala.annotation.internal.sharable
5152
import scala.annotation.threadUnsafe
5253

5354
object Types extends TypeUtils {
5455

55-
val Reduction = new Property.Key[HashMap[Type, List[Int]]]
56+
val Reduction = new Property.Key[HashMap[Type, List[List[Type]]]]
5657

5758
@sharable private var nextId = 0
5859

@@ -1637,20 +1638,30 @@ object Types extends TypeUtils {
16371638
this match
16381639
case self: AppliedType =>
16391640
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}")
16431641
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
1642+
val decision: Either[ErrorType, List[List[Type]]] =
1643+
if history.contains(self.tycon) then
1644+
val stack = history(self.tycon) // Stack is non-empty
1645+
report.log(i"Match type reduction history for ${self.tycon}: $stack")
1646+
val currentArgsSize = self.args.map(_.typeSize)
1647+
val prevArgsSize = stack.head.map(_.typeSize)
1648+
val listOrd = scala.math.Ordering.Implicits.seqOrdering[Seq, Int]
1649+
if listOrd.gt(currentArgsSize, prevArgsSize) then
1650+
Left(ErrorType(IncreasingMatchReduction(self.tycon, stack.head, prevArgsSize, self.args, currentArgsSize)))
1651+
else if listOrd.equiv(currentArgsSize, prevArgsSize) then
1652+
if stack.contains(self.args) then
1653+
Left(ErrorType(CyclicMatchTypeReduction(self.tycon, self.args, currentArgsSize, stack)))
1654+
else Right(self.args :: stack)
1655+
else Right(self.args :: Nil) // currentArgsSize < prevArgsSize
1656+
else Right(self.args :: Nil)
1657+
decision match
1658+
case Left(err) => err
1659+
case Right(stack) =>
1660+
val newHistory = history.updated(self.tycon, stack)
1661+
val result =
1662+
given Context = ctx.fresh.setProperty(Reduction, newHistory)
1663+
mt.reduced.normalized
1664+
result
16541665
case _ => mt.reduced.normalized
16551666
case tp: AppliedType => tp.tryCompiletimeConstantFold
16561667
case _ => NoType

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,8 @@ 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
239+
case IncreasingMatchReductionID // errorNumber: 223
240+
case CyclicMatchTypeReductionID // errorNumber: 224
240241

241242
def errorNumber = ordinal - 1
242243

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

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3321,9 +3321,29 @@ 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."
3324+
class IncreasingMatchReduction(tpcon: Type, prevArgs: List[Type], prevSize: List[Int], curArgs: List[Type], curSize: List[Int])(using Context)
3325+
extends TypeMsg(IncreasingMatchReductionID):
3326+
def msg(using Context) =
3327+
i"""Match type reduction failed due to potentially infinite recursion.
3328+
|
3329+
|The reduction step for $tpcon resulted in strictly larger arguments (lexicographically):
3330+
| Previous: $prevArgs (size: $prevSize)
3331+
| Current: $curArgs (size: $curSize)
3332+
|
3333+
|To guarantee termination, recursive match types must strictly decrease in size
3334+
|or stay the same (without cycles)."""
3335+
def explain(using Context) = ""
3336+
3337+
class CyclicMatchTypeReduction(tpcon: Type, args: List[Type], argsSize: List[Int], stack: List[List[Type]])(using Context)
3338+
extends TypeMsg(CyclicMatchTypeReductionID):
3339+
def msg(using Context) =
3340+
val trace: String = stack.map(a => i"${tpcon}${a}").mkString(" -> ")
3341+
i"""Match type reduction failed due to a cycle.
3342+
|
3343+
|The match type $tpcon reduced to itself with the same arguments:
3344+
|$args
3345+
|
3346+
|Trace: $trace"""
33273347
def explain(using Context) = ""
33283348

33293349
class ClosureCannotHaveInternalParameterDependencies(mt: Type)(using Context)

tests/neg/i22587-neg-A.scala

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +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
1+
// Negative Test Case A: Direct Self-Reference Without Reduction
2+
// This SHOULD diverge because Loop[Int] => Loop[Float] => Loop[Double] => Loop[String] => Loop[Int] => ...
3+
// The type cycles without reaching a base case.
44

55
type Loop[X] = X match
6-
case Int => Loop[Int]
6+
case Int => Loop[Float]
7+
case Float => Loop[Double]
8+
case Double => Loop[String]
9+
case String => Loop[Int]
710
case _ => String
811

912
@main def test02(): Unit =

tests/neg/i22587-neg-B.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Test Case 3: Wrapping Type Without Progress (NEGATIVE)
1+
// Negative Test Case 2: Wrapping Type Without Progress
22
// This SHOULD diverge because Wrap[Int] => Wrap[List[Int]] => Wrap[List[List[Int]]] => ...
33
// The type grows infinitely without reaching a base case
44

tests/neg/i22587-neg-C.scala

Lines changed: 0 additions & 14 deletions
This file was deleted.

tests/pos/i22587-pos-A.scala

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
// Test Case 1: Simple Recursive Deconstruction (POSITIVE)
1+
// Positive Test Case A: Simple Recursive Deconstruction
22
// This should NOT diverge because recursion always reduces the type structure
33
// by unwrapping one layer (Option[t] -> t)
44

5+
type DoubleUnwrap[X, Y] = (X, Y) match
6+
case (AnyVal, Y) => (X, Unwrap[Y])
7+
case (X, AnyVal) => (Unwrap[X], Y)
8+
case (X, Y) => (Unwrap[X], Unwrap[Y])
9+
510
type Unwrap[X] = X match
611
case Option[t] => Unwrap[t]
712
case _ => X
@@ -10,9 +15,9 @@ type Unwrap[X] = X match
1015
val e1: Unwrap[Option[Option[Int]]] = 42
1116
println("Test 1 - Simple recursive unwrapping:")
1217
println(s"e1 value: $e1")
13-
14-
val e2: Unwrap[Option[String]] = "hello"
18+
19+
val e2: DoubleUnwrap[Option[String], Option[Int]] = ("hello", 42)
1520
println(s"e2 value: $e2")
16-
17-
val e3: Unwrap[Int] = 99
21+
22+
val e3: DoubleUnwrap[Int, Int] = (1, 2)
1823
println(s"e3 value: $e3")

tests/pos/i22587-pos-B.scala

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,13 @@
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)
1+
// Positive Test Case B: Binary Recursive construction
2+
// This should NOT diverge because although types have same complexity, the structure
3+
// changes by incrementing by one the numeric parameter until reaching a base case.
44

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
5+
6+
type Increase[X, Y, Z] = (X, Y, Z) match
7+
case (X, Y, 0) => Increase[X, Y, 1]
8+
case (X, 0, _) => Increase[X, 1, 0]
9+
case (0, _, _) => Increase[1, 0, 0]
10+
case _ => "done"
1111

1212
@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")
13+
val e1: Increase[0, 0, 0] = "done"

0 commit comments

Comments
 (0)