Skip to content

Commit

Permalink
Fix scala#16405 - wildcards prematurely resolving to Nothing
Browse files Browse the repository at this point in the history
This was a problem because it could it get in the way of some
metaprogramming techniques. The main issue was the fact that when typing
functions, the type inference would first look at the types from the
source method (resolving type wildcards to Nothing) and only after that,
it could look at the target method.
Now, in the case of wildcards we delay the resolution
from the source method until later, after which it is resolved according
to the target method. We also modify the target type resolution method
a bit, as just applying the above procedure would fail some of the
tests, eg. val y1: Function1[_, Nothing] = x => x
would be typed as Function1[Any, Nothing], which was incorrect.
  • Loading branch information
jchyb committed Jan 5, 2023
1 parent b65b0f2 commit 488165d
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 8 deletions.
9 changes: 5 additions & 4 deletions compiler/src/dotty/tools/dotc/typer/Inferencing.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@ object Inferencing {

/** Is type fully defined, meaning the type does not contain wildcard types
* or uninstantiated type variables. As a side effect, this will minimize
* any uninstantiated type variables, according to the given force degree,
* but only if the overall result of `isFullyDefined` is `true`.
* any uninstantiated type variables, according to the given force degree
* and the minimizeSelected setting, but only if the overall result of
* `isFullyDefined` is `true`.
* Variables that are successfully minimized do not count as uninstantiated.
*/
def isFullyDefined(tp: Type, force: ForceDegree.Value)(using Context): Boolean = {
def isFullyDefined(tp: Type, force: ForceDegree.Value, minimizeSelected: Boolean = false)(using Context): Boolean = {
val nestedCtx = ctx.fresh.setNewTyperState()
val result = new IsFullyDefinedAccumulator(force)(using nestedCtx).process(tp)
val result = new IsFullyDefinedAccumulator(force, minimizeSelected)(using nestedCtx).process(tp)
if (result) nestedCtx.typerState.commit()
result
}
Expand Down
24 changes: 20 additions & 4 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1234,7 +1234,22 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
// if expected parameter type(s) are wildcards, approximate from below.
// if expected result type is a wildcard, approximate from above.
// this can type the greatest set of admissible closures.
(pt1.argTypesLo.init, typeTree(interpolateWildcards(pt1.argTypesHi.last)))
// However for ? >: Nothing <: Any types (type wildcards),
// instead of approximating to Nothing or Any, we change them to bounded
// TypeVars to resolve later.
val init = pt1.argInfos.init.map {
case a @ TypeBounds(nt, at) if nt == defn.NothingType && at == defn.AnyType =>
interpolateWildcards(WildcardType(a))
case other => other.loBound
}

val last = pt1.argInfos.last match {
case a @ TypeBounds(nt, at) if nt == defn.NothingType && at == defn.AnyType =>
WildcardType(a)
case other => other.hiBound
}

(init, typeTree(interpolateWildcards(last)))
case RefinedType(parent, nme.apply, mt @ MethodTpe(_, formals, restpe))
if defn.isNonRefinedFunction(parent) && formals.length == defaultArity =>
(formals, untpd.DependentTypeTree(syms => restpe.substParams(mt, syms.map(_.termRef))))
Expand All @@ -1259,8 +1274,9 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
* The inference makes two attempts:
*
* 1. Compute the target type `T` and make it known that `S <: T`.
* If the expected type `S` can be fully defined under ForceDegree.flipBottom,
* pick this one (this might use the fact that S <: T for an upper approximation).
* If the expected type `S` can be fully defined under ForceDegree.flipBottom
* and with minimizedSelected option set as true, pick this one
* (this might use the fact that S <: T for an upper approximation).
* 2. Otherwise, if the target type `T` can be fully defined under ForceDegree.flipBottom,
* pick this one.
*
Expand All @@ -1280,7 +1296,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
else NoType
case _ => NoType
if target.exists then formal <:< target
if isFullyDefined(formal, ForceDegree.flipBottom) then formal
if isFullyDefined(formal, ForceDegree.flipBottom, minimizeSelected = true) then formal
else if target.exists && isFullyDefined(target, ForceDegree.flipBottom) then target
else NoType

Expand Down
31 changes: 31 additions & 0 deletions tests/run/16405.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import scala.compiletime.summonInline

case class TypeDesc[T](tpe: String)
object TypeDesc {
given nothing: TypeDesc[Nothing] = TypeDesc("Nothing")
given string: TypeDesc[String] = TypeDesc("String")
given int: TypeDesc[Int] = TypeDesc("Int")
}

def exampleFn(s: String, i: Int): Unit = ()

inline def argumentTypesOf[R](fun: (_, _) => R): (TypeDesc[?], TypeDesc[?]) = {
inline fun match {
case x: ((a, b) => R) =>
(scala.compiletime.summonInline[TypeDesc[a]], scala.compiletime.summonInline[TypeDesc[b]])
}
}
inline def argumentTypesOfNoWildCard[A, B, R](fun: (A, B) => R): (TypeDesc[?], TypeDesc[?]) = argumentTypesOf(fun)
inline def argumentTypesOfAllWildCard(fun: (?, ?) => ?): (TypeDesc[?], TypeDesc[?]) = argumentTypesOf(fun)

object Test {
def main(args: Array[String]): Unit = {
val expected = (TypeDesc.string, TypeDesc.int)
assert(argumentTypesOf(exampleFn) == expected)
assert(argumentTypesOf(exampleFn(_, _)) == expected)
assert(argumentTypesOfNoWildCard(exampleFn) == expected)
assert(argumentTypesOfNoWildCard(exampleFn(_, _)) == expected)
assert(argumentTypesOfAllWildCard(exampleFn) == expected)
assert(argumentTypesOfAllWildCard(exampleFn(_, _)) == expected)
}
}

0 comments on commit 488165d

Please sign in to comment.