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:
Generated
+5
@@ -13977,6 +13977,11 @@ public class FirBlackBoxCodegenTestGenerated extends AbstractFirBlackBoxCodegenT
|
|||||||
runTest("compiler/testData/codegen/box/inlineClasses/toStringCallingPrivateFun.kt");
|
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")
|
@TestMetadata("typeChecksForInlineClasses.kt")
|
||||||
public void testTypeChecksForInlineClasses() throws Exception {
|
public void testTypeChecksForInlineClasses() throws Exception {
|
||||||
runTest("compiler/testData/codegen/box/inlineClasses/typeChecksForInlineClasses.kt");
|
runTest("compiler/testData/codegen/box/inlineClasses/typeChecksForInlineClasses.kt");
|
||||||
|
|||||||
+21
-20
@@ -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.JvmBackendContext
|
||||||
import org.jetbrains.kotlin.backend.jvm.ir.JvmIrBuilder
|
import org.jetbrains.kotlin.backend.jvm.ir.JvmIrBuilder
|
||||||
import org.jetbrains.kotlin.backend.jvm.ir.createJvmIrBuilder
|
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.backend.jvm.lower.inlineclasses.unboxInlineClass
|
||||||
import org.jetbrains.kotlin.ir.builders.*
|
import org.jetbrains.kotlin.ir.builders.*
|
||||||
import org.jetbrains.kotlin.ir.declarations.IrClass
|
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 {
|
private fun JvmIrBuilder.lowerInlineClassArgument(expression: IrExpression): IrExpression? {
|
||||||
if (expression.type.unboxInlineClass() == expression.type)
|
if (InlineClassAbi.unboxType(expression.type) == null)
|
||||||
return expression
|
return null
|
||||||
|
|
||||||
val toStringFunction = expression.type.classOrNull?.owner?.toStringFunction
|
val toStringFunction = expression.type.classOrNull?.owner?.toStringFunction
|
||||||
?: return expression
|
?: return null
|
||||||
|
|
||||||
val toStringReplacement = backendContext.inlineClassReplacements.getReplacementFunction(toStringFunction)
|
val toStringReplacement = backendContext.inlineClassReplacements.getReplacementFunction(toStringFunction)
|
||||||
?: return expression
|
?: return null
|
||||||
|
// `C?` can only be unboxed if it wraps a reference type `T!!`, in which case the unboxed type
|
||||||
return irCall(toStringReplacement).apply {
|
// is `T?`. We can't pass that to `C.toString-impl` without checking for `null`.
|
||||||
putValueArgument(0, expression)
|
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 {
|
override fun visitStringConcatenation(expression: IrStringConcatenation): IrExpression {
|
||||||
@@ -131,22 +136,19 @@ private class JvmStringConcatenationLowering(val context: JvmBackendContext) : F
|
|||||||
else
|
else
|
||||||
this
|
this
|
||||||
|
|
||||||
|
val arguments = expression.arguments
|
||||||
val arguments = expression.arguments.map { lowerInlineClassArgument(it) }
|
|
||||||
|
|
||||||
when {
|
when {
|
||||||
arguments.isEmpty() ->
|
arguments.isEmpty() ->
|
||||||
irString("")
|
irString("")
|
||||||
|
|
||||||
arguments.size == 1 ->
|
arguments.size == 1 ->
|
||||||
callToString(arguments.single().unwrapImplicitNotNull())
|
lowerInlineClassArgument(arguments[0]) ?: callToString(arguments[0].unwrapImplicitNotNull())
|
||||||
|
|
||||||
arguments.size == 2 && arguments[0].type.isStringClassType() ->
|
arguments.size == 2 && arguments[0].type.isStringClassType() ->
|
||||||
irCall(backendContext.ir.symbols.intrinsicStringPlus).apply {
|
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?`)
|
// 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 -> {
|
else -> {
|
||||||
@@ -156,10 +158,9 @@ private class JvmStringConcatenationLowering(val context: JvmBackendContext) : F
|
|||||||
val appendFunction = typeToAppendFunction(argument.type)
|
val appendFunction = typeToAppendFunction(argument.type)
|
||||||
stringBuilder = irCall(appendFunction).apply {
|
stringBuilder = irCall(appendFunction).apply {
|
||||||
dispatchReceiver = stringBuilder
|
dispatchReceiver = stringBuilder
|
||||||
|
|
||||||
// Unwrapping IMPLICIT_NOTNULL is necessary for ALL arguments. There could be a call to `String.plus(Any?)`
|
// 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)`.
|
// 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 {
|
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"
|
||||||
|
}
|
||||||
-13
@@ -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;
|
|
||||||
+15
@@ -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
|
||||||
Vendored
+17
@@ -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
|
||||||
+5
@@ -15372,6 +15372,11 @@ public class BlackBoxCodegenTestGenerated extends AbstractBlackBoxCodegenTest {
|
|||||||
runTest("compiler/testData/codegen/box/inlineClasses/toStringCallingPrivateFun.kt");
|
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")
|
@TestMetadata("typeChecksForInlineClasses.kt")
|
||||||
public void testTypeChecksForInlineClasses() throws Exception {
|
public void testTypeChecksForInlineClasses() throws Exception {
|
||||||
runTest("compiler/testData/codegen/box/inlineClasses/typeChecksForInlineClasses.kt");
|
runTest("compiler/testData/codegen/box/inlineClasses/typeChecksForInlineClasses.kt");
|
||||||
|
|||||||
+8
-3
@@ -3192,9 +3192,14 @@ public class BytecodeTextTestGenerated extends AbstractBytecodeTextTest {
|
|||||||
runTest("compiler/testData/codegen/bytecodeText/inlineClasses/suspendFunctionMangling.kt");
|
runTest("compiler/testData/codegen/bytecodeText/inlineClasses/suspendFunctionMangling.kt");
|
||||||
}
|
}
|
||||||
|
|
||||||
@TestMetadata("toStringIsCalledByInlineClass.kt")
|
@TestMetadata("toStringOfInlineClassValue.kt")
|
||||||
public void testToStringIsCalledByInlineClass() throws Exception {
|
public void testToStringOfInlineClassValue() throws Exception {
|
||||||
runTest("compiler/testData/codegen/bytecodeText/inlineClasses/toStringIsCalledByInlineClass.kt");
|
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")
|
@TestMetadata("uIntArrayIteratorWithoutBoxing.kt")
|
||||||
|
|||||||
+5
@@ -14669,6 +14669,11 @@ public class LightAnalysisModeTestGenerated extends AbstractLightAnalysisModeTes
|
|||||||
runTest("compiler/testData/codegen/box/inlineClasses/simpleSecondaryConstructor.kt");
|
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 {
|
private void runTest(String testDataFilePath) throws Exception {
|
||||||
KotlinTestUtils.runTest(this::doTest, TargetBackend.JVM, testDataFilePath);
|
KotlinTestUtils.runTest(this::doTest, TargetBackend.JVM, testDataFilePath);
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -13977,6 +13977,11 @@ public class IrBlackBoxCodegenTestGenerated extends AbstractIrBlackBoxCodegenTes
|
|||||||
runTest("compiler/testData/codegen/box/inlineClasses/toStringCallingPrivateFun.kt");
|
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")
|
@TestMetadata("typeChecksForInlineClasses.kt")
|
||||||
public void testTypeChecksForInlineClasses() throws Exception {
|
public void testTypeChecksForInlineClasses() throws Exception {
|
||||||
runTest("compiler/testData/codegen/box/inlineClasses/typeChecksForInlineClasses.kt");
|
runTest("compiler/testData/codegen/box/inlineClasses/typeChecksForInlineClasses.kt");
|
||||||
|
|||||||
+8
-3
@@ -3287,9 +3287,14 @@ public class IrBytecodeTextTestGenerated extends AbstractIrBytecodeTextTest {
|
|||||||
runTest("compiler/testData/codegen/bytecodeText/inlineClasses/suspendFunctionMangling.kt");
|
runTest("compiler/testData/codegen/bytecodeText/inlineClasses/suspendFunctionMangling.kt");
|
||||||
}
|
}
|
||||||
|
|
||||||
@TestMetadata("toStringIsCalledByInlineClass.kt")
|
@TestMetadata("toStringOfInlineClassValue.kt")
|
||||||
public void testToStringIsCalledByInlineClass() throws Exception {
|
public void testToStringOfInlineClassValue() throws Exception {
|
||||||
runTest("compiler/testData/codegen/bytecodeText/inlineClasses/toStringIsCalledByInlineClass.kt");
|
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")
|
@TestMetadata("uIntArrayIteratorWithoutBoxing.kt")
|
||||||
|
|||||||
Generated
+5
@@ -12017,6 +12017,11 @@ public class IrJsCodegenBoxES6TestGenerated extends AbstractIrJsCodegenBoxES6Tes
|
|||||||
runTest("compiler/testData/codegen/box/inlineClasses/toStringCallingPrivateFun.kt");
|
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")
|
@TestMetadata("typeChecksForInlineClasses.kt")
|
||||||
public void testTypeChecksForInlineClasses() throws Exception {
|
public void testTypeChecksForInlineClasses() throws Exception {
|
||||||
runTest("compiler/testData/codegen/box/inlineClasses/typeChecksForInlineClasses.kt");
|
runTest("compiler/testData/codegen/box/inlineClasses/typeChecksForInlineClasses.kt");
|
||||||
|
|||||||
Generated
+5
@@ -12017,6 +12017,11 @@ public class IrJsCodegenBoxTestGenerated extends AbstractIrJsCodegenBoxTest {
|
|||||||
runTest("compiler/testData/codegen/box/inlineClasses/toStringCallingPrivateFun.kt");
|
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")
|
@TestMetadata("typeChecksForInlineClasses.kt")
|
||||||
public void testTypeChecksForInlineClasses() throws Exception {
|
public void testTypeChecksForInlineClasses() throws Exception {
|
||||||
runTest("compiler/testData/codegen/box/inlineClasses/typeChecksForInlineClasses.kt");
|
runTest("compiler/testData/codegen/box/inlineClasses/typeChecksForInlineClasses.kt");
|
||||||
|
|||||||
+5
@@ -12082,6 +12082,11 @@ public class JsCodegenBoxTestGenerated extends AbstractJsCodegenBoxTest {
|
|||||||
runTest("compiler/testData/codegen/box/inlineClasses/toStringCallingPrivateFun.kt");
|
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")
|
@TestMetadata("typeChecksForInlineClasses.kt")
|
||||||
public void testTypeChecksForInlineClasses() throws Exception {
|
public void testTypeChecksForInlineClasses() throws Exception {
|
||||||
runTest("compiler/testData/codegen/box/inlineClasses/typeChecksForInlineClasses.kt");
|
runTest("compiler/testData/codegen/box/inlineClasses/typeChecksForInlineClasses.kt");
|
||||||
|
|||||||
Reference in New Issue
Block a user