From a0ff581a2f92f88e66578a33f43722716a9fa434 Mon Sep 17 00:00:00 2001 From: Anton Bannykh Date: Thu, 22 Nov 2018 15:39:45 +0300 Subject: [PATCH] JS: refactoring --- .../kotlin/test/KotlinTestUtils.java | 5 + .../js/backend/ast/JsProgramFragment.kt | 2 + .../kotlin/js/inline/ApplyWrapper.kt | 139 -------- .../kotlin/js/inline/FunctionInlineMutator.kt | 4 +- .../kotlin/js/inline/FunctionReader.kt | 64 ++-- .../js/inline/InlineFunctionDefinition.kt | 70 ++++ .../inline/InlineSuspendFunctionSplitter.kt | 63 +--- .../kotlin/js/inline/InlineableResult.kt | 2 +- ...sController.kt => InlinerCycleReporter.kt} | 45 +-- .../jetbrains/kotlin/js/inline/InlinerImpl.kt | 220 ++++-------- .../kotlin/js/inline/InliningScope.kt | 336 ++++++++++++++++++ .../jetbrains/kotlin/js/inline/JsInliner.kt | 160 +-------- .../js/inline/context/FunctionContext.kt | 107 ++++-- .../kotlin/js/inline/context/NamingContext.kt | 4 +- .../kotlin/js/inline/util/collectUtils.kt | 2 +- .../kotlin/js/facade/K2JSTranslator.kt | 4 +- .../translate/general/AstGenerationResult.kt | 2 +- .../js/translate/general/Translation.java | 2 +- 18 files changed, 663 insertions(+), 568 deletions(-) delete mode 100644 js/js.inliner/src/org/jetbrains/kotlin/js/inline/ApplyWrapper.kt create mode 100644 js/js.inliner/src/org/jetbrains/kotlin/js/inline/InlineFunctionDefinition.kt rename js/js.inliner/src/org/jetbrains/kotlin/js/inline/{InlineDfsController.kt => InlinerCycleReporter.kt} (73%) create mode 100644 js/js.inliner/src/org/jetbrains/kotlin/js/inline/InliningScope.kt diff --git a/compiler/tests-common/tests/org/jetbrains/kotlin/test/KotlinTestUtils.java b/compiler/tests-common/tests/org/jetbrains/kotlin/test/KotlinTestUtils.java index 6d68261bd79..559c6545d8b 100644 --- a/compiler/tests-common/tests/org/jetbrains/kotlin/test/KotlinTestUtils.java +++ b/compiler/tests-common/tests/org/jetbrains/kotlin/test/KotlinTestUtils.java @@ -107,6 +107,9 @@ public class KotlinTestUtils { private static final boolean DONT_IGNORE_TESTS_WORKING_ON_COMPATIBLE_BACKEND = Boolean.getBoolean("org.jetbrains.kotlin.dont.ignore.tests.working.on.compatible.backend"); + private static final boolean SKIP_IR_TESTS = + Boolean.getBoolean("org.jetbrains.kotlin.skip.ir.tests"); + private static final boolean AUTOMATICALLY_UNMUTE_PASSED_TESTS = false; private static final boolean AUTOMATICALLY_MUTE_FAILED_TESTS = false; @@ -1045,6 +1048,8 @@ public class KotlinTestUtils { // * sometimes, for too common/general names, it shows many variants to navigate // * it adds an additional step for navigation -- you must choose an exact file to navigate public static void runTest0(DoTest test, TargetBackend targetBackend, String testDataFilePath) throws Exception { + if (SKIP_IR_TESTS && (targetBackend == TargetBackend.JS_IR || targetBackend == TargetBackend.JVM_IR)) return; + File testDataFile = new File(testDataFilePath); boolean isIgnored = isIgnoredTarget(targetBackend, testDataFile); diff --git a/js/js.ast/src/org/jetbrains/kotlin/js/backend/ast/JsProgramFragment.kt b/js/js.ast/src/org/jetbrains/kotlin/js/backend/ast/JsProgramFragment.kt index 8e7ce23fd03..b1113891991 100644 --- a/js/js.ast/src/org/jetbrains/kotlin/js/backend/ast/JsProgramFragment.kt +++ b/js/js.ast/src/org/jetbrains/kotlin/js/backend/ast/JsProgramFragment.kt @@ -17,4 +17,6 @@ class JsProgramFragment(val scope: JsScope, val packageFqn: String) { val inlineModuleMap: MutableMap = LinkedHashMap() var tests: JsStatement? = null var mainFunction: JsStatement? = null + // TODO serialize/deserialize; teach Merger about this thing + val inlinedFunctionWrappers = mutableMapOf() } diff --git a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/ApplyWrapper.kt b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/ApplyWrapper.kt deleted file mode 100644 index 6506f109428..00000000000 --- a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/ApplyWrapper.kt +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license - * that can be found in the license/LICENSE.txt file. - */ - -package org.jetbrains.kotlin.js.inline - -import org.jetbrains.kotlin.js.backend.ast.* -import org.jetbrains.kotlin.js.backend.ast.metadata.localAlias -import org.jetbrains.kotlin.js.backend.ast.metadata.staticRef -import org.jetbrains.kotlin.js.inline.util.collectDefinedNamesInAllScopes -import org.jetbrains.kotlin.js.inline.util.collectNamedFunctions -import org.jetbrains.kotlin.js.inline.util.getImportTag -import org.jetbrains.kotlin.js.inline.util.replaceNames -import org.jetbrains.kotlin.js.translate.utils.JsAstUtils -import java.util.ArrayList -import java.util.HashMap - -fun applyWrapper( - wrapper: JsBlock, function: JsFunction, originalFunction: JsFunction, - inlineFunctionDepth: Int, - replacementsInducedByWrappers: MutableMap>, - existingImports: MutableMap, - additionalImports: MutableList>, - existingNameBindings: MutableMap, - additionalNameBindings: MutableList, - inlinedModuleAliases: MutableSet, - inverseNameBindings: Map, - addPrevious: (JsStatement) -> Unit -) { - - // TODO Decrypt the comment below - // Apparently we should avoid this trick when we implement fair support for crossinline - val replacements = replacementsInducedByWrappers.computeIfAbsent(originalFunction) { k -> - val newReplacements = HashMap() - - val copiedStatements = ArrayList() - val importStatements = ArrayList>() - wrapper.statements.asSequence() - .filterNot { it is JsReturn } - .map { it.deepCopy() } - .forEach { statement -> - if (inlineFunctionDepth == 0) { - replaceExpressionsWithLocalAliases(statement, inlinedModuleAliases) - } - - if (statement is JsVars) { - val tag = getImportTag(statement) - if (tag != null) { - // TODO handle JsVars with multiple vars? - val name = statement.vars[0].name - var existingName: JsName? = if (inlineFunctionDepth == 0) name.localAlias else null - if (existingName == null) { - existingName = existingImports.computeIfAbsent(tag) { - importStatements.add(tag to statement.vars[0]) - val alias = JsScope.declareTemporaryName(name.ident) - alias.copyMetadataFrom(name) - newReplacements[name] = JsAstUtils.pureFqn(alias, null) - alias - } - } - - if (name !== existingName) { - val replacement = JsAstUtils.pureFqn(existingName, null) - newReplacements[name] = replacement - } - - return@forEach - } - } - - copiedStatements.add(statement) - } - - val definedNames = (importStatements.asSequence().map { it.second } + copiedStatements.asSequence()) - .flatMap { node -> collectDefinedNamesInAllScopes(node).asSequence() } - .filter { name -> !newReplacements.containsKey(name) } - .toSet() - for (name in definedNames) { - val alias = JsScope.declareTemporaryName(name.ident) - alias.copyMetadataFrom(name) - val replacement = JsAstUtils.pureFqn(alias, null) - newReplacements[name] = replacement - } - - for ((tag, statement) in importStatements) { - val renamed = replaceNames(statement, newReplacements) - additionalImports.add(Triple(tag,renamed.initExpression, renamed.name)) - additionalNameBindings.add(JsNameBinding(tag, renamed.name)) - } - - for (statement in copiedStatements) { - addPrevious(replaceNames(statement, newReplacements)) - } - - for ((key, value) in collectNamedFunctions(JsBlock(copiedStatements))) { - if (key.staticRef is JsFunction) { - key.staticRef = value - } - } - - newReplacements - } - - replaceNames(function, replacements) - - // Copy nameBinding's for inlined localAlias'es - for (nameRef in replacements.values) { - val name = nameRef.name - if (name != null && !existingNameBindings.containsKey(name)) { - val tag = inverseNameBindings[name] - if (tag != null) { - existingNameBindings[name] = tag - additionalNameBindings.add(JsNameBinding(tag, name)) - } - } - } -} - -private fun replaceExpressionsWithLocalAliases(statement: JsStatement, inlinedModuleAliases: MutableSet) { - object : JsVisitorWithContextImpl() { - override fun endVisit(x: JsNameRef, ctx: JsContext) { - replaceIfNecessary(x, ctx) - } - - override fun endVisit(x: JsArrayAccess, ctx: JsContext) { - replaceIfNecessary(x, ctx) - } - - private fun replaceIfNecessary(expression: JsExpression, ctx: JsContext) { - val alias = expression.localAlias - if (alias != null) { - ctx.replaceMe(alias.makeRef()) - inlinedModuleAliases.add(alias) - } - } - - }.accept(statement) -} \ No newline at end of file diff --git a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/FunctionInlineMutator.kt b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/FunctionInlineMutator.kt index 73993fc41bb..85f45587fe8 100644 --- a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/FunctionInlineMutator.kt +++ b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/FunctionInlineMutator.kt @@ -35,7 +35,7 @@ private constructor( private val invokedFunction: JsFunction val namingContext = inliningContext.newNamingContext() val body: JsBlock - var resultExpr: JsExpression? = null + var resultExpr: JsNameRef? = null private var resultName: JsName? = null var breakLabel: JsLabel? = null private val currentStatement = inliningContext.statementContext.currentNode @@ -56,7 +56,7 @@ private constructor( namingContext.applyRenameTo(body) resultExpr = resultExpr?.let { - namingContext.applyRenameTo(it) as JsExpression + namingContext.applyRenameTo(it) as JsNameRef } } diff --git a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/FunctionReader.kt b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/FunctionReader.kt index 21359f59698..e41a1e13f30 100644 --- a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/FunctionReader.kt +++ b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/FunctionReader.kt @@ -36,7 +36,6 @@ import org.jetbrains.kotlin.js.translate.utils.JsDescriptorUtils.getModuleName import org.jetbrains.kotlin.resolve.descriptorUtil.isExtension import org.jetbrains.kotlin.resolve.inline.InlineStrategy import org.jetbrains.kotlin.utils.JsLibraryUtils -import org.jetbrains.kotlin.utils.sure import java.io.File import java.io.StringReader @@ -58,8 +57,7 @@ private val SPECIAL_FUNCTION_PATTERN = Regex("var\\s+($JS_IDENTIFIER)\\s*=\\s*($ class FunctionReader( private val reporter: JsConfig.Reporter, private val config: JsConfig, - private val currentModuleName: JsName, - private val moduleNameMap: Map + private val currentModuleName: JsName ) { /** * fileContent: .js file content, that contains this module definition. @@ -84,8 +82,8 @@ class FunctionReader( val offsetToSourceMapping by lazy(offsetToSourceMappingProvider) val wrapFunctionRegex = specialFunctions.entries - .singleOrNull { (_, v) -> v == SpecialFunction.WRAP_FUNCTION }?.key - ?.let { Regex("\\s*$it\\s*\\(\\s*").toPattern() } + .filter { (_, v) -> v == SpecialFunction.WRAP_FUNCTION } // TODO This is a hack! Investigate duplicates! + .map { Regex("\\s*${it.key}\\s*\\(\\s*").toPattern() } } private val moduleNameToInfo by lazy { @@ -166,19 +164,20 @@ class FunctionReader( override fun toString() = text.substring(offset) } - private val emptyFunctionWrapper = FunctionWithWrapper(JsFunction(object : JsScope("") {}, ""), null) + private object NotFoundMarker : Any() - private val functionCache = object : SLRUCache(50, 50) { - override fun createValue(descriptor: CallableDescriptor): FunctionWithWrapper = - readFunction(descriptor) ?: emptyFunctionWrapper + private val functionCache = object : SLRUCache(50, 50) { + // LibraryInlineFunctionDefinition | NotFoundMarker + override fun createValue(key: CallableDescriptor): Any = + readFunction(key) ?: NotFoundMarker } - operator fun get(descriptor: CallableDescriptor): FunctionWithWrapper? { + operator fun get(descriptor: CallableDescriptor): LibraryInlineFunctionDefinition? { val existed = functionCache.get(descriptor) - return if (existed == emptyFunctionWrapper) null else existed + return if (existed === NotFoundMarker) null else existed as LibraryInlineFunctionDefinition } - private fun readFunction(descriptor: CallableDescriptor): FunctionWithWrapper? { + private fun readFunction(descriptor: CallableDescriptor): LibraryInlineFunctionDefinition? { val moduleName = getModuleName(descriptor) if (moduleName !in moduleNameToInfo.keys()) return null @@ -191,7 +190,8 @@ class FunctionReader( return null } - private fun readFunctionFromSource(descriptor: CallableDescriptor, info: ModuleInfo): FunctionWithWrapper? { + // TODO move renamings to a proper place + private fun readFunctionFromSource(descriptor: CallableDescriptor, info: ModuleInfo): LibraryInlineFunctionDefinition? { val source = info.fileContent var tag = Namer.getFunctionTag(descriptor, config) val tagForModule = tag @@ -213,16 +213,24 @@ class FunctionReader( } val sourcePart = ShallowSubSequence(source, offset, source.length) - val wrapFunctionMatcher = info.wrapFunctionRegex?.matcher(sourcePart) - val isWrapped = wrapFunctionMatcher?.lookingAt() == true - if (isWrapped) { - offset += wrapFunctionMatcher!!.end() + var isWrapped = false + for (regex in info.wrapFunctionRegex) { + val wrapFunctionMatcher = regex.matcher(sourcePart) + isWrapped = wrapFunctionMatcher.lookingAt() == true + if (isWrapped) { + offset += wrapFunctionMatcher!!.end() + break + } } val position = info.offsetToSourceMapping[offset] val jsScope = JsRootScope(JsProgram()) - val functionExpr = parseFunction(source, info.filePath, position, offset, ThrowExceptionOnErrorReporter, jsScope) ?: - return null + val functionExpr = try { + parseFunction(source, info.filePath, position, offset, ThrowExceptionOnErrorReporter, jsScope) ?: + return null + } catch (t: Throwable) { + throw Error("Exception while reading function '$tag' from ${info.filePath}", t) + } functionExpr.fixForwardNameReferences() val (function, wrapper) = if (isWrapped) { InlineMetadata.decomposeWrapper(functionExpr) ?: return null @@ -230,7 +238,7 @@ class FunctionReader( else { FunctionWithWrapper(functionExpr, null) } - val moduleReference = moduleNameMap[tagForModule]?.deepCopy() ?: currentModuleName.makeRef() +// val moduleReference = moduleNameMap[tagForModule]?.deepCopy() ?: currentModuleName.makeRef() val wrapperStatements = wrapper?.statements?.filter { it !is JsReturn } val sourceMap = info.sourceMap @@ -243,20 +251,22 @@ class FunctionReader( } val allDefinedNames = collectDefinedNamesInAllScopes(function) - val replacements = hashMapOf(info.moduleVariable to moduleReference, - info.kotlinVariable to Namer.kotlinObject()) - replaceExternalNames(function, replacements, allDefinedNames) - wrapperStatements?.forEach { replaceExternalNames(it, replacements, allDefinedNames) } +// val replacements = hashMapOf(info.moduleVariable to moduleReference, +// info.kotlinVariable to Namer.kotlinObject()) +// replaceExternalNames(function, replacements, allDefinedNames) +// wrapperStatements?.forEach { replaceExternalNames(it, replacements, allDefinedNames) } + function.markInlineArguments(descriptor) markDefaultParams(function) + // TODO maybe move elsewhere? markSpecialFunctions(function, allDefinedNames, info, jsScope) - val namesWithoutSizeEffects = wrapperStatements.orEmpty().asSequence() + val namesWithoutSideEffects = wrapperStatements.orEmpty().asSequence() .flatMap { collectDefinedNames(it).asSequence() } .toSet() function.accept(object : RecursiveJsVisitor() { override fun visitNameRef(nameRef: JsNameRef) { - if (nameRef.name in namesWithoutSizeEffects && nameRef.qualifier == null) { + if (nameRef.name in namesWithoutSideEffects && nameRef.qualifier == null) { nameRef.sideEffects = SideEffectKind.PURE } super.visitNameRef(nameRef) @@ -269,7 +279,7 @@ class FunctionReader( } } - return FunctionWithWrapper(function, wrapper) + return LibraryInlineFunctionDefinition(tagForModule, FunctionWithWrapper(function, wrapper), info) } private fun markSpecialFunctions(function: JsFunction, allDefinedNames: Set, info: ModuleInfo, scope: JsScope) { diff --git a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/InlineFunctionDefinition.kt b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/InlineFunctionDefinition.kt new file mode 100644 index 00000000000..07b3433c7e7 --- /dev/null +++ b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/InlineFunctionDefinition.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.js.inline + +import org.jetbrains.kotlin.js.backend.ast.JsProgramFragment +import org.jetbrains.kotlin.js.inline.util.FunctionWithWrapper + +sealed class InlineFunctionDefinition { + abstract val functionWithWrapper: FunctionWithWrapper + + abstract val tag: String? + + open fun process() {} + +// imports / nameBindings + // modules + // renameFor scope? + + +} + + +// Current module, new fragment. Expressed through `defineInlineFunction`. Should be self-contained +open class PublicInlineFunctionDefinition( + override val tag: String, + override val functionWithWrapper: FunctionWithWrapper, + val fragment: JsProgramFragment, + val scope: ProgramFragmentInliningScope +) : InlineFunctionDefinition() { + override fun process() { + scope.process() + } +} + +// Current module, from Binary AST +class BinaryInlineFunctionDefinition( + override val tag: String, + override val functionWithWrapper: FunctionWithWrapper, + val fragment: JsProgramFragment +): InlineFunctionDefinition() { + + +} + +// Current module, new fragment, used within scope only. Private functions, lambdas. +class LocalInlineFunctionDefinition( + override val functionWithWrapper: FunctionWithWrapper, + val scope: InliningScope +) : InlineFunctionDefinition() { + + override val tag = null + + override fun process() { + // TODO this is incorrect! + scope.process() + } +} + +// Deserialized from a binary dependency (.js file) +open class LibraryInlineFunctionDefinition( + override val tag: String, + override val functionWithWrapper: FunctionWithWrapper, + val moduleInfo: FunctionReader.ModuleInfo +): InlineFunctionDefinition() { + + +} \ No newline at end of file diff --git a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/InlineSuspendFunctionSplitter.kt b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/InlineSuspendFunctionSplitter.kt index 876120e281b..8951ee70f60 100644 --- a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/InlineSuspendFunctionSplitter.kt +++ b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/InlineSuspendFunctionSplitter.kt @@ -9,26 +9,11 @@ import org.jetbrains.kotlin.js.backend.ast.* import org.jetbrains.kotlin.js.backend.ast.metadata.coroutineMetadata import org.jetbrains.kotlin.js.backend.ast.metadata.isInlineableCoroutineBody import org.jetbrains.kotlin.js.translate.expression.InlineMetadata -import java.util.ArrayList -import java.util.HashMap -import java.util.HashSet class InlineSuspendFunctionSplitter( - val existingNameBindings: MutableMap, - val existingImports: MutableMap, - val inlineFunctionDepth: Int, - val inverseNameBindings: Map, - val replacementsInducedByWrappers: MutableMap>, - val addPrevious: (JsStatement) -> Unit + val scope: ProgramFragmentInliningScope ) : JsVisitorWithContextImpl() { - val inlinedModuleAliases = HashSet() - - val additionalNameBindings = ArrayList() - - val additionalImports = mutableListOf>() - - override fun endVisit(x: JsExpressionStatement, ctx: JsContext<*>) { val e = x.expression if (e is JsBinaryOperation) { @@ -57,40 +42,24 @@ class InlineSuspendFunctionSplitter( private fun splitExportedSuspendInlineFunctionDeclarations(expression: JsExpression): JsFunction? { val inlineMetadata = InlineMetadata.decompose(expression) if (inlineMetadata != null) { - val (originalFunction, wrapperBody) = inlineMetadata.function - if (originalFunction.coroutineMetadata != null) { - val statementContext = lastStatementLevelContext + inlineMetadata.function.let { f -> + if (f.function.coroutineMetadata != null) { + val statementContext = lastStatementLevelContext - // This function will be exported to JS - val function = originalFunction.deepCopy() + // This function will be exported to JS + val function = scope.importFunctionDefinition(PublicInlineFunctionDefinition(inlineMetadata.tag.value, inlineMetadata.function, scope.fragment, scope)) - // Original function should be not be transformed into a state machine - originalFunction.setName(null) - originalFunction.coroutineMetadata = null - originalFunction.isInlineableCoroutineBody = true - if (wrapperBody != null) { - // Extract local declarations - applyWrapper( - wrapperBody, - function, - originalFunction, - inlineFunctionDepth, - replacementsInducedByWrappers, - existingImports, - additionalImports, - existingNameBindings, - additionalNameBindings, - inlinedModuleAliases, - inverseNameBindings, - addPrevious - ) + // Original function should be not be transformed into a state machine + f.function.setName(null) + f.function.coroutineMetadata = null + f.function.isInlineableCoroutineBody = true + + // Keep the `defineInlineFunction` for the inliner to find + statementContext.addNext(expression.makeStmt()) + + // Return the function body to be used without inlining. + return function } - - // Keep the `defineInlineFunction` for the inliner to find - statementContext.addNext(expression.makeStmt()) - - // Return the function body to be used without inlining. - return function } } return null diff --git a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/InlineableResult.kt b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/InlineableResult.kt index 27d95f13b7d..825f75ecb09 100644 --- a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/InlineableResult.kt +++ b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/InlineableResult.kt @@ -19,7 +19,7 @@ package org.jetbrains.kotlin.js.inline import org.jetbrains.kotlin.js.backend.ast.JsStatement import org.jetbrains.kotlin.js.backend.ast.JsExpression -class InlineableResult( +data class InlineableResult( val inlineableBody: JsStatement, val resultExpression: JsExpression? ) diff --git a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/InlineDfsController.kt b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/InlinerCycleReporter.kt similarity index 73% rename from js/js.inliner/src/org/jetbrains/kotlin/js/inline/InlineDfsController.kt rename to js/js.inliner/src/org/jetbrains/kotlin/js/inline/InlinerCycleReporter.kt index 8477ea15f47..8a8d7d5b33b 100644 --- a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/InlineDfsController.kt +++ b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/InlinerCycleReporter.kt @@ -7,10 +7,8 @@ package org.jetbrains.kotlin.js.inline import org.jetbrains.kotlin.diagnostics.DiagnosticSink import org.jetbrains.kotlin.diagnostics.Errors -import org.jetbrains.kotlin.js.backend.ast.JsBlock import org.jetbrains.kotlin.js.backend.ast.JsFunction import org.jetbrains.kotlin.js.backend.ast.JsInvocation -import org.jetbrains.kotlin.js.backend.ast.JsName import org.jetbrains.kotlin.js.backend.ast.metadata.descriptor import org.jetbrains.kotlin.js.backend.ast.metadata.inlineStrategy import org.jetbrains.kotlin.js.backend.ast.metadata.psiElement @@ -23,27 +21,18 @@ import org.jetbrains.kotlin.js.inline.util.refreshLabelNames import org.jetbrains.kotlin.resolve.inline.InlineStrategy import java.util.* -class InlineDfsController( +/** + * There are two ways the inliner may arrive to an inline function declaration. Either the topmost visitor arrive to a declaration, which + * has never been used before. Or it visits it's invocation, and has to obtain the inline function body. Current strategy is to processes + * the inline function declaration before inlining it's invocation. + * + * Thus the inliner effectively implements a DFS. InlinerCycleReporter manages the DFS state. Also it detects and reports cycles. + */ +class InlinerCycleReporter( val trace: DiagnosticSink, - val functions: Map, - val accessors: Map, - val functionContext: FunctionContext + private val functionContext: FunctionContext ) { - val functionsByWrapperNodes = - HashMap() - val functionsByFunctionNodes = - HashMap() - - init { - (functions.values.asSequence() + accessors.values.asSequence()).forEach { f -> - functionsByFunctionNodes[f.function] = f - if (f.wrapperBody != null) { - functionsByWrapperNodes[f.wrapperBody] = f - } - } - } - private val processedFunctions = IdentitySet() private val inProcessFunctions = IdentitySet() // these are needed for error reporting, when inliner detects cycle @@ -51,6 +40,7 @@ class InlineDfsController( private val inlineCallInfos = LinkedList() + // TODO This looks like a hack val currentNamedFunction: JsFunction? get() = if (namedFunctionsStack.empty()) null else namedFunctionsStack.peek() @@ -59,19 +49,14 @@ class InlineDfsController( assert(!inProcessFunctions.contains(function)) { "Inliner has revisited function" } inProcessFunctions.add(function) - if (function in functionsByFunctionNodes.keys) { + if (function in functionContext.functionsByFunctionNodes.keys) { namedFunctionsStack.push(function) } } fun endFunction(function: JsFunction) { - refreshLabelNames(function.body, function.scope) - - removeUnusedLocalFunctionDeclarations(function) processedFunctions.add(function) - FunctionPostProcessor(function).apply() - assert(inProcessFunctions.contains(function)) inProcessFunctions.remove(function) @@ -82,17 +67,15 @@ class InlineDfsController( // Return true iff the definition should be visited by the inliner - fun visitCall(call: JsInvocation): Boolean { - val definition = functionContext.getFunctionDefinition(call) + fun shouldProcess(definition: FunctionWithWrapper, call: JsInvocation): Boolean { currentNamedFunction?.let { inlineCallInfos.add(JsCallInfo(call, it)) } - - if (inProcessFunctions.contains(definition.function)) { + if (definition.function in inProcessFunctions) { reportInlineCycle(call, definition.function) - } else if (!processedFunctions.contains(definition.function)) { + } else if (definition.function !in processedFunctions) { return true } diff --git a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/InlinerImpl.kt b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/InlinerImpl.kt index f91184f49bc..770e9852f1a 100644 --- a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/InlinerImpl.kt +++ b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/InlinerImpl.kt @@ -9,144 +9,84 @@ import org.jetbrains.kotlin.js.backend.ast.* import org.jetbrains.kotlin.js.backend.ast.metadata.forcedReturnVariable import org.jetbrains.kotlin.js.backend.ast.metadata.inlineStrategy import org.jetbrains.kotlin.js.backend.ast.metadata.synthetic +import org.jetbrains.kotlin.js.inline.clean.FunctionPostProcessor +import org.jetbrains.kotlin.js.inline.clean.removeUnusedLocalFunctionDeclarations import org.jetbrains.kotlin.js.inline.context.FunctionContext import org.jetbrains.kotlin.js.inline.context.InliningContext -import org.jetbrains.kotlin.js.inline.util.FunctionWithWrapper -import org.jetbrains.kotlin.js.inline.util.getImportTag +import org.jetbrains.kotlin.js.inline.util.refreshLabelNames import org.jetbrains.kotlin.js.translate.declaration.transformSpecialFunctionsToCoroutineMetadata import org.jetbrains.kotlin.js.translate.utils.JsAstUtils -import java.util.ArrayList -import java.util.HashMap -import java.util.HashSet +// TODO stateless? class InlinerImpl( - val existingNameBindings: MutableMap, - val existingImports: MutableMap, - val inlineFunctionDepth: Int, - val dfsController: InlineDfsController, - val inverseNameBindings: Map, + val cycleReporter: InlinerCycleReporter, val functionContext: FunctionContext, - val addPrevious: (JsStatement) -> Unit + // TODO other way around? Need to find a correct inliner by function declaration. + val scope: InliningScope ) : JsVisitorWithContextImpl() { - val inlinedModuleAliases = HashSet() - - val replacementsInducedByWrappers: MutableMap> = mutableMapOf() - - val additionalNameBindings = ArrayList() - - val additionalImports = mutableListOf>() override fun visit(function: JsFunction, context: JsContext<*>): Boolean { - val functionWithWrapper = dfsController.functionsByFunctionNodes[function] - if (functionWithWrapper != null) { - visit(functionWithWrapper) + functionContext.functionsByFunctionNodes[function]?.let { (function, wrapper) -> + visit(function, wrapper) return false - } else { - dfsController.startFunction(function) - return super.visit(function, context) - } - } - - override fun endVisit(function: JsFunction, context: JsContext<*>) { - super.endVisit(function, context) - if (!dfsController.functionsByFunctionNodes.containsKey(function)) { - dfsController.endFunction(function) } + visit(function, null) + return true } override fun visit(x: JsBlock, ctx: JsContext<*>): Boolean { - val functionWithWrapper = dfsController.functionsByWrapperNodes[x] - if (functionWithWrapper != null) { - visit(functionWithWrapper) + // TODO Seems like a very roundabout way. Probably should reuse same approach as in CoroutineTransformer and ...Splitter + // TODO That approach might be missing inline properties. Check! + functionContext.functionsByWrapperNodes[x]?.let { (function, wrapper) -> + visit(function, wrapper) return false } return super.visit(x, ctx) } - private fun visit(functionWithWrapper: FunctionWithWrapper) { - dfsController.startFunction(functionWithWrapper.function) + private fun visit(function: JsFunction, wrapperBody: JsBlock?) { + cycleReporter.startFunction(function) - val wrapperBody = functionWithWrapper.wrapperBody - if (wrapperBody != null) { - val existingImports = HashMap() - - for (statement in wrapperBody.statements) { - if (statement is JsVars) { - val tag = getImportTag(statement) - if (tag != null) { - existingImports[tag] = statement.vars[0].name - } - } - } - - val additionalStatements = mutableListOf() - val innerInliner = InlinerImpl( - existingNameBindings, - existingImports, - inlineFunctionDepth + 1, - dfsController, - inverseNameBindings, - functionContext - ) { - additionalStatements.add(it) - } - for (statement in wrapperBody.statements) { - if (statement !is JsReturn) { - innerInliner.acceptStatement(statement) - } else { - innerInliner.accept((statement.expression as JsFunction).body) - } - } - - val importStatements = innerInliner.additionalImports.map { (_, importExpr, name) -> - JsAstUtils.newVar(name, importExpr) - } - - // TODO keep order - wrapperBody.statements.addAll(0, importStatements + additionalStatements) + // TODO Different visitors? + if (wrapperBody != null && scope is ProgramFragmentInliningScope) { + PublicInlineFunctionInliningScope(scope.fragment, cycleReporter, functionContext, function, wrapperBody).process() } else { - accept(functionWithWrapper.function.body) + // TODO this is still not super-clear + accept(function.body) } - dfsController.endFunction(functionWithWrapper.function) + // Cleanup + refreshLabelNames(function.body, function.scope) + removeUnusedLocalFunctionDeclarations(function) + FunctionPostProcessor(function).apply() + + cycleReporter.endFunction(function) } - override fun visit(call: JsInvocation, context: JsContext<*>): Boolean { - if (!hasToBeInlined(call)) return true + override fun endVisit(call: JsInvocation, ctx: JsContext) { + if (hasToBeInlined(call)) { - if (dfsController.visitCall(call)) { - val definition = functionContext.getFunctionDefinition(call) + val definition = functionContext.getFunctionDefinition(call, scope) - for (i in 0 until call.arguments.size) { - val argument = call.arguments[i] - call.arguments[i] = accept(argument) + if (cycleReporter.shouldProcess(definition.functionWithWrapper, call)) { + definition.process() } - visit(definition) - return false + inline(call, definition, ctx) } - return true - } - - override fun endVisit(x: JsInvocation, ctx: JsContext) { - if (hasToBeInlined(x)) { - inline(x, ctx) - } - - dfsController.endVisit(x) + cycleReporter.endVisit(call) } + // TODO This could be extracted into a separate pass. + // TODO Don't forget to run it on the freshly inlined code! override fun doAcceptStatementList(statements: MutableList) { var i = 0 while (i < statements.size) { - val additionalStatements = - ExpressionDecomposer.preserveEvaluationOrder(statements[i]) { node -> - node is JsInvocation && hasToBeInlined( - node - ) - } + val additionalStatements = ExpressionDecomposer.preserveEvaluationOrder(statements[i]) { node -> + node is JsInvocation && hasToBeInlined(node) + } statements.addAll(i, additionalStatements) i += additionalStatements.size + 1 } @@ -154,61 +94,28 @@ class InlinerImpl( super.doAcceptStatementList(statements) } - private fun inline(call: JsInvocation, context: JsContext) { - var functionWithWrapper = functionContext.getFunctionDefinition(call) + // TODO a lot of code... Probably a bad sign + private fun inline(call: JsInvocation, definition: InlineFunctionDefinition, context: JsContext) { - // Since we could get functionWithWrapper as a simple function directly from staticRef (which always points on implementation) - // we should check if we have a known wrapper for it - dfsController.functionsByFunctionNodes[functionWithWrapper.function]?.let { - functionWithWrapper = it - } + // --------------- + // This should be isolated - val function = functionWithWrapper.function.deepCopy() + val function = scope.importFunctionDefinition(definition) + // TODO This should be done inside the importer function.body = transformSpecialFunctionsToCoroutineMetadata(function.body) - if (functionWithWrapper.wrapperBody != null) { - applyWrapper( - functionWithWrapper.wrapperBody!!, - function, - functionWithWrapper.function, - inlineFunctionDepth, - replacementsInducedByWrappers, - existingImports, - additionalImports, - existingNameBindings, - additionalNameBindings, - inlinedModuleAliases, - inverseNameBindings - ) { - addPrevious(accept(it)) - } - } - val inliningContext = InliningContext(lastStatementLevelContext) + // ------------------- - val inlineableResult = - FunctionInlineMutator.getInlineableCallReplacement(call, function, inliningContext) + val statementContext = lastStatementLevelContext + + val (inlineableBody, resultExpression) = + FunctionInlineMutator.getInlineableCallReplacement(call, function, InliningContext(statementContext)) - val inlineableBody = inlineableResult.inlineableBody - var resultExpression = inlineableResult.resultExpression - val statementContext = inliningContext.statementContext // body of inline function can contain call to lambdas that need to be inlined val inlineableBodyWithLambdasInlined = accept(inlineableBody) assert(inlineableBody === inlineableBodyWithLambdasInlined) - // Support non-local return from secondary constructor - // Returns from secondary constructors should return `$this` object. - // TODO This seems brittle - val currentFunction = dfsController.currentNamedFunction - if (currentFunction != null) { - val returnVariable = currentFunction.forcedReturnVariable - if (returnVariable != null) { - inlineableBody.accept(object : RecursiveJsVisitor() { - override fun visitReturn(x: JsReturn) { - x.expression = returnVariable.makeRef() - } - }) - } - } + patchReturnsFromSecondaryConstructor(inlineableBody) statementContext.addPrevious(JsAstUtils.flattenStatement(inlineableBody)) @@ -221,13 +128,28 @@ class InlinerImpl( return } - resultExpression = accept(resultExpression) - resultExpression.synthetic = true - context.replaceMe(resultExpression) + // TODO Why accept? Seems unnecessary... Some inline call in the qualifier? Shouldn't this be done along with the lambdas then? + accept(resultExpression)?.let { + it.synthetic = true + context.replaceMe(it) + } + } + + private fun patchReturnsFromSecondaryConstructor(inlineableBody: JsStatement) { + // Support non-local return from secondary constructor + // Returns from secondary constructors should return `$this` object. + // TODO This seems brittle + cycleReporter.currentNamedFunction?.forcedReturnVariable?.let { returnVariable -> + inlineableBody.accept(object : RecursiveJsVisitor() { + override fun visitReturn(x: JsReturn) { + x.expression = returnVariable.makeRef() + } + }) + } } private fun hasToBeInlined(call: JsInvocation): Boolean { val strategy = call.inlineStrategy - return if (strategy == null || !strategy.isInline) false else functionContext.hasFunctionDefinition(call) + return if (strategy == null || !strategy.isInline) false else functionContext.hasFunctionDefinition(call, scope) } } \ No newline at end of file diff --git a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/InliningScope.kt b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/InliningScope.kt new file mode 100644 index 00000000000..75ffffe47b5 --- /dev/null +++ b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/InliningScope.kt @@ -0,0 +1,336 @@ +/* + * Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.js.inline + +import org.jetbrains.kotlin.js.backend.ast.* +import org.jetbrains.kotlin.js.backend.ast.metadata.imported +import org.jetbrains.kotlin.js.backend.ast.metadata.localAlias +import org.jetbrains.kotlin.js.backend.ast.metadata.staticRef +import org.jetbrains.kotlin.js.inline.clean.removeUnusedFunctionDefinitions +import org.jetbrains.kotlin.js.inline.clean.removeUnusedImports +import org.jetbrains.kotlin.js.inline.clean.simplifyWrappedFunctions +import org.jetbrains.kotlin.js.inline.context.FunctionContext +import org.jetbrains.kotlin.js.inline.util.* +import org.jetbrains.kotlin.js.translate.utils.JsAstUtils +import java.util.* + + +// Handles interpreting an inline function in terms of the current context. +// Either an program fragment, or a public inline function +sealed class InliningScope { + + private val cache = mutableMapOf>() + + protected fun computeIfAbsent(tag: String?, fn: () -> Map): Map { + if (tag == null) return fn() + + return cache.computeIfAbsent(tag) { fn() } + } + + abstract fun importFunctionDefinition(f: InlineFunctionDefinition): JsFunction + + abstract fun process() + + abstract val fragment: JsProgramFragment +} + +class ProgramFragmentInliningScope( + override val fragment: JsProgramFragment, + val functionContext: FunctionContext, + val rootInliner: JsInliner +) : InliningScope() { + + private val existingModules = fragment.importedModules.mapTo(IdentitySet()) { it.internalName } + + private val existingImports = fragment.nameBindings.associateTo(mutableMapOf()) { it.key to it.name } + + private val existingNameBindings = fragment.nameBindings.associateTo(IdentityHashMap()) { it.name to it.key } + + private val additionalDeclarations = mutableListOf() + + private var processed = false + + override fun process() { + if (!processed) { + // TODO is this even needed? + processed = true + + val inliner = InlinerImpl(rootInliner.cycleReporter, functionContext, this) + + // TODO any way and/or need to visit everything inside the fragment? + inliner.acceptStatement(fragment.declarationBlock) + + // TODO Atm it's placed after inliner in order not to perform the body inlining twice. Is that OK? + // Ideally it could be moved to the coroutine transformers. The info regarding which inline function wrappers have been imported + // on top level should be persisted for that sake. Also it going to be needed in order to avoid duplicate code. + InlineSuspendFunctionSplitter(this).accept(fragment.declarationBlock) + + // Mostly for the sake of post-processor + // TODO are inline function marked with @Test possible? + if (fragment.tests != null) { + inliner.acceptStatement(fragment.tests) + } + // TODO wrap in a function in order to do the post-processing + inliner.acceptStatement(fragment.initializerBlock) + + updateProgramFragment() + } + } + + private fun updateProgramFragment() { + // TODO fix the order + // TODO this probably will be replaced with a special tag -> block map for the imported stuff, so that we can merge same imports. + // TODO in that case this method will become obsolete + fragment.declarationBlock.statements.addAll(0, additionalDeclarations) + + // post-processing + val block = JsBlock(fragment.declarationBlock, fragment.initializerBlock, fragment.exportBlock) + fragment.tests?.let { block.statements.add(it) } + fragment.mainFunction?.let { block.statements.add(it) } + + simplifyWrappedFunctions(block) + removeUnusedFunctionDefinitions(block, collectNamedFunctions(block)) + + // TODO simplify + val usedImports = removeUnusedImports(block) + fragment.nameBindings.filter { + !it.name.imported || it.name in usedImports + }.let { + fragment.nameBindings.clear() + fragment.nameBindings.addAll(it) + } + val existingTags = fragment.nameBindings.map { it.key }.toSet() + val newImports = fragment.imports.filter { (k, _) -> k in existingTags } + fragment.imports.clear() + for ((k, v) in newImports) { + fragment.imports[k] = v + } + } + + private fun addInlinedModule(moduleName: JsName) { + if (moduleName !in existingModules) { + fragment.importedModules.add(moduleMap[moduleName]!!.let { + // Copy so that the Merger.kt doesn't operate on the same instance in different fragments. + JsImportedModule(it.externalName, it.internalName, it.plainReference) + }) + } + } + + private fun addImport(tag: String, e: JsExpression) { + fragment.imports[tag] = e + } + + private fun addNameBinding(binding: JsNameBinding) { + fragment.nameBindings.add(binding) + existingNameBindings[binding.name] = binding.key + } + + override fun importFunctionDefinition(f: InlineFunctionDefinition): JsFunction { + + // Apparently we should avoid this trick when we implement fair support for crossinline + // That's because crossinline lambdas inline into the declaration block and specialize those. + val replacements = computeIfAbsent(f.tag) { + val newReplacements = HashMap() + + val copiedStatements = ArrayList() + val importStatements = ArrayList>() + + f.functionWithWrapper.wrapperBody?.let { + it.statements.asSequence() + .filterNot { it is JsReturn } + .map { it.deepCopy() } + .forEach { statement -> + replaceExpressionsWithLocalAliases(statement) + + if (statement is JsVars) { + val tag = getImportTag(statement) + if (tag != null) { + // TODO handle JsVars with multiple vars? + val name = statement.vars[0].name + var existingName: JsName? = name.localAlias + if (existingName == null) { + existingName = existingImports.computeIfAbsent(tag) { + importStatements.add(tag to statement.vars[0]) + val alias = JsScope.declareTemporaryName(name.ident) + alias.copyMetadataFrom(name) + newReplacements[name] = JsAstUtils.pureFqn(alias, null) + alias + } + } + + if (name !== existingName) { + val replacement = JsAstUtils.pureFqn(existingName, null) + newReplacements[name] = replacement + } + + return@forEach + } + } + + copiedStatements.add(statement) + } + } + + (importStatements.asSequence().map { JsVars(it.second) } + copiedStatements.asSequence()) + .flatMap { node -> collectDefinedNamesInAllScopes(node).asSequence() } + .filter { name -> !newReplacements.containsKey(name) } + .forEach { name -> + val alias = JsScope.declareTemporaryName(name.ident) + alias.copyMetadataFrom(name) + val replacement = JsAstUtils.pureFqn(alias, null) + newReplacements[name] = replacement + } + + + for ((tag, statement) in importStatements) { + val renamed = replaceNames(statement, newReplacements) + // TODO shouldn't this be done at `existingImports.computeIfAbsent` moment? + addImport(tag, renamed.initExpression) + + addNameBinding(JsNameBinding(tag, renamed.name)) + } + + if (f.tag != null) { + fragment.inlinedFunctionWrappers[f.tag!!] = JsGlobalBlock().also { + copiedStatements.mapTo(it.statements) { replaceNames(it, newReplacements) } + } + } else { + // TODO Handle it better? + for (statement in copiedStatements) { + additionalDeclarations.add(replaceNames(statement, newReplacements)) + } + } + + // TODO shouldn't this be moved to renamer? + for ((key, value) in collectNamedFunctions(JsBlock(copiedStatements))) { + if (key.staticRef is JsFunction) { + key.staticRef = value + } + } + + newReplacements + } + + val paramMap = f.functionWithWrapper.function.parameters.associate { + val alias = JsScope.declareTemporaryName(it.name.ident) + alias.copyMetadataFrom(it.name) + it.name to JsAstUtils.pureFqn(alias, null) + } + + val result = f.functionWithWrapper.function.deepCopy() + + replaceNames(result, replacements) + replaceNames(result, paramMap) + + return result + } + + private fun replaceExpressionsWithLocalAliases(statement: JsStatement) { + object : JsVisitorWithContextImpl() { + override fun endVisit(x: JsNameRef, ctx: JsContext) { + replaceIfNecessary(x, ctx) + } + + override fun endVisit(x: JsArrayAccess, ctx: JsContext) { + replaceIfNecessary(x, ctx) + } + + private fun replaceIfNecessary(expression: JsExpression, ctx: JsContext) { + val alias = expression.localAlias + if (alias != null) { + ctx.replaceMe(alias.makeRef()) + addInlinedModule(alias) + } + } + + }.accept(statement) + } +} + +class PublicInlineFunctionInliningScope( + override val fragment: JsProgramFragment, + cycleReporter: InlinerCycleReporter, + functionContext: FunctionContext, + val function: JsFunction, + val wrapperBody: JsBlock +) : InliningScope() { + + val additionalStatements = mutableListOf() + + val innerInliner = InlinerImpl( + cycleReporter, + functionContext, + this + ) + + override fun process() { + for (statement in wrapperBody.statements) { + if (statement !is JsReturn) { + innerInliner.acceptStatement(statement) + } else { + innerInliner.accept((statement.expression as JsFunction).body) + } + } + + // TODO keep order + wrapperBody.statements.addAll(0, additionalStatements) + } + + private fun addPrevious(statement: JsStatement) { + // TODO Is this correct? + additionalStatements.add(innerInliner.accept(statement)) + } + + override fun importFunctionDefinition(f: InlineFunctionDefinition): JsFunction { + // TODO Decrypt the comment below + // Apparently we should avoid this trick when we implement fair support for crossinline + val replacements = computeIfAbsent(f.tag) { + val newReplacements = HashMap() + + // TODO Why don't we collect existing imports? + + val copiedStatements = f.functionWithWrapper.wrapperBody!!.statements.asSequence() + .filterNot { it is JsReturn } + .map { it.deepCopy() }.toList() + + val definedNames = copiedStatements.asSequence() + .flatMap { node -> collectDefinedNamesInAllScopes(node).asSequence() } + .filter { name -> !newReplacements.containsKey(name) } + .toSet() + for (name in definedNames) { + val alias = JsScope.declareTemporaryName(name.ident) + alias.copyMetadataFrom(name) + val replacement = JsAstUtils.pureFqn(alias, null) + newReplacements[name] = replacement + } + + for (statement in copiedStatements) { + addPrevious(replaceNames(statement, newReplacements)) + } + + for ((key, value) in collectNamedFunctions(JsBlock(copiedStatements))) { + if (key.staticRef is JsFunction) { + key.staticRef = value + } + } + + newReplacements + } + + val paramMap = f.functionWithWrapper.function.parameters.associate { + val alias = JsScope.declareTemporaryName(it.name.ident) + alias.copyMetadataFrom(it.name) + it.name to JsAstUtils.pureFqn(alias, null) + } + + val result = f.functionWithWrapper.function.deepCopy() + + replaceNames(result, replacements) + replaceNames(result, paramMap) + + return result + } +} \ No newline at end of file diff --git a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/JsInliner.kt b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/JsInliner.kt index a2f4aa7ac03..b4edf18c70c 100644 --- a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/JsInliner.kt +++ b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/JsInliner.kt @@ -8,160 +8,34 @@ package org.jetbrains.kotlin.js.inline import org.jetbrains.kotlin.diagnostics.DiagnosticSink import org.jetbrains.kotlin.js.backend.ast.* import org.jetbrains.kotlin.js.config.JsConfig -import org.jetbrains.kotlin.js.inline.clean.* import org.jetbrains.kotlin.js.inline.context.FunctionContext -import org.jetbrains.kotlin.js.inline.util.* -import java.util.* - -import org.jetbrains.kotlin.js.inline.util.IdentitySet import org.jetbrains.kotlin.js.translate.general.AstGenerationResult -class JsInliner private constructor( - config: JsConfig, - functionReader: FunctionReader, - val functions: Map, - val accessors: Map, - private val inverseNameBindings: Map, - private val moduleMap: Map, - private val trace: DiagnosticSink +class JsInliner( + val reporter: JsConfig.Reporter, + val config: JsConfig, + val trace: DiagnosticSink, + val translationResult: AstGenerationResult ) { - private val functionContext = FunctionContext(functionReader, config, functions, accessors) - - private fun addInlinedModules(fragment: JsProgramFragment, inlinedModuleAliases: Set) { - val existingModules = fragment.importedModules.mapTo(IdentitySet()) { it.internalName } - inlinedModuleAliases.forEach { - if (it !in existingModules) { - fragment.importedModules.add(moduleMap[it]!!.let { - // Copy so that the Merger.kt doesn't operate on the same instance in different fragments. - JsImportedModule(it.externalName, it.internalName, it.plainReference) - }) + init { + // TODO Isn't there a better way to achieve this? Also there is a bug with private inline properties + DummyAccessorInvocationTransformer().let { + for (fragment in translationResult.newFragments) { + it.accept(fragment.declarationBlock) + it.accept(fragment.initializerBlock) } } } - fun process(fragment: JsProgramFragment, existingImports: MutableMap) { - val existingNameBindings = fragment.nameBindings.associateTo(IdentityHashMap()) { it.name to it.key } + val functionContext = FunctionContext(this) - val dfsController = InlineDfsController(trace, functions, accessors, functionContext) + val cycleReporter = InlinerCycleReporter(trace, functionContext) - val additionalDeclarations = mutableListOf() - val inliner = InlinerImpl( - existingNameBindings, - existingImports, - 0, - dfsController, - inverseNameBindings, - functionContext - ) { - additionalDeclarations.add(it) - } - - val inlineSuspendFnSplitter = InlineSuspendFunctionSplitter( - existingNameBindings, - existingImports, - 0, - inverseNameBindings, - inliner.replacementsInducedByWrappers - ) { - additionalDeclarations.add(it) - } - - inliner.acceptStatement(fragment.declarationBlock) - - inlineSuspendFnSplitter.accept(fragment.declarationBlock) - - // Mostly for the sake of post-processor - // TODO are inline function marked with @Test possible? - if (fragment.tests != null) { - inliner.acceptStatement(fragment.tests) - } - inliner.acceptStatement(fragment.initializerBlock) - - // TODO fix the order - fragment.declarationBlock.statements.addAll(0, additionalDeclarations) - fragment.nameBindings.addAll(inliner.additionalNameBindings) - // TODO actually bogus - fragment.nameBindings.addAll(inlineSuspendFnSplitter.additionalNameBindings) - - (inliner.additionalImports.asSequence() + inlineSuspendFnSplitter.additionalImports.asSequence()).forEach { (tag, e, _) -> - fragment.imports[tag] = e - } - - addInlinedModules(fragment, inliner.inlinedModuleAliases) - addInlinedModules(fragment, inlineSuspendFnSplitter.inlinedModuleAliases) - } - - companion object { - - // TODO decrypt; Possibly error-prone due to late renaming - // TODO Possibly apply post-processing outside of FunctionReader? - // Since we compile each source file in its own context (and we may loose these context when performing incremental compilation) - // we don't use contexts to generate proper names for modules. Instead, we generate all necessary information during - // translation and rely on it here. - private fun buildModuleNameMap(fragments: List): Map { - return fragments.flatMap { it.inlineModuleMap.entries }.associate { (k, v) -> k to v } - } - - - fun process( - reporter: JsConfig.Reporter, - config: JsConfig, - trace: DiagnosticSink, - translationResult: AstGenerationResult - ) { - val accessorInvocationTransformer = DummyAccessorInvocationTransformer() - for (fragment in translationResult.newFragments) { - accessorInvocationTransformer.accept(fragment.declarationBlock) - accessorInvocationTransformer.accept(fragment.initializerBlock) - } - - val referencedInlineFunctionTags = collectInlineFunctionTags(config, translationResult.newFragments) - val fragments = referencedInlineFunctionTags.mapNotNull { - translationResult.inlineFunctionTagMap[it]?.let { unit -> - translationResult.translate(unit).fragment - } - } - - val functions = collectNamedFunctionsAndWrappers(translationResult.newFragments) - - val accessors = collectAccessors(fragments) - - // TODO update on fragment loading - val inverseNameBindings = inverseNameBindings(*fragments.toTypedArray()) - - val functionReader = FunctionReader(reporter, config, translationResult.innerModuleName, buildModuleNameMap(fragments)) - - val moduleMap = fragments.asSequence().flatMap { it.importedModules.asSequence() }.associate { it.internalName to it } - - val inliner = JsInliner(config, functionReader, functions, accessors, inverseNameBindings, moduleMap, trace) - for (fragment in translationResult.newFragments) { - inliner.process(fragment, inverseNameBindings(fragment).entries.associateTo(mutableMapOf()) { (name, tag) -> tag to name }) - } - - for (fragment in translationResult.newFragments) { - val block = JsBlock(fragment.declarationBlock, fragment.initializerBlock, fragment.exportBlock) - val usedImports = removeUnusedImports(block) - for ((key, name) in fragment.nameBindings) { - if (name !in usedImports && key in fragment.imports) { - fragment.imports.remove(key) - } - } - simplifyWrappedFunctions(block) - removeUnusedFunctionDefinitions(block, collectNamedFunctions(block)) - } - } - - private fun inverseNameBindings(vararg fragments: JsProgramFragment): MutableMap { - val name2Tag = mutableMapOf() - fragments.forEach { - it.nameBindings.forEach { (tag, name) -> - name2Tag[name] = tag - } - } - - return name2Tag + fun process() { + for (fragment in translationResult.newFragments) { + functionContext.scopeForFragment(fragment).process() } } -} +} \ No newline at end of file diff --git a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/context/FunctionContext.kt b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/context/FunctionContext.kt index af2ee3e78d3..3fba92b86d2 100644 --- a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/context/FunctionContext.kt +++ b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/context/FunctionContext.kt @@ -21,29 +21,39 @@ import org.jetbrains.kotlin.js.backend.ast.* import org.jetbrains.kotlin.js.backend.ast.metadata.isCallableReference import org.jetbrains.kotlin.js.backend.ast.metadata.descriptor import org.jetbrains.kotlin.js.config.JsConfig -import org.jetbrains.kotlin.js.inline.FunctionReader +import org.jetbrains.kotlin.js.inline.* import org.jetbrains.kotlin.js.inline.util.* import org.jetbrains.kotlin.js.translate.context.Namer -import org.jetbrains.kotlin.js.translate.utils.JsDescriptorUtils.getModuleName +import org.jetbrains.kotlin.js.translate.general.AstGenerationResult +import java.util.HashMap class FunctionContext( - private val functionReader: FunctionReader, - private val config: JsConfig, - private val functions: Map, - private val accessors: Map + val inliner: JsInliner ) { - fun lookUpStaticFunction(functionName: JsName?): FunctionWithWrapper? = functions[functionName] + private val functionReader = FunctionReader(inliner.reporter, inliner.config, inliner.translationResult.innerModuleName) - fun lookUpStaticFunctionByTag(functionTag: String): FunctionWithWrapper? = accessors[functionTag] + private data class FunctionsAndAccessors(val functions: Map, val accessors: Map) - fun getFunctionDefinition(call: JsInvocation): FunctionWithWrapper { - return getFunctionDefinitionImpl(call)!! + private val fragmentInfo = mutableMapOf() + + private fun lookUpStaticFunction(functionName: JsName?, fragment: JsProgramFragment): FunctionWithWrapper? = + fragmentInfo[fragment]?.run { functions[functionName] } + + private fun lookUpStaticFunctionByTag(functionTag: String, fragment: JsProgramFragment): FunctionWithWrapper? = + fragmentInfo[fragment]?.run { accessors[functionTag] } + + fun getFunctionDefinition(call: JsInvocation, scope: InliningScope): InlineFunctionDefinition { + return getFunctionDefinitionImpl(call, scope)!! } - fun hasFunctionDefinition(call: JsInvocation): Boolean { - return getFunctionDefinitionImpl(call) != null + fun hasFunctionDefinition(call: JsInvocation, scope: InliningScope): Boolean { + return getFunctionDefinitionImpl(call, scope) != null } + val functionsByWrapperNodes = HashMap() + + val functionsByFunctionNodes = HashMap() + /** * Gets function definition by invocation. * @@ -74,17 +84,19 @@ class FunctionContext( * 5. Qualifier can be JsNameRef with ref to case [3] * in case of local function with closure. */ - private fun getFunctionDefinitionImpl(call: JsInvocation): FunctionWithWrapper? { + private fun getFunctionDefinitionImpl(call: JsInvocation, scope: InliningScope): InlineFunctionDefinition? { + // Ensure we have the local function information + loadFragment(scope.fragment) val descriptor = call.descriptor if (descriptor != null) { - return lookUpFunctionDirect(descriptor) ?: lookUpFunctionIndirect(call) ?: lookUpFunctionExternal(descriptor) + return lookUpFunctionDirect(descriptor) ?: lookUpFunctionIndirect(call, scope) ?: lookUpFunctionExternal(descriptor) } - return lookUpFunctionIndirect(call) + return lookUpFunctionIndirect(call, scope) } - private fun lookUpFunctionIndirect(call: JsInvocation): FunctionWithWrapper? { + private fun lookUpFunctionIndirect(call: JsInvocation, scope: InliningScope): LocalInlineFunctionDefinition? { /** remove ending `()` */ val callQualifier: JsExpression = if (isCallInvocation(call)) { (call.qualifier as JsNameRef).qualifier!! @@ -97,19 +109,70 @@ class FunctionContext( return when (qualifier) { is JsInvocation -> { tryExtractCallableReference(qualifier) ?: getSimpleName(qualifier)?.let { simpleName -> - lookUpStaticFunction(simpleName)?.let { if (isFunctionCreator(it.function)) it else null } + lookUpStaticFunction(simpleName, scope.fragment)?.let { if (isFunctionCreator(it.function)) it else null } } } - is JsNameRef -> lookUpStaticFunction(qualifier.name) - is JsFunction -> FunctionWithWrapper(qualifier, null) + is JsNameRef -> lookUpStaticFunction(qualifier.name, scope.fragment) + + // Since we could get functionWithWrapper as a simple function directly from staticRef (which always points on implementation) + // we should check if we have a known wrapper for it + is JsFunction -> functionsByFunctionNodes[qualifier] ?: FunctionWithWrapper(qualifier, null) else -> null + }?.let { + LocalInlineFunctionDefinition(it, scope) } } - private fun lookUpFunctionDirect(descriptor: CallableDescriptor): FunctionWithWrapper? = - lookUpStaticFunctionByTag(Namer.getFunctionTag(descriptor, config)) + fun functionTag(call: JsInvocation): String? { + return call.descriptor?.let { Namer.getFunctionTag(it, inliner.config) } + } - private fun lookUpFunctionExternal(descriptor: CallableDescriptor): FunctionWithWrapper? = functionReader[descriptor] + private val newFragmentSet = inliner.translationResult.newFragments.toIdentitySet() + + private val inliningScopeCache = mutableMapOf() + + fun scopeForFragment(fragment: JsProgramFragment) = inliningScopeCache.computeIfAbsent(fragment) { + ProgramFragmentInliningScope(fragment, this, inliner) + } + + private fun loadFragment(fragment: JsProgramFragment) { + fragmentInfo.computeIfAbsent(fragment) { + FunctionsAndAccessors( + collectNamedFunctionsAndWrappers(listOf(fragment)), + collectAccessors(listOf(fragment)) + ).also { (functions, accessors) -> + (functions.values.asSequence() + accessors.values.asSequence()).forEach { f -> + functionsByFunctionNodes[f.function] = f + if (f.wrapperBody != null) { + functionsByWrapperNodes[f.wrapperBody] = f + } + } + } + } + } + + private fun fragmentByTag(tag: String): JsProgramFragment? { + return inliner.translationResult.inlineFunctionTagMap[tag]?.let { unit -> + inliner.translationResult.translate(unit).fragment.also { loadFragment(it) } + } + } + + private fun lookUpFunctionDirect(descriptor: CallableDescriptor): InlineFunctionDefinition? = + Namer.getFunctionTag(descriptor, inliner.config).let { tag -> + fragmentByTag(tag)?.let { fragment -> + lookUpStaticFunctionByTag(tag, fragment)?.let { + if (fragment !in newFragmentSet) { + BinaryInlineFunctionDefinition(tag, it, fragment) + } else { + // TODO This is a wrong scope =( + PublicInlineFunctionDefinition(tag, it, fragment, scopeForFragment(fragment)) + } + } + } + } + + + private fun lookUpFunctionExternal(descriptor: CallableDescriptor): LibraryInlineFunctionDefinition? = functionReader[descriptor] private fun tryExtractCallableReference(invocation: JsInvocation): FunctionWithWrapper? { if (invocation.isCallableReference) { diff --git a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/context/NamingContext.kt b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/context/NamingContext.kt index 6b74625a7f4..0a2f89b9b0c 100644 --- a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/context/NamingContext.kt +++ b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/context/NamingContext.kt @@ -22,7 +22,7 @@ import org.jetbrains.kotlin.js.inline.util.replaceNames import org.jetbrains.kotlin.js.translate.utils.JsAstUtils class NamingContext(private val statementContext: JsContext) { - private val renamings = mutableMapOf() + private val renamings = mutableMapOf() private val declarations = mutableListOf() private var addedDeclarations = false @@ -35,7 +35,7 @@ class NamingContext(private val statementContext: JsContext) { return replaceNames(target, renamings) } - fun replaceName(name: JsName, replacement: JsExpression) { + fun replaceName(name: JsName, replacement: JsNameRef) { assert(!renamings.containsKey(name)) { "$name has been renamed already" } renamings.put(name, replacement) diff --git a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/util/collectUtils.kt b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/util/collectUtils.kt index ed4401bf9fc..fdced6b7099 100644 --- a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/util/collectUtils.kt +++ b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/util/collectUtils.kt @@ -225,7 +225,7 @@ fun collectAccessors(scope: JsNode): Map { return accessors } -fun collectAccessors(fragments: List): Map { +fun collectAccessors(fragments: Iterable): Map { val result = mutableMapOf() for (fragment in fragments) { result += collectAccessors(fragment.declarationBlock) diff --git a/js/js.translator/src/org/jetbrains/kotlin/js/facade/K2JSTranslator.kt b/js/js.translator/src/org/jetbrains/kotlin/js/facade/K2JSTranslator.kt index 800f650b471..e80b788f594 100644 --- a/js/js.translator/src/org/jetbrains/kotlin/js/facade/K2JSTranslator.kt +++ b/js/js.translator/src/org/jetbrains/kotlin/js/facade/K2JSTranslator.kt @@ -113,12 +113,12 @@ class K2JSTranslator(private val config: JsConfig) { if (hasError(diagnostics)) return TranslationResult.Fail(diagnostics) checkCanceled() - JsInliner.process( + JsInliner( reporter, config, analysisResult.bindingTrace, translationResult - ) + ).process() if (hasError(diagnostics)) return TranslationResult.Fail(diagnostics) checkCanceled() diff --git a/js/js.translator/src/org/jetbrains/kotlin/js/translate/general/AstGenerationResult.kt b/js/js.translator/src/org/jetbrains/kotlin/js/translate/general/AstGenerationResult.kt index c40a1b18fd3..893df7667ea 100644 --- a/js/js.translator/src/org/jetbrains/kotlin/js/translate/general/AstGenerationResult.kt +++ b/js/js.translator/src/org/jetbrains/kotlin/js/translate/general/AstGenerationResult.kt @@ -53,7 +53,7 @@ class AstGenerationResult( private val sourceRoots = config.sourceMapRoots.map { File(it) } private val deserializer = JsAstDeserializer(merger.program, sourceRoots) - fun translate(unit: TranslationUnit) = + fun translate(unit: TranslationUnit): FileTranslationResult = when (unit) { is TranslationUnit.SourceFile -> translatedSourceFiles[unit]!! is TranslationUnit.BinaryAst -> cache.getOrPut(unit) { diff --git a/js/js.translator/src/org/jetbrains/kotlin/js/translate/general/Translation.java b/js/js.translator/src/org/jetbrains/kotlin/js/translate/general/Translation.java index b129e860a3d..01d6230a3fb 100644 --- a/js/js.translator/src/org/jetbrains/kotlin/js/translate/general/Translation.java +++ b/js/js.translator/src/org/jetbrains/kotlin/js/translate/general/Translation.java @@ -324,7 +324,7 @@ public final class Translation { Map inlineFunctionTagMap = new HashMap<>(); - Map translatedSourceFiles = new HashMap<>(); + Map translatedSourceFiles = new LinkedHashMap<>(); for (TranslationUnit unit : units) { if (unit instanceof TranslationUnit.SourceFile) {