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.MetadataPackage; 021 import org.jetbrains.annotations.NotNull; 022 import org.jetbrains.annotations.Nullable; 023 import org.jetbrains.kotlin.builtins.InlineStrategy; 024 import org.jetbrains.kotlin.js.inline.context.*; 025 import org.jetbrains.kotlin.js.inline.exception.InlineRecursionException; 026 import org.jetbrains.kotlin.js.translate.context.TranslationContext; 027 028 import java.util.IdentityHashMap; 029 import java.util.Set; 030 import java.util.Stack; 031 032 import static org.jetbrains.kotlin.js.inline.FunctionInlineMutator.getInlineableCallReplacement; 033 import static org.jetbrains.kotlin.js.inline.clean.CleanPackage.removeUnusedFunctionDefinitions; 034 import static org.jetbrains.kotlin.js.inline.clean.CleanPackage.removeUnusedLocalFunctionDeclarations; 035 import static org.jetbrains.kotlin.js.inline.util.UtilPackage.*; 036 import static org.jetbrains.kotlin.js.translate.utils.JsAstUtils.flattenStatement; 037 038 public class JsInliner extends JsVisitorWithContextImpl { 039 040 private final IdentityHashMap<JsName, JsFunction> functions; 041 private final Stack<JsInliningContext> inliningContexts = new Stack<JsInliningContext>(); 042 private final Set<JsFunction> processedFunctions = IdentitySet(); 043 private final Set<JsFunction> inProcessFunctions = IdentitySet(); 044 private final FunctionReader functionReader; 045 046 /** 047 * A statement can contain more, than one inlineable sub-expressions. 048 * When inline call is expanded, current statement is shifted forward, 049 * but still has same statement context with same index on stack. 050 * 051 * The shifting is intentional, because there could be function literals, 052 * that need to be inlined, after expansion. 053 * 054 * After shifting following inline expansion in the same statement could be 055 * incorrect, because wrong statement index is used. 056 * 057 * To prevent this, after every shift this flag is set to true, 058 * so that visitor wont go deeper until statement is visited. 059 * 060 * Example: 061 * inline fun f(g: () -> Int): Int { val a = g(); return a } 062 * inline fun Int.abs(): Int = if (this < 0) -this else this 063 * 064 * val g = { 10 } 065 * >> val h = f(g).abs() // last statement context index 066 * 067 * val g = { 10 } // after inline 068 * >> val f$result // statement index was not changed 069 * val a = g() 070 * f$result = a 071 * val h = f$result.abs() // current expression still here; incorrect to inline abs(), 072 * // because statement context on stack point to different statement 073 */ 074 private boolean lastStatementWasShifted = false; 075 076 public static JsProgram process(@NotNull TranslationContext context) { 077 JsProgram program = context.program(); 078 IdentityHashMap<JsName, JsFunction> functions = collectNamedFunctions(program); 079 JsInliner inliner = new JsInliner(functions, new FunctionReader(context)); 080 inliner.accept(program); 081 removeUnusedFunctionDefinitions(program, functions); 082 return program; 083 } 084 085 private JsInliner( 086 @NotNull IdentityHashMap<JsName, JsFunction> functions, 087 @NotNull FunctionReader functionReader 088 ) { 089 this.functions = functions; 090 this.functionReader = functionReader; 091 } 092 093 @Override 094 public boolean visit(JsFunction function, JsContext context) { 095 inliningContexts.push(new JsInliningContext(function)); 096 097 if (inProcessFunctions.contains(function)) throw new InlineRecursionException(); 098 inProcessFunctions.add(function); 099 100 return super.visit(function, context); 101 } 102 103 @Override 104 public void endVisit(JsFunction function, JsContext context) { 105 super.endVisit(function, context); 106 refreshLabelNames(getInliningContext().newNamingContext(), function); 107 108 removeUnusedLocalFunctionDeclarations(function); 109 processedFunctions.add(function); 110 111 assert inProcessFunctions.contains(function); 112 inProcessFunctions.remove(function); 113 114 inliningContexts.pop(); 115 } 116 117 @Override 118 public boolean visit(JsInvocation call, JsContext context) { 119 if (call == null) { 120 return false; 121 } 122 123 if (shouldInline(call) && canInline(call)) { 124 JsFunction definition = getFunctionContext().getFunctionDefinition(call); 125 if (!processedFunctions.contains(definition)) { 126 accept(definition); 127 } 128 129 inline(call, context); 130 } 131 132 return !lastStatementWasShifted; 133 } 134 135 private void inline(@NotNull JsInvocation call, @NotNull JsContext context) { 136 JsInliningContext inliningContext = getInliningContext(); 137 FunctionContext functionContext = getFunctionContext(); 138 functionContext.declareFunctionConstructorCalls(call.getArguments()); 139 InlineableResult inlineableResult = getInlineableCallReplacement(call, inliningContext); 140 141 JsStatement inlineableBody = inlineableResult.getInlineableBody(); 142 JsExpression resultExpression = inlineableResult.getResultExpression(); 143 StatementContext statementContext = inliningContext.getStatementContext(); 144 145 /** 146 * Assumes, that resultExpression == null, when result is not needed. 147 * @see FunctionInlineMutator.isResultNeeded() 148 */ 149 if (resultExpression == null) { 150 statementContext.removeCurrentStatement(); 151 } else { 152 context.replaceMe(resultExpression); 153 } 154 155 /** @see #lastStatementWasShifted */ 156 statementContext.shiftCurrentStatementForward(); 157 InsertionPoint<JsStatement> insertionPoint = statementContext.getInsertionPoint(); 158 insertionPoint.insertAllAfter(flattenStatement(inlineableBody)); 159 } 160 161 /** 162 * Prevents JsInliner from traversing sub-expressions, 163 * when current statement was shifted forward. 164 */ 165 @Override 166 protected <T extends JsNode> void doTraverse(T node, JsContext ctx) { 167 if (node instanceof JsStatement) { 168 /** @see #lastStatementWasShifted */ 169 lastStatementWasShifted = false; 170 } 171 172 if (!lastStatementWasShifted) { 173 super.doTraverse(node, ctx); 174 } 175 } 176 177 @NotNull 178 private JsInliningContext getInliningContext() { 179 return inliningContexts.peek(); 180 } 181 182 @NotNull FunctionContext getFunctionContext() { 183 return getInliningContext().getFunctionContext(); 184 } 185 186 private boolean canInline(@NotNull JsInvocation call) { 187 FunctionContext functionContext = getFunctionContext(); 188 return functionContext.hasFunctionDefinition(call); 189 } 190 191 private static boolean shouldInline(@NotNull JsInvocation call) { 192 InlineStrategy strategy = MetadataPackage.getInlineStrategy(call); 193 return strategy != null && strategy.isInline(); 194 } 195 196 197 private class JsInliningContext implements InliningContext { 198 private final FunctionContext functionContext; 199 200 JsInliningContext(JsFunction function) { 201 functionContext = new FunctionContext(function, this, functionReader) { 202 @Nullable 203 @Override 204 protected JsFunction lookUpStaticFunction(@Nullable JsName functionName) { 205 return functions.get(functionName); 206 } 207 }; 208 } 209 210 @NotNull 211 @Override 212 public NamingContext newNamingContext() { 213 JsScope scope = getFunctionContext().getScope(); 214 InsertionPoint<JsStatement> insertionPoint = getStatementContext().getInsertionPoint(); 215 return new NamingContext(scope, insertionPoint); 216 } 217 218 @NotNull 219 @Override 220 public StatementContext getStatementContext() { 221 return new StatementContext() { 222 @NotNull 223 @Override 224 public JsContext getCurrentStatementContext() { 225 return getLastStatementLevelContext(); 226 } 227 228 @NotNull 229 @Override 230 protected JsStatement getEmptyStatement() { 231 return getFunctionContext().getEmpty(); 232 } 233 234 @Override 235 public void shiftCurrentStatementForward() { 236 super.shiftCurrentStatementForward(); 237 lastStatementWasShifted = true; 238 } 239 }; 240 } 241 242 @NotNull 243 @Override 244 public FunctionContext getFunctionContext() { 245 return functionContext; 246 } 247 } 248 }