/* * 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.JReferenceType; import com.google.gwt.dev.jjs.ast.JType; /** * <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 a String is compared to something * that may not be a <code>String</code>, 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 <code>String</code> status.</li> * <li>One side is definitely <code>String</code> and one side is definitely ! * <code>String</code>. <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> */ 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; } StringStatus lhsStatus = getStringStatus((JReferenceType) lhsType); StringStatus rhsStatus = getStringStatus((JReferenceType) rhsType); int strat = COMPARISON_STRAT[lhsStatus.getIndex()][rhsStatus.getIndex()]; switch (strat) { case STRAT_TRIPLE: { 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 STRAT_DOUBLE: { boolean lhsNullLit = lhs == program.getLiteralNull(); boolean rhsNullLit = rhs == program.getLiteralNull(); if ((lhsNullLit && rhsStatus == StringStatus.NOTSTRING) || (rhsNullLit && lhsStatus == StringStatus.NOTSTRING)) { /* * 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 StringStatus getStringStatus(JReferenceType type) { JClassType stringType = program.getTypeJavaLangString(); if (type == program.getTypeNull()) { return StringStatus.NULL; } else if (program.typeOracle.canTriviallyCast(type, stringType)) { return StringStatus.STRING; } else if (program.typeOracle.canTheoreticallyCast(type, stringType)) { return StringStatus.UNKNOWN; } else { return StringStatus.NOTSTRING; } } private JExpression maskUndefined(JExpression lhs) { assert ((JReferenceType) lhs.getType()).canBeNull(); JMethod maskMethod = program.getIndexedMethod("Cast.maskUndefined"); JMethodCall lhsCall = new JMethodCall(lhs.getSourceInfo(), null, maskMethod, lhs.getType()); lhsCall.addArg(lhs); return lhsCall; } } /** * Represents what we know about an operand type in terms of its type and * <code>null</code> status. */ private enum StringStatus { NOTSTRING(2), NULL(3), STRING(1), UNKNOWN(0); private int index; StringStatus(int index) { this.index = index; } public int getIndex() { return index; } } /** * A map of the combinations where each comparison strategy should be used. */ private static int[][] COMPARISON_STRAT = { // ..U..S.!S..N {1, 1, 1, 0,}, // UNKNOWN {1, 0, 1, 0,}, // STRING {1, 1, 0, 0,}, // NOTSTRING {0, 0, 0, 0,}, // NULL }; /** * The comparison strategy of using ==. */ private static final int STRAT_DOUBLE = 0; /** * The comparison strategy of using ===. */ private static final int STRAT_TRIPLE = 1; public static void exec(JProgram program) { new EqualityNormalizer(program).execImpl(); } private static boolean canBeNull(JExpression x) { return ((JReferenceType) x.getType()).canBeNull(); } private final JProgram program; private EqualityNormalizer(JProgram program) { this.program = program; } private void execImpl() { BreakupAssignOpsVisitor breaker = new BreakupAssignOpsVisitor(); breaker.accept(program); } }