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    }