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 }