diff --git a/compiler/fir/fir2ir/tests/org/jetbrains/kotlin/codegen/ir/FirBlackBoxCodegenTestGenerated.java b/compiler/fir/fir2ir/tests/org/jetbrains/kotlin/codegen/ir/FirBlackBoxCodegenTestGenerated.java index 7b065747c7a..696cfc1614b 100644 --- a/compiler/fir/fir2ir/tests/org/jetbrains/kotlin/codegen/ir/FirBlackBoxCodegenTestGenerated.java +++ b/compiler/fir/fir2ir/tests/org/jetbrains/kotlin/codegen/ir/FirBlackBoxCodegenTestGenerated.java @@ -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"); diff --git a/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/lower/JvmStringConcatenationLowering.kt b/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/lower/JvmStringConcatenationLowering.kt index cb9f9537d04..4eb41451026 100644 --- a/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/lower/JvmStringConcatenationLowering.kt +++ b/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/lower/JvmStringConcatenationLowering.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 { diff --git a/compiler/testData/codegen/box/inlineClasses/toStringOfUnboxedNullable.kt b/compiler/testData/codegen/box/inlineClasses/toStringOfUnboxedNullable.kt new file mode 100644 index 00000000000..fafb9d1de16 --- /dev/null +++ b/compiler/testData/codegen/box/inlineClasses/toStringOfUnboxedNullable.kt @@ -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" +} diff --git a/compiler/testData/codegen/bytecodeText/inlineClasses/toStringIsCalledByInlineClass.kt b/compiler/testData/codegen/bytecodeText/inlineClasses/toStringIsCalledByInlineClass.kt deleted file mode 100644 index a1d87cc9a16..00000000000 --- a/compiler/testData/codegen/bytecodeText/inlineClasses/toStringIsCalledByInlineClass.kt +++ /dev/null @@ -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; diff --git a/compiler/testData/codegen/bytecodeText/inlineClasses/toStringOfInlineClassValue.kt b/compiler/testData/codegen/bytecodeText/inlineClasses/toStringOfInlineClassValue.kt new file mode 100644 index 00000000000..7798e2ee988 --- /dev/null +++ b/compiler/testData/codegen/bytecodeText/inlineClasses/toStringOfInlineClassValue.kt @@ -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 diff --git a/compiler/testData/codegen/bytecodeText/inlineClasses/toStringOfReferenceInlineClassValue.kt b/compiler/testData/codegen/bytecodeText/inlineClasses/toStringOfReferenceInlineClassValue.kt new file mode 100644 index 00000000000..7dcb0481cff --- /dev/null +++ b/compiler/testData/codegen/bytecodeText/inlineClasses/toStringOfReferenceInlineClassValue.kt @@ -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 diff --git a/compiler/tests/org/jetbrains/kotlin/codegen/BlackBoxCodegenTestGenerated.java b/compiler/tests/org/jetbrains/kotlin/codegen/BlackBoxCodegenTestGenerated.java index e45326de720..987b79477a6 100644 --- a/compiler/tests/org/jetbrains/kotlin/codegen/BlackBoxCodegenTestGenerated.java +++ b/compiler/tests/org/jetbrains/kotlin/codegen/BlackBoxCodegenTestGenerated.java @@ -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"); diff --git a/compiler/tests/org/jetbrains/kotlin/codegen/BytecodeTextTestGenerated.java b/compiler/tests/org/jetbrains/kotlin/codegen/BytecodeTextTestGenerated.java index 3e1313fd452..76b6f1d1834 100644 --- a/compiler/tests/org/jetbrains/kotlin/codegen/BytecodeTextTestGenerated.java +++ b/compiler/tests/org/jetbrains/kotlin/codegen/BytecodeTextTestGenerated.java @@ -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") diff --git a/compiler/tests/org/jetbrains/kotlin/codegen/LightAnalysisModeTestGenerated.java b/compiler/tests/org/jetbrains/kotlin/codegen/LightAnalysisModeTestGenerated.java index d62e4368b2b..9e29ccceda2 100644 --- a/compiler/tests/org/jetbrains/kotlin/codegen/LightAnalysisModeTestGenerated.java +++ b/compiler/tests/org/jetbrains/kotlin/codegen/LightAnalysisModeTestGenerated.java @@ -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); } diff --git a/compiler/tests/org/jetbrains/kotlin/codegen/ir/IrBlackBoxCodegenTestGenerated.java b/compiler/tests/org/jetbrains/kotlin/codegen/ir/IrBlackBoxCodegenTestGenerated.java index 9ac4bde3f2e..71c256c4c98 100644 --- a/compiler/tests/org/jetbrains/kotlin/codegen/ir/IrBlackBoxCodegenTestGenerated.java +++ b/compiler/tests/org/jetbrains/kotlin/codegen/ir/IrBlackBoxCodegenTestGenerated.java @@ -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"); diff --git a/compiler/tests/org/jetbrains/kotlin/codegen/ir/IrBytecodeTextTestGenerated.java b/compiler/tests/org/jetbrains/kotlin/codegen/ir/IrBytecodeTextTestGenerated.java index daefb1394b4..b404de80e7f 100644 --- a/compiler/tests/org/jetbrains/kotlin/codegen/ir/IrBytecodeTextTestGenerated.java +++ b/compiler/tests/org/jetbrains/kotlin/codegen/ir/IrBytecodeTextTestGenerated.java @@ -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") diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/es6/semantics/IrJsCodegenBoxES6TestGenerated.java b/js/js.tests/test/org/jetbrains/kotlin/js/test/es6/semantics/IrJsCodegenBoxES6TestGenerated.java index b7597e93fd5..76ec26f73b3 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/es6/semantics/IrJsCodegenBoxES6TestGenerated.java +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/es6/semantics/IrJsCodegenBoxES6TestGenerated.java @@ -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"); diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/ir/semantics/IrJsCodegenBoxTestGenerated.java b/js/js.tests/test/org/jetbrains/kotlin/js/test/ir/semantics/IrJsCodegenBoxTestGenerated.java index 2dc6cef1fb2..f98e1b8fc5e 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/ir/semantics/IrJsCodegenBoxTestGenerated.java +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/ir/semantics/IrJsCodegenBoxTestGenerated.java @@ -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"); diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/JsCodegenBoxTestGenerated.java b/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/JsCodegenBoxTestGenerated.java index a9b83274f1f..7deeac36ea6 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/JsCodegenBoxTestGenerated.java +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/semantics/JsCodegenBoxTestGenerated.java @@ -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");