// 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 "src/compiler/loop-variable-optimizer.h"

#include "src/compiler/common-operator.h"
#include "src/compiler/graph.h"
#include "src/compiler/node-marker.h"
#include "src/compiler/node-properties.h"
#include "src/compiler/node.h"
#include "src/zone/zone-containers.h"
#include "src/zone/zone.h"

namespace v8 {
namespace internal {
namespace compiler {

// Macro for outputting trace information from representation inference.
#define TRACE(...)                                  \
  do {                                              \
    if (FLAG_trace_turbo_loop) PrintF(__VA_ARGS__); \
  } while (false)

LoopVariableOptimizer::LoopVariableOptimizer(Graph* graph,
                                             CommonOperatorBuilder* common,
                                             Zone* zone)
    : graph_(graph),
      common_(common),
      zone_(zone),
      limits_(graph->NodeCount(), zone),
      induction_vars_(zone) {}

void LoopVariableOptimizer::Run() {
  ZoneQueue<Node*> queue(zone());
  queue.push(graph()->start());
  NodeMarker<bool> queued(graph(), 2);
  while (!queue.empty()) {
    Node* node = queue.front();
    queue.pop();
    queued.Set(node, false);

    DCHECK_NULL(limits_[node->id()]);
    bool all_inputs_visited = true;
    int inputs_end = (node->opcode() == IrOpcode::kLoop)
                         ? kFirstBackedge
                         : node->op()->ControlInputCount();
    for (int i = 0; i < inputs_end; i++) {
      if (limits_[NodeProperties::GetControlInput(node, i)->id()] == nullptr) {
        all_inputs_visited = false;
        break;
      }
    }
    if (!all_inputs_visited) continue;

    VisitNode(node);
    DCHECK_NOT_NULL(limits_[node->id()]);

    // Queue control outputs.
    for (Edge edge : node->use_edges()) {
      if (NodeProperties::IsControlEdge(edge) &&
          edge.from()->op()->ControlOutputCount() > 0) {
        Node* use = edge.from();
        if (use->opcode() == IrOpcode::kLoop &&
            edge.index() != kAssumedLoopEntryIndex) {
          VisitBackedge(node, use);
        } else if (!queued.Get(use)) {
          queue.push(use);
          queued.Set(use, true);
        }
      }
    }
  }
}

class LoopVariableOptimizer::Constraint : public ZoneObject {
 public:
  InductionVariable::ConstraintKind kind() const { return kind_; }
  Node* left() const { return left_; }
  Node* right() const { return right_; }

  const Constraint* next() const { return next_; }

  Constraint(Node* left, InductionVariable::ConstraintKind kind, Node* right,
             const Constraint* next)
      : left_(left), right_(right), kind_(kind), next_(next) {}

 private:
  Node* left_;
  Node* right_;
  InductionVariable::ConstraintKind kind_;
  const Constraint* next_;
};

class LoopVariableOptimizer::VariableLimits : public ZoneObject {
 public:
  static VariableLimits* Empty(Zone* zone) {
    return new (zone) VariableLimits();
  }

  VariableLimits* Copy(Zone* zone) const {
    return new (zone) VariableLimits(this);
  }

  void Add(Node* left, InductionVariable::ConstraintKind kind, Node* right,
           Zone* zone) {
    head_ = new (zone) Constraint(left, kind, right, head_);
    limit_count_++;
  }

  void Merge(const VariableLimits* other) {
    // Change the current condition list to a longest common tail
    // of this condition list and the other list. (The common tail
    // should correspond to the list from the common dominator.)

    // First, we throw away the prefix of the longer list, so that
    // we have lists of the same length.
    size_t other_size = other->limit_count_;
    const Constraint* other_limit = other->head_;
    while (other_size > limit_count_) {
      other_limit = other_limit->next();
      other_size--;
    }
    while (limit_count_ > other_size) {
      head_ = head_->next();
      limit_count_--;
    }

    // Then we go through both lists in lock-step until we find
    // the common tail.
    while (head_ != other_limit) {
      DCHECK(limit_count_ > 0);
      limit_count_--;
      other_limit = other_limit->next();
      head_ = head_->next();
    }
  }

  const Constraint* head() const { return head_; }

 private:
  VariableLimits() {}
  explicit VariableLimits(const VariableLimits* other)
      : head_(other->head_), limit_count_(other->limit_count_) {}

  const Constraint* head_ = nullptr;
  size_t limit_count_ = 0;
};

void InductionVariable::AddUpperBound(Node* bound,
                                      InductionVariable::ConstraintKind kind) {
  if (FLAG_trace_turbo_loop) {
    OFStream os(stdout);
    os << "New upper bound for " << phi()->id() << " (loop "
       << NodeProperties::GetControlInput(phi())->id() << "): " << *bound
       << std::endl;
  }
  upper_bounds_.push_back(Bound(bound, kind));
}

void InductionVariable::AddLowerBound(Node* bound,
                                      InductionVariable::ConstraintKind kind) {
  if (FLAG_trace_turbo_loop) {
    OFStream os(stdout);
    os << "New lower bound for " << phi()->id() << " (loop "
       << NodeProperties::GetControlInput(phi())->id() << "): " << *bound;
  }
  lower_bounds_.push_back(Bound(bound, kind));
}

void LoopVariableOptimizer::VisitBackedge(Node* from, Node* loop) {
  if (loop->op()->ControlInputCount() != 2) return;

  // Go through the constraints, and update the induction variables in
  // this loop if they are involved in the constraint.
  const VariableLimits* limits = limits_[from->id()];
  for (const Constraint* constraint = limits->head(); constraint != nullptr;
       constraint = constraint->next()) {
    if (constraint->left()->opcode() == IrOpcode::kPhi &&
        NodeProperties::GetControlInput(constraint->left()) == loop) {
      auto var = induction_vars_.find(constraint->left()->id());
      if (var != induction_vars_.end()) {
        var->second->AddUpperBound(constraint->right(), constraint->kind());
      }
    }
    if (constraint->right()->opcode() == IrOpcode::kPhi &&
        NodeProperties::GetControlInput(constraint->right()) == loop) {
      auto var = induction_vars_.find(constraint->right()->id());
      if (var != induction_vars_.end()) {
        var->second->AddLowerBound(constraint->left(), constraint->kind());
      }
    }
  }
}

void LoopVariableOptimizer::VisitNode(Node* node) {
  switch (node->opcode()) {
    case IrOpcode::kMerge:
      return VisitMerge(node);
    case IrOpcode::kLoop:
      return VisitLoop(node);
    case IrOpcode::kIfFalse:
      return VisitIf(node, false);
    case IrOpcode::kIfTrue:
      return VisitIf(node, true);
    case IrOpcode::kStart:
      return VisitStart(node);
    case IrOpcode::kLoopExit:
      return VisitLoopExit(node);
    default:
      return VisitOtherControl(node);
  }
}

void LoopVariableOptimizer::VisitMerge(Node* node) {
  // Merge the limits of all incoming edges.
  VariableLimits* merged = limits_[node->InputAt(0)->id()]->Copy(zone());
  for (int i = 1; i < node->InputCount(); i++) {
    merged->Merge(limits_[node->InputAt(i)->id()]);
  }
  limits_[node->id()] = merged;
}

void LoopVariableOptimizer::VisitLoop(Node* node) {
  DetectInductionVariables(node);
  // Conservatively take the limits from the loop entry here.
  return TakeConditionsFromFirstControl(node);
}

void LoopVariableOptimizer::VisitIf(Node* node, bool polarity) {
  Node* branch = node->InputAt(0);
  Node* cond = branch->InputAt(0);
  VariableLimits* limits = limits_[branch->id()]->Copy(zone());
  // Normalize to less than comparison.
  switch (cond->opcode()) {
    case IrOpcode::kJSLessThan:
    case IrOpcode::kSpeculativeNumberLessThan:
      AddCmpToLimits(limits, cond, InductionVariable::kStrict, polarity);
      break;
    case IrOpcode::kJSGreaterThan:
      AddCmpToLimits(limits, cond, InductionVariable::kNonStrict, !polarity);
      break;
    case IrOpcode::kJSLessThanOrEqual:
    case IrOpcode::kSpeculativeNumberLessThanOrEqual:
      AddCmpToLimits(limits, cond, InductionVariable::kNonStrict, polarity);
      break;
    case IrOpcode::kJSGreaterThanOrEqual:
      AddCmpToLimits(limits, cond, InductionVariable::kStrict, !polarity);
      break;
    default:
      break;
  }
  limits_[node->id()] = limits;
}

void LoopVariableOptimizer::AddCmpToLimits(
    VariableLimits* limits, Node* node, InductionVariable::ConstraintKind kind,
    bool polarity) {
  Node* left = node->InputAt(0);
  Node* right = node->InputAt(1);
  if (FindInductionVariable(left) || FindInductionVariable(right)) {
    if (polarity) {
      limits->Add(left, kind, right, zone());
    } else {
      kind = (kind == InductionVariable::kStrict)
                 ? InductionVariable::kNonStrict
                 : InductionVariable::kStrict;
      limits->Add(right, kind, left, zone());
    }
  }
}

void LoopVariableOptimizer::VisitStart(Node* node) {
  limits_[node->id()] = VariableLimits::Empty(zone());
}

void LoopVariableOptimizer::VisitLoopExit(Node* node) {
  return TakeConditionsFromFirstControl(node);
}

void LoopVariableOptimizer::VisitOtherControl(Node* node) {
  DCHECK_EQ(1, node->op()->ControlInputCount());
  return TakeConditionsFromFirstControl(node);
}

void LoopVariableOptimizer::TakeConditionsFromFirstControl(Node* node) {
  const VariableLimits* limits =
      limits_[NodeProperties::GetControlInput(node, 0)->id()];
  DCHECK_NOT_NULL(limits);
  limits_[node->id()] = limits;
}

const InductionVariable* LoopVariableOptimizer::FindInductionVariable(
    Node* node) {
  auto var = induction_vars_.find(node->id());
  if (var != induction_vars_.end()) {
    return var->second;
  }
  return nullptr;
}

InductionVariable* LoopVariableOptimizer::TryGetInductionVariable(Node* phi) {
  DCHECK_EQ(2, phi->op()->ValueInputCount());
  DCHECK_EQ(IrOpcode::kLoop, NodeProperties::GetControlInput(phi)->opcode());
  Node* initial = phi->InputAt(0);
  Node* arith = phi->InputAt(1);
  InductionVariable::ArithmeticType arithmeticType;
  if (arith->opcode() == IrOpcode::kJSAdd ||
      arith->opcode() == IrOpcode::kSpeculativeNumberAdd ||
      arith->opcode() == IrOpcode::kSpeculativeSafeIntegerAdd) {
    arithmeticType = InductionVariable::ArithmeticType::kAddition;
  } else if (arith->opcode() == IrOpcode::kJSSubtract ||
             arith->opcode() == IrOpcode::kSpeculativeNumberSubtract) {
    arithmeticType = InductionVariable::ArithmeticType::kSubtraction;
  } else {
    return nullptr;
  }

  // TODO(jarin) Support both sides.
  if (arith->InputAt(0) != phi) {
    if ((arith->InputAt(0)->opcode() != IrOpcode::kJSToNumber &&
         arith->InputAt(0)->opcode() != IrOpcode::kSpeculativeToNumber) ||
        arith->InputAt(0)->InputAt(0) != phi) {
      return nullptr;
    }
  }
  Node* incr = arith->InputAt(1);
  return new (zone())
      InductionVariable(phi, arith, incr, initial, zone(), arithmeticType);
}

void LoopVariableOptimizer::DetectInductionVariables(Node* loop) {
  if (loop->op()->ControlInputCount() != 2) return;
  TRACE("Loop variables for loop %i:", loop->id());
  for (Edge edge : loop->use_edges()) {
    if (NodeProperties::IsControlEdge(edge) &&
        edge.from()->opcode() == IrOpcode::kPhi) {
      Node* phi = edge.from();
      InductionVariable* induction_var = TryGetInductionVariable(phi);
      if (induction_var) {
        induction_vars_[phi->id()] = induction_var;
        TRACE(" %i", induction_var->phi()->id());
      }
    }
  }
  TRACE("\n");
}

void LoopVariableOptimizer::ChangeToInductionVariablePhis() {
  for (auto entry : induction_vars_) {
    // It only make sense to analyze the induction variables if
    // there is a bound.
    InductionVariable* induction_var = entry.second;
    DCHECK_EQ(MachineRepresentation::kTagged,
              PhiRepresentationOf(induction_var->phi()->op()));
    if (induction_var->upper_bounds().size() == 0 &&
        induction_var->lower_bounds().size() == 0) {
      continue;
    }
    // Insert the increment value to the value inputs.
    induction_var->phi()->InsertInput(graph()->zone(),
                                      induction_var->phi()->InputCount() - 1,
                                      induction_var->increment());
    // Insert the bound inputs to the value inputs.
    for (auto bound : induction_var->lower_bounds()) {
      induction_var->phi()->InsertInput(
          graph()->zone(), induction_var->phi()->InputCount() - 1, bound.bound);
    }
    for (auto bound : induction_var->upper_bounds()) {
      induction_var->phi()->InsertInput(
          graph()->zone(), induction_var->phi()->InputCount() - 1, bound.bound);
    }
    NodeProperties::ChangeOp(
        induction_var->phi(),
        common()->InductionVariablePhi(induction_var->phi()->InputCount() - 1));
  }
}

void LoopVariableOptimizer::ChangeToPhisAndInsertGuards() {
  for (auto entry : induction_vars_) {
    InductionVariable* induction_var = entry.second;
    if (induction_var->phi()->opcode() == IrOpcode::kInductionVariablePhi) {
      // Turn the induction variable phi back to normal phi.
      int value_count = 2;
      Node* control = NodeProperties::GetControlInput(induction_var->phi());
      DCHECK_EQ(value_count, control->op()->ControlInputCount());
      induction_var->phi()->TrimInputCount(value_count + 1);
      induction_var->phi()->ReplaceInput(value_count, control);
      NodeProperties::ChangeOp(
          induction_var->phi(),
          common()->Phi(MachineRepresentation::kTagged, value_count));

      // If the backedge is not a subtype of the phi's type, we insert a sigma
      // to get the typing right.
      Node* backedge_value = induction_var->phi()->InputAt(1);
      Type* backedge_type = NodeProperties::GetType(backedge_value);
      Type* phi_type = NodeProperties::GetType(induction_var->phi());
      if (!backedge_type->Is(phi_type)) {
        Node* backedge_control =
            NodeProperties::GetControlInput(induction_var->phi())->InputAt(1);
        Node* rename = graph()->NewNode(common()->TypeGuard(phi_type),
                                        backedge_value, backedge_control);
        induction_var->phi()->ReplaceInput(1, rename);
      }
    }
  }
}

#undef TRACE

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