diff --git a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/JsInliner.java b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/JsInliner.java deleted file mode 100644 index 03d62b24053..00000000000 --- a/js/js.inliner/src/org/jetbrains/kotlin/js/inline/JsInliner.java +++ /dev/null @@ -1,757 +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 com.intellij.psi.PsiElement; -import kotlin.jvm.functions.Function1; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.kotlin.backend.common.CommonCoroutineCodegenUtilKt; -import org.jetbrains.kotlin.config.CommonConfigurationKeysKt; -import org.jetbrains.kotlin.config.LanguageVersionSettings; -import org.jetbrains.kotlin.descriptors.CallableDescriptor; -import org.jetbrains.kotlin.descriptors.DeclarationDescriptor; -import org.jetbrains.kotlin.descriptors.FunctionDescriptor; -import org.jetbrains.kotlin.diagnostics.DiagnosticSink; -import org.jetbrains.kotlin.diagnostics.Errors; -import org.jetbrains.kotlin.js.backend.ast.*; -import org.jetbrains.kotlin.js.backend.ast.metadata.MetadataProperties; -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.context.InliningContext; -import org.jetbrains.kotlin.js.inline.context.NamingContext; -import org.jetbrains.kotlin.js.inline.util.*; -import org.jetbrains.kotlin.js.translate.expression.InlineMetadata; -import org.jetbrains.kotlin.resolve.inline.InlineStrategy; - -import java.util.*; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static org.jetbrains.kotlin.js.inline.util.CollectUtilsKt.getImportTag; -import static org.jetbrains.kotlin.js.inline.util.CollectionUtilsKt.IdentitySet; -import static org.jetbrains.kotlin.js.translate.declaration.InlineCoroutineUtilKt.transformSpecialFunctionsToCoroutineMetadata; -import static org.jetbrains.kotlin.js.translate.utils.JsAstUtils.flattenStatement; -import static org.jetbrains.kotlin.js.translate.utils.JsAstUtils.pureFqn; - -public class JsInliner extends JsVisitorWithContextImpl { - - private final JsConfig config; - private final Map functions; - private final Set namedFunctionsSet; - private final Map accessors; - private final Stack inliningContexts = new Stack<>(); - private final Set processedFunctions = CollectionUtilsKt.IdentitySet(); - private final Set inProcessFunctions = CollectionUtilsKt.IdentitySet(); - private final FunctionReader functionReader; - private final DiagnosticSink trace; - private Map existingImports = new HashMap<>(); - private JsContext statementContextForInline; - private final Map functionsByWrapperNodes = new HashMap<>(); - private final Map functionsByFunctionNodes = new HashMap<>(); - - // these are needed for error reporting, when inliner detects cycle - private final Stack namedFunctionsStack = new Stack<>(); - private final LinkedList inlineCallInfos = new LinkedList<>(); - private final Function1 canBeExtractedByInliner = - node -> node instanceof JsInvocation && hasToBeInlined((JsInvocation) node); - private int inlineFunctionDepth; - - private final Map> replacementsInducedByWrappers = new HashMap<>(); - - private final Map inverseNameBindings; - - private Map existingNameBindings = new HashMap<>(); - - private final List additionalNameBindings = new ArrayList<>(); - - private final Set inlinedModuleAliases = new HashSet<>(); - - public static void process( - @NotNull JsConfig.Reporter reporter, - @NotNull JsConfig config, - @NotNull DiagnosticSink trace, - @NotNull JsName currentModuleName, - @NotNull List fragments, - @NotNull List fragmentsToProcess, - @NotNull List importStatements - ) { - Map functions = CollectUtilsKt.collectNamedFunctionsAndWrappers(fragments); - Map accessors = CollectUtilsKt.collectAccessors(fragments); - Map inverseNameBindings = CollectUtilsKt.collectNameBindings(fragments); - - DummyAccessorInvocationTransformer accessorInvocationTransformer = new DummyAccessorInvocationTransformer(); - for (JsProgramFragment fragment : fragmentsToProcess) { - accessorInvocationTransformer.accept(fragment.getDeclarationBlock()); - accessorInvocationTransformer.accept(fragment.getInitializerBlock()); - } - FunctionReader functionReader = new FunctionReader(reporter, config, currentModuleName, fragments); - JsInliner inliner = new JsInliner(config, functions, accessors, inverseNameBindings, functionReader, trace); - - for (JsStatement statement : importStatements) { - inliner.processImportStatement(statement); - } - - Map moduleMap = fillModuleMap(buildModuleMap(fragments), fragmentsToProcess); - - for (JsProgramFragment fragment : fragmentsToProcess) { - inliner.existingImports.clear(); - inliner.additionalNameBindings.clear(); - inliner.inlinedModuleAliases.clear(); - inliner.existingNameBindings = CollectUtilsKt.collectNameBindings(Collections.singletonList(fragment)); - - inliner.acceptStatement(fragment.getDeclarationBlock()); - // Mostly for the sake of post-processor - // TODO are inline function marked with @Test possible? - if (fragment.getTests() != null) { - inliner.acceptStatement(fragment.getTests()); - } - - // There can be inlined function in top-level initializers, we need to optimize them as well - JsFunction fakeInitFunction = new JsFunction(JsDynamicScope.INSTANCE, fragment.getInitializerBlock(), ""); - JsGlobalBlock initWrapper = new JsGlobalBlock(); - initWrapper.getStatements().add(new JsExpressionStatement(fakeInitFunction)); - inliner.accept(initWrapper); - initWrapper.getStatements().remove(initWrapper.getStatements().size() - 1); - - fragment.getInitializerBlock().getStatements().addAll(0, initWrapper.getStatements()); - fragment.getNameBindings().addAll(inliner.additionalNameBindings); - inliner.addInlinedModules(fragment, moduleMap); - } - - for (JsProgramFragment fragment : fragmentsToProcess) { - JsBlock block = new JsBlock(fragment.getDeclarationBlock(), fragment.getInitializerBlock(), fragment.getExportBlock()); - RemoveUnusedImportsKt.removeUnusedImports(block); - SimplifyWrappedFunctionsKt.simplifyWrappedFunctions(block); - RemoveUnusedFunctionDefinitionsKt.removeUnusedFunctionDefinitions(block, CollectUtilsKt.collectNamedFunctions(block)); - } - } - - private void addInlinedModules(JsProgramFragment fragment, Map moduleMap) { - Set localMap = buildModuleMap(Collections.singletonList(fragment)).keySet(); - for (JsName inlinedModuleName : inlinedModuleAliases) { - if (!localMap.contains(inlinedModuleName)) { - fragment.getImportedModules().add(moduleMap.get(inlinedModuleName)); - } - } - } - - private static Map buildModuleMap(List fragments) { - return fillModuleMap(new HashMap<>(), fragments); - } - - private static Map fillModuleMap(Map map, List fragments) { - for (JsProgramFragment fragment : fragments) { - for (JsImportedModule module : fragment.getImportedModules()) { - map.put(module.getInternalName(), module); - } - } - return map; - } - - - private JsInliner( - @NotNull JsConfig config, - @NotNull Map functions, - @NotNull Map accessors, - @NotNull Map inverseNameBindings, - @NotNull FunctionReader functionReader, - @NotNull DiagnosticSink trace - ) { - this.config = config; - this.functions = functions; - this.namedFunctionsSet = IdentitySet(); - for (FunctionWithWrapper functionWithWrapper : functions.values()) { - namedFunctionsSet.add(functionWithWrapper.getFunction()); - } - this.accessors = accessors; - this.inverseNameBindings = inverseNameBindings; - this.functionReader = functionReader; - this.trace = trace; - - Stream.concat(functions.values().stream(), accessors.values().stream()) - .forEach(f -> { - functionsByFunctionNodes.put(f.getFunction(), f); - if (f.getWrapperBody() != null) { - functionsByWrapperNodes.put(f.getWrapperBody(), f); - } - }); - } - - private void processImportStatement(JsStatement statement) { - if (statement instanceof JsVars) { - JsVars jsVars = (JsVars) statement; - String tag = getImportTag(jsVars); - if (tag != null) { - existingImports.put(tag, jsVars.getVars().get(0).getName()); - } - } - } - - @Override - public boolean visit(@NotNull JsFunction function, @NotNull JsContext context) { - FunctionWithWrapper functionWithWrapper = functionsByFunctionNodes.get(function); - if (functionWithWrapper != null) { - visit(functionWithWrapper); - return false; - } - else { - if (statementContextForInline == null) { - statementContextForInline = getLastStatementLevelContext(); - startFunction(function); - boolean result = super.visit(function, context); - statementContextForInline = null; - return result; - } else { - startFunction(function); - return super.visit(function, context); - } - } - } - - @Override - public void endVisit(@NotNull JsFunction function, @NotNull JsContext context) { - super.endVisit(function, context); - if (!functionsByFunctionNodes.containsKey(function)) { - endFunction(function); - } - } - - private void startFunction(@NotNull JsFunction function) { - inliningContexts.push(new JsInliningContext(statementContextForInline)); - - assert !inProcessFunctions.contains(function): "Inliner has revisited function"; - inProcessFunctions.add(function); - - if (namedFunctionsSet.contains(function)) { - namedFunctionsStack.push(function); - } - } - - private void endFunction(@NotNull JsFunction function) { - NamingUtilsKt.refreshLabelNames(function.getBody(), function.getScope()); - - RemoveUnusedLocalFunctionDeclarationsKt.removeUnusedLocalFunctionDeclarations(function); - processedFunctions.add(function); - - new FunctionPostProcessor(function).apply(); - - assert inProcessFunctions.contains(function); - inProcessFunctions.remove(function); - - inliningContexts.pop(); - - if (!namedFunctionsStack.empty() && namedFunctionsStack.peek() == function) { - namedFunctionsStack.pop(); - } - } - - @Override - public boolean visit(@NotNull JsBlock x, @NotNull JsContext ctx) { - FunctionWithWrapper functionWithWrapper = functionsByWrapperNodes.get(x); - if (functionWithWrapper != null) { - visit(functionWithWrapper); - return false; - } - return super.visit(x, ctx); - } - - private void visit(@NotNull FunctionWithWrapper functionWithWrapper) { - JsContext oldContextForInline = statementContextForInline; - Map oldExistingImports = existingImports; - int oldInlineFunctionDepth = inlineFunctionDepth; - - ListContext innerContext = new ListContext<>(); - - JsBlock wrapperBody = functionWithWrapper.getWrapperBody(); - List statements = null; - if (wrapperBody != null) { - existingImports = new HashMap<>(); - statementContexts.push(innerContext); - statementContextForInline = innerContext; - inlineFunctionDepth++; - - for (JsStatement statement : wrapperBody.getStatements()) { - processImportStatement(statement); - } - assert functionWithWrapper.getWrapperBody() != null; - statements = functionWithWrapper.getWrapperBody().getStatements(); - if (!statements.isEmpty() && statements.get(statements.size() - 1) instanceof JsReturn) { - statements = statements.subList(0, statements.size() - 1); - } - - innerContext.traverse(statements); - statementContexts.pop(); - } else { - if (statementContextForInline == null) statementContextForInline = getLastStatementLevelContext(); - } - - startFunction(functionWithWrapper.getFunction()); - - JsBlock block = new JsBlock(functionWithWrapper.getFunction().getBody()); - innerContext.traverse(block.getStatements()); - functionWithWrapper.getFunction().getBody().traverse(this, innerContext); - - endFunction(functionWithWrapper.getFunction()); - - if (statements != null) { - statements.addAll(block.getStatements().subList(0, block.getStatements().size() - 1)); - } - - statementContextForInline = oldContextForInline; - existingImports = oldExistingImports; - inlineFunctionDepth = oldInlineFunctionDepth; - } - - @Override - public boolean visit(@NotNull JsInvocation call, @NotNull JsContext context) { - if (!hasToBeInlined(call)) return true; - - JsFunction containingFunction = getCurrentNamedFunction(); - - if (containingFunction != null) { - inlineCallInfos.add(new JsCallInfo(call, containingFunction)); - } - - FunctionWithWrapper definition = getFunctionContext().getFunctionDefinition(call); - - if (inProcessFunctions.contains(definition.getFunction())) { - reportInlineCycle(call, definition.getFunction()); - } - else if (!processedFunctions.contains(definition.getFunction())) { - for (int i = 0; i < call.getArguments().size(); ++i) { - JsExpression argument = call.getArguments().get(i); - call.getArguments().set(i, accept(argument)); - } - visit(definition); - return false; - } - - return true; - } - - @Override - public void endVisit(@NotNull JsInvocation x, @NotNull JsContext ctx) { - if (hasToBeInlined(x)) { - @SuppressWarnings("unchecked") - JsContext context = (JsContext) ctx; - inline(x, context); - } - - JsCallInfo lastCallInfo = null; - - if (!inlineCallInfos.isEmpty()) { - lastCallInfo = inlineCallInfos.getLast(); - } - - if (lastCallInfo != null && lastCallInfo.call == x) { - inlineCallInfos.removeLast(); - } - } - - @Override - public void endVisit(@NotNull JsExpressionStatement x, @NotNull JsContext ctx) { - JsExpression e = x.getExpression(); - if (e instanceof JsBinaryOperation) { - JsBinaryOperation binOp = (JsBinaryOperation) e; - if (binOp.getOperator() == JsBinaryOperator.ASG) { - JsExpression argument2 = binOp.getArg2(); - if (argument2 != null) { - JsFunction splitSuspendInlineFunction = splitExportedSuspendInlineFunctionDeclarations(argument2); - if (splitSuspendInlineFunction != null) { - binOp.setArg2(splitSuspendInlineFunction); - } - } - } - } - - super.endVisit(x, ctx); - } - - @Override - public void endVisit(@NotNull JsVars.JsVar x, @NotNull JsContext ctx) { - JsExpression initExpression = x.getInitExpression(); - if (initExpression != null) { - JsFunction splitSuspendInlineFunction = splitExportedSuspendInlineFunctionDeclarations(initExpression); - if (splitSuspendInlineFunction != null) { - x.setInitExpression(splitSuspendInlineFunction); - } - } - } - - @Nullable - private JsFunction splitExportedSuspendInlineFunctionDeclarations(@NotNull JsExpression expression) { - InlineMetadata inlineMetadata = InlineMetadata.decompose(expression); - if (inlineMetadata != null) { - FunctionWithWrapper functionWithWrapper = inlineMetadata.getFunction(); - JsFunction originalFunction = functionWithWrapper.getFunction(); - if (MetadataProperties.getCoroutineMetadata(originalFunction) != null) { - JsContext statementContext = getLastStatementLevelContext(); - - // This function will be exported to JS - JsFunction function = originalFunction.deepCopy(); - - // Original function should be not be transformed into a state machine - originalFunction.setName(null); - MetadataProperties.setCoroutineMetadata(originalFunction, null); - MetadataProperties.setInlineableCoroutineBody(originalFunction, true); - if (functionWithWrapper.getWrapperBody() != null) { - // Extract local declarations - applyWrapper(functionWithWrapper.getWrapperBody(), function, originalFunction, new JsInliningContext(statementContext)); - } - - // 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; - } - - @Override - protected void doAcceptStatementList(List statements) { - // at top level of js ast, contexts stack can be empty, - // but there is no inline calls anyway - if(!inliningContexts.isEmpty()) { - int i = 0; - - while (i < statements.size()) { - List additionalStatements = - ExpressionDecomposer.preserveEvaluationOrder(statements.get(i), canBeExtractedByInliner); - statements.addAll(i, additionalStatements); - i += additionalStatements.size() + 1; - } - } - - super.doAcceptStatementList(statements); - } - - private void inline(@NotNull JsInvocation call, @NotNull JsContext context) { - DeclarationDescriptor callDescriptor = MetadataProperties.getDescriptor(call); - if (isSuspendWithCurrentContinuation(callDescriptor, - CommonConfigurationKeysKt.getLanguageVersionSettings(config.getConfiguration()))) { - inlineSuspendWithCurrentContinuation(call, context); - return; - } - - JsInliningContext inliningContext = getInliningContext(); - FunctionWithWrapper functionWithWrapper = inliningContext.getFunctionContext().getFunctionDefinition(call); - - // 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 - if (functionsByFunctionNodes.containsKey(functionWithWrapper.getFunction())) { - functionWithWrapper = functionsByFunctionNodes.get(functionWithWrapper.getFunction()); - } - - JsFunction function = functionWithWrapper.getFunction().deepCopy(); - function.setBody(transformSpecialFunctionsToCoroutineMetadata(function.getBody())); - if (functionWithWrapper.getWrapperBody() != null) { - applyWrapper(functionWithWrapper.getWrapperBody(), function, functionWithWrapper.getFunction(), inliningContext); - } - InlineableResult inlineableResult = FunctionInlineMutator.getInlineableCallReplacement(call, function, inliningContext); - - JsStatement inlineableBody = inlineableResult.getInlineableBody(); - JsExpression resultExpression = inlineableResult.getResultExpression(); - JsContext statementContext = inliningContext.getStatementContext(); - // body of inline function can contain call to lambdas that need to be inlined - JsStatement inlineableBodyWithLambdasInlined = accept(inlineableBody); - assert inlineableBody == inlineableBodyWithLambdasInlined; - - // Support non-local return from secondary constructor - // Returns from secondary constructors should return `$this` object. - JsFunction currentFunction = getCurrentNamedFunction(); - if (currentFunction != null) { - JsName returnVariable = MetadataProperties.getForcedReturnVariable(currentFunction); - if (returnVariable != null) { - inlineableBody.accept(new RecursiveJsVisitor() { - @Override - public void visitReturn(@NotNull JsReturn x) { - x.setExpression(returnVariable.makeRef()); - } - }); - } - } - - statementContext.addPrevious(flattenStatement(inlineableBody)); - - /* - * Assumes, that resultExpression == null, when result is not needed. - * @see FunctionInlineMutator.isResultNeeded() - */ - if (resultExpression == null) { - statementContext.removeMe(); - return; - } - - resultExpression = accept(resultExpression); - MetadataProperties.setSynthetic(resultExpression, true); - context.replaceMe(resultExpression); - } - - private void applyWrapper( - @NotNull JsBlock wrapper, @NotNull JsFunction function, @NotNull JsFunction originalFunction, - @NotNull InliningContext inliningContext - ) { - // Apparently we should avoid this trick when we implement fair support for crossinline - Function> replacementGen = k -> { - JsContext ctx = k.context; - - Map newReplacements = new HashMap<>(); - - List copiedStatements = new ArrayList<>(); - for (JsStatement statement : wrapper.getStatements()) { - if (statement instanceof JsReturn) continue; - - statement = statement.deepCopy(); - if (inlineFunctionDepth == 0) { - replaceExpressionsWithLocalAliases(statement); - } - - if (statement instanceof JsVars) { - JsVars jsVars = (JsVars) statement; - String tag = getImportTag(jsVars); - if (tag != null) { - JsName name = jsVars.getVars().get(0).getName(); - JsName existingName = inlineFunctionDepth == 0 ? MetadataProperties.getLocalAlias(name) : null; - if (existingName == null) { - existingName = existingImports.computeIfAbsent(tag, t -> { - copiedStatements.add(jsVars); - JsName alias = JsScope.declareTemporaryName(name.getIdent()); - alias.copyMetadataFrom(name); - newReplacements.put(name, pureFqn(alias, null)); - return alias; - }); - } - - if (name != existingName) { - JsNameRef replacement = pureFqn(existingName, null); - newReplacements.put(name, replacement); - } - - continue; - } - } - - copiedStatements.add(statement); - } - - Set definedNames = copiedStatements.stream() - .flatMap(node -> CollectUtilsKt.collectDefinedNamesInAllScopes(node).stream()) - .filter(name -> !newReplacements.containsKey(name)) - .collect(Collectors.toSet()); - for (JsName name : definedNames) { - JsName alias = JsScope.declareTemporaryName(name.getIdent()); - alias.copyMetadataFrom(name); - JsNameRef replacement = pureFqn(alias, null); - newReplacements.put(name, replacement); - } - - for (JsStatement statement : copiedStatements) { - statement = RewriteUtilsKt.replaceNames(statement, newReplacements); - ctx.addPrevious(accept(statement)); - } - - for (Map.Entry entry : CollectUtilsKt.collectNamedFunctions(new JsBlock(copiedStatements)).entrySet()) { - if (MetadataProperties.getStaticRef(entry.getKey()) instanceof JsFunction) { - MetadataProperties.setStaticRef(entry.getKey(), entry.getValue()); - } - } - - return newReplacements; - }; - - JsWrapperKey key = new JsWrapperKey(inliningContext.getStatementContextBeforeCurrentFunction(), originalFunction); - Map replacements = replacementsInducedByWrappers.computeIfAbsent(key, replacementGen); - - RewriteUtilsKt.replaceNames(function, replacements); - - // Copy nameBinding's for inlined localAlias'es - for (JsNameRef nameRef : replacements.values()) { - JsName name = nameRef.getName(); - if (name != null && !existingNameBindings.containsKey(name)) { - String tag = inverseNameBindings.get(name); - if (tag != null) { - existingNameBindings.put(name, tag); - additionalNameBindings.add(new JsNameBinding(tag, name)); - } - } - } - } - - private void replaceExpressionsWithLocalAliases(@NotNull JsStatement statement) { - new JsVisitorWithContextImpl() { - @Override - public void endVisit(@NotNull JsNameRef x, @NotNull JsContext ctx) { - replaceIfNecessary(x, ctx); - } - - @Override - public void endVisit(@NotNull JsArrayAccess x, @NotNull JsContext ctx) { - replaceIfNecessary(x, ctx); - } - - private void replaceIfNecessary(@NotNull JsExpression expression, @NotNull JsContext ctx) { - JsName alias = MetadataProperties.getLocalAlias(expression); - if (alias != null) { - @SuppressWarnings("unchecked") - JsContext context = (JsContext) ctx; - context.replaceMe(alias.makeRef()); - inlinedModuleAliases.add(alias); - } - } - - }.accept(statement); - } - - private static boolean isSuspendWithCurrentContinuation( - @Nullable DeclarationDescriptor descriptor, - @NotNull LanguageVersionSettings languageVersionSettings - ) { - if (!(descriptor instanceof FunctionDescriptor)) return false; - return CommonCoroutineCodegenUtilKt.isBuiltInSuspendCoroutineUninterceptedOrReturn( - (FunctionDescriptor) descriptor.getOriginal(), languageVersionSettings - ); - } - - private void inlineSuspendWithCurrentContinuation(@NotNull JsInvocation call, @NotNull JsContext context) { - JsExpression lambda = call.getArguments().get(0); - JsExpression continuationArg = call.getArguments().get(call.getArguments().size() - 1); - - JsInvocation invocation = new JsInvocation(lambda, continuationArg); - MetadataProperties.setSuspend(invocation, true); - context.replaceMe(accept(invocation)); - } - - @NotNull - private JsInliningContext getInliningContext() { - return inliningContexts.peek(); - } - - @NotNull - private FunctionContext getFunctionContext() { - return getInliningContext().getFunctionContext(); - } - - @Nullable - private JsFunction getCurrentNamedFunction() { - if (namedFunctionsStack.empty()) return null; - return namedFunctionsStack.peek(); - } - - private void reportInlineCycle(@NotNull JsInvocation call, @NotNull JsFunction calledFunction) { - MetadataProperties.setInlineStrategy(call, InlineStrategy.NOT_INLINE); - Iterator it = inlineCallInfos.descendingIterator(); - - while (it.hasNext()) { - JsCallInfo callInfo = it.next(); - PsiElement psiElement = MetadataProperties.getPsiElement(callInfo.call); - - CallableDescriptor descriptor = MetadataProperties.getDescriptor(callInfo.call); - if (psiElement != null && descriptor != null) { - trace.report(Errors.INLINE_CALL_CYCLE.on(psiElement, descriptor)); - } - - if (callInfo.containingFunction == calledFunction) { - break; - } - } - } - - private boolean hasToBeInlined(@NotNull JsInvocation call) { - InlineStrategy strategy = MetadataProperties.getInlineStrategy(call); - if (strategy == null || !strategy.isInline()) return false; - - return getFunctionContext().hasFunctionDefinition(call); - } - - private class JsInliningContext implements InliningContext { - private final FunctionContext functionContext; - - @NotNull - private final JsContext statementContextBeforeCurrentFunction; - - JsInliningContext(@NotNull JsContext statementContextBeforeCurrentFunction) { - functionContext = new FunctionContext(functionReader, config) { - @Nullable - @Override - protected FunctionWithWrapper lookUpStaticFunction(@Nullable JsName functionName) { - return functions.get(functionName); - } - - @Nullable - @Override - protected FunctionWithWrapper lookUpStaticFunctionByTag(@NotNull String functionTag) { - return accessors.get(functionTag); - } - }; - this.statementContextBeforeCurrentFunction = statementContextBeforeCurrentFunction; - } - - @NotNull - @Override - public NamingContext newNamingContext() { - return new NamingContext(getStatementContext()); - } - - @NotNull - @Override - public JsContext getStatementContext() { - return getLastStatementLevelContext(); - } - - @NotNull - @Override - public FunctionContext getFunctionContext() { - return functionContext; - } - - @NotNull - @Override - public JsContext getStatementContextBeforeCurrentFunction() { - return statementContextBeforeCurrentFunction; - } - } - - private static class JsCallInfo { - @NotNull - public final JsInvocation call; - - @NotNull - public final JsFunction containingFunction; - - private JsCallInfo(@NotNull JsInvocation call, @NotNull JsFunction function) { - this.call = call; - containingFunction = function; - } - } - - static class JsWrapperKey { - final JsContext context; - private final JsFunction function; - - public JsWrapperKey(@NotNull JsContext context, @NotNull JsFunction function) { - this.context = context; - this.function = function; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - JsWrapperKey key = (JsWrapperKey) o; - return Objects.equals(context, key.context) && Objects.equals(function, key.function); - } - - @Override - public int hashCode() { - return Objects.hash(context, function); - } - } -} 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 new file mode 100644 index 00000000000..be7b1fbba44 --- /dev/null +++ b/js/js.inliner/src/org/jetbrains/kotlin/js/inline/JsInliner.kt @@ -0,0 +1,653 @@ +/* + * 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.backend.common.* +import org.jetbrains.kotlin.config.* +import org.jetbrains.kotlin.config.LanguageVersionSettings +import org.jetbrains.kotlin.descriptors.DeclarationDescriptor +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.diagnostics.DiagnosticSink +import org.jetbrains.kotlin.diagnostics.Errors +import org.jetbrains.kotlin.js.backend.ast.* +import org.jetbrains.kotlin.js.backend.ast.metadata.* +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.context.InliningContext +import org.jetbrains.kotlin.js.inline.context.NamingContext +import org.jetbrains.kotlin.js.inline.util.* +import org.jetbrains.kotlin.js.translate.expression.InlineMetadata +import org.jetbrains.kotlin.resolve.inline.InlineStrategy + +import java.util.* + +import org.jetbrains.kotlin.js.inline.util.getImportTag +import org.jetbrains.kotlin.js.inline.util.IdentitySet +import org.jetbrains.kotlin.js.translate.declaration.transformSpecialFunctionsToCoroutineMetadata +import org.jetbrains.kotlin.js.translate.utils.JsAstUtils.flattenStatement +import org.jetbrains.kotlin.js.translate.utils.JsAstUtils.pureFqn + +class JsInliner private constructor( + private val config: JsConfig, + private val functions: Map, + private val accessors: Map, + private val inverseNameBindings: Map, + private val functionReader: FunctionReader, + private val trace: DiagnosticSink +) : JsVisitorWithContextImpl() { + private val namedFunctionsSet: MutableSet = functions.values.mapTo(IdentitySet()) { it.function } + private val inliningContexts = Stack() + private val processedFunctions = IdentitySet() + private val inProcessFunctions = IdentitySet() + private var existingImports: MutableMap = HashMap() + private var statementContextForInline: JsContext? = null + + private val functionsByWrapperNodes = HashMap() + private val functionsByFunctionNodes = HashMap() + init { + (functions.values.asSequence() + accessors.values.asSequence()).forEach { f -> + functionsByFunctionNodes[f.function] = f + if (f.wrapperBody != null) { + functionsByWrapperNodes[f.wrapperBody] = f + } + } + } + + // these are needed for error reporting, when inliner detects cycle + private val namedFunctionsStack = Stack() + private val inlineCallInfos = LinkedList() + private val canBeExtractedByInliner: (JsNode) -> Boolean = { node -> node is JsInvocation && hasToBeInlined(node) } + private var inlineFunctionDepth: Int = 0 + + private val replacementsInducedByWrappers = HashMap>() + + private var existingNameBindings: MutableMap = HashMap() + + private val additionalNameBindings = ArrayList() + + private val inlinedModuleAliases = HashSet() + + private val inliningContext: JsInliningContext + get() = inliningContexts.peek() + + private val functionContext: FunctionContext + get() = inliningContext.functionContext + + private val currentNamedFunction: JsFunction? + get() = if (namedFunctionsStack.empty()) null else namedFunctionsStack.peek() + + private fun addInlinedModules(fragment: JsProgramFragment, moduleMap: Map) { + val localMap = buildModuleMap(listOf(fragment)).keys + for (inlinedModuleName in inlinedModuleAliases) { + if (!localMap.contains(inlinedModuleName)) { + fragment.importedModules.add(moduleMap[inlinedModuleName]!!) + } + } + } + + private fun processImportStatement(statement: JsStatement) { + if (statement is JsVars) { + val tag = getImportTag(statement) + if (tag != null) { + existingImports[tag] = statement.vars[0].name + } + } + } + + override fun visit(function: JsFunction, context: JsContext<*>): Boolean { + val functionWithWrapper = functionsByFunctionNodes[function] + if (functionWithWrapper != null) { + visit(functionWithWrapper) + return false + } else { + if (statementContextForInline == null) { + statementContextForInline = lastStatementLevelContext + startFunction(function) + val result = super.visit(function, context) + statementContextForInline = null + return result + } else { + startFunction(function) + return super.visit(function, context) + } + } + } + + override fun endVisit(function: JsFunction, context: JsContext<*>) { + super.endVisit(function, context) + if (!functionsByFunctionNodes.containsKey(function)) { + endFunction(function) + } + } + + private fun startFunction(function: JsFunction) { + inliningContexts.push(JsInliningContext(statementContextForInline!!)) + + assert(!inProcessFunctions.contains(function)) { "Inliner has revisited function" } + inProcessFunctions.add(function) + + if (namedFunctionsSet.contains(function)) { + namedFunctionsStack.push(function) + } + } + + private fun endFunction(function: JsFunction) { + refreshLabelNames(function.body, function.scope) + + removeUnusedLocalFunctionDeclarations(function) + processedFunctions.add(function) + + FunctionPostProcessor(function).apply() + + assert(inProcessFunctions.contains(function)) + inProcessFunctions.remove(function) + + inliningContexts.pop() + + if (!namedFunctionsStack.empty() && namedFunctionsStack.peek() == function) { + namedFunctionsStack.pop() + } + } + + override fun visit(x: JsBlock, ctx: JsContext<*>): Boolean { + val functionWithWrapper = functionsByWrapperNodes[x] + if (functionWithWrapper != null) { + visit(functionWithWrapper) + return false + } + return super.visit(x, ctx) + } + + private fun visit(functionWithWrapper: FunctionWithWrapper) { + val oldContextForInline = statementContextForInline + val oldExistingImports = existingImports + val oldInlineFunctionDepth = inlineFunctionDepth + + val innerContext = ListContext() + + val wrapperBody = functionWithWrapper.wrapperBody + var statements: MutableList? = null + if (wrapperBody != null) { + existingImports = HashMap() + statementContexts.push(innerContext) + statementContextForInline = innerContext + inlineFunctionDepth++ + + for (statement in wrapperBody.statements) { + processImportStatement(statement) + } + statements = wrapperBody.statements + if (!statements.isEmpty() && statements[statements.size - 1] is JsReturn) { + statements = statements.subList(0, statements.size - 1) + } + + innerContext.traverse(statements) + statementContexts.pop() + } else { + if (statementContextForInline == null) statementContextForInline = lastStatementLevelContext + } + + startFunction(functionWithWrapper.function) + + val block = JsBlock(functionWithWrapper.function.body) + innerContext.traverse(block.statements) + functionWithWrapper.function.body.traverse(this, innerContext) + + endFunction(functionWithWrapper.function) + + statements?.addAll(block.statements.subList(0, block.statements.size - 1)) + + statementContextForInline = oldContextForInline + existingImports = oldExistingImports + inlineFunctionDepth = oldInlineFunctionDepth + } + + override fun visit(call: JsInvocation, context: JsContext<*>): Boolean { + if (!hasToBeInlined(call)) return true + + val containingFunction = currentNamedFunction + + if (containingFunction != null) { + inlineCallInfos.add(JsCallInfo(call, containingFunction)) + } + + val definition = functionContext.getFunctionDefinition(call) + + if (inProcessFunctions.contains(definition.function)) { + reportInlineCycle(call, definition.function) + } else if (!processedFunctions.contains(definition.function)) { + for (i in 0 until call.arguments.size) { + val argument = call.arguments[i] + call.arguments[i] = accept(argument) + } + visit(definition) + return false + } + + return true + } + + override fun endVisit(x: JsInvocation, ctx: JsContext) { + if (hasToBeInlined(x)) { + inline(x, ctx) + } + + var lastCallInfo: JsCallInfo? = null + + if (!inlineCallInfos.isEmpty()) { + lastCallInfo = inlineCallInfos.last + } + + if (lastCallInfo != null && lastCallInfo.call == x) { + inlineCallInfos.removeLast() + } + } + + override fun endVisit(x: JsExpressionStatement, ctx: JsContext<*>) { + val e = x.expression + if (e is JsBinaryOperation) { + if (e.operator == JsBinaryOperator.ASG) { + e.arg2?.let { argument2 -> + val splitSuspendInlineFunction = splitExportedSuspendInlineFunctionDeclarations(argument2) + if (splitSuspendInlineFunction != null) { + e.arg2 = splitSuspendInlineFunction + } + } + } + } + + super.endVisit(x, ctx) + } + + override fun endVisit(x: JsVars.JsVar, ctx: JsContext<*>) { + val initExpression = x.initExpression ?: return + + val splitSuspendInlineFunction = splitExportedSuspendInlineFunctionDeclarations(initExpression) + if (splitSuspendInlineFunction != null) { + x.initExpression = splitSuspendInlineFunction + } + } + + 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 + + // This function will be exported to JS + val function = originalFunction.deepCopy() + + // 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, JsInliningContext(statementContext)) + } + + // 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 + } + + override fun doAcceptStatementList(statements: MutableList) { + // at top level of js ast, contexts stack can be empty, + // but there is no inline calls anyway + if (!inliningContexts.isEmpty()) { + var i = 0 + + while (i < statements.size) { + val additionalStatements = ExpressionDecomposer.preserveEvaluationOrder(statements[i], canBeExtractedByInliner) + statements.addAll(i, additionalStatements) + i += additionalStatements.size + 1 + } + } + + super.doAcceptStatementList(statements) + } + + private fun inline(call: JsInvocation, context: JsContext) { + val callDescriptor = call.descriptor + if (isSuspendWithCurrentContinuation( + callDescriptor, + config.configuration.languageVersionSettings + ) + ) { + inlineSuspendWithCurrentContinuation(call, context) + return + } + + val inliningContext = inliningContext + var functionWithWrapper = inliningContext.functionContext.getFunctionDefinition(call) + + // 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 + functionsByFunctionNodes[functionWithWrapper.function]?.let { + functionWithWrapper = it + } + + val function = functionWithWrapper.function.deepCopy() + function.body = transformSpecialFunctionsToCoroutineMetadata(function.body) + if (functionWithWrapper.wrapperBody != null) { + applyWrapper(functionWithWrapper.wrapperBody!!, function, functionWithWrapper.function, inliningContext) + } + val inlineableResult = FunctionInlineMutator.getInlineableCallReplacement(call, function, inliningContext) + + 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. + val currentFunction = currentNamedFunction + if (currentFunction != null) { + val returnVariable = currentFunction.forcedReturnVariable + if (returnVariable != null) { + inlineableBody.accept(object : RecursiveJsVisitor() { + override fun visitReturn(x: JsReturn) { + x.expression = returnVariable.makeRef() + } + }) + } + } + + statementContext.addPrevious(flattenStatement(inlineableBody)) + + /* + * Assumes, that resultExpression == null, when result is not needed. + * @see FunctionInlineMutator.isResultNeeded() + */ + if (resultExpression == null) { + statementContext.removeMe() + return + } + + resultExpression = accept(resultExpression) + resultExpression.synthetic = true + context.replaceMe(resultExpression) + } + + private fun applyWrapper( + wrapper: JsBlock, function: JsFunction, originalFunction: JsFunction, + inliningContext: InliningContext + ) { + val key = JsWrapperKey(inliningContext.statementContextBeforeCurrentFunction, originalFunction) + + // Apparently we should avoid this trick when we implement fair support for crossinline + val replacements = replacementsInducedByWrappers.computeIfAbsent(key) { k -> + val ctx = k.context + + val newReplacements = HashMap() + + val copiedStatements = ArrayList() + wrapper.statements.asSequence() + .filterNot { it is JsReturn } + .map { it.deepCopy() } + .forEach { statement -> + if (inlineFunctionDepth == 0) { + replaceExpressionsWithLocalAliases(statement) + } + + if (statement is JsVars) { + val tag = getImportTag(statement) + if (tag != null) { + val name = statement.vars[0].name + var existingName: JsName? = if (inlineFunctionDepth == 0) name.localAlias else null + if (existingName == null) { + existingName = existingImports.computeIfAbsent(tag) { + copiedStatements.add(statement) + val alias = JsScope.declareTemporaryName(name.ident) + alias.copyMetadataFrom(name) + newReplacements[name] = pureFqn(alias, null) + alias + } + } + + if (name !== existingName) { + val replacement = pureFqn(existingName, null) + newReplacements[name] = replacement + } + + return@forEach + } + } + + copiedStatements.add(statement) + } + + 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 = pureFqn(alias, null) + newReplacements[name] = replacement + } + + for (statement in copiedStatements) { + ctx.addPrevious(accept(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) { + 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) + } + + private fun inlineSuspendWithCurrentContinuation(call: JsInvocation, context: JsContext) { + val lambda = call.arguments[0] + val continuationArg = call.arguments[call.arguments.size - 1] + + val invocation = JsInvocation(lambda, continuationArg) + invocation.isSuspend = true + context.replaceMe(accept(invocation)) + } + + private fun reportInlineCycle(call: JsInvocation, calledFunction: JsFunction) { + call.inlineStrategy = InlineStrategy.NOT_INLINE + val it = inlineCallInfos.descendingIterator() + + while (it.hasNext()) { + val callInfo = it.next() + val psiElement = callInfo.call.psiElement + + val descriptor = callInfo.call.descriptor + if (psiElement != null && descriptor != null) { + trace.report(Errors.INLINE_CALL_CYCLE.on(psiElement, descriptor)) + } + + if (callInfo.containingFunction == calledFunction) { + break + } + } + } + + private fun hasToBeInlined(call: JsInvocation): Boolean { + val strategy = call.inlineStrategy + return if (strategy == null || !strategy.isInline) false else functionContext.hasFunctionDefinition(call) + + } + + private inner class JsInliningContext internal constructor(override val statementContextBeforeCurrentFunction: JsContext) : + InliningContext { + override val functionContext: FunctionContext + + override val statementContext: JsContext + get() = lastStatementLevelContext + + init { + functionContext = object : FunctionContext(functionReader, config) { + override fun lookUpStaticFunction(functionName: JsName?): FunctionWithWrapper? { + return functions[functionName] + } + + override fun lookUpStaticFunctionByTag(functionTag: String): FunctionWithWrapper? { + return accessors[functionTag] + } + } + } + + override fun newNamingContext(): NamingContext { + return NamingContext(statementContext) + } + } + + private class JsCallInfo(val call: JsInvocation, val containingFunction: JsFunction) + + internal class JsWrapperKey(val context: JsContext, private val function: JsFunction) { + + override fun equals(o: Any?): Boolean { + if (this === o) return true + if (o == null || javaClass != o.javaClass) return false + val key = o as JsWrapperKey? + return context == key!!.context && function == key.function + } + + override fun hashCode(): Int { + return Objects.hash(context, function) + } + } + + companion object { + + fun process( + reporter: JsConfig.Reporter, + config: JsConfig, + trace: DiagnosticSink, + currentModuleName: JsName, + fragments: List, + fragmentsToProcess: List, + importStatements: List + ) { + val functions = collectNamedFunctionsAndWrappers(fragments) + val accessors = collectAccessors(fragments) + val inverseNameBindings = collectNameBindings(fragments) + + val accessorInvocationTransformer = DummyAccessorInvocationTransformer() + for (fragment in fragmentsToProcess) { + accessorInvocationTransformer.accept(fragment.declarationBlock) + accessorInvocationTransformer.accept(fragment.initializerBlock) + } + val functionReader = FunctionReader(reporter, config, currentModuleName, fragments) + val inliner = JsInliner(config, functions, accessors, inverseNameBindings, functionReader, trace) + + for (statement in importStatements) { + inliner.processImportStatement(statement) + } + + val moduleMap = fillModuleMap(buildModuleMap(fragments), fragmentsToProcess) + + for (fragment in fragmentsToProcess) { + inliner.existingImports.clear() + inliner.additionalNameBindings.clear() + inliner.inlinedModuleAliases.clear() + inliner.existingNameBindings = collectNameBindings(listOf(fragment)) + + inliner.acceptStatement(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) + } + + // There can be inlined function in top-level initializers, we need to optimize them as well + val fakeInitFunction = JsFunction(JsDynamicScope, fragment.initializerBlock, "") + val initWrapper = JsGlobalBlock() + initWrapper.statements.add(JsExpressionStatement(fakeInitFunction)) + inliner.accept(initWrapper) + initWrapper.statements.removeAt(initWrapper.statements.size - 1) + + fragment.initializerBlock.getStatements().addAll(0, initWrapper.statements) + fragment.nameBindings.addAll(inliner.additionalNameBindings) + inliner.addInlinedModules(fragment, moduleMap) + } + + for (fragment in fragmentsToProcess) { + val block = JsBlock(fragment.declarationBlock, fragment.initializerBlock, fragment.exportBlock) + removeUnusedImports(block) + simplifyWrappedFunctions(block) + removeUnusedFunctionDefinitions(block, collectNamedFunctions(block)) + } + } + + private fun buildModuleMap(fragments: List): MutableMap { + return fillModuleMap(HashMap(), fragments) + } + + private fun fillModuleMap( + map: MutableMap, + fragments: List + ): MutableMap { + for (fragment in fragments) { + for (module in fragment.importedModules) { + map[module.internalName] = module + } + } + return map + } + + private fun isSuspendWithCurrentContinuation( + descriptor: DeclarationDescriptor?, + languageVersionSettings: LanguageVersionSettings + ): Boolean { + return (descriptor as? FunctionDescriptor)?.original?.isBuiltInSuspendCoroutineUninterceptedOrReturn( + languageVersionSettings + ) ?: false + } + } +} 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 15b9e4a5ddd..a6bc6470109 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 @@ -230,7 +230,7 @@ fun collectAccessors(fragments: List): Map): Map { +fun collectNameBindings(fragments: List): MutableMap { val result = mutableMapOf() for (fragment in fragments) { for (binding in fragment.nameBindings) {