001    /*
002     * Copyright 2010-2015 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 IdentityHashMap<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    
062                return hasToBeInlined(call);
063            }
064        };
065    
066        public static JsProgram process(@NotNull TranslationContext context) {
067            JsProgram program = context.program();
068            IdentityHashMap<JsName, JsFunction> functions = CollectUtilsKt.collectNamedFunctions(program);
069            JsInliner inliner = new JsInliner(functions, new FunctionReader(context), context.bindingTrace());
070            inliner.accept(program);
071            RemoveUnusedFunctionDefinitionsKt.removeUnusedFunctionDefinitions(program, functions);
072            return program;
073        }
074    
075        private JsInliner(
076                @NotNull IdentityHashMap<JsName, JsFunction> functions,
077                @NotNull FunctionReader functionReader,
078                @NotNull DiagnosticSink trace
079        ) {
080            this.functions = functions;
081            this.functionReader = functionReader;
082            this.trace = trace;
083        }
084    
085        @Override
086        public boolean visit(@NotNull JsFunction function, @NotNull JsContext context) {
087            inliningContexts.push(new JsInliningContext(function));
088            assert !inProcessFunctions.contains(function): "Inliner has revisited function";
089            inProcessFunctions.add(function);
090    
091            if (functions.containsValue(function)) {
092                namedFunctionsStack.push(function);
093            }
094    
095            return super.visit(function, context);
096        }
097    
098        @Override
099        public void endVisit(@NotNull JsFunction function, @NotNull JsContext context) {
100            super.endVisit(function, context);
101            NamingUtilsKt.refreshLabelNames(function.getBody(), function.getScope());
102    
103            RemoveUnusedLocalFunctionDeclarationsKt.removeUnusedLocalFunctionDeclarations(function);
104            processedFunctions.add(function);
105    
106            new FunctionPostProcessor(function.getBody()).apply();
107    
108            assert inProcessFunctions.contains(function);
109            inProcessFunctions.remove(function);
110    
111            inliningContexts.pop();
112    
113            if (!namedFunctionsStack.empty() && namedFunctionsStack.peek() == function) {
114                namedFunctionsStack.pop();
115            }
116        }
117    
118        @Override
119        public boolean visit(@NotNull JsInvocation call, @NotNull JsContext context) {
120            if (!hasToBeInlined(call)) return true;
121    
122            JsFunction containingFunction = getCurrentNamedFunction();
123    
124            if (containingFunction != null) {
125                inlineCallInfos.add(new JsCallInfo(call, containingFunction));
126            }
127    
128            JsFunction definition = getFunctionContext().getFunctionDefinition(call);
129    
130            if (inProcessFunctions.contains(definition))  {
131                reportInlineCycle(call, definition);
132            }
133            else if (!processedFunctions.contains(definition)) {
134                accept(definition);
135            }
136    
137            return true;
138        }
139    
140        @Override
141        public void endVisit(@NotNull JsInvocation x, @NotNull JsContext ctx) {
142            if (hasToBeInlined(x)) {
143                inline(x, ctx);
144            }
145    
146            JsCallInfo lastCallInfo = null;
147    
148            if (!inlineCallInfos.isEmpty()) {
149                lastCallInfo = inlineCallInfos.getLast();
150            }
151    
152            if (lastCallInfo != null && lastCallInfo.call == x) {
153                inlineCallInfos.removeLast();
154            }
155        }
156    
157        @Override
158        protected void doAcceptStatementList(List<JsStatement> statements) {
159            // at top level of js ast, contexts stack can be empty,
160            // but there is no inline calls anyway
161            if(!inliningContexts.isEmpty()) {
162                JsScope scope = getFunctionContext().getScope();
163                int i = 0;
164    
165                while (i < statements.size()) {
166                    List<JsStatement> additionalStatements =
167                            ExpressionDecomposer.preserveEvaluationOrder(scope, statements.get(i), canBeExtractedByInliner);
168                    statements.addAll(i, additionalStatements);
169                    i += additionalStatements.size() + 1;
170                }
171            }
172    
173            super.doAcceptStatementList(statements);
174        }
175    
176        private void inline(@NotNull JsInvocation call, @NotNull JsContext context) {
177            JsInliningContext inliningContext = getInliningContext();
178            InlineableResult inlineableResult = getInlineableCallReplacement(call, inliningContext);
179    
180            JsStatement inlineableBody = inlineableResult.getInlineableBody();
181            JsExpression resultExpression = inlineableResult.getResultExpression();
182            JsContext<JsStatement> statementContext = inliningContext.getStatementContext();
183            // body of inline function can contain call to lambdas that need to be inlined
184            JsStatement inlineableBodyWithLambdasInlined = accept(inlineableBody);
185            assert inlineableBody == inlineableBodyWithLambdasInlined;
186            statementContext.addPrevious(flattenStatement(inlineableBody));
187    
188            /**
189             * Assumes, that resultExpression == null, when result is not needed.
190             * @see FunctionInlineMutator.isResultNeeded()
191             */
192            if (resultExpression == null) {
193                statementContext.removeMe();
194                return;
195            }
196    
197            resultExpression = accept(resultExpression);
198            JsStatement currentStatement = statementContext.getCurrentNode();
199    
200            if (currentStatement instanceof JsExpressionStatement &&
201                ((JsExpressionStatement) currentStatement).getExpression() == call &&
202                (resultExpression == null || !SideEffectUtilsKt.canHaveSideEffect(resultExpression))
203            ) {
204                statementContext.removeMe();
205            }
206            else {
207                context.replaceMe(resultExpression);
208            }
209        }
210    
211        @NotNull
212        private JsInliningContext getInliningContext() {
213            return inliningContexts.peek();
214        }
215    
216        @NotNull
217        private FunctionContext getFunctionContext() {
218            return getInliningContext().getFunctionContext();
219        }
220    
221        @Nullable
222        private JsFunction getCurrentNamedFunction() {
223            if (namedFunctionsStack.empty()) return null;
224            return namedFunctionsStack.peek();
225        }
226    
227        private void reportInlineCycle(@NotNull JsInvocation call, @NotNull JsFunction calledFunction) {
228            MetadataProperties.setInlineStrategy(call, InlineStrategy.NOT_INLINE);
229            Iterator<JsCallInfo> it = inlineCallInfos.descendingIterator();
230    
231            while (it.hasNext()) {
232                JsCallInfo callInfo = it.next();
233                PsiElement psiElement = MetadataProperties.getPsiElement(callInfo.call);
234    
235                CallableDescriptor descriptor = MetadataProperties.getDescriptor(callInfo.call);
236                if (psiElement != null && descriptor != null) {
237                    trace.report(Errors.INLINE_CALL_CYCLE.on(psiElement, descriptor));
238                }
239    
240                if (callInfo.containingFunction == calledFunction) {
241                    break;
242                }
243            }
244        }
245    
246        private boolean hasToBeInlined(@NotNull JsInvocation call) {
247            InlineStrategy strategy = MetadataProperties.getInlineStrategy(call);
248            if (strategy == null || !strategy.isInline()) return false;
249    
250            return getFunctionContext().hasFunctionDefinition(call);
251        }
252    
253        private class JsInliningContext implements InliningContext {
254            private final FunctionContext functionContext;
255    
256            JsInliningContext(JsFunction function) {
257                functionContext = new FunctionContext(function, functionReader) {
258                    @Nullable
259                    @Override
260                    protected JsFunction lookUpStaticFunction(@Nullable JsName functionName) {
261                        return functions.get(functionName);
262                    }
263                };
264            }
265    
266            @NotNull
267            @Override
268            public NamingContext newNamingContext() {
269                JsScope scope = getFunctionContext().getScope();
270                return new NamingContext(scope, getStatementContext());
271            }
272    
273            @NotNull
274            @Override
275            public JsContext<JsStatement> getStatementContext() {
276                return getLastStatementLevelContext();
277            }
278    
279            @NotNull
280            @Override
281            public FunctionContext getFunctionContext() {
282                return functionContext;
283            }
284        }
285    
286        private static class JsCallInfo {
287            @NotNull
288            public final JsInvocation call;
289    
290            @NotNull
291            public final JsFunction containingFunction;
292    
293            private JsCallInfo(@NotNull JsInvocation call, @NotNull JsFunction function) {
294                this.call = call;
295                containingFunction = function;
296            }
297        }
298    }