JS/Inlining: minor improvements and clarifications after code review. Test whether expression without side effect does not prevent to relocate another expressions
This commit is contained in:
@@ -267,10 +267,9 @@ internal class ExpressionDecomposer private constructor(
|
|||||||
// Qualifier might be a reference to lambda property. See KT-7674
|
// Qualifier might be a reference to lambda property. See KT-7674
|
||||||
// An exception here is `fn.call()`, which are marked as side effect free. Further recognition of such
|
// An exception here is `fn.call()`, which are marked as side effect free. Further recognition of such
|
||||||
// case in inliner might be quite difficult, so never extract such call (and other calls marked this way).
|
// case in inliner might be quite difficult, so never extract such call (and other calls marked this way).
|
||||||
if (qualifier in containsNodeWithSideEffect && (qualifier as? HasMetadata)?.sideEffects != SideEffectKind.PURE) {
|
if ((qualifier !in containsNodeWithSideEffect || (qualifier as? HasMetadata)?.sideEffects == SideEffectKind.PURE) &&
|
||||||
qualifier = qualifier.extractToTemporary()
|
(callee != null && receiver != null && receiver in containsNodeWithSideEffect)
|
||||||
}
|
) {
|
||||||
else if (callee != null && receiver != null && receiver in containsNodeWithSideEffect) {
|
|
||||||
val receiverTmp = receiver.extractToTemporary()
|
val receiverTmp = receiver.extractToTemporary()
|
||||||
callee.qualifier = receiverTmp
|
callee.qualifier = receiverTmp
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -161,7 +161,7 @@ class IneffectiveStatementElimination(private val root: JsFunction) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Although it can be suspicious case, sometimes it really helps.
|
// Although it can be suspicious case, sometimes it really helps.
|
||||||
// Consider the following case: `Kotlin.modules['foo'].bar()`, where `foo` is inlineable. Expression decomposer produces
|
// Consider the following case: `Kotlin.modules['foo'].bar()`, where `bar` is inlineable. Expression decomposer produces
|
||||||
// var $tmp = Kotlin.modules['foo'];
|
// var $tmp = Kotlin.modules['foo'];
|
||||||
// $tmp.bar();
|
// $tmp.bar();
|
||||||
// Then, inlined body of `bar` never uses `$tmp`, therefore we can eliminate it, so `Kotlin.modules['foo']` remains.
|
// Then, inlined body of `bar` never uses `$tmp`, therefore we can eliminate it, so `Kotlin.modules['foo']` remains.
|
||||||
|
|||||||
+3
-4
@@ -24,8 +24,7 @@ import org.jetbrains.kotlin.js.inline.util.collectFreeVariables
|
|||||||
import org.jetbrains.kotlin.js.translate.utils.JsAstUtils
|
import org.jetbrains.kotlin.js.translate.utils.JsAstUtils
|
||||||
|
|
||||||
internal class TemporaryAssignmentElimination(private val root: JsBlock) {
|
internal class TemporaryAssignmentElimination(private val root: JsBlock) {
|
||||||
// We say "use" about any references to a temporary variable
|
private val referenceCount = mutableMapOf<JsName, Int>()
|
||||||
private val useCount = mutableMapOf<JsName, Int>()
|
|
||||||
|
|
||||||
// We say "usage" about special kind of a reference to a temporary variable in the following cases:
|
// We say "usage" about special kind of a reference to a temporary variable in the following cases:
|
||||||
// someVar = $tmp;
|
// someVar = $tmp;
|
||||||
@@ -129,7 +128,7 @@ internal class TemporaryAssignmentElimination(private val root: JsBlock) {
|
|||||||
|
|
||||||
private fun getUsageSequence(name: JsName): UsageSequence? {
|
private fun getUsageSequence(name: JsName): UsageSequence? {
|
||||||
return usageSequences.getOrPut(name) {
|
return usageSequences.getOrPut(name) {
|
||||||
if (useCount[name] != 1) return null
|
if (referenceCount[name] != 1) return null
|
||||||
|
|
||||||
val usage = usages[name]
|
val usage = usages[name]
|
||||||
val mappedUsage: UsageSequence? = when (usage) {
|
val mappedUsage: UsageSequence? = when (usage) {
|
||||||
@@ -255,7 +254,7 @@ internal class TemporaryAssignmentElimination(private val root: JsBlock) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun use(name: JsName) {
|
private fun use(name: JsName) {
|
||||||
useCount[name] = 1 + (useCount[name] ?: 0)
|
referenceCount[name] = 1 + (referenceCount[name] ?: 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class Usage(val statement: JsStatement) {
|
private sealed class Usage(val statement: JsStatement) {
|
||||||
|
|||||||
+13
-9
@@ -33,7 +33,7 @@ internal class TemporaryVariableElimination(function: JsFunction) {
|
|||||||
private val definedValues = mutableMapOf<JsName, JsExpression>()
|
private val definedValues = mutableMapOf<JsName, JsExpression>()
|
||||||
private val temporary = mutableSetOf<JsName>()
|
private val temporary = mutableSetOf<JsName>()
|
||||||
private var hasChanges = false
|
private var hasChanges = false
|
||||||
private val namesToProcess = function.collectLocalVariables()
|
private val localVariables = function.collectLocalVariables()
|
||||||
private val statementsToRemove = mutableSetOf<JsNode>()
|
private val statementsToRemove = mutableSetOf<JsNode>()
|
||||||
private val namesToSubstitute = mutableSetOf<JsName>()
|
private val namesToSubstitute = mutableSetOf<JsName>()
|
||||||
private val variablesToRemove = mutableSetOf<JsName>()
|
private val variablesToRemove = mutableSetOf<JsName>()
|
||||||
@@ -54,7 +54,7 @@ internal class TemporaryVariableElimination(function: JsFunction) {
|
|||||||
val assignment = JsAstUtils.decomposeAssignmentToVariable(x.expression)
|
val assignment = JsAstUtils.decomposeAssignmentToVariable(x.expression)
|
||||||
if (assignment != null) {
|
if (assignment != null) {
|
||||||
val (name, value) = assignment
|
val (name, value) = assignment
|
||||||
if (name in namesToProcess) {
|
if (name in localVariables) {
|
||||||
assignVariable(name, value)
|
assignVariable(name, value)
|
||||||
addVar(name)
|
addVar(name)
|
||||||
accept(value)
|
accept(value)
|
||||||
@@ -71,7 +71,7 @@ internal class TemporaryVariableElimination(function: JsFunction) {
|
|||||||
for (v in x.vars) {
|
for (v in x.vars) {
|
||||||
val name = v.name
|
val name = v.name
|
||||||
val value = v.initExpression
|
val value = v.initExpression
|
||||||
if (value != null && name in namesToProcess) {
|
if (value != null && name in localVariables) {
|
||||||
assignVariable(name, value)
|
assignVariable(name, value)
|
||||||
addVar(name)
|
addVar(name)
|
||||||
accept(value)
|
accept(value)
|
||||||
@@ -84,7 +84,7 @@ internal class TemporaryVariableElimination(function: JsFunction) {
|
|||||||
|
|
||||||
override fun visitNameRef(nameRef: JsNameRef) {
|
override fun visitNameRef(nameRef: JsNameRef) {
|
||||||
val name = nameRef.name
|
val name = nameRef.name
|
||||||
if (name != null && nameRef.qualifier == null && name in namesToProcess) {
|
if (name != null && nameRef.qualifier == null && name in localVariables) {
|
||||||
useVariable(name)
|
useVariable(name)
|
||||||
if (name !in currentScope) {
|
if (name !in currentScope) {
|
||||||
inconsistent += name
|
inconsistent += name
|
||||||
@@ -523,13 +523,16 @@ internal class TemporaryVariableElimination(function: JsFunction) {
|
|||||||
usages[name] = (usages[name] ?: 0) + 1
|
usages[name] = (usages[name] ?: 0) + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shouldConsiderUnused(name: JsName) = (definitions[name] ?: 0) == 1 && (usages[name] ?: 0) == 0 && name in temporary
|
private fun shouldConsiderUnused(name: JsName) = definitions[name] == 1 && (usages[name] ?: 0) == 0 && name in temporary
|
||||||
|
|
||||||
private fun shouldConsiderTemporary(name: JsName): Boolean {
|
private fun shouldConsiderTemporary(name: JsName): Boolean {
|
||||||
if (definitions[name] ?: 0 != 1 || name !in temporary) return false
|
if (definitions[name] != 1 || name !in temporary) return false
|
||||||
|
|
||||||
val expr = definedValues[name]
|
val expr = definedValues[name]
|
||||||
return (expr != null && isTrivial(expr)) || usages[name] ?: 0 == 1
|
// It's useful to copy trivial expressions when they are used more than once. Example are temporary variables
|
||||||
|
// that receiver another (non-temporary) variables. To prevent code from bloating, we don't treat large value literals
|
||||||
|
// as trivial expressions.
|
||||||
|
return (expr != null && isTrivial(expr)) || usages[name] == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isTrivial(expr: JsExpression): Boolean = when (expr) {
|
private fun isTrivial(expr: JsExpression): Boolean = when (expr) {
|
||||||
@@ -540,14 +543,15 @@ internal class TemporaryVariableElimination(function: JsFunction) {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
val name = expr.name
|
val name = expr.name
|
||||||
name in namesToProcess && when (definitions[name]) {
|
name in localVariables && when (definitions[name]) {
|
||||||
|
// Local variables with zero definitions are function parameters. We can relocate and copy them.
|
||||||
null, 0 -> true
|
null, 0 -> true
|
||||||
1 -> name !in namesToSubstitute || definedValues[name]?.let { isTrivial(it) } ?: false
|
1 -> name !in namesToSubstitute || definedValues[name]?.let { isTrivial(it) } ?: false
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is JsLiteral.JsValueLiteral -> true
|
is JsLiteral.JsValueLiteral -> expr.toString().length < 10
|
||||||
is JsInvocation -> expr.sideEffects == SideEffectKind.PURE && isTrivial(expr.qualifier) && expr.arguments.all { isTrivial(it) }
|
is JsInvocation -> expr.sideEffects == SideEffectKind.PURE && isTrivial(expr.qualifier) && expr.arguments.all { isTrivial(it) }
|
||||||
is JsArrayAccess -> isTrivial(expr.arrayExpression) && isTrivial(expr.indexExpression)
|
is JsArrayAccess -> isTrivial(expr.arrayExpression) && isTrivial(expr.indexExpression)
|
||||||
else -> false
|
else -> false
|
||||||
|
|||||||
+6
@@ -65,6 +65,12 @@ public class InlineSizeReductionTestGenerated extends AbstractInlineSizeReductio
|
|||||||
doTest(fileName);
|
doTest(fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TestMetadata("propertyReferenceDoesNotProduceSideEffect.kt")
|
||||||
|
public void testPropertyReferenceDoesNotProduceSideEffect() throws Exception {
|
||||||
|
String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/inlineSizeReduction/cases/propertyReferenceDoesNotProduceSideEffect.kt");
|
||||||
|
doTest(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
@TestMetadata("returnInlineCall.kt")
|
@TestMetadata("returnInlineCall.kt")
|
||||||
public void testReturnInlineCall() throws Exception {
|
public void testReturnInlineCall() throws Exception {
|
||||||
String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/inlineSizeReduction/cases/returnInlineCall.kt");
|
String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/inlineSizeReduction/cases/returnInlineCall.kt");
|
||||||
|
|||||||
+3
@@ -77,6 +77,9 @@ fun VariableAccessInfo.constructAccessExpression(ref: JsExpression): JsExpressio
|
|||||||
return ref
|
return ref
|
||||||
} else {
|
} else {
|
||||||
return if (value !is JsEmptyExpression) {
|
return if (value !is JsEmptyExpression) {
|
||||||
|
// This is useful when passing AST to TemporaryAssignmentElimination. It can bring
|
||||||
|
// property assignment like `obj.propertyName = $tmp` to places where `$tmp` gets its value,
|
||||||
|
// but only when it's sure that no side effects possible.
|
||||||
(ref as? HasMetadata)?.let { it.sideEffects = SideEffectKind.PURE }
|
(ref as? HasMetadata)?.let { it.sideEffects = SideEffectKind.PURE }
|
||||||
JsAstUtils.assignment(ref, value!!)
|
JsAstUtils.assignment(ref, value!!)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -75,7 +75,7 @@ object DefaultVariableAccessCase : VariableAccessCase() {
|
|||||||
override fun VariableAccessInfo.dispatchReceiver(): JsExpression {
|
override fun VariableAccessInfo.dispatchReceiver(): JsExpression {
|
||||||
val accessor = JsNameRef(variableName, dispatchReceiver!!)
|
val accessor = JsNameRef(variableName, dispatchReceiver!!)
|
||||||
val descriptor = callableDescriptor
|
val descriptor = callableDescriptor
|
||||||
if (descriptor is PropertyDescriptor && !JsDescriptorUtils.sideEffectsPossible(descriptor)) {
|
if (descriptor is PropertyDescriptor && !JsDescriptorUtils.sideEffectsPossibleOnRead(descriptor)) {
|
||||||
accessor.sideEffects = SideEffectKind.DEPENDS_ON_STATE
|
accessor.sideEffects = SideEffectKind.DEPENDS_ON_STATE
|
||||||
}
|
}
|
||||||
return constructAccessExpression(accessor)
|
return constructAccessExpression(accessor)
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ public final class JsDescriptorUtils {
|
|||||||
return accessorDescriptor == null || accessorDescriptor.isDefault();
|
return accessorDescriptor == null || accessorDescriptor.isDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean sideEffectsPossible(@NotNull PropertyDescriptor property) {
|
public static boolean sideEffectsPossibleOnRead(@NotNull PropertyDescriptor property) {
|
||||||
return !isDefaultAccessor(property.getGetter()) || ModalityKt.isOverridableOrOverrides(property) ||
|
return !isDefaultAccessor(property.getGetter()) || ModalityKt.isOverridableOrOverrides(property) ||
|
||||||
isStaticInitializationPossible(property);
|
isStaticInitializationPossible(property);
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+29
@@ -0,0 +1,29 @@
|
|||||||
|
// CHECK_VARS_COUNT: function=box count=1
|
||||||
|
|
||||||
|
package foo
|
||||||
|
|
||||||
|
var log = ""
|
||||||
|
|
||||||
|
class A() {
|
||||||
|
var x = 23
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sideEffect(): Int {
|
||||||
|
log += "sideEffect();"
|
||||||
|
return 42
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bar(a: Int, b: Int) {
|
||||||
|
log += "bar($a, $b);"
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun test(a: Int, b: Int) {
|
||||||
|
bar(b, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun box(): String {
|
||||||
|
val a = A()
|
||||||
|
test(sideEffect(), a.x)
|
||||||
|
assertEquals("sideEffect();bar(23, 42);", log)
|
||||||
|
return "OK"
|
||||||
|
}
|
||||||
+3
-3
@@ -4,18 +4,18 @@ function f(x) {
|
|||||||
|
|
||||||
function box() {
|
function box() {
|
||||||
var result1, result2, $a, $b;
|
var result1, result2, $a, $b;
|
||||||
|
|
||||||
if (f(true)) {
|
if (f(true)) {
|
||||||
$a = "1";
|
$a = "1";
|
||||||
}
|
}
|
||||||
result1 = $a;
|
result1 = $a;
|
||||||
if (result1 !== "1") return "fail1: " + result1;
|
if (result1 !== "1") return "fail1: " + result1;
|
||||||
|
|
||||||
if (f(false)) {
|
if (f(false)) {
|
||||||
$b = "1";
|
$b = "1";
|
||||||
}
|
}
|
||||||
result2 = $b;
|
result2 = $b;
|
||||||
if (result2 !== void 0) return "fail2: " + result2;
|
if (result2 !== void 0) return "fail2: " + result2;
|
||||||
|
|
||||||
return "OK";
|
return "OK";
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user