Skip to content
Draft
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
9 changes: 9 additions & 0 deletions compiler/src/dotty/tools/dotc/core/TypeApplications.scala
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,15 @@ object TypeApplications {
}

def apply(t: Type): Type = t match {
case t @ AppliedType(tycon, args1) if tycon.isRef(defn.MatchCaseClass) =>
// Match-type case patterns and case bodies are both invariant, unsafe
// positions for wildcard substitution. Substituting a wildcard here
// either makes a pattern spuriously match anything (for a top-level
// param in a pattern) or loses the relationship between occurrences
// (for a param used multiple times in a body). See issue #21013.
// Force nested-level treatment so wildcard substitution is skipped,
// leaving the AppliedType to be flagged by `isUnreducibleWild`.
t.derivedAppliedType(apply(tycon), args1.mapConserve(arg => atNestedLevel(apply(arg))))
case t @ AppliedType(tycon, args1) if tycon.typeSymbol.isClass =>
t.derivedAppliedType(apply(tycon), args1.mapConserve(applyArg))
case t @ RefinedType(parent, name, TypeAlias(info)) =>
Expand Down
62 changes: 57 additions & 5 deletions compiler/src/dotty/tools/dotc/core/Types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4795,15 +4795,44 @@ object Types extends TypeUtils {
else super.tryNormalize

/** Is this an unreducible application to wildcard arguments?
* This is the case if tycon is higher-kinded. This means
* it is a subtype of a hk-lambda, but not a match alias.
* (normal parameterized aliases are removed in `appliedTo`).
* This is the case if tycon is higher-kinded and at least one argument
* is a wildcard, *unless* tycon is either:
* - the pseudo match alias `compiletime.ops.int.S`, or
* - a match alias whose wildcard arguments correspond only to type
* parameters that appear solely in the match scrutinee.
* (Normal parameterized aliases are removed in `appliedTo`.)
* Applications of higher-kinded type constructors to wildcard arguments
* are equivalent to existential types, which are not supported.
*
* For match type aliases we restrict the exemption: substituting a
* wildcard for a type parameter is unsound whenever that parameter
* occurs in a pattern (because pattern captures would then see a
* wildcard-refinable type), in a case body, or in the declared upper
* bound. Only parameters used exclusively in the scrutinee are safe,
* since in that case `M[?]` is simply an unreducible match type.
* See issue #21013.
*/
def isUnreducibleWild(using Context): Boolean =
tycon.isLambdaSub && hasWildcardArg && !isMatchAlias
&& !(args.sizeIs == 1 && defn.isCompiletime_S(tycon.typeSymbol)) // S is a pseudo Match Alias
if !tycon.isLambdaSub || !hasWildcardArg then false
else if args.sizeIs == 1 && defn.isCompiletime_S(tycon.typeSymbol) then false
else matchAliasLambda match
case lam: HKTypeLambda => unsoundMatchAliasWildcardArgs(lam, args)
case _ => true

/** If this application is (the eta-expansion of) a match type alias,
* returns the underlying `HKTypeLambda` whose `resType` is a `MatchType`.
* Otherwise returns `NoType`. Used by `isUnreducibleWild` so that sound
* wildcard applications of match aliases (the parameter only appears in
* the scrutinee) can be accepted, while unsound ones are rejected.
*/
private def matchAliasLambda(using Context): Type = tycon match
case tr: TypeRef => tr.info match
case MatchAlias(l: HKTypeLambda) => l
case _ => NoType
case l: HKTypeLambda => l.resType match
case _: MatchType => l
case _ => NoType
case _ => NoType

def tryCompiletimeConstantFold(using Context): Type =
if myEvalRunId == ctx.runId then myEvalued
Expand Down Expand Up @@ -7317,4 +7346,27 @@ object Types extends TypeUtils {
private val keepIfRefining: AnnotatedType => Context ?=> Boolean = _.isRefining

val isBounds: Type => Boolean = _.isInstanceOf[TypeBounds]

/** Does applying `lam` (the HKTypeLambda body of a match alias) to `args`
* yield an unsound wildcard application? The application is unsound iff
* `lam.resType` is a `MatchType` and some wildcard argument (a `TypeBounds`)
* corresponds to a lambda parameter that occurs outside the match scrutinee
* — i.e., in the match's declared upper bound, in any case pattern, or in
* any case body. See issue #21013 for the motivation: substituting `?` for
* a type parameter in such a position is analogous to the classic
* `type U[X] = (X, X); type V = U[?]` unsoundness.
*/
def unsoundMatchAliasWildcardArgs(lam: HKTypeLambda, args: List[Type])(using Context): Boolean =
lam.resType match
case mt: MatchType =>
val collector = new TypeAccumulator[Set[Int]]:
def apply(acc: Set[Int], tp: Type): Set[Int] = tp match
case tp: TypeParamRef if tp.binder eq lam => acc + tp.paramNum
case _ => foldOver(acc, tp)
val unsafe = mt.cases.foldLeft(collector(Set.empty, mt.bound))(collector(_, _))
unsafe.nonEmpty && args.iterator.zipWithIndex.exists {
case (_: TypeBounds, i) => unsafe.contains(i)
case _ => false
}
case _ => false
}
8 changes: 8 additions & 0 deletions docs/_spec/03-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -1155,6 +1155,14 @@ For ´n \geq 1´, it is specified as:

The reduction of an "empty" match type `´X´ match { }` (which cannot be written in user programs) is a compile error.

#### Applications to Wildcard Arguments

Let `´M´` be a _match type alias_ of the form `type ´M´[´a_1´, ..., ´a_n´] = ´X´ match { case ´P_1´ => ´R_1´; ...; case ´P_k´ => ´R_k´ }` (optionally with a declared upper bound).
An application `´M´[´T_1´, ..., ´T_n´]` where some `´T_i´` is a wildcard type argument is legal only if every such `´a_i´` occurs exclusively in the scrutinee `´X´` — i.e., it does not occur in the declared upper bound, in any pattern `´P_j´`, or in any body `´R_j´`.

If a wildcard is substituted for a parameter that appears in a pattern, the pattern may spuriously match scrutinees that no non-wildcard substitution would match.
If a wildcard is substituted for a parameter that appears multiple times in a body (or in the declared upper bound), the resulting type loses the identification between those occurrences, which is the same unsoundness as substituting a wildcard for `X` in `[X] =>> (X, X)`.

### Skolem Types

```ebnf
Expand Down
25 changes: 25 additions & 0 deletions tests/neg/i21013.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Issue #21013: applying a match type alias to a wildcard argument is
// unsound when the type parameter appears outside the match scrutinee
// (in a pattern, a case body, or the declared upper bound).

type M1[K] = Double match
case K => Int // K appears in a pattern

type M2[K] = Double match
case Option[K] => Int // K appears in a pattern

type M3[X, Y] = X match
case Int => (Y, Y) // Y appears in a case body

type M4[K, S] <: List[K] = S match // K appears in the declared upper bound
case Int => List[K]

def Test: Unit =
val x1: M1[?] = ??? // error
val x2: M2[?] = ??? // error
val x3: M3[Int, ?] = ??? // error
val x4: M4[?, String] = ??? // error

// The unsoundness observed in #21013: M1[?] should not be a valid
// supertype of M1[Int]. With the fix, M1[?] itself is rejected.
// (If it were accepted, M1[?] would reduce to Int, but M1[Int] would not.)
34 changes: 34 additions & 0 deletions tests/pos/i21013.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Companion to tests/neg/i21013.scala: wildcard applications of match
// type aliases are sound, and must be accepted, when every wildcarded
// parameter occurs only in the scrutinee of the underlying match type.

// Sound: parameter only in scrutinee (single case)
type Ok1[X] = X match
case Int => String

val a1: Ok1[?] = ???
def b1: Ok1[?] = ???
val c1: List[Ok1[?]] = Nil
val d1: Option[Ok1[?]] = None

// Sound: parameter only in scrutinee (multiple cases, default)
type Ok2[X] = X match
case Int => String
case String => Int
case _ => Double

val a2: Ok2[?] = ???
def b2: Array[Ok2[?]] = null

// Sound mix: the wildcarded parameter only appears in the scrutinee,
// the other parameter is given concretely and may appear anywhere.
type Ok3[X, Y] = X match
case Int => (Y, Y)
case String => List[Y]

val a3: Ok3[?, Int] = ???
def b3: Ok3[?, String] = ???

// Bounded wildcard, still sound
def b4: Ok1[? <: Int] = ???
def b5: Ok2[? >: Nothing <: Int | String] = ???
Loading