diff --git a/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/CoroutineTransformationClassBuilder.kt b/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/CoroutineTransformationClassBuilder.kt index 5bfe0ecd5d0..85ae871f1b2 100644 --- a/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/CoroutineTransformationClassBuilder.kt +++ b/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/CoroutineTransformationClassBuilder.kt @@ -20,13 +20,12 @@ import com.intellij.util.containers.Stack import org.jetbrains.kotlin.codegen.* import org.jetbrains.kotlin.codegen.optimization.DeadCodeEliminationMethodTransformer import org.jetbrains.kotlin.codegen.optimization.FixStackWithLabelNormalizationMethodTransformer -import org.jetbrains.kotlin.codegen.optimization.common.OptimizationBasicInterpreter import org.jetbrains.kotlin.codegen.optimization.common.analyzeLiveness import org.jetbrains.kotlin.codegen.optimization.common.insnListOf import org.jetbrains.kotlin.codegen.optimization.common.removeEmptyCatchBlocks -import org.jetbrains.kotlin.codegen.optimization.transformer.MethodTransformer import org.jetbrains.kotlin.resolve.jvm.AsmTypes import org.jetbrains.kotlin.resolve.jvm.diagnostics.JvmDeclarationOrigin +import org.jetbrains.kotlin.utils.sure import org.jetbrains.org.objectweb.asm.Label import org.jetbrains.org.objectweb.asm.MethodVisitor import org.jetbrains.org.objectweb.asm.Opcodes @@ -191,7 +190,7 @@ class CoroutineTransformerMethodVisitor( private fun spillVariables(suspensionPoints: List, methodNode: MethodNode) { val instructions = methodNode.instructions - val frames = MethodTransformer.analyze(classBuilder.thisName, methodNode, OptimizationBasicInterpreter()) + val frames = performRefinedTypeAnalysis(methodNode, classBuilder.thisName) fun AbstractInsnNode.index() = instructions.indexOf(this) // We postpone these actions because they change instruction indices that we use when obtaining frames @@ -202,11 +201,11 @@ class CoroutineTransformerMethodVisitor( for (suspension in suspensionPoints) { val suspensionCallBegin = suspension.suspensionCallBegin val suspensionCallEnd = suspension.suspensionCallEnd - assert(frames[suspensionCallEnd.next.index()].stackSize == (if (suspension.returnType.sort == Type.VOID) 0 else 1)) { + assert(frames[suspensionCallEnd.next.index()]?.stackSize == (if (suspension.returnType.sort == Type.VOID) 0 else 1)) { "Stack should be spilled before suspension call" } - val frame = frames[suspensionCallBegin.index()] + val frame = frames[suspensionCallBegin.index()].sure { "Suspension points containing in dead code must be removed" } val localsCount = frame.locals val varsCountByType = mutableMapOf() diff --git a/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/refinedIntTypesAnalysis.kt b/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/refinedIntTypesAnalysis.kt new file mode 100644 index 00000000000..4397a718b6e --- /dev/null +++ b/compiler/backend/src/org/jetbrains/kotlin/codegen/coroutines/refinedIntTypesAnalysis.kt @@ -0,0 +1,232 @@ +/* + * Copyright 2010-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.codegen.coroutines + +import org.jetbrains.kotlin.codegen.optimization.common.* +import org.jetbrains.kotlin.codegen.optimization.fixStack.peek +import org.jetbrains.kotlin.codegen.optimization.fixStack.top +import org.jetbrains.kotlin.codegen.optimization.transformer.MethodTransformer +import org.jetbrains.org.objectweb.asm.Opcodes +import org.jetbrains.org.objectweb.asm.Type +import org.jetbrains.org.objectweb.asm.tree.* +import org.jetbrains.org.objectweb.asm.tree.analysis.BasicValue +import org.jetbrains.org.objectweb.asm.tree.analysis.Frame +import org.jetbrains.org.objectweb.asm.tree.analysis.SourceInterpreter +import org.jetbrains.org.objectweb.asm.tree.analysis.SourceValue +import java.util.* + +// BasicValue interpreter from ASM does not distinct 'int' types from other int-like types like 'byte' or 'boolean', +// neither do HotSpot and JVM spec. +// But it seems like Dalvik does not follow it, and spilling boolean value into an 'int' field fails with VerifyError on Android 4, +// so this function calculates refined frames' markup. +// Note that type of some values is only possible to determine by their usages (e.g. ICONST_1, BALOAD both may push boolean or byte on stack) +internal fun performRefinedTypeAnalysis(methodNode: MethodNode, thisName: String): Array?> { + val insnList = methodNode.instructions + val basicFrames = MethodTransformer.analyze(thisName, methodNode, OptimizationBasicInterpreter()) + val sourceValueFrames = MethodTransformer.analyze(thisName, methodNode, MySourceInterpreter()) + + val expectedTypeAndSourcesByInsnIndex: Array>?> = arrayOfNulls(insnList.size()) + + fun AbstractInsnNode.index() = insnList.indexOf(this) + + fun saveExpectedType(value: SourceValue?, expectedType: Type) { + if (value == null) return + if (expectedType.sort !in REFINED_INT_SORTS) return + + value.insns.forEach { + val index = insnList.indexOf(it) + + checkUpdatedExpectedType(expectedTypeAndSourcesByInsnIndex[index]?.first, expectedType) + + expectedTypeAndSourcesByInsnIndex[index] = + Pair(expectedType, + expectedTypeAndSourcesByInsnIndex[index]?.second.orEmpty() + value) + } + } + + fun saveExpectedTypeForArrayStore(insn: AbstractInsnNode, sourceValueFrame: Frame) { + val arrayStoreType = + when (insn.opcode) { + Opcodes.BASTORE -> Type.BYTE_TYPE + Opcodes.CASTORE -> Type.CHAR_TYPE + Opcodes.SASTORE -> Type.SHORT_TYPE + else -> return + } + + val insnIndex = insnList.indexOf(insn) + + val arrayArg = basicFrames[insnIndex].peek(2) + // may be different from 'arrayStoreType' in case of boolean arrays (BASTORE opcode is also used for them) + val expectedType = + if (arrayArg?.type?.sort == Type.ARRAY) + arrayArg.type.elementType + else + arrayStoreType + + saveExpectedType(sourceValueFrame.top(), expectedType) + } + + fun saveExpectedTypeForFieldOrMethod(insn: AbstractInsnNode, sourceValueFrame: Frame) { + when (insn.opcode) { + Opcodes.PUTFIELD, Opcodes.PUTSTATIC -> + saveExpectedType(sourceValueFrame.top(), Type.getType((insn as FieldInsnNode).desc)) + + Opcodes.INVOKESTATIC, Opcodes.INVOKEVIRTUAL, Opcodes.INVOKEINTERFACE, Opcodes.INVOKESPECIAL -> { + val argumentTypes = Type.getArgumentTypes((insn as MethodInsnNode).desc) + argumentTypes.withIndex().forEach { + val (argIndex, type) = it + saveExpectedType(sourceValueFrame.peek(argumentTypes.size - argIndex - 1), type) + } + } + } + } + + fun saveExpectedTypeForVarStore(insn: AbstractInsnNode, sourceValueFrame: Frame) { + if (insn.isIntStore()) { + val varIndex = (insn as VarInsnNode).`var` + // Considering next insn is important because variable initializer is emitted just before + // the beginning of variable + val nextInsn = InsnSequence(insn.next, insnList.last).firstOrNull(AbstractInsnNode::isMeaningful) + + val variableNode = + methodNode.findContainingVariableFromTable(insn, varIndex) + ?: methodNode.findContainingVariableFromTable(nextInsn ?: return, varIndex) + ?: return + + saveExpectedType(sourceValueFrame.top(), Type.getType(variableNode.desc)) + } + } + + for ((insnIndex, insn) in insnList.toArray().withIndex()) { + assert(insn.opcode != Opcodes.IRETURN) { + "Coroutine body must not contain IRETURN instructions because 'doResume' is always void" + } + + val sourceValueFrame = sourceValueFrames[insnIndex] ?: continue + + saveExpectedTypeForArrayStore(insn, sourceValueFrame) + saveExpectedTypeForFieldOrMethod(insn, sourceValueFrame) + saveExpectedTypeForVarStore(insn, sourceValueFrame) + } + + val refinedVarFrames = analyze(methodNode, object : BackwardAnalysisInterpreter { + override fun newFrame(maxLocals: Int): VarExpectedTypeFrame = VarExpectedTypeFrame(maxLocals) + + override fun def(frame: VarExpectedTypeFrame, insn: AbstractInsnNode) { + if (insn.isIntStore()) { + frame.expectedTypeByVarIndex[(insn as VarInsnNode).`var`] = null + } + } + + override fun use(frame: VarExpectedTypeFrame, insn: AbstractInsnNode) { + val (expectedType, sources) = expectedTypeAndSourcesByInsnIndex[insn.index()] ?: return + + sources.flatMap(SourceValue::insns).forEach { + insn -> + if (insn.isIntLoad()) { + frame.updateExpectedType((insn as VarInsnNode).`var`, expectedType) + } + } + } + }) + + val refinedFrames = Array(basicFrames.size) { + insnIndex -> + val current = Frame(basicFrames[insnIndex] ?: return@Array null) + + refinedVarFrames[insnIndex].expectedTypeByVarIndex.withIndex().filter { it.value != null }.forEach { + assert(current.getLocal(it.index)?.type == Type.INT_TYPE) { + "int type expected, but ${current.getLocal(it.index)?.type} was found in basic frames" + } + + current.setLocal(it.index, BasicValue(it.value)) + } + + current + } + + return refinedFrames +} + +private fun AbstractInsnNode.isIntLoad() = opcode == Opcodes.ILOAD +private fun AbstractInsnNode.isIntStore() = opcode == Opcodes.ISTORE + +private fun checkUpdatedExpectedType(was: Type?, new: Type) { + assert(was == null || was == new) { + "Conflicting expected types: $was/$new" + } +} + +private class MySourceInterpreter : SourceInterpreter() { + override fun copyOperation(insn: AbstractInsnNode, value: SourceValue) = + when { + insn.isStoreOperation() || insn.isLoadOperation() -> SourceValue(value.size, insn) + // For DUP* instructions return the same value (effectively ignore DUP's) + else -> value + } +} + +private val REFINED_INT_SORTS = setOf(Type.BOOLEAN, Type.CHAR, Type.BYTE, Type.SHORT) + +private fun MethodNode.findContainingVariableFromTable(insn: AbstractInsnNode, varIndex: Int): LocalVariableNode? { + val insnIndex = instructions.indexOf(insn) + return localVariables.firstOrNull { + it.index == varIndex && it.rangeContainsInsn(insnIndex, instructions) + } +} + +private fun LocalVariableNode.rangeContainsInsn(insnIndex: Int, insnList: InsnList) = + insnList.indexOf(start) < insnIndex && insnIndex < insnList.indexOf(end) + +private class VarExpectedTypeFrame(maxLocals: Int) : VarFrame { + val expectedTypeByVarIndex = arrayOfNulls(maxLocals) + + override fun mergeFrom(other: VarExpectedTypeFrame) { + assert(expectedTypeByVarIndex.size == other.expectedTypeByVarIndex.size) { + "Other VarExpectedTypeFrame has different size: ${expectedTypeByVarIndex.size} / ${other.expectedTypeByVarIndex.size}" + } + + for ((varIndex, type) in other.expectedTypeByVarIndex.withIndex()) { + updateExpectedType(varIndex, type ?: continue) + } + } + + fun updateExpectedType(varIndex: Int, new: Type) { + val was = expectedTypeByVarIndex[varIndex] + // Widening to int is always allowed + if (new == Type.INT_TYPE) return + + checkUpdatedExpectedType(was, new) + + expectedTypeByVarIndex[varIndex] = new + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other?.javaClass != javaClass) return false + + other as VarExpectedTypeFrame + + if (!Arrays.equals(expectedTypeByVarIndex, other.expectedTypeByVarIndex)) return false + + return true + } + + override fun hashCode(): Int { + return Arrays.hashCode(expectedTypeByVarIndex) + } +} diff --git a/compiler/testData/codegen/box/coroutines/intLikeVarSpilling/complicatedMerge.kt b/compiler/testData/codegen/box/coroutines/intLikeVarSpilling/complicatedMerge.kt new file mode 100644 index 00000000000..8e8c9f5d9f1 --- /dev/null +++ b/compiler/testData/codegen/box/coroutines/intLikeVarSpilling/complicatedMerge.kt @@ -0,0 +1,29 @@ +class Controller { + suspend fun suspendHere(x: Continuation) { + x.resume(Unit) + } +} + +fun builder(coroutine c: Controller.() -> Continuation) { + c(Controller()).resume(Unit) +} + +fun foo() = true + +private var booleanResult = false +fun setBooleanRes(x: Boolean) { + booleanResult = x +} + +fun box(): String { + builder { + val x = true + val y = false + suspendHere() + setBooleanRes(if (foo()) x else y) + } + + if (!booleanResult) return "fail 1" + + return "OK" +} diff --git a/compiler/testData/codegen/box/coroutines/intLikeVarSpilling/i2bResult.kt b/compiler/testData/codegen/box/coroutines/intLikeVarSpilling/i2bResult.kt new file mode 100644 index 00000000000..f4c86ef86fd --- /dev/null +++ b/compiler/testData/codegen/box/coroutines/intLikeVarSpilling/i2bResult.kt @@ -0,0 +1,28 @@ +class Controller { + suspend fun suspendHere(x: Continuation) { + x.resume(Unit) + } +} + +fun builder(coroutine c: Controller.() -> Continuation) { + c(Controller()).resume(Unit) +} + +private var byteResult: Byte = 0 +fun setByteRes(x: Byte) { + byteResult = x +} + +fun foo(): Int = 1 + +fun box(): String { + builder { + val x: Byte = foo().toByte() + suspendHere() + setByteRes(x) + } + + if (byteResult != 1.toByte()) return "fail 1" + + return "OK" +} diff --git a/compiler/testData/codegen/box/coroutines/intLikeVarSpilling/loadFromBooleanArray.kt b/compiler/testData/codegen/box/coroutines/intLikeVarSpilling/loadFromBooleanArray.kt new file mode 100644 index 00000000000..f791030fd76 --- /dev/null +++ b/compiler/testData/codegen/box/coroutines/intLikeVarSpilling/loadFromBooleanArray.kt @@ -0,0 +1,27 @@ +class Controller { + suspend fun suspendHere(x: Continuation) { + x.resume(Unit) + } +} + +fun builder(coroutine c: Controller.() -> Continuation) { + c(Controller()).resume(Unit) +} + +private var booleanResult = false +fun setBooleanRes(x: Boolean) { + booleanResult = x +} + +fun box(): String { + builder { + val a = booleanArrayOf(true) + val x = a[0] + suspendHere() + setBooleanRes(x) + } + + if (!booleanResult) return "fail 1" + + return "OK" +} diff --git a/compiler/testData/codegen/box/coroutines/intLikeVarSpilling/loadFromByteArray.kt b/compiler/testData/codegen/box/coroutines/intLikeVarSpilling/loadFromByteArray.kt new file mode 100644 index 00000000000..888b7c76543 --- /dev/null +++ b/compiler/testData/codegen/box/coroutines/intLikeVarSpilling/loadFromByteArray.kt @@ -0,0 +1,27 @@ +class Controller { + suspend fun suspendHere(x: Continuation) { + x.resume(Unit) + } +} + +fun builder(coroutine c: Controller.() -> Continuation) { + c(Controller()).resume(Unit) +} + +private var byteResult: Byte = 0 +fun setByteRes(x: Byte) { + byteResult = x +} + +fun box(): String { + builder { + val a = byteArrayOf(1) + val x = a[0] + suspendHere() + setByteRes(x) + } + + if (byteResult != 1.toByte()) return "fail 1" + + return "OK" +} diff --git a/compiler/testData/codegen/box/coroutines/intLikeVarSpilling/noVariableInTable.kt b/compiler/testData/codegen/box/coroutines/intLikeVarSpilling/noVariableInTable.kt new file mode 100644 index 00000000000..2d4af13cabf --- /dev/null +++ b/compiler/testData/codegen/box/coroutines/intLikeVarSpilling/noVariableInTable.kt @@ -0,0 +1,27 @@ +class Controller { + suspend fun suspendHere(x: Continuation) { + x.resume(Unit) + } +} + +fun builder(coroutine c: Controller.() -> Continuation) { + c(Controller()).resume(Unit) +} + +private var booleanResult = false +fun setBooleanRes(x: Boolean, ignored: Unit) { + booleanResult = x +} + +fun box(): String { + builder { + // 'true' value is spilled into variable and saved to field before suspension point + // It's important that there is no type info about this variable in local var table, + // so we should infer that ICONST_1 is a boolean value from it's usage + setBooleanRes(true, suspendHere()) + } + + if (!booleanResult) return "fail 1" + + return "OK" +} diff --git a/compiler/testData/codegen/box/coroutines/intLikeVarSpilling/sameIconst1ManyVars.kt b/compiler/testData/codegen/box/coroutines/intLikeVarSpilling/sameIconst1ManyVars.kt new file mode 100644 index 00000000000..4c10a589253 --- /dev/null +++ b/compiler/testData/codegen/box/coroutines/intLikeVarSpilling/sameIconst1ManyVars.kt @@ -0,0 +1,30 @@ +class Controller { + suspend fun suspendHere(x: Continuation) { + x.resume(Unit) + } +} + +fun builder(coroutine c: Controller.() -> Continuation) { + c(Controller()).resume(Unit) +} + +private var result: String = "" +fun setRes(x: Byte, y: Int) { + result = "$x#$y" +} + +fun foo(): Int = 1 + +fun box(): String { + builder { + val x: Byte = 1 + // No actual cast happens here + val y: Int = x.toInt() + suspendHere() + setRes(x, y) + } + + if (result != "1#1") return "fail 1" + + return "OK" +} diff --git a/compiler/testData/codegen/box/coroutines/intLikeVarSpilling/usedInArrayStore.kt b/compiler/testData/codegen/box/coroutines/intLikeVarSpilling/usedInArrayStore.kt new file mode 100644 index 00000000000..866998b14c7 --- /dev/null +++ b/compiler/testData/codegen/box/coroutines/intLikeVarSpilling/usedInArrayStore.kt @@ -0,0 +1,74 @@ +// WITH_RUNTIME +class Controller { + suspend fun suspendHere(x: Continuation) { + x.resume(Unit) + } +} + +fun builder(coroutine c: Controller.() -> Continuation) { + c(Controller()).resume(Unit) +} + +@JvmField +var booleanResult = booleanArrayOf() +@JvmField +var charResult = charArrayOf() +@JvmField +var byteResult = byteArrayOf() +@JvmField +var shortResult = shortArrayOf() +@JvmField +var intResult = intArrayOf() + +fun box(): String { + builder { + val x = true + suspendHere() + val a = BooleanArray(1) + a[0] = x + booleanResult = a + } + + if (!booleanResult[0]) return "fail 1" + + builder { + val x = '1' + suspendHere() + val a = CharArray(1) + a[0] = x + charResult = a + } + + if (charResult[0] != '1') return "fail 2" + + builder { + val x: Byte = 1 + suspendHere() + val a = ByteArray(1) + a[0] = x + byteResult = a + } + + if (byteResult[0] != 1.toByte()) return "fail 3" + + builder { + val x: Short = 1 + suspendHere() + val a = ShortArray(1) + a[0] = x + shortResult = a + } + + if (shortResult[0] != 1.toShort()) return "fail 4" + + builder { + val x: Int = 1 + suspendHere() + val a = IntArray(1) + a[0] = x + intResult = a + } + + if (intResult[0] != 1) return "fail 5" + return "OK" +} diff --git a/compiler/testData/codegen/box/coroutines/intLikeVarSpilling/usedInMethodCall.kt b/compiler/testData/codegen/box/coroutines/intLikeVarSpilling/usedInMethodCall.kt new file mode 100644 index 00000000000..a9939ff9721 --- /dev/null +++ b/compiler/testData/codegen/box/coroutines/intLikeVarSpilling/usedInMethodCall.kt @@ -0,0 +1,77 @@ +class Controller { + suspend fun suspendHere(x: Continuation) { + x.resume(Unit) + } +} + +fun builder(coroutine c: Controller.() -> Continuation) { + c(Controller()).resume(Unit) +} + +private var booleanResult = false +fun setBooleanRes(x: Boolean) { + booleanResult = x +} + +private var charResult: Char = '0' +fun setCharRes(x: Char) { + charResult = x +} + +private var byteResult: Byte = 0 +fun setByteRes(x: Byte) { + byteResult = x +} + +private var shortResult: Short = 0 +fun setShortRes(x: Short) { + shortResult = x +} + +private var intResult: Int = 0 +fun setIntRes(x: Int) { + intResult = x +} + +fun box(): String { + builder { + val x = true + suspendHere() + setBooleanRes(x) + } + + if (!booleanResult) return "fail 1" + + builder { + val x = '1' + suspendHere() + setCharRes(x) + } + + if (charResult != '1') return "fail 2" + + builder { + val x: Byte = 1 + suspendHere() + setByteRes(x) + } + + if (byteResult != 1.toByte()) return "fail 3" + + builder { + val x: Short = 1 + suspendHere() + setShortRes(x) + } + + if (shortResult != 1.toShort()) return "fail 4" + + builder { + val x: Int = 1 + suspendHere() + setIntRes(x) + } + + if (intResult != 1) return "fail 5" + return "OK" +} diff --git a/compiler/testData/codegen/box/coroutines/intLikeVarSpilling/usedInPutfield.kt b/compiler/testData/codegen/box/coroutines/intLikeVarSpilling/usedInPutfield.kt new file mode 100644 index 00000000000..f3996b77b4e --- /dev/null +++ b/compiler/testData/codegen/box/coroutines/intLikeVarSpilling/usedInPutfield.kt @@ -0,0 +1,64 @@ +// WITH_RUNTIME +class Controller { + suspend fun suspendHere(x: Continuation) { + x.resume(Unit) + } +} + +fun builder(coroutine c: Controller.() -> Continuation) { + c(Controller()).resume(Unit) +} + +@JvmField +var booleanResult = false +@JvmField +var charResult: Char = '0' +@JvmField +var byteResult: Byte = 0 +@JvmField +var shortResult: Short = 0 +@JvmField +var intResult: Int = 0 + +fun box(): String { + builder { + val x = true + suspendHere() + booleanResult = x + } + + if (!booleanResult) return "fail 1" + + builder { + val x = '1' + suspendHere() + charResult = x + } + + if (charResult != '1') return "fail 2" + + builder { + val x: Byte = 1 + suspendHere() + byteResult = x + } + + if (byteResult != 1.toByte()) return "fail 3" + + builder { + val x: Short = 1 + suspendHere() + shortResult = x + } + + if (shortResult != 1.toShort()) return "fail 4" + + builder { + val x: Int = 1 + suspendHere() + intResult = x + } + + if (intResult != 1) return "fail 5" + return "OK" +} diff --git a/compiler/testData/codegen/box/coroutines/intLikeVarSpilling/usedInVarStore.kt b/compiler/testData/codegen/box/coroutines/intLikeVarSpilling/usedInVarStore.kt new file mode 100644 index 00000000000..2ddb6f1b790 --- /dev/null +++ b/compiler/testData/codegen/box/coroutines/intLikeVarSpilling/usedInVarStore.kt @@ -0,0 +1,53 @@ +class Controller { + suspend fun suspendHere(x: Continuation) { + x.resume(Unit) + } +} + +fun builder(coroutine c: Controller.() -> Continuation) { + c(Controller()).resume(Unit) +} + +fun box(): String { + builder { + val x = true + suspendHere() + val y: Boolean = x + if (!y) throw IllegalStateException("fail 1") + } + + builder { + val x = '1' + suspendHere() + + val y: Char = x + if (y != '1') throw IllegalStateException("fail 2") + } + + builder { + val x: Byte = 1 + suspendHere() + + val y: Byte = x + if (y != 1.toByte()) throw IllegalStateException("fail 3") + } + + builder { + val x: Short = 1 + + suspendHere() + + val y: Short = x + if (y != 1.toShort()) throw IllegalStateException("fail 4") + } + + builder { + val x: Int = 1 + suspendHere() + + val y: Int = x + if (y != 1) throw IllegalStateException("fail 5") + } + + return "OK" +} diff --git a/compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/complicatedMerge.kt b/compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/complicatedMerge.kt new file mode 100644 index 00000000000..91c1e73f246 --- /dev/null +++ b/compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/complicatedMerge.kt @@ -0,0 +1,32 @@ +class Controller { + suspend fun suspendHere(x: Continuation) { + x.resume(Unit) + } +} + +fun builder(coroutine c: Controller.() -> Continuation) { + c(Controller()).resume(Unit) +} + +fun foo() = true + +private var booleanResult = false +fun setBooleanRes(x: Boolean) { + booleanResult = x +} + +fun box(): String { + builder { + val x = true + val y = false + suspendHere() + setBooleanRes(if (foo()) x else y) + } + + if (!booleanResult) return "fail 1" + + return "OK" +} + +// 1 PUTFIELD .*\.Z\$0 : Z +// 1 PUTFIELD .*\.Z\$1 : Z diff --git a/compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/i2bResult.kt b/compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/i2bResult.kt new file mode 100644 index 00000000000..e418af58b4a --- /dev/null +++ b/compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/i2bResult.kt @@ -0,0 +1,30 @@ +class Controller { + suspend fun suspendHere(x: Continuation) { + x.resume(Unit) + } +} + +fun builder(coroutine c: Controller.() -> Continuation) { + c(Controller()).resume(Unit) +} + +private var byteResult: Byte = 0 +fun setByteRes(x: Byte) { + byteResult = x +} + +fun foo(): Int = 1 + +fun box(): String { + builder { + val x: Byte = foo().toByte() + suspendHere() + setByteRes(x) + } + + if (byteResult != 1.toByte()) return "fail 1" + + return "OK" +} + +// 1 PUTFIELD .*\.B\$0 : B diff --git a/compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/loadFromBooleanArray.kt b/compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/loadFromBooleanArray.kt new file mode 100644 index 00000000000..e355ea9b293 --- /dev/null +++ b/compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/loadFromBooleanArray.kt @@ -0,0 +1,29 @@ +class Controller { + suspend fun suspendHere(x: Continuation) { + x.resume(Unit) + } +} + +fun builder(coroutine c: Controller.() -> Continuation) { + c(Controller()).resume(Unit) +} + +private var booleanResult = false +fun setBooleanRes(x: Boolean) { + booleanResult = x +} + +fun box(): String { + builder { + val a = booleanArrayOf(true) + val x = a[0] + suspendHere() + setBooleanRes(x) + } + + if (!booleanResult) return "fail 1" + + return "OK" +} + +// 1 PUTFIELD .*\.Z\$0 : Z diff --git a/compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/loadFromByteArray.kt b/compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/loadFromByteArray.kt new file mode 100644 index 00000000000..d6836ba66ff --- /dev/null +++ b/compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/loadFromByteArray.kt @@ -0,0 +1,29 @@ +class Controller { + suspend fun suspendHere(x: Continuation) { + x.resume(Unit) + } +} + +fun builder(coroutine c: Controller.() -> Continuation) { + c(Controller()).resume(Unit) +} + +private var byteResult: Byte = 0 +fun setByteRes(x: Byte) { + byteResult = x +} + +fun box(): String { + builder { + val a = byteArrayOf(1) + val x = a[0] + suspendHere() + setByteRes(x) + } + + if (byteResult != 1.toByte()) return "fail 1" + + return "OK" +} + +// 1 PUTFIELD .*\.B\$0 : B diff --git a/compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/noVariableInTable.kt b/compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/noVariableInTable.kt new file mode 100644 index 00000000000..521f6ba6b01 --- /dev/null +++ b/compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/noVariableInTable.kt @@ -0,0 +1,29 @@ +class Controller { + suspend fun suspendHere(x: Continuation) { + x.resume(Unit) + } +} + +fun builder(coroutine c: Controller.() -> Continuation) { + c(Controller()).resume(Unit) +} + +private var booleanResult = false +fun setBooleanRes(x: Boolean, ignored: Unit) { + booleanResult = x +} + +fun box(): String { + builder { + // 'true' value is spilled into variable and saved to field before suspension point + // It's important that there is no type info about this variable in local var table, + // so we should infer that ICONST_1 is a boolean value from it's usage + setBooleanRes(true, suspendHere()) + } + + if (!booleanResult) return "fail 1" + + return "OK" +} + +// 1 PUTFIELD .*\.Z\$0 : Z diff --git a/compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/sameIconst1ManyVars.kt b/compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/sameIconst1ManyVars.kt new file mode 100644 index 00000000000..6fe01877767 --- /dev/null +++ b/compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/sameIconst1ManyVars.kt @@ -0,0 +1,33 @@ +class Controller { + suspend fun suspendHere(x: Continuation) { + x.resume(Unit) + } +} + +fun builder(coroutine c: Controller.() -> Continuation) { + c(Controller()).resume(Unit) +} + +private var result: String = "" +fun setRes(x: Byte, y: Int) { + result = "$x#$y" +} + +fun foo(): Int = 1 + +fun box(): String { + builder { + val x: Byte = 1 + // No actual cast happens here + val y: Int = x.toInt() + suspendHere() + setRes(x, y) + } + + if (result != "1#1") return "fail 1" + + return "OK" +} + +// 1 PUTFIELD .*\.B\$0 : B +// 1 PUTFIELD .*\.I\$0 : I diff --git a/compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/usedInArrayStore.kt b/compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/usedInArrayStore.kt new file mode 100644 index 00000000000..69e628ff83c --- /dev/null +++ b/compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/usedInArrayStore.kt @@ -0,0 +1,80 @@ +// WITH_RUNTIME +class Controller { + suspend fun suspendHere(x: Continuation) { + x.resume(Unit) + } +} + +fun builder(coroutine c: Controller.() -> Continuation) { + c(Controller()).resume(Unit) +} + +@JvmField +var booleanResult = booleanArrayOf() +@JvmField +var charResult = charArrayOf() +@JvmField +var byteResult = byteArrayOf() +@JvmField +var shortResult = shortArrayOf() +@JvmField +var intResult = intArrayOf() + +fun box(): String { + builder { + val x = true + suspendHere() + val a = BooleanArray(1) + a[0] = x + booleanResult = a + } + + if (!booleanResult[0]) return "fail 1" + + builder { + val x = '1' + suspendHere() + val a = CharArray(1) + a[0] = x + charResult = a + } + + if (charResult[0] != '1') return "fail 2" + + builder { + val x: Byte = 1 + suspendHere() + val a = ByteArray(1) + a[0] = x + byteResult = a + } + + if (byteResult[0] != 1.toByte()) return "fail 3" + + builder { + val x: Short = 1 + suspendHere() + val a = ShortArray(1) + a[0] = x + shortResult = a + } + + if (shortResult[0] != 1.toShort()) return "fail 4" + + builder { + val x: Int = 1 + suspendHere() + val a = IntArray(1) + a[0] = x + intResult = a + } + + if (intResult[0] != 1) return "fail 5" + return "OK" +} + +// 1 PUTFIELD .*\.B\$0 : B +// 1 PUTFIELD .*\.C\$0 : C +// 1 PUTFIELD .*\.S\$0 : S +// 1 PUTFIELD .*\.Z\$0 : Z +// 1 PUTFIELD .*\.I\$0 : I diff --git a/compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/usedInMethodCall.kt b/compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/usedInMethodCall.kt new file mode 100644 index 00000000000..29d2540f1d9 --- /dev/null +++ b/compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/usedInMethodCall.kt @@ -0,0 +1,83 @@ +class Controller { + suspend fun suspendHere(x: Continuation) { + x.resume(Unit) + } +} + +fun builder(coroutine c: Controller.() -> Continuation) { + c(Controller()).resume(Unit) +} + +private var booleanResult = false +fun setBooleanRes(x: Boolean) { + booleanResult = x +} + +private var charResult: Char = '0' +fun setCharRes(x: Char) { + charResult = x +} + +private var byteResult: Byte = 0 +fun setByteRes(x: Byte) { + byteResult = x +} + +private var shortResult: Short = 0 +fun setShortRes(x: Short) { + shortResult = x +} + +private var intResult: Int = 0 +fun setIntRes(x: Int) { + intResult = x +} + +fun box(): String { + builder { + val x = true + suspendHere() + setBooleanRes(x) + } + + if (!booleanResult) return "fail 1" + + builder { + val x = '1' + suspendHere() + setCharRes(x) + } + + if (charResult != '1') return "fail 2" + + builder { + val x: Byte = 1 + suspendHere() + setByteRes(x) + } + + if (byteResult != 1.toByte()) return "fail 3" + + builder { + val x: Short = 1 + suspendHere() + setShortRes(x) + } + + if (shortResult != 1.toShort()) return "fail 4" + + builder { + val x: Int = 1 + suspendHere() + setIntRes(x) + } + + if (intResult != 1) return "fail 5" + return "OK" +} + +// 1 PUTFIELD .*\.B\$0 : B +// 1 PUTFIELD .*\.C\$0 : C +// 1 PUTFIELD .*\.S\$0 : S +// 1 PUTFIELD .*\.Z\$0 : Z +// 1 PUTFIELD .*\.I\$0 : I diff --git a/compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/usedInPutfield.kt b/compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/usedInPutfield.kt new file mode 100644 index 00000000000..e2701da4815 --- /dev/null +++ b/compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/usedInPutfield.kt @@ -0,0 +1,70 @@ +// WITH_RUNTIME +class Controller { + suspend fun suspendHere(x: Continuation) { + x.resume(Unit) + } +} + +fun builder(coroutine c: Controller.() -> Continuation) { + c(Controller()).resume(Unit) +} + +@JvmField +var booleanResult = false +@JvmField +var charResult: Char = '0' +@JvmField +var byteResult: Byte = 0 +@JvmField +var shortResult: Short = 0 +@JvmField +var intResult: Int = 0 + +fun box(): String { + builder { + val x = true + suspendHere() + booleanResult = x + } + + if (!booleanResult) return "fail 1" + + builder { + val x = '1' + suspendHere() + charResult = x + } + + if (charResult != '1') return "fail 2" + + builder { + val x: Byte = 1 + suspendHere() + byteResult = x + } + + if (byteResult != 1.toByte()) return "fail 3" + + builder { + val x: Short = 1 + suspendHere() + shortResult = x + } + + if (shortResult != 1.toShort()) return "fail 4" + + builder { + val x: Int = 1 + suspendHere() + intResult = x + } + + if (intResult != 1) return "fail 5" + return "OK" +} + +// 1 PUTFIELD .*\.B\$0 : B +// 1 PUTFIELD .*\.C\$0 : C +// 1 PUTFIELD .*\.S\$0 : S +// 1 PUTFIELD .*\.Z\$0 : Z +// 1 PUTFIELD .*\.I\$0 : I diff --git a/compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/usedInVarStore.kt b/compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/usedInVarStore.kt new file mode 100644 index 00000000000..855669e9e63 --- /dev/null +++ b/compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/usedInVarStore.kt @@ -0,0 +1,59 @@ +class Controller { + suspend fun suspendHere(x: Continuation) { + x.resume(Unit) + } +} + +fun builder(coroutine c: Controller.() -> Continuation) { + c(Controller()).resume(Unit) +} + +fun box(): String { + builder { + val x = true + suspendHere() + val y: Boolean = x + if (!y) throw IllegalStateException("fail 1") + } + + builder { + val x = '1' + suspendHere() + + val y: Char = x + if (y != '1') throw IllegalStateException("fail 2") + } + + builder { + val x: Byte = 1 + suspendHere() + + val y: Byte = x + if (y != 1.toByte()) throw IllegalStateException("fail 3") + } + + builder { + val x: Short = 1 + + suspendHere() + + val y: Short = x + if (y != 1.toShort()) throw IllegalStateException("fail 4") + } + + builder { + val x: Int = 1 + suspendHere() + + val y: Int = x + if (y != 1) throw IllegalStateException("fail 5") + } + + return "OK" +} + +// 1 PUTFIELD .*\.B\$0 : B +// 1 PUTFIELD .*\.C\$0 : C +// 1 PUTFIELD .*\.S\$0 : S +// 1 PUTFIELD .*\.Z\$0 : Z +// 1 PUTFIELD .*\.I\$0 : I diff --git a/compiler/tests/org/jetbrains/kotlin/codegen/BlackBoxCodegenTestGenerated.java b/compiler/tests/org/jetbrains/kotlin/codegen/BlackBoxCodegenTestGenerated.java index 915acee0701..0410c7dff26 100644 --- a/compiler/tests/org/jetbrains/kotlin/codegen/BlackBoxCodegenTestGenerated.java +++ b/compiler/tests/org/jetbrains/kotlin/codegen/BlackBoxCodegenTestGenerated.java @@ -4557,6 +4557,75 @@ public class BlackBoxCodegenTestGenerated extends AbstractBlackBoxCodegenTest { String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/varValueConflictsWithTableSameSort.kt"); doTest(fileName); } + + @TestMetadata("compiler/testData/codegen/box/coroutines/intLikeVarSpilling") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class IntLikeVarSpilling extends AbstractBlackBoxCodegenTest { + public void testAllFilesPresentInIntLikeVarSpilling() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("compiler/testData/codegen/box/coroutines/intLikeVarSpilling"), Pattern.compile("^(.+)\\.kt$"), true); + } + + @TestMetadata("complicatedMerge.kt") + public void testComplicatedMerge() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/intLikeVarSpilling/complicatedMerge.kt"); + doTest(fileName); + } + + @TestMetadata("i2bResult.kt") + public void testI2bResult() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/intLikeVarSpilling/i2bResult.kt"); + doTest(fileName); + } + + @TestMetadata("loadFromBooleanArray.kt") + public void testLoadFromBooleanArray() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/intLikeVarSpilling/loadFromBooleanArray.kt"); + doTest(fileName); + } + + @TestMetadata("loadFromByteArray.kt") + public void testLoadFromByteArray() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/intLikeVarSpilling/loadFromByteArray.kt"); + doTest(fileName); + } + + @TestMetadata("noVariableInTable.kt") + public void testNoVariableInTable() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/intLikeVarSpilling/noVariableInTable.kt"); + doTest(fileName); + } + + @TestMetadata("sameIconst1ManyVars.kt") + public void testSameIconst1ManyVars() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/intLikeVarSpilling/sameIconst1ManyVars.kt"); + doTest(fileName); + } + + @TestMetadata("usedInArrayStore.kt") + public void testUsedInArrayStore() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/intLikeVarSpilling/usedInArrayStore.kt"); + doTest(fileName); + } + + @TestMetadata("usedInMethodCall.kt") + public void testUsedInMethodCall() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/intLikeVarSpilling/usedInMethodCall.kt"); + doTest(fileName); + } + + @TestMetadata("usedInPutfield.kt") + public void testUsedInPutfield() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/intLikeVarSpilling/usedInPutfield.kt"); + doTest(fileName); + } + + @TestMetadata("usedInVarStore.kt") + public void testUsedInVarStore() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/coroutines/intLikeVarSpilling/usedInVarStore.kt"); + doTest(fileName); + } + } } @TestMetadata("compiler/testData/codegen/box/dataClasses") diff --git a/compiler/tests/org/jetbrains/kotlin/codegen/BytecodeTextTestGenerated.java b/compiler/tests/org/jetbrains/kotlin/codegen/BytecodeTextTestGenerated.java index 8e96e9d7543..1f82cdc3d64 100644 --- a/compiler/tests/org/jetbrains/kotlin/codegen/BytecodeTextTestGenerated.java +++ b/compiler/tests/org/jetbrains/kotlin/codegen/BytecodeTextTestGenerated.java @@ -792,6 +792,75 @@ public class BytecodeTextTestGenerated extends AbstractBytecodeTextTest { String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/bytecodeText/coroutines/varValueConflictsWithTableSameSort.kt"); doTest(fileName); } + + @TestMetadata("compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class IntLikeVarSpilling extends AbstractBytecodeTextTest { + public void testAllFilesPresentInIntLikeVarSpilling() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling"), Pattern.compile("^(.+)\\.kt$"), true); + } + + @TestMetadata("complicatedMerge.kt") + public void testComplicatedMerge() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/complicatedMerge.kt"); + doTest(fileName); + } + + @TestMetadata("i2bResult.kt") + public void testI2bResult() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/i2bResult.kt"); + doTest(fileName); + } + + @TestMetadata("loadFromBooleanArray.kt") + public void testLoadFromBooleanArray() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/loadFromBooleanArray.kt"); + doTest(fileName); + } + + @TestMetadata("loadFromByteArray.kt") + public void testLoadFromByteArray() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/loadFromByteArray.kt"); + doTest(fileName); + } + + @TestMetadata("noVariableInTable.kt") + public void testNoVariableInTable() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/noVariableInTable.kt"); + doTest(fileName); + } + + @TestMetadata("sameIconst1ManyVars.kt") + public void testSameIconst1ManyVars() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/sameIconst1ManyVars.kt"); + doTest(fileName); + } + + @TestMetadata("usedInArrayStore.kt") + public void testUsedInArrayStore() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/usedInArrayStore.kt"); + doTest(fileName); + } + + @TestMetadata("usedInMethodCall.kt") + public void testUsedInMethodCall() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/usedInMethodCall.kt"); + doTest(fileName); + } + + @TestMetadata("usedInPutfield.kt") + public void testUsedInPutfield() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/usedInPutfield.kt"); + doTest(fileName); + } + + @TestMetadata("usedInVarStore.kt") + public void testUsedInVarStore() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/bytecodeText/coroutines/intLikeVarSpilling/usedInVarStore.kt"); + doTest(fileName); + } + } } @TestMetadata("compiler/testData/codegen/bytecodeText/deadCodeElimination")