From b7a0e38738c0ad0c653e2a242c2259e2f8a84fb1 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 21 Dec 2022 21:09:07 +0000 Subject: [PATCH 01/13] [mypyc] Optimize conversions between int/bool Previously we used slow generic operations for these. --- mypyc/irbuild/ll_builder.py | 7 ++++++- mypyc/irbuild/specialize.py | 31 +++++++++++++++++++++++++++++++ mypyc/test-data/irbuild-int.test | 28 ++++++++++++++++++++++++++++ mypyc/test-data/run-integers.test | 12 ++++++++++++ 4 files changed, 77 insertions(+), 1 deletion(-) diff --git a/mypyc/irbuild/ll_builder.py b/mypyc/irbuild/ll_builder.py index 88b35a95c08c..019395eb2e98 100644 --- a/mypyc/irbuild/ll_builder.py +++ b/mypyc/irbuild/ll_builder.py @@ -326,6 +326,11 @@ def coerce( ): # Equivalent types return src + elif is_bool_rprimitive(src_type) and is_int_rprimitive(target_type): + shifted = self.int_op( + bool_rprimitive, src, Integer(1, bool_rprimitive), IntOp.LEFT_SHIFT + ) + return self.add(Extend(shifted, int_rprimitive, signed=False)) else: # To go from one unboxed type to another, we go through a boxed # in-between value, for simplicity. @@ -1795,7 +1800,7 @@ def matching_call_c( return target return None - def int_op(self, type: RType, lhs: Value, rhs: Value, op: int, line: int) -> Value: + def int_op(self, type: RType, lhs: Value, rhs: Value, op: int, line: int = -1) -> Value: """Generate a native integer binary op. Use native/C semantics, which sometimes differ from Python diff --git a/mypyc/irbuild/specialize.py b/mypyc/irbuild/specialize.py index 5810482cd43d..72e05f285147 100644 --- a/mypyc/irbuild/specialize.py +++ b/mypyc/irbuild/specialize.py @@ -33,8 +33,12 @@ ) from mypy.types import AnyType, TypeOfAny from mypyc.ir.ops import ( + Assign, BasicBlock, + Branch, + ComparisonOp, Extend, + Goto, Integer, RaiseStandardError, Register, @@ -51,6 +55,7 @@ dict_rprimitive, int32_rprimitive, int64_rprimitive, + int_rprimitive, is_dict_rprimitive, is_int32_rprimitive, is_int64_rprimitive, @@ -688,3 +693,29 @@ def translate_i32(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Value val = builder.accept(arg) return builder.coerce(val, int32_rprimitive, expr.line) return None + + +@specialize_function("builtins.bool") +def translate_bool(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Value | None: + if len(expr.args) != 1 or expr.arg_kinds[0] != ARG_POS: + return None + arg = expr.args[0] + arg_type = builder.node_type(arg) + if is_int_rprimitive(arg_type): + src = builder.accept(arg) + tmp = Register(bool_rprimitive) + b1, b2, b3 = BasicBlock(), BasicBlock(), BasicBlock() + chk = builder.builder.comparison_op( + src, Integer(0, int_rprimitive), ComparisonOp.EQ, expr.line + ) + builder.flush_keep_alives() + builder.add(Branch(chk, b1, b2, Branch.BOOL)) + builder.activate_block(b1) + builder.add(Assign(tmp, Integer(0, bool_rprimitive))) + builder.add(Goto(b3)) + builder.activate_block(b2) + builder.add(Assign(tmp, Integer(1, bool_rprimitive))) + builder.add(Goto(b3)) + builder.activate_block(b3) + return tmp + return None diff --git a/mypyc/test-data/irbuild-int.test b/mypyc/test-data/irbuild-int.test index e193c16ef979..c59969775db7 100644 --- a/mypyc/test-data/irbuild-int.test +++ b/mypyc/test-data/irbuild-int.test @@ -203,3 +203,31 @@ L0: def f5(): L0: return -2 + +[case testBoolToAndFromInt] +def b2i(b: bool) -> int: + return b +def i2b(n: int) -> bool: + return bool(n) +[out] +def b2i(b): + b, r0 :: bool + r1 :: int +L0: + r0 = b << 1 + r1 = extend r0: builtins.bool to builtins.int + return r1 +def i2b(n): + n :: int + r0 :: bit + r1 :: bool +L0: + r0 = n == 0 + if r0 goto L1 else goto L2 :: bool +L1: + r1 = 0 + goto L3 +L2: + r1 = 1 +L3: + return r1 diff --git a/mypyc/test-data/run-integers.test b/mypyc/test-data/run-integers.test index 74e7cd6b8fb7..945773ff5add 100644 --- a/mypyc/test-data/run-integers.test +++ b/mypyc/test-data/run-integers.test @@ -353,6 +353,9 @@ def is_true(x: int) -> bool: else: return False +def is_true2(x: int) -> bool: + return bool(x) + def is_false(x: int) -> bool: if not x: return True @@ -361,11 +364,20 @@ def is_false(x: int) -> bool: def test_int_as_bool() -> None: assert not is_true(0) + assert not is_true2(0) assert is_false(0) for x in 1, 55, -1, -7, 1 << 50, 1 << 101, -(1 << 50), -(1 << 101): assert is_true(x) + assert is_true2(x) assert not is_false(x) +def bool_as_int(b: bool) -> int: + return b + +def test_bool_as_int() -> None: + assert bool_as_int(False) == 0 + assert bool_as_int(True) == 1 + def test_divide() -> None: for x in range(-100, 100): for y in range(-100, 100): From 20334e0851edd9ba01366fb8450407f033946c36 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 26 Dec 2022 11:28:40 +0000 Subject: [PATCH 02/13] Simplify generated code --- mypyc/irbuild/ll_builder.py | 63 ++++++++++++++++++++++++++++++++ mypyc/irbuild/specialize.py | 16 +------- mypyc/test-data/irbuild-int.test | 12 +----- 3 files changed, 66 insertions(+), 25 deletions(-) diff --git a/mypyc/irbuild/ll_builder.py b/mypyc/irbuild/ll_builder.py index 019395eb2e98..b7a5f4b246ab 100644 --- a/mypyc/irbuild/ll_builder.py +++ b/mypyc/irbuild/ll_builder.py @@ -1647,6 +1647,69 @@ def shortcircuit_helper( self.activate_block(next_block) return target + def bool_value(self, value: Value) -> Value: + """Return bool(value). + + The result type can be bit_rprimitive or bool_rprimitive. + """ + if is_bool_rprimitive(value.type) or is_bit_rprimitive(value.type): + result = value + if is_runtime_subtype(value.type, int_rprimitive): + zero = Integer(0, short_int_rprimitive) + result = self.comparison_op(value, zero, ComparisonOp.NEQ, value.line) + elif is_fixed_width_rtype(value.type): + zero = Integer(0, value.type) + result = self.add(ComparisonOp(value, zero, ComparisonOp.NEQ)) + elif is_same_type(value.type, str_rprimitive): + result = self.call_c(str_check_if_true, [value], value.line) + elif is_same_type(value.type, list_rprimitive) or is_same_type( + value.type, dict_rprimitive + ): + length = self.builtin_len(value, value.line) + zero = Integer(0) + result = self.binary_op(length, zero, "!=", value.line) + elif ( + isinstance(value.type, RInstance) + and value.type.class_ir.is_ext_class + and value.type.class_ir.has_method("__bool__") + ): + # Directly call the __bool__ method on classes that have it. + result = self.gen_method_call(value, "__bool__", [], bool_rprimitive, value.line) + else: + value_type = optional_value_type(value.type) + if value_type is not None: + not_none = self.translate_is_op(value, self.none_object(), "is not", value.line) + always_truthy = False + if isinstance(value_type, RInstance): + # check whether X.__bool__ is always just the default (object.__bool__) + if not value_type.class_ir.has_method( + "__bool__" + ) and value_type.class_ir.is_method_final("__bool__"): + always_truthy = True + + if always_truthy: + result = not_none + else: + # "X | None" where X may be falsey and requires a check + result = Register(bit_rprimitive) + true, false, end = BasicBlock(), BasicBlock(), BasicBlock() + branch = Branch(not_none, true, false, Branch.BOOL) + self.add(branch) + self.activate_block(true) + # unbox_or_cast instead of coerce because we want the + # type to change even if it is a subtype. + remaining = self.unbox_or_cast(value, value_type, value.line) + b = self.bool_value(remaining) + self.add(Assign(result, b)) + self.goto(end) + self.activate_block(false) + self.add(Assign(result, not_none)) + self.goto(end) + self.activate_block(end) + else: + result = self.call_c(bool_op, [value], value.line) + return result + def add_bool_branch(self, value: Value, true: BasicBlock, false: BasicBlock) -> None: if is_runtime_subtype(value.type, int_rprimitive): zero = Integer(0, short_int_rprimitive) diff --git a/mypyc/irbuild/specialize.py b/mypyc/irbuild/specialize.py index 72e05f285147..2b4672ae072d 100644 --- a/mypyc/irbuild/specialize.py +++ b/mypyc/irbuild/specialize.py @@ -703,19 +703,5 @@ def translate_bool(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Value arg_type = builder.node_type(arg) if is_int_rprimitive(arg_type): src = builder.accept(arg) - tmp = Register(bool_rprimitive) - b1, b2, b3 = BasicBlock(), BasicBlock(), BasicBlock() - chk = builder.builder.comparison_op( - src, Integer(0, int_rprimitive), ComparisonOp.EQ, expr.line - ) - builder.flush_keep_alives() - builder.add(Branch(chk, b1, b2, Branch.BOOL)) - builder.activate_block(b1) - builder.add(Assign(tmp, Integer(0, bool_rprimitive))) - builder.add(Goto(b3)) - builder.activate_block(b2) - builder.add(Assign(tmp, Integer(1, bool_rprimitive))) - builder.add(Goto(b3)) - builder.activate_block(b3) - return tmp + return builder.builder.bool_value(src) return None diff --git a/mypyc/test-data/irbuild-int.test b/mypyc/test-data/irbuild-int.test index c59969775db7..1ccf0e6f2d82 100644 --- a/mypyc/test-data/irbuild-int.test +++ b/mypyc/test-data/irbuild-int.test @@ -220,14 +220,6 @@ L0: def i2b(n): n :: int r0 :: bit - r1 :: bool L0: - r0 = n == 0 - if r0 goto L1 else goto L2 :: bool -L1: - r1 = 0 - goto L3 -L2: - r1 = 1 -L3: - return r1 + r0 = n != 0 + return r0 From ee851dcec2349ff9bc719aff1b394f2d471fd64c Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 26 Dec 2022 11:30:02 +0000 Subject: [PATCH 03/13] Move test case to a dedicated file --- mypyc/test-data/irbuild-bool.test | 19 +++++++++++++++++++ mypyc/test-data/irbuild-int.test | 20 -------------------- mypyc/test/test_irbuild.py | 1 + 3 files changed, 20 insertions(+), 20 deletions(-) create mode 100644 mypyc/test-data/irbuild-bool.test diff --git a/mypyc/test-data/irbuild-bool.test b/mypyc/test-data/irbuild-bool.test new file mode 100644 index 000000000000..9d4dc23f4c60 --- /dev/null +++ b/mypyc/test-data/irbuild-bool.test @@ -0,0 +1,19 @@ +[case testBoolToAndFromInt] +def b2i(b: bool) -> int: + return b +def i2b(n: int) -> bool: + return bool(n) +[out] +def b2i(b): + b, r0 :: bool + r1 :: int +L0: + r0 = b << 1 + r1 = extend r0: builtins.bool to builtins.int + return r1 +def i2b(n): + n :: int + r0 :: bit +L0: + r0 = n != 0 + return r0 diff --git a/mypyc/test-data/irbuild-int.test b/mypyc/test-data/irbuild-int.test index 1ccf0e6f2d82..e193c16ef979 100644 --- a/mypyc/test-data/irbuild-int.test +++ b/mypyc/test-data/irbuild-int.test @@ -203,23 +203,3 @@ L0: def f5(): L0: return -2 - -[case testBoolToAndFromInt] -def b2i(b: bool) -> int: - return b -def i2b(n: int) -> bool: - return bool(n) -[out] -def b2i(b): - b, r0 :: bool - r1 :: int -L0: - r0 = b << 1 - r1 = extend r0: builtins.bool to builtins.int - return r1 -def i2b(n): - n :: int - r0 :: bit -L0: - r0 = n != 0 - return r0 diff --git a/mypyc/test/test_irbuild.py b/mypyc/test/test_irbuild.py index 8928f94d6211..cb5e690eed55 100644 --- a/mypyc/test/test_irbuild.py +++ b/mypyc/test/test_irbuild.py @@ -24,6 +24,7 @@ files = [ "irbuild-basic.test", "irbuild-int.test", + "irbuild-bool.test", "irbuild-lists.test", "irbuild-tuple.test", "irbuild-dict.test", From 3520c1fab422ee001c7b4fb35eb7685c71efc8fb Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 26 Dec 2022 12:03:42 +0000 Subject: [PATCH 04/13] Support more types --- mypyc/irbuild/ll_builder.py | 12 +-- mypyc/irbuild/specialize.py | 7 +- mypyc/test-data/irbuild-bool.test | 133 +++++++++++++++++++++++++++++- 3 files changed, 138 insertions(+), 14 deletions(-) diff --git a/mypyc/irbuild/ll_builder.py b/mypyc/irbuild/ll_builder.py index b7a5f4b246ab..abc4e95375d8 100644 --- a/mypyc/irbuild/ll_builder.py +++ b/mypyc/irbuild/ll_builder.py @@ -326,11 +326,13 @@ def coerce( ): # Equivalent types return src - elif is_bool_rprimitive(src_type) and is_int_rprimitive(target_type): + elif (is_bool_rprimitive(src_type) or is_bit_rprimitive(src_type)) and is_int_rprimitive(target_type): shifted = self.int_op( bool_rprimitive, src, Integer(1, bool_rprimitive), IntOp.LEFT_SHIFT ) return self.add(Extend(shifted, int_rprimitive, signed=False)) + elif (is_bool_rprimitive(src_type) or is_bit_rprimitive(src_type)) and is_fixed_width_rtype(target_type): + return self.add(Extend(src, target_type, signed=False)) else: # To go from one unboxed type to another, we go through a boxed # in-between value, for simplicity. @@ -1654,7 +1656,7 @@ def bool_value(self, value: Value) -> Value: """ if is_bool_rprimitive(value.type) or is_bit_rprimitive(value.type): result = value - if is_runtime_subtype(value.type, int_rprimitive): + elif is_runtime_subtype(value.type, int_rprimitive): zero = Integer(0, short_int_rprimitive) result = self.comparison_op(value, zero, ComparisonOp.NEQ, value.line) elif is_fixed_width_rtype(value.type): @@ -1699,11 +1701,11 @@ def bool_value(self, value: Value) -> Value: # unbox_or_cast instead of coerce because we want the # type to change even if it is a subtype. remaining = self.unbox_or_cast(value, value_type, value.line) - b = self.bool_value(remaining) - self.add(Assign(result, b)) + as_bool = self.bool_value(remaining) + self.add(Assign(result, as_bool)) self.goto(end) self.activate_block(false) - self.add(Assign(result, not_none)) + self.add(Assign(result, Integer(0, bit_rprimitive))) self.goto(end) self.activate_block(end) else: diff --git a/mypyc/irbuild/specialize.py b/mypyc/irbuild/specialize.py index 2b4672ae072d..a545695778d7 100644 --- a/mypyc/irbuild/specialize.py +++ b/mypyc/irbuild/specialize.py @@ -700,8 +700,5 @@ def translate_bool(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Value if len(expr.args) != 1 or expr.arg_kinds[0] != ARG_POS: return None arg = expr.args[0] - arg_type = builder.node_type(arg) - if is_int_rprimitive(arg_type): - src = builder.accept(arg) - return builder.builder.bool_value(src) - return None + src = builder.accept(arg) + return builder.builder.bool_value(src) diff --git a/mypyc/test-data/irbuild-bool.test b/mypyc/test-data/irbuild-bool.test index 9d4dc23f4c60..87f2e323e597 100644 --- a/mypyc/test-data/irbuild-bool.test +++ b/mypyc/test-data/irbuild-bool.test @@ -1,19 +1,144 @@ [case testBoolToAndFromInt] -def b2i(b: bool) -> int: +from mypy_extensions import i64 + +def bool_to_int(b: bool) -> int: return b -def i2b(n: int) -> bool: +def int_to_bool(n: int) -> bool: return bool(n) +def bool_to_i64(b: bool) -> i64: + return b +def i64_to_bool(n: i64) -> bool: + return bool(n) +def bit_to_int(n1: i64, n2: i64) -> int: + return bool(n1 == n2) +def bit_to_i64(n1: i64, n2: i64) -> i64: + return bool(n1 == n2) [out] -def b2i(b): +def bool_to_int(b): b, r0 :: bool r1 :: int L0: r0 = b << 1 r1 = extend r0: builtins.bool to builtins.int return r1 -def i2b(n): +def int_to_bool(n): n :: int r0 :: bit L0: r0 = n != 0 return r0 +def bool_to_i64(b): + b :: bool + r0 :: int64 +L0: + r0 = extend b: builtins.bool to int64 + return r0 +def i64_to_bool(n): + n :: int64 + r0 :: bit +L0: + r0 = n != 0 + return r0 +def bit_to_int(n1, n2): + n1, n2 :: int64 + r0 :: bit + r1 :: bool + r2 :: int +L0: + r0 = n1 == n2 + r1 = r0 << 1 + r2 = extend r1: builtins.bool to builtins.int + return r2 +def bit_to_i64(n1, n2): + n1, n2 :: int64 + r0 :: bit + r1 :: int64 +L0: + r0 = n1 == n2 + r1 = extend r0: bit to int64 + return r1 + +[case testBoolConversions] +from typing import List, Optional + +class C: pass +class D: + def __bool__(self) -> bool: + return True + +def list_to_bool(l: List[str]) -> bool: + return bool(l) + +def always_truthy_instance_to_bool(o: C) -> bool: + return bool(o) + +def instance_to_bool(o: D) -> bool: + return bool(o) + +def optional_truthy_to_bool(o: Optional[C]) -> bool: + return bool(o) + +def optional_maybe_falsey_to_bool(o: Optional[D]) -> bool: + return bool(o) +[out] +def D.__bool__(self): + self :: __main__.D +L0: + return 1 +def list_to_bool(l): + l :: list + r0 :: ptr + r1 :: native_int + r2 :: short_int + r3 :: bit +L0: + r0 = get_element_ptr l ob_size :: PyVarObject + r1 = load_mem r0 :: native_int* + keep_alive l + r2 = r1 << 1 + r3 = r2 != 0 + return r3 +def always_truthy_instance_to_bool(o): + o :: __main__.C + r0 :: int32 + r1 :: bit + r2 :: bool +L0: + r0 = PyObject_IsTrue(o) + r1 = r0 >= 0 :: signed + r2 = truncate r0: int32 to builtins.bool + return r2 +def instance_to_bool(o): + o :: __main__.D + r0 :: bool +L0: + r0 = o.__bool__() + return r0 +def optional_truthy_to_bool(o): + o :: union[__main__.C, None] + r0 :: object + r1 :: bit +L0: + r0 = load_address _Py_NoneStruct + r1 = o != r0 + return r1 +def optional_maybe_falsey_to_bool(o): + o :: union[__main__.D, None] + r0 :: object + r1 :: bit + r2 :: __main__.D + r3 :: bool + r4 :: bit +L0: + r0 = load_address _Py_NoneStruct + r1 = o != r0 + if r1 goto L1 else goto L2 :: bool +L1: + r2 = cast(__main__.D, o) + r3 = r2.__bool__() + r4 = r3 + goto L3 +L2: + r4 = 0 +L3: + return r4 From fe37a98811221938d33c396ebed899fcb5e9f419 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 26 Dec 2022 12:05:18 +0000 Subject: [PATCH 05/13] Update test case --- mypyc/test-data/irbuild-basic.test | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/mypyc/test-data/irbuild-basic.test b/mypyc/test-data/irbuild-basic.test index 16b085ad4927..1adf18c2d053 100644 --- a/mypyc/test-data/irbuild-basic.test +++ b/mypyc/test-data/irbuild-basic.test @@ -3075,8 +3075,7 @@ def call_sum(l, comparison): r1, r2 :: object r3, x :: int r4, r5 :: object - r6 :: bool - r7 :: object + r6, r7 :: bool r8, r9 :: int r10 :: bit L0: @@ -3091,8 +3090,8 @@ L2: r4 = box(int, x) r5 = PyObject_CallFunctionObjArgs(comparison, r4, 0) r6 = unbox(bool, r5) - r7 = box(bool, r6) - r8 = unbox(int, r7) + r7 = r6 << 1 + r8 = extend r7: builtins.bool to builtins.int r9 = CPyTagged_Add(r0, r8) r0 = r9 L3: From 0e1044bce366a229b78427a0b02ced7cbad97550 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 26 Dec 2022 12:05:41 +0000 Subject: [PATCH 06/13] Black --- mypyc/irbuild/ll_builder.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/mypyc/irbuild/ll_builder.py b/mypyc/irbuild/ll_builder.py index abc4e95375d8..12d4682da4e9 100644 --- a/mypyc/irbuild/ll_builder.py +++ b/mypyc/irbuild/ll_builder.py @@ -326,12 +326,16 @@ def coerce( ): # Equivalent types return src - elif (is_bool_rprimitive(src_type) or is_bit_rprimitive(src_type)) and is_int_rprimitive(target_type): + elif ( + is_bool_rprimitive(src_type) or is_bit_rprimitive(src_type) + ) and is_int_rprimitive(target_type): shifted = self.int_op( bool_rprimitive, src, Integer(1, bool_rprimitive), IntOp.LEFT_SHIFT ) return self.add(Extend(shifted, int_rprimitive, signed=False)) - elif (is_bool_rprimitive(src_type) or is_bit_rprimitive(src_type)) and is_fixed_width_rtype(target_type): + elif ( + is_bool_rprimitive(src_type) or is_bit_rprimitive(src_type) + ) and is_fixed_width_rtype(target_type): return self.add(Extend(src, target_type, signed=False)) else: # To go from one unboxed type to another, we go through a boxed From 4f3276af296aca8030918372e2feb4f95b1e547b Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 26 Dec 2022 12:11:56 +0000 Subject: [PATCH 07/13] Refactor --- mypyc/irbuild/ll_builder.py | 71 +++++++++++++------------------------ 1 file changed, 24 insertions(+), 47 deletions(-) diff --git a/mypyc/irbuild/ll_builder.py b/mypyc/irbuild/ll_builder.py index 12d4682da4e9..019f709f0acc 100644 --- a/mypyc/irbuild/ll_builder.py +++ b/mypyc/irbuild/ll_builder.py @@ -1717,54 +1717,31 @@ def bool_value(self, value: Value) -> Value: return result def add_bool_branch(self, value: Value, true: BasicBlock, false: BasicBlock) -> None: - if is_runtime_subtype(value.type, int_rprimitive): - zero = Integer(0, short_int_rprimitive) - self.compare_tagged_condition(value, zero, "!=", true, false, value.line) - return - elif is_fixed_width_rtype(value.type): - zero = Integer(0, value.type) - value = self.add(ComparisonOp(value, zero, ComparisonOp.NEQ)) - elif is_same_type(value.type, str_rprimitive): - value = self.call_c(str_check_if_true, [value], value.line) - elif is_same_type(value.type, list_rprimitive) or is_same_type( - value.type, dict_rprimitive - ): - length = self.builtin_len(value, value.line) - zero = Integer(0) - value = self.binary_op(length, zero, "!=", value.line) - elif ( - isinstance(value.type, RInstance) - and value.type.class_ir.is_ext_class - and value.type.class_ir.has_method("__bool__") - ): - # Directly call the __bool__ method on classes that have it. - value = self.gen_method_call(value, "__bool__", [], bool_rprimitive, value.line) + opt_value_type = optional_value_type(value.type) + if opt_value_type is None: + bool_value = self.bool_value(value) + self.add(Branch(bool_value, true, false, Branch.BOOL)) else: - value_type = optional_value_type(value.type) - if value_type is not None: - is_none = self.translate_is_op(value, self.none_object(), "is not", value.line) - branch = Branch(is_none, true, false, Branch.BOOL) - self.add(branch) - always_truthy = False - if isinstance(value_type, RInstance): - # check whether X.__bool__ is always just the default (object.__bool__) - if not value_type.class_ir.has_method( - "__bool__" - ) and value_type.class_ir.is_method_final("__bool__"): - always_truthy = True - - if not always_truthy: - # Optional[X] where X may be falsey and requires a check - branch.true = BasicBlock() - self.activate_block(branch.true) - # unbox_or_cast instead of coerce because we want the - # type to change even if it is a subtype. - remaining = self.unbox_or_cast(value, value_type, value.line) - self.add_bool_branch(remaining, true, false) - return - elif not is_bool_rprimitive(value.type) and not is_bit_rprimitive(value.type): - value = self.call_c(bool_op, [value], value.line) - self.add(Branch(value, true, false, Branch.BOOL)) + # Special-case optional types + is_none = self.translate_is_op(value, self.none_object(), "is not", value.line) + branch = Branch(is_none, true, false, Branch.BOOL) + self.add(branch) + always_truthy = False + if isinstance(opt_value_type, RInstance): + # check whether X.__bool__ is always just the default (object.__bool__) + if not opt_value_type.class_ir.has_method( + "__bool__" + ) and opt_value_type.class_ir.is_method_final("__bool__"): + always_truthy = True + + if not always_truthy: + # Optional[X] where X may be falsey and requires a check + branch.true = BasicBlock() + self.activate_block(branch.true) + # unbox_or_cast instead of coerce because we want the + # type to change even if it is a subtype. + remaining = self.unbox_or_cast(value, opt_value_type, value.line) + self.add_bool_branch(remaining, true, false) def call_c( self, From c37a320972693b6574250662308d178141497999 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 26 Dec 2022 12:14:50 +0000 Subject: [PATCH 08/13] Add run tests --- mypyc/test-data/run-bools.test | 57 ++++++++++++++++++++++++++++---- mypyc/test-data/run-i64.test | 31 +++++++++++++++++ mypyc/test-data/run-strings.test | 5 +++ 3 files changed, 86 insertions(+), 7 deletions(-) diff --git a/mypyc/test-data/run-bools.test b/mypyc/test-data/run-bools.test index a7afc5f2b1a2..e23b35d82fc5 100644 --- a/mypyc/test-data/run-bools.test +++ b/mypyc/test-data/run-bools.test @@ -15,6 +15,8 @@ True False [case testBoolOps] +from typing import Optional, Any + def f(x: bool) -> bool: if x: return False @@ -27,8 +29,8 @@ def test_if() -> None: def test_bitwise_and() -> None: # Use eval() to avoid constand folding - t = eval('True') # type: bool - f = eval('False') # type: bool + t: bool = eval('True') + f: bool = eval('False') assert t & t == True assert t & f == False assert f & t == False @@ -40,8 +42,8 @@ def test_bitwise_and() -> None: def test_bitwise_or() -> None: # Use eval() to avoid constand folding - t = eval('True') # type: bool - f = eval('False') # type: bool + t: bool = eval('True') + f: bool = eval('False') assert t | t == True assert t | f == True assert f | t == True @@ -53,8 +55,8 @@ def test_bitwise_or() -> None: def test_bitwise_xor() -> None: # Use eval() to avoid constand folding - t = eval('True') # type: bool - f = eval('False') # type: bool + t: bool = eval('True') + f: bool = eval('False') assert t ^ t == False assert t ^ f == True assert f ^ t == True @@ -66,7 +68,6 @@ def test_bitwise_xor() -> None: f ^= f assert f == False -[case testIsinstanceBool] def test_isinstance_bool() -> None: a = True b = 1.0 @@ -76,3 +77,45 @@ def test_isinstance_bool() -> None: assert isinstance(b, bool) == False assert isinstance(c, bool) == False assert isinstance(d, bool) == True + +class C: pass +class D: + def __init__(self, b: bool) -> None: + self.b = b + + def __bool__(self) -> bool: + return self.b + +class E: pass +class F(E): + def __init__(self, b: bool) -> None: + self.b = b + + def __bool__(self) -> bool: + return self.b + +def optional_to_bool1(o: Optional[C]) -> bool: + return bool(o) + +def optional_to_bool2(o: Optional[D]) -> bool: + return bool(o) + +def optional_to_bool3(o: Optional[E]) -> bool: + return bool(o) + +def test_optional_to_bool() -> None: + assert not optional_to_bool1(None) + assert optional_to_bool1(C()) + assert not optional_to_bool2(None) + assert not optional_to_bool2(D(False)) + assert optional_to_bool2(D(True)) + assert not optional_to_bool3(None) + assert optional_to_bool3(E()) + assert not optional_to_bool3(F(False)) + assert optional_to_bool3(F(True)) + +def test_any_to_bool() -> None: + a: Any = int() + b: Any = a + 1 + assert not bool(a) + assert bool(b) diff --git a/mypyc/test-data/run-i64.test b/mypyc/test-data/run-i64.test index 893b3f808f24..103de3601928 100644 --- a/mypyc/test-data/run-i64.test +++ b/mypyc/test-data/run-i64.test @@ -62,6 +62,37 @@ def test_comparisons() -> None: assert one != two assert not (one != one2) +def is_true(x: i64) -> bool: + if x: + return True + else: + return False + +def is_true2(x: i64) -> bool: + return bool(x) + +def is_false(x: i64) -> bool: + if not x: + return True + else: + return False + +def test_i64_as_bool() -> None: + assert not is_true(0) + assert not is_true2(0) + assert is_false(0) + for x in 1, 55, -1, -7, 1 << 40, -(1 << 50): + assert is_true(x) + assert is_true2(x) + assert not is_false(x) + +def bool_as_i64(b: bool) -> i64: + return b + +def test_bool_as_i64() -> None: + assert bool_as_i64(False) == 0 + assert bool_as_i64(True) == 1 + def div_by_3(x: i64) -> i64: return x // 3 diff --git a/mypyc/test-data/run-strings.test b/mypyc/test-data/run-strings.test index c2b010bdb2bd..4a20c13ce789 100644 --- a/mypyc/test-data/run-strings.test +++ b/mypyc/test-data/run-strings.test @@ -136,6 +136,9 @@ def is_true(x: str) -> bool: else: return False +def is_true2(x: str) -> bool: + return bool(x) + def is_false(x: str) -> bool: if not x: return True @@ -145,8 +148,10 @@ def is_false(x: str) -> bool: def test_str_to_bool() -> None: assert is_false('') assert not is_true('') + assert not is_true2('') for x in 'a', 'foo', 'bar', 'some string': assert is_true(x) + assert is_true2(x) assert not is_false(x) def test_str_min_max() -> None: From 6195208d23578d73aaf70a7bed98d95e3b28faa9 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 26 Dec 2022 12:24:14 +0000 Subject: [PATCH 09/13] Fix lint --- mypyc/irbuild/specialize.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/mypyc/irbuild/specialize.py b/mypyc/irbuild/specialize.py index a545695778d7..72d573ffc525 100644 --- a/mypyc/irbuild/specialize.py +++ b/mypyc/irbuild/specialize.py @@ -33,12 +33,8 @@ ) from mypy.types import AnyType, TypeOfAny from mypyc.ir.ops import ( - Assign, BasicBlock, - Branch, - ComparisonOp, Extend, - Goto, Integer, RaiseStandardError, Register, @@ -55,7 +51,6 @@ dict_rprimitive, int32_rprimitive, int64_rprimitive, - int_rprimitive, is_dict_rprimitive, is_int32_rprimitive, is_int64_rprimitive, From 4e4b9e8ee90ed0ef4a3c8410d9477747903a0677 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 26 Dec 2022 12:34:47 +0000 Subject: [PATCH 10/13] Specialize conversion of int/bool to int --- mypyc/irbuild/specialize.py | 14 ++++++++++++++ mypyc/test-data/irbuild-bool.test | 2 +- mypyc/test-data/irbuild-int.test | 19 +++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/mypyc/irbuild/specialize.py b/mypyc/irbuild/specialize.py index 72d573ffc525..42aa4e0f2b84 100644 --- a/mypyc/irbuild/specialize.py +++ b/mypyc/irbuild/specialize.py @@ -51,6 +51,8 @@ dict_rprimitive, int32_rprimitive, int64_rprimitive, + int_rprimitive, + is_bool_rprimitive, is_dict_rprimitive, is_int32_rprimitive, is_int64_rprimitive, @@ -690,6 +692,18 @@ def translate_i32(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Value return None +@specialize_function("builtins.int") +def translate_bool(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Value | None: + if len(expr.args) != 1 or expr.arg_kinds[0] != ARG_POS: + return None + arg = expr.args[0] + arg_type = builder.node_type(arg) + if is_bool_rprimitive(arg_type) or is_int_rprimitive(arg_type): + src = builder.accept(arg) + return builder.coerce(src, int_rprimitive, expr.line) + return None + + @specialize_function("builtins.bool") def translate_bool(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Value | None: if len(expr.args) != 1 or expr.arg_kinds[0] != ARG_POS: diff --git a/mypyc/test-data/irbuild-bool.test b/mypyc/test-data/irbuild-bool.test index 87f2e323e597..407ab8bcda93 100644 --- a/mypyc/test-data/irbuild-bool.test +++ b/mypyc/test-data/irbuild-bool.test @@ -58,7 +58,7 @@ L0: r1 = extend r0: bit to int64 return r1 -[case testBoolConversions] +[case testConversionToBool] from typing import List, Optional class C: pass diff --git a/mypyc/test-data/irbuild-int.test b/mypyc/test-data/irbuild-int.test index e193c16ef979..aebadce5650e 100644 --- a/mypyc/test-data/irbuild-int.test +++ b/mypyc/test-data/irbuild-int.test @@ -203,3 +203,22 @@ L0: def f5(): L0: return -2 + +[case testConvertIntegralToInt] +def bool_to_int(b: bool) -> int: + return int(b) + +def int_to_int(n: int) -> int: + return int(n) +[out] +def bool_to_int(b): + b, r0 :: bool + r1 :: int +L0: + r0 = b << 1 + r1 = extend r0: builtins.bool to builtins.int + return r1 +def int_to_int(n): + n :: int +L0: + return n From 04a639608cfa16ad6edf74daa391cea2fded3872 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 26 Dec 2022 12:42:48 +0000 Subject: [PATCH 11/13] Better explicit coercions from native int to int --- mypyc/irbuild/specialize.py | 9 +++++++-- mypyc/test-data/irbuild-i64.test | 26 ++++++++++++++++++++++++++ mypyc/test-data/run-i64.test | 10 ++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/mypyc/irbuild/specialize.py b/mypyc/irbuild/specialize.py index 42aa4e0f2b84..06babd2f7e1a 100644 --- a/mypyc/irbuild/specialize.py +++ b/mypyc/irbuild/specialize.py @@ -54,6 +54,7 @@ int_rprimitive, is_bool_rprimitive, is_dict_rprimitive, + is_fixed_width_rtype, is_int32_rprimitive, is_int64_rprimitive, is_int_rprimitive, @@ -693,12 +694,16 @@ def translate_i32(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Value @specialize_function("builtins.int") -def translate_bool(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Value | None: +def translate_int(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Value | None: if len(expr.args) != 1 or expr.arg_kinds[0] != ARG_POS: return None arg = expr.args[0] arg_type = builder.node_type(arg) - if is_bool_rprimitive(arg_type) or is_int_rprimitive(arg_type): + if ( + is_bool_rprimitive(arg_type) + or is_int_rprimitive(arg_type) + or is_fixed_width_rtype(arg_type) + ): src = builder.accept(arg) return builder.coerce(src, int_rprimitive, expr.line) return None diff --git a/mypyc/test-data/irbuild-i64.test b/mypyc/test-data/irbuild-i64.test index 9c942ea75219..c6b62996bc80 100644 --- a/mypyc/test-data/irbuild-i64.test +++ b/mypyc/test-data/irbuild-i64.test @@ -1559,6 +1559,32 @@ L2: L3: return r3 +[case testI64ExplicitConversionToInt_64bit] +from mypy_extensions import i64 + +def f(x: i64) -> int: + return int(x) +[out] +def f(x): + x :: int64 + r0, r1 :: bit + r2, r3, r4 :: int +L0: + r0 = x <= 4611686018427387903 :: signed + if r0 goto L1 else goto L2 :: bool +L1: + r1 = x >= -4611686018427387904 :: signed + if r1 goto L3 else goto L2 :: bool +L2: + r2 = CPyTagged_FromInt64(x) + r3 = r2 + goto L4 +L3: + r4 = x << 1 + r3 = r4 +L4: + return r3 + [case testI64ExplicitConversionFromLiteral] from mypy_extensions import i64 diff --git a/mypyc/test-data/run-i64.test b/mypyc/test-data/run-i64.test index 103de3601928..4436c134bf08 100644 --- a/mypyc/test-data/run-i64.test +++ b/mypyc/test-data/run-i64.test @@ -260,6 +260,16 @@ def test_coerce_to_and_from_int() -> None: m: int = x assert m == n +def test_coerce_to_and_from_int2() -> None: + for shift in range(0, 64): + for sign in 1, -1: + for delta in range(-5, 5): + n = sign * (1 << shift) + delta + if -(1 << 63) <= n < (1 << 63): + x: i64 = i64(n) + m: int = int(x) + assert m == n + def test_explicit_conversion_to_i64() -> None: x = i64(5) assert x == 5 From 8549b3ce28e0c61681e2ad081b585766fbf20245 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 26 Dec 2022 12:45:09 +0000 Subject: [PATCH 12/13] Add run tests --- mypyc/test-data/run-integers.test | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/mypyc/test-data/run-integers.test b/mypyc/test-data/run-integers.test index 945773ff5add..c65f36110b46 100644 --- a/mypyc/test-data/run-integers.test +++ b/mypyc/test-data/run-integers.test @@ -374,9 +374,21 @@ def test_int_as_bool() -> None: def bool_as_int(b: bool) -> int: return b +def bool_as_int2(b: bool) -> int: + return int(b) + def test_bool_as_int() -> None: assert bool_as_int(False) == 0 assert bool_as_int(True) == 1 + assert bool_as_int2(False) == 0 + assert bool_as_int2(True) == 1 + +def no_op_conversion(n: int) -> int: + return int(n) + +def test_no_op_conversion() -> None: + for x in 1, 55, -1, -7, 1 << 50, 1 << 101, -(1 << 50), -(1 << 101): + assert no_op_conversion(x) == x def test_divide() -> None: for x in range(-100, 100): From c95d5936ec2679cb2c6bce7660b43d1068892fe3 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 26 Dec 2022 12:57:29 +0000 Subject: [PATCH 13/13] Fix test case --- mypyc/test-data/irbuild-basic.test | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/mypyc/test-data/irbuild-basic.test b/mypyc/test-data/irbuild-basic.test index 1adf18c2d053..77085c7d670e 100644 --- a/mypyc/test-data/irbuild-basic.test +++ b/mypyc/test-data/irbuild-basic.test @@ -1108,7 +1108,9 @@ L0: return 1 [case testCallableTypes] -from typing import Callable +from typing import Callable, Any +from m import f + def absolute_value(x: int) -> int: return x if x > 0 else -x @@ -1116,7 +1118,7 @@ def call_native_function(x: int) -> int: return absolute_value(x) def call_python_function(x: int) -> int: - return int(x) + return f(x) def return_float() -> float: return 5.0 @@ -1127,6 +1129,9 @@ def return_callable_type() -> Callable[[], float]: def call_callable_type() -> float: f = return_callable_type() return f() +[file m.py] +def f(x: int) -> int: + return x [out] def absolute_value(x): x :: int @@ -1158,14 +1163,18 @@ L0: return r0 def call_python_function(x): x :: int - r0, r1, r2 :: object - r3 :: int + r0 :: dict + r1 :: str + r2, r3, r4 :: object + r5 :: int L0: - r0 = load_address PyLong_Type - r1 = box(int, x) - r2 = PyObject_CallFunctionObjArgs(r0, r1, 0) - r3 = unbox(int, r2) - return r3 + r0 = __main__.globals :: static + r1 = 'f' + r2 = CPyDict_GetItem(r0, r1) + r3 = box(int, x) + r4 = PyObject_CallFunctionObjArgs(r2, r3, 0) + r5 = unbox(int, r4) + return r5 def return_float(): r0 :: float L0: