From d0de3be2a9b3f11c080cb298e3efb9ff553cb833 Mon Sep 17 00:00:00 2001 From: silmeth Date: Sat, 29 Jun 2019 16:50:09 +0200 Subject: [PATCH] =?UTF-8?q?fix=20float=E2=80=93int=20cross-type=20comparis?= =?UTF-8?q?ons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #1069 --- tests/snippets/int_float_comparisons.py | 61 ++++++++++++++++++++++ tests/snippets/int_float_equality.py | 15 ------ vm/src/obj/objfloat.rs | 68 +++++++++++++++++++++---- 3 files changed, 119 insertions(+), 25 deletions(-) create mode 100644 tests/snippets/int_float_comparisons.py delete mode 100644 tests/snippets/int_float_equality.py diff --git a/tests/snippets/int_float_comparisons.py b/tests/snippets/int_float_comparisons.py new file mode 100644 index 000000000..400d47d07 --- /dev/null +++ b/tests/snippets/int_float_comparisons.py @@ -0,0 +1,61 @@ +# 10**308 cannot be represented exactly in f64, thus it is not equal to 1e308 float +assert not (10**308 == 1e308) +# but the 1e308 float can be converted to big int and then it still should be equal to itself +assert int(1e308) == 1e308 + +# and the equalities should be the same when operands switch sides +assert not (1e308 == 10**308) +assert 1e308 == int(1e308) + +# floats that cannot be converted to big ints shouldn’t crash the vm +import math +assert not (10**500 == math.inf) +assert not (math.inf == 10**500) +assert not (10**500 == math.nan) +assert not (math.nan == 10**500) + +# comparisons +# floats with worse than integer precision +assert 2.**54 > 2**54 - 1 +assert 2.**54 < 2**54 + 1 +assert 2.**54 >= 2**54 - 1 +assert 2.**54 <= 2**54 + 1 +assert 2.**54 == 2**54 +assert not 2.**54 == 2**54 + 1 + +# inverse operands +assert 2**54 - 1 < 2.**54 +assert 2**54 + 1 > 2.**54 +assert 2**54 - 1 <= 2.**54 +assert 2**54 + 1 >= 2.**54 +assert 2**54 == 2.**54 +assert not 2**54 + 1 == 2.**54 + +assert not 2.**54 < 2**54 - 1 +assert not 2.**54 > 2**54 + 1 + +# sub-int numbers +assert 1.3 > 1 +assert 1.3 >= 1 +assert 2.5 > 2 +assert 2.5 >= 2 +assert -0.3 < 0 +assert -0.3 <= 0 + +# int out of float range comparisons +assert 10**500 > 2.**54 +assert -10**500 < -0.12 + +# infinity and NaN comparisons +assert math.inf > 10**500 +assert math.inf >= 10**500 +assert not math.inf < 10**500 + +assert -math.inf < -10*500 +assert -math.inf <= -10*500 +assert not -math.inf > -10*500 + +assert not math.nan > 123 +assert not math.nan < 123 +assert not math.nan >= 123 +assert not math.nan <= 123 diff --git a/tests/snippets/int_float_equality.py b/tests/snippets/int_float_equality.py deleted file mode 100644 index fb240f8d3..000000000 --- a/tests/snippets/int_float_equality.py +++ /dev/null @@ -1,15 +0,0 @@ -# 10**308 cannot be represented exactly in f64, thus it is not equal to 1e308 float -assert not (10**308 == 1e308) -# but the 1e308 float can be converted to big int and then it still should be equal to itself -assert int(1e308) == 1e308 - -# and the equalities should be the same when operands switch sides -assert not (1e308 == 10**308) -assert 1e308 == int(1e308) - -# floats that cannot be converted to big ints shouldn’t crash the vm -import math -assert not (10**500 == math.inf) -assert not (math.inf == 10**500) -assert not (10**500 == math.nan) -assert not (math.nan == 10**500) diff --git a/vm/src/obj/objfloat.rs b/vm/src/obj/objfloat.rs index bd17147ad..e8e05465a 100644 --- a/vm/src/obj/objfloat.rs +++ b/vm/src/obj/objfloat.rs @@ -14,7 +14,7 @@ use crate::vm::VirtualMachine; use hexf; use num_bigint::{BigInt, ToBigInt}; use num_rational::Ratio; -use num_traits::{float::Float, ToPrimitive, Zero}; +use num_traits::{float::Float, sign::Signed, ToPrimitive, Zero}; /// Convert a string or number to a floating point number, if possible. #[pyclass(name = "float")] @@ -79,7 +79,7 @@ fn try_to_bigint(value: f64, vm: &VirtualMachine) -> PyResult { None => { if value.is_infinite() { Err(vm.new_overflow_error( - "OverflowError: cannot convert float NaN to integer".to_string(), + "OverflowError: cannot convert float infinity to integer".to_string(), )) } else if value.is_nan() { Err(vm @@ -111,6 +111,32 @@ fn inner_divmod(v1: f64, v2: f64, vm: &VirtualMachine) -> PyResult<(f64, f64)> { } } +fn inner_lt_int(value: f64, other_int: &BigInt) -> bool { + match (value.to_bigint(), other_int.to_f64()) { + (Some(self_int), Some(other_float)) => value < other_float || self_int < *other_int, + // finite float, other_int too big for float, + // the result depends only on other_int’s sign + (Some(_), None) => other_int.is_positive(), + // infinite float must be bigger or lower than any int, depending on its sign + _ if value.is_infinite() => value.is_sign_negative(), + // NaN, always false + _ => false, + } +} + +fn inner_gt_int(value: f64, other_int: &BigInt) -> bool { + match (value.to_bigint(), other_int.to_f64()) { + (Some(self_int), Some(other_float)) => value > other_float || self_int > *other_int, + // finite float, other_int too big for float, + // the result depends only on other_int’s sign + (Some(_), None) => other_int.is_negative(), + // infinite float must be bigger or lower than any int, depending on its sign + _ if value.is_infinite() => value.is_sign_positive(), + // NaN, always false + _ => false, + } +} + #[pyimpl] impl PyFloat { #[pymethod(name = "__eq__")] @@ -139,8 +165,9 @@ impl PyFloat { if objtype::isinstance(&i2, &vm.ctx.float_type()) { vm.ctx.new_bool(v1 < get_value(&i2)) } else if objtype::isinstance(&i2, &vm.ctx.int_type()) { - vm.ctx - .new_bool(v1 < objint::get_value(&i2).to_f64().unwrap()) + let other_int = objint::get_value(&i2); + + vm.ctx.new_bool(inner_lt_int(self.value, other_int)) } else { vm.ctx.not_implemented() } @@ -152,8 +179,18 @@ impl PyFloat { if objtype::isinstance(&i2, &vm.ctx.float_type()) { vm.ctx.new_bool(v1 <= get_value(&i2)) } else if objtype::isinstance(&i2, &vm.ctx.int_type()) { - vm.ctx - .new_bool(v1 <= objint::get_value(&i2).to_f64().unwrap()) + let other_int = objint::get_value(&i2); + + let result = if let (Some(self_int), Some(other_float)) = + (self.value.to_bigint(), other_int.to_f64()) + { + self.value <= other_float && self_int <= *other_int + } else { + // certainly not equal, forward to inner_lt_int + inner_lt_int(self.value, other_int) + }; + + vm.ctx.new_bool(result) } else { vm.ctx.not_implemented() } @@ -165,8 +202,9 @@ impl PyFloat { if objtype::isinstance(&i2, &vm.ctx.float_type()) { vm.ctx.new_bool(v1 > get_value(&i2)) } else if objtype::isinstance(&i2, &vm.ctx.int_type()) { - vm.ctx - .new_bool(v1 > objint::get_value(&i2).to_f64().unwrap()) + let other_int = objint::get_value(&i2); + + vm.ctx.new_bool(inner_gt_int(self.value, other_int)) } else { vm.ctx.not_implemented() } @@ -178,8 +216,18 @@ impl PyFloat { if objtype::isinstance(&i2, &vm.ctx.float_type()) { vm.ctx.new_bool(v1 >= get_value(&i2)) } else if objtype::isinstance(&i2, &vm.ctx.int_type()) { - vm.ctx - .new_bool(v1 >= objint::get_value(&i2).to_f64().unwrap()) + let other_int = objint::get_value(&i2); + + let result = if let (Some(self_int), Some(other_float)) = + (self.value.to_bigint(), other_int.to_f64()) + { + self.value >= other_float && self_int >= *other_int + } else { + // certainly not equal, forward to inner_gt_int + inner_gt_int(self.value, other_int) + }; + + vm.ctx.new_bool(result) } else { vm.ctx.not_implemented() }