/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.js.inline;
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.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.FunctionPostProcessor;
import org.jetbrains.kotlin.js.inline.clean.RemoveUnusedFunctionDefinitionsKt;
import org.jetbrains.kotlin.js.inline.clean.RemoveUnusedLocalFunctionDeclarationsKt;
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.CollectUtilsKt;
import org.jetbrains.kotlin.js.inline.util.CollectionUtilsKt;
import org.jetbrains.kotlin.js.inline.util.NamingUtilsKt;
import org.jetbrains.kotlin.resolve.inline.InlineStrategy;
import java.util.*;
import static org.jetbrains.kotlin.js.inline.FunctionInlineMutator.getInlineableCallReplacement;
import static org.jetbrains.kotlin.js.translate.utils.JsAstUtils.flattenStatement;
public class JsInliner extends JsVisitorWithContextImpl {
private final JsConfig config;
private final Map<JsName, JsFunction> functions;
private final Map<String, JsFunction> accessors;
private final Stack<JsInliningContext> inliningContexts = new Stack<>();
private final Set<JsFunction> processedFunctions = CollectionUtilsKt.IdentitySet();
private final Set<JsFunction> inProcessFunctions = CollectionUtilsKt.IdentitySet();
private final FunctionReader functionReader;
private final DiagnosticSink trace;
// these are needed for error reporting, when inliner detects cycle
private final Stack<JsFunction> namedFunctionsStack = new Stack<>();
private final LinkedList<JsCallInfo> inlineCallInfos = new LinkedList<>();
private final Function1<JsNode, Boolean> canBeExtractedByInliner =
node -> node instanceof JsInvocation && hasToBeInlined((JsInvocation) node);
public static void process(
@NotNull JsConfig config,
@NotNull DiagnosticSink trace,
@NotNull JsName currentModuleName,
@NotNull List<JsProgramFragment> fragments,
@NotNull List<JsProgramFragment> fragmentsToProcess
) {
Map<JsName, JsFunction> functions = CollectUtilsKt.collectNamedFunctions(fragments);
Map<String, JsFunction> accessors = CollectUtilsKt.collectAccessors(fragments);
DummyAccessorInvocationTransformer accessorInvocationTransformer = new DummyAccessorInvocationTransformer();
for (JsProgramFragment fragment : fragmentsToProcess) {
accessorInvocationTransformer.accept(fragment.getDeclarationBlock());
accessorInvocationTransformer.accept(fragment.getInitializerBlock());
}
FunctionReader functionReader = new FunctionReader(config, currentModuleName, fragments);
JsInliner inliner = new JsInliner(config, functions, accessors, functionReader, trace);
for (JsProgramFragment fragment : fragmentsToProcess) {
inliner.inliningContexts.push(inliner.new JsInliningContext());
inliner.accept(fragment.getDeclarationBlock());
// There can be inlined function in top-level initializers, we need to optimize them as well
JsFunction fakeInitFunction = new JsFunction(JsDynamicScope.INSTANCE, fragment.getInitializerBlock(), "");
inliner.accept(fakeInitFunction);
inliner.inliningContexts.pop();
JsBlock block = new JsBlock(fragment.getDeclarationBlock(), fragment.getInitializerBlock(), fragment.getExportBlock());
RemoveUnusedFunctionDefinitionsKt.removeUnusedFunctionDefinitions(block, functions);
}
}
private JsInliner(
@NotNull JsConfig config,
@NotNull Map<JsName, JsFunction> functions,
@NotNull Map<String, JsFunction> accessors,
@NotNull FunctionReader functionReader,
@NotNull DiagnosticSink trace
) {
this.config = config;
this.functions = functions;
this.accessors = accessors;
this.functionReader = functionReader;
this.trace = trace;
}
@Override
public boolean visit(@NotNull JsFunction function, @NotNull JsContext context) {
inliningContexts.push(new JsInliningContext());
assert !inProcessFunctions.contains(function): "Inliner has revisited function";
inProcessFunctions.add(function);
if (functions.containsValue(function)) {
namedFunctionsStack.push(function);
}
return super.visit(function, context);
}
@Override
public void endVisit(@NotNull JsFunction function, @NotNull JsContext context) {
super.endVisit(function, context);
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 JsInvocation call, @NotNull JsContext context) {
if (!hasToBeInlined(call)) return true;
JsFunction containingFunction = getCurrentNamedFunction();
if (containingFunction != null) {
inlineCallInfos.add(new JsCallInfo(call, containingFunction));
}
JsFunction definition = getFunctionContext().getFunctionDefinition(call);
if (inProcessFunctions.contains(definition)) {
reportInlineCycle(call, definition);
}
else if (!processedFunctions.contains(definition)) {
accept(definition);
}
return true;
}
@Override
public void endVisit(@NotNull JsInvocation x, @NotNull JsContext ctx) {
if (hasToBeInlined(x)) {
inline(x, ctx);
}
JsCallInfo lastCallInfo = null;
if (!inlineCallInfos.isEmpty()) {
lastCallInfo = inlineCallInfos.getLast();
}
if (lastCallInfo != null && lastCallInfo.call == x) {
inlineCallInfos.removeLast();
}
}
@Override
protected void doAcceptStatementList(List<JsStatement> 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<JsStatement> 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)) {
inlineSuspendWithCurrentContinuation(call, context);
return;
}
JsInliningContext inliningContext = getInliningContext();
InlineableResult inlineableResult = getInlineableCallReplacement(call, inliningContext);
JsStatement inlineableBody = inlineableResult.getInlineableBody();
JsExpression resultExpression = inlineableResult.getResultExpression();
JsContext<JsStatement> statementContext = inliningContext.getStatementContext();
// body of inline function can contain call to lambdas that need to be inlined
JsStatement inlineableBodyWithLambdasInlined = accept(inlineableBody);
assert inlineableBody == inlineableBodyWithLambdasInlined;
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 static boolean isSuspendWithCurrentContinuation(@Nullable DeclarationDescriptor descriptor) {
if (!(descriptor instanceof FunctionDescriptor)) return false;
return CommonCoroutineCodegenUtilKt.isBuiltInSuspendCoroutineOrReturn((FunctionDescriptor) descriptor.getOriginal());
}
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<JsCallInfo> 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;
JsInliningContext() {
functionContext = new FunctionContext(functionReader, config) {
@Nullable
@Override
protected JsFunction lookUpStaticFunction(@Nullable JsName functionName) {
return functions.get(functionName);
}
@Nullable
@Override
protected JsFunction lookUpStaticFunctionByTag(@NotNull String functionTag) {
return accessors.get(functionTag);
}
};
}
@NotNull
@Override
public NamingContext newNamingContext() {
return new NamingContext(getStatementContext());
}
@NotNull
@Override
public JsContext<JsStatement> getStatementContext() {
return getLastStatementLevelContext();
}
@NotNull
@Override
public FunctionContext getFunctionContext() {
return functionContext;
}
}
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;
}
}
}