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