From a8a3a9809b69a0b1c88dc69c00a4afaa6172283b Mon Sep 17 00:00:00 2001 From: Anand Krishnamoorthi <35780660+anakrish@users.noreply.github.com> Date: Mon, 1 Dec 2025 13:25:02 -0600 Subject: [PATCH] feat!: Use num-bigint for large numbers (#500) - Supply chain: Use the popular num-bigint crate for handling large integers - Optimization: Handle f64, i64, u64 directly. These will be the most common instances of a number. OPA number semantics isn't clear. https://github.com/open-policy-agent/opa/issues/6281 As part of this change, we update the following failing tests: - A local test that relies on what 15.3/3 evaluates to. With our current change, we round in a different direction than what OPA does, but consistent with Rust. We produce 5.1000000000000005 where as the OPA test expects 5.1. There is no clear definition in Rego of what the right answer is. Moreover, policies should not rely on exact floating point value comparison. Therefore this deviations is justified. The test is patched to pass. - Another local vm test that exercised 1.1 + 2.2 - Another local vm test that exercises 5.5 - 2.2 - An OPA test that expects that a large integer number say 10e308 is printed in exponent notation. num-bigint does not print using scientific notation and instead prints all the digits. The benefit of preserving this compatibility is not clear. We skip this test. - Doc tests that exercised handling floating point numbers with more than 15 (what f64 supports) digits of precision. There is no usecase for this scenario. The tests are updated to reflect the behavior. Signed-off-by: Anand Krishnamoorthi --- Cargo.lock | 51 +- Cargo.toml | 5 +- bindings/ffi/Cargo.lock | 43 +- bindings/java/Cargo.lock | 33 +- bindings/python/Cargo.lock | 33 +- bindings/ruby/Cargo.lock | 43 +- bindings/wasm/Cargo.lock | 33 +- src/builtins/units.rs | 122 +- src/number.rs | 1034 ++++++++++++----- src/value.rs | 45 +- .../cases/builtins/numbers/div.yaml | 2 +- tests/opa.rs | 5 + .../rvm/vm/suites/arithmetic_operations.yaml | 2 +- .../interpreter_operator_compatibility.yaml | 2 +- 14 files changed, 981 insertions(+), 472 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index da796a1..10e3fa2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -175,9 +175,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.46" +version = "1.2.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" +checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" dependencies = [ "find-msvc-tools", "shlex", @@ -270,7 +270,7 @@ dependencies = [ "heck", "proc-macro2 1.0.103", "quote 1.0.42", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -383,7 +383,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2 1.0.103", "quote 1.0.42", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1131,7 +1131,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2 1.0.103", "quote 1.0.42", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1197,11 +1197,12 @@ dependencies = [ "lazy_static", "mimalloc", "msvc_spectre_libs", + "num-bigint", + "num-traits", "num_cpus", "prettydiff", "rand", "regex", - "scientific", "semver", "serde", "serde_json", @@ -1234,12 +1235,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "scientific" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38a4b339a8de779ecb098a772ecbba2ace74e23ed959a5b4f30631d8bf1799a8" - [[package]] name = "scopeguard" version = "1.2.0" @@ -1279,7 +1274,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2 1.0.103", "quote 1.0.42", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1357,9 +1352,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.110" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2 1.0.103", "quote 1.0.42", @@ -1374,7 +1369,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2 1.0.103", "quote 1.0.42", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1406,7 +1401,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2 1.0.103", "quote 1.0.42", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1587,7 +1582,7 @@ dependencies = [ "bumpalo", "proc-macro2 1.0.103", "quote 1.0.42", - "syn 2.0.110", + "syn 2.0.111", "wasm-bindgen-shared", ] @@ -1640,7 +1635,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2 1.0.103", "quote 1.0.42", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1651,7 +1646,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2 1.0.103", "quote 1.0.42", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1738,28 +1733,28 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2 1.0.103", "quote 1.0.42", - "syn 2.0.110", + "syn 2.0.111", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "43fa6694ed34d6e57407afbccdeecfa268c470a7d2a5b0cf49ce9fcc345afb90" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26" dependencies = [ "proc-macro2 1.0.103", "quote 1.0.42", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1779,7 +1774,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2 1.0.103", "quote 1.0.42", - "syn 2.0.110", + "syn 2.0.111", "synstructure", ] @@ -1813,5 +1808,5 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2 1.0.103", "quote 1.0.42", - "syn 2.0.110", + "syn 2.0.111", ] diff --git a/Cargo.toml b/Cargo.toml index 7458253..611f7fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ doctest = false [features] default = ["full-opa", "arc", "rvm"] -arc = ["scientific/arc"] +arc = [] ast = [] azure_policy = ["dep:jsonschema", "arc", "dashmap"] azure-rbac = [] @@ -101,7 +101,8 @@ lazy_static = { version = "1.4.0", default-features = false } thiserror = { version = "2.0", default-features = false } data-encoding = { version = "2.8.0", optional = true, default-features=false, features = ["alloc"] } -scientific = { version = "0.5.3", default-features = false } +num-bigint = { version = "0.4", default-features = false } +num-traits = { version = "0.2", default-features = false } globset = { version = "0.4.16", features = ["simd-accel"], default-features = false, optional = true } regex = {version = "1.11.1", optional = true, default-features = false } diff --git a/bindings/ffi/Cargo.lock b/bindings/ffi/Cargo.lock index 8cdd0ba..64604e1 100644 --- a/bindings/ffi/Cargo.lock +++ b/bindings/ffi/Cargo.lock @@ -172,9 +172,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.46" +version = "1.2.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" +checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" dependencies = [ "find-msvc-tools", "shlex", @@ -211,18 +211,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.52" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa8120877db0e5c011242f96806ce3c94e0737ab8108532a76a3300a01db2ab8" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.52" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02576b399397b659c26064fbc92a75fede9d18ffd5f80ca1cd74ddab167016e1" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ "anstream", "anstyle", @@ -395,9 +395,9 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" @@ -533,12 +533,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.16.1", ] [[package]] @@ -965,9 +965,10 @@ dependencies = [ "lazy_static", "mimalloc", "msvc_spectre_libs", + "num-bigint", + "num-traits", "rand", "regex", - "scientific", "semver", "serde", "serde_json", @@ -1013,12 +1014,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" -[[package]] -name = "scientific" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38a4b339a8de779ecb098a772ecbba2ace74e23ed959a5b4f30631d8bf1799a8" - [[package]] name = "scopeguard" version = "1.2.0" @@ -1128,9 +1123,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.110" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -1471,18 +1466,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "43fa6694ed34d6e57407afbccdeecfa268c470a7d2a5b0cf49ce9fcc345afb90" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26" dependencies = [ "proc-macro2", "quote", diff --git a/bindings/java/Cargo.lock b/bindings/java/Cargo.lock index 719cc58..a7804ef 100644 --- a/bindings/java/Cargo.lock +++ b/bindings/java/Cargo.lock @@ -109,9 +109,9 @@ checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "cc" -version = "1.2.46" +version = "1.2.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" +checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" dependencies = [ "find-msvc-tools", "shlex", @@ -273,9 +273,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "iana-time-zone" @@ -405,9 +405,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", "hashbrown", @@ -840,9 +840,10 @@ dependencies = [ "lazy_static", "mimalloc", "msvc_spectre_libs", + "num-bigint", + "num-traits", "rand", "regex", - "scientific", "semver", "serde", "serde_json", @@ -883,12 +884,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "scientific" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38a4b339a8de779ecb098a772ecbba2ace74e23ed959a5b4f30631d8bf1799a8" - [[package]] name = "scopeguard" version = "1.2.0" @@ -983,9 +978,9 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "syn" -version = "2.0.110" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -1362,18 +1357,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "43fa6694ed34d6e57407afbccdeecfa268c470a7d2a5b0cf49ce9fcc345afb90" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26" dependencies = [ "proc-macro2", "quote", diff --git a/bindings/python/Cargo.lock b/bindings/python/Cargo.lock index bd3e62a..59bec80 100644 --- a/bindings/python/Cargo.lock +++ b/bindings/python/Cargo.lock @@ -103,9 +103,9 @@ checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" [[package]] name = "cc" -version = "1.2.46" +version = "1.2.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" +checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" dependencies = [ "find-msvc-tools", "shlex", @@ -251,9 +251,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" @@ -389,9 +389,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", "hashbrown", @@ -899,9 +899,10 @@ dependencies = [ "lazy_static", "mimalloc", "msvc_spectre_libs", + "num-bigint", + "num-traits", "rand", "regex", - "scientific", "semver", "serde", "serde_json", @@ -934,12 +935,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" -[[package]] -name = "scientific" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38a4b339a8de779ecb098a772ecbba2ace74e23ed959a5b4f30631d8bf1799a8" - [[package]] name = "scopeguard" version = "1.2.0" @@ -1034,9 +1029,9 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "syn" -version = "2.0.110" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -1311,18 +1306,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "43fa6694ed34d6e57407afbccdeecfa268c470a7d2a5b0cf49ce9fcc345afb90" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26" dependencies = [ "proc-macro2", "quote", diff --git a/bindings/ruby/Cargo.lock b/bindings/ruby/Cargo.lock index 94d9090..bd63a51 100644 --- a/bindings/ruby/Cargo.lock +++ b/bindings/ruby/Cargo.lock @@ -123,9 +123,9 @@ checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" [[package]] name = "cc" -version = "1.2.46" +version = "1.2.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" +checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" dependencies = [ "find-msvc-tools", "shlex", @@ -303,9 +303,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "iana-time-zone" @@ -435,9 +435,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", "hashbrown", @@ -942,13 +942,15 @@ dependencies = [ "lazy_static", "mimalloc", "msvc_spectre_libs", + "num-bigint", + "num-traits", "rand", "regex", - "scientific", "semver", "serde", "serde_json", "serde_yaml", + "smol_str", "thiserror", "url", "uuid", @@ -982,12 +984,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" -[[package]] -name = "scientific" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38a4b339a8de779ecb098a772ecbba2ace74e23ed959a5b4f30631d8bf1799a8" - [[package]] name = "scopeguard" version = "1.2.0" @@ -1097,6 +1093,15 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -1105,9 +1110,9 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "syn" -version = "2.0.110" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -1376,18 +1381,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "43fa6694ed34d6e57407afbccdeecfa268c470a7d2a5b0cf49ce9fcc345afb90" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26" dependencies = [ "proc-macro2", "quote", diff --git a/bindings/wasm/Cargo.lock b/bindings/wasm/Cargo.lock index 7c4825a..28d8ca8 100644 --- a/bindings/wasm/Cargo.lock +++ b/bindings/wasm/Cargo.lock @@ -103,9 +103,9 @@ checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" [[package]] name = "cc" -version = "1.2.46" +version = "1.2.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" +checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" dependencies = [ "find-msvc-tools", "shlex", @@ -266,9 +266,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "iana-time-zone" @@ -398,9 +398,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", "hashbrown", @@ -821,9 +821,10 @@ dependencies = [ "lazy_static", "mimalloc", "msvc_spectre_libs", + "num-bigint", + "num-traits", "rand", "regex", - "scientific", "semver", "serde", "serde_json", @@ -867,12 +868,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "scientific" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38a4b339a8de779ecb098a772ecbba2ace74e23ed959a5b4f30631d8bf1799a8" - [[package]] name = "scopeguard" version = "1.2.0" @@ -967,9 +962,9 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "syn" -version = "2.0.110" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -1313,18 +1308,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "43fa6694ed34d6e57407afbccdeecfa268c470a7d2a5b0cf49ce9fcc345afb90" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26" dependencies = [ "proc-macro2", "quote", diff --git a/src/builtins/units.rs b/src/builtins/units.rs index 55bbb18..af446c2 100644 --- a/src/builtins/units.rs +++ b/src/builtins/units.rs @@ -1,6 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use alloc::borrow::Cow; +use core::str::FromStr; + use crate::ast::{Expr, Ref}; use crate::builtins; use crate::builtins::utils::{ensure_args_count, ensure_string}; @@ -88,25 +91,21 @@ fn parse(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> Re _ => (string, ""), }; - // Propagating the underlying error is not useful here. - let v: Value = if number_part.starts_with('.') { - serde_json::from_str(format!("0{number_part}").as_str()) - } else { - serde_json::from_str(number_part) - } - .map_err(|_| params[0].span().error("could not parse number"))?; - - let mut n = match v { - Value::Number(n) => n.clone(), - _ => bail!(span.error("could not parse number")), - }; + // Canonicalize quirky literals (e.g. ".5", "-.1") so downstream parsing always sees + // a stable representation that includes an explicit leading digit. + let canonical_part = canonicalize_number_part(number_part); if let Some(e) = ten_exp(suffix) { - n.mul_assign(&Number::ten_pow(e)?)?; - Ok(Value::from(n)) + let combined = combine_decimal_exponent(canonical_part.as_ref(), e) + .ok_or_else(|| params[0].span().error("could not parse number"))?; + let number = Number::from_str(&combined) + .map_err(|_| params[0].span().error("could not parse number"))?; + Ok(Value::from(number)) } else if let Some(e) = two_exp(suffix) { - n.mul_assign(&Number::two_pow(e)?)?; - Ok(Value::from(n)) + let mut number = Number::from_str(canonical_part.as_ref()) + .map_err(|_| params[0].span().error("could not parse number"))?; + number.mul_assign(&Number::two_pow(e)?)?; + Ok(Value::from(number)) } else { Ok(Value::Undefined) } @@ -166,28 +165,83 @@ fn parse_bytes(span: &Span, params: &[Ref], args: &[Value], strict: bool) _ => (string, ""), }; - let v: Value = match if number_part.starts_with('.') { - serde_json::from_str(format!("0{number_part}").as_str()) - } else { - serde_json::from_str(number_part) - } { - Ok(v) => v, - Err(_) if strict => bail!(span.error("could not parse number")), - _ => return Ok(Value::Undefined), - }; - - let mut n = match v { - Value::Number(n) => n.clone(), - _ => bail!(span.error("could not parse number")), - }; + // Canonicalize quirky literals (e.g. ".5", "-.1") so downstream parsing always sees + // a stable representation that includes an explicit leading digit. + let canonical_part = canonicalize_number_part(number_part); if let Some(e) = twob_exp(suffix) { - n.mul_assign(&Number::two_pow(e)?)?; - Ok(Value::from(n.round())) + let mut number = match Number::from_str(canonical_part.as_ref()) { + Ok(n) => n, + Err(_) if strict => bail!(span.error("could not parse number")), + Err(_) => return Ok(Value::Undefined), + }; + number.mul_assign(&Number::two_pow(e)?)?; + Ok(Value::from(number.round())) } else if let Some(e) = tenb_exp(suffix) { - n.mul_assign(&Number::ten_pow(e)?)?; - Ok(Value::from(n.round())) + let combined = match combine_decimal_exponent(canonical_part.as_ref(), e) { + Some(s) => s, + None if strict => bail!(span.error("could not parse number")), + None => return Ok(Value::Undefined), + }; + let number = match Number::from_str(&combined) { + Ok(n) => n, + Err(_) if strict => bail!(span.error("could not parse number")), + Err(_) => return Ok(Value::Undefined), + }; + Ok(Value::from(number.round())) } else { Ok(Value::Undefined) } } + +// Normalizes numbers that start with a decimal point by injecting the implicit leading zero. +// This means inputs like ".5" and "-.1" become "0.5" / "-0.1", which Number::from_str accepts. +fn canonicalize_number_part(number_part: &str) -> Cow<'_, str> { + if let Some(rest) = number_part.strip_prefix("-.") { + Cow::Owned(format!("-0.{rest}")) + } else if let Some(rest) = number_part.strip_prefix("+.") { + Cow::Owned(format!("+0.{rest}")) + } else if let Some(rest) = number_part.strip_prefix('.') { + Cow::Owned(format!("0.{rest}")) + } else { + Cow::Borrowed(number_part) + } +} + +// Adds the unit-derived exponent (from suffix) to any exponent already present in the literal. +// Produces a canonical `mantissa eX` string or returns None when the combined exponent overflows. +fn combine_decimal_exponent(number: &str, suffix_exp: i32) -> Option { + if suffix_exp == 0 { + return Some(number.to_string()); + } + + let (mantissa, base_exp) = match split_decimal_exponent(number) { + Some(parts) => parts, + None => (number, 0), + }; + + if mantissa.is_empty() { + return None; + } + + let combined_exp = base_exp.checked_add(suffix_exp)?; + + if combined_exp == 0 { + Some(mantissa.to_string()) + } else { + Some(format!("{}e{}", mantissa, combined_exp)) + } +} + +// Splits scientific notation ("1.2e3") into mantissa/exponent so callers can adjust the exponent. +// Returns None when no exponent exists or the tail cannot be parsed into a 32-bit integer. +fn split_decimal_exponent(number: &str) -> Option<(&str, i32)> { + let idx = number.find(['e', 'E'])?; + let mantissa = &number[..idx]; + let exp_part = &number[idx + 1..]; + if exp_part.is_empty() { + return None; + } + let exponent = exp_part.parse::().ok()?; + Some((mantissa, exponent)) +} diff --git a/src/number.rs b/src/number.rs index 3c84b1d..459d062 100644 --- a/src/number.rs +++ b/src/number.rs @@ -1,70 +1,158 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use alloc::str::FromStr; -use core::cmp::{Ord, Ordering}; +use alloc::format; +use alloc::string::{String, ToString}; +use core::cmp::Ordering; use core::fmt::{Debug, Formatter}; +use core::str::FromStr; use anyhow::{anyhow, bail, Result}; +use num_bigint::BigInt as NumBigInt; +#[cfg(not(feature = "std"))] +use num_traits::float::FloatCore; +use num_traits::{One, Signed, ToPrimitive, Zero}; use serde::ser::Serializer; use serde::Serialize; use crate::*; -pub type BigInt = i128; +pub type BigInt = NumBigInt; -type BigFloat = scientific::Scientific; -const PRECISION: scientific::Precision = scientific::Precision::Digits(100); - -#[derive(Clone, Debug, PartialEq)] -pub struct BigDecimal { - d: BigFloat, -} - -impl AsRef for BigDecimal { - fn as_ref(&self) -> &BigFloat { - &self.d - } -} - -impl AsMut for BigDecimal { - fn as_mut(&mut self) -> &mut BigFloat { - &mut self.d - } -} - -impl From for BigDecimal { - fn from(value: BigFloat) -> Self { - BigDecimal { d: value } - } -} - -impl From for BigDecimal { - fn from(value: i128) -> Self { - BigDecimal { - d: Into::::into(value), - } - } -} - -impl BigDecimal { - fn is_integer(&self) -> bool { - self.d.decimals() <= 0 - } -} +const F64_SAFE_INTEGER: f64 = 9_007_199_254_740_992.0; // 2^53 #[derive(Clone)] pub enum Number { - // TODO: maybe specialize for u64, i64, f64 - Big(Rc), + UInt(u64), + Int(i64), + Float(f64), + BigInt(Rc), +} + +impl Number { + fn from_bigint_owned(value: BigInt) -> Self { + if value.is_zero() { + return Number::Int(0); + } + + if value.is_negative() { + if let Some(i) = value.to_i64() { + return Number::Int(i); + } + } else if let Some(u) = value.to_u64() { + return Number::UInt(u); + } else if let Some(i) = value.to_i64() { + return Number::Int(i); + } + + Number::BigInt(Rc::new(value)) + } + + fn from_i128(value: i128) -> Self { + if value >= 0 { + if let Ok(u) = u64::try_from(value) { + return Number::UInt(u); + } + } + + if let Ok(i) = i64::try_from(value) { + Number::Int(i) + } else { + Number::BigInt(Rc::new(BigInt::from(value))) + } + } + + fn to_bigint_owned(&self) -> Option { + match self { + Number::UInt(v) => Some(BigInt::from(*v)), + Number::Int(v) => Some(BigInt::from(*v)), + Number::BigInt(v) => Some((**v).clone()), + Number::Float(f) => Self::float_to_small_bigint(*f), + } + } + + fn float_to_small_bigint(value: f64) -> Option { + if !value.is_finite() || value.fract() != 0.0 { + return None; + } + + if value.abs() > F64_SAFE_INTEGER { + return None; + } + + if value >= 0.0 { + let u = value as u64; + if (u as f64) == value { + return Some(BigInt::from(u)); + } + } else { + let i = value as i64; + if (i as f64) == value { + return Some(BigInt::from(i)); + } + } + + None + } + + fn to_bigint_rc(&self) -> Option> { + match self { + Number::BigInt(v) => Some(v.clone()), + _ => self.to_bigint_owned().map(Rc::new), + } + } + + fn to_f64_lossy(&self) -> f64 { + match self { + Number::UInt(v) => *v as f64, + Number::Int(v) => *v as f64, + Number::Float(v) => *v, + Number::BigInt(v) => { + if let Some(f) = v.to_f64() { + f + } else if v.is_negative() { + f64::NEG_INFINITY + } else { + f64::INFINITY + } + } + } + } + + fn is_zero(&self) -> bool { + match self { + Number::UInt(0) | Number::Int(0) => true, + Number::Float(f) => *f == 0.0, + Number::BigInt(v) => v.is_zero(), + _ => false, + } + } + + fn ints_to_bigint(a: &Number, b: &Number) -> (BigInt, BigInt) { + (a.to_bigint_owned().unwrap(), b.to_bigint_owned().unwrap()) + } + + fn normalize_float(value: f64) -> Number { + if let Some(int) = Self::float_to_small_bigint(value) { + return Self::from_bigint_owned(int); + } + Number::Float(value) + } + + fn as_u32(&self) -> Option { + match self { + Number::UInt(v) if *v <= u32::MAX as u64 => Some(*v as u32), + Number::Int(v) if *v >= 0 && *v <= u32::MAX as i64 => Some(*v as u32), + Number::BigInt(v) => v.to_u32(), + _ => None, + } + } } impl Debug for Number { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), core::fmt::Error> { - match self { - Number::Big(b) => b.d.fmt(f), - } + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + f.write_str(&self.format_decimal()) } } @@ -73,117 +161,56 @@ impl Serialize for Number { where S: Serializer, { - match self { - Big(_) => { - let s = self.format_decimal(); - let v = serde_json::Number::from_str(&s) - .map_err(|_| serde::ser::Error::custom("could not serialize big number"))?; - v.serialize(serializer) - } - } + let s = self.format_decimal(); + let v = serde_json::Number::from_str(&s) + .map_err(|_| serde::ser::Error::custom("could not serialize number"))?; + v.serialize(serializer) } } -use Number::*; - -impl From for Number { - fn from(n: BigFloat) -> Self { - Self::Big(BigDecimal::from(n).into()) +impl From for Number { + fn from(value: BigInt) -> Self { + Number::from_bigint_owned(value) } } impl From for Number { - fn from(n: u64) -> Self { - BigFloat::from(n).into() + fn from(value: u64) -> Self { + Number::UInt(value) } } impl From for Number { - fn from(n: usize) -> Self { - BigFloat::from(n).into() + fn from(value: usize) -> Self { + Number::UInt(value as u64) } } impl From for Number { - fn from(n: u128) -> Self { - BigFloat::from(n).into() - } -} - -impl From for Number { - fn from(n: i128) -> Self { - BigFloat::from(n).into() + fn from(value: u128) -> Self { + if let Ok(n) = u64::try_from(value) { + Number::UInt(n) + } else { + Number::from_bigint_owned(BigInt::from(value)) + } } } impl From for Number { - fn from(n: i64) -> Self { - BigFloat::from(n).into() + fn from(value: i64) -> Self { + Number::Int(value) + } +} + +impl From for Number { + fn from(value: i128) -> Self { + Number::from_i128(value) } } impl From for Number { - fn from(n: f64) -> Self { - // Reading from float is not precise. Therefore, serialize to string and read. - match Self::from_str(&format!("{n}")) { - Ok(v) => v, - _ => BigFloat::ZERO.into(), - } - } -} - -impl Number { - pub fn as_u128(&self) -> Option { - match self { - Big(b) if b.is_integer() => u128::try_from(&b.d).ok(), - _ => None, - } - } - - pub fn as_i128(&self) -> Option { - match self { - Big(b) if b.is_integer() => i128::try_from(&b.d).ok(), - _ => None, - } - } - - pub fn as_u64(&self) -> Option { - match self { - Big(b) if b.is_integer() => u64::try_from(&b.d).ok(), - _ => None, - } - } - - pub fn as_i64(&self) -> Option { - match self { - Big(b) if b.is_integer() => i64::try_from(&b.d).ok(), - _ => None, - } - } - - pub fn as_f64(&self) -> Option { - match self { - Big(b) => { - let f = f64::from(&b.d); - match BigFloat::try_from(f) { - Ok(bf) if bf == b.d => Some(f), - _ => None, - } - } - } - } - - pub fn as_big(&self) -> Option> { - Some(match self { - Big(b) => b.clone(), - }) - } - - pub fn to_big(&self) -> Result> { - match self.as_big() { - Some(b) => Ok(b), - _ => bail!("Number::to_big failed"), - } + fn from(value: f64) -> Self { + Number::Float(value) } } @@ -194,28 +221,87 @@ impl FromStr for Number { type Err = ParseNumberError; fn from_str(s: &str) -> Result { - if let Ok(v) = BigFloat::from_str(s) { - return Ok(v.into()); + let trimmed = s.trim(); + if trimmed.is_empty() { + return Err(ParseNumberError); } - Ok(f64::from_str(s).map_err(|_| ParseNumberError)?.into()) + + let canonical = trimmed.replace('_', ""); + if canonical.is_empty() { + return Err(ParseNumberError); + } + + let normalized = if let Some(rest) = canonical.strip_prefix("-.") { + format!("-0.{rest}") + } else if let Some(rest) = canonical.strip_prefix("+.") { + format!("+0.{rest}") + } else if let Some(rest) = canonical.strip_prefix('.') { + format!("0.{rest}") + } else { + canonical + }; + + let normalized_ref = normalized.as_str(); + let is_integer_literal = !normalized_ref.contains('.') + && !normalized_ref.contains('e') + && !normalized_ref.contains('E'); + + if is_integer_literal { + let (sign, digits) = if let Some(rest) = normalized_ref.strip_prefix('-') { + (-1, rest) + } else if let Some(rest) = normalized_ref.strip_prefix('+') { + (1, rest) + } else { + (1, normalized_ref) + }; + + if !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit()) { + if let Some(mut value) = BigInt::parse_bytes(digits.as_bytes(), 10) { + if sign < 0 { + value = -value; + } + return Ok(Number::from_bigint_owned(value)); + } + } + } + + if let Some(value) = parse_scientific_bigint(normalized_ref) { + return Ok(Number::from_bigint_owned(value)); + } + + normalized_ref + .parse::() + .map(Number::Float) + .map_err(|_| ParseNumberError) + } +} + +impl PartialEq for Number { + fn eq(&self, other: &Self) -> bool { + if let (Some(a), Some(b)) = (self.to_bigint_owned(), other.to_bigint_owned()) { + return a == b; + } + + let a = self.to_f64_lossy(); + let b = other.to_f64_lossy(); + if a.is_nan() || b.is_nan() { + return false; + } + a == b } } impl Eq for Number {} -impl PartialEq for Number { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Big(a), Big(b)) => a.d == b.d, - } - } -} - impl Ord for Number { fn cmp(&self, other: &Self) -> Ordering { - match (self, other) { - (Big(a), Big(b)) => a.d.cmp(&b.d), + if let (Some(a), Some(b)) = (self.to_bigint_owned(), other.to_bigint_owned()) { + return a.cmp(&b); } + + self.to_f64_lossy() + .partial_cmp(&other.to_f64_lossy()) + .unwrap_or(Ordering::Equal) } } @@ -226,14 +312,149 @@ impl PartialOrd for Number { } impl Number { + pub fn as_u128(&self) -> Option { + match self { + Number::UInt(v) => Some(*v as u128), + Number::Int(v) if *v >= 0 => Some(*v as u128), + Number::BigInt(v) => v.to_u128(), + Number::Float(f) => { + if f.is_finite() && *f >= 0.0 && f.fract() == 0.0 { + let candidate = *f as u128; + if (candidate as f64) == *f { + return Some(candidate); + } + } + None + } + _ => None, + } + } + + pub fn as_i128(&self) -> Option { + match self { + Number::UInt(v) => Some(*v as i128), + Number::Int(v) => Some(*v as i128), + Number::BigInt(v) => v.to_i128(), + Number::Float(f) => { + if f.is_finite() && f.fract() == 0.0 { + let candidate = *f as i128; + if (candidate as f64) == *f { + return Some(candidate); + } + } + None + } + } + } + + pub fn as_u64(&self) -> Option { + match self { + Number::UInt(v) => Some(*v), + Number::Int(v) if *v >= 0 => Some(*v as u64), + Number::BigInt(v) => v.to_u64(), + Number::Float(f) => { + if f.is_finite() && *f >= 0.0 && f.fract() == 0.0 && *f <= u64::MAX as f64 { + let candidate = *f as u64; + if (candidate as f64) == *f { + return Some(candidate); + } + } + None + } + _ => None, + } + } + + pub fn as_i64(&self) -> Option { + match self { + Number::UInt(v) if *v <= i64::MAX as u64 => Some(*v as i64), + Number::Int(v) => Some(*v), + Number::BigInt(v) => v.to_i64(), + Number::Float(f) => { + if f.is_finite() + && f.fract() == 0.0 + && *f >= i64::MIN as f64 + && *f <= i64::MAX as f64 + { + let candidate = *f as i64; + if (candidate as f64) == *f { + return Some(candidate); + } + } + None + } + _ => None, + } + } + + pub fn as_f64(&self) -> Option { + match self { + Number::Float(f) if f.is_finite() => Some(*f), + Number::UInt(v) if *v <= F64_SAFE_INTEGER as u64 => Some(*v as f64), + Number::Int(v) if (*v as i128).abs() <= F64_SAFE_INTEGER as i128 => Some(*v as f64), + Number::BigInt(v) => { + if v.bits() <= 53 { + v.to_f64() + } else { + None + } + } + _ => None, + } + } + + pub fn as_big(&self) -> Option> { + self.to_bigint_rc() + } + + pub fn to_big(&self) -> Result> { + self.as_big() + .ok_or_else(|| anyhow!("Number::to_big failed")) + } + pub fn add_assign(&mut self, rhs: &Self) -> Result<()> { *self = self.add(rhs)?; Ok(()) } pub fn add(&self, rhs: &Self) -> Result { + if matches!(self, Number::Float(_)) || matches!(rhs, Number::Float(_)) { + return Ok(Number::normalize_float( + self.to_f64_lossy() + rhs.to_f64_lossy(), + )); + } + match (self, rhs) { - (Big(a), Big(b)) => Ok(Big(BigDecimal::from(&a.d + &b.d).into())), + (Number::UInt(a), Number::UInt(b)) => { + if let Some(sum) = a.checked_add(*b) { + Ok(Number::UInt(sum)) + } else { + Ok(Number::from_bigint_owned( + BigInt::from(*a) + BigInt::from(*b), + )) + } + } + (Number::Int(a), Number::Int(b)) => { + if let Some(sum) = a.checked_add(*b) { + Ok(Number::Int(sum)) + } else { + Ok(Number::from_bigint_owned( + BigInt::from(*a) + BigInt::from(*b), + )) + } + } + (Number::Int(a), Number::UInt(b)) | (Number::UInt(b), Number::Int(a)) => { + Ok(Number::from_i128(*a as i128 + *b as i128)) + } + (Number::BigInt(a), Number::BigInt(b)) => { + Ok(Number::from_bigint_owned((**a).clone() + (**b).clone())) + } + (Number::BigInt(a), other) | (other, Number::BigInt(a)) => { + let mut sum = (**a).clone(); + sum += other.to_bigint_owned().unwrap(); + Ok(Number::from_bigint_owned(sum)) + } + _ => unreachable!(), } } @@ -243,8 +464,45 @@ impl Number { } pub fn sub(&self, rhs: &Self) -> Result { + if matches!(self, Number::Float(_)) || matches!(rhs, Number::Float(_)) { + return Ok(Number::normalize_float( + self.to_f64_lossy() - rhs.to_f64_lossy(), + )); + } + match (self, rhs) { - (Big(a), Big(b)) => Ok(Big(BigDecimal::from(&a.d - &b.d).into())), + (Number::UInt(a), Number::UInt(b)) => { + if a >= b { + Ok(Number::UInt(a - b)) + } else { + Ok(Number::from_i128(*a as i128 - *b as i128)) + } + } + (Number::Int(a), Number::Int(b)) => { + if let Some(diff) = a.checked_sub(*b) { + Ok(Number::Int(diff)) + } else { + Ok(Number::from_bigint_owned( + BigInt::from(*a) - BigInt::from(*b), + )) + } + } + (Number::Int(a), Number::UInt(b)) => Ok(Number::from_i128(*a as i128 - *b as i128)), + (Number::UInt(a), Number::Int(b)) => Ok(Number::from_i128(*a as i128 - *b as i128)), + (Number::BigInt(a), Number::BigInt(b)) => { + Ok(Number::from_bigint_owned((**a).clone() - (**b).clone())) + } + (Number::BigInt(a), other) => { + let mut diff = (**a).clone(); + diff -= other.to_bigint_owned().unwrap(); + Ok(Number::from_bigint_owned(diff)) + } + (other, Number::BigInt(b)) => { + let mut diff = other.to_bigint_owned().unwrap(); + diff -= (**b).clone(); + Ok(Number::from_bigint_owned(diff)) + } + _ => unreachable!(), } } @@ -254,250 +512,464 @@ impl Number { } pub fn mul(&self, rhs: &Self) -> Result { + if matches!(self, Number::Float(_)) || matches!(rhs, Number::Float(_)) { + return Ok(Number::normalize_float( + self.to_f64_lossy() * rhs.to_f64_lossy(), + )); + } + match (self, rhs) { - (Big(a), Big(b)) => Ok(Big(BigDecimal::from(&a.d * &b.d).into())), + (Number::UInt(a), Number::UInt(b)) => { + let product = (*a as u128) * (*b as u128); + if let Ok(v) = u64::try_from(product) { + Ok(Number::UInt(v)) + } else { + Ok(Number::from_bigint_owned(BigInt::from(product))) + } + } + (Number::Int(a), Number::Int(b)) => { + if let Some(prod) = a.checked_mul(*b) { + Ok(Number::Int(prod)) + } else { + Ok(Number::from_bigint_owned( + BigInt::from(*a) * BigInt::from(*b), + )) + } + } + (Number::Int(a), Number::UInt(b)) | (Number::UInt(b), Number::Int(a)) => { + let lhs = *a as i128; + let rhs_val = *b as i128; + if let Some(prod) = lhs.checked_mul(rhs_val) { + Ok(Number::from_i128(prod)) + } else { + Ok(Number::from_bigint_owned( + BigInt::from(*a) * BigInt::from(*b), + )) + } + } + (Number::BigInt(a), Number::BigInt(b)) => { + Ok(Number::from_bigint_owned((**a).clone() * (**b).clone())) + } + (Number::BigInt(a), other) | (other, Number::BigInt(a)) => { + let product = (**a).clone() * other.to_bigint_owned().unwrap(); + Ok(Number::from_bigint_owned(product)) + } + _ => unreachable!(), } } pub fn divide(self, rhs: &Self) -> Result { - match (self, rhs) { - (Big(a), Big(b)) => { - let c = - a.d.div_truncate(&b.d, PRECISION) - .map_err(|e| anyhow!("{e}"))?; - Ok(Big(BigDecimal::from(c).into())) + if rhs.is_zero() { + bail!("division by zero"); + } + + if matches!(self, Number::Float(_)) || matches!(rhs, Number::Float(_)) { + return Ok(Number::Float(self.to_f64_lossy() / rhs.to_f64_lossy())); + } + + match (&self, rhs) { + (Number::UInt(a), Number::UInt(b)) => { + if *a % *b == 0 { + Ok(Number::UInt(*a / *b)) + } else { + Ok(Number::Float(self.to_f64_lossy() / rhs.to_f64_lossy())) + } } + (Number::Int(a), Number::Int(b)) => { + if *a % *b == 0 { + if let Some(q) = a.checked_div(*b) { + Ok(Number::Int(q)) + } else { + let quotient = BigInt::from(*a) / BigInt::from(*b); + Ok(Number::from_bigint_owned(quotient)) + } + } else { + Ok(Number::Float(self.to_f64_lossy() / rhs.to_f64_lossy())) + } + } + (Number::Int(a), Number::UInt(b)) => { + let lhs = *a as i128; + let rhs_i = *b as i128; + if lhs % rhs_i == 0 { + Ok(Number::from_i128(lhs / rhs_i)) + } else { + Ok(Number::Float(self.to_f64_lossy() / rhs.to_f64_lossy())) + } + } + (Number::UInt(a), Number::Int(b)) => { + let lhs = *a as i128; + let rhs_i = *b as i128; + if lhs % rhs_i == 0 { + Ok(Number::from_i128(lhs / rhs_i)) + } else { + Ok(Number::Float(self.to_f64_lossy() / rhs.to_f64_lossy())) + } + } + (Number::BigInt(a), Number::BigInt(b)) => { + let remainder = (&**a) % (&**b); + if remainder.is_zero() { + let quotient = (&**a) / (&**b); + Ok(Number::from_bigint_owned(quotient)) + } else { + Ok(Number::Float(self.to_f64_lossy() / rhs.to_f64_lossy())) + } + } + (Number::BigInt(a), _) => { + if let Some(b_big) = rhs.to_bigint_owned() { + let remainder = (&**a) % &b_big; + if remainder.is_zero() { + let quotient = (&**a) / &b_big; + Ok(Number::from_bigint_owned(quotient)) + } else { + Ok(Number::Float(self.to_f64_lossy() / rhs.to_f64_lossy())) + } + } else { + Ok(Number::Float(self.to_f64_lossy() / rhs.to_f64_lossy())) + } + } + (_, Number::BigInt(b)) => { + if let Some(a_big) = self.to_bigint_owned() { + let remainder = (&a_big) % (&**b); + if remainder.is_zero() { + let quotient = (&a_big) / (&**b); + Ok(Number::from_bigint_owned(quotient)) + } else { + Ok(Number::Float(self.to_f64_lossy() / rhs.to_f64_lossy())) + } + } else { + Ok(Number::Float(self.to_f64_lossy() / rhs.to_f64_lossy())) + } + } + _ => Ok(Number::Float(self.to_f64_lossy() / rhs.to_f64_lossy())), } } pub fn modulo(self, rhs: &Self) -> Result { - match (self, rhs) { - (Big(a), Big(b)) => { - let (_, c) = a.d.div_rem(&b.d).map_err(|e| anyhow!("{e}"))?; - Ok(Big(BigDecimal::from(c).into())) - } + if rhs.is_zero() { + bail!("modulo by zero"); } + + if !self.is_integer() || !rhs.is_integer() { + bail!("modulo on floating-point number"); + } + + let (a, b) = Number::ints_to_bigint(&self, rhs); + let rem = a % &b; + Ok(Number::from_bigint_owned(rem)) } pub fn is_integer(&self) -> bool { match self { - Big(b) => b.is_integer(), + Number::Float(f) => f.is_finite() && f.fract() == 0.0, + _ => true, } } pub fn is_positive(&self) -> bool { match self { - Big(b) => b.d.is_sign_positive(), + Number::UInt(_) => true, + Number::Int(v) => *v >= 0, + Number::BigInt(v) => !v.is_negative(), + Number::Float(f) => f.is_sign_positive(), } } fn ensure_integers(a: &Number, b: &Number) -> Option<(BigInt, BigInt)> { - match (a, b) { - (Big(a), Big(b)) if a.is_integer() && b.is_integer() => { - match (BigInt::try_from(&a.d), BigInt::try_from(&b.d)) { - (Ok(a), Ok(b)) => Some((a, b)), - _ => None, - } - } - _ => None, + if a.is_integer() && b.is_integer() { + Some((a.to_bigint_owned()?, b.to_bigint_owned()?)) + } else { + None } } fn ensure_integer(&self) -> Option { - match self { - Big(a) if a.is_integer() => BigInt::try_from(&a.d).ok(), - _ => None, + if self.is_integer() { + self.to_bigint_owned() + } else { + None } } pub fn and(&self, rhs: &Self) -> Option { - match Self::ensure_integers(self, rhs) { - Some((a, b)) => Some((a & b).into()), - _ => None, - } + let (a, b) = Self::ensure_integers(self, rhs)?; + Some(Number::from_bigint_owned(a & b)) } pub fn or(&self, rhs: &Self) -> Option { - match Self::ensure_integers(self, rhs) { - Some((a, b)) => Some((a | b).into()), - _ => None, - } + let (a, b) = Self::ensure_integers(self, rhs)?; + Some(Number::from_bigint_owned(a | b)) } pub fn xor(&self, rhs: &Self) -> Option { - match Self::ensure_integers(self, rhs) { - Some((a, b)) => Some((a ^ b).into()), - _ => None, - } + let (a, b) = Self::ensure_integers(self, rhs)?; + Some(Number::from_bigint_owned(a ^ b)) } pub fn lsh(&self, rhs: &Self) -> Option { - match Self::ensure_integers(self, rhs) { - Some((a, b)) => a.checked_shl(b as u32).map(|v| v.into()), - _ => None, - } + let shift = rhs.as_u32()? as usize; + let mut value = self.ensure_integer()?; + value <<= shift; + Some(Number::from_bigint_owned(value)) } pub fn rsh(&self, rhs: &Self) -> Option { - match Self::ensure_integers(self, rhs) { - Some((a, b)) => a.checked_shr(b as u32).map(|v| v.into()), - _ => None, - } + let shift = rhs.as_u32()? as usize; + let mut value = self.ensure_integer()?; + value >>= shift; + Some(Number::from_bigint_owned(value)) } pub fn neg(&self) -> Option { - self.ensure_integer().map(|a| (!a).into()) + let mut value = self.ensure_integer()?; + value = !value; + Some(Number::from_bigint_owned(value)) } pub fn abs(&self) -> Number { match self { - Big(b) => b.d.clone().abs().into(), + Number::UInt(_) => self.clone(), + Number::Int(v) => { + if let Some(abs) = v.checked_abs() { + Number::Int(abs) + } else { + Number::from_bigint_owned(BigInt::from(*v).abs()) + } + } + Number::BigInt(v) => Number::from_bigint_owned((**v).clone().abs()), + Number::Float(f) => Number::Float(f.abs()), } } pub fn floor(&self) -> Number { match self { - Big(b) => Big(BigDecimal::from(b.d.round( - scientific::Precision::Decimals(0), - scientific::Rounding::RoundDown, - )) - .into()), + Number::Float(f) => Number::normalize_float(f.floor()), + _ => self.clone(), } } pub fn ceil(&self) -> Number { match self { - Big(b) => Big(BigDecimal::from(b.d.round( - scientific::Precision::Decimals(0), - scientific::Rounding::RoundUp, - )) - .into()), + Number::Float(f) => Number::normalize_float(f.ceil()), + _ => self.clone(), } } pub fn round(&self) -> Number { match self { - Big(b) => Big(BigDecimal::from(b.d.round( - scientific::Precision::Decimals(0), - scientific::Rounding::RoundHalfAwayFromZero, - )) - .into()), + Number::Float(f) => Number::normalize_float(f.round()), + _ => self.clone(), } } pub fn two_pow(e: i32) -> Result { if e >= 0 { - Ok(BigFloat::from(2).powi(e as usize).into()) + Ok(two_pow_positive(e as u32)) } else { - Number::from(1u64).divide(&BigFloat::from(2).powi(-e as usize).into()) + let denom = two_pow_positive((-e) as u32); + Number::from(1u64).divide(&denom) } } pub fn ten_pow(e: i32) -> Result { if e >= 0 { - Ok(BigFloat::from(10).powi(e as usize).into()) + Ok(ten_pow_positive(e as u32)) } else { - Number::from(1u64).divide(&BigFloat::from(10).powi(-e as usize).into()) + let denom = ten_pow_positive((-e) as u32); + Number::from(1u64).divide(&denom) } } pub fn format_bin(&self) -> String { self.ensure_integer() - .map(|a| format!("{a:b}")) - .unwrap_or("".to_string()) + .map(|v| v.to_str_radix(2)) + .unwrap_or_default() } pub fn format_octal(&self) -> String { self.ensure_integer() - .map(|a| format!("{a:o}")) - .unwrap_or("".to_string()) + .map(|v| v.to_str_radix(8)) + .unwrap_or_default() } pub fn format_scientific(&self) -> String { match self { - Big(b) => format!("{}", b.d), + Number::Float(f) => format!("{:e}", f), + _ => self + .ensure_integer() + .map(|v| bigint_to_scientific(&v)) + .unwrap_or_else(|| format!("{:e}", self.to_f64_lossy())), } } pub fn format_decimal(&self) -> String { - if let Some(u) = self.as_u64() { - u.to_string() - } else if let Some(i) = self.as_i64() { - i.to_string() - } else if let Some(f) = self.as_f64() { - f.to_string() - } else { - let s = match self { - Big(b) => format!("{}", b.d), - }; - - // Remove trailing e0 - if s.ends_with("e0") { - return s[..s.len() - 2].to_string(); - } - - // Avoid e notation if full mantissa is written out. - let parts: Vec<&str> = s.split('e').collect(); - match self { - Big(b) => { - if b.d.is_sign_positive() { - if parts[0].len() == b.d.exponent1() as usize + 2 { - return parts[0].replace('.', ""); - } - } else if parts[0].len() == b.d.exponent1() as usize + 3 { - return parts[0].replace('.', ""); - } + match self { + Number::UInt(v) => v.to_string(), + Number::Int(v) => v.to_string(), + Number::BigInt(v) => v.to_string(), + Number::Float(f) => { + if f.is_nan() { + "NaN".to_string() + } else { + f.to_string() } } - - s } } pub fn format_decimal_with_width(&self, d: u32) -> String { match self { - Big(b) => { - let n = Big(BigDecimal::from(b.d.round( - scientific::Precision::Decimals(d as isize), - scientific::Rounding::RoundHalfAwayFromZero, - )) - .into()); - n.format_decimal() + Number::Float(f) => { + let factor = 10f64.powi(d as i32); + let rounded = (f * factor).round() / factor; + format!("{:.*}", d as usize, rounded) } + _ => self.format_decimal(), } } pub fn format_hex(&self) -> String { self.ensure_integer() - .map(|a| format!("{a:x}")) - .unwrap_or("".to_string()) + .map(|v| v.to_str_radix(16)) + .unwrap_or_default() } pub fn format_big_hex(&self) -> String { self.ensure_integer() - .map(|a| format!("{a:X}")) - .unwrap_or("".to_string()) + .map(|v| v.to_str_radix(16).to_ascii_uppercase()) + .unwrap_or_default() } } -#[cfg(test)] -mod test { - use crate::number::*; - - #[test] - fn display_number() { - let n = Number::from(123456f64); - assert_eq!(format!("{}", n.format_decimal()), "123456"); - } - - #[test] - fn division_matches_high_precision_decimal() { - let one = Number::from(1u64); - let three = Number::from(3u64); - let div = one.divide(&three).unwrap(); - let from_str: Number = "0.3333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333" - .parse() - .unwrap(); - assert_eq!(div, from_str); +fn two_pow_positive(exp: u32) -> Number { + if exp < 64 { + Number::UInt(1u64 << exp) + } else { + let mut value = BigInt::one(); + value <<= exp as usize; + Number::from_bigint_owned(value) } } + +fn pow10_bigint(exp: u32) -> BigInt { + if exp == 0 { + return BigInt::one(); + } + + let mut result = BigInt::one(); + let mut base = BigInt::from(10u8); + let mut e = exp; + + while e > 0 { + if e & 1 == 1 { + result *= &base; + } + if e > 1 { + base = &base * &base; + } + e >>= 1; + } + + result +} + +fn ten_pow_positive(exp: u32) -> Number { + if let Some(value) = 10u64.checked_pow(exp) { + Number::UInt(value) + } else { + Number::from_bigint_owned(pow10_bigint(exp)) + } +} + +fn bigint_to_scientific(value: &BigInt) -> String { + let s = value.to_string(); + let (sign, digits) = if let Some(rest) = s.strip_prefix('-') { + ("-", rest) + } else { + ("", s.as_str()) + }; + + if digits.len() <= 1 { + return format!("{}{}e0", sign, digits); + } + + let exponent = digits.len() as i32 - 1; + format!("{}{}.{}e{}", sign, &digits[0..1], &digits[1..], exponent) +} + +fn parse_scientific_bigint(input: &str) -> Option { + let (mantissa, exponent_part) = split_scientific_parts(input)?; + let exponent = exponent_part.parse::().ok()?; + scientific_parts_to_bigint(mantissa, exponent) +} + +fn split_scientific_parts(input: &str) -> Option<(&str, &str)> { + let idx = input.find(['e', 'E'])?; + let mantissa = &input[..idx]; + let exponent = &input[idx + 1..]; + if exponent.is_empty() { + None + } else { + Some((mantissa, exponent)) + } +} + +fn scientific_parts_to_bigint(mantissa: &str, exponent: i32) -> Option { + let (sign, unsigned) = if let Some(rest) = mantissa.strip_prefix('-') { + (-1, rest) + } else if let Some(rest) = mantissa.strip_prefix('+') { + (1, rest) + } else { + (1, mantissa) + }; + + if unsigned.is_empty() { + return None; + } + + let mut digits = String::new(); + let mut fractional_len: i32 = 0; + let mut seen_dot = false; + for ch in unsigned.chars() { + match ch { + '.' => { + if seen_dot { + return None; + } + seen_dot = true; + } + '0'..='9' => { + digits.push(ch); + if seen_dot { + fractional_len += 1; + } + } + _ => return None, + } + } + + if digits.is_empty() { + return Some(BigInt::zero()); + } + + while fractional_len > 0 && digits.ends_with('0') { + digits.pop(); + fractional_len -= 1; + } + + let adjusted_exponent = exponent.checked_sub(fractional_len)?; + if adjusted_exponent < 0 { + return None; + } + + let mut value = BigInt::parse_bytes(digits.as_bytes(), 10)?; + if adjusted_exponent > 0 { + let factor = pow10_bigint(u32::try_from(adjusted_exponent).ok()?); + value *= factor; + } + + if sign < 0 { + value = -value; + } + + Some(value) +} diff --git a/src/value.rs b/src/value.rs index e654f08..ad72e50 100644 --- a/src/value.rs +++ b/src/value.rs @@ -575,35 +575,32 @@ impl From for Value { /// # use regorus::*; /// # fn main() -> anyhow::Result<()> { /// assert_eq!( - /// Value::from(3.141592653589793), + /// Value::from(3.5f64), + /// Value::from_numeric_string("3.5")?); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`Value::Number`] stores floating-point values as `f64`, so it inherits the same + /// ~15-digit precision limit. Adding additional digits to either the literal or a parsed + /// numeric string causes both to round to the same `f64` value. + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// let from_float = Value::from(3.141592653589793238462f64); + /// let from_string = Value::from_numeric_string("3.141592653589793238462")?; + /// assert_eq!(from_float, from_string); + /// + /// // All representations round to approximately 15 digits. + /// assert_eq!( + /// from_float, /// Value::from_numeric_string("3.141592653589793")?); /// # Ok(()) /// # } /// ``` /// - /// Note, f64 can store only around 15 digits of precision whereas [`Value::Number`] - /// can store arbitrary precision. Adding an extra digit to the f64 literal in the above - /// example causes loss of precision and the Value created from f64 does not match the - /// Value parsed from numeric string (which is more precise). - /// ``` - /// # use regorus::*; - /// # fn main() -> anyhow::Result<()> { - /// // The last digit is lost in f64. - /// assert_ne!( - /// Value::from(3.1415926535897932), - /// Value::from_numeric_string("3.141592653589793232")?); - /// - /// // The value, in this case is equal to parsing the json number with last digit omitted. - /// assert_ne!( - /// Value::from(3.1415926535897932), - /// Value::from_numeric_string("3.14159265358979323")?); - /// # Ok(()) - /// # } - /// ``` - /// - /// If precision is important, it is better to construct numeric values from strings instead - /// of f64 when possible. - /// See [Value::from_numeric_string] + /// If additional precision is required, keep the raw data as strings or use an external + /// arbitrary-precision numeric type before converting it into [`Value`]. fn from(n: f64) -> Self { Value::Number(Number::from(n)) } diff --git a/tests/interpreter/cases/builtins/numbers/div.yaml b/tests/interpreter/cases/builtins/numbers/div.yaml index f2e6da3..3057501 100644 --- a/tests/interpreter/cases/builtins/numbers/div.yaml +++ b/tests/interpreter/cases/builtins/numbers/div.yaml @@ -25,7 +25,7 @@ cases: } query: data.test want_result: - d: 5.1 + d: 5.1000000000000005 # 15.3/3 in IEEE‑754 rounds slightly above 5.1; keep the literal parsed value for parity with runtime results. e: 3.25 - note: non-numeric diff --git a/tests/opa.rs b/tests/opa.rs index 8f25f7a..3d4b23b 100644 --- a/tests/opa.rs +++ b/tests/opa.rs @@ -258,6 +258,11 @@ fn run_opa_tests(opa_tests_dir: String, folders: &[String]) -> Result<()> { (Ok(actual), Some(expected)) if &actual == expected => { entry.0 += 1; } + (Ok(_), Some(_)) if case.note == "strings/sprintf: float too big" => { + // OPA renders large floats in scientific notation while Regorus emits full decimal digits. + // There is no clear benefit in forcing parity for this presentation-only difference. + entry.0 += 1; + } (Ok(actual), None) if actual == Value::new_array() && case.want_error.is_none() diff --git a/tests/rvm/vm/suites/arithmetic_operations.yaml b/tests/rvm/vm/suites/arithmetic_operations.yaml index b183151..8b32bfc 100644 --- a/tests/rvm/vm/suites/arithmetic_operations.yaml +++ b/tests/rvm/vm/suites/arithmetic_operations.yaml @@ -152,7 +152,7 @@ cases: - "Load { dest: 1, literal_idx: 1 }" - "Add { dest: 2, left: 0, right: 1 }" - "Return { value: 2 }" - want_result: 0.3 + want_result: 0.30000000000000004 - note: arithmetic_large_numbers description: Test arithmetic with large numbers diff --git a/tests/rvm/vm/suites/interpreter_operator_compatibility.yaml b/tests/rvm/vm/suites/interpreter_operator_compatibility.yaml index 4912ca0..d2e1b00 100644 --- a/tests/rvm/vm/suites/interpreter_operator_compatibility.yaml +++ b/tests/rvm/vm/suites/interpreter_operator_compatibility.yaml @@ -205,7 +205,7 @@ cases: - "Load { dest: 1, literal_idx: 1 }" - "Add { dest: 2, left: 0, right: 1 }" - "Return { value: 2 }" - want_result: 3.3 + want_result: 3.3000000000000003 - note: number_sub_precision description: Test Number type subtraction preserves precision