Erased casts supported

This commit is contained in:
Andrey Breslav
2013-09-03 19:54:27 +04:00
parent 173303104e
commit e3079ac667
26 changed files with 298 additions and 158 deletions
@@ -1,6 +1,8 @@
package org.jetbrains.jet.lang.types;
import com.google.common.base.Predicate;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.jet.lang.PlatformToKotlinClassMap;
import org.jetbrains.jet.lang.descriptors.ClassDescriptor;
@@ -8,10 +10,12 @@ import org.jetbrains.jet.lang.descriptors.ClassKind;
import org.jetbrains.jet.lang.descriptors.ClassifierDescriptor;
import org.jetbrains.jet.lang.descriptors.TypeParameterDescriptor;
import org.jetbrains.jet.lang.types.checker.JetTypeChecker;
import org.jetbrains.jet.lang.types.checker.TypeCheckingProcedure;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class CastDiagnosticsUtil {
@@ -44,11 +48,8 @@ public class CastDiagnosticsUtil {
List<JetType> aTypes = mapToPlatformIndependentTypes(a, platformToKotlinClassMap);
List<JetType> bTypes = mapToPlatformIndependentTypes(b, platformToKotlinClassMap);
for (int i = 0; i < aTypes.size(); i++) {
JetType aType = aTypes.get(i);
for (int j = 0; j < bTypes.size(); j++) {
JetType bType = bTypes.get(j);
for (JetType aType : aTypes) {
for (JetType bType : bTypes) {
if (JetTypeChecker.INSTANCE.isSubtypeOf(aType, bType)) return true;
if (JetTypeChecker.INSTANCE.isSubtypeOf(bType, aType)) return true;
}
@@ -91,5 +92,82 @@ public class CastDiagnosticsUtil {
return descriptor instanceof ClassDescriptor && ((ClassDescriptor) descriptor).getKind() == ClassKind.TRAIT;
}
/**
* Check if cast from supertype to subtype is erased.
* It is an error in "is" statement and warning in "as".
*/
public static boolean isCastErased(@NotNull JetType supertype, @NotNull JetType subtype, @NotNull JetTypeChecker typeChecker) {
// cast between T and T? is always OK
if (supertype.isNullable() || subtype.isNullable()) {
return isCastErased(TypeUtils.makeNotNullable(supertype), TypeUtils.makeNotNullable(subtype), typeChecker);
}
// if it is a upcast, it's never erased
if (typeChecker.isSubtypeOf(supertype, subtype)) return false;
// downcasting to a type parameter is always erased
if (isTypeParameter(subtype)) return true;
// Check that we are actually casting to a generic type
// NOTE: this does not account for 'as Array<List<T>>'
if (allParametersReified(subtype)) return false;
// Assume we are casting an expression of type Collection<Foo> to List<Bar>
// First, let's make List<T>, where T is a type variable
JetType subtypeWithVariables = TypeUtils.makeUnsubstitutedType(
subtype.getConstructor(),
ErrorUtils.createErrorScope("Scope for intermediate type. This type shouldn't be used outside isCastErased()", true));
// Now, let's find a supertype of List<T> that is a Collection of something,
// in this case it will be Collection<T>
JetType supertypeWithVariables = TypeCheckingProcedure.findCorrespondingSupertype(subtypeWithVariables, supertype);
if (supertypeWithVariables == null) return true;
// Now, let's try to unify Collection<T> and Collection<Foo>
// solution is a map from T to Foo
final List<TypeParameterDescriptor> variables = subtypeWithVariables.getConstructor().getParameters();
TypeUnifier.UnificationResult solution = TypeUnifier.unify(
new TypeProjection(supertype), new TypeProjection(supertypeWithVariables),
new Predicate<TypeConstructor>() {
@Override
public boolean apply(TypeConstructor typeConstructor) {
ClassifierDescriptor descriptor = typeConstructor.getDeclarationDescriptor();
return descriptor instanceof TypeParameterDescriptor && variables.contains(descriptor);
}
});
// If some of the parameters are not determined by unification, it means that these parameters are lost,
// let's put stars instead, so that we can only cast to something like List<*>, e.g. (a: Any) as List<*>
Map<TypeConstructor, TypeProjection> substitution = Maps.newHashMap(solution.getSubstitution());
for (TypeParameterDescriptor variable : variables) {
TypeProjection value = substitution.get(variable.getTypeConstructor());
if (value == null) {
substitution.put(
variable.getTypeConstructor(),
SubstitutionUtils.makeStarProjection(variable)
);
}
}
// At this point we have values for all type parameters of List
// Let's make a type by substituting them: List<T> -> List<Foo>
JetType staticallyKnownSubtype = TypeSubstitutor.create(substitution).substitute(subtypeWithVariables, Variance.INVARIANT);
// If the substitution failed, it means that the result is an impossible type, e.g. something like Out<in Foo>
// In this case, we can't guarantee anything, so the cast is considered to be erased
if (staticallyKnownSubtype == null) return true;
// If the type we calculated is a subtype of the cast target, it's OK to use the cast target instead.
// If not, it's wrong to use it
return !typeChecker.isSubtypeOf(staticallyKnownSubtype, subtype);
}
private static boolean allParametersReified(JetType subtype) {
for (TypeParameterDescriptor parameterDescriptor : subtype.getConstructor().getParameters()) {
if (!parameterDescriptor.isReified()) return false;
}
return true;
}
private CastDiagnosticsUtil() {}
}
@@ -353,13 +353,18 @@ public class TypeUtils {
@NotNull
public static JetType makeUnsubstitutedType(ClassDescriptor classDescriptor, JetScope unsubstitutedMemberScope) {
if (ErrorUtils.isError(classDescriptor)) {
return ErrorUtils.createErrorType("Unsubstituted type for " + classDescriptor);
return makeUnsubstitutedType(classDescriptor.getTypeConstructor(), unsubstitutedMemberScope);
}
@NotNull
public static JetType makeUnsubstitutedType(TypeConstructor typeConstructor, JetScope unsubstitutedMemberScope) {
if (ErrorUtils.isError(typeConstructor)) {
return ErrorUtils.createErrorType("Unsubstituted type for " + typeConstructor);
}
List<TypeProjection> arguments = getDefaultTypeProjections(classDescriptor.getTypeConstructor().getParameters());
List<TypeProjection> arguments = getDefaultTypeProjections(typeConstructor.getParameters());
return new JetTypeImpl(
Collections.<AnnotationDescriptor>emptyList(),
classDescriptor.getTypeConstructor(),
typeConstructor,
false,
arguments,
unsubstitutedMemberScope
@@ -17,7 +17,6 @@
package org.jetbrains.jet.lang.types.expressions;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.intellij.psi.PsiElement;
import com.intellij.psi.tree.IElementType;
import org.jetbrains.annotations.NotNull;
@@ -57,7 +56,6 @@ import org.jetbrains.jet.lang.resolve.scopes.receivers.ReceiverValue;
import org.jetbrains.jet.lang.resolve.scopes.receivers.TransientReceiver;
import org.jetbrains.jet.lang.types.*;
import org.jetbrains.jet.lang.types.checker.JetTypeChecker;
import org.jetbrains.jet.lang.types.checker.TypeCheckingProcedure;
import org.jetbrains.jet.lang.types.lang.KotlinBuiltIns;
import org.jetbrains.jet.lexer.JetTokens;
import org.jetbrains.jet.utils.ThrowingList;
@@ -235,156 +233,13 @@ public class BasicExpressionTypingVisitor extends ExpressionTypingVisitor {
context.trace.report(USELESS_CAST.on(expression.getOperationReference()));
}
else {
if (isCastErased(actualType, targetType, typeChecker)) {
if (CastDiagnosticsUtil.isCastErased(actualType, targetType, typeChecker)) {
context.trace.report(Errors.UNCHECKED_CAST.on(expression, actualType, targetType));
}
}
}
}
/**
* Check if assignment from supertype to subtype is erased.
* It is an error in "is" statement and warning in "as".
*/
public static boolean isCastErased(@NotNull JetType supertype, @NotNull JetType subtype, @NotNull JetTypeChecker typeChecker) {
if (!(subtype.getConstructor().getDeclarationDescriptor() instanceof ClassDescriptor)) {
// TODO: what if it is TypeParameterDescriptor?
return false;
}
// do not crash on error types
if (ErrorUtils.isErrorType(supertype) || ErrorUtils.isErrorType(subtype)) {
return false;
}
return hasDowncastsInTypeArguments(supertype, subtype, typeChecker) || hasErasedTypeArguments(supertype, subtype);
}
/*
Check if type arguments are downcasted, which cannot be checked at run-time.
Examples:
1. a: MutableList<out Any> is MutableList<String> - true, because 'String' is more specific
2. a: Collection<String> is List<Any> - false, because 'Any' is less specific, and it is guaranteed by static checker
3. a: MutableCollection<String> is MutableList<Any> - false, because these types have empty intersection (type parameter is invariant)
*/
private static boolean hasDowncastsInTypeArguments(
@NotNull JetType supertype,
@NotNull JetType subtype,
@NotNull JetTypeChecker typeChecker
) {
List<TypeParameterDescriptor> superParameters = supertype.getConstructor().getParameters();
// This map holds arguments for type parameters of all superclasses of a type (see method comment for sample)
Multimap<TypeConstructor, TypeProjection> subtypeSubstitutionMap = SubstitutionUtils.buildDeepSubstitutionMultimap(subtype);
for (int i = 0; i < superParameters.size(); i++) {
TypeProjection superArgument = supertype.getArguments().get(i);
TypeParameterDescriptor parameter = superParameters.get(i);
if (parameter.isReified()) {
continue;
}
Collection<TypeProjection> substituted = subtypeSubstitutionMap.get(parameter.getTypeConstructor());
for (TypeProjection substitutedArgument : substituted) {
// For sample #2 (a: Collection<String> is List<Any>):
// parameter = E declared in Collection
// superArgument = String
// substitutedArgument = Any, because Collection<Any> is the supertype of List<Any>
// 1. Any..Nothing
// 2. String..Nothing
// 3. String..String
JetType superOut = TypeCheckingProcedure.getOutType(parameter, superArgument);
JetType superIn = TypeCheckingProcedure.getInType(parameter, superArgument);
// 1. String..String
// 2. Any..Nothing
// 3. Any..Any
JetType subOut = TypeCheckingProcedure.getOutType(parameter, substitutedArgument);
JetType subIn = TypeCheckingProcedure.getInType(parameter, substitutedArgument);
// super type range must be a subset of sub type range
if (typeChecker.isSubtypeOf(superOut, subOut) && typeChecker.isSubtypeOf(subIn, superIn)) {
// continue
}
else {
return true;
}
}
}
return false;
}
/*
Check if type arguments are erased, that is they are not mapped to type parameters of supertype's class
Examples (MyMap is defined like this: trait MyMap<T>: Map<String, T>):
1. a: Any is List<String> - true
2. a: Collection<CharSequence> is List<String> - false
3. a: Map<String, String> is MyMap<String> - false
*/
private static boolean hasErasedTypeArguments(
@NotNull JetType supertype,
@NotNull JetType subtype
) {
// Erase all type arguments, replacing them with unsubstituted versions:
// 1. List<E>
// 2. List<E>
// 3. MyMap<T>
JetType subtypeCleared = TypeUtils.makeUnsubstitutedType(
(ClassDescriptor) subtype.getConstructor().getDeclarationDescriptor(), null);
// This map holds arguments for type parameters of all superclasses of a type (see method comment for sample)
// For all "E" declared in Collection, Iterable, etc., value will be type "E", where the latter E is declared in List
Multimap<TypeConstructor, TypeProjection> clearTypeSubstitutionMap =
SubstitutionUtils.buildDeepSubstitutionMultimap(subtypeCleared);
// This set will contain all arguments for type parameters of superclass which are mapped from type parameters of subtype's class
// 1. empty
// 2. [E declared in List]
// 3. [T declared in MyMap]
Set<JetType> clearSubstituted = new HashSet<JetType>();
List<TypeParameterDescriptor> superParameters = supertype.getConstructor().getParameters();
for (TypeParameterDescriptor superParameter : superParameters) {
Collection<TypeProjection> substituted = clearTypeSubstitutionMap.get(superParameter.getTypeConstructor());
for (TypeProjection substitutedProjection : substituted) {
clearSubstituted.add(substitutedProjection.getType());
}
}
// For each type parameter of subtype's class, we check that it is mapped to type parameters of supertype,
// that is its type is present in clearSubstituted set
List<TypeParameterDescriptor> subParameters = subtype.getConstructor().getParameters();
for (int i = 0; i < subParameters.size(); i++) {
TypeParameterDescriptor parameter = subParameters.get(i);
TypeProjection argument = subtype.getArguments().get(i);
if (parameter.isReified()) {
continue;
}
// "is List<*>", no check for type argument, actually
if (argument.equals(SubstitutionUtils.makeStarProjection(parameter))) {
continue;
}
// if parameter is mapped to nothing then it is erased
// 1. return from here
// 2. contains = true, don't return
// 3. contains = true, don't return
if (!clearSubstituted.contains(parameter.getDefaultType())) {
return true;
}
}
return false;
}
@Override
public JetTypeInfo visitThisExpression(JetThisExpression expression, ExpressionTypingContext context) {
JetType result = null;
@@ -285,7 +285,7 @@ public class PatternMatchingTypingVisitor extends ExpressionTypingVisitor {
context.trace.report(Errors.USELESS_NULLABLE_CHECK.on(nullableType));
}
checkTypeCompatibility(context, type, subjectType, typeReferenceAfterIs);
if (BasicExpressionTypingVisitor.isCastErased(subjectType, type, JetTypeChecker.INSTANCE)) {
if (CastDiagnosticsUtil.isCastErased(subjectType, type, JetTypeChecker.INSTANCE)) {
context.trace.report(Errors.CANNOT_CHECK_FOR_ERASED.on(typeReferenceAfterIs, type));
}
return new DataFlowInfos(context.dataFlowInfo.establishSubtyping(subjectDataFlowValue, type), context.dataFlowInfo);