| /* |
| * Copyright 2008 Google Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| * use this file except in compliance with the License. You may obtain a copy of |
| * the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| * License for the specific language governing permissions and limitations under |
| * the License. |
| */ |
| package com.google.gwt.dev.jjs.impl; |
| |
| import com.google.gwt.dev.jjs.ast.Context; |
| import com.google.gwt.dev.jjs.ast.JBinaryOperation; |
| import com.google.gwt.dev.jjs.ast.JBinaryOperator; |
| import com.google.gwt.dev.jjs.ast.JClassType; |
| import com.google.gwt.dev.jjs.ast.JExpression; |
| import com.google.gwt.dev.jjs.ast.JMethod; |
| import com.google.gwt.dev.jjs.ast.JMethodCall; |
| import com.google.gwt.dev.jjs.ast.JModVisitor; |
| import com.google.gwt.dev.jjs.ast.JProgram; |
| import com.google.gwt.dev.jjs.ast.JProgram.DispatchType; |
| import com.google.gwt.dev.jjs.ast.JReferenceType; |
| import com.google.gwt.dev.jjs.ast.JType; |
| |
| import java.util.Map; |
| |
| /** |
| * <p> |
| * Rewrite Java <code>==</code> so that it will execute correctly in JavaScript. |
| * After this pass, Java's <code>==</code> is considered equivalent to |
| * JavaScript's <code>===</code>. |
| * </p> |
| * <p> |
| * Whenever possible, a Java <code>==</code> is replaced by a JavaScript |
| * <code>==</code>. This is shorter than <code>===</code>, and it avoids any |
| * complication due to GWT treating both <code>null</code> and |
| * <code>undefined</code> as a valid translation of a Java <code>null</code>. |
| * </p> |
| * <p> |
| * However, whenever something that may be an unboxed type is compared to something |
| * that may not be a unboxed type, use <code>===</code>. A Java object |
| * compared to a string should always yield false, but that's not true when |
| * comparing in JavaScript using <code>==</code>. The cases where |
| * <code>===</code> must be used are: |
| * </p> |
| * <ul> |
| * <li>One or both sides have unknown <i>unboxed type</i> status.</li> |
| * <li>One side is definitely an <i>unboxed type</i> and one side is definitely ! |
| * <i>unboxed type</i>. <br/> |
| * TODO: This case could be optimized as |
| * <code>(a == null) & (b == null)</code>.</li> |
| * </ul> |
| * <p> |
| * Since <code>null !== undefined</code>, it is also necessary to normalize |
| * <code>null</code> vs. <code>undefined</code> if it's possible for one side to |
| * be <code>null</code> and the other to be <code>undefined</code>. |
| * </p> |
| * <p> An "unboxed type" is a String, Boolean, or Double that is represented as a naked raw |
| * JS type. |
| */ |
| public class EqualityNormalizer { |
| |
| /** |
| * Breaks apart certain complex assignments. |
| */ |
| private class BreakupAssignOpsVisitor extends JModVisitor { |
| |
| @Override |
| public void endVisit(JBinaryOperation x, Context ctx) { |
| JBinaryOperator op = x.getOp(); |
| if (op != JBinaryOperator.EQ && op != JBinaryOperator.NEQ) { |
| return; |
| } |
| JExpression lhs = x.getLhs(); |
| JExpression rhs = x.getRhs(); |
| JType lhsType = lhs.getType(); |
| JType rhsType = rhs.getType(); |
| if (!(lhsType instanceof JReferenceType)) { |
| assert !(rhsType instanceof JReferenceType); |
| return; |
| } |
| |
| UnboxedTypeStatus lhsStatus = getUnboxedTypeStatus((JReferenceType) lhsType); |
| UnboxedTypeStatus rhsStatus = getUnboxedTypeStatus((JReferenceType) rhsType); |
| int comparisonStrategy = COMPARISON_TABLE[lhsStatus.getIndex()][rhsStatus.getIndex()]; |
| |
| switch (comparisonStrategy) { |
| case T: { |
| if (canBeNull(lhs) && canBeNull(rhs)) { |
| /* |
| * If it's possible for one side to be null and the other side |
| * undefined, then mask both sides. |
| */ |
| lhs = maskUndefined(lhs); |
| rhs = maskUndefined(rhs); |
| } |
| |
| JBinaryOperation binOp = |
| new JBinaryOperation(x.getSourceInfo(), x.getType(), x.getOp(), lhs, rhs); |
| ctx.replaceMe(binOp); |
| break; |
| } |
| |
| case D: { |
| boolean lhsNullLit = lhs == program.getLiteralNull(); |
| boolean rhsNullLit = rhs == program.getLiteralNull(); |
| if ((lhsNullLit && rhsStatus == UnboxedTypeStatus.NOT_UNBOXEDTYPE) |
| || (rhsNullLit && lhsStatus == UnboxedTypeStatus.NOT_UNBOXEDTYPE)) { |
| /* |
| * If either side is a null literal and the other is non-String, |
| * replace with a null-check. |
| */ |
| String methodName; |
| if (op == JBinaryOperator.EQ) { |
| methodName = "Cast.isNull"; |
| } else { |
| methodName = "Cast.isNotNull"; |
| } |
| JMethod isNullMethod = program.getIndexedMethod(methodName); |
| JMethodCall call = new JMethodCall(x.getSourceInfo(), null, isNullMethod); |
| call.addArg(lhsNullLit ? rhs : lhs); |
| ctx.replaceMe(call); |
| } else { |
| // Replace with a call to Cast.jsEquals, which does a == internally. |
| String methodName; |
| if (op == JBinaryOperator.EQ) { |
| methodName = "Cast.jsEquals"; |
| } else { |
| methodName = "Cast.jsNotEquals"; |
| } |
| JMethod eqMethod = program.getIndexedMethod(methodName); |
| JMethodCall call = new JMethodCall(x.getSourceInfo(), null, eqMethod); |
| call.addArgs(lhs, rhs); |
| ctx.replaceMe(call); |
| } |
| break; |
| } |
| } |
| } |
| |
| private UnboxedTypeStatus getUnboxedTypeStatus(JReferenceType type) { |
| if (type.isNullType()) { |
| return UnboxedTypeStatus.NULL; |
| } else { |
| for (Map.Entry<JClassType, DispatchType> nativeDispatchType : |
| program.getRepresentedAsNativeTypesDispatchMap().entrySet()) { |
| if (type.getUnderlyingType() == nativeDispatchType.getKey()) { |
| switch (nativeDispatchType.getValue()) { |
| case DOUBLE: |
| return UnboxedTypeStatus.DOUBLE; |
| case BOOLEAN: |
| return UnboxedTypeStatus.BOOLEAN; |
| case STRING: |
| return UnboxedTypeStatus.STRING; |
| default: |
| throw new AssertionError("Shouldn't happen"); |
| } |
| } else if (!program.typeOracle.castFailsTrivially(type, nativeDispatchType.getKey())) { |
| return UnboxedTypeStatus.UNKNOWN; |
| } |
| } |
| return UnboxedTypeStatus.NOT_UNBOXEDTYPE; |
| } |
| } |
| |
| private JExpression maskUndefined(JExpression lhs) { |
| assert lhs.getType().canBeNull(); |
| |
| JMethod maskMethod = program.getIndexedMethod("Cast.maskUndefined"); |
| JMethodCall lhsCall = new JMethodCall(lhs.getSourceInfo(), null, maskMethod, lhs); |
| lhsCall.overrideReturnType(lhs.getType()); |
| return lhsCall; |
| } |
| } |
| |
| /** |
| * Represents what we know about an operand type in terms of its type and |
| * <code>null</code> status. |
| */ |
| private enum UnboxedTypeStatus { |
| NOT_UNBOXEDTYPE(4), NULL(5), DOUBLE(3), BOOLEAN(2) ,STRING(1), UNKNOWN(0); |
| |
| private int index; |
| |
| UnboxedTypeStatus(int index) { |
| this.index = index; |
| } |
| |
| public int getIndex() { |
| return index; |
| } |
| } |
| |
| /** |
| * The comparison strategy of using ==. |
| * Mnemonic: D = double eq |
| */ |
| private static final int D = 0; |
| |
| /** |
| * The comparison strategy of using ===. |
| * Mnemonic: T = triple eq |
| */ |
| private static final int T = 1; |
| |
| /** |
| * A map of the combinations where each comparison strategy should be used. |
| */ |
| private static int[][] COMPARISON_TABLE = { |
| // any type compared to unknown uses triple eq |
| {T, T, T, T, T, D,}, // UNKNOWN |
| // string type uses D only against String and null |
| {T, D, T, T, T, D,}, // STRING |
| // double type uses D only against Double and null |
| {T, T, D, T, T, D,}, // DOUBLE |
| // boolean type uses D only against Boolean and null |
| {T, T, T, D, T, D,}, // BOOLEAN |
| // non-unboxed type uses D only against other non-unboxed types and null |
| {T, T, T, T, D, D,}, // NOT_UNBOXEDTYPE |
| // null vs null is safe everywhere |
| {D, D, D, D, D, D,}, // NULL |
| }; |
| |
| public static void exec(JProgram program) { |
| new EqualityNormalizer(program).execImpl(); |
| } |
| |
| private static boolean canBeNull(JExpression x) { |
| return x.getType().canBeNull(); |
| } |
| |
| private final JProgram program; |
| |
| private EqualityNormalizer(JProgram program) { |
| this.program = program; |
| } |
| |
| private void execImpl() { |
| BreakupAssignOpsVisitor breaker = new BreakupAssignOpsVisitor(); |
| breaker.accept(program); |
| } |
| |
| } |