JS: convert JsInliner to Kotlin

This commit is contained in:
Anton Bannykh
2018-11-14 18:17:34 +03:00
parent 039625bb7e
commit ec276dbea4
3 changed files with 654 additions and 758 deletions
@@ -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<JsName, FunctionWithWrapper> functions;
private final Set<JsFunction> namedFunctionsSet;
private final Map<String, FunctionWithWrapper> 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;
private Map<String, JsName> existingImports = new HashMap<>();
private JsContext<JsStatement> statementContextForInline;
private final Map<JsBlock, FunctionWithWrapper> functionsByWrapperNodes = new HashMap<>();
private final Map<JsFunction, FunctionWithWrapper> functionsByFunctionNodes = new HashMap<>();
// 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);
private int inlineFunctionDepth;
private final Map<JsWrapperKey, Map<JsName, JsNameRef>> replacementsInducedByWrappers = new HashMap<>();
private final Map<JsName, String> inverseNameBindings;
private Map<JsName, String> existingNameBindings = new HashMap<>();
private final List<JsNameBinding> additionalNameBindings = new ArrayList<>();
private final Set<JsName> inlinedModuleAliases = new HashSet<>();
public static void process(
@NotNull JsConfig.Reporter reporter,
@NotNull JsConfig config,
@NotNull DiagnosticSink trace,
@NotNull JsName currentModuleName,
@NotNull List<JsProgramFragment> fragments,
@NotNull List<JsProgramFragment> fragmentsToProcess,
@NotNull List<JsStatement> importStatements
) {
Map<JsName, FunctionWithWrapper> functions = CollectUtilsKt.collectNamedFunctionsAndWrappers(fragments);
Map<String, FunctionWithWrapper> accessors = CollectUtilsKt.collectAccessors(fragments);
Map<JsName, String> 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<JsName, JsImportedModule> 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<JsName, JsImportedModule> moduleMap) {
Set<JsName> localMap = buildModuleMap(Collections.singletonList(fragment)).keySet();
for (JsName inlinedModuleName : inlinedModuleAliases) {
if (!localMap.contains(inlinedModuleName)) {
fragment.getImportedModules().add(moduleMap.get(inlinedModuleName));
}
}
}
private static Map<JsName, JsImportedModule> buildModuleMap(List<JsProgramFragment> fragments) {
return fillModuleMap(new HashMap<>(), fragments);
}
private static Map<JsName, JsImportedModule> fillModuleMap(Map<JsName, JsImportedModule> map, List<JsProgramFragment> fragments) {
for (JsProgramFragment fragment : fragments) {
for (JsImportedModule module : fragment.getImportedModules()) {
map.put(module.getInternalName(), module);
}
}
return map;
}
private JsInliner(
@NotNull JsConfig config,
@NotNull Map<JsName, FunctionWithWrapper> functions,
@NotNull Map<String, FunctionWithWrapper> accessors,
@NotNull Map<JsName, String> 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<JsStatement> oldContextForInline = statementContextForInline;
Map<String, JsName> oldExistingImports = existingImports;
int oldInlineFunctionDepth = inlineFunctionDepth;
ListContext<JsStatement> innerContext = new ListContext<>();
JsBlock wrapperBody = functionWithWrapper.getWrapperBody();
List<JsStatement> 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<JsNode> 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<JsStatement> 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<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<JsNode> 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<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;
// 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<JsWrapperKey, Map<JsName, JsNameRef>> replacementGen = k -> {
JsContext<JsStatement> ctx = k.context;
Map<JsName, JsNameRef> newReplacements = new HashMap<>();
List<JsStatement> 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<JsName> 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<JsName, JsFunction> 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<JsName, JsNameRef> 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<JsNode> 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<JsNode> 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;
@NotNull
private final JsContext<JsStatement> statementContextBeforeCurrentFunction;
JsInliningContext(@NotNull JsContext<JsStatement> 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<JsStatement> getStatementContext() {
return getLastStatementLevelContext();
}
@NotNull
@Override
public FunctionContext getFunctionContext() {
return functionContext;
}
@NotNull
@Override
public JsContext<JsStatement> 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<JsStatement> context;
private final JsFunction function;
public JsWrapperKey(@NotNull JsContext<JsStatement> 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);
}
}
}
@@ -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<JsName, FunctionWithWrapper>,
private val accessors: Map<String, FunctionWithWrapper>,
private val inverseNameBindings: Map<JsName, String>,
private val functionReader: FunctionReader,
private val trace: DiagnosticSink
) : JsVisitorWithContextImpl() {
private val namedFunctionsSet: MutableSet<JsFunction> = functions.values.mapTo(IdentitySet()) { it.function }
private val inliningContexts = Stack<JsInliningContext>()
private val processedFunctions = IdentitySet<JsFunction>()
private val inProcessFunctions = IdentitySet<JsFunction>()
private var existingImports: MutableMap<String, JsName> = HashMap()
private var statementContextForInline: JsContext<JsStatement>? = null
private val functionsByWrapperNodes = HashMap<JsBlock, FunctionWithWrapper>()
private val functionsByFunctionNodes = HashMap<JsFunction, FunctionWithWrapper>()
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<JsFunction>()
private val inlineCallInfos = LinkedList<JsCallInfo>()
private val canBeExtractedByInliner: (JsNode) -> Boolean = { node -> node is JsInvocation && hasToBeInlined(node) }
private var inlineFunctionDepth: Int = 0
private val replacementsInducedByWrappers = HashMap<JsWrapperKey, Map<JsName, JsNameRef>>()
private var existingNameBindings: MutableMap<JsName, String> = HashMap()
private val additionalNameBindings = ArrayList<JsNameBinding>()
private val inlinedModuleAliases = HashSet<JsName>()
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<JsName, JsImportedModule>) {
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<JsStatement>()
val wrapperBody = functionWithWrapper.wrapperBody
var statements: MutableList<JsStatement>? = 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<JsNode>) {
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<JsStatement>) {
// 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<JsNode>) {
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<JsName, JsNameRef>()
val copiedStatements = ArrayList<JsStatement>()
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<JsNode>) {
replaceIfNecessary(x, ctx)
}
override fun endVisit(x: JsArrayAccess, ctx: JsContext<JsNode>) {
replaceIfNecessary(x, ctx)
}
private fun replaceIfNecessary(expression: JsExpression, ctx: JsContext<JsNode>) {
val alias = expression.localAlias
if (alias != null) {
ctx.replaceMe(alias.makeRef())
inlinedModuleAliases.add(alias)
}
}
}.accept(statement)
}
private fun inlineSuspendWithCurrentContinuation(call: JsInvocation, context: JsContext<JsNode>) {
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<JsStatement>) :
InliningContext {
override val functionContext: FunctionContext
override val statementContext: JsContext<JsStatement>
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<JsStatement>, 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<JsProgramFragment>,
fragmentsToProcess: List<JsProgramFragment>,
importStatements: List<JsStatement>
) {
val functions = collectNamedFunctionsAndWrappers(fragments)
val accessors = collectAccessors(fragments)
val inverseNameBindings = collectNameBindings(fragments)
val accessorInvocationTransformer = DummyAccessorInvocationTransformer()
for (fragment in fragmentsToProcess) {
accessorInvocationTransformer.accept<JsGlobalBlock>(fragment.declarationBlock)
accessorInvocationTransformer.accept<JsGlobalBlock>(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<JsGlobalBlock>(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<JsProgramFragment>): MutableMap<JsName, JsImportedModule> {
return fillModuleMap(HashMap(), fragments)
}
private fun fillModuleMap(
map: MutableMap<JsName, JsImportedModule>,
fragments: List<JsProgramFragment>
): MutableMap<JsName, JsImportedModule> {
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
}
}
}
@@ -230,7 +230,7 @@ fun collectAccessors(fragments: List<JsProgramFragment>): Map<String, FunctionWi
return result
}
fun collectNameBindings(fragments: List<JsProgramFragment>): Map<JsName, String> {
fun collectNameBindings(fragments: List<JsProgramFragment>): MutableMap<JsName, String> {
val result = mutableMapOf<JsName, String>()
for (fragment in fragments) {
for (binding in fragment.nameBindings) {