JVM_IR: check for null when converting unboxed inline classes to strings

`C?` can be unboxed into `T?` if if wraps a reference type `T!!`, but in
this case `null` is not a valid value to pass to `toString-impl`.

 #KT-42005 Fixed
This commit is contained in:
pyos
2020-09-24 16:21:39 +02:00
committed by Alexander Udalov
parent d2bdab2ef5
commit bd6ead0467
14 changed files with 125 additions and 39 deletions
@@ -13977,6 +13977,11 @@ public class FirBlackBoxCodegenTestGenerated extends AbstractFirBlackBoxCodegenT
runTest("compiler/testData/codegen/box/inlineClasses/toStringCallingPrivateFun.kt");
}
@TestMetadata("toStringOfUnboxedNullable.kt")
public void testToStringOfUnboxedNullable() throws Exception {
runTest("compiler/testData/codegen/box/inlineClasses/toStringOfUnboxedNullable.kt");
}
@TestMetadata("typeChecksForInlineClasses.kt")
public void testTypeChecksForInlineClasses() throws Exception {
runTest("compiler/testData/codegen/box/inlineClasses/typeChecksForInlineClasses.kt");
@@ -14,6 +14,7 @@ import org.jetbrains.kotlin.backend.common.phaser.makeIrFilePhase
import org.jetbrains.kotlin.backend.jvm.JvmBackendContext
import org.jetbrains.kotlin.backend.jvm.ir.JvmIrBuilder
import org.jetbrains.kotlin.backend.jvm.ir.createJvmIrBuilder
import org.jetbrains.kotlin.backend.jvm.lower.inlineclasses.InlineClassAbi
import org.jetbrains.kotlin.backend.jvm.lower.inlineclasses.unboxInlineClass
import org.jetbrains.kotlin.ir.builders.*
import org.jetbrains.kotlin.ir.declarations.IrClass
@@ -102,19 +103,23 @@ private class JvmStringConcatenationLowering(val context: JvmBackendContext) : F
}
}
private fun JvmIrBuilder.lowerInlineClassArgument(expression: IrExpression): IrExpression {
if (expression.type.unboxInlineClass() == expression.type)
return expression
private fun JvmIrBuilder.lowerInlineClassArgument(expression: IrExpression): IrExpression? {
if (InlineClassAbi.unboxType(expression.type) == null)
return null
val toStringFunction = expression.type.classOrNull?.owner?.toStringFunction
?: return expression
?: return null
val toStringReplacement = backendContext.inlineClassReplacements.getReplacementFunction(toStringFunction)
?: return expression
return irCall(toStringReplacement).apply {
putValueArgument(0, expression)
}
?: return null
// `C?` can only be unboxed if it wraps a reference type `T!!`, in which case the unboxed type
// is `T?`. We can't pass that to `C.toString-impl` without checking for `null`.
return if (expression.type.isNullable())
irLetS(expression) {
irIfNull(context.irBuiltIns.stringType, irGet(it.owner), irString(null.toString()), irCall(toStringReplacement).apply {
putValueArgument(0, irGet(it.owner))
})
}
else
irCall(toStringReplacement).apply { putValueArgument(0, expression) }
}
override fun visitStringConcatenation(expression: IrStringConcatenation): IrExpression {
@@ -131,22 +136,19 @@ private class JvmStringConcatenationLowering(val context: JvmBackendContext) : F
else
this
val arguments = expression.arguments.map { lowerInlineClassArgument(it) }
val arguments = expression.arguments
when {
arguments.isEmpty() ->
irString("")
arguments.size == 1 ->
callToString(arguments.single().unwrapImplicitNotNull())
lowerInlineClassArgument(arguments[0]) ?: callToString(arguments[0].unwrapImplicitNotNull())
arguments.size == 2 && arguments[0].type.isStringClassType() ->
irCall(backendContext.ir.symbols.intrinsicStringPlus).apply {
putValueArgument(0, arguments[0].unwrapImplicitNotNull())
putValueArgument(0, lowerInlineClassArgument(arguments[0]) ?: arguments[0].unwrapImplicitNotNull())
// Unwrapping IMPLICIT_NOTNULL is not strictly necessary on 2nd argument (parameter type is `Any?`)
putValueArgument(1, arguments[1])
putValueArgument(1, lowerInlineClassArgument(arguments[1]) ?: arguments[1])
}
else -> {
@@ -156,10 +158,9 @@ private class JvmStringConcatenationLowering(val context: JvmBackendContext) : F
val appendFunction = typeToAppendFunction(argument.type)
stringBuilder = irCall(appendFunction).apply {
dispatchReceiver = stringBuilder
// Unwrapping IMPLICIT_NOTNULL is necessary for ALL arguments. There could be a call to `String.plus(Any?)`
// anywhere in the flattened IrStringConcatenation expression, e.g., `"foo" + (Java.platformString() + 123)`.
putValueArgument(0, argument.unwrapImplicitNotNull())
putValueArgument(0, lowerInlineClassArgument(argument) ?: argument.unwrapImplicitNotNull())
}
}
irCall(toStringFunction).apply {
@@ -0,0 +1,21 @@
// !LANGUAGE: +InlineClasses
// IGNORE_BACKEND: JVM
// IGNORE_LIGHT_ANALYSIS
inline class IC(val x: String)
fun IC?.foo() = toString() // `IC?` unboxed into `String?`
fun IC?.bar() = "$this"
fun assertEquals(a: String, b: String) {
if (a != b) throw AssertionError("$a != $b")
}
fun box(): String {
assertEquals((null as IC?).foo(), "null")
assertEquals((null as IC?).foo(), (null as IC?).toString())
assertEquals((null as IC?).foo(), (null as IC?).bar())
assertEquals(IC("x").foo(), "IC(x=x)")
assertEquals(IC("x").foo(), IC("x").toString())
assertEquals(IC("x").foo(), IC("x").bar())
return "OK"
}
@@ -1,13 +0,0 @@
// !LANGUAGE: +InlineClasses
// FILE: Z.kt
inline class Z(val x: Int)
// FILE: test.kt
fun testZ(z: Z) = z.toString()
fun testNZ(z: Z?) = z?.toString()
// @TestKt.class:
// 0 INVOKESTATIC Z\$Erased\.toString
// 0 INVOKESTATIC Z\-Erased\.toString
// 2 INVOKESTATIC Z\.toString-impl \(I\)Ljava/lang/String;
@@ -0,0 +1,15 @@
// !LANGUAGE: +InlineClasses
// FILE: Z.kt
inline class Z(val x: Int)
// FILE: test.kt
fun testZ(z: Z) = z.toString()
fun testZT(z: Z) = "$z"
fun testNZ(z: Z?) = z?.toString() // unboxed into Int after the null check
fun testNZA(z: Z?) = z.toString() // calls Any?.toString() on boxed value
fun testNZT(z: Z?) = "$z" // same
// @TestKt.class:
// 3 INVOKESTATIC Z\.toString-impl \(I\)Ljava/lang/String;
// 2 INVOKESTATIC java/lang/String.valueOf
@@ -0,0 +1,17 @@
// !LANGUAGE: +InlineClasses
// Completely incorrect bytecode - see `box/inlineClasses/toStringOfUnboxedNullable.kt`
// IGNORE_BACKEND: JVM
// FILE: Z.kt
inline class Z(val x: Any)
// FILE: test.kt
fun testZ(z: Z) = z.toString()
fun testZT(z: Z) = "$z"
fun testNZ(z: Z?) = z?.toString() // `Z?` is unboxed into `Any?` even before the null check
fun testNZA(z: Z?) = z.toString() // so all of these call toString-impl
fun testNZT(z: Z?) = "$z"
// @TestKt.class:
// 5 INVOKESTATIC Z\.toString-impl \(Ljava/lang/Object;\)Ljava/lang/String;
// 0 INVOKESTATIC java/lang/String.valueOf
@@ -15372,6 +15372,11 @@ public class BlackBoxCodegenTestGenerated extends AbstractBlackBoxCodegenTest {
runTest("compiler/testData/codegen/box/inlineClasses/toStringCallingPrivateFun.kt");
}
@TestMetadata("toStringOfUnboxedNullable.kt")
public void testToStringOfUnboxedNullable() throws Exception {
runTest("compiler/testData/codegen/box/inlineClasses/toStringOfUnboxedNullable.kt");
}
@TestMetadata("typeChecksForInlineClasses.kt")
public void testTypeChecksForInlineClasses() throws Exception {
runTest("compiler/testData/codegen/box/inlineClasses/typeChecksForInlineClasses.kt");
@@ -3192,9 +3192,14 @@ public class BytecodeTextTestGenerated extends AbstractBytecodeTextTest {
runTest("compiler/testData/codegen/bytecodeText/inlineClasses/suspendFunctionMangling.kt");
}
@TestMetadata("toStringIsCalledByInlineClass.kt")
public void testToStringIsCalledByInlineClass() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/inlineClasses/toStringIsCalledByInlineClass.kt");
@TestMetadata("toStringOfInlineClassValue.kt")
public void testToStringOfInlineClassValue() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/inlineClasses/toStringOfInlineClassValue.kt");
}
@TestMetadata("toStringOfReferenceInlineClassValue.kt")
public void testToStringOfReferenceInlineClassValue() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/inlineClasses/toStringOfReferenceInlineClassValue.kt");
}
@TestMetadata("uIntArrayIteratorWithoutBoxing.kt")
@@ -14669,6 +14669,11 @@ public class LightAnalysisModeTestGenerated extends AbstractLightAnalysisModeTes
runTest("compiler/testData/codegen/box/inlineClasses/simpleSecondaryConstructor.kt");
}
@TestMetadata("toStringOfUnboxedNullable.kt")
public void ignoreToStringOfUnboxedNullable() throws Exception {
runTest("compiler/testData/codegen/box/inlineClasses/toStringOfUnboxedNullable.kt");
}
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.JVM, testDataFilePath);
}
@@ -13977,6 +13977,11 @@ public class IrBlackBoxCodegenTestGenerated extends AbstractIrBlackBoxCodegenTes
runTest("compiler/testData/codegen/box/inlineClasses/toStringCallingPrivateFun.kt");
}
@TestMetadata("toStringOfUnboxedNullable.kt")
public void testToStringOfUnboxedNullable() throws Exception {
runTest("compiler/testData/codegen/box/inlineClasses/toStringOfUnboxedNullable.kt");
}
@TestMetadata("typeChecksForInlineClasses.kt")
public void testTypeChecksForInlineClasses() throws Exception {
runTest("compiler/testData/codegen/box/inlineClasses/typeChecksForInlineClasses.kt");
@@ -3287,9 +3287,14 @@ public class IrBytecodeTextTestGenerated extends AbstractIrBytecodeTextTest {
runTest("compiler/testData/codegen/bytecodeText/inlineClasses/suspendFunctionMangling.kt");
}
@TestMetadata("toStringIsCalledByInlineClass.kt")
public void testToStringIsCalledByInlineClass() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/inlineClasses/toStringIsCalledByInlineClass.kt");
@TestMetadata("toStringOfInlineClassValue.kt")
public void testToStringOfInlineClassValue() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/inlineClasses/toStringOfInlineClassValue.kt");
}
@TestMetadata("toStringOfReferenceInlineClassValue.kt")
public void testToStringOfReferenceInlineClassValue() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/inlineClasses/toStringOfReferenceInlineClassValue.kt");
}
@TestMetadata("uIntArrayIteratorWithoutBoxing.kt")
@@ -12017,6 +12017,11 @@ public class IrJsCodegenBoxES6TestGenerated extends AbstractIrJsCodegenBoxES6Tes
runTest("compiler/testData/codegen/box/inlineClasses/toStringCallingPrivateFun.kt");
}
@TestMetadata("toStringOfUnboxedNullable.kt")
public void testToStringOfUnboxedNullable() throws Exception {
runTest("compiler/testData/codegen/box/inlineClasses/toStringOfUnboxedNullable.kt");
}
@TestMetadata("typeChecksForInlineClasses.kt")
public void testTypeChecksForInlineClasses() throws Exception {
runTest("compiler/testData/codegen/box/inlineClasses/typeChecksForInlineClasses.kt");
@@ -12017,6 +12017,11 @@ public class IrJsCodegenBoxTestGenerated extends AbstractIrJsCodegenBoxTest {
runTest("compiler/testData/codegen/box/inlineClasses/toStringCallingPrivateFun.kt");
}
@TestMetadata("toStringOfUnboxedNullable.kt")
public void testToStringOfUnboxedNullable() throws Exception {
runTest("compiler/testData/codegen/box/inlineClasses/toStringOfUnboxedNullable.kt");
}
@TestMetadata("typeChecksForInlineClasses.kt")
public void testTypeChecksForInlineClasses() throws Exception {
runTest("compiler/testData/codegen/box/inlineClasses/typeChecksForInlineClasses.kt");
@@ -12082,6 +12082,11 @@ public class JsCodegenBoxTestGenerated extends AbstractJsCodegenBoxTest {
runTest("compiler/testData/codegen/box/inlineClasses/toStringCallingPrivateFun.kt");
}
@TestMetadata("toStringOfUnboxedNullable.kt")
public void testToStringOfUnboxedNullable() throws Exception {
runTest("compiler/testData/codegen/box/inlineClasses/toStringOfUnboxedNullable.kt");
}
@TestMetadata("typeChecksForInlineClasses.kt")
public void testTypeChecksForInlineClasses() throws Exception {
runTest("compiler/testData/codegen/box/inlineClasses/typeChecksForInlineClasses.kt");