/*
 * 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.ast;

import com.google.gwt.dev.MinimalRebuildCache;
import com.google.gwt.dev.common.InliningMode;
import com.google.gwt.dev.jjs.Correlation.Literal;
import com.google.gwt.dev.jjs.InternalCompilerException;
import com.google.gwt.dev.jjs.SourceInfo;
import com.google.gwt.dev.jjs.SourceOrigin;
import com.google.gwt.dev.jjs.impl.GwtAstBuilder;
import com.google.gwt.dev.jjs.impl.JjsUtils;
import com.google.gwt.dev.jjs.impl.TypeCategory;
import com.google.gwt.dev.jjs.impl.codesplitter.FragmentPartitioningResult;
import com.google.gwt.dev.js.CoverageInstrumentor;
import com.google.gwt.dev.util.StringInterner;
import com.google.gwt.dev.util.log.perf.AbstractJfrEvent;
import com.google.gwt.thirdparty.guava.common.base.CaseFormat;
import com.google.gwt.thirdparty.guava.common.base.Function;
import com.google.gwt.thirdparty.guava.common.base.Predicate;
import com.google.gwt.thirdparty.guava.common.collect.BiMap;
import com.google.gwt.thirdparty.guava.common.collect.Collections2;
import com.google.gwt.thirdparty.guava.common.collect.HashBiMap;
import com.google.gwt.thirdparty.guava.common.collect.ImmutableList;
import com.google.gwt.thirdparty.guava.common.collect.ImmutableMap;
import com.google.gwt.thirdparty.guava.common.collect.ImmutableSet;
import com.google.gwt.thirdparty.guava.common.collect.Iterables;
import com.google.gwt.thirdparty.guava.common.collect.Lists;
import com.google.gwt.thirdparty.guava.common.collect.Maps;
import com.google.gwt.thirdparty.guava.common.collect.Sets;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import jdk.jfr.Label;
import jdk.jfr.Name;

/**
 * Root for the AST representing an entire Java program.
 */
public class JProgram extends JNode implements ArrayTypeCreator {

  /**
   * Encapsulates all information necessary to deal with native represented types in an
   * generic fashion used throughout GWT. This can be extended later to deal with say, unboxed
   * Integer if desired.
   */
  public enum DispatchType {
    // These this list can be extended by creating the appropriate fields/methods on Cast,
    // as well as extending the TypeCategory enum and updating EqualityNormalizer.
    // The order in which these native types appear is the inverse as the way they are
    // checked by devirtualized method.
    BOOLEAN(true),
    DOUBLE(true),
    STRING(true),

    // non-native represented type values.
    HAS_JAVA_VIRTUAL_DISPATCH(false), JAVA_ARRAY(false), JSO(false);

    private final String castMapField;
    private final TypeCategory typeCategory;
    private final String className;

    DispatchType(boolean nativeType) {
      if (nativeType) {
        // These field are initialized to methods that are by-convention
        // The conventions are:
        // Cast.[boxedTypeName]CastMap for cast map fields
        // TypedCategory.TYPE_JAVA_LANG_[BoxedTypeName]
        String methodName = CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, name());
        this.castMapField = "Cast." + methodName + "CastMap";
        this.typeCategory = TypeCategory.valueOf("TYPE_JAVA_LANG_" + name());
        String simpleClassName = CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, name());
        this.className = "java.lang." + simpleClassName;
      } else {
        this.castMapField = null;
        this.typeCategory = null;
        this.className = null;
      }
    }

    public String getCastMapField() {
      return castMapField;
    }

    public TypeCategory getTypeCategory() {
      return typeCategory;
    }

    public String getClassName() {
      return className;
    }
  }

  @Name("gwt.compiler.java.Count")
  @Label("Count Java nodes")
  public static class JavaCountEvent extends AbstractJfrEvent {
    @Label("Number of Java nodes")
    public long nodeCount;
  }

  private static final class TreeStatistics extends JVisitor {
    private int nodeCount = 0;

    public int getNodeCount() {
      return nodeCount;
    }

    @Override
    public boolean visit(JNode x, Context ctx) {
      nodeCount++;
      return true;
    }
  }

  public static final Set<String> CODEGEN_TYPES_SET = Sets.newLinkedHashSet(Arrays.asList(
      "com.google.gwt.lang.Array", "com.google.gwt.lang.Cast", "com.google.gwt.lang.Exceptions",
      "com.google.gwt.lang.LongLib", "com.google.gwt.lang.Stats", "com.google.gwt.lang.Util",
      "java.lang.Object"));

  /*
   * Types which are not referenced by any Java code, but are required to exist
   * after Java optimizations have run in order to be used by backend
   * code-generation. These classes and their members, are considered live
   * by ControlFlowAnalysis, at all times. Immortal types always live in the
   * initial fragment and their definitions are hoisted to appear before all
   * other types. Only static methods and fields are allowed, and no clinits
   * are run. Field initializers must be primitives, literals, or one of
   * JSO.createObject() or JSO.createArray().
   *
   * Classes are inserted into the JsAST in the order they appear in the Set.
   */
  public static final Set<String> IMMORTAL_CODEGEN_TYPES_SET = Sets.newLinkedHashSet(Arrays.asList(
      "com.google.gwt.lang.CollapsedPropertyHolder",
      "com.google.gwt.lang.Runtime",
      "com.google.gwt.lang.ModuleUtils"));

  public static final String JAVASCRIPTOBJECT = "com.google.gwt.core.client.JavaScriptObject";

  public static final String CLASS_LITERAL_HOLDER = "com.google.gwt.lang.ClassLiteralHolder";

  /**
   * Types whose entire implementation is synthesized at compile time.
   */
  public static final Set<String> SYNTHETIC_TYPE_NAMES = Sets.newHashSet(CLASS_LITERAL_HOLDER);

  private static final Comparator<JArrayType> ARRAYTYPE_COMPARATOR =
      new Comparator<JArrayType>() {
        @Override
        public int compare(JArrayType o1, JArrayType o2) {
          int comp = o1.getDims() - o2.getDims();
          if (comp != 0) {
            return comp;
          }
          return o1.getName().compareTo(o2.getName());
        }
      };

  private static final Map<String, JPrimitiveType> primitiveTypes = Maps.newHashMap();

  @Deprecated
  private static final Map<String, JPrimitiveType> primitiveTypesDeprecated = Maps.newHashMap();

  static {
    if (CoverageInstrumentor.isCoverageEnabled()) {
      IMMORTAL_CODEGEN_TYPES_SET.add("com.google.gwt.lang.CoverageUtil");
    }
    CODEGEN_TYPES_SET.addAll(IMMORTAL_CODEGEN_TYPES_SET);

    primitiveTypes.put(JPrimitiveType.BOOLEAN.getName(), JPrimitiveType.BOOLEAN);
    primitiveTypes.put(JPrimitiveType.BYTE.getName(), JPrimitiveType.BYTE);
    primitiveTypes.put(JPrimitiveType.CHAR.getName(), JPrimitiveType.CHAR);
    primitiveTypes.put(JPrimitiveType.DOUBLE.getName(), JPrimitiveType.DOUBLE);
    primitiveTypes.put(JPrimitiveType.FLOAT.getName(), JPrimitiveType.FLOAT);
    primitiveTypes.put(JPrimitiveType.INT.getName(), JPrimitiveType.INT);
    primitiveTypes.put(JPrimitiveType.LONG.getName(), JPrimitiveType.LONG);
    primitiveTypes.put(JPrimitiveType.SHORT.getName(), JPrimitiveType.SHORT);
    primitiveTypes.put(JPrimitiveType.VOID.getName(), JPrimitiveType.VOID);

    primitiveTypesDeprecated.put(JPrimitiveType.BOOLEAN.getJsniSignatureName(),
        JPrimitiveType.BOOLEAN);
    primitiveTypesDeprecated.put(JPrimitiveType.BYTE.getJsniSignatureName(), JPrimitiveType.BYTE);
    primitiveTypesDeprecated.put(JPrimitiveType.CHAR.getJsniSignatureName(), JPrimitiveType.CHAR);
    primitiveTypesDeprecated.put(JPrimitiveType.DOUBLE.getJsniSignatureName(),
        JPrimitiveType.DOUBLE);
    primitiveTypesDeprecated.put(JPrimitiveType.FLOAT.getJsniSignatureName(), JPrimitiveType.FLOAT);
    primitiveTypesDeprecated.put(JPrimitiveType.INT.getJsniSignatureName(), JPrimitiveType.INT);
    primitiveTypesDeprecated.put(JPrimitiveType.LONG.getJsniSignatureName(), JPrimitiveType.LONG);
    primitiveTypesDeprecated.put(JPrimitiveType.SHORT.getJsniSignatureName(), JPrimitiveType.SHORT);
    primitiveTypesDeprecated.put(JPrimitiveType.VOID.getJsniSignatureName(), JPrimitiveType.VOID);
  }

  /**
   * Helper to create an assignment, used to initialize fields, etc.
   */
  public static JStatement createAssignmentStmt(SourceInfo info, JExpression lhs,
      JExpression rhs) {
    return createAssignment(info, lhs, rhs).makeStatement();
  }

  public static JBinaryOperation createAssignment(SourceInfo info, JExpression lhs,
      JExpression rhs) {
    return new JBinaryOperation(info, lhs.getType(), JBinaryOperator.ASG, lhs, rhs);
  }

  public static JLocal createLocal(SourceInfo info, String name, JType type, boolean isFinal,
      JMethodBody enclosingMethodBody) {
    assert (name != null);
    assert (type != null);
    assert (enclosingMethodBody != null);
    JLocal x = new JLocal(info, name, type, isFinal);
    enclosingMethodBody.addLocal(x);
    return x;
  }

  public static List<JDeclaredType> deserializeTypes(ObjectInputStream stream) throws IOException,
      ClassNotFoundException {
    @SuppressWarnings("unchecked")
    List<JDeclaredType> types = (List<JDeclaredType>) stream.readObject();
    for (JDeclaredType type : types) {
      type.readMembers(stream);
    }
    for (JDeclaredType type : types) {
      type.readMethodBodies(stream);
    }
    return types;
  }

  public static String getFullName(JMethod method) {
    return method.getEnclosingType().getName() + "." + method.getJsniSignature(false, true);
  }

  public static boolean isClinit(JMethod method) {
    JDeclaredType enclosingType = method.getEnclosingType();

    boolean isClinit = enclosingType != JClassType.NULL_CLASS &&
        method == enclosingType.getClinitMethod();
    assert !isClinit || method.getName().equals(GwtAstBuilder.CLINIT_METHOD_NAME);
    return isClinit;
  }

  public static boolean isInit(JMethod method) {
    JDeclaredType enclosingType = method.getEnclosingType();

    if (method.isStatic()) {
      // Hack, check the name.
      return method.getName().equals(GwtAstBuilder.STATIC_INIT_METHOD_NAME);
    }

    boolean isInit = enclosingType != null && method == enclosingType.getInitMethod();
    assert !isInit || method.getName().equals(GwtAstBuilder.INIT_NAME_METHOD_NAME);
    return isInit;
  }

  public static void serializeTypes(List<JDeclaredType> types, ObjectOutputStream stream)
      throws IOException {
    stream.writeObject(types);
    for (JDeclaredType type : types) {
      type.writeMembers(stream);
    }
    for (JDeclaredType type : types) {
      type.writeMethodBodies(stream);
    }
  }

  public final List<JClassType> codeGenTypes = Lists.newArrayList();

  public final List<JClassType> immortalCodeGenTypes = Lists.newArrayList();

  public final JTypeOracle typeOracle;

  /**
   * Special serialization treatment.
   */
  // TODO(stalcup): make this a set, or take special care to make updates unique when lazily loading
  // in types. At the moment duplicates are accumulating.
  private transient List<JDeclaredType> allTypes = Lists.newArrayList();

  private final Map<JType, JArrayType> arrayTypes = Maps.newHashMap();

  private Map<JReferenceType, JCastMap> castMaps;

  private BiMap<JType, JField> classLiteralFieldsByType;

  private final List<JMethod> entryMethods = Lists.newArrayList();

  private final Map<String, JField> indexedFields = Maps.newHashMap();

  private final Map<String, JMethod> indexedMethods = Maps.newHashMap();

  /**
   * An index of types, from type name to type instance.
   */
  private final Map<String, JDeclaredType> indexedTypes = Maps.newHashMap();

  /**
   * The set of names of types (beyond the basic INDEX_TYPES_SET) whose instance should be indexed
   * when seen.
   */
  private final Set<String> typeNamesToIndex = buildInitialTypeNamesToIndex();

  private final Map<JMethod, JMethod> instanceToStaticMap = Maps.newIdentityHashMap();

  // wrap up .add here, and filter out forced source
  private Set<String> referenceOnlyTypeNames = Sets.newHashSet();

  /**
   * Filled in by ReplaceRunAsync, once the numbers are known.
   */
  private List<JRunAsync> runAsyncs = Lists.newArrayList();

  private LinkedHashSet<JRunAsync> initialAsyncSequence = Sets.newLinkedHashSet();

  private List<Integer> initialFragmentIdSequence = Lists.newArrayList();

  private final Map<JMethod, JMethod> staticToInstanceMap = Maps.newIdentityHashMap();

  private final Map<String, JDeclaredType> typeNameMap = Maps.newHashMap();

  private Map<JField, JType> typesByClassLiteralField;

  private JClassType typeClass;
  private JClassType typeJavaLangObject;
  private JArrayType typeJavaLangObjectArray;
  private JClassType typeSpecialClassLiteralHolder;
  private JClassType typeSpecialJavaScriptObject;

  private JClassType typeString;

  private FragmentPartitioningResult fragmentPartitioningResult;

  private Map<JClassType, DispatchType> dispatchTypeByNativeType;

  /**
   * Add a pinned method.
   */
  public void addPinnedMethod(JMethod method) {
    method.setInliningMode(InliningMode.DO_NOT_INLINE);
    method.disallowDevirtualization();
  }

  public JProgram(MinimalRebuildCache minimalRebuildCache) {
    super(SourceOrigin.UNKNOWN);
    typeOracle = new JTypeOracle(this, minimalRebuildCache);
  }

  public void addEntryMethod(JMethod entryPoint) {
    assert !entryMethods.contains(entryPoint);
    entryMethods.add(entryPoint);
  }

  /**
   * Adds the given type name to the set of type names (beyond the basic INDEX_TYPES_SET) whose
   * instance should be indexed when seen.
   */
  public void addIndexedTypeName(String typeName) {
    typeNamesToIndex.add(typeName);
  }

  public void addReferenceOnlyType(JDeclaredType type) {
    referenceOnlyTypeNames.add(type.getName());
  }

  public void addType(JDeclaredType type) {
    allTypes.add(type);
    String name = type.getName();
    putIntoTypeMap(name, type);

    if (CODEGEN_TYPES_SET.contains(name)) {
      codeGenTypes.add((JClassType) type);
    }

    if (IMMORTAL_CODEGEN_TYPES_SET.contains(name)) {
      // Immortal types by definition won't run clinits.
      type.setClinitTarget(null);
      immortalCodeGenTypes.add((JClassType) type);
    }

    if (!typeNamesToIndex.contains(name)) {
      return;
    }

    indexedTypes.put(type.getShortName(), type);
    for (JMethod method : type.getMethods()) {
      if (!method.isPrivate()) {
        indexedMethods.put(JjsUtils.getIndexedName(method), method);
      }
    }
    for (JField field : type.getFields()) {
      indexedFields.put(JjsUtils.getIndexedName(field), field);
    }
    switch (name) {
      case "java.lang.Object":
        typeJavaLangObject = (JClassType) type;
        typeJavaLangObjectArray = getOrCreateArrayType(type, 1);
        break;
      case "java.lang.String":
        typeString = (JClassType) type;
        break;
      case "java.lang.Class":
        typeClass = (JClassType) type;
        break;
      case JAVASCRIPTOBJECT:
        typeSpecialJavaScriptObject = (JClassType) type;
        break;
      case CLASS_LITERAL_HOLDER:
        typeSpecialClassLiteralHolder = (JClassType) type;
        break;
    }
  }

  public static boolean isRepresentedAsNative(final String className) {
    return Iterables.any(Arrays.asList(DispatchType.values()), new Predicate<DispatchType>() {
      @Override
      public boolean apply(DispatchType dispatchType) {
        return className.equals(dispatchType.getClassName());
      }
    });
  }

  public boolean isRepresentedAsNativeJsPrimitive(JType type) {
    return getRepresentedAsNativeTypes().contains(type);
  }

  public Set<JClassType> getRepresentedAsNativeTypes() {
    return getRepresentedAsNativeTypesDispatchMap().keySet();
  }

  public Map<JClassType, DispatchType> getRepresentedAsNativeTypesDispatchMap() {
     if (dispatchTypeByNativeType == null) {
       ImmutableMap.Builder<JClassType, DispatchType> builder =
           new ImmutableMap.Builder<JClassType, DispatchType>();
       for (DispatchType dispatchType : DispatchType.values()) {
         if (dispatchType.getClassName() == null) {
           continue;
         }
         JClassType classType = (JClassType) getFromTypeMap(dispatchType.getClassName());
         assert classType != null : "Class " + dispatchType.getClassName() + " has not been loaded";
         builder.put(classType, dispatchType);
       }
       dispatchTypeByNativeType = builder.build();
     }
    return dispatchTypeByNativeType;
  }

  public EnumSet<DispatchType> getDispatchType(JReferenceType type) {
    if (!typeOracle.isInstantiatedType(type)) {
      return EnumSet.noneOf(DispatchType.class);
    }

    // Object methods can be dispatched to all four possible classes.
    if (type == getTypeJavaLangObject()) {
      return EnumSet.allOf(DispatchType.class);
    }

    if (type.isArrayType()) {
      // A variable of type Object[] could contain an instance of native JsType[], the latter
      // is treated as a JSO for devirtualization purposes.
      return EnumSet.of(DispatchType.JSO, DispatchType.JAVA_ARRAY);
    }
    EnumSet<DispatchType> dispatchSet = EnumSet.noneOf(DispatchType.class);
    DispatchType dispatchType = getRepresentedAsNativeTypesDispatchMap().get(type);
    if (dispatchType != null) {
      dispatchSet = EnumSet.of(dispatchType);
    } else if (typeOracle.isDualJsoInterface(type) || type.isJsNative()) {
      // If it is an interface implemented both by JSOs and regular Java Objects; native JsTypes
      // are considered JSOs for object method devirtualization.
      dispatchSet = EnumSet.of(DispatchType.HAS_JAVA_VIRTUAL_DISPATCH, DispatchType.JSO);
    } else if (typeOracle.isSingleJsoImpl(type) || type.isJsoType()) {
      // If it is either an interface implemented by JSOs or JavaScriptObject or one of its
      // subclasses.
      dispatchSet = EnumSet.of(DispatchType.JSO);
    }

    for (JDeclaredType potentialNativeDispatchType : getRepresentedAsNativeTypes()) {
      if (potentialNativeDispatchType == type) {
        continue;
      }

      if (typeOracle.isInstantiatedType(potentialNativeDispatchType)
          && typeOracle.isSuperClassOrInterface(potentialNativeDispatchType, type)) {
        dispatchSet.add(getRepresentedAsNativeTypesDispatchMap().get(potentialNativeDispatchType));
        dispatchSet.add(DispatchType.HAS_JAVA_VIRTUAL_DISPATCH);
      }
    }
    return dispatchSet;
  }

  /**
   * Return the greatest lower bound of two types. That is, return the largest
   * type that is a subtype of both inputs. If none exists return {@code thisType}.
   */
  public JReferenceType strengthenType(JReferenceType thisType, JReferenceType thatType) {
    if (thisType == thatType) {
      return thisType;
    }

    if (thisType.isNullType() || thatType.isNullType()) {
      return JReferenceType.NULL_TYPE;
    }

    if (!thisType.canBeNull() || !thatType.canBeNull()) {
      JReferenceType thisTypeNonNull = thisType.strengthenToNonNull();
      JReferenceType thatTypeNonNull = thatType.strengthenToNonNull();
      // .strengthenToNonNull does not guarantee that the resulting type is non null (e.g. JSOs).

      // If either is non-nullable, the result should be non-nullable, unless it is a type that
      // can not be made non-nullable, like a JSO.
      if (thisType != thisTypeNonNull || thatType != thatTypeNonNull) {
        return strengthenType(thisTypeNonNull, thatTypeNonNull);
      }
    }

    if (typeOracle.castSucceedsTrivially(thisType, thatType)) {
      return thisType;
    }

    if (typeOracle.castSucceedsTrivially(thatType, thisType)) {
      return thatType;
    }

    // This types are incompatible; ideally this code should not be reached, but there are two
    // situations where this happens:
    //   1 - unrelated interfaces;
    //   2 - unsafe code.
    // The original type is preserved in this case.
    return thisType;
  }

  /**
   * Return a minimal upper bound of a set of types. That is, a type
   * that is a supertype of all the input types and is as close as possible to the
   * input types.
   * <p>
   * NOTE: Ideally we would like to return the least upper bound but it does not exit as
   * the Java type hierarchy is not really a lattice.
   * <p>
   * Hence, this function depends on the collection order. E.g.
   * <p>
   * {@code
   *                I    O
   *                |\ / \
   *                | A  B
   *                \   /
   *                 \ /
   *                  C
   * }
   * <p>
   * where I is an interface an {O,A,B,C} are classes.
   * <p>
   * generalizeTypes({A,C}) could either be I or O.
   * <p>
   * In particular generalizeTypes({I,A,C}) = I and generalizeTypes({A,C,I}) = O.
   *
   */
  public JReferenceType generalizeTypes(Iterable<JReferenceType> types) {
    Iterator<JReferenceType> it = types.iterator();
    if (!it.hasNext()) {
      return JReferenceType.NULL_TYPE;
    }
    JReferenceType curType = it.next();
    while (it.hasNext()) {
      curType = generalizeTypes(curType, it.next());
      if (curType == typeJavaLangObject) {
        break;
      }
    }
    return curType;
  }

  /**
   * Return the least upper bound of two types. That is, the "smallest" type that
   * is a supertype of both types. In this lattice there the smallest element might no exist, there
   * might be multiple minimal elements neither of which is smaller than the others. E.g.
   * <p>
   * {@code
   *                 I      J
   *                | \    /|
   *                |  \  / |
   *                |   x   |
   *                |  / \  |
   *                | /   \ |
   *                 A     B
   * }
   * <p>
   * where I and J are interfaces, A and B are classes and both A and B implement I and J. In this
   * case both I and J are generalizing the types A and B.
   */
  private JReferenceType generalizeTypes(JReferenceType thisType, JReferenceType thatType) {

    if (!thisType.canBeNull() && !thatType.canBeNull()) {
      // Nullability is an orthogonal property, so remove non_nullability and perform the
      // generalization on the nullable types, and if both were NOT nullable then strengthen the
      // result to NOT nullable.
      //
      // not_nullable(A) v not_nullable(B) = not_nullable(A v B)
      JReferenceType nulllableGeneralizer =
          generalizeTypes(thisType.weakenToNullable(), thatType.weakenToNullable());
      return nulllableGeneralizer.strengthenToNonNull();
    }
    thisType = thisType.weakenToNullable();
    thatType = thatType.weakenToNullable();

    // From here on nullability does not need to be considered.

    // Generalization for exact types is as follows.
    // exact(A) v null = exact(A)
    // A v null = A
    if (thatType.isNullType()) {
      return thisType;
    }

    // null v exact(A) = exact(A)
    // null v A = A
    if (thisType.isNullType()) {
      return thatType;
    }

    // exact(A) v exact(A)  = exact(A)
    // A v A  = A
    if (thisType == thatType) {
      return thisType;
    }

    // exact(A) v exact(B) = A v B
    // A v exact(B) = A v B
    // exact(A) v B = A v B
    // A v B = A v B
    return generalizeUnderlyingTypes(thisType.getUnderlyingType(), thatType.getUnderlyingType());
  }

  private JReferenceType generalizeUnderlyingTypes(
      JReferenceType thisType, JReferenceType thatType) {

    // We should not have any analysis properties from this point forward.
    assert thisType == thisType.getUnderlyingType() && thatType == thatType.getUnderlyingType();

    if (thisType == thatType) {
      return thisType;
    }

    if (thisType instanceof JInterfaceType && thatType instanceof JInterfaceType) {
      return generalizeInterfaces((JInterfaceType) thisType, (JInterfaceType) thatType);
    }

    if (thisType instanceof JArrayType && thatType instanceof JArrayType) {
      return generalizeArrayTypes((JArrayType) thisType, (JArrayType) thatType);
    }

    if (thisType instanceof JClassType && thatType instanceof JClassType) {
      return generalizeClasses((JClassType) thisType, (JClassType) thatType);
    }

    JInterfaceType interfaceType = thisType instanceof JInterfaceType ? (JInterfaceType) thisType :
        (thatType instanceof JInterfaceType ? (JInterfaceType) thatType : null);

    JReferenceType nonInterfaceType = interfaceType == thisType ? thatType : thisType;

    // See if the class or the array is castable to the interface type.
    if (interfaceType != null &&
        typeOracle.castSucceedsTrivially(nonInterfaceType, interfaceType)) {
      return interfaceType;
    }

    // unrelated: the best commonality is Object
    return typeJavaLangObject;
  }

  private JReferenceType generalizeArrayTypes(JArrayType thisArrayType, JArrayType thatArrayType) {
    assert thisArrayType != thatArrayType;

    int thisDims = thisArrayType.getDims();
    int thatDims = thatArrayType.getDims();

    int minDims = Math.min(thisDims, thatDims);
      /*
       * At a bare minimum, any two arrays generalize to an Object array with
       * one less dim than the lesser of the two; that is, int[][][][] and
       * String[][][] generalize to Object[][]. If minDims is 1, then they
       * just generalize to Object.
       */
    JReferenceType minimalGeneralType = (minDims == 1) ? typeJavaLangObject :
        getOrCreateArrayType(typeJavaLangObject, minDims - 1);

    if (thisDims == thatDims) {

      // Try to generalize by leaf types
      JType thisLeafType = thisArrayType.getLeafType();
      JType thatLeafType = thatArrayType.getLeafType();

      if (!(thisLeafType instanceof JReferenceType) || !(thatLeafType instanceof JReferenceType)) {
        return minimalGeneralType;
      }

      /*
       * Both are reference types; the result is the generalization of the leaf types combined with
       * the number of dims; that is, Foo[] and Bar[] generalize to X[] where X is the
       * generalization of Foo and Bar.
       *
       * Never generalize arrays to arrays of {@link JAnalysisDecoratedType}. One of the reasons is
       * that array initialization is not accounted for in {@link TypeTightener}.
       */
      JReferenceType leafGeneralization = generalizeTypes(
          (JReferenceType) thisLeafType, (JReferenceType) thatLeafType).getUnderlyingType();
      return getOrCreateArrayType(leafGeneralization, thisDims);
    }

    // Different number of dims
    if (typeOracle.castSucceedsTrivially(thatArrayType, thisArrayType)) {
      return thisArrayType;
    }

    if (typeOracle.castSucceedsTrivially(thisArrayType, thatArrayType)) {
      return thatArrayType;
    }

    // Totally unrelated
    return minimalGeneralType;
  }

  private JReferenceType generalizeInterfaces(JInterfaceType thisInterface,
      JInterfaceType thatInterface) {
    if (typeOracle.castSucceedsTrivially(thisInterface, thatInterface)) {
      return thatInterface;
    }

    if (typeOracle.castSucceedsTrivially(thatInterface, thisInterface)) {
      return thisInterface;
    }

    // unrelated
    return typeJavaLangObject;
  }

  private JReferenceType generalizeClasses(JClassType thisClass, JClassType thatClass) {
  /*
   * see how far each type is from object; walk the one who's farther up
   * until they're even; then walk them up together until they meet (worst
   * case at Object)
   */
    int distance1 = countSuperTypes(thisClass);
    int distance2 = countSuperTypes(thatClass);
    for (; distance1 > distance2; --distance1) {
      thisClass = thisClass.getSuperClass();
    }

    for (; distance1 < distance2; --distance2) {
      thatClass = thatClass.getSuperClass();
    }

    while (thisClass != thatClass) {
      thisClass = thisClass.getSuperClass();
      thatClass = thatClass.getSuperClass();
    }

    return thisClass;
  }

  /**
   * Returns a sorted list of array types, so the returned set can be iterated
   * over without introducing nondeterminism.
   */
  public List<JArrayType> getAllArrayTypes() {
    List<JArrayType> result = Lists.newArrayList(arrayTypes.values());
    Collections.sort(result, ARRAYTYPE_COMPARATOR);
    return result;
  }

  /**
   * Returns an expression that evaluates to an array class literal at runtime.
   * <p>
   * Note: This version can only be called after {@link
   * com.google.gwt.dev.jjs.impl.ImplementClassLiteralsAsFields} has been run.
   */
  public JExpression createArrayClassLiteralExpression(SourceInfo sourceInfo,
      JClassLiteral leafTypeClassLiteral, int dimensions) {
    JField leafTypeClassLiteralField = leafTypeClassLiteral.getField();
    assert leafTypeClassLiteralField != null : "Array leaf type must have a class literal field; "
        + "either ImplementClassLiteralsAsField has not run yet or or there is an error computing"
        + "live class literals.";

    return new JMethodCall(sourceInfo, null, getIndexedMethod(
        RuntimeConstants.ARRAY_GET_CLASS_LITERAL_FOR_ARRAY),
        new JFieldRef(sourceInfo, null,  leafTypeClassLiteralField,
            leafTypeClassLiteralField.getEnclosingType()), getLiteralInt(dimensions));
  }

  public Map<JReferenceType, JCastMap> getCastMap() {
    return Collections.unmodifiableMap(castMaps);
  }

  public JCastMap getCastMap(JReferenceType referenceType) {
    // ensure jsonCastableTypeMaps has been initialized
    // it might not have been if the ImplementCastsAndTypeChecks has not been run
    if (castMaps == null) {
      initTypeInfo(null);
    }
    return castMaps.get(referenceType);
  }

  public JField getClassLiteralField(JType type) {
    return classLiteralFieldsByType.get(
        (type.isJsNative() || type.isJsoType()) ? getJavaScriptObject() : type);
  }

  public List<JDeclaredType> getDeclaredTypes() {
    return allTypes;
  }

  public List<JMethod> getEntryMethods() {
    return entryMethods;
  }

  public int getFragmentCount() {
    // Initial fragment is the +1.
    return runAsyncs.size() + 1;
  }

  public FragmentPartitioningResult getFragmentPartitioningResult() {
    return fragmentPartitioningResult;
  }

  // TODO(stalcup): this is a blatant bug. there's no unambiguous way to convert from binary name to
  // source name. JProgram needs to index types both ways.
  public JDeclaredType getFromTypeMap(String qualifiedBinaryOrSourceName) {
    String srcTypeName = qualifiedBinaryOrSourceName.replace('$', '.');

    return typeNameMap.get(srcTypeName);
  }

  public JField getIndexedField(String string) {
    JField field = indexedFields.get(string);
    if (field == null) {
      throw new InternalCompilerException("Unable to locate index field: " + string);
    }
    return field;
  }

  public Set<JField> getIndexedFields() {
    return ImmutableSet.copyOf(indexedFields.values());
  }

  public JMethod getIndexedMethod(String string) {
    JMethod method = indexedMethods.get(string);
    if (method == null) {
      throw new InternalCompilerException("Unable to locate index method: " + string);
    }
    return method;
  }

  public Set<JMethod> getIndexedMethods() {
    return ImmutableSet.copyOf(indexedMethods.values());
  }

  public JMethod getIndexedMethodOrNull(String string) {
    return indexedMethods.get(string);
  }

  public JDeclaredType getIndexedType(String string) {
    JDeclaredType type = indexedTypes.get(string);
    if (type == null) {
      throw new InternalCompilerException("Unable to locate index type: " + string);
    }
    return type;
  }

  public Collection<JDeclaredType> getIndexedTypes() {
    return Collections.unmodifiableCollection(indexedTypes.values());
  }

  public LinkedHashSet<JRunAsync> getInitialAsyncSequence() {
    return initialAsyncSequence;
  }

  public List<Integer> getInitialFragmentIdSequence() {
    return initialFragmentIdSequence;
  }

  public JClassType getJavaScriptObject() {
    return typeSpecialJavaScriptObject;
  }

  public JLiteral getLiteral(Object value) {
    return getLiteral(SourceOrigin.UNKNOWN, value);
  }

  public JLiteral getLiteral(SourceInfo info,  Object value) {
    if (value == null) {
      return getLiteralNull();
    }
    if (value instanceof String) {
      return getStringLiteral(info, (String) value);
    }
    if (value instanceof Integer) {
      return getLiteralInt((Integer) value);
    }
    if (value instanceof Long) {
      return getLiteralLong((Long) value);
    }
    if (value instanceof Character) {
      return getLiteralChar((Character) value);
    }
    if (value instanceof Boolean) {
      return getLiteralBoolean((Boolean) value);
    }
    if (value instanceof Double) {
      return getLiteralDouble((Double) value);
    }
    if (value instanceof Float) {
      return getLiteralFloat((Float) value);
    }
    throw new IllegalArgumentException("Unknown literal type for " + value);
  }

  public JBooleanLiteral getLiteralBoolean(boolean value) {
    return JBooleanLiteral.get(value);
  }

  public JCharLiteral getLiteralChar(char value) {
    return JCharLiteral.get(value);
  }

  public JDoubleLiteral getLiteralDouble(double d) {
    return JDoubleLiteral.get(d);
  }

  public JFloatLiteral getLiteralFloat(double f) {
    return JFloatLiteral.get(f);
  }

  public JIntLiteral getLiteralInt(int value) {
    return JIntLiteral.get(value);
  }

  public JLongLiteral getLiteralLong(long value) {
    return JLongLiteral.get(value);
  }

  public JNullLiteral getLiteralNull() {
    return JNullLiteral.INSTANCE;
  }

  public JStringLiteral getStringLiteral(SourceInfo sourceInfo, String s) {
    sourceInfo.addCorrelation(sourceInfo.getCorrelator().by(Literal.STRING));
    return new JStringLiteral(sourceInfo, StringInterner.get().intern(s), typeString);
  }

  public List<JDeclaredType> getModuleDeclaredTypes() {
    List<JDeclaredType> moduleDeclaredTypes = Lists.newArrayList();
    for (JDeclaredType type : allTypes) {
      if (isReferenceOnly(type)) {
        continue;
      }
      moduleDeclaredTypes.add(type);
    }
    return moduleDeclaredTypes;
  }

  public int getNodeCount() {
    try (JavaCountEvent event = new JavaCountEvent()) {
      TreeStatistics treeStats = new TreeStatistics();
      treeStats.accept(this);
      int numNodes = treeStats.getNodeCount();
      event.nodeCount = numNodes;
      return numNodes;
    }
  }

  public JField getNullField() {
    return JField.NULL_FIELD;
  }

  public JMethod getNullMethod() {
    return JMethod.NULL_METHOD;
  }

  public List<JRunAsync> getRunAsyncs() {
    return runAsyncs;
  }

  public int getCommonAncestorFragmentId(int thisFragmentId, int thatFragmentId) {
    return fragmentPartitioningResult.getCommonAncestorFragmentId(thisFragmentId, thatFragmentId);
  }

  public Collection<JType> getSubclasses(JType type) {
    return Collections2.transform(typeOracle.getSubClassNames(type.getName()),
        new Function<String, JType>() {
          @Override
          public JType apply(String typeName) {
            return getFromTypeMap(typeName);
          }
        }
    );
  }

  public JMethod getStaticImpl(JMethod method) {
    JMethod staticImpl = instanceToStaticMap.get(method);
    assert staticImpl == null || staticImpl.getEnclosingType().getMethods().contains(staticImpl);
    return staticImpl;
  }

  public JArrayType getTypeArray(JType elementType) {
    JArrayType arrayType = arrayTypes.get(elementType);
    if (arrayType == null) {
      arrayType = new JArrayType(elementType);
      arrayTypes.put(elementType, arrayType);
    }
    return arrayType;
  }

  // TODO(dankurka): Why does JProgram synthezise array types on the fly
  // Look into refactoring JProgram to get rid of this responsibility
  @Override
  public JArrayType getOrCreateArrayType(JType leafType, int dimensions) {
    assert dimensions > 0;
    assert (!(leafType instanceof JArrayType));
    JArrayType result = getTypeArray(leafType);
    while (dimensions > 1) {
      result = getTypeArray(result);
      --dimensions;
    }
    return result;
  }

  public JType getTypeByClassLiteralField(JField field) {
    return typesByClassLiteralField.get(field);
  }

  public JClassType getTypeClassLiteralHolder() {
    return typeSpecialClassLiteralHolder;
  }

  /**
   * Returns the JType corresponding to a JSNI type reference.
   */
  public JType getTypeFromJsniRef(String className) {
    int dim = 0;
    while (className.endsWith("[]")) {
      dim++;
      className = className.substring(0, className.length() - 2);
    }

    JType type = primitiveTypes.get(className);
    if (type == null) {
      type = getFromTypeMap(className);
    }
    // TODO(deprecation): remove support for this.
    if (type == null) {
      type = primitiveTypesDeprecated.get(className);
    }
    if (type == null || dim == 0) {
      return type;
    } else {
      return getOrCreateArrayType(type, dim);
    }
  }

  public JClassType getTypeJavaLangClass() {
    return typeClass;
  }

  public JClassType getTypeJavaLangObject() {
    return typeJavaLangObject;
  }

  public JArrayType getTypeJavaLangObjectArray() {
    return typeJavaLangObjectArray;
  }

  public JClassType getTypeJavaLangString() {
    return typeString;
  }

  public Set<String> getTypeNamesToIndex() {
    return typeNamesToIndex;
  }

  public JPrimitiveType getTypePrimitiveBoolean() {
    return JPrimitiveType.BOOLEAN;
  }

  public JPrimitiveType getTypePrimitiveByte() {
    return JPrimitiveType.BYTE;
  }

  public JPrimitiveType getTypePrimitiveChar() {
    return JPrimitiveType.CHAR;
  }

  public JPrimitiveType getTypePrimitiveDouble() {
    return JPrimitiveType.DOUBLE;
  }

  public JPrimitiveType getTypePrimitiveFloat() {
    return JPrimitiveType.FLOAT;
  }

  public JPrimitiveType getTypePrimitiveInt() {
    return JPrimitiveType.INT;
  }

  public JPrimitiveType getTypePrimitiveLong() {
    return JPrimitiveType.LONG;
  }

  public JPrimitiveType getTypePrimitiveShort() {
    return JPrimitiveType.SHORT;
  }

  public JPrimitiveType getTypeVoid() {
    return JPrimitiveType.VOID;
  }

  public void initTypeInfo(Map<JReferenceType, JCastMap> castMapForType) {
    castMaps = castMapForType;
    if (castMaps == null) {
      castMaps = Maps.newIdentityHashMap();
    }
  }

  public boolean isUntypedArrayType(JType type) {
    if (!type.isArrayType()) {
      return false;
    }

    JArrayType arrayType = (JArrayType) type;
    return arrayType.getLeafType().isJsNative();
  }

  public boolean isJavaLangString(JType type) {
    assert type != null;
    return type.getUnderlyingType() == typeString;
  }

  public boolean isJavaLangObject(JType type) {
    assert type != null;
    return type.getUnderlyingType() == typeJavaLangObject;
  }

  public boolean isReferenceOnly(JDeclaredType type) {
    if (type != null) {
      return referenceOnlyTypeNames.contains(type.getName());
    }
    return false;
  }

  public boolean isStaticImpl(JMethod method) {
    return staticToInstanceMap.containsKey(method);
  }

  /**
   * If the type is a JSO or an array of JSOs it returns cggcc.JavaScriptObject or an array of
   * cggcc.JavaScriptObject respectively; otherwise returns {@code type}.
   */
  public JType normalizeJsoType(JType type) {
    type = type.getUnderlyingType();

    if (type instanceof JArrayType) {
      return getOrCreateArrayType(normalizeJsoType(((JArrayType) type).getLeafType()),
          ((JArrayType) type).getDims());
    }

    if (type.isJsoType()) {
      return getJavaScriptObject();
    }
    return type;
  }

  public void putIntoTypeMap(String qualifiedBinaryName, JDeclaredType type) {
    // Make it into a source type name.
    String srcTypeName = qualifiedBinaryName.replace('$', '.');
    typeNameMap.put(srcTypeName, type);
  }

  public void putStaticImpl(JMethod method, JMethod staticImpl) {
    instanceToStaticMap.put(method, staticImpl);
    staticToInstanceMap.put(staticImpl, method);
  }

  public void recordClassLiteralFields(Map<JType, JField> classLiteralFields) {
    this.classLiteralFieldsByType = HashBiMap.create(classLiteralFields);
    this.typesByClassLiteralField = classLiteralFieldsByType.inverse();
  }

  public void removeStaticImplMapping(JMethod staticImpl) {
    JMethod instanceMethod = staticToInstanceMap.remove(staticImpl);
    if (instanceMethod != null) {
      instanceToStaticMap.remove(instanceMethod);
    }
  }

  public void removeReferenceOnlyType(JDeclaredType type) {
    referenceOnlyTypeNames.remove(type.getName());
  }

  public void setFragmentPartitioningResult(FragmentPartitioningResult result) {
    fragmentPartitioningResult = result;
  }

  public void setInitialFragmentIdSequence(List<Integer> initialFragmentIdSequence) {
    this.initialFragmentIdSequence = initialFragmentIdSequence;
  }

  public void setRunAsyncs(List<JRunAsync> runAsyncs) {
    this.runAsyncs = ImmutableList.copyOf(runAsyncs);
  }

  public void setInitialAsyncSequence(LinkedHashSet<JRunAsync> initialAsyncSequence) {
    assert this.initialAsyncSequence.isEmpty();
    initialFragmentIdSequence = Lists.newArrayList();
    // TODO(rluble): hack for now the initial fragments correspond to the initial runAsyncIds.
    initialFragmentIdSequence.addAll(
        Collections2.transform(initialAsyncSequence,
            new Function<JRunAsync, Integer>() {
              @Override
              public Integer apply(JRunAsync runAsync) {
                return runAsync.getRunAsyncId();
              }
            }));
    this.initialAsyncSequence = initialAsyncSequence;
  }

  /**
   * If {@code method} is a static impl method, returns the instance method
   * that {@code method} is the implementation of. Otherwise, returns{@code null}.
   */
  public JMethod instanceMethodForStaticImpl(JMethod method) {
    return staticToInstanceMap.get(method);
  }

  @Override
  public void traverse(JVisitor visitor, Context ctx) {
    if (visitor.visit(this, ctx)) {
      visitModuleTypes(visitor);
    }
    visitor.endVisit(this, ctx);
  }

  /**
   * Builds the starter set of type names that should be indexed when seen during addType(). This
   * set is a thread safe instance variable and external logic is free to modify it as further
   * requirements are discovered.
   */
  private static Set<String> buildInitialTypeNamesToIndex() {
    Set<String> typeNamesToIndex = Sets.newHashSet();
    typeNamesToIndex.addAll(ImmutableList.of("java.io.Serializable", "java.lang.Object",
        "java.lang.String", "java.lang.Class", "java.lang.CharSequence", "java.lang.Cloneable",
        "java.lang.Comparable", "java.lang.Enum", "java.lang.Iterable", "java.util.Iterator",
        "java.lang.AssertionError", "java.lang.Boolean", "java.lang.Byte", "java.lang.Character",
        "java.lang.Short", "java.lang.Integer", "java.lang.Long", "java.lang.Float",
        "java.lang.Double", "java.lang.Throwable", "com.google.gwt.core.client.GWT",
        JAVASCRIPTOBJECT, CLASS_LITERAL_HOLDER, "com.google.gwt.core.client.RunAsyncCallback",
        "com.google.gwt.core.client.impl.AsyncFragmentLoader",
        "com.google.gwt.core.client.impl.Impl",
        "com.google.gwt.core.client.prefetch.RunAsyncCode",
        "java.util.Objects"));
    typeNamesToIndex.addAll(CODEGEN_TYPES_SET);
    return typeNamesToIndex;
  }

  public void visitAllTypes(JVisitor visitor) {
    visitor.accept(allTypes);
  }

  public void visitModuleTypes(JVisitor visitor) {
    for (JDeclaredType type : allTypes) {
      if (isReferenceOnly(type)) {
        continue;
      }
      visitor.accept(type);
    }
  }

  private int countSuperTypes(JClassType type) {
    int count = 0;
    while ((type = type.getSuperClass()) != null) {
      ++count;
    }
    return count;
  }

  /**
   * See notes in {@link #writeObject(ObjectOutputStream)}.
   *
   * @see #writeObject(ObjectOutputStream)
   */
  private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
    allTypes = deserializeTypes(stream);
    stream.defaultReadObject();
  }

  /**
   * Serializing the Java AST is a multi-step process to avoid blowing out the
   * stack.
   *
   * <ol>
   * <li>Write all declared types in a lightweight manner to establish object
   * identity for types</li>
   * <li>Write all fields; write all methods in a lightweight manner to
   * establish object identity for methods</li>
   * <li>Write all method bodies</li>
   * <li>Write everything else, which will mostly refer to already-serialized
   * objects.</li>
   * <li>Write the bodies of the entry methods (unlike all other methods, these
   * are not contained by any type.</li>
   * </ol>
   *
   * The goal of this process to to avoid "running away" with the stack. Without
   * special logic here, lots of things would reference types, method body code
   * would reference both types and other methods, and really, really long
   * recursion chains would result.
   */
  private void writeObject(ObjectOutputStream stream) throws IOException {
    serializeTypes(allTypes, stream);
    stream.defaultWriteObject();
  }
}
