diff --git a/.gitignore b/.gitignore index 49f192f2..06639cad 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ Cargo.lock *.pyc TODO.md *.diff +!/lexical-parse-float/etc/correctness/Cargo.lock # Perftools perf.data diff --git a/ci/comprehensive.sh b/ci/comprehensive.sh index 6c52493a..702a9ce5 100755 --- a/ci/comprehensive.sh +++ b/ci/comprehensive.sh @@ -38,8 +38,8 @@ run_tests --all-features cd "${home}" if [ ! -z "${EXHAUSTIVE}" ]; then - if [ -z "${PYTHON}" ]; then - PYTHON=python - fi - $PYTHON "${home}/lexical-parse-float/etc/correctness/test-parse-random/runtests.py" +# Test the parse-float correctness tests + cd "${home}" + cd lexical-parse-float/etc/correctness + cargo run "${@}" --release --bin test-parse-random fi diff --git a/lexical-parse-float/etc/correctness/Cargo.lock b/lexical-parse-float/etc/correctness/Cargo.lock new file mode 100644 index 00000000..b80b4e3c --- /dev/null +++ b/lexical-parse-float/etc/correctness/Cargo.lock @@ -0,0 +1,613 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "windows-sys", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "indicatif" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" +dependencies = [ + "console", + "instant", + "number_prefix", + "portable-atomic", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lexical-parse-float" +version = "1.0.2" +dependencies = [ + "lexical-parse-integer", + "lexical-util", + "static_assertions", +] + +[[package]] +name = "lexical-parse-float-correctness" +version = "0.0.1" +dependencies = [ + "indicatif", + "lazy_static", + "lexical-parse-float", + "lexical-util", + "num", + "rand", + "rand_chacha", + "rand_isaac", + "rayon", + "regex", + "serde", + "serde_json", + "toml", +] + +[[package]] +name = "lexical-parse-integer" +version = "1.0.2" +dependencies = [ + "lexical-util", + "static_assertions", +] + +[[package]] +name = "lexical-util" +version = "1.0.3" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "libc" +version = "0.2.161" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "portable-atomic" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_isaac" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4373cd91b4f55722c553fb0f286edbb81ef3ff6eec7b99d1898a4110a0b28" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "serde" +version = "1.0.214" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.214" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.132" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "syn" +version = "2.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89275301d38033efb81a6e60e3497e734dfcc62571f2854bf4b16690398824c" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/lexical-parse-float/etc/correctness/Cargo.toml b/lexical-parse-float/etc/correctness/Cargo.toml index 4be39367..95263dfc 100644 --- a/lexical-parse-float/etc/correctness/Cargo.toml +++ b/lexical-parse-float/etc/correctness/Cargo.toml @@ -16,7 +16,11 @@ default-features = false features = [] [dependencies] -rand = "0.8" +rand = "0.8.5" +indicatif = { version = "0.17.8", default-features = false } +num = "0.4.3" +rand_chacha = "0.3" +rayon = "1" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" toml = "0.8" @@ -34,46 +38,13 @@ digit-separator = ["format", "regex", "lazy_static"] [workspace] -# Special testing binaries for the runtests.py scripts. -[[bin]] -name = "few_ones" -path = "test-parse-random/few_ones.rs" - -[[bin]] -name = "huge-pow10" -path = "test-parse-random/huge-pow10.rs" - -[[bin]] -name = "long-fractions" -path = "test-parse-random/long-fractions.rs" - -[[bin]] -name = "many-digits" -path = "test-parse-random/many-digits.rs" - -[[bin]] -name = "rand-f64" -path = "test-parse-random/rand-f64.rs" - -[[bin]] -name = "short-decimals" -path = "test-parse-random/short-decimals.rs" - -[[bin]] -name = "subnorm" -path = "test-parse-random/subnorm.rs" - -[[bin]] -name = "tiny-pow10" -path = "test-parse-random/tiny-pow10.rs" - -[[bin]] -name = "u32-small" -path = "test-parse-random/u32-small.rs" +[lib] +name = "test_parse_random" +path = "test-parse-random/lib.rs" [[bin]] -name = "u64-pow2" -path = "test-parse-random/u64-pow2.rs" +name = "test-parse-random" +path = "test-parse-random/main.rs" [[bin]] name = "test-parse-golang" diff --git a/lexical-parse-float/etc/correctness/test-parse-random/README.md b/lexical-parse-float/etc/correctness/test-parse-random/README.md new file mode 100644 index 00000000..21b20d0a --- /dev/null +++ b/lexical-parse-float/etc/correctness/test-parse-random/README.md @@ -0,0 +1,55 @@ +# Float Parsing Tests + +These are tests designed to test decimal to float conversions (`dec2flt`) used +by the standard library. + +It consistes of a collection of test generators that each generate a set of +patterns intended to test a specific property. In addition, there are exhaustive +tests (for <= `f32`) and fuzzers (for anything that can't be run exhaustively). + +The generators work as follows: + +- Each generator is a struct that lives somewhere in the `gen` module. Usually + it is generic over a float type. +- These generators must implement `Iterator`, which should return a context type + that can be used to construct a test string (but usually not the string + itself). +- They must also implement the `Generator` trait, which provides a method to + write test context to a string as a test case, as well as some extra metadata. + + The split between context generation and string construction is so that we can + reuse string allocations. +- Each generator gets registered once for each float type. Each of these + generators then get their iterator called, and each test case checked against + the float type's parse implementation. + +Some generators produce decimal strings, others create bit patterns that need to +be bitcasted to the float type, which then uses its `Display` implementation to +write to a string. For these, float to decimal (`flt2dec`) conversions also get +tested, if unintentionally. + +For each test case, the following is done: + +- The test string is parsed to the float type using the standard library's + implementation. +- The test string is parsed separately to a `BigRational`, which acts as a + representation with infinite precision. +- The rational value then gets checked that it is within the float's + representable values (absolute value greater than the smallest number to round + to zero, but less less than the first value to round to infinity). If these + limits are exceeded, check that the parsed float reflects that. +- For real nonzero numbers, the parsed float is converted into a rational using + `significand * 2^exponent`. It is then checked against the actual rational + value, and verified to be within half a bit's precision of the parsed value. + Also it is checked that ties round to even. + +This is all highly parallelized with `rayon`; test generators can run in +parallel, and their tests get chunked and run in parallel. + +There is a simple command line that allows filtering which tests are run, +setting the number of iterations for fuzzing tests, limiting failures, setting +timeouts, etc. See `main.rs` or run with `--help` for options. + +Note that when running via `./x`, only tests that take less than a few minutes +are run by default. Navigate to the crate (or pass `-C` to Cargo) and run it +directly to run all tests or pass specific arguments. diff --git a/lexical-parse-float/etc/correctness/test-parse-random/_common.rs b/lexical-parse-float/etc/correctness/test-parse-random/_common.rs deleted file mode 100644 index 4d415ddd..00000000 --- a/lexical-parse-float/etc/correctness/test-parse-random/_common.rs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2015 The Rust Project Developers. See the COPYRIGHT -// file at the top-level directory of this distribution and at -// http://rust-lang.org/COPYRIGHT. -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -use lexical_parse_float::FromLexical; -use std::io; -use std::io::prelude::*; -use std::mem::transmute; - -// Nothing up my sleeve: Just (PI - 3) in base 16. -#[allow(dead_code)] -pub const SEED: [u32; 3] = [0x243f_6a88, 0x85a3_08d3, 0x1319_8a2e]; -#[allow(dead_code)] -pub const ISAAC_SEED: [u8; 32] = [ - 49, 52, 49, 53, 57, 50, 54, 53, 51, 53, 56, 57, 55, 57, 51, 50, 51, 56, 52, 54, 50, 54, 52, 51, - 51, 56, 51, 50, 55, 57, 53, 48, -]; - -pub fn validate(text: &str) { - let mut out = io::stdout(); - let x: f64 = f64::from_lexical(text.as_bytes()).unwrap(); - let f64_bytes: u64 = unsafe { transmute(x) }; - let x: f32 = f32::from_lexical(text.as_bytes()).unwrap(); - let f32_bytes: u32 = unsafe { transmute(x) }; - writeln!(&mut out, "{:016x} {:08x} {}", f64_bytes, f32_bytes, text).unwrap(); -} diff --git a/lexical-parse-float/etc/correctness/test-parse-random/few_ones.rs b/lexical-parse-float/etc/correctness/test-parse-random/few_ones.rs deleted file mode 100644 index 2486df44..00000000 --- a/lexical-parse-float/etc/correctness/test-parse-random/few_ones.rs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2015 The Rust Project Developers. See the COPYRIGHT -// file at the top-level directory of this distribution and at -// http://rust-lang.org/COPYRIGHT. -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -mod _common; - -use _common::validate; - -fn main() { - let mut pow = vec![]; - for i in 0..63 { - pow.push(1u64 << i); - } - for a in &pow { - for b in &pow { - for c in &pow { - validate(&(a | b | c).to_string()); - } - } - } -} diff --git a/lexical-parse-float/etc/correctness/test-parse-random/gen/exhaustive.rs b/lexical-parse-float/etc/correctness/test-parse-random/gen/exhaustive.rs new file mode 100644 index 00000000..5d4b6df8 --- /dev/null +++ b/lexical-parse-float/etc/correctness/test-parse-random/gen/exhaustive.rs @@ -0,0 +1,43 @@ +use std::fmt::Write; +use std::ops::RangeInclusive; + +use crate::{Float, Generator, Int}; + +/// Test every possible bit pattern. This is infeasible to run on any float types larger than +/// `f32` (which takes about an hour). +pub struct Exhaustive { + iter: RangeInclusive, +} + +impl Generator for Exhaustive +where + RangeInclusive: Iterator, +{ + const NAME: &'static str = "exhaustive"; + const SHORT_NAME: &'static str = "exhaustive"; + + type WriteCtx = F; + + fn total_tests() -> u64 { + F::Int::MAX.try_into().unwrap_or(u64::MAX) + } + + fn new() -> Self { + Self { iter: F::Int::ZERO..=F::Int::MAX } + } + + fn write_string(s: &mut String, ctx: Self::WriteCtx) { + write!(s, "{ctx:e}").unwrap(); + } +} + +impl Iterator for Exhaustive +where + RangeInclusive: Iterator, +{ + type Item = F; + + fn next(&mut self) -> Option { + Some(F::from_bits(self.iter.next()?)) + } +} diff --git a/lexical-parse-float/etc/correctness/test-parse-random/gen/exponents.rs b/lexical-parse-float/etc/correctness/test-parse-random/gen/exponents.rs new file mode 100644 index 00000000..3748e9d3 --- /dev/null +++ b/lexical-parse-float/etc/correctness/test-parse-random/gen/exponents.rs @@ -0,0 +1,95 @@ +use std::fmt::Write; +use std::ops::RangeInclusive; + +use crate::traits::BoxGenIter; +use crate::{Float, Generator}; + +const SMALL_COEFF_MAX: i32 = 10_000; +const SMALL_EXP_MAX: i32 = 300; + +const SMALL_COEFF_RANGE: RangeInclusive = (-SMALL_COEFF_MAX)..=SMALL_COEFF_MAX; +const SMALL_EXP_RANGE: RangeInclusive = (-SMALL_EXP_MAX)..=SMALL_EXP_MAX; + +const LARGE_COEFF_RANGE: RangeInclusive = 0..=100_000; +const LARGE_EXP_RANGE: RangeInclusive = 300..=350; + +/// Check exponential values around zero. +pub struct SmallExponents { + iter: BoxGenIter, +} + +impl Generator for SmallExponents { + const NAME: &'static str = "small exponents"; + const SHORT_NAME: &'static str = "small exp"; + + /// `(coefficient, exponent)` + type WriteCtx = (i32, i32); + + fn total_tests() -> u64 { + ((1 + SMALL_COEFF_RANGE.end() - SMALL_COEFF_RANGE.start()) + * (1 + SMALL_EXP_RANGE.end() - SMALL_EXP_RANGE.start())) + .try_into() + .unwrap() + } + + fn new() -> Self { + let iter = SMALL_EXP_RANGE.flat_map(|exp| SMALL_COEFF_RANGE.map(move |coeff| (coeff, exp))); + + Self { iter: Box::new(iter) } + } + + fn write_string(s: &mut String, ctx: Self::WriteCtx) { + let (coeff, exp) = ctx; + write!(s, "{coeff}e{exp}").unwrap(); + } +} + +impl Iterator for SmallExponents { + type Item = (i32, i32); + + fn next(&mut self) -> Option { + self.iter.next() + } +} + +/// Check exponential values further from zero. +pub struct LargeExponents { + iter: BoxGenIter, +} + +impl Generator for LargeExponents { + const NAME: &'static str = "large positive exponents"; + const SHORT_NAME: &'static str = "large exp"; + + /// `(coefficient, exponent, is_positive)` + type WriteCtx = (u32, u32, bool); + + fn total_tests() -> u64 { + ((1 + LARGE_EXP_RANGE.end() - LARGE_EXP_RANGE.start()) + * (1 + LARGE_COEFF_RANGE.end() - LARGE_COEFF_RANGE.start()) + * 2) + .into() + } + + fn new() -> Self { + let iter = LARGE_EXP_RANGE + .flat_map(|exp| LARGE_COEFF_RANGE.map(move |coeff| (coeff, exp))) + .flat_map(|(coeff, exp)| [(coeff, exp, false), (coeff, exp, true)]); + + Self { iter: Box::new(iter) } + } + + fn write_string(s: &mut String, ctx: Self::WriteCtx) { + let (coeff, exp, is_positive) = ctx; + let sign = if is_positive { "" } else { "-" }; + write!(s, "{sign}{coeff}e{exp}").unwrap(); + } +} + +impl Iterator for LargeExponents { + type Item = (u32, u32, bool); + + fn next(&mut self) -> Option { + self.iter.next() + } +} diff --git a/lexical-parse-float/etc/correctness/test-parse-random/gen/fuzz.rs b/lexical-parse-float/etc/correctness/test-parse-random/gen/fuzz.rs new file mode 100644 index 00000000..213bcfc6 --- /dev/null +++ b/lexical-parse-float/etc/correctness/test-parse-random/gen/fuzz.rs @@ -0,0 +1,88 @@ +use std::any::{type_name, TypeId}; +use std::collections::BTreeMap; +use std::fmt::Write; +use std::marker::PhantomData; +use std::ops::Range; +use std::sync::Mutex; + +use rand::distributions::{Distribution, Standard}; +use rand::Rng; +use rand_chacha::rand_core::SeedableRng; +use rand_chacha::ChaCha8Rng; + +use crate::{Float, Generator, Int, SEED}; + +/// Mapping of float types to the number of iterations that should be run. +/// +/// We could probably make `Generator::new` take an argument instead of the global state, +/// but we only load this once so it works. +static FUZZ_COUNTS: Mutex> = Mutex::new(BTreeMap::new()); + +/// Generic fuzzer; just tests deterministic random bit patterns N times. +pub struct Fuzz { + iter: Range, + rng: ChaCha8Rng, + /// Allow us to use generics in `Iterator`. + marker: PhantomData, +} + +impl Fuzz { + /// Register how many iterations the fuzzer should run for a type. Uses some logic by + /// default, but if `from_cfg` is `Some`, that will be used instead. + pub fn set_iterations(from_cfg: Option) { + let count = if let Some(cfg_count) = from_cfg { + cfg_count + } else if F::BITS <= crate::MAX_BITS_FOR_EXHAUUSTIVE { + // If we run exhaustively, still fuzz but only do half as many bits. The only goal here is + // to catch failures from e.g. high bit patterns before exhaustive tests would get to them. + (F::Int::MAX >> (F::BITS / 2)).try_into().unwrap() + } else { + // Eveything bigger gets a fuzz test with as many iterations as `f32` exhaustive. + u32::MAX.into() + }; + + let _ = FUZZ_COUNTS.lock().unwrap().insert(TypeId::of::(), count); + } +} + +impl Generator for Fuzz +where + Standard: Distribution<::Int>, +{ + const NAME: &'static str = "fuzz"; + const SHORT_NAME: &'static str = "fuzz"; + + type WriteCtx = F; + + fn total_tests() -> u64 { + *FUZZ_COUNTS + .lock() + .unwrap() + .get(&TypeId::of::()) + .unwrap_or_else(|| panic!("missing fuzz count for {}", type_name::())) + } + + fn new() -> Self { + let rng = ChaCha8Rng::from_seed(SEED); + + Self { iter: 0..Self::total_tests(), rng, marker: PhantomData } + } + + fn write_string(s: &mut String, ctx: Self::WriteCtx) { + write!(s, "{ctx:e}").unwrap(); + } +} + +impl Iterator for Fuzz +where + Standard: Distribution<::Int>, +{ + type Item = >::WriteCtx; + + fn next(&mut self) -> Option { + let _ = self.iter.next()?; + let i: F::Int = self.rng.gen(); + + Some(F::from_bits(i)) + } +} diff --git a/lexical-parse-float/etc/correctness/test-parse-random/gen/integers.rs b/lexical-parse-float/etc/correctness/test-parse-random/gen/integers.rs new file mode 100644 index 00000000..070d188e --- /dev/null +++ b/lexical-parse-float/etc/correctness/test-parse-random/gen/integers.rs @@ -0,0 +1,104 @@ +use std::fmt::Write; +use std::ops::{Range, RangeInclusive}; + +use crate::traits::BoxGenIter; +use crate::{Float, Generator}; + +const SMALL_MAX_POW2: u32 = 19; + +/// All values up to the max power of two +const SMALL_VALUES: RangeInclusive = { + let max = 1i32 << SMALL_MAX_POW2; + (-max)..=max +}; + +/// Large values only get tested around powers of two +const LARGE_POWERS: Range = SMALL_MAX_POW2..128; + +/// We perturbe each large value around these ranges +const LARGE_PERTURBATIONS: RangeInclusive = -256..=256; + +/// Test all integers up to `2 ^ MAX_POW2` +pub struct SmallInt { + iter: RangeInclusive, +} + +impl Generator for SmallInt { + const NAME: &'static str = "small integer values"; + const SHORT_NAME: &'static str = "int small"; + + type WriteCtx = i32; + + fn total_tests() -> u64 { + (SMALL_VALUES.end() + 1 - SMALL_VALUES.start()).try_into().unwrap() + } + + fn new() -> Self { + Self { iter: SMALL_VALUES } + } + + fn write_string(s: &mut String, ctx: Self::WriteCtx) { + write!(s, "{ctx}").unwrap(); + } +} + +impl Iterator for SmallInt { + type Item = i32; + + fn next(&mut self) -> Option { + self.iter.next() + } +} + +/// Test much bigger integers than [`SmallInt`]. +pub struct LargeInt { + iter: BoxGenIter, +} + +impl LargeInt { + const EDGE_CASES: [i128; 7] = [ + i32::MIN as i128, + i32::MAX as i128, + i64::MIN as i128, + i64::MAX as i128, + u64::MAX as i128, + i128::MIN, + i128::MAX, + ]; +} + +impl Generator for LargeInt { + const NAME: &'static str = "large integer values"; + const SHORT_NAME: &'static str = "int large"; + + type WriteCtx = i128; + + fn total_tests() -> u64 { + u64::try_from( + (i128::from(LARGE_POWERS.end - LARGE_POWERS.start) + + i128::try_from(Self::EDGE_CASES.len()).unwrap()) + * (LARGE_PERTURBATIONS.end() + 1 - LARGE_PERTURBATIONS.start()), + ) + .unwrap() + } + + fn new() -> Self { + let iter = LARGE_POWERS + .map(|pow| 1i128 << pow) + .chain(Self::EDGE_CASES) + .flat_map(|base| LARGE_PERTURBATIONS.map(move |perturb| base.saturating_add(perturb))); + + Self { iter: Box::new(iter) } + } + + fn write_string(s: &mut String, ctx: Self::WriteCtx) { + write!(s, "{ctx}").unwrap(); + } +} +impl Iterator for LargeInt { + type Item = i128; + + fn next(&mut self) -> Option { + self.iter.next() + } +} diff --git a/lexical-parse-float/etc/correctness/test-parse-random/gen/long_fractions.rs b/lexical-parse-float/etc/correctness/test-parse-random/gen/long_fractions.rs new file mode 100644 index 00000000..b75148b7 --- /dev/null +++ b/lexical-parse-float/etc/correctness/test-parse-random/gen/long_fractions.rs @@ -0,0 +1,58 @@ +use std::char; +use std::fmt::Write; + +use crate::{Float, Generator}; + +/// Number of decimal digits to check (all of them). +const MAX_DIGIT: u32 = 9; +/// Test with this many decimals in the string. +const MAX_DECIMALS: usize = 410; +const PREFIX: &str = "0."; + +/// Test e.g. `0.1`, `0.11`, `0.111`, `0.1111`, ..., `0.2`, `0.22`, ... +pub struct RepeatingDecimal { + digit: u32, + buf: String, +} + +impl Generator for RepeatingDecimal { + const NAME: &'static str = "repeating decimal"; + const SHORT_NAME: &'static str = "dec rep"; + + type WriteCtx = String; + + fn total_tests() -> u64 { + u64::from(MAX_DIGIT + 1) * u64::try_from(MAX_DECIMALS + 1).unwrap() + 1 + } + + fn new() -> Self { + Self { digit: 0, buf: PREFIX.to_owned() } + } + + fn write_string(s: &mut String, ctx: Self::WriteCtx) { + *s = ctx; + } +} + +impl Iterator for RepeatingDecimal { + type Item = String; + + fn next(&mut self) -> Option { + if self.digit > MAX_DIGIT { + return None; + } + + let digit = self.digit; + let inc_digit = self.buf.len() - PREFIX.len() > MAX_DECIMALS; + + if inc_digit { + // Reset the string + self.buf.clear(); + self.digit += 1; + self.buf.write_str(PREFIX).unwrap(); + } + + self.buf.push(char::from_digit(digit, 10).unwrap()); + Some(self.buf.clone()) + } +} diff --git a/lexical-parse-float/etc/correctness/test-parse-random/gen/many_digits.rs b/lexical-parse-float/etc/correctness/test-parse-random/gen/many_digits.rs new file mode 100644 index 00000000..aab8d5d7 --- /dev/null +++ b/lexical-parse-float/etc/correctness/test-parse-random/gen/many_digits.rs @@ -0,0 +1,84 @@ +use std::char; +use std::fmt::Write; +use std::marker::PhantomData; +use std::ops::{Range, RangeInclusive}; + +use rand::distributions::{Distribution, Uniform}; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha8Rng; + +use crate::{Float, Generator, SEED}; + +/// Total iterations +const ITERATIONS: u64 = 5_000_000; + +/// Possible lengths of the string, excluding decimals and exponents +const POSSIBLE_NUM_DIGITS: RangeInclusive = 100..=400; + +/// Range of possible exponents +const EXP_RANGE: Range = -4500..4500; + +/// Try strings of random digits. +pub struct RandDigits { + rng: ChaCha8Rng, + iter: Range, + uniform: Uniform, + /// Allow us to use generics in `Iterator`. + marker: PhantomData, +} + +impl Generator for RandDigits { + const NAME: &'static str = "random digits"; + + const SHORT_NAME: &'static str = "rand digits"; + + type WriteCtx = String; + + fn total_tests() -> u64 { + ITERATIONS + } + + fn new() -> Self { + let rng = ChaCha8Rng::from_seed(SEED); + let range = Uniform::from(0..10); + + Self { rng, iter: 0..ITERATIONS, uniform: range, marker: PhantomData } + } + + fn write_string(s: &mut String, ctx: Self::WriteCtx) { + *s = ctx; + } +} + +impl Iterator for RandDigits { + type Item = String; + + fn next(&mut self) -> Option { + let _ = self.iter.next()?; + let num_digits = self.rng.gen_range(POSSIBLE_NUM_DIGITS); + let has_decimal = self.rng.gen_bool(0.2); + let has_exp = self.rng.gen_bool(0.2); + + let dec_pos = if has_decimal { Some(self.rng.gen_range(0..num_digits)) } else { None }; + + let mut s = String::with_capacity(num_digits); + + for pos in 0..num_digits { + let digit = char::from_digit(self.uniform.sample(&mut self.rng), 10).unwrap(); + s.push(digit); + + if let Some(dec_pos) = dec_pos { + if pos == dec_pos { + s.push('.'); + } + } + } + + if has_exp { + let exp = self.rng.gen_range(EXP_RANGE); + write!(s, "e{exp}").unwrap(); + } + + Some(s) + } +} diff --git a/lexical-parse-float/etc/correctness/test-parse-random/gen/sparse.rs b/lexical-parse-float/etc/correctness/test-parse-random/gen/sparse.rs new file mode 100644 index 00000000..389b7105 --- /dev/null +++ b/lexical-parse-float/etc/correctness/test-parse-random/gen/sparse.rs @@ -0,0 +1,100 @@ +use std::fmt::Write; + +use crate::traits::BoxGenIter; +use crate::{Float, Generator}; + +const POWERS_OF_TWO: [u128; 128] = make_powers_of_two(); + +const fn make_powers_of_two() -> [u128; 128] { + let mut ret = [0; 128]; + let mut i = 0; + while i < 128 { + ret[i] = 1 << i; + i += 1; + } + + ret +} + +/// Can't clone this result because of lifetime errors, just use a macro. +macro_rules! pow_iter { + () => { + (0..F::BITS).map(|i| F::Int::try_from(POWERS_OF_TWO[i as usize]).unwrap()) + }; +} + +/// Test all numbers that include three 1s in the binary representation as integers. +pub struct FewOnesInt +where + FewOnesInt: Generator, +{ + iter: BoxGenIter, +} + +impl Generator for FewOnesInt +where + >::Error: std::fmt::Debug, +{ + const NAME: &'static str = "few ones int"; + const SHORT_NAME: &'static str = "few ones int"; + + type WriteCtx = F::Int; + + fn total_tests() -> u64 { + u64::from(F::BITS).pow(3) + } + + fn new() -> Self { + let iter = pow_iter!() + .flat_map(move |a| pow_iter!().map(move |b| (a, b))) + .flat_map(move |(a, b)| pow_iter!().map(move |c| (a, b, c))) + .map(|(a, b, c)| a | b | c); + + Self { iter: Box::new(iter) } + } + + fn write_string(s: &mut String, ctx: Self::WriteCtx) { + write!(s, "{ctx}").unwrap(); + } +} + +impl Iterator for FewOnesInt { + type Item = F::Int; + + fn next(&mut self) -> Option { + self.iter.next() + } +} + +/// Similar to `FewOnesInt` except test those bit patterns as a float. +pub struct FewOnesFloat(FewOnesInt); + +impl Generator for FewOnesFloat +where + >::Error: std::fmt::Debug, +{ + const NAME: &'static str = "few ones float"; + const SHORT_NAME: &'static str = "few ones float"; + + type WriteCtx = F; + + fn total_tests() -> u64 { + FewOnesInt::::total_tests() + } + + fn new() -> Self { + Self(FewOnesInt::new()) + } + + fn write_string(s: &mut String, ctx: Self::WriteCtx) { + write!(s, "{ctx:e}").unwrap(); + } +} + +impl Iterator for FewOnesFloat { + type Item = F; + + fn next(&mut self) -> Option { + self.0.next().map(|i| F::from_bits(i)) + } +} diff --git a/lexical-parse-float/etc/correctness/test-parse-random/gen/spot_checks.rs b/lexical-parse-float/etc/correctness/test-parse-random/gen/spot_checks.rs new file mode 100644 index 00000000..18691f9d --- /dev/null +++ b/lexical-parse-float/etc/correctness/test-parse-random/gen/spot_checks.rs @@ -0,0 +1,101 @@ +use std::fmt::Write; + +use crate::traits::{Float, Generator}; + +const SPECIAL: &[&str] = &[ + "inf", "Inf", "iNf", "INF", "-inf", "-Inf", "-iNf", "-INF", "+inf", "+Inf", "+iNf", "+INF", + "nan", "NaN", "NAN", "nAn", "-nan", "-NaN", "-NAN", "-nAn", "+nan", "+NaN", "+NAN", "+nAn", + "1", "-1", "+1", "1e1", "-1e1", "+1e1", "1e-1", "-1e-1", "+1e-1", "1e+1", "-1e+1", "+1e+1", + "1E1", "-1E1", "+1E1", "1E-1", "-1E-1", "+1E-1", "1E+1", "-1E+1", "+1E+1", "0", "-0", "+0", +]; + +/// Check various non-numeric special strings. +pub struct Special { + iter: std::slice::Iter<'static, &'static str>, +} + +impl Generator for Special { + const NAME: &'static str = "special values"; + + const SHORT_NAME: &'static str = "special"; + + type WriteCtx = &'static str; + + fn total_tests() -> u64 { + SPECIAL.len().try_into().unwrap() + } + + fn new() -> Self { + Self { iter: SPECIAL.iter() } + } + + fn write_string(s: &mut String, ctx: Self::WriteCtx) { + s.write_str(ctx).unwrap(); + } +} + +impl Iterator for Special { + type Item = &'static str; + + fn next(&mut self) -> Option { + self.iter.next().copied() + } +} + +/// Strings that we know have failed in the past +const REGRESSIONS: &[&str] = &[ + // From + "1234567890123456789012345678901234567890e-340", + "2.225073858507201136057409796709131975934819546351645648023426109724822222021076945516529523908135087914149158913039621106870086438694594645527657207407820621743379988141063267329253552286881372149012981122451451889849057222307285255133155755015914397476397983411801999323962548289017107081850690630666655994938275772572015763062690663332647565300009245888316433037779791869612049497390377829704905051080609940730262937128958950003583799967207254304360284078895771796150945516748243471030702609144621572289880258182545180325707018860872113128079512233426288368622321503775666622503982534335974568884423900265498198385487948292206894721689831099698365846814022854243330660339850886445804001034933970427567186443383770486037861622771738545623065874679014086723327636718749999999999999999999999999999999999999e-308", + "2.22507385850720113605740979670913197593481954635164564802342610972482222202107694551652952390813508791414915891303962110687008643869459464552765720740782062174337998814106326732925355228688137214901298112245145188984905722230728525513315575501591439747639798341180199932396254828901710708185069063066665599493827577257201576306269066333264756530000924588831643303777979186961204949739037782970490505108060994073026293712895895000358379996720725430436028407889577179615094551674824347103070260914462157228988025818254518032570701886087211312807951223342628836862232150377566662250398253433597456888442390026549819838548794829220689472168983109969836584681402285424333066033985088644580400103493397042756718644338377048603786162277173854562306587467901408672332763671875e-308", + "0.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000222507385850720138309023271733240406421921598046233183055332741688720443481391819585428315901251102056406733973103581100515243416155346010885601238537771882113077799353200233047961014744258363607192156504694250373420837525080665061665815894872049117996859163964850063590877011830487479978088775374994945158045160505091539985658247081864511353793580499211598108576605199243335211435239014879569960959128889160299264151106346631339366347758651302937176204732563178148566435087212282863764204484681140761391147706280168985324411002416144742161856716615054015428508471675290190316132277889672970737312333408698898317506783884692609277397797285865965494109136909540613646756870239867831529068098461721092462539672851562500000000000000001", + "179769313486231580793728971405303415079934132710037826936173778980444968292764750946649017977587207096330286416692887910946555547851940402630657488671505820681908902000708383676273854845817711531764475730270069855571366959622842914819860834936475292719074168444365510704342711559699508093042880177904174497791.9999999999999999999999999999999999999999999999999999999999999999999999", + "2.47032822920623272e-324", + "6.631236871469758276785396630275967243399099947355303144249971758736286630139265439618068200788048744105960420552601852889715006376325666595539603330361800519107591783233358492337208057849499360899425128640718856616503093444922854759159988160304439909868291973931426625698663157749836252274523485312442358651207051292453083278116143932569727918709786004497872322193856150225415211997283078496319412124640111777216148110752815101775295719811974338451936095907419622417538473679495148632480391435931767981122396703443803335529756003353209830071832230689201383015598792184172909927924176339315507402234836120730914783168400715462440053817592702766213559042115986763819482654128770595766806872783349146967171293949598850675682115696218943412532098591327667236328125E-316", + "3.237883913302901289588352412501532174863037669423108059901297049552301970670676565786835742587799557860615776559838283435514391084153169252689190564396459577394618038928365305143463955100356696665629202017331344031730044369360205258345803431471660032699580731300954848363975548690010751530018881758184174569652173110473696022749934638425380623369774736560008997404060967498028389191878963968575439222206416981462690113342524002724385941651051293552601421155333430225237291523843322331326138431477823591142408800030775170625915670728657003151953664260769822494937951845801530895238439819708403389937873241463484205608000027270531106827387907791444918534771598750162812548862768493201518991668028251730299953143924168545708663913273994694463908672332763671875E-319", + "6.953355807847677105972805215521891690222119817145950754416205607980030131549636688806115726399441880065386399864028691275539539414652831584795668560082999889551357784961446896042113198284213107935110217162654939802416034676213829409720583759540476786936413816541621287843248433202369209916612249676005573022703244799714622116542188837770376022371172079559125853382801396219552418839469770514904192657627060319372847562301074140442660237844114174497210955449896389180395827191602886654488182452409583981389442783377001505462015745017848754574668342161759496661766020028752888783387074850773192997102997936619876226688096314989645766000479009083731736585750335262099860150896718774401964796827166283225641992040747894382698751809812609536720628966577351093292236328125E-310", + "3.339068557571188581835713701280943911923401916998521771655656997328440314559615318168849149074662609099998113009465566426808170378434065722991659642619467706034884424989741080790766778456332168200464651593995817371782125010668346652995912233993254584461125868481633343674905074271064409763090708017856584019776878812425312008812326260363035474811532236853359905334625575404216060622858633280744301892470300555678734689978476870369853549413277156622170245846166991655321535529623870646888786637528995592800436177901746286272273374471701452991433047257863864601424252024791567368195056077320885329384322332391564645264143400798619665040608077549162173963649264049738362290606875883456826586710961041737908872035803481241600376705491726170293986797332763671875E-319", + "2.4703282292062327208828439643411068618252990130716238221279284125033775363510437593264991818081799618989828234772285886546332835517796989819938739800539093906315035659515570226392290858392449105184435931802849936536152500319370457678249219365623669863658480757001585769269903706311928279558551332927834338409351978015531246597263579574622766465272827220056374006485499977096599470454020828166226237857393450736339007967761930577506740176324673600968951340535537458516661134223766678604162159680461914467291840300530057530849048765391711386591646239524912623653881879636239373280423891018672348497668235089863388587925628302755995657524455507255189313690836254779186948667994968324049705821028513185451396213837722826145437693412532098591327667236328124999e-324", + "2.4703282292062327208828439643411068618252990130716238221279284125033775363510437593264991818081799618989828234772285886546332835517796989819938739800539093906315035659515570226392290858392449105184435931802849936536152500319370457678249219365623669863658480757001585769269903706311928279558551332927834338409351978015531246597263579574622766465272827220056374006485499977096599470454020828166226237857393450736339007967761930577506740176324673600968951340535537458516661134223766678604162159680461914467291840300530057530849048765391711386591646239524912623653881879636239373280423891018672348497668235089863388587925628302755995657524455507255189313690836254779186948667994968324049705821028513185451396213837722826145437693412532098591327667236328125e-324", + "2.4703282292062327208828439643411068618252990130716238221279284125033775363510437593264991818081799618989828234772285886546332835517796989819938739800539093906315035659515570226392290858392449105184435931802849936536152500319370457678249219365623669863658480757001585769269903706311928279558551332927834338409351978015531246597263579574622766465272827220056374006485499977096599470454020828166226237857393450736339007967761930577506740176324673600968951340535537458516661134223766678604162159680461914467291840300530057530849048765391711386591646239524912623653881879636239373280423891018672348497668235089863388587925628302755995657524455507255189313690836254779186948667994968324049705821028513185451396213837722826145437693412532098591327667236328125001e-324", + "7.4109846876186981626485318930233205854758970392148714663837852375101326090531312779794975454245398856969484704316857659638998506553390969459816219401617281718945106978546710679176872575177347315553307795408549809608457500958111373034747658096871009590975442271004757307809711118935784838675653998783503015228055934046593739791790738723868299395818481660169122019456499931289798411362062484498678713572180352209017023903285791732520220528974020802906854021606612375549983402671300035812486479041385743401875520901590172592547146296175134159774938718574737870961645638908718119841271673056017045493004705269590165763776884908267986972573366521765567941072508764337560846003984904972149117463085539556354188641513168478436313080237596295773983001708984374999e-324", + "7.4109846876186981626485318930233205854758970392148714663837852375101326090531312779794975454245398856969484704316857659638998506553390969459816219401617281718945106978546710679176872575177347315553307795408549809608457500958111373034747658096871009590975442271004757307809711118935784838675653998783503015228055934046593739791790738723868299395818481660169122019456499931289798411362062484498678713572180352209017023903285791732520220528974020802906854021606612375549983402671300035812486479041385743401875520901590172592547146296175134159774938718574737870961645638908718119841271673056017045493004705269590165763776884908267986972573366521765567941072508764337560846003984904972149117463085539556354188641513168478436313080237596295773983001708984375e-324", + "7.4109846876186981626485318930233205854758970392148714663837852375101326090531312779794975454245398856969484704316857659638998506553390969459816219401617281718945106978546710679176872575177347315553307795408549809608457500958111373034747658096871009590975442271004757307809711118935784838675653998783503015228055934046593739791790738723868299395818481660169122019456499931289798411362062484498678713572180352209017023903285791732520220528974020802906854021606612375549983402671300035812486479041385743401875520901590172592547146296175134159774938718574737870961645638908718119841271673056017045493004705269590165763776884908267986972573366521765567941072508764337560846003984904972149117463085539556354188641513168478436313080237596295773983001708984375001e-324", + "94393431193180696942841837085033647913224148539854e-358", + "104308485241983990666713401708072175773165034278685682646111762292409330928739751702404658197872319129036519947435319418387839758990478549477777586673075945844895981012024387992135617064532141489278815239849108105951619997829153633535314849999674266169258928940692239684771590065027025835804863585454872499320500023126142553932654370362024104462255244034053203998964360882487378334860197725139151265590832887433736189468858614521708567646743455601905935595381852723723645799866672558576993978025033590728687206296379801363024094048327273913079612469982585674824156000783167963081616214710691759864332339239688734656548790656486646106983450809073750535624894296242072010195710276073042036425579852459556183541199012652571123898996574563824424330960027873516082763671875e-1075", + "0.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000247032822920623272088284396434110686182529901307162382212792841250337753635104375932649918180817996189898282347722858865463328355177969898199387398005390939063150356595155702263922908583924491051844359318028499365361525003193704576782492193656236698636584807570015857692699037063119282795585513329278343384093519780155312465972635795746227664652728272200563740064854999770965994704540208281662262378573934507363390079677619305775067401763246736009689513405355374585166611342237666786041621596804619144672918403005300575308490487653917113865916462395249126236538818796362393732804238910186723484976682350898633885879256283027559956575244555072551893136908362547791869486679949683240497058210285131854513962138377228261454376934125320985913276672363281249", + "0.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000247032822920623272088284396434110686182529901307162382212792841250337753635104375932649918180817996189898282347722858865463328355177969898199387398005390939063150356595155702263922908583924491051844359318028499365361525003193704576782492193656236698636584807570015857692699037063119282795585513329278343384093519780155312465972635795746227664652728272200563740064854999770965994704540208281662262378573934507363390079677619305775067401763246736009689513405355374585166611342237666786041621596804619144672918403005300575308490487653917113865916462395249126236538818796362393732804238910186723484976682350898633885879256283027559956575244555072551893136908362547791869486679949683240497058210285131854513962138377228261454376934125320985913276672363281251", +]; + +/// Check items that failed in the past. +pub struct RegressionCheck { + iter: std::slice::Iter<'static, &'static str>, +} + +impl Generator for RegressionCheck { + const NAME: &'static str = "regression check"; + + const SHORT_NAME: &'static str = "regression"; + + type WriteCtx = &'static str; + + fn total_tests() -> u64 { + REGRESSIONS.len().try_into().unwrap() + } + + fn new() -> Self { + Self { iter: REGRESSIONS.iter() } + } + + fn write_string(s: &mut String, ctx: Self::WriteCtx) { + s.write_str(ctx).unwrap(); + } +} + +impl Iterator for RegressionCheck { + type Item = &'static str; + + fn next(&mut self) -> Option { + self.iter.next().copied() + } +} diff --git a/lexical-parse-float/etc/correctness/test-parse-random/gen/subnorm.rs b/lexical-parse-float/etc/correctness/test-parse-random/gen/subnorm.rs new file mode 100644 index 00000000..4fe3b90a --- /dev/null +++ b/lexical-parse-float/etc/correctness/test-parse-random/gen/subnorm.rs @@ -0,0 +1,103 @@ +use std::cmp::min; +use std::fmt::Write; +use std::ops::RangeInclusive; + +use crate::{Float, Generator, Int}; + +/// Spot check some edge cases for subnormals. +pub struct SubnormEdgeCases { + cases: [F::Int; 6], + index: usize, +} + +impl SubnormEdgeCases { + /// Shorthand + const I1: F::Int = F::Int::ONE; + + fn edge_cases() -> [F::Int; 6] { + // Comments use an 8-bit mantissa as a demo + [ + // 0b00000001 + Self::I1, + // 0b10000000 + Self::I1 << (F::MAN_BITS - 1), + // 0b00001000 + Self::I1 << ((F::MAN_BITS / 2) - 1), + // 0b00001111 + Self::I1 << ((F::MAN_BITS / 2) - 1), + // 0b00001111 + Self::I1 << ((F::MAN_BITS / 2) - 1), + // 0b11111111 + F::MAN_MASK, + ] + } +} + +impl Generator for SubnormEdgeCases { + const NAME: &'static str = "subnormal edge cases"; + const SHORT_NAME: &'static str = "subnorm edge"; + + type WriteCtx = F; + + fn new() -> Self { + Self { cases: Self::edge_cases(), index: 0 } + } + + fn total_tests() -> u64 { + Self::edge_cases().len().try_into().unwrap() + } + + fn write_string(s: &mut String, ctx: Self::WriteCtx) { + write!(s, "{ctx:e}").unwrap(); + } +} + +impl Iterator for SubnormEdgeCases { + type Item = F; + + fn next(&mut self) -> Option { + let i = self.cases.get(self.index)?; + self.index += 1; + + Some(F::from_bits(*i)) + } +} + +/// Test all subnormals up to `1 << 22`. +pub struct SubnormComplete { + iter: RangeInclusive, +} + +impl Generator for SubnormComplete +where + RangeInclusive: Iterator, +{ + const NAME: &'static str = "subnormal"; + const SHORT_NAME: &'static str = "subnorm "; + + type WriteCtx = F; + + fn total_tests() -> u64 { + let iter = Self::new().iter; + (F::Int::ONE + *iter.end() - *iter.start()).try_into().unwrap() + } + + fn new() -> Self { + Self { iter: F::Int::ZERO..=min(F::Int::ONE << 22, F::MAN_BITS.try_into().unwrap()) } + } + + fn write_string(s: &mut String, ctx: Self::WriteCtx) { + write!(s, "{ctx:e}").unwrap(); + } +} + +impl Iterator for SubnormComplete +where + RangeInclusive: Iterator, +{ + type Item = F; + + fn next(&mut self) -> Option { + Some(F::from_bits(self.iter.next()?)) + } +} diff --git a/lexical-parse-float/etc/correctness/test-parse-random/huge-pow10.rs b/lexical-parse-float/etc/correctness/test-parse-random/huge-pow10.rs deleted file mode 100644 index 9d12a03d..00000000 --- a/lexical-parse-float/etc/correctness/test-parse-random/huge-pow10.rs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2015 The Rust Project Developers. See the COPYRIGHT -// file at the top-level directory of this distribution and at -// http://rust-lang.org/COPYRIGHT. -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -mod _common; - -use _common::validate; - -fn main() { - for e in 300..310 { - for i in 0..100000 { - validate(&format!("{}e{}", i, e)); - } - } -} diff --git a/lexical-parse-float/etc/correctness/test-parse-random/lib.rs b/lexical-parse-float/etc/correctness/test-parse-random/lib.rs new file mode 100644 index 00000000..f36e3928 --- /dev/null +++ b/lexical-parse-float/etc/correctness/test-parse-random/lib.rs @@ -0,0 +1,526 @@ +mod traits; +mod ui; +mod validate; + +use std::any::{type_name, TypeId}; +use std::cmp::min; +use std::ops::RangeInclusive; +use std::process::ExitCode; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{mpsc, OnceLock}; +use std::{fmt, time}; + +use indicatif::{MultiProgress, ProgressBar}; +use rand::distributions::{Distribution, Standard}; +use rayon::prelude::*; +use time::{Duration, Instant}; +use traits::{Float, Generator, Int}; + +/// Test generators. +mod gen { + pub mod exhaustive; + pub mod exponents; + pub mod fuzz; + pub mod integers; + pub mod long_fractions; + pub mod many_digits; + pub mod sparse; + pub mod spot_checks; + pub mod subnorm; +} + +/// How many failures to exit after if unspecified. +const DEFAULT_MAX_FAILURES: u64 = 20; + +/// Register exhaustive tests only for <= 32 bits. No more because it would take years. +const MAX_BITS_FOR_EXHAUUSTIVE: u32 = 32; + +/// If there are more tests than this threashold, the test will be defered until after all +/// others run (so as to avoid thread pool starvation). They also can be excluded with +/// `--skip-huge`. +const HUGE_TEST_CUTOFF: u64 = 5_000_000; + +/// Seed for tests that use a deterministic RNG. +const SEED: [u8; 32] = *b"3.141592653589793238462643383279"; + +/// Global configuration +#[derive(Debug)] +pub struct Config { + pub timeout: Duration, + /// Failures per test + pub max_failures: u64, + pub disable_max_failures: bool, + /// If `None`, the default will be used + pub fuzz_count: Option, + pub skip_huge: bool, +} + +impl Default for Config { + fn default() -> Self { + Self { + timeout: Duration::from_secs(60 * 60 * 3), + max_failures: DEFAULT_MAX_FAILURES, + disable_max_failures: false, + fuzz_count: None, + skip_huge: false, + } + } +} + +/// Collect, filter, and launch all tests. +pub fn run(cfg: Config, include: &[String], exclude: &[String]) -> ExitCode { + // With default parallelism, the CPU doesn't saturate. We don't need to be nice to + // other processes, so do 1.5x to make sure we use all available resources. + let threads = std::thread::available_parallelism().map(Into::into).unwrap_or(0) * 3 / 2; + rayon::ThreadPoolBuilder::new().num_threads(threads).build_global().unwrap(); + + let mut tests = register_tests(&cfg); + println!("registered"); + let initial_tests: Vec<_> = tests.iter().map(|t| t.name.clone()).collect(); + + let unmatched: Vec<_> = include + .iter() + .chain(exclude.iter()) + .filter(|filt| !tests.iter().any(|t| t.matches(filt))) + .collect(); + + assert!( + unmatched.is_empty(), + "filters were provided that have no matching tests: {unmatched:#?}" + ); + + tests.retain(|test| !exclude.iter().any(|exc| test.matches(exc))); + + if cfg.skip_huge { + tests.retain(|test| !test.is_huge_test()); + } + + if !include.is_empty() { + tests.retain(|test| include.iter().any(|inc| test.matches(inc))); + } + + for exc in initial_tests.iter().filter(|orig_name| !tests.iter().any(|t| t.name == **orig_name)) + { + println!("Skipping test '{exc}'"); + } + + println!("launching"); + let elapsed = launch_tests(&mut tests, &cfg); + ui::finish(&tests, elapsed, &cfg) +} + +/// Enumerate tests to run but don't actaully run them. +pub fn register_tests(cfg: &Config) -> Vec { + let mut tests = Vec::new(); + + // Register normal generators for all floats. + register_float::(&mut tests, cfg); + register_float::(&mut tests, cfg); + + tests.sort_unstable_by_key(|t| (t.float_name, t.gen_name)); + for i in 0..(tests.len() - 1) { + if tests[i].gen_name == tests[i + 1].gen_name { + panic!("dupliate test name {}", tests[i].gen_name); + } + } + + tests +} + +/// Register all generators for a single float. +fn register_float(tests: &mut Vec, cfg: &Config) +where + RangeInclusive: Iterator, + >::Error: std::fmt::Debug, + Standard: Distribution<::Int>, +{ + if F::BITS <= MAX_BITS_FOR_EXHAUUSTIVE { + // Only run exhaustive tests if there is a chance of completion. + TestInfo::register::>(tests); + } + + gen::fuzz::Fuzz::::set_iterations(cfg.fuzz_count); + + TestInfo::register::>(tests); + TestInfo::register::>(tests); + TestInfo::register::>(tests); + TestInfo::register::>(tests); + TestInfo::register::(tests); + TestInfo::register::(tests); + TestInfo::register::>(tests); + TestInfo::register::>(tests); + TestInfo::register::>(tests); + TestInfo::register::(tests); + TestInfo::register::(tests); + TestInfo::register::>(tests); + TestInfo::register::>(tests); +} + +/// Configuration for a single test. +#[derive(Debug)] +pub struct TestInfo { + pub name: String, + /// Tests are identified by the type ID of `(F, G)` (tuple of the float and generator type). + /// This gives an easy way to associate messages with tests. + id: TypeId, + float_name: &'static str, + gen_name: &'static str, + /// Name for display in the progress bar. + short_name: String, + total_tests: u64, + /// Function to launch this test. + launch: fn(&mpsc::Sender, &TestInfo, &Config), + /// Progress bar to be updated. + pb: Option, + /// Once completed, this will be set. + completed: OnceLock, +} + +impl TestInfo { + /// Check if either the name or short name is a match, for filtering. + fn matches(&self, pat: &str) -> bool { + self.short_name.contains(pat) || self.name.contains(pat) + } + + /// Create a `TestInfo` for a given float and generator, then add it to a list. + fn register>(v: &mut Vec) { + let f_name = type_name::(); + let gen_name = G::NAME; + let gen_short_name = G::SHORT_NAME; + + let info = TestInfo { + id: TypeId::of::<(F, G)>(), + float_name: f_name, + gen_name, + pb: None, + name: format!("{f_name} {gen_name}"), + short_name: format!("{f_name} {gen_short_name}"), + launch: test_runner::, + total_tests: G::total_tests(), + completed: OnceLock::new(), + }; + v.push(info); + } + + /// Pad the short name to a common width for progress bar use. + fn short_name_padded(&self) -> String { + format!("{:18}", self.short_name) + } + + /// Create a progress bar for this test within a multiprogress bar. + fn register_pb(&mut self, mp: &MultiProgress, drop_bars: &mut Vec) { + self.pb = Some(ui::create_pb(mp, self.total_tests, &self.short_name_padded(), drop_bars)); + } + + /// When the test is finished, update progress bar messages and finalize. + fn finalize_pb(&self, c: &Completed) { + let pb = self.pb.as_ref().unwrap(); + ui::finalize_pb(pb, &self.short_name_padded(), c); + } + + /// True if this should be run after all others. + fn is_huge_test(&self) -> bool { + self.total_tests >= HUGE_TEST_CUTOFF + } +} + +/// A message sent from test runner threads to the UI/log thread. +#[derive(Clone, Debug)] +struct Msg { + id: TypeId, + update: Update, +} + +impl Msg { + /// Wrap an `Update` into a message for the specified type. We use the `TypeId` of `(F, G)` to + /// identify which test a message in the channel came from. + fn new>(u: Update) -> Self { + Self { id: TypeId::of::<(F, G)>(), update: u } + } + + /// Get the matching test from a list. Panics if not found. + fn find_test<'a>(&self, tests: &'a [TestInfo]) -> &'a TestInfo { + tests.iter().find(|t| t.id == self.id).unwrap() + } + + /// Update UI as needed for a single message received from the test runners. + fn handle(self, tests: &[TestInfo], mp: &MultiProgress) { + let test = self.find_test(tests); + let pb = test.pb.as_ref().unwrap(); + + match self.update { + Update::Started => { + mp.println(format!("Testing '{}'", test.name)).unwrap(); + } + Update::Progress { executed, failures } => { + pb.set_message(format! {"{failures}"}); + pb.set_position(executed); + } + Update::Failure { fail, input, float_res } => { + mp.println(format!( + "Failure in '{}': {fail}. parsing '{input}'. Parsed as: {float_res}", + test.name + )) + .unwrap(); + } + Update::Completed(c) => { + test.finalize_pb(&c); + + let prefix = match c.result { + Ok(FinishedAll) => "Completed tests for", + Err(EarlyExit::Timeout) => "Timed out", + Err(EarlyExit::MaxFailures) => "Max failures reached for", + }; + + mp.println(format!( + "{prefix} generator '{}' in {:?}. {} tests run, {} failures", + test.name, c.elapsed, c.executed, c.failures + )) + .unwrap(); + test.completed.set(c).unwrap(); + } + }; + } +} + +/// Status sent with a message. +#[derive(Clone, Debug)] +enum Update { + /// Starting a new test runner. + Started, + /// Completed a out of b tests. + Progress { executed: u64, failures: u64 }, + /// Received a failed test. + Failure { + fail: CheckFailure, + /// String for which parsing was attempted. + input: Box, + /// The parsed & decomposed `FloatRes`, aleady stringified so we don't need generics here. + float_res: Box, + }, + /// Exited with an unexpected condition. + Completed(Completed), +} + +/// Result of an input did not parsing successfully. +#[derive(Clone, Debug)] +enum CheckFailure { + /// Above the zero cutoff but got rounded to zero. + UnexpectedZero, + /// Below the infinity cutoff but got rounded to infinity. + UnexpectedInf, + /// Above the negative infinity cutoff but got rounded to negative infinity. + UnexpectedNegInf, + /// Got a `NaN` when none was expected. + UnexpectedNan, + /// Expected `NaN`, got none. + ExpectedNan, + /// Expected infinity, got finite. + ExpectedInf, + /// Expected negative infinity, got finite. + ExpectedNegInf, + /// The value exceeded its error tolerance. + InvalidReal { + /// Error from the expected value, as a float. + error_float: Option, + /// Error as a rational string (since it can't always be represented as a float). + error_str: Box, + /// True if the error was caused by not rounding to even at the midpoint between + /// two representable values. + incorrect_midpoint_rounding: bool, + }, +} + +impl fmt::Display for CheckFailure { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + CheckFailure::UnexpectedZero => { + write!(f, "incorrectly rounded to 0 (expected nonzero)") + } + CheckFailure::UnexpectedInf => { + write!(f, "incorrectly rounded to +inf (expected finite)") + } + CheckFailure::UnexpectedNegInf => { + write!(f, "incorrectly rounded to -inf (expected finite)") + } + CheckFailure::UnexpectedNan => write!(f, "got a NaN where none was expected"), + CheckFailure::ExpectedNan => write!(f, "expected a NaN but did not get it"), + CheckFailure::ExpectedInf => write!(f, "expected +inf but did not get it"), + CheckFailure::ExpectedNegInf => write!(f, "expected -inf but did not get it"), + CheckFailure::InvalidReal { error_float, error_str, incorrect_midpoint_rounding } => { + if *incorrect_midpoint_rounding { + write!( + f, + "midpoint between two representable values did not correctly \ + round to even; error: {error_str}" + )?; + } else { + write!(f, "real number did not parse correctly; error: {error_str}")?; + } + + if let Some(float) = error_float { + write!(f, " ({float})")?; + } + Ok(()) + } + } + } +} + +/// Information about a completed test generator. +#[derive(Clone, Debug)] +struct Completed { + /// Finished tests (both successful and failed). + executed: u64, + /// Failed tests. + failures: u64, + /// Extra exit information if unsuccessful. + result: Result, + /// If there is something to warn about (e.g bad estimate), leave it here. + warning: Option>, + /// Total time to run the test. + elapsed: Duration, +} + +/// Marker for completing all tests (used in `Result` types). +#[derive(Clone, Debug)] +struct FinishedAll; + +/// Reasons for exiting early. +#[derive(Clone, Debug)] +enum EarlyExit { + Timeout, + MaxFailures, +} + +/// Run all tests in `tests`. +/// +/// This launches a main thread that receives messages and handlees UI updates, and uses the +/// rest of the thread pool to execute the tests. +fn launch_tests(tests: &mut [TestInfo], cfg: &Config) -> Duration { + // Run shorter tests first + tests.sort_unstable_by_key(|test| test.total_tests); + + for test in tests.iter() { + println!("Launching test '{}'", test.name); + } + + // Configure progress bars + let mut all_progress_bars = Vec::new(); + let mp = MultiProgress::new(); + mp.set_move_cursor(true); + for test in tests.iter_mut() { + test.register_pb(&mp, &mut all_progress_bars); + } + + ui::set_panic_hook(all_progress_bars); + + let (tx, rx) = mpsc::channel::(); + let start = Instant::now(); + + rayon::scope(|scope| { + // Thread that updates the UI + scope.spawn(|_scope| { + let rx = rx; // move rx + + loop { + if tests.iter().all(|t| t.completed.get().is_some()) { + break; + } + + let msg = rx.recv().unwrap(); + msg.handle(tests, &mp); + } + + // All tests completed; finish things up + drop(mp); + assert_eq!(rx.try_recv().unwrap_err(), mpsc::TryRecvError::Empty); + }); + + // Don't let the thread pool be starved by huge tests. Run faster tests first in parallel, + // then parallelize only within the rest of the tests. + let (huge_tests, normal_tests): (Vec<_>, Vec<_>) = + tests.iter().partition(|t| t.is_huge_test()); + + // Run the actual tests + normal_tests.par_iter().for_each(|test| ((test.launch)(&tx, test, cfg))); + + huge_tests.par_iter().for_each(|test| ((test.launch)(&tx, test, cfg))); + }); + + start.elapsed() +} + +/// Test runer for a single generator. +/// +/// This calls the generator's iterator multiple times (in parallel) and validates each output. +fn test_runner>(tx: &mpsc::Sender, _info: &TestInfo, cfg: &Config) { + tx.send(Msg::new::(Update::Started)).unwrap(); + + let total = G::total_tests(); + let gen = G::new(); + let executed = AtomicU64::new(0); + let failures = AtomicU64::new(0); + + let checks_per_update = min(total, 1000); + let started = Instant::now(); + + // Function to execute for a single test iteration. + let check_one = |buf: &mut String, ctx: G::WriteCtx| { + let executed = executed.fetch_add(1, Ordering::Relaxed); + buf.clear(); + G::write_string(buf, ctx); + + match validate::validate::(buf) { + Ok(()) => (), + Err(e) => { + tx.send(Msg::new::(e)).unwrap(); + let f = failures.fetch_add(1, Ordering::Relaxed); + // End early if the limit is exceeded. + if f >= cfg.max_failures { + return Err(EarlyExit::MaxFailures); + } + } + }; + + // Send periodic updates + if executed % checks_per_update == 0 { + let failures = failures.load(Ordering::Relaxed); + + tx.send(Msg::new::(Update::Progress { executed, failures })).unwrap(); + + if started.elapsed() > cfg.timeout { + return Err(EarlyExit::Timeout); + } + } + + Ok(()) + }; + + // Run the test iterations in parallel. Each thread gets a string buffer to write + // its check values to. + let res = gen.par_bridge().try_for_each_init(|| String::with_capacity(100), check_one); + + let elapsed = started.elapsed(); + let executed = executed.into_inner(); + let failures = failures.into_inner(); + + // Warn about bad estimates if relevant. + let warning = if executed != total && res.is_ok() { + let msg = format!("executed tests != estimated ({executed} != {total}) for {}", G::NAME); + + Some(msg.into()) + } else { + None + }; + + let result = res.map(|()| FinishedAll); + tx.send(Msg::new::(Update::Completed(Completed { + executed, + failures, + result, + warning, + elapsed, + }))) + .unwrap(); +} diff --git a/lexical-parse-float/etc/correctness/test-parse-random/long-fractions.rs b/lexical-parse-float/etc/correctness/test-parse-random/long-fractions.rs deleted file mode 100644 index 7f84e693..00000000 --- a/lexical-parse-float/etc/correctness/test-parse-random/long-fractions.rs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2016 The Rust Project Developers. See the COPYRIGHT -// file at the top-level directory of this distribution and at -// http://rust-lang.org/COPYRIGHT. -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -mod _common; - -use _common::validate; -use lexical_parse_float::FromLexical; -use std::char; - -fn main() { - for n in 0..10 { - let digit = char::from_digit(n, 10).unwrap(); - let mut s = "0.".to_string(); - for _ in 0..400 { - s.push(digit); - if f64::from_lexical(s.as_bytes()).is_ok() { - validate(&s); - } - } - } -} diff --git a/lexical-parse-float/etc/correctness/test-parse-random/main.rs b/lexical-parse-float/etc/correctness/test-parse-random/main.rs new file mode 100644 index 00000000..b11fd551 --- /dev/null +++ b/lexical-parse-float/etc/correctness/test-parse-random/main.rs @@ -0,0 +1,129 @@ +use std::process::ExitCode; +use std::time::Duration; + +use test_parse_random as tpr; + +static HELP: &str = r#"Usage: + + ./test-float-parse [--timeout x] [--exclude x] [--max-failures x] [INCLUDE ...] + ./test-float-parse [--fuzz-count x] [INCLUDE ...] + ./test-float-parse [--skip-huge] [INCLUDE ...] + ./test-float-parse --list + +Args: + + INCLUDE Include only tests with names containing these + strings. If this argument is not specified, all tests + are run. + --timeout N Exit after this amount of time (in seconds). + --exclude FILTER Skip tests containing this string. May be specified + more than once. + --list List available tests. + --max-failures N Limit to N failures per test. Defaults to 20. Pass + "--max-failures none" to remove this limit. + --fuzz-count N Run the fuzzer with N iterations. Only has an effect + if fuzz tests are enabled. Pass `--fuzz-count none` + to remove this limit. + --skip-huge Skip tests that run for a long time. + --all Reset previous `--exclude`, `--skip-huge`, and + `INCLUDE` arguments (useful for running all tests + via `./x`). +"#; + +enum ArgMode { + Any, + Timeout, + Exclude, + FuzzCount, + MaxFailures, +} + +fn main() -> ExitCode { + if cfg!(debug_assertions) { + println!( + "WARNING: running in debug mode. Release mode is recommended to reduce test duration." + ); + std::thread::sleep(Duration::from_secs(2)); + } + + let args: Vec<_> = std::env::args().skip(1).collect(); + if args.iter().any(|arg| arg == "--help" || arg == "-h") { + println!("{HELP}"); + return ExitCode::SUCCESS; + } + + if args.iter().any(|arg| arg == "--list") { + let tests = tpr::register_tests(&tpr::Config::default()); + println!("Available tests:"); + for t in tests { + println!("{}", t.name); + } + + return ExitCode::SUCCESS; + } + + let (cfg, include, exclude) = parse_args(args); + + tpr::run(cfg, &include, &exclude) +} + +/// Simple command argument parser +fn parse_args(args: Vec) -> (tpr::Config, Vec, Vec) { + let mut cfg = tpr::Config::default(); + + let mut mode = ArgMode::Any; + let mut include = Vec::new(); + let mut exclude = Vec::new(); + + for arg in args { + mode = match mode { + ArgMode::Any if arg == "--timeout" => ArgMode::Timeout, + ArgMode::Any if arg == "--exclude" => ArgMode::Exclude, + ArgMode::Any if arg == "--max-failures" => ArgMode::MaxFailures, + ArgMode::Any if arg == "--fuzz-count" => ArgMode::FuzzCount, + ArgMode::Any if arg == "--skip-huge" => { + cfg.skip_huge = true; + ArgMode::Any + } + ArgMode::Any if arg == "--all" => { + cfg.skip_huge = false; + include.clear(); + exclude.clear(); + ArgMode::Any + } + ArgMode::Any if arg.starts_with('-') => { + panic!("Unknown argument {arg}. Usage:\n{HELP}") + } + ArgMode::Any => { + include.push(arg); + ArgMode::Any + } + ArgMode::Timeout => { + cfg.timeout = Duration::from_secs(arg.parse().unwrap()); + ArgMode::Any + } + ArgMode::MaxFailures => { + if arg == "none" { + cfg.disable_max_failures = true; + } else { + cfg.max_failures = arg.parse().unwrap(); + } + ArgMode::Any + } + ArgMode::FuzzCount => { + if arg == "none" { + cfg.fuzz_count = Some(u64::MAX); + } else { + cfg.fuzz_count = Some(arg.parse().unwrap()); + } + ArgMode::Any + } + ArgMode::Exclude => { + exclude.push(arg); + ArgMode::Any + } + } + } + + (cfg, include, exclude) +} diff --git a/lexical-parse-float/etc/correctness/test-parse-random/many-digits.rs b/lexical-parse-float/etc/correctness/test-parse-random/many-digits.rs deleted file mode 100644 index 47c36e16..00000000 --- a/lexical-parse-float/etc/correctness/test-parse-random/many-digits.rs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2015 The Rust Project Developers. See the COPYRIGHT -// file at the top-level directory of this distribution and at -// http://rust-lang.org/COPYRIGHT. -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -mod _common; - -use _common::{validate, ISAAC_SEED}; -use rand::distributions::uniform::Uniform; -use rand::distributions::Distribution; -use rand::{Rng, SeedableRng}; -use rand_isaac::Isaac64Rng; -use std::char; - -fn main() { - let mut rnd = Isaac64Rng::from_seed(ISAAC_SEED); - let mut range = Uniform::new(0, 10); - for _ in 0..5_000_000u64 { - let num_digits = rnd.gen_range(100..400); - let digits = gen_digits(num_digits, &mut range, &mut rnd); - validate(&digits); - } -} - -fn gen_digits(n: u32, range: &mut Uniform, rnd: &mut Isaac64Rng) -> String { - let mut s = String::new(); - for _ in 0..n { - let digit = char::from_digit(range.sample(rnd), 10).unwrap(); - s.push(digit); - } - s -} diff --git a/lexical-parse-float/etc/correctness/test-parse-random/rand-f64.rs b/lexical-parse-float/etc/correctness/test-parse-random/rand-f64.rs deleted file mode 100644 index d807e0fc..00000000 --- a/lexical-parse-float/etc/correctness/test-parse-random/rand-f64.rs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2015 The Rust Project Developers. See the COPYRIGHT -// file at the top-level directory of this distribution and at -// http://rust-lang.org/COPYRIGHT. -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -mod _common; - -use _common::{validate, ISAAC_SEED}; -use rand::{RngCore, SeedableRng}; -use rand_isaac::Isaac64Rng; -use std::mem::transmute; - -fn main() { - let mut rng = Isaac64Rng::from_seed(ISAAC_SEED); - let mut i = 0; - while i < 10_000_000 { - let bits = rng.next_u64(); - let x: f64 = unsafe { transmute(bits) }; - if x.is_finite() { - validate(&format!("{:e}", x)); - i += 1; - } - } -} diff --git a/lexical-parse-float/etc/correctness/test-parse-random/runtests.py b/lexical-parse-float/etc/correctness/test-parse-random/runtests.py deleted file mode 100644 index 20713916..00000000 --- a/lexical-parse-float/etc/correctness/test-parse-random/runtests.py +++ /dev/null @@ -1,421 +0,0 @@ -#!/usr/bin/env python2.7 -# -# Copyright 2015 The Rust Project Developers. See the COPYRIGHT -# file at the top-level directory of this distribution and at -# http://rust-lang.org/COPYRIGHT. -# -# Licensed under the Apache License, Version 2.0 or the MIT license -# , at your -# option. This file may not be copied, modified, or distributed -# except according to those terms. - -""" -Testing dec2flt -=============== -These are *really* extensive tests. Expect them to run for hours. Due to the -nature of the problem (the input is a string of arbitrary length), exhaustive -testing is not really possible. Instead, there are exhaustive tests for some -classes of inputs for which that is feasible and a bunch of deterministic and -random non-exhaustive tests for covering everything else. - -The actual tests (generating decimal strings and feeding them to dec2flt) is -performed by a set of stand-along rust programs. This script compiles, runs, -and supervises them. The programs report the strings they generate and the -floating point numbers they converted those strings to, and this script -checks that the results are correct. - -You can run specific tests rather than all of them by giving their names -(without .rs extension) as command line parameters. - -Verification ------------- -The tricky part is not generating those inputs but verifying the outputs. -Comparing with the result of Python's float() does not cut it because -(and this is apparently undocumented) although Python includes a version of -Martin Gay's code including the decimal-to-float part, it doesn't actually use -it for float() (only for round()) instead relying on the system scanf() which -is not necessarily completely accurate. - -Instead, we take the input and compute the true value with bignum arithmetic -(as a fraction, using the ``fractions`` module). - -Given an input string and the corresponding float computed via Rust, simply -decode the float into f * 2^k (for integers f, k) and the ULP. -We can now easily compute the error and check if it is within 0.5 ULP as it -should be. Zero and infinites are handled similarly: - -- If the approximation is 0.0, the exact value should be *less or equal* - half the smallest denormal float: the smallest denormal floating point - number has an odd mantissa (00...001) and thus half of that is rounded - to 00...00, i.e., zero. -- If the approximation is Inf, the exact value should be *greater or equal* - to the largest finite float + 0.5 ULP: the largest finite float has an odd - mantissa (11...11), so that plus half an ULP is rounded up to the nearest - even number, which overflows. - -Implementation details ----------------------- -This directory contains a set of single-file Rust programs that perform -tests with a particular class of inputs. Each is compiled and run without -parameters, outputs (f64, f32, decimal) pairs to verify externally, and -in any case either exits gracefully or with a panic. - -If a test binary writes *anything at all* to stderr or exits with an -exit code that's not 0, the test fails. -The output on stdout is treated as (f64, f32, decimal) record, encoded thusly: - -- First, the bits of the f64 encoded as an ASCII hex string. -- Second, the bits of the f32 encoded as an ASCII hex string. -- Then the corresponding string input, in ASCII -- The record is terminated with a newline. - -Incomplete records are an error. Not-a-Number bit patterns are invalid too. - -The tests run serially but the validation for a single test is parallelized -with ``multiprocessing``. Each test is launched as a subprocess. -One thread supervises it: Accepts and enqueues records to validate, observe -stderr, and waits for the process to exit. A set of worker processes perform -the validation work for the outputs enqueued there. Another thread listens -for progress updates from the workers. - -Known issues ------------- -Some errors (e.g., NaN outputs) aren't handled very gracefully. -Also, if there is an exception or the process is interrupted (at least on -Windows) the worker processes are leaked and stick around forever. -They're only a few megabytes each, but still, this script should not be run -if you aren't prepared to manually kill a lot of orphaned processes. -""" -from __future__ import print_function -import sys -import os -import time -import struct -from fractions import Fraction -from collections import namedtuple -from subprocess import Popen, check_call, PIPE -from glob import glob -import multiprocessing -import threading -import ctypes -import binascii - -try: # Python 3 - import queue as Queue -except ImportError: # Python 2 - import Queue # noqa # pyright: ignore[reportMissingImports] - -NUM_WORKERS = 2 -UPDATE_EVERY_N = 50000 -INF = namedtuple('INF', '')() -NEG_INF = namedtuple('NEG_INF', '')() -ZERO = namedtuple('ZERO', '')() -MAILBOX = None # The queue for reporting errors to the main process. -STDOUT_LOCK = threading.Lock() -test_name = None -child_processes = [] -exit_status = 0 - - -def msg(*args): - with STDOUT_LOCK: - print("[" + test_name + "]", *args) - sys.stdout.flush() - - -def write_errors(): - global exit_status - f = open("errors.txt", 'w') - have_seen_error = False - while True: - args = MAILBOX.get() - if args is None: - f.close() - break - print(*args, file=f) - f.flush() - if not have_seen_error: - have_seen_error = True - msg("Something is broken:", *args) - msg("Future errors logged to errors.txt") - exit_status = 101 - - -def projectdir(): - file = os.path.realpath(__file__) - return os.path.dirname(os.path.dirname(file)) - - -def targetdir(): - return os.path.join(projectdir(), 'target') - - -def releasedir(): - return os.path.join(targetdir(), 'release') - - -def cargo(): - path = os.getcwd() - os.chdir(projectdir()) - check_call(['cargo', 'build', '--release']) - os.chdir(path) - - -def run(test): - global test_name - test_name = test - - t0 = time.perf_counter() - msg("setting up supervisor") - command = ['cargo', 'run', '--bin', test, '--release'] - proc = Popen(command, bufsize=1 << 20, stdin=PIPE, stdout=PIPE, stderr=PIPE) - done = multiprocessing.Value(ctypes.c_bool) - queue = multiprocessing.Queue(maxsize=5) # (maxsize=1024) - workers = [] - for n in range(NUM_WORKERS): - worker = multiprocessing.Process(name='Worker-' + str(n + 1), - target=init_worker, - args=[test, MAILBOX, queue, done]) - workers.append(worker) - child_processes.append(worker) - for worker in workers: - worker.start() - msg("running test") - interact(proc, queue) - with done.get_lock(): - done.value = True - for worker in workers: - worker.join() - msg("python is done") - assert queue.empty(), "did not validate everything" - dt = time.perf_counter() - t0 - msg("took", round(dt, 3), "seconds") - - -def interact(proc, queue): - n = 0 - while proc.poll() is None: - line = proc.stdout.readline() - if not line: - continue - assert line.endswith(b'\n'), "incomplete line: " + repr(line) - queue.put(line) - n += 1 - if n % UPDATE_EVERY_N == 0: - msg("got", str(n // 1000) + "k", "records") - msg("rust is done. exit code:", proc.returncode) - rest, stderr = proc.communicate() - if stderr: - msg("rust stderr output:", stderr) - for line in rest.split(b'\n'): - if not line: - continue - queue.put(line) - - -def main(): - global MAILBOX - files = glob(f'{projectdir()}/test-parse-random/*.rs') - basenames = [os.path.basename(i) for i in files] - all_tests = [os.path.splitext(f)[0] for f in basenames if not f.startswith('_')] - args = sys.argv[1:] - if args: - tests = [test for test in all_tests if test in args] - else: - tests = all_tests - if not tests: - print("Error: No tests to run") - sys.exit(1) - # Compile first for quicker feedback - cargo() - # Set up mailbox once for all tests - MAILBOX = multiprocessing.Queue() - mailman = threading.Thread(target=write_errors) - mailman.daemon = True - mailman.start() - for test in tests: - run(test) - MAILBOX.put(None) - mailman.join() - - -# ---- Worker thread code ---- - - -POW2 = {e: Fraction(2) ** e for e in range(-1100, 1100)} -HALF_ULP = {e: (Fraction(2) ** e)/2 for e in range(-1100, 1100)} -DONE_FLAG = None - - -def send_error_to_supervisor(*args): - MAILBOX.put(args) - - -def init_worker(test, mailbox, queue, done): - global test_name, MAILBOX, DONE_FLAG - test_name = test - MAILBOX = mailbox - DONE_FLAG = done - do_work(queue) - - -def is_done(): - with DONE_FLAG.get_lock(): - return DONE_FLAG.value - - -def do_work(queue): - while True: - try: - line = queue.get(timeout=0.01) - except Queue.Empty: - if queue.empty() and is_done(): - return - else: - continue - bin64, bin32, text = line.rstrip().split() - validate(bin64, bin32, text.decode('utf-8')) - - -def decode_binary64(x): - """ - Turn a IEEE 754 binary64 into (mantissa, exponent), except 0.0 and - infinity (positive and negative), which return ZERO, INF, and NEG_INF - respectively. - """ - x = binascii.unhexlify(x) - assert len(x) == 8, repr(x) - [bits] = struct.unpack(b'>Q', x) - if bits == 0: - return ZERO - exponent = (bits >> 52) & 0x7FF - negative = bits >> 63 - low_bits = bits & 0xFFFFFFFFFFFFF - if exponent == 0: - mantissa = low_bits - exponent += 1 - if mantissa == 0: - return ZERO - elif exponent == 0x7FF: - assert low_bits == 0, "NaN" - if negative: - return NEG_INF - else: - return INF - else: - mantissa = low_bits | (1 << 52) - exponent -= 1023 + 52 - if negative: - mantissa = -mantissa - return (mantissa, exponent) - - -def decode_binary32(x): - """ - Turn a IEEE 754 binary32 into (mantissa, exponent), except 0.0 and - infinity (positive and negative), which return ZERO, INF, and NEG_INF - respectively. - """ - x = binascii.unhexlify(x) - assert len(x) == 4, repr(x) - [bits] = struct.unpack(b'>I', x) - if bits == 0: - return ZERO - exponent = (bits >> 23) & 0xFF - negative = bits >> 31 - low_bits = bits & 0x7FFFFF - if exponent == 0: - mantissa = low_bits - exponent += 1 - if mantissa == 0: - return ZERO - elif exponent == 0xFF: - if negative: - return NEG_INF - else: - return INF - else: - mantissa = low_bits | (1 << 23) - exponent -= 127 + 23 - if negative: - mantissa = -mantissa - return (mantissa, exponent) - - -MIN_SUBNORMAL_DOUBLE = Fraction(2) ** -1074 -MIN_SUBNORMAL_SINGLE = Fraction(2) ** -149 # XXX unsure -MAX_DOUBLE = (2 - Fraction(2) ** -52) * (2 ** 1023) -MAX_SINGLE = (2 - Fraction(2) ** -23) * (2 ** 127) -MAX_ULP_DOUBLE = 1023 - 52 -MAX_ULP_SINGLE = 127 - 23 -DOUBLE_ZERO_CUTOFF = MIN_SUBNORMAL_DOUBLE / 2 -DOUBLE_INF_CUTOFF = MAX_DOUBLE + 2 ** (MAX_ULP_DOUBLE - 1) -SINGLE_ZERO_CUTOFF = MIN_SUBNORMAL_SINGLE / 2 -SINGLE_INF_CUTOFF = MAX_SINGLE + 2 ** (MAX_ULP_SINGLE - 1) - - -def validate(bin64, bin32, text): - try: - double = decode_binary64(bin64) - except AssertionError: - print(bin64, bin32, text) - raise - single = decode_binary32(bin32) - real = Fraction(text) - - if double is ZERO: - if real > DOUBLE_ZERO_CUTOFF: - record_special_error(text, "f64 zero") - elif double is INF: - if real < DOUBLE_INF_CUTOFF: - record_special_error(text, "f64 inf") - elif double is NEG_INF: - if -real < DOUBLE_INF_CUTOFF: - record_special_error(text, "f64 -inf") - elif len(double) == 2: - sig, k = double - validate_normal(text, real, sig, k, "f64") - else: - assert 0, "didn't handle binary64" - if single is ZERO: - if real > SINGLE_ZERO_CUTOFF: - record_special_error(text, "f32 zero") - elif single is INF: - if real < SINGLE_INF_CUTOFF: - record_special_error(text, "f32 inf") - elif single is NEG_INF: - if -real < SINGLE_INF_CUTOFF: - record_special_error(text, "f32 -inf") - elif len(single) == 2: - sig, k = single - validate_normal(text, real, sig, k, "f32") - else: - assert 0, "didn't handle binary32" - - -def record_special_error(text, descr): - send_error_to_supervisor(text.strip(), "wrongly rounded to", descr) - - -def validate_normal(text, real, sig, k, kind): - approx = sig * POW2[k] - error = abs(approx - real) - if error > HALF_ULP[k]: - record_normal_error(text, error, k, kind) - - -def record_normal_error(text, error, k, kind): - one_ulp = HALF_ULP[k + 1] - assert one_ulp == 2 * HALF_ULP[k] - relative_error = error / one_ulp - text = text.strip() - try: - err_repr = float(relative_error) - except ValueError: - err_repr = str(err_repr).replace('/', ' / ') - send_error_to_supervisor(err_repr, "ULP error on", text, "(" + kind + ")") - - -if __name__ == '__main__': - main() diff --git a/lexical-parse-float/etc/correctness/test-parse-random/short-decimals.rs b/lexical-parse-float/etc/correctness/test-parse-random/short-decimals.rs deleted file mode 100644 index 4909f7c5..00000000 --- a/lexical-parse-float/etc/correctness/test-parse-random/short-decimals.rs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2015 The Rust Project Developers. See the COPYRIGHT -// file at the top-level directory of this distribution and at -// http://rust-lang.org/COPYRIGHT. -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -mod _common; - -use _common::validate; - -fn main() { - // Skip e = 0 because small-u32 already does those. - for e in 1..301 { - for i in 0..10000 { - // If it ends in zeros, the parser will strip those (and adjust the exponent), - // which almost always (except for exponents near +/- 300) result in an input - // equivalent to something we already generate in a different way. - if i % 10 == 0 { - continue; - } - validate(&format!("{}e{}", i, e)); - validate(&format!("{}e-{}", i, e)); - } - } -} diff --git a/lexical-parse-float/etc/correctness/test-parse-random/subnorm.rs b/lexical-parse-float/etc/correctness/test-parse-random/subnorm.rs deleted file mode 100644 index 7e69ff69..00000000 --- a/lexical-parse-float/etc/correctness/test-parse-random/subnorm.rs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2015 The Rust Project Developers. See the COPYRIGHT -// file at the top-level directory of this distribution and at -// http://rust-lang.org/COPYRIGHT. -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -mod _common; - -use _common::validate; -use std::mem::transmute; - -fn main() { - for bits in 0u32..(1 << 21) { - let single: f32 = unsafe { transmute(bits) }; - validate(&format!("{:e}", single)); - let double: f64 = unsafe { transmute(bits as u64) }; - validate(&format!("{:e}", double)); - } -} diff --git a/lexical-parse-float/etc/correctness/test-parse-random/tiny-pow10.rs b/lexical-parse-float/etc/correctness/test-parse-random/tiny-pow10.rs deleted file mode 100644 index 50ca5e32..00000000 --- a/lexical-parse-float/etc/correctness/test-parse-random/tiny-pow10.rs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2015 The Rust Project Developers. See the COPYRIGHT -// file at the top-level directory of this distribution and at -// http://rust-lang.org/COPYRIGHT. -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -mod _common; - -use _common::validate; - -fn main() { - for e in 301..327 { - for i in 0..100000 { - validate(&format!("{}e-{}", i, e)); - } - } -} diff --git a/lexical-parse-float/etc/correctness/test-parse-random/traits.rs b/lexical-parse-float/etc/correctness/test-parse-random/traits.rs new file mode 100644 index 00000000..dc009ea2 --- /dev/null +++ b/lexical-parse-float/etc/correctness/test-parse-random/traits.rs @@ -0,0 +1,202 @@ +//! Interfaces used throughout this crate. + +use std::str::FromStr; +use std::{fmt, ops}; + +use num::bigint::ToBigInt; +use num::Integer; + +use crate::validate::Constants; + +/// Integer types. +#[allow(dead_code)] // Some functions only used for testing +pub trait Int: + Clone + + Copy + + fmt::Debug + + fmt::Display + + fmt::LowerHex + + ops::Add + + ops::Sub + + ops::Shl + + ops::Shr + + ops::BitAnd + + ops::BitOr + + ops::Not + + ops::AddAssign + + ops::BitAndAssign + + ops::BitOrAssign + + From + + TryFrom + + TryFrom + + TryFrom + + TryFrom + + TryInto + + TryInto + + ToBigInt + + PartialOrd + + Integer + + Send + + 'static +{ + type Signed: Int; + type Bytes: Default + AsMut<[u8]>; + + const BITS: u32; + const ZERO: Self; + const ONE: Self; + const MAX: Self; + + fn to_signed(self) -> Self::Signed; + fn wrapping_neg(self) -> Self; + fn trailing_zeros(self) -> u32; + + fn hex(self) -> String { + format!("{self:x}") + } +} + +macro_rules! impl_int { + ($($uty:ty, $sty:ty);+) => { + $( + impl Int for $uty { + type Signed = $sty; + type Bytes = [u8; Self::BITS as usize / 8]; + const BITS: u32 = Self::BITS; + const ZERO: Self = 0; + const ONE: Self = 1; + const MAX: Self = Self::MAX; + fn to_signed(self) -> Self::Signed { + self.try_into().unwrap() + } + fn wrapping_neg(self) -> Self { + self.wrapping_neg() + } + fn trailing_zeros(self) -> u32 { + self.trailing_zeros() + } + } + + impl Int for $sty { + type Signed = Self; + type Bytes = [u8; Self::BITS as usize / 8]; + const BITS: u32 = Self::BITS; + const ZERO: Self = 0; + const ONE: Self = 1; + const MAX: Self = Self::MAX; + fn to_signed(self) -> Self::Signed { + self + } + fn wrapping_neg(self) -> Self { + self.wrapping_neg() + } + fn trailing_zeros(self) -> u32 { + self.trailing_zeros() + } + } + )+ + } +} + +impl_int!(u32, i32; u64, i64); + +/// Floating point types. +pub trait Float: + Copy + fmt::Debug + fmt::LowerExp + FromStr + Sized + Send + 'static +{ + /// Unsigned integer of same width + type Int: Int; + type SInt: Int; + + /// Total bits + const BITS: u32; + + /// (Stored) bits in the mantissa) + const MAN_BITS: u32; + + /// Bits in the exponent + const EXP_BITS: u32 = Self::BITS - Self::MAN_BITS - 1; + + /// A saturated exponent (all ones) + const EXP_SAT: u32 = (1 << Self::EXP_BITS) - 1; + + /// The exponent bias, also its maximum value + const EXP_BIAS: u32 = Self::EXP_SAT >> 1; + + const MAN_MASK: Self::Int; + const SIGN_MASK: Self::Int; + + fn from_bits(i: Self::Int) -> Self; + fn to_bits(self) -> Self::Int; + + /// Rational constants associated with this float type. + fn constants() -> &'static Constants; + + fn is_sign_negative(self) -> bool { + (self.to_bits() & Self::SIGN_MASK) > Self::Int::ZERO + } + + /// Exponent without adjustment for bias. + fn exponent(self) -> u32 { + ((self.to_bits() >> Self::MAN_BITS) & Self::EXP_SAT.try_into().unwrap()).try_into().unwrap() + } + + fn mantissa(self) -> Self::Int { + self.to_bits() & Self::MAN_MASK + } +} + +macro_rules! impl_float { + ($($fty:ty, $ity:ty, $bits:literal);+) => { + $( + impl Float for $fty { + type Int = $ity; + type SInt = ::Signed; + const BITS: u32 = $bits; + const MAN_BITS: u32 = Self::MANTISSA_DIGITS - 1; + const MAN_MASK: Self::Int = (Self::Int::ONE << Self::MAN_BITS) - Self::Int::ONE; + const SIGN_MASK: Self::Int = Self::Int::ONE << (Self::BITS-1); + fn from_bits(i: Self::Int) -> Self { Self::from_bits(i) } + fn to_bits(self) -> Self::Int { self.to_bits() } + fn constants() -> &'static Constants { + use std::sync::LazyLock; + static CONSTANTS: LazyLock = LazyLock::new(Constants::new::<$fty>); + &CONSTANTS + } + } + )+ + } +} + +impl_float!(f32, u32, 32; f64, u64, 64); + +/// A test generator. Should provide an iterator that produces unique patterns to parse. +/// +/// The iterator needs to provide a `WriteCtx` (could be anything), which is then used to +/// write the string at a later step. This is done separately so that we can reuse string +/// allocations (which otherwise turn out to be a pretty expensive part of these tests). +pub trait Generator: Iterator + Send + 'static { + /// Full display and filtering name + const NAME: &'static str; + + /// Name for display with the progress bar + const SHORT_NAME: &'static str; + + /// The context needed to create a test string. + type WriteCtx: Send; + + /// Number of tests that will be run. + fn total_tests() -> u64; + + /// Constructor for this test generator. + fn new() -> Self; + + /// Create a test string given write context, which was produced as a step from the iterator. + /// + /// `s` will be provided empty. + fn write_string(s: &mut String, ctx: Self::WriteCtx); +} + +/// For tests that use iterator combinators, it is easier to just to box the iterator than trying +/// to specify its type. This is a shorthand for the usual type. +pub type BoxGenIter = Box>::WriteCtx> + Send>; diff --git a/lexical-parse-float/etc/correctness/test-parse-random/u32-small.rs b/lexical-parse-float/etc/correctness/test-parse-random/u32-small.rs deleted file mode 100644 index 571ac80e..00000000 --- a/lexical-parse-float/etc/correctness/test-parse-random/u32-small.rs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2015 The Rust Project Developers. See the COPYRIGHT -// file at the top-level directory of this distribution and at -// http://rust-lang.org/COPYRIGHT. -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -mod _common; - -use _common::validate; - -fn main() { - for i in 0..(1 << 19) { - validate(&i.to_string()); - } -} diff --git a/lexical-parse-float/etc/correctness/test-parse-random/u64-pow2.rs b/lexical-parse-float/etc/correctness/test-parse-random/u64-pow2.rs deleted file mode 100644 index 84927855..00000000 --- a/lexical-parse-float/etc/correctness/test-parse-random/u64-pow2.rs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2015 The Rust Project Developers. See the COPYRIGHT -// file at the top-level directory of this distribution and at -// http://rust-lang.org/COPYRIGHT. -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -mod _common; - -use _common::validate; - -fn main() { - for exp in 19..64 { - let power: u64 = 1 << exp; - validate(&power.to_string()); - for offset in 1..123 { - validate(&(power + offset).to_string()); - validate(&(power - offset).to_string()); - } - } - for offset in 0..123 { - validate(&(u64::MAX - offset).to_string()); - } -} diff --git a/lexical-parse-float/etc/correctness/test-parse-random/ui.rs b/lexical-parse-float/etc/correctness/test-parse-random/ui.rs new file mode 100644 index 00000000..f333bd4a --- /dev/null +++ b/lexical-parse-float/etc/correctness/test-parse-random/ui.rs @@ -0,0 +1,132 @@ +//! Progress bars and such. + +use std::io::{self, Write}; +use std::process::ExitCode; +use std::time::Duration; + +use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; + +use crate::{Completed, Config, EarlyExit, FinishedAll, TestInfo}; + +/// Templates for progress bars. +const PB_TEMPLATE: &str = "[{elapsed:3} {percent:3}%] {bar:20.cyan/blue} NAME ({pos}/{len}, {msg} f, {per_sec}, eta {eta})"; +const PB_TEMPLATE_FINAL: &str = + "[{elapsed:3} {percent:3}%] NAME ({pos}/{len}, {msg:.COLOR}, {per_sec}, {elapsed_precise})"; + +/// Create a new progress bar within a multiprogress bar. +pub fn create_pb( + mp: &MultiProgress, + total_tests: u64, + short_name_padded: &str, + all_bars: &mut Vec, +) -> ProgressBar { + let pb = mp.add(ProgressBar::new(total_tests)); + let pb_style = ProgressStyle::with_template(&PB_TEMPLATE.replace("NAME", short_name_padded)) + .unwrap() + .progress_chars("##-"); + + pb.set_style(pb_style.clone()); + pb.set_message("0"); + all_bars.push(pb.clone()); + pb +} + +/// Removes the status bar and replace it with a message. +pub fn finalize_pb(pb: &ProgressBar, short_name_padded: &str, c: &Completed) { + let f = c.failures; + + // Use a tuple so we can use colors + let (color, msg, finish_pb): (&str, String, fn(&ProgressBar, String)) = match &c.result { + Ok(FinishedAll) if f > 0 => { + ("red", format!("{f} f (finished with errors)",), ProgressBar::finish_with_message) + } + Ok(FinishedAll) => { + ("green", format!("{f} f (finished successfully)",), ProgressBar::finish_with_message) + } + Err(EarlyExit::Timeout) => { + ("red", format!("{f} f (timed out)"), ProgressBar::abandon_with_message) + } + Err(EarlyExit::MaxFailures) => { + ("red", format!("{f} f (failure limit)"), ProgressBar::abandon_with_message) + } + }; + + let pb_style = ProgressStyle::with_template( + &PB_TEMPLATE_FINAL.replace("NAME", short_name_padded).replace("COLOR", color), + ) + .unwrap(); + + pb.set_style(pb_style); + finish_pb(pb, msg); +} + +/// Print final messages after all tests are complete. +pub fn finish(tests: &[TestInfo], total_elapsed: Duration, cfg: &Config) -> ExitCode { + println!("\n\nResults:"); + + let mut failed_generators = 0; + let mut stopped_generators = 0; + + for t in tests { + let Completed { executed, failures, elapsed, warning, result } = t.completed.get().unwrap(); + + let stat = if result.is_err() { + stopped_generators += 1; + "STOPPED" + } else if *failures > 0 { + failed_generators += 1; + "FAILURE" + } else { + "SUCCESS" + }; + + println!( + " {stat} for generator '{name}'. {passed}/{executed} passed in {elapsed:?}", + name = t.name, + passed = executed - failures, + ); + + if let Some(warning) = warning { + println!(" warning: {warning}"); + } + + match result { + Ok(FinishedAll) => (), + Err(EarlyExit::Timeout) => { + println!(" exited early; exceded {:?} timeout", cfg.timeout) + } + Err(EarlyExit::MaxFailures) => { + println!(" exited early; exceeded {:?} max failures", cfg.max_failures) + } + } + } + + println!( + "{passed}/{} tests succeeded in {total_elapsed:?} ({passed} passed, {} failed, {} stopped)", + tests.len(), + failed_generators, + stopped_generators, + passed = tests.len() - failed_generators - stopped_generators, + ); + + if failed_generators > 0 || stopped_generators > 0 { + ExitCode::FAILURE + } else { + ExitCode::SUCCESS + } +} + +/// indicatif likes to eat panic messages. This workaround isn't ideal, but it improves things. +/// . +pub fn set_panic_hook(drop_bars: Vec) { + let hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + for bar in &drop_bars { + bar.abandon(); + println!(); + io::stdout().flush().unwrap(); + io::stderr().flush().unwrap(); + } + hook(info); + })); +} diff --git a/lexical-parse-float/etc/correctness/test-parse-random/validate.rs b/lexical-parse-float/etc/correctness/test-parse-random/validate.rs new file mode 100644 index 00000000..1eb3699c --- /dev/null +++ b/lexical-parse-float/etc/correctness/test-parse-random/validate.rs @@ -0,0 +1,364 @@ +//! Everything related to verifying that parsed outputs are correct. + +use std::any::type_name; +use std::collections::BTreeMap; +use std::ops::RangeInclusive; +use std::str::FromStr; +use std::sync::LazyLock; + +use num::bigint::ToBigInt; +use num::{BigInt, BigRational, FromPrimitive, Signed, ToPrimitive}; + +use crate::{CheckFailure, Float, Int, Update}; + +/// Powers of two that we store for constants. Account for binary128 which has a 15-bit exponent. +const POWERS_OF_TWO_RANGE: RangeInclusive = (-(2 << 15))..=(2 << 15); + +/// Powers of ten that we cache. Account for binary128, which can fit +4932/-4931 +const POWERS_OF_TEN_RANGE: RangeInclusive = -5_000..=5_000; + +/// Cached powers of 10 so we can look them up rather than recreating. +static POWERS_OF_TEN: LazyLock> = LazyLock::new(|| { + POWERS_OF_TEN_RANGE.map(|exp| (exp, BigRational::from_u32(10).unwrap().pow(exp))).collect() +}); + +/// Rational property-related constants for a specific float type. +#[allow(dead_code)] +#[derive(Debug)] +pub struct Constants { + /// The minimum positive value (a subnormal). + min_subnormal: BigRational, + /// The maximum possible finite value. + max: BigRational, + /// Cutoff between rounding to zero and rounding to the minimum value (min subnormal). + zero_cutoff: BigRational, + /// Cutoff between rounding to the max value and rounding to infinity. + inf_cutoff: BigRational, + /// Opposite of `inf_cutoff` + neg_inf_cutoff: BigRational, + /// The powers of two for all relevant integers. + powers_of_two: BTreeMap, + /// Half of each power of two. ULP = "unit in last position". + /// + /// This is a mapping from integers to half the precision available at that exponent. In other + /// words, `0.5 * 2^n` = `2^(n-1)`, which is half the distance between `m * 2^n` and + /// `(m + 1) * 2^n`, m ∈ ℤ. + /// + /// So, this is the maximum error from a real number to its floating point representation, + /// assuming the float type can represent the exponent. + half_ulp: BTreeMap, + /// Handy to have around so we don't need to reallocate for it + two: BigInt, +} + +impl Constants { + pub fn new() -> Self { + let two_int = &BigInt::from_u32(2).unwrap(); + let two = &BigRational::from_integer(2.into()); + + // The minimum subnormal (aka minimum positive) value. Most negative power of two is the + // minimum exponent (bias - 1) plus the extra from shifting within the mantissa bits. + let min_subnormal = two.pow(-(F::EXP_BIAS + F::MAN_BITS - 1).to_signed()); + + // The maximum value is the maximum exponent with a fully saturated mantissa. This + // is easiest to calculate by evaluating what the next value up would be if representable + // (zeroed mantissa, exponent increments by one, i.e. `2^(bias + 1)`), and subtracting + // a single LSB (`2 ^ (-mantissa_bits)`). + let max = (two - two.pow(-F::MAN_BITS.to_signed())) * (two.pow(F::EXP_BIAS.to_signed())); + let zero_cutoff = &min_subnormal / two_int; + + let inf_cutoff = &max + two_int.pow(F::EXP_BIAS - F::MAN_BITS - 1); + let neg_inf_cutoff = -&inf_cutoff; + + let powers_of_two: BTreeMap = + (POWERS_OF_TWO_RANGE).map(|n| (n, two.pow(n))).collect(); + let mut half_ulp = powers_of_two.clone(); + half_ulp.iter_mut().for_each(|(_k, v)| *v = &*v / two_int); + + Self { + min_subnormal, + max, + zero_cutoff, + inf_cutoff, + neg_inf_cutoff, + powers_of_two, + half_ulp, + two: two_int.clone(), + } + } +} + +/// Validate that a string parses correctly +pub fn validate(input: &str) -> Result<(), Update> { + let parsed: F = input + .parse() + .unwrap_or_else(|e| panic!("parsing failed for {}: {e}. Input: {input}", type_name::())); + + // Parsed float, decoded into significand and exponent + let decoded = decode(parsed); + + // Float parsed separately into a rational + let rational = Rational::parse(input); + + // Verify that the values match + decoded.check(rational, input) +} + +/// The result of parsing a string to a float type. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum FloatRes { + Inf, + NegInf, + Zero, + Nan, + /// A real number with significand and exponent. Value is `sig * 2 ^ exp`. + Real { + sig: F::SInt, + exp: i32, + }, +} + +impl FloatRes { + /// Given a known exact rational, check that this representation is accurate within the + /// limits of the float representation. If not, construct a failure `Update` to send. + fn check(self, expected: Rational, input: &str) -> Result<(), Update> { + let consts = F::constants(); + // let bool_helper = |cond: bool, err| cond.then_some(()).ok_or(err); + + let res = match (expected, self) { + // Easy correct cases + (Rational::Inf, FloatRes::Inf) + | (Rational::NegInf, FloatRes::NegInf) + | (Rational::Nan, FloatRes::Nan) => Ok(()), + + // Easy incorrect cases + ( + Rational::Inf, + FloatRes::NegInf | FloatRes::Zero | FloatRes::Nan | FloatRes::Real { .. }, + ) => Err(CheckFailure::ExpectedInf), + ( + Rational::NegInf, + FloatRes::Inf | FloatRes::Zero | FloatRes::Nan | FloatRes::Real { .. }, + ) => Err(CheckFailure::ExpectedNegInf), + ( + Rational::Nan, + FloatRes::Inf | FloatRes::NegInf | FloatRes::Zero | FloatRes::Real { .. }, + ) => Err(CheckFailure::ExpectedNan), + (Rational::Finite(_), FloatRes::Nan) => Err(CheckFailure::UnexpectedNan), + + // Cases near limits + (Rational::Finite(r), FloatRes::Zero) => { + if r <= consts.zero_cutoff { + Ok(()) + } else { + Err(CheckFailure::UnexpectedZero) + } + } + (Rational::Finite(r), FloatRes::Inf) => { + if r >= consts.inf_cutoff { + Ok(()) + } else { + Err(CheckFailure::UnexpectedInf) + } + } + (Rational::Finite(r), FloatRes::NegInf) => { + if r <= consts.neg_inf_cutoff { + Ok(()) + } else { + Err(CheckFailure::UnexpectedNegInf) + } + } + + // Actual numbers + (Rational::Finite(r), FloatRes::Real { sig, exp }) => Self::validate_real(r, sig, exp), + }; + + res.map_err(|fail| Update::Failure { + fail, + input: input.into(), + float_res: format!("{self:?}").into(), + }) + } + + /// Check that `sig * 2^exp` is the same as `rational`, within the float's error margin. + fn validate_real(rational: BigRational, sig: F::SInt, exp: i32) -> Result<(), CheckFailure> { + let consts = F::constants(); + + // `2^exp`. Use cached powers of two to be faster. + let two_exp = consts + .powers_of_two + .get(&exp) + .unwrap_or_else(|| panic!("missing exponent {exp} for {}", type_name::())); + + // Rational from the parsed value, `sig * 2^exp` + let parsed_rational = two_exp * sig.to_bigint().unwrap(); + let error = (parsed_rational - &rational).abs(); + + // Determine acceptable error at this exponent, which is halfway between this value + // (`sig * 2^exp`) and the next value up (`(sig+1) * 2^exp`). + let half_ulp = consts.half_ulp.get(&exp).unwrap(); + + // If we are within one error value (but not equal) then we rounded correctly. + if &error < half_ulp { + return Ok(()); + } + + // For values where we are exactly between two representable values, meaning that the error + // is exactly one half of the precision at that exponent, we need to round to an even + // binary value (i.e. mantissa ends in 0). + let incorrect_midpoint_rounding = if &error == half_ulp { + if sig & F::SInt::ONE == F::SInt::ZERO { + return Ok(()); + } + + // We rounded to odd rather than even; failing based on midpoint rounding. + true + } else { + // We are out of spec for some other reason. + false + }; + + let one_ulp = consts.half_ulp.get(&(exp + 1)).unwrap(); + assert_eq!(one_ulp, &(half_ulp * &consts.two), "ULP values are incorrect"); + + let relative_error = error / one_ulp; + + Err(CheckFailure::InvalidReal { + error_float: relative_error.to_f64(), + error_str: relative_error.to_string().into(), + incorrect_midpoint_rounding, + }) + } + + /// Remove trailing zeros in the significand and adjust the exponent + #[cfg(test)] + fn normalize(self) -> Self { + use std::cmp::min; + + match self { + Self::Real { sig, exp } => { + // If there are trailing zeroes, remove them and increment the exponent instead + let shift = min(sig.trailing_zeros(), exp.wrapping_neg().try_into().unwrap()); + Self::Real { sig: sig >> shift, exp: exp + i32::try_from(shift).unwrap() } + } + _ => self, + } + } +} + +/// Decompose a float into its integral components. This includes the implicit bit. +/// +/// If `allow_nan` is `false`, panic if `NaN` values are reached. +fn decode(f: F) -> FloatRes { + let ione = F::SInt::ONE; + let izero = F::SInt::ZERO; + + let mut exponent_biased = f.exponent(); + let mut mantissa = f.mantissa().to_signed(); + + if exponent_biased == 0 { + if mantissa == izero { + return FloatRes::Zero; + } + + exponent_biased += 1; + } else if exponent_biased == F::EXP_SAT { + if mantissa != izero { + return FloatRes::Nan; + } + + if f.is_sign_negative() { + return FloatRes::NegInf; + } + + return FloatRes::Inf; + } else { + // Set implicit bit + mantissa |= ione << F::MAN_BITS; + } + + let mut exponent = i32::try_from(exponent_biased).unwrap(); + + // Adjust for bias and the rnage of the mantissa + exponent -= i32::try_from(F::EXP_BIAS + F::MAN_BITS).unwrap(); + + if f.is_sign_negative() { + mantissa = mantissa.wrapping_neg(); + } + + FloatRes::Real { sig: mantissa, exp: exponent } +} + +/// A rational or its unrepresentable values. +#[derive(Clone, Debug, PartialEq)] +enum Rational { + Inf, + NegInf, + Nan, + Finite(BigRational), +} + +impl Rational { + /// Turn a string into a rational. `None` if `NaN`. + fn parse(s: &str) -> Rational { + let mut s = s; // lifetime rules + + if s.strip_prefix('+').unwrap_or(s).eq_ignore_ascii_case("nan") + || s.eq_ignore_ascii_case("-nan") + { + return Rational::Nan; + } + + if s.strip_prefix('+').unwrap_or(s).eq_ignore_ascii_case("inf") { + return Rational::Inf; + } + + if s.eq_ignore_ascii_case("-inf") { + return Rational::NegInf; + } + + // Fast path; no decimals or exponents ot parse + if s.bytes().all(|b| b.is_ascii_digit() || b == b'-') { + return Rational::Finite(BigRational::from_str(s).unwrap()); + } + + let mut ten_exp: i32 = 0; + + // Remove and handle e.g. `e-4`, `e+10`, `e5` suffixes + if let Some(pos) = s.bytes().position(|b| b == b'e' || b == b'E') { + let (dec, exp) = s.split_at(pos); + s = dec; + ten_exp = exp[1..].parse().unwrap(); + } + + // Remove the decimal and instead change our exponent + // E.g. "12.3456" becomes "123456 * 10^-4" + let mut s_owned; + if let Some(pos) = s.bytes().position(|b| b == b'.') { + ten_exp = ten_exp.checked_sub((s.len() - pos - 1).try_into().unwrap()).unwrap(); + s_owned = s.to_owned(); + s_owned.remove(pos); + s = &s_owned; + } + + // let pow = BigRational::from_u32(10).unwrap().pow(ten_exp); + let pow = + POWERS_OF_TEN.get(&ten_exp).unwrap_or_else(|| panic!("missing power of ten {ten_exp}")); + let r = pow + * BigInt::from_str(s) + .unwrap_or_else(|e| panic!("`BigInt::from_str(\"{s}\")` failed with {e}")); + Rational::Finite(r) + } + + #[cfg(test)] + fn expect_finite(self) -> BigRational { + let Self::Finite(r) = self else { + panic!("got non rational: {self:?}"); + }; + + r + } +} + +#[cfg(test)] +mod tests; diff --git a/lexical-parse-float/etc/correctness/test-parse-random/validate/tests.rs b/lexical-parse-float/etc/correctness/test-parse-random/validate/tests.rs new file mode 100644 index 00000000..ab0e7d8a --- /dev/null +++ b/lexical-parse-float/etc/correctness/test-parse-random/validate/tests.rs @@ -0,0 +1,149 @@ +use num::ToPrimitive; + +use super::*; + +#[test] +fn test_parse_rational() { + assert_eq!(Rational::parse("1234").expect_finite(), BigRational::new(1234.into(), 1.into())); + assert_eq!( + Rational::parse("-1234").expect_finite(), + BigRational::new((-1234).into(), 1.into()) + ); + assert_eq!(Rational::parse("1e+6").expect_finite(), BigRational::new(1000000.into(), 1.into())); + assert_eq!(Rational::parse("1e-6").expect_finite(), BigRational::new(1.into(), 1000000.into())); + assert_eq!( + Rational::parse("10.4e6").expect_finite(), + BigRational::new(10400000.into(), 1.into()) + ); + assert_eq!( + Rational::parse("10.4e+6").expect_finite(), + BigRational::new(10400000.into(), 1.into()) + ); + assert_eq!( + Rational::parse("10.4e-6").expect_finite(), + BigRational::new(13.into(), 1250000.into()) + ); + assert_eq!( + Rational::parse("10.4243566462342456234124").expect_finite(), + BigRational::new(104243566462342456234124_i128.into(), 10000000000000000000000_i128.into()) + ); + assert_eq!(Rational::parse("inf"), Rational::Inf); + assert_eq!(Rational::parse("+inf"), Rational::Inf); + assert_eq!(Rational::parse("-inf"), Rational::NegInf); + assert_eq!(Rational::parse("NaN"), Rational::Nan); +} + +#[test] +fn test_decode() { + assert_eq!(decode(0f32), FloatRes::Zero); + assert_eq!(decode(f32::INFINITY), FloatRes::Inf); + assert_eq!(decode(f32::NEG_INFINITY), FloatRes::NegInf); + assert_eq!(decode(1.0f32).normalize(), FloatRes::Real { sig: 1, exp: 0 }); + assert_eq!(decode(-1.0f32).normalize(), FloatRes::Real { sig: -1, exp: 0 }); + assert_eq!(decode(100.0f32).normalize(), FloatRes::Real { sig: 100, exp: 0 }); + assert_eq!(decode(100.5f32).normalize(), FloatRes::Real { sig: 201, exp: -1 }); + assert_eq!(decode(-4.004f32).normalize(), FloatRes::Real { sig: -8396997, exp: -21 }); + assert_eq!(decode(0.0004f32).normalize(), FloatRes::Real { sig: 13743895, exp: -35 }); + assert_eq!(decode(f32::from_bits(0x1)).normalize(), FloatRes::Real { sig: 1, exp: -149 }); +} + +#[test] +fn test_validate() { + validate::("0").unwrap(); + validate::("-0").unwrap(); + validate::("1").unwrap(); + validate::("-1").unwrap(); + validate::("1.1").unwrap(); + validate::("-1.1").unwrap(); + validate::("1e10").unwrap(); + validate::("1e1000").unwrap(); + validate::("-1e1000").unwrap(); + validate::("1e-1000").unwrap(); + validate::("-1e-1000").unwrap(); +} + +#[test] +fn test_validate_real() { + // Most of the arbitrary values come from checking against . + let r = &BigRational::from_float(10.0).unwrap(); + FloatRes::::validate_real(r.clone(), 10, 0).unwrap(); + FloatRes::::validate_real(r.clone(), 10, -1).unwrap_err(); + FloatRes::::validate_real(r.clone(), 10, 1).unwrap_err(); + + let r = &BigRational::from_float(0.25).unwrap(); + FloatRes::::validate_real(r.clone(), 1, -2).unwrap(); + FloatRes::::validate_real(r.clone(), 2, -2).unwrap_err(); + + let r = &BigRational::from_float(1234.5678).unwrap(); + FloatRes::::validate_real(r.clone(), 0b100110100101001000101011, -13).unwrap(); + FloatRes::::validate_real(r.clone(), 0b100110100101001000101010, -13).unwrap_err(); + FloatRes::::validate_real(r.clone(), 0b100110100101001000101100, -13).unwrap_err(); + + let r = &BigRational::from_float(-1234.5678).unwrap(); + FloatRes::::validate_real(r.clone(), -0b100110100101001000101011, -13).unwrap(); + FloatRes::::validate_real(r.clone(), -0b100110100101001000101010, -13).unwrap_err(); + FloatRes::::validate_real(r.clone(), -0b100110100101001000101100, -13).unwrap_err(); +} + +#[test] +#[allow(unused)] +fn test_validate_real_rounding() { + // Check that we catch when values don't round to even. + + // For f32, the cutoff between 1.0 and the next value up (1.0000001) is + // 1.000000059604644775390625. Anything below it should round down, anything above it should + // round up, and the value itself should round _down_ because `1.0` has an even significand but + // 1.0000001 is odd. + let v1_low_down = Rational::parse("1.00000005960464477539062499999").expect_finite(); + let v1_mid_down = Rational::parse("1.000000059604644775390625").expect_finite(); + let v1_high_up = Rational::parse("1.00000005960464477539062500001").expect_finite(); + + let exp = -(f32::MAN_BITS as i32); + let v1_down_sig = 1 << f32::MAN_BITS; + let v1_up_sig = (1 << f32::MAN_BITS) | 0b1; + + FloatRes::::validate_real(v1_low_down.clone(), v1_down_sig, exp).unwrap(); + FloatRes::::validate_real(v1_mid_down.clone(), v1_down_sig, exp).unwrap(); + FloatRes::::validate_real(v1_high_up.clone(), v1_up_sig, exp).unwrap(); + FloatRes::::validate_real(-v1_low_down.clone(), -v1_down_sig, exp).unwrap(); + FloatRes::::validate_real(-v1_mid_down.clone(), -v1_down_sig, exp).unwrap(); + FloatRes::::validate_real(-v1_high_up.clone(), -v1_up_sig, exp).unwrap(); + + // 1.000000178813934326171875 is between 1.0000001 and the next value up, 1.0000002. The middle + // value here should round _up_ since 1.0000002 has an even mantissa. + let v2_low_down = Rational::parse("1.00000017881393432617187499999").expect_finite(); + let v2_mid_up = Rational::parse("1.000000178813934326171875").expect_finite(); + let v2_high_up = Rational::parse("1.00000017881393432617187500001").expect_finite(); + + let v2_down_sig = v1_up_sig; + let v2_up_sig = (1 << f32::MAN_BITS) | 0b10; + + FloatRes::::validate_real(v2_low_down.clone(), v2_down_sig, exp).unwrap(); + FloatRes::::validate_real(v2_mid_up.clone(), v2_up_sig, exp).unwrap(); + FloatRes::::validate_real(v2_high_up.clone(), v2_up_sig, exp).unwrap(); + FloatRes::::validate_real(-v2_low_down.clone(), -v2_down_sig, exp).unwrap(); + FloatRes::::validate_real(-v2_mid_up.clone(), -v2_up_sig, exp).unwrap(); + FloatRes::::validate_real(-v2_high_up.clone(), -v2_up_sig, exp).unwrap(); + + // Rounding the wrong direction should error + for res in [ + FloatRes::::validate_real(v1_mid_down.clone(), v1_up_sig, exp), + FloatRes::::validate_real(v2_mid_up.clone(), v2_down_sig, exp), + FloatRes::::validate_real(-v1_mid_down.clone(), -v1_up_sig, exp), + FloatRes::::validate_real(-v2_mid_up.clone(), -v2_down_sig, exp), + ] { + let e = res.unwrap_err(); + let CheckFailure::InvalidReal { incorrect_midpoint_rounding: true, .. } = e else { + panic!("{e:?}"); + }; + } +} + +/// Just a quick check that the constants are what we expect. +#[test] +fn check_constants() { + assert_eq!(f32::constants().max.to_f32().unwrap(), f32::MAX); + assert_eq!(f32::constants().min_subnormal.to_f32().unwrap(), f32::from_bits(0x1)); + assert_eq!(f64::constants().max.to_f64().unwrap(), f64::MAX); + assert_eq!(f64::constants().min_subnormal.to_f64().unwrap(), f64::from_bits(0x1)); +}