# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you 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
#
#   http://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.
# ruff: noqa: F841
"""Test eliminate common subexpr pass"""

import numpy as np

import tvm
import tvm.testing
from tvm.relax.transform import EliminateCommonSubexpr
from tvm.script.parser import ir as I
from tvm.script.parser import relax as R
from tvm.script.parser import tirx as T


def verify(input, expected, call_only=False):
    tvm.ir.assert_structural_equal(EliminateCommonSubexpr(call_only)(input), expected)


def test_simple():
    @I.ir_module(s_tir=True)
    class Before:
        @R.function
        def foo(x: R.Tensor((2, 3), dtype="float32"), y: R.Tensor((2, 3), dtype="float32")):
            with R.dataflow():
                lv0 = R.add(x, y)
                lv1 = R.add(x, y)
                gv = R.multiply(lv0, lv1)
                R.output(gv)
            return gv

    @I.ir_module(s_tir=True)
    class Expected:
        @R.function
        def foo(x: R.Tensor((2, 3), dtype="float32"), y: R.Tensor((2, 3), dtype="float32")):
            with R.dataflow():
                lv0 = R.add(x, y)
                lv1 = lv0
                gv = R.multiply(lv0, lv0)
                R.output(gv)
            return gv

    verify(Before, Expected)


def test_constants():
    @I.ir_module(s_tir=True)
    class Before:
        @R.function
        def foo() -> R.Tuple(R.Tensor((), dtype="int32"), R.Tensor((2, 2), dtype="int32")):
            with R.dataflow():
                # we are not going to bind the constant 1 to a var
                lv0 = R.add(R.const(1, dtype="int32"), R.const(1, dtype="int32"))
                # we expect to bind the repeated large constants
                lv1 = R.add(
                    R.const(tvm.runtime.tensor(np.zeros((2, 2), dtype="int32"))),
                    R.const(tvm.runtime.tensor(np.zeros((2, 2), dtype="int32"))),
                )
                gv = (lv0, lv1)
                R.output(gv)
            return gv

    @I.ir_module(s_tir=True)
    class Expected:
        @R.function
        def foo() -> R.Tuple(R.Tensor((), dtype="int32"), R.Tensor((2, 2), dtype="int32")):
            with R.dataflow():
                lv0 = R.add(R.const(1, dtype="int32"), R.const(1, dtype="int32"))
                lv1 = R.add(
                    R.const(tvm.runtime.tensor(np.zeros((2, 2), dtype="int32"))),
                    R.const(tvm.runtime.tensor(np.zeros((2, 2), dtype="int32"))),
                )
                gv = (lv0, lv1)
                R.output(gv)
            return gv

    verify(Before, Expected)


def test_repeated_inner_tuples():
    """CSE is only applied at variable bindings

    To remain consistent with the behavior of the normalizer, tuples
    are kept as-is, even if they contain repeated sub-tuples.
    """

    @I.ir_module(s_tir=True)
    class Before:
        @R.function
        def foo(x: R.Tensor((), dtype="int32")) -> R.Tensor((), dtype="int32"):
            with R.dataflow():
                # repeated units: (x, x), (x, (x, x)), ((x, x), (x, (x, x)))
                tup = (((x, x), (x, (x, x))), ((x, x), (x, (x, x))), (x, (x, x)))
                gv = tup[0][0][1]
                R.output(gv)
            return gv

    Expected = Before

    verify(Before, Expected)


def test_inner_function():
    @I.ir_module(s_tir=True)
    class Before:
        @R.function
        def foo(x: R.Tensor((), dtype="int32")) -> R.Tensor((), dtype="int32"):
            with R.dataflow():
                # we are going to do CSE inside the local function
                @R.function
                def bar(y: R.Tensor((), dtype="int32")) -> R.Tensor((), dtype="int32"):
                    with R.dataflow():
                        # writing this out in ANF to illustrate why CSE behaves as it does
                        # result of ANF transforming R.add(R.add(y, y), R.add(y, y))
                        lv0 = R.add(y, y)
                        lv1 = R.add(y, y)
                        lv2 = R.add(lv0, lv1)
                        gv = lv2
                        R.output(gv)
                    return R.add(gv, gv)

                # also making the ANF explicit to better illustrate the result of CSE
                # result of ANF transforming R.add(R.add(bar(x), bar(x)), R.add(bar(x), bar(x)))
                lv0 = bar(x)
                lv1 = bar(x)
                lv2 = R.add(lv0, lv1)
                lv3 = bar(x)
                lv4 = bar(x)
                lv5 = R.add(lv3, lv4)
                lv6 = R.add(lv2, lv5)
                gv = lv6
                R.output(gv)
            return gv

    @I.ir_module(s_tir=True)
    class Expected:
        @R.function
        def foo(x: R.Tensor((), dtype="int32")) -> R.Tensor((), dtype="int32"):
            with R.dataflow():

                @R.function
                def bar(y: R.Tensor((), dtype="int32")) -> R.Tensor((), dtype="int32"):
                    with R.dataflow():
                        lv0 = R.add(y, y)
                        lv1 = lv0
                        lv2 = R.add(lv0, lv0)
                        gv = lv2
                        R.output(gv)
                    return R.add(gv, gv)

                # can further clean this up
                # using canonicalize bindings, eliminate unused bindings, and CSE again
                lv0 = bar(x)
                lv1 = lv0
                lv2 = R.add(lv0, lv0)
                lv3 = lv0
                lv4 = lv0
                lv5 = lv2
                lv6 = R.add(lv2, lv2)
                gv = lv6
                R.output(gv)
            return gv

    verify(Before, Expected)


def test_call_only():
    @I.ir_module(s_tir=True)
    class Before:
        @R.function
        def foo(x: R.Tensor((160,), dtype="float32")):
            with R.dataflow():
                lv1 = R.arange(R.prim_value(0), R.prim_value(160), R.prim_value(1), dtype="float32")
                lv2 = R.arange(R.prim_value(0), R.prim_value(160), R.prim_value(1), dtype="float32")
                lv3 = R.add(x, lv1)
                out = R.add(lv3, lv2)
                R.output(out)
            return out

    @I.ir_module(s_tir=True)
    class Expected:
        @R.function
        def foo(x: R.Tensor((160,), dtype="float32")) -> R.Tensor((160,), dtype="float32"):
            with R.dataflow():
                lv1 = R.arange(R.prim_value(0), R.prim_value(160), R.prim_value(1), dtype="float32")
                lv2 = lv1
                lv3 = R.add(x, lv1)
                out = R.add(lv3, lv1)
                R.output(out)
            return out

    verify(Before, Expected, call_only=True)


def test_cse_outside_dataflow():
    # same example as previously but it will work without a dataflow wrapper
    @I.ir_module(s_tir=True)
    class Before:
        @R.function
        def foo(x: R.Tensor((2, 3), dtype="float32"), y: R.Tensor((2, 3), dtype="float32")):
            lv0 = R.add(x, y)
            lv1 = R.add(x, y)
            gv = R.multiply(lv0, lv1)
            return gv

    @I.ir_module(s_tir=True)
    class Expected:
        @R.function
        def foo(x: R.Tensor((2, 3), dtype="float32"), y: R.Tensor((2, 3), dtype="float32")):
            lv0 = R.add(x, y)
            lv1 = lv0
            gv = R.multiply(lv0, lv0)
            return gv

    verify(Before, Expected)


def test_no_cse_across_dataflow():
    # same example as previously but it will work without a dataflow wrapper
    @I.ir_module(s_tir=True)
    class Before:
        @R.function(pure=False)
        def foo(x: R.Tensor((2, 3), dtype="float32"), y: R.Tensor((2, 3), dtype="float32")):
            with R.dataflow():
                lv0 = R.add(x, y)
                lv1 = R.add(x, y)
                gv1 = R.multiply(lv0, lv1)
                R.output(gv1)

            _ = R.print(format="Prevent dataflow block merging")

            with R.dataflow():
                lv2 = R.add(x, y)
                lv3 = R.add(x, y)
                gv2 = R.multiply(lv2, lv3)
                R.output(gv2)

            gv3 = R.add(x, y)
            gv4 = R.add(x, y)
            gv5 = R.multiply(gv3, gv4)

            output = R.add(R.add(gv1, gv2), gv5)
            return output

    @I.ir_module(s_tir=True)
    class Expected:
        @R.function(pure=False)
        def foo(x: R.Tensor((2, 3), dtype="float32"), y: R.Tensor((2, 3), dtype="float32")):
            with R.dataflow():
                # The R.add(x,y) may be de-duplicated within a dataflow block
                lv0 = R.add(x, y)
                lv1 = lv0
                gv1 = R.multiply(lv0, lv0)
                R.output(gv1)

            _ = R.print(format="Prevent dataflow block merging")

            with R.dataflow():
                # However, the later dataflow block may not be
                # de-duplicated using variables in the earlier block.
                lv2 = R.add(x, y)
                lv3 = lv2
                gv2 = R.multiply(lv2, lv2)
                R.output(gv2)

            # And while non-dataflow bindings can be de-duplicated,
            # they cannot be de-duplicated using bindings that were
            # valid in either of the earlier dataflow blocks.
            gv3 = R.add(x, y)
            gv4 = gv3
            gv5 = R.multiply(gv3, gv3)

            output = R.add(R.add(gv1, gv2), gv5)
            return output

    verify(Before, Expected)


def test_no_replacement_across_dataflow_boundary():
    @I.ir_module(s_tir=True)
    class Before:
        @R.function
        def main(x: R.Tensor((2, 3), dtype="float32"), y: R.Tensor((2, 3), dtype="float32")):
            with R.dataflow():
                A = R.add(x, y)
                # B has the same value as A, and so instances of B can be replaced with A.
                B = R.add(x, y)
                C = R.multiply(A, B)

                # However, B is exposed for use outside of the
                # DataflowBlock, while A is not.  Therefore, any
                # additional uses of `B` must NOT be replaced with
                # A.
                R.output(B, C)

            # In addition, because `A` is only valid within the
            # dataflow block, the `R.add(x,y)` cannot be de-duplicated
            # as another usage of `A`.
            D = R.add(x, y)
            return (B, C, D)

    @I.ir_module(s_tir=True)
    class Expected:
        @R.function
        def main(x: R.Tensor((2, 3), dtype="float32"), y: R.Tensor((2, 3), dtype="float32")):
            with R.dataflow():
                A = R.add(x, y)
                B = A
                C = R.multiply(A, A)
                R.output(B, C)

            D = B
            return (B, C, B)

    verify(Before, Expected)


def test_do_not_eliminate_impure():
    @I.ir_module(s_tir=True)
    class Before:
        @R.function(pure=False)
        def foo(x: R.Tensor((2, 3), dtype="float32"), y: R.Tensor((2, 3), dtype="float32")):
            # it's a repeated subexpression but it would be wrong to deduplicate it
            p1 = R.print(format="Message")
            p2 = R.print(format="Message")
            a1 = R.assert_op(R.const(False), format="Always fails")
            lv0 = R.add(x, y)
            lv1 = R.add(x, y)
            gv = R.multiply(lv0, lv1)
            a2 = R.assert_op(R.const(False), format="Always fails")
            return gv

    @I.ir_module(s_tir=True)
    class Expected:
        @R.function(pure=False)
        def foo(x: R.Tensor((2, 3), dtype="float32"), y: R.Tensor((2, 3), dtype="float32")):
            p1 = R.print(format="Message")
            p2 = R.print(format="Message")
            a1 = R.assert_op(R.const(False), format="Always fails")
            lv0 = R.add(x, y)
            lv1 = lv0
            gv = R.multiply(lv0, lv0)
            a2 = R.assert_op(R.const(False), format="Always fails")
            return gv

    verify(Before, Expected)


def test_do_not_eliminate_shape_expr():
    @I.ir_module(s_tir=True)
    class Before:
        @R.function
        def foo(x: R.Tensor((2, 3), dtype="float32"), y: R.Tensor((2, 3), dtype="float32")):
            x = R.reshape(x, [6])
            y = R.reshape(y, [6])
            z = R.add(x, y)
            return z

    Expected = Before

    verify(Before, Expected)


def test_do_not_eliminate_extern_func():
    @I.ir_module(s_tir=True)
    class Before:
        @R.function(pure=False)
        def foo(x: R.Tensor((2, 3), dtype="float32")):
            y = R.call_packed("extern_func_name", x, ty_args=R.Tensor([2, 3]))
            z = R.call_packed("extern_func_name", y, ty_args=R.Tensor([2, 3]))
            return z

    Expected = Before

    verify(Before, Expected)


def test_call_tir_tuple_arg():
    @I.ir_module(s_tir=True)
    class Before:
        @R.function
        def main(A: R.Tensor([16, 16], "int32"), B: R.Tensor([16, 16], "int32")):
            cls = Before
            Prod = R.call_tir(cls.product, [A, B], out_ty=R.Tensor([16, 16], "int32"))
            Sum = R.call_tir(cls.sum, [A, B], out_ty=R.Tensor([16, 16], "int32"))
            return (Prod, Sum)

        @T.prim_func(private=True, s_tir=True)
        def product(
            A: T.Buffer([16, 16], "int32"),
            B: T.Buffer([16, 16], "int32"),
            C: T.Buffer([16, 16], "int32"),
        ):
            for iters in T.grid(*A.shape):
                with T.sblock("compute"):
                    i, j = T.axis.remap("SS", iters)
                    C[i, j] = A[i, j] * B[i, j]

        @T.prim_func(private=True, s_tir=True)
        def sum(
            A: T.Buffer([16, 16], "int32"),
            B: T.Buffer([16, 16], "int32"),
            C: T.Buffer([16, 16], "int32"),
        ):
            for iters in T.grid(*A.shape):
                with T.sblock("compute"):
                    i, j = T.axis.remap("SS", iters)
                    C[i, j] = A[i, j] + B[i, j]

    Expected = Before

    # If EliminateCommonSubexpr produces unnormalized expressions,
    # normalization of those expressions may produce additional
    # variables bindings.  This test case should be agnostic to those
    # additional bindings, so DCE is applied after CSE.
    After = tvm.ir.transform.Sequential(
        [
            EliminateCommonSubexpr(),
            tvm.relax.transform.DeadCodeElimination(),
        ]
    )(Before)
    tvm.ir.assert_structural_equal(Expected, After)


def test_do_not_eliminate_dtype():
    @I.ir_module(s_tir=True)
    class Before:
        @R.function(pure=False)
        def foo() -> R.Tensor((32, 64), "int32"):
            obj: R.Any = R.vm.alloc_storage(R.shape([24576]), runtime_device_index=0, dtype="uint8")
            a: R.Tensor([32, 64], dtype="int32") = R.vm.alloc_tensor(
                obj, offset=0, shape=R.shape([32, 64]), dtype="int32"
            )
            ret_val: R.Tensor([32, 64], dtype="int32") = R.builtin.alloc_tensor(
                R.shape([32, 64]), R.dtype("int32"), R.prim_value(0)
            )
            _t1: R.Tuple = R.vm.kill_object(a)
            _t3: R.Tuple = R.vm.kill_object(obj)
            lv: R.Tensor([32, 64], dtype="int32") = ret_val
            return lv

    Expected = Before

    verify(Before, Expected)


def test_match_cast():
    @I.ir_module(s_tir=True)
    class Before:
        @R.function
        def foo(x: R.Tensor((2, 3), dtype="float32"), y: R.Tensor((2, 3), dtype="float32")):
            with R.dataflow():
                A1 = R.add(x, y)
                B1 = R.match_cast(A1, R.Tensor([2, 3], "float32"))

                A2 = R.add(x, y)
                B2 = R.match_cast(A2, R.Tensor([2, 3], "float32"))

                gv = R.multiply(B1, B2)
                R.output(gv)
            return gv

    @I.ir_module(s_tir=True)
    class Expected:
        @R.function
        def foo(x: R.Tensor((2, 3), dtype="float32"), y: R.Tensor((2, 3), dtype="float32")):
            with R.dataflow():
                A1 = R.add(x, y)
                B1 = R.match_cast(A1, R.Tensor([2, 3], "float32"))

                A2 = A1
                B2 = B1
                gv = R.multiply(B1, B1)
                R.output(gv)
            return gv

    verify(Before, Expected)


def test_match_cast_with_symbolic_vars():
    @I.ir_module(s_tir=True)
    class Before:
        @R.function
        def foo(x: R.Tensor(dtype="float32"), y: R.Tensor(dtype="float32")):
            with R.dataflow():
                A1 = R.add(x, y)

                n = T.int64()
                m = T.int64()
                B1 = R.match_cast(A1, R.Tensor([n, m], "float32"))

                A2 = R.add(x, y)
                p = T.int64()
                q = T.int64()
                B2 = R.match_cast(A2, R.Tensor([p, q], "float32"))

                gv = R.multiply(B1, B2)
                R.output(gv)
            return gv

    @I.ir_module(s_tir=True)
    class Expected:
        @R.function
        def foo(x: R.Tensor(dtype="float32"), y: R.Tensor(dtype="float32")):
            with R.dataflow():
                A1 = R.add(x, y)
                n = T.int64()
                m = T.int64()
                B1 = R.match_cast(A1, R.Tensor([n, m], "float32"))

                A2 = A1
                p = T.int64()
                q = T.int64()
                B2 = R.match_cast(A1, R.Tensor([p, q], "float32"))

                gv = R.multiply(B1, B2)
                R.output(gv)
            return gv

    verify(Before, Expected)


def test_replace_binding_within_branch_with_duplicate_before_branch():
    """Bindings before a branch may be used within the branch"""

    @I.ir_module(s_tir=True)
    class Before:
        @R.function
        def foo(
            x: R.Tensor((2, 3), dtype="float32"),
            y: R.Tensor((2, 3), dtype="float32"),
            condition: R.Prim("bool"),
        ):
            A = R.add(x, y)
            if condition:
                B = R.add(x, y)
                C = R.multiply(x, B)
                D = R.multiply(A, C)
            else:
                B = R.add(x, y)
                C = R.multiply(y, B)
                D = R.multiply(A, C)
            return D

    @I.ir_module(s_tir=True)
    class Expected:
        @R.function
        def foo(
            x: R.Tensor((2, 3), dtype="float32"),
            y: R.Tensor((2, 3), dtype="float32"),
            condition: R.Prim("bool"),
        ):
            A = R.add(x, y)
            if condition:
                B = A
                C = R.multiply(x, A)
                D = R.multiply(A, C)
            else:
                B = A
                C = R.multiply(y, A)
                D = R.multiply(A, C)
            return D

    verify(Before, Expected)


def test_keep_duplicate_across_if_and_then():
    """Bindings in `if` are not valid within `else`"""

    @I.ir_module(s_tir=True)
    class Before:
        @R.function
        def foo(
            x: R.Tensor((2, 3), dtype="float32"),
            y: R.Tensor((2, 3), dtype="float32"),
            condition: R.Prim("bool"),
        ):
            if condition:
                A = R.add(x, y)
                B = R.multiply(x, A)
            else:
                A = R.add(x, y)
                B = R.multiply(y, A)
            return B

    Expected = Before

    verify(Before, Expected)


def test_keep_duplicate_after_branch():
    """Only the final binding is valid after a if/else branch"""

    @I.ir_module(s_tir=True)
    class Before:
        @R.function
        def foo(
            x: R.Tensor((2, 3), dtype="float32"),
            y: R.Tensor((2, 3), dtype="float32"),
            condition: R.Prim("bool"),
        ):
            if condition:
                A = R.add(x, y)
                B = R.multiply(x, A)
            else:
                A = R.add(x, y)
                B = R.multiply(y, A)

            C = R.add(x, y)
            D = R.multiply(B, C)
            return D

    Expected = Before

    verify(Before, Expected)


def test_keep_alloc_tensor():
    @I.ir_module(s_tir=True)
    class Before:
        @R.function
        def foo(x: R.Tensor((2, 3), dtype="float32")):
            tmp_buf1 = R.builtin.alloc_tensor(R.shape([64]), R.dtype("int32"), R.prim_value(0))
            tmp_buf2 = R.builtin.alloc_tensor(R.shape([64]), R.dtype("int32"), R.prim_value(0))
            out = R.add(tmp_buf1, tmp_buf2)
            return out

    Expected = Before

    verify(Before, Expected)


def test_keep_alloc_storage():
    @I.ir_module(s_tir=True)
    class Before:
        @R.function
        def foo(x: R.Tensor((2, 3), dtype="float32")):
            tmp_storage1 = R.vm.alloc_storage(R.shape([64]), runtime_device_index=0, dtype="uint8")
            tmp_buf1 = R.vm.alloc_tensor(tmp_storage1, offset=0, shape=R.shape([64]), dtype="int32")
            tmp_storage2 = R.vm.alloc_storage(R.shape([64]), runtime_device_index=0, dtype="uint8")
            tmp_buf2 = R.vm.alloc_tensor(tmp_storage2, offset=0, shape=R.shape([64]), dtype="int32")
            out = R.add(tmp_buf1, tmp_buf2)
            return out

    Expected = Before

    verify(Before, Expected)


if __name__ == "__main__":
    tvm.testing.main()
