From bbd274a498e3872edd03d039de8f6148f08cc805 Mon Sep 17 00:00:00 2001 From: "johan.park" Date: Sun, 3 Nov 2019 01:55:10 +0900 Subject: [PATCH] Improve float.fromhex module representation case - Embrace hexadecimal without '0x', '.', 'p' - Embrace variety of 'nan', 'inf' case - Still overflow and near min, max, 0, 1 case isn't handled --- tests/snippets/floats.py | 211 +++++++++++++++++++++++++++++++++++---- vm/src/obj/objfloat.rs | 52 +++++++--- 2 files changed, 233 insertions(+), 30 deletions(-) diff --git a/tests/snippets/floats.py b/tests/snippets/floats.py index 0b63b3153..bf0b6c8d1 100644 --- a/tests/snippets/floats.py +++ b/tests/snippets/floats.py @@ -2,6 +2,10 @@ import math from testutils import assert_raises +NAN = float('nan') +INF = float('inf') +NINF = float('-inf') + 1 + 1.1 a = 1.2 @@ -240,21 +244,194 @@ a = 3.1__4 with assert_raises(SyntaxError): exec(src) -assert float.fromhex('0.0') == 0.0 -assert float.fromhex('-0.0') == 0.0 -assert float.fromhex('0x0p0') == 0.0 -assert float.fromhex('0x010p0') == 16.0 -assert float.fromhex('0x0.10p3') == 0.5 -assert float.fromhex('-0x0p0') == 0.0 -assert float.fromhex('0x0.p0') == 0.0 -assert float.fromhex('-0x0.p0') == 0.0 -assert float.fromhex('0x0.0p+0') == 0.0 -assert float.fromhex('-0x0.0p+0') == -0.0 -assert float.fromhex('0x1.000000p+0') == 1.0 -assert float.fromhex('-0x1.800000p+0') == -1.5 -assert float.fromhex('inf') == float('inf') -assert math.isnan(float.fromhex('nan')) -assert_raises(ValueError, lambda: float.fromhex('error')) +fromHex = float.fromhex +def identical(x, y): + if math.isnan(x) or math.isnan(y): + if math.isnan(x) == math.isnan(y): + return + elif x == y and (x != 0.0 or math.copysign(1.0, x) == math.copysign(1.0, y)): + return + raise SyntaxError(f"{x} not identical to {y}") + +invalid_inputs = [ + "infi", # misspelt infinities and nans + "-Infinit", + "++inf", + "-+Inf", + "--nan", + "+-NaN", + "snan", + "NaNs", + "nna", + "an", + "nf", + "nfinity", + "inity", + "iinity", + "0xnan", + "", + " ", + "x1.0p0", + "0xX1.0p0", + "+ 0x1.0p0", # internal whitespace + "- 0x1.0p0", + "0 x1.0p0", + "0x 1.0p0", + "0x1 2.0p0", + "+0x1 .0p0", + "0x1. 0p0", + "-0x1.0 1p0", + "-0x1.0 p0", + "+0x1.0p +0", + "0x1.0p -0", + "0x1.0p 0", + "+0x1.0p+ 0", + "-0x1.0p- 0", + "++0x1.0p-0", # double signs + "--0x1.0p0", + "+-0x1.0p+0", + "-+0x1.0p0", + "0x1.0p++0", + "+0x1.0p+-0", + "-0x1.0p-+0", + "0x1.0p--0", + "0x1.0.p0", + "0x.p0", # no hex digits before or after point + "0x1,p0", # wrong decimal point character + "0x1pa", + "0x1p\uff10", # fullwidth Unicode digits + "\uff10x1p0", + "0x\uff11p0", + "0x1.\uff10p0", + "0x1p0 \n 0x2p0", + "0x1p0\0 0x1p0", # embedded null byte is not end of string +] + +for x in invalid_inputs: + assert_raises(ValueError, lambda: fromHex(x)) + +value_pairs = [ + ("inf", INF), + ("-Infinity", -INF), + ("NaN", NAN), + ("1.0", 1.0), + ("-0x.2", -0.125), + ("-0.0", -0.0), +] +whitespace = [ + "", + " ", + "\t", + "\n", + "\n \t", + "\f", + "\v", + "\r" +] + +for inp, expected in value_pairs: + for lead in whitespace: + for trail in whitespace: + got = fromHex(lead + inp + trail) + identical(got, expected) + +MAX = fromHex('0x.fffffffffffff8p+1024') # max normal +MIN = fromHex('0x1p-1022') # min normal +TINY = fromHex('0x0.0000000000001p-1022') # min subnormal +EPS = fromHex('0x0.0000000000001p0') # diff between 1.0 and next float up + +# two spellings of infinity, with optional signs; case-insensitive +identical(fromHex('inf'), INF) +identical(fromHex('+Inf'), INF) +identical(fromHex('-INF'), -INF) +identical(fromHex('iNf'), INF) +identical(fromHex('Infinity'), INF) +identical(fromHex('+INFINITY'), INF) +identical(fromHex('-infinity'), -INF) +identical(fromHex('-iNFiNitY'), -INF) + +# nans with optional sign; case insensitive +identical(fromHex('nan'), NAN) +identical(fromHex('+NaN'), NAN) +identical(fromHex('-NaN'), NAN) +identical(fromHex('-nAN'), NAN) + +# variations in input format +identical(fromHex('1'), 1.0) +identical(fromHex('+1'), 1.0) +identical(fromHex('1.'), 1.0) +identical(fromHex('1.0'), 1.0) +identical(fromHex('1.0p0'), 1.0) +identical(fromHex('01'), 1.0) +identical(fromHex('01.'), 1.0) +identical(fromHex('0x1'), 1.0) +identical(fromHex('0x1.'), 1.0) +identical(fromHex('0x1.0'), 1.0) +identical(fromHex('+0x1.0'), 1.0) +identical(fromHex('0x1p0'), 1.0) +identical(fromHex('0X1p0'), 1.0) +identical(fromHex('0X1P0'), 1.0) +identical(fromHex('0x1P0'), 1.0) +identical(fromHex('0x1.p0'), 1.0) +identical(fromHex('0x1.0p0'), 1.0) +identical(fromHex('0x.1p4'), 1.0) +identical(fromHex('0x.1p04'), 1.0) +identical(fromHex('0x.1p004'), 1.0) +identical(fromHex('0x1p+0'), 1.0) +identical(fromHex('0x1P-0'), 1.0) +identical(fromHex('+0x1p0'), 1.0) +identical(fromHex('0x01p0'), 1.0) +identical(fromHex('0x1p00'), 1.0) +identical(fromHex(' 0x1p0 '), 1.0) +identical(fromHex('\n 0x1p0'), 1.0) +identical(fromHex('0x1p0 \t'), 1.0) +identical(fromHex('0xap0'), 10.0) +identical(fromHex('0xAp0'), 10.0) +identical(fromHex('0xaP0'), 10.0) +identical(fromHex('0xAP0'), 10.0) +identical(fromHex('0xbep0'), 190.0) +identical(fromHex('0xBep0'), 190.0) +identical(fromHex('0xbEp0'), 190.0) +identical(fromHex('0XBE0P-4'), 190.0) +identical(fromHex('0xBEp0'), 190.0) +identical(fromHex('0xB.Ep4'), 190.0) +identical(fromHex('0x.BEp8'), 190.0) +identical(fromHex('0x.0BEp12'), 190.0) + +# moving the point around +pi = fromHex('0x1.921fb54442d18p1') +identical(fromHex('0x.006487ed5110b46p11'), pi) +identical(fromHex('0x.00c90fdaa22168cp10'), pi) +identical(fromHex('0x.01921fb54442d18p9'), pi) +identical(fromHex('0x.03243f6a8885a3p8'), pi) +identical(fromHex('0x.06487ed5110b46p7'), pi) +identical(fromHex('0x.0c90fdaa22168cp6'), pi) +identical(fromHex('0x.1921fb54442d18p5'), pi) +identical(fromHex('0x.3243f6a8885a3p4'), pi) +identical(fromHex('0x.6487ed5110b46p3'), pi) +identical(fromHex('0x.c90fdaa22168cp2'), pi) +identical(fromHex('0x1.921fb54442d18p1'), pi) +identical(fromHex('0x3.243f6a8885a3p0'), pi) +identical(fromHex('0x6.487ed5110b46p-1'), pi) +identical(fromHex('0xc.90fdaa22168cp-2'), pi) +identical(fromHex('0x19.21fb54442d18p-3'), pi) +identical(fromHex('0x32.43f6a8885a3p-4'), pi) +identical(fromHex('0x64.87ed5110b46p-5'), pi) +identical(fromHex('0xc9.0fdaa22168cp-6'), pi) +identical(fromHex('0x192.1fb54442d18p-7'), pi) +identical(fromHex('0x324.3f6a8885a3p-8'), pi) +identical(fromHex('0x648.7ed5110b46p-9'), pi) +identical(fromHex('0xc90.fdaa22168cp-10'), pi) +identical(fromHex('0x1921.fb54442d18p-11'), pi) +identical(fromHex('0x1921fb54442d1.8p-47'), pi) +identical(fromHex('0x3243f6a8885a3p-48'), pi) +identical(fromHex('0x6487ed5110b46p-49'), pi) +identical(fromHex('0xc90fdaa22168cp-50'), pi) +identical(fromHex('0x1921fb54442d18p-51'), pi) +identical(fromHex('0x3243f6a8885a30p-52'), pi) +identical(fromHex('0x6487ed5110b460p-53'), pi) +identical(fromHex('0xc90fdaa22168c0p-54'), pi) +identical(fromHex('0x1921fb54442d180p-55'), pi) assert (0.0).hex() == '0x0.0p+0' assert (-0.0).hex() == '-0x0.0p+0' @@ -266,10 +443,10 @@ assert float('nan').hex() == 'nan' #for _ in range(10000): # f = random.random() * random.randint(0, 0x10000000000000000) -# assert f == float.fromhex(f.hex()) +# assert f == fromHex(f.hex()) # Test float exponent: -assert 1 if 1else 0 == 1 +assert 1 if 1 else 0 == 1 a = 3. assert a.__eq__(3) is True diff --git a/vm/src/obj/objfloat.rs b/vm/src/obj/objfloat.rs index 706bc76f0..17eb76a9f 100644 --- a/vm/src/obj/objfloat.rs +++ b/vm/src/obj/objfloat.rs @@ -583,30 +583,56 @@ impl PyFloat { #[pymethod] fn fromhex(repr: PyStringRef, vm: &VirtualMachine) -> PyResult { - hexf_parse::parse_hexf64(repr.as_str(), false) - .or_else(|_| repr.as_str().parse::()) - .or_else(|_| match repr.as_str() { + hexf_parse::parse_hexf64(repr.as_str().trim(), false).or_else(|_| { + match repr.as_str().to_lowercase().trim() { "nan" => Ok(std::f64::NAN), + "+nan" => Ok(std::f64::NAN), + "-nan" => Ok(std::f64::NAN), "inf" => Ok(std::f64::INFINITY), + "infinity" => Ok(std::f64::INFINITY), + "+inf" => Ok(std::f64::INFINITY), + "+infinity" => Ok(std::f64::INFINITY), "-inf" => Ok(std::f64::NEG_INFINITY), + "-infinity" => Ok(std::f64::NEG_INFINITY), value => { let mut hex = String::new(); - if value.contains("0x") { - for ch in value.chars() { - if ch == 'p' { - hex.push_str(".p"); - } else { - hex.push(ch); - } + let has_0x = value.contains("0x"); + let has_p = value.contains('p'); + let has_dot = value.contains('.'); + let mut start = 0; + + if !has_0x && value.starts_with('-') { + hex.push_str("-0x"); + start += 1; + } else if !has_0x { + hex.push_str("0x"); + if value.starts_with('+') { + start += 1; } - } else { - hex = value.to_string(); } + + for (index, ch) in value.chars().enumerate() { + if ch == 'p' && has_dot { + hex.push_str("p"); + } else if ch == 'p' && !has_dot { + hex.push_str(".p"); + } else if index >= start { + hex.push(ch); + } + } + + if !has_p && has_dot { + hex.push_str("p0"); + } else if !has_p && !has_dot { + hex.push_str(".p0") + } + hexf_parse::parse_hexf64(hex.as_str(), false).map_err(|_| { vm.new_value_error("invalid hexadecimal floating-point string".to_string()) }) } - }) + } + }) } #[pymethod]