Skip to content

Implement nextafter in Rust #2309

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 29 additions & 19 deletions Lib/test/test_math.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,8 @@ def testAcos(self):
self.assertRaises(ValueError, math.acos, -1 - eps)
self.assertTrue(math.isnan(math.acos(NAN)))

@unittest.skip('TODO: RUSTPYTHON')
# TODO: RUSTPYTHON
@unittest.expectedFailure
def testAcosh(self):
self.assertRaises(TypeError, math.acosh)
self.ftest('acosh(1)', math.acosh(1), 0)
Expand Down Expand Up @@ -300,7 +301,7 @@ def testAtan(self):
self.ftest('atan(-inf)', math.atan(NINF), -math.pi/2)
self.assertTrue(math.isnan(math.atan(NAN)))

@unittest.skip('TODO: RUSTPYTHON')
@unittest.expectedFailure # TODO: RUSTPYTHON
def testAtanh(self):
self.assertRaises(TypeError, math.atan)
self.ftest('atanh(0)', math.atanh(0), 0)
Expand Down Expand Up @@ -373,7 +374,8 @@ def testAtan2(self):
self.assertTrue(math.isnan(math.atan2(NAN, INF)))
self.assertTrue(math.isnan(math.atan2(NAN, NAN)))

@unittest.skip('TODO: RUSTPYTHON')
# TODO: RUSTPYTHON
@unittest.expectedFailure
def testCeil(self):
self.assertRaises(TypeError, math.ceil)
self.assertEqual(int, type(math.ceil(0.5)))
Expand Down Expand Up @@ -473,7 +475,8 @@ def testDegrees(self):
self.ftest('degrees(-pi/4)', math.degrees(-math.pi/4), -45.0)
self.ftest('degrees(0)', math.degrees(0), 0)

@unittest.skip('TODO: RUSTPYTHON')
# TODO: RUSTPYTHON
@unittest.expectedFailure
def testExp(self):
self.assertRaises(TypeError, math.exp)
self.ftest('exp(-1)', math.exp(-1), 1/math.e)
Expand All @@ -500,7 +503,8 @@ def testFactorial(self):
self.assertRaises(ValueError, math.factorial, -1)
self.assertRaises(ValueError, math.factorial, -10**100)

@unittest.skip('TODO: RUSTPYTHON')
# TODO: RUSTPYTHON
@unittest.expectedFailure
def testFactorialNonIntegers(self):
with self.assertWarns(DeprecationWarning):
self.assertEqual(math.factorial(5.0), 120)
Expand All @@ -523,7 +527,8 @@ def testFactorialHugeInputs(self):
with self.assertWarns(DeprecationWarning):
self.assertRaises(OverflowError, math.factorial, 1e100)

@unittest.skip('TODO: RUSTPYTHON')
# TODO: RUSTPYTHON
@unittest.expectedFailure
def testFloor(self):
self.assertRaises(TypeError, math.floor)
self.assertEqual(int, type(math.floor(0.5)))
Expand Down Expand Up @@ -737,7 +742,6 @@ def testGcd(self):
self.assertRaises(TypeError, gcd, 120, 1, 84.0)
#self.assertEqual(gcd(MyIndexable(120), MyIndexable(84)), 12) # TODO: RUSTPYTHON

@unittest.skip('TODO: RUSTPYTHON float support')
def testHypot(self):
from decimal import Decimal
from fractions import Fraction
Expand Down Expand Up @@ -812,7 +816,8 @@ def testHypot(self):
scale = FLOAT_MIN / 2.0 ** exp
self.assertEqual(math.hypot(4*scale, 3*scale), 5*scale)

@unittest.skip('TODO: RUSTPYTHON')
# TODO: RUSTPYTHON
@unittest.expectedFailure
def testDist(self):
from decimal import Decimal as D
from fractions import Fraction as F
Expand Down Expand Up @@ -1024,7 +1029,8 @@ def testlcm(self):
self.assertRaises(TypeError, lcm, 120, 0, 84.0)
# self.assertEqual(lcm(MyIndexable(120), MyIndexable(84)), 840) # TODO: RUSTPYTHON

@unittest.skip('TODO: RUSTPYTHON')
# TODO: RUSTPYTHON
@unittest.expectedFailure
def testLdexp(self):
self.assertRaises(TypeError, math.ldexp)
self.ftest('ldexp(0,1)', math.ldexp(0,1), 0)
Expand Down Expand Up @@ -1057,7 +1063,8 @@ def testLdexp(self):
self.assertEqual(math.ldexp(NINF, n), NINF)
self.assertTrue(math.isnan(math.ldexp(NAN, n)))

@unittest.skip('TODO: RUSTPYTHON')
# TODO: RUSTPYTHON
@unittest.expectedFailure
def testLog(self):
self.assertRaises(TypeError, math.log)
self.ftest('log(1/e)', math.log(1/math.e), -1)
Expand All @@ -1074,7 +1081,8 @@ def testLog(self):
self.assertEqual(math.log(INF), INF)
self.assertTrue(math.isnan(math.log(NAN)))

@unittest.skip('TODO: RUSTPYTHON')
# TODO: RUSTPYTHON
@unittest.expectedFailure
def testLog1p(self):
self.assertRaises(TypeError, math.log1p)
for n in [2, 2**90, 2**300]:
Expand Down Expand Up @@ -1142,7 +1150,8 @@ def testmodf(name, result, expected):
self.assertTrue(math.isnan(modf_nan[0]))
self.assertTrue(math.isnan(modf_nan[1]))

@unittest.skip('TODO: RUSTPYTHON')
# TODO: RUSTPYTHON
@unittest.expectedFailure
def testPow(self):
self.assertRaises(TypeError, math.pow)
self.ftest('pow(0,1)', math.pow(0,1), 0)
Expand Down Expand Up @@ -1564,7 +1573,8 @@ def testIsinf(self):
# still fails this part of the test on some platforms. For now, we only
# *run* test_exceptions() in verbose mode, so that this isn't normally
# tested.
@unittest.skip('TODO: RUSTPYTHON')
# TODO: RUSTPYTHON
@unittest.expectedFailure
@unittest.skipUnless(verbose, 'requires verbose mode')
def test_exceptions(self):
try:
Expand Down Expand Up @@ -1723,7 +1733,8 @@ def test_exceptions(self):
# self.fail('Failures in test_mtestfile:\n ' +
# '\n '.join(failures))

@unittest.skip('TODO: RUSTPYTHON')
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_prod(self):
prod = math.prod
self.assertEqual(prod([]), 1)
Expand Down Expand Up @@ -1810,7 +1821,8 @@ def _naive_prod(iterable, start=1):
self.assertEqual(type(prod([1, decimal.Decimal(2.0), 3, 4, 5, 6])),
decimal.Decimal)

@unittest.skip('TODO: RUSTPYTHON')
# TODO: RUSTPYTHON
@unittest.expectedFailure
def testPerm(self):
perm = math.perm
factorial = math.factorial
Expand Down Expand Up @@ -1875,7 +1887,8 @@ def testPerm(self):
self.assertIs(type(perm(IntSubclass(5), IntSubclass(k))), int)
self.assertIs(type(perm(MyIndexable(5), MyIndexable(k))), int)

@unittest.skip('TODO: RUSTPYTHON')
# TODO: RUSTPYTHON
@unittest.expectedFailure
def testComb(self):
comb = math.comb
factorial = math.factorial
Expand Down Expand Up @@ -2015,7 +2028,6 @@ def test_ulp(self):
with self.subTest(x=x):
self.assertEqual(math.ulp(-x), math.ulp(x))

@unittest.skip('TODO: RUSTPYTHON')
def test_issue39871(self):
# A SystemError should not be raised if the first arg to atan2(),
# copysign(), or remainder() cannot be converted to a float.
Expand Down Expand Up @@ -2146,7 +2158,6 @@ def test_integers(self):
self.assertAllClose(integer_examples, rel_tol=1e-8)
self.assertAllNotClose(integer_examples, rel_tol=1e-9)

@unittest.skip('TODO: RUSTPYTHON')
def test_decimals(self):
# test with Decimal values
from decimal import Decimal
Expand All @@ -2158,7 +2169,6 @@ def test_decimals(self):
self.assertAllClose(decimal_examples, rel_tol=1e-8)
self.assertAllNotClose(decimal_examples, rel_tol=1e-9)

@unittest.skip('TODO: RUSTPYTHON')
def test_fractions(self):
# test with Fraction values
from fractions import Fraction
Expand Down
1 change: 1 addition & 0 deletions common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ once_cell = "1.4.1"
siphasher = "0.3"
rand = "0.7.3"
derive_more = "0.99.9"
volatile = "0.3"
52 changes: 52 additions & 0 deletions common/src/cmp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use volatile::Volatile;

/// Compare 2 byte slices in a way that ensures that the timing of the operation can't be used to
/// glean any information about the data.
#[inline(never)]
#[cold]
pub fn timing_safe_cmp(a: &[u8], b: &[u8]) -> bool {
// we use raw pointers here to keep faithful to the C implementation and
// to try to avoid any optimizations rustc might do with slices
let len_a = a.len();
let a = a.as_ptr();
let len_b = b.len();
let b = b.as_ptr();
/* The volatile type declarations make sure that the compiler has no
* chance to optimize and fold the code in any way that may change
* the timing.
*/
let length: Volatile<usize>;
let mut left: Volatile<*const u8>;
let mut right: Volatile<*const u8>;
let mut result: u8 = 0;

/* loop count depends on length of b */
length = Volatile::new(len_b);
left = Volatile::new(std::ptr::null());
right = Volatile::new(b);

/* don't use else here to keep the amount of CPU instructions constant,
* volatile forces re-evaluation
* */
if len_a == length.read() {
left.write(Volatile::new(a).read());
result = 0;
}
if len_a != length.read() {
left.write(b);
result = 1;
}

for _ in 0..length.read() {
let l = left.read();
left.write(l.wrapping_add(1));
let r = right.read();
right.write(r.wrapping_add(1));
// safety: the 0..length range will always be either:
// * as long as the length of both a and b, if len_a and len_b are equal
// * as long as b, and both `left` and `right` are b
result |= unsafe { l.read_volatile() ^ r.read_volatile() };
}

result == 0
}
54 changes: 45 additions & 9 deletions common/src/float_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,9 @@ pub fn from_hex(s: &str) -> Option<f64> {
return Some(f);
}
match s.to_ascii_lowercase().as_str() {
"nan" => Some(f64::NAN),
"+nan" => Some(f64::NAN),
"-nan" => Some(f64::NAN),
"inf" => Some(f64::INFINITY),
"infinity" => Some(f64::INFINITY),
"+inf" => Some(f64::INFINITY),
"+infinity" => Some(f64::INFINITY),
"-inf" => Some(f64::NEG_INFINITY),
"-infinity" => Some(f64::NEG_INFINITY),
"nan" | "+nan" | "-nan" => Some(f64::NAN),
"inf" | "infinity" | "+inf" | "+infinity" => Some(f64::INFINITY),
"-inf" | "-infinity" => Some(f64::NEG_INFINITY),
value => {
let mut hex = String::with_capacity(value.len());
let has_0x = value.contains("0x");
Expand Down Expand Up @@ -216,6 +210,48 @@ pub fn divmod(v1: f64, v2: f64) -> Option<(f64, f64)> {
}
}

// nextafter algorithm based off of https://gitlab.com/bronsonbdevost/next_afterf
#[allow(clippy::float_cmp)]
pub fn nextafter(x: f64, y: f64) -> f64 {
if x == y {
y
} else if x.is_nan() || y.is_nan() {
f64::NAN
} else if x >= f64::INFINITY {
f64::MAX
} else if x <= f64::NEG_INFINITY {
f64::MIN
} else if x == 0.0 {
f64::from_bits(1).copysign(y)
} else {
// next x after 0 if y is farther from 0 than x, otherwise next towards 0
// the sign is a separate bit in floats, so bits+1 moves away from 0 no matter the float
let b = x.to_bits();
let bits = if (y > x) == (x > 0.0) { b + 1 } else { b - 1 };
let ret = f64::from_bits(bits);
if ret == 0.0 {
ret.copysign(x)
} else {
ret
}
}
}

pub fn ulp(x: f64) -> f64 {
if x.is_nan() {
return x;
}
let x = x.abs();
let x2 = nextafter(x, f64::INFINITY);
if x2.is_infinite() {
// special case: x is the largest positive representable float
let x2 = nextafter(x, f64::NEG_INFINITY);
x - x2
} else {
x2 - x
}
}

#[test]
fn test_to_hex() {
use rand::Rng;
Expand Down
1 change: 1 addition & 0 deletions common/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! A crate to hold types and functions common to all rustpython components.

pub mod borrow;
pub mod cmp;
pub mod float_ops;
pub mod hash;
pub mod lock;
Expand Down
1 change: 0 additions & 1 deletion vm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ sha-1 = "0.8"
sha2 = "0.8"
sha3 = "0.8"
blake2 = "0.8"
volatile = "0.3"

num-complex = { version = "0.3", features = ["serde"] }
num-bigint = { version = "0.3", features = ["serde"] }
Expand Down
Loading