/*
 * Copyright 2011 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.core.ext.TreeLogger;
import com.google.gwt.core.ext.TreeLogger.Type;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.linker.ArtifactSet;
import com.google.gwt.dev.CompilerContext;
import com.google.gwt.dev.MinimalRebuildCache;
import com.google.gwt.dev.cfg.ConfigurationProperty;
import com.google.gwt.dev.cfg.Property;
import com.google.gwt.dev.javac.CompilationProblemReporter;
import com.google.gwt.dev.javac.CompilationState;
import com.google.gwt.dev.javac.CompilationUnit;
import com.google.gwt.dev.javac.CompiledClass;
import com.google.gwt.dev.jdt.RebindPermutationOracle;
import com.google.gwt.dev.jjs.PrecompilationContext;
import com.google.gwt.dev.jjs.SourceInfo;
import com.google.gwt.dev.jjs.SourceOrigin;
import com.google.gwt.dev.jjs.ast.Context;
import com.google.gwt.dev.jjs.ast.HasName;
import com.google.gwt.dev.jjs.ast.JArrayType;
import com.google.gwt.dev.jjs.ast.JBinaryOperation;
import com.google.gwt.dev.jjs.ast.JBooleanLiteral;
import com.google.gwt.dev.jjs.ast.JCastOperation;
import com.google.gwt.dev.jjs.ast.JClassLiteral;
import com.google.gwt.dev.jjs.ast.JClassType;
import com.google.gwt.dev.jjs.ast.JConditional;
import com.google.gwt.dev.jjs.ast.JConstructor;
import com.google.gwt.dev.jjs.ast.JDeclaredType;
import com.google.gwt.dev.jjs.ast.JEnumType;
import com.google.gwt.dev.jjs.ast.JExpression;
import com.google.gwt.dev.jjs.ast.JExpressionStatement;
import com.google.gwt.dev.jjs.ast.JField;
import com.google.gwt.dev.jjs.ast.JFieldRef;
import com.google.gwt.dev.jjs.ast.JInstanceOf;
import com.google.gwt.dev.jjs.ast.JInterfaceType;
import com.google.gwt.dev.jjs.ast.JMember;
import com.google.gwt.dev.jjs.ast.JMethod;
import com.google.gwt.dev.jjs.ast.JMethod.Specialization;
import com.google.gwt.dev.jjs.ast.JMethodBody;
import com.google.gwt.dev.jjs.ast.JMethodCall;
import com.google.gwt.dev.jjs.ast.JModVisitor;
import com.google.gwt.dev.jjs.ast.JNameOf;
import com.google.gwt.dev.jjs.ast.JNewArray;
import com.google.gwt.dev.jjs.ast.JNewInstance;
import com.google.gwt.dev.jjs.ast.JNode;
import com.google.gwt.dev.jjs.ast.JNullLiteral;
import com.google.gwt.dev.jjs.ast.JPermutationDependentValue;
import com.google.gwt.dev.jjs.ast.JProgram;
import com.google.gwt.dev.jjs.ast.JReferenceType;
import com.google.gwt.dev.jjs.ast.JStringLiteral;
import com.google.gwt.dev.jjs.ast.JSwitchExpression;
import com.google.gwt.dev.jjs.ast.JThisRef;
import com.google.gwt.dev.jjs.ast.JTryStatement;
import com.google.gwt.dev.jjs.ast.JType;
import com.google.gwt.dev.jjs.ast.JUnsafeTypeCoercion;
import com.google.gwt.dev.jjs.ast.JVariable;
import com.google.gwt.dev.jjs.ast.RuntimeConstants;
import com.google.gwt.dev.jjs.ast.js.JDebuggerStatement;
import com.google.gwt.dev.jjs.ast.js.JsniFieldRef;
import com.google.gwt.dev.jjs.ast.js.JsniMethodBody;
import com.google.gwt.dev.jjs.ast.js.JsniMethodRef;
import com.google.gwt.dev.jjs.ast.js.JsonArray;
import com.google.gwt.dev.js.ast.JsNestingScope;
import com.google.gwt.dev.js.ast.JsProgram;
import com.google.gwt.dev.js.ast.JsRootScope;
import com.google.gwt.dev.util.JsniRef;
import com.google.gwt.dev.util.Name.BinaryName;
import com.google.gwt.dev.util.Name.InternalName;
import com.google.gwt.dev.util.StringInterner;
import com.google.gwt.dev.util.log.MetricName;
import com.google.gwt.dev.util.log.perf.AbstractJfrEvent;
import com.google.gwt.thirdparty.guava.common.base.Predicates;
import com.google.gwt.thirdparty.guava.common.collect.ImmutableMap;
import com.google.gwt.thirdparty.guava.common.collect.Iterables;
import com.google.gwt.thirdparty.guava.common.collect.LinkedListMultimap;
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.Multimap;
import com.google.gwt.thirdparty.guava.common.collect.Sets;
import com.google.gwt.thirdparty.guava.common.collect.Sets.SetView;

import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;

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

/**
 * Take independently-compiled types and merge them into a single AST.
 *
 * Works kind of like {@link ControlFlowAnalyzer} in terms of reachability,
 * except that in some cases it's easier to be conservative and visit relatively
 * more nodes than CFA would.
 *
 * Operates based on a work-queue to prevent recursion sickness.
 *
 * Must handle:
 *
 * - Type reference resolution
 *
 * - Field and method reference resolution
 *
 * - General code flow like ControlFlowAnalyzer
 *
 * - GWT.create(), GWT.runAsync(), Impl.getNameOf()
 *
 * - Stitch native methods into JsProgram
 *
 * - Class.desiredAssertionStatus, Class.isClassMetaDataEnabled, GWT.isClient,
 * GWT.isProdMode, GWT.isScript.
 */
// TODO: SOYC correlations.
// TODO(stalcup): perform only binary name based lookups so that libraries
// don't need to index compilation units by both source and binary name
// TODO(stalcup): shrink the translate/flowInto graph for reference only types to eliminate
// unnecessary loading of types and increase performance.
public class UnifyAst {

  /**
   * Embodies the access methods for the compiled class, compilation unit and type for a flavor of
   * type name.
   */
  private abstract class NameBasedTypeLocator {
    private final Map<String, CompiledClass> compiledClassesByTypeName;

    private NameBasedTypeLocator(Map<String, CompiledClass> compiledClassesByTypeName) {
      this.compiledClassesByTypeName = compiledClassesByTypeName;
    }

    protected abstract boolean hasCompileErrors(String typeName);

    protected abstract void logErrorTrace(TreeLogger branch, Type logLevel, String sourceName);

    protected CompilationUnit getCompilationUnitFromSource(String typeName) {
      return compiledClassesByTypeName.get(typeName).getUnit();
    }

    protected JDeclaredType getResolvedType(String typeName) {
      JDeclaredType resolvedType = program.getFromTypeMap(typeName);
      return resolvedType;
    }

    protected boolean resolvedTypeIsAvailable(String typeName) {
      return program.getFromTypeMap(typeName) != null;
    }

    protected boolean sourceCompilationUnitIsAvailable(String typeName) {
      return compiledClassesByTypeName.containsKey(typeName);
    }
  }

  @Name("gwt.compiler.GwtCreate")
  public static class GwtCreateEvent extends AbstractJfrEvent {
    @Label("Type Name")
    @Description("GWT.create type literal")
    String typeName;
    @Label("Caller")
    @Description("The source file that called GWT.create()")
    String caller;
  }

  private class UnifyVisitor extends JModVisitor {

    private JMethod currentMethod;

    @Override
    public void endVisit(JArrayType x, Context ctx) {
      assert false : "Should not get here";
    }

    @Override
    public void endVisit(JBinaryOperation x, Context ctx) {
      // Concat ops need to resolve string type.
      x.setType(translate(x.getType().getUnderlyingType()));
    }

    @Override
    public void endVisit(JCastOperation x, Context ctx) {
      x.resolve(translate(x.getCastType()));
    }

    @Override
    public void endVisit(JClassLiteral x, Context ctx) {
      JType refType = translate(x.getRefType());
      x.resolve(refType);

      // ImplementClassLiteralsAsFields: rescue enumType.values()/valueOf().
      if (refType instanceof JArrayType) {
        refType = ((JArrayType) refType).getLeafType();
      }

      JEnumType enumType = refType.isEnumOrSubclass();
      if (enumType == null) {
        return;
      }
      for (JMethod method : enumType.getMethods()) {
        if (!method.isStatic()) {
          continue;
        }
        if (method.getSignature().startsWith("values()") ||
            method.getSignature().startsWith("valueOf(Ljava/lang/String;)")) {
          flowInto(method);
        }
      }
    }

    @Override
    public void endVisit(JClassType x, Context ctx) {
      assert false : "Should not get here";
    }

    @Override
    public void endVisit(JConditional x, Context ctx) {
      x.setType(translate(x.getType()));
    }

    @Override
    public void endVisit(JConstructor x, Context ctx) {
      // Process as method.
      super.endVisit(x, ctx);
      instantiate(x.getEnclosingType());
    }

    @Override
    public void endVisit(JDeclaredType x, Context ctx) {
      assert false : "Should not get here";
    }

    @Override
    public void endVisit(JExpression x, Context ctx) {
      assert !x.getType().isExternal() || errorsFound;
    }

    @Override
    public void endVisit(JField x, Context ctx) {
      assert false : "Should not get here";
    }

    @Override
    public void endVisit(JFieldRef x, Context ctx) {
      JField field = translate(x.getSourceInfo(), x.getField());
      flowInto(field);
      x.resolve(field);
      // Should not have an overridden type at this point.
      assert x.getType() == x.getField().getType();
      assert !x.getEnclosingType().isExternal();
    }

    @Override
    public void endVisit(JInstanceOf x, Context ctx) {
      x.resolve(translate(x.getTestType()));
    }

    @Override
    public void endVisit(JInterfaceType x, Context ctx) {
      assert false : "Should not get here";
    }

    @Override
    public void endVisit(JMethod x, Context ctx) {
      currentMethod = null;
    }

    @Override
    public void endVisit(JMethodCall x, Context ctx) {
      // Already resolved during visit().
      JMethod target = x.getTarget();
      if (target.isExternal()) {
        assert errorsFound;
        return;
      }
      // Should not have an overridden type at this point.
      assert x instanceof JNewInstance || x.getType() == target.getType();

      flowInto(target);
    }

    @Override
    public void endVisit(JNameOf x, Context ctx) {
      HasName node = x.getNode();
      if (node instanceof JType) {
        node = translate((JType) node);
      } else if (node instanceof JMember) {
        node = translate(x.getSourceInfo(), (JMember) node);
      } else {
        assert false : "Should not get here";
      }
      x.resolve(node, (JClassType) translate(x.getType().getUnderlyingType()));
    }

    @Override
    public void endVisit(JNewArray x, Context ctx) {
      x.setType((JArrayType) translate(x.getArrayType()));
    }

    @Override
    public void endVisit(JNewInstance x, Context ctx) {
      JConstructor target = x.getTarget();
      if (target.isExternal()) {
        assert errorsFound;
        return;
      }
      flowInto(target);
    }

    @Override
    public void endVisit(JsniFieldRef x, Context ctx) {
      endVisit((JFieldRef) x, ctx);
    }

    @Override
    public void endVisit(JsniMethodBody x, Context ctx) {
      JsNestingScope funcScope = (JsNestingScope) x.getFunc().getScope();
      assert funcScope.getParent() == JsRootScope.INSTANCE;
      funcScope.nestInto(jsProgram.getScope());
    }

    @Override
    public void endVisit(JsniMethodRef x, Context ctx) {
      JMethod target = translate(x.getSourceInfo(), x.getTarget());
      x.resolve(target, program.getJavaScriptObject());
      flowInto(target);
    }

    @Override
    public void endVisit(JsonArray x, Context ctx) {
      x.resolve(translate(x.getType()));
    }

    @Override
    public void endVisit(JStringLiteral x, Context ctx) {
      JClassType stringType = program.getTypeJavaLangString();
      x.resolve(stringType);
      instantiate(stringType);
    }

    @Override
    public void endVisit(JSwitchExpression x, Context ctx) {
      x.setType(translate(x.getType()));
    }

    @Override
    public void endVisit(JThisRef x, Context ctx) {
      assert !x.getType().isExternal();
    }

    @Override
    public void endVisit(JTryStatement x, Context ctx) {
      // Needs to resolve the Exceptions Types explicitly they are multiple in Java 7 and
      // potentially different from the one in the exception variable.
      for (JTryStatement.CatchClause clause : x.getCatchClauses()) {
        List<JType> types = clause.getTypes();
        for (int i = 0; i <  types.size(); i++) {
          JReferenceType resolvedType = translate((JReferenceType) types.get(i));
          assert resolvedType.replaces(types.get(i));
          types.set(i, resolvedType);
        }
      }
    }

    @Override
    public void endVisit(JUnsafeTypeCoercion x, Context ctx) {
      x.resolve(translate(x.getCoercionType()));
    }

    @Override
    public void endVisit(JVariable x, Context ctx) {
      x.setType(translate(x.getType()));
    }

    @Override
    public boolean visit(JExpressionStatement x, Context ctx) {
      if (x.getExpr() instanceof JMethodCall) {
        JMethodCall call = (JMethodCall) x.getExpr();
        JMethod target = call.getTarget();
        if (GWT_DEBUGGER_METHOD_CALLS.contains(target.getQualifiedName())) {
          // We should see all calls here because GWT.debugger() returns void.
          ctx.replaceMe(new JDebuggerStatement(x.getSourceInfo()));
        }
      }
      return true;
    }

    @Override
    public boolean visit(JMethod x, Context ctx) {
      currentMethod = x;
      // Only visit contents of methods defined in types which are part of this compile. Visit
      // also clinits that are reachable to make sure all the nodes that are needed for
      // propagating compile time constants are available.
      return !program.isReferenceOnly(x.getEnclosingType()) ||
          x == x.getEnclosingType().getClinitMethod();
    }

    @Override
    public boolean visit(JMethodCall x, Context ctx) {
      JMethod target = translate(x.getSourceInfo(), x.getTarget());
      x.resolve(target);
      // Special handling for magic method calls.
      JExpression replacement = maybeHandleMagicMethodCall(x);
      if (replacement != null) {
        ctx.replaceMe(accept(replacement));
        return false;
      }
      return true;
    }

    private JExpression maybeHandleMagicMethodCall(JMethodCall methodCall) {
      JExpression result;
      switch (methodCall.getTarget().getQualifiedName()) {
        case GWT_CREATE:
        case OLD_GWT_CREATE:
          result = createRebindExpression(methodCall);
          break;
        case IMPL_GET_NAME_OF:
          result = handleImplNameOf(methodCall);
          break;
        case SYSTEM_GET_PROPERTY:
        case SYSTEM_GET_PROPERTY_WITH_DEFAULT:
          result =  handleSystemGetProperty(methodCall);
          break;
        default:
          // Not a magic method call, return null so that it does not get replaced.
          return null;
      }
      if (result == null) {
        // Handled magic call possibly with an error.
        return JNullLiteral.INSTANCE;
      }
      return result;
    }

    private JExpression handleSystemGetProperty(JMethodCall gwtGetPropertyCall) {
      assert (gwtGetPropertyCall.getArgs().size() == 1 || gwtGetPropertyCall.getArgs().size() == 2);
      JExpression propertyNameExpression = gwtGetPropertyCall.getArgs().get(0);
      boolean defaultVersionCalled = gwtGetPropertyCall.getArgs().size() == 2;
      JExpression defaultValueExpression = defaultVersionCalled ?
          gwtGetPropertyCall.getArgs().get(1) : null;

      if (!(propertyNameExpression instanceof JStringLiteral)) {
        error(gwtGetPropertyCall,
            "Only string constants may be used as property name in System.getProperty()");
        return null;
      }
      String propertyName = ((JStringLiteral) propertyNameExpression).getValue();

      if (!defaultVersionCalled && !isPropertyDefined(propertyName)) {
        error(gwtGetPropertyCall, "Property '" + propertyName + "' is not defined.");
        return null;
      }

      if (isMultivaluedProperty(propertyName)) {
        error(gwtGetPropertyCall,
            "Property '" + propertyName + "' is multivalued. " +
                "Multivalued properties are not supported by System.getProperty().");
        return null;
      }

      if (defaultValueExpression != null) {
        defaultValueExpression = accept(defaultValueExpression);
      }

      return JPermutationDependentValue.createRuntimeProperty(
          program, gwtGetPropertyCall.getSourceInfo(), propertyName, defaultValueExpression);
    }

    private JExpression createRebindExpression(JMethodCall gwtCreateCall) {
      assert (gwtCreateCall.getArgs().size() == 1);
      JExpression arg = gwtCreateCall.getArgs().get(0);
      if (!(arg instanceof JClassLiteral)) {
        error(gwtCreateCall, "Only class literals may be used as arguments to GWT.create()");
        return null;
      }
      JClassLiteral classLiteral = (JClassLiteral) arg;
      if (!(classLiteral.getRefType() instanceof JDeclaredType)) {
        error(gwtCreateCall,
            "Only classes and interfaces may be used as arguments to GWT.create()");
        return null;
      }

      try (GwtCreateEvent event = new GwtCreateEvent()) {
        event.typeName = classLiteral.getRefType().getName();
        event.caller = gwtCreateCall.getSourceInfo().getFileName();
        return createStaticRebindExpression(gwtCreateCall, classLiteral);
      }
    }

    private JExpression createStaticRebindExpression(JMethodCall gwtCreateCall,
        JClassLiteral classLiteral) {
      JDeclaredType type = (JDeclaredType) classLiteral.getRefType();
      String reboundTypeName = type.getName();
      // TODO(stalcup): below a MinimalRebuildCache pattern of "clear cache entries for a type" and
      // "rebuild cache entries for that type" is followed. There is a danger that a compile error
      // could occur between the two stages and leave the cache in an invalid state. Switch to a
      // transactionally safe update pattern like always updating a copy and swapping out the
      // original for the copy at the end of a successful compile.
      if (incrementalCompile) {
        // If this is the first time we've rebound this type during this compile.
        if (reboundTypeNames.add(reboundTypeName)) {
          // The rebinding of this type will accumulate rebound type to input resource associations,
          // but the accumulation should start from scratch, so clear any existing associations that
          // might have been collected in previous compiles.
          minimalRebuildCache.clearReboundTypeAssociations(reboundTypeName);
        }
        minimalRebuildCache.recordRebinderTypeForReboundType(reboundTypeName,
            currentMethod.getEnclosingType().getName());
        rebindPermutationOracle
            .getGeneratorContext().setCurrentRebindBinaryTypeName(reboundTypeName);
      }
      String requestedType = BinaryName.toSourceName(reboundTypeName);
      List<String> answers;
      try {
        answers = Lists.newArrayList(
            rebindPermutationOracle.getAllPossibleRebindAnswers(logger, requestedType));
        if (incrementalCompile) {
          // Accumulate generated artifacts so that they can be output on recompiles even if no
          // generators are run.
          ArtifactSet artifacts = rebindPermutationOracle.getGeneratorContext().getArtifacts();
          minimalRebuildCache.addGeneratedArtifacts(artifacts);
        }
        rebindPermutationOracle.getGeneratorContext().finish(logger);
        if (incrementalCompile) {
          // There may be more types known to be modified after Generator execution, which would
          // mean the previous stale types calculation was too small. Redo it.
          staleTypeNames =
              minimalRebuildCache.computeAndClearStaleTypesCache(logger, program.typeOracle);
          checkPreambleTypesStillFresh(logger);
          fullFlowIntoRemainingStaleTypes();
        }
      } catch (UnableToCompleteException e) {
        error(gwtCreateCall, "Failed to resolve '" + requestedType + "' via deferred binding");
        return null;
      }

      List<JExpression> instantiationExpressions =
          Lists.newArrayListWithCapacity(answers.size());
      for (String answer : answers) {
        JDeclaredType answerType = internalFindType(answer, sourceNameBasedTypeLocator, true);
        if (answerType == null) {
          error(gwtCreateCall, "Rebind result '" + answer + "' could not be found");
          return null;
        }
        if (!(answerType instanceof JClassType)) {
          error(gwtCreateCall, "Rebind result '" + answer + "' must be a class");
          return null;
        }
        if (answerType.isAbstract()) {
          error(gwtCreateCall, "Rebind result '" + answer + "' cannot be abstract");
          return null;
        }
        if (isJso(answerType)) {
          error(gwtCreateCall, "Rebind result '" + answer + "' cannot be a JSO");
          return null;
        }
        JExpression result = JjsUtils
            .createDefaultConstructorInstantiation(gwtCreateCall.getSourceInfo(),
                (JClassType) answerType);
        if (result == null) {
          error(gwtCreateCall,
              "Rebind result '" + answer + "' has no default (zero argument) constructors");
          return null;
        }
        instantiationExpressions.add(result);
      }
      assert answers.size() == instantiationExpressions.size();
      if (answers.size() == 1) {
        return instantiationExpressions.get(0);
      }
      return JPermutationDependentValue
          .createTypeRebind(program, gwtCreateCall.getSourceInfo(), requestedType,
              answers, instantiationExpressions);
    }

    private JExpression handleImplNameOf(final JMethodCall x) {
      assert (x.getArgs().size() == 1);
      JExpression arg = x.getArgs().get(0);
      if (!(arg instanceof JStringLiteral)) {
        error(x, "Only string literals may be used as arguments to Impl.getNameOf()");
        return null;
      }
      JStringLiteral stringLiteral = (JStringLiteral) arg;
      String stringValue = stringLiteral.getValue();
      JNode node = null;

      JsniRef ref = JsniRef.parse(stringValue);
      if (ref != null) {
        node = JsniRefLookup.findJsniRefTarget(ref, program, new JsniRefLookup.ErrorReporter() {
          @Override
          public void reportError(String errMsg) {
            error(x, errMsg);
          }
        });
      }
      if (node == null) {
        // Not found, must be null
        return null;
      }

      if (node instanceof JMethod) {
        flowInto((JMethod) node);
        program.addPinnedMethod((JMethod) node);
      }
      return new JNameOf(x.getSourceInfo(), program.getTypeJavaLangString(), (HasName) node);
    }
  }

  private boolean isMultivaluedProperty(String propertyName) {
    Property property = compilerContext.getModule().getProperties().find(propertyName);
    if (!(property instanceof ConfigurationProperty)) {
      return false;
    }

    return ((ConfigurationProperty) property).allowsMultipleValues();
  }

  private boolean isPropertyDefined(String propertyName) {
    return compilerContext.getModule().getProperties().find(propertyName) != null;
  }

  private static final String CLASS_DESIRED_ASSERTION_STATUS =
      "java.lang.Class.desiredAssertionStatus()Z";

  private static final String CLASS_IS_CLASS_METADATA_ENABLED =
      "java.lang.Class.isClassMetadataEnabled()Z";

  public static final String GWT_CREATE =
      "com.google.gwt.core.shared.GWT.create(Ljava/lang/Class;)Ljava/lang/Object;";

  public static final String SYSTEM_GET_PROPERTY =
      "java.lang.System.getProperty(Ljava/lang/String;)Ljava/lang/String;";

  public static final String SYSTEM_GET_PROPERTY_WITH_DEFAULT =
      "java.lang.System.getProperty(Ljava/lang/String;Ljava/lang/String;)" +
          "Ljava/lang/String;";

  private static final String GWT_DEBUGGER_SHARED = "com.google.gwt.core.shared.GWT.debugger()V";

  private static final String GWT_DEBUGGER_CLIENT = "com.google.gwt.core.client.GWT.debugger()V";

  private static final String GWT_IS_CLIENT = "com.google.gwt.core.shared.GWT.isClient()Z";

  private static final String GWT_IS_PROD_MODE = "com.google.gwt.core.shared.GWT.isProdMode()Z";

  private static final String GWT_IS_SCRIPT = "com.google.gwt.core.shared.GWT.isScript()Z";

  private static final String IMPL_GET_NAME_OF =
      "com.google.gwt.core.client.impl.Impl.getNameOf(Ljava/lang/String;)Ljava/lang/String;";

  public static final String OLD_GWT_CREATE =
      "com.google.gwt.core.client.GWT.create(Ljava/lang/Class;)Ljava/lang/Object;";

  private static final String OLD_GWT_IS_CLIENT = "com.google.gwt.core.client.GWT.isClient()Z";

  private static final String OLD_GWT_IS_PROD_MODE = "com.google.gwt.core.client.GWT.isProdMode()Z";

  private static final String OLD_GWT_IS_SCRIPT = "com.google.gwt.core.client.GWT.isScript()Z";

  /**
   * Methods for which the call site must be replaced with magic AST nodes.
   */
  private static final Set<String> GWT_DEBUGGER_METHOD_CALLS =
      Sets.newLinkedHashSet(Arrays.asList(GWT_DEBUGGER_SHARED, GWT_DEBUGGER_CLIENT));

  /**
   * Methods with magic implementations that the compiler must insert.
   */
  private final Map<String, JBooleanLiteral> replacementValueByMagicMethodQualifiedName;

  private final CompilationState compilationState;
  private final Map<String, CompiledClass> compiledClassesByInternalName;
  private final Map<String, CompiledClass> compiledClassesBySourceName;
  /**
   * JVisitor interferes with any exceptions thrown inside of a visitor traversal call tree so any
   * time UnifyAst wants to log an error and end operation care it should be done by manually
   * logging an error line and setting errorsFound to true. Adequate checking is already in place to
   * interpret this as ending further exploration and errorsFound = true is already being converted
   * to an UnableToCompleteException at the UnifyAst public function boundaries
   */
  private boolean errorsFound = false;
  private final Set<CompilationUnit> unitsWithErrorsAlreadyReported = Sets.newIdentityHashSet();

  /**
   * The set of types currently known to be instantiable. Like
   * {@link ControlFlowAnalyzer#instantiatedTypes}.
   */
  private final Set<JDeclaredType> instantiatedTypes = Sets.newIdentityHashSet();

  private final JsProgram jsProgram;

  /**
   * Fields and methods that are referenceable. Like
   * {@link ControlFlowAnalyzer#liveFieldsAndMethods}.
   */
  private final Set<JNode> liveFieldsAndMethods = Sets.newIdentityHashSet();

  /**
   * Types which have had all of their fields and methods resolved (as opposed to the default
   * behavior of only resolving the reachable ones). Currently only used when performing per-file
   * compilation/recompilation.
   */
  private final Set<String> fullFlowTypes = Sets.newHashSet();

  private final TreeLogger logger;
  private final CompilerContext compilerContext;
  private final Map<String, JMember> resolvedMembersByQualifiedName = Maps.newHashMap();
  private final JProgram program;
  private final RebindPermutationOracle rebindPermutationOracle;
  private final Set<String> reboundTypeNames = Sets.newHashSet();

  /**
   * The names of types whose per-file compilation cached Js and StatementRanges are known to no
   * longer be valid.
   * <p>
   * Is initialized to the full initial list at the beginning of exec() and may be recalculated
   * (larger) after Generator executions reveal more modified types.
   */
  private Set<String> staleTypeNames = Sets.newHashSet();

  /**
   * The names of stale types that have been processed (fully traversed) so far.
   */
  private Set<String> processedStaleTypeNames = Sets.newHashSet();

  /**
   * A work queue of methods whose bodies we need to traverse. Prevents
   * excessive stack use.
   */
  private final Queue<JMethod> methodsPending = Lists.newLinkedList();

  private final Set<String> liveVirtualMethods = Sets.newHashSet();
  private final Multimap<String, JMethod> pendingVirtualMethodsBySignature =
      LinkedListMultimap.create();

  private NameBasedTypeLocator sourceNameBasedTypeLocator;
  private NameBasedTypeLocator binaryNameBasedTypeLocator;
  private NameBasedTypeLocator internalNameBasedTypeLocator;

  private MinimalRebuildCache minimalRebuildCache;
  private boolean incrementalCompile;
  private final List<String> rootTypeSourceNames = Lists.newArrayList();

  public UnifyAst(TreeLogger logger, CompilerContext compilerContext, JProgram program,
      JsProgram jsProgram, PrecompilationContext precompilationContext) {

    this.incrementalCompile = compilerContext.getOptions().isIncrementalCompileEnabled();

    this.logger = logger;
    this.compilerContext = compilerContext;
    this.program = program;
    this.jsProgram = jsProgram;
    this.rebindPermutationOracle = precompilationContext.getRebindPermutationOracle();
    this.compilationState = rebindPermutationOracle.getCompilationState();
    this.compiledClassesByInternalName = compilationState.getClassFileMap();
    this.compiledClassesBySourceName = compilationState.getClassFileMapBySource();
    initializeNameBasedLocators();
    this.minimalRebuildCache = compilerContext.getMinimalRebuildCache();
    if (incrementalCompile) {
      this.staleTypeNames =
          minimalRebuildCache.computeAndClearStaleTypesCache(logger, program.typeOracle);
      checkPreambleTypesStillFresh(logger);
    }

    // Magical methods are implemented by replacing their bodies during unification.
    replacementValueByMagicMethodQualifiedName =
        ImmutableMap.<String, JBooleanLiteral>builder()
            .put(GWT_IS_CLIENT, JBooleanLiteral.TRUE)
            .put(OLD_GWT_IS_CLIENT, JBooleanLiteral.TRUE)
            .put(GWT_IS_PROD_MODE, JBooleanLiteral.TRUE)
            .put(OLD_GWT_IS_PROD_MODE, JBooleanLiteral.TRUE)
            .put(GWT_IS_SCRIPT, JBooleanLiteral.TRUE)
            .put(OLD_GWT_IS_SCRIPT, JBooleanLiteral.TRUE)
            .put(
                CLASS_DESIRED_ASSERTION_STATUS,
                JBooleanLiteral.get(compilerContext.getOptions().isEnableAssertions()))
            .put(
                CLASS_IS_CLASS_METADATA_ENABLED,
                JBooleanLiteral.get(!compilerContext.getOptions().isClassMetadataDisabled()))
            .build();
  }

  public void addRootTypes(Collection<String> rootTypeSourceNames) {
    assert this.rootTypeSourceNames.isEmpty();
    this.rootTypeSourceNames.addAll(rootTypeSourceNames);
  }

  /**
   * Special AST construction, useful for tests. Everything is resolved,
   * translated, and unified.
   */
  public void buildEverything() throws UnableToCompleteException {
    for (String internalName : compiledClassesByInternalName.keySet()) {
      String typeName = InternalName.toBinaryName(internalName);
      internalFindType(typeName, binaryNameBasedTypeLocator, true);
    }

    for (JDeclaredType type : program.getDeclaredTypes()) {
      fullFlowIntoType(type);
    }

    mainLoop();

    computeOverrides();
    if (errorsFound) {
      throw new UnableToCompleteException();
    }
    JavaAstVerifier.assertProgramIsConsistent(program);
  }

  /**
   * Translates and stitches (unifies) type ASTs into one connected graph.<br />
   *
   * Only types reachable from entry points are traversed. This speeds, saves memory trims
   * unreferenced elements.
   */
  public void exec() throws UnableToCompleteException {
    // Trace execution from entry points and resolve references.
    List<String> entryMethodNames = Lists.newArrayList();
    for (JMethod entryMethod : program.getEntryMethods()) {
      flowInto(entryMethod);
      entryMethodNames.add(entryMethod.getJsniSignature(true, true));
    }

    // Ensure that root types are loaded and possibly (depending on mode) traversed.
    List<String> rootTypeBinaryNames = Lists.newArrayList();
    for (String rootTypeSourceName : rootTypeSourceNames) {
      JDeclaredType rootType =
          internalFindType(rootTypeSourceName, sourceNameBasedTypeLocator, true);
      if (rootType == null) {
        continue;
      }
      rootTypeBinaryNames.add(rootType.getName());
      if (rootType.hasJsInteropEntryPoints()) {
        fullFlowIntoType(rootType);
      }
    }
    minimalRebuildCache.setRootTypeNames(rootTypeBinaryNames);
    minimalRebuildCache.setEntryMethodNames(entryMethodNames);

    // Some fields and methods in codegen types might only become referenced as the result of
    // visitor execution after unification. Since we don't want those fields are methods to be
    // prematurely pruned here we defensively trace them now.
    for (JClassType type : program.codeGenTypes) {
      flowInto(type);
    }

    // Make sure that the rewriting pass for the types that are represented as natives have the
    // needed members available.
    for (JDeclaredType type : program.getRepresentedAsNativeTypes()) {
      flowInto(type);
    }

    if (incrementalCompile) {
      fullFlowIntoRemainingStaleTypes();
    }

    /*
     * Since we're not actually optimizing here, it's easier to just visit
     * certain things up front instead of duplicating the exacting semantics of
     * ControlFlowAnalyzer.
     */
    // String literals.
    instantiate(program.getTypeJavaLangString());
    // ControlFlowAnalyzer.rescueByConcat().
    flowInto(program.getIndexedMethod(RuntimeConstants.OBJECT_TO_STRING));
    flowInto((JMethod)
        resolvedMembersByQualifiedName.get("java.lang.String.valueOf(C)Ljava/lang/String;"));

    // FixAssignmentsToUnboxOrCast
    AutoboxUtils autoboxUtils = new AutoboxUtils(program);
    for (JMethod method : autoboxUtils.getBoxMethods()) {
      flowInto(method);
    }
    for (JMethod method : autoboxUtils.getUnboxMethods()) {
      flowInto(method);
    }

    // ReplaceRunAsyncs
    if (compilerContext.getOptions().isRunAsyncEnabled()) {
      flowInto(program.getIndexedMethod(RuntimeConstants.ASYNC_FRAGMENT_LOADER_ON_LOAD));
      flowInto(program.getIndexedMethod(RuntimeConstants.ASYNC_FRAGMENT_LOADER_RUN_ASYNC));
    }

    // ImplementClassLiteralsAsFields
    staticInitialize(program.getTypeClassLiteralHolder());
    for (JMethod method : program.getTypeJavaLangClass().getMethods()) {
      if (method.isStatic() && method.getName().startsWith("createFor")) {
        flowInto(method);
      }
    }

    mainLoop();

    if (incrementalCompile) {
      int declaredTypesInModule = program.getModuleDeclaredTypes().size();

      MetricName.DECLARED_TYPES_IN_MODULE.setAmount(logger, declaredTypesInModule);

      logger.log(TreeLogger.INFO, "Unification traversed " + liveFieldsAndMethods.size()
          + " fields and methods and " + program.getDeclaredTypes().size() + " types. "
          + declaredTypesInModule
          + " are considered part of the current module and " + fullFlowTypes.size()
          + " had all of their fields and methods traversed.");

      Set<String> remainingStaleTypeNames = computeRemainingStaleTypeNames();
      if (!remainingStaleTypeNames.isEmpty()) {
        logger.log(TreeLogger.WARN, "Some stale types (" + remainingStaleTypeNames
            + ") were not reprocessed as was expected. This is either a compiler bug or a "
            + "Generator has legitimately stopped creating these types.");
      }
      // Record the list of names of stale types that were processed, for test assertion purposes.
      minimalRebuildCache.setProcessedStaleTypeNames(fullFlowTypes);
    }

    // Compute overrides before pruning, otherwise if a parent class method is pruned an overriding
    // child class method might not look like an override.
    List<JMethod> newStubMethods = computeOverrides();

    // Make sure the created methods have the right liveness computation and don't get incorrectly
    // pruned.
    for (JMethod method : newStubMethods) {
      if (instantiatedTypes.contains(method.getEnclosingType()) &&
          liveVirtualMethods.contains(method.getSignature())) {
        liveFieldsAndMethods.add(method);
      }
    }

    if (!incrementalCompile) {
      // Post-stitching clean-ups.
      pruneDeadFieldsAndMethods();
    }
    if (errorsFound) {
      // Already logged.
      throw new UnableToCompleteException();
    }
    JavaAstVerifier.assertProgramIsConsistent(program);
  }

  /**
   * Attempts to eagerly load and traverse all remaining known-stale types.
   * <p>
   * Some types may not exist till after some Generator execution so missing types will be
   * temporarily ignored.
   */
  private void fullFlowIntoRemainingStaleTypes() {
    for (String staleTypeName : computeRemainingStaleTypeNames()) {
      JDeclaredType staleType =
          internalFindType(staleTypeName, binaryNameBasedTypeLocator, false);
      if (staleType == null) {
        // The type is Generator output and so is not usually available in the list of types
        // provided from initial JDT compilation. The staleness marking process has already
        // handled this type by cascading the staleness marking onto the types that contain the
        // GWT.create() calls that process that create this type.
        continue;
      }
      // It's possible that the type was previously loaded before it was discovered to be stale (it
      // became stale as a result of a Generator execution). If this happens then the type will have
      // already been marked "reference only" in JProgram. This needs to be undone.
      program.removeReferenceOnlyType(staleType);

      // Make sure that the entire type is traversed.
      fullFlowIntoType(staleType);
    }
  }

  private void pruneDeadFieldsAndMethods() {
    assert !incrementalCompile;
    for (JDeclaredType type : program.getDeclaredTypes()) {
      // Remove dead fields.
      for (int fieldIndex = 0; fieldIndex < type.getFields().size(); ++fieldIndex) {
        JField field = type.getFields().get(fieldIndex);
        if (!liveFieldsAndMethods.contains(field)) {
          type.removeField(fieldIndex);
          --fieldIndex;
        }
      }

      // Empty the body of dead clinits.
      JMethod clinit = type.getClinitMethod();
      if (!liveFieldsAndMethods.contains(clinit)) {
        clinit.setBody(new JMethodBody(SourceOrigin.UNKNOWN));
      }

      // Remove dead methods, but never remove clinit.
      for (int methodIndex = 1; methodIndex < type.getMethods().size(); ++methodIndex) {
        JMethod method = type.getMethods().get(methodIndex);
        // Pruning dead methods from the override list can only be done accurately in
        // non-incremental compiles because of differences in which types are loaded and thus
        // which methods are considered live.
        Iterables.removeIf(method.getOverriddenMethods(),
            Predicates.not(Predicates.in(liveFieldsAndMethods)));
        Iterables.removeIf(method.getOverridingMethods(),
            Predicates.not(Predicates.in(liveFieldsAndMethods)));
        if (!liveFieldsAndMethods.contains(method)) {
          type.removeMethod(methodIndex);
          --methodIndex;
        }
      }
    }
  }

  private void assimilateSourceUnit(CompilationUnit unit, boolean reportErrors) {
    if (unit.isError()) {
      if (reportErrors && unitsWithErrorsAlreadyReported.add(unit)) {
        CompilationProblemReporter.reportErrors(logger, unit, false);
        CompilationProblemReporter.logErrorTrace(logger, TreeLogger.ERROR,
            compilerContext, unit.getTypeName(), true);
        errorsFound = true;
      }
      return;
    }
    // Staleness calculations need to be able to trace from CompilationUnit name to the names of
    // immediately nested types. So record those associations now.
    if (incrementalCompile) {
      compilerContext.getMinimalRebuildCache().recordNestedTypeNamesPerType(unit);
    }
    // TODO(zundel): ask for a recompile if deserialization fails?
    List<JDeclaredType> types = unit.getTypes();
    assert containsAllTypes(unit, types);
    for (JDeclaredType type : types) {
      program.addType(type);
      // If we're compiling per file and we already have currently valid output for this type.
      if (incrementalCompile && !needsNewJs(type)) {
        // Then make sure we don't output new Js for this type.
        program.addReferenceOnlyType(type);
      }
    }
    for (JDeclaredType type : types) {
      resolveType(type);
      processType(type);
    }
    // When compiling per file.
    if (incrementalCompile) {
      // It's possible that a users' edits have made a type referenceable that was not previously
      // referenceable.
      for (JDeclaredType type : types) {
        // Such a type won't have any cached JS and will need a full traversal to ensure it is
        // output (the full type with all fields and methods) as new JS.
        if (needsNewJs(type)) {
          fullFlowIntoType(type);
        }
      }
    }

    for (JDeclaredType type : types) {
      /*
       * Eagerly instantiate any type that requires devirtualization, i.e. String and
       * JavaScriptObject subtypes. That way we don't have to copy the exact semantics of
       * ControlFlowAnalyzer.
       */
      if (requiresDevirtualization(type)) {
        instantiate(type);
      }

      /*
       * We also flow into the types with JsInterop entry point because our first pass on root types
       * with JsInterop entry points are missing these inner classes. For native types this ensures
       * that the constructor is considered reachable as it might be needed later for instanceof
       * and casts.
       */
      if (type.hasJsInteropEntryPoints() || type.isJsNative() || type.isJsFunction()) {
        fullFlowIntoType(type);
      }
    }
  }

  /**
   * Ensure that if any preamble types have become stale then adequate steps are taken to ensure the
   * recreation of the entire preamble chunk.
   */
  private void checkPreambleTypesStillFresh(TreeLogger logger) {
    SetView<String> stalePreambleTypes =
        Sets.intersection(staleTypeNames, minimalRebuildCache.getPreambleTypeNames());
    if (!stalePreambleTypes.isEmpty()) {
      // Stale preamble types can't be gracefully replaced. We need to clear all per-file compile
      // related caches to force a full build.
      logger.log(TreeLogger.WARN,
          "Some preamble types became stale. Recreating them is forcing a full "
          + "recompile. Stale preamble types: " + stalePreambleTypes + ".");
      minimalRebuildCache.clearPerTypeJsCache();
      staleTypeNames.clear();
      // TODO: might be able to preserve the cache of all non-stale and non-preamble types.
    }
  }

  /**
    * Compute all overrides.
    */
   private List<JMethod> computeOverrides() {
     return new ComputeOverridesAndImplementDefaultMethods().exec(program);
   }

  private Set<String> computeRemainingStaleTypeNames() {
    return Sets.newHashSet(Sets.difference(staleTypeNames, processedStaleTypeNames));
  }

  private boolean containsAllTypes(CompilationUnit unit, List<JDeclaredType> types) {
    Set<String> binaryTypeNames = Sets.newHashSet();
    for (JDeclaredType type : types) {
      binaryTypeNames.add(type.getName());
    }
    for (CompiledClass cc : unit.getCompiledClasses()) {
      if (!binaryTypeNames.contains(InternalName.toBinaryName(cc.getInternalName()))) {
        return false;
      }
    }
    return true;
  }

  private void error(JNode x, String errorMessage) {
    error(x.getSourceInfo(), errorMessage);
  }

  private void error(SourceInfo sourceInfo, String errorMessage) {
    errorsFound = true;
    TreeLogger branch =
        logger
            .branch(TreeLogger.ERROR, "Errors in '" + sourceInfo.getFileName() + "'", null);
    // Append 'Line #: msg' to the error message.
    StringBuilder msgBuf = new StringBuilder();
    int line = sourceInfo.getStartLine();
    if (line > 0) {
      msgBuf.append("Line ");
      msgBuf.append(line);
      msgBuf.append(": ");
    }
    msgBuf.append(errorMessage);
    branch.log(TreeLogger.ERROR, msgBuf.toString());
  }

  /**
   * Resolves all fields and methods in the given type and marks it instantiable.
   * <p>
   * The net effect is to ensure the entire type is kept and inserted into the unified AST.
   */
  private void fullFlowIntoType(JDeclaredType type) {
    String typeName = type.getName();
    if (fullFlowTypes.contains(typeName) || typeName.endsWith("package-info")) {
      return;
    }
    // The traversal of this type will accumulate rebinder type to rebound type associations, but
    // the accumulation should start from scratch, so clear any existing associations that might
    // have been collected in previous compiles.
    minimalRebuildCache.clearRebinderTypeAssociations(typeName);
    fullFlowTypes.add(typeName);
    // Remove the type from the remaining stale types set so that the fullFlowIntoStaleTypes()
    // attempt is shorter.
    processedStaleTypeNames.add(typeName);
    instantiate(type);
    flowInto(type);
  }

  private void flowInto(JDeclaredType type) {
    for (JMethod method : type.getMethods()) {
      flowInto(method);
    }
    for (JField field : type.getFields()) {
      flowInto(field);
    }
  }

  private void flowInto(JField field) {
    if (field.isExternal()) {
      assert errorsFound;
      return;
    }
    if (field == JField.NULL_FIELD) {
      return;
    }
    if (liveFieldsAndMethods.contains(field)) {
      // already flown into.
      return;
    }
    liveFieldsAndMethods.add(field);
    field.setType(translate(field.getType()));
    if (field.isStatic()) {
      staticInitialize(field.getEnclosingType());
    }
  }

  private void flowInto(JMethod method) {
    if (method.isExternal()) {
      assert errorsFound;
      return;
    }
    if (method == JMethod.NULL_METHOD) {
      return;
    }
    if (liveFieldsAndMethods.contains(method)) {
      return;
    }

    liveFieldsAndMethods.add(method);

    method.resolve(
        translate(method.getOriginalReturnType()),
        translate(method.getOriginalParamTypes()),
        translate(method.getType()),
        translate(method.getThrownExceptions()));

    if (method.isStatic()) {
      staticInitialize(method.getEnclosingType());
    } else if (method.canBePolymorphic()) {
      String signature = method.getSignature();
      if (!liveVirtualMethods.contains(signature)) {
        liveVirtualMethods.add(signature);
        for (JMethod pendingMethod : pendingVirtualMethodsBySignature.removeAll(signature)) {
          assert instantiatedTypes.contains(pendingMethod.getEnclosingType());
          flowInto(pendingMethod);
        }
      }
    }
    resolveSpecialization(method);

    // Queue up the method to resolve the method body.
    methodsPending.add(method);
  }

  private void resolveSpecialization(JMethod method) {
    // TODO (cromwellian): Move to GwtAstBuilder eventually
    if (method.getSpecialization() == null) {
      return;
    }
    Specialization specialization = method.getSpecialization();
    if (specialization.getParams() == null) {
      logger.log(Type.ERROR, "Missing 'params' attribute at @SpecializeMethod for method "
          + method.getQualifiedName());
      errorsFound = true;
      return;
    }

    List<JType> resolvedParams = translate(specialization.getParams());

    JType resolvedReturn = translate(specialization.getReturns());

    String targetMethodSignature = JjsUtils.computeSignature(
        specialization.getTarget(), resolvedParams, resolvedReturn, false);

    JMethod targetMethod = JMethod.getExternalizedMethod(
        method.getEnclosingType().getName(), targetMethodSignature, false);
    JMethod resolvedTargetMethod = translate(method.getSourceInfo(), targetMethod);

    if (resolvedTargetMethod.isExternal()) {
      error(method.getSourceInfo(), "Unable to locate @SpecializeMethod target "
          + targetMethodSignature + " for method " + method.getQualifiedName());
      return;
    }

    flowInto(resolvedTargetMethod);
    specialization.resolve(resolvedParams, resolvedReturn, resolvedTargetMethod);
  }

  public NameBasedTypeLocator getSourceNameBasedTypeLocator() {
    return sourceNameBasedTypeLocator;
  }

  private void initializeNameBasedLocators() {
    sourceNameBasedTypeLocator = new NameBasedTypeLocator(compiledClassesBySourceName) {
      @Override
      protected boolean hasCompileErrors(String sourceName) {
        return compilerContext.getCompilationErrorsIndex().hasCompileErrors(sourceName);
      }

      @Override
      protected void logErrorTrace(TreeLogger branch, Type logLevel, String sourceName) {
        CompilationProblemReporter.logErrorTrace(branch, logLevel, compilerContext, sourceName,
            false);
      }
    };
    binaryNameBasedTypeLocator = new NameBasedTypeLocator(null) {
      @Override
      protected CompilationUnit getCompilationUnitFromSource(String binaryName) {
        // There is no binary name based index for this, use the internal name based one instead.
        return internalNameBasedTypeLocator.getCompilationUnitFromSource(
            BinaryName.toInternalName(binaryName));
      }

      @Override
      protected boolean sourceCompilationUnitIsAvailable(String binaryName) {
        // There is no binary name based index for this, use the internal name based one instead.
        return internalNameBasedTypeLocator.sourceCompilationUnitIsAvailable(
            BinaryName.toInternalName(binaryName));
      }

      @Override
      protected boolean hasCompileErrors(String binaryName) {
        return sourceNameBasedTypeLocator.hasCompileErrors(
            BinaryName.toSourceName(binaryName));
      }

      @Override
      protected void logErrorTrace(TreeLogger branch, Type logLevel, String binaryName) {
        sourceNameBasedTypeLocator.logErrorTrace(branch, logLevel,
            BinaryName.toSourceName(binaryName));
      }
    };
    internalNameBasedTypeLocator = new NameBasedTypeLocator(compiledClassesByInternalName) {
      @Override
      protected JDeclaredType getResolvedType(String internalName) {
        // There is no internal name based index for this, use the binary name based one instead.
        return binaryNameBasedTypeLocator.getResolvedType(InternalName.toBinaryName(internalName));
      }

      @Override
      protected boolean resolvedTypeIsAvailable(String internalName) {
        // There is no internal name based index for this, use the binary name based one instead.
        return binaryNameBasedTypeLocator.resolvedTypeIsAvailable(
            InternalName.toBinaryName(internalName));
      }

      @Override
      protected boolean hasCompileErrors(String internalName) {
        return sourceNameBasedTypeLocator.hasCompileErrors(
            InternalName.toSourceName(internalName));
      }

      @Override
      protected void logErrorTrace(TreeLogger branch, Type logLevel, String internalName) {
        sourceNameBasedTypeLocator.logErrorTrace(branch, logLevel,
            BinaryName.toSourceName(internalName));
      }
    };
  }

  private void instantiate(JDeclaredType type) {
    // Don't flow into all the parts of types defined outside this compile; except when the type is
    // requires devirtualization (JSOs, Strings, etc) in which case the original (non devirtualized)
    // methods may not be reachable anymore.
    if (program.isReferenceOnly(type) && !requiresDevirtualization(type)) {
      return;
    }
    if (type.isExternal()) {
      assert errorsFound;
      return;
    }

    if (instantiatedTypes.contains(type)) {
      return;
    }
    instantiatedTypes.add(type);
    if (type.getSuperClass() != null) {
      instantiate(translate(type.getSuperClass()));
    }
    for (JInterfaceType intf : type.getImplements()) {
      instantiate(translate(intf));
    }
    staticInitialize(type);

    // Flow into any reachable virtual methods.
    for (JMethod method : type.getMethods()) {
      if (method.canBeReferencedExternally()) {
        flowInto(method);
        continue;
      }
      if (!method.canBePolymorphic()) {
        continue;
      }

      String signature = method.getSignature();
      if (liveVirtualMethods.contains(signature)) {
        assert !pendingVirtualMethodsBySignature.containsKey(signature);
        flowInto(method);
      } else {
        pendingVirtualMethodsBySignature.put(signature, method);
      }
    }

    for (JField field : type.getFields()) {
      if (field.canBeReferencedExternally()) {
        flowInto(field);
      }
    }
  }

  private boolean requiresDevirtualization(JDeclaredType type) {
    // NOTE: these types are the ones {@link Devirtualizer} handles.
    return isJso(type) || type.isJsNative() ||
        // Use the version that takes names instead of instances as some
        // relevant instances might have not been leaded yet.
        JProgram.isRepresentedAsNative(type.getName());
  }

  private boolean isJso(JDeclaredType type) {
    if (type == null) {
      return false;
    }
    return type == program.getJavaScriptObject() || isJso(type.getSuperClass());
  }

  /**
   * Main loop: run through the queue doing deferred resolution. We could have
   * made this entirely recursive, but a work queue uses much less max stack.
   */
  private void mainLoop() {
    UnifyVisitor visitor = new UnifyVisitor();
    while (!methodsPending.isEmpty()) {
      visitor.accept(methodsPending.poll());
    }
  }

  private void processType(JDeclaredType type) {
    assert !type.isExternal();
    for (JMember member : type.getMembers()) {
      String qualifiedName = member.getQualifiedName();
      resolvedMembersByQualifiedName.put(qualifiedName, member);
      replaceMagicMethodBodies(member);
    }
  }

  private void replaceMagicMethodBodies(JMember member) {
    JExpression replacementExpression =
        replacementValueByMagicMethodQualifiedName.get(member.getQualifiedName());
    if (replacementExpression == null) {
      // Not a special method that needs replacement
      return;
    }
    JjsUtils.replaceMethodBody((JMethod) member, replacementExpression);
  }

  /**
   * During per file compilation, returns whether the given type has cached JS that can be reused.
   */
  private boolean needsNewJs(JDeclaredType type) {
    String typeName = type.getName();
    boolean hasOwnJs = minimalRebuildCache.hasJs(typeName);
    boolean isPartOfPreamble = minimalRebuildCache.getPreambleTypeNames().contains(typeName);
    return !hasOwnJs && !isPartOfPreamble;
  }

  private void resolveType(JDeclaredType type) {
    assert !type.isExternal();
    if (type.getEnclosingType() != null) {
      type.setEnclosingType(translate(type.getEnclosingType()));
    }
    if (type instanceof JClassType && type.getSuperClass() != null) {
      ((JClassType) type).setSuperClass(translate(type.getSuperClass()));
    }
    List<JInterfaceType> resolvedInterfaces = Lists.newArrayList();
    for (JInterfaceType intf : type.getImplements()) {
      resolvedInterfaces.add((JInterfaceType) translate(intf));
    }
    type.resolve(resolvedInterfaces, findPackageInfo(type));
  }

  private JDeclaredType findPackageInfo(JDeclaredType type) {
    String packagePrefix = type.getName();

    // Package prefix with trailing dot. Empty string if default package.
    packagePrefix = packagePrefix.substring(0,  packagePrefix.lastIndexOf('.') + 1);
    String pkgInfoClassName = StringInterner.get().intern(packagePrefix + "package-info");
    JDeclaredType pkgInfo = internalFindType(pkgInfoClassName, binaryNameBasedTypeLocator, false);
    // package-info classes are loaded only for their package level annotations' possible effect on
    // JsInterop configuration. They are not intended to be included in output.
    if (pkgInfo != null) {
      program.addReferenceOnlyType(pkgInfo);
    }
    return pkgInfo;
  }

  public JDeclaredType findType(String typeName, NameBasedTypeLocator nameBasedTypeLocator)
      throws UnableToCompleteException {
    JDeclaredType type = internalFindType(typeName, nameBasedTypeLocator, true);
    if (errorsFound) {
      // Already logged.
      throw new UnableToCompleteException();
    }
    return type;
  }

  private JDeclaredType internalFindType(String typeName,
      NameBasedTypeLocator nameBasedTypeLocator, boolean reportErrors) {
    if (nameBasedTypeLocator.resolvedTypeIsAvailable(typeName)) {
      // The type was already resolved.
      return nameBasedTypeLocator.getResolvedType(typeName);
    }

    if (nameBasedTypeLocator.sourceCompilationUnitIsAvailable(typeName)) {
      // Resolve from source.
      assimilateSourceUnit(nameBasedTypeLocator.getCompilationUnitFromSource(typeName),
          reportErrors);
      return nameBasedTypeLocator.getResolvedType(typeName);
    }

    if (reportErrors) {
      // The type could not be resolved as source; report the appropriate error.
      if (nameBasedTypeLocator.hasCompileErrors(typeName)) {
        TreeLogger branch = logger.branch(TreeLogger.ERROR, String.format(
            "Type %s could not be referenced because it previously failed to "
            + "compile with errors:", typeName));
        nameBasedTypeLocator.logErrorTrace(branch, TreeLogger.ERROR, typeName);
      } else {
        logger.log(TreeLogger.ERROR, String.format(
            "Could not find %s in types compiled from source. Is the source glob too strict?",
            typeName));
      }
      errorsFound = true;
    }
    return null;
  }

  private void staticInitialize(JDeclaredType type) {
    if (type.isExternal()) {
      assert errorsFound;
      return;
    }
    JMethod clinit = type.getClinitMethod();
    if (!liveFieldsAndMethods.contains(clinit)) {
      flowInto(clinit);
      if (type.getSuperClass() != null) {
        staticInitialize(translate(type.getSuperClass()));
      }
    }
  }

  /**
   * Replaces an external (stub) reference node to a particular type by the actual AST node if
   * necessary.
   */
  private <T extends JDeclaredType> T translate(T type) {
    if (!type.isExternal()) {
      return type;
    }

    T resolvedType = (T) internalFindType(type.getName(), binaryNameBasedTypeLocator, true);
    if (resolvedType == null) {
      assert errorsFound;
      return type;
    }
    assert !resolvedType.isExternal();
    return resolvedType;
  }

  /**
   * Replaces an external (stub) reference node to a particular member by the actual AST node if
   * necessary.
   */
  private <T extends JMember> T translate(SourceInfo sourceInfo, T member) {
    if (!member.isExternal()) {
      return member;
    }

    JDeclaredType enclosingType = translate(member.getEnclosingType());
    if (enclosingType.isExternal()) {
      assert errorsFound;
      return member;
    }

    String qualifiedName = member.getQualifiedName();
    T resolvedMember = (T) resolvedMembersByQualifiedName.get(qualifiedName);
    if (resolvedMember == null) {
      error(sourceInfo, "Reference to '" + qualifiedName + "' could not be resolved");
      return member;
    }

    assert !resolvedMember.isExternal();
    return resolvedMember;
  }

  /**
   * Replaces an external (stub) reference node to a particular type by the actual AST node if
   * necessary.
   */
  private JReferenceType translate(JReferenceType type) {
    JReferenceType result = type.getUnderlyingType();

    if (type instanceof JArrayType) {
      JArrayType arrayType = (JArrayType) type;
      result = program.getTypeArray(translate(arrayType.getElementType()));
    } else  if (type.isExternal()) {
      assert type instanceof JDeclaredType : "Unknown external type " + type.getName();
      result = translate((JDeclaredType) type);
    }
    assert !result.isExternal();

    if (!type.canBeNull()) {
      result = result.strengthenToNonNull();
    }

    return result;
  }

  private JType translate(JType type) {
    if (type.isPrimitiveType()) {
      return type;
    }
    return translate((JReferenceType) type);
  }

  private <T extends JType> List<T> translate(List<T> types) {
    List<T> translatedTypes = Lists.newArrayListWithCapacity(types.size());
    for (T type : types) {
      translatedTypes.add((T) translate(type));
    }
    return translatedTypes;
  }
}
