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 }