// Copyright 2016 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <iterator>

#include "src/compiler/store-store-elimination.h"

#include "src/compiler/all-nodes.h"
#include "src/compiler/js-graph.h"
#include "src/compiler/node-properties.h"
#include "src/compiler/simplified-operator.h"

namespace v8 {
namespace internal {
namespace compiler {

#define TRACE(fmt, ...)                                         \
  do {                                                          \
    if (FLAG_trace_store_elimination) {                         \
      PrintF("RedundantStoreFinder: " fmt "\n", ##__VA_ARGS__); \
    }                                                           \
  } while (false)

// CHECK_EXTRA is like CHECK, but has two or more arguments: a boolean
// expression, a format string, and any number of extra arguments. The boolean
// expression will be evaluated at runtime. If it evaluates to false, then an
// error message will be shown containing the condition, as well as the extra
// info formatted like with printf.
#define CHECK_EXTRA(condition, fmt, ...)                                 \
  do {                                                                   \
    if (V8_UNLIKELY(!(condition))) {                                     \
      V8_Fatal(__FILE__, __LINE__, "Check failed: %s. Extra info: " fmt, \
               #condition, ##__VA_ARGS__);                               \
    }                                                                    \
  } while (0)

#ifdef DEBUG
#define DCHECK_EXTRA(condition, fmt, ...) \
  CHECK_EXTRA(condition, fmt, ##__VA_ARGS__)
#else
#define DCHECK_EXTRA(condition, fmt, ...) ((void)0)
#endif

// Store-store elimination.
//
// The aim of this optimization is to detect the following pattern in the
// effect graph:
//
// - StoreField[+24, kRepTagged](263, ...)
//
//   ... lots of nodes from which the field at offset 24 of the object
//       returned by node #263 cannot be observed ...
//
// - StoreField[+24, kRepTagged](263, ...)
//
// In such situations, the earlier StoreField cannot be observed, and can be
// eliminated. This optimization should work for any offset and input node, of
// course.
//
// The optimization also works across splits. It currently does not work for
// loops, because we tend to put a stack check in loops, and like deopts,
// stack checks can observe anything.

// Assumption: every byte of a JS object is only ever accessed through one
// offset. For instance, byte 15 of a given object may be accessed using a
// two-byte read at offset 14, or a four-byte read at offset 12, but never
// both in the same program.
//
// This implementation needs all dead nodes removed from the graph, and the
// graph should be trimmed.

namespace {

typedef uint32_t StoreOffset;

struct UnobservableStore {
  NodeId id_;
  StoreOffset offset_;

  bool operator==(const UnobservableStore) const;
  bool operator!=(const UnobservableStore) const;
  bool operator<(const UnobservableStore) const;
};

}  // namespace

namespace {

// Instances of UnobservablesSet are immutable. They represent either a set of
// UnobservableStores, or the "unvisited empty set".
//
// We apply some sharing to save memory. The class UnobservablesSet is only a
// pointer wide, and a copy does not use any heap (or temp_zone) memory. Most
// changes to an UnobservablesSet might allocate in the temp_zone.
//
// The size of an instance should be the size of a pointer, plus additional
// space in the zone in the case of non-unvisited UnobservablesSets. Copying
// an UnobservablesSet allocates no memory.
class UnobservablesSet final {
 public:
  static UnobservablesSet Unvisited();
  static UnobservablesSet VisitedEmpty(Zone* zone);
  UnobservablesSet();  // unvisited
  UnobservablesSet(const UnobservablesSet& other) : set_(other.set_) {}

  UnobservablesSet Intersect(UnobservablesSet other, Zone* zone) const;
  UnobservablesSet Add(UnobservableStore obs, Zone* zone) const;
  UnobservablesSet RemoveSameOffset(StoreOffset off, Zone* zone) const;

  const ZoneSet<UnobservableStore>* set() const { return set_; }

  bool IsUnvisited() const { return set_ == nullptr; }
  bool IsEmpty() const { return set_ == nullptr || set_->empty(); }
  bool Contains(UnobservableStore obs) const {
    return set_ != nullptr && (set_->find(obs) != set_->end());
  }

  bool operator==(const UnobservablesSet&) const;
  bool operator!=(const UnobservablesSet&) const;

 private:
  explicit UnobservablesSet(const ZoneSet<UnobservableStore>* set)
      : set_(set) {}
  const ZoneSet<UnobservableStore>* set_;
};

}  // namespace

namespace {

class RedundantStoreFinder final {
 public:
  RedundantStoreFinder(JSGraph* js_graph, Zone* temp_zone);

  void Find();

  const ZoneSet<Node*>& to_remove_const() { return to_remove_; }

  void Visit(Node* node);

 private:
  static bool IsEffectful(Node* node);
  void VisitEffectfulNode(Node* node);
  UnobservablesSet RecomputeUseIntersection(Node* node);
  UnobservablesSet RecomputeSet(Node* node, UnobservablesSet uses);
  static bool CannotObserveStoreField(Node* node);

  void MarkForRevisit(Node* node);
  bool HasBeenVisited(Node* node);

  JSGraph* jsgraph() const { return jsgraph_; }
  Zone* temp_zone() const { return temp_zone_; }
  ZoneVector<UnobservablesSet>& unobservable() { return unobservable_; }
  UnobservablesSet& unobservable_for_id(NodeId id) {
    DCHECK_LT(id, unobservable().size());
    return unobservable()[id];
  }
  ZoneSet<Node*>& to_remove() { return to_remove_; }

  JSGraph* const jsgraph_;
  Zone* const temp_zone_;

  ZoneStack<Node*> revisit_;
  ZoneVector<bool> in_revisit_;
  // Maps node IDs to UnobservableNodeSets.
  ZoneVector<UnobservablesSet> unobservable_;
  ZoneSet<Node*> to_remove_;
  const UnobservablesSet unobservables_visited_empty_;
};

// To safely cast an offset from a FieldAccess, which has a potentially wider
// range (namely int).
StoreOffset ToOffset(int offset) {
  CHECK(0 <= offset);
  return static_cast<StoreOffset>(offset);
}

StoreOffset ToOffset(const FieldAccess& access) {
  return ToOffset(access.offset);
}

unsigned int RepSizeOf(MachineRepresentation rep) {
  return 1u << ElementSizeLog2Of(rep);
}
unsigned int RepSizeOf(FieldAccess access) {
  return RepSizeOf(access.machine_type.representation());
}

bool AtMostTagged(FieldAccess access) {
  return RepSizeOf(access) <= RepSizeOf(MachineRepresentation::kTagged);
}

bool AtLeastTagged(FieldAccess access) {
  return RepSizeOf(access) >= RepSizeOf(MachineRepresentation::kTagged);
}

}  // namespace

void RedundantStoreFinder::Find() {
  Visit(jsgraph()->graph()->end());

  while (!revisit_.empty()) {
    Node* next = revisit_.top();
    revisit_.pop();
    DCHECK_LT(next->id(), in_revisit_.size());
    in_revisit_[next->id()] = false;
    Visit(next);
  }

#ifdef DEBUG
  // Check that we visited all the StoreFields
  AllNodes all(temp_zone(), jsgraph()->graph());
  for (Node* node : all.reachable) {
    if (node->op()->opcode() == IrOpcode::kStoreField) {
      DCHECK_EXTRA(HasBeenVisited(node), "#%d:%s", node->id(),
                   node->op()->mnemonic());
    }
  }
#endif
}

void RedundantStoreFinder::MarkForRevisit(Node* node) {
  DCHECK_LT(node->id(), in_revisit_.size());
  if (!in_revisit_[node->id()]) {
    revisit_.push(node);
    in_revisit_[node->id()] = true;
  }
}

bool RedundantStoreFinder::HasBeenVisited(Node* node) {
  return !unobservable_for_id(node->id()).IsUnvisited();
}

void StoreStoreElimination::Run(JSGraph* js_graph, Zone* temp_zone) {
  // Find superfluous nodes
  RedundantStoreFinder finder(js_graph, temp_zone);
  finder.Find();

  // Remove superfluous nodes

  for (Node* node : finder.to_remove_const()) {
    if (FLAG_trace_store_elimination) {
      PrintF("StoreStoreElimination::Run: Eliminating node #%d:%s\n",
             node->id(), node->op()->mnemonic());
    }
    Node* previous_effect = NodeProperties::GetEffectInput(node);
    NodeProperties::ReplaceUses(node, nullptr, previous_effect, nullptr,
                                nullptr);
    node->Kill();
  }
}

bool RedundantStoreFinder::IsEffectful(Node* node) {
  return (node->op()->EffectInputCount() >= 1);
}

// Recompute unobservables-set for a node. Will also mark superfluous nodes
// as to be removed.

UnobservablesSet RedundantStoreFinder::RecomputeSet(Node* node,
                                                    UnobservablesSet uses) {
  switch (node->op()->opcode()) {
    case IrOpcode::kStoreField: {
      Node* stored_to = node->InputAt(0);
      FieldAccess access = OpParameter<FieldAccess>(node->op());
      StoreOffset offset = ToOffset(access);

      UnobservableStore observation = {stored_to->id(), offset};
      bool isNotObservable = uses.Contains(observation);

      if (isNotObservable && AtMostTagged(access)) {
        TRACE("  #%d is StoreField[+%d,%s](#%d), unobservable", node->id(),
              offset, MachineReprToString(access.machine_type.representation()),
              stored_to->id());
        to_remove().insert(node);
        return uses;
      } else if (isNotObservable && !AtMostTagged(access)) {
        TRACE(
            "  #%d is StoreField[+%d,%s](#%d), repeated in future but too "
            "big to optimize away",
            node->id(), offset,
            MachineReprToString(access.machine_type.representation()),
            stored_to->id());
        return uses;
      } else if (!isNotObservable && AtLeastTagged(access)) {
        TRACE("  #%d is StoreField[+%d,%s](#%d), observable, recording in set",
              node->id(), offset,
              MachineReprToString(access.machine_type.representation()),
              stored_to->id());
        return uses.Add(observation, temp_zone());
      } else if (!isNotObservable && !AtLeastTagged(access)) {
        TRACE(
            "  #%d is StoreField[+%d,%s](#%d), observable but too small to "
            "record",
            node->id(), offset,
            MachineReprToString(access.machine_type.representation()),
            stored_to->id());
        return uses;
      } else {
        UNREACHABLE();
      }
      break;
    }
    case IrOpcode::kLoadField: {
      Node* loaded_from = node->InputAt(0);
      FieldAccess access = OpParameter<FieldAccess>(node->op());
      StoreOffset offset = ToOffset(access);

      TRACE(
          "  #%d is LoadField[+%d,%s](#%d), removing all offsets [+%d] from "
          "set",
          node->id(), offset,
          MachineReprToString(access.machine_type.representation()),
          loaded_from->id(), offset);

      return uses.RemoveSameOffset(offset, temp_zone());
      break;
    }
    default:
      if (CannotObserveStoreField(node)) {
        TRACE("  #%d:%s can observe nothing, set stays unchanged", node->id(),
              node->op()->mnemonic());
        return uses;
      } else {
        TRACE("  #%d:%s might observe anything, recording empty set",
              node->id(), node->op()->mnemonic());
        return unobservables_visited_empty_;
      }
  }
  UNREACHABLE();
  return UnobservablesSet::Unvisited();
}

bool RedundantStoreFinder::CannotObserveStoreField(Node* node) {
  return node->opcode() == IrOpcode::kCheckedLoad ||
         node->opcode() == IrOpcode::kLoadElement ||
         node->opcode() == IrOpcode::kLoad ||
         node->opcode() == IrOpcode::kStore ||
         node->opcode() == IrOpcode::kEffectPhi ||
         node->opcode() == IrOpcode::kStoreElement ||
         node->opcode() == IrOpcode::kCheckedStore ||
         node->opcode() == IrOpcode::kUnsafePointerAdd ||
         node->opcode() == IrOpcode::kRetain;
}

// Initialize unobservable_ with js_graph->graph->NodeCount() empty sets.
RedundantStoreFinder::RedundantStoreFinder(JSGraph* js_graph, Zone* temp_zone)
    : jsgraph_(js_graph),
      temp_zone_(temp_zone),
      revisit_(temp_zone),
      in_revisit_(js_graph->graph()->NodeCount(), temp_zone),
      unobservable_(js_graph->graph()->NodeCount(),
                    UnobservablesSet::Unvisited(), temp_zone),
      to_remove_(temp_zone),
      unobservables_visited_empty_(UnobservablesSet::VisitedEmpty(temp_zone)) {}

void RedundantStoreFinder::Visit(Node* node) {
  // All effectful nodes should be reachable from End via a sequence of
  // control, then a sequence of effect edges. In VisitEffectfulNode we mark
  // all effect inputs for revisiting (if they might have stale state); here
  // we mark all control inputs at least once.

  if (!HasBeenVisited(node)) {
    for (int i = 0; i < node->op()->ControlInputCount(); i++) {
      Node* control_input = NodeProperties::GetControlInput(node, i);
      if (!HasBeenVisited(control_input)) {
        MarkForRevisit(control_input);
      }
    }
  }

  bool isEffectful = (node->op()->EffectInputCount() >= 1);
  if (isEffectful) {
    VisitEffectfulNode(node);
    DCHECK(HasBeenVisited(node));
  }

  if (!HasBeenVisited(node)) {
    // Mark as visited.
    unobservable_for_id(node->id()) = unobservables_visited_empty_;
  }
}

void RedundantStoreFinder::VisitEffectfulNode(Node* node) {
  if (HasBeenVisited(node)) {
    TRACE("- Revisiting: #%d:%s", node->id(), node->op()->mnemonic());
  }
  UnobservablesSet after_set = RecomputeUseIntersection(node);
  UnobservablesSet before_set = RecomputeSet(node, after_set);
  DCHECK(!before_set.IsUnvisited());

  UnobservablesSet stored_for_node = unobservable_for_id(node->id());
  bool cur_set_changed =
      (stored_for_node.IsUnvisited() || stored_for_node != before_set);
  if (!cur_set_changed) {
    // We will not be able to update the part of this chain above any more.
    // Exit.
    TRACE("+ No change: stabilized. Not visiting effect inputs.");
  } else {
    unobservable_for_id(node->id()) = before_set;

    // Mark effect inputs for visiting.
    for (int i = 0; i < node->op()->EffectInputCount(); i++) {
      Node* input = NodeProperties::GetEffectInput(node, i);
      TRACE("    marking #%d:%s for revisit", input->id(),
            input->op()->mnemonic());
      MarkForRevisit(input);
    }
  }
}

// Compute the intersection of the UnobservablesSets of all effect uses and
// return it. This function only works if {node} has an effect use.
//
// The result UnobservablesSet will always be visited.
UnobservablesSet RedundantStoreFinder::RecomputeUseIntersection(Node* node) {
  // {first} == true indicates that we haven't looked at any elements yet.
  // {first} == false indicates that cur_set is the intersection of at least one
  // thing.

  bool first = true;
  UnobservablesSet cur_set = UnobservablesSet::Unvisited();  // irrelevant

  for (Edge edge : node->use_edges()) {
    // Skip non-effect edges
    if (!NodeProperties::IsEffectEdge(edge)) {
      continue;
    }

    Node* use = edge.from();
    UnobservablesSet new_set = unobservable_for_id(use->id());
    // Include new_set in the intersection.
    if (first) {
      // Intersection of a one-element set is that one element
      first = false;
      cur_set = new_set;
    } else {
      // Take the intersection of cur_set and new_set.
      cur_set = cur_set.Intersect(new_set, temp_zone());
    }
  }

  if (first) {
    // There were no effect uses.
    auto opcode = node->op()->opcode();
    // List of opcodes that may end this effect chain. The opcodes are not
    // important to the soundness of this optimization; this serves as a
    // general sanity check. Add opcodes to this list as it suits you.
    //
    // Everything is observable after these opcodes; return the empty set.
    DCHECK_EXTRA(
        opcode == IrOpcode::kReturn || opcode == IrOpcode::kTerminate ||
            opcode == IrOpcode::kDeoptimize || opcode == IrOpcode::kThrow,
        "for #%d:%s", node->id(), node->op()->mnemonic());
    USE(opcode);  // silence warning about unused variable in release mode

    return unobservables_visited_empty_;
  } else {
    if (cur_set.IsUnvisited()) {
      cur_set = unobservables_visited_empty_;
    }

    return cur_set;
  }
}

UnobservablesSet UnobservablesSet::Unvisited() { return UnobservablesSet(); }

UnobservablesSet::UnobservablesSet() : set_(nullptr) {}

UnobservablesSet UnobservablesSet::VisitedEmpty(Zone* zone) {
  // Create a new empty UnobservablesSet. This allocates in the zone, and
  // can probably be optimized to use a global singleton.
  ZoneSet<UnobservableStore>* empty_set =
      new (zone->New(sizeof(ZoneSet<UnobservableStore>)))
          ZoneSet<UnobservableStore>(zone);
  return UnobservablesSet(empty_set);
}

// Computes the intersection of two UnobservablesSets. May return
// UnobservablesSet::Unvisited() instead of an empty UnobservablesSet for
// speed.
UnobservablesSet UnobservablesSet::Intersect(UnobservablesSet other,
                                             Zone* zone) const {
  if (IsEmpty() || other.IsEmpty()) {
    return Unvisited();
  } else {
    ZoneSet<UnobservableStore>* intersection =
        new (zone->New(sizeof(ZoneSet<UnobservableStore>)))
            ZoneSet<UnobservableStore>(zone);
    // Put the intersection of set() and other.set() in intersection.
    set_intersection(set()->begin(), set()->end(), other.set()->begin(),
                     other.set()->end(),
                     std::inserter(*intersection, intersection->end()));

    return UnobservablesSet(intersection);
  }
}

UnobservablesSet UnobservablesSet::Add(UnobservableStore obs,
                                       Zone* zone) const {
  bool present = (set()->find(obs) != set()->end());
  if (present) {
    return *this;
  } else {
    // Make a new empty set.
    ZoneSet<UnobservableStore>* new_set =
        new (zone->New(sizeof(ZoneSet<UnobservableStore>)))
            ZoneSet<UnobservableStore>(zone);
    // Copy the old elements over.
    *new_set = *set();
    // Add the new element.
    bool inserted = new_set->insert(obs).second;
    DCHECK(inserted);
    USE(inserted);  // silence warning about unused variable

    return UnobservablesSet(new_set);
  }
}

UnobservablesSet UnobservablesSet::RemoveSameOffset(StoreOffset offset,
                                                    Zone* zone) const {
  // Make a new empty set.
  ZoneSet<UnobservableStore>* new_set =
      new (zone->New(sizeof(ZoneSet<UnobservableStore>)))
          ZoneSet<UnobservableStore>(zone);
  // Copy all elements over that have a different offset.
  for (auto obs : *set()) {
    if (obs.offset_ != offset) {
      new_set->insert(obs);
    }
  }

  return UnobservablesSet(new_set);
}

// Used for debugging.
bool UnobservablesSet::operator==(const UnobservablesSet& other) const {
  if (IsUnvisited() || other.IsUnvisited()) {
    return IsEmpty() && other.IsEmpty();
  } else {
    // Both pointers guaranteed not to be nullptrs.
    return *set() == *other.set();
  }
}

bool UnobservablesSet::operator!=(const UnobservablesSet& other) const {
  return !(*this == other);
}

bool UnobservableStore::operator==(const UnobservableStore other) const {
  return (id_ == other.id_) && (offset_ == other.offset_);
}

bool UnobservableStore::operator!=(const UnobservableStore other) const {
  return !(*this == other);
}

bool UnobservableStore::operator<(const UnobservableStore other) const {
  return (id_ < other.id_) || (id_ == other.id_ && offset_ < other.offset_);
}

}  // namespace compiler
}  // namespace internal
}  // namespace v8