/**
 * INTERNAL: This is an internal library; its interface may change without notice.
 *
 * Provides classes for working with variable refinements.
 *
 * Variable refinements are expressions that appear in a guard node
 * (such as an `if` condition) and can be used to refine the information
 * derived about a variable by the flow analysis. Each refinement only
 * refines a single variable, and only certain simple constructs are
 * considered (primarily `typeof` expressions and comparisons).
 *
 * To perform the refinement, the expression is evaluated to a
 * `RefinementValue`, which is a more fine-grained version of `AbstractValue`
 * that distinguishes individual constants value. This evaluation takes place
 * in a context that restricts the value of the refined variable to a single
 * candidate value, which is one of the `AbstractValue`s that the flow analysis
 * infers for it.
 *
 * If the expression evaluates to a refinement value that represents `true`,
 * then the candidate value passes the refinement. Hence, it can be propagated
 * across a condition guard with outcome `true`.
 *
 * Conversely, if it evaluates to `false`, the candidate value can be propagated
 * across a condition guard with outcome `false`.
 *
 * Note that, like all abstract values, refinement values are overapproximations,
 * so the refinement can evaluate to both `true` and `false` for the same
 * candidate value.
 */

import javascript
private import AbstractValues
private import InferredTypes

/**
 * An expression that has the right syntactic structure to be used to
 * refine the abstract values inferred for a variable.
 */
abstract class RefinementCandidate extends Expr {
  /**
   * Gets a variable that is referenced somewhere in this expression.
   */
  abstract SsaSourceVariable getARefinedVar();

  /**
   * Gets a refinement value inferred for this expression in context `ctxt`.
   */
  abstract pragma[nomagic] RefinementValue eval(RefinementContext ctxt);
}

/**
 * A refinement candidate that references at most one variable, and hence
 * can be used to refine the abstract values inferred for that variable.
 */
class Refinement extends Expr {
  Refinement() {
    this instanceof RefinementCandidate and
    count(this.(RefinementCandidate).getARefinedVar()) <= 1
  }

  /**
   * Gets the variable refined by this expression, if any.
   */
  SsaSourceVariable getRefinedVar() {
    result = this.(RefinementCandidate).getARefinedVar()
  }

  /**
   * Gets a refinement value inferred for this expression in context `ctxt`.
   */
  RefinementValue eval(RefinementContext ctxt) {
    result = this.(RefinementCandidate).eval(ctxt)
  }
}

/** A literal, viewed as a refinement expression. */
private abstract class LiteralRefinement extends RefinementCandidate, Literal {
  override SsaSourceVariable getARefinedVar() {
    none()
  }

  override RefinementValue eval(RefinementContext ctxt) {
    ctxt.appliesTo(this) and result = eval()
  }

  /**
   * Gets the refinement value that represents this literal.
   */
  RefinementValue eval() {
    result = TAny()
  }
}

/** A `null` literal, viewed as a refinement expression. */
private class NullLiteralRefinement extends LiteralRefinement, NullLiteral {
  override RefinementValue eval() {
    result = TValueWithType(TTNull())
  }
}

/** A Boolean literal, viewed as a refinement expression. */
private class BoolRefinement extends LiteralRefinement, BooleanLiteral {
  override RefinementValue eval() {
    exists (boolean b | b.toString() = getValue() |
      result = TBoolConstant(b)
    )
  }
}

/** A constant string, viewed as a refinement expression. */
private class StringRefinement extends LiteralRefinement, ConstantString {
  override RefinementValue eval() {
    result = TStringConstant(getStringValue())
  }
}

/** A numeric literal, viewed as a refinement expression. */
private abstract class NumberRefinement extends LiteralRefinement, NumberLiteral {
  override RefinementValue eval() {
    result = TValueWithType(TTNumber())
  }
}

/**
 * An integer literal, viewed as a refinement expression.
 *
 * At the moment, we only refine with the integer zero, not with any
 * other integer values.
 */
private class IntRefinement extends NumberRefinement, NumberLiteral {
  IntRefinement() { getValue().toInt() = 0 }

  override RefinementValue eval() {
    result = TIntConstant(getValue().toInt())
  }
}

/**
 * A use of the global variable `undefined`, viewed as a refinement expression.
 */
private class UndefinedInRefinement extends RefinementCandidate, SyntacticConstants::UndefinedConstant {

  override SsaSourceVariable getARefinedVar() {
    none()
  }

  override RefinementValue eval(RefinementContext ctxt) {
    ctxt.appliesTo(this) and
    result = TValueWithType(TTUndefined())
  }
}

/** A variable use, viewed as a refinement expression. */
private class VariableRefinement extends RefinementCandidate, VarUse {
  VariableRefinement() {
    getVariable() instanceof SsaSourceVariable
  }

  override SsaSourceVariable getARefinedVar() {
    result = getVariable()
  }

  override RefinementValue eval(RefinementContext ctxt) {
    ctxt.appliesTo(this) and
    result = ctxt.(VarRefinementContext).getAValue()
  }
}

/** A parenthesized refinement expression. */
private class ParRefinement extends RefinementCandidate, ParExpr {
  ParRefinement() {
    getExpression() instanceof RefinementCandidate
  }

  override SsaSourceVariable getARefinedVar() {
    result = getExpression().(RefinementCandidate).getARefinedVar()
  }

  override RefinementValue eval(RefinementContext ctxt) {
    result = getExpression().(RefinementCandidate).eval(ctxt)
  }
}

/** A `typeof` refinement expression. */
private class TypeofRefinement extends RefinementCandidate, TypeofExpr {
  TypeofRefinement() {
    getOperand() instanceof RefinementCandidate
  }

  override SsaSourceVariable getARefinedVar() {
    result = getOperand().(RefinementCandidate).getARefinedVar()
  }

  override RefinementValue eval(RefinementContext ctxt) {
    exists (RefinementValue opVal |
      opVal = getOperand().(RefinementCandidate).eval(ctxt) and
      result = TStringConstant(opVal.typeof())
    )
  }
}

/** An equality test that can be used as a refinement expression. */
private class EqRefinement extends RefinementCandidate, EqualityTest {
  EqRefinement() {
    getLeftOperand() instanceof RefinementCandidate and
    getRightOperand() instanceof RefinementCandidate
  }

  override SsaSourceVariable getARefinedVar() {
    result = getLeftOperand().(RefinementCandidate).getARefinedVar() or
    result = getRightOperand().(RefinementCandidate).getARefinedVar()
  }

  override RefinementValue eval(RefinementContext ctxt) {
    exists (RefinementCandidate l, RefinementValue lv, RefinementCandidate r, RefinementValue rv |
      l = getLeftOperand() and r = getRightOperand() and
      lv = l.eval(ctxt) and rv = r.eval(ctxt) |
      // if both sides evaluate to a constant, compare them
      if lv instanceof SingletonRefinementValue and rv instanceof SingletonRefinementValue then
        exists (boolean s, boolean p | s = getStrictness() and p = getPolarity()|
          if lv.(SingletonRefinementValue).equals(rv, s) then
            result = TBoolConstant(p)
          else
            result = TBoolConstant(p.booleanNot())
        )
      // otherwise give up
      else
        result = TValueWithType(TTBoolean())
    )
  }

  private boolean getStrictness() {
    if this instanceof StrictEqualityTest then result = true
    else result = false
  }
}

/** An index expression that can be used as a refinement expression. */
private class IndexRefinement extends RefinementCandidate, IndexExpr {
  IndexRefinement() {
    getBase() instanceof RefinementCandidate and
    getIndex() instanceof RefinementCandidate
  }

  override SsaSourceVariable getARefinedVar() {
    result = getBase().(RefinementCandidate).getARefinedVar() or
    result = getIndex().(RefinementCandidate).getARefinedVar()
  }

  override RefinementValue eval(RefinementContext ctxt) {
    exists (RefinementCandidate base, RefinementValue baseVal,
            RefinementCandidate index, RefinementValue indexVal |
      base = getBase() and index = getIndex() and
      baseVal = base.eval(ctxt) and indexVal = index.eval(ctxt) |
      if exists(evalIndex(baseVal, indexVal)) then
        result = evalIndex(baseVal, indexVal)
      else
        result = TAny()
    )
  }
}

/**
 * Gets the abstract value representing the `i`th character of `s`,
 * if any.
 */
private bindingset[s, i]
RefinementValue evalIndex(StringConstant s, IntConstant i) {
  result = TStringConstant(s.getValue().charAt(i.getValue()))
}

/**
  * A context in which a refinement expression is analyzed.
 */
newtype TRefinementContext =
  /**
   * A refinement context associated with refinement `ref`, specifying that variable `var`
   * is assumed to have abstract value `val`.
   *
   * Each context keeps track of its associated `AnalyzedRefinement` node so that we can
   * restrict the `RefinementCandidate` expressions that it applies to: it should only
   * apply to those expressions that are syntactically nested inside the `AnalyzedRefinement`.
   */
  TVarRefinementContext(AnalyzedRefinement ref, SsaSourceVariable var, AbstractValue val) {
    var = ref.getSourceVariable() and
    val = ref.getAnInputRhsValue()
  }

/**
 * A context in which a refinement expression is analyzed.
 */
class RefinementContext extends TRefinementContext {
  /**
   * Holds if refinement expression `cand` might be analyzed in this context.
   */
  abstract predicate appliesTo(RefinementCandidate cand);

  /** Gets a textual representation of this element. */
  abstract string toString();
}

/**
 * A refinement context specifying that some variable is assumed to have one particular
 * abstract value.
 */
class VarRefinementContext extends RefinementContext, TVarRefinementContext {
  override predicate appliesTo(RefinementCandidate cand) {
    exists (AnalyzedRefinement ref, SsaSourceVariable var |
      this = TVarRefinementContext(ref, var, _) |
      cand = ref.getRefinement().getAChildExpr*() and
      not cand.getARefinedVar() != var
    )
  }

  /**
   * Gets the abstract refinement value the variable is assumed to have.
   */
  RefinementValue getAValue() {
    exists (AbstractValue av | this = TVarRefinementContext(_, _, av) |
      if av instanceof AbstractBoolean then
        result.(BoolConstant).getValue() = av.(AbstractBoolean).getBooleanValue()
      else if av instanceof AbstractZero then
        result.(IntConstant).getValue() = 0
      else if av instanceof AbstractEmpty then
        result.(StringConstant).getValue() = ""
      else if av instanceof AbstractNumString then
        result instanceof NumString
      else if av instanceof AbstractOtherString then
        result instanceof NonEmptyNonNumString
      else if av instanceof AbstractNonZero then
        result instanceof NonZeroNumber
      else
        result.(ValueWithType).getType() = av.getType()
    )
  }

  override string toString() {
    exists (Variable var, AbstractValue val | this = TVarRefinementContext(_, var, val) |
      result = "[" + var + " = " + val + "]"
    )
  }
}

/** Holds if `e` is nested inside a guard node. */
private predicate inGuard(Expr e) {
  e.getParentExpr*() = any(GuardControlFlowNode g).getTest()
}

/**
 * An abstract value of a refinement expression.
 */
private newtype TRefinementValue =
  /** An abstract value indicating that no refinement information is available. */
  TAny()
  or
  /** An abstract value representing all concrete values of type `tp`. */
  TValueWithType(InferredType tp)
  or
  /** An abstract value representing the Boolean value `b`. */
  TBoolConstant(boolean b) {
    b = true or b = false
  }
  or
  /**
   * An abstract value representing the string `s`.
   *
   * There are abstract values for every string literal appearing anywhere
   * in a guard node, as well as for the empty string, all `typeof` tags and
   * their characters.
   */
  TStringConstant(string s) {
    s = ""
    or
    s instanceof TypeofTag
    or
    s = any(StringRefinement sr | inGuard(sr)).getValue()
    or
    s = any(TypeofTag t).charAt(_)
  }
  or
  /**
   * An abstract value representing the integer `i`.
   *
   * There are abstract values for zero and for every integer literal appearing
   * in a guard node.
   */
  TIntConstant(int i) {
    i = 0 or
    i = any(IntRefinement ir | inGuard(ir)).getValue().toInt()
  }
  or
  /**
   * An abstract value representing a non-empty string that coerces to a number
   * (and not `NaN`).
   */
  TNumString()
  or
  /**
   * An abstract value representing a string `s` such that `Number(s)` is `NaN`.
   */
  TNonEmptyNonNumString()
  or
  /**
   * An abstract value representing a non-zero number.
   */
  TNonZeroNumber()

/**
 * An abstract value of a refinement expression.
 */
class RefinementValue extends TRefinementValue {
  /** Gets a textual representation of this element. */
  abstract string toString();

  /**
   * Gets the `typeof` tag of a concrete value represented by this abstract value.
   */
  abstract string typeof();

  /**
   * Gets the Boolean value of a concrete value represented by this abstract value.
   */
  abstract boolean getABooleanValue();
}

/**
 * A refinement value that represents exactly one concrete value.
 */
abstract class SingletonRefinementValue extends RefinementValue {
  /**
   * Holds if `this` equals `that` under strict or non-strict equality, depending
   * on the value of `isStrict`.
   */
  predicate equals(SingletonRefinementValue that, boolean isStrict) {
    this = that and
    (isStrict = true or isStrict = false)
  }
}

/** An abstract value indicating that no refinement information is available. */
private class AnyValue extends RefinementValue, TAny {
  override string toString() { result = "any value" }

  override string typeof() { result instanceof TypeofTag }

  override boolean getABooleanValue() { result = true or result = false }
}

/** An abstract value representing all concrete values of some `InferredType`. */
private class ValueWithType extends RefinementValue, TValueWithType {
  InferredType getType() { this = TValueWithType(result) }

  override string toString() { result = "any " + getType() }

  override string typeof() { result = getType().getTypeofTag() }

  override boolean getABooleanValue() {
    result = true or
    // only primitive types can be falsy
    getType() instanceof PrimitiveType and result = false
  }
}

/** An abstract value representing `null` or `undefined`. */
private class NullOrUndefined extends ValueWithType, SingletonRefinementValue {
  NullOrUndefined() { getType() instanceof TTNull or getType() instanceof TTUndefined }

  override boolean getABooleanValue() { result = false }

  override predicate equals(SingletonRefinementValue that, boolean isStrict) {
    SingletonRefinementValue.super.equals(that, isStrict) or
    isStrict = false and that instanceof NullOrUndefined
  }
}

/** An abstract value representing a Boolean constant. */
private class BoolConstant extends SingletonRefinementValue, TBoolConstant {
  boolean value;

  BoolConstant() { this = TBoolConstant(value) }

  /** Gets the Boolean value represented by this abstract value. */
  boolean getValue() { this = TBoolConstant(result) }

  override string toString() { result = value.toString() }

  override string typeof() { result = "boolean" }

  override boolean getABooleanValue() { result = value }

  override predicate equals(SingletonRefinementValue that, boolean isStrict) {
    SingletonRefinementValue.super.equals(that, isStrict) or
    isStrict = false and (
      value = false and (that.(StringConstant).isEmptyOrZero() or that = TIntConstant(0))
      or
      value = true and (that = TStringConstant("1") or that = TIntConstant(1))
    )
  }
}

/** An abstract value representing a string constant. */
private class StringConstant extends SingletonRefinementValue, TStringConstant {
  string value;

  StringConstant() { this = TStringConstant(value) }

  /** Gets the string represented by this abstract value. */
  string getValue() { this = TStringConstant(result) }

  /** Holds if this is the empty string or the string `"0"`. */
  predicate isEmptyOrZero() {
    value = "" or value = "0"
  }

  override string toString() { result = "'" + value + "'" }

  override string typeof() { result = "string" }

  override boolean getABooleanValue() {
    if value = "" then
      result = false
    else
      result = true
  }

  override predicate equals(SingletonRefinementValue that, boolean isStrict) {
    SingletonRefinementValue.super.equals(that, isStrict) or
    isStrict = false and (
      isEmptyOrZero() and that = TBoolConstant(false)
      or
      value = "1" and that = TBoolConstant(true)
      or
      that = TIntConstant(value.toInt())
    )
  }
}

/** An abstract value representing an integer value. */
private class IntConstant extends SingletonRefinementValue, TIntConstant {
  int value;

  IntConstant() { this = TIntConstant(value) }

  /** Gets the integer value represented by this abstract value. */
  int getValue() { this = TIntConstant(result) }

  override string toString() { result = value.toString() }

  override string typeof() { result = "number" }

  override boolean getABooleanValue() {
    if value = 0 then
      result = false
    else
      result = true
  }

  override predicate equals(SingletonRefinementValue that, boolean isStrict) {
    SingletonRefinementValue.super.equals(that, isStrict) or
    isStrict = false and (
      value = 0 and that = TBoolConstant(false)
      or
      value = 1 and that = TBoolConstant(true)
      or
      value = that.(StringConstant).getValue().toInt()
    )
  }
}

/**
 * An abstract value representing a non-empty string `s` such that
 * `Number(s)` is not `NaN`.
 */
private class NumString extends RefinementValue, TNumString {
  override string toString() { result = "numeric string" }
  override string typeof() { result = "string" }
  override boolean getABooleanValue() { result = true }
}

/**
 * An abstract value representing a string `s` such that `Number(s)`
 * is `NaN`.
 */
private class NonEmptyNonNumString extends RefinementValue, TNonEmptyNonNumString {
  override string toString() { result = "non-empty, non-numeric string" }
  override string typeof() { result = "string" }
  override boolean getABooleanValue() { result = true }
}

/**
 * An abstract value representing a non-zero number.
 */
private class NonZeroNumber extends RefinementValue, TNonZeroNumber {
  override string toString() { result = "non-zero number" }
  override string typeof() { result = "number" }
  override boolean getABooleanValue() { result = true }
}
