# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Tests for dependency_checker.py."""

import unittest
from compiler.front_end import dependency_checker
from compiler.front_end import glue
from compiler.util import error
from compiler.util import test_util


def _parse_snippet(emb_file):
  ir, unused_debug_info, errors = glue.parse_emboss_file(
      "m.emb",
      test_util.dict_file_reader({"m.emb": emb_file}),
      stop_before_step="find_dependency_cycles")
  assert not errors
  return ir


def _find_dependencies_for_snippet(emb_file):
  ir, unused_debug_info, errors = glue.parse_emboss_file(
      "m.emb",
      test_util.dict_file_reader({
          "m.emb": emb_file
      }),
      stop_before_step="set_dependency_order")
  assert not errors, errors
  return ir


class DependencyCheckerTest(unittest.TestCase):

  def test_error_on_simple_field_cycle(self):
    ir = _parse_snippet("struct Foo:\n"
                        "  0 [+field2]  UInt  field1\n"
                        "  0 [+field1]  UInt  field2\n")
    struct = ir.module[0].type[0].structure
    self.assertEqual([[
        error.error("m.emb", struct.field[0].source_location,
                    "Dependency cycle\nfield1"),
        error.note("m.emb", struct.field[1].source_location, "field2")
    ]], dependency_checker.find_dependency_cycles(ir))

  def test_error_on_self_cycle(self):
    ir = _parse_snippet("struct Foo:\n"
                        "  0 [+field1]  UInt  field1\n")
    struct = ir.module[0].type[0].structure
    self.assertEqual([[
        error.error("m.emb", struct.field[0].source_location,
                    "Dependency cycle\nfield1")
    ]], dependency_checker.find_dependency_cycles(ir))

  def test_error_on_triple_field_cycle(self):
    ir = _parse_snippet("struct Foo:\n"
                        "  0 [+field2]  UInt  field1\n"
                        "  0 [+field3]  UInt  field2\n"
                        "  0 [+field1]  UInt  field3\n")
    struct = ir.module[0].type[0].structure
    self.assertEqual([[
        error.error("m.emb", struct.field[0].source_location,
                    "Dependency cycle\nfield1"),
        error.note("m.emb", struct.field[1].source_location, "field2"),
        error.note("m.emb", struct.field[2].source_location, "field3"),
    ]], dependency_checker.find_dependency_cycles(ir))

  def test_error_on_complex_field_cycle(self):
    ir = _parse_snippet("struct Foo:\n"
                        "  0 [+field2]         UInt  field1\n"
                        "  0 [+field3+field4]  UInt  field2\n"
                        "  0 [+field1]         UInt  field3\n"
                        "  0 [+field2]         UInt  field4\n")
    struct = ir.module[0].type[0].structure
    self.assertEqual([[
        error.error("m.emb", struct.field[0].source_location,
                    "Dependency cycle\nfield1"),
        error.note("m.emb", struct.field[1].source_location, "field2"),
        error.note("m.emb", struct.field[2].source_location, "field3"),
        error.note("m.emb", struct.field[3].source_location, "field4"),
    ]], dependency_checker.find_dependency_cycles(ir))

  def test_error_on_simple_enum_value_cycle(self):
    ir = _parse_snippet("enum Foo:\n"
                        "  XX = YY\n"
                        "  YY = XX\n")
    enum = ir.module[0].type[0].enumeration
    self.assertEqual([[
        error.error("m.emb", enum.value[0].source_location,
                    "Dependency cycle\nXX"),
        error.note("m.emb", enum.value[1].source_location, "YY")
    ]], dependency_checker.find_dependency_cycles(ir))

  def test_no_error_on_no_cycle(self):
    ir = _parse_snippet("enum Foo:\n"
                        "  XX = 0\n"
                        "  YY = XX\n")
    self.assertEqual([], dependency_checker.find_dependency_cycles(ir))

  def test_error_on_cycle_nested(self):
    ir = _parse_snippet("struct Foo:\n"
                        "  struct Bar:\n"
                        "    0 [+field2]  UInt  field1\n"
                        "    0 [+field1]  UInt  field2\n"
                        "  0 [+1]  UInt  field\n")
    struct = ir.module[0].type[0].subtype[0].structure
    self.assertEqual([[
        error.error("m.emb", struct.field[0].source_location,
                    "Dependency cycle\nfield1"),
        error.note("m.emb", struct.field[1].source_location, "field2")
    ]], dependency_checker.find_dependency_cycles(ir))

  def test_error_on_import_cycle(self):
    ir, unused_debug_info, errors = glue.parse_emboss_file(
        "m.emb",
        test_util.dict_file_reader({"m.emb": 'import "n.emb" as n\n',
                                    "n.emb": 'import "m.emb" as m\n'}),
        stop_before_step="find_dependency_cycles")
    assert not errors
    self.assertEqual([[
        error.error("m.emb", ir.module[0].source_location,
                    "Import dependency cycle\nm.emb"),
        error.note("n.emb", ir.module[2].source_location, "n.emb")
    ]], dependency_checker.find_dependency_cycles(ir))

  def test_error_on_import_cycle_and_field_cycle(self):
    ir, unused_debug_info, errors = glue.parse_emboss_file(
        "m.emb",
        test_util.dict_file_reader({"m.emb": 'import "n.emb" as n\n'
                                             "struct Foo:\n"
                                             "  0 [+field1]  UInt  field1\n",
                                    "n.emb": 'import "m.emb" as m\n'}),
        stop_before_step="find_dependency_cycles")
    assert not errors
    struct = ir.module[0].type[0].structure
    self.assertEqual([[
        error.error("m.emb", ir.module[0].source_location,
                    "Import dependency cycle\nm.emb"),
        error.note("n.emb", ir.module[2].source_location, "n.emb")
    ], [
        error.error("m.emb", struct.field[0].source_location,
                    "Dependency cycle\nfield1")
    ]], dependency_checker.find_dependency_cycles(ir))

  def test_error_on_field_existence_self_cycle(self):
    ir = _parse_snippet("struct Foo:\n"
                        "  if x == 1:\n"
                        "    0 [+1]  UInt  x\n")
    struct = ir.module[0].type[0].structure
    self.assertEqual([[
        error.error("m.emb", struct.field[0].source_location,
                    "Dependency cycle\nx")
    ]], dependency_checker.find_dependency_cycles(ir))

  def test_error_on_field_existence_cycle(self):
    ir = _parse_snippet("struct Foo:\n"
                        "  if y == 1:\n"
                        "    0 [+1]  UInt  x\n"
                        "  if x == 0:\n"
                        "    1 [+1]  UInt  y\n")
    struct = ir.module[0].type[0].structure
    self.assertEqual([[
        error.error("m.emb", struct.field[0].source_location,
                    "Dependency cycle\nx"),
        error.note("m.emb", struct.field[1].source_location, "y")
    ]], dependency_checker.find_dependency_cycles(ir))

  def test_error_on_virtual_field_cycle(self):
    ir = _parse_snippet("struct Foo:\n"
                        "  let x = y\n"
                        "  let y = x\n")
    struct = ir.module[0].type[0].structure
    self.assertEqual([[
        error.error("m.emb", struct.field[0].source_location,
                    "Dependency cycle\nx"),
        error.note("m.emb", struct.field[1].source_location, "y")
    ]], dependency_checker.find_dependency_cycles(ir))

  def test_error_on_virtual_non_virtual_field_cycle(self):
    ir = _parse_snippet("struct Foo:\n"
                        "  let x = y\n"
                        "  x [+4]  UInt  y\n")
    struct = ir.module[0].type[0].structure
    self.assertEqual([[
        error.error("m.emb", struct.field[0].source_location,
                    "Dependency cycle\nx"),
        error.note("m.emb", struct.field[1].source_location, "y")
    ]], dependency_checker.find_dependency_cycles(ir))

  def test_error_on_non_virtual_virtual_field_cycle(self):
    ir = _parse_snippet("struct Foo:\n"
                        "  y [+4]  UInt  x\n"
                        "  let y = x\n")
    struct = ir.module[0].type[0].structure
    self.assertEqual([[
        error.error("m.emb", struct.field[0].source_location,
                    "Dependency cycle\nx"),
        error.note("m.emb", struct.field[1].source_location, "y")
    ]], dependency_checker.find_dependency_cycles(ir))

  def test_error_on_cycle_involving_subfield(self):
    ir = _parse_snippet("struct Bar:\n"
                        "  foo_b.x [+4]  Foo  foo_a\n"
                        "  foo_a.x [+4]  Foo  foo_b\n"
                        "struct Foo:\n"
                        "  0 [+4]  UInt  x\n")
    struct = ir.module[0].type[0].structure
    self.assertEqual([[
        error.error("m.emb", struct.field[0].source_location,
                    "Dependency cycle\nfoo_a"),
        error.note("m.emb", struct.field[1].source_location, "foo_b")
    ]], dependency_checker.find_dependency_cycles(ir))

  def test_dependency_ordering_with_no_dependencies(self):
    ir = _find_dependencies_for_snippet("struct Foo:\n"
                                        "  0 [+4]  UInt  a\n"
                                        "  4 [+4]  UInt  b\n")
    self.assertEqual([], dependency_checker.set_dependency_order(ir))
    struct = ir.module[0].type[0].structure
    self.assertEqual([0, 1], struct.fields_in_dependency_order[:2])

  def test_dependency_ordering_with_dependency_in_order(self):
    ir = _find_dependencies_for_snippet("struct Foo:\n"
                                        "  0 [+4]  UInt  a\n"
                                        "  a [+4]  UInt  b\n")
    self.assertEqual([], dependency_checker.set_dependency_order(ir))
    struct = ir.module[0].type[0].structure
    self.assertEqual([0, 1], struct.fields_in_dependency_order[:2])

  def test_dependency_ordering_with_dependency_in_reverse_order(self):
    ir = _find_dependencies_for_snippet("struct Foo:\n"
                                        "  b [+4]  UInt  a\n"
                                        "  0 [+4]  UInt  b\n")
    self.assertEqual([], dependency_checker.set_dependency_order(ir))
    struct = ir.module[0].type[0].structure
    self.assertEqual([1, 0], struct.fields_in_dependency_order[:2])

  def test_dependency_ordering_with_extra_fields(self):
    ir = _find_dependencies_for_snippet("struct Foo:\n"
                                        "  d [+4]   UInt  a\n"
                                        "  4 [+4]   UInt  b\n"
                                        "  8 [+4]   UInt  c\n"
                                        "  12 [+4]  UInt  d\n")
    self.assertEqual([], dependency_checker.set_dependency_order(ir))
    struct = ir.module[0].type[0].structure
    self.assertEqual([1, 2, 3, 0], struct.fields_in_dependency_order[:4])

  def test_dependency_ordering_scrambled(self):
    ir = _find_dependencies_for_snippet("struct Foo:\n"
                                        "  d [+4]   UInt  a\n"
                                        "  c [+4]   UInt  b\n"
                                        "  a [+4]   UInt  c\n"
                                        "  12 [+4]  UInt  d\n")
    self.assertEqual([], dependency_checker.set_dependency_order(ir))
    struct = ir.module[0].type[0].structure
    self.assertEqual([3, 0, 2, 1], struct.fields_in_dependency_order[:4])

  def test_dependency_ordering_multiple_dependents(self):
    ir = _find_dependencies_for_snippet("struct Foo:\n"
                                        "  d [+4]   UInt  a\n"
                                        "  d [+4]   UInt  b\n"
                                        "  d [+4]   UInt  c\n"
                                        "  12 [+4]  UInt  d\n")
    self.assertEqual([], dependency_checker.set_dependency_order(ir))
    struct = ir.module[0].type[0].structure
    self.assertEqual([3, 0, 1, 2], struct.fields_in_dependency_order[:4])

  def test_dependency_ordering_multiple_dependencies(self):
    ir = _find_dependencies_for_snippet("struct Foo:\n"
                                        "  b+c [+4]  UInt  a\n"
                                        "  4 [+4]    UInt  b\n"
                                        "  8 [+4]    UInt  c\n"
                                        "  a [+4]    UInt  d\n")
    self.assertEqual([], dependency_checker.set_dependency_order(ir))
    struct = ir.module[0].type[0].structure
    self.assertEqual([1, 2, 0, 3], struct.fields_in_dependency_order[:4])

  def test_dependency_ordering_with_parameter(self):
    ir = _find_dependencies_for_snippet("struct Foo:\n"
                                        "  0 [+1]  Bar(x)  b\n"
                                        "  1 [+1]  UInt    x\n"
                                        "struct Bar(x: UInt:8):\n"
                                        "  x [+1]  UInt    y\n")
    self.assertEqual([], dependency_checker.set_dependency_order(ir))
    struct = ir.module[0].type[0].structure
    self.assertEqual([1, 0], struct.fields_in_dependency_order[:2])

  def test_dependency_ordering_with_local_parameter(self):
    ir = _find_dependencies_for_snippet("struct Foo(x: Int:13):\n"
                                        "  0 [+x]  Int  b\n")
    self.assertEqual([], dependency_checker.set_dependency_order(ir))
    struct = ir.module[0].type[0].structure
    self.assertEqual([0], struct.fields_in_dependency_order[:1])


if __name__ == "__main__":
  unittest.main()
