JS: optimize loop over array.withIndex()
See KT-20932
This commit is contained in:
@@ -2494,6 +2494,18 @@ public class BoxJsTestGenerated extends AbstractBoxJsTest {
|
|||||||
doTest(fileName);
|
doTest(fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TestMetadata("overArrayWithIndex.kt")
|
||||||
|
public void testOverArrayWithIndex() throws Exception {
|
||||||
|
String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/box/expression/for/overArrayWithIndex.kt");
|
||||||
|
doTest(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestMetadata("overCollectionWithIndex.kt")
|
||||||
|
public void testOverCollectionWithIndex() throws Exception {
|
||||||
|
String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/box/expression/for/overCollectionWithIndex.kt");
|
||||||
|
doTest(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
@TestMetadata("rangeOptimization.kt")
|
@TestMetadata("rangeOptimization.kt")
|
||||||
public void testRangeOptimization() throws Exception {
|
public void testRangeOptimization() throws Exception {
|
||||||
String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/box/expression/for/rangeOptimization.kt");
|
String fileName = KotlinTestUtils.navigationMetadata("js/js.translator/testData/box/expression/for/rangeOptimization.kt");
|
||||||
|
|||||||
+164
-2
@@ -19,8 +19,7 @@
|
|||||||
package org.jetbrains.kotlin.js.translate.expression
|
package org.jetbrains.kotlin.js.translate.expression
|
||||||
|
|
||||||
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
|
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
|
||||||
import org.jetbrains.kotlin.descriptors.CallableDescriptor
|
import org.jetbrains.kotlin.descriptors.*
|
||||||
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
|
|
||||||
import org.jetbrains.kotlin.js.backend.ast.*
|
import org.jetbrains.kotlin.js.backend.ast.*
|
||||||
import org.jetbrains.kotlin.js.translate.callTranslator.CallTranslator
|
import org.jetbrains.kotlin.js.translate.callTranslator.CallTranslator
|
||||||
import org.jetbrains.kotlin.js.translate.context.TranslationContext
|
import org.jetbrains.kotlin.js.translate.context.TranslationContext
|
||||||
@@ -29,14 +28,22 @@ import org.jetbrains.kotlin.js.translate.intrinsic.functions.factories.ArrayFIF
|
|||||||
import org.jetbrains.kotlin.js.translate.utils.BindingUtils.*
|
import org.jetbrains.kotlin.js.translate.utils.BindingUtils.*
|
||||||
import org.jetbrains.kotlin.js.translate.utils.JsAstUtils
|
import org.jetbrains.kotlin.js.translate.utils.JsAstUtils
|
||||||
import org.jetbrains.kotlin.js.translate.utils.JsAstUtils.*
|
import org.jetbrains.kotlin.js.translate.utils.JsAstUtils.*
|
||||||
|
import org.jetbrains.kotlin.js.translate.utils.JsDescriptorUtils.getReceiverParameterForReceiver
|
||||||
import org.jetbrains.kotlin.js.translate.utils.PsiUtils.getLoopRange
|
import org.jetbrains.kotlin.js.translate.utils.PsiUtils.getLoopRange
|
||||||
import org.jetbrains.kotlin.js.translate.utils.TranslationUtils
|
import org.jetbrains.kotlin.js.translate.utils.TranslationUtils
|
||||||
|
import org.jetbrains.kotlin.name.ClassId
|
||||||
import org.jetbrains.kotlin.name.FqName
|
import org.jetbrains.kotlin.name.FqName
|
||||||
|
import org.jetbrains.kotlin.name.Name
|
||||||
import org.jetbrains.kotlin.psi.*
|
import org.jetbrains.kotlin.psi.*
|
||||||
|
import org.jetbrains.kotlin.resolve.BindingContext
|
||||||
|
import org.jetbrains.kotlin.resolve.DescriptorUtils.getFunctionByName
|
||||||
|
import org.jetbrains.kotlin.resolve.DescriptorUtils.getPropertyByName
|
||||||
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
|
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
|
||||||
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall
|
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall
|
||||||
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
|
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
|
||||||
import org.jetbrains.kotlin.resolve.scopes.receivers.ExpressionReceiver
|
import org.jetbrains.kotlin.resolve.scopes.receivers.ExpressionReceiver
|
||||||
|
import org.jetbrains.kotlin.resolve.scopes.receivers.ImplicitReceiver
|
||||||
|
import org.jetbrains.kotlin.types.KotlinType
|
||||||
|
|
||||||
fun createWhile(doWhile: Boolean, expression: KtWhileExpressionBase, context: TranslationContext): JsNode {
|
fun createWhile(doWhile: Boolean, expression: KtWhileExpressionBase, context: TranslationContext): JsNode {
|
||||||
val conditionExpression = expression.condition ?:
|
val conditionExpression = expression.condition ?:
|
||||||
@@ -85,6 +92,12 @@ private val stepFunctionName = FqName("kotlin.ranges.step")
|
|||||||
private val intRangeName = FqName("kotlin.ranges.IntRange")
|
private val intRangeName = FqName("kotlin.ranges.IntRange")
|
||||||
private val intProgressionName = FqName("kotlin.ranges.IntProgression")
|
private val intProgressionName = FqName("kotlin.ranges.IntProgression")
|
||||||
|
|
||||||
|
private val withIndexFqName = FqName("kotlin.collections.withIndex")
|
||||||
|
private val sequenceWithIndexFqName = FqName("kotlin.sequences.withIndex")
|
||||||
|
private val indicesFqName = FqName("kotlin.collections.indices")
|
||||||
|
|
||||||
|
private val sequenceFqName = FqName("kotlin.sequences.Sequence")
|
||||||
|
|
||||||
fun translateForExpression(expression: KtForExpression, context: TranslationContext): JsStatement {
|
fun translateForExpression(expression: KtForExpression, context: TranslationContext): JsStatement {
|
||||||
val loopRange = KtPsiUtil.deparenthesize(getLoopRange(expression))!!
|
val loopRange = KtPsiUtil.deparenthesize(getLoopRange(expression))!!
|
||||||
val rangeType = getTypeForExpression(context.bindingContext(), loopRange)
|
val rangeType = getTypeForExpression(context.bindingContext(), loopRange)
|
||||||
@@ -134,6 +147,45 @@ fun translateForExpression(expression: KtForExpression, context: TranslationCont
|
|||||||
JsScope.declareTemporary()
|
JsScope.declareTemporary()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun KtDeclaration.extractDescriptor() = context.bindingContext()[BindingContext.VARIABLE, this]?.takeUnless { it.name.isSpecial }
|
||||||
|
|
||||||
|
fun extractWithIndexCall(): WithIndexInfo? {
|
||||||
|
val resolvedCall = loopRange.getResolvedCall(context.bindingContext()) ?: return null
|
||||||
|
val fqName = resolvedCall.resultingDescriptor.fqNameSafe
|
||||||
|
val (indexDescriptor, elementDescriptor) = when (fqName) {
|
||||||
|
withIndexFqName, sequenceWithIndexFqName -> {
|
||||||
|
if (destructuringParameter == null) return null
|
||||||
|
destructuringParameter.entries.let { Pair(it[0].extractDescriptor(), it[1].extractDescriptor()) }
|
||||||
|
}
|
||||||
|
indicesFqName -> {
|
||||||
|
if (destructuringParameter != null) return null
|
||||||
|
val varDescriptor = context.bindingContext()[BindingContext.DECLARATION_TO_DESCRIPTOR, loopParameter] as?
|
||||||
|
VariableDescriptor ?: return null
|
||||||
|
Pair(varDescriptor, null)
|
||||||
|
}
|
||||||
|
else -> return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val receiverClass = resolvedCall.resultingDescriptor.extensionReceiverParameter?.type?.constructor?.declarationDescriptor as?
|
||||||
|
ClassDescriptor ?: return null
|
||||||
|
val receiverType = when {
|
||||||
|
KotlinBuiltIns.isArrayOrPrimitiveArray(receiverClass) -> WithIndexReceiverType.ARRAY
|
||||||
|
KotlinBuiltIns.isCollectionOrNullableCollection(receiverClass.defaultType) -> WithIndexReceiverType.COLLECTION
|
||||||
|
KotlinBuiltIns.isIterableOrNullableIterable(receiverClass.defaultType) -> WithIndexReceiverType.ITERABLE
|
||||||
|
receiverClass.fqNameSafe == sequenceFqName -> WithIndexReceiverType.SEQUENCE
|
||||||
|
else -> return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val receiver = resolvedCall.extensionReceiver ?: return null
|
||||||
|
val arrayExpr = when (receiver) {
|
||||||
|
is ExpressionReceiver -> Translation.translateAsExpression(receiver.expression, context)
|
||||||
|
is ImplicitReceiver -> context.getDispatchReceiver(getReceiverParameterForReceiver(receiver))
|
||||||
|
else -> return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return WithIndexInfo(receiverType, indexDescriptor, elementDescriptor, arrayExpr)
|
||||||
|
}
|
||||||
|
|
||||||
fun translateBody(itemValue: JsExpression?): JsStatement? {
|
fun translateBody(itemValue: JsExpression?): JsStatement? {
|
||||||
val realBody = expression.body?.let { Translation.translateAsStatementAndMergeInBlockIfNeeded(it, context) }
|
val realBody = expression.body?.let { Translation.translateAsStatementAndMergeInBlockIfNeeded(it, context) }
|
||||||
if (itemValue == null && destructuringParameter == null) {
|
if (itemValue == null && destructuringParameter == null) {
|
||||||
@@ -242,6 +294,87 @@ fun translateForExpression(expression: KtForExpression, context: TranslationCont
|
|||||||
return JsFor(initExpression, conditionExpression, incrementExpression, body)
|
return JsFor(initExpression, conditionExpression, incrementExpression, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun translateForOverArrayWithIndex(info: WithIndexInfo): JsStatement {
|
||||||
|
val range = context.cacheExpressionIfNeeded(info.range)
|
||||||
|
val indexVar = info.index?.let { context.getNameForDescriptor(it) } ?: JsScope.declareTemporary()
|
||||||
|
val valueVar = info.value?.let { context.getNameForDescriptor(it) }
|
||||||
|
|
||||||
|
val initExpression = newVar(indexVar, JsIntLiteral(0)).apply { source = expression }
|
||||||
|
val conditionExpression = inequality(indexVar.makeRef(), JsNameRef("length", range)).source(expression)
|
||||||
|
val incrementExpression = JsPrefixOperation(JsUnaryOperator.INC, indexVar.makeRef()).source(expression)
|
||||||
|
|
||||||
|
val body = JsBlock()
|
||||||
|
if (valueVar != null) {
|
||||||
|
body.statements += newVar(valueVar, JsArrayAccess(range, indexVar.makeRef())).apply { source = expression }
|
||||||
|
}
|
||||||
|
expression.body?.let { body.statements += Translation.translateAsStatement(it, context.innerBlock(body)) }
|
||||||
|
|
||||||
|
return JsFor(initExpression, conditionExpression, incrementExpression, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findCollection() =
|
||||||
|
context.currentModule.findClassAcrossModuleDependencies(ClassId.topLevel(KotlinBuiltIns.FQ_NAMES.collection))!!
|
||||||
|
|
||||||
|
fun translateForOverCollectionIndices(info: WithIndexInfo): JsStatement {
|
||||||
|
val range = context.cacheExpressionIfNeeded(info.range)
|
||||||
|
val indexVar = info.index?.let { context.getNameForDescriptor(it) } ?: JsScope.declareTemporary()
|
||||||
|
|
||||||
|
val initExpression = newVar(indexVar, JsIntLiteral(0)).apply { source = expression }
|
||||||
|
|
||||||
|
val sizeDescriptor = getPropertyByName(findCollection().unsubstitutedMemberScope, Name.identifier("size"))
|
||||||
|
val sizeName = context.getNameForDescriptor(sizeDescriptor)
|
||||||
|
val conditionExpression = inequality(indexVar.makeRef(), JsNameRef(sizeName, range)).source(expression)
|
||||||
|
|
||||||
|
val incrementExpression = JsPrefixOperation(JsUnaryOperator.INC, indexVar.makeRef()).source(expression)
|
||||||
|
|
||||||
|
val body = JsBlock()
|
||||||
|
expression.body?.let { body.statements += Translation.translateAsStatement(it, context.innerBlock(body)) }
|
||||||
|
|
||||||
|
return JsFor(initExpression, conditionExpression, incrementExpression, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findIterable() =
|
||||||
|
context.currentModule.findClassAcrossModuleDependencies(ClassId.topLevel(KotlinBuiltIns.FQ_NAMES.iterable))!!
|
||||||
|
|
||||||
|
fun findSequence() =
|
||||||
|
context.currentModule.findClassAcrossModuleDependencies(ClassId.topLevel(sequenceFqName))!!
|
||||||
|
|
||||||
|
fun translateForOverCollectionWithIndex(info: WithIndexInfo): JsStatement {
|
||||||
|
val range = context.cacheExpressionIfNeeded(info.range)
|
||||||
|
val indexVar = info.index?.let { context.getNameForDescriptor(it) }
|
||||||
|
val valueVar = info.value?.let { context.getNameForDescriptor(it) }
|
||||||
|
|
||||||
|
indexVar?.let { context.addStatementToCurrentBlock(newVar(it, JsIntLiteral(0)).apply { source = expression }) }
|
||||||
|
|
||||||
|
val iteratorVar = JsScope.declareTemporary()
|
||||||
|
val rangeOwner = if (info.receiverType == WithIndexReceiverType.SEQUENCE) findSequence() else findIterable()
|
||||||
|
val iteratorDescriptor = getFunctionByName(rangeOwner.unsubstitutedMemberScope, Name.identifier("iterator"))
|
||||||
|
val iteratorName = context.getNameForDescriptor(iteratorDescriptor)
|
||||||
|
val initExpression = newVar(iteratorVar, JsInvocation(pureFqn(iteratorName, range))).apply { source = expression }
|
||||||
|
|
||||||
|
val iteratorClassDescriptor = iteratorDescriptor.returnType!!.constructor.declarationDescriptor as ClassDescriptor
|
||||||
|
|
||||||
|
val hasNextDescriptor = getFunctionByName(iteratorClassDescriptor.unsubstitutedMemberScope, Name.identifier("hasNext"))
|
||||||
|
val hasNextName = context.getNameForDescriptor(hasNextDescriptor)
|
||||||
|
val hasNextInvocation = JsInvocation(pureFqn(hasNextName, iteratorVar.makeRef())).source(expression)
|
||||||
|
|
||||||
|
val nextDescriptor = getFunctionByName(iteratorClassDescriptor.unsubstitutedMemberScope, Name.identifier("next"))
|
||||||
|
val nextName = context.getNameForDescriptor(nextDescriptor)
|
||||||
|
val nextInvocation = JsInvocation(pureFqn(nextName, iteratorVar.makeRef())).source(expression)
|
||||||
|
|
||||||
|
val incrementExpression = indexVar?.let { JsPrefixOperation(JsUnaryOperator.INC, it.makeRef()).source(expression) }
|
||||||
|
|
||||||
|
val body = JsBlock()
|
||||||
|
body.statements += if (valueVar != null) {
|
||||||
|
newVar(valueVar, nextInvocation).apply { source = expression }
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
asSyntheticStatement(nextInvocation)
|
||||||
|
}
|
||||||
|
expression.body?.let { body.statements += Translation.translateAsStatement(it, context.innerBlock(body)) }
|
||||||
|
return JsFor(initExpression, hasNextInvocation, incrementExpression).also { it.body = body }
|
||||||
|
}
|
||||||
|
|
||||||
fun translateForOverIterator(): JsStatement {
|
fun translateForOverIterator(): JsStatement {
|
||||||
|
|
||||||
fun translateMethodInvocation(
|
fun translateMethodInvocation(
|
||||||
@@ -284,11 +417,27 @@ fun translateForExpression(expression: KtForExpression, context: TranslationCont
|
|||||||
}
|
}
|
||||||
|
|
||||||
val rangeLiteral = extractForOverRangeLiteral()
|
val rangeLiteral = extractForOverRangeLiteral()
|
||||||
|
val withIndexCall = extractWithIndexCall()
|
||||||
|
|
||||||
val result = when {
|
val result = when {
|
||||||
rangeLiteral != null ->
|
rangeLiteral != null ->
|
||||||
translateForOverLiteralRange(rangeLiteral)
|
translateForOverLiteralRange(rangeLiteral)
|
||||||
|
|
||||||
|
withIndexCall != null ->
|
||||||
|
when (withIndexCall.receiverType) {
|
||||||
|
WithIndexReceiverType.ARRAY -> translateForOverArrayWithIndex(withIndexCall)
|
||||||
|
WithIndexReceiverType.ITERABLE,
|
||||||
|
WithIndexReceiverType.SEQUENCE,
|
||||||
|
WithIndexReceiverType.COLLECTION -> {
|
||||||
|
if (withIndexCall.value == null && withIndexCall.receiverType == WithIndexReceiverType.COLLECTION) {
|
||||||
|
translateForOverCollectionIndices(withIndexCall)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
translateForOverCollectionWithIndex(withIndexCall)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
isForOverRange() ->
|
isForOverRange() ->
|
||||||
translateForOverRange()
|
translateForOverRange()
|
||||||
|
|
||||||
@@ -309,3 +458,16 @@ private enum class RangeType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private class RangeLiteral(val type: RangeType, val first: KtExpression, val second: KtExpression, var step: KtExpression?)
|
private class RangeLiteral(val type: RangeType, val first: KtExpression, val second: KtExpression, var step: KtExpression?)
|
||||||
|
|
||||||
|
private class WithIndexInfo(
|
||||||
|
val receiverType: WithIndexReceiverType,
|
||||||
|
val index: VariableDescriptor?, val value: VariableDescriptor?,
|
||||||
|
val range: JsExpression
|
||||||
|
)
|
||||||
|
|
||||||
|
private enum class WithIndexReceiverType {
|
||||||
|
ARRAY,
|
||||||
|
COLLECTION,
|
||||||
|
ITERABLE,
|
||||||
|
SEQUENCE
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
// EXPECTED_REACHABLE_NODES: 1128
|
||||||
|
// CHECK_CONTAINS_NO_CALLS: test1 except=toString
|
||||||
|
// CHECK_CONTAINS_NO_CALLS: test2 except=toString
|
||||||
|
// CHECK_CONTAINS_NO_CALLS: test3 except=toString
|
||||||
|
// CHECK_CONTAINS_NO_CALLS: test4 except=toString
|
||||||
|
|
||||||
|
fun test1(a: Array<String>): String {
|
||||||
|
var s = ""
|
||||||
|
for ((i, x) in a.withIndex()) {
|
||||||
|
s += "$i:$x;"
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
fun test2(a: Array<String>): String {
|
||||||
|
var s = ""
|
||||||
|
for ((_, x) in a.withIndex()) {
|
||||||
|
s += "$x;"
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
fun test3(a: Array<String>): String {
|
||||||
|
var s = ""
|
||||||
|
for ((i, _) in a.withIndex()) {
|
||||||
|
s += "$i;"
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
fun test4(a: Array<String>): String {
|
||||||
|
var s = ""
|
||||||
|
for (i in a.indices) {
|
||||||
|
s += "$i;"
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
fun box(): String {
|
||||||
|
val array = arrayOf("foo", "bar", "baz")
|
||||||
|
|
||||||
|
var r = test1(array)
|
||||||
|
if (r != "0:foo;1:bar;2:baz;") return "fail1: $r"
|
||||||
|
|
||||||
|
r = test2(array)
|
||||||
|
if (r != "foo;bar;baz;") return "fail2: $r"
|
||||||
|
|
||||||
|
r = test3(array)
|
||||||
|
if (r != "0;1;2;") return "fail3: $r"
|
||||||
|
|
||||||
|
r = test4(array)
|
||||||
|
if (r != "0;1;2;") return "fail4: $r"
|
||||||
|
|
||||||
|
return "OK"
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
// EXPECTED_REACHABLE_NODES: 1461
|
||||||
|
// CHECK_NOT_CALLED_IN_SCOPE: function=component1 scope=test1
|
||||||
|
// CHECK_NOT_CALLED_IN_SCOPE: function=component1 scope=test2
|
||||||
|
// CHECK_NOT_CALLED_IN_SCOPE: function=component1 scope=test3
|
||||||
|
// CHECK_CONTAINS_NO_CALLS: test4 except=toString
|
||||||
|
|
||||||
|
fun test1(a: Sequence<String>): String {
|
||||||
|
var s = ""
|
||||||
|
for ((i, x) in a.withIndex()) {
|
||||||
|
s += "$i:$x;"
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
fun test2(a: Collection<String>): String {
|
||||||
|
var s = ""
|
||||||
|
for ((_, x) in a.withIndex()) {
|
||||||
|
s += "$x;"
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
fun test3(a: List<String>): String {
|
||||||
|
var s = ""
|
||||||
|
for ((i, _) in a.withIndex()) {
|
||||||
|
s += "$i;"
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
fun test4(a: Set<String>): String {
|
||||||
|
var s = ""
|
||||||
|
for (i in a.indices) {
|
||||||
|
s += "$i;"
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
fun box(): String {
|
||||||
|
val list = listOf("foo", "bar", "baz")
|
||||||
|
|
||||||
|
var r = test1(list.asSequence())
|
||||||
|
if (r != "0:foo;1:bar;2:baz;") return "fail1: $r"
|
||||||
|
|
||||||
|
r = test2(list)
|
||||||
|
if (r != "foo;bar;baz;") return "fail2: $r"
|
||||||
|
|
||||||
|
r = test3(list)
|
||||||
|
if (r != "0;1;2;") return "fail3: $r"
|
||||||
|
|
||||||
|
r = test4(list.toSet())
|
||||||
|
if (r != "0;1;2;") return "fail4: $r"
|
||||||
|
|
||||||
|
return "OK"
|
||||||
|
}
|
||||||
+1
-1
@@ -17,4 +17,4 @@ fun box() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// LINES: 18 2 2 10 15 2 2 2 2 2 2 3 3 6 6 6 6 7 7 10 10 10 10 10 10 11 11 14 14 15 15 15 15 15 15 15 15 15 15 15 15 16 16
|
// LINES: 18 2 2 10 2 2 2 2 2 2 3 3 6 6 6 6 7 7 10 10 10 10 10 10 11 11 14 14 15 15 15 15 16 16
|
||||||
Reference in New Issue
Block a user