diff --git a/Cargo.lock b/Cargo.lock index 1d4c77ac5a..9d3580e8ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,7 +24,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.2.8", "once_cell", "version_check", ] @@ -118,6 +118,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.0" @@ -160,6 +166,16 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "block-padding", + "generic-array", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -169,6 +185,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-padding" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" + [[package]] name = "bumpalo" version = "3.12.0" @@ -277,8 +299,8 @@ checksum = "44bec8e5c9d09e439c4335b1af0abaab56dcf3b94999a936e1bb47b9134288f0" dependencies = [ "heck 0.4.1", "proc-macro-error", - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.52", + "quote 1.0.24", "syn 1.0.109", ] @@ -427,7 +449,7 @@ dependencies = [ "clap 2.34.0", "criterion-plot 0.4.5", "csv", - "itertools", + "itertools 0.10.5", "lazy_static", "num-traits", "oorandom", @@ -454,7 +476,7 @@ dependencies = [ "ciborium", "clap 3.2.23", "criterion-plot 0.5.0", - "itertools", + "itertools 0.10.5", "lazy_static", "num-traits", "oorandom", @@ -475,7 +497,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2673cc8207403546f45f5fd319a974b1e6983ad1a3ee7e6041650013be041876" dependencies = [ "cast", - "itertools", + "itertools 0.10.5", ] [[package]] @@ -485,7 +507,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" dependencies = [ "cast", - "itertools", + "itertools 0.10.5", ] [[package]] @@ -623,7 +645,7 @@ version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" dependencies = [ - "quote 1.0.23", + "quote 1.0.24", "syn 1.0.109", ] @@ -643,8 +665,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ "convert_case", - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.52", + "quote 1.0.24", "rustc_version", "syn 1.0.109", ] @@ -661,13 +683,22 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + [[package]] name = "digest" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ - "block-buffer", + "block-buffer 0.10.4", "crypto-common", ] @@ -727,6 +758,18 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +[[package]] +name = "embed-doc-image" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af36f591236d9d822425cb6896595658fa558fcebf5ee8accac1d4b92c47166e" +dependencies = [ + "base64 0.13.1", + "proc-macro2 1.0.52", + "quote 1.0.24", + "syn 1.0.109", +] + [[package]] name = "ena" version = "0.14.1" @@ -881,6 +924,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.2.8" @@ -889,7 +943,7 @@ checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] @@ -1050,6 +1104,15 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "itertools" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.10.5" @@ -1074,6 +1137,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "keccak" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3afef3b6eff9ce9d8ff9b3601125eec7f0c8cbac7abd14f355d053fa56c98768" +dependencies = [ + "cpufeatures", +] + [[package]] name = "lalrpop" version = "0.19.8" @@ -1085,7 +1157,7 @@ dependencies = [ "bit-set", "diff", "ena", - "itertools", + "itertools 0.10.5", "lalrpop-util", "petgraph", "pico-args", @@ -1175,8 +1247,8 @@ checksum = "a1d849148dbaf9661a6151d1ca82b13bb4c4c128146a88d05253b38d4e2f496c" dependencies = [ "beef", "fnv", - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.52", + "quote 1.0.24", "regex-syntax", "syn 1.0.109", ] @@ -1206,13 +1278,61 @@ dependencies = [ "url", ] +[[package]] +name = "malachite" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6cf7f4730c30071ba374fac86ad35b1cb7a0716f774737768667ea3fa1828e3" +dependencies = [ + "malachite-base", + "malachite-nz", + "malachite-q", +] + +[[package]] +name = "malachite-base" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b06bfa98a4b4802af5a4263b4ad4660e28e51e8490f6354eb9336c70767e1c5" +dependencies = [ + "itertools 0.9.0", + "rand", + "rand_chacha", + "ryu", + "sha3", +] + +[[package]] +name = "malachite-nz" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e21c64b7af5be3dc8cef16f786243faf59459fe4ba93b44efdeb264e5ade4" +dependencies = [ + "embed-doc-image", + "itertools 0.9.0", + "malachite-base", + "serde", +] + +[[package]] +name = "malachite-q" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3755e541d5134b5016594c9043094172c4dda9259b3ce824a7b8101941850360" +dependencies = [ + "itertools 0.9.0", + "malachite-base", + "malachite-nz", + "serde", +] + [[package]] name = "md-5" version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" dependencies = [ - "digest", + "digest 0.10.6", ] [[package]] @@ -1274,7 +1394,7 @@ checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.45.0", ] @@ -1309,6 +1429,8 @@ dependencies = [ "lalrpop", "lalrpop-util", "logos", + "malachite", + "malachite-q", "md-5", "nickel-lang-utilities", "once_cell", @@ -1463,6 +1585,12 @@ version = "11.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + [[package]] name = "os_str_bytes" version = "6.4.1" @@ -1535,8 +1663,8 @@ checksum = "75a1ef20bf3193c15ac345acb32e26b3dc3223aff4d77ae4fc5359567683796b" dependencies = [ "pest", "pest_meta", - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.52", + "quote 1.0.24", "syn 1.0.109", ] @@ -1588,7 +1716,7 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffac6a51110e97610dd3ac73e34a65b27e56a1e305df41bad1f616d8e1cb22f4" dependencies = [ - "base64", + "base64 0.21.0", "indexmap", "line-wrap", "quick-xml 0.27.1", @@ -1646,6 +1774,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "precomputed-hash" version = "0.1.1" @@ -1683,8 +1817,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.52", + "quote 1.0.24", "syn 1.0.109", "version_check", ] @@ -1695,8 +1829,8 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.52", + "quote 1.0.24", "version_check", ] @@ -1711,9 +1845,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.51" +version = "1.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +checksum = "1d0e1ae9e836cc3beddd63db0df682593d7e2d3d891ae8c9083d2113e1744224" dependencies = [ "unicode-ident", ] @@ -1770,9 +1904,9 @@ version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94144a1266e236b1c932682136dc35a9dee8d3589728f68130c7c3861ef96b28" dependencies = [ - "proc-macro2 1.0.51", + "proc-macro2 1.0.52", "pyo3-macros-backend", - "quote 1.0.23", + "quote 1.0.24", "syn 1.0.109", ] @@ -1782,8 +1916,8 @@ version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8df9be978a2d2f0cdebabb03206ed73b11314701a5bfe71b0d753b81997777f" dependencies = [ - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.52", + "quote 1.0.24", "syn 1.0.109", ] @@ -1816,11 +1950,11 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +checksum = "50686e0021c4136d1d453b2dfe059902278681512a34d4248435dc34b6b5c8ec" dependencies = [ - "proc-macro2 1.0.51", + "proc-macro2 1.0.52", ] [[package]] @@ -1833,6 +1967,47 @@ dependencies = [ "nibble_vec", ] +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core", +] + [[package]] name = "rayon" version = "1.7.0" @@ -1870,7 +2045,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "getrandom", + "getrandom 0.2.8", "redox_syscall", "thiserror", ] @@ -1965,8 +2140,8 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8218eaf5d960e3c478a1b0f129fa888dd3d8d22eb3de097e9af14c1ab4438024" dependencies = [ - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.52", + "quote 1.0.24", "syn 1.0.109", ] @@ -1999,15 +2174,15 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "semver" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" +checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" [[package]] name = "serde" -version = "1.0.154" +version = "1.0.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cdd151213925e7f1ab45a9bbfb129316bd00799784b174b7cc7bcd16961c49e" +checksum = "71f2b4817415c6d4210bfe1c7bfcf4801b2d904cb4d0e1a8fdb651013c9e86b8" dependencies = [ "serde_derive", ] @@ -2035,12 +2210,12 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.154" +version = "1.0.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fc80d722935453bcafdc2c9a73cd6fac4dc1938f0346035d84bf99fa9e33217" +checksum = "d071a94a3fac4aff69d023a7f411e33f40f3483f8c5190b1953822b6b76d7630" dependencies = [ - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.52", + "quote 1.0.24", "syn 1.0.109", ] @@ -2061,8 +2236,8 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "395627de918015623b32e7669714206363a7fc00382bf477e72c1f7533e8eafc" dependencies = [ - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.52", + "quote 1.0.24", "syn 1.0.109", ] @@ -2096,7 +2271,7 @@ checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.6", ] [[package]] @@ -2107,7 +2282,19 @@ checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.6", +] + +[[package]] +name = "sha3" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809" +dependencies = [ + "block-buffer 0.9.0", + "digest 0.9.0", + "keccak", + "opaque-debug", ] [[package]] @@ -2247,8 +2434,8 @@ checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" dependencies = [ "heck 0.3.3", "proc-macro-error", - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.52", + "quote 1.0.24", "syn 1.0.109", ] @@ -2292,8 +2479,8 @@ version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.52", + "quote 1.0.24", "unicode-ident", ] @@ -2426,8 +2613,8 @@ version = "1.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e" dependencies = [ - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.52", + "quote 1.0.24", "syn 1.0.109", ] @@ -2515,9 +2702,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.19.4" +version = "0.19.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a1eb0622d28f4b9c90adc4ea4b2b46b47663fde9ac5fafcb14a1369d5508825" +checksum = "7082a95d48029677a28f181e5f6422d0c8339ad8396a39d3f33d62a90c1f6c30" dependencies = [ "indexmap", "serde", @@ -2660,6 +2847,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -2687,8 +2880,8 @@ dependencies = [ "bumpalo", "log", "once_cell", - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.52", + "quote 1.0.24", "syn 1.0.109", "wasm-bindgen-shared", ] @@ -2699,7 +2892,7 @@ version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" dependencies = [ - "quote 1.0.23", + "quote 1.0.24", "wasm-bindgen-macro-support", ] @@ -2709,8 +2902,8 @@ version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" dependencies = [ - "proc-macro2 1.0.51", - "quote 1.0.23", + "proc-macro2 1.0.52", + "quote 1.0.24", "syn 1.0.109", "wasm-bindgen-backend", "wasm-bindgen-shared", diff --git a/Cargo.toml b/Cargo.toml index 8973591cb3..0c2f247fc0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,6 +64,8 @@ pretty = "0.11.3" comrak = { version = "0.16.0", optional = true, features = [] } once_cell = "1.17.1" typed-arena = "2.0.2" +malachite = {version = "0.3.2", features = ["enable_serde"] } +malachite-q = "0.3.2" [dev-dependencies] pretty_assertions = "1.3.0" diff --git a/benches/arrays.rs b/benches/arrays.rs index 32e36ee53d..5b39a10c85 100644 --- a/benches/arrays.rs +++ b/benches/arrays.rs @@ -3,7 +3,7 @@ use std::rc::Rc; use criterion::{criterion_main, Criterion}; use nickel_lang::term::{ array::{Array, ArrayAttrs}, - RichTerm, Term, + Number, RichTerm, Term, }; use nickel_lang_utilities::{ncl_bench_group, EvalMode}; use pprof::criterion::{Output, PProfProfiler}; @@ -20,7 +20,7 @@ fn ncl_random_array(len: usize) -> String { for _ in 0..len { acc = (a * acc + c) % m; - numbers.push(RichTerm::from(Term::Num(acc as f64))); + numbers.push(RichTerm::from(Term::Num(Number::from(acc)))); } let xs = RichTerm::from(Term::Array( diff --git a/doc/manual/syntax.md b/doc/manual/syntax.md index c2357d90fb..4be1a5c518 100644 --- a/doc/manual/syntax.md +++ b/doc/manual/syntax.md @@ -22,8 +22,20 @@ There are four basic kinds of values in Nickel : ### Numeric values Nickel has a support for numbers, positive and negative, with or without -decimals. Internally, those numbers are stored as 64-bits floating point -numbers, following the IEEE 754 standard. +decimals. Internally, those numbers are stored as arbitrary precision rational +numbers, meaning that basic arithmetic operations (addition, subtraction, +division and multiplication) don't incur rounding errors. Numbers are +deserialized as 64-bits floating point numbers. + +Raising to a non-integer power is one operation which can incur rounding errors: +both operands need to be converted to the nearest 64-bits floating point +numbers, the power is computed as a 64-bits floating point number as well, +and converted back to an arbitrary precision rational number. + +Numbers are serialized as integers whenever possible (when they fit exactly into +a 64-bits signed integer or a 64-bits unsigned integer), and as a 64 bits float +otherwise. The latter conversion might lose precision as well, for example when +serializing `1/3`. Examples: diff --git a/doc/manual/typing.md b/doc/manual/typing.md index 85e3ed4e5f..73cee78b7b 100644 --- a/doc/manual/typing.md +++ b/doc/manual/typing.md @@ -162,7 +162,10 @@ Let us now have a quick tour of the type system. The basic types are: - `Dyn`: the dynamic type. This is the type given to most expressions outside of a typed block. A value of type `Dyn` can be pretty much anything. -- `Number`: the only number type. Currently implemented as a 64bits float. +- `Number`: the only number type. Currently implemented as arbitrary precision + rationals. Common arithmetic operations are exact. The power function using + a non-integer exponent is computed on floating point values, which + might incur rounding errors. - `String`: a string, which must always be valid UTF8. - `Bool`: a boolean, that is either `true` or `false`. diff --git a/src/deserialize.rs b/src/deserialize.rs index 32a1e15744..ae55d13eb4 100644 --- a/src/deserialize.rs +++ b/src/deserialize.rs @@ -1,5 +1,6 @@ //! Deserialization of an evaluated program to plain Rust types. +use malachite::{num::conversion::traits::RoundingFrom, rounding_modes::RoundingMode}; use std::collections::HashMap; use std::iter::ExactSizeIterator; @@ -20,24 +21,7 @@ macro_rules! deserialize_number { V: Visitor<'de>, { match unwrap_term(self)? { - Term::Num(n) => visitor.$visit(n as $type), - other => Err(RustDeserializationError::InvalidType { - expected: "Number".to_string(), - occurred: RichTerm::from(other).to_string(), - }), - } - } - }; -} - -macro_rules! deserialize_number_round { - ($method:ident, $type:tt, $visit:ident) => { - fn $method(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - match unwrap_term(self)? { - Term::Num(n) => visitor.$visit(n.round() as $type), + Term::Num(n) => visitor.$visit($type::rounding_from(&n, RoundingMode::Nearest)), other => Err(RustDeserializationError::InvalidType { expected: "Number".to_string(), occurred: RichTerm::from(other).to_string(), @@ -70,7 +54,7 @@ impl<'de> serde::Deserializer<'de> for RichTerm { match unwrap_term(self)? { Term::Null => visitor.visit_unit(), Term::Bool(v) => visitor.visit_bool(v), - Term::Num(v) => visitor.visit_f64(v), + Term::Num(v) => visitor.visit_f64(f64::rounding_from(v, RoundingMode::Nearest)), Term::Str(v) => visitor.visit_string(v), Term::Enum(v) => visitor.visit_enum(EnumDeserializer { variant: v.into_label(), @@ -88,16 +72,16 @@ impl<'de> serde::Deserializer<'de> for RichTerm { } } - deserialize_number_round!(deserialize_i8, i8, visit_i8); - deserialize_number_round!(deserialize_i16, i16, visit_i16); - deserialize_number_round!(deserialize_i32, i32, visit_i32); - deserialize_number_round!(deserialize_i64, i64, visit_i64); - deserialize_number_round!(deserialize_i128, i128, visit_i128); - deserialize_number_round!(deserialize_u8, u8, visit_u8); - deserialize_number_round!(deserialize_u16, u16, visit_u16); - deserialize_number_round!(deserialize_u32, u32, visit_u32); - deserialize_number_round!(deserialize_u64, u64, visit_u64); - deserialize_number_round!(deserialize_u128, u128, visit_u128); + deserialize_number!(deserialize_i8, i8, visit_i8); + deserialize_number!(deserialize_i16, i16, visit_i16); + deserialize_number!(deserialize_i32, i32, visit_i32); + deserialize_number!(deserialize_i64, i64, visit_i64); + deserialize_number!(deserialize_i128, i128, visit_i128); + deserialize_number!(deserialize_u8, u8, visit_u8); + deserialize_number!(deserialize_u16, u16, visit_u16); + deserialize_number!(deserialize_u32, u32, visit_u32); + deserialize_number!(deserialize_u64, u64, visit_u64); + deserialize_number!(deserialize_u128, u128, visit_u128); deserialize_number!(deserialize_f32, f32, visit_f32); deserialize_number!(deserialize_f64, f64, visit_f64); diff --git a/src/eval/merge.rs b/src/eval/merge.rs index bcfb13971e..28164b0d8b 100644 --- a/src/eval/merge.rs +++ b/src/eval/merge.rs @@ -131,7 +131,7 @@ pub fn merge( } } (Term::Num(n1), Term::Num(n2)) => { - if (n1 - n2).abs() < f64::EPSILON { + if n1 == n2 { Ok(Closure::atomic_closure(RichTerm::new( Term::Num(n1), pos_op.into_inherited(), diff --git a/src/eval/operation.rs b/src/eval/operation.rs index 6b103d05d6..b2128096f6 100644 --- a/src/eval/operation.rs +++ b/src/eval/operation.rs @@ -18,6 +18,7 @@ use crate::{ identifier::Ident, label::ty_path, match_sharedterm, mk_app, mk_fun, mk_opn, mk_record, + parser::utils::parse_number, position::TermPos, serialize, serialize::ExportFormat, @@ -26,18 +27,27 @@ use crate::{ array::{Array, ArrayAttrs}, make as mk_term, record::{self, Field, FieldMetadata, RecordAttrs, RecordData}, - BinaryOp, MergePriority, NAryOp, PendingContract, RecordExtKind, RichTerm, SharedTerm, - StrChunk, Term, UnaryOp, + BinaryOp, MergePriority, NAryOp, Number, PendingContract, RecordExtKind, RichTerm, + SharedTerm, StrChunk, Term, UnaryOp, }, transform::Closurizable, }; -use md5::digest::Digest; +use malachite::{ + num::{ + arithmetic::traits::Pow, + basic::traits::{One, Zero}, + conversion::traits::{RoundingFrom, ToSci}, + }, + rounding_modes::RoundingMode, + Integer, +}; +use md5::digest::Digest; use simple_counter::*; use unicode_segmentation::UnicodeSegmentation; -use std::{collections::HashMap, iter::Extend, rc::Rc}; +use std::{collections::HashMap, convert::TryFrom, iter::Extend, rc::Rc}; generate_counter!(FreshVariableCounter, usize); @@ -604,48 +614,56 @@ impl VirtualMachine { .pop_arg(&self.cache) .ok_or_else(|| EvalError::NotEnoughArgs(2, String::from("generate"), pos_op))?; - if let Term::Num(n) = *t { - let n_int = n as usize; - if n < 0.0 || n.fract() != 0.0 { - Err(EvalError::Other( - format!( - "generate: expected the 1st argument to be a positive integer, got {n}" - ), - pos_op, - )) - } else { - let mut shared_env = Environment::new(); - let f_as_var = f.body.closurize(&mut self.cache, &mut env, f.env); - - // Array elements are closurized to preserve laziness of data structures. It - // maintains the invariant that any data structure only contain indices (that is, - // currently, variables). - let ts = (0..n_int) - .map(|n| { - mk_app!(f_as_var.clone(), Term::Num(n as f64)).closurize( - &mut self.cache, - &mut shared_env, - env.clone(), - ) - }) - .collect(); - - Ok(Closure { - body: RichTerm::new( - Term::Array(ts, ArrayAttrs::new().closurized()), - pos_op_inh, - ), - env: shared_env, - }) - } - } else { - Err(EvalError::TypeError( + let Term::Num(ref n) = *t else { + return Err(EvalError::TypeError( String::from("Num"), String::from("generate, 1st argument"), arg_pos, RichTerm { term: t, pos }, )) + }; + + if n < &Number::ZERO { + return Err(EvalError::Other( + format!( + "generate: expected the 1st argument to be a positive number, got {n}" + ), + pos_op, + )); } + + let Ok(n_int) = u32::try_from(n) else { + return Err(EvalError::Other( + format!( + "generate: expected the 1st argument to be an integer smaller that {}, got {n}", u32::MAX, + ), + pos_op, + )) + }; + + let mut shared_env = Environment::new(); + let f_as_var = f.body.closurize(&mut self.cache, &mut env, f.env); + + // Array elements are closurized to preserve laziness of data structures. It + // maintains the invariant that any data structure only contain indices (that is, + // currently, variables). + let ts = (0..n_int) + .map(|n| { + mk_app!(f_as_var.clone(), Term::Num(n.into())).closurize( + &mut self.cache, + &mut shared_env, + env.clone(), + ) + }) + .collect(); + + Ok(Closure { + body: RichTerm::new( + Term::Array(ts, ArrayAttrs::new().closurized()), + pos_op_inh, + ), + env: shared_env, + }) } UnaryOp::RecordMap() => { let (f, ..) = self.stack.pop_arg(&self.cache).ok_or_else(|| { @@ -833,7 +851,7 @@ impl VirtualMachine { if let Term::Array(ts, _) = &*t { // A num does not have any free variable so we can drop the environment Ok(Closure { - body: RichTerm::new(Term::Num(ts.len() as f64), pos_op_inh), + body: RichTerm::new(Term::Num(ts.len().into()), pos_op_inh), env: Environment::new(), }) } else { @@ -940,9 +958,9 @@ impl VirtualMachine { UnaryOp::CharCode() => { if let Term::Str(s) = &*t { if s.len() == 1 { - let code = (s.chars().next().unwrap() as u32) as f64; + let code = s.chars().next().unwrap() as u32; Ok(Closure::atomic_closure(RichTerm::new( - Term::Num(code), + Term::Num(code.into()), pos_op_inh, ))) } else { @@ -961,29 +979,29 @@ impl VirtualMachine { } } UnaryOp::CharFromCode() => { - if let Term::Num(code) = *t { - if code.fract() != 0.0 { - Err(EvalError::Other(format!("charFromCode: expected the argument to be an integer, got the floating-point value {code}"), pos_op)) - } else if code < 0.0 || code > (u32::MAX as f64) { - Err(EvalError::Other(format!("charFromCode: code out of bounds. Expected a value between 0 and {}, got {}", u32::MAX, code), pos_op)) - } else if let Some(car) = std::char::from_u32(code as u32) { - Ok(Closure::atomic_closure(RichTerm::new( - Term::Str(String::from(car)), - pos_op_inh, - ))) - } else { - Err(EvalError::Other( - format!("charFromCode: invalid character code {code}"), - pos_op, - )) - } - } else { - Err(EvalError::TypeError( + let Term::Num(ref code) = *t else { + return Err(EvalError::TypeError( String::from("Num"), String::from("charFromCode"), arg_pos, RichTerm { term: t, pos }, )) + }; + + let Ok(code_as_u32) = u32::try_from(code) else { + return Err(EvalError::Other(format!("charFromCode: expected the argument to be a positive integer smaller than {}, got {code}", u32::MAX), pos_op)); + }; + + if let Some(car) = std::char::from_u32(code_as_u32) { + Ok(Closure::atomic_closure(RichTerm::new( + Term::Str(String::from(car)), + pos_op_inh, + ))) + } else { + Err(EvalError::Other( + format!("charFromCode: invalid character code {code}"), + pos_op, + )) } } UnaryOp::StrUppercase() => { @@ -1020,7 +1038,7 @@ impl VirtualMachine { if let Term::Str(s) = &*t { let length = s.graphemes(true).count(); Ok(Closure::atomic_closure(RichTerm::new( - Term::Num(length as f64), + Term::Num(length.into()), pos_op_inh, ))) } else { @@ -1034,7 +1052,7 @@ impl VirtualMachine { } UnaryOp::ToStr() => { let result = match_sharedterm! {t, with { - Term::Num(n) => Ok(Term::Str(n.to_string())), + Term::Num(n) => Ok(Term::Str(format!("{}", n.to_sci()))), Term::Str(s) => Ok(Term::Str(s)), Term::Bool(b) => Ok(Term::Str(b.to_string())), Term::Enum(id) => Ok(Term::Str(id.to_string())), @@ -1042,18 +1060,19 @@ impl VirtualMachine { } else { Err(EvalError::Other( format!( - "strFrom: can't convert the argument of type {} to string", + "to_string: can't convert the argument of type {} to string", t.type_of().unwrap() ), pos, )) }}?; + Ok(Closure::atomic_closure(RichTerm::new(result, pos_op_inh))) } UnaryOp::NumFromStr() => { if let Term::Str(s) = &*t { - let n = s.parse::().map_err(|_| { - EvalError::Other(format!("numFrom: invalid num literal `{s}`"), pos) + let n = parse_number(s).map_err(|_| { + EvalError::Other(format!("num_from_string: invalid num literal `{s}`"), pos) })?; Ok(Closure::atomic_closure(RichTerm::new( Term::Num(n), @@ -1062,7 +1081,7 @@ impl VirtualMachine { } else { Err(EvalError::TypeError( String::from("Str"), - String::from("strLength"), + String::from("num_from_string"), arg_pos, RichTerm { term: t, pos }, )) @@ -1167,7 +1186,7 @@ impl VirtualMachine { mk_record!( ("matched", Term::Str(String::from(first_match.as_str()))), - ("index", Term::Num(first_match.start() as f64)), + ("index", Term::Num(first_match.start().into())), ( "groups", Term::Array(groups, ArrayAttrs::new().closurized()) @@ -1177,7 +1196,7 @@ impl VirtualMachine { //FIXME: what should we return when there's no match? mk_record!( ("matched", Term::Str(String::new())), - ("index", Term::Num(-1.)), + ("index", Term::Num(Number::from(-1))), ( "groups", Term::Array(Array::default(), ArrayAttrs::default()) @@ -1392,8 +1411,8 @@ impl VirtualMachine { } } BinaryOp::Plus() => { - if let Term::Num(n1) = *t1 { - if let Term::Num(n2) = *t2 { + if let Term::Num(ref n1) = *t1 { + if let Term::Num(ref n2) = *t2 { Ok(Closure::atomic_closure(RichTerm::new( Term::Num(n1 + n2), pos_op_inh, @@ -1422,8 +1441,8 @@ impl VirtualMachine { } } BinaryOp::Sub() => { - if let Term::Num(n1) = *t1 { - if let Term::Num(n2) = *t2 { + if let Term::Num(ref n1) = *t1 { + if let Term::Num(ref n2) = *t2 { Ok(Closure::atomic_closure(RichTerm::new( Term::Num(n1 - n2), pos_op_inh, @@ -1452,8 +1471,8 @@ impl VirtualMachine { } } BinaryOp::Mult() => { - if let Term::Num(n1) = *t1 { - if let Term::Num(n2) = *t2 { + if let Term::Num(ref n1) = *t1 { + if let Term::Num(ref n2) = *t2 { Ok(Closure::atomic_closure(RichTerm::new( Term::Num(n1 * n2), pos_op_inh, @@ -1482,9 +1501,9 @@ impl VirtualMachine { } } BinaryOp::Div() => { - if let Term::Num(n1) = *t1 { - if let Term::Num(n2) = *t2 { - if n2 == 0.0 { + if let Term::Num(ref n1) = *t1 { + if let Term::Num(ref n2) = *t2 { + if n2 == &Number::ZERO { Err(EvalError::Other(String::from("division by zero"), pos_op)) } else { Ok(Closure::atomic_closure(RichTerm::new( @@ -1516,25 +1535,8 @@ impl VirtualMachine { } } BinaryOp::Modulo() => { - if let Term::Num(n1) = *t1 { - if let Term::Num(n2) = *t2 { - Ok(Closure::atomic_closure(RichTerm::new( - Term::Num(n1 % n2), - pos_op_inh, - ))) - } else { - Err(EvalError::TypeError( - String::from("Num"), - String::from("%, 2nd argument"), - snd_pos, - RichTerm { - term: t2, - pos: pos2, - }, - )) - } - } else { - Err(EvalError::TypeError( + let Term::Num(ref n1) = *t1 else { + return Err(EvalError::TypeError( String::from("Num"), String::from("%, 1st argument"), fst_pos, @@ -1543,13 +1545,61 @@ impl VirtualMachine { pos: pos1, }, )) + }; + + let Term::Num(ref n2) = *t2 else { + return Err(EvalError::TypeError( + String::from("Number"), + String::from("%, 2nd argument"), + snd_pos, + RichTerm { + term: t2, + pos: pos2, + }, + )) + }; + + if n2 == &Number::ZERO { + return Err(EvalError::Other(String::from("division by zero (%)"), pos2)); } + + // This is the equivalent of `truncate()` for `Number` + let quotient = Number::from(Integer::rounding_from(n1 / n2, RoundingMode::Down)); + + Ok(Closure::atomic_closure(RichTerm::new( + Term::Num(n1 - quotient * n2), + pos_op_inh, + ))) } BinaryOp::Pow() => { - if let Term::Num(n1) = *t1 { - if let Term::Num(n2) = *t2 { + if let Term::Num(ref n1) = *t1 { + if let Term::Num(ref n2) = *t2 { + // Malachite's Rationals don't support exponents larger than `u64`. Anyway, + // the result of such an operation would be huge and impractical to + // store. + // + // We first try to convert the rational to an `i64`, in which case the + // power is computed in an exact way. + // + // If the conversion fails, we fallback to converting both the exponent and + // the value to the nearest `f64`, perform the exponentiation, and convert + // the result back to rationals, with a possible loss of precision. + let result = if let Ok(n2_as_i64) = i64::try_from(n2) { + n1.pow(n2_as_i64) + } else { + let result_as_f64 = f64::rounding_from(n1, RoundingMode::Nearest) + .powf(f64::rounding_from(n2, RoundingMode::Nearest)); + // The following conversion fails if the result is NaN or +/-infinity + Number::try_from_float_simplest(result_as_f64).map_err(|_| + EvalError::Other( + format!("invalid arithmetic operation: {n1}^{n2} returned {result_as_f64}, but {result_as_f64} isn't representable in Nickel"), + pos_op + ) + )? + }; + Ok(Closure::atomic_closure(RichTerm::new( - Term::Num(n1.powf(n2)), + Term::Num(result), pos_op_inh, ))) } else { @@ -1761,8 +1811,8 @@ impl VirtualMachine { } } BinaryOp::LessThan() => { - if let Term::Num(n1) = *t1 { - if let Term::Num(n2) = *t2 { + if let Term::Num(ref n1) = *t1 { + if let Term::Num(ref n2) = *t2 { Ok(Closure::atomic_closure(RichTerm::new( Term::Bool(n1 < n2), pos_op_inh, @@ -1791,8 +1841,8 @@ impl VirtualMachine { } } BinaryOp::LessOrEq() => { - if let Term::Num(n1) = *t1 { - if let Term::Num(n2) = *t2 { + if let Term::Num(ref n1) = *t1 { + if let Term::Num(ref n2) = *t2 { Ok(Closure::atomic_closure(RichTerm::new( Term::Bool(n1 <= n2), pos_op_inh, @@ -1821,8 +1871,8 @@ impl VirtualMachine { } } BinaryOp::GreaterThan() => { - if let Term::Num(n1) = *t1 { - if let Term::Num(n2) = *t2 { + if let Term::Num(ref n1) = *t1 { + if let Term::Num(ref n2) = *t2 { Ok(Closure::atomic_closure(RichTerm::new( Term::Bool(n1 > n2), pos_op_inh, @@ -1851,8 +1901,8 @@ impl VirtualMachine { } } BinaryOp::GreaterOrEq() => { - if let Term::Num(n1) = *t1 { - if let Term::Num(n2) = *t2 { + if let Term::Num(ref n1) = *t1 { + if let Term::Num(ref n2) = *t2 { Ok(Closure::atomic_closure(RichTerm::new( Term::Bool(n1 >= n2), pos_op_inh, @@ -2201,22 +2251,24 @@ impl VirtualMachine { }, BinaryOp::ArrayElemAt() => match (&*t1, &*t2) { (Term::Array(ts, attrs), Term::Num(n)) => { - let n_int = *n as usize; - if n.fract() != 0.0 { - Err(EvalError::Other(format!("elemAt: expected the 2nd argument to be an integer, got the floating-point value {n}"), pos_op)) - } else if *n < 0.0 || n_int >= ts.len() { - Err(EvalError::Other(format!("elemAt: index out of bounds. Expected a value between 0 and {}, got {}", ts.len(), n), pos_op)) - } else { - let elem_with_ctr = PendingContract::apply_all( - ts.get(n_int).unwrap().clone(), - attrs.pending_contracts.iter().cloned(), - pos1.into_inherited(), - ); - Ok(Closure { - body: elem_with_ctr, - env: env1, - }) + let Ok(n_as_usize) = usize::try_from(n) else { + return Err(EvalError::Other(format!("elemAt: expected the 2nd argument to be a positive integer smaller than {}, got {n}", usize::MAX), pos_op)) + }; + + if n_as_usize >= ts.len() { + return Err(EvalError::Other(format!("elemAt: index out of bounds. Expected an index between 0 and {}, got {}", ts.len(), n), pos_op)); } + + let elem_with_ctr = PendingContract::apply_all( + ts.get(n_as_usize).unwrap().clone(), + attrs.pending_contracts.iter().cloned(), + pos1.into_inherited(), + ); + + Ok(Closure { + body: elem_with_ctr, + env: env1, + }) } (Term::Array(..), _) => Err(EvalError::TypeError( String::from("Num"), @@ -2829,23 +2881,26 @@ impl VirtualMachine { match (&*fst, &*snd, &*thd) { (Term::Str(s), Term::Num(start), Term::Num(end)) => { - let start_int = *start as usize; - let end_int = *end as usize; - - if start.fract() != 0.0 { - Err(EvalError::Other(format!("substring: expected the 2nd argument (start) to be an integer, got the floating-point value {start}"), pos_op)) - } else if !s.is_char_boundary(start_int) { - Err(EvalError::Other(format!("substring: index out of bounds. Expected the 2nd argument (start) to be between 0 and {}, got {}", s.len(), start), pos_op)) - } else if end.fract() != 0.0 { - Err(EvalError::Other(format!("substring: expected the 3nd argument (end) to be an integer, got the floating-point value {end}"), pos_op)) - } else if end <= start || !s.is_char_boundary(end_int) { - Err(EvalError::Other(format!("substring: index out of bounds. Expected the 3rd argument (end) to be between {} and {}, got {}", start+1., s.len(), end), pos_op)) - } else { - Ok(Closure::atomic_closure(RichTerm::new( - Term::Str(s[start_int..end_int].to_owned()), - pos_op_inh, - ))) + let Ok(start_as_usize) = usize::try_from(start) else { + return Err(EvalError::Other(format!("substring: expected the 2nd argument (start) to be a positive integer smaller than {}, got {start}", usize::MAX), pos_op)); + }; + + let Ok(end_as_usize) = usize::try_from(end) else { + return Err(EvalError::Other(format!("substring: expected the 3nd argument (end) to be a positive integer smaller than {}, got the floating-point value {end}", usize::MAX), pos_op)); + }; + + if !s.is_char_boundary(start_as_usize) { + return Err(EvalError::Other(format!("substring: index out of bounds. Expected the 2nd argument (start) to be between 0 and {}, got {}", s.len(), start), pos_op)); } + + if end <= start || !s.is_char_boundary(end_as_usize) { + return Err(EvalError::Other(format!("substring: index out of bounds. Expected the 3rd argument (end) to be between {} and {}, got {}", start + Number::ONE, s.len(), end), pos_op)); + }; + + Ok(Closure::atomic_closure(RichTerm::new( + Term::Str(s[start_as_usize..end_as_usize].to_owned()), + pos_op_inh, + ))) } (Term::Str(_), Term::Num(_), _) => Err(EvalError::TypeError( String::from("Str"), @@ -3561,14 +3616,10 @@ mod tests { let mut vm: VirtualMachine = VirtualMachine::new(DummyResolver {}); - vm.stack.push_arg( - Closure::atomic_closure(Term::Num(5.0).into()), - TermPos::None, - ); - vm.stack.push_arg( - Closure::atomic_closure(Term::Num(46.0).into()), - TermPos::None, - ); + vm.stack + .push_arg(Closure::atomic_closure(mk_term::integer(5)), TermPos::None); + vm.stack + .push_arg(Closure::atomic_closure(mk_term::integer(46)), TermPos::None); let mut clos = Closure { body: Term::Bool(true).into(), @@ -3582,7 +3633,7 @@ mod tests { assert_eq!( clos, Closure { - body: Term::Num(46.0).into(), + body: mk_term::integer(46), env: Environment::new() } ); @@ -3594,14 +3645,14 @@ mod tests { let cont = OperationCont::Op2First( BinaryOp::Plus(), Closure { - body: Term::Num(6.0).into(), + body: mk_term::integer(6), env: Environment::new(), }, TermPos::None, ); let mut clos = Closure { - body: Term::Num(7.0).into(), + body: mk_term::integer(7), env: Environment::new(), }; let mut vm = VirtualMachine::new(DummyResolver {}); @@ -3612,7 +3663,7 @@ mod tests { assert_eq!( clos, Closure { - body: Term::Num(6.0).into(), + body: mk_term::integer(6), env: Environment::new() } ); @@ -3623,7 +3674,7 @@ mod tests { OperationCont::Op2Second( BinaryOp::Plus(), Closure { - body: Term::Num(7.0).into(), + body: mk_term::integer(7), env: Environment::new(), }, TermPos::None, @@ -3641,7 +3692,7 @@ mod tests { let cont: OperationCont = OperationCont::Op2Second( BinaryOp::Plus(), Closure { - body: Term::Num(7.0).into(), + body: mk_term::integer(7), env: Environment::new(), }, TermPos::None, @@ -3651,7 +3702,7 @@ mod tests { let mut vm: VirtualMachine = VirtualMachine::new(DummyResolver {}); let mut clos = Closure { - body: Term::Num(6.0).into(), + body: mk_term::integer(6), env: Environment::new(), }; vm.stack.push_op_cont(cont, 0, TermPos::None); @@ -3661,7 +3712,7 @@ mod tests { assert_eq!( clos, Closure { - body: Term::Num(13.0).into(), + body: mk_term::integer(13), env: Environment::new() } ); diff --git a/src/eval/tests.rs b/src/eval/tests.rs index fd1d6b03af..2472078740 100644 --- a/src/eval/tests.rs +++ b/src/eval/tests.rs @@ -5,6 +5,7 @@ use crate::error::ImportError; use crate::label::Label; use crate::parser::{grammar, lexer}; use crate::term::make as mk_term; +use crate::term::Number; use crate::term::{BinaryOp, StrChunk, UnaryOp}; use crate::transform::import_resolution::strict::resolve_imports; use crate::{mk_app, mk_fun}; @@ -29,7 +30,7 @@ fn parse(s: &str) -> Option { #[test] fn identity_over_values() { - let num = Term::Num(45.3); + let num = Term::Num(Number::try_from(45.3).unwrap()); assert_eq!(Ok(num.clone()), eval_no_import(num.into())); let boolean = Term::Bool(true); @@ -62,36 +63,46 @@ fn lone_var_panics() { #[test] fn only_fun_are_applicable() { - eval_no_import(mk_app!(Term::Bool(true), Term::Num(45.))).unwrap_err(); + eval_no_import(mk_app!(Term::Bool(true), mk_term::integer(45))).unwrap_err(); } #[test] fn simple_app() { - let t = mk_app!(mk_term::id(), Term::Num(5.0)); - assert_eq!(Ok(Term::Num(5.0)), eval_no_import(t)); + let t = mk_app!(mk_term::id(), mk_term::integer(5)); + assert_eq!(Ok(Term::Num(Number::from(5))), eval_no_import(t)); } #[test] fn simple_let() { - let t = mk_term::let_in("x", Term::Num(5.0), mk_term::var("x")); - assert_eq!(Ok(Term::Num(5.0)), eval_no_import(t)); + let t = mk_term::let_in("x", mk_term::integer(5), mk_term::var("x")); + assert_eq!(Ok(Term::Num(Number::from(5))), eval_no_import(t)); } #[test] fn simple_ite() { - let t = mk_term::if_then_else(Term::Bool(true), Term::Num(5.0), Term::Bool(false)); - assert_eq!(Ok(Term::Num(5.0)), eval_no_import(t)); + let t = mk_term::if_then_else(Term::Bool(true), mk_term::integer(5), Term::Bool(false)); + assert_eq!(Ok(Term::Num(Number::from(5))), eval_no_import(t)); } #[test] fn simple_plus() { - let t = mk_term::op2(BinaryOp::Plus(), Term::Num(5.0), Term::Num(7.5)); - assert_eq!(Ok(Term::Num(12.5)), eval_no_import(t)); + let t = mk_term::op2( + BinaryOp::Plus(), + mk_term::integer(5), + Term::Num(Number::try_from(7.5).unwrap()), + ); + assert_eq!( + Ok(Term::Num(Number::try_from(12.5).unwrap())), + eval_no_import(t) + ); } #[test] fn asking_for_various_types() { - let num = mk_term::op1(UnaryOp::Typeof(), Term::Num(45.3)); + let num = mk_term::op1( + UnaryOp::Typeof(), + Term::Num(Number::try_from(45.3).unwrap()), + ); assert_eq!(Ok(Term::Enum("Number".into())), eval_no_import(num)); let boolean = mk_term::op1(UnaryOp::Typeof(), Term::Bool(true)); @@ -159,9 +170,9 @@ fn imports() { vm.reset(); assert_eq!( vm.eval(mk_import_two, &Environment::new(),) - .map(Term::from) + .map(RichTerm::without_pos) .unwrap(), - Term::Num(2.0) + mk_term::integer(2) ); // let x = import "lib" in x.f @@ -247,35 +258,35 @@ fn initial_env() { initial_env.insert( Ident::from("g"), eval_cache.add( - Closure::atomic_closure(Term::Num(1.0).into()), + Closure::atomic_closure(mk_term::integer(1)), IdentKind::Let, BindingType::Normal, ), ); - let t = mk_term::let_in("x", Term::Num(2.0), mk_term::var("x")); + let t = mk_term::let_in("x", mk_term::integer(2), mk_term::var("x")); assert_eq!( VirtualMachine::new_with_cache(DummyResolver {}, eval_cache.clone()) .eval(t, &initial_env) - .map(Term::from), - Ok(Term::Num(2.0)) + .map(RichTerm::without_pos), + Ok(mk_term::integer(2)) ); - let t = mk_term::let_in("x", Term::Num(2.0), mk_term::var("g")); + let t = mk_term::let_in("x", mk_term::integer(2), mk_term::var("g")); assert_eq!( VirtualMachine::new_with_cache(DummyResolver {}, eval_cache.clone()) .eval(t, &initial_env) - .map(Term::from), - Ok(Term::Num(1.0)) + .map(RichTerm::without_pos), + Ok(mk_term::integer(1)) ); // Shadowing of the initial environment - let t = mk_term::let_in("g", Term::Num(2.0), mk_term::var("g")); + let t = mk_term::let_in("g", mk_term::integer(2), mk_term::var("g")); assert_eq!( VirtualMachine::new_with_cache(DummyResolver {}, eval_cache.clone()) .eval(t, &initial_env) - .map(Term::from), - Ok(Term::Num(2.0)) + .map(RichTerm::without_pos), + Ok(mk_term::integer(2)) ); } @@ -300,7 +311,7 @@ fn substitution() { let mut eval_cache = CacheImpl::new(); let initial_env = mk_env( vec![ - ("glob1", Term::Num(1.0).into()), + ("glob1", mk_term::integer(1)), ("glob2", parse("\"Glob2\"").unwrap()), ("glob3", Term::Bool(false).into()), ], diff --git a/src/parser/grammar.lalrpop b/src/parser/grammar.lalrpop index 32c24fabbd..02d4ed5bee 100644 --- a/src/parser/grammar.lalrpop +++ b/src/parser/grammar.lalrpop @@ -71,6 +71,8 @@ use crate::{ label::Label, }; +use malachite::num::basic::traits::Zero; + grammar<'input, 'err, 'wcard>( src_id: FileId, errors: &'err mut Vec, ParseError>>, @@ -141,8 +143,7 @@ SimpleFieldAnnotAtom: FieldMetadata = { ..Default::default() }, "|" "priority" => FieldMetadata { - // unwrap(): a literal can't be NaN - priority: MergePriority::Numeral(NumeralPriority::try_from(<>).unwrap()), + priority: MergePriority::Numeral(<>), ..Default::default() }, "|" "optional" => FieldMetadata { @@ -789,7 +790,7 @@ InfixExpr: UniTerm = { #[precedence(level="1")] "-" > => - UniTerm::from(mk_term::op2(BinaryOp::Sub(), Term::Num(0.0), <>)), + UniTerm::from(mk_term::op2(BinaryOp::Sub(), Term::Num(Number::ZERO), <>)), #[precedence(level="2")] #[assoc(side="left")] InfixBOpApp, @@ -894,19 +895,20 @@ TypeAtom: Types = { ), |erows, row| EnumRows(EnumRowsF::Extend { row, tail: Box::new(erows) }) ); - Types::from(TypeF::Enum(ty)) + + Types::from(TypeF::Enum(ty)) }, "{" "_" ":" > "}" => { Types::from(TypeF::Dict(Box::new(t))) }, "_" => { let id = *next_wildcard_id; - *next_wildcard_id += 1; + *next_wildcard_id += 1; Types::from(TypeF::Wildcard(id)) }, } -SignedNumLiteral: f64 = => { +SignedNumLiteral: Number = => { if sign.is_some() { -value } else { @@ -923,7 +925,7 @@ extern { "str literal" => Token::Str(StringToken::Literal(<&'input str>)), "str esc char" => Token::Str(StringToken::EscapedChar()), "multstr literal" => Token::MultiStr(MultiStringToken::Literal(<&'input str>)), - "num literal" => Token::Normal(NormalToken::NumLiteral()), + "num literal" => Token::Normal(NormalToken::NumLiteral()), "raw enum tag" => Token::Normal(NormalToken::RawEnumTag(<&'input str>)), "`\"" => Token::Normal(NormalToken::StrEnumTagBegin), diff --git a/src/parser/lexer.rs b/src/parser/lexer.rs index 06e2b49954..5da9674421 100644 --- a/src/parser/lexer.rs +++ b/src/parser/lexer.rs @@ -29,7 +29,11 @@ //! `0`, this is the end of the current interpolated expressions, and we leave the normal mode and //! go back to string mode. In our example, this is the second `}`: at this point, the lexer knows //! that the coming characters must be lexed as string tokens, and not as normal tokens. -use crate::parser::error::{LexicalError, ParseError}; +use super::{ + error::{LexicalError, ParseError}, + utils::parse_number, +}; +use crate::term::Number; use logos::Logos; use std::ops::Range; @@ -65,8 +69,8 @@ pub enum NormalToken<'input> { // regex for checking identifiers at ../lsp/nls/src/requests/completion.rs #[regex("_?[a-zA-Z][_a-zA-Z0-9-']*")] Identifier(&'input str), - #[regex("[0-9]*\\.?[0-9]+", |lex| lex.slice().parse())] - NumLiteral(f64), + #[regex("[0-9]*\\.?[0-9]+", |lex| parse_number(lex.slice()))] + NumLiteral(Number), // **IMPORTANT** // This regex should be kept in sync with the one for Identifier above. diff --git a/src/parser/tests.rs b/src/parser/tests.rs index b104c1b2b9..60d4c8428a 100644 --- a/src/parser/tests.rs +++ b/src/parser/tests.rs @@ -6,9 +6,11 @@ use crate::error::ParseError; use crate::identifier::Ident; use crate::parser::error::ParseError as InternalParseError; use crate::term::array::Array; +use crate::term::Number; use crate::term::Term::*; use crate::term::{make as mk_term, Term}; use crate::term::{record, BinaryOp, RichTerm, StrChunk, UnaryOp}; + use crate::{mk_app, mk_match}; use assert_matches::assert_matches; use codespan::Files; @@ -66,11 +68,14 @@ fn mk_symbolic_single_chunk(prefix: &str, s: &str) -> RichTerm { #[test] fn numbers() { - assert_eq!(parse_without_pos("22"), Num(22.0).into()); - assert_eq!(parse_without_pos("22.0"), Num(22.0).into()); - assert_eq!(parse_without_pos("22.22"), Num(22.22).into()); - assert_eq!(parse_without_pos("(22)"), Num(22.0).into()); - assert_eq!(parse_without_pos("((22))"), Num(22.0).into()); + assert_eq!(parse_without_pos("22"), mk_term::integer(22)); + assert_eq!(parse_without_pos("22.0"), mk_term::integer(22)); + assert_eq!( + parse_without_pos("22.22"), + Num(Number::try_from_float_simplest(22.22).unwrap()).into() + ); + assert_eq!(parse_without_pos("(22)"), mk_term::integer(22)); + assert_eq!(parse_without_pos("((22))"), mk_term::integer(22)); } #[test] @@ -116,14 +121,14 @@ fn symbolic_strings() { fn plus() { assert_eq!( parse_without_pos("3 + 4"), - Op2(BinaryOp::Plus(), Num(3.0).into(), Num(4.).into()).into() + Op2(BinaryOp::Plus(), mk_term::integer(3), mk_term::integer(4)).into() ); assert_eq!( parse_without_pos("(true + false) + 4"), Op2( BinaryOp::Plus(), Op2(BinaryOp::Plus(), Bool(true).into(), Bool(false).into()).into(), - Num(4.).into(), + mk_term::integer(4), ) .into() ); @@ -139,7 +144,11 @@ fn booleans() { fn ite() { assert_eq!( parse_without_pos("if true then 3 else 4"), - mk_app!(mk_term::op1(UnaryOp::Ite(), Bool(true)), Num(3.0), Num(4.0)) + mk_app!( + mk_term::op1(UnaryOp::Ite(), Bool(true)), + mk_term::integer(3), + mk_term::integer(4) + ) ); } @@ -147,12 +156,16 @@ fn ite() { fn applications() { assert_eq!( parse_without_pos("1 true 2"), - mk_app!(Num(1.0), Bool(true), Num(2.0)) + mk_app!(mk_term::integer(1), Bool(true), mk_term::integer(2)) ); assert_eq!( parse_without_pos("1 (2 3) 4"), - mk_app!(Num(1.0), mk_app!(Num(2.0), Num(3.0)), Num(4.0)) + mk_app!( + mk_term::integer(1), + mk_app!(mk_term::integer(2), mk_term::integer(3)), + mk_term::integer(4) + ) ); } @@ -218,16 +231,16 @@ fn enum_terms() { "match with raw tags", "match { `foo => true, `bar => false, _ => 456, } 123", mk_app!( - mk_match!(("foo", Bool(true)), ("bar", Bool(false)) ; Num(456.)), - Num(123.) + mk_match!(("foo", Bool(true)), ("bar", Bool(false)) ; mk_term::integer(456)), + mk_term::integer(123) ), ), ( "match with string tags", "match { `\"one:two\" => true, `\"three four\" => false, _ => 13 } 1", mk_app!( - mk_match!(("one:two", Bool(true)), ("three four", Bool(false)) ; Num(13.)), - Num(1.) + mk_match!(("one:two", Bool(true)), ("three four", Bool(false)) ; mk_term::integer(13)), + mk_term::integer(1) ), ), ]; @@ -260,9 +273,9 @@ fn record_terms() { RecRecord( record::RecordData::with_field_values( vec![ - (Ident::from("a"), Num(1.).into()), - (Ident::from("b"), Num(2.).into()), - (Ident::from("c"), Num(3.).into()), + (Ident::from("a"), mk_term::integer(1)), + (Ident::from("b"), mk_term::integer(2)), + (Ident::from("c"), mk_term::integer(3)), ] .into_iter() .collect() @@ -278,18 +291,18 @@ fn record_terms() { RecRecord( record::RecordData::with_field_values( vec![ - (Ident::from("a"), Num(1.).into()), - (Ident::from("d"), Num(42.).into()), + (Ident::from("a"), mk_term::integer(1)), + (Ident::from("d"), mk_term::integer(42)), ] .into_iter() .collect() ), vec![( - StrChunks(vec![StrChunk::expr(RichTerm::from(Num(123.)))]).into(), + StrChunks(vec![StrChunk::expr(mk_term::integer(123))]).into(), Field::from(mk_app!( - mk_term::op1(UnaryOp::Ite(), Num(4.)), - Num(5.), - Num(6.) + mk_term::op1(UnaryOp::Ite(), mk_term::integer(4)), + mk_term::integer(5), + mk_term::integer(6) )) )], None, @@ -302,8 +315,8 @@ fn record_terms() { RecRecord( record::RecordData::with_field_values( vec![ - (Ident::from("a"), Num(1.).into()), - (Ident::from("\"%}%"), Num(2.).into()), + (Ident::from("a"), mk_term::integer(1)), + (Ident::from("\"%}%"), mk_term::integer(2)), ] .into_iter() .collect() @@ -364,7 +377,7 @@ fn string_lexing() { Token::Normal(NormalToken::DoubleQuote), Token::Str(StringToken::Literal("1 + ")), Token::Str(StringToken::Interpolation), - Token::Normal(NormalToken::NumLiteral(1.0)), + Token::Normal(NormalToken::NumLiteral(Number::from(1))), Token::Normal(NormalToken::RBrace), Token::Str(StringToken::Literal(" + 2")), Token::Normal(NormalToken::DoubleQuote), @@ -379,7 +392,7 @@ fn string_lexing() { Token::Str(StringToken::Interpolation), Token::Normal(NormalToken::DoubleQuote), Token::Str(StringToken::Interpolation), - Token::Normal(NormalToken::NumLiteral(1.0)), + Token::Normal(NormalToken::NumLiteral(Number::from(1))), Token::Normal(NormalToken::RBrace), Token::Normal(NormalToken::DoubleQuote), Token::Normal(NormalToken::RBrace), @@ -417,7 +430,7 @@ fn string_lexing() { })), Token::MultiStr(MultiStringToken::Literal("text ")), Token::MultiStr(MultiStringToken::Interpolation), - Token::Normal(NormalToken::NumLiteral(1.0)), + Token::Normal(NormalToken::NumLiteral(Number::from(1))), Token::Normal(NormalToken::RBrace), Token::MultiStr(MultiStringToken::Literal(" etc.")), Token::MultiStr(MultiStringToken::End), diff --git a/src/parser/utils.rs b/src/parser/utils.rs index ccaaa2e5a5..62b3611d5f 100644 --- a/src/parser/utils.rs +++ b/src/parser/utils.rs @@ -19,11 +19,20 @@ use crate::{ term::{ make as mk_term, record::{Field, FieldMetadata, RecordAttrs, RecordData}, - BinaryOp, LabeledType, LetMetadata, RichTerm, StrChunk, Term, TypeAnnotation, UnaryOp, + BinaryOp, LabeledType, LetMetadata, Rational, RichTerm, StrChunk, Term, TypeAnnotation, + UnaryOp, }, types::{TypeF, Types}, }; +use malachite::num::conversion::traits::FromSciString; + +pub struct ParseNumberError; + +pub fn parse_number(slice: &str) -> Result { + Rational::from_sci_string(slice).ok_or(ParseNumberError) +} + /// Distinguish between the standard string opening delimiter `"`, the multi-line string /// opening delimter `m%"`, and the symbolic string opening delimiter `s%"`. #[derive(Copy, Clone, Eq, PartialEq, Debug)] diff --git a/src/pretty.rs b/src/pretty.rs index 0909399637..f3e78a93fb 100644 --- a/src/pretty.rs +++ b/src/pretty.rs @@ -4,11 +4,14 @@ use crate::parser::lexer::KEYWORDS; use crate::term::{ record::{Field, FieldMetadata}, - BinaryOp, MergePriority, RichTerm, StrChunk, Term, TypeAnnotation, UnaryOp, + BinaryOp, MergePriority, Number, RichTerm, StrChunk, Term, TypeAnnotation, UnaryOp, }; use crate::types::{EnumRows, EnumRowsF, RecordRowF, RecordRows, RecordRowsF, TypeF, Types}; + +use malachite::num::{basic::traits::Zero, conversion::traits::ToSci}; pub use pretty::{DocAllocator, DocBuilder, Pretty}; use regex::Regex; + use std::collections::HashMap; #[derive(Clone, Copy, Eq, PartialEq)] @@ -178,7 +181,7 @@ where } else { self.nil() }) - .append(match metadata.priority { + .append(match &metadata.priority { MergePriority::Bottom => self.line().append(self.text("| default")), MergePriority::Neutral => self.nil(), MergePriority::Numeral(p) => self @@ -433,7 +436,7 @@ where match self.as_ref() { Null => allocator.text("null"), Bool(v) => allocator.as_string(v), - Num(v) => allocator.as_string(v), + Num(n) => allocator.as_string(format!("{}", n.to_sci())), Str(v) => allocator.escaped_string(v).double_quotes(), StrChunks(chunks) => allocator.chunks( chunks, @@ -668,7 +671,7 @@ where .append(op.pretty(allocator)) .append(rtl.to_owned().pretty(allocator)) } else { - if (&BinaryOp::Sub(), &Num(0.0)) == (op, rtl.as_ref()) { + if (&BinaryOp::Sub(), &Num(Number::ZERO)) == (op, rtl.as_ref()) { allocator.text("-") } else if let crate::term::OpPos::Prefix = op.pos() { op.pretty(allocator) diff --git a/src/program.rs b/src/program.rs index cbd0f04b17..cb58a8d3b7 100644 --- a/src/program.rs +++ b/src/program.rs @@ -559,17 +559,20 @@ mod tests { #[test] fn evaluation_full() { - use crate::{mk_array, mk_record, term::Term}; + use crate::{ + mk_array, mk_record, + term::{make as mk_term, Term}, + }; let t = eval_full("[(1 + 1), (\"a\" ++ \"b\"), ([ 1, [1 + 2] ])]").unwrap(); // [2, "ab", [1, [3]]] let expd = mk_array!( - Term::Num(2_f64), + mk_term::integer(2), Term::Str(String::from("ab")), mk_array!( - Term::Num(1_f64), - mk_array!(Term::Num(3_f64); ArrayAttrs::new().closurized()); + mk_term::integer(1), + mk_array!(mk_term::integer(3); ArrayAttrs::new().closurized()); ArrayAttrs::new().closurized() ); ArrayAttrs::new().closurized() @@ -581,7 +584,7 @@ mod tests { // Records are parsed as RecRecords, so we need to build one by hand let expd = mk_record!(( "foo", - mk_record!(("bar", mk_record!(("baz", Term::Num(2.0))))) + mk_record!(("bar", mk_record!(("baz", mk_term::integer(2))))) )); assert_eq!(t.without_pos(), expd); diff --git a/src/serialize.rs b/src/serialize.rs index 0d2bfdf6ac..f169677ccf 100644 --- a/src/serialize.rs +++ b/src/serialize.rs @@ -1,18 +1,22 @@ //! Serialization of an evaluated program to various data format. +use malachite::{num::conversion::traits::RoundingFrom, rounding_modes::RoundingMode}; + use crate::{ error::SerializationError, term::{ array::{Array, ArrayAttrs}, record::RecordData, - RichTerm, Term, TypeAnnotation, + Number, RichTerm, Term, TypeAnnotation, }, }; use serde::{ de::{Deserialize, Deserializer}, - ser::{Error, Serialize, SerializeMap, SerializeSeq, Serializer}, + ser::{Serialize, SerializeMap, SerializeSeq, Serializer}, }; +use malachite::num::conversion::traits::IsInteger; + use std::{collections::HashMap, fmt, io, rc::Rc, str::FromStr}; /// Available export formats. @@ -65,23 +69,44 @@ impl FromStr for ExportFormat { } } -/// Implicitly convert float to integers when possible to avoid trailing zeros. Note this this -/// only work if the float is in range of either `i64` or `f64`. It seems there's no easy general -/// solution (working for both YAML, TOML, and JSON) to choose the way floating point values are -/// formatted. -pub fn serialize_num(n: &f64, serializer: S) -> Result +/// Implicitly convert numbers to primitive integers when possible, and serialize an exact +/// representation. Note that `u128` and `i128` aren't supported for common configuration formats +/// in serde, so we rather pick `i64` and `u64`, even if the former couple theoretically allows for +/// a wider range of rationals to be exactly represented. We don't expect values to be that large +/// in practice anyway: using arbitrary precision rationals is directed toward not introducing rounding errors +/// when performing simple arithmetic operations over decimals numbers, mostly. +/// +/// If the number doesn't fit into an `i64` or `u64`, we approximate it by the nearest `f64` and +/// serialize this value. This may incur a loss of precision, but this is expected: we can't +/// represent something like e.g. `1/3` exactly in JSON anyway. +pub fn serialize_num(n: &Number, serializer: S) -> Result where S: Serializer, { - if n.fract() == 0.0 { - if *n < 0.0 && *n >= (i64::MIN as f64) && *n <= (i64::MAX as f64) { - return (*n as i64).serialize(serializer); - } else if *n >= 0.0 && *n <= (u64::MAX as f64) { - return (*n as u64).serialize(serializer); + if n.is_integer() { + if *n < 0 { + if let Ok(n_as_integer) = i64::try_from(n) { + return n_as_integer.serialize(serializer); + } + } else if let Ok(n_as_uinteger) = u64::try_from(n) { + return n_as_uinteger.serialize(serializer); } } - n.serialize(serializer) + f64::rounding_from(n, RoundingMode::Nearest).serialize(serializer) +} + +/// Deserialize a Nickel number. As for parsing, we convert the number from a 64bits float +pub fn deserialize_num<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let as_f64 = f64::deserialize(deserializer)?; + Number::try_from_float_simplest(as_f64).map_err(|_| { + serde::de::Error::custom(format!( + "couldn't convert {as_f64} to a Nickel number: Nickel doesn't support NaN nor infinity" + )) + }) } /// Serializer for annotated values. @@ -106,7 +131,7 @@ where .iter_serializable() .collect::, _>>() .map_err(|missing_def_err| { - Error::custom(format!( + serde::ser::Error::custom(format!( "missing field definition for `{}`", missing_def_err.id )) diff --git a/src/term/mod.rs b/src/term/mod.rs index 03ca21af37..749f3557fa 100644 --- a/src/term/mod.rs +++ b/src/term/mod.rs @@ -34,6 +34,14 @@ use crate::{ }; use codespan::FileId; +pub use malachite::{ + num::{ + basic::traits::Zero, + conversion::traits::{IsInteger, RoundingFrom, ToSci}, + }, + rounding_modes::RoundingMode, + Integer, Rational, +}; use serde::{Deserialize, Serialize}; @@ -60,7 +68,8 @@ pub enum Term { Bool(bool), /// A floating-point value. #[serde(serialize_with = "crate::serialize::serialize_num")] - Num(f64), + #[serde(deserialize_with = "crate::serialize::deserialize_num")] + Num(Number), /// A literal string. Str(String), /// A string containing interpolated expressions, represented as a list of either literals or @@ -189,7 +198,7 @@ pub enum Term { /// ```nickel /// let r = { /// foo = bar + 1, - /// bar | Num, + /// bar | Number, /// baz = 2, /// } in /// r.baz + (r & {bar = 1}).foo @@ -205,8 +214,25 @@ pub enum Term { RuntimeError(EvalError), } +/// A unique sealing key, introduced by polymorphic contracts. pub type SealingKey = i32; +/// The underlying type representing Nickel numbers. Currently, numbers are arbitrary precision +/// rationals. +/// +/// Basic arithmetic operations are exact, without loss of precision (within the limits of +/// available memory). +/// +/// Raising to a power that doesn't fit in a signed 64bits number will lead to converting both +/// operands to 64-bits floats, performing the floating-point power operation, and converting back +/// to rationals, which can incur a loss of precision. +/// +/// [^number-serialization]: Conversion to string and serialization try to first convert the +/// rational as an exact signed or usigned 64-bits integer. If this succeeds, such operations don't +/// lose precision. Otherwise, the number is converted to the nearest 64bit float and then +/// serialized/printed, which can incur a loss of information. +pub type Number = Rational; + /// Type of let-binding. This only affects run-time behavior. Revertible bindings introduce /// revertible cache elements at evaluation, which are devices used for the implementation of recursive /// records merging. See the [`crate::eval::merge`] and [`crate::eval`] modules for more details. @@ -313,71 +339,18 @@ impl From for record::FieldMetadata { } } -/// A wrapper around f64 which makes `NaN` not representable. As opposed to floats, it is `Eq` and -/// `Ord`. -#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] -pub struct NumeralPriority(f64); - -/// Error raised when trying to convert a float with `NaN` value to a `NumeralPriority`. -#[derive(Debug, Copy, Clone)] -pub struct PriorityIsNaN; - -// The following impl are ok because `NumeralPriority(NaN)` can't be constructed. -impl Eq for NumeralPriority {} - -// We can't derive `Ord` because there is an `f64` inside -// but it is actually an `Ord` because `NaN` is forbidden. -// See `TryFrom` smart constructor. -#[allow(clippy::derive_ord_xor_partial_ord)] -impl Ord for NumeralPriority { - fn cmp(&self, other: &Self) -> Ordering { - // Ok: NaN is forbidden - self.partial_cmp(other).unwrap() - } -} - -impl NumeralPriority { - pub fn zero() -> Self { - NumeralPriority(0.0) - } -} - -impl TryFrom for NumeralPriority { - type Error = PriorityIsNaN; - - fn try_from(f: f64) -> Result { - if f.is_nan() { - Err(PriorityIsNaN) - } else { - Ok(NumeralPriority(f)) - } - } -} - -impl From for f64 { - fn from(n: NumeralPriority) -> Self { - n.0 - } -} - -impl fmt::Display for NumeralPriority { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Clone)] pub enum MergePriority { /// The priority of default values that are overridden by everything else. Bottom, /// The priority by default, when no priority annotation (`default`, `force`, `priority`) is /// provided. /// - /// Act as the value `MergePriority::Numeral(0.0)` with respect to ordering and equality + /// Act as the value `MergePriority::Numeral(0)` with respect to ordering and equality /// testing. The only way to discriminate this variant is to pattern match on it. Neutral, /// A numeral priority. - Numeral(NumeralPriority), + Numeral(Number), /// The priority of values that override everything else and can't be overridden. Top, } @@ -397,7 +370,7 @@ impl PartialEq for MergePriority { (MergePriority::Numeral(p1), MergePriority::Numeral(p2)) => p1 == p2, (MergePriority::Neutral, MergePriority::Numeral(p)) | (MergePriority::Numeral(p), MergePriority::Neutral) - if p == &NumeralPriority::zero() => + if p == &Number::ZERO => { true } @@ -422,8 +395,8 @@ impl Ord for MergePriority { (MergePriority::Top, _) | (_, MergePriority::Bottom) => Ordering::Greater, // Neutral and numeral. - (MergePriority::Neutral, MergePriority::Numeral(n)) => NumeralPriority::zero().cmp(n), - (MergePriority::Numeral(n), MergePriority::Neutral) => n.cmp(&NumeralPriority::zero()), + (MergePriority::Neutral, MergePriority::Numeral(n)) => Number::ZERO.cmp(n), + (MergePriority::Numeral(n), MergePriority::Neutral) => n.cmp(&Number::ZERO), } } } @@ -438,7 +411,7 @@ impl fmt::Display for MergePriority { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { MergePriority::Bottom => write!(f, "default"), - MergePriority::Neutral => write!(f, "{}", NumeralPriority::zero()), + MergePriority::Neutral => write!(f, "{}", Number::ZERO), MergePriority::Numeral(p) => write!(f, "{p}"), MergePriority::Top => write!(f, "force"), } @@ -683,7 +656,7 @@ impl Term { Term::Null => String::from("null"), Term::Bool(true) => String::from("true"), Term::Bool(false) => String::from("false"), - Term::Num(n) => format!("{n}"), + Term::Num(n) => format!("{}", n.to_sci()), Term::Str(s) => format!("\"{s}\""), Term::StrChunks(chunks) => { let chunks_str: Vec = chunks @@ -1716,10 +1689,10 @@ impl std::fmt::Display for RichTerm { /// It is used somehow as a match statement, going from /// ``` /// # use nickel_lang::term::{RichTerm, Term}; -/// let rt = RichTerm::from(Term::Num(5.0)); +/// let rt = RichTerm::from(Term::Bool(true)); /// /// match rt.term.into_owned() { -/// Term::Num(x) => x as usize, +/// Term::Bool(x) => usize::from(x), /// Term::Str(s) => s.len(), /// _ => 42, /// }; @@ -1728,10 +1701,10 @@ impl std::fmt::Display for RichTerm { /// ``` /// # use nickel_lang::term::{RichTerm, Term}; /// # use nickel_lang::match_sharedterm; -/// let rt = RichTerm::from(Term::Num(5.0)); +/// let rt = RichTerm::from(Term::Bool(true)); /// /// match_sharedterm!{rt.term, with { -/// Term::Num(x) => x as usize, +/// Term::Bool(x) => usize::from(x), /// Term::Str(s) => s.len(), /// } else 42 /// }; @@ -1965,6 +1938,10 @@ pub mod make { { Term::Import(path.into()).into() } + + pub fn integer(n: impl Into) -> RichTerm { + Term::Num(Number::from(n.into())).into() + } } #[cfg(test)] diff --git a/stdlib/number.ncl b/stdlib/number.ncl index 8168fe67cc..4376db9640 100644 --- a/stdlib/number.ncl +++ b/stdlib/number.ncl @@ -192,6 +192,19 @@ pow 2 8 => 256 ``` + + # Precision + + Nickel numbers are arbitrary precision rationals. If the exponent `y` is + an integer which fits in a 64-bits signed or unsigned integer (that is, if + `y` is an integer between `−2^63` and `2^64-1`), the result is computed + exactly. + + Otherwise, both operands `x` and `y` are converted to the nearest 64 bits + float (excluding `NaN` and infinity), and we compute the result as a 64 + bits float. This result is then converted back to a rational. In this + case, **be aware that both the conversion from rationals to floats, and + the power operation, might incur rounding errors**. "% = fun x n => %pow% x n, } diff --git a/tests/integration/imports.rs b/tests/integration/imports.rs index ef1eb9bc00..5c2b2ca2f3 100644 --- a/tests/integration/imports.rs +++ b/tests/integration/imports.rs @@ -1,6 +1,6 @@ use assert_matches::assert_matches; use nickel_lang::error::{Error, EvalError, ImportError, TypecheckError}; -use nickel_lang::term::Term; +use nickel_lang::term::{make as mk_term, RichTerm, Term}; use nickel_lang_utilities::TestProgram; use std::io::BufReader; use std::path::PathBuf; @@ -21,7 +21,10 @@ fn nested() { "should_be = 3", ) .unwrap(); - assert_eq!(prog.eval().map(Term::from), Ok(Term::Num(3.))); + assert_eq!( + prog.eval().map(RichTerm::without_pos), + Ok(mk_term::integer(3)) + ); } #[test] @@ -31,7 +34,10 @@ fn root_path() { "should_be = 44", ) .unwrap(); - assert_eq!(prog.eval().map(Term::from), Ok(Term::Num(44.))); + assert_eq!( + prog.eval().map(RichTerm::without_pos), + Ok(mk_term::integer(44)) + ); } #[test] @@ -41,7 +47,10 @@ fn multi_imports() { "should_be = 5", ) .unwrap(); - assert_eq!(prog.eval().map(Term::from), Ok(Term::Num(5.))); + assert_eq!( + prog.eval().map(RichTerm::without_pos), + Ok(mk_term::integer(5)) + ); } #[test] diff --git a/tests/integration/query.rs b/tests/integration/query.rs index fdfb294d5e..2da303969b 100644 --- a/tests/integration/query.rs +++ b/tests/integration/query.rs @@ -1,6 +1,7 @@ use nickel_lang::term::{ + make as mk_term, record::{Field, FieldMetadata}, - SharedTerm, Term, TypeAnnotation, + TypeAnnotation, }; use nickel_lang_utilities::TestProgram; @@ -14,7 +15,7 @@ pub fn test_query_metadata_basic() { let result = program.query(Some(String::from("val"))).unwrap(); assert_eq!(result.metadata.doc, Some(String::from("Test basic"))); - assert_eq!(result.value.unwrap().term, SharedTerm::new(Term::Num(2.0))); + assert_eq!(result.value.unwrap().without_pos(), mk_term::integer(2)); } #[test]