Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Force consistent MT post-redux normalisation, disallow infinite match types #18073

Merged
merged 4 commits into from
Jul 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

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: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/core/TypeComparer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3201,7 +3201,7 @@ class TrackingTypeComparer(initctx: Context) extends TypeComparer(initctx) {
}
}
case redux =>
MatchResult.Reduced(redux.simplified)
MatchResult.Reduced(redux)
case _ =>
MatchResult.Reduced(body)

Expand Down Expand Up @@ -3229,7 +3229,7 @@ class TrackingTypeComparer(initctx: Context) extends TypeComparer(initctx) {
MatchTypeTrace.noInstance(scrut, cas, fails)
NoType
case MatchResult.Reduced(tp) =>
tp
tp.simplified
dwijnand marked this conversation as resolved.
Show resolved Hide resolved
case Nil =>
val casesText = MatchTypeTrace.noMatchesText(scrut, cases)
ErrorType(reporting.MatchTypeNoCases(casesText))
Expand Down
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/core/TypeOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ object TypeOps:
isFullyDefined(tp, ForceDegree.all)
case _ =>
val normed = tp.tryNormalize
if normed.exists then normed else tp.map(simplify(_, theMap))
if normed.exists then simplify(normed, theMap) else tp.map(simplify(_, theMap))
case tp: TypeParamRef =>
val tvar = ctx.typerState.constraint.typeVarOfParam(tp)
if tvar.exists then tvar else tp
Expand Down Expand Up @@ -184,7 +184,7 @@ object TypeOps:
else tp.derivedAnnotatedType(parent1, annot)
case _: MatchType =>
val normed = tp.tryNormalize
if (normed.exists) normed else mapOver
if (normed.exists) simplify(normed, theMap) else mapOver
case tp: MethodicType =>
// See documentation of `Types#simplified`
val addTypeVars = new TypeMap with IdempotentCaptRefMap:
Expand Down
11 changes: 7 additions & 4 deletions compiler/src/dotty/tools/dotc/core/Types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ object Types {
* (since these are relevant for inference or resolution) but never consider prefixes
* (since these often do not constrain the search space anyway).
*/
def unusableForInference(using Context): Boolean = widenDealias match
def unusableForInference(using Context): Boolean = try widenDealias match
case AppliedType(tycon, args) => tycon.unusableForInference || args.exists(_.unusableForInference)
case RefinedType(parent, _, rinfo) => parent.unusableForInference || rinfo.unusableForInference
case TypeBounds(lo, hi) => lo.unusableForInference || hi.unusableForInference
Expand All @@ -369,6 +369,7 @@ object Types {
case CapturingType(parent, refs) => parent.unusableForInference || refs.elems.exists(_.unusableForInference)
case _: ErrorType => true
case _ => false
catch case ex: Throwable => handleRecursive("unusableForInference", show, ex)

/** Does the type carry an annotation that is an instance of `cls`? */
@tailrec final def hasAnnotation(cls: ClassSymbol)(using Context): Boolean = stripTypeVar match {
Expand Down Expand Up @@ -3482,9 +3483,11 @@ object Types {
private var myWidened: Type = _

private def computeAtoms()(using Context): Atoms =
if tp1.hasClassSymbol(defn.NothingClass) then tp2.atoms
else if tp2.hasClassSymbol(defn.NothingClass) then tp1.atoms
else tp1.atoms | tp2.atoms
val tp1n = tp1.normalized
dwijnand marked this conversation as resolved.
Show resolved Hide resolved
val tp2n = tp2.normalized
if tp1n.hasClassSymbol(defn.NothingClass) then tp2.atoms
else if tp2n.hasClassSymbol(defn.NothingClass) then tp1.atoms
else tp1n.atoms | tp2n.atoms

private def computeWidenSingletons()(using Context): Type =
val tp1w = tp1.widenSingletons
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/typer/Inferencing.scala
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ object Inferencing {
case tp => foldOver(x, tp)
}
catch case ex: Throwable =>
handleRecursive("check fully defined", tp.show, ex)
handleRecursive("check fully defined", tp.showSummary(20), ex)
dwijnand marked this conversation as resolved.
Show resolved Hide resolved
}

def process(tp: Type): Boolean =
Expand Down
2 changes: 2 additions & 0 deletions compiler/test/dotc/pos-test-pickling.blacklist
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ i15158.scala
i15155.scala
i15827.scala
i17149.scala
tuple-fold.scala
mt-redux-norm.perspective.scala

# Opaque type
i5720.scala
Expand Down
56 changes: 56 additions & 0 deletions compiler/test/dotty/tools/repl/ReplCompilerTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,62 @@ class ReplCompilerTests extends ReplTest:
)
}

@Test def i16596 =
initially {
run("""
|import scala.compiletime.{error, ops, requireConst}, ops.int.*
|import scala.quoted.*
|
|sealed trait Nat
|object Nat:
| case object Zero extends Nat
| case class Succ[N <: Nat](prev: N) extends Nat
|
| given zero: Zero.type = Zero
| given buildSucc[N <: Nat](using n: N): Succ[N] = Succ(n)
|
| def value[N <: Nat](using n: N): N = n
|
| def prevImpl[I <: Int: Type](expr: Expr[I])(using Quotes): Expr[I - 1] =
| val prev = expr.valueOrAbort - 1
| // this compiles, but fails at use time
| //Expr(prev).asExprOf[ops.int.-[I, 1]]
| Expr(prev).asInstanceOf[Expr[I - 1]]
|
| inline def prevOf[I <: Int](inline i: I): I - 1 = ${prevImpl('i)}
|
| type FromInt[I <: Int] <: Nat = I match
| case 0 => Zero.type
| case _ => Succ[FromInt[I - 1]]
|
| inline def fromInt[I <: Int & Singleton](i: I): FromInt[i.type] =
| requireConst(i)
| inline i match
| case _: 0 => Zero
| case _ =>
| inline if i < 0
| then error("cannot convert negative to Nat")
| else Succ(fromInt(prevOf[i.type](i)))
""".stripMargin)
}.andThen {
assertEquals(
"""// defined trait Nat
|// defined object Nat
|""".stripMargin, storedOutput())
run("Nat.fromInt(2)")
}.andThen {
assertEquals("val res0: Nat.Succ[Nat.Succ[Nat.Zero.type]] = Succ(Succ(Zero))", storedOutput().trim)
run("summon[Nat.FromInt[2]]")
}.andThen {
assertEquals("val res1: Nat.Succ[Nat.Succ[Nat.Zero.type]] = Succ(Succ(Zero))", storedOutput().trim)
run("Nat.fromInt(3)")
}.andThen {
assertEquals("val res2: Nat.Succ[Nat.Succ[Nat.Succ[Nat.Zero.type]]] = Succ(Succ(Succ(Zero)))", storedOutput().trim)
run("summon[Nat.FromInt[3]]")
}.andThen {
assertEquals("val res3: Nat.Succ[Nat.Succ[Nat.Succ[Nat.Zero.type]]] = Succ(Succ(Succ(Zero)))", storedOutput().trim)
}

@Test def i6200 =
initially {
run("""
Expand Down
8 changes: 4 additions & 4 deletions tests/neg-custom-args/isInstanceOf/i17435.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ object Test:
type JsonArray = mutable.Buffer[Json]

def encode(x: Json): Int = x match
case str: String => 1
case b: Boolean => 2
case i: Int => 3
case d: Double => 4
case str: String => 1 // error
case b: Boolean => 2 // error
case i: Int => 3 // error
case d: Double => 4 // error
case arr: JsonArray => 5 // error
case obj: JsonObject => 6 // error
case _ => 7
8 changes: 4 additions & 4 deletions tests/pos/i15158.scala → tests/neg/i15158.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ val x = foo {
type Rec[A] = A match
case String => Opt[Rec[String]]

val arr = new Buf[Rec[String]](8)
val arr2 = Buf[Rec[String]](8)
val arr3 = Buf.apply[Rec[String]](8)
val arr = new Buf[Rec[String]](8) // error
val arr2 = Buf[Rec[String]](8) // error
val arr3 = Buf.apply[Rec[String]](8) // error
}

import scala.collection.mutable
Expand All @@ -38,6 +38,6 @@ class Spec {
JsonPrimitive
]

val arr = new mutable.ArrayBuffer[Json](8)
val arr = new mutable.ArrayBuffer[Json](8) // error
}
}
7 changes: 7 additions & 0 deletions tests/neg/mt-recur.cov.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// like mt-recur.scala, but covariant
class Cov[+T]

type Recur[X] = X match
case Int => Cov[Recur[X]]

def x = ??? : Recur[Int] // error
10 changes: 10 additions & 0 deletions tests/neg/mt-recur.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// an example of an infinite recursion match type
// using an _invariant_ type constructor
// see mt-recur.cov.scala for covariant
// used to track the behaviour of match type reduction
class Inv[T]

type Recur[X] = X match
case Int => Inv[Recur[X]]

def x = ??? : Recur[Int] // error
35 changes: 35 additions & 0 deletions tests/pos/i16596.more.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import scala.compiletime.ops.int.*

object NatExample {
sealed trait Nat
object Nat {
case object Zero extends Nat
case class Succ[N <: Nat](prev: N) extends Nat

given zero: Zero.type = Zero
given buildSucc[N <: Nat](using n: N): Succ[N] = Succ(n)

def value[N <: Nat](using n: N): N = n

type FromInt[I <: Int] <: Nat = I match
case 0 => Zero.type
case _ => Succ[FromInt[I - 1]]

summon[FromInt[0] =:= Zero.type]
summon[FromInt[1] =:= Succ[Zero.type]]
summon[FromInt[2] =:= Succ[Succ[Zero.type]]]
summon[FromInt[3] =:= Succ[Succ[Succ[Zero.type]]]]
summon[FromInt[4] =:= Succ[Succ[Succ[Succ[Zero.type]]]]]

@main def test = {
require(summon[FromInt[0]] == Zero)
require(summon[FromInt[1]] == Succ(Zero))
require(summon[FromInt[2]] == Succ(Succ(Zero)))
require(summon[FromInt[3]] == Succ(Succ(Succ(Zero))))
// we can summon 4 if we write it out:
require(summon[Succ[Succ[Succ[Succ[Zero.type]]]]] == Succ(Succ(Succ(Succ(Zero)))))
// was: we cannot summon 4 using the match type
require(summon[FromInt[4]] == Succ(Succ(Succ(Succ(Zero)))))
}
}
}
28 changes: 28 additions & 0 deletions tests/pos/i16596.orig.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import scala.compiletime.ops.int

type Count0[N,T] <: Tuple = (N,T) match
case (0,_) => EmptyTuple
case (N,String) => String *: Count0[int.-[N, 1], String]
case (N,Int) => Int *: Count0[int.-[N, 1], Int]
case (N,Float) => Float *: Count0[int.-[N, 1], Float]
case (N,Double) => Double *: Count0[int.-[N, 1], Double]


type Count1[N,T] <: Tuple = (N,T) match
case (0,T) => EmptyTuple
case (N,String) => String *: Count1[int.-[N, 1], String]
case (N,Int) => Int *: Count1[int.-[N, 1], Int]
case (N,Float) => Float *: Count1[int.-[N, 1], Float]
case (N,Double) => Double *: Count1[int.-[N, 1], Double]

def t01 = summon[Count0[1, Int] =:= Int *: EmptyTuple ]
def t02 = summon[Count0[2, Int] =:= Int *: Int *: EmptyTuple]
def t03 = summon[Count0[3, Int] =:= Int *: Int *: Int *: EmptyTuple]
def t04 = summon[Count0[4, Int] =:= Int *: Int *: Int *: Int *: EmptyTuple]
def t05 = summon[Count0[5, Int] =:= Int *: Int *: Int *: Int *: Int *: EmptyTuple]

def t11 = summon[Count1[1, Int] =:= Int *: EmptyTuple ]
def t12 = summon[Count1[2, Int] =:= Int *: Int *: EmptyTuple]
def t13 = summon[Count1[3, Int] =:= Int *: Int *: Int *: EmptyTuple] // was: Fail from here
def t14 = summon[Count1[4, Int] =:= Int *: Int *: Int *: Int *: EmptyTuple]
def t15 = summon[Count1[5, Int] =:= Int *: Int *: Int *: Int *: Int *: EmptyTuple]
14 changes: 14 additions & 0 deletions tests/pos/i16596.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import scala.compiletime.ops.int, int.-

type Count[N, T] <: Tuple = (N, T) match
case (0, T) => EmptyTuple
case (N, T) => T *: Count[N - 1, T]

val a: Count[3, Int] = (1, 2, 3)
val b: Count[4, Int] = (1, 2, 3, 4)
val c: Count[5, Int] = (1, 2, 3, 4, 5)
val d: Count[6, Int] = (1, 2, 3, 4, 5, 6)
val z: Count[23, Int] = (
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
21, 22, 23)
16 changes: 16 additions & 0 deletions tests/pos/i17257.min.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// scalac: -Yno-deep-subtypes:false
// Minimisation of tests/run-macros/i17257
// to understand how changes to match type reduction
// impacted this use of Tuple.IsMappedBy.
//
// During match type reduction
// if we do NOT simplify the case lambda parameter instances
// then this large tuple make TypeComparer breach LogPendingSubTypesThreshold
// which, under -Yno-deep-subtypes, crashes the compilation.
class C[+A]
def foo[T <: Tuple : Tuple.IsMappedBy[C]] = ???
def bar[X] = foo[(
C[X], C[X], C[X], C[X], C[X], C[X], C[X], C[X], C[X], C[X],
C[X], C[X], C[X], C[X], C[X], C[X], C[X], C[X], C[X], C[X],
C[X], C[X], C[X],
)]
25 changes: 25 additions & 0 deletions tests/pos/mt-redux-norm.perspective.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// while making MT post-redux consistent in its normalisation/simplification
// one version of the change broke a line of the perspective community project in CI
// this is a minimisation of the failure

import scala.compiletime._, scala.deriving._

transparent inline def foo(using m: Mirror): Unit =
constValueTuple[m.MirroredElemLabels].toList.toSet // was:
//-- [E057] Type Mismatch Error: cat.scala:8:14 ----------------------------------
//8 |def test = foo(using summon[Mirror.Of[Cat]])
// | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// |Type argument ("name" : String) | Nothing does not conform to lower bound m$proxy1.MirroredElemLabels match {
// | case EmptyTuple => Nothing
// | case h *: t => h | Tuple.Fold[t, Nothing, [x, y] =>> x | y]
// |}
// |-----------------------------------------------------------------------------
// |Inline stack trace
// |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// |This location contains code that was inlined from cat.scala:4
//4 | constValueTuple[m.MirroredElemLabels].toList.toSet
// | ^

case class Cat(name: String)

def test = foo(using summon[Mirror.Of[Cat]])