diff --git a/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/codegen/ExpressionCodegen.kt b/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/codegen/ExpressionCodegen.kt index e36e620aa45..7eef721589f 100644 --- a/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/codegen/ExpressionCodegen.kt +++ b/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/codegen/ExpressionCodegen.kt @@ -47,7 +47,7 @@ import org.jetbrains.kotlin.ir.util.* import org.jetbrains.kotlin.ir.visitors.IrElementVisitor import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.resolve.jvm.AsmTypes -import org.jetbrains.kotlin.resolve.jvm.AsmTypes.OBJECT_TYPE +import org.jetbrains.kotlin.resolve.jvm.AsmTypes.* import org.jetbrains.kotlin.resolve.jvm.jvmSignature.JvmMethodParameterKind import org.jetbrains.kotlin.resolve.jvm.jvmSignature.JvmMethodSignature import org.jetbrains.kotlin.types.TypeSystemCommonBackendContext @@ -1167,6 +1167,36 @@ class ExpressionCodegen( return unitValue } + override fun visitStringConcatenation(expression: IrStringConcatenation, data: BlockInfo): PromisedValue { + assert(context.state.runtimeStringConcat.isDynamic) { + "IrStringConcatenation expression should be presented only with dynamic concatenation: ${expression.dump()}" + } + val generator = StringConcatGenerator(context.state.runtimeStringConcat, mv) + expression.arguments.forEach { arg -> + if (arg is IrConst<*>) { + val type = when (arg.kind) { + IrConstKind.Boolean -> Type.BOOLEAN_TYPE + IrConstKind.Char -> Type.CHAR_TYPE + IrConstKind.Int -> Type.INT_TYPE + IrConstKind.Long -> Type.LONG_TYPE + IrConstKind.Float -> Type.FLOAT_TYPE + IrConstKind.Double -> Type.DOUBLE_TYPE + IrConstKind.Byte -> Type.BYTE_TYPE + IrConstKind.Short -> Type.SHORT_TYPE + IrConstKind.String -> JAVA_STRING_TYPE + IrConstKind.Null -> OBJECT_TYPE + } + generator.putValueOrProcessConstant(StackValue.constant(arg.value, type, null)) + } else { + val value = arg.accept(this, data) + value.materializeAt(value.type, value.irType) + generator.invokeAppend(value.type) + } + } + generator.genToString() + return MaterialValue(this@ExpressionCodegen, JAVA_STRING_TYPE, context.irBuiltIns.stringType) + } + override fun visitGetClass(expression: IrGetClass, data: BlockInfo) = generateClassLiteralReference(expression, true, data) 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 74d479cac5a..08103946c82 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 @@ -20,13 +20,19 @@ import org.jetbrains.kotlin.ir.declarations.IrClass import org.jetbrains.kotlin.ir.declarations.IrFile import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction import org.jetbrains.kotlin.ir.expressions.* +import org.jetbrains.kotlin.ir.expressions.impl.IrStringConcatenationImpl import org.jetbrains.kotlin.ir.types.* import org.jetbrains.kotlin.ir.util.constructors import org.jetbrains.kotlin.ir.util.functions import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid -internal val jvmStringConcatenationLowering = makeIrFilePhase( - ::JvmStringConcatenationLowering, +internal val jvmStringConcatenationLowering = makeIrFilePhase( + { context: JvmBackendContext -> + if (!context.state.runtimeStringConcat.isDynamic) + JvmStringConcatenationLowering(context) + else + JvmDynamicStringConcatenationLowering(context) + }, name = "StringConcatenation", description = "Replace IrStringConcatenation with string builders", // flattenStringConcatenationPhase consolidates string concatenation expressions. @@ -34,6 +40,61 @@ internal val jvmStringConcatenationLowering = makeIrFilePhase( prerequisite = setOf(flattenStringConcatenationPhase, forLoopsPhase) ) +private val IrClass.toStringFunction: IrSimpleFunction + get() = functions.single { + with(FlattenStringConcatenationLowering) { it.isToString } + } + + +private fun IrBuilderWithScope.normalizeArgument(expression: IrExpression): IrExpression = + if (expression.type.isByte() || expression.type.isShort()) { + // There is no special append or valueOf function for byte and short on the JVM. + irImplicitCast(expression, context.irBuiltIns.intType) + } else if (expression is IrConst<*> && expression.kind == IrConstKind.String && (expression.value as String).length == 1) { + // PSI2IR generates const Strings for 1-length literals in string templates (e.g., the space between x and y in "$x $y"). + // We want to use the more efficient `append(Char)` function in such cases. This mirrors the behavior of the non-IR backend. + // + // In addition, this also means `append(Char)` will be used for the space in the following case: `x + " " + y`. The non-IR + // backend will still use `append(String)` in this case. + irChar((expression.value as String)[0]) + } else { + expression + } + +private fun JvmIrBuilder.callToString(expression: IrExpression): IrExpression { + val argument = normalizeArgument(expression) + val argumentType = if (argument.type.isPrimitiveType()) argument.type else context.irBuiltIns.anyNType + + return irCall(backendContext.ir.symbols.typeToStringValueOfFunction(argumentType)).apply { + putValueArgument(0, argument) + } +} + +private fun JvmIrBuilder.lowerInlineClassArgument(expression: IrExpression): IrExpression? { + if (InlineClassAbi.unboxType(expression.type) == null) + return null + val toStringFunction = expression.type.classOrNull?.owner?.toStringFunction + ?: return null + val toStringReplacement = backendContext.inlineClassReplacements.getReplacementFunction(toStringFunction) + ?: 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) } +} + +private fun IrExpression.unwrapImplicitNotNull() = + if (this is IrTypeOperatorCall && operator == IrTypeOperator.IMPLICIT_NOTNULL) + argument + else + this + /** * This lowering pass replaces [IrStringConcatenation]s with StringBuilder appends. * @@ -50,11 +111,6 @@ private class JvmStringConcatenationLowering(val context: JvmBackendContext) : F it.valueParameters.size == 0 } - private val IrClass.toStringFunction: IrSimpleFunction - get() = functions.single { - with(FlattenStringConcatenationLowering) { it.isToString } - } - private val toStringFunction = stringBuilder.toStringFunction private val defaultAppendFunction = stringBuilder.functions.single { @@ -73,49 +129,6 @@ private class JvmStringConcatenationLowering(val context: JvmBackendContext) : F private fun typeToAppendFunction(type: IrType): IrSimpleFunction = appendFunctions[type] ?: defaultAppendFunction - private fun IrBuilderWithScope.normalizeArgument(expression: IrExpression): IrExpression = - if (expression.type.isByte() || expression.type.isShort()) { - // There is no special append or valueOf function for byte and short on the JVM. - irImplicitCast(expression, context.irBuiltIns.intType) - } else if (expression is IrConst<*> && expression.kind == IrConstKind.String && (expression.value as String).length == 1) { - // PSI2IR generates const Strings for 1-length literals in string templates (e.g., the space between x and y in "$x $y"). - // We want to use the more efficient `append(Char)` function in such cases. This mirrors the behavior of the non-IR backend. - // - // In addition, this also means `append(Char)` will be used for the space in the following case: `x + " " + y`. The non-IR - // backend will still use `append(String)` in this case. - irChar((expression.value as String)[0]) - } else { - expression - } - - private fun JvmIrBuilder.callToString(expression: IrExpression): IrExpression { - val argument = normalizeArgument(expression) - val argumentType = if (argument.type.isPrimitiveType()) argument.type else context.irBuiltIns.anyNType - - return irCall(backendContext.ir.symbols.typeToStringValueOfFunction(argumentType)).apply { - putValueArgument(0, argument) - } - } - - private fun JvmIrBuilder.lowerInlineClassArgument(expression: IrExpression): IrExpression? { - if (InlineClassAbi.unboxType(expression.type) == null) - return null - val toStringFunction = expression.type.classOrNull?.owner?.toStringFunction - ?: return null - val toStringReplacement = backendContext.inlineClassReplacements.getReplacementFunction(toStringFunction) - ?: 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 { expression.transformChildrenVoid(this) return context.createJvmIrBuilder(currentScope!!.scope.scopeOwnerSymbol, expression.startOffset, expression.endOffset).run { @@ -124,12 +137,6 @@ private class JvmStringConcatenationLowering(val context: JvmBackendContext) : F // check (see KT-36625, pending language design decision). To maintain compatibility with the non-IR backend, we remove // IMPLICIT_NOTNULL casts from all arguments (nullability checks are generated in JvmArgumentNullabilityAssertionsLowering). - fun IrExpression.unwrapImplicitNotNull() = - if (this is IrTypeOperatorCall && operator == IrTypeOperator.IMPLICIT_NOTNULL) - argument - else - this - val arguments = expression.arguments when { arguments.isEmpty() -> @@ -165,3 +172,38 @@ private class JvmStringConcatenationLowering(val context: JvmBackendContext) : F } } } + +/** + * This lowering pass lowers inline classes arguments of [IrStringConcatenation]. + * Transformed [IrStringConcatenation] would be used as is in [ExpressionCodegen] for makeConcat/makeConcatWithConstants bytecode generation + */ +private class JvmDynamicStringConcatenationLowering(val context: JvmBackendContext) : FileLoweringPass, IrElementTransformerVoidWithContext() { + override fun lower(irFile: IrFile) = irFile.transformChildrenVoid() + + override fun visitStringConcatenation(expression: IrStringConcatenation): IrExpression { + expression.transformChildrenVoid(this) + return context.createJvmIrBuilder(currentScope!!.scope.scopeOwnerSymbol, expression.startOffset, expression.endOffset).run { + // When `String.plus(Any?)` is invoked with receiver of platform type String or String with enhanced nullability, this could + // fail a nullability check (NullPointerException) on the receiver. However, the non-IR backend currently does NOT insert this + // check (see KT-36625, pending language design decision). To maintain compatibility with the non-IR backend, we remove + // IMPLICIT_NOTNULL casts from all arguments (nullability checks are generated in JvmArgumentNullabilityAssertionsLowering). + + val arguments = expression.arguments + when { + arguments.isEmpty() -> + irString("") + + else -> { + IrStringConcatenationImpl( + expression.startOffset, + expression.endOffset, + expression.type, + arguments.map { argument -> + lowerInlineClassArgument(argument) ?: argument.unwrapImplicitNotNull() + }) + } + } + } + } +} + diff --git a/compiler/testData/codegen/bytecodeText/stringOperations/concatDynamic.kt b/compiler/testData/codegen/bytecodeText/stringOperations/concatDynamic.kt index 7f62670a90a..677a8986890 100644 --- a/compiler/testData/codegen/bytecodeText/stringOperations/concatDynamic.kt +++ b/compiler/testData/codegen/bytecodeText/stringOperations/concatDynamic.kt @@ -2,12 +2,15 @@ // JVM_TARGET: 9 class A +inline class IC(val x: String) + inline fun test(s: (String) -> Unit) { s("456") } -fun box(a: String, b: String?) { - val s = a + "1" + "2" + 3 + 4L + b + 5.0 + 6F + '7' + A() + true + false + 1u +fun box(a: String, b: String?, x: IC?) { + val p = 3147483648u + val s = a + "1" + "2" + 3 + 4L + b + 5.0 + 6F + '7' + A() + true + false + 3147483647u + p + x a.plus(b) b?.plus(a) @@ -17,9 +20,27 @@ fun box(a: String, b: String?) { test("123"::plus) } -// unsigned constant 1u processed as argument (last \u0001) - -// 1 "\\u00011234\\u00015.06.07\\u0001truefalse\\u0001" -// 6 INVOKEDYNAMIC makeConcatWithConstants +// 7 INVOKEDYNAMIC makeConcatWithConstants +// 1 "IC\(x=\\u0001\)" // 0 append -// 0 stringPlus \ No newline at end of file +// 0 stringPlus + +// JVM_TEMPLATES +// unsigned constant 3147483647u is processed as argument (\u0001) and it adds additional `UInt.toString-impl` call +// 1 "\\u00011234\\u00015.06.07\\u0001truefalse\\u0001\\u0001\\u0001" +// 2 INVOKESTATIC kotlin/UInt.toString-impl + +// Old backend perform inline class boxing... +// 1 INVOKESTATIC IC.box-impl + +// one in IC.toString() +// 1 INVOKESTATIC IC.toString-impl + +// JVM_IR_TEMPLATES +// unsigned constant 3147483647u is inlined +// 1 "\\u00011234\\u00015.06.07\\u0001truefalse3147483647\\u0001\\u0001" +// 1 INVOKESTATIC kotlin/UInt.toString-impl + +// .. but ir backend performs wise `toString-impl` call +// one in IC.toString() + one in concatenation +// 2 INVOKESTATIC IC.toString-impl \ No newline at end of file diff --git a/compiler/testData/codegen/bytecodeText/stringOperations/concatDynamicIndy.kt b/compiler/testData/codegen/bytecodeText/stringOperations/concatDynamicIndy.kt index b112205dff2..3f574a8222a 100644 --- a/compiler/testData/codegen/bytecodeText/stringOperations/concatDynamicIndy.kt +++ b/compiler/testData/codegen/bytecodeText/stringOperations/concatDynamicIndy.kt @@ -7,7 +7,7 @@ inline fun test(s: (String) -> Unit) { } fun box(a: String, b: String?) { - val s = a + "1" + "2" + 3 + 4L + b + 5.0 + 6F + '7' + A() + true + false + 1u + val s = a + "1" + "2" + 3 + 4L + b + 5.0 + 6F + '7' + A() + true + false + 3147483647u a.plus(b) b?.plus(a)