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