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


import pytest

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


def test_rewrite_defined_by_ir_module():
    @R.rewriter
    class Rewriter:
        @R.function
        def pattern(A: R.Tensor([16], "float32"), B: R.Tensor([16], "float32")):
            C = R.add(A, B)
            return C

        @R.function
        def replacement(A: R.Tensor([16], "float32"), B: R.Tensor([16], "float32")):
            C = R.call_pure_packed("my_optimized_add_impl", A, B, ty_args=R.Tensor([16], "float32"))
            return C

    @R.function
    def before(x: R.Tensor([32], "float32")):
        R.func_attr({"global_symbol": "main"})
        split = R.split(x, 2)
        lhs = split[0]
        rhs = split[1]
        out = lhs + rhs
        return out

    @R.function
    def expected(x: R.Tensor([32], "float32")):
        R.func_attr({"global_symbol": "main"})
        split = R.split(x, 2)
        lhs = split[0]
        rhs = split[1]
        out = R.call_pure_packed(
            "my_optimized_add_impl", lhs, rhs, ty_args=R.Tensor([16], "float32")
        )
        return out

    after = Rewriter(before)
    tvm.ir.assert_structural_equal(expected, after)


def test_missing_pattern_raises_error():
    """The rewriter must define a pattern to be matched"""

    with pytest.raises(KeyError, match="pattern"):

        @R.rewriter
        class Rewriter:
            @R.function
            def replacement():
                return R.tuple()


def test_incorrect_function_type_of_pattern_raises_error():
    """The rewriter's pattern must be a Relax function"""

    with pytest.raises(TypeError, match="pattern"):

        @R.rewriter
        class Rewriter:
            @T.prim_func(s_tir=True)
            def pattern():
                pass

            @R.function
            def replacement():
                return R.tuple()


def test_missing_replacement_raises_error():
    """The rewriter must define a replacement"""

    with pytest.raises(KeyError, match="replacement"):

        @R.rewriter
        class Rewriter:
            @R.function
            def pattern():
                return R.tuple()


def test_incorrect_function_type_of_replacement_raises_error():
    """The rewriter's replacement must be a Relax function"""

    with pytest.raises(TypeError, match="replacement"):

        @R.rewriter
        class Rewriter:
            @R.function
            def pattern():
                return R.tuple()

            @T.prim_func(s_tir=True)
            def replacement():
                pass


def test_mismatch_of_static_shapes_raises_error():
    """The pattern and replacement must accept the same shapes"""

    with pytest.raises(ValueError, match="must have the same signature"):

        @R.rewriter
        class Rewriter:
            @R.function
            def pattern(A: R.Tensor([32])):
                return A

            @R.function
            def replacement(A: R.Tensor([16])):
                return A


def test_rewriter_may_be_applied_to_ir_module():
    """A rewriter may mutate an IRModule

    The `PatternMatchingRewriter.__call__` implementation may accept
    either a single Relax function, or an entire IRModule.  If it is
    passed an IRModule, then all functions in the `IRModule` are
    updated.

    """

    @R.rewriter
    class Rewriter:
        @R.function
        def pattern(A: R.Tensor([16], "float32"), B: R.Tensor([16], "float32")):
            C = R.add(A, B)
            return C

        @R.function
        def replacement(A: R.Tensor([16], "float32"), B: R.Tensor([16], "float32")):
            C = R.call_pure_packed("my_optimized_add_impl", A, B, ty_args=R.Tensor([16], "float32"))
            return C

    @I.ir_module
    class Before:
        @R.function
        def func_a(x: R.Tensor([32], "float32")):
            split = R.split(x, 2)
            lhs = split[0]
            rhs = split[1]
            out = lhs + rhs
            return out

        @R.function
        def func_b(x: R.Tensor([16], "float32")):
            out = x + x
            return out

    @I.ir_module
    class Expected:
        @R.function
        def func_a(x: R.Tensor([32], "float32")):
            split = R.split(x, 2)
            lhs = split[0]
            rhs = split[1]
            out = R.call_pure_packed(
                "my_optimized_add_impl", lhs, rhs, ty_args=R.Tensor([16], "float32")
            )
            return out

        @R.function
        def func_b(x: R.Tensor([16], "float32")):
            out = R.call_pure_packed(
                "my_optimized_add_impl", x, x, ty_args=R.Tensor([16], "float32")
            )
            return out

    After = Rewriter(Before)
    tvm.ir.assert_structural_equal(Expected, After)


def test_rewriter_may_be_used_as_ir_transform():
    """A rewriter may be used as a tvm.ir.transform.Pass"""

    @R.rewriter
    class Rewriter:
        @R.function
        def pattern(A: R.Tensor([16], "float32"), B: R.Tensor([16], "float32")):
            C = R.add(A, B)
            return C

        @R.function
        def replacement(A: R.Tensor([16], "float32"), B: R.Tensor([16], "float32")):
            C = R.call_pure_packed("my_optimized_add_impl", A, B, ty_args=R.Tensor([16], "float32"))
            return C

    @I.ir_module
    class Before:
        @R.function
        def main(x: R.Tensor([16], "float32")):
            y = x + x
            return y

    @I.ir_module
    class Expected:
        @R.function
        def main(x: R.Tensor([16], "float32")):
            out = R.call_pure_packed(
                "my_optimized_add_impl", x, x, ty_args=R.Tensor([16], "float32")
            )
            return out

    After = tvm.ir.transform.Sequential([Rewriter])(Before)
    tvm.ir.assert_structural_equal(Expected, After)


def test_same_pattern_applied_multiple_times():
    """The pattern-match may apply multiple times"""

    @R.rewriter
    class Rewriter:
        @R.function
        def pattern(A: R.Tensor([16], "float32"), B: R.Tensor([16], "float32")):
            C = R.add(A, B)
            return C

        @R.function
        def replacement(A: R.Tensor([16], "float32"), B: R.Tensor([16], "float32")):
            C = R.call_pure_packed("my_optimized_add_impl", A, B, ty_args=R.Tensor([16], "float32"))
            return C

    @R.function(private=True)
    def before(x: R.Tensor([16], "float32")):
        y = x + x
        z = y + y
        return z

    @R.function(private=True)
    def expected(x: R.Tensor([16], "float32")):
        y = R.call_pure_packed("my_optimized_add_impl", x, x, ty_args=R.Tensor([16], "float32"))
        z = R.call_pure_packed("my_optimized_add_impl", y, y, ty_args=R.Tensor([16], "float32"))
        return z

    after = Rewriter(before)
    tvm.ir.assert_structural_equal(expected, after)


def test_composition_of_rewrite_rules():
    """Rewrite rules may be composed together"""

    @R.rewriter
    class RewriteAdd:
        @R.function
        def pattern(A: R.Tensor([16], "float32"), B: R.Tensor([16], "float32")):
            C = A + B
            return C

        @R.function
        def replacement(A: R.Tensor([16], "float32"), B: R.Tensor([16], "float32")):
            C = R.call_pure_packed("my_optimized_add_impl", A, B, ty_args=R.Tensor([16], "float32"))
            return C

    @R.rewriter
    class RewriteMultiply:
        @R.function
        def pattern(A: R.Tensor([16], "float32"), B: R.Tensor([16], "float32")):
            C = A * B
            return C

        @R.function
        def replacement(A: R.Tensor([16], "float32"), B: R.Tensor([16], "float32")):
            C = R.call_pure_packed("my_optimized_mul_impl", A, B, ty_args=R.Tensor([16], "float32"))
            return C

    @R.function(private=True)
    def before(
        A: R.Tensor([16], "float32"),
        B: R.Tensor([16], "float32"),
        C: R.Tensor([16], "float32"),
    ):
        D = A + B
        E = C * D
        return E

    @R.function(private=True)
    def expected(
        A: R.Tensor([16], "float32"),
        B: R.Tensor([16], "float32"),
        C: R.Tensor([16], "float32"),
    ):
        D = R.call_pure_packed("my_optimized_add_impl", A, B, ty_args=R.Tensor([16], "float32"))
        E = R.call_pure_packed("my_optimized_mul_impl", C, D, ty_args=R.Tensor([16], "float32"))
        return E

    rewriter = RewriteAdd | RewriteMultiply

    after = rewriter(before)
    tvm.ir.assert_structural_equal(expected, after)


def test_recursive_rewrite_rules():
    """Rewrite rules are applied until convergence

    In this test, both the `RewriteAdd` and `RewriteMultiply` patterns
    must be applied in order to produce the expected output.  However,
    the `RewriteMultiply` pattern relies on the expression produced by
    the `RewriteAdd` pass.

    """

    @R.rewriter
    class RewriteAdd:
        @R.function
        def pattern(A: R.Tensor([16], "float32")):
            return A + A

        @R.function
        def replacement(A: R.Tensor([16], "float32")):
            return A * R.const(2.0, "float32")

    @R.rewriter
    class RewriteMultiply:
        @R.function
        def pattern(A: R.Tensor([16], "float32"), B: R.Tensor([], "float32")):
            C = A * B
            return C

        @R.function
        def replacement(A: R.Tensor([16], "float32"), B: R.Tensor([], "float32")):
            C = R.call_pure_packed("my_optimized_mul_impl", A, B, ty_args=R.Tensor([16], "float32"))
            return C

    @R.function(private=True)
    def before(A: R.Tensor([16], "float32")):
        B = A + A
        return B

    @R.function(private=True)
    def expected(A: R.Tensor([16], "float32")):
        B = R.call_pure_packed(
            "my_optimized_mul_impl",
            A,
            R.const(2.0, "float32"),
            ty_args=R.Tensor([16], "float32"),
        )
        return B

    rewriter = RewriteAdd | RewriteMultiply

    after = rewriter(before)
    tvm.ir.assert_structural_equal(expected, after)


def test_rewrite_may_introduce_private_relax_subroutines():
    """The replacement may contain subroutines"""

    @R.rewriter
    class Rewriter:
        @R.function
        def pattern(A: R.Tensor([16], "float32")):
            return A + A

        @R.function
        def replacement(A: R.Tensor([16], "float32")):
            return Rewriter.subroutine(A)

        @R.function(private=True)
        def subroutine(A: R.Tensor([16], "float32")) -> R.Tensor([16], "float32"):
            return A * R.const(2.0, "float32")

    @I.ir_module
    class Before:
        @R.function
        def main(A: R.Tensor([16], "float32")):
            B = A + A
            C = B + B
            return C

    @I.ir_module
    class Expected:
        @R.function
        def main(A: R.Tensor([16], "float32")):
            B = Expected.subroutine(A)
            C = Expected.subroutine(B)
            return C

        @R.function(private=True)
        def subroutine(A: R.Tensor([16], "float32")) -> R.Tensor([16], "float32"):
            return A * R.const(2.0, "float32")

    After = Rewriter(Before)
    tvm.ir.assert_structural_equal(Expected, After)


def test_rewrite_only_introduces_private_subroutines_when_required():
    """Only subroutines that are used will be added to the module

    Like `test_rewrite_may_introduce_private_relax_subroutines`, but
    the rewritten function only requires some of the subroutines
    provided by the rewriter.

    """

    @R.rewriter
    class RewriteAdd:
        @R.function
        def pattern(A: R.Tensor([16], "float32")):
            return A + A

        @R.function
        def replacement(A: R.Tensor([16], "float32")):
            return RewriteAdd.subroutine_add(A)

        @R.function(private=True)
        def subroutine_add(A: R.Tensor([16], "float32")) -> R.Tensor([16], "float32"):
            return A * R.const(2.0, "float32")

    @R.rewriter
    class RewriteMul:
        @R.function
        def pattern(A: R.Tensor([16], "float32")):
            return A * A

        @R.function
        def replacement(A: R.Tensor([16], "float32")):
            return R.call_tir(RewriteMul.subroutine_mul, [A], out_ty=R.Tensor([16], "float32"))

        @T.prim_func(private=True, s_tir=True)
        def subroutine_mul(A: T.Buffer(16, "float32"), B: T.Buffer(16, "float32")):
            for i in range(16):
                B[i] = A[i] * A[i]

    @I.ir_module
    class Before:
        @R.function
        def main(A: R.Tensor([16], "float32")):
            B = A + A
            C = B + B
            return C

    @I.ir_module
    class Expected:
        @R.function
        def main(A: R.Tensor([16], "float32")):
            B = Expected.subroutine_add(A)
            C = Expected.subroutine_add(B)
            return C

        @R.function(private=True)
        def subroutine_add(A: R.Tensor([16], "float32")) -> R.Tensor([16], "float32"):
            return A * R.const(2.0, "float32")

    rewriter = RewriteAdd | RewriteMul

    After = rewriter(Before)
    tvm.ir.assert_structural_equal(Expected, After)


def test_rewriter_may_not_introduce_public_subroutines():
    """The rewriter may only introduce private functions"""

    with pytest.raises(ValueError, match="is publicly exposed"):

        @R.rewriter
        class Rewriter:
            @R.function
            def pattern(A: R.Tensor([16], "float32")):
                return A + A

            @R.function
            def replacement(A: R.Tensor([16], "float32")):
                return Rewriter.subroutine(A)

            @R.function
            def subroutine(A: R.Tensor([16], "float32")) -> R.Tensor([16], "float32"):
                return A * R.const(2.0, "float32")


def test_rewrite_branches_may_reuse_subroutine_name():
    """Each rewriter is independent, and may reuse subroutine names"""

    @R.rewriter
    class RewriteAdd:
        @R.function
        def pattern(A: R.Tensor([16], "float32")):
            return A + A

        @R.function
        def replacement(A: R.Tensor([16], "float32")):
            return RewriteAdd.subroutine(A)

        @R.function(private=True)
        def subroutine(A: R.Tensor([16], "float32")) -> R.Tensor([16], "float32"):
            return A * R.const(2.0, "float32")

    @R.rewriter
    class RewriteMul:
        @R.function
        def pattern(A: R.Tensor([16], "float32")):
            return A * A

        @R.function
        def replacement(A: R.Tensor([16], "float32")):
            return R.call_tir(RewriteMul.subroutine, [A], out_ty=R.Tensor([16], "float32"))

        @T.prim_func(private=True, s_tir=True)
        def subroutine(A: T.Buffer(16, "float32"), B: T.Buffer(16, "float32")):
            for i in range(16):
                B[i] = A[i] * A[i]

    @I.ir_module
    class Before:
        @R.function
        def main(A: R.Tensor([16], "float32")):
            B = A + A
            C = B * B
            return C

    @I.ir_module
    class Expected:
        @R.function
        def main(A: R.Tensor([16], "float32")):
            B = Expected.subroutine(A)
            C = R.call_tir(Expected.subroutine_1, [B], out_ty=R.Tensor([16], "float32"))
            return C

        @R.function(private=True)
        def subroutine(A: R.Tensor([16], "float32")) -> R.Tensor([16], "float32"):
            return A * R.const(2.0, "float32")

        @T.prim_func(private=True, s_tir=True)
        def subroutine_1(A: T.Buffer(16, "float32"), B: T.Buffer(16, "float32")):
            for i in range(16):
                B[i] = A[i] * A[i]

    rewriter = RewriteAdd | RewriteMul

    After = rewriter(Before)
    tvm.ir.assert_structural_equal(Expected, After)


def test_rewrite_of_explicit_relax_tuple():
    """The rewriter function may return a tuple

    When it occurs explicitly within the Relax function, the tuple
    pattern matches against the Relax tuple, and the Relax tuple is
    replaced.

    """

    @R.rewriter
    class Rewriter:
        @R.function
        def pattern(
            lhs_A: R.Tensor([16, 16], "float32"),
            lhs_B: R.Tensor([16, 16], "float32"),
            rhs: R.Tensor([16], "float32"),
        ):
            proj_A = R.matmul(lhs_A, rhs)
            proj_B = R.matmul(lhs_B, rhs)
            proj_tuple = (proj_A, proj_B)
            return proj_tuple

        @R.function
        def replacement(
            lhs_A: R.Tensor([16, 16], "float32"),
            lhs_B: R.Tensor([16, 16], "float32"),
            rhs: R.Tensor([16], "float32"),
        ):
            lhs = R.concat([lhs_A, lhs_B])
            proj_concat = R.matmul(lhs, rhs)
            proj_tuple = R.split(proj_concat, 2)
            return proj_tuple

    @R.function(private=True)
    def before(
        state: R.Tensor([16], "float32"),
        A: R.Tensor([16, 16], "float32"),
        B: R.Tensor([16, 16], "float32"),
    ):
        proj_A = R.matmul(A, state)
        proj_B = R.matmul(B, state)
        proj_tuple = (proj_A, proj_B)
        out = proj_tuple[0] + proj_tuple[1]
        return out

    @R.function(private=True)
    def expected(
        state: R.Tensor([16], "float32"),
        A: R.Tensor([16, 16], "float32"),
        B: R.Tensor([16, 16], "float32"),
    ):
        concat_AB = R.concat([A, B])
        proj_concat = R.matmul(concat_AB, state)
        proj_tuple = R.split(proj_concat, 2)
        out = proj_tuple[0] + proj_tuple[1]
        return out

    after = Rewriter(before)
    tvm.ir.assert_structural_equal(expected, after)


def test_rewrite_of_output_relax_tuple():
    """The rewriter may update a tuple being returned

    Unlike most relax expressions, tuples may appear as nested
    expressions.  Pattern-matching should be aware of this option.

    Like `test_rewrite_of_explicit_relax_tuple`, but the tuple appears
    as the return value in the function being modified.

    """

    @R.rewriter
    class Rewriter:
        @R.function
        def pattern(
            lhs_A: R.Tensor([16, 16], "float32"),
            lhs_B: R.Tensor([16, 16], "float32"),
            rhs: R.Tensor([16], "float32"),
        ):
            proj_A = R.matmul(lhs_A, rhs)
            proj_B = R.matmul(lhs_B, rhs)
            proj_tuple = (proj_A, proj_B)
            return proj_tuple

        @R.function
        def replacement(
            lhs_A: R.Tensor([16, 16], "float32"),
            lhs_B: R.Tensor([16, 16], "float32"),
            rhs: R.Tensor([16], "float32"),
        ):
            lhs = R.concat([lhs_A, lhs_B])
            proj_concat = R.matmul(lhs, rhs)
            proj_tuple = R.split(proj_concat, 2)
            return proj_tuple

    @R.function(private=True)
    def before(
        state: R.Tensor([16], "float32"),
        A: R.Tensor([16, 16], "float32"),
        B: R.Tensor([16, 16], "float32"),
    ):
        proj_A = R.matmul(A, state)
        proj_B = R.matmul(B, state)
        return (proj_A, proj_B)

    @R.function(private=True)
    def expected(
        state: R.Tensor([16], "float32"),
        A: R.Tensor([16, 16], "float32"),
        B: R.Tensor([16, 16], "float32"),
    ):
        concat_AB = R.concat([A, B])
        proj_concat = R.matmul(concat_AB, state)
        proj_tuple = R.split(proj_concat, 2)
        return proj_tuple

    after = Rewriter(before)
    tvm.ir.assert_structural_equal(expected, after)


def test_rewrite_of_implicit_tuple():
    """The rewriter function may return a tuple

    The tuple being replaced does not need to explicitly exist within
    the updated Relax function.  So long as each element of the tuple
    pattern matches a Relax expression, the pattern match can apply.

    This rule ensures that pattern-matching is never broken when
    `CanonicalizeBindings` is applied.

    This test is identical to `test_rewrite_of_explicit_relax_tuple`,
    except that the function does not contain the round trip of
    packing `proj_A` and `proj_B` into a tuple, then immediately
    unpacking them from the tuple.

    """

    @R.rewriter
    class Rewriter:
        @R.function
        def pattern(
            lhs_A: R.Tensor([16, 16], "float32"),
            lhs_B: R.Tensor([16, 16], "float32"),
            rhs: R.Tensor([16], "float32"),
        ):
            proj_A = R.matmul(lhs_A, rhs)
            proj_B = R.matmul(lhs_B, rhs)
            proj_tuple = (proj_A, proj_B)
            return proj_tuple

        @R.function
        def replacement(
            lhs_A: R.Tensor([16, 16], "float32"),
            lhs_B: R.Tensor([16, 16], "float32"),
            rhs: R.Tensor([16], "float32"),
        ):
            lhs = R.concat([lhs_A, lhs_B])
            proj_concat = R.matmul(lhs, rhs)
            proj_tuple = R.split(proj_concat, 2)
            return proj_tuple

    @R.function(private=True)
    def before(
        state: R.Tensor([16], "float32"),
        A: R.Tensor([16, 16], "float32"),
        B: R.Tensor([16, 16], "float32"),
    ):
        proj_A = R.matmul(A, state)
        proj_B = R.matmul(B, state)
        out = proj_A + proj_B
        return out

    @R.function(private=True)
    def expected(
        state: R.Tensor([16], "float32"),
        A: R.Tensor([16, 16], "float32"),
        B: R.Tensor([16, 16], "float32"),
    ):
        concat_AB = R.concat([A, B])
        proj_concat = R.matmul(concat_AB, state)
        proj_tuple = R.split(proj_concat, 2)
        out = proj_tuple[0] + proj_tuple[1]
        return out

    after = Rewriter(before)
    tvm.ir.assert_structural_equal(expected, after)


def test_rewrite_of_implicit_tuple_with_shared_wildcard():
    """Tuple elements may depend on the same input

    Here, both elements of the tuple depend on `y`.

    """

    @R.rewriter
    class Rewriter:
        @R.function
        def pattern(
            x: R.Tensor([16], "float32"),
            y: R.Tensor([16], "float32"),
            z: R.Tensor([16], "float32"),
        ):
            lhs = x + y
            rhs = y + z
            return (lhs, rhs)

        @R.function
        def replacement(
            x: R.Tensor([16], "float32"),
            y: R.Tensor([16], "float32"),
            z: R.Tensor([16], "float32"),
        ):
            return R.call_pure_packed(
                "optimized_impl",
                x,
                y,
                z,
                ty_args=R.Tuple(
                    [
                        R.Tensor([16], "float32"),
                        R.Tensor([16], "float32"),
                    ]
                ),
            )

    @R.function(private=True)
    def before(
        A: R.Tensor([16], "float32"),
        B: R.Tensor([16], "float32"),
        C: R.Tensor([16], "float32"),
    ):
        lhs = A + B
        rhs = B + C
        out = R.multiply(lhs, rhs)
        return out

    @R.function(private=True)
    def expected(
        A: R.Tensor([16], "float32"),
        B: R.Tensor([16], "float32"),
        C: R.Tensor([16], "float32"),
    ):
        lhs_rhs = R.call_pure_packed(
            "optimized_impl",
            A,
            B,
            C,
            ty_args=R.Tuple(
                [
                    R.Tensor([16], "float32"),
                    R.Tensor([16], "float32"),
                ]
            ),
        )
        out = R.multiply(lhs_rhs[0], lhs_rhs[1])
        return out

    after = Rewriter(before)
    tvm.ir.assert_structural_equal(expected, after)


def test_no_rewrite_of_implicit_tuple_when_shared_wildcard_is_mismatched():
    """Tuple elements must match simultaneously

    Each element of the tuple matches individually, but the two
    elements both depend on `B`.  Because the first tuple element
    would require `y = B`, while the second tuple element would
    require `y = C`, the match fails.

    """

    @R.rewriter
    class Rewriter:
        @R.function
        def pattern(
            x: R.Tensor([16], "float32"),
            y: R.Tensor([16], "float32"),
            z: R.Tensor([16], "float32"),
        ):
            lhs = x + y
            rhs = y + z
            return (lhs, rhs)

        @R.function
        def replacement(
            A: R.Tensor([16], "float32"),
            B: R.Tensor([16], "float32"),
            C: R.Tensor([16], "float32"),
        ):
            return R.call_pure_packed(
                "optimized_impl",
                A,
                B,
                C,
                ty_args=R.Tuple(
                    [
                        R.Tensor([16], "float32"),
                        R.Tensor([16], "float32"),
                    ]
                ),
            )

    @R.function(private=True)
    def before(
        A: R.Tensor([16], "float32"),
        B: R.Tensor([16], "float32"),
        C: R.Tensor([16], "float32"),
        D: R.Tensor([16], "float32"),
    ):
        lhs = A + B
        rhs = C + D
        out = R.multiply(lhs, rhs)
        return out

    expected = before

    after = Rewriter(before)
    tvm.ir.assert_structural_equal(expected, after)


def test_implicit_tuple_may_not_introduce_extra_compute():
    """Matching of implicit tuple may not cause extra compute

    Here, the `(proj_A, proj_B)` tuple could be an implcit tuple
    match, but that would repeat the computation of `proj_A`.  It
    would be computed once on its own, to be used for `proj_A_on_B`,
    and once for computing `(proj_A, proj_B)`.

    """

    @R.rewriter
    class Rewriter:
        @R.function
        def pattern(
            lhs_A: R.Tensor([16, 16], "float32"),
            lhs_B: R.Tensor([16, 16], "float32"),
            rhs: R.Tensor([16, 16], "float32"),
        ):
            proj_A = R.matmul(lhs_A, rhs)
            proj_B = R.matmul(lhs_B, rhs)
            proj_tuple = (proj_A, proj_B)
            return proj_tuple

        @R.function
        def replacement(
            lhs_A: R.Tensor([16, 16], "float32"),
            lhs_B: R.Tensor([16, 16], "float32"),
            rhs: R.Tensor([16, 16], "float32"),
        ):
            lhs = R.concat([lhs_A, lhs_B])
            proj_concat = R.matmul(lhs, rhs)
            proj_tuple = R.split(proj_concat, 2)
            return proj_tuple

    @R.function(private=True)
    def before(
        state: R.Tensor([16, 16], "float32"),
        A: R.Tensor([16, 16], "float32"),
        B: R.Tensor([16, 16], "float32"),
    ):
        # This function has no location at which a tuple
        # `(proj_A,proj_B)` could be constructed, then unpacked.

        proj_A = R.matmul(A, state)

        # A tuple `(proj_A, proj_B)` could not be constructed at this
        # location, because `proj_B` has not yet been computed.

        proj_A_on_B = R.matmul(proj_A, B)
        proj_B = R.matmul(proj_A_on_B, state)

        # A tuple `(proj_A, proj_B)` could be constructed here, but a
        # use-site of `proj_A` has already occurred.  Implicit
        # matching of a tuple is only allowed if it would replace
        # every use-site of a variable.

        out = proj_A + proj_B
        return out

    expected = before

    after = Rewriter(before)
    tvm.ir.assert_structural_equal(expected, after)


def test_rewrite_of_implicit_tuple_with_three_elements():
    """Implicit tuples may contain three elements"""

    @R.rewriter
    class Rewriter:
        @R.function
        def pattern(qkv: R.Tensor([12288], "float32")):
            qkv_tuple = R.split(qkv, 3, axis=0)
            q = qkv_tuple[0]
            k = qkv_tuple[1]
            v = qkv_tuple[2]
            q_embed = R.call_pure_packed(
                "rotary_embedding", [q], ty_args=R.Tensor([4096], "float32")
            )
            k_embed = R.call_pure_packed(
                "rotary_embedding", [k], ty_args=R.Tensor([4096], "float32")
            )

            return (q_embed, k_embed, v)

        @R.function
        def replacement(qkv: R.Tensor([12288], "float32")):
            return R.call_pure_packed(
                "split_rotary_embedding",
                [qkv],
                ty_args=[
                    R.Tensor([4096], "float32"),
                    R.Tensor([4096], "float32"),
                    R.Tensor([4096], "float32"),
                ],
            )

    @R.function(private=True)
    def before(
        state: R.Tensor([4096], "float32"),
        proj_qkv: R.Tensor([12288, 4096], "float32"),
        kv_cache: R.Any,
    ):
        qkv = R.matmul(proj_qkv, state)
        qkv_tuple = R.split(qkv, 3, axis=0)
        q = qkv_tuple[0]
        k = qkv_tuple[1]
        v = qkv_tuple[2]
        q_embed = R.call_pure_packed("rotary_embedding", [q], ty_args=R.Tensor([4096], "float32"))
        k_embed = R.call_pure_packed("rotary_embedding", [k], ty_args=R.Tensor([4096], "float32"))

        attention = R.call_pure_packed(
            "compute_self_attention",
            [q_embed, k_embed, v, kv_cache],
            ty_args=R.Tensor([4096]),
        )

        return attention

    @R.function(private=True)
    def expected(
        state: R.Tensor([4096], "float32"),
        proj_qkv: R.Tensor([12288, 4096], "float32"),
        kv_cache: R.Any,
    ):
        qkv = R.matmul(proj_qkv, state)
        embedded_qkv_tuple = R.call_pure_packed(
            "split_rotary_embedding",
            [qkv],
            ty_args=[
                R.Tensor([4096], "float32"),
                R.Tensor([4096], "float32"),
                R.Tensor([4096], "float32"),
            ],
        )

        v = embedded_qkv_tuple[2]
        q_embed = embedded_qkv_tuple[0]
        k_embed = embedded_qkv_tuple[1]

        attention = R.call_pure_packed(
            "compute_self_attention",
            [q_embed, k_embed, v, kv_cache],
            ty_args=R.Tensor([4096]),
        )

        return attention

    after = Rewriter(before)
    tvm.ir.assert_structural_equal(expected, after)


def test_pattern_matching_may_not_reorder_across_impure_functions():
    """Matched pattern must be ordered with respect to impure functions

    To ensure that debug printouts, memory management, performance
    measurements, etc are not impacted by a pattern match, a pattern
    must be entirely before, or entirely after an impure function.  A
    pattern match in which some parts of the matched expression are
    performed before an impure function, while others are performed
    afterwards, is not allowed.

    In this test, the matmul and the add may not be fused, because the
    impure print statement occurs between them.

    """

    @R.rewriter
    class Rewriter:
        @R.function
        def pattern(
            state: R.Tensor([16], "float32"),
            weights: R.Tensor([16, 16], "float32"),
            bias: R.Tensor([16], "float32"),
        ):
            state = R.matmul(weights, state)
            state = R.add(bias, state)
            return state

        @R.function
        def replacement(
            state: R.Tensor([16], "float32"),
            weights: R.Tensor([16, 16], "float32"),
            bias: R.Tensor([16], "float32"),
        ):
            return R.call_pure_packed(
                "my_optimized_fma_impl",
                state,
                weights,
                bias,
                ty_args=R.Tensor([16], "float32"),
            )

    @R.function(private=True, pure=False)
    def before(
        state: R.Tensor([16], "float32"),
        weights: R.Tensor([16, 16], "float32"),
        bias: R.Tensor([16], "float32"),
    ):
        R.print(format="Start of function")
        state = R.matmul(weights, state)
        R.print(format="After matmul, before add")
        state = R.add(bias, state)
        R.print(format="End of function")
        return state

    expected = before

    after = Rewriter(before)
    tvm.ir.assert_structural_equal(expected, after)


def test_pattern_matching_may_occur_between_impure_functions():
    """Matched pattern may be adjacent to impure functions

    To ensure that debug printouts, memory management, performance
    measurements, etc are not impacted by a pattern match, a pattern
    must be entirely before, or entirely after an impure function.  A
    pattern match in which some parts of the matched expression are
    performed before an impure function, while others are performed
    afterwards, is not allowed.

    In this test, the matmul and the add may be fused, because the
    pattern occurs without an impure print statement in-between.

    """

    @R.rewriter
    class Rewriter:
        @R.function
        def pattern(
            state: R.Tensor([16], "float32"),
            weights: R.Tensor([16, 16], "float32"),
            bias: R.Tensor([16], "float32"),
        ):
            state = R.matmul(weights, state)
            state = R.add(bias, state)
            return state

        @R.function
        def replacement(
            state: R.Tensor([16], "float32"),
            weights: R.Tensor([16, 16], "float32"),
            bias: R.Tensor([16], "float32"),
        ):
            return R.call_pure_packed(
                "my_optimized_fma_impl",
                state,
                weights,
                bias,
                ty_args=R.Tensor([16], "float32"),
            )

    @R.function(private=True, pure=False)
    def before(
        state: R.Tensor([16], "float32"),
        weights: R.Tensor([16, 16], "float32"),
        bias: R.Tensor([16], "float32"),
    ):
        R.print(format="Start of function")
        state = R.matmul(weights, state)
        state = R.add(bias, state)
        R.print(format="End of function")
        return state

    @R.function(private=True, pure=False)
    def expected(
        state: R.Tensor([16], "float32"),
        weights: R.Tensor([16, 16], "float32"),
        bias: R.Tensor([16], "float32"),
    ):
        R.print(format="Start of function")
        state = R.call_pure_packed(
            "my_optimized_fma_impl",
            state,
            weights,
            bias,
            ty_args=R.Tensor([16], "float32"),
        )
        R.print(format="End of function")
        return state

    after = Rewriter(before)
    tvm.ir.assert_structural_equal(expected, after)


def test_rewrite_may_apply_within_conditional():
    """Rewrites may apply within to inner dataflow regions

    While dataflow regions may not contain conditionals, they may
    occur within the body of conditionals.

    """

    @R.rewriter
    class Rewriter:
        @R.function
        def pattern(A: R.Tensor([16], "float32"), B: R.Tensor([16], "float32")):
            return A + B

        @R.function
        def replacement(A: R.Tensor([16], "float32"), B: R.Tensor([16], "float32")):
            return R.call_pure_packed(
                "my_optimized_add_impl", A, B, ty_args=R.Tensor([16], "float32")
            )

    @R.function(private=True)
    def before(A: R.Tensor([16], "float32"), B: R.Tensor([16], "float32"), cond: R.Prim("bool")):
        if cond:
            out = A + B
        else:
            C = A + B
            out = C + B
        return out

    @R.function(private=True)
    def expected(A: R.Tensor([16], "float32"), B: R.Tensor([16], "float32"), cond: R.Prim("bool")):
        if cond:
            out = R.call_pure_packed(
                "my_optimized_add_impl", A, B, ty_args=R.Tensor([16], "float32")
            )
        else:
            C = R.call_pure_packed("my_optimized_add_impl", A, B, ty_args=R.Tensor([16], "float32"))
            out = R.call_pure_packed(
                "my_optimized_add_impl", C, B, ty_args=R.Tensor([16], "float32")
            )
        return out

    after = Rewriter(before)
    tvm.ir.assert_structural_equal(expected, after)


def test_match_dynamic_shape():
    """Pattern match/rewrites may be dynamic

    The tuple being replaced does not need to explicitly exist within
    the updated Relax function.  So long as each element of the tuple
    pattern matches a Relax expression, the pattern match can apply.

    This rule ensures that pattern-matching is never broken when
    `CanonicalizeBindings` is applied.

    This test is identical to `test_rewrite_of_explicit_relax_tuple`,
    except that the function does not contain the round trip of
    packing `proj_A` and `proj_B` into a tuple, then immediately
    unpacking them from the tuple.

    """

    @R.rewriter
    class Rewriter:
        @R.function
        def pattern(
            lhs_A: R.Tensor(["N1", "M"], "float32"),
            lhs_B: R.Tensor(["N2", "M"], "float32"),
            rhs: R.Tensor(["M"], "float32"),
        ):
            proj_A = R.matmul(lhs_A, rhs)
            proj_B = R.matmul(lhs_B, rhs)
            return (proj_A, proj_B)

        @R.function
        def replacement(
            lhs_A: R.Tensor(["N1", "M"], "float32"),
            lhs_B: R.Tensor(["N2", "M"], "float32"),
            rhs: R.Tensor(["M"], "float32"),
        ):
            N1 = T.int64()
            N2 = T.int64()

            lhs = R.concat([lhs_A, lhs_B])
            proj_concat = R.matmul(lhs, rhs)
            proj_A: R.Tensor([N1], "float32") = R.strided_slice(
                proj_concat, axes=[0], begin=[0], end=[N1]
            )
            proj_B: R.Tensor([N2], "float32") = R.strided_slice(
                proj_concat, axes=[0], begin=[N1], end=[N2 + N1]
            )
            return (proj_A, proj_B)

    @R.function(private=True)
    def before(
        state: R.Tensor([16], "float32"),
        A: R.Tensor([16, 16], "float32"),
        B: R.Tensor([16, 16], "float32"),
    ):
        proj_A = R.matmul(A, state)
        proj_B = R.matmul(B, state)
        out = proj_A + proj_B
        return out

    @R.function(private=True)
    def expected(
        state: R.Tensor([16], "float32"),
        A: R.Tensor([16, 16], "float32"),
        B: R.Tensor([16, 16], "float32"),
    ):
        N1 = T.int64()
        M = T.int64()
        N2 = T.int64()
        with R.dataflow():
            lhs_A = R.match_cast(A, R.Tensor([N1, M], "float32"))
            lhs_B = R.match_cast(B, R.Tensor([N2, M], "float32"))
            R.output(lhs_A, lhs_B)
        concat_AB: R.Tensor([32, 16], "float32") = R.concat([lhs_A, lhs_B])
        proj_concat: R.Tensor([32], "float32") = R.matmul(concat_AB, state)
        proj_A: R.Tensor([16], "float32") = R.strided_slice(
            proj_concat, axes=[0], begin=[0], end=[N1]
        )
        proj_B: R.Tensor([16], "float32") = R.strided_slice(
            proj_concat, axes=[0], begin=[N1], end=[N2 + N1]
        )
        out = proj_A + proj_B
        return out

    after = Rewriter(before)
    tvm.ir.assert_structural_equal(expected, after)


def test_match_dynamic_pattern_against_dynamic_shape():
    """A dynamic pattern may match a static shape"""

    @R.rewriter
    class Rewriter:
        @R.function
        def pattern(
            A: R.Tensor(["M", "N"], "float32"),
            B: R.Tensor(["N", "N"], "float32"),
        ):
            return R.matmul(A, B)

        @R.function
        def replacement(
            A: R.Tensor(["M", "N"], "float32"),
            B: R.Tensor(["N", "N"], "float32"),
        ):
            M = T.int64()
            N = T.int64()
            return R.call_pure_packed(
                "my_optimized_square_matmul",
                A,
                B,
                ty_args=R.Tensor([M, N], "float32"),
            )

    @R.function(private=True)
    def before(
        A: R.Tensor(["N", "N*2"], "float32"),
        B: R.Tensor(["N*2", "N*2"], "float32"),
        C: R.Tensor(["N", "N"], "float32"),
    ):
        N = T.int64()
        D: R.Tensor([N, N * 2], "float32") = R.matmul(A, B)
        E: R.Tensor([N * 2, N], "float32") = R.permute_dims(D)
        F: R.Tensor([N * 2, N], "float32") = R.matmul(E, C)
        return F

    @R.function(private=True)
    def expected(
        A: R.Tensor(["N", "N*2"], "float32"),
        B: R.Tensor(["N*2", "N*2"], "float32"),
        C: R.Tensor(["N", "N"], "float32"),
    ):
        N = T.int64()

        D: R.Tensor([N, N * 2], "float32") = R.call_pure_packed(
            "my_optimized_square_matmul",
            A,
            B,
            ty_args=R.Tensor([N, N * 2], "float32"),
        )
        E: R.Tensor([N * 2, N], "float32") = R.permute_dims(D)
        F: R.Tensor([N * 2, N], "float32") = R.call_pure_packed(
            "my_optimized_square_matmul",
            E,
            C,
            ty_args=R.Tensor([N * 2, N], "float32"),
        )
        return F

    after = Rewriter(before)
    tvm.ir.assert_structural_equal(expected, after)


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