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");
|
||||
}
|
||||
|
||||
@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");
|
||||
|
||||
+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.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"
|
||||
}
|
||||
-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");
|
||||
}
|
||||
|
||||
@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");
|
||||
|
||||
+8
-3
@@ -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")
|
||||
|
||||
+5
@@ -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);
|
||||
}
|
||||
|
||||
+5
@@ -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");
|
||||
|
||||
+8
-3
@@ -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")
|
||||
|
||||
Generated
+5
@@ -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");
|
||||
|
||||
Generated
+5
@@ -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");
|
||||
|
||||
+5
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user