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.translate.expression;
018    
019    import org.jetbrains.annotations.NotNull;
020    import org.jetbrains.annotations.Nullable;
021    import org.jetbrains.kotlin.backend.common.CodegenUtil;
022    import org.jetbrains.kotlin.builtins.KotlinBuiltIns;
023    import org.jetbrains.kotlin.js.backend.ast.*;
024    import org.jetbrains.kotlin.js.translate.context.Namer;
025    import org.jetbrains.kotlin.js.translate.context.TranslationContext;
026    import org.jetbrains.kotlin.js.translate.general.AbstractTranslator;
027    import org.jetbrains.kotlin.js.translate.general.Translation;
028    import org.jetbrains.kotlin.js.translate.operation.InOperationTranslator;
029    import org.jetbrains.kotlin.js.translate.utils.BindingUtils;
030    import org.jetbrains.kotlin.js.translate.utils.JsAstUtils;
031    import org.jetbrains.kotlin.lexer.KtTokens;
032    import org.jetbrains.kotlin.psi.*;
033    import org.jetbrains.kotlin.psi.psiUtil.PsiUtilsKt;
034    import org.jetbrains.kotlin.types.KotlinType;
035    
036    import java.util.HashMap;
037    import java.util.Map;
038    
039    import static org.jetbrains.kotlin.js.translate.utils.JsAstUtils.not;
040    
041    public final class WhenTranslator extends AbstractTranslator {
042        @Nullable
043        public static JsNode translate(@NotNull KtWhenExpression expression, @NotNull TranslationContext context) {
044            return new WhenTranslator(expression, context).translate();
045        }
046    
047        @NotNull
048        private final KtWhenExpression whenExpression;
049    
050        @Nullable
051        private final JsExpression expressionToMatch;
052    
053        private WhenTranslator(@NotNull KtWhenExpression expression, @NotNull TranslationContext context) {
054            super(context);
055    
056            whenExpression = expression;
057    
058            KtExpression subject = expression.getSubjectExpression();
059            expressionToMatch = subject != null ? context.defineTemporary(Translation.translateAsExpression(subject, context)) : null;
060        }
061    
062        private JsNode translate() {
063            JsIf currentIf = null;
064            JsIf resultIf = null;
065            for (KtWhenEntry entry : whenExpression.getEntries()) {
066                JsBlock statementBlock = new JsBlock();
067                JsStatement statement = translateEntryExpression(entry, context(), statementBlock);
068    
069                if (resultIf == null && entry.isElse()) {
070                    context().addStatementsToCurrentBlockFrom(statementBlock);
071                    return statement;
072                }
073                statement = JsAstUtils.mergeStatementInBlockIfNeeded(statement, statementBlock);
074    
075                if (resultIf == null) {
076                    currentIf = JsAstUtils.newJsIf(translateConditions(entry, context()), statement);
077                    resultIf = currentIf;
078                }
079                else {
080                    if (entry.isElse()) {
081                        currentIf.setElseStatement(statement);
082                        return resultIf;
083                    }
084                    JsBlock conditionsBlock = new JsBlock();
085                    JsIf nextIf = JsAstUtils.newJsIf(translateConditions(entry, context().innerBlock(conditionsBlock)), statement);
086                    JsStatement statementToAdd = JsAstUtils.mergeStatementInBlockIfNeeded(nextIf, conditionsBlock);
087                    currentIf.setElseStatement(statementToAdd);
088                    currentIf = nextIf;
089                }
090            }
091    
092            if (currentIf != null && currentIf.getElseStatement() == null && isExhaustive()) {
093                JsExpression noWhenMatchedInvocation = new JsInvocation(JsAstUtils.pureFqn("noWhenBranchMatched", Namer.kotlinObject()));
094                currentIf.setElseStatement(JsAstUtils.asSyntheticStatement(noWhenMatchedInvocation));
095            }
096    
097            return resultIf != null ? resultIf : JsLiteral.NULL;
098        }
099    
100        private boolean isExhaustive() {
101            KotlinType type = bindingContext().getType(whenExpression);
102            boolean isStatement = type != null && KotlinBuiltIns.isUnit(type) && !type.isMarkedNullable();
103            return CodegenUtil.isExhaustive(bindingContext(), whenExpression, isStatement);
104        }
105    
106        @NotNull
107        private static JsStatement translateEntryExpression(
108                @NotNull KtWhenEntry entry,
109                @NotNull TranslationContext context,
110                @NotNull JsBlock block) {
111            KtExpression expressionToExecute = entry.getExpression();
112            assert expressionToExecute != null : "WhenEntry should have whenExpression to execute.";
113            return Translation.translateAsStatement(expressionToExecute, context, block);
114        }
115    
116        @NotNull
117        private JsExpression translateConditions(@NotNull KtWhenEntry entry, @NotNull TranslationContext context) {
118            KtWhenCondition[] conditions = entry.getConditions();
119    
120            assert conditions.length > 0 : "When entry (not else) should have at least one condition";
121    
122            if (conditions.length == 1) {
123                return translateCondition(conditions[0], context);
124            }
125    
126            JsExpression result = translateCondition(conditions[0], context);
127            for (int i = 1; i < conditions.length; i++) {
128                result = translateOrCondition(result, conditions[i], context);
129            }
130    
131            return result;
132        }
133    
134        @NotNull
135        private JsExpression translateOrCondition(
136                @NotNull JsExpression leftExpression,
137                @NotNull KtWhenCondition condition,
138                @NotNull TranslationContext context
139        ) {
140            TranslationContext rightContext = context.innerBlock();
141            JsExpression rightExpression = translateCondition(condition, rightContext);
142            context.moveVarsFrom(rightContext);
143            if (rightContext.currentBlockIsEmpty()) {
144                return new JsBinaryOperation(JsBinaryOperator.OR, leftExpression, rightExpression);
145            } else {
146                assert rightExpression instanceof JsNameRef : "expected JsNameRef, but: " + rightExpression;
147                JsNameRef result = (JsNameRef) rightExpression;
148                JsIf ifStatement = JsAstUtils.newJsIf(leftExpression, JsAstUtils.assignment(result, JsLiteral.TRUE).makeStmt(),
149                                                      rightContext.getCurrentBlock());
150                context.addStatementToCurrentBlock(ifStatement);
151                return result;
152            }
153        }
154    
155        @NotNull
156        private JsExpression translateCondition(@NotNull KtWhenCondition condition, @NotNull TranslationContext context) {
157            JsExpression patternMatchExpression = translateWhenConditionToBooleanExpression(condition, context);
158            if (isNegated(condition)) {
159                return not(patternMatchExpression);
160            }
161            return patternMatchExpression;
162        }
163    
164        @NotNull
165        private JsExpression translateWhenConditionToBooleanExpression(
166                @NotNull KtWhenCondition condition,
167                @NotNull TranslationContext context
168        ) {
169            if (condition instanceof KtWhenConditionIsPattern) {
170                return translateIsCondition((KtWhenConditionIsPattern) condition, context);
171            }
172            else if (condition instanceof KtWhenConditionWithExpression) {
173                return translateExpressionCondition((KtWhenConditionWithExpression) condition, context);
174            }
175            else if (condition instanceof KtWhenConditionInRange) {
176                return translateRangeCondition((KtWhenConditionInRange) condition, context);
177            }
178            throw new AssertionError("Unsupported when condition " + condition.getClass());
179        }
180    
181        @NotNull
182        private JsExpression translateIsCondition(@NotNull KtWhenConditionIsPattern conditionIsPattern, @NotNull TranslationContext context) {
183            JsExpression expressionToMatch = getExpressionToMatch();
184            assert expressionToMatch != null : "An is-check is not allowed in when() without subject.";
185    
186            KtTypeReference typeReference = conditionIsPattern.getTypeReference();
187            assert typeReference != null : "An is-check must have a type reference.";
188    
189            KtExpression expressionToMatchNonTranslated = whenExpression.getSubjectExpression();
190            assert expressionToMatchNonTranslated != null : "expressionToMatch != null => expressionToMatchNonTranslated != null: " +
191                                                            PsiUtilsKt.getTextWithLocation(conditionIsPattern);
192            JsExpression result = Translation.patternTranslator(context).translateIsCheck(expressionToMatch, typeReference);
193            return result != null ? result : JsLiteral.TRUE;
194        }
195    
196        @NotNull
197        private JsExpression translateExpressionCondition(@NotNull KtWhenConditionWithExpression condition, @NotNull TranslationContext context) {
198            KtExpression patternExpression = condition.getExpression();
199            assert patternExpression != null : "Expression pattern should have an expression.";
200    
201            JsExpression expressionToMatch = getExpressionToMatch();
202            if (expressionToMatch == null) {
203                return Translation.patternTranslator(context).translateExpressionForExpressionPattern(patternExpression);
204            }
205            else {
206                KtExpression subject = whenExpression.getSubjectExpression();
207                assert subject != null : "Subject must be non-null since expressionToMatch is non-null: " +
208                                         PsiUtilsKt.getTextWithLocation(condition);
209                KotlinType type = BindingUtils.getTypeForExpression(bindingContext(), whenExpression.getSubjectExpression());
210                return Translation.patternTranslator(context).translateExpressionPattern(type, expressionToMatch, patternExpression);
211            }
212        }
213    
214        @NotNull
215        private JsExpression translateRangeCondition(@NotNull KtWhenConditionInRange condition, @NotNull TranslationContext context) {
216            KtExpression patternExpression = condition.getRangeExpression();
217            assert patternExpression != null : "Expression pattern should have an expression: " +
218                                               PsiUtilsKt.getTextWithLocation(condition);
219    
220            JsExpression expressionToMatch = getExpressionToMatch();
221            assert expressionToMatch != null : "Range pattern is only available for 'when (C) { in ... }'  expressions: " +
222                                               PsiUtilsKt.getTextWithLocation(condition);
223    
224            Map<KtExpression, JsExpression> subjectAliases = new HashMap<KtExpression, JsExpression>();
225            subjectAliases.put(whenExpression.getSubjectExpression(), expressionToMatch);
226            TranslationContext callContext = context.innerContextWithAliasesForExpressions(subjectAliases);
227            boolean negated = condition.getOperationReference().getReferencedNameElementType() == KtTokens.NOT_IN;
228            return new InOperationTranslator(callContext, expressionToMatch, condition.getRangeExpression(), condition.getOperationReference(),
229                                             negated).translate();
230        }
231    
232        @Nullable
233        private JsExpression getExpressionToMatch() {
234            return expressionToMatch;
235        }
236    
237        private static boolean isNegated(@NotNull KtWhenCondition condition) {
238            if (condition instanceof KtWhenConditionIsPattern) {
239                return ((KtWhenConditionIsPattern)condition).isNegated();
240            }
241            return false;
242        }
243    }