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