# 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.
# pylint: disable=missing-function-docstring,missing-module-docstring
# ruff: noqa: E741, F401
import sys

import pytest

import tvm
import tvm.testing
from tvm import tirx
from tvm.s_tir.schedule.testing import (
    assert_structural_equal_ignore_global_symbol,
    verify_trace_roundtrip,
)
from tvm.script import tirx as T

# pylint: disable=no-member,invalid-name,unused-variable


@T.prim_func(s_tir=True)
def element_wise(a: T.handle, b: T.handle) -> None:
    A = T.match_buffer(a, (128, 128))
    B = T.match_buffer(b, (128, 128))
    for i, j in T.grid(128, 128):
        with T.sblock("B"):
            vi, vj = T.axis.remap("SS", [i, j])
            B[vi, vj] = A[vi, vj] * 2.0


@T.prim_func(s_tir=True)
def element_wise_parallelized(a: T.handle, b: T.handle) -> None:
    A = T.match_buffer(a, (128, 128))
    B = T.match_buffer(b, (128, 128))
    for i0 in T.parallel(0, 128):
        for i1 in T.serial(0, 128):
            with T.sblock("B"):
                vi, vj = T.axis.remap("SS", [i0, i1])
                B[vi, vj] = A[vi, vj] * 2.0


@T.prim_func(s_tir=True)
def element_wise_i_bound(a: T.handle, b: T.handle) -> None:
    A = T.match_buffer(a, (128, 128))
    B = T.match_buffer(b, (128, 128))
    for i0 in T.thread_binding(0, 128, thread="threadIdx.x"):
        for i1 in T.serial(0, 128):
            with T.sblock("B"):
                vi, vj = T.axis.remap("SS", [i0, i1])
                B[vi, vj] = A[vi, vj] * 2.0


@T.prim_func(s_tir=True)
def element_wise_compute_at_split(a: T.handle, c: T.handle) -> None:
    A = T.match_buffer(a, (128, 128))
    C = T.match_buffer(c, (128, 128))
    B = T.sblock_alloc_buffer((128, 128))
    for i in T.serial(0, 128):
        for j0 in T.serial(0, 128):
            with T.sblock("B"):
                vi, vj = T.axis.remap("SS", [i, j0])
                B[vi, vj] = A[vi, vj] * 2.0
        for j1o, j1i in T.grid(32, 4):
            with T.sblock("C"):
                vi = T.axis.S(128, i)
                vj = T.axis.S(128, j1o * 4 + j1i)
                C[vi, vj] = B[vi, vj] + 1.0


@T.prim_func(s_tir=True)
def element_wise_compute_at_split_vectorized(a: T.handle, c: T.handle) -> None:
    A = T.match_buffer(a, (128, 128))
    C = T.match_buffer(c, (128, 128))
    B = T.sblock_alloc_buffer((128, 128))
    for i in T.serial(0, 128):
        for j0 in T.serial(0, 128):
            with T.sblock("B"):
                vi, vj = T.axis.remap("SS", [i, j0])
                B[vi, vj] = A[vi, vj] * 2.0
        for j1o in T.serial(0, 32):
            for j1i in T.vectorized(0, 4):
                with T.sblock("C"):
                    vi = T.axis.S(128, i)
                    vj = T.axis.S(128, j1o * 4 + j1i)
                    C[vi, vj] = B[vi, vj] + 1.0


@T.prim_func(s_tir=True)
def element_wise_split_predicate(a: T.handle, b: T.handle) -> None:
    A = T.match_buffer(a, [128, 128])
    B = T.match_buffer(b, [128, 128])
    for i, j_0, j_1 in T.grid(128, 13, 10):
        with T.sblock("B"):
            T.where(j_0 * 10 + j_1 < 128)
            vi = T.axis.S(128, i)
            vj = T.axis.S(128, j_0 * 10 + j_1)
            B[vi, vj] = A[vi, vj] * 2.0


@T.prim_func(s_tir=True)
def element_wise_split_predicate_parallelized(a: T.handle, b: T.handle) -> None:
    A = T.match_buffer(a, [128, 128])
    B = T.match_buffer(b, [128, 128])
    for i in T.serial(0, 128):
        for j_0 in T.parallel(0, 13):
            for j_1 in T.serial(0, 10):
                with T.sblock("B"):
                    T.where(j_0 * 10 + j_1 < 128)
                    vi = T.axis.S(128, i)
                    vj = T.axis.S(128, j_0 * 10 + j_1)
                    B[vi, vj] = A[vi, vj] * 2.0


@T.prim_func(s_tir=True)
def element_wise_split_predicate_vectorized(a: T.handle, b: T.handle) -> None:
    A = T.match_buffer(a, [128, 128])
    B = T.match_buffer(b, [128, 128])
    for i in T.vectorized(0, 128):
        for j_0, j_1 in T.grid(13, 10):
            with T.sblock("B"):
                T.where(j_0 * 10 + j_1 < 128)
                vi = T.axis.S(128, i)
                vj = T.axis.S(128, j_0 * 10 + j_1)
                B[vi, vj] = A[vi, vj] * 2.0


@T.prim_func(s_tir=True)
def element_wise_compute_at_split_j0_j1o_bound(a: T.handle, c: T.handle) -> None:
    A = T.match_buffer(a, (128, 128))
    C = T.match_buffer(c, (128, 128))
    B = T.sblock_alloc_buffer((128, 128))
    for i in T.serial(0, 128):
        for j0 in T.thread_binding(0, 128, thread="threadIdx.x"):
            with T.sblock("B"):
                vi, vj = T.axis.remap("SS", [i, j0])
                B[vi, vj] = A[vi, vj] * 2.0
        for j1o in T.thread_binding(0, 32, thread="threadIdx.x"):
            for j1i in T.serial(0, 4):
                with T.sblock("C"):
                    vi = T.axis.S(128, i)
                    vj = T.axis.S(128, j1o * 4 + j1i)
                    C[vi, vj] = B[vi, vj] + 1.0


@T.prim_func(s_tir=True)
def matmul(a: T.handle, b: T.handle, c: T.handle) -> None:
    A = T.match_buffer(a, (128, 128))
    B = T.match_buffer(b, (128, 128))
    C = T.match_buffer(c, (128, 128))

    for i, j, k in T.grid(128, 128, 128):
        with T.sblock("C"):
            vi, vj, vk = T.axis.remap("SSR", [i, j, k])
            with T.init():
                C[vi, vj] = 0.0
            C[vi, vj] = C[vi, vj] + A[vi, vk] * B[vj, vk]


@T.prim_func(s_tir=True)
def rowsum(a: T.handle, b: T.handle) -> None:
    A = T.match_buffer(a, (128, 128))
    B = T.match_buffer(b, (128,))

    for i, k in T.grid(128, 128):
        with T.sblock("B"):
            vi, vk = T.axis.remap("SR", [i, k])
            with T.init():
                B[vi] = 0.0
            B[vi] = B[vi] + A[vi, vk]


@T.prim_func(s_tir=True)
def rowsum_unrolled(a: T.handle, b: T.handle) -> None:
    A = T.match_buffer(a, (128, 128))
    B = T.match_buffer(b, (128,))
    for i0 in T.unroll(0, 128):
        for i1 in T.serial(0, 128):
            with T.sblock("B"):
                vi, vk = T.axis.remap("SR", [i0, i1])
                with T.init():
                    B[vi] = 0.0
                B[vi] = B[vi] + A[vi, vk]


@T.prim_func(s_tir=True)
def rowsum_not_quasi_affine(a: T.handle, b: T.handle) -> None:
    A = T.match_buffer(a, (128, 128))
    B = T.match_buffer(b, (128,))

    for i, k in T.grid(128, 16):
        with T.sblock("B"):
            vi = T.axis.S(128, i)
            vk = T.axis.R(128, T.floordiv(k * k, 2))
            with T.init():
                B[vi] = 0.0
            B[vi] = B[vi] + A[vi, vk]


@T.prim_func(s_tir=True)
def rowsum_not_compact_data_flow(a: T.handle, b: T.handle) -> None:
    A = T.match_buffer(a, (128, 128))
    B = T.match_buffer(b, (128,))

    for i, k in T.grid(128, 16):
        with T.sblock("B"):
            vi, vk = T.axis.remap("SR", [i, k])
            with T.init():
                B[vk] = 0.0
            B[vk] = B[vk] + A[vi, vk]


@T.prim_func(s_tir=True)
def rowsum_cross_thread_reduction(a: T.handle, b: T.handle) -> None:
    A = T.match_buffer(a, (128, 128))
    B = T.match_buffer(b, (128,))
    for i0 in T.serial(0, 128):
        for i1 in T.thread_binding(0, 128, thread="threadIdx.x"):
            with T.sblock("B"):
                vi, vk = T.axis.remap("SR", [i0, i1])
                with T.init():
                    B[vi] = 0.0
                B[vi] = B[vi] + A[vi, vk]


@T.prim_func(s_tir=True)
def opaque_block(a: T.handle) -> None:
    A = T.match_buffer(a, (16,))
    for i in T.serial(0, 15):
        with T.sblock("opaque"):
            A[i + 1] = A[i + 1] + A[i]


@T.prim_func(s_tir=True)
def block_inside_init(a: T.handle, b: T.handle) -> None:
    A = T.match_buffer(a, [128, 128, 128], dtype="float32")
    B = T.match_buffer(b, [128, 128], dtype="float32")
    for i in T.serial(0, 128):
        with T.sblock("outer"):
            vi = T.axis.S(128, i)
            with T.init():
                for j in T.serial(0, 128):
                    with T.sblock("init"):
                        vj = T.axis.S(128, j)
                        B[vi, vj] = 0.0
            for k in T.serial(0, 128):
                for j in T.serial(0, 128):
                    with T.sblock("inner"):
                        vj, vk = T.axis.remap("SR", [j, k])
                        B[vi, vj] = B[vi, vj] + A[vi, vj, vk]


@T.prim_func(s_tir=True)
def thread_bound_block_inside_init(a: T.handle, b: T.handle) -> None:
    A = T.match_buffer(a, [128, 128, 128], dtype="float32")
    B = T.match_buffer(b, [128, 128], dtype="float32")
    for i in T.thread_binding(0, 128, thread="threadIdx.x"):
        with T.sblock("outer"):
            vi = T.axis.S(128, i)
            with T.init():
                for j in T.serial(0, 128):
                    with T.sblock("init"):
                        vj = T.axis.S(128, j)
                        B[vi, vj] = 0.0
            for k in T.serial(0, 128):
                for j in T.serial(0, 128):
                    with T.sblock("inner"):
                        vj, vk = T.axis.remap("SR", [j, k])
                        B[vi, vj] = B[vi, vj] + A[vi, vj, vk]


@T.prim_func(s_tir=True)
def decomposed_gemm(
    A: T.Buffer((16, 16), "float32"),
    B: T.Buffer((16, 16), "float32"),
    C: T.Buffer((16, 16), "float32"),
):
    local = T.sblock_alloc_buffer((16, 16), "float32")
    for i, j in T.grid(4, 4):
        for ii, jj in T.grid(4, 4):
            with T.sblock("init"):
                vi = T.axis.S(16, i * 4 + ii)
                vj = T.axis.S(16, j * 4 + jj)
                local[vi, vj] = 0
        for k, ii, jj in T.grid(16, 4, 4):
            with T.sblock("update"):
                vi = T.axis.S(16, i * 4 + ii)
                vj = T.axis.S(16, j * 4 + jj)
                vk = T.axis.R(16, k)
                local[vi, vj] += A[vi, vk] * B[vj, vk]
        for ii, jj in T.grid(4, 4):
            with T.sblock("C"):
                vi = T.axis.S(16, i * 4 + ii)
                vj = T.axis.S(16, j * 4 + jj)
                C[vi, vj] = local[vi, vj]


@T.prim_func(s_tir=True)
def decomposed_gemm_after_vectorize(
    A: T.Buffer((16, 16), "float32"),
    B: T.Buffer((16, 16), "float32"),
    C: T.Buffer((16, 16), "float32"),
):
    local = T.sblock_alloc_buffer((16, 16), "float32")
    for i, j in T.grid(4, 4):
        for ii, jj in T.grid(4, 4):
            with T.sblock("init"):
                vi = T.axis.S(16, i * 4 + ii)
                vj = T.axis.S(16, j * 4 + jj)
                local[vi, vj] = 0
        for k, ii, jj in T.grid(16, 4, 4):
            with T.sblock("update"):
                vi = T.axis.S(16, i * 4 + ii)
                vj = T.axis.S(16, j * 4 + jj)
                vk = T.axis.R(16, k)
                local[vi, vj] += A[vi, vk] * B[vj, vk]
        for ii in range(4):
            for jj in T.vectorized(4):
                with T.sblock("C"):
                    vi = T.axis.S(16, i * 4 + ii)
                    vj = T.axis.S(16, j * 4 + jj)
                    C[vi, vj] = local[vi, vj]


@T.prim_func(s_tir=True)
def nested_block_bind(
    A: T.Buffer((16, 16, 16, 16), "float32"), B: T.Buffer((16, 16, 16), "float32")
):
    for i, j in T.grid(16, 16):
        with T.sblock("outer"):
            vi, vj = T.axis.remap("SS", [i, j])
            for k, l in T.grid(16, 16):
                with T.sblock("inner"):
                    vk, vl = T.axis.remap("SR", [k, l])
                    with T.init():
                        B[vi, vj, vk] = 0.0
                    B[vi, vj, vk] = B[vi, vj, vk] + A[vi, vj, vk, vl]


@T.prim_func(s_tir=True)
def thread_bound_nested_block(
    A: T.Buffer((16, 16, 16, 16), "float32"), B: T.Buffer((16, 16, 16), "float32")
) -> None:
    for i in T.serial(16):
        for j in T.thread_binding(16, thread="blockIdx.x"):
            with T.sblock("outer"):
                vi, vj = T.axis.remap("SS", [i, j])
                for k in T.serial(16):
                    for l in T.thread_binding(16, thread="threadIdx.x"):
                        with T.sblock("inner"):
                            vk, vl = T.axis.remap("SR", [k, l])
                            with T.init():
                                B[vi, vj, vk] = T.float32(0)
                            B[vi, vj, vk] = B[vi, vj, vk] + A[vi, vj, vk, vl]


@T.prim_func(s_tir=True)
def nested_block_bind_after_cache_read(
    A: T.Buffer((16, 16), "float32"), B: T.Buffer((16,), "float32")
) -> None:
    for i in T.serial(16):
        with T.sblock("outer"):
            vi = T.axis.spatial(16, i)
            A_shared = T.sblock_alloc_buffer([1, 16], dtype="float32", scope="shared")
            for ax0, ax1 in T.grid(1, 16):
                with T.sblock("A_shared"):
                    v0 = T.axis.spatial(16, vi + ax0)
                    v1 = T.axis.spatial(16, ax1)
                    A_shared[v0, v1] = A[v0, v1]
            for j in T.serial(16):
                with T.sblock("inner"):
                    vj = T.axis.reduce(16, j)
                    with T.init():
                        B[vi] = T.float32(0)
                    B[vi] = B[vi] + A_shared[vi, vj]


@T.prim_func(s_tir=True)
def thread_bound_nested_block_after_cache_read(
    A: T.Buffer((16, 16), "float32"), B: T.Buffer((16,), "float32")
) -> None:
    for i in T.thread_binding(16, thread="blockIdx.x"):
        with T.sblock("outer"):
            vi = T.axis.spatial(16, i)
            A_shared = T.sblock_alloc_buffer([1, 16], dtype="float32", scope="shared")
            for ax0, ax1 in T.grid(1, 16):
                with T.sblock("A_shared"):
                    v0 = T.axis.spatial(16, vi + ax0)
                    v1 = T.axis.spatial(16, ax1)
                    A_shared[v0, v1] = A[v0, v1]
            for j in T.thread_binding(16, thread="threadIdx.x"):
                with T.sblock("inner"):
                    vj = T.axis.reduce(16, j)
                    with T.init():
                        B[vi] = T.float32(0)
                    B[vi] = B[vi] + A_shared[vi, vj]


@T.prim_func(s_tir=True)
def decomposed_gemm_parallelize_init(
    A: T.Buffer((16, 16), "float32"),
    B: T.Buffer((16, 16), "float32"),
    C: T.Buffer((16, 16), "float32"),
) -> None:
    local = T.sblock_alloc_buffer([16, 16], dtype="float32")
    for i, j in T.grid(4, 4):
        for ii in T.serial(4):
            for jj in T.vectorized(4):
                with T.sblock("init"):
                    vi = T.axis.spatial(16, i * 4 + ii)
                    vj = T.axis.spatial(16, j * 4 + jj)
                    T.reads()
                    T.writes(local[vi, vj])
                    local[vi, vj] = 0
        for k, ii, jj in T.grid(16, 4, 4):
            with T.sblock("update"):
                vi = T.axis.spatial(16, i * 4 + ii)
                vj = T.axis.spatial(16, j * 4 + jj)
                vk = T.axis.reduce(16, k)
                T.reads(local[vi, vj], A[vi, vk], B[vj, vk])
                T.writes(local[vi, vj])
                local[vi, vj] = local[vi, vj] + A[vi, vk] * B[vj, vk]
        for ii, jj in T.grid(4, 4):
            with T.sblock("C"):
                vi = T.axis.spatial(16, i * 4 + ii)
                vj = T.axis.spatial(16, j * 4 + jj)
                T.reads(local[vi, vj])
                T.writes(C[vi, vj])
                C[vi, vj] = local[vi, vj]


@T.prim_func(s_tir=True)
def scatter_compute(A: T.Buffer((16,), "float32"), B: T.Buffer((16,), "float32")):
    for i in T.grid(8):
        with T.sblock("first_half"):
            vi = T.axis.spatial(16, 8 + i)
            B[vi] = A[vi - 8]

    for i in T.grid(8):
        with T.sblock("last_half"):
            vi = T.axis.spatial(16, i)
            B[vi] = A[vi + 8]


@T.prim_func(s_tir=True)
def scatter_compute_parallelize(
    A: T.Buffer((16,), "float32"), B: T.Buffer((16,), "float32")
) -> None:
    # body
    # with T.sblock("root")
    for i in T.parallel(8):
        with T.sblock("first_half"):
            vi = T.axis.spatial(16, 8 + i)
            T.reads(A[vi - 8])
            T.writes(B[vi])
            B[vi] = A[vi - 8]
    for i in T.parallel(8):
        with T.sblock("last_half"):
            vi = T.axis.spatial(16, i)
            T.reads(A[vi + 8])
            T.writes(B[vi])
            B[vi] = A[vi + 8]


# pylint: enable=no-member,invalid-name,unused-variable


def test_parallel():
    s = tvm.s_tir.Schedule(element_wise, debug_mask="all")
    i, _ = s.get_loops(s.get_sblock("B"))
    s.parallel(i)
    assert_structural_equal_ignore_global_symbol(s.mod["main"], element_wise_parallelized)
    verify_trace_roundtrip(s, mod=element_wise)


def test_parallel_predicate():
    s = tvm.s_tir.Schedule(element_wise_split_predicate, debug_mask="all")
    _, j, _ = s.get_loops(s.get_sblock("B"))
    s.parallel(j)
    assert_structural_equal_ignore_global_symbol(
        s.mod["main"], element_wise_split_predicate_parallelized
    )
    verify_trace_roundtrip(s, mod=element_wise_split_predicate)


def test_parallel_reduction_block_iter():
    s = tvm.s_tir.Schedule(matmul, debug_mask="all")
    _, _, k = s.get_loops(s.get_sblock("C"))
    with pytest.raises(tvm.s_tir.ScheduleError):
        s.parallel(k)


def test_parallel_not_quasi_affine():
    s = tvm.s_tir.Schedule(rowsum_not_quasi_affine, debug_mask="all")
    i, _ = s.get_loops(s.get_sblock("B"))
    with pytest.raises(tvm.s_tir.ScheduleError):
        s.parallel(i)


def test_parallel_not_compact_data_flow():
    s = tvm.s_tir.Schedule(rowsum_not_compact_data_flow, debug_mask="all")
    i, _ = s.get_loops(s.get_sblock("B"))
    with pytest.raises(tvm.s_tir.ScheduleError):
        s.parallel(i)


def test_vectorize():
    s = tvm.s_tir.Schedule(element_wise_compute_at_split, debug_mask="all")
    _, _, j1i = s.get_loops(s.get_sblock("C"))
    s.vectorize(j1i)
    assert_structural_equal_ignore_global_symbol(
        s.mod["main"], element_wise_compute_at_split_vectorized
    )
    verify_trace_roundtrip(s, mod=element_wise_compute_at_split)


def test_vectorize_predicate():
    s = tvm.s_tir.Schedule(element_wise_split_predicate, debug_mask="all")
    i, _, _ = s.get_loops(s.get_sblock("B"))
    s.vectorize(i)
    assert_structural_equal_ignore_global_symbol(
        s.mod["main"], element_wise_split_predicate_vectorized
    )
    verify_trace_roundtrip(s, mod=element_wise_split_predicate)


def test_vectorize_opaque_block():
    s = tvm.s_tir.Schedule(opaque_block, debug_mask="all")
    (i,) = s.get_loops(s.get_sblock("opaque"))
    with pytest.raises(tvm.s_tir.ScheduleError):
        s.vectorize(i)


def test_unroll():
    s = tvm.s_tir.Schedule(rowsum, debug_mask="all")
    i, _ = s.get_loops(s.get_sblock("B"))
    s.unroll(i)
    assert_structural_equal_ignore_global_symbol(s.mod["main"], rowsum_unrolled)
    verify_trace_roundtrip(s, mod=rowsum)


def test_unroll_after_bind():
    s = tvm.s_tir.Schedule(rowsum, debug_mask="all")
    i, _ = s.get_loops(s.get_sblock("B"))
    s.bind(i, "blockIdx.x")
    s.unroll(i)
    assert_structural_equal_ignore_global_symbol(s.mod["main"], rowsum_unrolled)
    verify_trace_roundtrip(s, mod=rowsum)


def test_bind1():
    s = tvm.s_tir.Schedule(element_wise, debug_mask="all")
    i, _ = s.get_loops(s.get_sblock("B"))
    s.bind(i, "threadIdx.x")
    assert_structural_equal_ignore_global_symbol(s.mod["main"], element_wise_i_bound)
    verify_trace_roundtrip(s, mod=element_wise)


def test_bind2():
    s = tvm.s_tir.Schedule(element_wise_compute_at_split, debug_mask="all")
    _, j0 = s.get_loops(s.get_sblock("B"))
    _, j1o, _ = s.get_loops(s.get_sblock("C"))
    s.bind(j0, "threadIdx.x")
    s.bind(j1o, "threadIdx.x")
    assert_structural_equal_ignore_global_symbol(
        s.mod["main"], element_wise_compute_at_split_j0_j1o_bound
    )
    verify_trace_roundtrip(s, mod=element_wise_compute_at_split)


def test_bind_cross_thread_reduction():
    s = tvm.s_tir.Schedule(rowsum, debug_mask="all")
    _, k = s.get_loops(s.get_sblock("B"))
    s.bind(k, "threadIdx.x")
    assert_structural_equal_ignore_global_symbol(s.mod["main"], rowsum_cross_thread_reduction)
    verify_trace_roundtrip(s, mod=rowsum)


def test_bind_not_cross_thread_reduction():
    s = tvm.s_tir.Schedule(rowsum, debug_mask="all")
    _, k = s.get_loops(s.get_sblock("B"))
    with pytest.raises(tvm.s_tir.ScheduleError):
        s.bind(k, "blockIdx.x")


def test_bind_after_bind():
    s = tvm.s_tir.Schedule(element_wise, debug_mask="all")
    i, _ = s.get_loops(s.get_sblock("B"))
    s.bind(i, "blockIdx.x")
    s.bind(i, "threadIdx.x")
    assert_structural_equal_ignore_global_symbol(s.mod["main"], element_wise_i_bound)
    verify_trace_roundtrip(s, mod=element_wise)


def test_block_inside_init():
    s = tvm.s_tir.Schedule(block_inside_init, debug_mask="all")
    (i,) = s.get_loops(s.get_sblock("outer"))
    s.bind(i, "threadIdx.x")
    assert_structural_equal_ignore_global_symbol(s.mod["main"], thread_bound_block_inside_init)
    verify_trace_roundtrip(s, mod=block_inside_init)


def test_vectorize_after_decompose():
    s = tvm.s_tir.Schedule(decomposed_gemm, debug_mask="all")
    jj = s.get_loops(s.get_sblock("C"))[-1]
    s.vectorize(jj)
    assert_structural_equal_ignore_global_symbol(s.mod["main"], decomposed_gemm_after_vectorize)
    verify_trace_roundtrip(s, mod=decomposed_gemm)


def test_nested_block_bind():
    s = tvm.s_tir.Schedule(nested_block_bind)
    block_outer = s.get_sblock("outer")
    block_inner = s.get_sblock("inner")
    _, j = s.get_loops(block_outer)
    _, l = s.get_loops(block_inner)
    s.bind(l, "threadIdx.x")
    s.bind(j, "blockIdx.x")
    assert_structural_equal_ignore_global_symbol(s.mod["main"], thread_bound_nested_block)
    verify_trace_roundtrip(s, mod=nested_block_bind)


def test_nexted_block_bind_after_cache_read():
    s = tvm.s_tir.Schedule(nested_block_bind_after_cache_read)
    block_outer = s.get_sblock("outer")
    block_inner = s.get_sblock("inner")
    (i,) = s.get_loops(block_outer)
    (j,) = s.get_loops(block_inner)
    s.bind(i, "blockIdx.x")
    s.bind(j, "threadIdx.x")
    assert_structural_equal_ignore_global_symbol(
        s.mod["main"], thread_bound_nested_block_after_cache_read
    )
    verify_trace_roundtrip(s, mod=nested_block_bind_after_cache_read)


def test_vectorize_init():
    s = tvm.s_tir.Schedule(decomposed_gemm, debug_mask="all")
    init_blk = s.get_sblock("init")
    upd_blk = s.get_sblock("update")
    _, _, ii_0, jj_0 = s.get_loops(init_blk)
    _, _, k_1, ii_1, jj_1 = s.get_loops(upd_blk)
    s.vectorize(jj_0)
    assert_structural_equal_ignore_global_symbol(s.mod["main"], decomposed_gemm_parallelize_init)
    verify_trace_roundtrip(s, mod=decomposed_gemm)


def test_scatter_parallelize():
    s = tvm.s_tir.Schedule(scatter_compute, debug_mask="all")
    first = s.get_sblock("first_half")
    last = s.get_sblock("last_half")
    (i_0,) = s.get_loops(first)
    (i_1,) = s.get_loops(last)
    s.parallel(i_0)
    s.parallel(i_1)
    assert_structural_equal_ignore_global_symbol(s.mod["main"], scatter_compute_parallelize)
    verify_trace_roundtrip(s, mod=scatter_compute)


def test_bind_thread_iter_var_dtype():
    @T.prim_func(private=True, s_tir=True)
    def before(
        A: T.Buffer((T.int64(128), T.int64(128))),
        B: T.Buffer((T.int64(128), T.int64(128))),
    ) -> None:
        for i, j in T.grid(T.int64(128), T.int64(128)):
            with T.sblock("B"):
                vi, vj = T.axis.remap("SS", [i, j])
                B[vi, vj] = A[vi, vj] * 2.0

    @T.prim_func(private=True, s_tir=True)
    def expected(
        A: T.Buffer((T.int64(128), T.int64(128))),
        B: T.Buffer((T.int64(128), T.int64(128))),
    ) -> None:
        for i0 in T.thread_binding(T.int64(128), thread="threadIdx.x"):
            # Use T.serial with explicit int64 min so the inner sblock iter_var dom
            # is all-int64 (matches what `s.bind` emits; `range(T.int64(128))` parses
            # min as int32 even when extent is int64).
            for i1 in T.serial(T.int64(0), T.int64(128)):
                with T.sblock("B"):
                    vi, vj = T.axis.remap("SS", [i0, i1])
                    B[vi, vj] = A[vi, vj] * 2.0

    s = tvm.s_tir.Schedule(before, debug_mask="all")
    i, _ = s.get_loops(s.get_sblock("B"))
    s.bind(i, "threadIdx.x")
    assert_structural_equal_ignore_global_symbol(s.mod["main"], expected)
    verify_trace_roundtrip(s, mod=before)


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