From 8149189585ae0957c76387883c65b9197d3b0934 Mon Sep 17 00:00:00 2001 From: Sergej Jaskiewicz Date: Mon, 20 Dec 2021 18:33:47 +0300 Subject: [PATCH] [JS IR] Optimize away upcasts #KT-50212 Fixed --- .../backend/js/lower/TypeOperatorLowering.kt | 47 +++++- .../codegen/boxError/semantic/missedBody.kt | 6 +- .../kotlin/js/test/BoxJsTestGenerated.java | 6 + .../js/test/ir/IrBoxJsTestGenerated.java | 6 + .../box/expression/cast/redundantCast.kt | 150 ++++++++++++++++++ .../testData/box/reified/isTNullable.kt | 3 +- .../testData/box/standardClasses/any.kt | 9 +- 7 files changed, 217 insertions(+), 10 deletions(-) create mode 100644 js/js.translator/testData/box/expression/cast/redundantCast.kt diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/TypeOperatorLowering.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/TypeOperatorLowering.kt index 3bdfbb60d11..fbf362e6fcc 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/TypeOperatorLowering.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/TypeOperatorLowering.kt @@ -9,6 +9,7 @@ import org.jetbrains.kotlin.backend.common.BodyLoweringPass import org.jetbrains.kotlin.backend.common.compilationException import org.jetbrains.kotlin.backend.common.ir.isPure import org.jetbrains.kotlin.ir.IrStatement +import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET import org.jetbrains.kotlin.ir.backend.js.JsIrBackendContext import org.jetbrains.kotlin.ir.backend.js.ir.JsIrArithBuilder import org.jetbrains.kotlin.ir.backend.js.ir.JsIrBuilder @@ -18,6 +19,7 @@ import org.jetbrains.kotlin.ir.declarations.IrDeclarationBase import org.jetbrains.kotlin.ir.declarations.IrDeclarationParent import org.jetbrains.kotlin.ir.expressions.* import org.jetbrains.kotlin.ir.expressions.impl.IrCompositeImpl +import org.jetbrains.kotlin.ir.expressions.impl.IrConstImpl import org.jetbrains.kotlin.ir.expressions.impl.IrTypeOperatorCallImpl import org.jetbrains.kotlin.ir.symbols.IrClassSymbol import org.jetbrains.kotlin.ir.symbols.IrTypeParameterSymbol @@ -211,12 +213,53 @@ class TypeOperatorLowering(val context: JsIrBackendContext) : BodyLoweringPass { } } + /** + * The general logic for generating a runtime type check is as follows: + * ``` + * ┌─────────────────────────────┬───────────────────────────────┐ + * │ to non-null │ to nullable │ + * ┌─────────────╋━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ + * │ ┃ instanceof check │ + * │from non-null┃ OR │ + * │ ┃ advanced check │ + * ├─────────────╋─────────────────────────────┬───────────────────────────────┤ + * │ ┃ instanceof check │ null check + instanceof check │ + * │from nullable┃ OR │ OR │ + * │ ┃ null check + advanced check │ null check + advanced check │ + * └─────────────┻─────────────────────────────┴───────────────────────────────┘ + * ``` + * Note: advanced check is performed when casting to primitive types, array types, function types, or interfaces. + * + * If we statically know that this is an upcast, we try to generate no checks. + * In this case, the following logic is applied: + * ``` + * ┌─────────────┬─────────────┐ + * │ to non-null │ to nullable │ + * ┌─────────────╋━━━━━━━━━━━━━┻━━━━━━━━━━━━━┫ + * │from non-null┃ true │ + * ├─────────────╋─────────────┬─────────────┤ + * │from nullable┃ null check │ true │ + * └─────────────┻─────────────┴─────────────┘ + * ``` + */ private fun generateTypeCheck(argument: () -> IrExpression, toType: IrType): IrExpression { val toNotNullable = toType.makeNotNull() val argumentInstance = argument() - val instanceCheck = generateTypeCheckNonNull(argumentInstance, toNotNullable) - val isFromNullable = argumentInstance.type.isNullable() + val fromType = argumentInstance.type + val fromNotNullable = fromType.makeNotNull() + + val isFromNullable = fromType.isNullable() val isToNullable = toType.isNullable() + + if (fromNotNullable !is IrDynamicType && fromNotNullable.isSubtypeOf(toNotNullable, context.typeSystem)) { + // This is an upcast + return when { + isFromNullable && !isToNullable -> calculator.run { not(nullCheck(argument())) } + else -> IrConstImpl.constTrue(UNDEFINED_OFFSET, UNDEFINED_OFFSET, context.irBuiltIns.booleanType) + } + } + + val instanceCheck = generateTypeCheckNonNull(argumentInstance, toNotNullable) val isNativeCheck = !advancedCheckRequired(toNotNullable) return when { diff --git a/compiler/testData/codegen/boxError/semantic/missedBody.kt b/compiler/testData/codegen/boxError/semantic/missedBody.kt index 1572e7dac24..17aa09f87fe 100644 --- a/compiler/testData/codegen/boxError/semantic/missedBody.kt +++ b/compiler/testData/codegen/boxError/semantic/missedBody.kt @@ -4,9 +4,9 @@ // MODULE: lib // FILE: t.kt -fun bar(a: String, b: String): String +fun bar(a: String, b: String): Any -fun foo(): String { +fun foo(): Any { return bar("O", "K") } @@ -17,4 +17,4 @@ fun box(): String { val r = foo() if (r is String) return r return "OK" -} \ No newline at end of file +} diff --git a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/BoxJsTestGenerated.java b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/BoxJsTestGenerated.java index 8a81d4a5cf8..e63ff05d20d 100644 --- a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/BoxJsTestGenerated.java +++ b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/BoxJsTestGenerated.java @@ -2281,6 +2281,12 @@ public class BoxJsTestGenerated extends AbstractBoxJsTest { runTest("js/js.translator/testData/box/expression/cast/primitiveToClass.kt"); } + @Test + @TestMetadata("redundantCast.kt") + public void testRedundantCast() throws Exception { + runTest("js/js.translator/testData/box/expression/cast/redundantCast.kt"); + } + @Test @TestMetadata("reifiedToNotNull.kt") public void testReifiedToNotNull() throws Exception { diff --git a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrBoxJsTestGenerated.java b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrBoxJsTestGenerated.java index 286d6a5dab9..4bdfa25e0f7 100644 --- a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrBoxJsTestGenerated.java +++ b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrBoxJsTestGenerated.java @@ -2671,6 +2671,12 @@ public class IrBoxJsTestGenerated extends AbstractIrBoxJsTest { runTest("js/js.translator/testData/box/expression/cast/primitiveToClass.kt"); } + @Test + @TestMetadata("redundantCast.kt") + public void testRedundantCast() throws Exception { + runTest("js/js.translator/testData/box/expression/cast/redundantCast.kt"); + } + @Test @TestMetadata("reifiedToNotNull.kt") public void testReifiedToNotNull() throws Exception { diff --git a/js/js.translator/testData/box/expression/cast/redundantCast.kt b/js/js.translator/testData/box/expression/cast/redundantCast.kt new file mode 100644 index 00000000000..9de4a3aed1a --- /dev/null +++ b/js/js.translator/testData/box/expression/cast/redundantCast.kt @@ -0,0 +1,150 @@ +// EXPECTED_REACHABLE_NODES: 1306 + +// Test that upcasts are optimized away + +open class Base + +class Derived: Base() + +var counter = 0 +var _derived = Derived() + +@JsExport +fun getDerived(): Derived { + counter += 1 + return _derived +} + +@JsExport +fun getDerivedNullable(): Derived? { + counter += 1 + return _derived +} + +@JsExport +fun getDerivedNull(): Derived? { + counter += 1 + return null +} + +// CHECK_CONTAINS_NO_CALLS: upcast1 except=getDerived IGNORED_BACKENDS=JS +// CHECK_TERNARY_OPERATOR_COUNT: function=upcast1 count=0 IGNORED_BACKENDS=JS +// CHECK_IF_COUNT: function=upcast1 count=0 IGNORED_BACKENDS=JS +// CHECK_BINOP_COUNT: function=upcast1 count=0 symbol=instanceof IGNORED_BACKENDS=JS +fun upcast1() = getDerived() as Base + +// CHECK_CONTAINS_NO_CALLS: upcast2 except=getDerived IGNORED_BACKENDS=JS +// CHECK_TERNARY_OPERATOR_COUNT: function=upcast2 count=0 IGNORED_BACKENDS=JS +// CHECK_IF_COUNT: function=upcast2 count=0 IGNORED_BACKENDS=JS +// CHECK_BINOP_COUNT: function=upcast2 count=0 symbol=instanceof IGNORED_BACKENDS=JS +fun upcast2() = getDerived() as Base? + +// CHECK_BINOP_COUNT: function=upcast3 count=0 symbol=instanceof IGNORED_BACKENDS=JS +fun upcast3() = getDerivedNullable() as Base + +// CHECK_CONTAINS_NO_CALLS: upcast4 except=getDerivedNullable IGNORED_BACKENDS=JS +// CHECK_TERNARY_OPERATOR_COUNT: function=upcast4 count=0 IGNORED_BACKENDS=JS +// CHECK_IF_COUNT: function=upcast4 count=0 IGNORED_BACKENDS=JS +// CHECK_BINOP_COUNT: function=upcast1 count=0 symbol=instanceof IGNORED_BACKENDS=JS +fun upcast4() = getDerivedNullable() as Base? + +// CHECK_BINOP_COUNT: function=upcast5 count=0 symbol=instanceof IGNORED_BACKENDS=JS +fun upcast5() = getDerivedNull() as Base + +// CHECK_CONTAINS_NO_CALLS: upcast6 except=getDerivedNull IGNORED_BACKENDS=JS +// CHECK_TERNARY_OPERATOR_COUNT: function=upcast6 count=0 IGNORED_BACKENDS=JS +// CHECK_IF_COUNT: function=upcast6 count=0 IGNORED_BACKENDS=JS +// CHECK_BINOP_COUNT: function=upcast6 count=0 symbol=instanceof IGNORED_BACKENDS=JS +fun upcast6() = getDerivedNull() as Base? + +// CHECK_CONTAINS_NO_CALLS: safeCast1 except=getDerived IGNORED_BACKENDS=JS +// CHECK_TERNARY_OPERATOR_COUNT: function=safeCast1 count=0 IGNORED_BACKENDS=JS +// CHECK_IF_COUNT: function=safeCast1 count=0 IGNORED_BACKENDS=JS +// CHECK_BINOP_COUNT: function=safeCast1 count=0 symbol=instanceof IGNORED_BACKENDS=JS +fun safeCast1() = getDerived() as? Base + +// CHECK_CONTAINS_NO_CALLS: safeCast2 except=getDerived IGNORED_BACKENDS=JS +// CHECK_TERNARY_OPERATOR_COUNT: function=safeCast1 count=0 IGNORED_BACKENDS=JS +// CHECK_IF_COUNT: function=safeCast1 count=0 IGNORED_BACKENDS=JS +// CHECK_BINOP_COUNT: function=safeCast2 count=0 symbol=instanceof IGNORED_BACKENDS=JS +fun safeCast2() = getDerived() as? Base? + +// CHECK_BINOP_COUNT: function=safeCast3 count=0 symbol=instanceof IGNORED_BACKENDS=JS +fun safeCast3() = getDerivedNullable() as? Base + +// CHECK_CONTAINS_NO_CALLS: safeCast4 except=getDerivedNullable IGNORED_BACKENDS=JS +// CHECK_TERNARY_OPERATOR_COUNT: function=safeCast4 count=0 IGNORED_BACKENDS=JS +// CHECK_IF_COUNT: function=safeCast4 count=0 IGNORED_BACKENDS=JS +// CHECK_BINOP_COUNT: function=safeCast4 count=0 symbol=instanceof IGNORED_BACKENDS=JS +fun safeCast4() = getDerivedNullable() as? Base? + +// CHECK_BINOP_COUNT: function=safeCast5 count=0 symbol=instanceof IGNORED_BACKENDS=JS +fun safeCast5() = getDerivedNull() as? Base + +// CHECK_CONTAINS_NO_CALLS: safeCast6 except=getDerivedNull IGNORED_BACKENDS=JS +// CHECK_TERNARY_OPERATOR_COUNT: function=safeCast6 count=0 IGNORED_BACKENDS=JS +// CHECK_IF_COUNT: function=safeCast6 count=0 IGNORED_BACKENDS=JS +// CHECK_BINOP_COUNT: function=safeCast6 count=0 symbol=instanceof IGNORED_BACKENDS=JS +fun safeCast6() = getDerivedNull() as? Base? + +// CHECK_CONTAINS_NO_CALLS: upcast1 except=getDerived IGNORED_BACKENDS=JS +// CHECK_TERNARY_OPERATOR_COUNT: function=upcast1 count=0 IGNORED_BACKENDS=JS +// CHECK_IF_COUNT: function=upcast1 count=0 IGNORED_BACKENDS=JS +// CHECK_BINOP_COUNT: function=instanceCheck1 count=0 symbol=instanceof IGNORED_BACKENDS=JS +fun instanceCheck1() = getDerived() is Base + +// CHECK_CONTAINS_NO_CALLS: upcast2 except=getDerived IGNORED_BACKENDS=JS +// CHECK_TERNARY_OPERATOR_COUNT: function=upcast2 count=0 IGNORED_BACKENDS=JS +// CHECK_IF_COUNT: function=upcast2 count=0 IGNORED_BACKENDS=JS +// CHECK_BINOP_COUNT: function=instanceCheck2 count=0 symbol=instanceof IGNORED_BACKENDS=JS +fun instanceCheck2() = getDerived() is Base? + +// CHECK_BINOP_COUNT: function=instanceCheck3 count=0 symbol=instanceof IGNORED_BACKENDS=JS +fun instanceCheck3() = getDerivedNullable() is Base + +// CHECK_CONTAINS_NO_CALLS: upcast4 except=getDerivedNullable IGNORED_BACKENDS=JS +// CHECK_TERNARY_OPERATOR_COUNT: function=upcast4 count=0 IGNORED_BACKENDS=JS +// CHECK_IF_COUNT: function=upcast4 count=0 IGNORED_BACKENDS=JS +// CHECK_BINOP_COUNT: function=instanceCheck4 count=0 symbol=instanceof IGNORED_BACKENDS=JS +fun instanceCheck4() = getDerivedNullable() is Base? + +// CHECK_BINOP_COUNT: function=instanceCheck5 count=0 symbol=instanceof IGNORED_BACKENDS=JS +fun instanceCheck5() = getDerivedNull() is Base + +// CHECK_CONTAINS_NO_CALLS: upcast6 except=getDerivedNull IGNORED_BACKENDS=JS +// CHECK_TERNARY_OPERATOR_COUNT: function=upcast6 count=0 IGNORED_BACKENDS=JS +// CHECK_IF_COUNT: function=upcast6 count=0 IGNORED_BACKENDS=JS +// CHECK_BINOP_COUNT: function=instanceCheck6 count=0 symbol=instanceof IGNORED_BACKENDS=JS +fun instanceCheck6() = getDerivedNull() is Base? + +fun box(): String { + assertSame(_derived, upcast1(), "upcast1()") + assertSame(_derived, upcast2(), "upcast2()") + assertSame(_derived, upcast3(), "upcast3()") + assertSame(_derived, upcast4(), "upcast4()") + failsClassCast("upcast5()") { upcast5() } + assertSame(null, upcast6(), "upcast6()") + assertEquals(6, counter) + + counter = 0 + + assertSame(_derived, safeCast1(), "safeCast1()") + assertSame(_derived, safeCast2(), "safeCast2()") + assertSame(_derived, safeCast3(), "safeCast3()") + assertSame(_derived, safeCast4(), "safeCast4()") + assertSame(null, safeCast5(), "safeCast5()") + assertSame(null, safeCast6(), "safeCast6()") + assertEquals(6, counter) + + counter = 0 + + assertTrue(instanceCheck1(), "instanceCheck1()") + assertTrue(instanceCheck2(), "instanceCheck2()") + assertTrue(instanceCheck3(), "instanceCheck3()") + assertTrue(instanceCheck4(), "instanceCheck4()") + assertFalse(instanceCheck5(), "instanceCheck5()") + assertTrue(instanceCheck6(), "instanceCheck6()") + assertEquals(6, counter) + + return "OK" +} diff --git a/js/js.translator/testData/box/reified/isTNullable.kt b/js/js.translator/testData/box/reified/isTNullable.kt index b3532f33f6d..34d5b6426dd 100644 --- a/js/js.translator/testData/box/reified/isTNullable.kt +++ b/js/js.translator/testData/box/reified/isTNullable.kt @@ -3,7 +3,8 @@ package foo // CHECK_NOT_CALLED: isTypeOfOrNull // CHECK_NULLS_COUNT: function=box count=10 TARGET_BACKENDS=JS -// CHECK_NULLS_COUNT: function=box count=6 IGNORED_BACKENDS=JS +// CHECK_NULLS_COUNT: function=box count=0 IGNORED_BACKENDS=JS +// CHECK_CONTAINS_NO_CALLS: box except=assertEquals;A IGNORED_BACKENDS=JS inline fun Any?.isTypeOfOrNull() = this is T? diff --git a/js/js.translator/testData/box/standardClasses/any.kt b/js/js.translator/testData/box/standardClasses/any.kt index 7fb71144b36..fb6fb192a90 100644 --- a/js/js.translator/testData/box/standardClasses/any.kt +++ b/js/js.translator/testData/box/standardClasses/any.kt @@ -1,6 +1,5 @@ // EXPECTED_REACHABLE_NODES: 1285 // CHECK_CALLED_IN_SCOPE: function=isType scope=box TARGET_BACKENDS=JS -// CHECK_CALLED_IN_SCOPE: function=isObject scope=box IGNORED_BACKENDS=JS package foo class A : Any() @@ -16,9 +15,11 @@ fun box(): String { if (arrayOf(1, 2, 3).asAny() !is Any) return "fail3" - if (createNakedObject() is Any) return "fail4" + if (testUtils.isLegacyBackend()) { + if (createNakedObject() is Any) return "fail4" + } - if (({ }).asAny() !is Any) return "fail5" + if (({ }).asAny() !is Any) return "fail5" if ((23).asAny() !is Any) return "fail6" @@ -31,4 +32,4 @@ fun box(): String { return "OK" } -fun createNakedObject(): Any? = js("Object.create(null)") \ No newline at end of file +fun createNakedObject(): Any? = js("Object.create(null)")