# 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: F811, F841


import pytest

import tvm
import tvm.testing
from tvm import relax as rx
from tvm import tirx
from tvm.relax.analysis import (
    all_global_vars,
    all_vars,
    bound_vars,
    free_vars,
    has_reshape_pattern,
    name_to_binding,
    remove_all_unused,
    udchain,
    used_vars,
)
from tvm.script import ir as I
from tvm.script import relax as R
from tvm.script import tirx as T


def var_name_set(vars: list[rx.Var | rx.GlobalVar]) -> set[str]:
    return {v.name if isinstance(v, rx.Var) else v.name_hint for v in vars}


def test_use_def():
    m = tirx.Var("m", "int64")
    n = tirx.Var("n", "int64")
    x = rx.Var("x", R.Tensor([m, n], "float16"))
    y = rx.Var("y", R.Tensor([n], "float16"))
    ib = rx.BlockBuilder()
    with ib.function("func", [x, y]):
        with ib.dataflow():
            lv0 = ib.emit(rx.op.add(x, y))
            lv1 = ib.emit(rx.op.multiply(lv0, y))
            gv0 = ib.emit_output(lv1)
        ib.emit_func_output(gv0)
    dfb = ib.get()["func"].body.blocks[0]
    udc = udchain(dfb)
    assert set(udc[x]) == {lv0}
    assert set(udc[y]) == {lv0, lv1}
    assert set(udc[lv0]) == {lv1}
    assert set(udc[lv1]) == {gv0}
    assert set(udc[gv0]) == set()


@pytest.mark.parametrize(
    "expr_fn, expected_var_names",
    [
        (lambda x, y, z: rx.op.add(x, y), {"x", "y"}),
        (lambda x, y, z: rx.op.multiply(x, x), {"x"}),
        (lambda x, y, z: rx.Tuple([x, y, z]), {"x", "y", "z"}),
    ],
    ids=["binary_op", "self_reference", "tuple"],
)
def test_used_vars(expr_fn, expected_var_names):
    m = tirx.Var("m", "int64")
    n = tirx.Var("n", "int64")
    x = rx.Var("x", R.Tensor([m, n], "float16"))
    y = rx.Var("y", R.Tensor([n], "float16"))
    z = rx.Var("z", R.Tensor([m], "float16"))

    expr = expr_fn(x, y, z)
    result = used_vars(expr)
    assert var_name_set(result) == expected_var_names


def test_chained_remove_all_unused():
    @tvm.script.ir_module
    class IdentityUnused:
        @R.function
        def main(x: R.Tensor((32, 32), "float32")) -> R.Tensor:
            with R.dataflow():
                lv0 = x
                unused0 = R.call_dps_packed("my_sigmoid", (x,), R.Tensor((32, 32), dtype="float32"))
                unused1 = R.call_dps_packed(
                    "my_dps_func", (unused0,), R.Tensor((32, 32), dtype="float32")
                )
                R.output(lv0)
            return lv0

    optimized = remove_all_unused(IdentityUnused["main"])

    @tvm.script.ir_module
    class GroundTruth:
        @R.function
        def main(x: R.Tensor((32, 32), "float32")) -> R.Tensor:
            with R.dataflow():
                lv0 = x
                R.output(lv0)
            return lv0

    tvm.ir.assert_structural_equal(optimized, GroundTruth["main"])


def test_binding_block_remove_all_unused():
    """Remove unused dataflow bindings

    Removal of unused bindings may not remove side effects.  Since
    bindings within a dataflow block are guaranteed not to have side
    effects, they may be removed if unused.
    """

    @tvm.script.ir_module
    class IdentityUnused:
        @R.function(pure=False)
        def main(x: R.Tensor((32, 32), "float32")) -> R.Tensor:
            with R.dataflow():
                lv0 = x
                unused0 = R.call_dps_packed("my_sigmoid", (x,), R.Tensor((32, 32), dtype="float32"))
                unused1 = R.call_dps_packed(
                    "my_dps_func", (unused0,), R.Tensor((32, 32), dtype="float32")
                )
                R.output(lv0)
            z = R.call_packed("vm.builtin.copy", lv0, ty_args=(R.Tensor((32, 32), "float32")))
            return z

    optimized = remove_all_unused(IdentityUnused["main"])

    @tvm.script.ir_module
    class GroundTruth:
        @R.function(pure=False)
        def main(x: R.Tensor((32, 32), "float32")) -> R.Tensor:
            with R.dataflow():
                lv0 = x
                R.output(lv0)
            z = R.call_packed("vm.builtin.copy", lv0, ty_args=(R.Tensor((32, 32), "float32")))
            return z

    tvm.ir.assert_structural_equal(optimized, GroundTruth["main"])


def test_binding_block_remove_unused_pure_without_dataflow():
    """Remove unused dataflow bindings

    Removal of unused bindings may not remove side effects.  Unused
    bindings whose value is a pure operation
    (e.g. `R.call_dps_packed`) may be removed, even if outside of a
    dataflow block.
    """

    @R.function(private=True)
    def before(x: R.Tensor((32, 32), "float32")) -> R.Tensor:
        lv0 = x
        unused0 = R.call_dps_packed("my_sigmoid", (x,), R.Tensor((32, 32), dtype="float32"))
        unused1 = R.call_dps_packed("my_dps_func", (unused0,), R.Tensor((32, 32), dtype="float32"))
        return x

    @R.function(private=True)
    def expected(x: R.Tensor((32, 32), "float32")) -> R.Tensor:
        return x

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


def test_binding_block_keep_impure_without_dataflow():
    """Remove unused dataflow bindings

    Removal of unused bindings may not remove side effects.  Unused
    bindings whose value is an impure operation (e.g. `R.call_packed`)
    may not be removed, as outside of a dataflow block they may
    contain side effects.
    """

    @R.function(private=True, pure=False)
    def before(x: R.Tensor((32, 32), "float32")) -> R.Tensor:
        lv0 = x
        y = R.call_packed("vm.builtin.copy", lv0, ty_args=(R.Tensor((32, 32), "float32")))
        return y

    expected = before

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


def test_binding_block_keep_pure_func_used_only_for_impure():
    """Keep bindings that are used for impure functions

    Removal of unused bindings may not result in use of undefined
    variables.  Unused bindings whose value is an impure operation
    (e.g. `R.call_packed`) may not be removed, nor may any of their
    inputs.

    This is a regression test to catch an earlier failure mode, in
    which tracking of unused variables only back-propagated from the
    return value of functions, and did not consider variables that
    were required to execute impure function calls.  In that failure
    mode, the binding of `y` would be removed as unused, even though
    it was required to evaluate the packed function.
    """

    @R.function(private=True, pure=False)
    def before(x: R.Tensor((32, 32), "int32")):
        y = x * R.const(2)
        z = R.call_packed(
            "function_maybe_with_side_effects", y, ty_args=(R.Tensor((32, 32), "int32"))
        )
        return R.tuple()

    expected = before

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


def test_binding_block_remove_all_unused_func_without_dataflow():
    @tvm.script.ir_module
    class IdentityUnused:
        @R.function(pure=False)
        def main(x: R.Tensor((32, 32), "float32")) -> R.Tensor:
            lv0 = x

            @R.function
            def internal_unused_func(A: R.Tensor((32, 32), "float32")) -> R.Tensor:
                return A

            z = R.call_packed("vm.builtin.copy", lv0, ty_args=(R.Tensor((32, 32), "float32")))
            return z

    optimized = remove_all_unused(IdentityUnused["main"])

    @tvm.script.ir_module
    class GroundTruth:
        @R.function(pure=False)
        def main(x: R.Tensor((32, 32), "float32")) -> R.Tensor:
            lv0 = x
            z = R.call_packed("vm.builtin.copy", lv0, ty_args=(R.Tensor((32, 32), "float32")))
            return z

    tvm.ir.assert_structural_equal(optimized, GroundTruth["main"])


def test_binding_block_fake_unused_remove_all_unused():
    @tvm.script.ir_module
    class IdentityUnused:
        @R.function(pure=False)
        def main(x: R.Tensor((32, 32), "float32")) -> R.Tensor:
            with R.dataflow():
                lv0 = x
                R.output(lv0)
            z = R.call_packed("vm.builtin.copy", lv0, ty_args=(R.Tensor((32, 32), "float32")))
            return lv0

    optimized = remove_all_unused(IdentityUnused["main"])

    @tvm.script.ir_module
    class GroundTruth:
        @R.function(pure=False)
        def main(x: R.Tensor((32, 32), "float32")) -> R.Tensor:
            with R.dataflow():
                lv0 = x
                R.output(lv0)
            # This might bring side effect so cannot be removed.
            z = R.call_packed("vm.builtin.copy", lv0, ty_args=(R.Tensor((32, 32), "float32")))
            return lv0

    tvm.ir.assert_structural_equal(optimized, GroundTruth["main"])


def test_edge_binding_block_fake_unused_remove_all_unused():
    @tvm.script.ir_module
    class IdentityUnused:
        @R.function(pure=False)
        def main(x: R.Tensor((32, 32), "float32")) -> R.Tensor((32, 32), "float32"):
            z = R.call_packed("vm.builtin.copy", x, ty_args=(R.Tensor((32, 32), "float32")))
            return x

    optimized = remove_all_unused(IdentityUnused["main"])
    tvm.ir.assert_structural_equal(optimized, IdentityUnused["main"])


def test_edge_binding_block_fake_unused_remove_all_unused2():
    @tvm.script.ir_module
    class IdentityUnused:
        @R.function
        def main(x: R.Tensor((3,), dtype="int64")) -> R.Tensor(dtype="int32", ndim=3):
            m = T.int64()
            n = T.int64()
            k = T.int64()
            with R.dataflow():
                lv: R.Shape(ndim=3) = R.call_pure_packed(
                    "vm.builtin.tensor_to_shape", x, ty_args=(R.Shape(ndim=3),)
                )
                lv1: R.Shape([m, n, k]) = R.match_cast(lv, R.Shape([m, n, k]))
                gv: R.Tensor((m, n, k), dtype="int32") = R.full(
                    R.shape([m, n, k]), R.const(1, "int32"), dtype="int32"
                )
                R.output(gv)
            return gv

    optimized = remove_all_unused(IdentityUnused["main"])
    tvm.ir.assert_structural_equal(optimized, IdentityUnused["main"])


def test_remove_all_unused_from_dataflow_block():
    """Like test_chained_remove_all_unused, but on a SeqExpr"""

    @R.function
    def before(x: R.Tensor((32, 32), "float32")) -> R.Tensor:
        with R.dataflow():
            lv0 = x
            unused0 = R.call_dps_packed("my_sigmoid", (x,), R.Tensor((32, 32), dtype="float32"))
            unused1 = R.call_dps_packed(
                "my_dps_func", (unused0,), R.Tensor((32, 32), dtype="float32")
            )
            R.output(lv0)
        return lv0

    @R.function
    def expected(x: R.Tensor((32, 32), "float32")) -> R.Tensor:
        with R.dataflow():
            lv0 = x
            R.output(lv0)
        return lv0

    after = remove_all_unused(before.body)
    tvm.ir.assert_structural_equal(expected.body, after, map_free_vars=True)


def test_remove_all_unused_from_binding_block():
    """Like test_chained_remove_all_unused, but on a SeqExpr"""

    @R.function
    def before(x: R.Tensor((32, 32), "float32")) -> R.Tensor:
        lv0 = x
        unused0 = R.call_dps_packed("my_sigmoid", (x,), R.Tensor((32, 32), dtype="float32"))
        unused1 = R.call_dps_packed("my_dps_func", (unused0,), R.Tensor((32, 32), dtype="float32"))
        return lv0

    @R.function
    def expected(x: R.Tensor((32, 32), "float32")) -> R.Tensor:
        lv0 = x
        return lv0

    after = remove_all_unused(before.body)
    tvm.ir.assert_structural_equal(expected.body, after, map_free_vars=True)


def test_retain_impure_calls_unused_in_binding_block():
    """An impure call may have side effects, and must be kept"""

    @R.function(pure=False)
    def before(x: R.Tensor((32, 32), "float32")) -> R.Tensor:
        lv0 = x
        unused0 = R.call_packed("my_impure_call", x, ty_args=R.Tensor((32, 32), dtype="float32"))
        unused1 = R.call_dps_packed("my_unused_call", (lv0,), R.Tensor((32, 32), dtype="float32"))
        return lv0

    @R.function(pure=False)
    def expected(x: R.Tensor((32, 32), "float32")) -> R.Tensor:
        lv0 = x
        unused0 = R.call_packed("my_impure_call", x, ty_args=R.Tensor((32, 32), dtype="float32"))
        return lv0

    after = remove_all_unused(before.body)
    tvm.ir.assert_structural_equal(expected.body, after, map_free_vars=True)


def test_retain_calls_to_impure_builtin_ops():
    @I.ir_module(s_tir=True)
    class Module:
        @T.prim_func(private=True, s_tir=True)
        def my_tir(A: T.handle, B: T.handle, n: T.int64):
            T.evaluate(0)

        @R.function(pure=False)
        def main(x: R.Tensor(("n",), "float32")):
            cls = Module
            n = T.int64()
            storage = R.memory.alloc_storage((n * 4,), 0, "global", "float32")
            alloc = R.memory.alloc_tensor(storage, R.prim_value(0), R.shape([n]), "float32")
            # "call_tir_dyn" is impure which shouldn't be removed.
            R.vm.call_tir_dyn(cls.my_tir, (x, alloc, R.shape([n])))
            # "kill_tensor"/"kill_storage" are impure which shouldn't be removed.
            R.memory.kill_tensor(alloc)
            R.memory.kill_storage(storage)
            return x

    after = remove_all_unused(Module["main"])
    tvm.ir.assert_structural_equal(after, Module["main"], map_free_vars=True)


def test_name_to_binding_var_shadowing():
    @R.function
    def main(x: R.Tensor((32, 32), "float32")) -> R.Tensor:
        with R.dataflow():
            lv0 = x
            lv1 = lv0
            R.output(lv1)

        with R.dataflow():
            lv0 = lv1  # shadowing
            lv2 = lv0
            R.output(lv2)
        return lv2

    n2binding = name_to_binding(main)

    assert "lv0" in n2binding
    assert "lv1" in n2binding
    assert "lv2" in n2binding

    assert len(n2binding["lv0"]) == 2


@tvm.script.ir_module
class VarExample:
    @R.function
    def func(a: R.Tensor) -> R.Tensor:
        # normalized into assigning R.add(a, a) to a var and returning it
        return R.add(a, a)

    @R.function
    def main(x: R.Tensor, y: R.Tensor) -> R.Tensor:
        cls = VarExample
        z = R.add(x, y)
        # no binding here
        _ = R.match_cast(x, R.Tensor((5, 5)))
        with R.dataflow():
            q = R.add(z, z)
            p = cls.func(q)
            r = R.match_cast(p, R.Tensor((5, 5)))
            s = r
            R.output(s)
        return s


def test_all_vars():
    vars = all_vars(VarExample["func"])
    assert len(vars) == 2
    assert vars[0].name == "a"
    # the body of the seq expr in the func body is a var
    assert vars[1] == VarExample["func"].body.body

    var_names = var_name_set(all_vars(VarExample["main"]))
    assert var_names == {"_", "x", "y", "z", "p", "q", "r", "s"}


def test_all_vars_from_expr_using_dataflow():
    """all_vars() should return all Var, including DataflowVar"""
    func = VarExample["main"]
    cls_func_q = func.body.blocks[1].bindings[1].value

    var_names = var_name_set(all_vars(cls_func_q))
    assert var_names == {"q"}


def test_bound_vars():
    vars = bound_vars(VarExample["func"])
    assert len(vars) == 2
    assert vars[0].name == "a"
    # the body of the seq expr in the func body is a bound var
    assert vars[1] == VarExample["func"].body.body

    # all the vars are bound
    var_names = var_name_set(bound_vars(VarExample["main"]))
    assert var_names == {"_", "x", "y", "z", "p", "q", "r", "s"}

    # if we consider only the body, then the function arguments are not bound
    body_names = var_name_set(bound_vars(VarExample["main"].body))
    assert body_names == {"_", "z", "p", "q", "r", "s"}

    # only binding is in the (normalized) body
    simple_body_vars = bound_vars(VarExample["func"].body)
    assert len(simple_body_vars) == 1
    assert simple_body_vars[0] == VarExample["func"].body.body


def test_free_vars():
    # all the vars are bound
    assert len(free_vars(VarExample["func"])) == 0
    assert len(free_vars(VarExample["main"])) == 0

    # the arguments are free if we look only at the bodies
    func_free = var_name_set(free_vars(VarExample["func"].body))
    main_free = var_name_set(free_vars(VarExample["main"].body))
    assert len(func_free) == 1
    assert len(main_free) == 2
    assert "a" in func_free
    assert main_free == {"x", "y"}

    # function that captures vars
    x = rx.Var("x", R.Tensor(ndim=-1))
    y = rx.Var("y", R.Tensor(ndim=-1))
    z = rx.Var("z", R.Tensor(ndim=-1))
    inner = rx.Function(
        [z],
        rx.op.add(x, rx.op.add(y, z)),
        ret_ty=R.Tensor(ndim=-1),
    )
    outer = rx.Function(
        [x, y],
        rx.Call(inner, [y]),
        ret_ty=R.Tensor(ndim=-1),
    )
    assert len(free_vars(outer)) == 0
    assert var_name_set(free_vars(inner)) == {"x", "y"}


@pytest.mark.parametrize("definition_site", ["parameter", "match_cast"])
def test_free_vars_primitive_definition_sites(definition_site):
    n = tirx.Var("n", "int64")
    if definition_site == "parameter":
        y = rx.Var("y", rx.TensorType([n], "float32"))
    else:
        y = rx.Var("y", rx.TensorType(ndim=1, dtype="float32"))

    bb = rx.BlockBuilder()
    with bb.function("inner", [y]):
        if definition_site == "match_cast":
            bb.match_cast(y, rx.TensorType([n], "float32"))
        output = bb.emit(rx.op.ones((n,), "float32"))
        bb.emit_func_output(output)

    inner = bb.get()["inner"]
    assert rx.analysis.check_well_formed(inner)
    assert not free_vars(inner)


def test_all_global_vars():
    # there is one call to "func"
    global_vars = all_global_vars(VarExample["main"])
    assert len(global_vars) == 1
    assert global_vars[0].name_hint == "func"

    gv1 = rx.GlobalVar("gv1")
    gv2 = rx.GlobalVar("gv2")
    gv3 = rx.GlobalVar("gv3")
    call = rx.Call(gv1, [gv2, gv3])
    call_var_names = var_name_set(all_global_vars(call))
    assert call_var_names == {"gv1", "gv2", "gv3"}


def test_reshape_pattern_reshape():
    @T.prim_func(s_tir=True)
    def reshape(
        rxplaceholder: T.Buffer((1, 2, 3, 4), "float32"),
        T_reshape: T.Buffer((8, 3), "float32"),
    ):
        for i0, i1 in T.grid(8, 3):
            with T.sblock("T_reshape"):
                ax0, ax1 = T.axis.remap("SS", [i0, i1])
                T.reads(
                    rxplaceholder[
                        (ax0 * 3 + ax1) // 24,
                        (ax0 * 3 + ax1) % 24 // 12,
                        (ax0 * 3 + ax1) % 12 // 4,
                        (ax0 * 3 + ax1) % 4,
                    ]
                )
                T.writes(T_reshape[ax0, ax1])
                T_reshape[ax0, ax1] = rxplaceholder[
                    (ax0 * 3 + ax1) // 24,
                    (ax0 * 3 + ax1) % 24 // 12,
                    (ax0 * 3 + ax1) % 12 // 4,
                    (ax0 * 3 + ax1) % 4,
                ]

    assert has_reshape_pattern(reshape)


def test_reshape_pattern_reshape_scheduled():
    @T.prim_func(s_tir=True)
    def reshape_scheduled(
        rxplaceholder: T.Buffer((1, 2, 3, 4), "float32"),
        T_reshape: T.Buffer((8, 3), "float32"),
    ):
        for i0_i1_fused_0 in T.thread_binding(1, thread="blockIdx.x"):
            for i0_i1_fused_1 in T.thread_binding(24, thread="threadIdx.x"):
                with T.sblock("T_reshape"):
                    ax0 = T.axis.spatial(8, (i0_i1_fused_0 * 24 + i0_i1_fused_1) // 3)
                    ax1 = T.axis.spatial(3, (i0_i1_fused_0 * 24 + i0_i1_fused_1) % 3)
                    T.reads(
                        rxplaceholder[
                            (ax0 * 3 + ax1) // 24,
                            (ax0 * 3 + ax1) % 24 // 12,
                            (ax0 * 3 + ax1) % 12 // 4,
                            (ax0 * 3 + ax1) % 4,
                        ]
                    )
                    T.writes(T_reshape[ax0, ax1])
                    T_reshape[ax0, ax1] = rxplaceholder[
                        (ax0 * 3 + ax1) // 24,
                        (ax0 * 3 + ax1) % 24 // 12,
                        (ax0 * 3 + ax1) % 12 // 4,
                        (ax0 * 3 + ax1) % 4,
                    ]

    assert has_reshape_pattern(reshape_scheduled)


def test_reshape_pattern_zero_extent():
    @T.prim_func(s_tir=True)
    def transpose_zero(
        rxplaceholder: T.Buffer((3, 0, 4), "float32"),
        T_transpose: T.Buffer((0, 3, 4), "float32"),
    ):
        for i0, i1, i2 in T.grid(0, 3, 4):
            with T.sblock("T_transpose"):
                ax0, ax1, ax2 = T.axis.remap("SSS", [i0, i1, i2])
                T.reads(rxplaceholder[ax1, ax0, ax2])
                T.writes(T_transpose[ax0, ax1, ax2])
                T_transpose[ax0, ax1, ax2] = rxplaceholder[ax1, ax0, ax2]

    assert not has_reshape_pattern(transpose_zero)


def test_reshape_pattern_expand_dims():
    @T.prim_func(s_tir=True)
    def expand_dims(
        rxplaceholder: T.Buffer((2, 3, 4), "float32"),
        expand_dims: T.Buffer((2, 1, 1, 1, 3, 1, 4, 1), "float32"),
    ):
        T.func_attr({"tirx.noalias": True})
        for i0, i1, i2, i3, i4, i5, i6, i7 in T.grid(2, 1, 1, 1, 3, 1, 4, 1):
            with T.sblock("expand_dims"):
                i0_1, i1_1, i2_1, i3_1, i4_1, i5_1, i6_1, i7_1 = T.axis.remap(
                    "SSSSSSSS", [i0, i1, i2, i3, i4, i5, i6, i7]
                )
                T.reads(rxplaceholder[i0_1, i4_1, i6_1])
                T.writes(expand_dims[i0_1, i1_1, i2_1, i3_1, i4_1, i5_1, i6_1, i7_1])
                expand_dims[i0_1, i1_1, i2_1, i3_1, i4_1, i5_1, i6_1, i7_1] = rxplaceholder[
                    i0_1, i4_1, i6_1
                ]

    assert has_reshape_pattern(expand_dims)


def test_reshape_pattern_dyn_1():
    @T.prim_func(s_tir=True)
    def reshape(var_A: T.handle, var_T_reshape: T.handle):
        n = T.int64()
        A = T.match_buffer(var_A, (n, T.int64(32), T.int64(128)), "float16")
        T_reshape = T.match_buffer(
            var_T_reshape, (T.int64(1), n, T.int64(32), T.int64(128)), "float16"
        )
        for ax0, ax1, ax2, ax3 in T.grid(T.int64(1), n, T.int64(32), T.int64(128)):
            with T.sblock("T_reshape"):
                v_ax0, v_ax1, v_ax2, v_ax3 = T.axis.remap("SSSS", [ax0, ax1, ax2, ax3])
                T.reads(
                    A[
                        ((v_ax3 // T.int64(128) + v_ax2) // T.int64(32) + v_ax0 * n + v_ax1) % n,
                        (v_ax3 // T.int64(128) + v_ax2) % T.int64(32),
                        v_ax3 % T.int64(128),
                    ]
                )
                T.writes(T_reshape[v_ax0, v_ax1, v_ax2, v_ax3])
                T_reshape[v_ax0, v_ax1, v_ax2, v_ax3] = A[
                    ((v_ax3 // T.int64(128) + v_ax2) // T.int64(32) + v_ax0 * n + v_ax1) % n,
                    (v_ax3 // T.int64(128) + v_ax2) % T.int64(32),
                    v_ax3 % T.int64(128),
                ]

    assert has_reshape_pattern(reshape)


def test_reshape_pattern_dyn_2():
    @T.prim_func(s_tir=True)
    def reshape(var_A: T.handle, var_T_reshape: T.handle):
        n = T.int64()
        A = T.match_buffer(var_A, (T.int64(1), n), "int32")
        T_reshape = T.match_buffer(var_T_reshape, (n,), "int32")
        for ax0 in range(n):
            with T.sblock("T_reshape"):
                v_ax0 = T.axis.spatial(n, ax0)
                T.reads(A[T.int64(0), v_ax0 % n])
                T.writes(T_reshape[v_ax0])
                T_reshape[v_ax0] = A[T.int64(0), v_ax0 % n]

    assert has_reshape_pattern(reshape)


def test_reshape_pattern_dyn_3():
    @T.prim_func(s_tir=True)
    def reshape(var_A: T.handle, var_T_reshape: T.handle):
        T.func_attr({"op_pattern": 8, "tirx.noalias": True})
        n = T.int64()
        A = T.match_buffer(var_A, (n, T.int64(4096)), "float16")
        T_reshape = T.match_buffer(var_T_reshape, (T.int64(1), n, T.int64(4096)), "float16")
        for ax0, ax1, ax2 in T.grid(T.int64(1), n, T.int64(4096)):
            with T.sblock("T_reshape"):
                v_ax0, v_ax1, v_ax2 = T.axis.remap("SSS", [ax0, ax1, ax2])
                T.reads(A[(v_ax2 // T.int64(4096) + v_ax0 * n + v_ax1) % n, v_ax2 % T.int64(4096)])
                T.writes(T_reshape[v_ax0, v_ax1, v_ax2])
                T_reshape[v_ax0, v_ax1, v_ax2] = A[
                    (v_ax2 // T.int64(4096) + v_ax0 * n + v_ax1) % n, v_ax2 % T.int64(4096)
                ]

    assert has_reshape_pattern(reshape)


def test_reshape_pattern_dyn_4():
    @T.prim_func(s_tir=True)
    def reshape(var_A: T.handle, var_T_reshape: T.handle):
        T.func_attr({"op_pattern": 8, "tirx.noalias": True})
        n = T.int64()
        A = T.match_buffer(var_A, (T.int64(1), n, T.int64(4096)), "float16")
        T_reshape = T.match_buffer(
            var_T_reshape, (T.int64(1), n, T.int64(32), T.int64(128)), "float16"
        )
        for ax0, ax1, ax2, ax3 in T.grid(T.int64(1), n, T.int64(32), T.int64(128)):
            with T.sblock("T_reshape"):
                v_ax0, v_ax1, v_ax2, v_ax3 = T.axis.remap("SSSS", [ax0, ax1, ax2, ax3])
                T.reads(
                    A[
                        T.int64(0),
                        ((v_ax2 * T.int64(128) + v_ax3) // T.int64(4096) + v_ax0 * n + v_ax1) % n,
                        (v_ax2 * T.int64(128) + v_ax3) % T.int64(4096),
                    ]
                )
                T.writes(T_reshape[v_ax0, v_ax1, v_ax2, v_ax3])
                T_reshape[v_ax0, v_ax1, v_ax2, v_ax3] = A[
                    T.int64(0),
                    ((v_ax2 * T.int64(128) + v_ax3) // T.int64(4096) + v_ax0 * n + v_ax1) % n,
                    (v_ax2 * T.int64(128) + v_ax3) % T.int64(4096),
                ]

    assert has_reshape_pattern(reshape)


def test_reshape_pattern_dyn_5():
    @T.prim_func(s_tir=True)
    def reshape(var_A: T.handle, var_T_reshape: T.handle):
        T.func_attr({"op_pattern": 8, "tirx.noalias": True})
        n = T.int64()
        A = T.match_buffer(var_A, (T.int64(1), n, T.int64(32), T.int64(128)), "float16")
        T_reshape = T.match_buffer(var_T_reshape, (T.int64(1), n, T.int64(4096)), "float16")
        # with T.sblock("root"):
        for ax0, ax1, ax2 in T.grid(T.int64(1), n, T.int64(4096)):
            with T.sblock("T_reshape"):
                v_ax0, v_ax1, v_ax2 = T.axis.remap("SSS", [ax0, ax1, ax2])
                T.reads(
                    A[
                        T.int64(0),
                        (v_ax2 // T.int64(4096) + v_ax0 * n + v_ax1) % n,
                        v_ax2 % T.int64(4096) // T.int64(128),
                        v_ax2 % T.int64(128),
                    ]
                )
                T.writes(T_reshape[v_ax0, v_ax1, v_ax2])
                T_reshape[v_ax0, v_ax1, v_ax2] = A[
                    T.int64(0),
                    (v_ax2 // T.int64(4096) + v_ax0 * n + v_ax1) % n,
                    v_ax2 % T.int64(4096) // T.int64(128),
                    v_ax2 % T.int64(128),
                ]

    assert has_reshape_pattern(reshape)


def test_reshape_pattern_with_raggedness():
    @T.prim_func(s_tir=True)
    def reshape_raggedness(
        A: T.Buffer((100, 768), "float32"),
        src_indptr: T.Buffer((9,), "int32"),
        B: T.Buffer((100, 12, 64), "float32"),
    ):
        for b in T.serial(8):
            with T.sblock("block0"):
                vb = T.axis.spatial(8, b)
                for i in T.serial(src_indptr[vb + 1] - src_indptr[vb]):
                    for h in T.serial(12):
                        for f in T.serial(64):
                            with T.sblock("block1"):
                                vi, vh, vf = T.axis.remap("SSS", [i, h, f])
                                B[src_indptr[vb] + vi, vh, vf] = A[
                                    src_indptr[vb] + vi, vh * 64 + vf
                                ]

    assert has_reshape_pattern(reshape_raggedness)


def test_reshape_pattern_reject_seqstmt():
    @T.prim_func(s_tir=True)
    def identity_bias(A: T.Buffer((4, 4), "float32"), B: T.Buffer((4, 4), "float32")):
        C = T.sblock_alloc_buffer((128, 128), "float32")
        for i0, i1 in T.grid(4, 4):
            with T.sblock("identity"):
                vi0, vi1 = T.axis.remap("SS", [i0, i1])
                C[vi0, vi1] = A[vi0, vi1]
        for i0, i1 in T.grid(4, 4):
            with T.sblock("identity"):
                vi0, vi1 = T.axis.remap("SS", [i0, i1])
                B[vi0, vi1] = C[vi0, vi1] + T.float32(1)

    @T.prim_func(s_tir=True)
    def identity_identity(A: T.Buffer((4, 4), "float32"), B: T.Buffer((4, 4), "float32")):
        C = T.sblock_alloc_buffer((128, 128), "float32")
        for i0, i1 in T.grid(4, 4):
            with T.sblock("identity"):
                vi0, vi1 = T.axis.remap("SS", [i0, i1])
                C[vi0, vi1] = A[vi0, vi1]
        for i0, i1 in T.grid(4, 4):
            with T.sblock("identity"):
                vi0, vi1 = T.axis.remap("SS", [i0, i1])
                B[vi0, vi1] = C[vi0, vi1]

    assert not has_reshape_pattern(identity_bias)
    assert not has_reshape_pattern(identity_identity)


def test_reshape_pattern_reject_reduction():
    @T.prim_func(s_tir=True)
    def reduction(A: T.Buffer((4, 4), "float32"), B: T.Buffer((4,), "float32")):
        for i0, i1 in T.grid(4, 4):
            with T.sblock("identity"):
                vi0, vi1 = T.axis.remap("SR", [i0, i1])
                with T.init():
                    B[vi0] = T.float32(0)
                B[vi0] = B[vi0] + A[vi0, vi1]

    assert not has_reshape_pattern(reduction)


def test_reshape_pattern_reject_reduction():
    @T.prim_func(s_tir=True)
    def reduction(A: T.Buffer((4, 4), "float32"), B: T.Buffer((4,), "float32")):
        for i0, i1 in T.grid(4, 4):
            with T.sblock("identity"):
                vi0, vi1 = T.axis.remap("SR", [i0, i1])
                with T.init():
                    B[vi0] = T.float32(0)
                B[vi0] = B[vi0] + A[vi0, vi1]

    assert not has_reshape_pattern(reduction)


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