Skip to content

Commit ab48a55

Browse files
Streamline tryNormalize with underlyingMatchType (#20268)
The overall goal was to have a more uniform treatment of `tryNormalize` rather than the three overrides, making the logic easier to follow. It also now reuses `underlyingMatchType` for it, which not only has a caching benefit but also ensures consistent results between them. In particular, making `tryNormalize.exists` imply `underlyingMatchType.exists`, which one might assume as true but did not hold in general previously.
2 parents f2829c3 + 1bfa819 commit ab48a55

File tree

8 files changed

+96
-69
lines changed

8 files changed

+96
-69
lines changed

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

+40-68
Original file line numberDiff line numberDiff line change
@@ -490,14 +490,7 @@ object Types extends TypeUtils {
490490
case _ => false
491491

492492
/** Does this application expand to a match type? */
493-
def isMatchAlias(using Context): Boolean = underlyingMatchType.exists
494-
495-
def underlyingMatchType(using Context): Type = stripped match {
496-
case tp: MatchType => tp
497-
case tp: HKTypeLambda => tp.resType.underlyingMatchType
498-
case tp: AppliedType => tp.underlyingMatchType
499-
case _ => NoType
500-
}
493+
def isMatchAlias(using Context): Boolean = underlyingNormalizable.isMatch
501494

502495
/** Is this a higher-kinded type lambda with given parameter variances?
503496
* These lambdas are used as the RHS of higher-kinded abstract types or
@@ -1550,19 +1543,24 @@ object Types extends TypeUtils {
15501543
}
15511544
deskolemizer(this)
15521545

1553-
/** The result of normalization using `tryNormalize`, or the type itself if
1554-
* tryNormlize yields NoType
1546+
/** The result of normalization, or the type itself if none apply. */
1547+
final def normalized(using Context): Type = tryNormalize.orElse(this)
1548+
1549+
/** If this type has an underlying match type or applied compiletime.ops,
1550+
* then the result after applying all toplevel normalizations, otherwise NoType.
15551551
*/
1556-
final def normalized(using Context): Type = {
1557-
val normed = tryNormalize
1558-
if (normed.exists) normed else this
1559-
}
1552+
def tryNormalize(using Context): Type = underlyingNormalizable match
1553+
case mt: MatchType => mt.reduced.normalized
1554+
case tp: AppliedType => tp.tryCompiletimeConstantFold
1555+
case _ => NoType
15601556

1561-
/** If this type can be normalized at the top-level by rewriting match types
1562-
* of S[n] types, the result after applying all toplevel normalizations,
1563-
* otherwise NoType
1557+
/** Perform successive strippings, and beta-reductions of applied types until
1558+
* a match type or applied compiletime.ops is reached, if any, otherwise NoType.
15641559
*/
1565-
def tryNormalize(using Context): Type = NoType
1560+
def underlyingNormalizable(using Context): Type = stripped.stripLazyRef match
1561+
case tp: MatchType => tp
1562+
case tp: AppliedType => tp.underlyingNormalizable
1563+
case _ => NoType
15661564

15671565
private def widenDealias1(keep: AnnotatedType => Context ?=> Boolean)(using Context): Type = {
15681566
val res = this.widen.dealias1(keep, keepOpaques = false)
@@ -3258,8 +3256,6 @@ object Types extends TypeUtils {
32583256
private var myRef: Type | Null = null
32593257
private var computed = false
32603258

3261-
override def tryNormalize(using Context): Type = ref.tryNormalize
3262-
32633259
def ref(using Context): Type =
32643260
if computed then
32653261
if myRef == null then
@@ -4625,8 +4621,8 @@ object Types extends TypeUtils {
46254621
private var myEvalRunId: RunId = NoRunId
46264622
private var myEvalued: Type = uninitialized
46274623

4628-
private var validUnderlyingMatch: Period = Nowhere
4629-
private var cachedUnderlyingMatch: Type = uninitialized
4624+
private var validUnderlyingNormalizable: Period = Nowhere
4625+
private var cachedUnderlyingNormalizable: Type = uninitialized
46304626

46314627
def isGround(acc: TypeAccumulator[Boolean])(using Context): Boolean =
46324628
if myGround == 0 then myGround = if acc.foldOver(true, this) then 1 else -1
@@ -4690,37 +4686,25 @@ object Types extends TypeUtils {
46904686
case nil => x
46914687
foldArgs(op(x, tycon), args)
46924688

4693-
/** Exists if the tycon is a TypeRef of an alias with an underlying match type.
4694-
* Anything else should have already been reduced in `appliedTo` by the TypeAssigner.
4689+
/** Exists if the tycon is a TypeRef of an alias with an underlying match type,
4690+
* or a compiletime applied type. Anything else should have already been
4691+
* reduced in `appliedTo` by the TypeAssigner. This may reduce several
4692+
* HKTypeLambda applications before the underlying normalizable type is reached.
46954693
*/
4696-
override def underlyingMatchType(using Context): Type =
4697-
if ctx.period != validUnderlyingMatch then
4698-
cachedUnderlyingMatch = superType.underlyingMatchType
4699-
validUnderlyingMatch = validSuper
4700-
cachedUnderlyingMatch
4694+
override def underlyingNormalizable(using Context): Type =
4695+
if ctx.period != validUnderlyingNormalizable then tycon match
4696+
case tycon: TypeRef if defn.isCompiletimeAppliedType(tycon.symbol) =>
4697+
cachedUnderlyingNormalizable = this
4698+
validUnderlyingNormalizable = ctx.period
4699+
case _ =>
4700+
cachedUnderlyingNormalizable = superType.underlyingNormalizable
4701+
validUnderlyingNormalizable = validSuper
4702+
cachedUnderlyingNormalizable
47014703

4702-
override def tryNormalize(using Context): Type = tycon.stripTypeVar match {
4703-
case tycon: TypeRef =>
4704-
def tryMatchAlias = tycon.info match
4705-
case AliasingBounds(alias) if isMatchAlias =>
4706-
trace(i"normalize $this", typr, show = true) {
4707-
MatchTypeTrace.recurseWith(this) {
4708-
alias.applyIfParameterized(args.map(_.normalized)).tryNormalize
4709-
/* `applyIfParameterized` may reduce several HKTypeLambda applications
4710-
* before the underlying MatchType is reached.
4711-
* Even if they do not involve any match type normalizations yet,
4712-
* we still want to record these reductions in the MatchTypeTrace.
4713-
* They should however only be attempted if they eventually expand
4714-
* to a match type, which is ensured by the `isMatchAlias` guard.
4715-
*/
4716-
}
4717-
}
4718-
case _ =>
4719-
NoType
4720-
tryCompiletimeConstantFold.orElse(tryMatchAlias)
4721-
case _ =>
4722-
NoType
4723-
}
4704+
override def tryNormalize(using Context): Type =
4705+
if isMatchAlias && MatchTypeTrace.isRecording then
4706+
MatchTypeTrace.recurseWith(this)(superType.tryNormalize)
4707+
else super.tryNormalize
47244708

47254709
/** Is this an unreducible application to wildcard arguments?
47264710
* This is the case if tycon is higher-kinded. This means
@@ -5183,13 +5167,6 @@ object Types extends TypeUtils {
51835167
private var myReduced: Type | Null = null
51845168
private var reductionContext: util.MutableMap[Type, Type] | Null = null
51855169

5186-
override def tryNormalize(using Context): Type =
5187-
try
5188-
reduced.normalized
5189-
catch
5190-
case ex: Throwable =>
5191-
handleRecursive("normalizing", s"${scrutinee.show} match ..." , ex)
5192-
51935170
private def thisMatchType = this
51945171

51955172
def reduced(using Context): Type = atPhaseNoLater(elimOpaquePhase) {
@@ -5292,7 +5269,7 @@ object Types extends TypeUtils {
52925269
def apply(bound: Type, scrutinee: Type, cases: List[Type])(using Context): MatchType =
52935270
unique(new CachedMatchType(bound, scrutinee, cases))
52945271

5295-
def thatReducesUsingGadt(tp: Type)(using Context): Boolean = tp.underlyingMatchType match
5272+
def thatReducesUsingGadt(tp: Type)(using Context): Boolean = tp.underlyingNormalizable match
52965273
case mt: MatchType => mt.reducesUsingGadt
52975274
case _ => false
52985275

@@ -5741,7 +5718,8 @@ object Types extends TypeUtils {
57415718
/** Common supertype of `TypeAlias` and `MatchAlias` */
57425719
abstract class AliasingBounds(val alias: Type) extends TypeBounds(alias, alias) {
57435720

5744-
def derivedAlias(alias: Type)(using Context): AliasingBounds
5721+
def derivedAlias(alias: Type)(using Context): AliasingBounds =
5722+
if alias eq this.alias then this else AliasingBounds(alias)
57455723

57465724
override def computeHash(bs: Binders): Int = doHash(bs, alias)
57475725
override def hashIsStable: Boolean = alias.hashIsStable
@@ -5763,10 +5741,7 @@ object Types extends TypeUtils {
57635741

57645742
/** = T
57655743
*/
5766-
class TypeAlias(alias: Type) extends AliasingBounds(alias) {
5767-
def derivedAlias(alias: Type)(using Context): AliasingBounds =
5768-
if (alias eq this.alias) this else TypeAlias(alias)
5769-
}
5744+
class TypeAlias(alias: Type) extends AliasingBounds(alias)
57705745

57715746
/** = T where `T` is a `MatchType`
57725747
*
@@ -5775,10 +5750,7 @@ object Types extends TypeUtils {
57755750
* If we assumed full substitutivity, we would have to reject all recursive match
57765751
* aliases (or else take the jump and allow full recursive types).
57775752
*/
5778-
class MatchAlias(alias: Type) extends AliasingBounds(alias) {
5779-
def derivedAlias(alias: Type)(using Context): AliasingBounds =
5780-
if (alias eq this.alias) this else MatchAlias(alias)
5781-
}
5753+
class MatchAlias(alias: Type) extends AliasingBounds(alias)
57825754

57835755
object TypeBounds {
57845756
def apply(lo: Type, hi: Type)(using Context): TypeBounds =

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -2044,7 +2044,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
20442044
case _ => false
20452045
}
20462046

2047-
val result = pt.underlyingMatchType match {
2047+
val result = pt.underlyingNormalizable match {
20482048
case mt: MatchType if isMatchTypeShaped(mt) =>
20492049
typedDependentMatchFinish(tree, sel1, selType, tree.cases, mt)
20502050
case _ =>

compiler/test/dotc/neg-best-effort-pickling.blacklist

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ illegal-match-types.scala
1515
i13780-1.scala
1616
i20317a.scala
1717
i11226.scala
18+
i974.scala
1819

1920
# semantic db generation fails in the first compilation
2021
i1642.scala

tests/neg/i12049d.check

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
-- [E007] Type Mismatch Error: tests/neg/i12049d.scala:14:52 -----------------------------------------------------------
2+
14 |val x: M[NotRelevant[Nothing], Relevant[Nothing]] = 2 // error
3+
| ^
4+
| Found: (2 : Int)
5+
| Required: M[NotRelevant[Nothing], Relevant[Nothing]]
6+
|
7+
| Note: a match type could not be fully reduced:
8+
|
9+
| trying to reduce M[NotRelevant[Nothing], Relevant[Nothing]]
10+
| trying to reduce Relevant[Nothing]
11+
| failed since selector Nothing
12+
| is uninhabited (there are no values of that type).
13+
|
14+
| longer explanation available when compiling with `-explain`

tests/neg/i12049d.scala

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
2+
trait A
3+
trait B
4+
5+
type M[X, Y] = Y match
6+
case A => Int
7+
case B => String
8+
9+
type Relevant[Z] = Z match
10+
case A => B
11+
type NotRelevant[Z] = Z match
12+
case B => A
13+
14+
val x: M[NotRelevant[Nothing], Relevant[Nothing]] = 2 // error

tests/pos/i20482.scala

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
trait WrapperType[A]
2+
3+
case class Foo[A]()
4+
5+
case class Bar[A]()
6+
7+
type FooToBar[D[_]] = [A] =>> D[Unit] match {
8+
case Foo[Unit] => Bar[A]
9+
}
10+
11+
case class Test()
12+
object Test {
13+
implicit val wrapperType: WrapperType[Bar[Test]] = new WrapperType[Bar[Test]] {}
14+
}
15+
16+
val test = summon[WrapperType[FooToBar[Foo][Test]]]
+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
2+
type Rec[X] = X match
3+
case Int => Rec[X]
4+
5+
type M[Unused, Y] = Y match
6+
case String => Double
7+
8+
def foo[X](d: M[Rec[X], "hi"]) = ???
+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
2+
def Test = foo[Int](3d) // crash before changes

0 commit comments

Comments
 (0)