From 5620f6832d8d1a9ba171bf4a04034f5917ba1315 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 28 Dec 2021 16:10:57 +0000 Subject: [PATCH 01/56] [mypyc] Use an unboxed representation for floats Instead of each float value being a heap-allocated Python object, use unboxed C doubles to represent floats. This makes float operations much faster, and this also significantly reduces memory use of floats (when not stored in Python containers, which always use a boxed representation). Update IR to support float arithmetic and comparison ops, and float literals. Also add a few primitives corresponding to common math functions, such as `math.sqrt`. These don't require any boxing or unboxing. (I will add more of these in follow-up PRs.) Use -113.0 as an overlapping error value for floats. This is similar to native ints. Reuse much of the infrastructure we have to support overlapping error values with native ints (e.g. various bitmaps). Also improve support for negative float literals. There are two backward compatibility breaks worth highlighting. First, assigning an int value to a float variable is disallowed within mypyc, since narrowing down to a different value representation is inefficient and can lose precision. Second, information about float subclasses is lost during unboxing. This makes the bm_float benchmark about 5x faster and the raytrace benchmark about 3x faster. Closes mypyc/mypyc#966 (I'll create separate issues for remaining open issues). --- mypyc/analysis/dataflow.py | 13 +- mypyc/analysis/ircheck.py | 8 + mypyc/analysis/selfleaks.py | 8 + mypyc/codegen/emit.py | 12 ++ mypyc/codegen/emitfunc.py | 22 +++ mypyc/common.py | 1 + mypyc/ir/ops.py | 94 +++++++++- mypyc/ir/pprint.py | 13 +- mypyc/ir/rtypes.py | 13 +- mypyc/irbuild/constant_fold.py | 28 ++- mypyc/irbuild/expression.py | 6 + mypyc/irbuild/function.py | 2 +- mypyc/irbuild/ll_builder.py | 47 ++++- mypyc/lib-rt/CPy.h | 11 +- mypyc/lib-rt/float_ops.c | 27 +++ mypyc/lib-rt/int_ops.c | 13 +- mypyc/lib-rt/mypyc_util.h | 3 + mypyc/lib-rt/setup.py | 10 +- mypyc/primitives/float_ops.py | 46 ++++- mypyc/primitives/int_ops.py | 4 +- mypyc/test-data/exceptions.test | 72 ++++++++ mypyc/test-data/fixtures/ir.py | 8 + mypyc/test-data/irbuild-basic.test | 42 ++--- mypyc/test-data/irbuild-constant-fold.test | 6 +- mypyc/test-data/irbuild-dunders.test | 5 +- mypyc/test-data/irbuild-float.test | 202 +++++++++++++++++++++ mypyc/test-data/run-floats.test | 158 +++++++++++++++- mypyc/test/test_irbuild.py | 1 + mypyc/transform/exceptions.py | 9 +- test-data/unit/lib-stub/math.pyi | 1 + 30 files changed, 811 insertions(+), 74 deletions(-) create mode 100644 mypyc/lib-rt/float_ops.c create mode 100644 mypyc/test-data/irbuild-float.test create mode 100644 test-data/unit/lib-stub/math.pyi diff --git a/mypyc/analysis/dataflow.py b/mypyc/analysis/dataflow.py index 21c4da8981d10..9b964d54607a1 100644 --- a/mypyc/analysis/dataflow.py +++ b/mypyc/analysis/dataflow.py @@ -18,6 +18,9 @@ ComparisonOp, ControlOp, Extend, + Float, + FloatComparisonOp, + FloatOp, GetAttr, GetElementPtr, Goto, @@ -245,9 +248,15 @@ def visit_load_global(self, op: LoadGlobal) -> GenAndKill[T]: def visit_int_op(self, op: IntOp) -> GenAndKill[T]: return self.visit_register_op(op) + def visit_float_op(self, op: FloatOp) -> GenAndKill[T]: + return self.visit_register_op(op) + def visit_comparison_op(self, op: ComparisonOp) -> GenAndKill[T]: return self.visit_register_op(op) + def visit_float_comparison_op(self, op: FloatComparisonOp) -> GenAndKill[T]: + return self.visit_register_op(op) + def visit_load_mem(self, op: LoadMem) -> GenAndKill[T]: return self.visit_register_op(op) @@ -444,7 +453,7 @@ def analyze_undefined_regs( def non_trivial_sources(op: Op) -> set[Value]: result = set() for source in op.sources(): - if not isinstance(source, Integer): + if not isinstance(source, (Integer, Float)): result.add(source) return result @@ -454,7 +463,7 @@ def visit_branch(self, op: Branch) -> GenAndKill[Value]: return non_trivial_sources(op), set() def visit_return(self, op: Return) -> GenAndKill[Value]: - if not isinstance(op.value, Integer): + if not isinstance(op.value, (Integer, Float)): return {op.value}, set() else: return set(), set() diff --git a/mypyc/analysis/ircheck.py b/mypyc/analysis/ircheck.py index 719faebfcee8c..5e4e993641ef8 100644 --- a/mypyc/analysis/ircheck.py +++ b/mypyc/analysis/ircheck.py @@ -16,6 +16,8 @@ ControlOp, DecRef, Extend, + FloatComparisonOp, + FloatOp, GetAttr, GetElementPtr, Goto, @@ -381,6 +383,12 @@ def visit_int_op(self, op: IntOp) -> None: def visit_comparison_op(self, op: ComparisonOp) -> None: self.check_compatibility(op, op.lhs.type, op.rhs.type) + def visit_float_op(self, op: FloatOp) -> None: + pass + + def visit_float_comparison_op(self, op: FloatComparisonOp) -> None: + pass + def visit_load_mem(self, op: LoadMem) -> None: pass diff --git a/mypyc/analysis/selfleaks.py b/mypyc/analysis/selfleaks.py index 16c1050acf917..e5874e9c29950 100644 --- a/mypyc/analysis/selfleaks.py +++ b/mypyc/analysis/selfleaks.py @@ -14,6 +14,8 @@ Cast, ComparisonOp, Extend, + FloatComparisonOp, + FloatOp, GetAttr, GetElementPtr, Goto, @@ -160,6 +162,12 @@ def visit_int_op(self, op: IntOp) -> GenAndKill: def visit_comparison_op(self, op: ComparisonOp) -> GenAndKill: return CLEAN + def visit_float_op(self, op: FloatOp) -> GenAndKill: + return CLEAN + + def visit_float_comparison_op(self, op: FloatComparisonOp) -> GenAndKill: + return CLEAN + def visit_load_mem(self, op: LoadMem) -> GenAndKill: return CLEAN diff --git a/mypyc/codegen/emit.py b/mypyc/codegen/emit.py index 6e0c89dd0ecf3..0f1f5ad071ada 100644 --- a/mypyc/codegen/emit.py +++ b/mypyc/codegen/emit.py @@ -895,6 +895,16 @@ def emit_unbox( self.emit_line(f"{dest} = CPyLong_AsInt32({src});") # TODO: Handle 'optional' # TODO: Handle 'failure' + elif is_float_rprimitive(typ): + if declare_dest: + self.emit_line("double {};".format(dest)) + # TODO: Don't use __float__ and __index__ + self.emit_line(f"{dest} = PyFloat_AsDouble({src});") + self.emit_lines( + f"if ({dest} == -1.0 && PyErr_Occurred()) {{", f"{dest} = -113.0;", "}" + ) + # TODO: Handle 'optional' + # TODO: Handle 'failure' elif isinstance(typ, RTuple): self.declare_tuple_struct(typ) if declare_dest: @@ -983,6 +993,8 @@ def emit_box( self.emit_line(f"{declaration}{dest} = PyLong_FromLong({src});") elif is_int64_rprimitive(typ): self.emit_line(f"{declaration}{dest} = PyLong_FromLongLong({src});") + elif is_float_rprimitive(typ): + self.emit_line(f"{declaration}{dest} = PyFloat_FromDouble({src});") elif isinstance(typ, RTuple): self.declare_tuple_struct(typ) self.emit_line(f"{declaration}{dest} = PyTuple_New({len(typ.types)});") diff --git a/mypyc/codegen/emitfunc.py b/mypyc/codegen/emitfunc.py index e7fb7db80413d..873f574922393 100644 --- a/mypyc/codegen/emitfunc.py +++ b/mypyc/codegen/emitfunc.py @@ -25,6 +25,9 @@ ComparisonOp, DecRef, Extend, + Float, + FloatComparisonOp, + FloatOp, GetAttr, GetElementPtr, Goto, @@ -671,6 +674,18 @@ def visit_comparison_op(self, op: ComparisonOp) -> None: lhs_cast = self.emit_signed_int_cast(op.lhs.type) self.emit_line(f"{dest} = {lhs_cast}{lhs} {op.op_str[op.op]} {rhs_cast}{rhs};") + def visit_float_op(self, op: FloatOp) -> None: + dest = self.reg(op) + lhs = self.reg(op.lhs) + rhs = self.reg(op.rhs) + self.emit_line("%s = %s %s %s;" % (dest, lhs, op.op_str[op.op], rhs)) + + def visit_float_comparison_op(self, op: FloatComparisonOp) -> None: + dest = self.reg(op) + lhs = self.reg(op.lhs) + rhs = self.reg(op.rhs) + self.emit_line("%s = %s %s %s;" % (dest, lhs, op.op_str[op.op], rhs)) + def visit_load_mem(self, op: LoadMem) -> None: dest = self.reg(op) src = self.reg(op.src) @@ -732,6 +747,13 @@ def reg(self, reg: Value) -> str: elif val <= -(1 << 31): s += "LL" return s + elif isinstance(reg, Float): + r = repr(reg.value) + if r == "inf": + return "INFINITY" + elif r == "-inf": + return "-INFINITY" + return r else: return self.emitter.reg(reg) diff --git a/mypyc/common.py b/mypyc/common.py index c8da5ff63bab1..05e13370cb98c 100644 --- a/mypyc/common.py +++ b/mypyc/common.py @@ -69,6 +69,7 @@ "getargs.c", "getargsfast.c", "int_ops.c", + "float_ops.c", "str_ops.c", "bytes_ops.c", "list_ops.c", diff --git a/mypyc/ir/ops.py b/mypyc/ir/ops.py index 51a0bffcf3f17..e1b22f9ab6023 100644 --- a/mypyc/ir/ops.py +++ b/mypyc/ir/ops.py @@ -25,6 +25,7 @@ RVoid, bit_rprimitive, bool_rprimitive, + float_rprimitive, int_rprimitive, is_bit_rprimitive, is_bool_rprimitive, @@ -190,6 +191,25 @@ def __init__(self, value: int, rtype: RType = short_int_rprimitive, line: int = self.type = rtype self.line = line + def numeric_value(self) -> int: + if is_short_int_rprimitive(self.type) or is_int_rprimitive(self.type): + return self.value // 2 + return self.value + + +class Float(Value): + """Float literal. + + Floating point literals are treated as constant values and are generally + not included in data flow analyses and such, unlike Register and + Op subclasses. + """ + + def __init__(self, value: float, line: int = -1) -> None: + self.value = value + self.type = float_rprimitive + self.line = line + class Op(Value): """Abstract base class for all IR operations. @@ -1042,7 +1062,7 @@ class IntOp(RegisterOp): """Binary arithmetic or bitwise op on integer operands (e.g., r1 = r2 + r3). These ops are low-level and are similar to the corresponding C - operations (and unlike Python operations). + operations. The left and right values must have low-level integer types with compatible representations. Fixed-width integers, short_int_rprimitive, @@ -1156,6 +1176,70 @@ def accept(self, visitor: OpVisitor[T]) -> T: return visitor.visit_comparison_op(self) +class FloatOp(RegisterOp): + """Binary float arithmetic op (e.g., r1 = r2 + r3). + + These ops are low-level and are similar to the corresponding C + operations (and unlike Python operations). + + The left and right values must be floats. + """ + + error_kind = ERR_NEVER + + ADD: Final = 0 + SUB: Final = 1 + MUL: Final = 2 + DIV: Final = 3 + + op_str: Final = {ADD: "+", SUB: "-", MUL: "*", DIV: "/"} + + op_to_id = {op: op_id for op_id, op in op_str.items()} # type: Final + + def __init__(self, lhs: Value, rhs: Value, op: int, line: int = -1) -> None: + super().__init__(line) + self.type = float_rprimitive + self.lhs = lhs + self.rhs = rhs + self.op = op + + def sources(self) -> List[Value]: + return [self.lhs, self.rhs] + + def accept(self, visitor: "OpVisitor[T]") -> T: + return visitor.visit_float_op(self) + + +class FloatComparisonOp(RegisterOp): + """Low-level comparison op for floats.""" + + error_kind = ERR_NEVER + + EQ: Final = 200 + NEQ: Final = 201 + LT: Final = 202 + GT: Final = 203 + LE: Final = 204 + GE: Final = 205 + + op_str: Final = {EQ: "==", NEQ: "!=", LT: "<", GT: ">", LE: "<=", GE: ">="} + + op_to_id: Final = {op: op_id for op_id, op in op_str.items()} + + def __init__(self, lhs: Value, rhs: Value, op: int, line: int = -1) -> None: + super().__init__(line) + self.type = bit_rprimitive + self.lhs = lhs + self.rhs = rhs + self.op = op + + def sources(self) -> List[Value]: + return [self.lhs, self.rhs] + + def accept(self, visitor: "OpVisitor[T]") -> T: + return visitor.visit_float_comparison_op(self) + + class LoadMem(RegisterOp): """Read a memory location: result = *(type *)src. @@ -1405,6 +1489,14 @@ def visit_int_op(self, op: IntOp) -> T: def visit_comparison_op(self, op: ComparisonOp) -> T: raise NotImplementedError + @abstractmethod + def visit_float_op(self, op: FloatOp) -> T: + raise NotImplementedError + + @abstractmethod + def visit_float_comparison_op(self, op: FloatComparisonOp) -> T: + raise NotImplementedError + @abstractmethod def visit_load_mem(self, op: LoadMem) -> T: raise NotImplementedError diff --git a/mypyc/ir/pprint.py b/mypyc/ir/pprint.py index cb9e4a2d25418..639a59bd0c892 100644 --- a/mypyc/ir/pprint.py +++ b/mypyc/ir/pprint.py @@ -23,6 +23,9 @@ ControlOp, DecRef, Extend, + Float, + FloatComparisonOp, + FloatOp, GetAttr, GetElementPtr, Goto, @@ -241,6 +244,12 @@ def visit_comparison_op(self, op: ComparisonOp) -> str: "%r = %r %s %r%s", op, op.lhs, ComparisonOp.op_str[op.op], op.rhs, sign_format ) + def visit_float_op(self, op: FloatOp) -> str: + return self.format("%r = %r %s %r", op, op.lhs, FloatOp.op_str[op.op], op.rhs) + + def visit_float_comparison_op(self, op: FloatComparisonOp) -> str: + return self.format("%r = %r %s %r", op, op.lhs, op.op_str[op.op], op.rhs) + def visit_load_mem(self, op: LoadMem) -> str: return self.format("%r = load_mem %r :: %t*", op, op.src, op.type) @@ -289,6 +298,8 @@ def format(self, fmt: str, *args: Any) -> str: assert isinstance(arg, Value) if isinstance(arg, Integer): result.append(str(arg.value)) + elif isinstance(arg, Float): + result.append(repr(arg.value)) else: result.append(self.names[arg]) elif typespec == "d": @@ -445,7 +456,7 @@ def generate_names_for_ir(args: list[Register], blocks: list[BasicBlock]) -> dic continue if isinstance(value, Register) and value.name: name = value.name - elif isinstance(value, Integer): + elif isinstance(value, (Integer, Float)): continue else: name = "r%d" % temp_index diff --git a/mypyc/ir/rtypes.py b/mypyc/ir/rtypes.py index babfe0770f35e..4ccab56ef8328 100644 --- a/mypyc/ir/rtypes.py +++ b/mypyc/ir/rtypes.py @@ -221,6 +221,8 @@ def __init__( self.c_undefined = "2" elif ctype in ("PyObject **", "void *"): self.c_undefined = "NULL" + elif ctype == "double": + self.c_undefined = "-113.0" else: assert False, "Unrecognized ctype: %r" % ctype @@ -366,7 +368,14 @@ def __hash__(self) -> int: # 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) +float_rprimitive: Final = RPrimitive( + "builtins.float", + is_unboxed=True, + is_refcounted=False, + ctype="double", + size=8, + error_overlap=True, +) # An unboxed Python bool value. This actually has three possible values # (0 -> False, 1 -> True, 2 -> error). If you only need True/False, use @@ -527,6 +536,8 @@ def visit_rprimitive(self, t: RPrimitive) -> str: return "8" # "8 byte integer" elif t._ctype == "int32_t": return "4" # "4 byte integer" + elif t._ctype == "double": + return "F" assert not t.is_unboxed, f"{t} unexpected unboxed type" return "O" diff --git a/mypyc/irbuild/constant_fold.py b/mypyc/irbuild/constant_fold.py index 4e9eb53b9222f..8ce99182ed24e 100644 --- a/mypyc/irbuild/constant_fold.py +++ b/mypyc/irbuild/constant_fold.py @@ -18,12 +18,22 @@ constant_fold_binary_str_op, constant_fold_unary_int_op, ) -from mypy.nodes import Expression, IntExpr, MemberExpr, NameExpr, OpExpr, StrExpr, UnaryExpr, Var +from mypy.nodes import ( + Expression, + FloatExpr, + IntExpr, + MemberExpr, + NameExpr, + OpExpr, + StrExpr, + UnaryExpr, + Var, +) from mypyc.irbuild.builder import IRBuilder # All possible result types of constant folding -ConstantValue = Union[int, str] -CONST_TYPES: Final = (int, str) +ConstantValue = Union[int, str, float] +CONST_TYPES: Final = (int, str, float) def constant_fold_expr(builder: IRBuilder, expr: Expression) -> ConstantValue | None: @@ -35,6 +45,8 @@ def constant_fold_expr(builder: IRBuilder, expr: Expression) -> ConstantValue | return expr.value if isinstance(expr, StrExpr): return expr.value + if isinstance(expr, FloatExpr): + return expr.value elif isinstance(expr, NameExpr): node = expr.node if isinstance(node, Var) and node.is_final: @@ -60,4 +72,14 @@ def constant_fold_expr(builder: IRBuilder, expr: Expression) -> ConstantValue | value = constant_fold_expr(builder, expr.expr) if isinstance(value, int): return constant_fold_unary_int_op(expr.op, value) + if isinstance(value, float): + return constant_fold_unary_float_op(expr.op, value) + return None + + +def constant_fold_unary_float_op(op: str, value: float) -> Optional[float]: + if op == "-": + return -value + elif op == "+": + return value return None diff --git a/mypyc/irbuild/expression.py b/mypyc/irbuild/expression.py index 5997bdbd0a432..23a9838a61d66 100644 --- a/mypyc/irbuild/expression.py +++ b/mypyc/irbuild/expression.py @@ -54,6 +54,7 @@ Assign, BasicBlock, ComparisonOp, + Float, Integer, LoadAddress, LoadLiteral, @@ -289,6 +290,9 @@ def transform_call_expr(builder: IRBuilder, expr: CallExpr) -> Value: callee = callee.analyzed.expr # Unwrap type application if isinstance(callee, MemberExpr): + if isinstance(callee.expr, RefExpr) and isinstance(callee.expr.node, MypyFile): + # Call a module-level function, not a method. + return translate_call(builder, expr, callee) return apply_method_specialization(builder, expr, callee) or translate_method_call( builder, expr, callee ) @@ -566,6 +570,8 @@ def try_constant_fold(builder: IRBuilder, expr: Expression) -> Value | None: return builder.load_int(value) elif isinstance(value, str): return builder.load_str(value) + elif isinstance(value, float): + return Float(value) return None diff --git a/mypyc/irbuild/function.py b/mypyc/irbuild/function.py index 02155d70e928e..ba2e4d2ba10bf 100644 --- a/mypyc/irbuild/function.py +++ b/mypyc/irbuild/function.py @@ -643,7 +643,7 @@ def f(builder: IRBuilder, x: object) -> int: ... args = args[: -base_sig.num_bitmap_args] arg_kinds = arg_kinds[: -base_sig.num_bitmap_args] arg_names = arg_names[: -base_sig.num_bitmap_args] - bitmap_args = builder.builder.args[-base_sig.num_bitmap_args :] + bitmap_args = list(builder.builder.args[-base_sig.num_bitmap_args :]) # We can do a passthrough *args/**kwargs with a native call, but if the # args need to get distributed out to arguments, we just let python handle it diff --git a/mypyc/irbuild/ll_builder.py b/mypyc/irbuild/ll_builder.py index 2391ccc4d0ed4..0a6c0360e5bce 100644 --- a/mypyc/irbuild/ll_builder.py +++ b/mypyc/irbuild/ll_builder.py @@ -46,6 +46,9 @@ Cast, ComparisonOp, Extend, + Float, + FloatComparisonOp, + FloatOp, GetAttr, GetElementPtr, Goto, @@ -89,13 +92,13 @@ c_pyssize_t_rprimitive, c_size_t_rprimitive, dict_rprimitive, - float_rprimitive, int_rprimitive, is_bit_rprimitive, is_bool_rprimitive, is_bytes_rprimitive, is_dict_rprimitive, is_fixed_width_rtype, + is_float_rprimitive, is_int32_rprimitive, is_int64_rprimitive, is_int_rprimitive, @@ -126,6 +129,7 @@ dict_update_in_display_op, ) from mypyc.primitives.exc_ops import err_occurred_op, keep_propagating_op +from mypyc.primitives.float_ops import int_to_float_op from mypyc.primitives.generic_ops import ( generic_len_op, generic_ssize_t_len_op, @@ -340,6 +344,12 @@ def coerce( 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)) + elif isinstance(src, Integer) and is_float_rprimitive(target_type): + if is_tagged(src_type): + return Float(float(src.value // 2)) + return Float(float(src.value)) + elif is_tagged(src_type) and is_float_rprimitive(target_type): + return self.call_c(int_to_float_op, [src], line) else: # To go from one unboxed type to another, we go through a boxed # in-between value, for simplicity. @@ -1166,7 +1176,7 @@ def load_int(self, value: int) -> Value: def load_float(self, value: float) -> Value: """Load a float literal value.""" - return self.add(LoadLiteral(value, float_rprimitive)) + return Float(value) def load_str(self, value: str) -> Value: """Load a str literal value. @@ -1328,6 +1338,20 @@ def binary_op(self, lreg: Value, rreg: Value, op: str, line: int) -> Value: if is_tagged(rtype) and is_subtype(ltype, rtype): lreg = self.coerce(lreg, short_int_rprimitive, line) return self.compare_tagged(lreg, rreg, op, line) + if is_float_rprimitive(ltype) or is_float_rprimitive(rtype): + if isinstance(lreg, Integer): + lreg = Float(float(lreg.numeric_value())) + elif isinstance(rreg, Integer): + rreg = Float(float(rreg.numeric_value())) + if is_float_rprimitive(lreg.type) and is_float_rprimitive(rreg.type): + if op in FloatComparisonOp.op_to_id: + return self.compare_floats(lreg, rreg, FloatComparisonOp.op_to_id[op], line) + if op.endswith("="): + base_op = op[:-1] + else: + base_op = op + if base_op in FloatOp.op_to_id: + return self.float_op(lreg, rreg, FloatOp.op_to_id[base_op], line) call_c_ops_candidates = binary_ops.get(op, []) target = self.matching_call_c(call_c_ops_candidates, [lreg, rreg], line) @@ -1556,6 +1580,10 @@ def unary_op(self, value: Value, expr_op: str, line: int) -> Value: return self.int_op(typ, value, Integer(-1, typ), IntOp.XOR, line) elif expr_op == "+": return value + if is_float_rprimitive(typ) and expr_op == "-": + # Translate to '0 - x' + return self.float_op(Float(0.0), value, FloatOp.SUB, line) + if isinstance(value, Integer): # TODO: Overflow? Unsigned? num = value.value @@ -1564,6 +1592,8 @@ def unary_op(self, value: Value, expr_op: str, line: int) -> Value: return Integer(-num, typ, value.line) if is_tagged(typ) and expr_op == "+": return value + if isinstance(value, Float): + return Float(-value.value, value.line) if isinstance(typ, RInstance): if expr_op == "-": method = "__neg__" @@ -1713,6 +1743,8 @@ def bool_value(self, value: Value) -> Value: ): # Directly call the __bool__ method on classes that have it. result = self.gen_method_call(value, "__bool__", [], bool_rprimitive, value.line) + elif is_float_rprimitive(value.type): + result = self.compare_floats(value, Float(0.0), FloatComparisonOp.NEQ, value.line) else: value_type = optional_value_type(value.type) if value_type is not None: @@ -1890,6 +1922,17 @@ def int_op(self, type: RType, lhs: Value, rhs: Value, op: int, line: int = -1) - """ return self.add(IntOp(type, lhs, rhs, op, line)) + def float_op(self, lhs: Value, rhs: Value, op: int, line: int) -> Value: + """Generate a native float binary op. + + Args: + op: FloatOp.* constant (e.g. FloatOp.ADD) + """ + return self.add(FloatOp(lhs, rhs, op, line)) + + def compare_floats(self, lhs: Value, rhs: Value, op: int, line: int) -> Value: + return self.add(FloatComparisonOp(lhs, rhs, op, line)) + def fixed_width_int_op(self, type: RType, lhs: Value, rhs: Value, op: int, line: int) -> Value: """Generate a binary op using Python fixed-width integer semantics. diff --git a/mypyc/lib-rt/CPy.h b/mypyc/lib-rt/CPy.h index 016a6d3ea9e06..87e89b4ca6334 100644 --- a/mypyc/lib-rt/CPy.h +++ b/mypyc/lib-rt/CPy.h @@ -147,9 +147,9 @@ CPyTagged CPyTagged_Lshift(CPyTagged left, CPyTagged right); bool CPyTagged_IsEq_(CPyTagged left, CPyTagged right); bool CPyTagged_IsLt_(CPyTagged left, CPyTagged right); PyObject *CPyTagged_Str(CPyTagged n); +CPyTagged CPyTagged_FromFloat(double f); PyObject *CPyLong_FromStrWithBase(PyObject *o, CPyTagged base); PyObject *CPyLong_FromStr(PyObject *o); -PyObject *CPyLong_FromFloat(PyObject *o); PyObject *CPyBool_Str(bool b); int64_t CPyLong_AsInt64(PyObject *o); int64_t CPyInt64_Divide(int64_t x, int64_t y); @@ -283,6 +283,14 @@ static inline bool CPyTagged_IsLe(CPyTagged left, CPyTagged right) { } +// Float operations + + +double CPyFloat_Abs(double x); +double CPyFloat_Sqrt(double x); +double CPyFloat_FromTagged(CPyTagged x); + + // Generic operations (that work with arbitrary types) @@ -452,7 +460,6 @@ PyObject *CPyBytes_Join(PyObject *sep, PyObject *iter); int CPyBytes_Compare(PyObject *left, PyObject *right); - // Set operations diff --git a/mypyc/lib-rt/float_ops.c b/mypyc/lib-rt/float_ops.c new file mode 100644 index 0000000000000..36755d431f570 --- /dev/null +++ b/mypyc/lib-rt/float_ops.c @@ -0,0 +1,27 @@ +// Float primitive operations +// +// These are registered in mypyc.primitives.float_ops. + +#include +#include "CPy.h" + + +double CPyFloat_Abs(double x) { + return x >= 0.0 ? x : -x; +} + + +double CPyFloat_FromTagged(CPyTagged x) { + if (CPyTagged_CheckShort(x)) { + return CPyTagged_ShortAsSsize_t(x); + } + return PyFloat_AsDouble(CPyTagged_LongAsObject(x)); +} + +double CPyFloat_Sqrt(double x) { + if (x < 0.0) { + PyErr_SetString(PyExc_ValueError, "math domain error"); + return CPY_FLOAT_ERROR; + } + return sqrt(x); +} diff --git a/mypyc/lib-rt/int_ops.c b/mypyc/lib-rt/int_ops.c index 5ea2f65d5776e..5678f0d9aaf70 100644 --- a/mypyc/lib-rt/int_ops.c +++ b/mypyc/lib-rt/int_ops.c @@ -293,13 +293,14 @@ PyObject *CPyLong_FromStr(PyObject *o) { return CPyLong_FromStrWithBase(o, base); } -PyObject *CPyLong_FromFloat(PyObject *o) { - if (PyLong_Check(o)) { - CPy_INCREF(o); - return o; - } else { - return PyLong_FromDouble(PyFloat_AS_DOUBLE(o)); +CPyTagged CPyTagged_FromFloat(double f) { + if (f < (double)CPY_TAGGED_MAX && f > CPY_TAGGED_MIN) { + return (CPyTagged)f << 1; } + PyObject *o = PyLong_FromDouble(f); + if (o == NULL) + return CPY_INT_TAG; + return CPyTagged_StealFromObject(o); } PyObject *CPyBool_Str(bool b) { diff --git a/mypyc/lib-rt/mypyc_util.h b/mypyc/lib-rt/mypyc_util.h index 0fae239cbb9ec..13672087fbbcf 100644 --- a/mypyc/lib-rt/mypyc_util.h +++ b/mypyc/lib-rt/mypyc_util.h @@ -56,6 +56,9 @@ typedef PyObject CPyModule; // Error value for fixed-width (low-level) integers #define CPY_LL_INT_ERROR -113 +// Error value for floats +#define CPY_FLOAT_ERROR -113.0 + typedef void (*CPyVTableItem)(void); static inline CPyTagged CPyTagged_ShortFromInt(int x) { diff --git a/mypyc/lib-rt/setup.py b/mypyc/lib-rt/setup.py index e04d7041ad722..3dd06cbe7f4c8 100644 --- a/mypyc/lib-rt/setup.py +++ b/mypyc/lib-rt/setup.py @@ -21,7 +21,15 @@ ext_modules=[ Extension( "test_capi", - ["test_capi.cc", "init.c", "int_ops.c", "list_ops.c", "exc_ops.c", "generic_ops.c"], + [ + "test_capi.cc", + "init.c", + "int_ops.c", + "float_ops.c", + "list_ops.c", + "exc_ops.c", + "generic_ops.c", + ], depends=["CPy.h", "mypyc_util.h", "pythonsupport.h"], extra_compile_args=["-Wno-unused-function", "-Wno-sign-compare"] + compile_args, library_dirs=["../external/googletest/make"], diff --git a/mypyc/primitives/float_ops.py b/mypyc/primitives/float_ops.py index 535606df6176a..45ee0996708dd 100644 --- a/mypyc/primitives/float_ops.py +++ b/mypyc/primitives/float_ops.py @@ -2,18 +2,27 @@ from __future__ import annotations -from mypyc.ir.ops import ERR_MAGIC -from mypyc.ir.rtypes import float_rprimitive, object_rprimitive, str_rprimitive +from mypyc.ir.ops import ERR_MAGIC, ERR_MAGIC_OVERLAPPING, ERR_NEVER +from mypyc.ir.rtypes import float_rprimitive, int_rprimitive, object_rprimitive, str_rprimitive from mypyc.primitives.registry import function_op, load_address_op # Get the 'builtins.float' type object. load_address_op(name="builtins.float", type=object_rprimitive, src="PyFloat_Type") +# float(int) +int_to_float_op = function_op( + name="builtins.float", + arg_types=[int_rprimitive], + return_type=float_rprimitive, + c_function_name="CPyFloat_FromTagged", + error_kind=ERR_MAGIC_OVERLAPPING, +) + # float(str) function_op( name="builtins.float", arg_types=[str_rprimitive], - return_type=float_rprimitive, + return_type=object_rprimitive, c_function_name="PyFloat_FromString", error_kind=ERR_MAGIC, ) @@ -23,6 +32,33 @@ name="builtins.abs", arg_types=[float_rprimitive], return_type=float_rprimitive, - c_function_name="PyNumber_Absolute", - error_kind=ERR_MAGIC, + c_function_name="CPyFloat_Abs", + error_kind=ERR_NEVER, +) + +# math.sin(float) +function_op( + name="math.sin", + arg_types=[float_rprimitive], + return_type=float_rprimitive, + c_function_name="sin", + error_kind=ERR_NEVER, +) + +# math.cos(float) +function_op( + name="math.cos", + arg_types=[float_rprimitive], + return_type=float_rprimitive, + c_function_name="cos", + error_kind=ERR_NEVER, +) + +# math.sqrt(float) +function_op( + name="math.sqrt", + arg_types=[float_rprimitive], + return_type=float_rprimitive, + c_function_name="CPyFloat_Sqrt", + error_kind=ERR_NEVER, ) diff --git a/mypyc/primitives/int_ops.py b/mypyc/primitives/int_ops.py index 7eda9bab7e3c6..15b658e30b53b 100644 --- a/mypyc/primitives/int_ops.py +++ b/mypyc/primitives/int_ops.py @@ -50,8 +50,8 @@ function_op( name=int_name, arg_types=[float_rprimitive], - return_type=object_rprimitive, - c_function_name="CPyLong_FromFloat", + return_type=int_rprimitive, + c_function_name="CPyTagged_FromFloat", error_kind=ERR_MAGIC, ) diff --git a/mypyc/test-data/exceptions.test b/mypyc/test-data/exceptions.test index 187551249676e..16bf8ba1eb89d 100644 --- a/mypyc/test-data/exceptions.test +++ b/mypyc/test-data/exceptions.test @@ -570,6 +570,34 @@ L0: c.x = r1 return 1 +[case testExceptionWithOverlappingFloatErrorValue] +def f() -> float: + return 0.0 + +def g() -> float: + return f() +[out] +def f(): +L0: + return 0.0 +def g(): + r0 :: float + r1 :: bit + r2 :: object + r3 :: float +L0: + r0 = f() + r1 = r0 == -113.0 + if r1 goto L2 else goto L1 :: bool +L1: + return r0 +L2: + r2 = PyErr_Occurred() + if not is_error(r2) goto L3 (error at g:5) else goto L1 +L3: + r3 = :: float + return r3 + [case testExceptionWithLowLevelIntAttribute] from mypy_extensions import i32, i64 @@ -639,3 +667,47 @@ L5: L6: r6 = :: int64 return r6 + +[case testExceptionWithFloatAttribute] +class C: + def __init__(self, x: float, y: float) -> None: + self.x = x + if x: + self.y = y + +def f(c: C) -> float: + return c.x + c.y +[out] +def C.__init__(self, x, y): + self :: __main__.C + x, y :: float + r0 :: bit +L0: + self.x = x + r0 = x != 0.0 + if r0 goto L1 else goto L2 :: bool +L1: + self.y = y +L2: + return 1 +def f(c): + c :: __main__.C + r0, r1 :: float + r2 :: bit + r3 :: float + r4 :: object + r5 :: float +L0: + r0 = c.x + r1 = c.y + r2 = r1 == -113.0 + if r2 goto L2 else goto L1 :: bool +L1: + r3 = r0 + r1 + return r3 +L2: + r4 = PyErr_Occurred() + if not is_error(r4) goto L3 (error at f:8) else goto L1 +L3: + r5 = :: float + return r5 diff --git a/mypyc/test-data/fixtures/ir.py b/mypyc/test-data/fixtures/ir.py index 27e225f273bc1..f978b089ba012 100644 --- a/mypyc/test-data/fixtures/ir.py +++ b/mypyc/test-data/fixtures/ir.py @@ -112,13 +112,21 @@ class float: def __init__(self, x: object) -> None: pass def __add__(self, n: float) -> float: pass def __sub__(self, n: float) -> float: pass + def __rsub__(self, n: float) -> float: pass def __mul__(self, n: float) -> float: pass def __truediv__(self, n: float) -> float: pass + def __mod__(self, n: float) -> float: pass def __pow__(self, n: float) -> float: pass def __neg__(self) -> float: pass def __pos__(self) -> float: pass def __abs__(self) -> float: pass def __invert__(self) -> float: pass + def __eq__(self, x: object) -> bool: pass + def __ne__(self, x: object) -> bool: pass + def __lt__(self, x: float) -> bool: ... + def __le__(self, x: float) -> bool: ... + def __gt__(self, x: float) -> bool: ... + def __ge__(self, x: float) -> bool: ... class complex: def __init__(self, x: object, y: object = None) -> None: pass diff --git a/mypyc/test-data/irbuild-basic.test b/mypyc/test-data/irbuild-basic.test index a06977d037b20..e6426cdeea534 100644 --- a/mypyc/test-data/irbuild-basic.test +++ b/mypyc/test-data/irbuild-basic.test @@ -1016,35 +1016,24 @@ def assign_and_return_float_sum() -> float: return f1 * f2 + f3 [out] def assign_and_return_float_sum(): - r0, f1, r1, f2, r2, f3 :: float - r3 :: object - r4 :: float - r5 :: object - r6 :: float -L0: - r0 = 1.0 - f1 = r0 - r1 = 2.0 - f2 = r1 - r2 = 3.0 - f3 = r2 - r3 = PyNumber_Multiply(f1, f2) - r4 = cast(float, r3) - r5 = PyNumber_Add(r4, f3) - r6 = cast(float, r5) - return r6 + f1, f2, f3, r0, r1 :: float +L0: + f1 = 1.0 + f2 = 2.0 + f3 = 3.0 + r0 = f1 * f2 + r1 = r0 + f3 + return r1 [case testLoadComplex] def load() -> complex: return 5j+1.0 [out] def load(): - r0 :: object - r1 :: float - r2 :: object + r0, r1, r2 :: object L0: r0 = 5j - r1 = 1.0 + r1 = box(float, 1.0) r2 = PyNumber_Add(r0, r1) return r2 @@ -1176,10 +1165,8 @@ L0: r5 = unbox(int, r4) return r5 def return_float(): - r0 :: float L0: - r0 = 5.0 - return r0 + return 5.0 def return_callable_type(): r0 :: dict r1 :: str @@ -1196,7 +1183,7 @@ L0: r0 = return_callable_type() f = r0 r1 = PyObject_CallFunctionObjArgs(f, 0) - r2 = cast(float, r1) + r2 = unbox(float, r1) return r2 [case testCallableTypesWithKeywordArgs] @@ -3573,7 +3560,7 @@ def f() -> None: def f(): i, r0 :: int r1, i__redef__, r2 :: str - r3, i__redef____redef__ :: float + i__redef____redef__ :: float L0: i = 0 r0 = CPyTagged_Add(i, 2) @@ -3582,8 +3569,7 @@ L0: i__redef__ = r1 r2 = CPyStr_Append(i__redef__, i__redef__) i__redef__ = r2 - r3 = 0.0 - i__redef____redef__ = r3 + i__redef____redef__ = 0.0 return 1 [case testNewType] diff --git a/mypyc/test-data/irbuild-constant-fold.test b/mypyc/test-data/irbuild-constant-fold.test index 7d9127887aa6b..2d715d08b089b 100644 --- a/mypyc/test-data/irbuild-constant-fold.test +++ b/mypyc/test-data/irbuild-constant-fold.test @@ -148,12 +148,12 @@ L0: r0 = object 4 r1 = object 6 r2 = PyNumber_TrueDivide(r0, r1) - r3 = cast(float, r2) + r3 = unbox(float, r2) x = r3 r4 = object 10 r5 = object 5 r6 = PyNumber_TrueDivide(r4, r5) - r7 = cast(float, r6) + r7 = unbox(float, r6) y = r7 return 1 def unsupported_pow(): @@ -163,7 +163,7 @@ L0: r0 = object 3 r1 = object -1 r2 = CPyNumber_Power(r0, r1) - r3 = cast(float, r2) + r3 = unbox(float, r2) p = r3 return 1 diff --git a/mypyc/test-data/irbuild-dunders.test b/mypyc/test-data/irbuild-dunders.test index 82f04dcdf6870..3c140d927c0f9 100644 --- a/mypyc/test-data/irbuild-dunders.test +++ b/mypyc/test-data/irbuild-dunders.test @@ -184,10 +184,8 @@ L0: return 6 def C.__float__(self): self :: __main__.C - r0 :: float L0: - r0 = 4.0 - return r0 + return 4.0 def C.__pos__(self): self :: __main__.C L0: @@ -223,4 +221,3 @@ L0: r6 = c.__bool__() r7 = c.__complex__() return 1 - diff --git a/mypyc/test-data/irbuild-float.test b/mypyc/test-data/irbuild-float.test new file mode 100644 index 0000000000000..34f387878eb7a --- /dev/null +++ b/mypyc/test-data/irbuild-float.test @@ -0,0 +1,202 @@ +[case testFloatAdd] +def f(x: float, y: float) -> float: + return x + y +def g(x: float) -> float: + z = x - 1.5 + return 2.5 * z +[out] +def f(x, y): + x, y, r0 :: float +L0: + r0 = x + y + return r0 +def g(x): + x, r0, z, r1 :: float +L0: + r0 = x - 1.5 + z = r0 + r1 = 2.5 * z + return r1 + +[case testFloatBoxAndUnbox] +from typing import Any +def f(x: float) -> object: + return x +def g(x: Any) -> float: + return x +[out] +def f(x): + x :: float + r0 :: object +L0: + r0 = box(float, x) + return r0 +def g(x): + x :: object + r0 :: float +L0: + r0 = unbox(float, x) + return r0 + +[case testFloatNeg] +def f(x: float) -> float: + y = x * -0.5 + return -y +[out] +def f(x): + x, r0, y, r1 :: float +L0: + r0 = x * -0.5 + y = r0 + r1 = 0.0 - y + return r1 + +[case testFloatCoerceFromInt] +from mypy_extensions import i64 + +def from_int(x: int) -> float: + return x + +def from_literal() -> float: + return 5 + +def from_literal_neg() -> float: + return -2 +[out] +def from_int(x): + x :: int + r0 :: float +L0: + r0 = CPyFloat_FromTagged(x) + return r0 +def from_literal(): +L0: + return 5.0 +def from_literal_neg(): +L0: + return -2.0 + +[case testFloatOperatorAssignment] +def f(x: float, y: float) -> float: + x += y + x -= 5.0 + return x +[out] +def f(x, y): + x, y, r0, r1 :: float +L0: + r0 = x + y + x = r0 + r1 = x - 5.0 + x = r1 + return x + +[case testFloatComparison] +def lt(x: float, y: float) -> bool: + return x < y +def eq(x: float, y: float) -> bool: + return x == y +[out] +def lt(x, y): + x, y :: float + r0 :: bit +L0: + r0 = x < y + return r0 +def eq(x, y): + x, y :: float + r0 :: bit +L0: + r0 = x == y + return r0 + +[case testFloatOpWithLiteralInt] +def f(x: float) -> None: + y = x * 2 + z = 1 - y + b = z < 3 + c = 0 == z +[out] +def f(x): + x, r0, y, r1, z :: float + r2 :: bit + b :: bool + r3 :: bit + c :: bool +L0: + r0 = x * 2.0 + y = r0 + r1 = 1.0 - y + z = r1 + r2 = z < 3.0 + b = r2 + r3 = 0.0 == z + c = r3 + return 1 + +[case testFloatCallFunctionWithLiteralInt] +def f(x: float) -> None: pass + +def g() -> None: + f(3) + f(-2) +[out] +def f(x): + x :: float +L0: + return 1 +def g(): + r0, r1 :: None +L0: + r0 = f(3.0) + r1 = f(-2.0) + return 1 + +[case testFloatAsBool] +def f(x: float) -> int: + if x: + return 2 + else: + return 5 +[out] +def f(x): + x :: float + r0 :: bit +L0: + r0 = x != 0.0 + if r0 goto L1 else goto L2 :: bool +L1: + return 4 +L2: + return 10 +L3: + unreachable + +[case testCallSqrtViaMathModule] +import math + +def f(x: float) -> float: + return math.sqrt(x) +[out] +def f(x): + x, r0 :: float +L0: + r0 = CPyFloat_Sqrt(x) + return r0 + +[case testFloatFinalConstant] +from typing_extensions import Final + +X: Final = 123.0 +Y: Final = -1.0 + +def f() -> float: + a = X + return a + Y +[out] +def f(): + a, r0 :: float +L0: + a = 123.0 + r0 = a + -1.0 + return r0 diff --git a/mypyc/test-data/run-floats.test b/mypyc/test-data/run-floats.test index 1b67a1190cd8b..6918cdb003576 100644 --- a/mypyc/test-data/run-floats.test +++ b/mypyc/test-data/run-floats.test @@ -1,19 +1,73 @@ # Test cases for floats (compile and run) -[case testStrToFloat] +[case testFloatOps] +from typing import Any, cast +from typing_extensions import Final +from testutil import assertRaises + +MAGIC: Final = -113.0 + +def test_arithmetic() -> None: + zero = float(0.0) + one = zero + 1.0 + x = one + one / 2.0 + assert x == 1.5 + assert x - one == 0.5 + assert x * x == 2.25 + assert x / 2.0 == 0.75 + assert x * (-0.5) == -0.75 + assert -x == -1.5 + +def test_boxing_and_unboxing() -> None: + x = 1.5 + boxed: Any = x + assert repr(boxed) == "1.5" + assert type(boxed) is float + y: float = boxed + assert y == x + boxed_int: Any = 5 + assert type(boxed_int) is int + z: float = boxed_int + assert z == 5.0 + +def test_unboxing_failure() -> None: + boxed: Any = '1.5' + with assertRaises(TypeError): + x: float = boxed + +def test_coerce_from_int_literal() -> None: + x: float = 34 + assert x == 34.0 + y: float = -1 + assert y == -1.0 + +def test_coerce_from_short_tagged_int() -> None: + n = int() - 17 + x: float = n + assert x == -17.0 + for i in range(-300, 300): + y: float = i + o: object = y + assert o == i + +def test_coerce_from_long_tagged_int() -> None: + n = int() + 2**100 + x: float = n + assert repr(x) == '1.2676506002282294e+30' + n = int() - 2**100 + y: float = n + assert repr(y) == '-1.2676506002282294e+30' + def str_to_float(x: str) -> float: return float(x) -[file driver.py] -from native import str_to_float +def test_str_to_float() -> None: + assert str_to_float("1") == 1.0 + assert str_to_float("1.234567") == 1.234567 + assert str_to_float("44324") == 44324.0 + assert str_to_float("23.4") == 23.4 + assert str_to_float("-43.44e-4") == -43.44e-4 -assert str_to_float("1") == 1.0 -assert str_to_float("1.234567") == 1.234567 -assert str_to_float("44324") == 44324.0 -assert str_to_float("23.4") == 23.4 -assert str_to_float("-43.44e-4") == -43.44e-4 - -[case testFloatArithmetic] def test_abs() -> None: assert abs(0.0) == 0.0 assert abs(-1.234567) == 1.234567 @@ -28,3 +82,87 @@ def test_float_min_max() -> None: assert min(y, x) == 20.0 assert max(x, y) == 30.0 assert max(y, x) == 30.0 + +def default(x: float = 0) -> float: + return x + +def test_float_default_value() -> None: + assert default(1.2) == 1.2 + assert default() == 0.0 + +class C: + def __init__(self, x: float) -> None: + self.x = x + +def test_float_attr() -> None: + for i in range(-200, 200): + f = float(i) + c = C(f) + assert c.x == f + a: Any = c + assert a.x == f + c.x = MAGIC + assert c.x == MAGIC + assert a.x == MAGIC + a.x = 1.0 + assert a.x == 1.0 + a.x = MAGIC + assert a.x == MAGIC + +class D: + def __init__(self, x: float) -> None: + if x: + self.x = x + +def test_float_attr_maybe_undefned() -> None: + for i in range(-200, 200): + if i == 0: + d = D(0.0) + with assertRaises(AttributeError): + d.x + a: Any = d + with assertRaises(AttributeError): + a.x + d.x = MAGIC + assert d.x == MAGIC + assert a.x == MAGIC + d.x = 0.0 + assert d.x == 0.0 + assert a.x == 0.0 + a.x = MAGIC + assert a.x == MAGIC + d = D(0.0) + a = cast(Any, d) + a.x = MAGIC + assert d.x == MAGIC + else: + f = float(i) + d = D(f) + assert d.x == f + a2: Any = d + assert a2.x == f + +def f(x: float) -> float: + return x + 1 + +def test_return_values() -> None: + a: Any = f + for i in range(-200, 200): + x = float(i) + assert f(x) == x + 1 + assert a(x) == x + 1 + +def exc() -> float: + raise IndexError('x') + +def test_exception() -> None: + with assertRaises(IndexError): + exc() + a: Any = exc + with assertRaises(IndexError): + a() + +def test_undefined_local_var() -> None: + if int(): + x = 1.0 + todo # TODO incomplete diff --git a/mypyc/test/test_irbuild.py b/mypyc/test/test_irbuild.py index cb5e690eed55d..86bdf7c590d8f 100644 --- a/mypyc/test/test_irbuild.py +++ b/mypyc/test/test_irbuild.py @@ -31,6 +31,7 @@ "irbuild-set.test", "irbuild-str.test", "irbuild-bytes.test", + "irbuild-float.test", "irbuild-statements.test", "irbuild-nested.test", "irbuild-classes.test", diff --git a/mypyc/transform/exceptions.py b/mypyc/transform/exceptions.py index 2851955ff38fa..8676f17237853 100644 --- a/mypyc/transform/exceptions.py +++ b/mypyc/transform/exceptions.py @@ -23,6 +23,7 @@ Branch, CallC, ComparisonOp, + Float, GetAttr, Integer, LoadErrorValue, @@ -33,7 +34,7 @@ TupleGet, Value, ) -from mypyc.ir.rtypes import RTuple, bool_rprimitive +from mypyc.ir.rtypes import RTuple, bool_rprimitive, is_float_rprimitive from mypyc.primitives.exc_ops import err_occurred_op from mypyc.primitives.registry import CFunctionDescription @@ -173,7 +174,11 @@ def insert_overlapping_error_value_check(ops: list[Op], target: Value) -> Compar ops.append(item) return insert_overlapping_error_value_check(ops, item) else: - errvalue = Integer(int(typ.c_undefined), rtype=typ) + errvalue: Value + if is_float_rprimitive(target.type): + errvalue = Float(float(typ.c_undefined)) + else: + errvalue = Integer(int(typ.c_undefined), rtype=op.type) op = ComparisonOp(target, errvalue, ComparisonOp.EQ) ops.append(op) return op diff --git a/test-data/unit/lib-stub/math.pyi b/test-data/unit/lib-stub/math.pyi new file mode 100644 index 0000000000000..25038e0e28223 --- /dev/null +++ b/test-data/unit/lib-stub/math.pyi @@ -0,0 +1 @@ +def sqrt(__x: float) -> float: ... From 0ea696d9ede63668ed665f7e413f18c4ff31ec2b Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 31 Dec 2022 16:46:54 +0000 Subject: [PATCH 02/56] Fix after rebase --- mypy/constant_fold.py | 10 ++++++++++ mypyc/irbuild/constant_fold.py | 9 +-------- mypyc/test-data/irbuild-any.test | 8 +++----- mypyc/transform/exceptions.py | 2 +- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/mypy/constant_fold.py b/mypy/constant_fold.py index a22c1b9ba9e58..a1011397eba88 100644 --- a/mypy/constant_fold.py +++ b/mypy/constant_fold.py @@ -64,6 +64,8 @@ def constant_fold_expr(expr: Expression, cur_mod_id: str) -> ConstantValue | Non value = constant_fold_expr(expr.expr, cur_mod_id) if isinstance(value, int): return constant_fold_unary_int_op(expr.op, value) + if isinstance(value, float): + return constant_fold_unary_float_op(expr.op, value) return None @@ -110,6 +112,14 @@ def constant_fold_unary_int_op(op: str, value: int) -> int | None: return None +def constant_fold_unary_float_op(op: str, value: float) -> float | None: + if op == "-": + return -value + elif op == "+": + return value + return None + + def constant_fold_binary_str_op(op: str, left: str, right: str) -> str | None: if op == "+": return left + right diff --git a/mypyc/irbuild/constant_fold.py b/mypyc/irbuild/constant_fold.py index 8ce99182ed24e..bc71052f54183 100644 --- a/mypyc/irbuild/constant_fold.py +++ b/mypyc/irbuild/constant_fold.py @@ -16,6 +16,7 @@ from mypy.constant_fold import ( constant_fold_binary_int_op, constant_fold_binary_str_op, + constant_fold_unary_float_op, constant_fold_unary_int_op, ) from mypy.nodes import ( @@ -75,11 +76,3 @@ def constant_fold_expr(builder: IRBuilder, expr: Expression) -> ConstantValue | if isinstance(value, float): return constant_fold_unary_float_op(expr.op, value) return None - - -def constant_fold_unary_float_op(op: str, value: float) -> Optional[float]: - if op == "-": - return -value - elif op == "+": - return value - return None diff --git a/mypyc/test-data/irbuild-any.test b/mypyc/test-data/irbuild-any.test index 8d4e085179ae8..6c245cea923ff 100644 --- a/mypyc/test-data/irbuild-any.test +++ b/mypyc/test-data/irbuild-any.test @@ -187,15 +187,14 @@ def f() -> None: def f(): r0, r1 :: object r2, a :: int - r3, r4, b :: float + r3, b :: float L0: r0 = object 1 r1 = PyNumber_Absolute(r0) r2 = unbox(int, r1) a = r2 - r3 = 1.1 - r4 = PyNumber_Absolute(r3) - b = r4 + r3 = CPyFloat_Abs(1.1) + b = r3 return 1 [case testFunctionBasedOps] @@ -237,4 +236,3 @@ L0: r4 = unbox(int, r3) r5 = box(int, r4) return r5 - diff --git a/mypyc/transform/exceptions.py b/mypyc/transform/exceptions.py index 8676f17237853..bf5e60659f8f0 100644 --- a/mypyc/transform/exceptions.py +++ b/mypyc/transform/exceptions.py @@ -178,7 +178,7 @@ def insert_overlapping_error_value_check(ops: list[Op], target: Value) -> Compar if is_float_rprimitive(target.type): errvalue = Float(float(typ.c_undefined)) else: - errvalue = Integer(int(typ.c_undefined), rtype=op.type) + errvalue = Integer(int(typ.c_undefined), rtype=typ) op = ComparisonOp(target, errvalue, ComparisonOp.EQ) ops.append(op) return op From 3d5f397e3f1104a2fd5bcc688531bd69e7a319d5 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 27 Aug 2022 15:29:59 +0100 Subject: [PATCH 03/56] Support undefined floats in locals --- mypyc/test-data/fixtures/ir.py | 1 + mypyc/test-data/run-floats.test | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/mypyc/test-data/fixtures/ir.py b/mypyc/test-data/fixtures/ir.py index f978b089ba012..a4e1bc5756276 100644 --- a/mypyc/test-data/fixtures/ir.py +++ b/mypyc/test-data/fixtures/ir.py @@ -296,6 +296,7 @@ class ValueError(Exception): pass class AttributeError(Exception): pass class ImportError(Exception): pass class NameError(Exception): pass +class UnboundLocalError(NameError): pass class LookupError(Exception): pass class KeyError(LookupError): pass class IndexError(LookupError): pass diff --git a/mypyc/test-data/run-floats.test b/mypyc/test-data/run-floats.test index 6918cdb003576..89223dcf0e527 100644 --- a/mypyc/test-data/run-floats.test +++ b/mypyc/test-data/run-floats.test @@ -163,6 +163,17 @@ def test_exception() -> None: a() def test_undefined_local_var() -> None: + if not int(): + x = -113.0 + assert x == -113.0 if int(): - x = 1.0 - todo # TODO incomplete + y = -113.0 + with assertRaises(UnboundLocalError, 'local variable "y" referenced before assignment'): + print(y) + if not int(): + x2 = -1.0 + assert x2 == -1.0 + if int(): + y2 = -1.0 + with assertRaises(UnboundLocalError, 'local variable "y2" referenced before assignment'): + print(y2) From 8954cc9b5305ca00d2a9368c7e97f18887b4c1a8 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 27 Aug 2022 15:42:22 +0100 Subject: [PATCH 04/56] Support float default args --- mypyc/irbuild/ll_builder.py | 2 ++ mypyc/test-data/irbuild-float.test | 17 +++++++++++++++++ mypyc/test-data/run-floats.test | 17 +++++++++++++---- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/mypyc/irbuild/ll_builder.py b/mypyc/irbuild/ll_builder.py index 0a6c0360e5bce..85ccf295d3c8d 100644 --- a/mypyc/irbuild/ll_builder.py +++ b/mypyc/irbuild/ll_builder.py @@ -1037,6 +1037,8 @@ def native_args_to_positional( elif not lst: if is_fixed_width_rtype(arg.type): output_arg = Integer(0, arg.type) + elif is_float_rprimitive(arg.type): + output_arg = Float(0.0) else: output_arg = self.add(LoadErrorValue(arg.type, is_borrowed=True)) else: diff --git a/mypyc/test-data/irbuild-float.test b/mypyc/test-data/irbuild-float.test index 34f387878eb7a..33ddda368d638 100644 --- a/mypyc/test-data/irbuild-float.test +++ b/mypyc/test-data/irbuild-float.test @@ -200,3 +200,20 @@ L0: a = 123.0 r0 = a + -1.0 return r0 + +[case testFloatDefaultArg] +def f(x: float = 1.5) -> float: + return x +[out] +def f(x, __bitmap): + x :: float + __bitmap, r0 :: uint32 + r1 :: bit +L0: + r0 = __bitmap & 1 + r1 = r0 == 0 + if r1 goto L1 else goto L2 :: bool +L1: + x = 1.5 +L2: + return x diff --git a/mypyc/test-data/run-floats.test b/mypyc/test-data/run-floats.test index 89223dcf0e527..a077f5d98ae3e 100644 --- a/mypyc/test-data/run-floats.test +++ b/mypyc/test-data/run-floats.test @@ -83,12 +83,21 @@ def test_float_min_max() -> None: assert max(x, y) == 30.0 assert max(y, x) == 30.0 -def default(x: float = 0) -> float: - return x +def default(x: float = 2) -> float: + return x + 1 def test_float_default_value() -> None: - assert default(1.2) == 1.2 - assert default() == 0.0 + assert default(1.2) == 2.2 + for i in range(-200, 200): + assert default(float(i)) == i + 1 + assert default() == 3.0 + +def test_float_default_value_wrapper() -> None: + f: Any = default + assert f(1.2) == 2.2 + for i in range(-200, 200): + assert f(float(i)) == i + 1 + assert f() == 3.0 class C: def __init__(self, x: float) -> None: From 62abec1b91c05ee54fb7dbd9ea0088e9b490b03f Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 31 Aug 2022 21:11:22 +0100 Subject: [PATCH 05/56] WIP mixed operations test case --- mypyc/test-data/irbuild-float.test | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/mypyc/test-data/irbuild-float.test b/mypyc/test-data/irbuild-float.test index 33ddda368d638..6b5db8746b38c 100644 --- a/mypyc/test-data/irbuild-float.test +++ b/mypyc/test-data/irbuild-float.test @@ -217,3 +217,11 @@ L1: x = 1.5 L2: return x + +[case testFloatMixedOperations] +def f(x: float, y: int) -> None: + if x < y: + z = x + y + x += y +[out] +x From c8214d09304d590323403e8272ebef63b7075f58 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 10 Sep 2022 13:39:12 +0100 Subject: [PATCH 06/56] Support mixed float/int arithmetic --- mypyc/irbuild/ll_builder.py | 9 +++++++- mypyc/test-data/fixtures/ir.py | 1 + mypyc/test-data/irbuild-float.test | 37 ++++++++++++++++++++++++++++-- 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/mypyc/irbuild/ll_builder.py b/mypyc/irbuild/ll_builder.py index 85ccf295d3c8d..09cdea7de174a 100644 --- a/mypyc/irbuild/ll_builder.py +++ b/mypyc/irbuild/ll_builder.py @@ -349,7 +349,7 @@ def coerce( return Float(float(src.value // 2)) return Float(float(src.value)) elif is_tagged(src_type) and is_float_rprimitive(target_type): - return self.call_c(int_to_float_op, [src], line) + return self.int_to_float(src, line) else: # To go from one unboxed type to another, we go through a boxed # in-between value, for simplicity. @@ -1345,6 +1345,10 @@ def binary_op(self, lreg: Value, rreg: Value, op: str, line: int) -> Value: lreg = Float(float(lreg.numeric_value())) elif isinstance(rreg, Integer): rreg = Float(float(rreg.numeric_value())) + elif is_int_rprimitive(lreg.type): + lreg = self.int_to_float(lreg, line) + elif is_int_rprimitive(rreg.type): + rreg = self.int_to_float(rreg, line) if is_float_rprimitive(lreg.type) and is_float_rprimitive(rreg.type): if op in FloatComparisonOp.op_to_id: return self.compare_floats(lreg, rreg, FloatComparisonOp.op_to_id[op], line) @@ -2087,6 +2091,9 @@ def new_tuple_with_length(self, length: Value, line: int) -> Value: """ return self.call_c(new_tuple_with_length_op, [length], line) + def int_to_float(self, n: Value, line: int) -> Value: + return self.call_c(int_to_float_op, [n], line) + # Internal helpers def decompose_union_helper( diff --git a/mypyc/test-data/fixtures/ir.py b/mypyc/test-data/fixtures/ir.py index a4e1bc5756276..c3da90b87c25a 100644 --- a/mypyc/test-data/fixtures/ir.py +++ b/mypyc/test-data/fixtures/ir.py @@ -111,6 +111,7 @@ def encode(self, x: str=..., y: str=...) -> bytes: ... class float: def __init__(self, x: object) -> None: pass def __add__(self, n: float) -> float: pass + def __radd__(self, n: float) -> float: pass def __sub__(self, n: float) -> float: pass def __rsub__(self, n: float) -> float: pass def __mul__(self, n: float) -> float: pass diff --git a/mypyc/test-data/irbuild-float.test b/mypyc/test-data/irbuild-float.test index 6b5db8746b38c..21b95d9b4774f 100644 --- a/mypyc/test-data/irbuild-float.test +++ b/mypyc/test-data/irbuild-float.test @@ -222,6 +222,39 @@ L2: def f(x: float, y: int) -> None: if x < y: z = x + y - x += y + x -= y + z = y + z + if y == x: + x -= 1 [out] -x +def f(x, y): + x :: float + y :: int + r0 :: float + r1 :: bit + r2, r3, z, r4, r5, r6, r7, r8 :: float + r9 :: bit + r10 :: float +L0: + r0 = CPyFloat_FromTagged(y) + r1 = x < r0 + if r1 goto L1 else goto L2 :: bool +L1: + r2 = CPyFloat_FromTagged(y) + r3 = x + r2 + z = r3 + r4 = CPyFloat_FromTagged(y) + r5 = x - r4 + x = r5 + r6 = CPyFloat_FromTagged(y) + r7 = r6 + z + z = r7 +L2: + r8 = CPyFloat_FromTagged(y) + r9 = r8 == x + if r9 goto L3 else goto L4 :: bool +L3: + r10 = x - 1.0 + x = r10 +L4: + return 1 From f222c1f1db70078334c8fd459dc30720bdf00601 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 10 Sep 2022 14:11:54 +0100 Subject: [PATCH 07/56] Add run tests --- mypyc/test-data/run-floats.test | 38 +++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/mypyc/test-data/run-floats.test b/mypyc/test-data/run-floats.test index a077f5d98ae3e..c0ca3aa1001f3 100644 --- a/mypyc/test-data/run-floats.test +++ b/mypyc/test-data/run-floats.test @@ -18,6 +18,44 @@ def test_arithmetic() -> None: assert x * (-0.5) == -0.75 assert -x == -1.5 +def test_mixed_arithmetic() -> None: + zf = float(0.0) + zn = int() + assert (zf + 5.5) + (zn + 1) == 6.5 + assert (zn - 2) - (zf - 5.5) == 3.5 + x = zf + 3.4 + x += zn + 2 + assert x == 5.4 + +def test_comparisons() -> None: + zero = float(0.0) + one = zero + 1.0 + x = one + one / 2.0 + assert x < (1.51 + zero) + assert not (x < (1.49 + zero)) + assert x > (1.49 + zero) + assert not (x > (1.51 + zero)) + assert x <= (1.5 + zero) + assert not (x <= (1.49 + zero)) + assert x >= (1.5 + zero) + assert not (x >= (1.51 + zero)) + +def test_mixed_comparisons() -> None: + zf = float(0.0) + zn = int() + if (zf + 1.0) == (zn + 1): + assert True + else: + assert False + if (zf + 1.1) == (zn + 1): + assert False + else: + assert True + assert (zf + 1.1) != (zn + 1) + assert (zf + 1.1) > (zn + 1) + assert not (zf + 0.9) > (zn + 1) + assert (zn + 1) < (zf + 1.1) + def test_boxing_and_unboxing() -> None: x = 1.5 boxed: Any = x From 80ebe04aa2ff709d29377c49fdb739a7cb241e18 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 10 Sep 2022 16:06:50 +0100 Subject: [PATCH 08/56] Add primitive for true divide of int by int (resulting in a float) --- mypyc/lib-rt/int_ops.c | 19 +++++++++++++++ mypyc/primitives/int_ops.py | 3 +++ mypyc/test-data/irbuild-float.test | 38 ++++++++++++++++++++++++++++++ mypyc/test-data/run-integers.test | 22 ++++++++++++++++- 4 files changed, 81 insertions(+), 1 deletion(-) diff --git a/mypyc/lib-rt/int_ops.c b/mypyc/lib-rt/int_ops.c index 5678f0d9aaf70..48583b056b836 100644 --- a/mypyc/lib-rt/int_ops.c +++ b/mypyc/lib-rt/int_ops.c @@ -640,3 +640,22 @@ int32_t CPyInt32_Remainder(int32_t x, int32_t y) { void CPyInt32_Overflow() { PyErr_SetString(PyExc_OverflowError, "int too large to convert to i32"); } + +double CPyTagged_TrueDivide(CPyTagged x, CPyTagged y) { + if (unlikely(y == 0)) { + PyErr_SetString(PyExc_ZeroDivisionError, "division by zero"); + return CPY_FLOAT_ERROR; + } + if (likely(!CPyTagged_CheckLong(x) && !CPyTagged_CheckLong(y))) { + return (double)((Py_ssize_t)x >> 1) / (double)((Py_ssize_t)y >> 1); + } else { + PyObject *xo = CPyTagged_AsObject(x); + PyObject *yo = CPyTagged_AsObject(y); + PyObject *result = PyNumber_TrueDivide(xo, yo); + if (result == NULL) { + return CPY_FLOAT_ERROR; + } + return PyFloat_AsDouble(result); + } + return 1.0; +} diff --git a/mypyc/primitives/int_ops.py b/mypyc/primitives/int_ops.py index 15b658e30b53b..711306a1dab85 100644 --- a/mypyc/primitives/int_ops.py +++ b/mypyc/primitives/int_ops.py @@ -126,6 +126,9 @@ def int_binary_op( int_binary_op(">>", "CPyTagged_Rshift", error_kind=ERR_MAGIC) int_binary_op("<<", "CPyTagged_Lshift", error_kind=ERR_MAGIC) +int_binary_op("/", "CPyTagged_TrueDivide", return_type=float_rprimitive, + error_kind=ERR_MAGIC_OVERLAPPING) + # This should work because assignment operators are parsed differently # and the code in irbuild that handles it does the assignment # regardless of whether or not the operator works in place anyway. diff --git a/mypyc/test-data/irbuild-float.test b/mypyc/test-data/irbuild-float.test index 21b95d9b4774f..e41db8b732b85 100644 --- a/mypyc/test-data/irbuild-float.test +++ b/mypyc/test-data/irbuild-float.test @@ -258,3 +258,41 @@ L3: x = r10 L4: return 1 + +[case testFloatDivide] +def f1(x: float, y: float) -> float: + z = x / y + z = z / 2.0 + return z / 3 +def f2(n: int, m: int) -> float: + return n / m +def f3(f: float, n: int) -> float: + x = f / n + return n / x +[out] +def f1(x, y): + x, y, r0, z, r1, r2 :: float +L0: + r0 = x / y + z = r0 + r1 = z / 2.0 + z = r1 + r2 = z / 3.0 + return r2 +def f2(n, m): + n, m :: int + r0 :: float +L0: + r0 = CPyTagged_TrueDivide(n, m) + return r0 +def f3(f, n): + f :: float + n :: int + r0, r1, x, r2, r3 :: float +L0: + r0 = CPyFloat_FromTagged(n) + r1 = f / r0 + x = r1 + r2 = CPyFloat_FromTagged(n) + r3 = r2 / x + return r3 diff --git a/mypyc/test-data/run-integers.test b/mypyc/test-data/run-integers.test index c65f36110b46a..d575e141b5671 100644 --- a/mypyc/test-data/run-integers.test +++ b/mypyc/test-data/run-integers.test @@ -173,6 +173,7 @@ assert test_isinstance_int_and_not_bool(1) == True [case testIntOps] from typing import Any +from testutil import assertRaises def check_and(x: int, y: int) -> None: # eval() can be trusted to calculate expected result @@ -390,7 +391,7 @@ 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: +def test_floor_divide() -> None: for x in range(-100, 100): for y in range(-100, 100): if y != 0: @@ -470,6 +471,25 @@ def test_floor_divide_by_literal() -> None: assert div_by_3(i) == i_boxed // int('3') assert div_by_4(i) == i_boxed // int('4') +def test_true_divide() -> None: + for x in range(-150, 100): + for y in range(-150, 100): + if y != 0: + assert x / y == getattr(x, "__truediv__")(y) + large1 = (123 + int())**123 + large2 = (121 + int())**121 + assert large1 / large2 == getattr(large1, "__truediv__")(large2) + assert large1 / 135 == getattr(large1, "__truediv__")(135) + assert large1 / -2 == getattr(large1, "__truediv__")(-2) + assert 17 / large2 == getattr(17, "__truediv__")(large2) + + huge = 10**1000 + int() + with assertRaises(OverflowError, "integer division result too large for a float"): + huge / 2 + with assertRaises(OverflowError, "integer division result too large for a float"): + huge / -2 + assert 1 / huge == 0.0 + [case testIntMinMax] def test_int_min_max() -> None: x: int = 200 From 8ae9c984b2df2e9dab75f028d1af43fa39166d20 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 10 Sep 2022 16:38:37 +0100 Subject: [PATCH 09/56] Make float(f) a no-op for float arguments --- mypyc/irbuild/specialize.py | 13 +++++++++++++ mypyc/test-data/irbuild-float.test | 18 ++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/mypyc/irbuild/specialize.py b/mypyc/irbuild/specialize.py index 8cb24c5b47dac..39fbf0f6f242e 100644 --- a/mypyc/irbuild/specialize.py +++ b/mypyc/irbuild/specialize.py @@ -62,6 +62,7 @@ list_rprimitive, set_rprimitive, str_rprimitive, + is_float_rprimitive, ) from mypyc.irbuild.builder import IRBuilder from mypyc.irbuild.for_helpers import ( @@ -728,3 +729,15 @@ def translate_bool(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Value arg = expr.args[0] src = builder.accept(arg) return builder.builder.bool_value(src) + + +@specialize_function("builtins.float") +def translate_float(builder: IRBuilder, expr: CallExpr, callee: RefExpr) -> Optional[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_float_rprimitive(arg_type): + # No-op float conversion. + return builder.accept(arg) + return None diff --git a/mypyc/test-data/irbuild-float.test b/mypyc/test-data/irbuild-float.test index e41db8b732b85..ada24e4d3e117 100644 --- a/mypyc/test-data/irbuild-float.test +++ b/mypyc/test-data/irbuild-float.test @@ -296,3 +296,21 @@ L0: r2 = CPyFloat_FromTagged(n) r3 = r2 / x return r3 + +[case testFloatExplicitConversions] +def f(f: float, n: int) -> int: + x = float(n) + y = float(x) # no-op + return int(y) +[out] +def f(f, n): + f :: float + n :: int + r0, x, y :: float + r1 :: int +L0: + r0 = CPyFloat_FromTagged(n) + x = r0 + y = x + r1 = CPyTagged_FromFloat(y) + return r1 From 9d874ce1244841f7e1745f5b1a6385f2c4e1c0e0 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 10 Sep 2022 16:44:06 +0100 Subject: [PATCH 10/56] Fix overflow handling when converting to int --- mypyc/lib-rt/float_ops.c | 6 +++++- mypyc/test-data/run-floats.test | 8 ++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/mypyc/lib-rt/float_ops.c b/mypyc/lib-rt/float_ops.c index 36755d431f570..20d7f0071411d 100644 --- a/mypyc/lib-rt/float_ops.c +++ b/mypyc/lib-rt/float_ops.c @@ -15,7 +15,11 @@ double CPyFloat_FromTagged(CPyTagged x) { if (CPyTagged_CheckShort(x)) { return CPyTagged_ShortAsSsize_t(x); } - return PyFloat_AsDouble(CPyTagged_LongAsObject(x)); + double result = PyFloat_AsDouble(CPyTagged_LongAsObject(x)); + if (unlikely(result == -1.0) && PyErr_Occurred()) { + return CPY_FLOAT_ERROR; + } + return result; } double CPyFloat_Sqrt(double x) { diff --git a/mypyc/test-data/run-floats.test b/mypyc/test-data/run-floats.test index c0ca3aa1001f3..e210ea352c12f 100644 --- a/mypyc/test-data/run-floats.test +++ b/mypyc/test-data/run-floats.test @@ -96,6 +96,14 @@ def test_coerce_from_long_tagged_int() -> None: y: float = n assert repr(y) == '-1.2676506002282294e+30' +def test_coerce_from_very_long_tagged_int() -> None: + n = int() + 10**1000 + with assertRaises(OverflowError, "int too large to convert to float"): + x: float = n + n = int() - 10**1000 + with assertRaises(OverflowError, "int too large to convert to float"): + y: float = n + def str_to_float(x: str) -> float: return float(x) From 916cda97c1a877c60c7e9af145bf95ee7df7f153 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 10 Sep 2022 18:20:35 +0100 Subject: [PATCH 11/56] WIP float divide by zero test case --- mypyc/test-data/run-floats.test | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/mypyc/test-data/run-floats.test b/mypyc/test-data/run-floats.test index e210ea352c12f..6d6f80d933bfb 100644 --- a/mypyc/test-data/run-floats.test +++ b/mypyc/test-data/run-floats.test @@ -17,6 +17,10 @@ def test_arithmetic() -> None: assert x / 2.0 == 0.75 assert x * (-0.5) == -0.75 assert -x == -1.5 + assert x % 0.4 == 0.29999999999999993 + assert x % -0.4 == -0.10000000000000009 + assert (-x) % 0.4 == 0.10000000000000009 + assert (-x) % -0.4 == -0.29999999999999993 def test_mixed_arithmetic() -> None: zf = float(0.0) @@ -27,6 +31,14 @@ def test_mixed_arithmetic() -> None: x += zn + 2 assert x == 5.4 +def test_arithmetic_errors() -> None: + zero = float(0.0) + one = zero + 1.0 + with assertRaises(ZeroDivisionError, "float division by zero"): + print(one / zero) + with assertRaises(ZeroDivisionError, "float modulo"): + print(one % zero) + def test_comparisons() -> None: zero = float(0.0) one = zero + 1.0 From 3bbc76afda50ce999762bbccb1df95e71990e150 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sun, 11 Sep 2022 14:25:35 +0100 Subject: [PATCH 12/56] Add some irchecking of int and float ops --- mypyc/analysis/ircheck.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/mypyc/analysis/ircheck.py b/mypyc/analysis/ircheck.py index 5e4e993641ef8..1ce7f4c60d361 100644 --- a/mypyc/analysis/ircheck.py +++ b/mypyc/analysis/ircheck.py @@ -62,6 +62,7 @@ set_rprimitive, str_rprimitive, tuple_rprimitive, + is_float_rprimitive, ) @@ -223,6 +224,14 @@ def check_compatibility(self, op: Op, t: RType, s: RType) -> None: if not can_coerce_to(t, s) or not can_coerce_to(s, t): self.fail(source=op, desc=f"{t.name} and {s.name} are not compatible") + def expect_float(self, op: Op, v: Value) -> None: + if not is_float_rprimitive(v.type): + self.fail(op, f"Float expected (actual type is {v.type})") + + def expect_non_float(self, op: Op, v: Value) -> None: + if is_float_rprimitive(v.type): + self.fail(op, f"Float not expected") + def visit_goto(self, op: Goto) -> None: self.check_control_op_targets(op) @@ -378,16 +387,21 @@ def visit_load_global(self, op: LoadGlobal) -> None: pass def visit_int_op(self, op: IntOp) -> None: - pass + self.expect_non_float(op, op.lhs) + self.expect_non_float(op, op.rhs) def visit_comparison_op(self, op: ComparisonOp) -> None: self.check_compatibility(op, op.lhs.type, op.rhs.type) + self.expect_non_float(op, op.lhs) + self.expect_non_float(op, op.rhs) def visit_float_op(self, op: FloatOp) -> None: - pass + self.expect_float(op, op.lhs) + self.expect_float(op, op.rhs) def visit_float_comparison_op(self, op: FloatComparisonOp) -> None: - pass + self.expect_float(op, op.lhs) + self.expect_float(op, op.rhs) def visit_load_mem(self, op: LoadMem) -> None: pass From c2080666591a70ab1b49b06c47fa7db7cb600084 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sun, 11 Sep 2022 15:02:47 +0100 Subject: [PATCH 13/56] Implement float division and modulus --- mypyc/codegen/emitfunc.py | 6 +- mypyc/ir/ops.py | 9 +- mypyc/irbuild/ll_builder.py | 77 +++++++++++--- mypyc/lib-rt/CPy.h | 1 + mypyc/primitives/float_ops.py | 9 ++ mypyc/test-data/irbuild-constant-fold.test | 19 +--- mypyc/test-data/irbuild-float.test | 116 +++++++++++++++++---- mypyc/test-data/run-floats.test | 19 +++- 8 files changed, 199 insertions(+), 57 deletions(-) diff --git a/mypyc/codegen/emitfunc.py b/mypyc/codegen/emitfunc.py index 873f574922393..9f5364e8b7120 100644 --- a/mypyc/codegen/emitfunc.py +++ b/mypyc/codegen/emitfunc.py @@ -678,7 +678,11 @@ def visit_float_op(self, op: FloatOp) -> None: dest = self.reg(op) lhs = self.reg(op.lhs) rhs = self.reg(op.rhs) - self.emit_line("%s = %s %s %s;" % (dest, lhs, op.op_str[op.op], rhs)) + if op.op != FloatOp.MOD: + self.emit_line("%s = %s %s %s;" % (dest, lhs, op.op_str[op.op], rhs)) + else: + # TODO: This may set errno as a side effect, that is a little sketchy. + self.emit_line("%s = fmod(%s, %s);" % (dest, lhs, rhs)) def visit_float_comparison_op(self, op: FloatComparisonOp) -> None: dest = self.reg(op) diff --git a/mypyc/ir/ops.py b/mypyc/ir/ops.py index e1b22f9ab6023..2a5ff5c0ad93a 100644 --- a/mypyc/ir/ops.py +++ b/mypyc/ir/ops.py @@ -915,6 +915,7 @@ class RaiseStandardError(RegisterOp): UNBOUND_LOCAL_ERROR: Final = "UnboundLocalError" RUNTIME_ERROR: Final = "RuntimeError" NAME_ERROR: Final = "NameError" + ZERO_DIVISION_ERROR: Final = "ZeroDivisionError" def __init__(self, class_name: str, value: str | Value | None, line: int) -> None: super().__init__(line) @@ -1180,7 +1181,7 @@ class FloatOp(RegisterOp): """Binary float arithmetic op (e.g., r1 = r2 + r3). These ops are low-level and are similar to the corresponding C - operations (and unlike Python operations). + operations (and somewhat different from Python operations). The left and right values must be floats. """ @@ -1191,10 +1192,10 @@ class FloatOp(RegisterOp): SUB: Final = 1 MUL: Final = 2 DIV: Final = 3 + MOD: Final = 4 - op_str: Final = {ADD: "+", SUB: "-", MUL: "*", DIV: "/"} - - op_to_id = {op: op_id for op_id, op in op_str.items()} # type: Final + op_str: Final = {ADD: "+", SUB: "-", MUL: "*", DIV: "/", MOD: "%"} + op_to_id: Final = {op: op_id for op_id, op in op_str.items()} def __init__(self, lhs: Value, rhs: Value, op: int, line: int = -1) -> None: super().__init__(line) diff --git a/mypyc/irbuild/ll_builder.py b/mypyc/irbuild/ll_builder.py index 09cdea7de174a..018d7165bd0ed 100644 --- a/mypyc/irbuild/ll_builder.py +++ b/mypyc/irbuild/ll_builder.py @@ -117,6 +117,7 @@ pointer_rprimitive, short_int_rprimitive, str_rprimitive, + float_rprimitive, ) from mypyc.irbuild.mapper import Mapper from mypyc.irbuild.util import concrete_arg_kind @@ -129,7 +130,7 @@ dict_update_in_display_op, ) from mypyc.primitives.exc_ops import err_occurred_op, keep_propagating_op -from mypyc.primitives.float_ops import int_to_float_op +from mypyc.primitives.float_ops import int_to_float_op, copysign_op from mypyc.primitives.generic_ops import ( generic_len_op, generic_ssize_t_len_op, @@ -1357,7 +1358,7 @@ def binary_op(self, lreg: Value, rreg: Value, op: str, line: int) -> Value: else: base_op = op if base_op in FloatOp.op_to_id: - return self.float_op(lreg, rreg, FloatOp.op_to_id[base_op], line) + return self.float_op(lreg, rreg, base_op, line) call_c_ops_candidates = binary_ops.get(op, []) target = self.matching_call_c(call_c_ops_candidates, [lreg, rreg], line) @@ -1588,7 +1589,7 @@ def unary_op(self, value: Value, expr_op: str, line: int) -> Value: return value if is_float_rprimitive(typ) and expr_op == "-": # Translate to '0 - x' - return self.float_op(Float(0.0), value, FloatOp.SUB, line) + return self.float_op(Float(0.0), value, '-', line) if isinstance(value, Integer): # TODO: Overflow? Unsigned? @@ -1928,13 +1929,58 @@ def int_op(self, type: RType, lhs: Value, rhs: Value, op: int, line: int = -1) - """ return self.add(IntOp(type, lhs, rhs, op, line)) - def float_op(self, lhs: Value, rhs: Value, op: int, line: int) -> Value: - """Generate a native float binary op. + def float_op(self, lhs: Value, rhs: Value, op: str, line: int) -> Value: + """Generate a native float binary arithmetic operation. + + This follows Python semantics (e.g. raise exception on division by zero). + Add a FloatOp directly if you want low-level semantics. Args: - op: FloatOp.* constant (e.g. FloatOp.ADD) + op: Binary operator (e.g. '+' or '*') """ - return self.add(FloatOp(lhs, rhs, op, line)) + op_id = FloatOp.op_to_id[op] + if op_id in (FloatOp.DIV, FloatOp.MOD): + if not (isinstance(rhs, Float) and rhs.value != 0.0): + c = self.compare_floats(rhs, Float(0.0), FloatComparisonOp.EQ, line) + err, ok = BasicBlock(), BasicBlock() + self.add(Branch(c, err, ok, Branch.BOOL, rare=True)) + self.activate_block(err) + if op_id == FloatOp.DIV: + msg = "float division by zero" + else: + msg = "float modulo" + self.add(RaiseStandardError(RaiseStandardError.ZERO_DIVISION_ERROR, msg, line)) + self.add(Unreachable()) + self.activate_block(ok) + if op_id == FloatOp.MOD: + # Adjust the result to match Python semantics (FloatOp follows C semantics). + return self.float_mod(lhs, rhs, line) + else: + return self.add(FloatOp(lhs, rhs, op_id, line)) + + def float_mod(self, lhs: Value, rhs: Value, line: int) -> Value: + """Perform x % y on floats using Python semantics.""" + mod = self.add(FloatOp(lhs, rhs, FloatOp.MOD, line)) + res = Register(float_rprimitive) + self.add(Assign(res, mod)) + tricky, adjust, copysign, done = BasicBlock(), BasicBlock(), BasicBlock(), BasicBlock() + is_zero = self.add(FloatComparisonOp(res, Float(0.0), FloatComparisonOp.EQ, line)) + self.add(Branch(is_zero, copysign, tricky, Branch.BOOL)) + self.activate_block(tricky) + same_signs = self.is_same_float_signs(type, lhs, rhs, line) + self.add(Branch(same_signs, done, adjust, Branch.BOOL)) + self.activate_block(adjust) + adj = self.float_op(res, rhs, '+', line) + self.add(Assign(res, adj)) + self.add(Goto(done)) + self.activate_block(copysign) + # If the remainder is zero, CPython ensures the result has the + # same sign as the denominator. + adj = self.call_c(copysign_op, [Float(0.0), rhs], line) + self.add(Assign(res, adj)) + self.add(Goto(done)) + self.activate_block(done) + return res def compare_floats(self, lhs: Value, rhs: Value, op: int, line: int) -> Value: return self.add(FloatComparisonOp(lhs, rhs, op, line)) @@ -1981,13 +2027,12 @@ def inline_fixed_width_divide(self, type: RType, lhs: Value, rhs: Value, line: i res = Register(type) div = self.int_op(type, lhs, rhs, IntOp.DIV, line) self.add(Assign(res, div)) - diff_signs = self.is_different_native_int_signs(type, lhs, rhs, line) + same_signs = self.is_same_native_int_signs(type, lhs, rhs, line) tricky, adjust, done = BasicBlock(), BasicBlock(), BasicBlock() - self.add(Branch(diff_signs, done, tricky, Branch.BOOL)) + self.add(Branch(same_signs, done, tricky, Branch.BOOL)) self.activate_block(tricky) mul = self.int_op(type, res, rhs, IntOp.MUL, line) mul_eq = self.add(ComparisonOp(mul, lhs, ComparisonOp.EQ, line)) - adjust = BasicBlock() self.add(Branch(mul_eq, done, adjust, Branch.BOOL)) self.activate_block(adjust) adj = self.int_op(type, res, Integer(1, type), IntOp.SUB, line) @@ -2001,12 +2046,11 @@ def inline_fixed_width_mod(self, type: RType, lhs: Value, rhs: Value, line: int) res = Register(type) mod = self.int_op(type, lhs, rhs, IntOp.MOD, line) self.add(Assign(res, mod)) - diff_signs = self.is_different_native_int_signs(type, lhs, rhs, line) + same_signs = self.is_same_native_int_signs(type, lhs, rhs, line) tricky, adjust, done = BasicBlock(), BasicBlock(), BasicBlock() - self.add(Branch(diff_signs, done, tricky, Branch.BOOL)) + self.add(Branch(same_signs, done, tricky, Branch.BOOL)) self.activate_block(tricky) is_zero = self.add(ComparisonOp(res, Integer(0, type), ComparisonOp.EQ, line)) - adjust = BasicBlock() self.add(Branch(is_zero, done, adjust, Branch.BOOL)) self.activate_block(adjust) adj = self.int_op(type, res, rhs, IntOp.ADD, line) @@ -2015,11 +2059,16 @@ def inline_fixed_width_mod(self, type: RType, lhs: Value, rhs: Value, line: int) self.activate_block(done) return res - def is_different_native_int_signs(self, type: RType, a: Value, b: Value, line: int) -> Value: + def is_same_native_int_signs(self, type: RType, a: Value, b: Value, line: int) -> Value: neg1 = self.add(ComparisonOp(a, Integer(0, type), ComparisonOp.SLT, line)) neg2 = self.add(ComparisonOp(b, Integer(0, type), ComparisonOp.SLT, line)) return self.add(ComparisonOp(neg1, neg2, ComparisonOp.EQ, line)) + def is_same_float_signs(self, type: RType, a: Value, b: Value, line: int) -> Value: + neg1 = self.add(FloatComparisonOp(a, Float(0.0), FloatComparisonOp.LT, line)) + neg2 = self.add(FloatComparisonOp(b, Float(0.0), FloatComparisonOp.LT, line)) + return self.add(ComparisonOp(neg1, neg2, ComparisonOp.EQ, line)) + def comparison_op(self, lhs: Value, rhs: Value, op: int, line: int) -> Value: return self.add(ComparisonOp(lhs, rhs, op, line)) diff --git a/mypyc/lib-rt/CPy.h b/mypyc/lib-rt/CPy.h index 87e89b4ca6334..73af55bf474c0 100644 --- a/mypyc/lib-rt/CPy.h +++ b/mypyc/lib-rt/CPy.h @@ -158,6 +158,7 @@ int32_t CPyLong_AsInt32(PyObject *o); int32_t CPyInt32_Divide(int32_t x, int32_t y); int32_t CPyInt32_Remainder(int32_t x, int32_t y); void CPyInt32_Overflow(void); +double CPyTagged_TrueDivide(CPyTagged x, CPyTagged y); static inline int CPyTagged_CheckLong(CPyTagged x) { return x & CPY_INT_TAG; diff --git a/mypyc/primitives/float_ops.py b/mypyc/primitives/float_ops.py index 45ee0996708dd..b1054f2d810a5 100644 --- a/mypyc/primitives/float_ops.py +++ b/mypyc/primitives/float_ops.py @@ -62,3 +62,12 @@ c_function_name="CPyFloat_Sqrt", error_kind=ERR_NEVER, ) + +# math.copysign(float, float) +copysign_op = function_op( + name="math.copysign", + arg_types=[float_rprimitive, float_rprimitive], + return_type=float_rprimitive, + c_function_name="copysign", + error_kind=ERR_NEVER, +) diff --git a/mypyc/test-data/irbuild-constant-fold.test b/mypyc/test-data/irbuild-constant-fold.test index 2d715d08b089b..866953f0c09a6 100644 --- a/mypyc/test-data/irbuild-constant-fold.test +++ b/mypyc/test-data/irbuild-constant-fold.test @@ -140,21 +140,12 @@ L0: rshift_neg = r3 return 1 def unsupported_div(): - r0, r1, r2 :: object - r3, x :: float - r4, r5, r6 :: object - r7, y :: float + r0, x, r1, y :: float L0: - r0 = object 4 - r1 = object 6 - r2 = PyNumber_TrueDivide(r0, r1) - r3 = unbox(float, r2) - x = r3 - r4 = object 10 - r5 = object 5 - r6 = PyNumber_TrueDivide(r4, r5) - r7 = unbox(float, r6) - y = r7 + r0 = CPyTagged_TrueDivide(8, 12) + x = r0 + r1 = CPyTagged_TrueDivide(20, 10) + y = r1 return 1 def unsupported_pow(): r0, r1, r2 :: object diff --git a/mypyc/test-data/irbuild-float.test b/mypyc/test-data/irbuild-float.test index ada24e4d3e117..10169dddfbaa6 100644 --- a/mypyc/test-data/irbuild-float.test +++ b/mypyc/test-data/irbuild-float.test @@ -259,43 +259,76 @@ L3: L4: return 1 -[case testFloatDivide] -def f1(x: float, y: float) -> float: +[case testFloatDivideSimple] +def f(x: float, y: float) -> float: z = x / y z = z / 2.0 return z / 3 -def f2(n: int, m: int) -> float: - return n / m -def f3(f: float, n: int) -> float: - x = f / n - return n / x [out] -def f1(x, y): - x, y, r0, z, r1, r2 :: float +def f(x, y): + x, y :: float + r0 :: bit + r1 :: bool + r2, z, r3, r4 :: float L0: - r0 = x / y - z = r0 - r1 = z / 2.0 - z = r1 - r2 = z / 3.0 - return r2 -def f2(n, m): + r0 = y == 0.0 + if r0 goto L1 else goto L2 :: bool +L1: + r1 = raise ZeroDivisionError('float division by zero') + unreachable +L2: + r2 = x / y + z = r2 + r3 = z / 2.0 + z = r3 + r4 = z / 3.0 + return r4 + +[case testFloatDivideIntOperand] +def f(n: int, m: int) -> float: + return n / m +[out] +def f(n, m): n, m :: int r0 :: float L0: r0 = CPyTagged_TrueDivide(n, m) return r0 -def f3(f, n): + +[case testFloatResultOfIntDivide] +def f(f: float, n: int) -> float: + x = f / n + return n / x +[out] +def f(f, n): f :: float n :: int - r0, r1, x, r2, r3 :: float + r0 :: float + r1 :: bit + r2 :: bool + r3, x, r4 :: float + r5 :: bit + r6 :: bool + r7 :: float L0: r0 = CPyFloat_FromTagged(n) - r1 = f / r0 - x = r1 - r2 = CPyFloat_FromTagged(n) - r3 = r2 / x - return r3 + r1 = r0 == 0.0 + if r1 goto L1 else goto L2 :: bool +L1: + r2 = raise ZeroDivisionError('float division by zero') + unreachable +L2: + r3 = f / r0 + x = r3 + r4 = CPyFloat_FromTagged(n) + r5 = x == 0.0 + if r5 goto L3 else goto L4 :: bool +L3: + r6 = raise ZeroDivisionError('float division by zero') + unreachable +L4: + r7 = r4 / x + return r7 [case testFloatExplicitConversions] def f(f: float, n: int) -> int: @@ -314,3 +347,40 @@ L0: y = x r1 = CPyTagged_FromFloat(y) return r1 + +[case testFloatModulo] +def f(x: float, y: float) -> float: + return x % y +[out] +def f(x, y): + x, y :: float + r0 :: bit + r1 :: bool + r2, r3 :: float + r4, r5, r6, r7 :: bit + r8, r9 :: float +L0: + r0 = y == 0.0 + if r0 goto L1 else goto L2 :: bool +L1: + r1 = raise ZeroDivisionError('float modulo') + unreachable +L2: + r2 = x % y + r3 = r2 + r4 = r3 == 0.0 + if r4 goto L5 else goto L3 :: bool +L3: + r5 = x < 0.0 + r6 = y < 0.0 + r7 = r5 == r6 + if r7 goto L6 else goto L4 :: bool +L4: + r8 = r3 + y + r3 = r8 + goto L6 +L5: + r9 = copysign(0.0, y) + r3 = r9 +L6: + return r3 diff --git a/mypyc/test-data/run-floats.test b/mypyc/test-data/run-floats.test index 6d6f80d933bfb..4ad6a78d64013 100644 --- a/mypyc/test-data/run-floats.test +++ b/mypyc/test-data/run-floats.test @@ -4,9 +4,13 @@ from typing import Any, cast from typing_extensions import Final from testutil import assertRaises +import math MAGIC: Final = -113.0 +# Various different float values +float_vals = [float(n) * 0.25 for n in range(-10, 10)] + [-0.0, 1.0/3.0, math.sqrt(2.0), 1.23e200, -2.34e200, 5.43e-100, -6.532e-200, float('inf'), -float('inf'), float('nan')] + def test_arithmetic() -> None: zero = float(0.0) one = zero + 1.0 @@ -17,10 +21,23 @@ def test_arithmetic() -> None: assert x / 2.0 == 0.75 assert x * (-0.5) == -0.75 assert -x == -1.5 + for x in float_vals: + for y in float_vals: + if y != 0: + assert repr(x / y) == repr(getattr(x, "__truediv__")(y)) + +def test_mod() -> None: + zero = float(0.0) + one = zero + 1.0 + x = one + one / 2.0 assert x % 0.4 == 0.29999999999999993 - assert x % -0.4 == -0.10000000000000009 assert (-x) % 0.4 == 0.10000000000000009 + assert x % -0.4 == -0.10000000000000009 assert (-x) % -0.4 == -0.29999999999999993 + for x in float_vals: + for y in float_vals: + if y != 0: + assert repr(x % y) == repr(getattr(x, "__mod__")(y)) def test_mixed_arithmetic() -> None: zf = float(0.0) From 218def27e41cd86742b19df6b31bebec7c6d7b48 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sun, 11 Sep 2022 16:01:00 +0100 Subject: [PATCH 14/56] Add and improve some math primitives --- mypyc/lib-rt/CPy.h | 4 +++ mypyc/lib-rt/float_ops.c | 37 ++++++++++++++++++++++++---- mypyc/primitives/float_ops.py | 32 ++++++++++++++++++------ mypyc/test-data/fixtures/testutil.py | 15 +++++++++++ mypyc/test-data/run-floats.test | 31 ++++++++++------------- mypyc/test/test_run.py | 1 + test-data/unit/lib-stub/math.pyi | 5 ++++ 7 files changed, 95 insertions(+), 30 deletions(-) diff --git a/mypyc/lib-rt/CPy.h b/mypyc/lib-rt/CPy.h index 73af55bf474c0..f209f9875dbd0 100644 --- a/mypyc/lib-rt/CPy.h +++ b/mypyc/lib-rt/CPy.h @@ -288,8 +288,12 @@ static inline bool CPyTagged_IsLe(CPyTagged left, CPyTagged right) { double CPyFloat_Abs(double x); +double CPyFloat_Sin(double x); +double CPyFloat_Cos(double x); double CPyFloat_Sqrt(double x); double CPyFloat_FromTagged(CPyTagged x); +bool CPyFloat_IsInf(double x); +bool CPyFloat_IsNaN(double x); // Generic operations (that work with arbitrary types) diff --git a/mypyc/lib-rt/float_ops.c b/mypyc/lib-rt/float_ops.c index 20d7f0071411d..a22b9c975feaf 100644 --- a/mypyc/lib-rt/float_ops.c +++ b/mypyc/lib-rt/float_ops.c @@ -6,11 +6,11 @@ #include "CPy.h" -double CPyFloat_Abs(double x) { - return x >= 0.0 ? x : -x; +static double CPy_DomainError() { + PyErr_SetString(PyExc_ValueError, "math domain error"); + return CPY_FLOAT_ERROR; } - double CPyFloat_FromTagged(CPyTagged x) { if (CPyTagged_CheckShort(x)) { return CPyTagged_ShortAsSsize_t(x); @@ -22,10 +22,37 @@ double CPyFloat_FromTagged(CPyTagged x) { return result; } +double CPyFloat_Abs(double x) { + return x >= 0.0 ? x : -x; +} + +double CPyFloat_Sin(double x) { + double v = sin(x); + if (unlikely(isnan(v)) && !isnan(x)) { + return CPy_DomainError(); + } + return v; +} + +double CPyFloat_Cos(double x) { + double v = cos(x); + if (unlikely(isnan(v)) && !isnan(x)) { + return CPy_DomainError(); + } + return v; +} + double CPyFloat_Sqrt(double x) { if (x < 0.0) { - PyErr_SetString(PyExc_ValueError, "math domain error"); - return CPY_FLOAT_ERROR; + return CPy_DomainError(); } return sqrt(x); } + +bool CPyFloat_IsInf(double x) { + return isinf(x) != 0; +} + +bool CPyFloat_IsNaN(double x) { + return isnan(x) != 0; +} diff --git a/mypyc/primitives/float_ops.py b/mypyc/primitives/float_ops.py index b1054f2d810a5..4b71f32034a9e 100644 --- a/mypyc/primitives/float_ops.py +++ b/mypyc/primitives/float_ops.py @@ -2,8 +2,8 @@ from __future__ import annotations -from mypyc.ir.ops import ERR_MAGIC, ERR_MAGIC_OVERLAPPING, ERR_NEVER -from mypyc.ir.rtypes import float_rprimitive, int_rprimitive, object_rprimitive, str_rprimitive +from mypyc.ir.ops import ERR_MAGIC, ERR_MAGIC_OVERLAPPING, ERR_NEVER, ERR_MAGIC_OVERLAPPING +from mypyc.ir.rtypes import float_rprimitive, int_rprimitive, object_rprimitive, str_rprimitive, bool_rprimitive from mypyc.primitives.registry import function_op, load_address_op # Get the 'builtins.float' type object. @@ -41,8 +41,8 @@ name="math.sin", arg_types=[float_rprimitive], return_type=float_rprimitive, - c_function_name="sin", - error_kind=ERR_NEVER, + c_function_name="CPyFloat_Sin", + error_kind=ERR_MAGIC_OVERLAPPING, ) # math.cos(float) @@ -50,8 +50,8 @@ name="math.cos", arg_types=[float_rprimitive], return_type=float_rprimitive, - c_function_name="cos", - error_kind=ERR_NEVER, + c_function_name="CPyFloat_Cos", + error_kind=ERR_MAGIC_OVERLAPPING, ) # math.sqrt(float) @@ -60,7 +60,7 @@ arg_types=[float_rprimitive], return_type=float_rprimitive, c_function_name="CPyFloat_Sqrt", - error_kind=ERR_NEVER, + error_kind=ERR_MAGIC_OVERLAPPING, ) # math.copysign(float, float) @@ -71,3 +71,21 @@ c_function_name="copysign", error_kind=ERR_NEVER, ) + +# math.isinf(float) +function_op( + name="math.isinf", + arg_types=[float_rprimitive], + return_type=bool_rprimitive, + c_function_name="CPyFloat_IsInf", + error_kind=ERR_NEVER, +) + +# math.isnan(float) +function_op( + name="math.isnan", + arg_types=[float_rprimitive], + return_type=bool_rprimitive, + c_function_name="CPyFloat_IsNaN", + error_kind=ERR_NEVER, +) diff --git a/mypyc/test-data/fixtures/testutil.py b/mypyc/test-data/fixtures/testutil.py index 7b4fcc9fc1ca3..e2e7420f7b9ad 100644 --- a/mypyc/test-data/fixtures/testutil.py +++ b/mypyc/test-data/fixtures/testutil.py @@ -2,10 +2,22 @@ from contextlib import contextmanager from collections.abc import Iterator +import math from typing import ( Any, Iterator, TypeVar, Generator, Optional, List, Tuple, Sequence, Union, Callable, Awaitable, ) +from typing_extensions import Final + +FLOAT_MAGIC: Final = -113.0 + +# Various different float values +float_vals = [ + float(n) * 0.25 for n in range(-10, 10) +] + [ + -0.0, 1.0/3.0, math.sqrt(2.0), 1.23e200, -2.34e200, 5.43e-100, -6.532e-200, + float('inf'), -float('inf'), float('nan'), FLOAT_MAGIC +] @contextmanager def assertRaises(typ: type, msg: str = '') -> Iterator[None]: @@ -17,6 +29,9 @@ def assertRaises(typ: type, msg: str = '') -> Iterator[None]: else: assert False, f"Expected {typ.__name__} but got no exception" +def assertDomainError() -> Any: + return assertRaises(ValueError, "math domain error") + T = TypeVar('T') U = TypeVar('U') V = TypeVar('V') diff --git a/mypyc/test-data/run-floats.test b/mypyc/test-data/run-floats.test index 4ad6a78d64013..b0273447da723 100644 --- a/mypyc/test-data/run-floats.test +++ b/mypyc/test-data/run-floats.test @@ -3,14 +3,9 @@ [case testFloatOps] from typing import Any, cast from typing_extensions import Final -from testutil import assertRaises +from testutil import assertRaises, float_vals, FLOAT_MAGIC import math -MAGIC: Final = -113.0 - -# Various different float values -float_vals = [float(n) * 0.25 for n in range(-10, 10)] + [-0.0, 1.0/3.0, math.sqrt(2.0), 1.23e200, -2.34e200, 5.43e-100, -6.532e-200, float('inf'), -float('inf'), float('nan')] - def test_arithmetic() -> None: zero = float(0.0) one = zero + 1.0 @@ -185,13 +180,13 @@ def test_float_attr() -> None: assert c.x == f a: Any = c assert a.x == f - c.x = MAGIC - assert c.x == MAGIC - assert a.x == MAGIC + c.x = FLOAT_MAGIC + assert c.x == FLOAT_MAGIC + assert a.x == FLOAT_MAGIC a.x = 1.0 assert a.x == 1.0 - a.x = MAGIC - assert a.x == MAGIC + a.x = FLOAT_MAGIC + assert a.x == FLOAT_MAGIC class D: def __init__(self, x: float) -> None: @@ -207,18 +202,18 @@ def test_float_attr_maybe_undefned() -> None: a: Any = d with assertRaises(AttributeError): a.x - d.x = MAGIC - assert d.x == MAGIC - assert a.x == MAGIC + d.x = FLOAT_MAGIC + assert d.x == FLOAT_MAGIC + assert a.x == FLOAT_MAGIC d.x = 0.0 assert d.x == 0.0 assert a.x == 0.0 - a.x = MAGIC - assert a.x == MAGIC + a.x = FLOAT_MAGIC + assert a.x == FLOAT_MAGIC d = D(0.0) a = cast(Any, d) - a.x = MAGIC - assert d.x == MAGIC + a.x = FLOAT_MAGIC + assert d.x == FLOAT_MAGIC else: f = float(i) d = D(f) diff --git a/mypyc/test/test_run.py b/mypyc/test/test_run.py index cd4ea8396cce8..9598b9865f1e1 100644 --- a/mypyc/test/test_run.py +++ b/mypyc/test/test_run.py @@ -42,6 +42,7 @@ "run-i64.test", "run-i32.test", "run-floats.test", + "run-math.test", "run-bools.test", "run-strings.test", "run-bytes.test", diff --git a/test-data/unit/lib-stub/math.pyi b/test-data/unit/lib-stub/math.pyi index 25038e0e28223..f70dffdfcedb1 100644 --- a/test-data/unit/lib-stub/math.pyi +++ b/test-data/unit/lib-stub/math.pyi @@ -1 +1,6 @@ def sqrt(__x: float) -> float: ... +def sin(__x: float) -> float: ... +def cos(__x: float) -> float: ... +def copysign(__x: float, __y: float) -> float: ... +def isinf(__x: float) -> bool: ... +def isnan(__x: float) -> bool: ... From de5dd64ef84ecfa88f49273ab862ba844ef1a841 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sun, 11 Sep 2022 16:55:10 +0100 Subject: [PATCH 15/56] Improve run tests --- mypyc/test-data/run-floats.test | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/mypyc/test-data/run-floats.test b/mypyc/test-data/run-floats.test index b0273447da723..3706c4b8b42a0 100644 --- a/mypyc/test-data/run-floats.test +++ b/mypyc/test-data/run-floats.test @@ -18,6 +18,9 @@ def test_arithmetic() -> None: assert -x == -1.5 for x in float_vals: for y in float_vals: + assert repr(x + y) == repr(getattr(x, "__add__")(y)) + assert repr(x - y) == repr(getattr(x, "__sub__")(y)) + assert repr(x * y) == repr(getattr(x, "__mul__")(y)) if y != 0: assert repr(x / y) == repr(getattr(x, "__truediv__")(y)) @@ -63,6 +66,14 @@ def test_comparisons() -> None: assert not (x <= (1.49 + zero)) assert x >= (1.5 + zero) assert not (x >= (1.51 + zero)) + for x in float_vals: + for y in float_vals: + assert (x <= y) == getattr(x, "__le__")(y) + assert (x < y) == getattr(x, "__lt__")(y) + assert (x >= y) == getattr(x, "__ge__")(y) + assert (x > y) == getattr(x, "__gt__")(y) + assert (x == y) == getattr(x, "__eq__")(y) + assert (x != y) == getattr(x, "__ne__")(y) def test_mixed_comparisons() -> None: zf = float(0.0) @@ -91,6 +102,11 @@ def test_boxing_and_unboxing() -> None: assert type(boxed_int) is int z: float = boxed_int assert z == 5.0 + for xx in float_vals: + bb: Any = xx + yy: float = bb + assert repr(xx) == repr(bb) + assert repr(xx) == repr(yy) def test_unboxing_failure() -> None: boxed: Any = '1.5' @@ -146,12 +162,10 @@ def test_abs() -> None: assert abs(-43.44e-4) == 43.44e-4 def test_float_min_max() -> None: - x: float = 20.0 - y: float = 30.0 - assert min(x, y) == 20.0 - assert min(y, x) == 20.0 - assert max(x, y) == 30.0 - assert max(y, x) == 30.0 + for x in float_vals: + for y in float_vals: + min_any: Any = min + assert repr(min(x, y)) == repr(min_any(x, y)) def default(x: float = 2) -> float: return x + 1 @@ -230,6 +244,11 @@ def test_return_values() -> None: x = float(i) assert f(x) == x + 1 assert a(x) == x + 1 + for x in float_vals: + if not math.isnan(x): + assert f(x) == x + 1 + else: + assert math.isnan(f(x)) def exc() -> float: raise IndexError('x') From c1cdda101a3a84176bea4db24213f4f57246e1d5 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 14 Sep 2022 19:12:36 +0100 Subject: [PATCH 16/56] Make +x a no-op for floats --- mypyc/irbuild/ll_builder.py | 9 ++++++--- mypyc/test-data/irbuild-float.test | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/mypyc/irbuild/ll_builder.py b/mypyc/irbuild/ll_builder.py index 018d7165bd0ed..042a1f79e2cdb 100644 --- a/mypyc/irbuild/ll_builder.py +++ b/mypyc/irbuild/ll_builder.py @@ -1587,9 +1587,12 @@ def unary_op(self, value: Value, expr_op: str, line: int) -> Value: return self.int_op(typ, value, Integer(-1, typ), IntOp.XOR, line) elif expr_op == "+": return value - if is_float_rprimitive(typ) and expr_op == "-": - # Translate to '0 - x' - return self.float_op(Float(0.0), value, '-', line) + if is_float_rprimitive(typ): + if expr_op == "-": + # Translate to '0 - x' + return self.float_op(Float(0.0), value, '-', line) + elif expr_op == "+": + return value if isinstance(value, Integer): # TODO: Overflow? Unsigned? diff --git a/mypyc/test-data/irbuild-float.test b/mypyc/test-data/irbuild-float.test index 10169dddfbaa6..e00550818cb30 100644 --- a/mypyc/test-data/irbuild-float.test +++ b/mypyc/test-data/irbuild-float.test @@ -38,9 +38,9 @@ L0: r0 = unbox(float, x) return r0 -[case testFloatNeg] +[case testFloatNegAndPos] def f(x: float) -> float: - y = x * -0.5 + y = +x * -0.5 return -y [out] def f(x): From 47acc064fa2b31c55cc616fce2362bbe0ac5f4bc Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Wed, 14 Sep 2022 19:30:47 +0100 Subject: [PATCH 17/56] Fix float negation --- mypyc/analysis/dataflow.py | 4 ++++ mypyc/analysis/ircheck.py | 4 ++++ mypyc/analysis/selfleaks.py | 4 ++++ mypyc/codegen/emitfunc.py | 6 ++++++ mypyc/ir/ops.py | 21 +++++++++++++++++++++ mypyc/ir/pprint.py | 4 ++++ mypyc/irbuild/ll_builder.py | 4 ++-- mypyc/test-data/irbuild-float.test | 2 +- mypyc/test-data/run-floats.test | 2 ++ 9 files changed, 48 insertions(+), 3 deletions(-) diff --git a/mypyc/analysis/dataflow.py b/mypyc/analysis/dataflow.py index 9b964d54607a1..877fdaf778840 100644 --- a/mypyc/analysis/dataflow.py +++ b/mypyc/analysis/dataflow.py @@ -20,6 +20,7 @@ Extend, Float, FloatComparisonOp, + FloatNeg, FloatOp, GetAttr, GetElementPtr, @@ -251,6 +252,9 @@ def visit_int_op(self, op: IntOp) -> GenAndKill[T]: def visit_float_op(self, op: FloatOp) -> GenAndKill[T]: return self.visit_register_op(op) + def visit_float_neg(self, op: FloatNeg) -> GenAndKill[T]: + return self.visit_register_op(op) + def visit_comparison_op(self, op: ComparisonOp) -> GenAndKill[T]: return self.visit_register_op(op) diff --git a/mypyc/analysis/ircheck.py b/mypyc/analysis/ircheck.py index 1ce7f4c60d361..477c968549fb6 100644 --- a/mypyc/analysis/ircheck.py +++ b/mypyc/analysis/ircheck.py @@ -17,6 +17,7 @@ DecRef, Extend, FloatComparisonOp, + FloatNeg, FloatOp, GetAttr, GetElementPtr, @@ -399,6 +400,9 @@ def visit_float_op(self, op: FloatOp) -> None: self.expect_float(op, op.lhs) self.expect_float(op, op.rhs) + def visit_float_neg(self, op: FloatNeg) -> None: + self.expect_float(op, op.src) + def visit_float_comparison_op(self, op: FloatComparisonOp) -> None: self.expect_float(op, op.lhs) self.expect_float(op, op.rhs) diff --git a/mypyc/analysis/selfleaks.py b/mypyc/analysis/selfleaks.py index e5874e9c29950..288c366e50e5f 100644 --- a/mypyc/analysis/selfleaks.py +++ b/mypyc/analysis/selfleaks.py @@ -15,6 +15,7 @@ ComparisonOp, Extend, FloatComparisonOp, + FloatNeg, FloatOp, GetAttr, GetElementPtr, @@ -165,6 +166,9 @@ def visit_comparison_op(self, op: ComparisonOp) -> GenAndKill: def visit_float_op(self, op: FloatOp) -> GenAndKill: return CLEAN + def visit_float_neg(self, op: FloatNeg) -> GenAndKill: + return CLEAN + def visit_float_comparison_op(self, op: FloatComparisonOp) -> GenAndKill: return CLEAN diff --git a/mypyc/codegen/emitfunc.py b/mypyc/codegen/emitfunc.py index 9f5364e8b7120..c6af1309550bb 100644 --- a/mypyc/codegen/emitfunc.py +++ b/mypyc/codegen/emitfunc.py @@ -27,6 +27,7 @@ Extend, Float, FloatComparisonOp, + FloatNeg, FloatOp, GetAttr, GetElementPtr, @@ -684,6 +685,11 @@ def visit_float_op(self, op: FloatOp) -> None: # TODO: This may set errno as a side effect, that is a little sketchy. self.emit_line("%s = fmod(%s, %s);" % (dest, lhs, rhs)) + def visit_float_neg(self, op: FloatNeg) -> None: + dest = self.reg(op) + src = self.reg(op.src) + self.emit_line(f"{dest} = -{src};") + def visit_float_comparison_op(self, op: FloatComparisonOp) -> None: dest = self.reg(op) lhs = self.reg(op.lhs) diff --git a/mypyc/ir/ops.py b/mypyc/ir/ops.py index 2a5ff5c0ad93a..137150bc195a8 100644 --- a/mypyc/ir/ops.py +++ b/mypyc/ir/ops.py @@ -1211,6 +1211,23 @@ def accept(self, visitor: "OpVisitor[T]") -> T: return visitor.visit_float_op(self) +class FloatNeg(RegisterOp): + """Float negation op (r1 = -r2).""" + + error_kind = ERR_NEVER + + def __init__(self, src: Value, line: int = -1) -> None: + super().__init__(line) + self.type = float_rprimitive + self.src = src + + def sources(self) -> List[Value]: + return [self.src] + + def accept(self, visitor: "OpVisitor[T]") -> T: + return visitor.visit_float_neg(self) + + class FloatComparisonOp(RegisterOp): """Low-level comparison op for floats.""" @@ -1494,6 +1511,10 @@ def visit_comparison_op(self, op: ComparisonOp) -> T: def visit_float_op(self, op: FloatOp) -> T: raise NotImplementedError + @abstractmethod + def visit_float_neg(self, op: FloatNeg) -> T: + raise NotImplementedError + @abstractmethod def visit_float_comparison_op(self, op: FloatComparisonOp) -> T: raise NotImplementedError diff --git a/mypyc/ir/pprint.py b/mypyc/ir/pprint.py index 639a59bd0c892..82e82913c9a67 100644 --- a/mypyc/ir/pprint.py +++ b/mypyc/ir/pprint.py @@ -25,6 +25,7 @@ Extend, Float, FloatComparisonOp, + FloatNeg, FloatOp, GetAttr, GetElementPtr, @@ -247,6 +248,9 @@ def visit_comparison_op(self, op: ComparisonOp) -> str: def visit_float_op(self, op: FloatOp) -> str: return self.format("%r = %r %s %r", op, op.lhs, FloatOp.op_str[op.op], op.rhs) + def visit_float_neg(self, op: FloatNeg) -> str: + return self.format("%r = -%r", op, op.src) + def visit_float_comparison_op(self, op: FloatComparisonOp) -> str: return self.format("%r = %r %s %r", op, op.lhs, op.op_str[op.op], op.rhs) diff --git a/mypyc/irbuild/ll_builder.py b/mypyc/irbuild/ll_builder.py index 042a1f79e2cdb..7c654c443a2ff 100644 --- a/mypyc/irbuild/ll_builder.py +++ b/mypyc/irbuild/ll_builder.py @@ -48,6 +48,7 @@ Extend, Float, FloatComparisonOp, + FloatNeg, FloatOp, GetAttr, GetElementPtr, @@ -1589,8 +1590,7 @@ def unary_op(self, value: Value, expr_op: str, line: int) -> Value: return value if is_float_rprimitive(typ): if expr_op == "-": - # Translate to '0 - x' - return self.float_op(Float(0.0), value, '-', line) + return self.add(FloatNeg(value, line)) elif expr_op == "+": return value diff --git a/mypyc/test-data/irbuild-float.test b/mypyc/test-data/irbuild-float.test index e00550818cb30..0b493fbd902f6 100644 --- a/mypyc/test-data/irbuild-float.test +++ b/mypyc/test-data/irbuild-float.test @@ -48,7 +48,7 @@ def f(x): L0: r0 = x * -0.5 y = r0 - r1 = 0.0 - y + r1 = -y return r1 [case testFloatCoerceFromInt] diff --git a/mypyc/test-data/run-floats.test b/mypyc/test-data/run-floats.test index 3706c4b8b42a0..8ff6a76817933 100644 --- a/mypyc/test-data/run-floats.test +++ b/mypyc/test-data/run-floats.test @@ -17,6 +17,8 @@ def test_arithmetic() -> None: assert x * (-0.5) == -0.75 assert -x == -1.5 for x in float_vals: + assert repr(-x) == repr(getattr(x, "__neg__")()) + for y in float_vals: assert repr(x + y) == repr(getattr(x, "__add__")(y)) assert repr(x - y) == repr(getattr(x, "__sub__")(y)) From 8dbd05afc463c9b53788fd623f8b630c6a306450 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 24 Sep 2022 09:11:56 +0100 Subject: [PATCH 18/56] More tests --- mypyc/test-data/run-floats.test | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/mypyc/test-data/run-floats.test b/mypyc/test-data/run-floats.test index 8ff6a76817933..17b9f120eebd9 100644 --- a/mypyc/test-data/run-floats.test +++ b/mypyc/test-data/run-floats.test @@ -155,6 +155,14 @@ def test_str_to_float() -> None: assert str_to_float("44324") == 44324.0 assert str_to_float("23.4") == 23.4 assert str_to_float("-43.44e-4") == -43.44e-4 + assert str_to_float("-43.44e-4") == -43.44e-4 + assert math.isinf(str_to_float("inf")) + assert math.isinf(str_to_float("-inf")) + assert str_to_float("inf") > 0.0 + assert str_to_float("-inf") < 0.0 + assert math.isnan(str_to_float("nan")) + assert math.isnan(str_to_float("NaN")) + assert repr(str_to_float("-0.0")) == "-0.0" def test_abs() -> None: assert abs(0.0) == 0.0 From fc5cd16dbfbc4509ffc4365a2732b9e6134afaf9 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 24 Sep 2022 09:12:07 +0100 Subject: [PATCH 19/56] Fix float abs --- mypyc/lib-rt/CPy.h | 1 - mypyc/lib-rt/float_ops.c | 4 ---- mypyc/primitives/float_ops.py | 2 +- mypyc/test-data/run-floats.test | 3 +++ 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/mypyc/lib-rt/CPy.h b/mypyc/lib-rt/CPy.h index f209f9875dbd0..4d3a8f5adb455 100644 --- a/mypyc/lib-rt/CPy.h +++ b/mypyc/lib-rt/CPy.h @@ -287,7 +287,6 @@ static inline bool CPyTagged_IsLe(CPyTagged left, CPyTagged right) { // Float operations -double CPyFloat_Abs(double x); double CPyFloat_Sin(double x); double CPyFloat_Cos(double x); double CPyFloat_Sqrt(double x); diff --git a/mypyc/lib-rt/float_ops.c b/mypyc/lib-rt/float_ops.c index a22b9c975feaf..e9dbf723e3f1e 100644 --- a/mypyc/lib-rt/float_ops.c +++ b/mypyc/lib-rt/float_ops.c @@ -22,10 +22,6 @@ double CPyFloat_FromTagged(CPyTagged x) { return result; } -double CPyFloat_Abs(double x) { - return x >= 0.0 ? x : -x; -} - double CPyFloat_Sin(double x) { double v = sin(x); if (unlikely(isnan(v)) && !isnan(x)) { diff --git a/mypyc/primitives/float_ops.py b/mypyc/primitives/float_ops.py index 4b71f32034a9e..c64f26fa05802 100644 --- a/mypyc/primitives/float_ops.py +++ b/mypyc/primitives/float_ops.py @@ -32,7 +32,7 @@ name="builtins.abs", arg_types=[float_rprimitive], return_type=float_rprimitive, - c_function_name="CPyFloat_Abs", + c_function_name="fabs", error_kind=ERR_NEVER, ) diff --git a/mypyc/test-data/run-floats.test b/mypyc/test-data/run-floats.test index 17b9f120eebd9..a685483471627 100644 --- a/mypyc/test-data/run-floats.test +++ b/mypyc/test-data/run-floats.test @@ -170,6 +170,9 @@ def test_abs() -> None: assert abs(44324.732) == 44324.732 assert abs(-23.4) == 23.4 assert abs(-43.44e-4) == 43.44e-4 + abs_any: Any = abs + for x in float_vals: + assert repr(abs(x)) == repr(abs_any(x)) def test_float_min_max() -> None: for x in float_vals: From 0b2a642f1e4d3900d9e99baf3806f63b5473aed9 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 24 Sep 2022 09:26:08 +0100 Subject: [PATCH 20/56] Add more test cases --- mypyc/test-data/irbuild-float.test | 21 +++++++++++++++++++-- mypyc/test-data/run-floats.test | 28 ++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/mypyc/test-data/irbuild-float.test b/mypyc/test-data/irbuild-float.test index 0b493fbd902f6..e4e7d5e66f92c 100644 --- a/mypyc/test-data/irbuild-float.test +++ b/mypyc/test-data/irbuild-float.test @@ -52,8 +52,6 @@ L0: return r1 [case testFloatCoerceFromInt] -from mypy_extensions import i64 - def from_int(x: int) -> float: return x @@ -76,6 +74,25 @@ def from_literal_neg(): L0: return -2.0 +[case testConvertBetweenFloatAndInt] +def to_int(x: float) -> int: + return int(x) +def from_int(x: int) -> float: + return float(x) +[out] +def to_int(x): + x :: float + r0 :: int +L0: + r0 = CPyTagged_FromFloat(x) + return r0 +def from_int(x): + x :: int + r0 :: float +L0: + r0 = CPyFloat_FromTagged(x) + return r0 + [case testFloatOperatorAssignment] def f(x: float, y: float) -> float: x += y diff --git a/mypyc/test-data/run-floats.test b/mypyc/test-data/run-floats.test index a685483471627..805f498c0bad3 100644 --- a/mypyc/test-data/run-floats.test +++ b/mypyc/test-data/run-floats.test @@ -142,9 +142,35 @@ def test_coerce_from_very_long_tagged_int() -> None: n = int() + 10**1000 with assertRaises(OverflowError, "int too large to convert to float"): x: float = n + with assertRaises(OverflowError, "int too large to convert to float"): + x2: float = int(n) n = int() - 10**1000 with assertRaises(OverflowError, "int too large to convert to float"): y: float = n + with assertRaises(OverflowError, "int too large to convert to float"): + y2: float = int(n) + +def test_explicit_conversion_from_int() -> None: + float_any: Any = float + a = [0, 1, 2, 3, -1, -2, 13257, -928745] + for n in range(1, 100): + for delta in -1, 0, 1, 2342345: + a.append(2**n + delta) + a.append(-2**n + delta) + for x in a: + assert repr(float(x)) == repr(float_any(x)) + +def test_explicit_conversion_to_int() -> None: + int_any: Any = int + for x in float_vals: + if math.isinf(x): + with assertRaises(OverflowError, "cannot convert float infinity to integer"): + int(x) + elif math.isnan(x): + with assertRaises(ValueError, "cannot convert float NaN to integer"): + int(x) + else: + assert repr(int(x)) == repr(int_any(x)) def str_to_float(x: str) -> float: return float(x) @@ -179,6 +205,8 @@ def test_float_min_max() -> None: for y in float_vals: min_any: Any = min assert repr(min(x, y)) == repr(min_any(x, y)) + max_any: Any = max + assert repr(max(x, y)) == repr(max_any(x, y)) def default(x: float = 2) -> float: return x + 1 From 1cb15ca10b6fc63ed0bb52151d39fbeb4b5afd0e Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 24 Sep 2022 09:49:00 +0100 Subject: [PATCH 21/56] Add primitive for math.exp --- mypyc/lib-rt/CPy.h | 1 + mypyc/lib-rt/float_ops.c | 13 +++++++++++++ mypyc/primitives/float_ops.py | 9 +++++++++ mypyc/test-data/fixtures/testutil.py | 3 +++ test-data/unit/lib-stub/math.pyi | 2 ++ 5 files changed, 28 insertions(+) diff --git a/mypyc/lib-rt/CPy.h b/mypyc/lib-rt/CPy.h index 4d3a8f5adb455..db43a7373e4a5 100644 --- a/mypyc/lib-rt/CPy.h +++ b/mypyc/lib-rt/CPy.h @@ -290,6 +290,7 @@ static inline bool CPyTagged_IsLe(CPyTagged left, CPyTagged right) { double CPyFloat_Sin(double x); double CPyFloat_Cos(double x); double CPyFloat_Sqrt(double x); +double CPyFloat_Exp(double x); double CPyFloat_FromTagged(CPyTagged x); bool CPyFloat_IsInf(double x); bool CPyFloat_IsNaN(double x); diff --git a/mypyc/lib-rt/float_ops.c b/mypyc/lib-rt/float_ops.c index e9dbf723e3f1e..b4ac143da3231 100644 --- a/mypyc/lib-rt/float_ops.c +++ b/mypyc/lib-rt/float_ops.c @@ -11,6 +11,11 @@ static double CPy_DomainError() { return CPY_FLOAT_ERROR; } +static double CPy_MathRangeError() { + PyErr_SetString(PyExc_OverflowError, "math range error"); + return CPY_FLOAT_ERROR; +} + double CPyFloat_FromTagged(CPyTagged x) { if (CPyTagged_CheckShort(x)) { return CPyTagged_ShortAsSsize_t(x); @@ -45,6 +50,14 @@ double CPyFloat_Sqrt(double x) { return sqrt(x); } +double CPyFloat_Exp(double x) { + double v = exp(x); + if (unlikely(v == INFINITY) && x != INFINITY) { + return CPy_MathRangeError(); + } + return v; +} + bool CPyFloat_IsInf(double x) { return isinf(x) != 0; } diff --git a/mypyc/primitives/float_ops.py b/mypyc/primitives/float_ops.py index c64f26fa05802..2fa2327142785 100644 --- a/mypyc/primitives/float_ops.py +++ b/mypyc/primitives/float_ops.py @@ -63,6 +63,15 @@ error_kind=ERR_MAGIC_OVERLAPPING, ) +# math.exp(float) +function_op( + name="math.exp", + arg_types=[float_rprimitive], + return_type=float_rprimitive, + c_function_name="CPyFloat_Exp", + error_kind=ERR_MAGIC_OVERLAPPING, +) + # math.copysign(float, float) copysign_op = function_op( name="math.copysign", diff --git a/mypyc/test-data/fixtures/testutil.py b/mypyc/test-data/fixtures/testutil.py index e2e7420f7b9ad..a2a06019ec760 100644 --- a/mypyc/test-data/fixtures/testutil.py +++ b/mypyc/test-data/fixtures/testutil.py @@ -32,6 +32,9 @@ def assertRaises(typ: type, msg: str = '') -> Iterator[None]: def assertDomainError() -> Any: return assertRaises(ValueError, "math domain error") +def assertMathRangeError() -> Any: + return assertRaises(OverflowError, "math range error") + T = TypeVar('T') U = TypeVar('U') V = TypeVar('V') diff --git a/test-data/unit/lib-stub/math.pyi b/test-data/unit/lib-stub/math.pyi index f70dffdfcedb1..9761e6e0d7092 100644 --- a/test-data/unit/lib-stub/math.pyi +++ b/test-data/unit/lib-stub/math.pyi @@ -1,6 +1,8 @@ def sqrt(__x: float) -> float: ... def sin(__x: float) -> float: ... def cos(__x: float) -> float: ... +def exp(__x: float) -> float: ... def copysign(__x: float, __y: float) -> float: ... def isinf(__x: float) -> bool: ... def isnan(__x: float) -> bool: ... +def isfinite(__x: float) -> bool: ... From 987d93e289660575bd3e79c308d28ba51cfe9622 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 24 Sep 2022 09:56:33 +0100 Subject: [PATCH 22/56] Add primitive for math.floor --- mypyc/lib-rt/CPy.h | 1 + mypyc/lib-rt/float_ops.c | 5 +++++ mypyc/primitives/float_ops.py | 9 +++++++++ test-data/unit/lib-stub/math.pyi | 1 + 4 files changed, 16 insertions(+) diff --git a/mypyc/lib-rt/CPy.h b/mypyc/lib-rt/CPy.h index db43a7373e4a5..e8761dbd05092 100644 --- a/mypyc/lib-rt/CPy.h +++ b/mypyc/lib-rt/CPy.h @@ -291,6 +291,7 @@ double CPyFloat_Sin(double x); double CPyFloat_Cos(double x); double CPyFloat_Sqrt(double x); double CPyFloat_Exp(double x); +CPyTagged CPyFloat_Floor(double x); double CPyFloat_FromTagged(CPyTagged x); bool CPyFloat_IsInf(double x); bool CPyFloat_IsNaN(double x); diff --git a/mypyc/lib-rt/float_ops.c b/mypyc/lib-rt/float_ops.c index b4ac143da3231..4be280199c01f 100644 --- a/mypyc/lib-rt/float_ops.c +++ b/mypyc/lib-rt/float_ops.c @@ -58,6 +58,11 @@ double CPyFloat_Exp(double x) { return v; } +CPyTagged CPyFloat_Floor(double x) { + double v = floor(x); + return CPyTagged_FromFloat(v); +} + bool CPyFloat_IsInf(double x) { return isinf(x) != 0; } diff --git a/mypyc/primitives/float_ops.py b/mypyc/primitives/float_ops.py index 2fa2327142785..3d7fc2e50cdbc 100644 --- a/mypyc/primitives/float_ops.py +++ b/mypyc/primitives/float_ops.py @@ -72,6 +72,15 @@ error_kind=ERR_MAGIC_OVERLAPPING, ) +# math.floor(float) +function_op( + name="math.floor", + arg_types=[float_rprimitive], + return_type=int_rprimitive, + c_function_name="CPyFloat_Floor", + error_kind=ERR_MAGIC, +) + # math.copysign(float, float) copysign_op = function_op( name="math.copysign", diff --git a/test-data/unit/lib-stub/math.pyi b/test-data/unit/lib-stub/math.pyi index 9761e6e0d7092..767459426ac14 100644 --- a/test-data/unit/lib-stub/math.pyi +++ b/test-data/unit/lib-stub/math.pyi @@ -2,6 +2,7 @@ def sqrt(__x: float) -> float: ... def sin(__x: float) -> float: ... def cos(__x: float) -> float: ... def exp(__x: float) -> float: ... +def floor(__x: float) -> int: ... def copysign(__x: float, __y: float) -> float: ... def isinf(__x: float) -> bool: ... def isnan(__x: float) -> bool: ... From 1054d4aa0d51e50d4e7f3a3928905888f751a1f3 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 24 Sep 2022 09:58:19 +0100 Subject: [PATCH 23/56] Add primitive for math.ceil --- mypyc/lib-rt/CPy.h | 1 + mypyc/lib-rt/float_ops.c | 5 +++++ mypyc/primitives/float_ops.py | 9 +++++++++ test-data/unit/lib-stub/math.pyi | 1 + 4 files changed, 16 insertions(+) diff --git a/mypyc/lib-rt/CPy.h b/mypyc/lib-rt/CPy.h index e8761dbd05092..b68bd213b330e 100644 --- a/mypyc/lib-rt/CPy.h +++ b/mypyc/lib-rt/CPy.h @@ -292,6 +292,7 @@ double CPyFloat_Cos(double x); double CPyFloat_Sqrt(double x); double CPyFloat_Exp(double x); CPyTagged CPyFloat_Floor(double x); +CPyTagged CPyFloat_Ceil(double x); double CPyFloat_FromTagged(CPyTagged x); bool CPyFloat_IsInf(double x); bool CPyFloat_IsNaN(double x); diff --git a/mypyc/lib-rt/float_ops.c b/mypyc/lib-rt/float_ops.c index 4be280199c01f..71071d0a73620 100644 --- a/mypyc/lib-rt/float_ops.c +++ b/mypyc/lib-rt/float_ops.c @@ -63,6 +63,11 @@ CPyTagged CPyFloat_Floor(double x) { return CPyTagged_FromFloat(v); } +CPyTagged CPyFloat_Ceil(double x) { + double v = ceil(x); + return CPyTagged_FromFloat(v); +} + bool CPyFloat_IsInf(double x) { return isinf(x) != 0; } diff --git a/mypyc/primitives/float_ops.py b/mypyc/primitives/float_ops.py index 3d7fc2e50cdbc..218f1a7dee7ed 100644 --- a/mypyc/primitives/float_ops.py +++ b/mypyc/primitives/float_ops.py @@ -81,6 +81,15 @@ error_kind=ERR_MAGIC, ) +# math.ceil(float) +function_op( + name="math.ceil", + arg_types=[float_rprimitive], + return_type=int_rprimitive, + c_function_name="CPyFloat_Ceil", + error_kind=ERR_MAGIC, +) + # math.copysign(float, float) copysign_op = function_op( name="math.copysign", diff --git a/test-data/unit/lib-stub/math.pyi b/test-data/unit/lib-stub/math.pyi index 767459426ac14..46107d88bee42 100644 --- a/test-data/unit/lib-stub/math.pyi +++ b/test-data/unit/lib-stub/math.pyi @@ -3,6 +3,7 @@ def sin(__x: float) -> float: ... def cos(__x: float) -> float: ... def exp(__x: float) -> float: ... def floor(__x: float) -> int: ... +def ceil(__x: float) -> int: ... def copysign(__x: float, __y: float) -> float: ... def isinf(__x: float) -> bool: ... def isnan(__x: float) -> bool: ... From f54e6187111e2f9c0bcb0101f655487be3db44fc Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 24 Sep 2022 10:00:13 +0100 Subject: [PATCH 24/56] Add primitive for math.fabs --- mypyc/primitives/float_ops.py | 9 +++++++++ test-data/unit/lib-stub/math.pyi | 1 + 2 files changed, 10 insertions(+) diff --git a/mypyc/primitives/float_ops.py b/mypyc/primitives/float_ops.py index 218f1a7dee7ed..e976580074628 100644 --- a/mypyc/primitives/float_ops.py +++ b/mypyc/primitives/float_ops.py @@ -90,6 +90,15 @@ error_kind=ERR_MAGIC, ) +# math.fabs(float) +function_op( + name="math.fabs", + arg_types=[float_rprimitive], + return_type=float_rprimitive, + c_function_name="fabs", + error_kind=ERR_NEVER, +) + # math.copysign(float, float) copysign_op = function_op( name="math.copysign", diff --git a/test-data/unit/lib-stub/math.pyi b/test-data/unit/lib-stub/math.pyi index 46107d88bee42..e9ac99e5f68c2 100644 --- a/test-data/unit/lib-stub/math.pyi +++ b/test-data/unit/lib-stub/math.pyi @@ -4,6 +4,7 @@ def cos(__x: float) -> float: ... def exp(__x: float) -> float: ... def floor(__x: float) -> int: ... def ceil(__x: float) -> int: ... +def fabs(__x: float) -> float: ... def copysign(__x: float, __y: float) -> float: ... def isinf(__x: float) -> bool: ... def isnan(__x: float) -> bool: ... From d7e044ea0b7f789b638aac28a4752b8672feb40c Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 24 Sep 2022 10:03:59 +0100 Subject: [PATCH 25/56] Add primitive for math.log --- mypyc/lib-rt/CPy.h | 1 + mypyc/lib-rt/float_ops.c | 7 +++++++ mypyc/primitives/float_ops.py | 9 +++++++++ test-data/unit/lib-stub/math.pyi | 1 + 4 files changed, 18 insertions(+) diff --git a/mypyc/lib-rt/CPy.h b/mypyc/lib-rt/CPy.h index b68bd213b330e..175ade8e9af7b 100644 --- a/mypyc/lib-rt/CPy.h +++ b/mypyc/lib-rt/CPy.h @@ -291,6 +291,7 @@ double CPyFloat_Sin(double x); double CPyFloat_Cos(double x); double CPyFloat_Sqrt(double x); double CPyFloat_Exp(double x); +double CPyFloat_Log(double x); CPyTagged CPyFloat_Floor(double x); CPyTagged CPyFloat_Ceil(double x); double CPyFloat_FromTagged(CPyTagged x); diff --git a/mypyc/lib-rt/float_ops.c b/mypyc/lib-rt/float_ops.c index 71071d0a73620..8fdcdb3643611 100644 --- a/mypyc/lib-rt/float_ops.c +++ b/mypyc/lib-rt/float_ops.c @@ -58,6 +58,13 @@ double CPyFloat_Exp(double x) { return v; } +double CPyFloat_Log(double x) { + if (x <= 0.0) { + return CPy_DomainError(); + } + return log(x); +} + CPyTagged CPyFloat_Floor(double x) { double v = floor(x); return CPyTagged_FromFloat(v); diff --git a/mypyc/primitives/float_ops.py b/mypyc/primitives/float_ops.py index e976580074628..e55002091516e 100644 --- a/mypyc/primitives/float_ops.py +++ b/mypyc/primitives/float_ops.py @@ -72,6 +72,15 @@ error_kind=ERR_MAGIC_OVERLAPPING, ) +# math.log(float) +function_op( + name="math.log", + arg_types=[float_rprimitive], + return_type=float_rprimitive, + c_function_name="CPyFloat_Log", + error_kind=ERR_MAGIC_OVERLAPPING, +) + # math.floor(float) function_op( name="math.floor", diff --git a/test-data/unit/lib-stub/math.pyi b/test-data/unit/lib-stub/math.pyi index e9ac99e5f68c2..e0754ef76a9e2 100644 --- a/test-data/unit/lib-stub/math.pyi +++ b/test-data/unit/lib-stub/math.pyi @@ -2,6 +2,7 @@ def sqrt(__x: float) -> float: ... def sin(__x: float) -> float: ... def cos(__x: float) -> float: ... def exp(__x: float) -> float: ... +def log(__x: float) -> float: ... def floor(__x: float) -> int: ... def ceil(__x: float) -> int: ... def fabs(__x: float) -> float: ... From 2d3b7a3be87fd39e08d68b15205a352a86fcdbef Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 24 Sep 2022 11:17:53 +0100 Subject: [PATCH 26/56] Add primitive for math.tan and math primiive run tests --- mypyc/lib-rt/CPy.h | 1 + mypyc/lib-rt/float_ops.c | 7 ++ mypyc/primitives/float_ops.py | 9 +++ mypyc/test-data/fixtures/testutil.py | 3 +- mypyc/test-data/run-math.test | 102 +++++++++++++++++++++++++++ test-data/unit/lib-stub/math.pyi | 2 + 6 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 mypyc/test-data/run-math.test diff --git a/mypyc/lib-rt/CPy.h b/mypyc/lib-rt/CPy.h index 175ade8e9af7b..7af4ec60435b1 100644 --- a/mypyc/lib-rt/CPy.h +++ b/mypyc/lib-rt/CPy.h @@ -289,6 +289,7 @@ static inline bool CPyTagged_IsLe(CPyTagged left, CPyTagged right) { double CPyFloat_Sin(double x); double CPyFloat_Cos(double x); +double CPyFloat_Tan(double x); double CPyFloat_Sqrt(double x); double CPyFloat_Exp(double x); double CPyFloat_Log(double x); diff --git a/mypyc/lib-rt/float_ops.c b/mypyc/lib-rt/float_ops.c index 8fdcdb3643611..a36b6444ced7b 100644 --- a/mypyc/lib-rt/float_ops.c +++ b/mypyc/lib-rt/float_ops.c @@ -43,6 +43,13 @@ double CPyFloat_Cos(double x) { return v; } +double CPyFloat_Tan(double x) { + if (unlikely(isinf(x))) { + return CPy_DomainError(); + } + return tan(x); +} + double CPyFloat_Sqrt(double x) { if (x < 0.0) { return CPy_DomainError(); diff --git a/mypyc/primitives/float_ops.py b/mypyc/primitives/float_ops.py index e55002091516e..22b11c39a232b 100644 --- a/mypyc/primitives/float_ops.py +++ b/mypyc/primitives/float_ops.py @@ -54,6 +54,15 @@ error_kind=ERR_MAGIC_OVERLAPPING, ) +# math.tan(float) +function_op( + name="math.tan", + arg_types=[float_rprimitive], + return_type=float_rprimitive, + c_function_name="CPyFloat_Tan", + error_kind=ERR_MAGIC_OVERLAPPING, +) + # math.sqrt(float) function_op( name="math.sqrt", diff --git a/mypyc/test-data/fixtures/testutil.py b/mypyc/test-data/fixtures/testutil.py index a2a06019ec760..0143622d9eaff 100644 --- a/mypyc/test-data/fixtures/testutil.py +++ b/mypyc/test-data/fixtures/testutil.py @@ -16,7 +16,8 @@ float(n) * 0.25 for n in range(-10, 10) ] + [ -0.0, 1.0/3.0, math.sqrt(2.0), 1.23e200, -2.34e200, 5.43e-100, -6.532e-200, - float('inf'), -float('inf'), float('nan'), FLOAT_MAGIC + float('inf'), -float('inf'), float('nan'), FLOAT_MAGIC, math.pi, 2.0 * math.pi, math.pi / 2.0, + -math.pi / 2.0 ] @contextmanager diff --git a/mypyc/test-data/run-math.test b/mypyc/test-data/run-math.test new file mode 100644 index 0000000000000..aa365ea573983 --- /dev/null +++ b/mypyc/test-data/run-math.test @@ -0,0 +1,102 @@ +# Test cases for the math module (compile and run) + +[case testMathOps] +from typing import Any +from typing_extensions import Final +import math +from testutil import assertRaises, float_vals, assertDomainError, assertMathRangeError + +pymath: Any = math + +def test_sqrt() -> None: + for x in float_vals: + if x >= 0 or math.isnan(x): + assert repr(math.sqrt(x)) == repr(pymath.sqrt(x)) + elif x < 0: + with assertDomainError(): + math.sqrt(x) + with assertDomainError(): + pymath.sqrt(x) + +def test_sin() -> None: + for x in float_vals: + if not math.isinf(x): + assert repr(math.sin(x)) == repr(pymath.sin(x)) + else: + with assertDomainError(): + math.sin(x) + with assertDomainError(): + pymath.sin(x) + +def test_cos() -> None: + for x in float_vals: + if not math.isinf(x): + assert repr(math.cos(x)) == repr(pymath.cos(x)) + else: + with assertDomainError(): + math.cos(x) + with assertDomainError(): + pymath.cos(x) + +def test_tan() -> None: + for x in float_vals: + if math.isinf(x): + with assertDomainError(): + math.tan(x) + else: + assert repr(math.tan(x)) == repr(pymath.tan(x)) + +def test_exp() -> None: + for x in float_vals: + if math.isfinite(x) and x > 1e100: + with assertMathRangeError(): + math.exp(x) + else: + assert repr(math.exp(x)) == repr(pymath.exp(x)) + +def test_log() -> None: + for x in float_vals: + if x <= 0.0: + with assertDomainError(): + math.log(x) + else: + assert repr(math.log(x)) == repr(pymath.log(x)) + +def test_floor() -> None: + for x in float_vals: + if math.isinf(x): + with assertRaises(OverflowError, "cannot convert float infinity to integer"): + math.floor(x) + elif math.isnan(x): + with assertRaises(ValueError, "cannot convert float NaN to integer"): + math.floor(x) + else: + assert repr(math.floor(x)) == repr(pymath.floor(x)) + +def test_ceil() -> None: + for x in float_vals: + if math.isinf(x): + with assertRaises(OverflowError, "cannot convert float infinity to integer"): + math.ceil(x) + elif math.isnan(x): + with assertRaises(ValueError, "cannot convert float NaN to integer"): + math.ceil(x) + else: + assert repr(math.ceil(x)) == repr(pymath.ceil(x)) + +def test_fabs() -> None: + for x in float_vals: + assert repr(math.fabs(x)) == repr(pymath.fabs(x)) + +def test_copysign() -> None: + for x in float_vals: + for y in float_vals: + assert repr(math.copysign(x, y)) == repr(pymath.copysign(x, y)) + +def test_isinf() -> None: + for x in float_vals: + assert repr(math.isinf(x)) == repr(pymath.isinf(x)) + +def test_isnan() -> None: + for x in float_vals: + assert repr(math.isnan(x)) == repr(pymath.isnan(x)) diff --git a/test-data/unit/lib-stub/math.pyi b/test-data/unit/lib-stub/math.pyi index e0754ef76a9e2..daefad084a080 100644 --- a/test-data/unit/lib-stub/math.pyi +++ b/test-data/unit/lib-stub/math.pyi @@ -1,6 +1,8 @@ +pi: float def sqrt(__x: float) -> float: ... def sin(__x: float) -> float: ... def cos(__x: float) -> float: ... +def tan(__x: float) -> float: ... def exp(__x: float) -> float: ... def log(__x: float) -> float: ... def floor(__x: float) -> int: ... From 289bc292d7022810d30c30c376599c355734fb5c Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 24 Sep 2022 11:21:33 +0100 Subject: [PATCH 27/56] Add more float values to test --- mypyc/test-data/fixtures/testutil.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mypyc/test-data/fixtures/testutil.py b/mypyc/test-data/fixtures/testutil.py index 0143622d9eaff..8fb29d40db7ae 100644 --- a/mypyc/test-data/fixtures/testutil.py +++ b/mypyc/test-data/fixtures/testutil.py @@ -17,7 +17,11 @@ ] + [ -0.0, 1.0/3.0, math.sqrt(2.0), 1.23e200, -2.34e200, 5.43e-100, -6.532e-200, float('inf'), -float('inf'), float('nan'), FLOAT_MAGIC, math.pi, 2.0 * math.pi, math.pi / 2.0, - -math.pi / 2.0 + -math.pi / 2.0, + -1.7976931348623158e+308, + -2.2250738585072014e-308, + 1.7976931348623158e+308, + 2.2250738585072014e-308, ] @contextmanager From 2de8e0b602c8f683016dd556ec46e1798c983546 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 24 Sep 2022 11:21:43 +0100 Subject: [PATCH 28/56] Black --- mypyc/irbuild/ll_builder.py | 2 +- mypyc/primitives/float_ops.py | 8 +++++++- mypyc/primitives/int_ops.py | 5 +++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/mypyc/irbuild/ll_builder.py b/mypyc/irbuild/ll_builder.py index 7c654c443a2ff..5f6d7fda39711 100644 --- a/mypyc/irbuild/ll_builder.py +++ b/mypyc/irbuild/ll_builder.py @@ -1973,7 +1973,7 @@ def float_mod(self, lhs: Value, rhs: Value, line: int) -> Value: same_signs = self.is_same_float_signs(type, lhs, rhs, line) self.add(Branch(same_signs, done, adjust, Branch.BOOL)) self.activate_block(adjust) - adj = self.float_op(res, rhs, '+', line) + adj = self.float_op(res, rhs, "+", line) self.add(Assign(res, adj)) self.add(Goto(done)) self.activate_block(copysign) diff --git a/mypyc/primitives/float_ops.py b/mypyc/primitives/float_ops.py index 22b11c39a232b..78d2576e9c476 100644 --- a/mypyc/primitives/float_ops.py +++ b/mypyc/primitives/float_ops.py @@ -3,7 +3,13 @@ from __future__ import annotations from mypyc.ir.ops import ERR_MAGIC, ERR_MAGIC_OVERLAPPING, ERR_NEVER, ERR_MAGIC_OVERLAPPING -from mypyc.ir.rtypes import float_rprimitive, int_rprimitive, object_rprimitive, str_rprimitive, bool_rprimitive +from mypyc.ir.rtypes import ( + float_rprimitive, + int_rprimitive, + object_rprimitive, + str_rprimitive, + bool_rprimitive, +) from mypyc.primitives.registry import function_op, load_address_op # Get the 'builtins.float' type object. diff --git a/mypyc/primitives/int_ops.py b/mypyc/primitives/int_ops.py index 711306a1dab85..13dca720eba23 100644 --- a/mypyc/primitives/int_ops.py +++ b/mypyc/primitives/int_ops.py @@ -126,8 +126,9 @@ def int_binary_op( int_binary_op(">>", "CPyTagged_Rshift", error_kind=ERR_MAGIC) int_binary_op("<<", "CPyTagged_Lshift", error_kind=ERR_MAGIC) -int_binary_op("/", "CPyTagged_TrueDivide", return_type=float_rprimitive, - error_kind=ERR_MAGIC_OVERLAPPING) +int_binary_op( + "/", "CPyTagged_TrueDivide", return_type=float_rprimitive, error_kind=ERR_MAGIC_OVERLAPPING +) # This should work because assignment operators are parsed differently # and the code in irbuild that handles it does the assignment From 2141f4dd474bd7ba26a977243a60fbfab67148f0 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 24 Sep 2022 13:18:56 +0100 Subject: [PATCH 29/56] Add primitive for float floor division --- mypyc/lib-rt/CPy.h | 1 + mypyc/lib-rt/float_ops.c | 49 ++++++++++++++++++++++++++++++ mypyc/primitives/float_ops.py | 10 +++++- mypyc/test-data/fixtures/ir.py | 1 + mypyc/test-data/irbuild-float.test | 20 ++++++++++++ mypyc/test-data/run-floats.test | 9 ++++++ 6 files changed, 89 insertions(+), 1 deletion(-) diff --git a/mypyc/lib-rt/CPy.h b/mypyc/lib-rt/CPy.h index 7af4ec60435b1..2824c023582f8 100644 --- a/mypyc/lib-rt/CPy.h +++ b/mypyc/lib-rt/CPy.h @@ -287,6 +287,7 @@ static inline bool CPyTagged_IsLe(CPyTagged left, CPyTagged right) { // Float operations +double CPyFloat_FloorDivide(double x, double y); double CPyFloat_Sin(double x); double CPyFloat_Cos(double x); double CPyFloat_Tan(double x); diff --git a/mypyc/lib-rt/float_ops.c b/mypyc/lib-rt/float_ops.c index a36b6444ced7b..1452636597ef1 100644 --- a/mypyc/lib-rt/float_ops.c +++ b/mypyc/lib-rt/float_ops.c @@ -89,3 +89,52 @@ bool CPyFloat_IsInf(double x) { bool CPyFloat_IsNaN(double x) { return isnan(x) != 0; } + +// From CPython 3.10.0, Objects/floatobject.c +static void +_float_div_mod(double vx, double wx, double *floordiv, double *mod) +{ + double div; + *mod = fmod(vx, wx); + /* fmod is typically exact, so vx-mod is *mathematically* an + exact multiple of wx. But this is fp arithmetic, and fp + vx - mod is an approximation; the result is that div may + not be an exact integral value after the division, although + it will always be very close to one. + */ + div = (vx - *mod) / wx; + if (*mod) { + /* ensure the remainder has the same sign as the denominator */ + if ((wx < 0) != (*mod < 0)) { + *mod += wx; + div -= 1.0; + } + } + else { + /* the remainder is zero, and in the presence of signed zeroes + fmod returns different results across platforms; ensure + it has the same sign as the denominator. */ + *mod = copysign(0.0, wx); + } + /* snap quotient to nearest integral value */ + if (div) { + *floordiv = floor(div); + if (div - *floordiv > 0.5) { + *floordiv += 1.0; + } + } + else { + /* div is zero - get the same sign as the true quotient */ + *floordiv = copysign(0.0, vx / wx); /* zero w/ sign of vx/wx */ + } +} + +double CPyFloat_FloorDivide(double x, double y) { + double mod, floordiv; + if (y == 0) { + PyErr_SetString(PyExc_ZeroDivisionError, "float floor division by zero"); + return CPY_FLOAT_ERROR; + } + _float_div_mod(x, y, &floordiv, &mod); + return floordiv; +} diff --git a/mypyc/primitives/float_ops.py b/mypyc/primitives/float_ops.py index 78d2576e9c476..51e851a47e654 100644 --- a/mypyc/primitives/float_ops.py +++ b/mypyc/primitives/float_ops.py @@ -10,11 +10,19 @@ str_rprimitive, bool_rprimitive, ) -from mypyc.primitives.registry import function_op, load_address_op +from mypyc.primitives.registry import function_op, load_address_op, binary_op # Get the 'builtins.float' type object. load_address_op(name="builtins.float", type=object_rprimitive, src="PyFloat_Type") +binary_op( + name="//", + arg_types=[float_rprimitive, float_rprimitive], + return_type=float_rprimitive, + c_function_name="CPyFloat_FloorDivide", + error_kind=ERR_MAGIC_OVERLAPPING, +) + # float(int) int_to_float_op = function_op( name="builtins.float", diff --git a/mypyc/test-data/fixtures/ir.py b/mypyc/test-data/fixtures/ir.py index c3da90b87c25a..f6e934ac90bb9 100644 --- a/mypyc/test-data/fixtures/ir.py +++ b/mypyc/test-data/fixtures/ir.py @@ -116,6 +116,7 @@ def __sub__(self, n: float) -> float: pass def __rsub__(self, n: float) -> float: pass def __mul__(self, n: float) -> float: pass def __truediv__(self, n: float) -> float: pass + def __floordiv__(self, n: float) -> float: pass def __mod__(self, n: float) -> float: pass def __pow__(self, n: float) -> float: pass def __neg__(self) -> float: pass diff --git a/mypyc/test-data/irbuild-float.test b/mypyc/test-data/irbuild-float.test index e4e7d5e66f92c..3f4f2884e0af3 100644 --- a/mypyc/test-data/irbuild-float.test +++ b/mypyc/test-data/irbuild-float.test @@ -401,3 +401,23 @@ L5: r3 = r9 L6: return r3 + +[case testFloatFloorDivide] +def f(x: float, y: float) -> float: + return x // y +def g(x: float, y: int) -> float: + return x // y +[out] +def f(x, y): + x, y, r0 :: float +L0: + r0 = CPyFloat_FloorDivide(x, y) + return r0 +def g(x, y): + x :: float + y :: int + r0, r1 :: float +L0: + r0 = CPyFloat_FromTagged(y) + r1 = CPyFloat_FloorDivide(x, r0) + return r1 diff --git a/mypyc/test-data/run-floats.test b/mypyc/test-data/run-floats.test index 805f498c0bad3..13964532e40bb 100644 --- a/mypyc/test-data/run-floats.test +++ b/mypyc/test-data/run-floats.test @@ -39,6 +39,15 @@ def test_mod() -> None: if y != 0: assert repr(x % y) == repr(getattr(x, "__mod__")(y)) +def test_floor_div() -> None: + for x in float_vals: + for y in float_vals: + if y != 0: + assert repr(x // y) == repr(getattr(x, "__floordiv__")(y)) + else: + with assertRaises(ZeroDivisionError, "float floor division by zero"): + x // y + def test_mixed_arithmetic() -> None: zf = float(0.0) zn = int() From c98c0433a32085193a711c2e98c5108e9688cf81 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 24 Sep 2022 14:26:05 +0100 Subject: [PATCH 30/56] Refactor test cases --- mypyc/test-data/run-math.test | 88 ++++++++++------------------------- 1 file changed, 25 insertions(+), 63 deletions(-) diff --git a/mypyc/test-data/run-math.test b/mypyc/test-data/run-math.test index aa365ea573983..42af782a1c57d 100644 --- a/mypyc/test-data/run-math.test +++ b/mypyc/test-data/run-math.test @@ -1,92 +1,54 @@ # Test cases for the math module (compile and run) [case testMathOps] -from typing import Any +from typing import Any, Callable from typing_extensions import Final import math from testutil import assertRaises, float_vals, assertDomainError, assertMathRangeError pymath: Any = math -def test_sqrt() -> None: +def validate_one_arg(test: Callable[[float], float], validate: Callable[[float], float]) -> None: for x in float_vals: - if x >= 0 or math.isnan(x): - assert repr(math.sqrt(x)) == repr(pymath.sqrt(x)) - elif x < 0: - with assertDomainError(): - math.sqrt(x) - with assertDomainError(): - pymath.sqrt(x) + try: + expected = validate(x) + except Exception as e: + try: + test(x) + assert False, f"no exception raised for {x!r}, expected {e!r}" + except Exception as e2: + assert repr(e) == repr(e2), f"actual for {x!r}: {e2!r}, expected: {e!r}" + continue + actual = test(x) + assert repr(actual) == repr(expected), ( + f"actual for {x!r}: {actual!r}, expected {expected!r}") + +def test_sqrt() -> None: + validate_one_arg(lambda x: math.sqrt(x), pymath.sqrt) def test_sin() -> None: - for x in float_vals: - if not math.isinf(x): - assert repr(math.sin(x)) == repr(pymath.sin(x)) - else: - with assertDomainError(): - math.sin(x) - with assertDomainError(): - pymath.sin(x) + validate_one_arg(lambda x: math.sin(x), pymath.sin) def test_cos() -> None: - for x in float_vals: - if not math.isinf(x): - assert repr(math.cos(x)) == repr(pymath.cos(x)) - else: - with assertDomainError(): - math.cos(x) - with assertDomainError(): - pymath.cos(x) + validate_one_arg(lambda x: math.cos(x), pymath.cos) def test_tan() -> None: - for x in float_vals: - if math.isinf(x): - with assertDomainError(): - math.tan(x) - else: - assert repr(math.tan(x)) == repr(pymath.tan(x)) + validate_one_arg(lambda x: math.tan(x), pymath.tan) def test_exp() -> None: - for x in float_vals: - if math.isfinite(x) and x > 1e100: - with assertMathRangeError(): - math.exp(x) - else: - assert repr(math.exp(x)) == repr(pymath.exp(x)) + validate_one_arg(lambda x: math.exp(x), pymath.exp) def test_log() -> None: - for x in float_vals: - if x <= 0.0: - with assertDomainError(): - math.log(x) - else: - assert repr(math.log(x)) == repr(pymath.log(x)) + validate_one_arg(lambda x: math.log(x), pymath.log) def test_floor() -> None: - for x in float_vals: - if math.isinf(x): - with assertRaises(OverflowError, "cannot convert float infinity to integer"): - math.floor(x) - elif math.isnan(x): - with assertRaises(ValueError, "cannot convert float NaN to integer"): - math.floor(x) - else: - assert repr(math.floor(x)) == repr(pymath.floor(x)) + validate_one_arg(lambda x: math.floor(x), pymath.floor) def test_ceil() -> None: - for x in float_vals: - if math.isinf(x): - with assertRaises(OverflowError, "cannot convert float infinity to integer"): - math.ceil(x) - elif math.isnan(x): - with assertRaises(ValueError, "cannot convert float NaN to integer"): - math.ceil(x) - else: - assert repr(math.ceil(x)) == repr(pymath.ceil(x)) + validate_one_arg(lambda x: math.ceil(x), pymath.ceil) def test_fabs() -> None: - for x in float_vals: - assert repr(math.fabs(x)) == repr(pymath.fabs(x)) + validate_one_arg(lambda x: math.fabs(x), pymath.fabs) def test_copysign() -> None: for x in float_vals: From 40e991aa50a5baf77c51acf178c1f34216daffb2 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 24 Sep 2022 17:20:50 +0100 Subject: [PATCH 31/56] Add primitive for math.pow --- mypyc/lib-rt/float_ops.c | 50 ++++++++++++++++++++++++++++++++ mypyc/primitives/float_ops.py | 9 ++++++ mypyc/test-data/run-math.test | 24 +++++++++++++++ test-data/unit/lib-stub/math.pyi | 1 + 4 files changed, 84 insertions(+) diff --git a/mypyc/lib-rt/float_ops.c b/mypyc/lib-rt/float_ops.c index 1452636597ef1..aac91df98b79c 100644 --- a/mypyc/lib-rt/float_ops.c +++ b/mypyc/lib-rt/float_ops.c @@ -138,3 +138,53 @@ double CPyFloat_FloorDivide(double x, double y) { _float_div_mod(x, y, &floordiv, &mod); return floordiv; } + +// Adapted from CPython 3.10.7 +double CPyFloat_Pow(double x, double y) { + if (!isfinite(x) || !isfinite(y)) { + if (isnan(x)) + return y == 0.0 ? 1.0 : x; /* NaN**0 = 1 */ + else if (isnan(y)) + return x == 1.0 ? 1.0 : y; /* 1**NaN = 1 */ + else if (isinf(x)) { + int odd_y = isfinite(y) && fmod(fabs(y), 2.0) == 1.0; + if (y > 0.0) + return odd_y ? x : fabs(x); + else if (y == 0.0) + return 1.0; + else /* y < 0. */ + return odd_y ? copysign(0.0, x) : 0.0; + } + else if (isinf(y)) { + if (fabs(x) == 1.0) + return 1.0; + else if (y > 0.0 && fabs(x) > 1.0) + return y; + else if (y < 0.0 && fabs(x) < 1.0) { + if (x == 0.0) { /* 0**-inf: divide-by-zero */ + return CPy_DomainError(); + } + return -y; /* result is +inf */ + } else + return 0.0; + } + } + double r = pow(x, y); + if (!isfinite(r)) { + if (isnan(r)) { + return CPy_DomainError(); + } + /* + an infinite result here arises either from: + (A) (+/-0.)**negative (-> divide-by-zero) + (B) overflow of x**y with x and y finite + */ + else if (isinf(r)) { + if (x == 0.0) + return CPy_DomainError(); + else + return CPy_MathRangeError(); + } + } + return r; +} diff --git a/mypyc/primitives/float_ops.py b/mypyc/primitives/float_ops.py index 51e851a47e654..5433fb31987d5 100644 --- a/mypyc/primitives/float_ops.py +++ b/mypyc/primitives/float_ops.py @@ -131,6 +131,15 @@ error_kind=ERR_NEVER, ) +# math.pow(float, float) +pow_op = function_op( + name="math.pow", + arg_types=[float_rprimitive, float_rprimitive], + return_type=float_rprimitive, + c_function_name="CPyFloat_Pow", + error_kind=ERR_MAGIC_OVERLAPPING, +) + # math.copysign(float, float) copysign_op = function_op( name="math.copysign", diff --git a/mypyc/test-data/run-math.test b/mypyc/test-data/run-math.test index 42af782a1c57d..e1d497a65d310 100644 --- a/mypyc/test-data/run-math.test +++ b/mypyc/test-data/run-math.test @@ -23,6 +23,27 @@ def validate_one_arg(test: Callable[[float], float], validate: Callable[[float], assert repr(actual) == repr(expected), ( f"actual for {x!r}: {actual!r}, expected {expected!r}") +def validate_two_arg(test: Callable[[float, float], float], + validate: Callable[[float, float], float]) -> None: + for x in float_vals: + for y in float_vals: + args = f"({x!r}, {y!r})" + try: + expected = validate(x, y) + except Exception as e: + try: + test(x, y) + assert False, f"no exception raised for {args}, expected {e!r}" + except Exception as e2: + assert repr(e) == repr(e2), f"actual for {args}: {e2!r}, expected: {e!r}" + continue + try: + actual = test(x, y) + except Exception as e: + assert False, f"no exception expected for {args}, got {e!r}" + assert repr(actual) == repr(expected), ( + f"actual for {args}: {actual!r}, expected {expected!r}") + def test_sqrt() -> None: validate_one_arg(lambda x: math.sqrt(x), pymath.sqrt) @@ -50,6 +71,9 @@ def test_ceil() -> None: def test_fabs() -> None: validate_one_arg(lambda x: math.fabs(x), pymath.fabs) +def test_pow() -> None: + validate_two_arg(lambda x, y: math.pow(x, y), pymath.pow) + def test_copysign() -> None: for x in float_vals: for y in float_vals: diff --git a/test-data/unit/lib-stub/math.pyi b/test-data/unit/lib-stub/math.pyi index daefad084a080..85f3b3f169e10 100644 --- a/test-data/unit/lib-stub/math.pyi +++ b/test-data/unit/lib-stub/math.pyi @@ -8,6 +8,7 @@ def log(__x: float) -> float: ... def floor(__x: float) -> int: ... def ceil(__x: float) -> int: ... def fabs(__x: float) -> float: ... +def pow(__x: float, __y: float) -> float: ... def copysign(__x: float, __y: float) -> float: ... def isinf(__x: float) -> bool: ... def isnan(__x: float) -> bool: ... From fd117e66d29bc539375cda03b5fc65142bbd2984 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 24 Sep 2022 17:22:17 +0100 Subject: [PATCH 32/56] Refactor test case --- mypyc/test-data/run-math.test | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mypyc/test-data/run-math.test b/mypyc/test-data/run-math.test index e1d497a65d310..ef1336acb6454 100644 --- a/mypyc/test-data/run-math.test +++ b/mypyc/test-data/run-math.test @@ -75,9 +75,7 @@ def test_pow() -> None: validate_two_arg(lambda x, y: math.pow(x, y), pymath.pow) def test_copysign() -> None: - for x in float_vals: - for y in float_vals: - assert repr(math.copysign(x, y)) == repr(pymath.copysign(x, y)) + validate_two_arg(lambda x, y: math.copysign(x, y), pymath.copysign) def test_isinf() -> None: for x in float_vals: From d721828ffdd6335085c89cf0e8c3ae058a80c11d Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 24 Sep 2022 17:23:37 +0100 Subject: [PATCH 33/56] Add docstrings --- mypyc/test-data/run-math.test | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mypyc/test-data/run-math.test b/mypyc/test-data/run-math.test index ef1336acb6454..64d5c1812afad 100644 --- a/mypyc/test-data/run-math.test +++ b/mypyc/test-data/run-math.test @@ -9,6 +9,7 @@ from testutil import assertRaises, float_vals, assertDomainError, assertMathRang pymath: Any = math def validate_one_arg(test: Callable[[float], float], validate: Callable[[float], float]) -> None: + """Ensure that test and validate behave the same for various float args.""" for x in float_vals: try: expected = validate(x) @@ -25,6 +26,7 @@ def validate_one_arg(test: Callable[[float], float], validate: Callable[[float], def validate_two_arg(test: Callable[[float, float], float], validate: Callable[[float, float], float]) -> None: + """Ensure that test and validate behave the same for various float args.""" for x in float_vals: for y in float_vals: args = f"({x!r}, {y!r})" From 6205767a52ecdf5dac485615952d8487a5b8d2f7 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 31 Dec 2022 17:14:02 +0000 Subject: [PATCH 34/56] Fix test cases --- mypyc/lib-rt/CPy.h | 1 + mypyc/test-data/irbuild-any.test | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/mypyc/lib-rt/CPy.h b/mypyc/lib-rt/CPy.h index 2824c023582f8..e01acec4d2f09 100644 --- a/mypyc/lib-rt/CPy.h +++ b/mypyc/lib-rt/CPy.h @@ -288,6 +288,7 @@ static inline bool CPyTagged_IsLe(CPyTagged left, CPyTagged right) { double CPyFloat_FloorDivide(double x, double y); +double CPyFloat_Pow(double x, double y); double CPyFloat_Sin(double x); double CPyFloat_Cos(double x); double CPyFloat_Tan(double x); diff --git a/mypyc/test-data/irbuild-any.test b/mypyc/test-data/irbuild-any.test index 6c245cea923ff..31eadfe28c017 100644 --- a/mypyc/test-data/irbuild-any.test +++ b/mypyc/test-data/irbuild-any.test @@ -193,7 +193,7 @@ L0: r1 = PyNumber_Absolute(r0) r2 = unbox(int, r1) a = r2 - r3 = CPyFloat_Abs(1.1) + r3 = fabs(1.1) b = r3 return 1 From 9a4db2d57b140552357fa31b61eb6144273d51c4 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 31 Dec 2022 17:26:47 +0000 Subject: [PATCH 35/56] Fix math.pow on Python 3.11 --- mypyc/lib-rt/float_ops.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mypyc/lib-rt/float_ops.c b/mypyc/lib-rt/float_ops.c index aac91df98b79c..f9ff47848dc6e 100644 --- a/mypyc/lib-rt/float_ops.c +++ b/mypyc/lib-rt/float_ops.c @@ -161,9 +161,11 @@ double CPyFloat_Pow(double x, double y) { else if (y > 0.0 && fabs(x) > 1.0) return y; else if (y < 0.0 && fabs(x) < 1.0) { + #if PY_VERSION_HEX < 0x030B0000 if (x == 0.0) { /* 0**-inf: divide-by-zero */ return CPy_DomainError(); } + #endif return -y; /* result is +inf */ } else return 0.0; From 4c893370727e33a5ef5434abf4d02f8a7c7cd72b Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Fri, 27 Jan 2023 11:48:23 +0000 Subject: [PATCH 36/56] Update float to native int conversion tests --- mypyc/test-data/irbuild-i32.test | 30 +++++++++++++++++++++++++----- mypyc/test-data/irbuild-i64.test | 28 +++++++++++++++++++++++----- 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/mypyc/test-data/irbuild-i32.test b/mypyc/test-data/irbuild-i32.test index 7ea3c08647285..a5d69fb55ee09 100644 --- a/mypyc/test-data/irbuild-i32.test +++ b/mypyc/test-data/irbuild-i32.test @@ -526,9 +526,29 @@ L0: return r0 def float_to_i32(x): x :: float - r0 :: object - r1 :: int32 + r0 :: int + r1 :: native_int + r2, r3, r4 :: bit + r5 :: native_int + r6, r7 :: int32 L0: - r0 = CPyLong_FromFloat(x) - r1 = unbox(int32, r0) - return r1 + r0 = CPyTagged_FromFloat(x) + r1 = r0 & 1 + r2 = r1 == 0 + if r2 goto L1 else goto L4 :: bool +L1: + r3 = r0 < 4294967296 :: signed + if r3 goto L2 else goto L4 :: bool +L2: + r4 = r0 >= -4294967296 :: signed + if r4 goto L3 else goto L4 :: bool +L3: + r5 = r0 >> 1 + r6 = truncate r5: native_int to int32 + r7 = r6 + goto L5 +L4: + CPyInt32_Overflow() + unreachable +L5: + return r7 diff --git a/mypyc/test-data/irbuild-i64.test b/mypyc/test-data/irbuild-i64.test index f616893d8fe53..9c12b10b35626 100644 --- a/mypyc/test-data/irbuild-i64.test +++ b/mypyc/test-data/irbuild-i64.test @@ -1900,12 +1900,30 @@ L0: return r0 def float_to_i64(x): x :: float - r0 :: object - r1 :: int64 + r0 :: int + r1 :: native_int + r2 :: bit + r3, r4 :: int64 + r5 :: ptr + r6 :: c_ptr + r7 :: int64 L0: - r0 = CPyLong_FromFloat(x) - r1 = unbox(int64, r0) - return r1 + r0 = CPyTagged_FromFloat(x) + r1 = r0 & 1 + r2 = r1 == 0 + if r2 goto L1 else goto L2 :: bool +L1: + r3 = r0 >> 1 + r4 = r3 + goto L3 +L2: + r5 = r0 ^ 1 + r6 = r5 + r7 = CPyLong_AsInt64(r6) + r4 = r7 + keep_alive r0 +L3: + return r4 [case testI64IsinstanceNarrowing] from typing import Union From 71d8568a17b5ceed29cf58e1ccbcbe3b97c51ac5 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Fri, 27 Jan 2023 11:51:57 +0000 Subject: [PATCH 37/56] Update i64 test case --- mypyc/test-data/run-i64.test | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypyc/test-data/run-i64.test b/mypyc/test-data/run-i64.test index cd4ac19532d25..bcde39fed5ff1 100644 --- a/mypyc/test-data/run-i64.test +++ b/mypyc/test-data/run-i64.test @@ -315,7 +315,8 @@ def test_explicit_conversion_from_float() -> None: assert from_float(0.0) == 0 assert from_float(1.456) == 1 assert from_float(-1234.567) == -1234 - assert from_float(2**63 - 1) == 2**63 - 1 + # Subtract 1024 due to limited precision of 64-bit floats + assert from_float(2**63 - 1024) == 2**63 - 1024 assert from_float(-2**63) == -2**63 # The error message could be better, but this is acceptable with assertRaises(OverflowError, "int too large to convert to i64"): From 1462b9ff4f42fe6a585de04fcd14c71be6ed4f1f Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Fri, 27 Jan 2023 12:07:13 +0000 Subject: [PATCH 38/56] Update test case on Python 3.11 (not sure why this changed) --- mypyc/test-data/run-classes.test | 1 - 1 file changed, 1 deletion(-) diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index 268e07f6bde4c..cddaac13751ad 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -1853,7 +1853,6 @@ Represents a sequence of values. Updates itself by next, which is a new value. Traceback (most recent call last): File "driver.py", line 5, in print (x.rankine) - ^^^^^^^^^ File "native.py", line 16, in rankine raise NotImplementedError NotImplementedError From 0d710b1a1f306b51c9154bf23d6162bef6b2c024 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Fri, 27 Jan 2023 12:13:20 +0000 Subject: [PATCH 39/56] isort and lint --- mypyc/analysis/ircheck.py | 5 +++-- mypyc/irbuild/ll_builder.py | 4 ++-- mypyc/irbuild/specialize.py | 2 +- mypyc/primitives/float_ops.py | 6 +++--- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/mypyc/analysis/ircheck.py b/mypyc/analysis/ircheck.py index 477c968549fb6..2e6b7320e8987 100644 --- a/mypyc/analysis/ircheck.py +++ b/mypyc/analysis/ircheck.py @@ -46,6 +46,7 @@ TupleSet, Unbox, Unreachable, + Value, ) from mypyc.ir.pprint import format_func from mypyc.ir.rtypes import ( @@ -57,13 +58,13 @@ bytes_rprimitive, dict_rprimitive, int_rprimitive, + is_float_rprimitive, is_object_rprimitive, list_rprimitive, range_rprimitive, set_rprimitive, str_rprimitive, tuple_rprimitive, - is_float_rprimitive, ) @@ -231,7 +232,7 @@ def expect_float(self, op: Op, v: Value) -> None: def expect_non_float(self, op: Op, v: Value) -> None: if is_float_rprimitive(v.type): - self.fail(op, f"Float not expected") + self.fail(op, "Float not expected") def visit_goto(self, op: Goto) -> None: self.check_control_op_targets(op) diff --git a/mypyc/irbuild/ll_builder.py b/mypyc/irbuild/ll_builder.py index 5f6d7fda39711..3d222b8107d18 100644 --- a/mypyc/irbuild/ll_builder.py +++ b/mypyc/irbuild/ll_builder.py @@ -93,6 +93,7 @@ c_pyssize_t_rprimitive, c_size_t_rprimitive, dict_rprimitive, + float_rprimitive, int_rprimitive, is_bit_rprimitive, is_bool_rprimitive, @@ -118,7 +119,6 @@ pointer_rprimitive, short_int_rprimitive, str_rprimitive, - float_rprimitive, ) from mypyc.irbuild.mapper import Mapper from mypyc.irbuild.util import concrete_arg_kind @@ -131,7 +131,7 @@ dict_update_in_display_op, ) from mypyc.primitives.exc_ops import err_occurred_op, keep_propagating_op -from mypyc.primitives.float_ops import int_to_float_op, copysign_op +from mypyc.primitives.float_ops import copysign_op, int_to_float_op from mypyc.primitives.generic_ops import ( generic_len_op, generic_ssize_t_len_op, diff --git a/mypyc/irbuild/specialize.py b/mypyc/irbuild/specialize.py index 39fbf0f6f242e..ff9df0cd597b0 100644 --- a/mypyc/irbuild/specialize.py +++ b/mypyc/irbuild/specialize.py @@ -55,6 +55,7 @@ is_bool_rprimitive, is_dict_rprimitive, is_fixed_width_rtype, + is_float_rprimitive, is_int32_rprimitive, is_int64_rprimitive, is_int_rprimitive, @@ -62,7 +63,6 @@ list_rprimitive, set_rprimitive, str_rprimitive, - is_float_rprimitive, ) from mypyc.irbuild.builder import IRBuilder from mypyc.irbuild.for_helpers import ( diff --git a/mypyc/primitives/float_ops.py b/mypyc/primitives/float_ops.py index 5433fb31987d5..14e8d4caf09cf 100644 --- a/mypyc/primitives/float_ops.py +++ b/mypyc/primitives/float_ops.py @@ -2,15 +2,15 @@ from __future__ import annotations -from mypyc.ir.ops import ERR_MAGIC, ERR_MAGIC_OVERLAPPING, ERR_NEVER, ERR_MAGIC_OVERLAPPING +from mypyc.ir.ops import ERR_MAGIC, ERR_MAGIC_OVERLAPPING, ERR_NEVER from mypyc.ir.rtypes import ( + bool_rprimitive, float_rprimitive, int_rprimitive, object_rprimitive, str_rprimitive, - bool_rprimitive, ) -from mypyc.primitives.registry import function_op, load_address_op, binary_op +from mypyc.primitives.registry import binary_op, function_op, load_address_op # Get the 'builtins.float' type object. load_address_op(name="builtins.float", type=object_rprimitive, src="PyFloat_Type") From 4b4b7a1a21fbde1866a567f805c73d410b7ef00c Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 28 Jan 2023 17:36:55 +0000 Subject: [PATCH 40/56] Test coercing from bool to float --- mypyc/test-data/run-floats.test | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mypyc/test-data/run-floats.test b/mypyc/test-data/run-floats.test index 13964532e40bb..7aa552ef03d5f 100644 --- a/mypyc/test-data/run-floats.test +++ b/mypyc/test-data/run-floats.test @@ -118,6 +118,11 @@ def test_boxing_and_unboxing() -> None: yy: float = bb assert repr(xx) == repr(bb) assert repr(xx) == repr(yy) + for b in True, False: + boxed_bool: Any = b + assert type(boxed_bool) is bool + zz: float = boxed_bool + assert zz == int(b) def test_unboxing_failure() -> None: boxed: Any = '1.5' From 72db9fcd8b88aa962298a5f7220fc35a0c77c3ab Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 28 Jan 2023 17:44:03 +0000 Subject: [PATCH 41/56] Test floats in properties, glue methods and traits --- mypyc/test-data/run-floats.test | 168 ++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) diff --git a/mypyc/test-data/run-floats.test b/mypyc/test-data/run-floats.test index 7aa552ef03d5f..67bd571103253 100644 --- a/mypyc/test-data/run-floats.test +++ b/mypyc/test-data/run-floats.test @@ -330,3 +330,171 @@ def test_undefined_local_var() -> None: y2 = -1.0 with assertRaises(UnboundLocalError, 'local variable "y2" referenced before assignment'): print(y2) + +[case testFloatGlueMethodsAndInheritance] +from typing import Any +from typing_extensions import Final + +from mypy_extensions import trait + +from testutil import assertRaises + +MAGIC: Final = -113.0 + +class Base: + def foo(self) -> float: + return 5.0 + + def bar(self, x: float = 2.0) -> float: + return x + 1 + + def hoho(self, x: float) -> float: + return x - 1 + +class Derived(Base): + def foo(self, x: float = 5.0) -> float: + return x + 10 + + def bar(self, x: float = 3, y: float = 20) -> float: + return x + y + 2 + + def hoho(self, x: float = 7) -> float: + return x - 2 + +def test_derived_adds_bitmap() -> None: + b: Base = Derived() + assert b.foo() == 15 + +def test_derived_adds_another_default_arg() -> None: + b: Base = Derived() + assert b.bar() == 25 + assert b.bar(1) == 23 + assert b.bar(MAGIC) == MAGIC + 22 + +def test_derived_switches_arg_to_have_default() -> None: + b: Base = Derived() + assert b.hoho(5) == 3 + assert b.hoho(MAGIC) == MAGIC - 2 + +@trait +class T: + @property + def x(self) -> float: ... + @property + def y(self) -> float: ... + +class C(T): + x: float = 1.0 + y: float = 4 + +def test_read_only_property_in_trait_implemented_as_attribute() -> None: + c = C() + c.x = 5.5 + assert c.x == 5.5 + c.x = MAGIC + assert c.x == MAGIC + assert c.y == 4 + c.y = 6.5 + assert c.y == 6.5 + t: T = C() + assert t.y == 4 + t = c + assert t.x == MAGIC + c.x = 55.5 + assert t.x == 55.5 + assert t.y == 6.5 + a: Any = c + assert a.x == 55.5 + assert a.y == 6.5 + a.x = 7.0 + a.y = 8.0 + assert a.x == 7 + assert a.y == 8 + +class D(T): + xx: float + + @property + def x(self) -> float: + return self.xx + + @property + def y(self) -> float: + raise TypeError + +def test_read_only_property_in_trait_implemented_as_property() -> None: + d = D() + d.xx = 5 + assert d.x == 5 + d.xx = MAGIC + assert d.x == MAGIC + with assertRaises(TypeError): + d.y + t: T = d + assert t.x == MAGIC + d.xx = 6 + assert t.x == 6 + with assertRaises(TypeError): + t.y + +@trait +class T2: + x: float + y: float + +class C2(T2): + pass + +def test_inherit_trait_attribute() -> None: + c = C2() + c.x = 5.0 + assert c.x == 5 + c.x = MAGIC + assert c.x == MAGIC + with assertRaises(AttributeError): + c.y + c.y = 6.0 + assert c.y == 6.0 + t: T2 = C2() + with assertRaises(AttributeError): + t.y + t = c + assert t.x == MAGIC + c.x = 55 + assert t.x == 55 + assert t.y == 6 + a: Any = c + assert a.x == 55 + assert a.y == 6 + a.x = 7.0 + a.y = 8.0 + assert a.x == 7 + assert a.y == 8 + +class D2(T2): + x: float + y: float = 4 + +def test_implement_trait_attribute() -> None: + d = D2() + d.x = 5.0 + assert d.x == 5 + d.x = MAGIC + assert d.x == MAGIC + assert d.y == 4 + d.y = 6.0 + assert d.y == 6 + t: T2 = D2() + assert t.y == 4 + t = d + assert t.x == MAGIC + d.x = 55 + assert t.x == 55 + assert t.y == 6 + a: Any = d + assert a.x == 55 + assert a.y == 6 + a.x = 7.0 + a.y = 8.0 + assert a.x == 7 + assert a.y == 8 From 4a4376c6bd7327dfc56c91f39cd20144b67e251e Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 4 Mar 2023 10:10:43 +0000 Subject: [PATCH 42/56] Update test case --- mypyc/test-data/irbuild-any.test | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypyc/test-data/irbuild-any.test b/mypyc/test-data/irbuild-any.test index 31eadfe28c017..8274e3d5c6190 100644 --- a/mypyc/test-data/irbuild-any.test +++ b/mypyc/test-data/irbuild-any.test @@ -227,12 +227,12 @@ L0: def f3(): r0, r1, r2, r3 :: object r4 :: int - r5 :: object + r5 :: float L0: r0 = object 2 r1 = object 5 r2 = object 3 r3 = PyNumber_Power(r0, r1, r2) r4 = unbox(int, r3) - r5 = box(int, r4) + r5 = CPyFloat_FromTagged(r4) return r5 From bdca8ab966aecf111d4916bc989fc6a49d75ce0b Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 11 Mar 2023 13:25:43 +0000 Subject: [PATCH 43/56] Disallow narrowing a float value to int --- mypyc/irbuild/builder.py | 20 +++++++++++++++++--- mypyc/test-data/irbuild-float.test | 25 +++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/mypyc/irbuild/builder.py b/mypyc/irbuild/builder.py index a49429f1c6ecc..2370e9bd14354 100644 --- a/mypyc/irbuild/builder.py +++ b/mypyc/irbuild/builder.py @@ -93,9 +93,11 @@ c_pyssize_t_rprimitive, dict_rprimitive, int_rprimitive, + is_float_rprimitive, is_list_rprimitive, is_none_rprimitive, is_object_rprimitive, + is_tagged, is_tuple_rprimitive, none_rprimitive, object_rprimitive, @@ -665,13 +667,13 @@ def read( def assign(self, target: Register | AssignmentTarget, rvalue_reg: Value, line: int) -> None: if isinstance(target, Register): - self.add(Assign(target, self.coerce(rvalue_reg, target.type, line))) + self.add(Assign(target, self.coerce_rvalue(rvalue_reg, target.type, line))) elif isinstance(target, AssignmentTargetRegister): - rvalue_reg = self.coerce(rvalue_reg, target.type, line) + rvalue_reg = self.coerce_rvalue(rvalue_reg, target.type, line) self.add(Assign(target.register, rvalue_reg)) elif isinstance(target, AssignmentTargetAttr): if isinstance(target.obj_type, RInstance): - rvalue_reg = self.coerce(rvalue_reg, target.type, line) + rvalue_reg = self.coerce_rvalue(rvalue_reg, target.type, line) self.add(SetAttr(target.obj, target.attr, rvalue_reg, line)) else: key = self.load_str(target.attr) @@ -698,6 +700,18 @@ def assign(self, target: Register | AssignmentTarget, rvalue_reg: Value, line: i else: assert False, "Unsupported assignment target" + def coerce_rvalue(self, rvalue: Value, rtype: RType, line: int) -> Value: + if is_float_rprimitive(rtype) and is_tagged(rvalue.type): + typename = rvalue.type.short_name() + if typename == "short_int": + typename = "int" + self.error( + "Incompatible value representations in assignment " + + f'(expression has type "{typename}", variable has type "float")', + line, + ) + return self.coerce(rvalue, rtype, line) + def process_sequence_assignment( self, target: AssignmentTargetTuple, rvalue: Value, line: int ) -> None: diff --git a/mypyc/test-data/irbuild-float.test b/mypyc/test-data/irbuild-float.test index 3f4f2884e0af3..64982703df0a8 100644 --- a/mypyc/test-data/irbuild-float.test +++ b/mypyc/test-data/irbuild-float.test @@ -421,3 +421,28 @@ L0: r0 = CPyFloat_FromTagged(y) r1 = CPyFloat_FloorDivide(x, r0) return r1 + +[case testFloatNarrowToIntDisallowed] +class C: + x: float + +def narrow_local(x: float, n: int) -> int: + x = n # E: Incompatible value representations in assignment (expression has type "int", variable has type "float") + return x + +def narrow_tuple_lvalue(x: float, y: float, n: int) -> int: + x, y = 1.0, n # E: Incompatible value representations in assignment (expression has type "int", variable has type "float") + return x + +def narrow_multiple_lvalues(x: float, y: float, n: int) -> int: + x = a = n # E: Incompatible value representations in assignment (expression has type "int", variable has type "float") + a = y = n # E: Incompatible value representations in assignment (expression has type "int", variable has type "float") + return x + y + +def narrow_attribute(c: C, n: int) -> int: + c.x = n # E: Incompatible value representations in assignment (expression has type "int", variable has type "float") + return c.x + +def narrow_using_int_literal(x: float) -> int: + x = 1 # E: Incompatible value representations in assignment (expression has type "int", variable has type "float") + return x From cc2aedffe28335ca08ada1f245e0f118a8e291c7 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 11 Mar 2023 13:46:11 +0000 Subject: [PATCH 44/56] Fix type check --- mypyc/irbuild/ll_builder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypyc/irbuild/ll_builder.py b/mypyc/irbuild/ll_builder.py index 3d222b8107d18..4b2496ef90182 100644 --- a/mypyc/irbuild/ll_builder.py +++ b/mypyc/irbuild/ll_builder.py @@ -1970,7 +1970,7 @@ def float_mod(self, lhs: Value, rhs: Value, line: int) -> Value: is_zero = self.add(FloatComparisonOp(res, Float(0.0), FloatComparisonOp.EQ, line)) self.add(Branch(is_zero, copysign, tricky, Branch.BOOL)) self.activate_block(tricky) - same_signs = self.is_same_float_signs(type, lhs, rhs, line) + same_signs = self.is_same_float_signs(lhs, rhs, line) self.add(Branch(same_signs, done, adjust, Branch.BOOL)) self.activate_block(adjust) adj = self.float_op(res, rhs, "+", line) @@ -2067,7 +2067,7 @@ def is_same_native_int_signs(self, type: RType, a: Value, b: Value, line: int) - neg2 = self.add(ComparisonOp(b, Integer(0, type), ComparisonOp.SLT, line)) return self.add(ComparisonOp(neg1, neg2, ComparisonOp.EQ, line)) - def is_same_float_signs(self, type: RType, a: Value, b: Value, line: int) -> Value: + def is_same_float_signs(self, a: Value, b: Value, line: int) -> Value: neg1 = self.add(FloatComparisonOp(a, Float(0.0), FloatComparisonOp.LT, line)) neg2 = self.add(FloatComparisonOp(b, Float(0.0), FloatComparisonOp.LT, line)) return self.add(ComparisonOp(neg1, neg2, ComparisonOp.EQ, line)) From 937df679a4b1ad9ce8300d386c8d5cf0e4bf4fbe Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 11 Mar 2023 14:37:35 +0000 Subject: [PATCH 45/56] Add test case --- mypyc/test-data/irbuild-float.test | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/mypyc/test-data/irbuild-float.test b/mypyc/test-data/irbuild-float.test index 64982703df0a8..f20cca6e841e4 100644 --- a/mypyc/test-data/irbuild-float.test +++ b/mypyc/test-data/irbuild-float.test @@ -108,6 +108,23 @@ L0: x = r1 return x +[case testFloatOperatorAssignmentWithInt] +def f(x: float, y: int) -> None: + x += y + x -= 5 +[out] +def f(x, y): + x :: float + y :: int + r0, r1, r2 :: float +L0: + r0 = CPyFloat_FromTagged(y) + r1 = x + r0 + x = r1 + r2 = x - 5.0 + x = r2 + return 1 + [case testFloatComparison] def lt(x: float, y: float) -> bool: return x < y From e8e694fdf43b1bc13021f2ea88a42c0dd1d4de14 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 11 Mar 2023 14:37:44 +0000 Subject: [PATCH 46/56] Fix test case --- mypyc/test-data/irbuild-float.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypyc/test-data/irbuild-float.test b/mypyc/test-data/irbuild-float.test index f20cca6e841e4..70f7bc131abdc 100644 --- a/mypyc/test-data/irbuild-float.test +++ b/mypyc/test-data/irbuild-float.test @@ -449,7 +449,7 @@ def narrow_local(x: float, n: int) -> int: def narrow_tuple_lvalue(x: float, y: float, n: int) -> int: x, y = 1.0, n # E: Incompatible value representations in assignment (expression has type "int", variable has type "float") - return x + return y def narrow_multiple_lvalues(x: float, y: float, n: int) -> int: x = a = n # E: Incompatible value representations in assignment (expression has type "int", variable has type "float") From 00fd468886b3962044f843cb7d2f2b46411dfee4 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 11 Mar 2023 14:41:31 +0000 Subject: [PATCH 47/56] Add test case --- mypyc/test-data/irbuild-float.test | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mypyc/test-data/irbuild-float.test b/mypyc/test-data/irbuild-float.test index 70f7bc131abdc..52f7a3eb81f10 100644 --- a/mypyc/test-data/irbuild-float.test +++ b/mypyc/test-data/irbuild-float.test @@ -463,3 +463,10 @@ def narrow_attribute(c: C, n: int) -> int: def narrow_using_int_literal(x: float) -> int: x = 1 # E: Incompatible value representations in assignment (expression has type "int", variable has type "float") return x + +[case testFloatInitializeFromInt] +def init(n: int) -> None: + # These are strictly speaking safe, since these don't narrow, but for consistency with + # narrowing assignments, generate errors here + x: float = n # E: Incompatible value representations in assignment (expression has type "int", variable has type "float") + y: float = 5 # E: Incompatible value representations in assignment (expression has type "int", variable has type "float") From 01f670b99631d7d79d6999d3a4b23c7b77f7c8cc Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 11 Mar 2023 14:47:11 +0000 Subject: [PATCH 48/56] Fix test cases to not assign ints to float variables --- mypyc/test-data/run-floats.test | 38 ++++++++++++++++----------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/mypyc/test-data/run-floats.test b/mypyc/test-data/run-floats.test index 67bd571103253..cd36be7b2033a 100644 --- a/mypyc/test-data/run-floats.test +++ b/mypyc/test-data/run-floats.test @@ -110,7 +110,7 @@ def test_boxing_and_unboxing() -> None: y: float = boxed assert y == x boxed_int: Any = 5 - assert type(boxed_int) is int + assert [type(boxed_int)] == [int] # Avoid mypy type narrowing z: float = boxed_int assert z == 5.0 for xx in float_vals: @@ -129,40 +129,38 @@ def test_unboxing_failure() -> None: with assertRaises(TypeError): x: float = boxed +def identity(x: float) -> float: + return x + def test_coerce_from_int_literal() -> None: - x: float = 34 - assert x == 34.0 - y: float = -1 - assert y == -1.0 + assert identity(34) == 34.0 + assert identity(-1) == -1.0 def test_coerce_from_short_tagged_int() -> None: n = int() - 17 - x: float = n - assert x == -17.0 + assert identity(n) == -17.0 for i in range(-300, 300): - y: float = i - o: object = y - assert o == i + assert identity(i) == float(i) def test_coerce_from_long_tagged_int() -> None: n = int() + 2**100 - x: float = n + x = identity(n) assert repr(x) == '1.2676506002282294e+30' n = int() - 2**100 - y: float = n + y = identity(n) assert repr(y) == '-1.2676506002282294e+30' def test_coerce_from_very_long_tagged_int() -> None: n = int() + 10**1000 with assertRaises(OverflowError, "int too large to convert to float"): - x: float = n + identity(n) with assertRaises(OverflowError, "int too large to convert to float"): - x2: float = int(n) + identity(int(n)) n = int() - 10**1000 with assertRaises(OverflowError, "int too large to convert to float"): - y: float = n + identity(n) with assertRaises(OverflowError, "int too large to convert to float"): - y2: float = int(n) + identity(int(n)) def test_explicit_conversion_from_int() -> None: float_any: Any = float @@ -424,7 +422,7 @@ class D(T): def test_read_only_property_in_trait_implemented_as_property() -> None: d = D() - d.xx = 5 + d.xx = 5.0 assert d.x == 5 d.xx = MAGIC assert d.x == MAGIC @@ -432,7 +430,7 @@ def test_read_only_property_in_trait_implemented_as_property() -> None: d.y t: T = d assert t.x == MAGIC - d.xx = 6 + d.xx = 6.0 assert t.x == 6 with assertRaises(TypeError): t.y @@ -460,7 +458,7 @@ def test_inherit_trait_attribute() -> None: t.y t = c assert t.x == MAGIC - c.x = 55 + c.x = 55.0 assert t.x == 55 assert t.y == 6 a: Any = c @@ -488,7 +486,7 @@ def test_implement_trait_attribute() -> None: assert t.y == 4 t = d assert t.x == MAGIC - d.x = 55 + d.x = 55.0 assert t.x == 55 assert t.y == 6 a: Any = d From 6cd75b823dc49225204842853d8bc0ede2176f11 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 11 Mar 2023 15:03:07 +0000 Subject: [PATCH 49/56] Update test case --- mypyc/test-data/irbuild-float.test | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mypyc/test-data/irbuild-float.test b/mypyc/test-data/irbuild-float.test index 52f7a3eb81f10..fa9bf1da4800d 100644 --- a/mypyc/test-data/irbuild-float.test +++ b/mypyc/test-data/irbuild-float.test @@ -464,6 +464,11 @@ def narrow_using_int_literal(x: float) -> int: x = 1 # E: Incompatible value representations in assignment (expression has type "int", variable has type "float") return x +def narrow_using_declaration(n: int) -> int: + x: float + x = n # E: Incompatible value representations in assignment (expression has type "int", variable has type "float") + return x + [case testFloatInitializeFromInt] def init(n: int) -> None: # These are strictly speaking safe, since these don't narrow, but for consistency with From 6384c03484b38948dd361510e6077d1d8a985ad4 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 11 Mar 2023 15:09:00 +0000 Subject: [PATCH 50/56] Add i64 to float conversion test case --- mypyc/test-data/irbuild-i64.test | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/mypyc/test-data/irbuild-i64.test b/mypyc/test-data/irbuild-i64.test index 9c12b10b35626..5408cf5fea495 100644 --- a/mypyc/test-data/irbuild-i64.test +++ b/mypyc/test-data/irbuild-i64.test @@ -1925,6 +1925,34 @@ L2: L3: return r4 +[case testI64ConvertToFloat] +from mypy_extensions import i64 + +def i64_to_float(x: i64) -> float: + return float(x) +[out] +def i64_to_float(x): + x :: int64 + r0, r1 :: bit + r2, r3, r4 :: int + r5 :: float +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: + r5 = CPyFloat_FromTagged(r3) + return r5 + [case testI64IsinstanceNarrowing] from typing import Union from mypy_extensions import i64 From a177197ecc82bd6b29f03a00c21541cc9058b590 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 11 Mar 2023 18:17:52 +0000 Subject: [PATCH 51/56] Fix tests on 32-bit architecture --- mypyc/test-data/irbuild-i32.test | 35 +++++++++++++++- mypyc/test-data/irbuild-i64.test | 69 +++++++++++++++++++++++++++++++- 2 files changed, 101 insertions(+), 3 deletions(-) diff --git a/mypyc/test-data/irbuild-i32.test b/mypyc/test-data/irbuild-i32.test index a5d69fb55ee09..725e183657b18 100644 --- a/mypyc/test-data/irbuild-i32.test +++ b/mypyc/test-data/irbuild-i32.test @@ -481,7 +481,7 @@ L0: z = -3 return 1 -[case testI32ExplicitConversionFromVariousTypes] +[case testI32ExplicitConversionFromVariousTypes_64bit] from mypy_extensions import i32 def bool_to_i32(b: bool) -> i32: @@ -552,3 +552,36 @@ L4: unreachable L5: return r7 + +[case testI32ExplicitConversionFromFloat_32bit] +from mypy_extensions import i32 + +def float_to_i32(x: float) -> i32: + return i32(x) +[out] +def float_to_i32(x): + x :: float + r0 :: int + r1 :: native_int + r2 :: bit + r3, r4 :: int32 + r5 :: ptr + r6 :: c_ptr + r7 :: int32 +L0: + r0 = CPyTagged_FromFloat(x) + r1 = r0 & 1 + r2 = r1 == 0 + if r2 goto L1 else goto L2 :: bool +L1: + r3 = r0 >> 1 + r4 = r3 + goto L3 +L2: + r5 = r0 ^ 1 + r6 = r5 + r7 = CPyLong_AsInt32(r6) + r4 = r7 + keep_alive r0 +L3: + return r4 diff --git a/mypyc/test-data/irbuild-i64.test b/mypyc/test-data/irbuild-i64.test index 5408cf5fea495..74eb126b937dc 100644 --- a/mypyc/test-data/irbuild-i64.test +++ b/mypyc/test-data/irbuild-i64.test @@ -1844,7 +1844,7 @@ L2: L3: return r4 -[case testI64ExplicitConversionFromVariousTypes] +[case testI64ExplicitConversionFromVariousTypes_64bit] from mypy_extensions import i64 def bool_to_i64(b: bool) -> i64: @@ -1925,7 +1925,41 @@ L2: L3: return r4 -[case testI64ConvertToFloat] +[case testI64ExplicitConversionFromFloat_32bit] +from mypy_extensions import i64 + +def float_to_i64(x: float) -> i64: + return i64(x) +[out] +def float_to_i64(x): + x :: float + r0 :: int + r1 :: native_int + r2 :: bit + r3, r4, r5 :: int64 + r6 :: ptr + r7 :: c_ptr + r8 :: int64 +L0: + r0 = CPyTagged_FromFloat(x) + r1 = r0 & 1 + r2 = r1 == 0 + if r2 goto L1 else goto L2 :: bool +L1: + r3 = extend signed r0: builtins.int to int64 + r4 = r3 >> 1 + r5 = r4 + goto L3 +L2: + r6 = r0 ^ 1 + r7 = r6 + r8 = CPyLong_AsInt64(r7) + r5 = r8 + keep_alive r0 +L3: + return r5 + +[case testI64ConvertToFloat_64bit] from mypy_extensions import i64 def i64_to_float(x: i64) -> float: @@ -1953,6 +1987,37 @@ L4: r5 = CPyFloat_FromTagged(r3) return r5 +[case testI64ConvertToFloat_32bit] +from mypy_extensions import i64 + +def i64_to_float(x: i64) -> float: + return float(x) +[out] +def i64_to_float(x): + x :: int64 + r0, r1 :: bit + r2, r3 :: int + r4 :: native_int + r5 :: int + r6 :: float +L0: + r0 = x <= 1073741823 :: signed + if r0 goto L1 else goto L2 :: bool +L1: + r1 = x >= -1073741824 :: signed + if r1 goto L3 else goto L2 :: bool +L2: + r2 = CPyTagged_FromInt64(x) + r3 = r2 + goto L4 +L3: + r4 = truncate x: int64 to native_int + r5 = r4 << 1 + r3 = r5 +L4: + r6 = CPyFloat_FromTagged(r3) + return r6 + [case testI64IsinstanceNarrowing] from typing import Union from mypy_extensions import i64 From 3c5931d721c9e4b41e3871f3807d94367e80a36c Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 11 Mar 2023 19:18:04 +0000 Subject: [PATCH 52/56] Add missing error check to property getter wrappers --- mypyc/codegen/emitclass.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mypyc/codegen/emitclass.py b/mypyc/codegen/emitclass.py index a9b51b8ff1a4d..bf1f152f1bb1b 100644 --- a/mypyc/codegen/emitclass.py +++ b/mypyc/codegen/emitclass.py @@ -1004,6 +1004,7 @@ def generate_readonly_getter( emitter.ctype_spaced(rtype), NATIVE_PREFIX, func_ir.cname(emitter.names) ) ) + emitter.emit_error_check("retval", rtype, "return NULL;") emitter.emit_box("retval", "retbox", rtype, declare_dest=True) emitter.emit_line("return retbox;") else: From 8ab5e25bc40bee5dc683148b660c486d70e50a2b Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 11 Mar 2023 19:20:52 +0000 Subject: [PATCH 53/56] Work around mypyc bug --- mypyc/ir/ops.py | 6 +++++- mypyc/irbuild/ll_builder.py | 5 +++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/mypyc/ir/ops.py b/mypyc/ir/ops.py index 137150bc195a8..e3bf6f921a67f 100644 --- a/mypyc/ir/ops.py +++ b/mypyc/ir/ops.py @@ -1195,7 +1195,6 @@ class FloatOp(RegisterOp): MOD: Final = 4 op_str: Final = {ADD: "+", SUB: "-", MUL: "*", DIV: "/", MOD: "%"} - op_to_id: Final = {op: op_id for op_id, op in op_str.items()} def __init__(self, lhs: Value, rhs: Value, op: int, line: int = -1) -> None: super().__init__(line) @@ -1211,6 +1210,11 @@ def accept(self, visitor: "OpVisitor[T]") -> T: return visitor.visit_float_op(self) +# We can't have this in the FloatOp class body, because of +# https://github.com/mypyc/mypyc/issues/932. +float_op_to_id: Final = {op: op_id for op_id, op in FloatOp.op_str.items()} + + class FloatNeg(RegisterOp): """Float negation op (r1 = -r2).""" diff --git a/mypyc/irbuild/ll_builder.py b/mypyc/irbuild/ll_builder.py index 4b2496ef90182..73e7f896ef614 100644 --- a/mypyc/irbuild/ll_builder.py +++ b/mypyc/irbuild/ll_builder.py @@ -71,6 +71,7 @@ Unbox, Unreachable, Value, + float_op_to_id, int_op_to_id, ) from mypyc.ir.rtypes import ( @@ -1358,7 +1359,7 @@ def binary_op(self, lreg: Value, rreg: Value, op: str, line: int) -> Value: base_op = op[:-1] else: base_op = op - if base_op in FloatOp.op_to_id: + if base_op in float_op_to_id: return self.float_op(lreg, rreg, base_op, line) call_c_ops_candidates = binary_ops.get(op, []) @@ -1941,7 +1942,7 @@ def float_op(self, lhs: Value, rhs: Value, op: str, line: int) -> Value: Args: op: Binary operator (e.g. '+' or '*') """ - op_id = FloatOp.op_to_id[op] + op_id = float_op_to_id[op] if op_id in (FloatOp.DIV, FloatOp.MOD): if not (isinstance(rhs, Float) and rhs.value != 0.0): c = self.compare_floats(rhs, Float(0.0), FloatComparisonOp.EQ, line) From e85f057b44a55aaa1b6c9698471afe657b3e9605 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sat, 11 Mar 2023 20:30:01 +0000 Subject: [PATCH 54/56] Work around another issue --- mypyc/ir/ops.py | 7 +++++-- mypyc/irbuild/ll_builder.py | 5 +++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/mypyc/ir/ops.py b/mypyc/ir/ops.py index e3bf6f921a67f..adf24de235fff 100644 --- a/mypyc/ir/ops.py +++ b/mypyc/ir/ops.py @@ -1246,8 +1246,6 @@ class FloatComparisonOp(RegisterOp): op_str: Final = {EQ: "==", NEQ: "!=", LT: "<", GT: ">", LE: "<=", GE: ">="} - op_to_id: Final = {op: op_id for op_id, op in op_str.items()} - def __init__(self, lhs: Value, rhs: Value, op: int, line: int = -1) -> None: super().__init__(line) self.type = bit_rprimitive @@ -1262,6 +1260,11 @@ def accept(self, visitor: "OpVisitor[T]") -> T: return visitor.visit_float_comparison_op(self) +# We can't have this in the FloatOp class body, because of +# https://github.com/mypyc/mypyc/issues/932. +float_comparison_op_to_id: Final = {op: op_id for op_id, op in FloatComparisonOp.op_str.items()} + + class LoadMem(RegisterOp): """Read a memory location: result = *(type *)src. diff --git a/mypyc/irbuild/ll_builder.py b/mypyc/irbuild/ll_builder.py index 73e7f896ef614..d41b532f9228e 100644 --- a/mypyc/irbuild/ll_builder.py +++ b/mypyc/irbuild/ll_builder.py @@ -71,6 +71,7 @@ Unbox, Unreachable, Value, + float_comparison_op_to_id, float_op_to_id, int_op_to_id, ) @@ -1353,8 +1354,8 @@ def binary_op(self, lreg: Value, rreg: Value, op: str, line: int) -> Value: elif is_int_rprimitive(rreg.type): rreg = self.int_to_float(rreg, line) if is_float_rprimitive(lreg.type) and is_float_rprimitive(rreg.type): - if op in FloatComparisonOp.op_to_id: - return self.compare_floats(lreg, rreg, FloatComparisonOp.op_to_id[op], line) + if op in float_comparison_op_to_id: + return self.compare_floats(lreg, rreg, float_comparison_op_to_id[op], line) if op.endswith("="): base_op = op[:-1] else: From e45dba5744cd335349828409e25f0e38b622fbb8 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Sun, 12 Mar 2023 10:57:51 +0000 Subject: [PATCH 55/56] Fix test on 3.11 --- mypyc/test-data/run-classes.test | 1 + 1 file changed, 1 insertion(+) diff --git a/mypyc/test-data/run-classes.test b/mypyc/test-data/run-classes.test index cddaac13751ad..268e07f6bde4c 100644 --- a/mypyc/test-data/run-classes.test +++ b/mypyc/test-data/run-classes.test @@ -1853,6 +1853,7 @@ Represents a sequence of values. Updates itself by next, which is a new value. Traceback (most recent call last): File "driver.py", line 5, in print (x.rankine) + ^^^^^^^^^ File "native.py", line 16, in rankine raise NotImplementedError NotImplementedError From c9d714707e83ec3cb6b8e90ab8849ad2c3b8ea34 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Tue, 14 Mar 2023 18:34:29 +0000 Subject: [PATCH 56/56] Add subnormal values --- mypyc/test-data/fixtures/testutil.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/mypyc/test-data/fixtures/testutil.py b/mypyc/test-data/fixtures/testutil.py index 8fb29d40db7ae..5a4b1d0f549e2 100644 --- a/mypyc/test-data/fixtures/testutil.py +++ b/mypyc/test-data/fixtures/testutil.py @@ -15,13 +15,29 @@ float_vals = [ float(n) * 0.25 for n in range(-10, 10) ] + [ - -0.0, 1.0/3.0, math.sqrt(2.0), 1.23e200, -2.34e200, 5.43e-100, -6.532e-200, - float('inf'), -float('inf'), float('nan'), FLOAT_MAGIC, math.pi, 2.0 * math.pi, math.pi / 2.0, + -0.0, + 1.0/3.0, + math.sqrt(2.0), + 1.23e200, + -2.34e200, + 5.43e-100, + -6.532e-200, + float('inf'), + -float('inf'), + float('nan'), + FLOAT_MAGIC, + math.pi, + 2.0 * math.pi, + math.pi / 2.0, -math.pi / 2.0, - -1.7976931348623158e+308, - -2.2250738585072014e-308, - 1.7976931348623158e+308, - 2.2250738585072014e-308, + -1.7976931348623158e+308, # Smallest finite value + -2.2250738585072014e-308, # Closest to zero negative normal value + -7.5491e-312, # Arbitrary negative subnormal value + -5e-324, # Closest to zero negative subnormal value + 1.7976931348623158e+308, # Largest finite value + 2.2250738585072014e-308, # Closest to zero positive normal value + -6.3492e-312, # Arbitrary positive subnormal value + 5e-324, # Closest to zero positive subnormal value ] @contextmanager