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 }