JS/Inlining: introduce removal of expression statements without side effects
This commit is contained in:
@@ -103,7 +103,7 @@ public class JsInliner extends JsVisitorWithContextImpl {
|
|||||||
RemoveUnusedLocalFunctionDeclarationsKt.removeUnusedLocalFunctionDeclarations(function);
|
RemoveUnusedLocalFunctionDeclarationsKt.removeUnusedLocalFunctionDeclarations(function);
|
||||||
processedFunctions.add(function);
|
processedFunctions.add(function);
|
||||||
|
|
||||||
new FunctionPostProcessor(function.getBody()).apply();
|
new FunctionPostProcessor(function).apply();
|
||||||
|
|
||||||
assert inProcessFunctions.contains(function);
|
assert inProcessFunctions.contains(function);
|
||||||
inProcessFunctions.remove(function);
|
inProcessFunctions.remove(function);
|
||||||
|
|||||||
@@ -16,16 +16,17 @@
|
|||||||
|
|
||||||
package org.jetbrains.kotlin.js.inline.clean
|
package org.jetbrains.kotlin.js.inline.clean
|
||||||
|
|
||||||
import com.google.dart.compiler.backend.js.ast.JsBlock
|
import com.google.dart.compiler.backend.js.ast.JsFunction
|
||||||
|
|
||||||
class FunctionPostProcessor(private val root: JsBlock) {
|
class FunctionPostProcessor(root: JsFunction) {
|
||||||
val optimizations = listOf(
|
val optimizations = listOf(
|
||||||
{ TemporaryAssignmentElimination(root).apply() },
|
{ TemporaryAssignmentElimination(root.body).apply() },
|
||||||
{ RedundantLabelRemoval(root).apply() },
|
{ RedundantLabelRemoval(root.body).apply() },
|
||||||
{ TemporaryVariableElimination(root).apply() },
|
{ TemporaryVariableElimination(root.body).apply() },
|
||||||
{ IfStatementReduction(root).apply() },
|
{ IfStatementReduction(root.body).apply() },
|
||||||
{ DeadCodeElimination(root).apply() },
|
{ DeadCodeElimination(root.body).apply() },
|
||||||
{ RedundantVariableDeclarationElimination(root).apply() }
|
{ RedundantVariableDeclarationElimination(root.body).apply() },
|
||||||
|
{ IneffectiveStatementElimination(root).apply() }
|
||||||
)
|
)
|
||||||
// TODO: reduce to A || B, A && B if possible
|
// TODO: reduce to A || B, A && B if possible
|
||||||
|
|
||||||
|
|||||||
+151
@@ -0,0 +1,151 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010-2016 JetBrains s.r.o.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.jetbrains.kotlin.js.inline.clean
|
||||||
|
|
||||||
|
import com.google.dart.compiler.backend.js.ast.*
|
||||||
|
import com.google.dart.compiler.backend.js.ast.metadata.sideEffects
|
||||||
|
import com.google.dart.compiler.backend.js.ast.metadata.synthetic
|
||||||
|
import org.jetbrains.kotlin.js.inline.util.collectLocalVariables
|
||||||
|
import org.jetbrains.kotlin.js.translate.utils.JsAstUtils
|
||||||
|
|
||||||
|
class IneffectiveStatementElimination(private val root: JsFunction) {
|
||||||
|
private val localVars = mutableSetOf<JsName>()
|
||||||
|
private var hasChanges = false
|
||||||
|
|
||||||
|
fun apply(): Boolean {
|
||||||
|
analyze()
|
||||||
|
process()
|
||||||
|
return hasChanges
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun analyze() {
|
||||||
|
localVars += root.collectLocalVariables()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun process() {
|
||||||
|
object : JsVisitorWithContextImpl() {
|
||||||
|
override fun visit(x: JsExpressionStatement, ctx: JsContext<JsNode>): Boolean {
|
||||||
|
if (x.synthetic) {
|
||||||
|
val replacement = replace(x.expression)
|
||||||
|
if (replacement.size != 1 || replacement[0] != x.expression) {
|
||||||
|
hasChanges = true
|
||||||
|
ctx.addPrevious(replacement.map { JsExpressionStatement(it).apply { synthetic = true } }.toList())
|
||||||
|
ctx.removeMe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.visit(x, ctx)
|
||||||
|
}
|
||||||
|
}.accept(root.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun replace(expression: JsExpression): List<JsExpression> {
|
||||||
|
return when (expression) {
|
||||||
|
is JsNameRef -> {
|
||||||
|
val qualifier = expression.qualifier
|
||||||
|
if (qualifier == null && expression.name in localVars) {
|
||||||
|
listOf()
|
||||||
|
}
|
||||||
|
else if (!expression.sideEffects) {
|
||||||
|
if (qualifier != null) replace(qualifier) else listOf()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
listOf(expression)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is JsUnaryOperation -> {
|
||||||
|
when (expression.operator) {
|
||||||
|
JsUnaryOperator.DEC, JsUnaryOperator.INC, JsUnaryOperator.DELETE -> listOf(expression)
|
||||||
|
else -> replace(expression.arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is JsBinaryOperation -> {
|
||||||
|
if (expression.sideEffects) {
|
||||||
|
when (expression.operator) {
|
||||||
|
JsBinaryOperator.AND, JsBinaryOperator.OR -> {
|
||||||
|
val right = replace(expression.arg2)
|
||||||
|
if (right.isEmpty()) replace(expression.arg1) else listOf(expression)
|
||||||
|
}
|
||||||
|
JsBinaryOperator.INOP, JsBinaryOperator.INSTANCEOF -> listOf(expression)
|
||||||
|
else -> {
|
||||||
|
if (!expression.operator.isAssignment) {
|
||||||
|
replace(expression.arg1) + replace(expression.arg2)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
listOf(expression)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
listOf(expression)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is JsInvocation -> {
|
||||||
|
if (!expression.sideEffects) {
|
||||||
|
replace(expression.qualifier) + replaceMany(expression.arguments)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
listOf(expression)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is JsNew -> {
|
||||||
|
if (!expression.sideEffects) {
|
||||||
|
replace(expression.constructorExpression) + replaceMany(expression.arguments)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
listOf(expression)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is JsConditional -> {
|
||||||
|
val thenExpr = replace(expression.thenExpression)
|
||||||
|
val elseExpr = replace(expression.elseExpression)
|
||||||
|
when {
|
||||||
|
thenExpr.isEmpty() && elseExpr.isEmpty() -> replace(expression.testExpression)
|
||||||
|
thenExpr.isEmpty() -> listOf(JsAstUtils.or(expression.testExpression, expression.elseExpression))
|
||||||
|
elseExpr.isEmpty() -> listOf(JsAstUtils.and(expression.testExpression, expression.thenExpression))
|
||||||
|
else -> listOf(expression)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is JsArrayAccess -> {
|
||||||
|
if (!expression.sideEffects) {
|
||||||
|
replace(expression.arrayExpression) + replace(expression.indexExpression)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
listOf(expression)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is JsLiteral.JsValueLiteral -> listOf()
|
||||||
|
|
||||||
|
is JsArrayLiteral -> replaceMany(expression.expressions)
|
||||||
|
|
||||||
|
is JsObjectLiteral -> expression.propertyInitializers.map { replace(it.labelExpr) + replace(it.valueExpr) }.flatten()
|
||||||
|
|
||||||
|
is JsFunction -> if (expression.name == null) listOf() else listOf(expression)
|
||||||
|
|
||||||
|
else -> listOf(expression)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun replaceMany(expressions: List<JsExpression>) = expressions.map { replace(it) }.flatten()
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2010-2015 JetBrains s.r.o.
|
* Copyright 2010-2016 JetBrains s.r.o.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -125,6 +125,8 @@ fun collectDefinedNames(scope: JsNode): Set<JsName> {
|
|||||||
|
|
||||||
fun JsFunction.collectFreeVariables() = collectUsedNames(body) - collectDefinedNames(body) - parameters.map { it.name }
|
fun JsFunction.collectFreeVariables() = collectUsedNames(body) - collectDefinedNames(body) - parameters.map { it.name }
|
||||||
|
|
||||||
|
fun JsFunction.collectLocalVariables() = collectDefinedNames(body) + parameters.map { it.name }
|
||||||
|
|
||||||
fun collectJsProperties(scope: JsNode): IdentityHashMap<JsName, JsExpression> {
|
fun collectJsProperties(scope: JsNode): IdentityHashMap<JsName, JsExpression> {
|
||||||
val collector = PropertyCollector()
|
val collector = PropertyCollector()
|
||||||
collector.accept(scope)
|
collector.accept(scope)
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ abstract class BasicOptimizerTest(private var basePath: String) {
|
|||||||
for (statement in unoptimizedAst) {
|
for (statement in unoptimizedAst) {
|
||||||
object : RecursiveJsVisitor() {
|
object : RecursiveJsVisitor() {
|
||||||
override fun visitFunction(x: JsFunction) {
|
override fun visitFunction(x: JsFunction) {
|
||||||
FunctionPostProcessor(x.body).apply()
|
FunctionPostProcessor(x).apply()
|
||||||
super.visitFunction(x)
|
super.visitFunction(x)
|
||||||
}
|
}
|
||||||
}.accept(statement)
|
}.accept(statement)
|
||||||
|
|||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010-2016 JetBrains s.r.o.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.jetbrains.kotlin.js.test.optimizer
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class IneffectiveStatementEliminationTest() : BasicOptimizerTest("ineffective-statement-elimination") {
|
||||||
|
@Test fun binary() = box()
|
||||||
|
|
||||||
|
@Test fun unary() = box()
|
||||||
|
|
||||||
|
@Test fun conditional() = box()
|
||||||
|
|
||||||
|
@Test fun literal() = box()
|
||||||
|
}
|
||||||
Vendored
+67
@@ -0,0 +1,67 @@
|
|||||||
|
var global = 0;
|
||||||
|
|
||||||
|
function se() {
|
||||||
|
return ++global;
|
||||||
|
}
|
||||||
|
|
||||||
|
function se2(value) {
|
||||||
|
++global;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function test1() {
|
||||||
|
se(); se(); se(); se(); se();
|
||||||
|
se(); se(); se(); se(); se();
|
||||||
|
se(); se(); se(); se(); se();
|
||||||
|
se(); se(); se(); se(); se();
|
||||||
|
se(); se(); se(); se(); se();
|
||||||
|
se(); se(); se(); se(); se();
|
||||||
|
se(); se(); se();
|
||||||
|
}
|
||||||
|
|
||||||
|
function test2() {
|
||||||
|
try {
|
||||||
|
se() in se();
|
||||||
|
return "fail2a: `in` should not be removed"
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
se() instanceof se();
|
||||||
|
return "fail2b: `instanceof` should not be removed"
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
return "OK"
|
||||||
|
}
|
||||||
|
|
||||||
|
function test3() {
|
||||||
|
var x = true;
|
||||||
|
var y = false;
|
||||||
|
|
||||||
|
se2(true);
|
||||||
|
x && se2(true);
|
||||||
|
se2(true) && se2(true);
|
||||||
|
|
||||||
|
se2(false);
|
||||||
|
y || se2(false);
|
||||||
|
se2(false) || se2(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function box() {
|
||||||
|
test1();
|
||||||
|
if (global != 33) return "fail1: " + global;
|
||||||
|
|
||||||
|
var result = test2();
|
||||||
|
if (result !== "OK") return result;
|
||||||
|
if (global != 37) return "fail2: " + global;
|
||||||
|
|
||||||
|
test3();
|
||||||
|
if (global != 45) return "fail3: " + global;
|
||||||
|
|
||||||
|
return "OK";
|
||||||
|
}
|
||||||
Vendored
+76
@@ -0,0 +1,76 @@
|
|||||||
|
var global = 0;
|
||||||
|
|
||||||
|
function se() {
|
||||||
|
return ++global;
|
||||||
|
}
|
||||||
|
|
||||||
|
function se2(value) {
|
||||||
|
++global;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function test1() {
|
||||||
|
var $a = (se() - se() + se()) * se();
|
||||||
|
var $b = se() % se() / se();
|
||||||
|
var $c = se() >> se();
|
||||||
|
var $d = se() << se();
|
||||||
|
var $e = se() >>> se();
|
||||||
|
var $f = se() | se() & se() ^ se();
|
||||||
|
var $g1 = se() == se();
|
||||||
|
var $g2 = se() === se();
|
||||||
|
var $g3 = se() != se();
|
||||||
|
var $g4 = se() !== se();
|
||||||
|
var $h1 = se() > se();
|
||||||
|
var $h2 = se() >= se();
|
||||||
|
var $h3 = se() < se();
|
||||||
|
var $h4 = se() <= se();
|
||||||
|
}
|
||||||
|
|
||||||
|
function test2() {
|
||||||
|
try {
|
||||||
|
var $a = se() in se();
|
||||||
|
return "fail2a: `in` should not be removed"
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var $b = se() instanceof se();
|
||||||
|
return "fail2b: `instanceof` should not be removed"
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
return "OK"
|
||||||
|
}
|
||||||
|
|
||||||
|
function test3() {
|
||||||
|
var x = true;
|
||||||
|
var y = false;
|
||||||
|
|
||||||
|
var $a1 = se2(true) && x;
|
||||||
|
var $a2 = x && se2(true);
|
||||||
|
var $a3 = se2(true) && se2(true);
|
||||||
|
var $a4 = x && x;
|
||||||
|
|
||||||
|
var $b1 = se2(false) || y;
|
||||||
|
var $b2 = y || se2(false);
|
||||||
|
var $b3 = se2(false) || se2(false);
|
||||||
|
var $b4 = y || y;
|
||||||
|
}
|
||||||
|
|
||||||
|
function box() {
|
||||||
|
test1();
|
||||||
|
if (global != 33) return "fail1: " + global;
|
||||||
|
|
||||||
|
var result = test2();
|
||||||
|
if (result !== "OK") return result;
|
||||||
|
if (global != 37) return "fail2: " + global;
|
||||||
|
|
||||||
|
test3();
|
||||||
|
if (global != 45) return "fail3: " + global;
|
||||||
|
|
||||||
|
return "OK";
|
||||||
|
}
|
||||||
Vendored
+22
@@ -0,0 +1,22 @@
|
|||||||
|
var global = 0;
|
||||||
|
|
||||||
|
function se(value) {
|
||||||
|
++global;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function test() {
|
||||||
|
var x = true;
|
||||||
|
se(x);
|
||||||
|
x ? se("foo") : se("bar");
|
||||||
|
x || se("bar");
|
||||||
|
x && se("foo");
|
||||||
|
se(x) ? se("foo") : se("bar");
|
||||||
|
}
|
||||||
|
|
||||||
|
function box() {
|
||||||
|
test();
|
||||||
|
if (global != 5) return "fail1: " + global;
|
||||||
|
|
||||||
|
return "OK";
|
||||||
|
}
|
||||||
Vendored
+23
@@ -0,0 +1,23 @@
|
|||||||
|
var global = 0;
|
||||||
|
|
||||||
|
function se(value) {
|
||||||
|
++global;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function test() {
|
||||||
|
var x = true;
|
||||||
|
var $a = se(x) ? "foo" : "bar";
|
||||||
|
var $b = x ? "foo" : "bar";
|
||||||
|
var $c = x ? se("foo") : se("bar");
|
||||||
|
var $d = x ? "foo" : se("bar");
|
||||||
|
var $e = x ? se("foo") : "bar";
|
||||||
|
var $f = se(x) ? se("foo") : se("bar");
|
||||||
|
}
|
||||||
|
|
||||||
|
function box() {
|
||||||
|
test();
|
||||||
|
if (global != 5) return "fail1: " + global;
|
||||||
|
|
||||||
|
return "OK";
|
||||||
|
}
|
||||||
Vendored
+23
@@ -0,0 +1,23 @@
|
|||||||
|
var global = 0;
|
||||||
|
|
||||||
|
function se(value) {
|
||||||
|
++global;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function test() {
|
||||||
|
se(2);
|
||||||
|
se(3);
|
||||||
|
se('foo');
|
||||||
|
se('bar');
|
||||||
|
se('y');
|
||||||
|
se(3);
|
||||||
|
se('null');
|
||||||
|
}
|
||||||
|
|
||||||
|
function box() {
|
||||||
|
test();
|
||||||
|
if (global != 7) return "fail: " + test();
|
||||||
|
|
||||||
|
return "OK";
|
||||||
|
}
|
||||||
Vendored
+21
@@ -0,0 +1,21 @@
|
|||||||
|
var global = 0;
|
||||||
|
|
||||||
|
function se(value) {
|
||||||
|
++global;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function test() {
|
||||||
|
var $a = { x: se(2), y: se(3) };
|
||||||
|
var $b = [se("foo"), se("bar")];
|
||||||
|
var $c = "x" + se("y");
|
||||||
|
var $d = 2 + se(3);
|
||||||
|
var $e = se("null") + null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function box() {
|
||||||
|
test();
|
||||||
|
if (global != 7) return "fail: " + test();
|
||||||
|
|
||||||
|
return "OK";
|
||||||
|
}
|
||||||
Vendored
+41
@@ -0,0 +1,41 @@
|
|||||||
|
var global = 0;
|
||||||
|
|
||||||
|
function se(value) {
|
||||||
|
++global;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function test1() {
|
||||||
|
se(23);
|
||||||
|
se(23);
|
||||||
|
se(false);
|
||||||
|
se(-1);
|
||||||
|
se(23);
|
||||||
|
se(23);
|
||||||
|
}
|
||||||
|
|
||||||
|
function test2() {
|
||||||
|
++global;
|
||||||
|
global++;
|
||||||
|
--global;
|
||||||
|
global--;
|
||||||
|
}
|
||||||
|
|
||||||
|
function test3() {
|
||||||
|
var obj = { x: 2, y: 3};
|
||||||
|
delete obj.x;
|
||||||
|
return ("x" in obj) + ":" + ("y" in obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
function box() {
|
||||||
|
test1();
|
||||||
|
if (global != 6) return "fail1: " + global;
|
||||||
|
|
||||||
|
test2();
|
||||||
|
if (global != 6) return "fail2: " + global;
|
||||||
|
|
||||||
|
var result = test3();
|
||||||
|
if (result != "false:true") return "fail3: " + result;
|
||||||
|
|
||||||
|
return "OK";
|
||||||
|
}
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
var global = 0;
|
||||||
|
|
||||||
|
function se(value) {
|
||||||
|
++global;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function test1() {
|
||||||
|
var $a = +se(23);
|
||||||
|
var $b = -se(23);
|
||||||
|
var $c = !se(false);
|
||||||
|
var $d = ~se(-1);
|
||||||
|
var $e = typeof se(23);
|
||||||
|
var $f = void se(23);
|
||||||
|
}
|
||||||
|
|
||||||
|
function test2() {
|
||||||
|
var $a = ++global;
|
||||||
|
var $b = global++;
|
||||||
|
var $c = --global;
|
||||||
|
var $d = global--;
|
||||||
|
}
|
||||||
|
|
||||||
|
function test3() {
|
||||||
|
var obj = { x: 2, y: 3};
|
||||||
|
var $tmp = delete obj.x;
|
||||||
|
return ("x" in obj) + ":" + ("y" in obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
function box() {
|
||||||
|
test1();
|
||||||
|
if (global != 6) return "fail1: " + global;
|
||||||
|
|
||||||
|
test2();
|
||||||
|
if (global != 6) return "fail2: " + global;
|
||||||
|
|
||||||
|
var result = test3();
|
||||||
|
if (result != "false:true") return "fail3: " + result;
|
||||||
|
|
||||||
|
return "OK";
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user