All Downloads are FREE. Search and download functionalities are using the official Maven repository.

.kotlin.kotlin-compiler.1.3.11.source-code.JsInliner Maven / Gradle / Ivy

There is a newer version: 2.0.20
Show newest version
/*
 * 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.*;
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<>();

    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);
        }

        for (JsProgramFragment fragment : fragmentsToProcess) {
            inliner.existingImports.clear();
            inliner.additionalNameBindings.clear();
            inliner.existingNameBindings = CollectUtilsKt.collectNameBindings(Collections.singletonList(fragment));

            inliner.acceptStatement(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(), "");
            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);
        }

        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 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)) {
            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 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 static 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 context) {
                JsName alias = MetadataProperties.getLocalAlias(expression);
                if (alias != null) {
                    context.replaceMe(alias.makeRef());
                }
            }

        }.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);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy