Skip to content

Commit ddbea69

Browse files
authored
[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.
1 parent 9ccd081 commit ddbea69

File tree

13 files changed

+335
-57
lines changed

13 files changed

+335
-57
lines changed

mypyc/analysis/dataflow.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
BasicBlock, OpVisitor, Assign, AssignMulti, Integer, LoadErrorValue, RegisterOp, Goto, Branch,
1010
Return, Call, Box, Unbox, Cast, Op, Unreachable, TupleGet, TupleSet, GetAttr, SetAttr,
1111
LoadLiteral, LoadStatic, InitStatic, MethodCall, RaiseStandardError, CallC, LoadGlobal,
12-
Truncate, IntOp, LoadMem, GetElementPtr, LoadAddress, ComparisonOp, SetMem, KeepAlive
12+
Truncate, IntOp, LoadMem, GetElementPtr, LoadAddress, ComparisonOp, SetMem, KeepAlive, Extend
1313
)
1414
from mypyc.ir.func_ir import all_values
1515

@@ -199,6 +199,9 @@ def visit_call_c(self, op: CallC) -> GenAndKill[T]:
199199
def visit_truncate(self, op: Truncate) -> GenAndKill[T]:
200200
return self.visit_register_op(op)
201201

202+
def visit_extend(self, op: Extend) -> GenAndKill[T]:
203+
return self.visit_register_op(op)
204+
202205
def visit_load_global(self, op: LoadGlobal) -> GenAndKill[T]:
203206
return self.visit_register_op(op)
204207

mypyc/analysis/ircheck.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
InitStatic, TupleGet, TupleSet, IncRef, DecRef, Call, MethodCall, Cast,
88
Box, Unbox, RaiseStandardError, CallC, Truncate, LoadGlobal, IntOp, ComparisonOp,
99
LoadMem, SetMem, GetElementPtr, LoadAddress, KeepAlive, Register, Integer,
10-
BaseAssign
10+
BaseAssign, Extend
1111
)
1212
from mypyc.ir.rtypes import (
1313
RType, RPrimitive, RUnion, is_object_rprimitive, RInstance, RArray,
@@ -326,6 +326,9 @@ def visit_call_c(self, op: CallC) -> None:
326326
def visit_truncate(self, op: Truncate) -> None:
327327
pass
328328

329+
def visit_extend(self, op: Extend) -> None:
330+
pass
331+
329332
def visit_load_global(self, op: LoadGlobal) -> None:
330333
pass
331334

mypyc/analysis/selfleaks.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
OpVisitor, Register, Goto, Assign, AssignMulti, SetMem, Call, MethodCall, LoadErrorValue,
55
LoadLiteral, GetAttr, SetAttr, LoadStatic, InitStatic, TupleGet, TupleSet, Box, Unbox,
66
Cast, RaiseStandardError, CallC, Truncate, LoadGlobal, IntOp, ComparisonOp, LoadMem,
7-
GetElementPtr, LoadAddress, KeepAlive, Branch, Return, Unreachable, RegisterOp, BasicBlock
7+
GetElementPtr, LoadAddress, KeepAlive, Branch, Return, Unreachable, RegisterOp, BasicBlock,
8+
Extend
89
)
910
from mypyc.ir.rtypes import RInstance
1011
from mypyc.analysis.dataflow import MAYBE_ANALYSIS, run_analysis, AnalysisResult, CFG
@@ -115,6 +116,9 @@ def visit_call_c(self, op: CallC) -> GenAndKill:
115116
def visit_truncate(self, op: Truncate) -> GenAndKill:
116117
return CLEAN
117118

119+
def visit_extend(self, op: Extend) -> GenAndKill:
120+
return CLEAN
121+
118122
def visit_load_global(self, op: LoadGlobal) -> GenAndKill:
119123
return CLEAN
120124

mypyc/codegen/emit.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
is_list_rprimitive, is_dict_rprimitive, is_set_rprimitive, is_tuple_rprimitive,
1818
is_none_rprimitive, is_object_rprimitive, object_rprimitive, is_str_rprimitive,
1919
int_rprimitive, is_optional_type, optional_value_type, is_int32_rprimitive,
20-
is_int64_rprimitive, is_bit_rprimitive, is_range_rprimitive, is_bytes_rprimitive
20+
is_int64_rprimitive, is_bit_rprimitive, is_range_rprimitive, is_bytes_rprimitive,
21+
is_fixed_width_rtype
2122
)
2223
from mypyc.ir.func_ir import FuncDecl
2324
from mypyc.ir.class_ir import ClassIR, all_concrete_classes
@@ -479,9 +480,16 @@ def emit_cast(self,
479480
return
480481

481482
# TODO: Verify refcount handling.
482-
if (is_list_rprimitive(typ) or is_dict_rprimitive(typ) or is_set_rprimitive(typ)
483-
or is_str_rprimitive(typ) or is_range_rprimitive(typ) or is_float_rprimitive(typ)
484-
or is_int_rprimitive(typ) or is_bool_rprimitive(typ) or is_bit_rprimitive(typ)):
483+
if (is_list_rprimitive(typ)
484+
or is_dict_rprimitive(typ)
485+
or is_set_rprimitive(typ)
486+
or is_str_rprimitive(typ)
487+
or is_range_rprimitive(typ)
488+
or is_float_rprimitive(typ)
489+
or is_int_rprimitive(typ)
490+
or is_bool_rprimitive(typ)
491+
or is_bit_rprimitive(typ)
492+
or is_fixed_width_rtype(typ)):
485493
if declare_dest:
486494
self.emit_line(f'PyObject *{dest};')
487495
if is_list_rprimitive(typ):
@@ -496,12 +504,13 @@ def emit_cast(self,
496504
prefix = 'PyRange'
497505
elif is_float_rprimitive(typ):
498506
prefix = 'CPyFloat'
499-
elif is_int_rprimitive(typ):
507+
elif is_int_rprimitive(typ) or is_fixed_width_rtype(typ):
508+
# TODO: Range check for fixed-width types?
500509
prefix = 'PyLong'
501510
elif is_bool_rprimitive(typ) or is_bit_rprimitive(typ):
502511
prefix = 'PyBool'
503512
else:
504-
assert False, 'unexpected primitive type'
513+
assert False, f'unexpected primitive type: {typ}'
505514
check = '({}_Check({}))'
506515
if likely:
507516
check = f'(likely{check})'
@@ -765,6 +774,20 @@ def emit_unbox(self,
765774
self.emit_line(failure)
766775
self.emit_line('} else')
767776
self.emit_line(f' {dest} = 1;')
777+
elif is_int64_rprimitive(typ):
778+
# Whether we are borrowing or not makes no difference.
779+
if declare_dest:
780+
self.emit_line(f'int64_t {dest};')
781+
self.emit_line(f'{dest} = CPyLong_AsInt64({src});')
782+
# TODO: Handle 'optional'
783+
# TODO: Handle 'failure'
784+
elif is_int32_rprimitive(typ):
785+
# Whether we are borrowing or not makes no difference.
786+
if declare_dest:
787+
self.emit_line('int32_t {};'.format(dest))
788+
self.emit_line('{} = CPyLong_AsInt32({});'.format(dest, src))
789+
# TODO: Handle 'optional'
790+
# TODO: Handle 'failure'
768791
elif isinstance(typ, RTuple):
769792
self.declare_tuple_struct(typ)
770793
if declare_dest:

mypyc/codegen/emitfunc.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
LoadStatic, InitStatic, TupleGet, TupleSet, Call, IncRef, DecRef, Box, Cast, Unbox,
1313
BasicBlock, Value, MethodCall, Unreachable, NAMESPACE_STATIC, NAMESPACE_TYPE, NAMESPACE_MODULE,
1414
RaiseStandardError, CallC, LoadGlobal, Truncate, IntOp, LoadMem, GetElementPtr,
15-
LoadAddress, ComparisonOp, SetMem, Register, LoadLiteral, AssignMulti, KeepAlive, ERR_FALSE
15+
LoadAddress, ComparisonOp, SetMem, Register, LoadLiteral, AssignMulti, KeepAlive, Extend,
16+
ERR_FALSE
1617
)
1718
from mypyc.ir.rtypes import (
1819
RType, RTuple, RArray, is_tagged, is_int32_rprimitive, is_int64_rprimitive, RStruct,
@@ -210,6 +211,10 @@ def visit_assign(self, op: Assign) -> None:
210211
# clang whines about self assignment (which we might generate
211212
# for some casts), so don't emit it.
212213
if dest != src:
214+
# We sometimes assign from an integer prepresentation of a pointer
215+
# to a real pointer, and C compilers insist on a cast.
216+
if op.src.type.is_unboxed and not op.dest.type.is_unboxed:
217+
src = f'(void *){src}'
213218
self.emit_line(f'{dest} = {src};')
214219

215220
def visit_assign_multi(self, op: AssignMulti) -> None:
@@ -538,6 +543,15 @@ def visit_truncate(self, op: Truncate) -> None:
538543
# for C backend the generated code are straight assignments
539544
self.emit_line(f"{dest} = {value};")
540545

546+
def visit_extend(self, op: Extend) -> None:
547+
dest = self.reg(op)
548+
value = self.reg(op.src)
549+
if op.signed:
550+
src_cast = self.emit_signed_int_cast(op.src.type)
551+
else:
552+
src_cast = self.emit_unsigned_int_cast(op.src.type)
553+
self.emit_line("{} = {}{};".format(dest, src_cast, value))
554+
541555
def visit_load_global(self, op: LoadGlobal) -> None:
542556
dest = self.reg(op)
543557
ann = ''
@@ -551,6 +565,10 @@ def visit_int_op(self, op: IntOp) -> None:
551565
dest = self.reg(op)
552566
lhs = self.reg(op.lhs)
553567
rhs = self.reg(op.rhs)
568+
if op.op == IntOp.RIGHT_SHIFT:
569+
# Signed right shift
570+
lhs = self.emit_signed_int_cast(op.lhs.type) + lhs
571+
rhs = self.emit_signed_int_cast(op.rhs.type) + rhs
554572
self.emit_line(f'{dest} = {lhs} {op.op_str[op.op]} {rhs};')
555573

556574
def visit_comparison_op(self, op: ComparisonOp) -> None:
@@ -624,7 +642,10 @@ def reg(self, reg: Value) -> str:
624642
s = str(val)
625643
if val >= (1 << 31):
626644
# Avoid overflowing signed 32-bit int
627-
s += 'ULL'
645+
if val >= (1 << 63):
646+
s += 'ULL'
647+
else:
648+
s += 'LL'
628649
elif val == -(1 << 63):
629650
# Avoid overflowing C integer literal
630651
s = '(-9223372036854775807LL - 1)'

mypyc/ir/ops.py

Lines changed: 83 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
RType, RInstance, RTuple, RArray, RVoid, is_bool_rprimitive, is_int_rprimitive,
2222
is_short_int_rprimitive, is_none_rprimitive, object_rprimitive, bool_rprimitive,
2323
short_int_rprimitive, int_rprimitive, void_rtype, pointer_rprimitive, is_pointer_rprimitive,
24-
bit_rprimitive, is_bit_rprimitive
24+
bit_rprimitive, is_bit_rprimitive, is_fixed_width_rtype
2525
)
2626

2727
if TYPE_CHECKING:
@@ -90,6 +90,9 @@ def terminator(self) -> 'ControlOp':
9090
ERR_FALSE: Final = 2
9191
# Always fails
9292
ERR_ALWAYS: Final = 3
93+
# Like ERR_MAGIC, but the magic return overlaps with a possible return value, and
94+
# an extra PyErr_Occurred() check is also required
95+
ERR_MAGIC_OVERLAPPING: Final = 4
9396

9497
# Hack: using this line number for an op will suppress it in tracebacks
9598
NO_TRACEBACK_LINE_NO = -10000
@@ -489,14 +492,17 @@ class Call(RegisterOp):
489492
The call target can be a module-level function or a class.
490493
"""
491494

492-
error_kind = ERR_MAGIC
493-
494495
def __init__(self, fn: 'FuncDecl', args: Sequence[Value], line: int) -> None:
495-
super().__init__(line)
496496
self.fn = fn
497497
self.args = list(args)
498498
assert len(self.args) == len(fn.sig.args)
499499
self.type = fn.sig.ret_type
500+
ret_type = fn.sig.ret_type
501+
if not ret_type.error_overlap:
502+
self.error_kind = ERR_MAGIC
503+
else:
504+
self.error_kind = ERR_MAGIC_OVERLAPPING
505+
super().__init__(line)
500506

501507
def sources(self) -> List[Value]:
502508
return list(self.args[:])
@@ -508,14 +514,11 @@ def accept(self, visitor: 'OpVisitor[T]') -> T:
508514
class MethodCall(RegisterOp):
509515
"""Native method call obj.method(arg, ...)"""
510516

511-
error_kind = ERR_MAGIC
512-
513517
def __init__(self,
514518
obj: Value,
515519
method: str,
516520
args: List[Value],
517521
line: int = -1) -> None:
518-
super().__init__(line)
519522
self.obj = obj
520523
self.method = method
521524
self.args = args
@@ -524,7 +527,13 @@ def __init__(self,
524527
method_ir = self.receiver_type.class_ir.method_sig(method)
525528
assert method_ir is not None, "{} doesn't have method {}".format(
526529
self.receiver_type.name, method)
527-
self.type = method_ir.ret_type
530+
ret_type = method_ir.ret_type
531+
self.type = ret_type
532+
if not ret_type.error_overlap:
533+
self.error_kind = ERR_MAGIC
534+
else:
535+
self.error_kind = ERR_MAGIC_OVERLAPPING
536+
super().__init__(line)
528537

529538
def sources(self) -> List[Value]:
530539
return self.args[:] + [self.obj]
@@ -605,8 +614,11 @@ def __init__(self, obj: Value, attr: str, line: int, *, borrow: bool = False) ->
605614
self.attr = attr
606615
assert isinstance(obj.type, RInstance), 'Attribute access not supported: %s' % obj.type
607616
self.class_type = obj.type
608-
self.type = obj.type.attr_type(attr)
609-
self.is_borrowed = borrow
617+
attr_type = obj.type.attr_type(attr)
618+
self.type = attr_type
619+
if is_fixed_width_rtype(attr_type):
620+
self.error_kind = ERR_NEVER
621+
self.is_borrowed = borrow and attr_type.is_refcounted
610622

611623
def sources(self) -> List[Value]:
612624
return [self.obj]
@@ -829,12 +841,14 @@ class Unbox(RegisterOp):
829841
representation. Only supported for types with an unboxed representation.
830842
"""
831843

832-
error_kind = ERR_MAGIC
833-
834844
def __init__(self, src: Value, typ: RType, line: int) -> None:
835-
super().__init__(line)
836845
self.src = src
837846
self.type = typ
847+
if not typ.error_overlap:
848+
self.error_kind = ERR_MAGIC
849+
else:
850+
self.error_kind = ERR_MAGIC_OVERLAPPING
851+
super().__init__(line)
838852

839853
def sources(self) -> List[Value]:
840854
return [self.src]
@@ -924,22 +938,20 @@ class Truncate(RegisterOp):
924938
925939
Truncate a value from type with more bits to type with less bits.
926940
927-
Both src_type and dst_type should be non-reference counted integer
928-
types or bool. Note that int_rprimitive is reference counted so
929-
it should never be used here.
941+
dst_type and src_type can be native integer types, bools or tagged
942+
integers. Tagged integers should have the tag bit unset.
930943
"""
931944

932945
error_kind = ERR_NEVER
933946

934947
def __init__(self,
935948
src: Value,
936-
src_type: RType,
937949
dst_type: RType,
938950
line: int = -1) -> None:
939951
super().__init__(line)
940952
self.src = src
941-
self.src_type = src_type
942953
self.type = dst_type
954+
self.src_type = src.type
943955

944956
def sources(self) -> List[Value]:
945957
return [self.src]
@@ -951,6 +963,41 @@ def accept(self, visitor: 'OpVisitor[T]') -> T:
951963
return visitor.visit_truncate(self)
952964

953965

966+
class Extend(RegisterOp):
967+
"""result = extend src from src_type to dst_type
968+
969+
Extend a value from a type with fewer bits to a type with more bits.
970+
971+
dst_type and src_type can be native integer types, bools or tagged
972+
integers. Tagged integers should have the tag bit unset.
973+
974+
If 'signed' is true, perform sign extension. Otherwise, the result will be
975+
zero extended.
976+
"""
977+
978+
error_kind = ERR_NEVER
979+
980+
def __init__(self,
981+
src: Value,
982+
dst_type: RType,
983+
signed: bool,
984+
line: int = -1) -> None:
985+
super().__init__(line)
986+
self.src = src
987+
self.type = dst_type
988+
self.src_type = src.type
989+
self.signed = signed
990+
991+
def sources(self) -> List[Value]:
992+
return [self.src]
993+
994+
def stolen(self) -> List[Value]:
995+
return []
996+
997+
def accept(self, visitor: 'OpVisitor[T]') -> T:
998+
return visitor.visit_extend(self)
999+
1000+
9541001
class LoadGlobal(RegisterOp):
9551002
"""Load a low-level global variable/pointer.
9561003
@@ -1035,6 +1082,11 @@ def accept(self, visitor: 'OpVisitor[T]') -> T:
10351082
return visitor.visit_int_op(self)
10361083

10371084

1085+
# We can't have this in the IntOp class body, because of
1086+
# https://github.com/mypyc/mypyc/issues/932.
1087+
int_op_to_id: Final = {op: op_id for op_id, op in IntOp.op_str.items()}
1088+
1089+
10381090
class ComparisonOp(RegisterOp):
10391091
"""Low-level comparison op for integers and pointers.
10401092
@@ -1076,6 +1128,15 @@ class ComparisonOp(RegisterOp):
10761128
UGE: '>=',
10771129
}
10781130

1131+
signed_ops: Final = {
1132+
'==': EQ,
1133+
'!=': NEQ,
1134+
'<': SLT,
1135+
'>': SGT,
1136+
'<=': SLE,
1137+
'>=': SGE,
1138+
}
1139+
10791140
def __init__(self, lhs: Value, rhs: Value, op: int, line: int = -1) -> None:
10801141
super().__init__(line)
10811142
self.type = bit_rprimitive
@@ -1327,6 +1388,10 @@ def visit_call_c(self, op: CallC) -> T:
13271388
def visit_truncate(self, op: Truncate) -> T:
13281389
raise NotImplementedError
13291390

1391+
@abstractmethod
1392+
def visit_extend(self, op: Extend) -> T:
1393+
raise NotImplementedError
1394+
13301395
@abstractmethod
13311396
def visit_load_global(self, op: LoadGlobal) -> T:
13321397
raise NotImplementedError

0 commit comments

Comments
 (0)