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 }