from __future__ import annotations import unittest from mypyc.analysis.ircheck import FnError, can_coerce_to, check_func_ir from mypyc.ir.class_ir import ClassIR from mypyc.ir.func_ir import FuncDecl, FuncIR, FuncSignature from mypyc.ir.ops import ( Assign, BasicBlock, Goto, Integer, LoadAddress, LoadLiteral, Op, Register, Return, ) from mypyc.ir.pprint import format_func from mypyc.ir.rtypes import ( RInstance, RType, RUnion, bytes_rprimitive, int32_rprimitive, int64_rprimitive, none_rprimitive, object_rprimitive, pointer_rprimitive, str_rprimitive, ) def assert_has_error(fn: FuncIR, error: FnError) -> None: errors = check_func_ir(fn) assert errors == [error] def assert_no_errors(fn: FuncIR) -> None: assert not check_func_ir(fn) NONE_VALUE = Integer(0, rtype=none_rprimitive) class TestIrcheck(unittest.TestCase): def setUp(self) -> None: self.label = 0 def basic_block(self, ops: list[Op]) -> BasicBlock: self.label += 1 block = BasicBlock(self.label) block.ops = ops return block def func_decl(self, name: str, ret_type: RType | None = None) -> FuncDecl: if ret_type is None: ret_type = none_rprimitive return FuncDecl( name=name, class_name=None, module_name="module", sig=FuncSignature(args=[], ret_type=ret_type), ) def test_valid_fn(self) -> None: assert_no_errors( FuncIR( decl=self.func_decl(name="func_1"), arg_regs=[], blocks=[self.basic_block(ops=[Return(value=NONE_VALUE)])], ) ) def test_block_not_terminated_empty_block(self) -> None: block = self.basic_block([]) fn = FuncIR(decl=self.func_decl(name="func_1"), arg_regs=[], blocks=[block]) assert_has_error(fn, FnError(source=block, desc="Block not terminated")) def test_valid_goto(self) -> None: block_1 = self.basic_block([Return(value=NONE_VALUE)]) block_2 = self.basic_block([Goto(label=block_1)]) fn = FuncIR(decl=self.func_decl(name="func_1"), arg_regs=[], blocks=[block_1, block_2]) assert_no_errors(fn) def test_invalid_goto(self) -> None: block_1 = self.basic_block([Return(value=NONE_VALUE)]) goto = Goto(label=block_1) block_2 = self.basic_block([goto]) fn = FuncIR( decl=self.func_decl(name="func_1"), arg_regs=[], # block_1 omitted blocks=[block_2], ) assert_has_error(fn, FnError(source=goto, desc="Invalid control operation target: 1")) def test_invalid_register_source(self) -> None: ret = Return(value=Register(type=none_rprimitive, name="r1")) block = self.basic_block([ret]) fn = FuncIR(decl=self.func_decl(name="func_1"), arg_regs=[], blocks=[block]) assert_has_error(fn, FnError(source=ret, desc="Invalid op reference to register 'r1'")) def test_invalid_op_source(self) -> None: ret = Return(value=LoadLiteral(value="foo", rtype=str_rprimitive)) block = self.basic_block([ret]) fn = FuncIR(decl=self.func_decl(name="func_1"), arg_regs=[], blocks=[block]) assert_has_error( fn, FnError(source=ret, desc="Invalid op reference to op of type LoadLiteral") ) def test_invalid_return_type(self) -> None: ret = Return(value=Integer(value=5, rtype=int32_rprimitive)) fn = FuncIR( decl=self.func_decl(name="func_1", ret_type=int64_rprimitive), arg_regs=[], blocks=[self.basic_block([ret])], ) assert_has_error( fn, FnError(source=ret, desc="Cannot coerce source type i32 to dest type i64") ) def test_invalid_assign(self) -> None: arg_reg = Register(type=int64_rprimitive, name="r1") assign = Assign(dest=arg_reg, src=Integer(value=5, rtype=int32_rprimitive)) ret = Return(value=NONE_VALUE) fn = FuncIR( decl=self.func_decl(name="func_1"), arg_regs=[arg_reg], blocks=[self.basic_block([assign, ret])], ) assert_has_error( fn, FnError(source=assign, desc="Cannot coerce source type i32 to dest type i64") ) def test_can_coerce_to(self) -> None: cls = ClassIR(name="Cls", module_name="cls") valid_cases = [ (int64_rprimitive, int64_rprimitive), (str_rprimitive, str_rprimitive), (str_rprimitive, object_rprimitive), (object_rprimitive, str_rprimitive), (RUnion([bytes_rprimitive, str_rprimitive]), str_rprimitive), (str_rprimitive, RUnion([bytes_rprimitive, str_rprimitive])), (RInstance(cls), object_rprimitive), ] invalid_cases = [ (int64_rprimitive, int32_rprimitive), (RInstance(cls), str_rprimitive), (str_rprimitive, bytes_rprimitive), ] for src, dest in valid_cases: assert can_coerce_to(src, dest) for src, dest in invalid_cases: assert not can_coerce_to(src, dest) def test_duplicate_op(self) -> None: arg_reg = Register(type=int32_rprimitive, name="r1") assign = Assign(dest=arg_reg, src=Integer(value=5, rtype=int32_rprimitive)) block = self.basic_block([assign, assign, Return(value=NONE_VALUE)]) fn = FuncIR(decl=self.func_decl(name="func_1"), arg_regs=[], blocks=[block]) assert_has_error(fn, FnError(source=assign, desc="Func has a duplicate op")) def test_pprint(self) -> None: block_1 = self.basic_block([Return(value=NONE_VALUE)]) goto = Goto(label=block_1) block_2 = self.basic_block([goto]) fn = FuncIR( decl=self.func_decl(name="func_1"), arg_regs=[], # block_1 omitted blocks=[block_2], ) errors = [(goto, "Invalid control operation target: 1")] formatted = format_func(fn, errors) assert formatted == [ "def func_1():", "L0:", " goto L1", " ERR: Invalid control operation target: 1", ] def test_load_address_declares_register(self) -> None: rx = Register(str_rprimitive, "x") ry = Register(pointer_rprimitive, "y") load_addr = LoadAddress(pointer_rprimitive, rx) assert_no_errors( FuncIR( decl=self.func_decl(name="func_1"), arg_regs=[], blocks=[ self.basic_block( ops=[load_addr, Assign(ry, load_addr), Return(value=NONE_VALUE)] ) ], ) )