[JVM IR] Use JVM8 support for unsigned int operations

- unmute tests
- add test to ensure JVM target is respected
- add test to cover smart-casted cases
- implement function matching and replacement
- Switching on uint constants
- introduce lowering for standard library replacements
This commit is contained in:
Kristoffer Andersen
2020-05-18 09:57:53 +02:00
committed by Dmitry Petrov
parent c95216dc5d
commit f0ff8f202c
19 changed files with 302 additions and 43 deletions
@@ -300,6 +300,8 @@ private val jvmFilePhases =
anonymousObjectSuperConstructorPhase then
tailrecPhase then
jvmStandardLibraryBuiltInsPhase then
forLoopsPhase then
jvmInlineClassPhase then
@@ -616,6 +616,52 @@ class JvmSymbols(
}
}
private val javaLangInteger: IrClassSymbol = createClass(FqName("java.lang.Integer")) { klass ->
klass.addFunction("compareUnsigned", irBuiltIns.intType, isStatic = true).apply {
addValueParameter("x", irBuiltIns.intType)
addValueParameter("y", irBuiltIns.intType)
}
klass.addFunction("divideUnsigned", irBuiltIns.intType, isStatic = true).apply {
addValueParameter("dividend", irBuiltIns.intType)
addValueParameter("divisor", irBuiltIns.intType)
}
klass.addFunction("remainderUnsigned", irBuiltIns.intType, isStatic = true).apply {
addValueParameter("dividend", irBuiltIns.intType)
addValueParameter("divisor", irBuiltIns.intType)
}
klass.addFunction("toUnsignedString", irBuiltIns.stringType, isStatic = true).apply {
addValueParameter("i", irBuiltIns.intType)
}
}
val compareUnsignedInt: IrSimpleFunctionSymbol = javaLangInteger.functionByName("compareUnsigned")
val divideUnsignedInt: IrSimpleFunctionSymbol = javaLangInteger.functionByName("divideUnsigned")
val remainderUnsignedInt: IrSimpleFunctionSymbol = javaLangInteger.functionByName("remainderUnsigned")
val toUnsignedStringInt: IrSimpleFunctionSymbol = javaLangInteger.functionByName("toUnsignedString")
private val javaLangLong: IrClassSymbol = createClass(FqName("java.lang.Long")) { klass ->
klass.addFunction("compareUnsigned", irBuiltIns.intType, isStatic = true).apply {
addValueParameter("x", irBuiltIns.longType)
addValueParameter("y", irBuiltIns.longType)
}
klass.addFunction("divideUnsigned", irBuiltIns.longType, isStatic = true).apply {
addValueParameter("dividend", irBuiltIns.longType)
addValueParameter("divisor", irBuiltIns.longType)
}
klass.addFunction("remainderUnsigned", irBuiltIns.longType, isStatic = true).apply {
addValueParameter("dividend", irBuiltIns.longType)
addValueParameter("divisor", irBuiltIns.longType)
}
klass.addFunction("toUnsignedString", irBuiltIns.stringType, isStatic = true).apply {
addValueParameter("i", irBuiltIns.longType)
}
}
val compareUnsignedLong: IrSimpleFunctionSymbol = javaLangLong.functionByName("compareUnsigned")
val divideUnsignedLong: IrSimpleFunctionSymbol = javaLangLong.functionByName("divideUnsigned")
val remainderUnsignedLong: IrSimpleFunctionSymbol = javaLangLong.functionByName("remainderUnsigned")
val toUnsignedStringLong: IrSimpleFunctionSymbol = javaLangLong.functionByName("toUnsignedString")
val systemArraycopy: IrSimpleFunctionSymbol = systemClass.functionByName("arraycopy")
val signatureStringIntrinsic: IrSimpleFunctionSymbol =
@@ -43,48 +43,125 @@ class SwitchGenerator(private val expression: IrWhen, private val data: BlockInf
val calls = callToLabels.map { it.call }
// Checks if all conditions are CALL EQEQ(tmp_variable, some_constant)
if (!areConstComparisons(calls))
return null
// To generate a switch from a when it must be a comparison of a single
// variable, the "subject", against a series of constants. We assume the
// subject is the left hand side of the first condition, provided the
// first condition is a comparison. If the first condition is of the form:
//
// CALL EQEQ(<unsafe-coerce><UInt, Int>(var),_)
//
// we must be trying to generate an _unsigned_ int switch, and need to
// account for unsafe-coerce in all comparisons that arise from the
// wrapping and unwrapping of the UInt inline class wrapper. Otherwise,
// this is a primitive Int or String switch, with a condition of the form
//
// CALL EQEQ(var,_)
// Subject should be the same for all conditions. Let's pick the first.
val subject = callToLabels[0].call.getValueArgument(0)!! as IrGetValue
val firstCondition = callToLabels[0].call
if (firstCondition.symbol != codegen.classCodegen.context.irBuiltIns.eqeqSymbol) return null
val subject = firstCondition.getValueArgument(0)
return when {
subject is IrCall && subject.isCoerceFromUIntToInt() ->
generateUIntSwitch(subject.getValueArgument(0)!! as? IrGetValue, calls, callToLabels, expressionToLabels, elseExpression)
subject is IrGetValue ->
generatePrimitiveSwitch(subject, calls, callToLabels, expressionToLabels, elseExpression)
else ->
null
}?.genOptimizedIfEnoughCases()
}
// Don't generate repeated cases, which are unreachable but allowed in Kotlin.
// Only keep the first encountered case:
val cases =
callToLabels.map { ValueToLabel((it.call.getValueArgument(1) as IrConst<*>).value, it.label) }.distinctBy { it.value }
fun IrCall.isCoerceFromUIntToInt(): Boolean =
symbol == codegen.classCodegen.context.ir.symbols.unsafeCoerceIntrinsic
&& getTypeArgument(0)?.isUInt() == true
&& getTypeArgument(1)?.isInt() == true
// Remove labels and "then expressions" that are not reachable.
val reachableLabels = HashSet(cases.map { it.label })
expressionToLabels.removeIf { it.label !in reachableLabels }
private fun generateUIntSwitch(
subject: IrGetValue?,
conditions: List<IrCall>,
callToLabels: ArrayList<CallToLabel>,
expressionToLabels: ArrayList<ExpressionToLabel>,
elseExpression: IrExpression?
): Switch? {
if (subject == null) return null
// We check that all conditions are of the form
// CALL EQEQ (<unsafe-coerce><UInt,Int>(subject),
// <unsafe-coerce><UInt,Int>( Constant ))
if (!areConstUIntComparisons(conditions)) return null
// Filter repeated cases. Allowed in Kotlin but unreachable.
val cases = callToLabels.map {
val constCoercion = it.call.getValueArgument(1)!! as IrCall
val constValue = (constCoercion.getValueArgument(0) as IrConst<*>).value
ValueToLabel(
constValue,
it.label
)
}.distinctBy { it.value }
expressionToLabels.removeUnreachableLabels(cases)
return IntSwitch(
subject,
elseExpression,
expressionToLabels,
cases
)
}
private fun generatePrimitiveSwitch(
subject: IrGetValue,
conditions: List<IrCall>,
callToLabels: ArrayList<CallToLabel>,
expressionToLabels: ArrayList<ExpressionToLabel>,
elseExpression: IrExpression?
): Switch? {
// Checks if all conditions are CALL EQEQ(var,constant)
if (!areConstantComparisons(conditions)) return null
return when {
areConstIntComparisons(calls) ->
areConstIntComparisons(conditions) -> {
val cases = extractSwitchCasesAndFilterUnreachableLabels(callToLabels, expressionToLabels)
IntSwitch(
subject,
elseExpression,
expressionToLabels,
cases
)
areConstStringComparisons(calls) ->
}
areConstStringComparisons(conditions) -> {
val cases = extractSwitchCasesAndFilterUnreachableLabels(callToLabels, expressionToLabels)
StringSwitch(
subject,
elseExpression,
expressionToLabels,
cases
)
else -> null // TODO: Enum, etc.
}?.genOptimizedIfEnoughCases()
}
else ->
null
}
}
private fun areConstComparisons(conditions: List<IrCall>): Boolean {
// All branches must be CALL 'EQEQ(Any?, Any?)': Boolean
if (conditions.any { it.symbol != codegen.classCodegen.context.irBuiltIns.eqeqSymbol })
return false
// Check that all conditions are of the form
//
// CALL EQEQ (<unsafe-coerce><UInt,Int>(subject), <unsafe-coerce><UInt,Int>( Constant ))
//
// where subject is taken to be the first variable compared on the left hand side, if any.
private fun areConstUIntComparisons(conditions: List<IrCall>): Boolean {
val lhs = conditions.map { it.takeIf { it.symbol == codegen.context.irBuiltIns.eqeqSymbol }?.getValueArgument(0) as? IrCall }
if (lhs.any { it == null || !it.isCoerceFromUIntToInt() }) return false
val lhsVariableAccesses = lhs.map { it!!.getValueArgument(0) as? IrGetValue }
if (lhsVariableAccesses.any { it == null || it.symbol != lhsVariableAccesses[0]!!.symbol }) return false
// All LHS refer to the same tmp variable.
val lhs = conditions.map { it.getValueArgument(0) as? IrGetValue }
val rhs = conditions.map { it.getValueArgument(1) as? IrCall }
if (rhs.any { it == null || !it.isCoerceFromUIntToInt() || it.getValueArgument(0) !is IrConst<*> }) return false
return true
}
private fun areConstantComparisons(conditions: List<IrCall>): Boolean {
// All conditions are equality checks && all LHS refer to the same tmp variable.
val lhs = conditions.map { it.takeIf { it.symbol == codegen.context.irBuiltIns.eqeqSymbol }?.getValueArgument(0) as? IrGetValue }
if (lhs.any { it == null || it.symbol != lhs[0]!!.symbol })
return false
@@ -122,6 +199,25 @@ class SwitchGenerator(private val expression: IrWhen, private val data: BlockInf
return true
}
private fun extractSwitchCasesAndFilterUnreachableLabels(
callToLabels: List<CallToLabel>,
expressionToLabels: ArrayList<ExpressionToLabel>
): List<ValueToLabel> {
// Don't generate repeated cases, which are unreachable but allowed in Kotlin.
// Only keep the first encountered case:
val cases =
callToLabels.map { ValueToLabel((it.call.getValueArgument(1) as IrConst<*>).value, it.label) }.distinctBy { it.value }
expressionToLabels.removeUnreachableLabels(cases)
return cases
}
private fun ArrayList<ExpressionToLabel>.removeUnreachableLabels(cases: List<ValueToLabel>) {
val reachableLabels = HashSet(cases.map { it.label })
removeIf { it.label !in reachableLabels }
}
// psi2ir lowers multiple cases to nested conditions. For example,
//
// when (subject) {
@@ -53,7 +53,8 @@ val jvmInlineClassPhase = makeIrFilePhase(
name = "Inline Classes",
description = "Lower inline classes",
// forLoopsPhase may produce UInt and ULong which are inline classes.
prerequisite = setOf(forLoopsPhase)
// standard library replacements are done on the unmangled names for UInt and ULong classes.
prerequisite = setOf(forLoopsPhase, jvmStandardLibraryBuiltInsPhase)
)
/**
@@ -46,7 +46,7 @@ class JvmOptimizationLowering(val context: JvmBackendContext) : FileLoweringPass
dispatchReceiverParameter != null
private fun getOperandsIfCallToEqeqOrEquals(call: IrCall): Pair<IrExpression, IrExpression>? =
private fun getOperandsIfCallToEQEQOrEquals(call: IrCall): Pair<IrExpression, IrExpression>? =
when {
call.symbol == context.irBuiltIns.eqeqSymbol -> {
val left = call.getValueArgument(0)!!
@@ -103,7 +103,7 @@ class JvmOptimizationLowering(val context: JvmBackendContext) : FileLoweringPass
return (expression.dispatchReceiver as IrCall).dispatchReceiver!!
}
getOperandsIfCallToEqeqOrEquals(expression)?.let { (left, right) ->
getOperandsIfCallToEQEQOrEquals(expression)?.let { (left, right) ->
return when {
left.isNullConst() && right.isNullConst() ->
IrConstImpl.constTrue(expression.startOffset, expression.endOffset, context.irBuiltIns.booleanType)
@@ -0,0 +1,90 @@
/*
* Copyright 2010-2020 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.phaser.makeIrFilePhase
import org.jetbrains.kotlin.backend.jvm.JvmBackendContext
import org.jetbrains.kotlin.config.JvmTarget
import org.jetbrains.kotlin.ir.declarations.IrFile
import org.jetbrains.kotlin.ir.expressions.IrCall
import org.jetbrains.kotlin.ir.expressions.IrExpression
import org.jetbrains.kotlin.ir.expressions.impl.IrCallImpl
import org.jetbrains.kotlin.ir.symbols.IrFunctionSymbol
import org.jetbrains.kotlin.ir.types.IrType
import org.jetbrains.kotlin.ir.util.fqNameForIrSerialization
import org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid
internal val jvmStandardLibraryBuiltInsPhase = makeIrFilePhase(
::JvmStandardLibraryBuiltInsLowering,
name = "JvmStandardLibraryBuiltInsLowering",
description = "Use Java Standard Library implementations of built-ins"
)
class JvmStandardLibraryBuiltInsLowering(val context: JvmBackendContext) : FileLoweringPass {
override fun lower(irFile: IrFile) {
if (context.state.target < JvmTarget.JVM_1_8) return
val transformer = object : IrElementTransformerVoid() {
override fun visitCall(expression: IrCall): IrExpression {
expression.transformChildren(this, null)
val parentClass = expression.symbol.owner.parent.fqNameForIrSerialization.asString()
val functionName = expression.symbol.owner.name.asString()
Jvm8builtInReplacements[parentClass to functionName]?.let {
return expression.replaceWithCallTo(it)
}
return expression
}
}
irFile.transformChildren(transformer, null)
}
private val Jvm8builtInReplacements = mapOf(
("kotlin.UInt" to "compareTo") to context.ir.symbols.compareUnsignedInt,
("kotlin.UInt" to "div") to context.ir.symbols.divideUnsignedInt,
("kotlin.UInt" to "rem") to context.ir.symbols.remainderUnsignedInt,
("kotlin.UInt" to "toString") to context.ir.symbols.toUnsignedStringInt,
("kotlin.ULong" to "compareTo") to context.ir.symbols.compareUnsignedLong,
("kotlin.ULong" to "div") to context.ir.symbols.divideUnsignedLong,
("kotlin.ULong" to "rem") to context.ir.symbols.remainderUnsignedLong,
("kotlin.ULong" to "toString") to context.ir.symbols.toUnsignedStringLong
)
// Originals are so far only instance methods, and the replacements are
// statics, so we copy dispatch receivers to a value argument if needed.
private fun IrCall.replaceWithCallTo(replacement: IrFunctionSymbol) =
IrCallImpl(
startOffset,
endOffset,
type,
replacement
).also { newCall ->
var valueArgumentOffset = 0
this.dispatchReceiver?.let {
newCall.putValueArgument(valueArgumentOffset, it.coerceTo(replacement.owner.valueParameters[valueArgumentOffset].type))
valueArgumentOffset++
}
(0 until valueArgumentsCount).forEach {
newCall.putValueArgument(it + valueArgumentOffset, getValueArgument(it)!!.coerceTo(replacement.owner.valueParameters[it].type))
}
}
private fun IrExpression.coerceTo(target: IrType): IrExpression =
IrCallImpl(
startOffset,
endOffset,
target,
context.ir.symbols.unsafeCoerceIntrinsic
).also { call ->
call.putTypeArgument(0, type)
call.putTypeArgument(1, target)
call.putValueArgument(0, this)
}
}
@@ -0,0 +1,15 @@
// WITH_RUNTIME
val ua = 1234U
val ub = 5678U
fun box(): String {
if (ua.compareTo(ub) > 0) {
throw AssertionError()
}
return "OK"
}
// 1 kotlin/UnsignedKt.uintCompare
// 0 INVOKESTATIC java/lang/Integer.compareUnsigned \(II\)I
@@ -1,7 +1,5 @@
// JVM_TARGET: 1.8
// WITH_RUNTIME
// IGNORE_BACKEND: JVM_IR
// TODO KT-36838 Use potentially intrinsified methods for unsigned available in JDK 1.8+ in JVM_IR
val ua = 1234U
val ub = 5678U
@@ -1,7 +1,5 @@
// JVM_TARGET: 1.8
// WITH_RUNTIME
// IGNORE_BACKEND: JVM_IR
// TODO KT-36838 Use potentially intrinsified methods for unsigned available in JDK 1.8+ in JVM_IR
val ua = 1234U
val ub = 5678U
@@ -1,7 +1,5 @@
// JVM_TARGET: 1.8
// WITH_RUNTIME
// IGNORE_BACKEND: JVM_IR
// TODO KT-36838 Use potentially intrinsified methods for unsigned available in JDK 1.8+ in JVM_IR
val ua = 1234U
val ub = 5678U
@@ -0,0 +1,7 @@
// JVM_TARGET: 1.8
// WITH_RUNTIME
fun both(a: Any?, b: Any?) = if (a is UInt && b is UInt) a < b else null!!
// 1 compareUnsigned
// 0 uintCompare
@@ -1,7 +1,5 @@
// JVM_TARGET: 1.8
// WITH_RUNTIME
// IGNORE_BACKEND: JVM_IR
// TODO KT-36838 Use potentially intrinsified methods for unsigned available in JDK 1.8+ in JVM_IR
fun box(): String {
val min = 0U.toString()
@@ -1,7 +1,5 @@
// JVM_TARGET: 1.8
// WITH_RUNTIME
// IGNORE_BACKEND: JVM_IR
// TODO KT-36838 Use potentially intrinsified methods for unsigned available in JDK 1.8+ in JVM_IR
val ua = 1234UL
val ub = 5678UL
@@ -1,7 +1,5 @@
// JVM_TARGET: 1.8
// WITH_RUNTIME
// IGNORE_BACKEND: JVM_IR
// TODO KT-36838 Use potentially intrinsified methods for unsigned available in JDK 1.8+ in JVM_IR
val ua = 1234UL
val ub = 5678UL
@@ -1,7 +1,5 @@
// JVM_TARGET: 1.8
// WITH_RUNTIME
// IGNORE_BACKEND: JVM_IR
// TODO KT-36838 Use potentially intrinsified methods for unsigned available in JDK 1.8+ in JVM_IR
val ua = 1234UL
val ub = 5678UL
@@ -1,7 +1,5 @@
// JVM_TARGET: 1.8
// WITH_RUNTIME
// IGNORE_BACKEND: JVM_IR
// TODO KT-36838 Use potentially intrinsified methods for unsigned available in JDK 1.8+ in JVM_IR
fun box(): String {
val min = 0UL.toString()
@@ -1,6 +1,4 @@
// WITH_RUNTIME
// IGNORE_BACKEND: JVM_IR
// TODO KT-36839 Generate 'when' with unsigned subject using TABLESWITCH/LOOKUPSWITCH in JVM_IR
const val M1: UInt = 2147483648u
@@ -4483,6 +4483,11 @@ public class BytecodeTextTestGenerated extends AbstractBytecodeTextTest {
KotlinTestUtils.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("compiler/testData/codegen/bytecodeText/unsignedTypes"), Pattern.compile("^(.+)\\.kt$"), null, TargetBackend.JVM, true);
}
@TestMetadata("unsignedIntCompare_before.kt")
public void testUnsignedIntCompare_before() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/unsignedTypes/unsignedIntCompare_before.kt");
}
@TestMetadata("unsignedIntCompare_jvm18.kt")
public void testUnsignedIntCompare_jvm18() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/unsignedTypes/unsignedIntCompare_jvm18.kt");
@@ -4498,6 +4503,11 @@ public class BytecodeTextTestGenerated extends AbstractBytecodeTextTest {
runTest("compiler/testData/codegen/bytecodeText/unsignedTypes/unsignedIntRemainder_jvm18.kt");
}
@TestMetadata("unsignedIntSmartCasts_jvm18.kt")
public void testUnsignedIntSmartCasts_jvm18() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/unsignedTypes/unsignedIntSmartCasts_jvm18.kt");
}
@TestMetadata("unsignedIntToString_jvm18.kt")
public void testUnsignedIntToString_jvm18() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/unsignedTypes/unsignedIntToString_jvm18.kt");
@@ -4441,6 +4441,11 @@ public class IrBytecodeTextTestGenerated extends AbstractIrBytecodeTextTest {
KotlinTestUtils.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("compiler/testData/codegen/bytecodeText/unsignedTypes"), Pattern.compile("^(.+)\\.kt$"), null, TargetBackend.JVM_IR, true);
}
@TestMetadata("unsignedIntCompare_before.kt")
public void testUnsignedIntCompare_before() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/unsignedTypes/unsignedIntCompare_before.kt");
}
@TestMetadata("unsignedIntCompare_jvm18.kt")
public void testUnsignedIntCompare_jvm18() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/unsignedTypes/unsignedIntCompare_jvm18.kt");
@@ -4456,6 +4461,11 @@ public class IrBytecodeTextTestGenerated extends AbstractIrBytecodeTextTest {
runTest("compiler/testData/codegen/bytecodeText/unsignedTypes/unsignedIntRemainder_jvm18.kt");
}
@TestMetadata("unsignedIntSmartCasts_jvm18.kt")
public void testUnsignedIntSmartCasts_jvm18() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/unsignedTypes/unsignedIntSmartCasts_jvm18.kt");
}
@TestMetadata("unsignedIntToString_jvm18.kt")
public void testUnsignedIntToString_jvm18() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/unsignedTypes/unsignedIntToString_jvm18.kt");