# Copyright 2018 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.


from . import base


# Extra flags randomly added to all fuzz tests with numfuzz. List of tuples
# (probability, flag).
EXTRA_FLAGS = [
    (0.1, '--always-turbofan'),
    (0.1, '--assert-types'),
    (0.1, '--cache=code'),
    (0.1, '--force-slow-path'),
    (0.2, '--future'),
    (0.5, '--harmony'),
    # TODO(v8:13524): Enable when issue is fixed
    # TODO(v8:13528): Enable when issue is fixed
    # (0.1, '--harmony-struct'),
    (0.1, '--jit-fuzzing'),
    (0.5, '--js-staging'),
    (0.1, '--liftoff'),
    (0.1, '--maglev'),
    (0.1, '--maglev-future'),
    (0.1, '--minor-ms'),
    (0.2, '--no-analyze-environment-liveness'),
    # TODO(machenbach): Enable when it doesn't collide with crashing on missing
    # simd features.
    #(0.1, '--no-enable-sse3'),
    #(0.1, '--no-enable-ssse3'),
    #(0.1, '--no-enable-sse4_1'),
    (0.1, '--no-enable-sse4_2'),
    (0.1, '--no-enable-sahf'),
    (0.1, '--no-enable-avx'),
    (0.1, '--no-enable-fma3'),
    (0.1, '--no-enable-bmi1'),
    (0.1, '--no-enable-bmi2'),
    (0.1, '--no-enable-lzcnt'),
    (0.1, '--no-enable-popcnt'),
    (0.3, '--no-lazy-feedback-allocation'),
    (0.1, '--no-liftoff'),
    (0.1, '--no-turbofan'),
    (0.1, '--no-wasm-tier-up'),
    (0.5, '--optimize-on-next-call-optimizes-to-maglev'),
    (0.1, '--regexp-interpret-all'),
    (0.1, '--regexp-tier-up-ticks=0'),
    (0.1, '--regexp-tier-up-ticks=10'),
    (0.1, '--regexp-tier-up-ticks=100'),
    (0.1, '--shared-string-table'),
    (0.1, '--shared-heap'),
    (0.1, '--stress-background-compile'),
    (0.2, '--stress-flush-code'),
    (0.1, '--stress-lazy-source-positions'),
    (0.1, '--stress-maglev'),
    (0.1, '--stress-wasm-code-gc'),
    (0.2, '--turboshaft'),
    (0.1, '--turbolev'),
    (0.1, '--turbo-instruction-scheduling'),
    (0.1, '--turbo-stress-instruction-scheduling'),
    (0.1, '--stress-wasm-memory-moving'),
    (0.1, '--stress-scavenger-conservative-object-pinning-random'),
    (0.1, '--conservative-stack-scanning'),
    (0.1, '--precise-object-pinning'),
    (0.25, '--wasm-staging'),
    (0.1, '--ephemeron-fixpoint-iterations=0'),
    (0.25, '--experimental-wasm-revectorize'),
    (0.5, '--additive-safe-int-feedback'),
]

MIN_DEOPT = 1
MAX_DEOPT = 10**9
ANALYSIS_SUFFIX = 'analysis'


def random_extra_flags(rng):
  """Returns a random list of flags chosen from the configurations in
  EXTRA_FLAGS.
  """
  return [flag for prob, flag in EXTRA_FLAGS if rng.random() < prob]


def _flag_prefix(flag):
  """Returns the flag part before an equal sign."""
  if '=' not in flag:
    return flag
  else:
    return flag[0:flag.index('=')]


def _invert_flag(flag):
  """Flips a --flag and its --no-flag counterpart."""
  assert flag.startswith('--')
  if flag.startswith('--no-'):
    return '--' + flag[len('--no-'):]
  else:
    return '--no-' + flag[2:]


def _drop_contradictory_flags(new_flags, existing_flags):
  """Drops flags that have a simple contradiction with an existing flag.

  Contradictions checked for:
  - Repetition: --flag --flag
  - Repetition with param: --flag=foo --flag=bar
  - Negation: --flag --no-flag
  - Inverse negation: --no-flag --flag
  - For simplicity also drops combinations of negation and param, which don't
    occur in practice.

  Args:
    new_flags: new flags to filter from
    existing_flags: existing flags checked against
  Returns: A list of flags without contradictions.
  """
  existing_flag_prefixes = set(_flag_prefix(flag) for flag in existing_flags)

  def contradictory_flag(flag):
    flag_prefix = _flag_prefix(flag)
    if not flag_prefix.startswith('--'):
      return False
    return (flag_prefix in existing_flag_prefixes or
            _invert_flag(flag_prefix) in existing_flag_prefixes)

  return [flag for flag in new_flags if not contradictory_flag(flag)]


class FuzzerConfig(object):
  def __init__(self, probability, analyzer, fuzzer):
    """
    Args:
      probability: of choosing this fuzzer (0; 10]
      analyzer: instance of Analyzer class, can be None if no analysis is needed
      fuzzer: instance of Fuzzer class
    """
    assert probability > 0 and probability <= 10

    self.probability = probability
    self.analyzer = analyzer
    self.fuzzer = fuzzer


class Analyzer(object):
  def get_analysis_flags(self):
    raise NotImplementedError()

  def do_analysis(self, result):
    raise NotImplementedError()


class Fuzzer(object):
  def create_flags_generator(self, rng, test, analysis_value):
    """
    Args:
      rng: random number generator
      test: test for which to create flags
      analysis_value: value returned by the analyzer. None if there is no
        corresponding analyzer to this fuzzer or the analysis phase is disabled
    """
    raise NotImplementedError()


# TODO(majeski): Allow multiple subtests to run at once.
class FuzzerProc(base.TestProcProducer):
  @staticmethod
  def create(options):
    return FuzzerProc(
        options.fuzzer_rng(),
        options.fuzzer_tests_count(),
        options.fuzzer_configs(),
        options.combine_tests,
    )

  def __init__(self, rng, count, fuzzers, disable_analysis=False):
    """
    Args:
      rng: random number generator used to select flags and values for them
      count: number of tests to generate based on each base test
      fuzzers: list of FuzzerConfig instances
      disable_analysis: disable analysis phase and filtering base on it. When
        set, processor passes None as analysis result to fuzzers
    """
    super(FuzzerProc, self).__init__('Fuzzer')
    self._rng = rng
    self._count = count
    self._fuzzer_configs = fuzzers
    self._disable_analysis = disable_analysis
    self._gens = {}

  def test_suffix(self, test):
    return test.subtest_id

  def _next_test(self, test):
    if self.is_stopped:
      return False

    analysis_subtest = self._create_analysis_subtest(test)
    if analysis_subtest:
      return self._send_test(analysis_subtest)

    self._gens[test.procid] = self._create_gen(test)
    return self._try_send_next_test(test)

  def _create_analysis_subtest(self, test):
    if self._disable_analysis:
      return None

    analysis_flags = []
    for fuzzer_config in self._fuzzer_configs:
      if fuzzer_config.analyzer:
        analysis_flags += fuzzer_config.analyzer.get_analysis_flags()

    if analysis_flags:
      analysis_flags = list(set(analysis_flags))
      return test.create_subtest(
          self, ANALYSIS_SUFFIX, flags=analysis_flags, keep_output=True)

  def _result_for(self, test, subtest, result):
    if not self._disable_analysis:
      if result is not None and subtest.procid.endswith(
          f'{self.name}-{ANALYSIS_SUFFIX}'):
        # Analysis phase, for fuzzing we drop the result.
        if result.has_unexpected_output:
          self._send_result(test, None)
          return

        self._gens[test.procid] = self._create_gen(test, result)

    self._try_send_next_test(test)

  def _create_gen(self, test, analysis_result=None):
    # It will be called with analysis_result==None only when there is no
    # analysis phase at all, so no fuzzer has it's own analyzer.
    gens = []
    indexes = []
    for fuzzer_config in self._fuzzer_configs:
      analysis_value = None
      if analysis_result and fuzzer_config.analyzer:
        analysis_value = fuzzer_config.analyzer.do_analysis(analysis_result)
        if not analysis_value:
          # Skip fuzzer for this test since it doesn't have analysis data
          continue
      p = fuzzer_config.probability
      flag_gen = fuzzer_config.fuzzer.create_flags_generator(self._rng, test,
                                                             analysis_value)
      indexes += [len(gens)] * p
      gens.append((p, flag_gen))

    if not gens:
      # No fuzzers for this test, skip it
      return

    i = 0
    while not self._count or i < self._count:
      main_index = self._rng.choice(indexes)
      _, main_gen = gens[main_index]

      flags = random_extra_flags(self._rng) + next(main_gen)
      for index, (p, gen) in enumerate(gens):
        if index == main_index:
          continue
        if self._rng.randint(1, 10) <= p:
          flags += next(gen)

      flags.append('--fuzzer-random-seed=%s' % self._next_seed())

      flags = _drop_contradictory_flags(flags, test.get_flags())
      yield test.create_subtest(self, str(i), flags=flags)

      i += 1

  def _try_send_next_test(self, test):
    for subtest in self._gens[test.procid]:
      if self._send_test(subtest):
        return True
      elif self.is_stopped:
        return False

    del self._gens[test.procid]
    return False

  def _next_seed(self):
    seed = None
    while not seed:
      seed = self._rng.randint(-2147483648, 2147483647)
    return seed


class ScavengeAnalyzer(Analyzer):
  def get_analysis_flags(self):
    return ['--fuzzer-gc-analysis']

  def do_analysis(self, result):
    for line in reversed(result.output.stdout.splitlines()):
      if line.startswith('### Maximum new space size reached = '):
        return int(float(line.split()[7]))


class ScavengeFuzzer(Fuzzer):
  def create_flags_generator(self, rng, test, analysis_value):
    while True:
      yield ['--stress-scavenge=%d' % (analysis_value or 100)]


class MarkingAnalyzer(Analyzer):
  def get_analysis_flags(self):
    return ['--fuzzer-gc-analysis']

  def do_analysis(self, result):
    for line in reversed(result.output.stdout.splitlines()):
      if line.startswith('### Maximum marking limit reached = '):
        return int(float(line.split()[6]))


class MarkingFuzzer(Fuzzer):
  def create_flags_generator(self, rng, test, analysis_value):
    while True:
      yield ['--stress-marking=%d' % (analysis_value or 100)]


class GcIntervalAnalyzer(Analyzer):
  def get_analysis_flags(self):
    return ['--fuzzer-gc-analysis']

  def do_analysis(self, result):
    for line in reversed(result.output.stdout.splitlines()):
      if line.startswith('### Allocations = '):
        return int(float(line.split()[3][:-1]))


class GcIntervalFuzzer(Fuzzer):
  def create_flags_generator(self, rng, test, analysis_value):
    if analysis_value:
      value = analysis_value // 10
    else:
      value = 10000
    while True:
      yield ['--random-gc-interval=%d' % value]


class CompactionFuzzer(Fuzzer):
  def create_flags_generator(self, rng, test, analysis_value):
    while True:
      yield ['--stress-compaction-random']


class InterruptBudgetFuzzer(Fuzzer):
  def create_flags_generator(self, rng, test, analysis_value):
    while True:
      # Half with, half without lazy feedback allocation. The first flag
      # overwrites potential flag negations from the extra flags list.
      flag1 = rng.choice(
          ['--lazy-feedback-allocation', '--no-lazy-feedback-allocation'])
      flag2 = '--invocation-count-for-turbofan=%d' % rng.randint(0, 240)
      flag3 = '--invocation-count-for-maglev=%d' % rng.randint(0, 120)
      flag4 = '--invocation-count-for-feedback-allocation=%d' % rng.randint(
          0, 8)

      yield [flag1, flag2, flag3, flag4]


class AllocationOffsetFuzzer(Fuzzer):
  """Creates a random number of fake allocations before the actual test."""

  def create_flags_generator(self, rng, test, analysis_value):
    while True:
      n_objects = rng.randint(0, 20)
      n_vars = rng.randint(0, 10)
      n_proxies = rng.randint(0, 4)
      array_size = rng.choice([0, rng.randint(1, 100000)])

      flags = []

      def add(content):
        # Pad with one space so that shell deterministically adds quotations.
        flags.extend(['-e', f' {content}'])

      if n_objects:
        add('[];' * n_objects)
      if n_vars:
        add(' '.join([f'var __pv_{i};' for i in range(n_vars)]))
      if n_proxies:
        add(' '.join([
            f'var __pp_{i} = new Proxy({{}}, {{}});' for i in range(n_proxies)
        ]))
      if array_size:
        add(f'var __pa = new Array({array_size});')

      yield flags

class StackSizeFuzzer(Fuzzer):
  def create_flags_generator(self, rng, test, analysis_value):
    while True:
      yield ['--stack-size=%d' % rng.randint(54, 983)]


class TaskDelayFuzzer(Fuzzer):
  def create_flags_generator(self, rng, test, analysis_value):
    while True:
      yield ['--stress-delay-tasks']


class ThreadPoolSizeFuzzer(Fuzzer):
  def create_flags_generator(self, rng, test, analysis_value):
    while True:
      yield ['--thread-pool-size=%d' % rng.randint(1, 8)]


class DeoptAnalyzer(Analyzer):
  def get_analysis_flags(self):
    return ['--deopt-every-n-times=%d' % MAX_DEOPT,
            '--print-deopt-stress']

  def do_analysis(self, result):
    for line in reversed(result.output.stdout.splitlines()):
      if line.startswith('=== Stress deopt counter: '):
        counter = MAX_DEOPT - int(line.split(' ')[-1])
        if counter < MIN_DEOPT:
          # Skip this test since we won't generate any meaningful interval with
          # given minimum.
          return None
        return counter


class DeoptFuzzer(Fuzzer):
  def create_flags_generator(self, rng, test, analysis_value):
    while True:
      if analysis_value:
        value = analysis_value // 2
      else:
        value = 10000
      interval = rng.randint(MIN_DEOPT, max(value, MIN_DEOPT))
      yield ['--deopt-every-n-times=%d' % interval]


FUZZERS = {
    'allocation': (None, AllocationOffsetFuzzer),
    'compaction': (None, CompactionFuzzer),
    'delay': (None, TaskDelayFuzzer),
    'deopt': (DeoptAnalyzer, DeoptFuzzer),
    'gc_interval': (GcIntervalAnalyzer, GcIntervalFuzzer),
    'interrupt': (None, InterruptBudgetFuzzer),
    'marking': (MarkingAnalyzer, MarkingFuzzer),
    'scavenge': (ScavengeAnalyzer, ScavengeFuzzer),
    'stack': (None, StackSizeFuzzer),
    'threads': (None, ThreadPoolSizeFuzzer),
}


def create_fuzzer_config(name, probability, *args, **kwargs):
  analyzer_class, fuzzer_class = FUZZERS[name]
  return FuzzerConfig(
      probability,
      analyzer_class(*args, **kwargs) if analyzer_class else None,
      fuzzer_class(*args, **kwargs),
  )
