diff --git a/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/arguments/K2JSCompilerArguments.kt b/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/arguments/K2JSCompilerArguments.kt index aba055ff0da..5fcddbc3bf7 100644 --- a/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/arguments/K2JSCompilerArguments.kt +++ b/compiler/cli/cli-common/src/org/jetbrains/kotlin/cli/common/arguments/K2JSCompilerArguments.kt @@ -178,6 +178,12 @@ class K2JSCompilerArguments : CommonCompilerArguments() { @Argument(value = "-Xir-new-ir2js", description = "New fragment-based ir2js") var irNewIr2Js: Boolean by FreezableVar(true) + @Argument( + value = "-Xir-generate-inline-anonymous-functions", + description = "Lambda expressions that capture values are translated into in-line anonymous JavaScript functions" + ) + var irGenerateInlineAnonymousFunctions: Boolean by FreezableVar(false) + @Argument( value = "-Xinclude", valueDescription = "", diff --git a/compiler/cli/cli-js/src/org/jetbrains/kotlin/cli/js/K2JsIrCompiler.kt b/compiler/cli/cli-js/src/org/jetbrains/kotlin/cli/js/K2JsIrCompiler.kt index 6cd1f6dad9e..e6f4ea9b289 100644 --- a/compiler/cli/cli-js/src/org/jetbrains/kotlin/cli/js/K2JsIrCompiler.kt +++ b/compiler/cli/cli-js/src/org/jetbrains/kotlin/cli/js/K2JsIrCompiler.kt @@ -163,6 +163,7 @@ class K2JsIrCompiler : CLICompiler() { configurationJs.put(CLIConfigurationKeys.ALLOW_KOTLIN_PACKAGE, arguments.allowKotlinPackage) configurationJs.put(CLIConfigurationKeys.RENDER_DIAGNOSTIC_INTERNAL_NAME, arguments.renderInternalDiagnosticNames) configurationJs.put(JSConfigurationKeys.PROPERTY_LAZY_INITIALIZATION, arguments.irPropertyLazyInitialization) + configurationJs.put(JSConfigurationKeys.GENERATE_INLINE_ANONYMOUS_FUNCTIONS, arguments.irGenerateInlineAnonymousFunctions) if (!checkKotlinPackageUsage(environmentForJS.configuration, sourcesFiles)) return ExitCode.COMPILATION_ERROR diff --git a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/InteropCallableReferenceLowering.kt b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/InteropCallableReferenceLowering.kt index 68b49692db6..774c3b3217f 100644 --- a/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/InteropCallableReferenceLowering.kt +++ b/compiler/ir/backend.js/src/org/jetbrains/kotlin/ir/backend/js/lower/InteropCallableReferenceLowering.kt @@ -28,12 +28,16 @@ import org.jetbrains.kotlin.ir.types.classifierOrNull import org.jetbrains.kotlin.ir.types.isUnit import org.jetbrains.kotlin.ir.util.* import org.jetbrains.kotlin.ir.visitors.* +import org.jetbrains.kotlin.js.config.JSConfigurationKeys import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.utils.addToStdlib.cast import org.jetbrains.kotlin.utils.addToStdlib.safeAs class InteropCallableReferenceLowering(val context: JsIrBackendContext) : BodyLoweringPass { + val generateInlineAnonymousFunctions: Boolean + get() = context.configuration.getBoolean(JSConfigurationKeys.GENERATE_INLINE_ANONYMOUS_FUNCTIONS) + /** * A factory that creates [IrFunctionExpression] for lambdas being constructed. * @@ -121,7 +125,8 @@ class InteropCallableReferenceLowering(val context: JsIrBackendContext) : BodyLo val closureUsageAnalyser = ClosureUsageAnalyser() - irFile.acceptChildrenVoid(closureUsageAnalyser) // TODO: Only do this if lambda inlining is enabled with a feature flag! + if (generateInlineAnonymousFunctions) + irFile.acceptChildrenVoid(closureUsageAnalyser) val callableReferenceClassTransformer = CallableReferenceClassTransformer( ctorToFactoryMap, @@ -317,8 +322,9 @@ class InteropCallableReferenceLowering(val context: JsIrBackendContext) : BodyLo // This allows us to avoid allocating a new object each time the lambda is created. liftLambda(ctorToFreeFunctionMap, lambdaInfo) } else if ( + generateInlineAnonymousFunctions // If the lambda constructor is called from more than one place, don't inline. - closureUsageAnalyser.getLambdaConstructorCalls(lambdaClass.primaryConstructor!!.symbol).size == 1 + && closureUsageAnalyser.getLambdaConstructorCalls(lambdaClass.primaryConstructor!!.symbol).size == 1 // In-line anonymous functions that capture variables declared in loops are dangerous. // See https://stackoverflow.com/questions/750486/javascript-closure-inside-loops-simple-practical-example && !closureUsageAnalyser.lambdaCapturesVariablesDeclaredInLoops(lambdaClass) diff --git a/compiler/testData/cli/js/jsExtraHelp.out b/compiler/testData/cli/js/jsExtraHelp.out index 72727d18946..9027bd8ca8c 100644 --- a/compiler/testData/cli/js/jsExtraHelp.out +++ b/compiler/testData/cli/js/jsExtraHelp.out @@ -17,6 +17,8 @@ where advanced options include: Print declarations' reachability info to stdout during performing DCE -Xir-dce-runtime-diagnostic={log|exception} Enable runtime diagnostics when performing DCE instead of removing declarations + -Xir-generate-inline-anonymous-functions + Lambda expressions that capture values are translated into in-line anonymous JavaScript functions -Xir-minimized-member-names Perform minimization for names of members -Xir-module-name= Specify a compilation module name for IR backend -Xir-new-ir2js New fragment-based ir2js diff --git a/compiler/tests-common-new/tests/org/jetbrains/kotlin/test/directives/JsEnvironmentConfigurationDirectives.kt b/compiler/tests-common-new/tests/org/jetbrains/kotlin/test/directives/JsEnvironmentConfigurationDirectives.kt index 8367226c924..632aa721152 100644 --- a/compiler/tests-common-new/tests/org/jetbrains/kotlin/test/directives/JsEnvironmentConfigurationDirectives.kt +++ b/compiler/tests-common-new/tests/org/jetbrains/kotlin/test/directives/JsEnvironmentConfigurationDirectives.kt @@ -112,6 +112,11 @@ object JsEnvironmentConfigurationDirectives : SimpleDirectivesContainer() { applicability = DirectiveApplicability.Global ) + val GENERATE_INLINE_ANONYMOUS_FUNCTIONS by directive( + description = "translate lambdas into in-line anonymous functions", + applicability = DirectiveApplicability.Global + ) + val SAFE_EXTERNAL_BOOLEAN by directive( description = "", applicability = DirectiveApplicability.Global diff --git a/compiler/tests-common-new/tests/org/jetbrains/kotlin/test/services/CompilerConfigurationProvider.kt b/compiler/tests-common-new/tests/org/jetbrains/kotlin/test/services/CompilerConfigurationProvider.kt index 2c7ae289a7c..bf0c00b29a6 100644 --- a/compiler/tests-common-new/tests/org/jetbrains/kotlin/test/services/CompilerConfigurationProvider.kt +++ b/compiler/tests-common-new/tests/org/jetbrains/kotlin/test/services/CompilerConfigurationProvider.kt @@ -120,6 +120,10 @@ fun createCompilerConfiguration(module: TestModule, configurators: List PROPERTY_LAZY_INITIALIZATION = CompilerConfigurationKey.create("perform lazy initialization for properties"); + public static final CompilerConfigurationKey GENERATE_INLINE_ANONYMOUS_FUNCTIONS = + CompilerConfigurationKey.create("translate lambdas into in-line anonymous functions"); + public static final CompilerConfigurationKey WASM_ENABLE_ARRAY_RANGE_CHECKS = CompilerConfigurationKey.create("enable array range checks"); diff --git a/js/js.tests/test/org/jetbrains/kotlin/generators/tests/GenerateJsTests.kt b/js/js.tests/test/org/jetbrains/kotlin/generators/tests/GenerateJsTests.kt index 3e53c57b683..c03cdfe8901 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/generators/tests/GenerateJsTests.kt +++ b/js/js.tests/test/org/jetbrains/kotlin/generators/tests/GenerateJsTests.kt @@ -79,7 +79,7 @@ fun main(args: Array) { generateTestGroupSuiteWithJUnit5(args) { testGroup("js/js.tests/tests-gen", "js/js.translator/testData", testRunnerMethodName = "runTest0") { testClass { - model("box/", pattern = "^([^_](.+))\\.kt$") + model("box/", pattern = "^([^_](.+))\\.kt$", excludeDirs = listOf("closure/inlineAnonymousFunctions")) } testClass { diff --git a/js/js.tests/test/org/jetbrains/kotlin/js/test/ir/AbstractJsIrTest.kt b/js/js.tests/test/org/jetbrains/kotlin/js/test/ir/AbstractJsIrTest.kt index f0ec9491027..07c9f919c32 100644 --- a/js/js.tests/test/org/jetbrains/kotlin/js/test/ir/AbstractJsIrTest.kt +++ b/js/js.tests/test/org/jetbrains/kotlin/js/test/ir/AbstractJsIrTest.kt @@ -65,6 +65,12 @@ abstract class AbstractJsIrTest( ::JsIrRecompiledArtifactsIdentityHandler, ) } + + forTestsMatching("${JsEnvironmentConfigurator.TEST_DATA_DIR_PATH}/box/closure/inlineAnonymousFunctions/*") { + defaultDirectives { + +JsEnvironmentConfigurationDirectives.GENERATE_INLINE_ANONYMOUS_FUNCTIONS + } + } } } } diff --git a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/BoxJsTestGenerated.java b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/BoxJsTestGenerated.java index 95e223916a1..8b97b0d4fd6 100644 --- a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/BoxJsTestGenerated.java +++ b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/BoxJsTestGenerated.java @@ -22,7 +22,7 @@ import java.util.regex.Pattern; public class BoxJsTestGenerated extends AbstractBoxJsTest { @Test public void testAllFilesPresentInBox() throws Exception { - KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("js/js.translator/testData/box"), Pattern.compile("^([^_](.+))\\.kt$"), null, TargetBackend.JS, true); + KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("js/js.translator/testData/box"), Pattern.compile("^([^_](.+))\\.kt$"), null, TargetBackend.JS, true, "closure/inlineAnonymousFunctions"); } @Nested @@ -347,7 +347,7 @@ public class BoxJsTestGenerated extends AbstractBoxJsTest { public class Closure { @Test public void testAllFilesPresentInClosure() throws Exception { - KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("js/js.translator/testData/box/closure"), Pattern.compile("^([^_](.+))\\.kt$"), null, TargetBackend.JS, true); + KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("js/js.translator/testData/box/closure"), Pattern.compile("^([^_](.+))\\.kt$"), null, TargetBackend.JS, true, "inlineAnonymousFunctions"); } @Test @@ -356,12 +356,6 @@ public class BoxJsTestGenerated extends AbstractBoxJsTest { runTest("js/js.translator/testData/box/closure/closureArrayListInstance.kt"); } - @Test - @TestMetadata("closureCodeSize.kt") - public void testClosureCodeSize() throws Exception { - runTest("js/js.translator/testData/box/closure/closureCodeSize.kt"); - } - @Test @TestMetadata("closureFunctionAsArgument.kt") public void testClosureFunctionAsArgument() throws Exception { diff --git a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrBoxJsTestGenerated.java b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrBoxJsTestGenerated.java index 311d6fc37e7..3908aa9dba2 100644 --- a/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrBoxJsTestGenerated.java +++ b/js/js.tests/tests-gen/org/jetbrains/kotlin/js/test/ir/IrBoxJsTestGenerated.java @@ -356,12 +356,6 @@ public class IrBoxJsTestGenerated extends AbstractIrBoxJsTest { runTest("js/js.translator/testData/box/closure/closureArrayListInstance.kt"); } - @Test - @TestMetadata("closureCodeSize.kt") - public void testClosureCodeSize() throws Exception { - runTest("js/js.translator/testData/box/closure/closureCodeSize.kt"); - } - @Test @TestMetadata("closureFunctionAsArgument.kt") public void testClosureFunctionAsArgument() throws Exception { @@ -631,6 +625,58 @@ public class IrBoxJsTestGenerated extends AbstractIrBoxJsTest { public void testWrappedVariableInExtensionFun() throws Exception { runTest("js/js.translator/testData/box/closure/wrappedVariableInExtensionFun.kt"); } + + @Nested + @TestMetadata("js/js.translator/testData/box/closure/inlineAnonymousFunctions") + @TestDataPath("$PROJECT_ROOT") + public class InlineAnonymousFunctions { + @Test + public void testAllFilesPresentInInlineAnonymousFunctions() throws Exception { + KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("js/js.translator/testData/box/closure/inlineAnonymousFunctions"), Pattern.compile("^([^_](.+))\\.kt$"), null, TargetBackend.JS_IR, true); + } + + @Test + @TestMetadata("closureCodeSize.kt") + public void testClosureCodeSize() throws Exception { + runTest("js/js.translator/testData/box/closure/inlineAnonymousFunctions/closureCodeSize.kt"); + } + + @Test + @TestMetadata("closureInWithInsideWith.kt") + public void testClosureInWithInsideWith() throws Exception { + runTest("js/js.translator/testData/box/closure/inlineAnonymousFunctions/closureInWithInsideWith.kt"); + } + + @Test + @TestMetadata("inlineChain.kt") + public void testInlineChain() throws Exception { + runTest("js/js.translator/testData/box/closure/inlineAnonymousFunctions/inlineChain.kt"); + } + + @Test + @TestMetadata("lambdaChain.kt") + public void testLambdaChain() throws Exception { + runTest("js/js.translator/testData/box/closure/inlineAnonymousFunctions/lambdaChain.kt"); + } + + @Test + @TestMetadata("lambdaParameters.kt") + public void testLambdaParameters() throws Exception { + runTest("js/js.translator/testData/box/closure/inlineAnonymousFunctions/lambdaParameters.kt"); + } + + @Test + @TestMetadata("localParameterInCallback.kt") + public void testLocalParameterInCallback() throws Exception { + runTest("js/js.translator/testData/box/closure/inlineAnonymousFunctions/localParameterInCallback.kt"); + } + + @Test + @TestMetadata("twiceRegeneratedAnonymousObject.kt") + public void testTwiceRegeneratedAnonymousObject() throws Exception { + runTest("js/js.translator/testData/box/closure/inlineAnonymousFunctions/twiceRegeneratedAnonymousObject.kt"); + } + } } @Nested diff --git a/js/js.translator/testData/box/closure/closureCodeSize.kt b/js/js.translator/testData/box/closure/inlineAnonymousFunctions/closureCodeSize.kt similarity index 100% rename from js/js.translator/testData/box/closure/closureCodeSize.kt rename to js/js.translator/testData/box/closure/inlineAnonymousFunctions/closureCodeSize.kt diff --git a/js/js.translator/testData/box/closure/inlineAnonymousFunctions/closureInWithInsideWith.kt b/js/js.translator/testData/box/closure/inlineAnonymousFunctions/closureInWithInsideWith.kt new file mode 100644 index 00000000000..59d2fda529a --- /dev/null +++ b/js/js.translator/testData/box/closure/inlineAnonymousFunctions/closureInWithInsideWith.kt @@ -0,0 +1,25 @@ +// KT-4237 With in with + +package foo + +class A { + val ok = "OK" +} + +class B + +fun with(o: T, body: T.() -> Unit) { + o.body() +} + +fun box(): String { + var o = "" + + with(A()) { + with(B()) { + o = ok + } + } + + return o +} diff --git a/js/js.translator/testData/box/closure/inlineAnonymousFunctions/inlineChain.kt b/js/js.translator/testData/box/closure/inlineAnonymousFunctions/inlineChain.kt new file mode 100644 index 00000000000..ff2504b3164 --- /dev/null +++ b/js/js.translator/testData/box/closure/inlineAnonymousFunctions/inlineChain.kt @@ -0,0 +1,29 @@ +// FILE: 1.kt + +package test + +interface A { + fun run() +} + +inline fun testNested(crossinline f: (String) -> Unit) { + object : A { + override fun run() { + f("OK") + } + }.run() +} + +inline fun test(crossinline f: (String) -> Unit) { + testNested { it -> { f(it) }()} +} + +// FILE: 2.kt + +import test.* + +fun box(): String { + var result = "fail" + test { it -> result = it } + return result +} diff --git a/js/js.translator/testData/box/closure/inlineAnonymousFunctions/lambdaChain.kt b/js/js.translator/testData/box/closure/inlineAnonymousFunctions/lambdaChain.kt new file mode 100644 index 00000000000..3dfa283eec8 --- /dev/null +++ b/js/js.translator/testData/box/closure/inlineAnonymousFunctions/lambdaChain.kt @@ -0,0 +1,29 @@ +// FILE: 1.kt + +package test + +inline fun inlineFun(arg: T, f: (T) -> Unit) { + f(arg) +} + +// FILE: 2.kt + +import test.* + +fun box(): String { + val param = "start" + var result = "fail" + inlineFun("1") { c -> + { + inlineFun("2") { a -> + { + { + result = param + c + a + }() + }() + } + }() + } + + return if (result == "start12") "OK" else "fail: $result" +} diff --git a/js/js.translator/testData/box/closure/inlineAnonymousFunctions/lambdaParameters.kt b/js/js.translator/testData/box/closure/inlineAnonymousFunctions/lambdaParameters.kt new file mode 100644 index 00000000000..f2406f28905 --- /dev/null +++ b/js/js.translator/testData/box/closure/inlineAnonymousFunctions/lambdaParameters.kt @@ -0,0 +1,16 @@ +fun bar(x: T, y: (T) -> Boolean): Boolean = y(x) && jsTypeOf(x.asDynamic()) != "number" + +fun typeOf(x: dynamic) = js("typeof x") + +fun box(): String { + val f = { x: Char -> + val a: Char = x + val b: Any = x + typeOf(a) == "number" && typeOf(b) == "object" + } + + if (!f('Q')) return "fail1" + if (!bar('W', f)) return "fail2" + + return "OK" +} diff --git a/js/js.translator/testData/box/closure/inlineAnonymousFunctions/localParameterInCallback.kt b/js/js.translator/testData/box/closure/inlineAnonymousFunctions/localParameterInCallback.kt new file mode 100644 index 00000000000..2bfdc15ac3e --- /dev/null +++ b/js/js.translator/testData/box/closure/inlineAnonymousFunctions/localParameterInCallback.kt @@ -0,0 +1,30 @@ +// KJS_WITH_FULL_RUNTIME +package foo + + +fun box(): String { + val oneTwo = Array(2) { + it + 1 + } + val a = ArrayList<() -> Int>() + for (i in oneTwo) { + for (l in 1..2) { + val j = l + a.add({ + var res = 0 + for (t in 0..2) { + res += i * j + } + res + }) + } + } + var sum = 0 + for (f in a) { + sum += f() + } + + if (sum != 27) return "fail: $sum" + + return "OK" +} diff --git a/js/js.translator/testData/box/closure/inlineAnonymousFunctions/twiceRegeneratedAnonymousObject.kt b/js/js.translator/testData/box/closure/inlineAnonymousFunctions/twiceRegeneratedAnonymousObject.kt new file mode 100644 index 00000000000..f757d211c48 --- /dev/null +++ b/js/js.translator/testData/box/closure/inlineAnonymousFunctions/twiceRegeneratedAnonymousObject.kt @@ -0,0 +1,26 @@ +// WITH_STDLIB +// WITH_COROUTINES +// FILE: 1.kt +package test + +inline fun foo(crossinline x: () -> Unit) = suspend { + try { } finally { + // This object is regenerated twice (normal return & "catch Throwable, execute finally, and rethrow") + // It doesn't *need* to be, but this should work regardless. + { x() }() + } +} + +// FILE: 2.kt +import helpers.* +import kotlin.coroutines.* +import kotlin.coroutines.intrinsics.* +import test.* + +var result = "fail" +fun box(): String { + suspend { + foo { result = "OK" }() + }.startCoroutine(EmptyContinuation) + return result +}