# 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 front_end.synthetics."""

import unittest
from compiler.front_end import glue
from compiler.front_end import synthetics
from compiler.util import error
from compiler.util import ir_data
from compiler.util import test_util


class SyntheticsTest(unittest.TestCase):

  def _find_attribute(self, field, name):
    result = None
    for attribute in field.attribute:
      if attribute.name.text == name:
        self.assertIsNone(result)
        result = attribute
    self.assertIsNotNone(result)
    return result

  def _make_ir(self, emb_text):
    ir, unused_debug_info, errors = glue.parse_emboss_file(
        "m.emb",
        test_util.dict_file_reader({"m.emb": emb_text}),
        stop_before_step="desugar")
    assert not errors, errors
    return ir

  def test_nothing_to_do(self):
    ir = self._make_ir("struct Foo:\n"
                       "  0 [+1]     UInt      x\n"
                       "  1 [+1]     UInt:8[]  y\n")
    self.assertEqual([], synthetics.desugar(ir))

  def test_adds_anonymous_bits_fields(self):
    ir = self._make_ir("struct Foo:\n"
                       "  0 [+1]  bits:\n"
                       "    0 [+4]  Bar   bar\n"
                       "    4 [+4]  UInt  uint\n"
                       "  1 [+1]  bits:\n"
                       "    0 [+4]  Bits  nested_bits\n"
                       "enum Bar:\n"
                       "  BAR = 0\n"
                       "bits Bits:\n"
                       "  0 [+4]  UInt  uint\n")
    self.assertEqual([], synthetics.desugar(ir))
    structure = ir.module[0].type[0].structure
    # The first field should be the anonymous bits structure.
    self.assertTrue(structure.field[0].HasField("location"))
    # Then the aliases generated for those structures.
    self.assertEqual("bar", structure.field[1].name.name.text)
    self.assertEqual("uint", structure.field[2].name.name.text)
    # Then the second anonymous bits.
    self.assertTrue(structure.field[3].HasField("location"))
    # Then the alias from the second anonymous bits.
    self.assertEqual("nested_bits", structure.field[4].name.name.text)

  def test_adds_correct_existence_condition(self):
    ir = self._make_ir("struct Foo:\n"
                       "  0 [+1]  bits:\n"
                       "    0 [+4]  UInt  bar\n")
    self.assertEqual([], synthetics.desugar(ir))
    bits_field = ir.module[0].type[0].structure.field[0]
    alias_field = ir.module[0].type[0].structure.field[1]
    self.assertEqual("bar", alias_field.name.name.text)
    self.assertEqual(bits_field.name.name.text,
                     alias_field.existence_condition.function.args[0].function.
                     args[0].field_reference.path[0].source_name[-1].text)
    self.assertEqual(bits_field.name.name.text,
                     alias_field.existence_condition.function.args[1].function.
                     args[0].field_reference.path[0].source_name[-1].text)
    self.assertEqual("bar",
                     alias_field.existence_condition.function.args[1].function.
                     args[0].field_reference.path[1].source_name[-1].text)
    self.assertEqual(
        ir_data.FunctionMapping.PRESENCE,
        alias_field.existence_condition.function.args[0].function.function)
    self.assertEqual(
        ir_data.FunctionMapping.PRESENCE,
        alias_field.existence_condition.function.args[1].function.function)
    self.assertEqual(ir_data.FunctionMapping.AND,
                     alias_field.existence_condition.function.function)

  def test_adds_correct_read_transform(self):
    ir = self._make_ir("struct Foo:\n"
                       "  0 [+1]  bits:\n"
                       "    0 [+4]  UInt  bar\n")
    self.assertEqual([], synthetics.desugar(ir))
    bits_field = ir.module[0].type[0].structure.field[0]
    alias_field = ir.module[0].type[0].structure.field[1]
    self.assertEqual("bar", alias_field.name.name.text)
    self.assertEqual(
        bits_field.name.name.text,
        alias_field.read_transform.field_reference.path[0].source_name[-1].text)
    self.assertEqual(
        "bar",
        alias_field.read_transform.field_reference.path[1].source_name[-1].text)

  def test_adds_correct_abbreviation(self):
    ir = self._make_ir("struct Foo:\n"
                       "  0 [+1]  bits:\n"
                       "    0 [+4]  UInt  bar\n"
                       "    4 [+4]  UInt  baz (qux)\n")
    self.assertEqual([], synthetics.desugar(ir))
    bar_alias = ir.module[0].type[0].structure.field[1]
    baz_alias = ir.module[0].type[0].structure.field[2]
    self.assertFalse(bar_alias.HasField("abbreviation"))
    self.assertEqual("qux", baz_alias.abbreviation.text)

  def test_anonymous_bits_sets_correct_is_synthetic(self):
    ir = self._make_ir("struct Foo:\n"
                       "  0 [+1]  bits:\n"
                       "    0 [+4]  UInt  bar (b)\n")
    self.assertEqual([], synthetics.desugar(ir))
    bits_field = ir.module[0].type[0].subtype[0].structure.field[0]
    alias_field = ir.module[0].type[0].structure.field[1]
    self.assertFalse(alias_field.name.source_location.is_synthetic)
    self.assertTrue(alias_field.HasField("abbreviation"))
    self.assertFalse(alias_field.abbreviation.source_location.is_synthetic)
    self.assertTrue(alias_field.HasField("read_transform"))
    read_alias = alias_field.read_transform
    self.assertTrue(read_alias.source_location.is_synthetic)
    self.assertTrue(
        read_alias.field_reference.path[0].source_location.is_synthetic)
    alias_condition = alias_field.existence_condition
    self.assertTrue(alias_condition.source_location.is_synthetic)
    self.assertTrue(
        alias_condition.function.args[0].source_location.is_synthetic)
    self.assertTrue(bits_field.name.source_location.is_synthetic)
    self.assertTrue(bits_field.name.name.source_location.is_synthetic)
    self.assertTrue(bits_field.abbreviation.source_location.is_synthetic)

  def test_adds_text_output_skip_attribute_to_anonymous_bits(self):
    ir = self._make_ir("struct Foo:\n"
                       "  0 [+1]  bits:\n"
                       "    0 [+4]  UInt  bar (b)\n")
    self.assertEqual([], synthetics.desugar(ir))
    bits_field = ir.module[0].type[0].structure.field[0]
    text_output_attribute = self._find_attribute(bits_field, "text_output")
    self.assertEqual("Skip", text_output_attribute.value.string_constant.text)

  def test_skip_attribute_is_marked_as_synthetic(self):
    ir = self._make_ir("struct Foo:\n"
                       "  0 [+1]  bits:\n"
                       "    0 [+4]  UInt  bar\n")
    self.assertEqual([], synthetics.desugar(ir))
    bits_field = ir.module[0].type[0].structure.field[0]
    attribute = self._find_attribute(bits_field, "text_output")
    self.assertTrue(attribute.source_location.is_synthetic)
    self.assertTrue(attribute.name.source_location.is_synthetic)
    self.assertTrue(attribute.value.source_location.is_synthetic)
    self.assertTrue(
        attribute.value.string_constant.source_location.is_synthetic)

  def test_adds_size_in_bytes(self):
    ir = self._make_ir("struct Foo:\n"
                       "  1 [+l]  UInt:8[]  bytes\n"
                       "  0 [+1]  UInt      length (l)\n")
    self.assertEqual([], synthetics.desugar(ir))
    structure = ir.module[0].type[0].structure
    size_in_bytes_field = structure.field[2]
    max_size_in_bytes_field = structure.field[3]
    min_size_in_bytes_field = structure.field[4]
    self.assertEqual("$size_in_bytes", size_in_bytes_field.name.name.text)
    self.assertEqual(ir_data.FunctionMapping.MAXIMUM,
                     size_in_bytes_field.read_transform.function.function)
    self.assertEqual("$max_size_in_bytes",
                     max_size_in_bytes_field.name.name.text)
    self.assertEqual(ir_data.FunctionMapping.UPPER_BOUND,
                     max_size_in_bytes_field.read_transform.function.function)
    self.assertEqual("$min_size_in_bytes",
                     min_size_in_bytes_field.name.name.text)
    self.assertEqual(ir_data.FunctionMapping.LOWER_BOUND,
                     min_size_in_bytes_field.read_transform.function.function)
    # The correctness of $size_in_bytes et al are tested much further down
    # stream, in tests of the generated C++ code.

  def test_adds_size_in_bits(self):
    ir = self._make_ir("bits Foo:\n"
                       "  1 [+9]  UInt  hi\n"
                       "  0 [+1]  Flag  lo\n")
    self.assertEqual([], synthetics.desugar(ir))
    structure = ir.module[0].type[0].structure
    size_in_bits_field = structure.field[2]
    max_size_in_bits_field = structure.field[3]
    min_size_in_bits_field = structure.field[4]
    self.assertEqual("$size_in_bits", size_in_bits_field.name.name.text)
    self.assertEqual(ir_data.FunctionMapping.MAXIMUM,
                     size_in_bits_field.read_transform.function.function)
    self.assertEqual("$max_size_in_bits",
                     max_size_in_bits_field.name.name.text)
    self.assertEqual(ir_data.FunctionMapping.UPPER_BOUND,
                     max_size_in_bits_field.read_transform.function.function)
    self.assertEqual("$min_size_in_bits",
                     min_size_in_bits_field.name.name.text)
    self.assertEqual(ir_data.FunctionMapping.LOWER_BOUND,
                     min_size_in_bits_field.read_transform.function.function)
    # The correctness of $size_in_bits et al are tested much further down
    # stream, in tests of the generated C++ code.

  def test_adds_text_output_skip_attribute_to_size_in_bytes(self):
    ir = self._make_ir("struct Foo:\n"
                       "  1 [+l]  UInt:8[]  bytes\n"
                       "  0 [+1]  UInt      length (l)\n")
    self.assertEqual([], synthetics.desugar(ir))
    size_in_bytes_field = ir.module[0].type[0].structure.field[2]
    self.assertEqual("$size_in_bytes", size_in_bytes_field.name.name.text)
    text_output_attribute = self._find_attribute(size_in_bytes_field,
                                                 "text_output")
    self.assertEqual("Skip", text_output_attribute.value.string_constant.text)

  def test_replaces_next(self):
    ir = self._make_ir("struct Foo:\n"
                       "  1     [+2]  UInt:8[]  a\n"
                       "  $next [+4]  UInt      b\n"
                       "  $next [+1]  UInt      c\n")
    self.assertEqual([], synthetics.desugar(ir))
    offset_of_b = ir.module[0].type[0].structure.field[1].location.start
    self.assertTrue(offset_of_b.HasField("function"))
    self.assertEqual(offset_of_b.function.function, ir_data.FunctionMapping.ADDITION)
    self.assertEqual(offset_of_b.function.args[0].constant.value, "1")
    self.assertEqual(offset_of_b.function.args[1].constant.value, "2")
    offset_of_c = ir.module[0].type[0].structure.field[2].location.start
    self.assertEqual(
            offset_of_c.function.args[0].function.args[0].constant.value, "1")
    self.assertEqual(
            offset_of_c.function.args[0].function.args[1].constant.value, "2")
    self.assertEqual(offset_of_c.function.args[1].constant.value, "4")

  def test_next_in_first_field(self):
    ir = self._make_ir("struct Foo:\n"
                       "  $next [+2]  UInt:8[]  a\n"
                       "  $next [+4]  UInt      b\n")
    struct = ir.module[0].type[0].structure
    self.assertEqual([[
        error.error("m.emb", struct.field[0].location.start.source_location,
                    "`$next` may not be used in the first physical field of " +
                    "a structure; perhaps you meant `0`?"),
    ]], synthetics.desugar(ir))

  def test_next_in_size(self):
    ir = self._make_ir("struct Foo:\n"
                       "  0 [+2]      UInt:8[]  a\n"
                       "  1 [+$next]  UInt      b\n")
    struct = ir.module[0].type[0].structure
    self.assertEqual([[
        error.error("m.emb", struct.field[1].location.size.source_location,
                    "`$next` may only be used in the start expression of a " +
                    "physical field."),
    ]], synthetics.desugar(ir))


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