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

import unittest
from compiler.front_end import glue
from compiler.front_end import write_inference
from compiler.util import ir_data
from compiler.util import test_util


class WriteInferenceTest(unittest.TestCase):

  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="set_write_methods")
    assert not errors, errors
    return ir

  def test_adds_physical_write_method(self):
    ir = self._make_ir("struct Foo:\n"
                       "  0 [+1]  UInt  x\n")
    self.assertEqual([], write_inference.set_write_methods(ir))
    self.assertTrue(
        ir.module[0].type[0].structure.field[0].write_method.physical)

  def test_adds_read_only_write_method_to_non_alias_virtual(self):
    ir = self._make_ir("struct Foo:\n"
                       "  let x = 5\n")
    self.assertEqual([], write_inference.set_write_methods(ir))
    self.assertTrue(
        ir.module[0].type[0].structure.field[0].write_method.read_only)

  def test_adds_alias_write_method_to_alias_of_physical_field(self):
    ir = self._make_ir("struct Foo:\n"
                       "  let x = y\n"
                       "  0 [+1]  UInt  y\n")
    self.assertEqual([], write_inference.set_write_methods(ir))
    field = ir.module[0].type[0].structure.field[0]
    self.assertTrue(field.write_method.HasField("alias"))
    self.assertEqual(
        "y", field.write_method.alias.path[0].canonical_name.object_path[-1])

  def test_adds_alias_write_method_to_alias_of_alias_of_physical_field(self):
    ir = self._make_ir("struct Foo:\n"
                       "  let x = z\n"
                       "  let z = y\n"
                       "  0 [+1]  UInt  y\n")
    self.assertEqual([], write_inference.set_write_methods(ir))
    field = ir.module[0].type[0].structure.field[0]
    self.assertTrue(field.write_method.HasField("alias"))
    self.assertEqual(
        "z", field.write_method.alias.path[0].canonical_name.object_path[-1])

  def test_adds_read_only_write_method_to_alias_of_read_only(self):
    ir = self._make_ir("struct Foo:\n"
                       "  let x = y\n"
                       "  let y = 5\n")
    self.assertEqual([], write_inference.set_write_methods(ir))
    field = ir.module[0].type[0].structure.field[0]
    self.assertTrue(field.write_method.read_only)

  def test_adds_read_only_write_method_to_alias_of_alias_of_read_only(self):
    ir = self._make_ir("struct Foo:\n"
                       "  let x = z\n"
                       "  let z = y\n"
                       "  let y = 5\n")
    self.assertEqual([], write_inference.set_write_methods(ir))
    field = ir.module[0].type[0].structure.field[0]
    self.assertTrue(field.write_method.read_only)

  def test_adds_read_only_write_method_to_alias_of_parameter(self):
    ir = self._make_ir("struct Foo(x: UInt:8):\n"
                       "  let y = x\n")
    self.assertEqual([], write_inference.set_write_methods(ir))
    field = ir.module[0].type[0].structure.field[0]
    self.assertTrue(field.write_method.read_only)

  def test_adds_transform_write_method_to_base_value_field(self):
    ir = self._make_ir("struct Foo:\n"
                       "  0 [+1]  UInt  x\n"
                       "  let y = x + 50\n")
    self.assertEqual([], write_inference.set_write_methods(ir))
    field = ir.module[0].type[0].structure.field[1]
    transform = field.write_method.transform
    self.assertTrue(transform)
    self.assertEqual(
        "x",
        transform.destination.path[0].canonical_name.object_path[-1])
    self.assertEqual(ir_data.FunctionMapping.SUBTRACTION,
                     transform.function_body.function.function)
    arg0, arg1 = transform.function_body.function.args
    self.assertEqual("$logical_value",
                     arg0.builtin_reference.canonical_name.object_path[0])
    self.assertEqual("50", arg1.constant.value)

  def test_adds_transform_write_method_to_negative_base_value_field(self):
    ir = self._make_ir("struct Foo:\n"
                       "  0 [+1]  UInt  x\n"
                       "  let y = x - 50\n")
    self.assertEqual([], write_inference.set_write_methods(ir))
    field = ir.module[0].type[0].structure.field[1]
    transform = field.write_method.transform
    self.assertTrue(transform)
    self.assertEqual(
        "x",
        transform.destination.path[0].canonical_name.object_path[-1])
    self.assertEqual(ir_data.FunctionMapping.ADDITION,
                     transform.function_body.function.function)
    arg0, arg1 = transform.function_body.function.args
    self.assertEqual("$logical_value",
                     arg0.builtin_reference.canonical_name.object_path[0])
    self.assertEqual("50", arg1.constant.value)

  def test_adds_transform_write_method_to_reversed_base_value_field(self):
    ir = self._make_ir("struct Foo:\n"
                       "  0 [+1]  UInt  x\n"
                       "  let y = 50 + x\n")
    self.assertEqual([], write_inference.set_write_methods(ir))
    field = ir.module[0].type[0].structure.field[1]
    transform = field.write_method.transform
    self.assertTrue(transform)
    self.assertEqual(
        "x",
        transform.destination.path[0].canonical_name.object_path[-1])
    self.assertEqual(ir_data.FunctionMapping.SUBTRACTION,
                     transform.function_body.function.function)
    arg0, arg1 = transform.function_body.function.args
    self.assertEqual("$logical_value",
                     arg0.builtin_reference.canonical_name.object_path[0])
    self.assertEqual("50", arg1.constant.value)

  def test_adds_transform_write_method_to_reversed_negative_base_value_field(
      self):
    ir = self._make_ir("struct Foo:\n"
                       "  0 [+1]  UInt  x\n"
                       "  let y = 50 - x\n")
    self.assertEqual([], write_inference.set_write_methods(ir))
    field = ir.module[0].type[0].structure.field[1]
    transform = field.write_method.transform
    self.assertTrue(transform)
    self.assertEqual(
        "x",
        transform.destination.path[0].canonical_name.object_path[-1])
    self.assertEqual(ir_data.FunctionMapping.SUBTRACTION,
                     transform.function_body.function.function)
    arg0, arg1 = transform.function_body.function.args
    self.assertEqual("50", arg0.constant.value)
    self.assertEqual("$logical_value",
                     arg1.builtin_reference.canonical_name.object_path[0])

  def test_adds_transform_write_method_to_nested_invertible_field(self):
    ir = self._make_ir("struct Foo:\n"
                       "  0 [+1]  UInt  x\n"
                       "  let y = 30 + (50 - x)\n")
    self.assertEqual([], write_inference.set_write_methods(ir))
    field = ir.module[0].type[0].structure.field[1]
    transform = field.write_method.transform
    self.assertTrue(transform)
    self.assertEqual(
        "x",
        transform.destination.path[0].canonical_name.object_path[-1])
    self.assertEqual(ir_data.FunctionMapping.SUBTRACTION,
                     transform.function_body.function.function)
    arg0, arg1 = transform.function_body.function.args
    self.assertEqual("50", arg0.constant.value)
    self.assertEqual(ir_data.FunctionMapping.SUBTRACTION, arg1.function.function)
    arg10, arg11 = arg1.function.args
    self.assertEqual("$logical_value",
                     arg10.builtin_reference.canonical_name.object_path[0])
    self.assertEqual("30", arg11.constant.value)

  def test_does_not_add_transform_write_method_for_parameter_target(self):
    ir = self._make_ir("struct Foo(x: UInt:8):\n"
                       "  let y = 50 + x\n")
    self.assertEqual([], write_inference.set_write_methods(ir))
    field = ir.module[0].type[0].structure.field[0]
    self.assertEqual("read_only", field.write_method.WhichOneof("method"))

  def test_adds_transform_write_method_with_complex_auxiliary_subexpression(
      self):
    ir = self._make_ir("struct Foo:\n"
                       "  0 [+1]  UInt  x\n"
                       "  let y = x - $max(Foo.$size_in_bytes, Foo.z)\n"
                       "  let z = 500\n")
    self.assertEqual([], write_inference.set_write_methods(ir))
    field = ir.module[0].type[0].structure.field[1]
    transform = field.write_method.transform
    self.assertTrue(transform)
    self.assertEqual(
        "x",
        transform.destination.path[0].canonical_name.object_path[-1])
    self.assertEqual(ir_data.FunctionMapping.ADDITION,
                     transform.function_body.function.function)
    args = transform.function_body.function.args
    self.assertEqual("$logical_value",
                     args[0].builtin_reference.canonical_name.object_path[0])
    self.assertEqual(field.read_transform.function.args[1], args[1])


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