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