From ddbea6988c0913c70ed16cd2fda6064e301b4b63 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 11 Jun 2022 19:19:19 +0100 Subject: [PATCH] [mypyc] Foundational work to help support native ints (#12884) Some IR and codegen changes that help with native int support. This was split off from a branch with a working implementation of native ints to make reviewing easier. Some tests and primitives are missing here and I will include them in follow-up PRs. Summary of major changes below. 1) Allow ambiguous error returns from functions. Since all values of `i64` values are valid return values, none can be reserved for errors. The approach here is to have the error value overlap a valid value, and use `PyErr_Occurred()` as a secondary check to make sure it actually was an error. 2) Add `Extend` op which extends a value to a larger integer type with either zero or sign extension. 3) Improve subtype checking with native int types. 4) Fill in other minor gaps in IR and codegen support for native ints. Work on mypyc/mypyc#837. --- mypyc/analysis/dataflow.py | 5 +- mypyc/analysis/ircheck.py | 5 +- mypyc/analysis/selfleaks.py | 6 +- mypyc/codegen/emit.py | 35 ++++++++++-- mypyc/codegen/emitfunc.py | 25 ++++++++- mypyc/ir/ops.py | 101 +++++++++++++++++++++++++++------ mypyc/ir/pprint.py | 9 ++- mypyc/ir/rtypes.py | 103 +++++++++++++++++++++++++++++----- mypyc/irbuild/ll_builder.py | 2 +- mypyc/subtype.py | 9 ++- mypyc/test/test_emitfunc.py | 38 +++++++++++-- mypyc/test/test_subtype.py | 21 ++++++- mypyc/transform/exceptions.py | 33 ++++++++++- 13 files changed, 335 insertions(+), 57 deletions(-) diff --git a/mypyc/analysis/dataflow.py b/mypyc/analysis/dataflow.py index 053efc733845..528c04af546f 100644 --- a/mypyc/analysis/dataflow.py +++ b/mypyc/analysis/dataflow.py @@ -9,7 +9,7 @@ BasicBlock, OpVisitor, Assign, AssignMulti, Integer, LoadErrorValue, RegisterOp, Goto, Branch, Return, Call, Box, Unbox, Cast, Op, Unreachable, TupleGet, TupleSet, GetAttr, SetAttr, LoadLiteral, LoadStatic, InitStatic, MethodCall, RaiseStandardError, CallC, LoadGlobal, - Truncate, IntOp, LoadMem, GetElementPtr, LoadAddress, ComparisonOp, SetMem, KeepAlive + Truncate, IntOp, LoadMem, GetElementPtr, LoadAddress, ComparisonOp, SetMem, KeepAlive, Extend ) from mypyc.ir.func_ir import all_values @@ -199,6 +199,9 @@ def visit_call_c(self, op: CallC) -> GenAndKill[T]: def visit_truncate(self, op: Truncate) -> GenAndKill[T]: return self.visit_register_op(op) + def visit_extend(self, op: Extend) -> GenAndKill[T]: + return self.visit_register_op(op) + def visit_load_global(self, op: LoadGlobal) -> GenAndKill[T]: return self.visit_register_op(op) diff --git a/mypyc/analysis/ircheck.py b/mypyc/analysis/ircheck.py index 6c8e8d7f18e5..8217d9865c4b 100644 --- a/mypyc/analysis/ircheck.py +++ b/mypyc/analysis/ircheck.py @@ -7,7 +7,7 @@ InitStatic, TupleGet, TupleSet, IncRef, DecRef, Call, MethodCall, Cast, Box, Unbox, RaiseStandardError, CallC, Truncate, LoadGlobal, IntOp, ComparisonOp, LoadMem, SetMem, GetElementPtr, LoadAddress, KeepAlive, Register, Integer, - BaseAssign + BaseAssign, Extend ) from mypyc.ir.rtypes import ( RType, RPrimitive, RUnion, is_object_rprimitive, RInstance, RArray, @@ -326,6 +326,9 @@ def visit_call_c(self, op: CallC) -> None: def visit_truncate(self, op: Truncate) -> None: pass + def visit_extend(self, op: Extend) -> None: + pass + def visit_load_global(self, op: LoadGlobal) -> None: pass diff --git a/mypyc/analysis/selfleaks.py b/mypyc/analysis/selfleaks.py index ae3731a40ac3..4ba6cfb28eb3 100644 --- a/mypyc/analysis/selfleaks.py +++ b/mypyc/analysis/selfleaks.py @@ -4,7 +4,8 @@ OpVisitor, Register, Goto, Assign, AssignMulti, SetMem, Call, MethodCall, LoadErrorValue, LoadLiteral, GetAttr, SetAttr, LoadStatic, InitStatic, TupleGet, TupleSet, Box, Unbox, Cast, RaiseStandardError, CallC, Truncate, LoadGlobal, IntOp, ComparisonOp, LoadMem, - GetElementPtr, LoadAddress, KeepAlive, Branch, Return, Unreachable, RegisterOp, BasicBlock + GetElementPtr, LoadAddress, KeepAlive, Branch, Return, Unreachable, RegisterOp, BasicBlock, + Extend ) from mypyc.ir.rtypes import RInstance from mypyc.analysis.dataflow import MAYBE_ANALYSIS, run_analysis, AnalysisResult, CFG @@ -115,6 +116,9 @@ def visit_call_c(self, op: CallC) -> GenAndKill: def visit_truncate(self, op: Truncate) -> GenAndKill: return CLEAN + def visit_extend(self, op: Extend) -> GenAndKill: + return CLEAN + def visit_load_global(self, op: LoadGlobal) -> GenAndKill: return CLEAN diff --git a/mypyc/codegen/emit.py b/mypyc/codegen/emit.py index 0815dd3c3bd0..b1f886ee3f5f 100644 --- a/mypyc/codegen/emit.py +++ b/mypyc/codegen/emit.py @@ -17,7 +17,8 @@ is_list_rprimitive, is_dict_rprimitive, is_set_rprimitive, is_tuple_rprimitive, is_none_rprimitive, is_object_rprimitive, object_rprimitive, is_str_rprimitive, int_rprimitive, is_optional_type, optional_value_type, is_int32_rprimitive, - is_int64_rprimitive, is_bit_rprimitive, is_range_rprimitive, is_bytes_rprimitive + is_int64_rprimitive, is_bit_rprimitive, is_range_rprimitive, is_bytes_rprimitive, + is_fixed_width_rtype ) from mypyc.ir.func_ir import FuncDecl from mypyc.ir.class_ir import ClassIR, all_concrete_classes @@ -479,9 +480,16 @@ def emit_cast(self, return # TODO: Verify refcount handling. - if (is_list_rprimitive(typ) or is_dict_rprimitive(typ) or is_set_rprimitive(typ) - or is_str_rprimitive(typ) or is_range_rprimitive(typ) or is_float_rprimitive(typ) - or is_int_rprimitive(typ) or is_bool_rprimitive(typ) or is_bit_rprimitive(typ)): + if (is_list_rprimitive(typ) + or is_dict_rprimitive(typ) + or is_set_rprimitive(typ) + or is_str_rprimitive(typ) + or is_range_rprimitive(typ) + or is_float_rprimitive(typ) + or is_int_rprimitive(typ) + or is_bool_rprimitive(typ) + or is_bit_rprimitive(typ) + or is_fixed_width_rtype(typ)): if declare_dest: self.emit_line(f'PyObject *{dest};') if is_list_rprimitive(typ): @@ -496,12 +504,13 @@ def emit_cast(self, prefix = 'PyRange' elif is_float_rprimitive(typ): prefix = 'CPyFloat' - elif is_int_rprimitive(typ): + elif is_int_rprimitive(typ) or is_fixed_width_rtype(typ): + # TODO: Range check for fixed-width types? prefix = 'PyLong' elif is_bool_rprimitive(typ) or is_bit_rprimitive(typ): prefix = 'PyBool' else: - assert False, 'unexpected primitive type' + assert False, f'unexpected primitive type: {typ}' check = '({}_Check({}))' if likely: check = f'(likely{check})' @@ -765,6 +774,20 @@ def emit_unbox(self, self.emit_line(failure) self.emit_line('} else') self.emit_line(f' {dest} = 1;') + elif is_int64_rprimitive(typ): + # Whether we are borrowing or not makes no difference. + if declare_dest: + self.emit_line(f'int64_t {dest};') + self.emit_line(f'{dest} = CPyLong_AsInt64({src});') + # TODO: Handle 'optional' + # TODO: Handle 'failure' + elif is_int32_rprimitive(typ): + # Whether we are borrowing or not makes no difference. + if declare_dest: + self.emit_line('int32_t {};'.format(dest)) + self.emit_line('{} = CPyLong_AsInt32({});'.format(dest, src)) + # TODO: Handle 'optional' + # TODO: Handle 'failure' elif isinstance(typ, RTuple): self.declare_tuple_struct(typ) if declare_dest: diff --git a/mypyc/codegen/emitfunc.py b/mypyc/codegen/emitfunc.py index ce428daaee71..683bf3e7a034 100644 --- a/mypyc/codegen/emitfunc.py +++ b/mypyc/codegen/emitfunc.py @@ -12,7 +12,8 @@ LoadStatic, InitStatic, TupleGet, TupleSet, Call, IncRef, DecRef, Box, Cast, Unbox, BasicBlock, Value, MethodCall, Unreachable, NAMESPACE_STATIC, NAMESPACE_TYPE, NAMESPACE_MODULE, RaiseStandardError, CallC, LoadGlobal, Truncate, IntOp, LoadMem, GetElementPtr, - LoadAddress, ComparisonOp, SetMem, Register, LoadLiteral, AssignMulti, KeepAlive, ERR_FALSE + LoadAddress, ComparisonOp, SetMem, Register, LoadLiteral, AssignMulti, KeepAlive, Extend, + ERR_FALSE ) from mypyc.ir.rtypes import ( RType, RTuple, RArray, is_tagged, is_int32_rprimitive, is_int64_rprimitive, RStruct, @@ -210,6 +211,10 @@ def visit_assign(self, op: Assign) -> None: # clang whines about self assignment (which we might generate # for some casts), so don't emit it. if dest != src: + # We sometimes assign from an integer prepresentation of a pointer + # to a real pointer, and C compilers insist on a cast. + if op.src.type.is_unboxed and not op.dest.type.is_unboxed: + src = f'(void *){src}' self.emit_line(f'{dest} = {src};') def visit_assign_multi(self, op: AssignMulti) -> None: @@ -538,6 +543,15 @@ def visit_truncate(self, op: Truncate) -> None: # for C backend the generated code are straight assignments self.emit_line(f"{dest} = {value};") + def visit_extend(self, op: Extend) -> None: + dest = self.reg(op) + value = self.reg(op.src) + if op.signed: + src_cast = self.emit_signed_int_cast(op.src.type) + else: + src_cast = self.emit_unsigned_int_cast(op.src.type) + self.emit_line("{} = {}{};".format(dest, src_cast, value)) + def visit_load_global(self, op: LoadGlobal) -> None: dest = self.reg(op) ann = '' @@ -551,6 +565,10 @@ def visit_int_op(self, op: IntOp) -> None: dest = self.reg(op) lhs = self.reg(op.lhs) rhs = self.reg(op.rhs) + if op.op == IntOp.RIGHT_SHIFT: + # Signed right shift + lhs = self.emit_signed_int_cast(op.lhs.type) + lhs + rhs = self.emit_signed_int_cast(op.rhs.type) + rhs self.emit_line(f'{dest} = {lhs} {op.op_str[op.op]} {rhs};') def visit_comparison_op(self, op: ComparisonOp) -> None: @@ -624,7 +642,10 @@ def reg(self, reg: Value) -> str: s = str(val) if val >= (1 << 31): # Avoid overflowing signed 32-bit int - s += 'ULL' + if val >= (1 << 63): + s += 'ULL' + else: + s += 'LL' elif val == -(1 << 63): # Avoid overflowing C integer literal s = '(-9223372036854775807LL - 1)' diff --git a/mypyc/ir/ops.py b/mypyc/ir/ops.py index d36fcfb9e7eb..8474b5ab58e2 100644 --- a/mypyc/ir/ops.py +++ b/mypyc/ir/ops.py @@ -21,7 +21,7 @@ RType, RInstance, RTuple, RArray, RVoid, is_bool_rprimitive, is_int_rprimitive, is_short_int_rprimitive, is_none_rprimitive, object_rprimitive, bool_rprimitive, short_int_rprimitive, int_rprimitive, void_rtype, pointer_rprimitive, is_pointer_rprimitive, - bit_rprimitive, is_bit_rprimitive + bit_rprimitive, is_bit_rprimitive, is_fixed_width_rtype ) if TYPE_CHECKING: @@ -90,6 +90,9 @@ def terminator(self) -> 'ControlOp': ERR_FALSE: Final = 2 # Always fails ERR_ALWAYS: Final = 3 +# Like ERR_MAGIC, but the magic return overlaps with a possible return value, and +# an extra PyErr_Occurred() check is also required +ERR_MAGIC_OVERLAPPING: Final = 4 # Hack: using this line number for an op will suppress it in tracebacks NO_TRACEBACK_LINE_NO = -10000 @@ -489,14 +492,17 @@ class Call(RegisterOp): The call target can be a module-level function or a class. """ - error_kind = ERR_MAGIC - def __init__(self, fn: 'FuncDecl', args: Sequence[Value], line: int) -> None: - super().__init__(line) self.fn = fn self.args = list(args) assert len(self.args) == len(fn.sig.args) self.type = fn.sig.ret_type + ret_type = fn.sig.ret_type + if not ret_type.error_overlap: + self.error_kind = ERR_MAGIC + else: + self.error_kind = ERR_MAGIC_OVERLAPPING + super().__init__(line) def sources(self) -> List[Value]: return list(self.args[:]) @@ -508,14 +514,11 @@ def accept(self, visitor: 'OpVisitor[T]') -> T: class MethodCall(RegisterOp): """Native method call obj.method(arg, ...)""" - error_kind = ERR_MAGIC - def __init__(self, obj: Value, method: str, args: List[Value], line: int = -1) -> None: - super().__init__(line) self.obj = obj self.method = method self.args = args @@ -524,7 +527,13 @@ def __init__(self, method_ir = self.receiver_type.class_ir.method_sig(method) assert method_ir is not None, "{} doesn't have method {}".format( self.receiver_type.name, method) - self.type = method_ir.ret_type + ret_type = method_ir.ret_type + self.type = ret_type + if not ret_type.error_overlap: + self.error_kind = ERR_MAGIC + else: + self.error_kind = ERR_MAGIC_OVERLAPPING + super().__init__(line) def sources(self) -> List[Value]: return self.args[:] + [self.obj] @@ -605,8 +614,11 @@ def __init__(self, obj: Value, attr: str, line: int, *, borrow: bool = False) -> self.attr = attr assert isinstance(obj.type, RInstance), 'Attribute access not supported: %s' % obj.type self.class_type = obj.type - self.type = obj.type.attr_type(attr) - self.is_borrowed = borrow + attr_type = obj.type.attr_type(attr) + self.type = attr_type + if is_fixed_width_rtype(attr_type): + self.error_kind = ERR_NEVER + self.is_borrowed = borrow and attr_type.is_refcounted def sources(self) -> List[Value]: return [self.obj] @@ -829,12 +841,14 @@ class Unbox(RegisterOp): representation. Only supported for types with an unboxed representation. """ - error_kind = ERR_MAGIC - def __init__(self, src: Value, typ: RType, line: int) -> None: - super().__init__(line) self.src = src self.type = typ + if not typ.error_overlap: + self.error_kind = ERR_MAGIC + else: + self.error_kind = ERR_MAGIC_OVERLAPPING + super().__init__(line) def sources(self) -> List[Value]: return [self.src] @@ -924,22 +938,20 @@ class Truncate(RegisterOp): Truncate a value from type with more bits to type with less bits. - Both src_type and dst_type should be non-reference counted integer - types or bool. Note that int_rprimitive is reference counted so - it should never be used here. + dst_type and src_type can be native integer types, bools or tagged + integers. Tagged integers should have the tag bit unset. """ error_kind = ERR_NEVER def __init__(self, src: Value, - src_type: RType, dst_type: RType, line: int = -1) -> None: super().__init__(line) self.src = src - self.src_type = src_type self.type = dst_type + self.src_type = src.type def sources(self) -> List[Value]: return [self.src] @@ -951,6 +963,41 @@ def accept(self, visitor: 'OpVisitor[T]') -> T: return visitor.visit_truncate(self) +class Extend(RegisterOp): + """result = extend src from src_type to dst_type + + Extend a value from a type with fewer bits to a type with more bits. + + dst_type and src_type can be native integer types, bools or tagged + integers. Tagged integers should have the tag bit unset. + + If 'signed' is true, perform sign extension. Otherwise, the result will be + zero extended. + """ + + error_kind = ERR_NEVER + + def __init__(self, + src: Value, + dst_type: RType, + signed: bool, + line: int = -1) -> None: + super().__init__(line) + self.src = src + self.type = dst_type + self.src_type = src.type + self.signed = signed + + def sources(self) -> List[Value]: + return [self.src] + + def stolen(self) -> List[Value]: + return [] + + def accept(self, visitor: 'OpVisitor[T]') -> T: + return visitor.visit_extend(self) + + class LoadGlobal(RegisterOp): """Load a low-level global variable/pointer. @@ -1035,6 +1082,11 @@ def accept(self, visitor: 'OpVisitor[T]') -> T: return visitor.visit_int_op(self) +# We can't have this in the IntOp class body, because of +# https://github.com/mypyc/mypyc/issues/932. +int_op_to_id: Final = {op: op_id for op_id, op in IntOp.op_str.items()} + + class ComparisonOp(RegisterOp): """Low-level comparison op for integers and pointers. @@ -1076,6 +1128,15 @@ class ComparisonOp(RegisterOp): UGE: '>=', } + signed_ops: Final = { + '==': EQ, + '!=': NEQ, + '<': SLT, + '>': SGT, + '<=': SLE, + '>=': SGE, + } + def __init__(self, lhs: Value, rhs: Value, op: int, line: int = -1) -> None: super().__init__(line) self.type = bit_rprimitive @@ -1327,6 +1388,10 @@ def visit_call_c(self, op: CallC) -> T: def visit_truncate(self, op: Truncate) -> T: raise NotImplementedError + @abstractmethod + def visit_extend(self, op: Extend) -> T: + raise NotImplementedError + @abstractmethod def visit_load_global(self, op: LoadGlobal) -> T: raise NotImplementedError diff --git a/mypyc/ir/pprint.py b/mypyc/ir/pprint.py index 40243dac96e9..e6cd721e4c27 100644 --- a/mypyc/ir/pprint.py +++ b/mypyc/ir/pprint.py @@ -11,7 +11,7 @@ LoadStatic, InitStatic, TupleGet, TupleSet, IncRef, DecRef, Call, MethodCall, Cast, Box, Unbox, RaiseStandardError, CallC, Truncate, LoadGlobal, IntOp, ComparisonOp, LoadMem, SetMem, GetElementPtr, LoadAddress, Register, Value, OpVisitor, BasicBlock, ControlOp, LoadLiteral, - AssignMulti, KeepAlive, Op, ERR_NEVER + AssignMulti, KeepAlive, Op, Extend, ERR_NEVER ) from mypyc.ir.func_ir import FuncIR, all_values_full from mypyc.ir.module_ir import ModuleIRs @@ -172,6 +172,13 @@ def visit_call_c(self, op: CallC) -> str: def visit_truncate(self, op: Truncate) -> str: return self.format("%r = truncate %r: %t to %t", op, op.src, op.src_type, op.type) + def visit_extend(self, op: Extend) -> str: + if op.signed: + extra = ' signed' + else: + extra = '' + return self.format("%r = extend%s %r: %t to %t", op, extra, op.src, op.src_type, op.type) + def visit_load_global(self, op: LoadGlobal) -> str: ann = f' ({repr(op.ann)})' if op.ann else '' return self.format('%r = load_global %s :: static%s', op, op.identifier, ann) diff --git a/mypyc/ir/rtypes.py b/mypyc/ir/rtypes.py index 2c875d7c8f01..010e25976f1c 100644 --- a/mypyc/ir/rtypes.py +++ b/mypyc/ir/rtypes.py @@ -45,11 +45,21 @@ class RType: is_unboxed = False # This is the C undefined value for this type. It's used for initialization # if there's no value yet, and for function return value on error/exception. + # + # TODO: This shouldn't be specific to C or a string c_undefined: str # If unboxed: does the unboxed version use reference counting? is_refcounted = True # C type; use Emitter.ctype() to access _ctype: str + # If True, error/undefined value overlaps with a valid value. To + # detect an exception, PyErr_Occurred() must be used in addition + # to checking for error value as the return value of a function. + # + # For example, no i64 value can be reserved for error value, so we + # pick an arbitrary value (e.g. -113) to signal error, but this is + # also a valid non-error value. + error_overlap = False @abstractmethod def accept(self, visitor: 'RTypeVisitor[T]') -> T: @@ -173,29 +183,40 @@ class RPrimitive(RType): def __init__(self, name: str, + *, is_unboxed: bool, is_refcounted: bool, + is_native_int: bool = False, + is_signed: bool = False, ctype: str = 'PyObject *', - size: int = PLATFORM_SIZE) -> None: + size: int = PLATFORM_SIZE, + error_overlap: bool = False) -> None: RPrimitive.primitive_map[name] = self self.name = name self.is_unboxed = is_unboxed - self._ctype = ctype self.is_refcounted = is_refcounted + self.is_native_int = is_native_int + self.is_signed = is_signed + self._ctype = ctype self.size = size - # TODO: For low-level integers, they actually don't have undefined values - # we need to figure out some way to represent here. + self.error_overlap = error_overlap if ctype == 'CPyTagged': self.c_undefined = 'CPY_INT_TAG' - elif ctype in ('int32_t', 'int64_t', 'CPyPtr', 'uint32_t', 'uint64_t'): + elif ctype in ('int32_t', 'int64_t'): + # This is basically an arbitrary value that is pretty + # unlikely to overlap with a real value. + self.c_undefined = '-113' + elif ctype in ('CPyPtr', 'uint32_t', 'uint64_t'): + # TODO: For low-level integers, we need to invent an overlapping + # error value, similar to int64_t above. self.c_undefined = '0' elif ctype == 'PyObject *': # Boxed types use the null pointer as the error value. self.c_undefined = 'NULL' elif ctype == 'char': self.c_undefined = '2' - elif ctype == 'PyObject **': + elif ctype in ('PyObject **', 'void *'): self.c_undefined = 'NULL' else: assert False, 'Unrecognized ctype: %r' % ctype @@ -265,16 +286,42 @@ def __hash__(self) -> int: # Low level integer types (correspond to C integer types) int32_rprimitive: Final = RPrimitive( - "int32", is_unboxed=True, is_refcounted=False, ctype="int32_t", size=4 + "int32", + is_unboxed=True, + is_refcounted=False, + is_native_int=True, + is_signed=True, + ctype="int32_t", + size=4, + error_overlap=True, ) int64_rprimitive: Final = RPrimitive( - "int64", is_unboxed=True, is_refcounted=False, ctype="int64_t", size=8 + "int64", + is_unboxed=True, + is_refcounted=False, + is_native_int=True, + is_signed=True, + ctype="int64_t", + size=8, + error_overlap=True, ) uint32_rprimitive: Final = RPrimitive( - "uint32", is_unboxed=True, is_refcounted=False, ctype="uint32_t", size=4 + "uint32", + is_unboxed=True, + is_refcounted=False, + is_native_int=True, + is_signed=False, + ctype="uint32_t", + size=4, ) uint64_rprimitive: Final = RPrimitive( - "uint64", is_unboxed=True, is_refcounted=False, ctype="uint64_t", size=8 + "uint64", + is_unboxed=True, + is_refcounted=False, + is_native_int=True, + is_signed=False, + ctype="uint64_t", + size=8, ) # The C 'int' type @@ -282,16 +329,34 @@ def __hash__(self) -> int: if IS_32_BIT_PLATFORM: c_size_t_rprimitive = uint32_rprimitive - c_pyssize_t_rprimitive = RPrimitive('native_int', is_unboxed=True, is_refcounted=False, - ctype='int32_t', size=4) + c_pyssize_t_rprimitive = RPrimitive( + 'native_int', + is_unboxed=True, + is_refcounted=False, + is_native_int=True, + is_signed=True, + ctype='int32_t', + size=4, + ) else: c_size_t_rprimitive = uint64_rprimitive - c_pyssize_t_rprimitive = RPrimitive('native_int', is_unboxed=True, is_refcounted=False, - ctype='int64_t', size=8) + c_pyssize_t_rprimitive = RPrimitive( + 'native_int', + is_unboxed=True, + is_refcounted=False, + is_native_int=True, + is_signed=True, + ctype='int64_t', + size=8, + ) -# Low level pointer, represented as integer in C backends +# Untyped pointer, represented as integer in the C backend pointer_rprimitive: Final = RPrimitive("ptr", is_unboxed=True, is_refcounted=False, ctype="CPyPtr") +# Untyped pointer, represented as void * in the C backend +c_pointer_rprimitive: Final = RPrimitive("c_ptr", is_unboxed=False, is_refcounted=False, + ctype="void *") + # Floats are represent as 'float' PyObject * values. (In the future # we'll likely switch to a more efficient, unboxed representation.) float_rprimitive: Final = RPrimitive("builtins.float", is_unboxed=False, is_refcounted=True) @@ -361,6 +426,10 @@ def is_int64_rprimitive(rtype: RType) -> bool: (rtype is c_pyssize_t_rprimitive and rtype._ctype == 'int64_t')) +def is_fixed_width_rtype(rtype: RType) -> bool: + return is_int32_rprimitive(rtype) or is_int64_rprimitive(rtype) + + def is_uint32_rprimitive(rtype: RType) -> bool: return rtype is uint32_rprimitive @@ -445,6 +514,10 @@ def visit_rprimitive(self, t: 'RPrimitive') -> str: return 'I' elif t._ctype == 'char': return 'C' + elif t._ctype == 'int64_t': + return '8' # "8 byte integer" + elif t._ctype == 'int32_t': + return '4' # "4 byte integer" assert not t.is_unboxed, f"{t} unexpected unboxed type" return 'O' diff --git a/mypyc/irbuild/ll_builder.py b/mypyc/irbuild/ll_builder.py index d5154707538b..20c8e3a80acf 100644 --- a/mypyc/irbuild/ll_builder.py +++ b/mypyc/irbuild/ll_builder.py @@ -1337,7 +1337,7 @@ def call_c(self, if desc.truncated_type is None: result = target else: - truncate = self.add(Truncate(target, desc.return_type, desc.truncated_type)) + truncate = self.add(Truncate(target, desc.truncated_type)) result = truncate if result_type and not is_runtime_subtype(result.type, result_type): if is_none_rprimitive(result_type): diff --git a/mypyc/subtype.py b/mypyc/subtype.py index 7e852f33bf4a..4ba8f6301c63 100644 --- a/mypyc/subtype.py +++ b/mypyc/subtype.py @@ -3,7 +3,7 @@ from mypyc.ir.rtypes import ( RType, RInstance, RPrimitive, RTuple, RVoid, RTypeVisitor, RUnion, RStruct, RArray, is_bool_rprimitive, is_int_rprimitive, is_tuple_rprimitive, is_short_int_rprimitive, - is_object_rprimitive, is_bit_rprimitive + is_object_rprimitive, is_bit_rprimitive, is_tagged, is_fixed_width_rtype ) @@ -43,14 +43,17 @@ def visit_runion(self, left: RUnion) -> bool: def visit_rprimitive(self, left: RPrimitive) -> bool: right = self.right if is_bool_rprimitive(left): - if is_int_rprimitive(right): + if is_tagged(right) or is_fixed_width_rtype(right): return True elif is_bit_rprimitive(left): - if is_bool_rprimitive(right) or is_int_rprimitive(right): + if is_bool_rprimitive(right) or is_tagged(right) or is_fixed_width_rtype(right): return True elif is_short_int_rprimitive(left): if is_int_rprimitive(right): return True + elif is_fixed_width_rtype(left): + if is_int_rprimitive(right): + return True return left is right def visit_rtuple(self, left: RTuple) -> bool: diff --git a/mypyc/test/test_emitfunc.py b/mypyc/test/test_emitfunc.py index 96d9155214b3..8ea0906aec61 100644 --- a/mypyc/test/test_emitfunc.py +++ b/mypyc/test/test_emitfunc.py @@ -9,7 +9,7 @@ from mypyc.ir.ops import ( BasicBlock, Goto, Return, Integer, Assign, AssignMulti, IncRef, DecRef, Branch, Call, Unbox, Box, TupleGet, GetAttr, SetAttr, Op, Value, CallC, IntOp, LoadMem, - GetElementPtr, LoadAddress, ComparisonOp, SetMem, Register, Unreachable, Cast + GetElementPtr, LoadAddress, ComparisonOp, SetMem, Register, Unreachable, Cast, Extend ) from mypyc.ir.rtypes import ( RTuple, RInstance, RType, RArray, int_rprimitive, bool_rprimitive, list_rprimitive, @@ -31,6 +31,7 @@ from mypyc.primitives.int_ops import int_neg_op from mypyc.subtype import is_subtype from mypyc.namegen import NameGenerator +from mypyc.common import PLATFORM_SIZE class TestFunctionEmitterVisitor(unittest.TestCase): @@ -258,11 +259,11 @@ def test_list_set_item(self) -> None: list_set_item_op.is_borrowed, list_set_item_op.error_kind, 55), """cpy_r_r0 = CPyList_SetItem(cpy_r_l, cpy_r_n, cpy_r_o);""") - def test_box(self) -> None: + def test_box_int(self) -> None: self.assert_emit(Box(self.n), """cpy_r_r0 = CPyTagged_StealAsObject(cpy_r_n);""") - def test_unbox(self) -> None: + def test_unbox_int(self) -> None: self.assert_emit(Unbox(self.m, int_rprimitive, 55), """if (likely(PyLong_Check(cpy_r_m))) cpy_r_r0 = CPyTagged_FromObject(cpy_r_m); @@ -271,6 +272,14 @@ def test_unbox(self) -> None: } """) + def test_box_i64(self) -> None: + self.assert_emit(Box(self.i64), + """cpy_r_r0 = PyLong_FromLongLong(cpy_r_i64);""") + + def test_unbox_i64(self) -> None: + self.assert_emit(Unbox(self.o, int64_rprimitive, 55), + """cpy_r_r0 = CPyLong_AsInt64(cpy_r_o);""") + def test_list_append(self) -> None: self.assert_emit(CallC(list_append_op.c_function_name, [self.l, self.o], list_append_op.return_type, list_append_op.steals, @@ -382,7 +391,9 @@ def test_int_op(self) -> None: self.assert_emit(IntOp(short_int_rprimitive, self.s1, self.s2, IntOp.LEFT_SHIFT, 1), """cpy_r_r0 = cpy_r_s1 << cpy_r_s2;""") self.assert_emit(IntOp(short_int_rprimitive, self.s1, self.s2, IntOp.RIGHT_SHIFT, 1), - """cpy_r_r0 = cpy_r_s1 >> cpy_r_s2;""") + """cpy_r_r0 = (Py_ssize_t)cpy_r_s1 >> (Py_ssize_t)cpy_r_s2;""") + self.assert_emit(IntOp(short_int_rprimitive, self.i64, self.i64_1, IntOp.RIGHT_SHIFT, 1), + """cpy_r_r0 = cpy_r_i64 >> cpy_r_i64_1;""") def test_comparison_op(self) -> None: # signed @@ -438,7 +449,7 @@ def test_assign_multi(self) -> None: def test_long_unsigned(self) -> None: a = Register(int64_rprimitive, 'a') self.assert_emit(Assign(a, Integer(1 << 31, int64_rprimitive)), - """cpy_r_a = 2147483648ULL;""") + """cpy_r_a = 2147483648LL;""") self.assert_emit(Assign(a, Integer((1 << 31) - 1, int64_rprimitive)), """cpy_r_a = 2147483647;""") @@ -545,6 +556,23 @@ def test_cast_and_branch_no_merge_4(self) -> None: next_branch=branch, ) + def test_extend(self) -> None: + a = Register(int32_rprimitive, 'a') + self.assert_emit(Extend(a, int64_rprimitive, signed=True), + """cpy_r_r0 = cpy_r_a;""") + self.assert_emit(Extend(a, int64_rprimitive, signed=False), + """cpy_r_r0 = (uint32_t)cpy_r_a;""") + if PLATFORM_SIZE == 4: + self.assert_emit(Extend(self.n, int64_rprimitive, signed=True), + """cpy_r_r0 = (Py_ssize_t)cpy_r_n;""") + self.assert_emit(Extend(self.n, int64_rprimitive, signed=False), + """cpy_r_r0 = cpy_r_n;""") + if PLATFORM_SIZE == 8: + self.assert_emit(Extend(a, int_rprimitive, signed=True), + """cpy_r_r0 = cpy_r_a;""") + self.assert_emit(Extend(a, int_rprimitive, signed=False), + """cpy_r_r0 = (uint32_t)cpy_r_a;""") + def assert_emit(self, op: Op, expected: str, diff --git a/mypyc/test/test_subtype.py b/mypyc/test/test_subtype.py index e106a1eaa4b7..e006e5425174 100644 --- a/mypyc/test/test_subtype.py +++ b/mypyc/test/test_subtype.py @@ -2,7 +2,10 @@ import unittest -from mypyc.ir.rtypes import bit_rprimitive, bool_rprimitive, int_rprimitive +from mypyc.ir.rtypes import ( + bit_rprimitive, bool_rprimitive, int_rprimitive, int64_rprimitive, int32_rprimitive, + short_int_rprimitive +) from mypyc.subtype import is_subtype from mypyc.rt_subtype import is_runtime_subtype @@ -11,10 +14,26 @@ class TestSubtype(unittest.TestCase): def test_bit(self) -> None: assert is_subtype(bit_rprimitive, bool_rprimitive) assert is_subtype(bit_rprimitive, int_rprimitive) + assert is_subtype(bit_rprimitive, short_int_rprimitive) + assert is_subtype(bit_rprimitive, int64_rprimitive) + assert is_subtype(bit_rprimitive, int32_rprimitive) def test_bool(self) -> None: assert not is_subtype(bool_rprimitive, bit_rprimitive) assert is_subtype(bool_rprimitive, int_rprimitive) + assert is_subtype(bool_rprimitive, short_int_rprimitive) + assert is_subtype(bool_rprimitive, int64_rprimitive) + assert is_subtype(bool_rprimitive, int32_rprimitive) + + def test_int64(self) -> None: + assert is_subtype(int64_rprimitive, int_rprimitive) + assert not is_subtype(int64_rprimitive, short_int_rprimitive) + assert not is_subtype(int64_rprimitive, int32_rprimitive) + + def test_int32(self) -> None: + assert is_subtype(int32_rprimitive, int_rprimitive) + assert not is_subtype(int32_rprimitive, short_int_rprimitive) + assert not is_subtype(int32_rprimitive, int64_rprimitive) class TestRuntimeSubtype(unittest.TestCase): diff --git a/mypyc/transform/exceptions.py b/mypyc/transform/exceptions.py index 52b25aceffe3..e845de1fcf19 100644 --- a/mypyc/transform/exceptions.py +++ b/mypyc/transform/exceptions.py @@ -12,11 +12,14 @@ from typing import List, Optional from mypyc.ir.ops import ( - Value, BasicBlock, LoadErrorValue, Return, Branch, RegisterOp, Integer, ERR_NEVER, ERR_MAGIC, - ERR_FALSE, ERR_ALWAYS, NO_TRACEBACK_LINE_NO + Value, BasicBlock, LoadErrorValue, Return, Branch, RegisterOp, ComparisonOp, CallC, + Integer, ERR_NEVER, ERR_MAGIC, ERR_FALSE, ERR_ALWAYS, ERR_MAGIC_OVERLAPPING, + NO_TRACEBACK_LINE_NO ) from mypyc.ir.func_ir import FuncIR from mypyc.ir.rtypes import bool_rprimitive +from mypyc.primitives.registry import CFunctionDescription +from mypyc.primitives.exc_ops import err_occurred_op def insert_exception_handling(ir: FuncIR) -> None: @@ -81,6 +84,20 @@ def split_blocks_at_errors(blocks: List[BasicBlock], # this is a hack to represent the always fail # semantics, using a temporary bool with value false target = Integer(0, bool_rprimitive) + elif op.error_kind == ERR_MAGIC_OVERLAPPING: + errvalue = Integer(int(target.type.c_undefined), rtype=op.type) + comp = ComparisonOp(target, errvalue, ComparisonOp.EQ) + cur_block.ops.append(comp) + new_block2 = BasicBlock() + new_blocks.append(new_block2) + branch = Branch(comp, true_label=new_block2, false_label=new_block, + op=Branch.BOOL) + cur_block.ops.append(branch) + cur_block = new_block2 + target = primitive_call(err_occurred_op, [], target.line) + cur_block.ops.append(target) + variant = Branch.IS_ERROR + negated = True else: assert False, 'unknown error kind %d' % op.error_kind @@ -101,3 +118,15 @@ def split_blocks_at_errors(blocks: List[BasicBlock], cur_block = new_block return new_blocks + + +def primitive_call(desc: CFunctionDescription, args: List[Value], line: int) -> CallC: + return CallC( + desc.c_function_name, + [], + desc.return_type, + desc.steals, + desc.is_borrowed, + desc.error_kind, + line, + )