Use while loop for progressions that cannot overflow (instead of

do-while with enclosing "not empty" check).

Also do not add additional "not empty" condition for `until` loops when
the given bound is a constant != MIN_VALUE.
This commit is contained in:
Mark Punzalan
2019-03-29 10:33:30 -07:00
committed by max-kammerer
parent ba0e016c4e
commit 7680e7fd56
25 changed files with 403 additions and 94 deletions
@@ -23,6 +23,7 @@ import org.jetbrains.kotlin.ir.symbols.IrVariableSymbol
import org.jetbrains.kotlin.ir.types.getClass
import org.jetbrains.kotlin.ir.types.toKotlinType
import org.jetbrains.kotlin.ir.util.functions
import org.jetbrains.kotlin.ir.util.patchDeclarationParents
import org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid
import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid
@@ -39,41 +40,50 @@ val forLoopsPhase = makeIrFilePhase(
* a simple while loop over primitive induction variable.
*
* For example, this loop:
*
* ```
* for (i in first..last step foo) { ... }
* for (i in first..last) { // Do something with i }
* ```
*
* is represented in IR in such a manner:
* ```
* val it = (first..last).iterator()
* while (it.hasNext()) {
* val i = it.next()
* // Do something with i
* }
* ```
* We transform it into one of the following loops:
*
* ```
* val it = (first..last step foo).iterator()
* while (it.hasNext()) {
* val i = it.next()
* ...
* }
* ```
* // 1. If the induction variable cannot overflow, i.e., `last` is const and != MAX_VALUE (if increasing, or MIN_VALUE if decreasing),
* // OR if loop is an until loop (e.g., `for (i in first until last)`):
*
* We transform it into the following loop:
* var inductionVar = first
* while (inductionVar <= last) { // (inductionVar >= last if the progression is decreasing)
* val i = inductionVar
* inductionVar++
* // Do something with i
* }
*
* ```
* var it = first
* if (it <= last) { // (it >= last if the progression is decreasing)
* do {
* val i = it++
* ...
* } while (i != last)
* }
* ```
*
* In case of iteration over array we transform it into following:
* // 2. If the induction variable CAN overflow, i.e., `last` is not const or is MAX/MIN_VALUE:
*
* var inductionVar = first
* if (inductionVar <= last) { // (inductionVar >= last if the progression is decreasing)
* // Loop is not empty
* do {
* val i = inductionVar
* inductionVar++
* // Do something with i
* } while (i != last)
* }
* ```
* while (it <= array.size - 1) {
* val i = array[it]
* it++
* ...
* }
* In case of iteration over an array, we transform it into the following:
* ```
* var inductionVar = 0
* val last = array.size - 1
* while (inductionVar <= last) {
* val i = array[inductionVar++]
* // Do something with i
* }
* ```
*/
internal class ForLoopsLowering(val context: CommonBackendContext) : FileLoweringPass {
@@ -202,17 +212,13 @@ private class RangeLoopTransformer(
// The "next" statement (at the top of the loop):
//
// ```
// val i = it.next()
// ```
// val i = it.next()
//
// ...is lowered into:
//
// ```
// val i = initialValue() // `inductionVariable` for progressions
// // `array[inductionVariable]` for arrays
// inductionVariable = inductionVariable + step
// ```
// val i = initializeLoopVariable() // `inductionVariable` for progressions
// // `array[inductionVariable]` for arrays
// inductionVariable = inductionVariable + step
return with(context.createIrBuilder(getScopeOwnerSymbol(), initializer.startOffset, initializer.endOffset)) {
variable.initializer = forLoopInfo.initializeLoopVariable(symbols, this)
val plusFun = forLoopInfo.inductionVariable.type.getClass()!!.functions.first {
@@ -65,7 +65,7 @@ internal sealed class HeaderInfo(
) {
val direction: ProgressionDirection by lazy {
// If step is a constant (either Int or Long), then we can determine the direction.
val stepValue = (step as? IrConst<*>)?.value as? Number?
val stepValue = (step as? IrConst<*>)?.value as? Number
val stepValueAsLong = stepValue?.toLong()
when {
stepValueAsLong == null -> ProgressionDirection.UNKNOWN
@@ -82,9 +82,42 @@ internal class ProgressionHeaderInfo(
first: IrExpression,
last: IrExpression,
step: IrExpression,
canOverflow: Boolean? = null,
val additionalVariables: List<IrVariable> = listOf(),
val additionalNotEmptyCondition: IrExpression? = null
) : HeaderInfo(progressionType, first, last, step)
) : HeaderInfo(progressionType, first, last, step) {
private val _canOverflow: Boolean? = canOverflow
val canOverflow: Boolean by lazy {
if (_canOverflow != null) return@lazy _canOverflow
// Induction variable can overflow if it is not a const, or is MAX/MIN_VALUE (depending on direction).
val lastValue = (last as? IrConst<*>)?.value
val lastValueAsLong = when (lastValue) {
is Number -> lastValue.toLong()
is Char -> lastValue.toLong()
else -> return@lazy true // If "last" is not a const Number or Char.
}
val constLimitAsLong = when (direction) {
ProgressionDirection.UNKNOWN ->
// If we don't know the direction, we can't be sure which limit to use.
return@lazy true
ProgressionDirection.DECREASING ->
when (progressionType) {
ProgressionType.INT_PROGRESSION -> Int.MIN_VALUE.toLong()
ProgressionType.CHAR_PROGRESSION -> Char.MIN_VALUE.toLong()
ProgressionType.LONG_PROGRESSION -> Long.MIN_VALUE
}
ProgressionDirection.INCREASING ->
when (progressionType) {
ProgressionType.INT_PROGRESSION -> Int.MAX_VALUE.toLong()
ProgressionType.CHAR_PROGRESSION -> Char.MAX_VALUE.toLong()
ProgressionType.LONG_PROGRESSION -> Long.MAX_VALUE
}
}
constLimitAsLong == lastValueAsLong
}
}
/** Information about a for-loop over an array. The internal induction variable used is an Int. */
internal class ArrayHeaderInfo(
@@ -66,24 +66,36 @@ internal class ProgressionLoopHeader(
get() = headerInfo.additionalVariables + listOf(inductionVariable, last, step)
override fun buildInnerLoop(builder: DeclarationIrBuilder, loop: IrLoop, newBody: IrExpression?): IrLoop = with(builder) {
// Condition: loopVariable != last
assert(loopVariable != null)
val booleanNotFun = context.irBuiltIns.booleanClass.functions.first { it.owner.name.asString() == "not" }
val newCondition = irCallOp(booleanNotFun, booleanNotFun.owner.returnType, irCall(context.irBuiltIns.eqeqSymbol).apply {
putValueArgument(0, irGet(loopVariable!!))
putValueArgument(1, irGet(last))
})
// TODO: Build while loop (instead of do-while) where possible, e.g., in "until" ranges, or when "last" is known to be < MAX_VALUE
IrDoWhileLoopImpl(loop.startOffset, loop.endOffset, loop.type, loop.origin).apply {
label = loop.label
condition = newCondition
body = newBody
if (headerInfo.canOverflow) {
// Condition: loopVariable != last. We cannot use the induction variable because it can overflow.
assert(loopVariable != null)
val booleanNotFun = context.irBuiltIns.booleanClass.functions.first { it.owner.name.asString() == "not" }
val newCondition = irCallOp(booleanNotFun, booleanNotFun.owner.returnType, irCall(context.irBuiltIns.eqeqSymbol).apply {
putValueArgument(0, irGet(loopVariable!!))
putValueArgument(1, irGet(last))
})
IrDoWhileLoopImpl(loop.startOffset, loop.endOffset, loop.type, loop.origin).apply {
label = loop.label
condition = newCondition
body = newBody
}
} else {
// If the induction variable cannot overflow, use a while loop using the "not empty" condition.
val newCondition = buildNotEmptyCondition(this@with)
IrWhileLoopImpl(loop.startOffset, loop.endOffset, loop.type, loop.origin).apply {
label = loop.label
condition = newCondition
body = newBody
}
}
}
// If the induction variable cannot overflow, we do NOT need the enclosing "not empty" check because the for-loop is lowered into a
// while loop that uses the "not empty" condition (see buildInnerLoop()); the enclosing check would be redundant.
override fun buildNotEmptyConditionIfNecessary(builder: DeclarationIrBuilder): IrExpression? =
if (headerInfo.canOverflow) buildNotEmptyCondition(builder) else null
private fun buildNotEmptyCondition(builder: DeclarationIrBuilder): IrExpression =
with(builder) {
val builtIns = context.irBuiltIns
val progressionKotlinType = progressionType.elementType(builtIns).toKotlinType()
@@ -158,7 +170,7 @@ internal class ArrayLoopHeader(
get() = listOf(headerInfo.arrayVariable, inductionVariable, last, step)
override fun buildInnerLoop(builder: DeclarationIrBuilder, loop: IrLoop, newBody: IrExpression?): IrLoop = with(builder) {
// Condition: loopVariable != last
// Condition: inductionVariable != last
val builtIns = context.irBuiltIns
val callee = builtIns.lessOrEqualFunByOperandType[builtIns.int]!!
val newCondition = irCall(callee).apply {
@@ -17,6 +17,7 @@ import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin
import org.jetbrains.kotlin.ir.declarations.IrFunction
import org.jetbrains.kotlin.ir.descriptors.IrBuiltIns
import org.jetbrains.kotlin.ir.expressions.IrCall
import org.jetbrains.kotlin.ir.expressions.IrConst
import org.jetbrains.kotlin.ir.expressions.IrExpression
import org.jetbrains.kotlin.ir.expressions.IrStatementOrigin
import org.jetbrains.kotlin.ir.types.getClass
@@ -79,42 +80,65 @@ internal class UntilHandler(private val context: CommonBackendContext, private v
override fun build(call: IrCall, data: ProgressionType): HeaderInfo? =
with(context.createIrBuilder(call.symbol, call.startOffset, call.endOffset)) {
// `last = bound - 1` for the loop `for (i in first until bound)`.
val bound = scope.createTemporaryVariable(
ensureNotNullable(
call.getValueArgument(0)!!.castIfNecessary(
data.elementType(context.irBuiltIns),
data.elementCastFunctionName
)
), nameHint = "bound",
origin = IrDeclarationOrigin.FOR_LOOP_IMPLICIT_VARIABLE
val boundArg = call.getValueArgument(0)!!
val bound = ensureNotNullable(
boundArg.castIfNecessary(
data.elementType(context.irBuiltIns),
data.elementCastFunctionName
)
)
val decFun = data.decFun(context.irBuiltIns)
val last = irCallOp(decFun.symbol, bound.type, irGet(bound))
// `bound` may be needed for an additional condition to the emptiness check (see comments below). If so, store `bound` in a
// temporary variable as it may be an expression with side-effects and we should only evaluate it once.
val boundVar = if (needsMinValueCondition(data, boundArg)) scope.createTemporaryVariable(
bound,
nameHint = "bound",
origin = IrDeclarationOrigin.FOR_LOOP_IMPLICIT_VARIABLE
) else null
// The default "not empty" check cannot be used for the required for the corner case:
// `last = bound - 1` for the loop `for (i in first until bound)`.
val decFun = data.decFun(context.irBuiltIns)
val last = irCallOp(decFun.symbol, bound.type, if (boundVar != null) irGet(boundVar) else bound)
// The default "not empty" check cannot be used for the corner case:
//
// for (i in a until MIN_VALUE) {}
//
// ...which should always be considered an empty range. When the given bound is MIN_VALUE, and because `last = bound - 1`,
// "last" will underflow to MAX_VALUE, therefore the default "not empty" check:
//
// if (first <= last) { /* loop */ }
// if (inductionVar <= last) { /* loop */ }
//
// ...will always be true and won't consider the range as empty. Therefore, we need to add an additional condition to the
// emptiness check so that it becomes:
// "not empty" check so that it becomes:
//
// if (first <= last && bound > MIN_VALUE) { /* loop */ }
// if (inductionVar <= last && bound > MIN_VALUE) { /* loop */ }
ProgressionHeaderInfo(
data,
first = call.extensionReceiver!!,
last = last,
step = irInt(1),
additionalVariables = listOf(bound),
additionalNotEmptyCondition = buildMinValueCondition(data, irGet(bound))
canOverflow = false,
additionalVariables = listOfNotNull(boundVar),
additionalNotEmptyCondition = if (boundVar != null) buildMinValueCondition(data, irGet(boundVar)) else null
)
}
/** Returns true (i.e., min value condition is needed) if `bound` is non-const OR is MIN_VALUE. */
private fun needsMinValueCondition(progressionType: ProgressionType, bound: IrExpression): Boolean {
val boundValue = (bound as? IrConst<*>)?.value
val boundValueAsLong = when (boundValue) {
is Number -> boundValue.toLong()
is Char -> boundValue.toLong()
else -> return true // If "bound" is not a const Number or Char.
}
val minValueAsLong = when (progressionType) {
ProgressionType.INT_PROGRESSION -> Int.MIN_VALUE.toLong()
ProgressionType.CHAR_PROGRESSION -> Char.MIN_VALUE.toLong()
ProgressionType.LONG_PROGRESSION -> Long.MIN_VALUE
}
return minValueAsLong == boundValueAsLong
}
private fun DeclarationIrBuilder.buildMinValueCondition(progressionType: ProgressionType, bound: IrExpression): IrExpression {
val irBuiltIns = context.irBuiltIns
val minConst = when (progressionType) {
@@ -154,7 +178,8 @@ internal class IndicesHandler(val context: CommonBackendContext) : ProgressionHa
data,
first = irInt(0),
last = last,
step = irInt(1)
step = irInt(1),
canOverflow = false // Cannot overflow because `last` is at most MAX_VALUE - 1
)
}
}