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

import pytest

import tvm
import tvm.script
import tvm.testing
from tvm import relax
from tvm.ir.base import assert_structural_equal
from tvm.relax.transform.transform import CanonicalizeBindings
from tvm.script import ir as I
from tvm.script import relax as R
from tvm.script import tirx as T


def verify(input, expected):
    tvm.ir.assert_structural_equal(CanonicalizeBindings()(input), expected)


def test_simple_assignments():
    @I.ir_module
    class TestChainAssignments:
        @R.function
        def main(x: R.Tensor):
            y = x
            z = y
            q = z
            p = q
            o = p
            return o

    @I.ir_module
    class Expected:
        @R.function
        def main(x: R.Tensor):
            return x

    verify(TestChainAssignments, Expected)


def test_dataflow_block():
    @I.ir_module
    class TestDataflowAssignments:
        @R.function
        def main(x: R.Tensor):
            with R.dataflow():
                y = R.const(1)
                z = y
                o = z
                p = o
                m = p
                n = m
                R.output(n)
            return n

    @I.ir_module
    class Expected:
        @R.function
        def main(x: R.Tensor):
            with R.dataflow():
                n = R.const(1)
                R.output(n)
            return n

    verify(TestDataflowAssignments, Expected)


def test_assign_to_output_in_dataflow_block():
    @I.ir_module
    class TestDataflowAssignments:
        @R.function
        def main(x: R.Tensor):
            with R.dataflow():
                y = x  # is not a dataflow var
                z = y
                o = z
                p = o
                m = p
                n = m
                R.output(n)
            return n

    @I.ir_module
    class Expected:
        @R.function
        def main(x: R.Tensor):
            # we get a dataflow block where the
            # only assignment is n = x, which we can eliminate,
            # resulting in an empty block that is normalized away
            return x

    verify(TestDataflowAssignments, Expected)


def test_ops():
    @I.ir_module
    class TestOps:
        @R.function
        def main(x: R.Tensor, y: R.Tensor):
            w = y
            q = x
            z = R.add(w, q)
            return R.add(q, z)

    @I.ir_module
    class Expected:
        @R.function
        def main(x: R.Tensor, y: R.Tensor):
            z = R.add(y, x)
            return R.add(x, z)

    verify(TestOps, Expected)


@pytest.mark.xfail(reason="The lhs and rhs of an assignment should have the same type.")
def test_casting():
    @I.ir_module
    class TestCasting:
        @R.function
        def main(x: R.Tensor) -> R.Any:
            y = x
            # z will be treated as Any even though it's a tensor
            z: R.Any = y
            return z

    @I.ir_module
    class Expected:
        @R.function
        def main(x: R.Tensor) -> R.Any:
            # Cannot unify because the cast indicates user intent
            z: R.Any = x
            return z

    verify(TestCasting, Expected)


def test_match_cast():
    @I.ir_module
    class TestMatchCast:
        @R.function
        def main(x: R.Tensor):
            q = x
            m, n = T.int64(), T.int64()
            z = R.match_cast(q, R.Tensor((m, n)))
            w = z
            return w

    @I.ir_module
    class Expected:
        @R.function
        def main(x: R.Tensor):
            # can't get rid of z because its ty is different from x's
            m, n = T.int64(), T.int64()
            z = R.match_cast(x, R.Tensor((m, n)))
            return z

    verify(TestMatchCast, Expected)


def test_same_shape():
    @I.ir_module
    class TestSameShape:
        @R.function
        def main(x: R.Tensor(("m", "n"), "float32")):
            m, n = T.int64(), T.int64()
            y = x
            # trivial check
            z = R.match_cast(x, R.Tensor((m, n), "float32"))
            w = z
            q = R.add(w, y)
            return R.add(q, w)

    @I.ir_module
    class Expected:
        @R.function
        def main(x: R.Tensor(("m", "n"), "float32")):
            # the trivial check is canonicalized into a var binding
            # and then eliminated
            q = R.add(x, x)
            return R.add(q, x)

    verify(TestSameShape, Expected)


def test_change_shape():
    @I.ir_module
    class TestChangeShape:
        @R.function
        def main(x: R.Tensor(ndim=2)):
            y = x
            # The MatchCast is non-trivial, as it introduces new shape
            # vars.  Because the input tensor has an unknown shape
            # rather than a symbolic shape, these new shape vars
            # cannot be expressed in terms of previous variables.
            # Therefore, the match cast must be retained.
            o, p = T.int64(), T.int64()
            z = R.match_cast(x, R.Tensor((o, p)))
            w = z
            q = R.add(w, y)
            return R.add(q, w)

    @I.ir_module
    class Expected:
        @R.function
        def main(x: R.Tensor(ndim=2)):
            o, p = T.int64(), T.int64()
            z = R.match_cast(x, R.Tensor((o, p)))
            # the ty field on q will need to be updated
            q = R.add(z, x)
            return R.add(q, z)

    verify(TestChangeShape, Expected)


def test_replace_symbolic_variable_and_remove_match_cast():
    @I.ir_module
    class TestChangeShape:
        @R.function
        def main(x: R.Tensor(("m", "n"))):
            y = x
            # The MatchCast is non-trivial, as it introduces new shape
            # vars.  However, the new shape vars are redundant, and
            # are replaced by canonicalization.  After replacing the
            # new shape vars, the MatchCast is trivial and may be
            # removed.
            o, p = T.int64(), T.int64()
            z = R.match_cast(x, R.Tensor((o, p)))
            w = z
            q = R.add(w, y)
            return R.add(q, w)

    @I.ir_module
    class Expected:
        @R.function
        def main(x: R.Tensor(("m", "n"))):
            m = T.int64()
            n = T.int64()
            q: R.Tensor([m, n]) = R.add(x, x)
            return R.add(q, x)

    verify(TestChangeShape, Expected)


def test_replace_symbolic_variable_and_remove_match_cast_of_tuple():
    """Symbolic variables may be defined in R.match_cast of tuple

    This test is similar to
    `test_replace_symbolic_variable_and_remove_match_cast`, except
    that the MatchCast is performed on a Relax tuple.

    This is a regression test.  Earlier implementations only inferred
    TIR variables from `R.match_cast` of tensors, shapes, and prim
    values, but omitted tuples.

    """

    @I.ir_module
    class Before:
        @R.function
        def main(x: R.Tuple(R.Tensor(("m", "n")))):
            y = x
            o, p = T.int64(), T.int64()
            z = R.match_cast(x, R.Tuple(R.Tensor((o, p))))
            w = z
            q = R.add(w[0], y[0])
            return R.add(q, w[0])

    @I.ir_module
    class Expected:
        @R.function
        def main(x: R.Tuple(R.Tensor(("m", "n")))):
            q = R.add(x[0], x[0])
            return R.add(q, x[0])

    verify(Before, Expected)


def test_unwrap_tuple():
    @I.ir_module
    class Before:
        @R.function
        def main(x: R.Tensor, y: R.Tensor):
            tuple_var = (x, y)
            w = tuple_var[0]
            q = tuple_var[1]
            z = R.add(w, q)
            return R.add(q, z)

    @I.ir_module
    class Expected:
        @R.function
        def main(x: R.Tensor, y: R.Tensor):
            tuple_var = (x, y)
            z = R.add(x, y)
            return R.add(y, z)

    verify(Before, Expected)


def test_basic_folding_example():
    @I.ir_module
    class Input:
        @R.function
        def main() -> R.Tensor((), "int32"):
            with R.dataflow():
                y = R.const(1)
                n = y
                R.output(n)
            return n

    @I.ir_module
    class Expected:
        @R.function
        def main() -> R.Tensor((), "int32"):
            with R.dataflow():
                n = R.const(1)
                R.output(n)
            return n

    verify(Input, Expected)


def test_fold_match_cast():
    @I.ir_module
    class Input:
        @R.function
        def main() -> R.Tensor((), "int32"):
            with R.dataflow():
                y = R.const(1)
                n = R.match_cast(y, R.Tensor((), "int32"))
                R.output(n)
            return n

    @I.ir_module
    class Expected:
        @R.function
        def main() -> R.Tensor((), "int32"):
            with R.dataflow():
                # the cast is trivial, so it is removed
                n = R.const(1)
                R.output(n)
            return n

    verify(Input, Expected)


def test_fold_variables_from_match_cast():
    """Symbolic variables in R.match_cast may be inferred

    If the argument to `R.match_cast` has known shape parameters, they
    may be used to infer symbolic shape parameters.

    """

    @I.ir_module
    class Before:
        @R.function
        def main(
            state: R.Tensor([16], dtype="float32"),
            A: R.Tensor([16, 16], dtype="float32"),
            B: R.Tensor([16, 16], dtype="float32"),
        ):
            N1 = T.int64()
            M = T.int64()
            N2 = T.int64()

            # The symbolic variables `N1`, `N2` and `M` are defined by
            # these `R.match_cast` statements.  Since the inputs have
            # a known shape, the values of these symbolic variables
            # may be inferred.
            lhs_A = R.match_cast(A, R.Tensor([N1, M], dtype="float32"))
            lhs_B = R.match_cast(B, R.Tensor([N2, M], dtype="float32"))
            rhs = R.match_cast(state, R.Tensor([M], dtype="float32"))

            # The symbolic shapes propagate downstream.
            lhs: R.Tensor([N1 + N2, M], "float32") = R.concat((lhs_A, lhs_B), axis=0)
            proj_concat: R.Tensor([N1 + N2], "float32") = R.matmul(lhs, rhs)
            proj_A = R.strided_slice(
                proj_concat,
                (R.prim_value(0),),
                (R.prim_value(0),),
                (R.prim_value(N1),),
                assume_inbound=False,
            )
            proj_B = R.strided_slice(
                proj_concat,
                [R.prim_value(0)],
                [R.prim_value(N1)],
                [R.prim_value(N1 + N2)],
                assume_inbound=False,
            )
            return (proj_A, proj_B)

    @I.ir_module
    class Expected:
        @R.function
        def main(
            state: R.Tensor([16], dtype="float32"),
            A: R.Tensor([16, 16], dtype="float32"),
            B: R.Tensor([16, 16], dtype="float32"),
        ):
            # Shape annotations use the inferred static values, but runtime
            # primitive arguments remain symbolic.  Keep the match-casts that
            # define the symbols used by those runtime arguments.
            N1 = T.int64()
            M = T.int64()
            N2 = T.int64()

            lhs_A = R.match_cast(A, R.Tensor([N1, M], dtype="float32"))
            lhs_B = R.match_cast(B, R.Tensor([N2, M], dtype="float32"))
            lhs: R.Tensor([32, 16], dtype="float32") = R.concat((lhs_A, lhs_B), axis=0)
            proj_concat: R.Tensor([32], dtype="float32") = R.matmul(lhs, state)
            proj_A: R.Tensor([16], dtype="float32") = R.strided_slice(
                proj_concat,
                [R.prim_value(0)],
                [R.prim_value(0)],
                [R.prim_value(N1)],
                assume_inbound=False,
            )
            proj_B: R.Tensor([16], dtype="float32") = R.strided_slice(
                proj_concat,
                [R.prim_value(0)],
                [R.prim_value(N1)],
                [R.prim_value(N1 + N2)],
                assume_inbound=False,
            )
            return (proj_A, proj_B)

    verify(Before, Expected)


def test_inconsistent_match_cast_raises_error():
    """Symbolic variables from R.match_cast must be consistent

    All match cast statements must provide consistent definitions for
    symbolic variables.  In this test, the value of `M` would be
    inferred as 16 from either `state` or `A`, but would be inferred
    as 32 from `B`.

    """

    @I.ir_module
    class Before:
        @R.function
        def main(
            state: R.Tensor([16], dtype="float32"),
            A: R.Tensor([16, 16], dtype="float32"),
            B: R.Tensor([32, 32], dtype="float32"),
        ):
            N1 = T.int64()
            M = T.int64()
            N2 = T.int64()

            # These R.match_cast statements define inconsistent values
            # for the symbolic shape parameters.
            lhs_A = R.match_cast(A, R.Tensor([N1, M], dtype="float32"))
            lhs_B = R.match_cast(B, R.Tensor([N2, M], dtype="float32"))
            rhs = R.match_cast(state, R.Tensor([M], dtype="float32"))

            lhs: R.Tensor([N1 + N2, M], "float32") = R.concat((lhs_A, lhs_B), axis=0)
            proj_concat: R.Tensor([N1 + N2], "float32") = R.matmul(lhs, rhs)
            proj_A = R.strided_slice(
                proj_concat,
                (R.prim_value(0),),
                (R.prim_value(0),),
                (R.prim_value(N1),),
                assume_inbound=False,
            )
            proj_B = R.strided_slice(
                proj_concat,
                [R.prim_value(0)],
                [R.prim_value(N1)],
                [R.prim_value(N1 + N2)],
                assume_inbound=False,
            )
            return (proj_A, proj_B)

    with pytest.raises(ValueError, match="MatchCast statements must be consistent"):
        CanonicalizeBindings()(Before)


def test_match_cast_may_have_distinct_values_in_branches():
    """Conditional branches may have different values of symbolic variables

    Here, the value of `N` can be inferred as 16 within the `if`
    branch and as 32 within the `else` branch.

    """

    @I.ir_module
    class Before:
        @R.function
        def main(
            state: R.Tensor(["N"], dtype="float32"),
            A: R.Tensor(["M", 16], dtype="float32"),
            B: R.Tensor(["M", 32], dtype="float32"),
            scale: R.Prim("float32"),
        ):
            N = T.int64()
            M = T.int64()

            if N == 16:
                weights: R.Tensor([M, 16], "float32") = A * scale
                weights: R.Tensor([M, N], "float32") = R.match_cast(
                    weights, R.Tensor([M, N], "float32")
                )
                weights: R.Tensor([M, N], "float32") = weights * scale
            else:
                weights: R.Tensor([M, 32], "float32") = B * scale
                weights: R.Tensor([M, N], "float32") = R.match_cast(
                    weights, R.Tensor([M, N], "float32")
                )
                weights: R.Tensor([M, N], "float32") = weights * scale

            weights: R.Tensor([M, N], "float32") = weights * scale

            out: R.Tensor([M], "float32") = R.matmul(weights, state)

            return out

    @I.ir_module
    class Expected:
        @R.function
        def main(
            state: R.Tensor(["N"], dtype="float32"),
            A: R.Tensor(["M", 16], dtype="float32"),
            B: R.Tensor(["M", 32], dtype="float32"),
            scale: R.Prim("float32"),
        ):
            N = T.int64()
            M = T.int64()

            if N == 16:
                # Prior to the R.match_cast, the
                weights: R.Tensor([M, 16], "float32") = A * scale
                weights: R.Tensor([M, N], "float32") = R.match_cast(
                    weights, R.Tensor([M, N], "float32")
                )
                # The scaled weights within the branch may perform
                # shape inference knowing that N==16.
                weights: R.Tensor([M, 16], "float32") = weights * scale
                # The match cast on exiting the if branch restores the
                weights = R.match_cast(weights, R.Tensor([M, N], "float32"))
            else:
                # Prior to the R.match_cast, the
                weights: R.Tensor([M, 32], "float32") = B * scale
                weights: R.Tensor([M, N], "float32") = R.match_cast(
                    weights, R.Tensor([M, N], "float32")
                )
                # Within the else-branch, the R.match_cast implies
                # that N==32.  While this conflicts with the earlier
                # definition, the two occur in separate branches, so
                # this is legal.
                # The scaled weights within the branch may perform
                # shape inference knowing that N==32.
                weights: R.Tensor([M, 32], "float32") = weights * scale
                weights = R.match_cast(weights, R.Tensor([M, N], "float32"))

            # Outside of the conditional, we no longer have a known
            # value for N, so this shape inference must be done using
            # a dynamic shape for `N`.
            weights: R.Tensor([M, N], "float32") = weights * scale

            # After the conditional branch, we no longer have a known
            # value of N, so this shape inference must use the dynamic
            # shape.
            out: R.Tensor([M], "float32") = R.matmul(weights, state)

            return out

    verify(Before, Expected)


def test_multiple_outputs():
    @I.ir_module
    class Input:
        @R.function
        def main():
            with R.dataflow():
                x = R.const(1)
                y = R.const(1)
                z = R.const(1)
                l = x
                m = y
                n = z
                R.output(l, m, n)
            return (l, m, n)

    @I.ir_module
    class Expected:
        @R.function
        def main():
            with R.dataflow():
                l = R.const(1)
                m = R.const(1)
                n = R.const(1)
                R.output(l, m, n)
            return (l, m, n)

    verify(Input, Expected)


def test_single_output_multiple_nondataflow():
    """Non-dataflow vars being updated may also be part trivial bindings

    Like `test_multiple_outputs`, but only `n` is used in the return
    statement.
    """

    @I.ir_module
    class Input:
        @R.function
        def main():
            with R.dataflow():
                x = R.const(1)
                y = R.const(1)
                z = R.const(1)
                l = x
                m = y
                n = z
                R.output(l, m, n)
            return n

    @I.ir_module
    class Expected:
        @R.function
        def main():
            with R.dataflow():
                l = R.const(1)
                m = R.const(1)
                n = R.const(1)
                R.output(n)
            return n

    verify(Input, Expected)


def test_fold_const_to_output():
    @I.ir_module
    class Before:
        @R.function
        def main() -> R.Tensor((), "int32"):
            with R.dataflow():
                n = R.const(1)
                R.output(n)
            return n

    @I.ir_module
    class Expected:
        @R.function
        def main() -> R.Tensor((), "int32"):
            with R.dataflow():
                n = R.const(1)
                R.output(n)
            return R.const(1)

    verify(Before, Expected)


def test_canonicalize_var_to_dataflow_var_if_legal():
    """Canonicalize Var to DataflowVar inside DataflowBlock

    DataflowVar instances may only be used inside a DataflowBlock.  If
    a trivial binding `y = x` occurs, where `x` is a `DataflowVar` and
    `y` is a `Var`, replacing `y` with `x` may result in usage of a
    `DataflowVar` outside of a `DataflowBlock`.
    """

    @I.ir_module
    class Before:
        @R.function
        def main(x: R.Tensor):
            with R.dataflow():
                y = R.add(x, R.const(1))
                z = R.add(y, R.const(1))
                R.output(y, z)
            return z

    @I.ir_module
    class Expected:
        @R.function
        def main(x: R.Tensor):
            with R.dataflow():
                y = R.add(x, R.const(1))
                z = R.add(y, R.const(1))
                R.output(z)
            return z

    after = relax.transform.CanonicalizeBindings()(Before)
    assert_structural_equal(Expected, after)


def test_update_dataflow_computations_if_var_replacement_occurs():
    """Canonicalize Var to DataflowVar inside DataflowBlock

    DataflowBlocks may produce additional outputs after the first
    output Var, and these additional outputs may be in terms of the
    first output.  Computations that depend on a replaced var must be
    updated to remain well-formed.
    """

    @I.ir_module
    class Before:
        @R.function
        def main(x: R.Tensor):
            with R.dataflow():
                lv1 = R.add(x, R.const(1))
                gv1 = lv1
                gv2 = R.add(lv1, R.const(1))
                R.output(gv1, gv2)
            return (gv1, gv2)

    @I.ir_module
    class Expected:
        @R.function
        def main(x: R.Tensor):
            with R.dataflow():
                # lv1 has been replaced with gv1
                gv1 = R.add(x, R.const(1))
                # So gv1 must be used in the computation of gv2
                gv2 = R.add(gv1, R.const(1))
                R.output(gv1, gv2)
            return (gv1, gv2)

    after = relax.transform.CanonicalizeBindings()(Before)
    assert_structural_equal(Expected, after)


def test_update_dataflow_computations_if_var_replacement_occurs_after_usage():
    """Canonicalize Var to DataflowVar inside DataflowBlock

    Like test_update_dataflow_computations_if_var_replacement_occurs,
    but the usage of a DataflowVar occurs before the trivial binding
    that causes it to be replaced.
    """

    @I.ir_module
    class Before:
        @R.function
        def main(x: R.Tensor):
            with R.dataflow():
                lv1 = R.add(x, R.const(1))
                gv2 = R.add(lv1, R.const(1))
                gv1 = lv1
                R.output(gv1, gv2)
            return (gv1, gv2)

    @I.ir_module
    class Expected:
        @R.function
        def main(x: R.Tensor):
            with R.dataflow():
                # lv1 has been replaced with gv1
                gv1 = R.add(x, R.const(1))
                # So gv1 must be used in the computation of gv2
                gv2 = R.add(gv1, R.const(1))
                # Even though the trivial binding of "gv1 = lv1"
                # occurred in this position.
                R.output(gv1, gv2)
            return (gv1, gv2)

    after = relax.transform.CanonicalizeBindings()(Before)
    assert_structural_equal(Expected, after)


def test_replace_var_with_dataflow_if_all_usage_within_dataflow_block():
    """Canonicalize Var to DataflowVar inside DataflowBlock

    Like `test_update_dataflow_computations_if_var_replacement_occurs`,
    except that `gv1` is not part of the function's return value.  When
    deciding which variable to replace, the following logic is applied:

    1. Normally, when encountering `x = y`, replace usage of `x` with `y`.

    2. Unless the trivial binding is a `var_x = dataflow_y`, in which case
       replace `dataflow_y` with `var_x` at the point of definition.  This
       prevents usage of `dataflow_y` from escaping the dataflow block.

    3. Unless `var_x` has no usage outside the dataflow block, in which
       case we replace usage of `var_x` with `dataflow_y`.

    The third rule ensures that canonicalization can occur in a single
    step.  Otherwise, the output of this test case would contain a
    non-dataflow var defined within a dataflow block, and only used within
    that dataflow block.  (Equivalent to the input for the test case
    `test_canonicalize_var_to_dataflow_var_if_legal`.)
    """

    @I.ir_module
    class Before:
        @R.function
        def main(x: R.Tensor):
            with R.dataflow():
                lv1 = R.add(x, R.const(1))
                gv1 = lv1
                gv2 = R.add(lv1, R.const(1))
                R.output(gv1, gv2)
            return gv2

    @I.ir_module
    class Expected:
        @R.function
        def main(x: R.Tensor):
            with R.dataflow():
                gv1 = R.add(x, R.const(1))
                gv2 = R.add(gv1, R.const(1))
                R.output(gv2)
            return gv2

    after = relax.transform.CanonicalizeBindings()(Before)
    assert_structural_equal(Expected, after)


def test_canonicalize_var_to_dataflow_with_trivial_binding():
    """Canonicalize Var to DataflowVar inside DataflowBlock

    Like
    `test_replace_var_with_dataflow_if_all_usage_within_dataflow_block`,
    except the non-DataflowVar is on the right-hand side of the trivial
    binding.
    """

    @I.ir_module
    class Before:
        @R.function
        def main(x: R.Tensor):
            with R.dataflow():
                gv1 = R.add(x, R.const(1))
                lv1 = gv1
                gv2 = R.add(lv1, R.const(1))
                R.output(gv1, gv2)
            return gv2

    @I.ir_module
    class Expected:
        @R.function
        def main(x: R.Tensor):
            with R.dataflow():
                gv1 = R.add(x, R.const(1))
                gv2 = R.add(gv1, R.const(1))
                R.output(gv2)
            return gv2

    after = relax.transform.CanonicalizeBindings()(Before)
    assert_structural_equal(Expected, after)


def test_canonicalize_with_updated_ty():
    """CanonicalizeBindings and Normalizer may both replace a Var

    If the CanonicalizeBindings pass has no replacements to make for a
    variable, it must still delegate to the ExprMutator.  This is because
    a variable replacement may have occurred as part of the IRNormalizer,
    in order to provide better type.
    """

    @I.ir_module
    class Before:
        @R.function(private=True)
        def main(A: R.Tensor(("n", 16), dtype="int32")) -> R.Tensor(("n", 16), dtype="int32"):
            # CanonicalizeBindings recognizes this trivial binding, and
            # replaces `B` with `A`.
            B = A
            # The value is updated from `R.add(B,B)` to `R.add(A,A)`.
            # Changing the value triggers struct inference, allowing the
            # shape to be updated to `[n,16]`.  This requires a variable
            # replacement, which is tracked by the `ExprMutator`.
            C: R.Tensor(dtype="int32", ndim=2) = R.add(B, B)
            # Replacement of `C` is not explicitly tracked by
            # CanonicalizeBindings.  However, if CanonicalizeBindings just
            # returns `GetRef<Var>(var)`, `ExprMutator` cannot apply the
            # replacement, and this will try to return the old
            # version of `C` with `ndim=2`.
            return C

    @I.ir_module
    class Expected:
        @R.function(private=True)
        def main(A: R.Tensor(("n", 16), dtype="int32")) -> R.Tensor(("n", 16), dtype="int32"):
            n = T.int64()
            C: R.Tensor([n, 16], "int32") = R.add(A, A)
            return C

    after = relax.transform.CanonicalizeBindings()(Before)
    assert_structural_equal(Expected, after)


def test_canonicalize_trivial_binding_to_dataflow_var():
    """Canonicalize Var to DataflowVar inside DataflowBlock

    DataflowVar instances may only be used inside a DataflowBlock.  If
    a trivial binding `y = x` occurs, where `x` is a `DataflowVar` and
    `y` is a `Var`, replacing `y` with `x` may result in usage of a
    `DataflowVar` outside of a `DataflowBlock`.

    If a binding exists solely to convert from DataflowVar into Var,
    then canonicalization replaces the earlier DataflowVar with a Var.
    """

    @I.ir_module
    class Before:
        @R.function
        def main(x: R.Tensor):
            with R.dataflow():
                y = R.add(x, R.const(1))
                z = y
                R.output(z)
            return z

    @I.ir_module
    class Expected:
        @R.function
        def main(x: R.Tensor):
            with R.dataflow():
                y = R.add(x, R.const(1))
                R.output(y)
            return y

    after = relax.transform.CanonicalizeBindings()(Before)
    assert_structural_equal(Expected, after)


def test_canonicalize_multiple_trivial_binding_to_dataflow_var():
    """Canonicalize Var to DataflowVar inside DataflowBlock

    Like test_canonicalize_trivial_binding_to_dataflow_var, but there
    exist multiple trivial bindings to the DataflowVar.
    """

    @I.ir_module
    class Before:
        @R.function
        def main(w: R.Tensor):
            with R.dataflow():
                x = R.add(w, R.const(1))
                y = x
                z = x
                R.output(y, z)
            return (y, z)

    @I.ir_module
    class Expected:
        @R.function
        def main(w: R.Tensor):
            with R.dataflow():
                x = R.add(w, R.const(1))
                R.output(x)
            return (x, x)

    after = relax.transform.CanonicalizeBindings()(Before)
    assert_structural_equal(Expected, after)


def test_canonicalize_trivial_var_binding_inside_dataflow_block():
    """Canonicalize Var to DataflowVar inside DataflowBlock

    Canonicalization handles cases where a Var could be replaced by a
    DataflowVar, and where a Var is a trivial binding.  If these two
    cases both occur, should produce reasonable results.
    """

    @I.ir_module
    class Before:
        @R.function
        def main(x: R.Tensor):
            with R.dataflow():
                y = R.add(x, R.const(1))
                z = y
                R.output(y, z)
            return z

    @I.ir_module
    class Expected:
        @R.function
        def main(x: R.Tensor):
            with R.dataflow():
                y = R.add(x, R.const(1))
                R.output(y)
            return y

    after = relax.transform.CanonicalizeBindings()(Before)
    assert_structural_equal(Expected, after)


def test_canonicalize_across_non_dataflow_tuple():
    """Canonicalize Var to DataflowVar inside DataflowBlock"""

    @I.ir_module
    class Before:
        @R.function
        def main(x: R.Tensor):
            with R.dataflow():
                y = R.add(x, R.const(1))
                z = (y,)
                gv = R.add(z[0], R.const(1))
                R.output(z, gv)
            return gv

    @I.ir_module
    class Expected:
        @R.function
        def main(x: R.Tensor):
            with R.dataflow():
                y = R.add(x, R.const(1))
                z = (y,)
                gv = R.add(y, R.const(1))
                R.output(gv)
            return gv

    after = relax.transform.CanonicalizeBindings()(Before)
    assert_structural_equal(Expected, after)


def test_var_used_in_distinct_df_blocks():
    """If a var is used only in dataflow blocks,
    but outside of the one where it was originally defined,
    it should be exposed as an output."""

    @I.ir_module
    class Before:
        @R.function(pure=False)
        def main(x: R.Tensor, y: R.Tensor) -> R.Tensor:
            with R.dataflow():
                z = R.add(x, y)
                w = R.multiply(z, y)
                v = R.add(w, x)
                # v must remain exposed!
                R.output(v)
            _ = R.print(format="Hi mom!")
            with R.dataflow():
                a = R.multiply(v, v)
                b = R.add(a, a)
                c = R.subtract(b, a)
                d = R.add(c, c)
                R.output(d)
            return d

    after = relax.transform.CanonicalizeBindings()(Before)
    assert_structural_equal(Before, after)


def test_inner_function():
    @I.ir_module
    class Before:
        @R.function(pure=False)
        def main(x: R.Tensor, y: R.Tensor) -> R.Tensor:
            with R.dataflow():

                @R.function(pure=False)
                def inner_func(x: R.Tensor, y: R.Tensor) -> R.Tensor:
                    with R.dataflow():
                        z = R.add(x, y)
                        w = R.multiply(x, z)
                        v = R.add(y, w)
                        R.output(z, w, v)
                    _ = R.print(format="oops")
                    with R.dataflow():
                        a = R.multiply(v, v)
                        b = R.add(a, a)
                        c = R.multiply(a, b)
                        R.output(a, b, c)
                    return c

                z = R.add(x, y)
                w = R.multiply(z, z)
                v = R.divide(w, z)
                R.output(inner_func, z, v, w)
            q = inner_func(w, v)
            with R.dataflow():
                a = R.multiply(q, q)
                b = R.add(a, a)
                c = R.multiply(b, a)
                R.output(a, b, c)
            return c

    # expected: we do not need to expose all the outputs
    @I.ir_module
    class Expected:
        @R.function(pure=False)
        def main(x: R.Tensor, y: R.Tensor) -> R.Tensor:
            with R.dataflow():

                @R.function(pure=False)
                def inner_func(x: R.Tensor, y: R.Tensor) -> R.Tensor:
                    with R.dataflow():
                        z = R.add(x, y)
                        w = R.multiply(x, z)
                        v = R.add(y, w)
                        R.output(v)
                    _ = R.print(format="oops")
                    with R.dataflow():
                        a = R.multiply(v, v)
                        b = R.add(a, a)
                        c = R.multiply(a, b)
                        R.output(c)
                    return c

                z = R.add(x, y)
                w = R.multiply(z, z)
                v = R.divide(w, z)
                R.output(inner_func, v, w)
            q = inner_func(w, v)
            with R.dataflow():
                a = R.multiply(q, q)
                b = R.add(a, a)
                c = R.multiply(b, a)
                R.output(c)
            return c

    after = relax.transform.CanonicalizeBindings()(Before)
    assert_structural_equal(Expected, after)


def test_canonicalize_inside_branches():
    @I.ir_module
    class Before:
        @R.function
        def main(x: R.Tensor, y: R.Tensor) -> R.Tensor:
            with R.dataflow():
                z = R.add(x, y)
                R.output(z)
            if R.const(True):
                with R.dataflow():
                    w = R.add(z, z)
                    v = R.multiply(w, w)
                    # w does not need to be output
                    R.output(w, v)
                q = v
            else:
                with R.dataflow():
                    w = R.multiply(z, z)
                    v = R.add(w, w)
                    R.output(w, v)
                q = v
            return q

    @I.ir_module
    class Expected:
        @R.function
        def main(x: R.Tensor, y: R.Tensor) -> R.Tensor:
            with R.dataflow():
                z = R.add(x, y)
                R.output(z)
            if R.const(True):
                with R.dataflow():
                    w = R.add(z, z)
                    v = R.multiply(w, w)
                    R.output(v)
                q = v
            else:
                with R.dataflow():
                    w = R.multiply(z, z)
                    v = R.add(w, w)
                    R.output(v)
                q = v
            return q

    after = relax.transform.CanonicalizeBindings()(Before)
    assert_structural_equal(Expected, after)


def test_canonicalization_causes_ty_update():
    """Regression test for failure mode causing undefined variable

    The ExprMutator is only allowed to update a variable's type
    if the value bound to it has new type.  When
    CanonicalizeBindings replaces a trivial binding, this may provide
    better type as a result.  If this happens, the

    In previous implementations, ExprMutator::ReEmitBinding defined a
    remap for `binding->var`, even if the derived class defined a
    replacement by overriding `VisitVarDef`.  If the derived class
    defines a new variable binding by overriding `VisitVarDef`, and
    also causes a variable replacement by overriding `VisitExpr` and
    returning a type with different type, then `ExprMutator`
    must check for both `binding->var` *AND* `new_var`.  The
    former may be present in the unmodified graph, and the latter may
    be produced by the derived class before delegating to the base
    class.
    """

    @I.ir_module
    class Before:
        @R.function
        def transform_params(
            A: R.Tensor(("vocab_size", 4096), dtype="float16"),
            B: R.Tensor((6144, 4096), dtype="float16"),
        ):
            with R.dataflow():
                # Trivial binding of `DataFlow = NonDataFlow`.
                # Wherever `C` is used, Canonicalization will attempt
                # to replace it with `B`.
                C = B

                # RHS contains `(A,C)`, which CanonicalizeBindings
                # replaces with `(A,B)`.  Because this changes the
                # RHS, a new LHS (and new type!) will be
                # generated.
                D: R.Tuple(
                    R.Tensor(dtype="float16", ndim=2),
                    R.Tensor((6144, 4096), dtype="float16"),
                ) = (A, C)

                # Trivial binding of `NonDataFlow = DataFlow`.  The
                # definition of `D` will be replaced with a definition
                # of `E`.  This definition of `E` will then be updated
                # to have a known shape.
                E = D
                R.output(E)

            # By the time `E` is encountered at a usage site, the
            # `ExprMutator` must have a replacement for the old
            # version of `E` with `ndim=2` to the new versions of `E`
            # with `shape=[vocab_size,4096]`.
            return E

    @I.ir_module
    class Expected:
        @R.function
        def transform_params(
            A: R.Tensor(("vocab_size", 4096), dtype="float16"),
            B: R.Tensor((6144, 4096), dtype="float16"),
        ):
            vocab_size = T.int64()
            with R.dataflow():
                E: R.Tuple(
                    R.Tensor((vocab_size, 4096), dtype="float16"),
                    R.Tensor((6144, 4096), dtype="float16"),
                ) = (A, B)

                R.output(E)
            return E

    after = relax.transform.CanonicalizeBindings()(Before)
    assert_structural_equal(Expected, after)


def test_unwrap_tuple_of_constant():
    @I.ir_module
    class TestChainAssignments:
        @R.function
        def main():
            tup = (R.const(0, "int64"), R.const(1, "int64"))
            x = tup[0]
            y = tup[1]
            z = R.add(x, y)
            return z

    @I.ir_module
    class Expected:
        @R.function
        def main():
            tup = (R.const(0, "int64"), R.const(1, "int64"))
            x = tup[0]
            y = tup[1]
            z = R.add(R.const(0, "int64"), R.const(1, "int64"))
            return z

    verify(TestChainAssignments, Expected)


def test_trivial_binding_of_replaced_non_dataflow_var():
    @I.ir_module
    class Before:
        @R.function
        def main(param_tuple: R.Tuple([R.Tensor])):
            with R.dataflow():
                A = param_tuple[0]
                B = A
                C = R.add(A, B)
                R.output(A, B, C)
            return C

    @I.ir_module
    class Expected:
        @R.function
        def main(param_tuple: R.Tuple([R.Tensor])):
            with R.dataflow():
                A = param_tuple[0]
                C = R.add(A, A)
                R.output(C)
            return C

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

    def _get_binding_names(mod):
        return [binding.var.name for binding in mod["main"].body.blocks[0].bindings]

    expected_names = _get_binding_names(Expected)
    after_names = _get_binding_names(After)

    assert after_names == expected_names


def test_trace_tuple_through_round_trip():
    """Canonicalize to the orignal tuple, without unwrap/rewrap."""

    @I.ir_module
    class Before:
        @R.function
        def main(param_tuple: R.Tuple([R.Tensor, R.Tensor, R.Tensor])):
            with R.dataflow():
                A = param_tuple[0]
                B = param_tuple[1]
                C = param_tuple[2]
                output = (A, B, C)
                R.output(output)
            return output

    @I.ir_module
    class Expected:
        @R.function
        def main(param_tuple: R.Tuple([R.Tensor, R.Tensor, R.Tensor])):
            with R.dataflow():
                A = param_tuple[0]
                B = param_tuple[1]
                C = param_tuple[2]
                R.output()

            return param_tuple

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


def test_trace_partial_tuple_through_round_trip():
    """Canonicalize to the orignal tuple, without unwrap/rewrap."""

    @I.ir_module
    class Before:
        @R.function
        def main(param_tuple: R.Tuple([R.Tensor, R.Tensor, R.Tensor])):
            with R.dataflow():
                A = param_tuple[0]
                B = param_tuple[1]
                output = (A, B)
                R.output(output)
            return output

    Expected = Before

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


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