//===- subzero/src/IceRangeSpec.cpp - Include/exclude specification -------===//
//
//                        The Subzero Code Generator
//
// This file is distributed under the University of Illinois Open Source
// License. See LICENSE.TXT for details.
//
//===----------------------------------------------------------------------===//
///
/// \file
/// \brief Implements a class for specifying sets of names and number ranges to
/// match against.  This is specified as a comma-separated list of clauses.
/// Each clause optionally starts with '-' to indicate exclusion instead of
/// inclusion.  A clause can be a name, or a numeric range X:Y, or a single
/// number X.  The X:Y form indicates a range of numbers greater than or equal
/// to X and strictly less than Y.  A missing "X" is taken to be 0, and a
/// missing "Y" is taken to be infinite.  E.g., "0:" and ":" specify the entire
/// set.
///
/// This is essentially the same implementation as in szbuild.py, except that
/// regular expressions are not used for the names.
///
//===----------------------------------------------------------------------===//

#include "IceRangeSpec.h"
#include "IceStringPool.h"

#include <cctype>
#include <string>
#include <unordered_set>
#include <vector>

namespace Ice {

bool RangeSpec::HasNames = false;

namespace {

/// Helper function to parse "X" or "X:Y" into First and Last.
/// - "X" is treated as "X:X+1".
/// - ":Y" is treated as "0:Y".
/// - "X:" is treated as "X:inf"
///
/// Behavior is undefined if "X" or "Y" is not a proper number (since std::stoul
/// throws an exception).
///
/// If the string doesn't contain 1 or 2 ':' delimiters, or X>=Y,
/// report_fatal_error is called.
void getRange(const std::string &Token, uint32_t *First, uint32_t *Last) {
  bool Error = false;
  auto Tokens = RangeSpec::tokenize(Token, RangeSpec::DELIM_RANGE);
  if (Tokens.size() == 1) {
    *First = std::stoul(Tokens[0]);
    *Last = *First + 1;
  } else if (Tokens.size() == 2) {
    *First = Tokens[0].empty() ? 0 : std::stoul(Tokens[0]);
    *Last = Tokens[1].empty() ? RangeSpec::RangeMax : std::stoul(Tokens[1]);
  } else {
    Error = true;
  }
  if (*First >= *Last) {
    Error = true;
  }
  if (Error) {
    llvm::report_fatal_error("Invalid range " + Token);
  }
}

/// Helper function to add one token to the include or exclude set.  The token
/// is examined and then treated as either a numeric range or a single name.
void record(const std::string &Token, RangeSpec::Desc *D) {
  if (Token.empty())
    return;
  // Mark that an include or exclude was explicitly given.  This affects the
  // default decision when matching a value that wasn't explicitly provided in
  // the include or exclude list.
  D->IsExplicit = true;
  // A range is identified by starting with a digit or a ':'.
  if (Token[0] == RangeSpec::DELIM_RANGE || std::isdigit(Token[0])) {
    uint32_t First, Last;
    getRange(Token, &First, &Last);
    if (Last == RangeSpec::RangeMax) {
      D->AllFrom = std::min(D->AllFrom, First);
    } else {
      if (Last >= D->Numbers.size())
        D->Numbers.resize(Last + 1);
      D->Numbers.set(First, Last);
    }
  } else {
    // Otherwise treat it as a single name.
    D->Names.insert(Token);
  }
}

} // end of anonymous namespace

std::vector<std::string> RangeSpec::tokenize(const std::string &Spec,
                                             char Delimiter) {
  std::vector<std::string> Tokens;
  if (!Spec.empty()) {
    std::string::size_type StartPos = 0;
    std::string::size_type DelimPos = 0;
    while (DelimPos != std::string::npos) {
      DelimPos = Spec.find(Delimiter, StartPos);
      Tokens.emplace_back(Spec.substr(StartPos, DelimPos - StartPos));
      StartPos = DelimPos + 1;
    }
  }
  return Tokens;
}

/// Initialize the RangeSpec with the given string.  Calling init multiple times
/// (e.g. init("A");init("B");) is equivalent to init("A,B"); .
void RangeSpec::init(const std::string &Spec) {
  auto Tokens = tokenize(Spec, DELIM_LIST);
  for (const auto &Token : Tokens) {
    if (Token[0] == '-') {
      exclude(Token.substr(1));
    } else {
      include(Token);
    }
  }
  if (!Includes.Names.empty() || !Excludes.Names.empty())
    HasNames = true;
}

/// Determine whether the given Name/Number combo match the specification given
/// to the init() method.  Explicit excludes take precedence over explicit
/// includes.  If the combo doesn't match any explicit include or exclude:
/// - false if the init() string is empty (no explicit includes or excludes)
/// - true if there is at least one explicit exclude and no explicit includes
/// - false otherwise (at least one explicit include)
bool RangeSpec::match(const std::string &Name, uint32_t Number) const {
  // No match if it is explicitly excluded by name or number.
  if (Excludes.Names.find(Name) != Excludes.Names.end())
    return false;
  if (Number >= Excludes.AllFrom)
    return false;
  if (Number < Excludes.Numbers.size() && Excludes.Numbers[Number])
    return false;

  // Positive match if it is explicitly included by name or number.
  if (Includes.Names.find(Name) != Includes.Names.end())
    return true;
  if (Number >= Includes.AllFrom)
    return true;
  if (Number < Includes.Numbers.size() && Includes.Numbers[Number])
    return true;

  // Otherwise use the default decision.
  return Excludes.IsExplicit && !Includes.IsExplicit;
}

void RangeSpec::include(const std::string &Token) { record(Token, &Includes); }

void RangeSpec::exclude(const std::string &Token) { record(Token, &Excludes); }

} // end of namespace Ice