# 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 re from functools import partial import numpy as np import tvm import tvm.testing from tvm import relax as rx from tvm import tirx from tvm.relax.testing import dump_ast from tvm.relax.testing.ast_printer import ASTPrinter from tvm.script import relax as R from tvm.script import tirx as T # Overload dump_ast to test both type and type annotations dump_ast = partial(dump_ast, include_ty_annotations=True) def strip_whitespace(text: str) -> str: """ Remove all whitespace to avoid reasoning about newlines and indents """ return re.sub(r"\s", "", text) def normalize(func: rx.Function) -> rx.Function: """ Normalize the expr to fill in the ty fields everywhere """ # using a default mutator to use the BlockBuilder's normalizer, # which oddly differs from the Normalize pass @rx.expr_functor.mutator class DefaultMutator(rx.PyExprMutator): pass mod = tvm.IRModule() mod["main"] = func mut = DefaultMutator(mod) mod["main"] = mut.visit_expr(func) return mod["main"] def assert_fields(nodename: str, fields: dict[str, str], target: str) -> None: """ Given a target string, ensure that the string defines the specified node and that the given mappings of fields to values are present in the string. Strips all whitespace in the target and fields. Does not assume any particular ordering for the fields. """ stripped_target = strip_whitespace(target) assert stripped_target.startswith(f"{nodename}(") for field, value in fields.items(): assert f"{field}={strip_whitespace(value)}" in stripped_target # test cases are mostly adapted from text_expr, only testing very basic properties def test_var() -> None: v0 = rx.Var("v0") v0_str = dump_ast(v0) assert v0_str == 'Var(name_hint="v0")' v1 = rx.Var("v1", R.Tensor([54, 96], "float32")) v1_no_annos = dump_ast(v1, include_ty_annotations=False) assert v1_no_annos == 'Var(name_hint="v1")' v1_annos = dump_ast(v1) assert v1_annos != v1_no_annos assert "Expr" in v1_annos assert "ty" in v1_annos def test_dataflow_var() -> None: v0 = rx.DataflowVar("v0") v0_str = dump_ast(v0) assert v0_str == 'DataflowVar(name_hint="v0")' v1 = rx.DataflowVar("v1", R.Tensor([54, 96], "float16")) v1_no_annos = dump_ast(v1, include_ty_annotations=False) assert v1_no_annos == 'DataflowVar(name_hint="v1")' v1_annos = dump_ast(v1) assert v1_annos != v1_no_annos assert "Expr" in v1_annos assert "ty" in v1_annos def test_match_cast() -> None: # match_cast([16, 8], [m, n]) m = tirx.Var("m", dtype="int64") n = tirx.Var("n", dtype="int64") shape = rx.const([16, 8], "int32") var = rx.Var("v0", R.Shape()) b0 = rx.MatchCast(var, shape, R.Tensor([m, n], "int32")) b0_str = dump_ast(b0) assert b0_str.startswith("MatchCast(") assert "Constant" in b0_str assert "Expr(value=`m" in b0_str assert "Expr(value=`n" in b0_str assert "16" in b0_str assert "8" in b0_str # var1: Tensor((m, n), "float32") = # match_cast(var0: R.Tensor("float32"), [m, n]) value = rx.Var("value", R.Tensor("float32")) var = rx.Var("v1", R.Tensor([m, n], "float32")) b1 = rx.MatchCast(var, value, R.Tensor([m, n], "float32")) b1_str = dump_ast(b1) assert b1_str.startswith("MatchCast(") assert "Expr(value=`m" in b1_str assert "Expr(value=`n" in b1_str assert b1_str != dump_ast(b1, include_ty_annotations=False) def test_var_binding() -> None: v0 = rx.Var("v0") val = rx.const(np.random.rand(24, 56)) b0 = rx.VarBinding(v0, val) b0_str = dump_ast(b0, include_ty_annotations=False) assert b0_str.startswith("VarBinding(") assert 'var=Var(name_hint="v0")' in b0_str assert "value=" in b0_str assert "Constant(" in b0_str def test_binding_block() -> None: m = tirx.Var("m", dtype="int64") n = tirx.Var("n", dtype="int64") shape = rx.const([16, 8], "int32") b0 = rx.MatchCast(rx.Var("v0"), shape, R.Tensor([m, n], "int32")) v0 = rx.Var("v0") val = rx.const(np.random.rand(24, 56)) b1 = rx.VarBinding(v0, val) block0 = rx.BindingBlock([b0, b1]) block0_str = dump_ast(block0) assert block0_str.startswith("BindingBlock(") assert "bindings=" in block0_str assert "VarBinding(" in block0_str assert "MatchCast(" in block0_str assert '"v0"' in block0_str def test_dataflow_block() -> None: m = tirx.Var("m", dtype="int64") n = tirx.Var("n", dtype="int64") shape = rx.const([16, 8], "int32") b0 = rx.MatchCast(rx.Var("v0"), shape, R.Tensor([m, n], "int32")) v0 = rx.Var("v0") val = rx.const(np.random.rand(24, 56)) b1 = rx.VarBinding(v0, val) block0 = rx.DataflowBlock([b0, b1]) block0_str = dump_ast(block0) assert block0_str.startswith("DataflowBlock(") assert "bindings=" in block0_str assert "VarBinding(" in block0_str assert "MatchCast(" in block0_str assert '"v0"' in block0_str def test_seq_expr() -> None: x = rx.Var("foo") bindings = [rx.VarBinding(x, rx.const(1))] blocks = [rx.BindingBlock(bindings)] seqe = rx.SeqExpr(blocks, x) seqe_str = dump_ast(seqe) assert seqe_str.startswith("SeqExpr(") assert "blocks=" in seqe_str assert "BindingBlock(" in seqe_str assert "VarBinding(" in seqe_str assert "Constant(" in seqe_str assert 'var=Var(name_hint="foo")' in seqe_str assert "value=Constant(data" in strip_whitespace(seqe_str) assert "body=" in seqe_str def test_shape_expr() -> None: m = tirx.Var("m", dtype="int32") n = tirx.Var("n", dtype="int32") s = rx.ShapeExpr([m, n]) s_str = dump_ast(s) assert s_str.startswith("ShapeExpr(") assert "values=" in s_str assert "Expr(value=`m: int32`)" in s_str assert "Expr(value=`n: int32`)" in s_str def test_func(): x = rx.Var("foo", R.Tensor("float32", ndim=2)) bindings = [rx.VarBinding(x, rx.const(1))] blocks = [rx.BindingBlock(bindings)] seqe = rx.SeqExpr(blocks, x) func = rx.Function([x], seqe, R.Tensor("float32")) func = func.with_attr("global_symbol", "func") func_str = dump_ast(func) assert func_str.startswith("Function(") assert "params=" in func_str assert "body=" in func_str assert "ret_ty=" in func_str assert "is_pure=" in func_str assert "attrs=" in func_str assert '"global_symbol": "func"' in func_str assert "SeqExpr(" in func_str assert "blocks=" in func_str assert "VarBinding(" in func_str def test_shape_of(): v0 = rx.Var("v0", R.Tensor(ndim=2)) s0 = rx.get_shape_of(v0) s0_str = dump_ast(s0) assert s0_str.startswith("Call(") assert 'op=Op(name="relax.shape_of")' in s0_str assert "args=" in s0_str assert 'name_hint="v0"' in s0_str v1 = rx.Var("v1", R.Tensor([96, 54])) s1 = rx.get_shape_of(v1) s1_str = dump_ast(s1) assert s1_str.startswith("ShapeExpr("), s1_str assert "values=" in s1_str assert "Expr(value=`T.int64(96)`)" in s1_str assert "Expr(value=`T.int64(54)`)" in s1_str def test_shape_expr(): shape_expr = rx.ShapeExpr([10, 20]) shape_expr_str = dump_ast(shape_expr) assert shape_expr_str.startswith("ShapeExpr(") assert "values" in shape_expr_str assert "Expr(value=`T.int64(10)`)" in shape_expr_str assert "Expr(value=`T.int64(20)`)" in shape_expr_str def test_types(): printer = ASTPrinter() assert strip_whitespace(printer.visit_type_(rx.ShapeType(ndim=-1))) == "ShapeType(ndim=-1)" assert strip_whitespace(printer.visit_type_(rx.ShapeType(ndim=1))) == "ShapeType(ndim=1)" object_type = rx.AnyType() assert strip_whitespace(printer.visit_type_(object_type)) == "AnyType()" packed_type = rx.PackedFuncType() assert strip_whitespace(printer.visit_type_(packed_type)) == "PackedFuncType()" tensor_type = rx.TensorType(ndim=2, dtype="int32") assert strip_whitespace(printer.visit_type_(tensor_type)) == "TensorType(ndim=2,dtype=int32)" unit_type = rx.TupleType([]) assert strip_whitespace(printer.visit_type_(unit_type)) == "TupleType(fields=[])" tuple_type = rx.TupleType([rx.ShapeType(ndim=-1), object_type]) assert_fields( "TupleType", {"fields": "[ShapeType(ndim=-1),AnyType()]"}, strip_whitespace(printer.visit_type_(tuple_type)), ) func_type = rx.FuncType([tensor_type], unit_type) assert_fields( "FuncType", { "params": "[TensorType(ndim=2, dtype=int32)]", "ret": "TupleType(fields=[])", "purity": "True", }, printer.visit_type_(func_type), ) def test_ty(): printer = ASTPrinter() assert printer.visit_ty_(rx.AnyType()) == "AnyType()" assert printer.visit_ty_(tvm.ir.PrimType("int32")) == "PrimType(dtype=int32)" # empty shape empty_ssi = rx.ShapeType() assert printer.visit_ty_(empty_ssi) == "ShapeType(ndim=-1)" # include some dimensions shape_info = rx.ShapeType([tirx.IntImm("int64", 1), tirx.IntImm("int64", 2)]) assert strip_whitespace(printer.visit_ty_(shape_info)) == strip_whitespace( """ ShapeType( ndim=2, values=[ Expr(value=`T.int64(1)`), Expr(value=`T.int64(2)`) ] ) """ ) # tensor type default_tsi = rx.TensorType() assert strip_whitespace(printer.visit_ty_(default_tsi)) == "TensorType(dtype=float32,ndim=-1)" # use a var as the shape x = rx.Var("x", ty=rx.ShapeType(values=[])) var_tsi = rx.TensorType(shape=x, dtype="int32") assert strip_whitespace(printer.visit_ty_(var_tsi)) == strip_whitespace( """ TensorType( dtype=int32, shape=Var( name_hint="x", ty=ShapeType(ndim=0, values=[]) ) ) """ ) empty_tuple = rx.TupleType([]) assert printer.visit_ty_(empty_tuple) == "TupleType(fields=[])" tuple_of_shape = rx.TupleType([empty_ssi]) assert strip_whitespace(printer.visit_ty_(tuple_of_shape)) == strip_whitespace( """ TupleType(fields=[ ShapeType(ndim=-1) ]) """ ) simple_func = rx.FuncType([], rx.AnyType()) assert ( strip_whitespace(printer.visit_ty_(simple_func)) == "FuncType(params=[],ret=AnyType(),purity=True)" ) def test_call_packed(): # test case from test_parser @R.function(pure=False) def f( x: R.Tensor((32, "m"), "float32"), y: R.Tensor(("m",), "float32"), r: R.Tensor(dtype="int64"), ) -> R.Any: m = T.int64() z: R.Tensor((32, m), "float32") = R.multiply(x, y) w: R.Tensor(ndim=2) = R.multiply(z, z) q: R.Tensor = R.add(w, w) t = R.add(w, z) sh: R.Shape = R.shape_of(t) o: R.Any = R.call_packed( "contrib.tensor_array_stack", x, y, ty_args=R.Any(), test_attr=True ) return o # checking that the call_packed call is turned into a call to an extern func f_str = strip_whitespace( dump_ast( f, include_ty_annotations=False, include_call_attrs=True, ) ) # the function has an annotated return type assert "ret_ty=AnyType()" in f_str # the purity attribute is set to false assert "is_pure=False" assert isinstance(f.body, rx.SeqExpr) extern_call = f.body.blocks[0].bindings[-1].value extern_call_text = dump_ast( extern_call, include_ty_annotations=False, include_call_attrs=True, ) assert strip_whitespace(extern_call_text) in f_str assert_fields( "Call", { "op": 'ExternFunc(global_symbol="contrib.tensor_array_stack")', "args": '[Var(name_hint="x"), Var(name_hint="y")]', "ty_args": "[AnyType()]", "attrs": '{"test_attr": True}', }, extern_call_text, ) # check that the op call is there too op_call = f.body.blocks[0].bindings[0].value op_call_text = dump_ast( op_call, include_ty_annotations=False, include_call_attrs=True, ) assert strip_whitespace(op_call_text) in f_str assert_fields( "Call", { "op": 'Op(name="relax.multiply")', "args": '[Var(name_hint="x"), Var(name_hint="y")]', }, op_call_text, ) def test_op_attrs(): x = rx.Var("x", R.Tensor((10,), "float32")) # Manually create a Call with attributes to test printer support for Op attributes op = tvm.ir.Op.get("relax.add") attrs = tvm.ir.make_node("ir.DictAttrs", my_attr="my_value") call_node = rx.Call(op, [x, x], attrs=attrs) call_str = dump_ast(call_node, include_call_attrs=True) assert_fields( "Call", { "op": 'Op(name="relax.add")', "attrs": '{"my_attr": "my_value"}', }, call_str, ) def test_call_tir(): # also from test_parser @tvm.script.ir_module class TestCallTIR: @T.prim_func(s_tir=True) def addone(A_handle: T.handle, B_handle: T.handle) -> None: m = T.int64() n = T.int64() A = T.match_buffer(A_handle, (m, n), "float32") B = T.match_buffer(B_handle, (m, n), "float32") T.func_attr({"global_symbol": "addone"}) for i, j in T.grid(m, n): with T.sblock("addone"): vi, vj = T.axis.remap("SS", [i, j]) B[vi, vj] = A[vi, vj] + T.int32(1) @R.function def foo(x: R.Tensor(("m", "n"), "float32")): m, n = T.int64(), T.int64() gv0 = R.call_tir(TestCallTIR.addone, (x,), R.Tensor((m, n), dtype="float32")) return gv0 mod = TestCallTIR foo = mod["foo"] foo_str = strip_whitespace( dump_ast( foo, include_ty_annotations=False, include_call_attrs=False, ) ) assert foo_str.startswith('Function(params=[Var(name_hint="x")]') # call_tir is an op in Relax and it takes an extern func as an argument assert isinstance(foo.body, rx.SeqExpr) tir_call = foo.body.blocks[0].bindings[0].value tir_call_text = dump_ast( tir_call, include_ty_annotations=False, include_call_attrs=False, ) assert_fields( "Call", { "op": 'Op(name="relax.call_tir")', "args": """[ GlobalVar(name_hint="addone"), Tuple(fields=[Var(name_hint="x")]) ]""", "ty_args": """[ TensorType( dtype=float32, shape=ShapeExpr( values=[ Expr(value=`m`), Expr(value=`n`) ] ) ) ]""", }, tir_call_text, ) assert strip_whitespace(tir_call_text) in foo_str def test_call_dps_packed(): @R.function def foo(x: R.Tensor(("m", "n"), "float32")): m, n = T.int64(), T.int64() gv0 = R.call_dps_packed("test.op.identity", (x,), R.Tensor((m, n), dtype="float32")) return gv0 foo_str = strip_whitespace( dump_ast( foo, include_ty_annotations=False, include_call_attrs=False, ) ) assert foo_str.startswith('Function(params=[Var(name_hint="x")]') # call_dps_packed is an op in Relax and it takes an extern func as an argument assert isinstance(foo.body, rx.SeqExpr) tir_call = foo.body.blocks[0].bindings[0].value tir_call_text = dump_ast( tir_call, include_ty_annotations=False, include_call_attrs=False, ) assert_fields( "Call", { "op": 'Op(name="relax.call_dps_packed")', "args": """[ ExternFunc(global_symbol="test.op.identity"), Tuple(fields=[Var(name_hint="x")]) ]""", "ty_args": """[ TensorType( dtype=float32, shape=ShapeExpr( values=[ Expr(value=`m`), Expr(value=`n`) ] ) ) ]""", }, tir_call_text, ) assert strip_whitespace(tir_call_text) in foo_str def test_operators(): @R.function def foo(x: R.Tensor): return R.unique(x, sorted=True, axis=-1) foo_str = strip_whitespace( dump_ast( foo, include_ty_annotations=False, ) ) assert 'Op(name="relax.unique")' in foo_str # the sorted argument is true, so it will be a boolean Expr assert "Expr(value=`T.bool(True)`)" in foo_str # axis is -1 assert "Expr(value=`T.int64(-1)`)" in foo_str @R.function(pure=False) def bar(x: R.Tensor): return R.print(x, format="{}") bar_str = strip_whitespace( dump_ast( bar, include_ty_annotations=False, ) ) # the format string is a StringImm argument assert 'StringImm(value="{}")' in bar_str def test_print_ty_annotation_non_var(): @R.function def f() -> R.Tensor: return R.const([1, 2]) body = normalize(f).body body_str = strip_whitespace(dump_ast(body)) # the constant has a shape of (2,) ty = strip_whitespace( """ ty=TensorType( dtype=int32, shape=ShapeExpr( values=[Expr(value=`T.int64(2)`)], ty=ShapeType( ndim=1, values=[Expr(value=`T.int64(2)`)] ) ) ) """ ) assert ty in body_str def test_print_type_annotation_non_var(): @R.function def f() -> R.Shape: return R.shape_of(R.const(1)) body = normalize(f).body assert isinstance(body, rx.SeqExpr) call = body.blocks[-1].bindings[-1].value assert isinstance(call, rx.Call) def test_if(): @R.function def f(cond: R.Tensor((), dtype="bool")) -> R.Tensor((), dtype="int32"): if cond: x = R.const(1) else: x = R.const(2) return x body = normalize(f).body assert isinstance(body, rx.SeqExpr) body_str = strip_whitespace(dump_ast(body)) # we expect both branches to be seq exprs assert "If" in body_str assert "true_branch=SeqExpr(" in body_str assert "false_branch=SeqExpr(" in body_str def test_tuple_get_item(): @R.function def f(x: R.Tuple(R.Tensor((), dtype="int32"))) -> R.Tensor((), dtype="int32"): return x[0] body = normalize(f).body assert isinstance(body, rx.SeqExpr) body_str = strip_whitespace(dump_ast(body)) assert "TupleGetItem" in body_str assert 'tuple_value=Var(name_hint="x"' in body_str assert "index=0" in body_str def test_prim_value(): prim_value = tirx.IntImm("int64", 1) prim_str = strip_whitespace(dump_ast(prim_value)) assert prim_str == strip_whitespace( """ Expr(value=`T.int64(1)`) """ ) def test_string_imm(): string_imm = rx.StringImm("test") str_str = strip_whitespace(dump_ast(string_imm)) assert str_str == strip_whitespace( """ StringImm( value="test", ty=AnyType() ) """ ) def test_datatype_imm(): data_type_imm = rx.DataTypeImm("int32") data_type_str = strip_whitespace(dump_ast(data_type_imm)) assert data_type_str == strip_whitespace( """ DataTypeImm( value=int32, ty=AnyType() ) """ ) if __name__ == "__main__": tvm.testing.main()