/*******************************************************************************
 * Copyright (c) 2002 - 2006 IBM Corporation.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package com.ibm.wala.ipa.callgraph.propagation;

import java.util.Iterator;
import java.util.List;
import java.util.Set;

import com.ibm.wala.analysis.reflection.IllegalArgumentExceptionContext;
import com.ibm.wala.classLoader.CallSiteReference;
import com.ibm.wala.classLoader.IClass;
import com.ibm.wala.classLoader.IField;
import com.ibm.wala.classLoader.IMethod;
import com.ibm.wala.classLoader.Language;
import com.ibm.wala.classLoader.NewSiteReference;
import com.ibm.wala.classLoader.SyntheticClass;
import com.ibm.wala.fixpoint.UnaryOperator;
import com.ibm.wala.ipa.callgraph.AnalysisCache;
import com.ibm.wala.ipa.callgraph.AnalysisOptions;
import com.ibm.wala.ipa.callgraph.CGNode;
import com.ibm.wala.ipa.callgraph.CallGraph;
import com.ibm.wala.ipa.callgraph.CallGraphBuilder;
import com.ibm.wala.ipa.callgraph.CallGraphBuilderCancelException;
import com.ibm.wala.ipa.callgraph.Context;
import com.ibm.wala.ipa.callgraph.ContextSelector;
import com.ibm.wala.ipa.callgraph.Entrypoint;
import com.ibm.wala.ipa.callgraph.impl.AbstractRootMethod;
import com.ibm.wala.ipa.callgraph.impl.ExplicitCallGraph;
import com.ibm.wala.ipa.callgraph.propagation.rta.RTAContextInterpreter;
import com.ibm.wala.ipa.cha.IClassHierarchy;
import com.ibm.wala.ssa.SSAAbstractInvokeInstruction;
import com.ibm.wala.types.TypeReference;
import com.ibm.wala.util.CancelException;
import com.ibm.wala.util.CancelRuntimeException;
import com.ibm.wala.util.MonitorUtil.IProgressMonitor;
import com.ibm.wala.util.collections.HashSetFactory;
import com.ibm.wala.util.debug.Assertions;
import com.ibm.wala.util.intset.IntSet;
import com.ibm.wala.util.intset.IntSetAction;
import com.ibm.wala.util.intset.IntSetUtil;
import com.ibm.wala.util.intset.MutableIntSet;
import com.ibm.wala.util.warnings.Warning;
import com.ibm.wala.util.warnings.Warnings;

/**
 * This abstract base class provides the general algorithm for a call graph builder that relies on propagation through an iterative
 * dataflow solver
 * 
 * TODO: This implementation currently keeps all points to sets live ... even those for local variables that do not span
 * interprocedural boundaries. This may be too space-inefficient .. we can consider recomputing local sets on demand.
 */
public abstract class PropagationCallGraphBuilder implements CallGraphBuilder {
  private final static boolean DEBUG_ALL = false;

  final static boolean DEBUG_ASSIGN = DEBUG_ALL | false;

  private final static boolean DEBUG_ARRAY_LOAD = DEBUG_ALL | false;

  private final static boolean DEBUG_ARRAY_STORE = DEBUG_ALL | false;

  private final static boolean DEBUG_FILTER = DEBUG_ALL | false;

  final protected static boolean DEBUG_GENERAL = DEBUG_ALL | false;

  private final static boolean DEBUG_GET = DEBUG_ALL | false;

  private final static boolean DEBUG_PUT = DEBUG_ALL | false;

  private final static boolean DEBUG_ENTRYPOINTS = DEBUG_ALL | false;

  /**
   * Meta-data regarding how pointers are modeled
   */
  protected PointerKeyFactory pointerKeyFactory;

  /**
   * The object that represents the java.lang.Object class
   */
  final private IClass JAVA_LANG_OBJECT;

  /**
   * Governing class hierarchy
   */
  final protected IClassHierarchy cha;

  /**
   * Special rules for bypassing Java calls
   */
  final protected AnalysisOptions options;

  /**
   * Cache of IRs and things
   */
  private final AnalysisCache analysisCache;

  /**
   * Set of nodes that have already been traversed for constraints
   */
  final private Set<CGNode> alreadyVisited = HashSetFactory.make();

  /**
   * At any given time, the set of nodes that have been discovered but not yet processed for constraints
   */
  private Set<CGNode> discoveredNodes = HashSetFactory.make();

  /**
   * Set of calls (CallSiteReferences) that are created by entrypoints
   */
  final protected Set<CallSiteReference> entrypointCallSites = HashSetFactory.make();

  /**
   * The system of constraints used to build this graph
   */
  protected PropagationSystem system;

  /**
   * Algorithm used to solve the system of constraints
   */
  private IPointsToSolver solver;

  /**
   * The call graph under construction
   */
  protected final ExplicitCallGraph callGraph;

  /**
   * Singleton operator for assignments
   */
  protected final static AssignOperator assignOperator = new AssignOperator();

  /**
   * singleton operator for filter
   */
  public final FilterOperator filterOperator = new FilterOperator();

  /**
   * singleton operator for inverse filter
   */
  protected final InverseFilterOperator inverseFilterOperator = new InverseFilterOperator();

  /**
   * An object which interprets methods in context
   */
  private SSAContextInterpreter contextInterpreter;

  /**
   * A context selector which may use information derived from the propagation-based dataflow.
   */
  protected ContextSelector contextSelector;

  /**
   * An object that abstracts how to model instances in the heap.
   */
  protected InstanceKeyFactory instanceKeyFactory;

  /**
   * Algorithmic choice: should the GetfieldOperator and PutfieldOperator cache its previous history to reduce work?
   */
  final private boolean rememberGetPutHistory = true;

  /**
   * @param cha governing class hierarchy
   * @param options governing call graph construction options
   * @param pointerKeyFactory factory which embodies pointer abstraction policy
   */
  protected PropagationCallGraphBuilder(IClassHierarchy cha, AnalysisOptions options, AnalysisCache cache,
      PointerKeyFactory pointerKeyFactory) {
    if (cha == null) {
      throw new IllegalArgumentException("cha is null");
    }
    if (options == null) {
      throw new IllegalArgumentException("options is null");
    }
    assert cache != null;
    this.cha = cha;
    this.options = options;
    this.analysisCache = cache;
    // we need pointer keys to handle reflection
    assert pointerKeyFactory != null;
    this.pointerKeyFactory = pointerKeyFactory;
    callGraph = createEmptyCallGraph(cha, options);
    try {
      callGraph.init();
    } catch (CancelException e) {
      if (DEBUG_GENERAL) {
        System.err.println("Could not initialize the call graph due to node number constraints: " + e.getMessage());
      }
    }
    callGraph.setInterpreter(contextInterpreter);
    JAVA_LANG_OBJECT = cha.lookupClass(TypeReference.JavaLangObject);
  }

  protected ExplicitCallGraph createEmptyCallGraph(IClassHierarchy cha, AnalysisOptions options) {
    return new ExplicitCallGraph(cha, options, getAnalysisCache());
  }

  /**
   * @return true iff the klass represents java.lang.Object
   */
  protected boolean isJavaLangObject(IClass klass) {
    return (klass.getReference().equals(TypeReference.JavaLangObject));
  }

  public CallGraph makeCallGraph(AnalysisOptions options) throws IllegalArgumentException, CancelException {
    return makeCallGraph(options, null);
  }

  /*
   * @see com.ibm.wala.ipa.callgraph.CallGraphBuilder#makeCallGraph(com.ibm.wala.ipa.callgraph.AnalysisOptions)
   */
  public CallGraph makeCallGraph(AnalysisOptions options, IProgressMonitor monitor) throws IllegalArgumentException,
      CallGraphBuilderCancelException {
    if (options == null) {
      throw new IllegalArgumentException("options is null");
    }
    system = makeSystem(options);

    if (DEBUG_GENERAL) {
      System.err.println("Enter makeCallGraph!");
    }

    if (DEBUG_GENERAL) {
      System.err.println("Initialized call graph");
    }

    system.setMinEquationsForTopSort(options.getMinEquationsForTopSort());
    system.setTopologicalGrowthFactor(options.getTopologicalGrowthFactor());
    system.setMaxEvalBetweenTopo(options.getMaxEvalBetweenTopo());

    discoveredNodes = HashSetFactory.make();
    discoveredNodes.add(callGraph.getFakeRootNode());

    // Set up the initially reachable methods and classes
    for (Iterator it = options.getEntrypoints().iterator(); it.hasNext();) {
      Entrypoint E = (Entrypoint) it.next();
      if (DEBUG_ENTRYPOINTS) {
        System.err.println("Entrypoint: " + E);
      }
      SSAAbstractInvokeInstruction call = E.addCall((AbstractRootMethod) callGraph.getFakeRootNode().getMethod());

      if (call == null) {
        Warnings.add(EntrypointResolutionWarning.create(E));
      } else {
        entrypointCallSites.add(call.getCallSite());
      }
    }
    
/** BEGIN Custom change: throw exception on empty entry points. This is a severe issue that should not go undetected! */
    if (entrypointCallSites.isEmpty()) {
      throw new IllegalStateException("Could not create a entrypoint callsites."
          + " This happens when some parameters of the method can not be generated automatically "
          + "(e.g. when they refer to an interface or an abstract class).");
    }
    
/** END Custom change: throw exception on empty entry points. This is a severe issue that should not go undetected! */
    customInit();

    solver = makeSolver();
    try {
      solver.solve(monitor);
    } catch (CancelException e) {
      CallGraphBuilderCancelException c = CallGraphBuilderCancelException.createCallGraphBuilderCancelException(e, callGraph,
          system.extractPointerAnalysis(this));
      throw c;
    } catch (CancelRuntimeException e) {
      CallGraphBuilderCancelException c = CallGraphBuilderCancelException.createCallGraphBuilderCancelException(e, callGraph,
          system.extractPointerAnalysis(this));
      throw c;
    }

    return callGraph;
  }

  protected PropagationSystem makeSystem(AnalysisOptions options) {
    return new PropagationSystem(callGraph, pointerKeyFactory, instanceKeyFactory);
  }

  protected abstract IPointsToSolver makeSolver();

  /**
   * A warning for when we fail to resolve a call to an entrypoint
   */
  private static class EntrypointResolutionWarning extends Warning {

    final Entrypoint entrypoint;

    EntrypointResolutionWarning(Entrypoint entrypoint) {
      super(Warning.SEVERE);
      this.entrypoint = entrypoint;
    }

    @Override
    public String getMsg() {
      return getClass().toString() + " : " + entrypoint;
    }

    public static EntrypointResolutionWarning create(Entrypoint entrypoint) {
      return new EntrypointResolutionWarning(entrypoint);
    }
  }

  protected void customInit() {
  }

  /**
   * Add constraints for a node.
   * @param monitor 
   * 
   * @return true iff any new constraints are added.
   */
  protected abstract boolean addConstraintsFromNode(CGNode n, IProgressMonitor monitor) throws CancelException;

  /**
   * Add constraints from newly discovered nodes. Note: the act of adding constraints may discover new nodes, so this routine is
   * iterative.
   * 
   * @return true iff any new constraints are added.
   * @throws CancelException 
   */
  protected boolean addConstraintsFromNewNodes(IProgressMonitor monitor) throws CancelException {
    boolean result = false;
    while (!discoveredNodes.isEmpty()) {
      Iterator<CGNode> it = discoveredNodes.iterator();
      discoveredNodes = HashSetFactory.make();
      while (it.hasNext()) {
        CGNode n = it.next();
        result |= addConstraintsFromNode(n, monitor);
      }
    }
    return result;
  }

  /**
   * @return the PointerKey that acts as a representative for the class of pointers that includes the local variable identified by
   *         the value number parameter.
   */
  public PointerKey getPointerKeyForLocal(CGNode node, int valueNumber) {
    return pointerKeyFactory.getPointerKeyForLocal(node, valueNumber);
  }

  /**
   * @return the PointerKey that acts as a representative for the class of pointers that includes the local variable identified by
   *         the value number parameter.
   */
  public FilteredPointerKey getFilteredPointerKeyForLocal(CGNode node, int valueNumber, FilteredPointerKey.TypeFilter filter) {
    assert filter != null;
    return pointerKeyFactory.getFilteredPointerKeyForLocal(node, valueNumber, filter);
  }

  public FilteredPointerKey getFilteredPointerKeyForLocal(CGNode node, int valueNumber, IClass filter) {
    return getFilteredPointerKeyForLocal(node, valueNumber, new FilteredPointerKey.SingleClassFilter(filter));
  }

  public FilteredPointerKey getFilteredPointerKeyForLocal(CGNode node, int valueNumber, InstanceKey filter) {
    return getFilteredPointerKeyForLocal(node, valueNumber, new FilteredPointerKey.SingleInstanceFilter(filter));
  }

  /**
   * @return the PointerKey that acts as a representative for the class of pointers that includes the return value for a node
   */
  public PointerKey getPointerKeyForReturnValue(CGNode node) {
    return pointerKeyFactory.getPointerKeyForReturnValue(node);
  }

  /**
   * @return the PointerKey that acts as a representative for the class of pointers that includes the exceptional return value
   */
  public PointerKey getPointerKeyForExceptionalReturnValue(CGNode node) {
    return pointerKeyFactory.getPointerKeyForExceptionalReturnValue(node);
  }

  /**
   * @return the PointerKey that acts as a representative for the class of pointers that includes the contents of the static field
   */
  public PointerKey getPointerKeyForStaticField(IField f) {
    assert f != null : "null FieldReference";
    return pointerKeyFactory.getPointerKeyForStaticField(f);
  }

  /**
   * @return the PointerKey that acts as a representation for the class of pointers that includes the given instance field. null if
   *         there's some problem.
   * @throws IllegalArgumentException if I is null
   * @throws IllegalArgumentException if field is null
   */
  public PointerKey getPointerKeyForInstanceField(InstanceKey I, IField field) {
    if (field == null) {
      throw new IllegalArgumentException("field is null");
    }
    if (I == null) {
      throw new IllegalArgumentException("I is null");
    }
    IClass t = field.getDeclaringClass();
    IClass C = I.getConcreteType();
    if (!(C instanceof SyntheticClass)) {
      if (!getClassHierarchy().isSubclassOf(C, t)) {
        return null;
      }
    }

    return pointerKeyFactory.getPointerKeyForInstanceField(I, field);
  }

  /**
   * TODO: expand this API to differentiate between different array indices
   * 
   * @param I an InstanceKey representing an abstract array
   * @return the PointerKey that acts as a representation for the class of pointers that includes the given array contents, or null
   *         if none found.
   * @throws IllegalArgumentException if I is null
   */
  public PointerKey getPointerKeyForArrayContents(InstanceKey I) {
    if (I == null) {
      throw new IllegalArgumentException("I is null");
    }
    IClass C = I.getConcreteType();
    if (!C.isArrayClass()) {
      assert false : "illegal arguments: " + I;
    }
    return pointerKeyFactory.getPointerKeyForArrayContents(I);
  }

  /**
   * Handle assign of a particular exception instance into an exception variable
   * 
   * @param exceptionVar points-to set for a variable representing a caught exception
   * @param catchClasses set of TypeReferences that the exceptionVar may catch
   * @param e a particular exception instance
   */
  protected void assignInstanceToCatch(PointerKey exceptionVar, Set<IClass> catchClasses, InstanceKey e) {
    if (catches(catchClasses, e.getConcreteType(), cha)) {
      system.newConstraint(exceptionVar, e);
    }
  }

  /**
   * Generate a set of constraints to represent assignment to an exception variable in a catch clause. Note that we use
   * FilterOperator to filter out types that the exception handler doesn't catch.
   * 
   * @param exceptionVar points-to set for a variable representing a caught exception
   * @param catchClasses set of TypeReferences that the exceptionVar may catch
   * @param e points-to-set representing a thrown exception that might be caught.
   */
  protected void addAssignmentsForCatchPointerKey(PointerKey exceptionVar, Set<IClass> catchClasses, PointerKey e) {
    if (DEBUG_GENERAL) {
      System.err.println("addAssignmentsForCatch: " + catchClasses);
    }
    // this is tricky ... we want to filter based on a number of classes ... so we can't
    // just used a FilteredPointerKey for the exceptionVar. Instead, we create a new
    // "typed local" for each catch class, and coalesce the results using
    // assignment
    for (IClass c : catchClasses) {
      if (c.getReference().equals(c.getClassLoader().getLanguage().getThrowableType())) {
        system.newConstraint(exceptionVar, assignOperator, e);
      } else {
        FilteredPointerKey typedException = TypedPointerKey.make(exceptionVar, c);
        system.newConstraint(typedException, filterOperator, e);
        system.newConstraint(exceptionVar, assignOperator, typedException);
      }
    }
  }

  /**
   * A warning for when we fail to resolve a call to an entrypoint
   */
  @SuppressWarnings("unused")
  private static class ExceptionLookupFailure extends Warning {

    final TypeReference t;

    ExceptionLookupFailure(TypeReference t) {
      super(Warning.SEVERE);
      this.t = t;
    }

    @Override
    public String getMsg() {
      return getClass().toString() + " : " + t;
    }

    public static ExceptionLookupFailure create(TypeReference t) {
      return new ExceptionLookupFailure(t);
    }
  }

  /**
   * A pointer key that delegates to an untyped variant, but adds a type filter
   */
  public final static class TypedPointerKey implements FilteredPointerKey {

    private final IClass type;

    private final PointerKey base;

    static TypedPointerKey make(PointerKey base, IClass type) {
      assert type != null;
      return new TypedPointerKey(base, type);
    }

    private TypedPointerKey(PointerKey base, IClass type) {
      this.type = type;
      this.base = base;
      assert type != null;
      assert !(type instanceof FilteredPointerKey);
    }

    /*
     * @see com.ibm.wala.ipa.callgraph.propagation.FilteredPointerKey#getTypeFilter()
     */
    public TypeFilter getTypeFilter() {
      return new SingleClassFilter(type);
    }

    @Override
    public boolean equals(Object obj) {
      // instanceof is OK because this class is final
      if (obj instanceof TypedPointerKey) {
        TypedPointerKey other = (TypedPointerKey) obj;
        return type.equals(other.type) && base.equals(other.base);
      } else {
        return false;
      }
    }

    @Override
    public int hashCode() {
      return 67931 * base.hashCode() + type.hashCode();
    }

    @Override
    public String toString() {
      return "{ " + base + " type: " + type + "}";
    }

    public PointerKey getBase() {
      return base;
    }
  }

  /**
   * @param catchClasses Set of TypeReference
   * @param klass an Exception Class
   * @return true iff klass is a subclass of some element of the Set
   * @throws IllegalArgumentException if catchClasses is null
   */
  public static boolean catches(Set<IClass> catchClasses, IClass klass, IClassHierarchy cha) {
    if (catchClasses == null) {
      throw new IllegalArgumentException("catchClasses is null");
    }
    // quick shortcut
    if (catchClasses.size() == 1) {
      IClass c = catchClasses.iterator().next();
      if (c != null && c.getReference().equals(TypeReference.JavaLangThread)) {
        return true;
      }
    }
    for (IClass c : catchClasses) {
      if (c != null && cha.isAssignableFrom(c, klass)) {
        return true;
      }
    }
    return false;
  }

  public static boolean representsNullType(InstanceKey key) throws IllegalArgumentException {
    if (key == null) {
      throw new IllegalArgumentException("key == null");
    }
    IClass cls = key.getConcreteType();
    Language L = cls.getClassLoader().getLanguage();
    return L.isNullType(cls.getReference());
  }

  /**
   * The FilterOperator is a filtered set-union. i.e. the LHS is `unioned' with the RHS, but filtered by the set associated with
   * this operator instance. The filter is the set of InstanceKeys corresponding to the target type of this cast. This is still
   * monotonic.
   * 
   * LHS U= (RHS n k)
   * 
   * 
   * Unary op: <lhs>:= Cast_k( <rhs>)
   * 
   * (Again, technically a binary op -- see note for Assign)
   * 
   * TODO: these need to be canonicalized.
   * 
   */
  public class FilterOperator extends UnaryOperator<PointsToSetVariable> implements IPointerOperator {

    protected FilterOperator() {
    }

    /*
     * @see com.ibm.wala.dataflow.UnaryOperator#evaluate(com.ibm.wala.dataflow.IVariable, com.ibm.wala.dataflow.IVariable)
     */
    @Override
    public byte evaluate(PointsToSetVariable lhs, PointsToSetVariable rhs) {

      FilteredPointerKey pk = (FilteredPointerKey) lhs.getPointerKey();

      if (DEBUG_FILTER) {
        String S = "EVAL Filter " + lhs.getPointerKey() + " " + rhs.getPointerKey();
        S += "\nEVAL      " + lhs + " " + rhs;
        System.err.println(S);
      }
      if (rhs.size() == 0) {
        return NOT_CHANGED;
      }

      boolean changed = false;
      FilteredPointerKey.TypeFilter filter = pk.getTypeFilter();
      changed = filter.addFiltered(system, lhs, rhs);

      if (DEBUG_FILTER) {
        System.err.println("RESULT " + lhs + (changed ? " (changed)" : ""));
      }

      return changed ? CHANGED : NOT_CHANGED;
    }

    /*
     * @see com.ibm.wala.ipa.callgraph.propagation.IPointerOperator#isComplex()
     */
    public boolean isComplex() {
      return false;
    }

    @Override
    public String toString() {
      return "Filter ";
    }

    @Override
    public boolean equals(Object obj) {
      // these objects are canonicalized for the duration of a solve
      return this == obj;
    }

    @Override
    public int hashCode() {
      return 88651;
    }

  }

  public IClassHierarchy getClassHierarchy() {
    return cha;
  }

  public AnalysisOptions getOptions() {
    return options;
  }

  public IClass getJavaLangObject() {
    return JAVA_LANG_OBJECT;
  }

  public ExplicitCallGraph getCallGraph() {
    return callGraph;
  }

  /**
   * Subclasses must register the context interpreter before building a call graph.
   */
  public void setContextInterpreter(SSAContextInterpreter interpreter) {
    contextInterpreter = interpreter;
    callGraph.setInterpreter(interpreter);
  }

  /*
   * @see com.ibm.detox.ipa.callgraph.CallGraphBuilder#getPointerAnalysis()
   */
  public PointerAnalysis getPointerAnalysis() {
    return system.extractPointerAnalysis(this);
  }

  public PropagationSystem getPropagationSystem() {
    return system;
  }

  public PointerKeyFactory getPointerKeyFactory() {
    return pointerKeyFactory;
  }

/** BEGIN Custom change: setter for pointerkey factory */
  public void setPointerKeyFactory(PointerKeyFactory pkFact) {
    pointerKeyFactory = pkFact;
  }

/** END Custom change: setter for pointerkey factory */
  public RTAContextInterpreter getContextInterpreter() {
    return contextInterpreter;
  }

  /**
   * @param caller the caller node
   * @param iKey an abstraction of the receiver of the call (or null if not applicable)
   * @return the CGNode to which this particular call should dispatch.
   */
  protected CGNode getTargetForCall(CGNode caller, CallSiteReference site, IClass recv, InstanceKey iKey[]) {

    IMethod targetMethod = options.getMethodTargetSelector().getCalleeTarget(caller, site, recv);

    // this most likely indicates an exclusion at work; the target selector
    // should have issued a warning
    if (targetMethod == null || targetMethod.isAbstract()) {
      return null;
    }
    Context targetContext = contextSelector.getCalleeTarget(caller, site, targetMethod, iKey);
    
    if (targetContext instanceof IllegalArgumentExceptionContext) {
      return null;
    }
    try {
      return getCallGraph().findOrCreateNode(targetMethod, targetContext);
    } catch (CancelException e) {
      return null;
    }
  }
  
  /**
   * @return the context selector for this call graph builder
   */
  public ContextSelector getContextSelector() {
    return contextSelector;
  }

  public void setContextSelector(ContextSelector selector) {
    contextSelector = selector;
  }

  public InstanceKeyFactory getInstanceKeys() {
    return instanceKeyFactory;
  }

  public void setInstanceKeys(InstanceKeyFactory keys) {
    this.instanceKeyFactory = keys;
  }

  /**
   * @return the InstanceKey that acts as a representative for the class of objects that includes objects allocated at the given new
   *         instruction in the given node
   */
  public InstanceKey getInstanceKeyForAllocation(CGNode node, NewSiteReference allocation) {
    return instanceKeyFactory.getInstanceKeyForAllocation(node, allocation);
  }

  /**
   * @param dim the dimension of the array whose instance we would like to model. dim == 0 represents the first dimension, e.g., the
   *          [Object; instances in [[Object; e.g., the [[Object; instances in [[[Object; dim == 1 represents the second dimension,
   *          e.g., the [Object instances in [[[Object;
   * @return the InstanceKey that acts as a representative for the class of array contents objects that includes objects allocated
   *         at the given new instruction in the given node
   */
  public InstanceKey getInstanceKeyForMultiNewArray(CGNode node, NewSiteReference allocation, int dim) {
    return instanceKeyFactory.getInstanceKeyForMultiNewArray(node, allocation, dim);
  }

  public <T> InstanceKey getInstanceKeyForConstant(TypeReference type, T S) {
    return instanceKeyFactory.getInstanceKeyForConstant(type, S);
  }

  public InstanceKey getInstanceKeyForClassObject(TypeReference type) {
    return instanceKeyFactory.getInstanceKeyForClassObject(type);
  }

  public boolean haveAlreadyVisited(CGNode node) {
    return alreadyVisited.contains(node);
  }

  protected void markAlreadyVisited(CGNode node) {
    alreadyVisited.add(node);
  }

  /**
   * record that we've discovered a node
   */
  public void markDiscovered(CGNode node) {
    discoveredNodes.add(node);
  }

  protected void markChanged(CGNode node) {
    alreadyVisited.remove(node);
    discoveredNodes.add(node);
  }

  protected boolean wasChanged(CGNode node) {
    return discoveredNodes.contains(node) && !alreadyVisited.contains(node);
  }

  /**
   * Binary op: <dummy>:= ArrayLoad( &lt;arrayref>) Side effect: Creates new equations.
   */
  public final class ArrayLoadOperator extends UnarySideEffect implements IPointerOperator {
    protected final MutableIntSet priorInstances = rememberGetPutHistory ? IntSetUtil.make() : null;

    @Override
    public String toString() {
      return "ArrayLoad";
    }

    public ArrayLoadOperator(PointsToSetVariable def) {
      super(def);
      system.registerFixedSet(def, this);
    }

    @Override
    public byte evaluate(PointsToSetVariable rhs) {
      if (DEBUG_ARRAY_LOAD) {
        PointsToSetVariable def = getFixedSet();
        String S = "EVAL ArrayLoad " + rhs.getPointerKey() + " " + def.getPointerKey();
        System.err.println(S);
        System.err.println("EVAL ArrayLoad " + def + " " + rhs);
        if (priorInstances != null) {
          System.err.println("prior instances: " + priorInstances + " " + priorInstances.getClass());
        }
      }

      if (rhs.size() == 0) {
        return NOT_CHANGED;
      }
      final PointerKey object = rhs.getPointerKey();

      PointsToSetVariable def = getFixedSet();
      final PointerKey dVal = def.getPointerKey();

      final MutableBoolean sideEffect = new MutableBoolean();
      IntSetAction action = new IntSetAction() {
        public void act(int i) {
          InstanceKey I = system.getInstanceKey(i);
          if (!I.getConcreteType().isArrayClass()) {
            return;
          }
          TypeReference C = I.getConcreteType().getReference().getArrayElementType();
          if (C.isPrimitiveType()) {
            return;
          }
          PointerKey p = getPointerKeyForArrayContents(I);
          if (p == null) {
            return;
          }

          if (DEBUG_ARRAY_LOAD) {
            System.err.println("ArrayLoad add assign: " + dVal + " " + p);
          }
          sideEffect.b |= system.newFieldRead(dVal, assignOperator, p, object);
        }
      };
      if (priorInstances != null) {
        rhs.getValue().foreachExcluding(priorInstances, action);
        priorInstances.addAll(rhs.getValue());
      } else {
        rhs.getValue().foreach(action);
      }
      byte sideEffectMask = sideEffect.b ? (byte) SIDE_EFFECT_MASK : 0;
      return (byte) (NOT_CHANGED | sideEffectMask);
    }

    @Override
    public int hashCode() {
      return 9871 + super.hashCode();
    }

    @Override
    public boolean equals(Object o) {
      return super.equals(o);
    }

    @Override
    protected boolean isLoadOperator() {
      return true;
    }

    /*
     * @see com.ibm.wala.ipa.callgraph.propagation.IPointerOperator#isComplex()
     */
    public boolean isComplex() {
      return true;
    }
  }

  /**
   * Binary op: <dummy>:= ArrayStore( &lt;arrayref>) Side effect: Creates new equations.
   */
  public final class ArrayStoreOperator extends UnarySideEffect implements IPointerOperator {
    @Override
    public String toString() {
      return "ArrayStore";
    }

    public ArrayStoreOperator(PointsToSetVariable val) {
      super(val);
      system.registerFixedSet(val, this);
    }

    @Override
    public byte evaluate(PointsToSetVariable rhs) {
      if (DEBUG_ARRAY_STORE) {
        PointsToSetVariable val = getFixedSet();
        String S = "EVAL ArrayStore " + rhs.getPointerKey() + " " + val.getPointerKey();
        System.err.println(S);
        System.err.println("EVAL ArrayStore " + rhs + " " + getFixedSet());
      }

      if (rhs.size() == 0) {
        return NOT_CHANGED;
      }
      PointerKey object = rhs.getPointerKey();

      PointsToSetVariable val = getFixedSet();
      PointerKey pVal = val.getPointerKey();

      List<InstanceKey> instances = system.getInstances(rhs.getValue());
      boolean sideEffect = false;
      for (Iterator<InstanceKey> it = instances.iterator(); it.hasNext();) {
        InstanceKey I = it.next();
        if (!I.getConcreteType().isArrayClass()) {
          continue;
        }
        if (I instanceof ZeroLengthArrayInNode) {
          continue;
        }
        TypeReference C = I.getConcreteType().getReference().getArrayElementType();
        if (C.isPrimitiveType()) {
          continue;
        }
        IClass contents = getClassHierarchy().lookupClass(C);
        if (contents == null) {
          assert false : "null type for " + C + " " + I.getConcreteType();
        }
        PointerKey p = getPointerKeyForArrayContents(I);
        if (DEBUG_ARRAY_STORE) {
          System.err.println("ArrayStore add filtered-assign: " + p + " " + pVal);
        }

        // note that the following is idempotent
        if (isJavaLangObject(contents)) {
          sideEffect |= system.newFieldWrite(p, assignOperator, pVal, object);
        } else {
          sideEffect |= system.newFieldWrite(p, filterOperator, pVal, object);
        }
      }
      byte sideEffectMask = sideEffect ? (byte) SIDE_EFFECT_MASK : 0;
      return (byte) (NOT_CHANGED | sideEffectMask);
    }

    @Override
    public int hashCode() {
      return 9859 + super.hashCode();
    }

    public boolean isComplex() {
      return true;
    }

    @Override
    public boolean equals(Object o) {
      return super.equals(o);
    }

    @Override
    protected boolean isLoadOperator() {
      return false;
    }
  }

  /**
   * Binary op: <dummy>:= GetField( <ref>) Side effect: Creates new equations.
   */
  public class GetFieldOperator extends UnarySideEffect implements IPointerOperator {
    private final IField field;

    protected final MutableIntSet priorInstances = rememberGetPutHistory ? IntSetUtil.make() : null;

    public GetFieldOperator(IField field, PointsToSetVariable def) {
      super(def);
      this.field = field;
      system.registerFixedSet(def, this);
    }

    @Override
    public String toString() {
      return "GetField " + getField() + "," + getFixedSet().getPointerKey();
    }

    @Override
    public byte evaluate(PointsToSetVariable rhs) {
      if (DEBUG_GET) {
        String S = "EVAL GetField " + getField() + " " + getFixedSet().getPointerKey() + " " + rhs.getPointerKey() + getFixedSet()
            + " " + rhs;
        System.err.println(S);
      }

      PointsToSetVariable ref = rhs;
      if (ref.size() == 0) {
        return NOT_CHANGED;
      }
      final PointerKey object = ref.getPointerKey();
      PointsToSetVariable def = getFixedSet();
      final PointerKey dVal = def.getPointerKey();

      IntSet value = filterInstances(ref.getValue());
      if (DEBUG_GET) {
        System.err.println("filtered value: " + value + " " + value.getClass());
        if (priorInstances != null) {
          System.err.println("prior instances: " + priorInstances + " " + priorInstances.getClass());
        }
      }
      final MutableBoolean sideEffect = new MutableBoolean();
      IntSetAction action = new IntSetAction() {
        public void act(int i) {
          InstanceKey I = system.getInstanceKey(i);
          if (!representsNullType(I)) {
            PointerKey p = getPointerKeyForInstanceField(I, getField());

            if (p != null) {
              if (DEBUG_GET) {
                String S = "Getfield add constraint " + dVal + " " + p;
                System.err.println(S);
              }
              sideEffect.b |= system.newFieldRead(dVal, assignOperator, p, object);
            }
          }
        }
      };
      if (priorInstances != null) {
        value.foreachExcluding(priorInstances, action);
        priorInstances.addAll(value);
      } else {
        value.foreach(action);
      }
      byte sideEffectMask = sideEffect.b ? (byte) SIDE_EFFECT_MASK : 0;
      return (byte) (NOT_CHANGED | sideEffectMask);
    }

    /**
     * Subclasses can override as needed
     */
    protected IntSet filterInstances(IntSet value) {
      return value;
    }

    @Override
    public int hashCode() {
      return 9857 * getField().hashCode() + getFixedSet().hashCode();
    }

    @Override
    public boolean equals(Object o) {
      if (o instanceof GetFieldOperator) {
        GetFieldOperator other = (GetFieldOperator) o;
        return getField().equals(other.getField()) && getFixedSet().equals(other.getFixedSet());
      } else {
        return false;
      }
    }

    protected IField getField() {
      return field;
    }

    @Override
    protected boolean isLoadOperator() {
      return true;
    }

    /*
     * @see com.ibm.wala.ipa.callgraph.propagation.IPointerOperator#isComplex()
     */
    public boolean isComplex() {
      return true;
    }
  }

  /**
   * Operator that represents a putfield
   */
  public class PutFieldOperator extends UnarySideEffect implements IPointerOperator {
    private final IField field;

    protected final MutableIntSet priorInstances = rememberGetPutHistory ? IntSetUtil.make() : null;

    @Override
    public String toString() {
      return "PutField" + getField();
    }

    public PutFieldOperator(IField field, PointsToSetVariable val) {
      super(val);
      this.field = field;
      system.registerFixedSet(val, this);
    }

    /*
     * @see com.ibm.wala.ipa.callgraph.propagation.IPointerOperator#isComplex()
     */
    public boolean isComplex() {
      return true;
    }

    @Override
    public byte evaluate(PointsToSetVariable rhs) {
      if (DEBUG_PUT) {
        String S = "EVAL PutField " + getField() + " " + (getFixedSet()).getPointerKey() + " " + rhs.getPointerKey()
            + getFixedSet() + " " + rhs;
        System.err.println(S);
      }

      if (rhs.size() == 0) {
        return NOT_CHANGED;
      }
      final PointerKey object = rhs.getPointerKey();

      PointsToSetVariable val = getFixedSet();
      final PointerKey pVal = val.getPointerKey();
      IntSet value = rhs.getValue();
      value = filterInstances(value);
      final UnaryOperator<PointsToSetVariable> assign = getPutAssignmentOperator();
      if (assign == null) {
        Assertions.UNREACHABLE();
      }
      final MutableBoolean sideEffect = new MutableBoolean();
      IntSetAction action = new IntSetAction() {
        public void act(int i) {
          InstanceKey I = system.getInstanceKey(i);
          if (!representsNullType(I)) {
            if (DEBUG_PUT) {
              String S = "Putfield consider instance " + I;
              System.err.println(S);
            }
            PointerKey p = getPointerKeyForInstanceField(I, getField());
            if (p != null) {
              if (DEBUG_PUT) {
                String S = "Putfield add constraint " + p + " " + pVal;
                System.err.println(S);
              }
              sideEffect.b |= system.newFieldWrite(p, assign, pVal, object);
            }
          }
        }
      };
      if (priorInstances != null) {
        value.foreachExcluding(priorInstances, action);
        priorInstances.addAll(value);
      } else {
        value.foreach(action);
      }
      byte sideEffectMask = sideEffect.b ? (byte) SIDE_EFFECT_MASK : 0;
      return (byte) (NOT_CHANGED | sideEffectMask);
    }

    /**
     * Subclasses can override as needed
     */
    protected IntSet filterInstances(IntSet value) {
      return value;
    }

    @Override
    public int hashCode() {
      return 9857 * getField().hashCode() + getFixedSet().hashCode();
    }

    @Override
    public boolean equals(Object o) {
      if (o != null && o.getClass().equals(getClass())) {
        PutFieldOperator other = (PutFieldOperator) o;
        return getField().equals(other.getField()) && getFixedSet().equals(other.getFixedSet());
      } else {
        return false;
      }
    }

    /**
     * subclasses (e.g. XTA) can override this to enforce a filtered assignment. returns null if there's a problem.
     */
    public UnaryOperator<PointsToSetVariable> getPutAssignmentOperator() {
      return assignOperator;
    }

    /**
     * @return Returns the field.
     */
    protected IField getField() {
      return field;
    }

    @Override
    protected boolean isLoadOperator() {
      return false;
    }
  }

  /**
   * Update the points-to-set for a field to include a particular instance key.
   */
  public final class InstancePutFieldOperator extends UnaryOperator<PointsToSetVariable> implements IPointerOperator {
    final private IField field;

    final private InstanceKey instance;

    protected final MutableIntSet priorInstances = rememberGetPutHistory ? IntSetUtil.make() : null;

    @Override
    public String toString() {
      return "InstancePutField" + field;
    }

    public InstancePutFieldOperator(IField field, InstanceKey instance) {
      this.field = field;
      this.instance = instance;
    }

    /**
     * Simply add the instance to each relevant points-to set.
     */
    @Override
    public byte evaluate(PointsToSetVariable dummyLHS, PointsToSetVariable var) {
      PointsToSetVariable ref = var;
      if (ref.size() == 0) {
        return NOT_CHANGED;
      }
      IntSet value = ref.getValue();
      final MutableBoolean sideEffect = new MutableBoolean();
      IntSetAction action = new IntSetAction() {
        public void act(int i) {
          InstanceKey I = system.getInstanceKey(i);
          if (!representsNullType(I)) {
            PointerKey p = getPointerKeyForInstanceField(I, field);
            if (p != null) {
              sideEffect.b |= system.newConstraint(p, instance);
            }
          }
        }
      };
      if (priorInstances != null) {
        value.foreachExcluding(priorInstances, action);
        priorInstances.addAll(value);
      } else {
        value.foreach(action);
      }
      byte sideEffectMask = sideEffect.b ? (byte) SIDE_EFFECT_MASK : 0;
      return (byte) (NOT_CHANGED | sideEffectMask);
    }

    @Override
    public int hashCode() {
      return field.hashCode() + 9839 * instance.hashCode();
    }

    @Override
    public boolean equals(Object o) {
      if (o instanceof InstancePutFieldOperator) {
        InstancePutFieldOperator other = (InstancePutFieldOperator) o;
        return field.equals(other.field) && instance.equals(other.instance);
      } else {
        return false;
      }
    }

    /*
     * @see com.ibm.wala.ipa.callgraph.propagation.IPointerOperator#isComplex()
     */
    public boolean isComplex() {
      return true;
    }
  }

  /**
   * Update the points-to-set for an array contents to include a particular instance key.
   */
  public final class InstanceArrayStoreOperator extends UnaryOperator<PointsToSetVariable> implements IPointerOperator {
    final private InstanceKey instance;

    protected final MutableIntSet priorInstances = rememberGetPutHistory ? IntSetUtil.make() : null;

    @Override
    public String toString() {
      return "InstanceArrayStore ";
    }

    public InstanceArrayStoreOperator(InstanceKey instance) {
      this.instance = instance;
    }

    /**
     * Simply add the instance to each relevant points-to set.
     */
    @Override
    public byte evaluate(PointsToSetVariable dummyLHS, PointsToSetVariable var) {
      PointsToSetVariable arrayref = var;
      if (arrayref.size() == 0) {
        return NOT_CHANGED;
      }
      IntSet value = arrayref.getValue();
      final MutableBoolean sideEffect = new MutableBoolean();
      IntSetAction action = new IntSetAction() {
        public void act(int i) {
          InstanceKey I = system.getInstanceKey(i);
          if (!I.getConcreteType().isArrayClass()) {
            return;
          }
          if (I instanceof ZeroLengthArrayInNode) {
            return;
          }
          TypeReference C = I.getConcreteType().getReference().getArrayElementType();
          if (C.isPrimitiveType()) {
            return;
          }
          IClass contents = getClassHierarchy().lookupClass(C);
          if (contents == null) {
            assert false : "null type for " + C + " " + I.getConcreteType();
          }
          PointerKey p = getPointerKeyForArrayContents(I);
          if (contents.isInterface()) {
            if (getClassHierarchy().implementsInterface(instance.getConcreteType(), contents)) {
              sideEffect.b |= system.newConstraint(p, instance);
            }
          } else {
            if (getClassHierarchy().isSubclassOf(instance.getConcreteType(), contents)) {
              sideEffect.b |= system.newConstraint(p, instance);
            }
          }
        }
      };
      if (priorInstances != null) {
        value.foreachExcluding(priorInstances, action);
        priorInstances.addAll(value);
      } else {
        value.foreach(action);
      }
      byte sideEffectMask = sideEffect.b ? (byte) SIDE_EFFECT_MASK : 0;
      return (byte) (NOT_CHANGED | sideEffectMask);
    }

    @Override
    public int hashCode() {
      return 9839 * instance.hashCode();
    }

    @Override
    public boolean equals(Object o) {
      if (o instanceof InstanceArrayStoreOperator) {
        InstanceArrayStoreOperator other = (InstanceArrayStoreOperator) o;
        return instance.equals(other.instance);
      } else {
        return false;
      }
    }

    /*
     * @see com.ibm.wala.ipa.callgraph.propagation.IPointerOperator#isComplex()
     */
    public boolean isComplex() {
      return true;
    }
  }

  protected MutableIntSet getMutableInstanceKeysForClass(IClass klass) {
    return system.cloneInstanceKeysForClass(klass);
  }

  protected IntSet getInstanceKeysForClass(IClass klass) {
    return system.getInstanceKeysForClass(klass);
  }

  /**
   * @param klass a class
   * @return an int set which represents the subset of S that correspond to subtypes of klass
   */
  protected IntSet filterForClass(IntSet S, IClass klass) {
    MutableIntSet filter = null;
    if (klass.getReference().equals(TypeReference.JavaLangObject)) {
      return S;
    } else {
      filter = getMutableInstanceKeysForClass(klass);

      boolean debug = false;
      if (DEBUG_FILTER) {
        String s = "klass     " + klass;
        System.err.println(s);
        System.err.println("initial filter    " + filter);
      }
      filter.intersectWith(S);

      if (DEBUG_FILTER && debug) {
        System.err.println("final filter    " + filter);
      }
    }
    return filter;
  }

  protected class InverseFilterOperator extends FilterOperator {
    public InverseFilterOperator() {
      super();
    }

    @Override
    public String toString() {
      return "InverseFilter";
    }

    /*
     * @see com.ibm.wala.ipa.callgraph.propagation.IPointerOperator#isComplex()
     */
    @Override
    public boolean isComplex() {
      return false;
    }

    /*
     * simply check if rhs contains a malleable.
     * 
     * @see com.ibm.wala.dataflow.UnaryOperator#evaluate(com.ibm.wala.dataflow.IVariable, com.ibm.wala.dataflow.IVariable)
     */
    @Override
    public byte evaluate(PointsToSetVariable lhs, PointsToSetVariable rhs) {

      FilteredPointerKey pk = (FilteredPointerKey) lhs.getPointerKey();
      FilteredPointerKey.TypeFilter filter = pk.getTypeFilter();

      boolean debug = false;
      if (DEBUG_FILTER) {
        String S = "EVAL InverseFilter/" + filter + " " + lhs.getPointerKey() + " " + rhs.getPointerKey();
        S += "\nEVAL      " + lhs + " " + rhs;
        System.err.println(S);
      }
      if (rhs.size() == 0) {
        return NOT_CHANGED;
      }

      boolean changed = filter.addInverseFiltered(system, lhs, rhs);

      if (DEBUG_FILTER) {
        if (debug) {
          System.err.println("RESULT " + lhs + (changed ? " (changed)" : ""));
        }
      }
      return changed ? CHANGED : NOT_CHANGED;
    }
  }

  protected IPointsToSolver getSolver() {
    return solver;
  }

  /**
   * Add constraints when the interpretation of a node changes (e.g. reflection)
   * @param monitor 
   * @throws CancelException 
   */
  public void addConstraintsFromChangedNode(CGNode node, IProgressMonitor monitor) throws CancelException {
    unconditionallyAddConstraintsFromNode(node, monitor);
  }

  protected abstract boolean unconditionallyAddConstraintsFromNode(CGNode node, IProgressMonitor monitor) throws CancelException;

  protected static class MutableBoolean {
    // a horrendous hack since we don't have closures
    boolean b = false;
  }

  public AnalysisCache getAnalysisCache() {
    return analysisCache;
  };

}
