From 42c213d950a3c3c8bf76cbdf51398c32c5338604 Mon Sep 17 00:00:00 2001 From: Sergej Jaskiewicz Date: Tue, 2 Nov 2021 10:42:28 +0000 Subject: [PATCH] [JS IR] Explicitly convert Long values to String in string concatenation #KT-39891 Fixed --- .../kotlin/ir/backend/js/JsLoweringPhases.kt | 7 ++ .../js/lower/JsStringConcatenationLowering.kt | 89 +++++++++++++++++++ .../testData/codegen/box/intrinsics/kt8666.kt | 10 ++- .../testData/box/number/kt26706.kt | 12 +-- 4 files changed, 111 insertions(+), 7 deletions(-) create mode 100644 compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/JsStringConcatenationLowering.kt diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/JsLoweringPhases.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/JsLoweringPhases.kt index bf8613a8a1d..35284c919b2 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/JsLoweringPhases.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/JsLoweringPhases.kt @@ -161,6 +161,12 @@ private val expectDeclarationsRemovingPhase = makeDeclarationTransformerPhase( description = "Remove expect declaration from module fragment" ) +private val stringConcatenationLoweringPhase = makeJsModulePhase( + ::JsStringConcatenationLowering, + name = "JsStringConcatenationLowering", + description = "Call toString() for values of some types when concatenating strings" +).toModuleLowering() + private val lateinitNullableFieldsPhase = makeDeclarationTransformerPhase( ::NullableFieldsForLateinitCreationLowering, name = "LateinitNullableFields", @@ -820,6 +826,7 @@ private val loweringList = listOf( copyInlineFunctionBodyLoweringPhase, removeInlineDeclarationsWithReifiedTypeParametersLoweringPhase, createScriptFunctionsPhase, + stringConcatenationLoweringPhase, callableReferenceLowering, singleAbstractMethodPhase, tailrecLoweringPhase, diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/JsStringConcatenationLowering.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/JsStringConcatenationLowering.kt new file mode 100644 index 00000000000..06eaa7a0867 --- /dev/null +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/JsStringConcatenationLowering.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2010-2021 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.ir.backend.js.lower + +import org.jetbrains.kotlin.backend.common.CommonBackendContext +import org.jetbrains.kotlin.backend.common.FileLoweringPass +import org.jetbrains.kotlin.ir.backend.js.ir.JsIrBuilder +import org.jetbrains.kotlin.ir.declarations.IrFile +import org.jetbrains.kotlin.ir.expressions.* +import org.jetbrains.kotlin.ir.symbols.IrFunctionSymbol +import org.jetbrains.kotlin.ir.types.* +import org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid +import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid +import org.jetbrains.kotlin.utils.addToStdlib.safeAs + +class JsStringConcatenationLowering(val context: CommonBackendContext) : FileLoweringPass { + override fun lower(irFile: IrFile) { + irFile.transformChildrenVoid(JsStringConcatenationTransformer(context)) + } +} + +private class JsStringConcatenationTransformer(val context: CommonBackendContext) : IrElementTransformerVoid() { + + private val IrType.shouldExplicitlyConvertToString: Boolean + get() { + // If the type is Long or a supertype of Long, we want to call toString() on values of that type. + // See KT-39891 + if (this !is IrSimpleType) return false + return when (classifier.signature) { + IdSignatureValues.any, IdSignatureValues.comparable, IdSignatureValues.number, IdSignatureValues._long -> true + else -> false + } + } + + private fun IrExpression.explicitlyConvertedToString(): IrExpression { + assert(type.shouldExplicitlyConvertToString) + + return if (type.isNullable()) { + JsIrBuilder.buildCall(context.ir.symbols.extensionToString).apply { + extensionReceiver = this@explicitlyConvertedToString + } + } else { + JsIrBuilder.buildCall(context.ir.symbols.memberToString).apply { + dispatchReceiver = this@explicitlyConvertedToString + } + } + } + + private val IrFunctionSymbol.isStringPlus: Boolean + get() = context.ir.symbols.isStringPlus(this) + + override fun visitCall(expression: IrCall): IrExpression { + fun explicitlyConvertToStringIfNeeded(): IrExpression { + val lastArgIndex = expression.valueArgumentsCount - 1 + val plusArg = expression.getValueArgument(lastArgIndex) ?: return super.visitCall(expression) + if (!plusArg.type.shouldExplicitlyConvertToString) + return super.visitCall(expression) + + expression.putValueArgument(lastArgIndex, plusArg.explicitlyConvertedToString()) + return expression + } + + if (expression.valueArgumentsCount == 0) + return super.visitCall(expression) + + if (expression.symbol.isStringPlus) + return explicitlyConvertToStringIfNeeded() + + if (expression.dispatchReceiver.safeAs()?.symbol?.isStringPlus == true) + return explicitlyConvertToStringIfNeeded() + + return super.visitCall(expression) + } + + override fun visitStringConcatenation(expression: IrStringConcatenation): IrExpression { + expression + .transformChildrenVoid(object : IrElementTransformerVoid() { + override fun visitExpression(expression: IrExpression): IrExpression { + if (expression.type.shouldExplicitlyConvertToString) + return expression.explicitlyConvertedToString() + return expression + } + }) + return super.visitStringConcatenation(expression) + } +} diff --git a/compiler/testData/codegen/box/intrinsics/kt8666.kt b/compiler/testData/codegen/box/intrinsics/kt8666.kt index 540bd21a93f..c2296d30835 100644 --- a/compiler/testData/codegen/box/intrinsics/kt8666.kt +++ b/compiler/testData/codegen/box/intrinsics/kt8666.kt @@ -1,20 +1,26 @@ -// IGNORE_BACKEND: JS_IR -// IGNORE_BACKEND: JS_IR_ES6 val MAX_LONG = "9223372036854775807" val PREFIX = "max = " fun box(): String { if (MAX_LONG != "${Long.MAX_VALUE}") return "fail template" if (MAX_LONG != "" + Long.MAX_VALUE) return "fail \"\" +" + if (MAX_LONG != ("" as String?) + Long.MAX_VALUE) return "fail \"\"? +" if (MAX_LONG != "".plus(Long.MAX_VALUE)) return "fail \"\".plus" + if (MAX_LONG != ("" as String?).plus(Long.MAX_VALUE)) return "fail \"\"?.plus" if (MAX_LONG != (String::plus)("", Long.MAX_VALUE)) return "fail String::plus" + if (MAX_LONG != (String?::plus)("", Long.MAX_VALUE)) return "fail String?::plus" if (MAX_LONG != (""::plus)(Long.MAX_VALUE)) return "fail \"\"::plus" + if (MAX_LONG != (("" as String?)::plus)(Long.MAX_VALUE)) return "fail \"\"?::plus" if (PREFIX + MAX_LONG != "max = ${Long.MAX_VALUE}") return "fail template with prefix" if (PREFIX + MAX_LONG != PREFIX + Long.MAX_VALUE) return "fail \"$PREFIX\" +" + if (PREFIX + MAX_LONG != (PREFIX as String?) + Long.MAX_VALUE) return "fail \"$PREFIX\"? +" if (PREFIX + MAX_LONG != PREFIX.plus(Long.MAX_VALUE)) return "fail \"$PREFIX\".plus" + if (PREFIX + MAX_LONG != (PREFIX as String?).plus(Long.MAX_VALUE)) return "fail \"$PREFIX\"?.plus" if (PREFIX + MAX_LONG != (String::plus)(PREFIX, Long.MAX_VALUE)) return "fail String::plus($PREFIX, ...)" + if (PREFIX + MAX_LONG != (String?::plus)(PREFIX, Long.MAX_VALUE)) return "fail String?::plus($PREFIX, ...)" if (PREFIX + MAX_LONG != (PREFIX::plus)(Long.MAX_VALUE)) return "fail \"$PREFIX\"::plus" + if (PREFIX + MAX_LONG != ((PREFIX as String?)::plus)(Long.MAX_VALUE)) return "fail \"$PREFIX\"?::plus" return "OK" } diff --git a/js/js.translator/testData/box/number/kt26706.kt b/js/js.translator/testData/box/number/kt26706.kt index f7e89d2883b..0e717baff93 100644 --- a/js/js.translator/testData/box/number/kt26706.kt +++ b/js/js.translator/testData/box/number/kt26706.kt @@ -1,5 +1,3 @@ -// IGNORE_BACKEND: JS_IR -// IGNORE_BACKEND: JS_IR_ES6 // KJS_WITH_FULL_RUNTIME // EXPECTED_REACHABLE_NODES: 1378 package foo @@ -11,9 +9,13 @@ fun box(): String { if (" ${x.toLong() as Number}" != " $x") return "FAIL 3" if (" ${x.toLong() as Any}" != " $x") return "FAIL 4" if (" ${x.toLong() as Long?}" != " $x") return "FAIL 5" - if (" ${x.toLong() as Comparable?}" != " $x") return "FAIL 6" - if (" ${x.toLong() as Number?}" != " $x") return "FAIL 7" - if (" ${x.toLong() as Any?}" != " $x") return "FAIL 8" + if (" ${null as Long?}" != " null") return "FAIL 6" + if (" ${x.toLong() as Comparable?}" != " $x") return "FAIL 7" + if (" ${null as Comparable?}" != " null") return "FAIL 8" + if (" ${x.toLong() as Number?}" != " $x") return "FAIL 9" + if (" ${null as Number?}" != " null") return "FAIL 10" + if (" ${x.toLong() as Any?}" != " $x") return "FAIL 11" + if (" ${null as Any?}" != " null") return "FAIL 12" return "OK" } \ No newline at end of file