diff --git a/compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/lower/FlattenStringConcatenationLowering.kt b/compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/lower/FlattenStringConcatenationLowering.kt index 48aadf53dc6..3ddef3c595f 100644 --- a/compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/lower/FlattenStringConcatenationLowering.kt +++ b/compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/lower/FlattenStringConcatenationLowering.kt @@ -80,42 +80,55 @@ class FlattenStringConcatenationLowering(val context: CommonBackendContext) : Fi ) /** @return true if the given expression is a call to [String.plus] */ - private fun isStringPlusCall(expression: IrCall): Boolean { - val function = expression.symbol.owner - val receiver = expression.dispatchReceiver ?: expression.extensionReceiver + private val IrCall.isStringPlusCall: Boolean + get() { + val function = symbol.owner + val receiver = dispatchReceiver ?: extensionReceiver - return receiver != null - && receiver.type.isStringClassType() - && function.returnType.isStringClassType() - && function.valueParameters.size == 1 - && function.name.asString() == "plus" - && function.fqNameWhenAvailable?.parent() in PARENT_NAMES - } + return receiver != null + && receiver.type.isStringClassType() + && function.returnType.isStringClassType() + && function.valueParameters.size == 1 + && function.name.asString() == "plus" + && function.fqNameWhenAvailable?.parent() in PARENT_NAMES + } + + /** @return true if the function is Any.toString or an override of Any.toString */ + val IrSimpleFunction.isToString: Boolean + get() { + if (name.asString() != "toString" || valueParameters.size != 0 || !returnType.isString()) + return false + + return (dispatchReceiverParameter != null && extensionReceiverParameter == null + && (dispatchReceiverParameter?.type?.isAny() == true || this.overriddenSymbols.isNotEmpty())) + } + + /** @return true if the function is Any?.toString */ + private val IrSimpleFunction.isNullableToString: Boolean + get() { + if (name.asString() != "toString" || valueParameters.size != 0 || !returnType.isString()) + return false + + return dispatchReceiverParameter == null + && extensionReceiverParameter?.type?.isNullableAny() == true + && fqNameWhenAvailable?.parent() == KotlinBuiltIns.BUILT_INS_PACKAGE_FQ_NAME + } /** @return true if the given expression is a call to [toString] */ - private fun isToStringCall(expression: IrCall): Boolean { - if (expression.superQualifierSymbol != null) - return false + private val IrCall.isToStringCall: Boolean + get() { + if (superQualifierSymbol != null) + return false - val function = expression.symbol.owner - if (function.name.asString() != "toString" || function.valueParameters.size != 0 || !function.returnType.isString()) - return false + val function = symbol.owner as? IrSimpleFunction + ?: return false - // Check for Any?.toString, which is an extension method defined in the kotlin package. - if (function.dispatchReceiverParameter == null - && function.extensionReceiverParameter?.type?.isNullableAny() == true - && function.fqNameWhenAvailable?.parent() == KotlinBuiltIns.BUILT_INS_PACKAGE_FQ_NAME) - return true - - // Check for Any.toString or an override. - return (function.dispatchReceiverParameter != null && function.extensionReceiverParameter == null - && (function.dispatchReceiverParameter?.type?.isAny() == true - || (function as? IrSimpleFunction)?.overriddenSymbols?.isNotEmpty() == true)) - } + return function.isToString || function.isNullableToString + } /** @return true if the given expression is a [IrStringConcatenation], or an [IrCall] to [toString] or [String.plus]. */ private fun isStringConcatenationExpression(expression: IrExpression): Boolean = - (expression is IrStringConcatenation) || (expression is IrCall) && (isStringPlusCall(expression) || isToStringCall(expression)) + (expression is IrStringConcatenation) || (expression is IrCall) && (expression.isStringPlusCall || expression.isToStringCall) /** Recursively collects string concatenation arguments from the given expression. */ private fun collectStringConcatenationArguments(expression: IrExpression): List { diff --git a/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/JvmLower.kt b/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/JvmLower.kt index b850a3c5fc0..1a7dee23032 100644 --- a/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/JvmLower.kt +++ b/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/JvmLower.kt @@ -218,6 +218,7 @@ private val jvmFilePhases = flattenStringConcatenationPhase then foldConstantLoweringPhase then computeStringTrimPhase then + jvmStringConcatenationLowering then defaultArgumentStubPhase then defaultArgumentInjectorPhase then diff --git a/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/JvmSymbols.kt b/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/JvmSymbols.kt index e944b1d0b10..c694862bd81 100644 --- a/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/JvmSymbols.kt +++ b/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/JvmSymbols.kt @@ -104,6 +104,10 @@ class JvmSymbols( addValueParameter("value", irBuiltIns.anyNType) addValueParameter("expression", irBuiltIns.stringType) } + klass.addFunction("stringPlus", irBuiltIns.stringType, isStatic = true).apply { + addValueParameter("self", irBuiltIns.stringType.makeNullable()) + addValueParameter("other", irBuiltIns.anyNType) + } } val checkExpressionValueIsNotNull: IrSimpleFunctionSymbol = @@ -115,6 +119,9 @@ class JvmSymbols( override val ThrowUninitializedPropertyAccessException: IrSimpleFunctionSymbol = intrinsicsClass.functions.single { it.owner.name.asString() == "throwUninitializedPropertyAccessException" } + val intrinsicStringPlus: IrFunctionSymbol = + intrinsicsClass.functions.single { it.owner.name.asString() == "stringPlus" } + override val stringBuilder: IrClassSymbol get() = context.getTopLevelClass(FqName("java.lang.StringBuilder")) @@ -437,9 +444,26 @@ class JvmSymbols( addValueParameter("v", irBuiltIns.anyNType) returnType = irBuiltIns.stringType }.symbol + + private val javaLangString: IrClassSymbol = context.getTopLevelClass(FqName("java.lang.String")) + + private val defaultValueOfFunction = javaLangString.functions.single { + it.owner.name.asString() == "valueOf" && it.owner.valueParameters.singleOrNull()?.type?.isNullableAny() == true + } + + private val valueOfFunctions: Map = + (context.irBuiltIns.primitiveIrTypes + context.irBuiltIns.stringType).associateWith { type -> + javaLangString.functions.singleOrNull { + it.owner.name.asString() == "valueOf" && it.owner.valueParameters.singleOrNull()?.type == type + } + } + + fun typeToStringValueOfFunction(type: IrType): IrSimpleFunctionSymbol = + valueOfFunctions[type] ?: defaultValueOfFunction } private fun IrClassSymbol.functionByName(name: String): IrSimpleFunctionSymbol = functions.single { it.owner.name.asString() == name } + private fun IrClassSymbol.fieldByName(name: String): IrFieldSymbol = fields.single { it.owner.name.asString() == name } 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 80f1d9fea5f..fac4a672d2f 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 @@ -671,54 +671,6 @@ class ExpressionCodegen( } } - override fun visitStringConcatenation(expression: IrStringConcatenation, data: BlockInfo): PromisedValue { - expression.markLineNumber(startOffset = true) - val arity = expression.arguments.size - when { - arity == 0 -> mv.aconst("") - arity == 1 -> { - // Convert single arg to string. - val arg = expression.arguments[0] - val result = arg.accept(this, data).boxInlineClasses(arg.type).materialized - if (!arg.type.isString()) { - result.genToString(mv) - } - } - arity == 2 && expression.arguments[0].type.isStringClassType() -> { - // Call the stringPlus intrinsic - for ((index, argument) in expression.arguments.withIndex()) { - val result = argument.accept(this, data).boxInlineClasses(argument.type).materialized - if (result.type.sort != Type.OBJECT) { - result.genToString(mv) - } else if (index == 0) { - result.coerce(context.irBuiltIns.stringType).materialize() - } - } - mv.invokestatic( - IrIntrinsicMethods.INTRINSICS_CLASS_NAME, - "stringPlus", - "(Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String;", - false - ) - } - else -> { - // Use StringBuilder to concatenate. - genStringBuilderConstructor(mv) - for (argument in expression.arguments) { - genInvokeAppendMethod(mv, argument.accept(this, data).boxInlineClasses(argument.type).materialized.type, null) - } - mv.invokevirtual("java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false) - } - } - return expression.onStack - } - - private fun MaterialValue.genToString(v: InstructionAdapter) { - val asmType = - if (irType.getClass()?.isInline == true) OBJECT_TYPE else stringValueOfType(type) - v.invokestatic("java/lang/String", "valueOf", Type.getMethodDescriptor(AsmTypes.JAVA_STRING_TYPE, asmType), false) - } - override fun visitWhileLoop(loop: IrWhileLoop, data: BlockInfo): PromisedValue { val continueLabel = markNewLabel() val endLabel = Label() 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 new file mode 100644 index 00000000000..0a60f5d7cc2 --- /dev/null +++ b/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/lower/JvmStringConcatenationLowering.kt @@ -0,0 +1,145 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.backend.jvm.lower + +import org.jetbrains.kotlin.backend.common.FileLoweringPass +import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext +import org.jetbrains.kotlin.backend.common.lower.FlattenStringConcatenationLowering +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.unboxInlineClass +import org.jetbrains.kotlin.ir.builders.IrBuilderWithScope +import org.jetbrains.kotlin.ir.builders.irCall +import org.jetbrains.kotlin.ir.builders.irImplicitCast +import org.jetbrains.kotlin.ir.builders.irString +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.IrExpression +import org.jetbrains.kotlin.ir.expressions.IrStringConcatenation +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, + name = "StringConcatenation", + description = "Replace IrStringConcatenation with string builders" +) + +/** + * This lowering pass replaces [IrStringConcatenation]s with StringBuilder appends. + * + * This pass is based on [StringConcatenationLowering] in backend.common. The main difference + * is that this pass also handles JVM specific optimizations, such as calling stringPlus + * for two arguments, and properly handles inline classes. + */ +private class JvmStringConcatenationLowering(val context: JvmBackendContext) : FileLoweringPass, IrElementTransformerVoidWithContext() { + override fun lower(irFile: IrFile) = irFile.transformChildrenVoid() + + private val stringBuilder = context.ir.symbols.stringBuilder.owner + + private val constructor = stringBuilder.constructors.single { + 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 { + it.name.asString() == "append" && + it.valueParameters.size == 1 && + it.valueParameters.single().type.isNullableAny() + } + + private val appendFunctions: Map = + (context.irBuiltIns.primitiveIrTypes + context.irBuiltIns.stringType).associateWith { type -> + stringBuilder.functions.singleOrNull { + it.name.asString() == "append" && it.valueParameters.singleOrNull()?.type == type + } + } + + private fun typeToAppendFunction(type: IrType): IrSimpleFunction = + appendFunctions[type] ?: defaultAppendFunction + + // There is no special append or valueOf function for byte and short on the JVM. + private fun IrBuilderWithScope.widenIntegerType(expression: IrExpression): IrExpression = + if (expression.type.isByte() || expression.type.isShort()) { + irImplicitCast(expression, context.irBuiltIns.intType) + } else { + expression + } + + private fun JvmIrBuilder.callToString(expression: IrExpression): IrExpression { + if (expression.type.isString()) + return expression + + val argument = widenIntegerType(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 (expression.type.unboxInlineClass() == expression.type) + return expression + + val toStringFunction = expression.type.classOrNull?.owner?.toStringFunction + ?: return expression + + val toStringReplacement = backendContext.inlineClassReplacements.getReplacementFunction(toStringFunction) + ?: return expression + + return irCall(toStringReplacement.function).apply { + putValueArgument(0, expression) + } + } + + override fun visitStringConcatenation(expression: IrStringConcatenation): IrExpression { + expression.transformChildrenVoid(this) + return context.createJvmIrBuilder(currentScope!!.scope.scopeOwnerSymbol, expression.startOffset, expression.endOffset).run { + val arguments = expression.arguments.map { lowerInlineClassArgument(it) } + + when { + arguments.isEmpty() -> + irString("") + + arguments.size == 1 -> + callToString(arguments.single()) + + arguments.size == 2 && arguments[0].type.isStringClassType() -> + irCall(backendContext.ir.symbols.intrinsicStringPlus).apply { + putValueArgument(0, arguments[0]) + putValueArgument(1, arguments[1]) + } + + else -> { + var stringBuilder = irCall(constructor) + for (arg in arguments) { + val argument = widenIntegerType(arg) + val appendFunction = typeToAppendFunction(argument.type) + stringBuilder = irCall(appendFunction).apply { + dispatchReceiver = stringBuilder + putValueArgument(0, argument) + } + } + irCall(toStringFunction).apply { + dispatchReceiver = stringBuilder + } + } + } + } + } +} diff --git a/compiler/testData/codegen/bytecodeText/inlineClasses/inlineClassInGeneratedToString.kt b/compiler/testData/codegen/bytecodeText/inlineClasses/inlineClassInGeneratedToString.kt index d20ee956b16..e1a7fab3496 100644 --- a/compiler/testData/codegen/bytecodeText/inlineClasses/inlineClassInGeneratedToString.kt +++ b/compiler/testData/codegen/bytecodeText/inlineClasses/inlineClassInGeneratedToString.kt @@ -1,5 +1,4 @@ // !LANGUAGE: +InlineClasses -// IGNORE_BACKEND: JVM_IR // FILE: Z.kt inline class Z(val value: Int) diff --git a/compiler/testData/codegen/bytecodeText/inlineClasses/inlineClassInStringTemplate.kt b/compiler/testData/codegen/bytecodeText/inlineClasses/inlineClassInStringTemplate.kt index c06041f4787..baf0e9511ca 100644 --- a/compiler/testData/codegen/bytecodeText/inlineClasses/inlineClassInStringTemplate.kt +++ b/compiler/testData/codegen/bytecodeText/inlineClasses/inlineClassInStringTemplate.kt @@ -1,5 +1,4 @@ // !LANGUAGE: +InlineClasses -// IGNORE_BACKEND: JVM_IR // FILE: Z.kt inline class Z(val value: Int) diff --git a/compiler/testData/codegen/bytecodeText/inlineClasses/resultApiStringInterpolationDoesntBox.kt b/compiler/testData/codegen/bytecodeText/inlineClasses/resultApiStringInterpolationDoesntBox.kt index f43dfb8c4ce..40c793108d6 100644 --- a/compiler/testData/codegen/bytecodeText/inlineClasses/resultApiStringInterpolationDoesntBox.kt +++ b/compiler/testData/codegen/bytecodeText/inlineClasses/resultApiStringInterpolationDoesntBox.kt @@ -1,4 +1,3 @@ -// IGNORE_BACKEND: JVM_IR // WITH_RUNTIME // FILE: test.kt fun test() {