JS/RTTI: if it's known that T <: X & Y, where T is non-reified type variable, for each a is T check that a is X && a is Y

This commit is contained in:
Alexey Andreev
2016-04-29 11:31:11 +03:00
parent 6454613b51
commit 7ff658414f
10 changed files with 213 additions and 103 deletions
@@ -61,5 +61,6 @@ enum class TypeCheck {
TYPEOF,
INSTANCEOF,
OR_NULL,
IS_ANY
IS_ANY,
AND_PREDICATE
}
@@ -59,6 +59,12 @@ public class CastTestGenerated extends AbstractCastTest {
doTest(fileName);
}
@TestMetadata("castToGenericTypeWithMultipleUpperBounds.kt")
public void testCastToGenericTypeWithMultipleUpperBounds() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/expression/cast/cases/castToGenericTypeWithMultipleUpperBounds.kt");
doTest(fileName);
}
@TestMetadata("castToGenericTypeWithUpperBound.kt")
public void testCastToGenericTypeWithUpperBound() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/expression/cast/cases/castToGenericTypeWithUpperBound.kt");
@@ -56,23 +56,25 @@ public class DirectiveTestUtils {
private static final DirectiveHandler FUNCTION_CALLED_IN_SCOPE = new DirectiveHandler("CHECK_CALLED_IN_SCOPE") {
@Override
void processEntry(@NotNull JsNode ast, @NotNull ArgumentsHelper arguments) throws Exception {
checkCalledInScope(ast, arguments.getNamedArgument("function"), arguments.getNamedArgument("scope"));
// Be more restrictive, check qualified match by default
checkCalledInScope(ast, arguments.getNamedArgument("function"), arguments.getNamedArgument("scope"),
parseBooleanArgument(arguments, "qualified", true));
}
};
private static final DirectiveHandler FUNCTION_NOT_CALLED_IN_SCOPE = new DirectiveHandler("CHECK_NOT_CALLED_IN_SCOPE") {
@Override
void processEntry(@NotNull JsNode ast, @NotNull ArgumentsHelper arguments) throws Exception {
checkNotCalledInScope(ast, arguments.getNamedArgument("function"), arguments.getNamedArgument("scope"));
// Be more restrictive, check unqualified match by default
checkNotCalledInScope(ast, arguments.getNamedArgument("function"), arguments.getNamedArgument("scope"),
parseBooleanArgument(arguments, "qualified", false));
}
};
private static final DirectiveHandler METHOD_NOT_CALLED_IN_SCOPE = new DirectiveHandler("CHECK_METHOD_NOT_CALLED_IN_SCOPE") {
@Override
void processEntry(@NotNull JsNode ast, @NotNull ArgumentsHelper arguments) throws Exception {
checkMethodNotCalledInScope(ast, arguments.getNamedArgument("function"), arguments.getNamedArgument("scope"));
}
};
private static boolean parseBooleanArgument(@NotNull ArgumentsHelper arguments, @NotNull String name, boolean defaultValue) {
String value = arguments.findNamedArgument(name);
return value != null ? Boolean.parseBoolean(value) : defaultValue;
}
private static final DirectiveHandler FUNCTIONS_HAVE_SAME_LINES = new DirectiveHandler("CHECK_FUNCTIONS_HAVE_SAME_LINES") {
@Override
@@ -196,7 +198,6 @@ public class DirectiveTestUtils {
FUNCTION_NOT_CALLED,
FUNCTION_CALLED_IN_SCOPE,
FUNCTION_NOT_CALLED_IN_SCOPE,
METHOD_NOT_CALLED_IN_SCOPE,
FUNCTIONS_HAVE_SAME_LINES,
COUNT_LABELS,
COUNT_VARS,
@@ -234,50 +235,38 @@ public class DirectiveTestUtils {
public static void checkCalledInScope(
@NotNull JsNode node,
@NotNull String functionName,
@NotNull String scopeFunctionName
@NotNull String scopeFunctionName,
boolean checkQualifier
) throws Exception {
String errorMessage = functionName + " is not called inside " + scopeFunctionName;
assertFalse(errorMessage, isCalledInScope(node, functionName, scopeFunctionName));
assertFalse(errorMessage, isCalledInScope(node, functionName, scopeFunctionName, checkQualifier));
}
public static void checkNotCalledInScope(
@NotNull JsNode node,
@NotNull String functionName,
@NotNull String scopeFunctionName
@NotNull String scopeFunctionName,
boolean checkQualifier
) throws Exception {
String errorMessage = functionName + " is called inside " + scopeFunctionName;
assertTrue(errorMessage, isCalledInScope(node, functionName, scopeFunctionName));
}
private static void checkMethodNotCalledInScope(
@NotNull JsNode node,
@NotNull String functionName,
@NotNull String scopeFunctionName
) throws Exception {
String errorMessage = functionName + " is called inside " + scopeFunctionName;
assertTrue(errorMessage, isMethodCalledInScope(node, functionName, scopeFunctionName));
assertTrue(errorMessage, isCalledInScope(node, functionName, scopeFunctionName, checkQualifier));
}
private static boolean isCalledInScope(
@NotNull JsNode node,
@NotNull String functionName,
@NotNull String scopeFunctionName
@NotNull String scopeFunctionName,
boolean checkQualifier
) throws Exception {
JsNode scope = AstSearchUtil.getFunction(node, scopeFunctionName);
CallCounter counter = CallCounter.countCalls(scope);
return counter.getQualifiedCallsCount(functionName) == 0;
}
private static boolean isMethodCalledInScope(
@NotNull JsNode node,
@NotNull String functionName,
@NotNull String scopeFunctionName
) throws Exception {
JsNode scope = AstSearchUtil.getFunction(node, scopeFunctionName);
CallCounter counter = CallCounter.countCalls(scope);
return counter.getUnqualifiedCallsCount(functionName) == 0;
if (checkQualifier) {
return counter.getQualifiedCallsCount(functionName) == 0;
}
else {
return counter.getUnqualifiedCallsCount(functionName) == 0;
}
}
private abstract static class DirectiveHandler {
@@ -34,6 +34,8 @@ import org.jetbrains.kotlin.name.FqNameUnsafe;
import org.jetbrains.kotlin.resolve.DescriptorUtils;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static com.google.dart.compiler.backend.js.ast.JsScopesKt.JsObjectScope;
import static org.jetbrains.kotlin.js.translate.utils.JsAstUtils.fqnWithoutSideEffects;
@@ -403,9 +405,14 @@ public final class Namer {
return invokeFunctionAndSetTypeCheckMetadata("orNull", callable, TypeCheck.OR_NULL);
}
@NotNull
public JsExpression andPredicate(@NotNull JsExpression a, @NotNull JsExpression b) {
return invokeFunctionAndSetTypeCheckMetadata("andPredicate", Arrays.asList(a, b), TypeCheck.AND_PREDICATE);
}
@NotNull
public JsExpression isAny() {
return invokeFunctionAndSetTypeCheckMetadata("isAny", null, TypeCheck.IS_ANY);
return invokeFunctionAndSetTypeCheckMetadata("isAny", Collections.<JsExpression>emptyList(), TypeCheck.IS_ANY);
}
@NotNull
@@ -423,13 +430,19 @@ public final class Namer {
@NotNull String functionName,
@Nullable JsExpression argument,
@NotNull TypeCheck metadata
) {
List<JsExpression> arguments = argument != null ? Collections.singletonList(argument) : Collections.<JsExpression>emptyList();
return invokeFunctionAndSetTypeCheckMetadata(functionName, arguments, metadata);
}
@NotNull
private JsExpression invokeFunctionAndSetTypeCheckMetadata(
@NotNull String functionName,
@NotNull List<JsExpression> arguments,
@NotNull TypeCheck metadata
) {
JsInvocation invocation = new JsInvocation(kotlin(functionName));
if (argument != null) {
invocation.getArguments().add(argument);
}
invocation.getArguments().addAll(arguments);
MetadataProperties.setTypeCheck(invocation, metadata);
MetadataProperties.setSideEffects(invocation, false);
return invocation;
@@ -41,7 +41,6 @@ import org.jetbrains.kotlin.psi.KtTypeReference;
import org.jetbrains.kotlin.resolve.DescriptorUtils;
import org.jetbrains.kotlin.types.DynamicTypesKt;
import org.jetbrains.kotlin.types.KotlinType;
import org.jetbrains.kotlin.types.TypeIntersector;
import org.jetbrains.kotlin.types.typeUtil.TypeUtilsKt;
import static org.jetbrains.kotlin.builtins.FunctionTypesKt.isFunctionTypeOrSubtype;
@@ -157,7 +156,13 @@ public final class PatternTranslator extends AbstractTranslator {
return getIsTypeCheckCallableForReifiedType(typeParameterDescriptor);
}
return doGetIsTypeCheckCallable(TypeIntersector.getUpperBoundsAsType(typeParameterDescriptor));
JsExpression result = null;
for (KotlinType upperBound : typeParameterDescriptor.getUpperBounds()) {
JsExpression next = doGetIsTypeCheckCallable(upperBound);
result = result != null ? namer().andPredicate(result, next) : next;
}
assert result != null : "KotlinType is expected to return at least one upper bound: " + type;
return result;
}
JsNameRef typeName = getClassNameReference(type);
@@ -35,7 +35,7 @@ private class TypeCheckRewritingVisitor(private val context: TranslationContext)
override fun visit(x: JsFunction, ctx: JsContext<*>): Boolean {
scopes.push(x.scope)
localVars.push(IdentitySet())
localVars.push(IdentitySet<JsName>().apply { this += x.parameters.map { it.name } })
return super.visit(x, ctx)
}
@@ -50,19 +50,14 @@ private class TypeCheckRewritingVisitor(private val context: TranslationContext)
super.endVisit(x, ctx)
}
override fun endVisit(x: JsConditional, ctx: JsContext<JsNode>) {
val test = x.testExpression
}
override fun visit(x: JsInvocation, ctx: JsContext<JsNode>): Boolean {
// callee(calleeArgument)(argument)
val callee = x.qualifier as? JsInvocation
val calleeArgument = callee?.arguments?.firstOrNull()
val calleeArguments = callee?.arguments
val argument = x.arguments.firstOrNull()
if (callee != null && argument != null) {
val replacement = getReplacement(callee, calleeArgument, argument)
if (callee != null && argument != null && calleeArguments != null) {
val replacement = getReplacement(callee, calleeArguments, argument)
if (replacement != null) {
ctx.replaceMe(accept(replacement))
@@ -73,64 +68,100 @@ private class TypeCheckRewritingVisitor(private val context: TranslationContext)
return true
}
private fun getReplacement(callee: JsInvocation, calleeArgument: JsExpression?, argument: JsExpression): JsExpression? {
if (calleeArgument == null) {
// `Kotlin.isAny()(argument)` -> `argument != null`
if (callee.typeCheck == TypeCheck.IS_ANY) {
return TranslationUtils.isNotNullCheck(argument)
private fun getReplacement(callee: JsInvocation, calleeArguments: List<JsExpression>, argument: JsExpression): JsExpression? {
val typeCheck = callee.typeCheck
return when (typeCheck) {
TypeCheck.IS_ANY -> {
// `Kotlin.isAny()(argument)` -> `argument != null`
if (calleeArguments.isEmpty()) TranslationUtils.isNotNullCheck(argument) else null
}
return null
}
// `Kotlin.isTypeOf(calleeArgument)(argument)` -> `typeOf argument === calleeArgument`
if (callee.typeCheck == TypeCheck.TYPEOF) {
return typeOfIs(argument, calleeArgument as JsStringLiteral)
}
// `Kotlin.isInstanceOf(calleeArgument)(argument)` -> `argument instanceof calleeArgument`
if (callee.typeCheck == TypeCheck.INSTANCEOF) {
return context.namer().isInstanceOf(argument, calleeArgument)
}
// `Kotlin.orNull(calleeArgument)(argument)` -> `(tmp = argument) == null || calleeArgument(tmp)`
if (callee.typeCheck == TypeCheck.OR_NULL) {
if (calleeArgument is JsInvocation && calleeArgument.typeCheck == TypeCheck.OR_NULL) {
return JsInvocation(calleeArgument, argument)
TypeCheck.TYPEOF -> {
// `Kotlin.isTypeOf(calleeArgument)(argument)` -> `typeOf argument === calleeArgument`
if (calleeArguments.size == 1) typeOfIs(argument, calleeArguments[0] as JsStringLiteral) else null
}
var nullCheckTarget = argument
var nextCheckTarget = argument
if (argument.isAssignmentToLocalVar) {
// `Kotlin.orNull(Kotlin.isInstance(SomeType))(localVar=someExpr)` -> `(localVar=someExpr) != null || Kotlin.isInstance(SomeType)(localVar)`
val localVar = (argument as JsBinaryOperation).getArg1()
nextCheckTarget = localVar
}
else if (!argument.isLocalVar) {
val currentScope = scopes.peek()
val tmp = currentScope.declareTemporary()
val statementContext = lastStatementLevelContext
statementContext.addPrevious(newVar(tmp, null))
nullCheckTarget = assignment(tmp.makeRef(), argument)
nextCheckTarget = tmp.makeRef()
TypeCheck.INSTANCEOF -> {
// `Kotlin.isInstanceOf(calleeArgument)(argument)` -> `argument instanceof calleeArgument`
if (calleeArguments.size == 1) context.namer().isInstanceOf(argument, calleeArguments[0]) else null
}
val isNull = TranslationUtils.isNullCheck(nullCheckTarget)
return or(isNull, JsInvocation(calleeArgument, nextCheckTarget))
TypeCheck.OR_NULL -> {
// `Kotlin.orNull(calleeArgument)(argument)` -> `(tmp = argument) == null || calleeArgument(tmp)`
if (calleeArguments.size == 1) getReplacementForOrNull(argument, calleeArguments[0]) else null
}
TypeCheck.AND_PREDICATE -> {
// `Kotlin.andPredicate(p1, p2)(argument)` -> `p1(tmp = argument) && p2(tmp)`
if (calleeArguments.size == 2) {
getReplacementForAndPredicate(argument, calleeArguments[0], calleeArguments[1])
}
else {
null
}
}
null -> null
}
return null
}
private val JsExpression.isLocalVar: Boolean
get() {
if (localVars.empty() || this !is JsNameRef) return false
val name = this.getName()
return name != null && localVars.peek().contains(name)
private fun getReplacementForOrNull(argument: JsExpression, calleeArgument: JsExpression): JsExpression {
if (calleeArgument is JsInvocation && calleeArgument.typeCheck == TypeCheck.OR_NULL) {
return JsInvocation(calleeArgument, argument)
}
var nullCheckTarget = argument
var nextCheckTarget = argument
if (argument.isAssignmentToLocalVar) {
// `Kotlin.orNull(Kotlin.isInstance(SomeType))(localVar=someExpr)` ->
// `(localVar=someExpr) != null || Kotlin.isInstance(SomeType)(localVar)`
val localVar = (argument as JsBinaryOperation).arg1
nextCheckTarget = localVar
}
else if (argument.needsAlias) {
val currentScope = scopes.peek()
val tmp = currentScope.declareTemporary()
val statementContext = lastStatementLevelContext
statementContext.addPrevious(newVar(tmp, null))
nullCheckTarget = assignment(tmp.makeRef(), argument)
nextCheckTarget = tmp.makeRef()
}
val isNull = TranslationUtils.isNullCheck(nullCheckTarget)
return or(isNull, JsInvocation(calleeArgument, nextCheckTarget))
}
private fun getReplacementForAndPredicate(argument: JsExpression, p1: JsExpression, p2: JsExpression): JsExpression {
val (arg1, arg2) = if (!argument.needsAlias) {
Pair(argument, argument)
}
else if (argument.isAssignmentToLocalVar) {
Pair(argument, JsAstUtils.decomposeAssignment(argument)!!.first)
}
else {
val currentScope = scopes.peek()
val tmp = currentScope.declareTemporary()
val statementContext = lastStatementLevelContext
statementContext.addPrevious(newVar(tmp, null))
Pair(assignment(tmp.makeRef(), argument), tmp.makeRef())
}
val first = accept(JsInvocation(p1, arg1) as JsExpression)
val second = accept(JsInvocation(p2, arg2) as JsExpression)
return JsAstUtils.and(first, second)
}
private val JsExpression.needsAlias: Boolean
get() = when (this) {
is JsStringLiteral -> false
else -> !isLocalVar
}
private val JsExpression.isLocalVar: Boolean
get() = localVars.isNotEmpty() && this is JsNameRef && name.let { it != null && it in localVars.peek() }
private val JsExpression.isAssignmentToLocalVar: Boolean
get() = this is JsBinaryOperation && getOperator() == JsBinaryOperator.ASG
get() = localVars.isNotEmpty() &&
JsAstUtils.decomposeAssignmentToVariable(this).let { it != null && it.first in localVars.peek() }
}
@@ -0,0 +1,59 @@
package foo
open class A()
interface X
interface Y
class B() : A(), X, Y {
override fun toString() = "B"
}
class C() : A(), X, Y {
override fun toString() = "C"
}
class D() : A() {
override fun toString() = "D"
}
class E() : X {
override fun toString() = "E"
}
class F() : A(), Y {
override fun toString() = "E"
}
fun <T> test(a: Any): String where T : A, T : X, T : Y {
return (try {
a as T
}
catch (e: Exception) {
"error"
}).toString()
}
fun box(): String {
val b = B()
val c = C()
val d = D()
val e = E()
val f = F()
assertEquals("B", test<B>(b))
assertEquals("B", test<C>(b))
assertEquals("C", test<B>(c))
assertEquals("C", test<C>(c))
assertEquals("error", test<B>(d))
assertEquals("error", test<C>(d))
assertEquals("error", test<B>(e))
assertEquals("error", test<C>(e))
assertEquals("error", test<B>(f))
assertEquals("error", test<C>(f))
return "OK"
}
@@ -1,6 +1,6 @@
package foo
// CHECK_METHOD_NOT_CALLED_IN_SCOPE: scope=box function=isType
// CHECK_NOT_CALLED_IN_SCOPE: scope=box function=isType
open class A()
+6
View File
@@ -560,6 +560,12 @@ var Kotlin = {};
return object != null;
}
};
Kotlin.andPredicate = function (a, b) {
return function (object) {
return a(object) && b(object);
}
};
Kotlin.kotlinModuleMetadata = function (abiVersion, moduleName, data) {
};
+1 -1
View File
@@ -1,7 +1,7 @@
package foo
// CHECK_NOT_CALLED: isTypeOfOrNull
// CHECK_NULLS_COUNT: function=box count=8
// CHECK_NULLS_COUNT: function=box count=10
inline
fun <reified T> Any?.isTypeOfOrNull() = this is T?