JS IR: make CodeCleaner more conservative

The CodeCleaner treated all expressions with type `Nothing` as
non-terminating. This is not true for cases when return type is generic,
e.g:

```
fun <T> foo(): T = Any() as T

foo<Nothing>() // type: Nothing
```

This change makes the CodeCleaner more conservative, so that it doesn't
treat to such functions as non-terminating. This eliminates the need
to change call types from Nothing to Unit in
KotlinNothingValueExceptionLowering.
This commit is contained in:
Anton Bannykh
2020-04-30 18:42:35 +03:00
parent 8809abdbac
commit b5f31a3d76
2 changed files with 26 additions and 24 deletions
@@ -38,36 +38,14 @@ class KotlinNothingValueExceptionLowering(
// call ThrowKotlinNothingValueException(): Nothing
// }: Nothing
//
// Changing type of 'foo' to 'kotlin.Unit' is requires so that the 'ThrowKotlinNothingValueException(): Nothing'
// is not considered dead code and is not removed.
// Note that type 'kotlin.Nothing' might be inferred in some cases of projected types.
// See KT-30330 for an example of such code where call of type 'kotlin.Nothing' terminates and produces some value
// (although doing so by subverting the type system).
backendContext.createIrBuilder(parent, expression.startOffset, expression.endOffset).run {
irBlock(expression, null, context.irBuiltIns.nothingType) {
+super.visitCall(changeTypeToUnit(expression))
+super.visitCall(expression)
+irCall(backendContext.ir.symbols.ThrowKotlinNothingValueException)
}
}
} else {
super.visitCall(expression)
}
private fun changeTypeToUnit(call: IrCall): IrCall =
IrCallImpl(
call.startOffset, call.endOffset,
backendContext.irBuiltIns.unitType,
call.symbol,
call.typeArgumentsCount, call.valueArgumentsCount, call.origin, call.superQualifierSymbol
).also { newCall ->
for (i in 0 until call.typeArgumentsCount) {
newCall.putTypeArgument(i, call.getTypeArgument(i))
}
newCall.dispatchReceiver = call.dispatchReceiver
newCall.extensionReceiver = call.extensionReceiver
for (i in 0 until call.valueArgumentsCount) {
newCall.putValueArgument(i, call.getValueArgument(i))
}
}
}
}
@@ -7,6 +7,7 @@ package org.jetbrains.kotlin.ir.backend.js.lower.cleanup
import org.jetbrains.kotlin.backend.common.BodyLoweringPass
import org.jetbrains.kotlin.ir.IrElement
import org.jetbrains.kotlin.ir.IrStatement
import org.jetbrains.kotlin.ir.backend.js.utils.isPure
import org.jetbrains.kotlin.ir.declarations.IrDeclaration
import org.jetbrains.kotlin.ir.expressions.*
@@ -62,7 +63,7 @@ private class CodeCleaner : IrElementVisitorVoid {
unreachable -> false
it is IrExpression && it.isPure(true) -> false
else -> {
unreachable = it is IrExpression && it.type.isNothing()
unreachable = it.doesNotReturn()
true
}
}
@@ -73,6 +74,29 @@ private class CodeCleaner : IrElementVisitorVoid {
statements += newStatements
}
// Checks if it is safe to assume the statement doesn't return (e.g. throws an exception or loops infinitely)
// Takes into account cases like `fun <T> foo(): T = Any() as T`, which could be used as `foo<Nothing>()` and terminate despite the call type `Nothing`.
// Assumes that only functions with explicit return type `Nothing` do not return.
// Also see KotlinNothingValueExceptionLowering.kt
private fun IrStatement.doesNotReturn(): Boolean {
if (this !is IrExpression || !type.isNothing()) return false
var hasFakeNothingCalls = false
acceptVoid(object : IrElementVisitorVoid {
override fun visitElement(element: IrElement) {
element.acceptChildrenVoid(this)
}
override fun visitCall(expression: IrCall) {
super.visitCall(expression)
hasFakeNothingCalls = hasFakeNothingCalls || expression.type.isNothing() && !expression.symbol.owner.returnType.isNothing()
}
})
return !hasFakeNothingCalls
}
override fun visitElement(element: IrElement) {
element.acceptChildrenVoid(this)
}