Skip to content

Commit 108f8d0

Browse files
authored
Force consistent MT post-redux normalisation, disallow infinite match types (#18073)
2 parents cdc9fe8 + 8aec1d1 commit 108f8d0

15 files changed

+213
-17
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3212,7 +3212,7 @@ class TrackingTypeComparer(initctx: Context) extends TypeComparer(initctx) {
32123212
}
32133213
}
32143214
case redux =>
3215-
MatchResult.Reduced(redux.simplified)
3215+
MatchResult.Reduced(redux)
32163216
case _ =>
32173217
MatchResult.Reduced(body)
32183218

@@ -3240,7 +3240,7 @@ class TrackingTypeComparer(initctx: Context) extends TypeComparer(initctx) {
32403240
MatchTypeTrace.noInstance(scrut, cas, fails)
32413241
NoType
32423242
case MatchResult.Reduced(tp) =>
3243-
tp
3243+
tp.simplified
32443244
case Nil =>
32453245
val casesText = MatchTypeTrace.noMatchesText(scrut, cases)
32463246
ErrorType(reporting.MatchTypeNoCases(casesText))

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ object TypeOps:
147147
isFullyDefined(tp, ForceDegree.all)
148148
case _ =>
149149
val normed = tp.tryNormalize
150-
if normed.exists then normed else tp.map(simplify(_, theMap))
150+
if normed.exists then simplify(normed, theMap) else tp.map(simplify(_, theMap))
151151
case tp: TypeParamRef =>
152152
val tvar = ctx.typerState.constraint.typeVarOfParam(tp)
153153
if tvar.exists then tvar else tp
@@ -184,7 +184,7 @@ object TypeOps:
184184
else tp.derivedAnnotatedType(parent1, annot)
185185
case _: MatchType =>
186186
val normed = tp.tryNormalize
187-
if (normed.exists) normed else mapOver
187+
if (normed.exists) simplify(normed, theMap) else mapOver
188188
case tp: MethodicType =>
189189
// See documentation of `Types#simplified`
190190
val addTypeVars = new TypeMap with IdempotentCaptRefMap:

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ object Types {
359359
* (since these are relevant for inference or resolution) but never consider prefixes
360360
* (since these often do not constrain the search space anyway).
361361
*/
362-
def unusableForInference(using Context): Boolean = widenDealias match
362+
def unusableForInference(using Context): Boolean = try widenDealias match
363363
case AppliedType(tycon, args) => tycon.unusableForInference || args.exists(_.unusableForInference)
364364
case RefinedType(parent, _, rinfo) => parent.unusableForInference || rinfo.unusableForInference
365365
case TypeBounds(lo, hi) => lo.unusableForInference || hi.unusableForInference
@@ -369,6 +369,7 @@ object Types {
369369
case CapturingType(parent, refs) => parent.unusableForInference || refs.elems.exists(_.unusableForInference)
370370
case _: ErrorType => true
371371
case _ => false
372+
catch case ex: Throwable => handleRecursive("unusableForInference", show, ex)
372373

373374
/** Does the type carry an annotation that is an instance of `cls`? */
374375
@tailrec final def hasAnnotation(cls: ClassSymbol)(using Context): Boolean = stripTypeVar match {
@@ -3490,9 +3491,11 @@ object Types {
34903491
private var myWidened: Type = _
34913492

34923493
private def computeAtoms()(using Context): Atoms =
3493-
if tp1.hasClassSymbol(defn.NothingClass) then tp2.atoms
3494-
else if tp2.hasClassSymbol(defn.NothingClass) then tp1.atoms
3495-
else tp1.atoms | tp2.atoms
3494+
val tp1n = tp1.normalized
3495+
val tp2n = tp2.normalized
3496+
if tp1n.hasClassSymbol(defn.NothingClass) then tp2.atoms
3497+
else if tp2n.hasClassSymbol(defn.NothingClass) then tp1.atoms
3498+
else tp1n.atoms | tp2n.atoms
34963499

34973500
private def computeWidenSingletons()(using Context): Type =
34983501
val tp1w = tp1.widenSingletons

compiler/src/dotty/tools/dotc/typer/Inferencing.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ object Inferencing {
198198
case tp => foldOver(x, tp)
199199
}
200200
catch case ex: Throwable =>
201-
handleRecursive("check fully defined", tp.show, ex)
201+
handleRecursive("check fully defined", tp.showSummary(20), ex)
202202
}
203203

204204
def process(tp: Type): Boolean =

compiler/test/dotc/pos-test-pickling.blacklist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ i15158.scala
5151
i15155.scala
5252
i15827.scala
5353
i17149.scala
54+
tuple-fold.scala
55+
mt-redux-norm.perspective.scala
5456

5557
# Opaque type
5658
i5720.scala

compiler/test/dotty/tools/repl/ReplCompilerTests.scala

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,62 @@ class ReplCompilerTests extends ReplTest:
169169
)
170170
}
171171

172+
@Test def i16596 =
173+
initially {
174+
run("""
175+
|import scala.compiletime.{error, ops, requireConst}, ops.int.*
176+
|import scala.quoted.*
177+
|
178+
|sealed trait Nat
179+
|object Nat:
180+
| case object Zero extends Nat
181+
| case class Succ[N <: Nat](prev: N) extends Nat
182+
|
183+
| given zero: Zero.type = Zero
184+
| given buildSucc[N <: Nat](using n: N): Succ[N] = Succ(n)
185+
|
186+
| def value[N <: Nat](using n: N): N = n
187+
|
188+
| def prevImpl[I <: Int: Type](expr: Expr[I])(using Quotes): Expr[I - 1] =
189+
| val prev = expr.valueOrAbort - 1
190+
| // this compiles, but fails at use time
191+
| //Expr(prev).asExprOf[ops.int.-[I, 1]]
192+
| Expr(prev).asInstanceOf[Expr[I - 1]]
193+
|
194+
| inline def prevOf[I <: Int](inline i: I): I - 1 = ${prevImpl('i)}
195+
|
196+
| type FromInt[I <: Int] <: Nat = I match
197+
| case 0 => Zero.type
198+
| case _ => Succ[FromInt[I - 1]]
199+
|
200+
| inline def fromInt[I <: Int & Singleton](i: I): FromInt[i.type] =
201+
| requireConst(i)
202+
| inline i match
203+
| case _: 0 => Zero
204+
| case _ =>
205+
| inline if i < 0
206+
| then error("cannot convert negative to Nat")
207+
| else Succ(fromInt(prevOf[i.type](i)))
208+
""".stripMargin)
209+
}.andThen {
210+
assertEquals(
211+
"""// defined trait Nat
212+
|// defined object Nat
213+
|""".stripMargin, storedOutput())
214+
run("Nat.fromInt(2)")
215+
}.andThen {
216+
assertEquals("val res0: Nat.Succ[Nat.Succ[Nat.Zero.type]] = Succ(Succ(Zero))", storedOutput().trim)
217+
run("summon[Nat.FromInt[2]]")
218+
}.andThen {
219+
assertEquals("val res1: Nat.Succ[Nat.Succ[Nat.Zero.type]] = Succ(Succ(Zero))", storedOutput().trim)
220+
run("Nat.fromInt(3)")
221+
}.andThen {
222+
assertEquals("val res2: Nat.Succ[Nat.Succ[Nat.Succ[Nat.Zero.type]]] = Succ(Succ(Succ(Zero)))", storedOutput().trim)
223+
run("summon[Nat.FromInt[3]]")
224+
}.andThen {
225+
assertEquals("val res3: Nat.Succ[Nat.Succ[Nat.Succ[Nat.Zero.type]]] = Succ(Succ(Succ(Zero)))", storedOutput().trim)
226+
}
227+
172228
@Test def i6200 =
173229
initially {
174230
run("""

tests/neg-custom-args/isInstanceOf/i17435.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ object Test:
1414
type JsonArray = mutable.Buffer[Json]
1515

1616
def encode(x: Json): Int = x match
17-
case str: String => 1
18-
case b: Boolean => 2
19-
case i: Int => 3
20-
case d: Double => 4
17+
case str: String => 1 // error
18+
case b: Boolean => 2 // error
19+
case i: Int => 3 // error
20+
case d: Double => 4 // error
2121
case arr: JsonArray => 5 // error
2222
case obj: JsonObject => 6 // error
2323
case _ => 7

tests/pos/i15158.scala renamed to tests/neg/i15158.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ val x = foo {
1313
type Rec[A] = A match
1414
case String => Opt[Rec[String]]
1515

16-
val arr = new Buf[Rec[String]](8)
17-
val arr2 = Buf[Rec[String]](8)
18-
val arr3 = Buf.apply[Rec[String]](8)
16+
val arr = new Buf[Rec[String]](8) // error
17+
val arr2 = Buf[Rec[String]](8) // error
18+
val arr3 = Buf.apply[Rec[String]](8) // error
1919
}
2020

2121
import scala.collection.mutable
@@ -38,6 +38,6 @@ class Spec {
3838
JsonPrimitive
3939
]
4040

41-
val arr = new mutable.ArrayBuffer[Json](8)
41+
val arr = new mutable.ArrayBuffer[Json](8) // error
4242
}
4343
}

tests/neg/mt-recur.cov.scala

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// like mt-recur.scala, but covariant
2+
class Cov[+T]
3+
4+
type Recur[X] = X match
5+
case Int => Cov[Recur[X]]
6+
7+
def x = ??? : Recur[Int] // error

tests/neg/mt-recur.scala

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// an example of an infinite recursion match type
2+
// using an _invariant_ type constructor
3+
// see mt-recur.cov.scala for covariant
4+
// used to track the behaviour of match type reduction
5+
class Inv[T]
6+
7+
type Recur[X] = X match
8+
case Int => Inv[Recur[X]]
9+
10+
def x = ??? : Recur[Int] // error

tests/pos/i16596.more.scala

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import scala.compiletime.ops.int.*
2+
3+
object NatExample {
4+
sealed trait Nat
5+
object Nat {
6+
case object Zero extends Nat
7+
case class Succ[N <: Nat](prev: N) extends Nat
8+
9+
given zero: Zero.type = Zero
10+
given buildSucc[N <: Nat](using n: N): Succ[N] = Succ(n)
11+
12+
def value[N <: Nat](using n: N): N = n
13+
14+
type FromInt[I <: Int] <: Nat = I match
15+
case 0 => Zero.type
16+
case _ => Succ[FromInt[I - 1]]
17+
18+
summon[FromInt[0] =:= Zero.type]
19+
summon[FromInt[1] =:= Succ[Zero.type]]
20+
summon[FromInt[2] =:= Succ[Succ[Zero.type]]]
21+
summon[FromInt[3] =:= Succ[Succ[Succ[Zero.type]]]]
22+
summon[FromInt[4] =:= Succ[Succ[Succ[Succ[Zero.type]]]]]
23+
24+
@main def test = {
25+
require(summon[FromInt[0]] == Zero)
26+
require(summon[FromInt[1]] == Succ(Zero))
27+
require(summon[FromInt[2]] == Succ(Succ(Zero)))
28+
require(summon[FromInt[3]] == Succ(Succ(Succ(Zero))))
29+
// we can summon 4 if we write it out:
30+
require(summon[Succ[Succ[Succ[Succ[Zero.type]]]]] == Succ(Succ(Succ(Succ(Zero)))))
31+
// was: we cannot summon 4 using the match type
32+
require(summon[FromInt[4]] == Succ(Succ(Succ(Succ(Zero)))))
33+
}
34+
}
35+
}

tests/pos/i16596.orig.scala

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import scala.compiletime.ops.int
2+
3+
type Count0[N,T] <: Tuple = (N,T) match
4+
case (0,_) => EmptyTuple
5+
case (N,String) => String *: Count0[int.-[N, 1], String]
6+
case (N,Int) => Int *: Count0[int.-[N, 1], Int]
7+
case (N,Float) => Float *: Count0[int.-[N, 1], Float]
8+
case (N,Double) => Double *: Count0[int.-[N, 1], Double]
9+
10+
11+
type Count1[N,T] <: Tuple = (N,T) match
12+
case (0,T) => EmptyTuple
13+
case (N,String) => String *: Count1[int.-[N, 1], String]
14+
case (N,Int) => Int *: Count1[int.-[N, 1], Int]
15+
case (N,Float) => Float *: Count1[int.-[N, 1], Float]
16+
case (N,Double) => Double *: Count1[int.-[N, 1], Double]
17+
18+
def t01 = summon[Count0[1, Int] =:= Int *: EmptyTuple ]
19+
def t02 = summon[Count0[2, Int] =:= Int *: Int *: EmptyTuple]
20+
def t03 = summon[Count0[3, Int] =:= Int *: Int *: Int *: EmptyTuple]
21+
def t04 = summon[Count0[4, Int] =:= Int *: Int *: Int *: Int *: EmptyTuple]
22+
def t05 = summon[Count0[5, Int] =:= Int *: Int *: Int *: Int *: Int *: EmptyTuple]
23+
24+
def t11 = summon[Count1[1, Int] =:= Int *: EmptyTuple ]
25+
def t12 = summon[Count1[2, Int] =:= Int *: Int *: EmptyTuple]
26+
def t13 = summon[Count1[3, Int] =:= Int *: Int *: Int *: EmptyTuple] // was: Fail from here
27+
def t14 = summon[Count1[4, Int] =:= Int *: Int *: Int *: Int *: EmptyTuple]
28+
def t15 = summon[Count1[5, Int] =:= Int *: Int *: Int *: Int *: Int *: EmptyTuple]

tests/pos/i16596.scala

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import scala.compiletime.ops.int, int.-
2+
3+
type Count[N, T] <: Tuple = (N, T) match
4+
case (0, T) => EmptyTuple
5+
case (N, T) => T *: Count[N - 1, T]
6+
7+
val a: Count[3, Int] = (1, 2, 3)
8+
val b: Count[4, Int] = (1, 2, 3, 4)
9+
val c: Count[5, Int] = (1, 2, 3, 4, 5)
10+
val d: Count[6, Int] = (1, 2, 3, 4, 5, 6)
11+
val z: Count[23, Int] = (
12+
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
13+
11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
14+
21, 22, 23)

tests/pos/i17257.min.scala

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// scalac: -Yno-deep-subtypes:false
2+
// Minimisation of tests/run-macros/i17257
3+
// to understand how changes to match type reduction
4+
// impacted this use of Tuple.IsMappedBy.
5+
//
6+
// During match type reduction
7+
// if we do NOT simplify the case lambda parameter instances
8+
// then this large tuple make TypeComparer breach LogPendingSubTypesThreshold
9+
// which, under -Yno-deep-subtypes, crashes the compilation.
10+
class C[+A]
11+
def foo[T <: Tuple : Tuple.IsMappedBy[C]] = ???
12+
def bar[X] = foo[(
13+
C[X], C[X], C[X], C[X], C[X], C[X], C[X], C[X], C[X], C[X],
14+
C[X], C[X], C[X], C[X], C[X], C[X], C[X], C[X], C[X], C[X],
15+
C[X], C[X], C[X],
16+
)]
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// while making MT post-redux consistent in its normalisation/simplification
2+
// one version of the change broke a line of the perspective community project in CI
3+
// this is a minimisation of the failure
4+
5+
import scala.compiletime._, scala.deriving._
6+
7+
transparent inline def foo(using m: Mirror): Unit =
8+
constValueTuple[m.MirroredElemLabels].toList.toSet // was:
9+
//-- [E057] Type Mismatch Error: cat.scala:8:14 ----------------------------------
10+
//8 |def test = foo(using summon[Mirror.Of[Cat]])
11+
// | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
12+
// |Type argument ("name" : String) | Nothing does not conform to lower bound m$proxy1.MirroredElemLabels match {
13+
// | case EmptyTuple => Nothing
14+
// | case h *: t => h | Tuple.Fold[t, Nothing, [x, y] =>> x | y]
15+
// |}
16+
// |-----------------------------------------------------------------------------
17+
// |Inline stack trace
18+
// |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
19+
// |This location contains code that was inlined from cat.scala:4
20+
//4 | constValueTuple[m.MirroredElemLabels].toList.toSet
21+
// | ^
22+
23+
case class Cat(name: String)
24+
25+
def test = foo(using summon[Mirror.Of[Cat]])

0 commit comments

Comments
 (0)