001    /*
002     * Copyright 2010-2016 JetBrains s.r.o.
003     *
004     * Licensed under the Apache License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     * http://www.apache.org/licenses/LICENSE-2.0
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    
017    package org.jetbrains.kotlin.js.inline;
018    
019    import com.google.dart.compiler.backend.js.ast.*;
020    import com.google.dart.compiler.backend.js.ast.metadata.MetadataProperties;
021    import com.intellij.psi.PsiElement;
022    import kotlin.jvm.functions.Function1;
023    import org.jetbrains.annotations.NotNull;
024    import org.jetbrains.annotations.Nullable;
025    import org.jetbrains.kotlin.descriptors.CallableDescriptor;
026    import org.jetbrains.kotlin.diagnostics.DiagnosticSink;
027    import org.jetbrains.kotlin.diagnostics.Errors;
028    import org.jetbrains.kotlin.js.inline.clean.FunctionPostProcessor;
029    import org.jetbrains.kotlin.js.inline.clean.RemoveUnusedFunctionDefinitionsKt;
030    import org.jetbrains.kotlin.js.inline.clean.RemoveUnusedLocalFunctionDeclarationsKt;
031    import org.jetbrains.kotlin.js.inline.context.FunctionContext;
032    import org.jetbrains.kotlin.js.inline.context.InliningContext;
033    import org.jetbrains.kotlin.js.inline.context.NamingContext;
034    import org.jetbrains.kotlin.js.inline.util.*;
035    import org.jetbrains.kotlin.js.translate.context.TranslationContext;
036    import org.jetbrains.kotlin.resolve.inline.InlineStrategy;
037    
038    import java.util.*;
039    
040    import static org.jetbrains.kotlin.js.inline.FunctionInlineMutator.getInlineableCallReplacement;
041    import static org.jetbrains.kotlin.js.translate.utils.JsAstUtils.flattenStatement;
042    
043    public class JsInliner extends JsVisitorWithContextImpl {
044    
045        private final Map<JsName, JsFunction> functions;
046        private final Stack<JsInliningContext> inliningContexts = new Stack<JsInliningContext>();
047        private final Set<JsFunction> processedFunctions = CollectionUtilsKt.IdentitySet();
048        private final Set<JsFunction> inProcessFunctions = CollectionUtilsKt.IdentitySet();
049        private final FunctionReader functionReader;
050        private final DiagnosticSink trace;
051    
052        // these are needed for error reporting, when inliner detects cycle
053        private final Stack<JsFunction> namedFunctionsStack = new Stack<JsFunction>();
054        private final LinkedList<JsCallInfo> inlineCallInfos = new LinkedList<JsCallInfo>();
055        private final Function1<JsNode, Boolean> canBeExtractedByInliner = new Function1<JsNode, Boolean>() {
056            @Override
057            public Boolean invoke(JsNode node) {
058                if (!(node instanceof JsInvocation)) return false;
059    
060                JsInvocation call = (JsInvocation) node;
061                return hasToBeInlined(call);
062            }
063        };
064    
065        public static JsProgram process(@NotNull TranslationContext context) {
066            JsProgram program = context.program();
067            IdentityHashMap<JsName, JsFunction> functions = CollectUtilsKt.collectNamedFunctions(program);
068            JsInliner inliner = new JsInliner(functions, new FunctionReader(context), context.bindingTrace());
069            inliner.accept(program);
070            RemoveUnusedFunctionDefinitionsKt.removeUnusedFunctionDefinitions(program, functions);
071            return program;
072        }
073    
074        private JsInliner(
075                @NotNull Map<JsName, JsFunction> functions,
076                @NotNull FunctionReader functionReader,
077                @NotNull DiagnosticSink trace
078        ) {
079            this.functions = functions;
080            this.functionReader = functionReader;
081            this.trace = trace;
082        }
083    
084        @Override
085        public boolean visit(@NotNull JsFunction function, @NotNull JsContext context) {
086            inliningContexts.push(new JsInliningContext(function));
087            assert !inProcessFunctions.contains(function): "Inliner has revisited function";
088            inProcessFunctions.add(function);
089    
090            if (functions.containsValue(function)) {
091                namedFunctionsStack.push(function);
092            }
093    
094            return super.visit(function, context);
095        }
096    
097        @Override
098        public void endVisit(@NotNull JsFunction function, @NotNull JsContext context) {
099            super.endVisit(function, context);
100            NamingUtilsKt.refreshLabelNames(function.getBody(), function.getScope());
101    
102            RemoveUnusedLocalFunctionDeclarationsKt.removeUnusedLocalFunctionDeclarations(function);
103            processedFunctions.add(function);
104    
105            new FunctionPostProcessor(function).apply();
106    
107            assert inProcessFunctions.contains(function);
108            inProcessFunctions.remove(function);
109    
110            inliningContexts.pop();
111    
112            if (!namedFunctionsStack.empty() && namedFunctionsStack.peek() == function) {
113                namedFunctionsStack.pop();
114            }
115        }
116    
117        @Override
118        public boolean visit(@NotNull JsInvocation call, @NotNull JsContext context) {
119            if (!hasToBeInlined(call)) return true;
120    
121            JsFunction containingFunction = getCurrentNamedFunction();
122    
123            if (containingFunction != null) {
124                inlineCallInfos.add(new JsCallInfo(call, containingFunction));
125            }
126    
127            JsFunction definition = getFunctionContext().getFunctionDefinition(call);
128    
129            if (inProcessFunctions.contains(definition))  {
130                reportInlineCycle(call, definition);
131            }
132            else if (!processedFunctions.contains(definition)) {
133                accept(definition);
134            }
135    
136            return true;
137        }
138    
139        @Override
140        public void endVisit(@NotNull JsInvocation x, @NotNull JsContext ctx) {
141            if (hasToBeInlined(x)) {
142                inline(x, ctx);
143            }
144    
145            JsCallInfo lastCallInfo = null;
146    
147            if (!inlineCallInfos.isEmpty()) {
148                lastCallInfo = inlineCallInfos.getLast();
149            }
150    
151            if (lastCallInfo != null && lastCallInfo.call == x) {
152                inlineCallInfos.removeLast();
153            }
154        }
155    
156        @Override
157        protected void doAcceptStatementList(List<JsStatement> statements) {
158            // at top level of js ast, contexts stack can be empty,
159            // but there is no inline calls anyway
160            if(!inliningContexts.isEmpty()) {
161                JsScope scope = getFunctionContext().getScope();
162                int i = 0;
163    
164                while (i < statements.size()) {
165                    List<JsStatement> additionalStatements =
166                            ExpressionDecomposer.preserveEvaluationOrder(scope, statements.get(i), canBeExtractedByInliner);
167                    statements.addAll(i, additionalStatements);
168                    i += additionalStatements.size() + 1;
169                }
170            }
171    
172            super.doAcceptStatementList(statements);
173        }
174    
175        private void inline(@NotNull JsInvocation call, @NotNull JsContext context) {
176            JsInliningContext inliningContext = getInliningContext();
177            InlineableResult inlineableResult = getInlineableCallReplacement(call, inliningContext);
178    
179            JsStatement inlineableBody = inlineableResult.getInlineableBody();
180            JsExpression resultExpression = inlineableResult.getResultExpression();
181            JsContext<JsStatement> statementContext = inliningContext.getStatementContext();
182            // body of inline function can contain call to lambdas that need to be inlined
183            JsStatement inlineableBodyWithLambdasInlined = accept(inlineableBody);
184            assert inlineableBody == inlineableBodyWithLambdasInlined;
185            statementContext.addPrevious(flattenStatement(inlineableBody));
186    
187            /**
188             * Assumes, that resultExpression == null, when result is not needed.
189             * @see FunctionInlineMutator.isResultNeeded()
190             */
191            if (resultExpression == null) {
192                statementContext.removeMe();
193                return;
194            }
195    
196            context.replaceMe(accept(resultExpression));
197        }
198    
199        @NotNull
200        private JsInliningContext getInliningContext() {
201            return inliningContexts.peek();
202        }
203    
204        @NotNull
205        private FunctionContext getFunctionContext() {
206            return getInliningContext().getFunctionContext();
207        }
208    
209        @Nullable
210        private JsFunction getCurrentNamedFunction() {
211            if (namedFunctionsStack.empty()) return null;
212            return namedFunctionsStack.peek();
213        }
214    
215        private void reportInlineCycle(@NotNull JsInvocation call, @NotNull JsFunction calledFunction) {
216            MetadataProperties.setInlineStrategy(call, InlineStrategy.NOT_INLINE);
217            Iterator<JsCallInfo> it = inlineCallInfos.descendingIterator();
218    
219            while (it.hasNext()) {
220                JsCallInfo callInfo = it.next();
221                PsiElement psiElement = MetadataProperties.getPsiElement(callInfo.call);
222    
223                CallableDescriptor descriptor = MetadataProperties.getDescriptor(callInfo.call);
224                if (psiElement != null && descriptor != null) {
225                    trace.report(Errors.INLINE_CALL_CYCLE.on(psiElement, descriptor));
226                }
227    
228                if (callInfo.containingFunction == calledFunction) {
229                    break;
230                }
231            }
232        }
233    
234        private boolean hasToBeInlined(@NotNull JsInvocation call) {
235            InlineStrategy strategy = MetadataProperties.getInlineStrategy(call);
236            if (strategy == null || !strategy.isInline()) return false;
237    
238            return getFunctionContext().hasFunctionDefinition(call);
239        }
240    
241        private class JsInliningContext implements InliningContext {
242            private final FunctionContext functionContext;
243    
244            JsInliningContext(JsFunction function) {
245                functionContext = new FunctionContext(function, functionReader) {
246                    @Nullable
247                    @Override
248                    protected JsFunction lookUpStaticFunction(@Nullable JsName functionName) {
249                        return functions.get(functionName);
250                    }
251                };
252            }
253    
254            @NotNull
255            @Override
256            public NamingContext newNamingContext() {
257                JsScope scope = getFunctionContext().getScope();
258                return new NamingContext(scope, getStatementContext());
259            }
260    
261            @NotNull
262            @Override
263            public JsContext<JsStatement> getStatementContext() {
264                return getLastStatementLevelContext();
265            }
266    
267            @NotNull
268            @Override
269            public FunctionContext getFunctionContext() {
270                return functionContext;
271            }
272        }
273    
274        private static class JsCallInfo {
275            @NotNull
276            public final JsInvocation call;
277    
278            @NotNull
279            public final JsFunction containingFunction;
280    
281            private JsCallInfo(@NotNull JsInvocation call, @NotNull JsFunction function) {
282                this.call = call;
283                containingFunction = function;
284            }
285        }
286    }