Skip to content

Fix #5302: Check well-formedness of inferred types #8462

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

Merged
merged 8 commits into from
Mar 12, 2020
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
6 changes: 4 additions & 2 deletions compiler/src/dotty/tools/dotc/core/TypeOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -370,14 +370,16 @@ trait TypeOps { this: Context => // TODO: Make standalone object.
* In fact the current treatment for this sitiuation can so far only be classified as "not obviously wrong",
* (maybe it still needs to be revised).
*/
def boundsViolations(args: List[Tree], boundss: List[TypeBounds], instantiate: (Type, List[Type]) => Type, app: Type)(implicit ctx: Context): List[BoundsViolation] = {
def boundsViolations(args: List[Tree], boundss: List[TypeBounds],
instantiate: (Type, List[Type]) => Type, app: Type)(
implicit ctx: Context): List[BoundsViolation] = {
val argTypes = args.tpes

/** Replace all wildcards in `tps` with `<app>#<tparam>` where `<tparam>` is the
* type parameter corresponding to the wildcard.
*/
def skolemizeWildcardArgs(tps: List[Type], app: Type) = app match {
case AppliedType(tycon, args) if tycon.typeSymbol.isClass && !scala2CompatMode =>
case AppliedType(tycon: TypeRef, args) if tycon.typeSymbol.isClass && !scala2CompatMode =>
tps.zipWithConserve(tycon.typeSymbol.typeParams) {
(tp, tparam) => tp match {
case _: TypeBounds => app.select(tparam)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ enum ErrorMessageID extends java.lang.Enum[ErrorMessageID] {
ExpectedTokenButFoundID,
MixedLeftAndRightAssociativeOpsID,
CantInstantiateAbstractClassOrTraitID,
DUMMY_AVAILABLE_1,
UnreducibleApplicationID,
OverloadedOrRecursiveMethodNeedsResultTypeID,
RecursiveValueNeedsResultTypeID,
CyclicReferenceInvolvingID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ abstract class Message(val errorId: ErrorMessageID) { self =>
val kind = self.kind
val explanation = self.explanation
}

def appendExplanation(suffix: => String): Message = new Message(errorId):
val msg = self.msg
val kind = self.kind
val explanation = self.explanation ++ suffix
}

/** An extended message keeps the contained message from being evaluated, while
Expand Down
12 changes: 10 additions & 2 deletions compiler/src/dotty/tools/dotc/reporting/diagnostic/messages.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1235,6 +1235,14 @@ object messages {
|""".stripMargin
}

case class UnreducibleApplication(tycon: Type)(using Context) extends Message(UnreducibleApplicationID):
val kind = "Type"
val msg = em"unreducible application of higher-kinded type $tycon to wildcard arguments"
val explanation =
em"""|An abstract type constructor cannot be applied to wildcard arguments.
|Such applications are equivalent to existential types, which are not
|supported in Scala 3."""

case class OverloadedOrRecursiveMethodNeedsResultType(cycleSym: Symbol)(implicit ctx: Context)
extends Message(OverloadedOrRecursiveMethodNeedsResultTypeID) {
val kind: String = "Cyclic"
Expand Down Expand Up @@ -1464,13 +1472,13 @@ object messages {
val parameters = if (numParams == 1) "parameter" else "parameters"
val msg: String = em"Missing type $parameters for $tpe"
val kind: String = "Type Mismatch"
val explanation: String = em"A fully applied type is expected but $tpe takes $numParams $parameters."
val explanation: String = em"A fully applied type is expected but $tpe takes $numParams $parameters"
}

case class DoesNotConformToBound(tpe: Type, which: String, bound: Type)(
err: Errors)(implicit ctx: Context)
extends Message(DoesNotConformToBoundID) {
val msg: String = em"Type argument ${tpe} does not conform to $which bound $bound ${err.whyNoMatchStr(tpe, bound)}"
val msg: String = em"Type argument ${tpe} does not conform to $which bound $bound${err.whyNoMatchStr(tpe, bound)}"
val kind: String = "Type Mismatch"
val explanation: String = ""
}
Expand Down
41 changes: 34 additions & 7 deletions compiler/src/dotty/tools/dotc/transform/PostTyper.scala
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,24 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase
tree
}

private def processValOrDefDef(tree: Tree)(using Context): tree.type =
tree match
case tree: ValOrDefDef if !tree.symbol.is(Synthetic) =>
checkInferredWellFormed(tree.tpt)
case _ =>
processMemberDef(tree)

private def checkInferredWellFormed(tree: Tree)(using ctx: Context): Unit = tree match
case tree: TypeTree
if tree.span.isZeroExtent
// don't check TypeTrees with non-zero extent;
// these are derived from explicit types
&& !ctx.reporter.errorsReported
// don't check if errors were already reported; this avoids follow-on errors
// for inferred types if explicit types are already ill-formed
=> Checking.checkAppliedTypesIn(tree)
case _ =>

private def transformSelect(tree: Select, targs: List[Tree])(implicit ctx: Context): Tree = {
val qual = tree.qualifier
qual.symbol.moduleClass.denot match {
Expand Down Expand Up @@ -226,17 +244,26 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase
else dropInlines.transform(arg)))
else
tree
methPart(app) match {
def app1 =
// reverse order of transforming args and fun. This way, we get a chance to see other
// well-formedness errors before reporting errors in possible inferred type args of fun.
val args1 = transform(app.args)
cpy.Apply(app)(transform(app.fun), args1)
methPart(app) match
case Select(nu: New, nme.CONSTRUCTOR) if isCheckable(nu) =>
// need to check instantiability here, because the type of the New itself
// might be a type constructor.
Checking.checkInstantiable(tree.tpe, nu.posd)
withNoCheckNews(nu :: Nil)(super.transform(app))
withNoCheckNews(nu :: Nil)(app1)
case _ =>
super.transform(app)
}
app1
case UnApply(fun, implicits, patterns) =>
// Reverse transform order for the same reason as in `app1` above.
val patterns1 = transform(patterns)
cpy.UnApply(tree)(transform(fun), transform(implicits), patterns1)
case tree: TypeApply =>
val tree1 @ TypeApply(fn, args) = normalizeTypeArgs(tree)
args.foreach(checkInferredWellFormed)
if (fn.symbol != defn.ChildAnnot.primaryConstructor)
// Make an exception for ChildAnnot, which should really have AnyKind bounds
Checking.checkBounds(args, fn.tpe.widen.asInstanceOf[PolyType])
Expand All @@ -262,11 +289,11 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase
}
case tree: ValDef =>
val tree1 = cpy.ValDef(tree)(rhs = normalizeErasedRhs(tree.rhs, tree.symbol))
processMemberDef(super.transform(tree1))
processValOrDefDef(super.transform(tree1))
case tree: DefDef =>
annotateContextResults(tree)
val tree1 = cpy.DefDef(tree)(rhs = normalizeErasedRhs(tree.rhs, tree.symbol))
processMemberDef(superAcc.wrapDefDef(tree1)(super.transform(tree1).asInstanceOf[DefDef]))
processValOrDefDef(superAcc.wrapDefDef(tree1)(super.transform(tree1).asInstanceOf[DefDef]))
case tree: TypeDef =>
val sym = tree.symbol
if (sym.isClass)
Expand Down Expand Up @@ -298,7 +325,7 @@ class PostTyper extends MacroTransform with IdentityDenotTransformer { thisPhase
else if (tree.tpt.symbol == defn.orType)
() // nothing to do
else
Checking.checkAppliedType(tree, boundsCheck = !ctx.mode.is(Mode.Pattern))
Checking.checkAppliedType(tree)
super.transform(tree)
case SingletonTypeTree(ref) =>
Checking.checkRealizable(ref.tpe, ref.posd)
Expand Down
62 changes: 47 additions & 15 deletions compiler/src/dotty/tools/dotc/typer/Checking.scala
Original file line number Diff line number Diff line change
Expand Up @@ -42,21 +42,37 @@ import scala.internal.Chars.isOperatorPart
object Checking {
import tpd._

/** Add further information for error messages involving applied types if the
* type is inferred:
* 1. the full inferred type is a TypeTree node
* 2. the applied type causing the error, if different from (1)
*/
private def showInferred(msg: Message, app: Type, tpt: Tree)(using ctx: Context): Message =
if tpt.isInstanceOf[TypeTree] then
def subPart = if app eq tpt.tpe then "" else i" subpart $app of"
msg.append(i" in$subPart inferred type ${tpt}")
.appendExplanation("\n\nTo fix the problem, provide an explicit type.")
else msg

/** A general checkBounds method that can be used for TypeApply nodes as
* well as for AppliedTypeTree nodes. Also checks that type arguments to
* *-type parameters are fully applied.
* See TypeOps.boundsViolations for an explanation of the parameters.
* @param tpt If bounds are checked for an AppliedType, the type tree representing
* or (in case it is inferred) containing the type.
* See TypeOps.boundsViolations for an explanation of the first four parameters.
*/
def checkBounds(args: List[tpd.Tree], boundss: List[TypeBounds], instantiate: (Type, List[Type]) => Type, app: Type = NoType)(implicit ctx: Context): Unit = {
def checkBounds(args: List[tpd.Tree], boundss: List[TypeBounds],
instantiate: (Type, List[Type]) => Type, app: Type = NoType, tpt: Tree = EmptyTree)(implicit ctx: Context): Unit =
args.lazyZip(boundss).foreach { (arg, bound) =>
if (!bound.isLambdaSub && !arg.tpe.hasSimpleKind)
errorTree(arg, MissingTypeParameterInTypeApp(arg.tpe))
if !bound.isLambdaSub && !arg.tpe.hasSimpleKind then
errorTree(arg,
showInferred(MissingTypeParameterInTypeApp(arg.tpe), app, tpt))
}
for ((arg, which, bound) <- ctx.boundsViolations(args, boundss, instantiate, app))
for (arg, which, bound) <- ctx.boundsViolations(args, boundss, instantiate, app) do
ctx.error(
DoesNotConformToBound(arg.tpe, which, bound)(err),
showInferred(DoesNotConformToBound(arg.tpe, which, bound)(err),
app, tpt),
arg.sourcePos.focus)
}

/** Check that type arguments `args` conform to corresponding bounds in `tl`
* Note: This does not check the bounds of AppliedTypeTrees. These
Expand All @@ -71,31 +87,33 @@ object Checking {
* check that it or one of its supertypes can be reduced to a normal application.
* Unreducible applications correspond to general existentials, and we
* cannot handle those.
* @param tree The applied type tree to check
* @param tpt If `tree` is synthesized from a type in a TypeTree,
* the original TypeTree, or EmptyTree otherwise.
*/
def checkAppliedType(tree: AppliedTypeTree, boundsCheck: Boolean)(implicit ctx: Context): Unit = {
def checkAppliedType(tree: AppliedTypeTree, tpt: Tree = EmptyTree)(using ctx: Context): Unit = {
val AppliedTypeTree(tycon, args) = tree
// If `args` is a list of named arguments, return corresponding type parameters,
// otherwise return type parameters unchanged
val tparams = tycon.tpe.typeParams
def argNamed(tparam: ParamInfo) = args.find {
case NamedArg(name, _) => name == tparam.paramName
case _ => false
}.getOrElse(TypeTree(tparam.paramRef))
val orderedArgs = if (hasNamedArg(args)) tparams.map(argNamed) else args
val bounds = tparams.map(_.paramInfoAsSeenFrom(tree.tpe).bounds)
def instantiate(bound: Type, args: List[Type]) =
tparams match
case LambdaParam(lam, _) :: _ =>
HKTypeLambda.fromParams(tparams, bound).appliedTo(args)
case _ =>
bound // paramInfoAsSeenFrom already took care of instantiation in this case
if (boundsCheck) checkBounds(orderedArgs, bounds, instantiate, tree.tpe)
if !ctx.mode.is(Mode.Pattern) // no bounds checking in patterns
&& tycon.symbol != defn.TypeBoxClass // TypeBox types are generated for capture
// conversion, may contain AnyKind as arguments
then
checkBounds(args, bounds, instantiate, tree.tpe, tpt)

def checkWildcardApply(tp: Type): Unit = tp match {
case tp @ AppliedType(tycon, _) =>
if (tycon.isLambdaSub && tp.hasWildcardArg)
ctx.errorOrMigrationWarning(
ex"unreducible application of higher-kinded type $tycon to wildcard arguments",
showInferred(UnreducibleApplication(tycon), tp, tpt),
tree.sourcePos)
case _ =>
}
Expand All @@ -104,6 +122,20 @@ object Checking {
checkValidIfApply(ctx.addMode(Mode.AllowLambdaWildcardApply))
}

/** Check all applied type trees in inferred type `tpt` for well-formedness */
def checkAppliedTypesIn(tpt: TypeTree)(implicit ctx: Context): Unit =
val checker = new TypeTraverser:
def traverse(tp: Type) =
tp match
case AppliedType(tycon, argTypes) =>
checkAppliedType(
untpd.AppliedTypeTree(TypeTree(tycon), argTypes.map(TypeTree))
.withType(tp).withSpan(tpt.span.toSynthetic),
tpt)
case _ =>
traverseChildren(tp)
checker.traverse(tpt.tpe)

def checkNoWildcard(tree: Tree)(implicit ctx: Context): Tree = tree.tpe match {
case tpe: TypeBounds => errorTree(tree, "no wildcard type allowed here")
case _ => tree
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/util/Spans.scala
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ object Spans {
def isSourceDerived: Boolean = !isSynthetic

/** Is this a zero-extent span? */
def isZeroExtent: Boolean = start == end
def isZeroExtent: Boolean = exists && start == end

/** A span where all components are shifted by a given `offset`
* relative to this span.
Expand Down
16 changes: 12 additions & 4 deletions tests/neg/i4382.check
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
-- Error: tests/neg/i4382.scala:3:10 -----------------------------------------------------------------------------------
-- [E043] Type Error: tests/neg/i4382.scala:3:10 -----------------------------------------------------------------------
3 | def v1: Id[_] = ??? // error
| ^^^^^
| unreducible application of higher-kinded type App.Id to wildcard arguments
-- Error: tests/neg/i4382.scala:6:10 -----------------------------------------------------------------------------------

longer explanation available when compiling with `-explain`
-- [E043] Type Error: tests/neg/i4382.scala:6:10 -----------------------------------------------------------------------
6 | def v2: HkL[_] = ??? // error
| ^^^^^^
| unreducible application of higher-kinded type App.HkL to wildcard arguments
-- Error: tests/neg/i4382.scala:9:10 -----------------------------------------------------------------------------------

longer explanation available when compiling with `-explain`
-- [E043] Type Error: tests/neg/i4382.scala:9:10 -----------------------------------------------------------------------
9 | def v3: HkU[_] = ??? // error
| ^^^^^^
| unreducible application of higher-kinded type App.HkU to wildcard arguments
-- Error: tests/neg/i4382.scala:12:10 ----------------------------------------------------------------------------------

longer explanation available when compiling with `-explain`
-- [E043] Type Error: tests/neg/i4382.scala:12:10 ----------------------------------------------------------------------
12 | def v4: HkAbs[_] = ??? // error
| ^^^^^^^^
| unreducible application of higher-kinded type App.HkAbs to wildcard arguments

longer explanation available when compiling with `-explain`
3 changes: 3 additions & 0 deletions tests/neg/i5302.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
type L[X]
def foo = { class A; null.asInstanceOf[L[A]] } // error
def bar(x: L[_]) = x // error
6 changes: 6 additions & 0 deletions tests/neg/i6205.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- [E057] Type Mismatch Error: tests/neg/i6205.scala:4:9 ---------------------------------------------------------------
4 | def foo = // error
| ^
| Type argument Nothing does not conform to lower bound Null in inferred type Contra[Nothing]

longer explanation available when compiling with `-explain`
8 changes: 8 additions & 0 deletions tests/neg/i6205.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class Contra[-T >: Null]

object Test:
def foo = // error
class A
new Contra[A]

val x = foo